Files
el/lang
will.anderson 9e28defcab fix: bounds-check btree_insert + read_body to survive bloated store / WAL replay (live crash-loop root cause)
int_max_keys() computed an internal B+tree node's key capacity as
IDX_BODY/8 - 1 (2041 at a 16 KB page), dividing by 8 and ignoring that
each key also carries an 8-byte child pointer. The true capacity is
(IDX_BODY-8)/16 = 1020. An internal node was therefore allowed to grow to
~2x what a page holds; once it crossed 1020 keys, btree_insert's write-back
overran its STORE_PAGE_SIZE stack page buffer and smashed the stack canary
(__stack_chk_fail / SIGABRT). A clean/small store never grows an internal
node that large, so it never tripped; the ~8x-bloated live store (a day of
tombstone churn) plus a 44 MB un-checkpointed WAL replayed on open pushed a
node over the boundary during redo -> deterministic crash loop
(btree_insert <- apply_edge_put <- engram_open <- engram_store_boot).

Fixes:
- int_max_keys: use (IDX_BODY-8)/16 so internal nodes split at the real
  page capacity.
- btree_insert: reject any page whose on-disk nkeys exceeds physical
  capacity (fail loud, never smash the stack) -- overflow is now impossible
  regardless of on-disk content.
- read_body: bound the slot (off,len) and record length to the page before
  dereferencing; a stale/torn index entry could otherwise make store_get_node
  read off the stack (observed EXC_BAD_ACCESS on the bloated store). Fail safe.

Verified on a COPY of the live store: unfixed binary SIGABRTs in btree_insert
on open; fixed binary boots clean, recovers the store, checkpoints the WAL,
and M5 compaction shrinks 458 MB -> 57.7 MB with node/edge counts preserved.
2026-08-12 18:27:13 -05:00
..