Why Modern iOS Jailbreaks Ignore Pointer Authentication
Table of contents
When Apple introduced PAC (Pointer Authentication Codes) with the A12 generation and the arm64e architecture, it was presented as a major answer to memory corruption exploits. The basic idea was powerful: function pointers would be cryptographically signed, making it much harder for attackers to redirect control flow with classic ROP or JOP style techniques.
For a while, PAC did exactly what it was supposed to do. Even if an attacker managed to get kernel read and write, turning that into clean code execution still required a valid way to deal with authenticated pointers. Early jailbreaks therefore spent a lot of energy on PAC bypasses.
But modern jailbreaks changed strategy. Instead of fighting PAC directly, they started asking a simpler question:
What if we do not need to forge function pointers at all?
That shift is why many modern iOS jailbreaks effectively ignore PAC and rely on data-only attacks instead.
The Core Idea
PAC protects control flow. It helps ensure that when the CPU is about to branch through an authenticated pointer, that pointer has not been tampered with.
What PAC does not protect is ordinary kernel data:
- integers
- flags
- structure fields
- labels
- credentials
- trust cache state
That distinction matters a lot. Many kernel functions that jailbreaks care about are ultimately just helpers that read or update data structures. If a jailbreak can directly modify the underlying data, it may not need to call the function at all.
This is the heart of the PAC-less approach:
- gain a kernel memory primitive
- strengthen it until memory protections are no longer in the way
- directly patch the data structures that matter
- skip the whole problem of authenticated control-flow pointers
A Clue from Real Jailbreak Code
If you look at a modern jailbreak codebase such as Dopamine, the environment logic makes this strategy fairly explicit. For example:
- (BOOL)isPACBypassRequired
{
if (![self isArm64e]) return NO;
if (@available(iOS 15.2, *)) {
return NO;
}
return YES;
}The important message here is not the exact implementation detail. It is the policy:
- on older systems, PAC bypasses may still matter
- on newer systems, the jailbreak intentionally skips that path
That is not because PAC became weak. It is because the exploit chain evolved.
Why Data-Only Attacks Work
A data-only attack does not try to hijack execution through a forged function pointer. Instead, it changes the kernel’s state by directly rewriting the values that the kernel later trusts.
For a jailbreak, that can mean:
- changing a process credential
- modifying a MAC label for sandbox escape
- updating trust cache related state
- patching fields used by entitlement or execution policy logic
From the attacker’s perspective, this is often enough. If the end goal is “make the kernel believe X is true”, then directly writing the value for X can be simpler than building a full kcall path through authenticated pointers.
The Modern Exploitation Chain
In broad terms, a modern jailbreak often follows this sequence:
- use a kernel exploit to gain virtual kernel read and write
- skip PAC bypass
- use another stage to bypass later protection layers such as PPL
- upgrade to stronger physical or unrestricted memory access
- directly patch kernel data structures
The crucial observation is that PAC is only one layer in the broader protection model. Once the exploit chain grows strong enough to work below or around the relevant data path, PAC stops being the bottleneck.
Example: Sandbox Escape Without a PAC Bypass
A good example is sandbox escape through MAC label manipulation. Older approaches might depend on invoking a kernel function through a callable primitive. Newer approaches often just write the target value directly.
The contrast looks like this:
void jbclient_root_set_mac_label(uint64_t slot, uint64_t label, uint64_t value) {
if (host_is_arm64e() && jbinfo(usesPACBypass)) {
kcall(NULL, ksymbol(mac_label_set), 3, (uint64_t[]){ label, slot, value });
} else {
kwrite64(label + ((slot + 1) * sizeof(uint64_t)), value);
}
}The modern branch is the important one. Instead of calling mac_label_set, it computes the location of the relevant field and overwrites it directly.
That means:
- no forged function pointer
- no authenticated indirect branch
- no PAC-sensitive call path
The kernel never gets a chance to reject a bad control-flow pointer, because the exploit never asks it to follow one.
Why This Is a Bigger Strategic Shift
This change is more than just a coding preference. It reflects a different view of exploitation:
- older exploit chains focused on building a callable kernel function interface
- newer exploit chains focus on becoming a direct memory editor
Once the attacker reaches that point, many traditional “call this kernel routine” goals become simple data patching tasks.
In other words, the jailbreak community stopped insisting on defeating PAC head-on and instead asked:
Can we get the same end result without touching authenticated control flow at all?
In many cases, the answer turned out to be yes.
Why PAC Still Matters
None of this means PAC is useless. PAC still raises the bar significantly for:
- control-flow hijacking
- gadget-based exploitation
- forged indirect branches
- many classic code-reuse techniques
The lesson is not that PAC failed. The lesson is that attackers adapted by moving to a part of the system PAC was never designed to protect: raw data state.
That is an important distinction. PAC remains valuable, but it is not a complete answer once a jailbreak chain reaches strong enough memory primitives.
Where PPL Fits In
One reason this PAC-less strategy becomes practical is that modern jailbreaks usually need more than early virtual kernel read and write. They also need to deal with later memory protection layers, such as PPL.
Once those barriers are bypassed, the exploit can often write to the physical or protected memory backing the data structures it cares about. At that point, the jailbreak no longer needs a signed function pointer for many post-exploitation tasks.
That is why the chain often looks like:
kernel exploit
-> early KRW
-> no PAC bypass
-> PPL bypass
-> stronger memory write primitive
-> direct kernel state modificationThe exploit chain is still difficult. It is just difficult in a different way than older PAC-centric jailbreak designs.
Conclusion
Modern iOS jailbreaks often ignore PAC not because PAC stopped working, but because the objective changed. Instead of trying to forge valid authenticated control-flow pointers, they increasingly rely on data-only attacks that patch kernel state directly.
That is the key strategic shift:
- PAC protects pointers used for control flow
- jailbreaks increasingly target data state instead
Once a chain has enough memory access, directly rewriting labels, credentials, and policy-related structures can be more practical than building a traditional kcall path. In that world, PAC is still present, but it is no longer the main obstacle.