The diagnostic milestones used to root-cause the intermittent boot
'freeze' (attach_device milestones, usbhubd HUBFLOW decision points,
reactor/enumerator startup) moved from info to debug! so production
logs stay clean. One-line-per-boot startup confirmations (scheme
registered, daemon ready, reactor started, enumerator started, reactor
dispatch mode, enumerator running) remain at info as genuinely useful
low-volume startup evidence.
SetPortFeature(PORT_INDICATOR) was issued unconditionally for every
connected+enabled port. QEMU's usb-hub does not advertise indicator
support and NAKs the request indefinitely; with no control-transfer
timeout the daemon hung before the post-enable debounce, so devices
behind the hub never attached. Gate on wHubCharacteristics bit 7
(HUB_CHAR_PORTIND, USB 2.0 Table 11-13), matching Linux hub_configure's
has_indicators check.
Log SetPortFeature(PORT_RESET) result, wait_for_reset's first fetch,
and its completion/timeout outcomes at info level to pinpoint where
hub port-reset polling stalls in QEMU.
Post-debounce state, pre-attach state, and attach() result logged at
info level to pinpoint where hub-child enumeration stalls in QEMU.
Diagnostic; level to be revisited.
The debounce's inner clear of C_PORT_CONNECTION could hang or be
ignored by the hub, and our control transfers have no timeout — so a
literal port of Linux's clear-inside-the-loop design deadlocked hub
enumeration: the debounce never converged and the reset/attach path was
never reached (observed on QEMU as usbhubd logging the port's first
status with connection_changed=true and then going silent forever).
Judge the 100ms stable window on the connection STATE alone (the same
stability property hub_port_debounce_be_connected protects): a stuck or
hung change bit can no longer deadlock enumeration — it only
re-triggers a poll, and is cleared once, best-effort, after a
successful attach (matching Linux port_event's C_PORT_CONNECTION
clear). New regression test debounce_stuck_change_bit_still_converges
locks the deadlock scenario; 15/15 tests pass.
Root cause of hub-child enumeration stalling: after a synchronous
action (power-on, reset) the port's state changes WITHOUT a new
interrupt ever being generated, and the loop's next step was a blocking
EP1 transfer_read with no timeout — the daemon never woke to observe
the state it had just caused, so devices behind a hub sat
enabled-but-never-attached forever.
Move the blocking transfer_read onto a worker thread that forwards each
interrupt bitmap as a u64 mask over an mpsc channel. The main loop
waits with recv_timeout(POLL_FALLBACK_MS): a real bitmap processes only
the changed ports (interrupt fast path), while timeout/disconnect/error
falls back to an all-ports poll (progress guarantee). Matches the Linux
model where hub_irq is the accelerator but port status reads are always
authoritative.
Two latent bugs exposed by test-usb-hub-qemu.sh against a real hub
topology (QEMU usb-hub with usb-kbd behind it):
1. Status-change bitmap was indexed off by one: the loop checked
bit (port - 1), but USB 2.0 spec 11.12.4 defines bit 0 = hub status
change and bit N = port N status change (Linux hub_irq uses bit i
for port i). Every change was processed on the wrong port — a device
on port N was never enumerated (its bit was read as port N+1).
2. The event loop's first action was a blocking EP1 interrupt read;
when the hub delivers no immediate status-change interrupt the
daemon hung before ever scanning a port. Linux hub_activate()
performs a full port scan before arming the status-change endpoint;
the first iteration now forces an all-ports mask with the same
semantics. Also widened the fallback all-ports mask to bits 1..=ports
(bit 0 = hub, skipped).
Port the Linux 7.1 hub.c port state machine into usbhubd, replacing the
minimal connect/reset handling:
New module port_ops.rs (pure logic, side effects injected, 14 unit
tests):
- debounce_until_connected: hub_port_debounce_be_connected() port
(hub.c:4696-4737) — 25ms polls, connection stable for 100ms, 2s
budget, connection-change bit cleared in-loop.
- wait_for_reset: hub_port_wait_reset() port (hub.c:2953-3047) — 10ms
polls until RESET clears with CONNECTION set, escalate to 200ms after
two short waits, 800ms budget; then 50ms TRSTRCY recovery (hub.c:3159)
and C_PORT_RESET clear. Replaces the previous bare sleep(10ms).
- wait_for_u0: USB 3.0 polling→U0 wait after port power-on — 36ms steps,
400ms ceiling (tPollingLFPSTimeout = 360ms; Linux hub.c:1226 debounce
path).
- accumulate_hub_delay_ns: wHubDelay chain rule (hub.c:1507-1519:
wHubDelay + parent->hub_delay + 40ns, cap 65535ns).
main.rs wiring:
- Port status normalized to PortStatusSnapshot (decouples the state
machine from the V2/V3 wire formats; V3 link state extracted from
bits 8:5).
- Debounce on connection-change before attach; C_PORT_ENABLE cleared
once handled (Linux port_event semantics).
- Reset path uses wait_for_reset instead of sleep(10ms).
- USB 3 power-on path waits for U0 before proceeding.
- wHubDelay: ancestor-chain walk fetching USB 3 ancestor hub
descriptors, accumulated per Linux; delivered to newly attached
SuperSpeed children via SET_ISOCH_DELAY (USB 3.0 9.4.11; Linux
message.c:1142 — hubs and non-SS skipped, children inherit the hub's
accumulated delay verbatim per hub.c:5128-5129).
- attach/detach failure logs now identify the port.
hub.rs (xhcid usb module):
- HubDescriptorV3 extended with device_removable: u16 — the SS hub
descriptor is 12 bytes (spec Table 10-15); the old struct under-read
by 2 bytes. Stale TODO corrected: SS descriptors have no
PortPwrCtrlMask (that is USB 2.0-only, still unparsed).
Verified: cargo check clean (0 usbhubd warnings), 14/14 usbhubd tests,
xhcid unaffected (43/43 tests).