Control Flow Integrity, or CFI, is a catch-all term for a family of mitigations that try to constrain indirect branches—calls, jumps, returns—to a set of targets that are either statically or dynamically approved. On paper, it closes the gap left by W^X and ASLR: even if an attacker gets arbitrary write, they can’t redirect execution to a gadget of their choosing. In practice, the implementations shipping in Windows, macOS, iOS, Android, and various hypervisors are a patchwork of coarse-grained checks, shadow stacks, and compiler-inserted validation that often fails in ways the vendors don’t advertise. This article is a field report on what actually happens when you try to bypass or reproduce those claims on x86-64 and ARM64.

For anyone working at the bench—whether that means extracting a baseband bootloader over JTAG, fault-injecting a SEP, or emulating a UEFI module—CFI is not an abstract security property. It is a set of concrete constraints on the indirect branch instructions you can hijack, the return addresses you can forge, and the exception paths you can abuse. The gap between the vendor’s threat model and the silicon’s actual behavior is where the interesting bugs live.

Close-up of a circuit board with a central processor chip and surrounding components

What CFI Actually Checks

At the instruction level, CFI splits into two broad categories: forward-edge protection for indirect calls and jumps, and backward-edge protection for returns. Forward-edge schemes typically insert a check before an indirect call or jump to verify that the target address belongs to a set of valid function entry points. Backward-edge schemes protect the return address, either by keeping a separate shadow stack or by encrypting the return address with a key stored in a register or memory that the attacker cannot easily read.

The devil is in the granularity. A fine-grained CFI policy would validate every indirect branch against the exact set of functions whose address could legitimately flow to that call site. That requires precise points-to analysis, which is expensive and brittle across shared libraries and JIT code. What ships instead is usually a coarse approximation: all functions with the same signature, all functions in the same module, or all functions whose address has been taken anywhere in the program. Coarse-grained CFI stops some exploits, but it leaves a large gadget space for an attacker who can chain existing call targets.

Windows: Control Flow Guard and CET

Windows Control Flow Guard (CFG) is a forward-edge check inserted by the compiler before indirect calls. The target address is passed to a bitmap lookup that records which addresses are valid function entry points. If the bit is not set, the process terminates. The bitmap is stored in a read-only section, but the check itself is a function call into ntdll!LdrpValidateUserCallTarget or a direct bitmap test in newer builds. The problem is that the bitmap is coarse: any function whose address has been taken is marked valid, regardless of whether that function could legitimately be called from the current site. An attacker who can overwrite a function pointer can redirect it to any other address-taken function in the module. That is not a full bypass, but it is a large reduction in attacker effort.

Intel CET adds a hardware shadow stack for returns and an indirect branch tracking (IBT) mechanism for forward edges. On x86-64, IBT uses the ENDBR64 instruction as a landing pad. If an indirect call or jump lands on an instruction that is not ENDBR64, the CPU raises a control-protection fault. The shadow stack is a separate stack in memory that stores only return addresses, protected by a page-table bit that makes it writable only by dedicated CALL and RET operations. In theory, this is a strong combination. In practice, the shadow stack is only as good as the OS’s handling of setjmp, longjmp, exception unwinding, and signal delivery. Each of those paths must manually adjust the shadow stack pointer, and each adjustment is a potential hole.

macOS and iOS: Pointer Authentication

Apple’s ARM64 devices use Pointer Authentication Codes (PACs). A PAC is a short cryptographic hash of the pointer value and a context value, computed with a key held in a system register. The PAC is stored in the unused high bits of the pointer. Before an indirect branch or a return, the hardware or compiler-inserted code verifies the PAC. If the check fails, the CPU raises an exception. The keys are supposed to be inaccessible to user code, but they are not per-process; they are per-exception-level. A kernel exploit that can read the PAC keys or sign arbitrary pointers defeats the scheme for the entire system.

PAC is not a shadow stack. It authenticates the pointer value, not the control flow path. An attacker who can forge a valid PAC for a chosen target can redirect execution to that target. The PAC is only 16 to 24 bits depending on the pointer size and the reserved bits, so a brute-force attack is possible if the target does not crash the process on failure. Apple mitigates this by rate-limiting PAC failures in some contexts, but the underlying weakness remains: PAC is a probabilistic check, not a deterministic one.

Android and Linux: Clang CFI and Shadow Stacks

Android uses Clang’s forward-edge CFI for the kernel and some userspace components. The compiler generates type-based checks: before an indirect call, it compares the target’s type identifier against the expected type for that call site. The type identifiers are stored in a read-only section, and the check is a simple comparison and branch. This is finer-grained than Windows CFG because it distinguishes function signatures, but it still allows any function with a matching signature to be called. In the kernel, where many functions share the same signature, the gadget space remains large.

Linux has been slower to adopt CFI. The mainline kernel has supported Clang CFI for arm64 since 5.13, but x86-64 support is still not universal. The kernel’s indirect call sites are numerous, and the performance cost of type checks is measurable. Shadow stacks for the kernel are also in progress, but the interaction with ptrace, kprobes, and perf makes the implementation messy. Each of those subsystems can legitimately modify control flow, and each modification must be reflected in the shadow stack.

Macro photograph of a green circuit board with a processor chip and resistors

Where CFI Breaks in Practice

The most reliable bypasses do not attack the CFI check itself. They attack the assumptions underneath it. A shadow stack assumes that the return address is the only thing that matters. But an attacker who can overwrite a saved frame pointer, a function pointer, or a vtable entry can still redirect execution without touching the return address. CFI protects the branch, not the data that feeds the branch.

