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:
co-authored by
Claude Sonnet 5
parent
1c220ad4b4
commit
9f0f33dfc5
@@ -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. */
|
||||
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
|
||||
* content this reads, independent of whatever sig->cert_devblocks
|
||||
* 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;
|
||||
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
|
||||
* thumbdrive is present. Revised 2026-09-06: this used to call
|
||||
* 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
|
||||
* console stuck showing the first identity's own dead-paired prompt.
|
||||
*
|
||||
* Uses g_wirebind_attached_username (the bare username, captured at
|
||||
* attach time) rather than deriving it from entry.name (which is
|
||||
* already the "<user>~user" registry name) -- see capsule_wirebind_
|
||||
* try_attach()'s own comment on why the two names are tracked
|
||||
* separately. Called before either caller clears that field. */
|
||||
static void wirebind_teardown_console(void) {
|
||||
if (g_wirebind_attached_username[0] == '\0') return;
|
||||
* Takes the bare username explicitly (FABRIC-3.md §XXVIII Stage 4,
|
||||
* 2026-09-14 -- was g_wirebind_attached_username directly, which only
|
||||
* ever names the single console-paired identity; unclean-detach of any
|
||||
* other simultaneously-live identity needs its own username, not the
|
||||
* console-pairing singleton's, so this can no longer read the global
|
||||
* itself). capsule_wirebind_eject() still passes the singleton's own
|
||||
* 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;
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
@@ -299,6 +380,14 @@ static void wirebind_teardown_console(void) {
|
||||
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) {
|
||||
VMRegistryEntry entry;
|
||||
if (wirebind_resolve_attached(&entry) != 0) {
|
||||
@@ -310,8 +399,9 @@ int capsule_wirebind_eject(void) {
|
||||
* remove (§F.10 decision 1). */
|
||||
blk_vm_flush_all((VM *)entry.vm_ptr);
|
||||
|
||||
wirebind_teardown_console();
|
||||
wirebind_teardown_console(g_wirebind_attached_username);
|
||||
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_dev = (struct blkio_dev *) 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) {
|
||||
/* FABRIC-3.md §VII follow-on, 2026-09-06: same defect class as
|
||||
* capsule_zuse_boot_logout()'s own fix, found in the same live
|
||||
* verification session -- this used to take no device parameter at
|
||||
* all, so an unrelated device detaching (Zuse's own drive, or general-
|
||||
* purpose USB use) while a WIREBIND user stayed attached would
|
||||
* incorrectly tear down that user's session too. The single-USB-
|
||||
* device constraint this was written under never let a *different*
|
||||
* device be the one detaching while a WIREBIND user's own stayed live
|
||||
* -- stale now that genuine multi-device attach exists. */
|
||||
if (dev != g_wirebind_attached_dev) return;
|
||||
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): was `dev !=
|
||||
* g_wirebind_attached_dev`, comparing against the single console-
|
||||
* pairing global -- correct only for the single most-recently-
|
||||
* attached identity (see g_wirebind_live[]'s own doc comment above
|
||||
* for the real bug this caused: every earlier identity's device-pull
|
||||
* was silently ignored, leaking that VM forever). Now resolves via
|
||||
* the per-device live table instead, so any of several simultaneously
|
||||
* live identities' own device-pull is handled correctly. */
|
||||
wirebind_live_entry_t *live = wirebind_live_find_by_dev(dev);
|
||||
if (!live) return; /* not a tracked WIREBIND device, or already torn down */
|
||||
|
||||
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;
|
||||
* data since the last flush is lost, matching real-OS unclean-removal
|
||||
* semantics. */
|
||||
wirebind_teardown_console();
|
||||
capsule_vm_kill(entry.name);
|
||||
g_wirebind_attached_valid = 0;
|
||||
g_wirebind_attached_dev = (struct blkio_dev *) 0;
|
||||
g_wirebind_attached_username[0] = '\0';
|
||||
* semantics. The console VM has no switch-signal participation (only
|
||||
* user VMs do, once Stage 4's registration lands) so it is always
|
||||
* safely LIVE and killable regardless of the user VM's own state
|
||||
* below. */
|
||||
wirebind_teardown_console(live->username);
|
||||
int killed = (capsule_vm_kill(entry.name) == 0);
|
||||
|
||||
console_puts("WIREBIND: ");
|
||||
console_puts(entry.name);
|
||||
console_println(" detached without flush (device already gone)");
|
||||
/* Only clear the console-pairing singleton if THIS device is the one
|
||||
* it currently tracks -- an unrelated identity's own detach must
|
||||
* 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) {
|
||||
|
||||
Reference in New Issue
Block a user