"...the definition of 'sane compiler' grows ever looser."

The binary you run is not the program you wrote. The compiler optimizer rewrites
your source in ways you never see — and some of those changes can
silently and legally turn seemingly secure code into
vulnerable binaries. The same line can be
safe under one compiler and exploitable under another,
with nothing in the source to tell you which: a vulnerability held in
superposition, collapsed only when you build. Schrödinger's TOCTOU explores
compiler-invented loads and their widespread
implications for time-of-check to time-of-use (TOCTOU) vulnerabilities — found
across open-source
kernels,
hypervisors,
enclaves,
firmware,
and libraries.
Everywhere we look, seemingly secure code is left exposed to the whims of the
compiler. But those are a sample, not a boundary; the
same bugs are very likely in your code too.

Start with something easy.

How many times does this function load *p?

unsigned int g(unsigned short *p) { short t = *p; /* copy *p into a local for safekeeping */ return (unsigned short)t - t; }
Hint: the answer is 1 — the source loads *p a single time into t.

Paste it into Compiler Explorer (arm gcc 14.2.0,
-O2) and count the loads from r0, which holds p:

g: ldrh r2, [r0] # load *p, once ldrsh r0, [r0] # load *p, twice subs r0, r2, r0 bx lr
One load in the source, two in the binary. The second is an invented load
a read the compiler manufactured that you never wrote. It is legal under the C
abstract machine, which assumes memory cannot change between two reads. But when
that memory is attacker-writable, the assumption becomes an exploit: the
invented load can fall after a security check, silently reopening a
time-of-check to time-of-use (TOCTOU) window the programmer believed they had
closed. The value you validated and the value you use are no longer guaranteed
to be the same — even though you never wrote code that re-read it.

The challenge proves the invented load exists; let's see how that turns into memory corruption.

In a TOCTOU vulnerability, a program checks that a value is safe, then uses the
value. However, a window for exploitation exists if an attacker can change
the value in the sliver of time between those two reads – the harmless value
passes the check while the dangerous one is the one that gets used:

if (shared->len <= 20) // CHECK reads shared->len // ** attacker modifies shared->len ** memcpy(out, shared->data, shared->len); // USE reads it again: buffer overflow
The textbook fix is to snapshot first: copy any data the attacker might
tamper with into a local the attacker can't reach, and then trust nothing but
that local. Once len is in a local it is frozen — an attacker racing the
shared memory can no longer touch it — so the check and the copy are guaranteed
to see the same value. That is how the code in receive below fixes the TOCTOU:
it snapshots the message, validates the snapshot, and publishes the validated
copy into slot for a consumer to forward:

```

include

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? /
}
`` By the source, this is correct.lenis read exactly once — into the snapshot — so the value that clears the<= 20check is the value published intoslot`.
The TOCTOU window is closed and the code is safe.

Except it isn't. Under x86-64 gcc -O2, receive reads it from the
original shared memory twice: once
as a scalar to gate the check, and again as part of the bulk
copy that gets published into slot:

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]
The check runs on READ #1; the value that lands in slot.len is READ #2. An
attacker who flips len between them passes a safe value to the <= 20 check
while an oversized one is published into slot — and forward then copies that
many bytes into out[20], the exact overflow the snapshot was meant to prevent,
reintroduced by the optimizer.

This is turned into a complete proof-of-concept in
poc/example.c, where the code uses the canonical
TOCTOU-hardened approach: an untrusted message struct gets snapshotted into
local so that it cannot be modified, the snapshot's local.len is validated
against the buffer capacity, and only the validated copy is published into
slot; a consumer later copies slot.len payload bytes into a fixed buffer.
Simultaneously, an attacker races shared->len. An unexpected invented load
from the compiler re-reads shared->len for the bulk publish, so slot.len
carries the attacker's oversized value even though the check passed —
reintroducing the TOCTOU the programmer was trying to defend against, and
creating a seemingly impossible buffer overflow — from thin air.

By the time C reaches machine code, it's been reshaped by frontend lowering, IR optimizations, register allocation, and backend codegen — a deep, multi-stage pipeline making decisions you can't see. There is no one stage to blame. The invented load is an emergent property of the whole pipeline, not a bug in any part of it.

