# 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` once agreed only by a sentence in a comment, and that agreement failed open — the clause with the live hole behind it, where raising `maximum_devices` alone left every device id past the end of `iommu.confined` unconfined while `confineDevice` still reported success, a privilege escalation rather than a fix. The assert closed that: it held the two together while both stayed fixed, and when the device table was later made dynamic — no `maximum_devices` any more, only a per-registrar quota — that forced them apart, the assert having done its job. `confined` now grows to cover every id the broker mints, and `confineDevice` refuses when it cannot record a confinement rather than failing open. ## The worked bad case `devices_broker.maximum_devices`, which had no comment at all, before it was made dynamic: ```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.