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