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

Quickshell SIGSEGV tearing down the workspaces Repeater when Hyprland moves workspaces between monitors

Reported by kevinlouie · filed Aug 27, 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

Quickshell SIGSEGV tearing down the workspaces Repeater when Hyprland moves workspaces between monitorsrevision 1 · captured 2026-09-28 21:24 UTC via poll

What happened

Quickshell died with SIGSEGV while Hyprland was shuffling workspaces between monitors during a monitor-profile change. The shell restarted automatically; no user data was lost.

The fault is in the bar's workspaces widget. plugins/bar/widgets/Workspaces.qml rebuilds its Repeater from inside Quickshell's own mid-mutation valuesChanged emit, and its delegates hold live HyprlandWorkspace QObject references.

Symbolized stack

Arch debuginfod, Quickshell 0.3.1, Qt 6.11.2. si_addr = 0x3e8001ad392 (garbage pointer, not null).

#5  QV4::Value::as<QV4::QObjectWrapper>            qv4heap_p.h:46          <-- SEGV
#7  QV4::WeakValue::free                           qv4persistent.cpp:455
#10 QQmlData::destroyed                            qqmlengine.cpp:1427
#11 QObject::~QObject                              qobject.cpp:1084
#12 QQmlDMListAccessorData::~QQmlDMListAccessorData qqmldmlistaccessordata.cpp:25
#13 QQmlDelegateModelPrivate::release              qqmldelegatemodel.cpp:633
#14 QQuickRepeater::clear                          qquickrepeater.cpp:422
#15 QQuickRepeater::setModel                       qquickrepeater.cpp:179
#17 QQmlPropertyPrivate::write                     qqmlproperty.cpp:1978
#21 QQmlBinding::update                            qqmlbinding.cpp:166
#22 QQmlNotifier::emitNotify                       qqmlnotifier.cpp:75
#24 UntypedObjectModel::valuesChanged              moc_model.cpp:195
#25 ObjectModel<HyprlandWorkspace>::insertObject   core/model.hpp:93       <-- emit mid-insert
#26 ObjectModel<HyprlandWorkspace>::insertObjectSorted
#27 HyprlandIpc::onEvent                           ipc/connection.cpp:349
#28 HyprlandIpc::eventSocketReady                  ipc/connection.cpp:136
#33 QAbstractSocketPrivate::canReadNotification    qabstractsocket.cpp:673

Frame 27 locals identify the event exactly:

name    = "7"
existed = false

So Quickshell had no workspace 7, created one, and emitted valuesChanged() from inside insertObject while the model list was still being mutated. That synchronous emit re-enters QML, which tears the whole Repeater down.

Why the Repeater is torn down at all

plugins/bar/widgets/Workspaces.qml:51:

