FForge
← Labsxl-03Source to assembly●●●○

Crash site vs root cause: walk the SSA def-use chain backward

cross_level/truncptr_rootcause.ll
evidence to leave with
Faulting store traced back to the trunc; valgrind names the uninitialised half in the C analogue
read the source · cross_level/truncptr_rootcause.ll
; ============================================================ ; CROSS-LEVEL LESSON 3: CRASH SITE vs ROOT CAUSE — trace backward. ; ------------------------------------------------------------ ; The instruction that SIGSEGVs is innocent. The real defect is upstream. ; Here a valid 64-bit pointer is destroyed long before it is used: ; ; ROOT CAUSE (here): %chopped = trunc i64 %addr to i32 ; keep only low 32 bits ; store i32 %chopped, ptr %slot ; write 4 bytes into slot ; ... ; CRASH SITE (here): store i32 456, ptr %wild ; deref the wrecked pointer ; ; %slot is a 4-byte (i32) alloca, but we reload it with `load ptr` -- an 8-BYTE ; load. That reads the 4 bytes we wrote PLUS 4 bytes of ADJACENT stack, so the ; high half of the rebuilt pointer is garbage. The pointer is wild -> the final ; store faults (SIGSEGV, deterministically on this box). ; ; To debug this you START at the faulting store and walk the SSA def-use chain ; BACKWARD (every value names the instruction that produced it): ; ; store i32 456, ptr %wild <- crash site; who made %wild? ; %wild = load ptr, ptr %slot <- 8-byte load from a 4-byte slot! ; store i32 %chopped, ptr %slot <- only 4 bytes were ever written ; %chopped = trunc i64 %addr to i32 <- *** ROOT CAUSE *** ; %addr = ptrtoint ptr %target to i64 ; ; TWO things this proves: ; * `opt -passes=verify` calls this VALID. Opaque `ptr` carries no width, so ; "trunc i64 to i32" then "load i64" is structurally fine. Verified != correct. ; * `valgrind --track-origins=yes` (on the native exe) reports ; "Use of uninitialised value of size 8" and traces the origin back to the ; stack slot whose high 4 bytes we never wrote — i.e. straight to the trunc. ; ; Build & run (from cross_level/, local LLVM on PATH): ; opt -passes=verify truncptr_rootcause.ll -disable-output && echo VALID ; llc -filetype=obj truncptr_rootcause.ll -o tr.o && clang tr.o -o truncptr_ir ; ./truncptr_ir ; echo exit=$? ; (REAL output for every step is in cross_level/README.md.) ; ============================================================ target triple = "x86_64-unknown-linux-gnu" define i32 @main() { entry: ; a real 4-byte object we intend to write to %target = alloca i32, align 4 store i32 123, ptr %target, align 4 ; a 4-byte (i32) slot -- too small to hold a 64-bit pointer %slot = alloca i32, align 4 ; address-of target as a 64-bit integer %addr = ptrtoint ptr %target to i64 ; *** ROOT CAUSE: keep only the low 32 bits of the address *** %chopped = trunc i64 %addr to i32 ; write those 4 bytes into the 4-byte slot (well-typed; semantically lossy) store i32 %chopped, ptr %slot, align 4 ; reload as an 8-BYTE pointer from the 4-byte slot: the high 4 bytes come from ; ADJACENT stack -> uninitialised garbage %wild = load ptr, ptr %slot, align 8 ; CRASH SITE: store through the wrecked pointer -> SIGSEGV store i32 456, ptr %wild, align 4 ret i32 0 }
◇
Finish the lab: write the evidence note

A lab is done when you can say what it showed on your machine — not when you have read it.