
Erkennt vom Compiler erzeugte Speicherzugriffe, die sicheres C in TOCTOU-Schwachstellen verwandeln. Umfasst automatisierte Quellcode-Audits, Unicorn-basierte Binäranalyse sowie Compiler-/Architektur-/Flag-Sweeps über 100+ Projekte.
"...die Definition eines 'vernünftigen Compilers' wird immer lockerer."
Die Binärdatei, die du ausführst, ist nicht das Programm, das du geschrieben hast. Der Compiler-Optimierer schreibt
deinen Quellcode auf Arten um, die du nie siehst — und einige dieser Änderungen können
still und legal scheinbar sicheren Code in
verwundbare Binärdateien verwandeln. Dieselbe Zeile kann
unter einem Compiler sicher und unter einem anderen ausnutzbar sein,
und nichts im Quellcode verrät dir, welche der beiden Möglichkeiten zutrifft: eine Schwachstelle in
Superposition, die erst beim Erstellen kollabiert. Schrödinger's TOCTOU untersucht
vom Compiler erfundene Loads und ihre weitreichenden
Auswirkungen auf Time-of-Check-to-Time-of-Use (TOCTOU)-Schwachstellen — zu finden
in Open-Source-Kernels,
Hypervisoren,
Enklaven,
Firmware
und Bibliotheken.
Überall, wo wir hinschauen, bleibt scheinbar sicherer Code den Launen des
Compilers ausgesetzt. Aber das ist eine Stichprobe, keine Grenze; die
gleichen Fehler sind sehr wahrscheinlich auch in deinem Code.
"Fang mit etwas Einfachem an."
Wie oft lädt diese Funktion *p?```c
unsigned int g(unsigned short *p)
{
short t = p; / copy *p into a local for safekeeping */
return (unsigned short)t - t;
}
Hinweis: Die Antwort ist 1 — der Quellcode lädt `*p` genau einmal in `t`.
Füge es in [Compiler Explorer](https://godbolt.org/z/c5K9P4dPd) ein (`arm gcc 14.2.0`,
`-O2`) und zähle die Ladevorgänge aus `r0`, das `p` enthält:```asm
g:
ldrh r2, [r0] # load *p, once
ldrsh r0, [r0] # load *p, twice
subs r0, r2, r0
bx lr
Ein Load im Quellcode, zwei im Binary. Der zweite ist ein erfundener Load — ein Lesevorgang, den der Compiler erzeugt hat und den du nie geschrieben hast. Er ist unter der C abstrakten Maschine legal, die annimmt, dass sich Speicher zwischen zwei Lesevorgängen nicht ändern kann. Aber wenn dieser Speicher für Angreifer beschreibbar ist, wird die Annahme zu einem Exploit: Der erfundene Load kann nach einer Sicherheitsprüfung fallen und so ein Zeitfenster zwischen Prüfung und Verwendung (Time-of-Check to Time-of-Use, TOCTOU) lautlos wieder öffnen, von dem der Programmierer glaubte, es geschlossen zu haben. Der Wert, den du validiert hast, und der Wert, den du verwendest, sind nicht mehr garantiert identisch — obwohl du nie Code geschrieben hast, der ihn erneut liest.
Die Challenge beweist, dass der erfundene Load existiert; schauen wir uns an, wie daraus Speicherkorruption wird.
Bei einer TOCTOU-Schwachstelle prüft ein Programm, ob ein Wert sicher ist, und verwendet dann den Wert. Jedoch existiert ein Fenster für Ausbeutung, wenn ein Angreifer den Wert in dem winzigen Zeitraum zwischen diesen beiden Lesevorgängen ändern kann – der harmlose Wert besteht die Prüfung, während der gefährliche verwendet wird:```c if (shared->len <= 20) // CHECK reads shared->len // ** attacker modifies shared->len ** memcpy(out, shared->data, shared->len); // USE reads it again: buffer overflow
Der Lehrbuch-Fix besteht darin, **zuerst einen Snapshot zu erstellen**: Kopiere alle Daten, mit denen ein Angreifer manipulieren könnte, in einen lokalen Bereich, den der Angreifer nicht erreichen kann, und vertraue anschließend nichts außer diesem lokalen Bereich. Sobald `len` in einem lokalen Bereich liegt, ist es eingefroren — ein Angreifer, der mit dem gemeinsamen Speicher um die Wette läuft, kann es nicht mehr berühren —, sodass die Prüfung und die Kopie garantiert denselben Wert sehen. Genau so behebt der Code in `receive` unten das TOCTOU-Problem: Er erstellt einen Snapshot der Nachricht, validiert den Snapshot und veröffentlicht die validierte Kopie in `slot`, damit ein Consumer sie weiterleiten kann:```c
#include <string.h>
struct message {
int len; /* payload length */
char data[20]; /* payload */
};
struct message slot; /* the most recently validated message */
char out[20]; /* fixed 20-byte destination */
void receive(struct message *shared) {
struct message local = *shared; /* 1. snapshot untrusted input */
if (local.len <= 20) /* 2. validate the snapshot */
slot = local; /* 3. publish the validated copy */
}
void forward(void) { /* the time of use, later */
memcpy(out, slot.data, slot.len); /* slot.len was checked <= 20 ... right? */
}
Dem Quellcode nach ist das korrekt. len wird genau einmal gelesen — in den Snapshot —, sodass der Wert, der die <= 20-Prüfung erfüllt, der Wert ist, der in slot veröffentlicht wird. Das TOCTOU-Fenster ist geschlossen und der Code ist sicher.
Nur ist er das nicht. Unter x86-64 gcc -O2 liest receive es zweimal aus dem ursprünglichen Shared Memory: einmal als Skalar, um die Prüfung zu steuern, und erneut als Teil der Bulk-Kopie, die in slot veröffentlicht wird:```nasm
receive:
cmp DWORD PTR [rdi], 20 ; READ #1: the CHECK reads shared->len directly
movdqu xmm0, XMMWORD PTR [rdi] ; READ #2: the bulk copy re-reads it (len is byte 0)
mov rax, QWORD PTR [rdi+16] ; (the bulk copy's tail: struct bytes 16-23)
jg .L1 ; len > 20? skip the publish
mov QWORD PTR slot[rip+16], rax ; (publish that tail)
movaps XMMWORD PTR slot[rip], xmm0 ; and publish the TOCTOU-vulnerable snapshot
.L1:
ret
forward:
movsx rdx, DWORD PTR slot[rip] ; copy size = slot.len, the unchecked READ #2 value
mov esi, OFFSET FLAT:slot+4 ; src = slot.data
mov edi, OFFSET FLAT:out ; dst = out[20]
jmp memcpy ; copies slot.len bytes into out[20]
Die Prüfung läuft bei READ #1; der Wert, der in `slot.len` landet, ist READ #2. Ein
Angreifer, der `len` zwischen diesen beiden dreht, übergibt einen sicheren Wert an die `<= 20`-Prüfung
während ein übermäßig großer Wert in `slot` veröffentlicht wird – und `forward` dann so viele
Bytes in `out[20]` kopiert, genau der Overflow, den der Snapshot verhindern sollte,
durch den Optimierer wieder eingeführt.
Dies wird in
[`poc/example.c`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/poc/example.c), wobei der Code den kanonischen
TOCTOU-gehärteten Ansatz verwendet: Eine nicht vertrauenswürdige `message`-Struktur wird per Snapshot in
`local` kopiert, sodass sie nicht modifiziert werden kann; das `local.len` des Snapshots wird validiert
gegen die Pufferkapazität, und nur die validierte Kopie wird in
`slot` veröffentlicht; ein Konsument kopiert später `slot.len` Nutzdatenbytes in einen festen Puffer.
Gleichzeitig konkurriert ein Angreifer mit `shared->len`. Ein unerwarteter, erfundener Load
des Compilers liest `shared->len` für die Massenveröffentlichung erneut, sodass `slot.len`
den übergroßen Wert des Angreifers trägt, obwohl die Prüfung bestanden wurde –
was den TOCTOU, gegen den sich der Programmierer verteidigen wollte, wieder einführt, und
einen scheinbar unmöglichen Pufferüberlauf aus dem Nichts erzeugt.
## Ursache
> *Wenn C Maschinencode erreicht, wurde es durch Frontend-Lowering,
> IR-Optimierungen, Registerallokation und Backend-Codegen – eine tiefe, mehrstufige
> Pipeline, die Entscheidungen trifft, die du nicht sehen kannst. Es gibt keine einzelne Stufe, die man beschuldigen kann. Der
> erfundene Load ist eine emergente Eigenschaft der gesamten Pipeline, kein Bug in irgendeinem
> Teil davon.*
An diesem Punkt: Compiler *können* erfundene Loads emittieren, und genau das Idiom, das
den Bug verhindern sollte – Snapshot, Validieren, Verwenden – ist das, was ihn wieder einführt. Der nächste
Schritt (um zu wissen, ob wir tatsächlich verwundbar sind) ist zu charakterisieren, *wann* es
passiert. Es stellt sich heraus, dass das schwierig ist.
In [`cat-states/`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/cat-states/) suchen wir nach den Proofs-of-Concept, die zeigen,
dass es real ist – und dass es überall vorkommt:
| Mechanismus | Toolchains | Ziele |
|---|---|---|
| [**Rematerialization**](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/cat-states/README.md#rematerialization-class-1) | GCC, Clang, ICX, ICC, MSVC | x86-64, i386, m68k, VAX, MSP430 |
| [**Width-mismatch reload**](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/cat-states/README.md#width-mismatch-reload-class-2) | GCC | ARM, MIPS, MIPS64, RV64, s390x |
| [**Bulk-vs-scalar overlap**](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/cat-states/README.md#bulk-vs-scalar-overlap-class-3) | GCC, Clang, ICX, MSVC | x86-64, ARM, AArch64, AVR, Xtensa, SPARC, PPC64, s390x, MIPS64, RV64, m68k, MSP430, VAX, HPPA |
| [**Cross-class reload**](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/cat-states/README.md#cross-class-reload-class-4) | GCC | x86-64, s390x |
| [**CISC mem-op fold**](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/cat-states/README.md#cisc-alu-mem-op-fold-class-7) | GCC, Clang | m68k, MSP430, s390x, VAX, 6502 |
| [**Byte-order reload**](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/cat-states/README.md#byte-order-divergent-reload-class-8) | GCC | s390x |
Jedes der obigen PoCs verankert einen einzelnen Punkt, an dem der Load *erscheinen kann*;
[`alpha-lab/`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/alpha-lab/) kartiert den Raum darum herum, um herauszufinden, wo die Grenzen
liegen – eine dreistufige Pipeline, die von einer einzigen `.c`-Datei angetrieben wird.
Der [Matrix-Runner](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/alpha-lab/matrix_runner.py) überstreicht die Compiler ×
Architektur × Flags-Matrix auf [Compiler Explorer](https://godbolt.org);
der [Load-Detektor](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/alpha-lab/detect.py) führt jedes resultierende Binary unter
[Unicorn](https://www.unicorn-engine.org/) aus und fängt jedes Byte, das zweimal gelesen wird; und
der [Flag-Minimierer](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/alpha-lab/flag_search.py) delta-debuggt jeden Treffer auf die
minimale Flag-Menge, die einen sicheren Build in einen Doppel-Lese-TOCTOU kippt.
**Das Ergebnis**: Kein einzelner Compiler, kein einzelnes Flag und kein einzelner Pass ist schuld – das Doppel-Lesen
entsteht aus dem komplexen Zusammenspiel vieler Compiler-Ebenen, die jeweils
lokal gültige Entscheidungen treffen. Der Effekt ist nichtlinear: Kleine Änderungen an Quelle,
Flags oder Ziel können zu [unterschiedlichen Ergebnissen führen](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-imagemagick-7.1.2-25.md#candidate-1--readsunimage-sun_infolength-alloc-vs-copy). Der einzige zuverlässige Weg,
um zu wissen, ob eine bestimmte Zeile verwundbar ist, ist [**sie zu kompilieren und nachzusehen.**](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/alpha-lab/README.md#same-source-different-outcome)
**Die Katze lebt – und sie ist es nicht.** Bis du baust, ist eine Aufrufstelle, die
eine lokale Kopie per Snapshot sichert, validiert und verwendet, *weder* sicher *noch* verwundbar –
sie ist beides, und der Compiler, seine Version, das Ziel und die Flags entscheiden,
was davon. Der Build ist die Messung, und er kollabiert die Superposition auf die eine
oder andere Weise. Dies ist eine **Schrödinger-TOCTOU**: eine Prüfung eines Werts, den der
Programmierer für eingefroren hielt, den der C-Standard dem Compiler stillschweigend erlaubt,
erneut aus angreiferkontrolliertem Speicher zu lesen. Die Kiste bleibt geschlossen, bis jemand,
irgendwo eine Toolchain auswählt und sie öffnet.
## Auswirkung
> *Das Muster taucht fast überall auf – verwoben in den sorgfältigsten
> überprüften Code der Welt durch einfaches idiomatisches C.*
Das Problem ist **praktisch unlösbar**. Derselbe Code-Ausschnitt kann
verwundbar oder nicht verwundbar sein, abhängig von der genauen Kombination aus Compiler ×
Version × Architektur × Flags – und es gibt mehr solcher Kombinationen, als es
Atome im beobachtbaren Universum gibt. Es einzugrenzen, selbst für nur eine Codebasis, ist
eine nahezu hoffnungslose Suche; es über das gesamte Ökosystem zu tun, ist weitaus schlimmer.
Selbst zu entscheiden, ob eine *einzelne* Aufrufstelle sicher ist, widersetzt sich der Inspektion: eine
mögliche Barriere wie das `copy_from_user` des Kernels
[schließt den Bug erst aus](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/README.md#analysis)
nachdem etwa sechs Ebenen aus Inlining, Makros und `CONFIG`/CPU-Feature-Verzweigungen in
einem undurchsichtigen `asm` enden – und dieselbe Quellzeile ist in anderen
Konfigurationen überhaupt keine Barriere. Die Aufruf*stelle* zu lesen, sagt uns nichts.
Der einzige Weg nach vorn ist Automatisierung. Eine heuristikbasierte Analyse wurde über
prominente Open-Source-Ziele ausgeführt – Hypervisoren, TEE/Enclave-Laufzeiten, Firmware,
Kernel-Subsysteme, Protokollbibliotheken – und fand **300+ Schrödinger-TOCTOUs**
in **100+ sicherheitskritischen Projekten**: Stellen, an denen der C-Standard dem Compiler *erlaubt*,
angreiferbeschreibbaren Speicher zwischen einer Prüfung und ihrer Verwendung erneut zu lesen.
Die automatisierte Analyse identifiziert die Vertrauensgrenzen, sucht nach dem
Schrödinger-Muster und bewertet Wahrscheinlichkeit/Auswirkung/Risiko.
Die Ergebnisse zeigen, dass scheinbar harmlose, vom Compiler erfundene Loads leicht zu
verheerenden Konsequenzen führen können.
Der Compiler erfindet nicht so sehr einen *Load* als vielmehr die *Fähigkeit*, die dieser Load einem
Angreifer in die Hand gibt:
---
- **vom Compiler erfundener VM-Escape** — [QEMU](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-qemu-v11.0.1.md#candidate-1--ahci-prdtl-highest-impact), [Xen](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-xen-ptwalk-RELEASE-4.21.1.md#candidate-1--guest_walk_tables-pte-walk), [bhyve](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-bhyve-release-15.0.0.md#candidate-1--ahci-prdt-byte-count-write-path-oob-write), [KVM](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-linux-v7.0-kvm-host.md#candidate-1--svm-nested-vmcb12-save-area-cache-flagship), [ACRN](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-acrn-v3.3.md#candidate-1--nested-ept-shadow-walk)
- **vom Compiler erfundener Root** — [siw](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-linux-v7.0-rdma-rxe-siw.md#candidate-1--siw-siw_rqe_get-num_sge-headline), [VMBus](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-linux-v7.0-hyperv-vmbus.md#candidate-2--msgtype-dispatch-index), [systemd](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-systemd-v260.md#candidate-1--sd_journal_enumerate_fields-sz-field-payload-size-alloc-vs-copy), [af-packet](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-linux-v7.0-af-packet.md#candidate-1--tp_len-tx-packet-length), [snd-pcm](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-linux-snd-pcm-v7.0.md#candidate-1--snd_pcm_indirect_playback_transfer-appl_ptr-snapshot-used-for-diff-and-stored-baseline), [seL4](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-sel4-15.0.0.md#candidate-1-flagship--untyped-retype-object-window)
- **vom Compiler erfundene Plattform-Persistenz** — [edk2](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-edk2-edk2-stable202605.md#candidate-1--smmlockboxrestore), [coreboot](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-coreboot-26.03.md#candidate-1--smmstore_rawread_region-bufsize--com-buffer-mapping-overflow), [U-Boot](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-u-boot-v2026.04.md#candidate-1--virtqueue_get_buf-used-ring-id-primary), [OpenSBI](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-opensbi-v1.8.1.md#candidate-1--dbtr-update-trigger-index-primary)
- **vom Compiler erfundener Enclave-Einbruch** — [SGX](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-intel-sgx-sdk-sgx_2.29.md#candidate-1--generated-ecall-ininout-copy-in-headline-structural), [Keystone](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-keystone-master-88c49ee.md#candidate-1--edge_call_get_ptr_from_offset--edge_call_ret_ptr-host-written-return-offsetsize), [OpenEnclave](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-openenclave-v0.19.15.md#candidate-1--sgx-ecall-context-ocall-buffer), [OP-TEE](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-optee-os-4.10.0.md#candidate-1--register_shm-raw-tmem-reads)
---
Jeder einzelne dieser Fälle kann für sich genommen katastrophal sein, aber die Breite ist es, was beunruhigt:
Dasselbe Muster taucht überall dort auf, wo die Analyse hinschaut, in Code, der nichts
gemeinsam hat außer dem Idiom:
| Ziel | Stelle | Auswirkung |
|---|---|---|
| **QEMU** | [`ahci_populate_sglist`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-qemu-v11.0.1.md#candidate-1--ahci-prdtl-highest-impact) | AHCI-PRDT-Länge des Gasts einmal verriegelt → **OOB-Read / angreifer-gesteuertes Host-DMA** |
| **Linux / RDMA** | [`siw_rqe_get`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-linux-v7.0-rdma-rxe-siw.md#candidate-1--siw-siw_rqe_get-num_sge-headline) | Software-RDMA-`num_sge` wiederverwendet → **Kernel-OOB-Write** |
| **edk2 / UEFI** | [`SmmLockBoxRestore`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-edk2-edk2-stable202605.md#candidate-1--smmlockboxrestore) | SMM-Pufferlänge wiederverwendet → **OOB-Write in SMRAM** (Ring -2) |
| **TPM 2.0** | [`CryptParameterDecryption`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-ms-tpm-20-ref-v1.83r1.md#candidate-1--cryptparameterdecryption-in-place-decrypt-length) | Länge der In-Place-Entschlüsselung wiederverwendet → **OOB-Write in der TPM-Vertrauenswurzel** |
| **seL4** | [`decodeUntypedInvocation`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-sel4-15.0.0.md#candidate-1-flagship--untyped-retype-object-window) | Retype-Objektfenster wiederverwendet → **Kernel-Kompromittierung** |
| **Xen** | [`guest_walk_tables`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-xen-ptwalk-RELEASE-4.21.1.md#86-per-candidate-finding) | Gast-PTE beim Walk wiederverwendet → **Privilegieneskalation** |
| **SGX** | [edger8r-ECALL-Brücke](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-intel-sgx-sdk-sgx_2.29.md#candidate-1--generated-ecall-ininout-copy-in-headline-structural) | `[in]`/`[in,out]`-Länge für `malloc`/`memcpy_s` wiederverwendet → **Enclave-Heap-Overflow** (bei jedem ECALL) |
| **ARM TF-A** | [`spmc_ffa_fill_desc`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-tf-a-v2.15.0.md#candidate-1--spmc-ffa_mem_sharelend-send-path-primary-could--yes) | FF-A-Deskriptorfeld zur Dimensionierung von `memcpy` wiederverwendet → **Heap-Overflow im EL3-Secure-Monitor** |
| **Linux / Hyper-V** | [Hyper-V-VMBus `__vmbus_on_msg_dpc`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-linux-v7.0-hyperv-vmbus.md#candidate-2--msgtype-dispatch-index) | Host-`msgtype` zur Indizierung der Handler-Tabelle wiederverwendet → **wilder indirekter Aufruf** im Gast-Kernel |
| **U-Boot** | [`virtqueue_get_buf`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-u-boot-v2026.04.md#candidate-1--virtqueue_get_buf-used-ring-id-primary) | Virtio-used-ring-`id` als Array-Index wiederverwendet → **Heap-OOB-Read/Write** im Bootloader |
| **glibc** | [`_dl_check_map_versions`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-glibc-glibc-2.42.md#candidate-1--_dl_check_map_versions-verneed-version-index-write) | VERNEED-Versionsindex des dynamischen Loaders als Schreib-Subscript wiederverwendet → **OOB-Write in `ld.so`** beim Mapping einer präparierten Shared Library |
| **systemd** | [`sd_journal_enumerate_fields`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-systemd-v260.md#candidate-1--sd_journal_enumerate_fields-sz-field-payload-size-alloc-vs-copy) | Journal-Feldgröße über Allok/Kopie wiederverwendet → **Heap-OOB-Write** in `journalctl`/`coredumpctl` (häufig als Root) |
| **git** | [`read_table_of_contents`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-git-v2.54.0.md#candidate-1--read_table_of_contents-chunk-offset-to-start-pointer) | Object-Store-Chunk-Offset als Chunk-Basis/-Größe wiederverwendet → **OOB-Read** beim Parsen eines präparierten `.idx` / Multi-Pack-Index / Commit-Graph (geteiltes Repo / Forge-Backend) |
| **SQLite** | [`btreeComputeFreeSpace`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-sqlite-version-3.53.2.md#candidate-1--btreecomputefreespace-freeblock-offset-pc-→-data-index) | B-Tree-Freeblock-Offset als Seitenindex wiederverwendet → **OOB-Read einer `mmap`'ten Datenbankseite** |
| **FreeType** | [`ft_var_readpackedpoints`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-freetype-VER-2-14-3.md#candidate-1--ft_var_readpackedpoints-gvar-packed-point-count-n) | Gepackte Punktanzahl der Variable-Font wiederverwendet → **Heap-OOB-Write** beim Rendern einer präparierten Schriftart (allgegenwärtig: Android / Chrome / Desktop) |
| **libtiff** | [`NeXTDecode`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-libtiff-v4.7.1.md#candidate-1--nextdecode-literalspan-off--n-controlled-oob-write) | NeXT-RLE-Span-Offset/-Länge wiederverwendet → **Heap-OOB-Write** beim Dekodieren eines präparierten TIFF (Standard-`mmap`-Lesemodus) |
| **binutils / ld** | [`sframe_decode`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-binutils-binutils-2_46_1.md#candidate-1--sframe_decode-sfh_num_fdes-fde-table-alloc-vs-fill) | SFrame-FDE-Anzahl als Allokationsgröße **und** Füllgrenze wiederverwendet → **Heap-OOB-Write im Linker** bei einem präparierten Objekt |
| **ClamAV** | [`autoit` EA05 `csize`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-clamav-clamav-1.5.2.md#candidate-1--autoit-ea05-csize-alloc-vs-fill-heap-oob-write) | AutoIt-`csize` als Allokationsgröße **und** Kopierlänge wiederverwendet → **Heap-OOB-Write** im Scanner |
| **YARA** | [`pe_parse_exports`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-yara-v4.5.7.md#candidate-1--pe_parse_exports-number_of_exports-loop-bound) | PE-Exportanzahl als Schleifengrenze wiederverwendet → **OOB-Read** beim Scannen einer präparierten Probe |
| **WAMR** | [`_vprintf_wa`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-wamr-WAMR-2.4.4.md#candidate-1--_vprintf_wa-s-handler-s_offset-string-address-rematerialization) | Gast-`%s`-Offset jenseits der Sandbox-Arena erneut gelesen → **OOB-Read, der Host-Speicher an den Wasm-Gast leakt** |
| **ImageMagick** | [`ReadSUNImage`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-imagemagick-7.1.2-25.md#candidate-1--readsunimage-sun_infolength-alloc-vs-copy) | SUN-Raster-Länge als Allokationsgröße **und** Kopierlänge wiederverwendet → **Heap-OOB-Write → RCE** beim Dekodieren eines präparierten Bildes (LTO-Builds) |
| **FreeBSD** | [`virtqueue_dequeue`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-freebsd-drivers-release-15.0.0.md#candidate-1--virtqueue_dequeue-used-ring-desc_idx) | Vom Host geschriebene Virtio-used-ring-`id` als unbegrenzter Array-Index wiederverwendet → **Deskriptor-Double-Free / UAF** im Kernel |
Alles ist verwundbar. Und alles ist es nicht. In jeder Situation tut die Quelle
das Richtige: einen Snapshot der nicht vertrauenswürdigen Eingabe erstellen, die Kopie validieren, die
Kopie verwenden. Aber in jedem Fall erlaubt der C-Standard dem Compiler stillschweigend,
diesen Prozess optional *rückgängig zu machen* und aus dem Nichts eine TOCTOU zu erzeugen. Ob eine
bestimmte Stelle ausnutzbar ist, ist *keine Eigenschaft der Quelle*: Es wird entschieden
vom Compiler, seiner Version, der Architektur und den Flags, und es kollabiert nur
auf eine Seite, wenn du baust. Bis dahin ist jede einzelne beides – eine Schwachstelle in
Superposition, auf Quelltextebene nicht unterscheidbar von Code, der wirklich
in Ordnung ist. Jede ist eine Schrödinger-TOCTOU – und die obige Tabelle zeigt, wie sie
in großem Maßstab aussehen.
Das Beunruhigende ist nicht, dass diese bestimmten Projekte fehlerhaft sind – sondern
dass das Muster fast überall dort auftaucht, wo die Analyse hinschaut, verwoben in [den
sorgfältigsten überprüften Code der Welt](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-sel4-15.0.0.md#82-executive-summary)
durch nichts anderes als idiomatisches C. Die 100+ Repositories sind eine
**Stichprobe, nicht die Grenze**: Derselbe latente Bug erreicht mit ziemlicher Sicherheit auch deine
eigene Codebasis.
Die vollständige Prüf- und Auswirkungsanalyse befindet sich in [observer-effect/](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/)
und ihrer [REPORT.md](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/REPORT.md).
## Lösungen
> *Es gibt keine.*
Aber hier sind ein paar Dinge, die wir trotzdem versuchen können.
Die Reflexlösung besteht darin, den Load zu fixieren – [`volatile`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/BARRIERS.md#i-1--volatile--read_once-access-site-latch), [`READ_ONCE`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/BARRIERS.md#i-1--volatile--read_once-access-site-latch), ein
[Atomic](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/BARRIERS.md#i-2--atomic--acquire-load), ein [`"memory"`-clobber `barrier()`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/BARRIERS.md#i-3--memory-clobber-compiler-barrier). Diese sind spezifikationskonform und überleben
`-O3`, LTO und Inlining; wo ein bloßer Read *[gefunden](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-linux-v7.0-kvm-host-sev-snp.md#candidate-1--snp_begin_psc-idx_end-loop-bound)* wurde, sind sie [der richtige
Patch](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/README.md#confirmed-in-the-wild). Leider flicken sie die Wunde, aber nicht die Ursache:
- **`volatile` wäscht stillschweigend weg.** Es [qualifiziert *den Lvalue-Zugriff*](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/cat-states/README.md#the-volatile-cat-state), nicht
das Objekt, den Zeiger oder die Region. Ein `volatile T *p`, gelesen durch einen einfachen
Lvalue, [bietet null Schutz](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/cat-states/volatile_lvalue_launder.c), und der
Qualifizierer wird **ohne Diagnose** entfernt, wenn er [durch den `const void *`
von `memcpy` geht](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/cat-states/volatile_memcpy_overlap.c) – es gibt [kein volatile-erhaltendes `memcpy`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/BARRIERS.md#false-friends--look-like-barriers-but-are-not). Die Barriere, die du
geschrieben hast, verpufft an dem Aufruf, den du nicht geschrieben hast.
- **`READ_ONCE` skaliert nicht.** „`READ_ONCE` verwenden" bedeutet wirklich: annotiere
*jeden* angreifererreichbaren Zugriff auf [*jedes* Feld](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-linux-v7.0-rdma-rxe-siw.md#candidate-1--siw-siw_rqe_get-num_sge-headline), für immer, und [positioniere einen
Zaun zwischen dem Read und *allen* seinen Verwendungen](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/BARRIERS.md#i-3--memory-clobber-compiler-barrier). [Einen verpassen](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-linux-v7.0-io_uring.md#candidate-1--nvme_uring_cmd_io-nsid) und die Disziplin ist
hinfällig. Sie [kann nicht in großem Maßstab durchgesetzt werden](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/BARRIERS.md#i-1--volatile--read_once-access-site-latch) und regrediert stillschweigend.
- **Die Korrektheit einer `barrier()` liegt mehrere Frames von der Quellzeile entfernt.** Zu entscheiden,
ob ein `copy_from_user(&local, uptr, n)` überhaupt einen `"memory"`-Clobber trägt,
bedeutet, [fünf Inline-Ebenen und einen Out-of-Line-Aufruf zu verfolgen](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/README.md#analysis) vom generischen C
bis in architekturspezifisches asm, wobei eine Handvoll `CONFIG`/CPU-Feature/`__builtin`-
Verzweigungen aufgelöst werden müssen. Und selbst wenn er gefunden ist, [nennt der Clobber keinen Read](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/BARRIERS.md#i-3--memory-clobber-compiler-barrier): Ein Schritt daneben und er [pinnt
nichts](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/BARRIERS.md#i-3--memory-clobber-compiler-barrier); ein Schritt in die andere Richtung und er [*erzwingt* genau das Reload, das er
stoppen sollte](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/BARRIERS.md#principles--shared-facts-the-cards-lean-on).
Aber was wichtiger ist: Die Quelle verlangt von vornherein nie ein Reload. Das ist
das tiefere Problem. Der Programmierer schrieb `local.len` und *meinte* `local.len`: ein
Wert, einmal gelesen. Wenn wir `x` sagen, meinen wir `x`, nicht „`x`, aber `y`, wenn der Compiler
das stattdessen mag.“ Das Reload wird unterhalb der abstrakten Maschine erfunden, also
sieht der Code, der die Annotation benötigt, [identisch aus wie der Code, der es
nicht tut](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/alpha-lab/README.md#same-source-different-outcome) – es gibt [kein
Signal an der Stelle, dass eine Barriere
erforderlich ist](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/BARRIERS.md#proposed-levers-do-not-exist-in-usable-form-today).
Du kannst dich nicht daran erinnern, einen Read abzusichern, den du nie geschrieben hast.
Der vollständige Katalog der Verteidigungen – mit ihren [Stärken](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-linux-binder-v7.0.md#executive-summary) und [Schwächen](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/audits/audit-libspdm-3.8.2.md#durability-assessment) – befindet sich im
[Barriers-Bericht](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/BARRIERS.md).
## Öffne die Kiste
> *Das TOCTOU-aus-dem-Nichts-Muster ist überall. Prüfe, ob dein Code es hat.*
Prüfe deinen eigenen Code mit
[`observer-effect/AUDIT-PROMPT.md`](https://github.com/xoreaxeaxeax/schrodingers-toctou/blob/HEAD/observer-effect/AUDIT-PROMPT.md), das
nach den Vertrauensgrenzen sucht, nach dem Schrödinger-Muster sucht, basierend auf
spezifikationskonformen Barrieren bereinigt und Wahrscheinlichkeit/Auswirkung/Risiko bewertet. Gib es deinem
bevorzugten Coding-Agenten mit deiner Quelle im Kontext und richte es auf ein Subsystem:```sh
cd ~/your-project # the codebase you want audited
claude -p "$(cat path/to/observer-effect/AUDIT-PROMPT.md)
Audit drivers/net/ for invented-load TOCTOUs."
Es hängt von nichts anderem in diesem Repo ab — kopiere die eine Datei und los geht's.
Schrödinger's TOCTOU zerlegt eine spezifische Instanziierung einer beliebigen
Optimierung, die von der 500-seitigen C-Spezifikation erlaubt wird. Aber das kratzt nur an
der Oberfläche: Es gibt noch so viel zu erkunden. Dieses Repository wird
weiterhin die unerwarteten Wege aufspüren, erfassen und katalogisieren, mit denen dein
Lieblingscompiler dich untergräbt — still, legal und auf jeder Optimierungsstufe.
"... wenn gcc das täte, würde ein Großteil des Kernels in Flammen aufgehen."
— Paul E. McKenney, LKML, 2009-04-16 · lore
"Die Leute reden gerne über 'sicheres C', aber Compiler-Leute haben jahrzehntelang aktiv versucht, C unsicherer zu machen. Das C-Standardkomitee war daran beteiligt."
— Linus Torvalds, 2025-02-21 · lore
"Ich würde es sehr bevorzugen, einen Compiler-Schalter zu haben, der den Compiler anweist, nicht solche verdammt dummen Dinge zu tun, anstatt jedes zweite load/store im Kernel mit volatile zu markieren."
— Peter Zijlstra, 2015-06-17 · lore
"Die Spezifikation ist nur so viel Klopapier. Das EINZIGE, was zählt, ist, was echte Hardware tut."
— Linus Torvalds, 2006-12-04 · lore
"Bei dieser Argumentation müssen wir die Hälfte des Kernels mit _ONCE() zupflastern … Können wir endlich ein Machtwort sprechen und den Compiler- und Standardkomitee-Leuten sagen, sie sollen diesen Wahnsinn stoppen?
— Thomas Gleixner, 2019-08-16 · lore
"Compiler, die Dinge 'optimieren', um Felder zu berühren, die vom Quellcode nicht berührt werden, sind einfach von Natur aus fehlerhafter Mist. Ich habe überhaupt kein Interesse daran, ihrem Wahnsinn entgegenzukommen ... Zu behaupten, dass sie als volatile markiert werden müssen, ist ein Symptom eines kranken Compiler-Entwicklers."
— Linus Torvalds, 2014-12-04 · lore
"Verrückt? Wahrscheinlich schon. Aber es gibt Compiler-Leute, die darauf schwören."
— Paul E. McKenney, LKML, 2008-02-04 · lore
"Es ist eine gute Sache, wenn sie alle Codepfade getestet haben, aber sie wurden ausnahmslos mit einem Compiler getestet, der sich nicht extra bemüht, "legalen, aber idiotischen" Code zu erzeugen. Daher werden die Tests im Allgemeinen keine Fälle finden, in denen der Compiler möglicherweise berechtigt war, etwas anderes zu tun. ... Compiler- Leute, die das nicht erkennen, sind keine Compiler-Leute. Sie sind Akademiker, die mit geistiger Masturbation beschäftigt."
— Linus Torvalds, LKML, 2007-01-04 · lore
"Natürlich sind es nicht die dummen Compiler, die mir Sorgen machen, sondern eher die klugen ..."
— Paul E. McKenney, LKML, 2013-10-09 · lore
"... wir hatten Compiler-Entwickler, die sagen: „Wenn du die Spezifikationen liest, ist das in Ordnung". Nein, das ist nicht in Ordnung. Denn die Realität schlägt jede fadenscheinige Spezifikationslesart."
— Linus Torvalds, LKML, 2019-08-16 · lore
"... die Definition von 'vernünftigem Compiler' wird immer lockerer."
— Paul E. McKenney, LKML, 2013-09-24 · lore
Schrödinger's TOCTOU ist ein Forschungsprojekt von Christopher Domas (@xoreaxeaxeax)
