Proof Portal

Project overview

Omarchy

ProbeLabs65 findings · 95 requirements

A proof layer — requirements, tests and verified fixes — for two of Omarchy's subsystems: the application menu (launcher scripts, QML model, JSONC config, search and selection) and the lock screen (lock scripts, QML, PAM authentication). Scope is deliberately limited to those components of omacom/omarchy; the rest of the distribution is not covered.

Inbox
ReportR-2143GitHub issuePublicFixed

Lock screen: fingerprint reader disabled by libfprint thermal guard after ~3 min, then 250 ms retry loop

Reported by gfotakis · filed Oct 1, 2026 · open at source

Decided: Duplicate — decided by agents · verified 3 days ago.

R-443 is the earliest report of this defect — filed Sep 16, 2026, open upstream. This report (filed Oct 1, 2026) describes the same defect, so it is folded under it in the inbox.

Decided by agents · verified· Oct 9, 2026

At a glance

1 · What is claimed?
Only the report text below — no validator has restated it as checkable claims yet.
2 · What does it affect?
AreaIn the audited arealock· Decided by agents · verified
3 · Is it reproducible?
No validation has been run yet.
4 · What remains unclear?
  • Not validated yet: no claims, no reproducer.
5 · What happens next?
Decided by agents and verified by a second, independent agent — see the decision.

Original report

Lock screen: fingerprint reader disabled by libfprint thermal guard after ~3 min, then 250 ms retry looprevision 1 · captured 2026-10-01 10:23 UTC via poll

What happens

After the screen has been locked for about three minutes, fingerprint unlock stops working and stays dead until the session is unlocked with the password.

The lock plugin keeps pam_fprintd armed for the whole lock (shell/plugins/lock/Service.qml). libfprint's thermal guard then disables the reader, and from that point every attempt fails instantly with:

fprintd[...]: Device reported an error during identify: Device disabled to prevent overheating.

fingerprintRetryTimer (250 ms) restarts the PAM session after each failure, so the shell retries about 2.4 times per second for as long as the screen stays locked. One overnight lock produced 54,319 of these errors (22:38 to 05:11, roughly 8,500 per hour), and the reader never recovered in that time — presumably because it is never left idle long enough to cool down.

Expected

Fingerprint unlock still works after a long lock, and a device error does not cause a tight retry loop.

Steps to reproduce

  1. Enroll a fingerprint (omarchy setup security fingerprint).
  2. Lock the screen and leave it locked for more than ~3 minutes.
  3. journalctl -b | grep 'prevent overheating' shows the error repeating several times a second; the reader no longer unlocks.

Timeline from the journal: lock at 22:35, normal ~30 s PAM timeouts until 22:37, first overheat error at 22:38:14, then continuous.

System

  • Omarchy 4.0.4-1, kernel 7.2.5-3-omarchy
  • libfprint 1.94.100-1, fprintd 1.94.5-2
  • Reader: 05ba:000a DigitalPersona, Inc. Fingerprint Reader (U.are.U 4000/4000B/4500, press type)
  • Alienware 15 DA15265, AMD Ryzen 7 260 (Radeon 780M) + NVIDIA RTX 5060 Mobile

Suggested fix

Only scan for a short window after lock and after user activity (wakeRequested), abort the fingerprint PAM session when the window closes, and back off when an attempt fails immediately. I am running this as a local clone of omarchy.lock; fingerprint unlock inside the window works. The long-lock case is not yet confirmed overnight.

--- /usr/share/omarchy/shell/plugins/lock/Service.qml	2026-09-15 16:04:52.000000000 -0500
+++ Service.qml	2026-10-01 05:18:55.524369703 -0500
@@ -32,6 +32,14 @@
   property string lastEvent: "init"
   property string lastEventAt: ""
   property bool strandedLock: false
+
+  // The reader only scans for a short window after user activity. Scanning for
+  // the whole lock trips libfprint's thermal guard ("Device disabled to prevent
+  // overheating") after ~3 minutes, and the reader then stays dead until unlock.
+  readonly property int fingerprintWindowMs: 30000
+  readonly property int fingerprintRetryMaxMs: 30000
+  property double fingerprintStartedAt: 0
+  property int fingerprintFastFailures: 0
   property bool strandedLockResolved: false
 
   readonly property bool locked: lockRequested || sessionLock.locked || sessionLock.secure
