[4] LiteralParser structure confusion via __proto__ setter re-entrancy
LiteralParser caches a structure transition before parsing the value — a nested __proto__ setter reshapes the object first, and the write lands at the stale offset.
Rated High because the diff adds a re-validation of a cached structure transition that was previously applied to write into a JSFinalObject's butterfly across a JS re-entrancy boundary, yielding a controlled wrong-offset/out-of-bounds write reachable from web content; developing that into full read/write requires heap grooming, which the diff does not establish.
LiteralParser has a fast path for caching transitions when parsing a literal with an existing transition, done before the object literal is actually parsed. During parsing, user code may run due to setters for __proto__, which may invalidate the original object's structure and thus its cached transition. The PR fixes this by taking the slow path if the structure changes.
Source/JavaScriptCore/runtime/LiteralParser.cpp
JSTests/stress/literal-parser-proto-setter.js
Patch Details
The fast path caches a Structure transition (ExistingProperty { newStructure, offset }) computed from the object's structure before the property value is parsed. The patch renames the captured local to originalStructure and adds a post-parseRecursively guard: after the nested value is parsed (which may have run arbitrary JS), it checks object->structure() != originalStructure; if so and the cached result is still an ExistingProperty, it discards the transition by resetting property to an Identifier, forcing the slow lookup. The butterfly-resize/nukeStructureAndSetButterfly block now compares and allocates against originalStructure.
Failure to re-validate a cached object-structure transition across a JavaScript re-entrancy boundary (a proto setter) before applying it to mutate the object's butterfly.
Background
A Structure is JSC's hidden-class object describing an object's property layout; adding a property follows or creates a transition to a new Structure. trySingleTransition() returns the single cached property-addition transition, letting the parser skip a hash lookup. The Butterfly is out-of-line storage holding named-property and indexed values; a property's offset indexes into it. nukeStructureAndSetButterfly atomically swaps an object's structure id and butterfly pointer during a resize. The LiteralParser fast path is used for JSON.parse and for evaluating object/array literals: it builds a JSFinalObject and, for each property, either follows a cached transition (writing the value straight into the butterfly at the transition offset) or falls back to a generic put. Setting __proto__ in an object literal invokes a user-defined __proto__ setter if one is installed on Object.prototype — so native parsing code synchronously calls into JavaScript that can mutate engine state before returning.
Analysis
This is a structure/type confusion (a TOCTOU across a JS re-entrancy boundary) leading to an out-of-bounds or wrong-slot butterfly write. Before the fix, parseRecursively snapshotted the in-progress object's Structure and used it to cache a single property-addition transition, then parsed the property value via a recursive call. That recursive parse can execute arbitrary JavaScript when a nested literal contains __proto__ and a setter is installed on Object.prototype. The setter can mutate the in-progress object's shape, so object->structure() no longer equals the snapshotted structure — yet the cached newStructure/offset are still relative to the old structure.
The pre-fix code applied that stale transition to the now-differently-shaped object: it resized the butterfly using the old outOfLineCapacity(), set the structure id to the stale value, and wrote the parsed value at the stale offset. Because offset and the capacity delta were computed for a structure that no longer describes the object, the write lands at an offset that does not correspond to the object's true layout. The regression test demonstrates the trigger: the __proto__ setter, on first fire, installs a setter for index 0, which alters subsequent property additions; warm-up passes (eval("({...,a:1})") twice) prime the outer object's structure with a cached single transition for property a, and the third eval with a:{__proto__:0} fires the setter mid-parse. The precise mechanism by which installing the index-0 setter diverges originalStructure is inferred from the added guard, not directly shown.
If an attacker grooms the property name/index set so the stale offset lands outside the object's true out-of-line storage, this would yield a controlled wrong-offset / out-of-bounds write into a JSFinalObject butterfly, plus a structure id set to a shape that does not match the object's true layout — a structure-confusion primitive that could be developed toward relative read/write of adjacent heap data. Realising a strong primitive would require heap grooming to place a useful victim adjacent to the corrupted butterfly, and exploitation yields corruption only within the WebContent renderer.
This vulnerability weakens memory-type safety inside the renderer. The security model assumes the Structure a LiteralParser uses to compute a property offset still describes the object when the value is written into the butterfly; before the fix that invariant was violated whenever a nested-literal __proto__ setter re-entered JS mid-parse. __proto__ is uniquely dangerous inside literal parsers — it is the one literal token that can synchronously invoke a user setter during construction — so any fast path that caches object state before parsing a __proto__-bearing value must assume arbitrary engine mutation occurred.
Note: The exact structure-divergence mechanism and the OOB-write groomability are inferred from the added guard and test rather than directly visible; the core TOCTOU and its fix (re-read object->structure() and bail to the slow path on mismatch) are fully supported by the diff.
Audit directions
- Structure snapshots applied after a JS re-entrancy boundary. Native code that snapshots an object's
Structure/transition, crosses a JS re-entrancy boundary, then applies the snapshot to write into the butterfly. Audit other LiteralParser/JSON fast paths and direct-butterfly-write helpers for caches not re-validated after a recursive value parse. Start inLiteralParser.cpparoundparseRecursively,addPropertyTransitionToExistingStructure, and everynukeStructureAndSetButterfly/allocateMoreOutOfLineStoragecall site that follows a parse step. __proto__as a re-entrancy trigger during construction. Grep forunderscoreProto/__proto__handling in object-literal and JSON construction paths and verify each treats a__proto__value-parse as a point after which structure, butterfly, and prototype may have changed. Examinem_visitedUnderscoreProtologic and the bytecode object-literal builder (op_new_object/ put-by-id fast paths) for the same TOCTOU.- Precomputed offsets applied to possibly-transitioned objects. Audit fast paths that store a
(Structure*, PropertyOffset)pair and later write at that offset; verify the current structure still matches before the store. SearchtrySingleTransitionandtransitionOffsetconsumers acrossSource/JavaScriptCore/runtimefor caches taken before a callback-capable operation.