aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorUlf Hermann <ulf.hermann@qt.io>2026-09-10 18:35:20 +0200
committerUlf Hermann <ulf.hermann@qt.io>2026-09-22 14:34:44 +0000
commitc97113c495f5eae3adbaa585398974000da4af36 (patch)
treeabe431ce7f6b167cf0279b77a81256a7e1714e0d
parentc090e90a39852961e960ae5b827ce8086e251e32 (diff)
QtQml: Clear locals pointer after AOT function call
This protects the stack from any code that mistakenly tries to access the AOT locals after the AOT function has returned. This should not be possible, but preventing it is cheap and messing it up is easy. Task-number: QTBUG-149512 Change-Id: I2267755a5204b5bf4f7e2eff5815a0cf8533bd7f Reviewed-by: Olivier De Cannière <olivier.decanniere@qt.io>
-rw-r--r--src/qml/jsruntime/qv4vme_moth.cpp7
-rw-r--r--tests/auto/qml/qmlcppcodegen/data/CMakeLists.txt1
-rw-r--r--tests/auto/qml/qmlcppcodegen/data/aotLocalsGarbage.qml30
-rw-r--r--tests/auto/qml/qmlcppcodegen/tst_qmlcppcodegen.cpp57
4 files changed, 95 insertions, 0 deletions
diff --git a/src/qml/jsruntime/qv4vme_moth.cpp b/src/qml/jsruntime/qv4vme_moth.cpp
index ffbb52f737..2934bf4873 100644
--- a/src/qml/jsruntime/qv4vme_moth.cpp
+++ b/src/qml/jsruntime/qv4vme_moth.cpp
@@ -450,6 +450,13 @@ void VME::exec(MetaTypesStackFrame *frame, ExecutionEngine *engine)
aotContext.engine = engine->jsEngine();
aotContext.compilationUnit = function->executableCompilationUnit();
function->aotCompiledCode(&aotContext, argv);
+
+ // The tracked-locals storage is a local variable of the AOT-compiled
+ // function. We should not be able to use it between here and the
+ // popping of the stack frame. However, nulling it is a cheap defense
+ // in depth that will make mistaken code crash with a clean null pointer
+ // dereference or skip rather than corrupt random stack values.
+ frame->setLocals(nullptr);
});
}
diff --git a/tests/auto/qml/qmlcppcodegen/data/CMakeLists.txt b/tests/auto/qml/qmlcppcodegen/data/CMakeLists.txt
index 77c3f6deec..eb0f08c33a 100644
--- a/tests/auto/qml/qmlcppcodegen/data/CMakeLists.txt
+++ b/tests/auto/qml/qmlcppcodegen/data/CMakeLists.txt
@@ -130,6 +130,7 @@ set(qml_files
callObjectLookupOnNull.qml
callWithSpread.qml
childobject.qml
+ aotLocalsGarbage.qml
collector.qml
colorAsVariant.qml
colorString.qml
diff --git a/tests/auto/qml/qmlcppcodegen/data/aotLocalsGarbage.qml b/tests/auto/qml/qmlcppcodegen/data/aotLocalsGarbage.qml
new file mode 100644
index 0000000000..10d64f13ca
--- /dev/null
+++ b/tests/auto/qml/qmlcppcodegen/data/aotLocalsGarbage.qml
@@ -0,0 +1,30 @@
+// Copyright (C) 2026 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+pragma Strict
+import QtQml
+
+QtObject {
+ id: self
+
+ property Component c: QtObject { objectName: "tracked" }
+ property QtObject hidden: c.createObject()
+
+ // Hands out the only remaining reference to "hidden", wrapped in a deeply
+ // nested map. While this function is running, the object is kept alive by
+ // the AOT-compiled function's tracked locals. Once it has returned, the
+ // returned value is in flight: it lives in coerceAndCall()'s buffer, which
+ // the garbage collector does not scan.
+ //
+ // The nesting matters. Converting the returned map to JS recurses once per
+ // level and allocates on every level, so a collector that runs during the
+ // conversion runs with plenty of C++ stack churn on top of the frame this
+ // function just vacated.
+ function takeHidden() : var {
+ var m = { o: hidden }
+ for (var i = 0; i < 300; ++i)
+ m = { n: m }
+ hidden = null
+ return m
+ }
+}
diff --git a/tests/auto/qml/qmlcppcodegen/tst_qmlcppcodegen.cpp b/tests/auto/qml/qmlcppcodegen/tst_qmlcppcodegen.cpp
index 7b62828a93..36695737e2 100644
--- a/tests/auto/qml/qmlcppcodegen/tst_qmlcppcodegen.cpp
+++ b/tests/auto/qml/qmlcppcodegen/tst_qmlcppcodegen.cpp
@@ -23,6 +23,10 @@
#include <private/qqmlbind_p.h>
#include <private/qqmlengine_p.h>
#include <private/qqmlpropertycachecreator_p.h>
+#include <private/qqmlvmemetaobject_p.h>
+#include <private/qv4functionobject_p.h>
+#include <private/qv4mm_p.h>
+#include <private/qv4qobjectwrapper_p.h>
#include <QtTest/qsignalspy.h>
#include <QtTest/qtest.h>
@@ -84,6 +88,7 @@ private slots:
void callObjectLookupOnNull();
void callWithSpread();
void collectGarbageDuringAotCode();
+ void collectGarbageAfterAotCodeReturned();
void colorAsVariant();
void colorString();
void compareOriginals();
@@ -1158,6 +1163,58 @@ void tst_QmlCppCodegen::callWithSpread()
QVERIFY(o);
}
+void tst_QmlCppCodegen::collectGarbageAfterAotCodeReturned()
+{
+ QQmlEngine engine;
+ QQmlComponent c(&engine, QUrl(u"qrc:/qt/qml/TestTypes/aotLocalsGarbage.qml"_s));
+ QVERIFY2(c.isReady(), qPrintable(c.errorString()));
+ std::unique_ptr<QObject> o(c.create());
+ QVERIFY(o);
+
+ QPointer<QObject> tracked = o->property("hidden").value<QObject *>();
+ QVERIFY(tracked);
+ QCOMPARE(tracked->objectName(), u"tracked"_s);
+
+ const int coreIndex = o->metaObject()->indexOfMethod("takeHidden()");
+ QVERIFY(coreIndex >= 0);
+ QQmlVMEMetaObject *vme = QQmlVMEMetaObject::getForMethod(o.get(), coreIndex);
+ QVERIFY(vme);
+
+ QV4::ExecutionEngine *v4 = engine.handle();
+ QV4::Scope scope(v4);
+
+ // Same handle the meta-object itself calls QML methods through
+ // (QQmlVMEMetaObject::metaCall), so this exercises the regular entry point.
+ QV4::Scoped<QV4::JavaScriptFunctionObject> take(scope, vme->method(0));
+ QVERIFY(take);
+ QCOMPARE(take->function()->name()->toQString(), u"takeHidden"_s);
+
+ // Ask for a return type the function does not produce. That is what makes
+ // coerceAndCall() coerce the result *after* the AOT-compiled function has
+ // returned -- the only point at which the engine allocates while the
+ // returned-from frame is still on engine->currentStackFrame.
+ //
+ // NB: This is not something that will happen organically. We do it here in
+ // order to simulate possibly mistaken code elswhere in QtQml.
+ QStringList result;
+ void *args[] = { &result };
+ const QMetaType types[] = { QMetaType::fromType<QStringList>() };
+
+ // Collect on every allocation, so a collection is guaranteed to happen
+ // inside that coercion -- repeatedly, at every level of its recursion, by
+ // which point the coercion's own frames have overwritten the stack the AOT
+ // function's tracked-locals storage used to occupy.
+ v4->memoryManager->aggressiveGC = true;
+
+ take->call(o.get(), args, types, 0);
+
+ // Reaching this line is the test. If collectFromJSStack() marks through the
+ // storage the AOT function left behind, the coercion's own frames have
+ // overwritten that storage by now, so the virtual call goes through a
+ // clobbered vtable and likely results in a crash.
+ QVERIFY(tracked);
+}
+
void tst_QmlCppCodegen::collectGarbageDuringAotCode()
{
QQmlEngine engine;