kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

Bug C — SQLite segfaults at runtime when built at -O1

Status: FIXED. The ecosystem gate is green for sqlite:O1 (golden + vs-clang). Component: -O1 native emit (src/opt/pass_native_emit.c, IR_BITFIELD_STORE) — a register-allocation clobber, not the linker or front end. Severity (when open): the sqlite shell built at -O1 crashed before producing output.

Root cause and fix

A C bitfield assignment p->field = v (here a 2-bit field, v == 0) lowers to a read-modify-write: load the storage word at the field's address, clear the field bits, OR in the new value, store the word back. The address must stay live across the whole sequence.

The -O1 native emitter materialized the value without telling the allocator to avoid the address registers (it passed REG_NONE, REG_NONE as the registers-to-avoid). Under register pressure SQLite's pointer spilled and was reloaded into scratch[0]; the value-0 materialization then also picked scratch[0], emitting mov w9, #0 over the just-reloaded address in x9 — nulling it. The following ldr w16, [x9] dereferenced 0.

ldr  x9, [sp, #..]   ; reload spilled pointer (address) into scratch x9
mov  w9, #0          ; materialize the value 0 — into the SAME scratch x9 (clobber!)
ldr  w16, [x9]       ; load storage word from 0 → SIGSEGV

Fix: IR_BITFIELD_STORE now materializes the value avoiding the address base/index registers, and collapses the address to a single scratch first when no other scratch is free — exactly what IR_STORE and IR_AGG_SET already do. Regression test: test/asm/roundtrip/bitfield_store_spill.c (red at -O1 before the fix: crashed; green after at both -O0 and -O1).

How this surfaced

How this surfaced

This fault was masked by Bug B (sqlite-o1-lkit-ro.md): until Bug B was fixed, the sqlite:O1 link failed, so the -O1 binary never existed and never ran. With Bug B fixed the link succeeds, and the binary now crashes at runtime — so this is a distinct, pre-existing -O1 miscompile that Bug B was hiding, not a regression from the Bug B fix.

What is and isn't affected:

Symptom

$ ./sqlite3 :memory: < test/ecosystem/scripts/sqlite.sql
Segmentation fault: 11      # exit 139

Under lldb (arm64-macOS):

stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
frame #0: sqlite_o1`... + N        # ldr w16, [x9]  with x9 == 0

A null-pointer load (x9 == 0). The enclosing symbol lldb prints is the nearest preceding exported symbol with a very large +offset, i.e. the real (local) function is mis-symbolicated — reduce against kit nm/DWARF to pin the function.

Reproduce

make provision-ecosystem          # once (network)
KIT=build/kit
SDK="$(xcrun --sdk macosx --show-sdk-path)"
SQ="$(sh scripts/ecosystem.sh srcdir sqlite)"
DEFS="-DSQLITE_THREADSAFE=0 -DSQLITE_OMIT_LOAD_EXTENSION"

$KIT cc -O1 --sysroot "$SDK" $DEFS -I "$SQ" -c "$SQ/sqlite3.c" -o /tmp/sqlite3.o
$KIT ar rcs /tmp/libsqlite3.a /tmp/sqlite3.o
$KIT cc -O1 --sysroot "$SDK" $DEFS -I "$SQ" "$SQ/shell.c" /tmp/libsqlite3.a -lc -o /tmp/sqlite3
/tmp/sqlite3 :memory: < test/ecosystem/scripts/sqlite.sql   # SIGSEGV before the fix; now runs clean

Notes for digging