hyprland/ipc: use deleteLater() for workspace and toplevel deletion#499
hyprland/ipc: use deleteLater() for workspace and toplevel deletion#499simaotwx wants to merge 1 commit intoquickshell-mirror:masterfrom
Conversation
Fix use-after-free crash when rapidly switching workspaces. The crash occurs because QML bindings may still reference workspace/toplevel objects during the same event loop iteration after immediate deletion. Crash stack trace: #0 QV4::QObjectWrapper::wrap (libQt6Qml.so.6) quickshell-mirror#1 QV4::loadProperty (libQt6Qml.so.6) quickshell-mirror#2 QV4::QQmlTypeWrapper::lookupSingletonProperty (libQt6Qml.so.6) Using deleteLater() defers deletion to the next event loop iteration, ensuring all QML bindings have completed their updates first.
|
I haven't been able to reproduce the actual crash at all, or a contrived sample. The QML engine uses QPointer or an equivalent internally that auto-nulls references, and queued connections don't crash, so this has been kind of an interesting issue for me since it has popped up occasionally. I took some time trying to write something crash prone but I haven't been able to trip it. Can you? class DummyChild: public QObject {
Q_OBJECT;
QML_ELEMENT;
Q_PROPERTY(QString dummyProp MEMBER mDummy NOTIFY dummyChanged);
public:
DummyChild() = default;
DummyChild(QObject* parent): QObject(parent) {}
Q_INVOKABLE void deleteMe();
QString mDummy = "foo";
signals:
void dummyChanged();
void aboutToDelete();
};
class DummyObject: public QObject {
Q_OBJECT;
QML_ELEMENT;
Q_PROPERTY(DummyChild* child MEMBER mChild NOTIFY childChanged);
public:
DummyObject();
Q_INVOKABLE void deleteChild();
DummyChild* mChild = new DummyChild(this);
private slots:
void childDestroyed();
signals:
void childChanged();
};void DummyChild::deleteMe() {
emit this->aboutToDelete();
mDummy = "bar";
QMetaObject::invokeMethod(this, &DummyChild::dummyChanged, Qt::QueuedConnection);
delete this;
}
void DummyObject::deleteChild() {
if (!this->mChild) return;
this->mChild->deleteMe();
}
DummyObject::DummyObject() {
QObject::connect(this->mChild, &QObject::destroyed, this, &DummyObject::childDestroyed);
}
void DummyObject::childDestroyed() {
qDebug() << "Child destroyed";
this->mChild = nullptr;
emit this->childChanged();
}import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
FloatingWindow {
id: root
DummyObject {
id: bait
child.onAboutToDelete: {
console.log(`About to delete ${bait.child}!`)
root.childHolder = bait.child
}
}
property var childHolder;
property string fooProp: `child: ${bait.child} dummy ${childHolder.dummyProp}`
onFooPropChanged: console.log(fooProp)
RowLayout {
Button {
text: "Parent"
onClicked: bait.deleteChild()
}
Button {
text: "Child"
onClicked: bait.child.deleteMe()
}
}
} |
|
Thanks for getting back to me, I haven't had the crash ever since I added the patch in this PR to my local build, so I assume it fixes the issue even though it's not easy to reproduce. I'll see if I can find some time to test out your suggestion. |
|
Thanks. My main concern about merging things like this is I want to understand the actual scope of why and where this is a problem because if its here its likely in any number of other places. |
Problem
Quickshell crashes with SIGSEGV when rapidly switching workspaces in Hyprland. The crash occurs because workspace and toplevel objects are deleted immediately while QML bindings may still reference them during the same event loop iteration.
Root Cause
In src/wayland/hyprland/ipc/connection.cpp, workspace and toplevel objects are deleted with delete immediately after being removed from their containers. However, QML property bindings that depend on these objects may not have been re-evaluated yet, causing them to access freed memory when the Qt event loop continues processing.
Solution
Replace delete workspace and delete toplevel with deleteLater() to defer destruction until the next event loop iteration. This ensures all QML bindings have completed their updates before the objects are destroyed.
Stack trace
Testing
Rapidly switch between workspaces using keybindings (e.g., Mod+1, Mod+2 repeatedly). Before this fix, quickshell would crash within a few switches. After the fix, no crashes occur.
Here are some snippets to demonstrate where this issue was coming from: