[RFC] Hardening mode for the compiler

This is a joint proposal from: @AaronBallman, @shafik, @Endill, and @cor3ntin (with helpful input from others!)

Safety and security of C and C++ programs has been an important issue in the ecosystem for a while. Both WG21 and WG14 are making plans on how to improve these aspects of the language from their end, but the standard is constrained by what it can talk about and the speed at which it can move. Implementations need to be the driving force behind improving this situation; we’re ultimately responsible for the quality of what we provide our users. To that end, we should seriously consider what the Clang approach to hardening will be.

Thankfully, our implementation already provides a ton of mechanisms to help users improve the safety and security of their code. Unfortunately, those mechanisms are scattered throughout the implementation (some are feature flags, some are machine flags, some are macros, some are warnings, etc), poorly documented or not documented at all, and are not always easy to use. There are guides online to help users, but this requires more proactive consideration from users than we think is reasonable.

We’re proposing a path forward to unify a lot of our existing mechanisms in an easy-to-use way for users.

Setting User Expectations

One difficult decision we frequently face is “will this change break existing code?”. To date, user expectations have been set that we will work hard to avoid breaking their existing, working, correct code. For example, when deciding whether to emit a diagnostic or not, if we cannot prove something is wrong, we will frequently suppress the diagnostic (reduce false positives). However, this is in direct conflict with the needs for safety and security. In that scenario, if we cannot prove something is right, we should generally diagnose the construct (increase false positives). So this mode needs to set user expectations appropriately: your code breaking between compiler releases is a feature, not a bug. Obviously, we still want to strive for a low false positive rate in hardened code; it’s just that we should feel comfortable with rejecting correct, but suspicious, code. This includes a need for potentially ABI breaking changes; when the next spectre vulnerability hits, we want users to get the mitigation automatically rather than having to learn that they need the mitigation at all.

Prior Art in GCC

