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

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
+26
View File
@@ -3553,3 +3553,29 @@ No FORTH-visible behavior changed, as intended -- this stage was purely "make th
complete," nothing yet reads or writes any of it beyond the ISR's own entry/exit. Stage 1 (per-VM complete," nothing yet reads or writes any of it beyond the ISR's own entry/exit. Stage 1 (per-VM
native stacks) is next. native stacks) is next.
**Stage 1 CLOSED, same day.** Every VM now gets its own dedicated 2 MiB native (C) stack at
birth, allocated but not yet executed on. Design correction made before writing any code: the
plan's own text said to clone `sk_vm_arena_alloc()`'s guard-page pattern, but investigation
showed that pattern is actually Mama-only -- `host_services.c`'s `kernel_alloc()` gives every
baby VM a plain `kmalloc()` block for its 5 MB dictionary arena, not a real guarded PMM
allocation; only Hera gets the true singleton. Stacks are different: a stack overflow is
precisely the failure mode guard pages exist for, and a corrupted stack can corrupt whatever
saved context Stage 2 later trusts, so the new `sk_vm_native_stack_alloc()`/`_free()`
(`arena.c`) gives **every** VM without exception a real, independent `pmm_alloc_contiguous()`
allocation with guard pages -- no singleton, no kmalloc fallback. Size (2 MiB) is not a guess:
it matches this project's own established kernel-stack convention (`g_kernel_stack`/
`g_rpi5_native_stack` in the arch entry files), and that single 2 MiB stack today already
carries the entire shared kernel C stack's worth of nested VM-EXEC/`execute_colon_word`
recursion across every VM combined -- generous headroom for one VM alone.
Wired at both VM-creation sites: `sk_vm_bootstrap_parity()` (Hera) and `capsule_birth_baby()`
(every other VM, WIREBIND-birthed identities included -- purely additive there, no attach/detach
behavior change). Three new fields on `VM` (`native_stack_paddr`/`native_stack_guard_vaddr`/
`native_stack_top`), freed in `vm_cleanup()` alongside the existing `call_stack` free.
Allocation failure is deliberately non-fatal to birth (degrades to "not yet switchable," not
"stillborn," since nothing executes on these stacks yet regardless).
All 3 architectures re-verified clean boot to `ok>`, no native-stack allocation failures logged
for any Tripod-fleet VM on any arch. Stage 2 (the actual save/restore switch primitive,
cooperative only, no timer) is next.
+1 -1
View File
@@ -1,5 +1,5 @@
# Capsule Block Manifest — Auto-generated # Capsule Block Manifest — Auto-generated
<!-- Generated by mkcapsule --manifest 2026-09-13T13:21:10Z --> <!-- Generated by mkcapsule --manifest 2026-09-13T18:28:32Z -->
<!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. --> <!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. -->
<!-- Hand-written justifications and immutability notes live --> <!-- Hand-written justifications and immutability notes live -->
<!-- in MANIFEST.md alongside this auto-generated index. --> <!-- in MANIFEST.md alongside this auto-generated index. -->
BIN
View File
Binary file not shown.
+29
View File
@@ -62,6 +62,35 @@ size_t sk_vm_arena_size(void);
int sk_vm_arena_is_initialized(void); int sk_vm_arena_is_initialized(void);
void sk_vm_arena_assert_guards(const char *tag); void sk_vm_arena_assert_guards(const char *tag);
/**
* sk_vm_native_stack_t - one VM's own dedicated native (C) stack.
*
* FABRIC-3.md §XXVIII (Stage 1, preemptive-context-switch per-VM native
* stacks, 2026-09-13). Unlike sk_vm_arena_alloc() -- whose PMM+guard-page
* path is exercised only for Hera; every baby VM's "arena" is actually a
* plain kmalloc block, per host_services.c's kernel_alloc() -- every native
* stack, for every VM without exception, gets its own real, independent
* pmm_alloc_contiguous() allocation with guard pages. A stack overflow is
* exactly the failure mode guard pages exist for, and unlike the dictionary
* arena, a corrupted stack can also corrupt whatever saved context Stage 2
* later trusts -- worth the extra PMM pages every VM, not just Hera.
*
* Caller owns this struct (stored directly on the VM, mirroring how
* vm->memory already holds its own arena pointer directly rather than going
* through any module-level registry) and passes it back unchanged to
* sk_vm_native_stack_free().
*/
typedef struct {
uint64_t paddr; /* physical base (for pmm_free_contiguous) */
uint64_t guard_vaddr; /* virtual base of the whole guarded region */
uint64_t stack_top; /* initial SP value -- stacks grow down on all
* 3 arches, so this is guard_vaddr + one guard
* page + the full stack size */
} sk_vm_native_stack_t;
int sk_vm_native_stack_alloc(sk_vm_native_stack_t *out);
void sk_vm_native_stack_free(const sk_vm_native_stack_t *stack);
#endif /* __STARKERNEL__ */ #endif /* __STARKERNEL__ */
#endif /* STARKERNEL_VM_ARENA_H */ #endif /* STARKERNEL_VM_ARENA_H */
+10
View File
@@ -645,6 +645,16 @@ typedef struct VM
int call_stack_max; /**< High-water mark depth (DoE metric) */ int call_stack_max; /**< High-water mark depth (DoE metric) */
/** @} */ /** @} */
/** @name Native execution stack (FABRIC-3.md §XXVIII, Stage 1, 2026-09-13)
* @{
*/
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). */
/** @} */
/** @name Stadium Identity (item 4.2, FABRIC-0.md §25.5) /** @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
+21
View File
@@ -34,6 +34,7 @@
#include "starkernel/kmalloc.h" #include "starkernel/kmalloc.h"
#include "starkernel/console.h" #include "starkernel/console.h"
#include "starkernel/vm/stadium.h" /* item 4.1a -- stadium_grant_quota() */ #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/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 "starkernel/timer.h" /* timer_calibration_record()->vm_mode -- FABRIC-2.md §I.5 CONTRIB trust tier */
#include "vm.h" #include "vm.h"
@@ -627,6 +628,26 @@ CapsuleRunResult capsule_birth_baby(
* own reservoir, not vm_uuid_hera()'s (item 4.1's hardcoded default). */ * own reservoir, not vm_uuid_hera()'s (item 4.1's hardcoded default). */
((VM *)new_vm)->stadium_vm_id = vm_id; ((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 /* 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 * 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 * 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; 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) { void sk_vm_arena_assert_guards(const char *tag) {
if (!vm_arena_initialized) { if (!vm_arena_initialized) {
return; return;
@@ -59,6 +59,7 @@
#include "starkernel/capsule_birth.h" #include "starkernel/capsule_birth.h"
#include "starkernel/capsule_run.h" #include "starkernel/capsule_run.h"
#include "starkernel/capsule_vm_physics.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 "vm_internal.h"
#include "platform_time.h" #include "platform_time.h"
#include "test_runner/include/test_runner.h" #include "test_runner/include/test_runner.h"
@@ -266,6 +267,21 @@ int sk_vm_bootstrap_parity(ParityPacket *out) {
return -1; 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. /* Capsule subsystem: hooks, registry (Hera), run log.
* Called exactly once here for Mama — child VMs go through * Called exactly once here for Mama — child VMs go through
* capsule_birth_baby() which never calls vm_init_with_host(). */ * capsule_birth_baby() which never calls vm_init_with_host(). */
+17
View File
@@ -364,6 +364,23 @@ void vm_cleanup(VM* vm)
#ifdef __STARKERNEL__ #ifdef __STARKERNEL__
sf_free(vm->call_stack); sf_free(vm->call_stack);
vm->call_stack = NULL; 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 #endif
} }