@@ -120,7 +128,9 @@
     failedAttempts = 0
     authenticatingPassword = false
     fingerprintAuthenticating = false
+    fingerprintFastFailures = 0
     fingerprintRetryTimer.stop()
+    fingerprintWindowTimer.stop()
     if (passwordPam.active) passwordPam.abort()
     if (fingerprintPam.active) fingerprintPam.abort()
   }
@@ -208,22 +218,50 @@
 
   function startFingerprint() {
     if (!lockRequested || !sessionLock.secure || !fingerprintConfigured) return
+    if (!fingerprintWindowTimer.running) return
     if (fingerprintPam.active || fingerprintAuthenticating) return
 
     fingerprintAuthenticating = true
+    fingerprintStartedAt = Date.now()
     if (!fingerprintPam.start()) {
       fingerprintAuthenticating = false
     }
   }
 
+  // Called on lock and on every key press or pointer move on the lock screen.
+  function openFingerprintWindow() {
+    if (!lockRequested || !sessionLock.secure || !fingerprintConfigured) return
+
+    fingerprintWindowTimer.restart()
+    if (!fingerprintRetryTimer.running) startFingerprint()
+  }
+
+  function closeFingerprintWindow() {
+    fingerprintRetryTimer.stop()
+    if (fingerprintPam.active) fingerprintPam.abort()
+    fingerprintAuthenticating = false
+  }
+
+  function retryFingerprint() {
+    if (!lockRequested || !fingerprintConfigured || !fingerprintWindowTimer.running) return
+
+    // An attempt that dies at once is a device error (busy, overheated), not a
+    // timeout or a bad read. Back off so the reader gets time to recover.
+    if (Date.now() - fingerprintStartedAt < 2000) fingerprintFastFailures += 1
+    else fingerprintFastFailures = 0
+
+    fingerprintRetryTimer.interval = Math.min(fingerprintRetryMaxMs, 250 * Math.pow(2, fingerprintFastFailures))
+    fingerprintRetryTimer.restart()
+  }
+
   function handleFingerprintFinished(result) {
     fingerprintAuthenticating = false
 
     if (!lockRequested) return
     if (result === PamResult.Success) {
       finishUnlock()
-    } else if (fingerprintConfigured) {
-      fingerprintRetryTimer.restart()
+    } else {
+      retryFingerprint()
     }
   }
 
@@ -238,7 +276,7 @@
         root.pendingSessionLock = false
         sessionLockStabilizeTimer.stop()
         pendingSessionLockTimer.stop()
-        root.startFingerprint()
+        root.openFingerprintWindow()
       }
     }
 
@@ -280,7 +318,7 @@
         onPasswordTextEdited: function(password) { root.enteredPassword = password }
         onSubmitPassword: function(password) { root.submitPassword(password) }
         onClearFailureRequested: root.failureMessage = ""
-        onWakeRequested: root.runWake()
+        onWakeRequested: { root.runWake(); root.openFingerprintWindow() }
       }
 
     }
@@ -349,11 +387,18 @@
 
     onError: function(error) {
       root.fingerprintAuthenticating = false
-      if (root.lockRequested && root.fingerprintConfigured) fingerprintRetryTimer.restart()
+      root.retryFingerprint()
     }
   }
 
   Timer {
+    id: fingerprintWindowTimer
+    interval: root.fingerprintWindowMs
+    repeat: false
+    onTriggered: root.closeFingerprintWindow()
+  }
+
+  Timer {
     id: fingerprintRetryTimer
     interval: 250
     repeat: false
@@ -381,7 +426,7 @@
     stdout: StdioCollector { id: fingerprintCheckStdout; waitForEnd: true }
     onExited: {
       root.fingerprintConfigured = String(fingerprintCheckStdout.text || "").trim() === "yes"
-      if (root.lockRequested && root.fingerprintConfigured) root.startFingerprint()
+      if (root.lockRequested && root.fingerprintConfigured) root.openFingerprintWindow()
       else if (!root.fingerprintConfigured && fingerprintPam.active) fingerprintPam.abort()
     }
   }
