From 77e70018780c1e66e1ea06b12103083a6115abd4 Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Sun, 9 Aug 2026 14:08:15 +0100 Subject: [PATCH] usb: a hub yanked from a root port takes its subtree with it (H2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- system/drivers/usb-xhci-bus/usb-xhci-bus.zig | 8 ++++++ test/qemu_test.py | 28 ++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/system/drivers/usb-xhci-bus/usb-xhci-bus.zig b/system/drivers/usb-xhci-bus/usb-xhci-bus.zig index c6bbcf2..b85f586 100644 --- a/system/drivers/usb-xhci-bus/usb-xhci-bus.zig +++ b/system/drivers/usb-xhci-bus/usb-xhci-bus.zig @@ -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 }); diff --git a/test/qemu_test.py b/test/qemu_test.py index e6d35c0..d66334d 100644 --- a/test/qemu_test.py +++ b/test/qemu_test.py @@ -1009,6 +1009,34 @@ CASES = [ r"[\s\S]*device-manager: delegated device \d+ to /system/drivers/usb-hid-keyboard" r"[\s\S]*usb-hid-keyboard: ok)", "fail": r"DANOS-TEST-RESULT: FAIL"}, + # Hot-plug matrix H2 (docs/hot-plug-matrix-plan.md): yank a POPULATED hub. + # One device_del removes the hub with a keyboard and a mouse behind it — the + # bus's recursive teardown (tearDownHubDevice, children first) must report + # every downstream interface removed, the manager must reap BOTH bound + # drivers, and re-adding the hub and both devices must rebuild the subtree. + # The reaping lines only exist post-yank, so `reaping X ... X: ok` chains + # are unambiguously second-generation. + {"name": "usb-hub-yank", + "build_case": "usb-hid", + "smp": 4, + "timeout": 150, + "qemu_extra": ["-device", "qemu-xhci,id=xhci2", + "-device", "usb-hub,bus=xhci2.0,port=1,id=yhub", + "-device", "usb-kbd,bus=xhci2.0,port=1.1,id=ykbd", + "-device", "usb-mouse,bus=xhci2.0,port=1.2,id=ymouse"], + "qmp_sequence": [ + {"delay": 8, "command": "device_del", "arguments": {"id": "yhub"}}, + {"delay": 14, "command": "device_add", + "arguments": {"driver": "usb-hub", "bus": "xhci2.0", "port": "1", "id": "yhub2"}}, + {"delay": 17, "command": "device_add", + "arguments": {"driver": "usb-kbd", "bus": "xhci2.0", "port": "1.1", "id": "ykbd2"}}, + {"delay": 18, "command": "device_add", + "arguments": {"driver": "usb-mouse", "bus": "xhci2.0", "port": "1.2", "id": "ymouse2"}}, + ], + "expect": r"(?s)(?=.*hub device slot \d+ disconnected)" + r"(?=.*device-manager: reaping \S*usb-hid-keyboard[\s\S]*usb-hid-keyboard: ok)" + r"(?=.*device-manager: reaping \S*usb-hid-mouse[\s\S]*usb-hid-mouse: ok)", + "fail": r"DANOS-TEST-RESULT: FAIL"}, # The kernel VFS root (M-F): the mount table serves the initrd at /system — # path resolution, node status/read (an ELF magic), and directory listing, # asserted kernel-side.