From 9317d4874ba0ed9d50bc8ebc6fb1beffb6a04cb4 Mon Sep 17 00:00:00 2001 From: Red Bear OS Date: Sun, 19 Jul 2026 09:46:05 +0900 Subject: [PATCH] usbhubd: fix status-bitmap off-by-one + add initial full port scan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- drivers/usb/usbhubd/src/main.rs | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/drivers/usb/usbhubd/src/main.rs b/drivers/usb/usbhubd/src/main.rs index ec4efec6d2..5edab889dc 100644 --- a/drivers/usb/usbhubd/src/main.rs +++ b/drivers/usb/usbhubd/src/main.rs @@ -390,16 +390,30 @@ fn main() { // only poll GetPortStatus for ports whose bit is set. // If EP1 is unavailable, fall back to polling all ports. // - // Bitmap size: ceil(ports / 8) bytes. - // Port N is bit (N-1) of byte (N-1)/8. + // Bitmap size: ceil((ports + 1) / 8) bytes — bit 0 is the hub itself, + // bit N is port N (USB 2.0 spec 11.12.4; Linux hub_irq uses bit i for + // port i). The previous revision used bit (N-1) for port N, an + // off-by-one that processed every event on the wrong port. const POLL_FALLBACK_MS: u64 = 200; - let bitmap_size = (ports as usize + 7) / 8; + let bitmap_size = (ports as usize + 8) / 8; let mut bitmap = vec![0u8; bitmap_size]; + // Mask with bits 1..=ports set (every real port; bit 0 = hub, skipped). + let all_ports_mask: u64 = ((1u64 << ports) - 1) << 1; + + // Linux hub_activate() performs a full port scan before arming the + // status-change endpoint — devices already connected at hub power-on + // must be enumerated even if no interrupt ever arrives (a blocking + // first EP1 read would otherwise hang the daemon before any scan). + let mut first_scan = true; + loop { // Build a port-change mask. - // Bit N set = port (N+1) needs processing. - let changed: u64 = if let Some(ref mut ep) = intr_ep_handle { + // Bit N set = port N needs processing (bit 0 = hub status change). + let changed: u64 = if first_scan { + first_scan = false; + all_ports_mask + } else if let Some(ref mut ep) = intr_ep_handle { match ep.transfer_read(&mut bitmap) { Ok(_) => { let mut mask = 0u64; @@ -412,16 +426,16 @@ fn main() { } Err(e) => { log::warn!("usbhubd: interrupt transfer failed ({}), falling back to poll", e); - (1u64 << ports) - 1 + all_ports_mask } } } else { // Polling mode: process all ports. thread::sleep(time::Duration::from_millis(POLL_FALLBACK_MS)); - (1u64 << ports) - 1 + all_ports_mask }; for port in 1..=ports { - let bit = 1u64 << (port - 1); + let bit = 1u64 << port; if (changed & bit) == 0 { continue; }