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:
Daniel Samson
2026-08-08 11:09:54 +01:00
parent 7db5fba884
commit a86559648e
25 changed files with 1520 additions and 168 deletions
+40 -2
View File
@@ -43,11 +43,11 @@ pub const SystemCall = enum(u64) {
ipc_call = 9, // ipc_call(h, message, len, reply, cap) -> reply_len: send + block for reply
ipc_reply_wait = 10, // ipc_reply_wait(h, reply, len, receive, cap) -> receive_len (+badge in rdx)
device_enumerate = 11, // device_enumerate(buffer, maximum) -> count: snapshot the device table
device_claim = 12, // device_claim(id) -> ok: take exclusive ownership of a device
device_claim = 12, // device_claim(id) -> 0/-errno: take exclusive ownership of a device (-ENODEV no such id, -EBUSY someone owns it, -ECONFINE the IOMMU would not confine it)
mmio_map = 13, // mmio_map(id, resource_index) -> virtual_address: map a claimed device's MMIO into this address space
irq_bind = 14, // irq_bind(id, resource_index, endpoint): deliver a device IRQ as an IPC notification
irq_ack = 15, // irq_ack(id, resource_index): re-arm a bound IRQ after servicing it
device_register = 16, // device_register(parent_id, descriptor) -> id: publish a child of a device you claimed
device_register = 16, // device_register(parent_id, descriptor) -> id/-errno: publish a child of a device you claimed (-ENOSPC table full, -ECHILDREN parent full, -ERANGE resource escapes the parent, -ENODEV/-EPERM bad parent, -E2BIG too many resources)
system_spawn = 17, // system_spawn(name_ptr, name_len, arguments_ptr, arguments_len, exit_endpoint) -> child process id: start a named initial-ramdisk binary as a new ring-3 process
dma_alloc = 18, // dma_alloc(len, flags) -> virtual_address (rax), physical_address (rdx): contiguous, pinned, uncacheable DMA memory
dma_free = 19, // dma_free(virtual_address, len) -> 0: release a prior dma_alloc
@@ -88,6 +88,44 @@ pub const SystemCall = enum(u64) {
_,
};
/// **The errno space** — the one vocabulary of refusal, returned as `-value` in the
/// system_call result register and echoed by user-space providers in a reply status.
/// Canonical here because it crosses the kernel↔user boundary in both directions:
/// the kernel restates these in system/kernel/ipc-synchronous.zig, and the envelope
/// restates the provider-facing subset in library/protocol/envelope/envelope.zig
/// (which cannot import this module — the `protocol` package deliberately has no
/// dependencies, so a comptime cross-check in library/device/driver/driver.zig and
/// system/kernel/tests.zig holds the two halves together).
///
/// The rule these serve: **a refusal must say which rule refused it.** A caller that
/// gets one number for five different reasons cannot report, retry or route around
/// any of them — see docs/fixed-bounds-audit.md, where a bare -1 turned "this bus is
/// at its child cap" into a machine that booted with no USB and no storage, and cost
/// a debugging session to tell apart from four other causes.
///
/// Values are stable: `failed()` in the runtime treats the top 4096 return values as
/// errors (the Linux convention), so anything here must stay well inside 1..4095.
pub const EBADF: i64 = 1; // bad handle
pub const E2BIG: i64 = 2; // argument exceeds its maximum (a message, a descriptor's resource count)
pub const EFAULT: i64 = 3; // buffer unmapped / outside the user half
pub const ENOENT: i64 = 4; // no such name
pub const ENOSPC: i64 = 5; // a kernel table is full (handles, devices)
pub const ENOMEM: i64 = 6; // out of memory
pub const EPEER: i64 = 7; // peer died before replying (its process exited or was killed)
pub const ESRCH: i64 = 8; // no such process (process_kill of an unknown/dead id)
pub const EPERM: i64 = 9; // not permitted (the caller is not the owner/supervisor)
pub const ENOSYS: i64 = 10; // this protocol has no such operation
pub const EPROTO: i64 = 11; // malformed packet: shorter than the verb it names
pub const EBUSY: i64 = 12; // the thing asked for is held by someone still alive
pub const ENODEV: i64 = 13; // no such device id
pub const ECHILDREN: i64 = 14; // this parent already holds as many children as it can
pub const ERANGE: i64 = 15; // a resource escapes the window it must fall inside
pub const ECONFINE: i64 = 16; // the device could not be placed under IOMMU translation
/// The highest errno defined above. A cheap guard for anyone switching over the
/// space, and the number to bump when adding one.
pub const errno_maximum: i64 = 16;
/// `futex_wait` return codes (in rax).
pub const futex_woken: u64 = 0; // woken by a futex_wake
pub const futex_mismatch: u64 = 1; // *addr != expected on entry; the caller did not block