Post

CVE-2025-31277 Inside the JavaScriptCore JIT Vulnerability That Opened iPhones to DarkSword

CVE-2025-31277 Inside the JavaScriptCore JIT Vulnerability That Opened iPhones to DarkSword

Author: Security Research
Date: March 21, 2026
Severity: High (CVSS 8.8)
Tags:


Executive Summary

CVE-2025-31277 is a JIT type confusion / use-after-free memory corruption vulnerability in JavaScriptCore — the JavaScript engine that powers Apple’s Safari browser and WebKit runtime. It was the Stage 1 entry point of the DarkSword exploit chain, providing attackers with an arbitrary memory read/write primitive inside Safari’s WebContent renderer process — the critical foothold from which all subsequent stages of full iPhone kernel compromise were built.

Exploited in the wild by multiple state-sponsored groups and commercial surveillance vendors since November 2025 as part of the DarkSword campaign, CVE-2025-31277 exemplifies why JIT compilers remain one of the most lucrative and dangerous attack surfaces in modern software. The vulnerability was patched in iOS 18.6 (August 2025) — but DarkSword’s operators continued using it against the hundreds of millions of devices that hadn’t yet updated.

This post provides a technical deep dive into:

  • How JavaScriptCore’s DFG JIT works
  • What type confusion means at the machine code level
  • How CVE-2025-31277 was likely structured and exploited
  • How it served as the gateway to full kernel compromise in DarkSword
  • What it tells us about the ongoing war between JIT performance and security

CVE Summary

FieldDetail
CVE IDCVE-2025-31277
ProductApple WebKit / JavaScriptCore
Affected PlatformsiOS/iPadOS < 18.6, Safari < 18.6, macOS Sequoia < 15.6, watchOS < 11.6, visionOS < 2.6, tvOS < 18.6
Vulnerability TypeJIT type confusion / use-after-free (memory corruption)
CVSS v3.1 Score8.8 (High)
Attack VectorNetwork (via malicious web content in Safari)
Attack ComplexityLow
Privileges RequiredNone
User InteractionRequired (visit malicious page)
ImpactArbitrary memory R/W → RCE in WebContent process
Role in DarkSwordStage 1 (for iOS 18.4 – 18.5) — initial RCE entry point
Patched IniOS 18.6, Safari 18.6, macOS Sequoia 15.6
Fix Description“Improved memory handling” (Apple)
Active ExploitationYes — DarkSword campaign, Nov 2025+

Understanding JavaScriptCore and the JIT Pipeline

Before diving into the vulnerability, we need to understand the architecture that makes it possible.

JavaScriptCore: The Engine Under Safari

JavaScriptCore (JSC) is Apple’s open-source JavaScript engine, embedded in WebKit. Every webpage you load in Safari executes its JavaScript through JSC. It must balance two competing demands:

  1. Security — untrusted JavaScript from arbitrary websites must be sandboxed
  2. Performance — modern web apps are JavaScript-heavy, speed is critical

To achieve performance, JSC uses tiered Just-In-Time compilation — progressively more aggressive optimization as code runs more frequently:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
JavaScript Source Code
        ↓
LLInt (Low-Level Interpreter)
  First execution — pure interpretation
  Gathers profiling data (what types are actually used?)
        ↓
Baseline JIT
  Generates simple native code
  Adds type feedback collection
        ↓
DFG JIT (Data Flow Graph JIT) ← CVE-2025-31277 lives here
  Speculative optimization based on profiling
  Generates highly optimized machine code
  Makes type assumptions → speed gains
        ↓
FTL JIT (Faster Than Light JIT)
  LLVM-based maximum optimization
  Inlines functions, eliminates branches

How DFG JIT Works: Speculative Optimization

The DFG JIT is where CVE-2025-31277 originates. Understanding it requires grasping type speculation:

1
2
3
4
5
6
function add(a, b) {
    return a + b;
}

// Called 1000 times with integers:
add(1, 2); add(5, 3); add(10, 7); ...

DFG observes that a and b are always integers. It compiles a specialized native version:

