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:
parent
6c5d39bac5
commit
0100fa34c8
3 changed files with 267 additions and 0 deletions
147
docs/VIEWTOP-AND-DENIAL.md
Normal file
147
docs/VIEWTOP-AND-DENIAL.md
Normal 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).
|
||||
119
docs/tasks/41-attested-producers.md
Normal file
119
docs/tasks/41-attested-producers.md
Normal 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.
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue