Allow limited extensions to globals
We can allow a, overriding data members of globals, such as Error.name b, adding members that don't clash with any internals c, any manipulation of toString(), toLocaleString(), valueOf(), and constructor To that effect, add a "Locked" flag to our internal classes. If that is set, disallow changing prototypes and when defining a property, check if it shadows any non-configurable property. Furthermore, make all non-primitive properties that are not meant to be overridden non-configurable and non-writable. constructor, toString(), toLocaleString() and valueOf() are exempt because they are explicitly meant to be overridden by users. Therefore, we let that happen and refrain from optimizing them or triggering their implicit invocation in optimized code. [ChangeLog][QtQml][Important Behavior Changes] The JavaScript global objects are not frozen anymore in a QML engine. Instead, they are selectively locked. You can extend the objects with new members as long as you don't shadow any existing methods, and you can change or override data members. This also means that most methods of Object.prototype, which was previously exempt from the freezing, cannot be changed anymore. You can, however, change or override constructor, toString(), toLocaleString() and valueOf() on any prototype. Those are clearly meant to be overridden by user code. Fixes: QTBUG-101298 Task-number: QTBUG-84341 Change-Id: Id77db971f76c8f48b18e7a93607da5f947ecfc3e Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io>
This commit is contained in:
parent
d3b3fef5a8
commit
b6cf0672e6
|
@ -2129,7 +2129,7 @@ QV4::ReturnedValue ExecutionEngine::callInContext(QV4::Function *function, QObje
|
|||
void ExecutionEngine::initQmlGlobalObject()
|
||||
{
|
||||
initializeGlobal();
|
||||
freezeObject(*globalObject);
|
||||
lockObject(*globalObject);
|
||||
}
|
||||
|
||||
void ExecutionEngine::initializeGlobal()
|
||||
|
@ -2233,6 +2233,58 @@ void ExecutionEngine::freezeObject(const QV4::Value &value)
|
|||
freeze_recursive(this, o);
|
||||
}
|
||||
|
||||
void ExecutionEngine::lockObject(const QV4::Value &value)
|
||||
{
|
||||
QV4::Scope scope(this);
|
||||
ScopedObject object(scope, value);
|
||||
if (!object)
|
||||
return;
|
||||
|
||||
std::vector<Heap::Object *> stack { object->d() };
|
||||
|
||||
// Methods meant to be overridden
|
||||
const PropertyKey writableMembers[] = {
|
||||
id_toString()->propertyKey(),
|
||||
id_toLocaleString()->propertyKey(),
|
||||
id_valueOf()->propertyKey(),
|
||||
id_constructor()->propertyKey()
|
||||
};
|
||||
const auto writableBegin = std::begin(writableMembers);
|
||||
const auto writableEnd = std::end(writableMembers);
|
||||
|
||||
while (!stack.empty()) {
|
||||
object = stack.back();
|
||||
stack.pop_back();
|
||||
|
||||
if (object->as<QV4::QObjectWrapper>() || object->internalClass()->isLocked())
|
||||
continue;
|
||||
|
||||
Scoped<InternalClass> locked(scope, object->internalClass()->locked());
|
||||
QV4::ScopedObject member(scope);
|
||||
|
||||
// Taking this copy is cheap. It's refcounted. This avoids keeping a reference
|
||||
// to the original IC.
|
||||
const SharedInternalClassData<PropertyKey> nameMap = locked->d()->nameMap;
|
||||
|
||||
for (uint i = 0, end = locked->d()->size; i < end; ++i) {
|
||||
const PropertyKey key = nameMap.at(i);
|
||||
if (!key.isStringOrSymbol())
|
||||
continue;
|
||||
if ((member = *object->propertyData(i))) {
|
||||
stack.push_back(member->d());
|
||||
if (std::find(writableBegin, writableEnd, key) == writableEnd) {
|
||||
PropertyAttributes attributes = locked->d()->find(key).attributes;
|
||||
attributes.setConfigurable(false);
|
||||
attributes.setWritable(false);
|
||||
locked = locked->changeMember(key, attributes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object->setInternalClass(locked->d());
|
||||
}
|
||||
}
|
||||
|
||||
void ExecutionEngine::startTimer(const QString &timerName)
|
||||
{
|
||||
if (!m_time.isValid())
|
||||
|
|
|
@ -673,6 +673,7 @@ public:
|
|||
void createQtObject();
|
||||
|
||||
void freezeObject(const QV4::Value &value);
|
||||
void lockObject(const QV4::Value &value);
|
||||
|
||||
// Return the list of illegal id names (the names of the properties on the global object)
|
||||
const QSet<QString> &illegalNames() const;
|
||||
|
|
|
@ -395,6 +395,9 @@ static Heap::InternalClass *cleanInternalClass(Heap::InternalClass *orig)
|
|||
case InternalClassTransition::Frozen:
|
||||
child = child->d()->frozen();
|
||||
continue;
|
||||
case InternalClassTransition::Locked:
|
||||
child = child->d()->locked();
|
||||
continue;
|
||||
default:
|
||||
Q_ASSERT(it->flags != 0);
|
||||
Q_ASSERT(it->flags < InternalClassTransition::StructureChange);
|
||||
|
@ -515,6 +518,24 @@ Heap::InternalClass *InternalClass::nonExtensible()
|
|||
return newClass;
|
||||
}
|
||||
|
||||
InternalClass *InternalClass::locked()
|
||||
{
|
||||
if (isLocked())
|
||||
return this;
|
||||
|
||||
Transition temp = { { PropertyKey::invalid() }, nullptr, Transition::Locked};
|
||||
Transition &t = lookupOrInsertTransition(temp);
|
||||
if (t.lookup)
|
||||
return t.lookup;
|
||||
|
||||
Heap::InternalClass *newClass = engine->newClass(this);
|
||||
newClass->flags |= Locked;
|
||||
|
||||
t.lookup = newClass;
|
||||
Q_ASSERT(t.lookup);
|
||||
return newClass;
|
||||
}
|
||||
|
||||
void InternalClass::addMember(QV4::Object *object, PropertyKey id, PropertyAttributes data, InternalClassEntry *entry)
|
||||
{
|
||||
Q_ASSERT(id.isStringOrSymbol());
|
||||
|
|
|
@ -282,6 +282,7 @@ struct InternalClassTransition
|
|||
ProtoClass = StructureChange | (1 << 3),
|
||||
Sealed = StructureChange | (1 << 4),
|
||||
Frozen = StructureChange | (1 << 5),
|
||||
Locked = StructureChange | (1 << 6),
|
||||
};
|
||||
|
||||
bool operator==(const InternalClassTransition &other) const
|
||||
|
@ -299,6 +300,7 @@ struct InternalClass : Base {
|
|||
Sealed = 1 << 1,
|
||||
Frozen = 1 << 2,
|
||||
UsedAsProto = 1 << 3,
|
||||
Locked = 1 << 4,
|
||||
};
|
||||
enum { MaxRedundantTransitions = 255 };
|
||||
|
||||
|
@ -324,6 +326,7 @@ struct InternalClass : Base {
|
|||
bool isSealed() const { return flags & Sealed; }
|
||||
bool isFrozen() const { return flags & Frozen; }
|
||||
bool isUsedAsProto() const { return flags & UsedAsProto; }
|
||||
bool isLocked() const { return flags & Locked; }
|
||||
|
||||
void init(ExecutionEngine *engine);
|
||||
void init(InternalClass *other);
|
||||
|
@ -331,6 +334,7 @@ struct InternalClass : Base {
|
|||
|
||||
Q_QML_PRIVATE_EXPORT QString keyAt(uint index) const;
|
||||
Q_REQUIRED_RESULT InternalClass *nonExtensible();
|
||||
Q_REQUIRED_RESULT InternalClass *locked();
|
||||
|
||||
static void addMember(QV4::Object *object, PropertyKey id, PropertyAttributes data, InternalClassEntry *entry);
|
||||
Q_REQUIRED_RESULT InternalClass *addMember(PropertyKey identifier, PropertyAttributes data, InternalClassEntry *entry = nullptr);
|
||||
|
|
|
@ -17,10 +17,14 @@
|
|||
#include "qv4symbol_p.h"
|
||||
#include "qv4proxy_p.h"
|
||||
|
||||
#include <QtCore/qloggingcategory.h>
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
using namespace QV4;
|
||||
|
||||
Q_LOGGING_CATEGORY(lcJavaScriptGlobals, "qt.qml.js.globals")
|
||||
|
||||
DEFINE_OBJECT_VTABLE(Object);
|
||||
|
||||
void Object::setInternalClass(Heap::InternalClass *ic)
|
||||
|
@ -946,12 +950,29 @@ bool Object::virtualDefineOwnProperty(Managed *m, PropertyKey id, const Property
|
|||
return o->internalDefineOwnProperty(scope.engine, index, nullptr, p, attrs);
|
||||
}
|
||||
|
||||
auto memberIndex = o->internalClass()->find(id);
|
||||
Scoped<InternalClass> ic(scope, o->internalClass());
|
||||
auto memberIndex = ic->d()->find(id);
|
||||
|
||||
if (!memberIndex.isValid()) {
|
||||
if (!o->isExtensible())
|
||||
return false;
|
||||
|
||||
// If the IC is locked, you're not allowed to shadow any unconfigurable properties.
|
||||
if (ic->d()->isLocked()) {
|
||||
while (Heap::Object *prototype = ic->d()->prototype) {
|
||||
ic = prototype->internalClass;
|
||||
const auto entry = ic->d()->find(id);
|
||||
if (entry.isValid()) {
|
||||
if (entry.attributes.isConfigurable())
|
||||
break;
|
||||
qCWarning(lcJavaScriptGlobals).noquote()
|
||||
<< QStringLiteral("You cannot shadow the locked property "
|
||||
"'%1' in QML.").arg(id.toQString());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scoped<StringOrSymbol> name(scope, id.asStringOrSymbol());
|
||||
ScopedProperty pd(scope);
|
||||
pd->copy(p, attrs);
|
||||
|
@ -985,11 +1006,12 @@ bool Object::virtualSetPrototypeOf(Managed *m, const Object *proto)
|
|||
{
|
||||
Q_ASSERT(m->isObject());
|
||||
Object *o = static_cast<Object *>(m);
|
||||
Heap::Object *current = o->internalClass()->prototype;
|
||||
Heap::InternalClass *ic = o->internalClass();
|
||||
Heap::Object *current = ic->prototype;
|
||||
Heap::Object *protod = proto ? proto->d() : nullptr;
|
||||
if (current == protod)
|
||||
return true;
|
||||
if (!o->internalClass()->isExtensible())
|
||||
if (!ic->isExtensible() || ic->isLocked())
|
||||
return false;
|
||||
Heap::Object *p = protod;
|
||||
while (p) {
|
||||
|
@ -999,7 +1021,7 @@ bool Object::virtualSetPrototypeOf(Managed *m, const Object *proto)
|
|||
break;
|
||||
p = p->prototype();
|
||||
}
|
||||
o->setInternalClass(o->internalClass()->changePrototype(protod));
|
||||
o->setInternalClass(ic->changePrototype(protod));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
import QtQml
|
||||
|
||||
QtObject {
|
||||
property string myErrorName: {
|
||||
var e = new Error;
|
||||
try {
|
||||
e.name = "MyError1";
|
||||
} finally {
|
||||
return e.name;
|
||||
}
|
||||
}
|
||||
|
||||
property string errorName: {
|
||||
var e = new Error;
|
||||
try {
|
||||
Error.prototype.name = "MyError2";
|
||||
} finally {
|
||||
return e.name
|
||||
}
|
||||
}
|
||||
|
||||
property int mathMax: {
|
||||
// Cannot change methods of builtins
|
||||
try {
|
||||
Math.max = function(a, b) { return 10 };
|
||||
} finally {
|
||||
return Math.max(3, 4)
|
||||
}
|
||||
}
|
||||
|
||||
property int extendGlobal: {
|
||||
// Can add new methods to globals
|
||||
try {
|
||||
Array.prototype.myMethod = function() { return 32 }
|
||||
} finally {
|
||||
return (new Array).myMethod()
|
||||
}
|
||||
}
|
||||
|
||||
property string prototypeTrick: {
|
||||
// Cannot change prototypes of locked objects
|
||||
try {
|
||||
SyntaxError.prototype.setPrototypeOf({
|
||||
toLocaleString : function() { return "not a SyntaxError"}
|
||||
});
|
||||
} finally {
|
||||
return (new SyntaxError).toLocaleString();
|
||||
}
|
||||
}
|
||||
|
||||
property string shadowMethod1: {
|
||||
// Can override Object.prototype methods meant to be changed
|
||||
try {
|
||||
TypeError.prototype.toLocaleString = function() { return "not a TypeError"};
|
||||
} finally {
|
||||
return (new TypeError).toLocaleString();
|
||||
}
|
||||
}
|
||||
|
||||
property bool shadowMethod2: {
|
||||
// Cannot override Object.prototype methods not meant to be changed
|
||||
try {
|
||||
TypeError.prototype.hasOwnProperty = function() { return true };
|
||||
} finally {
|
||||
return (new TypeError).hasOwnProperty("foobar");
|
||||
}
|
||||
}
|
||||
|
||||
property string changeObjectProto1: {
|
||||
// Can change Object.prototype methods meant to be changed
|
||||
try {
|
||||
Object.prototype.toLocaleString = function() { return "not an Object"};
|
||||
} finally {
|
||||
return (new Object).toLocaleString();
|
||||
}
|
||||
}
|
||||
|
||||
property bool changeObjectProto2: {
|
||||
// Cannot change Object.prototype methods not meant to be changed
|
||||
try {
|
||||
Object.prototype.hasOwnProperty = function() { return true };
|
||||
} finally {
|
||||
return (new Object).hasOwnProperty("foobar");
|
||||
}
|
||||
}
|
||||
|
||||
property string defineProperty1: {
|
||||
// Can define a property that shadows an existing one meant to be changed
|
||||
try {
|
||||
Object.defineProperty(URIError.prototype, "toLocaleString", {
|
||||
value: function() { return "not a URIError" }
|
||||
})
|
||||
} finally {
|
||||
return (new URIError).toLocaleString();
|
||||
}
|
||||
}
|
||||
|
||||
property bool defineProperty2: {
|
||||
// Cannot define a property that shadows an existing one not meant to be changed
|
||||
try {
|
||||
Object.defineProperty(URIError.prototype, "hasOwnProperty", {
|
||||
value: function() { return true }
|
||||
})
|
||||
} finally {
|
||||
return (new URIError).hasOwnProperty("foobar");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -71,6 +71,7 @@ private slots:
|
|||
void qobjectToString();
|
||||
void qtNamespaceInQtObject();
|
||||
void nativeModuleImport();
|
||||
void lockedRootObject();
|
||||
|
||||
public slots:
|
||||
QObject *createAQObjectForOwnershipTest ()
|
||||
|
@ -1637,6 +1638,27 @@ void tst_qqmlengine::nativeModuleImport()
|
|||
QCOMPARE(o->property("e").toString(), QStringLiteral("TheName"));
|
||||
}
|
||||
|
||||
void tst_qqmlengine::lockedRootObject()
|
||||
{
|
||||
QQmlEngine engine;
|
||||
QQmlComponent c(&engine, testFileUrl("lockedRootObject.qml"));
|
||||
QVERIFY2(c.isReady(), qPrintable(c.errorString()));
|
||||
QTest::ignoreMessage(
|
||||
QtWarningMsg, "You cannot shadow the locked property 'hasOwnProperty' in QML.");
|
||||
QScopedPointer<QObject> o(c.create());
|
||||
QCOMPARE(o->property("myErrorName").toString(), QStringLiteral("MyError1"));
|
||||
QCOMPARE(o->property("errorName").toString(), QStringLiteral("MyError2"));
|
||||
QCOMPARE(o->property("mathMax").toInt(), 4);
|
||||
QCOMPARE(o->property("extendGlobal").toInt(), 32);
|
||||
QCOMPARE(o->property("prototypeTrick").toString(), QStringLiteral("SyntaxError"));
|
||||
QCOMPARE(o->property("shadowMethod1").toString(), QStringLiteral("not a TypeError"));
|
||||
QCOMPARE(o->property("shadowMethod2").toBool(), false);
|
||||
QCOMPARE(o->property("changeObjectProto1").toString(), QStringLiteral("not an Object"));
|
||||
QCOMPARE(o->property("changeObjectProto2").toBool(), false);
|
||||
QCOMPARE(o->property("defineProperty1").toString(), QStringLiteral("not a URIError"));
|
||||
QCOMPARE(o->property("defineProperty2").toBool(), false);
|
||||
}
|
||||
|
||||
QTEST_MAIN(tst_qqmlengine)
|
||||
|
||||
#include "tst_qqmlengine.moc"
|
||||
|
|
Loading…
Reference in New Issue