4 Commits
Author SHA1 Message Date
Robert Allan JamesandClaude Sonnet 5 16cc74243c std79 DoE campaign rerun post-Stage-4: 81/81, §XIV caught live by rerun's own harness bug (FABRIC-3.md §XXXI)
Build / build-amd64-iso (push) Canceled after 0s
Build / build-aarch64-iso (push) Canceled after 0s
Build / build-riscv64-img (push) Canceled after 0s
Reran the established 3x9x3 randomized full-factorial campaign
(std79-doe.fth) on all 3 architectures per standing project discipline
(any Stadium-adjacent change reruns the whole DoE from the top).

First amd64 attempt exposed a real test-harness bug that re-triggered
the already-known §XIV concurrent-attach gap: the sequential-attach wait
loop checked for any recent WIREBIND-attached line instead of the
specific identity requested, firing the next device_add before the
kernel finished the current one. Only 2 of 8 identities attached; the
campaign itself completed cleanly with graceful "VM-EXEC: VM not found"
refusals rather than corrupting anything. Fixed the wait loop, discarded
the invalid run's campaign result (its boot log kept for the record),
reran clean.

Corrected reruns: all 8 identities individually confirmed on all 3
architectures, zero VM-not-found errors, zero faults, 81/81 trials
correct against established baseline values. Raw logs archived at
experiments/std79-doe/results-20260915-stage4/.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BWpNjdwPtFLuVLaAq44L9K
2026-09-15 04:56:31 -04:00
Robert Allan JamesandClaude Sonnet 5 597f5a6cd8 Stage 4 verified: reap mechanism proven, real WIREBIND multiuser+multitasking confirmed live (FABRIC-3.md §XXX)
Reap mechanism (increments 2+3): temporary probe using Artemis as a safe
stand-in parked identity, 3/3 checks PASS on all 3 architectures (refuse
on SWITCHED_OUT, correct post-reap state, switch-signal slot released).
Probe reverted, all 3 architectures re-verified clean.

Live multiuser verification (increment 4, no code changes): a real
previously-unattached identity thumbdrive attached via QMP on a running
boot on all 3 architectures. VM-EXEC dispatch into her own live VM
computed correctly, tagged with her own name in console output, full
Tripod fleet unaffected. EJECT cleanly tore down both her VMs and
released the switch-signal slot -- confirms increment 1's per-device fix
under a real live attach/detach.

Found and flagged, not fixed: interactive USE on a freshly-attached
identity halts the kernel outright. Confirmed NOT caused by Stage 4 --
reproduced identically on the commit before any Stage 4 work. Direct
VM-EXEC dispatch into the same identity works correctly; this is
specific to the USE/BINDSTEP codepath, plausibly never caught before
since every prior identity campaign used VM-EXEC, never interactive USE.

Stage 4's original scope is complete: multitasking (Tripod) and
multiuser (WIREBIND) are now genuinely composed, verified against real
hardware-driven identity attach on all 3 architectures.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BWpNjdwPtFLuVLaAq44L9K
2026-09-15 03:39:37 -04:00
Robert Allan JamesandClaude Sonnet 5 d9da82b065 Stage 4 increments 2+3: WIREBIND VMs as switch-signal participants + mark-and-defer tombstone reap (FABRIC-3.md §XXVIII Stage 4)
Increment 2: WIREBIND user VMs (the ones that actually run FORTH work;
console VMs are pure REPL proxies and never participate) register as
Stage 3 switch-signal participants at attach, unregister at teardown.
Slot table bumped 8 -> 16, matching messaging.4th's own VM-MAX -- a real,
already-agreed ceiling, not an invented number. Added
sk_vm_switch_signal_unregister() (compaction-based; Tripod VMs never
needed removal, WIREBIND VMs cycle constantly and would otherwise
exhaust the bounded table).

Increment 3: implements the plan's own ratified option (A) for the
async-detach UAF risk -- mark-and-defer via a new pending_reap flag on
VMRegistryEntry, deliberately not a new VMState (capsule_vm_kill()
already treats VM_STATE_DEAD as idempotent success, which would silently
swallow a reap attempt; SWITCHED_OUT still accurately describes a
tombstoned VM until the moment it's actually freed). unclean_detach()
sets it when capsule_vm_kill() refuses a SWITCHED_OUT target; the Stage 3
checkpoint (vm_core.c) checks it before ever attempting to resume a
pending switch target, and calls the new capsule_vm_force_reap() instead
-- the one caller allowed to bypass capsule_vm_kill()'s own refusal,
because it runs at the exact safe cooperative point the switcher itself
controls. A new idle-tick sweep cleans up the WIREBIND live-table entry
once the reap has actually happened.

Verified clean on all 3 architectures (baseline regression -- no
WIREBIND attach happens in a plain boot). The reap mechanism's own
correctness under a genuinely parked context is verified separately,
next, via a temporary deterministic probe.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BWpNjdwPtFLuVLaAq44L9K
2026-09-15 00:58:34 -04:00
Robert Allan JamesandClaude Sonnet 5 9f0f33dfc5 Stage 4 increment 1: per-device WIREBIND tracking, fixing a real multi-identity detach leak (FABRIC-3.md §XXVIII Stage 4)
capsule_wirebind_unclean_detach()/eject() tracked "the attached identity"
as a single global, correct for the console-pairing UX (one physical
console) but wrong for detach safety: since §XV/§XVI proved multiple
identities genuinely live simultaneously via this same attach path, every
attach after the first silently overwrote the singleton, so an unclean
detach of any but the most-recently-attached identity was silently
ignored -- that VM leaked forever, no trace in the log.

Adds a per-device live-identity table, separate from the (unchanged)
console-pairing singleton, so unclean-detach resolves any attached
device to its own identity. Sized off messaging.4th's own VM-MAX (16)
minus Tripod's 3 reserved slots, not an invented number. Corrects the
stale "single-USB-device constraint" doc claim in capsule_wirebind.h,
false since §XV/§XVI.

Groundwork for Stage 4's real deliverable (WIREBIND VMs as switch-signal
participants) -- this increment only fixes detach targeting; switch-
signal registration is next.

