# Hyprlock: separate authentication from reader cleanup

A lock screen can authenticate correctly and still mishandle the lifetime of its authentication work. In September I traced two such problems in Hyprlock: synchronous fingerprint setup and cleanup delayed the Wayland event loop, while an unused password conversation could be cancelled after a successful fingerprint unlock and recorded as a failed authentication. My local correction moved device work off the critical path and deferred password authentication until actual password submission. It retained the password policy and fingerprint matching decision.

## Scope and research dates

The implementation work and successful-unlock checks were recorded on 20 September 2026. The follow-up investigation of the device cleanup path was recorded on 21 September. This article was assembled on 30 September from those records and a fresh comparison against Hyprlock v0.9.6. The fprintd and libfprint source versions in the investigation were v1.94.5 and v1.94.100.

The work was a local patch and diagnostic investigation. The available records establish neither an upstream submission nor a general authentication bypass. The useful security result is a clearer transaction boundary: a successful method should not accidentally generate failure accounting for another method the user never attempted.

## Three milestones that should not be conflated

I separated the lock request, successful authentication, and resource release. Each marks a different promise. A keybinding requests locking; the compositor acknowledgement establishes the lock; a successful authentication result authorizes unlocking; releasing the reader completes resource cleanup. Measuring process exit alone hides these distinctions.

The Wayland session-lock protocol makes the compositor responsible for hiding normal surfaces before reporting the session locked. It also requires the session to remain locked if the locker dies. These semantics are why I measured compositor acknowledgement rather than treating the appearance of the locker's process as proof of protection. [Wayland session-lock protocol](https://gitlab.freedesktop.org/wayland/wayland-protocols/-/blob/main/staging/ext-session-lock/ext-session-lock-v1.xml).

Once authentication succeeds, waiting for peripheral cleanup is a separate decision. Making that wait synchronous on a rendering/event loop can freeze the interface even though the identity decision is already complete. Conversely, moving it asynchronously must not turn a timeout or cleanup error into authentication success.

## The fingerprint path

In v0.9.6, creating the fingerprint device proxy performs `GetDefaultDevice` synchronously. In the successful-match branch, `handleVerifyStatus` calls `stopVerify()` before queuing unlock. That routine waits for `VerifyStop`. Discovery and final cleanup therefore add blocking work around the actual matching result. [Pinned Hyprlock fingerprint implementation](https://github.com/hyprwm/hyprlock/blob/v0.9.6/src/auth/Fingerprint.cpp).

The local patch changed discovery to an asynchronous D-Bus request. Its reply creates the device proxy and continues the existing claim/verify sequence. For a successful match, it requests `VerifyStop` asynchronously and queues unlock immediately. Only the successful-match branch receives this treatment; the change does not manufacture a match or relax the failed-match retry threshold.

This ordering is consistent with the API's distinction between a verification result and stopping an operation. `VerifyStatus` supplies the match result, while `VerifyStop` terminates verification work. The application still has to release the claimed device during shutdown. [fprintd device interface](https://fprint.freedesktop.org/fprintd-dev/Device.html).

## Why cleanup was visible to the user

The follow-up trace found that the Goodix driver reports an identification result before all post-result device states finish. Its state machine then includes a finger-removal wait and power-button-shield cleanup. That explains how an application can have a valid match result while reader work is still in progress. [Pinned Goodix implementation](https://gitlab.freedesktop.org/libfprint/libfprint/-/blob/v1.94.100/libfprint/drivers/goodixmoc/goodix.c#L440).

fprintd's stop handler permits an already reported operation to complete before cancellation. The relevant grace constant is one second, but it uses a GLib seconds timer; the first firing is not a precise one-second deadline. I therefore did not label the observed 1.4-second pause a hard-coded 1.4-second sleep. [fprintd stop handler](https://gitlab.freedesktop.org/libfprint/fprintd/-/blob/v1.94.5/src/device.c#L1798), [GLib timer semantics](https://docs.gtk.org/glib/func.timeout_add_seconds.html).

The original direct checks, outside Hyprlock, measured stop request/reply durations of approximately 1408.6 and 1408.3 milliseconds. Release then took less than a millisecond. A later instrumented successful run completed its finger-removal stage in about 435.7 milliseconds, followed by roughly 8.6 milliseconds of shield cleanup. That variability matters: it supports a post-match device dependency without establishing a fixed delay or blaming USB power management.

## The password conversation was an independent transaction

The upstream PAM worker starts authentication eagerly and waits inside its conversation callback for input. If fingerprint authentication unlocks the session first, termination wakes that callback, which returns a conversation error. Depending on the PAM stack, a cancelled authentication transaction can participate in failure accounting even though the user supplied no wrong password. [Pinned Hyprlock PAM implementation](https://github.com/hyprwm/hyprlock/blob/v0.9.6/src/auth/Pam.cpp).

My patch waits for password submission before starting PAM when native fingerprint authentication is enabled. A small pending-input flag allows the first PAM prompt to consume that already submitted input rather than requiring another Enter. Termination is checked after the initial wait, so fingerprint-only success exits the worker without starting a password transaction. When native fingerprint authentication is disabled, the eager behavior is retained.

This is an application-lifecycle correction. Removing the failure-accounting module, lowering its threshold, or broadly accepting conversation errors would alter the authentication policy. None of those is required to stop opening an unnecessary transaction.

## Recorded validation and its limits

The existing records contain the following checks; I did not repeat live authentication while preparing this article.

| Check | Recorded result |
|---|---|
| Isolated compositor rendering and deferred-PAM shutdown | Completed with private PAM and no usable system fingerprint bus |
| Three real fingerprint unlocks | Succeeded without new failure-counter entries |
| Password-only submission | First Enter succeeded; successful PAM result preceded unlock by about 6.8 ms |
| Physical lock request to compositor acknowledgement | Baseline 1041 ms; patched observation 563 ms |
| Authentication policy files | Recorded byte-for-byte unchanged |

These are software/journal milestones, not optical panel measurements. Adjacent match/unlock log entries shared a timestamp, so I cannot recover an exact sub-millisecond unlock latency. The isolated compositor check also did not exercise the real reader or real account failure counter.

The remaining validation should cover incorrect-password retries, delayed discovery callbacks during shutdown, immediate relock, and suspend/resume. Those cases were not exercised in the recorded check. Callback lifetime deserves particular attention: moving work asynchronously removes a blocking dependency but introduces a requirement that callbacks cannot access a destroyed authentication object.

## A reusable repair pattern

For authentication clients, I would write the lifecycle explicitly: start only the methods actually attempted; accept only their documented success result; schedule authorized UI state changes independently of slow cleanup; retain deterministic release and termination. Failure counters should describe failed user attempts, rather than artefacts of another method winning a race.

For review, the strongest evidence combines the caller's source, the service contract, a trace outside the caller, and narrowly stated application checks. That combination located the delay without reverting driver retry protection or weakening password policy. It also prevents a responsive interface from being mistaken for proof that every cleanup and retry path is safe.
