fix(hermes): one delivery path -- a VM takes its own messages on its own turn, never on another's stack

FABRIC-3.5 SXXXII, closing FABRIC-3 SXXIX bug 2 (SXXXVI).  A send marks the
receiver for the switch signal; the VM's own loop on its native stack takes
its messages and yields; Hera's idle beat makes the pending switch instead
of running other VMs' messages on her stack; every VM gets switcher turns
from birth; the drain refuses any call outside the VM's own context (the
test: it held every relayed line before, none now).

Found on the way: amd64's first-entry frame left rsp 8 bytes off the SysV
entry alignment (#GP on the first interpreter run on a VM's own stack), and
a VM yielding from its loop was marked SWITCHED_OUT, which stopped console
relay after one message and would have made KILL refuse it.

Relay of four lines to a WIREBIND identity on all three ISAs, five
hot-plugged identities, and standard acceptance (POST 1052, hash
0x08873e0f44b7cb2a) all pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-14 10:37:57 -04:00
co-authored by Claude Opus 5.5
parent b7e8e4b058
commit 029a8ed9cf
34 changed files with 110307 additions and 119 deletions
+30 -68
View File
@@ -46,6 +46,7 @@
#include "starkernel/capsule_run.h"
#include "starkernel/vm/bootstrap/sk_vm_bootstrap.h"
#include "starkernel/vm/kernel_hermes.h" /* FABRIC-3.6.md task 3.8 -- sk_hermes_send_one() */
#include "starkernel/vm/switch.h" /* one path: sk_vm_switch_service_pending() */
#include "starkernel/vm/stadium.h" /* task 3.8 -- stadium_conserved() evidence print */
#include "block_subsystem.h"
#include "word_source/include/keyboard_words.h"
@@ -534,10 +535,11 @@ static void sk_cursor_show(void)
*
* FABRIC-3.6.md Phase 4 (Stage E), 2026-09-22: this used to guard a
* VM-EXEC "MSG-TICK" dispatch into every live child VM (FABRIC-2.md
* Phase C, common:messaging.4th, now removed). The mechanism changed --
* sk_repl_idle() now calls sk_hermes_drain_checkpoint() directly per VM,
* not VM-EXEC -- but the reentrancy hazard these two flags exist for is
* unchanged: drain_checkpoint() still calls vm_interpret() internally
* Phase C, common:messaging.4th, now removed). The mechanism changed
* twice -- to a direct sk_hermes_drain_checkpoint() per VM (2026-09-22),
* then to one switch per beat plus Hera's own drain (2026-10-14, one
* path) -- but the reentrancy hazard these two flags exist for is
* unchanged for Hera's own drain: it calls vm_interpret() internally
* (on the drained message's own payload), and sk_repl_idle() is itself
* called from the blocking KEY/EXPECT/QUERY reads (sk_console_getkey()/
* sk_console_readline(), which run *from inside* the executing VM's own
@@ -563,9 +565,9 @@ static int g_idle_pump_active; /* zero-initialized (BSS) */
* cursor, a fixed number of dispatches per beat). FABRIC-3.6.md Phase 4
* (Stage E), 2026-09-22: that whole mechanism (MSG-TICK, the batching
* cap, and this comment's own prior form) is gone along with
* common:messaging.4th itself -- see sk_repl_idle()'s own current
* comment, just below, for why the replacement (kernel-Hermes drain)
* does not carry the same O(N) cost and needs no cap. */
* common:messaging.4th itself. Since 2026-10-14 a beat makes at most one
* switch (the one the switch signal has pending) and drains only Hera:
* O(1), no cap needed. */
static void sk_repl_idle(VM *active_vm)
{
@@ -853,34 +855,24 @@ static void sk_repl_idle(VM *active_vm)
* comment. */
capsule_wirebind_reap_idle_check();
/* FABRIC-3.6.md Phase 4 (Stage E, Category B strip), 2026-09-22:
* this used to be a distributed FORTH messaging pump -- FABRIC-2.md
* Phase C's own design, VM-EXEC'ing MSG-TICK into every live VM's
* own dictionary once per idle beat (common:messaging.4th, now
* removed entirely). That mechanism, and the comment that used to
* sit here explaining it, are both gone: kernel-Hermes's own drain
* (below) has needed no FORTH word since task 3.9, and every
* messaging.4th-owned message type had a real cutover by task 3.10
* (FABRIC-3.6.md task 3.11, Phase 3 gate). The prior version of this
* comment also claimed "Hera never loads common:messaging.4th" --
* that was already wrong before this strip (capsules/init.4th line
* 21 EXECs it directly; task 3.10's own live FIND-based check
* confirmed it), not just made moot by removing the file. She still
* has the only persistent idle tick, so she is still the one that
* walks the registry once per idle beat -- now purely to give every
* live VM's own kernel-Hermes queue a chance to drain, batched the
* same round-robin way the old MSG-TICK dispatch was (SK_MSG_PUMP_
* BATCH below), kept for the same reason: bounding this to O(K)
* regardless of total VM count, not O(N) every beat. */
/* ONE PATH (FABRIC-3.5.md §XXXII, built 2026-10-14). Hera has the only
* persistent idle beat, so this is where a VM with messages waiting
* gets its turn while she sits at her prompt: the switch the switch
* signal has pending is made here, from whichever VM owns this stack,
* and the target takes its own messages on its own stack (switch.c's
* sk_vm_switch_entry()). Until 2026-10-14 this walked every live VM
* and ran each one's messages for it, here, on Hera's stack -- the
* second delivery path, now gone (and refused: kernel_hermes.c). Before
* 2026-09-22 it VM-EXEC'd MSG-TICK into each VM instead (FABRIC-2.md
* Phase C, common:messaging.4th, removed). */
VM *mama = (VM *)sk_get_mama_vm();
/* Reentrancy guard: never run the pump while mama is mid-interpret
* (a dispatched line, or recursively from within the pump's own
* vm_interpret). vm_interpret() clobbers the VM's single input
* buffer, so re-entering it here while KEY/EXPECT/QUERY blocks inside
* a live parse truncates the rest of that line. Deferring the MSG-TICK
* drain to the next prompt boundary is safe -- draining is best-effort
* and simply resumes next beat. */
/* Reentrancy guard: not while Hera is mid-line (a dispatched line, or
* recursively from within this beat's own work). A switch made from
* inside a KEY/EXPECT/QUERY wait would be safe -- the whole stack is
* parked and comes back intact -- but Hera's own messages run in her
* interpreter, which must not be entered mid-line; and nothing here is
* urgent: it resumes next beat, at the prompt. */
if (g_mama_interpreting || g_idle_pump_active) {
if (console_tx_count() == tx_before_idle) {
console_cancel_deferred_line_start();
@@ -890,42 +882,12 @@ static void sk_repl_idle(VM *active_vm)
g_idle_pump_active = 1;
{
/* FABRIC-3.6.md Phase 4 (Stage E), 2026-09-22: the SK_MSG_PUMP_
* BATCH cap this loop used to enforce existed to bound the cost
* of the old MSG-TICK VM-EXEC dispatch -- a genuinely expensive
* per-VM vm_interpret() call, confirmed live as a real O(N)
* bottleneck at just 9 VMs on riscv64 (see this section's own
* history above). sk_hermes_drain_checkpoint() (task 3.4) is not
* that: its own fast path is a single global-counter read
* (sk_hermes_pending_total == 0 -> return immediately,
* kernel_hermes.c's own doc comment), so walking every live VM
* every beat costs one cheap integer read per VM in the common
* case (nothing in flight), not an O(N) wall -- the cap this
* loop's own comment history worried about does not apply to
* this call. Removed: every live VM (except mama, handled
* separately below) is now visited every idle beat, matching
* what task 3.9's own testing already exercised, without the
* ceil(N/SK_MSG_PUMP_BATCH)-beat delivery latency the cap was
* accidentally imposing on kernel-Hermes drain specifically. */
uint32_t count = capsule_vm_registry_count();
uint32_t i;
for (i = 0; i < count; i++) {
VMRegistryEntry ent;
if (capsule_vm_registry_get_by_index(i, &ent) == 0 &&
ent.state == VM_STATE_LIVE &&
ent.vm_ptr != (void *)mama) {
(void)sk_hermes_drain_checkpoint((VM *)ent.vm_ptr);
}
}
VM *cur = sk_vm_switch_current_vm();
if (cur) (void)sk_vm_switch_service_pending(cur);
/* Hera's own messages, when this beat is running on her stack:
* she is the VM switched in, at her prompt -- her own context. */
if (!cur || cur == mama) (void)sk_hermes_drain_checkpoint(mama);
}
/* Hera's own kernel-Hermes queue needs draining too, same as every
* other VM's -- she is excluded from the loop above not because she
* has nothing to drain, but because she IS the pump: a self-
* targeting call here runs directly, no VM-EXEC/reentrancy concern
* (sk_hermes_drain_checkpoint() is a plain C call, not a dispatch
* into anyone else's input buffer). */
(void)sk_hermes_drain_checkpoint(mama);
g_idle_pump_active = 0;
if (console_tx_count() == tx_before_idle) {