kernel: claim refuses delegated hardware — and E2 had already closed the hole

The rule as planned: a device that was given to someone may be handed on,
never taken. Implemented, and honest about what it is worth.

Writing the test showed the plan had the wrong step doing the work. A
delegated device is HELD, so an attempt to take it is refused as
AlreadyClaimed before the giver is ever consulted; and once a borrower's
death returns the device to its lender — or clears both when the lender is
gone — there is no state where a device is unheld and still on loan. The
window a stranger could have used stops existing at E2. This check is
unreachable.

It stays anyway: one comparison, failing closed, guarding any future path
that frees a device without clearing its giver, which is exactly the hole
this run closed. The comment says it is unreachable rather than implying a
protection it does not provide.

The attacker fixture does not gain the assertion that was deferred to this
step, and its header records why: there is no refusal for it to observe, and
on a bare boot with no device manager nothing is delegated at all, so the
assertion had nothing to bite on. It failed loudly on its first run rather
than passing quietly, which is the only reason this was noticed.

It also leaves the loader's framebuffer alone without naming it: nobody
delegates the framebuffer, so it has no giver, so the display service claims
it exactly as before.

Suite 118/118.
This commit is contained in:
Daniel Samson
2026-08-08 22:31:04 +01:00
parent 1a1d92cba9
commit ba195fa0a2
4 changed files with 38 additions and 10 deletions
+18
View File
@@ -251,6 +251,22 @@ pub fn enumerateFrom(start: usize, out: []device_abi.DeviceDescriptor) usize {
pub fn claim(id: u64, owner: u32) ClaimError!void {
if (id >= count) return error.NoSuchDevice;
if (claimed[@intCast(id)] != null) return error.AlreadyClaimed;
// **Delegated hardware may be handed on, never taken.**
//
// Belt and braces, and worth being honest about: with the loan rule above this is
// **currently unreachable**. A device that was given to someone is held, so it is
// refused as `AlreadyClaimed` before reaching here; and when the holder dies the
// device goes back to its lender (or, if the lender is gone, has its giver cleared
// with its claim), so there is no state where a device is unheld *and* still on
// loan. The window a stranger could have used simply stops existing.
//
// It stays because it is one comparison and it fails closed: any future path that
// frees a device without clearing its giver would otherwise hand delegated
// hardware to whoever asked first, which is exactly the hole this run closed.
//
// Note it leaves the loader's framebuffer alone without naming it: nobody delegates
// the framebuffer, so it has no giver, so the display service claims it as always.
if (giver[@intCast(id)] != null) return error.NotYours;
claimed[@intCast(id)] = owner;
}
@@ -428,6 +444,7 @@ pub fn errnoOf(e: RegisterError) i64 {
pub const ClaimError = error{
NoSuchDevice, // no device with that id
AlreadyClaimed, // a live task already owns it
NotYours, // delegated hardware: it has a giver, so it must be handed on, not taken
};
/// Why a `transfer` was refused.
@@ -474,6 +491,7 @@ pub fn claimErrnoOf(e: ClaimError) i64 {
return switch (e) {
error.NoSuchDevice => abi.ENODEV,
error.AlreadyClaimed => abi.EBUSY,
error.NotYours => abi.EPERM,
};
}