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:
@@ -150,6 +150,57 @@ they are wire values a Rust program needs verbatim. What stays private in
|
||||
`abi.zig` is exactly the thing the vDSO exists to hide: the `SystemCall`
|
||||
numbers and the trap convention.
|
||||
|
||||
## Errors: the errno space
|
||||
|
||||
A failed call returns `-errno`. The runtime detects failure the way Linux
|
||||
does — a return value in the top 4096 — so every code stays inside 1..4095.
|
||||
These are **public**: unlike the call numbers, a caller must be able to read
|
||||
them verbatim, and they are the same vocabulary whether the number came from
|
||||
the kernel or from a user-space provider answering over IPC.
|
||||
|
||||
They are defined once in `system/abi.zig`. The kernel restates them in
|
||||
`system/kernel/ipc-synchronous.zig` and the envelope restates the
|
||||
provider-facing subset in `library/protocol/envelope/envelope.zig` (the
|
||||
`protocol` package deliberately depends on nothing, so it cannot import
|
||||
`abi`); a comptime check in `library/device/driver/driver.zig` makes drift a
|
||||
compile error.
|
||||
|
||||
| # | Name | Meaning |
|
||||
|---|------|---------|
|
||||
| 1 | `EBADF` | bad handle |
|
||||
| 2 | `E2BIG` | an argument exceeds its maximum (a message, a descriptor's resource count) |
|
||||
| 3 | `EFAULT` | buffer unmapped, or outside the user half |
|
||||
| 4 | `ENOENT` | no such name |
|
||||
| 5 | `ENOSPC` | a kernel table is full (handles, devices) |
|
||||
| 6 | `ENOMEM` | out of memory |
|
||||
| 7 | `EPEER` | the peer died before replying — its process exited or was killed |
|
||||
| 8 | `ESRCH` | no such process |
|
||||
| 9 | `EPERM` | not permitted: the caller is not the owner or supervisor |
|
||||
| 10 | `ENOSYS` | this protocol has no such operation |
|
||||
| 11 | `EPROTO` | malformed packet: shorter than the verb it names |
|
||||
| 12 | `EBUSY` | the thing asked for is held by someone still alive |
|
||||
| 13 | `ENODEV` | no such device id |
|
||||
| 14 | `ECHILDREN` | this parent already holds as many children as it can |
|
||||
| 15 | `ERANGE` | a resource escapes the window it must fall inside |
|
||||
| 16 | `ECONFINE` | the device could not be placed under IOMMU translation |
|
||||
|
||||
`EPEER` is the one with no POSIX counterpart and it is worth stating plainly:
|
||||
synchronous IPC blocks the caller until the server replies, so the caller
|
||||
needs an answer for "the server died while I was waiting." It is not a
|
||||
transport error and not a refusal — the request may well have been carried
|
||||
out — it says only that no reply is coming. A client that treats it as
|
||||
"retry" can duplicate work; the honest response is to re-resolve the protocol
|
||||
name, because the provider it held is gone.
|
||||
|
||||
**A refusal names the rule that refused it.** This is a rule and not a
|
||||
courtesy. `device_register` alone can fail six ways, and until each got its
|
||||
own code a bus driver could only report "refused" — which is how an AMD
|
||||
desktop came to boot with a working display, no USB and no storage, with
|
||||
three independent causes indistinguishable in the log. See
|
||||
[fixed-bounds-audit.md](../fixed-bounds-audit.md). A new failure mode that
|
||||
does not fit an existing code gets a new one here rather than borrowing the
|
||||
nearest.
|
||||
|
||||
## Enforcement, and an honest threat model
|
||||
|
||||
Renumbering only has teeth if the kernel **refuses syscalls that don't come
|
||||
|
||||
Reference in New Issue
Block a user