Stage 1: per-VM native stacks, allocated but not yet executed on (FABRIC-3.md §XXVIII)
Build / build-amd64-iso (push) Canceled after 0s
Build / build-aarch64-iso (push) Canceled after 0s
Build / build-riscv64-img (push) Canceled after 0s

Second stage of the preemptive context-switching plan. Every VM (Hera,
every capsule_birth_baby()-born VM including WIREBIND identities) now
gets its own dedicated 2 MiB native C stack at birth -- but nothing runs
on it yet, that's Stage 2. Pure allocation-machinery proof.

Design correction made before writing code: the plan called for cloning
sk_vm_arena_alloc()'s guard-page pattern, but that pattern turns out to
be Mama-only -- host_services.c's kernel_alloc() gives every baby VM a
plain kmalloc() block for its dictionary arena, not a real guarded PMM
allocation. Stacks get the real treatment instead (new
sk_vm_native_stack_alloc()/_free() in arena.c): independent
pmm_alloc_contiguous() + guard pages for every VM without exception, no
singleton, no kmalloc fallback -- a stack overflow is exactly the
failure mode guard pages exist for, and a corrupted stack could corrupt
whatever saved context Stage 2 trusts.

2 MiB size matches this project's own established kernel-stack
convention (g_kernel_stack/g_rpi5_native_stack), not a guess -- that one
shared 2 MiB stack today already carries all VMs' combined nested
VM-EXEC recursion.

Three new VM struct fields, freed in vm_cleanup() alongside the existing
call_stack free. Allocation failure is non-fatal to birth.

All 3 architectures re-verified clean boot to ok>, no native-stack
allocation failures for any Tripod-fleet VM.

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 14:30:46 -04:00
co-authored by Claude Sonnet 5
parent 15672ce17c
commit 57ac3fc304
15 changed files with 27337 additions and 1 deletions
+21
View File
@@ -34,6 +34,7 @@
#include "starkernel/kmalloc.h"
#include "starkernel/console.h"
#include "starkernel/vm/stadium.h" /* item 4.1a -- stadium_grant_quota() */
#include "starkernel/vm/arena.h" /* FABRIC-3.md §XXVIII Stage 1 -- sk_vm_native_stack_alloc() */
#include "starkernel/session.h" /* session_register()/session_set_pinned() -- FABRIC-2.md §H.12 step 5 */
#include "starkernel/timer.h" /* timer_calibration_record()->vm_mode -- FABRIC-2.md §I.5 CONTRIB trust tier */
#include "vm.h"
@@ -627,6 +628,26 @@ CapsuleRunResult capsule_birth_baby(
* own reservoir, not vm_uuid_hera()'s (item 4.1's hardcoded default). */
((VM *)new_vm)->stadium_vm_id = vm_id;
/* FABRIC-3.md §XXVIII, Stage 1 (2026-09-13): every VM gets its own
* native stack from the moment it exists, including WIREBIND-birthed
* identity VMs -- purely additive here, nothing executes on it yet
* (that's Stage 2). Allocation failure is not fatal to birth itself:
* a VM with no native stack yet is exactly as functional today as
* every VM has always been (nothing currently runs on a dedicated
* per-VM stack), so this degrades to "not yet switchable," not
* "stillborn." native_stack_top stays 0, the pre-Stage-2 default. */
{
sk_vm_native_stack_t ns;
if (sk_vm_native_stack_alloc(&ns) == 0) {
((VM *)new_vm)->native_stack_paddr = ns.paddr;
((VM *)new_vm)->native_stack_guard_vaddr = ns.guard_vaddr;
((VM *)new_vm)->native_stack_top = ns.stack_top;
} else {
log_message(LOG_WARN, "capsule_birth_baby: native stack allocation failed for %s",
capsule_name);
}
}
/* item 4.6 fix (FABRIC-1.md, 2026-08-18): granted here, before IDENTITY
* exec, not after a confirmed live birth as item 4.1a originally placed
* it. item 4.1a's placement assumed no VM's own IDENTITY code would ever
+86
View File
@@ -265,6 +265,92 @@ int sk_vm_arena_is_initialized(void) {
return vm_arena_initialized;
}
/* ===========================================================================
* Per-VM native stack (FABRIC-3.md §XXVIII, Stage 1, 2026-09-13)
* ===========================================================================
*
* 2 MiB, matching this project's own established kernel-stack convention
* (g_kernel_stack / g_rpi5_native_stack in each arch's entry assembly) --
* not a guessed number. That single 2 MiB stack today accommodates the
* ENTIRE shared kernel C stack's worth of nested VM-EXEC/execute_colon_word
* recursion across every VM combined, so 2 MiB for one VM's own stack alone
* is generous headroom, not a guess at the edge of what's needed.
*/
#define SK_VM_STACK_SIZE (2ULL * 1024ULL * 1024ULL)
#define SK_VM_STACK_PAGES ((SK_VM_STACK_SIZE + (2ULL * SK_VM_GUARD_SIZE) + PMM_PAGE_SIZE - 1) / PMM_PAGE_SIZE)
#if !(defined(ARCH_AARCH64) || defined(__riscv))
/* amd64 needs its own virtual-address walk, separate from arena's
* next_arena_vaddr -- different logical region, would otherwise collide. */
#define SK_VM_STACK_ARENA_VADDR 0xFFFF980000000000ULL
static uint64_t next_stack_vaddr = SK_VM_STACK_ARENA_VADDR;
#endif
/**
* sk_vm_native_stack_alloc - allocate one VM's own dedicated native stack.
*
* Always a fresh, independent PMM allocation with real guard pages -- no
* singleton, no kmalloc fallback, unlike sk_vm_arena_alloc()'s Mama-only
* special case. Returns 0 on success (fields of *out populated), -1 on
* failure.
*/
int sk_vm_native_stack_alloc(sk_vm_native_stack_t *out) {
uint64_t paddr, guard_vaddr;
if (!out) return -1;
paddr = pmm_alloc_contiguous(SK_VM_STACK_PAGES);
if (paddr == 0) {
console_println("sk_vm_native_stack_alloc: PMM allocation failed");
return -1;
}
#if defined(ARCH_AARCH64) || defined(__riscv)
guard_vaddr = paddr;
(void)(VMM_FLAG_WRITABLE); (void)(VMM_FLAG_NX);
#else
guard_vaddr = next_stack_vaddr;
next_stack_vaddr += SK_VM_STACK_PAGES * PMM_PAGE_SIZE;
uint64_t flags = VMM_FLAG_WRITABLE | VMM_FLAG_NX;
if (vmm_map_range(guard_vaddr, paddr,
SK_VM_STACK_PAGES * PMM_PAGE_SIZE, flags) != 0) {
console_println("sk_vm_native_stack_alloc: VMM mapping failed");
pmm_free_contiguous(paddr, SK_VM_STACK_PAGES);
next_stack_vaddr -= SK_VM_STACK_PAGES * PMM_PAGE_SIZE;
return -1;
}
#endif
guard_fill(guard_vaddr);
guard_fill(guard_vaddr + SK_VM_GUARD_SIZE + SK_VM_STACK_SIZE);
out->paddr = paddr;
out->guard_vaddr = guard_vaddr;
out->stack_top = guard_vaddr + SK_VM_GUARD_SIZE + SK_VM_STACK_SIZE;
return 0;
}
/**
* sk_vm_native_stack_free - release a VM's native stack.
*
* Mirrors sk_vm_arena_free()'s unmap/free sequence, but per-call (this is
* not a singleton) -- safe to call once per successful
* sk_vm_native_stack_alloc(), and the caller (vm_cleanup()) is responsible
* for not calling it twice on the same struct.
*/
void sk_vm_native_stack_free(const sk_vm_native_stack_t *stack) {
if (!stack || stack->paddr == 0) return;
#if !(defined(ARCH_AARCH64) || defined(__riscv))
for (uint64_t offset = 0; offset < SK_VM_STACK_PAGES * PMM_PAGE_SIZE; offset += PMM_PAGE_SIZE) {
vmm_unmap_page(stack->guard_vaddr + offset);
}
#endif
pmm_free_contiguous(stack->paddr, SK_VM_STACK_PAGES);
}
void sk_vm_arena_assert_guards(const char *tag) {
if (!vm_arena_initialized) {
return;
@@ -59,6 +59,7 @@
#include "starkernel/capsule_birth.h"
#include "starkernel/capsule_run.h"
#include "starkernel/capsule_vm_physics.h"
#include "starkernel/vm/arena.h" /* FABRIC-3.md §XXVIII Stage 1 -- sk_vm_native_stack_alloc() */
#include "vm_internal.h"
#include "platform_time.h"
#include "test_runner/include/test_runner.h"
@@ -266,6 +267,21 @@ int sk_vm_bootstrap_parity(ParityPacket *out) {
return -1;
}
/* FABRIC-3.md §XXVIII, Stage 1 (2026-09-13): Hera gets her own native
* stack too, same as every baby/WIREBIND VM (capsule_birth_baby()).
* Not fatal on failure, same reasoning as there -- nothing executes on
* this yet (Stage 2). */
{
sk_vm_native_stack_t ns;
if (sk_vm_native_stack_alloc(&ns) == 0) {
vm->native_stack_paddr = ns.paddr;
vm->native_stack_guard_vaddr = ns.guard_vaddr;
vm->native_stack_top = ns.stack_top;
} else {
console_println("VM: Hera native stack allocation failed");
}
}
/* Capsule subsystem: hooks, registry (Hera), run log.
* Called exactly once here for Mama — child VMs go through
* capsule_birth_baby() which never calls vm_init_with_host(). */
+17
View File
@@ -364,6 +364,23 @@ void vm_cleanup(VM* vm)
#ifdef __STARKERNEL__
sf_free(vm->call_stack);
vm->call_stack = NULL;
/* FABRIC-3.md §XXVIII, Stage 1 (2026-09-13): free the native stack
* allocated at birth (capsule_birth_baby() / sk_vm_bootstrap_parity()).
* Nothing executes on it yet (that's Stage 2), so there is no
* switched-out-state guard to check here yet -- Stage 2 adds that
* guard at the KILL call sites, before this function is ever reached
* for a VM that would need it. */
if (vm->native_stack_paddr != 0) {
sk_vm_native_stack_t ns;
ns.paddr = vm->native_stack_paddr;
ns.guard_vaddr = vm->native_stack_guard_vaddr;
ns.stack_top = vm->native_stack_top;
sk_vm_native_stack_free(&ns);
vm->native_stack_paddr = 0;
vm->native_stack_guard_vaddr = 0;
vm->native_stack_top = 0;
}
#endif
}