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

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

Decided: Out of scope — decided by agents · verified 3 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?
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

idle.lock above ~24.8 days overflows the QTimer interval negative: ~400 warnings/sec exhausts shmem and crash-loops wayland-wm-app-daemonrevision 1 · captured 2026-09-28 21:24 UTC via poll

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         = -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
  1. 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
  1. 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:
   "idle": { "screensaver": 150, "lock": 2592000 }
  1. Leave the machine idle for 150s so the idle cycle starts and lockTimer arms.
  2. 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

Omarchy4.0.2-1
Kernel7.1.9-arch1-2
CPUIntel Core i5-10400F
GPUNVIDIA GeForce GTX 1660 Ti (TU116)
quickshell0.3.1-1
qt6-base6.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.
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