; DFG-compiled add() — assumes integer arguments
mov  eax, [a_value]    ; load first arg
add  eax, [b_value]    ; integer add (fast!)
ret                    ; return

This is much faster than a generic version that handles any JavaScript type. But there’s a contract: if the assumption is ever violated (someone calls add("hello", 42)), DFG must “bail out” — deoptimize back to a slower tier that handles all types.

The security question: what happens if that bail-out fails or is incorrectly triggered?

JSValue: How JavaScript Values Are Represented in Memory

All JavaScript values in JSC are represented as 64-bit JSValue structures using NaN-boxing encoding:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
JSValue: 64-bit value with type embedded in high bits

Double (floating point number):
  Any valid IEEE 754 double (< 0xFFFE000000000000)

Integer (32-bit int packed):
  0xFFFF000000000000 | int32_value

Pointer to JSObject (heap-allocated object):
  0x0000XXXXXXXXXXXX  (pointer with upper bits zero/small)

Special values:
  0xFFFFFFFFFFFFFFFF = undefined
  0xFFFF000000000002 = null
  0xFFFF000000000003 = true
  0xFFFF000000000004 = false

Critical insight: The same 64-bit pattern is interpreted differently depending on its type tag. If the DFG JIT incorrectly identifies the type, it will treat an object pointer as a floating point number — or vice versa — causing catastrophic memory misinterpretation.


CVE-2025-31277: The Type Confusion Bug

Nature of the Vulnerability

CVE-2025-31277 is described as a JIT optimization/type confusion vulnerability in the DFG layer of JavaScriptCore, with a use-after-free component. Apple’s fix description was simply “improved memory handling.”

Based on the vulnerability class and how similar JSC DFG bugs have historically worked, the likely root cause involves one of two patterns:

Pattern A: Incorrect Speculative Type

The DFG speculatively marks an object as a specific type (e.g., Int32 or Double). Under certain operation sequences, an object can be transformed (its “structure” changed — JSC’s internal class descriptor) after the DFG’s type snapshot is taken, but before the generated code executes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Conceptual — illustrative of type confusion class
function confuse(arr) {
    // DFG profiles: arr is always an array of doubles
    // Generates optimized code treating arr[0] as Double
    return arr[0] + 1.0;
}

// Warm up DFG with doubles:
for (let i = 0; i < 10000; i++) confuse([1.1, 2.2]);

// Now trigger: replace arr[0] with an object DURING execution
// via a side effect in a property getter or Proxy trap
// The DFG-compiled code treats the object pointer as a double
// Reading the raw bits of an object pointer as a float → type confusion

Pattern B: Use-After-Free via GC Race

An object is enqueued for garbage collection. The DFG JIT holds a reference to the freed memory through a type-profiling structure. When the GC reuses that memory for a new object, the JIT’s reference now points to the new object — but with the old type information:

1
2
3
4
5
6
7
Timeline:
T1: DFG records: slot[0] = type:Double, ptr=0xABCD0000
T2: GC frees object at 0xABCD0000
T3: New JSObject allocated at 0xABCD0000 (same address)
T4: DFG-compiled code accesses slot[0]
    → Reads new JSObject's header bits as a Double
    → type confusion achieved

Achieving Arbitrary Memory R/W: addrof and fakeobj

Once type confusion is achieved — the ability to read an object’s pointer bits as a float, or write a float’s bits as a pointer — attackers construct two fundamental exploit primitives:

1. addrof primitive — get the memory address of any JavaScript object:

1
2
3
4
5
6
7
8
9
10
11
// CONCEPTUAL — illustrative only
function addrof(target) {
    // Put target at a position where type confusion reads it as float
    confusedArray[0] = target;      // Store object
    triggerConfusion();              // Trigger type confusion
    return confusedArray[0];        // Read as float — raw pointer bits!
    // The float's bit pattern = the object's memory address
}

let secretAddr = addrof(sensitiveObject);
// secretAddr is now a float whose bits = pointer to sensitiveObject

