login greeter
git clone https://git.lucas.co/cce-display-manager.git
CLAUDE.md (9.9K)
1 # CLAUDE.md
2
3 > This is the `cce-display-manager` crate, inside the larger **`cce` Cargo
4 > workspace** — read `../cce-compositor/WORKSPACE.md` first for the multi-repo
5 > layout, the standalone-build rule, `ccebuild`, and the `cce-ui` toolkit. This
6 > file covers only what is specific to this crate. `README.md` is the
7 > user-facing account (keys, install, logs, the keyring chain); read it too.
8
9 `cce-display-manager` **is the login path.** A mistake here is not a broken
10 window: it is a machine the user cannot log into, and the usual tools for
11 finding out why (their session) are the thing that is missing. Every change
12 here gets verified by a REAL login before it is called done, and every
13 install leaves the rollback in place (see "Installing and verifying").
14
15 The whole crate is `src/main.rs`. One binary, four roles, each its own process,
16 chosen by argv in `main`:
17
18 - **daemon** (no args, root, the unit's `ExecStart`) — owns the tty, runs the
19 resume watchdog thread, and loops: greeter → session → greeter.
20 - **greeter** (`--greeter`, root, under `cage -s`) — the `cce-ui` login screen.
21 Checks the password (PAM `cce-display-manager-password`, authenticate +
22 acct_mgmt only — see "No PAM session in the greeter") and prints one line on
23 stdout for the daemon: `AUTH_SUCCESS|user|exec|is_wayland|password`
24 (`auth_success_line` / `parse_auth_success`; the password is the LAST field,
25 taken verbatim to end of line, because it may contain `|`).
26 - **fingerprint helper** (`--fprint-auth <user>`) — one attempt on the
27 `cce-display-manager-fprint` stack, in a separate PROCESS because
28 `pam_fprintd` blocks uninterruptibly and only killing the process makes
29 fprintd release the sensor. `PR_SET_PDEATHSIG` ties it to the greeter.
30 - **session worker** (`--session-worker <tty> <user> <is_wayland> <exec>`,
31 root) — opens the PAM/logind session, runs the session as the user, closes
32 PAM when it exits. Password on STDIN (never argv — world-readable in
33 /proc); it reports `SESSION_ID <id>` on stdout and then points stdout at
34 /dev/null.
35
36 ## The rules that keep logins working
37
38 **The session worker is a fresh process, never a `fork()` of the daemon**
39 (since 2026-09-25). The daemon is multi-threaded (the resume watchdog), and a
40 forked child inherits whatever locks another thread held at that instant; the
41 worker then does PAM, env writes, logging and spawns. That class of wedge froze
42 the GREETER's login on "Authenticating…" (2026-09-18: `pam_gnome_keyring`'s
43 auto_start forking out of the threaded Vulkan greeter). The daemon runs the
44 worker with `Command` and NO `pre_exec`, so only async-signal-safe work
45 happens between its fork and exec. Do not reintroduce `libc::fork` here, and do
46 not add a `pre_exec` to the daemon's spawns.
47
48 **No PAM session in the greeter.** The greeter authenticates only; the session
49 worker opens the real session. `open_session` in the greeter registered a
50 throwaway logind session under cage and ran session modules inside a threaded
51 Vulkan process — the freeze above.
52
53 **Hand the worker the password that was VERIFIED, verbatim.**
54 `State::auth_password` is set by each attempt (empty for a fingerprint) and is
55 what `AUTH_SUCCESS` carries. Two bugs lived here: the password was `trim()`ed
56 (a password with a leading/trailing space could never log in), and a
57 fingerprint success sent the password BOX's text, so a half-typed password
58 sent the worker down the password stack to fail a login the greeter had
59 accepted. An empty password means "already verified" to the worker
60 (`session_pam_service` → the autologin stack, `pam_permit`) — so the greeter's
61 word is the whole of a fingerprint login's authentication.
62
63 **End the logind session when the session is over** (`terminate_session`,
64 from the DAEMON after the worker exits — pam_systemd put the worker inside the
65 scope). With logind's `KillUserProcesses=no`, closing PAM only marks a session
66 `closing`; its leftover processes live on in the scope. Every login leaked that
67 way until 2026-09-25 (eight sessions stuck `closing`, 16 orphaned 1Password
68 helpers, 1.9 GB). Use **`kill-session`, not `terminate-session`**: once the
69 leader has exited, logind has abandoned the scope and TerminateSession does
70 NOTHING — no error, no journal line (measured). SIGTERM, poll up to 2 s for the
71 session to go, then SIGKILL; polled rather than timed on a thread, because the
72 next worker is spawned right after. This also runs on a compositor-restart
73 relaunch: nothing survives into the new session from the old scope today (the
74 new compositor starts its clients fresh); if clients ever reconnect ACROSS a
75 restart, they must be carried into the new session instead.
76
77 **One keyring provider: the TPM-sealed `gnome-keyring-daemon` unit** (README,
78 "Login keyring"). `pam_gnome_keyring` must stay OUT of the PAM stacks —
79 `no_pam_stack_starts_a_keyring` enforces it. In a stack it started a second
80 daemon at every login that could not unlock the keyring (its password is the
81 sealed random one) and raced the unit.
82
83 **Ask the CONTEXT which field has focus** (`focused_field`, by widget id —
84 the one record `set_focused` writes). `Adapted::focused(ctx)` ignores the
85 context and asks the wrapped widget's own flag, which a TextBox does not keep:
86 the greeter's Tab and Enter asked it until 2026-09-25, it never matched, so
87 Tab went one way only and Enter did nothing in either field. The first
88 frame's focus comes from `initial_focus` for the same reason.
89
90 **The runner reaches the widgets through `ui_context()`**, which the greeter
91 must implement. Before each frame the runner shapes every registered widget
92 (`prepare_text`), and that is what records a TextBox's glyph positions;
93 without the hook none were recorded and the caret fell back to a column grid
94 off an inked-width estimate, drifting off the typed text.
95
96 **F1 / F2 run `systemctl poweroff` / `reboot`** (the greeter is root). Never
97 press them in a shadow greeter run as yourself — polkit lets an active local
98 session power off unasked, so it would take the machine down. Put a fake
99 `systemctl` first on the greeter's PATH instead; that is how they were
100 tested. A power key bumps `auth_request_id` like any new attempt, or the
101 fingerprint scan it cancels reports "helper exited" over its status line.
102
103 ## Where things go
104
105 - **Session output** → `/run/user/<uid>/cce-session.log` (previous one `.old`),
106 opened in the session command's `pre_exec` AS THE USER — a root open in a
107 user-owned directory could be symlinked at any file. The Bash console
108 session (`CONSOLE_SESSION_EXEC`) keeps the tty instead. It used to inherit the
109 daemon's stdout: a world-readable root log, truncated at every daemon start.
110 - **Daemon / greeter / cage / worker logs** → the journal. The units set
111 `StandardOutput/StandardError=journal`; the daemon skips its old
112 `/var/log/cce-display-manager-<tty>.log` only when `JOURNAL_STREAM` says
113 systemd connected it, so a new binary under an old unit (or an F5 re-exec of
114 a daemon an old unit started) still logs to the file. `JOURNAL_STREAM` is
115 removed from the session's environment.
116 - **Persistent state** → `/var/lib/cce-display-manager/last_user`,
117 `last_session`. Runtime → `/run/cce-display-manager-<tty>/` (the greeter's
118 `XDG_RUNTIME_DIR`, 0700).
119 - **Compositor restart** → `ccectl restart-compositor` writes
120 `/tmp/cce-restart-requested-<user>` and exits; the daemon relaunches the
121 same session on the autologin stack. The flag is honored only as a regular
122 file owned by that user (`symlink_metadata`), since anyone can create names
123 in /tmp.
124
125 **The unit restarts the daemon after any exit** (`Restart=always`, since
126 2026-09-25). Ctrl+C at the greeter exits it (130 from the greeter → the daemon
127 `exit(0)`), which left no login screen until a reboot. A daemon that dies
128 mid-session has already ended the session — it leads tty1's session, and the
129 compositor and `startcce`, in its foreground process group, take the kernel's
130 SIGHUP with the default action (checked in `/proc/<pid>/status`) — so a
131 restart then cannot put a greeter beside a live session. If the compositor
132 ever starts ignoring SIGHUP, that reasoning has to be redone.
133
134 ## Installing and verifying
135
136 The login runs `/usr/bin/cce-display-manager`, the units in
137 `/etc/systemd/system` and the stacks in `/etc/pam.d` — all installed by
138 **`ccebuild install-system`** (root, backs up to `*.bak-<date>`), never by
139 `ccebuild install`, which only puts the keyring helper scripts (needed: the TPM
140 drop-in runs one) and an unused copy of the binary in `~/.local/bin`.
141 `make install` runs both. `ccebuild install-system --dry-run` shows what would
142 change, and is how to check a build actually differs from what is installed.
143
144 **A new binary does not reach the running DAEMON.** Each login starts the
145 greeter from `/usr/bin`, but the daemon keeps the code it started with (its
146 `/proc/<pid>/exe` reads `(deleted)`): F5 at the greeter re-execs it from
147 `/usr/bin`, a reboot restarts it. So daemon-side changes (the session worker,
148 session termination) need a logout + F5 + login to be tested at all. Do NOT
149 restart `cce-display-manager@tty1` from inside a session — it owns the tty the
150 session is on.
151
152 What the suite covers (`cargo test -p cce-display-manager`): the line
153 protocols, arg building/parsing, PAM service choice, session-id validation,
154 that no stack starts a keyring, and (`tests/session_worker.rs`, the real
155 binary) that `--session-worker` refuses a non-root caller and a malformed argv.
156 It cannot cover PAM, logind or the handoff — those need root and a seat. Verify
157 on a real login, reading `journalctl -u cce-display-manager@tty1 -b` (or the
158 /var/log file, see above), `loginctl list-sessions`, and the session log.
159 Rollback is Ctrl+Alt+F2 (logind's auto-VTs), restore the `.bak`, reboot.
160
161 ## Known gaps
162
163 - `WLR_DRM_DEVICES=/dev/dri/card1:/dev/dri/card0` for the greeter's cage is
164 this machine's card numbering, hard-coded; `startcce` pins card1 the same way.
165 - `busctl monitor` (the resume watchdog) and `chvt` / `loginctl` are shelled
166 out to, deliberately — a D-Bus crate would widen a root binary's
167 dependencies for three calls.