usbhubd: fix status-bitmap off-by-one + add initial full port scan

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).
This commit is contained in:
Red Bear OS
2026-07-19 09:46:05 +09:00
parent d7f31916bc
commit 9317d4874b
+22 -8
View File
@@ -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;
}