Featured image of post Warum lässt sich mathematisch beweisen, dass ein Programm 100 % fehlerfrei ist – ohne es ein einziges Mal auszuführen? Die Wissenschaft der formalen Verifikation

Warum lässt sich mathematisch beweisen, dass ein Programm 100 % fehlerfrei ist – ohne es ein einziges Mal auszuführen? Die Wissenschaft der formalen Verifikation

Von Dijkstras berühmtem Aphorismus und dem Satz von Rice über die Floyd-Hoare-Logik, Z3-SMT-Solver, temporale Logik und Model Checking (TLA+) bis zur Katastrophe der Ariane 5, abstrakter Interpretation (Galois-Verbindungen) und interaktiven Theorembeweisern (Coq, Lean 4, CompCert, seL4).

1. Die Grenzen des Testens und die Unentscheidbarkeit: Warum 1.000.000 bestandene Tests nichts beweisen

1.1 Dijkstras unsterblicher Leitsatz und die Explosion des Eingaberaums

Im Jahr 1969 formulierte der Turing-Preisträger Edsger W. Dijkstra auf der NATO-Konferenz für Software Engineering das berühmteste Urteil über das Testen von Software:

“Program testing can be used to show the presence of bugs, but never to show their absence!" (Programmtests können dazu benutzt werden, die Anwesenheit von Fehlern zu zeigen, aber niemals deren Abwesenheit!)

Selbst eine elementare Funktion f(x, y), die lediglich zwei vorzeichenbehaftete 64-Bit-Ganzzahlen (int64_t) entgegennimmt, besitzt eine astronomische Anzahl möglicher Eingabepaare:

$$N_{\text{inputs}} = 2^{64} \times 2^{64} = 2^{128} \approx 3{,}4028 \times 10^{38}$$

Würde ein Supercomputer eine Milliarde ($10^9$) Tests pro Sekunde ausführen, dauerte der vollständige Test aller Eingaben rund $1{,}08 \times 10^{22}$ Jahre – das 780-Milliarden-Fache des Alters unseres Universums! Kommen dynamischer Heap-Speicher, Netzwerkverzögerungen und nichtdeterministische Thread-Verschränkungen (Interleavings) hinzu, divergiert die Zahl der Ausführungspfade gegen unendlich ($\infty$).

Ein berühmtes reales Beispiel enthüllte Joshua Bloch im Jahr 2006: In der Java-Standardbibliothek (java.util.Arrays.binarySearch) schlummerte neun Jahre lang ein kritischer Integer-Overflow-Bug in der binären Suche:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public static int binarySearch(int[] a, int key) {
    int low = 0;
    int high = a.length - 1;
    while (low <= high) {
        int mid = (low + high) / 2; // ★ FATALER OVERFLOW-BUG: negativ, wenn low + high >= 2^31!
        int midVal = a[mid];
        if (midVal < key) low = mid + 1;
        else if (midVal > key) high = mid - 1;
        else return mid;
    }
    return -(low + 1);
}

Sobald die Summe low + high bei großen Arrays das Maximum einer vorzeichenbehafteten 32-Bit-Ganzzahl ($2^{31} - 1 = 2.147.483.647$) überschreitet, läuft die Addition im Zweierkomplement in eine negative Zahl über und löst sofort eine ArrayIndexOutOfBoundsException aus.

1.2 Das Halteproblem von Turing (1936) und der Satz von Rice (1953)

Warum schreiben wir dann nicht einfach ein Analyseprogramm, das den Quellcode jedes beliebigen Programms $P$ liest und exakt entscheidet, ob $P$ jemals abstürzt? Dem stehen Alan Turings Halteproblem (1936) und der Satz von Rice (1953) entgegen:

$$\text{Satz (Rice, 1953): Jede nichttriviale semantische Eigenschaft } \mathcal{S} \text{ berechenbarer Funktionen ist unentscheidbar.}$$

Kein Verifikationswerkzeug kann für beliebige Programme gleichzeitig (1) vollautomatisch terminieren, (2) korrekt sein (Soundness: keine übersehenen Fehler / Null False Negatives) und (3) vollständig sein (Completeness: keine Fehlalarme / Null False Positives).

1.3 Wie formale Verifikation den Satz von Rice überwindet: Sound Over-Approximation

Die formale Verifikation garantiert kompromisslose Korrektheit (Soundness), indem sie entweder menschliche bzw. KI-gestützte Invarianten nutzt oder eine korrekte Überapproximation (Sound Over-Approximation) $\mathcal{R}_{\text{approx}} \supseteq \mathcal{R}_{\text{exact}}$ aller erreichbaren Zustände berechnet. Gilt für die Menge der verbotenen Fehlerzustände $\mathcal{B}$:

