Release a dead process's device claims (M17.1)

Every path out of a process (exit, fault, kill) now releases its device
claims alongside its IRQ and MSI bindings, so a restarted driver can claim
its hardware again — the cleanup half of process-lifecycle.md's iron rule 1.
MSI vectors were already swept by irq.releaseOwner; claims were the gap.
The claim-release test proves kill -> release -> re-claim, plus the broker
release in isolation.
This commit is contained in:
Daniel Samson
2026-07-12 23:23:49 +01:00
parent ed76cbbc79
commit 888eaa74e1
6 changed files with 92 additions and 5 deletions
+57
View File
@@ -132,6 +132,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
processKillTest(boot_information);
} else if (eql(case, "supervision")) {
supervisionTest(boot_information);
} else if (eql(case, "claim-release")) {
claimReleaseTest(boot_information);
} else if (eql(case, "initial-ramdisk")) {
initialRamdiskTest(boot_information);
} else if (eql(case, "vfs")) {
@@ -1454,6 +1456,61 @@ fn processKillTest(boot_information: *const BootInformation) void {
result();
}
/// M17.1: a dead process's device claims are released by the reap, so a restarted
/// driver can claim its hardware again (docs/process-lifecycle.md iron rule 1).
/// First the broker release in isolation — two owners, one released, the other's
/// claim must survive. Then the death-path wiring with a real child: the claim is
/// made on the child's behalf (the broker is kernel-callable), the child is
/// killed, and once the exit notification arrives — posted last, after release —
/// the device must be unclaimed and claimable again.
fn claimReleaseTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: claim-release\n", .{});
var buffer: [2]device_abi.DeviceDescriptor = undefined;
const total = devices_broker.enumerate(&buffer);
check("the device tree is seeded (>= 2 devices)", total >= 2);
if (total < 2) {
result();
return;
}
// The broker release in isolation.
check("device 0 claimed by owner 111", devices_broker.claim(0, 111));
check("device 1 claimed by owner 222", devices_broker.claim(1, 222));
devices_broker.releaseAllOwnedBy(111);
check("owner 111's claim is released", devices_broker.ownerOf(0) == null);
check("owner 222's claim survives", (devices_broker.ownerOf(1) orelse 0) == 222);
devices_broker.releaseAllOwnedBy(222);
check("cleanup released owner 222", devices_broker.ownerOf(1) == null);
// The death-path wiring: a real process dies holding a claim.
check("bootloader handed over /system/services/init", boot_information.init_len != 0);
if (boot_information.init_len == 0) {
result();
return;
}
const image = @as([*]const u8, @ptrFromInt(boot_handoff.physicalToVirtual(boot_information.init_base)))[0..boot_information.init_len];
const me = scheduler.currentId();
const endpoint = ipcsync.createIpcEndpoint() orelse {
check("exit endpoint allocated", false);
result();
return;
};
const child = process.spawnProcessSupervised(image, 4, &.{"/system/services/init"}, me, endpoint) catch 0;
check("supervised child spawned", child != 0);
check("device 0 claimed on the child's behalf", devices_broker.claim(0, child));
check("the kill is accepted", process.killProcess(me, child) == 0);
var badge: u64 = 0;
var received_cap: u64 = 0;
_ = ipcsync.replyWait(endpoint, 0, 0, 0, 0, abi.no_cap, &badge, &received_cap);
check("the exit notification arrived", badge == abi.notify_badge_bit | abi.notify_exit_bit | child);
check("death released the child's claim", devices_broker.ownerOf(0) == null);
check("the device is claimable again", devices_broker.claim(0, me));
devices_broker.releaseAllOwnedBy(me);
result();
}
/// The whole user-side surface at once: spawn process-test's supervisor role,
/// which — entirely from ring 3 — creates an exit endpoint, spawns its two
/// children supervised, sees them in process_enumerate, kills them (one blocked,