Sichere Software ist Pflicht Funktionale Sicherheit von Embedded-Systemen

Mit zunehmender Digitalisierung in Embedded Systemen  nimmt die Software eine wichtige Stellung ein.
Mit zunehmender Digitalisierung in Embedded Systemen nimmt die Software eine wichtige Stellung ein.

Mit zunehmender Digitalisierung vieler Fahrzeugkomponenten wird die Software bei Embedded-Systemen immer wichtiger. Nicht zuletzt, was die funktionale Sicherheit fast aller Bordsysteme betrifft. Die einschlägigen Normen fordern dabei geeignete Maßnahmen zur Fehlervermeidung in der Entwicklung.

An Software ist heute im Fahrzeug kein Mangel. Im Premium-Segment finden sich mittlerweile zum Teil über 100 verschiedene vernetzte Systeme, die zusammen über 100 Millionen Code-Zeilen umfassen. Relativ neu ist zudem, dass diese Systeme über unterschiedliche Technologien untereinander und mit der Außenwelt kommunizieren. Der inzwischen gängige Begriff des »Smartphones auf Rädern« trifft die Situation recht gut. Damit entstehen sehr grundsätzliche Risiken. Denn mit der Dominanz der Software gehen auch Programmierfehler und Schwachstellen einher, die zu unplausiblen Zuständen der Gesamtsysteme führen können. Und eine komplexe, fehlerfreie Software wird es zumindest in absehbarer Zeit nicht geben. Verschiedene Studien zeigen, dass kommerzielle Software im Durchschnitt einen Fehler pro 1000 Code-Zeilen aufweist.

Open-Source-Software ist durch die Kontrolle der Communities und die Offenheit etwas besser, hier geht man von 0,68 Fehlern pro 1000 Zeilen aus. Im Idealfall, der sogenannten Reinraum-Entwicklung, bleiben noch durchschnittlich 0,1 Fehler auf 1000 Zeilen Code. Würde man im Automotive-Bereich nach den aufwendigen Reinraum-Kriterien entwickeln, blieben also im besten Fall bei 100 Millionen Code-Zeilen noch 10.000 Fehler in den Bordsystemen unentdeckt – und damit 10.000 potenzielle Störungen und Angriffsziele. Im Hinblick auf die sicherheitsrelevanten Systeme sollten diese Zahlen alarmieren.

Die Anforderungen, die an Steuerungen und Software in mechatronischen Systemen generell zu stellen sind, lassen sich auf drei zentrale Aufgaben herunterbrechen:

  • Das System muss bei allen Systemzuständen gesteuert werden.
  • Bei Fehlfunktionen muss das System in einen sicheren Zustand gebracht werden.
  • Fehler müssen dem Benutzer gemeldet werden.

Fehler in der Software bedeuten, dass die Erfüllung dieser Anforderungen nicht sicher gewährleistet ist. Weil es – zumindest ab einer gewissen Komplexität – keine fehlerfreie Software geben kann, ist der sichere Betrieb eines Systems nicht mit absoluter Zuverlässigkeit möglich.

Testing alleine reicht nicht aus

Die Norm IEC 61508 zur funktionalen Sicherheit für elektrische, elektronische und programmierbare elektronische Systeme (E/E/PE-Systeme) und die daraus abgeleitete ISO 26262 legen deswegen völlig zurecht großen Wert auf die Qualitätssicherung über den gesamten Lebenszyklus der Systeme hinweg. Allerdings kann moderne, komplexe Software unüberschaubar viele verschiedene Zustände einnehmen. Es ist in der Regel nicht möglich, alle denkbaren Zustände durch das Testing abzudecken.

Das kann verschiedene Gründe haben: Die entsprechenden Programmteile werden beim Testing nicht durchlaufen, der Fehler tritt bei den definierten Testfällen nicht auf oder er sorgt im Testszenario nicht für eine Abweichung vom erwarteten Ergebnis. Bei einem Entertainment-System kann das verschmerzbar sein – auch wenn es bereits Fälle gab, in denen die Steuerung des Fahrzeugs über eben dieses System angegriffen wurde. Bei sicherheitsrelevanten Systemen jedoch müssen weitere Maßnahmen zur Qualitätssicherung hinweg greifen.

Nicht umsonst gehen die Normen davon aus, dass es nicht möglich ist, Produkte aus der Serienfertigung mit einem wirtschaftlich vertretbaren Aufwand über die gesamte Betriebsdauer hinweg fehlerfrei zu halten. IEC 61508 und ISO 26262 heben vielmehr darauf ab, wie sich Fehler soweit möglich vermeiden oder aufspüren lassen, und wie die fast zwangsläufig auftretenden Fehler beherrscht werden können. Dazu definieren die Normen Methoden, Maßnahmen und Prozesse, die mit zunehmendem Gefährdungsgrad strenger und komplexer werden. Im Fall der Software-Entwicklung betrifft dies vor allem die Qualitätssicherung innerhalb des Software Development Lifecycles (SDLC) und regelmäßige Reviews während der Nutzungsdauer der Software.

Das grundsätzliche Problem dabei: Für sicherheitsrelevante Systeme ist die im Embedded-Bereich meist verbreitete Programmiersprache C eigentlich nicht die erste Wahl. Als C in den 1970er-Jahren erstmals definiert wurde, lag der Fokus auf Geschwindigkeit und Flexibilität. Das Internet gab es nicht, Cyber-Sicherheit war kein Thema. Mit C++ wurden die beiden größten daraus resultierenden Probleme zwar entschärft, aber nicht aus der Welt geschafft: Type-Sicherheit und ungeprüfte Pointer-Arithmetik. Besonders die Pointer-Arithmetik ist für viele Sicherheitsprobleme verantwortlich, die Wurzel der weit verbreiteten Buffer Overruns.

