- 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 ahardening-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 ahardening-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 with the ABI rules - for example, in a kernel, where floating point execution is mostly forbidden, there are a few compilation units using real floats. To call into them, first you call some special functions that make the floating point registers usable, and then you call into the CU safely by not having any floats in the signature of the function on the boundary (example by Alice Ryhl).
However, for mitigations, the expected case for disabling mitigations is less people knowing what they are doing, and more people that don’t agree with the performance/security tradeoff they bring. In that case, we should allow the executable-writer to be aware of the tradeoff being made, rather than letting libraries in the middle decide it for them.
Target modifier, enforced mitigation, neither, or both?
For every single mitigation-like flag:
- If mixing values of the flag can cause unsound behavior, sanitizer false positives, or crashes, it should be a target modifier.
- If mixing values of the flag can hurt a production program’s security posture, it should be an enforced mitigation.
- If the flag is a sanitizer intended for use only in fuzzing and testing, and mixing values of it does not lead to unsound behavior, it should be neither.
- If mixing values of the flag hurt a production program’s security posture in some cases, and lead to unsound behavior in other cases, it should be both. I am not aware of a current flag that fits this pattern.
-emit=component-info
One possible “big” alternative would be emitting a component info file, via
-emit=component-info. For example:
{
"components": [
{
"type": "crate",
"name": "foo",
"mitigations": ["a"]
}
]
}
We could then have Cargo run over that file and look for missing mitigations.
Without support in Cargo, this would be not good enough since it would take an annoying enough amount of effort for people to scan the generated json files.
It is important to avoid this sort of JSON being bikeshed-land.
An advantage is that people could write their own custom policies, which would take some pressure off bikesheds for things like overflow checks.
In some sense, this is moving complexity from rustc to Cargo, which I’m not sure reduces it overall.
A disadvantage is that it would be possible to enable a mitigation in rustc but
not in Cargo (e.g. by setting RUSTFLAGS), which would make it non-enforced.
A big disadvantage of this is that it adds much complexity and deeper-than-we-want integration to projects that are not using Cargo, since they have to implement the scanning integration themselves.
Even if this alternative is not selected, some sort of -emit=component-info does
feel like a good feature, though one that deserves a separate RFC. There does not
seem to be a conflict between -emit=component-info and
-C allow-partial-mitigations.
Why not an external tool?
This is somewhat hard to do with an external tool, since there is no way of looking at a binary and telling what mitigations its components have.
There are however some external tools that do check for mitigations, but they have limitations:
hardening-check(1)exists, but its check for stack smashing protection only checks that at least 1 function has stack cookies, rather than checking that every interesting function has it enabled.- The Linux kernel has
objtool, which checks for some other mitigations (for example, retpolines). It however needs to access the.oobject files rather than the final linked executable or shared library - which requires its user to control the linking process - and also has hardcoded limitations that make it only suitable for the Linux kernel, rather than being useful as a general-purpose tool. - Fedora has a tool called
annobin, which is able to parse the.gnu.build.attributessection, which compilers can use to indicate mitigation support. That does fit our needs, but is specific to the GNU/Linux world - it is desirable to have a solution that works on all platforms. - There is a similar tool called
checksec, with similar limitations.
Mitigations that are manifestly visible from the program header
For some mitigations, the mitigation is enabled for an entire program executable, by setting a flag in the program header.
Many of these mitigations normally require all the code within the program to be compatible with them, and therefore they normally work in a way where every object file indicates whether it is compatible with them, and with the mitigation being enabled for the ELF if all of the comprising object files are compatible with it.
Mitigations that act like that:
- Position-independent code (
ET_DYN) - Non-executable stack (
PT_GNU_STACK) .note.gnu.propertymitigations
Other mitigations are also manifest from the program header, but are selected at link-time, without requiring any changes to the compilation of the comprising object files:
- relro (
PT_GNU_RELRO)
In these cases, programs such as hardening-check(1) can often be used
to check the flags in the program header and see whether the mitigation
is enabled.
.note.gnu.property
The .note.gnu.property field contains a number of properties
(for example, GNU_PROPERTY_AARCH64_FEATURE_1_BTI) that are used to indicate
that the compiled code contains certain mitigations, for example BTI
(-Zbranch-protection=bti).
When linking multiple objects, the linker sets the resulting property to be the logical AND of the properties of the constituent objects.
For protections such as BTI, the mitigation can only be turned on if all code within the compiled binary supports it - if one of the object files doesn’t, the loader has to leave the mitigation turned off entirely. The ELF loader uses the value of the property within the loaded executable to decide whether to turn on the mitigation.
If it could be arranged, using .note.gnu.property could allow mitigation tracking
to propagate across languages - with the final compilation step intentionally erroring
out if the property is not enabled. However, this is also a disadvantage - adding a
new property to .note.gnu.property requires cooperation from the target owners.
Therefore, it might be useful as a future step with cooperation from the target owners, but is not good if we want to be able to add new enforced mitigations without requiring cooperation from all platforms.
Passing enforced mitigations to the linker
Currently, the mitigation enforcement RFC does not do anything for native dependencies, which can be compiled without mitigations within a program that supports mitigations.
As far as I can tell, the rustc compiler currently does not read the object files
of native dependencies, but instead passes to the linker arguments that allow the
linker to find and link in the object files.
If a linker accepts flags that enforce certain mitigations - currently, these are mostly mitigations manifest from the program header - the compiler should pass these flags to the relevant linker.
Since that is a breaking change as it breaks compilation with existing C code, it might need to be done over an edition or linker-change boundary.
Use of the lint mechanism
In theory, mitigation enforcement could be a collection of lints, accessible via our standard lint infrastructure, and as such e.g. exposed in Cargo via existing lint configuation.
The problem with this is that lints are designed to be capped - normally, lints are intended
for cases where a probable bug exists, but generally, for code that is not currently under active
development, users prefer to use the code with a probable, pre-existing bug as opposed to
changing it, and cap the lints (using e.g. --cap-lints=warn) while sending the warnings to
a place that might be handled eventually.
Lints have a force-warn option that always warns, as well as a forbid option that overrides
allow. We would need a force-error-but-allow-allow option that overrides capping lints
but does not override allow, which would make the lint ordering a lattice as opposed
to a total ordering, and be undesirable.
For mitigations, this is an undesirable state, as you generally do not want to deploy code with pre-existing missing mitigations.
Also, lints are generally controlled by developers, while mitigations are normally controlled by DevSecOps engineers, and it is reasonable to keep the separation functional.
.gnu.build.attributes
This is a Fedora feature. It actually behaves pretty similarly to how we expect
mitigation enforcement to work - a compiler plugin written by Fedora makes
C/C++ compilers output a .gnu.build.attributes section indicating which
mitigations are present or absent. The linker aggregates all of these sections,
and annocheck can be used to check whether an object file declares an
interesting mitigation as missing.
This is currently a Fedora-ism as far as I can tell. It can be used with non-Fedora linkers (of course, in that case, C code will probably not indicate which mitigations it is missing).
It might be a good idea to coordinate with Fedora and have rustc emit this metadata, but since it only works on Linux platforms, probably better to not solely rely on it.
Prior art
The panic strategy
The Rust compiler already has infrastructure to detect flag mismatches: the
flags -Cpanic and -Zpanic-in-drop. The prebuilt stdlib comes with different
pieces depending on which strategy is used, although panic landing flags are
not entirely removed when using -Cpanic=abort, as only part of the prebuilt
stdlib is switched out.
Target modifiers
.note.gnu.property
The .note.gnu.property section discussed previously is an example of C code
detecting mismatches of a flag at link time.
Unresolved questions
Future possibilities
A possible future extension could be to provide a mechanism to enforce mitigations across C code and Rust code. This would be an interesting extension, but it would require cross-language effort that will take a long period of time to finish. Similarly, another possible future extension could be to catch mitigation mismatches when using dynamic linking.