establishment: a reporter's death reaps its subtree, and the re-report rebuilds it

P3 of docs/establishment-planes-plan.md — the restart-zombie fix. A class
driver cannot observe its provider's death: an HID driver blocks on
interrupt reports that will simply never come, and storage answers its
callers with refusals forever. Worse, the dead generation's still-used
entries made the matcher's dedupe refuse the respawn when the restarted bus
re-reported — the subtree was a permanent zombie, which is the exact
opposite of the restart-a-driver-live goal the driver model exists for.

pruneChildrenOf now reaps: each pruned child's bound driver is killed and
its entry cleared (the exit notification finds no entry, so the death is
never double-counted; its device returns by the loan rule; its stored
endpoint handle is closed). The re-report then spawns a fresh generation
whose hellos fetch the successor's channel.

The usb-report drill now asserts the subtree WORKS after the restart: the
respawned storage opens its device on the NEW bus instance and reads block
0. Discrimination: against the pre-reap manager the drill fails — no reap
line, no post-restart respawn (the survivors were zombies), verified by a
stash run. Note the scenario boots no input service, so the HID drivers of
BOTH generations exit after their input lookup times out — storage is the
functional proof.
This commit is contained in:
Daniel Samson
2026-08-09 12:34:13 +01:00
parent 1d7850239d
commit 8710944a92
2 changed files with 41 additions and 4 deletions
@@ -212,16 +212,41 @@ fn addChild(parent: u64, bus_address: u64, identity: u64, device_id: u64, report
/// protocol state (slots, rings) that died with the process — keeping the nodes
/// would be keeping a lie. The restarted instance rediscovers and re-reports.
/// Watchers hear the honest story: removed now, added again on rediscovery.
///
/// **A reporter's death reaps its subtree.** The class drivers spawned for the
/// pruned children hold channels into the dead process; they cannot observe
/// the death themselves (an HID driver blocks on interrupt reports that will
/// simply never come — a silent zombie), and their still-`used` entries would
/// make the matcher's dedupe refuse the respawn when the re-report arrives.
/// So each is killed and its entry cleared: the re-report spawns a fresh
/// instance, whose hello fetches the successor's channel. That is the restart
/// story working — kill a bus driver and only its subtree blinks
/// (docs/establishment-planes-plan.md P3).
fn pruneChildrenOf(reporter: u32) void {
for (&children) |*child| {
if (child.used and child.reporter == reporter) {
std.log.info("child removed (device {d} port {d})", .{ child.parent, child.bus_address });
reapDriverBoundTo(child.device_id);
child.used = false;
Serve.publish(.child_removed, 0, .{ .parent = child.parent, .bus_address = child.bus_address });
}
}
}
/// Kill the driver bound to a pruned child and clear its entry — the exit
/// notification that follows finds no entry and is ignored, so the death is
/// never double-counted, and the freed entry is what lets the re-report
/// respawn. The device it was given returns to us by the kernel's loan rule.
fn reapDriverBoundTo(device_id: u64) void {
const driver = driverEntryForDevice(device_id) orelse return;
if (driver.process_id != 0) {
std.log.info("reaping {s} (its provider died)", .{driver.name()});
_ = process.kill(driver.process_id);
}
if (driver.endpoint) |endpoint| _ = ipc.close(endpoint);
driver.* = .{};
}
/// How many children a driver instance has reported (the test-usb-restart
/// trigger counts these).
fn childCountOf(reporter: u32) u32 {