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:
co-authored by
Claude Sonnet 5
parent
9f0f33dfc5
commit
d9da82b065
@@ -1,5 +1,5 @@
|
||||
# Capsule Block Manifest — Auto-generated
|
||||
<!-- Generated by mkcapsule --manifest 2026-09-15T03:50:27Z -->
|
||||
<!-- Generated by mkcapsule --manifest 2026-09-15T04:46:38Z -->
|
||||
<!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. -->
|
||||
<!-- Hand-written justifications and immutability notes live -->
|
||||
<!-- in MANIFEST.md alongside this auto-generated index. -->
|
||||
|
||||
Binary file not shown.
@@ -294,6 +294,34 @@ int capsule_vm_find_by_name_nocase(const char *name, VMRegistryEntry *out);
|
||||
*/
|
||||
void capsule_vm_set_state(VMUuid vm_id, uint32_t state);
|
||||
|
||||
/**
|
||||
* capsule_vm_set_pending_reap - FABRIC-3.md §XXVIII Stage 4 (2026-09-14):
|
||||
* mark vm_id for deferred teardown once it is no longer worth resuming --
|
||||
* see VMRegistryEntry.pending_reap's own doc comment for the full
|
||||
* rationale. No-op if vm_id isn't registered.
|
||||
*
|
||||
* @param vm_id VM ID to mark.
|
||||
* @param pending 1 to mark, 0 to clear (e.g. a re-attach of the same
|
||||
* identity before the switcher ever reaped it).
|
||||
*/
|
||||
void capsule_vm_set_pending_reap(VMUuid vm_id, int pending);
|
||||
|
||||
/**
|
||||
* capsule_vm_force_reap - FABRIC-3.md §XXVIII Stage 4 (2026-09-14):
|
||||
* unconditionally tear down vm_id regardless of VM_STATE_SWITCHED_OUT --
|
||||
* the one caller allowed to bypass capsule_vm_kill()'s own refusal there,
|
||||
* because this is called *by* the Stage 3 switcher itself (vm_core.c's
|
||||
* checkpoint, on noticing pending_reap set), at the one point that
|
||||
* genuinely knows the parked native-stack context will never be resumed.
|
||||
* Also releases the VM's own switch-signal slot
|
||||
* (sk_vm_switch_signal_unregister()) -- generic cleanup, independent of
|
||||
* whatever subsystem (WIREBIND today) set pending_reap in the first
|
||||
* place. No-op if vm_id isn't registered or is already DEAD.
|
||||
*
|
||||
* @param vm_id VM ID to reap.
|
||||
*/
|
||||
void capsule_vm_force_reap(VMUuid vm_id);
|
||||
|
||||
/**
|
||||
* capsule_vm_registry_set_name - Assign a symbolic name to a registered VM
|
||||
*
|
||||
|
||||
@@ -128,6 +128,23 @@ typedef struct {
|
||||
* own quota at birth, explicitly evicted at
|
||||
* KILL. Not Hera's; she is pinned and never
|
||||
* reaches this field's purpose. */
|
||||
int pending_reap; /* FABRIC-3.md §XXVIII Stage 4 (2026-09-14):
|
||||
* set (capsule_vm_set_pending_reap()) when
|
||||
* this VM is VM_STATE_SWITCHED_OUT and its
|
||||
* owning WIREBIND device has physically
|
||||
* detached -- capsule_vm_kill() correctly
|
||||
* refuses to free a parked context, but the
|
||||
* memory still needs reclaiming once it's
|
||||
* no longer coming back. Deliberately a
|
||||
* plain flag, not a new VMState: `state`
|
||||
* still accurately reads SWITCHED_OUT (a
|
||||
* live context genuinely is parked there)
|
||||
* until the Stage 3 checkpoint
|
||||
* (vm_core.c) notices this flag instead of
|
||||
* attempting to resume it, and calls
|
||||
* capsule_vm_force_reap() there instead --
|
||||
* a safe point the switcher itself
|
||||
* controls, not the async detach handler. */
|
||||
} VMRegistryEntry;
|
||||
|
||||
/*===========================================================================
|
||||
|
||||
@@ -65,6 +65,17 @@
|
||||
* critical-section protection against being switched away mid-setup). */
|
||||
int sk_vm_switch_signal_register(VMUuid vm_id);
|
||||
|
||||
/* Remove a switch-signal participant (FABRIC-3.md §XXVIII Stage 4,
|
||||
* 2026-09-14) -- Tripod VMs never need this (they live forever), but
|
||||
* WIREBIND-birthed identity VMs cycle through attach/detach repeatedly
|
||||
* and must free their slot for reuse, or the bounded table exhausts
|
||||
* after SK_SWITCH_MAX_SLOTS attach/detach cycles. Compacts the table
|
||||
* (small, bounded, mutated only at attach/detach -- not a hot path).
|
||||
* Clears a pending switch targeting this VM, if any, so the checkpoint
|
||||
* never attempts to switch into a no-longer-registered participant.
|
||||
* No-op (returns -1) if vm_id was never registered. */
|
||||
int sk_vm_switch_signal_unregister(VMUuid vm_id);
|
||||
|
||||
/* Called from heartbeat_tick() (ISR context) every timer tick. Cheap:
|
||||
* iterates only the registered slots (bounded, small). */
|
||||
void sk_vm_switch_signal_tick(void);
|
||||
|
||||
@@ -167,6 +167,18 @@ const char *capsule_wirebind_attached_username(void);
|
||||
*/
|
||||
void capsule_wirebind_overflow_idle_check(void);
|
||||
|
||||
/**
|
||||
* capsule_wirebind_reap_idle_check - FABRIC-3.md §XXVIII Stage 4
|
||||
* (2026-09-14): sweeps the per-device live-identity table for entries
|
||||
* whose VM has been reaped (capsule_vm_force_reap(), called from the
|
||||
* Stage 3 checkpoint on a pending_reap target -- see that function's own
|
||||
* doc comment) and removes the now-stale table entry. Does NOT itself
|
||||
* free anything or call capsule_vm_kill()/force_reap() -- purely
|
||||
* bookkeeping cleanup after the fact. Called at the same ~1 Hz idle
|
||||
* cadence as capsule_wirebind_overflow_idle_check().
|
||||
*/
|
||||
void capsule_wirebind_reap_idle_check(void);
|
||||
|
||||
#endif /* __STARKERNEL__ */
|
||||
|
||||
#endif /* STARKERNEL_CAPSULE_WIREBIND_H */
|
||||
|
||||
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
File diff suppressed because it is too large
Load Diff
@@ -37,6 +37,7 @@
|
||||
#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 "starkernel/capsule_vm_switch_signal.h" /* FABRIC-3.md §XXVIII Stage 4 -- capsule_vm_force_reap() */
|
||||
#include "vm.h"
|
||||
#include "platform_alloc.h"
|
||||
/* No LOG_LINE_MAX include-order constraint anymore: vm.h's own
|
||||
@@ -323,6 +324,62 @@ void capsule_vm_set_state(VMUuid vm_id, uint32_t state) {
|
||||
if (entry) entry->state = state;
|
||||
}
|
||||
|
||||
void capsule_vm_set_pending_reap(VMUuid vm_id, int pending) {
|
||||
VMRegistryEntry *entry = vm_find_entry_ptr(vm_id);
|
||||
if (entry) entry->pending_reap = pending;
|
||||
}
|
||||
|
||||
void capsule_vm_force_reap(VMUuid vm_id) {
|
||||
VMRegistryEntry *entry = vm_find_entry_ptr(vm_id);
|
||||
if (!entry) return;
|
||||
|
||||
/* Idempotent, matching capsule_vm_kill()'s own convention -- the
|
||||
* Stage 3 checkpoint that calls this cannot itself know whether some
|
||||
* other path (a later re-attach's own teardown, say) already reaped
|
||||
* this exact VM before the checkpoint got to it. */
|
||||
if (entry->state == VM_STATE_DEAD) return;
|
||||
|
||||
/* Hera is never a switch-signal participant subject to pending_reap
|
||||
* in the first place, but refuse on principle anyway -- matches
|
||||
* capsule_vm_kill()'s own unconditional rule. */
|
||||
if (vm_uuid_is_hera(entry->vm_id)) return;
|
||||
|
||||
VM *vm = (VM *)entry->vm_ptr;
|
||||
|
||||
/* Deliberately NOT capsule_vm_kill()'s VM_STATE_SWITCHED_OUT refusal
|
||||
* -- that guard exists precisely for the case this function handles:
|
||||
* the caller (vm_core.c's Stage 3 checkpoint) has already decided,
|
||||
* at a safe cooperative point it alone controls, that this parked
|
||||
* native-stack context will never be resumed. Freeing it here is the
|
||||
* deferred half of the mark-and-defer design (FABRIC-3.md §XXVIII
|
||||
* Stage 4) -- see VMRegistryEntry.pending_reap's own doc comment. */
|
||||
if (entry->stadium_patron_cell != STADIUM_CELL_NONE) {
|
||||
(void)stadium_evict(entry->stadium_patron_cell);
|
||||
entry->stadium_patron_cell = STADIUM_CELL_NONE;
|
||||
}
|
||||
|
||||
if (vm) {
|
||||
vm_cleanup(vm);
|
||||
sf_free(vm);
|
||||
}
|
||||
|
||||
/* Generic cleanup, independent of whatever subsystem set
|
||||
* pending_reap -- a reaped VM is no longer a switch-signal
|
||||
* participant either way. */
|
||||
sk_vm_switch_signal_unregister(vm_id);
|
||||
|
||||
capsule_parity_log_kill(vm_id, entry->name);
|
||||
|
||||
console_puts("REAP: ");
|
||||
console_puts(entry->name[0] ? entry->name : "(unnamed)");
|
||||
console_println(" tombstone reclaimed");
|
||||
|
||||
entry->vm_ptr = (void *)0;
|
||||
entry->state = VM_STATE_DEAD;
|
||||
entry->pending_reap = 0;
|
||||
for (uint32_t i = 0; i < VM_NAME_MAX; i++) entry->name[i] = '\0';
|
||||
}
|
||||
|
||||
void capsule_vm_registry_set_name(VMUuid vm_id, const char *name) {
|
||||
VMRegistryEntry *entry;
|
||||
if (!name) return;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "starkernel/user_identity_seed.h"
|
||||
#include "starkernel/console.h"
|
||||
#include "starkernel/repl.h" /* sk_repl_get/set_active_vm() -- FABRIC-2.md §F.10 EJECT */
|
||||
#include "starkernel/capsule_vm_switch_signal.h" /* FABRIC-3.md §XXVIII Stage 4 -- register/unregister */
|
||||
#include "blkio.h"
|
||||
#include "block_subsystem.h" /* compute_crc64() */
|
||||
#include "word_source/include/block_words.h" /* blk_vm_flush_all() -- §F.10 EJECT */
|
||||
@@ -295,8 +296,23 @@ void capsule_wirebind_try_attach(struct blkio_dev *dev,
|
||||
live->in_use = 1;
|
||||
live->dev = dev;
|
||||
live->user_vm_id = user_id;
|
||||
live->switch_slot = -1;
|
||||
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): the user VM is
|
||||
* the one that actually runs FORTH work (VM-EXEC/MSG-TICK
|
||||
* dispatch target it directly) -- it participates in
|
||||
* preemptive switching. The console VM does not: it is a
|
||||
* REPL-session proxy whose own control flow
|
||||
* sk_repl_get_active_vm() already owns, never itself the
|
||||
* target of VM-EXEC or MSG-SEND, so it has nothing for a
|
||||
* switch-in to preempt. Registration failure (table full)
|
||||
* degrades to "this identity won't be preempted," not to a
|
||||
* failed attach -- the identity is still fully usable via
|
||||
* ordinary VM-EXEC/console-relay dispatch, same as before
|
||||
* Stage 4 existed. */
|
||||
live->switch_slot = sk_vm_switch_signal_register(user_id);
|
||||
memcpy(live->username, username, sizeof(live->username));
|
||||
if (live->switch_slot < 0) {
|
||||
console_println("WIREBIND: switch-signal table full -- this identity won't be preempted");
|
||||
}
|
||||
} else {
|
||||
console_println("WIREBIND: live-identity table full -- detach/switch tracking degraded for this attach");
|
||||
}
|
||||
@@ -382,6 +398,16 @@ static void wirebind_teardown_console(const char *username) {
|
||||
|
||||
static void wirebind_live_remove(wirebind_live_entry_t *e) {
|
||||
if (!e) return;
|
||||
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): centralized here rather
|
||||
* than at each of eject()/unclean_detach()'s own call sites -- both
|
||||
* paths that legitimately free this VM (as opposed to the tombstone
|
||||
* path, which does not call this while a VM is still switched-out)
|
||||
* must release its switch-signal slot the same way, or repeated
|
||||
* attach/detach cycles exhaust the bounded slot table. No-op if it
|
||||
* was never registered (table was full at attach time). */
|
||||
if (e->switch_slot >= 0) {
|
||||
sk_vm_switch_signal_unregister(e->user_vm_id);
|
||||
}
|
||||
e->in_use = 0;
|
||||
e->dev = (struct blkio_dev *) 0;
|
||||
e->username[0] = '\0';
|
||||
@@ -457,14 +483,35 @@ void capsule_wirebind_unclean_detach(struct blkio_dev *dev) {
|
||||
} else {
|
||||
/* capsule_vm_kill() refused -- VM_STATE_SWITCHED_OUT, a live
|
||||
* register/stack context is parked on this VM's own native
|
||||
* stack. Leave the live-table entry in place rather than
|
||||
* discard it: the tombstone-and-reap mechanism (next increment)
|
||||
* marks it here for the Stage 3 switcher to discard safely on
|
||||
* its own next attempt to resume this VM, instead of freeing
|
||||
* memory a parked context still points into. */
|
||||
* stack. Mark it for the Stage 3 checkpoint (vm_core.c) to reap
|
||||
* on its own next attempt to resume this VM, instead of freeing
|
||||
* memory a parked context still points into -- see
|
||||
* VMRegistryEntry.pending_reap's own doc comment. The live-table
|
||||
* entry stays in place until then: capsule_wirebind_reap_idle_
|
||||
* check() below removes it once capsule_vm_force_reap() has
|
||||
* actually run and the VM reads back DEAD. */
|
||||
capsule_vm_set_pending_reap(live->user_vm_id, 1);
|
||||
console_puts("WIREBIND: ");
|
||||
console_puts(entry.name);
|
||||
console_println(" device gone but VM is switched-out -- deferred");
|
||||
console_println(" device gone but VM is switched-out -- deferred reap");
|
||||
}
|
||||
}
|
||||
|
||||
void capsule_wirebind_reap_idle_check(void) {
|
||||
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): the Stage 3 checkpoint
|
||||
* (vm_core.c) is the only thing that actually frees a pending_reap
|
||||
* VM -- see capsule_vm_force_reap()'s own doc comment for why (it
|
||||
* must run at a cooperative point the switcher itself controls, not
|
||||
* from this idle tick). This sweep only cleans up the now-stale
|
||||
* live-table entry once that has already happened; it is not the
|
||||
* reap itself and never calls capsule_vm_kill()/force_reap(). */
|
||||
for (uint32_t i = 0; i < WIREBIND_MAX_LIVE; i++) {
|
||||
if (!g_wirebind_live[i].in_use) continue;
|
||||
VMRegistryEntry entry;
|
||||
if (capsule_vm_registry_get(g_wirebind_live[i].user_vm_id, &entry) != 0 ||
|
||||
entry.state == VM_STATE_DEAD) {
|
||||
wirebind_live_remove(&g_wirebind_live[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -642,6 +642,12 @@ static void sk_repl_idle(VM *active_vm)
|
||||
* growth loop. */
|
||||
capsule_wirebind_overflow_idle_check();
|
||||
|
||||
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): same cadence, cleans up
|
||||
* the per-device live-identity table once the Stage 3 checkpoint has
|
||||
* actually reaped a pending_reap VM -- see that function's own doc
|
||||
* comment. */
|
||||
capsule_wirebind_reap_idle_check();
|
||||
|
||||
/* FABRIC-2.md Phase C (2026-08-28): distributed messaging pump. Every
|
||||
* live VM except Hera herself now owns its own MSG-ARENA/CH-ARENA and
|
||||
* MSG-TICK word (see capsules/common/messaging.4th) instead of only
|
||||
|
||||
@@ -929,12 +929,27 @@ void execute_colon_word(VM* vm)
|
||||
VMRegistryEntry target_entry;
|
||||
if (capsule_vm_registry_get(target_id, &target_entry) == 0 &&
|
||||
target_entry.vm_ptr && target_entry.vm_ptr != (void *)vm) {
|
||||
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): a target
|
||||
* marked pending_reap is a WIREBIND identity whose
|
||||
* owning device has physically detached while it sat
|
||||
* parked SWITCHED_OUT -- capsule_vm_kill() already
|
||||
* refused to free it when that happened (see
|
||||
* VMRegistryEntry.pending_reap's own doc comment).
|
||||
* This checkpoint is the safe point the mark-and-
|
||||
* defer design calls for: reap instead of resuming,
|
||||
* and skip the switch this tick -- take_pending()
|
||||
* already consumed the pending flag, so no other
|
||||
* checkpoint will try this same dead end. */
|
||||
if (target_entry.pending_reap) {
|
||||
capsule_vm_force_reap(target_id);
|
||||
} else {
|
||||
sk_vm_switch_signal_note_switch_performed(target_id);
|
||||
sk_vm_context_switch(vm, (VM *)target_entry.vm_ptr);
|
||||
/* Resumes here once something later switches back. */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (vm->error) { vm->ecw_nesting--; return; }
|
||||
|
||||
Reference in New Issue
Block a user