Stack Canaries, NX, ASLR/PIE, RELRO and FORTIFY_SOURCE
What each classic exploit mitigation protects, the GCC and Clang flags that enable it, what it costs, and how to verify a binary with checksec and readelf.
Modern toolchains ship a set of mitigations that turn many memory-safety bugs from "attacker runs code" into "process aborts". None of them fixes a bug, and each has known limits, but together they raise the cost of exploitation dramatically. This guide covers the classic set on Linux (stack canaries, non-executable memory, ASLR with PIE, RELRO, FORTIFY_SOURCE and stack clash protection), shows the flags for GCC and Clang, and explains how to verify them on a real binary. Hardware-assisted control-flow protection is covered separately in control-flow integrity.
The recommended baseline
The OpenSSF Compiler Options Hardening Guide for C and C++ is the best current reference. A reasonable production baseline for GCC or Clang on x86-64 Linux looks like this:
CFLAGS="-O2 -g \
-Wall -Wextra -Wformat=2 -Werror=format-security \
-D_FORTIFY_SOURCE=3 \
-D_GLIBCXX_ASSERTIONS \
-fstack-protector-strong \
-fstack-clash-protection \
-fcf-protection=full \
-fPIE \
-ftrivial-auto-var-init=zero"
LDFLAGS="-pie -Wl,-z,relro -Wl,-z,now -Wl,-z,noexecstack"
On AArch64, replace -fcf-protection=full with -mbranch-protection=standard. Many distributions already inject most of these through their packaging defaults, but software built outside the distribution (vendored dependencies, container images, CI artifacts) frequently misses them. When _FORTIFY_SOURCE is already defined by the toolchain, use -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 to avoid redefinition warnings.
What each mitigation does
| Mitigation | Flag(s) | Protects against | Typical cost |
|---|---|---|---|
| Stack canary | -fstack-protector-strong | Linear stack overflows reaching saved data | Low (a load and compare per instrumented function) |
| NX / DEP | Default; -Wl,-z,noexecstack | Executing injected data | None |
| PIE | -fPIE -pie | Predictable code addresses in the main binary | Small on x86-64 |
| ASLR | Kernel (kernel.randomize_va_space=2) | Hard-coded stack, heap and library addresses | None |
| Partial RELRO | -Wl,-z,relro | Overwrites of relocation data outside .got.plt | None |
| Full RELRO | -Wl,-z,relro -Wl,-z,now | Overwrites of any GOT entry | Startup time for many imports |
| FORTIFY_SOURCE | -D_FORTIFY_SOURCE=3 (needs -O1+) | Overflows in memcpy, strcpy, sprintf and friends with known sizes | Low |
| Stack clash protection | -fstack-clash-protection | Large frames or alloca jumping the guard page | Low |
| Auto var init | -ftrivial-auto-var-init=zero | Leaks and bugs from uninitialised stack variables | Low to moderate |
| C++ library assertions | -D_GLIBCXX_ASSERTIONS | Out-of-bounds operator[], bad iterators in libstdc++ | Low |
Stack canaries
The compiler places a random value, read from thread-local storage (fs:0x28 on x86-64 glibc), between a function's local arrays and its saved frame pointer and return address. The epilogue compares it before returning and calls __stack_chk_fail, which aborts with *** stack smashing detected *** if it changed. The glibc canary's lowest byte is zero, so string functions that stop at a NUL byte cannot copy it. The stack protector also reorders locals so that arrays sit above scalars and pointers. See the stack canary glossary entry and the stack overflow guide for the frame diagram.
Limits: canaries only detect corruption at function return, only for instrumented functions, and not for writes that skip over the canary or target other locals. A memory disclosure bug can also leak the canary value.
NX / DEP
Pages are marked non-executable unless they contain code. The ELF GNU_STACK program header tells the kernel whether the stack needs to be executable; it should read RW, never RWE. An executable stack is usually caused by an assembly file without a .note.GNU-stack section, which makes the linker fall back to an executable stack; recent binutils warn about this.
Limits: NX stops injected code, not the reuse of existing code. That is the gap control-flow integrity addresses.
ASLR and PIE
ASLR randomizes the base addresses of the stack, heap, mmap region and vDSO at every execution. On Linux, kernel.randomize_va_space=2 enables full randomization, and it is the default. PIE extends randomization to the executable itself; without PIE, the main binary always loads at the same address, which leaves plenty of predictable code. GCC in most distributions now builds PIE by default.
Limits: randomization is per process, not per request. A forking server whose children never exec shares its parent's layout, and any information leak that reveals one address typically reveals the whole module's layout.
RELRO
Dynamic linking relies on the Global Offset Table (GOT), a table of addresses filled in by the loader. Writable function pointers used for every library call are an obvious target. Partial RELRO makes most relocation data read-only after startup; full RELRO (-z now) resolves every symbol eagerly so the entire GOT can be protected. See the RELRO glossary entry.
FORTIFY_SOURCE
With _FORTIFY_SOURCE defined and optimization on, glibc headers redirect calls such as memcpy, strcpy, snprintf and read to checking variants whenever the compiler can determine the destination object's size. Level 2 uses sizes known at compile time; level 3 (GCC 12+ and glibc 2.34+, or recent Clang) uses __builtin_dynamic_object_size to cover sizes only known at runtime, which substantially increases coverage. Violations abort with *** buffer overflow detected ***. Level 2 and above also reject %n in writable format strings.
Stack clash protection
A very large stack allocation, often from a variable-length array or alloca with an attacker-influenced size, can move the stack pointer past the guard page straight into another mapping. -fstack-clash-protection makes the compiler touch each page as the stack grows, so the guard page is always hit.
Verifying a binary
Never assume a binary is hardened because the build system says so. Check the artifact.
checksec
checksec (the standalone script, or the implementation shipped with pwntools) summarizes the main properties:
checksec --file=./server
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable
Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH No Symbols Yes 6 11
readelf, by hand
Every property can be confirmed with binutils:
# NX: GNU_STACK should be RW, not RWE
readelf -lW ./server | grep GNU_STACK
# RELRO: GNU_RELRO segment present; full RELRO also needs BIND_NOW
readelf -lW ./server | grep GNU_RELRO
readelf -dW ./server | grep -E 'BIND_NOW|FLAGS'
# PIE: type DYN plus the PIE flag
readelf -hW ./server | grep 'Type:'
readelf -dW ./server | grep FLAGS_1
# Canary and FORTIFY: look for the imported checking functions
readelf -sW --dyn-syms ./server | grep -E '__stack_chk_fail|_chk@'
Expected output on a hardened binary includes GNU_STACK ... RW, a GNU_RELRO segment, FLAGS BIND_NOW (or FLAGS_1 NOW PIE), Type: DYN, and imports such as __stack_chk_fail and __memcpy_chk.
On Debian and Ubuntu, hardening-check from the devscripts package performs similar checks. In CI, fail the build if a release artifact lacks PIE, full RELRO or a non-executable stack.
Windows equivalents
The same ideas exist in the MSVC toolchain, with different names:
| Linux / GCC / Clang | Windows / MSVC |
|---|---|
-fstack-protector-strong | /GS (on by default) |
NX (GNU_STACK) | /NXCOMPAT (DEP) |
| PIE + ASLR | /DYNAMICBASE, /HIGHENTROPYVA |
Clang CFI / -fcf-protection | /guard:cf (Control Flow Guard), /CETCOMPAT (shadow stack) |
-D_FORTIFY_SOURCE | Secure CRT functions, /sdl |
Tools such as dumpbin /headers or PowerShell modules like Get-PESecurity report these flags for PE files.
Common pitfalls
- Optimization off.
_FORTIFY_SOURCErequires-O1or higher; debug builds at-O0silently skip it. - Static linking of old dependencies. A statically linked library built without hardening brings unprotected functions into a hardened binary.
- Assembly files. Missing
.note.GNU-stacksections make the whole binary's stack executable. - Vendored build systems. Third-party
Makefiles andCMakeLists.txtoften overrideCFLAGS. Verify the final artifact, not the configuration. - Believing the checklist. A binary can pass every check and still be exploitable through a logic flaw or an information leak. Mitigations buy time and cost; they do not buy correctness.
Checklist
- Adopt the OpenSSF hardening baseline for every C and C++ build, including internal tools and containers.
- Verify release artifacts with
checksecorreadelfin CI and fail on regressions. - Keep
kernel.randomize_va_space=2on every host. - Add control-flow protection (
-fcf-protection=fullor-mbranch-protection=standard) as covered in control-flow integrity. - Keep hunting bugs with sanitizers and fuzzing: mitigations are the last layer, not the first.