Medium CVSS 8.8 webkit Type Confusion 🔧 Commit mapped

Overview

Medium
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to memory corruption
ComponentJSC DFG
Bug ClassType Confusion
Tracker291742
Fix commit90aa8070e1b6 (WebKit/WebKit) +124/-34
CWECWE-119 (Buffer bounds error)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedYuhao Hu, Yan Kang, Chenggang Wu, and Xiaojie Wei
Disclosed2025-07-29

Background

OSR exit
On-Stack Replacement deoptimization that bails out of optimized (FTL) code back to a lower tier, requiring the optimized state to be reconstructed into the values the baseline interpreter expects.
Object sinking / materialization
A DFG/FTL optimization that eliminates an allocation whose object doesn’t escape and instead recreates (‘materializes’) it lazily at the points (including OSR exits) where it is actually needed.
AvailabilityMap / Availability
Compiler bookkeeping mapping each bytecode local and each PromotedHeapLocation of a sunken object to how its value can be recovered at exit — from a DFG node and/or a FlushedAt stack location.
FlushedAt / flush
A record that a value has been spilled to a specific virtual register (stack slot) and its format; OSR exit may recover the value from that slot when the flush is ‘useful’.
PutStack / KillStack
DFG nodes that write a value to, or mark dead, a stack slot; PutStack in particular can reuse a virtual register that a still-referenced sunken-object field was previously flushed to.
validateFTLOSRExitLiveness
A JSC option enabling extra debug validation of OSR-exit availability/liveness; the new validateAvailability runs under it and the regression test requires it.

Root Cause Analysis

The patch fixes a stale-flush bug in DFG/FTL OSR-availability analysis that governs how sunken (materialized) objects are reconstructed on OSR exit. AvailabilityMap tracks, for each bytecode local and for each PromotedHeapLocation of a phantom/sunken object, an Availability that may carry a FlushedAt location — a virtual register (stack slot) where the value has been spilled. On OSR exit the FTL recovers a materialization’s field either from its SSA node or, if the field is ‘flush useful’, from that stack slot. The violated invariant is that a heap availability’s FlushedAt stack slot must still hold the expected DFG node’s value at the exit point. LocalOSRAvailabilityCalculator::executeNode processed GetStack/PutStack/KillStack by updating only the m_locals flush state; it never invalidated heap availabilities that pointed at a stack slot which a later PutStack or KillStack reused for a different value. Consequently a sunken object’s promoted-location Availability could keep a FlushedAt pointing at a virtual register whose contents had since been clobbered, so on OSR exit the materialization would read a wrong/attacker-influenced value out of that slot.

The fix adds a killHeaps lambda invoked when executeNode sees a PutStack (for its target operand) or KillStack: it scans m_availability.m_heap and, for every heap entry whose FlushedAt virtualRegister equals the clobbered operand’s register, resets it to FlushedAt(ConflictingFlush), forcing the OSR exit to rematerialize from the SSA node instead of the stale slot. The heap/local pruning was also refactored from the mutating pruneHeap()+pruneByLiveness() into a non-mutating filterByLiveness() that first filters locals by bytecode liveness and then rebuilds the heap from nodes reachable from the surviving locals, and a debug-only validateAvailability() was added (gated on Options::validateFTLOSRExitLiveness) that cross-checks each ‘flush useful’ heap entry against the local occupying the same stack slot and DFG_CRASHes if they disagree on the node or flush format. New ASSERT(!isDead()) checks on inline-call-frame argumentCount/callee/argument availabilities, and an ASSERT(property.value().kind() != ExitValueDead) in the OSR exit stub compiler, assert the corrected invariant at materialization time.

The added regression test osr-availability-heap-materialization-clobbered.js drives phantom arguments objects (arg()/arg.apply()) inside a loop with a throwing call so the sunken arguments materialization’s flushed slot is reused, reproducing the disagreement.

Key insight
OSR-exit availability tracked that a sunken object’s field was flushed to a stack slot but failed to invalidate that flush when a later PutStack/KillStack reused the same slot; the fix is to kill stale heap flushes on slot clobbering so materialization falls back to the true SSA value.

Attack Path

  1. Trigger FTL tiering Run a hot function repeatedly so JSC tiers it up to the FTL, enabling object sinking/allocation elimination and OSR-exit-based deoptimization.
  2. Create sinkable phantom objects with flushed fields Use constructs the DFG sinks — e.g. arguments objects from a nested function plus Function.prototype.apply, closures, and inlined call frames — so their fields become PromotedHeapLocations whose Availability is flushed to stack slots.
  3. Force stack-slot reuse Shape control flow (a loop with a throwing/invalid call such as (3881)(arg2) caught in try/catch) so a later PutStack/KillStack reuses the same virtual register that a live sunken-object field’s FlushedAt still references.
  4. Provoke an OSR exit Cause a speculation failure at that point so the FTL runs the OSR exit and materializes the sunken object, recovering the affected field from the now-clobbered stack slot instead of its true SSA value.
  5. Obtain a type-confused / wrong-valued field The materialized object receives an attacker-influenced or type-inconsistent value in one of its slots, yielding a JS-level object whose internal state violates its structure’s assumptions (inference: the concrete corruption depends on which slot and value are groomed into the reused stack slot).
  6. Escalate Standard JSC exploitation from a value/type confusion: build addrof/fakeobj, corrupt a butterfly/length or structure to gain arbitrary R/W, then achieve code execution — all within the WebContent process (background).

Impact Assessment

