kernel: a grant is a loan — a dead borrower returns the device to its lender

When a driver dies, a device it was *given* now goes back to whoever lent
it, rather than to nobody. The device manager gets its hardware back the
instant a driver dies and hands it to the replacement, with no window in
between.

That window was real: the kernel released the claim to no one and the
manager re-claimed first-come, so every driver restart reopened the hole
this run is closing. It also becomes load-bearing at the next step — once
claim refuses a device that has a giver, releasing to nobody would strand a
dead driver's hardware permanently, because nobody could ever take it again.

A dead lender is no lender: the claim and the giver clear together, so a
device is never owed to a ghost. A device nobody lent is released outright,
exactly as before.

The broker cannot see the task table, so liveness arrives through the same
hook idiom the scheduler already uses. Null means assume dead, so a kernel
built without the hook frees claims rather than handing them to a ghost.

A stale binary nearly passed as proof for the third time this session: the
first discrimination patch left `alive` unused, the build failed with three
errors, and the old binary reported every assertion passing. Checking the
build before reading results is what caught it.

Suite 118/118.
This commit is contained in:
Daniel Samson
2026-08-08 22:21:25 +01:00
parent 4ca57fc37e
commit 1a1d92cba9
4 changed files with 55 additions and 4 deletions
+33 -3
View File
@@ -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;
}
}