aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorUlf Hermann <ulf.hermann@qt.io>2025-09-11 09:17:18 +0200
committerQt Cherry-pick Bot <cherrypick_bot@qt-project.org>2025-09-16 01:49:52 +0000
commit37b529d2de0c5fc7c6ae57b92b2039a8c8635af0 (patch)
tree6301c97d2777d27f22ed8067f5f9e18df9ac2459
parent24335a40a75a17d44003791f7f914f1f5e849b32 (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.cpp65
-rw-r--r--tests/auto/qml/qmlcppcodegen/data/CMakeLists.txt1
-rw-r--r--tests/auto/qml/qmlcppcodegen/data/disappearingArrowFunction.qml28
-rw-r--r--tests/auto/qml/qmlcppcodegen/tst_qmlcppcodegen.cpp51
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;