$$\mathcal{R}_{\text{approx}} \cap \mathcal{B} = \emptyset \implies \mathcal{R}_{\text{exact}} \cap \mathcal{B} = \emptyset$$

dann ist das Programm mathematisch zu 100 % fehlerfrei über alle unendlich vielen Ausführungen hinweg!

  flowchart TD
    A["Exakte erreichbare Zustandsmenge R_exact von Programm P (Unentscheidbar nach Rice)"] --> B["Vier Säulen der Formalen Verifikation"]
    B --> C["Säule 1: Deduktive Verifikation (Floyd-Hoare-Logik & SMT)"]
    B --> D["Säule 2: Model Checking (Temporale Logik & TLA+)"]
    B --> E["Säule 3: Abstrakte Interpretation (Galois-Verbindung & Astrée)"]
    B --> F["Säule 4: Interaktive Theorembeweiser (Coq / Lean 4 / Isabelle)"]
    C --> C1["Schleifeninvarianten I + automatischer Beweis der VCs durch Z3 SMT-Solver"]
    D --> D1["Endliches Kripke-Modell M; symbolische Zustandssuche via BDDs/SAT/IC3"]
    E --> E1["Automatische Überapproximation R_exact ⊆ γ(a#) beweist γ(a#) ∩ Bad = ∅"]
    F --> F1["Curry-Howard-Isomorphismus: Mathematische Beweisterme geprüft durch Mini-Kernel"]

2. Säule 1: Deduktive Verifikation — Floyd-Hoare-Logik, Schwächste Vorbedingungen und SMT-Solver

2.1 Das Hoare-Tripel, Schleifeninvarianten und Ranking-Funktionen

1969 begründete C. A. R. Hoare aufbauend auf Robert W. Floyd die Floyd-Hoare-Logik. Ihr Grundbaustein ist das Hoare-Tripel:

$$\{P\}\; C \;\{Q\}$$
  • Partielle Korrektheit: Gilt Vorbedingung $P$ vor der Ausführung von Befehl $C$ und terminiert $C$, so gilt danach Nachbedingung $Q$.
  • Totale Korrektheit: Unter Vorbedingung $P$ terminiert $C$ garantiert, und im Endzustand gilt $Q$.

Die zentralen Schlussregeln lauten:

  1. Zuweisungsaxiom (Assignment): $\{\,Q[E/x]\,\}\; x := E \;\{\,Q\,\}$.
  2. While-Schleifenregel mit Schleifeninvariante $I$ und Ranking-Funktion $V \in \mathbb{N}$: $$\frac{\{I \land B \land V = v_0\}\; S \;\{I \land V < v_0\} \qquad (I \land B) \implies V \ge 0}{[I]\; \text{while } B \text{ do } S \;[I \land \neg B]}$$

Mit Dijkstras Kalkül der schwächsten Vorbedingungen (Weakest Precondition, $\text{wp}(C, Q)$, 1975) wird die Programmverifikation vollständig auf die Gültigkeit mathematischer Verifikationsbedingungen (Verification Conditions, VCs) $P \implies \text{wp}(C, Q)$ zurückgeführt.

2.2 Verifizierte Binärsuche in Dafny und der Z3-SMT-Solver

Wenn wir die Mittelpunktsberechnung zu mid := low + (high - low) / 2 korrigieren, beweist der Z3-SMT-Solver (entwickelt von Leonardo de Moura und Nikolaj Bjørner bei Microsoft Research, basierend auf DPLL(T) über uninterpretierte Funktionen EUF, lineare Ganzzahlarithmetik LIA, Bitvektoren $\mathcal{T}_{\text{BV}}$ und Arrays $\mathcal{T}_{\text{A}}$) in Millisekunden die vollständige Korrektheit:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
method BinarySearch(a: array<int>, key: int) returns (index: int)
  requires forall i, j :: 0 <= i < j < a.Length ==> a[i] <= a[j]
  ensures 0 <= index ==> index < a.Length && a[index] == key
  ensures index < 0 ==> forall k :: 0 <= k < a.Length ==> a[k] != key
{
  var low := 0;
  var high := a.Length;
  while low < high
    invariant 0 <= low <= high <= a.Length
    invariant forall i :: 0 <= i < a.Length && !(low <= i < high) ==> a[i] != key
    decreases high - low
  {
    var mid := low + (high - low) / 2; // Z3 beweist: kein Overflow und 0 <= mid < a.Length!
    if a[mid] < key { low := mid + 1; }
    else if key < a[mid] { high := mid; }
    else { return mid; }
  }
  return -1;
}

3. Säule 2: Model Checking und Temporale Logik — Erschöpfende Zustandssuche in verteilten Systemen

