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-904GitHub issuePublicFixed

Session lock silently lost on output re-add: desktop exposed without authentication (and quickshell SIGABRT on the same path)

Reported by nikitaprokopov · filed Sep 6, 2026 · open at source

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

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

Session lock silently lost on output re-add: desktop exposed without authentication (and quickshell SIGABRT on the same path)revision 1 · captured 2026-09-28 21:23 UTC via poll

What happens

A locked session can be lost without any authentication when the compositor finishes the
ext-session-lock while the shell holds it. Two distinct symptoms, same trigger class
(output re-add / lock re-request while locked):

A. Shell aborts — FATAL: Tried to show lockscreen surfaces without active lock,
quickshell dies with SIGABRT and is restarted by its crash handler.

B. Silent unlock (worse) — no crash at all: WlSessionLock.locked goes to false
and plugins/lock/Service.qml treats that as a normal unlock, wakes the display and
leaves the full desktop exposed. No password is ever asked.

On a laptop whose lid policy is "lock (and hibernate on battery)" this defeats the lock:
the machine comes back from dpms-off / resume showing the unlocked desktop.

Expected

A lock that disappears without a successful PAM authentication must be re-established
(and reported), never treated as an unlock.

Steps to reproduce

Omarchy defaults, stock shell config (Config Path: /usr/share/omarchy/shell/shell.qml),
misc:allow_session_lock_restore = true (Omarchy default, default/hypr/looknfeel.lua:115).

omarchy-shell lock lock
sleep 3
omarchy-shell lock status            # {"locked":true,"secure":true,...}
omarchy-hyprland-session-locked; echo $?   # 0 = compositor holds the lock

# force an output re-add while locked (a resume from suspend/hibernate does the same)
hyprctl dispatch 'hl.dsp.dpms({ state = "off" })'; sleep 2
hyprctl dispatch 'hl.dsp.dpms({ state = "on" })'; sleep 5

omarchy-hyprland-session-locked; echo $?   # 1 = lock is GONE
omarchy-shell lock status            # {"locked":false,"secure":false,"lastEvent":"unlocked"}

omarchy-shell shell rescanPlugins while locked reproduces the same loss (that path tends
to produce symptom A rather than B).

Observed rate on this machine, always starting from secure=true:

trigger while lockedattemptslock lostof those, with SIGABRT
omarchy-shell shell rescanPlugins522
dpms off → dpms on310

Where it goes wrong

plugins/lock/Service.qml, onLockStateChanged:

if (!locked && root.lockRequested) {
  root.lockRequested = false
  ...
  root.runWake()
}

Any transition of sessionLock.locked to false is accepted as "the user unlocked".
The only legitimate producer of that transition is finishUnlock() (line 157) after PAM
success; a compositor-side finished event takes the same branch. There is no
"was this authenticated?" flag, so an involuntary lock loss silently becomes an unlock.

Symptom A comes from the other side of the same situation: requestSessionLock()
(sessionLock.locked = true, line 76) is reached again — from recoverStrandedLock()
(lines 94-100) or the stabilize/pending timers — after the lock was finished, and
quickshell aborts instead of failing the request. Two crash stacks, both ending in a QML
property write:

QQmlTimer::ticked -> QQmlBoundSignalExpression::evaluate -> QObjectWrapper::setProperty
  -> FATAL: Tried to show lockscreen surfaces without active lock

QWaylandScreen::maybeInitialize -> QWindowSystemInterface::handleScreenAdded
  -> QQmlNotifier::emitNotify -> QObjectWrapper::setProperty
  -> FATAL: Tried to show lockscreen surfaces without active lock

Suggested fix shape

  1. Record authentication in finishUnlock() and, in onLockStateChanged, when the lock

drops without it: logEvent("lock-lost: relocking"), call beginLock() and send a
critical notification instead of runWake().
2. Do not re-request a session lock in the same process after the compositor finished one
(guard requestSessionLock()), which also avoids the quickshell abort. The abort itself
looks like missing hardening on the quickshell side and can be filed there separately.

System

  • Omarchy 4.0.2-1, quickshell 0.3.1-1, hyprland 0.56.2-1, Qt 6.11.2
  • Kernel 7.1.9-arch1-2, Lenovo Yoga Pro 9 16IMH9 (hybrid Intel Arc iGPU + RTX 4060 Mobile)
  • Crash reports available locally: ~/.cache/quickshell/crashes/{7h1u7hwkt,7h1ptiwkt,dc95qgtxkt,dc95e0uxkt}

(Signal: Aborted (6), log tail is the FATAL line above)
- omarchy debug --no-sudo --print output can be attached on request; a screenshot of the
exposed desktop after step 4 exists but is not attached because it shows private content.

