rfcs icon indicating copy to clipboard operation
rfcs copied to clipboard

Mitigation enforcement

Open arielb1 opened this issue 3 months ago • 23 comments

Zulip: https://rust-lang.zulipchat.com/#narrow/channel/131828-t-compiler/topic/Mitigation.20enforcement.20.28.60-C.20allow-partial-mitigations.60.29/with/539293124

Rendered

arielb1 avatar Sep 13 '25 19:09 arielb1

FYI: Your rendered link doesn't work because you updated the filename in the URL to account for the PR number, but didn't actually update the filename in the code itself.

clarfonthey avatar Sep 13 '25 20:09 clarfonthey

FYI: Your rendered link doesn't work because you updated the filename in the URL to account for the PR number, but didn't actually update the filename in the code itself.

Fixed

arielb1 avatar Sep 13 '25 20:09 arielb1

Some projects may already have tooling to check certain things are as expected, e.g. objtool in the Linux kernel -- perhaps worth mentioning in "Motivation" or "External tools" sections. Of course, having a higher level layer checking things look OK is good, and likely could cover different aspects and would work for more projects.

ojeda avatar Sep 14 '25 09:09 ojeda

Some projects may already have tooling to check certain things are as expected, e.g. objtool in the Linux kernel -- perhaps worth mentioning in "Motivation" or "External tools" sections

I mentioned hardening-check. If you have experience with objtool, you can add that as well.

I also couldn't find any documentation for objtool used as a hardening check tool, so if you could provide me some I would try to include it.

arielb1 avatar Sep 14 '25 09:09 arielb1

The docs are here: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/tools/objtool/Documentation/objtool.txt

I mentioned objtool because it is lower level, e.g. it inspects the instruction stream and so on, while hardening-check, AFAIK, is more about probing declared information using other tools (mainly, at least, from a quick look -- Cc @kees).

ojeda avatar Sep 14 '25 10:09 ojeda

So it looks like it works on a per-.o file basis rather than on a per-executable/shared-library basis, so it requires controlling the linker, basically.

Is that right?

hardening-check, AFAIK, is more about probing declared information using other tools

hardening-check is a lot less sophisticated, but it does perform some checks on the instruction stream - e.g. looking for checks to stack_chk_fail. It also works on executables rather than object files.

I do think that the big difference is "tool that works on .o files vs. tool that works on executables", since if the tool needs .o files you have to use it as a part of the compilation process.

arielb1 avatar Sep 14 '25 10:09 arielb1

it does perform some checks on the instruction stream - e.g. looking for checks to stack_chk_fail

I don't think the stack_chk_fail check counts as "checks on the instruction stream". The one that comes close to that is the stack clash one, which is essentially checking regexes against the disassembly.

since if the tool needs .o files you have to use it as a part of the compilation process.

objtool has checks on linked files too, and more generally one could make a tool that works on final executables (there are very advanced analyzers out there, after all). In any case, projects that care about these bits (i.e. those with custom tools like the kernel) can likely deal with a custom build process that inspects object files. So I don't think that is a big difference -- I would say the big differences are the use case and the level at which the checks are performed.

Anyway, none of the above really matters -- I mentioned objtool because it is the kind of tool that the "Why not an external tool?" section (or the motivation) should probably mention, given it is a good example of a tool that is able to perform certain non-trivial low level checks for a quite complex project (and is open source). Having more layers checks things from different angles is good! :)

Thanks for working on this!

ojeda avatar Sep 14 '25 11:09 ojeda

If there was a "magic" analyzer that would reliably do the sanitizer enforcement, there would be much less need for it as a compiler flag.

As far as I can tell, the existing analyzers either have large holes or require significant project-specific intervention.

That of course does not mean they are not useful, only that they don't automatically solve the problem for everyone.

arielb1 avatar Sep 14 '25 11:09 arielb1

Not sure if there is a disagreement here, but just in case: I didn't claim there is a "magic" analyzer out there solving this. Quite the contrary -- I said that even if such a tool existed that covered everything, having an independent check at another layer like this RFC proposes would still be useful. The objtool relevance is not because it solves the issue completely or for every project, but rather because it is an important example of such tooling, especially since the Linux kernel is mentioned.

