The block client gains subscribeMedium / unsubscribeMedium /
decodeMediumChanged (the reserved subscribe/unsubscribe verbs plus the
MediumChanged decode), so a consumer never hand-rolls the wire format.
The volume manager subscribes to each device it adopts and consumes the
event through the new on_buffered_message seam — never the protocol
dispatch, whose op numbers collide with the manager's own hello.
A medium leaving while its device stays in the tree (a card reader, an
eject) now runs the SAME kill-retire-remount path as a pulled stick:
absent retires the volume, present re-probes it. That closes the "two
triggers, one lifecycle" the architecture specifies — device-presence
polling alone could never see a medium leave under a present device.
dropDevice unsubscribes before closing so the driver's bounded
subscriber table frees the slot; on a dead channel (a real pull) the
call fails fast, proven by volume-removal still passing.
New volume-medium-change case: eject the medium (not the device) ->
"medium absent" -> the manager unmounts. Fails against a pre-S5 manager
that never subscribed. Suite 132/132 (device-authority is the known
child-cleanup flake, green on rerun).
The block protocol gains define_range (appended, numbers hold): confine the
process named by `badge` to blocks [base, base+count). usb-storage keeps a
per-badge range table and, in read/write, translates volume-relative LBAs
(base added) and refuses any transfer past the volume end. geometry returns
the confined size, so a filesystem mounts against what it may actually touch.
The security seam (decision 4, settled): the clamp lives at the PROVIDER, so
a channel carries exactly the authority it grants — handing a filesystem the
whole disk plus a base offset would let it reach the neighbouring partition.
The gate: a confined caller may NOT call define_range, so a filesystem cannot
widen its own range or confine anyone; only an unconfined party (the volume
manager, whole-device) may. The volume manager defines a filesystem's range
before handing it the channel, so the ordering holds by construction.
Default (no range for a badge) is the whole device — behaviour-neutral for a
single-volume boot and what the volume manager itself uses to probe
partitions. The range table is declared through bounds.md as a runaway
detector (ours, refuse at limit), not a real-partition cap. Neutral:
fat-mount, usb-storage, iommu-usb-storage green. The discrimination fixture
(a confined process reads past its range and is refused) follows next.
The kernel was always symmetric (dma_bind 51 / dma_unbind 52); the two
protocols that forward an attachment up the stack were one-way, so a live
client could grant a device reach into its buffer but never revoke it while
alive — exactly the one-way lifecycle the storage architecture's enforcement
section forbids. Death stays the mechanical backstop; detach is the living
process's path.
Both verbs are appended, so every existing number holds. The shape mirrors
attach precisely: the same region capability rides the cap slot again — the
kernel matches the region, so no layer retains anything between the calls
(the bus never kept the handle; now it never needs to).
fat's bring-up does attach -> detach -> attach, exercising both verbs
through the whole chain (fat -> storage -> bus -> kernel) on every boot: a
broken detach fails every fat case instead of lying dormant until the first
buffer replacement. Honest scope: the round trip proves the plumbing; unbind
semantics are the kernel iommu tests' (map/unmap/translationOf); the full
composition (detach then DMA faults) is a future iommu-fault extension.
P2 of docs/establishment-planes-plan.md. usb-storage serves nameless — one
process per stick cannot share an exclusive bind, and a second stick used to
die silently on -EBUSY before ever helloing. Its one hello now moves both
directions at once: the block-serving endpoint up, its controller channel
down. fat finds its volume through the manager — a new `.consumer` role asks
for the channel of the driver BOUND TO a device (distinct from the device's
reporter), found by enumerating the tree for the mass-storage identity.
fat stays single-volume; the boot-volume-by-content choice is M21.
The conversion immediately caught a live truncation of exactly the audit's
shape: ChildEntry grew to 32 bytes, one enumerate reply holds ~7, and a real
tree carries a dozen ACPI nodes before the first USB child — the storage
entry silently never fit (the protocol comment already said "paging joins
the protocol if a tree ever outgrows one packet"). enumerate is now paged:
Header.target is the start cursor, a short page is the end; device-list's
page-0 read is unchanged.
Grant rows move with the code: the block bind and fat's block open die, fat
gains open device-manager. Gate: 18 cases green including the registry
trio, device-list, and both IOMMU storage variants.
P1 of docs/establishment-planes-plan.md. The bus no longer binds
/protocol/usb-transfer — the bind race made whichever instance came second
unreachable, which on a real three-controller Ryzen meant a mouse no class
driver could reach ("could not open device 50"). Each instance hands its
serving endpoint up in the hello that already delegates its controller, and
class drivers receive their OWN controller's channel from helloForChannel —
routed by the manager's lineage, retried while a provider is mid-restart,
on one manager handle so retries never spend handle-table slots.
usb.open(bus, id) now takes the channel it used to look up; the name rows
leave protocol.csv with the code (bind 67, opens 119/120/122); the
conformance fixture's prose stops claiming the bus binds; and the stale
input-client import leaves the bus with the channel one.
Gate: 19 QEMU cases green (usb family, hubs, both IOMMU variants, fat chain,
boot-from-USB, orderly shutdown, conformance).
P0 of docs/establishment-planes-plan.md; no behavior changes yet, nothing
sets the new flag or sends a hello capability.
- service.run gains a reply-capability out-slot (replyWithCapability), the
registry idiom init already uses, lifted into the harness; null stays the
untouched common path. Subscribers gains claimArrival() so a provider
handler can keep a turn capability through the same flag the reserved
subscribe uses.
- Hello wire struct: the padding byte becomes wants_channel — old callers
wire-compatibly say 0, and the manager nominates a reply capability ONLY
when asked, because a capability sent to a caller that never reads one is
a leaked slot in that caller's table.
- driver.helloExchange: one handshake can hand a serving endpoint up and
receive the device's provider channel down (usb-storage will need both at
once). No channel in the reply is retryable, never a verdict.
- The manager stores each instance's serving endpoint on its Driver entry,
routes consumer hellos by lineage (child -> reporter -> endpoint), replaces
on re-hello, and closes the stale handle on death - the 32-slot table is
the bound that makes forgetting this boot-fatal.
The rule as planned: a device that was given to someone may be handed on,
never taken. Implemented, and honest about what it is worth.
Writing the test showed the plan had the wrong step doing the work. A
delegated device is HELD, so an attempt to take it is refused as
AlreadyClaimed before the giver is ever consulted; and once a borrower's
death returns the device to its lender — or clears both when the lender is
gone — there is no state where a device is unheld and still on loan. The
window a stranger could have used stops existing at E2. This check is
unreachable.
It stays anyway: one comparison, failing closed, guarding any future path
that frees a device without clearing its giver, which is exactly the hole
this run closed. The comment says it is unreachable rather than implying a
protection it does not provide.
The attacker fixture does not gain the assertion that was deferred to this
step, and its header records why: there is no refusal for it to observe, and
on a bare boot with no device manager nothing is delegated at all, so the
assertion had nothing to bite on. It failed loudly on its first run rather
than passing quietly, which is the only reason this was noticed.
It also leaves the loader's framebuffer alone without naming it: nobody
delegates the framebuffer, so it has no giver, so the display service claims
it exactly as before.
Suite 118/118.
The mechanism behind delegation, which device-manager.md named as the step
after hello: the device manager claims what discovery seeded and hands each
device to the driver it matched, so assignment stops being
first-come-first-served.
It is a MOVE, not a copy. A claim is exclusive (driver-model.md, invariant
1), so the giver stops holding the device the instant the receiver starts.
That is why this is a new syscall rather than the M13 capability path, where
a passed handle is shared refcounted — exclusivity cannot be expressed that
way.
The kernel's whole rule is that you may give away what you hold. It has no
notion of which task is the device manager and deliberately gains none: a
binary name inside the kernel is not something that cannot safely live in
user space. A recipient that does not exist is refused, because a device
moved to nobody would be unreachable for the rest of the boot — nothing
un-holds a device but task death.
Three errnos, each naming its own rule: ENODEV no such device, EPERM you do
not hold it, ESRCH no such recipient.
Nothing uses it yet. The five claimants move across one at a time in D4-D5,
so the suite stays green throughout and a regression names the driver that
caused it.
Ten assertions, verified to discriminate: removing the ownership check flips
four of them, including the giveaway that an illegal transfer then blocks
the legitimate claim behind it.
Suite 116 -> 117.
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.
These were the awkward ones. Each began with an operation packed into a
single byte — two of them with a version wedged in beside it — so there was
no wrapping them: the layouts had to be rebuilt. The device manager's own
enumerate and subscribe become the reserved verbs that mean the same thing
everywhere, its replies lose three status structs the envelope already
carries, and a device id becomes the packet's target. Power drops the
version it repeated on every request, because describe is the handshake,
and stops claiming a 64-byte ceiling it never needed for calls. USB moves a
control transfer's data to the packet tail in both directions, which makes
the status length the transferred length and retires a field that had been
saying the same thing twice.
The danger in this one was not the protocols but their readers. Init
recognised a power button by two bytes at the head of a message, the ACPI
service dispatched on the first byte, the xHCI driver read its operation
with a raw integer load, and the HID drivers reinterpreted a report
wholesale — none of which would have failed to compile once the layouts
moved. They would simply have stopped: no shutdown on the power button, no
reports from the keyboard. Every one of them now reads through the
generated types, and the shutdown gate that answers only a subscriber is
the same code it was.
Two sizes were decided by measuring rather than assuming. The child-added
message is both a request and the event broadcast to subscribers, and
alignment rounds it to 48 bytes, which puts its packet exactly on the
64-byte push floor — a test pins that, because a field added carelessly
would now overflow it. The interrupt report gives up eight bytes of inline
room to make space for the header; the two drivers that produce reports
send eight and four.
Suite 110/110.
The folded header stops being a rule in a document and becomes the layout
on the wire. Verbs number from sixteen, leaving describe, enumerate,
subscribe and unsubscribe reserved and answered the same way by every
provider — none of them writes a line to do it. What each protocol used to
carry in a field of its own now travels in the header: a vfs node and a
display layer are the packet's target, and a reply opens with a status the
envelope stamps rather than one each protocol spelled for itself.
Display gains the most. One forty-byte request had served eleven verbs, so
attach_scanout smuggled stride through x, refresh through y and format
through colour, and every coordinate crossed as a bitcast. Per-operation
structs end all three: the fields have their own names and their own signs,
and the tile payload grows to 224 bytes because the prefix shrank. Scanout
loses a message maximum of 64 it had no business declaring — it answers
calls, and the floor for a call is 256 — and virtio-gpu stops hard-coding
that number at its harness.
Two changes are semantic rather than notational. A directory now ends at an
entry with no name, because the fixed part of a reply always travels and a
zero-length reply no longer exists to mean anything. And input joins the
service harness, the last loop in the tree that answered no ping and heard
no terminate; its subscriber table, its pruning and its fan-out are the
same code, and a shutdown now asks it to stop instead of killing it.
A new conformance case reads the registry's own listing and asks every
protocol it finds for its name, its version and its verb count, then offers
a verb nobody defines and requires -ENOSYS — the envelope's promise,
checked against providers rather than against itself. What it cannot reach
in that boot it names on the serial line instead of passing quietly.
Suite 110/110.
A protocol is reached by name now, not by a compile-time integer. Init is
PID 1 and already knows which binary it started, so init serves /protocol
as a vfs backend: bind claims a contract with the provider's endpoint
attached, open answers with that endpoint as the reply's capability, and
readdir lists what is bound with the task and binary behind it. The kernel
reserves the prefix — nothing may mount over it, under it, or unmount it —
and ServiceId, ipc_register and ipc_lookup are gone, their syscall numbers
left vacant.
A bind is authorized by who the caller *is*: the kernel-stamped binary
together with the supervising task's identity, matched against
/system/configuration/protocol.csv. Identity, not spelling — spawn is
ungated, so an attacker can run any bundled binary, and a name-only rule
would have let it launder grants through an init of its own making. A name
a live process holds is refused to everyone else; a dead one's is released.
Three review rounds against a hostile ring-3 process found what 108 green
tests could not, because the suite contains no attacker. Publishing init's
supervision endpoint as the registry put PID 1's mailbox in every process's
hands, where two forged bytes reached the shutdown path: privileged traffic
is now believed only from the task that holds the contract it speaks for.
A capability arriving on a request outlived every path that ignored it,
one handle per call until the table was full — in init, and in the harness
ten services share — so the arriving capability is owned by the turn and
released unless a handler says otherwise. And the kernel let anyone holding
an endpoint handle aim signals, timers, exit notices and interrupts at it:
binding now requires having created it.
Suite 108/108. The new protocol-registry case asserts eleven properties,
each one an attack that must fail.
/etc/init.csv and /etc/devices.csv become /system/configuration/*.csv (the
repo's etc/ moves to system/configuration/, mirroring the runtime tree),
/var/log becomes /system/logs, and /mnt/usb becomes /volumes/usb. The
kernel VFS gains a carve-out so FAT may serve exactly /system/configuration
and /system/logs beneath the initrd-backed /system while /system and /test
themselves stay unshadowable; FAT's single /var mount splits into those two
rewritten mounts. The kvfs readdir check learns /system's third child and
the ramdisk spawn sweep skips the configuration tree.
Suite 106/106.
Each library domain (kernel, device, client, protocol, csv,
xkeyboard-config) now owns a build.zig + build.zig.zon that wires and
exports its modules, with a standalone `zig build test` per domain.
The kernel package also exports abi (source stays in system/abi.zig) so
every consumer names one module instance. The root build swaps its
createModule calls for b.dependency(...).module(...) — no binary moves;
the root is the pilot consumer (docs/build-packages-plan.md). Path
dependencies deduplicate by resolved location, so the diamond
(root -> device -> kernel, root -> kernel) yields a single instance of
each module. Boot-image file list unchanged.
Replaces L2's interim DMA pool (every buffer reachable by every claimed
device) with true per-grant confinement: a device reaches only buffers
whose capability was delegated to its driver.
Kernel:
- DmaRegionObject (handle kind 2): a delegation token naming a dma_alloc'd
region, passable across processes on the IPC cap slot like an endpoint
or shared-memory object. Frames stay owned by the allocating address
space (freed on dma_free/teardown as before); the token carries a `dead`
flag so a stale downstream handle can no longer bind a freed region.
- dma_alloc gains the dma_shareable flag: it returns a capability handle
in r8 and every region is tracked in a registry. A task's own regions
auto-bind into the devices it claims (its rings just work); foreign
buffers are bound explicitly.
- dma_bind / dma_unbind / handle_close syscalls (51-53). dma_bind maps a
held region (or shared-memory) capability into a claimed device's domain;
it is idempotent. handle_close reclaims a table slot (raised 16 -> 32).
- dma_free and task death unmap a region from every domain and invalidate
BEFORE its frames return to the allocator — the stale-IOTLB use-after-
free window, closed structurally.
Protocols (flag-day): block gains attach, usb-transfer gains dma_attach —
each carries a region capability on the cap slot. fat allocates its bounce
buffer shareable and attaches it; usb-storage allocates its transport
buffers shareable, attaches them to the controller, and forwards fat's
capability downstream; usb-xhci-bus binds and closes; virtio-gpu binds its
shared scanout surface. The physical addresses on the wire are unchanged
(identity IOVA), so no register-programming code moved.
Cross-process DMA (fat -> usb-storage -> xHC) now flows only through
delegated capabilities. iommu-usb-storage / iommu-usb-hid / iommu-fault
all green under per-grant enforcement; 104/104 overall (fail-open paths
unchanged).
Replaces L1's shared blanket identity domain with a private translation
domain per claimed PCI function. A device now reaches only:
- the DMA pool: every dma_alloc'd region, mapped into every claimed
device's domain (poolAdd/poolRemove, driven from the dma_alloc and
dma_free syscalls). This keeps the cross-process buffer handoff
working (fat's bounce buffer reaches the xHC) while blocking the
kernel, page tables, process heaps, MMIO, and unallocated RAM.
- its own firmware reserved region (RMRR), seeded at confine time.
The pool is the honest interim: devices can still reach one another's
DMA buffers. The DMA-region capability layer (next) narrows it to
per-grant reachability.
dma_free unmaps from every domain and invalidates BEFORE the frames
return to the allocator, closing the stale-IOTLB use-after-free window.
Driver death tears down its domains (detach + free tables) before the
broker claims and DMA frames are released.
New iommu_fault_drain syscall (+ driver.iommuFaultDrain) forces pending
fault records to the log on demand. The new iommu-fault case proves it:
a claimed e1000e is programmed to DMA-fetch its TX ring from an unmapped
page; VT-d faults the access (bdf 00:03.0 addr 0x1000 reason 0x6) and the
system stays alive. 104/104.
library/device/pci is now the complete generic floor a leaf PCI driver
needs, instead of just what virtio-gpu used:
- pci-class: capability IDs, MSI/MSI-X/power-management/PCI-Express
register layouts, extended-capability header decode (host-tested),
per-bit command constants, remaining header offsets.
- pci.Function: header accessors, disableBusMaster + interrupt-disable
helpers, findCapability, programMsi/disableMsi, MsiX vector-table
struct, ensurePowerStateD0, functionLevelReset (BAR save/restore),
extended-capability iterator.
Proven by the new pci-caps QEMU case: a pci-cap-test fixture claims an
extra e1000e (PM+MSI+PCIe+MSI-X, no danos driver) and readback-verifies
every surface, including the first driver-side use of msi_bind.
usb-xhci-bus converts from 8 ms event-ring polling to message-signalled
interrupts: plain MSI where offered (real Intel xHC), MSI-X entry 0
otherwise (qemu-xhci has no MSI capability), byte-identical polling as
fallback. The timer survives as a 250 ms port-reconcile/lost-edge tick —
real-hardware USB2 hub debounce still needs it. MSI setup runs BEFORE
controller bring-up: QEMU's xhci only registers the MSI-X vector as used
when IMAN.IE is written while MSI-X is already enabled; interrupts are
silently dropped otherwise (real hardware does not care about the order).
101/101 QEMU cases green; real-hardware smoke passed (mouse works,
boot 2026-07-23T174805Z, plain-MSI branch, vector 33).
Replace init's hardcoded boot_services array with an authoritative,
human-readable service list read at boot, mirroring /etc/devices.csv. Each row
is a service binary path followed by its argv; startup order is file order,
shutdown the reverse. There is no hardcoded fallback — a missing file starts
nothing (the no-ramdisk isolation behavior).
- library/csv: shared CSV helpers (comment strip, field iteration) with unit
tests; device-registry is refactored onto them so both /etc/*.csv files parse
through one place.
- init reads /etc/init.csv into fixed-max static tables (the same pattern as the
device registry) and passes each row's argv straight to spawnSupervised. This
also makes boot-time modes (e.g. device-manager test-usb-restart) expressible
as data rather than hardcoded.
- Diagnose mode (-Ddiagnose omits the display stack) becomes build-time file
selection between etc/init.csv and etc/init-diagnose.csv, so init carries no
comptime service logic; the diagnose build option is dropped from init.
- build.zig: csv module wired; /etc/init.csv bundled into the initrd; the
device-registry tests move to a dedicated block since they now import csv.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KJqSiLLchDUUCoXn5jsiwd
Replace the three hardcoded switch tables (pciDriverForIdentity, hidDriverFor,
usbDriverForIdentity) with an authoritative, human-readable device registry the
manager reads at boot. Matching is most-specific-wins across
base/subclass/prog_if/vendor/device/subsystem/hid, so a precise vendor:device
rule and a generic class rule coexist; an unmatched device is logged, never
guessed. This resolves docs' "matching stays code until the third bus".
- ABI: child_added and DeviceDescriptor gain vendor/device/subsystem; child_added
gains a bus discriminator (BusKind) so PCI and USB class triples match against
the right namespace.
- pci-bus reads vendor/device (config 0x00) and subsystem (0x2C, type-0) and
reports them.
- library/device/registry: freestanding CSV parser + matchDriver() with
specificity scoring; 5 unit tests wired into `zig build test`.
- etc/devices.csv bundled into the initrd; the kernel serves /etc directly, so
the manager reads it before any filesystem service is up (fat starts later).
- virtio-gpu: drop the now-redundant post-spawn 1AF4:1050 re-confirm, since the
registry binds this driver by exact identity.
- Remove the orphaned system/drivers/display driver (unreferenced by build or
registry).
- docs: new devices-csv.md; device-manager.md "matching stays code" resolved.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KJqSiLLchDUUCoXn5jsiwd
Every user binary and the two device-logic library modules (pci, usb) now
`@import` the concern modules directly instead of aliasing through `runtime`:
runtime.ipc/process/time/service/input/block/display -> @import("<module>")
runtime.device / runtime.device_manager -> @import("driver")
runtime.fs -> @import("file-system")
runtime.Thread -> @import("thread").Thread
runtime.system.{write,writeRecord,klog*} -> logging.*
runtime.system.{sleep,timerOnce,wallClock,clock} -> time.*
runtime.system.{spawn*,kill,exit,yield,processes,...}-> process.*
runtime.system.{mmap,munmap,PROT_*} -> memory.*
runtime.dma.* / runtime.shared_memory.* / runtime.allocator -> memory.*
Each consumer keeps its own alias name (e.g. `const device = @import("driver")`),
so call sites are unchanged and there are no collisions with local `driver`
variables. build.zig now injects the concern modules into every user binary via
`default_imports`; pci/usb module import lists were updated to match.
The `runtime` and `system` shims remain for one more step (root.zig still uses
runtime); they are deleted in C5. Nothing but root.zig imports `runtime` now.
Verified: zig build, zig build test, and 17 QEMU cases (smoke, device-manager,
logger, fat-mount, fat-mutations, usb-storage, usb-hid, display-native,
virtio-gpu, input, thread-spawn, thread-mutex, process-kill, shared-memory,
driver-restart, acpi-ps2, pci-scan).
Follow-up cleanup of the just-moved kernel device code:
- power.zig -> acpi.zig. Its reboot() is built entirely on the FADT reset
register (acpi.power_information) plus the legacy 0xCF9/8042 fallbacks — it is
ACPI reboot, so it becomes acpi.reboot (the "P" in ACPI). platform.reboot
still delegates; soft-off/S5 stays the ring-3 acpi service's job as before.
- device-tree.zig -> fdt.zig. It is a discovery *backend* (the ARM/FDT parser,
a sibling of acpi.zig), not part of the model — renaming it to its actual
subject kills the confusing device-tree / device-model DeviceTree name clash
and makes acpi.zig + fdt.zig read as the two parallel backends.
- Flatten: device-model.zig, acpi.zig, fdt.zig, platform.zig move out of the
system/kernel/devices/ subdir up into system/kernel/, joining devices-broker.zig
(already flat). The kernel dir is a flat pile by convention (only architecture/
is a subdir), so the subdir — and its poor "devices" name — is gone.
Pure restructure; "platform" module name unchanged, all cross-file deps are
relative siblings that moved together. zig build + test green; smoke, discovery,
acpi-parse, acpi-ps2, acpi-report, power-button, orderly-shutdown, reboot pass.
With the shared device data (device-abi, pci-class, usb-abi, usb-ids, acpi-ids,
aml) now in library/device/, what remained in system/devices/ was purely
kernel-internal: the firmware-discovery machinery and the rich pointer-based
device model (platform, device-model, acpi, device-tree, power), reached only
through the "platform" module by three kernel files. It belongs with the kernel.
Move it to system/kernel/devices/. A pure relocation: the "platform" module
name is unchanged and every cross-dir dependency is a module import, so only the
one b.path and some comments move. The top-level split is now clean —
system/kernel/ is the kernel, library/device/ the shared device libraries,
system/{drivers,services} the userspace. The runtime /system/devices concept
(the virtual device tree) is unaffected; system/kernel/devices/ is its
implementation.
Also fixes two comment refs that still pointed device-abi at its pre-Wave-1a
home (system/devices/) — it lives at library/device/model/ now.
zig build + test green; smoke, discovery, acpi-parse, acpi-ps2, acpi-report pass.
mmio is device-driver code, so it joins the other domains under
library/device/mmio/ (module name "mmio" unchanged — a pure relocation, only
the build paths move). And its abbreviated function names are spelled out per
docs/coding-standards.md:
read -> readRegister mb -> memoryBarrier
write -> writeRegister rmb -> readMemoryBarrier
wmb -> writeMemoryBarrier
All call sites updated (virtio-gpu, usb-xhci-library, pci.Function); the two
display-driver placeholders import mmio but use nothing, so they're untouched.
Docs (driver-model graph, README layout, drivers.md, the FHS note) follow the
new path and names.
zig build + test green; virtio-gpu, display-native, display-reattach, usb-hid,
usb-hub, usb-storage, pci-scan pass.
Finish the direct-import convention: a protocol is now aliased to its module
name in snake_case in every file that imports one — consumers and the runtime
client wrappers alike. This kills the last of the alias variety (the generic
`protocol`, plus `power` and `transfer`), so `device_manager_protocol.Hello`
means the same thing everywhere and grepping a module name finds all its uses.
Pure rename (identifier uses only; prose references left untouched via a
guarded pattern). 14 files, 299/299 lines.
zig build + test green; 16 QEMU cases pass (smoke, device-manager,
driver-restart, device-list, pci-scan, usb-hid, usb-storage, display-service,
display-native, display-reattach, input, acpi-ps2, vfs, fat-mount,
power-button, orderly-shutdown).
The USB class-driver transfer client was library/runtime/usb.zig, re-exported
as runtime.usb — which compiled the USB client and usb-transfer-protocol into
every user binary (init, fat, the compositor…), none of which speak USB. It is
bus-family logic, not core runtime.
Move it to its domain home, library/device/usb/usb.zig (module "usb"), alongside
the usb-abi and usb-ids data modules; fix its internal imports to go through the
runtime module; and re-export usb.abi / usb.ids so a class driver reaches the
whole USB domain through one import. runtime.usb and runtime's
usb-transfer-protocol import are removed; the three class drivers
(usb-hid keyboard/mouse, usb-storage) import module "usb" directly.
zig build + test green; usb-hid, usb-storage pass.
New pci logic module (library/device/pci/pci.zig): a device driver's view of
the one PCI function it has claimed — Function.map (config space = resource 0),
vendorId/deviceId/command/status, enableMemoryAndBusMaster, mapBar (BAR decode
+ resource correlation + mmio_map, cached), and a capabilities() iterator. The
generic PCI mechanics every leaf PCI driver used to re-derive inline.
The config-space layout it needs — header offsets, the command MEM|bus-master
bits, the status capabilities-list bit, the capability-pointer mask, and the
BAR bit fields — is named in the pci-class data module (a "Configuration-space
layout" section), so the bus enumerator can share the same constants later.
virtio-gpu is the first consumer: its inline mapBar + walkCapabilities + config
header reads are gone, replaced by pci.Function; only the virtio-specific
cfg_type dispatch (and the virtio common-config cfgRead/cfgWrite, which are NOT
PCI config space) stay in the driver. The generic display driver will use the
same module. pci-bus's enumerator (arbitrary-function probing) is untouched.
zig build + test green; virtio-gpu, display-native, display-reattach, pci-scan pass.
First step of the device-code reorganization (plan: group device code by
domain, split each domain into a shareable data module + a logic module).
This moves the pure-data modules — the enums, wire types, and taxonomies
that any layer including the kernel can import cheaply — out of
system/devices/ and into their domain home:
device-abi -> library/device/model/device-abi.zig
pci-class -> library/device/pci/pci-class.zig
usb-abi -> library/device/usb/usb-abi.zig
usb-ids -> library/device/usb/usb-ids.zig
acpi-ids -> library/device/acpi/acpi-ids.zig
aml/ -> library/device/acpi/aml/
Module names are unchanged, so this is a pure file move: only the
b.addModule paths and the host-test file list in build.zig change; no
importer is touched. system/devices/ now holds only the kernel-internal
device model (device-model, platform, acpi, device-tree, power).
The device *logic* (the pci Function helper, the usb transfer client) and
the kernel's cosmetic taxonomy dependency are handled in following commits.
zig build + zig build test green.