FForge
← Labsir-02IR and the pipeline●●●○

Hand-written IR: verifier says VALID, program crashes

ir/lesson2_truncptr.ll
evidence to leave with
verify passes; native and lli runs SIGSEGV; debugify pins IR line 10
read the source · ir/lesson2_truncptr.ll
; ============================================================ ; IR LESSON 2: a pointer SILENTLY TRUNCATED through a 32-bit slot ; ------------------------------------------------------------ ; Hand-authored LLVM IR (opaque pointers, LLVM 21/23 compatible). It takes a ; valid 64-bit pointer, ptrtoint's it to i64, TRUNCATES to i32 (dropping the ; high 32 bits of the address), stores those 32 bits, reloads them, and rebuilds ; a pointer via inttoptr. On x86-64 a stack address looks like 0x00007ffff7.... ; so chopping the high half yields a bogus address -> the final store SIGSEGVs. ; ; THE LESSON (this is the whole point): ; `opt -passes=verify` calls this IR **VALID**. Verification checks STRUCTURE ; (types of each instruction's operands, dominance, well-formed CFG), not ; whether your pointer arithmetic is *sane*. With OPAQUE pointers, `ptr` ; carries no element/width type, so a 64->32 truncation of an address is just ; "an i64 truncated to i32" — perfectly well-typed. The size mismatch that ; destroys the pointer is INVISIBLE to the verifier. A verifier is not a bug ; finder; "verified" only means "well-formed". ; ; WORKFLOW (run from ir/, with the LOCAL LLVM on PATH): ; export PATH=$HOME/llvm-project/build/bin:$PATH ; llvm-as lesson2_truncptr.ll -o lesson2.bc # assemble .ll -> .bc ; opt -passes=verify lesson2_truncptr.ll -disable-output && echo VALID ; # => prints VALID (!) ; llc -filetype=obj lesson2_truncptr.ll -o lesson2.o # IR -> object ; clang lesson2.o -o lesson2_exe # link ; ./lesson2_exe ; echo "exit=$?" # => SIGSEGV, exit 139 ; lli lesson2_truncptr.ll ; echo "exit=$?" # JIT-run the IR: same SIGSEGV ; opt -passes=debugify lesson2_truncptr.ll -S -o lesson2.dbgfy.ll ; # attach per-instr lines, ; # then llc+gdb to map the ; # fault to an IR line. ; (See ir/README.md for the REAL captured output of every command above.) ; ============================================================ 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 32-bit slot -- TOO SMALL to hold a 64-bit pointer %slot = alloca i32, align 4 ; address-of target as a 64-bit integer %as_int = ptrtoint ptr %target to i64 ; *** TRUNCATE 64 -> 32: silently drops the high half of the address *** %trunc = trunc i64 %as_int to i32 ; stash the truncated address in the 32-bit slot (well-typed; semantically wrong) store i32 %trunc, ptr %slot, align 4 ; read it back and rebuild a pointer from the 32 surviving bits %back32 = load i32, ptr %slot, align 4 %back64 = zext i32 %back32 to i64 %badptr = inttoptr i64 %back64 to ptr ; high 32 bits are now ZERO ; write through the broken pointer -> almost certainly SIGSEGV store i32 456, ptr %badptr, align 4 ; <-- THE BUG (faults here) 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.