feat(usb): every xHCI controller is driven, not only the first

boards/BRINGUP.md A1, FABRIC-3 SXLII.  xhci.c keeps a table of controllers
(up to 8) and services each; kernel_main brings up index 0, 1, ... until
there is none.  repl.c's and blkio_usb.c's per-slot state is per
controller now (a slot ID is a controller's own), and each USB keyboard
keeps its own last report and lights; an unplugged keyboard releases what
it held.

Test first: keyboard and identity on a second controller behind a bridge,
Zuse on the first -- before, only Zuse; after, all three, on all three
ISAs.  Hot-plug and unplug on the second controller.  Acceptance unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rajames
2026-10-14 14:27:38 -04:00
co-authored by Claude Opus 5.5
parent 739cb9c163
commit e729c64ba3
27 changed files with 81701 additions and 260 deletions
+202 -186
View File
@@ -218,8 +218,6 @@ blkio_dev_t *sk_repl_get_artemis_usb_dev(void) {
* sk_word_blk_attach_ack() below (a real dictionary word, called from a
* completely different call stack than the idle loop) can resolve an
* incoming ack's raw pointer back to the slot it belongs to. */
static blkio_dev_t *g_usb_blk_dev_slots = (void *)0;
static uint32_t g_usb_blk_dev_slot_count = 0;
/* One pending entry per slot, indexed the same way msc_slots[]/
* usb_blk_dev_slots[] already are (index 0 unused, matches precedent).
@@ -240,7 +238,17 @@ typedef struct {
* virtio-blk path already does synchronously. */
int is_artemis;
} sk_blk_attach_pending_t;
static sk_blk_attach_pending_t *g_blk_attach_pending = (void *)0;
/* Per xHCI controller (2026-10-14): slot IDs are a controller's own, so
* slot 1 on one and slot 1 on another are different devices. Each entry
* holds that controller's blkio_dev_t per slot and its pending attaches,
* the controller being xhci_dev_at() of the same index. */
typedef struct {
blkio_dev_t *slots;
uint32_t count;
sk_blk_attach_pending_t *pending;
} sk_usb_ctrl_t;
static sk_usb_ctrl_t g_usb_ctrl[XHCI_MAX_CONTROLLERS];
/* BLK-ATTACH-ACK ( dev-addr ok? -- ): VM-EXEC'd into Hera by Artemis's
* own MSG-TICK once HERA-BLK-ATTACH-REQ's BLK-ATTACH call resolves.
@@ -270,21 +278,24 @@ static void sk_word_blk_attach_ack(VM *vm) {
return;
}
if (!g_usb_blk_dev_slots || !g_blk_attach_pending) return;
uint32_t found_slot = 0;
for (uint32_t i = 1; i < g_usb_blk_dev_slot_count; i++) {
if (&g_usb_blk_dev_slots[i] == dev) { found_slot = i; break; }
uint32_t found_ctrl = 0, found_slot = 0;
for (uint32_t c = 0; c < XHCI_MAX_CONTROLLERS && found_slot == 0; c++) {
if (!g_usb_ctrl[c].slots || !g_usb_ctrl[c].pending) continue;
for (uint32_t i = 1; i < g_usb_ctrl[c].count; i++) {
if (&g_usb_ctrl[c].slots[i] == dev) { found_ctrl = c; found_slot = i; break; }
}
}
if (found_slot == 0 || !g_blk_attach_pending[found_slot].pending) return;
g_blk_attach_pending[found_slot].pending = 0;
if (found_slot == 0) return;
sk_blk_attach_pending_t *pending = g_usb_ctrl[found_ctrl].pending;
if (!pending[found_slot].pending) return;
pending[found_slot].pending = 0;
if (!ok_flag) {
log_message(LOG_ERROR, "xhci: USB MSC block-subsystem attach failed");
return;
}
xhci_dev_t *xdev = xhci_get_dev();
xhci_dev_t *xdev = xhci_dev_at(found_ctrl);
xhci_msc_slot_t *ms = xdev ? xhci_msc_slot_for(xdev, found_slot) : (void *)0;
if (ms) ms->bot_msc_attached = 1;
g_attached_blk_dev = dev;
@@ -329,9 +340,9 @@ static void sk_word_blk_attach_ack(VM *vm) {
console_println(line);
}
homeblocks_sig_result_t sig_rc = g_blk_attach_pending[found_slot].sig_rc;
homeblocks_sig_t sig = g_blk_attach_pending[found_slot].sig;
int is_artemis = g_blk_attach_pending[found_slot].is_artemis;
homeblocks_sig_result_t sig_rc = pending[found_slot].sig_rc;
homeblocks_sig_t sig = pending[found_slot].sig;
int is_artemis = pending[found_slot].is_artemis;
if (is_artemis) {
/* FABRIC-3.md §XXVI follow-on: bus-agnostic Artemis discovery.
@@ -567,185 +578,189 @@ static void sk_repl_idle(VM *active_vm)
* own doc comment in xhci_driver.h for why: xhci_bot_wait_for_idle()'s
* 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();
/* FABRIC-3.md §VII (2026-09-05): was `static blkio_dev_t usb_blk_dev`
* ("single-device scope, matching the xHCI driver's own") -- now a
* per-slot registry, same sizing/allocation precedent as xhci_dev_t's
* own msc_slots[] (sized off xdev->max_slots, allocated once on first
* idle tick after xdev is known, since this file has no bringup-time
* hook of its own). Every slot with a pending attach/detach flag is
* serviced this tick, not just one -- a single `if` here used to mean
* a second device's pending flag would sit unnoticed until the first's
* flag was consumed and cleared. */
if (xdev && (!g_usb_blk_dev_slots || g_usb_blk_dev_slot_count < xdev->max_slots + 1)) {
size_t bytes = (size_t)(xdev->max_slots + 1) * sizeof(blkio_dev_t);
blkio_dev_t *fresh = (blkio_dev_t *)kmalloc_aligned(bytes, 64);
size_t pending_bytes = (size_t)(xdev->max_slots + 1) * sizeof(sk_blk_attach_pending_t);
sk_blk_attach_pending_t *fresh_pending = (sk_blk_attach_pending_t *)kmalloc_aligned(pending_bytes, 64);
if (fresh && fresh_pending) {
memset(fresh, 0, bytes);
memset(fresh_pending, 0, pending_bytes);
g_usb_blk_dev_slots = fresh;
g_usb_blk_dev_slot_count = xdev->max_slots + 1;
g_blk_attach_pending = fresh_pending;
/* Every controller in turn (2026-10-14): each has its own slots. */
for (uint32_t xc = 0; xc < xhci_dev_count() && xc < XHCI_MAX_CONTROLLERS; xc++) {
xhci_dev_t *xdev = xhci_dev_at(xc);
sk_usb_ctrl_t *uc = &g_usb_ctrl[xc];
/* FABRIC-3.md §VII (2026-09-05): was `static blkio_dev_t usb_blk_dev`
* ("single-device scope, matching the xHCI driver's own") -- now a
* per-slot registry, same sizing/allocation precedent as xhci_dev_t's
* own msc_slots[] (sized off xdev->max_slots, allocated once on first
* idle tick after xdev is known, since this file has no bringup-time
* hook of its own). Every slot with a pending attach/detach flag is
* serviced this tick, not just one -- a single `if` here used to mean
* a second device's pending flag would sit unnoticed until the first's
* flag was consumed and cleared. */
if (xdev && (!uc->slots || uc->count < xdev->max_slots + 1)) {
size_t bytes = (size_t)(xdev->max_slots + 1) * sizeof(blkio_dev_t);
blkio_dev_t *fresh = (blkio_dev_t *)kmalloc_aligned(bytes, 64);
size_t pending_bytes = (size_t)(xdev->max_slots + 1) * sizeof(sk_blk_attach_pending_t);
sk_blk_attach_pending_t *fresh_pending = (sk_blk_attach_pending_t *)kmalloc_aligned(pending_bytes, 64);
if (fresh && fresh_pending) {
memset(fresh, 0, bytes);
memset(fresh_pending, 0, pending_bytes);
uc->slots = fresh;
uc->count = xdev->max_slots + 1;
uc->pending = fresh_pending;
}
}
}
for (uint32_t slot_id = 1; xdev && g_usb_blk_dev_slots && slot_id <= xdev->max_slots; slot_id++) {
xhci_msc_slot_t *ms = xhci_msc_slot_for(xdev, slot_id);
if (!ms || !ms->bot_msc_attach_pending) continue;
ms->bot_msc_attach_pending = 0;
blkio_dev_t *usb_blk_dev = &g_usb_blk_dev_slots[slot_id];
for (uint32_t slot_id = 1; xdev && uc->slots && slot_id <= xdev->max_slots; slot_id++) {
xhci_msc_slot_t *ms = xhci_msc_slot_for(xdev, slot_id);
if (!ms || !ms->bot_msc_attach_pending) continue;
ms->bot_msc_attach_pending = 0;
blkio_dev_t *usb_blk_dev = &uc->slots[slot_id];
int rc = blkio_usb_open_msc(usb_blk_dev, xdev, slot_id);
if (rc == 0) {
/* FABRIC-2.md Milestone 4: warn on blank/foreign/unrecognized
* media -- the "warn" half. No "refuse" half yet: blkio_usb.c
* has no SCSI WRITE(10) support at all (Milestone 2's biggest
* open item), so there is no write path today to refuse --
* only read-only attach, which is also the general-purpose USB
* block I/O path this repo already relies on for unrelated
* testing, not exclusively a home-blocks identity workflow.
* Refusing attach on blank media here would break that
* legitimate use without protecting anything real yet. Refuse
* belongs on the write path, once WRITE(10) gives it something
* to gate.
*
* HOMEBLOCKS_SIG_START_FBLOCK (devblock 1): the real, final
* location -- GPT was dropped permanently, this is not an
* interim value (FABRIC-2.md §F.8/§F.13). */
homeblocks_sig_t sig;
homeblocks_sig_result_t sig_rc =
homeblocks_sig_check(usb_blk_dev, HOMEBLOCKS_SIG_START_FBLOCK, &sig);
switch (sig_rc) {
case HOMEBLOCKS_SIG_OK:
log_message(LOG_DEBUG, "xhci: USB drive recognized as a home-blocks drive");
/* FABRIC-3.md §VII (2026-09-05): g_homeblocks_dev/
* g_attached_blk_dev stay single "most recently
* attached" pointers by deliberate, scoped choice --
* the multi-device fix's target was the driver/backend
* corrupting each other's live state when two devices
* are attached at once (fixed above and in xhci.c/
* blkio_usb.c), not making every console-facing FORTH
* word (RUNCAP et al, mama_forth_words.c) multi-device
* aware -- the console still interacts with one device
* at a time, matching its own single-active-REPL
* design. Revisit if a real use case needs otherwise. */
g_homeblocks_dev = usb_blk_dev;
g_homeblocks_sig = sig;
g_homeblocks_sig_valid = 1;
break;
case HOMEBLOCKS_SIG_BLANK:
log_message(LOG_DEBUG, "xhci: USB drive not recognized (blank or foreign media) -- read-only general use only");
break;
case HOMEBLOCKS_SIG_BAD_VERSION:
log_message(LOG_WARN, "xhci: USB drive has a home-blocks header of an unrecognized version -- read-only general use only");
break;
case HOMEBLOCKS_SIG_BAD_CRC:
log_message(LOG_WARN, "xhci: USB drive has a home-blocks header that fails its checksum (corrupt or tampered) -- read-only general use only");
break;
case HOMEBLOCKS_SIG_READ_ERROR:
log_message(LOG_ERROR, "xhci: USB drive signature check failed to read the device -- read-only general use only");
break;
}
/* FABRIC-3.md §XXVI follow-on: a device isn't an identity
* thumbdrive (sig_rc above came back BLANK, i.e. no 'LAHB'
* magic) -- check whether it's Artemis's own disk instead
* (distinct 'ARTM' magic, same devblock-1 convention). Only
* attempted on BLANK, not on every device: a drive that
* already checked out as home-blocks (or failed a home-blocks
* version/CRC check) can't also be Artemis's disk, and
* skipping the second read keeps the common identity-drive
* case down to one signature check per attach, same as
* before this feature existed. */
int is_artemis_disk = 0;
if (sig_rc == HOMEBLOCKS_SIG_BLANK) {
artemis_sig_t asig;
artemis_sig_result_t art_rc = artemis_sig_check(usb_blk_dev, &asig);
if (art_rc == ARTEMIS_SIG_OK) {
log_message(LOG_DEBUG, "xhci: USB drive recognized as Artemis's own disk");
is_artemis_disk = 1;
int rc = blkio_usb_open_msc(usb_blk_dev, xdev, slot_id);
if (rc == 0) {
/* FABRIC-2.md Milestone 4: warn on blank/foreign/unrecognized
* media -- the "warn" half. No "refuse" half yet: blkio_usb.c
* has no SCSI WRITE(10) support at all (Milestone 2's biggest
* open item), so there is no write path today to refuse --
* only read-only attach, which is also the general-purpose USB
* block I/O path this repo already relies on for unrelated
* testing, not exclusively a home-blocks identity workflow.
* Refusing attach on blank media here would break that
* legitimate use without protecting anything real yet. Refuse
* belongs on the write path, once WRITE(10) gives it something
* to gate.
*
* HOMEBLOCKS_SIG_START_FBLOCK (devblock 1): the real, final
* location -- GPT was dropped permanently, this is not an
* interim value (FABRIC-2.md §F.8/§F.13). */
homeblocks_sig_t sig;
homeblocks_sig_result_t sig_rc =
homeblocks_sig_check(usb_blk_dev, HOMEBLOCKS_SIG_START_FBLOCK, &sig);
switch (sig_rc) {
case HOMEBLOCKS_SIG_OK:
log_message(LOG_DEBUG, "xhci: USB drive recognized as a home-blocks drive");
/* FABRIC-3.md §VII (2026-09-05): g_homeblocks_dev/
* g_attached_blk_dev stay single "most recently
* attached" pointers by deliberate, scoped choice --
* the multi-device fix's target was the driver/backend
* corrupting each other's live state when two devices
* are attached at once (fixed above and in xhci.c/
* blkio_usb.c), not making every console-facing FORTH
* word (RUNCAP et al, mama_forth_words.c) multi-device
* aware -- the console still interacts with one device
* at a time, matching its own single-active-REPL
* design. Revisit if a real use case needs otherwise. */
g_homeblocks_dev = usb_blk_dev;
g_homeblocks_sig = sig;
g_homeblocks_sig_valid = 1;
break;
case HOMEBLOCKS_SIG_BLANK:
log_message(LOG_DEBUG, "xhci: USB drive not recognized (blank or foreign media) -- read-only general use only");
break;
case HOMEBLOCKS_SIG_BAD_VERSION:
log_message(LOG_WARN, "xhci: USB drive has a home-blocks header of an unrecognized version -- read-only general use only");
break;
case HOMEBLOCKS_SIG_BAD_CRC:
log_message(LOG_WARN, "xhci: USB drive has a home-blocks header that fails its checksum (corrupt or tampered) -- read-only general use only");
break;
case HOMEBLOCKS_SIG_READ_ERROR:
log_message(LOG_ERROR, "xhci: USB drive signature check failed to read the device -- read-only general use only");
break;
}
}
/* FABRIC-2.md §F.20/§F.21 / §F.5/§F.23 (WIREBIND): Zuse
* genesis-mint/attach-authenticate and regular-identity
* verify-then-birth-then-pair both used to run synchronously,
* right here, before storage was even registered. Moved
* (Bob, 2026-09-07, "wait for ack, safer for identity data")
* to sk_word_blk_attach_ack() above, run only once Artemis
* confirms the storage-attach succeeded -- identity birth
* no longer happens on top of storage that might not have
* registered. Stash what that deferred call needs. */
if (g_blk_attach_pending) {
g_blk_attach_pending[slot_id].pending = 1;
g_blk_attach_pending[slot_id].sig_rc = sig_rc;
g_blk_attach_pending[slot_id].sig = sig;
g_blk_attach_pending[slot_id].is_artemis = is_artemis_disk;
}
/* FABRIC-3.md §XXVI follow-on: a device isn't an identity
* thumbdrive (sig_rc above came back BLANK, i.e. no 'LAHB'
* magic) -- check whether it's Artemis's own disk instead
* (distinct 'ARTM' magic, same devblock-1 convention). Only
* attempted on BLANK, not on every device: a drive that
* already checked out as home-blocks (or failed a home-blocks
* version/CRC check) can't also be Artemis's disk, and
* skipping the second read keeps the common identity-drive
* case down to one signature check per attach, same as
* before this feature existed. */
int is_artemis_disk = 0;
if (sig_rc == HOMEBLOCKS_SIG_BLANK) {
artemis_sig_t asig;
artemis_sig_result_t art_rc = artemis_sig_check(usb_blk_dev, &asig);
if (art_rc == ARTEMIS_SIG_OK) {
log_message(LOG_DEBUG, "xhci: USB drive recognized as Artemis's own disk");
is_artemis_disk = 1;
}
}
/* Storage-attach registration (blk_subsys_attach_device(),
* BLK-ATTACH C primitive) is Artemis's own domain now, not
* Hera's -- she keeps polling/sig-checking but no longer
* performs this step herself. Hera can't use her own
* MSG-SEND (see g_usb_blk_dev_slots's own doc comment
* above for why), so this is a direct VM-EXEC into
* Artemis's dictionary -- the same mechanism the MSG-TICK
* pump below already uses -- rather than a real enqueued
* message. HERA-BLK-ATTACH-REQ (capsules/artemis/init.4th)
* runs BLK-ATTACH then replies via her own real MSG-SEND,
* delivered back to Hera by the ordinary MSG-TICK pump. */
{
char cmd[96];
int n = snprintf(cmd, sizeof(cmd),
"S\" %llu HERA-BLK-ATTACH-REQ\" S\" Artemis\" VM-EXEC",
(unsigned long long)(uintptr_t)usb_blk_dev);
if (n > 0 && (size_t)n < sizeof(cmd)) {
vm_interpret((VM *)sk_get_mama_vm(), cmd);
/* FABRIC-2.md §F.20/§F.21 / §F.5/§F.23 (WIREBIND): Zuse
* genesis-mint/attach-authenticate and regular-identity
* verify-then-birth-then-pair both used to run synchronously,
* right here, before storage was even registered. Moved
* (Bob, 2026-09-07, "wait for ack, safer for identity data")
* to sk_word_blk_attach_ack() above, run only once Artemis
* confirms the storage-attach succeeded -- identity birth
* no longer happens on top of storage that might not have
* registered. Stash what that deferred call needs. */
if (uc->pending) {
uc->pending[slot_id].pending = 1;
uc->pending[slot_id].sig_rc = sig_rc;
uc->pending[slot_id].sig = sig;
uc->pending[slot_id].is_artemis = is_artemis_disk;
}
/* Storage-attach registration (blk_subsys_attach_device(),
* BLK-ATTACH C primitive) is Artemis's own domain now, not
* Hera's -- she keeps polling/sig-checking but no longer
* performs this step herself. Hera can't use her own
* MSG-SEND (see uc->slots's own doc comment
* above for why), so this is a direct VM-EXEC into
* Artemis's dictionary -- the same mechanism the MSG-TICK
* pump below already uses -- rather than a real enqueued
* message. HERA-BLK-ATTACH-REQ (capsules/artemis/init.4th)
* runs BLK-ATTACH then replies via her own real MSG-SEND,
* delivered back to Hera by the ordinary MSG-TICK pump. */
{
char cmd[96];
int n = snprintf(cmd, sizeof(cmd),
"S\" %llu HERA-BLK-ATTACH-REQ\" S\" Artemis\" VM-EXEC",
(unsigned long long)(uintptr_t)usb_blk_dev);
if (n > 0 && (size_t)n < sizeof(cmd)) {
vm_interpret((VM *)sk_get_mama_vm(), cmd);
}
}
}
}
}
/* 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).
* Per-slot loop now (FABRIC-3.md §VII, 2026-09-05), same reasoning as
* the attach loop above. */
for (uint32_t slot_id = 1; xdev && g_usb_blk_dev_slots && slot_id <= xdev->max_slots; slot_id++) {
xhci_msc_slot_t *ms = xhci_msc_slot_for(xdev, slot_id);
if (!ms || !ms->bot_msc_detach_pending) continue;
ms->bot_msc_detach_pending = 0;
blkio_dev_t *usb_blk_dev = &g_usb_blk_dev_slots[slot_id];
/* 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).
* Per-slot loop now (FABRIC-3.md §VII, 2026-09-05), same reasoning as
* the attach loop above. */
for (uint32_t slot_id = 1; xdev && uc->slots && slot_id <= xdev->max_slots; slot_id++) {
xhci_msc_slot_t *ms = xhci_msc_slot_for(xdev, slot_id);
if (!ms || !ms->bot_msc_detach_pending) continue;
ms->bot_msc_detach_pending = 0;
blkio_dev_t *usb_blk_dev = &uc->slots[slot_id];
blk_subsys_detach_device(usb_blk_dev);
if (g_homeblocks_dev == usb_blk_dev) {
g_homeblocks_dev = (void *)0;
g_homeblocks_sig_valid = 0;
blk_subsys_detach_device(usb_blk_dev);
if (g_homeblocks_dev == usb_blk_dev) {
g_homeblocks_dev = (void *)0;
g_homeblocks_sig_valid = 0;
}
if (g_attached_blk_dev == usb_blk_dev) {
g_attached_blk_dev = (void *)0;
}
/* FABRIC-2.md §F.10 decision 2 (UNCLEAN, closed alongside EJECT):
* the device is already gone -- no-op if WIREBIND never had
* anything tracked (general-purpose USB use, not a home-blocks
* identity drive), OR if the device that left wasn't the one
* WIREBIND tracks (FABRIC-3.md §VII follow-on, 2026-09-06 --
* genuine multi-device attach means it might be a different
* device leaving while a WIREBIND user's own stays attached). */
capsule_wirebind_unclean_detach(usb_blk_dev);
/* FABRIC-2.md §I.8, re-scoped 2026-09-04: Zuse logs out on device
* removal exactly like a WIREBIND user -- no-op if the device
* that just left wasn't hers (FABRIC-3.md §VII follow-on,
* 2026-09-06: that no-op is now real, see capsule_zuse_boot_
* logout()'s own updated doc comment). */
capsule_zuse_boot_logout((VM *)sk_get_mama_vm(), usb_blk_dev);
}
if (g_attached_blk_dev == usb_blk_dev) {
g_attached_blk_dev = (void *)0;
}
/* FABRIC-2.md §F.10 decision 2 (UNCLEAN, closed alongside EJECT):
* the device is already gone -- no-op if WIREBIND never had
* anything tracked (general-purpose USB use, not a home-blocks
* identity drive), OR if the device that left wasn't the one
* WIREBIND tracks (FABRIC-3.md §VII follow-on, 2026-09-06 --
* genuine multi-device attach means it might be a different
* device leaving while a WIREBIND user's own stays attached). */
capsule_wirebind_unclean_detach(usb_blk_dev);
/* FABRIC-2.md §I.8, re-scoped 2026-09-04: Zuse logs out on device
* removal exactly like a WIREBIND user -- no-op if the device
* that just left wasn't hers (FABRIC-3.md §VII follow-on,
* 2026-09-06: that no-op is now real, see capsule_zuse_boot_
* logout()'s own updated doc comment). */
capsule_zuse_boot_logout((VM *)sk_get_mama_vm(), usb_blk_dev);
}
/* FABRIC-0.md/FABRIC-1.md Section V item 6: "a cheap 'anything dirty?
@@ -779,7 +794,7 @@ static void sk_repl_idle(VM *active_vm)
* based relocation) vs. deliberately left open (overflow-triggered
* migration, needs a call site threaded from WIREBIND).
*
* FABRIC-3.md, 2026-09-09: skipped while any g_blk_attach_pending
* FABRIC-3.md, 2026-09-09: skipped while any g_usb_ctrl[].pending
* entry is still pending -- this scan and the storage-attach message
* round-trip (HERA-BLK-ATTACH-REQ/BLK-ATTACH-ACK) both touch the
* block subsystem/Artemis's own virtio-backed storage, and live-
@@ -791,10 +806,11 @@ static void sk_repl_idle(VM *active_vm)
* with nothing to do. */
{
int attach_in_flight = 0;
if (g_blk_attach_pending) {
for (uint32_t c = 0; c < XHCI_MAX_CONTROLLERS && !attach_in_flight; c++) {
uint32_t pi;
for (pi = 0; pi < g_usb_blk_dev_slot_count; pi++) {
if (g_blk_attach_pending[pi].pending) { attach_in_flight = 1; break; }
if (!g_usb_ctrl[c].pending) continue;
for (pi = 0; pi < g_usb_ctrl[c].count; pi++) {
if (g_usb_ctrl[c].pending[pi].pending) { attach_in_flight = 1; break; }
}
}
if (!attach_in_flight) blk_migration_idle_check();