Aikido

Die 10 Codierungsregeln der Nasa für sicherheitskritischen Code

Einführung

Sicherheitskritische Software, wie sie in Raumfahrzeugen oder Automobilsystemen verwendet wird, erfordert extrem zuverlässigen Code. Zu diesem Zweck hat das Jet Propulsion Laboratory der NASA im Jahr 2006 die
‍"Power of 10" -Codierregeln entwickelt. Diese prägnanten Richtlinien entfernen komplexe C-Konstrukte, die schwer zu analysieren sind, und sorgen dafür, dass der Code einfach, überprüfbar und zuverlässig bleibt.

Heutzutage können Tools wie Aikido Code Quality mit benutzerdefinierten Prüfungen eingerichtet werden, um alle zehn Regeln bei jeder neuen Pull-Anfrage durchzusetzen. In diesem Artikel erklären wir jede Regel, warum sie wichtig ist, und geben Codebeispiele, die sowohl falsche als auch richtige Ansätze zeigen.

Warum diese Regeln wichtig sind

Die Regeln der NASA konzentrieren sich auf Lesbarkeit, Analysierbarkeit und Zuverlässigkeit, die für missionskritische Anwendungen wie Raumschiffsteuerungs- und Flugsoftware unerlässlich sind. Indem sie obskure C-Konstrukte verbieten und defensive Prüfungen erzwingen, erleichtern die Richtlinien die Überprüfung von Code und den Nachweis seiner Korrektheit. Sie ergänzen Standards wie MISRA C, indem sie sich mit Mustern befassen, die statische Analysatoren oft übersehen. Durch die Vermeidung von Rekursionen und dynamischem Speicher wird beispielsweise die Ressourcennutzung vorhersehbar, während die Durchsetzung von Rückgabewertprüfungen dazu beiträgt, dass viele Fehler bereits bei der Kompilierung erkannt werden.

Bei der Untersuchung eines eingebetteten Massensystems wie der elektronischen Drosselklappen-Firmware von Toyota durch die NASA wurden sogar Hunderte von Regelverstößen festgestellt. Dies zeigt, dass bei realen Projekten oft die gleichen Probleme auftreten, die diese Regeln verhindern sollen. Jede Regel in der Liste verhindert eine Klasse von häufigen Fehlern (unkontrollierte Schleifen, Null-Zeiger-Dereferenzen, unsichtbare Seiteneffekte usw.). Das Ignorieren dieser Regeln kann zu subtilen Laufzeitfehlern, Sicherheitslücken oder nicht-deterministischem Verhalten führen. Werden dagegen alle zehn Regeln beachtet, wird die statische Verifikation wesentlich einfacher.

Automatisierte Werkzeuge sind wichtig. Codequalitätsplattformen können so konfiguriert werden, dass sie verbotene Konstrukte oder Muster erkennen. Diese Regeln werden automatisch bei jeder Pull-Anfrage ausgeführt, um Probleme zu erkennen, bevor der Code zusammengeführt wird.

Verknüpfung von Kontext und Regeln

