git.lucas.co / cce-authenticator
login authentication (PAM + fingerprint)
git clone https://git.lucas.co/cce-authenticator.git

commitfdd1b3c402d083670fe76ffc2c6c2ee05fba6091
parent549e953d15
authorLucas Galante <[email protected]>
date2026-08-22 12:01
docs: the retry path is verified now, and record how without risking a lockout

CLAUDE.md still said the helper and retry paths were code-verified only. They were
verified against a live prompt this session, so that line was simply false — and
the technique that made it safe was nowhere written down.

The safe move is to kill the live helper rather than type a wrong password: the
process exits non-zero exactly as a rejected password makes it, so the retry path
runs identically with no authentication attempted and no exposure to faillock
(deny=3, unlock_time=600 — three wrong answers lock the account for ten minutes).

Also records how to find the helper at all, because both usual approaches fail
silently on it: the name is 22 characters so comm truncates to polkit-agent-he and
pgrep -x matches nothing, and it is setuid root so /proc/<pid>/exe is unreadable
and matching the exe link reports no helper while three are running. Enumerate the
agent's children and read state from /proc/<pid>/stat instead. Zombie count is
noted as a standing regression check for the leak fixed in 549e953.

Co-Authored-By: Claude Fable 5 <[email protected]>

 CLAUDE.md | 25 ++++++++++++++++++++++---
 1 file changed, 22 insertions(+), 3 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 5478a0e..830cbeb 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -88,9 +88,28 @@ and stranded dialogs. The crate's one test locks those orderings in.
   path there (2s success, 1s exit).
 - **Never live-test `pkexec` from the shadow session** — the D-Bus *system* bus is
   shared, so the prompt lands on the real screen.
-- **Never run `polkit-agent-helper-1` by hand.** It drives real PAM: it can light up
-  the fingerprint reader and trip `pam_faillock`, which has locked this machine out
-  before. The helper and retry paths are consequently code-verified only.
+- **A real prompt is cheap and safe to raise: `pkexec true`.** Kill that client and
+  polkitd sends `CancelAuthentication`, which is how the cancel path gets exercised
+  end to end. `grim -g "<x>,<y> <w>x<h>"` (geometry from `ccectl windows`) captures
+  the dialog. Everything except a *successful* authentication can be verified this way.
+- **Do not test a failed attempt by typing a wrong password**, and never run
+  `polkit-agent-helper-1` by hand. Both drive real PAM: `deny=3` / `unlock_time=600`
+  in `faillock.conf` means three wrong answers lock the account for ten minutes, and
+  this machine has been locked out that way before. **Kill the live helper instead** —
+  the process exits non-zero exactly as a rejected password makes it, so the retry path
+  runs identically with no authentication ever attempted. Killing it three times walks
+  `RETRIES` down and should give three distinct helper pids, two `restarted helper`
+  lines, then `no attempts left` and no fourth spawn.
+- **Finding the helper defeats both usual tricks.** `polkit-agent-helper-1` is 22
+  characters, so `comm` truncates to `polkit-agent-he` and `pgrep -x` matches nothing;
+  it is setuid root, so `/proc/<pid>/exe` is unreadable and matching on the exe link
+  silently finds *no* helper while several are running. Enumerate the agent's children
+  — `/proc/$(systemctl --user show -p MainPID --value cce-polkit-agent)/task/<pid>/children`
+  — and read state from `/proc/<pid>/stat` to tell the live helper from reaped corpses.
+- **Zombie count is a standing regression check.** Cancel a few prompts and confirm no
+  child stays in state `Z`: killing a `Child` does not reap it, and taking it out of
+  the shared slot stops the reader thread from waiting on it, which leaked one zombie
+  per cancelled prompt for the life of the session until it was fixed.
 - Agent health: `systemctl --user status cce-polkit-agent`, and the journal should say
   `Successfully registered`. `RUST_LOG=info` (set by the unit) narrates every request.