← WebKit Silent-Fix Report — 2026-W22

78c04ea7a1  [JSC] Check initial object structure in tryEnsureAbsence in DFG

severity high class TypeConfusion confidence 0.85 JSC DFG exploitable-grade
Shu-yu Guo Fri May 29 09:41:46 2026 -0700 full: 78c04ea7a1bc88f883951d5b5a1ad75dd4676e4e bug report ↗ view on GitHub ↗
Primitive: JIT miscompile from uncached absence condition on head structure
Triage note: Adds head-structure cacheability check to prevent an unsound absence-based DFG optimization (OSR/type confusion).
Contents

The bug at a glance

High. tryEnsureAbsence in the DFG produces an ObjectPropertyConditionSet that the JIT caches as a proof that a property is absent, enabling aggressive folding (e.g. of property loads and Promise.resolve fast paths). Failing to validate the head (own) object structure means an own property that actually exists can be treated as absent, producing a JIT miscompile / type confusion where later code assumes the wrong shape. High value primitive, backed by a targeted stress test and rdar.

generateConditionsForPropertyMissConcurrently only walks the prototype chain to prove a property is missing; it never validates the head object’s own structure. tryEnsureAbsence separately re-validated only the prototype-chain objects, so if the head structure itself contains the property (or is otherwise non-cacheable for absence), the absence proof is generated anyway and cached as sound.

Root cause

In the DFG, Graph::tryEnsureAbsence attempts to build a set of object property conditions proving that a given identifier is absent from an object and its entire prototype chain, so the compiler can fold accesses that depend on that absence. It obtains headStructure (the structure of the base object) and then calls generateConditionsForPropertyMissConcurrently, which walks only the prototype chain producing Absence conditions for each prototype object.

The pre-patch code, after generating the set, looped over the prototype-chain objects and validated each: rejecting structures that overridesGetOwnPropertySlot, that are not propertyAccessesAreCacheable, not propertyAccessesAreCacheableForAbsence, that actually contain the property (isValidOffset(getConcurrently(…))), or that hasPolyProto. Critically, this validation was applied only to the prototype objects returned in the condition set, never to headStructure itself. Because generateConditionsForPropertyMissConcurrently does not examine the head object’s own slots, nothing verified that the head structure did not itself define the property.

The consequence: if the base object’s own structure contains the identifier (for example an own toJSON or then property), tryEnsureAbsence would still return a valid condition set asserting the property is absent. Downstream DFG optimizations then treat the property as missing on that object, skipping the real own property. The attached test drives this via +tmp.toJSON where tmp can be an object whose own structure has toJSON, and via Promise.resolve which consults a then property; the stale absence proof causes the wrong value to be folded in.

The fix factors the per-structure validation into an isAbsenceCacheable lambda and, crucially, applies it to headStructure before trusting the generated conditions: ‘if (!isAbsenceCacheable(headStructure)) return ObjectPropertyConditionSet::invalid();’. The same lambda replaces the inline prototype-object checks. Now, if the head object’s own structure has the property, overrides getOwnPropertySlot, is non-cacheable, poly-proto, etc., the absence proof is rejected and the unsound optimization is not performed. A follow-up (177d2cad35) further tightens isAbsenceCacheable to also reject dictionary structures.

Key code

Head structure now validated before caching absence (DFGGraph.cpp)

    auto isAbsenceCacheable = [&](Structure* structure) {
        if (structure->typeInfo().overridesGetOwnPropertySlot())
            return false;
        if (!structure->propertyAccessesAreCacheable())
            return false;
        if (!structure->propertyAccessesAreCacheableForAbsence())
            return false;
        unsigned attributes;
        if (isValidOffset(structure->getConcurrently(identifier.uid(), attributes)))
            return false;
        if (structure->hasPolyProto())
            return false;
        return true;
    };

    // generateConditionsForPropertyMissConcurrently only walks the prototype chain, so validate
    // headStructure first.
    if (!isAbsenceCacheable(headStructure))
        return ObjectPropertyConditionSet::invalid();

