aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLinus Jahn <lnj@kaidan.im>2026-08-06 01:03:07 +0200
committerQt Cherry-pick Bot <cherrypick_bot@qt-project.org>2026-09-08 14:12:44 +0000
commita5fca50366313c05cb0e1a573761871d93554cfb (patch)
tree44f7b31554329f1dfa4328d4601cde2d290e5c4d
parent374476a50c5031d1790cda7a2a286e92b50b5432 (diff)
QtQuick Templates: Don't begin deferred execution re-entrantly
Populating a deferred property can re-enter quickBeginDeferred(): writing the property notifies its observers, and a binding among them may read the same property back. If the deferred binding evaluates to null, the pointer stays null, so the caller's "if (!delegate || complete)" check does not stop that second execution. The second QtQuickPrivate::beginDeferred() then incremented QQmlEnginePrivate::inProgressCreations and stored its DeferredState in the delegate, where it replaced the outer one. The outer state ended up in a local variable of the nested call and was destroyed without ever being completed, so the counter was never decremented and the engine complained on teardown: There are still "N" items in the process of being created at engine destruction. Bail out when the delegate is already executing. This is hit in practice by kirigami-addons' FormRadioDelegate, which uses RadioButton { contentItem: null }, in combination with qqc2-desktop-style, whose indicator reads control.contentItem from its x/y bindings. Pick-to: 6.11 Fixes: QTBUG-148846 Change-Id: I0ddb8ef374efeb68ce59db285d54664e99606854 Reviewed-by: Mitch Curtis <mitch.curtis@qt.io> Reviewed-by: Olivier De Cannière <olivier.decanniere@qt.io> (cherry picked from commit 77fc1fd6d4d3b2c3222564f34a0f80ea102fa9e4) Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
-rw-r--r--src/quicktemplates/qquickdeferredexecute_p_p.h5
-rw-r--r--tests/auto/quickcontrols/deferred/data/reentrantExecution.qml9
-rw-r--r--tests/auto/quickcontrols/deferred/tst_qquickdeferred.cpp66
3 files changed, 80 insertions, 0 deletions
diff --git a/src/quicktemplates/qquickdeferredexecute_p_p.h b/src/quicktemplates/qquickdeferredexecute_p_p.h
index aa6ddc4c87..946ae584fb 100644
--- a/src/quicktemplates/qquickdeferredexecute_p_p.h
+++ b/src/quicktemplates/qquickdeferredexecute_p_p.h
@@ -41,6 +41,11 @@ void quickBeginDeferred(QObject *object, const QString &property, QQuickDeferred
if (!QQmlVME::componentCompleteEnabled())
return;
+ // Populating can re-enter this: writing the property notifies observers, and a binding among
+ // them may read it back. Beginning again would orphan the outer DeferredState.
+ if (delegate.isExecuting())
+ return;
+
QtQuickPrivate::beginDeferred(object, property, &delegate, delegate.setExecuting(true));
delegate.setExecuting(false);
}
diff --git a/tests/auto/quickcontrols/deferred/data/reentrantExecution.qml b/tests/auto/quickcontrols/deferred/data/reentrantExecution.qml
new file mode 100644
index 0000000000..069f40510e
--- /dev/null
+++ b/tests/auto/quickcontrols/deferred/data/reentrantExecution.qml
@@ -0,0 +1,9 @@
+// Copyright (C) 2026 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import test
+
+// Overrides the default from classBegin(), so the property changes while it is being executed.
+ReentrantDeferredTester {
+ objectProperty: null
+}
diff --git a/tests/auto/quickcontrols/deferred/tst_qquickdeferred.cpp b/tests/auto/quickcontrols/deferred/tst_qquickdeferred.cpp
index 1c50531d57..bc1685b4c6 100644
--- a/tests/auto/quickcontrols/deferred/tst_qquickdeferred.cpp
+++ b/tests/auto/quickcontrols/deferred/tst_qquickdeferred.cpp
@@ -7,6 +7,8 @@
#include <QtQuick/qquickitem.h>
#include <QtQuickTemplates2/private/qquickdeferredexecute_p_p.h>
#include <QQmlIncubator>
+#include <QtQml/private/qqmlengine_p.h>
+#include <QQmlParserStatus>
class DeferredPropertyTester : public QObject
{
@@ -42,6 +44,55 @@ private:
QQuickDeferredPointer<QQuickItem> m_object = nullptr;
};
+// Mirrors how QQuickControl handles its deferred contentItem.
+class ReentrantDeferredTester : public QObject, public QQmlParserStatus
+{
+ Q_OBJECT
+ Q_INTERFACES(QQmlParserStatus)
+ Q_PROPERTY(QQuickItem *objectProperty READ objectProperty WRITE setObjectProperty NOTIFY objectChanged)
+ Q_CLASSINFO("DeferredPropertyNames", "objectProperty")
+
+public:
+ void classBegin() override { m_object = new QQuickItem; } // the default a style would set
+ void componentComplete() override { execute(true); }
+
+ QQuickItem *objectProperty()
+ {
+ execute(false);
+ return m_object;
+ }
+
+ void setObjectProperty(QQuickItem *obj)
+ {
+ if (m_object == obj)
+ return;
+ m_object = obj;
+
+ // Stands in for setContentItem_helper(), where writing the property updates the implicit
+ // size, which notifies observers, and a binding among them reads the property back.
+ if (m_object.isExecuting())
+ objectProperty();
+ else
+ emit objectChanged();
+ }
+
+signals:
+ void objectChanged();
+
+private:
+ void execute(bool complete)
+ {
+ if (m_object.wasExecuted())
+ return;
+ if (!m_object || complete)
+ quickBeginDeferred(this, "objectProperty", m_object);
+ if (complete)
+ quickCompleteDeferred(this, "objectProperty", m_object);
+ }
+
+ QQuickDeferredPointer<QQuickItem> m_object = nullptr;
+};
+
class tst_qquickdeferred : public QQmlDataTest
{
Q_OBJECT
@@ -50,6 +101,7 @@ public:
private slots:
void noSpuriousBinding();
void abortedIncubation();
+ void reentrantExecution();
};
@@ -84,6 +136,20 @@ void tst_qquickdeferred::abortedIncubation()
}
}
+// Reading the property back while it is being populated must not start a second deferred
+// creation: the outer one would be orphaned and never completed.
+void tst_qquickdeferred::reentrantExecution()
+{
+ qmlRegisterType<ReentrantDeferredTester>("test", 1, 0, "ReentrantDeferredTester");
+
+ QQmlEngine engine;
+ QQmlComponent comp(&engine, testFileUrl("reentrantExecution.qml"));
+ std::unique_ptr<QObject> root(comp.create());
+ QVERIFY2(root, qPrintable(comp.errorString()));
+
+ QCOMPARE(QQmlEnginePrivate::get(&engine)->inProgressCreations, 0);
+}
+
QTEST_MAIN(tst_qquickdeferred)
#include "tst_qquickdeferred.moc"