Safeguard
Supply Chain Security•

Control Flow Integrity Explained: Intel CET, Arm PAC and BTI, and CFI

Control flow integrity stops hijacked returns and indirect calls. Learn Intel CET, Arm PAC and BTI, Clang CFI, Windows CFG and how to verify them.

Safeguard Security Team
40 min read

Control flow integrity (CFI) is a family of protections that keep a running program on the paths its compiler intended, so a memory corruption bug cannot simply redirect a return or an indirect call to code of the attacker's choosing. Hardware shadow stacks (Intel CET, AMD shadow stacks, Arm Guarded Control Stack) and Arm pointer authentication protect returns; Intel indirect branch tracking, Arm BTI, Clang CFI and Windows Control Flow Guard protect indirect calls and jumps.

None of these features turn on just because the CPU supports them. Each one needs the right compiler flag, a linker that keeps the marker, a C library and kernel that switch it on, and every shared library in the process built the same way. This guide explains how each mechanism works at the concept level, which flags and operating system versions you need, how to verify a binary and a running process, and the one unmarked dependency that can quietly turn the whole thing off.

Key takeaways

  • CFI protects two edges. The backward edge is the function return. The forward edge is an indirect call or jump through a function pointer or virtual table. Shadow stacks and return address signing guard the backward edge; landing pad checks and type checks guard the forward edge.
  • On x86, CET has two halves. Shadow stack (SHSTK) checks every return against a protected copy. Indirect branch tracking (IBT) requires every indirect call or jump to land on an ENDBR instruction. In the 64-bit Linux kernel today, user space shadow stack and kernel IBT are the supported combination.
  • On Arm, PAC and BTI do the same jobs. Pointer authentication signs the return address with a secret key (Armv8.3-A). Branch Target Identification restricts where indirect branches may land (Armv8.5-A). Guarded Control Stack is Arm's shadow stack, supported for user space since Linux 6.13.
  • Software CFI adds type checks. Clang CFI checks that an indirect call targets a function of the right type; kCFI is the kernel friendly variant. Windows Control Flow Guard checks indirect call targets against a bitmap of valid function entries.
  • One unmarked object can switch protection off. On Linux the CET markers live in an ELF property note, and a single object or shared library built without them can leave the executable unmarked or make glibc refuse or downgrade at dlopen time.
  • CFI is not memory safety. It makes a memory corruption bug harder to turn into arbitrary code execution. It does not stop data corruption, information leaks or logic bugs, so patching the underlying bug still matters.

What is control flow integrity?

Every compiled program has a control flow graph: the set of places each instruction can transfer control to. A direct call to a named function has exactly one target, baked into the instruction. A return, an indirect call through a function pointer, a virtual method call and a jump through a table are different, because the target is read from memory or a register at run time. If an attacker can corrupt that memory, the target changes.

Control flow integrity is the policy that each of those run time transfers must still land somewhere the original program could legitimately go. The idea was formalised by Martín Abadi, Mihai Budiu and Úlfar Erlingsson at Microsoft Research in a paper presented at the ACM Conference on Computer and Communications Security in November 2005 (technical report MSR-TR-2005-18). Their abstract describes CFI enforcement as simple, formally provable even against powerful adversaries, and practical through software rewriting on commodity systems. Almost every mechanism in this post is a refinement of that idea, either moved into hardware for speed or narrowed into a type check the compiler can add cheaply.

The attack classes CFI targets

Older exploits injected new machine code and jumped to it. Non executable memory (DEP on Windows, the NX bit on Linux) closed that door, so attackers moved to code reuse: stitching together short fragments of instructions that already exist in the program and its libraries.

  • Return oriented programming (ROP) overwrites return addresses on the stack so that each ret lands in a short fragment that ends in another ret. Chained together, the fragments perform work the program never intended. The GCC manual names ROP as the threat -fcf-protection is meant to address.
  • Jump oriented and call oriented programming (JOP and COP) use the same idea with indirect jumps and calls instead of returns, usually by corrupting a function pointer or a virtual table pointer.

This post explains how defences stop those chains at the concept level. It contains no attack code and does not describe how to bypass any mitigation.

Forward edge and backward edge

The single most useful mental model is the two edges:

EdgeWhat transfers controlTypical corruptionHardware defenceSoftware defence
Backward edgeFunction return (ret)Overwritten return address on the stackShadow stack (Intel CET SHSTK, AMD shadow stacks, Arm GCS); Arm PAC return signingStack canaries (detect, not CFI); Clang ShadowCallStack on arm64
Forward edgeIndirect call or jump, virtual call, jump tableOverwritten function pointer or vtable pointerIntel IBT (ENDBR), Arm BTI (BTI landing pads)Clang CFI (-fsanitize=cfi), kCFI, Windows CFG