Patch walkthrough

  • Source/JavaScriptCore/dfg/DFGGraph.cpp — tryEnsureAbsence gains an isAbsenceCacheable(Structure*) lambda consolidating the cacheability tests: overridesGetOwnPropertySlot, propertyAccessesAreCacheable, propertyAccessesAreCacheableForAbsence, isValidOffset(getConcurrently(uid)) (i.e. the property is actually present), and hasPolyProto. A new call ‘if (!isAbsenceCacheable(headStructure)) return ObjectPropertyConditionSet::invalid();’ validates the base object’s own structure before generateConditionsForPropertyMissConcurrently is trusted. The old inline block that duplicated these checks for each prototype object is replaced by a call to the same lambda, so both head and prototype structures are now validated identically.

Background

ObjectPropertyConditionSet — A set of conditions (Present/Absence/Equivalence on specific structures) the JIT installs as watchpoints/checks; if all hold, a speculated access can be folded to a constant or fast path.

tryEnsureAbsence — DFG helper that tries to prove an identifier is absent from an object and its prototype chain so accesses/misses can be optimized (e.g. Promise then-lookup, toJSON, custom accessors).

generateConditionsForPropertyMissConcurrently — Builds Absence conditions by walking the prototype chain only; it assumes the caller has independently validated the head object’s own structure.

propertyAccessesAreCacheableForAbsence — A structure flag indicating that the absence of a property on that structure is stable enough to be cached without missing later mutations.

Vulnerability window

  1. Warm up — opt() is run ~200 times so the DFG compiles it, and tryEnsureAbsence proves toJSON/then absent on the involved objects.
  2. Missing head check — Because head structure is never validated, an object whose own structure actually defines toJSON is accepted as ‘absent’.
  3. Fold — The compiler folds +tmp.toJSON / Promise.resolve’s then lookup assuming the property is absent, using the wrong value.
  4. Trigger — Setting trigger=true and re-invoking opt makes the divergence observable (array[0] becomes an object where a double was assumed).
  5. Confusion — Subsequent array[0].x dereferences a value of an unexpected type, the type-confusion symptom.
  6. Fix — isAbsenceCacheable(headStructure) rejects the proof when the own structure carries the property.

Proof of concept

Verbatim opt() from dfg-ensure-absence-own-then-property.js. object1 (from createObject1) has an own toJSON:1 and object2 (createObject2) has own toJSON:{}. Because tmp’s own structure carries toJSON, the pre-patch tryEnsureAbsence wrongly proved it absent, so +tmp.toJSON and the Promise.resolve/then path were folded to the wrong shape. main() warms opt for 200 iterations then flips trigger and reads array[0].x to surface the confusion. Regression test, not a weaponized exploit.

function opt(container1, object2, array, thenable, flags) {
    const promise = new Promise(() => {});

    container1.x;
    thenable.x;

    const object1 = Object.getPrototypeOf(container1);

    let tmp = object1;
    object1.a;

    if (flags & 1) {
        tmp = object2;
        tmp.b;

        0[0];
    }

    +tmp.toJSON;
    +tmp.toJSON;

    array[0];
    Promise.resolve(+tmp.toJSON === 1 ? thenable : promise);

    array[0] = 2.3023e-320;
}

Exploitation

  1. Prime the proof — Repeatedly invoke the target function until the DFG installs the unsound absence-based folding for toJSON/then.
  2. Diverge types — Use the folded fast path to make the engine believe a property is absent while it is present, steering a value (double vs object) that the JIT then reinterprets.
  3. Type confusion — Follow with an access (array[0].x) that dereferences the mistyped value as an object pointer, the standard route from JIT miscompile to a fake-object/OOB primitive.
  4. Escalate — Chain the confusion into addrof/fakeobj and ultimately arbitrary read/write, the usual JSC exploitation arc.

Detection & hunting

For defenders and SOC / detection engineers:

  • Absence proofs without head validation
  • Divergent behavior post-DFG

Audit directions

  • Callers of generateConditionsForPropertyMiss* —
  • isAbsenceCacheable completeness
  • Structure vs live object

Before / after

Loading diff…