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-859GitHub issuePublicDecided· Out of scope

`omarchy-crash-watch` self-sustains a coredump loop: its self-exclusion guard matches process names, but its own health check crashes as `quickshell`

Reported by pfunderdome · filed Sep 7, 2026 · open at source

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

`omarchy-crash-watch` self-sustains a coredump loop: its self-exclusion guard matches process names, but its own health check crashes as `quickshell`revision 1 · captured 2026-09-28 21:23 UTC via poll

What happened

On omarchy 4.0.2-1, omarchy-crash-watch ran a self-sustaining crash loop for roughly 51 hours (2026-09-05 06:11:15 → 2026-09-07 09:48 MDT), producing 76,265 core dumps and 4.1 GB in /var/lib/systemd/coredump, and burning 1h 40m of CPU.

Every one of those dumps was the same command — the watcher's own notification health check:

Command Line:  qs ipc -n -p /usr/share/omarchy/shell call -- notifications ping
Control Group: /user.slice/user-1000.slice/[email protected]/app.slice/omarchy-crash-watch.service
Signal:        SIGSEGV

300 of 300 sampled dumps were that exact command line.

Stopping the service alone took the rate from ~118 dumps/60s to 0 in 30s, with no other change.

Expected

omarchy-crash-watch announces a crash and stops. Its own machinery should never be able to generate the crashes it reports.

The defect

announce() first calls omarchy-notification-wait (/usr/bin/omarchy-crash-watch:33), which polls with the shell IPC client:

# /usr/share/omarchy/bin/omarchy-notification-wait
attempts=$((timeout * 10))
while (( attempts > 0 )); do
  if omarchy-shell notifications ping >/dev/null 2>&1 && notification_server_ready; then
    exit 0
  fi
  attempts=$((attempts - 1))
  sleep 0.1
done

Each omarchy-shell notifications ping execs qs ipc … notifications ping. When that client segfaults (see "Trigger" below), each of those ~100 attempts writes a core dump, and every core dump is a new journal event on the MESSAGE_ID the watcher is tailing.

The script does have self-exclusion, at /usr/bin/omarchy-crash-watch:76:

# Never announce our own machinery, or it notifies about itself.
[[ $name == omarchy-crash-* || $name == omarchy-agent-* ]] && continue