C-Standard ist liberal

Dazu kommt, dass die Definition, was ein korrektes C-Programm ist, der Flexibilität zuliebe äußerst liberal ausfällt. Es gibt zahlreiche Ambiguitäten, die vom Compiler interpretiert und aufgelöst werden müssen. Viele Entwickler sind zudem überrascht, wenn sie erfahren, dass undefiniertes Verhalten eines C-Programms nicht gegen den Standard verstößt. Überspitzt gesagt: Es ist völlig in Ordnung, wenn das Programm irgendwas tut. C99 etwa nennt 191 Varianten von undefiniertem Verhalten. In der Regel versucht der Compiler in diesem Fall, das wahrscheinlich Sinnvollste zu tun. Sofern der Compiler das Problem erkennt. Auch nichtspezifiziertes Verhalten eines C-Programms ist aus Sicht des Standards okay. In diesem Fall bietet der Standard eine Auswahl an Möglichkeiten, die Situation aufzulösen. Es liegt am Compiler, welche Lösung genutzt wird. Aus Sicht der funktionalen Sicherheit ist das nicht ideal – Programmierfehler können extrem leicht gemacht und oft nur schwer erkannt werden

Das Problem wird in der Automotive-Branche seit geraumer Zeit durch die Standards MISRA C und MISRA C++ adressiert. Definiert wurde diese Sammlung von Regeln und Richtlinien Ende der 90er-Jahre von der britischen Motor Industry Software Reliability Association. Das Ziel dabei: Sicherheit, Zuverlässigkeit und die Portierbarkeit von in C/C++ geschriebenen Programmen zu verbessern.

Die aktuelle Version MISRA C:2012 umfasst 143 Regeln, die statisch überprüft werden können, und die im Standard zulässige aber in der Praxis kritische Programmierpraktiken verbieten. Zudem werden 16 Direktiven genannt,
die sich mit dem Entwicklungsprozess und den Entwicklungsrichtlinien befassen. Durch die Überprüfbarkeit der Regeln nimmt die statische Code-Analyse (Bild 1) eine wichtige Rolle bei MISRA C ein.

Automatisiert Fehler finden

Bei der statischen Code-Analyse wird die Software nicht wie beim Testing dynamisch durchlaufen. Die Analyse basiert auf einem Modell, das vom Analyse-Tool anhand des Source-Codes erzeugt wird. Die statische Analyse untersucht das Modell mithilfe sogenannter Checker auf Fehler, Schwachstellen und Abweichungen von definierten Programmierstandards. Buffer Overruns, Null-Pointer-Dereferenzierungen oder Zugriffe auf potenziell unsichere Datenquellen können so erkannt und frühzeitig behoben werden. Dazu muss kein lauffähiger Code vorliegen. Zudem lässt sich das Verfahren automatisieren und so direkt in die Werkzeugkette und das Build-System integrieren.
Das Vorgehen ähnelt der Arbeitsweise eines Compilers: Aus den geparsten Quellen erzeugt das Analysewerkzeug die Intermediate Representation (IR) (Bild 2). Während ein Compiler aus der IR den Objektcode erzeugen würde, nutzt die statische Analyse die IR, um alle Daten- und Steuerungsflüsse durch das Programm zu untersuchen. Damit findet die statische Analyse auch Pro¬bleme, die im dynamischen Testing eventuell unentdeckt bleiben, weil sie den Ausgang der Testfälle nicht beeinflussen.

Eine Besonderheit bei der statischen Analyse stellt das Tool CodeSonar von GrammaTech dar: CodeSonar kann auch Dateien analysieren, die nur binär vorliegen und deswegen nicht einfach in eine IR überführt werden können. Das betrifft vor allem Code von Zulieferern, der zwar immer mehr auch in der Embedded-Entwicklung eingesetzt wird, aber nur extrem schwer auf Bugs oder andere Qualitätsprobleme hin untersucht werden kann.

MISRA C markiert die Regeln nach zwei Kriterien: Statische Überprüfbarkeit und Zuverlässigkeit, mit der Abweichungen erkannt werden können. Kann eine Regelverletzung gefunden werden, indem nur die betreffende Compilation Unit analysiert wird, fällt diese Regel unter die Kategorie »Single Translation Unit«. Müssen alle Compilation Units betrachtet werden, die zu einem Programm gehören, wird die Regel der Kategorie »System« zugerechnet. Nicht alle MISRA-C-Regeln sind ohne entsprechenden Aufwand statisch überprüfbar, viele benötigen ein ausgereiftes Analysewerkzeug.

Einfachere Regeln jedoch können heute bereits mit den integrierten Analysefähigkeiten vieler Compiler überwacht werden. So etwa die Regel 15.1: Diese besagt, dass der Befehl goto nicht genutzt werden soll. Ob der Befehl im Code auftritt, kann über eine einfache Syntaxprüfung ermittelt werden. Dazu muss das Analyse-Tool nur die Syntax der Compilation Unit parsen. Etwas komplexer ist zum Beispiel die Regel 5.2. Diese besagt, dass Bezeichner, die innerhalb des gleichen Geltungsbereichs und Namensraums deklariert wurden, eindeutig sein müssen. Auch das ist eine relativ einfache syntaktische Regel, jedoch muss ein Analysewerkzeug zur Überprüfung über die Symboltabelle der Compilation Unit verfügen, um die Bezeichner und deren Geltungsbereich zu ermitteln.