diff --git a/docs/bounds-track-plan.md b/docs/bounds-track-plan.md index 8a893a0..49e6968 100644 --- a/docs/bounds-track-plan.md +++ b/docs/bounds-track-plan.md @@ -125,7 +125,7 @@ Two things fall out rather than being special-cased: | Step | What | |---|---| | E1 | Record a giver per device; `device_transfer` and the spawn grant set it — **done** | -| E2 | On task death a device reverts to its giver if alive, else its claim clears | +| E2 | On task death a device reverts to its giver if alive, else its claim clears — **done** | | E3 | `device_claim` refuses a device that has a giver | | E4 | The manager claims every resource-bearing device at boot, so nothing is left takeable | | E5 | The attacker fixture gains the claim half it has been waiting for since D2 | diff --git a/system/kernel/devices-broker.zig b/system/kernel/devices-broker.zig index 0b2b10e..7b16b73 100644 --- a/system/kernel/devices-broker.zig +++ b/system/kernel/devices-broker.zig @@ -272,11 +272,41 @@ pub fn ownerOf(id: u64) ?u32 { /// hardware again (docs/process-lifecycle.md iron rule 1: cleanup is the kernel's /// job). The devices stay in the table — they describe hardware, which did not go /// away — only their ownership clears. +/// Whether a task is still alive, injected by the process layer (which owns the task +/// table) the same way the scheduler's other hooks are. Null means "assume not", so a +/// kernel built without it clears claims rather than handing them to a ghost. +pub var task_alive_hook: ?*const fn (u32) bool = null; + +fn alive(task: u32) bool { + const hook = task_alive_hook orelse return false; + return hook(task); +} + pub fn releaseAllOwnedBy(owner: u32) void { - for (claimed[0..count]) |*slot| { - if (slot.*) |o| { - if (o == owner) slot.* = null; + for (claimed[0..count], 0..) |*slot, id| { + const holder = slot.* orelse continue; + if (holder != owner) continue; + + // **A grant is a loan.** A device this task was *given* goes back to whoever + // lent it, not to nobody — so the device manager gets its hardware back the + // instant a driver dies, and hands it to the replacement. + // + // Without this the kernel released the claim to no one and the manager + // re-claimed first-come, so every driver restart reopened the window this + // rule closes. And once `claim` refuses a device that has a giver, releasing + // to nobody would strand it: no one could ever take it again. + // + // A dead lender is no lender: clear the claim and the giver together, so the + // device is genuinely free rather than owed to a ghost. + if (giver[id]) |lender| { + if (alive(lender)) { + slot.* = lender; + giver[id] = null; // returned; it is the lender's own again, not on loan + continue; + } + giver[id] = null; } + slot.* = null; } } diff --git a/system/kernel/process.zig b/system/kernel/process.zig index 756e783..e167312 100644 --- a/system/kernel/process.zig +++ b/system/kernel/process.zig @@ -190,6 +190,15 @@ pub fn init() void { scheduler.timer_tick_hook = timerSweepLocked; scheduler.group_exit_hook = groupExitLocked; scheduler.space_mapping_release_hook = dropSpaceMappingHook; + // The broker owns devices; the process layer owns the task table. It asks whether a + // lender is still alive before handing a dead driver's device back to it. + devices_broker.task_alive_hook = taskAliveLocked; +} + +/// Whether `id` names a live task. The broker calls this through its hook when +/// deciding if a dead holder's device can go back to the task that lent it. +fn taskAliveLocked(id: u32) bool { + return scheduler.taskByIdLocked(id) != null; } /// Return -1 (as an unsigned bit pattern) in the system_call result register. diff --git a/system/kernel/tests.zig b/system/kernel/tests.zig index 1f84bea..7681ce4 100644 --- a/system/kernel/tests.zig +++ b/system/kernel/tests.zig @@ -4154,8 +4154,20 @@ fn deviceTransferTest(boot_information: *const boot_handoff.BootInformation) voi const absent = if (devices_broker.transfer(9999, me, child)) |_| false else |e| e == error.NoSuchDevice; check("a device that does not exist is refused", absent); + // **A grant is a loan.** The child holds device 0 and `me` lent it, so the child's + // death must hand it back rather than release it to nobody. That is what lets the + // device manager re-delegate to a restarted driver — and, once `claim` refuses a + // device that has a giver, it is the only thing that stops a dead driver's hardware + // being stranded forever. + check("the child still holds the lent device", devices_broker.ownerOf(0) == child); devices_broker.releaseAllOwnedBy(child); + check("the borrower's death returns the device to its lender", devices_broker.ownerOf(0) == me); + check("and it is no longer on loan", devices_broker.giverOf(0) == null); + + // A device with no lender still simply frees on death, as it always did. + check("claimed a second device with no lender", claimOk(parentless, me)); devices_broker.releaseAllOwnedBy(me); + check("a device nobody lent is released outright", devices_broker.ownerOf(parentless) == null); result(); }