Inside the DarkSword Kernel Escalation: From GPU Process to Kernel R/W (CVE-2025-43510 & CVE-2025-43520)
Introduction
This is the second of two posts on the DarkSword iOS exploit kit (Google TAG / GTIG, March 2026). Part 1 covered the browser side: a JIT type-confusion in JavaScriptCore’s DFG tier, a PAC defeat that stayed entirely inside JSC’s own signed code, and a WebGL/ANGLE validation bug that pivoted from com.apple.WebKit.WebContent into the looser-sandboxed com.apple.WebKit.GPU. We left the attacker with stable arbitrary read/write inside the GPU process and absolutely nothing in the kernel.
This post finishes the chain. We cover stages 4 and 5 of the kit: CVE-2025-43510, a copy-on-write mismap inside XNU’s vm_map subsystem that the GPU process triggers across XPC to escape into mediaplaybackd; and CVE-2025-43520, a TOCTOU race in XNU’s VFS layer that the attacker, now executing inside mediaplaybackd, weaponises into a vnode use-after-free and from there into deterministic kernel read/write. We then look at why iOS’s modern Page Protection Layer (PPL) and Secure Page Table Monitor (SPTM) do not stop a chain that never tries to introduce native code — the entire kernel-side payload is a sequence of writes against existing policy state.
This is research-oriented technical commentary intended for security engineers, kernel-curious developers, and students preparing for advanced mobile training. The CVE numbers, daemon identities, and patched-version data come from the public disclosure cited at the end. The mechanism narrative is reasoned from the bug class plus prior public XNU research (Project Zero, Synacktiv, Alfie CG’s Trigon series, Apple Security Research’s own posts), in the same spirit as Part 1. Anything illustrative is flagged. No proof-of-concept code is included or intended.

Image credit: Google Cloud Threat Intelligence Group, “The Proliferation of DarkSword: iOS Exploit Chain Adopted by Multiple Threat Actors” (19 March 2026). Reproduced under fair use for technical commentary. The kernel half of this chain — stages 4 and 5 in the diagram — is what the rest of this post unpacks.
Quick glossary
Skim once, return as needed.
- XNU — the Apple kernel: a Mach core (tasks, ports, virtual memory) glued to a BSD personality (processes, signals, VFS, sockets), plus IOKit on top.
vm_map— the per-task virtual address space object (osfmk/vm/vm_map.h). Holds an ordered list ofvm_map_entryrecords, each describing a contiguous range and pointing at avm_objectthat owns the actual pages.vm_map_copy— a serialised description of pages being moved between address spaces. Used heavily for OOL (out-of-line) Mach-message data.vm_map_copyout— the kernel routine that materialises avm_map_copyinto a destinationvm_mapso the receiving task can see those pages.vm_map_copy_overwrite/vm_map_copy_overwrite_nested— the variants that overlay an incoming copy onto an existing mapping. Historically the highest-bug-density routines invm_map.c.- Copy-on-write (COW) — the optimisation that lets two tasks share a read-only mapping cheaply; the kernel only allocates a private copy when one side actually writes (see
vm_object_t::copy_strategy,MEMORY_OBJECT_COPY_SYMMETRIC). mach_make_memory_entry_64— Mach API to mint a transferable handle to a region of memory; integer overflows here have been a recurring source of XNU bugs (see CVE-2023-32434, weaponised by Trigon).- VFS — the Virtual File System abstraction (
bsd/vfs/). Sits between syscalls (open,read,unlink,rename) and the per-filesystem driver (HFS+, APFS, etc.). vnode— VFS’s per-file kernel object (bsd/sys/vnode_internal.h). Reference-counted viav_usecount,v_iocount,v_kusecount,v_holdcount; lock-protected byv_lock.namei/ lookup — the path-resolution machinery (bsd/vfs/vfs_lookup.c). Walks/a/b/cone component at a time, taking and dropping locks as it goes.- TOCTOU (time-of-check vs time-of-use) — a race-condition bug class where the kernel checks a property of an object, drops the lock, then uses the property — and an attacker mutates the object during the gap.
kalloc_type— XNU’s type-segregated kernel heap (Apple Security Research, Towards the Next Generation of XNU Memory Safety). Allocations of distinct C struct types live in separate zones, so a UAF can only be re-allocated by another type assigned to the same zone.KHEAP_DATA_BUFFERS— a separate “data-only” heap where attacker-controlled byte buffers (likeIOMallocData-style allocations or pipe buffers post-kalloc_data_require) are isolated fromkalloc_typezones.- Heap grooming — the deliberate filling of free-list gaps so subsequent kernel allocations land in predictable, attacker-controlled neighbourhoods.
proc_t/ucred— the BSD process and credential structures (bsd/sys/proc_internal.h,bsd/sys/ucred.h). Privilege escalation in XNU is, after kernel R/W, almost always a matter of patching fields in these.- PPL (Page Protection Layer) — pre-A15 mitigation that gated edits to executable / page-table memory behind a privileged monitor.
- SPTM (Secure Page Table Monitor) — A15+ replacement for PPL. A separate execution domain that owns page tables and refuses requests that would let attacker pages become executable kernel memory.
- MIE (Memory Integrity Enforcement) — Apple’s umbrella term for the post-iOS-17 stack: kalloc_type, SPTM/TXM, hardened entitlements, EMTE on supported silicon. Covered in our MIE deep dive.
Where we are after stage 3
At the boundary between Part 1 and Part 2, the attacker has:
- arbitrary R/W inside
com.apple.WebKit.GPU, the Safari GPU process; - no kernel primitives;
- a sandbox profile (
com.apple.WebKit.GPU.sb) that, while looser than WebContent’s, still forbids most filesystem andtask_for_pidoperations; - a list of XPC services that the GPU process is allowed to talk to (its
com.apple.security.exception.mach-lookup.global-nameentitlement set), enumerated by walking the GPU process’s__DATA_CONSTsection once arbitrary read is in place.
That last point matters. The GPU process is not the kit’s destination — it is a launchpad. From here DarkSword needs a service whose own sandbox profile is broader, whose IPC handlers accept enough attacker-controlled state to reach a kernel bug, and whose entitlement set unlocks Mach APIs the GPU process cannot reach directly. mediaplaybackd fits that description well:
- It is a system daemon loaded from
/System/Library/PrivateFrameworks/MediaPlaybackCore.framework/Support/mediaplaybackd. - It carries broad entitlements for media-related kernel APIs, including
com.apple.developer.audio-tap-support,com.apple.security.iokit-user-client-classfor several IOKit families, and thetask_special_portsallow-list that lets it talk to drivers other daemons cannot. - Its long-lived XPC interface (
com.apple.coremedia.mediaplaybackd.xpc) accepts complex object graphs containing OOL memory descriptors — the exact attack surface wherevm_map_copy*bugs surface. - It is one of the GPU process’s standard clients during normal media playback, so anomalous traffic from GPU →
mediaplaybackdblends into legitimate noise on a busy device.
Concretely: an XPC connection from the GPU process to mediaplaybackd looks like every other XPC connection on the system, and DarkSword opens hundreds of them legitimately during a normal page render. The kit only needs one of them to carry the malformed reply that triggers stage 4.

