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:
- sqlite at -O0 builds, runs, and matches clang byte-for-byte (green).
- yyjson at -O0 and -O1 both build, run, and are correct (green) — so the -O1 pipeline is not broadly broken; this is specific to SQLite's -O1 code.
- The failure reproduces on both the one-shot and the separate compile-then-link paths at -O1, so it is independent of the link path.
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
- Bisect the SQLite TU: compile
sqlite3.cat -O1 butshell.cat -O0 (and vice versa) to localize which TU's -O1 code faults. - This tree is under active -O1 optimizer/type-cache work; confirm whether the fault predates those changes (build an -O1 sqlite from an earlier revision) before assuming it is purely a long-standing optimizer bug.
- A standalone reducer (smallest SQL + smallest pragma path that crashes) next to this doc would make the null-deref tractable; still TODO.