control-flow integrity
/ C-F-I /
A program's legitimate flow is like a railway with sanctioned junctions: trains may only switch tracks at real switches, to destinations the timetable allows. Control-flow hijacking is a train jumping to any rail it likes. Control-Flow Integrity (CFI) is a defense that installs guards at the switches: at each indirect jump or call, it checks that the destination is one the program is actually allowed to reach, and aborts if not.
Concretely, the dangerous transfers are the INDIRECT ones — an indirect call through a function pointer (call rax), a virtual call through a vtable, and a return (ret). Direct calls to fixed addresses cannot be hijacked, so CFI focuses on the indirect ones. At compile time, the compiler computes, for each indirect call site, the set of valid targets (for instance, only functions whose type signature matches, or only the members of a class's virtual table). It then inserts a runtime check before each indirect transfer: is the computed target in the allowed set? If yes, proceed; if no, the program is under attack, so abort. This splits naturally into forward-edge CFI (guarding calls and jumps) and backward-edge CFI (guarding returns), the latter usually enforced by a shadow stack. So even if an attacker corrupts a function pointer or vtable entry, the check rejects the bogus destination before the jump.
It matters because CFI attacks the GOAL of code-reuse exploits directly: a ROP chain or a hijacked vtable call tries to send control somewhere the program never intended, and that is exactly what CFI forbids. The honest framing: CFI is not all-or-nothing. Its strength depends on how PRECISE the allowed-target sets are — coarse-grained CFI (allow any function start) still leaves room for clever chains, while fine-grained, type-based CFI is much tighter but costlier and harder to apply to large C codebases. It also does not fix the memory bug; it makes a hijack that exploits the bug fail. Hardware help (Intel CET's indirect-branch tracking, ARM's BTI) makes enforcement cheaper.
// before an indirect call, CFI inserts a check (conceptually): if (!valid_target(fp)) // is fp in this call site's allowed set? __cfi_abort(); // corrupted pointer -> kill the process call fp // only reached for a sanctioned destination
Each indirect transfer is gated by a check that the destination is one the program is allowed to reach.
CFI's protection is only as good as its target-set precision: coarse CFI still permits some gadget chains; and forward-edge CFI guards calls while backward-edge (returns) typically needs a shadow stack — neither repairs the underlying bug.