At this point: compilers can emit invented loads, and the very idiom meant to
prevent the bug — snapshot, validate, use — is what reintroduces it. The next
step (to know whether we are actually vulnerable) is to characterize when it
happens. Turns out that's hard.

In cat-states/, we search for the proofs-of-concept that show
it is real — and that it is everywhere:

| Mechanism | Toolchains | Targets |
|---|---|---|
| Rematerialization | GCC, Clang, ICX, ICC, MSVC | x86-64, i386, m68k, VAX, MSP430 |
| Width-mismatch reload | GCC | ARM, MIPS, MIPS64, RV64, s390x |
| Bulk-vs-scalar overlap | GCC, Clang, ICX, MSVC | x86-64, ARM, AArch64, AVR, Xtensa, SPARC, PPC64, s390x, MIPS64, RV64, m68k, MSP430, VAX, HPPA |
| Cross-class reload | GCC | x86-64, s390x |
| CISC mem-op fold | GCC, Clang | m68k, MSP430, s390x, VAX, 6502 |
| Byte-order reload | GCC | s390x |

Each PoC above pins down a single point where the load can appear;
alpha-lab/ charts the space around it to find where the edges
fall — a three-stage pipeline driven from a single .c file.
The matrix runner sweeps the compiler ×
architecture × flag matrix on Compiler Explorer;
the load detector runs each resulting binary under
Unicorn and catches any byte read twice; and
the flag minimizer delta-debugs each hit down to the
minimal flag set that flips a secure build into a double-read TOCTOU.

The result: no single compiler, flag, or pass is to blame — the double-read
emerges from the complex interaction of many compiler layers, each making
locally valid decisions. The effect is non-linear: small changes in source,
flags, or target can cascade into different outcomes. The only reliable way to
know whether a given line is vulnerable is to compile it and look.

The cat is alive — and it's not. Until you build, a call site that
snapshots, validates, and uses a local copy is neither safe nor vulnerable —
it is both, and the compiler, its version, the target, and the flags decide
which. The build is the measurement, and it collapses the superposition one way
or the other. This is a Schrödinger TOCTOU: a check on a value the
programmer believed frozen, that the C standard quietly permits the compiler to
re-read from attacker-controlled memory. The box stays closed until someone,
somewhere, picks a toolchain and opens it.

The pattern appears nearly everywhere — woven into the most carefully reviewed code in the world through simple idiomatic C.

The problem is virtually intractable. The same snippet of code can be
vulnerable or not vulnerable depending on the precise combination of compiler ×
version × architecture × flags — and there are more such combinations than there
are atoms in the observable universe. Bounding it for even a single codebase is
a near-hopeless search; doing it across the ecosystem is far worse.

Even deciding whether a single call site is safe resists inspection: a
possible barrier like the kernel's copy_from_user only
forecloses the bug
after ~six layers of inlining, macros, and CONFIG/CPU-feature forks bottom out
in an opaque asm — and the same source line is no barrier at all in other
configurations. Reading the call site tells us nothing.

The only path forward is automation. A heuristic-based analysis was run across
prominent open-source targets — hypervisors, TEE/enclave runtimes, firmware,
kernel subsystems, protocol libraries — and found 300+ Schrödinger TOCTOUs
across 100+ security-critical projects: sites where the C standard permits
the compiler to re-read attacker-writable memory between a check and its use.
The automated analysis identifies the trust boundaries, searches for the
Schrödinger pattern, and assesses likelihood/impact/risk.

The results show that seemingly innocuous compiler-invented loads easily cascade into devastating consequences.

The compiler doesn't invent a load so much as the capability that load hands
an attacker:

  • compiler-invented VM escape— QEMU, Xen, bhyve, KVM, ACRN
  • compiler-invented root— siw, VMBus, systemd, af-packet, snd-pcm, seL4
  • compiler-invented platform persistence— edk2, coreboot, U-Boot, OpenSBI
  • compiler-invented enclave breach— SGX, Keystone, OpenEnclave, OP-TEE

Each of these can be catastrophic on its own, but the breadth is what unsettles: the same shape turns up everywhere the analysis looks, in code that shares nothing but the idiom:

