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:
Robert Allan James
2026-08-25 14:10:05 -04:00
co-authored by Claude Sonnet 5
parent 3b085dd875
commit af267a52a6
22 changed files with 46979 additions and 13 deletions
+32 -1
View File
@@ -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;