Critical CVSS 8.8 webkit UAF CISA KEV 🔧 Commit mapped

Overview

Critical
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
DescriptionProcessing maliciously crafted web content may lead to arbitrary code execution. Apple is aware of a report that this issue may have been exploited in an extremely sophisticated attack against specific targeted individuals on versions of iOS before iOS 26. CVE-2025-14174 was also issued in response to this report.
ComponentJSC DFG
Bug ClassUAF
Tracker302502
Fix commitb21a503b579a (WebKit/WebKit) +13/-7
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVListed
CreditedGoogle Threat Analysis Group
Disclosed2025-12-12

Background

Store/write barrier
A generational GC hook recorded when a reference is stored into an object, so the collector knows about old-to-new pointers; missing one can hide a live reference.
Escape analysis (barrier insertion)
The DFG phase tracks which allocations escape so it can insert barriers only where needed; under-approximating escapes drops required barriers.
Phi node
An SSA merge whose real producers are its transitive incoming values; bookkeeping that stops at the Phi misses those producers.
Epoch
A per-node marker the phase uses to track escape state within a region; setEpoch(Epoch()) resets it.

Root Cause Analysis

This fixes a use-after-free rooted in JavaScriptCore’s DFG store-barrier insertion phase, part of the change Apple shipped for an in-the-wild, extremely targeted exploit chain (issued alongside CVE-2025-14174). DFGStoreBarrierInsertionPhase decides where GC store barriers are required by tracking, per epoch, which allocations may have ’escaped’ (become reachable such that a write into them needs a barrier). The pre-patch code reset an allocation’s tracking with a plain node->setEpoch(Epoch()) in several places, and a local escape lambda that did the same only inside the wroteHeapOrStack block. Critically, in Global (whole-procedure) mode this failed to account for Phi nodes: when a value flows through a Phi, its true producers are the Phi’s transitive incoming values, so resetting only the Phi node left its incoming allocations still marked as non-escaped. An allocation that actually escaped through a Phi could therefore be treated as not needing a store barrier, so a reference stored into it would skip the generational write barrier. Without that barrier the garbage collector can miss an old-to-new pointer, collect an object that is still reachable, and later use of that reference is a use-after-free.

The fix hoists a single escape lambda that, in PhaseMode::Global, walks m_interpreter->phiChildren()->forAllTransitiveIncomingValues(node, …) and resets the epoch of every transitive incoming value (falling back to node->setEpoch(Epoch()) otherwise), and routes all three reset sites through it.

The restored invariant is that escape/barrier bookkeeping propagates through Phis, so no escaped allocation loses its required store barrier. The precise object freed in the real exploit is not in this diff and is inferred; the patch establishes the missed-barrier-through-Phi root cause.

Key insight
In whole-procedure mode the store-barrier phase reset only the Phi node and not its transitive incoming values, so an allocation that escaped through a Phi could lose its required write barrier — a GC-invariant violation that becomes a use-after-free.

Attack Path

  1. Reach the FTL/DFG tier Run crafted JS so a hot function is compiled with the DFG store-barrier insertion phase in Global mode.
  2. Route an allocation through a Phi Structure control flow so an escaping object is produced via a Phi’s incoming values, hitting the un-propagated reset.
  3. Elide a store barrier Store a reference into that object; because it was mis-tracked as non-escaped, the generational write barrier is omitted.
  4. Trigger GC and reuse Let the collector run; it misses the old-to-new edge and frees a still-referenced object, yielding a use-after-free.
  5. Weaponize Reclaim the freed cell with controlled data to build arbitrary read/write and code execution in WebContent.

Impact Assessment

A critical, remotely reachable use-after-free in the WebContent process caused by an elided GC store barrier — one of the strongest JSC primitives, giving an attacker a freed-then-reused object under heap-grooming control and a well-trodden path to arbitrary read/write and code execution. Apple reports this was used in an extremely sophisticated attack against targeted individuals, underscoring its exploitability in practice.

Changed Functions

FunctionChangeNotes
handleNode escape handling (StoreBarrierInsertionPhase)
Source/JavaScriptCore/dfg/DFGStoreBarrierInsertionPhase.cpp
modified Adds a hoisted escape lambda that, in Global mode, resets the epoch of all transitive incoming Phi values (forAllTransitiveIncomingValues) and routes the three reset sites (heap-overlap removeIf, wroteHeapOrStack, and the final potentialStackEscapes loop) through it.

Files Changed

  • Source/JavaScriptCore/dfg/DFGStoreBarrierInsertionPhase.cpp

Audit Directions

  • Same phase: all epoch resets
    In DFGStoreBarrierInsertionPhase.cpp confirm every setEpoch(Epoch()) now goes through escape() and that no path resets a Phi without forAllTransitiveIncomingValues.
  • Phi-aware bookkeeping
    Grep other DFG/FTL analyses that iterate nodes and reset/propagate state for handling of Phi transitive inputs (phiChildren(), forAllTransitiveIncomingValues) versus treating the Phi as a leaf.
  • Barrier correctness elsewhere
    Review clobberize-driven escape/barrier logic and ArrayifyToStructure/PutByOffset barrier sites for under-approximation of escapes.
diff --git a/Source/JavaScriptCore/dfg/DFGStoreBarrierInsertionPhase.cpp b/Source/JavaScriptCore/dfg/DFGStoreBarrierInsertionPhase.cpp
index 88cb74d592c3..29b9a17175b7 100644
--- a/Source/JavaScriptCore/dfg/DFGStoreBarrierInsertionPhase.cpp
+++ b/Source/JavaScriptCore/dfg/DFGStoreBarrierInsertionPhase.cpp
@@ -204,7 +204,17 @@ class StoreBarrierInsertionPhase : public Phase {
         bool result = true;
 
         UncheckedKeyHashMap<AbstractHeap, Node*> potentialStackEscapes;
-        
+        auto escape = [&](Node* node) {
+            if (mode == PhaseMode::Global) {
+                m_interpreter->phiChildren()->forAllTransitiveIncomingValues(
+                    node,
+                    [&](Node* incoming) {
+                        incoming->setEpoch(Epoch());
+                    });
+            } else
+                node->setEpoch(Epoch());
+        };
+
         for (m_nodeIndex = 0; m_nodeIndex < block->size(); ++m_nodeIndex) {
             m_node = block->at(m_nodeIndex);
             
@@ -460,7 +470,7 @@ class StoreBarrierInsertionPhase : public Phase {
                         return;
                     potentialStackEscapes.removeIf([&] (const auto& entry) {
                         if (entry.key.overlaps(heap)) {
-                            entry.value->setEpoch(Epoch());
+                            escape(entry.value);
                             return true;
                         }
                         return false;
@@ -480,10 +490,6 @@ class StoreBarrierInsertionPhase : public Phase {
                 clobberize(m_graph, m_node, readFunc, writeFunc, NoOpClobberize());
 
                 if (wroteHeapOrStack) {
-                    auto escape = [&] (Node* node) {
-                        node->setEpoch(Epoch());
-                    };
-
                     auto escapeToTheStack = [&] (Node* node) {
                         if (node->epoch() == m_currentEpoch) {
                             RELEASE_ASSERT(!!preciseStackWrite);
@@ -549,7 +555,7 @@ class StoreBarrierInsertionPhase : public Phase {
 
         {
             for (auto* node : potentialStackEscapes.values())
-                node->setEpoch(Epoch());
+                escape(node);
             potentialStackEscapes.clear();
         }
         
Loading diff…

Original Bug Report

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