CVE-2026-20608
Overview
Background
- PropertyTable
- The per-structure table in JSC that records each own property’s key, storage offset, and attribute bits, and which also stores engine-internal private-name slots.
- Private name
- An engine-internal, non-spec-visible key used to back JavaScript class private fields (#x), managed by dedicated private-field opcodes rather than ordinary property access.
- PropertyAttribute (DontDelete/ReadOnly/Accessor)
- Attribute bits on a property controlling deletability (DontDelete = non-configurable), writability (ReadOnly = non-writable), and whether it is a getter/setter.
- Object.seal / Object.freeze
- ECMAScript operations that make an object’s own properties non-configurable (seal) and additionally non-writable for data properties (freeze); by spec they apply only to normal own properties.
- LogicError crash
- A non-memory-safety fault where a violated internal invariant trips an assertion/release-assert, terminating the process rather than corrupting memory.
Root Cause Analysis
The patch touches JSC’s PropertyTable, which backs the layout of an object’s properties, including engine-internal private-name slots created for JavaScript class private fields (e.g. #field). PropertyTable::seal() and PropertyTable::freeze() are invoked when user code runs Object.seal()/Object.freeze() on an object. Per the ECMAScript spec these operations apply only to normal, spec-visible own properties: seal marks them non-configurable (DontDelete) and freeze additionally marks data properties non-writable (ReadOnly).
Before the fix, both routines iterated over EVERY entry in the property table via forEachPropertyMutable and unconditionally OR’d in DontDelete (seal) or DontDelete|ReadOnly (freeze), with no check for whether the entry’s key was a private name. Private fields are stored in the same property table as ordinary properties but are not real object properties; they are engine-managed storage that the private-field access opcodes assume they fully control. When freeze stamped ReadOnly onto a private-field slot, a subsequent private-field write such as this.#field = v (the PutPrivateName / put_private_name path) hit a slot that the engine believed must always be writable, violating that internal invariant. The mismatch between the private-field machinery’s assumption (‘private slots are engine-controlled and writable’) and the attribute now present on the slot (ReadOnly/DontDelete) leads to an assertion failure / release-assert crash — the LogicError described. The read-side predicates isSealed()/isFrozen() had the mirror problem: they inspected private-name entries when deciding whether an object was sealed or frozen, so an object holding private fields could be classified inconsistently with how seal/freeze had (or should have) treated it.
The fix wraps every one of these attribute mutations and checks in if (!PropertyName(entry.key()).isPrivateName()), so private-name slots are skipped entirely: seal/freeze no longer alter their attributes, and isSealed/isFrozen no longer consider them. This restores the invariant that private-field slots are exempt from user-triggered seal/freeze semantics and remain writable/engine-managed, matching the regression test where 10000 instances of a class with a private field are sealed or frozen and then have their private field written. The concrete crashing opcode and assertion live in the private-field write path (not shown in this diff); that mechanism is an inference from the test and the attribute change, though the attribute corruption itself is exactly what the patch shows.
Attack Path
- Define a class with a private field
Author JS declares a class with an instance private field (e.g.
class C { #field; setField(v){ this.#field = v; } }), which causes JSC to allocate a private-name slot in the instance’s PropertyTable. - Seal or freeze an instance Call Object.seal(obj) or Object.freeze(obj) on an instance. PropertyTable::seal/freeze iterate all entries and, pre-patch, stamp DontDelete (seal) and DontDelete|ReadOnly (freeze) onto the private-field slot as well.
- Trigger a private-field write on the frozen/sealed slot
Invoke a method that executes
this.#field = v. The private-name put path operates on a slot that is now marked ReadOnly/DontDelete, contradicting its invariant that private storage is always engine-writable. - Force the assertion/crash Repeat in a hot loop (the test uses 10000 iterations) so JIT tiers and the private-field write path are exercised; the invariant violation surfaces as a release-assert / unexpected termination of the WebContent process.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
PropertyTable::sealSource/JavaScriptCore/runtime/PropertyTable.cpp |
modified | Now skips private-name entries before OR'ing in DontDelete, so private-field slots are no longer marked non-configurable by Object.seal. |
PropertyTable::freezeSource/JavaScriptCore/runtime/PropertyTable.cpp |
modified | Wraps the DontDelete/ReadOnly attribute stamping in a private-name guard, preventing private-field slots from being made ReadOnly and thereby preventing the crashing private-field write. |
PropertyTable::isSealedSource/JavaScriptCore/runtime/PropertyTable.cpp |
modified | Excludes private-name entries from the DontDelete check so an object with private fields is still correctly reported as sealed. |
PropertyTable::isFrozenSource/JavaScriptCore/runtime/PropertyTable.cpp |
modified | Excludes private-name entries from the DontDelete/ReadOnly checks so frozen-state reporting stays consistent with the seal/freeze changes. |
Files Changed
JSTests/stress/private-names-seal-freeze.jsSource/JavaScriptCore/runtime/PropertyTable.cpp
Audit Directions
- Other PropertyTable mutators/iteratorsAudit every use of forEachPropertyMutable / forEachProperty in PropertyTable.cpp and Structure that bulk-edits or inspects attributes; grep for
setAttributes(,entry.attributes(), and confirm each has aPropertyName(entry.key()).isPrivateName()guard where private slots should be exempt. - preventExtensions / other integrity operationsCheck JSObject/Structure implementations of preventExtensions, defineOwnProperty, and structure transitions that iterate properties, grepping for
PropertyAttribute::DontDelete/ReadOnlyOR-assignments applied across all entries without a private-name filter. - Symbol-keyed and other special keysLook for places that assume
isPrivateName()is the only special key class; grep forisSymbol(),isPrivateName(), and well-known-symbol handling near attribute changes to ensure other engine-internal keys are similarly excluded from freeze/seal semantics. - Private-field access path assumptionsReview put_private_name / get_private_name and their DFG/FTL nodes; grep for
PutPrivateName,DefinePrivateField, and any ASSERT that a private slot is writable, to find other places that could crash if a private slot ever carried ReadOnly/DontDelete.