Repository navigation
flinmemory is a false positive for mbar externals in headless builds; dereferencing variabledata segfaults #896
Description
Rationale
flinmemory is not trustworthy per-type in the headless build. For menubar (mbar) externals the headless stub marks the value resident without loading it, so variabledata still holds a raw dbaddress rather than a pointer. Any code that trusts the flag and dereferences the payload segfaults.
This is a landmine for every generic ODB walker, independent of the tool that found it: the flag means "resident" everywhere else, so the natural if (flinmemory) { ... } guard is a false positive exactly where it is most dangerous.
Current State
frontier-cli/stubs/headless_menu_stubs.c:618:
boolean menuverbinmemory_context (const db_context *ctx, hdlexternalvariable hvariable) {
/*
* Menu externals are not materialized during headless migration.
* Mark as "in memory" to skip actual loading, but keep the address for
* packing.
*/
(void) ctx;
(**hvariable).flinmemory = true;
return true;
}ensure_external_in_memory() (Common/source/langexternal.c:916) dispatches idmenuprocessor here, so a caller that materializes an external and then reads variabledata gets a dbaddress reinterpreted as a handle.
Verified reproduction (2026-08-15, pure-C walker, no interpreter):
- Walking
mainResponder.rootand dereferencingvariabledataformainResponder.menu-> SIGSEGV, exit 139, reproducible. - Materialization itself "succeeds" (returns true); the crash is on first dereference afterwards.
- Same failure shape on any
mbar: 43 inVirgin.root, 1 inmainResponder.root, 11 inmanila.root.
Independent corroboration: mainResponder.menu is also the single mbar the 2026-08-11 ODB census failed to measure -- it records size -1 (its catchable-failure sentinel) for exactly that value. Two tools, two different mechanisms, same value.
Related exposure
ut_export_walk_table() (frontier-cli/main.c:2444) guards subtable recursion with if ((**hv).flinmemory). For idtableprocessor that guard is merely incomplete (a non-resident subtable is silently skipped, so an exporter can omit an entire subtree). The concern here is the general one: the flag is being used as ground truth for residency, and for at least one external kind it is not.
Also worth noting for anyone auditing this area: a second stub, menuverbpack-adjacent code, exists specifically to "keep the address for packing", so the current behavior is deliberate for the migration path -- it is the generic readers that are unsafe, not the migration.
Desired Outcome
- Make the contract explicit: either have
menuverbinmemory_contextfail honestly in headless (returning false so callers can report), or introduce a distinct predicate (e.g.external_payload_is_resident()) that generic walkers can trust per-type. - Audit existing
flinmemoryreaders for the same assumption. - Document the trap wherever residency flags are described, so the next walker author does not rediscover it with a segfault.
- Regression test: materialize an
mbarin a headless build and assert the payload is either genuinely loaded or reported as unavailable -- never a dereferenceable-looking lie.
Workaround in place
--diff-roots (root-build Step 1) compares mbar and wptext at the stored level (raw disk bytes at the external's address) and never materializes them. wptext is excluded for a different reason -- force-materializing those exhausts memory on a full-root walk (Common/source/db_format.c:1694) -- and the two rationales are kept distinct in that tool's output so they are not conflated later.
Activity
fix(odb): P0 -- mbar compare was vacuous; skip-region offsets were wr…
fix(odb): P1 -- name-keyed matching (hfirstsort is insertion order) +…
fix(odb): P2 batch -- resource teardown, float bytes, subtree depth, …
fix(odb): make the ut-sync guard provable; define never-stored-both-s…
test(odb): commit behavioral fixtures; every compare path proves DETE…
feat(odb): --diff-roots value-level equality walker (root-build Step …