xHCI: drive Port Reset on port reuse; WIREBIND: kill the console VM too (FABRIC-3.md §XI.5)
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:
co-authored by
Claude Sonnet 5
parent
9ea5580ace
commit
b301317902
+79
-8
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user