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
@@ -152,6 +152,19 @@ static struct {
|
||||
uint64_t total_user_lbn; /* total user-visible LBNs across RAM + all device slots */
|
||||
blk_dev_slot_t *head; /* linked list of device slots */
|
||||
|
||||
/* Bumped on every attach/detach (Milestone 2h). Freeing a slot then
|
||||
* immediately allocating a new one for a same-LBN re-attach can hand
|
||||
* back the *same* heap address (confirmed live: glibc's allocator does
|
||||
* exactly this for a free() followed immediately by a same-size
|
||||
* calloc(), with nothing else allocated in between) -- so a raw
|
||||
* pointer comparison against a cached blk_get_buffer() result cannot
|
||||
* reliably detect "this LBN's device changed underneath a caller
|
||||
* holding a stale cached pointer." A monotonic epoch can't be fooled
|
||||
* by address reuse the way a pointer comparison was found to be (see
|
||||
* block_words.c's blk_vm_find(), the one place outside this file that
|
||||
* caches a blk_get_buffer() result across calls). */
|
||||
uint64_t epoch;
|
||||
|
||||
int initialized;
|
||||
} g = {0};
|
||||
|
||||
@@ -533,6 +546,7 @@ int blk_subsys_add_raw_device(uint8_t *buf, uint32_t nblocks) {
|
||||
|
||||
chain_append(slot);
|
||||
g.total_user_lbn += nblocks;
|
||||
g.epoch++;
|
||||
|
||||
log_message(LOG_INFO, "blk: raw device LBN %u..%u (%u blocks)",
|
||||
slot->start_lbn, slot->start_lbn + nblocks - 1, nblocks);
|
||||
@@ -562,6 +576,7 @@ int blk_subsys_attach_device(struct blkio_dev *dev) {
|
||||
|
||||
chain_append(slot);
|
||||
g.total_user_lbn += slot->user_blocks;
|
||||
g.epoch++;
|
||||
|
||||
log_message(LOG_INFO,
|
||||
"blk: disk '%s' v2 LBN %u..%u (%u user blocks); "
|
||||
@@ -576,6 +591,55 @@ int blk_subsys_attach_device(struct blkio_dev *dev) {
|
||||
return BLK_OK;
|
||||
}
|
||||
|
||||
/* Milestone 2h hot-detach. Deliberately refuses anything but the current
|
||||
* chain tail: block_subsystem.c's own architecture doc (top of this file)
|
||||
* has USB/future devices as the *last* link specifically so a removal
|
||||
* never has to renumber any other slot's start_lbn -- a mid-chain removal
|
||||
* would corrupt every later slot's LBN range, so this is refused outright
|
||||
* rather than attempted.
|
||||
*
|
||||
* 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 --
|
||||
* pretending to try would just call blkio_write() against a vanished
|
||||
* device for no benefit. Revisit if a future graceful-unmount path (as
|
||||
* opposed to a surprise removal) wants a best-effort flush first; today
|
||||
* every removal this driver can observe is a surprise removal.
|
||||
*/
|
||||
int blk_subsys_detach_device(struct blkio_dev *dev) {
|
||||
if (!g.initialized) return BLK_ENODEV;
|
||||
if (!dev) return BLK_EINVAL;
|
||||
|
||||
blk_dev_slot_t *prev = NULL;
|
||||
blk_dev_slot_t *s = g.head;
|
||||
while (s && s->dev != dev) { prev = s; s = s->next; }
|
||||
if (!s) return BLK_ENODEV;
|
||||
|
||||
if (s->next) {
|
||||
log_message(LOG_WARN, "blk: refusing detach of non-tail device (LBN %u..%u)",
|
||||
s->start_lbn, s->start_lbn + s->user_blocks - 1);
|
||||
return BLK_EINVAL;
|
||||
}
|
||||
|
||||
log_message(LOG_INFO, "blk: detaching disk '%s' LBN %u..%u (%u user blocks)",
|
||||
s->vol_meta.label, s->start_lbn, s->start_lbn + s->user_blocks - 1,
|
||||
s->user_blocks);
|
||||
|
||||
if (prev) prev->next = NULL; else g.head = NULL;
|
||||
g.total_user_lbn -= s->user_blocks;
|
||||
g.epoch++;
|
||||
|
||||
blkio_close(dev);
|
||||
if (s->bam) free(s->bam);
|
||||
free(s);
|
||||
|
||||
return BLK_OK;
|
||||
}
|
||||
|
||||
uint64_t blk_subsys_epoch(void) {
|
||||
return g.epoch;
|
||||
}
|
||||
|
||||
int blk_subsys_shutdown(void) {
|
||||
if (!g.initialized) return BLK_OK;
|
||||
|
||||
|
||||
+17
-3
@@ -98,18 +98,32 @@ static void sk_repl_idle(void)
|
||||
* busy-wait -- which blkio_usb_open_msc() uses internally -- must
|
||||
* never run from inside xhci_poll_events()'s own call frame). */
|
||||
xhci_dev_t *xdev = xhci_get_dev();
|
||||
static blkio_dev_t usb_blk_dev; /* single-device scope, matching the xHCI
|
||||
* driver's own; referenced by both the
|
||||
* attach and detach handling below. */
|
||||
if (xdev && xdev->bot_msc_attach_pending) {
|
||||
xdev->bot_msc_attach_pending = 0;
|
||||
uint32_t slot_id = xdev->bot_msc_attach_slot_id;
|
||||
|
||||
static blkio_dev_t usb_blk_dev;
|
||||
int rc = blkio_usb_open_msc(&usb_blk_dev, xdev, slot_id);
|
||||
if (rc == 0) {
|
||||
blk_subsys_attach_device(&usb_blk_dev);
|
||||
if (rc == 0 && blk_subsys_attach_device(&usb_blk_dev) == BLK_OK) {
|
||||
xdev->bot_msc_attached = 1;
|
||||
} else {
|
||||
console_println("xhci: USB MSC block-subsystem attach failed");
|
||||
}
|
||||
}
|
||||
|
||||
/* Milestone 2h hot-detach: the device disconnected (PORTSC, inside the
|
||||
* xhci_poll_events() call above) after having actually attached.
|
||||
* blk_subsys_detach_device() is local block_subsystem.c bookkeeping --
|
||||
* no device round-trip, so it wouldn't strictly need to run outside
|
||||
* xhci_poll_events()'s own call frame -- but handling it here anyway
|
||||
* matches the attach path's shape and keeps xhci.c decoupled from
|
||||
* block_subsystem.c (see bot_msc_detach_pending's own doc comment). */
|
||||
if (xdev && xdev->bot_msc_detach_pending) {
|
||||
xdev->bot_msc_detach_pending = 0;
|
||||
blk_subsys_detach_device(&usb_blk_dev);
|
||||
}
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
|
||||
@@ -327,6 +327,8 @@ int xhci_bringup(xhci_dev_t *dev)
|
||||
dev->bot_cap_block_size = 0;
|
||||
dev->bot_msc_attach_pending = 0;
|
||||
dev->bot_msc_attach_slot_id = 0;
|
||||
dev->bot_msc_attached = 0;
|
||||
dev->bot_msc_detach_pending = 0;
|
||||
dev->next_action = XHCI_NEXT_ACTION_NONE;
|
||||
dev->next_action_slot_id = 0;
|
||||
dev->next_action_length = 0;
|
||||
@@ -1141,6 +1143,15 @@ void xhci_poll_events(void)
|
||||
* the Disable Slot command below can be issued
|
||||
* right now. */
|
||||
dev->port_slot_id[port_id - 1] = 0;
|
||||
/* Milestone 2h hot-detach: fire on the disconnect
|
||||
* itself, independent of whether Disable Slot can
|
||||
* be sent right now -- see bot_msc_detach_pending's
|
||||
* own doc comment for why this point, not Disable
|
||||
* Slot completion. */
|
||||
if (dev->bot_msc_attached) {
|
||||
dev->bot_msc_attached = 0;
|
||||
dev->bot_msc_detach_pending = 1;
|
||||
}
|
||||
if (dev->connect_state == XHCI_CONN_IDLE) {
|
||||
dev->pending_disable_slot_id = disconnecting_slot_id;
|
||||
dev->connect_state = XHCI_CONN_AWAIT_DISABLE_SLOT;
|
||||
|
||||
@@ -155,8 +155,34 @@ void empty_all_buffers(VM *vm) {
|
||||
|
||||
/* --- Block I/O window helpers ----------------------------------------- */
|
||||
|
||||
/* Discard the whole VM block window if the device chain has changed since
|
||||
* it was last validated (Milestone 2h). A device hot-detach followed by a
|
||||
* later re-attach can reuse both the exact same LBN range
|
||||
* (block_subsystem.c's chain always appends at the current tail) *and* the
|
||||
* exact same blk_get_buffer() return address (confirmed live: glibc's
|
||||
* allocator hands the just-freed slot straight back to the very next
|
||||
* same-size calloc()), so neither LBN nor a cached pointer is a reliable
|
||||
* "still the same device" signal on its own. Any epoch change discards
|
||||
* every cached slot outright -- no flush attempt, matching
|
||||
* blk_subsys_detach_device()'s own reasoning: whatever device the stale
|
||||
* content belonged to may already be gone by the time this runs. Called at
|
||||
* the top of every function below that reads vm->blk_vm_lbn[]/
|
||||
* vm->blk_vm_cbuf[] directly, not just blk_vm_find() -- blk_vm_flush_all()
|
||||
* walks the same arrays without going through blk_vm_find() first. */
|
||||
static void blk_vm_check_epoch(VM *vm) {
|
||||
uint64_t epoch = blk_subsys_epoch();
|
||||
if (epoch == vm->blk_vm_epoch) return;
|
||||
for (int i = 0; i < BLK_VM_SLOTS; i++) {
|
||||
vm->blk_vm_lbn[i] = 0;
|
||||
vm->blk_vm_cbuf[i] = NULL;
|
||||
vm->blk_vm_dirty[i] = 0;
|
||||
}
|
||||
vm->blk_vm_epoch = epoch;
|
||||
}
|
||||
|
||||
/* Find the slot holding lbn; return slot index or -1 if not loaded. */
|
||||
static int blk_vm_find(VM *vm, uint32_t lbn) {
|
||||
blk_vm_check_epoch(vm);
|
||||
for (int i = 0; i < BLK_VM_SLOTS; i++) {
|
||||
if (vm->blk_vm_lbn[i] == lbn && vm->blk_vm_cbuf[i] != NULL)
|
||||
return i;
|
||||
@@ -240,8 +266,13 @@ static vaddr_t blk_vm_assign(VM *vm, uint32_t lbn) {
|
||||
|
||||
/* Sync all dirty slots to their C buffers and flush the subsystem.
|
||||
* Re-resolve each buffer pointer by LBN rather than trusting the stored
|
||||
* one -- see blk_vm_evict for why a stored pointer can go stale. */
|
||||
* one -- see blk_vm_evict for why a stored pointer can go stale. Checks
|
||||
* the epoch first (Milestone 2h, see blk_vm_check_epoch()) since this
|
||||
* function walks vm->blk_vm_lbn[]/vm->blk_vm_cbuf[] directly rather than
|
||||
* through blk_vm_find() -- a stale-epoch dirty slot must be discarded, not
|
||||
* flushed onto whatever device now owns that LBN. */
|
||||
static void blk_vm_flush_all(VM *vm) {
|
||||
blk_vm_check_epoch(vm);
|
||||
for (int i = 0; i < BLK_VM_SLOTS; i++) {
|
||||
if (vm->blk_vm_cbuf[i] != NULL && vm->blk_vm_dirty[i]) {
|
||||
vaddr_t base = BLK_VM_WINDOW_BASE + (vaddr_t)i * BLOCK_SIZE;
|
||||
|
||||
Reference in New Issue
Block a user