@@ -424,9 +469,8 @@
         root.armBlankTimer()
         return
       }
-      // Only a password check in flight should hold the display up. The
-      // fingerprint PAM stays armed for the whole lock, so gating on
-      // `authenticating` here would keep the panel lit until unlock.
+      // Only a password check in flight should hold the display up; a
+      // fingerprint scan in its window must not keep the panel lit.
       if (root.lockRequested && !root.authenticatingPassword) root.runBlank()
     }
   }
View raw text
## What happens

After the screen has been locked for about three minutes, fingerprint unlock stops working and stays dead until the session is unlocked with the password.

The lock plugin keeps `pam_fprintd` armed for the whole lock (`shell/plugins/lock/Service.qml`). libfprint's thermal guard then disables the reader, and from that point every attempt fails instantly with:

```
fprintd[...]: Device reported an error during identify: Device disabled to prevent overheating.
```

`fingerprintRetryTimer` (250 ms) restarts the PAM session after each failure, so the shell retries about 2.4 times per second for as long as the screen stays locked. One overnight lock produced 54,319 of these errors (22:38 to 05:11, roughly 8,500 per hour), and the reader never recovered in that time — presumably because it is never left idle long enough to cool down.

## Expected

Fingerprint unlock still works after a long lock, and a device error does not cause a tight retry loop.

## Steps to reproduce

1. Enroll a fingerprint (`omarchy setup security fingerprint`).
2. Lock the screen and leave it locked for more than ~3 minutes.
3. `journalctl -b | grep 'prevent overheating'` shows the error repeating several times a second; the reader no longer unlocks.

Timeline from the journal: lock at 22:35, normal ~30 s PAM timeouts until 22:37, first overheat error at 22:38:14, then continuous.

## System

- Omarchy 4.0.4-1, kernel 7.2.5-3-omarchy
- libfprint 1.94.100-1, fprintd 1.94.5-2
- Reader: `05ba:000a DigitalPersona, Inc. Fingerprint Reader` (U.are.U 4000/4000B/4500, press type)
- Alienware 15 DA15265, AMD Ryzen 7 260 (Radeon 780M) + NVIDIA RTX 5060 Mobile

## Suggested fix

Only scan for a short window after lock and after user activity (`wakeRequested`), abort the fingerprint PAM session when the window closes, and back off when an attempt fails immediately. I am running this as a local clone of `omarchy.lock`; fingerprint unlock inside the window works. The long-lock case is not yet confirmed overnight.

