diff options
| author | Ulf Hermann <ulf.hermann@qt.io> | 2026-09-29 08:14:54 +0000 |
|---|---|---|
| committer | Ulf Hermann <ulf.hermann@qt.io> | 2026-09-30 16:03:57 +0000 |
| commit | 71761b88075ddc38791986e85a7903493f212b97 (patch) | |
| tree | e7d7db8e6c83f0c242894fe3b118f227bb2f4512 | |
| parent | d2796ee66b886a0d55faca933fba5c8301c8b303 (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>
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" |
