Four bugs found live verifying the 8 identity thumbdrives (FABRIC-3.md §IX)
All found by actually running the identity workflow §VII/§VIII made possible, not by code review: 1. Zuse/WIREBIND cross-contamination on detach: capsule_zuse_boot_logout() and capsule_wirebind_unclean_detach() both had no device parameter, so an unrelated device detaching (while the real owner's own stayed attached) incorrectly tore down the wrong session. Both now compare the departing device against their own tracked one, mirroring capsule_wirebind.c's pre-existing g_wirebind_attached_dev precedent. 2. Dictionary-entry memory leak: vm_create_word()'s sf_malloc()'d DictEntry (plus a second per-entry allocation for transition_metrics) was never freed by vm_cleanup(), in both the hosted and kernel implementations. Caused a real kernel PANIC after 8-9 repeated VM birth/kill cycles in one boot. Fixed by walking vm->latest in both. 3. sf_malloc/sf_free (alloc_kernel.c) was a 4MB bump arena with a deliberate no-op free, sized on "VM born once, never killed" -- fix #2 alone didn't stop the panic because free() itself discarded the pointer regardless. Given a real free list (first-fit reuse). 4. Headless-console gate didn't re-engage after a mid-boot logout: the original fix (sk_console_mark_login(), one-way sticky) only gated the first login of the boot. Replaced with a live check (sk_console_identity_present()) re-evaluated continuously, including inside sk_console_readline()'s own blocking idle loop -- the console is normally sitting blocked there when a hot-unplug logout happens, so checking only at the top of the REPL loop wasn't enough. Also: MINT now verifies its own write (verify_mint(), capsule_mint.c) by reading back through the same check a real attach performs, rather than trusting blkio_write()'s BLK_OK alone -- logged via log_message(), not console_println(), per direct instruction. Verified live, amd64: the full 8-identity repeated attach/detach cycle that previously panicked at the same point every time now completes clean, and a full serial-log sweep found zero bare unauthenticated prompts anywhere in the run. Three-arch clean-qemu acceptance passed. Still open, not fixed here: a 3+-simultaneous-device USB enumeration failure found in a separate live test, not yet root-caused. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018EjXFo7mPXjUMjfJeuUUz4
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
0bae928aad
commit
2c1b3cd695
+155
@@ -1340,3 +1340,158 @@ headless-console work) — no regression in existing `KILL`/`USE` behavior. Not
|
||||
stress-tested against the specific dangling-pointer scenario (kill the actively-`USE`'d VM,
|
||||
then dispatch another command) — the fix is small and directly addresses the confirmed gap,
|
||||
but that specific repro was not re-run live after the fix landed.
|
||||
|
||||
## IX. Four bugs found live minting/verifying the 8 identity thumbdrives — CLOSED 2026-09-06
|
||||
|
||||
All four surfaced by actually exercising the system this project's own documentation exists
|
||||
to describe — the thumbdrive-identity workflow §VII/§VIII made possible — not by code review.
|
||||
Each is documented here with the same discipline: what broke, why, the fix, and what was
|
||||
verified afterward.
|
||||
|
||||
### IX.1 — Zuse/WIREBIND cross-contamination on detach
|
||||
|
||||
**Bug:** with §VII's multi-device fix in place, live-verifying all 8 minted identities (boot
|
||||
with Zuse attached, hot-plug each target drive one at a time, confirm `WIREBIND: <username>
|
||||
attached and ready`) surfaced a real cross-contamination bug: detaching drive `00` printed
|
||||
`Zuse: session ended -- reattach to re-authenticate` even though Zuse's own drive was never
|
||||
touched. Root cause: `capsule_zuse_boot_logout()` (`capsule_zuse_boot.c:164`, called
|
||||
unconditionally from `repl.c`'s per-slot detach loop for every departing device) had no way
|
||||
to tell "some other device detached" from "my own device detached" — its only guard was a
|
||||
bare boolean (`g_zuse_attached_this_device`), not a device comparison, despite its own header
|
||||
doc comment already claiming that no-op existed. The exact same defect class existed in the
|
||||
sibling function, `capsule_wirebind_unclean_detach()` (`capsule_wirebind.c:267`) — also no
|
||||
device parameter, so an unrelated device detaching while a WIREBIND user's own stayed
|
||||
attached would have incorrectly torn down that user's session too (not separately triggered
|
||||
live, found by inspection once the first instance was understood).
|
||||
|
||||
**Fix:** both functions now take the departing `struct blkio_dev *dev` and compare it against
|
||||
their own tracked device (`g_zuse_attached_dev`, new — mirrors `capsule_wirebind.c`'s own
|
||||
pre-existing `g_wirebind_attached_dev` precedent exactly) before acting; every other value is
|
||||
a no-op. `repl.c`'s detach loop passes the actual departing `usb_blk_dev` pointer it already
|
||||
had in scope. `EJECT` (`mama_forth_words.c`'s `mama_word_eject()`, an explicit operator
|
||||
command with no specific detaching device to check against) now calls a new accessor,
|
||||
`capsule_zuse_boot_attached_dev()`, passing her own tracked device straight back in — trivially
|
||||
matches when she's genuinely attached, correctly no-ops via the existing boolean guard
|
||||
otherwise.
|
||||
|
||||
**Verified:** live, amd64 — a full 8-identity sequential attach/detach cycle (Zuse detached
|
||||
first, then bob/rajames, 00-06 one at a time) with zero cross-contamination; `(zuse) ok>`
|
||||
correctly persisted through unrelated detaches before this fix's own follow-on (§IX.3) closed
|
||||
the remaining gap where her session ending correctly still left a bare prompt visible.
|
||||
|
||||
### IX.2 — Dictionary-entry memory leak (`vm_cleanup()`), causing a real kernel PANIC
|
||||
|
||||
**Bug:** repeating the WIREBIND birth/kill cycle 8-9 times in one boot (exactly what
|
||||
identity verification does) produced a genuine kernel PANIC on the 8th-9th cycle every time:
|
||||
cascading `vm_create_word: malloc failed` errors during capsule loading into the newly-born
|
||||
VM, followed by `PANIC: Stadium: eviction selected patron zero (Hera) -- governor invariant
|
||||
broken` and a full system halt. Root cause, found by tracing the allocation path: every
|
||||
dictionary word (`vm_create_word()`, `dictionary_management.c:429`) allocates its own
|
||||
`DictEntry` via `sf_malloc()` — separate from the VM's own `vm->memory` arena entirely — but
|
||||
`vm_cleanup()` (**both** `src/starkernel/vm/vm_core.c:286` kernel and `src/vm_bootstrap.c:370`
|
||||
hosted) freed `vm->memory`, the hotwords cache, rolling window, SSM state, and call stack, but
|
||||
never walked the VM's own dictionary chain (`vm->latest`) to free the individual `DictEntry`
|
||||
allocations — confirmed via grep that no such free existed anywhere in the codebase. Every
|
||||
word a VM ever defined (hundreds per birth, given capsule loading) leaked permanently on kill.
|
||||
Never noticed before this session: the hosted binary normally only calls `vm_cleanup()` once
|
||||
at process exit (the OS reclaims everything anyway), and kernel VMs were normally born once
|
||||
and kept alive for a whole boot, not repeatedly born and killed.
|
||||
|
||||
**Fix:** both `vm_cleanup()` implementations now walk `vm->latest` via each entry's own
|
||||
`->link`, freeing `entry->transition_metrics` (a second, separate per-entry `sf_malloc()`,
|
||||
`dictionary_management.c:455` — also unfreed anywhere) before the entry itself, saving the
|
||||
next-link before each free since freeing destroys it.
|
||||
|
||||
**Verified:** live, amd64, the exact repeated-cycle scenario that panicked — no longer panics
|
||||
alone (see §IX.3, this fix alone was insufficient until the allocator itself was also fixed).
|
||||
|
||||
### IX.3 — `sf_malloc`/`sf_free` was a bump allocator with a deliberate no-op free
|
||||
|
||||
**Bug, found immediately after §IX.2's fix didn't change the panic at all:** the kernel's own
|
||||
`sf_malloc`/`sf_free` (`src/starkernel/vm/alloc_kernel.c`) is a fixed 4MB static bump arena
|
||||
whose `sf_free()` was a **documented, deliberate no-op** — "VM is long-lived (no restart)...
|
||||
free is a no-op... no fragmentation issues in practice." That premise was true until this
|
||||
session's own repeated-birth-kill workflow. §IX.2's fix, while correct application-level
|
||||
hygiene, was functionally inert against this specific allocator: calling `sf_free()`
|
||||
diligently changes nothing when `sf_free()` itself throws the block away regardless.
|
||||
|
||||
**Fix:** `sf_malloc`/`sf_free` now implement a real free list. Every allocation carries a
|
||||
fixed header (`size` + a free-list `next` link, live only while free) immediately before the
|
||||
returned pointer. `sf_free()` pushes the block onto `g_free_list`; `sf_malloc()` searches it
|
||||
first-fit before falling back to bump-allocating a fresh block from the arena, exactly as
|
||||
before. Deliberately no splitting/coalescing — this workload's repeated allocations are for
|
||||
the same capsules loaded into a fresh VM each time, so freed blocks from a just-killed VM's
|
||||
dictionary are typically an exact or near-exact fit for the next VM's own, making first-fit
|
||||
reuse the right simplicity tradeoff. `sf_realloc()` (pre-existing "orphans the old block"
|
||||
behavior) was deliberately left untouched — out of scope for this fix, no evidence it's
|
||||
exercised in the failure path.
|
||||
|
||||
**Verified:** live, amd64, the full 8-identity repeated birth/kill cycle that panicked at the
|
||||
identical point on every prior run (confirmed twice) — no panic, all 9 sessions (Zuse + 8)
|
||||
confirmed in one continuous boot. Three-arch `clean qemu` acceptance also re-run clean after
|
||||
§IX.1/§IX.3/§IX.4 landed together.
|
||||
|
||||
### IX.4 — Headless-until-login gate didn't re-engage after a mid-boot logout
|
||||
|
||||
**Bug:** §VIII's original headless-console gate (`sk_console_mark_login()`/`sk_console_
|
||||
login_occurred()`) was a one-way sticky flag, gating only the very first login of the boot
|
||||
before `sk_repl(mama)` was ever called. Once tripped once, it never reset — a later full
|
||||
logout (nobody attached at all) fell through to a bare, unauthenticated `ok>` instead of going
|
||||
silent again, for the rest of the boot. Found live during the same 8-identity verification
|
||||
run, flagged directly: *"we should never see an unauthorized, no session forth prompt."*
|
||||
Fixing just the top-level loop wasn't enough either: the console is typically sitting blocked
|
||||
inside `sk_console_readline()` waiting for input at the moment a logout happens (via hot-
|
||||
unplug detected from *inside* that same call's own idle-service loop), so a check only at the
|
||||
top of `sk_repl_run()`'s loop wouldn't take effect until a line was actually typed next.
|
||||
|
||||
**Fix:** `sk_console_mark_login()`/`sk_console_login_occurred()` retired entirely, replaced by
|
||||
a live check, `sk_console_identity_present()` (`repl.c`) — mirrors `sk_print_prompt()`'s own
|
||||
`zuse_session`/WIREBIND-username check exactly, evaluated fresh every time rather than latched
|
||||
once. Three call sites updated: `sk_repl_headless_wait()`'s own loop condition; a new check at
|
||||
the top of `sk_repl_run()`'s main loop (re-enters headless wait whenever nobody is currently
|
||||
attached, not just before the very first login); and, critically, inside `sk_console_
|
||||
readline()`'s own idle branch itself — when the identity present at prompt-print time logs out
|
||||
while the call is still blocked waiting for input with nothing typed (`n == 0`), it now
|
||||
returns -1 instead of re-printing a (correctly bare, but still unauthenticated) prompt and
|
||||
continuing to wait. Callers with `reanchor_prompt` nonzero (the REPL's own top-level prompt
|
||||
sites) check for -1 and loop back without printing `" ok"`, landing back at the top-of-loop
|
||||
check, which re-enters silence immediately. `shim.c`'s `fgets()` (`reanchor_prompt == 0`)
|
||||
never receives -1.
|
||||
|
||||
**Verified:** live, amd64, full 8-identity sequential cycle (Zuse's own detach, then each of 8
|
||||
WIREBIND users' own detach) — grepped the entire session's serial log for every prompt-prefix
|
||||
occurrence: 9 total `ok>` lines, every single one carrying a name (`(zuse)`, `(rajames)`,
|
||||
`(00)`...`(06)`) — zero bare, unauthenticated prompts anywhere in the run. Three-arch
|
||||
`clean qemu` acceptance re-run clean with this fix included.
|
||||
|
||||
### IX.5 — `MINT` now verifies its own write
|
||||
|
||||
**Motivation:** a separate live test (attaching Zuse + 8 identities simultaneously without
|
||||
ever detaching any, exercising genuine 3+-device concurrent attach for the first time) hit a
|
||||
4th-device USB enumeration failure (`xhci: not a Mass Storage/SCSI/BOT device`) — a real,
|
||||
still-open defect (not yet root-caused, tracked as follow-on work, not part of this closure).
|
||||
That failure raised a fair question: could `MINT` itself silently report success without the
|
||||
identity actually being readable back afterward? `capsule_mint_identity()`
|
||||
(`capsule_mint.c`) previously trusted every `blkio_write()`'s own `BLK_OK` return as
|
||||
sufficient proof; it never confirmed any of it was actually readable back.
|
||||
|
||||
**Fix:** a new `verify_mint()` (`capsule_mint.c`) runs immediately after all writes and the
|
||||
flush, before returning `MINT_OK`: re-runs `homeblocks_sig_check()` — the exact same check a
|
||||
real later attach performs — then separately re-reads the identity record devblock and checks
|
||||
magic/version/CRC/full-content match against what was written. A new result code,
|
||||
`MINT_ERR_VERIFY_FAILED` (`capsule_mint.h`), is returned (and reported by the `MINT` word's
|
||||
own existing console switch, `mama_forth_words.c`) if any check fails. Logged entirely via
|
||||
`log_message()` (`LOG_ERROR` per specific mismatch, `LOG_INFO` on success) rather than
|
||||
`console_println()`, per direct instruction — no added unconditional console/serial noise;
|
||||
the `MINT` word's own final pass/fail line still reports to the console as it already did for
|
||||
every other `MintResult` case.
|
||||
|
||||
**Not yet re-verified against a real failure** — every mint performed after this fix landed
|
||||
(the original 8 identities were already minted before it existed) has succeeded, so the
|
||||
failure-path logging itself hasn't been exercised live yet, only compiled. Three-arch
|
||||
`clean qemu` acceptance passed with this change included.
|
||||
|
||||
**Still open, not addressed by this closure:** the 3+-simultaneous-device USB enumeration
|
||||
failure that motivated §IX.5 (`xhci: not a Mass Storage/SCSI/BOT device` on a 4th concurrent
|
||||
attach) remains unexplained and unfixed — tracked as the next item, separate from the four
|
||||
closures above.
|
||||
|
||||
Reference in New Issue
Block a user