Watch
1
0
Fork
You've already forked SouveraineOS
0
SouveraineOS/docs/session-authority-boot-order.md
Fimeg 341a2fe060 docs: take the lockscreen out of casey's uid
The current user unit wins the boot race but still runs as the human whose Personal key the lock is meant to evict. Record the target souveraine-session principal, its narrow Wayland/PAM/data reach, and the fact that the rich shell is presentation—not release authority.
2026-08-17 12:43:45 -04:00

198 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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).
**Ownership correction, 2026-08-17:** the zero-gap handoff below describes the
current implementation, not the final principal boundary. The rich shell is a
presentation client; it must not become the authority merely because it draws
the normal lock face. The target keeps lock policy and release under sessiond
running as dedicated `souveraine-session`, while Casey is only the PAM subject
and Personal-key owner. The living design is
`../../souveraine/saf/authority/01-session.md`.
## 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)
**Superseded 2026-08-15 — steps 13 below are the pre-packaging route and three
of them are now banned. Kept as the record of how Phase C first reached the
phone; do not follow them.** The current route:
1. `pacman -Syu` on the phone. sessiond, its unit and (since TASK-76) its PAM
file are all package-owned.
2. Cold boot, and step 4 below is still the expected sequence.
The original, and why each step retired:
1. ~~On-device build: `cargo build --release --features sessiond --bin
souveraine-sessiond` in `~/build/souveraine` (nice 15, -j 3), install to
`/usr/local/bin`.~~ Banned 2026-07-24 — `souveraine/CLAUDE.md`: binaries
hand-copied to `/usr/local/bin` are owned by no package and silently never
update; sessiond sat five days behind and its state machine never ran.
archdev cross-compiles, CI packages, pacman delivers.
2. ~~Sync Pixel3Arch rootfs-overlay bits.~~ The overlay reaches a device only
on flash and the phone does not flash (TASK-28). Both files are packaged
now: the unit from `PKGBUILD.prebuilt`, the PAM file added 2026-08-15.
The overlay copies are a shadowing hazard, not a delivery route (TASK-76).
3. ~~`deploy.sh --phone` for the shell.~~ `START-HERE.md` §5: never. TASK-25
demoted it to a recovery instrument for an explicitly quiesced target,
after it killed a live conversation mid-use.
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).