Bevor man sich mit den einzelnen Vorschriften befasst, ist es wichtig, den Kontext zu verstehen:

  • Zielsprache: Die "Power of 10"-Regeln der NASA wurden für C geschrieben, eine Sprache mit umfassender Tool-Unterstützung (Compiler, Analysatoren, Debugger), die aber auch für undefiniertes Verhalten berüchtigt ist. Sie setzen keine Garbage Collection oder fortgeschrittene Speicherverwaltung voraus. Wenn man nur einfaches, gut strukturiertes C verwendet, kann man die statische Analyse nutzen, um Programmeigenschaften zu beweisen.
  • Statische Analyse: Es gibt viele Regeln, die automatische Überprüfungen erleichtern. Durch das Verbot von Rekursionen (Regel 1) und die Forderung nach Schleifenbegrenzungen (Regel 2) können Werkzeuge beispielsweise nachweisen, wie viele Iterationen oder Stacknutzung eine Funktion haben kann. Auch das Verbot komplexer Makros und die Begrenzung von Zeigern (Regeln 8-9) machen Codemuster explizit, anstatt sie in Präprozessormagie oder mehrfachen Umleitungen zu verstecken.
  • Entwicklungs-Workflow: In modernen DevSecOps-Pipelines werden diese Regeln Teil der CI-Prüfungen. Code-Qualitäts-Tools können mit GitHub, GitLab oder Bitbucket integriert werden, um jede Pull-Anfrage zu überprüfen und sowohl einfache Probleme als auch komplexere Muster zu erkennen. Sie können für jede NASA-Richtlinie eine benutzerdefinierte Regel erstellen, z. B. "Kennzeichnen Sie jede Verwendung von goto oder rekursiven Funktionsaufrufen" oder "Stellen Sie sicher, dass jede Schleife eine literale Grenze hat". Sobald diese Regeln konfiguriert sind, werden sie bei jedem zukünftigen Code-Scan automatisch angewendet, um Verstöße frühzeitig zu erkennen und Hinweise zur Behebung zu geben.

Kurz gesagt, die 10 Regeln der NASA verkörpern eine defensive, analysierbare C-Programmierung. Im Folgenden listen wir jede Regel auf, zeigen, wie guter und schlechter Code aussieht, und erklären, warum die Regel existiert und welche Risiken sie mindert.‍

Die 10 NASA-Kodierungsregeln

1. Vermeiden Sie einen komplexen Kontrollfluss.

Verwenden Sie kein goto, setjmp oder longjmp und vermeiden Sie das Schreiben rekursiver Funktionen in jedem Teil des Codes.

‍❌ Nicht konformes Beispiel

// Non-compliant: recursive function call
int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n-1);   // recursion (direct)
}

Konformes Beispiel (verwendet Schleife)

// Compliant: uses an explicit loop instead of recursion
int factorial(int n) {
    int result = 1;
    for (int i = n; i > 1; --i) {
        result *= i;
    }
    return result;
}

Warum das wichtig ist: Rekursion und Goto erzeugen einen nicht-linearen Kontrollfluss, der schwer zu verstehen ist. Rekursive Aufrufe machen den Aufrufgraph zyklisch und die Stapeltiefe unbegrenzt; Goto erzeugt Spaghetti-Code. Durch die Verwendung einfacher Schleifen und geradlinigen Codes kann ein statischer Analyzer die Stack-Nutzung und die Programmpfade leicht überprüfen. Ein Verstoß gegen diese Regel könnte zu unerwarteten Stack-Überläufen oder logischen Pfaden führen, die manuell nur schwer zu überprüfen sind.

2. Schleifen müssen feste obere Schranken haben.

Jede Schleife sollte eine zur Kompilierzeit überprüfbare Grenze haben.

Nicht konformes Beispiel (nicht begrenzte Schleife):

// Non-compliant: loop with dynamic or unknown bound
int i = 0;
while (array[i] != 0) {
    doSomething(array[i]);
    i++;
}

Konformes Beispiel (fest gebundene Schleife):

// Compliant: loop with explicit fixed upper bound and assert
#define MAX_LEN 100
for (int i = 0; i < MAX_LEN; i++) {
    if (array[i] == 0) break;
    doSomething(array[i]);
}

Warum das wichtig ist: Unbegrenzte Schleifen können ewig laufen oder die Ressourcengrenzen überschreiten. Mit einer festen Begrenzung können Werkzeuge die maximalen Iterationen statisch nachweisen. In sicherheitskritischen Systemen könnte eine fehlende Begrenzung zu einer durchlaufenden Schleife führen. Durch das Erzwingen einer expliziten Grenze (oder einer statischen Array-Größe) stellen wir sicher, dass Schleifen vorhersehbar beendet werden. Ohne diese Regel würde ein Fehler in der Schleifenlogik möglicherweise erst bei der Implementierung bemerkt (z. B. ein Off-by-One, das eine Endlosschleife verursacht).

3. Kein dynamischer Speicher nach der Initialisierung.

Vermeiden Sie malloc/free oder jegliche Heap-Verwendung im laufenden Code; verwenden Sie nur feste oder Stack-Zuweisungen.

Nicht konformes Beispiel (verwendet malloc)

// Non-compliant: dynamic allocation inside the code
void storeData(int size) {
    int *buffer = malloc(size * sizeof(int));
    if (buffer == NULL) return;
    // ... use buffer ...
    free(buffer);
}

Konformes Beispiel (statische Zuweisung)

// Compliant: fixed-size array on stack or global
#define MAX_SIZE 256
void storeData() {
    int buffer[MAX_SIZE];
    // ... use buffer without dynamic alloc ...
}

Warum dies wichtig ist: Die dynamische Speicherzuweisung während der Laufzeit kann zu unvorhersehbarem Verhalten, Speicherfragmentierung oder Zuweisungsfehlern führen, insbesondere in Systemen mit begrenzten Ressourcen wie Raumfahrzeugen oder eingebetteten Steuerungen. Wenn malloc oder free mitten in der Mission fehlschlagen, kann die Software abstürzen oder sich unvorhersehbar verhalten. Die ausschließliche Verwendung von Speicher mit fester Größe oder Stack-Zuweisung gewährleistet ein deterministisches Verhalten, vereinfacht die Validierung und verhindert Speicherlecks während der Laufzeit.

4. Die Funktionen passen auf eine Seite (~60 Zeilen).

Halten Sie jede Funktion kurz (etwa ≤ 60 Zeilen).

Nicht konformes Beispiel

// Non-compliant: hundreds of lines in one function (not shown)
void processAllData() {
    // ... imagine 100+ lines of code doing many tasks ...
}

Konformes Beispiel (modulare Funktionen)

// Compliant: break the task into clear sub-functions
void processAllData() {
    preprocessData();
    analyzeData();
    postprocessData();
}
void preprocessData() { /* ... */ }
void analyzeData()   { /* ... */ }
void postprocessData(){ /* ... */ }

Warum das wichtig ist: Extrem lange Funktionen sind schwer zu verstehen, zu testen und als Einheit zu überprüfen. Indem man jede Funktion auf eine konzeptionelle Aufgabe (und auf eine Druckseite) beschränkt, werden Codeprüfungen und statische Überprüfungen überschaubar. Wenn sich eine Funktion über zu viele Zeilen erstreckt, können logische Fehler oder Randbedingungen übersehen werden. Die Aufteilung des Codes in kleinere Funktionen verbessert die Übersichtlichkeit und erleichtert die Durchsetzung anderer Regeln (z. B. Behauptungsdichte und Rückgabeprüfungen pro Funktion).

5. Verwenden Sie mindestens zwei assert-Anweisungen pro Funktion.

Jede Funktion sollte defensive Prüfungen durchführen.

Beispiel für Nichtkonformität (keine Anforderungen):

int get_element(int *array, size_t size, size_t index) {
return array[index];
}

Konformes Beispiel (mit Assertions):

int get_element(int *array, size_t size, size_t index) {
    assert(array != NULL);        // Assertion 1: pointer validity
    assert(index < size);          // Assertion 2: bounds check
    
    if (array == NULL) return -1;  // Recovery: return error
    if (index >= size) return -1;  // Recovery: return error
    
    return array[index];
}

Warum dies wichtig ist: Behauptungen sind die erste Verteidigungslinie gegen ungültige Bedingungen. Die NASA fand heraus, dass eine höhere Assertion-Dichte die Chance, Fehler zu finden, deutlich erhöht. Mit mindestens zwei Assertions pro Funktion (zur Überprüfung von Vorbedingungen, Grenzwerten und Invarianten) dokumentiert der Code seine Annahmen selbst und zeigt Anomalien beim Testen sofort an. Ohne Asserts könnte sich ein unerwarteter Wert unbemerkt ausbreiten und weit entfernt von der Fehlerquelle zu einem Fehler führen.

6. Deklarieren Sie Daten mit minimalem Umfang.

