Post-reorg cleanup: POSIX layer, and naming fixes

Follow-up to the monorepo re-org. Suite 35/35 plus host tests green.

POSIX compatibility is now its own library, library/posix/ (unistd, stdio),
layered strictly over the runtime — it calls the runtime's IPC/heap, never
system calls directly. The runtime is now POSIX-free (the danos-native
application ABI). The VFS wire protocol is danos-native throughout
(Stat -> FileStatus, .stat -> .status, O_CREAT -> create); the POSIX layer
maps the POSIX spellings at the boundary. The coding standard's ABI-name
exception is scoped to one place: a file is allowed POSIX spellings only if it
lives under library/posix/ — everywhere else, danos naming with no exception.

Naming fixes, all mechanical:
- initrd -> initial-ramdisk: the source file, the module, the tool
  (make-initial-ramdisk.py), the artifact (initial-ramdisk.img, including the
  bootloader's load path), and the identifiers.
- system/kernel/device-service.zig -> devices-broker.zig: it is ring-0 kernel
  code (the trusted device table + claim capability), not a ring-3 service. The
  future user-space device *manager* (policy) will live in system/services/.
- Dropped the daemon `d` suffix: hpetd -> hpet, busd -> bus. A driver lives in
  system/drivers/, so the folder already says what it is; encoding the role in
  the name too is redundant. The coding standard drops that exception.
- system/devices/aml/interp.zig -> interpreter.zig (the type was already
  Interpreter).
This commit is contained in:
Daniel Samson
2026-07-10 13:33:06 +01:00
parent 8754d4e46a
commit ceacc6b514
27 changed files with 308 additions and 254 deletions
+181
View File
@@ -0,0 +1,181 @@
//! 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 danos = @import("danos");
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]danos.DeviceDescriptor = undefined;
var claimed: [maximum_devices]?u32 = .{null} ** maximum_devices; // owner task id, or null
var count: usize = 0;
/// 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;
for (&claimed) |*c| c.* = null;
walk(device_tree.root, danos.no_parent);
}
/// 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) danos.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 danos.no_parent; // children of a dropped node become roots, not orphans
}
var d = std.mem.zeroes(danos.DeviceDescriptor);
d.id = count;
d.parent = parent_id;
d.class = @intFromEnum(node.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, danos.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: []danos.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)];
}
/// Resource `index` of device `id`, or null if out of range.
pub fn resourceOf(id: u64, index: u64) ?danos.ResourceDescriptor {
if (id >= count) return null;
const d = &devices[@intCast(id)];
if (index >= d.resource_count) return null;
return d.resources[@intCast(index)];
}
/// 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: danos.ResourceDescriptor, child: danos.ResourceDescriptor) bool {
if (parent.kind != child.kind) return false;
if (child.kind == @intFromEnum(danos.ResourceKind.irq)) return parent.start == child.start;
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 danos.DeviceDescriptor) RegisterError!u64 {
const parent_owner = ownerOf(parent_id) orelse return error.BadParent;
if (parent_owner != owner) return error.BadParent;
if (descriptor.resource_count > danos.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;
}
var d = std.mem.zeroes(danos.DeviceDescriptor);
d.id = count;
d.parent = parent_id;
d.class = descriptor.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;
}