Verified clean on all 3 architectures (no WIREBIND attach happens in a
plain boot, so this is a regression check on the existing Tripod-only
path; live multi-identity verification comes with the switch-signal
registration increment).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BWpNjdwPtFLuVLaAq44L9K
2026-09-15 00:02:20 -04:00
39 changed files with 245590 additions and 46 deletions
+140
View File
@@ -3988,3 +3988,143 @@ indicated a larger new initiative is coming that will be introduced during Stage
-- not detailed here since it hasn't been scoped yet, flagged only so this closing note doesn't
read as "nothing more is coming."
## XXX. Stage 4 -- WIREBIND-scope extension, built and verified live (2026-09-14/15)
**Bob's call: proceed with Stage 4 immediately rather than wait, folding the two §XXIX-deferred
bugs in only where they turned out to actually block the work** (neither did, in the end --
both stay open, tracked below in their original form). Built in the same staged,
one-commit-per-increment discipline as Stages 0-3.
**Increment 1 (commit `9f0f33d`) -- de-singleton WIREBIND detach tracking.** Found before
writing any Stage 4 code, on advisor review: `capsule_wirebind_unclean_detach()`/`eject()`
tracked "the attached identity" as a single global (`g_wirebind_attached_*`), correct for the
one-physical-console UX but wrong for detach safety once §XV/§XVI's own campaign already proved
multiple identities live simultaneously via this same attach path -- every attach after the
first silently overwrote the singleton, so an unclean detach of any but the *most recently
attached* identity was silently ignored: no console trace, no teardown, the VM and its native
stack leaked forever. Added a per-device live-identity table (`g_wirebind_live[]`, sized off
messaging.4th's own `VM-MAX` minus Tripod's 3 reserved slots -- 13, not an invented number),
kept separate from the console-pairing singleton, which is untouched and still governs the
`(user)` prompt exactly as before. Corrected the stale "single-USB-device constraint (§F.8)"
claim in `capsule_wirebind.h`'s own doc comments, false since §XV/§XVI.
**Increments 2+3 (commit `d9da82b`) -- switch-signal participation + mark-and-defer tombstone
reap.** WIREBIND *user* VMs (the ones that run real FORTH work; console VMs are pure REPL
proxies with nothing for a switch-in to preempt) now register as Stage 3 switch-signal
participants at attach (`sk_vm_switch_signal_register()`), unregister at teardown. Slot table
bumped 8 -> 16 (`VM-MAX`, the same real ceiling increment 1 used). Added
`sk_vm_switch_signal_unregister()` (compaction-based -- Tripod never needed removal, WIREBIND
VMs cycle constantly and would otherwise exhaust the bounded table).
Implements the plan's own ratified option (A) for the async-detach UAF risk named all the way
back when Stage 4 was first scoped (`logical-snuggling-bear.md`): mark-and-defer via a new
`pending_reap` flag on `VMRegistryEntry` -- deliberately a plain flag, not a new `VMState`
(`capsule_vm_kill()` already treats `VM_STATE_DEAD` as idempotent success, which would silently
swallow a reap attempt; `SWITCHED_OUT` still accurately describes a tombstoned-but-not-yet-freed
VM). `unclean_detach()` sets it when `capsule_vm_kill()` refuses a `SWITCHED_OUT` target instead
of just logging and dropping it on the floor; the Stage 3 checkpoint (`vm_core.c`) checks it
*before* ever attempting to resume a pending switch target, and calls the new
`capsule_vm_force_reap()` instead -- the one caller allowed to bypass `capsule_vm_kill()`'s own
refusal, because it runs at the exact safe cooperative point the switcher itself controls, the
same reasoning the original plan named. A new idle-tick sweep
(`capsule_wirebind_reap_idle_check()`) cleans up the stale live-table entry once the reap has
actually happened.
**Reap-mechanism verification (temporary probe, reverted, per this project's write-probe-
capture-revert discipline):** the mechanism has no real hardware timing dependency by design
(it operates purely on registry/switch-signal state), so proving it live against real WIREBIND
hardware timing would have tested the choreography, not the mechanism. Used Artemis as a safe
stand-in "parked identity" instead: force her into `VM_STATE_SWITCHED_OUT`, confirm
`capsule_vm_kill()` refuses, set `pending_reap`, call `capsule_vm_force_reap()` directly, confirm
`state == DEAD`/`vm_ptr == NULL`/`pending_reap == 0`/switch-signal slot released. 3/3 checks PASS
on all 3 architectures (Artemis is deliberately left dead for the remainder of each probe boot --
documented as expected, not a defect). Probe reverted; all 3 architectures re-verified clean at
rest afterward, no `REAP-TEST` output, Artemis alive and normal.
**Increment 4 -- live multiuser verification against real WIREBIND hardware (no code changes,
verification only).** A real, previously-unattached identity thumbdrive (`bob-thumb-ident.img`,
whose on-disk username is actually `rajames` -- confirmed live, not assumed) attached via QMP
`device_add` on a running boot, sequential per §XIV's own established workaround (one identity,
wait for `WIREBIND: rajames attached and ready`, no simultaneous-attach race attempted -- §XIV
itself remains open, sidestepped not fixed, exactly as §XXIX left it). Verified on all 3
architectures: `S" 1 1 + . " S" rajames~user" VM-EXEC` correctly computed `2` inside her own
live VM, tagged `[zuse@rajames~user]` in the console output, with the full Tripod fleet
simultaneously live and unaffected. `EJECT` (amd64) cleanly tore down both the console and user
VM, flushed, and released the switch-signal slot -- no leak, no crash, confirming increment 1's
per-device fix holds under a real live attach/detach, not just the reap probe's synthetic one.
**A real, pre-existing bug found during this verification, confirmed NOT caused by Stage 4:**
interactive `USE <identity>` on a freshly-WIREBIND-attached identity halts the kernel outright
(`VM fault -- emergency console disabled; halting`, the genuine C-level fault handler, not a
graceful FORTH-level error) -- reproduced identically on commit `1c220ad` (the last commit
before any Stage 4 work) via a temporary `git checkout` to that commit, same attach, same `USE
rajames`, same crash. Root cause not chased further this pass (out of Stage 4's own scope) --
plausibly never caught before because every prior identity-correctness campaign (§XII.4, §XV,
§XVI) drove identities via direct `VM-EXEC` dispatch, never through the interactive
`USE`/BINDSTEP console-relay path this specific crash requires. `VM-EXEC` dispatch into the same
identity, immediately before and after, works correctly -- this is specific to the `USE`
codepath (`mama_word_use()`, `mama_forth_words.c`), not a general identity-attach defect.
Flagged for a future pass, not fixed here.
**Verification method note, matching prior sessions' own established practice:** live device
attach used real QMP `blockdev-add`/`device_add` against the running instance's own QMP socket,
not a build-time `QEMU_EXTRA` mechanism -- confirmed identical to the technique §XIV/§XV already
used. Interactive console input used QMP `send-key`, extended this pass to handle uppercase
letters (shift+lowercase) and symbols needed for real command lines (`~`, `"`, `+`) -- the
existing `1 1 + .` probe from §XXVIII.2 only needed digits and `+`.
**Status: Stage 4's original scope (WIREBIND VMs as switch-signal participants, with the async-
detach UAF risk closed per the plan's own ratified design) is complete and verified live on all
3 architectures, real hardware-driven identity attach included.** Multitasking (fixed Tripod
fleet) and multiuser (WIREBIND identities) are now genuinely composed, not just coexisting.
Remaining open items, unchanged in status from §XXIX plus the one new finding: §XIV (concurrent
attach detection gap, still open, sidestepped via sequential attach), the MSG-TICK/Stage-3
dual-ownership rough edge (still open, not observed failing), and the newly-found pre-existing
`USE`/BINDSTEP crash (flagged, not fixed).
## XXXI. std79 DoE campaign rerun post-Stage-4: 81/81, plus §XIV caught live by the rerun's own
test harness (2026-09-15)
**Bob's explicit request: rerun the established 3×9×3 randomized full-factorial campaign
(§XV/§XVI, `experiments/std79-doe/std79-doe.fth`) on all 3 architectures**, matching this
project's own standing discipline that any Stadium-adjacent mechanism change reruns the whole
DoE from the top, not just a fresh 3-arch boot smoke test.
**A real harness bug caught §XIV live, on the first attempt.** The orchestration script's
sequential-attach wait loop checked for *any* recent `WIREBIND: <name> attached and ready` line
rather than the *specific* identity just requested, so it repeatedly reported success within a
couple of seconds and fired the next `device_add` before the kernel had genuinely finished the
current one -- exactly the near-simultaneous-attach pattern §XIV names as the trigger. Result on
amd64's first attempt: only 2 of 8 identities (`rajames`, `06`) ever actually attached; the
other 6 silently never triggered their own `WIREBIND: <id> attached` line at all, matching §XIV's
own description precisely. **The campaign itself did not corrupt anything or crash** -- `VM-EXEC`
dispatch into a never-attached identity name resolves cleanly to `VM-EXEC: VM not found: <name>`
and returns, so those specific trials' computational content simply never ran while the
DOE-RUN/header bookkeeping around them still printed and the run reached "STD79-DOE: complete"
normally. Caught by checking `VM-EXEC: VM not found` occurrence count in the resulting log
(non-zero, should always be zero) before trusting the run -- not by anything failing loudly.
Fixed the wait loop to confirm the specific identity by name with a per-identity timeout and a
hard abort on failure; discarded the invalid first amd64 run's campaign result (its own boot log
kept, not discarded, per this project's logs-are-audit-artifacts convention) and reran clean.
**Clean reruns, all 3 architectures, corrected harness:** all 8 non-Zuse identities confirmed
individually attached (26-52s each, consistent with historical single-identity attach timing),
zero `VM-EXEC: VM not found` occurrences, zero `VM fault` halts, `STD79-DOE: complete` reached on
every boot. All 27 run_ids (0-26) present on every architecture, verified both by direct scan and
by hand-tracing the known cosmetic CSV-splice artifact (heartbeat CSV rows interleaving with
trial `DOE-RUN,` output on the shared serial line, documented since 2026-09-12) across the one or
two run_ids each raw log happened to splice mid-token. **Result: 81/81 trials correct across
amd64/aarch64/riscv64, zero mismatches from established baseline values** (the same
distinctive `M*`/`M/MOD`/`2147483648 2/` markers this project has checked every prior run).
Raw logs archived at `experiments/std79-doe/results-20260915-stage4/` per the existing directory
convention; see that directory's own README entry for the harness-bug detail in brief.
**Scope note, stated explicitly so a future reader doesn't over-claim from this result:** this
campaign drives every identity via direct `VM-EXEC` dispatch, unchanged since §XV -- it does
**not** itself exercise Stage 4's new switch-signal participation (WIREBIND VMs as genuine
preemption targets). That was verified separately, live, in §XXX (real QMP-driven attach +
`VM-EXEC` compute-check + `EJECT` teardown, one identity, all 3 architectures). This campaign's
own value here is what it has always been: a large-N cross-identity, cross-architecture
correctness regression check, now re-confirmed clean after Stage 4's registry/switch-signal
plumbing changes -- not a preemption-specific test.
+1 -1
View File
@@ -1,5 +1,5 @@
# Capsule Block Manifest — Auto-generated
<!-- Generated by mkcapsule --manifest 2026-09-15T00:05:32Z -->
<!-- Generated by mkcapsule --manifest 2026-09-15T08:37:42Z -->
<!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. -->
<!-- Hand-written justifications and immutability notes live -->
<!-- in MANIFEST.md alongside this auto-generated index. -->
BIN
View File
Binary file not shown.
+18
View File
@@ -95,3 +95,21 @@ sd(K) = 0, 100% conserved, across all three architectures, nine identities, and
replicates — zero deviation.** See `report-20260912/README.md` for the full pipeline and the
report itself for the complete analysis and discussion, including why this also serves as a
free regression check on the two Stadium fixes (§XVI–XVII) that landed the same week.
`results-20260915-stage4/` — rerun after FABRIC-3.md §XXVIII Stage 4 (WIREBIND VMs as Stage 3
switch-signal participants + mark-and-defer tombstone reap), same discipline as every prior
Stadium-adjacent change: any relevant mechanism change reruns the whole DoE from the top, one
boot per architecture, all 9 identities simultaneously live, same seed (12345). **Result: 81/81
trials correct, 0 mismatches, 0 `VM-EXEC: VM not found` errors, zero `VM fault` halts, on all
three architectures.** Note for anyone rerunning this: the sequential-attach wait loop must
confirm each identity's own specific `WIREBIND: <name> attached and ready` line before
proceeding to the next — a loop that just checks for *any* recent WIREBIND-attached line will
spuriously "succeed" immediately and fire the next `device_add` before the kernel has actually
finished the current one, re-triggering §XIV's own known concurrent-attach detection gap (caught
live during this rerun: a first attempt with a buggy wait loop attached only 2 of 8 identities,
though the campaign itself still completed cleanly with graceful `VM-EXEC: VM not found`
refusals for the missing ones rather than corrupting anything — the harness bug, not a kernel
defect). This campaign still drives every identity via direct `VM-EXEC` dispatch, same as every
prior run — it does not itself exercise Stage 4's new switch-signal participation, which was
verified separately (see FABRIC-3.md §XXX for the live multiuser-attach + preemption-fleet
verification that does).
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
+28
View File
@@ -294,6 +294,34 @@ int capsule_vm_find_by_name_nocase(const char *name, VMRegistryEntry *out);
*/
void capsule_vm_set_state(VMUuid vm_id, uint32_t state);
/**
* capsule_vm_set_pending_reap - FABRIC-3.md §XXVIII Stage 4 (2026-09-14):
* mark vm_id for deferred teardown once it is no longer worth resuming --
* see VMRegistryEntry.pending_reap's own doc comment for the full
* rationale. No-op if vm_id isn't registered.
*
* @param vm_id VM ID to mark.
* @param pending 1 to mark, 0 to clear (e.g. a re-attach of the same
* identity before the switcher ever reaped it).
*/
void capsule_vm_set_pending_reap(VMUuid vm_id, int pending);
/**
* capsule_vm_force_reap - FABRIC-3.md §XXVIII Stage 4 (2026-09-14):
* unconditionally tear down vm_id regardless of VM_STATE_SWITCHED_OUT --
* the one caller allowed to bypass capsule_vm_kill()'s own refusal there,
* because this is called *by* the Stage 3 switcher itself (vm_core.c's
* checkpoint, on noticing pending_reap set), at the one point that
* genuinely knows the parked native-stack context will never be resumed.
* Also releases the VM's own switch-signal slot
* (sk_vm_switch_signal_unregister()) -- generic cleanup, independent of
* whatever subsystem (WIREBIND today) set pending_reap in the first
* place. No-op if vm_id isn't registered or is already DEAD.
*
* @param vm_id VM ID to reap.
*/
void capsule_vm_force_reap(VMUuid vm_id);
/**
* capsule_vm_registry_set_name - Assign a symbolic name to a registered VM
*
+17
View File
@@ -128,6 +128,23 @@ typedef struct {
* own quota at birth, explicitly evicted at
* KILL. Not Hera's; she is pinned and never
* reaches this field's purpose. */
int pending_reap; /* FABRIC-3.md §XXVIII Stage 4 (2026-09-14):
* set (capsule_vm_set_pending_reap()) when
* this VM is VM_STATE_SWITCHED_OUT and its
* owning WIREBIND device has physically
* detached -- capsule_vm_kill() correctly
* refuses to free a parked context, but the
* memory still needs reclaiming once it's
* no longer coming back. Deliberately a
* plain flag, not a new VMState: `state`
* still accurately reads SWITCHED_OUT (a
* live context genuinely is parked there)
* until the Stage 3 checkpoint
* (vm_core.c) notices this flag instead of
* attempting to resume it, and calls
* capsule_vm_force_reap() there instead --
* a safe point the switcher itself
* controls, not the async detach handler. */
} VMRegistryEntry;
/*===========================================================================
@@ -65,6 +65,17 @@
* critical-section protection against being switched away mid-setup). */
int sk_vm_switch_signal_register(VMUuid vm_id);
/* Remove a switch-signal participant (FABRIC-3.md §XXVIII Stage 4,
* 2026-09-14) -- Tripod VMs never need this (they live forever), but
* WIREBIND-birthed identity VMs cycle through attach/detach repeatedly
* and must free their slot for reuse, or the bounded table exhausts
* after SK_SWITCH_MAX_SLOTS attach/detach cycles. Compacts the table
* (small, bounded, mutated only at attach/detach -- not a hot path).
* Clears a pending switch targeting this VM, if any, so the checkpoint
* never attempts to switch into a no-longer-registered participant.
* No-op (returns -1) if vm_id was never registered. */
int sk_vm_switch_signal_unregister(VMUuid vm_id);
/* Called from heartbeat_tick() (ISR context) every timer tick. Cheap:
* iterates only the registered slots (bounded, small). */
void sk_vm_switch_signal_tick(void);
+36 -10
View File
@@ -92,9 +92,16 @@ void capsule_wirebind_try_attach(struct blkio_dev *dev,
* optional, to avoid a dangling console pointer; capsule_vm_kill() by
* name; clear the tracked state.
*
* Single-USB-device constraint (§F.8) means there is never more than one
* candidate, so this always targets "whatever's currently attached" --
* no name argument.
* EJECT is inherently about "whichever identity is currently paired to
* the one physical console" -- targets that singleton, no name argument.
* (Corrected 2026-09-14: this used to claim a "single-USB-device
* constraint (§F.8)" meant there was never more than one candidate --
* stale even at the time this correction was written; §XV/§XVI
* (2026-09-11/12) proved 9 identities genuinely simultaneously live via
* this same attach path. EJECT staying console-singleton-scoped is a
* deliberate UX choice now, not a hardware constraint -- see
* capsule_wirebind_unclean_detach()'s own doc below for the function
* that DOES need to reach every live identity, not just this one.)
*
* @return 0 on success, -1 if nothing was attached to eject.
*/
@@ -107,14 +114,21 @@ int capsule_wirebind_eject(void);
* bot_msc_detach_pending hot-unplug signal (repl.c) -- the device is
* already gone by the time this runs, so no flush is attempted; data
* since the last flush is lost, which is correct unclean-removal
* semantics. Otherwise identical to capsule_wirebind_eject(): same
* active-VM reset-before-kill step, same tracked-state clear.
* semantics.
*
* FABRIC-3.md §VII follow-on, 2026-09-06: now requires the departing
* device to actually be the one tracked as this WIREBIND user's own
* (g_wirebind_attached_dev) -- a real bug otherwise, found live once
* genuine multi-device attach made a *different* device's detach
* reachable while a WIREBIND user's own stayed attached.
* FABRIC-3.md §VII follow-on, 2026-09-06: requires the departing device
* to actually be one WIREBIND is tracking -- a real bug otherwise, found
* live once genuine multi-device attach made a *different* device's
* detach reachable while a WIREBIND user's own stayed attached.
*
* FABRIC-3.md §XXVIII Stage 4, 2026-09-14: resolves the departing device
* against a per-device live-identity table now, not the single
* console-pairing global capsule_wirebind_eject() uses -- with several
* identities simultaneously live (§XV/§XVI), any one of them can be the
* device that just disappeared, not only the most recently attached.
* Refuses (rather than freeing) a VM currently VM_STATE_SWITCHED_OUT --
* see capsule_vm_kill()'s own guard -- leaving it tracked for the Stage 3
* switcher to reap on its own next resume attempt instead.
*
* @param dev The device that just detached; every other value is a no-op.
*/
@@ -153,6 +167,18 @@ const char *capsule_wirebind_attached_username(void);
*/
void capsule_wirebind_overflow_idle_check(void);
/**
* capsule_wirebind_reap_idle_check - FABRIC-3.md §XXVIII Stage 4
* (2026-09-14): sweeps the per-device live-identity table for entries
* whose VM has been reaped (capsule_vm_force_reap(), called from the
* Stage 3 checkpoint on a pending_reap target -- see that function's own
* doc comment) and removes the now-stale table entry. Does NOT itself
* free anything or call capsule_vm_kill()/force_reap() -- purely
* bookkeeping cleanup after the fact. Called at the same ~1 Hz idle
* cadence as capsule_wirebind_overflow_idle_check().
*/
void capsule_wirebind_reap_idle_check(void);
#endif /* __STARKERNEL__ */
#endif /* STARKERNEL_CAPSULE_WIREBIND_H */
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
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
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
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
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
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
+57
View File
@@ -37,6 +37,7 @@
#include "starkernel/vm/arena.h" /* FABRIC-3.md §XXVIII Stage 1 -- sk_vm_native_stack_alloc() */
#include "starkernel/session.h" /* session_register()/session_set_pinned() -- FABRIC-2.md §H.12 step 5 */
#include "starkernel/timer.h" /* timer_calibration_record()->vm_mode -- FABRIC-2.md §I.5 CONTRIB trust tier */
#include "starkernel/capsule_vm_switch_signal.h" /* FABRIC-3.md §XXVIII Stage 4 -- capsule_vm_force_reap() */
#include "vm.h"
#include "platform_alloc.h"
/* No LOG_LINE_MAX include-order constraint anymore: vm.h's own
@@ -323,6 +324,62 @@ void capsule_vm_set_state(VMUuid vm_id, uint32_t state) {
if (entry) entry->state = state;
}
void capsule_vm_set_pending_reap(VMUuid vm_id, int pending) {
VMRegistryEntry *entry = vm_find_entry_ptr(vm_id);
if (entry) entry->pending_reap = pending;
}
void capsule_vm_force_reap(VMUuid vm_id) {
VMRegistryEntry *entry = vm_find_entry_ptr(vm_id);
if (!entry) return;
/* Idempotent, matching capsule_vm_kill()'s own convention -- the
* Stage 3 checkpoint that calls this cannot itself know whether some
* other path (a later re-attach's own teardown, say) already reaped
* this exact VM before the checkpoint got to it. */
if (entry->state == VM_STATE_DEAD) return;
/* Hera is never a switch-signal participant subject to pending_reap
* in the first place, but refuse on principle anyway -- matches
* capsule_vm_kill()'s own unconditional rule. */
if (vm_uuid_is_hera(entry->vm_id)) return;
VM *vm = (VM *)entry->vm_ptr;
/* Deliberately NOT capsule_vm_kill()'s VM_STATE_SWITCHED_OUT refusal
* -- that guard exists precisely for the case this function handles:
* the caller (vm_core.c's Stage 3 checkpoint) has already decided,
* at a safe cooperative point it alone controls, that this parked
* native-stack context will never be resumed. Freeing it here is the
* deferred half of the mark-and-defer design (FABRIC-3.md §XXVIII
* Stage 4) -- see VMRegistryEntry.pending_reap's own doc comment. */
if (entry->stadium_patron_cell != STADIUM_CELL_NONE) {
(void)stadium_evict(entry->stadium_patron_cell);
entry->stadium_patron_cell = STADIUM_CELL_NONE;
}
if (vm) {
vm_cleanup(vm);
sf_free(vm);
}
/* Generic cleanup, independent of whatever subsystem set
* pending_reap -- a reaped VM is no longer a switch-signal
* participant either way. */
sk_vm_switch_signal_unregister(vm_id);
capsule_parity_log_kill(vm_id, entry->name);
console_puts("REAP: ");
console_puts(entry->name[0] ? entry->name : "(unnamed)");
console_println(" tombstone reclaimed");
entry->vm_ptr = (void *)0;
entry->state = VM_STATE_DEAD;
entry->pending_reap = 0;
for (uint32_t i = 0; i < VM_NAME_MAX; i++) entry->name[i] = '\0';
}
void capsule_vm_registry_set_name(VMUuid vm_id, const char *name) {
VMRegistryEntry *entry;
if (!name) return;
@@ -32,9 +32,14 @@
#include "starkernel/vm/switch.h" /* sk_vm_switch_current_vm() -- see below */
#include <stddef.h>
/* Headroom beyond today's 3 participants (Hera/Hermes/Artemis) -- see
* the header's own doc comment on why this isn't hardcoded to 3. */
#define SK_SWITCH_MAX_SLOTS 8
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): bumped from 8 to 16 to admit
* WIREBIND identity VMs alongside the fixed Tripod fleet. 16 is not a new
* guess -- it matches messaging.4th's own VM-MAX (the real, already-
* agreed system-wide ceiling: 3 permanent Tripod slots + 13 for
* identities, "identities get NEW slots 3-10, never renumbered" plus
* headroom to VM-MAX itself). See feedback_no_hardcoded_small_scale_bounds
* in project memory -- an invented cap here was rejected before. */
#define SK_SWITCH_MAX_SLOTS 16
/* Ticks a non-running participant must accumulate readiness before a
* switch to it is requested. Simple linear accumulate-then-threshold,
@@ -82,6 +87,29 @@ int sk_vm_switch_signal_register(VMUuid vm_id) {
return g_slot_count++;
}
int sk_vm_switch_signal_unregister(VMUuid vm_id) {
int slot = slot_for_vm_id(vm_id);
if (slot < 0) return -1;
for (int i = slot; i < g_slot_count - 1; i++) {
g_slots[i] = g_slots[i + 1];
}
g_slot_count--;
/* A pending switch targeting this VM must not survive its removal --
* the checkpoint in execute_colon_word() would otherwise try to
* switch into a VM no longer tracked here. */
if (g_pending && vm_uuid_equal(g_pending_target, vm_id)) {
g_pending = 0;
}
/* g_current_slot_cached is a DoE-observable value only (see its own
* declaration comment); if compaction just invalidated it, the very
* next tick() recomputes it from scratch via slot_for_vm_id(), so a
* momentarily-stale cache here is harmless. */
return 0;
}
void sk_vm_switch_signal_tick(void) {
VM *current;
int current_slot, i;
+193 -29
View File
@@ -19,6 +19,7 @@
#include "starkernel/user_identity_seed.h"
#include "starkernel/console.h"
#include "starkernel/repl.h" /* sk_repl_get/set_active_vm() -- FABRIC-2.md §F.10 EJECT */
#include "starkernel/capsule_vm_switch_signal.h" /* FABRIC-3.md §XXVIII Stage 4 -- register/unregister */
#include "blkio.h"
#include "block_subsystem.h" /* compute_crc64() */
#include "word_source/include/block_words.h" /* blk_vm_flush_all() -- §F.10 EJECT */
@@ -45,6 +46,63 @@ static char g_wirebind_attached_username[USER_IDENTITY_USERNAME_MAX] = {0};
* without a new attach-time argument threaded through the idle loop. */
static struct blkio_dev *g_wirebind_attached_dev = (struct blkio_dev *) 0;
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): the singleton above correctly
* models "who is paired to the one physical console" (repl.c's own
* console-relay/`(user)` prompt logic genuinely is single-valued -- there
* is only one serial/framebuffer console). It does NOT correctly model
* "which devices are live," despite g_wirebind_attached_eject()'s own
* header doc claiming a "single-USB-device constraint (§F.8)" -- that
* claim is stale: §XV/§XVI (2026-09-11/12) proved 9 identities genuinely
* simultaneously live via this exact attach path. Every attach after the
* first silently overwrote the singleton, so capsule_wirebind_unclean_
* detach()'s `dev != g_wirebind_attached_dev` check only ever matched the
* *most recently attached* identity -- any earlier identity's drive
* being pulled hit that check, returned early, and orphaned: never torn
* down, its VM and native stack leaked forever, no console/log trace at
* all. Silent because nothing in this project's verification history
* ever detached anything but the single most-recent identity.
*
* This new table is additive and separate from the console singleton
* above (which is untouched, still governs console pairing exactly as
* before) -- it tracks every currently-live WIREBIND identity by its own
* device pointer, so unclean-detach (and Stage 4's own switch-signal
* registration bookkeeping) can resolve *any* attached device to *its
* own* identity, not just the last one. Sized off messaging.4th's own
* VM-MAX (16) minus the 3 slots Hera/Hermes/Artemis permanently reserve
* (capsules/common/messaging.4th: "identities get NEW slots 3-10, never
* renumbered") -- a real, already-agreed system-wide ceiling, not a new
* guess (see feedback_no_hardcoded_small_scale_bounds in project memory:
* Bob has rejected an invented cap here before). */
#define WIREBIND_MAX_LIVE 13u
typedef struct {
int in_use;
struct blkio_dev *dev;
VMUuid user_vm_id;
char username[USER_IDENTITY_USERNAME_MAX];
int switch_slot; /* Stage 4: index into the switch-signal
* table, or -1 if not yet registered
* (wired in the next increment). */
} wirebind_live_entry_t;
static wirebind_live_entry_t g_wirebind_live[WIREBIND_MAX_LIVE];
static wirebind_live_entry_t *wirebind_live_find_by_dev(struct blkio_dev *dev) {
for (uint32_t i = 0; i < WIREBIND_MAX_LIVE; i++) {
if (g_wirebind_live[i].in_use && g_wirebind_live[i].dev == dev) {
return &g_wirebind_live[i];
}
}
return (wirebind_live_entry_t *)0;
}
static wirebind_live_entry_t *wirebind_live_alloc(void) {
for (uint32_t i = 0; i < WIREBIND_MAX_LIVE; i++) {
if (!g_wirebind_live[i].in_use) return &g_wirebind_live[i];
}
return (wirebind_live_entry_t *)0;
}
/* WIREBIND_CERT_MAX_DEVBLOCKS: a sane upper bound on how much cert
* content this reads, independent of whatever sig->cert_devblocks
* claims -- MINT (capsule_mint.c) only ever writes 1 devblock's worth
@@ -224,6 +282,42 @@ void capsule_wirebind_try_attach(struct blkio_dev *dev,
g_wirebind_attached_dev = dev;
memcpy(g_wirebind_attached_username, username, sizeof(g_wirebind_attached_username));
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): additive per-device
* tracking, independent of the console-pairing singleton just above
* (which stays exactly as it was) -- see g_wirebind_live[]'s own doc
* comment. A full table is reported, not silently dropped: running
* out of the real, sourced ceiling here means something is actually
* wrong (more identities live than this system's own VM-MAX allows
* for), worth knowing about rather than quietly leaving a device
* untracked for unclean-detach/switch-signal purposes. */
{
wirebind_live_entry_t *live = wirebind_live_alloc();
if (live) {
live->in_use = 1;
live->dev = dev;
live->user_vm_id = user_id;
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): the user VM is
* the one that actually runs FORTH work (VM-EXEC/MSG-TICK
* dispatch target it directly) -- it participates in
* preemptive switching. The console VM does not: it is a
* REPL-session proxy whose own control flow
* sk_repl_get_active_vm() already owns, never itself the
* target of VM-EXEC or MSG-SEND, so it has nothing for a
* switch-in to preempt. Registration failure (table full)
* degrades to "this identity won't be preempted," not to a
* failed attach -- the identity is still fully usable via
* ordinary VM-EXEC/console-relay dispatch, same as before
* Stage 4 existed. */
live->switch_slot = sk_vm_switch_signal_register(user_id);
memcpy(live->username, username, sizeof(live->username));
if (live->switch_slot < 0) {
console_println("WIREBIND: switch-signal table full -- this identity won't be preempted");
}
} else {
console_println("WIREBIND: live-identity table full -- detach/switch tracking degraded for this attach");
}
}
/* Decided 2026-09-05: no console for the running system unless a
* thumbdrive is present. Revised 2026-09-06: this used to call
* sk_console_mark_login() here (a one-way sticky flag) -- the gate is
@@ -281,15 +375,18 @@ static int wirebind_resolve_attached(VMRegistryEntry *out) {
* second identity right after an unclean detach of a first left the
* console stuck showing the first identity's own dead-paired prompt.
*
* Uses g_wirebind_attached_username (the bare username, captured at
* attach time) rather than deriving it from entry.name (which is
* already the "<user>~user" registry name) -- see capsule_wirebind_
* try_attach()'s own comment on why the two names are tracked
* separately. Called before either caller clears that field. */
static void wirebind_teardown_console(void) {
if (g_wirebind_attached_username[0] == '\0') return;
* Takes the bare username explicitly (FABRIC-3.md §XXVIII Stage 4,
* 2026-09-14 -- was g_wirebind_attached_username directly, which only
* ever names the single console-paired identity; unclean-detach of any
* other simultaneously-live identity needs its own username, not the
* console-pairing singleton's, so this can no longer read the global
* itself). capsule_wirebind_eject() still passes the singleton's own
* username, since EJECT (a typed command) is inherently about "whichever
* identity is currently console-paired." */
static void wirebind_teardown_console(const char *username) {
if (!username || username[0] == '\0') return;
VMRegistryEntry console_entry;
if (capsule_vm_find_by_name(g_wirebind_attached_username, &console_entry) != 0 ||
if (capsule_vm_find_by_name(username, &console_entry) != 0 ||
console_entry.state != VM_STATE_LIVE) {
return;
}
@@ -299,6 +396,24 @@ static void wirebind_teardown_console(void) {
capsule_vm_kill(console_entry.name);
}
static void wirebind_live_remove(wirebind_live_entry_t *e) {
if (!e) return;
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): centralized here rather
* than at each of eject()/unclean_detach()'s own call sites -- both
* paths that legitimately free this VM (as opposed to the tombstone
* path, which does not call this while a VM is still switched-out)
* must release its switch-signal slot the same way, or repeated
* attach/detach cycles exhaust the bounded slot table. No-op if it
* was never registered (table was full at attach time). */
if (e->switch_slot >= 0) {
sk_vm_switch_signal_unregister(e->user_vm_id);
}
e->in_use = 0;
e->dev = (struct blkio_dev *) 0;
e->username[0] = '\0';
e->switch_slot = -1;
}
int capsule_wirebind_eject(void) {
VMRegistryEntry entry;
if (wirebind_resolve_attached(&entry) != 0) {
@@ -310,8 +425,9 @@ int capsule_wirebind_eject(void) {
* remove (§F.10 decision 1). */
blk_vm_flush_all((VM *)entry.vm_ptr);
wirebind_teardown_console();
wirebind_teardown_console(g_wirebind_attached_username);
capsule_vm_kill(entry.name);
wirebind_live_remove(wirebind_live_find_by_dev(g_wirebind_attached_dev));
g_wirebind_attached_valid = 0;
g_wirebind_attached_dev = (struct blkio_dev *) 0;
g_wirebind_attached_username[0] = '\0';
@@ -323,32 +439,80 @@ int capsule_wirebind_eject(void) {
}
void capsule_wirebind_unclean_detach(struct blkio_dev *dev) {
/* FABRIC-3.md §VII follow-on, 2026-09-06: same defect class as
* capsule_zuse_boot_logout()'s own fix, found in the same live
* verification session -- this used to take no device parameter at
* all, so an unrelated device detaching (Zuse's own drive, or general-
* purpose USB use) while a WIREBIND user stayed attached would
* incorrectly tear down that user's session too. The single-USB-
* device constraint this was written under never let a *different*
* device be the one detaching while a WIREBIND user's own stayed live
* -- stale now that genuine multi-device attach exists. */
if (dev != g_wirebind_attached_dev) return;
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): was `dev !=
* g_wirebind_attached_dev`, comparing against the single console-
* pairing global -- correct only for the single most-recently-
* attached identity (see g_wirebind_live[]'s own doc comment above
* for the real bug this caused: every earlier identity's device-pull
* was silently ignored, leaking that VM forever). Now resolves via
* the per-device live table instead, so any of several simultaneously
* live identities' own device-pull is handled correctly. */
wirebind_live_entry_t *live = wirebind_live_find_by_dev(dev);
if (!live) return; /* not a tracked WIREBIND device, or already torn down */
VMRegistryEntry entry;
if (wirebind_resolve_attached(&entry) != 0) return;
if (capsule_vm_registry_get(live->user_vm_id, &entry) != 0 ||
entry.state == VM_STATE_DEAD || entry.state == VM_STATE_STILLBORN) {
wirebind_live_remove(live);
return;
}
/* Device is already gone (§F.10 decision 2) -- no flush attempted;
* data since the last flush is lost, matching real-OS unclean-removal
* semantics. */
wirebind_teardown_console();
capsule_vm_kill(entry.name);
g_wirebind_attached_valid = 0;
g_wirebind_attached_dev = (struct blkio_dev *) 0;
g_wirebind_attached_username[0] = '\0';
* semantics. The console VM has no switch-signal participation (only
* user VMs do, once Stage 4's registration lands) so it is always
* safely LIVE and killable regardless of the user VM's own state
* below. */
wirebind_teardown_console(live->username);
int killed = (capsule_vm_kill(entry.name) == 0);
console_puts("WIREBIND: ");
console_puts(entry.name);
console_println(" detached without flush (device already gone)");
/* Only clear the console-pairing singleton if THIS device is the one
* it currently tracks -- an unrelated identity's own detach must
* never disturb who is paired to the one physical console. */
if (g_wirebind_attached_valid && g_wirebind_attached_dev == dev) {
g_wirebind_attached_valid = 0;
g_wirebind_attached_dev = (struct blkio_dev *) 0;
g_wirebind_attached_username[0] = '\0';
}
if (killed) {
console_puts("WIREBIND: ");
console_puts(entry.name);
console_println(" detached without flush (device already gone)");
wirebind_live_remove(live);
} else {
/* capsule_vm_kill() refused -- VM_STATE_SWITCHED_OUT, a live
* register/stack context is parked on this VM's own native
* stack. Mark it for the Stage 3 checkpoint (vm_core.c) to reap
* on its own next attempt to resume this VM, instead of freeing
* memory a parked context still points into -- see
* VMRegistryEntry.pending_reap's own doc comment. The live-table
* entry stays in place until then: capsule_wirebind_reap_idle_
* check() below removes it once capsule_vm_force_reap() has
* actually run and the VM reads back DEAD. */
capsule_vm_set_pending_reap(live->user_vm_id, 1);
console_puts("WIREBIND: ");
console_puts(entry.name);
console_println(" device gone but VM is switched-out -- deferred reap");
}
}
void capsule_wirebind_reap_idle_check(void) {
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): the Stage 3 checkpoint
* (vm_core.c) is the only thing that actually frees a pending_reap
* VM -- see capsule_vm_force_reap()'s own doc comment for why (it
* must run at a cooperative point the switcher itself controls, not
* from this idle tick). This sweep only cleans up the now-stale
* live-table entry once that has already happened; it is not the
* reap itself and never calls capsule_vm_kill()/force_reap(). */
for (uint32_t i = 0; i < WIREBIND_MAX_LIVE; i++) {
if (!g_wirebind_live[i].in_use) continue;
VMRegistryEntry entry;
if (capsule_vm_registry_get(g_wirebind_live[i].user_vm_id, &entry) != 0 ||
entry.state == VM_STATE_DEAD) {
wirebind_live_remove(&g_wirebind_live[i]);
}
}
}
const char *capsule_wirebind_attached_username(void) {
+6
View File
@@ -642,6 +642,12 @@ static void sk_repl_idle(VM *active_vm)
* growth loop. */
capsule_wirebind_overflow_idle_check();
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): same cadence, cleans up
* the per-device live-identity table once the Stage 3 checkpoint has
* actually reaped a pending_reap VM -- see that function's own doc
* comment. */
capsule_wirebind_reap_idle_check();
/* FABRIC-2.md Phase C (2026-08-28): distributed messaging pump. Every
* live VM except Hera herself now owns its own MSG-ARENA/CH-ARENA and
* MSG-TICK word (see capsules/common/messaging.4th) instead of only
+18 -3
View File
@@ -929,9 +929,24 @@ void execute_colon_word(VM* vm)
VMRegistryEntry target_entry;
if (capsule_vm_registry_get(target_id, &target_entry) == 0 &&
target_entry.vm_ptr && target_entry.vm_ptr != (void *)vm) {
sk_vm_switch_signal_note_switch_performed(target_id);
sk_vm_context_switch(vm, (VM *)target_entry.vm_ptr);
/* Resumes here once something later switches back. */
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): a target
* marked pending_reap is a WIREBIND identity whose
* owning device has physically detached while it sat
* parked SWITCHED_OUT -- capsule_vm_kill() already
* refused to free it when that happened (see
* VMRegistryEntry.pending_reap's own doc comment).
* This checkpoint is the safe point the mark-and-
* defer design calls for: reap instead of resuming,
* and skip the switch this tick -- take_pending()
* already consumed the pending flag, so no other
* checkpoint will try this same dead end. */
if (target_entry.pending_reap) {
capsule_vm_force_reap(target_id);
} else {
sk_vm_switch_signal_note_switch_performed(target_id);
sk_vm_context_switch(vm, (VM *)target_entry.vm_ptr);
/* Resumes here once something later switches back. */
}
}
}
}