CVE-2025-43529
Overview
Impact:
Description: Processing 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.
Researchers: Google Threat Analysis Group
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.
Patch Details
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();
}
Background
Advisory: Apple Security Advisory
| Attribute | Value |
|---|---|
| CVE | CVE-2025-43529 |
| Bugzilla | 302502 |
| Component | JSC |
| Bug Class | UAF |
| Severity | critical |
| Commit | b21a503b579a8ab1… |
| Advisory | Apple Advisory |
Terminology
- 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
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.
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.
Changed Functions
| Function | File | Change | Note |
|---|---|---|---|
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 Files
Source/JavaScriptCore/dfg/DFGStoreBarrierInsertionPhase.cpp