175 lines
10 KiB
Markdown
175 lines
10 KiB
Markdown
# Session authority — boot order and crash survival
|
|
|
|
Companion to `SESSION-AUTHORITY-DOCTRINE.md` (§11) and
|
|
`SESSION-TRUST-ARCHITECTURE.md`. Scope: the lock surface must exist before any
|
|
other surface at boot, and the lock must outlive the shell process. Written
|
|
2026-07-16 after the boot where an NM password dialog rendered before the
|
|
lockscreen.
|
|
|
|
**Status: Phase C implemented 2026-07-16** (Casey's call: skip A/B, build the
|
|
authority). `souveraine 856c6e5` — `souveraine-sessiond` (feature `sessiond`),
|
|
`src/sessiond/{protocol,auth,draw,lock,server}.rs`, plus the shell-side
|
|
`SessiondBridge.qml` and the `LockScreen.initIfReady` handshake.
|
|
`Pixel3Arch 7a85376` — pam.d service, user unit, hook start line,
|
|
`allow_session_lock_restore`. The handoff uses the abandon+restore trick: a
|
|
lock client that drops its connection WITHOUT unlocking leaves the compositor
|
|
holding the session locked, and the restore flag lets the next client inherit
|
|
it — so sessiond→shell handoff and shell-crash→sessiond retake both pass
|
|
through zero unlocked instants. NOT yet deployed to the phone (was
|
|
unreachable); deploy checklist below. Phases A/B remain skipped, except the
|
|
restore flag and splash timing which Phase C absorbed (sessiond pokes
|
|
splash-signal from its own first frame).
|
|
|
|
## What actually happens at boot today
|
|
|
|
1. `souveraine-splash` takes DRM master (`Before=greetd.service`) and covers
|
|
the panel. It is **visual cover only** — its 30 s watchdog fails *open*
|
|
(fades and hands off regardless of whether a lock exists).
|
|
2. Hyprland starts under the splash. Whether libinput events reach (invisible)
|
|
surfaces while the splash still holds DRM is unverified — worth one test,
|
|
because if they do, the pre-lock window is interactive, not just visible.
|
|
3. `hyprland.start` hook: wallpaper, dt2w, hypridle restart, `hyprpm reload`
|
|
(+ sleeps), then `sleep 3 && qs -c souveraine`. The shell arrives seconds
|
|
after the compositor, unsupervised (raw exec child, no restart).
|
|
4. Inside the shell, **every panel activates on `Config.ready`**, but the Lock
|
|
panel additionally waits for `Persistent.ready` before `initIfReady()`
|
|
requests the session lock (`LockScreen.qml`). Between those two gates, any
|
|
other surface — bar, background, the NetworkManager secret-agent dialog —
|
|
can render first. `GlobalStates.screenLocked` starts `false`: the shell
|
|
boots fail-open and locks after the fact.
|
|
5. `splash-signal` fires when the lock is *requested*
|
|
(`onScreenLockedChanged`), not when the compositor acks (`secure`). The
|
|
1.2 s fade can beat the lock surface mapping. Same reading-vs-reality shape
|
|
the doctrine already bans (§4, §9).
|
|
|
|
## Crash truth table today
|
|
|
|
| Scenario | What happens |
|
|
| --- | --- |
|
|
| Shell dies while locked | Compositor keeps the session locked (ext-session-lock fail-closed) and shows the fallback screen. But `misc:allow_session_lock_restore` is **false** on the phone, so no new client may take over the dead client's lock — the device is a locked brick until someone ssh-es in. Nothing restarts the shell. |
|
|
| Shell dies while unlocked | No lock authority exists at all. `blueline-power-button` logs the IPC failure and blanks anyway (interim by design). |
|
|
| Shell restarts by hand while locked | `Persistent.states.lock.locked` re-locks it — but only because `allow_session_lock_restore` would permit the new lock, which it currently does not. Untested path. |
|
|
|
|
## Phase A — fail-closed ordering inside the shell
|
|
|
|
Small, current architecture, closes the observed boot hole.
|
|
|
|
1. **Hoist the Lock scope out of the panel family** into `shell.qml`, first
|
|
child, not behind the family's `LazyLoader`.
|
|
2. **Gate the panel family on the lock decision**: `SouveraineFamily` loads on
|
|
`Config.ready && lockResolved`, where `lockResolved` means `initIfReady()`
|
|
has run — either the session is locked (and ideally secure) or it decided
|
|
startup should be unlocked. No shell surface can precede the lock decision;
|
|
the NM dialog lands after unlock instead of before lock.
|
|
3. **Default-closed start on the phone.** `screenLocked` cannot read config at
|
|
instantiation (Config loads async). A synchronous signal is needed; an env
|
|
var in the launch line (`SOUVERAINE_LOCK_AT_START=1 qs …`) is the candidate
|
|
— `screenLocked` starts true when set, and `initIfReady()` demotes if the
|
|
persisted/config state says otherwise. Alternative: default true
|
|
everywhere and accept a lock-flash on every dev-loop shell restart on the
|
|
laptop. Decision below.
|
|
4. **`splash-signal` moves to the `secure` ack** (`screenLockSecureChanged`),
|
|
so the bloom dissolves onto a mapped lock surface, never a racing one.
|
|
5. **`misc:allow_session_lock_restore = true`** in hyprland.lua. One line;
|
|
prerequisite for every recovery story below and for the existing
|
|
Persistent re-lock path to work at all.
|
|
|
|
## Phase B — supervision
|
|
|
|
- The shell becomes a systemd user unit, same pattern as `hypridle.service`
|
|
(dies and respawns with the Hyprland instance; env already exported to the
|
|
user manager in `hyprland.start`). `Restart=on-failure`. The earlier
|
|
systemd-run failure was env stripping (`XDG_RUNTIME_DIR`); a real unit under
|
|
the user manager does not have that problem — verify on device before
|
|
trusting it.
|
|
- With Phase A ordering + the Persistent lock mirror + lock restore enabled,
|
|
a shell that crashes while locked self-restores to the lock surface with no
|
|
ssh intervention.
|
|
- The splash watchdog stays (nothing may hold DRM hostage), but by the time
|
|
it can fire, either the lock is up or the compositor is showing bare
|
|
wallpaper — not an unlocked desktop with dialogs.
|
|
|
|
## Phase C — the authority below the shell (`souveraine-sessiond`)
|
|
|
|
Not greenfield. This is the session-authority face of the unified authority
|
|
doctrine §11 already names — capability tiers, capability gate (RedFlag's
|
|
attested-binary / capability-token pattern pointed inward, §10), seed identity
|
|
as signer, memfs as audit trail. The souveraine services family this daemon
|
|
joins **already exists**: `souveraine-machined` owns the machine Ed25519 seed
|
|
and serves pubkey/sign over `/run/souveraine/machined.sock` (clients get
|
|
signatures, never key material); `souveraine-secrets` backs
|
|
`org.freedesktop.secrets` from the same seed lineage. sessiond is the next
|
|
binary in that family, and per §11 the session authority and the binary
|
|
authority are the same authority — sessiond verifies capability tokens on the
|
|
things that ask it to unlock/inhibit/step-up, and its own binary carries the
|
|
same attestation it checks (RedFlag constraint #5 applies: policy before
|
|
kernel stops; eBPF raises bypass cost later).
|
|
|
|
Shape:
|
|
|
|
- A small Rust daemon in the substrate (peer of `souveraine-machined` /
|
|
`souveraine-secrets`), systemd user unit ordered before the shell. Signing
|
|
needs go through machined's socket like every other service.
|
|
- **At session start it takes ext-session-lock immediately** — the lock
|
|
exists before the shell process does. The splash becomes purely aesthetic.
|
|
- It renders a **minimal** lock surface (spartan PIN entry) and owns the PAM
|
|
conversation. Enough to unlock a phone whose shell is a corpse.
|
|
- **Steady state stays shell-owned.** The shell remains the normal lock
|
|
client in-session (rich surface: swipe-to-unlock, widgets — the lockscreen
|
|
recreation queue item). sessiond intervenes exactly twice:
|
|
- *Boot*: holds the lock until the shell announces ready, then hands off
|
|
(one unlock→lock gap per boot, before any user interaction; mitigated by
|
|
doing the handoff while the splash still covers).
|
|
- *Crash while locked*: shell heartbeat drops, sessiond takes over the dead
|
|
client's lock (`allow_session_lock_restore`) and offers minimal unlock.
|
|
- sessiond is then the natural home for the rest of the authority surface:
|
|
the lock IPC endpoint (`blueline-power-button` talks to it instead of `qs
|
|
ipc`), the PrepareForSleep lock race (doctrine §8), the `souveraine-stepup`
|
|
PAM service, capability-token verification for session verbs (§10), and
|
|
eventually the portal Inhibit backend (§6).
|
|
- This lands inside the services-infrastructure review the security audit
|
|
register already queued (SECURITY-AUDIT.md "Next thread"): session
|
|
authority, secrets, federation transport, sensor service, machined —
|
|
reviewed as one composition, not five builds. sessiond's design belongs to
|
|
that review, not to a standalone lockscreen fix.
|
|
|
|
Open question, deliberately not answered here: does the rich lockscreen
|
|
eventually move *into* sessiond (lock surface never depends on shell health,
|
|
but widgets need shell data over IPC), or does the two-client
|
|
boot/crash-fallback model above stay permanent? The two-client model is
|
|
consistent with doctrine §4 and vastly cheaper; revisit only if the handoff
|
|
gap or heartbeat proves flaky in practice.
|
|
|
|
## Decisions needed
|
|
|
|
1. Phase A default-closed mechanism: env var in the phone launch line vs
|
|
lock-at-start everywhere (laptop dev-loop pays a lock-flash per restart).
|
|
2. Phase ordering vs the rest of the queue (favorites, lockscreen
|
|
recreation). Note lockscreen recreation and Phase C interact: where the
|
|
rich surface lives decides where that work lands — and Phase C itself
|
|
belongs to the services-infrastructure review.
|
|
|
|
## Deploy checklist (phone, when reachable)
|
|
|
|
1. On-device build: `cargo build --release --features sessiond --bin
|
|
souveraine-sessiond` in `~/build/souveraine` (nice 15, -j 3), install to
|
|
`/usr/local/bin`.
|
|
2. Sync Pixel3Arch rootfs-overlay bits: `/etc/pam.d/souveraine-sessiond`,
|
|
`/etc/systemd/user/souveraine-sessiond.service`; `systemctl --user
|
|
daemon-reload`.
|
|
3. Deploy hyprland.lua (restore flag + hook line) and `deploy.sh --phone`
|
|
for the shell (bridge + LockScreen patch).
|
|
4. Cold boot. Expected: splash → sessiond PIN surface (spartan) → shell lock
|
|
replaces it seamlessly once quickshell is up. `journalctl --user -u
|
|
souveraine-sessiond` shows acquire → handoff → locked_ack.
|
|
|
|
## Verification owed when implemented
|
|
|
|
- Boot with Phase A: no surface (including NM dialog) before the lock; splash
|
|
dissolves onto a mapped lock surface.
|
|
- `kill -9` the shell while locked → systemd restarts it → lock surface
|
|
returns without ssh (Phases A+B, restore flag on).
|
|
- Taps during the splash window: verify whether input reaches surfaces while
|
|
splash holds DRM master.
|
|
- Laptop path unchanged: SDDM boot, no lock-flash regression on shell
|
|
restart (unless decision 1 chooses lock-at-start everywhere).
|