aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorUlf Hermann <ulf.hermann@qt.io>2026-09-29 08:14:54 +0000
committerUlf Hermann <ulf.hermann@qt.io>2026-09-30 16:03:57 +0000
commit71761b88075ddc38791986e85a7903493f212b97 (patch)
treee7d7db8e6c83f0c242894fe3b118f227bb2f4512
parentd2796ee66b886a0d55faca933fba5c8301c8b303 (diff)
QtQml: Throw a ReferenceError when reading a name from a lost object
Once the QML scope object or the context object is gone, a name that was found on it is not defined anymore. The uncached QML context lookup throws a ReferenceError then, and so does the code qmlcachegen generates. The cached QML context lookups for properties and methods of the scope object and the context object, however, silently yielded undefined, and the function went on. Therefore, a function compiled by qmlcachegen behaved differently from the same function run by the interpreter or the JIT once the lookup had been cached: The compiled function aborted while the interpreted one continued with undefined and caused whatever side effects came after. Throw the ReferenceError from the cached lookups, too. Pick-to: 6.12 6.8 Change-Id: I755e04c2158abf0c2a3e234564722155ba583d9f Reviewed-by: Olivier De Cannière <olivier.decanniere@qt.io>
-rw-r--r--src/qml/jsruntime/qv4qmlcontext.cpp35
-rw-r--r--tests/auto/qml/qmlcppcodegen/data/CMakeLists.txt1
-rw-r--r--tests/auto/qml/qmlcppcodegen/data/lostContextObject.qml28
-rw-r--r--tests/auto/qml/qmlcppcodegen/tst_qmlcppcodegen.cpp43
-rw-r--r--tests/auto/qml/qqmlecmascript/data/deletedContextObjectProperty.qml22
-rw-r--r--tests/auto/qml/qqmlecmascript/tst_qqmlecmascript.cpp57
6 files changed, 170 insertions, 16 deletions
diff --git a/src/qml/jsruntime/qv4qmlcontext.cpp b/src/qml/jsruntime/qv4qmlcontext.cpp
index ecf56e90c5..0fd7cdc1ff 100644
--- a/src/qml/jsruntime/qv4qmlcontext.cpp
+++ b/src/qml/jsruntime/qv4qmlcontext.cpp
@@ -632,8 +632,17 @@ static ReturnedValue revertObjectMethodLookup(Lookup *l, ExecutionEngine *engine
return QQmlContextWrapper::resolveQmlContextPropertyLookupGetter(l, engine, base);
}
+// The object may be gone once it is being destroyed. Its name is not defined then, just like
+// when resolving the name anew.
+static ReturnedValue throwLostObjectReferenceError(Lookup *l, ExecutionEngine *engine)
+{
+ return engine->throwReferenceError(
+ engine->currentStackFrame->v4Function->compilationUnit->runtimeStrings[l->nameIndex]
+ ->toQString());
+}
+
template<typename Call>
-ReturnedValue callWithScopeObject(ExecutionEngine *engine, Value *base, Call c)
+ReturnedValue callWithScopeObject(Lookup *l, ExecutionEngine *engine, Value *base, Call c)
{
Scope scope(engine);
Scoped<QmlContext> qmlContext(scope, engine->qmlContext());
@@ -641,11 +650,8 @@ ReturnedValue callWithScopeObject(ExecutionEngine *engine, Value *base, Call c)
return QV4::Encode::undefined();
QObject *scopeObject = qmlContext->qmlScope();
- if (!scopeObject)
- return QV4::Encode::undefined();
-
- if (QQmlData::wasDeleted(scopeObject))
- return QV4::Encode::undefined();
+ if (!scopeObject || QQmlData::wasDeleted(scopeObject))
+ return throwLostObjectReferenceError(l, engine);
ScopedValue obj(scope, QV4::QObjectWrapper::wrap(engine, scopeObject));
@@ -657,7 +663,7 @@ ReturnedValue callWithScopeObject(ExecutionEngine *engine, Value *base, Call c)
ReturnedValue QQmlContextWrapper::lookupScopeObjectProperty(Lookup *l, ExecutionEngine *engine, Value *base)
{
- return callWithScopeObject(engine, base, [l, engine, base](const Value &obj) {
+ return callWithScopeObject(l, engine, base, [l, engine, base](const Value &obj) {
const QObjectWrapper::Flags flags = l->forCall
? QObjectWrapper::NoFlag
: QObjectWrapper::AttachMethods;
@@ -669,7 +675,7 @@ ReturnedValue QQmlContextWrapper::lookupScopeObjectProperty(Lookup *l, Execution
ReturnedValue QQmlContextWrapper::lookupScopeObjectMethod(Lookup *l, ExecutionEngine *engine, Value *base)
{
- return callWithScopeObject(engine, base, [l, engine, base](const Value &obj) {
+ return callWithScopeObject(l, engine, base, [l, engine, base](const Value &obj) {
const QObjectWrapper::Flags flags = l->forCall
? QObjectWrapper::NoFlag
: QObjectWrapper::AttachMethods;
@@ -680,7 +686,7 @@ ReturnedValue QQmlContextWrapper::lookupScopeObjectMethod(Lookup *l, ExecutionEn
}
template<typename Call>
-ReturnedValue callWithContextObject(ExecutionEngine *engine, Value *base, Call c)
+ReturnedValue callWithContextObject(Lookup *l, ExecutionEngine *engine, Value *base, Call c)
{
Scope scope(engine);
Scoped<QmlContext> qmlContext(scope, engine->qmlContext());
@@ -692,11 +698,8 @@ ReturnedValue callWithContextObject(ExecutionEngine *engine, Value *base, Call c
return QV4::Encode::undefined();
QObject *contextObject = context->contextObject();
- if (!contextObject)
- return QV4::Encode::undefined();
-
- if (QQmlData::wasDeleted(contextObject))
- return QV4::Encode::undefined();
+ if (!contextObject || QQmlData::wasDeleted(contextObject))
+ return throwLostObjectReferenceError(l, engine);
ScopedValue obj(scope, QV4::QObjectWrapper::wrap(engine, contextObject));
@@ -709,7 +712,7 @@ ReturnedValue callWithContextObject(ExecutionEngine *engine, Value *base, Call c
ReturnedValue QQmlContextWrapper::lookupContextObjectProperty(
Lookup *l, ExecutionEngine *engine, Value *base)
{
- return callWithContextObject(engine, base, [l, engine, base](const Value &obj) {
+ return callWithContextObject(l, engine, base, [l, engine, base](const Value &obj) {
const QObjectWrapper::Flags flags = l->forCall
? QObjectWrapper::NoFlag
: QObjectWrapper::AttachMethods;
@@ -722,7 +725,7 @@ ReturnedValue QQmlContextWrapper::lookupContextObjectProperty(
ReturnedValue QQmlContextWrapper::lookupContextObjectMethod(
Lookup *l, ExecutionEngine *engine, Value *base)
{
- return callWithContextObject(engine, base, [l, engine, base](const Value &obj) {
+ return callWithContextObject(l, engine, base, [l, engine, base](const Value &obj) {
const QObjectWrapper::Flags flags = l->forCall
? QObjectWrapper::NoFlag
: QObjectWrapper::AttachMethods;
diff --git a/tests/auto/qml/qmlcppcodegen/data/CMakeLists.txt b/tests/auto/qml/qmlcppcodegen/data/CMakeLists.txt
index 009edbb34d..92831cb33d 100644
--- a/tests/auto/qml/qmlcppcodegen/data/CMakeLists.txt
+++ b/tests/auto/qml/qmlcppcodegen/data/CMakeLists.txt
@@ -248,6 +248,7 @@ set(qml_files
listPropertyAsModel.qml
listToString.qml
listlength.qml
+ lostContextObject.qml
markJSValue.qml
markRecursive.qml
math.qml
diff --git a/tests/auto/qml/qmlcppcodegen/data/lostContextObject.qml b/tests/auto/qml/qmlcppcodegen/data/lostContextObject.qml
new file mode 100644
index 0000000000..772058190c
--- /dev/null
+++ b/tests/auto/qml/qmlcppcodegen/data/lostContextObject.qml
@@ -0,0 +1,28 @@
+pragma ComponentBehavior: Bound
+import QtQml
+
+QtObject {
+ id: root
+
+ property QtObject inner: component.createObject(root)
+
+ property Component component: Component {
+ QtObject {
+ property int value: 5
+
+ function read(): int {
+ const v = value
+ console.warn("continued")
+ return v
+ }
+
+ property QtObject child: QtObject {
+ function read(): int {
+ const v = value
+ console.warn("continued")
+ return v
+ }
+ }
+ }
+ }
+}
diff --git a/tests/auto/qml/qmlcppcodegen/tst_qmlcppcodegen.cpp b/tests/auto/qml/qmlcppcodegen/tst_qmlcppcodegen.cpp
index e8ced6cbea..4fa96c7f49 100644
--- a/tests/auto/qml/qmlcppcodegen/tst_qmlcppcodegen.cpp
+++ b/tests/auto/qml/qmlcppcodegen/tst_qmlcppcodegen.cpp
@@ -212,6 +212,8 @@ private slots:
void listOfInlineComponent();
void listPropertyAsModel();
void listToString();
+ void lostContextObject_data();
+ void lostContextObject();
void lotsOfRegisters();
void math();
void mathMinMax();
@@ -4006,6 +4008,47 @@ void tst_QmlCppCodegen::listToString()
std::unique_ptr<QObject> o(c.create());
}
+void tst_QmlCppCodegen::lostContextObject_data()
+{
+ QTest::addColumn<bool>("onScopeObject");
+ QTest::newRow("scope object") << true;
+ QTest::newRow("context object") << false;
+}
+
+void tst_QmlCppCodegen::lostContextObject()
+{
+ QFETCH(bool, onScopeObject);
+
+ QQmlEngine engine;
+ QQmlComponent component(&engine, QUrl(u"qrc:/qt/qml/TestTypes/lostContextObject.qml"_s));
+ QVERIFY2(component.isReady(), qPrintable(component.errorString()));
+ std::unique_ptr<QObject> root(component.create());
+ QVERIFY(root);
+
+ QObject *inner = root->property("inner").value<QObject *>();
+ QVERIFY(inner);
+ QObject *child = inner->property("child").value<QObject *>();
+ QVERIFY(child);
+
+ // The child outlives its context object, like a control's background.
+ child->setParent(root.get());
+
+ QJSValue read = engine.newQObject(onScopeObject ? inner : child).property(u"read"_s);
+ QVERIFY(read.isCallable());
+
+ // Populate the lookups.
+ QTest::ignoreMessage(QtWarningMsg, "continued");
+ QCOMPARE(read.call().toInt(), 5);
+
+ delete inner;
+
+ // Reading the value of the lost object throws. The function must not continue.
+ QTest::failOnWarning("continued");
+ const QJSValue result = read.call();
+ QVERIFY(result.isError());
+ QCOMPARE(result.errorType(), QJSValue::ReferenceError);
+}
+
void tst_QmlCppCodegen::lotsOfRegisters()
{
QQmlEngine engine;
diff --git a/tests/auto/qml/qqmlecmascript/data/deletedContextObjectProperty.qml b/tests/auto/qml/qqmlecmascript/data/deletedContextObjectProperty.qml
new file mode 100644
index 0000000000..4315eea822
--- /dev/null
+++ b/tests/auto/qml/qqmlecmascript/data/deletedContextObjectProperty.qml
@@ -0,0 +1,22 @@
+import QtQml
+
+QtObject {
+ id: root
+
+ property QtObject inner: component.createObject(root)
+
+ property Component component: Component {
+ QtObject {
+ property int value: 5
+ function method() { return 7 }
+
+ function readValue() { return value }
+ function callMethod() { return method() }
+
+ property QtObject child: QtObject {
+ function readValue() { return value }
+ function callMethod() { return method() }
+ }
+ }
+ }
+}
diff --git a/tests/auto/qml/qqmlecmascript/tst_qqmlecmascript.cpp b/tests/auto/qml/qqmlecmascript/tst_qqmlecmascript.cpp
index f773e8477d..c6b57b8cdd 100644
--- a/tests/auto/qml/qqmlecmascript/tst_qqmlecmascript.cpp
+++ b/tests/auto/qml/qqmlecmascript/tst_qqmlecmascript.cpp
@@ -451,6 +451,8 @@ private slots:
void namedFunctionExpressionOwnNameIsShadowable();
void cuObjectIndex();
void vmeMetaObjectAccessors();
+ void deletedContextObjectProperty_data();
+ void deletedContextObjectProperty();
private:
// static void propertyVarWeakRefCallback(v8::Persistent<v8::Value> object, void* parameter);
@@ -10983,6 +10985,61 @@ void tst_qqmlecmascript::vmeMetaObjectAccessors()
vme->parentVMEMetaObject();
}
+void tst_qqmlecmascript::deletedContextObjectProperty_data()
+{
+ QTest::addColumn<bool>("onScopeObject");
+ QTest::addColumn<QString>("function");
+ QTest::addColumn<QVariant>("expected");
+ QTest::addColumn<bool>("cached");
+
+ for (const bool cached : { false, true }) {
+ const char *suffix = cached ? " cached" : "";
+ QTest::addRow("scope object property%s", suffix)
+ << true << u"readValue"_s << QVariant(5) << cached;
+ QTest::addRow("scope object method%s", suffix)
+ << true << u"callMethod"_s << QVariant(7) << cached;
+ QTest::addRow("context object property%s", suffix)
+ << false << u"readValue"_s << QVariant(5) << cached;
+ QTest::addRow("context object method%s", suffix)
+ << false << u"callMethod"_s << QVariant(7) << cached;
+ }
+}
+
+void tst_qqmlecmascript::deletedContextObjectProperty()
+{
+ QFETCH(bool, onScopeObject);
+ QFETCH(QString, function);
+ QFETCH(QVariant, expected);
+ QFETCH(bool, cached);
+
+ QQmlEngine engine;
+ QQmlComponent component(&engine, testFileUrl("deletedContextObjectProperty.qml"));
+ QVERIFY2(component.isReady(), qPrintable(component.errorString()));
+ QScopedPointer<QObject> root(component.create());
+ QVERIFY(root);
+
+ QObject *inner = root->property("inner").value<QObject *>();
+ QVERIFY(inner);
+ QObject *child = inner->property("child").value<QObject *>();
+ QVERIFY(child);
+
+ // Like a control's background, the child is not a QObject child of its context object. It is
+ // not marked as deleted along with it, and its functions can still run after the context has
+ // lost its context object.
+ child->setParent(root.data());
+
+ QJSValue fn = engine.newQObject(onScopeObject ? inner : child).property(function);
+ QVERIFY(fn.isCallable());
+ if (cached)
+ QCOMPARE(fn.call().toVariant(), expected);
+
+ // No matter whether the lookup was cached before, the name is not defined anymore.
+ QQmlData::markAsDeleted(inner);
+ const QJSValue result = fn.call();
+ QVERIFY(result.isError());
+ QCOMPARE(result.errorType(), QJSValue::ReferenceError);
+}
+
QTEST_MAIN(tst_qqmlecmascript)
#include "tst_qqmlecmascript.moc"