2. fakeobj primitive — create a JavaScript object at an arbitrary memory address:

1
2
3
4
5
6
7
8
9
10
11
12
// CONCEPTUAL — illustrative only
function fakeobj(addr) {
    // Write the address bits as a float at a position
    // where type confusion will interpret float as object pointer
    confusedArray[0] = addr;        // Write raw address as float
    triggerConfusion();              // Trigger type confusion
    return confusedArray[0];        // Read as JSObject pointer!
    // Returns a "fake" JS object backed by memory at addr
}

let fakeObject = fakeobj(targetMemoryAddress);
// fakeObject is a JS object whose backing storage starts at targetMemoryAddress

3. Constructing arbitrary R/W from addrof + fakeobj:

The standard technique is to craft a fake JSArray object pointing to attacker-controlled memory as its “butterfly” (element storage):

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 exploit primitive construction
// Full arbitrary read/write achieved by crafting a fake JSArray

// Step 1: Create a real container object and get its address
let container = {
    butterfly_ptr: null,    // Will hold controlled butterfly
    structure_id: realArrayStructureId
};

// Step 2: Get the address of container
let containerAddr = addrof(container);

// Step 3: Craft a fake array object with container as its butterfly
// Container's butterfly_ptr field = our target address
let fakeArr = fakeobj(containerAddr - BUTTERFLY_OFFSET);

// Now:
// fakeArr[0] = arbitrary read from target_addr
// fakeArr[0] = value → arbitrary write to target_addr

function read64(addr) {
    container.butterfly_ptr = itof(addr);  // set butterfly to target
    return fakeArr[0];
}

function write64(addr, value) {
    container.butterfly_ptr = itof(addr);
    fakeArr[0] = value;
}

With read64 and write64, the attacker has arbitrary read/write within the WebContent process — the full RCE primitive needed to proceed to DarkSword Stage 2.


From Memory R/W to Code Execution: Bypassing JIT Cage

Even with arbitrary R/W in the WebContent process, there’s one more obstacle: JIT Cage — Apple’s mitigation that prevents JIT-compiled code from writing to executable (JIT) memory regions.

JIT Cage works by mapping JIT memory regions in a way that only a special hardware-gated path can write to them. Bypassing this requires:

  1. Reading the WebContent process’s memory map using the arbitrary R/W primitive
  2. Finding the base address of WebKit’s binary via ASLR slide calculation
  3. Locating function pointers in writable memory that are called by JavaScript execution paths
  4. Overwriting a function pointer to point to attacker-controlled ROP (Return Oriented Programming) gadget chain

For DarkSword, this step feeds directly into Stage 2 (CVE-2026-20700’s PAC bypass), which handles the pointer authentication hurdle that would otherwise prevent function pointer overwriting.


CVE-2025-31277 in the DarkSword Kill Chain

Here’s exactly how CVE-2025-31277 fits into DarkSword’s full attack flow:

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
30
31
32
33
34
35
36
37
38
39
[Victim loads malicious webpage in Safari]
           │
           │ Hidden iframe loads DarkSword JavaScript
           ▼
[Fingerprinting: check iOS version]
     ├── iOS 18.4 or 18.5? → Use CVE-2025-31277
     └── iOS 18.6 or 18.7? → Use CVE-2025-43529 (fallback)
           │
           ▼ [CVE-2025-31277 path]
[Stage 1: JIT warmup phase]
  JavaScript repeatedly calls target functions
  DFG profiles type usage, JIT-compiles optimized native code
           │
           ▼
[Stage 1: Trigger type confusion]
  Crafted JS violates DFG's type assumption
  Object pointer misread as float (or vice versa)
  Type confusion achieved in DFG JIT compiled code
           │
           ▼
[Stage 1: Build addrof + fakeobj primitives]
  Exploit the confusion to get object addresses (addrof)
  Craft fake objects at controlled memory (fakeobj)
           │
           ▼
[Stage 1: Construct arbitrary R/W]
  Fake JSArray with controlled butterfly pointer
  read64() / write64() functions now operational
  FULL ARBITRARY READ/WRITE IN WEBCONTENTPROCESS
           │
           ▼
