build: a new compile-time ceiling declares itself or does not land

The convention that tunables live in system/parameters.zig with their
reasoning attached predates this and got 2% compliance — 5 of 235. A
convention with no teeth is how a bare `const maximum_devices = 64` reached
an AMD desktop and cost it USB and storage. This is the same rule with a
gate behind it.

tools/check-bounds.py finds every bound-shaped declaration — a `maximum_*`
const with a literal value, or a type with a literal array length — and
requires the five-field block above it: what it counts, who decides its
size, what it protects, what happens at the limit, and how anyone finds out.

The at-limit vocabulary is closed: refuse, degrade, truncate, grow. There is
deliberately no way to spell "silent", no way to spell "drop", and nothing
meaning "allow", so the behaviours that did the damage cannot be written
down. Truncation is legal only carrying a marker the reader can see, which
is why klog_maximum_message qualifies and a USB descriptor cut at 512 bytes
does not.

An array length that names a declared bound is not itself a bound; only
literal lengths are flagged, which pushes ceilings toward having names.

The 273 that predate the rule are allowlisted so this lands without a
tree-wide sweep in front of it, and that list may only shrink: declaring a
bound means deleting its line, and the check fails on a stale entry too.
Nothing may be added.

Wired into `zig build test` and available alone as `zig build bounds`. Not
in the default build — it reads the whole tree, and a red bounds check
should not stop you booting a kernel.

Five are now declared rather than allowlisted. Writing them out is its own
argument: maximum_devices reads "protects: nothing — this is a sizing guess
about someone else's computer", and maximum_tasks now carries the fact that
it has been raised twice, each time by something that outgrew it.

