The convention that tunables live in system/parameters.zig with their reasoning attached predates this and got 2% compliance — 5 of 235. A convention with no teeth is how a bare `const maximum_devices = 64` reached an AMD desktop and cost it USB and storage. This is the same rule with a gate behind it. tools/check-bounds.py finds every bound-shaped declaration — a `maximum_*` const with a literal value, or a type with a literal array length — and requires the five-field block above it: what it counts, who decides its size, what it protects, what happens at the limit, and how anyone finds out. The at-limit vocabulary is closed: refuse, degrade, truncate, grow. There is deliberately no way to spell "silent", no way to spell "drop", and nothing meaning "allow", so the behaviours that did the damage cannot be written down. Truncation is legal only carrying a marker the reader can see, which is why klog_maximum_message qualifies and a USB descriptor cut at 512 bytes does not. An array length that names a declared bound is not itself a bound; only literal lengths are flagged, which pushes ceilings toward having names. The 273 that predate the rule are allowlisted so this lands without a tree-wide sweep in front of it, and that list may only shrink: declaring a bound means deleting its line, and the check fails on a stale entry too. Nothing may be added. Wired into `zig build test` and available alone as `zig build bounds`. Not in the default build — it reads the whole tree, and a red bounds check should not stop you booting a kernel. Five are now declared rather than allowlisted. Writing them out is its own argument: maximum_devices reads "protects: nothing — this is a sizing guess about someone else's computer", and maximum_tasks now carries the fact that it has been raised twice, each time by something that outgrew it. Verified the gate refuses an undeclared bound, a declared one using forbidden vocabulary, and an allowlist entry that has since been declared. Suite 115/115.
424 lines
20 KiB
Zig
424 lines
20 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 abi = @import("abi");
|
|
const platform = @import("platform");
|
|
const device_abi = @import("device-abi");
|
|
|
|
/// bound: device nodes for the whole machine — firmware-discovered plus every child a
|
|
/// bus driver registers at runtime
|
|
/// decided-by: hardware
|
|
/// protects: nothing — this is a sizing guess about someone else's computer, which is
|
|
/// why an AMD Ryzen booted with a working display, no USB and no storage
|
|
/// at-limit: refuse — ENOSPC from device_register; `dropped` counts discovery losses
|
|
/// observed-by: the bus driver's line naming the reason (pci-bus reconciles functions
|
|
/// found against registered), and kernel.zig:203 for discovery drops
|
|
pub 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.
|
|
///
|
|
/// bound: children one claimed parent may register — in practice every PCI function on
|
|
/// the machine, since pci-bus registers them all under the one host bridge
|
|
/// decided-by: hardware
|
|
/// protects: the shared device table, against a driver looping device_register — but
|
|
/// it is a proxy for an authorisation the kernel does not perform, since any
|
|
/// process may claim any unclaimed device (docs/bounds-track-plan.md phase 2)
|
|
/// at-limit: refuse — ECHILDREN, distinct from a full table
|
|
/// observed-by: pci-bus logs the reason per refused function, and warns at end of scan
|
|
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`). For kernel callers with a buffer big
|
|
/// enough to take the whole table in one go.
|
|
pub fn enumerate(out: []device_abi.DeviceDescriptor) usize {
|
|
_ = enumerateFrom(0, out);
|
|
return count;
|
|
}
|
|
|
|
/// How many devices the table holds — the total `device_enumerate` reports back
|
|
/// however few of them fit in the caller's buffer.
|
|
pub fn deviceCount() usize {
|
|
return count;
|
|
}
|
|
|
|
/// Copy up to `out.len` descriptors starting at table index `start`, returning how
|
|
/// many were filled (0 once `start` reaches the end). The chunked form: the
|
|
/// `device_enumerate` system call bounces the table out through a small kernel
|
|
/// buffer, one chunk at a time, because a descriptor is far too big to stage a
|
|
/// whole user-requested array of them on a 16 KiB kernel stack.
|
|
pub fn enumerateFrom(start: usize, out: []device_abi.DeviceDescriptor) usize {
|
|
if (start >= count) return 0;
|
|
const n = @min(count - start, out.len);
|
|
@memcpy(out[0..n], devices[start..][0..n]);
|
|
return n;
|
|
}
|
|
|
|
/// Take exclusive ownership of device `id` for task `owner`. The two ways this can
|
|
/// fail want different responses from a driver — a stale id means re-enumerate, a
|
|
/// live claimant means back off — so they are distinguishable (`ClaimError`).
|
|
pub fn claim(id: u64, owner: u32) ClaimError!void {
|
|
if (id >= count) return error.NoSuchDevice;
|
|
if (claimed[@intCast(id)] != null) return error.AlreadyClaimed;
|
|
claimed[@intCast(id)] = owner;
|
|
}
|
|
|
|
/// 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;
|
|
}
|
|
|
|
/// Why a `register` was refused. Each variant maps to its own errno (`errnoOf`), so
|
|
/// a bus driver's log line can name the rule that stopped it — "this parent is at
|
|
/// its child cap" and "the table is full" want different fixes, and telling them
|
|
/// apart from a bare -1 cost a debugging session (docs/fixed-bounds-audit.md).
|
|
pub const RegisterError = error{
|
|
NoSpace, // the device table is full
|
|
NoSuchParent, // no device with that id
|
|
NotYourParent, // that device exists but this task has not claimed it
|
|
TooManyResources, // the descriptor declares more resources than one device may hold
|
|
TooManyChildren, // this parent is at maximum_children_per_parent
|
|
NotContained, // a child resource escapes its parent's window
|
|
};
|
|
|
|
/// The errno a refused `register` returns to ring 3.
|
|
pub fn errnoOf(e: RegisterError) i64 {
|
|
return switch (e) {
|
|
error.NoSpace => abi.ENOSPC,
|
|
error.NoSuchParent => abi.ENODEV,
|
|
error.NotYourParent => abi.EPERM,
|
|
error.TooManyResources => abi.E2BIG,
|
|
error.TooManyChildren => abi.ECHILDREN,
|
|
error.NotContained => abi.ERANGE,
|
|
};
|
|
}
|
|
|
|
/// Why a `claim` was refused.
|
|
pub const ClaimError = error{
|
|
NoSuchDevice, // no device with that id
|
|
AlreadyClaimed, // a live task already owns it
|
|
};
|
|
|
|
/// The errno a refused `claim` returns to ring 3. (`ECONFINE` — the claim stood but
|
|
/// the IOMMU would not confine the device — is raised by the caller in
|
|
/// system/kernel/process.zig, which is what rolls the claim back.)
|
|
pub fn claimErrnoOf(e: ClaimError) i64 {
|
|
return switch (e) {
|
|
error.NoSuchDevice => abi.ENODEV,
|
|
error.AlreadyClaimed => abi.EBUSY,
|
|
};
|
|
}
|
|
|
|
/// The id of a child of `parent_id` already identical to `descriptor`, or null.
|
|
/// Exact on class, identity and every resource — anything less would let a bus
|
|
/// silently adopt an entry that is not the device it just found. The caller must
|
|
/// have bounded `descriptor.resource_count` first.
|
|
fn existingChild(parent_id: u64, descriptor: *const device_abi.DeviceDescriptor) ?u64 {
|
|
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;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// 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 {
|
|
if (parent_id >= count) return error.NoSuchParent;
|
|
const parent_owner = ownerOf(parent_id) orelse return error.NotYourParent;
|
|
if (parent_owner != owner) return error.NotYourParent;
|
|
// Bounds every `descriptor.resources` read below, including the match scan's.
|
|
if (descriptor.resource_count > device_abi.maximum_device_resources) return error.TooManyResources;
|
|
|
|
// 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 child under the same parent returns the existing id instead of
|
|
// appending a duplicate.
|
|
//
|
|
// Checked **before the caps**, because a re-registration consumes no slot.
|
|
// Charging it against the child cap refused a restarted bus its own devices the
|
|
// second time it started, which turned the supervision restart this system leans
|
|
// on into a one-way ratchet toward a degraded machine. The match is exact — class,
|
|
// identity, and every resource — so an entry returned this way was contained when
|
|
// it was first admitted, and a stored descriptor's resources never change
|
|
// afterwards (they are written only by `record`, `seedDisplay` and the append
|
|
// below).
|
|
if (existingChild(parent_id, descriptor)) |existing_id| return existing_id;
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
}
|