[Stage 1: Pre-stage 2 prep]
  Read WebKit ASLR slide
  Locate PAC-related structures in memory
  Prepare data needed for CVE-2026-20700 PAC bypass
           │
           ▼ → Proceeds to Stage 2, 3, 4, 5, 6...
           
[Eventually: Root kernel access + GHOSTBLADE/GHOSTKNIFE/GHOSTSABER deployed]

Historical Context: JIT Type Confusion in JavaScriptCore

CVE-2025-31277 is far from unique — it belongs to a well-documented class of JSC DFG vulnerabilities:

CVEYearTypeNotes
CVE-2018-42332018DFG type confusion via Proxy/CreateThisFirst major exploited DFG bug
CVE-2022-428562022FTL JIT type confusionZero-day, exploited in the wild
CVE-2023-374502023WebKit type confusionExploited same day as disclosure
CVE-2024-232222024Type confusionZero-day, arbitrary code execution
CVE-2025-242012025WebKit OOB write (JIT context)Zero-day, patched March 2025
CVE-2025-312772025DFG JIT type confusion / UAFDarkSword Stage 1
CVE-2025-435292025DFG JIT GC bug (DarkSword fallback)Patched iOS 18.7.3

The pattern is consistent: the DFG JIT’s speculative optimization engine is attacked repeatedly because it is one of the few places in iOS where performance demands require accepting a fundamental tension with security.


Why JIT Compilers Are Perpetually Vulnerable

The Fundamental Tension

JIT optimization relies on trusted assumptions about types to generate fast code. Security requires never trusting input. These two principles are in direct conflict.

Every performance optimization the DFG JIT makes — “this variable is always an integer,” “this array always holds doubles,” “this function never changes its structure” — is a potential type confusion attack vector if an attacker can violate that assumption.

The Complexity Problem

The DFG JIT is one of the most complex subsystems in any software stack. Its optimization passes, speculative compilation, bailout handling, and interaction with the garbage collector involve millions of lines of code. Each interaction between subsystems is a potential vulnerability surface.

The GC interaction alone is notoriously difficult to reason about:

1
2
3
4
5
6
7
Garbage Collector runs concurrently
    ↓
Object freed while DFG code holds reference
    ↓
Same memory reused for different object
    ↓
DFG's stale reference → wrong type → confusion

Apple’s Mitigations and Their Limits

Apple has implemented several mitigations specifically targeting JIT exploitation:

JIT Cage: Prevents writing to JIT-compiled code regions. Effective but bypassable with sufficient arbitrary R/W (as DarkSword demonstrated via CVE-2026-20700).

PAC (Pointer Authentication Codes): Signs function pointers with hardware cryptographic tags. Prevents naive function pointer overwriting. Bypassable via CVE-2026-20700 in dyld.

Gigacage: Restricts where certain sensitive memory regions can be allocated. Raises the bar for butterfly pointer manipulation.

Lockdown Mode’s JIT disable: The strongest mitigation — disabling JIT compilation entirely in Safari under Lockdown Mode. Eliminates the entire attack class at the cost of performance. DarkSword Stage 1 cannot function without JIT.


Patch Analysis: What Apple Fixed

Apple’s advisory stated: “Improved memory handling” — characteristically minimal. Based on the vulnerability class and patch timing (iOS 18.6, August 2025), the fix likely involved one or more of:

Type check hardening:

1
2
3
4
5
6
7
8
9
10
11
12
// Before (vulnerable — pseudocode):
if (node->hasResult() && speculativeType == SpecInt32) {
    // Generate optimized integer code — no re-check at runtime
    generateFastIntegerOperation(node);
}

// After (patched — pseudocode):
if (node->hasResult() && speculativeType == SpecInt32) {
    // Add runtime type guard — verify assumption before use
    addTypeCheck(node, SpecInt32, ExitKind::BadType);
    generateFastIntegerOperation(node);
}