Verified the gate refuses an undeclared bound, a declared one using
forbidden vocabulary, and an allowlist entry that has since been declared.
Suite 115/115.
This commit is contained in:
Daniel Samson
2026-08-08 11:27:27 +01:00
parent 568823a4fb
commit f4eb88e7d2
7 changed files with 537 additions and 4 deletions
+13
View File
@@ -397,6 +397,19 @@ pub fn build(b: *std.Build) void {
// here (compiled for the host rather than inheriting a freestanding target) —
// which also compile-checks that the three-way split stays self-consistent.
const test_step = b.step("test", "Run tests");
// Every compile-time ceiling states what it counts, who decides its size, what it
// protects, what happens when it is reached, and how anyone finds out
// (docs/os-development/bounds.md). The ~273 that predate the rule are listed in
// tools/bounds-allowlist.txt so this could land without a tree-wide sweep first;
// that list may only shrink. Part of `test` rather than the default build: it reads
// the whole tree, and a red bounds check should not stop you booting a kernel.
const bounds_check = b.addSystemCommand(&.{ "python3", "tools/check-bounds.py" });
bounds_check.setCwd(b.path("."));
const bounds_step = b.step("bounds", "Check that every compile-time ceiling declares itself");
bounds_step.dependOn(&bounds_check.step);
test_step.dependOn(&bounds_check.step);
for ([_][]const u8{
"system/boot-handoff.zig",
"system/abi.zig",
+1 -1
View File
@@ -13,7 +13,7 @@ next one starts.*
| Step | What | State |
|---|---|---|
| L1 | Reclamation: a dead task's registrations die with its claims | **stopped — the step was wrong; see open question 4** |
| L2 | Bounds build check + allowlist; declare what we have already touched | not started |
| L2 | Bounds build check + allowlist; declare what we have already touched | **done** — `zig build bounds`, 273 allowlisted, 5 declared |
| L3 | xHCI: slot count from `HCSPARAMS1.MaxSlots`, not 8 | not started |
| L4 | USB: configuration descriptor sized by `wTotalLength`, not 512 | not started |
| L5 | USB: interfaces from the descriptor, and the misattributed-endpoint bug | not started |
+17
View File
@@ -23,6 +23,14 @@ const abi = @import("abi");
const platform = @import("platform");
const device_abi = @import("device-abi");
/// bound: device nodes for the whole machine — firmware-discovered plus every child a
/// bus driver registers at runtime
/// decided-by: hardware
/// protects: nothing — this is a sizing guess about someone else's computer, which is
/// why an AMD Ryzen booted with a working display, no USB and no storage
/// at-limit: refuse — ENOSPC from device_register; `dropped` counts discovery losses
/// observed-by: the bus driver's line naming the reason (pci-bus reconciles functions
/// found against registered), and kernel.zig:203 for discovery drops
pub const maximum_devices = 64;
/// Cap on children a single parent may have. A zero-resource child (legal — a USB
@@ -31,6 +39,15 @@ pub const maximum_devices = 64;
/// `device_register` and exhaust the whole table, permanently denying it to every other
/// driver. This bounds the blast radius of one claim; a real quota (and a
/// `device_release` to reclaim on exit) is future work — see docs/driver-model.md.
///
/// bound: children one claimed parent may register — in practice every PCI function on
/// the machine, since pci-bus registers them all under the one host bridge
/// decided-by: hardware
/// protects: the shared device table, against a driver looping device_register — but
/// it is a proxy for an authorisation the kernel does not perform, since any
/// process may claim any unclaimed device (docs/bounds-track-plan.md phase 2)
/// at-limit: refuse — ECHILDREN, distinct from a full table
/// observed-by: pci-bus logs the reason per refused function, and warns at end of scan
const maximum_children_per_parent = 16;
var devices: [maximum_devices]device_abi.DeviceDescriptor = undefined;
+14 -1
View File
@@ -39,7 +39,20 @@ const page_size: u64 = abi.page_size;
const page_mask: u64 = page_size - 1;
const huge_page_size: u64 = 2 * 1024 * 1024;
/// One domain per claimed PCI function. 64 mirrors devices-broker's device cap.
/// One domain per claimed PCI function. Coupled to devices-broker's device cap — and
/// coupled *in code*, by the comptime assert beside `confined` below, because when
/// these two agreed only by this sentence the disagreement failed open.
///
/// Both VT-d and AMD-Vi report the number of domains they support in a capability
/// register. We should be reading it rather than choosing 64 — docs/bounds-track-plan.md
/// phase 4.
///
/// bound: IOMMU translation domains, one per claimed DMA-capable device
/// decided-by: hardware
/// protects: the statically sized domain and confinement tables
/// at-limit: refuse — ECONFINE; the claim is rolled back and the device is not driven,
/// because a claim that cannot be confined must not stand
/// observed-by: the claiming driver's own line naming ECONFINE
pub const maximum_domains = 64;
pub const invalid_domain: u16 = 0xFFFF;
+18 -2
View File
@@ -17,8 +17,13 @@
/// arrays (discovery pool, scheduler state, per-core GDT/TSS). Generous headroom:
/// those structs are small, and the *large* per-core resources (kernel and IST
/// stacks) are allocated at bring-up for cores that actually come online, so this
/// ceiling is cheap. A machine with more logical CPUs has its surplus reported and
/// left parked (see acpi `cpusDropped`).
/// ceiling is cheap.
///
/// bound: logical CPUs the kernel tracks
/// decided-by: hardware
/// protects: the per-CPU bookkeeping arrays, which are sized at compile time
/// at-limit: degrade — the surplus cores are left parked, never brought online
/// observed-by: platform.cpusDropped() -> the WARNING at kernel.zig:281
pub const maximum_cpus = 128;
/// Maximum tasks (kernel threads) alive at once — the static task-table size. Each
@@ -29,6 +34,17 @@ pub const maximum_cpus = 128;
/// for the USB stack: the xHCI bus driver spawns a supervised class-driver instance
/// per matched interface (keyboard, mouse, mass storage), on top of the FAT and
/// block servers and the growing ramdisk bundle.
///
/// The history above is the argument against this number: it has been raised twice,
/// each time by a machine or a bundle that outgrew it, which is the pattern the
/// bounds rule exists to stop. It is `ours` only because the task table is static;
/// how many drivers a machine needs is decided by how much hardware it has.
///
/// bound: kernel threads alive at once — the static task-table size
/// decided-by: hardware
/// protects: the statically allocated task table
/// at-limit: refuse — spawn fails; a supervised driver is never started
/// observed-by: the spawning supervisor's own log line; see docs/bounds-track-plan.md
pub const maximum_tasks = 48;
/// Each task's kernel stack (also each AP's bring-up stack), in bytes.
+285
View File
@@ -0,0 +1,285 @@
# Bounds that predate the rule (docs/os-development/bounds.md).
#
# Generated from the tree as it stood when the check landed, so the gate could start
# without a 278-site sweep in front of it. Every line is a compile-time ceiling that
# has not yet said what it counts, who decides its size, what it protects, what
# happens when it is reached, or how anyone finds out.
#
# **This list may only shrink.** Declaring a bound means deleting its line; the check
# fails if a listed bound is now declared, and fails if a listed bound has vanished.
# Nothing may be added to it — a new ceiling declares itself or does not land.
#
# docs/fixed-bounds-audit.md is the analysis of how they got here.
boot/efi.zig:buffer
boot/efi.zig:info_buffer
boot/efi.zig:maximum_bundled
boot/efi.zig:maximum_tree_depth
library/device/acpi/aml/interpreter.zig:argbuf
library/device/acpi/aml/interpreter.zig:args
library/device/acpi/aml/interpreter.zig:locals
library/device/acpi/aml/interpreter.zig:maximum_segments
library/device/acpi/aml/interpreter.zig:notify_queue
library/device/acpi/aml/namespace.zig:segment
library/device/acpi/aml/parser.zig:maximum_segments
library/device/driver/driver.zig:lookup_attempts
library/device/model/device-abi.zig:hid
library/device/model/device-abi.zig:maximum_device_resources
library/device/pci/pci.zig:bar_virtual
library/device/pci/pci.zig:bars
library/device/registry/device-registry.zig:cols
library/device/registry/device-registry.zig:rules
library/device/registry/device-registry.zig:rules
library/device/registry/device-registry.zig:rules
library/device/registry/device-registry.zig:rules
library/device/registry/device-registry.zig:rules
library/kernel/channel.zig:bind_attempts
library/kernel/channel.zig:name_maximum
library/kernel/channel.zig:path_maximum
library/kernel/file-system.zig:name_buffer
library/kernel/file-system.zig:out
library/kernel/logging.zig:buffer
library/kernel/process.zig:blob
library/kernel/process.zig:receive
library/kernel/process.zig:table
library/kernel/service.zig:subscriber_capacity
library/kernel/start.zig:buffer
library/kernel/thread.zig:readers
library/kernel/thread.zig:writers
library/protocol/device-manager/device-manager-protocol.zig:hid
library/protocol/display/display-protocol.zig:max_modes
library/protocol/envelope/envelope.zig:packet_maximum
library/protocol/envelope/envelope.zig:post_maximum
library/protocol/power/power-protocol.zig:hid
library/protocol/scanout/scanout-protocol.zig:max_modes
library/protocol/usb-transfer/usb-transfer-protocol.zig:max_report_data
library/protocol/usb-transfer/usb-transfer-protocol.zig:max_reported_endpoints
library/protocol/usb-transfer/usb-transfer-protocol.zig:setup
system/abi.zig:errno_maximum
system/abi.zig:klog_maximum_message
system/abi.zig:maximum_process_name
system/boot-handoff.zig:kernel_segments
system/drivers/pci-bus/pci-bus.zig:line
system/drivers/pci-bus/pci-bus.zig:sub_buffer
system/drivers/ps2-bus/mouse-packet.zig:bytes
system/drivers/ps2-bus/scancode.zig:pressed
system/drivers/ps2-bus/scancode.zig:set2_base
system/drivers/ps2-bus/scancode.zig:set2_extended
system/drivers/usb-hid/hid-report.zig:keys
system/drivers/usb-hid/keyboard.zig:receive
system/drivers/usb-hid/mouse.zig:receive
system/drivers/usb-storage/bulk-only-transport.zig:cdb
system/drivers/usb-storage/scsi.zig:op_read_capacity_10
system/drivers/usb-storage/usb-storage.zig:capacity_bytes
system/drivers/usb-xhci-bus/usb-xhci-bus.zig:hid_buffer
system/drivers/usb-xhci-bus/usb-xhci-bus.zig:maker_buffer
system/drivers/usb-xhci-bus/usb-xhci-bus.zig:maker_buffer
system/drivers/usb-xhci-bus/usb-xhci-bus.zig:prev_connected
system/drivers/usb-xhci-bus/usb-xhci-bus.zig:product_buffer
system/drivers/usb-xhci-bus/usb-xhci-bus.zig:product_buffer
system/drivers/usb-xhci-bus/usb-xhci-library.zig:blob
system/drivers/usb-xhci-bus/usb-xhci-library.zig:buffer
system/drivers/usb-xhci-bus/usb-xhci-library.zig:bytes
system/drivers/usb-xhci-bus/usb-xhci-library.zig:data
system/drivers/usb-xhci-bus/usb-xhci-library.zig:descriptor
system/drivers/usb-xhci-bus/usb-xhci-library.zig:head
system/drivers/usb-xhci-bus/usb-xhci-library.zig:header
system/drivers/usb-xhci-bus/usb-xhci-library.zig:max_devices
system/drivers/usb-xhci-bus/usb-xhci-library.zig:max_endpoints_per_interface
system/drivers/usb-xhci-bus/usb-xhci-library.zig:max_interfaces
system/drivers/usb-xhci-bus/usb-xhci-library.zig:max_subscriptions
system/drivers/usb-xhci-bus/usb-xhci-library.zig:port_changes
system/drivers/usb-xhci-bus/usb-xhci-library.zig:raw
system/drivers/usb-xhci-bus/usb-xhci-library.zig:report_queue_capacity
system/drivers/usb-xhci-bus/usb-xhci-library.zig:request_set_hub_depth
system/drivers/virtio-gpu/virtio-gpu-protocol.zig:edid
system/drivers/virtio-gpu/virtio-gpu-protocol.zig:max_scanouts
system/drivers/virtio-gpu/virtio-gpu.zig:descriptors
system/drivers/virtio-gpu/virtio-gpu.zig:max_height
system/drivers/virtio-gpu/virtio-gpu.zig:max_width
system/initial-ramdisk.zig:buffer
system/initial-ramdisk.zig:buffer
system/initial-ramdisk.zig:maximum_name
system/kernel/acpi.zig:APIC
system/kernel/acpi.zig:BERT
system/kernel/acpi.zig:CPEP
system/kernel/acpi.zig:DMAR
system/kernel/acpi.zig:DSDT
system/kernel/acpi.zig:ECDT
system/kernel/acpi.zig:EINJ
system/kernel/acpi.zig:ERST
system/kernel/acpi.zig:FACP
system/kernel/acpi.zig:FACS
system/kernel/acpi.zig:HEST
system/kernel/acpi.zig:HPET
system/kernel/acpi.zig:IVRS
system/kernel/acpi.zig:MCFG
system/kernel/acpi.zig:MPST
system/kernel/acpi.zig:MSCT
system/kernel/acpi.zig:PMTT
system/kernel/acpi.zig:PSDT
system/kernel/acpi.zig:RASF
system/kernel/acpi.zig:RSDT
system/kernel/acpi.zig:SBST
system/kernel/acpi.zig:SLIT
system/kernel/acpi.zig:SPCR
system/kernel/acpi.zig:SRAT
system/kernel/acpi.zig:SSDT
system/kernel/acpi.zig:XSDT
system/kernel/acpi.zig:aml_block_len
system/kernel/acpi.zig:aml_block_physical
system/kernel/acpi.zig:holes
system/kernel/acpi.zig:maximum_rmrr
system/kernel/acpi.zig:nb
system/kernel/acpi.zig:nb
system/kernel/acpi.zig:nb
system/kernel/acpi.zig:oem_id
system/kernel/acpi.zig:oem_id
system/kernel/acpi.zig:oem_id
system/kernel/acpi.zig:oem_table_id
system/kernel/acpi.zig:overrides
system/kernel/acpi.zig:rmrr_limit_offset
system/kernel/acpi.zig:signature
system/kernel/acpi.zig:signature
system/kernel/acpi.zig:signature
system/kernel/architecture/x86_64/cpu.zig:irq_vector_count
system/kernel/architecture/x86_64/idt.zig:gate_count
system/kernel/architecture/x86_64/ioapic.zig:overrides
system/kernel/architecture/x86_64/iommu-amd.zig:buffer
system/kernel/architecture/x86_64/iommu-amd.zig:buffer
system/kernel/architecture/x86_64/iommu-intel.zig:buffer
system/kernel/architecture/x86_64/iommu-intel.zig:buffer
system/kernel/architecture/x86_64/iommu-intel.zig:context_table
system/kernel/device-model.zig:buffer
system/kernel/device-model.zig:cbuf
system/kernel/device-model.zig:hid_buffer
system/kernel/device-model.zig:maximum_resources
system/kernel/device-model.zig:name_buffer
system/kernel/device-model.zig:rbuf
system/kernel/heap.zig:heap_maximum
system/kernel/ipc-synchronous.zig:MESSAGE_MAXIMUM
system/kernel/ipc-synchronous.zig:POST_MAXIMUM
system/kernel/ipc-synchronous.zig:notify_buffer
system/kernel/ipc-synchronous.zig:post_capacity
system/kernel/irq.zig:maximum_gsi
system/kernel/irq.zig:msi_bound
system/kernel/irq.zig:msi_owner
system/kernel/irq.zig:vector_gsi
system/kernel/kernel.zig:buffer
system/kernel/kernel.zig:buffer
system/kernel/kernel.zig:buffer
system/kernel/kernel.zig:isos
system/kernel/kernel.zig:maximum_wake_attempts
system/kernel/log-ring.zig:message
system/kernel/log-ring.zig:out
system/kernel/log.zig:buffer
system/kernel/log.zig:maximum_sinks
system/kernel/log.zig:message
system/kernel/log.zig:ring_capacity
system/kernel/process.zig:chunk
system/kernel/process.zig:chunk
system/kernel/process.zig:chunk
system/kernel/process.zig:chunk
system/kernel/process.zig:exit_record_capacity
system/kernel/process.zig:exit_subscriber_capacity
system/kernel/process.zig:maximum_argument_bytes
system/kernel/process.zig:maximum_arguments
system/kernel/process.zig:maximum_dma_regions
system/kernel/process.zig:maximum_mmap_pages
system/kernel/process.zig:maximum_mount_prefix
system/kernel/process.zig:maximum_mount_rewrite
system/kernel/process.zig:maximum_pages
system/kernel/process.zig:maximum_resolve_path
system/kernel/process.zig:maximum_segments
system/kernel/process.zig:maximum_shared_memory_pages
system/kernel/process.zig:name_buffer
system/kernel/process.zig:timer_capacity
system/kernel/process.zig:word_bytes
system/kernel/process.zig:write_buffer
system/kernel/scheduler.zig:ipc_maximum_handles
system/kernel/scheduler.zig:maximum_space_mappings
system/kernel/vfs.zig:maximum_directories
system/kernel/vfs.zig:maximum_mounts
system/kernel/vfs.zig:maximum_prefix
system/kernel/vfs.zig:maximum_rewrite
system/services/acpi/acpi.zig:blocks
system/services/acpi/acpi.zig:buffer
system/services/acpi/acpi.zig:hid
system/services/acpi/acpi.zig:mmio_scratch
system/services/acpi/acpi.zig:name
system/services/acpi/acpi.zig:registered
system/services/device-manager/device-manager.zig:arguments
system/services/device-manager/device-manager.zig:id_text
system/services/device-manager/device-manager.zig:maximum_children
system/services/device-manager/device-manager.zig:maximum_drivers
system/services/device-manager/device-manager.zig:name_buffer
system/services/device-manager/device-manager.zig:registry_rules
system/services/device-manager/device-manager.zig:registry_source
system/services/display/backend.zig:device_table
system/services/display/backend.zig:line
system/services/display/compositor.zig:capacity
system/services/display/compositor.zig:maximum_columns
system/services/display/compositor.zig:maximum_rects
system/services/display/compositor.zig:maximum_rows
system/services/display/display.zig:line
system/services/display/display.zig:line
system/services/display/display.zig:line
system/services/display/display.zig:list
system/services/display/display.zig:maximum_layers
system/services/display/display.zig:mode_list
system/services/fat/engine.zig:buf
system/services/fat/engine.zig:buf
system/services/fat/engine.zig:buf
system/services/fat/engine.zig:buffer
system/services/fat/engine.zig:cached_back
system/services/fat/engine.zig:chunk
system/services/fat/engine.zig:chunk
system/services/fat/engine.zig:chunk
system/services/fat/engine.zig:device_back
system/services/fat/engine.zig:display
system/services/fat/engine.zig:long_name
system/services/fat/engine.zig:long_name
system/services/fat/engine.zig:long_name
system/services/fat/engine.zig:max_transfer_sectors
system/services/fat/engine.zig:pair
system/services/fat/engine.zig:pair
system/services/fat/engine.zig:payload
system/services/fat/engine.zig:payload
system/services/fat/engine.zig:payload
system/services/fat/engine.zig:readback
system/services/fat/engine.zig:run
system/services/fat/engine.zig:run
system/services/fat/engine.zig:short
system/services/fat/engine.zig:short
system/services/fat/engine.zig:short
system/services/fat/engine.zig:stem
system/services/fat/engine.zig:tail_buffer
system/services/fat/engine.zig:units
system/services/fat/engine.zig:value
system/services/fat/engine.zig:value
system/services/fat/engine.zig:window
system/services/fat/on-disk.zig:filesystem_type
system/services/fat/on-disk.zig:filesystem_type
system/services/fat/on-disk.zig:jump
system/services/fat/on-disk.zig:name
system/services/fat/on-disk.zig:name1
system/services/fat/on-disk.zig:name2
system/services/fat/on-disk.zig:name3
system/services/fat/on-disk.zig:oem_name
system/services/fat/on-disk.zig:volume_label
system/services/fat/on-disk.zig:volume_label
system/services/init/init.zig:binary
system/services/init/init.zig:init_csv
system/services/init/init.zig:max_service_args
system/services/init/init.zig:max_services
system/services/init/init.zig:maximum_bindings
system/services/init/init.zig:maximum_grants
system/services/init/init.zig:maximum_name
system/services/init/init.zig:maximum_restarts
system/services/init/init.zig:process_table
system/services/init/init.zig:protocol_csv
system/services/logger/logger.zig:chunk
system/services/logger/logger.zig:gap_line
system/services/logger/logger.zig:line
system/services/logger/logger.zig:line
system/services/logger/logger.zig:maximum_files
system/services/logger/logger.zig:stamp
+189
View File
@@ -0,0 +1,189 @@
#!/usr/bin/env python3
"""Every compile-time ceiling states what it is doing there.
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, one shape, and one recurring way of going wrong — see
docs/fixed-bounds-audit.md, where 235 of them turned up, 139 on quantities the machine
or a file decides rather than us, and 171 silent when reached.
This is the gate that keeps new ones from joining them. It does not resize anything and
it makes no judgement about whether a bound should exist; it only refuses one that will
not say what it is for. The declaration is a doc comment immediately above:
/// 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;
`decided-by` is the field the audit turned on: `hardware` and `external` mean the
quantity is not ours to choose, and a fixed bound on one of those is a defect rather
than a tunable. `at-limit`'s vocabulary is closed on purpose — there is no `silent`,
no `drop`, and nothing meaning *allow*, so the behaviours that caused the damage cannot
be written down. `truncate` is legal only with a marker the reader can see.
An array length that names a declared bound (`[maximum_devices]Descriptor`) is not
itself a bound: the number lives at the declaration, and that is where it is declared.
Only literal lengths are flagged, which pushes ceilings toward having names.
The ~235 that already exist are listed in tools/bounds-allowlist.txt so this can land
without a tree-wide sweep in front of it. That list may only shrink: declaring a bound
means deleting its line, and a stale line is an error too.
Usage: check-bounds.py [--list] [repo-root]
--list print every undeclared bound found, for regenerating the allowlist
"""
import re
import sys
from pathlib import Path
# Directories worth gating. `test/` is excluded: a fixture's `[100]MemoryRegion` is a
# test input, not a ceiling the system runs into.
ROOTS = ("system", "library", "boot")
SKIP_PARTS = {".zig-cache", "zig-out", ".git", ".claude", "vendor", "generated"}
SKIP_FILES = {"tests.zig"}
FIELDS = ("bound", "decided-by", "protects", "at-limit", "observed-by")
DECIDED_BY = {"hardware", "external", "ours"}
AT_LIMIT = {"refuse", "degrade", "truncate", "grow"}
# A `const` whose name reads like a ceiling and whose value is an integer literal.
NAMED = re.compile(
r"^\s*(?:pub\s+)?const\s+([A-Za-z_]\w*)\s*(?::\s*[\w.\[\]]+\s*)?=\s*"
r"(\d[\d_]*|0x[0-9a-fA-F_]+)\s*(?:\*\s*\d[\d_]*\s*)*;"
)
NAME_IS_BOUND = re.compile(r"(^|_)(maximum|max|limit|capacity|depth|attempts|count)($|_)", re.I)
# A declaration or struct field whose type carries a *literal* array length.
ARRAY_DECL = re.compile(r"^\s*(?:pub\s+)?(?:const|var)\s+([A-Za-z_]\w*)\s*:[^=]*?\[\s*(\d[\d_]*)\s*\]")
ARRAY_FIELD = re.compile(r"^\s*([A-Za-z_]\w*)\s*:\s*\[\s*(\d[\d_]*)\s*\]")
# Struct padding and reserved fields are shapes, not ceilings — nothing is ever "held"
# in them. Everything else with a literal length is a candidate, *including* the tidy
# powers of two: `[512]u8` and `[64]Range` were the two worst findings in the audit, and
# any size-based exemption would have skipped exactly them. A length that is genuinely a
# fact rather than a ceiling says so in its declaration ("decided-by: hardware, the PCI
# spec gives a function 6 BARs") — that is what the declaration is for.
NOT_A_BOUND_NAME = re.compile(r"^_*(padding|pad|reserved|unused|spare)\d*$", re.I)
def sources(root: Path):
for top in ROOTS:
base = root / top
if not base.is_dir():
continue
for path in sorted(base.rglob("*.zig")):
if SKIP_PARTS & set(path.parts) or path.name in SKIP_FILES:
continue
yield path
def declaration_above(lines, index):
"""The `/// key: value` block immediately above line `index`, as a dict."""
fields = {}
i = index - 1
while i >= 0:
stripped = lines[i].strip()
if not stripped.startswith("///"):
break
body = stripped[3:].strip()
match = re.match(r"([a-z-]+):\s*(.+)", body)
if match:
fields[match.group(1)] = match.group(2).strip()
i -= 1
return fields
def problems_with(fields):
"""Why a declaration is not acceptable, or an empty list."""
missing = [f for f in FIELDS if f not in fields or not fields[f]]
if missing:
return ["missing " + ", ".join(missing)]
out = []
if fields["decided-by"] not in DECIDED_BY:
out.append(f"decided-by must be one of {sorted(DECIDED_BY)}, not {fields['decided-by']!r}")
verb = fields["at-limit"].split()[0].strip("-:,").lower()
if verb not in AT_LIMIT:
out.append(
f"at-limit must start with one of {sorted(AT_LIMIT)}, not {verb!r}. "
"There is deliberately no way to say 'silent', 'drop', or anything meaning 'allow'"
)
if verb == "truncate" and len(fields["at-limit"].split()) < 3:
out.append("at-limit: truncate must say how a reader can TELL it happened")
return out
def find(root: Path):
"""Every bound-shaped declaration: (relative path, name, value, line, fields)."""
for path in sources(root):
rel = path.relative_to(root).as_posix()
lines = path.read_text(encoding="utf-8", errors="replace").split("\n")
for n, line in enumerate(lines):
if line.lstrip().startswith("//"):
continue
name = value = None
m = NAMED.match(line)
if m and NAME_IS_BOUND.search(m.group(1)):
name, value = m.group(1), m.group(2)
else:
m = ARRAY_DECL.match(line) or ARRAY_FIELD.match(line)
if m and not NOT_A_BOUND_NAME.match(m.group(1)):
name, value = m.group(1), m.group(2)
if name:
yield rel, name, value, n + 1, declaration_above(lines, n)
def main():
argv = [a for a in sys.argv[1:] if not a.startswith("--")]
listing = "--list" in sys.argv
root = Path(argv[0]) if argv else Path(__file__).resolve().parent.parent
allow_path = root / "tools" / "bounds-allowlist.txt"
allowed = set()
if allow_path.exists():
for raw in allow_path.read_text().split("\n"):
entry = raw.split("#", 1)[0].strip()
if entry:
allowed.add(entry)
undeclared, bad, seen = [], [], set()
for rel, name, value, line, fields in find(root):
key = f"{rel}:{name}"
seen.add(key)
if not fields:
(undeclared if key not in allowed else []).append((key, value, line))
continue
for why in problems_with(fields):
bad.append((key, line, why))
if key in allowed and not problems_with(fields):
bad.append((key, line, "now declared — delete its line from tools/bounds-allowlist.txt"))
if listing:
for key, value, line in sorted(undeclared):
print(f"{key} # = {value}, line {line}")
for key in sorted(allowed - seen):
print(f"# STALE: {key}")
return 0
stale = sorted(allowed - seen)
if not undeclared and not bad and not stale:
return 0
print("bounds check failed\n", file=sys.stderr)
for key, value, line in sorted(undeclared):
print(f" {key} (= {value}, line {line})", file=sys.stderr)
print(" no declaration. A ceiling states what it counts, who decides its", file=sys.stderr)
print(" size, what it protects, what happens at the limit, and how you", file=sys.stderr)
print(" find out. See docs/os-development/bounds.md.", file=sys.stderr)
for key, line, why in sorted(bad):
print(f" {key} (line {line}): {why}", file=sys.stderr)
for key in stale:
print(f" {key}: allowlisted but no longer found — delete its line", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())