Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for control-flow protection #93439

Merged
merged 1 commit into from
Feb 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
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
21 changes: 20 additions & 1 deletion compiler/rustc_codegen_llvm/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ use rustc_middle::ty::layout::{
};
use rustc_middle::ty::{self, Instance, Ty, TyCtxt};
use rustc_middle::{bug, span_bug};
use rustc_session::config::{BranchProtection, CFGuard, CrateType, DebugInfo, PAuthKey, PacRet};
use rustc_session::config::{BranchProtection, CFGuard, CFProtection};
use rustc_session::config::{CrateType, DebugInfo, PAuthKey, PacRet};
use rustc_session::Session;
use rustc_span::source_map::Span;
use rustc_span::symbol::Symbol;
Expand Down Expand Up @@ -287,6 +288,24 @@ pub unsafe fn create_module<'ll>(
);
}

// Pass on the control-flow protection flags to LLVM (equivalent to `-fcf-protection` in Clang).
if let CFProtection::Branch | CFProtection::Full = sess.opts.debugging_opts.cf_protection {
llvm::LLVMRustAddModuleFlag(
llmod,
llvm::LLVMModFlagBehavior::Override,
abrown marked this conversation as resolved.
Show resolved Hide resolved
"cf-protection-branch\0".as_ptr().cast(),
1,
)
}
if let CFProtection::Return | CFProtection::Full = sess.opts.debugging_opts.cf_protection {
llvm::LLVMRustAddModuleFlag(
llmod,
llvm::LLVMModFlagBehavior::Override,
"cf-protection-return\0".as_ptr().cast(),
1,
)
}

llmod
}

Expand Down
25 changes: 21 additions & 4 deletions compiler/rustc_session/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,22 @@ pub enum CFGuard {
Checks,
}

/// The different settings that the `-Z cf-protection` flag can have.
#[derive(Clone, Copy, PartialEq, Hash, Debug)]
pub enum CFProtection {
/// Do not enable control-flow protection
None,

/// Emit control-flow protection for branches (enables indirect branch tracking).
Branch,

/// Emit control-flow protection for returns.
Return,

/// Emit control-flow protection for both branches and returns.
Full,
}