GC write barrier strengthening: Ensuring that when the GC marks an object for collection, any DFG type speculation data referencing that object is immediately invalidated — preventing use-after-free through stale type profiles.

Bailout condition coverage: Adding additional deoptimization triggers for edge cases where the type assumption could be violated in previously unchecked ways.


Detection

For Security Researchers / iOS Forensics

CVE-2025-31277 exploitation occurs entirely in-memory within the JIT-compiled execution context. Classic detection approaches:

Crash logs (partial):

1
2
3
4
5
Settings → Privacy → Analytics → Analytics Data
Look for:
- WebContent crashes near suspicious JavaScript execution
- jsc.crash logs with DFG-related stack frames
- JSC::DFG::* function names in crash traces

Memory analysis:

  • iVerify and similar tools can detect kernel-level anomalies resulting from DarkSword exploitation
  • Direct memory forensics on jailbroken/research devices: look for unusually large WebContent process memory maps
  • Butterfly pointer anomalies in JS heap (requires specialized tooling)

Network behavioral detection:

  • After Stage 1 succeeds, DarkSword proceeds rapidly — look for burst HTTPS traffic from Safari
  • Connections to attacker C2 from WebContent process immediately after page load

For Blue Teams

1
2
3
4
5
6
7
8
9
Network monitoring:
- Alert on HTTPS connections from mobile devices to newly registered domains
- Detect unusual POST requests with large encrypted bodies immediately after page loads
- Flag DNS queries to DarkSword-associated infrastructure (use threat intel feeds)

MDM / Endpoint:
- Enforce minimum iOS version policy
- Alert on devices running iOS < 18.7.6 accessing corporate resources
- Deploy mobile threat detection (Lookout, Zimperium, iVerify)

Mitigation

Patch Status by Platform

PlatformVulnerable RangePatched Version
iOS / iPadOS< 18.618.6 (CVE-2025-31277 fixed)
Safari< 18.618.6
macOS Sequoia< 15.615.6
watchOS< 11.611.6
visionOS< 2.62.6
tvOS< 18.618.6

Note: CVE-2025-31277 was patched in iOS 18.6. DarkSword’s operators switched to CVE-2025-43529 (patched in iOS 18.7.3) for newer devices. Full DarkSword protection requires iOS 18.7.6 or iOS 26.3.1+ which patch all six chain CVEs.

Recommendations

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1. Update ALL Apple devices to latest available iOS
   Priority: iOS 18.7.6 or iOS 26.3.1

2. Enable Lockdown Mode for high-risk individuals
   Disables JIT in Safari → blocks CVE-2025-31277 class entirely

3. Enterprise MDM: 
   Enforce minimum OS version, block non-compliant device access

4. For security researchers:
   Study this class of vulnerability via:
   - WebKit source: https://github.com/WebKit/WebKit
   - JSC DFG documentation: https://docs.webkit.org/Deep%20Dive/JSC/
   - Prior DFG bug writeups (links in references)

Conclusion

CVE-2025-31277 is a textbook example of the JIT compiler security problem: an engine that must make aggressive type assumptions to deliver performance will, eventually, have those assumptions exploited by attackers willing to study its internals.

What elevates CVE-2025-31277 from an interesting browser bug to a critical threat is its weaponization within DarkSword. As Stage 1 of a six-stage full-chain iOS exploit, it provided the initial arbitrary memory access needed to defeat every subsequent security layer — PAC, sandboxes, kernel isolation — ultimately delivering GHOSTBLADE spyware to hundreds of millions of iPhones without any visible indication of compromise.

The lesson for the security community: browser JIT engines are permanent attack surfaces. Apple’s Lockdown Mode — which disables JIT entirely — is the only reliable protection against this class of attack for high-risk individuals. For everyone else: patch aggressively, enable automatic updates, and understand that every unpatched iOS device running Safari is a potential entry point for nation-state-grade exploitation.


References


This post is intended for security researchers, browser security engineers, and mobile threat analysts. All code samples are conceptual and based on publicly documented vulnerability classes. No functional exploit code is provided or implied.

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