Halten Sie Variablen so lokal wie möglich; vermeiden Sie globale Variablen.

Beispiel für nicht konforme Daten ( globale Daten):

// Non-compliant: global variable visible everywhere
int statusFlag;
void setStatus(int f) {
    statusFlag = f;
}

Konformes Beispiel (lokaler Geltungsbereich):

// Compliant: local variable inside function
void setStatus(int f) {
    int statusFlag = f;
    // ... use statusFlag only here ...
}

Warum das wichtig ist: Durch die Minimierung des Geltungsbereichs werden Kopplung und unbeabsichtigte Wechselwirkungen reduziert. Wenn eine Variable nur innerhalb einer Funktion benötigt wird, besteht bei ihrer globalen Deklaration die Gefahr, dass anderer Code sie unerwartet verändert. Wenn Daten lokal gehalten werden, ist jede Funktion in sich geschlossener und frei von Nebeneffekten, was die Analyse und das Testen vereinfacht. Verstöße (wie die Wiederverwendung globaler Zustände) können aufgrund von Aliasing oder unerwarteten Änderungen zu schwer auffindbaren Fehlern führen.

7. Prüfen Sie alle Funktionsrückgabewerte und Parameter.

Der Aufrufer muss jeden nicht leeren Rückgabewert überprüfen; jede Funktion muss ihre Eingabeparameter validieren.

❌ Nicht konformes Beispiel (Rückgabewert wird ignoriert)

int bad_mission_control(int velocity, int time) {
    int distance;
    calculate_trajectory(velocity, time, &distance);  // Didn't check!
    return distance;  // Could be garbage if calculation failed
}

Konformes Beispiel

int good_mission_control(int velocity, int time) {
    int distance;
    int status = calculate_trajectory(velocity, time, &distance);
    
    if (status != 0) {  // Checked the return value
        return -1;  // Propagate error to caller
    }
    
    return distance;  // Safe to use
}

Warum dies wichtig ist: Das Ignorieren von Rückgabewerten oder ungültigen Parametern ist eine der Hauptquellen für Bugs. Wird zum Beispiel malloc nicht überprüft, kann dies zu einer Null-Zeiger-Dereferenz führen. Ebenso kann die Nichtüberprüfung von Eingaben (z. B. Array-Indizes oder Format-Strings) zu Pufferüberläufen oder Abstürzen führen. Die NASA verlangt, dass jede Rückkehr behandelt wird (oder explizit in void umgewandelt wird, um die Absicht zu signalisieren) und jedes Argument überprüft wird. Dieser "catch-all"-Ansatz stellt sicher, dass kein Fehler stillschweigend ignoriert wird.

8. Beschränken Sie den Präprozessor auf Includes und einfache Makros.

Vermeiden Sie komplexe Makros oder bedingte Kompilierungstricks.

Beispiel für eine nicht konforme Vorgehensweise (komplexe Markierung):

#definieren DECLARE_FUNC(name) void func_##name(void)

DECLARE_FUNC(init);  // Erweitert zu: void func_init(void)

Konformes Beispiel (einfache Makros / Inline):

// Compliant: use inline function or straightforward definitions
static inline int sqr(int x) { return x*x; }
#define MAX_BUFFER 256

Warum dies wichtig ist: Komplexe Makros (insbesondere mehrzeilige oder funktionsähnliche Makros) können die Logik verbergen, den Kontrollfluss verwirren und die statische Analyse vereiteln. Durch die Beschränkung des Präprozessors auf triviale Aufgaben (z. B. Konstanten und Header) bleibt der Code eindeutig. Das Ersetzen von Makros durch Inline-Funktionen verbessert zum Beispiel die Typüberprüfung und die Debugging-Möglichkeiten. Ohne diese Regel könnten subtile Makroexpansionsfehler oder Fehler bei der bedingten Kompilierung bei der Überprüfung unbemerkt bleiben.

9. Begrenzen Sie die Verwendung von Zeigern.

Beschränkung der Indirektion auf eine einzige Ebene - Vermeidung von int** und Funktionszeigern.

