Eine der Hauptproblemquellen von C und C++ sind Speicherzugriffsfehler, die durch Pufferüberläufe, Speicherlecks u. a. ausgelöst werden. Mit einer neuen Technik, der Advanced Dynamic Code Analysis (ADCA), werden Embedded-Entwickler dabei unterstützt, solche Fehler in Zukunft zu vermeiden.
Jedes Jahr wird vom US National Cybersecurity Center of Excellence eine Liste zu den derzeit häufigsten und folgenreichsten Softwareschwachstellen veröffentlicht, die Common Weakness Enumeration Top 25 Most Dangerous Software Weaknesses (CWE Top 25) [1]. Zur Erstellung der Liste nutzt das CWE-Team die »Common Vulnerabilities and Exposures«-Daten (CVE), die in der »National Vulnerability Database« (NVD) des National Institute of Standards and Technology (NIST) zu finden sind, sowie die mit jedem CVE-Datensatz verbundenen CVSS-Bewertungen (Common Vulnerability Scoring System), wobei der Schwerpunkt auf CVE-Datensätzen aus dem Katalog »Known Exploited Vulnerabilities« (KEV) der Cybersecurity and Infrastructure Security Agency (CISA) lag. Auf diese Daten wird eine Formel angewandt, um jede Schwachstelle nach Häufigkeit und Schweregrad zu bewerten.
Der für die Berechnung der Top 25 für das Jahr 2022 analysierte Datensatz enthielt insgesamt 37.899 CVE-Einträge aus den beiden vorangegangenen Kalenderjahren. Im Jahr 2022 sind vier der elf größten Schwachstellen speicherbezogene Fehler. Out-of-Bounds-Fehler stehen an der Spitze der CWE-Top-25-Liste 2022. Es gibt sie in drei Varianten (Bild 1), aber das Grundproblem ist immer dasselbe: Ein Programm schreibt außerhalb eines zugewiesenen Speicherbereichs oder liest Daten daraus.
Daten können an einen falschen Ort im Speicherbereich geschrieben/gelesen werden, wenn ihre Größe den zugewiesenen Speicherbereich überschreitet, was zu einem Pufferüberlauf führt, oder wenn das Programm die Datengröße oder die Adresse falsch berechnet. Das tritt beispielsweise auf, wenn ein Programm versucht, auf einen ungültigen Index in einem Array zuzugreifen, d. h. einen Index, der kleiner als 0 oder gleich oder größer als der Umfang des Arrays ist.
Neben den Out-of-Bounds-Fehlern gibt es verschiedene andere Arten von Speicherzugriffsfehlern, die zwar seltener auftreten, aber in ihren Auswirkungen nicht weniger dramatisch sein können. Dazu gehören:
➔ Undefiniertes Verhalten: Beispiele für undefiniertes Verhalten sind der Überlauf von vorzeichenbehafteten Ganzzahlen, die Dereferenzierung von Null-Pointern, die mehrfache Änderung desselben Skalars in einem Ausdruck ohne Sequenzpunkte und der Zugriff auf ein Objekt über einen Pointer eines anderen Typs.
➔ Speicherlecks: Speicherlecks treten auf, wenn Programmierer Speicher zuweisen und vergessen, den Speicher mit der Funktion delete() oder dem delete[]-Operator wieder freizugeben. Eines der häufigsten Speicherlecks tritt in C++ durch die Verwendung des falschen delete-Operators auf. Der delete()-Operator sollte verwendet werden, um einen einzelnen zugewiesenen Speicherplatz freizugeben, wogegen der delete[-Operator verwendet werden sollte, um ein Array von Datenwerten freizugeben.
➔ Verwendung von Speicher nach dem Freigeben: Diese Art von Fehler tritt auf, wenn auf Speicher verwiesen wird, nachdem er freigegeben wurde. Die Verwendung von zuvor freigegebenem Speicher kann je nach Instanziierung und Zeitpunkt des Fehlers eine Reihe von negativen Folgen haben, die von der Beschädigung gültiger Daten bis zur Ausführung beliebigen Codes reichen.
➔ Uninitialisierte Speicherzugriffe: Tritt auf, wenn die Anwendung aus adressierbarem Speicher liest, der seit seiner Zuweisung noch nicht mit Initialwerten gefüllt wurde. Zu dem Fehler kann es aufgrund einer falschen Initialisierungsreihenfolge oder einer Race-Condition in einer Multithreading-Anwendung kommen.
➔ Stack-Verwendung nach Return: Dieser Fehler tritt auf, wenn auf den Speicher der Stack-Variablen zugegriffen wird, nachdem die deklarierende Funktion zurückgesprungen ist.
Bild 2 zeigt kurze Code-Beispiele für diese Fehlertypen.
Zur Vermeidung von Speicherfehlern in C/C++-Code gibt es bereits eine Reihe von dynamischen Code-Analyse-Tools, von denen die beiden in Bild 3 gezeigten die bekanntesten sind: Valgrind und AddressSanitizer oder kurz ASan. Beide sind für verschiedene CPU-Architekturen verfügbar und instrumentieren den Code auf unterschiedliche Weise. Die damit verbundene Verlangsamung und der erhöhte Speicherbedarf verbietet aber die Verwendung beider Tools für Embedded-Echtzeitanwendungen.
Valgrind ist im Wesentlichen eine virtuelle Maschine, die Just-in-Time-Kompilierungstechniken verwendet, einschließlich dynamischer Neukompilierung [2]. Nichts aus dem ursprünglichen Programm wird jemals direkt auf dem Host-Prozessor ausgeführt. Stattdessen übersetzt Valgrind das Programm zunächst in eine temporäre, einfachere Form, die sogenannte Zwischenrepräsentation (IR), die eine prozessorneutrale, statische, auf einer einzigen Zuweisung basierende Form ist. Nach der Konvertierung kann ein Tool – es gibt mehrere Tools, die in Valgrind enthalten sind; das Standardtool und am häufigsten verwendete ist Memcheck – beliebige Transformationen an der IR vornehmen, bevor Valgrind die IR wieder in Maschinencode übersetzt und den Host-Prozessor sie ausführen lässt. Valgrind kompiliert den Binärcode neu, damit er auf Host- und Ziel-CPUs – oder simulierten CPUs – derselben Architektur läuft.
Memcheck fügt um fast alle Anweisungen herum einen zusätzlichen Instrumentierungscode ein, der die Gültigkeit und die Adressierbarkeit in den so genannten V-Bits bzw. A-Bits überwacht. Wenn Daten verschoben oder manipuliert werden, behält der Instrumentierungscode die A- und V-Bits im Auge, sodass sie auf Ein-Bit-Ebene immer korrekt sind. Darüber hinaus ersetzt Memcheck die Standard-C-Speicherzuweisung durch seine eigene Implementierung, die auch Speicherschutzvorrichtungen um alle zugewiesenen Blöcke herum mit den auf »ungültig« gesetzten A-Bits enthält. Diese Funktion ermöglicht es Memcheck, Fehler zu erkennen, bei denen ein Programm einen zugewiesenen Block um eine kleine Abweichung verlässt. Zu den Problemen, die Memcheck erkennen und vor denen es warnen kann, gehören:
➔ Verwendung von nicht initialisiertem Speicher.
➔ Lesen/Schreiben von Speicher, nachdem er freigegeben wurde.
➔ Lesen/Schreiben hinter dem Ende von Malloc‘d-Blöcken.
➔ Lecks im Speicher.
Der Preis dafür ist allerdings hoch. Programme, die unter Memcheck laufen, sind in der Regel 20–30-mal langsamer als Programme, die außerhalb von Valgrind laufen, und benötigen wesentlich mehr Speicher.
AddressSanitizer (ASan) ist ein Open-Source-Programmiertool, das von Google-Sicherheitsforschern entwickelt wurde, um Probleme beim Speicherzugriff in C- und C++-Programmen zu erkennen [3]. Es erkennt Speicherfehler wie Pufferüberläufe oder Zugriffe auf einen »Dangling Pointer« (use-after-free). AddressSanitizer basiert auf Compiler-Instrumentierung und direkt abgebildetem Schattenspeicher. Um die Speicherzuweisung zu instrumentieren und undichte Stellen zu identifizieren, werden die Funktionen der Malloc- und Free-Familie ersetzt, sodass jede Speicherzuweisung/-freigabe vom Tool überwacht wird. Alle Speicher, auf die nicht zugegriffen werden sollte, werden für ungültig erklärt. Dazu gehören Speicher um zugewiesene Bereiche, freigegebener Speicher und Speicher um Variablen auf dem Stack. Dann wird jeder lesende oder schreibende Speicherzugriff zu einem Code kompiliert, der prüft, ob diese Speicheradresse gültig ist oder nicht. Ist sie nicht zulässig, wird ein Fehler gemeldet.
Grundsätzlich ist der virtuelle Adressraum einer Anwendung unterteilt in den Hauptanwendungsspeicher, der vom Anwendungscode verwendet wird, und einen Schattenspeicher, der Metadaten über nicht adressierbaren Speicher speichert. AddressSanitizer bildet jeweils 8 Bytes des Anwendungsspeichers auf 1 Byte des Schattenspeichers ab. Wenn eine Speicheradresse adressierbar ist, ist das Bit im Schattenspeicher 0. Wenn eine Speicheradresse nicht adressierbar ist, ist das Bit im Schattenspeicher 1. Auf diese Weise kann AddressSanitizer erkennen, welcher Speicherzugriff erlaubt ist und welcher nicht, sowie Fehler melden.
Wie bei Valgrind müssen Softwareentwickler für ASan einen hohen Preis in Form von Geschwindigkeitsverlust und Speicherbedarf zahlen. Programme, die unter ASan laufen, sind in der Regel 2-mal langsamer als Programme, die außerhalb von ASan laufen, und sie benötigen im Durchschnitt 240 % mehr Speicher.
Wenn die vorhandenen Instrumente für Entwickler von Embedded-Software nicht brauchbar sind, stellt sich sofort die Frage, was tun. Bild 4 zeigt die Lauterbach-Trace-Pyramide mit der neuen ADCA-Technik an der Spitze. ADCA basiert auf Lauterbachs Context Tracking System (CTS), das wiederum auf einem Standard-Echtzeit-Flow-Trace basiert.
Immer höhere Integrationsdichten und Preisdruck haben dazu geführt, dass viele Prozessoren CPU-Kern, Cache, Peripherie, Flash- und RAM-Speicher in nur einem Gehäuse integriert haben (System-on-Chip, SoC), die in vielen Fällen nicht einmal mehr eine externe Speicherschnittstelle haben.
Sie verfügen daher zusätzlich zur Debug-Schnittstelle über eine spezielle Trace-Schnittstelle auf dem Chip. Diese macht den Programmablauf in komprimierter Form nach außen hin sichtbar (Bild 5). Dies wird als Flow-Trace-Verfahren bezeichnet.
Als Trace-Schnittstelle wird ein Trace-Bus verwendet, über den Programmablaufdaten und/oder Datenzugriffe in komprimierter Form übertragen werden. Die Informationen des Adress-/Datenbusses werden so übertragen, wie sie direkt am CPU-Kern anfallen. Damit können auch Zugriffe auf chipinternen Flash- oder RAM-Speicher – insbesondere auf den Cache – aufgezeichnet werden.
Nur wenige Trace-Schnittstellen unterstützen das gezielte Ein- und Ausschalten oder die Definition von Adressfenstern, innerhalb derer Trace-Daten erzeugt werden. Die einzige Lösung ist ein Trace-Tool mit einem großen Trace-Speicher, in dem alle Trace-Daten ungefiltert aufgezeichnet werden. Bei Aufzeichnungszeiten von mehreren Sekunden ist es sehr wahrscheinlich, dass der gesuchte Fehler in der Aufzeichnung gefunden werden kann. Darüber hinaus stehen mehrere Programmläufe für Laufzeitstatistiken und/oder Code-Coverage-Analysen zur Verfügung. Für das Flow-Trace-Verfahren werden CPU-spezifische Trace-Adapter und Auswertealgorithmen verwendet. Die mechanischen und elektrischen Details der Trace-Schnittstelle sind in der Regel vom CPU-Hersteller vordefiniert und werden somit von den Trace-Tool-Herstellern unterstützt. Der Anwender erhält ein Plug-and-Play-Tool, das fast keine oder nur einfache Einstellungen erfordert. Wenn diese Schnittstelle von der CPU angeboten wird, ist sie die mit Abstand komfortabelste Trace-Option.
Einziger Nachteil dieser Trace-Technik ist das Bandbreitenproblem, d. h. wenn intern im SoC mehr Trace-Daten erzeugt werden, als über die Trace-Schnittstelle übertragen werden können. Diesem Problem begegnen die Halbleiterhersteller mit FIFO-Puffern und Datenreduktion.
Wenn ein Softwareentwickler sich ausschließlich auf Flow Trace verlässt, verbringt er manchmal viel Zeit mit der Analyse des Trace-Listings, um herauszufinden, welche Anweisungen, Daten oder Systemzustände zu Fehlfunktionen des Zielsystems geführt haben.
Lauterbachs Trace-basiertes Debugging (CTS) ermöglicht es dem Anwender, den Zustand des Zielsystems an einem ausgewählten Punkt auf Basis der im Trace-Speicher abgetasteten Informationen wiederherzustellen (Bild 6). Von diesem Ausgangspunkt aus können die zuvor in Echtzeit im Trace-Speicher aufgezeichneten Programmschritte in der TRACE32 PowerView GUI erneut auf Fehler untersucht werden. Voraussetzung für ein vollwertiges Trace-basiertes Debugging ist, dass der komplette Programm- und Datenfluss bis zum Stopp der Programmausführung im Trace-Buffer vorhanden ist.
Nach dem Einschalten des Kontextverfolgungssystems kann ein Aufzeichnungspunkt im Trace ausgewählt werden, für den der Zielsystemzustand im Befehlssatzsimulator neu erzeugt werden soll. Der Programmzähler wird dann im Source-Listing automatisch auf den Punkt gesetzt, an dem er sich zum Zeitpunkt der Aufzeichnung befand.
Alle Debug-Befehle wie Single Step, Step Over Call, Go Return etc. können nun verwendet werden. Die CPU-Anweisungen werden so abgearbeitet, wie sie im Trace-Speicher aufgezeichnet wurden. Die Debug-Funktion wurde weiter ausgebaut, sodass es auch möglich ist, rückwärtszugehen.
Da der Befehlssatzsimulator es ermöglicht, die einzelnen Befehle erneut auszuführen, ist es auch wünschenswert, die Änderungen von Variablen, Speichern und Registern verfolgen zu können.
Es ist sogar möglich, die meisten Trace-Lücken zu schließen, die durch die begrenzte Bandbreite des Trace-Ports entstehen. Wenn nur Lesezyklen abgetastet werden, um eine Überlastung des Trace-Ports zu vermeiden, kann CTS alle Schreibzyklen rekonstruieren.
Darüber hinaus ermöglicht TRACE32 eine Cache-Analyse mittels CTS, d. h. auf Basis der in einer Trace-Aufzeichnung erfassten Programmausführung. Ist die MMU-Architektur eingestellt, berücksichtigt die Cache-Analyse alle Manipulationen an den Cache-Kontrollregistern für die Cache-Analyse: Cache-Flushes, Ein- und Ausschalten der Caches und Cache-Locks.
Zusammenfassend lässt sich sagen, dass Lauterbachs CTS das Debugging erheblich vereinfachen kann, insbesondere für Echtzeitanwendungen, für die das Debugging im Stop-Modus allein oft nicht ausreicht.
ADCA ist ein weitergehender CTS-Modus zur Untersuchung und Behebung von Speicherzugriffsfehlern, die durch verschiedene Arten von Fehlern ausgelöst werden. Es wird nach dem Startcode ausgeführt und erfasst die anfänglichen Speicher- und Registerzustände, wenn Stack und Daten gültig sind.
Die erweiterte dynamische Code-Analyse (ADCA) benötigt den kompletten Programm-Trace-Flow und genügend Daten, um alle Zeiger sowie das komplette Trace aus dem Start-up-Code zu rekonstruieren – das Tracen nur eines kleinen Teils im FIFO-Modus ist nicht ausreichend.
Das Tool ist auf korrekte und vollständige Debug-Informationen des Compilers angewiesen und muss vom Compiler generierte und optimierte Konstrukte verstehen. Außerdem muss es speziellen Code wie Interrupts und RTOS-Task-Wechsel verstehen.
Die Kernfunktion ist eine Zuordnung von statischen und dynamischen Tags zu allen Daten und Speicheradressen. Tags sind unsichtbare Metainformationen, die vom Debugger mitgezogen werden. Speicherverletzungen werden nach dem Schlüssel-Schloss-Prinzip erkannt: Ein Datum mit einem bestimmten Tag darf nur auf eine Speicheradresse zugreifen, die mit demselben Tag verbunden ist (Bild 7).
Nach der Ausführung des Programms und der Erfassung der Trace-Daten kann die ADCA-Verarbeitung starten und die identifizierten Fehler lassen sich in verschiedenen Ansichten der Software TRACE32 PowerView anzeigen (Bild 8).
Der ADCA-Bericht zeigt eine Zusammenfassung aller möglichen Speicherzugriffsfehler (Bild 9). Ein konkretes Beispiel aus dem Bericht wird im Folgenden diskutiert: Ein Fehler beim Zugriff auf ein Array namens »check_array1« zusammen mit der Speicheradresse der Zugriffsverletzung (0x418f5c).
Diese Informationen lassen sich nutzen, um den Code zu analysieren und den/die Fehler zu beheben, indem zusätzliche Informationen verarbeitet werden, die in der TRACE32 PowerView GUI angezeigt werden.
Die erste aller möglichen Ansichten von TRACE32 PowerView aus den in Bild 8 gezeigten Möglichkeiten soll die Speicheranzeige (Bild 10) sein. Im linken Teil der Anzeige ist ein Tagwechsel an der Adresse 418F5C zu beobachten. Die letzte gültige Speicheradresse, die dem Array »check_array1« (Tag 28A) zugeordnet ist, ist also 418F5B. Im mittleren Teil der Speicheranzeige ist zu erkennen, dass der höchste gültige Index für »check_array1« – verbunden mit den Speicheradressen 418F58 bis 418F5B und Tag 28A – »9« ist. Diese Informationen sind wertvoll für die Fehlererkennung, aber sie reichen natürlich noch nicht aus, um den Fehler zu beheben.
Die nächste Anzeige aus Bild 8, die untersucht werden muss, ist die Registeranzeige (Bild 11). Offensichtlich ist das Register R1 mit dem Tag 28A verbunden, der zum Array »check_array1« gehört, wie es in Bild 10 gezeigt wurde. Leider zeigt R1 auf die Speicheradresse 418F5C, die sich außerhalb des gültigen Adressraums für »check_array1« befindet – ungültig, weil 418F5C mit 28B und nicht mit 28A gekennzeichnet ist (Bild 10).
Zu diesem Zeitpunkt ist der Fehler bereits so gut wie entdeckt. Es fehlt nur noch das Quellfenster aus Bild 8, das in Bild 12 dargestellt ist. Mit den Informationen aus Bild 11 lässt sich leicht feststellen, dass das Register R1 die ungültige Speicheradresse für einen Schreibzugriff auf »check_array1[i]« enthält, die von Lauterbachs ADCA zu Beginn untersucht wurde.
Mit den Informationen aus Bild 10 ist der Grund für diesen ungültigen Zugriff offensichtlich: Die Indexvariable »i« wird von 0 auf 10 erhöht, was die Speicherzugriffsverletzung beim letzten Schleifendurchlauf »check_array1[10]« verursacht. Um den Fehler zu beheben, ist nur der Code von »i<11« in »i<10« zu ändern.
Auch wenn es heute bereits einige Code-Analyse-Tools gibt, sind diese für typische Echtzeit-Embedded-Softwareanwendungen nicht geeignet. Im Gegensatz dazu erfüllt die Advanced Dynamic Code Analysis von Lauterbach die Bedürfnisse von Embedded-Entwicklern hinsichtlich Echtzeitanforderungen und Erkennung der relevantesten potenziellen Speicherzugriffsfehler, die in C/C++ auftreten.
ADCA ist ein großer Schritt nach vorn bei der Identifizierung dieser schwer zu findenden und reproduzierbaren Fehler und unterstützt sogar Multicore-Architekturen. Dennoch gibt es einige Einschränkungen, über die ein Entwickler sich auch im Klaren sein muss. Bestimmte Konstrukte und Fehlertypen können mit ADCA nicht erkannt werden. Diese Art von Fehlern wird jedoch in der Regel vom Compiler erkannt. Spezielle Code-Konstrukte erfordern möglicherweise zusätzliche Einstellungen, um Informationen für die Analyse bereitzustellen.
Die ADCA-Technik wird in einer der nächsten Versionen von TRACE32 PowerView als Softwareupdate ohne zusätzliche Kosten für Lauterbach-Kunden verfügbar sein. Sie wird nicht als einzelnes Produkt verkauft, das als Add-on für Trace-Tools anderer Hersteller verwendet werden könnte.
Literatur
[1] 2022 CWE Top 25 Most Dangerous Software Weaknesses. Common Weakness Enumeration, Website, https://cwe.mitre.org/top25/archive/ 2022/2022_cwe_top25.html, abgerufen am 12. Februar 2023.
[2] Valgrind. Valgrind Developers, Website, https://valgrind.org, abgerufen am 12. Februar 2023.
[3] AddressSanitizer. The Clang Team, Website, https://clang.llvm.org/docs/AddressSanitizer.html, abgerufen am 12. Februar 2023.
[I] Lynch, J.: The Worst Computer Bugs in History: The Ariane 5 Disaster. SmartBear Software, 7. September 2017, www.bugsnag.com/blog/ bug-day-ariane-5-disaster, abgerufen am 12. Februar 2023.
[II] Statsenko, A.: Killer Bug. Therac-25: Quick-and-Dirty. PVS-Studio, 10. Oktober 2016, https://pvs-studio.com/en/blog/posts/0438/, abgerufen am 12. Februar 2023.
[III] Thompson, C.: New details about the fatal Tesla Autopilot crash reveal the driver‘s last minutes. Insider, 20. Juni 2017, www.businessinsider.com/details-about-the-fatal-tesla-autopilot-accident-released-2017-6, abgerufen am 12. Februar 2023.
Der Autor
Stephan Lauterbach ist Mitbegründer, Geschäftsführer und Entwicklungsleiter von Lauterbach, einem Hersteller von vollständigen, modularen und erweiterbaren Mikroprozessor- Entwicklungstools. Seit der Gründung im Jahr 1979 ist das Unternehmen stetig gewachsen und beschäftigt heute mehr als 140 Mitarbeiter, die weltweit von elf Standorten aus Kunden betreuen. Der Hauptsitz des Unternehmens, die Produktentwicklung und die Fertigung befinden sich in Höhenkirchen-Siegertsbrunn bei München.
Die gefährlichsten Softwareschwachstellen |
---|
Softwarefehler können kostspielig und verheerend sein. Im Jahr 1996 explodierte die Rakete Ariane 5 der Europäischen Weltraumorganisation (ESA) nur 39 s nach dem Start, was zu einem Schaden von mehr als 370 Mio. US-Dollar führte [I]. Die Ursache war eine interne Software-Exception des Trägheitsnavigationssystems (SRI, Sagnac Ring Interferometer), die während der Ausführung einer Datenkonvertierung von einer 64-bit-Gleitkommazahl in einen 16-bit-Wert mit Vorzeichen verursacht wurde. Der Wert der Gleitkommazahl war größer als der, der durch eine 16-bit-Ganzzahl mit Vorzeichen dargestellt werden konnte. Das Ergebnis war ein Operandenfehler. Die Datenkonvertierungsroutinen im Ada-Code waren nicht vor Operandenfehlern geschützt, obwohl andere Konvertierungen vergleichbarer Variablen an der gleichen Stelle im Code geschützt waren. Der Fehler trat in einem Teil der Software auf, der die Ausrichtung der Trägheitsplattform steuerte. Aufgrund eines unerwartet hohen Werts einer internen Funktion, die BH (Bias Horizontal) genannt wird und mit der von der Plattform erfassten horizontalen Geschwindigkeit zusammenhängt, kam es zu diesem Operandenfehler. Dieser Wert wird als Indikator für die Ausrichtungsgenauigkeit über die Zeit berechnet. Obwohl die Konstruktion des in der Ariane 5 verwendeten SRI fast identisch mit dem der Ariane 4 war, fiel der Wert von BH viel höher aus als erwartet, da der frühe Teil der Flugbahn von Ariane 5 sich von dem der Ariane 4 unterschied und zu wesentlich höheren Werten der horizontalen Geschwindigkeit führte. Aber nicht nur in solchen extremen Beispielen, sondern auch in alltäglichen Embedded-Anwendungen können solche Bugs eine große Gefahrenquelle darstellen. Im medizinischen Bereich verursachte ein Therac-25-Strahlentherapiegerät für Krebspatienten zwischen 1985 und 1987 aufgrund einer Wettlaufsituation um den Zugriff auf Ressourcen (Race- Condition) eine massive Strahlungsüberdosis, die zum Tod von drei Patienten führte und bei mindestens drei weiteren Patienten schwere Verletzungen verursachte [II]. Der Bediener hatte die Absicht, den Strahl mit niedriger Leistung zu verwenden, obwohl in Wirklichkeit der Strahl mit hoher Leistung ohne die Spreizmagnete eingesetzt wurde, wodurch eine viel höhere Dosis als erwartet abgegeben wurde. Dies war auf eine Race-Condition in der Codebasis zurückzuführen, die bereits im Vorgängermodell Therac-20 vorhanden war, aber durch Hardware-Sicherheitsmechanismen verhindert wurde. Die Software bestand aus mehreren Routinen, die gleichzeitig liefen. Sowohl die Dateneingabe- als auch die Tastaturhandhabungsroutine teilten sich eine einzige Variable, die festhielt, ob der Techniker die Eingabe von Befehlen abgeschlossen hatte. Sobald die Dateneingabephase als abgeschlossen markiert wurde, begann die Magneteinstellungsphase. Wurde jedoch in der Dateneingabe- phase während der acht Sekunden andauernden Magneteinstellungsphase eine bestimmte Abfolge von Bearbeitungen vorgenommen, wurde die Einstellung aufgrund des Werts der Variable am Ende nicht auf die Maschinensteuerung (Hardware) übertragen. In der Automobilbranche gab es mehrere Todesopfer aufgrund von Fehlern in autonom fahrenden Autos. Im Jahr 2016 beispielsweise erkannten die Sensoren eines Autos vor einem strahlenden Frühlingshimmel einen großen, weißen 18-Rad-Lkw mit Anhänger nicht, der die Autobahn querte. Das Auto fuhr mit voller Geschwindigkeit unter den Anhänger [III]. Um solche Fehler in Zukunft zu vermeiden, bieten automatische Code-Analyse-Tools dem Entwickler ein wertvolles Werkzeug, um Fehler in der immer komplexer werdenden Software zu erkennen. |