Files
danos/test/system/services/iommu-fault-test/iommu-fault-test.zig
T
Daniel Samson a86559648e kernel: a refusal names its rule, and two bounds stop failing open
An AMD Ryzen booted to a working compositor with no USB and no storage,
and the log said only "register refused". A tree-wide audit of every
compile-time ceiling followed: 235 of them, 139 on quantities the machine
or a file decides rather than us, 5 documented anywhere, 171 silent when
reached. docs/fixed-bounds-audit.md has the inventory.

Errno attribution. The errno space was split between the kernel and the
envelope, free to drift; it is now one list in system/abi.zig, restated on
both sides, with a comptime check in library/device/driver where the two
halves are visible. device_register's six refusals and device_claim's three
are distinct codes, so a bus driver can say which rule stopped it, and
BadParent splits into NoSuchParent and NotYourParent. pci-bus reconciles
found against registered instead of counting refused functions as found.

Idempotency ordering. The child cap was checked before the identity match,
so a restarted bus was refused its own devices — the supervision restart the
system leans on ratcheted toward a degraded machine. A re-registration
consumes no slot and is now admitted first.

IOMMU fail-closed. confineDevice returned success for a device id past the
confinement table, leaving the device outside every domain while the caller
believed it confined — unreachable only while ids stop at 64, which both the
inventory move and a hardware-reported domain count would change. It refuses
now, and the coupling to the broker's device cap is a comptime assert rather
than a sentence in a comment.

PCI apertures. The bridge's MMIO apertures are derived from the holes in the
firmware memory map, and the derivation copied sub-4 GiB entries into a
fixed [64] array and skipped the rest. A skipped region is not merely lost:
the gap finder concludes it is free, so a real machine's 60-200 entry map
yields an aperture over live RAM, and containment then admits a child BAR
covering kernel memory. Rewritten to walk the map in place, with the hole
finder extracted as a pure function and driven by a synthetic 100-entry map
in a new test case. Both new tests were verified to fail on the old code.

parameters.zig gains the rationale it was missing and loses a stale sentence
pointing at the wrong file; vdso.md documents the errno space, including
EPEER, which had no written meaning anywhere.

docs/os-development/bounds.md is how a ceiling is declared from here.
docs/bounds-track-plan.md is the plan to remove the ones we invented.

Suite 114 -> 115.
2026-08-08 11:09:54 +01:00

120 lines
5.2 KiB
Zig

//! iommu-fault-test — the negative proof for IOMMU enforcement. The iommu-fault QEMU
//! case boots with VT-d enabled and an extra e1000e NIC no danos driver claims; this
//! fixture claims it, then deliberately programs its transmit engine to DMA from a
//! physical address that was never dma_alloc'd (so it is in no device's domain). The
//! IOMMU must fault that access — the descriptor fetch never reaches memory — and the
//! system must stay alive. A kernel `DANOS-IOMMU-FAULT` line plus this fixture's
//! `system alive` marker is the pass.
//!
//! The rogue target is the e1000e's transmit descriptor RING base itself: the very first
//! DMA the engine issues on a doorbell write is the descriptor fetch from that base, so
//! pointing the ring at an unmapped page makes the first access the faulting one — no
//! valid descriptor need be crafted.
const std = @import("std");
const device = @import("driver");
const time = @import("time");
const logging = @import("logging");
const mmio = @import("mmio");
const pci = @import("pci");
const pci_class = @import("pci-class");
const intel_vendor: u16 = 0x8086;
const e1000e_device: u16 = 0x10D3;
const ethernet_class: u64 = pci_class.ClassCode.pack(.{
.base = @intFromEnum(pci_class.BaseClass.network),
.subclass = @intFromEnum(pci_class.network.SubClass.ethernet),
.prog_if = 0,
});
// e1000e transmit-engine registers (Intel 82574L datasheet §Register Descriptions),
// byte offsets within BAR0. VERIFY-AGAINST-SPEC held on first bring-up: these are the
// legacy TX ring registers.
const reg_tctl = 0x0400; // Transmit Control
const reg_tdbal = 0x3800; // TX Descriptor Base Address Low
const reg_tdbah = 0x3804; // TX Descriptor Base Address High
const reg_tdlen = 0x3808; // TX Descriptor Length (bytes, 128-byte aligned)
const reg_tdh = 0x3810; // TX Descriptor Head
const reg_tdt = 0x3818; // TX Descriptor Tail
const tctl_en: u32 = 1 << 1; // Transmit Enable
const tctl_psp: u32 = 1 << 3; // Pad Short Packets
/// A low physical page that user DMA never touches — never returned by dma_alloc (whose
/// arena is far higher), so it is in no device's IOMMU domain. The e1000e's descriptor
/// fetch from here is exactly the out-of-domain access the unit must block.
const rogue_physical: u64 = 0x1000;
var descriptor: device.DeviceDescriptor = undefined;
fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
var line: [128]u8 = undefined;
_ = logging.write(std.fmt.bufPrint(&line, fmt, arguments) catch return);
}
pub fn main() void {
const nic_id: u64 = found: {
var tries: u32 = 0;
while (tries < 150) : (tries += 1) {
var descriptors: [64]device.DeviceDescriptor = undefined;
const total = device.enumerate(&descriptors);
for (descriptors[0..@min(total, descriptors.len)]) |*entry| {
if (entry.class == @intFromEnum(device.DeviceClass.pci_device) and entry.pci_class == ethernet_class) {
descriptor = entry.*;
break :found entry.id;
}
}
time.sleepMillis(100);
}
_ = logging.write("iommu-fault-test: FAIL no ethernet function found\n");
return;
};
device.claim(nic_id) catch {
_ = logging.write("iommu-fault-test: FAIL claim\n");
return;
};
var function = pci.Function.map(nic_id, &descriptor) orelse {
_ = logging.write("iommu-fault-test: FAIL config-space map\n");
return;
};
if (function.vendorId() != intel_vendor or function.deviceId() != e1000e_device) {
_ = logging.write("iommu-fault-test: FAIL not an e1000e\n");
return;
}
function.enableMemoryAndBusMaster();
const bar0 = function.mapBar(0) orelse {
_ = logging.write("iommu-fault-test: FAIL map BAR0\n");
return;
};
// Point the TX ring at the rogue page and kick the engine: TDBA = rogue, a non-zero
// length, head=0, enable, then tail=1 so the engine fetches descriptor 0 — a DMA
// read from the rogue page, which the IOMMU must fault.
_ = logging.write("iommu-fault-test: pointing e1000e TX ring at an unmapped page\n");
mmio.writeRegister(u32, bar0 + reg_tdbal, @truncate(rogue_physical));
mmio.writeRegister(u32, bar0 + reg_tdbah, @intCast(rogue_physical >> 32));
mmio.writeRegister(u32, bar0 + reg_tdlen, 128);
mmio.writeRegister(u32, bar0 + reg_tdh, 0);
mmio.writeRegister(u32, bar0 + reg_tctl, tctl_en | tctl_psp);
mmio.writeMemoryBarrier();
mmio.writeRegister(u32, bar0 + reg_tdt, 1); // doorbell: fetch descriptor 0
// Give the engine time to attempt the fetch, then force the fault records to the log.
time.sleepMillis(200);
const faults = device.iommuFaultDrain();
writeLine("iommu-fault-test: drained {d} iommu fault(s)\n", .{faults});
if (faults == 0) {
_ = logging.write("iommu-fault-test: FAIL rogue DMA was not blocked\n");
return;
}
// Liveness: the system survived the blocked DMA — read our own config space back.
if (function.vendorId() != intel_vendor) {
_ = logging.write("iommu-fault-test: FAIL device unreadable after fault\n");
return;
}
_ = logging.write("iommu-fault-test: system alive\n");
}