xHCI: drive Port Reset on port reuse; WIREBIND: kill the console VM too (FABRIC-3.md §XI.5)
Build / build-amd64-iso (push) Waiting to run
Build / build-aarch64-iso (push) Waiting to run
Build / build-riscv64-img (push) Waiting to run

Two independent bugs that together caused a reliable hotplug wedge:
reusing an xHCI port for a second identity right after an unclean
detach of a first would leave no further hotplug events reaching the
guest at all.

Bug 1 (xhci.c/xhci_driver.h): the xHCI driver never drove PORTSC.PR --
a known, named gap since Milestone 2e (the code's own comment flagged
it, PORTSC_PR/PRC were defined but never referenced). A port's first
connect each boot reads PED already set, so skipping the reset
happened to work; a second device on the same port after a prior
disconnect reads PED clear, and Address Device reliably failed without
an explicit reset cycle. New XHCI_CONN_AWAIT_PORT_RESET state drives
PR and waits for PED to read set before proceeding to Enable Slot.

Bug 2 (capsule_wirebind.c): capsule_wirebind_eject()/unclean_detach()
compared g_repl_active_vm against the *user* VM's pointer
(g_wirebind_attached_vm_id tracks that one, not the console VM) --
never equal, since USE/g_repl_active_vm always points at the console
VM. The guard never fired and the console VM was never killed at all,
only orphaned -- paired to a dead user VM but still the REPL's active
session. New wirebind_teardown_console() helper resolves and tears
down the console VM by its own tracked bare username.

