Fix EXEC/LOAD block-persistence gap and WIREBIND/USE interpreter-race panic (FABRIC-3.md §XII)
Build / build-amd64-iso (push) Waiting to run
Build / build-aarch64-iso (push) Waiting to run
Build / build-riscv64-img (push) Waiting to run

Found live while building a cross-ISA FORTH-79 dictionary exerciser:

- capsule_exec_init() zeroed a capsule's block content immediately after
  running it, so LOAD (a genuine FORTH-79 standard word, ACL-allowed even
  for locked identities) could never actually read back what EXEC had
  just written. Removed the clear from capsule_exec_init(); block content
  now persists like any other Standard BLOCK/BUFFER/UPDATE write.
  kernel_main.c's own explicit post-birth clear of Mama's init.4th range
  is untouched.

- USE could redirect the console to a WIREBIND identity's VM before that
  VM's vm_enable_interpreter() step of its own birth sequence had run,
  causing the next typed line to hit vm_assert_interpreter_enabled() and
  panic the entire machine -- not the per-session-recoverable ACL-fault
  path a redirected VM otherwise gets. USE now checks interpreter_enabled
  first and refuses with a retry message instead.

Also includes the amd64/aarch64/riscv64 acceptance boot logs and DoE CSVs
from this session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EXieurDfDSsDFdnSyusuWo
This commit is contained in:
Robert Allan James
2026-09-10 16:07:38 -04:00
co-authored by Claude Sonnet 5
parent d5722986b2
commit 662ef44e59
13 changed files with 36605 additions and 7 deletions
+69
View File
@@ -2035,3 +2035,72 @@ FORTH-79/83 personality and has been individually verified standalone. Not re-ru
3-arch acceptance pass on its own -- this is a data-only change to one thumbdrive image, no 3-arch acceptance pass on its own -- this is a data-only change to one thumbdrive image, no
kernel/capsule code touched, and amd64's own live verification above already exercises the same kernel/capsule code touched, and amd64's own live verification above already exercises the same
code paths (§XI.4/§XI.5) the last 3-arch pass (§XI.5, commit `b301317`) already accepted. code paths (§XI.4/§XI.5) the last 3-arch pass (§XI.5, commit `b301317`) already accepted.
## XII. Standard-dictionary cross-ISA exerciser campaign — two real bugs found live and CLOSED,
2026-09-10
Goal of the session: write one FORTH-79-standard-only exerciser program, run it identically
against every identity (`zuse` plus all 8 restricted identities) on all three architectures, and
diff the results for cross-ISA parity. Getting the mechanism working at all surfaced two real
bugs, both fixed here; the actual 27-leg campaign (3 arch × 9 identity) is what resumes next.
### XII.1 — `EXEC`/`LOAD` gap: a locked identity had no standard-compliant way to load capsule
source — CLOSED
`EXEC` (the kernel's named-capsule loader) was correctly found to be absent from
`ACL-STD79-LIST` (`capsules/acl-std79.4th`) -- not itself a bug, `EXEC` is kernel-specific, not a
FORTH-79 standard word. The real gap: `BLOCK`/`LOAD`/`THRU` (genuine standard words) *are* on the
allowlist, and `capsule_exec_payload()` genuinely does commit each capsule block's content to real
ramdrive storage (`write_ramdrive_block()`) before executing it -- so `LOAD` should have been a
legitimate standard-compliant fallback. It wasn't: `capsule_exec_init()` (what `EXEC` actually
calls, `mama_forth_words.c:1383`) followed `capsule_exec_payload()` with an unconditional
`capsule_clear_blocks()`, zeroing every block the capsule had just used before `EXEC` even
returned. The window where a capsule's content was actually present in block storage was entirely
inside one synchronous `EXEC` call -- nothing external, including a separately-running locked
identity's own session, could ever reach it with `LOAD` afterward. Confirmed by reading
`capsule_loader.c` directly, not inferred.
**Fix:** removed the `capsule_clear_blocks()` call from `capsule_exec_init()` (`capsule_loader.c`).
Block content written by `EXEC` now persists in ramdrive storage exactly like any other
Standard-compliant `BLOCK`/`BUFFER`/`UPDATE` write would, until something explicitly overwrites or
blanks it. The other caller of `capsule_clear_blocks()` -- `kernel_main.c`'s own explicit call
right after Mama's init.4th birth, to free that specific block range for later interactive
block-editor use -- is untouched; that one was already an explicit, separate, documented choice by
its own caller, not something `capsule_exec_init()` did on every caller's behalf. Verified:
`capsule_birth_mama()` calls `vm_exec_fn` directly (not `capsule_exec_init()`), so this fix has no
effect on Mama's own boot path.
### XII.2 — `USE` into a WIREBIND identity before its interpreter is enabled panics the whole
machine — CLOSED
Found live: attaching identity `03` via QMP hotplug, then immediately sending
`S" 03" USE` the moment the log printed `WIREBIND: 03 attached and ready -- USE it to begin`,
produced not an ACL fault but a full kernel panic --
`[StarKernel HAL] PANIC: interpreter invoked before bootstrap completion` -- and the machine
halted completely (`System halted.`, QEMU process left spinning uselessly). Root cause: a WIREBIND
identity's VM starts with `interpreter_enabled = 0` (`vm_bootstrap.c:162`) and only gets
`vm_enable_interpreter()` called as a later, separate step of the same birth sequence. The
"attached and ready" message prints as soon as the VM is registered, before that later step is
guaranteed to have run. `mama_word_use()` (`USE`) checked the target for dead/stillborn state but
never checked interpreter readiness before redirecting the whole console's input to it -- the very
next line typed would hit `vm_assert_interpreter_enabled()` inside `vm_interpret()` and call
`host->panic()`, which is a hard, unrecoverable halt, not the per-session-recoverable ACL-fault
path a redirected VM otherwise gets (§XI.3's fix). This is a real race independent of automation
speed: a human typing `USE` right after seeing "ready" hits it exactly the same way.
**Fix:** `mama_word_use()` (`mama_forth_words.c`) now checks `((VM *)entry.vm_ptr)->interpreter_
enabled` before redirecting, refusing with `USE: <name> not ready yet -- still bootstrapping, try
again` instead of performing the redirect. The caller just retries a moment later. Did not touch
the birth sequence itself (i.e., did not try to make the "attached and ready" message wait for
`vm_enable_interpreter()` to actually run first) -- narrower, lower-risk fix at the point of actual
failure.
**Both fixes verified:** amd64 rebuild clean, zero new warnings.
**Status:** exerciser campaign resuming with the fixed build. The 24-case exerciser itself (see
scratchpad `std79-exerciser.fth`, not yet committed -- FORTH-79 standard words only, biased toward
mixed/double-precision arithmetic and shift/negate boundary cases per cross-ISA risk) had already
produced one interesting, unconfirmed data point before the panic: `-123 456 M*` printed
`DOUBLE-OVERFLOW` via `D.` on amd64, identically for both `zuse` and `rajames` -- worth checking
against the other two architectures once the campaign completes; not yet root-caused or reported
as a bug on its own.
+1 -1
View File
@@ -1,5 +1,5 @@
# Capsule Block Manifest — Auto-generated # Capsule Block Manifest — Auto-generated
<!-- Generated by mkcapsule --manifest 2026-09-10T15:14:02Z --> <!-- Generated by mkcapsule --manifest 2026-09-10T20:06:42Z -->
<!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. --> <!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. -->
<!-- Hand-written justifications and immutability notes live --> <!-- Hand-written justifications and immutability notes live -->
<!-- in MANIFEST.md alongside this auto-generated index. --> <!-- in MANIFEST.md alongside this auto-generated index. -->
BIN
View File
Binary file not shown.
+14 -4
View File
@@ -72,8 +72,11 @@ int capsule_load_blocks(const uint8_t *payload, uint64_t length,
* capsule_clear_blocks - Zero all ramdrive slots referenced in a capsule payload * capsule_clear_blocks - Zero all ramdrive slots referenced in a capsule payload
* *
* Re-parses the payload for "Block <num>" headers and zeros each * Re-parses the payload for "Block <num>" headers and zeros each
* corresponding 1024-byte ramdrive slot. Called after execution to * corresponding 1024-byte ramdrive slot. NOT called by capsule_exec_init()
* reclaim ramdrive space. * (fixed 2026-09-10 -- see its own doc comment): callers that specifically
* want a capsule's block range freed for reuse (e.g. kernel_main.c, right
* after Mama's own init.4th birth, to free that range for interactive
* block-editor use) call this themselves, explicitly, after exec returns.
* *
* @param payload Raw capsule payload bytes * @param payload Raw capsule payload bytes
* @param length Payload length in bytes * @param length Payload length in bytes
@@ -94,13 +97,20 @@ void capsule_clear_blocks(const uint8_t *payload, uint64_t length);
int capsule_exec_payload(void *vm_opaque, const uint8_t *payload, uint64_t length); int capsule_exec_payload(void *vm_opaque, const uint8_t *payload, uint64_t length);
/** /**
* capsule_exec_init - Load, execute, and clean up an init capsule * capsule_exec_init - Load and execute an init capsule
* *
* Full init sequence: * Full init sequence:
* 1. Locate capsule by colon-separated name in the capsule directory * 1. Locate capsule by colon-separated name in the capsule directory
* 2. Validate content hash * 2. Validate content hash
* 3. capsule_exec_payload: populate block device + execute content * 3. capsule_exec_payload: populate block device + execute content
* 4. Zero block slots for userspace (capsule_clear_blocks) *
* Block content is NOT cleared afterward (fixed 2026-09-10): it stays
* resident in ramdrive storage so a subsequent Standard BLOCK/LOAD on the
* same block number reads back what EXEC just wrote, matching FORTH-79
* block-persistence semantics. Callers that specifically want the old
* "free this capsule's block range" behavior (e.g. kernel_main.c freeing
* init.4th's range for later interactive block-editor use) call
* capsule_clear_blocks() themselves, explicitly, after this returns.
* *
* @param vm VM to execute on * @param vm VM to execute on
* @param capsule_name Colon-separated capsule name, e.g. "init.4th" * @param capsule_name Colon-separated capsule name, e.g. "init.4th"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+14 -2
View File
@@ -581,8 +581,20 @@ CapsuleRunResult capsule_exec_init(
const uint8_t *payload = capsule_get_payload(cap, arena); const uint8_t *payload = capsule_get_payload(cap, arena);
if (!payload) return CAPSULE_RUN_ERR_INVALID; if (!payload) return CAPSULE_RUN_ERR_INVALID;
/* Unified: populate block device + execute; then free slots for userspace */ /* Populate block device + execute. Block content is left in place
* afterward -- capsule_exec_payload() already commits it to real
* ramdrive storage (write_ramdrive_block()) specifically so FORTH
* code can address it by number; a Standard BLOCK/LOAD on that same
* number must be able to read back what EXEC just ran, the same way
* any other block-storage write persists until something explicitly
* overwrites or blanks it. Previously this called capsule_clear_blocks()
* here, zeroing the slots immediately after execution -- that made
* capsule content invisible to LOAD the instant EXEC returned, which
* is not how FORTH-79 BLOCK/LOAD is supposed to behave, and is also
* the reason a FORTH-79/83-locked identity (no EXEC in its own
* dictionary, see capsules/acl-std79.4th) had no standard-compliant
* way to ever load capsule-authored source: LOAD was on the allowlist
* but nothing was ever left for it to find. Found and fixed 2026-09-10. */
int rc = capsule_exec_payload(vm, payload, cap->length); int rc = capsule_exec_payload(vm, payload, cap->length);
capsule_clear_blocks(payload, cap->length);
return (rc == 0) ? CAPSULE_RUN_OK : CAPSULE_RUN_ERR_EXEC_FAIL; return (rc == 0) ? CAPSULE_RUN_OK : CAPSULE_RUN_ERR_EXEC_FAIL;
} }
+16
View File
@@ -483,6 +483,22 @@ void mama_word_use(VM *vm)
return; return;
} }
/* Found live 2026-09-10: WIREBIND announces "attached and ready --
* USE it to begin" the moment the target VM is registered, but
* vm_enable_interpreter() is a separate, later step of that same
* birth sequence. USE redirecting here before that step lands means
* the very next line typed hits vm_assert_interpreter_enabled()
* inside vm_interpret() and calls host->panic() -- a hard, whole-
* machine halt, not the per-session-recoverable fault path the REPL
* loop otherwise gives a redirected VM. Refuse the redirect instead;
* the caller can just retry USE a moment later once birth finishes. */
if (entry.vm_ptr && !((VM *)entry.vm_ptr)->interpreter_enabled) {
console_puts("USE: ");
console_puts(name_buf);
console_println(" not ready yet -- still bootstrapping, try again");
return;
}
/* BINDSTEP (FABRIC-2.md §F.9/§F.24): if the target has a real /* BINDSTEP (FABRIC-2.md §F.9/§F.24): if the target has a real
* installed identity (WIREBIND set this at attach time), re-verify * installed identity (WIREBIND set this at attach time), re-verify
* it against whatever drive is CURRENTLY attached -- live, not * it against whatever drive is CURRENTLY attached -- live, not