Proof Portal
Omarchy
ProbeLabs65 findings · 95 requirementsA 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.
idle.lock above ~24.8 days overflows the QTimer interval negative: ~400 warnings/sec exhausts shmem and crash-loops wayland-wm-app-daemon
Reported by rnieuwen · filed Sep 2, 2026 · open at source
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?
- AreaOutside the audited area· 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
What happened
A large idle.lock value in ~/.config/omarchy/shell.json overflows the idle service's QTimer interval, producing a runaway Qt warning loop that eventually exhausts shmem/tmpfs and takes the desktop down.
plugins/services/idle/Service.qml:267 binds the interval in milliseconds:
Timer {
id: lockTimer
interval: root.lockDelaySeconds * 1000
repeat: false
onTriggered: if (root.idleEnabled && root.idledThisCycle) root.lockSystem("lock-timeout")
}QTimer/QQmlTimer intervals are a signed 32-bit int, so any lockDelaySeconds above 2147483 (~24.8 days) wraps negative. With idle.lock: 2592000 (30 days — a natural way to approximate "never lock"):
lockDelaySeconds = 2592000 - 150 = 2591850
2591850 * 1000 = 2591850000 ms
INT32_MAX = 2147483647
wraps to = -1702967296Qt clamps the negative interval to 1ms and logs a warning on every timer start:
omarchy-shell[1194]: WARN: QBasicTimer::start: negative intervals aren't allowed; the interval will be set to 1ms.Measured on my machine: ~424 warnings/second, 9,101,214 lines in a single session.
Impact
This is not just log noise. The flood escalates into a system-wide failure:
- Journald hits its 4G cap; weeks of real system history are rotated away.
- Quickshell's log memfd fills, and
shmem/tmpfs allocation starts failing:
omarchy-shell: ERROR quickshell.logging: Failed to copy log from memfd with error code 28 "No space left on device"
voxtype: WARN Failed to write PID file: No space left on device (os error 28)
uwsm_app-daemon: [Errno 28] No space left on devicewayland-wm-app-daemon.servicecrashes on the ENOSPC and enters a restart loop. Each restarted daemon receives a truncated request:
uwsm_app-daemon: received:
uwsm_app-daemon: sent: error 'Invalid arguments: ' 2- Every app launch then fails with a critical
App failure / Invalid arguments:desktop notification (urgency: 2,expireTimeout: 0, so they never expire and persist across restarts).
The machine became unusable and needed a hard restart. Note that df shows plenty of free disk throughout — the ENOSPC is shmem exhaustion, which makes this quite hard to diagnose from the symptom.
Steps to reproduce
- Set in
~/.config/omarchy/shell.json:
"idle": { "screensaver": 150, "lock": 2592000 }- Leave the machine idle for 150s so the idle cycle starts and
lockTimerarms. - Watch the flood:
journalctl -f | grep QBasicTimer
omarchy-shell idle status # "lockDelay": 2591850, "timers": { "lock": true }Expected
Either the timer is armed correctly for the configured duration, or the value is clamped/rejected — not silently wrapped into a 1ms busy loop.
Suggested fix
secondsFromConfig in plugins/services/idle/IdleModel.js already guards the low end but not the high end:
function secondsFromConfig(value, fallback) {
var n = Number(value)
if (!isFinite(n) || n < 0) return fallback
return Math.floor(n)
}Clamping to the largest value that survives the * 1000 conversion would fix it:
// Timer intervals are milliseconds in a signed 32-bit int.
var MAX_TIMER_SECONDS = 2147483 // floor(INT32_MAX / 1000)
function secondsFromConfig(value, fallback) {
var n = Number(value)
if (!isFinite(n) || n < 0) return fallback
return Math.min(Math.floor(n), MAX_TIMER_SECONDS)
}screensaverDelaySeconds (Service.qml:260) has the same exposure and would be covered by the same clamp.
Separately, it may be worth giving idle.lock an explicit "never" sentinel (e.g. 0 or false), since approximating it with a large number is what leads users into this.
System details
| Omarchy | 4.0.2-1 |
| Kernel | 7.1.9-arch1-2 |
| CPU | Intel Core i5-10400F |
| GPU | NVIDIA GeForce GTX 1660 Ti (TU116) |
| quickshell | 0.3.1-1 |
| qt6-base | 6.11.2-2 |
Related
- #9361 — critical notifications never auto-expire and have no close button. That's why the resulting
App failuretoasts pile up and survive restarts; this bug is one way to generate them in bulk. - #9745 — different cause, same class of consequence (runtime tmpfs filling up and taking the session down).
- #9496 — also concerns
idle.screensaver/idle.lockhandling inshell.json.
View raw text
### What happened
A large `idle.lock` value in `~/.config/omarchy/shell.json` overflows the idle service's QTimer interval, producing a runaway Qt warning loop that eventually exhausts shmem/tmpfs and takes the desktop down.
`plugins/services/idle/Service.qml:267` binds the interval in milliseconds:
```qml
Timer {
id: lockTimer
interval: root.lockDelaySeconds * 1000
repeat: false
onTriggered: if (root.idleEnabled && root.idledThisCycle) root.lockSystem("lock-timeout")
}
```
`QTimer`/`QQmlTimer` intervals are a signed 32-bit int, so any `lockDelaySeconds` above `2147483` (~24.8 days) wraps negative. With `idle.lock: 2592000` (30 days — a natural way to approximate "never lock"):
```
lockDelaySeconds = 2592000 - 150 = 2591850
2591850 * 1000 = 2591850000 ms
INT32_MAX = 2147483647
wraps to = -1702967296
```
Qt clamps the negative interval to 1ms and logs a warning on every timer start:
```
omarchy-shell[1194]: WARN: QBasicTimer::start: negative intervals aren't allowed; the interval will be set to 1ms.
```
Measured on my machine: **~424 warnings/second**, `9,101,214` lines in a single session.
### Impact
This is not just log noise. The flood escalates into a system-wide failure:
1. Journald hits its 4G cap; weeks of real system history are rotated away.
2. Quickshell's log memfd fills, and `shmem`/tmpfs allocation starts failing:
```
omarchy-shell: ERROR quickshell.logging: Failed to copy log from memfd with error code 28 "No space left on device"
voxtype: WARN Failed to write PID file: No space left on device (os error 28)
uwsm_app-daemon: [Errno 28] No space left on device
```
3. `wayland-wm-app-daemon.service` crashes on the ENOSPC and enters a restart loop. Each restarted daemon receives a truncated request:
```
uwsm_app-daemon: received:
uwsm_app-daemon: sent: error 'Invalid arguments: ' 2
```
4. Every app launch then fails with a critical `App failure / Invalid arguments:` desktop notification (`urgency: 2`, `expireTimeout: 0`, so they never expire and persist across restarts).
The machine became unusable and needed a hard restart. Note that `df` shows plenty of free disk throughout — the ENOSPC is shmem exhaustion, which makes this quite hard to diagnose from the symptom.
### Steps to reproduce
1. Set in `~/.config/omarchy/shell.json`:
```json
"idle": { "screensaver": 150, "lock": 2592000 }
```
2. Leave the machine idle for 150s so the idle cycle starts and `lockTimer` arms.
3. Watch the flood:
```bash
journalctl -f | grep QBasicTimer
omarchy-shell idle status # "lockDelay": 2591850, "timers": { "lock": true }
```
### Expected
Either the timer is armed correctly for the configured duration, or the value is clamped/rejected — not silently wrapped into a 1ms busy loop.
### Suggested fix
`secondsFromConfig` in `plugins/services/idle/IdleModel.js` already guards the low end but not the high end:
```js
function secondsFromConfig(value, fallback) {
var n = Number(value)
if (!isFinite(n) || n < 0) return fallback
return Math.floor(n)
}
```
Clamping to the largest value that survives the `* 1000` conversion would fix it:
```js
// Timer intervals are milliseconds in a signed 32-bit int.
var MAX_TIMER_SECONDS = 2147483 // floor(INT32_MAX / 1000)
function secondsFromConfig(value, fallback) {
var n = Number(value)
if (!isFinite(n) || n < 0) return fallback
return Math.min(Math.floor(n), MAX_TIMER_SECONDS)
}
```
`screensaverDelaySeconds` (`Service.qml:260`) has the same exposure and would be covered by the same clamp.
Separately, it may be worth giving `idle.lock` an explicit "never" sentinel (e.g. `0` or `false`), since approximating it with a large number is what leads users into this.
### System details
| | |
|---|---|
| Omarchy | 4.0.2-1 |
| Kernel | 7.1.9-arch1-2 |
| CPU | Intel Core i5-10400F |
| GPU | NVIDIA GeForce GTX 1660 Ti (TU116) |
| quickshell | 0.3.1-1 |
| qt6-base | 6.11.2-2 |
### Related
- #9361 — critical notifications never auto-expire and have no close button. That's why the resulting `App failure` toasts pile up and survive restarts; this bug is one way to generate them in bulk.
- #9745 — different cause, same class of consequence (runtime tmpfs filling up and taking the session down).
- #9496 — also concerns `idle.screensaver` / `idle.lock` handling in `shell.json`.
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
- Fixed bypr/9170· confirmed
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.
- Out of scopeDecided by agents · verified · 2026-10-09 02:14 UTC