Control-Flow Enforcement Technology (CET) and Indirect Branch Tracking

2026-06-03

You've seen the Shadow Stack protect returns. But ROP is only half of code-reuse attacks — the other half is JOP/COP (Jump/Call-Oriented Programming), which chains indirect calls and jumps instead of returns. Intel CET's second half, Indirect Branch Tracking (IBT), closes that gap.

The rule is simple: every indirect call or jmp target must be an ENDBR64 instruction (a 4-byte NOP on pre-CET CPUs, so binaries stay backward-compatible). The CPU maintains a small state machine per logical core: after an indirect branch, it enters WAIT_FOR_ENDBRANCH. If the next instruction fetched isn't ENDBR64, you get #CP (Control Protection), fault vector 21. Direct calls and rets don't trigger the state machine — only register-indirect or memory-indirect transfers.

This forces the attacker's gadgets to start at function entry points (which the compiler marks with ENDBR64) instead of anywhere in the middle of a function. Combined with the Shadow Stack, the gadget pool collapses from "every ret" to "every legitimate function head" — far fewer useful chains.

Real-world example: Compile a "hello world" with GCC 11+ on Tiger Lake or later: gcc -fcf-protection=full hello.c. Disassemble with objdump -d and you'll see every function — including PLT stubs — begin with f3 0f 1e fa (the endbr64 encoding). Inside main, the body has no ENDBR64, so a forged function pointer landing one instruction past the prologue faults immediately. The Linux kernel itself shipped IBT enforcement in 5.18 (2022); a single missing ENDBR64 on a function exposed as a function pointer (e.g., a syscall table entry) bricks the boot with #CP.

Rule of thumb: ENDBR64 costs 4 bytes per function. A typical libc has ~2,500 exported functions, so the I-cache footprint grows by ~10 KB — under 0.5% on a modern 256 KB L1i. The runtime cost of the state-machine check is zero on the fast path; the only measurable overhead comes from those extra bytes shifting code layout.

Two gotchas in practice: JIT compilers (V8, JVM) must emit ENDBR64 at every patchable entry, or indirect dispatch faults. And computed gotos in interpreters (the threaded-code trick) become illegal under IBT unless each label gets an ENDBR64 — which is why -fcf-protection on CPython requires a recent build that inserts them automatically.

See it in action: Check out Control-flow Enforcement Technology, and Xen Supervisor Shadow Stacks - Andrew Cooper, Citrix by The Xen Project to see this theory applied.
Key Takeaway: IBT turns every indirect branch into a typed jump: targets must opt in with ENDBR64, shrinking the attacker's gadget pool from "any instruction" to "any function entry."

All newsletters