FABRIC-3.md: scope DETACH and close UNCLEAN alongside it, flag a latent KILL bug

Traced capsule_vm_kill() fully -- it's real and complete, so DETACH needs only a caller plus flush/bookkeeping, not new teardown machinery. Found that D.3's "flush before eject" only makes sense as a deliberate pre-removal step, splitting this into a graceful path (new EJECT word) and the abrupt hot-unplug signal already wired -- per direct instruction, scoped both, closing the previously-separate UNCLEAN (M3) node as "same kill path, no flush attempt." Needs one small new piece of state (which VMUuid is attached via the home-blocks path) since BINDSTEP's live-reverify approach doesn't work once the device is already gone. Reports, without fixing, a separate pre-existing bug found while tracing this: capsule_vm_kill() never resets g_repl_active_vm, so plain KILL on a USE'd VM leaves a dangling pointer today.
This commit is contained in:
Robert Allan James
2026-08-27 14:37:37 -04:00
parent c4f9458ad6
commit 7f60df3d5c
+87 -8
View File
@@ -160,9 +160,11 @@ decisions get added here, not to `FABRIC-2.md`. Follow the same discipline `FABR
as a `compudynamics.c`-driven decision generically, not a `physics_hotwords_cache.c`
(`DictEntry*`-hardcoded) reuse question specifically.
- [ ] Decide and implement unclean-removal handling (Section U's explicitly flagged open
question — never answered) — at minimum, detect a mid-flush disconnect via Milestone 2e's
disconnect signal and decide what state that leaves affected blocks in.
- [x] **CLOSED 2026-08-27 (`FABRIC-3.md` §F.10), scoped alongside `DETACH`.** Unclean removal
runs the exact same VM-kill path `EJECT` (M5) uses, minus the flush step — skipped outright
since the device is confirmed gone by the time the existing `bot_msc_detach_pending` signal
fires, not attempted-and-ignored. Design done; the actual wiring into `sk_repl_idle()`'s
existing detach handling is not yet written.
### From FABRIC-2.md §X, Milestone 4 — Drive/credential security
@@ -687,7 +689,11 @@ decisions get added here, not to `FABRIC-2.md`. Follow the same discipline `FABR
already-working, currently-unguarded retarget word — check re-verifies the attached drive
live on every call; `installed=0` VMs stay freely targetable.
- [ ] Implement detach behavior on console disconnect or VM teardown.
- [ ] Implement detach behavior on console disconnect or VM teardown. **SCOPED 2026-08-27
(`FABRIC-3.md` §F.10)**: a new `EJECT` word — flush via the existing `blk_vm_flush_all()`,
reset the console's active VM if bound to the departing VM, then `capsule_vm_kill()` (already
complete, no new teardown mechanism needed). Needs one small new piece of state `WIREBIND`
must own: which `VMUuid` is currently attached via the home-blocks USB path.
### From FABRIC-2.md §X, Milestone 6 — Kernel/capsule PKI signing chain
@@ -1425,12 +1431,12 @@ graph TD
BMAPWRITE["❌ Write block-map to drive (M3)"]
BMAPREAD["❌ Read/validate block-map on insert (M3)"]
MIGSM["🟡 Migration state machine (M3)<br/>partially answered: ACL decides *when*"]
UNCLEAN[" Unclean-removal handling (M3)"]
UNCLEAN[" Unclean-removal handling (M3)<br/>CLOSED 2026-08-27 (§F.10) — same kill path as EJECT, minus flush"]
ACLKEY["❓ ACL 'bumps and holes' comparison<br/>semantics — SAME GAP in M5 and D.4"]
WIREBIND["❌ Wire hotplug → capsule_birth_baby() (M5)"]
BINDSTEP["❌ Extend sk_repl_set_active_vm()<br/>with key-match (M5) — SCOPED 2026-08-27 (§F.9)"]
DETACH["❌ Detach behavior (M5)"]
DETACH["❌ Detach behavior / new EJECT word (M5)<br/>SCOPED 2026-08-27 (§F.10)"]
EXPIRE["✅ EXPIRE reframed: session end<br/>= VM detach via COOL (§B + D.2)"]
RUNCAP["❌ Runtime capsule construction<br/>from thumbdrive content — SCOPED 2026-08-27 (§F.6)"]
@@ -1466,6 +1472,7 @@ graph TD
ACLKEY --> BINDSTEP
WIREBIND --> BINDSTEP
BINDSTEP --> DETACH
UNCLEAN -.->|same kill path, no flush| DETACH
DETACH -->|triggers| EXPIRE
MSGSHAPE --> MSGMIGRATE
@@ -1479,8 +1486,8 @@ graph TD
classDef unblocked fill:#a83,stroke:#333,color:#fff
classDef open fill:#666,stroke:#333,color:#fff
classDef partial fill:#883,stroke:#333,color:#fff
class M6,PH8,EXPIRE,MSGSHAPE,HOTPLUG done
class W10,STALL,FIRSTTOUCH,BMAPWRITE,BMAPREAD,UNCLEAN,WIREBIND,BINDSTEP,DETACH,MINT,BMAPFMT,CERTVERIFY,RUNCAP blocked
class M6,PH8,EXPIRE,MSGSHAPE,HOTPLUG,UNCLEAN done
class W10,STALL,FIRSTTOUCH,BMAPWRITE,BMAPREAD,WIREBIND,BINDSTEP,DETACH,MINT,BMAPFMT,CERTVERIFY,RUNCAP blocked
class ACLKEY,MSGMIGRATE,SSDSCOPE,ROUNDTRIP,POLYBLOCK open
class MIGSM partial
```
@@ -1526,6 +1533,14 @@ finished). Dashed arrows = softer "gates/informs" relationships.
(`mama_forth_words.c`) is a real, working, currently-unguarded retarget word. The only
genuinely new code this node needs is a small cross-file accessor for the currently-attached
device (mirroring `sk_repl_get_active_vm()`'s own precedent) plus the comparison itself.
- **`UNCLEAN` (M3) and `DETACH` (M5) were the same node wearing two names, in a different way
than `ACLKEY` was** (§F.10) — not identical questions asked twice, but one shared teardown
path (`capsule_vm_kill()`, already complete) forked by whether a flush is physically possible.
Scoping them together closed a long-open M3 item as a side effect of an M5 pass.
- **Found a real, pre-existing, independent bug while tracing `DETACH`** (§F.10): plain `KILL`
today never resets `g_repl_active_vm`, so killing the VM a console is currently `USE`'d onto
leaves a dangling pointer. Reported, not fixed — the new `EJECT`/hot-unplug call sites handle
this correctly, but the existing `KILL` word's own call sites do not.
**Not yet done:** an ordered plan (which node to attack first, given the graph). Per Captain
Bob's own framing, that's the next pass — "start asking and answering questions iteratively
@@ -1924,6 +1939,70 @@ refusal message text (matching `USE`'s existing console-message style); whether
this — `capsule_parity_log_birth_failed()` and friends, `parity.c` — but nothing says a
security-relevant refusal here needs the same treatment; not decided).
### F.10 — `DETACH` (the last M5 node) — and `UNCLEAN` closed alongside it
Traced `capsule_vm_kill()` (`capsule_birth.c:295-364`) fully before scoping, per D.2's own
framing that detach should reuse it: it's real and complete — evicts the Stadium patron cell
via `stadium_evict()`, `vm_cleanup()`+`sf_free()`s the VM, marks it `DEAD`, logs via
`capsule_parity_log_kill()`, is idempotent, and refuses to kill Hera. `DETACH` doesn't need a
new teardown mechanism, only a caller for this one plus the flush/bookkeeping around it.
**Real finding: D.3's "flush before eject" only makes physical sense as a deliberate
pre-removal step — once a drive is physically pulled, there's nothing left to write to.**
That splits this into two genuinely different paths, and per direct instruction this pass
scopes **both**, closing the previously-separate, long-open `UNCLEAN` node (M3) at the same
time rather than leaving it for later:
- **Graceful path (new `EJECT` word, no name conflict found):** the drive is still physically
present. Flush, then kill, then it's safe to remove.
- **Abrupt path (the existing `bot_msc_detach_pending` hot-unplug signal, already wired and
consumed in `sk_repl_idle()` for `blk_subsys_detach_device()`):** the drive is already gone.
No flush is attempted — data since the last flush is lost, which *is* correct unclean-removal
semantics, matching what every real OS does on a surprise USB yank. This is `UNCLEAN`'s
entire remaining scope, now closed as "same kill path as `EJECT`, minus the flush attempt."
**One genuinely new, small piece of state required for both paths** (not a new mechanism, a
missing link): a way to know *which VM* corresponds to the currently home-blocks-attached
drive. `BINDSTEP`'s live re-verify-and-compare approach (§F.9) only works while the drive is
still physically present and readable — useless for the abrupt path, where the device is
already gone by the time the signal fires. Decided: `WIREBIND` (§F.5, itself not yet built)
must set a small tracked `VMUuid` — "the VM currently attached via the home-blocks USB path" —
at successful birth; both `EJECT` and the hot-unplug handler read and clear it. No new registry
machinery needed beyond this: `capsule_vm_registry_get(vm_id, &entry)` (already exists) resolves
the `VMUuid` to a live entry, and `capsule_vm_kill(entry.name)` (already exists, takes a name
not an ID — chain the two rather than adding a kill-by-id variant) does the rest.
**Decisions made 2026-08-27:**
1. **`EJECT` (graceful):** resolve the tracked attached-VM id → entry; `blk_vm_flush_all(vm)`
(already exists, same function `sk_repl_idle()`'s own periodic sync already calls) while the
VM is still alive; if the console's active VM (`sk_repl_get_active_vm()`) is this same VM,
reset it to Hera (`sk_repl_set_active_vm(NULL)`) *before* teardown — necessary, not
optional, see the dangling-pointer finding below; then `capsule_vm_kill(entry.name)`; clear
the tracked attached-VM state. `capsule_vm_kill()`'s own idempotency covers the case where
the VM was already gone by some other path — no extra guard needed.
2. **Hot-unplug (`UNCLEAN`, closed):** identical steps minus the flush — skipped outright, not
attempted-and-ignored-on-failure, since the device is confirmed gone by the time this signal
fires. Same active-VM reset-before-kill step applies.
3. **`W10`'s edge to `DETACH` in the §E graph is specifically about the `EJECT` path's flush
step** — the hot-unplug/`UNCLEAN` path never touches `WRITE(10)` at all (nothing is written
during an unclean removal), so `UNCLEAN` itself is *not* blocked on `WRITE(10)`, only
`EJECT` is. Worth keeping distinct now that both share one node.
**Separate, already-existing bug found while tracing this, reported per standing instruction
(not fixed):** `capsule_vm_kill()` never touches `g_repl_active_vm`. Today, plain `KILL` on a
VM the console is currently `USE`'d onto leaves `g_repl_active_vm` dangling — the REPL would
fault on the next command dispatched through it. This is real and reachable *today*,
independent of `DETACH`/`EJECT`; decision 1 above only fixes it for the new `EJECT`/hot-unplug
call sites, not the pre-existing plain `KILL` word.
**Not yet scoped (deferred within this node):** the exact new tracked-state variable's
name/location (logically `WIREBIND`'s to own, but `WIREBIND` itself isn't built yet); whether
`EJECT` needs a name argument or always targets "whatever's currently attached" (leaning toward
the latter, given the single-USB-device constraint confirmed in `F.8` means there's never more
than one candidate — not committed here); the console message text distinguishing a graceful
eject from an unclean one in the transcript.
### D.5 — Scope expansion (2026-08-27): identity is common to every VM, not just users
Surfaced while scoping `ACLKEY`, stated directly: *"the whole object is to deliver a