Another common failure is the exception path. On Windows, structured exception handling (SEH) uses a linked list of exception registration records on the stack. The list is protected by SafeSEH and later by __C_specific_handler validation, but the unwinding itself can be abused. On ARM64, the PAC instruction signs the link register, but the exception return path uses a different register (ELR_EL1 or ELR_EL2) that may not be protected by the same mechanism. A fault injection that corrupts ELR_EL1 during an exception can bypass the return-address check entirely.

JIT code is another weak point. A JIT compiler generates code at runtime, and that code must be marked executable. The CFI policy must either exempt JIT code from checks or dynamically update the valid-target set. Both approaches create windows where an attacker who can write to the JIT code cache can redirect execution. Browsers and language runtimes have spent years hardening their JIT implementations, but the fundamental tension remains: CFI wants a static set of valid targets, and JIT wants to create new targets on the fly.

Fault Injection and CFI

At the bench, fault injection changes the calculus. A voltage glitch or electromagnetic pulse can skip the CFI check instruction entirely, or corrupt the comparison result so that an invalid target is accepted. The check is just a few instructions; skipping it is often easier than finding a gadget that passes the check. This is why hardware-enforced CFI is not a substitute for physical security. If an attacker has physical access to the device, the CFI check is just another instruction to glitch.

On embedded devices with custom firmware, the CFI story is even weaker. Many baseband and automotive ECUs do not use CFI at all, or they use a vendor-specific implementation that has never been publicly audited. The firmware is often extracted over JTAG and reverse-engineered, and the CFI checks, if any, are identified and patched out. The vendor’s claim of “hardware-enforced control flow integrity” often means a single check in the bootloader that can be bypassed with a well-timed glitch.

Reproducing Vendor Claims

When a vendor claims that their system uses CFI, the first step is to reproduce the claim. That means disassembling the firmware or binary and looking for the actual check instructions. On Windows, CFG is visible as a call to LdrpValidateUserCallTarget or a bitmap test before indirect calls. On macOS, PAC is visible as PACIA, PACIB, AUTIA, and AUTIB instructions around function entry and exit. On Android, Clang CFI is visible as a comparison of a type identifier before an indirect call.

The next step is to test the granularity. For each indirect call site, what is the set of valid targets? If the set is large, the CFI is coarse. If the set is small, the CFI is fine-grained. The difference matters because it determines how much work an attacker must do to find a usable gadget. A coarse-grained policy may still stop a simple stack smash, but it will not stop a determined attacker who can chain existing functions.

Finally, test the edge cases. What happens when an exception is thrown? What happens when a signal is delivered? What happens when a JIT compiler generates code? Each of these paths must interact with the CFI mechanism, and each interaction is a potential bypass. The vendors do not document these interactions in detail, so the only way to know is to test them.

Oscilloscope display showing a waveform trace during hardware testing

What This Means for the Bench

For anyone extracting firmware from a locked-down device, CFI is a hurdle, not a wall. The checks can be identified, analyzed, and bypassed. The bypass may be as simple as finding a valid target that does what you need, or as complex as glitching the check instruction. The key is to understand the specific implementation, not the marketing claim. A vendor that says “hardware-enforced CFI” may mean a shadow stack, a PAC, a bitmap check, or nothing at all. The only way to know is to look at the silicon and the firmware.

The broader lesson is that CFI is a mitigation, not a security boundary. It raises the cost of exploitation, but it does not eliminate the underlying vulnerability. An attacker who can write to memory can still corrupt data, and data corruption is often enough to achieve the attacker’s goal. CFI is one layer in a defense-in-depth strategy, and it should be treated as such.

FAQ

Does CFI stop all code-reuse attacks?

No. CFI constrains indirect branches to a set of valid targets, but the set is often large enough to contain useful gadgets. Coarse-grained CFI, in particular, allows an attacker to chain existing functions that share a signature or module. CFI also does not protect against data-only attacks, where the attacker corrupts non-control data to change the program’s behavior without redirecting execution.

What is the difference between a shadow stack and pointer authentication?

A shadow stack stores a separate copy of the return address on every call and verifies it on every return. It is a deterministic check: if the return address is modified, the check fails. Pointer authentication signs the return address with a cryptographic key and verifies the signature on return. It is a probabilistic check: an attacker who can forge a valid signature can bypass the check. Shadow stacks are stronger in theory, but they require more memory and more complex exception handling.

Can fault injection bypass CFI?

Yes. A voltage glitch or electromagnetic pulse can skip the CFI check instruction or corrupt the comparison result. The check is just a few instructions, and skipping it is often easier than finding a gadget that passes the check. Hardware-enforced CFI is not a substitute for physical security. If an attacker has physical access to the device, the CFI check is just another instruction to glitch.

Why do vendors use coarse-grained CFI instead of fine-grained?

Fine-grained CFI requires precise points-to analysis to determine the exact set of valid targets for each indirect call site. That analysis is expensive, brittle across shared libraries and JIT code, and often produces false positives that break legitimate code. Coarse-grained CFI is cheaper to implement and has lower performance overhead, but it leaves a larger gadget space for attackers. The tradeoff is between security and compatibility.

This article is part of a continuing series on hardware-enforced mitigations. The next installment will examine how CET shadow stacks interact with setjmp and longjmp on x86-64, and whether the exception unwinding path can be abused to corrupt the shadow stack pointer. If you have a specific device or firmware image you would like analyzed, the bench is always open.