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

Superseding a pending omarchy-menu-select leaks a permanent 20 fork/sec busy-wait loop

Reported by designgears · filed Aug 22, 2026 · open at source

Decided: Duplicate — 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?
AreaIn the audited areamenu· 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

Superseding a pending omarchy-menu-select leaks a permanent 20 fork/sec busy-wait looprevision 1 · captured 2026-09-28 21:24 UTC via poll

What happened

Any omarchy-menu-select request that is superseded rather than answered leaks a
bash process that spins in a sleep 0.05 busy-wait forever. Each leaked
process forks 20 times per second for the life of the session.

These accumulate silently and without bound. On my machine 15 had built up over
~30 minutes of normal use, driving the system to 273 forks/sec at idle
(baseline is ~2). A further one leaked during the hour I spent writing this
report — I found it parented to Hyprland, still spinning after 560s — so the rate
of accumulation tracks ordinary menu use.

Each leak also strands a mktemp file in /tmp (see below), so this is an
unbounded process and fd/inode leak for the life of the session.

On measured impact, to be straight about it: I could not measure an app-launch
penalty from this on a 16-core Core Ultra 9. Timing foot to first mapped window,
3 runs each, gave 81/94/88 ms at a clean 22 forks/sec and 78/98/83 ms under a
synthetic 312 forks/sec — no meaningful difference, and nautilus was 647 ms vs
660 ms. So I am not claiming this causes general desktop sluggishness; on this
hardware it does not. The bug I am reporting is the unbounded leak itself. On a
2–4 core machine the same 273 forks/sec would be a far larger share of the CPU,
and the leak grows without limit regardless of core count.

What I expected

A superseded or dismissed menu request should write its done_file so the
waiting script exits.

Root cause

bin/omarchy-menu-select:91 waits for the shell to signal completion:

omarchy-shell shell summon omarchy.menu "$payload" >/dev/null

while [[ ! -e $done_file ]]; do
  sleep 0.05
done

The done_file is only ever written by finishRequest() in
shell/plugins/menu/Menu.qml:117. The dismiss path guards that call on
dmenuActive (Menu.qml:789):

function cancel() {
    if (root.dmenuActive) root.finishRequest(null)
    opened = false
    filterText = ""
}

But openExistingMenu() (Menu.qml:795) unconditionally discards the pending
request and flips the mode the guard depends on:

function openExistingMenu(initialMenu) {
    requestSerial += 1
    mode = "menu"          // dmenuActive (Menu.qml:56) is now false
    requestActive = false
    selectionFile = ""
    doneFile = ""          // the waiting script's handle is gone
    ...

Since dmenuActive is mode === "select" || mode === "input" (Menu.qml:56),
setting mode = "menu" makes the cancel() guard false. So the later dismiss
cannot write the done_file, and finishRequest() would early-return anyway
because requestActive is false and doneFile is "".

The bash script has no timeout and no liveness check, so it waits forever.

Note the trap ... EXIT on line 73 never runs either, so the mktemp
selection_file is leaked to /tmp as well — I had 14 stale ones.

Steps to reproduce

# baseline
b=$(awk '/^processes/{print $2}' /proc/stat); sleep 2
a=$(awk '/^processes/{print $2}' /proc/stat); echo "$(( (a-b)/2 )) forks/sec"

# 1. start a select request
printf 'alpha\nbravo\n' | omarchy-menu-select 'ReproTest' -- --width 400 --height 300 &

# 2. supersede it with the regular menu (SUPER+ALT+SPACE works too)
omarchy-shell shell summon omarchy.menu ''

# 3. dismiss that menu with Escape, then observe

Result — the menu is fully gone from hyprctl layers, but:

$ ps -o pid,etimes,stat,wchan:20,args -p <pid>
    PID ETIMES STAT WCHAN                COMMAND
 160400     45 S    do_wait              /bin/bash .../omarchy-menu-select ReproTest ...

$ for i in $(seq 10); do pgrep -P 160400 -a; done | sort -u
161585 sleep 0.05
161588 sleep 0.05
161591 sleep 0.05
...

Measured fork rate, same machine:

stateforks/sec
idle baseline2
1 leaked spinner20
15 leaked spinners (as found)273

Killing the orphans returns the machine to 2 forks/sec. The leaked processes
never exit on their own — I confirmed one still in do_wait after 45s, and the
in-the-wild one had been spinning for 560s when I found it.

Suggested fix

Two independent problems; the first is the actual bug.

  1. Don't drop a pending request on the floor. Have openExistingMenu() (and

any other path that clears doneFile/requestActive) finish the outstanding
request first, e.g. call finishRequest(null) before reassigning mode,
rather than relying on a cancel() guard that the reassignment itself
invalidates.

  1. Make the wait cheap and bounded. Even once fixed, a 20 Hz fork loop is a

costly way to wait on a file. inotifywait on the parent directory, or a
read on a fifo, would block at zero cost — and a liveness check against the
shell would keep a missed signal from hanging forever instead of leaking
silently.

System details

omarchy          4.0.0-1  (channel: stable, reported up to date)
quickshell-git   0.3.0.r20.g28771c7-2
hyprland         0.56.2-1
kernel           7.1.8-arch1-3
hardware         ASUS ROG Zephyrus G16 GU605CR

Both affected files are owned by omarchy 4.0.0-1 per pacman -Qo.

Not a duplicate of #7025

#7025 (omarchy-menu-keybindings hangs forever) also involves a stuck
keybindings menu, but the mechanism is unrelated: there, build_lua_bind_cache()
spins in an infinite ipairs() loop against a permissive hl stub, blocking
before the menu is ever summoned. Here the menu is summoned and displayed
normally, no Lua evaluation is involved (my configs contain no get_monitors /
get_windows / get_workspaces calls, and no lua process exists), and the
hang is in the post-summon done_file wait. Fixing #7025 would not address this.

It is the same class as #7124, #7150, #6989, #6881 and #6172 (orphaned helper
processes outliving their owner) but a different site and trigger.

I also checked the closed sluggishness reports: #6768 is Panther Lake / Arc B390
specific and was fixed in omacom-io/omarchy-pkgs#138, and #4336 concerns the
keybindings window's contents, not a leaked process. Neither is this bug, so
this is a new report rather than a regression.


Filed by Claude Opus 5 via Claude Code.

View raw text
## What happened

Any `omarchy-menu-select` request that is superseded rather than answered leaks a
`bash` process that spins in a `sleep 0.05` busy-wait **forever**. Each leaked
process forks 20 times per second for the life of the session.

These accumulate silently and without bound. On my machine 15 had built up over
~30 minutes of normal use, driving the system to **273 forks/sec at idle**
(baseline is ~2). A further one leaked during the hour I spent writing this
report — I found it parented to Hyprland, still spinning after 560s — so the rate
of accumulation tracks ordinary menu use.

Each leak also strands a `mktemp` file in `/tmp` (see below), so this is an
unbounded process **and** fd/inode leak for the life of the session.

**On measured impact, to be straight about it:** I could not measure an app-launch
penalty from this on a 16-core Core Ultra 9. Timing `foot` to first mapped window,
3 runs each, gave 81/94/88 ms at a clean 22 forks/sec and 78/98/83 ms under a
synthetic 312 forks/sec — no meaningful difference, and `nautilus` was 647 ms vs
660 ms. So I am **not** claiming this causes general desktop sluggishness; on this
hardware it does not. The bug I am reporting is the unbounded leak itself. On a
2–4 core machine the same 273 forks/sec would be a far larger share of the CPU,
and the leak grows without limit regardless of core count.

## What I expected

A superseded or dismissed menu request should write its `done_file` so the
waiting script exits.

## Root cause

`bin/omarchy-menu-select:91` waits for the shell to signal completion:

```bash
omarchy-shell shell summon omarchy.menu "$payload" >/dev/null

while [[ ! -e $done_file ]]; do
  sleep 0.05
done
```

The `done_file` is only ever written by `finishRequest()` in
`shell/plugins/menu/Menu.qml:117`. The dismiss path guards that call on
`dmenuActive` (`Menu.qml:789`):

```qml
function cancel() {
    if (root.dmenuActive) root.finishRequest(null)
    opened = false
    filterText = ""
}
```

But `openExistingMenu()` (`Menu.qml:795`) unconditionally discards the pending
request *and* flips the mode the guard depends on:

```qml
function openExistingMenu(initialMenu) {
    requestSerial += 1
    mode = "menu"          // dmenuActive (Menu.qml:56) is now false
    requestActive = false
    selectionFile = ""
    doneFile = ""          // the waiting script's handle is gone
    ...
```