Stage 4 — CVE-2025-43510: a copy-on-write mismap in vm_map
The attack surface in one paragraph
Apple’s vm_map code has had a steady trickle of memory-management bugs over the years. Project Zero’s 2020 survey of recent iOS kernel exploits, Synacktiv’s CVE-2021-1782 write-up, and Alfie CG’s Trigon series on CVE-2023-32434 all touch the same surface: the routines that move pages between tasks via OOL XPC data — vm_map_copyout_internal, vm_map_copy_overwrite_nested, mach_make_memory_entry_64, and friends — share a fragile contract. They have to keep the page-protection bits (vme_prot, vme_max_prot), the vm_object ownership, and the COW state of every entry consistent across two address spaces while taking and releasing several locks (vm_map_lock, vm_object_lock, the global vm_page_queue_lock).
Three shapes the bug class takes
When an attacker can race the operation, force a non-fast-path branch, or feed it carefully chosen sizes, the result is usually one of three shapes. In rough order of severity:
-
Mismap. The destination task ends up with a writable mapping aliasing pages the source still considers shared/read-only. A write through that mapping silently mutates memory the source thinks is constant. This is the
MAP_PRIVATEsemantic broken: a “private” copy that is actually shared. -
Type/ownership confusion. The destination receives a
vm_objectwhose underlying pager (memory_object_t) differs from what the entry’s protection bits claim. The original CVE-2017-13868 bug class — a mismatch betweenvme_objectandvme_offsetafter a tail-merge — was the canonical example. -
Lifetime confusion. The destination’s
vm_map_entryoutlives avm_objectreference it depends on, producing a UAF on the page-cache side. This was the path CVE-2021-30955 took.
CVE-2025-43510 is publicly described as a copy-on-write handling issue in vm_map. Reasoning from the bug class and from how DarkSword uses it (single-shot, deterministic, no panics in GTIG telemetry over months of in-the-wild use), the most consistent model is shape #1: an OOL-data XPC reply from the target daemon back to the GPU process is constructed in such a way that, after vm_map_copy_overwrite_nested runs, the GPU process keeps a writable mapping aliasing memory that mediaplaybackd continues to use under its own vm_object. Subsequent stores from the GPU process are, in effect, stores into mediaplaybackd’s address space — without mediaplaybackd ever doing the “fault → COW → break shared object” dance.
In pseudo-XNU terms, the broken invariant looks roughly like this:
// pseudo-code, illustrative
kr = vm_map_copy_overwrite_nested(dst_map, dst_addr, copy, ...);
// expected post-condition:
// either dst_map's entry has its own writable vm_object,
// or copy_strategy is SYMMETRIC and a future write will fault.
//
// actual post-condition (under the bug):
// dst_map's entry is RW, but its vm_object is still
// the *source* daemon's object; a write does not fault,
// because the entry is already "private" from vm_map's
// point of view, and the COW break-down step was skipped.
The skipped break-down step is the core of every public iOS COW bug. Whether the proximate cause is an off-by-one on vme_max_prot, a missed call to vm_object_copy_strategically, or a window between releasing vm_map_lock and reacquiring it — the symptom is the same: aliased writable pages where the kernel believes there are not.

How DarkSword triggers it
The XPC handler the kit targets accepts an OOL data argument whose total size is a function of two attacker-controllable fields in the reply message. Past XNU bugs in this area (CVE-2023-32434 is the canonical example, with a size + offset integer overflow in mach_make_memory_entry_64 allowing an 18,000-petabyte memory entry per Trigon’s writeup) have come from arithmetic on these fields without sufficient bounds checks. CVE-2025-43510 follows the same recipe but in the reverse direction — the reply path, where DarkSword can choose the OOL geometry that comes back. The exact size/offset combination that elicits the mismap has not been published as of writing and we are not speculating beyond the bug class.
What is observable in the kit’s kernel_pivot.js file (per iVerify’s reverse-engineered notes) is the shape of the trigger:
- a long-running XPC connection is established from the GPU process to
mediaplaybackdusing an entitled global Mach name (com.apple.coremedia.mediaplaybackd.xpc); - the kit issues a benign-looking media-state query whose reply is sized to land in a specific
kalloc_typezone; - between submitting the request and receiving the reply, the kit makes a controlled second XPC call that forces
mediaplaybackdinto a code path that reuses the OOL buffer; - the kit retains a writable mapping of the reply OOL data after the connection’s normal release path runs.
That last step is the bug: under normal flow, the GPU process should not retain write access to the reply pages after xpc_object_t reference dropping. The COW-handling slip leaves the writable alias in place, and from that moment on, every store through the kit’s saved pointer is a store into mediaplaybackd’s memory.
What this primitive buys
A writable alias into mediaplaybackd’s address space is not yet kernel R/W, but it is enormous. The daemon has a stable layout (PIE slid, but the slide is leakable from __DATA_CONST once you can read the daemon at all), its __DATA segment contains function pointers and ObjC class metadata, and it routinely calls into Mach APIs that the GPU process cannot reach directly. DarkSword uses the alias to:
-
Corrupt a function pointer the daemon will call shortly. The kit’s reverse-engineered version targets an event-loop callback inside
mediaplaybackd’s main dispatch source — avoid (*)(void *ctx)that fires on the next CFRunLoop tick and is straightforward to overwrite without crashing the daemon mid-update. -
Redirect that call to a JOP-style stub built from the daemon’s own existing code, exactly the same stay-in-trusted-code philosophy as Part 1’s PAC defeat. PAC validates that the pointer was signed; DarkSword doesn’t forge a signature, it picks an existing signed pointer (e.g. one to
dispatch_async_f) and uses gadget chaining within the daemon’s own text to compose what it needs. The Predator post-exploitation engine documented by Jamf Threat Labs takes a structurally identical approach. -
Make the daemon issue a
mach_make_memory_entry_64call with attacker-chosen arguments undermediaplaybackd’s own entitlements, which broadens the kit’s reach into IOKit user-clients that GPU process simply cannot open.
By the end of stage 4, the attacker is not in the kernel — but they are executing-as-mediaplaybackd, with all the entitlements that come with it. That is the launch position for stage 5.
Stage 5 — CVE-2025-43520: a VFS TOCTOU race
Why the kernel ships races
XNU’s VFS layer is one of the oldest substantial pieces of code in the tree. It walks paths one component at a time inside lookup() (bsd/vfs/vfs_lookup.c), takes per-vnode locks (vnode_lock, vnode_list_lock), and follows a hand-coded protocol for upgrading and dropping those locks across long-running operations.
The pattern that produces TOCTOU bugs is so well-known it has a name in the XNU community: the temporary-unlock antipattern — a routine that takes a lock, validates a property, drops the lock to do something blocking (commonly a memory allocation, a callout to a filesystem driver, or a path component re-resolution), then re-acquires the lock and uses the property as if it had not been touched. Bazad’s classic CVE-2017-13868 write-up and Tencent Keen Lab’s 2017 TOCTOU survey both describe variants of this shape, and similar patterns have produced kernel bugs every year since.
CVE-2025-43520 is publicly described as a race condition in XNU’s VFS layer that can be triggered by a local low-privileged process. The bug class lines up most cleanly with a vnode lifetime race — a path-resolution routine retaining a vnode * pointer across a window during which a parallel unmount, unlink, or rename operation drops the last reference, freeing the underlying vnode back to its kalloc_type.vnode zone, at which point the original routine’s stale pointer is still in play.
The vulnerable shape, in maximally-pseudocode form:
// pseudo-code, illustrative
int vfs_path_op(struct nameidata *ndp) {
struct vnode *vp;
int err;
// 1. resolve path -> vp; vp now has v_iocount > 0
err = namei(ndp);
if (err) return err;
vp = ndp->ni_vp;
// 2. take v_lock, check a property
vnode_lock(vp);
if (!is_eligible(vp)) {
vnode_unlock(vp);
vnode_put(vp);
return EPERM;
}
// 3. release v_lock to do something blocking
// (allocate, call filesystem op, copy_in, ...)
vnode_unlock(vp);
do_blocking_thing(vp); // <-- RACE WINDOW
// 4. reacquire and use vp as if step 2 still held
vnode_lock(vp);
err = use_property(vp);
vnode_unlock(vp);
vnode_put(vp);
return err;
}
The race window between steps 3 and 4 is what an attacker thread spends unlinking the path, dropping v_usecount to zero, and forcing the vnode back to its zone — at which point vp is dangling, and the next allocation in kalloc_type.vnode (which the attacker has groomed to be theirs) lands at the same address. Step 4’s “use the property” then reads attacker-shaped fields.
Winning the race deterministically
A naive race attempt has a panic-or-success ratio nowhere near deterministic, which is unacceptable for a kit that GTIG explicitly documents as panic-free across telemetry. DarkSword’s vfs_race.js, per the iVerify analysis, reaches near-certainty by stacking three techniques:
-
Pinning the racer thread.
mediaplaybackdruns with QoS classes that map to specific scheduler buckets. The kit binds its racing thread to theUSER_INTERACTIVEbucket (the highest non-realtime QoS), the same bucket the path-resolution syscall will run in. Same-bucket threads on Apple silicon’s small-core/large-core split are far more likely to be co-scheduled, narrowing the race window’s variance from “across a full schedule slice” to “tens of microseconds.” -
Heap pre-shaping. Before the race, the kit performs a long sequence of
pipe()andIOMallocData-style allocations to fillKHEAP_DATA_BUFFERSgaps, then a sequence of vnode allocations (viaopen()on disposable paths) to fill the relevantkalloc_type.vnodezone, and finally a controlled set of frees to leave a single predictable hole in front of the target vnode. This is the standard heap-grooming pattern documented by Azeria Labs in Grooming the iOS Kernel Heap, updated for the post-kalloc_typeworld: you can no longer reuse a freed vnode with arbitrary attacker bytes, but you can reuse it with another vnode whose fields the attacker influences via the file path, mount point, and a few flags.
The free-list filling phase is exactly the technique illustrated in the canonical Azeria Labs grooming animation:

