Die Annahme, die der Sonne zu nah flog

Anfang der 1980er Jahre stand NASA vor einer Frage, die die sicherheitskritische Entwicklung bis heute verfolgt: Wie toleriert man Bugs, die man noch nicht gefunden hat? Ihre Antwort war N-Version Programming. Man gibt dieselbe Spezifikation an drei unabhängige Teams. Lässt alle drei Programme parallel laufen. Stimmt über die Outputs ab. Wenn ein Team einen Bug geschrieben hat, überstimmen ihn die anderen beiden.

Es klingt wie gesunder mathematischer Menschenverstand. Es ist aber falsch.

Im Jahr 1986 veröffentlichten John Knight und Nancy Leveson das Ergebnis eines großangelegten, von NASA finanzierten Experiments. Siebenundzwanzig Graduierte an zwei Universitäten schrieben unabhängige Implementierungen derselben Spezifikation für einen ballistischen Raketenabfangjäger. Jedes Programm war für sich genommen extrem zuverlässig. Sechs Versionen versagten überhaupt nie. Dreiundzwanzig bestanden mehr als 99,9 % einer Million zufällig generierter Testfälle.

Doch wenn mehrere Versionen bei derselben Eingabe versagten, taten sie das weitaus häufiger gemeinsam, als statistische Unabhängigkeit vorhersagen würde. Der z-score lag bei 100,55. Bei einem 99 %-Konfidenzintervall wurde die Nullhypothese der Unabhängigkeit vernichtet. NASA hatte darauf gewettet, dass menschlicher Fehler zufällig ist. Knight und Leveson bewiesen, dass er clusterbildend auftritt.

Was N-Version Programming tatsächlich verspricht

Die Logik hinter N-Version Programming ist aus der Hardware-Redundanz übernommen. Befestigt man drei identische Gyroskope an einer Rakete und eines driftet, überstimmen die anderen beiden die Drift. Die Fehler sind unabhängig, weil die Gyroskope physische Objekte sind, die unabhängigen Fertigungstoleranzen, Temperaturgradienten und Schwingungsmoden unterliegen.

Software ist anders. Wenn man drei Teams bittet, dasselbe Problem zu lösen, bekommt man nicht drei unabhängige Würfelwürfe. Man bekommt drei Menschen, die dieselbe mehrdeutige Spezifikation gelesen, dieselben Algorithmen aus denselben Lehrbüchern gelernt und Code in derselben Sprache mit derselben Standardbibliothek geschrieben haben. Ihre Fehler korrelieren, weil ihre Inputs korrelieren.

Die N-Version-Architektur sieht in der Praxis so aus:

from typing import Callable, List, TypeVar

T = TypeVar('T')

def n_version_vote(
    implementations: List[Callable[[float], T]],
    input_value: float
) -> T:
    """Run N versions and return the majority output."""
    results = [impl(input_value) for impl in implementations]
    
    # Simple majority vote
    from collections import Counter
    counts = Counter(str(r) for r in results)
    most_common = counts.most_common(1)[0][0]
    
    # Return the actual value that matched the winning string
    for r in results:
        if str(r) == most_common:
            return r
    
    raise RuntimeError("No consensus")

Dieser Voter ist der einfache Teil. Der schwierige Teil ist die Annahme, die sich darin verbirgt: dass implementations[0], implementations[1] und implementations[2] bei unkorrelierten Teilmengen des Input-Raums versagen. Knight und Leveson zeigten, dass diese Annahme der Fehler ist.

Woher die korrelierten Fehler kommen

Das Experiment von Knight und Leveson offenbarte zwei unterschiedliche Mechanismen für Common-Mode-Failure, und keiner von beiden ist behebbar, indem man Teams auffordert, „sich mehr anzustrengen“.

Der erste ist Mehrdeutigkeit der Spezifikation. Die Spezifikation des Raketenabfangjägers enthielt Edge Cases, die wirklich schwierig waren. Acht der siebenundzwanzig Programmierer behandelten den Fall falsch, bei dem drei Radar-Punkte kollinear waren. Die Fehler waren unterschiedlich. Einer hatte einen Off-by-One in einem Array-Index. Einer verwendete einen Algorithmus mit schlechter numerischer Stabilität. Einer vergaß einen Boundary Case vollständig. Aber die Fehler häuften sich bei denselben schwierigen Inputs, weil das Problem selbst schwierig war, nicht weil die Programmierer nachlässig waren.