Repeater {
  model: root.workspaceIds()

workspaceIds() reads Hyprland.workspaces.values, so the binding is subscribed to valuesChanged, and it returns a fresh JS array on every evaluation. Every workspace add/remove therefore assigns a new model and triggers a full setModel → clear() → destroy-all-delegates cycle — from inside the mid-mutation emit. GridLayout.columns (line 45) binds the same signal.

The delegate holds a live QObject (line 55):

readonly property var workspace: root.workspaceById(modelData)
readonly property bool occupied: workspace !== null && workspace.toplevels.values.length > 0

Inference, not proven from the frames: the delegate for modelData == 7 still held a QObjectWrapper for the HyprlandWorkspace destroyed by the earlier destroyworkspace>>7. Destroying that delegate data inside the emit walks a stale V4 persistent slot, which matches the garbage si_addr rather than a null deref. The delegate identity is not recoverable from the available frames.

Trigger / reproduction

Framework Laptop 13 with two external displays, lid transition causing hyprmoncfgd to re-apply the "Desktop Mode" monitor profile while the lock screen was coming up. Quickshell's own log tail immediately before the crash:

21:22:02  omarchy lock ... secure=true
21:22:03  hyprmoncfgd: applied profile "Desktop Mode"; monitoradded:eDP-1
          destroyworkspace>>7
          monitoradded>>eDP-1
          moveworkspace>>1,DP-2
          destroyworkspace>>8
          moveworkspace>>3,DP-2
          workspace>>7                    <-- recreates ws 7 (existed=false)
          moveworkspace>>5,DP-2
          moveworkspace>>7,DP-2
          monitorremoved>>eDP-1
21:22:06  SIGSEGV

Any monitor add/remove that makes Hyprland destroy and recreate workspaces should reach this. It does not require the lock screen — the lock screen only supplied the monitor churn here.

Not a recent regression

The same crash class was recorded on this machine on 2026-08-16 (QQuickRepeater::regenerate / setModel) under quickshell-git 0.3.0.r20.g28771c7 and Qt 6.11.1, before the 2026-08-25 switch to quickshell 0.3.1. Workspaces.qml is byte-identical to current basecamp/omarchy HEAD.

Relationship to #6952

#6952 is the same class — a Repeater rebuilt from a model-mutation signal with delegates holding live Quickshell QObjects — but not the same bug. That one is PipeWire-driven and faults on the incubation path (VDMListDelegateDataType::createMissingProperties, qqmldmlistaccessordata_p.h:222, null QObject* row). This one is Hyprland-IPC-driven and faults on the destruction path (QQmlDMListAccessorData dtor → QV4::WeakValue::free), in a different file. Filing separately rather than as a comment there, but they likely want the same fix pattern.

Suggested fix

The codebase already has the defense in two places:

  • plugins/panels/bluetooth/Model.js deviceRow() — primitives-only rows, commented "delegates never hold Device QObject wrappers … which segfaults quickshell", with actions re-resolving the live object via deviceFor().
  • plugins/notifications/Service.qml:189 — Qt.callLater to avoid mutating a model while a Repeater is mid-incubation.

Workspaces.qml does neither. Projecting the delegate to primitives (id, occupied, focused) and resolving the HyprlandWorkspace only at press time would remove the dangling wrapper, and deferring the model reassignment with Qt.callLater would get the reassignment out of Quickshell's mid-mutation emit. Keeping the model array stable when the id set is unchanged would also cut most of the teardown churn, since workspaceIds() returns [1..5] plus extras and usually produces an identical list.

Arguably ObjectModel::insertObject emitting valuesChanged() before the insert has settled deserves an upstream Quickshell report too.

System

Omarchy       4.0.1-1 (stable channel)
quickshell    0.3.1-1
qt6-declarative 6.11.2-1
qt6-base      6.11.2-2
hyprland      0.56.2-1
kernel        7.1.9-arch1-2
hardware      Framework Laptop 13 (AMD Ryzen AI 300 Series)
monitors      DP-2 ASUS PG32UCDP 3840x2160@120, DP-7 PanaScope PX329 2560x1440@144, eDP-1 internal

Crash report on the reporting machine: ~/.cache/quickshell/crashes/pc12k0ckt (PID 1758098, 2026-08-26 21:22:06 CEST). omarchy debug is not available in 4.0.1, so the above was collected by hand.


Filed by Claude Opus 5 via Claude Code.

View raw text
## What happened

Quickshell died with SIGSEGV while Hyprland was shuffling workspaces between monitors during a monitor-profile change. The shell restarted automatically; no user data was lost.

The fault is in the bar's workspaces widget. `plugins/bar/widgets/Workspaces.qml` rebuilds its `Repeater` from inside Quickshell's own mid-mutation `valuesChanged` emit, and its delegates hold live `HyprlandWorkspace` QObject references.

## Symbolized stack

Arch debuginfod, Quickshell 0.3.1, Qt 6.11.2. `si_addr = 0x3e8001ad392` (garbage pointer, not null).

```
#5  QV4::Value::as<QV4::QObjectWrapper>            qv4heap_p.h:46          <-- SEGV
#7  QV4::WeakValue::free                           qv4persistent.cpp:455
#10 QQmlData::destroyed                            qqmlengine.cpp:1427
#11 QObject::~QObject                              qobject.cpp:1084
#12 QQmlDMListAccessorData::~QQmlDMListAccessorData qqmldmlistaccessordata.cpp:25
#13 QQmlDelegateModelPrivate::release              qqmldelegatemodel.cpp:633
#14 QQuickRepeater::clear                          qquickrepeater.cpp:422
#15 QQuickRepeater::setModel                       qquickrepeater.cpp:179
#17 QQmlPropertyPrivate::write                     qqmlproperty.cpp:1978
#21 QQmlBinding::update                            qqmlbinding.cpp:166
#22 QQmlNotifier::emitNotify                       qqmlnotifier.cpp:75
#24 UntypedObjectModel::valuesChanged              moc_model.cpp:195
#25 ObjectModel<HyprlandWorkspace>::insertObject   core/model.hpp:93       <-- emit mid-insert
#26 ObjectModel<HyprlandWorkspace>::insertObjectSorted
#27 HyprlandIpc::onEvent                           ipc/connection.cpp:349
#28 HyprlandIpc::eventSocketReady                  ipc/connection.cpp:136
#33 QAbstractSocketPrivate::canReadNotification    qabstractsocket.cpp:673
```

Frame 27 locals identify the event exactly:

```
name    = "7"
existed = false
```

So Quickshell had no workspace 7, created one, and emitted `valuesChanged()` from inside `insertObject` while the model list was still being mutated. That synchronous emit re-enters QML, which tears the whole Repeater down.

## Why the Repeater is torn down at all

`plugins/bar/widgets/Workspaces.qml:51`:

```qml
Repeater {
  model: root.workspaceIds()
```

`workspaceIds()` reads `Hyprland.workspaces.values`, so the binding is subscribed to `valuesChanged`, and it returns a **fresh JS array on every evaluation**. Every workspace add/remove therefore assigns a new model and triggers a full `setModel` → `clear()` → destroy-all-delegates cycle — from inside the mid-mutation emit. `GridLayout.columns` (line 45) binds the same signal.

The delegate holds a live QObject (line 55):

```qml
readonly property var workspace: root.workspaceById(modelData)
readonly property bool occupied: workspace !== null && workspace.toplevels.values.length > 0
```

Inference, not proven from the frames: the delegate for `modelData == 7` still held a `QObjectWrapper` for the `HyprlandWorkspace` destroyed by the earlier `destroyworkspace>>7`. Destroying that delegate data inside the emit walks a stale V4 persistent slot, which matches the garbage `si_addr` rather than a null deref. The delegate identity is not recoverable from the available frames.

## Trigger / reproduction

Framework Laptop 13 with two external displays, lid transition causing `hyprmoncfgd` to re-apply the "Desktop Mode" monitor profile while the lock screen was coming up. Quickshell's own log tail immediately before the crash:

```
21:22:02  omarchy lock ... secure=true
21:22:03  hyprmoncfgd: applied profile "Desktop Mode"; monitoradded:eDP-1
          destroyworkspace>>7
          monitoradded>>eDP-1
          moveworkspace>>1,DP-2
          destroyworkspace>>8
          moveworkspace>>3,DP-2
          workspace>>7                    <-- recreates ws 7 (existed=false)
          moveworkspace>>5,DP-2
          moveworkspace>>7,DP-2
          monitorremoved>>eDP-1
21:22:06  SIGSEGV
```

Any monitor add/remove that makes Hyprland destroy and recreate workspaces should reach this. It does not require the lock screen — the lock screen only supplied the monitor churn here.

## Not a recent regression

The same crash class was recorded on this machine on 2026-08-16 (`QQuickRepeater::regenerate` / `setModel`) under `quickshell-git 0.3.0.r20.g28771c7` and Qt 6.11.1, before the 2026-08-25 switch to `quickshell 0.3.1`. `Workspaces.qml` is byte-identical to current `basecamp/omarchy` HEAD.

## Relationship to #6952

#6952 is the same *class* — a `Repeater` rebuilt from a model-mutation signal with delegates holding live Quickshell QObjects — but not the same bug. That one is PipeWire-driven and faults on the **incubation** path (`VDMListDelegateDataType::createMissingProperties`, `qqmldmlistaccessordata_p.h:222`, null `QObject*` row). This one is Hyprland-IPC-driven and faults on the **destruction** path (`QQmlDMListAccessorData` dtor → `QV4::WeakValue::free`), in a different file. Filing separately rather than as a comment there, but they likely want the same fix pattern.

## Suggested fix

The codebase already has the defense in two places:

- `plugins/panels/bluetooth/Model.js` `deviceRow()` — primitives-only rows, commented *"delegates never hold Device QObject wrappers … which segfaults quickshell"*, with actions re-resolving the live object via `deviceFor()`.
- `plugins/notifications/Service.qml:189` — `Qt.callLater` to avoid mutating a model while a Repeater is mid-incubation.

`Workspaces.qml` does neither. Projecting the delegate to primitives (`id`, `occupied`, `focused`) and resolving the `HyprlandWorkspace` only at press time would remove the dangling wrapper, and deferring the model reassignment with `Qt.callLater` would get the reassignment out of Quickshell's mid-mutation emit. Keeping the model array stable when the id set is unchanged would also cut most of the teardown churn, since `workspaceIds()` returns `[1..5]` plus extras and usually produces an identical list.

Arguably `ObjectModel::insertObject` emitting `valuesChanged()` before the insert has settled deserves an upstream Quickshell report too.

## System

```
Omarchy       4.0.1-1 (stable channel)
quickshell    0.3.1-1
qt6-declarative 6.11.2-1
qt6-base      6.11.2-2
hyprland      0.56.2-1
kernel        7.1.9-arch1-2
hardware      Framework Laptop 13 (AMD Ryzen AI 300 Series)
monitors      DP-2 ASUS PG32UCDP 3840x2160@120, DP-7 PanaScope PX329 2560x1440@144, eDP-1 internal
```

Crash report on the reporting machine: `~/.cache/quickshell/crashes/pc12k0ckt` (PID 1758098, 2026-08-26 21:22:06 CEST). `omarchy debug` is not available in 4.0.1, so the above was collected by hand.

---

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 09:28 UTC