Stage 2: cooperative VM context switch primitive, proven on all 3 arches (FABRIC-3.md §XXVIII)
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:
co-authored by
Claude Sonnet 5
parent
57ac3fc304
commit
f790d0995e
+56
@@ -3579,3 +3579,59 @@ All 3 architectures re-verified clean boot to `ok>`, no native-stack allocation
|
||||
for any Tripod-fleet VM on any arch. Stage 2 (the actual save/restore switch primitive,
|
||||
cooperative only, no timer) is next.
|
||||
|
||||
**Stage 2 CLOSED, same day.** The real save/restore switch primitive now exists and is proven
|
||||
correct on all 3 architectures -- the first time anything has ever executed on a VM's own
|
||||
native stack (Stage 1's stacks were allocated but unused until now).
|
||||
|
||||
Deliberately NOT built the same way as Stage 0's interrupt trap frame -- `sk_vm_switch_to()`
|
||||
(new `switch.S` beside each arch's `isr.S`) is an ordinary function call, so the standard ABI
|
||||
already guarantees every caller-saved register is the caller's own problem; only the
|
||||
callee-saved set needs explicit handling (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, the FP
|
||||
half still correctly FS-gated like Stage 0's trap frame, but read once and reused for both the
|
||||
save and restore decision within one call rather than persisted -- FS is genuine single global
|
||||
CPU state, not part of what's "switched"). A sibling `sk_vm_switch_prime()` in each same file
|
||||
builds the synthetic first-entry frame (zeroed callee-saved slots + a trampoline address in the
|
||||
return-address slot) so a VM's first-ever switch-in lands somewhere real instead of into
|
||||
whatever real prior caller the layout would otherwise imply -- kept in assembly, not C, so the
|
||||
frame layout can never drift out of sync between the two functions.
|
||||
|
||||
New C glue (`vm/switch.c`, `vm/switch.h`): `sk_vm_context_switch(from, to)` -- handles first-entry
|
||||
priming vs. resuming a previously-parked context, and updates registry state around the switch
|
||||
(new `VM_STATE_SWITCHED_OUT`, distinct from `VM_STATE_STOPPED` per the Stage 1 correction --
|
||||
STOPPED means no live frame, SWITCHED_OUT means the opposite). `sk_vm_switch_entry()` is the
|
||||
minimal permanent trampoline every freshly-entered VM lands in: no production behavior is
|
||||
defined yet (that's later-stage work), so it just yields straight back to whoever switched to
|
||||
it, forever -- must never fall off the end, since there is no legitimate return address below a
|
||||
synthetic first-entry frame.
|
||||
|
||||
**Closed the confirmed unguarded-KILL UAF** found during planning: `capsule_vm_kill()`,
|
||||
`mama_word_kill()`, and `capsule_vm_kill_all_nonmama()` all now refuse (or, for the
|
||||
cold-restart-only path, silently leak rather than free -- harmless there specifically, since
|
||||
`arch_cold_reset()` always follows immediately and wipes everything regardless) tearing down a
|
||||
VM in `VM_STATE_SWITCHED_OUT`. A nice side effect discovered while verifying, not something
|
||||
built deliberately: the existing MSG-TICK idle-pump (`repl.c`) already filters on
|
||||
`VM_STATE_LIVE`, so it automatically stopped dispatching into Hermes the moment she was
|
||||
switched out, with zero code changes needed there.
|
||||
|
||||
**Verification, per this project's write-probe-capture-revert discipline:** a temporary
|
||||
`SWITCH-TEST` word (registered in Hera's own vocabulary, triggered once at boot right after
|
||||
Hermes's birth, since there's no way to type interactively into a foreground-only QEMU session)
|
||||
round-tripped a distinctive sentinel through 5 real Hera<->Hermes context switches. All 3
|
||||
architectures: 5/5 rounds, 0 failures, PASS, clean continuation of boot to `ok>` afterward. The
|
||||
probe word, its boot-time trigger, and its helper were then fully reverted -- re-verified clean
|
||||
on all 3 architectures again with the probe gone, confirming no residual effects (Hermes back to
|
||||
normal `VM_STATE_LIVE` MSG-TICK dispatch once no longer switched out).
|
||||
|
||||
Also fixed along the way: `Makefile.starkernel`'s `LOADER_EXTRA_SRCS`/`LOADER_ASM` lists needed
|
||||
`switch.c`/`switch.S` added explicitly (this project's "loader" PE binary is effectively the
|
||||
full running kernel, not a thin bootstrap stage -- confirmed by `isr.S` already being in
|
||||
`LOADER_ASM`); and aarch64's `switch.S` needed the same `#ifndef _WIN32` guard around `.hidden`
|
||||
that `isr.S` already carries, since aarch64's loader is assembled via clang targeting
|
||||
`aarch64-pc-windows-msvc` (PE/COFF, no `.hidden` equivalent) -- missed on the first pass, caught
|
||||
by the aarch64 build failing outright.
|
||||
|
||||
Stage 3 (timer-driven preemption, Tripod fleet only, the new run-readiness signal) is next --
|
||||
the biggest remaining stage, and the first one that actually amends the §21.1
|
||||
nothing-is-concurrent-on-one-hart ruling for real.
|
||||
|
||||
|
||||
+3
-1
@@ -462,7 +462,8 @@ LOADER_SRCS_BASE := \
|
||||
|
||||
LOADER_ASM := \
|
||||
$(KERNEL_SRC)/arch/$(ARCH)/boot.S \
|
||||
$(KERNEL_SRC)/arch/$(ARCH)/isr.S
|
||||
$(KERNEL_SRC)/arch/$(ARCH)/isr.S \
|
||||
$(KERNEL_SRC)/arch/$(ARCH)/switch.S
|
||||
|
||||
ifeq ($(ARCH),amd64)
|
||||
LOADER_ASM += $(KERNEL_SRC)/arch/$(ARCH)/kernel_entry.S
|
||||
@@ -539,6 +540,7 @@ VM_CORE_SRCS := $(filter-out $(VM_EXCLUDE),$(VM_ALL_SRCS))
|
||||
|
||||
LOADER_EXTRA_SRCS := \
|
||||
$(KERNEL_SRC)/vm/arena.c \
|
||||
$(KERNEL_SRC)/vm/switch.c \
|
||||
$(KERNEL_SRC)/vm/parity.c \
|
||||
$(KERNEL_SRC)/vm/bootstrap/sk_vm_bootstrap.c \
|
||||
$(KERNEL_SRC)/vm/host/shim.c \
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Capsule Block Manifest — Auto-generated
|
||||
<!-- Generated by mkcapsule --manifest 2026-09-13T18:28:32Z -->
|
||||
<!-- Generated by mkcapsule --manifest 2026-09-13T19:23:06Z -->
|
||||
<!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. -->
|
||||
<!-- Hand-written justifications and immutability notes live -->
|
||||
<!-- in MANIFEST.md alongside this auto-generated index. -->
|
||||
|
||||
Binary file not shown.
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
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.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
@@ -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)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* switch.S (aarch64) - Cooperative VM context switch primitive.
|
||||
*
|
||||
* FABRIC-3.md §XXVIII (Stage 2, preemptive-context-switch save/restore
|
||||
* primitive, 2026-09-13). Deliberately NOT isr.S's interrupt trap frame
|
||||
* (Stage 0) -- that saves everything because an interrupt has no call
|
||||
* site. sk_vm_switch_to() is an ordinary `bl`, so AAPCS64 already makes
|
||||
* every caller-saved register (x0-x18, v0-v7, v16-v31) the caller's own
|
||||
* problem across the call. Only the callee-saved set needs explicit
|
||||
* handling: x19-x28, x29 (fp), x30 (lr), and d8-d15 (only the low 64
|
||||
* bits of v8-v15 are callee-saved per AAPCS64).
|
||||
*
|
||||
* void sk_vm_switch_to(uint64_t *save_sp_out, uint64_t new_sp);
|
||||
* x0 = save_sp_out, x1 = new_sp
|
||||
*
|
||||
* For a VM that has never run before, new_sp must point at a synthetic
|
||||
* 160-byte frame built by the C glue (switch.c) to this exact layout,
|
||||
* with x30 (lr) set to the trampoline entry address and everything else
|
||||
* zeroed, so the final `ret` below (which branches to lr) jumps there.
|
||||
*/
|
||||
|
||||
.section .text
|
||||
.balign 4
|
||||
|
||||
.equ SW_X19, 0
|
||||
.equ SW_X21, 16
|
||||
.equ SW_X23, 32
|
||||
.equ SW_X25, 48
|
||||
.equ SW_X27, 64
|
||||
.equ SW_X29, 80 /* x29 (fp), x30 (lr) */
|
||||
.equ SW_D8, 96
|
||||
.equ SW_D10, 112
|
||||
.equ SW_D12, 128
|
||||
.equ SW_D14, 144
|
||||
.equ SW_SIZE, 160
|
||||
|
||||
/* .hidden is an ELF-only visibility directive with no COFF equivalent;
|
||||
* this file is also assembled for the aarch64 loader, which clang targets
|
||||
* at aarch64-pc-windows-msvc (PE/COFF) -- guard on _WIN32 (defined only by
|
||||
* that target), matching isr.S's own precedent exactly. */
|
||||
#ifndef _WIN32
|
||||
.hidden sk_vm_switch_to
|
||||
#endif
|
||||
|
||||
.global sk_vm_switch_to
|
||||
sk_vm_switch_to:
|
||||
sub sp, sp, #SW_SIZE
|
||||
|
||||
stp x19, x20, [sp, #SW_X19]
|
||||
stp x21, x22, [sp, #SW_X21]
|
||||
stp x23, x24, [sp, #SW_X23]
|
||||
stp x25, x26, [sp, #SW_X25]
|
||||
stp x27, x28, [sp, #SW_X27]
|
||||
stp x29, x30, [sp, #SW_X29]
|
||||
stp d8, d9, [sp, #SW_D8]
|
||||
stp d10, d11, [sp, #SW_D10]
|
||||
stp d12, d13, [sp, #SW_D12]
|
||||
stp d14, d15, [sp, #SW_D14]
|
||||
|
||||
mov x2, sp
|
||||
str x2, [x0] /* *save_sp_out = sp (this context's parked SP) */
|
||||
|
||||
mov sp, x1 /* sp = new_sp (switch stacks) */
|
||||
|
||||
ldp x19, x20, [sp, #SW_X19]
|
||||
ldp x21, x22, [sp, #SW_X21]
|
||||
ldp x23, x24, [sp, #SW_X23]
|
||||
ldp x25, x26, [sp, #SW_X25]
|
||||
ldp x27, x28, [sp, #SW_X27]
|
||||
ldp x29, x30, [sp, #SW_X29]
|
||||
ldp d8, d9, [sp, #SW_D8]
|
||||
ldp d10, d11, [sp, #SW_D10]
|
||||
ldp d12, d13, [sp, #SW_D12]
|
||||
ldp d14, d15, [sp, #SW_D14]
|
||||
|
||||
add sp, sp, #SW_SIZE
|
||||
ret
|
||||
|
||||
/*
|
||||
* uint64_t sk_vm_switch_prime(uint64_t stack_top, void *entry);
|
||||
*
|
||||
* Builds the synthetic frame above at the top of a fresh native stack, so
|
||||
* the first ever sk_vm_switch_to() into it lands at `entry` (x30/lr) via
|
||||
* its final `ret`. Kept in this file (not C) so the layout can never
|
||||
* drift from sk_vm_switch_to() above -- one set of offsets, not two.
|
||||
*/
|
||||
#ifndef _WIN32
|
||||
.hidden sk_vm_switch_prime
|
||||
#endif
|
||||
|
||||
.global sk_vm_switch_prime
|
||||
sk_vm_switch_prime:
|
||||
sub x2, x0, #SW_SIZE
|
||||
stp xzr, xzr, [x2, #SW_X19]
|
||||
stp xzr, xzr, [x2, #SW_X21]
|
||||
stp xzr, xzr, [x2, #SW_X23]
|
||||
stp xzr, xzr, [x2, #SW_X25]
|
||||
stp xzr, xzr, [x2, #SW_X27]
|
||||
stp xzr, x1, [x2, #SW_X29] /* x29=0, x30(lr)=entry */
|
||||
stp xzr, xzr, [x2, #SW_D8]
|
||||
stp xzr, xzr, [x2, #SW_D10]
|
||||
stp xzr, xzr, [x2, #SW_D12]
|
||||
stp xzr, xzr, [x2, #SW_D14]
|
||||
mov x0, x2
|
||||
ret
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* switch.S (amd64) - Cooperative VM context switch primitive.
|
||||
*
|
||||
* FABRIC-3.md §XXVIII (Stage 2, preemptive-context-switch save/restore
|
||||
* primitive, 2026-09-13). This is deliberately NOT the same mechanism as
|
||||
* isr.S's interrupt trap frame (Stage 0) -- that saves every register
|
||||
* because an interrupt is not a call site the interrupted code knows
|
||||
* about. sk_vm_switch_to() IS an ordinary function call (SWITCH-TO's C
|
||||
* handler calls it directly), so the standard System V AMD64 ABI already
|
||||
* guarantees every caller-saved register (rax, rcx, rdx, rsi, rdi, r8-r11,
|
||||
* all XMM) is the caller's own problem to preserve across the call, exactly
|
||||
* like any other call. Only the callee-saved set -- rbx, rbp, r12-r15 --
|
||||
* needs explicit handling here, because this function does not really
|
||||
* "return" in the normal sense: it swaps to a completely different stack
|
||||
* and (from the newly-resumed context's point of view) appears to return
|
||||
* from whichever earlier call to this same function originally parked it.
|
||||
* SysV has no callee-saved XMM registers at all, so there is no FP state
|
||||
* to carry here.
|
||||
*
|
||||
* void sk_vm_switch_to(uint64_t *save_sp_out, uint64_t new_sp);
|
||||
* rdi = save_sp_out, rsi = new_sp
|
||||
*
|
||||
* For a VM that has never run before, new_sp must point at a synthetic
|
||||
* frame built to this exact layout by the C glue (switch.c): six zeroed
|
||||
* quadwords (r15, r14, r13, r12, rbx, rbp -- values irrelevant, never a
|
||||
* real prior context) followed by the trampoline entry address at
|
||||
* new_sp+48, so the final `ret` below jumps there instead of into some
|
||||
* prior caller.
|
||||
*/
|
||||
|
||||
.intel_syntax noprefix
|
||||
.section .text
|
||||
|
||||
.hidden sk_vm_switch_to
|
||||
|
||||
.global sk_vm_switch_to
|
||||
sk_vm_switch_to:
|
||||
push rbp
|
||||
push rbx
|
||||
push r12
|
||||
push r13
|
||||
push r14
|
||||
push r15
|
||||
|
||||
mov [rdi], rsp /* *save_sp_out = rsp (this context's parked SP) */
|
||||
mov rsp, rsi /* rsp = new_sp (switch stacks) */
|
||||
|
||||
pop r15
|
||||
pop r14
|
||||
pop r13
|
||||
pop r12
|
||||
pop rbx
|
||||
pop rbp
|
||||
ret
|
||||
|
||||
/*
|
||||
* uint64_t sk_vm_switch_prime(uint64_t stack_top, void *entry);
|
||||
*
|
||||
* Builds the synthetic frame described above at the top of a fresh native
|
||||
* stack, so the first ever sk_vm_switch_to() into it lands at `entry`
|
||||
* instead of some real prior caller. Returns the resulting sp value to
|
||||
* pass as sk_vm_switch_to()'s new_sp. Kept in this file (not C) so the
|
||||
* layout can never drift from what sk_vm_switch_to() above actually
|
||||
* expects -- one set of offsets, not two copies to keep in sync.
|
||||
*/
|
||||
.hidden sk_vm_switch_prime
|
||||
|
||||
.global sk_vm_switch_prime
|
||||
sk_vm_switch_prime:
|
||||
mov rax, rdi
|
||||
sub rax, 56
|
||||
mov qword ptr [rax+0], 0 /* r15 */
|
||||
mov qword ptr [rax+8], 0 /* r14 */
|
||||
mov qword ptr [rax+16], 0 /* r13 */
|
||||
mov qword ptr [rax+24], 0 /* r12 */
|
||||
mov qword ptr [rax+32], 0 /* rbx */
|
||||
mov qword ptr [rax+40], 0 /* rbp */
|
||||
mov [rax+48], rsi /* return address = entry */
|
||||
ret
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* switch.S (riscv64) - Cooperative VM context switch primitive.
|
||||
*
|
||||
* FABRIC-3.md §XXVIII (Stage 2, preemptive-context-switch save/restore
|
||||
* primitive, 2026-09-13). Deliberately NOT isr.S's interrupt trap frame
|
||||
* (Stage 0) -- that saves everything because an async trap has no call
|
||||
* site. sk_vm_switch_to() is an ordinary `call`, so the LP64D psABI
|
||||
* already makes every caller-saved register (ra excepted -- see below --
|
||||
* t0-t6, a0-a7, ft0-ft11, fa0-fa7) the caller's own problem across the
|
||||
* call. Only the callee-saved set needs explicit handling: s0-s11,
|
||||
* fs0-fs11, and ra itself (ra is not "callee-saved" in the usual sense,
|
||||
* but this function does not really return to its own caller -- it swaps
|
||||
* stacks and later resumes as if returning from whichever earlier call
|
||||
* parked this context, so ra must be captured explicitly here rather than
|
||||
* left to a normal prologue/epilogue).
|
||||
*
|
||||
* The FP half is saved only when sstatus.FS != Off -- same reasoning and
|
||||
* bit test as isr.S's trap frame, checked once per call and reused for
|
||||
* both the save and restore decision (nothing changes FS in between; it
|
||||
* is genuine single global CPU state, not part of what's "switched," so
|
||||
* this function reads it but never writes it).
|
||||
*
|
||||
* void sk_vm_switch_to(uint64_t *save_sp_out, uint64_t new_sp);
|
||||
* a0 = save_sp_out, a1 = new_sp
|
||||
*
|
||||
* For a VM that has never run before, new_sp must point at a synthetic
|
||||
* 208-byte frame built by the C glue (switch.c) with ra set to the
|
||||
* trampoline entry address and everything else zeroed, so the final
|
||||
* `ret` below (jalr via ra) jumps there.
|
||||
*/
|
||||
|
||||
.section .text
|
||||
.balign 4
|
||||
|
||||
.equ SW_S0, 0
|
||||
.equ SW_RA, 96
|
||||
.equ SW_FS0, 104
|
||||
.equ SW_SIZE, 208
|
||||
|
||||
.equ SSTATUS_FS_SHIFT, 13
|
||||
.equ SSTATUS_FS_MASK, 3
|
||||
|
||||
.hidden sk_vm_switch_to
|
||||
|
||||
.global sk_vm_switch_to
|
||||
sk_vm_switch_to:
|
||||
addi sp, sp, -SW_SIZE
|
||||
|
||||
sd s0, SW_S0+0(sp)
|
||||
sd s1, SW_S0+8(sp)
|
||||
sd s2, SW_S0+16(sp)
|
||||
sd s3, SW_S0+24(sp)
|
||||
sd s4, SW_S0+32(sp)
|
||||
sd s5, SW_S0+40(sp)
|
||||
sd s6, SW_S0+48(sp)
|
||||
sd s7, SW_S0+56(sp)
|
||||
sd s8, SW_S0+64(sp)
|
||||
sd s9, SW_S0+72(sp)
|
||||
sd s10, SW_S0+80(sp)
|
||||
sd s11, SW_S0+88(sp)
|
||||
sd ra, SW_RA(sp)
|
||||
|
||||
csrr t0, sstatus
|
||||
srli t1, t0, SSTATUS_FS_SHIFT
|
||||
andi t1, t1, SSTATUS_FS_MASK
|
||||
beqz t1, 1f
|
||||
|
||||
fsd fs0, SW_FS0+0(sp)
|
||||
fsd fs1, SW_FS0+8(sp)
|
||||
fsd fs2, SW_FS0+16(sp)
|
||||
fsd fs3, SW_FS0+24(sp)
|
||||
fsd fs4, SW_FS0+32(sp)
|
||||
fsd fs5, SW_FS0+40(sp)
|
||||
fsd fs6, SW_FS0+48(sp)
|
||||
fsd fs7, SW_FS0+56(sp)
|
||||
fsd fs8, SW_FS0+64(sp)
|
||||
fsd fs9, SW_FS0+72(sp)
|
||||
fsd fs10, SW_FS0+80(sp)
|
||||
fsd fs11, SW_FS0+88(sp)
|
||||
1:
|
||||
sd sp, 0(a0) /* *save_sp_out = sp (this context's parked SP) */
|
||||
mv sp, a1 /* sp = new_sp (switch stacks) */
|
||||
|
||||
ld s0, SW_S0+0(sp)
|
||||
ld s1, SW_S0+8(sp)
|
||||
ld s2, SW_S0+16(sp)
|
||||
ld s3, SW_S0+24(sp)
|
||||
ld s4, SW_S0+32(sp)
|
||||
ld s5, SW_S0+40(sp)
|
||||
ld s6, SW_S0+48(sp)
|
||||
ld s7, SW_S0+56(sp)
|
||||
ld s8, SW_S0+64(sp)
|
||||
ld s9, SW_S0+72(sp)
|
||||
ld s10, SW_S0+80(sp)
|
||||
ld s11, SW_S0+88(sp)
|
||||
ld ra, SW_RA(sp)
|
||||
|
||||
beqz t1, 2f
|
||||
|
||||
fld fs0, SW_FS0+0(sp)
|
||||
fld fs1, SW_FS0+8(sp)
|
||||
fld fs2, SW_FS0+16(sp)
|
||||
fld fs3, SW_FS0+24(sp)
|
||||
fld fs4, SW_FS0+32(sp)
|
||||
fld fs5, SW_FS0+40(sp)
|
||||
fld fs6, SW_FS0+48(sp)
|
||||
fld fs7, SW_FS0+56(sp)
|
||||
fld fs8, SW_FS0+64(sp)
|
||||
fld fs9, SW_FS0+72(sp)
|
||||
fld fs10, SW_FS0+80(sp)
|
||||
fld fs11, SW_FS0+88(sp)
|
||||
2:
|
||||
addi sp, sp, SW_SIZE
|
||||
ret
|
||||
|
||||
/*
|
||||
* uint64_t sk_vm_switch_prime(uint64_t stack_top, void *entry);
|
||||
*
|
||||
* Builds the synthetic frame above at the top of a fresh native stack, so
|
||||
* the first ever sk_vm_switch_to() into it lands at `entry` (ra) via its
|
||||
* final `ret`. Kept in this file (not C) so the layout can never drift
|
||||
* from sk_vm_switch_to() above -- one set of offsets, not two. FP slots
|
||||
* are zeroed via plain integer stores (sd, not fsd) -- this is just
|
||||
* writing zero bytes to memory, not an FP computation, so it needs no
|
||||
* FS-enabled check at all, unlike every other f-register touch in this
|
||||
* file.
|
||||
*/
|
||||
.hidden sk_vm_switch_prime
|
||||
|
||||
.global sk_vm_switch_prime
|
||||
sk_vm_switch_prime:
|
||||
addi t0, a0, -SW_SIZE
|
||||
|
||||
sd zero, SW_S0+0(t0)
|
||||
sd zero, SW_S0+8(t0)
|
||||
sd zero, SW_S0+16(t0)
|
||||
sd zero, SW_S0+24(t0)
|
||||
sd zero, SW_S0+32(t0)
|
||||
sd zero, SW_S0+40(t0)
|
||||
sd zero, SW_S0+48(t0)
|
||||
sd zero, SW_S0+56(t0)
|
||||
sd zero, SW_S0+64(t0)
|
||||
sd zero, SW_S0+72(t0)
|
||||
sd zero, SW_S0+80(t0)
|
||||
sd zero, SW_S0+88(t0)
|
||||
sd a1, SW_RA(t0)
|
||||
|
||||
sd zero, SW_FS0+0(t0)
|
||||
sd zero, SW_FS0+8(t0)
|
||||
sd zero, SW_FS0+16(t0)
|
||||
sd zero, SW_FS0+24(t0)
|
||||
sd zero, SW_FS0+32(t0)
|
||||
sd zero, SW_FS0+40(t0)
|
||||
sd zero, SW_FS0+48(t0)
|
||||
sd zero, SW_FS0+56(t0)
|
||||
sd zero, SW_FS0+64(t0)
|
||||
sd zero, SW_FS0+72(t0)
|
||||
sd zero, SW_FS0+80(t0)
|
||||
sd zero, SW_FS0+88(t0)
|
||||
|
||||
mv a0, t0
|
||||
ret
|
||||
@@ -392,6 +392,19 @@ int capsule_vm_kill(const char *name) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* FABRIC-3.md §XXVIII, Stage 2 (2026-09-13): a switched-out VM has a
|
||||
* live saved context parked on its own native stack -- freeing vm_ptr
|
||||
* here would leave that saved SP pointing into freed memory, a real
|
||||
* use-after-free the moment anything ever tried to resume it. Refuse
|
||||
* rather than silently corrupt; the caller can retry once the VM has
|
||||
* been switched back in and reaches a normal LIVE state. */
|
||||
if (entry->state == VM_STATE_SWITCHED_OUT) {
|
||||
console_puts("KILL: ");
|
||||
console_puts(name);
|
||||
console_println(" refused -- switched-out (context parked)");
|
||||
return -1;
|
||||
}
|
||||
|
||||
vm_id = entry->vm_id;
|
||||
vm = (VM *)entry->vm_ptr;
|
||||
|
||||
@@ -440,7 +453,17 @@ void capsule_vm_kill_all_nonmama(void) {
|
||||
(void)stadium_evict(node->entry.stadium_patron_cell);
|
||||
node->entry.stadium_patron_cell = STADIUM_CELL_NONE;
|
||||
}
|
||||
if (vm) {
|
||||
/* FABRIC-3.md §XXVIII, Stage 2 (2026-09-13): a switched-out VM has
|
||||
* a live saved context parked on its own native stack -- skip the
|
||||
* free (leaked, not corrupted) rather than invalidate memory a
|
||||
* parked SP still points into. Harmless here specifically: every
|
||||
* caller of this function (BYE/cold-restart) calls
|
||||
* arch_cold_reset() immediately afterward, which wipes all memory
|
||||
* regardless of what this function left allocated. */
|
||||
if (vm && node->entry.state == VM_STATE_SWITCHED_OUT) {
|
||||
log_message(LOG_WARN, "capsule_vm_kill_all_nonmama: leaving %s allocated (switched-out)",
|
||||
node->entry.name);
|
||||
} else if (vm) {
|
||||
vm->halted = 1;
|
||||
vm_cleanup(vm);
|
||||
sf_free(vm);
|
||||
|
||||
@@ -586,6 +586,15 @@ void mama_word_kill(VM *vm)
|
||||
* before capsule_vm_kill()'s own Hera guard below ever runs. */
|
||||
{
|
||||
VMRegistryEntry entry;
|
||||
/* FABRIC-3.md §XXVIII, Stage 2 (2026-09-13): mirror the refusal
|
||||
* capsule_vm_kill() itself now enforces -- checked here too so the
|
||||
* heat-fanout/active-VM cleanup below never runs against a VM
|
||||
* that's about to be refused anyway. */
|
||||
if (capsule_vm_find_by_name_nocase(name_buf, &entry) == 0 &&
|
||||
entry.state == VM_STATE_SWITCHED_OUT) {
|
||||
console_println("KILL: refused -- switched-out (context parked)");
|
||||
return;
|
||||
}
|
||||
if (capsule_vm_find_by_name_nocase(name_buf, &entry) == 0 &&
|
||||
entry.state == VM_STATE_LIVE) {
|
||||
if (!vm_uuid_is_hera(entry.vm_id)) {
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
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 <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 globals, 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) -- set immediately before a
|
||||
* first-ever switch into a VM, read exactly once, synchronously, at the
|
||||
* very top of sk_vm_switch_entry() before any other switch could occur.
|
||||
* Preemption (Stage 3) will need to revisit this; flagged there, not here. */
|
||||
static VM *g_switch_entry_vm;
|
||||
static VM *g_switch_back_to;
|
||||
|
||||
/**
|
||||
* sk_vm_switch_entry - trampoline for a VM's first-ever entry.
|
||||
*
|
||||
* 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
|
||||
* switched to it, forever. Must never fall off the end -- there is no
|
||||
* legitimate return address below a synthetic first-entry frame, unlike
|
||||
* an ordinary function.
|
||||
*/
|
||||
void sk_vm_switch_entry(void) {
|
||||
VM *self = g_switch_entry_vm;
|
||||
VM *back = g_switch_back_to;
|
||||
for (;;) {
|
||||
sk_vm_context_switch(self, back);
|
||||
}
|
||||
}
|
||||
|
||||
int sk_vm_context_switch(VM *from, VM *to) {
|
||||
uint64_t new_sp;
|
||||
|
||||
if (!from || !to || to->native_stack_top == 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (to->native_stack_saved_sp == 0) {
|
||||
/* First-ever entry: synthesize the initial frame. */
|
||||
g_switch_entry_vm = to;
|
||||
g_switch_back_to = from;
|
||||
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, VM_STATE_SWITCHED_OUT);
|
||||
|
||||
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);
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif /* __STARKERNEL__ */
|
||||
Reference in New Issue
Block a user