Nicht konformes Beispiel (mehrfache Umleitung):

// Nicht konform: Doppelzeiger und Funktionszeiger
int **doublePtr;
int (*funcPtr)(int) = someFunction;

Konformes Beispiel (einzelner Zeiger):

// Konform: Zeiger auf einer Ebene, keine Funktionszeiger
int *singlePtr;
// Expliziter Aufruf anstelle von Funktionszeigern
int result = someFunction(5);

Warum dies wichtig ist: Mehrere Ebenen von Zeigern und Funktionszeigern verkomplizieren den Datenfluss und machen es schwer zu verfolgen, auf welchen Speicher oder Code zugegriffen wird. Statische Analysatoren müssen jede Umleitung auflösen, was im Allgemeinen unentscheidbar sein kann. Durch die Beschränkung auf Single-Pointer-Referenzen bleibt der Code einfacher und sicherer. Ein Verstoß dagegen kann zu unklarem Aliasing (ein Zeiger verändert Daten durch einen anderen) oder unerwartetem Rückrufverhalten führen, was in sicherheitskritischen Kontexten riskant ist.

10. Kompilieren Sie mit allen aktivierten Warnungen und beheben Sie diese.

Aktivieren Sie alle Compiler-Warnungen und beheben Sie sie vor der Veröffentlichung.

Nicht konformes Beispiel (Code mit Warnungen)

// Non-compliant: code that generates warnings (uninitialized, suspicious assignment)
int x;
if (x = 5) {  // bug: should be '==' or initialize x
    // ...
}
printf("%d\n", x);  // warning: 'x' is used uninitialized

Konformes Beispiel (saubere Kompilierung)

// Compliant: initialize variables and use '==' in condition
int x = 0;
if (x == 5) {
    // ...
}
printf("%d\n", x);

Warum dies wichtig ist: Compiler-Warnungen weisen oft auf echte Fehler hin (wie nicht initialisierte Variablen, Typ-Fehlanpassungen oder unbeabsichtigte Zuweisungen). Die NASA-Regel schreibt vor, dass keine Warnung ignoriert wird. Vor jeder Freigabe sollte der Code ohne Warnungen unter maximalen Verbositätseinstellungen kompiliert werden. Durch diese Praxis werden viele triviale Fehler frühzeitig erkannt. Wenn eine Warnung nicht behoben werden kann, sollte der Code so umstrukturiert oder dokumentiert werden, dass die Warnung erst gar nicht auftritt.

Jede dieser Regeln beseitigt eine Kategorie von versteckten Fehlern. Wenn sie zusammen befolgt werden, machen sie den C-Code wesentlich berechenbarer und überprüfbarer.

Schlussfolgerung

Die 10 Regeln der NASA (die "Macht der 10") bieten einen klaren und effektiven Kodierungsstandard für kritische C-Software. Durch die Vermeidung komplexer Konstrukte und die Durchsetzung von Prüfungen verringern sie die Wahrscheinlichkeit versteckter Fehler und machen eine statische Analyse möglich. In der modernen Entwicklung können diese Richtlinien mit Code-Qualitätswerkzeugen automatisiert werden. Es können benutzerdefinierte Regeln definiert werden, um Verstöße gegen die NASA-Richtlinien zu kennzeichnen, und diese Regeln können bei jeder Pull-Anfrage ausgeführt werden, um den Entwicklern ein sofortiges Feedback zu geben.

Die frühzeitige Übernahme dieser Prüfungen führt zu sichererem, qualitativ hochwertigerem Code, der leichter zu warten ist. Auch außerhalb der Luft- und Raumfahrt gelten diese Prinzipien: kleine, klare Funktionen, explizite Schleifen, defensive Programmierung und keine haarsträubende Zeiger-Gymnastik. Das Befolgen und Automatisieren dieser Regeln mit einem Code-Qualitätswerkzeug hilft Ihrem Team, Fehler frühzeitig zu erkennen und zuverlässigere Software zu liefern.

FAQs

