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
+76 -3
View File
@@ -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)
+1 -1
View File
@@ -1,5 +1,5 @@
# Capsule Block Manifest — Auto-generated
<!-- Generated by mkcapsule --manifest 2026-08-25T16:48:38Z -->
<!-- Generated by mkcapsule --manifest 2026-08-25T18:05:55Z -->
<!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. -->
<!-- Hand-written justifications and immutability notes live -->
<!-- in MANIFEST.md alongside this auto-generated index. -->
+13 -5
View File
@@ -58,11 +58,19 @@ carrying timestamp noise in git history.
- `usb-thumbdrive-test.img` — 64MB raw image backing a QEMU `usb-storage`
device attached to the xHCI controller's bus (`xhci0.0`) for Milestone 2e/
2h hotplug testing, added 2026-08-22. Not yet formatted with any
LithosAnanke/Artemis header — at this point in the driver's development
it only needs to exist as a backing store for a live Port Status Change
event; formatting comes once `blkio_usb.c` and `blk_subsys_attach_device()`
wiring exist (Milestone 2h).
2h hotplug testing, added 2026-08-22. Blank (all zero) — `blkio_usb.c` +
`blk_subsys_attach_device()` wiring (Milestone 2h, done 2026-08-25) attach
it as `BLK_FMT_PROVISIONAL` every time, which is the intended, exercised
state; not yet `BLK_FMT_FORMATTED` via `BLK-CONFIRM-FORMAT`.
- `usb-thumbdrive-test2.img` — 64MB raw image, added 2026-08-25 for
Milestone 2h hot-detach/re-attach verification. Filled with a repeating
`HOTDETACH-REATTACH-FIXTURE-2026-08-25--` ASCII pattern, deliberately
distinguishable from `usb-thumbdrive-test.img`'s all-zero content — the
point is proving a block read *after* detaching `usb-thumbdrive-test.img`
and re-attaching this one actually returns this pattern rather than
silently replaying the old device's cached (all-zero) content, which is
exactly the class of bug a same-LBN-range device swap can cause if the
VM block window cache (`vm->blk_vm_cbuf[]`) isn't re-validated on a hit.
**Convention, standing as of 2026-08-22: every virtual disk/thumb-drive image
used for testing — Artemis persistence disks above, and USB Mass Storage
File diff suppressed because one or more lines are too long
+20
View File
@@ -225,6 +225,26 @@ int blk_subsys_init(VM *vm, uint8_t *ram_base, size_t ram_size);
int blk_subsys_attach_device(struct blkio_dev *dev);
/* Milestone 2h hot-detach. Refuses (BLK_EINVAL) unless dev's slot is the
* current chain tail -- see this function's own doc comment in
* block_subsystem.c for why. Discards any dirty cache/BAM/vol_meta state
* rather than attempting to flush it (the device is already physically
* gone by the time this is called). Returns BLK_ENODEV if dev isn't
* attached, BLK_EINVAL if dev is NULL or not the chain tail.
*/
int blk_subsys_detach_device(struct blkio_dev *dev);
/* Monotonic counter, bumped on every attach/detach (Milestone 2h). A raw
* pointer comparison against a blk_get_buffer() result cannot reliably
* detect a same-address device swap (glibc's allocator can hand back the
* exact address just free()'d by a detach to the very next attach's
* calloc()) -- callers that cache a blk_get_buffer() result across calls
* (block_words.c's VM block window) must instead compare this epoch
* against the value they last observed, invalidating their whole cache on
* any change rather than trusting a stored pointer's identity.
*/
uint64_t blk_subsys_epoch(void);
int blk_subsys_shutdown(void);
uint8_t *blk_get_buffer(uint32_t block_num, int writable);
+20
View File
@@ -243,6 +243,26 @@ typedef struct {
* xhci_poll_events()" constraint) then blk_subsys_attach_device(). */
uint8_t bot_msc_attach_pending;
uint32_t bot_msc_attach_slot_id;
/* Set by sk_repl_idle() once blk_subsys_attach_device() actually
* succeeds (not by the SET_CONFIGURATION handler itself -- attach can
* still fail, e.g. a bad capacity query, in which case there is
* nothing to detach later). Read by the PORTSC disconnect handler
* below to decide whether this disconnect needs a block-subsystem
* detach at all -- a device that never successfully attached (or that
* was already detached) produces no spurious detach flag. */
uint8_t bot_msc_attached;
/* Set by the PORTSC disconnect handler (see xhci_poll_events()'s own
* disconnect handling) only when bot_msc_attached is set -- same
* flag+consume-in-sk_repl_idle() shape as bot_msc_attach_pending,
* chosen deliberately over hooking the Disable Slot completion:
* disconnect is the unambiguous signal, while Disable Slot is only
* even issued when connect_state == XHCI_CONN_IDLE (see the "command
* ring busy" skip path) and would silently miss a detach otherwise.
* No xhci_bot_wait_for_idle() call is needed for detach itself (no
* device round-trip -- it's local block_subsystem.c bookkeeping), but
* consuming it in sk_repl_idle() anyway matches the attach path's own
* shape and keeps xhci.c decoupled from block_subsystem.c. */
uint8_t bot_msc_detach_pending;
/* Deferred chaining: a doorbell ring (new control transfer) must
* never happen synchronously from inside xhci_poll_events()'s event-
+2
View File
@@ -460,6 +460,8 @@ typedef struct VM
uint8_t *blk_vm_cbuf[BLK_VM_SLOTS]; /* C buffer pointer from blk_get_buffer */
uint8_t blk_vm_dirty[BLK_VM_SLOTS]; /* 1 = vm->memory modified since last sync */
int blk_vm_next; /* Round-robin eviction cursor */
uint64_t blk_vm_epoch; /* blk_subsys_epoch() as of last cache validation --
* see block_words.c's blk_vm_find() */
/** @name Physics Hot-Words Cache
* @{
Binary file not shown.
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
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+64
View File
@@ -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
View File
@@ -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);
}
}
/*===========================================================================
+11
View File
@@ -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;
+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;