Skip to content

Commit 52f7403

Browse files
author
Ariel Ben-Yehuda
committed
mitigation enforcement
1 parent bd62885 commit 52f7403

File tree

1 file changed

+347
-0
lines changed

1 file changed

+347
-0
lines changed
Lines changed: 347 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,347 @@
1+
- Feature Name: `mitigation_enforcement`
2+
- Start Date: 2025-09-13
3+
- RFC PR: [rust-lang/rfcs#3855](https://github.com/rust-lang/rfcs/pull/3855)
4+
- Rust Issue: None
5+
6+
# Summary
7+
[summary]: #summary
8+
9+
Introduce the concept of "mitigation enforcement", so that when compiling
10+
a crate with mitigations enabled (for example, `-C stack-protector`),
11+
a compilation error will happen if the produced artifact would contain Rust
12+
code without the same mitigations enabled.
13+
14+
This in many cases would require use of `-Z build-std`, since the standard
15+
library only comes with a single set of enabled mitigations per target.
16+
17+
Mitigation enforcement should be disableable by the end-user via a compiler
18+
flag.
19+
20+
# Motivation
21+
[motivation]: #motivation
22+
23+
Memory unsafety mitigations are important for reducing the chance that a vulnerability
24+
ends up being exploitable.
25+
26+
While in Rust, memory unsafety is less of a concern than in C, mitigations are
27+
still important for several reasons:
28+
29+
1. Some mitigations (for example, straight line speculation mitigation,
30+
[`-Z harden-sls`]) mitigate the impact of Spectre-style speculative
31+
execution vulnerabilities, that exist in Rust just as well as C.
32+
2. Many Rust programs also contain large C/C++ components, that can have
33+
memory vulnerabilities.
34+
3. Many Rust programs use `unsafe`, that can introduce memory unsafety
35+
and vulnerabilities.
36+
37+
Mitigations are generally enabled by passing a flag to the compiler (for
38+
example, [`-Z harden-sls`] or [`-Z stack-protector`]). If the compilation
39+
process of a program is complex, it is very easy to end up accidentally
40+
not passing the flag to one of the constituent object files.
41+
42+
This can have one of several consequences:
43+
44+
1. In some cases (for example `-Z fixed-x18 -Z sanitizer=shadow-call-stack`),
45+
the mitigation changes the ABI, and linking together code with different
46+
mitigation settings leads to undefined behavior such as crashes even
47+
in the absence of an attack. In these cases, the sanitizer should be a
48+
[target modifier] rather than using this RFC.
49+
2. For "Spectre-type" mitigations (e.g. `harden-sls`), if there is some reachable
50+
code in your address space without a retpoline, attackers can execute a
51+
Spectre attack, even if there is 0 UB in your code.
52+
3. For "CFI-type" mitigations (e.g. kcfi), if there is reachable code in your
53+
address space that does not have that sanitizer enabled, attackers can use it to
54+
leverage an already-existing memory vulnerability into ROP execution, even
55+
if the memory vulnerability is in a completely different part of the code than
56+
the part that has the mitigation disabled
57+
4. For "local" mitigations (e.g. stack protector, or C's `-fwrapv` - which I don't think
58+
Rust has), the mitigation protects the code when it is in the right place
59+
relative to the bug - a stack protector helps basically when it protects the buffer
60+
that overflows, and it does not matter which other functions have a stack protector.
61+
62+
To avoid these consequences, teams that write software with high security needs - for
63+
example, browsers and the Linux kernel - need to have a way to make sure that the
64+
programs they produce have the mitigations they want enabled.
65+
66+
On the other hand, for teams that write software in a more messy environment, it
67+
can be hard to chase down all dependencies, and especially for "local" mitigations,
68+
being able to enable them on an object-by-object basis is the only thing that allows
69+
for the mitigations to actually be deployed. Especially important is progressive
70+
deployment - it's much easier to introduce mitigations 1 crate at a time than
71+
to introduce mitigations a whole program at a time, even if the end goal is
72+
to introduce the mitigations to the entire program.
73+
74+
[target modifier]: https://github.com/rust-lang/rfcs/pull/3716
75+
[`-Z harden-sls`]: https://github.com/rust-lang/compiler-team/issues/869
76+
[`-Z stack-protector`]: https://github.com/rust-lang/rust/issues/114903
77+
[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
78+
79+
# Guide-level explanation
80+
[guide-level-explanation]: #guide-level-explanation
81+
82+
When you use a mitigation, such as `-C stack-protector=strong`, if one of your
83+
dependencies does not have that mitigation enabled, compilation will fail.
84+
85+
> Error: your program uses the crate `foo`, that is not protected by
86+
> `-C stack-protector=strong`.
87+
>
88+
> Recompile that crate with the mitigation enabled, or use
89+
> `-C stack-protector=strong-noenforce` to allow creating an artifact
90+
> that has the mitigation only partially enabled.
91+
92+
# Reference-level explanation
93+
[reference-level-explanation]: #reference-level-explanation
94+
95+
Every flag value that enables a mitigation for which enforcement is desired is split
96+
into 2 separate values, "enforcing" and "non-enforcing" mode. The enforcing mode
97+
is the default, non-enforcing mode is constructed by adding `-noenforce` to the
98+
name of the value, for example `-C stack-protector=strong-noenforce` or
99+
`-C sanitizer=shadow-call-stack-noenforce`.
100+
101+
> It is possible to bikeshed the exact naming scheme.
102+
103+
> Every new mitigation would need to decide whether it adopts this scheme,
104+
> but mitigations are expected to adopt it.
105+
106+
Every crate gets a metadata field that contains the set of mitigations it has enabled.
107+
108+
When compiling a crate, if the current crate has a mitigation with enforcement
109+
turned on, and one of the dependencies does not have that mitigation turned
110+
on (whether enforcing or not), a compilation error results.
111+
112+
If a mitigation has multiple "levels", a lower level at a child crate is compatible
113+
with a higher level at a base crate.
114+
115+
The error happens independent of the target crate type (you get an error
116+
if you are building an rlib, not just the final executable).
117+
118+
For example, with `-C stack-protector`, the compatibility table will be
119+
as follows:
120+
121+
| Base\Child | none | none-noenforce | strong | strong-noenforce | all | all-noenforce |
122+
| ---------------- | ---- | -------------- | ------ | -------------------- | ----- | -------------------- |
123+
| none | OK | OK | error | OK - child noenforce | error | OK - child noenforce |
124+
| none-noenforce | OK | OK | error | OK - child noenforce | error | OK - child noenforce |
125+
| strong | OK | OK | OK | OK | error | OK - child noenforce |
126+
| strong-noenforce | OK | OK | OK | OK | error | OK - child noenforce |
127+
| all | OK | OK | OK | OK | OK | OK |
128+
| all-noenforce | OK | OK | OK | OK | OK | OK |
129+
130+
If a program has multiple flags of the same kind, the last flag wins, so e.g.
131+
`-C stack-protector=strong-noenforce -C stack-protector=strong` is the same as
132+
`-C stack-protector=strong`.
133+
134+
# Drawbacks
135+
[drawbacks]: #drawbacks
136+
137+
The `-noenforce` syntax is ugly, and the
138+
`-C allow-partial-mitigations=stack-protector` syntax is either order-dependent
139+
or does not allow for easy appending.
140+
141+
# Rationale and alternatives
142+
[rationale-and-alternatives]: #rationale-and-alternatives
143+
144+
## Syntax alternatives
145+
146+
### -C stack-protector=none-noenforce
147+
148+
The option `-C stack-protector=none-noenforce` is the same as
149+
`-C stack-protector=none`. I am not sure whether we should have both, but
150+
it feels that orthogonality is in favor of having both.
151+
152+
### -C allow-partial-mitigations
153+
154+
Instead of having `-C stack-protector=strong-noenforce`, we could have the
155+
syntax be `-C stack-protector=strong -C allow-partial-mitigations=stack-protector`.
156+
157+
Some people feel that syntax is prettier. In that case, we have 2 options:
158+
159+
#### Without order dependency
160+
161+
This is the simplest to implement. With that,
162+
`-C stack-protector=strong -C allow-partial-mitigations=stack-protector -C stack-protector=strong`
163+
is the same as `-C stack-protector=strong -C allow-partial-mitigations=stack-protector`.
164+
165+
This is unfortunate, because `-C stack-protector=strong -C allow-partial-mitigations=stack-protector` is
166+
a pretty good default for distributions to set. If a distribution sets that, and an application
167+
believes they are turning on enforcing stack protection by using `-C stack-protector=strong`,
168+
the application will not be getting enforcement due to the distribution setting
169+
`-C allow-partial-mitigations=stack-protector`.
170+
171+
On the other hand, maybe there is not actually desire to add
172+
`-C stack-protector=strong -C allow-partial-mitigations=stack-protector` as a default?
173+
174+
Maybe it is actually possible to ship a `-C stack-protector=strong` standard library and
175+
add a `-C stack-protector=strong` default, since the enforcement check only works
176+
"towards roots"?
177+
178+
#### With order dependency
179+
180+
With a small amount of implementation effort, we could have `-C stack-protector=strong` reset the
181+
`-C allow-partial-mitigations=stack-protector` state, so that
182+
`-C stack-protector=strong -C allow-partial-mitigations=stack-protector -C stack-protector=strong`
183+
is equivalent to `-C stack-protector=strong`.
184+
185+
This would work quite well, but I am not sure that rustc wants to have order between different
186+
kinds of CLI arguments.
187+
188+
## Interaction with `-C unsafe-allow-abi-mismatch` / `-C pretend-mitigation-enabled`
189+
190+
The proposed rules do not interact with `-C unsafe-allow-abi-mismatch` at all, so if
191+
you have a "sanitizer runtime" crate that is compiled with the following options:
192+
193+
> -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
194+
195+
Then dependencies will need to use it via `-C sanitizer=shadow-call-stack-noenforce`
196+
rather than `-C sanitizer=shadow-call-stack`, otherwise they will get an error.
197+
198+
As far as I can need, there is no current demand for that sort of sanitizer runtime,
199+
but if that is desired, it might be a good idea to add a
200+
`-C pretend-mitigation-enabled=shadow-call-stack`, and possibly to make
201+
`-C unsafe-allow-abi-mismatch` do that for crates that are target modifiers.
202+
203+
## Defaults
204+
205+
We want that the most obvious way to enable mitigations (e.g.
206+
`-C stack-protector=strong` or `-C sanitizer=shadow-call-stack`) to turn on
207+
enforcement, since that will set people up to a pit of success where mitigations
208+
are enabled throughout.
209+
210+
However, we do want an easy way for distribution owners (for example,
211+
Ubuntu) to turn on mitigations in a non-enforcing way, as is done
212+
today e.g. [by Ubuntu with `-fstack-protector-strong`]. Distributions can't easily
213+
add a new mitigation in an enforcing way, as that will cause widespread
214+
breakage, but they can fairly easily turn a mitigation on in a non-enforcing way.
215+
216+
We do want the combination of defaults to combine in a nice way - if the
217+
distributioon sets `-C stack-protector=strong-noenforce`, and the user adds
218+
`-C stack-protector=strong`, we want the result to be stack-protector set
219+
to strong and enforcing.
220+
221+
On the other hand, maybe there is not actually desire to add
222+
`-C stack-protector=strong -C allow-partial-mitigations=stack-protector` as a default,
223+
which would make this less interesting?
224+
225+
Maybe it is actually possible to ship a `-C stack-protector=strong` standard library and
226+
add a `-C stack-protector=strong` default, since the enforcement check only works
227+
"towards roots"?
228+
229+
[by Ubuntu with `-fstack-protector-strong`]: https://wiki.ubuntu.com/ToolChain/CompilerFlags
230+
231+
## The standard library
232+
233+
One big place where it's very easy to end up with mixed mitigations is the
234+
standard library. The standard library comes compiled with just a single
235+
set of mitigations enabled (as of Rust 1.88: none), and without `-Z build-std`,
236+
it is only possible to use the mitigation settings in the shipped standard
237+
library.
238+
239+
If we find out that some mitigations have a positive cost-benefit ratio
240+
for the standard library (probably at least [`-Z stack-protector`]), we
241+
probably want to ship a standard library supporting them by default, but
242+
in a way that still allows people to compile code without mitigations,
243+
if that fulfills their cost/benefit ratios better.
244+
245+
## Why not target modifiers?
246+
247+
The [target modifier] feature provides a similar goal of preventing mismatches in compiler
248+
settings.
249+
250+
There are several issues with using target modifiers for mitigations:
251+
252+
### The name unsafe-allow-abi-mismatch
253+
254+
The name of the flag that allows mixing target modifiers, `-C unsafe-allow-abi-mismatch`,
255+
does not make sense for cases that are not "unsafe ABI mismatches". It also uses the
256+
word "unsafe", which we prefer not to use except in cases that can result in actual
257+
unsoundness.
258+
259+
### The behavior of unsafe-allow-abi-mismatch
260+
261+
The behavior of `-C unsafe-allow-abi-mismatch` is also not ideal for mitigations.
262+
263+
The flag marks a crate as basically having a "wildcard target modifier", which allows it
264+
to compile with crates with any value of the target modifier.
265+
266+
This is quite good for the original use case - it's an "I know what I am doing" flag
267+
that allows "runtime" crates to be mixed in even if they subtly play with the
268+
ABI rules - for example, in a kernel, where floating point execution is mostly
269+
forbidden, there are a few compilation units using real floats. To call into them, first
270+
you call some special functions that make the floating point registers usable, and then
271+
you call into the CU safely by not having any floats in the signature of the function on
272+
the boundary ([example by Alice Ryhl]).
273+
274+
However, for mitigations, the expected case for disabling mitigations is less people
275+
knowing what they are doing, and more people that don't agree with the performance/security
276+
tradeoff they bring. In that case, we should allow the executable-writer to be aware
277+
of the tradeoff being made, rather than letting libraries in the middle decide it
278+
for them.
279+
280+
## Why not an external tool?
281+
282+
This is somewhat hard to do with an external tool, since there is
283+
no way of looking at a binary and telling what mitigations its components
284+
have (for example [`hardening-check(1)`], exists, but its check for
285+
stack smashing protection only checks that at least 1 function has stack
286+
cookies, rather than checking that every interesting function has it
287+
enabled).
288+
289+
[`hardening-check(1)`]: https://manpages.debian.org/testing/devscripts/hardening-check.1.en.html
290+
291+
## .note.gnu.property
292+
293+
The `.note.gnu.property` field contains a number of properties
294+
(for example, [`GNU_PROPERTY_AARCH64_FEATURE_1_BTI`]) that are used to indicate
295+
that the compiled code contains certain mitigations, for example BTI
296+
(`-Zbranch-protection=bti`).
297+
298+
When linking multiple objects, the linker sets the resulting property to be the
299+
logical AND of the properties of the constituent objects.
300+
301+
For protections such as BTI, the mitigation can only be turned on if all code
302+
within the compiled binary supports it - if one of the object files doesn't,
303+
the loader has to leave the mitigation turned off entirely. The ELF loader uses
304+
the value of the property within the loaded executable to decide whether
305+
to turn on the mitigation.
306+
307+
If it could be arranged, using `.note.gnu.property` could allow mitigation tracking
308+
to propagate across languages - with the final compilation step intentionally erroring
309+
out if the property is not enabled. However, this is also a disadvantage - adding a
310+
new property to `.note.gnu.property` requires cooperation from the target owners.
311+
312+
Therefore, it might be useful as a future step with cooperation from the target owners,
313+
but is not good if we want to be able to add new enforced mitigations without requiring
314+
cooperation from all platforms.
315+
316+
[`GNU_PROPERTY_AARCH64_FEATURE_1_BTI`]: https://docs.rs/object/0.37/object/elf/constant.GNU_PROPERTY_AARCH64_FEATURE_1_BTI.html
317+
318+
# Prior art
319+
[prior-art]: #prior-art
320+
321+
## The panic strategy
322+
323+
The Rust compiler already *has* infrastructure to detect flag mismatches: the
324+
flags `-Cpanic` and `-Zpanic-in-drop`. The prebuilt stdlib comes with different
325+
pieces depending on which strategy is used, although panic landing flags are
326+
not entirely removed when using `-Cpanic=abort`, as only part of the prebuilt
327+
stdlib is switched out.
328+
329+
## Target modifiers
330+
331+
## .note.gnu.property
332+
333+
The `.note.gnu.property` section discussed previously is an example of C code
334+
detecting mismatches of a flag at link time.
335+
336+
# Unresolved questions
337+
[unresolved-questions]: #unresolved-questions
338+
339+
# Future possibilities
340+
[future-possibilities]: #future-possibilities
341+
342+
A possible future extension could be to provide a mechanism to enforce
343+
mitigations across C code and Rust code. This would be an interesting
344+
extension, but it would require cross-language effort that will
345+
take a long period of time to finish. Similarly, another possible
346+
future extension could be to catch mitigation mismatches when using
347+
dynamic linking.

0 commit comments

Comments
 (0)