diff --git a/docs/driver-model.md b/docs/driver-model.md index 23c0416..33d5a78 100644 --- a/docs/driver-model.md +++ b/docs/driver-model.md @@ -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 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. +- **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)` 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 @@ -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 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*. A driver that can program a bus-mastering engine can make that device write to any diff --git a/system/devices/acpi.zig b/system/devices/acpi.zig index 38bc990..7b78146 100644 --- a/system/devices/acpi.zig +++ b/system/devices/acpi.zig @@ -89,6 +89,20 @@ pub const PlatformInformation = struct { /// ISA-IRQ-to-GSI remappings from the MADT (for future IOAPIC routing). overrides: [16]IsoEntry = undefined, 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. @@ -233,6 +247,7 @@ const SLIT: [4]u8 = "SLIT".*; /// System Resource Affinity Table (SRAT) const SRAT: [4]u8 = "SRAT".*; /// Secondary System Description Table (SSDT) +const DMAR: [4]u8 = "DMAR".*; const SSDT: [4]u8 = "SSDT".*; /// Serial Port Console Redirection table (SPCR) — the firmware's console UART. const SPCR: [4]u8 = "SPCR".*; @@ -440,6 +455,8 @@ fn handleTable(device_tree: *DeviceTree, hal: Hal, sdt_physical: u64) !void { parseFadt(header); } else if (std.mem.eql(u8, &sig, &SPCR)) { parseSpcr(header); + } else if (std.mem.eql(u8, &sig, &DMAR)) { + parseDmar(hal, header); } else if (std.mem.eql(u8, &sig, &SSDT)) { // Secondary namespace bytecode — collect for the sleep-state (`_Sx`) scan. 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; } +// 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 ----------------------------------- /// The PCI bus context while descending the ACPI namespace: the generic host diff --git a/system/kernel/tests.zig b/system/kernel/tests.zig index f94f1b9..cb2a624 100644 --- a/system/kernel/tests.zig +++ b/system/kernel/tests.zig @@ -88,6 +88,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void { dmaTest(); } else if (eql(case, "msi")) { msiTest(); + } else if (eql(case, "iommu")) { + iommuTest(); } else if (eql(case, "smp")) { smpTest(); } else if (eql(case, "affinity")) { @@ -1034,6 +1036,21 @@ fn msiTest() void { 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_ran: bool = false; diff --git a/test/qemu_test.py b/test/qemu_test.py index 0e60bdf..398ce63 100644 --- a/test/qemu_test.py +++ b/test/qemu_test.py @@ -140,6 +140,12 @@ CASES = [ {"name": "msi", "expect": r"DANOS-TEST-RESULT: PASS", "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. {"name": "smp", "smp": 4, @@ -310,6 +316,8 @@ def run_case(arch, case): 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) 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) try: timeout = case.get("timeout", TIMEOUT)