#[derive(Clone, Copy, Debug, PartialEq, Hash)]
pub enum OptLevel {
No, // -O0
Expand Down Expand Up @@ -2630,11 +2646,11 @@ impl PpMode {
/// we have an opt-in scheme here, so one is hopefully forced to think about
/// how the hash should be calculated when adding a new command-line argument.
crate mod dep_tracking {
use super::LdImpl;
use super::{
BranchProtection, CFGuard, CrateType, DebugInfo, ErrorOutputType, InstrumentCoverage,
LinkerPluginLto, LocationDetail, LtoCli, OptLevel, OutputType, OutputTypes, Passes,
SourceFileHashAlgorithm, SwitchWithOptPath, SymbolManglingVersion, TrimmedDefPaths,
BranchProtection, CFGuard, CFProtection, CrateType, DebugInfo, ErrorOutputType,
InstrumentCoverage, LdImpl, LinkerPluginLto, LocationDetail, LtoCli, OptLevel, OutputType,
OutputTypes, Passes, SourceFileHashAlgorithm, SwitchWithOptPath, SymbolManglingVersion,
TrimmedDefPaths,
};
use crate::lint;
use crate::options::WasiExecModel;
Expand Down Expand Up @@ -2715,6 +2731,7 @@ crate mod dep_tracking {
NativeLibKind,
SanitizerSet,
CFGuard,
CFProtection,
TargetTriple,
Edition,
LinkerPluginLto,
Expand Down
22 changes: 22 additions & 0 deletions compiler/rustc_session/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ mod desc {
pub const parse_sanitizer_memory_track_origins: &str = "0, 1, or 2";
pub const parse_cfguard: &str =
"either a boolean (`yes`, `no`, `on`, `off`, etc), `checks`, or `nochecks`";
pub const parse_cfprotection: &str = "`none`|`no`|`n` (default), `branch`, `return`, or `full`|`yes`|`y` (equivalent to `branch` and `return`)";
pub const parse_strip: &str = "either `none`, `debuginfo`, or `symbols`";
pub const parse_linker_flavor: &str = ::rustc_target::spec::LinkerFlavor::one_of();
pub const parse_optimization_fuel: &str = "crate=integer";
Expand Down Expand Up @@ -695,6 +696,25 @@ mod parse {
true
}

crate fn parse_cfprotection(slot: &mut CFProtection, v: Option<&str>) -> bool {
if v.is_some() {
let mut bool_arg = None;
if parse_opt_bool(&mut bool_arg, v) {
abrown marked this conversation as resolved.
Show resolved Hide resolved
*slot = if bool_arg.unwrap() { CFProtection::Full } else { CFProtection::None };
return true;
}
}

*slot = match v {
None | Some("none") => CFProtection::None,
Some("branch") => CFProtection::Branch,
Some("return") => CFProtection::Return,
Some("full") => CFProtection::Full,
Some(_) => return false,
};
Comment on lines +708 to +714
Copy link
Member

Choose a reason for hiding this comment

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

It seems like it'd be pretty hard to extend this kind of CLI in a sensible manner in the future if the underlying CF protection concept changes in any way.

Copy link
Contributor Author

@abrown abrown Feb 1, 2022

Choose a reason for hiding this comment

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

To address this and your comments below about wanting a unified -Zbranch-protection: I agree that it would be nice to unify these various properties (see -Ccf-guard as well) but I think that could reasonably be discussed in a separate issue, mainly because I suspect the various flags are not exactly equivalent and are implemented with different assumptions (e.g., cf-protection expects certain ELF tags, kernel support, and architecture support). If such an issue exists, I would be glad to comment there. But I would not expect to have to figure that out in this PR. I was hoping this could just add support with the same flags that Clang exposes (-fcf-protection=none|branch|return|full).

true
}

crate fn parse_linker_flavor(slot: &mut Option<LinkerFlavor>, v: Option<&str>) -> bool {
match v.and_then(LinkerFlavor::from_str) {
Some(lf) => *slot = Some(lf),
Expand Down Expand Up @@ -1142,6 +1162,8 @@ options! {
"select which borrowck is used (`mir` or `migrate`) (default: `migrate`)"),
branch_protection: BranchProtection = (BranchProtection::default(), parse_branch_protection, [TRACKED],
"set options for branch target identification and pointer authentication on AArch64"),
cf_protection: CFProtection = (CFProtection::None, parse_cfprotection, [TRACKED],
"instrument control-flow architecture protection"),
cgu_partitioning_strategy: Option<String> = (None, parse_opt_string, [TRACKED],
"the codegen unit partitioning strategy to use"),
chalk: bool = (false, parse_bool, [TRACKED],
Expand Down
40 changes: 40 additions & 0 deletions src/doc/unstable-book/src/compiler-flags/cf-protection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# `cf-protection`

This option enables control-flow enforcement technology (CET) on x86; a more detailed description of
CET is available [here]. Similar to `clang`, this flag takes one of the following values:

- `none` - Disable CET completely (this is the default).
- `branch` - Enable indirect branch tracking (`IBT`).
- `return` - Enable shadow stack (`SHSTK`).
- `full` - Enable both `branch` and `return`.

[here]: https://www.intel.com/content/www/us/en/develop/articles/technical-look-control-flow-enforcement-technology.html

This flag only applies to the LLVM backend: it sets the `cf-protection-branch` and
`cf-protection-return` flags on LLVM modules. Note, however, that all compiled modules linked
together must have the flags set for the compiled output to be CET-enabled. Currently, Rust's
standard library does not ship with CET enabled by default, so you may need to rebuild all standard
modules with a `cargo` command like:

```sh
$ RUSTFLAGS="-Z cf-protection=full" RUSTC="rustc-custom" cargo +nightly build -Z build-std --target x86_64-unknown-linux-gnu
```

### Detection

An ELF binary is CET-enabled if it has the `IBT` and `SHSTK` tags, e.g.:

```sh
$ readelf -a target/x86_64-unknown-linux-gnu/debug/example | grep feature:
Properties: x86 feature: IBT, SHSTK
```

### Troubleshooting

To display modules that are not CET enabled, examine the linker errors available when `cet-report` is enabled:

```sh
$ RUSTC_LOG=rustc_codegen_ssa::back::link=info rustc-custom -v -Z cf-protection=full -C link-arg="-Wl,-z,cet-report=warning" -o example example.rs
...
/usr/bin/ld: /.../build/x86_64-unknown-linux-gnu/stage1/lib/rustlib/x86_64-unknown-linux-gnu/lib/libstd-d73f7266be14cb8b.rlib(std-d73f7266be14cb8b.std.f7443020-cgu.12.rcgu.o): warning: missing IBT and SHSTK properties
```
38 changes: 38 additions & 0 deletions src/test/codegen/cf-protection.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Test that the correct module flags are emitted with different control-flow protection flags.

// revisions: undefined none branch return full
// needs-llvm-components: x86
// [undefined] compile-flags:
// [none] compile-flags: -Z cf-protection=none
// [branch] compile-flags: -Z cf-protection=branch
// [return] compile-flags: -Z cf-protection=return
// [full] compile-flags: -Z cf-protection=full
// compile-flags: --target x86_64-unknown-linux-gnu

#![crate_type = "lib"]
Copy link
Member

@nagisa nagisa Feb 13, 2022

Choose a reason for hiding this comment

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

This needs to be a #![no_core] test. There are a number of others that use #![no_core] that you can use as an example.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, re-submitted. I'm still not exactly sure why this must be a no_core test--can you point to an explanation for that? I'm also going to clean up the PR description message since it looks like that is what gets rolled up.

Copy link
Member

@nagisa nagisa Feb 14, 2022

Choose a reason for hiding this comment

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

That's because the full rustc test suite involves running on architectures other than x86_64 as well. This test utilizes cross-compilation by the virtue of specifying

// compile-flags: --target x86_64-unknown-linux-gnu 

and yet the standard library is not made available for this target, leading to a test failure. Hence #![no_core]. The alternative would be to specify

// only-x86

instead, but that would mean anybody developing the compiler on non-x86 machines would not get the coverage this test provides.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also, what is the correct state to put this PR in? Should I give another rustbot command to change the label? Or do you?

#![feature(no_core, lang_items)]
#![no_core]

#[lang="sized"]
trait Sized { }

// A basic test function.
pub fn test() {
}

// undefined-NOT: !"cf-protection-branch"
// undefined-NOT: !"cf-protection-return"

// none-NOT: !"cf-protection-branch"
// none-NOT: !"cf-protection-return"

// branch-NOT: !"cf-protection-return"
// branch: !"cf-protection-branch", i32 1
// branch-NOT: !"cf-protection-return"

// return-NOT: !"cf-protection-branch"
// return: !"cf-protection-return", i32 1
// return-NOT: !"cf-protection-branch"

// full: !"cf-protection-branch", i32 1
// full: !"cf-protection-return", i32 1