Hardware landing pads are coarse: they say "this is a legitimate place for some indirect branch to land", not "this is the right function for this particular call". Software type checks are finer grained but cost more and usually need whole program information. Real deployments combine them.

How CFI fits with the other exploit mitigations

CFI sits on top of the mitigations covered in our post on ASLR, PIE and RELRO. Address randomisation makes targets hard to find; RELRO makes the global offset table read only; stack protectors detect a smashed frame on the way out. CFI adds a check at the moment control actually moves. Memory safety approaches go a level deeper and try to stop the corruption itself, either in hardware, as compared in Arm MTE versus CHERI, or in the language, as covered in our introduction to C and C++ memory safety vulnerabilities.

A short timeline of CFI support

Where a source gives a version but no date, the table lists the version only.

WhenWhat landedSource
November 2005Abadi, Budiu and Erlingsson publish "Control-Flow Integrity" (MSR-TR-2005-18, ACM CCS)Microsoft Research
GCC 8, Clang 7-fcf-protection flag for x86 CET instrumentationOpenSSF hardening guide
GCC 9, Clang 8-mbranch-protection=standard for AArch64 PAC and BTIOpenSSF hardening guide
glibc 2.28glibc can be built with --enable-cet (IBT and SHSTK), needs GCC 8 and binutils 2.29glibc NEWS
Linux 5.0User space support for Armv8.3 pointer authenticationKernel Newbies, Linux 5.7 notes
31 May 2020Linux 5.7 adds pointer authentication for arm64 kernel codeKernel Newbies
2 Aug 2020Linux 5.8 adds Armv8.5 BTI for user and kernel space, plus Shadow Call Stack on arm64Kernel Newbies
Visual Studio 2019 16.7/CETCOMPAT linker property and /guard:ehcontMicrosoft Learn
22 May 2022Linux 5.18 adds Intel CET IBT for the kernelKernel Newbies
11 Dec 2022Linux 6.1 adds kCFI for x86_64 and replaces the arm64 Clang CFI implementation, targeting Clang 16Kernel Newbies
Windows 11 2022 updateKernel-mode Hardware-enforced Stack Protection available, off by defaultMicrosoft Learn
29 Oct 2023Linux 6.6 adds x86 user space shadow stack (64-bit kernels)Kernel Newbies, kernel docs
glibc 2.39glibc syncs with the Linux 6.6 shadow stack interface; --enable-cet only on x86-64glibc NEWS
19 Jan 2025Linux 6.13 adds Arm Guarded Control Stack for user spaceKernel Newbies, kernel docs

The pattern is worth noticing. Compiler support arrived years before the operating system switched anything on. That gap is why many binaries already carry the markers, and why many fleets still do not enforce them.

How hardware shadow stacks work

A shadow stack is a second stack that holds only return addresses and that ordinary code cannot write. The Linux kernel documentation describes the x86 version in plain terms: when the processor executes a CALL, it pushes the return address to both the normal stack and the shadow stack. On return it pops both copies and compares them. If they differ, the processor raises a control protection fault and the process is stopped.

The protection comes from where the shadow stack lives. Its pages are marked with a special page table encoding: the kernel docs note that shadow stack page table entries are required to be read only and dirty, a combination ordinary memory never uses. Normal store instructions cannot modify them. Only CALL, RET and a small set of dedicated shadow stack instructions can. So even if a bug lets an attacker overwrite the return address on the normal stack, the protected copy still holds the real one, and the mismatch is caught at the ret.

Intel CET and AMD shadow stacks

Intel's Control-flow Enforcement Technology (CET) is the x86 feature set that introduced both shadow stack and indirect branch tracking. Microsoft's documentation for Kernel-mode Hardware-enforced Stack Protection lists the hardware requirement as Intel CET or AMD shadow stacks, naming 11th Gen Intel Core Mobile processors and AMD Zen 3 cores and newer. The Linux kernel documentation adds an important caution: not all CPUs have both shadow stack and indirect branch tracking. Check the flags on the machine you care about rather than assuming one implies the other.

What Linux does with shadow stacks

The kernel documentation is specific about the moving parts:

  • Three requirements. Supporting hardware, a kernel built with X86_USER_SHADOW_STACK (which needs binutils 2.29 or LLVM 6 or later), and user space libraries compiled with it. The nousershstk boot parameter turns it off.
  • Discovery. /proc/cpuinfo shows user_shstk when both the hardware and the running kernel support user space shadow stacks.
  • The loader decides. The kernel does not read the ELF markers. The glibc dynamic loader or static start up code calls arch_prctl(ARCH_SHSTK_ENABLE, ...) and can lock the setting with ARCH_SHSTK_LOCK.
  • Per thread, inherited. Enablement is inherited on clone, each new pthread gets its own shadow stack, and everything is reset on exec.
  • Size. Each shadow stack is reserved at MIN(RLIMIT_STACK, 4 GB). With the common 8 MB stack limit, that is 8 MB of virtual address space, far more than return addresses alone need.
  • Signals and 32-bit code. Signal handlers share the program's shadow stack, with a kernel verified token pushed on delivery. 32-bit execution inside a shadow stack process faults.

To see whether a running process actually has it, read /proc/PID/status. The kernel reports x86_Thread_features: shstk (and wrss if the rarely needed write capability is on), plus an x86_Thread_features_locked line.

What Windows does with shadow stacks

Windows uses shadow stacks in two places. In user mode, a binary opts in by linking with /CETCOMPAT, which Microsoft documents as marking an x64 image as compatible with CET Shadow Stack; it has been available since Visual Studio 2019. In kernel mode, Kernel-mode Hardware-enforced Stack Protection gives every kernel stack a matching shadow stack. Microsoft's documentation is direct about the outcome of a mismatch in the kernel: the system stops with a blue screen rather than continue with corrupted control flow.

That kernel feature has real prerequisites. It is off by default. It needs the Windows 11 2022 update or newer, supporting hardware, and both virtualization-based security and hypervisor-enforced code integrity (memory integrity) turned on. We explain those two foundations in our post on Windows virtualization-based security, HVCI and memory integrity. It can be enabled in the Windows Security app under Core isolation, or through Group Policy under the Device Guard virtualization-based security setting.

Microsoft pairs the two halves explicitly: Control Flow Guard enforces integrity on indirect calls, and hardware-enforced stack protection enforces integrity on returns.

Exceptions and context switches on Windows

Structured exception handling legitimately moves the instruction pointer outside call and return, through a CONTEXT structure passed to calls like NtContinue. Microsoft's /guard:ehcont makes the compiler emit a table of valid exception continuation targets, and the system checks a requested address against the shadow stack and then that table. Binaries with no table are allowed for compatibility, so only code built with it is protected. It must reach both compiler and linker and applies to 64-bit processes.

Arm Guarded Control Stack

Arm's equivalent is the Guarded Control Stack (GCS). The Linux documentation describes it as a separate stack, writable only through specific GCS operations, that holds the call stack: a procedure call pushes the current program counter, and on RET the address in the link register is checked against the top of the GCS. The current pointer lives in the GCSPR_EL0 system register, readable by user space but changeable only through GCS instructions.

Support is advertised as HWCAP_GCS. GCS is enabled per thread with prctl(PR_SET_SHADOW_STACK_STATUS, ...), can be locked with a lock the process cannot remove, is inherited by new threads and is disabled on exec. Violations arrive as SIGSEGV with si_code set to SEGV_CPERR. Because the interface is shared with x86, the ABI says "shadow stack" rather than GCS. GCC's -mbranch-protection=gcs turns on GCS compatible code generation.

How indirect branch tracking and Arm BTI work

Shadow stacks protect returns. Indirect calls and jumps need a different check, because there is no "expected" value to compare against: a function pointer can legitimately point at many functions.

Intel IBT and the ENDBR instruction

Intel's answer is indirect branch tracking. The compiler places an ENDBR64 instruction (or ENDBR32 in 32-bit code) at the start of every function that might be reached indirectly, and at other legitimate indirect targets. When IBT is active, the processor tracks each indirect CALL or JMP. If the next instruction executed is not an ENDBR, it raises a control protection fault.

The Kernel Newbies notes for Linux 5.18 describe IBT as coarse grained forward edge CFI, and that coarseness is the key limitation. IBT does not know which function a call site expects; any function that begins with ENDBR is acceptable. It blocks jumps into the middle of functions, the raw material of JOP, but not a corrupted pointer calling the wrong whole function. On processors without CET, ENDBR runs as a no-op, so the same binary runs everywhere.

On Linux, the kernel documentation is plain: in the 64-bit kernel today, only user space shadow stack and kernel IBT are supported. Compilers already emit ENDBR in user space code, but user space IBT enforcement is not part of the documented upstream interface.

Arm Branch Target Identification

Arm's Branch Target Identification (BTI), introduced in Armv8.5-A, is the same idea with more precision. The compiler inserts BTI landing pads, pages holding BTI aware code are marked as guarded, and an indirect branch into a guarded page must land on a compatible BTI instruction. The Linux 5.8 notes describe this as letting branch targets limit the types of branch from which they can be called. Support is advertised as HWCAP2_BTI, the instructions are hint space no-ops on older cores, and Linux has supported BTI for user and kernel space since 5.8.

Two GCC details deserve attention:

  • -mbranch-protection=bti turns on BTI landing pads, and standard turns on all branch protection features at their standard level. The OpenSSF guide notes that in Clang, standard is equivalent to bti+pac-ret.
  • On x86, GCC by default skips CET instrumentation for switch statements that use a jump table. The manual notes that jump tables live in read only memory, so this matters only if the table index is attacker controlled; -mcet-switch changes it.

