Increment 2: WIREBIND user VMs (the ones that actually run FORTH work; console VMs are pure REPL proxies and never participate) register as Stage 3 switch-signal participants at attach, unregister at teardown. Slot table bumped 8 -> 16, matching messaging.4th's own VM-MAX -- a real, already-agreed ceiling, not an invented number. Added sk_vm_switch_signal_unregister() (compaction-based; Tripod VMs never needed removal, WIREBIND VMs cycle constantly and would otherwise exhaust the bounded table). Increment 3: implements the plan's own ratified option (A) for the async-detach UAF risk -- mark-and-defer via a new pending_reap flag on VMRegistryEntry, deliberately not a new VMState (capsule_vm_kill() already treats VM_STATE_DEAD as idempotent success, which would silently swallow a reap attempt; SWITCHED_OUT still accurately describes a tombstoned VM until the moment it's actually freed). unclean_detach() sets it when capsule_vm_kill() refuses a SWITCHED_OUT target; the Stage 3 checkpoint (vm_core.c) checks it before ever attempting to resume a pending switch target, and calls the new capsule_vm_force_reap() instead -- the one caller allowed to bypass capsule_vm_kill()'s own refusal, because it runs at the exact safe cooperative point the switcher itself controls. A new idle-tick sweep cleans up the WIREBIND live-table entry once the reap has actually happened. Verified clean on all 3 architectures (baseline regression -- no WIREBIND attach happens in a plain boot). The reap mechanism's own correctness under a genuinely parked context is verified separately, next, via a temporary deterministic probe. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BWpNjdwPtFLuVLaAq44L9K
227 lines
8.3 KiB
C
227 lines
8.3 KiB
C
/*
|
||
StarForth — Steady-State Virtual Machine Runtime
|
||
|
||
Copyright (c) 2023–2025 Robert A. James
|
||
All rights reserved.
|
||
|
||
This file is part of the StarForth project.
|
||
|
||
Licensed under the StarForth License, Version 1.0 (the "License");
|
||
you may not use this file except in compliance with the License.
|
||
|
||
You may obtain a copy of the License at:
|
||
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
|
||
|
||
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||
express or implied, including but not limited to the warranties of
|
||
merchantability, fitness for a particular purpose, and noninfringement.
|
||
|
||
See the License for the specific language governing permissions and
|
||
limitations under the License.
|
||
*/
|
||
|
||
/**
|
||
* capsule_vm_switch_signal.c - see the header for the full contract
|
||
* (FABRIC-3.md §XXVIII, Stage 3, 2026-09-13).
|
||
*/
|
||
|
||
#ifdef __STARKERNEL__
|
||
|
||
#include "starkernel/capsule_vm_switch_signal.h"
|
||
#include "vm.h"
|
||
#include "starkernel/vm/switch.h" /* sk_vm_switch_current_vm() -- see below */
|
||
#include <stddef.h>
|
||
|
||
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): bumped from 8 to 16 to admit
|
||
* WIREBIND identity VMs alongside the fixed Tripod fleet. 16 is not a new
|
||
* guess -- it matches messaging.4th's own VM-MAX (the real, already-
|
||
* agreed system-wide ceiling: 3 permanent Tripod slots + 13 for
|
||
* identities, "identities get NEW slots 3-10, never renumbered" plus
|
||
* headroom to VM-MAX itself). See feedback_no_hardcoded_small_scale_bounds
|
||
* in project memory -- an invented cap here was rejected before. */
|
||
#define SK_SWITCH_MAX_SLOTS 16
|
||
|
||
/* Ticks a non-running participant must accumulate readiness before a
|
||
* switch to it is requested. Simple linear accumulate-then-threshold,
|
||
* not the fancier relaxation law a later pass might want -- correctness
|
||
* and a clean single-writer/single-reader story mattered more than the
|
||
* exact law for this first cut (this stage's own design-review latitude,
|
||
* per the plan). Reset to 0 the instant a slot becomes current, so a
|
||
* VM that just ran needs a full threshold's worth of ticks idle before
|
||
* it can be selected again -- a crude but real fairness property. */
|
||
#define SK_SWITCH_READINESS_THRESHOLD 50u
|
||
|
||
typedef struct {
|
||
VMUuid vm_id;
|
||
uint32_t readiness;
|
||
int has_work; /* Stage 3 follow-on (2026-09-14): set by
|
||
* sk_vm_switch_signal_mark_work() (mainline,
|
||
* MSG-SEND hook), cleared here in tick() the
|
||
* instant this slot becomes current -- see that
|
||
* function's own doc comment in the header. */
|
||
} sk_switch_slot_entry_t;
|
||
|
||
static sk_switch_slot_entry_t g_slots[SK_SWITCH_MAX_SLOTS];
|
||
static int g_slot_count;
|
||
static int g_pending;
|
||
static VMUuid g_pending_target;
|
||
|
||
/* DoE CSV exposure (FABRIC-3.md §XXVIII Stage 3 follow-on, 2026-09-13). */
|
||
static int g_current_slot_cached = -1;
|
||
static uint64_t g_switch_count;
|
||
static uint32_t g_ticks_since_switch;
|
||
|
||
static int slot_for_vm_id(VMUuid vm_id) {
|
||
int i;
|
||
for (i = 0; i < g_slot_count; i++) {
|
||
if (vm_uuid_equal(g_slots[i].vm_id, vm_id)) return i;
|
||
}
|
||
return -1;
|
||
}
|
||
|
||
int sk_vm_switch_signal_register(VMUuid vm_id) {
|
||
if (g_slot_count >= SK_SWITCH_MAX_SLOTS) return -1;
|
||
g_slots[g_slot_count].vm_id = vm_id;
|
||
g_slots[g_slot_count].readiness = 0;
|
||
g_slots[g_slot_count].has_work = 0;
|
||
return g_slot_count++;
|
||
}
|
||
|
||
int sk_vm_switch_signal_unregister(VMUuid vm_id) {
|
||
int slot = slot_for_vm_id(vm_id);
|
||
if (slot < 0) return -1;
|
||
|
||
for (int i = slot; i < g_slot_count - 1; i++) {
|
||
g_slots[i] = g_slots[i + 1];
|
||
}
|
||
g_slot_count--;
|
||
|
||
/* A pending switch targeting this VM must not survive its removal --
|
||
* the checkpoint in execute_colon_word() would otherwise try to
|
||
* switch into a VM no longer tracked here. */
|
||
if (g_pending && vm_uuid_equal(g_pending_target, vm_id)) {
|
||
g_pending = 0;
|
||
}
|
||
|
||
/* g_current_slot_cached is a DoE-observable value only (see its own
|
||
* declaration comment); if compaction just invalidated it, the very
|
||
* next tick() recomputes it from scratch via slot_for_vm_id(), so a
|
||
* momentarily-stale cache here is harmless. */
|
||
return 0;
|
||
}
|
||
|
||
void sk_vm_switch_signal_tick(void) {
|
||
VM *current;
|
||
int current_slot, i;
|
||
|
||
/* FABRIC-3.md §XXVIII Stage 3 follow-on correction (2026-09-14): was
|
||
* vm_log_attributed_vm() -- wrong source. That tracks nested
|
||
* vm_interpret() calls for log attribution and has no way to know a
|
||
* VM is running while parked inside switch.c's own raw-C trampoline
|
||
* (sk_vm_switch_entry()), which never itself calls vm_interpret(). A
|
||
* VM that only ever ran the trampoline was therefore never seen as
|
||
* "current," so its own readiness/has_work below never reset --
|
||
* tick() re-armed the very next timer tick, forever, once nothing
|
||
* else happened to be independently flipping attribution around it
|
||
* (found live: a switch storm, 101% CPU, log frozen). sk_vm_switch_
|
||
* current_vm() is authoritative instead: the switch mechanism itself
|
||
* updates it on every switch, so it reflects who is physically
|
||
* running regardless of whether that VM ever calls vm_interpret(). */
|
||
current = sk_vm_switch_current_vm();
|
||
if (!current) return; /* boot: no switch has happened yet, not seeded */
|
||
|
||
current_slot = slot_for_vm_id(current->stadium_vm_id);
|
||
if (current_slot < 0) return; /* current VM isn't a registered participant */
|
||
|
||
g_current_slot_cached = current_slot;
|
||
g_ticks_since_switch++;
|
||
|
||
for (i = 0; i < g_slot_count; i++) {
|
||
if (i == current_slot) {
|
||
g_slots[i].readiness = 0;
|
||
g_slots[i].has_work = 0;
|
||
} else {
|
||
g_slots[i].readiness++;
|
||
}
|
||
}
|
||
|
||
if (!g_pending) {
|
||
for (i = 0; i < g_slot_count; i++) {
|
||
if (i != current_slot &&
|
||
g_slots[i].readiness >= SK_SWITCH_READINESS_THRESHOLD &&
|
||
g_slots[i].has_work) {
|
||
g_pending = 1;
|
||
g_pending_target = g_slots[i].vm_id;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
VMUuid sk_vm_switch_signal_take_pending(void) {
|
||
if (!g_pending) return vm_uuid_none();
|
||
g_pending = 0;
|
||
return g_pending_target;
|
||
}
|
||
|
||
void sk_vm_switch_signal_note_switch_performed(VMUuid target_id) {
|
||
/* FABRIC-3.md §XXVIII Stage 3 follow-on correction (2026-09-14):
|
||
* reset the target's own readiness/has_work HERE, deterministically,
|
||
* at the switch decision point -- not by waiting for tick()'s
|
||
* current-slot polling to observe it running. That polling depends on
|
||
* sk_vm_switch_current_vm() catching a real ISR tick during whatever
|
||
* window the target is genuinely "current" for, and for a VM whose
|
||
* only production behavior is switch.c's placeholder trampoline (an
|
||
* immediate yield-back), that window can be as small as a few
|
||
* instructions -- too narrow for a timer tick to reliably land in.
|
||
* Without this, the target's eligibility never clears, tick() re-arms
|
||
* on the very next timer tick, and the result is a continuous switch
|
||
* storm (found live: 101% CPU, zero forward progress). This makes
|
||
* correctness independent of ISR timing; current_slot (via
|
||
* sk_vm_switch_current_vm()/tick()) remains useful as a DoE-observable
|
||
* value and for excluding the running slot from readiness accrual,
|
||
* but is no longer load-bearing for storm prevention. */
|
||
int slot = slot_for_vm_id(target_id);
|
||
if (slot >= 0) {
|
||
g_slots[slot].readiness = 0;
|
||
g_slots[slot].has_work = 0;
|
||
}
|
||
g_switch_count++;
|
||
g_ticks_since_switch = 0;
|
||
}
|
||
|
||
void sk_vm_switch_signal_mark_work(VMUuid vm_id) {
|
||
int slot = slot_for_vm_id(vm_id);
|
||
if (slot < 0) return;
|
||
g_slots[slot].has_work = 1;
|
||
}
|
||
|
||
uint64_t sk_vm_switch_signal_switch_count(void) {
|
||
return g_switch_count;
|
||
}
|
||
|
||
uint32_t sk_vm_switch_signal_ticks_since_switch(void) {
|
||
return g_ticks_since_switch;
|
||
}
|
||
|
||
int sk_vm_switch_signal_current_slot(void) {
|
||
return g_current_slot_cached;
|
||
}
|
||
|
||
int sk_vm_switch_signal_slot_count(void) {
|
||
return g_slot_count;
|
||
}
|
||
|
||
uint32_t sk_vm_switch_signal_readiness(int slot) {
|
||
if (slot < 0 || slot >= g_slot_count) return 0;
|
||
return g_slots[slot].readiness;
|
||
}
|
||
|
||
uint32_t sk_vm_switch_signal_readiness_of(VMUuid vm_id) {
|
||
int slot = slot_for_vm_id(vm_id);
|
||
if (slot < 0) return 0;
|
||
return g_slots[slot].readiness;
|
||
}
|
||
|
||
#endif /* __STARKERNEL__ */
|