View raw text
## What happens

A locked session can be lost without any authentication when the compositor finishes the
`ext-session-lock` while the shell holds it. Two distinct symptoms, same trigger class
(output re-add / lock re-request while locked):

**A. Shell aborts** — `FATAL: Tried to show lockscreen surfaces without active lock`,
quickshell dies with SIGABRT and is restarted by its crash handler.

**B. Silent unlock (worse)** — no crash at all: `WlSessionLock.locked` goes to `false`
and `plugins/lock/Service.qml` treats that as a normal unlock, wakes the display and
leaves the full desktop exposed. No password is ever asked.

On a laptop whose lid policy is "lock (and hibernate on battery)" this defeats the lock:
the machine comes back from dpms-off / resume showing the unlocked desktop.

## Expected

A lock that disappears without a successful PAM authentication must be re-established
(and reported), never treated as an unlock.

## Steps to reproduce

Omarchy defaults, stock shell config (`Config Path: /usr/share/omarchy/shell/shell.qml`),
`misc:allow_session_lock_restore = true` (Omarchy default, `default/hypr/looknfeel.lua:115`).

```bash
omarchy-shell lock lock
sleep 3
omarchy-shell lock status            # {"locked":true,"secure":true,...}
omarchy-hyprland-session-locked; echo $?   # 0 = compositor holds the lock

# force an output re-add while locked (a resume from suspend/hibernate does the same)
hyprctl dispatch 'hl.dsp.dpms({ state = "off" })'; sleep 2
hyprctl dispatch 'hl.dsp.dpms({ state = "on" })'; sleep 5

omarchy-hyprland-session-locked; echo $?   # 1 = lock is GONE
omarchy-shell lock status            # {"locked":false,"secure":false,"lastEvent":"unlocked"}
```

`omarchy-shell shell rescanPlugins` while locked reproduces the same loss (that path tends
to produce symptom A rather than B).

Observed rate on this machine, always starting from `secure=true`:

| trigger while locked | attempts | lock lost | of those, with SIGABRT |
|---|---|---|---|
| `omarchy-shell shell rescanPlugins` | 5 | 2 | 2 |
| `dpms off` → `dpms on` | 3 | 1 | 0 |

## Where it goes wrong

`plugins/lock/Service.qml`, `onLockStateChanged`:

```qml
if (!locked && root.lockRequested) {
  root.lockRequested = false
  ...
  root.runWake()
}
```

Any transition of `sessionLock.locked` to `false` is accepted as "the user unlocked".
The only legitimate producer of that transition is `finishUnlock()` (line 157) after PAM
success; a compositor-side `finished` event takes the same branch. There is no
"was this authenticated?" flag, so an involuntary lock loss silently becomes an unlock.

Symptom A comes from the other side of the same situation: `requestSessionLock()`
(`sessionLock.locked = true`, line 76) is reached again — from `recoverStrandedLock()`
(lines 94-100) or the stabilize/pending timers — after the lock was finished, and
quickshell aborts instead of failing the request. Two crash stacks, both ending in a QML
property write:

```
QQmlTimer::ticked -> QQmlBoundSignalExpression::evaluate -> QObjectWrapper::setProperty
  -> FATAL: Tried to show lockscreen surfaces without active lock

QWaylandScreen::maybeInitialize -> QWindowSystemInterface::handleScreenAdded
  -> QQmlNotifier::emitNotify -> QObjectWrapper::setProperty
  -> FATAL: Tried to show lockscreen surfaces without active lock
```

## Suggested fix shape

1. Record authentication in `finishUnlock()` and, in `onLockStateChanged`, when the lock
   drops without it: `logEvent("lock-lost: relocking")`, call `beginLock()` and send a
   critical notification instead of `runWake()`.
2. Do not re-request a session lock in the same process after the compositor finished one
   (guard `requestSessionLock()`), which also avoids the quickshell abort. The abort itself
   looks like missing hardening on the quickshell side and can be filed there separately.

## System

- Omarchy 4.0.2-1, quickshell 0.3.1-1, hyprland 0.56.2-1, Qt 6.11.2
- Kernel 7.1.9-arch1-2, Lenovo Yoga Pro 9 16IMH9 (hybrid Intel Arc iGPU + RTX 4060 Mobile)
- Crash reports available locally: `~/.cache/quickshell/crashes/{7h1u7hwkt,7h1ptiwkt,dc95qgtxkt,dc95e0uxkt}`
  (`Signal: Aborted (6)`, log tail is the FATAL line above)
- `omarchy debug --no-sudo --print` output can be attached on request; a screenshot of the
  exposed desktop after step 4 exists but is not attached because it shows private content.

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.