Stage 4 increments 2+3: WIREBIND VMs as switch-signal participants + mark-and-defer tombstone reap (FABRIC-3.md §XXVIII Stage 4)

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
This commit is contained in:
Robert Allan James
2026-09-15 00:58:34 -04:00
co-authored by Claude Sonnet 5
parent 9f0f33dfc5
commit d9da82b065
15 changed files with 36422 additions and 14 deletions
@@ -32,9 +32,14 @@
#include "starkernel/vm/switch.h" /* sk_vm_switch_current_vm() -- see below */
#include <stddef.h>
/* Headroom beyond today's 3 participants (Hera/Hermes/Artemis) -- see
* the header's own doc comment on why this isn't hardcoded to 3. */
#define SK_SWITCH_MAX_SLOTS 8
/* 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,
@@ -82,6 +87,29 @@ int sk_vm_switch_signal_register(VMUuid vm_id) {
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;