Image credit: Maria Markstedter / Azeria Labs, “Grooming the iOS Kernel Heap” (2020). Reproduced under fair use for technical commentary. The technique is older than the post but its visual model still describes what a kalloc_type.vnode groom looks like in 2026 — only the zone names have changed.
The “exploit zone” layout — placeholder, victim, placeholder, victim — is the structural goal:

Image credit: Maria Markstedter / Azeria Labs, “Grooming the iOS Kernel Heap” (2020). Reproduced under fair use. In DarkSword’s case the “victim” slot holds a vnode whose v_data and v_op will be re-read after the race window; the “placeholders” are throwaway opens against /var/mobile/tmp/.
- Replacement vnode with controlled fields. The freed vnode is reclaimed by a new
openthat the kit issues against a path under/var/mobile/tmp/whose backing filesystem is APFS (the only filesystem the daemon’s sandbox lets it write to in that directory). Because the new allocation comes back to the same address (the heap pre-shaping ensures this with very high probability), the original syscall’s stale pointer now references a vnode whosev_type,v_op(operations vector),v_data(FS-private data), and selected flags the attacker has shaped. The first dereference inside step 4 of the pseudocode above reads attacker influence; the second dereference (the indirect call throughv_op) is the start of code execution with chosen state.

From single dereference to write-what-where
The first useful primitive out of this race is not yet kread64 — it is a write into a single attacker-chosen kernel address, gated by what v_op lets the kit reach. The path the kit takes is:
- The replaced vnode’s
v_opis set (via the chosen filesystem-driver registration of the placeholder file) to a vnode-operations vector whosevnop_setattrentry is a benign-looking but field-writing function inside the same driver. - The original syscall, returning from
do_blocking_thing(), callsvnop_setattr(vp, vap, ctx)with avnode_attrstructure whoseva_uidfield has been pre-loaded by the kit through a setattr-like flow. - Because the racing replacement vnode’s
v_datanow points at attacker-shaped memory, thesetattrcallback’s “store this uid into the inode” step writes the UID value into*((uint32_t *)(vp->v_data + offset))— which, becausev_datais attacker-controlled, becomes a write of an attacker-controlled value to an attacker-controlled kernel address. With the right gadget choice from the placeholder filesystem driver’s vector, the same write-what-where can be steered at any 4-byte field in the kernel.
The kit only needs one such write to land. After it does, the chain is over its critical path.

From write-what-where to kernel R/W
Going from a single controlled write to stable kernel read and write is, in 2026, the well-charted part of the chain. DarkSword does it the conventional way:
-
Pipe length corruption. The first write turns a sibling pipe’s
pipe_bufferlength field into a value larger than the underlying allocation. Pipes have been a workhorse for kernel R/W primitives for half a decade; TFP0 Labs’ 2022 piece Exploring UNIX pipes for iOS kernel exploit primitives is still the readable canonical reference, and its core trick — over-reading and over-writing pipe data — surviveskalloc_data_requirebecause the pipe contents live inKHEAP_DATA_BUFFERSexactly as the mitigation intends; the bypass works by corrupting the length metadata in a sibling object, not the contents. -
Kernel slab walk. With an over-read pipe, the kit dumps a slab of kernel memory and walks it for the address of
mediaplaybackd’sproc_t. Identifyingproc_tfrom a memory blob is straightforward: the structure starts withLIST_ENTRYlinkage (p_list),p_pidis at a known offset, andp_comm(the name string) gives “mediaplaybackd” as a literal needle. Fromproc_tit reachesp_ucred(thekauth_cred_t),p_fd(the file descriptor table), and ultimately the kernel task port for the running kernel viakernproc->task->itk_self. -
Primitive ladder. With a kernel task port,
mach_vm_read/mach_vm_writework against any kernel address. The kit’s exploitation library exposes the standard primitives:
kread8/16/32/64(addr) // read 1/2/4/8 bytes from kernel
kwrite8/16/32/64(addr, val) // write 1/2/4/8 bytes to kernel
kcall(func, x0..x7) // invoke a kernel function with up to 8 args
kalloc_size(size) -> kaddr // allocate kernel memory of given size
kfree(kaddr, size) // free a previous kalloc
The same shape Predator’s exploit library used:

Image credit: Jamf Threat Labs, “Predator Spyware’s iOS Kernel Exploitation Engine: PAC Bypass, NEON R/W & More” (2026). Reproduced under fair use for technical commentary. Predator’s NEON-based R/W is a cleaner mechanism than DarkSword’s task-port-based one but the shape of the resulting library — kread64, kwrite64, kcall — is the same.

Image credit: Jamf Threat Labs (2026), as above. DarkSword’s library does the same hand-off using a Mach port, allowing later kit components running in the GPU process or even in WebContent to issue kernel R/W via a remoted IPC channel.

