Skip to content

Keep the elements of a frozen, sealed or non-extensible array in the ArrayStorage vector - #752

Open
robobun wants to merge 2 commits into
mainfrom
robobun/cb1239ce/frozen-array-vector
Open

robobun wants to merge 2 commits into
mainfrom
robobun/cb1239ce/frozen-array-vector

Conversation

@robobun

@robobun robobun commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Related to oven-sh/bun#44305 and oven-sh/bun#6360. Supersedes #337 (its gate widening and bulk attribute pass are part of this commit). Carries the ArrayClass handling and freeze-array-prototype.js from #622; arrayLengthIsReadOnly here is the generalisation of that PR's didFreeze bit (it is also set by Object.defineProperty(a, "length", { writable: false })), so the two can merge in either order with a small conflict on Structure bit 10.

Problem

  • Object.freeze on 300k arrays of 16 ints takes about 1 s in bun and 25 to 50 ms in node. Reads from the frozen arrays are 9 to 13x slower than from plain arrays (node: about 4x). The heap for those arrays grows from 53 MB to 172 MB.
  • JSObject::enterDictionaryIndexingMode (runtime/JSObject.cpp) moves every element into the SparseArrayValueMap (sparse mode, vector length 0). Every producer goes through it: preventExtensions, seal, freeze and JSArray::setLengthWritable. After that each a[i] read misses the JIT vector paths and does a hash lookup in C++.

