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