66c2f3e5399ae1ec937949fa4b246aea4bc64ea2
62
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5c5896fbc1 |
Stage A: fix USE's silent stack-underflow/bad-address guards, and the Hera fault-scoping gap they exposed (FABRIC-3.md §XXXII.1)
mama_word_use()'s two silent vm->error=1 guards (dsp<1 stack underflow, NULL from vm_ptr()) now print a diagnostic and return, matching the function's other five guards. Live verification of that fix alone surfaced a bigger problem: with the guard no longer silent, the REPL proceeds to interpret the leftover token as an unrecognized word, which independently sets vm->error, and sk_repl_step()/sk_repl_run()'s Hera-branch still hard-halted on that. Investigated kernel_main.c's boot/capsule-load paths directly: they already catch and clear mama->error entirely separately, before sk_repl_run() is ever entered -- so the "no fallthrough surface" halt in these two REPL functions was never protecting a boot-time fault, only an ordinary interactive REPL-turn one. Both functions now recover unconditionally on any VM's error, Hera included, matching how a redirected (WIREBIND/USE'd) identity's session already recovered. Removed the now-fully-unused sk_fault_handler(). Verified live, amd64: USE rajames at Zuse's own console prints the new diagnostic, then "VM fault -- session recovered, resuming", console stays interactive afterward. Three-arch clean qemu acceptance passed before and after the Hera-fault-scoping change. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BWpNjdwPtFLuVLaAq44L9K |
||
|
|
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 |
||
|
|
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 |
||
|
|
05ae7aa886 |
Fix SWITCH-MARK-WORK off-by-one: was tripping vm->error on every MSG-SEND (FABRIC-3.md §XXVIII.2)
mama_word_switch_mark_work()'s stack-underflow guard checked dsp < 2, requiring 3+ items, when it only ever needs the 2 IDX>NAME leaves it (caddr u). Since dsp is index-based (2 items == dsp 1), this rejected every normal call. MSG-SEND tail-calls SWITCH-MARK-WORK unconditionally, so this fired on every message sent anywhere in the system -- visible only where a caller happened to check the target VM's error flag afterward (mama_word_vm_exec()'s "VM-EXEC: ERROR in Artemis" report). Verified clean on all 3 architectures: boot reaches Startup: Artemis live -> zuse@Hera] ok> with no VM-EXEC: ERROR line at all. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BWpNjdwPtFLuVLaAq44L9K |
||
|
|
66beae7fd4 |
Stage 3 follow-on: message-arrival eligibility hook + trampoline-blind switch-storm fix (FABRIC-3.md §XXVIII.2)
Implements the message-arrival eligibility signal FABRIC-3.md §XXVIII.1 left open (has_work per-slot flag, set via new SWITCH-MARK-WORK primitive from MSG-SEND) so an idle VM never becomes a switch target purely by waiting out the readiness threshold. Also root-causes and fixes a second, independent switch-storm: the tick's "who is current" check used vm_log_attributed_vm(), which can't see a VM parked in switch.c's own raw trampoline. Replaced with a dedicated g_switch_current_vm tracked by the switch mechanism itself, and moved target-slot eligibility reset to the switch decision point instead of relying on ISR polling to observe a window that can be only a few instructions wide. Verified live on all 3 architectures: clean boot to zuse@Hera] ok>, live cross-VM message dispatch, and (since a quiet log looks identical to a livelocked storm once the DoE probe is gone) confirmed genuine REPL liveness via QMP send-key + screendump on aarch64/riscv64, not log inspection alone. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BWpNjdwPtFLuVLaAq44L9K |
||
|
|
862d7d9c48 |
Stage 3 follow-on: fix stack-ownership corruption + DoE switch columns (FABRIC-3.md §XXVIII.1)
DoE CSV gained 6 switch-signal columns (switch_count_cumulative, switch_current_slot, switch_*_readiness, switch_ticks_since), and verifying them with a boot-time HB-ON probe surfaced a real livelock: the preemption checkpoint could fire inside a VM-EXEC-nested execute_colon_word() call and switch away from a stack it didn't own, parking a borrowed region of the caller's stack under the wrong VM's saved-context pointer. The trampoline bounce was the visible (safe) half of this; the corruption was the quiet half, live in every prior "clean" Stage 3 boot without ever showing up in the log. Fixed by gating the checkpoint on being at the outermost vm_interpret() call (g_vm_interpret_depth / sk_vm_at_outermost_interpret(), vm_core.c), per Bob's decision. Also fixed two related bugs found in the same pass: g_switch_back_to was a single global stale after first entry, now per-VM state (native_switch_back_to); note_switch_performed() fired on resume instead of switch-out, now called before the switch. Verified on all 3 architectures: steady log growth (no freeze), zero leaked QEMU processes, DoE columns internally consistent, Hermes/Artemis confirmed genuinely executing (not just trampoline-bouncing). Temporary HB-ON boot probe reverted after capture. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016UNhH1mhi52i6Qihh7ZV5S |
||
|
|
986d042aa7 |
Stage 3: timer-driven preemptive switching, live on all 3 arches (FABRIC-3.md §XXVIII)
Fourth stage of the preemptive context-switching plan, and the biggest. LithosAnanke now genuinely, continuously preempts between Hera, Hermes, and Artemis -- timer-driven, running live for the entire remainder of every boot once the Tripod fleet registers, not a bounded probe. A real design fork was resolved before writing code: the naive approach (the timer ISR calling Stage 2's sk_vm_context_switch() directly) is broken -- Stage 0's trap frame lives on whatever stack was active at interrupt time, and jumping to a different stack via Stage 2's own independent swap mid-handler would abandon that trap frame unresumed, guaranteed corruption on the first tick. Chose the safer of two named options: the ISR only ever sets a flag and returns completely normally through its own full epilogue; the actual switch happens moments later, via Stage 2's already-proven mechanism, at a safe cooperative checkpoint on the mainline (execute_colon_word()'s per-word dispatch loop, checked on literally every word, not throttled to the existing 256-word heartbeat-tuning cadence) -- confirmed with the user that word-level granularity is fine-grained enough given the eventual Zynq FPGA target where a word is a mnemonic. New capsule_vm_switch_signal.c/.h: a purpose-built run-readiness signal, deliberately separate from capsule_vm_physics.c's execution-heat engine (that one's own header documents itself as never touched from interrupt context, by design). Slot table sized with headroom (8) rather than hardcoded to today's 3 participants, so extending participation later is another register() call, not a redesign -- per direct request to leave room for swapping the participant set. Simple linear accumulate-then- threshold for this first cut; a fancier law can replace it later without touching the mechanism around it. heartbeat_tick() gains its one deliberate, documented amendment to this file's own top-half/bottom-half discipline -- the first time this codebase reaches into VM-scheduling state from real ISR context. Registration happens only after all three VMs are fully born, right before the REPL starts -- no critical-section protection yet against being switched away mid-birth-setup. Known, flagged rough edge (not reconciled this pass): MSG-TICK's own idle-pump and this new mechanism can still independently move control between the same VMs; not observed to interact badly in verification, but not fully unified either. Verified interactively at the console on all 3 architectures with continuous background preemption running throughout -- amd64 computed `1 1 + .` -> 2, aarch64 computed `1 1 + dup DUP * . CR` -> 4, both correct, REPL fully responsive, zero fault indicators over sustained runtime. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016UNhH1mhi52i6Qihh7ZV5S |
||
|
|
f790d0995e |
Stage 2: cooperative VM context switch primitive, proven on all 3 arches (FABRIC-3.md §XXVIII)
Third stage of the preemptive context-switching plan. The real save/restore switch mechanism now exists -- the first time anything has ever executed on a VM's own native stack (Stage 1 allocated them, unused). New sk_vm_switch_to() (switch.S, one per arch) is an ordinary function call, not an interrupt -- so unlike Stage 0's trap frame, the ABI already covers every caller-saved register; only the callee-saved set needs explicit save/restore (amd64: rbx/rbp/r12-r15, no FP at all since SysV has no callee-saved XMM; aarch64: x19-x28/x29/x30 + d8-d15; riscv64: s0-s11/ra + fs0-fs11, FS-gated like Stage 0 but read once and reused for both halves within one call, since FS is genuine global CPU state, not part of what's switched). A sibling sk_vm_switch_prime() in the same file builds the synthetic first-entry frame, kept in assembly so the layout can never drift out of sync with sk_vm_switch_to() itself. New switch.c/switch.h: sk_vm_context_switch(from, to) handles first-entry priming vs. resuming a parked context, and updates registry state (new VM_STATE_SWITCHED_OUT, distinct from VM_STATE_STOPPED -- STOPPED means no live frame, this means the opposite). sk_vm_switch_entry() is the minimal permanent trampoline every freshly-entered VM lands in: no production behavior defined yet, so it just yields straight back to whoever switched to it, forever. Closes the confirmed unguarded-KILL UAF found during planning: capsule_vm_kill(), mama_word_kill(), and capsule_vm_kill_all_nonmama() all now refuse (or silently leak rather than free, on the cold-restart path where arch_cold_reset() wipes everything immediately after anyway) tearing down a switched-out VM. Side effect found, not built on purpose: the existing MSG-TICK idle-pump already filters on VM_STATE_LIVE, so it automatically stopped dispatching into a switched-out VM with zero changes needed there. Verified via a temporary SWITCH-TEST probe (boot-triggered, since nothing can type interactively into a foreground-only QEMU session) that round-tripped a sentinel through 5 real Hera<->Hermes switches on all 3 architectures: 5/5 rounds, 0 failures, clean continuation to ok>. Probe fully reverted after capture; kernel_main.c shows zero diff. Also: Makefile.starkernel's LOADER_EXTRA_SRCS/LOADER_ASM needed the new files added explicitly (this project's "loader" PE binary is the full running kernel, not a thin bootstrap stage), and aarch64's switch.S needed the same #ifndef _WIN32 guard around .hidden that isr.S already carries (aarch64's loader assembles via clang targeting a PE/COFF target with no .hidden equivalent) -- caught by a build failure, fixed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016UNhH1mhi52i6Qihh7ZV5S |
||
|
|
57ac3fc304 |
Stage 1: per-VM native stacks, allocated but not yet executed on (FABRIC-3.md §XXVIII)
Second stage of the preemptive context-switching plan. Every VM (Hera, every capsule_birth_baby()-born VM including WIREBIND identities) now gets its own dedicated 2 MiB native C stack at birth -- but nothing runs on it yet, that's Stage 2. Pure allocation-machinery proof. Design correction made before writing code: the plan called for cloning sk_vm_arena_alloc()'s guard-page pattern, but that pattern turns out to be Mama-only -- host_services.c's kernel_alloc() gives every baby VM a plain kmalloc() block for its dictionary arena, not a real guarded PMM allocation. Stacks get the real treatment instead (new sk_vm_native_stack_alloc()/_free() in arena.c): independent pmm_alloc_contiguous() + guard pages for every VM without exception, no singleton, no kmalloc fallback -- a stack overflow is exactly the failure mode guard pages exist for, and a corrupted stack could corrupt whatever saved context Stage 2 trusts. 2 MiB size matches this project's own established kernel-stack convention (g_kernel_stack/g_rpi5_native_stack), not a guess -- that one shared 2 MiB stack today already carries all VMs' combined nested VM-EXEC recursion. Three new VM struct fields, freed in vm_cleanup() alongside the existing call_stack free. Allocation failure is non-fatal to birth. All 3 architectures re-verified clean boot to ok>, no native-stack allocation failures for any Tripod-fleet VM. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016UNhH1mhi52i6Qihh7ZV5S |
||
|
|
a8b16d41da |
Unify console prompt to [user@VM]; fix real personality-block truncation; correct §XXV's wrong lockdown conclusion (FABRIC-3.md §XXVI)
Investigating the std79 lockdown finding from FABRIC-3.md §XXV led to a real discovery: WIREBIND births TWO VMs per identity, a console proxy under the plain username and the actual restricted identity under <username>~user (capsule_wirebind.c). Every test in §XXV targeted the console proxy, which was never locked down at all. Retested against the correct target (rajames~user): the lockdown works exactly as designed. §XXV's "lockdown never engages" conclusion was wrong -- corrected here, not deleted, since the mistake and how it was caught are worth keeping (see the new feedback memory: confirm which specific VM a name resolves to before concluding anything, when a subsystem is known to birth more than one VM per identity). Two real, separate things found along the way are kept regardless of that correction: - capsule_runcap.c: the reserved personality devblock was read in full (mostly zero-padding after a short ~200-byte string) with no terminator, producing "WARN: block 4998 exceeds 1KB, truncating" on every std79-locked identity's birth, universal, since at least 2026-09-10. Fixed by trimming to the first NUL byte actually found -- real, but harmless to execution (real content sat in the truncated block's surviving head); it mattered for capsule_id/ content_hash being computed over padding instead of real content. - console.h/console.c/repl.c: unified the prompt from a separately- computed "[VMName] (user)" into a single "[user@VMName]" line prefix -- exactly the ambiguity that caused the original misdiagnosis (the prompt showed only the WIREBIND username, identical whether USE had targeted the console proxy or the real ~user identity). Implemented as a registered callback (console_set_user_prefix_provider()) rather than console.c calling into WIREBIND/session logic directly, since console.c is a clean HAL module with no prior dependency on capsule-level subsystems. Verified: clean build on all 3 architectures, zero new warnings, identical dict_hash/capsule_hash to every prior boot this session (console/prompt-only change). Full 9-identity messaging campaign re-run end to end: 202s, zero faults, all 8 identities at 99/99 tokens, zero regression. Also surfaced, not yet acted on: the full campaign's own console tags now visibly show which VM each identity's tests actually reached ([zuse@rajames], not [zuse@rajames~user]) -- messaging.4th's VM-NAMES-INIT registers identities by plain username, so std79-doe. fth's turn-attractor has been dispatching to each identity's console proxy, not the actual locked-down identity, since the messaging rewrite. Flagged for a deliberate decision, not investigated further. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EXieurDfDSsDFdnSyusuWo |
||
|
|
3c2daf50d1 |
Extend BIRTH/CAPSULE-BIRTH to all VMs symmetrically; flag a real std79 lockdown gap (FABRIC-3.md §XXV)
Scoping the workload-into-factorial design's placement-mode factor led to a real architectural improvement: rather than EXEC-ing a workload capsule into an already-running, ACL-locked identity's own persistent dictionary (filesystem-shaped, doesn't dodge the block- collision exposure just traced in §XXIV), a workload now runs as a fresh ephemeral child VM, BIRTH'd per trial and reaped after -- matching the project's own stated principle of automanagement over imposed policy. CAPSULE-BIRTH already passes vm->stadium_vm_id (who is birthing this VM) as the new child's parent, not a hardcoded Hera constant, confirmed by reading the C -- so a workload trial genuinely inherits the specific identity's own lineage when that identity does the birthing. Which surfaced a real premise: only Hera could call BIRTH/ CAPSULE-BIRTH at all (registered only in register_mama_forth_words(), confirmed directly, not part of the earlier §XX messaging-symmetry fix which deliberately kept this as one of her remaining privileges). Extended symmetrically now, agreed explicitly before touching code: - mama_forth_words.c: BIRTH and CAPSULE-BIRTH added to register_child_vm_words(), matching §XX's own pattern. - acl-std79.4th: ' BIRTH , ' CAPSULE-BIRTH , added to ACL-STD79-LIST (new block 4048) -- a deliberate, explicit, named exception to the lockdown's own "standard words only" guarantee, not a silent one. Symmetric registration alone can't weaken any lockdown on its own: ACL-LOCKDOWN-STD79 is allowlist-based, deny-by-default, so a newly registered word is auto-denied there unless explicitly added. Verified: clean build on all 3 architectures, zero new warnings. Hera's own dict_hash unchanged (expected); Hermes/Artemis show the same new dict_hash on all 3 architectures. Live-tested against a real attached std79-locked identity: CAPSULE-BIRTH executes correctly (returns vm_uuid_none() for a deliberately out-of-range capsule-id, zero fault, zero ACL denial). Found, and explicitly stopped short of fixing, a separate pre- existing gap while verifying the above: MSG-STATUS and MSG-K (messaging.4th words, not on the std79 allowlist) execute for a locked identity instead of being denied. ACL-LOCKDOWN-STD79 is confirmed to actually run; something more specific isn't reaching messaging.4th's dictionary entries. Root cause not traced -- needs its own investigation into vm_core.c's dictionary-link mechanics and whichever capsule actually loads messaging for these identities. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EXieurDfDSsDFdnSyusuWo |
||
|
|
7edccd2c35 |
Hera can't message: root-caused and fixed by symmetry (FABRIC-3.md §XX)
Hera never loaded common:messaging.4th, unlike every other VM in the fleet. The standing belief was this was deliberate, to avoid moving her dict_hash off baseline. Checked live instead of assumed: loading messaging.4th into her dictionary silently dropped every colon- definition referencing one of 8 STADIUM-* primitives that register_child_vm_words() gives every other VM but register_mama_forth_words() never gave her -- a missing-primitive gap, not a designed privilege boundary. Confirmed mama_word_birth is genuinely VM-agnostic and SPAWN-EVENT is an unwired placeholder before proposing the fix. Fix: register the same 8 STADIUM-* primitives for Hera, load messaging.4th from init.4th the same way Hermes/Artemis/console/mint already do, and give her own idle-loop context a direct MSG-TICK call (not VM-EXEC, which would hit the same reentrancy class the existing per-other-VM pump loop already guards against) so her own queued messages actually drain. Her dictionary is now a proper superset of every child VM's, plus her remaining extra privileges -- not structurally different from any other VM, just additionally privileged. Verified: dict_hash identical across amd64/aarch64/riscv64 (0xc8f4b09e36f4fc4a), Hermes/Artemis dict_hashes unchanged and still cross-arch identical, all three boot clean to zuse)ok> with zero UNKNOWN WORD faults, mkcapsule --lint clean. Unblocks rewriting the turn-attractor (FABRIC-3.md §XIX) to coordinate via real MSG-SEND/MSG-TICK instead of blocking VM-EXEC. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EXieurDfDSsDFdnSyusuWo |
||
|
|
5a9425b91b |
Add VM-HEAT primitive: groundwork for a compudynamic turn-attractor (FABRIC-3.md §XIX)
Prompted by reading the std79-doe K report: checked whether the campaign's
"real cross-VM dispatch load" was actually concurrent or strictly
serialized. It's serialized at two levels -- VM-EXEC's vm_interpret(target,
...) is a direct synchronous C call (Hera fully blocked until it returns),
and even the background physics tick (vm_tick(), vm_runtime.c) is driven
by each VM's own execution loop, so idle identities accrue zero ticks
between their own turns. K's perfect conservation (FABRIC-3.md §XVIII)
verifies sequential per-VM accounting correctness, not concurrent-access
safety, since there was never concurrent access to test.
Agreed direction: fix this without a scheduler, by reusing the same
least-dense-candidate judgment stadium_admit() already trusts for
eviction, applied to "whose turn is next" instead of "who gets evicted" --
a fleet-level turn-attractor giving the next turn to whichever live
identity currently has the lowest execution_heat_q48, no fixed round-robin,
no priorities, no preemption. Lives beside Stadium in
capsule_vm_physics.c (already the fleet-level consumer of Stadium
primitives, e.g. the K mechanism itself), not inside stadium.c ("the
floor" -- residency/eviction, a different concern from turn order) and
not a new subsystem.
This pass lands only the primitive the mechanism needs: VM-HEAT
( c-addr u -- heat-q48 ), pushing a named VM's current
execution_heat_q48 via vm_physics_heat_of() -- previously C-internal
only (doe_log_heat_by_name(), doe_log.c), never exposed to FORTH. Silent
0 on an unknown/dead name (no print/error), since a turn-attractor
scanning many candidates every turn shouldn't have to filter console
noise for names that simply aren't live. Registered everywhere
VM-EXEC/VM-CALL already are. Builds clean on all three architectures;
live-tested on amd64: Hera -> 65452, Hermes -> 43, unknown name -> 0, no
faults.
The turn-attractor loop itself (std79-doe.fth's trial ordering) is not
yet built -- next step, not done here.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EXieurDfDSsDFdnSyusuWo
|
||
|
|
e51a8d229e |
Fix stadium_grant_quota() donor floor; rerun std79 DoE clean, 81/81 (FABRIC-3.md §XVII)
capsule_birth.c hardcoded every new VM's initial Stadium quota grant to split from Hera specifically. Since a grant always halves whatever the donor currently has, Hera's own free list converges toward empty after a bounded number of grants — independent of whether the Stadium as a whole still had spare capacity, since VMs she'd granted to earlier typically still held nearly all of their own share untouched. Past that point every subsequent VM birth's Stadium grant would be silently refused (soft-failed, non-fatal by existing design), even with plenty of capacity sitting idle elsewhere. Fixed by adding an O(1)-maintained free_count to StadiumVMQuota (incremented in stadium_evict(), decremented at both of stadium_admit()'s free-list-pop sites, set/adjusted in stadium_grant_quota()'s own split — this also let grant_quota drop its old O(free-list length) counting walk in favor of an O(1) read) and stadium_best_donor(), an O(live VM count) scan over quota slots returning whichever in-use VM currently has the most free cells. capsule_birth.c's birth path now splits from that VM instead of unconditionally vm_uuid_hera(). Verified with another full rerun of the 3x9x3 std79 DoE campaign from scratch — same discipline as the prior Stadium fix (any defect repair reruns the whole DoE from the top) — one continuous boot per architecture, all 9 identities simultaneously live throughout. 81/81 trials correct, 0 mismatches, DOE-RUN header sequence md5-identical to every prior run. aarch64 ~280s total (vs ~290s for the O(ncells)-scan fix alone — confirms no regression). Both known Stadium defects are now closed together on one clean campaign rerun. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EXieurDfDSsDFdnSyusuWo |
||
|
|
662ef44e59 |
Fix EXEC/LOAD block-persistence gap and WIREBIND/USE interpreter-race panic (FABRIC-3.md §XII)
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 |
||
|
|
b301317902 |
xHCI: drive Port Reset on port reuse; WIREBIND: kill the console VM too (FABRIC-3.md §XI.5)
Two independent bugs that together caused a reliable hotplug wedge: reusing an xHCI port for a second identity right after an unclean detach of a first would leave no further hotplug events reaching the guest at all. Bug 1 (xhci.c/xhci_driver.h): the xHCI driver never drove PORTSC.PR -- a known, named gap since Milestone 2e (the code's own comment flagged it, PORTSC_PR/PRC were defined but never referenced). A port's first connect each boot reads PED already set, so skipping the reset happened to work; a second device on the same port after a prior disconnect reads PED clear, and Address Device reliably failed without an explicit reset cycle. New XHCI_CONN_AWAIT_PORT_RESET state drives PR and waits for PED to read set before proceeding to Enable Slot. Bug 2 (capsule_wirebind.c): capsule_wirebind_eject()/unclean_detach() compared g_repl_active_vm against the *user* VM's pointer (g_wirebind_attached_vm_id tracks that one, not the console VM) -- never equal, since USE/g_repl_active_vm always points at the console VM. The guard never fired and the console VM was never killed at all, only orphaned -- paired to a dead user VM but still the REPL's active session. New wirebind_teardown_console() helper resolves and tears down the console VM by its own tracked bare username. Verified live on amd64: the exact repro (identity 01 on port 2, unclean detach, identity 02 on the same port immediately after) -- previously wedged with "xhci: address device failed" and no further hotplug activity; now attaches cleanly and fast, both VMs' KILL messages appear, console is immediately interactive on the new identity. Three-arch clean qemu acceptance passed, all clean on the first attempt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Ec88YKxxhZGG1RNnune78 |
||
|
|
1839a2b0c3 |
WIREBIND cert verification: load Zuse's root pubkey independently of her live session
Root cause of the remaining "identity attach doesn't complete when Zuse never attaches this boot" issue: capsule_wirebind_verify_cert() gated on mama_vm->zuse_cert_installed, which is only ever set when Zuse's own drive attaches and authenticates this specific boot (capsule_zuse_boot_try_attach() -> install_and_activate() -> vm_zuse_cert_install()). Without her, any other identity's WIREBIND cert verification silently refused -- correctly, by the old design, but that design conflated two genuinely different things: "can mint new identities" (needs Zuse's live private seed, a real privileged operation) and "can verify an existing identity's cert" (needs nothing but her already-public key). That public key was already being persisted independently of her live session: zuse_genesis_marker_t (zuse_genesis_marker.h) stores it in the kernel's own top-of-device metadata fence (Artemis's resident storage), written once at genesis, specifically *not* alongside her private seed (which stays only on her own removable thumbdrive) -- the type's own doc comment says as much. It just wasn't being loaded for anything but confirming which drive is genuinely hers. Fix: a new capsule_zuse_boot_load_root_pubkey() (capsule_zuse_boot.c) reads that marker and populates two new VM fields, zuse_root_pubkey_known / zuse_root_pubkey (vm.h) -- deliberately separate from zuse_cert_installed/zuse_cert_seed/zuse_cert_pubkey, which stay untouched and still gate MINT exactly as before. Called once from kernel_main.c as soon as Artemis's own storage attaches, unconditionally, independent of whether Zuse's own drive is ever attached this boot. capsule_wirebind_verify_cert()/capsule_wirebind_try_attach() now check zuse_root_pubkey_known instead of zuse_cert_installed. One identity's attach must not depend on another identity's live presence -- each identity stands on its own once the fleet's root of trust has been established once, ever. Verified live, amd64: identity 00 (disk/thumbdrives/00-thumb-ident.img) now attaches and completes WIREBIND in 19 seconds with Zuse's own drive never attached this boot at all (previously: unbounded, many real minutes or effectively never, before today's other fixes; still slow/ stuck after those, stuck specifically on this silent refusal). Zuse's own attach flow re-verified unaffected (regression check, amd64). Three-arch clean qemu acceptance (amd64/aarch64/riscv64) passed with this change included. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Ec88YKxxhZGG1RNnune78 |
||
|
|
1a263555e2 |
Fix pathological migration scan that stalled WIREBIND identity attach
Root cause (found by a fresh subagent after an extended live-debugging investigation into "identity 00 attaches slowly/stalls when Zuse never attached first this boot"): blk_migration_idle_check() was generalized earlier today to walk every attached device slot uniformly instead of hardcoding first_disk_slot() (Artemis's own disk). But its per-slot scan can only early-exit once it finds a devblock that is BOTH "hot" (claimed and worn) AND "free" -- and a just-attached, never-claimed USB identity drive can never satisfy the "hot" half by design (claiming only ever happens via blk_firsttouch_claim(), which only ever targets first_disk_slot()). So the scan ran to completion -- the drive's entire ~16,000 devblocks, mostly cache misses over slow emulated USB/BOT -- every single idle tick, forever, blocking sk_repl_idle() (and therefore the console and the storage-attach message round-trip) each time. Fix, in src/block_subsystem.c: a new has_ever_claimed flag on blk_dev_slot_t (set in blk_set_meta(), the single choke point every BLK_FLAG_CLAIMED transition passes through) skips the scan entirely, O(1), for any slot nothing has ever claimed -- the common case for a freshly-attached drive. A new migration_scan_lbn resume cursor bounds *any* slot's per-tick cost to MIGRATION_SCAN_BUDGET (256) devblocks examined, picking up where the previous tick left off instead of restarting from start_lbn every time -- restores this function's own documented "coarse cadence, cheap early-exit" design intent for every device, not just the one it used to hardcode. Also along the way (kept, all real improvements, verified live): - src/starkernel/usb/xhci.c: xhci_wait_bit()/xhci_bot_wait_for_idle() had zero yield hints in their MMIO-polling loops; added arch_relax() to both (matches virtio_blk.c below) -- a tight loop of nothing but MMIO reads can starve TCG's own host-side timer injection under QEMU. - src/starkernel/virtio/virtio_blk.c: vblk_io()'s spin bound was 33M iterations with zero logging on timeout; a single real (still not fully root-caused) timeout cost 31+ minutes of CPU before this was caught. Reduced to 1M and added a log line naming the failing sector, turning a silent, effectively-unbounded stall into a fast, loud failure -- callers already tolerate BLKIO_EIO. - src/starkernel/repl.c: blk_migration_idle_check() deferred for any idle tick where a storage-attach message round-trip is still pending, to keep the two block-subsystem-touching paths from interleaving; the existing MSG-TICK pump now checks the target VM's own dictionary for MSG-TICK (vm_find_word + acl_allow) before VM-EXECing into it, instead of spamming "VM-EXEC: ERROR" every tick for a VM that doesn't have -- or isn't allowed -- the word (e.g. a FORTH-79/83-locked-down identity); fixed a real -Wunused-variable build break in the EMERGENCY_CONSOLE_ ENABLED=1 path (unused today, but needed live to reproduce this bug with no identity attached at all). - src/starkernel/capsule/capsule_mint.c: dropped the dead S" common:messaging.4th" EXEC / MSG-CD-INIT lines from MINT_RESTRICTED_PERSONALITY -- a FORTH-79/83-locked-down identity has no legitimate use for a messaging vocabulary it can never call. Status: the pathological CPU-climbing scan is confirmed fixed (verified live: CPU stays flat across an extended run instead of climbing without bound). The WIREBIND storage-attach message round-trip still does not complete promptly in the "Zuse never attached, other identity attaches first" scenario -- a separate, still-open issue in the message-delivery path itself, not the scan. Tracked as follow-on work. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Ec88YKxxhZGG1RNnune78 |
||
|
|
f4ded3e1a8 |
MINT: add a FORTH-79/83-standard-words-only lockdown personality
Captain Bob, 2026-09-07: "starting with that 00 user we created, we're
going to give access only to FORTH 79 and 83 standard words. everything
else is locked down."
New capsules/acl-std79.4th (blocks 4023-4047): walks a VM's own
dictionary (>LINK/LINK> traversal, same as ACL-INIT-PRIMITIVES/WORDS
already use) and permanently denies+pins every word not on an explicit
FORTH-79/83 allowlist, extracted from the real registered word set
(stack_words.c through control_words.c), not recited from memory.
Deliberately excludes, beyond plain non-standard words: BYE (100% ACL
bypass to the emergency console -- "needs more discussion, exclude for
now"), COLD/WARM/REBOOT/SAVE-SYSTEM (system lifecycle), the block/screen
editor L/S/SHOW/EDIT/UPDATE/SAVE-BUFFERS (lets a session rewrite
persistent block/capsule content, defeating the lockdown even though
nominally standard), BLK-ACL-*/BLK-OWNER@ (StarForth-specific), and
FORGET/FENCE (flagged as an unrestricted superpower word, 2026-09-03
audit). Keeps WORDS/VLIST/SEE (introspection only -- ACL is enforced
per-target-word at execution time regardless of how an XT was
obtained) and the parenthesized control-flow runtime primitives
((BRANCH) etc. -- IF/DO/LOOP compile calls to these; denying them
breaks ordinary control flow, not security).
MintPersonality enum (capsule_mint.h) lets capsule_mint_identity()
select which personality-source template gets written to a new
identity's devblock -- MINT_PERSONALITY_DEFAULT (unchanged) or
MINT_PERSONALITY_STD79_LOCKDOWN (EXECs acl-std79.4th then
ACL-LOCKDOWN-STD79 as the VM's own last bootstrap step). The actual
restriction logic stays entirely in FORTH per .claude/CLAUDE.md's
Word-Level ACL System rules ("ACL policy belongs in ACL.4th, never in
C") -- capsule_mint.c only picks which few-line bootstrap stub to
write. MINT's own stack signature gains a trailing restrict? flag;
capsule_zuse_boot.c's genesis mint (Zuse herself) explicitly passes
MINT_PERSONALITY_DEFAULT -- the superuser is never restricted.
Two real bugs found and fixed live during testing, both the same class
of self-referential fault: ACL-LOCKDOWN-STD79's own walk loop calls
ACL-STD79-ALLOWED?/ACL-STD79-LIST/ACL-ALLOW!/ACL-PIN on every single
iteration to do its job -- none of those are FORTH-79/83 standard
words, so the walk was denying its own load-bearing infrastructure
partway through and then faulting the next time it tried to call it
("VM fault -- emergency console disabled; halting", reproduced twice
live). Fixed by explicitly protecting all four in the allowlist
(block 4047) -- they must stay allowed for the walk to finish, not
because they belong on a "standard words" list.
Verified live end-to-end: minted a throwaway test identity with the
restrict? flag, confirmed her WIREBIND birth completes cleanly (no
faults, no shadow conflicts) on a single real attach, then USE'd into
her VM and confirmed standard arithmetic and user-defined words work
(1 2 + . -> 3; : X 5 5 * . ; X -> 25) while KILL is entirely unknown to
her dictionary and VM-EXEC is denied. One real, non-fatal side effect
found and left as-is (not asked to fix): the fleet's inter-VM messaging
pump (MSG-ARENA) is also denied by the lockdown, logging a harmless
per-idle-tick warning -- a fully locked-down VM doesn't participate in
message routing.
Not yet applied to the real identity 00 -- this commit is the
mechanism, verified against a disposable test identity only.
Three-arch clean qemu acceptance (single Zuse device, standard
regression case) passed on amd64, aarch64, and riscv64.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ec88YKxxhZGG1RNnune78
|
||
|
|
d8a195b8d8 |
WIREBIND: kill the orphaned console VM when the user VM birth fails
Found live during identity-heap-capacity testing (2026-09-07, hotplugging Zuse + 8 identities one at a time and measuring the kernel heap arena via a temporary allocator-stats probe, since reverted): every WIREBIND identity attach births two VMs in sequence -- a "console" VM, then the real "user" VM. When the second birth failed (arena fragmentation under concurrent VM load, a separate, not-yet-fixed capacity issue), capsule_wirebind_try_attach() logged the failure and returned, but the console VM that had *already succeeded* was never torn down. It stays live and registered under the identity's username, consuming its own ~228KB of the fixed 4MB kernel heap arena forever -- nothing ever points a real user at it, since WIREBIND only ever hands the caller the user VM's id. This turns every failed identity attach into a permanent net loss of heap rather than a neutral retry: confirmed live that a failed attach left the arena 228,576 bytes worse off than before the attempt, and every subsequent attempt starts from that worse baseline, compounding. Fix: call capsule_vm_kill(username) on the now-orphaned console VM before returning from the failure path -- the same teardown capsule_wirebind_eject()/capsule_wirebind_unclean_detach() already use elsewhere in this file (vm_cleanup() + sf_free(), confirmed live to actually reclaim per-word dictionary allocations, FABRIC-3.md §IX.2/§IX.3). Verified live with the same allocator-stats probe (written, captured, reverted -- not part of this commit): after the fix, a forced user-VM birth failure now returns the arena to exactly its pre-attempt byte count (3,526,256, matching the baseline precisely) instead of leaking 228,576 bytes. Three-arch clean qemu acceptance (single Zuse device, the standard regression case) passed on amd64, aarch64, and riscv64. The underlying capacity/fragmentation question (why the 7th concurrent identity's arena allocation fails at all despite technically-sufficient free bytes) is a separate, open architecture question -- not addressed here. See project memory for the full measured numbers. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Ec88YKxxhZGG1RNnune78 |
||
|
|
2c1b3cd695 |
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 |
||
|
|
9e81de3f43 |
xHCI/BOT driver: genuine multi-device support (FABRIC-3.md §VII)
Per-slot registry (xhci_msc_slot_t/dev->msc_slots, sized off the controller's own reported max_slots) replaces the single-device scalar fields the driver carried since Milestones 2e-2h. Boot-time port scan no longer stops at the first connected device; a connect/disconnect that arrives while the Command Ring is busy is now queued and drained instead of dropped. blkio_usb.c and repl.c's own single-device state (device descriptor buffers, blkio_dev_t, attach bookkeeping) became per-slot registries the same way. Live multi-device testing (not just compiling) surfaced a second, more severe bug outside the original plan: transfer_purpose and next_action were also single scalars shared across the whole controller. Two devices enumerating concurrently could have one's completion silently overwrite the other's still-outstanding one, permanently stalling it with no error. Fixed by moving both per-slot and, critically, reading the Transfer Event TRB's own real Slot ID field instead of trusting external bookkeeping. Verified live, all three architectures, mandatory clean-qemu acceptance: existing single-device path unchanged, and two devices attached simultaneously (amd64) both progress independently through enumeration without corrupting or stalling each other. Also in this pass (implemented and verified in earlier turns this session, committed together per direct instruction): - Headless-until-login console policy: no prompt/banner until a real identity logs in via an attached thumbdrive (WIREBIND or Zuse, neither special), reusing EMERGENCY_CONSOLE_ENABLED as the debug/recovery escape hatch (now default-off). - KILL/g_repl_active_vm dangling-pointer fix: killing the VM the console is currently USE'd onto now detaches back to Hera first, matching the existing EJECT/UNCLEAN precedent. FABRIC-3.md §VII/§VIII carry full closure notes for all three. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018EjXFo7mPXjUMjfJeuUUz4 |
||
|
|
4d4ab59189 |
Build the FIRSTTOUCH overflow trigger flagged in FABRIC-2.md §I.2
The overflow-triggered migration path (a WIREBIND-attached identity's own drive running low on space) was scoped but never built -- only the trigger-detection call site was missing, per this section's own text. - capsule_wirebind.c now tracks the attached blkio_dev* alongside the already-tracked VM id, set in try_attach() and cleared in both EJECT/UNCLEAN paths. - New capsule_wirebind_overflow_idle_check(), called once per idle tick in repl.c right alongside blk_migration_idle_check() (same cadence): reads the attached drive's free/total via blk_get_device_free_blocks(), and if free space is below a fixed 10% threshold, extends the identity's pool with a one-time blk_firsttouch_claim() of 8 additional devblocks on Artemis's system-resident device. - New blk_owner_has_claim(owner_fp) in block_subsystem.c answers the debounce question blk_firsttouch_claim()'s own doc comment had left open: a disk scan, not a RAM flag, so the already-extended answer survives reboot/reattach, matching BMAPFMT's "ownership travels with the block" model. Premise checked before building (does a WIREBIND-attached drive actually give a real free/total signal, or does it stay PROVISIONAL/raw): traced repl.c's attach sequence and confirmed blk_subsys_attach_device() runs on the same dev pointer right after WIREBIND, and a WIREBIND-eligible drive is always already STFR/v2-formatted, so the signal is real. Premise held, unlike the BAM item's overstated one. Verified with the mandatory 3-arch QEMU acceptance (identical dictionary hashes, no regression) plus a live logic test of blk_owner_has_claim(): a temporary TEST-OWNER-CLAIM word, run once via SK_CMD and reverted, confirmed it correctly detects the claiming owner and rejects an unrelated one. The low-disk-space-triggers-a-claim path itself is not verified end-to-end -- that needs a real minted WIREBIND-user thumbdrive with deliberately tiny capacity, out of scope for this pass; noted as such in the FABRIC-2.md §I.2 closure note rather than overclaimed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018EjXFo7mPXjUMjfJeuUUz4 |
||
|
|
b031b802e3 |
Rename FABRIC series: FABRIC.md->0, FABRIC-2.md->1, FABRIC-3.md->2, FABRIC-4.md unchanged
FABRIC.md -> FABRIC-0.md FABRIC-2.md -> FABRIC-1.md FABRIC-3.md -> FABRIC-2.md (the current/living document) FABRIC-4.md unchanged (new #3 to follow separately) Every cross-reference repo-wide updated to match, including doc-comment citations inside kernel source (.c/.h) files -- done via an ordered placeholder substitution (FABRIC-3.md->placeholder2, FABRIC-2.md-> placeholder1, FABRIC.md->placeholder0, then placeholders resolved to final names) in a single pass per file to avoid double-shifting already-renamed references. One line in capsules/font.4th grew past the 64-char block-format limit as a side effect of the longer filename; shortened it and reverified with mkcapsule --lint (34/34 pass) before rebuilding. Verified 3-arch boot to ok> (amd64/aarch64/riscv64, each in the foreground) after the fix; logs and DoE CSVs from this session's verification runs included per this repo's own audit-artifact convention. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019YcT3H2PQeyujrzjqS3Var |
||
|
|
eeceec21a5 |
FABRIC-3.md §I.5: Milestone 7 trust tiers (QEMU-vs-real-hardware), closing it
Closes the contributor-capsule/trust-tier punch-list item. Decided direction: QEMU-vs-real-hardware conditional enforcement. Found before building on that decision: the obvious mechanism (expose TimerInfo.vm_mode) only works on amd64 -- aarch64 and riscv64 both had vm_mode hardcoded to 1 unconditionally, meaning they'd always report "running under QEMU" even on real hardware. Built real detection for both instead of shipping that: aarch64 checks the ACPI RSDP's OEM ID for QEMU's "BOCHS " SeaBIOS-heritage signature; riscv64 checks the devicetree root compatible property for "qemu". Confirmed vm_mode was otherwise unread anywhere else in either file first -- zero risk to existing timing behavior. CAPSULE_FLAG_CONTRIB (mkcapsule.c: FLAG_CONTRIB) path-matches on capsules/contrib/, mirroring FLAG_MAMA_INIT's exact-match pattern. contrib_capsule_refused() (capsule_birth.c) enforces: no additional check under QEMU (same WARN-only as everything else); on real hardware, a contrib capsule additionally requires CAPSULE_SIG_OK, since it has no other provenance to fall back on. Wired into capsule_birth_baby() and capsule_run_experiment(). Also updates §I.7 (Milestone 9): its stated precondition (Milestone 7 closing) is now met, flagged as stale rather than treated as a green light to design networking from nothing. Verified 3-arch boot to ok> (amd64/aarch64/riscv64, each in the foreground) -- compile/boot verification only; the real-hardware enforcement branch is unverifiable from this environment, same as all of §I.6. logs and DoE CSVs from this session's verification runs included per this repo's own audit-artifact convention. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019YcT3H2PQeyujrzjqS3Var |
||
|
|
1d468a65b1 |
FABRIC-3.md §I.1: (user) console prompt segment, closing the 4.4s->4.3->1.11 chain
Extends the REPL prompt to "[VM name] (user) ok>" (e.g. "[Hera] (zuse) ok>") per the locked FABRIC.md §4.4s spec, unblocked by this session's own §I.3/§I.8 identity-tracking work. Adds capsule_wirebind_attached_username() alongside the existing tracked VMUuid, and a new sk_print_prompt() helper (repl.c) that all three prompt call sites now go through -- checks Zuse first, then a WIREBIND user, prints nothing when neither is attached. Closes 4.4s, 4.3 (console umbrella), and formally settles 1.11 (dirty-event granularity) as region-based per FABRIC-2.md's own "no independent path" ruling -- a decision closure only, not an implementation, so §17.4 (framebuffer heat/decay physics) stays open, re-scoped precisely: blocked on the dirty-region-tracking mechanism existing, not on 1.11's decision. Documents a reported-but-unreproduced terminal defect (§I.9) as a new punch-list item -- investigated the readline/keyboard-bridge code paths, found nothing conclusive, needs a live repro with a serial log before it's actionable. Verified 3-arch boot to ok> (amd64/aarch64/riscv64, each in the foreground); logs and DoE CSVs from this session's verification runs included per this repo's own audit-artifact convention. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019YcT3H2PQeyujrzjqS3Var |
||
|
|
60d9c2520e |
FABRIC-3.md §I.3/§I.8: WIREBIND EJECT/detach + EXPIRE re-scoped as logout
Closes §I.3 (Milestone 5 remainder): WIREBIND now tracks which VM is attached via the home-blocks USB path, and a new EJECT word plus the existing hot-unplug signal both flush/reset-console/kill through it (FABRIC-3.md §F.10). Closes §I.8 (EXPIRE/ACL), re-scoped: the original "admit the zuse session as a Stadium patron and reap on TTL" plan was invalidated a second time -- Zuse authenticates directly onto Hera, who is patron zero and permanently pinned, so there is no patron for a reap sweep to ever find. Built instead as a detach-triggered logout (capsule_zuse_boot_logout()), the same trigger EJECT/hot-unplug use for regular WIREBIND users, so neither identity is a special case. Required a companion fix: install_and_activate() used to skip re-running ACL-ZUSE-BOOT whenever the cert was already installed, which made a logout permanent for the rest of the boot; the outer re-attach gate now checks zuse_session (clears on logout) instead of zuse_cert_installed (a deliberate permanent one-way ratchet, left untouched). Verified 3-arch boot to ok> (amd64/aarch64/riscv64, each in the foreground) after both steps; logs and DoE CSVs from this session's verification runs included per this repo's own audit-artifact convention. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019YcT3H2PQeyujrzjqS3Var |
||
|
|
2b9fa02354 |
§H.12 steps 21-22: ELEVATE-REQUEST + ELEVATE-GRANT + SEND-ELEVATE-REQUEST
Phase 7 complete, closing out §H.12's punch list. MSG-DELIVER turned out to VM-EXEC payload text directly rather than dispatching by type, so the "handler" is ELEVATE-GRANT, a word the delivered text calls. New Hera-only C primitives (ZUSE-ELIGIBLE?, NAME>XT, ELEVATE-PUBKEY-UNPACK) stay plain and unconditional; capsules/zuse-eligibility.4th composes the actual eligibility check + ACL-ALLOW!/ACL-TTL! grant in FORTH. SEND-ELEVATE-REQUEST (common:messaging.4th) builds the payload text and sends it via the item-20-gated CH-REQUEST. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QgooKd5hJNtTYqB6CyK5f9 |
||
|
|
7d53344875 |
§H.12 step 19: ZUSE-ELIGIBILITY-ADD word, no gating (corrected mid-step)
Plain FORTH word wrapping zuse_eligibility_add() unconditionally. Two wrong first attempts (C-level zuse_session check, then a FORTH wrapper checking it) both corrected: zuse_session isn't a special axis needing its own gate anywhere -- Zuse's authority is the absence of any ACL restricting her, not a flag any word checks. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QgooKd5hJNtTYqB6CyK5f9 |
||
|
|
f4615cf605 |
§H.12 step 18: zuse_eligibility.c -- read/add/membership-check
is_member() (fail-closed) and add() (idempotent, chains new devblocks onto the tail as needed) over the item-17 eligibility-list devblock chain, mirroring capsule_zuse_boot.c's magic/version/CRC-64 validation convention. No callers yet. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QgooKd5hJNtTYqB6CyK5f9 |
||
|
|
a268abe925 |
§H.12 steps 10-11: creator-ceiling enforcement, birth-time ACL snapshot
dictionary_snapshot_acl_from_parent(child, parent): walks the child's dictionary, copies acl_allow/acl_mode/acl_pinned/acl_ttl from the parent's matching word (by name, via vm_find_word() -- FIND's own lookup, not modified) onto the child's entry. One-time snapshot at birth, no live sync, matching H.3's decided rationale (a program developed against one ACL set must not have it silently changed by later parent changes). Called once, after dict_hash/parity logging rather than before -- the snapshot depends on the parent's current ACL state, which can vary run-to-run once Zuse elevations exist, so applying it earlier would break the "same capsule twice produces the same dict hash" determinism invariant. Verified 3-arch boot to ok> (amd64/aarch64/riscv64). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ed86a759e1 |
§H.12 steps 7-9: thread real parent VMUuid through the birth call chain
Session.parent now comes from the actual birthing VM's own stadium_vm_id, not a hardcoded vm_uuid_hera(). Added a VMUuid parent parameter to capsule_birth_baby() and, one level up, to capsule_console_birth()/capsule_runcap_birth() (neither had a VM* in their own signature, but every caller did). Updated all 6 real call sites: BIRTH, CAPSULE-BIRTH, CONNECT-ARTEMIS, CONNECT-HERMES, RUNCAP-TEST, PAIR-TEST (mama_forth_words.c) and the console+user birth pair in capsule_wirebind_try_attach() (capsule_wirebind.c). Two functions had their vm parameter marked __attribute__((unused)), now genuinely used -- attribute removed. Steps 8 (Session.name from capsule name) and 9 (identity defaults to installed=0) were already satisfied by step 5's existing session_register() call and its identity-zeroing -- confirmed by inspection, no further code needed. Verified 3-arch boot to ok> (amd64/aarch64/riscv64). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
09998af999 |
§H.12 steps 5-6: pin Hera/Hermes/Artemis in generic capsule-birth admission
capsule_birth.c's generic admission block now registers a session for every born VM (session_register) and pins it (session_set_pinned) when the birthing capsule is Hera/Hermes/Artemis. Bug found and fixed via a temporary probe (written, run, captured, reverted): the fleet-foundation name check first used an exact-match comparison against "Hermes"/"Artemis", but capsule_name is actually "hermes:init.4th"/"artemis:init.4th" (the real namespace:filename convention) -- the check silently never matched, both would-be-pinned VMs stayed unpinned. Fixed with a new vm_name_prefix_eq_nocase() helper matching everything before a literal ':'. Probe confirmed pinned=0 before the fix, pinned=1 after, on all relevant VMs. Session.parent is hardcoded to vm_uuid_hera() for now (every birth through this path is Hera-initiated today); step 7 generalizes this to the actual birthing VM's own id. Verified 3-arch boot to ok> (amd64/aarch64/riscv64) on the final, probe-free code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
09857b7228 |
G.2 (v2.0.0): unified rng_get_bytes() entropy entry point; virtio-rng sole backend
The QEMU-verifiable slice of the real-hardware RNG driver (per FABRIC-3.md §G.2). New include/starkernel/rng.h + src/starkernel/rng/rng.c provide the single entropy entry point: rng_init() probes the backend set (v2.0.0: virtio-rng only) and, on no backend, prints a loud boot-time warning while rng_get_bytes() returns RNG_ERR_NO_BACKEND - never silently degrading to a deterministic seed. The backend-selection switch in rng.c is the exact seam v2.5.0's per-arch drivers (amd64 RDRAND, riscv64 Zkr, aarch64 peripheral) plug into without touching the call path. Consumers route through the unified layer instead of virtio-rng directly: capsule_mint.c (identity seed + drive_uuid) and kernel_main.c phase 8 (rng_init()). virtio_rng.c stays as the sole backend. Built clean on amd64/aarch64/riscv64. QEMU amd64 boot: POST 1012/0/0 + ok>, "rng: backend = virtio-rng" + "entropy: ready", Zuse identity confirmed from thumbdrive - mint/cert behavior unchanged. FABRIC-3.md §G.2 v2.0.0 slice marked BUILT+VERIFIED. |
||
|
|
5689c397fc |
Bug-fix sweep: repl reentrancy, virtio/blocksys bounds, identity CRCs, LOG_LINE_MAX
Code review fixes, all compile clean (hosted gcc + aarch64/riscv64 kernel flags):
- repl.c (H1): reentrancy guards on the MSG-TICK idle pump. sk_repl_idle()
now defers when Hera is mid-interpret (g_mama_interpreting) or when its
own vm_interpret is on the stack (g_idle_pump_active), so a blocking
KEY/EXPECT/QUERY inside a dispatched line can no longer re-enter the
interpreter and clobber the in-flight input buffer.
- virtio_rng.c: clamp device-returned used_len to VRNG_BUF_SIZE before the
caller's data_buf copy, closing a device-controlled OOB read.
- block_subsystem.c: first-write path now keys off created_time==0 instead
of dead magic==0 so fresh blocks get a real created_time stamp; first_free/
last_allocated fixed to absolute Forth LBNs (set in blk_compute_fresh_geometry
from slot->start_lbn, no longer the wrong physical-BAM-index values from
compute_totals_from_B); physical-bounds guard on blk_meta_zone_read/write
prevents unsigned underflow on a corrupt fence >= device size.
- capsule_zuse_boot.c / capsule_wirebind.c: identity seed validated magic ->
version -> CRC-64 (compute_crc64 over offsetof(crc)) before trusting it,
so a corrupt/format-mismatched record is refused, never loaded.
- log.h / starkernel/log.h: unused LOG_LINE_MAX 256 renamed LOG_MSG_LINE_MAX
to lift the include-order collision with vm.h's LOG_LINE_MAX 64; stale
include-order comments dropped (kernel_main.c, shim.c, capsule_birth.c).
- FABRIC-3.md: three stale-doc carry-forward items closed [x] with
|
||
|
|
d00e6cb50d |
MINT: default personality now loads common:messaging.4th (fixes MSG-TICK error-spam)
capsule_mint_identity()'s MINT_DEFAULT_PERSONALITY only ever defined WELCOME, unlike capsule_console.c's own CONSOLE_IDENTITY_SRC. The moment WIREBIND made a freshly-minted identity's VM live, Hera's per-idle-tick MSG-TICK pump (repl.c Phase C) began erroring on it every tick forever -- UNKNOWN WORD: 'MSG-TICK'. Every identity minted before this fix would hit the same infinite error-spam on going live, not just this one. Fixed by loading common:messaging.4th + calling MSG-CD-INIT before WELCOME, mirroring capsule_console.c's own pattern exactly. Found and verified minting the first real second identity end to end: disk/captain-bob.img, hot-attached via QMP alongside an already- authenticated Zuse session, MINTed, WIREBIND-attached, switched into live with USE, WELCOME confirmed. Zero MSG-TICK errors over 18+ seconds of idle-loop cycling after the fix (was immediate and continuous before). Full three-architecture regression clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KBjfeLPo71sUQ8zC7V7P5m |
||
|
|
849b83b727 |
Zuse default-attach: xHCI initial-port-scan fix, ZUSEDISK wiring, mismatched-marker resync
xhci_bringup() now scans for already-connected ports at bring-up (xhci_scan_ports_for_already_connected()), not just later hotplug events, so a USB device present on the QEMU command line at launch is detected. Makefile.starkernel attaches disk/zuse.img on the xhci0 bus by default in all three arch qemu targets (ZUSEDISK=, empties for a bare boot). capsule_mint_identity() gained a drive_known_blank param to skip a fully redundant second homeblocks_sig_check() when the caller already confirmed HOMEBLOCKS_SIG_BLANK itself. Root-caused what looked like a hang after the drive attached: Artemis's fence still carried a genesis marker from before a mid-session reformat, while the reformatted disk/zuse.img read back BLANK -- a mismatched pair capsule_zuse_boot_try_attach() correctly declined to act on, leaving the boot idling at a plain ok> with nothing left to log (indistinguishable from a hang under slow TCG). Fixed by zeroing both disk/artemis.img and disk/zuse.img at their original sizes, giving a matched blank pair. Verified full three-architecture acceptance: amd64 fresh genesis-mint, aarch64/riscv64 clean reload against the same now-minted images. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KBjfeLPo71sUQ8zC7V7P5m |
||
|
|
09d78c99d0 |
BINDSTEP + fence-persistence fix: identity arc closed end to end
Two items, closed together per direct instruction.
1. Fence-persistence root cause, found and fixed: meta_fence_blocks
(the field gating whether blk_meta_zone_write() can succeed at all)
was carved out of what used to be unused padding in blk_volume_meta_t
-- the code's own comment already documented this. disk/artemis.img
was formatted before that field existed, so its on-disk bytes there
have always read back as 0, and the existing-volume load path
(blk_format_or_load_disk()) never recomputes it -- only a fresh
format does. Every "fence write FAILED" message this entire session,
old block-fence flow and new zuse_genesis_marker_t alike, traces to
this one thing. Patching the field in place without redoing the rest
of the geometry would risk corrupting whatever's already allocated
near the top of the volume, so the only safe fix is a genuine
reformat -- done, with explicit confirmation, since it discards
disk/artemis.img's accumulated persistent test state (regenerated
fresh at next boot regardless, not real data). Verified: fence write
now succeeds with no failure suffix, and the full mint-once ->
reboot -> reattach -> re-authenticate cycle works for the first time
this session ("Zuse: identity confirmed from attached thumbdrive",
ZUSE-SESSION? goes 0 -> -1 without re-minting).
2. BINDSTEP (FABRIC-3.md §F.9): capsule_wirebind_verify_cert() extracted
as a shared function so WIREBIND (the original attach) and BINDSTEP
(every USE of an identity-locked VM) check the exact same thing the
exact same way. mama_word_use() now re-verifies live, not cached,
whenever the target VM has VMIdentity.installed=1 -- reads whatever
drive is CURRENTLY attached, re-verifies its cert, compares owner
pubkey against the target's own installed identity, refuses on any
mismatch or no drive attached. A target with installed=0 (Hera,
Hermes, Artemis, any console VM) stays freely targetable, unchanged.
Two related bugs found and fixed live while testing BINDSTEP, not
assumed away: USE was Mama-only, so a console-paired session (§F.22)
had no way back to Hera at all -- any attempt to call USE from inside
a console VM hit "UNKNOWN WORD: USE", a genuine dead end. Per direct
instruction, USE isn't console-specific -- it should work VM-to-VM
universally, same as VM-EXEC already does -- so it's now registered in
register_child_vm_words() too. That alone wasn't enough: the console
relay (sk_repl_dispatch_line()) would have captured a bare USE call and
sent it to the paired user VM as a message instead of running it.
Fixed with a small suffix-match guard (sk_repl_line_calls_use()) --
real FORTH syntax always puts USE last, so a trailing-token check
reliably recognizes it without needing a full tokenizer, and it always
runs directly, never relayed.
Verified live end to end: USE on an unlocked VM works unconditionally;
USE escaping a console back to Hera now works; USE on an identity-
locked VM succeeds while its own drive is attached and is refused
once detached ("USE: FinT~user refused -- no matching identity
currently attached"). Clean 3-architecture regression, including
confirming disk/artemis.img's reformatted geometry loads correctly as
an already-recognized volume ("Artemis: LithosAnanke disk -- resuming")
on aarch64 and riscv64 too, not just the amd64 boot it was reformatted
under.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZGkimpfyh63EZyRkNbkPD
|
||
|
|
6fc0ee33a9 |
WIREBIND: real thumbdrive-attach call site, no manual steps
Assembles pieces already built and individually verified this session -- CERTVERIFY (vm_identity_from_cert(), Phase A/B), RUNCAP, the console-VM + user-VM pair (§F.22) -- into one automatic sequence, replacing the RUNCAP-TEST/PAIR-TEST diagnostic words that exercised each piece by hand. New capsule_wirebind_try_attach() (capsule_wirebind.h/.c), called from sk_repl_idle() alongside capsule_zuse_boot_try_attach() on every HOMEBLOCKS_SIG_OK attach: sig->cert_offset==0 means this is Zuse's own genesis-mode drive (no cert region) -- that's already capsule_zuse_boot_try_attach()'s job, skip. Otherwise, with Zuse already authenticated this boot (nothing to verify a regular cert against otherwise), reads the cert devblock(s) and calls vm_identity_from_cert() against mama_vm's own zuse_cert_pubkey and the drive's own drive_uuid. On success: reads the drive's own user_identity_seed_t for its username, births a console VM + RUNCAP-born user VM pair (idempotent -- no-ops if that username is already live this session), installs the verified VMIdentity onto the user VM, and registers the "<username>~user" pairing sk_repl_dispatch_line() (repl.c, §F.22) looks for. Deliberately does NOT auto-USE the new console -- that stays an explicit, ACL-gated step (BINDSTEP, §F.9), not something a bare attach should trigger silently. Verified end-to-end live in QEMU, including a genuine negative case: attached disk/user1.img (signed by a different, earlier-session Zuse instance) and got a correct "cert verification FAILED -- drive refused" -- proof the check is real, not a rubber stamp. Minted a fresh identity with this boot's own Zuse, reattached, and got "WIREBIND: SamS attached and ready" printed with zero manual commands, followed by a working USE + async WELCOME relay end to end (queued, no UNKNOWN WORD, delivered and executed in the paired user VM on the next idle tick). Clean 3-architecture regression: Hermes/Artemis both birth live, no unexpected ACL denials or UNKNOWN WORD. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ZGkimpfyh63EZyRkNbkPD |
||
|
|
b0f12710bb |
Console-VM + user-VM pair: real async message-passing relay
Console sessions now route through the same general VM-to-VM messaging system (Phase C) any VM can already use for its own reasons -- not a synchronous shortcut. Per direct instruction: real async MSG-SEND/ MSG-DELIVER (Option B), not a VM-EXEC-based synchronous relay, because messaging is a general capability, not a console-specific mechanism. New CONSOLE-CMD-EVENT message type (common:messaging.4th). New sk_repl_dispatch_line() (repl.c), called from both sk_repl_step and sk_repl_run in place of a direct vm_interpret(): if the active VM's own name has a live "<name>~user" counterpart registered, the raw input line is wrapped as an S"-embedded CONSOLE-CMD-EVENT MSG-SEND and interpreted on the console VM instead of being run directly -- the console's own next MSG-TICK (Hera's idle pump) delivers it into the paired user VM via VM-EXEC, same mechanism every other message already uses. Falls back to direct interpretation if there's no pairing, or if the line contains a `"` (known v1 limitation, warned about explicitly rather than silently mishandled). New capsule_console_birth() (capsule_console.h/.c): a bare VM whose only content is loading common:messaging.4th -- the console side of a pairing, parallel in shape to RUNCAP's user-VM birth but with fixed embedded content instead of a devblock read (no identity, no thumbdrive involved). New PAIR-TEST diagnostic word (mama_forth_words.c, matches RUNCAP-TEST's own precedent): births both halves of a pairing and registers the "<name>~user" mapping. Not the real pairing call site -- that's the eventual attach/onboarding flow -- this exists to exercise the relay live before that flow exists. Found and fixed a real, serious bug live: console_set_vm_name() stored the caller's raw pointer instead of copying it. mama_word_use() (USE) passes a VMRegistryEntry field living on its own stack frame -- once USE returns, that pointer dangles, corrupting every console tag after the first USE (observed directly as garbled "[[]" / binary-looking prefixes instead of "[CaptBob]"). Fixed at the source: console_set_ vm_name() now copies into internal storage. That surfaced a second, related bug across every console_get_vm_name()-based save/restore call site in mama_forth_words.c (BIRTH, VM-STEP, VM-EXEC, CONNECT-HERMES, CONNECT-ARTEMIS): saving just a pointer into the single internal buffer meant an intervening console_set_vm_name() call silently corrupted the saved value before the restore ever ran. New console_save_vm_name() copies into caller-owned storage; every save/restore site updated. Verified end-to-end, live in QEMU: typed WELCOME at a paired console VM -- it did not execute directly (no UNKNOWN WORD), printed ok immediately (queued, async), and on the next idle tick "[CaptBob~user] Minted identity -- default personality" appeared on its own -- genuine delivery and execution in the paired user VM through the real MSG-SEND/MSG-DELIVER pipeline. Console tags confirmed clean (no garbling) across all three architectures' full regression boot. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ZGkimpfyh63EZyRkNbkPD |
||
|
|
cc9521d2cc |
Retire emergency CLI: Zuse goes thumbdrive-resident, ACL.4th activated
Three tightly-coupled changes, verified together per Captain Bob's own "getting rid of the emergency cli" direction: 1. Zuse's identity is thumbdrive-resident, never system-resident. New zuse_genesis_marker_t (magic/version/zuse_pubkey[32]/crc) replaces zuse_cert_devblock_t's slot in the top-of-device fence -- the system now remembers only that a root identity exists and its pubkey, never a seed. zuse_cert_devblock_t is kept in the repo, marked superseded, no longer written by any code path. capsule_mint_identity() grows a genesis mode (issuer_vm=NULL): no cert is built or written (Zuse isn't verified against a separate signer -- she's recognized by pubkey match against the marker) and two new optional out-params (out_pubkey/out_seed) let the caller install the cert immediately after a genesis mint. New capsule_zuse_boot_try_attach() (capsule_zuse_boot.c), called from sk_repl_idle() on every fresh USB attach (the only point in the boot lifecycle a thumbdrive can actually be detected -- attach polling doesn't exist yet at kernel_main.c's old one-shot mint point, which is why that whole block is gone): no marker + blank drive -> genesis-mint; marker present + matching drive -> read its own user_identity_seed_t, install the cert. Either way, re-runs ACL-ZUSE-BOOT (zuse.4th) so zuse_session activates exactly like it always has for a same-boot cert install -- ACL-PIN only blocks redefinition, not re-execution, so no new C-side auth logic needed. 2. ACL.4th activated (capsules/init.4th) -- inactive all session until now. Found and fixed a real bug this immediately surfaced: zuse.4th's ACL-ZUSE-BOOT tried `['] ACL-ZUSE-BOOT ACL-PIN` from inside its own still-compiling definition -- the word isn't findable yet at that point, so the whole definition silently failed to compile every previous boot this session (dormant, since ACL.4th never loaded). Fixed: pin after the definition closes, not from within it -- it only needs to happen once anyway, and pinning doesn't block the re-invocation genesis/attach needs. 3. The unauthenticated emergency-CLI ACL bypass is retired (repl.c): `emergency_console = is_hera ? (zuse_session ? 0 : 1) : 0` deleted from both sk_repl_step and sk_repl_run. Every word run from Hera's own bare prompt now goes through ordinary ACL enforcement; emergency_console is driven only by the genuine C-level fault handler again. Added ZUSE-SESSION? (starforth_words.c), a read-only diagnostic matching ZUSE-PUBKEY@'s own precedent, to verify the whole chain directly rather than by inference. Verified end-to-end live in QEMU: fresh boot, no thumbdrive -> ZUSE-SESSION? reads 0. Attach a genuinely blank drive via QMP -> genesis mint fires automatically (no typing) -> ZUSE-SESSION? reads -1 (true). Hermes/Artemis both birth clean on all three architectures with ACL now actually enforced for the first time all session -- no denials, no UNKNOWN WORD beyond the deliberate POST self-test cases. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ZGkimpfyh63EZyRkNbkPD |
||
|
|
6fd87923a5 |
MINT: parameterize with full name, username, email, phone
Extends user_identity_seed_t (version 2) with fixed-size full_name/ username/email/phone fields -- plenty of unused pad space (4016 bytes) was already there. Deliberately NOT encoded into the DER cert's Subject field: that would mean building a real X.509 RDNSequence (OIDs for commonName/emailAddress, PrintableString/UTF8String tagging), well past this project's own stated "deliberately not a general ASN.1/X.509 [builder]" scope. This human-readable profile data isn't security-relevant the way pubkey/serial are (the only two fields CERTVERIFY/BINDSTEP actually check) -- it travels alongside the keypair in the plain identity record instead. capsule_mint_identity() takes full_name/username (required, validated non-empty and within their fixed field widths) and email/phone (NULL or empty = null, matching the schema's own nullable convention). The MINT FORTH word's stack signature grows to 4 string pairs ( fname-c fname-u uname-c uname-u email-c email-u phone-c phone-u -- ok? ). Verified live in QEMU: minted two real identities with real profile data -- Zuse (full_name "Konrad Suse", username "Zuse", zuse@pantheon.org) onto disk/zuse.img, and a regular user (full_name "Captain Bob", username "CaptBob", capt.bob@pantheon.org) onto disk/user1.img -- then read the raw devblock bytes back off both images directly and confirmed every field byte-exact at its correct struct offset. Clean 3-architecture regression boot confirms no side effects. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ZGkimpfyh63EZyRkNbkPD |
||
|
|
f6e2737f1e |
Phase E: MINT -- real keypair, Zuse-signed DER cert, working default identity
capsule_mint_identity() (new capsule_mint.h/.c): mints a fresh identity onto a blank/unminted thumbdrive -- real Ed25519 keypair from virtio_rng, a fresh drive_uuid (independent random draw, not derived from the identity seed, per FABRIC-3.md §F.8 decision 3), a Zuse-signed DER cert in the CERTVERIFY format, and a small working default personality (a real WELCOME word, not a stub -- FABRIC-3.md §F.6/§F.8's own "default personality content" question stays open, but whatever mints today must actually do something when RUNCAP births it). Refuses to overwrite a drive that already reads as a recognized home-blocks drive, mirroring WRITE(10)'s own refuse-on-non-blank posture (decided now, not just "reasonable by analogy" as §F.8 left it). x509_build_user_cert() (x509_ed25519.h/.c): the encode-side counterpart to the existing decode functions (x509_extract_ed25519_pubkey(), x509_verify_signature(), x509_extract_serial()) -- a minimal DER TLV writer producing exactly the fields those functions read. Host-tested round-trip against the real decoder before trusting it in the kernel, including a high-bit-serial case that exercises the DER integer-padding rule; all assertions pass (pubkey/serial round-trip, signature verifies against the real issuer, correctly rejects the wrong key and a corrupted signature). New user_identity_seed_t (user_identity_seed.h): the on-disk record for a minted identity's own keypair, same magic+version+fields+pad-to-4096+ real-CRC convention as zuse_cert_devblock_t and homeblocks_sig_t. Fixed devblock layout: sig(1), cert(2), seed record(3), default personality(4). New MINT word (mama_forth_words.c) and a small accessor (sk_repl_get_attached_blk_dev(), repl.h/.c) exposing the currently attached USB device regardless of home-blocks recognition -- MINT's own target is a blank drive, which by definition never sets Phase D's sk_repl_get_homeblocks_dev(). Verified end-to-end live in QEMU: MINT on a genuinely blank test drive, then (after a detach/reattach so the sig cache picks up the fresh header -- a known workflow gap, not fixed here, flagged for whoever builds the real Console onboarding flow) RUNCAP birthed a VM from that drive's own newly-minted content, and VM-EXECing its WELCOME word printed the default personality banner. The full mint-to-birth Tripod identity flow works end to end for the first time. Clean 3-architecture regression boot confirms no side effects on normal boot. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ZGkimpfyh63EZyRkNbkPD |
||
|
|
e1e839258d |
Phase D: RUNCAP -- runtime capsule construction from thumbdrive content
capsule_runcap_birth() (new capsule_runcap.h/.c): builds a heap-only, single-entry CapsuleDirHeader + CapsuleDesc + CapsuleNameEntry + arena from a home-blocks drive's identity_src region (skipping the first devblock, reserved for MINT's user_identity_seed_t record) and hands it to the existing, unmodified capsule_birth_baby() -- no new birth mechanism, matching FABRIC-3.md §F.6's own trace. Found and closed a real gap in that trace along the way: capsule_birth_baby()'s signature check calls capsule_get_signatures(), which unconditionally returns the compile-time-baked global array -- meaningless for a heap-built directory, where index 0 would compare RUNCAP's own content against whatever real capsule happens to occupy that slot in the baked array (guaranteed-wrong, not a security check). Added an explicit skip_pki_sig flag (0 for all 4 existing call sites, 1 for RUNCAP): that content's trust comes from CERTVERIFY, a separate root, not the capsule-PKI chain. Also found live: capsule_birth_baby() never sets the registry entry's own .name (every existing caller does this itself afterward via capsule_vm_registry_set_name() -- RUNCAP now does too), and capsule_exec_payload() requires a "Block NNNN" header per chunk of content or it's silently skipped, never executed -- not a bug, but necessary context for whoever authors MINT's default personality content next. Added a small accessor pair (repl.h/.c) exposing the currently attached home-blocks device/sig -- the same gap F.9's own BINDSTEP scoping had already flagged, needed by both. Verified end-to-end live in QEMU: synthetic identity-source content written directly to a thumbdrive image's raw devblocks (no capsule build, no mkcapsule) was read, compiled, and executed by a genuinely new VM via a diagnostic RUNCAP-TEST word -- confirmed via VM-EXEC invoking a word defined only in that source. Clean 3-architecture regression boot (no RUNCAP drive attached) confirms no side effects. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ZGkimpfyh63EZyRkNbkPD |
||
|
|
21bca315ff |
Phase C: distributed messaging capsule + idle-loop pump
Extract the messaging vocabulary (arenas, MSG-*/CH-*/MBR-* words) out of capsules/hermes/init.4th into a new shared capsules/common/messaging.4th that Hermes and Artemis each load at birth, giving every VM its own private MSG-ARENA/CH-ARENA instead of only Hermes having one. Hermes stays the owner of the one real, canonical COMMON-CH; Artemis subscribes into it via VM-EXEC at her own birth, and Hermes proactively subscribes Hera (idx 0) since Hera always exists first. Hera does NOT get her own copy: register_child_vm_words()'s own doc comment explains why the STADIUM-* primitives common:messaging.4th depends on are deliberately never registered in her dictionary (keeps her dict_hash off item 4.1's baseline). Confirmed live by loading it into her dictionary anyway first -- every colon-definition referencing an unregistered Stadium primitive was silently dropped (MSG-HEAT@/!, CH-HEAT@/!, MSG-COOL-ALL, MSG-TICK all missing after boot). Reverted that path; she orchestrates via BIRTH/VM-EXEC/VM-CALL instead. Added capsule_vm_registry_get_by_index() (capsule_birth.c/.h) for registry enumeration by birth-order position, and a pump in repl.c's existing idle hook that walks every live VM once per idle beat and VM-EXECs MSG-TICK into each one except Hera's own entry. Verified clean (no UNKNOWN WORD / VM-EXEC errors after birth) on all three architectures. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ZGkimpfyh63EZyRkNbkPD |
||
|
|
0ec91b517a |
Implement CERTVERIFY -- real DER cert verification, tested against OpenSSL
Phase B of the identity pipeline (FABRIC-3.md §F.7/§F.17): - x509_ed25519.c/.h: two new DER walkers alongside the existing pubkey extractor -- x509_verify_signature() (verifies a cert's outer Ed25519 signature over the raw, exactly-as-encoded tbsCertificate bytes, real signature verification against issuer_pubkey, rejects non-Ed25519 signatureAlgorithm) and x509_extract_serial() (extracts the serialNumber INTEGER, stripping a DER padding byte if present, for the drive_uuid binding decided in §F.7). - vm_identity.c: vm_identity_from_cert(), ties the three DER primitives together into the actual CERTVERIFY check -- signature verifies against issuer_pubkey, serialNumber matches this drive's own drive_uuid, subject pubkey extracts cleanly -- and populates a VMIdentity on success. acl_caps is caller-supplied, not read from the cert (nothing in the decided cert fields encodes capabilities); deciding what a verified identity is allowed to do is policy for the caller (WIREBIND, not yet built), not this function's job. Verified two ways: a standalone host-side test harness (not part of the kernel build) links the real source files against a real openssl- generated Ed25519 X.509 cert -- extracted pubkey, extracted serial, and signature verification all match ground truth, plus two negative tests (wrong issuer pubkey, corrupted signature) both correctly rejected. Then the actual kernel build verified live on all three architectures: clean compile, clean boot to ok>, Hermes/Artemis both live with no KILL. Same pre-existing, unrelated Zuse fence-write anomaly observed on all three (not caused by this change, not chased here). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ZGkimpfyh63EZyRkNbkPD |
||
|
|
c07184e984 |
Add VMIdentity type, relocate homeblocks_sig_t off devblock 0
Phase A of the identity pipeline (FABRIC-3.md §F.2/§F.16, §F.13):
- New include/starkernel/vm_identity.h: VMIdentity{owner_pubkey[32],
installed, acl_caps}, its own header per the project's "give real-shaped
data its own header" convention (VMUuid's own precedent), embedded by
value on struct VM. acl_caps is an independent capability bitmask, not
an ordered privilege tier (decided 2026-08-28) -- Zuse isn't a
structurally special VM, her identity just has every bit set
(VM_IDENTITY_CAP_ALL). No individual capability bits assigned yet,
deliberate slack matching blk_meta_t's own acl_reserved precedent --
real bits get names once the operation they gate (BINDSTEP, MINT, ...)
actually gets built.
- Applied the devblock-1 relocation decided in §F.13: new
HOMEBLOCKS_SIG_START_FBLOCK constant (homeblocks_sig.h), repl.c's
homeblocks_sig_check() call site updated from the literal 0. Also
applied the still-owed blockmap_offset/blockmap_devblocks ->
identity_src_offset/identity_src_devblocks rename decided in §F.6 (no
other code referenced the old names). Updated the header's own stale
GPT-relative doc comments to match -- GPT was dropped permanently, this
location is final, not an interim stand-in.
Verified live on all three architectures: clean compile, clean boot to
ok>, Hermes/Artemis both confirmed live with no KILL (no regression from
last session's session-less fix). One pre-existing, unrelated anomaly
observed consistently on all three arches, not caused by this change (no
code touched here intersects Zuse's own fence-write path): "Zuse: minted
but fence write FAILED (not persistent)" -- flagged for its own
investigation, not chased here.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZGkimpfyh63EZyRkNbkPD
|
||
|
|
030a3e6dc0 |
Milestone 6: hard-refuse flip, scoped to INVALID only -- closes Milestone 6
Before flipping, found CAPSULE_SIG_MISSING (no signature at all) is the normal state everywhere except this machine -- CI and any other checkout have no access to the offline key, by design. Refusing on MISSING the same as INVALID would brick boot everywhere but here. Decided (on request): enforce ONLY on CAPSULE_SIG_INVALID (a signature that IS present but doesn't verify -- unambiguous tampering/corruption evidence). MISSING/NO_ROOT_KEY stay WARN-only permanently. All three capsule_birth.c call sites now return CAPSULE_RUN_ERR_INVALID on CAPSULE_SIG_INVALID, after logging the same WARN as before. Verified on all three architectures, both directions, per the original rollout commitment: positive case (real signed capsules) reboots clean with zero warnings on amd64/aarch64/riscv64. Negative case (same one-byte signature corruption used for the WARN-only proof, on Mama's own init.4th) now genuinely refuses identically on all three: "capsule sig: init.4th: INVALID" then "Init: Mama birth FAILED". The feared "no ok> at all" blast radius didn't materialize -- kernel_main.c already had graceful error handling for a failed Mama birth (log and continue, pre-existing code); the kernel reaches a degraded ok> rather than crashing, on all three architectures. Final acceptance pass (real signed capsules, tampering reverted) clean on all three. Milestone 6 is now fully closed except magic-number content-type detection (shared with Milestone 4, separate scope, not started). Documented in FABRIC-3.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U14ET9CWAtbQMbYqomKgXd |
||
|
|
2fc55f47e1 |
Milestone 6: mkcapsule signing + capsule_birth.c wiring, WARN-only
First attempt shelled out to `openssl pkeyutl -sign` (fork/execlp, not system() -- avoided shell string interpolation of the key path). Corrected on request: no new external host binary dependency when the repo's own code can do the job -- same standing preference as the earlier anti-file correction. Rewritten to link ed25519_sign() (already verified against OpenSSL in Phase B) directly into mkcapsule. New tools/pkcs8_ed25519.c: a narrow DER walker (same shape as x509_ed25519.c, deliberately not shared -- small enough that duplicating a few TLV-walking lines beat threading a header between the kernel crypto tree and host tooling) extracting the raw seed from the intermediate's PKCS#8 private key, plus a minimal self-written base64 decoder (PEM is openssl genpkey's default output; no decoder existed anywhere in the repo). Verified end-to-end before wiring anything in: the extracted seed's derived pubkey matches the cert's exactly, and a full self-contained sign+verify round-trip (zero openssl) passes. CapsuleDesc had no spare bytes, so signatures live in a new parallel CapsuleSigEntry array, emitted by a new `mkcapsule --sign-key <path>` flag (omitted/missing key -> has_sig=0 everywhere, graceful, not a build failure -- CI has no access to the offline key). New capsule_sig.c/.h: capsule_verify_signature(), a separate function, not folded into the already-tested capsule_validate(). Finds and caches the embedded intermediate cert's pubkey once per boot, then verifies against it. Wired into all three capsule_validate() call sites in capsule_birth.c via log_message(LOG_WARN, ...) -- never refuses yet, per the earlier staged-rollout decision. Verified independently, both directions, live in the real kernel: a full clean build (38 signed capsules) boots clean on all three architectures with zero warnings. Separately, hand-corrupted one byte of Mama's own init.4th capsule's stored signature (not its payload/hash, which capsule_validate() already catches and would have masked the test) and rebuilt just the changed object: produced exactly "capsule sig: init.4th: INVALID -- signature does not verify" on boot, and the kernel still reached ok> -- proving warn-only doesn't refuse anything yet. Reverted before the final, untampered 3-arch acceptance pass. Still open: flipping WARN to hard-refuse (separate, deliberate step) and the BLOCK_MAP.md signature-status column. Documented in FABRIC-3.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U14ET9CWAtbQMbYqomKgXd |
||
|
|
3e0966095c |
Stadium: real VM-patron admission + real COOL dispatch at KILL (FABRIC-3.md §B)
capsule_vm_kill() had zero Stadium involvement (vm_cleanup()/sf_free() only), and child-VM birth only ever called stadium_grant_quota() -- a resource pool for the VM's own future word/block patrons, never stadium_admit() for the VM itself. stadium_birth_hera() looked like a precedent but admits Hera into her own quota as a permanently pinned cell 0, which can never reach stadium_evict() -- not a working example of COOL firing for a VM. Adds size_t stadium_patron_cell to VMRegistryEntry. At birth, right after the existing stadium_grant_quota() call, admits a candidate into the new VM's own quota mirroring stadium_birth_hera()'s shape (identity=0, mass=1, behaviour=COOL) but deliberately unpinned -- pinning would need a new "unpin" primitive (none exists) to ever evict it later, and unpinned costs nothing since nothing wires COOL's dispatch body to kill anything; the worst case of an unrelated natural eviction is stale bookkeeping, tolerated the same way stadium_word_forget() already tolerates staleness elsewhere. At capsule_vm_kill() and capsule_vm_kill_all_nonmama(): stadium_evict() the tracked cell if still resident, silently tolerating refusal (already gone). stadium_dispatch()'s COOL case needed no new payload body -- same as it already is for words, where COOL has no defined extra action beyond stadium_evict()'s own universal reservoir credit. On investigation this turned out not to be entangled with the still-iterating Tripod/Zuse/messaging vision after all -- birth and kill already funnel through two single choke points, so the earlier deferral (previous commit) was overcautious. Verified live: a second, new "Stadium: dispatch cell=... behaviour= COOL" now fires immediately before every PARITY:KILL line, for both Hermes and Artemis, distinct from the pre-existing COMMON-CH word-eviction self-test's own COOL print. Conservation (resident_sum + reservoir == Q48_ONE) intact throughout. Clean zero-warning compile and clean boot on all three architectures. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn |