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:
@@ -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",
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user