Safeguard
Developer Tools•

Hardening flags: which belong in production and which in development

Some C and C++ flags catch bugs during development, others change how shipped code behaves. A sorting guide based on the OpenSSF hardening notes.

Safeguard Security Team
3 min read

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

  1. Turn the flag on in a test branch and run the whole suite.
  2. Compare performance on a representative workload.
  3. Compare binary size, since some flags add code.
  4. Ship to a small share of traffic first, if you can.
  5. Record the flag, the date and the measured cost in your flag table.

A simple policy

  1. Developer builds: warnings on, sanitizers on, debug hardening on.
  2. CI: same as developer builds, with -Werror allowed because you control the compiler version.
  3. Release builds: runtime hardening flags on, no -Werror, no sanitizers.
  4. 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.

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.