Stage 3 follow-on: fix stack-ownership corruption + DoE switch columns (FABRIC-3.md §XXVIII.1)
DoE CSV gained 6 switch-signal columns (switch_count_cumulative, switch_current_slot, switch_*_readiness, switch_ticks_since), and verifying them with a boot-time HB-ON probe surfaced a real livelock: the preemption checkpoint could fire inside a VM-EXEC-nested execute_colon_word() call and switch away from a stack it didn't own, parking a borrowed region of the caller's stack under the wrong VM's saved-context pointer. The trampoline bounce was the visible (safe) half of this; the corruption was the quiet half, live in every prior "clean" Stage 3 boot without ever showing up in the log. Fixed by gating the checkpoint on being at the outermost vm_interpret() call (g_vm_interpret_depth / sk_vm_at_outermost_interpret(), vm_core.c), per Bob's decision. Also fixed two related bugs found in the same pass: g_switch_back_to was a single global stale after first entry, now per-VM state (native_switch_back_to); note_switch_performed() fired on resume instead of switch-out, now called before the switch. Verified on all 3 architectures: steady log growth (no freeze), zero leaked QEMU processes, DoE columns internally consistent, Hermes/Artemis confirmed genuinely executing (not just trampoline-bouncing). Temporary HB-ON boot probe reverted after capture. 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
986d042aa7
commit
862d7d9c48
+22
-13
@@ -39,14 +39,17 @@
|
||||
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. */
|
||||
/* Single-writer-mainline global, 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) -- identifies which VM a freshly-primed
|
||||
* native stack belongs to. Read exactly once, synchronously, at the very
|
||||
* top of sk_vm_switch_entry() before any other first-entry could occur.
|
||||
* Safe to keep as a plain global (unlike the old g_switch_back_to this
|
||||
* file used to also carry -- see native_switch_back_to's own doc comment
|
||||
* in vm.h for why that one had to move to per-VM state): a VM's identity
|
||||
* never changes across the lifetime of its own trampoline instance, only
|
||||
* who most recently switched into it does. */
|
||||
static VM *g_switch_entry_vm;
|
||||
static VM *g_switch_back_to;
|
||||
|
||||
/**
|
||||
* sk_vm_switch_entry - trampoline for a VM's first-ever entry.
|
||||
@@ -55,15 +58,17 @@ static VM *g_switch_back_to;
|
||||
* 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.
|
||||
* most recently switched to it, forever. Reads self->native_switch_back_to
|
||||
* fresh every iteration (not a value captured once) so a later switch-in
|
||||
* from a different VM is honored, not silently bounced to whoever entered
|
||||
* first. 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);
|
||||
sk_vm_context_switch(self, self->native_switch_back_to);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,10 +79,14 @@ int sk_vm_context_switch(VM *from, VM *to) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Recorded unconditionally (first-ever entry or a resume) so whoever
|
||||
* is parked at `to`'s native stack -- most often this placeholder
|
||||
* trampoline -- always knows who most recently switched into it. */
|
||||
to->native_switch_back_to = from;
|
||||
|
||||
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;
|
||||
|
||||
@@ -63,6 +63,12 @@
|
||||
#include "starkernel/capsule_vm_switch_signal.h" /* FABRIC-3.md §XXVIII Stage 3 */
|
||||
#include "starkernel/vm/switch.h" /* FABRIC-3.md §XXVIII Stage 2 -- sk_vm_context_switch() */
|
||||
#include "starkernel/capsule_birth.h" /* capsule_vm_registry_get() */
|
||||
|
||||
/* FABRIC-3.md §XXVIII Stage 3 correction (2026-09-13): forward-declared
|
||||
* here because execute_colon_word() (uses it) appears earlier in this file
|
||||
* than g_vm_interpret_depth's definition (kept next to g_log_attrib_vm,
|
||||
* near vm_interpret() -- see that doc comment for the full rationale). */
|
||||
int sk_vm_at_outermost_interpret(void);
|
||||
#endif
|
||||
#include "word_source/include/vocabulary_words.h"
|
||||
#include "vm_internal.h"
|
||||
@@ -893,13 +899,35 @@ void execute_colon_word(VM* vm)
|
||||
* Only ever acts when this word's own execution left the VM in a
|
||||
* clean, resumable state (past the error/abort/exit_colon checks
|
||||
* below would be too late -- this runs before them, deliberately,
|
||||
* so a switch never happens mid-unwind). */
|
||||
{
|
||||
* so a switch never happens mid-unwind).
|
||||
*
|
||||
* Stage 3 correction (2026-09-13): also gated on
|
||||
* sk_vm_at_outermost_interpret() -- see g_vm_interpret_depth's own
|
||||
* doc comment above vm_interpret(). If this word is executing
|
||||
* nested inside a VM-EXEC/VM-CALL dispatch (from another VM's own
|
||||
* vm_interpret() call, still on the mainline stack), `vm` here does
|
||||
* not own the physical stack it's currently running on, so a real
|
||||
* switch must not fire -- it would park that borrowed stack region
|
||||
* under this VM's saved-context pointer, corrupting the enclosing
|
||||
* VM's own call chain the moment it resumes. Left pending
|
||||
* (take_pending() is not called at all in that case) so the next
|
||||
* checkpoint at the outermost frame picks it up instead of losing
|
||||
* it.
|
||||
*
|
||||
* note_switch_performed() is called BEFORE sk_vm_context_switch(),
|
||||
* not after (Stage 3 follow-on correction, 2026-09-13): the switch
|
||||
* call does not return until something later switches back to
|
||||
* `vm`, which could be an arbitrary number of ticks later --
|
||||
* calling it after would attribute switch_count_cumulative/
|
||||
* switch_ticks_since to the wrong (resume) event instead of the
|
||||
* actual switch-out this counts. */
|
||||
if (sk_vm_at_outermost_interpret()) {
|
||||
VMUuid target_id = sk_vm_switch_signal_take_pending();
|
||||
if (!vm_uuid_equal(target_id, vm_uuid_none())) {
|
||||
VMRegistryEntry target_entry;
|
||||
if (capsule_vm_registry_get(target_id, &target_entry) == 0 &&
|
||||
target_entry.vm_ptr && target_entry.vm_ptr != (void *)vm) {
|
||||
sk_vm_switch_signal_note_switch_performed();
|
||||
sk_vm_context_switch(vm, (VM *)target_entry.vm_ptr);
|
||||
/* Resumes here once something later switches back. */
|
||||
}
|
||||
@@ -1084,6 +1112,20 @@ static VM *g_log_attrib_vm; /* zero-initialized (BSS) */
|
||||
|
||||
VM *vm_log_attributed_vm(void) { return g_log_attrib_vm; }
|
||||
|
||||
/* FABRIC-3.md §XXVIII Stage 3 correction (2026-09-13): nesting depth of
|
||||
* vm_interpret() calls, tracked the same save/restore-adjacent way as
|
||||
* g_log_attrib_vm just above. A nested call (VM-EXEC/VM-CALL dispatching
|
||||
* into a different VM's own dictionary mid-interpret) runs on physical
|
||||
* stack memory it does not own -- the Stage 3 switch checkpoint in
|
||||
* execute_colon_word() below must never fire a real context switch there,
|
||||
* only at the outermost vm_interpret() call, or it parks a borrowed stack
|
||||
* region under the wrong VM's saved-context pointer (silent corruption,
|
||||
* not just the trampoline livelock this was first found chasing -- see
|
||||
* FABRIC-3.md for the full writeup). */
|
||||
static int g_vm_interpret_depth;
|
||||
|
||||
int sk_vm_at_outermost_interpret(void) { return g_vm_interpret_depth <= 1; }
|
||||
|
||||
void vm_interpret(VM* vm, const char* input)
|
||||
{
|
||||
if (!vm || !input) return;
|
||||
@@ -1091,6 +1133,7 @@ void vm_interpret(VM* vm, const char* input)
|
||||
|
||||
VM *prev_attrib_vm = g_log_attrib_vm;
|
||||
g_log_attrib_vm = vm;
|
||||
g_vm_interpret_depth++;
|
||||
|
||||
/* Load into input buffer (cap + NUL) */
|
||||
size_t n = 0, cap = INPUT_BUFFER_SIZE ? INPUT_BUFFER_SIZE - 1 : 0;
|
||||
@@ -1123,6 +1166,7 @@ void vm_interpret(VM* vm, const char* input)
|
||||
}
|
||||
}
|
||||
|
||||
g_vm_interpret_depth--;
|
||||
g_log_attrib_vm = prev_attrib_vm;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user