kernel: record who gave each device away

One field, and the rest of the run follows from it. A device that was given
to someone is delegated hardware: it may be handed on, never taken, and when
its holder dies it goes back to whoever lent it instead of becoming free for
anyone to grab.

It also settles the framebuffer without mentioning it. Nobody delegates the
loader's framebuffer, so it has no giver, so the display service claims it
exactly as it always has — no exemption and no reference to display anywhere
in the rule.

No behaviour changes here; the field is recorded and read by nothing yet.

The test found a real bug on its first run, before the discrimination check.
The sentinel for "nobody gave this" was 0 — and task 0 is a real task, the
kernel's own, so a device given away by task 0 read back as belonging to
nobody. Both giver and registrar are optionals now. The second was a latent
bug from D9: the per-registrar allowance would have miscounted every device
task 0 registered.

Suite 118/118.
This commit is contained in:
Daniel Samson
2026-08-08 22:12:01 +01:00
parent ca1126537d
commit 4ca57fc37e
3 changed files with 46 additions and 7 deletions
+1 -1
View File
@@ -124,7 +124,7 @@ Two things fall out rather than being special-cased:
| Step | What | | Step | What |
|---|---| |---|---|
| E1 | Record a giver per device; `device_transfer` and the spawn grant set it | | 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 |
| E3 | `device_claim` refuses a device that has a giver | | 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 | | E4 | The manager claims every resource-bearing device at boot, so nothing is left takeable |
+33 -6
View File
@@ -65,8 +65,23 @@ var claimed: []?u32 = &.{};
/// The task that called `register` for each device, so the per-registrar allowance can /// The task that called `register` for each device, so the per-registrar allowance can
/// be charged to whoever caused the entry. Firmware-discovered nodes carry `no_registrar` /// be charged to whoever caused the entry. Firmware-discovered nodes carry `no_registrar`
/// — they are the kernel's own, not anybody's doing. /// — they are the kernel's own, not anybody's doing.
var registrar: []u32 = &.{}; /// Optional, not a sentinel: **task 0 is a real task** (the kernel's own), so any
const no_registrar: u32 = 0; /// "none" value inside the id space is a device belonging to somebody reading back as
/// belonging to nobody. Found the moment the first test asserted a giver, because the
/// task doing the giving was task 0.
var registrar: []?u32 = &.{};
/// **Who gave each device away**, or `no_giver` if nobody ever did.
///
/// One field, and the whole authority rule follows from it: a device that was *given*
/// to someone is delegated hardware, so it may only be handed on, never taken
/// (`claim` refuses it); and when its holder dies it goes back to whoever lent it,
/// rather than becoming free for anyone to grab.
///
/// It also settles the framebuffer without mentioning it. Nobody delegates the
/// loader's framebuffer, so it has no giver, so the display service claims it exactly
/// as it always has — no exemption, no special case, no `display` anywhere in the rule.
var giver: []?u32 = &.{};
var count: usize = 0; var count: usize = 0;
/// Grow the three parallel arrays so at least one more device fits. False if the heap /// Grow the three parallel arrays so at least one more device fits. False if the heap
@@ -86,9 +101,12 @@ fn reserve() bool {
claimed = grown_claimed; claimed = grown_claimed;
const grown_registrar = allocator.realloc(registrar, wanted) catch return false; const grown_registrar = allocator.realloc(registrar, wanted) catch return false;
registrar = grown_registrar; registrar = grown_registrar;
for (claimed[count..], registrar[count..]) |*slot, *who| { const grown_giver = allocator.realloc(giver, wanted) catch return false;
giver = grown_giver;
for (claimed[count..], registrar[count..], giver[count..]) |*slot, *who, *lender| {
slot.* = null; slot.* = null;
who.* = no_registrar; who.* = null;
lender.* = null;
} }
return true; return true;
} }
@@ -98,7 +116,7 @@ fn reserve() bool {
fn registeredBy(task: u32) usize { fn registeredBy(task: u32) usize {
var n: usize = 0; var n: usize = 0;
for (registrar[0..count]) |who| { for (registrar[0..count]) |who| {
if (who == task) n += 1; if (who != null and who.? == task) n += 1;
} }
return n; return n;
} }
@@ -120,7 +138,8 @@ pub fn init(device_tree: *const platform.DeviceTree) void {
dropped = 0; dropped = 0;
display_device = null; display_device = null;
for (claimed) |*c| c.* = null; for (claimed) |*c| c.* = null;
for (registrar) |*r| r.* = no_registrar; for (registrar) |*r| r.* = null;
for (giver) |*g| g.* = null;
walk(device_tree.root, device_abi.no_parent); walk(device_tree.root, device_abi.no_parent);
} }
@@ -235,6 +254,13 @@ pub fn claim(id: u64, owner: u32) ClaimError!void {
claimed[@intCast(id)] = owner; claimed[@intCast(id)] = owner;
} }
/// The task that gave device `id` away, or null if nobody ever did. A device with a
/// giver is delegated hardware: it may be handed on, never taken.
pub fn giverOf(id: u64) ?u32 {
if (id >= count) return null;
return giver[@intCast(id)];
}
/// The task that owns device `id`, or null. /// The task that owns device `id`, or null.
pub fn ownerOf(id: u64) ?u32 { pub fn ownerOf(id: u64) ?u32 {
if (id >= count) return null; if (id >= count) return null;
@@ -408,6 +434,7 @@ pub fn transfer(id: u64, from: u32, to: u32) TransferError!void {
const holder = claimed[@intCast(id)] orelse return error.NotHeld; const holder = claimed[@intCast(id)] orelse return error.NotHeld;
if (holder != from) return error.NotHeld; if (holder != from) return error.NotHeld;
claimed[@intCast(id)] = to; claimed[@intCast(id)] = to;
giver[@intCast(id)] = from;
} }
/// The errno a refused `claim` returns to ring 3. (`ECONFINE` — the claim stood but /// The errno a refused `claim` returns to ring 3. (`ECONFINE` — the claim stood but
+12
View File
@@ -4121,6 +4121,10 @@ fn deviceTransferTest(boot_information: *const boot_handoff.BootInformation) voi
const unheld = if (devices_broker.transfer(0, me, child)) |_| false else |e| e == error.NotHeld; const unheld = if (devices_broker.transfer(0, me, child)) |_| false else |e| e == error.NotHeld;
check("an unheld device cannot be transferred", unheld); check("an unheld device cannot be transferred", unheld);
// A second device this test never hands over, so the "no giver" half below is a
// real observation rather than a restatement of the first.
const parentless: u64 = 1;
check("claimed device 0", claimOk(0, me)); check("claimed device 0", claimOk(0, me));
// The move itself. // The move itself.
@@ -4139,6 +4143,14 @@ fn deviceTransferTest(boot_information: *const boot_handoff.BootInformation) voi
const stranger = if (devices_broker.transfer(0, 9999, me)) |_| false else |e| e == error.NotHeld; const stranger = if (devices_broker.transfer(0, 9999, me)) |_| false else |e| e == error.NotHeld;
check("a stranger cannot transfer another task's device", stranger); check("a stranger cannot transfer another task's device", stranger);
// The giver is recorded. One field, and the authority rule follows from it: a
// device that was *given* to someone is delegated hardware, so it may be handed on
// but never taken, and when its holder dies it returns to whoever lent it instead
// of becoming free for anyone. Nothing has a giver until it is handed over —
// which is why the loader's framebuffer needs no exemption from either rule.
check("the giver is recorded on a transfer", devices_broker.giverOf(0) == me);
check("a device nobody handed over has no giver", devices_broker.giverOf(parentless) == null);
const absent = if (devices_broker.transfer(9999, me, child)) |_| false else |e| e == error.NoSuchDevice; 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); check("a device that does not exist is refused", absent);