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:
@@ -22,14 +22,47 @@ inline fn failed(r: usize) bool {
|
||||
return r > ~@as(usize, 0) - 4095;
|
||||
}
|
||||
|
||||
/// The errno inside a failed return. Only meaningful when `failed(r)`.
|
||||
inline fn errnoOf(r: usize) i64 {
|
||||
return -@as(i64, @bitCast(r));
|
||||
}
|
||||
|
||||
// The envelope restates the kernel's errno numbering by hand, because the
|
||||
// `protocol` package deliberately depends on nothing (so it cannot import `abi`).
|
||||
// This module is one of the few that can see both halves, so it is where they are
|
||||
// held together: drift becomes a compile error here rather than a driver reporting
|
||||
// the wrong reason for a refusal. Anything linking a driver compiles this.
|
||||
comptime {
|
||||
if (envelope.ENOENT != abi.ENOENT) @compileError("envelope.ENOENT has drifted from abi.ENOENT");
|
||||
if (envelope.ENOSPC != abi.ENOSPC) @compileError("envelope.ENOSPC has drifted from abi.ENOSPC");
|
||||
if (envelope.EPERM != abi.EPERM) @compileError("envelope.EPERM has drifted from abi.EPERM");
|
||||
if (envelope.ENOSYS != abi.ENOSYS) @compileError("envelope.ENOSYS has drifted from abi.ENOSYS");
|
||||
if (envelope.EPROTO != abi.EPROTO) @compileError("envelope.EPROTO has drifted from abi.EPROTO");
|
||||
if (envelope.EBUSY != abi.EBUSY) @compileError("envelope.EBUSY has drifted from abi.EBUSY");
|
||||
}
|
||||
|
||||
/// Copy up to `buffer.len` device descriptors into `buffer`; returns the total count.
|
||||
pub fn enumerate(buffer: []DeviceDescriptor) usize {
|
||||
return sc.systemCall2(.device_enumerate, @intFromPtr(buffer.ptr), buffer.len);
|
||||
}
|
||||
|
||||
/// Take exclusive ownership of device `id`. Returns false if taken or invalid.
|
||||
pub fn claim(id: u64) bool {
|
||||
return !failed(sc.systemCall1(.device_claim, id));
|
||||
/// Why a `claim` failed. Worth distinguishing: `AlreadyClaimed` means back off and
|
||||
/// let the owner have it, `NoSuchDevice` means this id is stale and the caller should
|
||||
/// re-enumerate, and `NotConfined` means the machine could not place the device under
|
||||
/// IOMMU translation — the claim was rolled back, and that one is a fault report, not
|
||||
/// a retry. `Refused` is an errno this library does not know a name for.
|
||||
pub const ClaimError = error{ NoSuchDevice, AlreadyClaimed, NotConfined, Refused };
|
||||
|
||||
/// Take exclusive ownership of device `id`.
|
||||
pub fn claim(id: u64) ClaimError!void {
|
||||
const r = sc.systemCall1(.device_claim, id);
|
||||
if (!failed(r)) return;
|
||||
return switch (errnoOf(r)) {
|
||||
abi.ENODEV => error.NoSuchDevice,
|
||||
abi.EBUSY => error.AlreadyClaimed,
|
||||
abi.ECONFINE => error.NotConfined,
|
||||
else => error.Refused,
|
||||
};
|
||||
}
|
||||
|
||||
/// Map resource `resource_index` (which must be an MMIO window) of claimed device
|
||||
@@ -56,11 +89,38 @@ pub const no_pci_class = device_abi.no_pci_class;
|
||||
/// a bus driver may only subdivide what it already owns. `descriptor.id` and `descriptor.parent`
|
||||
/// are ignored. A device with no resources at all is fine — a USB device is reached
|
||||
/// through its controller, not by MMIO.
|
||||
pub fn register(parent_id: u64, descriptor: *const DeviceDescriptor) ?u64 {
|
||||
pub fn register(parent_id: u64, descriptor: *const DeviceDescriptor) RegisterError!u64 {
|
||||
const r = sc.systemCall2(.device_register, parent_id, @intFromPtr(descriptor));
|
||||
return if (failed(r)) null else r;
|
||||
if (!failed(r)) return r;
|
||||
return switch (errnoOf(r)) {
|
||||
abi.ENOSPC => error.TableFull,
|
||||
abi.ENODEV => error.NoSuchParent,
|
||||
abi.EPERM => error.NotYourParent,
|
||||
abi.E2BIG => error.TooManyResources,
|
||||
abi.ECHILDREN => error.ParentFull,
|
||||
abi.ERANGE => error.NotContained,
|
||||
abi.EFAULT => error.BadDescriptor,
|
||||
else => error.Refused,
|
||||
};
|
||||
}
|
||||
|
||||
/// Why a `register` failed. These are not interchangeable and a bus driver should
|
||||
/// say which one it hit: `ParentFull` and `TableFull` are different ceilings with
|
||||
/// different fixes, and `NotContained` is not a ceiling at all — it means the child
|
||||
/// resource escaped the window the parent actually owns. Reporting all of them as
|
||||
/// one refusal is what made an AMD desktop boot with no USB and no storage, and gave
|
||||
/// no way to tell which of three causes it was (docs/fixed-bounds-audit.md).
|
||||
pub const RegisterError = error{
|
||||
TableFull, // the kernel's device table is full, machine-wide
|
||||
ParentFull, // this parent already holds as many children as it can
|
||||
NoSuchParent, // no device with that id
|
||||
NotYourParent, // that device exists but this process has not claimed it
|
||||
TooManyResources, // the descriptor declares more resources than one device may hold
|
||||
NotContained, // a resource escapes the parent's window
|
||||
BadDescriptor, // the descriptor pointer did not read back
|
||||
Refused, // an errno this library does not know a name for
|
||||
};
|
||||
|
||||
/// Bind resource `resource_index` (which must be an IRQ) of claimed device `device_id` to
|
||||
/// `endpoint`. From then on the interrupt arrives as an asynchronous notification:
|
||||
/// `ipc.replyWait` on that endpoint returns with the high bit set in `badge` and the
|
||||
|
||||
Reference in New Issue
Block a user