← All issues

[4] LiteralParser structure confusion via __proto__ setter re-entrancy

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.

🔒

A cached object-shape decision is applied across a JavaScript re-entrancy boundary — the ownership and corruption implications of the stale layout are explored in depth.

Subscribe to read more

🔒

Multiple reusable audit patterns identified for re-entrancy-induced structure staleness, with concrete LiteralParser and runtime starting points for variant discovery.

Subscribe to read more