← WebKit Silent-Fix Report — 2026-W21

b07dca9615  [JSC] GetByStatus::computeFor should not walk proto chain for direct property access

severity high class TypeConfusion confidence 0.65 JSC bytecode/DFG exploitable-grade
Vignesh Rao Wed May 20 21:27:58 2026 -0700 full: b07dca9615f1d691b97fa6cf2a0915d2a5ae68e6 bug report ↗ view on GitHub ↗
Primitive: GetByStatus walked prototype chain for direct-property access
Triage note: Inline-cache/status type-confusion feeding the JIT.
Contents

The bug at a glance

This is a JIT type-confusion/soundness bug: GetByStatus::computeFor walked the prototype chain even for direct (own-property) accesses like GetByIdDirect and GetPrivateNameById, so it could hand the DFG a status describing a prototype property when the actual bytecode semantics require an own-property lookup. Feeding the optimizing compiler a status that contradicts the real access semantics lets it install an inline cache / speculation that reads the wrong slot or elides checks, the classic route from a JIT mis-informing itself to attacker-controlled memory access. It is high severity because it originates in the compiler’s model of memory layout and is reachable from ordinary property reads, though the diff itself does not include a memory-corruption regression test, so the observed change is the correctness fix rather than a demonstrated exploit.

The surface is every optimized property read: op_get_by_id, op_get_by_id_direct, private-name gets, and op_get_from_scope for globals, all of which the DFG models via GetByStatus. Web content reaches it simply by executing property accesses hot enough to tier up; direct accesses (class private fields via GetPrivateNameById, and GetByIdDirect) are the ones whose status was computed incorrectly, and those arise from perfectly ordinary class/field JavaScript.

Root cause

GetByStatus is the DFG/FTL’s summary of how a get-by-id-shaped operation behaves, derived from profiling and structure information; the compiler consults it to decide what speculation and inline-cache shape to emit. The structure-set overload GetByStatus::computeFor(JSGlobalObject*, const StructureSet&, CacheableIdentifier) is used to fold a get when the base’s structure set is known. Its old body contained a helper (attempToFold) that, when the property was not an own property of the structures in the set, walked the prototype chain looking for the property and could return a status describing that prototype access. A stale comment even flagged the hazard: it said the function ‘is also used for GetByIdDirect since this function only looks into direct properties. When supporting prototype chains, we should split this for GetById and GetByIdDirect.’ The bug is that prototype-walking support was added to this shared entry point without splitting direct from normal, so a GetByIdDirect (which must only ever resolve own properties) could be told about a property that only exists up the prototype chain.

The consequence is a semantic mismatch the JIT cannot detect: for a direct access the runtime will NOT consult the prototype, so if computeFor reports a simple prototype-based variant, the compiler may emit code/speculation predicated on a property location that the direct access will never actually read — a type confusion between the compiler’s belief and the executed access. Downstream, this status flows into DFGAbstractInterpreterInlines executeEffects (which folds get-by-id when structure and identifier are known) and DFGConstantFoldingPhase foldConstants, both of which will constant-fold or specialize based on the wrong variant.

The fix threads the access kind through the API. GetByStatus gains enum class LookupMode : bool { Normal, Direct }; the structure-set computeFor takes a new LookupMode mode parameter and only performs the prototype walk when mode == LookupMode::Normal (if (mode == GetByStatus::LookupMode::Normal) { if (auto result = attempToFold()) return result.value(); }), otherwise falling through to the simple self-access path. DFGNode gains propertyLookupMode(), which maps node opcodes to the correct mode: GetByIdDirect, GetByIdDirectFlush and GetPrivateNameById return Direct, while GetById, GetByIdFlush and GetByIdMegamorphic return Normal (any other op hits RELEASE_ASSERT_NOT_REACHED). The abstract interpreter and constant-folding phase now pass node->propertyLookupMode() into computeFor, so a direct-access node can never be given a prototype-derived status. The ByteCodeParser call for op_get_from_scope is explicitly passed LookupMode::Normal with a comment that a global-property get should walk the global object’s prototype chain — documenting that this particular site legitimately wants prototype semantics. Net effect: the compiler’s status now matches the executed access semantics for both direct and normal gets, removing the type-confusion input.

Key code

