M16: IOMMU detection (DMAR parsing)

Detect the IOMMU: discovery now parses the ACPI DMAR table, finds the first VT-d
DMA-remapping unit (DRHD), maps its register block, and records its version and
capabilities (iommu_present/base/version/capabilities in the platform info). On QEMU's
emulated intel-iommu this reads back a real unit (base 0xfed90000, version 1.0).

This is detection only, and deliberately so. A full VT-d bring-up — per-device
translation domains that confine a driver's DMA to the buffers it dma_alloc'd — is the
real device-side safety guarantee, but it cannot be verified without a DMA-capable
device driver (none exist yet) and QEMU's intel-iommu to fault against. Writing that
enforcement now would be a large body of unverifiable page-table code; it belongs with
the first DMA driver, which is both the natural order and the only way to test it. Until
then the caveat stands in full: device_claim on a DMA-capable device is still equivalent
to granting ring 0. The docs say so plainly.

New `iommu` test (harness boots it with -device intel-iommu via a new per-case qemu_extra
hook) confirms the DMAR is parsed and the unit's registers read. Suite 40/40 plus host
tests.
This commit is contained in:
Daniel Samson
2026-07-10 20:07:01 +01:00
parent 4ef21fa083
commit e612d948d2
4 changed files with 94 additions and 1 deletions
+14 -1
View File
@@ -152,6 +152,12 @@ If a class driver needs `mmio`, it has become an HCD and should be one.
ack cycle. Legacy INTx (`_PRT` parsing + shared lines) is deliberately skipped — MSI ack cycle. Legacy INTx (`_PRT` parsing + shared lines) is deliberately skipped — MSI
is the real answer. QEMU's HPET has no MSI, so delivery is proven with a self-IPI; the is the real answer. QEMU's HPET has no MSI, so delivery is proven with a self-IPI; the
first PCI driver is the first real consumer. first PCI driver is the first real consumer.
- **M16 (detection)** — the IOMMU is now *found*: discovery parses the ACPI DMAR table,
maps the first VT-d unit, and reads its version + capabilities (`iommu_present` in the
platform info). This is detection only — **no translation domains are programmed, so
DMA is still unprotected** (the caveat below). Enforcement lands with the first DMA
driver, which is what there is to protect and test against. Proven in the `iommu` test,
booted with an emulated `intel-iommu`.
- **`system_spawn`** — a user-space supervisor starts a driver: `system_spawn(name)` - **`system_spawn`** — a user-space supervisor starts a driver: `system_spawn(name)`
loads a binary bundled in the initial-ramdisk as a fresh ring-3 process. This is what loads a binary bundled in the initial-ramdisk as a fresh ring-3 process. This is what
turned the device manager from "log the match" into "run the driver": the kernel now turned the device manager from "log the match" into "run the driver": the kernel now
@@ -314,7 +320,14 @@ capability walk (MSI, MSI-X, PCIe extended caps) without any new syscall.
Note QEMU's HPET reports `Tn_FSB_INT_DEL_CAP = 0` — no MSI — so `hpet` can never Note QEMU's HPET reports `Tn_FSB_INT_DEL_CAP = 0` — no MSI — so `hpet` can never
exercise this path. The first MSI driver will be the first PCI driver. exercise this path. The first MSI driver will be the first PCI driver.
## M16 — the IOMMU, and the honest caveat ## M16 — the IOMMU, and the honest caveat ◑ detection done, enforcement pending
*The IOMMU is now detected (DMAR parsed, VT-d unit mapped and read — see the `iommu`
test), but **enforcement is not built**: no translation domains are programmed, so the
caveat below still holds in full. Detection can't be taken further usefully until there
is a DMA driver to protect and QEMU's `intel-iommu` to test the protection against —
building the per-device domains alongside that first driver is both the natural order
and the only way to verify them. The rest of this section is the original caveat.*
Everything above is capability-gated at the *CPU*. None of it is gated at the *device*. Everything above is capability-gated at the *CPU*. None of it is gated at the *device*.
A driver that can program a bus-mastering engine can make that device write to any A driver that can program a bus-mastering engine can make that device write to any
+55
View File
@@ -89,6 +89,20 @@ pub const PlatformInformation = struct {
/// ISA-IRQ-to-GSI remappings from the MADT (for future IOAPIC routing). /// ISA-IRQ-to-GSI remappings from the MADT (for future IOAPIC routing).
overrides: [16]IsoEntry = undefined, overrides: [16]IsoEntry = undefined,
override_count: usize = 0, override_count: usize = 0,
/// Whether an IOMMU (VT-d DMA-remapping unit) was found in the ACPI DMAR table.
/// When false, `device_claim` on a DMA-capable device is equivalent to granting
/// ring 0 — a device can DMA to any physical address (docs/driver-model.md M16).
/// Detection is the first step; per-device domain enforcement lands with the first
/// DMA driver.
iommu_present: bool = false,
/// MMIO base of the first DMA-remapping hardware unit (DMAR DRHD), when present.
iommu_base: u64 = 0,
/// The unit's Version register (offset 0x00) — its low byte is major.minor;
/// reading it back nonzero confirms a real, mappable VT-d unit.
iommu_version: u32 = 0,
/// The unit's Capability register (offset 0x08): supported address widths, number
/// of domains, etc. Recorded now; consumed when enforcement is built.
iommu_capabilities: u64 = 0,
}; };
/// Filled in by `discover`; the architecture layer reads it during bring-up. /// Filled in by `discover`; the architecture layer reads it during bring-up.
@@ -233,6 +247,7 @@ const SLIT: [4]u8 = "SLIT".*;
/// System Resource Affinity Table (SRAT) /// System Resource Affinity Table (SRAT)
const SRAT: [4]u8 = "SRAT".*; const SRAT: [4]u8 = "SRAT".*;
/// Secondary System Description Table (SSDT) /// Secondary System Description Table (SSDT)
const DMAR: [4]u8 = "DMAR".*;
const SSDT: [4]u8 = "SSDT".*; const SSDT: [4]u8 = "SSDT".*;
/// Serial Port Console Redirection table (SPCR) — the firmware's console UART. /// Serial Port Console Redirection table (SPCR) — the firmware's console UART.
const SPCR: [4]u8 = "SPCR".*; const SPCR: [4]u8 = "SPCR".*;
@@ -440,6 +455,8 @@ fn handleTable(device_tree: *DeviceTree, hal: Hal, sdt_physical: u64) !void {
parseFadt(header); parseFadt(header);
} else if (std.mem.eql(u8, &sig, &SPCR)) { } else if (std.mem.eql(u8, &sig, &SPCR)) {
parseSpcr(header); parseSpcr(header);
} else if (std.mem.eql(u8, &sig, &DMAR)) {
parseDmar(hal, header);
} else if (std.mem.eql(u8, &sig, &SSDT)) { } else if (std.mem.eql(u8, &sig, &SSDT)) {
// Secondary namespace bytecode — collect for the sleep-state (`_Sx`) scan. // Secondary namespace bytecode — collect for the sleep-state (`_Sx`) scan.
addAmlBlock(sdt_physical); addAmlBlock(sdt_physical);
@@ -750,6 +767,44 @@ fn parseSpcr(header: *const SystemDescriptorTableHeader) void {
platform_information.spcr_kind = fadt(u8, base, len, spcr_interface_type) orelse 0; platform_information.spcr_kind = fadt(u8, base, len, spcr_interface_type) orelse 0;
} }
// DMAR remapping-structure layout (Intel VT-d spec §8): the DMAR-specific header is 12
// bytes (host-address-width, flags, 10 reserved), then a list of {type u16, length u16}
// structures. Type 0 is a DRHD (DMA Remapping Hardware Unit Definition), whose 64-bit
// register base sits at offset 8 within it.
const dmar_structures_offset = 48; // 36-byte ACPI header + 12-byte DMAR header
const dmar_type_drhd: u16 = 0;
const drhd_register_base_offset = 8;
/// DMAR -> detect the IOMMU. Find the first DMA-remapping hardware unit, map its
/// register block, and record its version and capabilities. This is *detection only*:
/// it tells the system an IOMMU exists (so `device_claim` on a DMA device could one day
/// be gated by a per-device translation domain), but no domains are programmed yet —
/// enforcement is built with the first DMA driver, which is what there is to protect and
/// test against. See docs/driver-model.md (M16), the honest caveat.
fn parseDmar(hal: Hal, header: *const SystemDescriptorTableHeader) void {
const base: [*]align(1) const u8 = @ptrCast(header);
const total: usize = header.length;
var off: usize = dmar_structures_offset;
while (off + 4 <= total) {
const kind = fadt(u16, base, total, off) orelse break;
const length = fadt(u16, base, total, off + 2) orelse break;
if (length < 4 or off + length > total) break; // malformed; stop rather than loop
if (kind == dmar_type_drhd) {
const register_base = fadt(u64, base, total, off + drhd_register_base_offset) orelse 0;
if (register_base != 0) {
const regs = hal.mapMmio(register_base, abi.page_size, true);
platform_information.iommu_present = true;
platform_information.iommu_base = register_base;
platform_information.iommu_version = @as(*const volatile u32, @ptrFromInt(regs + 0x00)).*;
platform_information.iommu_capabilities = @as(*const volatile u64, @ptrFromInt(regs + 0x08)).*;
return; // first unit is enough for detection; multi-unit is future
}
}
off += length;
}
}
// --- AML namespace -> generic device tree ----------------------------------- // --- AML namespace -> generic device tree -----------------------------------
/// The PCI bus context while descending the ACPI namespace: the generic host /// The PCI bus context while descending the ACPI namespace: the generic host
+17
View File
@@ -88,6 +88,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
dmaTest(); dmaTest();
} else if (eql(case, "msi")) { } else if (eql(case, "msi")) {
msiTest(); msiTest();
} else if (eql(case, "iommu")) {
iommuTest();
} else if (eql(case, "smp")) { } else if (eql(case, "smp")) {
smpTest(); smpTest();
} else if (eql(case, "affinity")) { } else if (eql(case, "affinity")) {
@@ -1034,6 +1036,21 @@ fn msiTest() void {
result(); result();
} }
/// IOMMU (M16): with an emulated VT-d unit present (the harness boots this case with
/// `-device intel-iommu`), danos must find it in the ACPI DMAR table, map its register
/// block, and read back a real version. This is *detection*, the honest first step —
/// no translation domains are programmed yet, so DMA is still unprotected; enforcement
/// lands with the first DMA driver (docs/driver-model.md M16).
fn iommuTest() void {
log("DANOS-TEST-BEGIN: iommu\n", .{});
const pinfo = platform.platformInformation();
check("IOMMU found in the DMAR table", pinfo.iommu_present);
check("VT-d unit has a register base", pinfo.iommu_base != 0);
check("VT-d version register reads back nonzero (real, mappable unit)", pinfo.iommu_version != 0);
log("DANOS-IOMMU: base=0x{x} version=0x{x} capabilities=0x{x}\n", .{ pinfo.iommu_base, pinfo.iommu_version, pinfo.iommu_capabilities });
result();
}
var proc_worker_run: bool = true; var proc_worker_run: bool = true;
var proc_worker_ran: bool = false; var proc_worker_ran: bool = false;
+8
View File
@@ -140,6 +140,12 @@ CASES = [
{"name": "msi", {"name": "msi",
"expect": r"DANOS-TEST-RESULT: PASS", "expect": r"DANOS-TEST-RESULT: PASS",
"fail": r"DANOS-TEST-RESULT: FAIL"}, "fail": r"DANOS-TEST-RESULT: FAIL"},
# IOMMU (M16): boot with an emulated VT-d unit and confirm danos parses the DMAR
# table and reads the unit's registers. Detection only — enforcement is future.
{"name": "iommu",
"qemu_extra": ["-device", "intel-iommu,intremap=off"],
"expect": r"DANOS-TEST-RESULT: PASS",
"fail": r"DANOS-TEST-RESULT: FAIL"},
# Parallelism: needs more than one core, so this case boots with -smp 4. # Parallelism: needs more than one core, so this case boots with -smp 4.
{"name": "smp", {"name": "smp",
"smp": 4, "smp": 4,
@@ -310,6 +316,8 @@ def run_case(arch, case):
cmd = [arch["qemu"]] + arch["qemu_args"](arch, esp, vars_fd, serial) cmd = [arch["qemu"]] + arch["qemu_args"](arch, esp, vars_fd, serial)
if case.get("smp"): # some cases need more than one core (e.g. parallelism) if case.get("smp"): # some cases need more than one core (e.g. parallelism)
cmd += ["-smp", str(case["smp"])] cmd += ["-smp", str(case["smp"])]
if case.get("qemu_extra"): # extra qemu args, e.g. -device intel-iommu for the IOMMU case
cmd += case["qemu_extra"]
qemu = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) qemu = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
try: try:
timeout = case.get("timeout", TIMEOUT) timeout = case.get("timeout", TIMEOUT)