recorded decision · public · no signup

accepted2026-06-22rust-lang/rfcs

Mitigation enforcement

What was chosen

  • The Rust compiler will introduce mitigation enforcement, causing a compilation error if an artifact contains Rust code without enabled mitigations.adr
  • Mitigation enforcement will be disableable by the end-user via a compiler flag.adr
  • The compiler CLI flags will determine whether a mitigation allows or denies partial mitigations using `-C allow-partial-mitigations` and `-C deny-partial-mitigations`.adr
  • Enabling a mitigation (e.g., `-C stack-protector=strong`) will default to turning on enforcement for that mitigation.adr
  • Each crate will include a metadata field detailing its enabled mitigations.adr
  • If a mitigation has multiple levels, a stricter level in a dependency is compatible with a looser level in the current crate, but not vice-versa.adr

What was ruled out· 2

  • Target modifiers are rejected as the primary mechanism for mitigation enforcement due to unsuitable naming and behavior for this use case.adr
  • An external tool for checking mitigations is insufficient because it cannot reliably determine mitigations for all components within a binary.adr

Constraints

  • Complex compilation processes make it easy to accidentally omit mitigation flags, which is a problem mitigation enforcement aims to solve.adr
  • Teams with high security needs require a mechanism to ensure all produced programs have desired mitigations enabled.adr

The recorded why

§ Summary

Introduce the concept of "mitigation enforcement", so that when compiling a crate with mitigations enabled (for example, -C stack-protector), a compilation error will happen if the produced artifact would contain Rust code without the same mitigations enabled.

This in many cases would require use of -Z build-std, since the standard library only comes with a single set of enabled mitigations per target.

Mitigation enforcement should be disableable by the end-user via a compiler flag.

§ Motivation

Memory unsafety mitigations are important for reducing the chance that a vulnerability ends up being exploitable.

While in Rust, memory unsafety is less of a concern than in C, mitigations are still important for several reasons:

  1. Some mitigations (for example, straight line speculation mitigation, -Z harden-sls) mitigate the impact of Spectre-style speculative execution vulnerabilities, that exist in Rust just as well as C.
  2. Many Rust programs also contain large C/C++ components, that can have memory vulnerabilities. The exploitation of these vulnerabilities is made easier by the presence of mitigation-less Rust code within the same address-space.
  3. Many Rust programs use unsafe, that can introduce memory unsafety and vulnerabilities.

Mitigations are generally enabled by passing a flag to the compiler (for example, -Z harden-sls or -Z stack-protector). If the compilation process of a program is complex, it is very easy to end up accidentally not passing the flag to one of the constituent object files.

This can have one of several consequences:

  1. In some cases (for example -Z fixed-x18 -Z sanitizer=shadow-call-stack), the mitigation changes the ABI, and linking together code with different mitigation settings leads to undefined behavior such as crashes even in the absence of an attack. In these cases, the sanitizer should be a target modifier rather than using this RFC.
  2. For "Spectre-type" mitigations (e.g. harden-sls), if there is some reachable code in your address space without a retpoline, attackers can execute a Spectre attack, even if there is 0 UB in your code.
  3. For "CFI-type" mitigations (e.g. kcfi), if there is reachable code in your address space that does not have that sanitizer enabled, attackers can use it to leverage an already-existing memory vulnerability into ROP execution, even if the memory vulnerability is in a completely different part of the code than the part that has the mitigation disabled
  4. For "local" mitigations (e.g. stack protector, or C's -fwrapv - which I don't think Rust has), the mitigation protects the code when it is in the right place relative to the bug - a stack protector helps basically when it protects the buffer that overflows, and it does not matter which other functions have a stack protector.

To avoid these consequences, teams that write software with high security needs - for example, browsers and the Linux kernel - need to have a way to make sure that the programs they produce have the mitigations they want enabled.

On the other hand, for teams that write software in a more uncoordinated environment, it can be hard to chase down all dependencies, and especially for "local" mitigations, being able to enable them on an object-by-object basis is the only thing that allows for the mitigations to actually be deployed. Especially important is progressive deployment - it's much easier to introduce mitigations 1 crate at a time than to introduce mitigations a whole program at a time, even if the end goal is to introduce the mitigations to the entire program.

