· 2 min read ·

The Threat CFI Is Actually Defending Against

Source: isocpp

Memory safety gets most of the attention in C++ security discussions, and for good reason. Buffer overflows and use-after-free vulnerabilities have caused decades of damage. But there is a class of attack that memory safety improvements alone cannot close, and Control Flow Integrity is the mechanism designed to address it.

James McNellis gave a keynote on CFI at Meeting C++ 2025, published on isocpp.org in December 2025, and it is worth revisiting as a grounding document. McNellis has practical experience deploying CFI in large-scale C++ systems, which gives the talk a different texture than a pure introductory survey.

What the attack looks like

The vulnerability CFI targets is not a specific bug pattern; it is a structural property of how programs transfer control. When a program executes an indirect call, it loads an address from memory and jumps to it. In C++, this happens constantly: virtual method dispatch loads a function address from a vtable, callbacks stored in function pointers, signal handlers, and similar patterns all involve the runtime deciding where execution goes next.

If an attacker can write to the memory holding that address, they choose the destination. The program’s own code becomes the weapon, stitched together from attacker-controlled jumps. This is return-oriented programming and its variants, and these techniques work even against programs with no exploitable memory bugs of their own, as long as they call into code that does.

CFI constrains indirect transfers so that at each call site, only a specific set of targets is valid. A corrupted address that falls outside that set causes a trap rather than a redirect.

Forward and backward edges

The standard framing divides this into two categories. Forward-edge CFI covers indirect calls and jumps; in C++, virtual dispatch is the primary concern. The compiler instruments call sites to verify that the target address belongs to a valid implementation for the declared type:

// The vtable pointer is validated before dispatch occurs
// An attacker-corrupted pointer triggers a fault here rather than redirecting execution
base_ptr->virtual_method();

Clang’s implementation, enabled with -fsanitize=cfi-vcall, handles this with whole-program analysis. It computes the set of valid targets for each virtual call site at compile time, then inserts a runtime check. This requires link-time optimization and typically -fvisibility=hidden; without visibility into all potential targets, the check cannot be sound.

Backward-edge CFI protects return addresses. Stack smashing attacks corrupt the address a function will return to; shadow stacks address this by maintaining a separate, hardware-protected copy of return addresses that the CPU compares against the on-stack address before committing the return. Intel’s CET and ARM’s Pointer Authentication both provide hardware support for this model, and Windows has deployed CET broadly.

The deployment gap

The concept is straightforward. The deployment is not. McNellis’s practical experience is where the keynote earns its value: large C++ codebases contain function pointer casts that technically violate type rules, libraries compiled without CFI support, and patterns at ABI boundaries that create coverage gaps. Whole-program CFI that covers the application binary does not extend to CFI-unaware dependencies, and the interaction at those boundaries matters for actual security guarantees.

For anyone working on C++ code that handles untrusted input or runs in security-sensitive contexts, CFI belongs in the conversation alongside memory safety tooling. The two are complementary, not competing. The McNellis keynote is a reliable starting point before going deeper into compiler documentation and toolchain specifics.

Was this interesting?