GCC has added a -fhardened mode (https://gcc.gnu.org/onlinedocs/gcc-15.1.0/gcc/Instrumentation-Options.html#index-fhardened). This mode enables several feature flags (-ftrivial-auto-var-init, -fstack-protector-strong, etc) and predefines some macros (_FORTIFY_SOURCE=3, _GLIBCXX_ASSERTIONS). It does not currently modify any diagnostic behavior or set any machine flags.

We could follow GCC’s lead and do exactly as they do and add -fhardened which aims for compatibility with GCC. However, we do not recommend aiming for an identical implementation between Clang and GCC. Differences in behavior between the two compilers already makes matching features difficult. When it comes to omnibus options, it becomes even harder. So we will likely never behave the same as GCC anyway – better to set user expectations up front that Clang and GCC hardening aim to solve the same problem, but may do so in different ways sometimes.

Goals

We want to enable various -f, -m, -D, and -W flags in this hardened mode, but we do not want users to have to manually figure out the set to enable. So we’re looking to add some one-shot way for the user to enable various flags, for example:

  • It can enable various -f flags by default: -ftrivial-auto-var-init, -fPIE, -fcf-protection, etc.
  • It can enable various -m flags by default: -mspeculative-load-hardening, -mlvi-hardening, etc.
  • It can enable various -W flags by default: -Wall, -Wextra, -Werror=return-type, etc.
  • It can enable any hardening modes available for the standard library
  • It can predefine various macros: _FORTIFY_SOURCE, _GLIBCXX_ASSERTIONS, etc.
  • It can require the user to specify an explicit language standard mode
  • It can refuse to compile code against older language standards we feel are inherently unsafe, like C89 or C++98
  • It can pass along linker flags like enabling ASLR

The exact flags and behaviors can be determined as we go and can be extended as we introduce new functionality or diagnostics into the compiler.

Proposal

There are multiple ways we could surface this functionality. We’re presenting options to see what direction the community thinks will best meet the goals outlined. Other alternatives may also exist which are worth considering.

Config File

A configuration file could be shipped alongside Clang. Users would set the hardened mode by passing --config=hardened (or some other single flag that effectively translates into –config under the hood). A benefit of this approach is that maintenance of hardened mode is very easy and downstreams can modify the behavior easily to meet their own needs.

Driver

By using a new driver mode, we reset user expectations about how compiler upgrades will go. They’re no longer running clang, it’s now a “different” compiler. The significant downside to a new driver is that it can be a challenge to integrate it into existing build systems like CMake. However, clang --driver-mode= can still be used as a stopgap solution until build systems catch up.

Orthogonal Flags

We could have orthogonal -fhardened, -mhardened, and -Whardened options to specify language dialect flags and macros, machine flags, and diagnostic flags respectively. It’s not a single flag for a user to pass to get all the hardening, but it does let the user have an a la carte approach to opting into hardening.

Single Flag

We could have a single flag such as -fhardened which enables all the macros, language dialect, machine, and diagnostic options so that we have a single flag for users to opt into everything. However, it would be a novel direction; we don’t have any other flags with such wide-reaching impacts across -f, -m, -W, etc behaviors. Also, if we name the flag -fhardened, there will be pressure to be compatible across GCC and Clang, which is a burden on both communities, so if we go with a single flag, we should pick a unique name for it.

What Are We Deciding Right Now?

We are looking for high-level direction from the community on how to proceed. Once we know that the community supports the notion of a hardened mode, and we know the general shape of how the community wants that mode surfaced, we intend to come back with a separate proposal for that particular path forward as well as the initial set of functionality enabled by that mode.

13 Likes

My personal stake in the ground (may or may not be shared by anyone else) is that I prefer a driver mode or --config over a single flag or orthogonal flags.

I like that a new driver mode really resets user expectations about how the compiler behaves. I think that gives us a lot more freedom of choice for how to surface hardening decisions without having to worry about “clang” as it used today. However, I also recognize that integration with build systems is a problem, particularly early on.

I think --config is a pretty reasonable compromise. The fact that it’s easy for us to maintain and easy for downstreams to customize with their own downstream flags is a really good thing! It doesn’t reset user expectations about how Clang works, but it still fits nicely with our usual last-flag-wins behavior and so it’s sufficiently easy to adopt.

I don’t like the single flag because I think it’s trying to do too much under one flag; I really think that hardening will require touching more than just language dialect flags, and it’s an odd design to have a -f flag that impacts -f, -m, -D/-U, and -W flags.

I don’t like the orthogonal flags because it’s not a one-shot thing for users to opt into and the split seems likely to lead to frustration or missed opportunities.

That said, this is a complex design space, so none of these opinions are strongly held.

3 Likes

Thank you for the RFC, I believe this is a very good initiative to improve the default level of security (and consequently safety).

Some points to consider.

In a previous discussion [Clang] Add support for -fhardened · Issue #122687 · llvm/llvm-project · GitHub it was suggested to also include libc++ hardening mode into the scope. Since libc++ hardening provides several levels, this resulted in a question if the hardening option also needed multiple levels.

In addition, some of the options suggested in the OpenSSF guide are specific to Linux or, at least, not available in embedded environments, which again suggests that it would be nice to have the hardening option differ in settings be able to work for both.

Config files is a very interesting option, as you suggested, however it has the limitation that it cannot express any conditions to include or not an option, thus in case there are different levels or sets of hardening options, this would need different config files, which may become unmanageable quickly.

From this point of view, driver implementation is more flexible, though a bit more difficult to change.

Of course, there is always an option to extend config files to make them more expressive or, as discussed in the recent embedded working group call, provide some other mechanism that would allow adding/customizing command line options based on other command line options, however this should be a wider design consideration, not for hardening specifically.

1 Like

To clarify, does that mean clang would deliberately ignore -fhardened or even refuse to compile? What would be the compatibility story here?

On one hand I think it is valuable to let users customize this configuration for their needs but on the other hand I am not sure if we can still call the customized version hardened. Seeing a config called hardened that barely turns on any of the mitigations sounds deceiving and makes auditing for security a bit harder.

This is a bit concerning even if we have a stopgap solution. I think the less friction users have to adopt this mode the better.

Also, I am not sure how well could we manage user expectations from the compiler. There might be a significant portion of the C++ developers who barely ever interact with the compiler directly. They might either invoke build systems or push a button in an IDE. Any decision that we make about the command line interface to manage expectations would never propagate to these users.

An unrelated question: whatever we come up with do we plan to backport it to some stable point releases to ease adoption for some folks?

1 Like

How does this work, for setups that already use config files for target specific configuration?

To be concrete; for my mingw targets, I use a bunch of nondefault settings (-rtlib=compiler-rt -unwindlib=libunwind -stdlib=libc++ -fuse-ld=lld). These are picked up from config files named e.g. x86_64-w64-windows-gnu.cfg next to the clang binary, implicitly when invoked as either x86_64-w64-mingw32-clang or clang --target=x86_64-w64-mingw32. (The target triple gets normalized before picking the config file name to look for.) If an an explicit --config option here inhibits the search for the implicit config file for the target, it would break my setup.

Assuming we didn’t decide we wanted to aim for perfect compatibility with GCC, I think we’d likely reject -fhardened as an unknown command line option. I don’t think we’d want to accept and silently ignore it.

The compatibility story would be that we’re ABI incompatible with GCC and with other versions of Clang with this flag.

It’s really no different than any other downstream decision to deviate from upstream, right? A downstream could patch Clang in any given way to change the set of options they enable.

Backport to earlier versions of Clang? I wasn’t expecting we’d do that.

I was thinking that you can have multiple --config uses on the same command line, with the usual last-flag-wins behavior. Would that suffice for your needs?

Oh, maybe I’m talking past you.

I was thinking this configuration file would be shipped in some sort of “resources” directory as a compiler artifact. So it wasn’t editable for users (per se), but it was something that distributions or downstreams could modify without having to change Clang’s source code directly.

Users would do something like: clang --config=hardened foo.cpp and --config would look for hardened along the usual paths, but it would include the resource directory so the user doesn’t have to spell out a full path themselves.

This does mean something could come along and hijack the file found by inserting something in the search paths, though. So maybe we could spell it --hardened and it acts as though it was --config but with extra measures to only look in one place for the file.

Not necessarily; currently a number of cases rely on the config files being picked up implicitly.

If I have e.g. x86_64-w64-mingw32-clang as a symlink to the clang executable, clang implicitly figures out the config file to load. Instead of a symlink, this can also be a wrapper script/executable (it actually is) - this could then internally invoke clang --config=x86_64-w64-mingw32 (or even invoke clang --my-custom-defaults - this actually was how I had it set up before).

However when using other clang tooling, e.g. clangd (similar things also apply with clang-scan-deps), it only sees the command invocations from compile_commands.json, where it sees that the build system is invoking x86_64-w64-mingw32-clang. clangd then figures out that this is mostly equivalent to clang --target=x86_64-w64-mingw32(which does trigger looking for such an implicit config file) - but it can’t guess that `x86_64-w64-mingw32-clang actually is a wrapper script or figure out what nondefault flags that script might be passing.

So in order for my nondefault config flags to get picked up by all sorts of clang tooling, I use implicitly loaded config files, which all tools end up loading correctly, rather than having a wrapper explicitly pass options.

1 Like

My personal stake here is that I feel like the driver option feels like a bit too much friction.

I personally prefer orthogonal flags with the ability to have a config if you want really fine grain control.

I would be happy with just a config as compromise, it feels like a bit of friction but not too bad and I think some sort of config will be needed even if we have flags.

I don’t have a good feeling for how the community will feel but I think I would rather see us move forward than get stuck on this point.

3 Likes

Hi Aaron o/

Thanks for proposing this :slight_smile:

A pain point for getting distros to adopt -fhardened is that the flag is not tunable [0][1].

For example, being able to “-fhardened=no-stack-protector,no-relro” would turn off stack protection and relro. - e.g., by Neal Gompa [1]

The issue with non-tunable omnibus flags is that users (and distros) turn off the flag (and therefore disable all child flags) if one of the child flags cause a package to not build.

By getting distros to adopt flags, they will rebuild their package archive and discover related FTBFS bugs in the packages. The distro which opts into a flag first pays the highest triage and reporting cost, but make it easier for other distros to follow step.

edit: These omnibus flags are great in the sense that downstreams pickup new recommendations over time (i.e., causes distro toolchain teams to begin identifying package bugs around the same time). Extending these flags with tunables would be awesome!

Thanks for taking the time to consider my comment.

[0] Bug #2080267 “Please add -fhardened to default build flags” : Bugs : gcc-14 package : Ubuntu
[1] 2312869 – Consider using -fhardened for GCC

1 Like

I guess my biggest concern when you’re talking about some kinds of hardening measures is that it’s hard to make it one-size-fits-all for some kinds of mitigations.

Some mitigations require highly specific environments. Like, in some environment, it might make sense to strongly enable ARM BTI: build all code with it, and pass a flag to the linker to error if it sees non-BTI code. But if you turn that on with some random Linux distribution which isn’t built with that in mind, builds will just blow up with an error the user can’t fix. If you have top-to-bottom control, you can ensure consistency, but LLVM community doesn’t have that kind of control over the environments people are using.

So I think we need to rely on vendors and users to some extent; we can explain what we think a hardened config should look like, but ultimately we can’t pick the “best” flags without context. So I think we need more of a “hardening menu”, so vendors/users can choose.


Along those lines, I think we should ensure that everything is controlled by specific flags. Like, if we want to require users to explicitly pass an -std flag referring to a recent language standard, we should add a flag -fsecure-language-standard.

4 Likes

Here is my take the gcc documentation on -fhardening already says it may different between major versions of gcc.

“The precise flags enabled may change between major releases of GCC,”

So why not just reuse the option. That is there is compatibility option wise but internally it will be different based on what the compiler developers think is best.

1 Like

I should mention the list gcc came up with was discussed from a distro point of view. So for Linux targets get input from distros and android from goolgle, Darwin from apple, etc. The base flags will be shared.

I would prefer the Theo De Raadt scorched earth approach and i would implicitly enable hardening options, by default, and then force the user to disable them if they need to be unsafe. This way, we already know what the default is, and force developers to modify the code to behave that most sane way. In 1972 it probably didnt matter when writing C programs, but in 2025 it very much does.

Could we descope ABI-impacting options here? I think the hardening option (however it ends up being spelled) would likely become a “best practice” kind of thing, but as a distributor I really would hate to have to turn off that option (or worse: audit every project for consistent usage) because it affects the ABI.

Libc++’s model there is IMO worth emulating - the hardening modes explicitly stay ABI-compatible.

3 Likes

Happy to connect LLVM with the security teams of major distros for input.

It would be great to make -ftrivial-auto-var-init=zero and similar flags default, which have significant security value and both minimal performance cost and break potential. The C++ Standards Committee suggested this flag alone would mitigate 10% of C++ vulns [0]. I’ve written about a related vuln in rsync [1][2].

Having as many hardening defaults opted in as feasible would be great. The OpenSSF’s Hardening Guide recommendations attempt to balance security value against performance cost and break potential [3].

[0] https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2723r1.html
[1] https://ubuntu.com/blog/rsync-remote-code-execution
[2] Mitigating a rsync Vulnerability: A Lesson in Compiler Hardening
[3] Compiler Options Hardening Guide for C and C++ | OpenSSF Best Practices Working Group

p.s., if anyone is interested in how compiler flags are applied across distro packages see: compiler-flags-distro/README.md at main · jvoisin/compiler-flags-distro · GitHub

Note, that in most cases these flags are only being applied to packages built by the distro and are not being applied to the compiler itself (i.e., if you use that distro in your CI, you probably need to opt into hardening flags).

edit, as a new forum user I am unable to post more than three times in this thread

Basically we want more “optionaility” than using the same flag as gcc would give us. If we use the same flag there will be a lot of pressure to attempt one for one compatibility, which may not even exist with the current flags gcc is using.

We also realize that some folks may want to be super aggressive and some folks may not be able to be as aggressive due to various complex reasons. We don’t want to limited to the least common denominator. We consider there may be a subset of folks who really want to aggressively get to something they consider “Safe C++” and they should be able to forge ahead without harming other users that can not be so aggressive.

Tl;DR; we don’t think there can be one flag to rule them all here.

What I already understood from previous conversion:

On the one end, we want to make it very easy for users to enable hardening and preferably as much as possible. On the other end, we want to make it very easy to disable pieces of this hardening such that features can be disabled.

We want something where users will expect breakages on upgrade, yet it shouldn’t break existing build systems by being too different.

When I hear this, I’m immediately thinking about clang-tidy. A config file that’s easy to generate, which can have the safe defaults. Whenever a new option gets added, it is enabled by default and a new config file can be generated.

The advantage of this would be that the decision can be made on a global level, without having to repeat them on the command line (making files like compile_commands.json/build.ninja smaller)

The generated config file can have an explicit disclaimer about upgrade behavior expectations.

Maybe it can even be made more generic, basically becoming a kind of toolchain file which describes the new defaults we want while preserving existing behavior when used without config file. Which sounds almost like a separate driver.

I already see me using such a config file.

1 Like