Files
danos/system/kernel/devices-broker.zig
Daniel Samson f477ef7d9f 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.
2026-07-26 18:31:27 +01:00

334 lines
15 KiB
Zig

//! Device service: the kernel side of user-space driver access. At boot it
//! flattens the discovered device tree (src/device) into a stable, id-indexed
//! snapshot and a per-device claim table. User drivers enumerate the snapshot,
//! claim the device they own, and map its MMIO — the claim is the capability that
//! gates `mmio_map`/`irq_bind`, so a process can only ever touch hardware the
//! firmware-neutral device tree says it owns.
//!
//! The table is a **tree**: each entry carries its parent's id. Firmware discovery
//! seeds it, and a **bus driver** grows it — a process that has claimed a bus can
//! `register` children below it as it enumerates them (USB devices behind a hub, PCI
//! functions behind a bridge, comparators inside a timer block).
//!
//! Registration is where the capability model earns its keep. A `DeviceDescriptor` is, in
//! effect, a licence to map physical memory: whoever claims it may `mmio_map` its
//! `.memory` resources and `irq_bind` its `.irq` resources. If a bus driver could
//! invent arbitrary resources, it would invent one covering the kernel's RAM, claim
//! it, and map it. So `register` enforces **containment**: every resource of a child
//! must lie inside a resource of the same kind on its parent. A bus driver can only
//! ever subdivide what it was already given.
const std = @import("std");
const platform = @import("platform");
const device_abi = @import("device-abi");
const maximum_devices = 64;
/// Cap on children a single parent may have. A zero-resource child (legal — a USB
/// device is addressed through its controller, not by MMIO) sidesteps the containment
/// check, so without a bound a process that claimed one device could loop
/// `device_register` and exhaust the whole table, permanently denying it to every other
/// driver. This bounds the blast radius of one claim; a real quota (and a
/// `device_release` to reclaim on exit) is future work — see docs/driver-model.md.
const maximum_children_per_parent = 16;
var devices: [maximum_devices]device_abi.DeviceDescriptor = undefined;
var claimed: [maximum_devices]?u32 = .{null} ** maximum_devices; // owner task id, or null
var count: usize = 0;
/// The id of the seeded framebuffer node (`seedDisplay`), or null when the machine
/// handed over no framebuffer. Lets the process layer recognise the display claim
/// (to quiesce the bootstrap console) without threading the id through every caller.
var display_device: ?u64 = null;
/// Devices discovery found but the table had no room for. Non-zero means the machine
/// is bigger than `maximum_devices` and some hardware is simply invisible to drivers —
/// which would otherwise be an entirely silent failure. Logged at boot.
pub var dropped: usize = 0;
/// Snapshot the device tree into the flat table. Run once, right after discovery.
pub fn init(device_tree: *const platform.DeviceTree) void {
count = 0;
dropped = 0;
display_device = null;
for (&claimed) |*c| c.* = null;
walk(device_tree.root, device_abi.no_parent);
}
/// Publish the loader's framebuffer as a `display` device — a root-level node with one
/// write-combining `memory` resource over the linear framebuffer and its geometry in
/// `.display`. The framebuffer is *not* firmware-discovered (it rides the
/// [[boot-handoff]], not the device tree), so it is seeded explicitly, after `init`.
/// Returns the new device id, or null when there is no framebuffer (headless) or the
/// table is full. Idempotent-ish: only ever call once per boot.
pub fn seedDisplay(base: u64, width: u32, height: u32, pitch: u32, format: u32, refresh_hz: u32) ?u64 {
if (base == 0 or width == 0 or height == 0) return null; // headless
if (count >= maximum_devices) {
dropped += 1;
return null;
}
var d = std.mem.zeroes(device_abi.DeviceDescriptor);
d.id = count;
d.parent = device_abi.no_parent;
d.class = @intFromEnum(device_abi.DeviceClass.display);
d.pci_class = device_abi.no_pci_class;
d.resource_count = 1;
d.resources[0] = .{
.kind = @intFromEnum(device_abi.ResourceKind.memory),
.start = base,
.len = @as(u64, height) * pitch,
.flags = device_abi.resource_flag_write_combining,
};
d.display = .{ .width = width, .height = height, .pitch = pitch, .format = format, .refresh_hz = refresh_hz };
devices[count] = d;
display_device = d.id;
count += 1;
return d.id;
}
/// The id of the seeded framebuffer device, or null when none was seeded.
pub fn displayDevice() ?u64 {
return display_device;
}
/// Whether the framebuffer device is currently claimed by some process. The bootstrap
/// console uses this (via the process layer) to fall silent while a display service
/// owns the screen, and to resume if that service dies and its claim is released.
pub fn displayClaimed() bool {
const id = display_device orelse return false;
return ownerOf(id) != null;
}
/// Record `node` (unless it's the synthetic root) and recurse, threading the id we
/// assigned it down to its children as their parent.
fn walk(node: *platform.Device, parent_id: u64) void {
const id = if (node.class == .root) device_abi.no_parent else record(node, parent_id);
var child = node.first_child;
while (child) |c| : (child = c.next_sibling) walk(c, id);
}
fn record(node: *platform.Device, parent_id: u64) u64 {
if (count >= maximum_devices) {
dropped += 1;
return device_abi.no_parent; // children of a dropped node become roots, not orphans
}
var d = std.mem.zeroes(device_abi.DeviceDescriptor);
d.id = count;
d.parent = parent_id;
d.class = @intFromEnum(node.class);
d.pci_class = if (node.ids.pci_class) |code| code else device_abi.no_pci_class;
const h = node.hid();
d.hid_len = @min(h.len, d.hid.len);
@memcpy(d.hid[0..d.hid_len], h[0..d.hid_len]);
const rc = @min(node.resource_count, device_abi.maximum_device_resources);
d.resource_count = rc;
for (0..rc) |i| {
const r = node.resources[i];
d.resources[i] = .{ .kind = @intFromEnum(r.kind), .start = r.start, .len = r.len };
}
devices[count] = d;
count += 1;
return d.id;
}
/// Copy up to `out.len` device descriptors into `out`; returns the total count
/// available (which may exceed `out.len`).
pub fn enumerate(out: []device_abi.DeviceDescriptor) usize {
const n = @min(count, out.len);
@memcpy(out[0..n], devices[0..n]);
return count;
}
/// Take exclusive ownership of device `id` for task `owner`. Fails if the id is
/// out of range or already claimed.
pub fn claim(id: u64, owner: u32) bool {
if (id >= count) return false;
if (claimed[@intCast(id)] != null) return false;
claimed[@intCast(id)] = owner;
return true;
}
/// The task that owns device `id`, or null.
pub fn ownerOf(id: u64) ?u32 {
if (id >= count) return null;
return claimed[@intCast(id)];
}
/// Release every claim held by `owner` — called by the process layer on every
/// path out of a process (exit, fault, kill), so a restarted driver can claim its
/// hardware again (docs/process-lifecycle.md iron rule 1: cleanup is the kernel's
/// job). The devices stay in the table — they describe hardware, which did not go
/// away — only their ownership clears.
pub fn releaseAllOwnedBy(owner: u32) void {
for (claimed[0..count]) |*slot| {
if (slot.*) |o| {
if (o == owner) slot.* = null;
}
}
}
/// 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;
const d = &devices[@intCast(id)];
if (index >= d.resource_count) return null;
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
/// and would otherwise vacuously "fit" anywhere.
fn contains(parent: device_abi.ResourceDescriptor, child: device_abi.ResourceDescriptor) bool {
if (parent.kind != child.kind) return false;
if (child.kind == @intFromEnum(device_abi.ResourceKind.irq)) {
// Range containment: an interrupt line is still indivisible (a child owns
// exactly one GSI), but a parent may own a *range* of lines so a broad
// owner — the acpi-tables node, whose firmware names any legacy IRQ —
// can contain its children's specific lines. A length-1 parent range is
// exactly the old equality rule, so existing single-IRQ parents are
// unaffected.
const span = if (parent.len == 0) 1 else parent.len;
return child.start >= parent.start and child.start < parent.start + span;
}
if (child.len == 0 or parent.len == 0) return false;
// No overflow: a resource that wraps the address space is not containable.
const child_end = std.math.add(u64, child.start, child.len) catch return false;
const parent_end = std.math.add(u64, parent.start, parent.len) catch return false;
return child.start >= parent.start and child_end <= parent_end;
}
pub const RegisterError = error{
NoSpace, // the device table is full
BadParent, // no such device, or not claimed by this task
TooManyResources,
TooManyChildren, // this parent is at maximum_children_per_parent
NotContained, // a child resource escapes its parent's window
};
/// Number of devices currently recorded with `parent_id` as their parent.
fn childCount(parent_id: u64) usize {
var n: usize = 0;
for (devices[0..count]) |d| {
if (d.parent == parent_id) n += 1;
}
return n;
}
/// Publish `descriptor` as a child of `parent_id`, on behalf of `owner`. Returns the new
/// device id. The child is left **unclaimed**, so another process (a class driver)
/// can claim it — that is how a bus hands a device to its driver.
///
/// `owner` must have claimed `parent_id`, and every resource in `descriptor` must be
/// contained in a parent resource of the same kind. A device with no resources is
/// fine and common: a USB device is addressed through its controller, not by MMIO.
pub fn register(parent_id: u64, owner: u32, descriptor: *const device_abi.DeviceDescriptor) RegisterError!u64 {
const parent_owner = ownerOf(parent_id) orelse return error.BadParent;
if (parent_owner != owner) return error.BadParent;
if (descriptor.resource_count > device_abi.maximum_device_resources) return error.TooManyResources;
if (childCount(parent_id) >= maximum_children_per_parent) return error.TooManyChildren;
if (count >= maximum_devices) return error.NoSpace;
const parent = &devices[@intCast(parent_id)];
for (0..@intCast(descriptor.resource_count)) |i| {
const r = descriptor.resources[i];
var ok = false;
for (0..@intCast(parent.resource_count)) |j| {
if (contains(parent.resources[j], r)) ok = true;
}
if (!ok) return error.NotContained;
}
// Idempotent on exact match (docs/device-manager.md): a restarted
// registering bus re-registers what it rediscovers, and the table has no
// unregister — an identical (class, identity, resources) child under the
// same parent returns the existing id instead of appending a duplicate.
for (devices[0..count]) |*existing| {
if (existing.parent != parent_id) continue;
if (existing.class != descriptor.class) continue;
if (existing.pci_class != descriptor.pci_class) continue;
if (existing.hid_len != descriptor.hid_len) continue;
if (!std.mem.eql(u8, existing.hid[0..@intCast(existing.hid_len)], descriptor.hid[0..@intCast(descriptor.hid_len)])) continue;
if (existing.resource_count != descriptor.resource_count) continue;
var same = true;
for (0..@intCast(descriptor.resource_count)) |i| {
const a = existing.resources[i];
const b = descriptor.resources[i];
if (a.kind != b.kind or a.start != b.start or a.len != b.len) same = false;
}
if (same) return existing.id;
}
var d = std.mem.zeroes(device_abi.DeviceDescriptor);
d.id = count;
d.parent = parent_id;
d.class = descriptor.class;
d.pci_class = descriptor.pci_class;
d.hid_len = @min(descriptor.hid_len, d.hid.len);
@memcpy(d.hid[0..@intCast(d.hid_len)], descriptor.hid[0..@intCast(d.hid_len)]);
d.resource_count = descriptor.resource_count;
for (0..@intCast(descriptor.resource_count)) |i| d.resources[i] = descriptor.resources[i];
devices[count] = d;
count += 1;
return d.id;
}