Stage 4 increment 1: per-device WIREBIND tracking, fixing a real multi-identity detach leak (FABRIC-3.md §XXVIII Stage 4)

capsule_wirebind_unclean_detach()/eject() tracked "the attached identity"
as a single global, correct for the console-pairing UX (one physical
console) but wrong for detach safety: since §XV/§XVI proved multiple
identities genuinely live simultaneously via this same attach path, every
attach after the first silently overwrote the singleton, so an unclean
detach of any but the most-recently-attached identity was silently
ignored -- that VM leaked forever, no trace in the log.

Adds a per-device live-identity table, separate from the (unchanged)
console-pairing singleton, so unclean-detach resolves any attached
device to its own identity. Sized off messaging.4th's own VM-MAX (16)
minus Tripod's 3 reserved slots, not an invented number. Corrects the
stale "single-USB-device constraint" doc claim in capsule_wirebind.h,
false since §XV/§XVI.

Groundwork for Stage 4's real deliverable (WIREBIND VMs as switch-signal
participants) -- this increment only fixes detach targeting; switch-
signal registration is next.

Verified clean on all 3 architectures (no WIREBIND attach happens in a
plain boot, so this is a regression check on the existing Tripod-only
path; live multi-identity verification comes with the switch-signal
registration increment).

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:02:20 -04:00
co-authored by Claude Sonnet 5
parent 1c220ad4b4
commit 9f0f33dfc5
7 changed files with 27316 additions and 40 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
# Capsule Block Manifest — Auto-generated # Capsule Block Manifest — Auto-generated
<!-- Generated by mkcapsule --manifest 2026-09-15T00:05:32Z --> <!-- Generated by mkcapsule --manifest 2026-09-15T03:50:27Z -->
<!-- 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.
+24 -10
View File
@@ -92,9 +92,16 @@ void capsule_wirebind_try_attach(struct blkio_dev *dev,
* optional, to avoid a dangling console pointer; capsule_vm_kill() by * optional, to avoid a dangling console pointer; capsule_vm_kill() by
* name; clear the tracked state. * name; clear the tracked state.
* *
* Single-USB-device constraint (§F.8) means there is never more than one * EJECT is inherently about "whichever identity is currently paired to
* candidate, so this always targets "whatever's currently attached" -- * the one physical console" -- targets that singleton, no name argument.
* no name argument. * (Corrected 2026-09-14: this used to claim a "single-USB-device
* constraint (§F.8)" meant there was never more than one candidate --
* stale even at the time this correction was written; §XV/§XVI
* (2026-09-11/12) proved 9 identities genuinely simultaneously live via
* this same attach path. EJECT staying console-singleton-scoped is a
* deliberate UX choice now, not a hardware constraint -- see
* capsule_wirebind_unclean_detach()'s own doc below for the function
* that DOES need to reach every live identity, not just this one.)
* *
* @return 0 on success, -1 if nothing was attached to eject. * @return 0 on success, -1 if nothing was attached to eject.
*/ */
@@ -107,14 +114,21 @@ int capsule_wirebind_eject(void);
* bot_msc_detach_pending hot-unplug signal (repl.c) -- the device is * bot_msc_detach_pending hot-unplug signal (repl.c) -- the device is
* already gone by the time this runs, so no flush is attempted; data * already gone by the time this runs, so no flush is attempted; data
* since the last flush is lost, which is correct unclean-removal * since the last flush is lost, which is correct unclean-removal
* semantics. Otherwise identical to capsule_wirebind_eject(): same * semantics.
* active-VM reset-before-kill step, same tracked-state clear.
* *
* FABRIC-3.md §VII follow-on, 2026-09-06: now requires the departing * FABRIC-3.md §VII follow-on, 2026-09-06: requires the departing device
* device to actually be the one tracked as this WIREBIND user's own * to actually be one WIREBIND is tracking -- a real bug otherwise, found
* (g_wirebind_attached_dev) -- a real bug otherwise, found live once * live once genuine multi-device attach made a *different* device's
* genuine multi-device attach made a *different* device's detach * detach reachable while a WIREBIND user's own stayed attached.
* reachable while a WIREBIND user's own stayed attached. *
* FABRIC-3.md §XXVIII Stage 4, 2026-09-14: resolves the departing device
* against a per-device live-identity table now, not the single
* console-pairing global capsule_wirebind_eject() uses -- with several
* identities simultaneously live (§XV/§XVI), any one of them can be the
* device that just disappeared, not only the most recently attached.
* Refuses (rather than freeing) a VM currently VM_STATE_SWITCHED_OUT --
* see capsule_vm_kill()'s own guard -- leaving it tracked for the Stage 3
* switcher to reap on its own next resume attempt instead.
* *
* @param dev The device that just detached; every other value is a no-op. * @param dev The device that just detached; every other value is a no-op.
*/ */
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
+146 -29
View File
@@ -45,6 +45,63 @@ static char g_wirebind_attached_username[USER_IDENTITY_USERNAME_MAX] = {0};
* without a new attach-time argument threaded through the idle loop. */ * without a new attach-time argument threaded through the idle loop. */
static struct blkio_dev *g_wirebind_attached_dev = (struct blkio_dev *) 0; static struct blkio_dev *g_wirebind_attached_dev = (struct blkio_dev *) 0;
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): the singleton above correctly
* models "who is paired to the one physical console" (repl.c's own
* console-relay/`(user)` prompt logic genuinely is single-valued -- there
* is only one serial/framebuffer console). It does NOT correctly model
* "which devices are live," despite g_wirebind_attached_eject()'s own
* header doc claiming a "single-USB-device constraint (§F.8)" -- that
* claim is stale: §XV/§XVI (2026-09-11/12) proved 9 identities genuinely
* simultaneously live via this exact attach path. Every attach after the
* first silently overwrote the singleton, so capsule_wirebind_unclean_
* detach()'s `dev != g_wirebind_attached_dev` check only ever matched the
* *most recently attached* identity -- any earlier identity's drive
* being pulled hit that check, returned early, and orphaned: never torn
* down, its VM and native stack leaked forever, no console/log trace at
* all. Silent because nothing in this project's verification history
* ever detached anything but the single most-recent identity.
*
* This new table is additive and separate from the console singleton
* above (which is untouched, still governs console pairing exactly as
* before) -- it tracks every currently-live WIREBIND identity by its own
* device pointer, so unclean-detach (and Stage 4's own switch-signal
* registration bookkeeping) can resolve *any* attached device to *its
* own* identity, not just the last one. Sized off messaging.4th's own
* VM-MAX (16) minus the 3 slots Hera/Hermes/Artemis permanently reserve
* (capsules/common/messaging.4th: "identities get NEW slots 3-10, never
* renumbered") -- a real, already-agreed system-wide ceiling, not a new
* guess (see feedback_no_hardcoded_small_scale_bounds in project memory:
* Bob has rejected an invented cap here before). */
#define WIREBIND_MAX_LIVE 13u
typedef struct {
int in_use;
struct blkio_dev *dev;
VMUuid user_vm_id;
char username[USER_IDENTITY_USERNAME_MAX];
int switch_slot; /* Stage 4: index into the switch-signal
* table, or -1 if not yet registered
* (wired in the next increment). */
} wirebind_live_entry_t;
static wirebind_live_entry_t g_wirebind_live[WIREBIND_MAX_LIVE];
static wirebind_live_entry_t *wirebind_live_find_by_dev(struct blkio_dev *dev) {
for (uint32_t i = 0; i < WIREBIND_MAX_LIVE; i++) {
if (g_wirebind_live[i].in_use && g_wirebind_live[i].dev == dev) {
return &g_wirebind_live[i];
}
}
return (wirebind_live_entry_t *)0;
}
static wirebind_live_entry_t *wirebind_live_alloc(void) {
for (uint32_t i = 0; i < WIREBIND_MAX_LIVE; i++) {
if (!g_wirebind_live[i].in_use) return &g_wirebind_live[i];
}
return (wirebind_live_entry_t *)0;
}
/* WIREBIND_CERT_MAX_DEVBLOCKS: a sane upper bound on how much cert /* WIREBIND_CERT_MAX_DEVBLOCKS: a sane upper bound on how much cert
* content this reads, independent of whatever sig->cert_devblocks * content this reads, independent of whatever sig->cert_devblocks
* claims -- MINT (capsule_mint.c) only ever writes 1 devblock's worth * claims -- MINT (capsule_mint.c) only ever writes 1 devblock's worth
@@ -224,6 +281,27 @@ void capsule_wirebind_try_attach(struct blkio_dev *dev,
g_wirebind_attached_dev = dev; g_wirebind_attached_dev = dev;
memcpy(g_wirebind_attached_username, username, sizeof(g_wirebind_attached_username)); memcpy(g_wirebind_attached_username, username, sizeof(g_wirebind_attached_username));
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): additive per-device
* tracking, independent of the console-pairing singleton just above
* (which stays exactly as it was) -- see g_wirebind_live[]'s own doc
* comment. A full table is reported, not silently dropped: running
* out of the real, sourced ceiling here means something is actually
* wrong (more identities live than this system's own VM-MAX allows
* for), worth knowing about rather than quietly leaving a device
* untracked for unclean-detach/switch-signal purposes. */
{
wirebind_live_entry_t *live = wirebind_live_alloc();
if (live) {
live->in_use = 1;
live->dev = dev;
live->user_vm_id = user_id;
live->switch_slot = -1;
memcpy(live->username, username, sizeof(live->username));
} else {
console_println("WIREBIND: live-identity table full -- detach/switch tracking degraded for this attach");
}
}
/* Decided 2026-09-05: no console for the running system unless a /* Decided 2026-09-05: no console for the running system unless a
* thumbdrive is present. Revised 2026-09-06: this used to call * thumbdrive is present. Revised 2026-09-06: this used to call
* sk_console_mark_login() here (a one-way sticky flag) -- the gate is * sk_console_mark_login() here (a one-way sticky flag) -- the gate is
@@ -281,15 +359,18 @@ static int wirebind_resolve_attached(VMRegistryEntry *out) {
* second identity right after an unclean detach of a first left the * second identity right after an unclean detach of a first left the
* console stuck showing the first identity's own dead-paired prompt. * console stuck showing the first identity's own dead-paired prompt.
* *
* Uses g_wirebind_attached_username (the bare username, captured at * Takes the bare username explicitly (FABRIC-3.md §XXVIII Stage 4,
* attach time) rather than deriving it from entry.name (which is * 2026-09-14 -- was g_wirebind_attached_username directly, which only
* already the "<user>~user" registry name) -- see capsule_wirebind_ * ever names the single console-paired identity; unclean-detach of any
* try_attach()'s own comment on why the two names are tracked * other simultaneously-live identity needs its own username, not the
* separately. Called before either caller clears that field. */ * console-pairing singleton's, so this can no longer read the global
static void wirebind_teardown_console(void) { * itself). capsule_wirebind_eject() still passes the singleton's own
if (g_wirebind_attached_username[0] == '\0') return; * username, since EJECT (a typed command) is inherently about "whichever
* identity is currently console-paired." */
static void wirebind_teardown_console(const char *username) {
if (!username || username[0] == '\0') return;
VMRegistryEntry console_entry; VMRegistryEntry console_entry;
if (capsule_vm_find_by_name(g_wirebind_attached_username, &console_entry) != 0 || if (capsule_vm_find_by_name(username, &console_entry) != 0 ||
console_entry.state != VM_STATE_LIVE) { console_entry.state != VM_STATE_LIVE) {
return; return;
} }
@@ -299,6 +380,14 @@ static void wirebind_teardown_console(void) {
capsule_vm_kill(console_entry.name); capsule_vm_kill(console_entry.name);
} }
static void wirebind_live_remove(wirebind_live_entry_t *e) {
if (!e) return;
e->in_use = 0;
e->dev = (struct blkio_dev *) 0;
e->username[0] = '\0';
e->switch_slot = -1;
}
int capsule_wirebind_eject(void) { int capsule_wirebind_eject(void) {
VMRegistryEntry entry; VMRegistryEntry entry;
if (wirebind_resolve_attached(&entry) != 0) { if (wirebind_resolve_attached(&entry) != 0) {
@@ -310,8 +399,9 @@ int capsule_wirebind_eject(void) {
* remove (§F.10 decision 1). */ * remove (§F.10 decision 1). */
blk_vm_flush_all((VM *)entry.vm_ptr); blk_vm_flush_all((VM *)entry.vm_ptr);
wirebind_teardown_console(); wirebind_teardown_console(g_wirebind_attached_username);
capsule_vm_kill(entry.name); capsule_vm_kill(entry.name);
wirebind_live_remove(wirebind_live_find_by_dev(g_wirebind_attached_dev));
g_wirebind_attached_valid = 0; g_wirebind_attached_valid = 0;
g_wirebind_attached_dev = (struct blkio_dev *) 0; g_wirebind_attached_dev = (struct blkio_dev *) 0;
g_wirebind_attached_username[0] = '\0'; g_wirebind_attached_username[0] = '\0';
@@ -323,32 +413,59 @@ int capsule_wirebind_eject(void) {
} }
void capsule_wirebind_unclean_detach(struct blkio_dev *dev) { void capsule_wirebind_unclean_detach(struct blkio_dev *dev) {
/* FABRIC-3.md §VII follow-on, 2026-09-06: same defect class as /* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): was `dev !=
* capsule_zuse_boot_logout()'s own fix, found in the same live * g_wirebind_attached_dev`, comparing against the single console-
* verification session -- this used to take no device parameter at * pairing global -- correct only for the single most-recently-
* all, so an unrelated device detaching (Zuse's own drive, or general- * attached identity (see g_wirebind_live[]'s own doc comment above
* purpose USB use) while a WIREBIND user stayed attached would * for the real bug this caused: every earlier identity's device-pull
* incorrectly tear down that user's session too. The single-USB- * was silently ignored, leaking that VM forever). Now resolves via
* device constraint this was written under never let a *different* * the per-device live table instead, so any of several simultaneously
* device be the one detaching while a WIREBIND user's own stayed live * live identities' own device-pull is handled correctly. */
* -- stale now that genuine multi-device attach exists. */ wirebind_live_entry_t *live = wirebind_live_find_by_dev(dev);
if (dev != g_wirebind_attached_dev) return; if (!live) return; /* not a tracked WIREBIND device, or already torn down */
VMRegistryEntry entry; VMRegistryEntry entry;
if (wirebind_resolve_attached(&entry) != 0) return; if (capsule_vm_registry_get(live->user_vm_id, &entry) != 0 ||
entry.state == VM_STATE_DEAD || entry.state == VM_STATE_STILLBORN) {
wirebind_live_remove(live);
return;
}
/* Device is already gone (§F.10 decision 2) -- no flush attempted; /* Device is already gone (§F.10 decision 2) -- no flush attempted;
* data since the last flush is lost, matching real-OS unclean-removal * data since the last flush is lost, matching real-OS unclean-removal
* semantics. */ * semantics. The console VM has no switch-signal participation (only
wirebind_teardown_console(); * user VMs do, once Stage 4's registration lands) so it is always
capsule_vm_kill(entry.name); * safely LIVE and killable regardless of the user VM's own state
g_wirebind_attached_valid = 0; * below. */
g_wirebind_attached_dev = (struct blkio_dev *) 0; wirebind_teardown_console(live->username);
g_wirebind_attached_username[0] = '\0'; int killed = (capsule_vm_kill(entry.name) == 0);
console_puts("WIREBIND: "); /* Only clear the console-pairing singleton if THIS device is the one
console_puts(entry.name); * it currently tracks -- an unrelated identity's own detach must
console_println(" detached without flush (device already gone)"); * never disturb who is paired to the one physical console. */
if (g_wirebind_attached_valid && g_wirebind_attached_dev == dev) {
g_wirebind_attached_valid = 0;
g_wirebind_attached_dev = (struct blkio_dev *) 0;
g_wirebind_attached_username[0] = '\0';
}
if (killed) {
console_puts("WIREBIND: ");
console_puts(entry.name);
console_println(" detached without flush (device already gone)");
wirebind_live_remove(live);
} 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. */
console_puts("WIREBIND: ");
console_puts(entry.name);
console_println(" device gone but VM is switched-out -- deferred");
}
} }
const char *capsule_wirebind_attached_username(void) { const char *capsule_wirebind_attached_username(void) {