Skip to content

Stalled lock latches lockRequested, so every later lock() returns "ok" without locking — machine suspends unlocked #10299

Description

@mchldotdev

Summary

When a lock request stalls after lock-pending: screen-stabilizing, lockRequested stays latched true forever. Because the lock plugin derives locked from lockRequested, the lock IPC method then short-circuits and returns "ok" for every subsequent request without ever locking. The lid-close lock therefore becomes a silent no-op and the machine suspends with the session fully exposed. On resume, Hyprland shows its crashed-lockscreen failsafe.

The stall itself is intermittent. The latch that turns one stall into "every later lock silently does nothing, and reports success" is deterministic, and is the part that makes this a security issue rather than a cosmetic one.

What happened

Single boot, no user config beyond stock idle values (screensaver: 150, lock: 300):

08:48:21  omarchy idle: process-start: lock omarchy-system-lock
08:48:21  omarchy lock  lock-requested
08:48:21  omarchy lock  lock-pending: screen-stabilizing
08:48:21  omarchy idle: process-exit: lock exitCode=0 status=0
          <-- no session-locked=true, no secure=true, ever
08:50:51  omarchy idle: process-start: screensaver   (so lock isLocked == false)
08:53:21  omarchy idle: process-exit: lock exitCode=0 status=0   (second idle lock, also a no-op)
08:57:44  systemd-logind: Lid closed.
08:57:57  systemd-logind: Suspending...
          <-- suspended UNLOCKED
10:52:36  systemd-logind: Lid opened.  (resume)
          <-- Hyprland crashed-lockscreen failsafe on screen
10:52:43  omarchy lock  session-locked=false
10:52:43  omarchy lock  unlocked

Between 08:48:21 and 10:52:43 the omarchy lock namespace logged nothing at all. sessionLock.locked = true was reached (no lock-pending: no-real-screen was logged, so hasRealScreen() was true and requestSessionLock() ran to completion), but onLockStateChanged never fired — Quickshell silently declined to take the ext-session-lock.

Expected behavior

A lock request that cannot be satisfied should fail loudly and reset lockRequested, so the next request — in particular the lid-close one — gets a real attempt. A machine must never suspend unlocked because an earlier lock stalled.

Root cause

plugins/lock/Service.qml:

readonly property bool locked: lockRequested || sessionLock.locked || sessionLock.secure
IpcHandler {
  target: "lock"

  function lock(): string {
    if (!root.passwordPamConfigured) return "missing-pam"
    if (!root.locked && !root.beginLock()) return "failed"
    return "ok"
  }

Once lockRequested is true, root.locked is true, so beginLock() is never called again and the method returns "ok". There is no timeout that resets lockRequested when the session lock never materialises — pendingSessionLockTimer retries requestSessionLock() forever, and requestSessionLock() returns immediately at its first guard once lockRequested is set but the lock never arrives.

Nothing downstream can notice, either:

  • omarchy-system-lock runs omarchy-shell lock lock >/dev/null and never checks the reply.
  • omarchy-system-sleep-lock's lock_state() reads requested: true, secure: false and classifies it as locking — "the shell has the request and is working on it, so leave it alone" — so it waits out its whole budget instead of re-requesting.

So the one component that could have caught this deliberately stays quiet, because a latched-stuck request is indistinguishable from a healthy in-progress one.

Suggested direction

Give the pending state a deadline. If sessionLock.locked has not become true within some bound of beginLock(), log a failure, reset lockRequested/pendingSessionLock, and let the next caller retry from scratch. lock() returning "ok" should mean "the session is locked or is provably becoming locked", not "a request was recorded at some point".

Distinguishing requested-but-stalled from locking in lock status would also let omarchy-system-sleep-lock re-request rather than wait, which is what it already does for the idle state.

Steps to reproduce

The stall is intermittent and I could not reproduce it on demand. The latch is trivially reproducible once the stall has happened: while lock status reports requested: true, sessionLocked: false, secure: false, every omarchy-shell lock lock returns ok and nothing locks, including omarchy system lock and the lid-close path.

Contrast: on the same shell process (PID unchanged), a manual omarchy-shell lock lock from a clean state reaches secure=true in ~400-600 ms, twice out of two attempts:

0.0s  req=True  sessLocked=False secure=False last=lock-pending: screen-stabilizing
0.4s  req=True  sessLocked=True  secure=True  last=secure=true

Possibly relevant

The shell had been restarted with omarchy restart shell about 8 minutes before the stalled lock (08:40:32; the stall was at 08:48:21). The same process later locked correctly on demand, so a restart is clearly not sufficient to cause it, but it may be part of the trigger and I did not want to leave it out.

omarchy-sleep-lock.service also failed three times on resume with Failed to inhibit: The operation inhibition has been requested for is already running (restart counter 1→4 at 10:52:36-10:52:42), which looks like #7280.

Related

System details

  • Omarchy: 4.0.2-1
  • Hyprland: 0.56.2
  • Quickshell: 0.3.1-1
  • Kernel: 7.1.9-arch1-2
  • Model: LENOVO Yoga 7 2-in-1 16AKP10 (83JU)
  • CPU: AMD Ryzen AI 7 350 w/ Radeon 860M
  • GPU: AMD Krackan [Radeon 840M / 860M Graphics]
  • Single internal display, no external monitors
  • /etc/pam.d/omarchy-lock-password present and valid; fingerprint not configured
  • All 96 migrations applied

omarchy debug is not available in 4.0.2 (Unknown Omarchy command), so the details above were collected by hand.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions