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
+28
View File
@@ -52,6 +52,9 @@
#include "starkernel/kmalloc.h"
#include "console.h"
#include "vm.h" /* task 3.4 -- VM struct fields, vm_interpret() */
#include "starkernel/vm/switch.h" /* one path: sk_vm_switch_current_vm() */
#include "starkernel/log_attrib.h" /* one path: vm_log_attributed_vm() */
#include "starkernel/capsule_vm_switch_signal.h" /* one path: sk_vm_switch_signal_mark_work() */
/* Freestanding: no libc printf. Prints an unsigned decimal, no leading
* zeros -- same small helper stadium.c/capsule_vm_switch_signal.c each
@@ -531,6 +534,9 @@ int sk_hermes_send_one(VMUuid from, VMUuid to, uint32_t type, uint32_t channel_i
return -1;
}
/* One path (2026-10-14): the receiver takes it on its own turn -- ask
* the switcher to give it one. */
sk_vm_switch_signal_mark_work(to);
return 0;
}
@@ -594,6 +600,28 @@ int sk_hermes_drain_checkpoint(VM *vm) {
msg = sk_hermes_pending_peek(self);
if (!msg) return 0;
/* ONE PATH (FABRIC-3.5.md §XXXII, built 2026-10-14): a VM takes its
* messages itself, in its own context, and nowhere else -- either
* - its own interpreter is the one running, at the outermost text
* (the checkpoint in execute_colon_word()), or
* - it is the VM switched in, and its interpreter is not mid-line on
* some other, parked, stack (switch.c's sk_vm_switch_entry(), and
* Hera's own idle beat at her prompt).
* Anything else would run one VM's message on another's stack, or
* enter an interpreter that is already mid-line. Refused, and the
* message stays queued for the VM's own turn. */
{
VM *cur = sk_vm_switch_current_vm();
int running_here = vm->interpret_depth > 0 &&
vm == vm_log_attributed_vm() &&
sk_vm_at_outermost_interpret();
int switched_in = vm->interpret_depth == 0 && (!cur || vm == cur);
if (!running_here && !switched_in) {
console_println("Kernel-Hermes: message held -- its VM is not the one running");
return 0;
}
}
/* See kernel_hermes.h's own doc comment on this function for why all
* six of these must be preserved, not just the obvious ones. */
memcpy(saved_input, vm->input_buffer, sizeof(saved_input));
+74 -24
View File
@@ -30,6 +30,8 @@
#include "starkernel/vm/switch.h"
#include "vm.h"
#include "starkernel/capsule_birth.h" /* capsule_vm_set_state */
#include "starkernel/capsule_vm_switch_signal.h" /* one path: take_pending(), mark_work() */
#include "starkernel/vm/kernel_hermes.h" /* one path: sk_hermes_drain_checkpoint() */
#include <stdint.h>
/* Arch-specific (switch.S in each arch dir). sk_vm_switch_prime() builds
@@ -51,6 +53,8 @@ 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;
static int switch_core(struct VM *from, struct VM *to, VMState from_state);
/* 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
@@ -72,39 +76,81 @@ 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.
* sk_vm_switch_entry - what a VM runs on its own native stack.
*
* A freshly-entered VM has no production behavior defined yet -- what a
* switched-to VM should actually DO once running is later-stage work
* (message dispatch, an idle loop, whatever Stage 3+ needs). For now this
* is a minimal, permanent placeholder: immediately yield back to whoever
* most recently switched to it, forever. Reads self->native_switch_back_to
* Entered once, at a VM's first-ever switch-in, and never left: each turn
* it takes its own pending kernel-Hermes messages, then yields back to
* whoever most recently switched to it. Reads self->native_switch_back_to
* fresh every iteration (not a value captured once) so a later switch-in
* from a different VM is honored, not silently bounced to whoever entered
* first. Must never fall off the end -- there is no legitimate return
* address below a synthetic first-entry frame, unlike an ordinary
* function.
* from a different VM is honored. Must never fall off the end -- there is
* no legitimate return address below a synthetic first-entry frame.
*
* 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.
* History. Until 2026-10-14 this only yielded (a Stage 2 placeholder), and
* a VM's messages were run for it from Hera's idle beat, on her stack --
* the second of the two delivery paths FABRIC-3.5.md §XXXII ruled down to
* one. A first attempt at giving it work (2026-09-14: vm_interpret(self,
* "MSG-TICK") here) was reverted after a stall in
* sk_repl_headless_wait()'s WIREBIND wait, never root-caused; the switch
* storm it was aimed at was fixed at its source instead (switch.c and
* capsule_vm_switch_signal.c track "current" themselves).
*/
void sk_vm_switch_entry(void) {
VM *self = g_switch_entry_vm;
for (;;) {
sk_vm_context_switch(self, self->native_switch_back_to);
/* ONE PATH (FABRIC-3.5.md §XXXII, built 2026-10-14): what a VM does
* on its turn is take its own messages, here, on its own stack --
* the only place a VM that nothing else drives ever takes one
* (Hera's idle beat used to run them for it, on her stack). Not
* while its interpreter is mid-line on some other, parked, stack
* (a console VM waiting in KEY, say): then they wait, and it asks
* for another turn. */
if (self->interpret_depth == 0) {
while (sk_hermes_drain_checkpoint(self) != 0) { }
}
if (sk_hermes_pending_count(self->stadium_vm_id) > 0) {
sk_vm_switch_signal_mark_work(self->stadium_vm_id);
}
switch_core(self, self->native_switch_back_to, VM_STATE_LIVE);
}
}
int sk_vm_context_switch(VM *from, VM *to) {
int sk_vm_switch_service_pending(VM *from) {
VMRegistryEntry target;
VMUuid target_id = sk_vm_switch_signal_take_pending();
if (!from || vm_uuid_equal(target_id, vm_uuid_none())) return 0;
if (capsule_vm_registry_get(target_id, &target) != 0 ||
!target.vm_ptr || target.vm_ptr == (void *)from) {
return 0;
}
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): a target marked
* pending_reap is a WIREBIND identity whose owning device has
* physically detached while it sat parked SWITCHED_OUT --
* capsule_vm_kill() already refused to free it when that happened
* (see VMRegistryEntry.pending_reap's own doc comment). This is the
* safe point the mark-and-defer design calls for: reap instead of
* resuming, and skip the switch -- take_pending() already consumed the
* pending flag, so no other caller will try this same dead end. */
if (target.pending_reap) {
capsule_vm_force_reap(target_id);
return 0;
}
sk_vm_switch_signal_note_switch_performed(target_id);
sk_vm_context_switch(from, (VM *)target.vm_ptr);
/* Resumes here once something later switches back. */
return 1;
}
/* `from_state` is what `from` is while it is parked. VM_STATE_SWITCHED_OUT
* for a VM switched out mid-line (the colon-word checkpoint, Hera's idle
* beat): its stack holds live frames, and capsule_vm_kill() must refuse
* it. VM_STATE_LIVE for a VM yielding from the top of its own loop in
* sk_vm_switch_entry() (2026-10-14): its stack holds nothing but that loop,
* so it is as alive, as killable and as reachable by name as a VM that has
* never had a turn -- marking it SWITCHED_OUT made the console stop
* relaying to a WIREBIND identity after its first message, and made KILL
* refuse every VM that had ever taken one. */
static int switch_core(VM *from, VM *to, VMState from_state) {
uint64_t new_sp;
if (!from || !to || to->native_stack_top == 0) {
@@ -126,7 +172,7 @@ 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);
capsule_vm_set_state(from->stadium_vm_id, from_state);
g_switch_current_vm = to;
sk_vm_switch_to(&from->native_stack_saved_sp, new_sp);
@@ -138,4 +184,8 @@ int sk_vm_context_switch(VM *from, VM *to) {
return 0;
}
int sk_vm_context_switch(VM *from, VM *to) {
return switch_core(from, to, VM_STATE_SWITCHED_OUT);
}
#endif /* __STARKERNEL__ */
+3 -25
View File
@@ -958,31 +958,7 @@ void execute_colon_word(VM* vm)
* 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) {
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): a target
* marked pending_reap is a WIREBIND identity whose
* owning device has physically detached while it sat
* parked SWITCHED_OUT -- capsule_vm_kill() already
* refused to free it when that happened (see
* VMRegistryEntry.pending_reap's own doc comment).
* This checkpoint is the safe point the mark-and-
* defer design calls for: reap instead of resuming,
* and skip the switch this tick -- take_pending()
* already consumed the pending flag, so no other
* checkpoint will try this same dead end. */
if (target_entry.pending_reap) {
capsule_vm_force_reap(target_id);
} else {
sk_vm_switch_signal_note_switch_performed(target_id);
sk_vm_context_switch(vm, (VM *)target_entry.vm_ptr);
/* Resumes here once something later switches back. */
}
}
}
(void)sk_vm_switch_service_pending(vm);
}
#endif
@@ -1202,6 +1178,7 @@ void vm_interpret(VM* vm, const char* input)
outer_buffer[outer_length] = '\0';
}
g_vm_interpret_depth++;
vm->interpret_depth++;
/* Load into input buffer (cap + NUL) */
size_t n = 0, cap = INPUT_BUFFER_SIZE ? INPUT_BUFFER_SIZE - 1 : 0;
@@ -1235,6 +1212,7 @@ void vm_interpret(VM* vm, const char* input)
}
g_vm_interpret_depth--;
vm->interpret_depth--;
if (nested) {
size_t k;
for (k = 0; k <= outer_length; k++) vm->input_buffer[k] = outer_buffer[k];