Files
LithosAnanke/kernel/src/vm/switch.c
T
rajamesandClaude Opus 5.5 029a8ed9cf 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>
2026-10-14 10:37:57 -04:00

192 lines
8.5 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* switch.c - Cooperative VM context switch glue (FABRIC-3.md §XXVIII,
* Stage 2, 2026-09-13). See switch.h for the contract.
*/
#ifdef __STARKERNEL__
#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
* the exact synthetic first-entry frame sk_vm_switch_to() expects -- kept
* in assembly, not duplicated here, so the layout can never drift out of
* sync between the two. */
extern void sk_vm_switch_to(uint64_t *save_sp_out, uint64_t new_sp);
extern uint64_t sk_vm_switch_prime(uint64_t stack_top, void *entry);
/* Single-writer-mainline global, safe under this stage's cooperative-only,
* one-VM-runs-at-a-time-on-one-hart discipline (same class of bookkeeping
* as vm_core.c's g_log_attrib_vm) -- identifies which VM a freshly-primed
* native stack belongs to. Read exactly once, synchronously, at the very
* top of sk_vm_switch_entry() before any other first-entry could occur.
* Safe to keep as a plain global (unlike the old g_switch_back_to this
* file used to also carry -- see native_switch_back_to's own doc comment
* in vm.h for why that one had to move to per-VM state): a VM's identity
* never changes across the lifetime of its own trampoline instance, only
* 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
* 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 - what a VM runs on its own native stack.
*
* 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. Must never fall off the end -- there is
* no legitimate return address below a synthetic first-entry frame.
*
* 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 (;;) {
/* 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_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) {
return -1;
}
/* Recorded unconditionally (first-ever entry or a resume) so whoever
* is parked at `to`'s native stack -- most often this placeholder
* trampoline -- always knows who most recently switched into it. */
to->native_switch_back_to = from;
if (to->native_stack_saved_sp == 0) {
/* First-ever entry: synthesize the initial frame. */
g_switch_entry_vm = to;
new_sp = sk_vm_switch_prime(to->native_stack_top, (void *)&sk_vm_switch_entry);
} else {
new_sp = to->native_stack_saved_sp;
to->native_stack_saved_sp = 0; /* about to be running, not parked */
}
capsule_vm_set_state(to->stadium_vm_id, VM_STATE_LIVE);
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);
/* 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;
}
int sk_vm_context_switch(VM *from, VM *to) {
return switch_core(from, to, VM_STATE_SWITCHED_OUT);
}
#endif /* __STARKERNEL__ */