Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/docs/session-authority-boot-order.md

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).