Stage 3 follow-on: message-arrival eligibility hook + trampoline-blind switch-storm fix (FABRIC-3.md §XXVIII.2)
Build / build-amd64-iso (push) Canceled after 0s
Build / build-aarch64-iso (push) Canceled after 0s
Build / build-riscv64-img (push) Canceled after 0s

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
This commit is contained in:
Robert Allan James
2026-09-14 17:39:20 -04:00
co-authored by Claude Sonnet 5
parent 862d7d9c48
commit 66beae7fd4
29 changed files with 121129 additions and 338 deletions
@@ -29,7 +29,7 @@
#include "starkernel/capsule_vm_switch_signal.h"
#include "vm.h"
#include "starkernel/log_attrib.h" /* vm_log_attributed_vm() */
#include "starkernel/vm/switch.h" /* sk_vm_switch_current_vm() -- see below */
#include <stddef.h>
/* Headroom beyond today's 3 participants (Hera/Hermes/Artemis) -- see
@@ -49,6 +49,11 @@
typedef struct {
VMUuid vm_id;
uint32_t readiness;
int has_work; /* Stage 3 follow-on (2026-09-14): set by
* sk_vm_switch_signal_mark_work() (mainline,
* MSG-SEND hook), cleared here in tick() the
* instant this slot becomes current -- see that
* function's own doc comment in the header. */
} sk_switch_slot_entry_t;
static sk_switch_slot_entry_t g_slots[SK_SWITCH_MAX_SLOTS];
@@ -73,6 +78,7 @@ int sk_vm_switch_signal_register(VMUuid vm_id) {
if (g_slot_count >= SK_SWITCH_MAX_SLOTS) return -1;
g_slots[g_slot_count].vm_id = vm_id;
g_slots[g_slot_count].readiness = 0;
g_slots[g_slot_count].has_work = 0;
return g_slot_count++;
}
@@ -80,8 +86,21 @@ void sk_vm_switch_signal_tick(void) {
VM *current;
int current_slot, i;
current = vm_log_attributed_vm();
if (!current) return; /* nothing interpreting right now (boot/driver/HAL context) */
/* FABRIC-3.md §XXVIII Stage 3 follow-on correction (2026-09-14): was
* vm_log_attributed_vm() -- wrong source. That tracks nested
* vm_interpret() calls for log attribution and has no way to know a
* VM is running while parked inside switch.c's own raw-C trampoline
* (sk_vm_switch_entry()), which never itself calls vm_interpret(). A
* VM that only ever ran the trampoline was therefore never seen as
* "current," so its own readiness/has_work below never reset --
* tick() re-armed the very next timer tick, forever, once nothing
* else happened to be independently flipping attribution around it
* (found live: a switch storm, 101% CPU, log frozen). sk_vm_switch_
* current_vm() is authoritative instead: the switch mechanism itself
* updates it on every switch, so it reflects who is physically
* running regardless of whether that VM ever calls vm_interpret(). */
current = sk_vm_switch_current_vm();
if (!current) return; /* boot: no switch has happened yet, not seeded */
current_slot = slot_for_vm_id(current->stadium_vm_id);
if (current_slot < 0) return; /* current VM isn't a registered participant */
@@ -92,6 +111,7 @@ void sk_vm_switch_signal_tick(void) {
for (i = 0; i < g_slot_count; i++) {
if (i == current_slot) {
g_slots[i].readiness = 0;
g_slots[i].has_work = 0;
} else {
g_slots[i].readiness++;
}
@@ -99,7 +119,9 @@ void sk_vm_switch_signal_tick(void) {
if (!g_pending) {
for (i = 0; i < g_slot_count; i++) {
if (i != current_slot && g_slots[i].readiness >= SK_SWITCH_READINESS_THRESHOLD) {
if (i != current_slot &&
g_slots[i].readiness >= SK_SWITCH_READINESS_THRESHOLD &&
g_slots[i].has_work) {
g_pending = 1;
g_pending_target = g_slots[i].vm_id;
break;
@@ -114,11 +136,38 @@ VMUuid sk_vm_switch_signal_take_pending(void) {
return g_pending_target;
}
void sk_vm_switch_signal_note_switch_performed(void) {
void sk_vm_switch_signal_note_switch_performed(VMUuid target_id) {
/* FABRIC-3.md §XXVIII Stage 3 follow-on correction (2026-09-14):
* reset the target's own readiness/has_work HERE, deterministically,
* at the switch decision point -- not by waiting for tick()'s
* current-slot polling to observe it running. That polling depends on
* sk_vm_switch_current_vm() catching a real ISR tick during whatever
* window the target is genuinely "current" for, and for a VM whose
* only production behavior is switch.c's placeholder trampoline (an
* immediate yield-back), that window can be as small as a few
* instructions -- too narrow for a timer tick to reliably land in.
* Without this, the target's eligibility never clears, tick() re-arms
* on the very next timer tick, and the result is a continuous switch
* storm (found live: 101% CPU, zero forward progress). This makes
* correctness independent of ISR timing; current_slot (via
* sk_vm_switch_current_vm()/tick()) remains useful as a DoE-observable
* value and for excluding the running slot from readiness accrual,
* but is no longer load-bearing for storm prevention. */
int slot = slot_for_vm_id(target_id);
if (slot >= 0) {
g_slots[slot].readiness = 0;
g_slots[slot].has_work = 0;
}
g_switch_count++;
g_ticks_since_switch = 0;
}
void sk_vm_switch_signal_mark_work(VMUuid vm_id) {
int slot = slot_for_vm_id(vm_id);
if (slot < 0) return;
g_slots[slot].has_work = 1;
}
uint64_t sk_vm_switch_signal_switch_count(void) {
return g_switch_count;
}
+41
View File
@@ -55,6 +55,7 @@
#include "starkernel/capsule_vm_physics.h"
#include "starkernel/vm/vm_internal.h"
#include "starkernel/vm/stadium.h" /* item 4.2 -- STADIUM-* words */
#include "starkernel/capsule_vm_switch_signal.h" /* FABRIC-3.md §XXVIII Stage 3 follow-on -- SWITCH-MARK-WORK */
#include "starkernel/vm/stadium_words.h" /* item 4.2 -- stadium_words_resident_heat() */
#include "starkernel/repl.h"
#include "starkernel/capsule_generated.h"
@@ -785,6 +786,43 @@ static void mama_word_vm_exec(VM *vm)
/* Stack clean on exit */
}
/**
* @brief SWITCH-MARK-WORK ( name-caddr name-u -- )
* FABRIC-3.md §XXVIII Stage 3 follow-on (2026-09-14): message-arrival
* eligibility hook. Called from MSG-SEND (capsules/common/messaging.4th)
* every time a message is sent to a VM, so the Stage 3 switch signal can
* tell "has recently been sent something" apart from "has just been idle
* long enough" before ever selecting it as a switch target. Silently
* ignores a malformed or unknown name -- this fires on the hot path of
* every message send, not a user-facing command, so it must never error
* or spam the console.
*/
static void mama_word_switch_mark_work(VM *vm)
{
char name[VM_NAME_MAX];
cell_t u, caddr;
uint32_t i;
VMRegistryEntry entry;
if (vm->dsp < 2) { vm->error = 1; return; }
u = vm_pop(vm);
caddr = vm_pop(vm);
if (u <= 0 || (uint32_t)u >= VM_NAME_MAX) return;
{
const uint8_t *p = vm_ptr(vm, (vaddr_t)caddr);
if (!p) return;
for (i = 0; i < (uint32_t)u; i++) name[i] = (char)p[i];
name[u] = '\0';
}
if (capsule_vm_find_by_name_nocase(name, &entry) == 0) {
sk_vm_switch_signal_mark_work(entry.vm_id);
}
}
/**
* @brief VM-CALL ( cmd-caddr cmd-u vm-name-caddr vm-name-u -- n )
* Like VM-EXEC but pops one cell from the target VM's TOS and pushes
@@ -1653,6 +1691,7 @@ void register_mama_forth_words(VM *vm)
register_word(vm, "VM-PHYSICS-STATUS", mama_word_vm_physics_status);
register_word(vm, "VM-STEP", mama_word_vm_step);
register_word(vm, "VM-EXEC", mama_word_vm_exec);
register_word(vm, "SWITCH-MARK-WORK", mama_word_switch_mark_work);
register_word(vm, "VM-CALL", mama_word_vm_call);
register_word(vm, "VM-HEAT", mama_word_vm_heat);
/* FABRIC-3.md SXX: the 8 STADIUM-* primitives register_child_vm_words()
@@ -1711,6 +1750,7 @@ void register_mama_forth_words(VM *vm)
register_word(vm, "VM-PHYSICS-STATUS", mama_word_vm_physics_status);
register_word(vm, "VM-STEP", mama_word_vm_step);
register_word(vm, "VM-EXEC", mama_word_vm_exec);
register_word(vm, "SWITCH-MARK-WORK", mama_word_switch_mark_work);
register_word(vm, "VM-CALL", mama_word_vm_call);
register_word(vm, "VM-HEAT", mama_word_vm_heat);
/* FABRIC-3.md SXX: the 8 STADIUM-* primitives register_child_vm_words()
@@ -1946,6 +1986,7 @@ void register_child_vm_words(VM *vm)
register_word(vm, "STOP", mama_word_stop);
register_word(vm, "EXEC", mama_word_exec);
register_word(vm, "VM-EXEC", mama_word_vm_exec);
register_word(vm, "SWITCH-MARK-WORK", mama_word_switch_mark_work);
register_word(vm, "VM-CALL", mama_word_vm_call);
register_word(vm, "VM-HEAT", mama_word_vm_heat);
/* USE (FABRIC-2.md §F.24): not console-specific -- any VM can
+6
View File
@@ -64,6 +64,7 @@ EFI_RUNTIME_SERVICES *g_sk_runtime_services = NULL;
#include "starkernel/capsule_birth.h" /* capsule_birth_mama, capsule_find_mama_init */
#include "starkernel/capsule_zuse_boot.h" /* capsule_zuse_boot_load_root_pubkey */
#include "starkernel/capsule_vm_switch_signal.h" /* FABRIC-3.md §XXVIII Stage 3 */
#include "starkernel/vm/switch.h" /* sk_vm_switch_set_current() -- Stage 3 follow-on */
#include "starkernel/artemis_sig.h" /* artemis_sig_check/genesis_stamp */
#include "starkernel/kmalloc.h"
#include "starkernel/repl.h"
@@ -997,6 +998,11 @@ static void kernel_main_deep(BootInfo *boot_info) {
VMRegistryEntry hera_entry, hermes_entry, artemis_entry;
if (capsule_vm_registry_get(vm_uuid_hera(), &hera_entry) == 0) {
sk_vm_switch_signal_register(hera_entry.vm_id);
/* Seed the switch mechanism's own "who is running" tracker
* (FABRIC-3.md §XXVIII Stage 3 follow-on, 2026-09-14) -- Hera
* is genuinely the one running here, before any switch has
* ever happened. */
sk_vm_switch_set_current(mama);
}
if (capsule_vm_find_by_name_nocase("Hermes", &hermes_entry) == 0 &&
hermes_entry.state == VM_STATE_LIVE) {
+34
View File
@@ -51,6 +51,26 @@ extern uint64_t sk_vm_switch_prime(uint64_t stack_top, void *entry);
* who most recently switched into it does. */
static VM *g_switch_entry_vm;
/* FABRIC-3.md §XXVIII Stage 3 follow-on (2026-09-14): which VM is
* currently running, from the switch mechanism's own point of view --
* independent of vm_log_attributed_vm() (vm_core.c), which is scoped to
* nested vm_interpret() calls for log-source attribution and has no way
* to know a VM is "running" while parked inside this file's own raw C
* trampoline (sk_vm_switch_entry()), never itself calling vm_interpret().
* sk_vm_switch_signal_tick() (capsule_vm_switch_signal.c) consults this
* instead of vm_log_attributed_vm() for exactly that reason -- see its
* own updated comment. Single-writer-mainline, same discipline as
* g_switch_entry_vm above: set unconditionally on every switch, by the
* only code path that can ever change who is physically running.
* Seeded once at boot via sk_vm_switch_set_current() for whichever VM is
* running before any switch has ever happened (Hera, at Stage 3
* registration time -- kernel_main.c). */
static VM *g_switch_current_vm;
VM *sk_vm_switch_current_vm(void) { return g_switch_current_vm; }
void sk_vm_switch_set_current(VM *vm) { g_switch_current_vm = vm; }
/**
* sk_vm_switch_entry - trampoline for a VM's first-ever entry.
*
@@ -64,6 +84,18 @@ static VM *g_switch_entry_vm;
* first. Must never fall off the end -- there is no legitimate return
* address below a synthetic first-entry frame, unlike an ordinary
* function.
*
* FABRIC-3.md §XXVIII Stage 3 follow-on (2026-09-14): an attempt to make
* this attributable by calling vm_interpret(self, "MSG-TICK") here was
* tried and reverted -- it introduced a second, distinct hang (a stall in
* sk_repl_headless_wait()'s WIREBIND-attachment wait, not root-caused) in
* place of the switch-storm it was meant to fix. The switch-storm's real
* cause (sk_vm_switch_signal_tick()'s "who is current" check via
* vm_log_attributed_vm() never seeing a VM that only ever runs this raw
* C trampoline) is instead fixed at the source in switch.c/
* capsule_vm_switch_signal.c: sk_vm_context_switch() now tracks "current"
* itself, independent of vm_interpret()'s own attribution bookkeeping.
* This placeholder's behavior is therefore unchanged from Stage 2.
*/
void sk_vm_switch_entry(void) {
VM *self = g_switch_entry_vm;
@@ -95,12 +127,14 @@ int sk_vm_context_switch(VM *from, VM *to) {
capsule_vm_set_state(to->stadium_vm_id, VM_STATE_LIVE);
capsule_vm_set_state(from->stadium_vm_id, VM_STATE_SWITCHED_OUT);
g_switch_current_vm = to;
sk_vm_switch_to(&from->native_stack_saved_sp, new_sp);
/* Resumes here only once something later switches back to `from` --
* from `from`'s own point of view, this call simply took a while. */
capsule_vm_set_state(from->stadium_vm_id, VM_STATE_LIVE);
g_switch_current_vm = from;
return 0;
}
+4 -2
View File
@@ -920,14 +920,16 @@ void execute_colon_word(VM* vm)
* `vm`, which could be an arbitrary number of ticks later --
* calling it after would attribute switch_count_cumulative/
* switch_ticks_since to the wrong (resume) event instead of the
* actual switch-out this counts. */
* actual switch-out this counts. It also resets target_id's own
* eligibility state directly (2026-09-14 correction) -- see its
* own doc comment for why that can't be left to ISR polling. */
if (sk_vm_at_outermost_interpret()) {
VMUuid target_id = sk_vm_switch_signal_take_pending();
if (!vm_uuid_equal(target_id, vm_uuid_none())) {
VMRegistryEntry target_entry;
if (capsule_vm_registry_get(target_id, &target_entry) == 0 &&
target_entry.vm_ptr && target_entry.vm_ptr != (void *)vm) {
sk_vm_switch_signal_note_switch_performed();
sk_vm_switch_signal_note_switch_performed(target_id);
sk_vm_context_switch(vm, (VM *)target_entry.vm_ptr);
/* Resumes here once something later switches back. */
}