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.
415 lines
23 KiB
Markdown
415 lines
23 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
|
||
|
||
- [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.
|
||
- [ ] **P3** — open grants: `protocol.csv` enforcement, denial test
|
||
- [ ] **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.)*
|
||
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.
|
||
|
||
## 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.
|