Post

CVE-2025-43529 The Garbage Collector Race That Gave DarkSword Its Teeth

CVE-2025-43529 The Garbage Collector Race That Gave DarkSword Its Teeth

Executive Summary

CVE-2025-43529 is a use-after-free (UAF) vulnerability in Apple’s JavaScriptCore — the JavaScript engine powering Safari and all browsers on iOS. Buried deep in the interaction between the DFG JIT compiler and the concurrent garbage collector, this bug allowed attackers to corrupt memory through a webpage visit and achieve arbitrary code execution inside Safari’s renderer process.

It was the Stage 1b of the DarkSword exploit chain — the adaptive fallback used specifically against iPhones running iOS 18.6 and 18.7, after CVE-2025-31277 had been patched. Where CVE-2025-31277 failed (patched in iOS 18.6), CVE-2025-43529 stepped in to keep the chain alive.

The root cause is both highly technical and deeply interesting: the DFG compiler’s StoreBarrierInsertionPhase correctly identified that a Phi node had escaped, but missed that the associated Upsilon nodes had also escaped. This single oversight caused the concurrent GC to skip scanning live objects — freeing memory that was still in use — leading directly to a use-after-free condition exploitable via pure JavaScript.

Reported to Apple by Google Threat Intelligence Group (GTIG) in late 2025, patched in iOS 18.7.3 and iOS 26.2. Added to CISA’s Known Exploited Vulnerabilities catalog. Exploited in the wild in targeted spyware campaigns against users in Ukraine, Saudi Arabia, Turkey, and Malaysia.


CVE Summary

FieldDetail
CVE IDCVE-2025-43529
ProductApple WebKit / JavaScriptCore
Affected PlatformsiOS/iPadOS < 18.7.3, Safari < 26.2, macOS Tahoe < 26.2, watchOS < 26.2, tvOS < 26.2, visionOS < 26.2
Vulnerability TypeUse-After-Free (CWE-416) — DFG GC missing store barrier
CVSS v3.1 Score8.8 (High)
CVSS2 Score10.0 (Critical)
Qualys QVS95
Attack VectorNetwork (malicious web content in Safari)
Privileges RequiredNone
User InteractionRequired (visit malicious page)
ImpactArbitrary memory R/W → RCE in WebContent process
Role in DarkSwordStage 1b — RCE entry point for iOS 18.6–18.7
Reported ByGoogle Threat Intelligence Group (GTIG)
Patched IniOS 18.7.3, iOS 26.2, Safari 26.2
Fix Description“Improved memory management” (Apple)
CISA KEVYes
Active ExploitationYes — DarkSword campaign, state actors + spyware vendors

Context: CVE-2025-43529 vs CVE-2025-31277

Before diving into the technical details, it’s important to understand how CVE-2025-43529 relates to its predecessor in DarkSword:

AspectCVE-2025-31277CVE-2025-43529
DarkSword StageStage 1a (primary)Stage 1b (fallback)
iOS Target18.4 – 18.518.6 – 18.7
Bug ClassType confusion / UAFGC race condition / UAF
ComponentDFG JIT type speculationDFG StoreBarrierInsertionPhase
Trigger MechanismJIT type assumption violationMissing write barrier → GC frees live object
Patched IniOS 18.6iOS 18.7.3

DarkSword was designed with adaptive Stage 1 selection — it fingerprinted the target’s iOS version and chose the appropriate exploit. When Apple patched CVE-2025-31277 in iOS 18.6, DarkSword’s operators were ready with CVE-2025-43529 for the newer versions. This demonstrates the professional, product-like quality of the DarkSword exploit kit.


Deep Dive: JavaScriptCore’s Concurrent Garbage Collector

To understand CVE-2025-43529, we must first understand how JSC’s garbage collector works and why write barriers are essential.

JSC’s GC Architecture

JavaScriptCore uses a generational, mostly-concurrent, non-compacting garbage collector. “Mostly-concurrent” means the GC runs simultaneously with the JavaScript execution thread (the mutator) — the GC is marking live objects and reclaiming dead ones while your JavaScript code is still running.

