From 4a2df587bbc813d6320e89f64ba2c24b4c2a419c Mon Sep 17 00:00:00 2001 From: Daniel Samson <12231216+daniel-samson@users.noreply.github.com> Date: Sun, 9 Aug 2026 13:51:07 +0100 Subject: [PATCH] establishment: unplug reaps like death, so a replug rebinds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hot-unplug path (onChildRemoved, a report from a live bus) cleared the child but left the bound class driver: a process blocked on reports that will never come, whose stale entry made the matcher's dedupe refuse the respawn when the device was plugged back in — the same wall the restart zombie hit, one path over. Unplug now reaps exactly like reporter death. The usb-hub-unplug case grows the replug: device_del the hub keyboard, then device_add it back (qmp_sequence); the ordered tail — child removed, reaping, delegated, ok — can only be satisfied by the second generation, since every boot keyboard's ok precedes the unplug. Discrimination: without the reap the replug never rebinds and the case times out (verified by stash run). Hub family, restart drill, and the two-controller proof all green (8/8). --- .../device-manager/device-manager.zig | 6 +++++ test/qemu_test.py | 26 ++++++++++++++----- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/system/services/device-manager/device-manager.zig b/system/services/device-manager/device-manager.zig index edf0b1f..94a6666 100644 --- a/system/services/device-manager/device-manager.zig +++ b/system/services/device-manager/device-manager.zig @@ -706,6 +706,12 @@ fn onChildRemoved(_: void, invocation: Invocation(device_manager_protocol.ChildR for (&children) |*child| { if (child.used and child.parent == report.parent and child.bus_address == report.bus_address and child.reporter == invocation.sender) { std.log.info("child removed (device {d} port {d})", .{ child.parent, child.bus_address }); + // Unplug reaps exactly like reporter death (pruneChildrenOf): the + // bound driver's device is gone and it cannot observe that — it + // blocks on reports that will never come — and its stale entry + // would make the dedupe refuse the respawn when the device is + // PLUGGED BACK IN. Reap now, and a replug's re-report rebinds. + reapDriverBoundTo(child.device_id); child.used = false; status = 0; } diff --git a/test/qemu_test.py b/test/qemu_test.py index ed4d059..b4dfa0a 100644 --- a/test/qemu_test.py +++ b/test/qemu_test.py @@ -955,10 +955,16 @@ CASES = [ "expect": r"(?s)(?=.*hub slot \d+ port \d+ device:.*0x0409)" r"(?=.*usb-hid-keyboard: ok \(device 3)", "fail": r"DANOS-TEST-RESULT: FAIL"}, - # Hub-downstream disconnect (B4c): device_del the keyboard behind the hub; - # the hub's status-change endpoint reports it, the device is torn down - # (ChildRemoved + Disable Slot). QEMU's hub DOES raise downstream changes - # (unlike root-port hot-plug). + # Hub-downstream disconnect AND replug (B4c + establishment): device_del the + # keyboard behind the hub — the hub's status-change endpoint reports it, the + # device is torn down (ChildRemoved + Disable Slot), and the manager REAPS + # the bound class driver (it blocks on reports that will never come, and its + # stale entry would make the dedupe refuse the respawn). Then device_add + # plugs it back: the re-report re-registers the same identity and the + # matcher spawns a fresh driver, which binds — the ordered tail (reaping → + # child removed → delegated → ok) can only be satisfied by the SECOND + # generation, since the boot keyboards' ok lines all precede the unplug. + # QEMU's hub DOES raise downstream changes (unlike root-port hot-plug). {"name": "usb-hub-unplug", "build_case": "usb-hid", "smp": 4, @@ -966,9 +972,17 @@ CASES = [ "qemu_extra": ["-device", "qemu-xhci,id=xhci2", "-device", "usb-hub,bus=xhci2.0,port=1", "-device", "usb-kbd,bus=xhci2.0,port=1.1,id=dkbd"], - "qmp_after": {"delay": 8, "command": "device_del", "arguments": {"id": "dkbd"}}, + "qmp_sequence": [ + {"delay": 8, "command": "device_del", "arguments": {"id": "dkbd"}}, + {"delay": 14, "command": "device_add", + "arguments": {"driver": "usb-kbd", "bus": "xhci2.0", "port": "1.1", "id": "dkbd2"}}, + ], "expect": r"(?s)(?=.*usb-hid-keyboard: ok \(device 3)" - r"(?=.*slot \d+ disconnected)", + r"(?=.*slot \d+ disconnected" + r"[\s\S]*device-manager: child removed" + r"[\s\S]*device-manager: reaping \S*usb-hid-keyboard" + 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"}, # 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,