diff options
| author | Ulf Hermann <ulf.hermann@qt.io> | 2025-09-11 09:17:18 +0200 |
|---|---|---|
| committer | Qt Cherry-pick Bot <cherrypick_bot@qt-project.org> | 2025-09-16 01:49:52 +0000 |
| commit | 37b529d2de0c5fc7c6ae57b92b2039a8c8635af0 (patch) | |
| tree | 6301c97d2777d27f22ed8067f5f9e18df9ac2459 | |
| parent | 24335a40a75a17d44003791f7f914f1f5e849b32 (diff) | |
QmlCompiler: Guard against disappearing arrow functions
You can override a QObject method with a JavaScript function and take
away the JavaScript function later by swapping out objects. This should
not crash.
Fixes: QTBUG-140074
Change-Id: I85b17f4f619235024d0f1a27b4ff4128c7a57083
Reviewed-by: Sami Shalayel <sami.shalayel@qt.io>
(cherry picked from commit 7105eb6d0d46949e235d213cfe77dda95f16c6c5)
Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
(cherry picked from commit d478f1112908e10780418ddefa81b5b80bfb53d7)
| -rw-r--r-- | src/qml/qml/qqml.cpp | 65 | ||||
| -rw-r--r-- | tests/auto/qml/qmlcppcodegen/data/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | tests/auto/qml/qmlcppcodegen/data/disappearingArrowFunction.qml | 28 | ||||
| -rw-r--r-- | tests/auto/qml/qmlcppcodegen/tst_qmlcppcodegen.cpp | 51 |
4 files changed, 107 insertions, 38 deletions
diff --git a/src/qml/qml/qqml.cpp b/src/qml/qml/qqml.cpp index 90b69b73ac..771d3744d1 100644 --- a/src/qml/qml/qqml.cpp +++ b/src/qml/qml/qqml.cpp @@ -2138,8 +2138,13 @@ static bool callQObjectMethodAsVariant( QV4::Scope scope(engine); QV4::ScopedValue wrappedObject(scope, QV4::QObjectWrapper::wrap(scope.engine, thisObject)); QV4::ScopedFunctionObject function(scope, lookup->getter(scope.engine, wrappedObject)); - Q_ASSERT(function); - Q_ASSERT(lookup->asVariant); // The getter mustn't reset the isVariant flag + + // The getter mustn't reset the isVariant flag + Q_ASSERT(lookup->asVariant); + + // Since we have an asVariant lookup, the function may have been overridden in the mean time. + if (!function) + return false; Q_ALLOCA_VAR(QMetaType, types, (argc + 1) * sizeof(QMetaType)); std::fill(types, types + argc + 1, QMetaType::fromType<QVariant>()); @@ -2231,31 +2236,6 @@ static bool callArrowFunction( Q_UNREACHABLE_RETURN(false); } -static bool callArrowFunctionAsVariant( - QV4::ExecutionEngine *engine, QV4::ArrowFunction *function, - QObject *thisObject, void **args, int argc) -{ - QV4::Function *v4Function = function->function(); - Q_ASSERT(v4Function); - - switch (v4Function->kind) { - case QV4::Function::JsUntyped: - // We cannot assert anything here because the method can be shadowed. - // That's why we wrap everything in QVariant. - case QV4::Function::AotCompiled: - case QV4::Function::JsTyped: { - Q_ALLOCA_VAR(QMetaType, types, (argc + 1) * sizeof(QMetaType)); - std::fill(types, types + argc + 1, QMetaType::fromType<QVariant>()); - function->call(thisObject, args, types, argc); - return !engine->hasException; - } - case QV4::Function::Eval: - break; - } - - Q_UNREACHABLE_RETURN(false); -} - bool AOTCompiledContext::callQmlContextPropertyLookup(uint index, void **args, int argc) const { QV4::Lookup *lookup = compilationUnit->runtimeLookups + index; @@ -2435,16 +2415,25 @@ bool AOTCompiledContext::callObjectPropertyLookup( : callQObjectMethod(engine->handle(), lookup, object, args, argc); case QV4::Lookup::Call::GetterQObjectProperty: case QV4::Lookup::Call::GetterQObjectPropertyFallback: { - const bool asVariant = lookup->asVariant; - // Here we always retrieve a fresh method via the getter. No need to re-init. + if (lookup->asVariant) { + // If the method can be shadowed, the overridden method can be taken away, too. + // In that case we might end up with a QObjectMethod or random other values instead. + // callQObjectMethodAsVariant is flexible enough to handle that. + return callQObjectMethodAsVariant(engine->handle(), lookup, object, args, argc); + } + + // Here we always retrieve a fresh ArrowFunction via the getter. QV4::Scope scope(engine->handle()); QV4::ScopedValue thisObject(scope, QV4::QObjectWrapper::wrap(scope.engine, object)); QV4::Scoped<QV4::ArrowFunction> function(scope, lookup->getter(scope.engine, thisObject)); + + // The getter mustn't touch the asVariant bit + Q_ASSERT(!lookup->asVariant); + + // If the method can't be shadowed, it has to stay the same. Q_ASSERT(function); - Q_ASSERT(lookup->asVariant == asVariant); // The getter mustn't touch the asVariant bit - return asVariant - ? callArrowFunctionAsVariant(scope.engine, function, qmlScopeObject, args, argc) - : callArrowFunction(scope.engine, function, qmlScopeObject, args, argc); + + return callArrowFunction(scope.engine, function, qmlScopeObject, args, argc); } default: break; @@ -2463,16 +2452,16 @@ void AOTCompiledContext::initCallObjectPropertyLookupAsVariant(uint index, QObje QV4::Lookup *lookup = compilationUnit->runtimeLookups + index; QV4::Scope scope(engine->handle()); - const auto throwInvalidObjectError = [&]() { + const auto throwInvalidObjectError = [&](const QString &object) { scope.engine->throwTypeError( - QStringLiteral("Property '%1' of object [object Object] is not a function") - .arg(compilationUnit->runtimeStrings[lookup->nameIndex]->toQString())); + QStringLiteral("Property '%1' of object %2 is not a function").arg( + compilationUnit->runtimeStrings[lookup->nameIndex]->toQString(), object)); }; const auto *ddata = QQmlData::get(object, false); if (ddata && ddata->hasVMEMetaObject && ddata->jsWrapper.isNullOrUndefined()) { // We cannot lookup functions on an object with VME metaobject but no QObjectWrapper - throwInvalidObjectError(); + throwInvalidObjectError(QStringLiteral("[object Object]")); return; } @@ -2490,7 +2479,7 @@ void AOTCompiledContext::initCallObjectPropertyLookupAsVariant(uint index, QObje return; } - throwInvalidObjectError(); + throwInvalidObjectError(thisObject->toQStringNoThrow()); } void AOTCompiledContext::initCallObjectPropertyLookup( diff --git a/tests/auto/qml/qmlcppcodegen/data/CMakeLists.txt b/tests/auto/qml/qmlcppcodegen/data/CMakeLists.txt index 9060efb993..19a297e92c 100644 --- a/tests/auto/qml/qmlcppcodegen/data/CMakeLists.txt +++ b/tests/auto/qml/qmlcppcodegen/data/CMakeLists.txt @@ -148,6 +148,7 @@ set(qml_files detachedreferences.qml dialog.qml dialogButtonBox.qml + disappearingArrowFunction.qml dynamicscene.qml enforceSignature.qml enumConversion.qml diff --git a/tests/auto/qml/qmlcppcodegen/data/disappearingArrowFunction.qml b/tests/auto/qml/qmlcppcodegen/data/disappearingArrowFunction.qml new file mode 100644 index 0000000000..cb97ab5c02 --- /dev/null +++ b/tests/auto/qml/qmlcppcodegen/data/disappearingArrowFunction.qml @@ -0,0 +1,28 @@ +pragma Strict +import QtQml + +QtObject { + property Person inner: Person { + function getName() : int { return 5 } + } + + property Person none: Person {} + + property Person evil: Person { + property string getName: "not a function" + } + + onObjectNameChanged: console.log(inner.getName()) + + function swapNone() { + let t = inner; + inner = none; + none = t; + } + + function swapEvil() { + let t = inner; + inner = evil; + evil = t; + } +} diff --git a/tests/auto/qml/qmlcppcodegen/tst_qmlcppcodegen.cpp b/tests/auto/qml/qmlcppcodegen/tst_qmlcppcodegen.cpp index 47523f235c..0164373e8d 100644 --- a/tests/auto/qml/qmlcppcodegen/tst_qmlcppcodegen.cpp +++ b/tests/auto/qml/qmlcppcodegen/tst_qmlcppcodegen.cpp @@ -101,6 +101,7 @@ private slots: void detachOnAssignment(); void detachedReferences(); void dialogButtonBox(); + void disappearingArrowFunction(); void enumConversion(); void enumFromBadSingleton(); void enumLookup(); @@ -1839,6 +1840,56 @@ void tst_QmlCppCodegen::dialogButtonBox() QPlatformDialogHelper::Ok | QPlatformDialogHelper::Cancel); } +void tst_QmlCppCodegen::disappearingArrowFunction() +{ + QQmlEngine engine; + const QUrl url(u"qrc:/qt/qml/TestTypes/disappearingArrowFunction.qml"_s); + QQmlComponent c(&engine, url); + QVERIFY2(c.isReady(), qPrintable(c.errorString())); + QScopedPointer<QObject> o(c.create()); + QVERIFY(!o.isNull()); + + QTest::ignoreMessage(QtDebugMsg, "5"); + o->setObjectName("no"); + + QMetaObject::invokeMethod(o.data(), "swapNone"); + QTest::ignoreMessage(QtDebugMsg, "Bart"); + o->setObjectName("nono"); + + QMetaObject::invokeMethod(o.data(), "swapNone"); + QTest::ignoreMessage(QtDebugMsg, "5"); + o->setObjectName("nonono"); + + const QRegularExpression warning( + QRegularExpression::escape(url.toString()) + + u"\\:15\\: TypeError\\: Property 'getName' of object " + "Person_QML_[0-9]+\\(0x[0-9a-f]+\\) is not a function"_s); + + QMetaObject::invokeMethod(o.data(), "swapEvil"); + QTest::ignoreMessage(QtWarningMsg, warning); + o->setObjectName("nononono"); + + QMetaObject::invokeMethod(o.data(), "swapEvil"); + QTest::ignoreMessage(QtDebugMsg, "5"); + o->setObjectName("nonononono"); + + QMetaObject::invokeMethod(o.data(), "swapNone"); + QTest::ignoreMessage(QtDebugMsg, "Bart"); + o->setObjectName("nononononono"); + + QMetaObject::invokeMethod(o.data(), "swapEvil"); + QTest::ignoreMessage(QtWarningMsg, warning); + o->setObjectName("nonononononono"); + + QMetaObject::invokeMethod(o.data(), "swapEvil"); + QTest::ignoreMessage(QtDebugMsg, "Bart"); + o->setObjectName("nononononononono"); + + QMetaObject::invokeMethod(o.data(), "swapNone"); + QTest::ignoreMessage(QtDebugMsg, "5"); + o->setObjectName("nonononononononono"); +} + void tst_QmlCppCodegen::enumConversion() { QQmlEngine engine; |
