usb: don't spin on a SuperSpeed hub's change bits; log hub port status

Real-hardware finding: a SuperSpeed hub (the user's Genesys, 4 SS
downstream ports) HUNG the bus driver at boot — it stopped logging with
no fault, and the hub's port power dropped. Cause: a SuperSpeed hub has
change bits a USB2 hub lacks (link-state, BH-reset), and the B4 servicing
— written and tested against QEMU's USB2 hub — never cleared them, so
the hub's status-change endpoint re-reported the same port forever and
the driver spun servicing it, never reaching the USB2 companion hub
where the full-speed keyboard actually lives.

hubPortStatusAck now clears the SuperSpeed-only change features too
(gated on hub speed; a USB2 hub is unaffected), and the tick services a
bounded batch of hub changes (32) before yielding — so even if a hub's
change bits misbehave, the driver cannot spin the machine. A per-port
status log line surfaces exactly what a hub reports, so the next real
boot shows the SS hub's port states and whether the USB2 companion
enumerates. QEMU usb-hub cases still green (the SS clears are gated off
for QEMU's USB2 hub).
This commit is contained in:
Daniel Samson 2026-07-21 22:44:46 +01:00
parent 7ee6033fa4
commit 27f87cb5ba
No known key found for this signature in database
GPG Key ID: A9EB2589C60F7268
2 changed files with 17 additions and 0 deletions

View File

@ -256,6 +256,7 @@ fn hubPortKey(hub_slot: u8, port: u16) u32 {
fn bringUpBehindHub(manager: runtime.ipc.Handle, engine: *library.Controller, hub: *library.Device, port: u16) void { fn bringUpBehindHub(manager: runtime.ipc.Handle, engine: *library.Controller, hub: *library.Device, port: u16) void {
const status = engine.hubPortStatusAck(hub, port) orelse return; const status = engine.hubPortStatusAck(hub, port) orelse return;
const connected = library.Controller.hubPortConnected(status); const connected = library.Controller.hubPortConnected(status);
std.log.info("hub slot {d} port {d} status 0x{x:0>8} ({s})", .{ hub.slot_id, port, status, if (connected) "connected" else "empty" });
const existing = engine.deviceOnHubPort(hub, port); const existing = engine.deviceOnHubPort(hub, port);
if (!connected) { if (!connected) {
@ -501,9 +502,15 @@ fn onNotification(badge: u64) void {
// Downstream hub-port changes (docs/usb-hub.md): a device connected on a // Downstream hub-port changes (docs/usb-hub.md): a device connected on a
// hub's downstream port is enumerated and registered here, so a keyboard // hub's downstream port is enumerated and registered here, so a keyboard
// behind a hub reaches its class driver like one on a root port. // behind a hub reaches its class driver like one on a root port.
// Cap per tick: even if a hub's change bits refuse to clear, the driver
// must not spin here it services a bounded batch and yields to the
// next tick (and to storage, input, everything else).
var serviced: u32 = 0;
while (engine.takeHubChange()) |change| { while (engine.takeHubChange()) |change| {
const manager = manager_handle orelse break; const manager = manager_handle orelse break;
bringUpBehindHub(manager, engine, change.hub, change.port); bringUpBehindHub(manager, engine, change.hub, change.port);
serviced += 1;
if (serviced >= 32) break;
} }
while (engine.takeReport()) |report| { while (engine.takeReport()) |report| {
var message = transfer.InterruptReport{ var message = transfer.InterruptReport{

View File

@ -951,8 +951,18 @@ pub const Controller = struct {
/// bits (so it can signal again). Returns the wPortStatus word. /// bits (so it can signal again). Returns the wPortStatus word.
pub fn hubPortStatusAck(self: *Controller, hub: *Device, port: u16) ?u32 { pub fn hubPortStatusAck(self: *Controller, hub: *Device, port: u16) ?u32 {
const status = self.readHubPortStatus(hub, port) orelse return null; const status = self.readHubPortStatus(hub, port) orelse return null;
// Acknowledge every latched change bit. A SuperSpeed hub has extra ones
// (link-state, BH-reset) beyond a USB2 hub's connection/reset leaving
// any set makes the hub's status-change endpoint re-report the same port
// forever, spinning the driver (a real SuperSpeed hub hung boot here;
// QEMU's USB2 hub has none of these). Clearing an inapplicable feature
// is harmless (the hub STALLs it and we move on).
_ = self.controlTransfer(hub, hubreq.clearPortFeature(hubreq.feature_c_port_connection, port), &.{}, false); _ = self.controlTransfer(hub, hubreq.clearPortFeature(hubreq.feature_c_port_connection, port), &.{}, false);
_ = self.controlTransfer(hub, hubreq.clearPortFeature(hubreq.feature_c_port_reset, port), &.{}, false); _ = self.controlTransfer(hub, hubreq.clearPortFeature(hubreq.feature_c_port_reset, port), &.{}, false);
if (hub.speed >= 4) {
_ = self.controlTransfer(hub, hubreq.clearPortFeature(hubreq.feature_c_port_link_state, port), &.{}, false);
_ = self.controlTransfer(hub, hubreq.clearPortFeature(hubreq.feature_c_bh_port_reset, port), &.{}, false);
}
return status; return status;
} }