xHCI/BOT driver: genuine multi-device support (FABRIC-3.md §VII)
Build / build-amd64-iso (push) Canceled after 0s
Build / build-aarch64-iso (push) Canceled after 0s
Build / build-riscv64-img (push) Canceled after 0s

Per-slot registry (xhci_msc_slot_t/dev->msc_slots, sized off the
controller's own reported max_slots) replaces the single-device scalar
fields the driver carried since Milestones 2e-2h. Boot-time port scan no
longer stops at the first connected device; a connect/disconnect that
arrives while the Command Ring is busy is now queued and drained instead
of dropped. blkio_usb.c and repl.c's own single-device state (device
descriptor buffers, blkio_dev_t, attach bookkeeping) became per-slot
registries the same way.

Live multi-device testing (not just compiling) surfaced a second, more
severe bug outside the original plan: transfer_purpose and next_action
were also single scalars shared across the whole controller. Two devices
enumerating concurrently could have one's completion silently overwrite
the other's still-outstanding one, permanently stalling it with no error.
Fixed by moving both per-slot and, critically, reading the Transfer Event
TRB's own real Slot ID field instead of trusting external bookkeeping.

Verified live, all three architectures, mandatory clean-qemu acceptance:
existing single-device path unchanged, and two devices attached
simultaneously (amd64) both progress independently through enumeration
without corrupting or stalling each other.

Also in this pass (implemented and verified in earlier turns this
session, committed together per direct instruction):
- Headless-until-login console policy: no prompt/banner until a real
  identity logs in via an attached thumbdrive (WIREBIND or Zuse, neither
  special), reusing EMERGENCY_CONSOLE_ENABLED as the debug/recovery
  escape hatch (now default-off).
- KILL/g_repl_active_vm dangling-pointer fix: killing the VM the console
  is currently USE'd onto now detaches back to Hera first, matching the
  existing EJECT/UNCLEAN precedent.

