← All issues

[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.

Severity: High | Component: JSC runtime LiteralParser | aa55894

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

- auto* structure = object->structure();
+ auto* originalStructure = object->structure();
auto property = [&, &vm = vm] ALWAYS_INLINE_LAMBDA -> Variant<ExistingProperty, Identifier> {
- if (Structure* transition = structure->trySingleTransition()) {
+ if (Structure* transition = originalStructure->trySingleTransition()) {
...
+ // After parseRecursively, user code may have run (e.g. due to a __proto__ setter in a
+ // nested object), which may have changed the structure of the object. This invalidates
+ // any cached transition, so reset it to Identifier to take the slow path.
+ if (object->structure() != originalStructure && std::holds_alternative<ExistingProperty>(property)) [[unlikely]]
+ property = Identifier::fromUid(vm, std::get<ExistingProperty>(property).structure->transitionPropertyName());
...
auto& [newStructure, offset] = std::get<ExistingProperty>(property);
Butterfly* newButterfly = object->butterfly();
- if (structure->outOfLineCapacity() != newStructure->outOfLineCapacity()) {
- newButterfly = object->allocateMoreOutOfLineStorage(vm, structure->outOfLineCapacity(), newStructure->outOfLineCapacity());
- object->nukeStructureAndSetButterfly(vm, structure->id(), newButterfly);
+ if (originalStructure->outOfLineCapacity() != newStructure->outOfLineCapacity()) {
+ newButterfly = object->allocateMoreOutOfLineStorage(vm, originalStructure->outOfLineCapacity(), newStructure->outOfLineCapacity());
+ object->nukeStructureAndSetButterfly(vm, originalStructure->id(), newButterfly);
}
validateOffset(offset);

JSTests/stress/literal-parser-proto-setter.js

+let fired = false;
+Object.prototype.__defineSetter__("__proto__", function(v) {
+ if (fired) return;
+ fired = true;
+ Object.prototype.__defineSetter__(0, function(){});
+});
+let ks = '"0":null,"1":2,"5":3';
+eval("({"+ks+",a:1})");
+eval("({"+ks+",a:1})");
+let o = eval("({"+ks+",a:{__proto__:0}})");
+if (o[1] !== 2)
+ throw new Error("incorrect eval result");

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.

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.

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.