```diff
--- /usr/share/omarchy/shell/plugins/lock/Service.qml	2026-09-15 16:04:52.000000000 -0500
+++ Service.qml	2026-10-01 05:18:55.524369703 -0500
@@ -32,6 +32,14 @@
   property string lastEvent: "init"
   property string lastEventAt: ""
   property bool strandedLock: false
+
+  // The reader only scans for a short window after user activity. Scanning for
+  // the whole lock trips libfprint's thermal guard ("Device disabled to prevent
+  // overheating") after ~3 minutes, and the reader then stays dead until unlock.
+  readonly property int fingerprintWindowMs: 30000
+  readonly property int fingerprintRetryMaxMs: 30000
+  property double fingerprintStartedAt: 0
+  property int fingerprintFastFailures: 0
   property bool strandedLockResolved: false
 
   readonly property bool locked: lockRequested || sessionLock.locked || sessionLock.secure
@@ -120,7 +128,9 @@
     failedAttempts = 0
     authenticatingPassword = false
     fingerprintAuthenticating = false
+    fingerprintFastFailures = 0
     fingerprintRetryTimer.stop()
+    fingerprintWindowTimer.stop()
     if (passwordPam.active) passwordPam.abort()
     if (fingerprintPam.active) fingerprintPam.abort()
   }
@@ -208,22 +218,50 @@
 
   function startFingerprint() {
     if (!lockRequested || !sessionLock.secure || !fingerprintConfigured) return
+    if (!fingerprintWindowTimer.running) return
     if (fingerprintPam.active || fingerprintAuthenticating) return
 
     fingerprintAuthenticating = true
+    fingerprintStartedAt = Date.now()
     if (!fingerprintPam.start()) {
       fingerprintAuthenticating = false
     }
   }
 
+  // Called on lock and on every key press or pointer move on the lock screen.
+  function openFingerprintWindow() {
+    if (!lockRequested || !sessionLock.secure || !fingerprintConfigured) return
+
+    fingerprintWindowTimer.restart()
+    if (!fingerprintRetryTimer.running) startFingerprint()
+  }
+
+  function closeFingerprintWindow() {
+    fingerprintRetryTimer.stop()
+    if (fingerprintPam.active) fingerprintPam.abort()
+    fingerprintAuthenticating = false
+  }
+
+  function retryFingerprint() {
+    if (!lockRequested || !fingerprintConfigured || !fingerprintWindowTimer.running) return
+
+    // An attempt that dies at once is a device error (busy, overheated), not a
+    // timeout or a bad read. Back off so the reader gets time to recover.
+    if (Date.now() - fingerprintStartedAt < 2000) fingerprintFastFailures += 1
+    else fingerprintFastFailures = 0
+
+    fingerprintRetryTimer.interval = Math.min(fingerprintRetryMaxMs, 250 * Math.pow(2, fingerprintFastFailures))
+    fingerprintRetryTimer.restart()
+  }
+
   function handleFingerprintFinished(result) {
     fingerprintAuthenticating = false
 
     if (!lockRequested) return
     if (result === PamResult.Success) {
       finishUnlock()
-    } else if (fingerprintConfigured) {
-      fingerprintRetryTimer.restart()
+    } else {
+      retryFingerprint()
     }
   }
 
@@ -238,7 +276,7 @@
         root.pendingSessionLock = false
         sessionLockStabilizeTimer.stop()
         pendingSessionLockTimer.stop()
-        root.startFingerprint()
+        root.openFingerprintWindow()
       }
     }
 
@@ -280,7 +318,7 @@
         onPasswordTextEdited: function(password) { root.enteredPassword = password }
         onSubmitPassword: function(password) { root.submitPassword(password) }
         onClearFailureRequested: root.failureMessage = ""
-        onWakeRequested: root.runWake()
+        onWakeRequested: { root.runWake(); root.openFingerprintWindow() }
       }
 
     }
@@ -349,11 +387,18 @@
 
     onError: function(error) {
       root.fingerprintAuthenticating = false
-      if (root.lockRequested && root.fingerprintConfigured) fingerprintRetryTimer.restart()
+      root.retryFingerprint()
     }
   }
 
   Timer {
+    id: fingerprintWindowTimer
+    interval: root.fingerprintWindowMs
+    repeat: false
+    onTriggered: root.closeFingerprintWindow()
+  }
+
+  Timer {
     id: fingerprintRetryTimer
     interval: 250
     repeat: false
@@ -381,7 +426,7 @@
     stdout: StdioCollector { id: fingerprintCheckStdout; waitForEnd: true }
     onExited: {
       root.fingerprintConfigured = String(fingerprintCheckStdout.text || "").trim() === "yes"
-      if (root.lockRequested && root.fingerprintConfigured) root.startFingerprint()
+      if (root.lockRequested && root.fingerprintConfigured) root.openFingerprintWindow()
       else if (!root.fingerprintConfigured && fingerprintPam.active) fingerprintPam.abort()
     }
   }
@@ -424,9 +469,8 @@
         root.armBlankTimer()
         return
       }
-      // Only a password check in flight should hold the display up. The
-      // fingerprint PAM stays armed for the whole lock, so gating on
-      // `authenticating` here would keep the panel lit until unlock.
+      // Only a password check in flight should hold the display up; a
+      // fingerprint scan in its window must not keep the panel lit.
       if (root.lockRequested && !root.authenticatingPassword) root.runBlank()
     }
   }
```

Validation

Not checked yet. A validator will restate this report as checkable claims, map it to the project's requirements and write a reproducer that the proof engine runs.

Linked

Decision

Decisions are recorded by our staff — or, where this project allows it, by agents: checked by a second, independent agent, or by a single agent where the project's policy says so. Any decision reached so far is shown below.

  • DuplicateR-443Decided by agents · verified · 2026-10-09 03:29 UTC