This concurrency is essential for performance (no “stop the world” pauses), but it creates a fundamental challenge:

1
2
3
4
5
6
7
8
9
10
11
12
PROBLEM: The Mutation Hazard

GC Thread:           Mutator Thread (JS execution):
  Scanning obj A...    
                       A.child = newObject   ← stores reference AFTER GC scanned A
  ...done with A
  
Result: GC never scanned newObject
        → GC thinks newObject is unreachable
        → GC frees newObject's memory
        → Mutator still holds reference to freed memory
        → USE-AFTER-FREE

Write Barriers (Store Barriers): The Solution

Write barriers (also called store barriers) are the solution. They are small pieces of code inserted around every write operation that modifies a reference in a managed object:

1
2
3
4
5
6
7
// Without write barrier (VULNERABLE):
object->child = newValue;   // Direct store — GC might miss this

// With write barrier (SAFE):
writeBarrier(object, newValue);   // Notifies GC: "I just stored newValue into object"
object->child = newValue;         // Then perform the store
// GC knows to scan newValue even if it already scanned object

In JSC’s DFG JIT, the StoreBarrierInsertionPhase is a compiler optimization pass responsible for inserting these write barriers at the right locations in JIT-compiled code.

SSA Form, Phi Nodes, and Upsilon Nodes

DFG uses a form of Static Single Assignment (SSA) for its intermediate representation. In SSA:

  • Every variable is assigned exactly once
  • When control flow merges (e.g., at the end of an if-else), Phi nodes represent the merged value

JSC’s DFG uses a specific variant:

  • Phi node: Represents a value that merges from multiple control-flow paths. It has a “shadow variable” that collects incoming values.
  • Upsilon node: The “sending side” — it assigns a value to a Phi’s shadow variable from a specific control-flow predecessor.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// JavaScript:
function example(condition) {
    let x;
    if (condition) {
        x = obj1;      // → Upsilon node: x_shadow = obj1
    } else {
        x = obj2;      // → Upsilon node: x_shadow = obj2
    }
    return x;          // → Phi node: x = phi(x_shadow)
}

// DFG IR representation (simplified):
BB0:
  branch(condition) → BB1, BB2

BB1:  
  Upsilon(@obj1, ^x_phi)    // "send" obj1 to Phi's shadow
  jump → BB3

BB2:
  Upsilon(@obj2, ^x_phi)    // "send" obj2 to Phi's shadow  
  jump → BB3

BB3:
  @x = Phi(^x_phi)          // Phi merges the value
  Return(@x)

The Root Cause: The Missing Store Barrier

What “Escaped” Means

In compiler analysis, a value “escapes” when it can be observed from outside its originally intended scope — it gets passed to another function, stored in a global, or otherwise becomes reachable from code the compiler can’t fully analyze.

When a Phi node “escapes,” it means the merged value can outlive the context where it was created. The StoreBarrierInsertionPhase is supposed to insert write barriers for escaped values to ensure the GC doesn’t miss them.

The Bug: Half-Escaped Analysis

CVE-2025-43529’s root cause was a subtle incompleteness in escape analysis:

1
2
3
4
5
6
7
What StoreBarrierInsertionPhase checked:
  ✓ Did the Phi node escape?
    → YES → insert write barrier for the Phi

What it MISSED:
  ✗ Did the Upsilon nodes (that feed into the Phi) also escape?
    → NOT CHECKED → NO write barrier inserted for Upsilons

In practice, when a Phi node is escaped, its associated Upsilon nodes are implicitly also escaped — they are writing values that will be observable through the escaped Phi. But the StoreBarrierInsertionPhase only checked the Phi itself, not its Upsilons.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Simplified pseudocode of the VULNERABLE logic:

void StoreBarrierInsertionPhase::process(Node* node) {
    if (node->op() == Phi) {
        if (node->hasEscaped()) {
            insertStoreBarrier(node);    // ✓ Barrier inserted for Phi
        }
        // BUG: Upsilon nodes that feed this Phi are NOT checked here
    }
    
    if (node->op() == Upsilon) {
        // No escape check for Upsilon
        // No store barrier inserted
        // Even if this Upsilon feeds an escaped Phi → NOTHING HAPPENS
    }
}