| Target | Site | Impact |
|---|---|---|
| QEMU | ahci_populate_sglist | guest AHCI PRDT length latched once → OOB read / attacker-directed host DMA |
| Linux / RDMA | siw_rqe_get | software-RDMA num_sgereused →kernel OOB write |
| edk2 / UEFI | SmmLockBoxRestore | SMM buffer length reused → OOB write into SMRAM(ring -2) |
| TPM 2.0 | CryptParameterDecryption | in-place decrypt length reused → OOB write in the TPM root-of-trust |
| seL4 | decodeUntypedInvocation | retype object-window reused → kernel compromise |
| Xen | guest_walk_tables | guest PTE reused on the walk → privilege escalation |
| SGX | edger8r ECALL bridge | [in]/[in,out]length reused formalloc/memcpy_s→enclave heap overflow(every ECALL) |
| ARM TF-A | spmc_ffa_fill_desc | FF-A descriptor field reused to size memcpy→heap overflow in the EL3 secure monitor |
| Linux / Hyper-V | Hyper-V VMBus __vmbus_on_msg_dpc | host msgtypereused to index handler table →wild indirect callin the guest kernel |
| U-Boot | virtqueue_get_buf | virtio used-ring idreused as array index →heap OOB read/writein the bootloader |
| glibc | _dl_check_map_versions | dynamic-loader VERNEED version index reused as a write subscript → OOB write in when mapping a crafted shared libraryld.so |
| systemd | sd_journal_enumerate_fields | journal field size reused across alloc/copy → heap OOB writeinjournalctl/coredumpctl(frequently root) |
| git | read_table_of_contents | object-store chunk offset reused as a chunk base/size → OOB readparsing a crafted.idx/ multi-pack-index / commit-graph (shared repo / forge backend) |
| SQLite | btreeComputeFreeSpace | B-tree freeblock offset reused as a page index → OOB read of an mmap'd database page |
| FreeType | ft_var_readpackedpoints | variable-font packed point-count reused → heap OOB writerendering a crafted font (ubiquitous: Android / Chrome / desktop) |
| libtiff | NeXTDecode | NeXT-RLE span offset/length reused → heap OOB writedecoding a crafted TIFF (defaultmmap'd read mode) |
| binutils / ld | sframe_decode | SFrame FDE count reused as alloc size andfill bound →heap OOB write in the linkeron a crafted object |
| ClamAV | autoitEA05csize | AutoIt csizereused as alloc sizeandcopy length →heap OOB writein the scanner |
| YARA | pe_parse_exports | PE export count reused as a loop bound → OOB readscanning a crafted sample |
| WAMR | _vprintf_wa | guest %soffset re-read past the sandbox arena →OOB read leaking host memory to the wasm guest |
| ImageMagick | ReadSUNImage | SUN-raster length reused as alloc size andcopy length →heap OOB write → RCEdecoding a crafted image (LTO builds) |
| FreeBSD | virtqueue_dequeue | host-written virtio used-ring idreused as an unbounded array index →descriptor double-free / UAFin the kernel |

Everything is vulnerable. And everything is not. In every situation, the source
does the right thing: snapshot the untrusted input, validate the copy, use the
copy. But in each, the C standard quietly permits the compiler to optionally
undo that process, and create a TOCTOU out of thin air. Whether a
given site is exploitable is not a property of the source: it is decided
by the compiler, its version, the architecture, the flags, and it collapses one
way only when you build. Until then each one is both — a vulnerability held in
superposition, indistinguishable at the source level from code that is genuinely
fine. Each is a Schrödinger TOCTOU — and the table above is what they look like
at scale.

The unsettling part is not that these particular projects are flawed — it is
that the pattern turns up nearly everywhere the analysis looks, woven into the
most carefully reviewed code in the world
through nothing more than idiomatic C. The 100+ repositories are a
sample, not the boundary: the same latent bug almost certainly reaches your
own codebase.

The full audit and impact analysis is in observer-effect/ and its REPORT.md.

There are none.

But here are some things we can try anyway.

The reflex solution is to try to pin the load — volatile, READ_ONCE, an
atomic, a "memory"-clobber barrier(). Those are spec-sound and survive
-O3, LTO, and inlining; where a bare read is found, they are the correct
patch. Unfortunately, they patch the wound, but not the cause:

  • volatilelaunders away silently.the lvalue access, not the object, the pointer, or the region. Avolatile T *pread through a plain lvalue gives zero protection, and the qualifier is dropped withno diagnosticwhen it passes throughmemcpy'sconst void *— there is no volatile-preservingmemcpy. The barrier you wrote evaporates at the call you didn't.
  • READ_ONCEdoesn't scale.READ_ONCE" really means: annotateeveryattacker-reachable access ofeveryfield, forever, and position a fence between the read andallof its uses. Miss one and the discipline is void. It cannot be enforced at scale, and it regresses silently.
  • ADeciding whether onebarrier()'s correctness lives frames from the source line.copy_from_user(&local, uptr, n)even carries a"memory"clobber means tracing five inlined layers and an out-of-line call from generic C into arch-specific asm, resolving a fistful ofCONFIG/CPU-feature/__builtinforks. And even once found, the clobber names no read: a step out of place it pins nothing; a step the other way itforcesthe very reload it should stop.

But more importantly: the source never asks for a reload to begin with. This is
the deeper issue. The programmer wrote local.len and meant local.len: one
value, read once. If we say x we mean x, not "x, but y if the compiler
likes that instead." The reload is invented beneath the abstract machine, so the
code that needs the annotation looks identical to the code that
doesn't — there is no
signal at the site that a barrier is
required.
You cannot remember to guard a read you never wrote.

The full catalog of defenses — with their strengths and failures — is in the barriers report.

The TOCTOU-from-thin-air pattern is everywhere. Check if your code has it.

Check your own code with
observer-effect/AUDIT-PROMPT.md, which will
look for the trust boundaries, search for the Schrödinger pattern, prune based on
spec-compliant barriers, and assess likelihood/impact/risk. Hand it to your
preferred coding agent with your source in context and point it at a subsystem:

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."
It depends on nothing else in this repo — copy the one file and go.

Schrödinger's TOCTOU dissects one specific instantiation of some random
optimization allowed by the 500-page C-specification. But it's just scratching
the surface: there is so much ground left to explore. This repository will
continue to poke, capture, and catalog the unexpected ways your favorite
compiler undercuts you — silently, legally, and at every optimization level.

"... if gcc did that, much of the kernel would go down in flames."

— Paul E. McKenney, LKML, 2009-04-16 · lore

"People love to talk about 'safe C', but compiler people have actively tried to make C unsafer for decades. The C standards committee has been complicit."

— Linus Torvalds, 2025-02-21 · lore

"I would very much prefer a compiler switch that instructs the compiler to not do bloody stupid things like this instead of marking every other load/store in the kernel with volatile."

— Peter Zijlstra, 2015-06-17 · lore

"The spec is just so much toilet paper. The ONLY thing that matters is what real hardware does."

— Linus Torvalds, 2006-12-04 · lore

"By that argumentation we need to plaster half of the kernel with _ONCE() … Can we finally put a foot down and tell compiler and standard committee people to stop this insanity?

— Thomas Gleixner, 2019-08-16 · lore

"Compilers that 'optimize' things to touch fields that aren't touched by the source code are simply inherently buggy shit. I'm not at all interested in catering to their insanity... Claiming that they need to be marked volatile is a symptom of a diseased compiler writer."

— Linus Torvalds, 2014-12-04 · lore

"Insane? Probably so. But there are compiler guys who swear by it."

— Paul E. McKenney, LKML, 2008-02-04 · lore

"It's a good thing if they havetestedall the code-paths, but they've invariably been tested with a compiler that doesn't go out of its way to try to generate "legal but idiotic" code. So the testing won't generally find cases where the compiler may have beenallowedto do something else. ... Compiler people who don't realize this aren't compiler people. They're academics involved with mental masturbation."

— Linus Torvalds, LKML, 2007-01-04 · lore

"Of course, it is not the stupid compilers that worry me, but rather the smart ones..."

— Paul E. McKenney, LKML, 2013-10-09 · lore

"... we've had compiler writers that say "if you read the specs, that's ok". No, it's not ok. Because reality trumps any weasel-spec-reading."

— Linus Torvalds, LKML, 2019-08-16 · lore

"... the definition of 'sane compiler' grows ever looser."

— Paul E. McKenney, LKML, 2013-09-24 · lore

  • Whitepaper: (coming soon)
  • Slides: (coming soon)
  • Presentation: (coming soon)

Schrödinger's TOCTOU is a research effort from Christopher Domas (@xoreaxeaxeax)