diff options
| author | Ulf Hermann <ulf.hermann@qt.io> | 2026-09-10 18:35:20 +0200 |
|---|---|---|
| committer | Ulf Hermann <ulf.hermann@qt.io> | 2026-09-22 14:34:44 +0000 |
| commit | c97113c495f5eae3adbaa585398974000da4af36 (patch) | |
| tree | abe431ce7f6b167cf0279b77a81256a7e1714e0d | |
| parent | c090e90a39852961e960ae5b827ce8086e251e32 (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.cpp | 7 | ||||
| -rw-r--r-- | tests/auto/qml/qmlcppcodegen/data/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | tests/auto/qml/qmlcppcodegen/data/aotLocalsGarbage.qml | 30 | ||||
| -rw-r--r-- | tests/auto/qml/qmlcppcodegen/tst_qmlcppcodegen.cpp | 57 |
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; |
