73689f5430825ecffb614dbcce8e6575dfea3d50 [JSC] `GetByStatus::computeFor` should not constant-fold prototype loads when the head structure is a dictionary
Triage note: GetByStatus::computeFor walked the prototype chain and constant-folded a prototype property even when the head structure was a cacheable dictionary; adding an own shadowing property to a dictionary does not transition the structure, so the JIT caches a stale prototype value and returns the wrong result. Fix adds `if (structure->isDictionary()) return std::nullopt;`, a JIT-soundness/value-confusion bug.
Contents
The bug at a glance
The bug is reachable from ordinary JavaScript: an attacker script drives a base object into a cacheable-dictionary structure, gets the DFG/FTL to constant-fold a prototype property load, and then shadows that property with an own value that the JIT continues to ignore. It yields a durable value confusion — the optimized code returns the wrong value with no OSR exit — which in the right idiom (e.g. a folded length, prototype method, or guard value) escalates to type confusion and memory corruption. High/8.1 fits a JIT soundness bug that requires a specific structure-shaping sequence but is fully script-reachable and defeats the engine’s own invariants.
JSC’s GetByStatus::computeFor is allowed to constant-fold a property read that actually resolves on the prototype, trusting that a single structure check on the base object proves “this object has no own property that shadows it.” That proof is airtight for normal structures, because adding an own property transitions the structure and invalidates the cached check. It is a lie for cacheable dictionaries: dictionary structures absorb new own properties in place without transitioning and without firing any watchpoint. So an attacker flattens an object into a dictionary that can’t be re-flattened, lets the DFG bake in the prototype’s value as a constant, then adds a shadowing own property — and the optimized code keeps handing back the stale prototype value forever. The one-line fix is to bail out of the prototype-folding walk the moment the head structure is a dictionary.
Root cause
The vulnerable state is inside GetByStatus::computeFor(JSGlobalObject*, const StructureSet&, ...) in Source/JavaScriptCore/bytecode/GetByStatus.cpp. When AI has proven a finite structure set for the base, computeFor walks up to maxPrototypeWalkDepth (8) prototype links from the head structure; if it finds the property on a prototype, it builds a Simple variant carrying an ObjectPropertyConditionSet and lets DFG AI constant-fold the GetById to the prototype’s value. The generated conditions (via generateConditions) cover only the prototype-chain objects and are watchpoint-backed; the absence of a shadowing own property on the base is assumed to be guaranteed by the base structure check alone.
The reaching path is a deliberately shaped object. The test creates o = Object.create(proto), adds ~200 own properties to overflow the transition-count limit so o’s structure becomes a cacheable dictionary, then does prototype accesses through an IC that flatten the dictionary and set hasBeenFlattenedBefore. It overflows again (properties p200..p399) to produce a second cacheable dictionary that inherits hasBeenFlattenedBefore — so future prototype-access ICs on o give up (they cannot flatten a dictionary that has already been flattened) and instead hand computeFor a structure set whose single member is that cacheable dictionary.
This is unsafe because adding an own property to a cacheable dictionary does not transition the structure and fires no watchpoint. computeFor folds o.protoProperty to the constant "PROTO_VALUE"; opt(o) returns that constant after warming. Then o.protoProperty = "OWN_VALUE" installs a shadowing own property without changing o’s structure ID — the exact structure the JIT checked still matches — so the guard on the optimized code still passes and it keeps returning the folded prototype value with no invalidation and no OSR exit. The engine’s model of the object and reality diverge permanently.
The fix adds, immediately after the null-structure check and before the prototype walk, if (structure->isDictionary()) return std::nullopt;. This makes computeFor refuse to constant-fold any prototype load whenever the head structure is a dictionary, mirroring the head-structure check the TryGetById folding path already performs. With no folded variant, GetById stays a real load that re-reads the object, so a later shadowing own property is honored.
Key code
Reject dictionary head structures before the prototype-chain walk (GetByStatus::computeFor)
if (!structure)
return std::nullopt;
+ if (structure->isDictionary())
+ return std::nullopt;
+
JSObject* prototype = nullptr;
auto* currentStructure = structure;
constexpr unsigned maxPrototypeWalkDepth = 8;
// ... walks the prototype chain, folding a prototype hit into a
// Simple variant with an ObjectPropertyConditionSet ...
Patch walkthrough
Source/JavaScriptCore/bytecode/GetByStatus.cpp— InGetByStatus::computeFor, right after theif (!structure) return std::nullopt;guard and before themaxPrototypeWalkDepthprototype-chain walk, addsif (structure->isDictionary()) return std::nullopt;. This rejects any head structure that is a dictionary, socomputeFornever produces a prototype-folding Simple variant for a dictionary base whose shadowing state cannot be pinned by a structure check. It intentionally mirrors the existing head-structure restriction used by the TryGetById folding path.
Background
GetByStatus::computeFor prototype folding — When AI proves a finite base structure set, computeFor may find the accessed property on a prototype and emit a Simple variant with an ObjectPropertyConditionSet. DFG/FTL then constant-folds the GetById to the prototype’s value, trusting the base structure check to prove no own property shadows it.
Cacheable dictionary structures — Objects with too many transitions are flattened into dictionary structures. A cacheable dictionary can be used by ICs, but crucially adding a new own property to it mutates the structure in place — no structure transition, no new StructureID, and no watchpoint fires. So a structure check cannot detect a newly added shadowing own property.
hasBeenFlattenedBefore — A structure flag preventing a dictionary that has already been flattened once from being flattened again. The PoC uses it to force a second cacheable dictionary that prototype-access ICs refuse to flatten, so they fall back to giving computeFor a dictionary-headed structure set instead of a normalized one.
ObjectPropertyConditionSet / watchpoints — The condition set records assumptions about prototype-chain objects (e.g. that a prototype still holds the property) and installs watchpoints that trigger OSR-invalidation when violated. It does not, and cannot, watch for a shadowing own property added to a dictionary base, which is the gap this bug exploits.
Vulnerability window
- Warm prototype IC —
warmProto(proto)runs 1e4 times so the property-replacement watchpoint set forproto.protoPropertyis created via a self IC onproto. - Dictionaryize base —
o = Object.create(proto)then ~200 own properties overflow the transition count, turningo’s structure into a cacheable dictionary; prototype accesses flatten it and sethasBeenFlattenedBefore. - Second overflow — Adding p200..p399 produces another cacheable dictionary that inherits
hasBeenFlattenedBefore, so prototype-access ICs onogive up rather than flatten, feeding computeFor a dictionary head. - Constant-fold —
opt(o)runs 1e5 times; DFG/FTL foldso.protoPropertyto the constant “PROTO_VALUE” based on the prototype walk and structure check. - Shadow —
o.protoProperty = "OWN_VALUE"adds a shadowing own property with no structure transition and no watchpoint fire. - Stale result —
opt(o)[1]still returns “PROTO_VALUE” (pre-fix) because the structure guard still matches and nothing invalidated the folded code — the test’s finalshouldBecatches the divergence.
Proof of concept
This is the committed regression test. It shows correctness divergence: pre-fix, opt(o)[1] returns the stale folded prototype value “PROTO_VALUE” even after a shadowing own property is added, while the interpreter path (o.protoProperty) correctly returns “OWN_VALUE”. It proves reachability and the value confusion but is not itself a memory-corruption exploit.
function shouldBe(a, e){ if (a !== e) throw new Error('bad: '+a+' expected '+e); }
let proto = { protoProperty: "PROTO_VALUE" };
function warmProto(p){ return p.protoProperty; }
noInline(warmProto);
for (let i=0;i<1e4;++i) shouldBe(warmProto(proto), "PROTO_VALUE");
let o = Object.create(proto);
o.x = 42;
for (let i=0;i<200;++i) o["p"+i]=i; // -> cacheable dictionary
for (let i=0;i<1e3;++i) shouldBe(warmProto(o), "PROTO_VALUE"); // flatten, set hasBeenFlattenedBefore
for (let i=200;i<400;++i) o["p"+i]=i; // second cacheable dictionary, IC gives up
function opt(o){ let a=o.x; let b=o.protoProperty; return [a,b]; }
noInline(opt);
for (let i=0;i<1e5;++i){ let [a,b]=opt(o); shouldBe(a,42); shouldBe(b,"PROTO_VALUE"); }
o.protoProperty = "OWN_VALUE"; // shadow, no structure transition
shouldBe(o.protoProperty, "OWN_VALUE");
shouldBe(opt(o)[1], "OWN_VALUE"); // fails pre-fix: still returns PROTO_VALUE
Exploitation
- Shape the dictionary base — Force the target object into a cacheable dictionary that has already been flattened (transition-count overflow, then a prototype IC flatten, then a second overflow) so computeFor is handed a dictionary-headed structure set for the accessed property.
- Bake in a stale constant — Warm an optimized function so DFG/FTL constant-folds a prototype-resolved property into the code, then add a shadowing own property to make the JIT’s cached value diverge from the real object without any OSR exit.
- Escalate the value confusion — Choose a folded value that the optimized code trusts as a type or bounds oracle (e.g. a folded array length, prototype method identity, or a guard constant) so the stale value drives an out-of-bounds access or type confusion. This escalation is idiom-dependent and not demonstrated by the patch’s test.
Detection & hunting
For defenders and SOC / detection engineers:
- Correctness divergence on dictionary bases —
- Folded prototype loads on dictionary structures —
- Missing OSR exit after property shadowing —
Audit directions
- Other status-folding paths —
- Dictionary structure invariants —
- Flatten/hasBeenFlattenedBefore interactions with ICs —