But name is derived from the executable basename (name=${exe##*/}), and the process that actually crashes is /usr/bin/quickshell. So name is quickshell, the guard never matches, and the watcher treats its own health-check subprocess as a third-party crash worth announcing.

The 60-second dedupe (dedupe_seconds=60) does not break the loop — it only sets its period. Each window: one announcement → up to 100 pings → ~100 core dumps in 10s → the window expires → the next dump triggers the next announcement. That predicts ~100–120 dumps/minute, and the measured rate was 118 per 60s.

Suggested fix

The coredump journal entries the watcher already parses carry the originating unit:

COREDUMP_USER_UNIT   = omarchy-crash-watch.service
COREDUMP_CGROUP      = /user.slice/…/app.slice/omarchy-crash-watch.service
COREDUMP_PROC_CGROUP = 0::/user.slice/…/app.slice/omarchy-crash-watch.service

Since the script already extracts fields with jq, adding COREDUMP_USER_UNIT to that extraction and skipping any crash originating from its own unit would close the loop by provenance rather than by name — robust regardless of which binary the health check happens to exec.

Trigger (not itself new)

The qs ipc segfault is quickshell's, and is already documented in #9745 — same stack, sorting stale instance locks:

#0  getStatus (d=…) at qdatetime.cpp:3168 → return d->m_status;
#1  QDateTime::isValid
#2  compareThreeWay(QDateTime const&, QDateTime const&)
#4  operator() at quickshell/src/launch/command.cpp:162
#13 std::ranges::sort over QList<InstanceLockInfo>

(symbolized via debuginfod)

This machine had 4,228 by-id instance directories, 4,223 of them dead. The live shell's own instance.lock was 0 bytes, which is what produced the invalid QDateTime.

The underlying shell crash-loop is #7380 / #9745: 5,499 relaunches and 17,407 lock-stranded / no outputs - creating placeholder screen events over the same 51 hours. Those relaunches exit 255 with no signal and so produce no core dumps of their own.

What is new here is that omarchy-crash-watch then multiplies that condition by roughly 14×: 5,499 signal-less shell relaunches became 76,265 core dumps, because the watcher's reaction to a crash is itself a crash. #9745 describes tmpfs exhaustion from leaked by-id directories; this is a separate amplifier in a different file, with a different fix, and it keeps running on its own once started.

Related but distinct: #8870 (omarchy-notification-wait timeout/attempt-count), #7822, #8613.

Recovery

systemctl --user stop omarchy-crash-watch.service   # rate → 0 immediately
# remove stale instance locks, keeping the live shell, then restart the shell

After clearing the locks and restarting the shell, qs ipc … notifications ping returns ok and omarchy-notification-wait exits 0.

System

  • omarchy 4.0.2-1
  • quickshell 0.3.1-1
  • qt6-base 6.11.2-2, qt6-wayland 6.11.2-1
  • hyprland 0.56.2-1
  • kernel 7.1.9-arch1-2
  • CPU Intel N95, GPU Intel Alder Lake-N UHD (i915)
  • Two monitors: HDMI-A-1, HDMI-A-2

(omarchy debug --no-sudo --print from the reporting guide does not exist in 4.0.2 — omarchy commands --all lists no debug subcommand — so the above was collected manually.)


Filed by Claude Opus 5 via Claude Code.

View raw text
---

## What happened

On omarchy 4.0.2-1, `omarchy-crash-watch` ran a self-sustaining crash loop for roughly 51 hours (2026-09-05 06:11:15 → 2026-09-07 09:48 MDT), producing **76,265 core dumps** and **4.1 GB** in `/var/lib/systemd/coredump`, and burning **1h 40m of CPU**.

Every one of those dumps was the same command — the watcher's own notification health check:

```
Command Line:  qs ipc -n -p /usr/share/omarchy/shell call -- notifications ping
Control Group: /user.slice/user-1000.slice/[email protected]/app.slice/omarchy-crash-watch.service
Signal:        SIGSEGV
```

300 of 300 sampled dumps were that exact command line.

Stopping the service alone took the rate from ~118 dumps/60s to **0 in 30s**, with no other change.

## Expected

`omarchy-crash-watch` announces a crash and stops. Its own machinery should never be able to generate the crashes it reports.

## The defect

`announce()` first calls `omarchy-notification-wait` (`/usr/bin/omarchy-crash-watch:33`), which polls with the shell IPC client:

```bash
# /usr/share/omarchy/bin/omarchy-notification-wait
attempts=$((timeout * 10))
while (( attempts > 0 )); do
  if omarchy-shell notifications ping >/dev/null 2>&1 && notification_server_ready; then
    exit 0
  fi
  attempts=$((attempts - 1))
  sleep 0.1
done
```

Each `omarchy-shell notifications ping` execs `qs ipc … notifications ping`. When that client segfaults (see "Trigger" below), each of those ~100 attempts writes a core dump, and every core dump is a new journal event on the `MESSAGE_ID` the watcher is tailing.

The script does have self-exclusion, at `/usr/bin/omarchy-crash-watch:76`:

```bash
# Never announce our own machinery, or it notifies about itself.
[[ $name == omarchy-crash-* || $name == omarchy-agent-* ]] && continue
```

But `name` is derived from the executable basename (`name=${exe##*/}`), and the process that actually crashes is `/usr/bin/quickshell`. So `name` is `quickshell`, the guard never matches, and the watcher treats its own health-check subprocess as a third-party crash worth announcing.

The 60-second dedupe (`dedupe_seconds=60`) does not break the loop — it only sets its period. Each window: one announcement → up to 100 pings → ~100 core dumps in 10s → the window expires → the next dump triggers the next announcement. That predicts ~100–120 dumps/minute, and the measured rate was **118 per 60s**.

## Suggested fix

The coredump journal entries the watcher already parses carry the originating unit:

```
COREDUMP_USER_UNIT   = omarchy-crash-watch.service
COREDUMP_CGROUP      = /user.slice/…/app.slice/omarchy-crash-watch.service
COREDUMP_PROC_CGROUP = 0::/user.slice/…/app.slice/omarchy-crash-watch.service
```

Since the script already extracts fields with `jq`, adding `COREDUMP_USER_UNIT` to that extraction and skipping any crash originating from its own unit would close the loop by provenance rather than by name — robust regardless of which binary the health check happens to exec.

## Trigger (not itself new)

The `qs ipc` segfault is quickshell's, and is already documented in #9745 — same stack, sorting stale instance locks:

```
#0  getStatus (d=…) at qdatetime.cpp:3168 → return d->m_status;
#1  QDateTime::isValid
#2  compareThreeWay(QDateTime const&, QDateTime const&)
#4  operator() at quickshell/src/launch/command.cpp:162
#13 std::ranges::sort over QList<InstanceLockInfo>
```

(symbolized via debuginfod)

This machine had 4,228 `by-id` instance directories, 4,223 of them dead. The live shell's own `instance.lock` was **0 bytes**, which is what produced the invalid `QDateTime`.

The underlying shell crash-loop is #7380 / #9745: 5,499 relaunches and 17,407 `lock-stranded` / `no outputs - creating placeholder screen` events over the same 51 hours. Those relaunches exit 255 with no signal and so produce no core dumps of their own.

**What is new here** is that `omarchy-crash-watch` then multiplies that condition by roughly 14×: 5,499 signal-less shell relaunches became 76,265 core dumps, because the watcher's reaction to a crash is itself a crash. #9745 describes tmpfs exhaustion from leaked `by-id` directories; this is a separate amplifier in a different file, with a different fix, and it keeps running on its own once started.

Related but distinct: #8870 (`omarchy-notification-wait` timeout/attempt-count), #7822, #8613.

## Recovery

```bash
systemctl --user stop omarchy-crash-watch.service   # rate → 0 immediately
# remove stale instance locks, keeping the live shell, then restart the shell
```

After clearing the locks and restarting the shell, `qs ipc … notifications ping` returns `ok` and `omarchy-notification-wait` exits 0.

## System

- omarchy 4.0.2-1
- quickshell 0.3.1-1
- qt6-base 6.11.2-2, qt6-wayland 6.11.2-1
- hyprland 0.56.2-1
- kernel 7.1.9-arch1-2
- CPU Intel N95, GPU Intel Alder Lake-N UHD (i915)
- Two monitors: HDMI-A-1, HDMI-A-2

(`omarchy debug --no-sudo --print` from the reporting guide does not exist in 4.0.2 — `omarchy commands --all` lists no `debug` subcommand — so the above was collected manually.)

---

Filed by Claude Opus 5 via Claude Code.

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

Not linked to any known issue, other report or change yet.

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 11:05 UTC