CVE-2025-43529
Overview
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.
Attack Path
- Reach the FTL/DFG tier Run crafted JS so a hot function is compiled with the DFG store-barrier insertion phase in Global mode.
- 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.
- Elide a store barrier Store a reference into that object; because it was mis-tracked as non-escaped, the generational write barrier is omitted.
- 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.
- Weaponize Reclaim the freed cell with controlled data to build arbitrary read/write and code execution in WebContent.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
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 resetsIn DFGStoreBarrierInsertionPhase.cpp confirm every setEpoch(Epoch()) now goes through escape() and that no path resets a Phi without forAllTransitiveIncomingValues.
- Phi-aware bookkeepingGrep 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 elsewhereReview clobberize-driven escape/barrier logic and ArrayifyToStructure/PutByOffset barrier sites for under-approximation of escapes.
Patch
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();
}