computeFor now gates the prototype walk on LookupMode, and Node maps opcodes to the mode (verbatim from diff)

    // We should do a prototype walk searching for the property only if this
    // is a normal access. Don't consult proto for for direct accesses.
    if (mode == GetByStatus::LookupMode::Normal) {
        if (auto result = attempToFold())
            return result.value();
    }

    GetByStatus result;
    result.m_state = Simple;

    // ---- DFGNode.h ----
    GetByStatus::LookupMode propertyLookupMode()
    {
        switch (op()) {
        case GetByIdDirect:
        case GetByIdDirectFlush:
        case GetPrivateNameById:
            return GetByStatus::LookupMode::Direct;
        case GetById:
        case GetByIdFlush:
        case GetByIdMegamorphic:
            return GetByStatus::LookupMode::Normal;
        default:
            RELEASE_ASSERT_NOT_REACHED();
        }
    }

Patch walkthrough

  • Source/JavaScriptCore/bytecode/GetByStatus.cpp — Adds a LookupMode parameter to the structure-set computeFor overload and gates the prototype-walking attempToFold call behind mode == Normal, so a Direct access never receives a prototype-derived variant. Removes the stale comment that acknowledged the function only handled direct properties, since the direct/normal distinction is now explicit.
  • Source/JavaScriptCore/bytecode/GetByStatus.h — Declares enum class LookupMode : bool { Normal, Direct } and updates the computeFor signature to take it, making the access kind a required part of the query rather than an implicit assumption.
  • Source/JavaScriptCore/dfg/DFGNode.h — Includes GetByStatus.h and adds Node::propertyLookupMode(), mapping GetByIdDirect/GetByIdDirectFlush/GetPrivateNameById to Direct and GetById/GetByIdFlush/GetByIdMegamorphic to Normal, with RELEASE_ASSERT_NOT_REACHED for unexpected opcodes so misuse is caught.
  • Source/JavaScriptCore/dfg/DFGAbstractInterpreterInlines.h — In executeEffects, computes lookupMode = node->propertyLookupMode() and passes it to computeFor, so the abstract interpreter folds get-by-id using the correct own-vs-prototype semantics for the specific node.
  • Source/JavaScriptCore/dfg/DFGConstantFoldingPhase.cpp — In foldConstants, likewise passes node->propertyLookupMode() into computeFor so constant folding of get-by-id respects the direct/normal distinction.
  • Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp — For op_get_from_scope on a global property, explicitly passes LookupMode::Normal with a comment that the global-property get should walk the global object’s prototype chain — the one site that intentionally wants prototype semantics.

Background

GetByStatus — A bytecode-level summary of how a get-by-id operation behaves — which structures it has seen, whether it is a simple self access, a prototype access, a getter call, or megamorphic. The DFG and FTL use it to decide whether to inline the access, what structure checks to speculate, and what inline-cache variant to emit. If the status misdescribes the access, the compiler’s emitted code is built on a false premise.

Direct vs. normal property access — A normal get (GetById) follows JavaScript semantics that consult the prototype chain if the own object lacks the property. A direct get (GetByIdDirect, and private-name access GetPrivateNameById) resolves only the object’s own properties and must never consult the prototype. Modeling a direct access as if it could read a prototype property is a semantic contradiction the runtime will never honor.

DFG AbstractInterpreter / executeEffects — The abstract interpreter propagates types and folds operations at compile time. In executeEffects, when a base’s structure set and the identifier are known it calls GetByStatus::computeFor to try to resolve the get statically. A wrong status here can cause the interpreter to prove a value’s type or location incorrectly, which then licenses unsound optimizations elsewhere.

DFG ConstantFoldingPhase — A DFG phase that replaces operations with constants or simpler forms when abstract values permit. It also queries GetByStatus; folding a direct get as though it hit a prototype property would substitute the wrong value or the wrong access, embedding the confusion into the compiled graph.

Inline caches and speculation — JSC specializes property access with inline caches keyed on structure, backed by DFG speculation guards. The compiler trusts GetByStatus to tell it what shape to guard and where the slot lives; a status that names a prototype slot for a direct access can produce a guard/load pair that reads memory the actual operation would not, the mechanism by which a mis-informed JIT turns into a type confusion.

op_get_from_scope for globals — Reading a global variable is modeled as a get on the global object, and per spec that lookup does traverse the global object’s prototype chain. This is the one call site the patch deliberately marks LookupMode::Normal, showing that prototype-walking is correct there — the bug was applying that same walk to direct-access opcodes, not to globals.

