kernel: a refusal names its rule, and two bounds stop failing open
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.
This commit is contained in:
@@ -0,0 +1,212 @@
|
|||||||
|
# The bounds track: removing the numbers we invented
|
||||||
|
|
||||||
|
*Plan, 2026-08-08. Follows [fixed-bounds-audit.md](fixed-bounds-audit.md) (235 ceilings,
|
||||||
|
139 on quantities we do not choose) and the AMD Ryzen that found the first one.*
|
||||||
|
|
||||||
|
## The principles this is derived from
|
||||||
|
|
||||||
|
1. **danOS is a microkernel.** Minimise what the kernel is responsible for; move
|
||||||
|
responsibility to user space so it can be restarted, or fixed live during
|
||||||
|
development, without taking the system down.
|
||||||
|
2. **Implement the specifications correctly**, with the limits those specifications
|
||||||
|
define — not limits we decide.
|
||||||
|
3. **Move as much responsibility as possible to user space** (the device manager).
|
||||||
|
4. **What remains in the kernel is minimal.**
|
||||||
|
5. **What remains in the kernel is there for security or for a hardware limitation.**
|
||||||
|
Nothing else earns a place.
|
||||||
|
|
||||||
|
Principle 5 is the test every bound is put to. For each one: *is this here because of
|
||||||
|
security, or because of a hardware limitation?* If neither, the storage does not belong
|
||||||
|
in the kernel and the bound is not a number to be resized — it is a thing to be moved or
|
||||||
|
deleted.
|
||||||
|
|
||||||
|
Applying it to the case that started this:
|
||||||
|
|
||||||
|
- `maximum_devices = 64` bounds an inventory of hardware. An inventory is neither a
|
||||||
|
security control nor a hardware limitation. **The table is in the wrong place**; the
|
||||||
|
number is a symptom.
|
||||||
|
- `maximum_children_per_parent = 16` exists because `device_claim` is unauthenticated —
|
||||||
|
any process can claim any unclaimed device ([devices-broker.zig:164](../system/kernel/devices-broker.zig:164)
|
||||||
|
checks only that the device exists and is free). The cap is a crude proxy for an
|
||||||
|
authorisation the kernel does not perform. **Fix the authorisation and the cap has
|
||||||
|
nothing to defend.**
|
||||||
|
- `maximum_domains = 64` bounds IOMMU translation domains. Security — stays in the
|
||||||
|
kernel. But VT-d and AMD-Vi both *report* how many domains they support in a
|
||||||
|
capability register. Principle 2: read it. We chose 64 without asking.
|
||||||
|
|
||||||
|
## The security invariants
|
||||||
|
|
||||||
|
Every phase must leave all five standing. This is the "without punching a hole" half of
|
||||||
|
the brief, and each phase below states how it is checked.
|
||||||
|
|
||||||
|
- **I1 Containment.** A process may map only physical memory inside a resource it was
|
||||||
|
granted. A bus may subdivide only what it already holds.
|
||||||
|
- **I2 Confinement.** A DMA-capable device is under IOMMU translation before its driver
|
||||||
|
can program it, or it is not driven at all.
|
||||||
|
- **I3 No self-granted authority.** A process holds what it was handed. It cannot name
|
||||||
|
its way into holding more.
|
||||||
|
- **I4 Death releases everything.** Every resource a task held is reclaimed when it
|
||||||
|
dies, on every path out.
|
||||||
|
- **I5 Refusal is attributable.** Every refusal names the rule that refused it.
|
||||||
|
|
||||||
|
## Phase 0 — Done
|
||||||
|
|
||||||
|
- **Errno attribution.** One errno space in `system/abi.zig`; `device_register`'s six
|
||||||
|
refusals and `device_claim`'s three are distinct codes; call sites name the reason;
|
||||||
|
`pci-bus` reconciles found against registered. (I5)
|
||||||
|
- **Idempotency ordering.** A re-registration consumes no slot, so a full parent
|
||||||
|
re-admits an identical child. A restarted bus is no longer billed for what it
|
||||||
|
rediscovers.
|
||||||
|
|
||||||
|
Suite 114/114.
|
||||||
|
|
||||||
|
## Phase 1 — Reclamation
|
||||||
|
|
||||||
|
**Nothing may become dynamic before this.** Today `count` only ever increases and
|
||||||
|
`releaseAllOwnedBy` clears a dead driver's *claims* but not its *registrations*. With a
|
||||||
|
fixed table that is a slow march to the cap; with dynamic storage it is an unbounded
|
||||||
|
leak, and every supervisor restart makes it worse.
|
||||||
|
|
||||||
|
- Extend the existing death sweep so a task's registrations go with its claims.
|
||||||
|
- A registration whose owner is gone is removed; its children are re-parented or removed
|
||||||
|
with it (they cannot outlive the authority that published them).
|
||||||
|
- Test: register under a claimed parent, kill the owner, assert the entries are gone and
|
||||||
|
the ids are not reused while any handle to them lives.
|
||||||
|
|
||||||
|
Invariant: **I4**.
|
||||||
|
|
||||||
|
## Phase 2 — Close the authorisation hole
|
||||||
|
|
||||||
|
The device manager already decides which driver gets which device — it matches against
|
||||||
|
`devices.csv` and spawns the driver with the device id as `argv[1]`. Nothing binds that
|
||||||
|
decision to the kernel's `claim`. A driver passes an integer; the kernel checks only
|
||||||
|
that the device is free.
|
||||||
|
|
||||||
|
Per principles 3 and 5: **the decision stays in user space; the kernel enforces only
|
||||||
|
possession.** The manager hands the driver the device it matched; the kernel's job is
|
||||||
|
that a driver holds what it was handed and nothing else.
|
||||||
|
|
||||||
|
- The manager passes a device to the driver it spawned, over the existing cap-passing
|
||||||
|
path. Possession is the authority.
|
||||||
|
- `device_claim` stops being a way to *acquire* a device by naming it.
|
||||||
|
- Exclusivity stops being a broker refusing a second claimant and becomes the ordinary
|
||||||
|
property of a thing only one process was given.
|
||||||
|
|
||||||
|
**`maximum_children_per_parent` is deleted here**, because after this a bus driver's
|
||||||
|
children are the devices it actually enumerated under a bus it was actually given, and
|
||||||
|
the rogue-driver-fills-the-table threat the cap was written for no longer exists.
|
||||||
|
|
||||||
|
Invariants: **I3** (the point of the phase), **I1** (containment is unchanged and still
|
||||||
|
checked on every subdivision), **I5**.
|
||||||
|
|
||||||
|
Acceptance: a driver that names a device it was not given is refused, with its own
|
||||||
|
errno. The QEMU suite gains an adversarial case for it — the audit's lesson was that
|
||||||
|
"the suite contains no attacker".
|
||||||
|
|
||||||
|
## Phase 3 — The inventory moves to user space
|
||||||
|
|
||||||
|
The kernel reads only three things out of a device descriptor: **physical ranges** (to
|
||||||
|
check a mapping falls inside one), **interrupt numbers**, and **one PCI BDF** (to key an
|
||||||
|
IOMMU domain). Vendor and device ids, class triples, subsystem ids, human-readable
|
||||||
|
names, bus numbers and parent links are stored solely so `device_enumerate` can hand
|
||||||
|
them back. That is the kernel acting as a distribution mechanism for data it does not
|
||||||
|
use — principle 5 excludes it.
|
||||||
|
|
||||||
|
- **Zero-resource devices leave the kernel entirely.** A USB device addressed through
|
||||||
|
its controller conveys no mapping authority; there is nothing for the kernel to
|
||||||
|
enforce. It is pure inventory and belongs to the device manager. (This is also the
|
||||||
|
case that sidesteps containment, which is why the cap existed.)
|
||||||
|
- Identity and topology move to the manager, which already receives them as
|
||||||
|
`child_added` reports and already holds the authoritative picture.
|
||||||
|
- `device_enumerate` retires; callers ask the manager, whose protocol already reserves
|
||||||
|
an `enumerate` verb. Public-ABI change — `docs/os-development/vdso.md` documents it.
|
||||||
|
- What the kernel keeps: for each device that carries resources, the ranges, the GSIs,
|
||||||
|
the BDF, and the owner.
|
||||||
|
|
||||||
|
After this, the kernel's table holds only resource-bearing devices, and the remaining
|
||||||
|
count is bounded by what the machine physically has rather than by us.
|
||||||
|
|
||||||
|
Invariants: **I1**, **I2** unchanged — both operate on resources, which do not move.
|
||||||
|
**I4** must be re-checked: the manager's table now needs its own reclamation, and it is
|
||||||
|
restartable, so it must be able to rebuild from the buses.
|
||||||
|
|
||||||
|
## Phase 4 — Ask the hardware and the specification
|
||||||
|
|
||||||
|
Principle 2, applied to every remaining bound. Each of these is a number the machine or
|
||||||
|
the standard already states, which we replaced with a guess. Independent of each other;
|
||||||
|
can proceed in any order.
|
||||||
|
|
||||||
|
| Today | Ask instead |
|
||||||
|
|---|---|
|
||||||
|
| `maximum_domains = 64` (IOMMU) | the VT-d / AMD-Vi capability register reports the domains supported |
|
||||||
|
| `max_devices = 8` (xHCI slots) | `HCSPARAMS1.MaxSlots` — the controller says (1–255) |
|
||||||
|
| `max_interfaces = 4`, `max_endpoints` | the configuration descriptor says |
|
||||||
|
| `blob: [512]u8` (USB config) | the device's `wTotalLength` |
|
||||||
|
| `below: [64]Range` (memory map) | UEFI reports the descriptor count |
|
||||||
|
| AML blobs capped at 6 | the XSDT's length field gives the entry count |
|
||||||
|
| `maximum_cpus = 128` | the MADT entry count |
|
||||||
|
| `maximum_gsi = 24` | the I/O APIC's redirection-entry count; and more than one I/O APIC exists |
|
||||||
|
| MSI-X vectors | the capability's table-size field (up to 2048) |
|
||||||
|
|
||||||
|
Several of these are in user space already (xHCI, USB descriptors) and are ordinary
|
||||||
|
allocations — principle 1 means those are also the safest to do first, since a mistake
|
||||||
|
restarts a driver rather than the machine.
|
||||||
|
|
||||||
|
Two in this table are **also** correctness fixes the audit found, and should carry their
|
||||||
|
regression tests: the xHCI `max_interfaces` path misattributes a fifth interface's
|
||||||
|
endpoints to interface 3, and `below: [64]Range` silently turns occupied RAM into a PCI
|
||||||
|
aperture — which is an **I1 violation reachable on real hardware**, not merely a lost
|
||||||
|
device. That one is the highest-priority item in this phase.
|
||||||
|
|
||||||
|
## Phase 5 — What legitimately remains
|
||||||
|
|
||||||
|
After phases 1–4 the survivors should be only:
|
||||||
|
|
||||||
|
- **Pre-allocator storage**: the PMM's own frame bitmap, the memory map the loader hands
|
||||||
|
over, the bootstrap page tables. You cannot allocate the allocator. (Hardware/boot
|
||||||
|
limitation — principle 5 admits these.)
|
||||||
|
- **Interrupt-context storage**: the IST stack and anything an exception path touches
|
||||||
|
without allocating.
|
||||||
|
- **Wire structures** whose layout the other side of a trust boundary parses.
|
||||||
|
- **Facts that are not ceilings**: a page is 4096 bytes; an ACPI name segment is 4.
|
||||||
|
|
||||||
|
Each is declared per [bounds.md](os-development/bounds.md) — what it counts, who decides
|
||||||
|
its size, what it protects, what happens at the limit, how you find out. And two numbers
|
||||||
|
that must agree agree in code, not in a comment:
|
||||||
|
|
||||||
|
```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");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## The one that must not wait
|
||||||
|
|
||||||
|
[`iommu.zig:107`](../system/kernel/iommu.zig:107) — `if (device_id >= confined.len) return
|
||||||
|
true;` — returns *success* without confining. It is unreachable today only because
|
||||||
|
device ids stop at 64. **Phases 3 and 4 both change the device count, and either makes
|
||||||
|
it live.** Fix it before them: out of range must refuse, never allow. (I2)
|
||||||
|
|
||||||
|
This is also the standing rule the audit argues for: at a bound, the safe direction is
|
||||||
|
refusal. A ceiling that fails open is not a limit, it is a switch that turns the
|
||||||
|
protection off.
|
||||||
|
|
||||||
|
## How this is verified
|
||||||
|
|
||||||
|
- The QEMU suite is the arbiter at every step; it is 114 cases and must stay green.
|
||||||
|
- Each fix lands with a test that **fails before it** — as the idempotency reorder did,
|
||||||
|
where exactly one assertion flipped.
|
||||||
|
- Adversarial cases for I1–I3 specifically: the audit's six real defects were all found
|
||||||
|
by asking "what would an attacker do", and the suite had never asked.
|
||||||
|
- The Ryzen is the acceptance test. It is the machine that found this, and the one that
|
||||||
|
proves it fixed.
|
||||||
|
|
||||||
|
## Sequencing
|
||||||
|
|
||||||
|
Phase 1 gates everything. Phase 2 gates phase 3 — the inventory cannot move until
|
||||||
|
authority is sound, or moving it is the hole. Phase 4 is independent and its user-space
|
||||||
|
items are the safest work in the track. Phase 5 is the record of what survived.
|
||||||
|
|
||||||
|
The IOMMU fail-open is fixed before phase 3 or 4 touches the device count.
|
||||||
@@ -0,0 +1,403 @@
|
|||||||
|
# Fixed-bounds audit
|
||||||
|
|
||||||
|
*2026-08-07. A tree-wide audit of every compile-time ceiling on a runtime quantity,
|
||||||
|
commissioned after an AMD Ryzen desktop booted to a working compositor with no USB
|
||||||
|
and no storage. Seven parallel sweeps, adversarial verification of each finding, and
|
||||||
|
a completeness critic. 235 bounds confirmed.*
|
||||||
|
|
||||||
|
The audit was not commissioned to fix that machine. It was commissioned to answer a
|
||||||
|
different question: **why did a machine have to find this?** The answer is in the
|
||||||
|
first table below, and it is not that anyone failed to predict an AMD desktop.
|
||||||
|
|
||||||
|
## What the audit found
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| Bounds confirmed | **235** |
|
||||||
|
| Decided by hardware or external data, not by us | **139** (59%) |
|
||||||
|
| Recorded in `system/parameters.zig` | **5** (2%) |
|
||||||
|
| With no comment explaining the number at all | **97** (41%) |
|
||||||
|
| Silent when exceeded — no log, no counter, no error | **171** (73%) |
|
||||||
|
| Severity critical / high | **17 / 26** |
|
||||||
|
|
||||||
|
The project has a stated convention: tunables live in `system/parameters.zig` with
|
||||||
|
their reasoning attached. Two percent of them do. That is the finding — not any
|
||||||
|
individual number.
|
||||||
|
|
||||||
|
And 59% of these are not tunables at all. They are guesses about someone else's
|
||||||
|
computer: how many PCI functions a board has, how many SSDTs its firmware ships,
|
||||||
|
how many descriptors its memory map carries, how many interfaces a USB headset
|
||||||
|
declares. A fixed bound on a quantity the machine decides is not a knob. It is a
|
||||||
|
defect with a plausible-looking number in it.
|
||||||
|
|
||||||
|
## Why "raise the number" is not available
|
||||||
|
|
||||||
|
This is the part that matters most, and it was found by the completeness critic
|
||||||
|
rather than by any of the seven sweeps.
|
||||||
|
|
||||||
|
`system/kernel/iommu.zig:97` sizes the per-device confinement table by
|
||||||
|
`maximum_domains = 64` — "64 mirrors devices-broker's device cap" — and indexes it
|
||||||
|
by **device id**. Line 107:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
pub fn confineDevice(device_id: u64, bdf: u16, owner: u32) bool {
|
||||||
|
if (!active) return true;
|
||||||
|
if (device_id >= confined.len) return true; // unusual id; leave it to fail-open
|
||||||
|
```
|
||||||
|
|
||||||
|
It returns `true` — success — without confining the device. The doc comment three
|
||||||
|
lines above states the opposite invariant: *"a claim that can't be confined must not
|
||||||
|
stand."*
|
||||||
|
|
||||||
|
Device ids are assigned `d.id = count` with `count < maximum_devices`, so today ids
|
||||||
|
run 0–63 and that branch is unreachable. **It becomes reachable the moment
|
||||||
|
`maximum_devices` is raised above 64.** Every device with id ≥ 64 would then be
|
||||||
|
claimed by a ring-3 driver, reported as successfully confined, and left outside every
|
||||||
|
IOMMU domain — an unconfined DMA master with a driver holding it.
|
||||||
|
|
||||||
|
So the one-line fix for the Ryzen — raise 64 to 512 — is a privilege escalation. Not
|
||||||
|
inelegant: escalating. Nothing in the type system, the tests or the comments would
|
||||||
|
have caught it, because the two constants agree only by a sentence in a comment.
|
||||||
|
|
||||||
|
## The Ryzen was three bugs, not one
|
||||||
|
|
||||||
|
Each of these independently produces "the xHCI and SATA controllers are missing."
|
||||||
|
Fixing any one of them leaves the machine broken by the next.
|
||||||
|
|
||||||
|
1. **`devices-broker.zig:33`, `maximum_children_per_parent = 16`** — `pci-bus`
|
||||||
|
registers *every* discovered function as a direct child of the one host bridge, so
|
||||||
|
16 is the ceiling on PCI functions for the whole machine. Enumeration runs in
|
||||||
|
bus/device/function order, so low-numbered chipset functions consume all 16 and the
|
||||||
|
high-numbered controllers — xHCI at 0x14, SATA at 0x17 — are refused. This is the
|
||||||
|
one that fired.
|
||||||
|
2. **`device-manager.zig:174`, `maximum_children = 64`** — the *userspace* inventory
|
||||||
|
has its own flat table. ACPI contributes ~34 nodes before PCI is scanned. Refusal
|
||||||
|
returns `-ENOSPC`, which `pci-bus.zig:246` discards and `acpi.zig:257` swallows in
|
||||||
|
an empty `catch {}`. The log line "child added" is printed *before* the status is
|
||||||
|
consulted, so the log says the device was added when it was not.
|
||||||
|
3. **`acpi.zig:651`, `gaps = [3]Range`** — only three sub-4 GiB MMIO holes become PCI
|
||||||
|
bridge apertures. A BAR landing in a fourth hole fails containment and is refused,
|
||||||
|
indistinguishably from a full table.
|
||||||
|
|
||||||
|
Three ceilings, three teams of one, one symptom. "Raise the limit" would have moved
|
||||||
|
the failure to the next one and produced a second debugging session from a second
|
||||||
|
photograph.
|
||||||
|
|
||||||
|
## Two latent security findings
|
||||||
|
|
||||||
|
**`acpi.zig:627`, `below = [64]Range`.** Firmware memory-map entries below 4 GiB, used
|
||||||
|
to derive the PCI bridge's apertures *from the gaps between described regions*. Past the
|
||||||
|
64th entry: `continue`. A dropped region is not merely missing — it vanishes from the
|
||||||
|
"described space" the gap-finder subtracts, so occupied physical memory is concluded to
|
||||||
|
be a free MMIO hole and registered as a bridge aperture. `devices-broker.contains()`
|
||||||
|
then admits a child BAR covering RAM, and its claimant can `mmio_map` it: a ring-3
|
||||||
|
read/write window onto kernel memory. Real UEFI maps carry 60–200 descriptors; OVMF
|
||||||
|
carries 15–25, which is why the suite has never approached it.
|
||||||
|
|
||||||
|
**`devices-broker.zig:217`, the PCI requester id is a `u16`** with no segment field,
|
||||||
|
propagated unbroken into both IOMMU backends. On a multi-segment machine two physically
|
||||||
|
distinct functions alias onto one translation structure.
|
||||||
|
|
||||||
|
Both are the same shape as the finding above: a bound whose failure mode is not "we run
|
||||||
|
out" but "the protection silently stops applying."
|
||||||
|
|
||||||
|
## Bugs found in passing
|
||||||
|
|
||||||
|
Not bounds, but found by looking at what happens at the bound:
|
||||||
|
|
||||||
|
- **`usb-xhci-library.zig:1331`** — `else if (device.interface_count < max_interfaces)`
|
||||||
|
has no `else`, so at the limit `current` is left pointing at interface 3 and the 5th
|
||||||
|
interface's endpoints are appended to interface 3's array. A class driver bound to
|
||||||
|
interface 3 can be handed an endpoint belonging to another interface. The
|
||||||
|
alternate-setting arm one line above does `current = null` correctly.
|
||||||
|
- **`usb-xhci-library.zig:899/1102`** — `allocateDevice()` failing returns after
|
||||||
|
`enableSlot()` already succeeded, with no Disable Slot. Every failed attempt
|
||||||
|
permanently leaks a controller slot.
|
||||||
|
- **`acpi.zig:445`** — the AML loop reserves `maximum_resources - 2` slots and the
|
||||||
|
function then adds **four** more resources. At 5 AML blocks the FADT is lost; at 6+
|
||||||
|
the broad IRQ window is lost too, so every legacy-IRQ device (PS/2 keyboard at IRQ 1)
|
||||||
|
fails containment. Real firmware ships 5–15 SSDTs; QEMU ships 2.
|
||||||
|
- **`fat.zig:252`** — a `u64` protocol offset reaches a `u32` engine parameter through a
|
||||||
|
bare `@intCast`. In ReleaseSafe — this project's build mode — that panics.
|
||||||
|
- **`boot/efi.zig:318`** — the bootstrap page tables map [0, 4 GiB) and nothing checks
|
||||||
|
that the handoff buffers UEFI allocated (BootInformation, the memory-map pool, the
|
||||||
|
initial ramdisk) landed below it. Several firmware implementations allocate top-down.
|
||||||
|
Above 4 GiB it is a triple fault with no output at all.
|
||||||
|
|
||||||
|
## Headroom actually measured
|
||||||
|
|
||||||
|
Every other bound here is hypothetical. One is a schedule:
|
||||||
|
`system/configuration/protocol.csv`, the manifest deciding which binary may bind which
|
||||||
|
protocol name, is at **50 of 64 grant rows and 12,668 of 16,384 bytes** — 78% and 77%.
|
||||||
|
|
||||||
|
To its credit, both overflow paths log (`init.zig:147` and `:238`). It will fail
|
||||||
|
loudly. It will still fail.
|
||||||
|
|
||||||
|
## The rule this suggests
|
||||||
|
|
||||||
|
The audit's own exemplar turned out to be broken, which is worth recording. Two sweeps
|
||||||
|
held up `devices_broker.dropped` as the model the others should be rewritten against.
|
||||||
|
It is not: `register()` never increments it, and `kernel.zig:203` reads it once at boot
|
||||||
|
*before* `seedDisplay` and long before any ring-3 driver exists. It can only report
|
||||||
|
firmware-discovery losses — precisely the opposite of the runtime case under audit.
|
||||||
|
|
||||||
|
`parameters.maximum_cpus` is the one bound that meets the standard: hardware-determined
|
||||||
|
and bounded, but it states what happens to the surplus (parked) and how you find out
|
||||||
|
(`platform.cpusDropped()` → a WARNING at `kernel.zig:281`). Verified.
|
||||||
|
|
||||||
|
So:
|
||||||
|
|
||||||
|
1. **A fixed bound on a quantity the hardware or an external file decides is a defect,
|
||||||
|
not a tunable.** It does not need a better number; it needs to not be fixed. This
|
||||||
|
covers 139 of the 235.
|
||||||
|
2. **A bound that must exist states three things**: what it protects against, what the
|
||||||
|
system does when it is reached, and how an operator finds out. One of 235 does.
|
||||||
|
3. **Refusal must be attributable.** `process.zig:944` collapses five distinct
|
||||||
|
`RegisterError` variants into a bare `-1`; `pci-bus` can only log "register refused"
|
||||||
|
with no reason. Half this audit's difficulty was that the machine could not say
|
||||||
|
which ceiling it hit.
|
||||||
|
4. **Two constants that must agree may not agree by comment.** `maximum_domains = 64`
|
||||||
|
and `maximum_devices = 64` are coupled by prose, and the coupling fails open.
|
||||||
|
|
||||||
|
## Appendix: the inventory
|
||||||
|
|
||||||
|
Sorted by severity, then file. `Decided by` is the audit's judgment of who chooses the
|
||||||
|
quantity — the machine, an external file or peer, or us.
|
||||||
|
| File:line | Bound | Value | Decided by | At the limit | Observable |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| `boot/efi.zig:318` | <inline literal> (4 * gib) | 4 GiB | hardware | No check exists. buildBootstrapTables maps [0, 4 GiB) as identity + physmap 2 MiB leaves and stops; the only carve-out is the framebuffer window at ef… | Nothing on this path. No log, no error, no panic. The last thing printed is the unconditional con_out line at … |
|
||||||
|
| `library/device/model/device-abi.zig:81` | maximum_device_resources | 8 | hardware | Silent drop, but at a different site than claimed. The live enforcement is system/kernel/device-model.zig:114 `if (self.resource_count >= maximum_reso… | None on any drop path, and the code says otherwise: device-model.zig:111-112 claims "Silently drops beyond `ma… |
|
||||||
|
| `system/drivers/pci-bus/pci-bus.zig:228` | device.register refusal path (kernel maximum_chi… | 16 children per parent / 64 … | hardware | system/kernel/devices-broker.zig:305 `if (childCount(parent_id) >= maximum_children_per_parent) return error.TooManyChildren;` and :306 `if (count >= … | One line per lost function: `std.log.info("register refused for {d}:{d}.{d}", .{ bus, dev, function })` (pci-b… |
|
||||||
|
| `system/drivers/usb-xhci-bus/usb-xhci-library.zig:227` | max_interfaces | 4 | external-data | CORRECTION to this claim's description: it is not only a silent drop. `parseConfiguration` (1326-1342) has no else on `else if (device.interface_count… | None. usb-xhci-bus.zig:368/425 log `{d} interface(s)` with the truncated count; nothing says any were dropped … |
|
||||||
|
| `system/drivers/usb-xhci-bus/usb-xhci-library.zig:333` | max_devices | 8 | hardware | Two different behaviours, exactly as claimed. Root port, `setupDevice` (899-902): `const device = self.allocateDevice() orelse { std.log.info("port {d… | Root ports: one log line. Hub-attached: nothing. I confirmed the leak is real — the only Disable Slot in the f… |
|
||||||
|
| `system/drivers/usb-xhci-bus/usb-xhci-library.zig:1304` | <inline literal> blob: [512]u8 | 512 | external-data | Silent truncation of device-supplied data. Lines 1304-1307: `var blob: [512]u8 = undefined; const length = @min(configuration.total_length, blob.len);… | None. `configuration.total_length` is read at 1299 and never compared to `blob.len`, never logged. The `{d} in… |
|
||||||
|
| `system/drivers/usb-xhci-bus/usb-xhci-library.zig:1305` | blob (inline [512]u8, declared line 1304) | 512 bytes | external-data | Silent truncation of externally-supplied data. The @min clamps the GET_DESCRIPTOR request to 512; parseConfiguration then walks it and stops dead at l… | Nothing. No log line, no counter, no comparison of configuration.total_length against blob.len. The symptom is… |
|
||||||
|
| `system/drivers/usb-xhci-bus/usb-xhci-library.zig:1331` | max_interfaces (= 4), used as `else if (device.i… | 4 | external-data | CORRUPTING, not merely dropping. When interface_count == 4, neither branch of the if/else-if runs, so `current` is NOT cleared — it still points at in… | Nothing. No log, no counter, no error. The device enumerates, `port N device: ... 4 interface(s)` is logged (a… |
|
||||||
|
| `system/kernel/acpi.zig:445` | <inline literal> device_model.maximum_resources … | 6 AML blocks, inside an 8-re… | hardware | Silent double loss. The loop stops at 6 blocks, so SSDTs 7+ are never published. Then acpi.zig:454-463 adds the io_port grant (7th), the SCI irq (8th)… | Nothing at all. No counter, no log, no error. The ring-3 acpi service simply finds an acpi-tables node without… |
|
||||||
|
| `system/kernel/acpi.zig:445` | device_model.maximum_resources - 2 (inline expre… | 6 AML blocks, out of 8 total… | hardware | Silent drop, then a silent cascade. Resources are appended in order: up to 6 AML memory resources (445-448), io_port (454), the SCI irq (459), the bro… | Nothing at the kernel. The only downstream trace is the acpi service writing "acpi: no FADT on the node — powe… |
|
||||||
|
| `system/kernel/acpi.zig:627` | below | [64]Range | hardware | Silent skip: `if (region.base >= (1 << 32) or below_count == below.len) continue;`. Every region past the 64th is treated as 'not described', i.e. as … | None directly, though kernel.zig:133 does print `" regions : {d} - entries in the firmware memory map"`, w… |
|
||||||
|
| `system/kernel/acpi.zig:627` | below (inline [64]Range) | 64 entries | hardware | Silent drop, and — worse than a drop — a corrupted result. A dropped region is not merely missing from the list; it disappears from the "described spa… | Nothing at all. No counter, no log line. The kernel prints the device tree including the bogus aperture, with … |
|
||||||
|
| `system/kernel/acpi.zig:651` | gaps | [3]Range | hardware | Silent replacement of the smallest kept gap (acpi.zig:658-664): a fourth (or fifth) MMIO hole is simply forgotten. A PCI function whose BAR lands in a… | Nothing. No log, no counter, and the resulting failure is indistinguishable at the caller from a table-full re… |
|
||||||
|
| `system/kernel/device-model.zig:66` | maximum_resources | 8 | external-data | `addResource` (line 113-118) returns false without recording. I grepped every call site: system/kernel/acpi.zig lines 448, 454, 459, 460, 463, 552, 55… | Nothing at the drop. Downstream there are two ring-3 lines that name the symptom and misattribute the cause: s… |
|
||||||
|
| `system/kernel/devices-broker.zig:33` | maximum_children_per_parent | 16 | hardware | Confirmed at devices-broker.zig:305: `if (childCount(parent_id) >= maximum_children_per_parent) return error.TooManyChildren;`, where `childCount` (28… | One reasonless line per lost device, and nothing from the kernel. Confirmed at pci-bus.zig:228-231: `const reg… |
|
||||||
|
| `system/services/acpi/acpi.zig:129` | blocks | [8][]const u8 | external-data | The `blocks: [8][]const u8` array at line 129 and its `if (block_count == blocks.len) continue;` at line 152 are dead — they can never fire, because t… | Only an indirect count that cannot reveal the loss: `std.log.info("parsed {d} AML blob(s), {d} namespace devic… |
|
||||||
|
| `system/services/device-manager/device-manager.zig:174` | maximum_children | 64 | hardware | Silent drop with a misleading log. `addChild` (180-194) returns false when no free slot exists. `onChildAdded` (442-452): `if (!addChild(...)) status … | Effectively none, and actively misleading. The -ENOSPC goes back to the bus driver, which discards it — verifi… |
|
||||||
|
| `boot/efi.zig:108` | <inline literal> (handles[0]) | 1 | hardware | Silent selection of GOP handle 0, and the two halves genuinely disagree — nativeResolution (efi.zig:167-176) iterates `for (handles) \|h\|` over every… | Nothing. No log names the handle count, the chosen adapter, or the resolved mode; the only con_out writes in t… |
|
||||||
|
| `library/device/driver/driver.zig:149` | <inline literal> | @min(total, buffer.len) | hardware | Silently searches a prefix: `const total = enumerate(buffer); const n = @min(total, buffer.len); for (@as([]DeviceDescriptor, buffer[0..n])) \|d\| { …… | Nothing. The discarded `total` is the only evidence that the buffer was too small, and it is thrown away on th… |
|
||||||
|
| `library/kernel/file-system.zig:227` | Entry.name_buffer | [64]u8 | external-data | Silent truncation on both readdir paths, exactly as claimed. Backend path, Directory.next line 264-266: `const nlen = @min(@min(@as(usize, header.name… | Nothing. No log, no flag, no short-count anywhere in library/kernel/file-system.zig. Entry.name() returns a pl… |
|
||||||
|
| `system/drivers/virtio-gpu/virtio-gpu.zig:49` | max_width / max_height (line 50) | 800 x 600 | hardware | Two distinct behaviours, and the claim conflates their visibility. (a) onSetMode, line 533-540: `if (w == 0 or h == 0 or w > max_width or h > max_heig… | Partial and misleading, as claimed. Confirmed the two adjacent lines: `std.log.info("EDID preferred mode {d}x{… |
|
||||||
|
| `system/kernel/architecture/x86_64/cpu.zig:489` | irq_vector_count | 14 | hardware | irq.zig:138 allocVector() scans base..base+count for a vector free of both an MSI binding and a GSI binding and returns null when none is; bind (irq.z… | No kernel log line. Driver-side, both messages verified: usb-xhci-bus.zig:279 `std.log.info("msi_bind unavaila… |
|
||||||
|
| `system/kernel/architecture/x86_64/idt.zig:17` | gate_count | 48 | our-design | Nothing is enforced at this line. `pub fn init()` at line 136 does `inline for (0..gate_count) \|vector\| { const stub = @extern(*const anyopaque, .{ … | None at runtime. The bound only ever becomes visible as the vector-exhaustion path in irq.zig, which each driv… |
|
||||||
|
| `system/kernel/architecture/x86_64/ioapic.zig:21` | base (module-level singleton) | 1 I/O APIC | hardware | Confirmed exactly. system/kernel/acpi.zig:547-556 adds EVERY MADT I/O APIC record to the device tree as ioapic0, ioapic1, ...; kernel.zig:545-548 then… | None. No log counts the discarded units, unlike the neighbouring drop counters that ARE logged (devices_broker… |
|
||||||
|
| `system/kernel/architecture/x86_64/iommu-amd.zig:56` | ring_entries (event log) | 256 | hardware | Confirmed. faultDrain (lines 180-198) reads EventHead/EventTail, walks the ring wrapping at `if (head >= ring_entries * 16) head = 0;`, and writes the… | None. I grepped the file: `const reg_status = 0x2020;` appears at line 32 and at NO other line — it is declare… |
|
||||||
|
| `system/kernel/architecture/x86_64/iommu-intel.zig:102` | <inline literal> 16 * 1024 | 16384 (bytes of VT-d registe… | hardware | No check. `faultDrain` computes `frcd_base = fro*16` with fro up to 0x3FF (16368 bytes) and `nfr = ((cap >> 40) & 0xFF) + 1` up to 256 registers of 16… | A kernel-mode #PF: the fault handler reports the vector/CR2 and halts the core. Loud, but reported as a page f… |
|
||||||
|
| `system/kernel/architecture/x86_64/iommu.zig:17` | Discovery.register_base (single unit) | 1 IOMMU unit | hardware | Confirmed. `pub const Discovery = struct { register_base: u64, amd: bool };` at iommu.zig:16-19 carries exactly one base. Intel (acpi.zig:826-852): th… | Asymmetric, exactly as claimed. Intel warns: system/kernel/iommu.zig:395-396 `if (info.iommu_extra_units > 0) … |
|
||||||
|
| `system/kernel/devices-broker.zig:25` | maximum_devices | 64 | hardware | Three inconsistent behaviours, all confirmed. Boot discovery: devices-broker.zig:111-114 `if (count >= maximum_devices) { dropped += 1; return device_… | Partial and aimed at the wrong path, exactly as claimed. kernel.zig:203-206 prints `"/system/kernel: WARNING {… |
|
||||||
|
| `system/kernel/iommu.zig:43` | maximum_domains | 64 | hardware | Two behaviours. Domain exhaustion is safe: `domainCreate` returns null → `confineDevice` false → process.zig:418-420 rolls the claim back and returns … | The rollback path is visible as a failed `device_claim`. The fail-open path is completely silent — no log, no … |
|
||||||
|
| `system/kernel/irq.zig:43` | maximum_gsi | 24 | hardware | Refusal at three places, all becoming a bare -1. irq.zig:158 `if (gsi >= maximum_gsi or !architecture.irqOwnsGsi(gsi)) return error.BadGsi;` -> proces… | Nothing in the kernel, but every caller logs: ps2-bus.zig:246 and :265, acpi.zig:315 "acpi: SCI irq_bind faile… |
|
||||||
|
| `system/kernel/irq.zig:139` | allocVector over architecture.irq_vector_count | 14 (vectors 33..46, from arc… | hardware | `fn allocVector() ?u8` (line 139) scans v in [irq_vector_base, irq_vector_base + irq_vector_count) = [33, 47) — 14 vectors — and returns null when all… | Better than claimed at the driver level, absent at the kernel level. The kernel logs nothing and has no counte… |
|
||||||
|
| `system/kernel/process.zig:546` | maximum_dma_regions | 256 | hardware | Confirmed, and the safety-invariant break is real. process.zig:549-556 `fn dmaRegistryAdd(object: ?*ipc.DmaRegionObject, physical: u64, len: u64, owne… | None whatsoever — confirmed. `dma_alloc` returns success, no log, no counter, no failed syscall. The symptom i… |
|
||||||
|
| `system/kernel/scheduler.zig:546` | maximum_tasks (via freeSlot in secondaryMain) | 48 (parameters.maximum_tasks… | hardware | Confirmed: `const t = freeSlot() orelse @panic("sched: task table full (AP idle task)");` at scheduler.zig:546, reached from `secondaryMain` with the … | A named panic on console/serial — loud, but the machine is dead and the message points at the task table rathe… |
|
||||||
|
| `system/kernel/vfs.zig:58` | maximum_mounts | 8 | our-design | FALSE SUCCESS plus a reference leak, confirmed end to end. installMount, vfs.zig:182: `const m = slot orelse return;` — it gives up silently when no s… | Worse than nothing: the syscall reports success, so the mounting service logs its own success — system/service… |
|
||||||
|
| `system/kernel/vfs.zig:86` | maximum_directories | 8 | external-data | Silent skip inside the boot walk: `if (directoryIndex(parent) == null and directory_count < maximum_directories)` (vfs.zig:141). An unregistered direc… | Nothing. No log, no counter, no `dropped` variable of the kind acpi.zig:542 and the device manager both mainta… |
|
||||||
|
| `system/kernel/vfs.zig:141` | maximum_directories (declared line 86) | 8 | external-data | Silent drop with a mount-level knock-on, confirmed. The ninth distinct ancestor is skipped by the guard at vfs.zig:141; it then fails to resolve (reso… | Nothing — no counter, no log line, no `dropped` variable of the sort acpi.zig:542 (cpu_information.dropped, pr… |
|
||||||
|
| `system/kernel/vfs.zig:182` | maximum_mounts (declared line 58) | 8 | our-design | False success, confirmed: installMount gives up at vfs.zig:182 (`const m = slot orelse return;`) and mountBackend returns true regardless at vfs.zig:3… | Nothing in the kernel. The mounting service logs its own success — fat.zig:173/:178/:183 all `std.log.info("mo… |
|
||||||
|
| `system/services/acpi/acpi.zig:542` | buffer (readHid) / Registered.hid / Notice.hid | [8]u8 | external-data | Both failures confirmed. (a) readHid (lines 540-566) accepts only an integer _HID it can EISA-decode: a method result must be `.integer`, and a static… | None for (a): a skipped device produces no line at all. For (b) the log at line 408, `std.log.info("power: not… |
|
||||||
|
| `system/services/acpi/acpi.zig:648` | descriptor.resources.len | 8 (maximum_device_resources,… | external-data | Silent drop of the surplus. `fn addResource(descriptor: *device.DeviceDescriptor, kind: device.ResourceKind, start: u64, len: u64) void { if (descript… | Nothing at the drop. The only echo is the per-device summary at line 241, `std.log.info("device {d} bus=acpi … |
|
||||||
|
| `system/services/device-manager/device-manager.zig:149` | maximum_drivers | 16 | hardware | `addDriver` (240-254) walks for a free slot and, finding none, falls through the loop to `std.log.info("driver table full; cannot supervise {s}", .{na… | One log line only (quoted above). No counter, no status to any caller, and it lands amid the stream of `child … |
|
||||||
|
| `system/services/device-manager/device-manager.zig:358` | <inline literal> | 64 | hardware | Silent truncation. Line 362-363: `const total = device.enumerate(buffer); const count = @min(total, buffer.len);` — the difference is computed and thr… | Nothing. No log mentions `total`. The only related line, `"/system/services/device-manager: no matchable devic… |
|
||||||
|
| `system/services/fat/fat.zig:73` | open_nodes | 32 | our-design | An error is returned, but a maximally confusing one: `const index = allocOpen() orelse return refused;` (line 242), where `refused` is `-envelope.ENOE… | Nothing on the server side — no log at all in allocOpen or onOpen. The client sees ENOENT and will report "fil… |
|
||||||
|
| `system/services/fat/fat.zig:252` | <inline literal> | u32 (via @intCast of a u64 p… | external-data | Panic. `vfs_protocol.Read.offset` is a `u64` (library/protocol/vfs/vfs-protocol.zig:82-86) and `engine.readFile` takes `offset: u32`; the bridge is a … | A process fault and whatever the supervisor logs about the death; nothing identifies the offending request or … |
|
||||||
|
| `boot/efi.zig:371` | maximum_bundled | 64 | external-data | Two different behaviours for one constant, confirmed. loadByManifest (efi.zig:507): `if (count.* == maximum_bundled) return;` — silent success with th… | Manifest path: nothing. Walk path: the unconditional con_out line at efi.zig:71-75, `log("EFI: no /system bina… |
|
||||||
|
| `boot/efi.zig:375` | maximum_tree_depth | 3 | our-design | `if (depth == maximum_tree_depth) continue;` (efi.zig:580) — the subdirectory is silently skipped and nothing beneath it is bundled. No error, no coun… | Nothing. The boot proceeds; the missing binary surfaces much later as an init spawn failure or a device-manage… |
|
||||||
|
| `boot/efi.zig:507` | maximum_bundled (declared line 371) | 64 | external-data | As described and verified: efi.zig:507 `if (count.* == maximum_bundled) return;` in loadByManifest is a silent stop keeping the first 64; efi.zig:590 … | Manifest path: nothing. Enumeration path: efi.zig:71-75 prints 'EFI: no /system binaries (TooManyBinaries) - b… |
|
||||||
|
| `boot/efi.zig:582` | <inline literal> (child_prefix bufPrint) | 64 bytes (initial_ramdisk.ma… | external-data | `const child = try std.fmt.bufPrint(&child_prefix, "{s}/{s}", .{ prefix, name });` into a [initial_ramdisk.maximum_name]u8 = [64]u8. error.NoSpaceLeft… | efi.zig:71-75 prints 'EFI: no /system binaries (NoSpaceLeft) - booting without user space' on con_out. Visible… |
|
||||||
|
| `boot/efi.zig:633` | ehdr.e_phnum / e_phoff / e_phentsize (no bound a… | unbounded — read straight fr… | external-data | Unchecked out-of-bounds read. efi.zig:632-636 forms `image.ptr + ehdr.e_phoff + i * ehdr.e_phentsize` as raw pointer arithmetic and @ptrCasts it — no … | Nothing until it faults or panics, at which point the machine is still in the firmware with no kernel. |
|
||||||
|
| `library/device/acpi/aml/interpreter.zig:158` | notify_queue | [16]NotifyEvent | external-data | Silent drop of the 17th and later Notify. `notify()` (line 571-576): `if (target) \|node\| { if (self.notify_count < self.notify_queue.len) { ...appen… | None. No log in interpreter.zig at all (grepped: zero std.log/log.print/logging.write calls in the file). `tak… |
|
||||||
|
| `library/device/acpi/aml/interpreter.zig:523` | <inline literal> | 100_000 | our-design | Silently exits the loop and continues executing the method as if the loop had terminated normally: `while (guard < 100_000) : (guard += 1) { … }` then… | Nothing. No log, no `error.Unsupported`, no distinguishable result — the caller receives a normal-looking valu… |
|
||||||
|
| `library/device/acpi/aml/parser.zig:24` | maximum_segments (and its disagreeing twin, libr… | 64 in the parser, 16 in the … | external-data | Both files silently drop the surplus segment after consuming its 4 bytes. Parser: appendSegment, parser.zig:137-143. Interpreter: `fn segment(self: *C… | None on either path. Neither file logs; acpi.zig discards ParseResult.consumed/total; interpreter errors surfa… |
|
||||||
|
| `library/device/usb/usb.zig:167` | <inline literal> | 100 attempts × 20 ms = 2 s | our-design | Gives up and returns null: `const bus = while (attempts < 100) : (attempts += 1) { if (channel.openEndpoint("usb-transfer")) \|handle\| break handle; … | Nothing here. The driver's own bail-out is what an operator sees, with no indication that it was a timeout rat… |
|
||||||
|
| `library/kernel/channel.zig:124` | <inline literal> | @min(available, into.len) | our-design | Silent truncation to the caller's buffer: `const available = @min(answer.len - envelope.prefix_size, @as(usize, status.len)); const taken = @min(avail… | Weak: `Response.status.len` holds the true length the provider sent, so a caller *could* compare it against `p… |
|
||||||
|
| `library/kernel/file-system.zig:85` | <inline literal> | [224]u8 (twice: lines 77 and… | our-design | Errors invisibly, but the enforcing code is in the KERNEL, not where the claim points. system/kernel/process.zig:1898 rejects the input outright: `if … | None. A too-long path and a nonexistent file are both `null` from open(); no log on either side — the kernel's… |
|
||||||
|
| `library/kernel/logging.zig:82` | <inline literal> | [256]u8 | our-design | Truncation, marked with a `~` — but the truncated slice is the *whole* buffer, not the written prefix: `const line = std.fmt.bufPrint(&buffer, prefix … | The trailing `~` marks the line as truncated, which is good; the garbage tail is not marked at all. |
|
||||||
|
| `library/kernel/process.zig:211` | <inline literal> | [32]ProcessDescriptor | our-design | Silent false negative: `var table: [32]ProcessDescriptor = undefined; const total = processes(&table); for (table[0..@min(total, table.len)]) \|descri… | Nothing. The function returns a plain bool; the discarded `total` is the only evidence and it is thrown away b… |
|
||||||
|
| `library/protocol/device-manager/device-manager-protocol.zig:161` | entries_per_reply | (envelope.packet_maximum - e… | hardware | Silent cut with a success status. `onEnumerate` (device-manager.zig:524-536): `for (&children) \|*child\| { if (!child.used) continue; if (written + e… | None, and structurally impossible for a client to detect: the protocol's own comment (149-152) says the count … |
|
||||||
|
| `library/protocol/envelope/envelope.zig:133` | packet_maximum | 256 | our-design | Two behaviours, exactly as claimed. Compile time: `Define` rejects an oversized fixed part with a message naming protocol, verb, size, prefix and floo… | Compile-time half is excellent. Run-time half is silent everywhere I traced it: library/device/block/block.zig… |
|
||||||
|
| `library/protocol/envelope/envelope.zig:134` | post_maximum | 64 | our-design | Compile error for an event payload that does not fit (`Define`, envelope.zig:365-370, message names the event and the push floor). At run time `encode… | Compile-time: named and precise. Run-time: nothing. The `orelse return` at service.zig:239 is the whole handli… |
|
||||||
|
| `library/protocol/usb-transfer/usb-transfer-protocol.zig:58` | max_report_data | 40 | hardware | Two stacked silent truncations. First in the engine: usb-xhci-library.zig:1569 `const n = @min(length, report.data.len);` against a `data: [64]u8` (li… | None. No log at either truncation site; the surrounding comment (usb-xhci-bus.zig:766-768) states the truncati… |
|
||||||
|
| `library/protocol/usb-transfer/usb-transfer-protocol.zig:62` | max_reported_endpoints | 4 | hardware | Silent drop, and the primary site is upstream of the one claimed. The bus driver discards surplus endpoint descriptors while parsing the configuration… | None. No log at the parse-time drop, none at the client cap; a driver's `findEndpoint` just returns null for a… |
|
||||||
|
| `system/boot-handoff.zig:143` | kernel_segments | 8 | our-design | Silent drop with a delayed fatal consequence, as described. boot/efi.zig:659-668 records only `if (n < boot_information.kernel_segments.len)` with no … | The loader says nothing. The kernel prints the count it received — kernel.zig:159 `log.print(" kernel segs: {… |
|
||||||
|
| `system/drivers/pci-bus/pci-bus.zig:81` | <inline literal> alloc(device.DeviceDescriptor, … | 64 | our-design | Not reachable today, so nothing happens. If the kernel table grew past the buffer: `device.enumerate(buffer)` returns the TOTAL, the search clamps wit… | One misleading line, `std.log.info("device {d} not in the device tree", .{bridge_id})` — it names a missing de… |
|
||||||
|
| `system/drivers/pci-bus/pci-bus.zig:81` | 64 (bare inline literal — no constant, no link t… | 64 | our-design | Same site as the earlier claim on line 81 — this is a duplicate. Unreachable today (`total` is bounded by the kernel's own maximum_devices = 64). If i… | `std.log.info("device {d} not in the device tree", .{bridge_id})` — misdescribes the cause and never prints `t… |
|
||||||
|
| `system/drivers/usb-hid/hid-report.zig:20` | KeyboardReport.keys: [6]u8, and max_transitions … | 6 concurrent keys; 14 transi… | hardware | THE CLAIM IS BACKWARDS. `max_transitions = 8 + 6` is derived from a wrong worst case, and `Transitions.add`'s guard (`if (self.count < self.items.len)… | None. `add` drops without logging and `Transitions` carries no overflow flag; `slice()` just returns the first… |
|
||||||
|
| `system/drivers/usb-storage/usb-storage.zig:61` | <inline literal> .lun = 0 in the CommandBlockWra… | 1 LUN (LUN 0 only) | hardware | There is no limit check — LUNs 1..15 simply do not exist to this driver. Every CBW is built with `.lun = 0` (usb-storage.zig:57-63), and I confirmed t… | Nothing. Nothing reads bMaxLUN, so nothing can report that a device has more than one logical unit. |
|
||||||
|
| `system/drivers/usb-storage/usb-storage.zig:121` | <inline literal> while (tries < 10) with time.sl… | 10 attempts = ~500 ms | hardware | Falls out of the loop and proceeds regardless (usb-storage.zig:120-127). INQUIRY's result is discarded (`_ = transact(...)`, line 129), then READ CAPA… | Present but misattributed, exactly as claimed: `_ = logging.write("/system/drivers/usb-storage: READ CAPACITY … |
|
||||||
|
| `system/drivers/usb-storage/usb-storage.zig:172` | <inline literal> @intCast(request.lba) -> u32, @… | LBA < 2^32, count < 2^16 (RE… | external-data | Bare, unchecked @intCast on both fields — verified there is no range check anywhere between the wire struct and the CDB. scsi.read10/write10 take (lba… | In a safe build, driver death with a fault exit reason and a device-manager restart with backoff — attributed … |
|
||||||
|
| `system/drivers/usb-storage/usb-storage.zig:172` | @intCast(request.lba) / @intCast(request.count) … | u64 -> u32 LBA, u32 -> u16 b… | external-data | Unchecked narrowing of two peer-chosen wire fields. library/protocol/block/block-protocol.zig:30-35 declares `Transfer { lba: u64, count: u32, physica… | Nothing in ReleaseFast; in a safe build a driver crash with a fault exit reason and a supervised restart, attr… |
|
||||||
|
| `system/drivers/usb-xhci-bus/usb-xhci-bus.zig:80` | opens (= [_]Open{.{}} ** 16) | 16 | our-design | Confirmed, including the success-on-failure shape. recordOpen (line 87) walks `opens` for a matching token, then for a free slot, and falls through to… | Nothing on the bus side — no log when the table fills, and the harness closes the unclaimed report endpoint si… |
|
||||||
|
| `system/drivers/usb-xhci-bus/usb-xhci-bus.zig:728` | prev_connected (declared line 326 as [64]bool), … | 64 | hardware | Confirmed off-by-one. Line 728: `while (port <= engine.max_ports and port <= prev_connected.len) : (port += 1)` with `var port: u32 = 1` — at port == … | The OOB half surfaces as a driver fault and a supervised restart into the same panic — loud but attributed to … |
|
||||||
|
| `system/drivers/usb-xhci-bus/usb-xhci-bus.zig:728` | prev_connected (inline [64]bool, declared line 3… | 64 | hardware | Same site as the earlier line-728 claim — this is a duplicate. Two failures, both confirmed. (1) Off-by-one out-of-bounds: `while (port <= engine.max_… | The panic half is loud (process fault + supervised restart loop) but misattributed. The silent-drop half has n… |
|
||||||
|
| `system/drivers/usb-xhci-bus/usb-xhci-library.zig:228` | max_endpoints_per_interface (and max_configured_… | 4 per interface, 16 configur… | hardware | Parsing: `if (interface.endpoint_count < max_endpoints_per_interface)` with no else — the 5th endpoint descriptor is silently discarded, so a class dr… | The endpoint-drop during parsing is entirely silent. The ring-exhaustion path reaches the class driver as a ge… |
|
||||||
|
| `system/drivers/usb-xhci-bus/usb-xhci-library.zig:228` | max_endpoints_per_interface | 4 | external-data | Silent drop, no else (1345-1354). An endpoint never recorded cannot be found by `endpointForAddress` (1377-1382), so a class driver's subscribe or bul… | None at the drop. The secondary consequence the claim names is real and I verified it: usb-xhci-bus.zig:612 se… |
|
||||||
|
| `system/drivers/usb-xhci-bus/usb-xhci-library.zig:328` | <inline literal> Report.data: [64]u8 (and usb_tr… | 64 in the driver, then 40 ov… | hardware | Truncated twice, silently: `const n = @min(length, report.data.len);` in enqueueReport, then `const n = @min(report.length, usb_transfer_protocol.max_… | Nothing. Both truncations are @min with no branch and no log. |
|
||||||
|
| `system/drivers/usb-xhci-bus/usb-xhci-library.zig:334` | max_subscriptions | 8 | hardware | Two paths, as claimed. Class driver: `subscribeInterrupt` (1482) `const subscription = self.allocateSubscription() orelse return false;` -> usb-xhci-b… | Class drivers log it: usb-hid/keyboard.zig:94 and mouse.zig:57 both write `interrupt subscribe failed`. The hu… |
|
||||||
|
| `system/drivers/usb-xhci-bus/usb-xhci-library.zig:335` | report_queue_capacity | 16 | hardware | `enqueueReport` (1561-1562): `if (self.report_count >= self.report_queue.len) return; // full: drop the newest` — the report is discarded and the subs… | None. The comment in the source is the only trace; no log, no counter. The symptom is dropped keystrokes or a … |
|
||||||
|
| `system/drivers/usb-xhci-bus/usb-xhci-library.zig:968` | hub_change_mask: u32 (declared line 298) and the… | 31 usable downstream ports | hardware | Ports 32..255 are never seeded and never serviced: the seed clamps to 0xFFFF_FFFE for hub_ports >= 31, and takeHubChange picks bits via @ctz on a u32,… | Nothing. No log line mentions bNbrPorts exceeding what the mask holds; the 'N downstream ports powered' line p… |
|
||||||
|
| `system/drivers/usb-xhci-bus/usb-xhci-library.zig:1456` | <inline literal> `.status = length` on the bulk … | 65535 (the xHCI TRB Transfer… | hardware | Unchecked write of a u32 into a 17-bit field. `bulkTransfer` (1452-1458) pushes `.status = length` with no mask and no split across TRBs; the status d… | Nothing on the way in — no check, no log. It surfaces later as a short/failed transfer or a lost completion at… |
|
||||||
|
| `system/drivers/virtio-gpu/virtio-gpu.zig:49` | max_width / max_height | 800 x 600 | hardware | set_mode errors visibly (line 536 refuses anything larger). But the EDID path is the silent one: readEdid (lines 413-442) parses the panel's preferred… | One std.log.info line naming the preferred mode, in the driver's log file, with nothing saying it was ignored. |
|
||||||
|
| `system/drivers/virtio-gpu/virtio-gpu.zig:208` | <inline literal> descriptors: [64]device.DeviceD… | 64 | hardware | NOT reachable today, contrary to the claim's framing. The kernel's own table is the binding wall: system/kernel/devices-broker.zig:25 `const maximum_d… | `device {d} not in the device tree` (line 213), which today only means a real absence. The kernel-side drop th… |
|
||||||
|
| `system/initial-ramdisk.zig:26` | maximum_name | 64 bytes | our-design | Four different behaviours, verified individually. (a) Capsule build: tools/pack-system-image.py:35-37 writes 'pack-system-image: path too long (>63)' … | (a) clear build error. (b)(c)(d) nothing: boot/efi.zig has exactly two output helpers (lines 773 and 795) and … |
|
||||||
|
| `system/kernel/acpi.zig:94` | overrides | [16]IsoEntry | hardware | Silent drop: `if (platform_information.override_count < platform_information.overrides.len) { ... }` with no else branch and no counter — unlike the C… | Nothing. A lost override means a legacy line is routed at the wrong GSI or the wrong polarity, which shows up … |
|
||||||
|
| `system/kernel/acpi.zig:94` | PlatformInformation.overrides | [16]IsoEntry | hardware | Silent drop, at three separate 16s that must agree by hand: system/kernel/acpi.zig:558 (no else branch), system/kernel/kernel.zig:248 (`var isos: [16]… | Nothing at any of the three clamps. No counter of the cpusDropped kind, no log line. |
|
||||||
|
| `system/kernel/acpi.zig:186` | aml_block_physical / aml_block_len | [32]u64 / [32]usize | hardware | Confirmed at acpi.zig:190-197: `fn addAmlBlock(sdt_physical: u64) void { if (aml_block_count >= aml_block_physical.len or sdt_physical == 0) return; .… | None — confirmed, no log and no dropped counter, unlike `cpu_information.dropped` (acpi.zig:539) and `rmrr_ski… |
|
||||||
|
| `system/kernel/architecture/x86_64/apic.zig:320` | <inline literal> 100_000_000 (calibratePit guard… | 100000000 | our-design | Confirmed: the loop falls out and calibration proceeds on a window that never happened. `ticks_per_ms = elapsed / calib_ms` and `tsc_hz = (tsc_end -% … | No failure line, but not quite "none": kernel.zig:322 prints `timer online ({d} Hz tick; timer clock {d} MHz, … |
|
||||||
|
| `system/kernel/architecture/x86_64/apic.zig:629` | localId (return type u8) / apic_id << 24 | 8-bit APIC id (0..255) | hardware | The claimed truncations are not reachable; the real defect is an omission. `sendInit`/`sendStartup` (lines 174-184) take a `u32` but every caller pass… | None for the dropped type-9 records. `cpu_information.dropped` (acpi.zig:542) counts only overflow past the 12… |
|
||||||
|
| `system/kernel/architecture/x86_64/iommu-amd.zig:56` | ring_entries (command buffer) | 256 | our-design | Confirmed. submitCommand (lines 221-228) writes the two qwords, advances `command_tail += 16`, wraps modulo the ring, and writes the tail register — w… | None — no head check, no full-ring detection, no log. |
|
||||||
|
| `system/kernel/architecture/x86_64/iommu-amd.zig:74` | fault_log_budget | 32 | hardware | logFault (lines 200-211) opens with `if (fault_log_budget == 0) return;` — the record's bdf and address are discarded outright, with no suppressed-fau… | One "(further faults suppressed)" line (line 210) and then permanent silence for the detail. Correcting the cl… |
|
||||||
|
| `system/kernel/architecture/x86_64/iommu-amd.zig:246` | <inline literal> 100_000 | 100000 | our-design | Confirmed verbatim at lines 243-253: the spin loop polls the completion frame for the 0xC0FFEE sentinel, and at `if (spins > 100_000)` warns once behi… | One line for the whole boot: "/system/kernel: AMD-Vi COMPLETION_WAIT store not observed — proceeding (QEMU pro… |
|
||||||
|
| `system/kernel/architecture/x86_64/iommu-intel.zig:77` | fault_log_budget | 32 | hardware | `logFault` (lines 252-270) takes the else branch and does `faults_suppressed += 1`. The budget is only ever decremented (line 255) — nothing resets it… | One line at the transition: 'DANOS-IOMMU-FAULT: (further faults suppressed)' (line 266). After that a device D… |
|
||||||
|
| `system/kernel/architecture/x86_64/iommu-intel.zig:294` | <inline literal> 10_000_000 (spin64) | 10000000 | our-design | `if (spins > 10_000_000) return;` — the function returns as if the invalidation completed. Callers (`invalidateDomain`, `globalInvalidate`, `detach`) … | Nothing at all — a bare `return` with no message, unlike `spinStatus` which prints "WARNING VT-d status bit ne… |
|
||||||
|
| `system/kernel/architecture/x86_64/paging.zig:206` | <inline literal> 0xFEE00000 | 0xFEE00000 (one 4 KiB page) | hardware | Confirmed and worse than "inconsistent": the two halves disagree with no reconciliation. paging.zig:206 maps exactly one hardcoded page `mapPage(pml4,… | A boot-time kernel fault reported as a fault address, never as "LAPIC relocated". The parsed truth is thrown a… |
|
||||||
|
| `system/kernel/architecture/x86_64/serial.zig:112` | <inline literal> 5_000 (writeByte guard) | 5000 | hardware | The loop simply falls through and `setRegister(0, c)` runs anyway (serial.zig:111-113) — the byte is written into a transmit-holding register that may… | Nothing. The corruption appears in the serial log itself, which is the channel you would use to notice it. |
|
||||||
|
| `system/kernel/heap.zig:28` | heap_maximum | 64 * 1024 * 1024 | our-design | Confirmed mixed. `grow` returns false at heap.zig:63 `if (start + bytes > heap_base + heap_maximum) return false;` -> `rawAlloc` null -> std.mem.Alloc… | For the graceful callers, a bare -1 to user space with no reason. For scheduler.zig:704, a panic naming the st… |
|
||||||
|
| `system/kernel/heap.zig:161` | <inline literal> alignment ceiling | 16 | our-design | `if (alignment.toByteUnits() > 16) return null;` confirmed at heap.zig:161 inside `allocImpl`. Returning null from the vtable alloc is how the allocat… | None. No log, no distinct error; the caller sees error.OutOfMemory and investigates memory pressure. |
|
||||||
|
| `system/kernel/ipc-synchronous.zig:40` | MESSAGE_MAXIMUM | 256 | our-design | Two different behaviours, both confirmed. The cap itself errors visibly: `if (message_len > MESSAGE_MAXIMUM or reply_cap > MESSAGE_MAXIMUM) return -E2… | -E2BIG for the cap. Nothing for the truncation — no counter, no flag bit, no errno. |
|
||||||
|
| `system/kernel/ipc-synchronous.zig:75` | post_capacity | 16 | external-data | Deliberate silent drop of the OLDEST message: sendLocked, ipc-synchronous.zig:568 `if (endpoint.post_tail -% endpoint.post_head >= post_capacity) endp… | None, and I checked the struct: PostSlot (ipc-synchronous.zig:79-83) carries only length and sender_id — no se… |
|
||||||
|
| `system/kernel/ipc-synchronous.zig:513` | @min(caller.ipc_send_len, receive_cap) (no const… | whatever the receiver's buff… | external-data | Silent truncation. replyWait, ipc-synchronous.zig:512-513: `if (dequeueSender(endpoint)) \|caller\| { const n = @min(caller.ipc_send_len, receive_cap)… | Nothing — no counter, no errno, no flag bit anywhere on this path. |
|
||||||
|
| `system/kernel/log.zig:202` | buffer (log.print) | [256]u8 | our-design | Confirmed: the entire line is discarded, not truncated. Lines 201-204 verbatim: `pub fn print(comptime fmt: []const u8, args: anytype) void { var buff… | Nothing. The message never appears and its absence is indistinguishable from the code path not having run. Not… |
|
||||||
|
| `system/kernel/pmm.zig:94` | <inline literal> (implicit: bitmap must land bel… | 4 GiB, stated only in prose | hardware | Two distinct failures. If NO usable region is large enough: `@panic("pmm: no region large enough for the frame bitmap")` — loud and fatal. If the chos… | The panic is legible. The above-4-GiB case is a triple fault or an early #PF with essentially no diagnostics. |
|
||||||
|
| `system/kernel/process.zig:98` | maximum_shared_memory_pages | 8192 (32 MiB) | hardware | Confirmed at process.zig:790: `if (pages == 0 or pages > maximum_shared_memory_pages) return fail(state);` in `systemSharedMemoryCreate`, giving a cle… | No kernel log; -1 only — confirmed. And the ambiguity the claim notes is real: `pmm.allocContiguous` failing t… |
|
||||||
|
| `system/kernel/process.zig:104` | maximum_mmap_pages | 8192 (32 MiB) | our-design | Confirmed at process.zig:2050: `if (pages == 0 or pages > maximum_mmap_pages) return fail(state);` in `systemMmap` — a clean -1. The independent secon… | -1 only, no log — confirmed. Indistinguishable from arena exhaustion and from 'not a user process' (line 2049)… |
|
||||||
|
| `system/kernel/process.zig:118` | maximum_resolve_path | 224 | external-data | Confirmed at process.zig:1898: `if (path_len == 0 or path_len > maximum_resolve_path or path_ptr >= user_half_end or path_ptr + path_len > user_half_e… | No log; -1 only — confirmed. There is no ENAMETOOLONG in this path (the function does have `failErr(state, ipc… |
|
||||||
|
| `system/kernel/process.zig:1542` | exit_subscriber_capacity | 16 | hardware | Confirmed at process.zig:1573-1581: `subscribeExits` scans `for (&exit_subscribers) \|*slot\| { if (slot.* == null) { endpoint.refcount += 1; slot.* =… | Materially worse than claimed, and this is my main correction. The kernel does return a specific -ENOSPC, but … |
|
||||||
|
| `system/kernel/process.zig:1649` | timer_capacity | 16 | our-design | Confirmed at process.zig:1684-1692: `systemTimerBind` scans `for (&one_shot_timers) \|*slot\| { if (slot.* == null) { ...; return architecture.setSyst… | Effectively none, correcting the claim. The kernel's -ENOSPC is narrowed to a bool by library/kernel/time.zig:… |
|
||||||
|
| `system/kernel/process.zig:2172` | maximum_segments | 16 | external-data | Confirmed at process.zig:2200: `if (ehdr.e_phnum > maximum_segments) return error.BadElf;`. The claim's key observation is right — the test is on `e_p… | None — confirmed. `error.BadElf` is erased at process.zig:1018 and `system_spawn` returns -1, so a developer i… |
|
||||||
|
| `system/kernel/process.zig:2173` | maximum_pages | 256 | external-data | Confirmed at process.zig:2238, inside the PT_LOAD loop: `total_pages += seg.pages(); if (total_pages > maximum_pages) return error.ProgramTooBig;`. Th… | No kernel log — confirmed. `error.ProgramTooBig` is erased at the syscall boundary and becomes the same -1 as … |
|
||||||
|
| `system/kernel/scheduler.zig:206` | maximum_space_mappings | 16 | our-design | Clean refusal, confirmed. `recordSpaceMappingLocked` (scheduler.zig:343-355) walks `entry.mappings` for a null slot and `return false` when full; proc… | A bare -1 from `shared_memory_map`/`shared_memory_create` — the same value as a bad handle, an exhausted arena… |
|
||||||
|
| `system/kernel/vfs.zig:143` | @memcpy(d.path[0..parent.len], parent) — implici… | 64 == 64, by coincidence | external-data | Safe today, and safe with slightly more margin than the claim states. parentOf (vfs.zig:119-123) returns a strict prefix ending before the last '/', s… | Nothing would warn. The coupling is invisible from either file: system/initial-ramdisk.zig:26-28 reasons about… |
|
||||||
|
| `system/kernel/vfs.zig:315` | name_out (via process.zig:1972 name_buffer) | [64]u8 | external-data | Silent truncation with a header that agrees with the truncation rather than reporting it: vfs.zig:314-317 `const n = @min(name.len, name_out.len); @me… | None. The contrast is fair — system/kernel/log-ring.zig sets abi.klog_flag_truncated for exactly this situatio… |
|
||||||
|
| `system/services/acpi/acpi.zig:81` | mmio_scratch | [4096]u8 | external-data | No overflow — the aliasing IS the failure. library/device/acpi/aml/interpreter.zig:708 (readRegionByte) and :720 (writeRegionByte) both do `const virt… | None. No log fires on a SystemMemory access in either the interpreter or the HAL, so an _STA or _CRS that cons… |
|
||||||
|
| `system/services/acpi/acpi.zig:113` | <inline literal> | 64 | hardware | Falls through silently and proceeds as though ACPI mode were enabled. `while (tries < 1000 and (halPioRead(2, pm1a_cnt) & sci_en_bit) == 0) : (tries +… | None that distinguishes the two outcomes. No log fires on give-up, and the success message "acpi: power button… |
|
||||||
|
| `system/services/acpi/acpi.zig:306` | <inline literal> | 1000 | hardware | Falls through silently and proceeds as though ACPI mode were enabled. `while (tries < 1000 and (halPioRead(2, pm1a_cnt) & sci_en_bit) == 0) : (tries +… | None that distinguishes the two outcomes. No log fires on give-up, and the success message "acpi: power button… |
|
||||||
|
| `system/services/device-manager/device-manager.zig:50` | registry_source | [8192]u8 | external-data | Silent truncation of the file. `loadRegistry` (63-68): `while (used < registry_source.len) { const n = file.read(registry_source[used..]) orelse break… | Nothing says the file was cut. Only downstream symptoms: `{d} malformed line(s) skipped` (line 70) if the frag… |
|
||||||
|
| `system/services/device-manager/device-manager.zig:51` | registry_source | 8192 bytes | external-data | Silent truncation of the file. `loadRegistry` (63-68): `while (used < registry_source.len) { const n = file.read(registry_source[used..]) orelse break… | Nothing says the file was cut. Only downstream symptoms: `{d} malformed line(s) skipped` (line 70) if the frag… |
|
||||||
|
| `system/services/device-manager/device-manager.zig:531` | <inline literal> | reply tail of a 256-byte pac… | our-design | Silent truncation with a success status: `if (written + entry_size > tail.len) break;` and the handler returns the byte count. There is no cursor and … | None. The client sees a short but well-formed list. |
|
||||||
|
| `system/services/display/backend.zig:27` | device_table | [64]device.DeviceDescriptor | hardware | Confirmed as written but NOT reachable today. findDisplay (lines 54-63) does `const total = device.enumerate(&device_table); const n = @min(total, dev… | Misleading when it does fire, as claimed: Gop.init exhausts its retries and writes `display: no framebuffer de… |
|
||||||
|
| `system/services/display/compositor.zig:288` | <inline literal> | src.len >= w*h*4, against a … | our-design | Silent no-op reported as success. blitTile does `if (src.len < @as(usize, w) * h * 4) return;`, but blitLayer (display.zig:236-242) ignores that, stil… | None whatsoever: no log, and a success status. The symptom is a blank region. |
|
||||||
|
| `system/services/display/display.zig:76` | maximum_layers | 16 | our-design | Confirmed. `createLayer` (display.zig:208-210) is `const slot = freeLayer() orelse return null;`, and `onCreateLayer` (695-700) is `const slot = creat… | Nothing server-side for a client-facing exhaustion — confirmed. The service's own cursor path does log (`_ = l… |
|
||||||
|
| `system/services/display/display.zig:395` | mode_list / list | [4]backend_mod.Mode | hardware | Silent truncation on both hops: the backend clamps with `const count = @min(@min(offered.count, scanout_protocol.max_modes), out.len);` (backend.zig:1… | None — no log names the number offered versus the number kept. The mode-set self-check only says "no alternate… |
|
||||||
|
| `system/services/fat/engine.zig:70` | sector_size | 512 | external-data | Mount is refused, confirmed. Both paths test `if (geometry.bytes_per_sector == sector_size)` — engine.zig:193 (bare FAT at LBA 0) and engine.zig:209 (… | A wrong diagnosis: `_ = logging.write("/system/services/fat: not a FAT filesystem\n")` at system/services/fat/… |
|
||||||
|
| `system/services/fat/engine.zig:193` | sector_size (declared line 70) | 512 | external-data | Refuses to mount rather than corrupting, confirmed at engine.zig:193 and the MBR-partition path at engine.zig:209; both fall through to `return null`. | system/services/fat/fat.zig:162 `_ = logging.write("/system/services/fat: not a FAT filesystem\n")` — false fo… |
|
||||||
|
| `system/services/fat/fat.zig:73` | open_nodes (inline [_]OpenNode{.{}} ** 32) | 32 | our-design | Error returned, but the WRONG error, and silently: `const index = allocOpen() orelse return refused;` where `const refused: isize = -envelope.ENOENT;`… | Nothing. No log line at exhaustion, and the client is told the file does not exist. An operator debugging this… |
|
||||||
|
| `system/services/fat/fat.zig:285` | <inline literal> | reply tail of a 256-byte pac… | our-design | Silent truncation with a success status: `const name_len = @min(listing.name_len, into.len);` then `return @intCast(name_len);` — the client receives … | None. A directory listing shows a chopped filename that then fails to open. |
|
||||||
|
| `system/services/fat/fat.zig:285` | into.len (inline; = envelope.packet_maximum - pr… | 224 bytes | external-data | Silent truncation, and the reply's name_len is set to the TRUNCATED value, so the client cannot tell there was more. The engine's own buffer holds 260… | Nothing. |
|
||||||
|
| `system/services/init/init.zig:70` | max_service_args | 4 | external-data | Silent drop of the surplus arguments: loadServices, init.zig:122-127 `while (it.next()) \|argument\| { if (argument.len == 0) continue; if (service.ar… | None, and the contrast with its immediate sibling is confirmed: the max_services cap ten lines above logs `"/s… |
|
||||||
|
| `system/services/init/init.zig:161` | maximum_bindings | 16 | our-design | An error is returned to the binder: onBind, init.zig:620-622 `const slot = for (&bindings) \|*binding\| { if (!binding.used) break binding; } else ret… | Weak, and the asymmetry is confirmed: within the same function the EPERM path logs (init.zig:602-608) and the … |
|
||||||
|
| `system/services/init/init.zig:162` | maximum_grants | 64 | external-data | Parsing stops at 64 rows: loadGrants, init.zig:237-240 `if (grant_count >= grants.len) { _ = logging.write("... protocol.csv has more rows than the ta… | The overflow itself is logged with a direct `logging.write` naming the file (init.zig:238). The consequences a… |
|
||||||
|
| `system/services/init/init.zig:220` | protocol_csv | 16384 bytes | external-data | Silent truncation mid-line, then a heuristic report. readConfiguration (init.zig:138-146) fills the buffer and stops; the tail rows vanish and the las… | One heuristic info log: init.zig:147 `if (used == into.len) std.log.info("{s} filled the read buffer — rows pa… |
|
||||||
|
| `system/services/init/init.zig:220` | protocol_csv (with maximum_grants = 64 rows alon… | 16384 bytes / 64 rows | external-data | Truncation, announced on both limits. readConfiguration reports the byte cap at init.zig:147 (`std.log.info("{s} filled the read buffer — rows past {d… | Both limits log, which is what makes this the calibration point. The byte-limit line rides std.log.info (the r… |
|
||||||
|
| `system/services/init/init.zig:274` | process_table | [64]process.ProcessDescripto… | our-design | Partly handled, partly not — confirmed exactly. refreshProcessTable (init.zig:278-282) sets `process_truncated = total > process_table.len`, and taskA… | A bind refusal outside identify does log (init.zig:602-608), but a failed identify in onBind returns -EPERM wi… |
|
||||||
|
| `boot/efi.zig:308` | pool_pages | 64 frames (256 KiB) | hardware | TablePool.alloc, efi.zig:264-265: `if (self.next >= self.cap) return error.OutOfBootstrapFrames;`. Every map2M/map4K path is `try`, so it unwinds clea… | Good, and verified: main (efi.zig:34-39) writes unconditionally to con_out — `log("\r\nEFI: boot failed: "); l… |
|
||||||
|
| `boot/efi.zig:543` | info_buffer | 1024 bytes | external-data | `const n = try directory.read(&info_buffer);` (efi.zig:545) into a [1024]u8. UEFI answers EFI_BUFFER_TOO_SMALL for an EFI_FILE_INFO that will not fit;… | efi.zig:71-75 on con_out, naming the UEFI error but not the file. |
|
||||||
|
| `boot/efi.zig:543` | info_buffer (inline [1024]u8) | 1024 bytes | external-data | `const n = try directory.read(&info_buffer);` (efi.zig:545) — EFI_BUFFER_TOO_SMALL becomes an error and the `try` propagates out of walkDirectory and … | efi.zig:71-75 on con_out; names the UEFI error, not the file. |
|
||||||
|
| `boot/efi.zig:660` | BootInformation.kernel_segments (declared system… | [8]KernelSegment | our-design | `if (n < boot_information.kernel_segments.len) { ... }` (efi.zig:659-668) with no else — the segment is copied to its physical address but never recor… | Nearly nothing, but not quite nothing: kernel.zig:159 prints `log.print(" kernel segs: {d} (mapped with W^X p… |
|
||||||
|
| `boot/efi.zig:680` | <inline literal> (attempts) and cap = info.len +… | 8 attempts; +8 spare descrip… | hardware | Correct and safe, as claimed. efi.zig:680-700: on each attempt both pool buffers are sized from the firmware's own descriptor count plus 8, a failed g… | The fatal case reaches main's handler: 'EFI: boot failed: ExitBootServicesFailed' on con_out, then hlt. No bre… |
|
||||||
|
| `boot/efi.zig:787` | buffer (logBytes) | 128 UTF-16 units | our-design | `if (i + 1 >= buffer.len) break;` (efi.zig:790) — the tail of the string is dropped, the result is NUL-terminated at the truncation point and printed.… | This IS the observability path — logBytes is the only way a runtime string (in practice an @errorName from mai… |
|
||||||
|
| `build/images.zig:152` | <inline literal> (mk_fat.addArg("64")) | 64 (MiB) | our-design | Build-time hard failure. tools/make-fat-image.py:67-68 — `if cluster >= self.cluster_count + 2: sys.exit("error: image out of clusters")` inside the c… | "error: image out of clusters" on stderr, which aborts the build. It names neither the file being added nor a … |
|
||||||
|
| `library/device/acpi/aml/interpreter.zig:55` | maximum_segments | 16 | external-data | Silent truncation in `Cursor.segment` (123-129): `const s = try self.take(4); if (name_path.count < maximum_segments) { ...store... }` — the cursor ad… | None. Line 299's comment `// unknown name -> treat as uninitialised` is by design, so a truncated name is indi… |
|
||||||
|
| `library/device/acpi/aml/parser.zig:24` | maximum_segments | 64 | external-data | appendSegment (parser.zig:137-143) always consumes the 4 name bytes via readNameSegment — so the cursor stays in sync — but silently discards the segm… | None. aml.zig:36-56 builds a ParseResult{consumed,total} precisely so a desync is detectable, but the only con… |
|
||||||
|
| `library/device/block/block.zig:95` | <inline literal> | 600 attempts × 50 ms = 30 s | our-design | Returns null; the caller (the FAT server) reports no volume. | Nothing logged at this level, but the number is justified against a measurement. |
|
||||||
|
| `library/device/driver/driver.zig:165` | lookup_attempts | 100 (× lookup_pause_ms = 20 … | our-design | `hello()` runs `while (attempts < lookup_attempts) : (attempts += 1) { if (channel.openEndpoint("device-manager")) \|handle\| break handle; time.sleep… | Good, and I confirmed it at driver.zig:181 — the else branch IS the log. Requiring callers also announce their… |
|
||||||
|
| `library/device/model/device-abi.zig:139` | DeviceDescriptor.hid | [8]u8 | hardware | No truncation is reachable today. The cited site (devices-broker.zig:121-122) cannot truncate: `node.hid()` returns `hid_buffer[0..hid_len]` from devi… | Not applicable — nothing truncates. The 8-byte-with-no-NUL width does cause a live defect elsewhere: system/se… |
|
||||||
|
| `library/device/pci/pci.zig:37` | bar_virtual | [6]usize | hardware | Errors: `pub fn mapBar(self: *Function, bar: u8) ?usize { if (bar >= 6) return null;` — a caller asking for BAR 6 gets null rather than an out-of-boun… | Null from `mapBar`, which each driver reports in its own words. |
|
||||||
|
| `library/device/pci/pci.zig:283` | CapabilityIterator.guard (and ExtendedCapability… | 48 and 480 | hardware | Iteration ends. Silent, but correctly so: the bound is derived from the size of the address space being walked, so reaching it means the device's chai… | Nothing — and it needs nothing, because the bound cannot cut a well-formed chain short. |
|
||||||
|
| `library/device/pci/pci.zig:286` | <inline literal> | 48 | hardware | The iterator silently ends: `if (self.cursor == 0 or self.guard >= 48) return null;` — indistinguishable from the end of a well-formed list. `findCapa… | Nothing. No log distinguishes 'no MSI capability' from 'capability list is a loop'. |
|
||||||
|
| `library/device/pci/pci.zig:371` | <inline literal> | 480 | hardware | Silently ends the iteration: `if (self.cursor == 0 or self.guard >= 480) return null;`. `findExtendedCapability` then reports absence. | Nothing. |
|
||||||
|
| `library/device/registry/device-registry.zig:200` | cols | [9][]const u8 | external-data | Errors and counts. parseLine: `if (count >= cols.len) return .malformed; // too many columns` and after the loop `if (count != cols.len) return .malfo… | Verified end to end, and it is genuinely good: system/services/device-manager/device-manager.zig:71 `if (resul… |
|
||||||
|
| `library/device/registry/device-registry.zig:239` | out_rules.len (caller-supplied) | caller's buffer | external-data | `if (result.count >= out_rules.len) { result.truncated = true; continue; }` inside parse — the surplus valid rules are dropped and the flag records it… | The best in the tree, and I confirmed the caller actually uses it: device-manager.zig:72 `if (result.truncated… |
|
||||||
|
| `library/kernel/channel.zig:37` | path_maximum | 224 | our-design | Refuses: `join` (285) `if (total > buffer.len) return null;`. `reach` (182-193) returns null when `file_system.fsResolve` cannot write the rewritten p… | None — null, no log, same documented three-way ambiguity. |
|
||||||
|
| `library/kernel/channel.zig:46` | name_maximum | 64 | our-design | Refuses, never truncates: `join` (282-290) `if (name.len == 0 or name.len > name_maximum) return null;`, which `openEndpoint` (238-242) turns into a n… | None (no log), and the null is deliberately three-way ambiguous per the doc at 231-237: no such contract / not… |
|
||||||
|
| `library/kernel/memory/heap.zig:199` | <inline literal> | 16 | our-design | Refuses cleanly: `fn allocImpl(_: *anyopaque, len: usize, alignment: std.mem.Alignment, _: usize) ?[*]u8 { if (alignment.toByteUnits() > 16) return nu… | An allocation failure, which std reports as OutOfMemory — a misleading name for an alignment refusal, and no l… |
|
||||||
|
| `library/kernel/process.zig:116` | <inline literal> | [8]u8 | our-design | The kernel refuses the *sender* rather than truncating (the receive capacity travels in r8 to `ipc_reply_wait`), so a client that calls into a process… | The refusal reaches the caller as an IPC failure; the stopping process logs nothing. |
|
||||||
|
| `library/kernel/process.zig:185` | <inline literal> | [256]u8 | our-design | Errors, cleanly: `if (len >= blob.len) return null;` and `if (len + argument.len > blob.len) return null;` — the spawn never happens and null is retur… | Poor. `spawn*` returning null is the same answer as 'no such binary' or 'task table full'; the caller (e.g. th… |
|
||||||
|
| `library/kernel/service.zig:94` | subscriber_capacity | 8 | our-design | onSubscribe (service.zig:264) walks `slots`, and if no slot is free returns `-envelope.ENOSPC`. The turn then closes the endpoint capability that arri… | No log in the provider. On the subscriber side the only real caller in the tree does log: system/services/disp… |
|
||||||
|
| `library/kernel/thread.zig:41` | tls_block_size | 64 | our-design | No check exists anywhere. thread.zig:88-90 carves `closure_addr` down from the stack top and then `const tls_base = (closure_addr - tls_block_size) & … | None — nothing tracks a TLS extent, so nothing could report it. The symptom would be a thread running with cor… |
|
||||||
|
| `library/protocol/display/display-protocol.zig:99` | max_modes | 4 | our-design | Silent drop of the surplus, but it can never fire. Provider side confirmed at system/services/display/display.zig:749-757: `var list: [4]backend_mod.M… | Nothing at either end — no log — but nothing is dropped either. |
|
||||||
|
| `library/protocol/scanout/scanout-protocol.zig:29` | max_modes | 4 | hardware | Silent truncation on both sides, exactly as claimed. Driver: virtio-gpu.zig:525-527 `var offered = scanout_protocol.Modes{ .count = offered_modes.len … | None. Confirmed: neither `modes()` in backend.zig nor the get_modes handler in virtio-gpu.zig emits anything a… |
|
||||||
|
| `library/xkeyboard-config/xkeyboard-config.zig:80` | map(layout, hid_usage: u8, …) | u8 (indexing Layout.keys: [2… | hardware | No overflow is possible — `keys: [256]Key` (library/xkeyboard-config/generated/layouts.zig:20) exactly covers the u8 domain, and an unmapped usage yie… | A key that maps to nothing produces no character; nothing distinguishes 'unmapped' from 'narrowed to the wrong… |
|
||||||
|
| `system/drivers/pci-bus/pci-bus.zig:36` | <inline literal> var line: [320]u8 (the per-func… | 320 bytes | our-design | `const text = std.fmt.bufPrint(&line, "...", .{...}) catch return;` at line 37 — the whole breadcrumb for that function is dropped and logFunction ret… | Nothing. The function simply does not appear in the boot log, which reads identically to "the walk did not fin… |
|
||||||
|
| `system/drivers/pci-bus/pci-bus.zig:182` | <inline literal> if (descriptor.resource_count >… | 8 | our-design | `break` out of the `while (i < 6)` BAR-sizing loop at line 181. Remaining BARs are never sized, never recorded in the descriptor and never restored-th… | Nothing. No log, no counter, and the loop's `configWrite16(bus, dev, function, 0x04, command)` decode-restore … |
|
||||||
|
| `system/drivers/ps2-bus/ps2-library.zig:464` | <inline literal> while (guard < 16) in drainOutp… | 16 bytes | hardware | Returns with the output-buffer-full bit possibly still set. No error, no retry, no signal to the caller (the function returns void). Callers are ps2-b… | None — silent void return. |
|
||||||
|
| `system/drivers/usb-hid/keyboard.zig:107` | <inline literal> receive: [64]u8 (identically at… | 64 | our-design | Currently sufficient with 4 bytes to spare: the protocol's own test asserts Protocol.event_maximum == 60 and event_maximum <= envelope.post_maximum (l… | Nothing here would report an over-long message; the driver's guard is `if (message.length < @sizeOf(hid.Keyboa… |
|
||||||
|
| `system/drivers/usb-xhci-bus/usb-xhci-bus.zig:521` | <inline literal> hid_buffer: [8]u8 (the "P<port>… | 8 bytes | our-design | Confirmed at lines 521-523: `var hid_buffer: [8]u8 = undefined; const hid_text = std.fmt.bufPrint(&hid_buffer, "P{d}I{d}", .{ port, interface.number }… | Nothing. `catch ""` swallows the bufPrint failure, register returns an existing id which looks like a normal i… |
|
||||||
|
| `system/drivers/usb-xhci-bus/usb-xhci-bus.zig:763` | <inline literal> if (serviced >= 32) break | 32 hub changes per tick | our-design | Deferral, not loss. `while (engine.takeHubChange()) \|change\| { ... serviced += 1; if (serviced >= 32) break; }` — the remaining changes stay in each… | Not needed; nothing is lost. There is no log, correctly. |
|
||||||
|
| `system/drivers/usb-xhci-bus/usb-xhci-library.zig:131` | trbs_per_ring (= page_size / @sizeOf(Trb)) | 256 | our-design | Correct wraparound. `push` (155-178) writes slot `enqueue_index`, and when the index reaches `trbs_per_ring - 1` (the Link slot) it re-installs the Li… | Not needed. |
|
||||||
|
| `system/drivers/usb-xhci-bus/usb-xhci-library.zig:456` | <inline literal> port_changes: [16]u32 | 16 | hardware | Silently dropped in `pump` (1610-1613): `if (self.port_change_count < self.port_changes.len) { self.port_changes[...] = port; self.port_change_count +… | Nothing at the drop site. Recovered in practice by the level reconcile in serviceController (usb-xhci-bus.zig:… |
|
||||||
|
| `system/drivers/usb-xhci-bus/usb-xhci-library.zig:456` | port_changes (inline [16]u32) | 16 | hardware | Silent drop, no else (pump, 1610-1613). The PORTSC change bits are already acknowledged at 1608-1609, so the edge is consumed even when the queue entr… | Nothing at the drop site; recovered by the independent per-port level reconcile at usb-xhci-bus.zig:727-741, w… |
|
||||||
|
| `system/drivers/usb-xhci-bus/usb-xhci-library.zig:496` | <inline literal> guard < 64 in the xECP capabili… | 64 extended capabilities | hardware | The `while (offset != 0 and guard < 64)` walk simply stops; a Supported Protocol capability past the 64th is not logged. Diagnostic only. | Nothing distinguishes 'chain ended' (the `if (next == 0) break;` at 509) from 'guard tripped'. |
|
||||||
|
| `system/drivers/usb-xhci-bus/usb-xhci-library.zig:603` | <inline literal> device_context_array = memory.d… | 512 entries (4096 / 8), vs M… | hardware | Safe, but only by accident of type width: slot_id is a u8 (`return @truncate(event.control >> 24)` in enableSlot), so `array[device.slot_id]` cannot e… | Not applicable today. |
|
||||||
|
| `system/drivers/usb-xhci-bus/usb-xhci-library.zig:1069` | <inline literal> while (tries < 200) with time.s… | 200 tries = ~1 s | hardware | Falls out of the loop, re-reads status at 1076, and if still not enabled: `std.log.info("hub slot {d} port {d}: reset did not enable", .{ hub.slot_id,… | Good — a specific log line naming hub slot and port, quoted above. |
|
||||||
|
| `system/drivers/virtio-gpu/virtio-gpu.zig:77` | queue_size | 16 | our-design | The only failure direction is a device offering fewer than 16, and it is checked and refused loudly — confirmed at lines 282-286: `const device_qsize … | Explicit log line carrying the device's actual value, then a clean bring-up failure. |
|
||||||
|
| `system/drivers/virtio-gpu/virtio-gpu.zig:163` | <inline literal> while (tries < 2000) in waitUse… | 2000 iterations ≈ 2 s | our-design | Returns false (line 172). submit returns false, command_nodata returns 0 (line 184), which never equals ok_nodata, so each bring-up call site logs its… | Good on the BRING-UP path only. The serving path is silent: presentFull's two failure returns (lines 461 and 4… |
|
||||||
|
| `system/drivers/virtio-gpu/virtio-gpu.zig:405` | <inline literal> .scanout_id = 0 (and resource_i… | 1 scanout, 1 resource, 1 bac… | hardware | No runtime limit exists — confirmed. Grepped the whole tree: `get_display_info`, `RespDisplayInfo` and `max_scanouts` appear ONLY in their declaration… | Nothing about head count, because it is never asked. The file header does state the scope: "this instance clai… |
|
||||||
|
| `system/drivers/virtio-gpu/virtio-gpu.zig:486` | <inline literal> while (tries < 50) with time.sl… | 50 tries = 1 s | our-design | Logs and returns, confirmed at lines 485-492: `const display = while (tries < 50) : (tries += 1) { if (channel.openEndpoint("display")) \|h\| break h;… | One clearly worded log line. The lasting consequence — the display stays on the boot framebuffer permanently —… |
|
||||||
|
| `system/drivers/virtio-gpu/virtio-gpu.zig:525` | <inline literal> .count = offered_modes.len set … | 2 offered vs 4 slots | our-design | Not reachable at current constants (offered_modes.len = 2 <= scanout_protocol.max_modes = 4), so nothing happens today. The shape is unsafe as describ… | Would be silent at the point of the bug; the downstream re-clamps mean the surplus is dropped rather than read… |
|
||||||
|
| `system/kernel/acpi.zig:130` | maximum_rmrr | 8 | hardware | Counted, not silent — confirmed at acpi.zig:873-882: `if (platform_information.rmrr_count < maximum_rmrr) { ...record... } else { platform_information… | Good, and I verified the log fires: system/kernel/iommu.zig:393-394 `if (info.rmrr_skipped > 0) log.print(" r… |
|
||||||
|
| `system/kernel/architecture/x86_64/cpu.zig:69` | systemCallArg (switch arms 0..5) | 6 arguments | our-design | `else => 0` — an argument index of 6 or more silently reads as zero rather than failing. Same shape at cpu.zig:788 (`pioRead` returns 0 for a width ot… | None; a caller asking for a seventh argument gets a plausible-looking 0. |
|
||||||
|
| `system/kernel/architecture/x86_64/gdt.zig:28` | entries | 7 | our-design | Compile-time only. `const entries = 7;` sizes the template (gdt.zig:36-44) and both gdts rows; setTssFor writes fixed indices 5 and 6, and loadOnThisC… | n/a — a build-time constraint. |
|
||||||
|
| `system/kernel/architecture/x86_64/gdt.zig:50` | gdts | [maximum_cpus][7]u64 (128 ta… | hardware | `loadOnThisCpu(cpu)` (gdt.zig:78-84) takes `@intFromPtr(&gdts[cpu])` with no guard in this file; in a safe build an out-of-range cpu is a bounds-check… | Via the ACPI/SMP path only: kernel.zig:281 prints 'cpus : WARNING {d} core(s) beyond pool cap dropped' from pl… |
|
||||||
|
| `system/kernel/architecture/x86_64/ioapic.zig:24` | overrides | [16]IsoEntry | hardware | Silent truncation at three independent layers, all verified: acpi.zig:558 `if (platform_information.override_count < platform_information.overrides.le… | None at any of the three layers — no counter, no log. |
|
||||||
|
| `system/kernel/architecture/x86_64/iommu-intel.zig:284` | <inline literal> 10_000_000 (spinStatus) | 10000000 | our-design | Logs a warning and returns; `enable()` then sets `gcmd_shadow \|= gcmd_te` and the core reports the IOMMU as online even though translation may not be… | "/system/kernel: WARNING VT-d status bit never set — translation may be incomplete" — a real log line, which i… |
|
||||||
|
| `system/kernel/architecture/x86_64/paging.zig:70` | bootstrap_physmap_limit | 4 << 30 (4 GiB) | hardware | `@panic("paging: table frame above the 4 GiB bootstrap physmap")` at paging.zig:84-85, guarded by `if (!on_own_tables and frame >= bootstrap_physmap_l… | A named panic message on the console/serial. The 9-line comment at lines 55-62 states the window, why it holds… |
|
||||||
|
| `system/kernel/architecture/x86_64/per-cpu.zig:50` | blocks | [maximum_cpus]ArchitecturePe… | hardware | `setLocal(index, …)` and `setKernelRsp(index, …)` index unguarded; bounded upstream, ReleaseSafe bounds check as backstop. A ReleaseFast build would w… | Nothing in this file. |
|
||||||
|
| `system/kernel/architecture/x86_64/serial.zig:83` | <inline literal> 10_000 (probe guard) | 10000 | hardware | The loop ends, `echo = register(0)` is read regardless, and `return echo == 0xAE` (serial.zig:83-86). A false answer sets uart_present = false, after … | Recoverable but indirect: present() is exposed (serial.zig:91-93) and the boot log reports the posture, and re… |
|
||||||
|
| `system/kernel/architecture/x86_64/smp.zig:124` | <inline literal> (1 << 32) | 4294967296 | our-design | `if (cr3 >= (1 << 32)) @panic("smp: kernel page tables above 4 GiB");` — the first statement of startAp, before arm() and before any INIT/SIPI is sent… | Named panic message that states the actual condition. |
|
||||||
|
| `system/kernel/architecture/x86_64/smp.zig:141` | <inline literal> @intCast(tramp_physical >> 12) | u8 SIPI vector (frame must b… | hardware | `const vector: u8 = @intCast(tramp_physical >> 12);` — in a safety-checked build (the default), a trampoline frame at or above 1 MiB triggers a "cast … | A safety-check panic with the generic cast message — it does not name the trampoline or the 1 MiB constraint. … |
|
||||||
|
| `system/kernel/architecture/x86_64/smp.zig:149` | <inline literal> 100 (ms AP wake window) | 100 | our-design | `const deadline = apic.millis() + 100;` then a pause-spin on ap_alive; on expiry `return false`. The caller at system/kernel/kernel.zig:437-444 retrie… | Good, and the best in this set. On give-up: `log.print("/system/kernel: cpu apic_id {d}: no response after {d… |
|
||||||
|
| `system/kernel/architecture/x86_64/tss.zig:48` | tss_table / ap_ist_top | [maximum_cpus]Tss and [maxim… | hardware | `rsp0Ptr` (62-64), `setApIstStack` (68-70) and `setupThisCpu` index `tss_table[cpu]` / `ap_ist_top[cpu]` with no guard in this file; the bound is enfo… | Reported by the layer above, and it really is reported: kernel.zig:280-281 `if (platform.cpusDropped() > 0) lo… |
|
||||||
|
| `system/kernel/device-model.zig:72` | name_buffer | [24]u8 | our-design | Silent truncation in `setName` (lines 94-98, `@min` then `@memcpy`), reached from `DeviceTree.init` (line 138) and `addChild` (line 159). In practice … | None, but nothing incorrect happens either; names are diagnostic only (the boot dump), never a matching key. |
|
||||||
|
| `system/kernel/device-model.zig:72` | Device.name_buffer | [24]u8 | our-design | Silent truncation in setName; unreachable in practice. See the kernel-core duplicate: every caller passes either a literal or the result of a bufPrint… | None needed. |
|
||||||
|
| `system/kernel/ipc-synchronous.zig:71` | POST_MAXIMUM | 64 | our-design | `if (len > POST_MAXIMUM) return -E2BIG;` — sendLocked, ipc-synchronous.zig:566, before anything is touched. A distinct errno straight back to the call… | -E2BIG, specific and actionable. |
|
||||||
|
| `system/kernel/ipc-synchronous.zig:113` | notify_buffer | [8]u64 | our-design | Silent drop of the NEWEST badge: notifyLocked, ipc-synchronous.zig:596-602 — the store happens only inside `if (endpoint.notify_tail -% endpoint.notif… | None, and the file argues at 593-595 why that is correct for a level rather than merely tolerable. |
|
||||||
|
| `system/kernel/ipc-synchronous.zig:113` | Endpoint.notify_buffer | [8]u64 | our-design | Silent drop of the newest badge — outside notifyLocked's guard nothing but the wake happens (ipc-synchronous.zig:596-602). | Nothing, by design. |
|
||||||
|
| `system/kernel/kernel.zig:246` | isos | [16]architecture.IsoEntry | hardware | Silent clamp: `var isos: [16]architecture.IsoEntry = undefined; const iso_n = @min(pinfo.override_count, isos.len);` (246-247) — a second truncation s… | None. The boot log prints the I/O APIC base and route information but never the override count. |
|
||||||
|
| `system/kernel/kernel.zig:421` | maximum_wake_attempts | 3 | our-design | The core is left parked and bring-up continues: the `while (attempt <= maximum_wake_attempts)` loop at 437 falls through to the log at 443-444; the ad… | Exemplary, and verified verbatim: `log.print("/system/kernel: cpu apic_id {d}: no response after {d} attempts… |
|
||||||
|
| `system/kernel/log.zig:41` | maximum_sinks | 8 | our-design | Silent ignore, confirmed at lines 47-52: `pub fn addSink(sink: SinkFn) void { if (sink_count < maximum_sinks) { sinks[sink_count] = sink; sink_count +… | None in code, though self-announcing in practice (output does not appear on the missing channel). |
|
||||||
|
| `system/kernel/log.zig:85` | ring_capacity | 512 * 1024 | our-design | Whole records are reclaimed from the tail, never a torn record — confirmed at log-ring.zig:48, `while (self.head + record_len - self.tail > capacity) … | Best in the tree, as claimed, and I verified each mechanism: per-boot monotonic `sequence` stamped into every … |
|
||||||
|
| `system/kernel/log.zig:244` | PanicRecord.message | [512]u8 | our-design | Silent truncation that is self-describing, confirmed at lines 251-256: `const n: u32 = @intCast(@min(message.len, panic_record.message.len)); @memcpy(… | The record carries its own length and the magic-last discipline prevents a torn read. Confirmed the caller's p… |
|
||||||
|
| `system/kernel/process.zig:109` | maximum_arguments / maximum_argument_bytes | 8 / 256 | our-design | All three checks confirmed. process.zig:976 `if (arguments_len > maximum_argument_bytes) return fail(state);`; process.zig:1012 `if (argc == maximum_a… | -1 from system_spawn with no log and the error name discarded at process.zig:1018 — confirmed. |
|
||||||
|
| `system/kernel/process.zig:148` | write_buffer | [256]u8 | our-design | Confirmed at process.zig:1801: `if (len <= write_buffer.len and ptr < user_half_end and ptr + len <= user_half_end) { ... } else { fail(state); }` — I… | -1 to the caller at the syscall, and the ring-level truncation is flagged in the record header — the only boun… |
|
||||||
|
| `system/kernel/process.zig:1460` | exit_record_capacity | 64 | our-design | Confirmed. `recordExitLocked` (1467-1470) overwrites the oldest via `exit_record_next = (exit_record_next + 1) % exit_record_capacity;` and an evicted… | -ESRCH with a documented interpretation, and the docstring (1455-1459) states the equivalence to 'an id that n… |
|
||||||
|
| `system/kernel/scheduler.zig:155` | ipc_maximum_handles | 32 | our-design | Visible and correctly unwound, confirmed. `installEntry` (ipc-synchronous.zig:618-627) scans `t.handles` and `return -ENOSPC`; the cap-passing path at… | -ENOSPC reaches the caller through the IPC status — a distinct, actionable error, unlike the -1 that most othe… |
|
||||||
|
| `system/kernel/scheduler.zig:485` | cpus (PerCpu pool) | [maximum_cpus]PerCpu = [128] | hardware | Cannot be overrun. acpi.zig:534-543 gates recording on `if (cpu_information.count < cpu_information.cpus.len)` and counts the surplus into `dropped`, … | kernel.zig:280-281 `if (platform.cpusDropped() > 0) log.print(" cpus : WARNING {d} core(s) beyond pool … |
|
||||||
|
| `system/kernel/tests.zig:410` | buffer (device enumerate staging) | [64]device_abi.DeviceDescrip… | our-design | Two coupled problems. (1) The value 64 duplicates devices-broker's `maximum_devices` in eight places (tests.zig:410, 1449, 2834, 3931, 4230, 4246, 437… | None for either. A test that silently examines a truncated table still passes. |
|
||||||
|
| `system/kernel/vfs.zig:59` | maximum_prefix / maximum_rewrite | 64 / 32 | our-design | Visible refusal on the mount path: mountBackend, vfs.zig:383-384 `if (!isAbsolute(prefix) or prefix.len < 2 or prefix.len > maximum_prefix) return fal… | A -1 return with no log and no distinct errno. The mounting service's own failure message is what an operator … |
|
||||||
|
| `system/kernel/vfs.zig:143` | Directory.path | [maximum_prefix]u8 = [64]u8 | external-data | `@memcpy(d.path[0..parent.len], parent);` (vfs.zig:143) into `path: [maximum_prefix]u8` (vfs.zig:88) with no @min and no guard — the only copy in the … | A boot-time panic in safe modes (loud but fatal, before init runs); nothing at all plus corrupted neighbours i… |
|
||||||
|
| `system/services/acpi/acpi.zig:74` | registered | [64]Registered | our-design | Unreachable — this is NOT the operative ceiling. `if (registered_count >= registered.len) return;` at line 513 is the first statement of registerDevic… | The real ceiling does log, once per lost device: `std.log.info("register refused for {s}", .{hid[0..@intCast(h… |
|
||||||
|
| `system/services/device-manager/device-manager.zig:51` | registry_rules | [64]registry.Rule | external-data | Rules past the 64th are dropped by `registry.parse` (library/device/registry/device-registry.zig:239-241): `if (result.count >= out_rules.len) { resul… | Good: device-manager.zig:72 `if (result.truncated) _ = logging.write("/system/services/device-manager: /system… |
|
||||||
|
| `system/services/device-manager/device-manager.zig:129` | Driver.name_buffer | [64]u8 | our-design | Silent truncation, not refusal: `addDriver` (242-244) `const n = @min(name.len, driver.name_buffer.len); @memcpy(driver.name_buffer[0..n], name[0..n])… | Indirect: the failure appears one step later as `failed to spawn {s}` (line 268) printing the truncated path, … |
|
||||||
|
| `system/services/device-manager/device-manager.zig:260` | id_text | [20]u8 | our-design | `arguments[0] = std.fmt.bufPrint(&id_text, "{d}", .{driver.device_id}) catch return;` (264) — on overflow `spawnDriver` returns without spawning AND w… | None — the only silent return in spawnDriver; every other failure path there logs (268 `failed to spawn`, 285/… |
|
||||||
|
| `system/services/display/backend.zig:69` | <inline literal> | 100 | our-design | Confirmed at lines 68-75: `var tries: u32 = 0; const found = while (tries < 100) : (tries += 1) { if (findDisplay()) \|f\| break f; time.sleepMillis(5… | Logged, but misleadingly — "headless?" is one of at least three possible causes (genuinely headless, discovery… |
|
||||||
|
| `system/services/display/compositor.zig:74` | DamageList.capacity | 16 | our-design | No loss: `add` (lines 79-93) ends `self.rects[capacity - 1] = self.rects[capacity - 1].unite(r);`, so the overflow rectangle is united into the last e… | Not needed — nothing is dropped and nothing becomes incorrect. |
|
||||||
|
| `system/services/display/compositor.zig:121` | TileGrid.maximum_columns / maximum_rows | 128 / 128 | hardware | Graceful, documented degradation. `reset` clamps with `@min((width + tile_size - 1) / tile_size, maximum_columns)` and the same for rows (lines 139-14… | No log, and none needed — the behaviour stays correct, only the repaint granularity changes. |
|
||||||
|
| `system/services/fat/engine.zig:24` | max_transfer_sectors | 8 | our-design | Not a failure mode, confirmed. The transfer loop clamps and issues more commands: `const run: u32 = @intCast(@min(full, max_transfer_sectors));`. The … | Not applicable — correctness is unaffected, only the number of device round-trips. |
|
||||||
|
| `system/services/fat/engine.zig:80` | block_cache_lines | 16 | our-design | Round-robin eviction, write-through — correctness preserved, only hit rate degrades. | Not needed. |
|
||||||
|
| `system/services/fat/engine.zig:871` | <inline literal> | 4096 | external-data | `if (sector_index > 4096) return null;` ends the search, so findFreeRun/addEntry return null, createFile/createDirectory return null and the VFS handl… | None in the service; no log, and the errno collapses into the same ENOENT everything else uses. |
|
||||||
|
| `system/services/fat/engine.zig:1047` | run | [21]EntryLoc | external-data | Silent partial cleanup, confirmed. `var run: [21]EntryLoc = undefined;` at engine.zig:1047 (removeFile, line 1046) and 1114 (rename, line 1110), recor… | None. |
|
||||||
|
| `system/services/init/init.zig:69` | max_services | 16 | external-data | Rows past the 16th are refused and parsing stops: init.zig:114-117 `if (service_count >= services.len) { _ = logging.write(...); break; }`. | Good: a direct `logging.write` (not just a ring entry) naming the file — `"/system/services/init: /system/conf… |
|
||||||
|
| `system/services/init/init.zig:84` | init_csv / protocol_csv | [4096]u8 / [16384]u8 | external-data | Truncation, detected and reported. readConfiguration (init.zig:138-149) fills the buffer and stops (`while (used < into.len) { const n = file.read(int… | An explicit log naming the file and the byte count. It is a heuristic (a file exactly the buffer's size gives … |
|
||||||
|
| `system/services/init/init.zig:84` | init_csv | 4096 bytes | external-data | Silent truncation mid-line via the shared readConfiguration (init.zig:141-146), after which the severed line parses as a service path that does not ex… | The same heuristic info log as protocol.csv: init.zig:147 '{s} filled the read buffer — rows past {d} bytes ar… |
|
||||||
|
| `system/services/init/init.zig:160` | maximum_name | 64 | our-design | Refused, not truncated — contractName, init.zig:498-502 `if (name.len == 0 or name.len > maximum_name) return null;`, and both callers answer with an … | None on the open path, deliberately (init.zig:652-677 explains why a refusal and an absence must be the same a… |
|
||||||
|
| `system/services/init/init.zig:173` | Binding.binary | [64]u8 | our-design | Silent truncation: onBind, init.zig:630-632 `const binary_len = @min(identity.binary.len, slot.binary.len); @memcpy(slot.binary[0..binary_len], identi… | The truncated path is what appears in the provenance line (init.zig:635) and in the EBUSY refusal message (ini… |
|
||||||
|
| `system/services/logger/logger.zig:53` | maximum_files | 24 | our-design | No loss. fileFor (line 229) looks for a matching cached name, then a free slot, then `const cached = slot orelse evictOne() orelse return null;` at li… | Nothing logged on eviction. The symptom is slow logging, not lost logging, which is the right trade. |
|
||||||
|
| `system/services/logger/logger.zig:57` | CachedFile.name | [logging.maximum_process_nam… | external-data | Panic, in a narrower window than claimed. fileFor formats the path FIRST (line 245, `std.fmt.bufPrint(&path, "{s}/{s}.log", ...) catch return null`) i… | A process fault, visible as the logger dying; init's crash-loop policy stops restarting it after three deaths,… |
|
||||||
|
| `system/services/logger/logger.zig:72` | carry_capacity | 64 + 256 + 64 | our-design | consume (line 187-191): `const rest = bytes.len - offset; if (rest > carry_capacity) { carry_len = 0; // cannot happen with sane frames; drop rather t… | Partly self-reporting, and the claim was right to credit it: deliver (lines 199-206) compares header.sequence … |
|
||||||
|
| `system/services/logger/logger.zig:82` | <inline literal> | 64 | our-design | Benign today. `service.run(64, .{ .init = initialise, .on_message = onMessage, ... })` at line 82 sizes the harness receive/reply buffer. The logger s… | None, and none needed while onMessage is a no-op. |
|
||||||
|
| `system/services/logger/logger.zig:243` | path | [base.len + 1 + 19 + 1 + log… | our-design | Silent record loss. `const full = std.fmt.bufPrint(&path, "{s}/{s}.log", .{ boot_directory[0..boot_directory_len], relative }) catch return null;` (li… | None. Records for that process never appear on disk, and the gap accounting cannot flag it: next_expected_sequ… |
|
||||||
|
| `tools/make-fat-image.py:53` | <inline literal> (65525) | 65525 clusters (~33 MiB at 5… | our-design | Loud build failure: `sys.exit(f"error: image too small for FAT32 ({self.cluster_count} clusters < 65525); use a larger size")` — and --verify repeats … | Build stops with a message naming both the actual count and the requirement, and telling the operator what to … |
|
||||||
|
| `tools/make-fat-image.py:212` | <inline literal> (LFN sequence numbering) | implicitly 20 entries / 255 … | our-design | Silent corruption (my reading; not exercised): `count = len(pairs) // 13` then `entry[0] = sequence \| (0x40 if sequence == count else 0)` — for a nam… | Nothing at build time; --verify only resolves EFI/BOOT/BOOTX64.EFI by short name, so a mangled LFN chain elsew… |
|
||||||
|
| `tools/make-fat-image.py:329` | <inline literal> (guard) | 100000 cluster hops | our-design | `return None`, which the caller turns into a misleading verdict: `sys.exit("verify: EFI/BOOT/BOOTX64.efi not found")` — a chain-too-long or cyclic FAT… | A wrong-but-visible error message. The operator is told the stub is absent when the real problem is FAT struct… |
|
||||||
|
| `tools/make-iso-image.py:151` | sector_count | 0xFFFF (32 MiB at 512 B/sect… | our-design | Deliberate silent clamp, confirmed at line 151: `sector_count = min(0xFFFF, esp_size // 512)`, packed into the El Torito default entry's 16-bit field … | Nothing at build time about the clamp itself, but the true geometry is visible: the builder prints the ESP siz… |
|
||||||
|
| `tools/make-xkeyboard-config.py:308` | <inline literal> (range(256)) / generated keys: … | 256 HID usages | hardware | No overflow inside the generator (HID_TO_NAME only maps usages below 0x100). The bound is exported into the generated `Layout.keys: [256]Key`, so any … | Nothing here; whatever the input service does with an out-of-range usage is its own concern (outside this area… |
|
||||||
|
| `tools/make-xkeyboard-config.py:314` | <inline literal> (range(4)) / generated levels: … | 4 shift levels per key | external-data | Silent truncation at generation time: `levels = [resolve_keysym(kd.levels[i], keysymdef) if i < len(kd.levels) else (0, 0) for i in range(4)]` — level… | Nothing. The generator prints no warning and the generated file looks complete; the missing characters only ap… |
|
||||||
|
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
# Device authority: the kernel stops keeping an inventory
|
||||||
|
|
||||||
|
*Design, drafted 2026-08-07. Not implemented. Prompted by a real machine: an AMD
|
||||||
|
Ryzen desktop enumerated more PCI functions than the kernel's device table would
|
||||||
|
hold, and the xHCI and SATA controllers were refused registration — so the
|
||||||
|
machine booted to the compositor with no USB and no storage.*
|
||||||
|
|
||||||
|
The kernel keeps a table of every device userspace discovers. It is a fixed
|
||||||
|
array of 64 descriptors, 344 bytes each, and a second cap allows any one parent
|
||||||
|
16 children. Neither number is written down anywhere as a decision:
|
||||||
|
`maximum_devices` has no comment and never reached `parameters.zig`, where every
|
||||||
|
other tunable in this kernel lives with its reasoning attached.
|
||||||
|
|
||||||
|
Raising them is not the fix. The numbers are wrong because the *table* is wrong:
|
||||||
|
it is an inventory of hardware, and an inventory of hardware is not something a
|
||||||
|
kernel needs. This document proposes replacing it with capabilities, which
|
||||||
|
removes the ceiling rather than moving it.
|
||||||
|
|
||||||
|
## What the kernel actually uses
|
||||||
|
|
||||||
|
Every read of a device descriptor from the kernel proper, exhaustively:
|
||||||
|
|
||||||
|
| Used for | What it needs |
|
||||||
|
|---|---|
|
||||||
|
| `mmio_map`, `io_read`/`io_write` | the physical range, to check the mapping falls inside it |
|
||||||
|
| `irq_bind`, `msi_bind` | the GSI, and that the caller owns the device |
|
||||||
|
| `dma_bind` | that the caller owns the device |
|
||||||
|
| IOMMU confinement | the **PCI BDF**, to key a domain |
|
||||||
|
| `device_register` | the parent's ranges, for the containment check |
|
||||||
|
| the boot display seed | one framebuffer window |
|
||||||
|
|
||||||
|
That is: **physical ranges, interrupt numbers, and one BDF.** Vendor, device and
|
||||||
|
subsystem ids, class triples, the human-readable names, the parent links, the
|
||||||
|
bus numbers — the kernel stores all of it and reads none of it. It is held so
|
||||||
|
that `device_enumerate` can hand it back to user space, which is the whole
|
||||||
|
mistake in one sentence: the kernel is acting as a distribution mechanism for
|
||||||
|
data it does not use.
|
||||||
|
|
||||||
|
## The split
|
||||||
|
|
||||||
|
Three concerns are tangled in one table.
|
||||||
|
|
||||||
|
**Platform bring-up** — timers, LAPIC/IOAPIC, CPU topology, the ECAM window, the
|
||||||
|
framebuffer the firmware left. The kernel derives these from ACPI before user
|
||||||
|
space exists and needs them to function. They never belonged in the device table
|
||||||
|
and mostly are not (the platform block in `kernel.zig` is separate); this
|
||||||
|
document does not change them.
|
||||||
|
|
||||||
|
**Resource authority** — which task may map which physical range, receive which
|
||||||
|
interrupt, touch which ports. This *must* stay in the kernel. It is the one
|
||||||
|
grant that cannot be audited after the fact: a process that maps arbitrary
|
||||||
|
physical memory owns the machine, page tables and IOMMU structures included.
|
||||||
|
This is memory protection, not device management, and it is why the answer is
|
||||||
|
not simply "move it all to the device manager".
|
||||||
|
|
||||||
|
**Device inventory** — what exists, what it is, how it is arranged, which driver
|
||||||
|
should bind it. This is `device-manager`'s job and is already half there: it
|
||||||
|
loads `devices.csv`, matches, spawns drivers, and receives `child_added` reports
|
||||||
|
over its own protocol. The kernel table duplicates what those reports already
|
||||||
|
carry.
|
||||||
|
|
||||||
|
## The proposal: a resource is a capability
|
||||||
|
|
||||||
|
Device resources join endpoints, shared memory and DMA regions as a kind in the
|
||||||
|
handle table.
|
||||||
|
|
||||||
|
1. **Roots.** At boot the kernel mints capabilities for the windows it learned
|
||||||
|
from firmware — the ECAM range, the framebuffer, the legacy port space — and
|
||||||
|
hands them to the first bus drivers. This is the only place device knowledge
|
||||||
|
enters the kernel, and it comes from ACPI, not from a driver's say-so.
|
||||||
|
2. **Subdivision.** A bus driver enumerating hardware derives a narrower
|
||||||
|
capability from one it holds: `resource_derive(cap, kind, start, len) → cap`.
|
||||||
|
The kernel checks the sub-range lies inside the capability being subdivided —
|
||||||
|
the same containment rule as today (`devices-broker.contains`), but checked
|
||||||
|
against *one capability the caller demonstrably holds* rather than by walking
|
||||||
|
a global tree.
|
||||||
|
3. **Delegation.** The driver passes that capability to the child driver over
|
||||||
|
IPC. Cap-passing already exists; this is the mechanism `subscribe` and
|
||||||
|
`attach_scanout` already use.
|
||||||
|
4. **Use.** `mmio_map`, `irq_bind`, `msi_bind`, `io_read`/`io_write` and
|
||||||
|
`dma_bind` take a capability handle instead of `(device_id, resource_index)`.
|
||||||
|
Possession *is* the authority — there is nothing to look up and no ownership
|
||||||
|
table to consult.
|
||||||
|
|
||||||
|
Exclusivity stops being a broker refusing a second claimant and becomes the
|
||||||
|
ordinary property of a capability: only one process was given it.
|
||||||
|
|
||||||
|
## What this buys
|
||||||
|
|
||||||
|
**No ceiling.** There is no table to size, so no machine is too big. The
|
||||||
|
Ryzen's enumeration stops being a limit to tune and becomes what it is — a fact
|
||||||
|
about a computer.
|
||||||
|
|
||||||
|
**Reclamation, free.** `count` in the broker today only ever increases;
|
||||||
|
`releaseAllOwnedBy` clears a dead driver's *claims* but never its entries. A
|
||||||
|
driver that crashes and is restarted re-registers its children and consumes the
|
||||||
|
table again — reachable today without any malice, given the device manager
|
||||||
|
restarts drivers by design. Capabilities die with the task.
|
||||||
|
|
||||||
|
**No quota needed.** The per-parent cap exists to stop one claimant looping
|
||||||
|
`device_register` and filling the shared table, because a zero-resource child
|
||||||
|
sidesteps the containment check. With no shared table there is nothing to
|
||||||
|
exhaust; a process can only ever subdivide what it was given, and its handles
|
||||||
|
are already bounded per task.
|
||||||
|
|
||||||
|
**A smaller kernel.** Three syscalls leave the ABI, one narrower one arrives,
|
||||||
|
and `devices-broker.zig` largely disappears along with both constants.
|
||||||
|
|
||||||
|
**The discipline the rest of the system already uses.** "The claim is the
|
||||||
|
capability" is written in the driver documentation as if it were already true.
|
||||||
|
This makes it true.
|
||||||
|
|
||||||
|
## Syscall surface
|
||||||
|
|
||||||
|
- `device_enumerate` — **retires.** It exists only to read the kernel's table.
|
||||||
|
Callers ask `device-manager`, whose protocol already reserves an `enumerate`
|
||||||
|
verb. Note this is a public-ABI change: `vdso.md` documents it.
|
||||||
|
- `device_register` — **splits.** The kernel half becomes `resource_derive`; the
|
||||||
|
publication half ("this device exists, here is what it is") becomes an IPC
|
||||||
|
message to `device-manager`, which is where the inventory belongs and where
|
||||||
|
`child_added` already carries the same facts.
|
||||||
|
- `device_claim` — **dissolves into possession**, except for the IOMMU (below).
|
||||||
|
- `mmio_map`, `irq_bind`, `msi_bind`, `io_read`, `io_write`, `dma_bind` — keep
|
||||||
|
their names and semantics; their first argument becomes a capability handle.
|
||||||
|
|
||||||
|
## The open question: where the IOMMU attaches
|
||||||
|
|
||||||
|
This is the one place the kernel still needs device *identity* rather than a
|
||||||
|
range. `confineDevice(device_id, bdf, owner)` builds a domain keyed by PCI BDF,
|
||||||
|
attaches it, and the claim is rolled back if confinement fails — deliberately,
|
||||||
|
so a device that cannot be confined is never driven.
|
||||||
|
|
||||||
|
Three options, none obviously right:
|
||||||
|
|
||||||
|
1. **The BDF rides the capability.** A memory capability derived for a PCI
|
||||||
|
function carries its BDF, and the kernel confines on first `mmio_map` or
|
||||||
|
`dma_bind`. Keeps the syscall count down; means a capability is no longer
|
||||||
|
purely a range.
|
||||||
|
2. **An explicit `device_attach(cap, bdf)`.** Honest and visible, but it puts a
|
||||||
|
BDF — a fact about PCI — into a kernel interface that otherwise knows nothing
|
||||||
|
about buses, and something must stop a caller naming a BDF that is not
|
||||||
|
theirs.
|
||||||
|
3. **The root PCI capability carries the segment, and derivation computes the
|
||||||
|
BDF.** Purest, but only works for PCI and the kernel would be parsing bus
|
||||||
|
topology, which is precisely what this document is trying to stop.
|
||||||
|
|
||||||
|
My inclination is (1), because confinement is a property of the resource being
|
||||||
|
granted rather than a separate action, and because it keeps the "possession is
|
||||||
|
authority" story intact. It needs the derivation call to know it is carving a
|
||||||
|
PCI function, which is a wart worth arguing about.
|
||||||
|
|
||||||
|
## What this does not solve
|
||||||
|
|
||||||
|
- **Hot-plug and removal.** Capabilities die with their holder, but a device
|
||||||
|
that physically disappears while a driver lives is the device manager's
|
||||||
|
problem and unchanged by this.
|
||||||
|
- **Quotas on physical memory.** Nothing here bounds how much a driver maps; it
|
||||||
|
bounds only *what* it may map. That was already true.
|
||||||
|
- **The device tree as a published thing.** `/system/devices` remains a device
|
||||||
|
manager concern, as the file-system hierarchy already assumes.
|
||||||
|
|
||||||
|
## Migration sketch
|
||||||
|
|
||||||
|
Not a plan yet — the phases need sizing once the IOMMU question is settled.
|
||||||
|
Rough shape: introduce the capability kind and `resource_derive` alongside the
|
||||||
|
existing table; convert the resource-consuming syscalls to accept either form;
|
||||||
|
move the inventory into `device-manager` and convert its clients off
|
||||||
|
`device_enumerate`; then delete the table, the two constants and the three
|
||||||
|
syscalls in one flag-day, as the `ServiceId` retirement did.
|
||||||
|
|
||||||
|
Every driver is affected, so the QEMU suite is the arbiter at each step, and the
|
||||||
|
Ryzen is the acceptance test — it is the machine that found this, and the one
|
||||||
|
that proves it fixed.
|
||||||
@@ -150,6 +150,57 @@ they are wire values a Rust program needs verbatim. What stays private in
|
|||||||
`abi.zig` is exactly the thing the vDSO exists to hide: the `SystemCall`
|
`abi.zig` is exactly the thing the vDSO exists to hide: the `SystemCall`
|
||||||
numbers and the trap convention.
|
numbers and the trap convention.
|
||||||
|
|
||||||
|
## Errors: the errno space
|
||||||
|
|
||||||
|
A failed call returns `-errno`. The runtime detects failure the way Linux
|
||||||
|
does — a return value in the top 4096 — so every code stays inside 1..4095.
|
||||||
|
These are **public**: unlike the call numbers, a caller must be able to read
|
||||||
|
them verbatim, and they are the same vocabulary whether the number came from
|
||||||
|
the kernel or from a user-space provider answering over IPC.
|
||||||
|
|
||||||
|
They are defined once in `system/abi.zig`. The kernel restates them in
|
||||||
|
`system/kernel/ipc-synchronous.zig` and the envelope restates the
|
||||||
|
provider-facing subset in `library/protocol/envelope/envelope.zig` (the
|
||||||
|
`protocol` package deliberately depends on nothing, so it cannot import
|
||||||
|
`abi`); a comptime check in `library/device/driver/driver.zig` makes drift a
|
||||||
|
compile error.
|
||||||
|
|
||||||
|
| # | Name | Meaning |
|
||||||
|
|---|------|---------|
|
||||||
|
| 1 | `EBADF` | bad handle |
|
||||||
|
| 2 | `E2BIG` | an argument exceeds its maximum (a message, a descriptor's resource count) |
|
||||||
|
| 3 | `EFAULT` | buffer unmapped, or outside the user half |
|
||||||
|
| 4 | `ENOENT` | no such name |
|
||||||
|
| 5 | `ENOSPC` | a kernel table is full (handles, devices) |
|
||||||
|
| 6 | `ENOMEM` | out of memory |
|
||||||
|
| 7 | `EPEER` | the peer died before replying — its process exited or was killed |
|
||||||
|
| 8 | `ESRCH` | no such process |
|
||||||
|
| 9 | `EPERM` | not permitted: the caller is not the owner or supervisor |
|
||||||
|
| 10 | `ENOSYS` | this protocol has no such operation |
|
||||||
|
| 11 | `EPROTO` | malformed packet: shorter than the verb it names |
|
||||||
|
| 12 | `EBUSY` | the thing asked for is held by someone still alive |
|
||||||
|
| 13 | `ENODEV` | no such device id |
|
||||||
|
| 14 | `ECHILDREN` | this parent already holds as many children as it can |
|
||||||
|
| 15 | `ERANGE` | a resource escapes the window it must fall inside |
|
||||||
|
| 16 | `ECONFINE` | the device could not be placed under IOMMU translation |
|
||||||
|
|
||||||
|
`EPEER` is the one with no POSIX counterpart and it is worth stating plainly:
|
||||||
|
synchronous IPC blocks the caller until the server replies, so the caller
|
||||||
|
needs an answer for "the server died while I was waiting." It is not a
|
||||||
|
transport error and not a refusal — the request may well have been carried
|
||||||
|
out — it says only that no reply is coming. A client that treats it as
|
||||||
|
"retry" can duplicate work; the honest response is to re-resolve the protocol
|
||||||
|
name, because the provider it held is gone.
|
||||||
|
|
||||||
|
**A refusal names the rule that refused it.** This is a rule and not a
|
||||||
|
courtesy. `device_register` alone can fail six ways, and until each got its
|
||||||
|
own code a bus driver could only report "refused" — which is how an AMD
|
||||||
|
desktop came to boot with a working display, no USB and no storage, with
|
||||||
|
three independent causes indistinguishable in the log. See
|
||||||
|
[fixed-bounds-audit.md](../fixed-bounds-audit.md). A new failure mode that
|
||||||
|
does not fit an existing code gets a new one here rather than borrowing the
|
||||||
|
nearest.
|
||||||
|
|
||||||
## Enforcement, and an honest threat model
|
## Enforcement, and an honest threat model
|
||||||
|
|
||||||
Renumbering only has teeth if the kernel **refuses syscalls that don't come
|
Renumbering only has teeth if the kernel **refuses syscalls that don't come
|
||||||
|
|||||||
@@ -22,14 +22,47 @@ inline fn failed(r: usize) bool {
|
|||||||
return r > ~@as(usize, 0) - 4095;
|
return r > ~@as(usize, 0) - 4095;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The errno inside a failed return. Only meaningful when `failed(r)`.
|
||||||
|
inline fn errnoOf(r: usize) i64 {
|
||||||
|
return -@as(i64, @bitCast(r));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The envelope restates the kernel's errno numbering by hand, because the
|
||||||
|
// `protocol` package deliberately depends on nothing (so it cannot import `abi`).
|
||||||
|
// This module is one of the few that can see both halves, so it is where they are
|
||||||
|
// held together: drift becomes a compile error here rather than a driver reporting
|
||||||
|
// the wrong reason for a refusal. Anything linking a driver compiles this.
|
||||||
|
comptime {
|
||||||
|
if (envelope.ENOENT != abi.ENOENT) @compileError("envelope.ENOENT has drifted from abi.ENOENT");
|
||||||
|
if (envelope.ENOSPC != abi.ENOSPC) @compileError("envelope.ENOSPC has drifted from abi.ENOSPC");
|
||||||
|
if (envelope.EPERM != abi.EPERM) @compileError("envelope.EPERM has drifted from abi.EPERM");
|
||||||
|
if (envelope.ENOSYS != abi.ENOSYS) @compileError("envelope.ENOSYS has drifted from abi.ENOSYS");
|
||||||
|
if (envelope.EPROTO != abi.EPROTO) @compileError("envelope.EPROTO has drifted from abi.EPROTO");
|
||||||
|
if (envelope.EBUSY != abi.EBUSY) @compileError("envelope.EBUSY has drifted from abi.EBUSY");
|
||||||
|
}
|
||||||
|
|
||||||
/// Copy up to `buffer.len` device descriptors into `buffer`; returns the total count.
|
/// Copy up to `buffer.len` device descriptors into `buffer`; returns the total count.
|
||||||
pub fn enumerate(buffer: []DeviceDescriptor) usize {
|
pub fn enumerate(buffer: []DeviceDescriptor) usize {
|
||||||
return sc.systemCall2(.device_enumerate, @intFromPtr(buffer.ptr), buffer.len);
|
return sc.systemCall2(.device_enumerate, @intFromPtr(buffer.ptr), buffer.len);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Take exclusive ownership of device `id`. Returns false if taken or invalid.
|
/// Why a `claim` failed. Worth distinguishing: `AlreadyClaimed` means back off and
|
||||||
pub fn claim(id: u64) bool {
|
/// let the owner have it, `NoSuchDevice` means this id is stale and the caller should
|
||||||
return !failed(sc.systemCall1(.device_claim, id));
|
/// re-enumerate, and `NotConfined` means the machine could not place the device under
|
||||||
|
/// IOMMU translation — the claim was rolled back, and that one is a fault report, not
|
||||||
|
/// a retry. `Refused` is an errno this library does not know a name for.
|
||||||
|
pub const ClaimError = error{ NoSuchDevice, AlreadyClaimed, NotConfined, Refused };
|
||||||
|
|
||||||
|
/// Take exclusive ownership of device `id`.
|
||||||
|
pub fn claim(id: u64) ClaimError!void {
|
||||||
|
const r = sc.systemCall1(.device_claim, id);
|
||||||
|
if (!failed(r)) return;
|
||||||
|
return switch (errnoOf(r)) {
|
||||||
|
abi.ENODEV => error.NoSuchDevice,
|
||||||
|
abi.EBUSY => error.AlreadyClaimed,
|
||||||
|
abi.ECONFINE => error.NotConfined,
|
||||||
|
else => error.Refused,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Map resource `resource_index` (which must be an MMIO window) of claimed device
|
/// Map resource `resource_index` (which must be an MMIO window) of claimed device
|
||||||
@@ -56,11 +89,38 @@ pub const no_pci_class = device_abi.no_pci_class;
|
|||||||
/// a bus driver may only subdivide what it already owns. `descriptor.id` and `descriptor.parent`
|
/// a bus driver may only subdivide what it already owns. `descriptor.id` and `descriptor.parent`
|
||||||
/// are ignored. A device with no resources at all is fine — a USB device is reached
|
/// are ignored. A device with no resources at all is fine — a USB device is reached
|
||||||
/// through its controller, not by MMIO.
|
/// through its controller, not by MMIO.
|
||||||
pub fn register(parent_id: u64, descriptor: *const DeviceDescriptor) ?u64 {
|
pub fn register(parent_id: u64, descriptor: *const DeviceDescriptor) RegisterError!u64 {
|
||||||
const r = sc.systemCall2(.device_register, parent_id, @intFromPtr(descriptor));
|
const r = sc.systemCall2(.device_register, parent_id, @intFromPtr(descriptor));
|
||||||
return if (failed(r)) null else r;
|
if (!failed(r)) return r;
|
||||||
|
return switch (errnoOf(r)) {
|
||||||
|
abi.ENOSPC => error.TableFull,
|
||||||
|
abi.ENODEV => error.NoSuchParent,
|
||||||
|
abi.EPERM => error.NotYourParent,
|
||||||
|
abi.E2BIG => error.TooManyResources,
|
||||||
|
abi.ECHILDREN => error.ParentFull,
|
||||||
|
abi.ERANGE => error.NotContained,
|
||||||
|
abi.EFAULT => error.BadDescriptor,
|
||||||
|
else => error.Refused,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Why a `register` failed. These are not interchangeable and a bus driver should
|
||||||
|
/// say which one it hit: `ParentFull` and `TableFull` are different ceilings with
|
||||||
|
/// different fixes, and `NotContained` is not a ceiling at all — it means the child
|
||||||
|
/// resource escaped the window the parent actually owns. Reporting all of them as
|
||||||
|
/// one refusal is what made an AMD desktop boot with no USB and no storage, and gave
|
||||||
|
/// no way to tell which of three causes it was (docs/fixed-bounds-audit.md).
|
||||||
|
pub const RegisterError = error{
|
||||||
|
TableFull, // the kernel's device table is full, machine-wide
|
||||||
|
ParentFull, // this parent already holds as many children as it can
|
||||||
|
NoSuchParent, // no device with that id
|
||||||
|
NotYourParent, // that device exists but this process has not claimed it
|
||||||
|
TooManyResources, // the descriptor declares more resources than one device may hold
|
||||||
|
NotContained, // a resource escapes the parent's window
|
||||||
|
BadDescriptor, // the descriptor pointer did not read back
|
||||||
|
Refused, // an errno this library does not know a name for
|
||||||
|
};
|
||||||
|
|
||||||
/// Bind resource `resource_index` (which must be an IRQ) of claimed device `device_id` to
|
/// Bind resource `resource_index` (which must be an IRQ) of claimed device `device_id` to
|
||||||
/// `endpoint`. From then on the interrupt arrives as an asynchronous notification:
|
/// `endpoint`. From then on the interrupt arrives as an asynchronous notification:
|
||||||
/// `ipc.replyWait` on that endpoint returns with the high bit set in `badge` and the
|
/// `ipc.replyWait` on that endpoint returns with the high bit set in `badge` and the
|
||||||
|
|||||||
+40
-2
@@ -43,11 +43,11 @@ pub const SystemCall = enum(u64) {
|
|||||||
ipc_call = 9, // ipc_call(h, message, len, reply, cap) -> reply_len: send + block for reply
|
ipc_call = 9, // ipc_call(h, message, len, reply, cap) -> reply_len: send + block for reply
|
||||||
ipc_reply_wait = 10, // ipc_reply_wait(h, reply, len, receive, cap) -> receive_len (+badge in rdx)
|
ipc_reply_wait = 10, // ipc_reply_wait(h, reply, len, receive, cap) -> receive_len (+badge in rdx)
|
||||||
device_enumerate = 11, // device_enumerate(buffer, maximum) -> count: snapshot the device table
|
device_enumerate = 11, // device_enumerate(buffer, maximum) -> count: snapshot the device table
|
||||||
device_claim = 12, // device_claim(id) -> ok: take exclusive ownership of a device
|
device_claim = 12, // device_claim(id) -> 0/-errno: take exclusive ownership of a device (-ENODEV no such id, -EBUSY someone owns it, -ECONFINE the IOMMU would not confine it)
|
||||||
mmio_map = 13, // mmio_map(id, resource_index) -> virtual_address: map a claimed device's MMIO into this address space
|
mmio_map = 13, // mmio_map(id, resource_index) -> virtual_address: map a claimed device's MMIO into this address space
|
||||||
irq_bind = 14, // irq_bind(id, resource_index, endpoint): deliver a device IRQ as an IPC notification
|
irq_bind = 14, // irq_bind(id, resource_index, endpoint): deliver a device IRQ as an IPC notification
|
||||||
irq_ack = 15, // irq_ack(id, resource_index): re-arm a bound IRQ after servicing it
|
irq_ack = 15, // irq_ack(id, resource_index): re-arm a bound IRQ after servicing it
|
||||||
device_register = 16, // device_register(parent_id, descriptor) -> id: publish a child of a device you claimed
|
device_register = 16, // device_register(parent_id, descriptor) -> id/-errno: publish a child of a device you claimed (-ENOSPC table full, -ECHILDREN parent full, -ERANGE resource escapes the parent, -ENODEV/-EPERM bad parent, -E2BIG too many resources)
|
||||||
system_spawn = 17, // system_spawn(name_ptr, name_len, arguments_ptr, arguments_len, exit_endpoint) -> child process id: start a named initial-ramdisk binary as a new ring-3 process
|
system_spawn = 17, // system_spawn(name_ptr, name_len, arguments_ptr, arguments_len, exit_endpoint) -> child process id: start a named initial-ramdisk binary as a new ring-3 process
|
||||||
dma_alloc = 18, // dma_alloc(len, flags) -> virtual_address (rax), physical_address (rdx): contiguous, pinned, uncacheable DMA memory
|
dma_alloc = 18, // dma_alloc(len, flags) -> virtual_address (rax), physical_address (rdx): contiguous, pinned, uncacheable DMA memory
|
||||||
dma_free = 19, // dma_free(virtual_address, len) -> 0: release a prior dma_alloc
|
dma_free = 19, // dma_free(virtual_address, len) -> 0: release a prior dma_alloc
|
||||||
@@ -88,6 +88,44 @@ pub const SystemCall = enum(u64) {
|
|||||||
_,
|
_,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// **The errno space** — the one vocabulary of refusal, returned as `-value` in the
|
||||||
|
/// system_call result register and echoed by user-space providers in a reply status.
|
||||||
|
/// Canonical here because it crosses the kernel↔user boundary in both directions:
|
||||||
|
/// the kernel restates these in system/kernel/ipc-synchronous.zig, and the envelope
|
||||||
|
/// restates the provider-facing subset in library/protocol/envelope/envelope.zig
|
||||||
|
/// (which cannot import this module — the `protocol` package deliberately has no
|
||||||
|
/// dependencies, so a comptime cross-check in library/device/driver/driver.zig and
|
||||||
|
/// system/kernel/tests.zig holds the two halves together).
|
||||||
|
///
|
||||||
|
/// The rule these serve: **a refusal must say which rule refused it.** A caller that
|
||||||
|
/// gets one number for five different reasons cannot report, retry or route around
|
||||||
|
/// any of them — see docs/fixed-bounds-audit.md, where a bare -1 turned "this bus is
|
||||||
|
/// at its child cap" into a machine that booted with no USB and no storage, and cost
|
||||||
|
/// a debugging session to tell apart from four other causes.
|
||||||
|
///
|
||||||
|
/// Values are stable: `failed()` in the runtime treats the top 4096 return values as
|
||||||
|
/// errors (the Linux convention), so anything here must stay well inside 1..4095.
|
||||||
|
pub const EBADF: i64 = 1; // bad handle
|
||||||
|
pub const E2BIG: i64 = 2; // argument exceeds its maximum (a message, a descriptor's resource count)
|
||||||
|
pub const EFAULT: i64 = 3; // buffer unmapped / outside the user half
|
||||||
|
pub const ENOENT: i64 = 4; // no such name
|
||||||
|
pub const ENOSPC: i64 = 5; // a kernel table is full (handles, devices)
|
||||||
|
pub const ENOMEM: i64 = 6; // out of memory
|
||||||
|
pub const EPEER: i64 = 7; // peer died before replying (its process exited or was killed)
|
||||||
|
pub const ESRCH: i64 = 8; // no such process (process_kill of an unknown/dead id)
|
||||||
|
pub const EPERM: i64 = 9; // not permitted (the caller is not the owner/supervisor)
|
||||||
|
pub const ENOSYS: i64 = 10; // this protocol has no such operation
|
||||||
|
pub const EPROTO: i64 = 11; // malformed packet: shorter than the verb it names
|
||||||
|
pub const EBUSY: i64 = 12; // the thing asked for is held by someone still alive
|
||||||
|
pub const ENODEV: i64 = 13; // no such device id
|
||||||
|
pub const ECHILDREN: i64 = 14; // this parent already holds as many children as it can
|
||||||
|
pub const ERANGE: i64 = 15; // a resource escapes the window it must fall inside
|
||||||
|
pub const ECONFINE: i64 = 16; // the device could not be placed under IOMMU translation
|
||||||
|
|
||||||
|
/// The highest errno defined above. A cheap guard for anyone switching over the
|
||||||
|
/// space, and the number to bump when adding one.
|
||||||
|
pub const errno_maximum: i64 = 16;
|
||||||
|
|
||||||
/// `futex_wait` return codes (in rax).
|
/// `futex_wait` return codes (in rax).
|
||||||
pub const futex_woken: u64 = 0; // woken by a futex_wake
|
pub const futex_woken: u64 = 0; // woken by a futex_wake
|
||||||
pub const futex_mismatch: u64 = 1; // *addr != expected on entry; the caller did not block
|
pub const futex_mismatch: u64 = 1; // *addr != expected on entry; the caller did not block
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ var ecam_physical: u64 = 0;
|
|||||||
var start_bus: u64 = 0;
|
var start_bus: u64 = 0;
|
||||||
var bus_count: u64 = 0;
|
var bus_count: u64 = 0;
|
||||||
var manager_handle: ipc.Handle = 0;
|
var manager_handle: ipc.Handle = 0;
|
||||||
|
/// Functions this scan discovered but could not publish. Counted so the end of the
|
||||||
|
/// scan can reconcile "found" against "registered" — a scan that silently returns a
|
||||||
|
/// subset of the machine is the failure this driver is most able to hide.
|
||||||
|
var refused: u32 = 0;
|
||||||
|
|
||||||
/// One aligned 32-bit read from a function's configuration space.
|
/// One aligned 32-bit read from a function's configuration space.
|
||||||
fn configRead(bus: u64, dev: u64, function: u64, offset: u64) u32 {
|
fn configRead(bus: u64, dev: u64, function: u64, offset: u64) u32 {
|
||||||
@@ -74,10 +78,10 @@ fn configWrite16(bus: u64, dev: u64, function: u64, offset: u64, value: u16) voi
|
|||||||
/// Claim the bridge, map the ECAM, hello the manager, then scan.
|
/// Claim the bridge, map the ECAM, hello the manager, then scan.
|
||||||
fn initialise(endpoint: ipc.Handle) bool {
|
fn initialise(endpoint: ipc.Handle) bool {
|
||||||
_ = endpoint;
|
_ = endpoint;
|
||||||
if (!device.claim(bridge_id)) {
|
device.claim(bridge_id) catch |e| {
|
||||||
std.log.info("unable to claim bridge device {d}", .{bridge_id});
|
std.log.info("unable to claim bridge device {d}: {s}", .{ bridge_id, @errorName(e) });
|
||||||
return false;
|
return false;
|
||||||
}
|
};
|
||||||
const buffer = memory.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
const buffer = memory.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
||||||
_ = logging.write("/system/drivers/pci-bus: out of memory\n");
|
_ = logging.write("/system/drivers/pci-bus: out of memory\n");
|
||||||
return false;
|
return false;
|
||||||
@@ -141,7 +145,13 @@ fn scan() void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
std.log.info("{d} functions found", .{found});
|
// Reconcile: "found" alone reads as success even when most of the machine was
|
||||||
|
// refused. If the two disagree, say so at a level that survives a scrollback.
|
||||||
|
if (refused == 0) {
|
||||||
|
std.log.info("{d} functions found, all registered", .{found});
|
||||||
|
} else {
|
||||||
|
std.log.warn("{d} functions found, {d} REFUSED — {d} registered", .{ found, refused, found - refused });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Register one function under the bridge and report it to the manager. The
|
/// Register one function under the bridge and report it to the manager. The
|
||||||
@@ -225,8 +235,12 @@ fn registerAndReport(bus: u64, dev: u64, function: u64, class_triple: u32) void
|
|||||||
// subsystem are read. Every discovered function is logged, matched or not.
|
// subsystem are read. Every discovered function is logged, matched or not.
|
||||||
logFunction(bus, dev, function, class_triple, descriptor.vendor, descriptor.device, descriptor.subsystem);
|
logFunction(bus, dev, function, class_triple, descriptor.vendor, descriptor.device, descriptor.subsystem);
|
||||||
|
|
||||||
const registered = device.register(bridge_id, &descriptor) orelse {
|
// Name the rule that refused. `ParentFull` and `TableFull` are different ceilings
|
||||||
std.log.info("register refused for {d}:{d}.{d}", .{ bus, dev, function });
|
// with different fixes, and `NotContained` is not a ceiling at all — it means the
|
||||||
|
// BAR escaped the bridge's own window (docs/fixed-bounds-audit.md).
|
||||||
|
const registered = device.register(bridge_id, &descriptor) catch |e| {
|
||||||
|
refused += 1;
|
||||||
|
std.log.warn("register refused for {d}:{d}.{d}: {s}", .{ bus, dev, function, @errorName(e) });
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
// The registered device id is the packet's target — the manager's object
|
// The registered device id is the packet's target — the manager's object
|
||||||
|
|||||||
@@ -114,10 +114,10 @@ pub fn main() void {
|
|||||||
_ = logging.write("/system/drivers/ps2-bus: found PS/2 controller\n");
|
_ = logging.write("/system/drivers/ps2-bus: found PS/2 controller\n");
|
||||||
_ = logging.write("/system/drivers/ps2-bus: initializing controller\n");
|
_ = logging.write("/system/drivers/ps2-bus: initializing controller\n");
|
||||||
|
|
||||||
if (!device.claim(controller_device_descriptor.id)) {
|
device.claim(controller_device_descriptor.id) catch |e| {
|
||||||
_ = logging.write("/system/drivers/ps2-bus: unable to claim controller \n");
|
std.log.warn("unable to claim controller: {s}", .{@errorName(e)});
|
||||||
return;
|
return;
|
||||||
}
|
};
|
||||||
|
|
||||||
const controller = ps2.Controller.init(controller_device_descriptor) orelse {
|
const controller = ps2.Controller.init(controller_device_descriptor) orelse {
|
||||||
_ = logging.write("/system/drivers/ps2-bus: controller is missing its IO ports\n");
|
_ = logging.write("/system/drivers/ps2-bus: controller is missing its IO ports\n");
|
||||||
@@ -255,14 +255,18 @@ pub fn main() void {
|
|||||||
if (port_device_types[@intFromEnum(ps2.Port.two)] != null) {
|
if (port_device_types[@intFromEnum(ps2.Port.two)] != null) {
|
||||||
if (ps2.findMouseDescriptor(buffer)) |descriptor| {
|
if (ps2.findMouseDescriptor(buffer)) |descriptor| {
|
||||||
if (findInterruptResourceIndex(descriptor)) |auxiliary_index| {
|
if (findInterruptResourceIndex(descriptor)) |auxiliary_index| {
|
||||||
if (device.claim(descriptor.id) and device.irqBind(descriptor.id, auxiliary_index, endpoint)) {
|
if (device.claim(descriptor.id)) |_| {
|
||||||
maybe_auxiliary_interrupt = .{
|
if (device.irqBind(descriptor.id, auxiliary_index, endpoint)) {
|
||||||
.device_id = descriptor.id,
|
maybe_auxiliary_interrupt = .{
|
||||||
.interrupt_index = auxiliary_index,
|
.device_id = descriptor.id,
|
||||||
.gsi = descriptor.resources[auxiliary_index].start,
|
.interrupt_index = auxiliary_index,
|
||||||
};
|
.gsi = descriptor.resources[auxiliary_index].start,
|
||||||
} else {
|
};
|
||||||
_ = logging.write("/system/drivers/ps2-bus: auxiliary irq_bind failed\n");
|
} else {
|
||||||
|
_ = logging.write("/system/drivers/ps2-bus: auxiliary irq_bind failed\n");
|
||||||
|
}
|
||||||
|
} else |e| {
|
||||||
|
std.log.warn("auxiliary claim failed: {s}", .{@errorName(e)});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,10 +173,10 @@ fn initialise(endpoint: ipc.Handle) bool {
|
|||||||
if (!channel.bindPatiently("usb-transfer", endpoint))
|
if (!channel.bindPatiently("usb-transfer", endpoint))
|
||||||
_ = logging.write("/system/drivers/usb-xhci-bus: /protocol/usb-transfer is another controller's; serving mine unnamed\n");
|
_ = logging.write("/system/drivers/usb-xhci-bus: /protocol/usb-transfer is another controller's; serving mine unnamed\n");
|
||||||
|
|
||||||
if (!device.claim(controller_id)) {
|
device.claim(controller_id) catch |e| {
|
||||||
std.log.info("unable to claim controller device {d}", .{controller_id});
|
std.log.warn("unable to claim controller device {d}: {s}", .{ controller_id, @errorName(e) });
|
||||||
return false;
|
return false;
|
||||||
}
|
};
|
||||||
|
|
||||||
// Fetch our own descriptor back for the controller's resources.
|
// Fetch our own descriptor back for the controller's resources.
|
||||||
const buffer = memory.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
const buffer = memory.allocator().alloc(device.DeviceDescriptor, 64) catch {
|
||||||
@@ -522,8 +522,8 @@ fn reportInterface(manager: ipc.Handle, port: u32, interface: library.InterfaceI
|
|||||||
const hid_text = std.fmt.bufPrint(&hid_buffer, "P{d}I{d}", .{ port, interface.number }) catch "";
|
const hid_text = std.fmt.bufPrint(&hid_buffer, "P{d}I{d}", .{ port, interface.number }) catch "";
|
||||||
descriptor.hid_len = hid_text.len;
|
descriptor.hid_len = hid_text.len;
|
||||||
@memcpy(descriptor.hid[0..hid_text.len], hid_text);
|
@memcpy(descriptor.hid[0..hid_text.len], hid_text);
|
||||||
const registered = device.register(controller_id, &descriptor) orelse {
|
const registered = device.register(controller_id, &descriptor) catch |e| {
|
||||||
std.log.info("register refused for port {d} interface {d}", .{ port, interface.number });
|
std.log.warn("register refused for port {d} interface {d}: {s}", .{ port, interface.number, @errorName(e) });
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -200,10 +200,10 @@ fn testPixel(index: u32) u32 {
|
|||||||
|
|
||||||
fn initialise(endpoint: ipc.Handle) bool {
|
fn initialise(endpoint: ipc.Handle) bool {
|
||||||
_ = endpoint;
|
_ = endpoint;
|
||||||
if (!device.claim(device_id)) {
|
device.claim(device_id) catch |e| {
|
||||||
std.log.info("unable to claim device {d}", .{device_id});
|
std.log.info("unable to claim device {d}: {s}", .{ device_id, @errorName(e) });
|
||||||
return false;
|
return false;
|
||||||
}
|
};
|
||||||
|
|
||||||
var descriptors: [64]device.DeviceDescriptor = undefined;
|
var descriptors: [64]device.DeviceDescriptor = undefined;
|
||||||
const total = device.enumerate(&descriptors);
|
const total = device.enumerate(&descriptors);
|
||||||
|
|||||||
+82
-40
@@ -615,17 +615,78 @@ var boot_memory_regions: []const boot_handoff.MemoryRegion = &.{};
|
|||||||
/// I/O-APIC region, the high one from 4 GiB (or the end of RAM above it) to
|
/// I/O-APIC region, the high one from 4 GiB (or the end of RAM above it) to
|
||||||
/// the 46-bit line. Coarse, mechanical, and AML-free — available at boot no
|
/// the 46-bit line. Coarse, mechanical, and AML-free — available at boot no
|
||||||
/// matter what later moved to user space.
|
/// matter what later moved to user space.
|
||||||
|
pub const AddressRange = struct { base: u64, end: u64 };
|
||||||
|
|
||||||
|
/// The largest holes below 4 GiB in a firmware memory map: the ranges the firmware
|
||||||
|
/// described *nothing* in, which is where a PCI BAR may legitimately live. Fills `out`
|
||||||
|
/// (keeping the largest, replacing the smallest held so far), ignores holes shorter
|
||||||
|
/// than `minimum`, and returns how many entries are non-empty; entries the caller
|
||||||
|
/// reads may be empty and are skipped by `end > base`.
|
||||||
|
///
|
||||||
|
/// **It never copies the map, and that is the point.** The firmware chooses how many
|
||||||
|
/// descriptors its map has — commonly 60–200 on a real machine, 15–25 under OVMF. The
|
||||||
|
/// previous version copied the sub-4 GiB entries into a fixed `[64]` array and
|
||||||
|
/// `continue`d past the rest, which did not merely lose them: a region absent from the
|
||||||
|
/// walk is a region this function concludes is *free*, so a large enough map yields an
|
||||||
|
/// "aperture" lying over live RAM. `device_register` containment would then admit a
|
||||||
|
/// child BAR covering kernel memory, and its claimant could `mmio_map` it. A bound
|
||||||
|
/// whose overflow hands out authority is not a limit; the fix is not a bigger array.
|
||||||
|
///
|
||||||
|
/// Pure, allocation-free, and linear-ish in the map (each inner pass consumes at least
|
||||||
|
/// one region, and it runs once at boot).
|
||||||
|
pub fn largestHolesBelow4G(
|
||||||
|
regions: []const boot_handoff.MemoryRegion,
|
||||||
|
minimum: u64,
|
||||||
|
out: []AddressRange,
|
||||||
|
) usize {
|
||||||
|
const limit: u64 = 1 << 32;
|
||||||
|
for (out) |*hole| hole.* = .{ .base = 0, .end = 0 };
|
||||||
|
|
||||||
|
var cursor: u64 = 0;
|
||||||
|
while (cursor < limit) {
|
||||||
|
// Step over every described region covering the cursor. Regions may overlap
|
||||||
|
// and chain, so repeat until the cursor stops moving.
|
||||||
|
var moved = true;
|
||||||
|
while (moved) {
|
||||||
|
moved = false;
|
||||||
|
for (regions) |region| {
|
||||||
|
if (region.base >= limit) continue;
|
||||||
|
const end = @min(region.base + region.pages * 4096, limit);
|
||||||
|
if (region.base <= cursor and end > cursor) {
|
||||||
|
cursor = end;
|
||||||
|
moved = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cursor >= limit) break;
|
||||||
|
|
||||||
|
// The cursor now sits in a hole; it runs to the next described base, or to
|
||||||
|
// 4 GiB if nothing is described above it.
|
||||||
|
var next: u64 = limit;
|
||||||
|
for (regions) |region| {
|
||||||
|
if (region.base >= limit) continue;
|
||||||
|
if (region.base > cursor and region.base < next) next = region.base;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (next - cursor >= minimum and out.len != 0) {
|
||||||
|
var smallest: usize = 0;
|
||||||
|
for (out, 0..) |hole, i| {
|
||||||
|
if (hole.end - hole.base < out[smallest].end - out[smallest].base) smallest = i;
|
||||||
|
}
|
||||||
|
if (next - cursor > out[smallest].end - out[smallest].base)
|
||||||
|
out[smallest] = .{ .base = cursor, .end = next };
|
||||||
|
}
|
||||||
|
cursor = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
var found: usize = 0;
|
||||||
|
for (out) |hole| {
|
||||||
|
if (hole.end > hole.base) found += 1;
|
||||||
|
}
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
fn addBridgeApertures(bridge: *device_model.Device) void {
|
fn addBridgeApertures(bridge: *device_model.Device) void {
|
||||||
// Below 4 GiB the described regions are sparse (RAM low, firmware flash
|
|
||||||
// and tables high), so the holes are the *gaps between* them — a single
|
|
||||||
// "after the last region" rule dies on OVMF's flash at the very top.
|
|
||||||
// Sort-merge the described ranges, then keep the three largest gaps
|
|
||||||
// (resource slots are bounded at 8 per device; ECAM + bus range + 3 + the
|
|
||||||
// high aperture fits). Above 4 GiB one aperture runs from the end of the
|
|
||||||
// described space to the 46-bit line.
|
|
||||||
const Range = struct { base: u64, end: u64 };
|
|
||||||
var below: [64]Range = undefined;
|
|
||||||
var below_count: usize = 0;
|
|
||||||
var high_end: u64 = 1 << 32;
|
var high_end: u64 = 1 << 32;
|
||||||
for (boot_memory_regions) |region| {
|
for (boot_memory_regions) |region| {
|
||||||
const end = region.base + region.pages * 4096;
|
const end = region.base + region.pages * 4096;
|
||||||
@@ -636,37 +697,18 @@ fn addBridgeApertures(bridge: *device_model.Device) void {
|
|||||||
// kernel image, the tables, the ramdisk all live there). Bring-up
|
// kernel image, the tables, the ramdisk all live there). Bring-up
|
||||||
// trust: only the bridge's claimant can register into the aperture.
|
// trust: only the bridge's claimant can register into the aperture.
|
||||||
if (region.kind == .usable and end > high_end) high_end = end;
|
if (region.kind == .usable and end > high_end) high_end = end;
|
||||||
if (region.base >= (1 << 32) or below_count == below.len) continue;
|
|
||||||
below[below_count] = .{ .base = region.base, .end = @min(end, 1 << 32) };
|
|
||||||
below_count += 1;
|
|
||||||
}
|
}
|
||||||
// Insertion sort by base (the map is small and this runs once at boot).
|
|
||||||
for (1..below_count) |i| {
|
// Three holes below 4 GiB. This three is not a guess about hardware: a
|
||||||
const key = below[i];
|
// `DeviceDescriptor` carries `maximum_device_resources` (8) resources, and the
|
||||||
var j = i;
|
// bridge spends them on ECAM + bus range + these + the high aperture. That cap is
|
||||||
while (j > 0 and below[j - 1].base > key.base) : (j -= 1) below[j] = below[j - 1];
|
// a wire struct in the kernel↔user ABI, so widening it is a separate change —
|
||||||
below[j] = key;
|
// docs/bounds-track-plan.md, phase 4. Keeping *fewer* holes is fail-closed: it
|
||||||
}
|
// refuses BARs, it never admits one.
|
||||||
// Walk the sorted ranges, collecting inter-region gaps of at least 1 MiB.
|
var holes: [3]AddressRange = undefined;
|
||||||
var gaps: [3]Range = .{Range{ .base = 0, .end = 0 }} ** 3;
|
_ = largestHolesBelow4G(boot_memory_regions, 1 << 20, &holes);
|
||||||
var cursor: u64 = 0;
|
for (holes) |hole| {
|
||||||
var index: usize = 0;
|
if (hole.end > hole.base) _ = bridge.addResource(.memory, hole.base, hole.end - hole.base);
|
||||||
while (index <= below_count) : (index += 1) {
|
|
||||||
const gap_end = if (index == below_count) (1 << 32) else below[index].base;
|
|
||||||
if (gap_end > cursor and gap_end - cursor >= (1 << 20)) {
|
|
||||||
// Keep the three largest, replacing the smallest kept so far.
|
|
||||||
var smallest: usize = 0;
|
|
||||||
for (gaps, 0..) |gap, gi| {
|
|
||||||
if (gap.end - gap.base < gaps[smallest].end - gaps[smallest].base) smallest = gi;
|
|
||||||
}
|
|
||||||
if (gap_end - cursor > gaps[smallest].end - gaps[smallest].base) {
|
|
||||||
gaps[smallest] = .{ .base = cursor, .end = gap_end };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (index < below_count and below[index].end > cursor) cursor = below[index].end;
|
|
||||||
}
|
|
||||||
for (gaps) |gap| {
|
|
||||||
if (gap.end > gap.base) _ = bridge.addResource(.memory, gap.base, gap.end - gap.base);
|
|
||||||
}
|
}
|
||||||
_ = bridge.addResource(.memory, high_end, (@as(u64, 1) << 46) - high_end);
|
_ = bridge.addResource(.memory, high_end, (@as(u64, 1) << 46) - high_end);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,10 +19,11 @@
|
|||||||
//! ever subdivide what it was already given.
|
//! ever subdivide what it was already given.
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const abi = @import("abi");
|
||||||
const platform = @import("platform");
|
const platform = @import("platform");
|
||||||
const device_abi = @import("device-abi");
|
const device_abi = @import("device-abi");
|
||||||
|
|
||||||
const maximum_devices = 64;
|
pub const maximum_devices = 64;
|
||||||
|
|
||||||
/// Cap on children a single parent may have. A zero-resource child (legal — a USB
|
/// Cap on children a single parent may have. A zero-resource child (legal — a USB
|
||||||
/// device is addressed through its controller, not by MMIO) sidesteps the containment
|
/// device is addressed through its controller, not by MMIO) sidesteps the containment
|
||||||
@@ -157,13 +158,13 @@ pub fn enumerateFrom(start: usize, out: []device_abi.DeviceDescriptor) usize {
|
|||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Take exclusive ownership of device `id` for task `owner`. Fails if the id is
|
/// Take exclusive ownership of device `id` for task `owner`. The two ways this can
|
||||||
/// out of range or already claimed.
|
/// fail want different responses from a driver — a stale id means re-enumerate, a
|
||||||
pub fn claim(id: u64, owner: u32) bool {
|
/// live claimant means back off — so they are distinguishable (`ClaimError`).
|
||||||
if (id >= count) return false;
|
pub fn claim(id: u64, owner: u32) ClaimError!void {
|
||||||
if (claimed[@intCast(id)] != null) return false;
|
if (id >= count) return error.NoSuchDevice;
|
||||||
|
if (claimed[@intCast(id)] != null) return error.AlreadyClaimed;
|
||||||
claimed[@intCast(id)] = owner;
|
claimed[@intCast(id)] = owner;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The task that owns device `id`, or null.
|
/// The task that owns device `id`, or null.
|
||||||
@@ -274,14 +275,70 @@ fn contains(parent: device_abi.ResourceDescriptor, child: device_abi.ResourceDes
|
|||||||
return child.start >= parent.start and child_end <= parent_end;
|
return child.start >= parent.start and child_end <= parent_end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Why a `register` was refused. Each variant maps to its own errno (`errnoOf`), so
|
||||||
|
/// a bus driver's log line can name the rule that stopped it — "this parent is at
|
||||||
|
/// its child cap" and "the table is full" want different fixes, and telling them
|
||||||
|
/// apart from a bare -1 cost a debugging session (docs/fixed-bounds-audit.md).
|
||||||
pub const RegisterError = error{
|
pub const RegisterError = error{
|
||||||
NoSpace, // the device table is full
|
NoSpace, // the device table is full
|
||||||
BadParent, // no such device, or not claimed by this task
|
NoSuchParent, // no device with that id
|
||||||
TooManyResources,
|
NotYourParent, // that device exists but this task has not claimed it
|
||||||
|
TooManyResources, // the descriptor declares more resources than one device may hold
|
||||||
TooManyChildren, // this parent is at maximum_children_per_parent
|
TooManyChildren, // this parent is at maximum_children_per_parent
|
||||||
NotContained, // a child resource escapes its parent's window
|
NotContained, // a child resource escapes its parent's window
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// The errno a refused `register` returns to ring 3.
|
||||||
|
pub fn errnoOf(e: RegisterError) i64 {
|
||||||
|
return switch (e) {
|
||||||
|
error.NoSpace => abi.ENOSPC,
|
||||||
|
error.NoSuchParent => abi.ENODEV,
|
||||||
|
error.NotYourParent => abi.EPERM,
|
||||||
|
error.TooManyResources => abi.E2BIG,
|
||||||
|
error.TooManyChildren => abi.ECHILDREN,
|
||||||
|
error.NotContained => abi.ERANGE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Why a `claim` was refused.
|
||||||
|
pub const ClaimError = error{
|
||||||
|
NoSuchDevice, // no device with that id
|
||||||
|
AlreadyClaimed, // a live task already owns it
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The errno a refused `claim` returns to ring 3. (`ECONFINE` — the claim stood but
|
||||||
|
/// the IOMMU would not confine the device — is raised by the caller in
|
||||||
|
/// system/kernel/process.zig, which is what rolls the claim back.)
|
||||||
|
pub fn claimErrnoOf(e: ClaimError) i64 {
|
||||||
|
return switch (e) {
|
||||||
|
error.NoSuchDevice => abi.ENODEV,
|
||||||
|
error.AlreadyClaimed => abi.EBUSY,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The id of a child of `parent_id` already identical to `descriptor`, or null.
|
||||||
|
/// Exact on class, identity and every resource — anything less would let a bus
|
||||||
|
/// silently adopt an entry that is not the device it just found. The caller must
|
||||||
|
/// have bounded `descriptor.resource_count` first.
|
||||||
|
fn existingChild(parent_id: u64, descriptor: *const device_abi.DeviceDescriptor) ?u64 {
|
||||||
|
for (devices[0..count]) |*existing| {
|
||||||
|
if (existing.parent != parent_id) continue;
|
||||||
|
if (existing.class != descriptor.class) continue;
|
||||||
|
if (existing.pci_class != descriptor.pci_class) continue;
|
||||||
|
if (existing.hid_len != descriptor.hid_len) continue;
|
||||||
|
if (!std.mem.eql(u8, existing.hid[0..@intCast(existing.hid_len)], descriptor.hid[0..@intCast(descriptor.hid_len)])) continue;
|
||||||
|
if (existing.resource_count != descriptor.resource_count) continue;
|
||||||
|
var same = true;
|
||||||
|
for (0..@intCast(descriptor.resource_count)) |i| {
|
||||||
|
const a = existing.resources[i];
|
||||||
|
const b = descriptor.resources[i];
|
||||||
|
if (a.kind != b.kind or a.start != b.start or a.len != b.len) same = false;
|
||||||
|
}
|
||||||
|
if (same) return existing.id;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Number of devices currently recorded with `parent_id` as their parent.
|
/// Number of devices currently recorded with `parent_id` as their parent.
|
||||||
fn childCount(parent_id: u64) usize {
|
fn childCount(parent_id: u64) usize {
|
||||||
var n: usize = 0;
|
var n: usize = 0;
|
||||||
@@ -299,9 +356,27 @@ fn childCount(parent_id: u64) usize {
|
|||||||
/// contained in a parent resource of the same kind. A device with no resources is
|
/// contained in a parent resource of the same kind. A device with no resources is
|
||||||
/// fine and common: a USB device is addressed through its controller, not by MMIO.
|
/// fine and common: a USB device is addressed through its controller, not by MMIO.
|
||||||
pub fn register(parent_id: u64, owner: u32, descriptor: *const device_abi.DeviceDescriptor) RegisterError!u64 {
|
pub fn register(parent_id: u64, owner: u32, descriptor: *const device_abi.DeviceDescriptor) RegisterError!u64 {
|
||||||
const parent_owner = ownerOf(parent_id) orelse return error.BadParent;
|
if (parent_id >= count) return error.NoSuchParent;
|
||||||
if (parent_owner != owner) return error.BadParent;
|
const parent_owner = ownerOf(parent_id) orelse return error.NotYourParent;
|
||||||
|
if (parent_owner != owner) return error.NotYourParent;
|
||||||
|
// Bounds every `descriptor.resources` read below, including the match scan's.
|
||||||
if (descriptor.resource_count > device_abi.maximum_device_resources) return error.TooManyResources;
|
if (descriptor.resource_count > device_abi.maximum_device_resources) return error.TooManyResources;
|
||||||
|
|
||||||
|
// Idempotent on exact match (docs/device-manager.md): a restarted registering
|
||||||
|
// bus re-registers what it rediscovers, and the table has no unregister — an
|
||||||
|
// identical child under the same parent returns the existing id instead of
|
||||||
|
// appending a duplicate.
|
||||||
|
//
|
||||||
|
// Checked **before the caps**, because a re-registration consumes no slot.
|
||||||
|
// Charging it against the child cap refused a restarted bus its own devices the
|
||||||
|
// second time it started, which turned the supervision restart this system leans
|
||||||
|
// on into a one-way ratchet toward a degraded machine. The match is exact — class,
|
||||||
|
// identity, and every resource — so an entry returned this way was contained when
|
||||||
|
// it was first admitted, and a stored descriptor's resources never change
|
||||||
|
// afterwards (they are written only by `record`, `seedDisplay` and the append
|
||||||
|
// below).
|
||||||
|
if (existingChild(parent_id, descriptor)) |existing_id| return existing_id;
|
||||||
|
|
||||||
if (childCount(parent_id) >= maximum_children_per_parent) return error.TooManyChildren;
|
if (childCount(parent_id) >= maximum_children_per_parent) return error.TooManyChildren;
|
||||||
if (count >= maximum_devices) return error.NoSpace;
|
if (count >= maximum_devices) return error.NoSpace;
|
||||||
|
|
||||||
@@ -315,26 +390,6 @@ pub fn register(parent_id: u64, owner: u32, descriptor: *const device_abi.Device
|
|||||||
if (!ok) return error.NotContained;
|
if (!ok) return error.NotContained;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Idempotent on exact match (docs/device-manager.md): a restarted
|
|
||||||
// registering bus re-registers what it rediscovers, and the table has no
|
|
||||||
// unregister — an identical (class, identity, resources) child under the
|
|
||||||
// same parent returns the existing id instead of appending a duplicate.
|
|
||||||
for (devices[0..count]) |*existing| {
|
|
||||||
if (existing.parent != parent_id) continue;
|
|
||||||
if (existing.class != descriptor.class) continue;
|
|
||||||
if (existing.pci_class != descriptor.pci_class) continue;
|
|
||||||
if (existing.hid_len != descriptor.hid_len) continue;
|
|
||||||
if (!std.mem.eql(u8, existing.hid[0..@intCast(existing.hid_len)], descriptor.hid[0..@intCast(descriptor.hid_len)])) continue;
|
|
||||||
if (existing.resource_count != descriptor.resource_count) continue;
|
|
||||||
var same = true;
|
|
||||||
for (0..@intCast(descriptor.resource_count)) |i| {
|
|
||||||
const a = existing.resources[i];
|
|
||||||
const b = descriptor.resources[i];
|
|
||||||
if (a.kind != b.kind or a.start != b.start or a.len != b.len) same = false;
|
|
||||||
}
|
|
||||||
if (same) return existing.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
var d = std.mem.zeroes(device_abi.DeviceDescriptor);
|
var d = std.mem.zeroes(device_abi.DeviceDescriptor);
|
||||||
d.id = count;
|
d.id = count;
|
||||||
d.parent = parent_id;
|
d.parent = parent_id;
|
||||||
|
|||||||
+26
-5
@@ -96,16 +96,37 @@ pub fn init() void {
|
|||||||
const Confined = struct { active: bool = false, owner: u32 = 0, bdf: u16 = 0, domain: u16 = invalid_domain };
|
const Confined = struct { active: bool = false, owner: u32 = 0, bdf: u16 = 0, domain: u16 = invalid_domain };
|
||||||
var confined: [maximum_domains]Confined = .{Confined{}} ** maximum_domains;
|
var confined: [maximum_domains]Confined = .{Confined{}} ** maximum_domains;
|
||||||
|
|
||||||
|
// `confined` is indexed by **device id**, so it must cover every id the broker can
|
||||||
|
// mint. These two numbers agreed only by a sentence in a comment above
|
||||||
|
// `maximum_domains` — and when they disagreed, `confineDevice` returned success for
|
||||||
|
// the ids it had no room for, leaving those devices unconfined DMA masters. Coupled
|
||||||
|
// bounds agree in code, not in prose (docs/os-development/bounds.md).
|
||||||
|
comptime {
|
||||||
|
if (maximum_domains < devices_broker.maximum_devices)
|
||||||
|
@compileError("iommu.confined is indexed by device id but is smaller than the " ++
|
||||||
|
"broker's device table: ids past its end cannot be confined, and so cannot " ++
|
||||||
|
"be claimed at all");
|
||||||
|
}
|
||||||
|
|
||||||
/// Place a just-claimed PCI function under IOMMU translation on behalf of `owner`: give
|
/// Place a just-claimed PCI function under IOMMU translation on behalf of `owner`: give
|
||||||
/// it a private empty domain, seed it with the device's own firmware reserved region,
|
/// it a private empty domain, seed it with the device's own firmware reserved region,
|
||||||
/// and attach. Its DMA buffers arrive afterward as explicit grants — the owner's own
|
/// and attach. Its DMA buffers arrive afterward as explicit grants — the owner's own
|
||||||
/// `dma_alloc`'d regions are bound by the claim path (`mapForDevice`), and cross-process
|
/// `dma_alloc`'d regions are bound by the claim path (`mapForDevice`), and cross-process
|
||||||
/// buffers by `dma_bind`. false only if a domain can't be allocated — the caller rolls
|
/// buffers by `dma_bind`. false when the device cannot be confined — the caller rolls
|
||||||
/// the claim back (a claim that can't be confined must not stand). No-op success when no
|
/// the claim back (a claim that can't be confined must not stand).
|
||||||
/// IOMMU exists (fail-open).
|
///
|
||||||
|
/// **Fail-closed at the table's edge.** There is exactly one deliberate fail-open here:
|
||||||
|
/// a machine with no IOMMU, which is a fact about the hardware rather than the size of
|
||||||
|
/// anything. Running out of *room to record* a confinement is not that, and must refuse.
|
||||||
pub fn confineDevice(device_id: u64, bdf: u16, owner: u32) bool {
|
pub fn confineDevice(device_id: u64, bdf: u16, owner: u32) bool {
|
||||||
if (!active) return true;
|
if (!active) return true; // no IOMMU on this machine — nothing to confine with
|
||||||
if (device_id >= confined.len) return true; // unusual id; leave it to fail-open
|
// A device id past the end of the record table. This returned `true` — success —
|
||||||
|
// leaving the device outside every domain while telling the caller it was
|
||||||
|
// confined, and rolling nothing back. It is unreachable only while device ids stop
|
||||||
|
// at `confined.len`; moving the inventory out of the kernel and taking the domain
|
||||||
|
// count from the hardware both change that, and either would have made a silent
|
||||||
|
// unconfined DMA master out of every device past the 64th.
|
||||||
|
if (device_id >= confined.len) return false;
|
||||||
const domain = domainCreate(owner, bdf) orelse return false;
|
const domain = domainCreate(owner, bdf) orelse return false;
|
||||||
|
|
||||||
// Firmware reserved region for this device, if any (real hardware; QEMU has none).
|
// Firmware reserved region for this device, if any (real hardware; QEMU has none).
|
||||||
|
|||||||
@@ -42,15 +42,22 @@ pub const MESSAGE_MAXIMUM: usize = 256;
|
|||||||
pub const maximum_handles = scheduler.ipc_maximum_handles;
|
pub const maximum_handles = scheduler.ipc_maximum_handles;
|
||||||
|
|
||||||
/// Errno-style failures, returned as `-value` in the system_call result register.
|
/// Errno-style failures, returned as `-value` in the system_call result register.
|
||||||
pub const EBADF: i64 = 1; // bad handle
|
/// Restated from the shared kernel↔user ABI (system/abi.zig), because ring 3 reads
|
||||||
pub const E2BIG: i64 = 2; // message exceeds MESSAGE_MAXIMUM
|
/// the same numbers — the same reason `notify_badge_bit` below is restated. New
|
||||||
pub const EFAULT: i64 = 3; // buffer unmapped / out of the user half
|
/// codes are added *there*, which is where the space is documented.
|
||||||
pub const ENOENT: i64 = 4; // no such name
|
pub const EBADF = abi.EBADF; // bad handle
|
||||||
pub const ENOSPC: i64 = 5; // handle table full
|
pub const E2BIG = abi.E2BIG; // message exceeds MESSAGE_MAXIMUM
|
||||||
pub const ENOMEM: i64 = 6; // out of memory
|
pub const EFAULT = abi.EFAULT; // buffer unmapped / out of the user half
|
||||||
pub const EPEER: i64 = 7; // peer died before replying (its process exited or was killed)
|
pub const ENOENT = abi.ENOENT; // no such name
|
||||||
pub const ESRCH: i64 = 8; // no such process (process_kill of an unknown/dead id)
|
pub const ENOSPC = abi.ENOSPC; // a kernel table is full
|
||||||
pub const EPERM: i64 = 9; // not permitted (process_kill by anyone but the supervisor)
|
pub const ENOMEM = abi.ENOMEM; // out of memory
|
||||||
|
pub const EPEER = abi.EPEER; // peer died before replying (its process exited or was killed)
|
||||||
|
pub const ESRCH = abi.ESRCH; // no such process (process_kill of an unknown/dead id)
|
||||||
|
pub const EPERM = abi.EPERM; // not permitted (process_kill by anyone but the supervisor)
|
||||||
|
pub const ENODEV = abi.ENODEV; // no such device id
|
||||||
|
pub const ECHILDREN = abi.ECHILDREN; // this parent is at its child cap
|
||||||
|
pub const ERANGE = abi.ERANGE; // a resource escapes its parent's window
|
||||||
|
pub const ECONFINE = abi.ECONFINE; // the device could not be placed under IOMMU translation
|
||||||
|
|
||||||
/// A badge with this bit set is an asynchronous notification (e.g. an IRQ), not a
|
/// A badge with this bit set is an asynchronous notification (e.g. an IRQ), not a
|
||||||
/// message from a client — there is no reply owed. The low bits carry the source
|
/// message from a client — there is no reply owed. The low bits carry the source
|
||||||
|
|||||||
@@ -26,6 +26,14 @@ pub const RegisterAccess = acpi.RegisterAccess;
|
|||||||
pub const IsoEntry = acpi.IsoEntry;
|
pub const IsoEntry = acpi.IsoEntry;
|
||||||
pub const Cpu = acpi.Cpu;
|
pub const Cpu = acpi.Cpu;
|
||||||
|
|
||||||
|
/// Where a PCI BAR may legitimately live: the holes in the firmware memory map.
|
||||||
|
/// Firmware-agnostic — it takes a boot-handoff map, not an ACPI table — and pure, so
|
||||||
|
/// the kernel self-test can drive it with a synthetic map. That is the only way to
|
||||||
|
/// check the invariant that matters here: an aperture must never cover memory the
|
||||||
|
/// firmware described, because containment would then admit a BAR over live RAM.
|
||||||
|
pub const AddressRange = acpi.AddressRange;
|
||||||
|
pub const largestHolesBelow4G = acpi.largestHolesBelow4G;
|
||||||
|
|
||||||
/// The FADT power register map discovery extracted (PM1 control, reset register),
|
/// The FADT power register map discovery extracted (PM1 control, reset register),
|
||||||
/// for kernel reboot and diagnostics. Sleep-state values are userspace's (S5 is
|
/// for kernel reboot and diagnostics. Sleep-state values are userspace's (S5 is
|
||||||
/// owned by the ring-3 acpi service), so they are not here.
|
/// owned by the ring-3 acpi service), so they are not here.
|
||||||
|
|||||||
+38
-28
@@ -402,36 +402,42 @@ fn systemDeviceEnumerate(state: *architecture.CpuState) void {
|
|||||||
architecture.setSystemCallResult(state, devices_broker.deviceCount());
|
architecture.setSystemCallResult(state, devices_broker.deviceCount());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// device_claim(id) -> 0/-1: take exclusive ownership of a device for this process.
|
/// device_claim(id) -> 0/-errno: take exclusive ownership of a device for this process.
|
||||||
|
/// `-ENODEV` no such id, `-EBUSY` a live task already owns it, `-ECONFINE` the claim
|
||||||
|
/// could not be placed under IOMMU translation and was rolled back.
|
||||||
fn systemDeviceClaim(state: *architecture.CpuState) void {
|
fn systemDeviceClaim(state: *architecture.CpuState) void {
|
||||||
const device_id = architecture.systemCallArg(state, 0);
|
const device_id = architecture.systemCallArg(state, 0);
|
||||||
const claim_flags = sync.enter();
|
const claim_flags = sync.enter();
|
||||||
defer sync.leave(claim_flags);
|
defer sync.leave(claim_flags);
|
||||||
if (devices_broker.claim(device_id, scheduler.current().id)) {
|
devices_broker.claim(device_id, scheduler.current().id) catch |e|
|
||||||
// Confine the device's DMA before the driver can program it: a PCI function
|
return failErr(state, devices_broker.claimErrnoOf(e));
|
||||||
// becomes reachable to the IOMMU only once claimed (until now its DMA is
|
|
||||||
// blocked). A claim that cannot be confined must not stand — roll it back —
|
// Confine the device's DMA before the driver can program it: a PCI function
|
||||||
// since the whole point is that claiming a DMA device is no longer equivalent
|
// becomes reachable to the IOMMU only once claimed (until now its DMA is
|
||||||
// to ring 0. No-op when no IOMMU exists (fail-open).
|
// blocked). A claim that cannot be confined must not stand — roll it back —
|
||||||
if (devices_broker.pciAddressOf(device_id)) |bdf| {
|
// since the whole point is that claiming a DMA device is no longer equivalent
|
||||||
const owner = scheduler.current().id;
|
// to ring 0. No-op when no IOMMU exists (fail-open).
|
||||||
if (!iommu.confineDevice(device_id, bdf, owner)) {
|
if (devices_broker.pciAddressOf(device_id)) |bdf| {
|
||||||
_ = devices_broker.unclaim(device_id, owner);
|
const owner = scheduler.current().id;
|
||||||
return fail(state);
|
if (!iommu.confineDevice(device_id, bdf, owner)) {
|
||||||
}
|
_ = devices_broker.unclaim(device_id, owner);
|
||||||
// Bind the buffers this task allocated before claiming the device (a driver
|
// Its own errno: "nobody could confine this" is a different world from
|
||||||
// that dma_alloc'd its rings, then claimed the controller).
|
// "someone else already has it", and a driver that cannot tell them apart
|
||||||
dmaBindOwnerRegionsInto(owner, device_id);
|
// cannot report the one that means the machine's DMA protection ran out.
|
||||||
|
return failErr(state, ipc.ECONFINE);
|
||||||
}
|
}
|
||||||
// A display service just took the framebuffer — quiesce the bootstrap console
|
// Bind the buffers this task allocated before claiming the device (a driver
|
||||||
// so the kernel and the service don't scribble over each other's pixels. The
|
// that dma_alloc'd its rings, then claimed the controller).
|
||||||
// claim releases (and the console resumes) automatically if the service dies;
|
dmaBindOwnerRegionsInto(owner, device_id);
|
||||||
// see releaseTaskResourcesLocked.
|
}
|
||||||
if (devices_broker.displayDevice()) |display_id| {
|
// A display service just took the framebuffer — quiesce the bootstrap console
|
||||||
if (device_id == display_id) console.setSuppressed(true);
|
// so the kernel and the service don't scribble over each other's pixels. The
|
||||||
}
|
// claim releases (and the console resumes) automatically if the service dies;
|
||||||
architecture.setSystemCallResult(state, 0);
|
// see releaseTaskResourcesLocked.
|
||||||
} else fail(state);
|
if (devices_broker.displayDevice()) |display_id| {
|
||||||
|
if (device_id == display_id) console.setSuppressed(true);
|
||||||
|
}
|
||||||
|
architecture.setSystemCallResult(state, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// mmio_map(device_id, resource_index) -> virtual_address: map a claimed device's MMIO window into
|
/// mmio_map(device_id, resource_index) -> virtual_address: map a claimed device's MMIO window into
|
||||||
@@ -931,17 +937,21 @@ fn systemDeviceRegister(state: *architecture.CpuState) void {
|
|||||||
const parent_id = architecture.systemCallArg(state, 0);
|
const parent_id = architecture.systemCallArg(state, 0);
|
||||||
const descriptor_ptr = architecture.systemCallArg(state, 1);
|
const descriptor_ptr = architecture.systemCallArg(state, 1);
|
||||||
const t = scheduler.current();
|
const t = scheduler.current();
|
||||||
if (t.address_space == 0) return fail(state);
|
if (t.address_space == 0) return failErr(state, ipc.EPERM);
|
||||||
|
|
||||||
var descriptor: device_abi.DeviceDescriptor = undefined;
|
var descriptor: device_abi.DeviceDescriptor = undefined;
|
||||||
if (!ipc.copyFromUser(t.address_space, descriptor_ptr, std.mem.asBytes(&descriptor))) return fail(state);
|
if (!ipc.copyFromUser(t.address_space, descriptor_ptr, std.mem.asBytes(&descriptor))) return failErr(state, ipc.EFAULT);
|
||||||
|
|
||||||
// Under the big kernel lock: the broker's table is also mutated by the
|
// Under the big kernel lock: the broker's table is also mutated by the
|
||||||
// death sweep (releaseAllOwnedBy) and read by enumerate on other cores —
|
// death sweep (releaseAllOwnedBy) and read by enumerate on other cores —
|
||||||
// ring-3 registration (M19) made those genuinely concurrent.
|
// ring-3 registration (M19) made those genuinely concurrent.
|
||||||
const flags = sync.enter();
|
const flags = sync.enter();
|
||||||
defer sync.leave(flags);
|
defer sync.leave(flags);
|
||||||
const id = devices_broker.register(parent_id, t.id, &descriptor) catch return fail(state);
|
// Each refusal carries its own errno (devices-broker.errnoOf) — the bus driver
|
||||||
|
// logs which rule stopped it, so "this parent is full" is never again mistaken
|
||||||
|
// for "the table is full" or "that resource escapes your window".
|
||||||
|
const id = devices_broker.register(parent_id, t.id, &descriptor) catch |e|
|
||||||
|
return failErr(state, devices_broker.errnoOf(e));
|
||||||
architecture.setSystemCallResult(state, id);
|
architecture.setSystemCallResult(state, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+103
-8
@@ -51,6 +51,15 @@ fn check(name: []const u8, ok: bool) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `devices_broker.claim` reduced to a bool, for the `check` assertions below. The
|
||||||
|
/// broker returns `ClaimError` so ring 3 can tell "stale id" from "someone already
|
||||||
|
/// owns it" (the errno space in system/abi.zig); a test that only asserts the claim
|
||||||
|
/// succeeded does not care which, and the ones that do match the error directly.
|
||||||
|
fn claimOk(id: u64, owner: u32) bool {
|
||||||
|
devices_broker.claim(id, owner) catch return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/// Emit the overall result line the harness matches, then the done sentinel.
|
/// Emit the overall result line the harness matches, then the done sentinel.
|
||||||
fn result() void {
|
fn result() void {
|
||||||
log("DANOS-TEST-RESULT: {s} ({d} passed, {d} failed)\n", .{
|
log("DANOS-TEST-RESULT: {s} ({d} passed, {d} failed)\n", .{
|
||||||
@@ -250,6 +259,8 @@ pub fn run(case: []const u8, boot_information: *const BootInformation) void {
|
|||||||
irqFreeTest();
|
irqFreeTest();
|
||||||
} else if (eql(case, "containment")) {
|
} else if (eql(case, "containment")) {
|
||||||
containmentTest();
|
containmentTest();
|
||||||
|
} else if (eql(case, "apertures")) {
|
||||||
|
apertureTest();
|
||||||
} else if (eql(case, "device-manager")) {
|
} else if (eql(case, "device-manager")) {
|
||||||
deviceManagerTest(boot_information);
|
deviceManagerTest(boot_information);
|
||||||
} else if (eql(case, "protocol-registry")) {
|
} else if (eql(case, "protocol-registry")) {
|
||||||
@@ -1475,7 +1486,7 @@ fn ioPortTest() void {
|
|||||||
check("discovered the acpi-tables I/O window", true);
|
check("discovered the acpi-tables I/O window", true);
|
||||||
|
|
||||||
const me = scheduler.current();
|
const me = scheduler.current();
|
||||||
check("claimed the io_port device", devices_broker.claim(id, me.id));
|
check("claimed the io_port device", claimOk(id, me.id));
|
||||||
check("an in-range access resolves to port 0x64", process.resolveIoPort(me, id, found_res, 0x64, 1) == 0x64);
|
check("an in-range access resolves to port 0x64", process.resolveIoPort(me, id, found_res, 0x64, 1) == 0x64);
|
||||||
check("a 4-byte access at the last port is refused", process.resolveIoPort(me, id, found_res, 0xFFFF, 4) == null);
|
check("a 4-byte access at the last port is refused", process.resolveIoPort(me, id, found_res, 0xFFFF, 4) == null);
|
||||||
check("an out-of-range offset is refused", process.resolveIoPort(me, id, found_res, 0x10000, 1) == null);
|
check("an out-of-range offset is refused", process.resolveIoPort(me, id, found_res, 0x10000, 1) == null);
|
||||||
@@ -2470,8 +2481,8 @@ fn claimReleaseTest(boot_information: *const BootInformation) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The broker release in isolation.
|
// The broker release in isolation.
|
||||||
check("device 0 claimed by owner 111", devices_broker.claim(0, 111));
|
check("device 0 claimed by owner 111", claimOk(0, 111));
|
||||||
check("device 1 claimed by owner 222", devices_broker.claim(1, 222));
|
check("device 1 claimed by owner 222", claimOk(1, 222));
|
||||||
devices_broker.releaseAllOwnedBy(111);
|
devices_broker.releaseAllOwnedBy(111);
|
||||||
check("owner 111's claim is released", devices_broker.ownerOf(0) == null);
|
check("owner 111's claim is released", devices_broker.ownerOf(0) == null);
|
||||||
check("owner 222's claim survives", (devices_broker.ownerOf(1) orelse 0) == 222);
|
check("owner 222's claim survives", (devices_broker.ownerOf(1) orelse 0) == 222);
|
||||||
@@ -2493,7 +2504,7 @@ fn claimReleaseTest(boot_information: *const BootInformation) void {
|
|||||||
};
|
};
|
||||||
const child = process.spawnProcessSupervised(image, 4, &.{"/system/services/init"}, me, endpoint) catch 0;
|
const child = process.spawnProcessSupervised(image, 4, &.{"/system/services/init"}, me, endpoint) catch 0;
|
||||||
check("supervised child spawned", child != 0);
|
check("supervised child spawned", child != 0);
|
||||||
check("device 0 claimed on the child's behalf", devices_broker.claim(0, child));
|
check("device 0 claimed on the child's behalf", claimOk(0, child));
|
||||||
|
|
||||||
check("the kill is accepted", process.killProcess(me, child) == 0);
|
check("the kill is accepted", process.killProcess(me, child) == 0);
|
||||||
var badge: u64 = 0;
|
var badge: u64 = 0;
|
||||||
@@ -2501,7 +2512,7 @@ fn claimReleaseTest(boot_information: *const BootInformation) void {
|
|||||||
_ = ipcsync.replyWait(endpoint, 0, 0, 0, 0, abi.no_cap, &badge, &received_cap);
|
_ = ipcsync.replyWait(endpoint, 0, 0, 0, 0, abi.no_cap, &badge, &received_cap);
|
||||||
check("the exit notification arrived", badge == abi.notify_badge_bit | abi.notify_exit_bit | child);
|
check("the exit notification arrived", badge == abi.notify_badge_bit | abi.notify_exit_bit | child);
|
||||||
check("death released the child's claim", devices_broker.ownerOf(0) == null);
|
check("death released the child's claim", devices_broker.ownerOf(0) == null);
|
||||||
check("the device is claimable again", devices_broker.claim(0, me));
|
check("the device is claimable again", claimOk(0, me));
|
||||||
devices_broker.releaseAllOwnedBy(me);
|
devices_broker.releaseAllOwnedBy(me);
|
||||||
result();
|
result();
|
||||||
}
|
}
|
||||||
@@ -3765,8 +3776,8 @@ fn killThreadedGroupTest(boot_information: *const BootInformation) void {
|
|||||||
// Claims for BOTH members: group death must release every member's claims
|
// Claims for BOTH members: group death must release every member's claims
|
||||||
// before the supervisor hears anything — the worker's by the deferred
|
// before the supervisor hears anything — the worker's by the deferred
|
||||||
// (condemned) path.
|
// (condemned) path.
|
||||||
check("device 0 claimed for the leader", devices_broker.claim(0, child));
|
check("device 0 claimed for the leader", claimOk(0, child));
|
||||||
check("device 1 claimed for the worker", devices_broker.claim(1, worker));
|
check("device 1 claimed for the worker", claimOk(1, worker));
|
||||||
scheduler.sleep(100); // let the worker really be running on another core
|
scheduler.sleep(100); // let the worker really be running on another core
|
||||||
check("the supervisor's kill is accepted", process.killProcess(me, child) == 0);
|
check("the supervisor's kill is accepted", process.killProcess(me, child) == 0);
|
||||||
const badge = awaitExitBadge(endpoint);
|
const badge = awaitExitBadge(endpoint);
|
||||||
@@ -3954,7 +3965,7 @@ fn containmentTest() void {
|
|||||||
result();
|
result();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
check("claimed the parent device", devices_broker.claim(parent_id, me));
|
check("claimed the parent device", claimOk(parent_id, me));
|
||||||
defer devices_broker.releaseAllOwnedBy(me);
|
defer devices_broker.releaseAllOwnedBy(me);
|
||||||
|
|
||||||
// A child whose window lies inside the parent's is accepted.
|
// A child whose window lies inside the parent's is accepted.
|
||||||
@@ -3976,10 +3987,94 @@ fn containmentTest() void {
|
|||||||
check("re-registering an identical child returns the same id", again != 0 and again == good);
|
check("re-registering an identical child returns the same id", again != 0 and again == good);
|
||||||
check("re-registering grew nothing", devices_broker.enumerate(&buffer) == before + 1);
|
check("re-registering grew nothing", devices_broker.enumerate(&buffer) == before + 1);
|
||||||
|
|
||||||
|
// Fill the parent to its child cap with distinct children (same window, different
|
||||||
|
// identity — the match is on identity, so each is a new device).
|
||||||
|
var filled: u32 = 0;
|
||||||
|
var capped = false;
|
||||||
|
while (filled < 64) : (filled += 1) {
|
||||||
|
var name: [4]u8 = .{ 'k', 0, 0, 0 };
|
||||||
|
name[1] = '0' + @as(u8, @intCast(filled / 10));
|
||||||
|
name[2] = '0' + @as(u8, @intCast(filled % 10));
|
||||||
|
var extra = childDescriptor(name[0..3], parent_window.start, 0x20);
|
||||||
|
_ = devices_broker.register(parent_id, me, &extra) catch |err| {
|
||||||
|
capped = err == error.TooManyChildren;
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
check("the parent reaches its child cap (TooManyChildren)", capped);
|
||||||
|
|
||||||
|
// The regression this ordering exists for: **a re-registration consumes no slot,
|
||||||
|
// so a full parent must not refuse one.** A crashed bus driver is restarted by its
|
||||||
|
// supervisor and re-registers everything it rediscovers; when the cap was checked
|
||||||
|
// before the identity match, the restart was refused its own devices and the
|
||||||
|
// machine degraded a little more on every crash.
|
||||||
|
const readmitted = devices_broker.register(parent_id, me, &fits) catch 0;
|
||||||
|
check("a full parent still re-admits an identical child", readmitted != 0 and readmitted == good);
|
||||||
|
|
||||||
|
// ...and the cap is genuinely still in force for anything new.
|
||||||
|
var novel = childDescriptor("knew", parent_window.start, 0x20);
|
||||||
|
const still_capped = if (devices_broker.register(parent_id, me, &novel)) |_| false else |err| err == error.TooManyChildren;
|
||||||
|
check("a full parent still refuses a new child", still_capped);
|
||||||
|
|
||||||
result();
|
result();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A minimal child descriptor with one memory resource, for the containment test.
|
/// A minimal child descriptor with one memory resource, for the containment test.
|
||||||
|
/// PCI host-bridge apertures are derived from the *holes* in the firmware memory map,
|
||||||
|
/// and a registered BAR must fall inside one. So the invariant is not "we find the
|
||||||
|
/// holes" but "an aperture never covers memory the firmware described" — an aperture
|
||||||
|
/// over RAM means `device_register` containment admits a child BAR over kernel memory,
|
||||||
|
/// and its claimant can `mmio_map` it.
|
||||||
|
///
|
||||||
|
/// The map's length is the firmware's choice: 60–200 descriptors on a real machine,
|
||||||
|
/// 15–25 under OVMF, which is why the suite never saw this. The derivation used to
|
||||||
|
/// copy sub-4 GiB entries into a fixed `[64]` array and skip the rest — and a skipped
|
||||||
|
/// region is not merely lost, it is one the gap finder concludes is *free*.
|
||||||
|
fn apertureTest() void {
|
||||||
|
// 100 described one-page regions, 2 MiB apart: 99 small holes between them, then
|
||||||
|
// one large hole from the last region up to 4 GiB. Under the old fixed array the
|
||||||
|
// 36 regions past the 64th vanished, so the "largest hole" ran from ~128 MiB to
|
||||||
|
// 4 GiB — straight across 36 regions the firmware had described.
|
||||||
|
const spacing: u64 = 2 << 20;
|
||||||
|
var regions: [100]boot_handoff.MemoryRegion = undefined;
|
||||||
|
for (®ions, 0..) |*region, i| {
|
||||||
|
region.* = .{ .base = @as(u64, i) * spacing, .pages = 1, .kind = .usable };
|
||||||
|
}
|
||||||
|
|
||||||
|
var holes: [3]platform.AddressRange = undefined;
|
||||||
|
const found = platform.largestHolesBelow4G(®ions, 1 << 20, &holes);
|
||||||
|
check("apertures were derived from a 100-entry map", found > 0);
|
||||||
|
|
||||||
|
var overlaps: usize = 0;
|
||||||
|
var largest: platform.AddressRange = .{ .base = 0, .end = 0 };
|
||||||
|
for (holes) |hole| {
|
||||||
|
if (hole.end <= hole.base) continue;
|
||||||
|
if (hole.end - hole.base > largest.end - largest.base) largest = hole;
|
||||||
|
for (regions) |region| {
|
||||||
|
const region_end = region.base + region.pages * 4096;
|
||||||
|
if (region.base < hole.end and region_end > hole.base) overlaps += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
check("no aperture overlaps described memory", overlaps == 0);
|
||||||
|
|
||||||
|
// The big hole is above the last described region, not across it.
|
||||||
|
const last_end = (regions.len - 1) * spacing + 4096;
|
||||||
|
check("the largest aperture starts after the last described region", largest.base == last_end);
|
||||||
|
check("the largest aperture runs to 4 GiB", largest.end == (1 << 32));
|
||||||
|
|
||||||
|
// A map the firmware describes nothing in is one whole hole; a map that describes
|
||||||
|
// everything has none. Neither may invent an aperture over something described.
|
||||||
|
var empty: [3]platform.AddressRange = undefined;
|
||||||
|
check("an empty map yields one hole", platform.largestHolesBelow4G(&.{}, 1 << 20, &empty) == 1);
|
||||||
|
check("that hole is the whole low space", empty[0].base == 0 and empty[0].end == (1 << 32));
|
||||||
|
|
||||||
|
const whole = [_]boot_handoff.MemoryRegion{.{ .base = 0, .pages = (1 << 32) / 4096, .kind = .usable }};
|
||||||
|
var none: [3]platform.AddressRange = undefined;
|
||||||
|
check("a fully described map yields no aperture", platform.largestHolesBelow4G(&whole, 1 << 20, &none) == 0);
|
||||||
|
|
||||||
|
result();
|
||||||
|
}
|
||||||
|
|
||||||
fn childDescriptor(hid: []const u8, start: u64, len: u64) device_abi.DeviceDescriptor {
|
fn childDescriptor(hid: []const u8, start: u64, len: u64) device_abi.DeviceDescriptor {
|
||||||
var child = std.mem.zeroes(device_abi.DeviceDescriptor);
|
var child = std.mem.zeroes(device_abi.DeviceDescriptor);
|
||||||
child.class = @intFromEnum(device_abi.DeviceClass.unknown);
|
child.class = @intFromEnum(device_abi.DeviceClass.unknown);
|
||||||
|
|||||||
@@ -4,8 +4,14 @@
|
|||||||
//! hiding the trade-offs. Keeping them here makes them visible at a glance and gives
|
//! hiding the trade-offs. Keeping them here makes them visible at a glance and gives
|
||||||
//! one spot to change them. They're plain `comptime` constants (zero runtime cost);
|
//! one spot to change them. They're plain `comptime` constants (zero runtime cost);
|
||||||
//! any one can later be promoted to a `-D` build option if a target needs to vary it
|
//! any one can later be promoted to a `-D` build option if a target needs to vary it
|
||||||
//! (see build.zig's `-Dtest-case` for the pattern). This keeps ps2-library.zig to what it
|
//! (see build.zig's `-Dtest-case` for the pattern). This keeps [[boot-handoff]] to what
|
||||||
//! actually is — the bootloader↔kernel handoff *contract* — with tunables living here.
|
//! it actually is — the loader↔kernel handoff *contract* — with tunables living here.
|
||||||
|
//!
|
||||||
|
//! **Kernel only, and deliberately so.** This file exists because tunables were
|
||||||
|
//! crowding the loader↔kernel contract they were split out of; it is not a registry for
|
||||||
|
//! the whole system. A driver's ring size belongs to that driver, a protocol's payload
|
||||||
|
//! cap to that protocol. How a ceiling is *declared*, wherever it lives, is
|
||||||
|
//! docs/os-development/bounds.md — a shape, not a shared list.
|
||||||
|
|
||||||
/// Ceiling on logical CPUs the kernel tracks — the size of the per-CPU bookkeeping
|
/// Ceiling on logical CPUs the kernel tracks — the size of the per-CPU bookkeeping
|
||||||
/// arrays (discovery pool, scheduler state, per-core GDT/TSS). Generous headroom:
|
/// arrays (discovery pool, scheduler state, per-core GDT/TSS). Generous headroom:
|
||||||
|
|||||||
@@ -119,10 +119,10 @@ pub fn main(init: process.Init) void {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
node_id = node.id;
|
node_id = node.id;
|
||||||
if (!device.claim(node_id)) {
|
device.claim(node_id) catch |e| {
|
||||||
_ = logging.write("/system/services/acpi: unable to claim acpi-tables\n");
|
std.log.warn("unable to claim acpi-tables: {s}", .{@errorName(e)});
|
||||||
return;
|
return;
|
||||||
}
|
};
|
||||||
|
|
||||||
// Map the node's resources: the AML blobs (bytecode), the FADT (intact
|
// Map the node's resources: the AML blobs (bytecode), the FADT (intact
|
||||||
// "FACP" header — decision 3), the io_port grant, and the SCI irq.
|
// "FACP" header — decision 3), the io_port grant, and the SCI irq.
|
||||||
@@ -519,8 +519,8 @@ fn registerDevice(node: *aml.Node, hid: [8]u8, interpreter: *aml.Interpreter) vo
|
|||||||
@memcpy(descriptor.hid[0..@intCast(hid_len)], hid[0..@intCast(hid_len)]);
|
@memcpy(descriptor.hid[0..@intCast(hid_len)], hid[0..@intCast(hid_len)]);
|
||||||
applyCrs(&descriptor, node, interpreter);
|
applyCrs(&descriptor, node, interpreter);
|
||||||
|
|
||||||
const id = device.register(node_id, &descriptor) orelse {
|
const id = device.register(node_id, &descriptor) catch |e| {
|
||||||
std.log.info("register refused for {s}", .{hid[0..@intCast(hid_len)]});
|
std.log.warn("register refused for {s}: {s}", .{ hid[0..@intCast(hid_len)], @errorName(e) });
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
registered[registered_count] = .{ .hid = hid, .hid_len = @intCast(hid_len), .device_id = id, .resource_count = descriptor.resource_count };
|
registered[registered_count] = .{ .hid = hid, .hid_len = @intCast(hid_len), .device_id = id, .resource_count = descriptor.resource_count };
|
||||||
|
|||||||
@@ -74,10 +74,12 @@ pub const Gop = struct {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!device.claim(found.id)) {
|
device.claim(found.id) catch |e| {
|
||||||
_ = logging.write("display: could not claim the framebuffer\n");
|
var line: [96]u8 = undefined;
|
||||||
|
_ = logging.write(std.fmt.bufPrint(&line, "display: could not claim the framebuffer: {s}\n", .{@errorName(e)}) catch
|
||||||
|
"display: could not claim the framebuffer\n");
|
||||||
return null;
|
return null;
|
||||||
}
|
};
|
||||||
// Resource 0 is the framebuffer memory window; the kernel maps it write-combining
|
// Resource 0 is the framebuffer memory window; the kernel maps it write-combining
|
||||||
// because the resource carries that flag (docs/display-plan.md D1).
|
// because the resource carries that flag (docs/display-plan.md D1).
|
||||||
const front_base = device.mmioMap(found.id, 0) orelse {
|
const front_base = device.mmioMap(found.id, 0) orelse {
|
||||||
|
|||||||
@@ -978,9 +978,20 @@ CASES = [
|
|||||||
# device_register containment (in-kernel): registering a child whose MMIO window
|
# device_register containment (in-kernel): registering a child whose MMIO window
|
||||||
# escapes its parent's grant is refused (NotContained) — else dev_register would map
|
# escapes its parent's grant is refused (NotContained) — else dev_register would map
|
||||||
# arbitrary physical memory — while an identical re-register stays idempotent.
|
# arbitrary physical memory — while an identical re-register stays idempotent.
|
||||||
|
# Also pins the cap ordering: a parent already at maximum_children_per_parent must
|
||||||
|
# still re-admit an identical child (a restarted bus consumes no slot re-reporting
|
||||||
|
# what it rediscovers) while still refusing a genuinely new one.
|
||||||
{"name": "containment",
|
{"name": "containment",
|
||||||
"expect": r"DANOS-TEST-RESULT: PASS",
|
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||||
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||||
|
# PCI host-bridge apertures come from the holes in the firmware memory map, and a
|
||||||
|
# registered BAR must fall inside one. The invariant is that an aperture never
|
||||||
|
# covers memory the firmware described - otherwise device_register containment
|
||||||
|
# admits a child BAR over live RAM. Driven with a synthetic 100-entry map, since
|
||||||
|
# OVMF only ever produces 15-25 and real firmware 60-200.
|
||||||
|
{"name": "apertures",
|
||||||
|
"expect": r"DANOS-TEST-RESULT: PASS",
|
||||||
|
"fail": r"DANOS-TEST-RESULT: FAIL"},
|
||||||
# IRQ teardown: an exiting driver's line is masked and its slot cleared (so no
|
# IRQ teardown: an exiting driver's line is masked and its slot cleared (so no
|
||||||
# ISR notifies a freed endpoint), and a sibling owner sharing that endpoint
|
# ISR notifies a freed endpoint), and a sibling owner sharing that endpoint
|
||||||
# keeps its own binding. A long-running driver never reaches this teardown path.
|
# keeps its own binding. A long-running driver never reaches this teardown path.
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ pub fn main(init: process.Init) void {
|
|||||||
// The respawn only reaches this line because the kernel released the
|
// The respawn only reaches this line because the kernel released the
|
||||||
// previous instance's claim at death. A failed claim exits cleanly — the
|
// previous instance's claim at death. A failed claim exits cleanly — the
|
||||||
// manager reads "meant to stop" and the scenario fails loudly by silence.
|
// manager reads "meant to stop" and the scenario fails loudly by silence.
|
||||||
if (!device.claim(assigned)) {
|
device.claim(assigned) catch {
|
||||||
_ = logging.write("crash-test: claim failed\n");
|
_ = logging.write("crash-test: claim failed\n");
|
||||||
return;
|
return;
|
||||||
}
|
};
|
||||||
|
|
||||||
var manager: ?ipc.Handle = null;
|
var manager: ?ipc.Handle = null;
|
||||||
var tries: u32 = 0;
|
var tries: u32 = 0;
|
||||||
|
|||||||
@@ -71,10 +71,10 @@ pub fn main() void {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!device.claim(nic_id)) {
|
device.claim(nic_id) catch {
|
||||||
_ = logging.write("iommu-fault-test: FAIL claim\n");
|
_ = logging.write("iommu-fault-test: FAIL claim\n");
|
||||||
return;
|
return;
|
||||||
}
|
};
|
||||||
var function = pci.Function.map(nic_id, &descriptor) orelse {
|
var function = pci.Function.map(nic_id, &descriptor) orelse {
|
||||||
_ = logging.write("iommu-fault-test: FAIL config-space map\n");
|
_ = logging.write("iommu-fault-test: FAIL config-space map\n");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ pub fn main() void {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
writeLine("pci-cap-test: claiming ethernet function (device {d})\n", .{nic_id});
|
writeLine("pci-cap-test: claiming ethernet function (device {d})\n", .{nic_id});
|
||||||
if (!check("claim", device.claim(nic_id))) return;
|
if (!check("claim", if (device.claim(nic_id)) |_| true else |_| false)) return;
|
||||||
var function = pci.Function.map(nic_id, &descriptor) orelse {
|
var function = pci.Function.map(nic_id, &descriptor) orelse {
|
||||||
_ = logging.write("pci-cap-test: FAIL config-space map\n");
|
_ = logging.write("pci-cap-test: FAIL config-space map\n");
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user