Das ist der „Schwierigkeitsfaktor“. Wenn ein Input an einer Boundary Condition liegt, knifflige Floating-Point-Mathematik erfordert oder einen unterspezifizierten State Transition beinhaltet, neigen unabhängige Teams dazu, an derselben Stelle zu scheitern. Ihre Lösungen unterscheiden sich. Ihre Failure Regions überlappen sich.

Der zweite Mechanismus sind geteilte mentale Modelle. Programmierer, die denselben Lehrplan durchlaufen haben, wenden dieselben Heuristiken an. Sie greifen zu demselben Sortieralgorithmus, demselben Defensive-Copy-Muster, demselben Epsilon-Vergleich für Floating-Point-Gleichheit. Wenn diese gemeinsame Default-Einstellung für das vorliegende Problem falsch ist, marschiert jedes Team über dieselbe Klippe.

NASAs eigenes Software Safety Guidebook erkannte dies schließlich ausdrücklich an. Es stellt fest, dass „viele Fachleute N-Version Programming als ineffektiv oder sogar kontraproduktiv betrachten“. In einer NASA-Studie zu einem Experimentalflugzeug stammte jedes einzelne während des Tests gefundene Software-Problem aus dem Redundancy Management System. Die primäre Flugsteuerungssoftware war makellos. Die N-Version-Schicht war das Einzige, das ausfiel.

Die Komplexitätssteuer, über die niemand spricht

Selbst wenn N-Version Programming so funktionieren würde wie beworben, fordert es einen hohen Preis. Man bezahlt drei vollständige Implementierungen. Man bezahlt einen Voter, der selbst Logik enthält. Man bezahlt den operativen Overhead, drei Prozesse laufen zu lassen und ihre Outputs abzugleichen.

Dieser Voter ist kein triviales Stück Code. Er muss Ties, Timeouts, divergierende Output-Formate und den Fall behandeln, in dem die Mehrheit falsch liegt. Brunelle und Eckhardt demonstrierten dies 1985 mit dem SIFT-Betriebssystem. Zwei neue N-Versionen überstimmten die ursprüngliche, korrekte Implementierung und lieferten eine falsche Antwort. Das Redundanzsystem erzeugte den Fehler, den es verhindern sollte.

Die Komplexität potenziert sich auf Weisen, die schwer messbar sind, bis sie einen beißen. Man hat nun drei Deployables zu versionieren, drei Testmatrizen zu pflegen und drei Teams, die Spezifikationsänderungen unterschiedlich interpretieren werden, wenn sich die Anforderungen unvermeidlich ändern. Die Oberfläche für menschlichen Fehler wächst schneller als sich die Zuverlässigkeit verbessert.

Was stattdessen tatsächlich funktioniert

NASA gab Fehlertoleranz nicht auf. Sie gaben die spezifische Annahme auf, dass Diversität aus Unabhängigkeit entsteht. Moderne High-Assurance-Systeme verwenden eine Kombination von Techniken, die die tatsächlichen Failure Modes adressieren, die Knight und Leveson identifizierten.

Formal Methods für den kritischen Pfad. Anstatt zu hoffen, dass drei Teams eine Spezifikation korrekt interpretieren, schreibt man die Spezifikation in einer maschinell prüfbaren Form. Tools wie TLA+, Coq und SPIN verifizieren, dass ein Design seine Invarianten erfüllt, bevor jemand eine Zeile Implementierungscode schreibt. NASAs eigenes Remote Agent Experiment nutzte SPIN, um Concurrency Bugs zu finden, die umfangreiche Tests durchschlüpft waren.

Diversität durch unterschiedliche Technologie-Stacks. Wenn man Redundanz braucht, muss sie tief gehen. Das Flugsteuerungssystem des Airbus A330 verwendet unterschiedliche Hardware-Architekturen, verschiedene Programmiersprachen und unabhängige Compiler für seine primären und Backup-Kanäle. Das Ziel ist nicht nur unabhängige Teams, sondern unabhängige Failure Modes auf jeder Ebene des Stacks.

Vereinfachung statt Duplizierung. Das NASA Software Safety Guidebook empfiehlt N-Version Programming letztlich nur für „kleine, einfache Funktionen“. Die Lektion ist unglamourös, aber effektiv: Das zuverlässigste System ist dasjenige, das einfach genug ist, um direkt darüber zu reasonen. Jede Zeile Redundancy-Management-Code ist eine Zeile, die versagen kann.