The primitive is a wrong-value / type-confusion at OSR-exit object materialization: a sunken object’s field is reconstructed from a clobbered stack slot, so an attacker who grooms which value occupies the reused slot can place an unexpected/type-inconsistent value into a JS object. In JSC such deopt-time confusions are routinely escalated to addrof/fakeobj and then to arbitrary read/write and code execution, so realistic escalation is toward RCE rather than a mere controlled crash. The bug is in JavaScriptCore and is confined to the WebContent process, subject to the WebKit sandbox; exploitation still requires a separate sandbox escape for full device compromise. Reachability is high (pure JS, no special API), matching the ‘maliciously crafted web content -> memory corruption’ description.

Changed Functions

FunctionChangeNotes
AvailabilityMap::filterByLiveness
Source/JavaScriptCore/dfg/DFGAvailabilityMap.cpp
added Non-mutating replacement for pruneByLiveness+pruneHeap: builds a fresh map, keeps only locals live in bytecode at 'where', then rebuilds the heap from PromotedHeapLocations whose base node is reachable from those live locals.
AvailabilityMap::pruneHeap
Source/JavaScriptCore/dfg/DFGAvailabilityMap.cpp
deleted Old in-place heap pruner folded into filterByLiveness.
AvailabilityMap::pruneByLiveness
Source/JavaScriptCore/dfg/DFGAvailabilityMap.cpp
modified Now just assigns *this = filterByLiveness(graph, where).
AvailabilityMap::validateAvailability
Source/JavaScriptCore/dfg/DFGAvailabilityMap.cpp
added Debug validator (used under validateFTLOSRExitLiveness): for each flush-useful heap entry with a valid flushed VirtualRegister, finds the local at the same slot and DFG_CRASHes if they disagree on the DFG node or flush format — directly encodes the violated invariant.
AvailabilityMap::AvailabilityMap (default ctor)
Source/JavaScriptCore/dfg/DFGAvailabilityMap.h
added Explicit default constructor needed now that filterByLiveness returns a fresh map by value.
LocalOSRAvailabilityCalculator::executeNode
Source/JavaScriptCore/dfg/DFGOSRAvailabilityAnalysisPhase.cpp
modified Core fix: adds killHeaps(operand) which, on PutStack (of data->operand) and KillStack (of unlinkedOperand), invalidates any heap Availability whose FlushedAt VirtualRegister matches the clobbered slot by setting FlushedAt(ConflictingFlush); also adds ASSERT(!isDead()) on inline-call-frame argumentCount/callee/argument.
OSRAvailabilityAnalysisPhase::run (block loop / exit handling)
Source/JavaScriptCore/dfg/DFGOSRAvailabilityAnalysisPhase.cpp
modified Runs validateAvailability after each executeNode under validateFTLOSRExitLiveness, and uses filterByLiveness at exit origins instead of copy-then-pruneByLiveness.
FTLLowerDFGToB3 OSR-exit lowering (compileNode / buildExitArguments)
Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
modified Restructures the validateFTLOSRExitLiveness guard and iterates availabilityMap.m_heap by reference; largely validation/cleanup around the same exit-materialization path.
compileStub (FTL OSR exit)
Source/JavaScriptCore/ftl/FTLOSRExitCompiler.cpp
modified Adds ASSERT(property.value().kind() != ExitValueDead) before recovering a materialization property value, asserting that no needed materialization field is dead.
PutStackSinkingPhase (liveness loops)
Source/JavaScriptCore/dfg/DFGPutStackSinkingPhase.cpp
modified Adds verbose dataLog tracing only; no behavioral change.

Files Changed

  • JSTests/stress/osr-availability-heap-materialization-clobbered.js
  • Source/JavaScriptCore/dfg/DFGAvailabilityMap.cpp
  • Source/JavaScriptCore/dfg/DFGAvailabilityMap.h
  • Source/JavaScriptCore/dfg/DFGOSRAvailabilityAnalysisPhase.cpp
  • Source/JavaScriptCore/dfg/DFGPutStackSinkingPhase.cpp
  • Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp
  • Source/JavaScriptCore/ftl/FTLOSRExitCompiler.cpp

Audit Directions

  • Other executeNode cases that write stack slots
    In DFGOSRAvailabilityAnalysisPhase.cpp audit every switch case that touches m_availability.m_locals.operand(…).setFlush or sets heap entries; confirm each slot-clobbering path now routes through killHeaps. Grep for setFlush, FlushedAt(ConflictingFlush), and KillStack/PutStack handling.
  • Heap availability vs local flush consistency
    Run/extend validateAvailability coverage: search for places that build PromotedHeapLocation entries with flushedAt() set (ArgumentCountPLoc, ArgumentsCalleePLoc, ArgumentPLoc and any *PLoc) and verify a corresponding live local always backs the same VirtualRegister; look for isFlushUseful() call sites.
  • Other consumers of FlushedAt at OSR exit
    In FTLLowerDFGToB3.cpp and FTLOSRExitCompiler.cpp grep for exitValueForAvailability, neededForMaterialization, and ExitValueDead to find any materialization/recovery path that trusts a flushed slot without checking the flush is still valid.
  • Availability liveness pruning across phases
    Search the whole dfg/ftl tree for remaining callers of pruneByLiveness / former pruneHeap semantics and for AvailabilityMap copies (assignments/merges) that could reintroduce heap entries referencing dead-or-reused slots; verify they now use filterByLiveness.

Original Bug Report

The reporter's bug is still restricted on the tracker.