Aikido

Nasa’s 10 Programmierregeln für sicherheitskritischen Code

Einleitung

Sicherheitskritische Software, wie sie in Raumfahrzeugen oder Automobilsystemen eingesetzt wird, erfordert extrem zuverlässigen Code. Um diesem Bedarf gerecht zu werden, entwickelte das Jet Propulsion Laboratory der NASA im Jahr 2006 die
„Power of 10“-Codierungsregeln. Diese prägnanten Richtlinien eliminieren komplexe C-Konstrukte, die schwer zu analysieren sind, und stellen sicher, dass der Code einfach, überprüfbar und zuverlässig bleibt.

Heute können Tools wie die Aikido Code Quality mit benutzerdefinierten Checks eingerichtet werden, um alle zehn Regeln bei jedem neuen Pull Request durchzusetzen. In diesem Artikel erklären wir jede Regel, warum sie wichtig ist, und stellen Codebeispiele vor, die sowohl falsche als auch korrekte 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 Raumfahrzeugsteuerung und Flugsoftware unerlässlich sind. Indem sie obskure C-Konstrukte verbieten und defensive Prüfungen erzwingen, erleichtern die Richtlinien die Code-Überprüfung und den Nachweis seiner Korrektheit. Sie ergänzen Standards wie MISRA C, indem sie Muster adressieren, die statische Analysatoren oft übersehen. Zum Beispiel hält das Vermeiden von Rekursion und dynamischem Speicher die Ressourcennutzung vorhersehbar, während das Erzwingen von Rückgabewertprüfungen hilft, viele Fehler zur Kompilierzeit abzufangen.

Tatsächlich fand eine NASA-Studie eines Massenmarkt-Embedded-Systems, wie der elektronischen Drosselklappen-Firmware von Toyota, Hunderte von Regelverstößen. Dies zeigt, dass reale Projekte oft auf dieselben Probleme stoßen, die diese Regeln verhindern sollen. Jede Regel in der Liste verhindert eine Klasse häufiger Fehler (unkontrollierte Schleifen, Null-Pointer-Dereferenzen, unsichtbare Nebenwirkungen usw.). Das Ignorieren dieser Regeln kann zu subtilen Laufzeitfehlern, Sicherheitslücken oder nicht-deterministischem Verhalten führen. Im Gegensatz dazu macht die Einhaltung aller zehn Regeln die statische Verifikation wesentlich handhabbarer.

Automatisierte Tools sind wichtig. Code-Quality-Plattformen können so konfiguriert werden, dass sie verbotene Konstrukte oder Muster erkennen. Diese Regeln werden automatisch bei jedem Pull Request ausgeführt und erkennen Probleme, bevor Code zusammengeführt wird.

Kontext zu den Regeln herstellen

Bevor wir uns den einzelnen Regeln widmen, ist es wichtig, den Kontext zu verstehen:

  • Zielsprache: Die NASA-Regeln „Power of 10“ wurden für C geschrieben, eine Sprache mit umfassender Tool-Unterstützung (Compiler, Analyzer, Debugger), die aber auch für undefinierte Verhaltensweisen berüchtigt ist. Sie setzen keine Garbage Collection oder fortschrittliches Speichermanagement voraus. Durch die Verwendung von einfachem, gut strukturiertem C kann man statische Analyse nutzen, um Programmeigenschaften zu beweisen.
  • Statische Analyse: Es gibt viele Regeln, um automatisierte Prüfungen zu erleichtern. Zum Beispiel ermöglicht das Verbot von Rekursion (Regel 1) und die Forderung nach Schleifengrenzen (Regel 2) Tools zu beweisen, wie viele Iterationen oder Stack-Nutzung jede Funktion haben kann. Ebenso macht das Verbot komplexer Makros und die Begrenzung von Zeigern (Regeln 8–9) Code-Muster explizit, anstatt sie in Präprozessor-Magie oder mehrfachen Indirektionen zu verstecken.
  • Entwicklungs-Workflow: In modernen DevSecOps-Pipelines werden diese Regeln Teil der CI-Checks. Code-Quality-Tools können mit GitHub, GitLab oder Bitbucket integriert werden, um jeden Pull Request zu überprüfen und sowohl einfache Probleme als auch komplexere Muster zu erkennen. Sie können eine benutzerdefinierte Regel für jede NASA-Richtlinie erstellen, z. B. „jede Verwendung von goto oder rekursiven Funktionsaufrufen kennzeichnen“ oder „sicherstellen, dass jede Schleife ein literales Limit hat“. Einmal konfiguriert, werden diese Regeln automatisch bei jedem zukünftigen Code-Scan angewendet, wodurch Verstöße frühzeitig erkannt und Anleitungen zur Behebung gegeben werden.

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

