test: the attacker the device suite never had

The audit's sharpest finding was structural, not a bug: a fully green suite
had hidden six real defects because it contains no attacker. Every device
case asserts that a driver handed its own hardware can drive it. None asked
what a process handed NOTHING can do.

device-authority-test is that process. It is spawned with no device and
asserts what it therefore cannot do: it cannot give away a device another
task holds, nor a free one, because the kernel's rule is that you may give
away what you hold and the device's state is irrelevant to a process holding
nothing. Asserted across every device the machine actually has, so it cannot
pass by accident of which one happened to be free at boot — six on QEMU,
none of them its.

A positive control runs first. device_enumerate works from this process, so
the refusals below it are decisions rather than a syscall path that is
simply broken here; without it, "everything failed" would read identically
to "the assertions are meaningless". A nonexistent device is refused as
NoSuchDevice rather than NotHeld, because a refusal that cannot name its own
rule is what cost a debugging session on the Ryzen.

What it deliberately does not assert, and says so in its header:
device_claim is still first-come-first-served at this point in the run. That
is the hole D6 closes, and the claim half of the invariant joins this
fixture then. Asserting it now would be writing a test that documents the
bug.

Verified to discriminate: removing the holder check flips "every transfer by
a non-holder is refused" while the positive control keeps passing.

Suite 117 -> 118.
This commit is contained in:
Daniel Samson
2026-08-08 17:23:17 +01:00
parent 3111c7c5e6
commit 33376f24ab
8 changed files with 174 additions and 1 deletions
@@ -0,0 +1,15 @@
//! The device-authority-test fixture as a binary package (docs/build-packages-plan.md):
//! this file names the binary and EXACTLY the modules its source imports —
//! build-support resolves each name from the domains this zon declares.
const std = @import("std");
const build_support = @import("build-support");
pub fn build(b: *std.Build) void {
const exe = build_support.userBinary(b, .{
.name = "device-authority-test",
.root_source_file = b.path("device-authority-test.zig"),
.imports = &.{ "driver", "logging", "process" },
});
b.installArtifact(exe);
}
@@ -0,0 +1,15 @@
.{
.name = .device_authority_test,
.version = "0.0.0",
.fingerprint = 0x4acbba0c105a1462, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{
// build-support supplies the shared recipe; kernel is implicit in
// every binary (the root shim + link script live there). The rest
// are exactly the homes of this binary's declared imports.
.@"build-support" = .{ .path = "../../../../build-support" },
.kernel = .{ .path = "../../../../library/kernel" },
.device = .{ .path = "../../../../library/device" },
},
.paths = .{""},
}
@@ -0,0 +1,87 @@
//! 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.
//!
//! **What this fixture cannot yet claim.** `device_claim` is still
//! first-come-first-served at this point in the run — that is the hole D6
//! closes. So the claim half of the invariant ("a process holds what it was
//! handed and cannot name its way into holding more") is deliberately NOT
//! asserted here; it is added to this fixture at D6, when it becomes true.
//! Asserting it now would mean writing a test that documents the bug.
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;
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);
if (failures == 0) {
line("device-authority: ok ({d} devices, none of them mine)\n", .{seen});
} else {
line("device-authority: 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();
}