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
@@ -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. -->
|
||||||
|
|||||||
Binary file not shown.
@@ -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
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user