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:
+40
-2
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user