Compiler flags fall into three groups: ones for development only, such as treating all warnings as errors, ones for shipped builds, such as zero initialization of locals, and ones that need careful testing first. Mixing the groups up either breaks distributed source builds or leaves bugs unprotected.
Development only
The OpenSSF guide says to use -Werror in development only and not in distributed source. A new compiler version can add warnings, and those turn into build failures for users who rebuild your code. Sanitizer builds, covered elsewhere in this series, also belong here, along with the debug mode of the C++ library hardening.
Production oriented flags in the guide
The guide lists these under production use:
- -ftrivial-auto-var-init=zero. Zero initializes automatic variables that have no initializer.
- -fno-delete-null-pointer-checks. Keeps null checks the compiler could otherwise remove.
- -fno-strict-overflow. Gives defined behavior to signed integer and pointer overflow.
- -fno-strict-aliasing. Stops the compiler assuming strict aliasing rules.
- -fzero-call-used-regs. Zeroes used general purpose registers on return, which limits data lifetime and gadgets. It needs Clang 16 or later.
Why they are not a free upgrade
Several of these change semantics in your favor, which can hide a bug instead of fixing it. Zero initialization turns an unpredictable value into a predictable one, which is safer, but the logic may still be wrong. Defined overflow behavior is safer than undefined behavior, yet the number is still wrong.
Run the test suite with the flags on, and watch benchmarks. Some flags cost a little speed or size.
An example of a flag that changes behavior
Take -fno-delete-null-pointer-checks. In C, dereferencing a null pointer is undefined behavior, so a compiler may reason that a later null check is unnecessary and remove it. Keeping the check means the program takes the defensive branch you wrote. That is a security win in production, but the better fix is to find why the pointer can be null at all.
The same logic applies to -fno-strict-overflow and -fno-strict-aliasing. They stop the optimizer from exploiting undefined behavior your code may be relying on by accident. Each can cost some speed, because the compiler has fewer assumptions to work with, so measure before you make them universal.
How to introduce one safely
- Turn the flag on in a test branch and run the whole suite.
- Compare performance on a representative workload.
- Compare binary size, since some flags add code.
- Ship to a small share of traffic first, if you can.
- Record the flag, the date and the measured cost in your flag table.
A simple policy
- Developer builds: warnings on, sanitizers on, debug hardening on.
- CI: same as developer builds, with
-Werrorallowed because you control the compiler version. - Release builds: runtime hardening flags on, no
-Werror, no sanitizers. - Distributed source: never
-Werror.
Document the exceptions
If a component cannot take a flag, record which flag, why and who will revisit it. Silent exceptions are how hardening quietly decays.
Quick check
Why not use -Werror everywhere?
According to the guide it is for development only. In distributed source, a newer compiler may add warnings that break the build for downstream users.
Sources
Last reviewed: 19 October 2026.