ojeda avatar Sep 14 '25 17:09 ojeda

A summary of the reasoning behind explicitly stating that it is allowed for the Rust compiler to accept a mitigation might be applied partially:

  • Allowing the user to inadvertently enable an exploit mitigation partially, or worse having this behavior by default, may lead to considerably reducing the effectiveness of the mitigation without the user knowing about it and thinking the program is otherwise protected or have security guarantees it actually doesn't have.

For solving this, I'm in favor of the simpler -C allow-partial-mitigations[=<mitigation1>,...,<mitigationN>]. It's simple, effective, and cover all cases where we wouldn't want the user to inadvertently enable an exploit mitigation partially (e.g., CFI, Stack Protector, etc.). It also aligns with the Rust philosophy of having the user to explicitly opt for the less safe approach.

For stack smashing protection specifically (which is beyond the scope of this RFC, but relevant to choosing the right approach):

  • For stack protector, the most common behavior the user gets for quite some time already is actually globally enabled and enforced, not partially enabled. Most major Linux distributions build their packages with stack protector strong, including the C standard library (which would be the equivalent of always using -Zbuild-std) and have the option implicitly enabled in their toolchains so the behavior the user gets is much closer to always using -Zbuild-std than not. For example, it's enabled by default in Ubuntu for quite some time already (see https://wiki.ubuntu.com/ToolChain/CompilerFlags).

For this, I propose either:

  1. That the user has to use -C allow-partial-mitigations=stack-protector to allow it to be applied partially as described above. (Otherwise, the user might inadvertently think they are getting the most common behavior as described above.)
  2. Do (1) and enable it with the strong mode/strategy by default both for the Rust standard library and the Rust compiler--this is the default for most major Linux distributions for quite some time already.

For (2), we could perform a comprehensive set of tests for binary size, build time, and run time performance and make a decision based upon the ROI (considering the returns are different from a program written in C or C++ compared to a program written in Rust).

rcvalle avatar Sep 15 '25 19:09 rcvalle

The docs are here: https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/tools/objtool/Documentation/objtool.txt

I mentioned objtool because it is lower level, e.g. it inspects the instruction stream and so on, while hardening-check, AFAIK, is more about probing declared information using other tools (mainly, at least, from a quick look -- Cc @kees).

Objtool is mainly designed to validate the expected construction of the (x86) kernel's functions and related metadata (e.g. "can we always unwind the stack correctly?" or "what things are reachable for indirect calls?")

As far as the mitigation enforcement idea as presented, I like the idea of having this be a declared compatibility thing to check. In C it is trivial to mix and match different mitigations and the resulting binary is very hard to analyze after the fact (see hardening-check which is best-effort). Rejecting mismatches up front would be nice! :)

kees avatar Sep 15 '25 23:09 kees

Do (1) and enable it with the strong mode/strategy by default both for the Rust standard library and the Rust compiler--this is the default for most major Linux distributions for quite some time already.

I do think there is an interesting middle point where we ship a libstd that has stack protector=strong enabled, but don't enable it for default in the toolchain. This means that people can easily enable complete stack protector for their application without -Z build-std, but there is less annoying performance impact.

In any case, it should not be relevant to this RFC.

arielb1 avatar Sep 16 '25 10:09 arielb1

For solving this, I'm in favor of the simpler -C allow-partial-mitigations[=<mitigation1>,...,<mitigationN>]. It's simple, effective, and cover all cases where we wouldn't want the user to inadvertently enable an exploit mitigation partially (e.g., CFI, Stack Protector, etc.). It also aligns with the Rust philosophy of having the user to explicitly opt for the less safe approach.

I also much prefer this approach to having many -noenforce variants of flag values. When a distribution of Rust ships a standard library with a mitigation enabled, then I think there are two ways of doing this that we should encourage:

  1. Using -Zbuild-std or an equivalent mechanism. Our draft for rust-lang/rust-project-goals#274 aims eventually to make it so that if a target modifier (or exploit mitigation, I guess) is enabled, then the mismatch with std is detected and std is rebuilt automatically. This seems like a reasonable user experience and addresses this for the Rust project's distribution of Rust.
  2. Shipping rustc w/ the default changed to match the flags used with the standard library, so the partial mitigations error doesn't trigger by default when rustc is used without any flags. I think I'd be happy for this to be what a Linux distribution/internal company distribution/etc does - as long as the flags/defaults changed are all otherwise stable.

davidtwco avatar Sep 17 '25 10:09 davidtwco

@wesleywiser

Made -C allow-partial-mitigations non-order-dependent the default, since there does not seem like enough desire for the order-dependent variant.

We can probably change our choice over an edition boundary if we want (can we?)

arielb1 avatar Sep 23 '25 14:09 arielb1

Should we go forward with this RFC?

arielb1 avatar Nov 01 '25 15:11 arielb1

Would you mind providing a summary here of the overall discussions and consensus for easier reference before we move forward with it?

rcvalle avatar Nov 03 '25 18:11 rcvalle

Would you mind providing a summary here of the overall discussions and consensus for easier reference before we move forward with it?

Most arguments are talked about in the RFC document.

I do think that @wesleywiser believes in some form of JSON-emitting -emit=component-info flag. That is not mentioned in the document but I do think it's worth talking with the Cargo team about, so I'm in favor of taking some time to talk with them.

I do think that for security-relevant things, rather than soundness-relevant things, it would be fine to have such a flag and doing the policy within Cargo, rather than doing the policy within rustc. The user-visible behavior would be mostly unchanged.

arielb1 avatar Nov 04 '25 21:11 arielb1

I am asking @weihanglo on the Cargo team for his opinion about having policy on the Cargo side.

arielb1 avatar Nov 04 '25 22:11 arielb1

I think this feature implemented on rustc is much more useful and benefits projects and teams that don't use Cargo [e.g., the projects I work on/for don't, Rust-for-Linux (cc @ojeda), etc].

rcvalle avatar Nov 04 '25 22:11 rcvalle

I do think that -emit=component-info and -C allow-partial-mitigations can live pretty well together - tho -emit=component-info deserves a separate RFC.

arielb1 avatar Nov 04 '25 22:11 arielb1

To the extent we have a large amount of projects that don't use Cargo but are not big enough to implement their own scanning scripts (I think RFL is big enough that they don't care about 1 more JSON scan, especially if it lets them implement a more-complex policy, but other projects are probably not big enough), then I agree that not having scanning-by-default for these is a big disadvantage.

arielb1 avatar Nov 04 '25 22:11 arielb1

Is there anything this is still waiting on? Currently making a draft implementation.

arielb1 avatar Nov 20 '25 14:11 arielb1

https://github.com/rust-lang/rust/pull/149357

arielb1 avatar Nov 26 '25 18:11 arielb1

I'm not sure we want to dive into any exploit mitigation implementation details here and set rules for compatibility or enforcement of their modes/options. We probably should just mention that they should be compatible, not invalidate the security guarantees/threat model provided by the exploit mitigation, and not result in UB, or just have the same mode/option enabled.

I do think that at least for stack protector, we want people to be able to enforce either strong or all separately. It seems to me that if someone wants "stack-protector=all" they should be able to enforce stack-protector to be all for all of their code.

I do think that -C stack-protector=all should enforce that dependencies have -C stack-protector=all as well (as opposed to merely -C stack-protector=strong)

Tho I don't care that much.

arielb1 avatar Dec 16 '25 23:12 arielb1

@rustbot merge

rcvalle avatar Dec 17 '25 18:12 rcvalle

@rcvalle do you mean @rfcbot fcp merge compiler

kennytm avatar Dec 18 '25 07:12 kennytm

Let me try:

@rfcbot fcp merge compiler

rcvalle avatar Dec 18 '25 18:12 rcvalle

Doesn't look like it worked

arielb1 avatar Dec 23 '25 14:12 arielb1