kernel: a refusal names its rule, and two bounds stop failing open
An AMD Ryzen booted to a working compositor with no USB and no storage, and the log said only "register refused". A tree-wide audit of every compile-time ceiling followed: 235 of them, 139 on quantities the machine or a file decides rather than us, 5 documented anywhere, 171 silent when reached. docs/fixed-bounds-audit.md has the inventory. Errno attribution. The errno space was split between the kernel and the envelope, free to drift; it is now one list in system/abi.zig, restated on both sides, with a comptime check in library/device/driver where the two halves are visible. device_register's six refusals and device_claim's three are distinct codes, so a bus driver can say which rule stopped it, and BadParent splits into NoSuchParent and NotYourParent. pci-bus reconciles found against registered instead of counting refused functions as found. Idempotency ordering. The child cap was checked before the identity match, so a restarted bus was refused its own devices — the supervision restart the system leans on ratcheted toward a degraded machine. A re-registration consumes no slot and is now admitted first. IOMMU fail-closed. confineDevice returned success for a device id past the confinement table, leaving the device outside every domain while the caller believed it confined — unreachable only while ids stop at 64, which both the inventory move and a hardware-reported domain count would change. It refuses now, and the coupling to the broker's device cap is a comptime assert rather than a sentence in a comment. PCI apertures. The bridge's MMIO apertures are derived from the holes in the firmware memory map, and the derivation copied sub-4 GiB entries into a fixed [64] array and skipped the rest. A skipped region is not merely lost: the gap finder concludes it is free, so a real machine's 60-200 entry map yields an aperture over live RAM, and containment then admits a child BAR covering kernel memory. Rewritten to walk the map in place, with the hole finder extracted as a pure function and driven by a synthetic 100-entry map in a new test case. Both new tests were verified to fail on the old code. parameters.zig gains the rationale it was missing and loses a stale sentence pointing at the wrong file; vdso.md documents the errno space, including EPEER, which had no written meaning anywhere. docs/os-development/bounds.md is how a ceiling is declared from here. docs/bounds-track-plan.md is the plan to remove the ones we invented. Suite 114 -> 115.
This commit is contained in:
@@ -19,10 +19,11 @@
|
||||
//! 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");
|
||||
|
||||
const maximum_devices = 64;
|
||||
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
|
||||
@@ -157,13 +158,13 @@ pub fn enumerateFrom(start: usize, out: []device_abi.DeviceDescriptor) usize {
|
||||
return n;
|
||||
}
|
||||
|
||||
/// 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;
|
||||
/// 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;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// The task that owns device `id`, or null.
|
||||
@@ -274,14 +275,70 @@ fn contains(parent: device_abi.ResourceDescriptor, child: device_abi.ResourceDes
|
||||
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
|
||||
BadParent, // no such device, or not claimed by this task
|
||||
TooManyResources,
|
||||
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;
|
||||
@@ -299,9 +356,27 @@ fn childCount(parent_id: u64) usize {
|
||||
/// 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 (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;
|
||||
|
||||
@@ -315,26 +390,6 @@ pub fn register(parent_id: u64, owner: u32, descriptor: *const device_abi.Device
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user