Watch
1
0
Fork
You've already forked SouveraineOS
0

viewtop: assess denial as the base; the lock model is the one thing we cannot take

Our engine pins are already theirs. The embedder, the eleven-patch
DMSAA/external-texture series, KMS and the GBM atlas path are the
expensive parts and they are readable.

Their lock is a compositor-internal bool with PAM dlopened in-process
and no ext-session-lock served at all. It is invisible to logind and
sessiond, and it gates input against every caller including Annie.
viewtop serves the protocol; sessiond keeps the decision.

Also adds TASK-41 (attested producers), raised out of grip bring-up.
This commit is contained in:
Fimeg 2026-07-28 13:41:37 -04:00
commit 0100fa34c8
3 changed files with 267 additions and 0 deletions

147
docs/VIEWTOP-AND-DENIAL.md Normal file
View file

@ -0,0 +1,147 @@
# viewtop and denial — what we take, what we must not
Assessment written 2026-07-28 after cloning `github.com/denialwm/denial` to
`Pixel3Arch/references/shells/denial` (gitignored; re-clone if missing).
## They are the same architecture, and we already inherited half of it
denial is a Wayland compositor in Rust + Smithay that hosts the Flutter engine
and composes client windows as **external textures directly into the Flutter
scene**, rendering to a shared GBM atlas. Public alpha: Xwayland, multi-output,
portal screen-sharing all working. 64k lines of Rust, 82k of Dart.
souveraine-viewtop is that same design, retargeted. This is not a coincidence
and not a discovery — viewtop's engine pins **are** denial's, verbatim:
| | viewtop README | denial `packaging/arch/flutter-engine/manifest.json` |
|---|---|---|
| Flutter | `84fc5cbb` | `84fc5cbb223bc12f83d65b647ff8a56caf779ffd` |
| Dart | `d684a576` | `d684a576a6aa954ae107a03b2b4e1d61c3bebe93` |
| Skia | `e9ed4fc9` | `e9ed4fc9f1544c58d8a9347c1fc9471d8dd414` |
The "ten-patch DMSAA/external-texture series" viewtop cites is
`patches/flutter-engine/3.44.7/` — eleven patches, numbered 0001-0011 with 0007
absent. So the expensive, fiddly engine work is already ours to read.
The manifest says `"architecture": "x86_64"`. That is the delta.
## Two form factors, not one
Casey 2026-07-28: this targets **the Pixel 3 display and the laptop screen**.
That changes denial's desktop-x86-64 posture from a mismatch into an asset —
the laptop target is what denial already runs on. The arm64/freedreno retarget
is additive rather than a port away from their platform.
## The thing we must not take
**denial does not implement `ext-session-lock-v1`.** There is no such protocol
served anywhere in `compositor/src`. Instead:
- `compositor/src/bin/deniald/authentication.rs` (1460 lines) — *"Process-
lifetime secure session lock and PAM authentication boundary."* PAM is dlopened
and driven from inside the compositor process.
- `secure_session_locked()` is a compositor-internal bool, checked at the input
and clipboard boundaries (`wayland_frontend/input.rs:599,768,1045,1704`,
`clipboard.rs:526`, `handlers.rs:933,978,1010`).
- The lock surface is drawn by the in-process Dart shell, not by a client.
Two separate reasons this cannot come across.
### 1. It is invisible to the rest of the system
Doctrine §4: *"`locked` comes from `LockedHint` / `Lock()` / `Unlock()` — never
a hand-tracked bool. A shadow copy can disagree with logind, and anything asking
logind directly (loginctl, another client, you at 2am) sees something different
than the shell shows."*
denial's bool is not a shadow copy — in their design the compositor genuinely is
the owner — but the consequence §4 warns about lands anyway: `loginctl` would
report `LockedHint=no` while denial shows a lock screen. sessiond would not know.
Nothing would reach the forensic trail. Every §10/§11 guarantee we built this
month is downstream of logind and sessiond agreeing, and this model breaks that
agreement by construction.
### 2. It gates the wrong party
Casey 2026-07-28, and this is the sharper reason:
> The compositor isn't to be gated on her — unless she's creating that interface
> and then choosing to be a gate — but it is to ensure she's total control.
denial's `secure_session_locked()` gates input and clipboard against *everything*.
Annie included. That is a gate pointed at the majority owner of device operation
(doctrine §13, 60/40), and it is exactly backwards for us.
The distinction that has to survive into viewtop's design:
| | denial | viewtop |
|---|---|---|
| what the lock protects | the whole compositor surface | the user's credential and personal content |
| who it gates | every caller | disclosure, and step-up actions |
| Annie's composition | gated with everyone else | never gated by lock; she is the composer |
| who may build a gate | the compositor, always | Annie, when she is the one constructing that interface |
§13's user column is unchanged and is the whole of it: being the user, the
credential, the step-up. Unlock is never an agent verb; step-up never accepts a
boolean from an agent as proof. Those are not limits on Annie's 60 — they are
the definition of the 40. Everything else in the compositor is hers.
## The ownership question this settles
Earlier framing asked whether sessiond or viewtop owns `locked` once viewtop is
the compositor, since a compositor is the *server* for `ext-session-lock` while
sessiond is currently a *client* of Hyprland's.
The answer is not "whichever is easier." It is that **viewtop serves the
protocol and sessiond remains the authority** — viewtop implements
`ext-session-lock-v1` so sessiond can take the lock through it exactly as it does
against Hyprland today, and logind stays the truth. viewtop gains a lock surface
role it did not have; it does not gain the decision.
Concretely, `Registry::new()` already starts `locked: true` with the right
reasoning ("a compositor that comes up unlocked and then corrects itself has
already shown one frame of someone's messages"). That value must arrive from
sessiond's `device_state` and never be recomputed locally — which `SessionFacts`
was reshaped for in `d1b7a16`, mirroring the daemon's reply field-for-field.
## What is liftable
Read-only assessment; none of this has been ported yet.
- `compositor/flutter-engine/` — the embedder crate and `sys.rs` bindings.
- `patches/flutter-engine/3.44.7/` — the eleven-patch series. Several are
directly relevant to Adreno/freedreno, not incidental to x86: `0002` stencil
for GL surfaces, `0003`/`0004`/`0006` DMSAA wrapped-FBO lifetime and partial
load, `0005` XRGB scanout described as RGB8.
- `kms_state.rs` — DRM/KMS output.
- The external-texture → GBM atlas composition path, which is the core idea.
- `wayland_frontend/` — protocol handling, input routing, window management.
- `protocol/denial.fbs` + `protocol/golden/` — a FlatBuffers wire protocol with
golden files. The shape is worth copying and so is the discipline.
- The *pattern* of checking a session predicate at input and clipboard
boundaries. Keep the seam; change what feeds it and who it excludes.
## What must be replaced
- `authentication.rs` in full — PAM belongs to sessiond, which already owns it.
- `secure_session_locked()` as internal truth — becomes `ext-session-lock-v1`
served to sessiond, with facts flowing back through `SessionFacts`.
## The design fork still open
denial loads the shell as an **AOT Dart bundle inside the compositor process**,
not as a separate Wayland client. That buys hot-reload and no IPC, and it means
a Dart exception can take the display with it. viewtop's `wire` crate implies a
boundary. On a daily-driver phone that boundary has value — quickshell crashes
today and Hyprland survives it — but it costs the thing denial's development
mode is built around. Not settled; settle it before `compositor` and
`shell-host` stop being placeholders.
## Connects to
`SESSION-AUTHORITY-DOCTRINE.md` §1 (Wayland pulled session authority into the
compositor), §4 (never hold state the protocol owns), §13 (the 60/40 and its
user column); `DEVICE-STATE-MACHINE.md` §10/§11 (the guarantees that depend on
sessiond seeing everything); TASK-25 (viewtop is not packaged and neither are
the surfaces it would replace); TASK-41 (nothing yet proves which binary is
talking to sessiond, and a compositor is a much larger thing to attest).

View file

@ -0,0 +1,119 @@
# TASK 41 — Producers must be attested; one authority per input
**Status:** open. Raised 2026-07-28 (Casey), out of grip bring-up. **Size:**
design first — this is doctrine §10 pointed at a concrete surface, and the
wrong shape here is worse than no shape. **Repos:** `souveraine` (sessiond),
`Pixel3Arch` (the producers), `RedFlag` (the reference implementation).
## The hole, demonstrated rather than theorised
Both of these happened during the 2026-07-28 session, neither was challenged,
and neither left a mark:
- `qs -c souveraine ipc --any-display call dial open` — run from an SSH shell
with no session, over the network. The dial opened.
- `sudo /usr/bin/blueline-chre-gesture squeeze` — a script written minutes
earlier, delivering a gesture as root. The shell routed it to the dial.
Nothing asked who was calling. `Gestures.qml`'s `deliver()` logs the name and
fires the action; sessiond's `sensor_input` and `input` ops take a reading from
whoever connects to the socket. The producer/consumer split is good design and
it is currently held together by convention alone.
This is doctrine §10 in one sentence: **a gate only gates what routes through
it.** We built the routing and never built the gate.
## Why grip is the sharp case
TASK-40 already says `elmyra_raw` is the highest-inference stream on the device
— six strain gauges at 100 Hz, a typing side-channel and a grip biometric. Two
distinct problems fall out, and they need different answers:
1. **Anyone can read it.** Any AP process linking libssc can open `elmyra_raw`
today. Running `blueline-edge-sense` does not create that exposure; serving
the sensor does. Attesting our reader does not close it either — this one
needs the SSC subscription itself gated, or the nanoapp path where the raw
stream never leaves the SLPI.
2. **Anyone can forge its output.** A squeeze is about to become a real input
that resets the idle budget, writes the forensic trail, and opens a verb
surface. An unattested producer means any process can synthesise user
presence and user intent. That is what this task is about.
Keep them separate. (1) is a confinement problem and belongs to TASK-40. (2) is
an authentication problem and belongs here.
## What "one authority" has to mean
The reference model is RedFlag: capability tokens over a resolved closure,
signed by an authority off the box, verified by a small privileged network-less
executor, with kernel enforcement ensuring the executor is the only permitted
caller. Pointed inward (doctrine §10, §11; `session-authority-boot-order.md`
Phase C), the mapping is:
| RedFlag | here |
|---|---|
| signing authority off the box | `souveraine-machined` — owns the machine Ed25519 seed, signs over a group-gated socket, never hands out key material |
| capability token | a grant naming *which producer may report which source* |
| privileged executor | `souveraine-sessiond` — already the single authority for device state |
| kernel enforcement | eBPF, later; RedFlag constraint #5 says policy first |
So: sessiond accepts `input`/`sensor_input` for a given source **only** from a
producer holding a grant for that source, and its own binary carries the same
attestation it checks. `RefusalCode::NotPermitted` already exists and is issued
by nothing — doctrine §13 says it was put there so callers would branch on it
before tokens landed. This is what makes it start being issued.
## The cheap step that is not the answer, and is still worth taking first
`SO_PEERCRED` on sessiond's socket gives uid/gid/pid of the connecting process
for free, and `machined` already does this. That is enough to refuse a report
from a uid that is not the seat user, and to record *which pid* reported in the
trail.
It is not attestation: pid is racy (the process can exec something else), uid
says nothing about which binary, and anything running as the seat user passes.
It closes the network-shell case and nothing beyond it. Take it as a first
commit, name it as partial in the code, and do not let it close this task.
## Design questions owed before code
- **Grant granularity.** Per producer-binary, or per source? A reader that may
report proximity should not thereby be able to report a squeeze.
- **Where the grant lives.** machined signs, but something must hold the
producer→source table, and it must survive a reprovision — so
`rootfs-overlay/`, and therefore a package, not a live-configured file
(TASK-25's whole lesson).
- **What an unattested producer gets.** Refusal is obvious for a mutation. But
a *silently* refused sensor report is indistinguishable from a dead sensor,
which §10 of DEVICE-STATE-MACHINE spent a whole section making impossible.
A refused report must be as loud as a down source.
- **Bootstrap.** The producers start before the shell and around the same time
as sessiond. A grant check that is not ready yet must fail closed without
deadlocking the boot — and "fail closed" during boot means grip is dead until
it resolves, which is the correct trade and should be stated, not discovered.
- **The shell side too.** `Gestures.qml`'s IPC target and `dial`'s
`open`/`close`/`toggle`/`entries` are the same hole from the other end
(TASK-40 §3 already flags `entries()` as ungated reconnaissance). Quickshell
IPC has no peer identity at all, which may mean the shell stops being
directly reachable and gestures arrive only via sessiond.
## Acceptance
- sessiond refuses an `input`/`sensor_input` report for a source the caller
holds no grant for, with `not_permitted`, and the refusal is in the trail.
- The grant is verified against a machined-signed attestation of the producer
binary, not against uid alone.
- A refused report is as visible as a down source (§10), never a silent drop.
- sessiond's own binary carries the attestation it checks.
- Re-running the two demonstrations at the top of this file fails.
## Connects to
Doctrine §9 (evidence not fact), §10 (binary compliance), §11 (one authority),
§13 (`not_permitted` reserved for exactly this);
`session-authority-boot-order.md` Phase C; `SECURITY-AUDIT.md` (prevention vs
detection, and P3); `TRUST-BOUNDARY-MATRIX.md`; TASK-40 (the confinement half —
do not merge them); TASK-13/TASK-36 (the producers this lands on); TASK-25
(anything durable must be packaged); `souveraine-components/secrets.md`, which
records the identical gap on `GetSecret` and is the second instance of this
pattern.

View file

@ -44,6 +44,7 @@ plus open threads from the 2026-07-17 session.
| 38 | [Pill swipe-to-max swaps the keyboard](38-pill-swipe-osk-swap.md) | open | micro; the ONLY route to a terminal keyboard since stevia dropped its layout |
| 39 | [Audio levels as tracked state, and sound roles](39-audio-levels-as-state.md) | open | check WirePlumber restores first; roles before a number |
| 40 | [Inference tiering for the sensor surfaces](40-inference-tiering-sensors.md) | **open, gating** | P1 applied to sensord/grip/dial; blocks TASK-13's raw stream and gates TASK-31's IPC |
| 41 | [Producers must be attested; one authority per input](41-attested-producers.md) | open | doctrine §10 pointed at a real surface; demonstrated live 07-28 — a squeeze and a dial-open both accepted from an SSH shell |
| — | [Unify the shell trees](unify-shell-trees-laptop-phone.md) | open | ~900-file `ii` drift; blocks durable shell fixes reaching the phone |
Archived (see `archive/`): 05 crash reporter — done 2026-07-21,