Since `dmenuActive` is `mode === "select" || mode === "input"` (`Menu.qml:56`),
setting `mode = "menu"` makes the `cancel()` guard false. So the later dismiss
cannot write the `done_file`, and `finishRequest()` would early-return anyway
because `requestActive` is false and `doneFile` is `""`.

The bash script has no timeout and no liveness check, so it waits forever.

Note the `trap ... EXIT` on line 73 never runs either, so the `mktemp`
`selection_file` is leaked to `/tmp` as well — I had 14 stale ones.

## Steps to reproduce

```bash
# baseline
b=$(awk '/^processes/{print $2}' /proc/stat); sleep 2
a=$(awk '/^processes/{print $2}' /proc/stat); echo "$(( (a-b)/2 )) forks/sec"

# 1. start a select request
printf 'alpha\nbravo\n' | omarchy-menu-select 'ReproTest' -- --width 400 --height 300 &

# 2. supersede it with the regular menu (SUPER+ALT+SPACE works too)
omarchy-shell shell summon omarchy.menu ''

# 3. dismiss that menu with Escape, then observe
```

Result — the menu is fully gone from `hyprctl layers`, but:

```
$ ps -o pid,etimes,stat,wchan:20,args -p <pid>
    PID ETIMES STAT WCHAN                COMMAND
 160400     45 S    do_wait              /bin/bash .../omarchy-menu-select ReproTest ...

$ for i in $(seq 10); do pgrep -P 160400 -a; done | sort -u
161585 sleep 0.05
161588 sleep 0.05
161591 sleep 0.05
...
```

Measured fork rate, same machine:

| state | forks/sec |
|---|---|
| idle baseline | 2 |
| 1 leaked spinner | 20 |
| 15 leaked spinners (as found) | 273 |

Killing the orphans returns the machine to 2 forks/sec. The leaked processes
never exit on their own — I confirmed one still in `do_wait` after 45s, and the
in-the-wild one had been spinning for 560s when I found it.

## Suggested fix

Two independent problems; the first is the actual bug.

1. **Don't drop a pending request on the floor.** Have `openExistingMenu()` (and
   any other path that clears `doneFile`/`requestActive`) finish the outstanding
   request first, e.g. call `finishRequest(null)` before reassigning `mode`,
   rather than relying on a `cancel()` guard that the reassignment itself
   invalidates.

2. **Make the wait cheap and bounded.** Even once fixed, a 20 Hz fork loop is a
   costly way to wait on a file. `inotifywait` on the parent directory, or a
   `read` on a fifo, would block at zero cost — and a liveness check against the
   shell would keep a missed signal from hanging forever instead of leaking
   silently.

## System details

```
omarchy          4.0.0-1  (channel: stable, reported up to date)
quickshell-git   0.3.0.r20.g28771c7-2
hyprland         0.56.2-1
kernel           7.1.8-arch1-3
hardware         ASUS ROG Zephyrus G16 GU605CR
```

Both affected files are owned by `omarchy 4.0.0-1` per `pacman -Qo`.

## Not a duplicate of #7025

#7025 (`omarchy-menu-keybindings` hangs forever) also involves a stuck
keybindings menu, but the mechanism is unrelated: there, `build_lua_bind_cache()`
spins in an infinite `ipairs()` loop against a permissive `hl` stub, blocking
before the menu is ever summoned. Here the menu is summoned and displayed
normally, no Lua evaluation is involved (my configs contain no `get_monitors` /
`get_windows` / `get_workspaces` calls, and no `lua` process exists), and the
hang is in the post-summon `done_file` wait. Fixing #7025 would not address this.

It is the same *class* as #7124, #7150, #6989, #6881 and #6172 (orphaned helper
processes outliving their owner) but a different site and trigger.

I also checked the closed sluggishness reports: #6768 is Panther Lake / Arc B390
specific and was fixed in `omacom-io/omarchy-pkgs#138`, and #4336 concerns the
keybindings window's *contents*, not a leaked process. Neither is this bug, so
this is a new report rather than a regression.

---

*Filed by Claude Opus 5 via Claude Code.*

Describes 2 defects

Also describesdmenu request lifecycle drops waiters on re-summon, on a busy resultProc, and for doneFile-only requests KI-MENU-REQUEST-LIFECYCLE— 1 other report

Earliest report: R-1570 (filed Aug 22, 2026).

  1. Closed upstream on 2026-10-09 by the bot omarchybot, as a duplicate (target not recorded).

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.