FABRIC-3.md §VII/§VIII carry full closure notes for all three.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018EjXFo7mPXjUMjfJeuUUz4
This commit is contained in:
Robert Allan James
2026-09-05 22:14:14 -04:00
co-authored by Claude Sonnet 5
parent cc6fcb6a0b
commit 9e81de3f43
55 changed files with 144832 additions and 572 deletions
+103 -20
View File
@@ -37,6 +37,7 @@
#include "starkernel/arch.h"
#include "starkernel/xhci_driver.h"
#include "starkernel/blkio_usb.h"
#include "starkernel/kmalloc.h"
#include "starkernel/homeblocks_sig.h"
#include "starkernel/capsule_birth.h"
#include "starkernel/capsule_zuse_boot.h"
@@ -96,6 +97,19 @@ static VM *g_repl_active_vm = (void *)0;
void sk_repl_set_active_vm(VM *vm) { g_repl_active_vm = vm; }
VM *sk_repl_get_active_vm(void) { return g_repl_active_vm; }
/*===========================================================================
* Headless-until-login gate, decided 2026-09-05: no console for the
* running system unless a thumbdrive is present. One shared flag, set by
* either login path (capsule_wirebind.c's regular-user console-VM birth,
* capsule_zuse_boot.c's own attach/genesis-mint) -- neither is special,
* per direct instruction. See repl.h's own doc comments.
*===========================================================================*/
static int g_console_login_occurred = 0;
void sk_console_mark_login(void) { g_console_login_occurred = 1; }
int sk_console_login_occurred(void) { return g_console_login_occurred; }
/*===========================================================================
* Currently attached home-blocks device: mirrors g_repl_active_vm's own
* shape (FABRIC-2.md §F.9's own precedent for this exact accessor). Set
@@ -228,14 +242,33 @@ static void sk_repl_idle(VM *active_vm)
* 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;
/* 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. */
static blkio_dev_t *usb_blk_dev_slots = (void *)0;
static uint32_t usb_blk_dev_slot_count = 0;
if (xdev && (!usb_blk_dev_slots || 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);
if (fresh) {
memset(fresh, 0, bytes);
usb_blk_dev_slots = fresh;
usb_blk_dev_slot_count = xdev->max_slots + 1;
}
}
for (uint32_t slot_id = 1; xdev && 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 = &usb_blk_dev_slots[slot_id];
int rc = blkio_usb_open_msc(&usb_blk_dev, xdev, 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
@@ -254,11 +287,22 @@ static void sk_repl_idle(VM *active_vm)
* 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);
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");
g_homeblocks_dev = &usb_blk_dev;
/* 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;
@@ -284,7 +328,7 @@ static void sk_repl_idle(VM *active_vm)
* be detected before the REPL's own idle polling exists to
* detect it). No-ops immediately if Zuse already has a real
* identity this boot. */
capsule_zuse_boot_try_attach(&usb_blk_dev, sig_rc, &sig, (VM *)sk_get_mama_vm());
capsule_zuse_boot_try_attach(usb_blk_dev, sig_rc, &sig, (VM *)sk_get_mama_vm());
/* FABRIC-2.md §F.5/§F.23 (WIREBIND): the real thumbdrive-
* attach call site for a regular (non-Zuse) identity --
@@ -295,12 +339,12 @@ static void sk_repl_idle(VM *active_vm)
* itself no-ops for a genesis-mode Zuse drive (no cert
* region) or before Zuse has authenticated this boot. */
if (sig_rc == HOMEBLOCKS_SIG_OK) {
capsule_wirebind_try_attach(&usb_blk_dev, &sig, (VM *)sk_get_mama_vm());
capsule_wirebind_try_attach(usb_blk_dev, &sig, (VM *)sk_get_mama_vm());
}
}
if (rc == 0 && blk_subsys_attach_device(&usb_blk_dev) == BLK_OK) {
xdev->bot_msc_attached = 1;
g_attached_blk_dev = &usb_blk_dev;
if (rc == 0 && blk_subsys_attach_device(usb_blk_dev) == BLK_OK) {
ms->bot_msc_attached = 1;
g_attached_blk_dev = usb_blk_dev;
} else {
log_message(LOG_ERROR, "xhci: USB MSC block-subsystem attach failed");
}
@@ -312,15 +356,21 @@ static void sk_repl_idle(VM *active_vm)
* 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);
if (g_homeblocks_dev == &usb_blk_dev) {
* 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 && 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 = &usb_blk_dev_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;
}
if (g_attached_blk_dev == &usb_blk_dev) {
if (g_attached_blk_dev == usb_blk_dev) {
g_attached_blk_dev = (void *)0;
}
@@ -576,6 +626,25 @@ int sk_console_getkey(VM *active_vm)
}
}
/* sk_repl_headless_wait - see repl.h's own doc comment. Same idle-service
* shape as sk_console_getkey() above, minus the key-reading entirely: no
* banner, no prompt, no console_getc()/readline of any kind -- this is
* exactly the "no console for the running system unless a thumbdrive is
* present" boundary, decided 2026-09-05. Exits the moment
* sk_console_login_occurred() becomes true. */
void sk_repl_headless_wait(VM *mama)
{
while (!sk_console_login_occurred()) {
heartbeat_service();
uint64_t now = heartbeat_ticks();
if (now - g_last_beat_tick >= SK_IDLE_BEAT_INTERVAL) {
g_last_beat_tick = now;
sk_repl_idle(mama);
}
arch_relax();
}
}
/* ?TERMINAL's real body (sf_terminal_ready(), shim.c): non-blocking peek --
* a single poll, no idle-servicing loop (a false result must return
* immediately, not block). Buffers a found byte in g_console_pending_key so
@@ -911,8 +980,16 @@ int sk_repl_step(VM *vm)
vm->abort_requested = 0;
if (vm->error) {
#if EMERGENCY_CONSOLE_ENABLED
console_puts(" ERROR\n");
vm->error = 0;
#else
/* Wired 2026-09-05: sk_fault_handler() existed but was never
* called from here -- the "halts VM on error" half of this
* function's own doc comment was aspirational, not real, until
* now. Real for the headless-until-login default. */
sk_fault_handler(vm);
#endif
} else {
console_puts(" ok\n");
}
@@ -955,8 +1032,14 @@ void sk_repl_run(VM *vm)
active->abort_requested = 0;
if (active->error) {
#if EMERGENCY_CONSOLE_ENABLED
console_puts(" ERROR\n");
active->error = 0;
#else
/* Wired 2026-09-05, same as sk_repl_step()'s matching branch
* above -- sk_fault_handler() existed but was never called. */
sk_fault_handler(active);
#endif
} else {
console_puts(" ok\n");
}