// FIXED logic:
void StoreBarrierInsertionPhase::process(Node* node) {
    if (node->op() == Upsilon) {
        PhiNode* correspondingPhi = node->phi();
        if (correspondingPhi->hasEscaped()) {
            insertStoreBarrier(node);    // ✓ Now also handles escaped Phi's Upsilons
        }
    }
}

The Resulting Use-After-Free

When an Upsilon node’s write is not covered by a store barrier, the concurrent GC can free the object being written through the Upsilon, even though it’s still reachable via the Phi:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Timeline of exploitation:

T1: JIT-compiled code executes Upsilon: stores reference to object X
    into Phi's shadow variable
    [No store barrier — GC not notified]

T2: Concurrent GC scans the Phi's shadow variable
    [Sees old/stale value from before T1]
    [Does NOT see object X]
    
T3: GC determines object X has no live references
    GC FREES object X's memory
    
T4: Phi node returns its value — which points to freed memory (object X)
    JavaScript code accesses the Phi's value
    → ACCESS TO FREED MEMORY → USE-AFTER-FREE

Exploitation: From UAF to Arbitrary R/W

Phase 1: Triggering the UAF

The attacker writes JavaScript that:

  1. Creates specific object patterns that pass through Phi/Upsilon nodes in DFG IR
  2. Forces DFG compilation (run the function enough times to trigger JIT)
  3. Races the GC by running code that creates/frees many objects quickly
  4. Triggers the missing-barrier path — causing a live object to be freed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// CONCEPTUAL — illustrative of UAF trigger pattern
// NOT actual exploit code

function createPhiEscapeScenario(condition, arr1, arr2) {
    let result;
    if (condition) {
        result = arr1;      // Upsilon → Phi (arr1)
    } else {
        result = arr2;      // Upsilon → Phi (arr2)  
    }
    
    // 'result' is a Phi node
    // If this escapes to an external function, arr1/arr2 Upsilons
    // may not get store barriers
    externalFn(result);     // Makes Phi "escape"
    
    return result[0];       // Access through potentially freed object
}

// Warm up for DFG compilation:
for (let i = 0; i < 10000; i++) {
    createPhiEscapeScenario(true, [1.1, 2.2], [3.3, 4.4]);
}

// Now trigger with conditions that race the GC:
triggerGCPressure();  // Many allocations to encourage GC to run
createPhiEscapeScenario(true, victimObject, decoyObject);
// → If GC races and frees victimObject during execution,
//   result[0] accesses freed memory → UAF

Phase 2: Heap Feng Shui — Controlling What Gets Allocated

After triggering the UAF (accessing freed memory), the attacker must control what gets allocated in the freed slot. This is “heap feng shui” — carefully shaping the heap’s allocation patterns:

1
2
3
4
5
6
7
8
9
10
11
// CONCEPTUAL — heap grooming pattern
// After freeing the victim object:

// Allocate controlled data structures of the exact same size
// as the freed object — the allocator will reuse the slot
let controlled = new Float64Array(SAME_SIZE_AS_VICTIM);
controlled[0] = 0x4141414141414141;  // Attacker-controlled bytes

// Now accessing the freed Phi value → reads from controlled
// The JSArray's "length" field is at a known offset
// If we control what's in the freed slot, we can corrupt length

Phase 3: Length Corruption → Relative R/W

By controlling the data in the freed memory slot and corrupting the internal length field of a JSArray:

1
2
3
4
5
6
7
8
9
10
11
// A JSArray with length=3 normally:
let arr = [1.1, 2.2, 3.3];
arr.length;  // → 3

// After UAF + heap grooming → arr's backing store corrupted:
arr.length;  // → 0xFFFFFFFF (attacker-controlled value!)

// Now arr acts like a huge array — OOB access possible:
arr[100000];  // Read from arbitrary memory offset
arr[100000] = value;  // Write to arbitrary memory offset
// → RELATIVE ARBITRARY READ/WRITE