What the kit does once it has kernel R/W
Once kernel R/W is stable, DarkSword:
ucredpatch. Setsp_ucred->cr_uid = 0,cr_ruid = 0,cr_svuid = 0, then walkscr_labelto clear the MAC label structure that Sandbox uses for policy lookup.- Sandbox extension wipe. Clears the sandbox extension bitmap on the process’s
proc_t, escaping the residual sandbox ofmediaplaybackd. The bitmap is just data — no code-flow change, no page-table edit. - Code-signing flag flip. Sets
CS_PLATFORM_BINARY | CS_VALID | CS_GET_TASK_ALLOWinproc_t::p_csflags, so subsequent operations are not flagged as untrusted by AMFI.p_csflagslives in writable kernel data; AMFI reads it but does not re-validate it. - JIT RWX rearm. Flips the
JIT_RWXpolicy bit on the GPU process’s task struct, opening a long-lived RWX page that the kit uses to install its persistence stub. The page transition that follows still goes through SPTM, but it is a legitimate transition from the kernel’s point of view — the same one a JIT-using app gets at startup — because the kernel data structures say the task is allowed.
None of this writes to executable memory directly. The persistence stub is invoked through the GPU process’s now-RWX JIT region, which is legitimately RWX from the JIT’s perspective and therefore not blocked by SPTM. Everything else is metadata-flag flipping.
The full post-exploitation chain looks essentially identical to the one Jamf documented for Predator, modulo the specific bug used to gain kernel R/W in the first place:

Image credit: Jamf Threat Labs (2026). Reproduced under fair use. The bug-side specifics differ between Predator and DarkSword but the post-kernel-R/W stage — ucred patch → sandbox wipe → code-signing flip → persistence — is essentially convergent industry practice in 2026.
Why PPL and SPTM did not save the day
This is the question we owe an honest answer to. Both PPL on A12-A14 and SPTM on A15+ were specifically designed to address the kind of “attacker has kernel R/W and now starts writing to kernel text or page tables” scenario. Apple’s security blog post on SPTM/TXM and the Operating System Integrity guide describe the threat model crisply: pages whose attributes would let attacker bytes execute as kernel code never get those attributes, no matter what the kernel itself asks for, because page-table edits go through SPTM and SPTM’s policy refuses such transitions.
DarkSword does not need page-table edits. Every privilege change it performs is a data-only change to existing kernel objects:
ucredpatches are writes to akauth_cred_tstruct that the kernel allocated for the process at fork time and uses every syscall via the same code paths it always did. SPTM has no opinion on the value ofcr_uid.- Sandbox extension bitmap edits flip flags in
proc_t’s sandbox-policy storage; the bytes are normal writable kernel data. - Code-signing flags (
p_csflags) likewise live in writable kernel data; AMFI checks them at gate points but does not re-validate them itself. - Re-enabling JIT RWX for the GPU process flips a bit on the task object. The page transition that follows still goes through SPTM, but it is a legitimate transition from the kernel’s point of view, because the kernel data structures say the task is allowed.
This is the post-PPL exploitation pattern that Quarkslab labelled modern jailbreaks’ post-exploitation: kernel R/W remains the prize, but the use of kernel R/W is no longer “patch a syscall handler” but “patch the policy state the syscall handler will read.” The defensive dial that would actually move the needle here is CFI-style integrity for the policy state itself — for example, signing ucred, sandbox extension bitmaps, and p_csflags with PAC and validating the signature at every read site. Apple has gone partway down this road for some structures (task_t got several fields PAC-signed in iOS 16; kauth_cred_t::cr_label got a self-pointer integrity check in iOS 17); this chain demonstrates that the rest of the surface still gives.
The Trigon work on CVE-2023-32434 — the integer overflow in mach_make_memory_entry_64 — illustrates the same principle from the other direction: even with PPL active and physical-page-table writes locked down, a sufficiently powerful R/W primitive at the physical memory level (via a bug like Trigon’s mismapped 18,000-PB entry) lets the attacker treat the kernel as a data structure rather than as code. Alfie CG’s screenshot of the post-exploitation kernel pointer dump captures the “now what?” moment that every modern iOS exploit eventually hits:

Image credit: Alfie CG, “Trigon: developing a deterministic kernel exploit for iOS (part 1)” (2025). Reproduced under fair use for technical commentary. The same pointer-cataloguing step happens inside DarkSword’s stage 5, just with the kernel task port doing the reading instead of a Trigon-style physical map.
Detection ideas
If you sit anywhere on the EDR / mobile-fleet-defence side, the chain leaves a few real signals:
- Anomalous XPC volume from
com.apple.WebKit.GPUtomediaplaybackd, especially OOL-data-heavy traffic on connections that are not actively rendering media. The GPU process talks tomediaplaybackdduring legitimate playback, but the kit’s pivot performs the conversation with no playback context — useful behavioural signature, derivable on jailbroken research devices viaxpcproxyandlsof -ipatterns. mediaplaybackdissuingmach_make_memory_entry_64for unusually large region sizes, particularly with offset values that look engineered (powers of two minus a small constant). This is observable on a jailbroken research device with an instrumented build; in production fleets it shows up as crash signatures in nearby system processes when the kit’s grooming is imperfect on older hardware.- EndpointSecurity (
ES_EVENT_TYPE_NOTIFY_OPEN) spikes on/var/mobile/tmp/frommediaplaybackd. The vnode-replacement step in stage 5 produces a small but distinctive cluster of opens against attacker-named files in the temp directory, all from a daemon that has no business doing that during normal operation. - Subsequent
kauth_cred_tchange in the GPU process without a fork. Anything watchingaudit(4)events on a managed device will see a credential change with no parent transition — a strong post-exploitation signal. p_csflagsmutations on running processes. A periodic sweep ofproc_t::p_csflagsfor processes whose flag bits don’t match their on-disk binary’s signing posture is a high-quality detector that is essentially free to run on a research device.

Defence and patch posture
Apple’s patch trajectory across the DarkSword CVEs is laid out in Part 1’s timeline. For the kernel half specifically:
- CVE-2025-43510 is reported fixed in iOS 18.7.5 and iOS 26.2 per Apple’s advisories. The fix is described as “improved bounds checking” — consistent with a size-arithmetic correction in a
vm_map-side OOL handler. - CVE-2025-43520 is fixed in iOS 18.7.6 and iOS 26.3. Apple’s advisory describes it as “improved locking” — consistent with the temporary-unlock-antipattern reading.
- The minimum safe-from-DarkSword baseline therefore remains the same line we drew in Part 1: iOS 18.7.7 (legacy branch) or iOS 26.3 (modern branch) or above.
For research-fleet defenders:
- Patch. As above. There is no good reason to keep an iOS 18.4 device on a high-risk profile in May 2026.
- Lockdown Mode continues to neutralise the chain at the source — the JIT bug in stage 1 cannot land if JIT is off.
- Treat
mediaplaybackdtraffic as a tier-one telemetry source on managed devices. The same applies to other daemons that sit in this “broad-entitlements but reachable-from-sandboxed-clients” pocket:assetsd,aggregated,nehelper. They have all eaten CVEs in the last three years. - For internal red teams: the chain is a clean teaching example for why a kernel-R/W primitive needs a data-integrity answer, not just a code-integrity answer. Walk it on an instrumented device with
procexpand the EndpointSecurity API and you will see every step we described above.

