usb: a hub yanked from a root port takes its subtree with it (H2)

The hot-plug matrix's predicted bug, found on first contact: tearDownPort
never recursed into a departing hub's children — only tearDownHubDevice
(a hub leaving one level down) did. Yank a populated hub from a root port
and the downstream slots stayed live against vanished hardware, their class
drivers were never reaped, and the replugged hub found its port still
occupied, so nothing ever re-enumerated: the subtree was gone for the boot.

tearDownPort now recurses children-first, exactly like tearDownHubDevice.
The usb-hub-yank case is the discrimination: one device_del removes a hub
carrying a keyboard AND a mouse, both drivers must be reaped, and the
re-added hub must rebind both — it failed against the unfixed bus and
passes with the recursion.
This commit is contained in:
Daniel Samson
2026-08-09 14:08:15 +01:00
parent a6a3402d92
commit 77e7001878
2 changed files with 36 additions and 0 deletions
@@ -490,6 +490,14 @@ fn deviceIsHub(usb_device: *const library.Device) bool {
fn tearDownPort(manager: ipc.Handle, engine: *library.Controller, port: u32) void {
const usb_device = engine.deviceOnPort(port) orelse return;
std.log.info("port {d} disconnected", .{port});
// A hub yanked from a root port takes its whole subtree with it — children
// first, recursively, exactly as tearDownHubDevice does for a hub leaving
// one level down. Without this, the downstream slots stayed live against
// vanished hardware, their class drivers were never reaped, and the
// replugged hub found its port still occupied — nothing re-enumerated.
if (usb_device.is_hub) {
while (engine.nextChildOf(usb_device.slot_id, 0)) |child| tearDownHubDevice(manager, engine, child);
}
for (usb_device.interfaces[0..usb_device.interface_count]) |*interface| {
if (interface.registered_device_id == 0) continue;
reportRemoved(manager, (@as(u64, port) << 8) | interface.number, .{ .port = port, .interface = interface.number });