Phase 4: Build addrof + fakeobj → Full Arbitrary R/W

Using the relative R/W, the attacker builds the two fundamental exploit primitives (same approach as CVE-2025-31277, different triggering mechanism):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// addrof: get memory address of any JS object
function addrof(target) {
    // Place target in a position where OOB read leaks its address
    craftedContainer[0] = target;
    return itof(relativeRead(craftedOffset));
    // Returns float whose bits = pointer to target
}

// fakeobj: create JS object at arbitrary memory address
function fakeobj(addr) {
    relativeWrite(craftedOffset, ftoi(addr));
    return craftedContainer[0];
    // Returns JS object backed by memory at addr
}

// From these: full arbitrary R/W within WebContent process
// → Same outcome as CVE-2025-31277, different path to get there

Phase 5: Code Execution

With arbitrary R/W in the WebContent process, DarkSword proceeds identically from both Stage 1 variants — locating function pointers, setting up for the PAC bypass in Stage 2 (CVE-2026-20700), and eventually achieving root kernel access.


A Critical Distinction: CVE-2025-43529 vs PPL/SPTM

An important technical note: DarkSword’s pure-JavaScript approach did not require direct PPL (Page Protection Layer) or SPTM (Secure Page Table Monitor) bypasses at this stage.

PPL and SPTM are Apple Silicon hardware-enforced protections that prevent even a compromised kernel from modifying page tables arbitrarily. Bypassing them typically requires additional hardware-level exploits.

DarkSword sidestepped this by:

  1. Staying in JavaScript for Stage 1 (CVE-2025-43529) → avoids needing kernel memory write at this stage
  2. Using a user-mode PAC bypass (CVE-2026-20700 in dyld) instead of a kernel-mode PAC bypass
  3. Routing through specific driver interfaces (Stage 5, CVE-2025-43510) that have legitimate kernel access paths

This architectural choice made DarkSword more portable and less dependent on hardware-specific exploits — a sign of sophisticated engineering.


The DFG JIT + GC Interaction: A Systemic Vulnerability Class

CVE-2025-43529 is not the first, and likely not the last, vulnerability arising from the DFG JIT and concurrent GC interaction:

CVEYearRoot CauseComponent
CVE-2018-41922018Missing write barrier in Array.prototype.reverseGC write barrier
CVE-2019-86012019Integer overflow in DFG’s compileNewArrayWithSpreadDFG compiler
CVE-2022-428562022FTL JIT type confusionFTL JIT
CVE-2025-312772025DFG type confusionDFG type speculation
CVE-2025-435292025Missing store barrier for Upsilon nodes of escaped PhiDFG + Concurrent GC

The pattern reveals a systemic architectural tension: JSC’s concurrent GC requires write barriers at every reference-writing operation. The DFG JIT generates machine code for these operations, but the barrier insertion is an optimization pass that can have edge cases. Every edge case is a potential vulnerability.

The specific class of bug in CVE-2025-43529 — incomplete escape propagation through SSA Phi/Upsilon pairs — is subtle because Phi and Upsilon nodes are conceptually distinct in the DFG IR, but semantically coupled (Upsilons only exist to feed Phi nodes). A developer analyzing the StoreBarrierInsertionPhase might naturally check “is this Phi escaped?” without asking “are the Upsilons that feed it implicitly escaped too?”


Detection

For Security Researchers

CVE-2025-43529 leaves few direct artifacts, but some signals:

WebContent crash logs (pre-exploitation crash during attacker’s probing):

1
2
3
4
5
Settings → Privacy & Security → Analytics & Improvements → Analytics Data
Look for:
- WebContent process crashes in DFG-related stack frames
- Crashes in JSC::DFG::* or JSC::GC::* functions
- Abnormal number of WebContent restarts around Safari browsing

Memory analysis indicators:

  • Unusually large WebContent process memory during benign Safari sessions
  • Heap pattern anomalies — many same-sized allocations/frees (heap grooming)
  • iVerify’s kernel behavioral monitoring can detect post-Stage 1 anomalies

