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.
141 lines
6.8 KiB
Markdown
141 lines
6.8 KiB
Markdown
# Bounds: how a ceiling is declared
|
|
|
|
*Design, 2026-08-08. Follows [fixed-bounds-audit.md](../fixed-bounds-audit.md), which
|
|
found 235 compile-time ceilings in this tree: 139 on quantities we do not choose, 5
|
|
recorded anywhere with their reasoning, and 171 that pass in silence when reached.*
|
|
|
|
A bound is a number chosen at compile time that decides how much of something the code
|
|
can hold. `const maximum_devices = 64`. `var below: [64]Range`. `var blob: [512]u8`.
|
|
Different units — devices, firmware memory-map entries, bytes of a USB descriptor — but
|
|
one shape, and one recurring way of going wrong.
|
|
|
|
## Where a bound lives
|
|
|
|
**Where the thing it bounds lives.** A driver's transfer-ring size belongs to that
|
|
driver; a protocol's payload cap belongs to that protocol; the kernel's task-table size
|
|
belongs to the kernel. There is no central list and this document does not propose one.
|
|
|
|
`system/parameters.zig` is not a counter-example. It is kernel-only, and it exists for a
|
|
specific historical reason: tunables had accumulated inside the loader↔kernel handoff
|
|
contract, and splitting them out kept that contract to what it actually is. It is a
|
|
tidying of one file's contents, not a registry the rest of the system reports to.
|
|
|
|
This matters for the mechanism below. An earlier draft had every bound declared through
|
|
a shared `bounds` module — which would have meant adding a dependency to roughly eight
|
|
package manifests, including `library/protocol`, which deliberately depends on nothing.
|
|
That is a coupling the problem does not require: a bound is a local fact about local
|
|
storage, and the only thing worth sharing is the *shape of the statement*, not a module.
|
|
|
|
## The declaration
|
|
|
|
A structured doc comment, immediately above the declaration, in the file that owns it:
|
|
|
|
```zig
|
|
/// bound: logical CPUs the kernel tracks
|
|
/// decided-by: hardware
|
|
/// protects: the per-CPU bookkeeping arrays, which are sized at compile time
|
|
/// at-limit: degrade — surplus cores are left parked, never brought online
|
|
/// observed-by: platform.cpusDropped() -> the WARNING at kernel.zig:281
|
|
pub const maximum_cpus = 128;
|
|
```
|
|
|
|
Five fields, all mandatory:
|
|
|
|
| Field | Answers |
|
|
|---|---|
|
|
| `bound` | what is counted, in plain words |
|
|
| `decided-by` | `hardware`, `external`, or `ours` — who chooses how large it gets |
|
|
| `protects` | what this ceiling defends against |
|
|
| `at-limit` | `refuse` / `degrade` / `truncate` / `grow`, and the detail |
|
|
| `observed-by` | how an operator finds out it was reached |
|
|
|
|
`decided-by` is the classification the audit turned on. `hardware` means the machine
|
|
chooses — PCI functions, CPUs, ACPI rows, memory-map entries. `external` means a file,
|
|
disk structure or peer chooses. `ours` means we do: a stack size, a tick rate, our own
|
|
protocol's payload. A fixed bound on the first two is a defect rather than a tunable.
|
|
|
|
## What the build step enforces
|
|
|
|
A step in `build.zig` reads the tree and fails on:
|
|
|
|
1. **A bound with no declaration.** A fixed-size array or a `maximum_*`/`max_*` constant
|
|
with no `bound:` block above it. The 235 that exist today are allowlisted by
|
|
file+line+name, so only newly written ones are gated — the rule can land without a
|
|
235-site sweep in front of it.
|
|
2. **A missing field.** All five or it fails. This alone is the 97 bounds with no
|
|
comment at all and the 171 with no observability.
|
|
3. **An unspeakable `at-limit`.** The vocabulary is closed. There is no `silent`, no
|
|
`drop`, and nothing meaning *allow*. `truncate` is legal only with a marker the
|
|
reader can see — `klog_maximum_message` qualifies because the record carries
|
|
`klog_flag_truncated`; the USB configuration descriptor cut at 512 bytes does not,
|
|
because nothing records that anything was lost.
|
|
4. **A stale allowlist entry.** If a listed bound is fixed or deleted, its entry goes
|
|
too, so the list can only shrink.
|
|
|
|
Because this is text and not a Zig type, it also covers `boot/`, which imports almost
|
|
nothing, and `tools/*.py`, where the audit found bounds as well. One mechanism, whole
|
|
tree, no new dependency edges.
|
|
|
|
## Coupled bounds
|
|
|
|
Two numbers that must agree, agreeing in code rather than in a comment — no module
|
|
needed, just a `comptime` block where one of them lives:
|
|
|
|
```zig
|
|
comptime {
|
|
if (maximum_domains != devices_broker.maximum_devices)
|
|
@compileError("iommu.confined is indexed by device id; an id past its end is " ++
|
|
"left unconfined while confineDevice still reports success");
|
|
}
|
|
```
|
|
|
|
`maximum_domains = 64` and `maximum_devices = 64` agree today only by a sentence in a
|
|
comment, and the agreement fails open. This is the clause with a live hole behind it,
|
|
and the reason raising `maximum_devices` alone would be a privilege escalation rather
|
|
than a fix.
|
|
|
|
## The worked bad case
|
|
|
|
`devices_broker.maximum_devices`, which had no comment at all:
|
|
|
|
```zig
|
|
/// bound: device nodes for the whole machine — firmware-discovered plus registered
|
|
/// decided-by: hardware
|
|
/// protects: nothing; this is a sizing guess about someone else's computer
|
|
/// at-limit: refuse — ENOSPC from device_register, dropped++ during discovery
|
|
/// observed-by: kernel.zig:203 counts discovery drops only, NOT runtime refusals
|
|
const maximum_devices = 64;
|
|
```
|
|
|
|
Writing it out is the argument. `decided-by: hardware` alongside a `protects` that
|
|
admits there is no threat describes a bound that should not be fixed at all, and
|
|
`observed-by` cannot be filled in honestly. The build step does not reject this — it
|
|
makes it impossible to write down without noticing.
|
|
|
|
## The work-list this produces
|
|
|
|
`decided-by` is machine-readable, so the sweep is a query: every `hardware` or
|
|
`external` bound whose `at-limit` is not `grow`. That is 139 of the 235, and it is the
|
|
order the fixing takes — by class, not by our guesses about which machines get run.
|
|
Reachability is exactly what a new computer changes; the Ryzen's cap was unreachable
|
|
until it wasn't.
|
|
|
|
## What this does not do
|
|
|
|
**It is a statement, not a proof.** Nothing checks that the code does what `at-limit`
|
|
claims. The enforcement is completeness and vocabulary: you cannot leave the question
|
|
unanswered, and you cannot answer it with "silently".
|
|
|
|
**It carries no occupancy.** Knowing `system/configuration/protocol.csv` sits at 50 of
|
|
64 grant rows still needs a counter and somewhere to report it. The declarations make
|
|
that cheap to add later; it is not here.
|
|
|
|
**It resizes nothing.** Declaring `maximum_devices` honestly does not make the Ryzen
|
|
work. It makes the next machine's failure legible, and it names the 139 that need real
|
|
fixes.
|
|
|
|
**Enforcement is at build time, not compile time.** A Zig type could have made a missing
|
|
field a compile error. That version needed the shared module, and the module was not
|
|
worth the coupling — so a missing field is a failed build step instead. In practice both
|
|
mean `zig build` stops; the difference is which stage prints the message.
|