iommu: per-device domains with interim DMA-pool enforcement
Replaces L1's shared blanket identity domain with a private translation
domain per claimed PCI function. A device now reaches only:
- the DMA pool: every dma_alloc'd region, mapped into every claimed
device's domain (poolAdd/poolRemove, driven from the dma_alloc and
dma_free syscalls). This keeps the cross-process buffer handoff
working (fat's bounce buffer reaches the xHC) while blocking the
kernel, page tables, process heaps, MMIO, and unallocated RAM.
- its own firmware reserved region (RMRR), seeded at confine time.
The pool is the honest interim: devices can still reach one another's
DMA buffers. The DMA-region capability layer (next) narrows it to
per-grant reachability.
dma_free unmaps from every domain and invalidates BEFORE the frames
return to the allocator, closing the stale-IOTLB use-after-free window.
Driver death tears down its domains (detach + free tables) before the
broker claims and DMA frames are released.
New iommu_fault_drain syscall (+ driver.iommuFaultDrain) forces pending
fault records to the log on demand. The new iommu-fault case proves it:
a claimed e1000e is programmed to DMA-fetch its TX ring from an unmapped
page; VT-d faults the access (bdf 00:03.0 addr 0x1000 reason 0x6) and the
system stays alive. 104/104.
This commit is contained in:
@@ -184,6 +184,15 @@ CASES = [
|
||||
"qemu_extra": ["-device", "intel-iommu,intremap=off"],
|
||||
"expect": r"(?s)(?=.*/system/kernel: iommu online)(?=.*usb-hid-keyboard: ok)(?=.*usb-hid-mouse: ok)",
|
||||
"fail": r"DANOS-TEST-RESULT: FAIL|DANOS-IOMMU-FAULT"},
|
||||
# Enforcement, the negative proof: a claimed e1000e fires a DMA at an unmapped page;
|
||||
# VT-d must fault it (logged) and the system must stay alive. The fault line is the
|
||||
# point here, so unlike the positive cases it appears in `expect`, not `fail`.
|
||||
{"name": "iommu-fault",
|
||||
"smp": 4,
|
||||
"timeout": 120,
|
||||
"qemu_extra": ["-device", "intel-iommu,intremap=off", "-device", "e1000e"],
|
||||
"expect": r"(?s)(?=.*DANOS-IOMMU-FAULT: bdf=)(?=.*iommu-fault-test: system alive)(?=.*DANOS-TEST-RESULT: PASS)",
|
||||
"fail": r"iommu-fault-test: FAIL|DANOS-TEST-RESULT: FAIL|CPU EXCEPTION|KERNEL PANIC"},
|
||||
# Port I/O grants: a claimed device's io_port resource lets a driver read/write its
|
||||
# ports (PS/2 status 0x64), gated by the claim; out-of-range/unclaimed is refused.
|
||||
{"name": "ioport",
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
//! 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;
|
||||
};
|
||||
|
||||
if (!device.claim(nic_id)) {
|
||||
_ = 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");
|
||||
}
|
||||
Reference in New Issue
Block a user