The decision's observer pass runs only on core 0; a switch into an
observer on any other core is refused and counted (a guard, 0 in every
run). Probe posing as core 1: no observer chosen, switch refused 0 -> 1;
as core 0 the same decision returns Hestia. 3-ISA acceptance clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hal/smp.c: dense logical core ids, a heap-sized core table, one-at-a-time
bring-up under a 1 s bound, park-wake reporting at the idle point.
amd64: MADT enumeration, a relocation-free real->long-mode trampoline
copied below 1 MiB, INIT-SIPI-SIPI via the LAPIC ICR; QEMU -smp 2.
Core 1 reports online with GS on its own block, parks with 0 wakes;
aarch64/riscv64 report 1 core until MC-1b/c. 3-ISA acceptance clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- New spinlock primitive (spinlock.h/.c): CAS on an owner word (core+1),
interrupts masked while any lock is held (state kept in the per-core
block, so release order doesn't matter). Recursion, release-not-held,
held-at-switch and destroyed-while-held are counted and logged, never
silent. Locks are no-ops until the boot core's block is installed
(riscv64's monolithic early boot prints before sk_percpu_boot_init()).
- Kernel locks in a documented order: hermes, switch, registry, doe, log,
console, heap, vmm, pmm. sf_mutex (dict_lock/tuning_lock) uses the same
lock word.
- Hermes: public functions over *_locked internals; never held across the
drain's vm_interpret_kind(); switch notifications after dropping the
lock; event payload moved off static storage. Switch table and registry
never call out while held (slot/entry copied under the lock).
- No lock crosses a switch: checked at context switch (leave, resume) and
the idle point.
- Probe (removed): every lock balances on all three ISAs; 0 held at
switch over 7.5M-51M checks; 0 recursions, 0 bad releases.
- Standard sequence on amd64/aarch64/riscv64: hashes and G byte-identical
to MC-0a, MINT/WIREBIND/USE ok, conservation true, 0 idle growth.
- Block subsystem split out as MC-0b2 (Captain Bob's ruling).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- New sk_percpu_t { self, core_id, current_vm }, reached via GS base
(amd64, %gs:0), TPIDR_EL1 (aarch64), tp (riscv64). Boot core's block is
one static object installed right after arch_early_init(); secondary
cores' blocks come with MC-1.
- switch.c's global g_switch_current_vm is gone; current VM is
sk_percpu()->current_vm, re-read after resume in the context switch.
- Probe (removed): normal path and every heartbeat ISR found core 0's
block on all three ISAs (ISR ok 889/1877/1625, bad 0).
- Standard sequence on amd64/aarch64/riscv64: hashes unchanged from O-C,
G A=40 B=40, MINT/WIREBIND/USE ok, conservation true, 0 idle growth.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- sk_vm_idle_point(may_drain_self): with no frame live and mail pending,
Hera drains one message and returns to the poll loop. Readline passes
n == 0 (the mid-edit gate), headless 1, the key read 0.
- Removed sk_repl_idle()'s timer-paced drain of Hera's queue and the two
flags that guarded it (g_mama_interpreting, g_idle_pump_active) -- the
idle point's frames check already covers that re-entry hazard.
Probe (reverted), all three ISAs: admission -> drain for mail to an idle
Hera 0-1 beats (was up to 100); a message arriving mid-edit is held until
the line is entered (2080-3587 beats), never printed into typed text; an
event queued during a busy line drains when it ends; BLK-ATTACH at boot 1
beat. Acceptance passes; parity, dict_hash set and relay ledger unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Permanent event channels 1/2/3 created at boot beside channel 0, exposed
as FORTH constants EV-DONE / EV-BORN / EV-DIED; CHANNEL-CLOSE refuses 0..3.
- sk_hermes_event(): `S" <name>" ON-...` as type 32 to every member except
the VM it's about; never blocks -- undeliverable copies are dropped,
counted (EV-DROPPED), printed and logged.
- Fired for participants only: EV-DONE from the switch trampoline when real
work runs dry (funded by the VM), EV-BORN at a LIVE VM's first naming
(funded by the newborn), EV-DIED on KILL/reap after departure (funded by
the root). VM.ev_real_work keeps event handling from firing EV-DONE, so
mutual subscribers can't storm.
Console evidence identical on amd64/aarch64/riscv64: one BORN per birth,
one DONE per job run dry (2 queued -> 1), one DIED per KILL, no SEND in any
job text; storm guard: exactly one reaction; channels 1-3 unclosable;
non-joined VMs see nothing; EV-DROPPED 0. Flood probe (reverted): 32
delivered, 7 dropped and counted, never blocked, all heat returned.
dict_hash set changes by the 4 words (546), identical across ISAs;
acceptance passes with held=2048.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each shown dead three ways (FABRIC-3.5 §XXII): the pump loop by its
condition, by construction (birth cap == switch table capacity, every birth
registers) and at run time (probe silent through boot, G interleave, an
8-worker DoE trial and the relay); SWITCH-MARK-WORK by callers, capsules
(source and baked), experiments/tools/logs; the depth counter and back_to
field by having no readers.
Kept: Hera's own pump drain (last timer-paced delivery, Hera only) and the
always-0 readiness CSV columns (a DoE schema call). Acceptance on amd64/
aarch64/riscv64: G interleave unchanged, cfg-0 trial fail=0 2/2 dead,
relay 3, idle growth 0; dict_hash set changed by the removed word
(word_count 531) and identical across ISAs. FABRIC-3.8 stages A-I done.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sk_vm_parked_idle(): saved context + exactly its own IDLE frame.
capsule_vm_kill_refusal() is the single kill rule, shared by
capsule_vm_kill() and the KILL word: refuse SWITCHED_OUT mid-task, parked
with mail pending, or the VM running; allow parked-idle. capsule_vm_kill()
now unregisters the switch slot (G6).
Finding fixed: vm_registry_alloc() never initialized pending_reap (kmalloc
does not zero); C-b's reap_pending walks every participant, so garbage
reaped a healthy VM (seen live before any KILL). Now zeroed at alloc.
Probe (reverted): baseline KILL refused, slots 3->4->4 (FAIL, proves G6);
with Stage D dprobe parked idle, KILL dead, slots 3->4->3, PASS on amd64/
aarch64/riscv64. Acceptance: dict_hash unchanged, relay 3, idle growth 0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Who runs next is now sk_vm_switch_decide(): among participants with work
(pending kernel-Hermes mail, parked mid-task, or Hera with console input),
the coolest by VM-physics heat; ties with the incumbent keep the incumbent,
ties among candidates go to the longest waiting (unique wait stamps). No
scheduler state, no timer: heartbeat_tick() no longer touches switching,
readiness/threshold/has_work/take_pending removed. Decided at owned word
checkpoints (colon words and, new, between top-level words) after a beat
or new mail, at the trampoline between messages, and at Hera's idle point
in the console poll loops. The idle pump skips participants (G3 closed for
them): the WIREBIND relay is delivered by a real switch to rajames~user.
Probe (restored away): relay via switch Hera->rajames~user->Hera on all
three ISAs, parked saved_sp inside own stack, walk-order disagreements 0
over tens of millions of decisions, 0 bytes idle log growth. Acceptance:
dict_hash unchanged, relay 3, no faults. Rulings: C-3, C-5 as proposed;
C-4 taken as (alpha), consequence recorded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Captain Bob: "give frames IDs and call by ID" -- frames stack per VM, each
with an ID. New vm_frame.h/.c: VMFrameEntry { VMFrameID id; VMUuid vm_id;
kind (INTERPRET/DRAIN/IDLE) }, a growable VMFrameStack embedded as VM.frames
(reserved at birth, freed in vm_cleanup, grown by allocate-copy-free).
vm_interpret() -> vm_interpret_kind() pushes a frame onto the running stack's
owner, holds only its ID, fails closed if it cannot grow, and pops by ID
(mismatch reported as VM FRAME CORRUPTION).
The Stage A/B loose fields are gone and everything is derived from the stack:
- ownership gate: one frame, and it is vm's own
- drain guard: a DRAIN frame for vm already on the running stack
- parked-idle: an IDLE frame the trampoline pushes around its yield
- log attribution: the top frame's vm_id, resolved by ID through the registry
The single saved-and-restored attribution global is removed.
Evidence: the probe baseline on the pre-frame code read HSSAS (the global
named Hestia while Hera resumed). With frames it reads HSHAS on all three
ISAs, with Hestia's top frame IDLE and Hera's frames back to 0; the probe is
reverted. Formal acceptance on all three ISAs: dict_hash identical and
unchanged, zero UNKNOWN WORD, faults, frame corruption or refused interprets,
and the WIREBIND relay 1 2 + . -> 3 works.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sk_vm_switch_entry() stops being the Stage-2 placeholder bounce and becomes
every switch participant's own idle loop: it drains one of its own
kernel-Hermes messages per pass, on its own native stack (driven by the drain's
return value, so a non-consuming drain cannot spin), and when nothing is
drained it marks the VM parked_idle and yields back. It is entirely OS-driven,
with no VM-side yield word. sk_hermes_drain_checkpoint() gains a per-VM
hermes_draining guard so a payload's own in-word checkpoint cannot drain a
second message inside the first. Still inert in normal operation: nothing
marks work until Stage C.
Finding fixed here: amd64 sk_vm_switch_prime built the first-entry frame at
top-56, entering with rsp 16-aligned instead of the SysV-required 8 mod 16, so
the first aligned SSE spill in a callee took #GP (logs/20260923-142526, kept).
It had been latent since SXXVIII Stage 2 because the placeholder never called
anything. The frame is now at top-64 with a zeroed pad slot. aarch64 and
riscv64 were already correct.
Evidence: a temporary kernel_main probe (reverted, not in this commit) queued
two messages to Hestia, the first calling a colon word, and switched into her.
It passed on all three ISAs: 0 pending, parked_idle, depth 0, context saved on
her own stack, SB-SEQ=12 (drained one at a time, not nested), ledger and
stadium_conserved restored. Formal acceptance without the probe on all three
ISAs: dict_hash identical and unchanged, zero UNKNOWN WORD or faults, and the
live WIREBIND mint -> USE -> 1 2 + . -> 3 relay.
FABRIC-3.8.md also records Captain Bob's rulings that no VM-side word ever gives
up control and that multitasking is a compudynamic hybrid, and opens SS3.12
(VM frames for per-VM execution state).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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
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
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