Die 10 NASA-Codierungsregeln

1. Vermeiden Sie komplexe Kontrollflüsse.

Verwenden Sie keine goto, setjmp oder longjmp, vermeiden Sie das Schreiben rekursiver Funktionen in irgendeinem Teil des Codes.

Nicht Compliance-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 Gotos erzeugen einen nicht-linearen Kontrollfluss, der schwer nachzuvollziehen ist. Rekursive Aufrufe machen den Aufruf-Graphen zyklisch und die Stack-Tiefe unbegrenzt; Gotos erzeugen Spaghetti-Code. Durch die Verwendung einfacher Schleifen und geradlinigen Codes kann ein statischer Analysator die Stack-Nutzung und Programm-Pfade leicht überprüfen. Ein Verstoß gegen diese Regel könnte zu unerwarteten Stack-Überläufen oder Logikpfaden führen, die manuell schwer zu überprüfen sind.

2. Schleifen müssen feste obere Grenzen haben.

Jede Schleife sollte ein zur Kompilierzeit überprüfbares Limit haben.

Nicht konformes Beispiel (unbegrenzte Schleife):

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

Konformes Beispiel (fest begrenzter 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 endlos laufen oder Ressourcenlimits überschreiten. Mit einer festen Begrenzung können Tools die maximale Anzahl von Iterationen statisch nachweisen. In sicherheitskritischen Systemen könnte eine fehlende Begrenzung eine außer Kontrolle geratene Schleife verursachen. Durch die Erzwingung einer expliziten Grenze (oder einer statischen Array-Größe) stellen wir sicher, dass Schleifen vorhersehbar terminieren. Ohne diese Regel könnte ein Fehler in der Schleifenlogik erst bei der Bereitstellung entdeckt werden (z. B. ein Off-by-One-Fehler, der eine Endlosschleife verursacht).

3. Kein dynamischer Speicher nach der Initialisierung.

malloc/free oder jegliche Heap-Nutzung im laufenden Code vermeiden; nur feste oder Stack-Allokation verwenden.

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 Allokation)

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

Warum das wichtig ist: Dynamische Speicherallokation zur Laufzeit kann zu unvorhersehbarem Verhalten, Speicherfragmentierung oder Allokationsfehlern führen, insbesondere in Systemen mit begrenzten Ressourcen wie Raumfahrzeugen oder eingebetteten Controllern. Wenn malloc oder free mitten in einer Mission fehlschlägt, kann die Software abstürzen oder sich unvorhersehbar verhalten. Die ausschließliche Verwendung von Speicher fester Größe oder Stack-allokiertem Speicher gewährleistet deterministisches Verhalten, vereinfacht die Validierung und verhindert Speicherlecks zur Laufzeit.

4. Funktionen passen auf eine Seite (ca. 60 Zeilen).

Halten Sie jede Funktion kurz (ungefähr ≤ 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 als Einheit schwer zu verstehen, zu testen und zu verifizieren. Indem jede Funktion auf eine konzeptionelle Aufgabe (und innerhalb einer gedruckten Seite) beschränkt wird, werden Code-Reviews und statische Prüfungen handhabbar. Wenn eine Funktion zu viele Zeilen umfasst, können logische Fehler oder Randbedingungen übersehen werden. Das Aufteilen von Code in kleinere Funktionen verbessert die Klarheit und erleichtert die Durchsetzung anderer Regeln (wie Assertionsdichte und Rückgabeprüfungen pro Funktion).

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

Jede Funktion sollte defensive Prüfungen durchführen.

Nicht konformes Beispiel (keine Assertions):

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 das wichtig ist: Assertions sind die erste Verteidigungslinie gegen ungültige Bedingungen. Die NASA stellte fest, dass eine höhere Assertionsdichte die Wahrscheinlichkeit, Fehler zu finden, erheblich erhöht. Mit mindestens zwei Asserts pro Funktion (die Vorbedingungen, Grenzen und Invarianten prüfen) dokumentiert der Code seine Annahmen selbst und meldet Anomalien sofort während des Tests. Ohne Asserts könnte sich ein unerwarteter Wert unbemerkt ausbreiten und einen Fehler verursachen, der weit von der Fehlerquelle entfernt ist.

6. Daten mit minimalem Geltungsbereich deklarieren.

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

Nicht konformes Beispiel (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: Die Minimierung des Gültigkeitsbereichs reduziert Kopplung und unbeabsichtigte Interaktionen. Wenn eine Variable nur innerhalb einer Funktion benötigt wird, birgt die globale Deklaration das Risiko, dass anderer Code sie unerwartet ändert. Indem Daten lokal gehalten werden, wird jede Funktion eigenständiger und nebenwirkungsfrei, was Analyse und Tests vereinfacht. Verstöße (wie die Wiederverwendung globalen Zustands) können zu schwer auffindbaren Fehlern aufgrund von Aliasing oder unerwarteten Modifikationen führen.

7. Alle Funktionsrückgabewerte und Parameter prüfen.

Der Aufrufer muss jeden Nicht-Void-Rückgabewert prüfen; jede Funktion muss ihre Eingabeparameter validieren.

❌ Nicht konformes Beispiel (ignoriert Rückgabewert)

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
}

Compliance-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 das wichtig ist: Das Ignorieren von Rückgabewerten oder ungültigen Parametern ist eine Hauptursache für Fehler. Zum Beispiel kann das Versäumnis, malloc zu überprüfen, zu einer Nullzeiger-Dereferenzierung führen. Ebenso kann das Nicht-Validieren von Eingaben (z. B. Array-Indizes oder Format-Strings) zu Pufferüberläufen oder Abstürzen führen. Die NASA verlangt, dass jeder Rückgabewert behandelt (oder explizit zu void gecastet wird, um die Absicht zu signalisieren) und jedes Argument verifiziert wird. Dieser umfassende Ansatz stellt sicher, dass kein Fehler stillschweigend ignoriert wird.

8. Den Präprozessor auf Includes und einfache Makros beschränken.

Komplexe Makros oder Tricks bei der bedingten Kompilierung vermeiden.

Nicht konformes Beispiel (komplexes Makro):

#define 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 das wichtig ist: Komplexe Makros (insbesondere mehrzeilige oder funktionsähnliche Makros) können Logik verbergen, den Kontrollfluss verwirren und die statische Analyse behindern. Die Beschränkung des Präprozessors auf triviale Aufgaben (z. B. Konstanten und Header) hält den Code explizit. Das Ersetzen von Makros durch Inline-Funktionen verbessert beispielsweise die Typüberprüfung und die Debugging-Fähigkeit. Ohne diese Regel könnten subtile Makro-Expansionsfehler oder Fehler bei der bedingten Kompilierung unbemerkt durch Reviews rutschen.

9. Beschränken Sie die Zeigernutzung.

Beschränken Sie die Indirektion auf eine einzige Ebene – vermeiden Sie int** und Funktionszeiger.

Nicht konformes Beispiel (mehrfache Indirektion):

// Nicht konform: Double-Pointer und Funktionspointer
int **doublePtr;
int (*funcPtr)(int) = someFunction;

Konformes Beispiel (einzelner Zeiger):

// Konform: Single-Level-Pointer, keine Funktionspointer
int *singlePtr;
// Expliziten Aufruf anstelle eines Funktionspointers verwenden
int result = someFunction(5);

Warum das wichtig ist: Mehrere Ebenen von Zeigern und Funktionszeigern erschweren den Datenfluss und machen es schwierig nachzuvollziehen, auf welchen Speicher oder Code zugegriffen wird. Statische Analysatoren müssen jede Indirektion auflösen, was im Allgemeinen unentscheidbar sein kann. Durch die Beschränkung auf Einzelzeiger-Referenzen bleibt der Code einfacher und sicherer. Ein Verstoß dagegen kann zu unklarem Aliasing (ein Zeiger modifiziert Daten über einen anderen) oder unerwartetem Callback-Verhalten führen, beides ist in sicherheitskritischen Kontexten riskant.

10. Mit allen aktivierten Warnungen kompilieren und diese beheben.

Aktivieren Sie jede Compiler-Warnung und beheben Sie diese 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 das wichtig ist: Compiler-Warnungen weisen oft auf echte Fehler hin (wie nicht initialisierte Variablen, Typenkonflikte oder unbeabsichtigte Zuweisungen). Die NASA-Regel schreibt vor, dass keine Warnung ignoriert wird. Vor jeder Veröffentlichung sollte der Code unter maximalen Ausführlichkeitseinstellungen ohne Warnungen kompilieren. Diese Praxis fängt viele triviale Fehler frühzeitig ab. Wenn eine Warnung nicht behoben werden kann, sollte der Code umstrukturiert oder dokumentiert werden, damit die Warnung gar nicht erst auftritt.

Jede dieser Regeln eliminiert eine Kategorie versteckter Fehler. Wenn sie zusammen befolgt werden, machen sie C-Code wesentlich vorhersehbarer und überprüfbarer.

Fazit

Die 10 Regeln der NASA (die „Power of 10“) bieten einen klaren und effektiven Programmierstandard für kritische C-Software. Durch das Vermeiden komplexer Konstrukte und das Erzwingen von Prüfungen reduzieren sie die Wahrscheinlichkeit versteckter Fehler und ermöglichen eine statische Analyse. In der modernen Entwicklung können diese Richtlinien mit Code-Qualitätstools automatisiert werden. Benutzerdefinierte Regeln können definiert werden, um jede Verletzung der NASA-Richtlinien zu kennzeichnen, und diese Regeln können bei jedem Pull Request ausgeführt werden, um den Entwickelnden sofortiges Feedback zu geben.

Die frühzeitige Einführung dieser Prüfungen führt zu sicherem, qualitativ hochwertigerem Code, der einfacher zu warten ist. Auch außerhalb der Luft- und Raumfahrt gelten die Prinzipien: kleine, klare Funktionen, explizite Schleifen, defensive Programmierung und keine komplexen Zeiger-Operationen. Das Befolgen und Automatisieren dieser Regeln mit einem Code-Qualitäts-Tool hilft Ihrem Team, Fehler frühzeitig zu erkennen und zuverlässigere Software auszuliefern.

FAQs

Haben Sie Fragen?

Gelten die Regeln der NASA nur für Raumfahrt- oder Embedded-Projekte?

Überhaupt nicht. Diese Regeln stammen aus einem sicherheitskritischen Kontext, 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 Entwickelnde außerhalb der NASA haben festgestellt, dass die Durchsetzung selbst einer Teilmenge dieser Richtlinien die Codequalität verbessert.

Wie setze ich diese Regeln automatisch durch?

Verwenden Sie ein statisches Analyse- oder Code-Review-Tool. Das Code Quality Tool von Aikido Security ermöglicht es Ihnen, benutzerdefinierte Regeln zu erstellen. Sie können eine kleine Regel für jede Richtlinie 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 jeden neuen Pull Request anhand Ihrer benutzerdefinierten Regeln und blockiert Zusammenführungen, wenn ein Verstoß vorliegt. Dies lässt sich nahtlos in GitHub/GitLab/Bitbucket usw. integrieren.

Warum muss ich dynamischen Speicher und Rekursion vermeiden?

Dynamische Speicherallokatoren (wie malloc) können fehlschlagen oder sich unvorhersehbar verhalten, und unkontrollierte Rekursion führt zu unbegrenzter Stack-Nutzung. In kritischer Software muss man oft Ressourcengrenzen nachweisen und Worst-Case-Szenarien behandeln. Indem malloc und Rekursion zur Laufzeit unterbunden werden, erzwingt man, dass der gesamte Speicher und die Aufruftiefe im Voraus bekannt sind. Dies verhindert klassische Fehler wie Speicherlecks, Überläufe oder Stack-Überläufe, die besonders gefährlich sind, wenn Menschenleben oder millionenschwere Ausrüstung auf dem Spiel stehen.

Was, wenn mein Projekt eine dieser Regeln brechen muss?

Die NASA-Richtlinien sind bewusst streng. Wenn Sie unbedingt abweichen müssen (z. B. durch Verwendung eines kleinen dynamischen Puffers), sollten Sie dies bewusst tun: dokumentieren Sie die Ausnahme, begründen Sie sie und fügen Sie gegebenenfalls Laufzeitprüfungen hinzu. Einige Teams behandeln einige Regeln lieber als Warnungen statt als Fehler, aber der sicherste Ansatz ist, den Code so umzugestalten, dass er konform ist. Die NASA-Regeln sind konservativ, aber genau deshalb funktionieren sie. Wenn Sie Aikido oder ein anderes Tool verwenden, könnten Sie eine Regel als geringe Priorität kennzeichnen, aber es ist immer noch am besten, das zugrunde liegende Problem zu beheben.

Kann Aikido NASA-Regelverstöße von anderen Problemen unterscheiden?

Ja. Die Regeln von Aikido sind anpassbar und taggbar. Sie können Ihre benutzerdefinierten Regeln als „NASA Regel 1“, „NASA Regel 2“ usw. kennzeichnen, sodass Verstöße klar zeigen, welche Richtlinie verletzt wurde. Aikido verfolgt auch Analysen im Zeitverlauf, sodass Sie Metriken wie die „NASA-Regel-Compliance-Rate“ über Ihre Codebasis hinweg sehen können. Diese Nachvollziehbarkeit hilft Teams, Korrekturen zu priorisieren und die Compliance bei Audits nachzuweisen.

Werden Sie jetzt sicher.

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

Keine Kreditkarte erforderlich | Scan-Ergebnisse in 32 Sek.