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
|
||||
Reference in New Issue
Block a user