library: the harness keeps the subscribers, and an id belongs to whoever opened it
Three services had each written the same thing and got it three different ways: input polled the process list to notice a dead subscriber, and only when someone else subscribed; the power service never noticed at all; the device manager noticed drivers but not subscribers. The harness owns the table now, driven by the events a protocol declares — it registers on the reserved verb, frames each event once, posts to everyone interested without waiting on any of them, and reclaims a slot when the kernel says its owner died. Interest masks moved to the envelope, so a subscriber that wants only mice asks the same way everywhere. Two consequences the plan had not foreseen. The device manager now hears a supervised child's death twice, once as its supervisor and once as a subscriber, so restart backoff counted every crash twice and gave up after half as many; it retires the id before counting. And the kernel's published exit table had eight slots for what is now six subscriptions in a plain boot, so it holds sixteen. The other half is a hole the design named early and left standing: a backend handed out a small integer and then honoured it from anyone. A process that guessed a file's node id read another client's file; a display layer had no owner at all, so any client could reconfigure or destroy any layer; a USB device token was never checked against the client that opened it. Each is now bound to the task that opened it, and a wrong owner gets exactly what an unknown id gets — the refusal must not become the oracle the identical answers elsewhere were designed to remove. Closing a file changed with it: it used to succeed unconditionally, which would have told a caller which ids existed. Suite 111/111, with a new case in which one process holds a file and a layer, hands both ids to a second process, and finds them untouched after that process has tried everything with them.
This commit is contained in:
parent
2719b93530
commit
1b1c587c14
|
|
@ -326,6 +326,7 @@ pub fn build(b: *std.Build) void {
|
||||||
if (test_case != null) for ([_][]const u8{
|
if (test_case != null) for ([_][]const u8{
|
||||||
"vfs-test", // the user-space VFS round-trip client
|
"vfs-test", // the user-space VFS round-trip client
|
||||||
"fat-test",
|
"fat-test",
|
||||||
|
"badge-scope-test", // the guessable-id probe: a second process names the first's node and layer
|
||||||
"shared-memory-server",
|
"shared-memory-server",
|
||||||
"shared-memory-client",
|
"shared-memory-client",
|
||||||
"crash-test", // hellos to the device manager, then faults — drives the crash-loop cap
|
"crash-test", // hellos to the device manager, then faults — drives the crash-loop cap
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@
|
||||||
.@"virtio-gpu" = .{ .path = "system/drivers/virtio-gpu" },
|
.@"virtio-gpu" = .{ .path = "system/drivers/virtio-gpu" },
|
||||||
.@"vfs-test" = .{ .path = "test/system/services/vfs-test", .lazy = true },
|
.@"vfs-test" = .{ .path = "test/system/services/vfs-test", .lazy = true },
|
||||||
.@"fat-test" = .{ .path = "test/system/services/fat-test", .lazy = true },
|
.@"fat-test" = .{ .path = "test/system/services/fat-test", .lazy = true },
|
||||||
|
.@"badge-scope-test" = .{ .path = "test/system/services/badge-scope-test", .lazy = true },
|
||||||
.@"shared-memory-server" = .{ .path = "test/system/services/shared-memory-server", .lazy = true },
|
.@"shared-memory-server" = .{ .path = "test/system/services/shared-memory-server", .lazy = true },
|
||||||
.@"shared-memory-client" = .{ .path = "test/system/services/shared-memory-client", .lazy = true },
|
.@"shared-memory-client" = .{ .path = "test/system/services/shared-memory-client", .lazy = true },
|
||||||
.@"crash-test" = .{ .path = "test/system/services/crash-test", .lazy = true },
|
.@"crash-test" = .{ .path = "test/system/services/crash-test", .lazy = true },
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,16 @@ one world.
|
||||||
| app → manager | `subscribe` (reserved verb 2) | receive published add/remove events; the subscriber's endpoint rides as the call's capability |
|
| app → manager | `subscribe` (reserved verb 2) | receive published add/remove events; the subscriber's endpoint rides as the call's capability |
|
||||||
| manager → app | `child_added` / `child_removed` events | the same two structs, pushed rather than called |
|
| manager → app | `child_added` / `child_removed` events | the same two structs, pushed rather than called |
|
||||||
|
|
||||||
|
The watcher table behind those last two rows is the **service harness's**
|
||||||
|
(`service.Subscribers`, shared with input and power), not the manager's: it
|
||||||
|
answers `subscribe`/`unsubscribe`, frames each event once for the fan-out, and
|
||||||
|
sweeps a watcher on its exit notification — where the manager previously had no
|
||||||
|
sweep for watchers at all. Its own supervised-driver exits are a different thing
|
||||||
|
and unchanged, except that a driver's death now arrives twice (the manager is
|
||||||
|
both its supervisor and a subscriber to published exits), so the manager retires
|
||||||
|
a dead driver's process id as it handles the first and the second finds nothing
|
||||||
|
to act on.
|
||||||
|
|
||||||
Two of what used to be the manager's own operations are the envelope's **reserved**
|
Two of what used to be the manager's own operations are the envelope's **reserved**
|
||||||
verbs, which mean the same thing at every provider in the system, so this protocol
|
verbs, which mean the same thing at every provider in the system, so this protocol
|
||||||
numbers only three of its own (`hello` = 16, `child_added` = 17,
|
numbers only three of its own (`hello` = 16, `child_added` = 17,
|
||||||
|
|
|
||||||
|
|
@ -211,6 +211,18 @@ shell, a terminal, a cursor, and a wallpaper:
|
||||||
| `damage` | mark a region of a layer dirty |
|
| `damage` | mark a region of a layer dirty |
|
||||||
| `present` | request a repaint: composited at the next frame-clock tick |
|
| `present` | request a repaint: composited at the next frame-clock tick |
|
||||||
|
|
||||||
|
**A layer belongs to the client that created it.** The id is a slot in a
|
||||||
|
sixteen-entry table — small, dense, guessable — so every verb above that names one is
|
||||||
|
answered only for the task whose `create_layer` produced it, and a layer that is
|
||||||
|
somebody else's is refused exactly as one that never existed (`-ENOENT`), so a client
|
||||||
|
cannot use the refusal to learn which ids are live
|
||||||
|
([protocol-namespace.md](../os-development/protocol-namespace.md): handles are scoped
|
||||||
|
per client, validated against the badge). The compositor's own layers — the cursor
|
||||||
|
sprite and the startup self-check's pair — are marked service-owned and are created by
|
||||||
|
direct call rather than over the protocol, so no client can move or destroy the
|
||||||
|
cursor. A dead client's layers are released on its exit notification, the same sweep
|
||||||
|
the FAT server runs for open files.
|
||||||
|
|
||||||
Text is intentionally *not* an operation — a client renders glyphs by blitting tiles
|
Text is intentionally *not* an operation — a client renders glyphs by blitting tiles
|
||||||
(the [PSF font](../../system/kernel/font.psf) path the console already uses can move into a
|
(the [PSF font](../../system/kernel/font.psf) path the console already uses can move into a
|
||||||
client). Keeping the protocol to rectangles and tiles keeps the compositor small and the
|
client). Keeping the protocol to rectangles and tiles keeps the compositor small and the
|
||||||
|
|
|
||||||
|
|
@ -108,15 +108,25 @@ This is the async counterpart of `ipc_call`, and the input service is its first
|
||||||
`publishJoystickEvent`. Publishing is a short synchronous `ipc_call` the service answers at
|
`publishJoystickEvent`. Publishing is a short synchronous `ipc_call` the service answers at
|
||||||
once; the service's own fan-out is asynchronous, so publishing never blocks on a slow
|
once; the service's own fan-out is asynchronous, so publishing never blocks on a slow
|
||||||
subscriber.
|
subscriber.
|
||||||
- The **service** ([input.zig](../../system/services/input/input.zig)) keeps a small subscriber
|
- The **service** ([input.zig](../../system/services/input/input.zig)) owns none of that
|
||||||
table (endpoint handle + owning task id + `device_mask`). On `publish` it `ipc_send`s the
|
machinery any more: the subscriber table (endpoint handle + owning task + interest mask),
|
||||||
event to every subscriber whose mask includes the event's device class. On `subscribe` it
|
the reserved `subscribe`/`unsubscribe` verbs, the fan-out, and the dead-subscriber sweep
|
||||||
stores the passed capability and mask and, as housekeeping, prunes any slot whose owning
|
are the shared harness's (`service.Subscribers` in
|
||||||
process has exited (checked against `process_enumerate`) — not for correctness (an async
|
[service.zig](../../library/kernel/service.zig)), so every event stream in the system has
|
||||||
send to an orphaned endpoint is harmless) but to reclaim the slot. The service runs on the
|
identical semantics. What is left in this file is what is actually about input: which
|
||||||
shared harness ([service.zig](../../library/kernel/service.zig)) like every other, so it
|
class an event belongs to, and which classes a subscriber asked for. On `publish` it names
|
||||||
answers the universal ping and exits on `terminate`; it was the last hand-rolled receive
|
the event's class and the harness `ipc_send`s the packet — framed once — to every
|
||||||
loop in the tree, and the last service a shutdown had to kill rather than ask.
|
subscriber whose mask includes it.
|
||||||
|
- **A dead subscriber goes away on its exit notification**, not on a poll. The service used
|
||||||
|
to walk `process_enumerate` on every subscribe and drop slots whose owner had gone; it now
|
||||||
|
subscribes to the kernel's published exits like the FAT server and the compositor do
|
||||||
|
([process-lifecycle.md](../os-development/process-lifecycle.md)), which reclaims the slot
|
||||||
|
*and* closes the endpoint capability in it promptly rather than at the next subscribe.
|
||||||
|
(The fan-out also drops a subscriber whose `ipc_send` fails, as a backstop for a
|
||||||
|
notification a full ring dropped.)
|
||||||
|
- The service runs on the shared harness like every other, so it answers the universal ping
|
||||||
|
and exits on `terminate`; it was the last hand-rolled receive loop in the tree, and the
|
||||||
|
last service a shutdown had to kill rather than ask.
|
||||||
|
|
||||||
Publisher and subscriber must be **separate processes**: a single thread that both
|
Publisher and subscriber must be **separate processes**: a single thread that both
|
||||||
published and serviced its own subscription would deadlock (its `publish` call blocks until
|
published and serviced its own subscription would deadlock (its `publish` call blocks until
|
||||||
|
|
|
||||||
|
|
@ -212,10 +212,24 @@ Open-node ids live in the backend. A client that dies without closing leaks
|
||||||
nothing permanently: the backend (the FAT server) subscribes to the kernel's
|
nothing permanently: the backend (the FAT server) subscribes to the kernel's
|
||||||
published process-exit events (docs/process-lifecycle.md) and releases a dead
|
published process-exit events (docs/process-lifecycle.md) and releases a dead
|
||||||
client's handles. The kernel VFS root needs no sweep at all — its node tokens
|
client's handles. The kernel VFS root needs no sweep at all — its node tokens
|
||||||
are permanent for a boot and carry no open state. Ids are plain integers, not
|
are permanent for a boot and carry no open state.
|
||||||
capabilities — a backend trusts its callers with each other's ids today, which
|
|
||||||
is acceptable while every client is part of the system image and worth
|
Ids are plain integers rather than capabilities, so the backend **scopes them
|
||||||
revisiting (per-client id namespaces) before third-party binaries arrive.
|
to the caller's badge**: an open node belongs to the task that opened it, and
|
||||||
|
every verb that names one — read, write, status, readdir, close — is answered
|
||||||
|
only for that task. A node id is a small number drawn from a table of
|
||||||
|
thirty-two, trivially guessable, and until this rule a backend honoured every
|
||||||
|
client's ids from every other client
|
||||||
|
(docs/os-development/protocol-namespace.md: *handles must be scoped per
|
||||||
|
client — validated against the badge, or drawn from a per-client id
|
||||||
|
namespace*).
|
||||||
|
|
||||||
|
The refusal is deliberately **identical to absence**: a node that is somebody
|
||||||
|
else's answers `-ENOENT`, exactly as one that was never opened, so a prober
|
||||||
|
learns nothing about which ids are live — the same discipline the protocol
|
||||||
|
namespace applies to a refused open. The owner is a *task*, because the badge
|
||||||
|
is: a threaded client uses a node from the thread that opened it, which is
|
||||||
|
already the granularity of the exit sweep that releases it.
|
||||||
|
|
||||||
## Evolution rules
|
## Evolution rules
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,12 @@ asked once at connect time rather than out of every packet's budget.
|
||||||
|
|
||||||
Events are published, not polled: like the input service, the service holds
|
Events are published, not polled: like the input service, the service holds
|
||||||
subscriber endpoints as capabilities and `ipc_send`s each event as a buffered
|
subscriber endpoints as capabilities and `ipc_send`s each event as a buffered
|
||||||
packet, so a slow or dead subscriber can never wedge the source. **The kind is
|
packet, so a slow or dead subscriber can never wedge the source. The table, the
|
||||||
the packet's operation** — one declared event per named kind, exactly as the
|
reserved `subscribe`/`unsubscribe` verbs and the fan-out are the **service
|
||||||
|
harness's** (`service.Subscribers`), shared with input and the device manager, so
|
||||||
|
the acpi service's own code is the ACPI half only — and a subscriber that dies is
|
||||||
|
now swept on its exit notification, where before this service had no sweep at
|
||||||
|
all. **The kind is the packet's operation** — one declared event per named kind, exactly as the
|
||||||
input service delivers one per device class — so a subscriber reads *what
|
input service delivers one per device class — so a subscriber reads *what
|
||||||
happened* out of the header rather than out of a tag inside the payload. The
|
happened* out of the header rather than out of a tag inside the payload. The
|
||||||
vocabulary is hardware-neutral:
|
vocabulary is hardware-neutral:
|
||||||
|
|
@ -69,7 +73,10 @@ sequence over everything else. The acpi service implements this as a **soft
|
||||||
gate** — it honors `shutdown` only from a process that is a *subscriber*, and
|
gate** — it honors `shutdown` only from a process that is a *subscriber*, and
|
||||||
init is the one subscriber. That stands in for "only the system supervisor may
|
init is the one subscriber. That stands in for "only the system supervisor may
|
||||||
power off" without hard-coding a pid, so it still holds under tests where PID 1
|
power off" without hard-coding a pid, so it still holds under tests where PID 1
|
||||||
is not init.
|
is not init. The question is asked of the harness's table now
|
||||||
|
(`Subscribers.has(sender)`), which is why the harness exposes it: the gate is
|
||||||
|
unchanged, including the badge being the whole of it — the badge is
|
||||||
|
kernel-stamped, so nothing inside a packet can claim to be init.
|
||||||
|
|
||||||
## Orderly shutdown
|
## Orderly shutdown
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -188,9 +188,15 @@ zombie state or privileged snooping:
|
||||||
state by all along is the id the exit event carries.
|
state by all along is the id the exit event carries.
|
||||||
|
|
||||||
Subscription, not broadcast-to-everyone: only processes that asked receive
|
Subscription, not broadcast-to-everyone: only processes that asked receive
|
||||||
events, the kernel keeps a bounded subscriber table, and delivery is the same
|
events, the kernel keeps a bounded subscriber table (sixteen — a normal boot
|
||||||
non-blocking coalescing notification as everything else — a dying process never
|
already fields six, since this is what *every* provider with per-client state
|
||||||
waits on its mourners. Subscribing is ungated, like `process_enumerate`: what is
|
releases on), and delivery is the same non-blocking coalescing notification as
|
||||||
|
everything else — a dying process never waits on its mourners.
|
||||||
|
A service does not usually write the sweep itself: the shared service harness
|
||||||
|
subscribes for it and drops a dead task's event subscriptions
|
||||||
|
(`service.Subscribers`), and a provider adds its own handler only for state the
|
||||||
|
harness knows nothing about — open files, layers, device tokens.
|
||||||
|
Subscribing is ungated, like `process_enumerate`: what is
|
||||||
running (and dying) is not a secret between cooperating processes. Subscribers
|
running (and dying) is not a secret between cooperating processes. Subscribers
|
||||||
do not receive the exit reason — the filesystem server does not care *why*
|
do not receive the exit reason — the filesystem server does not care *why*
|
||||||
the client died.
|
the client died.
|
||||||
|
|
@ -285,6 +291,10 @@ callbacks (`on_terminate`, `on_reload`) for programs that want defaults.
|
||||||
|
|
||||||
`service` owns the `replyWait` loop and folds every event source — signals,
|
`service` owns the `replyWait` loop and folds every event source — signals,
|
||||||
child exits, protocol messages — into callbacks, with the vocabulary's defaults:
|
child exits, protocol messages — into callbacks, with the vocabulary's defaults:
|
||||||
|
it also owns the **subscriber side** of any protocol that declares events
|
||||||
|
(`service.Subscribers`: the table, the reserved `subscribe`/`unsubscribe` verbs,
|
||||||
|
the fan-out, and the sweep on a subscriber's published exit), so every event
|
||||||
|
stream in the system behaves identically.
|
||||||
`terminate` returns from the loop (clean exit), the common `ping` is answered automatically,
|
`terminate` returns from the loop (clean exit), the common `ping` is answered automatically,
|
||||||
`reload` is ignored unless overridden. One loop, no locking, nothing reentrant. A
|
`reload` is ignored unless overridden. One loop, no locking, nothing reentrant. A
|
||||||
service author writes domain logic; the lifecycle contract is satisfied by the
|
service author writes domain logic; the lifecycle contract is satisfied by the
|
||||||
|
|
|
||||||
|
|
@ -36,11 +36,11 @@ plain `main` checkout always tells the truth about where the work is.**
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Working on | **P4c** — harness subscriber lift + badge-scoped client ids |
|
| Working on | **H2** — SMEP (group 4) |
|
||||||
| Branch carrying it | `feat/security-group-3` (pushed to origin) |
|
| Branch carrying it | `feat/security-group-4` (cut next) |
|
||||||
| On `main` | Phase 0, PM, H1, P1, P2, P3 — groups 1 and 2 merged |
|
| On `main` | everything through P4c — groups 1, 2 and 3 merged |
|
||||||
| Awaiting merge | P4a, P4b — land with the group 3 merge |
|
| Awaiting merge | nothing |
|
||||||
| Suite | 110 cases, all passing |
|
| Suite | 111 cases, all passing |
|
||||||
| Last updated | 2026-08-01 |
|
| Last updated | 2026-08-01 |
|
||||||
|
|
||||||
A checkbox below means the phase met its definition of green and was
|
A checkbox below means the phase met its definition of green and was
|
||||||
|
|
@ -67,8 +67,19 @@ group boundary.
|
||||||
- [x] **merge** group 2 → main, push
|
- [x] **merge** group 2 → main, push
|
||||||
- [x] **P4a** — clean protocols rebased onto `Define` (vfs, block, display, scanout, input; display's one overloaded request split per-operation and its field abuse ended, scanout's bogus 64-byte maximum deleted, directory EOF re-spelled as a nameless entry, input moved onto the service harness; new `protocol-conformance` case asks every reachable provider for `describe` and requires `-ENOSYS` for an undefined verb; suite 110/110)
|
- [x] **P4a** — clean protocols rebased onto `Define` (vfs, block, display, scanout, input; display's one overloaded request split per-operation and its field abuse ended, scanout's bogus 64-byte maximum deleted, directory EOF re-spelled as a nameless entry, input moved onto the service harness; new `protocol-conformance` case asks every reachable provider for `describe` and requires `-ENOSYS` for an undefined verb; suite 110/110)
|
||||||
- [x] **P4b** — misfit protocols rebased (device-manager, power, usb-transfer; every leading operation byte folded into the header, and with it the `device_id`/`device_token` that followed it — `Header.target` now carries the device in all three. device-manager's own `enumerate`/`subscribe` became the reserved verbs and its three `{status, reserved}` reply structs the envelope's `Status`; `ChildAdded` is one struct under two numbers, a call and an event, landing exactly on the 64-byte push floor. power's kinds became one declared event each, the input protocol's shape, so init reads *what happened* from the header; usb-transfer's control data stage moved to the packet tail in both directions, which made `Status.len` the transferred length and `actual_length` redundant. The two silent-breakage sites — init's byte-offset power parse and acpi's `message[0]` dispatch — are gone, the shutdown badge gate unchanged; three more rows in the conformance table. Suite 110/110)
|
- [x] **P4b** — misfit protocols rebased (device-manager, power, usb-transfer; every leading operation byte folded into the header, and with it the `device_id`/`device_token` that followed it — `Header.target` now carries the device in all three. device-manager's own `enumerate`/`subscribe` became the reserved verbs and its three `{status, reserved}` reply structs the envelope's `Status`; `ChildAdded` is one struct under two numbers, a call and an event, landing exactly on the 64-byte push floor. power's kinds became one declared event each, the input protocol's shape, so init reads *what happened* from the header; usb-transfer's control data stage moved to the packet tail in both directions, which made `Status.len` the transferred length and `actual_length` redundant. The two silent-breakage sites — init's byte-offset power parse and acpi's `message[0]` dispatch — are gone, the shutdown badge gate unchanged; three more rows in the conformance table. Suite 110/110)
|
||||||
- [ ] **P4c** — harness subscriber lift + badge-scoped per-client integers
|
- [x] **P4c** — harness subscriber lift + badge-scoped per-client integers (the
|
||||||
- [ ] **merge** group 3 → main, push
|
subscriber table, the reserved subscribe/unsubscribe verbs, the fan-out and the
|
||||||
|
dead-subscriber sweep are `service.Subscribers` now; input, acpi and
|
||||||
|
device-manager deleted three hand-rolled variants and their three different
|
||||||
|
ideas of when a subscriber goes away, standardizing on published exit
|
||||||
|
notifications — acpi had no sweep at all and input polled the process list on
|
||||||
|
every subscribe. The three guessable-id namespaces are scoped to the opening
|
||||||
|
badge: FAT node ids on every verb that names one, xHCI device tokens on open,
|
||||||
|
control, bulk and interrupt_subscribe, display layers on configure, fill, blit,
|
||||||
|
damage and destroy — each refusing a wrong owner with the *same* answer as an id
|
||||||
|
nobody holds. New `badge-scope` case, two processes of one fixture, every
|
||||||
|
refusal paired with a control; suite 111/111)
|
||||||
|
- [x] **merge** group 3 → main, push
|
||||||
- [ ] **H2** — SMEP on every core
|
- [ ] **H2** — SMEP on every core
|
||||||
- [ ] **HS** — SYSRET canonical-RIP guard
|
- [ ] **HS** — SYSRET canonical-RIP guard
|
||||||
- [ ] **H3** — SMAP + boot-patched `clac`; `-cpu max` in the harness; negative tests
|
- [ ] **H3** — SMAP + boot-patched `clac`; `-cpu max` in the harness; negative tests
|
||||||
|
|
@ -456,6 +467,33 @@ the wire formats will want them:*
|
||||||
node id and asserts refusal; kernel-side unit test for the harness sweep.
|
node id and asserts refusal; kernel-side unit test for the harness sweep.
|
||||||
Suite 111.
|
Suite 111.
|
||||||
|
|
||||||
|
*Landed. Four things the plan had not foreseen:*
|
||||||
|
|
||||||
|
- *One sweep idiom means one more kernel subscriber per provider, and the kernel's
|
||||||
|
published-exit table held **eight**. A normal boot now fields six (fat, input,
|
||||||
|
power, device-manager, display, and one per xHCI controller), so the table grew
|
||||||
|
to sixteen. It is not a table anyone notices until a service silently loses its
|
||||||
|
sweep, which is exactly the failure the old ceiling was two subscriptions away
|
||||||
|
from.*
|
||||||
|
- *The device manager hears each of its drivers die **twice** now — it is both the
|
||||||
|
supervisor its spawn named and, through the harness, a subscriber to published
|
||||||
|
exits — and the notify ring delivers the two badges separately. Untreated, one
|
||||||
|
death counted as two: the restart backoff doubled and the crash-loop cap fired
|
||||||
|
at half the deaths it names. `onDriverExit` therefore retires the dead process
|
||||||
|
id before it decides anything, and the second notification finds nothing to act
|
||||||
|
on. (The `driver-restart` and `pci-scan` drills are what would have caught it.)*
|
||||||
|
- *Refusal-equals-absence has a corollary for the verbs that **release**: FAT's
|
||||||
|
`close` used to answer 0 for an unknown node, so scoping it had to change that
|
||||||
|
too — a foreign node and a free one both answer `-ENOENT`, or the pair would
|
||||||
|
have been an oracle for which ids are live. The same applies to the harness's
|
||||||
|
`unsubscribe`.*
|
||||||
|
- *Ownership is per **task**, not per process, because the badge is: the kernel
|
||||||
|
stamps the sending thread's id, which is already the granularity of the exit
|
||||||
|
sweep that releases the state (a worker thread's death releases the handles that
|
||||||
|
worker opened). Nothing in the tree shares an id across its own threads today;
|
||||||
|
a per-process notion would need the kernel to stamp the leader, and belongs with
|
||||||
|
P5's spawner-wired namespaces if it is ever wanted.*
|
||||||
|
|
||||||
## H2 — SMEP
|
## H2 — SMEP
|
||||||
|
|
||||||
- Generalize the cpuid helper (`apic.zig:351-365`, private, subleaf-0) to
|
- Generalize the cpuid helper (`apic.zig:351-365`, private, subleaf-0) to
|
||||||
|
|
|
||||||
|
|
@ -87,11 +87,14 @@ pub fn build(b: *std.Build) void {
|
||||||
});
|
});
|
||||||
// The harness binds the service's contract name at startup, which is a
|
// The harness binds the service's contract name at startup, which is a
|
||||||
// conversation with the registry — hence channel (and time, for the patience
|
// conversation with the registry — hence channel (and time, for the patience
|
||||||
// a provider that beat init to the mount needs).
|
// a provider that beat init to the mount needs). It also owns the subscriber
|
||||||
|
// table and the fan-out, which are expressed in the envelope's vocabulary
|
||||||
|
// (the reserved subscribe verb, the push floor) — hence envelope.
|
||||||
_ = b.addModule("service", .{
|
_ = b.addModule("service", .{
|
||||||
.root_source_file = b.path("service.zig"),
|
.root_source_file = b.path("service.zig"),
|
||||||
.imports = &.{
|
.imports = &.{
|
||||||
.{ .name = "channel", .module = channel },
|
.{ .name = "channel", .module = channel },
|
||||||
|
.{ .name = "envelope", .module = protocol.module("envelope") },
|
||||||
.{ .name = "ipc", .module = ipc },
|
.{ .name = "ipc", .module = ipc },
|
||||||
.{ .name = "process", .module = process },
|
.{ .name = "process", .module = process },
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,12 @@
|
||||||
//! therefore safe, and keeping is explicit; the opposite arrangement quietly
|
//! therefore safe, and keeping is explicit; the opposite arrangement quietly
|
||||||
//! spends a handle-table slot per request.
|
//! spends a handle-table slot per request.
|
||||||
//!
|
//!
|
||||||
|
//! The harness also owns the **subscriber side** of a protocol that declares
|
||||||
|
//! `.events` — see `Subscribers`. The table, the reserved subscribe/unsubscribe
|
||||||
|
//! verbs, the fan-out, and the dead-subscriber sweep live here rather than in
|
||||||
|
//! each provider, so every event stream in the system has identical semantics
|
||||||
|
//! (docs/os-development/protocol-namespace.md, "Wiring").
|
||||||
|
//!
|
||||||
//! The liveness probe: a **zero-length request is the universal ping**, answered
|
//! The liveness probe: a **zero-length request is the universal ping**, answered
|
||||||
//! with a zero-length reply by the harness itself. No protocol's requests start
|
//! with a zero-length reply by the harness itself. No protocol's requests start
|
||||||
//! at length zero, so the encoding cannot collide, and there is nothing for a
|
//! at length zero, so the encoding cannot collide, and there is nothing for a
|
||||||
|
|
@ -19,9 +25,23 @@
|
||||||
//! is the diagnosis (see docs/ipc.md).
|
//! is the diagnosis (see docs/ipc.md).
|
||||||
|
|
||||||
const channel = @import("channel");
|
const channel = @import("channel");
|
||||||
|
const envelope = @import("envelope");
|
||||||
const ipc = @import("ipc");
|
const ipc = @import("ipc");
|
||||||
const process = @import("process");
|
const process = @import("process");
|
||||||
|
|
||||||
|
/// The harness's handle on a provider's subscriber table, type-erased because
|
||||||
|
/// `run` is not generic over the protocol while `Subscribers` is. A service names
|
||||||
|
/// its table once, as `Callbacks.subscribers`, and the loop does the rest: it
|
||||||
|
/// subscribes to published process exits at startup and drops a dead task's
|
||||||
|
/// subscriptions before the service's own notification callback ever sees the
|
||||||
|
/// badge.
|
||||||
|
pub const SubscriberHooks = struct {
|
||||||
|
/// Ask the kernel for published exit events on this service's endpoint.
|
||||||
|
watch: *const fn (endpoint: ipc.Handle) void,
|
||||||
|
/// Drop everything task `dead` had subscribed.
|
||||||
|
forget: *const fn (dead: u32) void,
|
||||||
|
};
|
||||||
|
|
||||||
pub const Callbacks = struct {
|
pub const Callbacks = struct {
|
||||||
/// Called once with the service's endpoint before the loop starts — the
|
/// Called once with the service's endpoint before the loop starts — the
|
||||||
/// place to subscribe to exit events, bind IRQs, or announce readiness.
|
/// place to subscribe to exit events, bind IRQs, or announce readiness.
|
||||||
|
|
@ -60,8 +80,216 @@ pub const Callbacks = struct {
|
||||||
/// `init` runs, so the service is reachable the moment it serves. A refusal
|
/// `init` runs, so the service is reachable the moment it serves. A refusal
|
||||||
/// (not granted, or a live provider already holds the name) aborts startup.
|
/// (not granted, or a live provider already holds the name) aborts startup.
|
||||||
service: ?[]const u8 = null,
|
service: ?[]const u8 = null,
|
||||||
|
/// This provider's subscriber table — `Subscribers(Protocol, Context).hooks`
|
||||||
|
/// — for a protocol that declares `.events`. Naming it here is what buys the
|
||||||
|
/// exit-notification sweep: the loop subscribes to published deaths at
|
||||||
|
/// startup and releases a dead subscriber's slot (and the endpoint capability
|
||||||
|
/// in it) when one lands.
|
||||||
|
subscribers: ?SubscriberHooks = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// How many subscribers one provider fans out to. Bounded like every table in
|
||||||
|
/// this system; a subscribe past the end is refused with `-ENOSPC` rather than
|
||||||
|
/// silently forgetting an earlier one.
|
||||||
|
pub const subscriber_capacity = 8;
|
||||||
|
|
||||||
|
/// The interest mask that means "every event of this protocol" — what a
|
||||||
|
/// subscriber which named no class gets, and what a provider passes when the
|
||||||
|
/// event it is publishing belongs to no class.
|
||||||
|
pub const every_event: u32 = 0;
|
||||||
|
|
||||||
|
/// The subscriber side of a protocol, for a provider whose contract declares
|
||||||
|
/// `.events` (docs/os-development/protocol-namespace.md: *the harness owns the
|
||||||
|
/// machinery — the subscriber table, the dead-subscriber sweep, and the fan-out
|
||||||
|
/// loop*). Three services hand-rolled this, with three different ideas of when a
|
||||||
|
/// dead subscriber goes away — a poll of the process list on subscribe, a drop on
|
||||||
|
/// a failed send, and nothing at all. This is the one idiom.
|
||||||
|
///
|
||||||
|
/// ```zig
|
||||||
|
/// const Subscriptions = service.Subscribers(power_protocol.Protocol, void);
|
||||||
|
/// ...
|
||||||
|
/// fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize {
|
||||||
|
/// return Subscriptions.dispatch({}, handlers, message, sender, arrived, reply);
|
||||||
|
/// }
|
||||||
|
/// pub fn main() void {
|
||||||
|
/// service.run(power_protocol.message_maximum, .{
|
||||||
|
/// .service = "power",
|
||||||
|
/// .on_message = onMessage,
|
||||||
|
/// .subscribers = Subscriptions.hooks,
|
||||||
|
/// });
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// What the provider still writes is its own events — `publish(.power_button, 0,
|
||||||
|
/// .{})`. Everything else happens here: registering the caller's endpoint on the
|
||||||
|
/// reserved `subscribe` verb, taking that capability out of the turn, dropping it
|
||||||
|
/// on `unsubscribe` or on the subscriber's death, and framing one packet for the
|
||||||
|
/// whole fan-out.
|
||||||
|
///
|
||||||
|
/// The table is per instantiation (a container-level `var` inside the generic
|
||||||
|
/// type), so a process providing two contracts gets two tables and neither can
|
||||||
|
/// see the other's subscribers.
|
||||||
|
pub fn Subscribers(comptime Protocol: type, comptime Context: type) type {
|
||||||
|
return struct {
|
||||||
|
/// The generated dispatch this provider answers with.
|
||||||
|
pub const Provider = Protocol.Provider(Context);
|
||||||
|
pub const Handlers = Provider.Handlers;
|
||||||
|
|
||||||
|
/// One registered subscriber: the endpoint events are pushed to (the
|
||||||
|
/// capability it handed over at subscribe time, which this slot owns),
|
||||||
|
/// the task that handed it over — the kernel-stamped badge, the only
|
||||||
|
/// source identity there is — and which classes of event it asked for.
|
||||||
|
const Slot = struct {
|
||||||
|
used: bool = false,
|
||||||
|
endpoint: ipc.Handle = 0,
|
||||||
|
task: u32 = 0,
|
||||||
|
interest: u32 = every_event,
|
||||||
|
};
|
||||||
|
|
||||||
|
var slots: [subscriber_capacity]Slot = .{Slot{}} ** subscriber_capacity;
|
||||||
|
|
||||||
|
/// Set when a slot has taken the capability the turn carried, and read
|
||||||
|
/// back in `dispatch`, which is where the turn's `Arrival` lives. The
|
||||||
|
/// generated dispatch hands a handler the raw handle rather than the
|
||||||
|
/// `Arrival` — deliberately, since a handler has no business closing the
|
||||||
|
/// turn's property — so the *claim* has to travel back out this way. One
|
||||||
|
/// turn, one handler, one thread: there is nothing here to race.
|
||||||
|
var claimed = false;
|
||||||
|
|
||||||
|
/// What `Callbacks.subscribers` is given.
|
||||||
|
pub const hooks: SubscriberHooks = .{ .watch = watchExits, .forget = forget };
|
||||||
|
|
||||||
|
fn watchExits(endpoint: ipc.Handle) void {
|
||||||
|
// Published exits, not a poll of the process list: a service must
|
||||||
|
// never depend on clients cleaning up after themselves, and it must
|
||||||
|
// not have to walk the whole table on every subscribe to find out
|
||||||
|
// either (docs/process-lifecycle.md, "Who learns of a death").
|
||||||
|
_ = process.subscribeExits(endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Release everything task `dead` had subscribed. The slot owns the
|
||||||
|
/// endpoint capability, so reclaiming the slot closes it — otherwise a
|
||||||
|
/// process that subscribes and dies costs a handle-table slot that never
|
||||||
|
/// comes back.
|
||||||
|
pub fn forget(dead: u32) void {
|
||||||
|
for (&slots) |*slot| {
|
||||||
|
if (slot.used and slot.task == dead) {
|
||||||
|
_ = ipc.close(slot.endpoint);
|
||||||
|
slot.* = .{};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether `task` is a subscriber — the gate for an operation a provider
|
||||||
|
/// honours from its subscribers and nobody else. The power service's
|
||||||
|
/// shutdown is the one: the badge is kernel-stamped, so nothing in a
|
||||||
|
/// packet can claim to be the subscriber that already ran the stop
|
||||||
|
/// sequence.
|
||||||
|
pub fn has(task: u32) bool {
|
||||||
|
for (&slots) |*slot| {
|
||||||
|
if (slot.used and slot.task == task) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Answer one received packet, with the reserved `subscribe` and
|
||||||
|
/// `unsubscribe` verbs already wired — a provider that leaves those two
|
||||||
|
/// handlers null (every provider should) gets the harness's. The turn's
|
||||||
|
/// capability is peeked, never taken, unless a slot actually kept it.
|
||||||
|
pub fn dispatch(
|
||||||
|
context: Context,
|
||||||
|
handlers: Handlers,
|
||||||
|
packet: []const u8,
|
||||||
|
sender: u32,
|
||||||
|
arrived: *ipc.Arrival,
|
||||||
|
reply: []u8,
|
||||||
|
) usize {
|
||||||
|
var wired = handlers;
|
||||||
|
if (wired.subscribe == null) wired.subscribe = onSubscribe;
|
||||||
|
if (wired.unsubscribe == null) wired.unsubscribe = onUnsubscribe;
|
||||||
|
claimed = false;
|
||||||
|
const written = Provider.dispatch(context, wired, packet, sender, arrived.peek(), reply);
|
||||||
|
if (claimed) _ = arrived.take();
|
||||||
|
return written;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push one event to every subscriber.
|
||||||
|
pub fn publish(
|
||||||
|
comptime event: Protocol.Event,
|
||||||
|
target: u64,
|
||||||
|
payload: Protocol.PayloadOf(event),
|
||||||
|
) void {
|
||||||
|
publishClass(event, target, payload, every_event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push one event to the subscribers whose interest mask includes
|
||||||
|
/// `class` (a subscriber that named no class takes everything). The
|
||||||
|
/// packet is framed **once**, outside the loop, so every subscriber of a
|
||||||
|
/// class receives identical bytes; and delivery is `ipc.send`, which
|
||||||
|
/// never blocks, so one slow or dead subscriber can never stall the rest
|
||||||
|
/// — the whole reason broadcast is a provider pattern and not a kernel
|
||||||
|
/// primitive.
|
||||||
|
pub fn publishClass(
|
||||||
|
comptime event: Protocol.Event,
|
||||||
|
target: u64,
|
||||||
|
payload: Protocol.PayloadOf(event),
|
||||||
|
class: u32,
|
||||||
|
) void {
|
||||||
|
var packet: [envelope.post_maximum]u8 = undefined;
|
||||||
|
const framed = Protocol.encodeEvent(event, target, payload, &packet) orelse return;
|
||||||
|
for (&slots) |*slot| {
|
||||||
|
if (!slot.used) continue;
|
||||||
|
if (!wants(slot.*, class)) continue;
|
||||||
|
// The sweep is what normally reclaims a dead subscriber, promptly
|
||||||
|
// and with its capability closed. This is the backstop for a
|
||||||
|
// notification that never arrived: an endpoint's notify ring is
|
||||||
|
// bounded, so a burst of deaths can drop one, and a send to an
|
||||||
|
// endpoint whose owner is gone fails rather than blocking.
|
||||||
|
if (!ipc.send(slot.endpoint, framed)) {
|
||||||
|
_ = ipc.close(slot.endpoint);
|
||||||
|
slot.* = .{};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wants(slot: Slot, class: u32) bool {
|
||||||
|
if (class == every_event) return true; // the event belongs to no class
|
||||||
|
if (slot.interest == every_event) return true; // the subscriber named none
|
||||||
|
return slot.interest & class != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The reserved `subscribe` verb: register the caller's endpoint (the
|
||||||
|
/// call's capability) for the classes its tail names. A refusal simply
|
||||||
|
/// returns and the turn closes what arrived — the harness's ownership
|
||||||
|
/// rule (`ipc.Arrival`), which is why a subscribe storm against a full
|
||||||
|
/// table cannot spend the handle table.
|
||||||
|
fn onSubscribe(_: Context, invocation: envelope.Invocation(void), _: envelope.Answer(void)) isize {
|
||||||
|
const endpoint = invocation.capability orelse return -envelope.EPROTO; // no endpoint passed
|
||||||
|
const interest = envelope.decodeSubscribe(invocation.tail).interest;
|
||||||
|
for (&slots) |*slot| {
|
||||||
|
if (slot.used) continue;
|
||||||
|
// Appended, not replaced: one task may hold several subscriptions
|
||||||
|
// on different endpoints (a client taking keyboard and mouse as
|
||||||
|
// two streams), and each is its own conversation.
|
||||||
|
slot.* = .{ .used = true, .endpoint = endpoint, .task = invocation.sender, .interest = interest };
|
||||||
|
claimed = true; // the table holds it until that task dies
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return -envelope.ENOSPC; // table full
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The reserved `unsubscribe` verb: every subscription the calling task
|
||||||
|
/// holds here goes, which is exactly what its death would do. It names no
|
||||||
|
/// endpoint because the badge already names the only subscriber a caller
|
||||||
|
/// can speak for — its own.
|
||||||
|
fn onUnsubscribe(_: Context, invocation: envelope.Invocation(void), _: envelope.Answer(void)) isize {
|
||||||
|
if (!has(invocation.sender)) return -envelope.ENOENT;
|
||||||
|
forget(invocation.sender);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// Run the service: create the endpoint, bind it under the service's contract
|
/// Run the service: create the endpoint, bind it under the service's contract
|
||||||
/// name (if it has one), bind signals to it, call `init`, then serve until
|
/// name (if it has one), bind signals to it, call `init`, then serve until
|
||||||
/// `terminate` arrives — at which point the loop returns and main's return is
|
/// `terminate` arrives — at which point the loop returns and main's return is
|
||||||
|
|
@ -74,6 +302,9 @@ pub fn run(comptime maximum_message: usize, callbacks: Callbacks) void {
|
||||||
if (!channel.bindPatiently(name, endpoint)) return;
|
if (!channel.bindPatiently(name, endpoint)) return;
|
||||||
}
|
}
|
||||||
_ = process.bindSignals(endpoint);
|
_ = process.bindSignals(endpoint);
|
||||||
|
// Before `init`, so a subscriber that arrives the instant the name is bound
|
||||||
|
// is already covered by the sweep that will release it.
|
||||||
|
if (callbacks.subscribers) |subscribers| subscribers.watch(endpoint);
|
||||||
if (callbacks.init) |initialise| {
|
if (callbacks.init) |initialise| {
|
||||||
if (!initialise(endpoint)) return;
|
if (!initialise(endpoint)) return;
|
||||||
}
|
}
|
||||||
|
|
@ -105,6 +336,13 @@ pub fn run(comptime maximum_message: usize, callbacks: Callbacks) void {
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// A death sweeps the subscriber table first, then still reaches the
|
||||||
|
// service: a provider often has its own per-client state to release
|
||||||
|
// (open file handles, device tokens, layers) and the same badge is
|
||||||
|
// the notice for both.
|
||||||
|
if (got.isChildExit()) {
|
||||||
|
if (callbacks.subscribers) |subscribers| subscribers.forget(got.childProcessId());
|
||||||
|
}
|
||||||
if (callbacks.on_notification) |onNotification| onNotification(got.badge);
|
if (callbacks.on_notification) |onNotification| onNotification(got.badge);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,41 @@ pub const operation_subscribe: u32 = 2; // capability = the subscriber's endpoin
|
||||||
pub const operation_unsubscribe: u32 = 3;
|
pub const operation_unsubscribe: u32 = 3;
|
||||||
pub const first_protocol_operation: u32 = 16;
|
pub const first_protocol_operation: u32 = 16;
|
||||||
|
|
||||||
|
/// The optional body of a reserved `subscribe`: **which** of a provider's events
|
||||||
|
/// the subscriber wants, as a bit mask whose meaning the protocol defines (the
|
||||||
|
/// input service's device classes are the model). A reserved verb carries no
|
||||||
|
/// typed request, so this rides the packet's tail — and zero, which is also what
|
||||||
|
/// a subscribe that sent no body at all reads as, means *every* event.
|
||||||
|
///
|
||||||
|
/// The mask lives here rather than in each protocol because the subscriber
|
||||||
|
/// machinery is the service harness's (library/kernel/service.zig): the harness
|
||||||
|
/// records the number, the protocol decides what its bits mean, and neither has
|
||||||
|
/// to know the other.
|
||||||
|
pub const Subscription = extern struct { interest: u32 = 0 };
|
||||||
|
|
||||||
|
/// Frame a `subscribe` request. The subscriber's own endpoint travels as the
|
||||||
|
/// call's *capability*, never in the packet — that is what makes the reverse
|
||||||
|
/// path unforgeable.
|
||||||
|
pub fn encodeSubscribe(interest: u32, buffer: []u8) ?[]u8 {
|
||||||
|
const header = Header{ .operation = operation_subscribe };
|
||||||
|
const body = Subscription{ .interest = interest };
|
||||||
|
return frame(std.mem.asBytes(&header), std.mem.asBytes(&body), &.{}, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Frame a bare `unsubscribe`: it names no event and no endpoint, because it
|
||||||
|
/// means "every subscription this task holds here" (one task, one voice).
|
||||||
|
pub fn encodeUnsubscribe(buffer: []u8) ?[]u8 {
|
||||||
|
const header = Header{ .operation = operation_unsubscribe };
|
||||||
|
return frame(std.mem.asBytes(&header), &.{}, &.{}, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The interest mask out of a `subscribe` packet's tail, on the provider's side.
|
||||||
|
/// A caller that sent no mask reads as the every-event mask.
|
||||||
|
pub fn decodeSubscribe(tail: []const u8) Subscription {
|
||||||
|
if (tail.len < @sizeOf(Subscription)) return .{};
|
||||||
|
return std.mem.bytesToValue(Subscription, tail[0..@sizeOf(Subscription)]);
|
||||||
|
}
|
||||||
|
|
||||||
/// The `describe` reply's fixed part, followed inline by `name_len` bytes of the
|
/// The `describe` reply's fixed part, followed inline by `name_len` bytes of the
|
||||||
/// protocol's name. This is the version handshake: the version is asked for
|
/// protocol's name. This is the version handshake: the version is asked for
|
||||||
/// once, at connect time, rather than re-carried by every packet out of a
|
/// once, at connect time, rather than re-carried by every packet out of a
|
||||||
|
|
@ -876,6 +911,21 @@ test "a truncated packet answers -EPROTO" {
|
||||||
// operation with `.request = extern struct { bytes: [241]u8 }`, or an event with
|
// operation with `.request = extern struct { bytes: [241]u8 }`, or an event with
|
||||||
// `.payload = extern struct { bytes: [49]u8 }`, fails to compile with the
|
// `.payload = extern struct { bytes: [49]u8 }`, fails to compile with the
|
||||||
// protocol, the verb, and the two numbers named in the message.
|
// protocol, the verb, and the two numbers named in the message.
|
||||||
|
test "a subscribe carries its interest mask in the reserved verb's tail" {
|
||||||
|
var buffer: [packet_maximum]u8 = undefined;
|
||||||
|
const packet = encodeSubscribe(0b101, &buffer).?;
|
||||||
|
try testing.expectEqual(operation_subscribe, headerOf(packet).?.operation);
|
||||||
|
try testing.expectEqual(@as(u32, 0b101), decodeSubscribe(packet[prefix_size..]).interest);
|
||||||
|
// No body at all — and a body too short to be one — read as "every event",
|
||||||
|
// which is what a subscriber that named nothing wants.
|
||||||
|
try testing.expectEqual(@as(u32, 0), decodeSubscribe(&.{}).interest);
|
||||||
|
try testing.expectEqual(@as(u32, 0), decodeSubscribe(&.{ 1, 2 }).interest);
|
||||||
|
|
||||||
|
const bare = encodeUnsubscribe(&buffer).?;
|
||||||
|
try testing.expectEqual(operation_unsubscribe, headerOf(bare).?.operation);
|
||||||
|
try testing.expectEqual(prefix_size, bare.len);
|
||||||
|
}
|
||||||
|
|
||||||
test "the floor counts the header once, and the boundary is exact" {
|
test "the floor counts the header once, and the boundary is exact" {
|
||||||
try testing.expect(fitsPacket(extern struct { bytes: [240]u8 }));
|
try testing.expect(fitsPacket(extern struct { bytes: [240]u8 }));
|
||||||
try testing.expect(!fitsPacket(extern struct { bytes: [241]u8 }));
|
try testing.expect(!fitsPacket(extern struct { bytes: [241]u8 }));
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@
|
||||||
//! - **subscribe** is the *reserved* verb, not one of this protocol's own: its shape — a
|
//! - **subscribe** is the *reserved* verb, not one of this protocol's own: its shape — a
|
||||||
//! synchronous call whose attached capability is the subscriber's endpoint — is exactly
|
//! synchronous call whose attached capability is the subscriber's endpoint — is exactly
|
||||||
//! what `envelope.operation_subscribe` means everywhere. The interest mask travels as the
|
//! what `envelope.operation_subscribe` means everywhere. The interest mask travels as the
|
||||||
//! packet's tail (`Subscribe`), because a reserved verb carries no typed request.
|
//! packet's tail (`envelope.Subscription`), because a reserved verb carries no typed
|
||||||
|
//! request; what this protocol supplies is the *meaning* of its bits — the device classes.
|
||||||
//! - **publish** is this protocol's one verb: a source sends one `InputEvent` and the
|
//! - **publish** is this protocol's one verb: a source sends one `InputEvent` and the
|
||||||
//! service answers at once, so publishing never blocks on a slow subscriber.
|
//! service answers at once, so publishing never blocks on a slow subscriber.
|
||||||
//! - **delivery** is an event push: the service `ipc_send`s each event to every interested
|
//! - **delivery** is an event push: the service `ipc_send`s each event to every interested
|
||||||
|
|
@ -294,12 +295,6 @@ pub const InputEvent = extern struct {
|
||||||
|
|
||||||
// --- the contract -----------------------------------------------------------
|
// --- the contract -----------------------------------------------------------
|
||||||
|
|
||||||
/// The body of a `subscribe` — the envelope's reserved verb 2, whose shape (a call whose
|
|
||||||
/// capability is the subscriber's own endpoint) this protocol adopts wholesale. A reserved
|
|
||||||
/// verb has no typed request, so the mask travels as the packet's tail and `encodeSubscribe`
|
|
||||||
/// is how a client lays it down. Zero means every class.
|
|
||||||
pub const Subscribe = extern struct { device_mask: u32 = 0 };
|
|
||||||
|
|
||||||
pub const Protocol = envelope.Define(.{
|
pub const Protocol = envelope.Define(.{
|
||||||
.name = "input",
|
.name = "input",
|
||||||
.version = 1,
|
.version = 1,
|
||||||
|
|
@ -334,23 +329,12 @@ pub fn eventOfDevice(device: u32) ?Event {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Frame a `subscribe` request: the reserved verb's header, then the interest mask. Null if
|
/// Frame a `subscribe` request: the reserved verb's header, then the interest mask. Null if
|
||||||
/// the buffer is too small. Spelled here rather than at each caller so the one place that
|
/// the buffer is too small. The mask itself is the envelope's `Subscription` — the interest
|
||||||
/// knows a reserved verb carries its body in the tail is the protocol module.
|
/// a reserved subscribe carries is universal, and the *meaning* of its bits (here: the
|
||||||
|
/// device classes above) is what each protocol supplies. Kept as a named helper because
|
||||||
|
/// `device_mask` is what an input caller calls it.
|
||||||
pub fn encodeSubscribe(device_mask: u32, buffer: []u8) ?[]u8 {
|
pub fn encodeSubscribe(device_mask: u32, buffer: []u8) ?[]u8 {
|
||||||
const total = envelope.prefix_size + @sizeOf(Subscribe);
|
return envelope.encodeSubscribe(device_mask, buffer);
|
||||||
if (buffer.len < total) return null;
|
|
||||||
const header = envelope.Header{ .operation = envelope.operation_subscribe };
|
|
||||||
const body = Subscribe{ .device_mask = device_mask };
|
|
||||||
@memcpy(buffer[0..envelope.prefix_size], std.mem.asBytes(&header));
|
|
||||||
@memcpy(buffer[envelope.prefix_size..][0..@sizeOf(Subscribe)], std.mem.asBytes(&body));
|
|
||||||
return buffer[0..total];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The interest mask out of a `subscribe` packet's tail, on the provider's side. A caller
|
|
||||||
/// that sent no mask at all means every class, which is what a zero mask means anyway.
|
|
||||||
pub fn decodeSubscribe(tail: []const u8) Subscribe {
|
|
||||||
if (tail.len < @sizeOf(Subscribe)) return .{};
|
|
||||||
return std.mem.bytesToValue(Subscribe, tail[0..@sizeOf(Subscribe)]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test "an event of every class fits the push floor, header included" {
|
test "an event of every class fits the push floor, header included" {
|
||||||
|
|
@ -388,7 +372,9 @@ test "a subscribe carries its mask in the tail of the reserved verb" {
|
||||||
var buffer: [envelope.packet_maximum]u8 = undefined;
|
var buffer: [envelope.packet_maximum]u8 = undefined;
|
||||||
const packet = encodeSubscribe(device_mouse, &buffer).?;
|
const packet = encodeSubscribe(device_mouse, &buffer).?;
|
||||||
try std.testing.expectEqual(envelope.operation_subscribe, envelope.headerOf(packet).?.operation);
|
try std.testing.expectEqual(envelope.operation_subscribe, envelope.headerOf(packet).?.operation);
|
||||||
try std.testing.expectEqual(device_mouse, decodeSubscribe(packet[envelope.prefix_size..]).device_mask);
|
// The provider side of this is the service harness's, which reads the same
|
||||||
|
// interest mask out of the tail for every protocol.
|
||||||
|
try std.testing.expectEqual(device_mouse, envelope.decodeSubscribe(packet[envelope.prefix_size..]).interest);
|
||||||
// A caller that sent nothing at all reads as the every-class mask.
|
// A caller that sent nothing at all reads as the every-class mask.
|
||||||
try std.testing.expectEqual(@as(u32, 0), decodeSubscribe(&.{}).device_mask);
|
try std.testing.expectEqual(@as(u32, 0), envelope.decodeSubscribe(&.{}).interest);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,16 @@
|
||||||
/test/system/services/input-source, kernel, open, input
|
/test/system/services/input-source, kernel, open, input
|
||||||
/test/system/services/input-test, kernel, open, input
|
/test/system/services/input-test, kernel, open, input
|
||||||
|
|
||||||
|
# The guessable-id probe (test/system/services/badge-scope-test) runs as two
|
||||||
|
# processes of one binary: the owner, which the scenario spawns, and the intruder,
|
||||||
|
# which the owner spawns with the ids it holds. Both reach the compositor — the
|
||||||
|
# owner to create the layer, the intruder to be refused it — so the binary is
|
||||||
|
# named twice, once per supervisor. The second row needs no 'supervise'
|
||||||
|
# delegation: the owner was spawned by the KERNEL, which is a chain init can
|
||||||
|
# vouch for on its own.
|
||||||
|
/test/system/services/badge-scope-test, kernel, open, display
|
||||||
|
/test/system/services/badge-scope-test, /test/*, open, display
|
||||||
|
|
||||||
# The conformance probe (test/system/services/protocol-conformance-test) asks
|
# The conformance probe (test/system/services/protocol-conformance-test) asks
|
||||||
# every provider its boot bound for the envelope's reserved verbs. It reaches
|
# every provider its boot bound for the envelope's reserved verbs. It reaches
|
||||||
# ONLY the two contracts its own scenario boots a provider for, named one at a
|
# ONLY the two contracts its own scenario boots a provider for, named one at a
|
||||||
|
|
|
||||||
|
Can't render this file because it contains an unexpected character in line 12 and column 15.
|
|
|
@ -61,22 +61,33 @@ fn timerInterval() u64 {
|
||||||
return if (msi_vector != null) reconcile_interval_ms else poll_interval_ms;
|
return if (msi_vector != null) reconcile_interval_ms else poll_interval_ms;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The class driver endpoints that opened each device, so interrupt reports can
|
/// The class driver that opened each device: the endpoint interrupt reports are
|
||||||
/// be pushed back to them. Keyed by the device token (the interface's device id).
|
/// pushed back to, and **the task that opened it** — the kernel-stamped badge, so
|
||||||
|
/// a device token is scoped to the client that was given it. Keyed by the device
|
||||||
|
/// token (the interface's device id).
|
||||||
|
///
|
||||||
|
/// The scoping is the point (docs/os-development/protocol-namespace.md: handles
|
||||||
|
/// are validated against the badge). A token is a small registered-device id any
|
||||||
|
/// process could name, and every packet that carries one used to be honoured from
|
||||||
|
/// anyone: a stranger could run control transfers on another driver's device, arm
|
||||||
|
/// interrupt polling on it, or redirect its reports.
|
||||||
const Open = struct {
|
const Open = struct {
|
||||||
used: bool = false,
|
used: bool = false,
|
||||||
device_token: u64 = 0,
|
device_token: u64 = 0,
|
||||||
|
owner: u32 = 0,
|
||||||
report_endpoint: usize = 0,
|
report_endpoint: usize = 0,
|
||||||
};
|
};
|
||||||
var opens = [_]Open{.{}} ** 16;
|
var opens = [_]Open{.{}} ** 16;
|
||||||
|
|
||||||
/// Remember (or replace) the endpoint that reports for `device_token`. Returns
|
/// Remember (or replace) the endpoint task `owner` receives reports for
|
||||||
/// whether the table kept the handle — false means the caller still owns it and
|
/// `device_token` on. Returns whether the table kept the handle — false means the
|
||||||
/// must dispose of it. A re-open supersedes the previous endpoint, and the one
|
/// caller still owns it and must dispose of it. A re-open by the **same** client
|
||||||
/// it displaced is closed here: the table holds exactly one reference per slot.
|
/// supersedes its previous endpoint, and the one it displaced is closed here: the
|
||||||
fn recordOpen(device_token: u64, report_endpoint: usize) bool {
|
/// table holds exactly one reference per slot.
|
||||||
|
fn recordOpen(device_token: u64, owner: u32, report_endpoint: usize) bool {
|
||||||
for (&opens) |*open| {
|
for (&opens) |*open| {
|
||||||
if (open.used and open.device_token == device_token) {
|
if (open.used and open.device_token == device_token) {
|
||||||
|
if (open.owner != owner) return false; // someone else's device; nothing kept
|
||||||
if (open.report_endpoint != report_endpoint) _ = ipc.close(open.report_endpoint);
|
if (open.report_endpoint != report_endpoint) _ = ipc.close(open.report_endpoint);
|
||||||
open.report_endpoint = report_endpoint;
|
open.report_endpoint = report_endpoint;
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -84,13 +95,33 @@ fn recordOpen(device_token: u64, report_endpoint: usize) bool {
|
||||||
}
|
}
|
||||||
for (&opens) |*open| {
|
for (&opens) |*open| {
|
||||||
if (!open.used) {
|
if (!open.used) {
|
||||||
open.* = .{ .used = true, .device_token = device_token, .report_endpoint = report_endpoint };
|
open.* = .{ .used = true, .device_token = device_token, .owner = owner, .report_endpoint = report_endpoint };
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false; // table full: not kept
|
return false; // table full: not kept
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether `device_token` is open to task `owner`. An open device belonging to
|
||||||
|
/// someone else answers exactly as one that was never opened, so a prober cannot
|
||||||
|
/// tell another driver's device from an absent one.
|
||||||
|
fn openedBy(device_token: u64, owner: u32) bool {
|
||||||
|
for (&opens) |*open| {
|
||||||
|
if (open.used and open.device_token == device_token) return open.owner == owner;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether `device_token` is open at all. Asked only after `openedBy` has said
|
||||||
|
/// the caller is not the holder, so an answer of true means *someone else* holds
|
||||||
|
/// it — one device, one class driver.
|
||||||
|
fn heldByAnother(device_token: u64) bool {
|
||||||
|
for (&opens) |*open| {
|
||||||
|
if (open.used and open.device_token == device_token) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
fn reportEndpointFor(device_token: u64) ?usize {
|
fn reportEndpointFor(device_token: u64) ?usize {
|
||||||
for (&opens) |*open| {
|
for (&opens) |*open| {
|
||||||
if (open.used and open.device_token == device_token) return open.report_endpoint;
|
if (open.used and open.device_token == device_token) return open.report_endpoint;
|
||||||
|
|
@ -98,6 +129,23 @@ fn reportEndpointFor(device_token: u64) ?usize {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Release every device a dead client held: its slot, and the report endpoint
|
||||||
|
/// capability in it. Driven by published process exits — the same sweep idiom the
|
||||||
|
/// FAT server uses for open files and the harness uses for subscribers — which is
|
||||||
|
/// also what lets a restarted class driver re-open the device its predecessor had.
|
||||||
|
fn releaseOpensOf(dead: u32) void {
|
||||||
|
for (&opens) |*open| {
|
||||||
|
if (open.used and open.owner == dead) {
|
||||||
|
// The engine first: it holds this endpoint's handle number per
|
||||||
|
// subscription, and the close below frees that number for reuse.
|
||||||
|
if (controller) |*engine| engine.releaseSubscriptions(open.device_token);
|
||||||
|
_ = ipc.close(open.report_endpoint);
|
||||||
|
std.log.info("released device {d} for dead client {d}", .{ open.device_token, dead });
|
||||||
|
open.* = .{};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var controller_id: u64 = device_manager_protocol.no_device;
|
var controller_id: u64 = device_manager_protocol.no_device;
|
||||||
|
|
||||||
/// Claim the assigned controller, find its register window, and hello the
|
/// Claim the assigned controller, find its register window, and hello the
|
||||||
|
|
@ -105,6 +153,10 @@ var controller_id: u64 = device_manager_protocol.no_device;
|
||||||
/// manager reads as "meant to stop" — a missing assignment is not a crash loop.
|
/// manager reads as "meant to stop" — a missing assignment is not a crash loop.
|
||||||
fn initialise(endpoint: ipc.Handle) bool {
|
fn initialise(endpoint: ipc.Handle) bool {
|
||||||
service_endpoint = endpoint;
|
service_endpoint = endpoint;
|
||||||
|
// Device tokens are per-client state, so this driver needs deaths for the
|
||||||
|
// same reason the FAT server does: a class driver that crashes must not keep
|
||||||
|
// its device open, or its restarted instance could never claim it back.
|
||||||
|
_ = process.subscribeExits(endpoint);
|
||||||
|
|
||||||
// The transfer contract, bound by hand rather than through the harness's
|
// The transfer contract, bound by hand rather than through the harness's
|
||||||
// `.service`, because **losing it is not fatal here**. One machine can carry
|
// `.service`, because **losing it is not fatal here**. One machine can carry
|
||||||
|
|
@ -543,11 +595,16 @@ const handlers = Serve.Handlers{
|
||||||
fn onOpen(_: void, invocation: Invocation(void), answer: Answer(usb_transfer_protocol.Opened)) isize {
|
fn onOpen(_: void, invocation: Invocation(void), answer: Answer(usb_transfer_protocol.Opened)) isize {
|
||||||
const engine = if (controller) |*c| c else return refused;
|
const engine = if (controller) |*c| c else return refused;
|
||||||
const found = engine.findInterface(invocation.target) orelse return refused;
|
const found = engine.findInterface(invocation.target) orelse return refused;
|
||||||
|
// A device another live client holds is refused exactly as an absent one: one
|
||||||
|
// device, one class driver. The predecessor's slot is released by the exit
|
||||||
|
// sweep, and notifications are delivered ahead of requests, so a *restarted*
|
||||||
|
// driver's open always finds the device free.
|
||||||
|
if (!openedBy(invocation.target, invocation.sender) and heldByAnother(invocation.target)) return refused;
|
||||||
|
|
||||||
// The report endpoint is claimed only if the open table actually keeps it;
|
// The report endpoint is claimed only if the open table actually keeps it;
|
||||||
// a full table leaves it to the turn to close.
|
// a full table leaves it to the turn to close.
|
||||||
if (invocation.capability) |endpoint| {
|
if (invocation.capability) |endpoint| {
|
||||||
if (recordOpen(invocation.target, endpoint)) capability_claimed = true;
|
if (recordOpen(invocation.target, invocation.sender, endpoint)) capability_claimed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var opened = usb_transfer_protocol.Opened{
|
var opened = usb_transfer_protocol.Opened{
|
||||||
|
|
@ -576,6 +633,7 @@ fn onOpen(_: void, invocation: Invocation(void), answer: Answer(usb_transfer_pro
|
||||||
/// written into `answer.tail()` — which is what `Status.len` then reports.
|
/// written into `answer.tail()` — which is what `Status.len` then reports.
|
||||||
fn onControl(_: void, invocation: Invocation(usb_transfer_protocol.Control), answer: Answer(void)) isize {
|
fn onControl(_: void, invocation: Invocation(usb_transfer_protocol.Control), answer: Answer(void)) isize {
|
||||||
const engine = if (controller) |*c| c else return refused;
|
const engine = if (controller) |*c| c else return refused;
|
||||||
|
if (!openedBy(invocation.target, invocation.sender)) return refused;
|
||||||
const found = engine.findInterface(invocation.target) orelse return refused;
|
const found = engine.findInterface(invocation.target) orelse return refused;
|
||||||
|
|
||||||
const setup = std.mem.bytesToValue(usb_abi.Request, &invocation.request.setup);
|
const setup = std.mem.bytesToValue(usb_abi.Request, &invocation.request.setup);
|
||||||
|
|
@ -600,6 +658,7 @@ fn onControl(_: void, invocation: Invocation(usb_transfer_protocol.Control), ans
|
||||||
/// to the endpoint this device's `open` handed over.
|
/// to the endpoint this device's `open` handed over.
|
||||||
fn onInterruptSubscribe(_: void, invocation: Invocation(usb_transfer_protocol.InterruptSubscribe), _: Answer(void)) isize {
|
fn onInterruptSubscribe(_: void, invocation: Invocation(usb_transfer_protocol.InterruptSubscribe), _: Answer(void)) isize {
|
||||||
const engine = if (controller) |*c| c else return refused;
|
const engine = if (controller) |*c| c else return refused;
|
||||||
|
if (!openedBy(invocation.target, invocation.sender)) return refused;
|
||||||
const found = engine.findInterface(invocation.target) orelse return refused;
|
const found = engine.findInterface(invocation.target) orelse return refused;
|
||||||
const endpoint = library.Controller.endpointForAddress(found.interface, invocation.request.endpoint_address) orelse return refused;
|
const endpoint = library.Controller.endpointForAddress(found.interface, invocation.request.endpoint_address) orelse return refused;
|
||||||
const report_endpoint = reportEndpointFor(invocation.target) orelse return refused;
|
const report_endpoint = reportEndpointFor(invocation.target) orelse return refused;
|
||||||
|
|
@ -610,6 +669,7 @@ fn onInterruptSubscribe(_: void, invocation: Invocation(usb_transfer_protocol.In
|
||||||
/// address), so sector-sized data never crosses IPC.
|
/// address), so sector-sized data never crosses IPC.
|
||||||
fn onBulk(_: void, invocation: Invocation(usb_transfer_protocol.Bulk), answer: Answer(usb_transfer_protocol.Transferred)) isize {
|
fn onBulk(_: void, invocation: Invocation(usb_transfer_protocol.Bulk), answer: Answer(usb_transfer_protocol.Transferred)) isize {
|
||||||
const engine = if (controller) |*c| c else return refused;
|
const engine = if (controller) |*c| c else return refused;
|
||||||
|
if (!openedBy(invocation.target, invocation.sender)) return refused;
|
||||||
const found = engine.findInterface(invocation.target) orelse return refused;
|
const found = engine.findInterface(invocation.target) orelse return refused;
|
||||||
const endpoint = library.Controller.endpointForAddress(found.interface, invocation.request.endpoint_address) orelse return refused;
|
const endpoint = library.Controller.endpointForAddress(found.interface, invocation.request.endpoint_address) orelse return refused;
|
||||||
const transferred = engine.bulkTransfer(found.device, endpoint, invocation.request.physical_address, invocation.request.length) orelse return refused;
|
const transferred = engine.bulkTransfer(found.device, endpoint, invocation.request.physical_address, invocation.request.length) orelse return refused;
|
||||||
|
|
@ -633,6 +693,12 @@ fn onDmaAttach(_: void, invocation: Invocation(void), _: Answer(void)) isize {
|
||||||
/// arriving after the drain takes IP 0→1 and fires a fresh edge instead of being
|
/// arriving after the drain takes IP 0→1 and fires a fresh edge instead of being
|
||||||
/// swallowed until the reconcile tick.
|
/// swallowed until the reconcile tick.
|
||||||
fn onNotification(badge: u64) void {
|
fn onNotification(badge: u64) void {
|
||||||
|
if (badge & ipc.notify_exit_bit != 0) {
|
||||||
|
// A class driver died: release the devices it held, so its successor can
|
||||||
|
// open them and no report is aimed at an endpoint that is gone.
|
||||||
|
releaseOpensOf(@intCast(badge & ~(ipc.notify_badge_bit | ipc.notify_exit_bit)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (badge & ipc.notify_timer_bit != 0) {
|
if (badge & ipc.notify_timer_bit != 0) {
|
||||||
serviceController();
|
serviceController();
|
||||||
_ = time.timerOnce(service_endpoint, timerInterval());
|
_ = time.timerOnce(service_endpoint, timerInterval());
|
||||||
|
|
|
||||||
|
|
@ -1498,6 +1498,24 @@ pub const Controller = struct {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stop reporting for `device_token`: deactivate every subscription tagged
|
||||||
|
/// with it, so no further report is queued and the slot can be reused. Called
|
||||||
|
/// when the class driver that owned the token dies — its endpoint handle is
|
||||||
|
/// closed with it, and a queued report would then be aimed at a handle number
|
||||||
|
/// the bus driver has since given to something else. In-process hub
|
||||||
|
/// subscriptions carry no token and are never touched.
|
||||||
|
///
|
||||||
|
/// One completion may already be in flight; it finds no active subscription
|
||||||
|
/// and is dropped as a foreign transfer event, which is exactly what it is.
|
||||||
|
pub fn releaseSubscriptions(self: *Controller, device_token: u64) void {
|
||||||
|
for (&self.subscriptions) |*subscription| {
|
||||||
|
if (!subscription.active or subscription.hub != null) continue;
|
||||||
|
if (subscription.device_token != device_token) continue;
|
||||||
|
subscription.active = false;
|
||||||
|
subscription.report_endpoint = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Arm (or re-arm) a subscription's endpoint with a Normal TRB pointing at its
|
// Arm (or re-arm) a subscription's endpoint with a Normal TRB pointing at its
|
||||||
// report buffer, and ring the endpoint's doorbell so the controller polls it.
|
// report buffer, and ring the endpoint's doorbell so the controller polls it.
|
||||||
fn armInterrupt(self: *Controller, subscription: *Subscription) void {
|
fn armInterrupt(self: *Controller, subscription: *Subscription) void {
|
||||||
|
|
|
||||||
|
|
@ -1520,7 +1520,15 @@ pub fn exitReasonOf(caller_id: u32, target_id: u32) i64 {
|
||||||
/// subscriptions — that must release what a dead client held and cannot learn it
|
/// subscriptions — that must release what a dead client held and cannot learn it
|
||||||
/// any other way (a client that simply never calls again looks like silence).
|
/// any other way (a client that simply never calls again looks like silence).
|
||||||
/// Bounded like every kernel table; each entry holds its own endpoint reference.
|
/// Bounded like every kernel table; each entry holds its own endpoint reference.
|
||||||
const exit_subscriber_capacity = 8;
|
///
|
||||||
|
/// Sixteen, not eight: a subscription is now what *every* provider with
|
||||||
|
/// per-client state uses to release it — the FAT server's open files, the input,
|
||||||
|
/// power and device-manager subscriber tables (the service harness subscribes for
|
||||||
|
/// them), the compositor's layers, and each USB controller driver's device tokens.
|
||||||
|
/// A single boot already fields six, and a machine with several xHCI controllers
|
||||||
|
/// fields one per controller, so the old ceiling was within two of a service
|
||||||
|
/// silently losing its sweep.
|
||||||
|
const exit_subscriber_capacity = 16;
|
||||||
const ExitSubscriber = struct { endpoint: *ipc.Endpoint, owner: u32 };
|
const ExitSubscriber = struct { endpoint: *ipc.Endpoint, owner: u32 };
|
||||||
var exit_subscribers: [exit_subscriber_capacity]?ExitSubscriber = .{null} ** exit_subscriber_capacity;
|
var exit_subscribers: [exit_subscriber_capacity]?ExitSubscriber = .{null} ** exit_subscriber_capacity;
|
||||||
|
|
||||||
|
|
@ -1538,16 +1546,27 @@ fn systemProcessSubscribe(state: *architecture.CpuState) void {
|
||||||
if (t.address_space == 0) return fail(state);
|
if (t.address_space == 0) return fail(state);
|
||||||
const endpoint = ipc.resolveHandle(t, architecture.systemCallArg(state, 0)) orelse return failErr(state, ipc.EBADF);
|
const endpoint = ipc.resolveHandle(t, architecture.systemCallArg(state, 0)) orelse return failErr(state, ipc.EBADF);
|
||||||
if (!ipc.ownedBy(endpoint, t)) return failErr(state, ipc.EPERM);
|
if (!ipc.ownedBy(endpoint, t)) return failErr(state, ipc.EPERM);
|
||||||
|
const result = subscribeExits(endpoint, t.id);
|
||||||
|
if (result < 0) return failErr(state, @intCast(-result));
|
||||||
|
architecture.setSystemCallResult(state, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Take a slot in the published-exit table for `endpoint`, owned by task `owner`.
|
||||||
|
/// The body of `process_subscribe` minus the authorization, so a kernel test can
|
||||||
|
/// exercise the fan-out (several subscribers, one death, every one notified) that
|
||||||
|
/// every provider's release-what-the-dead-client-held sweep is built on. Returns
|
||||||
|
/// 0, or -ENOSPC.
|
||||||
|
pub fn subscribeExits(endpoint: *ipc.Endpoint, owner: u32) i64 {
|
||||||
const flags = sync.enter();
|
const flags = sync.enter();
|
||||||
defer sync.leave(flags);
|
defer sync.leave(flags);
|
||||||
for (&exit_subscribers) |*slot| {
|
for (&exit_subscribers) |*slot| {
|
||||||
if (slot.* == null) {
|
if (slot.* == null) {
|
||||||
endpoint.refcount += 1; // the slot's own reference, dropped on unsubscribe-by-death
|
endpoint.refcount += 1; // the slot's own reference, dropped on unsubscribe-by-death
|
||||||
slot.* = .{ .endpoint = endpoint, .owner = t.id };
|
slot.* = .{ .endpoint = endpoint, .owner = owner };
|
||||||
return architecture.setSystemCallResult(state, 0);
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
failErr(state, ipc.ENOSPC);
|
return -ipc.ENOSPC;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// signal_bind(endpoint): nominate where this process's signals arrive — the
|
/// signal_bind(endpoint): nominate where this process's signals arrive — the
|
||||||
|
|
|
||||||
|
|
@ -2248,9 +2248,67 @@ fn processKillTest(boot_information: *const BootInformation) void {
|
||||||
}
|
}
|
||||||
check("neither victim is listed after its kill", !still_listed);
|
check("neither victim is listed after its kill", !still_listed);
|
||||||
check("a killed id stays dead (-ESRCH on a second kill)", process.killProcess(me, sleeper) == -ipcsync.ESRCH);
|
check("a killed id stays dead (-ESRCH on a second kill)", process.killProcess(me, sleeper) == -ipcsync.ESRCH);
|
||||||
|
|
||||||
|
publishedExitChecks(rd, me);
|
||||||
result();
|
result();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The mechanism every provider's release-what-a-dead-client-held sweep is built
|
||||||
|
/// on (docs/process-lifecycle.md, "Who learns of a death"): a **published** exit,
|
||||||
|
/// fanned out to every subscriber rather than only to the supervisor. The service
|
||||||
|
/// harness's subscriber sweep, the FAT server's open files, the compositor's
|
||||||
|
/// layers and the xHCI driver's device tokens all release on exactly this, and
|
||||||
|
/// several of them are subscribed at once in a normal boot — so what is checked
|
||||||
|
/// here is the fan-out: three independent subscribers, one death, three
|
||||||
|
/// notifications carrying the same badge, none of them the supervisor's.
|
||||||
|
///
|
||||||
|
/// Run at the end of the process-kill case, because a subscription is for every
|
||||||
|
/// death from then on and the checks above spawn victims of their own.
|
||||||
|
fn publishedExitChecks(rd: initial_ramdisk.Reader, me: u32) void {
|
||||||
|
var subscribers: [3]*ipcsync.Endpoint = undefined;
|
||||||
|
var subscribed: usize = 0;
|
||||||
|
while (subscribed < subscribers.len) : (subscribed += 1) {
|
||||||
|
subscribers[subscribed] = ipcsync.createIpcEndpoint() orelse break;
|
||||||
|
if (process.subscribeExits(subscribers[subscribed], me) != 0) break;
|
||||||
|
}
|
||||||
|
check("three endpoints subscribed to published exits", subscribed == subscribers.len);
|
||||||
|
if (subscribed != subscribers.len) return;
|
||||||
|
|
||||||
|
// A supervised child so the supervisor notification remains distinguishable:
|
||||||
|
// it lands on `endpoint`, the published ones on the three above.
|
||||||
|
const supervisor_endpoint = ipcsync.createIpcEndpoint() orelse {
|
||||||
|
check("supervisor endpoint allocated", false);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
var child: u32 = 0;
|
||||||
|
var i: u32 = 0;
|
||||||
|
while (i < rd.count) : (i += 1) {
|
||||||
|
const item = rd.entry(i) orelse continue;
|
||||||
|
if (!eql(initial_ramdisk.basename(item.name), "args-echo")) continue;
|
||||||
|
child = process.spawnProcessSupervised(item.blob, 4, &.{ "args-echo", "published-exit" }, me, supervisor_endpoint) catch 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
check("a clean-exit child spawned for the published exit", child != 0);
|
||||||
|
if (child == 0) return;
|
||||||
|
|
||||||
|
var badge: u64 = 0;
|
||||||
|
var received_cap: u64 = 0;
|
||||||
|
_ = ipcsync.replyWait(supervisor_endpoint, 0, 0, 0, 0, abi.no_cap, &badge, &received_cap);
|
||||||
|
check("the supervisor heard the child end", badge == abi.notify_badge_bit | abi.notify_exit_bit | child);
|
||||||
|
|
||||||
|
// The publication happens in the same locked section as the supervisor's
|
||||||
|
// notification and before it, so all three are already queued: a subscriber
|
||||||
|
// that had not heard would block here and time the harness out rather than
|
||||||
|
// pass vacuously.
|
||||||
|
var heard: usize = 0;
|
||||||
|
for (subscribers) |subscriber| {
|
||||||
|
badge = 0;
|
||||||
|
_ = ipcsync.replyWait(subscriber, 0, 0, 0, 0, abi.no_cap, &badge, &received_cap);
|
||||||
|
if (badge == abi.notify_badge_bit | abi.notify_exit_bit | child) heard += 1;
|
||||||
|
}
|
||||||
|
check("every subscriber heard the same death, not just the supervisor", heard == subscribers.len);
|
||||||
|
}
|
||||||
|
|
||||||
/// M17.1: a dead process's device claims are released by the reap, so a restarted
|
/// M17.1: a dead process's device claims are released by the reap, so a restarted
|
||||||
/// driver can claim its hardware again (docs/process-lifecycle.md iron rule 1).
|
/// driver can claim its hardware again (docs/process-lifecycle.md iron rule 1).
|
||||||
/// First the broker release in isolation — two owners, one released, the other's
|
/// First the broker release in isolation — two owners, one released, the other's
|
||||||
|
|
@ -2739,6 +2797,10 @@ fn fatMountTest(boot_information: *const BootInformation) void {
|
||||||
const init_ok = if (process.spawnBundled("/system/services/init")) true else |_| false;
|
const init_ok = if (process.spawnBundled("/system/services/init")) true else |_| false;
|
||||||
check("init spawned (boots the tree, incl. the fat server)", init_ok);
|
check("init spawned (boots the tree, incl. the fat server)", init_ok);
|
||||||
check("fat-test client spawned", spawnNamed(rd, "fat-test"));
|
check("fat-test client spawned", spawnNamed(rd, "fat-test"));
|
||||||
|
// The badge-scoping probe rides the same boot: it needs the fat server for a
|
||||||
|
// node id and the compositor for a layer id, and init starts both. It spawns
|
||||||
|
// its own second process — the intruder — with the ids it holds (P4c).
|
||||||
|
check("badge-scope-test owner spawned", spawnNamed(rd, "badge-scope-test"));
|
||||||
result();
|
result();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,15 +60,14 @@ const pwrbtn_bit: u16 = 1 << 8;
|
||||||
const sci_en_bit: u32 = 1 << 0;
|
const sci_en_bit: u32 = 1 << 0;
|
||||||
const slp_en: u32 = 1 << 13;
|
const slp_en: u32 = 1 << 13;
|
||||||
|
|
||||||
// The `.power` subscribers: endpoints handed over as capabilities, each
|
/// The `.power` subscribers, kept by the service harness (P4c): endpoints handed
|
||||||
// receiving events as buffered messages. Dropped on a failed send. The
|
/// over as capabilities, each receiving events as buffered messages, each swept
|
||||||
// subscriber's task id is kept too — a shutdown request is honored only from a
|
/// when its task dies. The table remembers which task subscribed, which is what
|
||||||
// subscriber (init subscribes; a stray process does not), the soft gate that
|
/// the shutdown gate below asks — a shutdown request is honored only from a
|
||||||
// stands in for "only the system supervisor may power off" without hardcoding
|
/// subscriber (init subscribes; a stray process does not), the soft gate that
|
||||||
// a pid the kernel's idle tasks would have taken.
|
/// stands in for "only the system supervisor may power off" without hardcoding a
|
||||||
const maximum_subscribers = 8;
|
/// pid the kernel's idle tasks would have taken.
|
||||||
var subscribers: [maximum_subscribers]?ipc.Handle = .{null} ** maximum_subscribers;
|
const Subscriptions = service.Subscribers(power_protocol.Protocol, void);
|
||||||
var subscriber_tasks: [maximum_subscribers]u32 = .{0} ** maximum_subscribers;
|
|
||||||
|
|
||||||
// Pass-1 registration record (see main): what pass 2 reports.
|
// Pass-1 registration record (see main): what pass 2 reports.
|
||||||
const Registered = struct { hid: [8]u8 = .{0} ** 8, hid_len: usize = 0, device_id: u64 = 0, resource_count: u64 = 0 };
|
const Registered = struct { hid: [8]u8 = .{0} ** 8, hid_len: usize = 0, device_id: u64 = 0, resource_count: u64 = 0 };
|
||||||
|
|
@ -201,6 +200,7 @@ pub fn main(init: process.Init) void {
|
||||||
.init = onInit,
|
.init = onInit,
|
||||||
.on_message = onMessage,
|
.on_message = onMessage,
|
||||||
.on_notification = onNotification,
|
.on_notification = onNotification,
|
||||||
|
.subscribers = Subscriptions.hooks,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -407,13 +407,13 @@ fn publishNotify(node: *aml.Node, code: u64) void {
|
||||||
const notice = power_protocol.Notice{ .code = @truncate(code), .hid = hid };
|
const notice = power_protocol.Notice{ .code = @truncate(code), .hid = hid };
|
||||||
std.log.info("power: notify {s} code {d}", .{ hid[0..7], code });
|
std.log.info("power: notify {s} code {d}", .{ hid[0..7], code });
|
||||||
if (std.mem.eql(u8, hid[0..7], "PNP0C0A")) {
|
if (std.mem.eql(u8, hid[0..7], "PNP0C0A")) {
|
||||||
publish(.battery, notice);
|
Subscriptions.publish(.battery, 0, notice);
|
||||||
} else if (std.mem.eql(u8, hid[0..7], "ACPI0003")) {
|
} else if (std.mem.eql(u8, hid[0..7], "ACPI0003")) {
|
||||||
publish(.ac, notice);
|
Subscriptions.publish(.ac, 0, notice);
|
||||||
} else if (std.mem.eql(u8, hid[0..7], "PNP0C0D")) {
|
} else if (std.mem.eql(u8, hid[0..7], "PNP0C0D")) {
|
||||||
publish(.lid, notice);
|
Subscriptions.publish(.lid, 0, notice);
|
||||||
} else {
|
} else {
|
||||||
publish(.notify, notice);
|
Subscriptions.publish(.notify, 0, notice);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -425,28 +425,10 @@ fn writeHex2(out: []u8, n: u32) void {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn publishButton() void {
|
fn publishButton() void {
|
||||||
publish(.power_button, .{});
|
// The kind is the packet's operation, so the harness frames it once and pushes
|
||||||
}
|
// the same bytes to every subscriber. There is no class here: a power event
|
||||||
|
// goes to everyone who asked for power events.
|
||||||
/// Push one event to every subscriber. The kind is the packet's operation, so
|
Subscriptions.publish(.power_button, 0, .{});
|
||||||
/// this is framed once, outside the loop — every subscriber gets identical
|
|
||||||
/// bytes. A subscriber whose endpoint stops accepting (it died) is dropped on
|
|
||||||
/// the failed send, so a dead one can never stall the rest.
|
|
||||||
fn publish(comptime kind: power_protocol.Event, notice: power_protocol.Notice) void {
|
|
||||||
var packet: [envelope.post_maximum]u8 = undefined;
|
|
||||||
const framed = power_protocol.Protocol.encodeEvent(kind, 0, notice, &packet) orelse return;
|
|
||||||
for (&subscribers) |*slot| {
|
|
||||||
if (slot.*) |handle| {
|
|
||||||
if (!ipc.send(handle, framed)) slot.* = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn isSubscriber(task: u32) bool {
|
|
||||||
for (&subscribers, 0..) |*slot, si| {
|
|
||||||
if (slot.* != null and subscriber_tasks[si] == task) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enter S5 (soft off): write SLP_TYP|SLP_EN to the PM1 control register(s).
|
/// Enter S5 (soft off): write SLP_TYP|SLP_EN to the PM1 control register(s).
|
||||||
|
|
@ -468,57 +450,37 @@ fn enterS5() void {
|
||||||
// --- harness callbacks --------------------------------------------------------
|
// --- harness callbacks --------------------------------------------------------
|
||||||
|
|
||||||
fn onNotification(badge: u64) void {
|
fn onNotification(badge: u64) void {
|
||||||
// The only notification the service binds is the SCI (an IRQ badge).
|
// Two kinds of notification reach this loop now. The SCI is the one this
|
||||||
_ = badge;
|
// service binds; the published process exits are the harness's, which it has
|
||||||
|
// already used to sweep the subscriber table before calling here. Everything
|
||||||
|
// that is not a bare IRQ badge must therefore be ignored — treating a death
|
||||||
|
// as an interrupt would clear PM1 status the firmware never set.
|
||||||
|
if (badge & (ipc.notify_exit_bit | ipc.notify_timer_bit | ipc.notify_message_bit | ipc.notify_signal_bit) != 0) return;
|
||||||
onSci();
|
onSci();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The generated power dispatch. One provider per system, so the handler context
|
|
||||||
/// is empty and the subscriber table stays in this file's globals.
|
|
||||||
const Serve = power_protocol.Protocol.Provider(void);
|
|
||||||
|
|
||||||
const Invocation = envelope.Invocation;
|
const Invocation = envelope.Invocation;
|
||||||
const Answer = envelope.Answer;
|
const Answer = envelope.Answer;
|
||||||
|
|
||||||
/// Set by `onSubscribe` when the subscriber table has taken the capability the
|
|
||||||
/// call carried, and read by `onMessage`, where the turn's `Arrival` lives.
|
|
||||||
var capability_claimed = false;
|
|
||||||
|
|
||||||
/// The power contract: the reserved `subscribe` (the subscriber's endpoint as
|
/// The power contract: the reserved `subscribe` (the subscriber's endpoint as
|
||||||
/// the call's capability) and `shutdown` (subscribers only). Device discovery
|
/// the call's capability, answered by the harness) and `shutdown` (subscribers
|
||||||
/// uses a different endpoint — the device manager's — so nothing here handles a
|
/// only). Device discovery uses a different endpoint — the device manager's — so
|
||||||
/// tree report.
|
/// nothing here handles a tree report.
|
||||||
fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize {
|
fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize {
|
||||||
capability_claimed = false;
|
return Subscriptions.dispatch({}, handlers, message, sender, arrived, reply);
|
||||||
const written = Serve.dispatch({}, handlers, message, sender, arrived.peek(), reply);
|
|
||||||
if (capability_claimed) _ = arrived.take();
|
|
||||||
return written;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlers = Serve.Handlers{ .shutdown = onShutdown, .subscribe = onSubscribe };
|
/// `subscribe` and `unsubscribe` are absent on purpose: the harness answers both.
|
||||||
|
const handlers = Subscriptions.Handlers{ .shutdown = onShutdown };
|
||||||
/// The subscriber's endpoint is claimed only when a slot takes it; a full table
|
|
||||||
/// refuses and the turn closes what arrived.
|
|
||||||
fn onSubscribe(_: void, invocation: Invocation(void), _: Answer(void)) isize {
|
|
||||||
const endpoint = invocation.capability orelse return -envelope.EPROTO;
|
|
||||||
for (&subscribers, 0..) |*slot, index| {
|
|
||||||
if (slot.* == null) {
|
|
||||||
slot.* = endpoint;
|
|
||||||
subscriber_tasks[index] = invocation.sender;
|
|
||||||
capability_claimed = true;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -envelope.ENOSPC;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Honored only from a power subscriber — init, which has already run the stop
|
/// Honored only from a power subscriber — init, which has already run the stop
|
||||||
/// sequence over everything else. The power service is mechanism (write S5);
|
/// sequence over everything else. The power service is mechanism (write S5);
|
||||||
/// deciding *when* to shut down and stopping the rest of the system first is
|
/// deciding *when* to shut down and stopping the rest of the system first is
|
||||||
/// init's policy. The badge is the whole gate: it is kernel-stamped, so nothing
|
/// init's policy. The badge is the whole gate: it is kernel-stamped, so nothing
|
||||||
/// in the packet can claim to be init.
|
/// in the packet can claim to be init. The subscriber table moved into the
|
||||||
|
/// harness; the question it answers has not changed.
|
||||||
fn onShutdown(_: void, invocation: Invocation(void), _: Answer(void)) isize {
|
fn onShutdown(_: void, invocation: Invocation(void), _: Answer(void)) isize {
|
||||||
if (!isSubscriber(invocation.sender)) return -envelope.EPERM;
|
if (!Subscriptions.has(invocation.sender)) return -envelope.EPERM;
|
||||||
enterS5();
|
enterS5();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,11 @@ const device_manager_protocol = @import("device-manager-protocol");
|
||||||
const envelope = @import("envelope");
|
const envelope = @import("envelope");
|
||||||
const registry = @import("device-registry");
|
const registry = @import("device-registry");
|
||||||
|
|
||||||
/// The generated device-manager dispatch. One manager per system, so the handler
|
/// The generated device-manager dispatch, plus the subscriber machinery the
|
||||||
/// context is empty and the tables stay in this file's globals.
|
/// harness owns (P4c): the watcher table, the reserved `subscribe` verb, the
|
||||||
const Serve = device_manager_protocol.Protocol.Provider(void);
|
/// exit sweep, and the fan-out. One manager per system, so the handler context is
|
||||||
|
/// empty and the tables stay in this file's globals.
|
||||||
|
const Serve = service.Subscribers(device_manager_protocol.Protocol, void);
|
||||||
|
|
||||||
const Invocation = envelope.Invocation;
|
const Invocation = envelope.Invocation;
|
||||||
const Answer = envelope.Answer;
|
const Answer = envelope.Answer;
|
||||||
|
|
@ -156,31 +158,6 @@ var test_scanout_killed = false;
|
||||||
var test_kill_pid: u32 = 0;
|
var test_kill_pid: u32 = 0;
|
||||||
var test_kill_due_ns: u64 = 0;
|
var test_kill_due_ns: u64 = 0;
|
||||||
|
|
||||||
/// The application subscribers (M18.3, the input-service pattern): endpoints
|
|
||||||
/// handed over as capabilities, each receiving every child add/remove as a
|
|
||||||
/// buffered message. A subscriber whose endpoint stops accepting (it died) is
|
|
||||||
/// dropped on the failed send.
|
|
||||||
const maximum_subscribers = 8;
|
|
||||||
var subscribers: [maximum_subscribers]?ipc.Handle = .{null} ** maximum_subscribers;
|
|
||||||
|
|
||||||
/// Push one event to every subscriber: the same struct a bus driver *called*
|
|
||||||
/// with, framed as an event instead — one encoding, both directions, told apart
|
|
||||||
/// by the packet's verb rather than by anything inside it. A subscriber whose
|
|
||||||
/// endpoint stops accepting (it died) is dropped on the failed send.
|
|
||||||
fn publish(
|
|
||||||
comptime event: device_manager_protocol.Event,
|
|
||||||
target: u64,
|
|
||||||
payload: device_manager_protocol.Protocol.PayloadOf(event),
|
|
||||||
) void {
|
|
||||||
var packet: [envelope.post_maximum]u8 = undefined;
|
|
||||||
const framed = device_manager_protocol.Protocol.encodeEvent(event, target, payload, &packet) orelse return;
|
|
||||||
for (&subscribers) |*slot| {
|
|
||||||
if (slot.*) |handle| {
|
|
||||||
if (!ipc.send(handle, framed)) slot.* = null; // dead subscriber
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The manager's mirror of what bus drivers report (docs/device-manager.md "the
|
/// The manager's mirror of what bus drivers report (docs/device-manager.md "the
|
||||||
/// tree"): the children, keyed by (parent, bus address), each remembering which
|
/// tree"): the children, keyed by (parent, bus address), each remembering which
|
||||||
/// driver instance reported it — that is what death-pruning sweeps by.
|
/// driver instance reported it — that is what death-pruning sweeps by.
|
||||||
|
|
@ -225,7 +202,7 @@ fn pruneChildrenOf(reporter: u32) void {
|
||||||
if (child.used and child.reporter == reporter) {
|
if (child.used and child.reporter == reporter) {
|
||||||
std.log.info("child removed (device {d} port {d})", .{ child.parent, child.bus_address });
|
std.log.info("child removed (device {d} port {d})", .{ child.parent, child.bus_address });
|
||||||
child.used = false;
|
child.used = false;
|
||||||
publish(.child_removed, 0, .{ .parent = child.parent, .bus_address = child.bus_address });
|
Serve.publish(.child_removed, 0, .{ .parent = child.parent, .bus_address = child.bus_address });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -240,7 +217,11 @@ fn childCountOf(reporter: u32) u32 {
|
||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The driver entry a live process id belongs to. Zero is not a process id here:
|
||||||
|
/// it is what `onDriverExit` writes back to retire an id it has already acted on,
|
||||||
|
/// so a second notification for the same death matches nothing.
|
||||||
fn driverByProcess(process_id: u32) ?*Driver {
|
fn driverByProcess(process_id: u32) ?*Driver {
|
||||||
|
if (process_id == 0) return null;
|
||||||
for (&drivers) |*driver| {
|
for (&drivers) |*driver| {
|
||||||
if (driver.used and driver.process_id == process_id) return driver;
|
if (driver.used and driver.process_id == process_id) return driver;
|
||||||
}
|
}
|
||||||
|
|
@ -308,8 +289,17 @@ fn spawnDriver(driver: *Driver) void {
|
||||||
/// is the whole restart decision: a clean exit meant to stop; anything else
|
/// is the whole restart decision: a clean exit meant to stop; anything else
|
||||||
/// restarts with backoff until the crash-loop cap.
|
/// restarts with backoff until the crash-loop cap.
|
||||||
fn onDriverExit(driver: *Driver) void {
|
fn onDriverExit(driver: *Driver) void {
|
||||||
pruneChildrenOf(driver.process_id);
|
const dead = driver.process_id;
|
||||||
const reason = process.exitReason(driver.process_id) orelse .fault;
|
// One death, two notifications: the manager is this driver's supervisor (its
|
||||||
|
// spawn named this endpoint) *and*, since P4c put the watcher table in the
|
||||||
|
// harness, a subscriber to published exits. Both badges carry the same id, and
|
||||||
|
// the ring delivers them separately — so the id is retired here, before any
|
||||||
|
// decision is taken, and the second notification finds no driver to act on.
|
||||||
|
// Without this the backoff would count one death twice and the crash-loop cap
|
||||||
|
// would fire at half the deaths it names.
|
||||||
|
driver.process_id = 0;
|
||||||
|
pruneChildrenOf(dead);
|
||||||
|
const reason = process.exitReason(dead) orelse .fault;
|
||||||
if (reason == .exited) {
|
if (reason == .exited) {
|
||||||
driver.state = .stopped;
|
driver.state = .stopped;
|
||||||
std.log.info("{s} exited cleanly; not restarting", .{driver.name()});
|
std.log.info("{s} exited cleanly; not restarting", .{driver.name()});
|
||||||
|
|
@ -410,27 +400,17 @@ fn initialise(endpoint: ipc.Handle) bool {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set by `onSubscribe` when the subscriber table has taken ownership of the
|
|
||||||
/// capability the call carried, and read by `onMessage`, where the turn's
|
|
||||||
/// `Arrival` lives. The generated dispatch hands a handler the raw handle rather
|
|
||||||
/// than the `Arrival` — deliberately, since a handler has no business closing the
|
|
||||||
/// turn's property — so the *claim* travels back out this way. One turn, one
|
|
||||||
/// handler, one thread: there is nothing here to race.
|
|
||||||
var capability_claimed = false;
|
|
||||||
|
|
||||||
fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize {
|
fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arrival) usize {
|
||||||
capability_claimed = false;
|
return Serve.dispatch({}, handlers, message, sender, arrived, reply);
|
||||||
const written = Serve.dispatch({}, handlers, message, sender, arrived.peek(), reply);
|
|
||||||
if (capability_claimed) _ = arrived.take();
|
|
||||||
return written;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `subscribe` and `unsubscribe` are absent on purpose: the harness answers both,
|
||||||
|
/// and its table is what `publish` fans out over.
|
||||||
const handlers = Serve.Handlers{
|
const handlers = Serve.Handlers{
|
||||||
.hello = onHello,
|
.hello = onHello,
|
||||||
.child_added = onChildAdded,
|
.child_added = onChildAdded,
|
||||||
.child_removed = onChildRemoved,
|
.child_removed = onChildRemoved,
|
||||||
.enumerate = onEnumerate,
|
.enumerate = onEnumerate,
|
||||||
.subscribe = onSubscribe,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The handshake. The device this driver was assigned is the packet's target.
|
/// The handshake. The device this driver was assigned is the packet's target.
|
||||||
|
|
@ -470,7 +450,7 @@ fn onChildAdded(_: void, invocation: Invocation(device_manager_protocol.ChildAdd
|
||||||
if (driverByProcess(sender)) |driver| {
|
if (driverByProcess(sender)) |driver| {
|
||||||
if (!addChild(report.parent, report.bus_address, report.identity, device_id, sender)) status = -envelope.ENOSPC;
|
if (!addChild(report.parent, report.bus_address, report.identity, device_id, sender)) status = -envelope.ENOSPC;
|
||||||
std.log.info("child added (device {d} port {d}, identity {d}) by {s}", .{ report.parent, report.bus_address, report.identity, driver.name() });
|
std.log.info("child added (device {d} port {d}, identity {d}) by {s}", .{ report.parent, report.bus_address, report.identity, driver.name() });
|
||||||
if (status == 0) publish(.child_added, device_id, report);
|
if (status == 0) Serve.publish(.child_added, device_id, report);
|
||||||
// Matching from reports (M19.3), now data-driven via the /system/configuration/devices.csv
|
// Matching from reports (M19.3), now data-driven via the /system/configuration/devices.csv
|
||||||
// registry: a registered child gets the most-specific driver its identity
|
// registry: a registered child gets the most-specific driver its identity
|
||||||
// matches, once — re-reports after a bus restart dedupe on the registered
|
// matches, once — re-reports after a bus restart dedupe on the registered
|
||||||
|
|
@ -556,24 +536,11 @@ fn onEnumerate(_: void, _: Invocation(void), answer: Answer(void)) isize {
|
||||||
return @intCast(written);
|
return @intCast(written);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The reserved `subscribe` verb: an application's endpoint arrived as the call's
|
|
||||||
/// capability. The table taking a slot is what claims it; a full table refuses
|
|
||||||
/// and lets the turn close it, so a subscribe storm cannot spend the handle table.
|
|
||||||
fn onSubscribe(_: void, invocation: Invocation(void), _: Answer(void)) isize {
|
|
||||||
const endpoint = invocation.capability orelse return -envelope.EPROTO;
|
|
||||||
for (&subscribers) |*slot| {
|
|
||||||
if (slot.* == null) {
|
|
||||||
slot.* = endpoint;
|
|
||||||
capability_claimed = true; // the table holds it from here
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return -envelope.ENOSPC;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn onNotification(badge: u64) void {
|
fn onNotification(badge: u64) void {
|
||||||
if (badge & ipc.notify_exit_bit != 0) {
|
if (badge & ipc.notify_exit_bit != 0) {
|
||||||
const dead: u32 = @intCast(badge & ~(ipc.notify_badge_bit | ipc.notify_exit_bit));
|
const dead: u32 = @intCast(badge & ~(ipc.notify_badge_bit | ipc.notify_exit_bit));
|
||||||
|
// The harness has already swept the watcher table for this death; what is
|
||||||
|
// left is the manager's own concern, its supervised drivers.
|
||||||
if (driverByProcess(dead)) |driver| onDriverExit(driver);
|
if (driverByProcess(dead)) |driver| onDriverExit(driver);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -592,5 +559,6 @@ pub fn main(init: process.Init) void {
|
||||||
.init = initialise,
|
.init = initialise,
|
||||||
.on_message = onMessage,
|
.on_message = onMessage,
|
||||||
.on_notification = onNotification,
|
.on_notification = onNotification,
|
||||||
|
.subscribers = Serve.hooks,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@ pub fn build(b: *std.Build) void {
|
||||||
.root_source_file = b.path("display.zig"),
|
.root_source_file = b.path("display.zig"),
|
||||||
.imports = &.{
|
.imports = &.{
|
||||||
"channel", "display-client", "display-protocol", "driver", "envelope", "input-client",
|
"channel", "display-client", "display-protocol", "driver", "envelope", "input-client",
|
||||||
"ipc", "logging", "memory", "scanout-protocol", "service",
|
"ipc", "logging", "memory", "process", "scanout-protocol",
|
||||||
"thread", "time",
|
"service", "thread", "time",
|
||||||
},
|
},
|
||||||
.threaded = true, // real atomics/TLS (docs/threading.md)
|
.threaded = true, // real atomics/TLS (docs/threading.md)
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ const std = @import("std");
|
||||||
const channel = @import("channel");
|
const channel = @import("channel");
|
||||||
const ipc = @import("ipc");
|
const ipc = @import("ipc");
|
||||||
const input = @import("input-client");
|
const input = @import("input-client");
|
||||||
|
const process = @import("process");
|
||||||
const Thread = @import("thread").Thread;
|
const Thread = @import("thread").Thread;
|
||||||
const service = @import("service");
|
const service = @import("service");
|
||||||
const time = @import("time");
|
const time = @import("time");
|
||||||
|
|
@ -74,8 +75,22 @@ var background: u32 = 0;
|
||||||
/// small copies rather than one huge bounding box (see compositor.DamageList).
|
/// small copies rather than one huge bounding box (see compositor.DamageList).
|
||||||
const maximum_layers = 16;
|
const maximum_layers = 16;
|
||||||
|
|
||||||
|
/// A layer belongs to the client that created it. `owner` is the kernel-stamped
|
||||||
|
/// badge of that task, and `service_owned` (0, an id no task wears) marks the
|
||||||
|
/// compositor's own layers — the cursor sprite and the self-check pair — which
|
||||||
|
/// this file creates by direct call rather than over the protocol.
|
||||||
|
///
|
||||||
|
/// Layer ids are slots in a sixteen-entry table: small, dense, and guessable, so
|
||||||
|
/// before this field any client could configure, draw into, or destroy any
|
||||||
|
/// other's layer — including the cursor. The check lives in the protocol handlers
|
||||||
|
/// (docs/os-development/protocol-namespace.md: handles validated against the
|
||||||
|
/// badge); the internal helpers stay unscoped precisely so the compositor can
|
||||||
|
/// still drive its own.
|
||||||
|
const service_owned: u32 = 0;
|
||||||
|
|
||||||
const Layer = struct {
|
const Layer = struct {
|
||||||
used: bool = false,
|
used: bool = false,
|
||||||
|
owner: u32 = service_owned,
|
||||||
x: i32 = 0,
|
x: i32 = 0,
|
||||||
y: i32 = 0,
|
y: i32 = 0,
|
||||||
z: u32 = 0,
|
z: u32 = 0,
|
||||||
|
|
@ -189,7 +204,8 @@ fn layerAt(id: u32) ?*Layer {
|
||||||
return &layers[id];
|
return &layers[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
fn createLayer(x: i32, y: i32, w: u32, h: u32, z: u32, visible: bool) ?u32 {
|
/// Create a layer for `owner` — `service_owned` for the compositor's own.
|
||||||
|
fn createLayer(owner: u32, x: i32, y: i32, w: u32, h: u32, z: u32, visible: bool) ?u32 {
|
||||||
if (w == 0 or h == 0) return null;
|
if (w == 0 or h == 0) return null;
|
||||||
const slot = freeLayer() orelse return null;
|
const slot = freeLayer() orelse return null;
|
||||||
const len = @as(usize, w) * h * 4;
|
const len = @as(usize, w) * h * 4;
|
||||||
|
|
@ -197,6 +213,7 @@ fn createLayer(x: i32, y: i32, w: u32, h: u32, z: u32, visible: bool) ?u32 {
|
||||||
if (memory.mmapFailed(base)) return null;
|
if (memory.mmapFailed(base)) return null;
|
||||||
layers[slot] = .{
|
layers[slot] = .{
|
||||||
.used = true,
|
.used = true,
|
||||||
|
.owner = owner,
|
||||||
.x = x,
|
.x = x,
|
||||||
.y = y,
|
.y = y,
|
||||||
.z = z,
|
.z = z,
|
||||||
|
|
@ -420,8 +437,8 @@ fn selfCheck() void {
|
||||||
const format = backend.info().format;
|
const format = backend.info().format;
|
||||||
const red = display_protocol.pack(format, 0xC0, 0x20, 0x20);
|
const red = display_protocol.pack(format, 0xC0, 0x20, 0x20);
|
||||||
const green = display_protocol.pack(format, 0x20, 0xC0, 0x20);
|
const green = display_protocol.pack(format, 0x20, 0xC0, 0x20);
|
||||||
const bottom = createLayer(100, 100, 80, 80, 0, true) orelse return fail_check("create");
|
const bottom = createLayer(service_owned, 100, 100, 80, 80, 0, true) orelse return fail_check("create");
|
||||||
const top = createLayer(140, 140, 80, 80, 1, true) orelse return fail_check("create");
|
const top = createLayer(service_owned, 140, 140, 80, 80, 1, true) orelse return fail_check("create");
|
||||||
_ = fillLayer(bottom, Rect.init(0, 0, 80, 80), red);
|
_ = fillLayer(bottom, Rect.init(0, 0, 80, 80), red);
|
||||||
_ = fillLayer(top, Rect.init(0, 0, 80, 80), green);
|
_ = fillLayer(top, Rect.init(0, 0, 80, 80), green);
|
||||||
present();
|
present();
|
||||||
|
|
@ -586,7 +603,7 @@ fn startCursorTracking() void {
|
||||||
const mode = backend.info();
|
const mode = backend.info();
|
||||||
cursor_origin_x = @divTrunc(@as(i32, @intCast(mode.width)), 2);
|
cursor_origin_x = @divTrunc(@as(i32, @intCast(mode.width)), 2);
|
||||||
cursor_origin_y = @divTrunc(@as(i32, @intCast(mode.height)), 2);
|
cursor_origin_y = @divTrunc(@as(i32, @intCast(mode.height)), 2);
|
||||||
const id = createLayer(cursor_origin_x, cursor_origin_y, cursor_size, cursor_size, cursor_z, true) orelse {
|
const id = createLayer(service_owned, cursor_origin_x, cursor_origin_y, cursor_size, cursor_size, cursor_z, true) orelse {
|
||||||
_ = logging.write("display: could not create cursor layer\n");
|
_ = logging.write("display: could not create cursor layer\n");
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
@ -603,6 +620,9 @@ fn startCursorTracking() void {
|
||||||
|
|
||||||
fn initialise(endpoint: ipc.Handle) bool {
|
fn initialise(endpoint: ipc.Handle) bool {
|
||||||
service_endpoint = endpoint;
|
service_endpoint = endpoint;
|
||||||
|
// Layers are per-client state, so the compositor needs deaths: a client that
|
||||||
|
// crashes leaves its surfaces on screen and its slots spent otherwise.
|
||||||
|
_ = process.subscribeExits(endpoint);
|
||||||
|
|
||||||
// Pick the scanout backend (GOP today). It logs the reason on failure.
|
// Pick the scanout backend (GOP today). It logs the reason on failure.
|
||||||
backend = backend_mod.select() orelse return false;
|
backend = backend_mod.select() orelse return false;
|
||||||
|
|
@ -635,9 +655,35 @@ fn initialise(endpoint: ipc.Handle) bool {
|
||||||
// id a u32: a value that does not fit is not a layer of ours, and `layerAt`
|
// id a u32: a value that does not fit is not a layer of ours, and `layerAt`
|
||||||
// refuses it the same way an out-of-range one is refused.
|
// refuses it the same way an out-of-range one is refused.
|
||||||
|
|
||||||
fn targetLayer(target: u64) ?u32 {
|
/// The layer a packet addresses, **for the task that sent it**: null unless the
|
||||||
if (target > std.math.maxInt(u32)) return null;
|
/// target names a used slot this sender created. A layer that is somebody else's
|
||||||
return @intCast(target);
|
/// is refused exactly as one that never existed, so a client cannot use the
|
||||||
|
/// refusal to learn which ids are live (P3's refusal-equals-absence, applied to
|
||||||
|
/// ids rather than names).
|
||||||
|
fn targetLayer(target: u64, sender: u32) ?u32 {
|
||||||
|
if (target > std.math.maxInt(u32)) return null; // a layer id is a u32
|
||||||
|
const id: u32 = @intCast(target);
|
||||||
|
const layer = layerAt(id) orelse return null;
|
||||||
|
if (layer.owner != sender) return null;
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Destroy every layer a dead client left behind — its surface is pages nobody
|
||||||
|
/// will ever draw into again, and its slot is one of sixteen. The published
|
||||||
|
/// exit events are the notice, the same sweep idiom the FAT server uses for open
|
||||||
|
/// files and the harness uses for subscribers.
|
||||||
|
fn releaseLayersOf(dead: u32) void {
|
||||||
|
var released: u32 = 0;
|
||||||
|
for (&layers, 0..) |*layer, id| {
|
||||||
|
if (layer.used and layer.owner == dead) {
|
||||||
|
_ = destroyLayer(@intCast(id));
|
||||||
|
released += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (released != 0) {
|
||||||
|
std.log.info("released {d} layer(s) for dead client {d}", .{ released, dead });
|
||||||
|
schedulePresent(); // the screen still shows what they painted
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn onInfo(_: void, _: Invocation(void), answer: Answer(display_protocol.Info)) isize {
|
fn onInfo(_: void, _: Invocation(void), answer: Answer(display_protocol.Info)) isize {
|
||||||
|
|
@ -648,37 +694,37 @@ fn onInfo(_: void, _: Invocation(void), answer: Answer(display_protocol.Info)) i
|
||||||
|
|
||||||
fn onCreateLayer(_: void, invocation: Invocation(display_protocol.CreateLayer), answer: Answer(display_protocol.Created)) isize {
|
fn onCreateLayer(_: void, invocation: Invocation(display_protocol.CreateLayer), answer: Answer(display_protocol.Created)) isize {
|
||||||
const request = invocation.request;
|
const request = invocation.request;
|
||||||
const slot = createLayer(request.x, request.y, request.width, request.height, request.z, request.visible != 0) orelse return refused;
|
const slot = createLayer(invocation.sender, request.x, request.y, request.width, request.height, request.z, request.visible != 0) orelse return refused;
|
||||||
answer.set(.{ .layer = slot });
|
answer.set(.{ .layer = slot });
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn onConfigureLayer(_: void, invocation: Invocation(display_protocol.ConfigureLayer), _: Answer(void)) isize {
|
fn onConfigureLayer(_: void, invocation: Invocation(display_protocol.ConfigureLayer), _: Answer(void)) isize {
|
||||||
const id = targetLayer(invocation.target) orelse return refused;
|
const id = targetLayer(invocation.target, invocation.sender) orelse return refused;
|
||||||
const request = invocation.request;
|
const request = invocation.request;
|
||||||
return if (configureLayer(id, request.x, request.y, request.z, request.visible != 0)) 0 else refused;
|
return if (configureLayer(id, request.x, request.y, request.z, request.visible != 0)) 0 else refused;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn onDestroyLayer(_: void, invocation: Invocation(void), _: Answer(void)) isize {
|
fn onDestroyLayer(_: void, invocation: Invocation(void), _: Answer(void)) isize {
|
||||||
const id = targetLayer(invocation.target) orelse return refused;
|
const id = targetLayer(invocation.target, invocation.sender) orelse return refused;
|
||||||
return if (destroyLayer(id)) 0 else refused;
|
return if (destroyLayer(id)) 0 else refused;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn onFillRect(_: void, invocation: Invocation(display_protocol.FillRect), _: Answer(void)) isize {
|
fn onFillRect(_: void, invocation: Invocation(display_protocol.FillRect), _: Answer(void)) isize {
|
||||||
const id = targetLayer(invocation.target) orelse return refused;
|
const id = targetLayer(invocation.target, invocation.sender) orelse return refused;
|
||||||
const request = invocation.request;
|
const request = invocation.request;
|
||||||
const local = Rect.init(request.x, request.y, @intCast(request.width), @intCast(request.height));
|
const local = Rect.init(request.x, request.y, @intCast(request.width), @intCast(request.height));
|
||||||
return if (fillLayer(id, local, request.colour)) 0 else refused;
|
return if (fillLayer(id, local, request.colour)) 0 else refused;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn onBlitTile(_: void, invocation: Invocation(display_protocol.BlitTile), _: Answer(void)) isize {
|
fn onBlitTile(_: void, invocation: Invocation(display_protocol.BlitTile), _: Answer(void)) isize {
|
||||||
const id = targetLayer(invocation.target) orelse return refused;
|
const id = targetLayer(invocation.target, invocation.sender) orelse return refused;
|
||||||
const request = invocation.request;
|
const request = invocation.request;
|
||||||
return if (blitLayer(id, request.x, request.y, request.width, request.height, invocation.tail)) 0 else refused;
|
return if (blitLayer(id, request.x, request.y, request.width, request.height, invocation.tail)) 0 else refused;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn onDamage(_: void, invocation: Invocation(display_protocol.Damage), _: Answer(void)) isize {
|
fn onDamage(_: void, invocation: Invocation(display_protocol.Damage), _: Answer(void)) isize {
|
||||||
const id = targetLayer(invocation.target) orelse return refused;
|
const id = targetLayer(invocation.target, invocation.sender) orelse return refused;
|
||||||
const l = layerAt(id) orelse return refused;
|
const l = layerAt(id) orelse return refused;
|
||||||
const request = invocation.request;
|
const request = invocation.request;
|
||||||
const screen = Rect{ .x = l.x + request.x, .y = l.y + request.y, .w = @intCast(request.width), .h = @intCast(request.height) };
|
const screen = Rect{ .x = l.x + request.x, .y = l.y + request.y, .w = @intCast(request.width), .h = @intCast(request.height) };
|
||||||
|
|
@ -733,13 +779,18 @@ fn onMessage(message: []const u8, reply: []u8, sender: u32, arrived: *ipc.Arriva
|
||||||
return Serve.dispatch({}, handlers, message, sender, arrived.peek(), reply);
|
return Serve.dispatch({}, handlers, message, sender, arrived.peek(), reply);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Two notification sources reach the compositor, and one coalesced badge can carry
|
/// Three notification sources reach the compositor, and one coalesced badge can carry
|
||||||
/// both, so each bit is handled independently. A **message-notification** is a poke from
|
/// more than one, so each bit is handled independently. A **message-notification** is a
|
||||||
/// the mouse-listener thread (a buffered self-`ipc.send`, `notify_message_bit`): fold the
|
/// poke from the mouse-listener thread (a buffered self-`ipc.send`, `notify_message_bit`):
|
||||||
/// newest cursor position into the scene. A **timer** (`notify_timer_bit`) is the frame
|
/// fold the newest cursor position into the scene. A **timer** (`notify_timer_bit`) is the
|
||||||
/// clock — or the deferred first native present after `attach_scanout` — either way,
|
/// frame clock — or the deferred first native present after `attach_scanout` — either way,
|
||||||
/// present the accumulated damage.
|
/// present the accumulated damage. A **published exit** (`notify_exit_bit`) is a client
|
||||||
|
/// gone: release the layers it left.
|
||||||
fn onNotification(badge: u64) void {
|
fn onNotification(badge: u64) void {
|
||||||
|
if (badge & ipc.notify_exit_bit != 0) {
|
||||||
|
releaseLayersOf(@intCast(badge & ~(ipc.notify_badge_bit | ipc.notify_exit_bit)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (badge & ipc.notify_message_bit != 0) renderCursor();
|
if (badge & ipc.notify_message_bit != 0) renderCursor();
|
||||||
if (badge & ipc.notify_timer_bit != 0) frameTick();
|
if (badge & ipc.notify_timer_bit != 0) frameTick();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,8 +66,9 @@ var ipc_block: IpcBlock = undefined;
|
||||||
var device_dirty: bool = false;
|
var device_dirty: bool = false;
|
||||||
var filesystem: engine.FileSystem = undefined;
|
var filesystem: engine.FileSystem = undefined;
|
||||||
|
|
||||||
// Open handles the VFS holds against this backend: each maps a node id to a
|
// Open handles clients hold against this backend: each maps a node id to a
|
||||||
// resolved engine node.
|
// resolved engine node, and to the client that opened it. `owner` is the
|
||||||
|
// kernel-stamped badge of the opening task — the only source identity there is.
|
||||||
const OpenNode = struct { used: bool = false, node: engine.Node = undefined, owner: u32 = 0 };
|
const OpenNode = struct { used: bool = false, node: engine.Node = undefined, owner: u32 = 0 };
|
||||||
var open_nodes = [_]OpenNode{.{}} ** 32;
|
var open_nodes = [_]OpenNode{.{}} ** 32;
|
||||||
|
|
||||||
|
|
@ -78,16 +79,31 @@ fn allocOpen() ?usize {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn openAt(id: u64) ?*OpenNode {
|
/// The open node `id` names **for `owner`** — null unless the id is in range, in
|
||||||
|
/// use, and this client's own. Node ids are small integers drawn from a table of
|
||||||
|
/// thirty-two, so they are trivially guessable; before this check every client
|
||||||
|
/// honoured every other client's ids, which is the hole
|
||||||
|
/// docs/os-development/protocol-namespace.md names ("handles must be scoped per
|
||||||
|
/// client — validated against the badge"). Nothing else about them changed: they
|
||||||
|
/// are still per-session, still swept when their owner dies.
|
||||||
|
///
|
||||||
|
/// The owner is a *task*, not a process, because the badge is: a threaded client
|
||||||
|
/// reads and writes a node from the thread that opened it, exactly as the exit
|
||||||
|
/// sweep already released a worker thread's handles when that thread died.
|
||||||
|
fn openFor(id: u64, owner: u32) ?*OpenNode {
|
||||||
if (id >= open_nodes.len) return null;
|
if (id >= open_nodes.len) return null;
|
||||||
const o = &open_nodes[@intCast(id)];
|
const o = &open_nodes[@intCast(id)];
|
||||||
return if (o.used) o else null;
|
if (!o.used or o.owner != owner) return null;
|
||||||
|
return o;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// What a handler returns when the thing asked for is not there — a bad node id,
|
/// What a handler returns when the thing asked for is not there — a bad node id,
|
||||||
/// a path that does not resolve, a mutation the volume refused. One errno for all
|
/// a node that is someone else's, a path that does not resolve, a mutation the
|
||||||
/// of them, because a filesystem's failures are all "no such thing" as far as the
|
/// volume refused. One errno for all of them, because a filesystem's failures are
|
||||||
/// file API can act on them.
|
/// all "no such thing" as far as the file API can act on them — and because
|
||||||
|
/// *someone else's* must be indistinguishable from *nobody's*, or the refusal
|
||||||
|
/// would itself tell a prober which ids are live (the same discipline the
|
||||||
|
/// protocol namespace's refused open follows).
|
||||||
const refused: isize = -envelope.ENOENT;
|
const refused: isize = -envelope.ENOENT;
|
||||||
|
|
||||||
/// How often to look for a block device while none is mounted. Storage arriving
|
/// How often to look for a block device while none is mounted. Storage arriving
|
||||||
|
|
@ -230,14 +246,14 @@ fn onOpen(_: void, invocation: Invocation(vfs_protocol.Open), answer: Answer(vfs
|
||||||
}
|
}
|
||||||
|
|
||||||
fn onRead(_: void, invocation: Invocation(vfs_protocol.Read), answer: Answer(void)) isize {
|
fn onRead(_: void, invocation: Invocation(vfs_protocol.Read), answer: Answer(void)) isize {
|
||||||
const o = openAt(invocation.target) orelse return refused;
|
const o = openFor(invocation.target, invocation.sender) orelse return refused;
|
||||||
const into = answer.tail();
|
const into = answer.tail();
|
||||||
const want = @min(@as(usize, invocation.request.len), into.len);
|
const want = @min(@as(usize, invocation.request.len), into.len);
|
||||||
return @intCast(filesystem.readFile(o.node, @intCast(invocation.request.offset), into[0..want]));
|
return @intCast(filesystem.readFile(o.node, @intCast(invocation.request.offset), into[0..want]));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn onWrite(_: void, invocation: Invocation(vfs_protocol.Write), answer: Answer(vfs_protocol.Written)) isize {
|
fn onWrite(_: void, invocation: Invocation(vfs_protocol.Write), answer: Answer(vfs_protocol.Written)) isize {
|
||||||
const o = openAt(invocation.target) orelse return refused;
|
const o = openFor(invocation.target, invocation.sender) orelse return refused;
|
||||||
const data = invocation.tail[0..@min(invocation.tail.len, invocation.request.len)];
|
const data = invocation.tail[0..@min(invocation.tail.len, invocation.request.len)];
|
||||||
const n = filesystem.writeFile(&o.node, @intCast(invocation.request.offset), data);
|
const n = filesystem.writeFile(&o.node, @intCast(invocation.request.offset), data);
|
||||||
answer.set(.{ .count = @intCast(n) });
|
answer.set(.{ .count = @intCast(n) });
|
||||||
|
|
@ -245,7 +261,7 @@ fn onWrite(_: void, invocation: Invocation(vfs_protocol.Write), answer: Answer(v
|
||||||
}
|
}
|
||||||
|
|
||||||
fn onStatus(_: void, invocation: Invocation(void), answer: Answer(vfs_protocol.FileStatus)) isize {
|
fn onStatus(_: void, invocation: Invocation(void), answer: Answer(vfs_protocol.FileStatus)) isize {
|
||||||
const o = openAt(invocation.target) orelse return refused;
|
const o = openFor(invocation.target, invocation.sender) orelse return refused;
|
||||||
const kind: vfs_protocol.NodeKind = if (o.node.is_directory) .directory else .regular;
|
const kind: vfs_protocol.NodeKind = if (o.node.is_directory) .directory else .regular;
|
||||||
answer.set(.{ .size = o.node.size, .kind = @intFromEnum(kind), .mtime = o.node.mtime });
|
answer.set(.{ .size = o.node.size, .kind = @intFromEnum(kind), .mtime = o.node.mtime });
|
||||||
return 0;
|
return 0;
|
||||||
|
|
@ -255,7 +271,7 @@ fn onStatus(_: void, invocation: Invocation(void), answer: Answer(vfs_protocol.F
|
||||||
/// cursor past the last child — is an entry with no name, which is how the
|
/// cursor past the last child — is an entry with no name, which is how the
|
||||||
/// protocol spells it now that the reply's length always counts the fixed part.
|
/// protocol spells it now that the reply's length always counts the fixed part.
|
||||||
fn onReaddir(_: void, invocation: Invocation(vfs_protocol.Readdir), answer: Answer(vfs_protocol.DirectoryEntry)) isize {
|
fn onReaddir(_: void, invocation: Invocation(vfs_protocol.Readdir), answer: Answer(vfs_protocol.DirectoryEntry)) isize {
|
||||||
const o = openAt(invocation.target) orelse return refused;
|
const o = openFor(invocation.target, invocation.sender) orelse return refused;
|
||||||
if (!o.node.is_directory) {
|
if (!o.node.is_directory) {
|
||||||
answer.set(.{});
|
answer.set(.{});
|
||||||
return 0;
|
return 0;
|
||||||
|
|
@ -272,8 +288,13 @@ fn onReaddir(_: void, invocation: Invocation(vfs_protocol.Readdir), answer: Answ
|
||||||
return @intCast(name_len);
|
return @intCast(name_len);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Closing is an operation on a node like any other, so it is scoped like any
|
||||||
|
/// other: a client may release its own handles and nobody else's. An id that is
|
||||||
|
/// not the caller's — free, out of range, or another client's — is refused
|
||||||
|
/// identically, so a close cannot be used to ask which ids are live either.
|
||||||
fn onClose(_: void, invocation: Invocation(void), _: Answer(void)) isize {
|
fn onClose(_: void, invocation: Invocation(void), _: Answer(void)) isize {
|
||||||
if (openAt(invocation.target)) |o| o.used = false;
|
const o = openFor(invocation.target, invocation.sender) orelse return refused;
|
||||||
|
o.used = false;
|
||||||
// Durable-on-close: if any block reached the device since the last flush,
|
// Durable-on-close: if any block reached the device since the last flush,
|
||||||
// commit its cache to stable media now (best-effort). This is what makes
|
// commit its cache to stable media now (best-effort). This is what makes
|
||||||
// init's shutdown log flush survive a real power-off, and is the right
|
// init's shutdown log flush survive a real power-off, and is the right
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ pub fn build(b: *std.Build) void {
|
||||||
const exe = build_support.userBinary(b, .{
|
const exe = build_support.userBinary(b, .{
|
||||||
.name = "input",
|
.name = "input",
|
||||||
.root_source_file = b.path("input.zig"),
|
.root_source_file = b.path("input.zig"),
|
||||||
.imports = &.{ "envelope", "input-protocol", "ipc", "logging", "process", "service" },
|
.imports = &.{ "envelope", "input-protocol", "ipc", "logging", "service" },
|
||||||
});
|
});
|
||||||
b.installArtifact(exe);
|
b.installArtifact(exe);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,133 +20,52 @@
|
||||||
//! handle and `ipc.send`s each event to it. That is the envelope's reserved `subscribe`
|
//! handle and `ipc.send`s each event to it. That is the envelope's reserved `subscribe`
|
||||||
//! verb, which this protocol adopts rather than defining its own.
|
//! verb, which this protocol adopts rather than defining its own.
|
||||||
//!
|
//!
|
||||||
//! P4a moved this service onto the shared harness (library/kernel/service.zig). It was the
|
//! P4a moved this service onto the shared harness (library/kernel/service.zig) — it was the
|
||||||
//! last hand-rolled receive loop in the tree, and the one service that answered neither the
|
//! last hand-rolled receive loop in the tree, and the one service that answered neither the
|
||||||
//! universal ping nor a `terminate` signal — so a shutdown had to kill it. The subscriber
|
//! universal ping nor a `terminate` signal. P4c finished the job: the subscriber table, the
|
||||||
//! table, the fan-out, and the prune-on-subscribe below are unchanged; lifting *those* into
|
//! fan-out, and the dead-subscriber sweep are the harness's now
|
||||||
//! the harness is a later milestone, and doing it here would have hidden this one.
|
//! (`service.Subscribers`), so what is left here is what is actually about input — which
|
||||||
|
//! device class an event belongs to, and which classes a subscriber asked for. The sweep
|
||||||
|
//! that replaced the old prune is the one idiom the system uses everywhere: published
|
||||||
|
//! process-exit notifications, not a poll of the process list on every subscribe.
|
||||||
|
|
||||||
const envelope = @import("envelope");
|
|
||||||
const ipc = @import("ipc");
|
const ipc = @import("ipc");
|
||||||
const process = @import("process");
|
|
||||||
const service = @import("service");
|
const service = @import("service");
|
||||||
const logging = @import("logging");
|
const logging = @import("logging");
|
||||||
const input_protocol = @import("input-protocol");
|
const input_protocol = @import("input-protocol");
|
||||||
|
const envelope = @import("envelope");
|
||||||
|
|
||||||
/// The generated input dispatch. One fan-out point per process, so the handler
|
/// The subscriber side of the input contract: the table, the reserved `subscribe`
|
||||||
/// context is empty and the subscriber table stays in this file's globals.
|
/// verb, the exit sweep, and the fan-out. One fan-out point per process, so the
|
||||||
const Serve = input_protocol.Protocol.Provider(void);
|
/// handler context is empty.
|
||||||
|
const Subscriptions = service.Subscribers(input_protocol.Protocol, void);
|
||||||
|
|
||||||
const Invocation = envelope.Invocation;
|
const Invocation = envelope.Invocation;
|
||||||
const Answer = envelope.Answer;
|
const Answer = envelope.Answer;
|
||||||
|
|
||||||
/// One registered subscriber: the endpoint we push events to (a capability it handed us at
|
/// Push `event` to every subscriber whose interest mask includes its device class. The
|
||||||
/// subscribe time) and the task id that owns it (the subscribe call's badge), so a slot
|
/// class is the packet's operation, so this picks *which event* to publish and the harness
|
||||||
/// left behind by a subscriber that exited can be reclaimed.
|
/// frames it once for the whole fan-out — the mapping from device to class is the only part
|
||||||
const Subscriber = struct {
|
/// of a broadcast that is this service's own.
|
||||||
used: bool = false,
|
|
||||||
endpoint: ipc.Handle = 0,
|
|
||||||
task_id: u32 = 0,
|
|
||||||
/// Which device classes this subscriber wants (an OR of input_protocol.device_*). An event
|
|
||||||
/// is delivered only if its device's bit is set here.
|
|
||||||
device_mask: u32 = 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
var subscribers = [_]Subscriber{.{}} ** 8;
|
|
||||||
|
|
||||||
/// Drop any subscriber whose owning process is no longer alive, so its slot (and the
|
|
||||||
/// endpoint reference it holds) can be reused. Cheap and only run on subscribe — the async
|
|
||||||
/// `send` to a dead subscriber's orphaned endpoint is harmless (it just fills a queue no
|
|
||||||
/// one drains), so this is housekeeping, not correctness.
|
|
||||||
fn pruneDeadSubscribers() void {
|
|
||||||
var table: [32]process.ProcessDescriptor = undefined;
|
|
||||||
const total = process.processes(&table);
|
|
||||||
const count = @min(total, table.len);
|
|
||||||
for (&subscribers) |*sub| {
|
|
||||||
if (!sub.used) continue;
|
|
||||||
var alive = false;
|
|
||||||
for (table[0..count]) |descriptor| {
|
|
||||||
if (descriptor.id == sub.task_id) {
|
|
||||||
alive = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// The slot owns the endpoint capability it was handed, so reclaiming the
|
|
||||||
// slot closes it — otherwise a process that subscribes and dies costs a
|
|
||||||
// handle-table slot that never comes back.
|
|
||||||
if (!alive) {
|
|
||||||
_ = ipc.close(sub.endpoint);
|
|
||||||
sub.* = .{};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Register `endpoint` (owned by task `task_id`) to receive the device classes in
|
|
||||||
/// `device_mask`. Returns false if the subscriber table is full.
|
|
||||||
fn addSubscriber(endpoint: ipc.Handle, task_id: u32, device_mask: u32) bool {
|
|
||||||
for (&subscribers) |*sub| {
|
|
||||||
if (!sub.used) {
|
|
||||||
sub.* = .{ .used = true, .endpoint = endpoint, .task_id = task_id, .device_mask = device_mask };
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Push `event` to every subscriber whose interest mask includes its device class.
|
|
||||||
/// `ipc.send` never blocks, so a slow or dead subscriber cannot stall delivery to others.
|
|
||||||
///
|
|
||||||
/// The class is the packet's operation, so the fan-out picks the event by device and the
|
|
||||||
/// packet is framed once, outside the loop — every subscriber of a class gets identical
|
|
||||||
/// bytes, which is what "one fan-out point per event domain" means on the wire.
|
|
||||||
fn broadcast(event: input_protocol.InputEvent) void {
|
fn broadcast(event: input_protocol.InputEvent) void {
|
||||||
const class = input_protocol.eventOfDevice(event.device) orelse return; // no class wants it
|
const class = input_protocol.deviceBit(event.device);
|
||||||
var packet: [envelope.post_maximum]u8 = undefined;
|
switch (input_protocol.eventOfDevice(event.device) orelse return) { // no class wants it
|
||||||
const framed = switch (class) {
|
.keyboard => Subscriptions.publishClass(.keyboard, 0, event.asKeyboard() orelse return, class),
|
||||||
.keyboard => input_protocol.Protocol.encodeEvent(.keyboard, 0, event.asKeyboard() orelse return, &packet),
|
.mouse => Subscriptions.publishClass(.mouse, 0, event.asMouse() orelse return, class),
|
||||||
.mouse => input_protocol.Protocol.encodeEvent(.mouse, 0, event.asMouse() orelse return, &packet),
|
.joystick => Subscriptions.publishClass(.joystick, 0, event.asJoystick() orelse return, class),
|
||||||
.joystick => input_protocol.Protocol.encodeEvent(.joystick, 0, event.asJoystick() orelse return, &packet),
|
|
||||||
} orelse return;
|
|
||||||
|
|
||||||
const bit = input_protocol.deviceBit(event.device);
|
|
||||||
for (&subscribers) |*sub| {
|
|
||||||
if (sub.used and sub.device_mask & bit != 0) _ = ipc.send(sub.endpoint, framed);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set by `onSubscribe` when the subscriber table has taken ownership of the capability the
|
|
||||||
/// call carried, and read by `onMessage`, which is where the turn's `Arrival` lives. The
|
|
||||||
/// generated dispatch hands a handler the raw handle rather than the `Arrival` — deliberately,
|
|
||||||
/// since a handler has no business closing the turn's property — so the *claim* has to travel
|
|
||||||
/// back out this way. One turn, one handler, one thread: there is nothing here to race.
|
|
||||||
var capability_claimed = false;
|
|
||||||
|
|
||||||
/// The reserved `subscribe` verb: register the caller's endpoint (the call's capability) for
|
|
||||||
/// the classes in the packet's tail. Refusals simply return, and the turn closes what arrived
|
|
||||||
/// — the ownership rule the harness states (`ipc.Arrival`), unchanged by the move onto it.
|
|
||||||
fn onSubscribe(_: void, invocation: Invocation(void), _: Answer(void)) isize {
|
|
||||||
const endpoint = invocation.capability orelse return -envelope.EPROTO; // no endpoint passed
|
|
||||||
// A zero mask means "everything" (a subscriber that named no class still wants input).
|
|
||||||
const requested = input_protocol.decodeSubscribe(invocation.tail).device_mask;
|
|
||||||
const mask = if (requested == 0) input_protocol.device_all else requested;
|
|
||||||
pruneDeadSubscribers();
|
|
||||||
if (!addSubscriber(endpoint, invocation.sender, mask)) return -envelope.ENOSPC; // table full
|
|
||||||
capability_claimed = true; // the subscriber table holds it until that task dies
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn onPublish(_: void, invocation: Invocation(input_protocol.InputEvent), _: Answer(void)) isize {
|
fn onPublish(_: void, invocation: Invocation(input_protocol.InputEvent), _: Answer(void)) isize {
|
||||||
broadcast(invocation.request);
|
broadcast(invocation.request);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlers = Serve.Handlers{ .publish = onPublish, .subscribe = onSubscribe };
|
/// `subscribe` and `unsubscribe` are absent on purpose: the harness answers both.
|
||||||
|
const handlers = Subscriptions.Handlers{ .publish = onPublish };
|
||||||
|
|
||||||
fn onMessage(message: []const u8, out: []u8, sender: u32, arrived: *ipc.Arrival) usize {
|
fn onMessage(message: []const u8, out: []u8, sender: u32, arrived: *ipc.Arrival) usize {
|
||||||
capability_claimed = false;
|
return Subscriptions.dispatch({}, handlers, message, sender, arrived, out);
|
||||||
const written = Serve.dispatch({}, handlers, message, sender, arrived.peek(), out);
|
|
||||||
if (capability_claimed) _ = arrived.take();
|
|
||||||
return written;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn initialise(_: ipc.Handle) bool {
|
fn initialise(_: ipc.Handle) bool {
|
||||||
|
|
@ -162,5 +81,6 @@ pub fn main() void {
|
||||||
.service = "input",
|
.service = "input",
|
||||||
.init = initialise,
|
.init = initialise,
|
||||||
.on_message = onMessage,
|
.on_message = onMessage,
|
||||||
|
.subscribers = Subscriptions.hooks,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -658,6 +658,18 @@ CASES = [
|
||||||
"timeout": 150,
|
"timeout": 150,
|
||||||
"expect": r"fat-test: rename ok",
|
"expect": r"fat-test: rename ok",
|
||||||
"fail": r"fat-test: mutations FAILED|DANOS-TEST-RESULT: FAIL"},
|
"fail": r"fat-test: mutations FAILED|DANOS-TEST-RESULT: FAIL"},
|
||||||
|
# P4c: badge-scoped per-client ids. Two processes of one fixture, on the same
|
||||||
|
# boot: the owner holds a FAT node id and a compositor layer id, the intruder
|
||||||
|
# names both and must be refused — while its own node and layer, and the
|
||||||
|
# owner's after the attempt, keep working. Every refusal is paired with a
|
||||||
|
# control, so a provider that refused everything (or honoured everything)
|
||||||
|
# fails this case rather than passing it. Reuses the fat-mount build.
|
||||||
|
{"name": "badge-scope",
|
||||||
|
"build_case": "fat-mount",
|
||||||
|
"smp": 4,
|
||||||
|
"timeout": 150,
|
||||||
|
"expect": r"(?s)(?=.*badge-scope-test: ok)(?=.*badge-scope-test: owner intact)",
|
||||||
|
"fail": r"badge-scope-test: FAILED|DANOS-TEST-RESULT: FAIL"},
|
||||||
# Phase 2d: filesystem timestamps — a freshly-created file's mtime is a real
|
# Phase 2d: filesystem timestamps — a freshly-created file's mtime is a real
|
||||||
# current wall-clock time (stamped from the RTC), read back through stat.
|
# current wall-clock time (stamped from the RTC), read back through stat.
|
||||||
{"name": "fat-mtime",
|
{"name": "fat-mtime",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,216 @@
|
||||||
|
//! test/system/services/badge-scope-test — the guessable-id probe. Two providers
|
||||||
|
//! hand small integers to a client and then take them back from anyone who says
|
||||||
|
//! the number: the FAT server's node ids (a table of thirty-two) and the
|
||||||
|
//! compositor's layer ids (a table of sixteen). P4c scoped both to the badge that
|
||||||
|
//! opened them (docs/os-development/protocol-namespace.md — *handles must be
|
||||||
|
//! scoped per client, validated against the badge*), and this fixture is the
|
||||||
|
//! proof.
|
||||||
|
//!
|
||||||
|
//! It runs as two processes of the same binary, because the hole is a
|
||||||
|
//! *cross-client* one and a single process cannot demonstrate it:
|
||||||
|
//!
|
||||||
|
//! - the **owner** (no arguments — the scenario spawns this one) opens a file
|
||||||
|
//! on the volume and creates a layer, keeps both, and spawns
|
||||||
|
//! - the **intruder** (the two ids as argv), which opens a file and a layer of
|
||||||
|
//! its own and then names the owner's.
|
||||||
|
//!
|
||||||
|
//! Every refusal is paired with the same operation on the intruder's own id, so
|
||||||
|
//! the case cannot pass against a provider that refuses everything; and the owner
|
||||||
|
//! re-uses both ids after the intruder has finished — including after its attempt
|
||||||
|
//! to *close* the file node — so the case cannot pass against a provider that
|
||||||
|
//! honoured the intruder and merely reported failure. The pass marker is
|
||||||
|
//! `badge-scope-test: ok` from the intruder plus `badge-scope-test: owner intact`
|
||||||
|
//! from the owner.
|
||||||
|
//!
|
||||||
|
//! What it cannot check: which id the intruder was refused *for*. A refusal is
|
||||||
|
//! deliberately identical to "no such id" — that is the discipline being tested —
|
||||||
|
//! so the fixture proves the boundary by construction (an id it was told about by
|
||||||
|
//! its parent, which is holding it) rather than by reading anything back.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const display = @import("display-client");
|
||||||
|
const fs = @import("file-system");
|
||||||
|
const ipc = @import("ipc");
|
||||||
|
const logging = @import("logging");
|
||||||
|
const process = @import("process");
|
||||||
|
const time = @import("time");
|
||||||
|
|
||||||
|
/// A scratch file on the volume, so the node the intruder tries to write through
|
||||||
|
/// is one nothing else reads. (A foreign write that *succeeded* would prove the
|
||||||
|
/// bug — it must not also damage the boot volume proving it.)
|
||||||
|
const held_path = "/volumes/usb/BADGE.TXT";
|
||||||
|
const held_contents = "held";
|
||||||
|
|
||||||
|
fn line(comptime format: []const u8, arguments: anytype) void {
|
||||||
|
var buffer: [192]u8 = undefined;
|
||||||
|
_ = logging.write(std.fmt.bufPrint(&buffer, format, arguments) catch return);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The fat server mounts /volumes/usb only after the whole USB storage chain is
|
||||||
|
/// up, and both instances race it.
|
||||||
|
fn waitForVolume() bool {
|
||||||
|
var tries: u32 = 0;
|
||||||
|
while (tries < 1400) : (tries += 1) {
|
||||||
|
if (fs.openDirectory("/volumes/usb")) |opened| {
|
||||||
|
var directory = opened;
|
||||||
|
directory.close();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
time.sleepMillis(50);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main(init: process.Init) void {
|
||||||
|
if (init.arguments.get(1)) |node_text| {
|
||||||
|
const layer_text = init.arguments.get(2) orelse {
|
||||||
|
_ = logging.write("badge-scope-test: FAILED (intruder without a layer id)\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
const node = std.fmt.parseInt(u64, node_text, 10) catch return;
|
||||||
|
const layer = std.fmt.parseInt(u32, layer_text, 10) catch return;
|
||||||
|
intrude(node, layer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
own();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- the owner ---------------------------------------------------------------
|
||||||
|
|
||||||
|
fn own() void {
|
||||||
|
if (!waitForVolume()) {
|
||||||
|
_ = logging.write("badge-scope-test: FAILED (/volumes/usb never became available)\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var held = fs.open(held_path, .{ .create = true, .truncate = true }) orelse {
|
||||||
|
_ = logging.write("badge-scope-test: FAILED (owner could not create its file)\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if ((held.writeAll(held_contents) orelse 0) != held_contents.len) {
|
||||||
|
_ = logging.write("badge-scope-test: FAILED (owner could not write its file)\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const layer = display.createLayer(240, 40, 32, 32, 3) orelse {
|
||||||
|
_ = logging.write("badge-scope-test: FAILED (owner could not create a layer)\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
_ = layer.fill(0, 0, 32, 32, display.color(0x20, 0x80, 0xC0));
|
||||||
|
line("badge-scope-test: owner holds node {d} and layer {d}\n", .{ held.node, layer.id });
|
||||||
|
|
||||||
|
// The intruder is told both ids outright: guessing them is not what is being
|
||||||
|
// tested (they are small integers — a prober would simply enumerate), and a
|
||||||
|
// fixture that had to search would be asserting on a race instead of on the
|
||||||
|
// rule.
|
||||||
|
var node_text: [24]u8 = undefined;
|
||||||
|
var layer_text: [12]u8 = undefined;
|
||||||
|
const arguments = [_][]const u8{
|
||||||
|
std.fmt.bufPrint(&node_text, "{d}", .{held.node}) catch return,
|
||||||
|
std.fmt.bufPrint(&layer_text, "{d}", .{layer.id}) catch return,
|
||||||
|
};
|
||||||
|
const endpoint = ipc.createIpcEndpoint() orelse {
|
||||||
|
_ = logging.write("badge-scope-test: FAILED (owner has no exit endpoint)\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
const intruder = process.spawnSupervised("badge-scope-test", &arguments, endpoint) orelse {
|
||||||
|
_ = logging.write("badge-scope-test: FAILED (could not spawn the intruder)\n");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
var receive: [8]u8 = undefined;
|
||||||
|
while (true) {
|
||||||
|
const got = ipc.replyWait(endpoint, &.{}, &receive, null);
|
||||||
|
if (got.isChildExit() and got.childProcessId() == intruder) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both ids must still be the owner's, and still work — the half of the proof
|
||||||
|
// the intruder cannot give, because a provider that had honoured its close or
|
||||||
|
// its destroy would have answered it exactly as one that refused.
|
||||||
|
held.seekTo(0);
|
||||||
|
var buffer: [16]u8 = undefined;
|
||||||
|
const read = held.read(&buffer) orelse 0;
|
||||||
|
const node_intact = read == held_contents.len and std.mem.eql(u8, buffer[0..read], held_contents);
|
||||||
|
const layer_intact = layer.fill(0, 0, 32, 32, display.color(0x20, 0xC0, 0x80));
|
||||||
|
if (node_intact and layer_intact) {
|
||||||
|
_ = logging.write("badge-scope-test: owner intact\n");
|
||||||
|
} else {
|
||||||
|
line("badge-scope-test: FAILED (owner lost its ids: node={} layer={})\n", .{ node_intact, layer_intact });
|
||||||
|
}
|
||||||
|
_ = layer.destroy();
|
||||||
|
held.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- the intruder -------------------------------------------------------------
|
||||||
|
|
||||||
|
fn intrude(foreign_node: u64, foreign_layer: u32) void {
|
||||||
|
if (!waitForVolume()) {
|
||||||
|
_ = logging.write("badge-scope-test: FAILED (/volumes/usb never became available)\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const node_verdict = probeNode(foreign_node);
|
||||||
|
const layer_verdict = probeLayer(foreign_layer);
|
||||||
|
if (node_verdict and layer_verdict) {
|
||||||
|
_ = logging.write("badge-scope-test: ok\n");
|
||||||
|
} else {
|
||||||
|
_ = logging.write("badge-scope-test: FAILED\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The FAT half: the intruder's own node works, the owner's does not.
|
||||||
|
fn probeNode(foreign: u64) bool {
|
||||||
|
var mine = fs.open(held_path, .{}) orelse {
|
||||||
|
_ = logging.write("badge-scope-test: FAILED (intruder could not open its own file)\n");
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
defer mine.close();
|
||||||
|
if (mine.backend == null or mine.node == foreign) {
|
||||||
|
line("badge-scope-test: FAILED (intruder's node {d} is not distinct from {d})\n", .{ mine.node, foreign });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer: [16]u8 = undefined;
|
||||||
|
const own_read = (mine.read(&buffer) orelse 0) == held_contents.len;
|
||||||
|
|
||||||
|
// The same backend channel, the same verbs, one different integer. Every one
|
||||||
|
// of these worked before the owner check went in.
|
||||||
|
var forged = fs.File{ .node = foreign, .backend = mine.backend };
|
||||||
|
const read_refused = forged.read(&buffer) == null;
|
||||||
|
const status_refused = forged.attributes() == null;
|
||||||
|
const write_refused = forged.write("intruded") == null;
|
||||||
|
forged.close(); // must not release the owner's node — the owner proves that after we exit
|
||||||
|
|
||||||
|
mine.seekTo(0);
|
||||||
|
const own_read_again = (mine.read(&buffer) orelse 0) == held_contents.len;
|
||||||
|
|
||||||
|
const ok = own_read and own_read_again and read_refused and status_refused and write_refused;
|
||||||
|
line("badge-scope-test: node own={} read_refused={} status_refused={} write_refused={} own_again={}\n", .{
|
||||||
|
own_read, read_refused, status_refused, write_refused, own_read_again,
|
||||||
|
});
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The display half: the intruder's own layer obeys it, the owner's ignores it.
|
||||||
|
fn probeLayer(foreign: u32) bool {
|
||||||
|
const mine = display.createLayer(200, 40, 32, 32, 4) orelse {
|
||||||
|
_ = logging.write("badge-scope-test: FAILED (intruder could not create a layer)\n");
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if (mine.id == foreign) {
|
||||||
|
line("badge-scope-test: FAILED (intruder's layer {d} is not distinct)\n", .{mine.id});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const own_fill = mine.fill(0, 0, 32, 32, display.color(0xC0, 0x40, 0x40));
|
||||||
|
const own_configure = mine.configure(200, 80, 4, true);
|
||||||
|
|
||||||
|
const forged = display.Layer{ .id = foreign };
|
||||||
|
const fill_refused = !forged.fill(0, 0, 32, 32, display.color(0xFF, 0, 0));
|
||||||
|
const configure_refused = !forged.configure(0, 0, 9, false);
|
||||||
|
const destroy_refused = !forged.destroy();
|
||||||
|
|
||||||
|
const own_still = mine.fill(0, 0, 32, 32, display.color(0x40, 0xC0, 0x40));
|
||||||
|
_ = mine.destroy();
|
||||||
|
|
||||||
|
const ok = own_fill and own_configure and own_still and fill_refused and configure_refused and destroy_refused;
|
||||||
|
line("badge-scope-test: layer own={} fill_refused={} configure_refused={} destroy_refused={} own_again={}\n", .{
|
||||||
|
own_fill, fill_refused, configure_refused, destroy_refused, own_still,
|
||||||
|
});
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
//! The badge-scope-test fixture as a binary package (docs/build-packages-plan.md):
|
||||||
|
//! this file names the binary and EXACTLY the modules its source imports —
|
||||||
|
//! build-support resolves each name from the domains this zon declares.
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const build_support = @import("build-support");
|
||||||
|
|
||||||
|
pub fn build(b: *std.Build) void {
|
||||||
|
const exe = build_support.userBinary(b, .{
|
||||||
|
.name = "badge-scope-test",
|
||||||
|
.root_source_file = b.path("badge-scope-test.zig"),
|
||||||
|
.imports = &.{ "display-client", "file-system", "ipc", "logging", "process", "time" },
|
||||||
|
});
|
||||||
|
b.installArtifact(exe);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
.{
|
||||||
|
.name = .badge_scope_test,
|
||||||
|
.version = "0.0.0",
|
||||||
|
.fingerprint = 0x92ab15512c4e5b49, // Changing this has security and trust implications.
|
||||||
|
.minimum_zig_version = "0.16.0",
|
||||||
|
.dependencies = .{
|
||||||
|
// build-support supplies the shared recipe; kernel is implicit in
|
||||||
|
// every binary (the root shim + link script live there). The rest
|
||||||
|
// are exactly the homes of this binary's declared imports.
|
||||||
|
.@"build-support" = .{ .path = "../../../../build-support" },
|
||||||
|
.kernel = .{ .path = "../../../../library/kernel" },
|
||||||
|
// client: the compositor half of the probe talks to /protocol/display
|
||||||
|
// through the same client library every other display client uses.
|
||||||
|
.client = .{ .path = "../../../../library/client" },
|
||||||
|
},
|
||||||
|
.paths = .{""},
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue