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-5049GitHub issuePublicReceived

Speed test panel leaks 8 unbounded curl workers when dismissed mid-run — saturates the link until reboot (152 GB in 76 min)

Reported by LAVA-3D · filed Sep 12, 2026Closed upstream on 2026-09-21 by bjarneo via an automated message, as a duplicate of #8515 (R-1389). GitHub close reason: completed.

Received and recorded verbatim. Nothing has been checked yet.

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?
Not yet mapped to any requirement or component.
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?
Our staff decide whether the report is valid. Where this project allows it, agents decide whether it duplicates another report or a known issue. No decision has been recorded yet.

Original report

Speed test panel leaks 8 unbounded curl workers when dismissed mid-run — saturates the link until reboot (152 GB in 76 min)revision 1 · captured 2026-10-04 12:10 UTC via poll

Summary

Dismissing the Speed Test panel while a measurement is running leaves its 8 traffic_worker subshells alive. They keep looping curl -o /dev/null against Netflix OCA nodes forever, at full line rate, with no byte cap and no timeout.

On my connection this silently downloaded 152 GB in 76 minutes at a sustained 496 Mbps. Every byte goes to /dev/null. Nothing in the UI indicates it is still running, and it survives until reboot or a manual kill. On a metered or capped connection this would be expensive.

Steps to reproduce

  1. Open the Ethernet (network) panel from the bar
  2. Click Speed Test
  3. Wait until the test actually starts (dials begin moving)
  4. Press Escape

The overlay closes, but the workers keep running:

$ ps -eo pid,ppid,etimes,args | grep '[o]marchy-network-speedtest'
 155244    1015      60 /bin/bash /usr/share/omarchy/bin/omarchy-network-speedtest down
 155245    1015      60 /bin/bash /usr/share/omarchy/bin/omarchy-network-speedtest down
 155246    1015      60 /bin/bash /usr/share/omarchy/bin/omarchy-network-speedtest down
 155249    1015      60 /bin/bash /usr/share/omarchy/bin/omarchy-network-speedtest down
 155251    1015      60 /bin/bash /usr/share/omarchy/bin/omarchy-network-speedtest down
 155253    1015      60 /bin/bash /usr/share/omarchy/bin/omarchy-network-speedtest down
 155255    1015      60 /bin/bash /usr/share/omarchy/bin/omarchy-network-speedtest down

ppid=1015 is systemd --user — the parent script is gone and the workers have been reparented. They sit at 496 Mbps indefinitely. Repeating the steps stacks another 8 on top (I accumulated 21 across three dismissals).

Expected: dismissing the panel stops all traffic.

Root cause

Two independent facts combine.

1. The workers are unbounded and rely entirely on the parent's EXIT trap.

bin/omarchy-network-speedtest forks parallel=8 background subshells:

traffic_worker() {
  ...
  while true; do
    url=${urls[$((idx % url_count))]}
    curl -fsS -o /dev/null "$url" 2>/dev/null || return
    idx=$((idx + 1))
  done
}

There is no --max-time, no byte cap, and no overall deadline. The only thing that ever stops them is trap cleanup EXIT in the parent.

2. That trap only survives SIGTERM, and the parent is being SIGKILLed.

Controlled test — start the script, let it settle, signal the parent, count survivors:

Signal to parentBeforeAfterSurvivors
SIGTERM90trap runs, workers reaped correctly
SIGKILL98all reparented to ppid=1015

The SIGKILL signature matches the production leak exactly (8 workers, ppid=1015).

On dismiss, plugins/panels/speedtest/Panel.qml close() does set speedTestProc.running = false, which sends SIGTERM — so the QML intent is correct. But the parent is blocked in sleep 1 inside its sampling loop:

while any_alive; do
  sleep 1
  ...
done

bash defers trap handling until the foreground sleep 1 returns, so the EXIT trap needs up to ~1s to fire. The panel is torn down and the Process object destroyed in that window, and the kill that follows lands before cleanup ever runs. The 8 workers are orphaned.

Note this makes the leak a race, which matches it being intermittent: dismissing very early or very late behaves differently from dismissing mid-phase.

Suggested fix

The durable fix is to make the workers self-limiting so they cannot outlive the parent regardless of which signal it receives. Relying on a trap in the parent is inherently fragile against SIGKILL.

Something like:

script_pid=$$   # capture before forking

traffic_worker() {
  local urls=("$@")
  ...
  while kill -0 "$script_pid" 2>/dev/null; do
    url=${urls[$((idx % url_count))]}
    curl --max-time 15 -fsS -o /dev/null "$url" 2>/dev/null || return
    idx=$((idx + 1))
  done
}

Two cheap belt-and-braces additions:

  • curl --max-time so a single request can never run unbounded
  • an overall wall-clock deadline in the script (the panel only ever needs ~5s per phase, so even a 30s cap would be generous) — this also protects any other caller, and the up direction has the same structure

Workaround

For anyone hitting this before a fix lands, a user-level systemd timer that reaps speedtest processes older than 60s (legitimate panel runs last ~10s) stops the bleeding. Verified: a 2s run is spared, an orphaned one is killed within a minute.

System

Omarchy4.0.3-1
Kernel7.2.3-arch1-3
CPUAMD Ryzen 5 3600 6-Core
GPUNVIDIA GeForce RTX 4070 Ti (AD104)
Hyprland0.56.2
quickshell0.3.1-1

(omarchy debug --no-sudo --print is referenced in the docs but omarchy debug is not a recognised command on 4.0.3 — details gathered manually.)

View raw text
## Summary

Dismissing the Speed Test panel while a measurement is running leaves its 8 `traffic_worker` subshells alive. They keep looping `curl -o /dev/null` against Netflix OCA nodes **forever**, at full line rate, with no byte cap and no timeout.

On my connection this silently downloaded **152 GB in 76 minutes** at a sustained 496 Mbps. Every byte goes to `/dev/null`. Nothing in the UI indicates it is still running, and it survives until reboot or a manual kill. On a metered or capped connection this would be expensive.

## Steps to reproduce

1. Open the Ethernet (network) panel from the bar
2. Click **Speed Test**
3. Wait until the test actually starts (dials begin moving)
4. Press **Escape**

The overlay closes, but the workers keep running:

```
$ ps -eo pid,ppid,etimes,args | grep '[o]marchy-network-speedtest'
 155244    1015      60 /bin/bash /usr/share/omarchy/bin/omarchy-network-speedtest down
 155245    1015      60 /bin/bash /usr/share/omarchy/bin/omarchy-network-speedtest down
 155246    1015      60 /bin/bash /usr/share/omarchy/bin/omarchy-network-speedtest down
 155249    1015      60 /bin/bash /usr/share/omarchy/bin/omarchy-network-speedtest down
 155251    1015      60 /bin/bash /usr/share/omarchy/bin/omarchy-network-speedtest down
 155253    1015      60 /bin/bash /usr/share/omarchy/bin/omarchy-network-speedtest down
 155255    1015      60 /bin/bash /usr/share/omarchy/bin/omarchy-network-speedtest down
```

`ppid=1015` is `systemd --user` — the parent script is gone and the workers have been reparented. They sit at 496 Mbps indefinitely. Repeating the steps stacks another 8 on top (I accumulated 21 across three dismissals).

**Expected:** dismissing the panel stops all traffic.

## Root cause

Two independent facts combine.

**1. The workers are unbounded and rely entirely on the parent's EXIT trap.**

`bin/omarchy-network-speedtest` forks `parallel=8` background subshells:

```bash
traffic_worker() {
  ...
  while true; do
    url=${urls[$((idx % url_count))]}
    curl -fsS -o /dev/null "$url" 2>/dev/null || return
    idx=$((idx + 1))
  done
}
```

There is no `--max-time`, no byte cap, and no overall deadline. The only thing that ever stops them is `trap cleanup EXIT` in the parent.

**2. That trap only survives SIGTERM, and the parent is being SIGKILLed.**

Controlled test — start the script, let it settle, signal the parent, count survivors:

| Signal to parent | Before | After | Survivors |
|---|---|---|---|
| SIGTERM | 9 | **0** | trap runs, workers reaped correctly |
| SIGKILL | 9 | **8** | all reparented to `ppid=1015` |

The SIGKILL signature matches the production leak exactly (8 workers, `ppid=1015`).

On dismiss, `plugins/panels/speedtest/Panel.qml` `close()` does set `speedTestProc.running = false`, which sends SIGTERM — so the QML intent is correct. But the parent is blocked in `sleep 1` inside its sampling loop:

```bash
while any_alive; do
  sleep 1
  ...
done
```

bash defers trap handling until the foreground `sleep 1` returns, so the EXIT trap needs up to ~1s to fire. The panel is torn down and the `Process` object destroyed in that window, and the kill that follows lands before `cleanup` ever runs. The 8 workers are orphaned.

Note this makes the leak a race, which matches it being intermittent: dismissing very early or very late behaves differently from dismissing mid-phase.

## Suggested fix

The durable fix is to make the workers self-limiting so they cannot outlive the parent regardless of which signal it receives. Relying on a trap in the parent is inherently fragile against SIGKILL.

Something like:

```bash
script_pid=$$   # capture before forking

traffic_worker() {
  local urls=("$@")
  ...
  while kill -0 "$script_pid" 2>/dev/null; do
    url=${urls[$((idx % url_count))]}
    curl --max-time 15 -fsS -o /dev/null "$url" 2>/dev/null || return
    idx=$((idx + 1))
  done
}
```

Two cheap belt-and-braces additions:

- `curl --max-time` so a single request can never run unbounded
- an overall wall-clock deadline in the script (the panel only ever needs ~5s per phase, so even a 30s cap would be generous) — this also protects any other caller, and the `up` direction has the same structure

## Workaround

For anyone hitting this before a fix lands, a user-level systemd timer that reaps speedtest processes older than 60s (legitimate panel runs last ~10s) stops the bleeding. Verified: a 2s run is spared, an orphaned one is killed within a minute.

## System

| | |
|---|---|
| Omarchy | 4.0.3-1 |
| Kernel | 7.2.3-arch1-3 |
| CPU | AMD Ryzen 5 3600 6-Core |
| GPU | NVIDIA GeForce RTX 4070 Ti (AD104) |
| Hyprland | 0.56.2 |
| quickshell | 0.3.1-1 |

(`omarchy debug --no-sudo --print` is referenced in the docs but `omarchy debug` is not a recognised command on 4.0.3 — details gathered manually.)

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.

No decision has been recorded yet.