Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
function assert(cond, msg) {
if (!cond)
throw new Error("FAIL: " + msg);
}

function warm(f, n = 1e4) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 nit (optional): New stress tests hard-code iteration counts (1e4/1e3) instead of using testLoopCount, which JSTests/README.md (imported by JSTests/CLAUDE.md) lists as a required rule so tests tier up only in configurations where it matters and stay under 200ms elsewhere. sweep:\b1e[34]\b in the three added JSTests/stress files. Fix: drive warm-up loops with testLoopCount (e.g. function warm(f, n = testLoopCount) here; for (let i = 0; i < testLoopCount; i++) in freeze-array-prototype.js:61 and object-assign-frozen-object-prototype.js:21/26/32/42/51). [also at: JSTests/stress/object-assign-frozen-object-prototype.js:20 - nit: new stress tests hard-code 1e4/1e3 iteration counts instead of testLoopCount; JSTests/README.md (imported by…]

Extended reasoning...

JSTests/CLAUDE.md imports JSTests/README.md, whose rule 2 states new tests are required to use testLoopCount/wasmTestLoopCount so the harness can scale iterations per configuration (eager tier-up vs. no-JIT vs. GC-heavy). All three new tests instead hard-code 1e4 (and 1e3) loops: define-property-same-value-keeps-adaptive-watchpoints.js:6/75, freeze-array-prototype.js:27/61/67, object-assign-frozen-object-prototype.js:21/26/32/42/51. In no-JIT or GC-stress configurations these loops run at fixed cost with no tier-up benefit, risking the 200ms budget, and in eager configurations they may over-iterate. Base branch has no such files; the diff introduces the violation.

Verification: nit — JSTests/README.md:20 (imported by JSTests/CLAUDE.md:1 via @ README.md) states as a hard rule for new tests: "Use testLoopCount or wasmTestLoopCount to control how many iterations a test runs. The jsc CLI sets these based on the configuration of the test, so tests iterate enough to tier up where that matters and exit early where it doesn't." All three new tests hard-code counts…

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");
}
85 changes: 85 additions & 0 deletions JSTests/stress/freeze-array-prototype.js
Original file line number Diff line number Diff line change
@@ -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));
63 changes: 63 additions & 0 deletions JSTests/stress/object-assign-frozen-object-prototype.js
Original file line number Diff line number Diff line change
@@ -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");
}
12 changes: 8 additions & 4 deletions Source/JavaScriptCore/dfg/DFGOperations.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<JSFinalObject>(target); targetObject && targetObject->canPerformFastPutInlineExcludingProto() && targetObject->isStructureExtensible()) {
auto* targetObject = dynamicDowncast<JSFinalObject>(target);
auto fastPutAvailability = targetObject ? targetObject->fastPutInlineAvailabilityExcludingProto() : JSObject::FastPutInlineAvailability::Unavailable;
if (fastPutAvailability != JSObject::FastPutInlineAvailability::Unavailable && targetObject->isStructureExtensible()) {
Vector<UniquedStringImpl*, 8> properties;
MarkedArgumentBuffer values;
if (!source->staticPropertiesReified()) {
Expand All @@ -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);
Expand All @@ -360,15 +362,17 @@ JSC_DEFINE_JIT_OPERATION(operationObjectAssignUntyped, void, (JSGlobalObject* gl
JSObject* source = sourceValue.toObject(globalObject);
OPERATION_RETURN_IF_EXCEPTION(scope);

if (auto* targetObject = dynamicDowncast<JSFinalObject>(target); targetObject && targetObject->canPerformFastPutInlineExcludingProto() && targetObject->isStructureExtensible()) {
auto* targetObject = dynamicDowncast<JSFinalObject>(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);
}

Vector<UniquedStringImpl*, 8> 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);
Expand Down
14 changes: 14 additions & 0 deletions Source/JavaScriptCore/runtime/JSArray.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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();
Expand All @@ -1330,6 +1342,8 @@ JSValue JSArray::pop(JSGlobalObject* globalObject)

switch (indexingType()) {
case ArrayClass:
if (!isLengthWritable()) [[unlikely]]
throwTypeError(globalObject, scope, ReadonlyPropertyWriteError);
return jsUndefined();

case ArrayWithUndecided:
Expand Down
3 changes: 3 additions & 0 deletions Source/JavaScriptCore/runtime/JSArray.h
Original file line number Diff line number Diff line change
Expand Up @@ -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&);

Expand Down Expand Up @@ -193,6 +194,8 @@ class JSArray : public JSNonFinalObject {
private:
bool isLengthWritable()
{
if (structure()->didFreeze()) [[unlikely]]
return false;
ArrayStorage* storage = arrayStorageOrNull();
if (!storage)
return true;
Expand Down
5 changes: 5 additions & 0 deletions Source/JavaScriptCore/runtime/JSArrayInlines.h
Original file line number Diff line number Diff line change
Expand Up @@ -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]];
}
Expand Down
15 changes: 13 additions & 2 deletions Source/JavaScriptCore/runtime/JSObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2892,7 +2892,8 @@ void JSObject::freeze(VM& vm)
if (isFrozen(vm))
return;
materializeLazyOwnProperties(vm);
enterDictionaryIndexingMode(vm);
if (!(indexingMode() == ArrayClass && inherits<JSArray>()))
enterDictionaryIndexingMode(vm);
{
Structure* oldStructure = structure();
DeferredStructureTransitionWatchpointFire deferred(vm, oldStructure);
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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<GetterSetter>(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());
Expand Down
4 changes: 4 additions & 0 deletions Source/JavaScriptCore/runtime/JSObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -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&);

Expand Down
Loading
Loading