Verified live on amd64: the exact repro (identity 01 on port 2,
unclean detach, identity 02 on the same port immediately after) --
previously wedged with "xhci: address device failed" and no further
hotplug activity; now attaches cleanly and fast, both VMs' KILL
messages appear, console is immediately interactive on the new
identity. Three-arch clean qemu acceptance passed, all clean on the
first attempt.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ec88YKxxhZGG1RNnune78
This commit is contained in:
Robert Allan James
2026-09-09 22:11:09 -04:00
co-authored by Claude Sonnet 5
parent 9ea5580ace
commit b301317902
14 changed files with 36161 additions and 38 deletions
+79 -8
View File
@@ -1907,11 +1907,82 @@ noted below): fast attach, `USE`, a standard word (`1 2 + .` / `9 9 * .`) comput
and `VM-EXEC` (ACL-denied for this restricted personality) recovering gracefully — "VM-EXEC:
ERROR in `NN~user`", console stays interactive, no halt — for every one of the 7.
**Noted but out of scope, not investigated further:** attaching a second identity's drive to
the same xHCI port within the same boot, immediately after an unclean (`device_del`, no `EJECT`
word) detach of a first identity, reliably wedged that port — no further hotplug events reached
the guest at all for any identity attached there afterward, console stuck on the dead first
identity's own prompt. Working around it (fresh port per identity, or a fresh boot per identity)
avoided it entirely and was sufficient for this verification pass; root cause (xHCI port-state
settling, `g_wirebind_attached_*`/`g_repl_active_vm` single-active-binding assumptions colliding
with a genuinely unclean detach, or both) is unexamined.
**Noted but out of scope, not investigated further at the time:** attaching a second identity's
drive to the same xHCI port within the same boot, immediately after an unclean (`device_del`, no
`EJECT` word) detach of a first identity, reliably wedged that port — no further hotplug events
reached the guest at all for any identity attached there afterward, console stuck on the dead
first identity's own prompt. Working around it (fresh port per identity, or a fresh boot per
identity) avoided it entirely and was sufficient for that verification pass. Root-caused and
fixed the same day — see §XI.5.
### XI.5 — The reused-port hotplug wedge: two real, independent bugs, both CLOSED
Investigated on request after §XI.4 flagged it. Two separate bugs, one in the xHCI driver, one
in WIREBIND's own teardown bookkeeping — both had to be present for the wedge to look as total
as it did; fixing either alone would have helped, fixing both closes it.
**Bug 1 — the xHCI driver never drove Port Reset (PORTSC.PR).** `xhci_handle_port_connected()`
(`src/starkernel/usb/xhci.c`) went straight from detecting a port's Current Connect Status to
Enable Slot -> Address Device, with no reset cycle in between. This was a known, named gap, not
a new regression -- the function's own comment (since Milestone 2e) already said "USB2 needs
software to drive PORTSC.PR and wait for PRC/PED before the device will respond to addressing --
not yet known which this driver's ports need." It logged whether `PED` (Port Enabled/Disabled)
was set but never acted on it. `XHCI_PORTSC_PR`/`XHCI_PORTSC_PRC` were defined in
`include/starkernel/xhci.h` from the start and never referenced anywhere in `xhci.c` -- the gap
was real and literally unused code sitting right there.
Why it only bit on port *reuse*: a device's first connect to a port each boot reads `PED` already
set (QEMU's emulated root hub presents it that way), so skipping the reset happened to work. After
a device disconnects, a *new* device on that same port reads `PED` clear -- per the xHCI/USB spec,
`Address Device` requires the port to be in the Enabled state, which only a completed Port Reset
produces. Without driving one, `Address Device` reliably failed -- confirmed live before the fix,
matching this exact log line every time:
```
[HADES][ERROR] xhci: address device failed
```
Fix: a new `XHCI_CONN_AWAIT_PORT_RESET` connect_state (`include/starkernel/xhci_driver.h`).
`xhci_handle_port_connected()` now checks `PED` on every connect: already set -> straight to
Enable Slot as before (the common, first-connect case, unchanged); clear -> write `PORTSC.PR`
(preserving `PP`, same discipline the existing CSC-ack write already used) and wait. The reset
lands as a fresh port-status-change event with `CCS` still set (device never left) -- the same
function handles that continuation: `PED` now set means the reset completed, proceed to Enable
Slot; still clear means keep waiting for the next event (no explicit timeout, matching this
driver's existing convention of relying on the poll cadence rather than hard deadlines). Both
`xhci_poll_events()`'s and `xhci_scan_ports_for_already_connected()`'s trailing PORTSC ack writes
now also acknowledge `PRC`, not just `CSC` (RW1C, unconditional like the existing `CSC` ack --
writing 1 to an already-clear bit is a no-op).
**Bug 2 — `capsule_wirebind_eject()`/`capsule_wirebind_unclean_detach()` compared against the
wrong VM, so the console VM was never cleaned up.** Both functions resolve their `entry` via
`wirebind_resolve_attached()`, which tracks `g_wirebind_attached_vm_id` -- documented, correctly,
as *the user VM's id* ("the user VM, not the console VM" -- `capsule_wirebind_try_attach()`'s own
comment). But `USE`/`g_repl_active_vm` always points at the *console* VM (`capsule_console_birth`,
registered under the bare username, e.g. `"01"`), never the user VM (`"01~user"`). So both
functions' `sk_repl_get_active_vm() == entry.vm_ptr` guard compared the console VM against the
user VM's pointer -- never equal, so the guard never fired, `g_repl_active_vm` was never cleared,
and -- worse -- the console VM was never killed at all (only `entry.name`, the user VM, was --
`capsule_vm_kill(entry.name)`). The console VM was left alive and orphaned, paired to a now-dead
user VM, and (because the guard never cleared it) still the REPL's own active session. This is
why a fresh WIREBIND attach for a second identity, even once Bug 1 stopped blocking the hotplug
itself, would have kept showing the first identity's dead-paired prompt rather than switching to
the new one.
Fix (`capsule_wirebind.c`): a new shared `wirebind_teardown_console()` helper, called from both
`capsule_wirebind_eject()` and `capsule_wirebind_unclean_detach()` before they clear
`g_wirebind_attached_username`. It resolves the console VM by that bare username (the field these
functions already track for exactly this purpose, kept separate from the user-VM-keyed
`g_wirebind_attached_vm_id` from the start), clears `g_repl_active_vm` if it currently points at
that console VM, and kills it -- the guard now compares like against like, and the console VM is
actually torn down, not just orphaned.
**Verified live, amd64:** the exact repro from §XI.4's note -- identity `01` attached on xHCI
port 2, unclean-detached (`device_del`, no `EJECT`), identity `02` attached on the *same* port 2
immediately after. Before this fix: wedged, no further hotplug events, "address device failed"
in the log. After: `02` attached cleanly and fast, log shows both `KILL: 01 dead` (the console
VM, new) and `KILL: 01~user dead` (the user VM, already worked) on `01`'s teardown, no "address
device failed" anywhere, and the console was immediately `USE`-able and fully interactive on
`02` (arithmetic computed correctly, `VM-EXEC` denial recovered gracefully per §XI.4's fix,
console stayed responsive). Three-arch `clean qemu` acceptance (amd64/aarch64/riscv64) passed,
all three clean on the first attempt.