Fix

  • enterDictionaryIndexingMode keeps the elements in the ArrayStorage vector and only switches the object to the SlowPutArrayStorage shape. LLInt and baseline never store into such a vector inline, and DFG / FTL now test the structure before they do. So every write reaches C++.
  • Three new Structure bits record the attributes of the vector elements and the length: vectorElementsAreNonConfigurable (Seal), vectorElementsAreReadOnly (Freeze), arrayLengthIsReadOnly (Freeze and the new SetArrayLengthReadOnly transition). They replace the map's LengthIsReadOnly flag. The C++ vector writers and readers consult them (putByIndex, trySetIndexQuickly, putDirectIndex, deletePropertyByIndex, defineOwnIndexedProperty, getOwnPropertySlotByIndex, the prototype-chain put intercept, JSArray::setLength / pop / push, Array.prototype.reverse).
  • A non-extensible object has either no sparse map or one in sparse mode. Elements move into the map when one index needs attributes of its own (Object.defineProperty on an element) or when the object already owned a map; they carry the structure's attributes with them, so the existing map code validates the descriptor.
  • An array with no elements (ArrayClass, such as Array.prototype) stays blank, so freezing Array.prototype keeps the array prototype chain watchpoint. A frozen object notes its read-only elements for the objects that inherit from it (notifyPresenceOfIndexedAccessors) when it becomes a prototype, not when it is frozen, so a frozen array that is no prototype keeps the builtin fast paths (slice, indexOf, spread).
  • Verified: JSTests/stress/frozen-array-vector-resident.js (new, all tiers, lockdown, eager), frozen-array-allocation-profile.js (new), object-seal-freeze-array-fast-path.js (from Object.seal / Object.freeze: take the JSObject fast path for JSArray and JSFinalObject with indexed properties #337), freeze-array-prototype.js (from [JSC] Freezing built-in prototypes should not permanently disable fast paths #622), 6363 filtered stress runs, test262 Object/{freeze,seal,preventExtensions,isFrozen,isSealed,defineProperty,...}, Array/length, Array.prototype mutators, Reflect, Proxy (5514 pass, 3 pre-existing failures listed in expectations.yaml). The CI run of the first revision had one failure in the full suite (the allocation profile case, fixed by frozen-array-allocation-profile.js).

Background

  • ArrayStorage is the indexed storage with a vector plus an optional SparseArrayValueMap for attributed or far-away indices. "Sparse mode" means the vector is empty and the map holds every element.
  • SlowPutArrayStorage is the shape JSC already uses when an indexed write cannot be done inline (indexed accessors on the prototype chain, "having a bad time"). Reads from its vector are inlined by every tier; the JITs never store into it without a C++ call, except the DFG / FTL in-bounds store, which now checks vectorElementsAreReadOnly.
  • A non-extensible object never has the plain ArrayStorage shape (suggestedArrayStorageTransition), so the plain ArrayStorage paths are unchanged.
  • Designs set aside: Object.seal / Object.freeze: take the JSObject fast path for JSArray and JSFinalObject with indexed properties #337 alone (the freeze loop gets cheaper, reads stay a hash lookup), folding the two conversions into one (same), a frozen copy-on-write butterfly (covers Object.freeze only, 22 convertFromCopyOnWrite callers to audit), and a new indexing shape (spends the last free shape and two ArrayModes bits, touches 13 JIT files).

Results (x86_64 release jsc, oven-sh/bun#44305 repro)

  • freeze of 300k x 16-int arrays: 970 ms to 1072 ms -> 34 ms to 47 ms.
  • reads from the frozen arrays (JIT-inlined a[i]): 9.5x to 9.8x plain -> 0.9x to 1.2x plain. Builtins that read the array (slice, indexOf, join, spread) take their generic path for the SlowPutArrayStorage shape, as they do today for the sparse one.
  • heap for 300k frozen 16-int arrays: 171.7 MB -> 52.9 MB (plain 52.7 MB). 300k SparseArrayValueMap cells -> 0.
  • jsc text size: +14,216 bytes (size, 45,907,907 -> 45,922,123).

Downsides

  • A DFG / FTL inlined store into a having-a-bad-time array (SlowPutArrayStorage, writable) pays one structure load and one branch more per store. Plain Int32 / Double / Contiguous / ArrayStorage stores are unchanged.
  • Object.defineProperty on one element of a sealed or frozen array still moves the whole array into the sparse map, as before. A sealed array that is then written through a[i] = v stays in the vector.
  • Object.freeze of an array with elements that is already a prototype still calls haveABadTime, as defining a read-only index on it always did. Freezing it before it becomes a prototype no longer does.

Fork-owned bits

  • Structure bitfield offsets 10, 11, 12 and TransitionKind::SetArrayLengthReadOnly = 18 are taken by this change; upstream has them free at this base.

Follow-ups noted

  • Tagged-template strings / raw arrays are built with per-index putDirectIndex(ReadOnly | DontDelete) before objectConstructorFreeze runs, so they are still sparse.
  • Object.freeze on class X extends Array instances and on Proxies still runs the generic SetIntegrityLevel loop (the target array itself is vector-resident once preventExtensions ran).

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Preview build of 98c438b: autobuild-preview-pr-752-98c438b4

@robobun
robobun force-pushed the robobun/cb1239ce/frozen-array-vector branch 2 times, most recently from e31304e to 172bb08 Compare October 1, 2026 07:33
@robobun

robobun commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator Author

Design summary for a maintainer decision, since this overlaps #622 and replaces #337.

The representation: a frozen, sealed or non-extensible array keeps its elements in the ArrayStorage vector and takes the SlowPutArrayStorage shape. Three Structure bits describe what the vector may not do: vectorElementsAreNonConfigurable (Seal), vectorElementsAreReadOnly (Freeze) and arrayLengthIsReadOnly (Freeze, and a new SetArrayLengthReadOnly transition for defineProperty(a, "length", { writable: false })). The JITs already read SlowPutArrayStorage vectors inline and never store into them without a C++ call, except the DFG / FTL in-bounds store, which now tests vectorElementsAreReadOnly. The sparse map is used only when one index needs attributes of its own. On the oven-sh/bun#44305 repro this takes Object.freeze of 300k small arrays from about 1 s to 35 ms and the reads from them from 9.5x plain to about 1x; 300k frozen arrays take 53 MB instead of 172 MB.

Overlap with #622: arrayLengthIsReadOnly does what didFreeze does there and more, and the ArrayClass handling (Array.prototype stays blank when frozen, its freeze-array-prototype.js) is carried over here with credit. The named-property and Object.assign parts of #622 are untouched. If #622 lands first, this rebases with a small conflict on Structure bit 10; if this lands first, #622 can drop its didFreeze hunks.

#337 is superseded: its gate widening and bulk seal / freeze of map entries are part of this commit, and its sparse-map LengthIsReadOnly flag is replaced by the Structure bit.

…ArrayStorage vector

Object.preventExtensions, Object.seal, Object.freeze and a read-only
"length" all went through JSObject::enterDictionaryIndexingMode, which
moved every element of the object into the SparseArrayValueMap (sparse
mode, vector length 0). After that every a[i] read missed the JIT vector
fast paths and did a hash lookup in C++, and the freeze itself cost two
butterfly conversions, a map cell and one hash insert per element.

Now enterDictionaryIndexingMode keeps the elements in the vector and only
switches the object to the SlowPutArrayStorage shape. LLInt and the
baseline JIT never store into a SlowPutArrayStorage vector inline, and
DFG / FTL now check the structure before they do, so every write reaches
the C++ paths. The attributes of the elements live on the Structure:
vectorElementsAreNonConfigurable (Seal), vectorElementsAreReadOnly
(Freeze) and arrayLengthIsReadOnly (Freeze and the new
SetArrayLengthReadOnly transition, which replaces the LengthIsReadOnly
flag of the sparse map). Entries of the sparse map keep their own
attributes; elements only move into the map when a single index needs
attributes of its own (Object.defineProperty on an element), and they
carry the structure's attributes with them.

A non-extensible object therefore never has the plain ArrayStorage shape
(suggestedArrayStorageTransition), which is what lets the plain
ArrayStorage paths stay as they were. The C++ vector writers and readers
consult the bits: putByIndex, trySetIndexQuickly, putDirectIndex and the
beyond-vector-length paths, deletePropertyByIndex, defineOwnIndexedProperty,
getOwnPropertySlotByIndex, the prototype-chain put intercept,
JSArray::setLength / pop / push and Array.prototype.reverse.

An array with no elements (ArrayClass, such as Array.prototype) stays
blank: the bits and isStructureExtensible() cover the later writes, so
freezing Array.prototype keeps the array prototype chain watchpoint. A
frozen object tells the objects that inherit from it about its read-only
elements (notifyPresenceOfIndexedAccessors) when it becomes a prototype,
not when it is frozen, so a frozen array that is no prototype keeps the
builtin fast paths.

The DFG folds a read of a frozen element from the vector as it folded one
from the sparse map; JSObject::freeze fences the element stores before the
structure store. ArrayAllocationProfile never records the
SlowPutArrayStorage shape, so one frozen array does not make the arrays
its allocation site creates next SlowPutArrayStorage. The baseline indexed
load and "in" ICs accept the SlowPutArrayStorage shape, whose vector they
already read. Object.isSealed / isFrozen answer from the structure for an
object that JSObject::seal / freeze sealed or froze.

Object.seal / Object.freeze take the JSObject::seal / freeze path for an
exact JSArray and for a JSFinalObject with indexed properties instead of
the generic per-index SetIntegrityLevel loop (from #337,
which this supersedes); JSArray has no lazily reified own properties, so
materializeLazyOwnProperties skips it.

On the oven-sh/bun#44305 repro (300k arrays of 16 ints, x86_64 release
jsc): freeze 970 ms -> 35-47 ms, reads from the frozen arrays 9.5x plain
-> 1.0-1.2x plain.

Supersedes #337. The ArrayClass handling and
freeze-array-prototype.js come from #622.

Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
@robobun
robobun force-pushed the robobun/cb1239ce/frozen-array-vector branch from 02ba7aa to 2d9ab4f Compare October 1, 2026 10:00
…orage store, as clobberize does

The in-bounds store takes a slow path that throws when the object is frozen; CFA asserts that the two agree.
@coderabbitai

coderabbitai Bot commented Oct 1, 2026

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

🧰 Additional context used
📚 Code guidelines (2)
Source/JavaScriptCore/CLAUDE.md — auto-discovered
CLAUDE.md — auto-discovered

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 7f2c4a4b-b638-4453-91a5-bd4389f6760b

📥 Commits

Reviewing files that changed from the base of the PR and between fb1167e and 98c438b.

📒 Files selected for processing (28)
  • JSTests/microbenchmarks/frozen-array-read.js
  • JSTests/microbenchmarks/object-seal-freeze-large-array.js
  • JSTests/stress/freeze-array-prototype.js
  • JSTests/stress/frozen-array-allocation-profile.js
  • JSTests/stress/frozen-array-vector-resident.js
  • JSTests/stress/object-integrity-blank-indexing.js
  • JSTests/stress/object-seal-freeze-array-fast-path.js
  • Source/JavaScriptCore/bytecode/ArrayAllocationProfile.cpp
  • Source/JavaScriptCore/bytecode/Repatch.cpp
  • Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h
  • Source/JavaScriptCore/dfg/DFGClobberize.h
  • Source/JavaScriptCore/dfg/DFGSpeculativeJIT.h
  • Source/JavaScriptCore/dfg/DFGSpeculativeJIT64.cpp
  • Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
  • Source/JavaScriptCore/runtime/ArrayPrototype.cpp
  • Source/JavaScriptCore/runtime/JSArray.cpp
  • Source/JavaScriptCore/runtime/JSArray.h
  • Source/JavaScriptCore/runtime/JSArrayInlines.h
  • Source/JavaScriptCore/runtime/JSObject.cpp
  • Source/JavaScriptCore/runtime/JSObject.h
  • Source/JavaScriptCore/runtime/JSObjectInlines.h
  • Source/JavaScriptCore/runtime/ObjectConstructor.cpp
  • Source/JavaScriptCore/runtime/SparseArrayValueMap.cpp
  • Source/JavaScriptCore/runtime/SparseArrayValueMap.h
  • Source/JavaScriptCore/runtime/Structure.cpp
  • Source/JavaScriptCore/runtime/Structure.h
  • Source/JavaScriptCore/runtime/StructureCreateInlines.h
  • Source/JavaScriptCore/runtime/StructureTransitionTable.h

Included review availability: This review used your included allowance. Your plan provides up to 5 included reviews per hour; 1 remain after this review.


Walkthrough

JavaScriptCore now records indexed-vector attributes and read-only array length in structure state. Runtime array and object operations, bytecode caches, and optimized JIT paths use that state. New stress tests and benchmarks cover frozen, sealed, and non-extensible arrays.

Changes

Array integrity and indexed storage

Layer / File(s) Summary
Structure and sparse-map integrity state
Source/JavaScriptCore/runtime/Structure.h, Source/JavaScriptCore/runtime/Structure.cpp, Source/JavaScriptCore/runtime/StructureTransitionTable.h, Source/JavaScriptCore/runtime/SparseArrayValueMap.h, Source/JavaScriptCore/runtime/SparseArrayValueMap.cpp
Structures track vector-element configurability and writability, plus read-only array length. Sparse-map entries gain seal and freeze operations.
Indexed-property storage and access
Source/JavaScriptCore/runtime/JSObject.h, Source/JavaScriptCore/runtime/JSObject.cpp, Source/JavaScriptCore/runtime/JSObjectInlines.h
Indexed access and dictionary-mode transitions apply structure-recorded attributes and extensibility rules.
Array operations and integrity-level APIs
Source/JavaScriptCore/runtime/JSArray.h, Source/JavaScriptCore/runtime/JSArray.cpp, Source/JavaScriptCore/runtime/JSArrayInlines.h, Source/JavaScriptCore/runtime/ArrayPrototype.cpp, Source/JavaScriptCore/runtime/JSObject.cpp, Source/JavaScriptCore/runtime/JSObject.h, Source/JavaScriptCore/runtime/StructureCreateInlines.h, Source/JavaScriptCore/runtime/ObjectConstructor.cpp
Array length and mutation paths enforce read-only length and vector attributes. Seal, freeze, and integrity queries use structure state and update sparse-map entries.
Allocation profiling and optimized array access
Source/JavaScriptCore/bytecode/ArrayAllocationProfile.cpp, Source/JavaScriptCore/bytecode/Repatch.cpp, Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h, Source/JavaScriptCore/dfg/DFGClobberize.h, Source/JavaScriptCore/dfg/DFGSpeculativeJIT.h, Source/JavaScriptCore/dfg/DFGSpeculativeJIT64.cpp, Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
Allocation profiles normalize slow-put array shapes. Bytecode caches and optimized access paths account for slow-put storage and read-only vector elements.
Integrity and indexed-storage validation
JSTests/microbenchmarks/*, JSTests/stress/freeze-array-prototype.js, JSTests/stress/frozen-array-allocation-profile.js, JSTests/stress/frozen-array-vector-resident.js, JSTests/stress/object-integrity-blank-indexing.js, JSTests/stress/object-seal-freeze-array-fast-path.js
Tests and benchmarks cover integrity operations, indexed-property behavior, prototype interactions, and allocation profiles.

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to 98c43

This change keeps frozen, sealed and non-extensible array elements in the vector. No concrete merge-blocking defect was identified in the supplied context. Normal CI validation is still advisable.

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description gives a detailed problem statement, implementation summary, validation results, performance data, and limitations. However, it does not follow the required template because it lacks a … Add the associated Bugzilla URL, include a "Reviewed by NOBODY (OOPS!)." line or the applicable reviewer information, and provide the required explanation followed by a list of changed paths and relevant functions. Ensure the PR is also lin…
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the primary change: retaining elements of frozen, sealed, and non-extensible arrays in the ArrayStorage vector.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description gives a detailed problem statement, implementation summary, validation results, performance data, and limitations. However, it does not follow the required template because it lacks a Bugzilla URL, the required reviewer line, and the template-style changed-file and function list.

Resolution

Add the associated Bugzilla URL, include a "Reviewed by NOBODY (OOPS!)." line or the applicable reviewer information, and provide the required explanation followed by a list of changed paths and relevant functions. Ensure the PR is also linked from the associated Bugzilla issue as required by the repository template.

  • Fix all pre-merge checks with AI
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.

Warning

Git: CodeRabbit could not clone the repository, so clone-backed analysis was skipped and this review may be incomplete. Verify repository clone access, such as SSH credentials, before requesting another full review. If clone access is intentionally unavailable, use path_filters to narrow the review scope.


Comment @coderabbitai help to get the list of available commands.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Beyond the inline findings, two other spots were examined and ruled out: the compiler-thread read of a frozen vector in DFGAbstractInterpreterInlines.h (the structure flag is checked first, with a load-load fence paired against the store fence in JSObject::freeze, and the fold only applies when the structure already says read-only), and the new structure-answered Object.isFrozen / isSealed fast path in ObjectConstructor.cpp (it only short-circuits to true and still falls through to the generic TestIntegrityLevel loop otherwise).

Extended reasoning...

The change reworks how JSC stores elements of non-extensible, sealed and frozen objects (ArrayStorage vector instead of the sparse map) across the runtime, DFG, FTL and Repatch, with three new Structure bits and a new transition kind. It touches no injection, auth or data-exposure surface, but it changes JS-visible integrity-level semantics, and the hunt ended on the bug cap with several confirmed semantic regressions posted inline, so approval is not appropriate.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Comment on lines 1213 to +1218
case NonArray:
// No indexed properties to convert. Once the caller makes the structure
// non-extensible, indexingShouldBeSparse() lazily handles later indexed
// writes; staying blank also keeps for-in enumerator caching usable.
// JSArray code paths (e.g. setLengthWritable) assume this method
// allocated ArrayStorage, so do not skip for JSArray subclasses that
// use NonArray indexing (e.g. $vm RuntimeArray with DerivedArrayType).
if (!inherits<JSArray>()) [[likely]]
return;
[[fallthrough]];
case ArrayClass:
// No elements to protect. The structure bits and isStructureExtensible() cover the later
// writes (putByIndexBeyondVectorLength, JSArray::push / pop / setLength), and staying blank
// keeps the for-in enumerator cache and the array prototype chain watchpoint usable.
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 An empty array whose "length" was made non-writable now silently grows on a[0] = v or a.push(v), where the base throws a TypeError. JSObject.cpp:1218 leaves an ArrayClass array blank for JSArray::setLengthWritable, but the structure stays extensible, so indexingShouldBeSparse() at JSObject.cpp:3499 is false and the gate at JSObject.cpp:3503 that tests arrayLengthIsReadOnly() is skipped. The put reaches createInitialForValueAndSet at JSObject.cpp:3516, which allocates an Int32 butterfly with length 1; from then on no fast path (pushInline, setLength for Int32/Double/Contiguous) checks the read-only length. … [also at: Source/JavaScriptCore/runtime/JSObject.cpp:3516 - After merging, Array.prototype whose "length" was made non-writable can still gain elements and its length grows; the…; Source/JavaScriptCore/runtime/JSArray.cpp:1271 - After merging, Object.seal(Array.prototype) or Object.preventExtensions(Array.prototype) no longer stops new elements:…; +1 more]

Why this was flagged

…Fix: a blank object with arrayLengthIsReadOnly() must reject a put at or beyond length before any storage is allocated, on both putByIndexBeyondVectorLength (JSObject.cpp:3499) and putDirectIndexSlowOrBeyondVectorLength (JSObject.cpp:3676), e.g. test the bit outside the indexingShouldBeSparse() branch or give the array SlowPutArrayStorage in setLengthWritable as the…

Script: let a = Array.prototype; Object.defineProperty(a, "length", { writable: false }); a[0] = 1; (or a.push(1), strict or sloppy). JSArray::setLengthWritable (JSArray.cpp:203) calls enterDictionaryIndexingMode, which returns at JSObject.cpp:1218 for ArrayClass, then applies the SetArrayLengthReadOnly transition; the structure is still extensible.

Verification: normal (bounded: only Array.prototype, the sole ArrayClass object, is affected) — triggered when a script makes Array.prototype's "length" non-writable without also making the object non-extensible, then writes an index or pushes on it.

Mechanism verified, with the candidate's example corrected.

Comment on lines +45 to +48
// The read-only elements of a frozen object intercept the indexed puts of the objects that
// inherit from it. JSObject::freeze leaves this note to here so that a frozen array that is
// no prototype keeps its fast paths; nothing inherits from this object yet, so no bad time.
if (oldStructure->vectorElementsAreReadOnly() && hasIndexedElementsInArrayStorage()) [[unlikely]] {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Instances constructed with a frozen array as their direct prototype (Reflect.construct(Array, [], F) with F.prototype = Object.freeze([1,2,3])) can overwrite the frozen element: x[0] = 9 succeeds where the base branch threw TypeError. JSObject::freeze now defers notifyPresenceOfIndexedAccessors to didBecomePrototype, but StructureCache::emptyStructureForPrototypeFromBaseStructure (StructureCache.cpp:101) evaluates anyObjectInChainMayInterceptIndexedAccesses before createEmptyStructure calls didBecomePrototype, so the subclass structure keeps the plain Int32/Undecided shape. Fix: ensure the AddIndexedAccessors note is visible before any caller decides a child's indexing shape, or note read-only elements eagerly in freeze. [also at: Source/JavaScriptCore/runtime/JSObject.cpp:2958 - Arrays constructed with a frozen array as new.target.prototype can overwrite the frozen prototype's read-only elements in place, where the base rejects the write.; Source/JavaScriptCore/runtime/StructureCreateInlines.h:51 - An array constructed with a frozen array as its new.target prototype can overwrite the frozen prototype's read-only elements; the base throws a TypeError.]

Why this was flagged

Trigger: function F() {} F.prototype = Object.freeze([1, 2, 3]); let x = Reflect.construct(Array, [], F); "use strict"; x[0] = 9;. Object.freeze takes the new JSObject::freeze fast path; since the array is not yet a prototype, JSObject.cpp:2957 skips notifyPresenceOfIndexedAccessors, so its structure lacks MayHaveIndexedAccessors. StructureCache::emptyStructureForPrototypeFromBaseStructure tests prototype->anyObjectInChainMayInterceptIndexedAccesses() at StructureCache.cpp:101 before createEmptyStructure runs prototype->didBecomePrototype(vm), so the instance structure is created with the base ArrayWithUndecided shape. At runtime x[0] = 9 goes through JSObject::putByIndex and stores directly; trySetIndexQuickly and the contiguous paths never consult the prototype, so the write succeeds and x[0] reads 9. On the base branch, freezing the array ran defineOwnIndexedProperty with ReadOnly, which called notifyPresenceOfIndexedAccessors at freeze time, so the subclass structure was created with SlowPutArrayStorageShape and the put threw ReadonlyPropertyWriteError.

Verification: In JSObject::freeze (Source/JavaScriptCore/runtime/JSObject.cpp:2957-2958) notifyPresenceOfIndexedAccessors is only called if (mayBePrototype() && hasIndexedElementsInArrayStorage()). StructureCache::emptyStructureForPrototypeFromBaseStructure (StructureCache.cpp:98-105) evaluates prototype->anyObjectInChainMayInterceptIndexedAccesses() BEFORE calling createEmptyStructure, so indexingType stays ArrayWithUndecided.

Comment on lines +3745 to +3755
// Only a non-extensible object gets here with an index inside its vector: its elements
// stay in the vector with the attributes the structure records for them.
ASSERT(!isStructureExtensible());
auto scope = DECLARE_THROW_SCOPE(vm);
WriteBarrier<Unknown>& valueSlot = storage->m_vector[i];
if (valueSlot) {
if (structure()->vectorElementsAreReadOnly())
return typeError(globalObject, scope, mode == PutDirectIndexShouldThrow, ReadonlyPropertyWriteError);
valueSlot.set(vm, this, value);
return true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Builtins that must use CreateDataPropertyOrThrow silently overwrite elements of a sealed array instead of throwing, and the new in-vector branch at JSObject.cpp:3745-3755 is what accepts the write. The branch checks only vectorElementsAreReadOnly and then stores, never comparing the implied configurable:true descriptor against the non-configurable element. Array.from.call(() => sealed, [5, 6]) or a class field [0] = v on a constructor that returns a sealed array therefore mutate the sealed element; the spec and V8 throw a TypeError. Fix: in PutDirectIndexShouldThrow / ShouldNotThrow mode, reject the write (NonExtensibleObjectPropertyDefineError or an unconfigurable-change error) when the element is non-configurable, matching the descriptor validation the sparse-map path performs.

Why this was flagged

Trigger: s = Object.seal([1, 2]); Array.from.call(function () { return s; }, [5, 6]) (Array.from uses @ putByValDirect, strict builtin, so putDirectIndex with PutDirectIndexShouldThrow); or class A { constructor() { return Object.seal([1]); } } class B extends A { [0] = 9 } ; new B(). The JSObjectInlines.h:1601 fast check fails because the object is non-extensible, so control reaches putDirectIndexSlowOrBeyondVectorLength, ALL_ARRAY_STORAGE case: at JSObject.cpp:3749-3755 valueSlot exists, vectorElementsAreReadOnly is false for a sealed object, and the value is stored. CreateDataProperty supplies a descriptor with configurable:true; ValidateAndApplyPropertyDescriptor must return false for a non-configurable current property, so the spec result is a TypeError and the element stays 1. The base's SparseArrayValueMap::putDirect (SparseArrayValueMap.cpp:131-138) also accepted the write, but it also cleared DontDelete, and this PR rewrites the path with its own attribute model.

Verification: Object.seal([1,2]) now takes switchToSlowPutArrayStorage (JSObject.cpp:1227), so the elements stay in the vector with only vectorElementsAreNonConfigurable set on the structure. The new in-vector branch at JSObject.cpp:3745-3755 checks only vectorElementsAreReadOnly, so the write succeeds silently. Array.from.call(function(){ return Object.seal([1,2]); }, [5,6]) threw TypeError on the base and now returns [5,6].

Comment on lines +1595 to +1597
case ArrayWithSlowPutArrayStorage:
// The elements of a non-extensible object carry attributes (Structure::vectorElementAttributes).
return propertyName < butterfly()->vectorLength() && isStructureExtensible();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 An extensible array whose "length" is read-only can be grown by CreateDataProperty callers (Array.from, flat, map via Symbol.species) when spare vector capacity exists; the base throws before touching it. The putDirectIndex fast-path lambda at JSObjectInlines.h:1597 only requires isStructureExtensible() for SlowPutArrayStorage, so an index between length and vectorLength goes to setIndexQuickly, which stores and bumps length past the read-only value. Fix: for SlowPutArrayStorage the fast path must also refuse an index at or beyond storage length when structure()->arrayLengthIsReadOnly() (mode is not LikePutDirect there), so the slow path's ReadonlyPropertyWriteError at JSObject.cpp:3597 is reached.

Why this was flagged

Trigger: let a=[1,2,3]; a.push(4,5,6,7,8); a.length=3; Object.defineProperty(a,"length",{writable:false}); try { Array.from.call(function(){ return a; }, [0,0,0,0,0]); } catch {} a.length — Array.from's @ putByValDirect(result, k, v) reaches operationPutByValDirect (JITOperations.cpp:1696) → putDirectIndex with PutDirectIndexShouldThrow. The array is ArrayWithSlowPutArrayStorage, extensible, vectorLength 8, length 3. For k=3 the lambda at JSObjectInlines.h:1597 returns true, setIndexQuicklyForArrayStorageIndexingType stores and calls storage->setLength(4); same for k=4; the array ends with length 5 before the final Set("length") throws. Spec: CreateDataPropertyOrThrow on an array index >= length with non-writable length returns false and throws at k=3, leaving the array unchanged, which is what the base did (read-only length implied sparse mode with vectorLength 0, so the slow path hit map->lengthIsReadOnly()). The slow-path check at JSObject.cpp:3597 exists but is bypassed by the fast path.

Verification: normal — triggered when a JSArray whose "length" was made read-only via Object.defineProperty (not frozen/preventExtensions, so still extensible) has spare vector capacity and receives a CreateDataProperty-style put at an index >= length (Array.from / map / flat / filter result via new this() or Symbol.species, @ putByValDirect).

Comment on lines +1243 to +1248
{
if (!hasAnyArrayStorage(indexingType()))
return false;
const ArrayStorage* storage = butterfly()->arrayStorage();
if (storage->m_numValuesInVector)
return true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 (optional) Every array in a realm can be pushed onto the SlowPutArrayStorage slow path after a frozen array with no elements becomes a prototype; the base does not do this. hasIndexedElementsInArrayStorage at JSObject.cpp:1248 trusts m_numValuesInVector, but enterDictionaryIndexingModeWhenArrayStorageAlreadyExists (JSObject.cpp:1191) copies that count into the new zero-length vector and never resets it, so an emptied map still counts as elements. JSObject::freeze (JSObject.cpp:2957) and didBecomePrototype (StructureCreateInlines.h:48) then call notifyPresenceOfIndexedAccessors, which is haveABadTime for the whole realm. Fix: zero m_numValuesInVector when the vector is emptied into the map (or count only vector slots that exist), so hasIndexedElementsInArrayStorage never reports phantom elements.

Why this was flagged

Trigger: a = [1,2,3]; Object.preventExtensions(a); Object.defineProperty(a, '0', {value: 1, enumerable: false}) moves the vector into the sparse map via enterDictionaryIndexingModeWhenArrayStorageAlreadyExists (JSObject.cpp:1167-1199); resizeArray at 1191 keeps m_numValuesInVector == 3 while vectorLength becomes 0. a.length = 0 (setLengthWithArrayStorage only touches map entries, usedVectorLength is 0) or delete a[i] empties the map; the stale count stays 3. Object.freeze(a) then Object.setPrototypeOf(o, a) or Object.create(a): didBecomePrototype (StructureCreateInlines.h:48) sees vectorElementsAreReadOnly and hasIndexedElementsInArrayStorage true (JSObject.cpp:1247-1248 returns on m_numValuesInVector), calls notifyPresenceOfIndexedAccessors, which transitions to AddIndexedAccessors and, once the object is a prototype, calls globalObject->haveABadTime (JSObject.cpp:1276). The same happens in JSObject::freeze at 2957 when the array is already a prototype. The base froze through the generic loop and only hit haveABadTime per ReadOnly index actually defined; with an empty map it defined none.

Verification: nit — triggered when an array that once entered sparse mode (e.g. Object.defineProperty on an index, or defineProperty after preventExtensions) is emptied (a.length = 0 / delete), then frozen and made a prototype (or frozen while already a prototype).

Comment on lines +66 to +70
// alone (JSObject::enterDictionaryIndexingMode). Only the header byte of lastArray is read:
// the array may be dead by now.
IndexingType lastArrayIndexingType = lastArray->indexingType();
if (hasSlowPutArrayStorage(lastArrayIndexingType))
lastArrayIndexingType = (lastArrayIndexingType & ~IndexingShapeMask) | ArrayStorageShape;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟣 pre-existing, not blocking: Code that freezes arrays from a hot literal or new Array site gets every later array from that site allocated as ArrayWithArrayStorage, losing Int32/Contiguous inline paths for arrays that are never frozen. ArrayAllocationProfile.cpp:68-70 maps a SlowPutArrayStorage lastArray to ArrayStorageShape and feeds it to leastUpperBoundOfIndexingTypes, so a single frozen array poisons the site's profile permanently. Freezing is now 25x cheaper and advertised as fast, so this runs at far higher rate than before. Fix: when lastArray is SlowPut because it is non-extensible (structure()->didPreventExtensions()), skip the upgrade and keep the site's current indexing type; only a genuinely bad-time array should upgrade.
A small fix can ride a push you are already making; otherwise a short reply is enough.

Why this was flagged

Entry point: any allocation site whose most recent array (lastArray) was passed to Object.freeze / seal / preventExtensions before the next updateProfile call. ArrayAllocationProfile::updateProfile at ArrayAllocationProfile.cpp:66-70 reads lastArray->indexingType(), rewrites SlowPutArrayStorageShape to ArrayStorageShape, and computes the lub with the site's current type; the lub of Int32 and ArrayStorage is ArrayWithArrayStorage, so m_currentIndexingType becomes ArrayStorage. The profile is monotonic: it never goes back down. Every subsequent array created at that site, frozen or not, is ArrayWithArrayStorage and reads/writes through the ArrayStorage path instead of the Int32/Contiguous paths. The base froze arrays so slowly that the pattern was avoided; this change makes freeze cheap, so the population reaching this line grows by orders of magnitude while the new frozen-array-allocation-profile.js test only checks the frozen array itself. Remedy: in updateProfile, when lastArray's structure has didPreventExtensions(), leave the profile unchanged rather than upgrading to ArrayStorage.

Verification: pre-existing — the base already produces the identical profile outcome by the same route, so merging makes nothing worse. ArrayAllocationProfile.cpp:68-70 rewrites SlowPutArrayStorageShape to ArrayStorageShape and the profile only moves up. But base enterDictionaryIndexingMode (JSObject.cpp:1195-1221) calls ensureArrayStorageSlow, so the frozen array's indexingType was already ArrayWithArrayStorage at the base.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant