aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorUlf Hermann <ulf.hermann@qt.io>2026-09-09 14:14:45 +0200
committerUlf Hermann <ulf.hermann@qt.io>2026-09-24 06:01:54 +0000
commit0dd80960f286d0f5d9b8d3ef3bb531033a30dad9 (patch)
tree602883f67320b662bc3a45b27a4d7370b8eabc5c
parent059ee9db91507b8b86cdf4bf033985c9ea41e0d4 (diff)
QtQml: Do not allow other engines to acces QQmlVMEMetaObject internals
In all cases where we do QQmlVMEMetaObject::get() we now require the engine to be specified. If it doesn't match, no QQmlVMEMetaObject is returned and the generic fallback is taken. For retrieving alias targets and connecting alias signals we don't actually need to retrieve anything engine-specific from the QQmlVMEMetaObject, though. Encapsulate these entirely in their respective static methods rather than handing out the QQmlVMEMetaObject itself. Pick-to: 6.12 6.8 Fixes: QTBUG-60984 Change-Id: I40af1e71560a3b5a07d59e2d76d0bf493c6b1dc4 Reviewed-by: Olivier De Cannière <olivier.decanniere@qt.io> Reviewed-by: Sami Shalayel <sami.shalayel@qt.io>
-rw-r--r--src/plugins/qmltooling/qmldbg_debugger/qqmlenginedebugservice.cpp4
-rw-r--r--src/qml/jsruntime/qv4qobjectwrapper.cpp35
-rw-r--r--src/qml/qml/qqmlabstractbinding.cpp4
-rw-r--r--src/qml/qml/qqmlbinding.cpp13
-rw-r--r--src/qml/qml/qqmlobjectcreator.cpp4
-rw-r--r--src/qml/qml/qqmlobjectcreator_p.h2
-rw-r--r--src/qml/qml/qqmlproperty.cpp17
-rw-r--r--src/qml/qml/qqmlvmemetaobject.cpp51
-rw-r--r--src/qml/qml/qqmlvmemetaobject_p.h31
-rw-r--r--src/qmlmeta/types/qqmlbind.cpp11
-rw-r--r--src/qmlmeta/types/qqmlconnections.cpp2
-rw-r--r--tests/auto/qml/debugger/qqmlpreviewdiff/tst_qqmlpreviewobjectpatch.cpp4
-rw-r--r--tests/auto/qml/qjsengine/tst_qjsengine.cpp158
-rw-r--r--tests/auto/qml/qmlcppcodegen/tst_qmlcppcodegen.cpp2
-rw-r--r--tests/auto/qml/qqmlecmascript/tst_qqmlecmascript.cpp12
-rw-r--r--tests/auto/qml/qqmllanguage/tst_qqmllanguage.cpp2
-rw-r--r--tests/auto/qml/qv4mm/data/sharedWithOtherEngine.qml7
-rw-r--r--tests/auto/qml/qv4mm/tst_qv4mm.cpp102
18 files changed, 360 insertions, 101 deletions
diff --git a/src/plugins/qmltooling/qmldbg_debugger/qqmlenginedebugservice.cpp b/src/plugins/qmltooling/qmldbg_debugger/qqmlenginedebugservice.cpp
index 187af7e621..5535feb77d 100644
--- a/src/plugins/qmltooling/qmldbg_debugger/qqmlenginedebugservice.cpp
+++ b/src/plugins/qmltooling/qmldbg_debugger/qqmlenginedebugservice.cpp
@@ -763,10 +763,10 @@ bool QQmlEngineDebugServiceImpl::setMethodBody(int objectId, const QString &meth
const QString jsfunction = QLatin1String("(function ") + method + QLatin1Char('(') + paramStr +
QLatin1String(") {") + body + QLatin1String("\n})");
- QQmlVMEMetaObject *vmeMetaObject = QQmlVMEMetaObject::get(object);
+ QV4::ExecutionEngine *v4 = qmlEngine(object)->handle();
+ QQmlVMEMetaObject *vmeMetaObject = QQmlVMEMetaObject::get(object, v4);
Q_ASSERT(vmeMetaObject); // the fact we found the property above should guarentee this
- QV4::ExecutionEngine *v4 = qmlEngine(object)->handle();
QV4::Scope scope(v4);
int lineNumber = 0;
diff --git a/src/qml/jsruntime/qv4qobjectwrapper.cpp b/src/qml/jsruntime/qv4qobjectwrapper.cpp
index 4be9adedd3..38e508cf19 100644
--- a/src/qml/jsruntime/qv4qobjectwrapper.cpp
+++ b/src/qml/jsruntime/qv4qobjectwrapper.cpp
@@ -293,7 +293,7 @@ static ReturnedValue loadProperty(
return QJSValuePrivate::convertToReturnedValue(v4, v);
}
- if (property.isQVariant()) {
+ if (property.isQVariant() || property.isVarProperty()) {
// We have to read the property even if it's a lazy-loaded reference object.
// Without reading it, we wouldn't know its inner type.
QVariant v;
@@ -371,9 +371,11 @@ ReturnedValue QObjectWrapper::getProperty(
if (property->isFunction() && !property->isVarProperty()) {
if (property->isVMEFunction()) {
- QQmlVMEMetaObject *vmemo = QQmlVMEMetaObject::get(object);
- Q_ASSERT(vmemo);
- return vmemo->vmeMethod(property->coreIndex());
+ if (QQmlVMEMetaObject *vmemo = QQmlVMEMetaObject::get(object, engine))
+ return vmemo->vmeMethod(property->coreIndex());
+
+ return QObjectMethod::create(
+ engine, (flags & AttachMethods) ? wrapper : nullptr, property->coreIndex());
} else if (property->isV4Function()) {
return QObjectMethod::create(
engine, (flags & AttachMethods) ? wrapper : nullptr, property->coreIndex());
@@ -398,12 +400,11 @@ ReturnedValue QObjectWrapper::getProperty(
}
if (property->isVarProperty()) {
- QQmlVMEMetaObject *vmemo = QQmlVMEMetaObject::get(object);
- Q_ASSERT(vmemo);
- return vmemo->vmeProperty(property->coreIndex());
- } else {
- return loadProperty(engine, wrapper, object, *property);
+ if (QQmlVMEMetaObject *vmemo = QQmlVMEMetaObject::get(object, engine))
+ return vmemo->vmeProperty(property->coreIndex());
}
+
+ return loadProperty(engine, wrapper, object, *property);
}
static OptionalReturnedValue getDestroyOrToStringMethod(
@@ -726,10 +727,10 @@ void QObjectWrapper::setProperty(
if (property->isVarProperty()) {
// allow assignment of "special" values (null, undefined, function) to var properties
- QQmlVMEMetaObject *vmemo = QQmlVMEMetaObject::get(object);
- Q_ASSERT(vmemo);
- vmemo->setVMEProperty(property->coreIndex(), value);
- return;
+ if (QQmlVMEMetaObject *vmemo = QQmlVMEMetaObject::get(object, engine)) {
+ vmemo->setVMEProperty(property->coreIndex(), value);
+ return;
+ }
}
#define PROPERTY_STORE(cpptype, value) \
@@ -740,9 +741,11 @@ void QObjectWrapper::setProperty(
QMetaObject::metacall(object, QMetaObject::WriteProperty, property->coreIndex(), argv);
const QMetaType propType = property->propType();
- // functions are already handled, except for the QJSValue case
+ // functions are already handled, except for the QJSValue case and for var properties of
+ // objects owned by another engine
Q_ASSERT(!value.as<FunctionObject>()
|| value.as<QV4::QQmlTypeWrapper>()
+ || property->isVarProperty()
|| propType == QMetaType::fromType<QJSValue>());
if (value.isNull() && property->isQObject()) {
@@ -774,10 +777,6 @@ void QObjectWrapper::setProperty(
PROPERTY_STORE(double, double(value.asDouble()));
} else if (propType == QMetaType::fromType<QString>() && value.isString()) {
PROPERTY_STORE(QString, value.toQStringNoThrow());
- } else if (property->isVarProperty()) {
- QQmlVMEMetaObject *vmemo = QQmlVMEMetaObject::get(object);
- Q_ASSERT(vmemo);
- vmemo->setVMEProperty(property->coreIndex(), value);
} else if (propType == QMetaType::fromType<QQmlScriptString>()
&& (value.isUndefined() || value.isPrimitive())) {
QQmlScriptString ss(value.toQStringNoThrow(), nullptr /* context */, object);
diff --git a/src/qml/qml/qqmlabstractbinding.cpp b/src/qml/qml/qqmlabstractbinding.cpp
index f877544d40..60001b3bbb 100644
--- a/src/qml/qml/qqmlabstractbinding.cpp
+++ b/src/qml/qml/qqmlabstractbinding.cpp
@@ -244,10 +244,10 @@ bool QQmlAbstractBinding::setTarget(
m_target = object;
for (bool isAlias = coreIsAlias; isAlias;) {
- QQmlVMEMetaObject *vme = QQmlVMEMetaObject::getForProperty(object, coreIndex);
int aValueTypeIndex;
- if (!vme->aliasTarget(coreIndex, &object, &coreIndex, &aValueTypeIndex)) {
+ if (!QQmlVMEMetaObject::aliasTarget(object, coreIndex, &object, &coreIndex,
+ &aValueTypeIndex)) {
// can't resolve id (yet)
return invalidate();
}
diff --git a/src/qml/qml/qqmlbinding.cpp b/src/qml/qml/qqmlbinding.cpp
index 41193d880b..f67ba32f4e 100644
--- a/src/qml/qml/qqmlbinding.cpp
+++ b/src/qml/qml/qqmlbinding.cpp
@@ -440,9 +440,9 @@ bool QQmlBinding::slowWrite(
const QMetaType metaType = valueTypeData.isValid() ? valueTypeData.propType() : core.propType();
QQmlJavaScriptExpression::DeleteWatcher watcher(this);
- if (core.isVarProperty()) {
- QQmlVMEMetaObject *vmemo = QQmlVMEMetaObject::get(m_target.data());
- Q_ASSERT(vmemo);
+ if (QQmlVMEMetaObject *vmemo = core.isVarProperty()
+ ? QQmlVMEMetaObject::get(m_target.data(), qmlEngine->handle())
+ : nullptr) {
QV4::Scope scope(qmlEngine->handle());
QV4::ScopedValue value(scope, qmlEngine->handle()->metaTypeToJS(resultType, result));
vmemo->setVMEProperty(core.coreIndex(),
@@ -488,7 +488,11 @@ Q_NEVER_INLINE bool QQmlBinding::slowWrite(const QQmlPropertyData &core,
QQmlJavaScriptExpression::DeleteWatcher watcher(this);
QVariant value;
- bool isVarProperty = core.isVarProperty();
+
+ // A "var" property of an object owned by another engine is an ordinary QVariant property to
+ // us. We must not store values from our own heap in its storage.
+ QQmlVMEMetaObject *vmemo = QQmlVMEMetaObject::get(m_target.data(), engine()->handle());
+ const bool isVarProperty = core.isVarProperty() && vmemo;
if (isUndefined) {
} else if (core.isQList()) {
@@ -519,7 +523,6 @@ Q_NEVER_INLINE bool QQmlBinding::slowWrite(const QQmlPropertyData &core,
return false;
}
- QQmlVMEMetaObject *vmemo = QQmlVMEMetaObject::get(m_target.data());
Q_ASSERT(vmemo);
vmemo->setVMEProperty(core.coreIndex(), result);
} else if (isUndefined
diff --git a/src/qml/qml/qqmlobjectcreator.cpp b/src/qml/qml/qqmlobjectcreator.cpp
index 398bd3b34b..0c646efb5a 100644
--- a/src/qml/qml/qqmlobjectcreator.cpp
+++ b/src/qml/qml/qqmlobjectcreator.cpp
@@ -1979,7 +1979,7 @@ bool QQmlObjectCreator::populateInstance(int index, QObject *instance, QObject *
_ddata->propertyCache = cache;
scopeObjectProtector = _ddata->jsWrapper.value();
} else {
- vmeMetaObject = QQmlVMEMetaObject::get(_qobject);
+ vmeMetaObject = QQmlVMEMetaObject::get(_qobject, v4);
}
registerObjectWithContextById(_compiledObject, _qobject);
@@ -2123,7 +2123,7 @@ void QQmlObjectCreator::repopulateBindings(
QQmlData *ddata = QQmlData::get(instance, /*create=*/true);
Q_ASSERT(ddata);
- QQmlVMEMetaObject *vmeMetaObject = QQmlVMEMetaObject::get(instance);
+ QQmlVMEMetaObject *vmeMetaObject = QQmlVMEMetaObject::get(instance, v4);
const QV4::CompiledData::Object *obj = compilationUnit->objectAt(index);
diff --git a/src/qml/qml/qqmlobjectcreator_p.h b/src/qml/qml/qqmlobjectcreator_p.h
index 403af5daf9..87bcacca78 100644
--- a/src/qml/qml/qqmlobjectcreator_p.h
+++ b/src/qml/qml/qqmlobjectcreator_p.h
@@ -321,7 +321,7 @@ private:
QObject *bindingTarget = instance;
QQmlPropertyCache::ConstPtr cache = declarativeData->propertyCache;
- QQmlVMEMetaObject *vmeMetaObject = QQmlVMEMetaObject::get(instance);
+ QQmlVMEMetaObject *vmeMetaObject = QQmlVMEMetaObject::get(instance, v4);
QObject *scopeObject = instance;
qt_ptr_swap(_scopeObject, scopeObject);
diff --git a/src/qml/qml/qqmlproperty.cpp b/src/qml/qml/qqmlproperty.cpp
index 025659c424..799608b47f 100644
--- a/src/qml/qml/qqmlproperty.cpp
+++ b/src/qml/qml/qqmlproperty.cpp
@@ -962,11 +962,12 @@ void QQmlPropertyPrivate::findAliasTarget(QObject *object, QQmlPropertyIndex bin
const QQmlPropertyData *propertyData =
data->propertyCache?data->propertyCache->property(coreIndex):nullptr;
if (propertyData && propertyData->isAlias()) {
- QQmlVMEMetaObject *vme = QQmlVMEMetaObject::getForProperty(object, coreIndex);
QObject *aObject = nullptr; int aCoreIndex = -1; int aValueTypeIndex = -1;
- if (vme->aliasTarget(coreIndex, &aObject, &aCoreIndex, &aValueTypeIndex)) {
- // This will either be a value type sub-reference or an alias to a value-type sub-reference not both
+ if (QQmlVMEMetaObject::aliasTarget(object, coreIndex, &aObject, &aCoreIndex,
+ &aValueTypeIndex)) {
+ // This will either be a value type sub-reference
+ // or an alias to a value-type sub-reference not both
Q_ASSERT(valueTypeIndex == -1 || aValueTypeIndex == -1);
QQmlPropertyIndex aBindingIndex(aCoreIndex);
@@ -1716,7 +1717,7 @@ static bool assignToQQmlListProperty(
auto result = QQmlPropertyPrivate::convertToQQmlListProperty(&prop, propertyMetaType, value);
if (useNonsignalingListOps && result == QQmlPropertyPrivate::ListCopyResult::Copied) {
- Q_ASSERT(QQmlVMEMetaObject::get(object));
+ Q_ASSERT(QQmlData::get(object)->hasVMEMetaObject);
QQmlVMEResolvedList(&prop).activateSignal();
}
@@ -2290,12 +2291,8 @@ static inline void flush_vme_signal(const QObject *object, int index, bool index
: data->propertyCache->method(index);
if (property && property->isVMESignal()) {
- QQmlVMEMetaObject *vme;
- if (indexInSignalRange)
- vme = QQmlVMEMetaObject::getForSignal(const_cast<QObject *>(object), index);
- else
- vme = QQmlVMEMetaObject::getForMethod(const_cast<QObject *>(object), index);
- vme->connectAliasSignal(index, indexInSignalRange);
+ QQmlVMEMetaObject::connectAlias(const_cast<QObject *>(object), index,
+ indexInSignalRange);
}
}
}
diff --git a/src/qml/qml/qqmlvmemetaobject.cpp b/src/qml/qml/qqmlvmemetaobject.cpp
index fb2e4f87ed..6315f03854 100644
--- a/src/qml/qml/qqmlvmemetaobject.cpp
+++ b/src/qml/qml/qqmlvmemetaobject.cpp
@@ -1550,16 +1550,21 @@ void QQmlVMEMetaObject::connectAlias(const QV4::CompiledData::Object *compiledOb
endpoint->tryConnect();
}
-void QQmlVMEMetaObject::connectAliasSignal(int index, bool indexInSignalRange)
+void QQmlVMEMetaObject::connectAlias(QObject *o, int coreIndex, bool indexInSignalRange)
{
- if (const QV4::CompiledData::Object *compiledObject = findCompiledObject()) {
- const int aliasId = index
- - (indexInSignalRange ? cache->signalOffset() : signalOffset())
- - compiledObject->nProperties;
- if (aliasId < 0 || aliasId >= int(compiledObject->nAliases))
- return;
+ const auto offset = [indexInSignalRange](QQmlVMEMetaObject *vme) {
+ return indexInSignalRange ? vme->cache->signalOffset() : vme->cache->methodOffset();
+ };
+
+ QQmlVMEMetaObject *vme = QQmlVMEMetaObject::get(o);
+ while (vme && offset(vme) > coreIndex)
+ vme = vme->parentVMEMetaObject();
- connectAlias(compiledObject, aliasId);
+ Q_ASSERT(vme);
+ if (const QV4::CompiledData::Object *compiledObject = vme->findCompiledObject()) {
+ const int aliasId = coreIndex - offset(vme) - compiledObject->nProperties;
+ if (aliasId >= 0 && aliasId < int(compiledObject->nAliases))
+ vme->connectAlias(compiledObject, aliasId);
}
}
@@ -1571,38 +1576,14 @@ void QQmlVMEMetaObject::activate(QObject *object, int index, void **args)
QMetaObject::activate(object, cache->signalOffset(), index, args);
}
-QQmlVMEMetaObject *QQmlVMEMetaObject::getForProperty(QObject *o, int coreIndex)
+bool QQmlVMEMetaObject::aliasTarget(QObject *o, int coreIndex, QObject **aObject, int *aCoreIndex,
+ int *aValueTypeIndex)
{
QQmlVMEMetaObject *vme = QQmlVMEMetaObject::get(o);
while (vme && vme->cache->propertyOffset() > coreIndex)
vme = vme->parentVMEMetaObject();
- Q_ASSERT(vme);
- return vme;
-}
-
-QQmlVMEMetaObject *QQmlVMEMetaObject::getForMethod(QObject *o, int coreIndex)
-{
- QQmlVMEMetaObject *vme = QQmlVMEMetaObject::get(o);
- while (vme && vme->cache->methodOffset() > coreIndex)
- vme = vme->parentVMEMetaObject();
-
- Q_ASSERT(vme);
- return vme;
-}
-
-/*! \internal
- \a coreIndex is in the signal index range (see QObjectPrivate::signalIndex()).
- This is different from QMetaMethod::methodIndex().
-*/
-QQmlVMEMetaObject *QQmlVMEMetaObject::getForSignal(QObject *o, int coreIndex)
-{
- QQmlVMEMetaObject *vme = QQmlVMEMetaObject::get(o);
- while (vme && vme->cache->signalOffset() > coreIndex)
- vme = vme->parentVMEMetaObject();
-
- Q_ASSERT(vme);
- return vme;
+ return vme && vme->aliasTarget(coreIndex, aObject, aCoreIndex, aValueTypeIndex);
}
QQmlVMEVariantQObjectPtr *QQmlVMEMetaObject::getQObjectGuardForProperty(int index) const
diff --git a/src/qml/qml/qqmlvmemetaobject_p.h b/src/qml/qml/qqmlvmemetaobject_p.h
index a7e503238e..ca606d40d4 100644
--- a/src/qml/qml/qqmlvmemetaobject_p.h
+++ b/src/qml/qml/qqmlvmemetaobject_p.h
@@ -233,12 +233,11 @@ public:
QV4::ReturnedValue vmeProperty(int index) const;
void setVMEProperty(int index, const QV4::Value &v);
- void connectAliasSignal(int index, bool indexInSignalRange);
+ static void connectAlias(QObject *o, int coreIndex, bool indexInSignalRange);
- static inline QQmlVMEMetaObject *get(QObject *o);
- static QQmlVMEMetaObject *getForProperty(QObject *o, int coreIndex);
- static QQmlVMEMetaObject *getForMethod(QObject *o, int coreIndex);
- static QQmlVMEMetaObject *getForSignal(QObject *o, int coreIndex);
+ static inline QQmlVMEMetaObject *get(QObject *o, QV4::ExecutionEngine *engine);
+ static bool aliasTarget(QObject *o, int coreIndex, QObject **aObject, int *aCoreIndex,
+ int *aValueTypeIndex);
static void list_append(QQmlListProperty<QObject> *prop, QObject *o);
static void list_clear(QQmlListProperty<QObject> *prop);
@@ -336,6 +335,8 @@ private:
friend class QQmlVMEResolvedList;
friend class QQmlVMEVariantQObjectPtr;
+ static inline QQmlVMEMetaObject *get(QObject *o);
+
const QV4::CompiledData::Object *findCompiledObject() const {
// If the executable CU has been stripped of its engine, it has an empty base CU
if (!m_compilationUnit || !m_compilationUnit->engine)
@@ -364,14 +365,20 @@ private:
QQmlVMEMetaObject *QQmlVMEMetaObject::get(QObject *obj)
{
- if (obj) {
- if (QQmlData *data = QQmlData::get(obj)) {
- if (data->hasVMEMetaObject)
- return static_cast<QQmlVMEMetaObject *>(QObjectPrivate::get(obj)->metaObject);
- }
- }
+ if (!obj)
+ return nullptr;
- return nullptr;
+ QQmlData *ddata = QQmlData::get(obj);
+ if (!ddata || !ddata->hasVMEMetaObject)
+ return nullptr;
+
+ return static_cast<QQmlVMEMetaObject *>(QObjectPrivate::get(obj)->metaObject);
+}
+
+QQmlVMEMetaObject *QQmlVMEMetaObject::get(QObject *obj, QV4::ExecutionEngine *engine)
+{
+ QQmlVMEMetaObject *vme = get(obj);
+ return (vme && vme->engine() == engine) ? vme : nullptr;
}
int QQmlVMEMetaObject::propOffset() const
diff --git a/src/qmlmeta/types/qqmlbind.cpp b/src/qmlmeta/types/qqmlbind.cpp
index 771cb0b974..47046c9d41 100644
--- a/src/qmlmeta/types/qqmlbind.cpp
+++ b/src/qmlmeta/types/qqmlbind.cpp
@@ -1305,7 +1305,8 @@ bool QQmlBindPrivate::isCurrent(QQmlBindEntry *entry) const
switch (entry->currentKind) {
case QQmlBindEntryKind::V4Value: {
auto propPriv = QQmlPropertyPrivate::get(entry->prop);
- QQmlVMEMetaObject *vmemo = QQmlVMEMetaObject::get(propPriv->object);
+ QQmlVMEMetaObject *vmemo =
+ QQmlVMEMetaObject::get(propPriv->object, propPriv->engine->handle());
Q_ASSERT(vmemo);
return QV4::RuntimeHelpers::strictEqual(
// fromReturnedValue is OK here because strictEqual will not allocate
@@ -1387,7 +1388,8 @@ void QQmlBindPrivate::preEvalEntry(QQmlBindEntry *entry)
if (restoreValue) {
QQmlAnyBinding::takeFrom(entry->prop); // we don't want to have a binding active
auto propPriv = QQmlPropertyPrivate::get(entry->prop);
- QQmlVMEMetaObject *vmemo = QQmlVMEMetaObject::get(propPriv->object);
+ QQmlVMEMetaObject *vmemo =
+ QQmlVMEMetaObject::get(propPriv->object, propPriv->engine->handle());
Q_ASSERT(vmemo);
vmemo->setVMEProperty(propPriv->core.coreIndex(),
*entry->previous.v4Value.valueRef());
@@ -1420,7 +1422,8 @@ void QQmlBindPrivate::preEvalEntry(QQmlBindEntry *entry)
auto propPriv = QQmlPropertyPrivate::get(entry->prop);
auto propData = propPriv->core;
if (!propPriv->valueTypeData.isValid() && propData.isVarProperty()) {
- QQmlVMEMetaObject *vmemo = QQmlVMEMetaObject::get(propPriv->object);
+ QQmlVMEMetaObject *vmemo =
+ QQmlVMEMetaObject::get(propPriv->object, propPriv->engine->handle());
Q_ASSERT(vmemo);
auto retVal = vmemo->vmeProperty(propData.coreIndex());
entry->previousKind = entry->previous.set(
@@ -1462,7 +1465,7 @@ void QQmlBindPrivate::postEvalEntry(QQmlBindEntry *entry)
break;
case QQmlBindEntryKind::V4Value: {
auto propPriv = QQmlPropertyPrivate::get(entry->prop);
- QQmlVMEMetaObject::get(propPriv->object)->setVMEProperty(
+ QQmlVMEMetaObject::get(propPriv->object, propPriv->engine->handle())->setVMEProperty(
propPriv->core.coreIndex(), *entry->current.v4Value.valueRef());
break;
}
diff --git a/src/qmlmeta/types/qqmlconnections.cpp b/src/qmlmeta/types/qqmlconnections.cpp
index 66d013fc2d..cd871ed771 100644
--- a/src/qmlmeta/types/qqmlconnections.cpp
+++ b/src/qmlmeta/types/qqmlconnections.cpp
@@ -415,7 +415,7 @@ void QQmlConnections::connectSignalsToMethods()
QV4::Scope scope(engine);
QV4::ScopedContext global(scope, engine->rootContext());
- if (QQmlVMEMetaObject *vmeMetaObject = QQmlVMEMetaObject::get(this)) {
+ if (QQmlVMEMetaObject *vmeMetaObject = QQmlVMEMetaObject::get(this, engine)) {
const int signalIndex = propPrivate->signalIndex();
auto *signal = new QQmlBoundSignal(target, signalIndex, this, qmlEngine(this));
signal->setEnabled(d->enabled);
diff --git a/tests/auto/qml/debugger/qqmlpreviewdiff/tst_qqmlpreviewobjectpatch.cpp b/tests/auto/qml/debugger/qqmlpreviewdiff/tst_qqmlpreviewobjectpatch.cpp
index 612d04a75d..e8981df6eb 100644
--- a/tests/auto/qml/debugger/qqmlpreviewdiff/tst_qqmlpreviewobjectpatch.cpp
+++ b/tests/auto/qml/debugger/qqmlpreviewdiff/tst_qqmlpreviewobjectpatch.cpp
@@ -4907,7 +4907,7 @@ void tst_QQmlPreviewObjectPatch::crossCompilationUnitBaseTypeChange()
QCOMPARE(inner->property("marker").toInt(), 1);
// The inner instance's innermost VME belongs to CrossCuInner's own compilation unit.
- const auto oldExecUnit = QQmlVMEMetaObject::get(inner)->compilationUnit();
+ const auto oldExecUnit = QQmlVMEMetaObject::get(inner, engine.handle())->compilationUnit();
QVERIFY(oldExecUnit);
QQmlComponent newComp(&engine, testFileUrl("CrossCuInnerNew.qml"));
@@ -4958,7 +4958,7 @@ void tst_QQmlPreviewObjectPatch::derivedTypeBaseTypeChangeFails()
QVERIFY(root);
// CrossCuInner's own compilation unit is the derived root's base level.
- const auto oldExecUnit = QQmlVMEMetaObject::get(root.get())->compilationUnit();
+ const auto oldExecUnit = QQmlVMEMetaObject::get(root.get(), engine.handle())->compilationUnit();
QVERIFY(oldExecUnit);
QQmlComponent newComp(&engine, testFileUrl("CrossCuInnerNew.qml"));
diff --git a/tests/auto/qml/qjsengine/tst_qjsengine.cpp b/tests/auto/qml/qjsengine/tst_qjsengine.cpp
index dbbff8eb74..425a21f988 100644
--- a/tests/auto/qml/qjsengine/tst_qjsengine.cpp
+++ b/tests/auto/qml/qjsengine/tst_qjsengine.cpp
@@ -136,6 +136,11 @@ private slots:
void newQObjectRace();
void newQObject_ownership();
void newQObject_deletedEngine();
+ void qmlObjectInOtherEngine();
+ void qmlMethodCalledFromOtherEngine();
+ void qmlObjectSignalHandlerFromOtherEngine();
+ void newQObject_ownershipAcrossEngines();
+ void newQObject_deletedOwningEngine();
void newQObjectPropertyCache();
void newQMetaObject();
void exceptionInSlot();
@@ -1117,6 +1122,159 @@ void tst_QJSEngine::newQObject_deletedEngine()
QTRY_VERIFY(spy.size());
}
+// A QObject created by QML carries per-instance JavaScript state: its "var" properties and its
+// QML methods live in a QV4::MemberData owned by the engine that created it. Handing such an
+// object to a second engine wraps it a second time. See QTBUG-60984.
+static constexpr char sharedQmlObject[] = R"(
+import QtQml
+QtObject {
+ property var payload: ({ marker: "created in the owning engine" })
+ property var stored
+ function store(value) { stored = value }
+ signal ping(string message)
+}
+)";
+
+static std::unique_ptr<QObject> createSharedObject(QQmlComponent *component)
+{
+ component->setData(sharedQmlObject, QUrl());
+ return std::unique_ptr<QObject>(component->create());
+}
+
+void tst_QJSEngine::qmlObjectInOtherEngine()
+{
+ QQmlEngine owner;
+ QJSEngine other;
+
+ QQmlComponent component(&owner);
+ const std::unique_ptr<QObject> shared = createSharedObject(&component);
+ QVERIFY2(shared.get(), qPrintable(component.errorString()));
+
+ owner.globalObject().setProperty(u"shared"_s, owner.newQObject(shared.get()));
+ other.globalObject().setProperty(u"shared"_s, other.newQObject(shared.get()));
+
+ // Anything a QVariant can carry crosses between the engines ...
+ owner.evaluate("shared.stored = 42");
+ QCOMPARE(other.evaluate("shared.stored").toInt(), 42);
+ other.evaluate("shared.stored = 'from the other engine'");
+ QCOMPARE(owner.evaluate("shared.stored").toString(), u"from the other engine"_s);
+ other.evaluate("shared.stored = shared");
+ QVERIFY(owner.evaluate("shared.stored === shared").toBool());
+
+ // ... but a JavaScript object belongs to the engine that created it.
+ QTest::ignoreMessage(QtWarningMsg, "JSValue can't be reassigned to another engine.");
+ QCOMPARE(other.evaluate("typeof shared.payload").toString(), u"undefined"_s);
+
+ // The owning engine is unaffected.
+ QCOMPARE(owner.evaluate("String(shared.payload.marker)").toString(),
+ u"created in the owning engine"_s);
+}
+
+void tst_QJSEngine::qmlMethodCalledFromOtherEngine()
+{
+ QQmlEngine owner;
+ QJSEngine other;
+
+ QQmlComponent component(&owner);
+ const std::unique_ptr<QObject> shared = createSharedObject(&component);
+ QVERIFY2(shared.get(), qPrintable(component.errorString()));
+
+ owner.globalObject().setProperty(u"shared"_s, owner.newQObject(shared.get()));
+ other.globalObject().setProperty(u"shared"_s, other.newQObject(shared.get()));
+
+ // The other engine gets a plain method wrapper. Calling it converts the arguments and runs
+ // the function in the engine that owns it.
+ const QJSValue called = other.evaluate("shared.store({ tag: 'from the other engine' }); true");
+ QVERIFY2(!called.isError(), qPrintable(called.toString()));
+ QCOMPARE(owner.evaluate("JSON.stringify(shared.stored)").toString(),
+ u"{\"tag\":\"from the other engine\"}"_s);
+
+ // A function has no QVariant representation that could cross, so it does not.
+ QTest::ignoreMessage(QtWarningMsg, "JSValue can't be reassigned to another engine.");
+ const QJSValue callback = other.evaluate("shared.store(function(x) { return x + 1 }); true");
+ QVERIFY2(!callback.isError(), qPrintable(callback.toString()));
+ QCOMPARE(owner.evaluate("typeof shared.stored").toString(), u"undefined"_s);
+
+ gc(owner);
+ gc(*other.handle());
+}
+
+void tst_QJSEngine::qmlObjectSignalHandlerFromOtherEngine()
+{
+ QQmlEngine owner;
+ QJSEngine other;
+
+ QQmlComponent component(&owner);
+ const std::unique_ptr<QObject> shared = createSharedObject(&component);
+ QVERIFY2(shared.get(), qPrintable(component.errorString()));
+
+ other.globalObject().setProperty(u"shared"_s, other.newQObject(shared.get()));
+
+ // Unlike a value stored in a "var" property, a signal handler stays rooted in the engine
+ // that created it.
+ const QJSValue connected = other.evaluate(
+ "var received = ''; shared.ping.connect(function(m) { received = m }); true");
+ QVERIFY2(!connected.isError(), qPrintable(connected.toString()));
+
+ gc(owner);
+ gc(*other.handle());
+
+ QVERIFY(QMetaObject::invokeMethod(shared.get(), "ping", Q_ARG(QString, u"hi"_s)));
+ QCOMPARE(other.evaluate("received").toString(), u"hi"_s);
+}
+
+void tst_QJSEngine::newQObject_ownershipAcrossEngines()
+{
+ QQmlEngine owner;
+ QJSEngine other;
+
+ QQmlComponent component(&owner);
+ QPointer<QObject> shared(createSharedObject(&component).release());
+ QVERIFY2(shared, qPrintable(component.errorString()));
+ QQmlEngine::setObjectOwnership(shared, QQmlEngine::JavaScriptOwnership);
+
+ // The owning engine wraps the object without keeping a strong reference to the wrapper ...
+ owner.newQObject(shared);
+
+ // ... while the other engine does keep one.
+ other.globalObject().setProperty(u"shared"_s, other.newQObject(shared));
+
+ gc(owner);
+
+ // The owning engine's collector destroys the object although the other engine holds a
+ // strong reference to it. See QTBUG-60984.
+ QEXPECT_FAIL("", "The second wrapper has no say in the object's lifetime", Abort);
+ QVERIFY(!shared.isNull());
+ QCOMPARE(other.evaluate("String(shared.payload.marker)").toString(),
+ u"created in the owning engine"_s);
+
+ delete shared;
+}
+
+void tst_QJSEngine::newQObject_deletedOwningEngine()
+{
+ QJSEngine other;
+ std::unique_ptr<QObject> shared;
+
+ {
+ QQmlEngine owner;
+ QQmlComponent component(&owner);
+ shared = createSharedObject(&component);
+ QVERIFY2(shared.get(), qPrintable(component.errorString()));
+ other.globalObject().setProperty(u"shared"_s, other.newQObject(shared.get()));
+ QCOMPARE(other.evaluate("typeof shared.store").toString(), u"function"_s);
+ }
+
+ // The object is C++-owned and outlives the engine that created it, but its "var" properties
+ // and QML methods lived in that engine's heap.
+ QVERIFY(shared);
+ const QJSValue payload = other.evaluate("shared.payload");
+ QVERIFY2(!payload.isError(), qPrintable(payload.toString()));
+ QCOMPARE(other.evaluate("typeof shared.store").toString(), u"function"_s);
+
+ gc(*other.handle());
+}
+
class TestQMetaObject : public QObject {
Q_OBJECT
Q_PROPERTY(int called READ called)
diff --git a/tests/auto/qml/qmlcppcodegen/tst_qmlcppcodegen.cpp b/tests/auto/qml/qmlcppcodegen/tst_qmlcppcodegen.cpp
index c841e75040..e8ced6cbea 100644
--- a/tests/auto/qml/qmlcppcodegen/tst_qmlcppcodegen.cpp
+++ b/tests/auto/qml/qmlcppcodegen/tst_qmlcppcodegen.cpp
@@ -1179,7 +1179,7 @@ void tst_QmlCppCodegen::collectGarbageAfterAotCodeReturned()
const int coreIndex = o->metaObject()->indexOfMethod("takeHidden()");
QVERIFY(coreIndex >= 0);
- QQmlVMEMetaObject *vme = QQmlVMEMetaObject::getForMethod(o.get(), coreIndex);
+ QQmlVMEMetaObject *vme = QQmlVMEMetaObject::get(o.get(), engine.handle());
QVERIFY(vme);
QV4::ExecutionEngine *v4 = engine.handle();
diff --git a/tests/auto/qml/qqmlecmascript/tst_qqmlecmascript.cpp b/tests/auto/qml/qqmlecmascript/tst_qqmlecmascript.cpp
index 928ab8a38d..cd17509e13 100644
--- a/tests/auto/qml/qqmlecmascript/tst_qqmlecmascript.cpp
+++ b/tests/auto/qml/qqmlecmascript/tst_qqmlecmascript.cpp
@@ -5924,8 +5924,8 @@ void tst_qqmlecmascript::propertyVarInheritance()
QObject *ico5 = object->property("varProperty").value<QObject*>()->property("inheritanceVarProperty").value<QObject*>()->property("vp").value<QObject*>()->property("vp").value<QObject*>()->property("vp").value<QObject*>()->property("vp").value<QObject*>();
QVERIFY(cco5);
QVERIFY(ico5);
- QQmlVMEMetaObject *icovmemo = QQmlVMEMetaObject::get(ico5);
- QQmlVMEMetaObject *ccovmemo = QQmlVMEMetaObject::get(cco5);
+ QQmlVMEMetaObject *icovmemo = QQmlVMEMetaObject::get(ico5, engine.handle());
+ QQmlVMEMetaObject *ccovmemo = QQmlVMEMetaObject::get(cco5, engine.handle());
QV4::WeakValue icoCanaryHandle;
QV4::WeakValue ccoCanaryHandle;
{
@@ -5973,8 +5973,10 @@ void tst_qqmlecmascript::propertyVarInheritance2()
QCOMPARE(childObject->property("textCanary").toInt(), 10);
QV4::WeakValue childObjectVarArrayValueHandle;
{
- childObjectVarArrayValueHandle.set(engine.handle(),
- QQmlVMEMetaObject::get(childObject)->vmeProperty(childObject->metaObject()->indexOfProperty("vp")));
+ childObjectVarArrayValueHandle.set(
+ engine.handle(),
+ QQmlVMEMetaObject::get(childObject, engine.handle())
+ ->vmeProperty(childObject->metaObject()->indexOfProperty("vp")));
QVERIFY(!childObjectVarArrayValueHandle.isUndefined());
gc(engine);
QVERIFY(!childObjectVarArrayValueHandle.isUndefined()); // should not have been collected yet.
@@ -10949,7 +10951,7 @@ void tst_qqmlecmascript::vmeMetaObjectAccessors()
QScopedPointer<QObject> obj(component.create());
QVERIFY(obj);
- QQmlVMEMetaObject *vme = QQmlVMEMetaObject::get(obj.data());
+ QQmlVMEMetaObject *vme = QQmlVMEMetaObject::get(obj.data(), engine.handle());
QVERIFY(vme);
// qmlObjectId() should return a non-negative index for the root object.
diff --git a/tests/auto/qml/qqmllanguage/tst_qqmllanguage.cpp b/tests/auto/qml/qqmllanguage/tst_qqmllanguage.cpp
index 92d00c768d..b0061805e5 100644
--- a/tests/auto/qml/qqmllanguage/tst_qqmllanguage.cpp
+++ b/tests/auto/qml/qqmllanguage/tst_qqmllanguage.cpp
@@ -5267,7 +5267,7 @@ void tst_qqmllanguage::propertyCacheInSync()
QVERIFY(!o.isNull());
QObject *anchors = qvariant_cast<QObject*>(o->property("anchors"));
QVERIFY(anchors);
- QQmlVMEMetaObject *vmemo = QQmlVMEMetaObject::get(anchors);
+ QQmlVMEMetaObject *vmemo = QQmlVMEMetaObject::get(anchors, engine.handle());
QVERIFY(vmemo);
QQmlPropertyCache::ConstPtr vmemoCache = vmemo->propertyCache();
QVERIFY(vmemoCache);
diff --git a/tests/auto/qml/qv4mm/data/sharedWithOtherEngine.qml b/tests/auto/qml/qv4mm/data/sharedWithOtherEngine.qml
new file mode 100644
index 0000000000..4d9ad698a1
--- /dev/null
+++ b/tests/auto/qml/qv4mm/data/sharedWithOtherEngine.qml
@@ -0,0 +1,7 @@
+import QtQml
+
+QtObject {
+ property var payload: ({ marker: "created in the owning engine" })
+ property var stored
+ function store(value) { stored = value }
+}
diff --git a/tests/auto/qml/qv4mm/tst_qv4mm.cpp b/tests/auto/qml/qv4mm/tst_qv4mm.cpp
index 51a419b01f..14fc6b6d9c 100644
--- a/tests/auto/qml/qv4mm/tst_qv4mm.cpp
+++ b/tests/auto/qml/qv4mm/tst_qv4mm.cpp
@@ -45,6 +45,10 @@ private slots:
void persistentValueMarking_data();
void persistentValueMarking();
void multiWrappedQObjects();
+ void foreignValueInVarProperty_data();
+ void foreignValueInVarProperty();
+ void ownedValueInForeignEngine_data();
+ void ownedValueInForeignEngine();
void accessParentOnDestruction();
void cleanInternalClasses();
void createObjectsOnDestruction();
@@ -260,6 +264,104 @@ void tst_qv4mm::multiWrappedQObjects()
QCOMPARE(engine2.memoryManager->m_pendingFreedObjectWrapperValue.size(), 0);
}
+void tst_qv4mm::foreignValueInVarProperty_data()
+{
+ QTest::addColumn<QString>("storeStatement");
+ QTest::addColumn<bool>("refused");
+
+ // Assigning converts the value via QVariant, which refuses to move a JavaScript object to
+ // another engine. Passing it to a QML method converts it to the argument type, which copies.
+ QTest::newRow("assigned") << QStringLiteral("shared.stored = crossed") << true;
+ QTest::newRow("passed to method") << QStringLiteral("shared.store(crossed)") << false;
+}
+
+// A "var" property of an object owned by one engine comes to hold a value that lives in the
+// heap of another engine. See QTBUG-60984.
+void tst_qv4mm::foreignValueInVarProperty()
+{
+ QFETCH(QString, storeStatement);
+ QFETCH(bool, refused);
+
+ QQmlEngine owner;
+ QJSEngine other;
+
+ QQmlComponent component(&owner, testFileUrl("sharedWithOtherEngine.qml"));
+ std::unique_ptr<QObject> shared(component.create());
+ QVERIFY2(shared.get(), qPrintable(component.errorString()));
+ other.globalObject().setProperty(QStringLiteral("shared"), other.newQObject(shared.get()));
+
+ if (refused)
+ QTest::ignoreMessage(QtWarningMsg, "JSValue can't be reassigned to another engine.");
+ const QJSValue crossed = other.evaluate(
+ QStringLiteral("var crossed = { tag: 'from the other engine' }; %1; true")
+ .arg(storeStatement));
+ QVERIFY2(!crossed.isError(), qPrintable(crossed.toString()));
+
+ // Mark bits live in the chunk of the object being marked, but they are only cleared at the
+ // end of the owning memory manager's sweep. So whatever the owning engine blackens in the
+ // other engine's chunks here stays black until that engine has swept once.
+ gc(owner);
+
+ // Everything the other engine allocates from now on hangs off objects that are already
+ // black for it.
+ const QJSValue allocated = other.evaluate(
+ QStringLiteral("crossed.late = { marker: 'allocated late' }; true"));
+ QVERIFY2(!allocated.isError(), qPrintable(allocated.toString()));
+
+ // Its mark phase skips those, never sees the new children, and frees them although they are
+ // still referenced.
+ gc(*other.handle());
+
+ QCOMPARE(other.evaluate(QStringLiteral("String(crossed.late.marker)")).toString(),
+ QStringLiteral("allocated late"));
+}
+
+void tst_qv4mm::ownedValueInForeignEngine_data()
+{
+ QTest::addColumn<QString>("accessExpression");
+ QTest::addColumn<bool>("refused");
+
+ // The "var" property holds a JavaScript object, which cannot leave its engine. The QML
+ // method is handed out as a plain method wrapper instead of its function object.
+ QTest::newRow("var property") << QStringLiteral("shared.payload") << true;
+ QTest::newRow("QML method") << QStringLiteral("shared.store") << false;
+}
+
+// The mirror image: a value living in the owning engine's heap becomes a GC root of another
+// engine, which then marks into chunks it does not own.
+void tst_qv4mm::ownedValueInForeignEngine()
+{
+ QFETCH(QString, accessExpression);
+ QFETCH(bool, refused);
+
+ QQmlEngine owner;
+ QJSEngine other;
+
+ QQmlComponent component(&owner, testFileUrl("sharedWithOtherEngine.qml"));
+ std::unique_ptr<QObject> shared(component.create());
+ QVERIFY2(shared.get(), qPrintable(component.errorString()));
+ owner.globalObject().setProperty(QStringLiteral("shared"), owner.newQObject(shared.get()));
+ other.globalObject().setProperty(QStringLiteral("shared"), other.newQObject(shared.get()));
+
+ if (refused)
+ QTest::ignoreMessage(QtWarningMsg, "JSValue can't be reassigned to another engine.");
+ const QJSValue escaped = other.evaluate(
+ QStringLiteral("var escaped = %1; true").arg(accessExpression));
+ QVERIFY2(!escaped.isError(), qPrintable(escaped.toString()));
+
+ gc(*other.handle());
+
+ const QJSValue allocated = owner.evaluate(
+ QStringLiteral("%1.late = { marker: 'allocated late' }; true").arg(accessExpression));
+ QVERIFY2(!allocated.isError(), qPrintable(allocated.toString()));
+
+ gc(owner);
+
+ QCOMPARE(owner.evaluate(QStringLiteral("String(%1.late.marker)").arg(accessExpression))
+ .toString(),
+ QStringLiteral("allocated late"));
+}
+
void tst_qv4mm::accessParentOnDestruction()
{
QQmlEngine engine;