How Arm pointer authentication signs return addresses

Pointer authentication (PAC) takes a different approach to the backward edge. Instead of keeping a protected copy, it makes the return address tamper evident.

The Linux kernel documentation describes the Armv8.3 extension this way. A pointer authentication code is derived from three inputs: the pointer, a second value such as the stack pointer, and a secret key held in system registers that user space cannot read. The code is stored in the unused high order bits of the pointer itself. Before the pointer is used, a matching instruction recomputes the code and checks it. A pointer that was modified without the key no longer verifies.

With -mbranch-protection=pac-ret, the compiler signs the return address on function entry, before it is saved to the stack, and authenticates it just before the return. If an attacker overwrites the saved return address, the attacker also has to produce a valid code for the new address, the current stack pointer and a key they cannot see.

Keys, widths and where PAC is used

  • Five keys. Two for instruction addresses (APIAKey, APIBKey), two for data addresses (APDAKey, APDBKey) and one generic key (APGAKey). Return signing uses the A instruction key by default; pac-ret+b-key switches to the B key.
  • Fresh keys per process. With CONFIG_ARM64_PTR_AUTH, the kernel assigns random keys to each process at exec; threads share them and fork preserves them.
  • Hint space compatibility. Some PAC instructions are NOPs on cores without the extension, so signed binaries run everywhere, but the OpenSSF guide warns the protection is not enforced there.
  • Kernel use. With ARM64_PTR_AUTH_KERNEL the kernel signs its own returns (Linux 5.7); user space support came in Linux 5.0.
  • Leaf functions and discovery. pac-ret+leaf also signs leaf functions. User space sees HWCAP_PACA and HWCAP_PACG.

Worked example: how many bits does the code get?

The kernel documentation gives the formula: the number of bits the PAC occupies is 55 minus the virtual address size configured by the kernel.

Virtual address sizePAC widthChance that one blind guess verifies
39 bits55 minus 39 = 16 bits1 in 65,536
48 bits55 minus 48 = 7 bits1 in 128
52 bits55 minus 52 = 3 bits1 in 8

The 48-bit row is the kernel docs' own example. PAC strength depends on configuration you may not control, and a failed check normally ends the process, so pair PAC with restart limits and with BTI and GCS rather than relying on it alone.

What software CFI adds

Hardware landing pads and return checks are cheap but coarse. Software CFI adds type information.

Clang CFI

Clang's documentation describes its CFI schemes as designed to abort the program on detecting certain kinds of undefined behaviour that could let an attacker subvert control flow, and as optimised enough to use in release builds. You enable them with -fsanitize=cfi or individual schemes:

SchemeWhat it checks
cfi-icallAn indirect call targets a function whose dynamic type matches the static type at the call site
cfi-vcallA virtual call goes through an object whose vtable pointer has the right dynamic type
cfi-nvcallA non-virtual member call goes through an object of the right dynamic type
cfi-mfcallAn indirect call through a member function pointer has the right dynamic type
cfi-derived-castA base to derived cast lands on the right dynamic type
cfi-unrelated-castA cast from void* or an unrelated type lands on the right dynamic type
cfi-cast-strictStricter versions of the cast checks

The type check is what makes this finer than IBT. Under cfi-icall, a call through a pointer of type "function taking a buffer and a length" cannot reach a function with a different signature, even though both start with valid landing pads.

The cost is build structure. Every scheme except kCFI needs link time optimisation (-flto or -flto=thin). The class based schemes need -fvisibility=hidden or similar, because default visibility disables checks for classes without visibility attributes. CFI enabled objects should be statically linked, which may rule out shared libraries; cross-DSO mode exists but its ABI is unstable. A violation aborts immediately by default; -fno-sanitize-trap= prints a diagnostic first for testing.

kCFI for kernels and low level code

-fsanitize=kcfi is Clang's alternative indirect call scheme for operating system kernels and similar code. Clang documents three properties that make it easier to adopt: it does not need LTO, it does not replace function pointers with jump table references, and it never breaks function address equality across shared objects. The trade-offs are that it only checks function pointers and is not compatible with execute only memory. Linux adopted kCFI in 6.1 for x86_64 and replaced its earlier arm64 Clang CFI implementation with it.

Clang also documents -fsanitize-kcfi-arity, which records each indirect callable function's register argument count for kernel techniques such as FineIBT.

Windows Control Flow Guard

Control Flow Guard (CFG) is Microsoft's forward edge scheme. The compiler records the valid indirect call targets in the binary's headers and injects a check before each indirect call; the Windows kernel keeps a bitmap of valid targets and terminates the process if a check fails. Microsoft's docs list the details that matter:

  • Pass /guard:cf to both compiler and linker, plus /DYNAMICBASE. It is off by default and incompatible with /ZI and /clr.
  • Since Visual Studio 2017 it also guards switch statements that generate jump tables.
  • It runs on Windows 10, Windows 11 and Windows Server 2019 and later, and stays compatible with CFG unaware versions.
  • Code built without the option is not protected, so Microsoft recommends building all code with it.

Like IBT, CFG checks "is this a valid function entry", not "is this the right function for this call". It is a strong reduction in attack surface, not a complete policy.

Comparison table: which mitigation does what

MitigationEdgeHardware neededCompiler or linker flagOS support (from the sources)
Intel CET shadow stackBackwardCET or AMD shadow stack capable CPUGCC and Clang -fcf-protection=return or full; MSVC link /CETCOMPATLinux user space 6.6 with glibc 2.39; Windows user mode with /CETCOMPAT; Windows kernel mode on Windows 11 2022 update with VBS and HVCI
Intel IBTForward (coarse)CET IBT capable CPU-fcf-protection=branch or fullLinux kernel IBT since 5.18; user space IBT not in the documented upstream interface
Arm PAC (pac-ret)BackwardArmv8.3-A or later-mbranch-protection=pac-ret or standardLinux user space 5.0, kernel 5.7
Arm BTIForward (coarse, by branch type)Armv8.5-A or later-mbranch-protection=bti or standardLinux user and kernel 5.8
Arm GCSBackwardGCS capable core-mbranch-protection=gcsLinux user space 6.13
Clang CFIForward (type checked)None-fsanitize=cfi plus -flto and -fvisibility=hiddenAny OS the program runs on
Clang kCFIForward (function pointer type)None-fsanitize=kcfiLinux kernel since 6.1 (x86_64, arm64)
Windows CFGForward (valid entry)None/guard:cf (compiler and linker)Windows 10, 11, Server 2019 and later
Windows EH continuationException context targetsUsed with shadow stacks/guard:ehcont64-bit processes on 64-bit Windows

Decision table: what to turn on for what you ship

You shipStart withAdd when ready
Linux x86-64 user space services-fcf-protection=full, verify markers on every ELF objectRun on 6.6 or newer kernels with glibc 2.39 or newer and confirm shstk in /proc/PID/status
Linux AArch64 user space-mbranch-protection=standardgcs on kernels from 6.13 when your toolchain and C library support it
C++ code with heavy virtual dispatchHardware flags aboveClang CFI with ThinLTO and -fvisibility=hidden for the main binary
Windows applications/guard:cf and /CETCOMPAT/guard:ehcont for 64-bit builds
Windows fleetsMemory integrity (HVCI)Kernel-mode Hardware-enforced Stack Protection once incompatible drivers are resolved
Custom Linux kernelsDistribution defaults for IBT and kCFIReview driver and module builds with the same compiler

Step by step: build, verify and audit CFI on Linux

These steps use only standard tools: GCC or Clang, binutils readelf, and files under /proc. Nothing here changes system state except the test binary you build.

Step 1: Check what the machine supports

On x86-64, look for the CPU and kernel flags:

# x86-64: shadow stack and IBT flags reported by the kernel
grep -o -w -E 'user_shstk|shstk|ibt' /proc/cpuinfo | sort -u
uname -r
ldd --version | head -1

user_shstk means both hardware and the running kernel support user space shadow stacks. You also want a 6.6 or newer kernel and glibc 2.39 or newer, as the OpenSSF hardening guide states.

On AArch64, look for the hardware capability names in the features line:

# AArch64: pointer authentication, BTI and GCS hardware capabilities
grep -m1 -o -w -E 'paca|pacg|bti|gcs' /proc/cpuinfo | sort -u
uname -r

Step 2: Compile with the right flags

For x86-64:

# Build with shadow stack and IBT markers, and make the linker
# complain about any input object that lacks them
gcc -O2 -fcf-protection=full -o hello hello.c -Wl,-z,cet-report=error

-fcf-protection alone is an alias for full, which means both branch (IBT) and return (shadow stack). GCC defines the __CET__ macro with bit 0 for branch and bit 1 for return, so source code and build scripts can tell what was requested. To override an earlier setting on the same command line, the GCC manual says to pass -fcf-protection=none first and then the value you want.

The linker option is the important one for supply chain hygiene. GNU ld's -z cet-report=error turns a missing GNU_PROPERTY_X86_FEATURE_1_IBT or GNU_PROPERTY_X86_FEATURE_1_SHSTK property in any input object into a hard error. The default is none, which reports nothing. warning is a good first step on a large codebase.

For AArch64:

# Build with PAC return signing and BTI landing pads
gcc -O2 -mbranch-protection=standard -o hello hello.c

If you build with LTO, the GCC manual adds -fcf-protection=check for the final link, which reports an error when LTO objects were compiled with different -fcf-protection values.

Step 3: Check the ELF property note

The markers live in the .note.gnu.property section. The kernel documentation shows how to check them:

readelf -n ./hello | grep -a -i 'feature'

On a fully marked x86-64 binary you should see a properties line listing x86 feature: IBT, SHSTK. On AArch64 the line names AArch64 feature: followed by BTI and PAC. If the line is missing, or lists only one feature, something in the link did not carry the marker.

Step 4: Verify at run time

A marked binary is not proof that protection is on. Check the process itself:

./hello &           # or pick the PID of a long running service
grep -E 'x86_Thread_features' /proc/$!/status

x86_Thread_features: shstk means the shadow stack is active. If the line is empty on a 6.6 or newer kernel, the loader did not enable it: check the binary and every library it loads. The OpenSSF guide says shadow stack must also be enabled at run time through glibc, giving GLIBC_TUNABLES=glibc.cpu.hwcaps=SHSTK as the example, so test the default behaviour of the exact glibc you ship. glibc also documents glibc.cpu.x86_shstk and glibc.cpu.x86_ibt tunables (on, off, permissive) for testing:

# Testing only: see what happens when a non-CET library is dlopened
GLIBC_TUNABLES=glibc.cpu.x86_shstk=permissive ./service

Step 5: Audit every binary and shared library you ship

This is where most real gaps are found. A container image or package can contain hundreds of ELF files, and only one needs to be unmarked to cause trouble. A short read only script lists the ones without both markers:

#!/usr/bin/env bash
# List ELF executables and shared objects that lack x86 IBT/SHSTK markers.
# Read only: it inspects files and changes nothing.
root="${1:-/usr}"
find "$root" -xdev -type f \( -perm -u+x -o -name '*.so*' \) -print0 2>/dev/null |
while IFS= read -r -d '' f; do
  head -c 4 "$f" 2>/dev/null | grep -q $'\x7fELF' || continue
  props=$(readelf -n "$f" 2>/dev/null | grep -a 'x86 feature')
  case "$props" in
    *IBT*SHSTK*) ;;                       # fully marked
    *) printf '%s\t%s\n' "$f" "${props:-no CET property}" ;;
  esac
done

For AArch64, change the pattern to match AArch64 feature with BTI and PAC. Run it against an unpacked container image file system or a package's install tree, not just your own build output. The interesting results are usually prebuilt vendor libraries, plugins, language runtime extensions and old static archives linked into new code.

To see which libraries a specific program will actually load:

ldd ./service | awk '/=>/ {print $3}' | xargs -r -n1 sh -c 'printf "%s\t" "$0"; readelf -n "$0" | grep -a -c "x86 feature: IBT, SHSTK"'

A 0 next to a library means it lacks the full marker set.

Step 6: Verify on Windows

Microsoft documents the CFG check with dumpbin from a Visual Studio command prompt:

dumpbin /headers /loadconfig service.exe

A CFG enabled binary shows Guard in its DLL characteristics, and the load config shows CF Instrumented and FID table present. Run the same check over every DLL your application loads. For shadow stack compatibility, confirm that /CETCOMPAT is set in the linker properties of every project you own, and ask suppliers of prebuilt DLLs to state whether theirs are built with it.

What breaks, and why

CFI is compatibility sensitive because it changes what the processor accepts as normal. These are the classes of breakage documented in the primary sources, plus the general categories every team runs into.

One unmarked object or library

On Linux, each object file carries its own CET property, and the linker only keeps the marker on the output when every input has it. A single assembly file, an old static archive or a third party object without the marker silently produces an unmarked executable. That is the reason GNU ld offers -z cet-report. At run time, glibc's documentation describes the default for dlopen: when shadow stack is enabled, loading a non CET shared library returns an error. The permissive setting avoids the error by turning shadow stack off instead. For IBT, glibc's default already disables IBT for non CET executables and shared libraries. Either way, one legacy plugin decides the protection level of the whole process.

Just in time compilers

JIT engines generate code at run time, so they must emit landing pads (ENDBR or BTI) at any address they will reach indirectly, and their own call and return conventions must agree with the shadow stack. On Windows, Microsoft documents that VirtualAlloc and VirtualProtect by default treat new executable pages as valid CFG call targets, and that a JIT can use PAGE_TARGETS_INVALID or PAGE_TARGETS_NO_UPDATE and then register only the real entry points with SetProcessValidCallTargets. Language runtimes with JITs usually ship their own CFI support; check the release notes for the version you run.

setjmp, longjmp, coroutines and unwinders

Anything that moves the stack pointer without a matching call and return has to update the shadow stack too. That includes longjmp, user level context switching, green threads, coroutine libraries and C++ exception unwinding. glibc's own implementations are adapted when glibc is built with CET, but hand written stack switching in a library or runtime is a common source of control protection faults on the first CET enabled run. On Windows, the equivalent issue for exception continuation is what /guard:ehcont exists to handle.