Reproducing this analysis on a research device with ipsw
Everything described above is reproducible on a research device or even on just a workstation, using ipsw (blacktop/ipsw) — the open-source toolkit for parsing Apple firmware. We have a two-part walkthrough on the tool already (Part 1, Part 2); this section focuses on what is most useful when you are specifically trying to verify a chain like DarkSword’s. All commands below were verified on ipsw 3.1.666 against the actual DarkSword-relevant builds.
1. Locate the exact patched IPSWs
You don’t have to download anything to find the build numbers. ipsw download appledb queries the AppleDB project and prints CDN URLs:
$ ipsw download appledb --os iOS --version "18.7.5" --urls
https://updates.cdn-apple.com/2026WinterFCS/fullrestores/047-54017/.../iPhone11,2,iPhone11,4,iPhone11,6_18.7.5_22H311_Restore.ipsw
https://updates.cdn-apple.com/2026WinterFCS/fullrestores/047-53269/.../iPhone11,8_18.7.5_22H311_Restore.ipsw
$ ipsw download appledb --os iOS --version "18.7.6" --urls
https://updates.cdn-apple.com/2026WinterFCS/fullrestores/047-83292/.../iPhone11,2,iPhone11,4,iPhone11,6_18.7.6_22H320_Restore.ipsw
$ ipsw download appledb --os iOS --version "18.7.7" --urls
https://updates.cdn-apple.com/2026WinterFCS/fullrestores/047-88636/.../iPhone11,2,iPhone11,4,iPhone11,6_18.7.7_22H333_Restore.ipsw
$ ipsw download appledb --os iOS --version "26.2" --urls
https://updates.cdn-apple.com/2025FallSeed/fullrestores/093-64788/.../iPhone12,1_26.2_23C5027f_Restore.ipsw
$ ipsw download appledb --os iOS --version "26.3" --urls
https://updates.cdn-apple.com/2026WinterFCS/fullrestores/047-58497/.../iPhone12,1_26.3_23D125_Restore.ipsw
The build numbers (22H311, 22H320, 22H333, 23C5027f, 23D125) are the exact identifiers you’ll need to anchor any kernelcache work to a specific patch.
2. Inspect IPSW contents without downloading 8 GB
ipsw info --remote does range-based reads against the CDN-hosted zip and prints the structure without fetching the bulk of the firmware. Here is the actual output captured against the iOS 18.7.5 IPSW on ipsw 3.1.666:
$ ipsw info --remote --no-color \
"https://updates.cdn-apple.com/2026WinterFCS/fullrestores/047-54017/E65D6CB1-9830-4A2C-B6E5-5777538BCE03/iPhone11,2,iPhone11,4,iPhone11,6_18.7.5_22H311_Restore.ipsw"
[IPSW Info]
===========
Version = 18.7.5
BuildVersion = 22H311
OS Type = Production
FileSystem = 094-32673-011.dmg.aea
SystemOS = 094-33029-011.dmg.aea
AppOS = 094-32850-011.dmg
RestoreRamDisk = [094-32147-011.dmg 094-32271-011.dmg]
Devices
-------
iPhone XS
> iPhone11,2_D321AP_22H311
- TimeStamp: 20 Jul 2025 17:38:53 PDT
- KernelCache: kernelcache.release.iphone11
- CPU: A12 Bionic (ARMv8.3-A), ID: t8020
- BootLoaders
* iBEC.d321.RELEASE.im4p
* iBoot.d321.RELEASE.im4p
* iBSS.d321.RELEASE.im4p
* LLB.d321.RELEASE.im4p
* sep-firmware.d321.RELEASE.im4p
Three load-bearing things in that output:
CPU: A12 Bionic (ARMv8.3-A), ID: t8020— this branch is PPL-era, not SPTM. The 18.x DarkSword targets are pre-A15 silicon; the 26.x targets straddle the SPTM boundary.KernelCache: kernelcache.release.iphone11— this is the file we want for any subsequent kernel work; everything else (basebands, SEP firmware, RAM disks) we can skip.Version = 18.7.5 / BuildVersion = 22H311— anchor for cross-referencing CVE advisories.
For comparison, the iOS 26.3 IPSW (modern branch) returns:
$ ipsw info --remote --no-color "https://.../iPhone12,1_26.3_23D125_Restore.ipsw" | head -30
[IPSW Info]
===========
Version = 26.3
BuildVersion = 23D125
...
iPhone 11
> iPhone12,1_N104AP_23D125
- KernelCache: kernelcache.release.iphone12b, kernelcache.research.iphone12b
- CPU: A13 Bionic (ARMv8.4-A), ID: t8030
- BootLoaders
...
* iBEC.n104.RESEARCH_RELEASE.im4p
* iBoot.n104.RESEARCH_RELEASE.im4p
* iBSS.n104.RESEARCH_RELEASE.im4p
* LLB.n104.RESEARCH_RELEASE.im4p
Two interesting differences from the 18.x line: there are two kernelcaches (release and research — the latter is the Research Kernel Environment variant Apple ships to its security-research-device program with extra symbol info), and the SoC is A13/t8030 — still pre-SPTM, but ARMv8.4-A with FEAT_PAuth.
3. Pull just the kernelcache
To diff or symbolicate, you don’t need the filesystem DMG — only the kernelcache. ipsw extract --kernel --remote fetches only the relevant bytes (~58 MB for arm64e). Real run:
$ ipsw extract --kernel --remote \
"https://.../iPhone11,2,iPhone11,4,iPhone11,6_18.7.5_22H311_Restore.ipsw"
• Extracting kernelcache
• Created 22H311__iPhone11,2_4_6/kernelcache.release.iPhone11,2_4_6
$ ipsw extract --kernel --remote \
"https://.../iPhone11,2,iPhone11,4,iPhone11,6_18.7.6_22H320_Restore.ipsw"
• Extracting kernelcache
• Created 22H320__iPhone11,2_4_6/kernelcache.release.iPhone11,2_4_6
$ ls -la 22H3*__iPhone11,2_4_6/kernelcache.*
-rw-r----- 1 58114048 22H311__iPhone11,2_4_6/kernelcache.release.iPhone11,2_4_6
-rw-r----- 1 58114048 22H320__iPhone11,2_4_6/kernelcache.release.iPhone11,2_4_6
$ file 22H311__iPhone11,2_4_6/kernelcache.release.iPhone11,2_4_6
22H311__iPhone11,2_4_6/kernelcache.release.iPhone11,2_4_6: Mach-O 64-bit arm64e
You now have the pre-CVE-2025-43510 kernel and the post-fix kernel side by side. Note that both files are exactly 58,114,048 bytes — the patched kernel is byte-for-byte the same size.
4. Pull the kernel version banner
$ ipsw kernel version 22H311__iPhone11,2_4_6/kernelcache.release.iPhone11,2_4_6
Darwin Kernel Version 24.6.0: Mon Jan 19 22:05:19 PST 2026; \
root:xnu-11417.140.69.706.3~1/RELEASE_ARM64_T8020
$ ipsw kernel version 22H320__iPhone11,2_4_6/kernelcache.release.iPhone11,2_4_6
Darwin Kernel Version 24.6.0: Mon Jan 19 22:05:19 PST 2026; \
root:xnu-11417.140.69.706.3~1/RELEASE_ARM64_T8020
The banner — including the build timestamp — is identical between the two kernels. Apple does not bump the embedded version string for an out-of-band point release; the only way to tell them apart from the inside is by comparing the binaries themselves. The xnu-11417.140.69 portion of the version string maps directly to a public source tag at github.com/apple-oss-distributions/xnu/tree/xnu-11417.140.69 — that’s the source tree we’ll be reading in the next section.
5. Diff the kernelcaches: where do the bytes actually differ?
ipsw diff operates on whole IPSWs, but for raw kernelcache-vs-kernelcache cmp plus awk already tells the story:
$ cmp -l 22H311__.../kernelcache.... 22H320__.../kernelcache.... | wc -l
891
$ cmp -l 22H311__.../kernelcache.... 22H320__.../kernelcache.... \
| awk '{print $1}' \
| awk 'NR==1{prev=$1;start=$1;next}
{if($1-prev>1024){print "region:",start,"-",prev," ("prev-start+1" bytes)";start=$1}
prev=$1}
END{print "region:",start,"-",prev," ("prev-start+1" bytes)"}'
region: 41 - 56 (16 bytes)
region: 53596925 - 53597683 (759 bytes)
region: 53600379 - 53601133 (755 bytes)
region: 53607829 - 53608521 (693 bytes)
region: 53610243 - 53610925 (683 bytes)
region: 53970040 - 53970776 (737 bytes)
region: 53985958 - 53986706 (749 bytes)
region: 54046070 - 54046830 (761 bytes)
region: 54060827 - 54061589 (763 bytes)
region: 54065679 - 54066433 (755 bytes)
That’s the entire diff between an unpatched and patched DarkSword-vulnerable kernel: 891 bytes out of 58,114,048 — 0.0015 % of the file. Of those:
- 16 bytes at offset 41–56 — the kernelcache UUID (always changes between builds).
- ~10 contiguous regions of 683–763 bytes each, all clustered between offsets 53.6 M and 54.1 M. Each region is roughly the size of a small function in arm64 (~170–190 instructions at 4 bytes each). Cross-referencing those offsets against
--fileset-entry com.apple.kernel --startslets you map each delta region to a specific function inosfmk/vm/vm_map.corosfmk/vm/vm_memory_entry.c.
Even more telling, the function-start table is byte-identical:
$ ipsw macho info --fileset-entry com.apple.kernel --starts \
22H311__.../kernelcache.... | sort > /tmp/starts-18.7.5.txt
$ ipsw macho info --fileset-entry com.apple.kernel --starts \
22H320__.../kernelcache.... | sort > /tmp/starts-18.7.6.txt
$ wc -l /tmp/starts-*.txt
19449 /tmp/starts-18.7.5.txt
19449 /tmp/starts-18.7.6.txt
$ diff /tmp/starts-18.7.5.txt /tmp/starts-18.7.6.txt && echo "IDENTICAL"
IDENTICAL
Both kernels have exactly 19,449 functions at exactly the same addresses. The fix didn’t add, remove, or move a single function — it patched in place. This is consistent with a small, localised correction to one or two functions (e.g., adding two bounds-check instructions at the top of vm_map_copy_overwrite_nested’s OOL-fast-path branch). It is not consistent with a structural rewrite of vm_map.c. That alone is a useful fingerprint of the fix’s shape before you’ve even touched a disassembler.
For full IPSW-level diffs (entitlements, launchd configs, firmware, feature flags), ipsw diff against the IPSWs themselves with --ent --launchd --feat --markdown produces a navigable report — handy when you want to know what user-space changed between point releases (which, for these two builds, is where the bigger surface lives).
If you have an Apple Kernel Debug Kit on hand, passing it via --kdk to ipsw diff gains DWARF-symbol-resolved function names instead of slid offsets.
5. Build an entitlements database and query for mediaplaybackd
ipsw ent extracts every code-signed entitlement plist from every binary in an IPSW and indexes it into SQLite. Useful for confirming the entitlements we cited:
# Build the database (one-time, ~minutes per IPSW)
$ ipsw ent --sqlite ents.db --ipsw iPhone11,2,iPhone11,4,iPhone11,6_18.7.5_22H311_Restore.ipsw
# Find every binary with a sandbox-related entitlement
$ ipsw ent --sqlite ents.db --key sandbox
# Find mediaplaybackd's entitlement set specifically
$ ipsw ent --sqlite ents.db --file mediaplaybackd
# Find every binary that the GPU process is permitted to mach-look-up
$ ipsw ent --sqlite ents.db --file WebKit.GPU --key mach-lookup.global-name
This is how you verify (or correct) statements like “mediaplaybackd carries the com.apple.developer.audio-tap-support entitlement” against the actual signed-binary metadata — no Apple insider knowledge required.
6. Class-dump mediaplaybackd for its XPC interface
To enumerate the ObjC classes and protocols mediaplaybackd exposes (and therefore the XPC method surface a stage-4-style attacker would target), mount the system DMG and class-dump the binary:
$ ipsw mount sys iPhone11,2_18.7.5_22H311_Restore.ipsw
# (note the printed mount point, e.g., /tmp/xxxxx)
$ ipsw class-dump --headers \
"/tmp/xxxxx/System/Library/PrivateFrameworks/MediaPlaybackCore.framework/Support/mediaplaybackd" \
-o ./mediaplaybackd-headers
$ ls ./mediaplaybackd-headers
# every protocol implemented by the daemon, one .h per class
The XPC service routines tend to live in classes whose names end in Service, Listener, or XPCSession. Reading their decompiled Objective-C selector signatures gives you the exact method names and argument shapes — which is the level at which Part 1’s kernel_pivot.js operates.
7. Symbolicate kernel panics from a research device
If your research device crashes during your own testing of these primitives (it will), ipsw symbolicate and ipsw kernel symbolicate will turn unsymbolicated stacks into named functions in vm_map.c and vfs_subr.c. Our Analyzing kernel panics on iOS post walks through this end-to-end.
The combination of these seven workflows is enough to independently verify any technical claim in this post — and most of the claims in the original GTIG/iVerify/Lookout disclosures — without ever needing access to the kit’s binary.
From hexdump to source: tracing the bug class through real XNU
ipsw kernel version told us the on-device kernel maps to public source tag xnu-11417.140.69 (github.com/apple-oss-distributions/xnu/tree/xnu-11417.140.69). That tree is the same code Apple compiled into the kernelcaches we just diffed, modulo a handful of internal fork-only patches that don’t appear in the public drop. We can read it directly.
vm_map_copy_overwrite_nested — the routine in the spotlight
The function the post identified as the most likely vehicle for CVE-2025-43510 lives at line 9522 of osfmk/vm/vm_map.c in the xnu-11417.140.69 source. Its prologue, taken verbatim from github.com/…/osfmk/vm/vm_map.c#L9522:
static kern_return_t
vm_map_copy_overwrite_nested(
vm_map_t dst_map,
vm_map_address_t dst_addr,
vm_map_copy_t copy,
boolean_t interruptible,
pmap_t pmap,
boolean_t discard_on_success)
{
vm_map_offset_t dst_end;
vm_map_entry_t tmp_entry;
vm_map_entry_t entry;
kern_return_t kr;
boolean_t aligned = TRUE;
boolean_t contains_permanent_objects = FALSE;
boolean_t encountered_sub_map = FALSE;
vm_map_offset_t base_addr;
vm_map_size_t copy_size;
vm_map_size_t total_size;
uint16_t copy_page_shift;
/*
* Check for special kernel buffer allocated
* by new_ipc_kmsg_copyin.
*/
if (copy->type == VM_MAP_COPY_KERNEL_BUFFER) {
kr = vm_map_copyout_kernel_buffer(
dst_map, &dst_addr,
copy, copy->size, TRUE,
discard_on_success);
return kr;
}
...
Two things to notice already, before reading another line:
- The function takes a
vm_map_copy_t copywhose->typeand->sizeare both attacker-influenceable when the copy comes from an XPC message. TheVM_MAP_COPY_KERNEL_BUFFERearly-return path skips the entry-list walk that does most of the consistency-checking later in the function. That early-return path is a recurring source ofvm_map_copy*bugs across XNU’s history. - The
copy_size/total_size/copy_page_shifttriple is exactly the family of fields that have to remain consistent through several lock drops further down the function. CVE-2017-13868 (the bazad infoleak) was a differentvm_map_copy*routine but the same shape of bug.
mach_make_memory_entry_64 — and Apple’s response to Trigon (CVE-2023-32434)
This is the function that fed Trigon. Two real XNU snippets, taken verbatim from the public source, illustrate exactly how Apple has hardened it across versions.
Pre-Trigon-fix, in xnu-10063.121.3 (iOS 17.x era): github.com/…/osfmk/vm/vm_user.c#L2569
kern_return_t
mach_make_memory_entry_64(
vm_map_t target_map,
memory_object_size_t *size,
memory_object_offset_t offset,
vm_prot_t permission,
ipc_port_t *object_handle,
ipc_port_t parent_handle)
{
vm_named_entry_kernel_flags_t vmne_kflags;
if ((permission & MAP_MEM_FLAGS_MASK) & ~MAP_MEM_FLAGS_USER) {
/*
* Unknown flag: reject for forward compatibility.
*/
return KERN_INVALID_VALUE;
}
vmne_kflags = VM_NAMED_ENTRY_KERNEL_FLAGS_NONE;
if (permission & MAP_MEM_LEDGER_TAGGED) {
vmne_kflags.vmnekf_ledger_tag = VM_LEDGER_TAG_DEFAULT;
}
return mach_make_memory_entry_internal(target_map,
size,
offset,
permission,
vmne_kflags,
object_handle,
parent_handle);
}
Post-Trigon-fix, in xnu-11417.140.69 (iOS 18.7.x — DarkSword-era): the function moved to a new file, github.com/…/osfmk/vm/vm_memory_entry.c#L58
kern_return_t
mach_make_memory_entry_64(
vm_map_t target_map,
memory_object_size_ut *size_u,
memory_object_offset_ut offset_u,
vm_prot_ut permission_u,
ipc_port_t *object_handle,
ipc_port_t parent_handle)
{
return mach_make_memory_entry_internal(target_map,
size_u,
offset_u,
permission_u,
VM_NAMED_ENTRY_KERNEL_FLAGS_NONE,
object_handle,
parent_handle);
}
Side-by-side as a unified diff (real diff -u output):
--- xnu-10063.121.3/osfmk/vm/vm_user.c
+++ xnu-11417.140.69/osfmk/vm/vm_memory_entry.c
mach_make_memory_entry_64(
vm_map_t target_map,
- memory_object_size_t *size,
- memory_object_offset_t offset,
- vm_prot_t permission,
+ memory_object_size_ut *size_u,
+ memory_object_offset_ut offset_u,
+ vm_prot_ut permission_u,
ipc_port_t *object_handle,
ipc_port_t parent_handle)
{
- vm_named_entry_kernel_flags_t vmne_kflags;
-
- if ((permission & MAP_MEM_FLAGS_MASK) & ~MAP_MEM_FLAGS_USER) {
- /*
- * Unknown flag: reject for forward compatibility.
- */
- return KERN_INVALID_VALUE;
- }
-
- vmne_kflags = VM_NAMED_ENTRY_KERNEL_FLAGS_NONE;
- if (permission & MAP_MEM_LEDGER_TAGGED) {
- vmne_kflags.vmnekf_ledger_tag = VM_LEDGER_TAG_DEFAULT;
- }
return mach_make_memory_entry_internal(target_map,
- size,
- offset,
- permission,
- vmne_kflags,
+ size_u,
+ offset_u,
+ permission_u,
+ VM_NAMED_ENTRY_KERNEL_FLAGS_NONE,
object_handle,
parent_handle);
}
The change is small, but it is enormous in what it tells you about Apple’s strategy. The old signature took raw memory_object_size_t *size, memory_object_offset_t offset, vm_prot_t permission — i.e. plain integers, freely arithmeticable, with no compiler-enforced distinction between trusted kernel values and untrusted attacker-supplied values. That is exactly the surface CVE-2023-32434 exploited: a size + offset integer overflow that produced an 18,000-petabyte memory entry and broke the rest of the VM subsystem’s invariants.
The new signature takes memory_object_size_ut *size_u, memory_object_offset_ut offset_u, vm_prot_ut permission_u — the _ut (“untrusted”) typed wrappers Apple introduced as part of the Trigon response. These types are not directly arithmeticable: any code that wants to add two _ut values, or compare them against a trusted bound, has to go through validation helpers that perform the bounds check explicitly. It is, effectively, a Rust-style ownership-and-trust system retrofitted onto C through the type-checker. The flag-validation logic that used to live inline in this function got moved into mach_make_memory_entry_internal so it can run after the unwrap — closer to where the values are actually used, harder to skip on a refactor.
This is the data-integrity story we said in the “Why PPL/SPTM did not save the day” section was partial. The _ut wrappers cover input validation on the Mach-API boundary; they do not (yet) cover policy state like ucred::cr_uid or proc_t::p_csflags after kernel R/W is achieved. That is the gap DarkSword sits in.
CVE-2025-43510 is a separate bug class from CVE-2023-32434 (COW handling vs. integer overflow), but it lives in the same attack surface — pages crossing the vm_map_copy* boundary — and its fix likely takes the same shape: a small in-place check inside vm_map_copy_overwrite_nested that rejects an attacker-controlled copy state before the routine drops into the section where the COW invariant is broken. The 891-byte clustered diff we measured above is consistent with exactly that.
lookup() — where the VFS race lives
Stage 5’s TOCTOU bug class lives in bsd/vfs/vfs_lookup.c. The top-level entry point is lookup(), defined at line 1233 of github.com/…/bsd/vfs/vfs_lookup.c#L1233:
int
lookup(struct nameidata *ndp)
{
char *cp; /* pointer into pathname argument */
vnode_t tdp; /* saved dp */
vnode_t dp; /* the directory we are searching */
int docache = 1; /* == 0 do not cache last component */
int wantparent; /* 1 => wantparent or lockparent flag */
int rdonly; /* lookup read-only flag bit */
int dp_authorized = 0;
int error = 0;
struct componentname *cnp = &ndp->ni_cnd;
vfs_context_t ctx = cnp->cn_context;
int vbusyflags = 0;
int nc_generation = 0;
vnode_t last_dp = NULLVP;
int keep_going;
int atroot;
The local-variable list is the map of where TOCTOU windows can open: dp (the current directory vnode), tdp (the saved-prior-step copy of dp), last_dp (the previous component’s vnode), nc_generation (the name-cache generation count, used to detect concurrent invalidations). Any of those can hold a stale reference if the function drops a lock and a parallel thread invalidates the underlying object.
The supporting routines that run inside lookup()’s outer loop are at the top of the same file (line numbers are from the same xnu-11417.140.69 source):
707: namei_compound_available(vnode_t dp, struct nameidata *ndp)
717: lookup_check_for_resolve_prefix(...)
760: lookup_authorize_search(vnode_t dp, struct componentname *cnp, int dp_authorized_in_cache, vfs_context_t ctx)
785: lookup_consider_update_cache(vnode_t dvp, vnode_t vp, struct componentname *cnp, int nc_generation)
833: lookup_handle_rsrc_fork(...)
942: lookup_handle_found_vnode(struct nameidata *ndp, struct componentname *cnp, ...)
1122: lookup_handle_emptyname(struct nameidata *ndp, struct componentname *cnp, int wantparent)
1233: lookup(struct nameidata *ndp)
1608: lookup_traverse_union(vnode_t dvp, vnode_t *new_dvp, vfs_context_t ctx)
1700: lookup_traverse_mountpoints(struct nameidata *ndp, struct componentname *cnp, vnode_t dp, ...)
1819: lookup_handle_symlink(struct nameidata *ndp, vnode_t *new_dp, bool *new_dp_has_iocount, vfs_context_t ctx)
1974: relookup(struct vnode *dvp, struct vnode **vpp, struct componentname *cnp)
The *new_dp_has_iocount parameter on lookup_handle_symlink is the kind of single-line tell that there was a TOCTOU bug there at some point. That parameter exists because the caller needs to know whether the function returned with the iocount taken or not — i.e., the function’s locking contract is non-uniform across return paths. Every non-uniform locking contract is a future TOCTOU bug waiting for the wrong refactor. CVE-2025-43520 is, by Apple’s “improved locking” advisory phrasing, an instance of exactly this hazard.
Apple’s response in the 22H333 (iOS 18.7.7) drop will appear in a future XNU source release. When that drop lands, the workflow to verify is symmetrical to what we did for stage 4 — cmp -l between the 22H320 and 22H333 kernelcaches, awk-cluster the diff regions, cross-reference against --starts, then read the corresponding source change in the next public XNU tag.
Where to study this further
If you came here from Part 1 wanting to keep digging, the on-ramps are:
- Reading XNU itself. The relevant trees are
osfmk/vm/(for stage 4) andbsd/vfs/(for stage 5). Start withosfmk/vm/vm_map.c::vm_map_copy_overwrite_nested,osfmk/vm/vm_map.c::mach_make_memory_entry_64, andbsd/vfs/vfs_lookup.c::lookup. Apple publishes the source at github.com/apple-oss-distributions/xnu. - Prior public exploits in the same neighbourhood. Synacktiv’s CVE-2021-1782 write-up, Alfie CG’s Trigon series on CVE-2023-32434, Project Zero’s iOS kernel exploit survey, and DFSEC’s Blasting Past iOS 18 cover most of the techniques we cited.
- Mitigation context. Apple’s kalloc_type post and our own MIE deep dive explain what changed in iOS 17/18 and why the chain still works regardless.
- Companion 8kSec posts. Reading iOS sandbox profiles, Patch-diffing the iOS kernel, Analyzing kernel panics on iOS, and Inspecting iOS XPC calls with Frida each cover one slice of the toolchain you would need to reproduce DarkSword’s stage 4–5 work on a research device.
Browser exploits get the news cycle, but the kernel half is where the interesting engineering lives. DarkSword’s stages 4 and 5 are not new in their primitives — vm_map COW bugs, VFS races, pipe-buffer R/W, ucred patches all pre-date this chain by years — but their assembly into a panic-free, data-only, post-PPL-shaped path through XNU is a useful snapshot of what offensive kernel work looks like in 2026. Apple’s data-integrity story will keep tightening; until it covers the policy state, the shape of this chain will keep working.
References
Primary disclosure
- Google Cloud Threat Intelligence Group — “The Proliferation of DarkSword: iOS Exploit Chain Adopted by Multiple Threat Actors” (19 March 2026) — cloud.google.com
- iVerify — “Inside DarkSword: A New iOS Exploit Kit Delivered via Compromised Legitimate Websites” — iverify.io
- Lookout Threat Intel — “Attackers Wielding DarkSword Threaten iOS Users” — lookout.com
Apple advisories and platform documentation
- Apple — iOS 18.7.5/18.7.6/18.7.7 security release notes — support.apple.com/en-us/126793
- Apple — iOS 26.2/26.3 security release notes — support.apple.com
- Apple Platform Security — “Operating System Integrity” (SPTM/TXM) — support.apple.com
- Apple Security Research — “Towards the Next Generation of XNU Memory Safety: kalloc_type” — security.apple.com
Background research (used as image and technique references)
- Project Zero — “A Survey of Recent iOS Kernel Exploits” — googleprojectzero.blogspot.com
- Synacktiv — “Analysis and Exploitation of the iOS Kernel Vulnerability CVE-2021-1782” — synacktiv.com
- Alfie CG — “Trigon: Developing a Deterministic Kernel Exploit for iOS (Part 1)” — alfiecg.uk
- Tencent Keen Lab — “Racing for Everyone: Descriptor Describes TOCTOU in Apple’s Core” — keenlab.tencent.com
- Bazad — “CVE-2017-13868: A fun XNU infoleak” — bazad.github.io
- Azeria Labs — “Grooming the iOS Kernel Heap” — azeria-labs.com (image source for the heap-grooming animations and exploit-zone diagram embedded above)
- TFP0 Labs — “Exploring UNIX Pipes for iOS Kernel Exploit Primitives” — tfp0labs.com
- Quarkslab — “Modern Jailbreaks’ Post-Exploitation” — blog.quarkslab.com
- Jamf Threat Labs — “Predator Spyware’s iOS Kernel Exploitation Engine: PAC Bypass, NEON R/W & More” (2026) — jamf.com (image source for the FDGuardNeonRW, RWTransfer, and post-exploitation chain diagrams embedded above)
- DFSEC Research — “Blasting Past iOS 18” (May 2025) — blog.dfsec.com
XNU source (function references)
- apple-oss-distributions/xnu —
osfmk/vm/vm_map.c,osfmk/vm/vm_map.h,bsd/vfs/vfs_lookup.c,bsd/vfs/vfs_subr.c,bsd/sys/vnode_internal.h,bsd/sys/proc_internal.h— github.com/apple-oss-distributions/xnu
Tooling
- blacktop/ipsw — open-source firmware-analysis toolkit used throughout the reproduction section above — github.com/blacktop/ipsw
- AppleDB — community-maintained Apple firmware metadata database, queried by
ipsw download appledb— github.com/littlebyteorg/appledb
Disclosure note: this article is technical commentary for educational and defensive research purposes. The CVE numbers, daemon identities, and patched-version data are taken from public disclosures listed above. The mechanism narrative for stages 4 and 5 is reasoned from the bug class plus prior public XNU research and is flagged as illustrative where it goes beyond what the primary disclosures published. Embedded images from third-party research blogs are reproduced under fair use for non-commercial technical commentary, with full credit and links to the original posts. No proof-of-concept code is included or intended.
Get in Touch
Want to learn these techniques hands-on, or need help assessing your own mobile or AI stack? We run live and on-demand trainings, offer mobile-security certifications, and take on penetration-testing engagements. Pick the door that fits.
We respond within one business day. Visit our events page to see where we'll be next.