#!/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())