Vulnerability window

  1. Profiling — A direct property access (e.g. a class private field read compiled to GetPrivateNameById, or a GetByIdDirect) executes and records structure information.
  2. Tier-up — The function is optimized; the DFG queries GetByStatus::computeFor with the observed structure set to model the access.
  3. Errant prototype walk — Pre-patch, computeFor unconditionally ran attempToFold, which walked the prototype chain and could return a simple status describing a property found on a prototype rather than on the object itself.
  4. Mismodel — The abstract interpreter / constant-folding phase accept this status and specialize the access as if the prototype property were the target, though the direct access will only ever read own properties.
  5. Unsound compile — The compiler emits speculation/inline-cache code predicated on the wrong slot or elides checks it should keep, creating a divergence between compiled behavior and real semantics — a type confusion.
  6. Post-patch — propertyLookupMode() forces Direct for those opcodes and computeFor skips the prototype walk, so the status matches the executed access and the mismodel cannot arise.

Triggering

The patch adds no regression test, so no verbatim PoC exists. A conceptual trigger: repeatedly perform a direct-access read whose object does NOT have the property as an own property but whose prototype does — for example a class instance read via a direct/private-name access where the same-named property lives on the prototype — on objects with a stable structure set, until the function tiers up into the DFG. Pre-patch, GetByStatus::computeFor would walk the prototype chain and return a simple variant describing the prototype slot, and the abstract interpreter/constant-folding phase would specialize the direct access against that prototype-derived structure/slot, diverging from the own-property-only semantics the direct access actually executes. Turning that divergence into an observable type confusion requires arranging objects whose own-property layout differs from the prototype-derived variant the compiler baked in.

Exploitation

  1. Induce the wrong status — Shape a direct/private-name access whose target property is absent as an own property but present on the prototype, and warm it so the DFG computes a prototype-derived GetByStatus for the direct node.
  2. Provoke divergence — Present objects at runtime whose actual own-property layout differs from the prototype-based variant the compiler specialized against, so the compiled access reads a slot/offset that does not correspond to the real semantics.
  3. Type confusion — The mismatch between the speculated structure/slot and the true object layout is a type confusion: a load can be steered to read a field of the wrong type or an out-of-line slot the object does not actually own.
  4. Honest limit — The public artifact is a correctness fix with no memory-corruption PoC; whether a given manifestation yields a controllable read/write depends on which variant the compiler picks and the resulting offset math. As delivered this is a demonstrated JIT-soundness/type-confusion input rather than a proven end-to-end primitive.

Detection & hunting

For defenders and SOC / detection engineers:

  • Direct-access speculation crashes — Crashes in DFG/FTL-compiled get-by-id paths involving GetByIdDirect or GetPrivateNameById, or bad-type/structure-check failures where a direct access appears to have consulted a prototype, match this bug. On assertion-enabled builds, RELEASE_ASSERT_NOT_REACHED in propertyLookupMode would flag an unexpected opcode reaching the new path.
  • Private-field access anomalies — Telemetry showing incorrect values or crashes from class private-field reads (GetPrivateNameById) under optimization is a targeted indicator, since private-name gets are direct accesses that were mismodeled.
  • Version gating — Fixed at commits.webkit.org/313636@main and on safari-7624.2.5.110. Builds before it compute prototype-walking status for direct accesses; treat optimized direct/private-field reads as a soundness risk there.

Audit directions

  • Shared status computation across access kinds — grep for GetByStatus::computeFor and PutByStatus/InByStatus/DeleteByStatus computeFor callers to confirm each passes an access-kind/mode; a single entry point serving both direct and normal semantics without a mode parameter is the architectural class here.
  • Prototype-walking in status/IC code — Search bytecode/ for prototype-chain traversal (proto walks, prototypeChain, conditions) inside status or inline-cache computation and verify it is gated to normal (proto-consulting) accesses only, never applied to GetByIdDirect/GetPrivateNameById.
  • Opcode-to-semantics mapping completeness — Audit propertyLookupMode()’s switch against the full set of get-by-id-shaped DFG opcodes; a new direct-access opcode omitted from the Direct list would silently fall to RELEASE_ASSERT or the wrong mode, reintroducing the mismatch.

Before / after

Loading diff…