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:
@@ -167,6 +167,20 @@ pub fn releaseAllOwnedBy(owner: u32) void {
|
||||
}
|
||||
}
|
||||
|
||||
/// Release the claim on `id` iff `owner` holds it — the rollback for a claim that
|
||||
/// cannot be confined (the IOMMU domain could not be created/attached). Returns true
|
||||
/// when a claim was actually cleared.
|
||||
pub fn unclaim(id: u64, owner: u32) bool {
|
||||
if (id >= count) return false;
|
||||
if (claimed[@intCast(id)]) |o| {
|
||||
if (o == owner) {
|
||||
claimed[@intCast(id)] = null;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Resource `index` of device `id`, or null if out of range.
|
||||
pub fn resourceOf(id: u64, index: u64) ?device_abi.ResourceDescriptor {
|
||||
if (id >= count) return null;
|
||||
@@ -175,6 +189,50 @@ pub fn resourceOf(id: u64, index: u64) ?device_abi.ResourceDescriptor {
|
||||
return d.resources[@intCast(index)];
|
||||
}
|
||||
|
||||
/// The PCI requester id (bus<<8 | device<<3 | function) of device `id`, derived from
|
||||
/// its config-space slice against its host bridge's ECAM window — the identity a VT-d
|
||||
/// context entry / AMD-Vi DTE is keyed by. null when `id` is not a PCI function or the
|
||||
/// geometry doesn't decode. The kernel never stored the BDF (the descriptor has no such
|
||||
/// field); pci-bus encodes it into resource 0's physical base as
|
||||
/// `ecam_base + ((bus - start_bus) << 20 | device << 15 | function << 12)`, and the
|
||||
/// requester id the device emits uses the absolute bus, so we add `start_bus << 8` back.
|
||||
pub fn pciAddressOf(id: u64) ?u16 {
|
||||
if (id >= count) return null;
|
||||
const d = &devices[@intCast(id)];
|
||||
if (d.class != @intFromEnum(device_abi.DeviceClass.pci_device)) return null;
|
||||
if (d.resource_count == 0) return null;
|
||||
const config = d.resources[0];
|
||||
if (config.kind != @intFromEnum(device_abi.ResourceKind.memory) or config.len != 4096) return null;
|
||||
|
||||
// Walk up to the host bridge, whose resource 0 is the segment's ECAM window and
|
||||
// resource 1 the bus_range (start_bus, bus_count).
|
||||
var parent = d.parent;
|
||||
while (parent != device_abi.no_parent and parent < count) {
|
||||
const p = &devices[@intCast(parent)];
|
||||
if (p.class == @intFromEnum(device_abi.DeviceClass.pci_host_bridge)) {
|
||||
if (p.resource_count < 2) return null;
|
||||
const ecam = p.resources[0];
|
||||
const bus_range = p.resources[1];
|
||||
if (config.start < ecam.start or config.start >= ecam.start + ecam.len) return null;
|
||||
const offset = config.start - ecam.start;
|
||||
const start_bus: u16 = @intCast(bus_range.start & 0xFF);
|
||||
return @intCast((offset >> 12) + (@as(u64, start_bus) << 8));
|
||||
}
|
||||
parent = p.parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Call `visit(id, bdf)` for every PCI function in the table — the IOMMU core's boot
|
||||
/// sweep to place every device under a domain. Only functions whose BDF decodes are
|
||||
/// visited.
|
||||
pub fn forEachPciFunction(visit: *const fn (id: u64, bdf: u16) void) void {
|
||||
var id: u64 = 0;
|
||||
while (id < count) : (id += 1) {
|
||||
if (pciAddressOf(id)) |bdf| visit(id, bdf);
|
||||
}
|
||||
}
|
||||
|
||||
/// Is `child` wholly inside `parent`? For a range (memory, io_port, bus_range) that's
|
||||
/// interval containment; for an irq it's equality, since an interrupt line is not
|
||||
/// divisible. Zero-length child ranges are refused — an empty window is meaningless
|
||||
|
||||
Reference in New Issue
Block a user