diff --git a/docs/bounds-track-plan.md b/docs/bounds-track-plan.md index 8044f10..8a893a0 100644 --- a/docs/bounds-track-plan.md +++ b/docs/bounds-track-plan.md @@ -124,7 +124,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 | +| 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 | | 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 | diff --git a/system/kernel/devices-broker.zig b/system/kernel/devices-broker.zig index 2d3acff..0b2b10e 100644 --- a/system/kernel/devices-broker.zig +++ b/system/kernel/devices-broker.zig @@ -65,8 +65,23 @@ var claimed: []?u32 = &.{}; /// 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` /// — they are the kernel's own, not anybody's doing. -var registrar: []u32 = &.{}; -const no_registrar: u32 = 0; +/// Optional, not a sentinel: **task 0 is a real task** (the kernel's own), so any +/// "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; /// 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; const grown_registrar = allocator.realloc(registrar, wanted) catch return false; 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; - who.* = no_registrar; + who.* = null; + lender.* = null; } return true; } @@ -98,7 +116,7 @@ fn reserve() bool { fn registeredBy(task: u32) usize { var n: usize = 0; for (registrar[0..count]) |who| { - if (who == task) n += 1; + if (who != null and who.? == task) n += 1; } return n; } @@ -120,7 +138,8 @@ pub fn init(device_tree: *const platform.DeviceTree) void { dropped = 0; display_device = 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); } @@ -235,6 +254,13 @@ pub fn claim(id: u64, owner: u32) ClaimError!void { 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. pub fn ownerOf(id: u64) ?u32 { 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; if (holder != from) return error.NotHeld; claimed[@intCast(id)] = to; + giver[@intCast(id)] = from; } /// The errno a refused `claim` returns to ring 3. (`ECONFINE` — the claim stood but diff --git a/system/kernel/tests.zig b/system/kernel/tests.zig index e8ccf80..1f84bea 100644 --- a/system/kernel/tests.zig +++ b/system/kernel/tests.zig @@ -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; 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)); // 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; 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; check("a device that does not exist is refused", absent);