Table of Contents

Control Flow Integrity (CFI): User vs Kernel Land

Used on CFI blog post

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 glanceUser-land CFIKernel-land CFI
ProtectsApps like browsers and editorsThe OS kernel and drivers
ExamplesLLVM CFI, Microsoft CFG, CET shadow stacksClang CFI and kCFI on Linux, CET IBT
Main constraintSome slowdown is often acceptableMust be very fast, and interrupts and exceptions make it harder
If it’s bypassedThe attacker controls one appThe attacker controls the whole system
On Apple siliconPointer 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 ModeKernel Mode
Limited access (safe sandbox)Full system access
Runs apps like browsers, IDEsRuns core OS code (device drivers)
Makes system calls to request privileged actionsExecutes 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. 

Portrait of Olivia Gallucci in garden, used in LNP article.

Written by Olivia Gallucci

Olivia is senior security engineer, certified personal trainer, and freedom software advocate. She writes about offensive security, open source software, and professional development.

Discover more from [ret]2read

A Monthly OS Internals Publication by Olivia Gallucci. Subscribe now to keep reading and get access to the full archive.

Continue reading