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
@@ -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