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.
102 lines
5.0 KiB
Zig
102 lines
5.0 KiB
Zig
//! device-authority-test — the attacker the device suite never had.
|
|
//!
|
|
//! The audit behind [docs/fixed-bounds-audit.md] found six real defects that a
|
|
//! fully green suite had missed, and the reason was structural: *the suite
|
|
//! contains no attacker*. Every device case asserts that a driver handed its
|
|
//! own hardware can drive it. None asks what a process that was handed
|
|
//! **nothing** can do.
|
|
//!
|
|
//! This binary is that process. It is spawned with no device, holds no device,
|
|
//! and asserts what it therefore cannot do
|
|
//! ([docs/os-development/device-authority.md]):
|
|
//!
|
|
//! 1. **A positive control first.** `device_enumerate` works from here, so
|
|
//! the refusals below are decisions rather than a syscall path that is
|
|
//! simply broken for this process. Without this, "everything failed" would
|
|
//! read identically to "the assertions are meaningless".
|
|
//! 2. **It cannot give away a device it does not hold** — not one another
|
|
//! task holds, and not a free one either. The kernel's whole rule is *you
|
|
//! may give away what you hold*, so the state of the device is irrelevant:
|
|
//! a process holding nothing can transfer nothing. That is asserted across
|
|
//! several ids precisely so it cannot pass by accident of which device
|
|
//! happened to be free at boot.
|
|
//! 3. **A device that does not exist is refused differently** — `NoSuchDevice`
|
|
//! rather than `NotHeld`. A refusal that cannot say which rule refused it
|
|
//! is what cost a debugging session on the Ryzen, so the distinction is
|
|
//! part of the contract and is tested as such.
|
|
//!
|
|
//! **Why there is no "cannot take a delegated device" assertion here.** The
|
|
//! hole this fixture was written for is closed, but not by a refusal it could
|
|
//! observe. A device that was given to someone is *held*, so an attempt to
|
|
//! take it is refused as `AlreadyClaimed` — the same answer as before. What
|
|
//! changed is what happens when the holder dies: the device returns to
|
|
//! whoever lent it instead of becoming free, so the window in which a
|
|
//! stranger could take it no longer exists. There is no moment to catch.
|
|
|
|
const std = @import("std");
|
|
const device = @import("driver");
|
|
const logging = @import("logging");
|
|
const process = @import("process");
|
|
|
|
fn line(comptime format: []const u8, arguments: anytype) void {
|
|
var buffer: [160]u8 = undefined;
|
|
_ = logging.write(std.fmt.bufPrint(&buffer, format, arguments) catch return);
|
|
}
|
|
|
|
var failures: usize = 0;
|
|
var process_table: [64]process.ProcessDescriptor = undefined;
|
|
|
|
fn check(name: []const u8, ok: bool) void {
|
|
if (!ok) failures += 1;
|
|
line("device-authority: {s} {s}\n", .{ if (ok) "ok" else "FAIL", name });
|
|
}
|
|
|
|
fn run() void {
|
|
// 1. The positive control: this process can reach the device syscalls at all.
|
|
var table: [64]device.DeviceDescriptor = undefined;
|
|
const total = device.enumerate(&table);
|
|
check("enumerate works from an unprivileged process", total > 0);
|
|
const seen = @min(total, table.len);
|
|
|
|
// 2. Holding nothing, it can give nothing away — whatever the device's state.
|
|
// Every id the machine actually has, so this cannot pass by luck.
|
|
var refused: usize = 0;
|
|
var wrong_reason: usize = 0;
|
|
for (table[0..seen]) |descriptor| {
|
|
device.transfer(descriptor.id, process.taskId()) catch |e| {
|
|
refused += 1;
|
|
if (e != error.NotHeld) wrong_reason += 1;
|
|
continue;
|
|
};
|
|
}
|
|
check("every transfer by a non-holder is refused", refused == seen);
|
|
check("each refusal says NotHeld, not something vaguer", wrong_reason == 0);
|
|
|
|
// 3. A device that does not exist is a different refusal, and says so.
|
|
const absent = if (device.transfer(0xFFFF_FFFF, process.taskId())) |_| false else |e| e == error.NoSuchDevice;
|
|
check("a device that does not exist is refused as absent", absent);
|
|
|
|
// 4. **The spawn is not a second way in.** A device now rides system_spawn, which
|
|
// would be a fine back door if the kernel checked ownership any less carefully
|
|
// there than it does in transfer: spawn a child, name someone else's device, and
|
|
// the child holds hardware nobody gave it. The refusal must happen before the
|
|
// child exists, so nothing is left running either.
|
|
if (seen != 0) {
|
|
const before = process.processes(&process_table);
|
|
const spawned = process.spawnSupervisedWithDevice("/test/system/services/device-authority-test", &.{}, null, table[0].id);
|
|
check("spawning with a device the caller does not hold is refused", spawned == null);
|
|
check("and no child was left behind by the refusal", process.processes(&process_table) == before);
|
|
}
|
|
|
|
if (failures == 0) {
|
|
line("device-authority: VERDICT ok ({d} devices, none of them mine)\n", .{seen});
|
|
} else {
|
|
line("device-authority: VERDICT FAILED {d} assertion(s)\n", .{failures});
|
|
}
|
|
}
|
|
|
|
pub fn main(startup: process.Init) void {
|
|
const role = startup.arguments.get(1) orelse return; // bare (ramdisk sweep): stay silent
|
|
if (std.mem.eql(u8, role, "run")) run();
|
|
}
|