The stack canary guards one value; PAC/MTE raised the bar on ARM. x86's answer is CET, Control-flow Enforcement Technology — two hardware features that, together, delete the gadget soup ROP and JOP rely on. Knowing precisely what they delete (and what they leave) is the difference between "mitigated" and "exploited."
- arch
- amd64-64-little
- CET
- shadow stack (SHSTK) + indirect branch tracking (IBT)
- compiler
- -fcf-protection (endbr64 at every indirect target)
- kills
- classic ROP and JOP/COP to mid-function gadgets
shadow stack — return addresses you can't reach#
With SHSTK, every call pushes the return address to two places: the normal
stack and a separate shadow stack in specially-protected memory you can't write
with an ordinary store. Every ret pops both and compares them — mismatch raises a
control-protection fault (#CP):
Overwriting the saved return address simply doesn't steer execution anymore — the shadow copy is untouched and authoritative. Classic ROP, which is nothing but overwritten return addresses, is dead.
IBT — every indirect branch must land on endbr64#
The second half covers indirect call/jmp. Under IBT, an indirect branch may
only target an instruction that is endbr64; land anywhere else and you get
#CP. The compiler drops an endbr64 at every legal indirect target — function
entries, basically:
JOP and COP — chaining jmp/call gadgets in the middle of functions — can no
longer aim at mid-function gadgets. Every indirect hop has to land on a function
entry.
what still works#
CET is a real, large tax. It is not a wall:
- Whole-function calls (COP to entries). IBT permits indirect calls to any
endbr64, and every function starts with one. If you control an indirect call site and its arguments, callingsystem— or any useful function — outright is still legal control flow. You lost gadgets, not functions. - Data-only attacks. Neither feature looks at data. Overwrite a
modprobe_path, a__malloc_hook-era pointer that gets called through a legal site, file-stream fields, or a length used in amemcpy— no return address forged, no indirect branch broken. - Loops and back-edges already in the program. The intended control flow does plenty; bend the data it flows over.
- Shadow stack writes, where they exist. If
wrssis enabled, or a bug gives a write to shadow-stack memory, orincsspgadgets let you skip entries, the return-address guarantee weakens. And if SHSTK isn't enabled for the thread at all, you're back to ordinary ROP.
the shape of a CET-era exploit
Stop forging return addresses; find an indirect call you control and aim it at
a real function entry, or skip control flow entirely and corrupt the data it
trusts:
segfault{endbr64_is_at_the_start_of_system_too}
CET ends the era of stitching mid-function gadgets into return-oriented chains — genuinely, that technique is over where it's deployed. What replaces it is older than ROP: call the functions that already exist with the arguments you want, or don't touch control flow at all and let corrupted data carry the exploit. The bug still decides; CET just narrows the menu.