Obfuscated and return-rewriting drivers

On Windows, Microsoft documents that drivers which hijack return addresses, often through code obfuscation engines used to protect intellectual property, are incompatible with kernel-mode stack protection. Windows maintains a blocklist of such drivers. When the feature is on, those drivers are not allowed to load, and if one is already installed the feature fails to enable until it is removed or updated.

Does CFI replace memory safety?

No. CFI constrains where control can go after memory is corrupted; it does not prevent the corruption. A buffer overflow that flips a permission flag or an out of bounds read that discloses memory never touches a return address or function pointer. CFI is not designed to stop these data only attacks.

Coarse forward edge checks also leave room. IBT, BTI and CFG accept any valid function entry, so a corrupted pointer can still call a legitimate but wrong function. Even type based CFI accepts any function with a matching signature.

CFI raises the cost of turning a bug into code execution. Memory safe languages, memory tagging, bounds checking and, above all, fixing the vulnerable component remain necessary. Our introduction to C and C++ memory safety vulnerabilities covers the bug classes CFI does not address.

Why CFI is a supply chain problem

Most of a running process is code you did not write: the C library, TLS and compression libraries, runtime extensions, vendor SDKs and plugins. Each carries its own CET or BTI marker, and the least protected one sets the level for the process.

That turns CFI into an inventory question:

  1. Which components does each image and package contain? You cannot audit markers on libraries you do not know you ship.
  2. Which of them are prebuilt? Source dependencies inherit your flags. Prebuilt wheels, gems, Node native modules, vendor .so files and static archives carry whatever flags their builder used.
  3. Which builds are old? The OpenSSF guide notes that upstream GCC defaults do not enable some mitigations and that newer distribution compilers are more likely to, so old builds and old toolchains are where markers go missing.
  4. Which have known vulnerabilities? A memory corruption CVE in a library you ship is exactly the bug CFI is meant to contain. If that library is also the unmarked one, you have both the bug and the gap.

Rebuilding a dependency with the right flags fixes the marker. Upgrading it to a fixed version fixes the bug. Both need the same starting point: knowing which component, which version, and where it runs.

Common mistakes

  • Assuming hardware support means protection is on. A CET capable CPU with a pre 6.6 kernel, an older glibc or an unmarked binary has no user space shadow stack. Check /proc/PID/status, not the CPU model.
  • Letting one unmarked dependency switch CET off. A legacy assembly file, plugin or prebuilt library quietly drops the marker or downgrades the process. Use -z cet-report=error in CI and audit every ELF in the final image.
  • Testing only debug builds. Optimisation level, LTO and link order change which objects end up in the final binary and how code is laid out. Run the marker audit and a smoke test on the exact release artifact you ship.
  • Treating CFI as a substitute for patching. CFI makes exploitation harder; it does not remove the vulnerability. A known memory corruption bug still needs the fixed version.
  • Leaving permissive tunables on in production. glibc.cpu.x86_shstk=permissive is useful for diagnosis. Left on, it lets any non CET library loaded later turn the protection off.
  • Forgetting the linker half on Windows. /guard:cf and /guard:ehcont must reach both compiler and linker when they run separately, and /CETCOMPAT is a linker option.
  • Ignoring JIT and stack switching code. Custom coroutine libraries, hand written context switches and embedded script engines are the first place control protection faults appear.
  • Turning on kernel stack protection without a driver review. On Windows, an incompatible driver blocks the feature from enabling. Review the incompatible driver list before a fleet rollout.

FAQ

What is the difference between a shadow stack and a stack canary?

A stack canary is a random value placed between local buffers and the saved return address. If a linear overflow overwrites the canary, the check at function exit detects it. A shadow stack keeps a hardware protected copy of the return address itself and compares it on every return. A canary can be skipped by a write that does not pass through it; a shadow stack does not depend on how the return address was changed.

Does Intel CET work on AMD processors?

Shadow stacks do. Microsoft's documentation lists AMD shadow stacks on Zen 3 and newer cores alongside Intel CET as supported hardware for kernel stack protection, and Linux uses the same user space interface. The Linux documentation notes that not every CPU has both shadow stack and indirect branch tracking, so check /proc/cpuinfo for each flag separately.

Which Linux kernel version supports user space shadow stacks?

Linux 6.6, released on 29 October 2023, added x86 user space shadow stack support for 64-bit kernels. You also need glibc 2.39 or newer, binaries and libraries built with -fcf-protection, and, per the OpenSSF guide, the shadow stack enabled at run time through glibc. Arm's Guarded Control Stack followed for user space in Linux 6.13.

How do I check if a binary is CET enabled?

Run readelf -n binary | grep -a feature and look for x86 feature: IBT, SHSTK in the GNU property note. To confirm that protection is actually active, check x86_Thread_features in /proc/PID/status while the program runs.

