Skip to content

SIGSEGV in WindowsetManager::onServerCommit() / endPropertyUpdateGroup() on ext-workspace tree rebuild during output disconnect (niri, multi-monitor) #831

Description

@bzbetty

SIGSEGV in WindowsetManager::onServerCommit() / endPropertyUpdateGroup() on ext-workspace tree rebuild during output disconnect (niri, multi-monitor)

Summary

Quickshell reliably SIGSEGVs when the compositor (niri) rebuilds its entire ext-workspace-v1 tree in a single done batch — which happens on output disconnect/reconnect, e.g. every time the session locks and the displays blank. The crash is a use-after-free: a workspace is moved to a new group while its old group is destroyed in the same done batch, and when ext_workspace_manager_v1_done() emits serverCommit(), WindowsetManager::onServerCommit() dereferences state tied to the freed group while flushing the property update group.

This is distinct from the known lock/output-disconnect crashes (#540, #503, #287): those crash in QV4::QObjectWrapper::wrap() or wl_proxy_get_listener(). This one crashes in Qt::endPropertyUpdateGroup() reached from the windowset / ext-workspace path, and reproduces on a build that includes the recent wayland/lock fixes (b93d613, d1760ed).

Version

Quickshell 0.3.0 (revision 4b4fca3224ab977dc515ac0bb78d00b3dfa71e00, distributed by sam local)

Built from main @ 4b4fca3 (latest commit), Qt 6.10.2, gcc 15.2.0, RelWithDebInfo, jemalloc ON, crash handler ON.

Environment

  • Compositor: niri (Ubuntu 26.04 package)
  • Multi-monitor: HDMI-A-1 (ultrawide 2560x1080) + DP-3 (4K). Crash fires when one output drops/reconnects.
  • GPU: AMD (amdgpu, 1002:67df)
  • Shell config: DankMaterialShell (uses Quickshell workspace/windowset APIs)
  • Trigger: session lock (display blanks → niri reports output disconnect → workspace tree rebuilt). Also reproduces on raw monitor power-cycle.
  • Frequency: every lock; 40 crash dumps accumulated in ~/.cache/quickshell/crashes/ over two days.

Stacktrace

#0  libc.so.6
#1  Qt::endPropertyUpdateGroup()                                       libQt6Core.so.6
#2  qs::wm::wayland::WindowsetManager::onServerCommit()                src/wayland/windowmanager/windowset.cpp:122
#3  libQt6Core.so.6
#4  qs::wayland::workspace::WorkspaceManager::ext_workspace_manager_v1_done()  src/wayland/windowmanager/ext_workspace.cpp:56
#5-7  libffi.so.8 (ffi_call)
#8-10 libwayland-client.so.0 (wl_display_dispatch_queue_pending)
#11 QtWaylandClient::QWaylandDisplay::flushRequests()                  libQt6WaylandClient.so.6
#12-15 libQt6Core.so.6 (QCoreApplication::exec)
#16 qs::launch::launch(...)                                            src/launch/launch.cpp:320

Log tail (the smoking gun)

The compositor rebuilds the workspace tree in one done batch — note workspace 0x...838339e0 is removed from group 0x...4a03f4e0, that group is destroyed, then the same workspace is re-added to a new group 0x...4a03f420, all before Workspace changes done:

INFO  qml: [DMSShell] Screens changed: 1 'HDMI-A-1'
DEBUG quickshell.wm.wayland.workspace: Workspace 0x...552650 removed from group 0x...4a03f4e0
DEBUG quickshell.wm.wayland.workspace: Destroyed workspace 0x...552650
DEBUG quickshell.wm.wayland.workspace: Workspace 0x...838339e0 removed from group 0x...4a03f4e0
DEBUG quickshell.wm.wayland.workspace: Destroyed group 0x...4a03f4e0
DEBUG quickshell.wm.wayland.workspace: Workspace 0x...838339e0 added to group 0x...4a03f420
DEBUG quickshell.wm.wayland.workspace: Updated name for workspace 0x...838339e0 to "2"
DEBUG quickshell.wm.wayland.workspace: Updated coordinates for workspace 0x...838339e0 to QList(0, 1)
DEBUG quickshell.wm.wayland.workspace: Workspace changes done       <-- triggers ext_workspace_manager_v1_done()
                                                                    <-- SIGSEGV

Suspected cause

In WorkspaceManager::ext_workspace_manager_v1_done() (ext_workspace.cpp:54):

void WorkspaceManager::ext_workspace_manager_v1_done() {
	emit this->serverCommit();                                 // <-- onServerCommit runs HERE
	for (auto* workspace: this->destroyedWorkspaces) delete workspace;  // <-- freed AFTER
	for (auto* group: this->destroyedGroups) delete group;
	...
}

serverCommit() is emitted (running WindowsetManager::onServerCommit() and flushing a property update group at windowset.cpp:122) while destroyedGroups/destroyedWorkspaces still hold pointers slated for deletion. When the compositor moves a workspace to a new group and destroys the old group in the same batch, a windowset/projection bound to the destroyed group appears to be dereferenced during the property flush. On a single-output steady state this never fires; it only triggers when an output drop forces a full group teardown+rebuild in one done.

Reproducer

  1. niri, two outputs.
  2. A shell using the workspace/windowset APIs (DankMaterialShell here).
  3. Lock the session (or power-cycle a monitor) so niri drops an output and rebuilds the ext-workspace tree.
  4. Quickshell SIGSEGVs; crash dump under ~/.cache/quickshell/crashes/.

Full report.txt and log.qslog.log available on request.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions