The OpenSSF Compiler Options Hardening Guide for C and C++ gives a short core set of flags: optimization at level two, warnings, fortify source at level three, stack protection, stack clash protection, position independent executables and full RELRO. They reduce exploitability, but do not make C code memory safe.
A starter block for GCC or Clang
CFLAGS="-O2 -Wall -Wformat -Wformat=2 -Wconversion -Wimplicit-fallthrough \
-Werror=format-security \
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 \
-fstrict-flex-arrays=3 \
-fstack-clash-protection -fstack-protector-strong \
-fPIE"
LDFLAGS="-pie -Wl,-z,noexecstack -Wl,-z,relro -Wl,-z,now"
For C++ add -D_GLIBCXX_ASSERTIONS with libstdc++, or the libc++ hardening macro covered in a separate post.
What each does
- -O2. Needed because fortify source requires at least level one optimization.
- Warning flags. Surface suspicious conversions, format string misuse and silent switch fallthrough.
- -Werror=format-security. Turns non literal format strings used without arguments into errors.
- Fortify source 3. Adds compile time and run time checks on unsafe libc calls and buffer overflows. The leading undefine clears any predefined level.
- -fstrict-flex-arrays=3. Treats only trailing arrays declared with empty brackets as flexible, giving the compiler more bounds information.
- Stack clash protection. Probes large stack allocations so they cannot jump over the guard gap.
- Stack protector strong. Adds canaries to detect stack buffer overflows.
- PIE. Lets address space layout randomization apply to the executable.
- Non executable stack and full RELRO. Block code on the stack and make the global offset table read only after load.
Architecture specific extras
On x86_64 the guide lists -fcf-protection=full for control flow enforcement, which needs kernel 6.6 or later and glibc 2.39 or later for the shadow stack. On AArch64 it lists -mbranch-protection=standard for branch target identification and pointer authentication.
What these flags do not do
They add checks and raise the cost of exploitation. They do not remove the bug, and they cannot stop a logic error that reads inside a valid buffer. Think of them as an interim control while you work through a memory safety roadmap.
Verifying the result
A flag in a Makefile does not prove the binary has the protection. Build systems drop variables, third party code ignores them, and linkers can behave differently across platforms. Add a post build check to CI that inspects the produced binaries for the protections you expect, such as position independence, a non executable stack and full RELRO, and fail the build when one is missing.
Keep a small table in your repository with one row per component: which flags it uses, which it is exempt from and the reason. When a toolchain upgrade lands, the table tells you what to retest. The guide also notes some flags need a minimum compiler version, for example zeroing used registers needs Clang 16 or later, so pin your toolchain versions in the same place.
Common pitfalls
- Setting fortify source without optimization, which leaves it with no effect because it needs at least level one.
- Forgetting to undefine a predefined fortify level before setting the new one.
- Applying executable flags to libraries, or the reverse.
- Treating the flags as a reason to postpone rewriting exposed parsers.
Rolling it out
- Enable warnings first and clean up.
- Add the runtime hardening flags on a branch and run the full test suite.
- Compare binary size and benchmark results.
- Gate releases on a script that checks the final binary for the expected protections.
Quick check
Can I combine fortify source with AddressSanitizer?
The guide says they are incompatible, so leave fortify source off in sanitizer test builds.
Sources
Last reviewed: 19 October 2026.