3.1 Kripke-Strukturen, LTL / CTL, Sicherheit (Safety) und Lebendigkeit (Liveness)

Für nebenläufige und verteilte Protokolle erfanden Edmund M. Clarke, E. Allen Emerson und Joseph Sifakis 1981 das Model Checking (Turing Award 2007), während Leslie Lamport TLA+ entwickelte (Turing Award 2013). Ein System wird als Kripke-Struktur $M = (S, S_0, R, L)$ modelliert und mit Linearer Temporaler Logik (LTL) oder Computation Tree Logic (CTL) (Amir Pnueli, 1977) spezifiziert:

  1. Sicherheit (Safety — „Nichts Schlimmes passiert jemals“): Gegenseitiger Ausschluss (Mutual Exclusion): $$\text{LTL}: \quad \mathbf{G}\, \neg (\text{crit}_1 \land \text{crit}_2) \qquad \text{CTL}: \quad \mathbf{AG}\, \neg (\text{crit}_1 \land \text{crit}_2)$$
  2. Lebendigkeit (Liveness — „Etwas Gutes passiert schließlich garantiert“): Freiheit von Aushungerung (Starvation Freedom): $$\text{LTL}: \quad \mathbf{G}\, (\text{req}_1 \implies \mathbf{F}\, \text{crit}_1) \qquad \text{CTL}: \quad \mathbf{AG}\, (\text{req}_1 \implies \mathbf{AF}\, \text{crit}_1)$$

3.2 Büchi-Automaten, BDDs, IC3 und TLA+ bei Amazon Web Services (AWS)

Ein LTL-Model-Checker übersetzt die negierte Spezifikation $\neg \varphi$ in einen Büchi-Automaten $A_{\neg \varphi}$ und prüft, ob das synchrone Produkt leer ist:

$$\mathcal{L}(M \times A_{\neg \varphi}) = \emptyset$$

Gegen die Zustandsexplosion (State Explosion Problem) nutzen moderne Werkzeuge Binary Decision Diagrams (BDDs, Bryant 1986 / McMillan 1990) für bis zu $10^{20}$ Zustände sowie SAT-basiertes Bounded Model Checking (BMC) und IC3 / PDR. Bei Amazon Web Services (AWS) deckte der Model Checker TLC (TLA+) in S3 und DynamoDB subtile verteilte Fehler auf, die eine exakte Sequenz von 35 verschränkten Schritten (Netzwerkpartitionen, Nachrichtenverzögerungen, Leader-Failover) erforderten – noch bevor eine einzige Zeile Produktionscode geschrieben wurde!


4. Säule 3: Abstrakte Interpretation und die Katastrophe der Ariane 5 (Flug 501)

4.1 4. Juni 1996: Warum sich die 370-Millionen-Dollar-Rakete Ariane 5 nach 37 Sekunden selbst zerstörte

Am 4. Juni 1996 explodierte die europäische Trägerrakete Ariane 5 auf ihrem Erstflug (Flug 501) nur 37 Sekunden nach dem Start – ein Schaden von 370 Millionen US-Dollar. Die Ursache war ein aus der Ariane 4 übernommenes Ada-Modul im Trägheitsnavigationssystem (SRI): Eine 64-Bit-Gleitkommavariable BH (Horizontalgeschwindigkeit) wurde in eine vorzeichenbehaftete 16-Bit-Ganzzahl ($-32.768 \dots +32.767$) konvertiert. Durch die weitaus höhere Beschleunigung der Ariane 5 überschritt BH den Wert $32.767$ und löste einen ungefangenen Hardware-Overflow-Trap aus. Sowohl das Backup- als auch das Hauptsystem schalteten sich ab und sendeten ein Diagnose-Bitmuster an den Bordcomputer, der dieses fälschlich als extreme Fluglage-Abweichung interpretierte und die Schubdüsen bis zum Anschlag auslenkte.

4.2 Abstrakte Interpretation (Cousot & Cousot 1977), Galois-Verbindungen und Astrée beim Airbus A380

