Control flow integrity (CFI) stops an attacker from hijacking where a program jumps next. The idea is the same in user land and in the kernel, but the two are built very differently, and a bypass in the kernel costs far more. The table below is the short version. If you already know how user mode and kernel mode differ, skip straight to CFI.
| At a glance | User-land CFI | Kernel-land CFI |
|---|---|---|
| Protects | Apps like browsers and editors | The OS kernel and drivers |
| Examples | LLVM CFI, Microsoft CFG, CET shadow stacks | Clang CFI and kCFI on Linux, CET IBT |
| Main constraint | Some slowdown is often acceptable | Must be very fast, and interrupts and exceptions make it harder |
| If it’s bypassed | The attacker controls one app | The attacker controls the whole system |
| On Apple silicon | Pointer Authentication (PAC) | PAC |
Quick refresher: user mode vs. kernel mode
There are typically two modes of operation in operating systems (OS): user mode and kernel mode. These modes provide a privilege separation between types of processes, ensuring that user applications cannot directly interfere with system operations.
Note: Some CPU architectures expose more than two privilege “rings,” and some microkernel designs break certain OS services into separate user-mode processes, but the broad concept of “user mode vs. kernel mode” remains as described here.
User mode
This is the restricted mode where most applications run. In user mode, processes are prevented from directly accessing hardware or executing privileged instructions. Instead, they rely on system calls to request services from the OS, which runs in kernel mode.
To look at this in practice, I use the process of saving a file. In user mode, I would use a text editor like VS Code. I would type a bunch of notes and then hit Ctrl+S to save.
The text editor (a user-mode application) cannot directly access the disk to save my file. Instead, it makes a system call (e.g., write() in Unix-like systems or WriteFile() on Windows). This is a request for a service that requires privileged access—in this case, writing to the disk.
🌸👋🏻 Enjoying this post? Get the next issue of [ret]2read. Once a month, I’ll email you whatever’s on my mind, usually Apple security research, offensive security, open source software freedom, and operating system internals.
Want your company or product in front of [ret]2read readers?
One sponsor per issue →
Kernel mode
On the other hand, there is kernel mode. This is the privileged mode where the OS’s functions and services run. In kernel mode, processes have direct access to the system’s hardware and memory.
Taking the example of saving a file to disk, I can see it operates differently in this space. Here, the system call transfers control to the OS kernel, switching the CPU into kernel mode. The kernel now has full access to hardware and system memory. It handles the request: locating the file on disk, writing the contents, updating file metadata. Once finished, it returns control back to user mode, and my text editor continues running as usual.
| User Mode | Kernel Mode |
|---|---|
| Limited access (safe sandbox) | Full system access |
| Runs apps like browsers, IDEs | Runs core OS code (device drivers) |
| Makes system calls to request privileged actions | Executes those privileged actions |
Control flow integrity (CFI)
Within these modes, control flow integrity (CFI) operates differently. CFI is a technique that ensures a program’s execution follows only legitimate control flow paths determined at compile time. CFI is primarily used to mitigate against control-flow hijacking attacks like return-oriented programming (ROP) and jump-oriented programming (JOP), which exploit vulnerabilities such as buffer overflows.
Read more: How to use ROP to bypass security mechanisms – Olivia A. Gallucci
User-land CFI
Let’s start with CFI in the user-land. In user-land, CFI helps secure applications from control flow hijacking, especially in scenarios where I can manipulate user-level programs through memory corruption vulnerabilities (e.g., stack or heap overflows).
Implementation
CFI in user-land is often implemented via compiler-based mechanisms, where checks are inserted into the compiled code to ensure the control flow adheres to an expected graph. For example, LLVM’s CFI or Microsoft’s Control Flow Guard (CFG) are often used for protecting user-land applications.
Read more: Compilation Phases Explained: Analysis and Synthesis – Olivia A. Gallucci
In user-land, CFI protects against attacks that target common programs (e.g., browsers and productivity software) and aims to stop me from executing arbitrary code by forcing the program to follow its legitimate control flow.
Kernel-land CFI
CFI’s role in kernel mode follows the same principle (only allow known/legitimate jump targets) but is implemented differently because of the kernel’s privileged environment.
So, let’s examine CFI in kernel-land!
Here, the goal is to protect the OS kernel (rather than applications) from control-flow hijacking, which could lead to full system compromise. Exploits in kernel-land usually have higher privileges than in user-land, which makes it extra dangerous.
Implementation
Kernel CFI is more complex due to the kernel’s “low-level” operations and performance requirements. This CFI needs to account for dynamic and complex behaviors, like system calls, interrupts, and exception handling.
One example is Clang’s CFI. Clang is a compiler, not a CFI scheme itself, but its CFI option adds checks to indirect calls when the code is built. Google used it to harden the Linux kernel on Android phones starting in 2018.
There is also kCFI, a newer Clang scheme built for the kernel. Linux has supported it since version 6.1. Before each indirect call, kCFI checks a type hash stored right before the target function, so a hijacked function pointer can only land on a function with the matching type.
Complementing these software-based protections is Intel’s Control-flow Enforcement Technology (CET), which introduces hardware-based CFI support. The main difference is where the check lives. Clang’s CFI and kCFI are checks the compiler adds to the code at build time, which then run as the program runs. CET does the checking in the CPU itself, in two parts: Indirect Branch Tracking (IBT) protects indirect calls and jumps, and shadow stacks protect return addresses. On Linux, the kernel has used IBT since version 5.18, and user-space programs can use shadow stacks since 6.6.
What about Macs? Apple silicon takes a different route. On arm64e, Pointer Authentication (PAC) signs return addresses and function pointers, and the CPU checks the signature before using them. Apple turns this on in both user land and the kernel. I cover how this works in CFI with Clang, macOS, and Clang on macOS and The Anatomy of a Mach-O: Structure, Code Signing, and PAC.
🌸👋🏻 Enjoying this post? Get the next issue of [ret]2read. Once a month, I’ll email you whatever’s on my mind, usually Apple security research, offensive security, open source software freedom, and operating system internals.
Conclusion
User-land CFI can afford more flexibility since performance penalties might be acceptable in some applications.
Kernel-level CFI tends to be more difficult to implement because of low overhead requirements, real-time constraints, and the dynamic nature of kernel execution (e.g., interrupts, exceptions, etc.). The kernel operates with higher privileges, meaning that a kernel-level control flow attack is potentially more damaging than a user-land attack.
If you enjoyed this article on CFI, consider reading ASLR, bypass techniques, and circumvention impacts.