Haben Sie Fragen?

Gelten die Regeln der NASA nur für Weltraum- oder eingebettete Projekte?

Ganz und gar nicht. Diese Regeln wurden in einem sicherheitskritischen Kontext entwickelt, lassen sich aber gut verallgemeinern. Jedes C-Projekt, das Wert auf Wartbarkeit und Zuverlässigkeit legt, kann davon profitieren. Tatsächlich ergänzen die Regeln Industriestandards wie MISRA C. Viele Entwickler außerhalb der NASA haben festgestellt, dass die Durchsetzung auch nur einer Teilmenge dieser Richtlinien die Codequalität verbessert.

Wie kann ich diese Regeln automatisch durchsetzen?

Verwenden Sie ein statisches Analyse- oder Code-Review-Tool. Mit dem Code-Qualitäts-Tool von Aikido Security können Sie eigene Regeln erstellen. Sie können für jede Richtlinie eine kleine Regel schreiben - z. B. eine, die jedes goto oder jede Funktion, die länger als 60 Zeilen ist, kennzeichnet - und diese in Aikido speichern. Aikido prüft dann jede neue Pull-Anfrage anhand Ihrer benutzerdefinierten Regeln und blockiert Zusammenführungen, wenn es einen Verstoß gibt. Dies lässt sich nahtlos mit GitHub/GitLab/Bitbucket usw. integrieren.

Warum muss ich dynamischen Speicher und Rekursion vermeiden?

Dynamische Speicherallokatoren (wie malloc) können versagen oder sich unvorhersehbar verhalten, und nicht verwaltete Rekursionen führen dazu, dass der Stack unbegrenzt genutzt wird. In kritischer Software müssen Sie oft Ressourcengrenzen nachweisen und Worst-Cases behandeln. Indem Sie malloc und Rekursion zur Laufzeit verbieten, erzwingen Sie, dass der gesamte Speicher und die Aufruftiefe im Voraus bekannt sind. Dies verhindert klassische Fehler wie Speicherlecks, Überlauf oder Stapelüberlauf, die besonders gefährlich sind, wenn Menschenleben oder millionenschwere Geräte auf dem Spiel stehen.

Was ist, wenn mein Projekt eine dieser Regeln brechen muss?

Die Richtlinien der NASA sind von vornherein streng. Wenn Sie davon abweichen müssen (z. B. bei Verwendung eines kleinen dynamischen Puffers), sollten Sie dies bewusst tun: Dokumentieren Sie die Ausnahme, begründen Sie sie und fügen Sie möglicherweise Laufzeitprüfungen hinzu. Einige Teams behandeln einige Regeln eher als Warnungen denn als Fehler, aber am sichersten ist es, den Code so umzugestalten, dass er den Regeln entspricht. Die NASA-Regeln sind konservativ, aber genau deshalb funktionieren sie. Wenn Sie Aikido oder ein anderes Tool verwenden, können Sie eine Regel als niedrig priorisiert markieren, aber es ist immer noch am besten, das zugrunde liegende Problem anzugehen.

Kann Aikido Verstöße gegen die NASA-Regeln von anderen Problemen unterscheiden?

Ja. Die Aikido-Regeln sind anpassbar und können mit Tags versehen werden. Sie können Ihre benutzerdefinierten Regeln mit "NASA-Regel 1", "NASA-Regel 2" usw. kennzeichnen, so dass bei Verstößen klar ersichtlich ist, welche Richtlinie verletzt wurde. Aikido verfolgt auch Analysen im Laufe der Zeit, so dass Sie Metriken wie " compliance " in Ihrer Codebasis sehen können. Diese Nachvollziehbarkeit hilft den Teams bei der Priorisierung von Korrekturen und dem Nachweis der compliance bei Audits.

Starten Sie kostenlos

Sichern Sie Ihren Code, Cloud und die Laufzeit in einem zentralen System.
Finden und beheben Sie Schwachstellen schnell  automatisch.

Keine Kreditkarte erforderlich | Scanergebnisse in 32 Sekunden.