Artemis Milestone 2h: hot-detach -- 2h complete
blk_subsys_detach_device() (block_subsystem.c) walks the device chain, refuses removal of anything but the current tail (a mid-chain removal would corrupt every later slot's start_lbn -- this architecture's own doc already argues USB stays last specifically to avoid that), unlinks, shrinks total_user_lbn, closes and frees the slot. Discards rather than flushes dirty state -- the device is physically gone by the time this runs (PORTSC disconnect only). Trigger wiring mirrors the attach path: bot_msc_attached (set only once attach actually succeeds) gates a new bot_msc_detach_pending flag set at PORTSC disconnect (not Disable Slot completion, which is conditionally skipped and would miss concurrent connect/disconnect pairs), consumed in sk_repl_idle(). Advisor flagged the real hazard ahead of time: block_words.c's VM block window (blk_vm_lbn[]/blk_vm_cbuf[]) can go stale across a detach then a same-LBN re-attach, and suggested a pointer-identity re-check in blk_vm_load() as a minimal fix. That fix was implemented, then directly falsified by its own designed-for-this test: attach a blank device, read a block (populating the cache), detach, re-attach a device with distinct content at the identical LBN, read again -- served stale content from the first device. Root cause, confirmed live: glibc's allocator hands free(slot) straight back to the very next same-size calloc(), so the "fresh" and stale pointers were bitwise identical despite being two different devices. Fixed properly with a monotonic blk_subsys_epoch() counter (bumped on every attach/detach) checked by a new blk_vm_check_epoch() helper at the one choke point (blk_vm_find(), plus blk_vm_flush_all() which reads the same arrays directly) that covers every path touching the window cache -- unfooled by address reuse. Verified live with a new disk/usb-thumbdrive-test2.img fixture (distinct content from the existing blank test image): attach A, read (cache hit populated), detach, re-attach B at the same LBN, read again -- correctly ran a fresh device read and returned B's real content, not A's stale cached zeros. The failing pointer-comparison attempt's own capture log kept as evidence, not deleted. All three architectures re-verified clean. FABRIC-2.md Section X 2h marked complete -- enumeration through hot-detach all live and verified; only WRITE(10) (2g's own still-open item) remains unimplemented in the driver, not blocking anything here. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CXjAPTEKrgY2Mrk25KoLDn
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
3b085dd875
commit
af267a52a6
+76
-3
@@ -3850,9 +3850,82 @@ call site of its own — USB is inherently hotplug, so a boot with no device con
|
||||
none of this new code, same as every earlier BOT increment's own probe-free acceptance runs):
|
||||
`logs/20260825-124143/amd64/`, `logs/20260825-124323/aarch64/`, `logs/20260825-124909/riscv64/`.
|
||||
|
||||
Still ahead for 2h: only the hot-**detach** path remains — `chain_append()` only adds, there is
|
||||
no removal function yet (Section V area A's identity-derived-offset work in Milestone 3 is a
|
||||
separate, later concern, not blocking here).
|
||||
**Hot-detach, done 2026-08-25 — 2h is complete.** `blk_subsys_detach_device()`
|
||||
(`block_subsystem.c`) walks `g.head` matching `slot->dev == dev`, refuses (`BLK_EINVAL`) unless
|
||||
the matched slot is the chain tail (this file's own architecture doc already argues USB/future
|
||||
devices are always last precisely so a removal never renumbers another slot's `start_lbn` — a
|
||||
mid-chain removal would corrupt everything after it, so this is refused outright rather than
|
||||
attempted), then unlinks, `g.total_user_lbn -= slot->user_blocks`, `blkio_close()`,
|
||||
`free(slot->bam)`, `free(slot)`. Deliberately discards rather than flushes any dirty cache/BAM/
|
||||
vol_meta state — the device is physically gone by the time this runs (called only after a real
|
||||
PORTSC disconnect), so a flush attempt cannot succeed; revisit if a future graceful-unmount path
|
||||
(as opposed to today's only path, a surprise removal) wants a best-effort flush first.
|
||||
|
||||
Trigger wiring mirrors the attach path's own shape: a new `bot_msc_attached` flag (`xhci_dev_t`)
|
||||
is set once `blk_subsys_attach_device()` actually succeeds (not by SET_CONFIGURATION itself —
|
||||
attach can fail, e.g. a bad capacity query, in which case there's nothing to detach later); the
|
||||
PORTSC disconnect handler sets `bot_msc_detach_pending` when `bot_msc_attached` is set, and
|
||||
`sk_repl_idle()` consumes it by calling `blk_subsys_detach_device()`. Deliberately hooked at
|
||||
disconnect itself, not Disable Slot completion — Disable Slot is only even issued when
|
||||
`connect_state == XHCI_CONN_IDLE` (see the existing "command ring busy" skip path a few lines
|
||||
above), so hooking there would silently miss a detach on a concurrent connect/disconnect pair.
|
||||
Unlike attach, detach needs no device round-trip (pure `block_subsystem.c` bookkeeping), so it
|
||||
doesn't strictly need `xhci_poll_events()`'s own call frame to have already returned — handled
|
||||
in `sk_repl_idle()` anyway, for shape symmetry and to keep `xhci.c` decoupled from
|
||||
`block_subsystem.c`.
|
||||
|
||||
**A second, more serious bug found live, past what advisor review alone caught.** Advisor
|
||||
flagged the real hazard correctly ahead of time: `vm->blk_vm_lbn[]`/`blk_vm_cbuf[]`
|
||||
(`block_words.c`'s VM block window, `BLOCK`/`BUFFER`/`UPDATE`'s cache) can go stale across a
|
||||
detach-then-reattach-at-the-same-LBN, since `block_subsystem.c`'s chain always appends at the
|
||||
current tail — and recommended a pointer-identity check in `blk_vm_load()`'s cache-hit path
|
||||
(re-resolve `blk_get_buffer()`, compare against the stored `cbuf`, treat a mismatch as a miss) as
|
||||
a minimal fix requiring no new API. That fix was implemented, then **directly falsified by its
|
||||
own designed-for-this test**: attach device A (blank), detach, re-attach device B (distinct
|
||||
content) at the identical LBN range, read that LBN — pointer comparison passed the check (i.e.
|
||||
called it a hit) and served **stale content from device A** anyway. Root cause: glibc's
|
||||
allocator hands `free(slot)` in `blk_subsys_detach_device()` straight back to the very next
|
||||
same-size `calloc(1, sizeof(*slot))` in `blk_subsys_attach_device()`, with nothing else allocated
|
||||
in between — confirmed live in this exact session, not inferred — so the "fresh" pointer and the
|
||||
stale one were bitwise identical despite belonging to two different physical devices. A pointer
|
||||
comparison cannot distinguish "still the same live device" from "a different device that
|
||||
happened to land at the same address" when the allocator is this deterministic.
|
||||
|
||||
Fixed properly with a monotonic `blk_subsys_epoch()` counter (`block_subsystem.c`, `uint64_t
|
||||
g.epoch`, bumped in `blk_subsys_attach_device()`, `blk_subsys_add_raw_device()`, and
|
||||
`blk_subsys_detach_device()`) that cannot be fooled by address reuse the way a pointer comparison
|
||||
was. `block_words.c` gained `blk_vm_check_epoch()`, called at the top of both `blk_vm_find()` and
|
||||
`blk_vm_flush_all()` (the only two functions reading `vm->blk_vm_lbn[]`/`vm->blk_vm_cbuf[]`
|
||||
directly rather than through `blk_vm_find()` first) — any epoch change since the VM's window was
|
||||
last validated discards every cached slot outright, no flush attempt, matching
|
||||
`blk_subsys_detach_device()`'s own "device is already gone" reasoning. `blk_vm_evict()`,
|
||||
`blk_vm_load()`'s and `blk_vm_assign()`'s own miss paths, and `block_word_update()` all route
|
||||
through `blk_vm_find()` first, so a single choke point covers every path that could otherwise
|
||||
read or write through a stale slot. A new `blk_vm_epoch` field on `VM` (`vm.h`) tracks the last
|
||||
validated epoch per VM; zero-initialised by `memset(vm, 0, sizeof(*vm))` at VM creation,
|
||||
consistent with `g.epoch` itself starting at 0 — no false invalidation on first use.
|
||||
|
||||
Verified live: attach `disk/usb-thumbdrive-test.img` (blank) at LBN 26074, read block 26074
|
||||
(populates the VM window cache with the blank content — critical, this is what actually exercises
|
||||
the cache-*hit* path rather than a miss), detach, re-attach `disk/usb-thumbdrive-test2.img` (new
|
||||
fixture, distinct repeating `HOTDETACH-REATTACH-FIXTURE-2026-08-25--` content, added this
|
||||
increment — see `disk/README.md`) at the identical LBN range, read block 26074 again. The second
|
||||
read correctly ran a full fresh TUR+READ10 cycle against the device (not a cached-hit shortcut)
|
||||
and `26074 BLOCK 64 TYPE CR` printed `HOTDETACH-REATTACH-FIXTURE-2026-08-25--...` — the new
|
||||
device's real content, not the stale zeros a working pointer-comparison fix would have kept
|
||||
serving. `logs/20260825-134747/amd64/` is that capture (also contains the `blk: detaching disk`
|
||||
line and the two `blk: disk ... LBN 26074..75184` attach lines, byte-identical between the two
|
||||
different backing devices, confirming the LBN-reuse premise itself). `logs/20260825-133413/amd64/`
|
||||
is the earlier run that caught the pointer-comparison bug in the first place — kept as evidence,
|
||||
not deleted, same as this project's established convention for a capture that found a real bug
|
||||
rather than just confirming success. All three architectures re-verified clean, nothing
|
||||
hot-attached: `logs/20260825-135600/amd64/`, `logs/20260825-140038/aarch64/`,
|
||||
`logs/20260825-140626/riscv64/`.
|
||||
|
||||
Milestone 2h is now complete: enumeration, BOT transport, TEST UNIT READY sequencing, READ
|
||||
CAPACITY(10), the `blkio_usb.c` backend, connect-time attach, and hot-detach are all live and
|
||||
verified on all three architectures. WRITE(10) remains unimplemented in the xHCI driver itself
|
||||
(2g's own still-open item) — a future increment, not blocking anything here.
|
||||
|
||||
### Milestone 3 — Block subsystem extensions (Section U items 3-6, Section V area A)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user