Die von Patrick Cousot und Radhia Cousot 1977 begründete Abstrakte Interpretation verbindet den konkreten Zustandsverband $(\mathcal{C}, \subseteq) = (\mathcal{P}(\Sigma), \subseteq)$ über eine Galois-Verbindung (Galois Connection) mit einem abstrakten Verband $(\mathcal{A}, \sqsubseteq^\#)$ (wie Intervallen $[a,b]$, Oktagonen $\pm x \pm y \le c$ oder konvexen Polyedern):

$$(\mathcal{C}, \subseteq) \underset{\alpha}{\overset{\gamma}{\leftrightarrows}} (\mathcal{A}, \sqsubseteq^\#) \quad \text{mit} \quad \alpha(S) \sqsubseteq^\# a^\# \iff S \subseteq \gamma(a^\#)$$

Gilt die lokale Korrektheitsbedingung $\alpha(F(\gamma(a^\#))) \sqsubseteq^\# F^\#(a^\#)$, so umschließt der mittels Widening-Operator ($\nabla$) schnell berechnete abstrakte Fixpunkt $\text{lfp}\, F^\#$ garantiert alle erreichbaren Zustände:

$$\mathcal{R}_{\text{exact}} = \text{lfp}\, F \subseteq \gamma(\text{lfp}\, F^\#)$$
  flowchart TD
    subgraph Concrete["Konkreter Bereich C = P(Σ) (Unendlich / Unentscheidbar)"]
        S["Konkrete Zustandsmenge S"] -->|"Konkreter Übergang F"| FS["Folgezustände F(S)"]
        LFP_C["Erreichbare Zustände R_exact = lfp F"]
    end
    subgraph Abstract["Abstrakter Bereich A (Intervalle / Oktagone / Ellipsoide)"]
        A_elem["Abstrakter Zustand a#"] -->|"Abstrakter Übergang F#"| FA["Abstrakter Folgezustand F#(a#)"]
        LFP_A["Abstrakter Fixpunkt lfp F# (Konvergenz durch Widening ∇)"]
    end
    S -->|"Abstraktion α(S)"| A_elem
    FA -->|"Konkretisierung γ(F#(a#)) ⊇ F(S)"| FS
    LFP_A -->|"Garantierte Sicherheit: R_exact ⊆ γ(lfp F#)"| LFP_C

Mit dem statischen Analysator Astrée gelang es dem Team um Cousot, den fast 1.000.000 Zeilen umfassenden C-Flugsteuerungscode des Airbus A380 vollautomatisch und mit null Fehlalarmen (Zero False Alarms) auf die vollständige Abwesenheit jeglicher Laufzeitfehler (Overflows, Division durch Null, Array-Grenzenverletzungen, ungültige Zeiger) zu verifizieren!


5. Säule 4: Interaktive Theorembeweiser (Coq / Lean 4 / Isabelle) und Gesamtvergleich

Auf Basis des Curry-Howard-Isomorphismus (Propositionen als Typen, Beweise als Programme) schufen interaktive Theorembeweiser (Coq / Rocq, Lean 4, Isabelle/HOL) zwei Meilensteine der Informatik:

  1. CompCert (Xavier Leroy, INRIA): Ein in Coq vollständig verifizierter optimierender C-Compiler. Beim berühmten Csmith-Fuzzing-Test (PLDI 2011) wurden in GCC und LLVM über 325 Miscompilation-Bugs gefunden – im verifizierten Teil von CompCert kein einziger!
  2. seL4 (Gerwin Klein et al.): Der weltweit erste in Isabelle/HOL bis hinunter zum ARM/RISC-V-Binärcode formal verifizierte Betriebssystem-Mikrokernel.
Verifikations-ParadigmaMathematisches FundamentWerkzeuge & SprachenAutomatisierungIndustrielle ReferenzprojekteKernstärke & Trade-off
1. Deduktive VerifikationFloyd-Hoare-Logik, $\text{wp}$-Kalkül, SMT-SolverDafny, Frama-C, SPARK Ada, Verus, Z3Halbautomatisch (Invarianten + SMT)Kryptobibliotheken, Flugsicherung, AWS CedarBeweist tiefe funktionale Korrektheit direkt im Code.
2. Model CheckingKripke-Strukturen, LTL/CTL, Büchi-Automaten, BDDsTLA+ (TLC), SPIN, NuSMV, CBMCVollautomatisch (auf endlichen Modellen)AWS S3 & DynamoDB, Paxos/Raft, CPU-DesignLiefert exakte Gegenbeispiel-Traces für Nebenläufigkeits-Bugs.
3. Abstrakte InterpretationVerbände, Galois-Verbindungen $(\alpha,\gamma)$, Widening $\nabla$Astrée, Polyspace, Infer (Meta)Vollautomatisch (leitet Invarianten selbst ab)Airbus A380/A350 Fly-by-Wire, Automotive-C-CodeSkaliert auf 1.000.000+ Zeilen C-Code ohne manuelle Annotationen.
4. Interaktive TheorembeweiserCurry-Howard-Isomorphismus, Abhängige Typentheorie / HOLCoq (Rocq), Lean 4, Isabelle/HOLInteraktiv (Mensch + Taktiken + KI)CompCert C-Compiler, seL4 OS-MikrokernelUneingeschränkte Ausdruckskraft; 100 % Sicherheit durch Mini-Kernel.
comments powered by Disqus