What is the difference between Arm PAC and BTI?

PAC protects pointers, most commonly return addresses, by signing them with a secret key and checking the signature before use. BTI protects indirect branch targets by requiring each one to land on a BTI landing pad. PAC defends the backward edge; BTI defends the forward edge. -mbranch-protection=standard turns on both.

Is Windows Control Flow Guard the same as CET?

No. Control Flow Guard is a compiler and operating system scheme that checks indirect call targets against a set of valid function entries. CET shadow stack is a processor feature that checks return addresses. Microsoft's documentation describes them as complementary: CFG for indirect calls, hardware-enforced stack protection for returns.

Does control flow integrity slow programs down?

Hardware schemes are designed to be cheap; the OpenSSF guide describes the performance impact of -fcf-protection=full as typically mild because of hardware assistance. Software schemes such as Clang CFI and CFG add checks to indirect calls, with cost that depends on how call heavy the code is. Measure on your own workload with the exact flags you plan to ship.

Why did CET get disabled for my process when one library was loaded?

Because the markers are per object. If glibc is configured to run permissively, loading a library built without CET markers turns shadow stack off for the process; with the default setting the dlopen fails instead. Rebuild or replace the library with a version built with -fcf-protection, then confirm with readelf -n.

Why software supply chain hygiene still matters for CFI

Control flow integrity is decided at build time, one object and one library at a time, and the weakest component in a process sets its protection level. The libraries that most often turn it off, or that carry the memory corruption bug CFI is meant to contain, are ordinary open source dependencies and prebuilt images, so knowing exactly which versions you ship, and moving them to fixed releases quickly, is part of making CFI work.

Fix it without waiting for upstream

Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.

1. Autonomous remediation: pull requests, not tickets

When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation categories include dependencies, licenses, security posture and findings, and each one is set separately. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.

2. Gold Open Source: the safe version in seconds, free

gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds.

3. Gold Registry: start from components that are already fixed

Gold Registry provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs and a completed license check, with signed build provenance (signed with Safeguard's key and verifiable with cosign, SLSA Build Level 3 as the design target) and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.

When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. Ask which packages it covers before you plan around it.

Your situationWhat to use
A fixed version existsAutonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version
You need to know today whether you are affectedGold Open Source lookup and "Am I affected?" checker, free and without login
You want vulnerable versions to stop shippingSafeguard policies and gates in CI
Upstream has no fix yet, or will not ship oneA custom Gold request; fork and patch
You want to start clean on new projectsPull hardened components and images from Gold Registry

One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.

See how Safeguard would help

To be plain about the fit: Safeguard does not inspect ELF property notes or PE load config tables, does not check binaries for CET, BTI or CFG markers, and does not configure kernels, glibc tunables or Windows security features. The marker audit in this post is something you run with readelf and dumpbin. Where Safeguard helps is the inventory and patch side. It scans repositories, container images and SBOM files for dependencies, matches them against vulnerability data and shows which projects contain a given component and version, so when your audit finds an unmarked or vulnerable library you can see everywhere it ships. Gold Open Source at gold.safeguard.sh gives free CVE intelligence with affected and fixed versions for those components.

The first demo is a real CLI session. Asked to fix a lodash CVE, the assistant reads the finding and advisory, asks before any command that changes something, edits package.json to lodash 4.17.21, runs npm install and npm test with your approval, prints a summary and pushes nothing. The same flow applies when a native module or bundled library in your project needs to move to a fixed release that was also rebuilt with modern hardening flags.

Click through it at your own pace, or open it full screen.

The second demo sets up the local runner on your own machine, approves it with its device code, then scans from the terminal. Build hosts and image pipelines that produce your hardened binaries often sit on internal networks, and the runner scans repositories and images where they are built.

Click through it at your own pace, or open it full screen.

The third demo connects Claude to the Safeguard MCP server with OAuth and lets you choose which tools Claude can use. It is a quick way to ask which of your projects carry a particular library version when your marker audit or an advisory points at it, with tool permissions you control.

Click through it at your own pace, or open it full screen.

To try the CLI, download Safeguard and follow the CLI documentation.

Quick check

A binary shows "x86 feature: IBT, SHSTK" in readelf, but /proc/PID/status shows no shstk. What should you check?

The marker only declares compatibility. Check that the kernel is 6.6 or newer with user_shstk in /proc/cpuinfo, that glibc is 2.39 or newer and configured to enable shadow stack, that nousershstk is not on the kernel command line, and that every shared library the process loads also carries the marker.

Which protects the forward edge: Arm PAC or Arm BTI?

BTI. It requires indirect branches to land on a BTI landing pad. PAC, used through pac-ret, signs and checks return addresses, which is the backward edge.

Sources

Last reviewed: 25 October 2026. Kernel, glibc and compiler support for CFI keeps moving; check the linked documentation for the versions you run.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.