Stage 2: cooperative VM context switch primitive, proven on all 3 arches (FABRIC-3.md §XXVIII)
Build / build-amd64-iso (push) Waiting to run
Build / build-aarch64-iso (push) Waiting to run
Build / build-riscv64-img (push) Waiting to run

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
This commit is contained in:
Robert Allan James
2026-09-13 15:25:39 -04:00
co-authored by Claude Sonnet 5
parent 57ac3fc304
commit f790d0995e
25 changed files with 54894 additions and 7 deletions
+15 -1
View File
@@ -87,9 +87,23 @@ typedef struct {
typedef enum {
VM_STATE_EMBRYO = 0, /* Allocated but not yet born */
VM_STATE_LIVE, /* Successfully born, operational */
VM_STATE_STOPPED, /* Suspended — execution state saved */
VM_STATE_STOPPED, /* Suspended — execution state saved. Set by
* the START word after STOP cleanly unwinds
* its C stack back to START's own frame: no
* live native frame remains, safe to free. */
VM_STATE_STILLBORN, /* Birth failed */
VM_STATE_DEAD, /* Terminated */
VM_STATE_SWITCHED_OUT, /* FABRIC-3.md §XXVIII, Stage 2 (2026-09-13):
* a live saved register/stack context is
* parked on this VM's own native stack
* (SWITCH-TO switched control away mid-
* execution). Deliberately distinct from
* VM_STATE_STOPPED -- that state means "no
* live native frame," this one means the
* opposite. KILL must refuse/defer here: the
* parked frame still points into vm->memory
* and the native stack, both of which a free
* would invalidate out from under it. */
} VMState;
typedef struct {
+51
View File
@@ -0,0 +1,51 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 20232025 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.h - Cooperative VM context switch (FABRIC-3.md §XXVIII, Stage 2,
* 2026-09-13).
*
* sk_vm_context_switch() suspends `from`'s execution exactly where it is
* (mid-C-call-stack, on from's own native stack -- Stage 1) and resumes
* `to` -- either for the first time ever (a synthesized initial frame,
* entering sk_vm_switch_entry()) or exactly where `to` was itself last
* switched out. Returns to the caller only once something later switches
* back to `from` -- from from's own point of view, this call simply
* takes a while to return, like any blocking call.
*
* Cooperative only: nothing here is interrupt-driven yet (that's Stage
* 3), so this is safe to call only from ordinary FORTH word dispatch,
* never from ISR context.
*/
#ifndef STARKERNEL_VM_SWITCH_H
#define STARKERNEL_VM_SWITCH_H
#ifdef __STARKERNEL__
struct VM;
int sk_vm_context_switch(struct VM *from, struct VM *to);
#endif /* __STARKERNEL__ */
#endif /* STARKERNEL_VM_SWITCH_H */
+8 -3
View File
@@ -650,9 +650,14 @@ typedef struct VM
*/
uint64_t native_stack_paddr; /**< Physical base (for teardown); 0 = not allocated */
uint64_t native_stack_guard_vaddr; /**< Virtual base of the whole guarded region */
uint64_t native_stack_top; /**< Initial SP value once something switches onto
* this stack -- Stage 1 only allocates it; nothing
* yet runs here (that's Stage 2). */
uint64_t native_stack_top; /**< Top of the allocated region (fixed, for the
* initial frame construction); 0 = not allocated. */
uint64_t native_stack_saved_sp; /**< Stage 2 (FABRIC-3.md §XXVIII, 2026-09-13): the
* parked SP of a live, switched-out register
* context on this VM's own native stack. 0 = never
* entered (no context parked yet); non-zero only
* while VMRegistryEntry.state ==
* VM_STATE_SWITCHED_OUT for this VM. */
/** @} */
/** @name Stadium Identity (item 4.2, FABRIC-0.md §25.5)