Hier ist, wie ein vereinfachter, spezifikationsgetriebener Ansatz in der Praxis aussieht. Anstatt drei undurchsichtige Implementierungen über Outputs abstimmen zu lassen, codiert man die kritische Invariante explizit und prüft sie zur Runtime:

from dataclasses import dataclass
from typing import Optional

@dataclass(frozen=True)
class LaunchCommand:
    thrust_level: float  # 0.0 to 1.0
    abort_flag: bool
    
    def is_valid(self) -> bool:
        """Runtime invariant check derived from the formal spec."""
        if not (0.0 <= self.thrust_level <= 1.0):
            return False
        if self.abort_flag and self.thrust_level > 0.0:
            return False
        return True

def execute_command(cmd: LaunchCommand) -> Optional[str]:
    if not cmd.is_valid():
        raise ValueError("Invariant violation: command violates safety spec")
    
    # Execute only after the single, explicit check passes.
    return f"Executing thrust={cmd.thrust_level}, abort={cmd.abort_flag}"

Das ist kein N-Version Programming. Es ist eine Implementierung mit einer expliziten, testbaren Safety Boundary. Die Boundary ist es, wohin die Anstrengung geht. Nicht in die Hoffnung, dass zwei von drei Teams es richtig gemacht haben.

Das moderne Echo

Das Paper von Knight und Leveson aus dem Jahr 1986 trägt eine Warnung, die jedes Jahr relevanter wird. Korrelierte Fehler erfordern keine korrelierten Teams. Sie erfordern korrelierte Inputs und korrelierte Schwierigkeit. Mit der Verbreitung von KI-gestützten Coding-Tools führen wir ein neues N-Version-Experiment im planetaren Maßstab durch. Auf überlappenden Korpora trainierte Modelle, die mit ähnlichen Patterns gepromptet werden, produzieren Code mit geteilten Failure Modes.

Neuere Forschung zur LLM-Code-Generierung deutet auf Co-Error-Raten zwischen 15 % und 30 % für KI-generierte Komponenten hin. Der Beta-Faktor, der Anteil der Fehler, die auf Common Cause zurückzuführen sind, könnte die von Knight und Leveson gemessenen Werte menschlicher Programmierer bereits übertreffen. Wir wiederholen das Experiment mit mehr Teilnehmern und derselben fehlerhaften Annahme.

NASA lernte diese Lektion auf die teure Art. Man braucht nicht drei Implementierungen. Man braucht eine Implementierung mit einer Invariante, die man formulieren, prüfen und vertrauen kann. Alles andere ist Optimismus, als Engineering verkleidet.

FAQ

Was ist N-Version Programming?

N-Version Programming ist eine Software-Fehlertoleranztechnik, bei der mehrere unabhängige Teams dieselbe Spezifikation implementieren. Die Programme laufen parallel, und ein Voter wählt den Majority Output aus, in der Annahme, dass unabhängige Teams unabhängige Fehler machen.

Hat NASA N-Version Programming tatsächlich eingesetzt?

Ja. NASA finanzierte die Frühforschung zu Multi-Version-Software, und in einigen Policy-Dokumenten wurde N-Version Programming faktisch für fehlertolerante Systeme vorgeschrieben. Die Engine-Power-up-Sequenz des Space Shuttles nutzte sie, obwohl NASA ihre Verwendung später auf kleine, einfache Funktionen einschränkte, nachdem empirische Studien ihre Grenzen offenbarten.

Was war das Knight-Leveson-Experiment?

Im Jahr 1986 ließen John Knight und Nancy Leveson siebenundzwanzig Programmierer unabhängige Implementierungen einer Raketenabfangjäger-Spezifikation schreiben. Bei über einer Million Tests zeigten die Programme deutlich mehr coincident failures, als statistische Unabhängigkeit vorhersagen würde, und widerlegten damit die Kernannahme, die N-Version Programming zugrunde liegt.

Lohnt sich N-Version Programming jemals?

In begrenzten Fällen. Die aktuelle NASA-Richtlinie legt nahe, dass es für kleine, klar definierte Funktionen angemessen sein kann, in denen echte Diversität durchgesetzt werden kann. Für die allgemeine Anwendungsentwicklung überwiegen die Komplexitätskosten und das korrelierte Fehlerrisiko den Nutzen. Formal Methods, Runtime-Invariant-Checking und Vereinfachung sind in der Regel bessere Investitionen.