From fc9615672826ca582acdbb85f8c45367c99768aa Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 10 Sep 2026 16:29:13 -0700 Subject: [PATCH] [JSC] Freezing built-in prototypes should not permanently disable fast paths Object.freeze on intrinsics (Array.prototype, RegExp.prototype, String.prototype, Promise.prototype, Map/Set.prototype, the Array constructor, Object.prototype) permanently fired JSGlobalObject fast-path watchpoints even though no property value changed. This made str.replace, spread, species creation, hole reads, slice/includes, promise resolution and Object.assign up to 200x slower for the rest of the process. - putDirectInternal: only fire a property's replacement watchpoint when the stored value actually changes, and only when the structure is watching. A defineProperty that only changes attributes no longer invalidates adaptive watchpoints that check the value. - validateAndApplyPropertyDescriptor: reuse the existing GetterSetter when the getter and setter are unchanged, so attribute-only redefinitions keep the same cell and Equivalence conditions on it stay valid. - Object.assign: when only a prototype (not the target) has read-only or accessor properties, check the source keys against the prototype chain instead of always taking the generic path. - Freezing a JSArray with no indexed storage (Array.prototype) keeps blank indexing instead of converting to sparse ArrayStorage. A new Structure bit, didFreeze, makes length read-only. Rejected indexed writes on such arrays throw without converting storage. This keeps the array prototype chain sane. --- ...y-same-value-keeps-adaptive-watchpoints.js | 90 +++++++++++++++++++ JSTests/stress/freeze-array-prototype.js | 85 ++++++++++++++++++ .../object-assign-frozen-object-prototype.js | 63 +++++++++++++ Source/JavaScriptCore/dfg/DFGOperations.cpp | 12 ++- Source/JavaScriptCore/runtime/JSArray.cpp | 14 +++ Source/JavaScriptCore/runtime/JSArray.h | 3 + .../JavaScriptCore/runtime/JSArrayInlines.h | 5 ++ Source/JavaScriptCore/runtime/JSObject.cpp | 15 +++- Source/JavaScriptCore/runtime/JSObject.h | 4 + .../JavaScriptCore/runtime/JSObjectInlines.h | 47 +++++++++- .../runtime/ObjectConstructor.cpp | 13 ++- .../runtime/ObjectConstructorInlines.h | 22 ++++- Source/JavaScriptCore/runtime/Structure.cpp | 6 ++ Source/JavaScriptCore/runtime/Structure.h | 1 + 14 files changed, 368 insertions(+), 12 deletions(-) create mode 100644 JSTests/stress/define-property-same-value-keeps-adaptive-watchpoints.js create mode 100644 JSTests/stress/freeze-array-prototype.js create mode 100644 JSTests/stress/object-assign-frozen-object-prototype.js diff --git a/JSTests/stress/define-property-same-value-keeps-adaptive-watchpoints.js b/JSTests/stress/define-property-same-value-keeps-adaptive-watchpoints.js new file mode 100644 index 0000000000000..64d2a87bd5f97 --- /dev/null +++ b/JSTests/stress/define-property-same-value-keeps-adaptive-watchpoints.js @@ -0,0 +1,90 @@ +function assert(cond, msg) { + if (!cond) + throw new Error("FAIL: " + msg); +} + +function warm(f, n = 1e4) { + let r; + for (let i = 0; i < n; i++) + r = f(i); + return r; +} + +{ + const proto = { method() { return 1; } }; + const o = Object.create(proto); + const read = () => o.method(); + warm(read); + Object.defineProperty(proto, "method", { writable: false }); + assert(warm(read) === 1, "same-value attribute change keeps value"); + Object.defineProperty(proto, "method", { value: () => 2 }); + assert(warm(read) === 2, "configurable read-only property redefined with a new value is observed"); +} + +{ + const origExec = RegExp.prototype.exec; + const run = () => "a-b".replace(/-/g, "+"); + warm(run, 1e3); + Object.defineProperty(RegExp.prototype, "exec", { writable: false }); + assert(run() === "a+b", "replace still works after exec made read-only"); + let called = 0; + Object.defineProperty(RegExp.prototype, "exec", { value: function (s) { called++; return origExec.call(this, s); } }); + assert(run() === "a+b" && called > 0, "replaced exec is called by String.prototype.replace"); + Object.defineProperty(RegExp.prototype, "exec", { value: origExec }); +} + +{ + const o = {}; + const g1 = () => 1; + const g2 = () => 2; + Object.defineProperty(o, "x", { get: g1, configurable: true }); + const read = () => o.x; + warm(read); + Object.defineProperty(o, "x", { enumerable: true }); + assert(warm(read) === 1, "accessor attribute change keeps getter"); + const desc = Object.getOwnPropertyDescriptor(o, "x"); + assert(desc.get === g1 && desc.enumerable, "descriptor updated"); + Object.defineProperty(o, "x", { get: g2 }); + assert(warm(read) === 2, "new getter observed"); + Object.defineProperty(o, "x", { set(v) { this._v = v; } }); + assert(o.x === 2, "getter preserved when only setter changes"); + o.x = 5; + assert(o._v === 5, "new setter called"); + Object.defineProperty(o, "x", { get: undefined }); + assert(o.x === undefined, "getter cleared"); +} + +{ + class MyArray extends Array { } + const a = MyArray.from([1, 2, 3]); + Object.defineProperty(Array, Symbol.species, { configurable: false }); + assert(a.map(x => x) instanceof MyArray, "subclass species still honored"); + assert([1, 2].map(x => x).constructor === Array, "plain array species"); +} + +{ + Object.freeze(Object.prototype); + Object.freeze(Array.prototype); + Object.freeze(Function.prototype); + Object.freeze(RegExp.prototype); + Object.freeze(String.prototype); + Object.freeze(Promise.prototype); + Object.freeze(Map.prototype); + Object.freeze(Set.prototype); + assert(Object.isFrozen(Object.prototype) && Object.isFrozen(RegExp.prototype), "isFrozen"); + for (let i = 0; i < 1e3; i++) { + assert("a-b".replace(/-/g, "+") === "a+b", "replace after freeze"); + assert([..."abc"].join("") === "abc", "string spread after freeze"); + assert(String(new String("x")) === "x", "String(obj) after freeze"); + assert([1, 2] + "" === "1,2", "array join after freeze"); + assert([...new Set([1, 2])].length === 2 && new Map([[1, 2]]).get(1) === 2, "Map/Set after freeze"); + } + let threw = false; + try { + (() => { "use strict"; ({}).toString = 1; })(); + } catch { + threw = true; + } + assert(threw, "override mistake still throws"); + assert(Object.getOwnPropertyDescriptor(RegExp.prototype, "flags").configurable === false, "accessor frozen"); +} diff --git a/JSTests/stress/freeze-array-prototype.js b/JSTests/stress/freeze-array-prototype.js new file mode 100644 index 0000000000000..a202e6cf038fd --- /dev/null +++ b/JSTests/stress/freeze-array-prototype.js @@ -0,0 +1,85 @@ +function assert(cond, msg) { + if (!cond) + throw new Error("FAIL: " + msg); +} + +function throwsTypeError(f) { + try { + f(); + } catch (e) { + return e instanceof TypeError; + } + return false; +} + +function strictSetLength(o, v) { "use strict"; o.length = v; } +function strictSetIndex(o, i, v) { "use strict"; o[i] = v; } + +function readHoles(n) { + const holey = [1, , 3, , 5]; + let undefs = 0; + for (let i = 0; i < n; i++) { + if (holey[i % 5] === undefined) + undefs++; + } + return undefs; +} +assert(readHoles(1e4) === 4e3, "holes before freeze"); + +const AP = Array.prototype; +Object.freeze(AP); + +assert(Object.isFrozen(AP), "isFrozen"); +assert(!Object.isExtensible(AP), "not extensible"); +const lengthDesc = Object.getOwnPropertyDescriptor(AP, "length"); +assert(lengthDesc.value === 0 && !lengthDesc.writable && !lengthDesc.configurable && !lengthDesc.enumerable, "length descriptor"); +assert(Object.getOwnPropertyDescriptor(AP, "push").writable === false, "method read-only"); + +assert(throwsTypeError(() => strictSetLength(AP, 1)), "strict length write throws"); +AP.length = 1; +assert(AP.length === 0, "sloppy length write ignored"); +assert(throwsTypeError(() => strictSetLength(AP, 0)), "strict same-value length write throws"); +assert(Reflect.set(AP, "length", 0) === false, "Reflect.set length"); +assert(Reflect.defineProperty(AP, "length", { value: 0 }) === true, "same-value define length"); +assert(Reflect.defineProperty(AP, "length", { value: 1 }) === false, "different-value define length"); + +assert(throwsTypeError(() => strictSetIndex(AP, 0, 1)), "strict index write throws"); +AP[0] = 1; +assert(AP[0] === undefined && !Object.hasOwn(AP, 0), "sloppy index write ignored"); +assert(throwsTypeError(() => Object.defineProperty(AP, 0, { value: 1 })), "define index throws"); +assert(Reflect.defineProperty(AP, 3, { value: 1 }) === false, "Reflect.defineProperty index"); + +assert(throwsTypeError(() => AP.push.call(AP, 1)), "push throws"); +assert(throwsTypeError(() => AP.push.call(AP)), "push with no args throws"); +assert(throwsTypeError(() => AP.pop.call(AP)), "pop throws"); +assert(throwsTypeError(() => AP.shift.call(AP)), "shift throws"); +assert(throwsTypeError(() => AP.unshift.call(AP, 1)), "unshift throws"); +assert(throwsTypeError(() => AP.unshift.call(AP)), "unshift no args throws"); +assert(throwsTypeError(() => AP.splice.call(AP, 0, 0, 1)), "splice insert throws"); +assert(AP.length === 0 && Object.getOwnPropertyNames(AP).every(k => isNaN(+k) || k === ""), "no indexed props leaked"); + +for (let i = 0; i < 1e4; i++) { + assert(throwsTypeError(() => AP.push.call(AP, i)), "push throws (warm)"); + assert(throwsTypeError(() => AP.pop.call(AP)), "pop throws (warm)"); + assert(throwsTypeError(() => strictSetLength(AP, i)), "length write throws (warm)"); +} + +assert(readHoles(1e4) === 4e3, "holes after freeze"); +assert([1, , 3].includes(undefined) && [, 2].indexOf(undefined) === -1, "includes/indexOf holes"); +assert([...[1, , 3]].length === 3 && [...[1, , 3]][1] === undefined, "spread holes"); +assert([1, , 3].slice(0)[1] === undefined && !(1 in [1, , 3].slice(0)), "slice holes"); + +const frozenEmpty = Object.freeze([]); +assert(throwsTypeError(() => frozenEmpty.push(1)) && throwsTypeError(() => frozenEmpty.pop()), "frozen empty literal"); +const frozenNewArray = Object.freeze(new Array()); +assert(throwsTypeError(() => frozenNewArray.push(1)) && throwsTypeError(() => frozenNewArray.pop()), "frozen new Array()"); +assert(throwsTypeError(() => strictSetLength(frozenNewArray, 0)), "frozen new Array() length"); +assert(Object.isFrozen(frozenNewArray) && frozenNewArray.length === 0, "frozen new Array() state"); + +const sealed = Object.seal(new Array()); +assert(throwsTypeError(() => sealed.push(1)), "sealed empty push throws"); +sealed.length = 5; +assert(sealed.length === 5 && !(0 in sealed), "sealed empty length writable"); + +if (typeof $vm !== "undefined") + assert($vm.indexingMode(AP) === "ArrayClass", "frozen Array.prototype keeps blank indexing after rejected writes: " + $vm.indexingMode(AP)); diff --git a/JSTests/stress/object-assign-frozen-object-prototype.js b/JSTests/stress/object-assign-frozen-object-prototype.js new file mode 100644 index 0000000000000..6a567c2684d60 --- /dev/null +++ b/JSTests/stress/object-assign-frozen-object-prototype.js @@ -0,0 +1,63 @@ +function assert(cond, msg) { + if (!cond) + throw new Error("FAIL: " + msg); +} + +function throwsTypeError(f) { + try { + f(); + } catch (e) { + return e instanceof TypeError; + } + return false; +} + +Object.freeze(Object.prototype); + +function assignOne(t, s) { return Object.assign(t, s); } +function assignTwo(t, s1, s2) { return Object.assign(t, s1, s2); } + +for (let i = 0; i < 1e4; i++) { + const r = assignOne({}, { a: i, b: 2 }); + assert(r.a === i && r.b === 2, "plain assign"); +} + +for (let i = 0; i < 1e4; i++) { + const t = {}; + assert(throwsTypeError(() => assignOne(t, { a: 1, toString: 2, b: 3 })), "colliding key throws"); + assert(t.a === 1 && !Object.hasOwn(t, "toString") && !Object.hasOwn(t, "b"), "keys before collision assigned, after not"); +} + +for (let i = 0; i < 1e4; i++) { + const t = {}; + assert(throwsTypeError(() => assignTwo(t, { a: 1 }, { constructor: 2 })), "multi-source collision throws"); + assert(t.a === 1 && !Object.hasOwn(t, "constructor"), "first source assigned"); + const r = assignTwo({}, { a: 1 }, { b: 2 }); + assert(r.a === 1 && r.b === 2, "multi-source plain"); +} + +{ + let setterCalls = 0; + const src = { x: 1, y: 2 }; + const proto = Object.freeze(Object.create(Object.prototype, { + x: { set(v) { setterCalls++; this._x = v; src.y = 99; }, get() { return this._x; } }, + })); + for (let i = 0; i < 1e4; i++) { + src.y = 2; + const t = Object.create(proto); + assignOne(t, src); + assert(t._x === 1 && t.y === 99 && !Object.hasOwn(t, "x"), "setter invoked and later key re-read"); + } + assert(setterCalls === 1e4, "setter call count"); +} + +for (let i = 0; i < 1e4; i++) { + const r = assignOne({}, JSON.parse('{"__proto__": {"polluted": 1}, "k": 1}')); + assert(r.k === 1, "json source with __proto__ key"); + assert(!Object.hasOwn(r, "__proto__") && r.polluted === 1, "__proto__ key goes through the Object.prototype setter"); +} + +{ + const r = Object.assign({}, { a: 1 }, [7, 8]); + assert(r.a === 1 && r[0] === 7 && r[1] === 8, "indexed source"); +} diff --git a/Source/JavaScriptCore/dfg/DFGOperations.cpp b/Source/JavaScriptCore/dfg/DFGOperations.cpp index c743effaba531..889f37e75f46d 100644 --- a/Source/JavaScriptCore/dfg/DFGOperations.cpp +++ b/Source/JavaScriptCore/dfg/DFGOperations.cpp @@ -317,7 +317,9 @@ JSC_DEFINE_JIT_OPERATION(operationObjectAssignObject, void, (JSGlobalObject* glo JITOperationPrologueCallFrameTracer tracer(vm, callFrame); auto scope = DECLARE_THROW_SCOPE(vm); - if (auto* targetObject = dynamicDowncast(target); targetObject && targetObject->canPerformFastPutInlineExcludingProto() && targetObject->isStructureExtensible()) { + auto* targetObject = dynamicDowncast(target); + auto fastPutAvailability = targetObject ? targetObject->fastPutInlineAvailabilityExcludingProto() : JSObject::FastPutInlineAvailability::Unavailable; + if (fastPutAvailability != JSObject::FastPutInlineAvailability::Unavailable && targetObject->isStructureExtensible()) { Vector properties; MarkedArgumentBuffer values; if (!source->staticPropertiesReified()) { @@ -336,7 +338,7 @@ JSC_DEFINE_JIT_OPERATION(operationObjectAssignObject, void, (JSGlobalObject* glo // https://bugs.webkit.org/show_bug.cgi?id=187837 // Do not clear since Vector::clear shrinks the backing store. - bool objectAssignFastSucceeded = objectAssignFast(globalObject, targetObject, source, properties, values); + bool objectAssignFastSucceeded = objectAssignFast(globalObject, targetObject, source, properties, values, fastPutAvailability == JSObject::FastPutInlineAvailability::AvailableIfPrototypesDoNotDefineProperties); OPERATION_RETURN_IF_EXCEPTION(scope); if (objectAssignFastSucceeded) OPERATION_RETURN(scope); @@ -360,7 +362,9 @@ JSC_DEFINE_JIT_OPERATION(operationObjectAssignUntyped, void, (JSGlobalObject* gl JSObject* source = sourceValue.toObject(globalObject); OPERATION_RETURN_IF_EXCEPTION(scope); - if (auto* targetObject = dynamicDowncast(target); targetObject && targetObject->canPerformFastPutInlineExcludingProto() && targetObject->isStructureExtensible()) { + auto* targetObject = dynamicDowncast(target); + auto fastPutAvailability = targetObject ? targetObject->fastPutInlineAvailabilityExcludingProto() : JSObject::FastPutInlineAvailability::Unavailable; + if (fastPutAvailability != JSObject::FastPutInlineAvailability::Unavailable && targetObject->isStructureExtensible()) { if (!source->staticPropertiesReified()) { source->reifyAllStaticProperties(globalObject); OPERATION_RETURN_IF_EXCEPTION(scope); @@ -368,7 +372,7 @@ JSC_DEFINE_JIT_OPERATION(operationObjectAssignUntyped, void, (JSGlobalObject* gl Vector properties; MarkedArgumentBuffer values; - bool objectAssignFastSucceeded = objectAssignFast(globalObject, targetObject, source, properties, values); + bool objectAssignFastSucceeded = objectAssignFast(globalObject, targetObject, source, properties, values, fastPutAvailability == JSObject::FastPutInlineAvailability::AvailableIfPrototypesDoNotDefineProperties); OPERATION_RETURN_IF_EXCEPTION(scope); if (objectAssignFastSucceeded) OPERATION_RETURN(scope); diff --git a/Source/JavaScriptCore/runtime/JSArray.cpp b/Source/JavaScriptCore/runtime/JSArray.cpp index f3ac1fada1d79..69d7ddd819c98 100644 --- a/Source/JavaScriptCore/runtime/JSArray.cpp +++ b/Source/JavaScriptCore/runtime/JSArray.cpp @@ -1249,6 +1249,8 @@ bool JSArray::setLength(JSGlobalObject* globalObject, unsigned newLength, bool t Butterfly* butterfly = this->butterfly(); switch (indexingMode()) { case ArrayClass: + if (!isLengthWritable()) [[unlikely]] + return typeError(globalObject, scope, throwException, ReadonlyPropertyWriteError); if (!newLength) return true; if (newLength >= MIN_SPARSE_ARRAY_INDEX) { @@ -1319,6 +1321,16 @@ bool JSArray::setLength(JSGlobalObject* globalObject, unsigned newLength, bool t } } +NEVER_INLINE void JSArray::pushToNonExtensibleArrayClass(JSGlobalObject* globalObject, JSValue value) +{ + VM& vm = globalObject->vm(); + auto scope = DECLARE_THROW_SCOPE(vm); + methodTable()->putByIndex(this, globalObject, 0, value, true); + RETURN_IF_EXCEPTION(scope, void()); + scope.release(); + setLength(globalObject, 1, true); +} + JSValue JSArray::pop(JSGlobalObject* globalObject) { VM& vm = globalObject->vm(); @@ -1330,6 +1342,8 @@ JSValue JSArray::pop(JSGlobalObject* globalObject) switch (indexingType()) { case ArrayClass: + if (!isLengthWritable()) [[unlikely]] + throwTypeError(globalObject, scope, ReadonlyPropertyWriteError); return jsUndefined(); case ArrayWithUndecided: diff --git a/Source/JavaScriptCore/runtime/JSArray.h b/Source/JavaScriptCore/runtime/JSArray.h index 44b0f41342656..c0f56cce1db19 100644 --- a/Source/JavaScriptCore/runtime/JSArray.h +++ b/Source/JavaScriptCore/runtime/JSArray.h @@ -106,6 +106,7 @@ class JSArray : public JSNonFinalObject { void pushInline(JSGlobalObject*, JSValue); JS_EXPORT_PRIVATE void push(JSGlobalObject*, JSValue); + void pushToNonExtensibleArrayClass(JSGlobalObject*, JSValue); JS_EXPORT_PRIVATE JSValue pop(JSGlobalObject*); JSValue fastShift(VM&); @@ -193,6 +194,8 @@ class JSArray : public JSNonFinalObject { private: bool isLengthWritable() { + if (structure()->didFreeze()) [[unlikely]] + return false; ArrayStorage* storage = arrayStorageOrNull(); if (!storage) return true; diff --git a/Source/JavaScriptCore/runtime/JSArrayInlines.h b/Source/JavaScriptCore/runtime/JSArrayInlines.h index 13f6968cf1176..5d9b8d372c605 100644 --- a/Source/JavaScriptCore/runtime/JSArrayInlines.h +++ b/Source/JavaScriptCore/runtime/JSArrayInlines.h @@ -242,6 +242,11 @@ ALWAYS_INLINE void JSArray::pushInline(JSGlobalObject* globalObject, JSValue val switch (indexingMode()) { case ArrayClass: { + if (!isStructureExtensible()) [[unlikely]] { + scope.release(); + pushToNonExtensibleArrayClass(globalObject, value); + return; + } createInitialUndecided(vm, 0); [[fallthrough]]; } diff --git a/Source/JavaScriptCore/runtime/JSObject.cpp b/Source/JavaScriptCore/runtime/JSObject.cpp index 22789c745f51d..4667975c103da 100644 --- a/Source/JavaScriptCore/runtime/JSObject.cpp +++ b/Source/JavaScriptCore/runtime/JSObject.cpp @@ -2892,7 +2892,8 @@ void JSObject::freeze(VM& vm) if (isFrozen(vm)) return; materializeLazyOwnProperties(vm); - enterDictionaryIndexingMode(vm); + if (!(indexingMode() == ArrayClass && inherits())) + enterDictionaryIndexingMode(vm); { Structure* oldStructure = structure(); DeferredStructureTransitionWatchpointFire deferred(vm, oldStructure); @@ -3410,6 +3411,8 @@ bool JSObject::putByIndexBeyondVectorLength(JSGlobalObject* globalObject, unsign switch (indexingType()) { case ALL_BLANK_INDEXING_TYPES: { if (indexingShouldBeSparse()) { + if (indexingMode() == ArrayClass && !isStructureExtensible() && !needsSlowPutIndexing()) [[unlikely]] + return typeError(globalObject, scope, shouldThrow, ReadonlyPropertyWriteError); auto* arrayStorage = ensureArrayStorageExistsAndEnterDictionaryIndexingMode(vm); if (!hasSlowPutArrayStorage(indexingType())) [[likely]] RELEASE_AND_RETURN(scope, putByIndexBeyondVectorLengthWithArrayStorage(globalObject, i, value, shouldThrow, arrayStorage)); @@ -4059,7 +4062,15 @@ bool validateAndApplyPropertyDescriptor(JSGlobalObject* globalObject, JSObject* ASSERT(attributes & PropertyAttribute::Accessor); JSObject* getter = descriptor.getterPresent() ? descriptor.getterObject() : (current.getterPresent() ? current.getterObject() : nullptr); JSObject* setter = descriptor.setterPresent() ? descriptor.setterObject() : (current.setterPresent() ? current.setterObject() : nullptr); - GetterSetter* getterSetter = GetterSetter::create(vm, globalObject, getter, setter); + GetterSetter* getterSetter = nullptr; + if (JSValue existing = object->getDirect(vm, propertyName); existing && existing.isGetterSetter()) { + auto* existingGetterSetter = uncheckedDowncast(existing.asCell()); + if ((getter ? existingGetterSetter->getter() == getter : existingGetterSetter->isGetterNull()) + && (setter ? existingGetterSetter->setter() == setter : existingGetterSetter->isSetterNull())) + getterSetter = existingGetterSetter; + } + if (!getterSetter) + getterSetter = GetterSetter::create(vm, globalObject, getter, setter); object->putDirectAccessor(globalObject, propertyName, getterSetter, attributes & ~PropertyAttribute::ReadOnly); } else { ASSERT(descriptor.isGenericDescriptor() || descriptor.isDataDescriptor()); diff --git a/Source/JavaScriptCore/runtime/JSObject.h b/Source/JavaScriptCore/runtime/JSObject.h index eceb3f4442226..13a243edf0bd0 100644 --- a/Source/JavaScriptCore/runtime/JSObject.h +++ b/Source/JavaScriptCore/runtime/JSObject.h @@ -651,6 +651,10 @@ class JSObject : public JSCell { bool canPerformFastPutInline(VM&, PropertyName); bool canPerformFastPutInlineExcludingProto(); + enum class FastPutInlineAvailability : uint8_t { Unavailable, Available, AvailableIfPrototypesDoNotDefineProperties }; + FastPutInlineAvailability fastPutInlineAvailabilityExcludingProto(); + bool prototypeChainHasReadOnlyOrAccessorProperty(VM&, PropertyName); + bool mayBePrototype() const; void didBecomePrototype(VM&); diff --git a/Source/JavaScriptCore/runtime/JSObjectInlines.h b/Source/JavaScriptCore/runtime/JSObjectInlines.h index 029a0ed0e8df5..7ad0ceb2b58e6 100644 --- a/Source/JavaScriptCore/runtime/JSObjectInlines.h +++ b/Source/JavaScriptCore/runtime/JSObjectInlines.h @@ -223,6 +223,46 @@ ALWAYS_INLINE bool JSObject::canPerformFastPutInline(VM& vm, PropertyName proper return canPerformFastPutInlineExcludingProto(); } +ALWAYS_INLINE JSObject::FastPutInlineAvailability JSObject::fastPutInlineAvailabilityExcludingProto() +{ + auto result = FastPutInlineAvailability::Available; + JSObject* obj = this; + while (true) { + Structure* structure = obj->structure(); + if (structure->typeInfo().overridesGetPrototype()) + return FastPutInlineAvailability::Unavailable; + if (obj != this && structure->typeInfo().overridesPut()) + return FastPutInlineAvailability::Unavailable; + if (structure->hasReadOnlyOrGetterSetterPropertiesExcludingProto()) { + if (obj == this) + return FastPutInlineAvailability::Unavailable; + result = FastPutInlineAvailability::AvailableIfPrototypesDoNotDefineProperties; + } + + JSValue prototype = obj->getPrototypeDirect(); + if (prototype.isNull()) + return result; + + obj = asObject(prototype); + } +} + +inline bool JSObject::prototypeChainHasReadOnlyOrAccessorProperty(VM& vm, PropertyName propertyName) +{ + for (JSValue prototype = getPrototypeDirect(); !prototype.isNull(); prototype = asObject(prototype)->getPrototypeDirect()) { + JSObject* object = asObject(prototype); + Structure* structure = object->structure(); + if (!structure->hasReadOnlyOrGetterSetterPropertiesExcludingProto()) + continue; + if (object->hasNonReifiedStaticProperties()) + return true; + unsigned attributes; + if (isValidOffset(structure->get(vm, propertyName, attributes)) && (attributes & PropertyAttribute::ReadOnlyOrAccessorOrCustomAccessorOrValue)) + return true; + } + return false; +} + template ALWAYS_INLINE typename std::invoke_result::type JSObject::getPropertySlot(JSGlobalObject* globalObject, PropertyName propertyName, CallbackWhenNoException callback) const { @@ -532,8 +572,10 @@ ALWAYS_INLINE ASCIILiteral JSObject::putDirectInternal(VM& vm, PropertyName prop return ReadonlyPropertyChangeError; } + bool shouldFireReplacement = structure->isWatchingReplacement() && getDirect(offset) != value; putDirectOffset(vm, offset, value); - structure->didReplaceProperty(offset); + if (shouldFireReplacement) [[unlikely]] + structure->didReplaceProperty(offset); // FIXME: Check attributes against PropertyAttribute::CustomAccessorOrValue. Changing GetterSetter should work w/o transition. // https://bugs.webkit.org/show_bug.cgi?id=214342 @@ -591,7 +633,8 @@ ALWAYS_INLINE ASCIILiteral JSObject::putDirectInternal(VM& vm, PropertyName prop if (mode == PutModePut && (currentAttributes & PropertyAttribute::ReadOnlyOrAccessorOrCustomAccessor)) return ReadonlyPropertyChangeError; - structure->didReplaceProperty(offset); + if (structure->isWatchingReplacement() && getDirect(offset) != value) [[unlikely]] + structure->didReplaceProperty(offset); putDirectOffset(vm, offset, value); // FIXME: Check attributes against PropertyAttribute::CustomAccessorOrValue. Changing GetterSetter should work w/o transition. diff --git a/Source/JavaScriptCore/runtime/ObjectConstructor.cpp b/Source/JavaScriptCore/runtime/ObjectConstructor.cpp index fee4fe0d7f52b..0dd7b6585beb6 100644 --- a/Source/JavaScriptCore/runtime/ObjectConstructor.cpp +++ b/Source/JavaScriptCore/runtime/ObjectConstructor.cpp @@ -325,12 +325,14 @@ JSC_DEFINE_HOST_FUNCTION(objectConstructorAssign, (JSGlobalObject* globalObject, // FIXME: Extend this for non JSFinalObject. For example, we would like to use this fast path for function objects too. // https://bugs.webkit.org/show_bug.cgi?id=185358 JSFinalObject* targetObject = dynamicDowncast(target); - bool targetCanPerformFastPut = targetObject && targetObject->canPerformFastPutInlineExcludingProto() && targetObject->isStructureExtensible(); + auto fastPutAvailability = targetObject ? targetObject->fastPutInlineAvailabilityExcludingProto() : JSObject::FastPutInlineAvailability::Unavailable; + bool targetCanPerformFastPut = fastPutAvailability != JSObject::FastPutInlineAvailability::Unavailable && targetObject->isStructureExtensible(); + bool mustCheckPrototypeProperties = fastPutAvailability == JSObject::FastPutInlineAvailability::AvailableIfPrototypesDoNotDefineProperties; unsigned argsCount = callFrame->argumentCount(); // argsCount == 2 case does not need to use arguments' batching. // We limit argsCount < 5 not to increase properties / values vector super large. - if (argsCount > 2 && argsCount < 5 && targetCanPerformFastPut) { + if (argsCount > 2 && argsCount < 5 && targetCanPerformFastPut && !mustCheckPrototypeProperties) { bool willBatch = true; for (unsigned i = 1; i < argsCount; ++i) { JSValue sourceValue = callFrame->uncheckedArgument(i); @@ -394,7 +396,7 @@ JSC_DEFINE_HOST_FUNCTION(objectConstructorAssign, (JSGlobalObject* globalObject, RETURN_IF_EXCEPTION(scope, { }); } - bool objectAssignFastSucceeded = objectAssignFast(globalObject, targetObject, source, properties, values); + bool objectAssignFastSucceeded = objectAssignFast(globalObject, targetObject, source, properties, values, mustCheckPrototypeProperties); RETURN_IF_EXCEPTION(scope, { }); if (objectAssignFastSucceeded) continue; @@ -1193,6 +1195,11 @@ JSObject* objectConstructorFreeze(JSGlobalObject* globalObject, JSObject* object return object; } + if (object->indexingMode() == ArrayClass && object->inherits() && !object->hasNonReifiedStaticProperties()) { + object->freeze(vm); + return object; + } + bool success = setIntegrityLevel(globalObject, vm, object); RETURN_IF_EXCEPTION(scope, nullptr); if (!success) [[unlikely]] { diff --git a/Source/JavaScriptCore/runtime/ObjectConstructorInlines.h b/Source/JavaScriptCore/runtime/ObjectConstructorInlines.h index 8e453137a12e9..d6b9e354afafa 100644 --- a/Source/JavaScriptCore/runtime/ObjectConstructorInlines.h +++ b/Source/JavaScriptCore/runtime/ObjectConstructorInlines.h @@ -281,7 +281,7 @@ ALWAYS_INLINE JSObject* tryCreateObjectViaCloning(VM& vm, JSGlobalObject* global return JSFinalObject::createWithButterflyCopyingInlineStorage(vm, sourceStructure, newButterfly, source->inlineStorage()); } -ALWAYS_INLINE bool objectAssignFast(JSGlobalObject* globalObject, JSFinalObject* target, JSObject* source, Vector& properties, MarkedArgumentBuffer& values) +ALWAYS_INLINE bool objectAssignFast(JSGlobalObject* globalObject, JSFinalObject* target, JSObject* source, Vector& properties, MarkedArgumentBuffer& values, bool mustCheckPrototypeProperties) { // |source| Structure does not have any getters. And target can perform fast put. // So enumerating properties and putting properties are non observable. @@ -309,6 +309,26 @@ ALWAYS_INLINE bool objectAssignFast(JSGlobalObject* globalObject, JSFinalObject* if (!sourceStructure->canPerformFastPropertyEnumerationCommon()) return false; + if (mustCheckPrototypeProperties) { + if (source->canHaveExistingOwnIndexedProperties()) + return false; + bool prototypeDefinesProperty = false; + sourceStructure->forEachProperty(vm, [&](const PropertyTableEntry& entry) -> bool { + if (entry.attributes() & PropertyAttribute::DontEnum) + return true; + PropertyName propertyName(entry.key()); + if (propertyName.isPrivateName()) + return true; + if (target->prototypeChainHasReadOnlyOrAccessorProperty(vm, propertyName)) { + prototypeDefinesProperty = true; + return false; + } + return true; + }); + if (prototypeDefinesProperty) + return false; + } + if (objectCloneFast(vm, target, source)) return true; diff --git a/Source/JavaScriptCore/runtime/Structure.cpp b/Source/JavaScriptCore/runtime/Structure.cpp index ab4a085c25dd9..ae1eb7ab05328 100644 --- a/Source/JavaScriptCore/runtime/Structure.cpp +++ b/Source/JavaScriptCore/runtime/Structure.cpp @@ -261,6 +261,7 @@ Structure::Structure(VM& vm, JSGlobalObject* globalObject, JSValue prototype, co setTransitionKind(TransitionKind::Unknown); setMayBePrototype(false); setDidPreventExtensions(typeInfo.overridesIsExtensible()); + setDidFreeze(false); setDidTransition(false); setStaticPropertiesReified(false); setTransitionWatchpointIsLikelyToBeFired(false); @@ -309,6 +310,7 @@ Structure::Structure(VM& vm, CreatingEarlyCellTag) setTransitionKind(TransitionKind::Unknown); setMayBePrototype(false); setDidPreventExtensions(typeInfo.overridesIsExtensible()); + setDidFreeze(false); setDidTransition(false); setStaticPropertiesReified(false); setTransitionWatchpointIsLikelyToBeFired(false); @@ -353,6 +355,7 @@ Structure::Structure(VM& vm, StructureVariant variant, Structure* previous) setTransitionKind(TransitionKind::Unknown); setMayBePrototype(previous->mayBePrototype()); setDidPreventExtensions(previous->didPreventExtensions()); + setDidFreeze(previous->didFreeze()); setDidTransition(true); setStaticPropertiesReified(previous->staticPropertiesReified()); setHasBeenDictionary(previous->hasBeenDictionary()); @@ -952,6 +955,9 @@ Structure* Structure::nonPropertyTransitionSlow(VM& vm, Structure* structure, Tr if (preventsExtensions(transitionKind)) transition->setDidPreventExtensions(true); + if (transitionKind == TransitionKind::Freeze) + transition->setDidFreeze(true); + if (transitionKind == TransitionKind::BecomePrototype) transition->setMayBePrototype(true); diff --git a/Source/JavaScriptCore/runtime/Structure.h b/Source/JavaScriptCore/runtime/Structure.h index 7a8d27911fd98..b6b9a8a463642 100644 --- a/Source/JavaScriptCore/runtime/Structure.h +++ b/Source/JavaScriptCore/runtime/Structure.h @@ -836,6 +836,7 @@ class Structure : public JSCell { DEFINE_BITFIELD(bool, hasNonEnumerableProperties, HasNonEnumerableProperties, 1, 6); DEFINE_BITFIELD(bool, hasSpecialProperties, HasSpecialProperties, 1, 7); DEFINE_BITFIELD(DefinitelyNonThenableState, definitelyNonThenableState, DefinitelyNonThenableState, 2, 8); // This flag can be flipped on the main thread at any timing. + DEFINE_BITFIELD(bool, didFreeze, DidFreeze, 1, 10); DEFINE_BITFIELD(TransitionKind, transitionKind, TransitionKind, 5, 13); DEFINE_BITFIELD(bool, isWatchingReplacement, IsWatchingReplacement, 1, 18); // This flag can be fliped on the main thread at any timing. DEFINE_BITFIELD(bool, mayBePrototype, MayBePrototype, 1, 19);