Supported mitigations

The following mitigations could find this feature interesting

Already Stable

  1. -C control-flow-guard This is a "CFI-type" mitigation on Windows, and therefore having it enabled only partially makes it far less protective.

    However, it is already stable, and we would need a -C deny-partial-mitigations=control-flow-guard or -C control-flow-guard-enforce (or bikeshed) to make it enforcing.

    We could also make it enforcing over an edition boundary.

  2. -C relocation-model If position-dependent code is compiled into a binary, then ASLR will not be able to be enabled.

    This is less of a problem in Rust than in C, since position-independent code is a rarely-changed default in Rust, and it can easily be checked via [hardening-check(1)] or similar tools since it's easily visible in the ELF header.

    However, we might still want to introduce a -C deny-partial-mitigations=position-independent or -C enforce-position-independent.

    As far as I can tell, there is no way to disable relro via stable Rust compilation flags.

  3. -C overflow-checks This can be thought of as a mitigation in some sense. It might make sense to add a way to make it enforcing, but I don't think it makes sense to make it enforcing by default since that is contrary to the normal use of turning overflow checks only for the crate under development.

    However, we might still want to introduce a -C deny-partial-mitigations=overflow-checks or -C enforce-overflow-checks. It probably does not make sense to make it enforcing over an edition boundary, since the desired default there is not to enforce. This probably merits a separate RFC/FCP.

Currently Unstable (as of rustc 1.89)

This RFC is not the place to make a decision of exactly which unstable mitigations should have enforcement enabled - that should take place as a part of their stabilization.

However, it would be good to see that enforcement fits well with sanitizers.

  1. -Z branch-protection/-Z cf-protection - control flow protection in ARM or Intel, respectively. Would probably want this. It uses .note.gnu.property which makes it active only if every object in the address space uses it, which makes it easy to detect via a [hardening-check(1)]-style tool, but since it is not the default, this would make it easier to make sure it is enabled.
  2. -Z ehcont-guard - Similar for -Z branch-protection/-Z cf-protection, but for Windows exception handlers - if every object in the address space uses it, NtContinue is only allowed to jump to valid exception handlers. This means it is easy to detect via a [hardening-check(1)]-style tool, but it probably makes sense to include it.
  3. -Z indirect-branch-cs-prefix - a part of retpoline (Spectre) mitigation on x86.
  4. -Z no-jump-tables - CFI-type mitigation? soon to be stabilized as -C jump-tables. Do we want to hold that stabilization as well?
  5. -Z retpoline and -Z retpoline-external-thunk - Spectre-type mitigation
  6. -Z sanitizer-based mitigations. A fairly large use case. As far as I can tell, enforcement makes sense for -Zsanitizer=kcfi, -Zsanitizer=memtag, -Zsanitizer=shadow-call-stack and does not make sense for -Zsanitizer=address, -Zsanitizer=dataflow, -Zsanitizer=hwaddress, -Zsanitizer=leak, -Zsanitizer=memory, -Zsanitizer=thread (-Z sanitizer=address should probably be a target modifier)
  7. -Z stack-protector - stack smashing protection, local-type mitigation
  8. -Z ub-checks: as far as I can tell, it is not intended as a mitigation, but since it prevents some UB, it might be thought of as one

§ Guide-level explanation

When you use a mitigation, such as -C stack-protector=strong, if one of your dependencies does not have that mitigation enabled, compilation will fail.

Error: your program uses the crate foo, that is not protected by -C stack-protector=strong.

Recompile that crate with the mitigation enabled, or use -C allow-partial-mitigations=stack-protector to allow creating an artifact that has the mitigation only partially enabled.

It is possible to disable -C allow-partial-mitigations=stack-protector via -C deny-partial-mitigations=stack-protector.