Network detection:

1
2
3
4
5
After Stage 1 succeeds, DarkSword advances rapidly:
- Alert on burst HTTPS traffic from Safari immediately after page load
- Monitor for connections to newly-registered domains from mobile devices
- DNS queries to infrastructure associated with DarkSword C2 patterns
  (use threat intel feeds from Lookout, Zimperium, iVerify)

YARA Rule (Pattern Detection for UAF Trigger in JS)

rule DarkSword_Stage1_GCRace_Pattern {
    meta:
        description = "Detects JS patterns consistent with GC race trigger for CVE-2025-43529"
        author = "Security Research"
        date = "2026-03-22"
    strings:
        // Heap spray + trigger patterns
        $s1 = "Float64Array" ascii
        $s2 = "ArrayBuffer" ascii  
        $s3 = "trigger" nocase ascii
        $s4 = /function.*phi.*upsilon/i
        // Suspicious size constants associated with JSObject internals
        $size1 = { 68 00 00 00 00 00 00 00 }  // 0x68 — common JSObject size
    condition:
        filesize < 500KB and
        ($s1 and $s2 and ($s3 or $s4))
}

Mitigation

Patch Status

PlatformVulnerable RangeFixed Version
iOS / iPadOS< 18.7.3 (and < 26.2)18.7.3 or 26.2
Safari< 26.226.2
macOS Tahoe< 26.226.2
watchOS< 26.226.2
tvOS< 26.226.2
visionOS< 26.226.2

Full DarkSword protection requires patching all 6 CVEs — the minimum safe version is iOS 18.7.6 or iOS 26.3.1.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1. Immediate: Update all Apple devices
   iOS 18.7.6 / iOS 26.3.1 (patches all 6 DarkSword CVEs)
   Settings → General → Software Update

2. Enable Automatic Updates:
   Settings → General → Software Update → Automatic Updates → ON

3. High-risk users: Enable Lockdown Mode
   Settings → Privacy & Security → Lockdown Mode → Turn On
   → Disables JIT in Safari → eliminates the entire JIT/GC vulnerability class

4. Enterprise MDM:
   Enforce minimum iOS version 18.7.6 or 26.3.1
   Block corporate resource access from non-compliant devices

5. Security researchers — study the fix:
   Compare JSC source before/after iOS 18.7.3 patch:
   Focus on: Source/JavaScriptCore/dfg/DFGStoreBarrierInsertionPhase.cpp
   Look for: Changes to how Phi and Upsilon node escape status is evaluated

MITRE ATT&CK Mapping

TacticTechniqueID
Initial AccessDrive-by CompromiseT1189
ExecutionExploitation for Client ExecutionT1203
Defense EvasionProcess Injection (via concurrent GC timing)T1055
Privilege EscalationExploitation for Privilege EscalationT1068
CollectionData from Local SystemT1005

Conclusion

CVE-2025-43529 represents the cutting edge of browser exploit engineering. The root cause — a missing write barrier for Upsilon nodes feeding an escaped Phi node — is not a simple oversight. It requires deep understanding of JSC’s DFG IR, SSA semantics, and the concurrent GC’s requirements to both find and exploit. The fact that DarkSword’s operators had this as a ready backup when iOS 18.6 patched their primary CVE speaks to the professional, product-oriented development of the DarkSword exploit kit.

The deeper lesson: concurrent garbage collectors and JIT compilers are each individually complex systems. Their interaction creates a combinatorially large attack surface. Write barriers must be inserted correctly at every reference-writing operation in JIT-compiled code. One missed edge case in escape analysis — Upsilon nodes feeding an escaped Phi — was enough to keep nation-state surveillance capabilities alive against hundreds of millions of iPhones for another version cycle.

Update. Enable Lockdown Mode. And appreciate that the people maintaining your phone’s security are fighting a technically sophisticated adversary who found this bug before Apple did.


References


This post is intended for security researchers, browser security engineers, and mobile threat analysts. All code samples are conceptual and based on publicly disclosed vulnerability class analysis.

This post is licensed under CC BY 4.0 by the author.