recorded decision · public · no signup
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
Constraints
The recorded why
- Feature Name:
mitigation_enforcement - Start Date: 2025-09-13
- RFC PR: rust-lang/rfcs#3855
- Tracking Issue: rust-lang/rust#154613
§ 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:
- 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. - 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.
- 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:
- 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. - 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. - 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
- 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
-
-C control-flow-guardThis 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-guardor-C control-flow-guard-enforce(or bikeshed) to make it enforcing.We could also make it enforcing over an edition boundary.
-
-C relocation-modelIf 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-independentor-C enforce-position-independent.As far as I can tell, there is no way to disable
relrovia stable Rust compilation flags. -
-C overflow-checksThis 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-checksor-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.
-Z branch-protection/-Z cf-protection- control flow protection in ARM or Intel, respectively. Would probably want this. It uses.note.gnu.propertywhich 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.-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,NtContinueis 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.-Z indirect-branch-cs-prefix- a part of retpoline (Spectre) mitigation on x86.-Z no-jump-tables- CFI-type mitigation? soon to be stabilized as-C jump-tables. Do we want to hold that stabilization as well?-Z retpolineand-Z retpoline-external-thunk- Spectre-type mitigation-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-stackand does not make sense for-Zsanitizer=address, -Zsanitizer=dataflow, -Zsanitizer=hwaddress, -Zsanitizer=leak, -Zsanitizer=memory, -Zsanitizer=thread(-Z sanitizer=addressshould probably be a target modifier)-Z stack-protector- stack smashing protection, local-type mitigation-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-protectorto allow creating an artifact that has the mitigation only partially enabled.It is possible to disable
-C allow-partial-mitigations=stack-protectorvia-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\Current | none | none+allow | strong | strong + allow partial | all | strong + allow partial |
|---|---|---|---|---|---|---|
| none | OK | OK | error | OK - current allows partial | error | OK - current allows partial |
| none + allow partial | OK | OK | error | OK - current allows partial | error | OK - current allows partial |
| strong | OK | OK | OK | OK | error | OK - current allows partial |
| strong + allow partial | OK | OK | OK | OK | error | OK - current allows partial |
| all | OK | OK | OK | OK | OK | OK |
| all + allow partial | OK | OK | OK | OK | OK | OK |
§ 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 ↗