Other flags that can be mitigations, for example -C overflow-checks=on, permit partial mitigations by default, but it is possible to make sure your dependencies have the same mitigation setting as you by passing -C deny-partial-mitigations=overflow-checks. That flag can be overridden by -C allow-partial-mitigations=overflow-checks.

§ Reference-level explanation

For every mitigation-like option, the compiler CLI flags determines whether that mitigation allows or denies partial mitigations. This can be turned on via -C allow-partial-mitigations=<mitigation> and turned off by -C deny-partial-mitigations=<mitigation> (for example, -C allow-partial-mitigations=stack-protector and -C deny-partial-mitigations=stack-protector).

These flags act like every other compiler flag, with the last flag winning if there are multiple values for the same mitigation.

When a mitigation option appears (for example, -C stack-protector=strong), the mitigation is denied for partial use. This is needed to allow for distributors and the like to set a default that allows partial mitigations without weakening the security of users.

For example,

-Callow-partial-mitigations=stack-protector -Callow-partial-mitigations=overflow-checks
-Callow-partial-mitigations=kcfi -Cdeny-partial-mitigations=overflow-checks -Csanitizer=kcfi

Will allow partial mitigations for stack-protector (since there's an allow) but deny partial mitigations for overflow-checks (since the later deny overrides the allow) and kcfi (since -Csanitizer=kcfi resets enforcement for CFI).

It is possible to bikeshed the exact naming scheme.

Every new mitigation would need to decide whether it adopts this scheme, but mitigations are expected to adopt it.

Every crate gets a metadata field that contains the set of mitigations it has enabled.

When compiling a crate, if the current crate has a mitigation with enforcement turned on, and one of the dependencies does not have that mitigation turned on (whether enforcing or not), a compilation error results.

If a mitigation has multiple "levels", a stricter level at a dependency is compatible with a looser level at the current (dependent) crate, but not vice-versa - for example, if the standard library crates were compiled with -C stack-protector=all (not discussing whether that is a wise idea), they would be compatible with every configuration of user crates.

The error happens independent of the target crate type (you get an error if you are building an rlib, not just the final executable).

For example, with -C stack-protector, the compatibility table will be as follows:

Dependency\Currentnonenone+allowstrongstrong + allow partialallstrong + allow partial
noneOKOKerrorOK - current allows partialerrorOK - current allows partial
none + allow partialOKOKerrorOK - current allows partialerrorOK - current allows partial
strongOKOKOKOKerrorOK - current allows partial
strong + allow partialOKOKOKOKerrorOK - current allows partial
allOKOKOKOKOKOK
all + allow partialOKOKOKOKOKOK

§ Drawbacks

§ Rationale and alternatives

Syntax alternatives

-C my-mitigation-noenforce

Instead of -C allow-partial-mitigations, it is possible to split every flag value that enables a mitigation for which enforcement is desired is split into 2 separate values, "enforcing" and "non-enforcing" mode. The enforcing mode is the default, non-enforcing mode is constructed by adding -noenforce to the name of the value, for example -C stack-protector=strong-noenforce or -C sanitizer=shadow-call-stack-noenforce.

If a program has multiple flags of the same kind, the last flag wins, so e.g. -C stack-protector=strong-noenforce -C stack-protector=strong is the same as -C stack-protector=strong.

-C stack-protector=none-noenforce

The option -C stack-protector=none-noenforce is the same as -C stack-protector=none. I am not sure whether we should have both, but it feels that orthogonality is in favor of having both.

Limiting the set of crates that are allowed to bypass enforcement

You could have a syntax like -C allow-partial-mitigations=stack-protector=@stdlib+foo, -C stack-protector=strong-noenforce=@stdlib+foo, or some other syntax (using + since , should have a different level of precedence), which would only allow the mitigation to be partial on a specified set of crate names.

@stdlib is used here to stand for all the sysroot crates, since the user does not want to specify them all (should we bikeshed the syntax?).

This is different from -C pretend-mitigation-enabled (which would be a mitigation equivalent of -C unsafe-allow-abi-mismatch), since it reflects a decision made by the application writer (dependent crate) rather than the library writer.

This can be done in a later stabilization that the core of the feature.

Impacts to syntax choices

The -C stack-protector=strong-noenforce=std+alloc+core syntax feels ugly.

If we decide that the noenforce syntax is important to allow for flag concatenation, we can certainly have both the noenforce syntax and the allow-partial-mitigations syntax, with noenforce disabling enforcement for all crates while allow-partial-mitigations disables it only for specific crates.

Interaction with -C unsafe-allow-abi-mismatch

The proposed rules do not interact with -C unsafe-allow-abi-mismatch at all, so if you have a "sanitizer runtime" crate that is compiled with the following options:

-C no-fixed-x18 -C sanitizer=shadow-call-stack=off -C unsafe-allow-abi-mismatch=fixed-x18 -C unsafe-allow-abi-mismatch=shadow-call-stack

Then dependencies will need to use it with -C allow-partial-mitigations=shadow-call-stack rather than -C sanitizer=shadow-call-stack, otherwise they will get an error.

As far as I can see, there is no current demand for that sort of sanitizer runtime, but if that is desired, it might be a good idea to add a -C pretend-mitigation-enabled=shadow-call-stack (which would act like -C allow-unsafe-api-mismatch and mark a crate as a "wildcard"), and possibly to make -C unsafe-allow-abi-mismatch act like -C pretend-mitigation-enabled as well for mitigations that are also target modifiers.

Defaults

We want that the most obvious way to enable mitigations (e.g. -C stack-protector=strong or -C sanitizer=shadow-call-stack) to turn on enforcement, since that will set people for a safer default where mitigations are enabled throughout.

However, we do want an easy way for distribution owners (for example, Ubuntu) to turn on mitigations in a non-enforcing way, as is done today e.g. by Ubuntu with -fstack-protector-strong. Distributions can't easily add a new mitigation in an enforcing way, as that will cause widespread breakage, but they can fairly easily turn a mitigation on in a non-enforcing way.

We do want the combination of defaults to combine in a nice way - if the distribution sets -C stack-protector=strong -C allow-partial-mitigations=stack-protector, and the user adds -C stack-protector=strong, we want the result to be stack-protector set to strong and enforcing. This is done by the "resetting" behavior of the CLI - setting -C stack-protector=strong -C allow-partial-mitigations=stack-protector -C stack-protector=strong acts as if the -C allow-partial-mitigations=stack-protector was not passed.

Distributors and packagers can set defaults for mitigations by setting some sort of RUSTFLAGS argument, or by patching the compiler to act as if there was an "implicit" prepended RUSTFLAGS argument.

The standard library

One big place where it's very easy to end up with mixed mitigations is the standard library. The standard library comes compiled with just a single set of mitigations enabled (as of Rust 1.88: PIC, NX, -z relro -z now), and without -Z build-std, it is only possible to use the mitigation settings in the shipped standard library.

If we find out that some mitigations have a positive cost-benefit ratio for the standard library (probably at least -Z stack-protector), we probably want to ship a standard library supporting them by default, but in a way that still allows people to compile code without mitigations (for example, ship a libstd with -C stack-protector=strong, but allow users to compile their own code with -C stack-protector=none using that libstd) if that fulfills their security/performance tradeoff better.

Why not target modifiers?

The target modifier feature provides a similar goal of preventing mismatches in compiler settings.

There are several issues with using target modifiers for mitigations:

The name unsafe-allow-abi-mismatch

The name of the flag that allows mixing target modifiers, -C unsafe-allow-abi-mismatch, does not make sense for cases that are not "unsafe ABI mismatches". It also uses the word "unsafe", which we prefer not to use except in cases that can result in actual unsoundness.

The behavior of unsafe-allow-abi-mismatch

The behavior of -C unsafe-allow-abi-mismatch is also not ideal for mitigations.

The flag marks a crate as basically having a "wildcard target modifier", which allows it to compile with crates with any value of the target modifier.

This is quite good for the original use case - it's an "I know what I am doing" flag that allows "runtime" crates to be mixed in even if they subtly play

Excerpt — the full document is at the cited source: text/3855-mitigation-enforcement.md