Proof Portal
Omarchy
ProbeLabs65 findings · 95 requirementsA 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.
Quickshell crash (SIGSEGV) in tray submenu: reentrant Repeater model swap in Tray.qml
Reported by vayners · filed Sep 22, 2026 · open at source
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
What happened
The Quickshell-based bar's tray popup menu (plugins/bar/widgets/Tray.qml) segfaults when clicking a tray menu row that has a submenu (hasChildren). Crash report saved under ~/.cache/quickshell/crashes/<run-id>/.
Environment
- Omarchy: 4.0.4 (
BUILD_IDfrom/etc/os-release) - Quickshell: 0.3.1 (Arch package)
- Qt: 6.11.2
- GPU: Intel i915 (integrated), Wayland/Hyprland session
- Signal: SIGSEGV (11)
Root cause
Tray.qml's submenu popup renders rows through:
Repeater {
model: root.currentChildren
delegate: Item {
id: menuRow
...
MouseArea {
id: rowMouse
onClicked: {
if (root.menuLevelSettling) return
if (menuRow.modelData.hasChildren) {
trayMenuFlick.contentY = 0
root.enterSubmenu(menuRow.modelData, menuRow.rowText) // <-- synchronous
} else {
menuRow.modelData.triggered()
root.close()
}
}
}
}
}root.currentChildren is computed from root.submenuStack:
readonly property var currentChildren: submenuDepth > 0
? submenuStack[submenuDepth - 1].opener.children
: trayMenuOpener.childrenand enterSubmenu() reassigns that stack synchronously, from inside the very click handler that belongs to a delegate of the Repeater this stack feeds:
function enterSubmenu(entry, title) {
var opener = submenuOpenerComponent.createObject(root, { menu: entry })
if (!opener) return
var stack = submenuStack.slice()
stack.push({ opener: opener, title: title })
submenuStack = stack // <-- changes root.currentChildren immediately
settleMenuLevel()
}Setting submenuStack immediately changes root.currentChildren, which is the Repeater's model. That drives QQuickRepeater::regenerate(), which destroys and recreates delegates — including the delegate (`menuRow`/`rowMouse`) whose `onClicked` handler is still executing on the call stack. The existing comment on the call site (a few lines further down, guarding a different symptom) already acknowledges this:
// Reset scroll BEFORE swapping the model: the swap destroys this delegate synchronously and ids stop resolving after....but only the Flickable.contentY reset was hardened against it — not the reentrant destroy itself. The result is a use-after-free while Quickshell incubates the freshly-regenerated delegate mid-teardown of the old one.
leaveSubmenu() (called from the submenu header's back-button MouseArea, outside the affected Repeater) has the same synchronous-reassignment pattern, lower risk since the clicking MouseArea isn't itself inside the regenerated Repeater, but same bug class.
Stacktrace (top frames, full report attached)
Signal: Segmentation fault (11)
#0 ... libc.so.6
#1 ... libQt6QmlModels.so.6
#2 ... libQt6QmlModels.so.6
#3 ... libQt6Qml.so.6
#4 QV4::QObjectWrapper::getQmlProperty(...)
#5 QV4::QQmlContextWrapper::getPropertyAndBase(...)
#6 QV4::ExecutionContext::getProperty(QV4::String*)
#7 QV4::Runtime::TypeofName::call(...)
...
#10 QV4::Module::evaluate()
#12 QQmlEnginePrivate::createInternalContext(...)
#14 QQmlObjectCreator::createInstance(...)
#16 QQmlIncubatorPrivate::incubate(...)
#17 QQmlEnginePrivate::incubate(...)
#19 QQuickRepeater::regenerate()
#20 QQuickRepeater::setModel(QVariant const&)
...
#106 QQuickMouseArea::setPressed(...)
#107 QQuickMouseArea::mouseReleaseEvent(QMouseEvent*)i.e. mouse release on the row's MouseArea → onClicked JS → submenuStack write → Repeater::setModel → regenerate() → incubating the next delegate → crash evaluating a typeof expression in that delegate's freshly-created QML context.
Suggested fix
Defer the submenuStack write with Qt.callLater(...) so the model swap (and the resulting Repeater.regenerate()) runs after the triggering click's C++/QML call stack has fully unwound, instead of mid-event. This idiom is already used elsewhere in this same codebase for the same class of problem (e.g. shell.qml:1461, shell.qml:1484, Ui/Panel.qml:29, plugins/agents/Panel.qml:433), so it matches existing style.
Patch (tested locally against 4.0.4, no more crashes after repeated rapid submenu enter/leave clicks):
--- a/shell/plugins/bar/widgets/Tray.qml
+++ b/shell/plugins/bar/widgets/Tray.qml
@@ function enterSubmenu(entry, title) {
function enterSubmenu(entry, title) {
var opener = submenuOpenerComponent.createObject(root, { menu: entry })
if (!opener) return
var stack = submenuStack.slice()
stack.push({ opener: opener, title: title })
- submenuStack = stack
+ // Defer the model swap: this runs synchronously from a row's own
+ // MouseArea.onClicked, and reassigning submenuStack destroys that row's
+ // delegate (incl. the MouseArea still mid-event) via Repeater.regenerate,
+ // which segfaults incubating the next delegate mid-teardown. Qt.callLater
+ // runs the swap after the click's call stack has fully unwound instead.
+ Qt.callLater(function() { root.submenuStack = stack })
settleMenuLevel()
}
function leaveSubmenu() {
if (submenuStack.length === 0) return
var stack = submenuStack.slice()
var top = stack.pop()
- submenuStack = stack
- top.opener.destroy()
+ // Same reentrancy hazard as enterSubmenu (see comment there): defer the
+ // model swap past the triggering click's call stack.
+ Qt.callLater(function() {
+ root.submenuStack = stack
+ top.opener.destroy()
+ })
settleMenuLevel()
}The comment at shell/plugins/bar/widgets/Tray.qml around the row MouseArea.onClicked mentions the synchronous rebuild is intentional so "the next row lands under a cursor that hasn't moved" — Qt.callLater only defers to the next event-loop tick (microseconds), so cursor position is unaffected while the reentrant destroy is avoided.
Steps to reproduce
- Have a tray application whose menu contains at least one submenu entry (anything with nested
QMenus exposed viaorg.kde.StatusNotifierItem/QsMenu, e.g. NetworkManager applet, some Electron apps). - Open the tray icon's menu.
- Click the entry that has a submenu (chevron
›). - Occasionally (more reliably when clicking quickly / repeatedly, or navigating back and forth), Quickshell crashes with SIGSEGV.
Happy to open a PR with the patch above if useful — already running it locally without recurrence.
View raw text
## What happened
The Quickshell-based bar's tray popup menu (`plugins/bar/widgets/Tray.qml`) segfaults when clicking a tray menu row that has a submenu (`hasChildren`). Crash report saved under `~/.cache/quickshell/crashes/<run-id>/`.
## Environment
- Omarchy: 4.0.4 (`BUILD_ID` from `/etc/os-release`)
- Quickshell: 0.3.1 (Arch package)
- Qt: 6.11.2
- GPU: Intel i915 (integrated), Wayland/Hyprland session
- Signal: SIGSEGV (11)
## Root cause
`Tray.qml`'s submenu popup renders rows through:
```qml
Repeater {
model: root.currentChildren
delegate: Item {
id: menuRow
...
MouseArea {
id: rowMouse
onClicked: {
if (root.menuLevelSettling) return
if (menuRow.modelData.hasChildren) {
trayMenuFlick.contentY = 0
root.enterSubmenu(menuRow.modelData, menuRow.rowText) // <-- synchronous
} else {
menuRow.modelData.triggered()
root.close()
}
}
}
}
}
```
`root.currentChildren` is computed from `root.submenuStack`:
```qml
readonly property var currentChildren: submenuDepth > 0
? submenuStack[submenuDepth - 1].opener.children
: trayMenuOpener.children
```
and `enterSubmenu()` reassigns that stack **synchronously**, from inside the very click handler that belongs to a delegate of the Repeater this stack feeds:
```qml
function enterSubmenu(entry, title) {
var opener = submenuOpenerComponent.createObject(root, { menu: entry })
if (!opener) return
var stack = submenuStack.slice()
stack.push({ opener: opener, title: title })
submenuStack = stack // <-- changes root.currentChildren immediately
settleMenuLevel()
}
```
Setting `submenuStack` immediately changes `root.currentChildren`, which is the `Repeater`'s `model`. That drives `QQuickRepeater::regenerate()`, which destroys and recreates delegates — **including the delegate (`menuRow`/`rowMouse`) whose `onClicked` handler is still executing on the call stack**. The existing comment on the call site (a few lines further down, guarding a different symptom) already acknowledges this:
> `// Reset scroll BEFORE swapping the model: the swap destroys this delegate synchronously and ids stop resolving after.`
...but only the `Flickable.contentY` reset was hardened against it — not the reentrant destroy itself. The result is a use-after-free while Quickshell incubates the freshly-regenerated delegate mid-teardown of the old one.
`leaveSubmenu()` (called from the submenu header's back-button `MouseArea`, outside the affected Repeater) has the same synchronous-reassignment pattern, lower risk since the clicking `MouseArea` isn't itself inside the regenerated Repeater, but same bug class.
## Stacktrace (top frames, full report attached)
```
Signal: Segmentation fault (11)
#0 ... libc.so.6
#1 ... libQt6QmlModels.so.6
#2 ... libQt6QmlModels.so.6
#3 ... libQt6Qml.so.6
#4 QV4::QObjectWrapper::getQmlProperty(...)
#5 QV4::QQmlContextWrapper::getPropertyAndBase(...)
#6 QV4::ExecutionContext::getProperty(QV4::String*)
#7 QV4::Runtime::TypeofName::call(...)
...
#10 QV4::Module::evaluate()
#12 QQmlEnginePrivate::createInternalContext(...)
#14 QQmlObjectCreator::createInstance(...)
#16 QQmlIncubatorPrivate::incubate(...)
#17 QQmlEnginePrivate::incubate(...)
#19 QQuickRepeater::regenerate()
#20 QQuickRepeater::setModel(QVariant const&)
...
#106 QQuickMouseArea::setPressed(...)
#107 QQuickMouseArea::mouseReleaseEvent(QMouseEvent*)
```
i.e. mouse release on the row's `MouseArea` → `onClicked` JS → `submenuStack` write → `Repeater::setModel` → `regenerate()` → incubating the next delegate → crash evaluating a `typeof` expression in that delegate's freshly-created QML context.
## Suggested fix
Defer the `submenuStack` write with `Qt.callLater(...)` so the model swap (and the resulting `Repeater.regenerate()`) runs after the triggering click's C++/QML call stack has fully unwound, instead of mid-event. This idiom is already used elsewhere in this same codebase for the same class of problem (e.g. `shell.qml:1461`, `shell.qml:1484`, `Ui/Panel.qml:29`, `plugins/agents/Panel.qml:433`), so it matches existing style.
Patch (tested locally against 4.0.4, no more crashes after repeated rapid submenu enter/leave clicks):
```diff
--- a/shell/plugins/bar/widgets/Tray.qml
+++ b/shell/plugins/bar/widgets/Tray.qml
@@ function enterSubmenu(entry, title) {
function enterSubmenu(entry, title) {
var opener = submenuOpenerComponent.createObject(root, { menu: entry })
if (!opener) return
var stack = submenuStack.slice()
stack.push({ opener: opener, title: title })
- submenuStack = stack
+ // Defer the model swap: this runs synchronously from a row's own
+ // MouseArea.onClicked, and reassigning submenuStack destroys that row's
+ // delegate (incl. the MouseArea still mid-event) via Repeater.regenerate,
+ // which segfaults incubating the next delegate mid-teardown. Qt.callLater
+ // runs the swap after the click's call stack has fully unwound instead.
+ Qt.callLater(function() { root.submenuStack = stack })
settleMenuLevel()
}
function leaveSubmenu() {
if (submenuStack.length === 0) return
var stack = submenuStack.slice()
var top = stack.pop()
- submenuStack = stack
- top.opener.destroy()
+ // Same reentrancy hazard as enterSubmenu (see comment there): defer the
+ // model swap past the triggering click's call stack.
+ Qt.callLater(function() {
+ root.submenuStack = stack
+ top.opener.destroy()
+ })
settleMenuLevel()
}
```
The comment at `shell/plugins/bar/widgets/Tray.qml` around the row `MouseArea.onClicked` mentions the synchronous rebuild is intentional so "the next row lands under a cursor that hasn't moved" — `Qt.callLater` only defers to the next event-loop tick (microseconds), so cursor position is unaffected while the reentrant destroy is avoided.
## Steps to reproduce
1. Have a tray application whose menu contains at least one submenu entry (anything with nested `QMenu`s exposed via `org.kde.StatusNotifierItem`/`QsMenu`, e.g. NetworkManager applet, some Electron apps).
2. Open the tray icon's menu.
3. Click the entry that has a submenu (chevron `›`).
4. Occasionally (more reliably when clicking quickly / repeatedly, or navigating back and forth), Quickshell crashes with SIGSEGV.
Happy to open a PR with the patch above if useful — already running it locally without recurrence.
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
- Related PRpr/12892· 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 17:25 UTC