Skip to content
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
347 changes: 347 additions & 0 deletions text/3855-mitigation-enforcement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,347 @@
- Feature Name: `mitigation_enforcement`
- Start Date: 2025-09-13
- RFC PR: [rust-lang/rfcs#3855](https://github.com/rust-lang/rfcs/pull/3855)
- Rust Issue: None

# Summary
[summary]: #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.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's be cool if this was somehow per dependency, like maybe you'd add some disable-mitigation-enforcement = {std} into the binary's Cargo.toml.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's be cool if this was somehow per dependency, like maybe you'd add some disable-mitigation-enforcement = {std} into the binary's Cargo.toml.

You probably want something in the style of -C allow-partial-mitigations=stack-protector=std+alloc+core, so you know which mitigations you are allowing.

(Of course, with also a syntax in Cargo, which should come with a separate RFC I think).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added that to alternatives

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specifying crate names from the sysroot is very problematic since std has about 10 different dependencies that would need to be specified and that are not stable.

What would we desired for this use case of allowing the sysroot and nothing else would be special syntax to allow it only in the sysroot. A list of crate names would not help.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would we desired for this use case of allowing the sysroot and nothing else would be special syntax to allow it only in the sysroot. A list of crate names would not help.

Do you think that special casing core (or @core or something) to apply to the entire sysroot would work?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's be cool if this was somehow per dependency, like maybe you'd add some disable-mitigation-enforcement = {std} into the binary's Cargo.toml.

You probably want something in the style of -C allow-partial-mitigations=stack-protector=std+alloc+core, so you know which mitigations you are allowing.

This would still be useful for crates outside std, for example, if a project experiences a mitigation "failure" in a non-security critical crate or feature branch.


# Motivation
[motivation]: #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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps worth noting here that especially in cases of mixed C(++) and Rust programs, some mitigations depend on the entire binary being instrumented or protected to be effective or work at all. Thus it can be very important for Rust code to have mitigations applied so that the C(++) parts of the program can continue to use their existing mitigations.

memory vulnerabilities.
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You always talk about stack-protector here, since that's your primary motivation of course, but I think it would be good to more explicitly list out all kinds of mitigations that Rust has it that people would like Rust to have in the future, to ensure that this makes sense for all of them (for example, the ones from https://doc.rust-lang.org/nightly/rustc/exploit-mitigations.html#exploit-mitigations-1).

I would especially be interested in whether there are existing stable mitigations that would like to make use of this, especially if it has a flag to toggle it.

Copy link
Member

@rcvalle rcvalle Sep 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some exploit mitigations that would benefit from it (e.g., CFI, and including most in the https://doc.rust-lang.org/nightly/rustc/exploit-mitigations.html#exploit-mitigations-1) precedes the Target Modifiers feature (which was intended to also solve this), but I don't think there are any stable exploit mitigations except maybe -C control-flow-guard.

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The equivalent of -fwrapv in Rust is -C overflow-checks=off. And if you're depending on it rather than one of the other alternatives, you probably want to make sure it's enabled everywhere that isn't using one of those alternatives.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not possible to have the Rust equivalent of -fno-wrapv (undefined behavior on signed integer wrapping).

-C overflow-checks=on is more equivalent to -ftrapv. Do people feel that there is a need to set it to enforcing?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand it doesn't quite fit the model here, but I've written code where we wanted to panic rather than wrapping, because wrapping meant we'd generated an incorrect value.

I've also written code where we relied on the guaranteed wrapping behaviour.

Copy link
Contributor Author

@arielb1 arielb1 Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll say this (enforcing for overflow checks) is off scope for the current RFC. There is no reason to do it in the "main" enforcing pulse. Feel free to open another RFC/FCP/whatever.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that it's not really a security mitigation in the classic sense and shouldn't be enforced.

FWIW, relying on wrapping behavior is incorrect, as Rust always considers overflow to be a bug, it just doesn't always check it. Use wrapping if you need wrapping.

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 messy 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.

[target modifier]: https://github.com/rust-lang/rfcs/pull/3716
[`-Z harden-sls`]: https://github.com/rust-lang/compiler-team/issues/869
[`-Z stack-protector`]: https://github.com/rust-lang/rust/issues/114903
[example by Alice Ryhl]: https://rust-lang.zulipchat.com/#narrow/channel/131828-t-compiler/topic/Target.20modifiers.20and.20-Cunsafe-allow-abi-mismatch/near/483871803

# Guide-level explanation
[guide-level-explanation]: #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 stack-protector=strong-noenforce` to allow creating an artifact
> that has the mitigation only partially enabled.

# Reference-level explanation
[reference-level-explanation]: #reference-level-explanation

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`.

> 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 lower level at a child crate is compatible
with a higher level at a base crate.

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:

| Base\Child | none | none-noenforce | strong | strong-noenforce | all | all-noenforce |
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use the words "dependency" instead of "child" here and above? Child is not a word we generally use for crate relationships which makes it a bit confusing

| ---------------- | ---- | -------------- | ------ | -------------------- | ----- | -------------------- |
| none | OK | OK | error | OK - child noenforce | error | OK - child noenforce |
| none-noenforce | OK | OK | error | OK - child noenforce | error | OK - child noenforce |
| strong | OK | OK | OK | OK | error | OK - child noenforce |
| strong-noenforce | OK | OK | OK | OK | error | OK - child noenforce |
| all | OK | OK | OK | OK | OK | OK |
| all-noenforce | OK | OK | OK | OK | OK | OK |

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`.

# Drawbacks
[drawbacks]: #drawbacks

The `-noenforce` syntax is ugly, and the
`-C allow-partial-mitigations=stack-protector` syntax is either order-dependent
or does not allow for easy appending.

# Rationale and alternatives
[rationale-and-alternatives]: #rationale-and-alternatives

## Syntax alternatives

### -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.

### -C allow-partial-mitigations

Instead of having `-C stack-protector=strong-noenforce`, we could have the
syntax be `-C stack-protector=strong -C allow-partial-mitigations=stack-protector`.

Some people feel that syntax is prettier. In that case, we have 2 options:

#### Without order dependency

This is the simplest to implement. With that,
`-C stack-protector=strong -C allow-partial-mitigations=stack-protector -C stack-protector=strong`
is the same as `-C stack-protector=strong -C allow-partial-mitigations=stack-protector`.

This is unfortunate, because `-C stack-protector=strong -C allow-partial-mitigations=stack-protector` is
a pretty good default for distributions to set. If a distribution sets that, and an application
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's some earlier discussion of "distributors setting flags by default". To my knowledge, there is no out of the box mechanism that supports this. It would be helpful to explain what you mean by this. Are distributors setting RUSTFLAGS in .bashrc or something? Is this something distros are actually doing or just a theoretical possibility?

Copy link
Contributor Author

@arielb1 arielb1 Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is commonly done in CFLAGS. I thought it was also done for RUSTFLAGS, but apparently not.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arch ships RUSTFLAGS overrides in default configuration for makepkg.

believes they are turning on enforcing stack protection by using `-C stack-protector=strong`,
the application will not be getting enforcement due to the distribution setting
`-C allow-partial-mitigations=stack-protector`.

On the other hand, maybe there is not actually desire to add
`-C stack-protector=strong -C allow-partial-mitigations=stack-protector` as a default?

Maybe it is actually possible to ship a `-C stack-protector=strong` standard library and
add a `-C stack-protector=strong` default, since the enforcement check only works
"towards roots"?

#### With order dependency

With a small amount of implementation effort, we could have `-C stack-protector=strong` reset the
`-C allow-partial-mitigations=stack-protector` state, so that
`-C stack-protector=strong -C allow-partial-mitigations=stack-protector -C stack-protector=strong`
is equivalent to `-C stack-protector=strong`.

This would work quite well, but I am not sure that rustc wants to have order between different
kinds of CLI arguments.

## Interaction with `-C unsafe-allow-abi-mismatch` / `-C pretend-mitigation-enabled`

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 via `-C sanitizer=shadow-call-stack-noenforce`
rather than `-C sanitizer=shadow-call-stack`, otherwise they will get an error.

As far as I can need, 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`, and possibly to make
`-C unsafe-allow-abi-mismatch` do that for crates that are 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 up to a pit of success 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
distributioon sets `-C stack-protector=strong-noenforce`, and the user adds
`-C stack-protector=strong`, we want the result to be stack-protector set
to strong and enforcing.

On the other hand, maybe there is not actually desire to add
`-C stack-protector=strong -C allow-partial-mitigations=stack-protector` as a default,
which would make this less interesting?

Maybe it is actually possible to ship a `-C stack-protector=strong` standard library and
add a `-C stack-protector=strong` default, since the enforcement check only works
"towards roots"?
Comment on lines +354 to +360
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the repetition of this content from the "With order dependency" section intended?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea, fixed


[by Ubuntu with `-fstack-protector-strong`]: https://wiki.ubuntu.com/ToolChain/CompilerFlags

## 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: none), 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,
if that fulfills their cost/benefit ratios 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.

## Why not an external tool?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a completely different alternative, it seems like we could extend rustc's CLI to support extracting information about the crate graph loaded during the compilation session into a specific format that can be read by other tools. For example, a JSON file which notes the name, location and compilation flags used to compile every crate loaded by the resolver during compilation.

The mitigation enforcement mechanism could then be built on top of that data in addition to other use cases the RFC mentions (but does not satisfy) such as such as confirming all crates share the same -C overflow-checks value.

Since we've already implemented one set of flags/policy to ensure crates agree on certain compiler flags (target modifiers) and now we're potentially implementing a second set with a slightly different policy, I think it would be helpful to explain why this approach should be preferred over a more general mechanism.

Copy link
Contributor Author

@arielb1 arielb1 Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we've already implemented one set of flags/policy to ensure crates agree on certain compiler flags (target modifiers) and now we're potentially implementing a second set with a slightly different policy, I think it would be helpful to explain why this approach should be preferred over a more general mechanism.

I don't think that target-modifier makes sense as a "hardening check", since it is required for soundness.

For an ELF property: I don't think it makes sense for Rust to invent one.

However: Fedora did invent a .gnu.build.attribute feature and got it used in some places (their gcc does not enable it by default, but they enable it if you pass -fplugin=annobin, which they pass to RPMs. We could try to ride on that, but it's a bit of a de-facto standard that might annoy the Fedora people.


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 (for example [`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).

[`hardening-check(1)`]: https://manpages.debian.org/testing/devscripts/hardening-check.1.en.html

## .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.

[`GNU_PROPERTY_AARCH64_FEATURE_1_BTI`]: https://docs.rs/object/0.37/object/elf/constant.GNU_PROPERTY_AARCH64_FEATURE_1_BTI.html

# Prior art
[prior-art]: #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
[unresolved-questions]: #unresolved-questions

# Future possibilities
[future-possibilities]: #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.