482 lines
28 KiB
Markdown
482 lines
28 KiB
Markdown
# Security track execution plan: paths, protocol namespace, SMEP/SMAP
|
||
|
||
The design is settled in
|
||
[communication.md](os-development/communication.md),
|
||
[protocol-namespace.md](os-development/protocol-namespace.md),
|
||
[file-system-hierarchy.md](file-system-development/file-system-hierarchy.md),
|
||
and [smep-smap.md](os-development/smep-smap.md). This file is the build order
|
||
— one phase at a time, each phase green before the next starts. Delete or
|
||
archive this file when the last milestone lands.
|
||
|
||
**Context a fresh session should read first:** the four design docs above,
|
||
then this plan's *Settled decisions* section — those decisions came out of a
|
||
full-code grounding pass (2026-07-31) and must not be re-derived or reopened.
|
||
|
||
**Definition of green, every phase:** `zig build` clean, `zig build test`
|
||
clean, `python3 test/qemu_test.py` passes (existing scenarios plus the
|
||
phase's new ones — record the suite count in the checkbox), and the relevant
|
||
design doc's status/known-gap lines updated in the same commit. Commit per
|
||
green phase, style `area: lower-case declarative summary`, **no co-author
|
||
trailers**. On a suite failure, read
|
||
`zig-out/qemu-test/<case>-failed-serial.log` before changing anything.
|
||
|
||
**Workflow:** work in a dedicated git worktree on feature branches cut from
|
||
`main` (one branch per milestone group as marked below); when a group's
|
||
phases are all green, merge to `main` and push. The loop marks a phase `[x]`
|
||
in the same commit that lands it.
|
||
|
||
**Numbering note:** milestones use the design docs' own names (PM, H1–H3,
|
||
HS, P1–P4) — the M-number sequence is left alone (M19–M22 are reserved by
|
||
the logging/USB-lifecycle track).
|
||
|
||
## Status
|
||
|
||
**Live state — updated on `main` after every phase, so this file read from a
|
||
plain `main` checkout always tells the truth about where the work is.**
|
||
|
||
| | |
|
||
|---|---|
|
||
| Working on | **P3** — green at 109/109, adversarial review running, not yet committed |
|
||
| Branch carrying it | `feat/security-group-2` (pushed to origin) |
|
||
| On `main` | through the group 1 merge (`f3bc23c`): Phase 0, PM, H1 |
|
||
| Committed on the branch, awaiting the group 2 merge | P1 (`1ff0991`), P2 (`1379b69`) |
|
||
| Suite | 109 cases, all passing |
|
||
| Last updated | 2026-08-01 |
|
||
|
||
A checkbox below means the phase met its definition of green and was
|
||
committed — on the branch named above, which reaches `main` at the next
|
||
group boundary.
|
||
|
||
- [x] **Phase 0** — baseline: suite green on `main` (106/106, 2026-07-31; `zig build` + `zig build test` clean at 9a32380), plan committed
|
||
- [x] **PM** — path-migration flag-day (`/etc`→`/system/configuration`, `/var/log`→`/system/logs`, `/mnt/usb`→`/volumes/usb`; vfs carve-out for the two writable `/system` subtrees, FAT's `/var` mount split in two; suite 106/106)
|
||
- [x] **H1** — the `user-memory` module; nine stragglers converted; leaf U/S+W checks (plus physmap-coverage confirmation, so an `mmio_map`'d buffer cannot fault ring 0 — this also closes the same hazard on the IPC path; `fs_resolve`'s out-capacity bound made overflow-safe; suite 107/107)
|
||
- [x] **merge** group 1 → main, push (f3bc23c, 2026-07-31)
|
||
- [x] **P1** — envelope module + `Define`; vfs `NodeKind.protocol` + open-reply-capability; client `Channel` (mechanics only, nothing converted; suite unchanged at 107)
|
||
- [x] **P2** — registry in init; `/protocol` reserved; ServiceId flag-day (11 binds, 17 lookups; `protocol.csv` grants, chain-attested identity, dead-owner rebind; the kernel's endpoint-death sweep generalized off the retired registry; suite 108/108). Three adversarial review rounds closed six defects a green suite had missed: a forged power event could shut the machine down; the ping path leaked a capability per call, first in init and then in the shared harness; supervisor attestation by name was defeated by a laundering deputy; and the kernel let any handle-holder bind signals, timers, exits and IRQs to an endpoint it did not own.
|
||
- [x] **P3** — open grants: `protocol.csv` enforcement, denial test. `onOpen`
|
||
consults the manifest with the same chain-attested identity a bind uses, and a
|
||
refused caller gets the *same* answer as one naming a contract nobody bound —
|
||
`-ENOENT`, no capability, the same reply bytes, no log line, and both questions
|
||
asked on every open so there is nothing to time. Twenty-seven `open` rows cover
|
||
the whole live client set. One wrinkle the plan had not foreseen: the driver
|
||
tree is three deep (device manager → PS/2 bus → keyboard/mouse) and attestation
|
||
is one hop, so a legitimate grandchild read exactly like a laundering deputy;
|
||
the manifest gained a third permission, `supervise`, which names an authorized
|
||
supervising task per contract and is deliberately **open-only**, leaving P2's
|
||
bind attestation and every refusal it makes untouched (suite 109/109)
|
||
- [ ] **merge** group 2 → main, push
|
||
- [ ] **P4a** — clean protocols rebased onto `Define` (vfs, block, display, scanout, input)
|
||
- [ ] **P4b** — misfit protocols rebased (device-manager, power, usb-transfer)
|
||
- [ ] **P4c** — harness subscriber lift + badge-scoped per-client integers
|
||
- [ ] **merge** group 3 → main, push
|
||
- [ ] **H2** — SMEP on every core
|
||
- [ ] **HS** — SYSRET canonical-RIP guard
|
||
- [ ] **H3** — SMAP + boot-patched `clac`; `-cpu max` in the harness; negative tests
|
||
- [ ] **merge** group 4 → main, push
|
||
|
||
---
|
||
|
||
## Settled decisions (grounding pass, 2026-07-31 — do not reopen)
|
||
|
||
These resolve every open wrinkle the code inventory surfaced. Where one
|
||
amends a design doc, the amendment lands in the same commit as the phase
|
||
that implements it.
|
||
|
||
1. **Every packet — request, reply, and event — begins with the envelope
|
||
`Header`, exactly as the design says; the header is FOLDED, never
|
||
stacked.** It absorbs each protocol's existing operation/id fields
|
||
rather than sitting on top of them, so the two apparent 64-byte-limit
|
||
offenders fit: `ChildAdded` re-lays to 60 bytes (its packed operation
|
||
byte and `device_id` become `Header.operation`/`.target`);
|
||
`InterruptReport` puts `device_token` in `Header.target` and trims
|
||
inline data 48 → 40 bytes (largest real report today is 8). A
|
||
headerless-events variant was considered and REJECTED (2026-07-31): it
|
||
re-invents per-protocol mini-headers and breaks uniform tooling. No
|
||
design-doc amendment; `Define`'s event check stays ≤ 64 *including*
|
||
the header.
|
||
2. **Bind/open authorization is chain-attested identity: the
|
||
kernel-stamped binary name PLUS the supervision chain**, both read from
|
||
the kernel's process records (`ProcessDescriptor` carries `name` and
|
||
`supervisor`; init walks the chain with `process_enumerate` — no new
|
||
protocol). A grant row names the binary *and* the supervisor expected
|
||
in its chain, so a malicious process re-spawning a granted binary
|
||
(ungated `spawn`, hostile argv — the confused deputy) is refused: its
|
||
chain roots at the attacker, not at init or device-manager. Name alone
|
||
is NOT sufficient — that was considered and rejected (2026-07-31).
|
||
Pure delegation (device-manager forwarding driver binds as
|
||
capabilities — "option B") is deliberately deferred to P5, whose
|
||
spawner-wired namespaces subsume it. Amends protocol-namespace.md's
|
||
"Authorization" bullet in P2.
|
||
3. **Grants live in a new manifest, `/system/configuration/protocol.csv`**
|
||
(rows: `binary-path, supervisor, bind|open, protocol-name`, where
|
||
`supervisor` is the binary expected in the caller's supervision chain —
|
||
`init` for init's own children, `kernel` for harness-spawned fixtures),
|
||
not in extra init.csv columns — today every post-path init.csv field is
|
||
argv, and overloading that is ambiguous. init parses both files.
|
||
*(P2 spelling: the supervisor column carries the binary exactly as the
|
||
kernel stamped it, so init's own children say `/system/services/init` and
|
||
the drivers say `/system/services/device-manager`; `kernel` stays a bare
|
||
word because a kernel task has no binary. A trailing `*` on any field
|
||
matches a subtree, which is how decision 4's `/test/` rule is expressed.)*
|
||
*(Clarification, 2026-08-01: the supervisor column names **the authorized
|
||
supervising task, matched by identity** — the binary is how the row spells
|
||
it, but init checks the task id. `kernel` is satisfied only by supervisor
|
||
id 0 (which only the kernel confers — user `system_spawn` always stamps the
|
||
caller); init's own path only by this init's task id; any other path only by
|
||
a task init spawned itself or one the kernel spawned. Matching the supervisor
|
||
by *name* alone is defeated by a laundering deputy — an attacker runs its own
|
||
instance of `/system/services/init`, has that spawn `/system/services/input`,
|
||
and both stamped names satisfy the row while the chain is entirely the
|
||
attacker's. Walking to the root of the chain does not fix it either, since
|
||
the laundered chain still roots at the real PID 1.)*
|
||
*(P3 amendment: a third permission, `supervise`, joins `bind|open`. One-hop
|
||
attestation cannot express the one three-deep chain in the tree — the device
|
||
manager starts the PS/2 bus, and the bus starts the keyboard and mouse
|
||
drivers — and nothing structural tells that chain apart from the laundering
|
||
deputy, since both are a granted binary spawned by a granted binary. Only
|
||
policy can: a `supervise` row names the authorized supervising task the way
|
||
every other row names a claimant (binary, its own supervisor, the contract it
|
||
concerns), and an `open` row may then name that task in its supervisor
|
||
column. The delegate is itself attested the ordinary strict way, so the chain
|
||
still anchors in init or the kernel one hop above it and the recursion stops
|
||
there. It is **open-only** on purpose — a delegate may vouch for what its
|
||
children *reach*, never for what they *claim* — so the bind path is
|
||
byte-for-byte P2's and the laundering-deputy refusal is untouched.)*
|
||
4. **Test fixtures bind under `/protocol/test/...`**, granted to any
|
||
binary whose path starts `/test/` — the subtree-scoping rule from the
|
||
design doc, dogfooded. `shared_memory_test` (the borrowed-ServiceId
|
||
hack) becomes `/protocol/test/shared-memory`; process-test's child gets
|
||
`/protocol/test/process`.
|
||
5. **Rebind after provider death:** a `bind` hitting an existing binding
|
||
succeeds only if the current owner process is dead (init checks
|
||
liveness); otherwise `-EBUSY`. Init also unbinds in `restartChild`
|
||
before respawning its own children. This preserves collision-refusal
|
||
while making restart work for providers init does not supervise.
|
||
6. **Cross-thread service access** (the display mouse-listener's
|
||
per-thread self-lookup, `display.zig:512`): threads resolve and open
|
||
`/protocol/<name>` like any client — once, at thread startup. No
|
||
special mechanism.
|
||
7. **The envelope module is `library/protocol/envelope/envelope.zig`**
|
||
(module name `envelope`) — the one protocol-package module not ending
|
||
in `-protocol`, because it is not a protocol. Wired as a new
|
||
`addModule` row in `library/protocol/build.zig` with its host tests in
|
||
that package's test step.
|
||
8. **The QEMU harness gains `-cpu max`** (in `qemu_args`,
|
||
`test/qemu_test.py:66-83`) so TCG exposes SMEP/SMAP — without it the
|
||
enabled paths never execute in CI. Landed in H2 so the flag soaks
|
||
before H3 depends on it.
|
||
9. **Scenario fixtures that need the registry are init-driven.** Kernel
|
||
test cases that today spawn providers directly (shared-memory,
|
||
process-test) either spawn init first or move to init.csv-driven
|
||
scenario boots — resolved per-case in P2 with the suite as the
|
||
arbiter.
|
||
*(P2 resolution: init gained a `registry` argv role — it mounts
|
||
`/protocol`, reads the grants, and starts no services — and each affected
|
||
case calls `spawnRegistry(rd)` before its own providers. Every case keeps
|
||
its own spawn set, so no scenario had to be re-shaped.)*
|
||
10. **The capsule-staleness caveat is documented, not fixed.** On-volume
|
||
edits to `/system/configuration/*.csv` do not reach the initrd copy
|
||
the loader boots (capsule shadows tree). Same drift exists today with
|
||
`/etc`; PM adds the note to file-system-hierarchy.md and moves on.
|
||
|
||
---
|
||
|
||
## PM — path-migration flag-day
|
||
|
||
One commit, everything moves together. The authoritative site inventory is
|
||
the grounding pass; the checklist order:
|
||
|
||
1. Move repo `etc/` → `configuration/` sources; fix the three CSVs'
|
||
self-referencing headers (`etc/init.csv:1,12`, `etc/devices.csv:1`,
|
||
`etc/init-diagnose.csv:1`).
|
||
2. `build.zig:309-311`: bundled entries `etc/...` →
|
||
`system/configuration/...` (this alone re-shapes the image, manifest,
|
||
and capsule — `tools/make-fat-image.py` and the EFI loader need
|
||
nothing; the tree-walk fallback even starts picking the CSVs up, a
|
||
bonus fix).
|
||
3. `system/kernel/vfs.zig` `mountBackend` (`:332-340`): allow exactly
|
||
`/system/configuration` and `/system/logs` as backend prefixes beneath
|
||
the initrd `/system` mount; keep refusing everything else under
|
||
`/system` and `/test`.
|
||
4. `system/services/fat/fat.zig`: `mount_point` → `/volumes/usb` (`:25`);
|
||
replace the `/var` mount (`:155`) with two `mountRewritten` calls for
|
||
`/system/configuration` and `/system/logs`; update the mount log lines
|
||
(the harness matches them).
|
||
5. `system/services/init/init.zig:76` and
|
||
`system/services/device-manager/device-manager.zig:48`: open the new
|
||
CSV paths; update the message strings (`init.zig:77,92`,
|
||
`device-manager.zig:49,61-63,454`).
|
||
6. `system/services/logger/logger.zig:44`: `base = "/system/logs"`
|
||
(buffers derive from `base.len` comptime — nothing else changes).
|
||
7. `system/kernel/tests.zig:2808-2810`: exclude `/system/configuration/`
|
||
from the spawn-everything sweep (the CSVs are not programs).
|
||
8. Tests: `fat-test.zig` and `vfs-test.zig` `/mnt/usb` literals →
|
||
`/volumes/usb`; harness regexes `test/qemu_test.py:175,211,632,717`.
|
||
9. Comment sweep (init, device-manager, logger, fat, engine, vfs, abi,
|
||
file-system, csv, device, protocol/device-manager, drivers, acpi,
|
||
build.zig — full list in the grounding inventory); delete vestigial
|
||
repo `var/`.
|
||
|
||
**Test:** no new case — the existing 106 are the test, since fat/logger/
|
||
init/device-manager scenarios all assert the new paths through their
|
||
regexes. Suite stays 106.
|
||
|
||
## H1 — user-memory copy discipline
|
||
|
||
New kernel module `system/kernel/user-memory.zig`:
|
||
|
||
- `copyFromUser` moves from ipc-synchronous.zig (which re-exports or
|
||
imports it); new `copyToUser(user_as, user_va, source) bool` — the
|
||
mechanical mirror (kernel-source `copyAcross` already does this for IPC
|
||
replies at `ipc-synchronous.zig:431,460`).
|
||
- The page walk gains leaf U/S and writable checks: `paging.translateIn`
|
||
(`architecture/x86_64/paging.zig:513-525`) tests only `present` today —
|
||
add a flags-accumulating variant (2 MiB leaves included); reads require
|
||
U/S, writes require U/S+W. Closes the TODO at
|
||
`ipc-synchronous.zig:20-22`.
|
||
- Convert the nine stragglers (table in smep-smap.md). Read direction is
|
||
local to `process.zig`; the write direction restructures callees with
|
||
kernel bounce buffers: `scheduler.enumerate` (`scheduler.zig:1209`),
|
||
`devices_broker.enumerate` (`devices-broker.zig:136`), `log.readAt`
|
||
(`log.zig:209`), and the `fs_node` flows through
|
||
`vfs.nodeRead/nodeStatus/nodeReaddir` (`vfs.zig:257/269/289`).
|
||
|
||
**Test:** kernel unit coverage in `system/kernel/tests.zig` for
|
||
`copyToUser` bounds/permission refusals; one new QEMU case `user-memory` —
|
||
a fixture passes an unmapped-but-in-range buffer to `klog_read`,
|
||
`process_enumerate`, and `fs_resolve` and asserts `-EFAULT` returns with
|
||
the system still alive (today each would oops the kernel). Suite 107.
|
||
|
||
## P1 — envelope, vfs additions, Channel
|
||
|
||
- `library/protocol/envelope/envelope.zig`: `Header` {operation:u32, pad,
|
||
target:u64}, `Status`, reserved verbs (describe=0, enumerate=1,
|
||
subscribe=2, unsubscribe=3, protocol verbs from 16), `packet_maximum`
|
||
= 256 / `post_maximum` = 64 (the floor constants protocols compile
|
||
against — nothing exports them today), and comptime
|
||
`Define(.{name, version, operations, events})` generating request/reply
|
||
types, encode/decode, a provider dispatch table (automatic `describe`,
|
||
`-ENOSYS` for unknown verbs), and compile-time size checks:
|
||
request/reply ≤ 256, each `.events` entry ≤ 64 *including* its Header
|
||
(decision 1). Host unit tests in the protocol package's test step.
|
||
- `library/protocol/vfs/vfs-protocol.zig`: `NodeKind.protocol = 7`; the
|
||
open-reply-may-carry-capability convention documented in the module.
|
||
Rewrite the value-pinning unit test (`:108-117`) to pin the *new*
|
||
stable values.
|
||
- `library/kernel/file-system.zig` + a new `Channel` type in
|
||
`library/kernel` (or `library/client`): `open("/protocol/<name>")` →
|
||
resolve, vfs open, receive the reply capability → a `Channel` wrapping
|
||
the handle with `call`/typed helpers. Nothing uses it yet — P2 converts
|
||
the world.
|
||
- Docs: vfs-protocol.md's NodeKind table gains value 7 (no
|
||
protocol-namespace.md amendment — decision 1 conforms to it as written).
|
||
|
||
**Test:** host unit tests only (envelope round-trips, size-check compile
|
||
errors via `error` tests, Channel plumbing against a mock). Suite stays
|
||
107.
|
||
|
||
## P2 — the registry; ServiceId flag-day
|
||
|
||
The single biggest phase; one branch, may be several commits, green at the
|
||
end of each.
|
||
|
||
- **init as registry backend** (`system/services/init/init.zig`): a second
|
||
endpoint (the supervision endpoint's reply-empty loop is unsuitable for
|
||
a vfs backend); serve vfs `open`/`readdir` over `/protocol` plus the
|
||
`bind` operation (name payload + capability). Mount `/protocol` before
|
||
spawning children. Parse `/system/configuration/protocol.csv`
|
||
(decision 3). Authorization by chain-attested identity (decision 2):
|
||
badge → kernel process records → binary name **and** supervision chain
|
||
(walk `supervisor` links) checked against the grant row's expected
|
||
supervisor. Unbind on child death in `restartChild`; dead-owner rebind
|
||
rule (decision 5).
|
||
Provenance: readdir/diagnostics show name → pid → binary path.
|
||
- **Kernel:** reserve `/protocol` — `mountBackend` refuses mounts at or
|
||
under it once bound, `installMount`'s remount-replace path refuses it,
|
||
and `fs_unmount` refuses it (`vfs.zig:164-181,332-351`,
|
||
`process.zig:1879-1889`). First mount wins (init is PID 1).
|
||
- **Harness:** `library/kernel/service.zig` `Callbacks.service:
|
||
?abi.ServiceId` becomes a protocol name; the register call (`:49-51`)
|
||
becomes bind-with-retry via the registry.
|
||
- **Flag-day conversion** — all 11 registration sites and 17 lookup sites
|
||
from the grounding inventory: providers (input:123, ps2-bus:223,
|
||
device-manager:569, acpi:193, usb-xhci-bus:676, usb-storage:205,
|
||
fat:307, display:699, virtio-gpu:550, shared-memory-server:43,
|
||
process-test:130 → `/protocol/test/...` per decision 4); clients
|
||
(input-client:53, display-client:28, driver.zig:173, usb.zig:139,
|
||
block.zig:72+87, ps2-bus keyboard:35 + mouse:34, virtio-gpu:478,
|
||
display:314+512 (decision 6), acpi:212, init:218+245 — init
|
||
short-circuits its own registry, shared-memory-client:22,
|
||
process-test:85, device-list:22, crash-test:32). Retry loops keep their
|
||
cadence, wrapping resolve+open instead of lookup.
|
||
- **Delete:** `abi.zig:36-37` (syscall ids — leave holes),
|
||
`abi.zig:287-303` (enum), `process.zig:223-224,314-343`,
|
||
`ipc-synchronous.zig:41-43,646-664` and the registry sweep in
|
||
`:121-140`; the wrappers `library/kernel/ipc.zig:33-35,47-50`; comment
|
||
sweep (irq.zig:50, tests.zig:3744, vdso.md's syscall table, the docs
|
||
list in the inventory).
|
||
- Kernel-spawned test scenarios made init-driven where they need the
|
||
registry (decision 9).
|
||
|
||
**Test:** new QEMU case `protocol-registry`: a fixture asserts (a) bind of
|
||
an ungranted name → `-EPERM`, (b) bind collision with a live owner →
|
||
`-EBUSY`, (c) provider kill → re-resolve reaches the restarted instance.
|
||
Every existing scenario doubles as conversion proof. Suite 108.
|
||
|
||
## P3 — open grants (restriction stage one)
|
||
|
||
- `protocol.csv` `open` rows enforced in the registry's `open` handler,
|
||
same name-based identity as bind. Default rows grant what today's
|
||
clients need (from the P2 conversion table); a deliberate hole for the
|
||
test fixture.
|
||
- Docs: protocol-namespace.md stage-one section gets its "landed" line.
|
||
|
||
**Test:** new QEMU case `protocol-denied`: a fixture granted
|
||
`/protocol/test/shared-memory` but not `/protocol/display` asserts open of
|
||
the first succeeds and the second fails identically to not-found. Suite
|
||
109.
|
||
|
||
*Landed. Four things the plan did not foresee, recorded because P4 and P5
|
||
inherit them:*
|
||
|
||
- *`supervise` — decision 3's amendment. The PS/2 keyboard and mouse drivers
|
||
are started by the PS/2 bus driver, which the device manager started: the
|
||
tree's one three-deep chain, and one hop deeper than attestation reaches.
|
||
Nothing structural separates it from the laundering deputy, so the manifest
|
||
says which delegate is authorized, per contract. Open-only, so P2's bind
|
||
attestation is unchanged.*
|
||
- *Indistinguishability is a claim about work, not only about bytes. `onOpen`
|
||
refreshes the process table, identifies the caller, scans the grants and
|
||
scans the bindings on **every** open and forms one verdict at the end; and
|
||
it logs nothing on any branch, because `klog_read` is ungated (a line
|
||
written on one branch is a line the refused caller can read) and a serial
|
||
line is milliseconds it could time. The operator's diagnosis is the pair the
|
||
namespace publishes anyway: `readdir /protocol` for what is bound, the
|
||
manifest for who may reach it.*
|
||
- *The fixture is `protocol-denied-test`, and its scenario boots the **input
|
||
service** so the forbidden name is genuinely bound — the fixture reads the
|
||
namespace listing to prove it before asking for it. Without a live provider
|
||
the case would be comparing two boot races and asserting nothing.*
|
||
- *Two channels stay open by design, named rather than papered over: `readdir`
|
||
over `/protocol` lists every bound name to anyone (deliberate — the tree is
|
||
diagnosable), and `/system/configuration/protocol.csv` is world-readable on
|
||
the `/system` mount. Stage one hides neither the set of contracts nor the
|
||
policy; what it removes is the **oracle in the reply**, which is what stage
|
||
two's parked and faked opens depend on.*
|
||
|
||
## P4a — clean protocols onto Define
|
||
|
||
vfs, block, display, scanout, input — the modules whose shapes map
|
||
directly (grounding inventory §1,3,4,6,8):
|
||
|
||
- vfs: `node` → `target`; `Reply.node` (open's result) moves to reply
|
||
payload — `library/kernel/file-system.zig` decoders change; readdir
|
||
stays a protocol verb.
|
||
- block: pure renumber; `attach`'s DMA cap rides the call as today.
|
||
- display: the overloaded 40-byte `Request` becomes per-operation structs
|
||
(attach_scanout's field abuse dies); `layer` → `target`; blit payload
|
||
grows to 224 bytes.
|
||
- scanout: renumber; drop its bogus `message_maximum=64` (sync floor is
|
||
256); fix virtio-gpu's hard-coded `service.run(256, …)` to the
|
||
generated constant.
|
||
- input: subscribe merges into reserved subscribe; publish renumbers;
|
||
the event re-lays onto the Header folded (operation = event kind,
|
||
target = 0; 16 + 28-byte payload = 44 ≤ 64); **input moves onto the
|
||
service harness** (it is the last hand-rolled loop, no ping/terminate
|
||
compliance today).
|
||
|
||
**Test:** new QEMU case `protocol-conformance`: a fixture opens every
|
||
registered protocol and asserts `describe` answers (name, version) and an
|
||
unknown verb returns `-ENOSYS`. Existing input/display/fat scenarios prove
|
||
the rebase. Suite 110.
|
||
|
||
## P4b — misfit protocols onto Define
|
||
|
||
device-manager, power, usb-transfer (inventory §2,5,7 — the u8-operation
|
||
re-layouts and raw-offset readers):
|
||
|
||
- device-manager: u8 operations → Header; its enumerate=4/subscribe=5
|
||
merge into the reserved verbs; `ChildAdded` splits its dual role —
|
||
request struct and event, both Header-first (folded to 60 B ≤ 64);
|
||
`ChildRemoved`'s (parent, bus_address) addressing stays payload.
|
||
- power: u8 operations → Header; subscribe merges; **init's raw
|
||
byte-offset event parsing (`init.zig:171-173`) and acpi's
|
||
`message[0]` dispatch (`acpi.zig:435-467`) are rewritten against the
|
||
generated types** — the two silent-breakage sites, called out so the
|
||
loop treats them as first-class conversions, not collateral.
|
||
- usb-transfer: `device_token` → `target` (already layout-identical);
|
||
`InterruptReport` re-lays onto the Header (`device_token` → `target`,
|
||
inline data trimmed 48 → 40 — largest real report is 8); control/bulk
|
||
budgets re-verified by `Define` (Status absorbs `actual_length`).
|
||
|
||
**Test:** existing scenarios are the proof (device hot-add, power button,
|
||
USB storage/HID all exercise these wires); the conformance case now covers
|
||
three more providers. Suite 110.
|
||
|
||
## P4c — harness subscriber lift + badge scoping
|
||
|
||
- `library/kernel/service.zig` grows the subscriber table, exit-
|
||
notification sweep, and fan-out loop declared via `Define(.events)`;
|
||
input (:33-116), acpi (:67-68,393-406), and device-manager (:155-166)
|
||
delete their hand-rolled variants. One sweep idiom: exit notifications
|
||
(fat's pattern), replacing input's process-list polling and acpi's
|
||
none-at-all.
|
||
- Badge-scoped per-client integers (the guessable-id holes): fat node ids
|
||
gain owner checks on every operation (`fat.zig:72-76`), xhci device
|
||
tokens validate sender and sweep on exit (`usb-xhci-bus.zig:66-88,479`),
|
||
display layers gain an owner field.
|
||
|
||
**Test:** extend the fat scenario: a second fixture guesses the first's
|
||
node id and asserts refusal; kernel-side unit test for the harness sweep.
|
||
Suite 111.
|
||
|
||
## H2 — SMEP
|
||
|
||
- Generalize the cpuid helper (`apic.zig:351-365`, private, subleaf-0) to
|
||
a shared probe; gate on `cpuid(0).eax >= 7`.
|
||
- Set CR4 bit 20 in `per-cpu.zig:initSystemCall` (or a sibling called
|
||
from both `cpu.zig:148` and `smp.zig:181` — the one path both BSP and
|
||
every AP already execute). Log enabled/absent (fail-open, IOMMU style).
|
||
- Harness: add `-cpu max` to `qemu_args` (decision 8).
|
||
|
||
**Test:** new QEMU case `fault-smep` mirroring the `fault-*` injector
|
||
pattern (`tests.zig:3906-3938`): ring-0 call through a pointer into a
|
||
user-mapped page; expect `page fault (vector 14)` + `error code : 0x11` +
|
||
kernel-half IP, machine reports the exception (deliberate-exception cases
|
||
put the text in `expect`, per `qemu_test.py:189`). Suite 112.
|
||
|
||
## HS — SYSRET canonical-RIP guard
|
||
|
||
- `isr.s` syscall exit (`:256`): validate RCX canonicality before
|
||
`sysretq`; non-canonical → `iretq` fallback (or kill), per the hazard
|
||
note at `isr.s:192-194`.
|
||
|
||
**Test:** kernel unit case driving a thread whose return RIP is forged
|
||
non-canonical via the syscall path if constructible cheaply; otherwise the
|
||
review-level proof plus the existing fault cases regression. Suite 112.
|
||
|
||
## H3 — SMAP
|
||
|
||
- `clac` patch site at `isr_common` (`isr.s:367`, before the CPL test —
|
||
ring-0 nesting inherits AC too): assemble a 3-byte NOP, patch to `clac`
|
||
at boot through the physmap (the `process.zig:1990-1995` /
|
||
`smp.zig:79-111` precedent), BSP-only before AP bring-up.
|
||
- Set CR4 bit 21 in the same per-CPU init as SMEP.
|
||
- Coding standards: kernel code touches user memory only through
|
||
`user-memory`; no `stac` anywhere, ever.
|
||
|
||
**Test:** new QEMU case `fault-smap`: ring-0 deliberate read of a mapped
|
||
user page; expect vector 14 + `error code : 0x1` + kernel IP. And the
|
||
whole suite becomes the tripwire — any missed straggler now fails loudly.
|
||
Suite 113.
|
||
|
||
---
|
||
|
||
**Explicitly out of scope** (own tracks, after this plan): P5 restriction
|
||
stage two (spawn's initial capability, namespace views, parked replies,
|
||
dedicated killable channels — needs a design session on the spawn
|
||
contract), file-path namespacing, trusted UI (display track), pipes/FIFOs
|
||
(Python track), `/applications` and its storage, `fs_mount`/`spawn`/
|
||
`klog_read` gating beyond the `/protocol` reserved prefix, KPTI, IPC
|
||
priority inheritance.
|