iommu: enable Intel VT-d translation with per-claim device confinement
First enforcement step of the IOVA track. A vendor-neutral IOMMU core (iommu.zig) drives an Intel VT-d backend (iommu-intel.zig) to give DMA a real translation layer instead of the fail-open free-for-all M16 left. - Boot posture is now stated explicitly: "iommu online (Intel VT-d)" with version/agaw/rmrr, or "none present - DMA fail-open (unisolated)". - DMAR parsing extended to select the INCLUDE_PCI_ALL unit (real Intel PCs put an iGPU-scoped unit first) and record single-path-endpoint RMRRs; multi-hop scopes and extra DRHDs are counted and warned, never silently dropped. - Translation is enabled at boot into a blanket identity domain (all RAM + RMRRs, 2 MiB leaves). PCI functions are enumerated post-boot by the ring-3 pci-bus driver, so a device is attached to the domain when its driver claims it (confineDevice, with claim rollback if confinement fails) and detached on driver death, before broker release and DMA frame teardown. Unclaimed devices are non-present: their DMA faults. - Interrupt remapping stays off, so MSI writes to 0xFEE00000 bypass translation and the interrupt-driven xHC keeps working. - devices-broker gains pciAddressOf (derives BDF from the config-space ECAM offset), unclaim, and forEachPciFunction. Faults are drained and logged rate-limited as DANOS-IOMMU-FAULT. Cases: iommu extended (translation on, scratch-domain map/resolve/unmap, zero idle faults); new iommu-usb-storage and iommu-usb-hid run the full storage + input stacks through translated DMA with MSI intact. 103/103.
This commit is contained in:
+111
-19
@@ -95,18 +95,49 @@ pub const PlatformInformation = struct {
|
||||
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.
|
||||
/// ring 0 — a device can DMA to any physical address (docs/driver-model.md M16),
|
||||
/// and the kernel says so at every boot (the fail-open platform log line). When
|
||||
/// true, the IOMMU core builds per-device translation domains from this record.
|
||||
iommu_present: bool = false,
|
||||
/// MMIO base of the first DMA-remapping hardware unit (DMAR DRHD), when present.
|
||||
/// MMIO base of the selected DMA-remapping hardware unit (the DRHD with
|
||||
/// INCLUDE_PCI_ALL — the catch-all unit covering every device not scoped to a
|
||||
/// more specific one; falls back to the first unit when none carries the flag).
|
||||
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.
|
||||
/// of domains, etc. Consumed by the IOMMU core when it enables translation.
|
||||
iommu_capabilities: u64 = 0,
|
||||
/// Whether the selected unit carries INCLUDE_PCI_ALL. False means every unit is
|
||||
/// device-scoped (unusual) — the core still enables on the selected unit but
|
||||
/// devices outside its scope remain untranslated.
|
||||
iommu_include_all: bool = false,
|
||||
/// DRHD units in the DMAR beyond the selected one. Devices scoped to those units
|
||||
/// (typically the integrated GPU) are NOT translated by v1 — the boot log warns.
|
||||
iommu_extra_units: u8 = 0,
|
||||
/// Reserved-memory regions (DMAR RMRRs): firmware-owned buffers a named device
|
||||
/// keeps DMAing into across the OS handoff (classically the xHC keyboard-emulation
|
||||
/// buffer). These must be identity-mapped in the device's domain BEFORE translation
|
||||
/// enables, or platform firmware breaks. Only single-path endpoint scopes are
|
||||
/// recorded; anything fancier is skipped with a loud log at parse time.
|
||||
rmrr: [maximum_rmrr]RmrrRegion = undefined,
|
||||
rmrr_count: usize = 0,
|
||||
/// RMRR device scopes the parser could not record (multi-hop paths, sub-hierarchy
|
||||
/// types, or table overflow). Non-zero means a device keeps an unmapped firmware
|
||||
/// buffer — the kernel boot log warns loudly (the platform module itself is
|
||||
/// log-free by design; it records, the kernel reports).
|
||||
rmrr_skipped: u8 = 0,
|
||||
};
|
||||
|
||||
pub const maximum_rmrr = 8;
|
||||
|
||||
/// One recorded RMRR: the device (requester id) and the inclusive physical range it
|
||||
/// must always be allowed to reach.
|
||||
pub const RmrrRegion = struct {
|
||||
bdf: u16,
|
||||
base: u64,
|
||||
limit: u64,
|
||||
};
|
||||
|
||||
/// Filled in by `discover`; the architecture layer reads it during bring-up.
|
||||
@@ -757,18 +788,33 @@ fn parseSpcr(header: *const SystemDescriptorTableHeader) void {
|
||||
|
||||
// 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.
|
||||
// structures. Type 0 is a DRHD (DMA Remapping Hardware Unit Definition): flags byte at
|
||||
// offset 4 (bit 0 = INCLUDE_PCI_ALL, the catch-all unit), 64-bit register base at
|
||||
// offset 8. Type 1 is an RMRR (Reserved Memory Region Reporting): a physical range at
|
||||
// offsets 8/16 (base / inclusive limit) that the device(s) named by the trailing
|
||||
// device-scope entries keep DMAing into across the firmware→OS handoff.
|
||||
const dmar_structures_offset = 48; // 36-byte ACPI header + 12-byte DMAR header
|
||||
const dmar_type_drhd: u16 = 0;
|
||||
const dmar_type_rmrr: u16 = 1;
|
||||
const drhd_flags_offset = 4;
|
||||
const drhd_include_pci_all: u8 = 1;
|
||||
const drhd_register_base_offset = 8;
|
||||
const rmrr_base_offset = 8;
|
||||
const rmrr_limit_offset = 16;
|
||||
const rmrr_scopes_offset = 24;
|
||||
// Device-scope entry (within DRHD/RMRR structures): type 1 = PCI endpoint; the path is
|
||||
// (device, function) byte pairs from offset 6, one pair per bridge hop plus the leaf.
|
||||
const scope_type_pci_endpoint: u8 = 1;
|
||||
const scope_start_bus_offset = 5;
|
||||
const scope_path_offset = 6;
|
||||
|
||||
/// 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.
|
||||
/// DMAR -> the VT-d unit(s) and reserved memory regions. Walks every remapping
|
||||
/// structure: selects the INCLUDE_PCI_ALL DRHD (the catch-all covering all devices not
|
||||
/// scoped elsewhere — commonly the SECOND unit on real machines, after an iGPU-scoped
|
||||
/// one), counts the rest so the boot log can warn that their devices stay untranslated,
|
||||
/// and records single-path endpoint RMRRs for the IOMMU core to pre-map before it
|
||||
/// enables translation. Multi-hop RMRR scopes are skipped loudly: better a named gap
|
||||
/// than a silent one.
|
||||
fn parseDmar(hal: Hal, header: *const SystemDescriptorTableHeader) void {
|
||||
const base: [*]align(1) const u8 = @ptrCast(header);
|
||||
const total: usize = header.length;
|
||||
@@ -780,19 +826,65 @@ fn parseDmar(hal: Hal, header: *const SystemDescriptorTableHeader) void {
|
||||
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;
|
||||
const include_all = ((fadt(u8, base, total, off + drhd_flags_offset) orelse 0) & drhd_include_pci_all) != 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
|
||||
// Selection: the INCLUDE_PCI_ALL unit wins; otherwise keep the first
|
||||
// seen. A later catch-all replaces an earlier scoped unit.
|
||||
const replace = !platform_information.iommu_present or
|
||||
(include_all and !platform_information.iommu_include_all);
|
||||
if (replace) {
|
||||
if (platform_information.iommu_present) platform_information.iommu_extra_units += 1;
|
||||
const regs = hal.mapMmio(register_base, abi.page_size, true);
|
||||
platform_information.iommu_present = true;
|
||||
platform_information.iommu_base = register_base;
|
||||
platform_information.iommu_include_all = include_all;
|
||||
platform_information.iommu_version = @as(*const volatile u32, @ptrFromInt(regs + 0x00)).*;
|
||||
platform_information.iommu_capabilities = @as(*const volatile u64, @ptrFromInt(regs + 0x08)).*;
|
||||
} else {
|
||||
platform_information.iommu_extra_units += 1;
|
||||
}
|
||||
}
|
||||
} else if (kind == dmar_type_rmrr) {
|
||||
parseRmrr(base, total, off, length);
|
||||
}
|
||||
off += length;
|
||||
}
|
||||
}
|
||||
|
||||
/// One RMRR structure: record a {bdf, base, limit} per single-path endpoint scope.
|
||||
fn parseRmrr(base: [*]align(1) const u8, total: usize, off: usize, length: usize) void {
|
||||
const range_base = fadt(u64, base, total, off + rmrr_base_offset) orelse return;
|
||||
const range_limit = fadt(u64, base, total, off + rmrr_limit_offset) orelse return;
|
||||
if (range_limit < range_base) return;
|
||||
|
||||
var scope = off + rmrr_scopes_offset;
|
||||
const end = off + length;
|
||||
while (scope + 6 <= end) {
|
||||
const scope_type = fadt(u8, base, total, scope) orelse break;
|
||||
const scope_length = fadt(u8, base, total, scope + 1) orelse break;
|
||||
if (scope_length < 6 or scope + scope_length > end) break;
|
||||
if (scope_type == scope_type_pci_endpoint and scope_length == scope_path_offset + 2) {
|
||||
// Single (device, function) pair: a directly-reachable endpoint.
|
||||
const bus = fadt(u8, base, total, scope + scope_start_bus_offset) orelse 0;
|
||||
const device = fadt(u8, base, total, scope + scope_path_offset) orelse 0;
|
||||
const function = fadt(u8, base, total, scope + scope_path_offset + 1) orelse 0;
|
||||
if (platform_information.rmrr_count < maximum_rmrr) {
|
||||
platform_information.rmrr[platform_information.rmrr_count] = .{
|
||||
.bdf = (@as(u16, bus) << 8) | (@as(u16, device) << 3) | function,
|
||||
.base = range_base,
|
||||
.limit = range_limit,
|
||||
};
|
||||
platform_information.rmrr_count += 1;
|
||||
} else {
|
||||
platform_information.rmrr_skipped +|= 1; // table full
|
||||
}
|
||||
} else {
|
||||
platform_information.rmrr_skipped +|= 1; // multi-hop path or non-endpoint scope
|
||||
}
|
||||
scope += scope_length;
|
||||
}
|
||||
}
|
||||
|
||||
// --- helpers ----------------------------------------------------------------
|
||||
|
||||
/// Sum `len` bytes; an ACPI table/pointer is valid when the low 8 bits are zero.
|
||||
|
||||
Reference in New Issue
Block a user