-
Notifications
You must be signed in to change notification settings - Fork 43
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
cfi: Simpler launder implementation for common types #1714
base: main
Are you sure you want to change the base?
Changes from all commits
4da7571
fc5f1b9
f466b9f
153db1f
969221d
17fe7f2
608c607
05a5902
40e768c
ea51249
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
// Licensed under the Apache-2.0 license | ||
|
||
// These tests are here so that they are excluded in FPGA tests. | ||
|
||
// These tests don't directly import the CFI code. If they fail, | ||
// this likely indicates that the CFI laundering code may not | ||
// be doing what we want, and we need to investigate. | ||
|
||
#[cfg(test)] | ||
mod test { | ||
|
||
const START: &str = " | ||
#![no_std] | ||
|
||
pub fn add(mut a: u32, mut b: u32) -> u32 { | ||
launder(a) + launder(a) + launder(b) + launder(b) | ||
} | ||
"; | ||
|
||
const LAUNDER: &str = " | ||
#[inline(always)] | ||
fn launder(mut val: u32) -> u32 { | ||
// Safety: this is a no-op, since we don't modify the input. | ||
unsafe { | ||
core::arch::asm!( | ||
\"/* {t} */\", | ||
t = inout(reg) val, | ||
); | ||
} | ||
val | ||
}"; | ||
|
||
const NO_LAUNDER: &str = " | ||
#[inline(always)] | ||
fn launder(mut val: u32) -> u32 { | ||
val | ||
} | ||
"; | ||
|
||
fn compile_to_riscv32_asm(src: String) -> String { | ||
let dir = std::env::temp_dir(); | ||
let src_path = dir.join("asm.rs"); | ||
let dst_path = dir.join("asm.s"); | ||
|
||
std::fs::write(src_path.clone(), src).expect("could not write asm file"); | ||
|
||
let p = std::process::Command::new("rustc") | ||
.args([ | ||
"--crate-type=lib", | ||
"--target", | ||
"riscv32imc-unknown-none-elf", | ||
"-C", | ||
"opt-level=s", | ||
"--emit", | ||
"asm", | ||
src_path.to_str().expect("could not convert path"), | ||
"-o", | ||
dst_path.to_str().expect("could not convert path"), | ||
]) | ||
.output() | ||
.expect("failed to compile"); | ||
assert!(p.status.success()); | ||
std::fs::read_to_string(dst_path).expect("could not read asm file") | ||
} | ||
|
||
#[test] | ||
fn test_launder() { | ||
// With no laundering, LLVM can simplify the double add to a shift left. | ||
let src = format!("{}{}", START, NO_LAUNDER); | ||
let asm = compile_to_riscv32_asm(src); | ||
assert!(asm.contains("sll")); | ||
|
||
// With laundering, LLVM cannot simplify the double add and has to use the register twice. | ||
let src = format!("{}{}", START, LAUNDER); | ||
let asm = compile_to_riscv32_asm(src); | ||
assert!(!asm.contains("sll")); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,7 +20,7 @@ use caliptra_error::CaliptraError; | |
use crate::CfiCounter; | ||
use core::cfg; | ||
use core::cmp::{Eq, Ord, PartialEq, PartialOrd}; | ||
use core::marker::Copy; | ||
use core::marker::{Copy, PhantomData}; | ||
|
||
/// CFI Panic Information | ||
#[derive(Debug, Clone, Copy, Eq, PartialEq)] | ||
|
@@ -94,16 +94,107 @@ impl From<CfiPanicInfo> for CaliptraError { | |
/// # Returns | ||
/// | ||
/// `T` - Same value | ||
pub fn cfi_launder<T>(val: T) -> T { | ||
pub fn cfi_launder<T>(val: T) -> T | ||
where | ||
Launder<T>: LaunderTrait<T>, | ||
{ | ||
if cfg!(feature = "cfi") { | ||
// Note: The black box seems to be disabling more optimization | ||
// than necessary and results in larger binary size | ||
core::hint::black_box(val) | ||
Launder { _val: PhantomData }.launder(val) | ||
} else { | ||
val | ||
} | ||
} | ||
|
||
pub trait LaunderTrait<T> { | ||
fn launder(&self, val: T) -> T { | ||
core::hint::black_box(val) | ||
} | ||
} | ||
|
||
pub struct Launder<T> { | ||
_val: PhantomData<T>, | ||
} | ||
|
||
// Inline-assembly laundering trick is adapted from OpenTitan: | ||
// https://github.com/lowRISC/opentitan/blob/master/sw/device/lib/base/hardened.h#L193 | ||
// | ||
// NOTE: This implementation is LLVM-specific, and should be considered to be | ||
// a no-op in every other compiler. For example, GCC has in the past peered | ||
// into the insides of assembly blocks. | ||
// | ||
// At the time of writing, it seems preferable to have something we know is | ||
// correct rather than being overly clever; this is recorded here in case | ||
// the current implementation is unsuitable and we need something more | ||
// carefully tuned. | ||
// | ||
// Unlike in C, we don't have volatile assembly blocks, so this doesn't | ||
// necessarily prevent reordering by LLVM. | ||
// | ||
// When we're building for static analysis, reduce false positives by | ||
// short-circuiting the inline assembly block. | ||
impl LaunderTrait<u32> for Launder<u32> { | ||
#[allow(asm_sub_register)] | ||
fn launder(&self, val: u32) -> u32 { | ||
let mut val = val; | ||
// Safety: this is a no-op, since we don't modify the input. | ||
unsafe { | ||
// We use inout so that LLVM thinks the value might | ||
// be mutated by the assembly and can't eliminate it. | ||
core::arch::asm!( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is magic enough to be worth a comment about why it works 😄 Also can we be confident that this will continue to work in future versions of LLVM? I wonder if we can write a test that proves the compiled binary didn't optimizer out values passed to cfi_launder. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll definitely write a comment. It's quite difficult to test this, I think, and it certainly is not guaranteed by LLVM in the future (nor would be basically any trick, probably, since all of these are meant to fool the compiler without actually doing anything). I verified it by loading code into Compiler Explorer for now. There are some ways to test this by invoking the compiler in a test and checking the emitted assembly and, for example, counting instructions with and without this function being applied. If this stops working in the future, this doesn't stop the code using it from working, it just means that one layer of protection is removed. And theoretically we'd notice because the code size would change. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added a test that invokes There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice :) Thanks! I talked with the OT folks and they do manual pre-TO validation for things like this. Might be worth doing something at ROM release time (or adding a note to pre-TO guidance for integrators) to spot check important sections. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we're mostly safe here, since we pin the version of Cargo (and presumably rustc and LLVM). So we mostly need to validate when we update our toolchain. |
||
"/* {t} */", | ||
t = inout(reg) val, | ||
); | ||
} | ||
val | ||
} | ||
} | ||
|
||
impl LaunderTrait<bool> for Launder<bool> { | ||
#[allow(asm_sub_register)] | ||
fn launder(&self, val: bool) -> bool { | ||
let mut val = val as u32; | ||
// Safety: this is a no-op, since we don't modify the input. | ||
unsafe { | ||
core::arch::asm!( | ||
"/* {t} */", | ||
t = inout(reg) val, | ||
); | ||
} | ||
val != 0 | ||
} | ||
} | ||
|
||
impl LaunderTrait<usize> for Launder<usize> { | ||
#[allow(asm_sub_register)] | ||
fn launder(&self, mut val: usize) -> usize { | ||
// Safety: this is a no-op, since we don't modify the input. | ||
unsafe { | ||
core::arch::asm!( | ||
"/* {t} */", | ||
t = inout(reg) val, | ||
); | ||
} | ||
val | ||
} | ||
} | ||
|
||
impl<const N: usize, T> LaunderTrait<[T; N]> for Launder<[T; N]> {} | ||
impl<'a, const N: usize, T> LaunderTrait<&'a [T; N]> for Launder<&'a [T; N]> { | ||
fn launder(&self, val: &'a [T; N]) -> &'a [T; N] { | ||
let mut valp = val.as_ptr() as *const [T; N]; | ||
// Safety: this is a no-op, since we don't modify the input. | ||
unsafe { | ||
core::arch::asm!( | ||
"/* {t} */", | ||
t = inout(reg) valp, | ||
); | ||
&*valp | ||
} | ||
} | ||
} | ||
impl LaunderTrait<Option<u32>> for Launder<Option<u32>> {} | ||
impl LaunderTrait<CfiPanicInfo> for Launder<CfiPanicInfo> {} | ||
|
||
/// Control flow integrity panic | ||
/// | ||
/// This panic is raised when the control flow integrity error is detected | ||
|
@@ -157,6 +248,7 @@ macro_rules! cfi_assert_macro { | |
pub fn $name<T>(lhs: T, rhs: T) | ||
where | ||
T: $trait1 + $trait2, | ||
Launder<T>: LaunderTrait<T>, | ||
{ | ||
if cfg!(feature = "cfi") { | ||
CfiCounter::delay(); | ||
|
@@ -184,10 +276,28 @@ cfi_assert_macro!(cfi_assert_lt, <, Ord, PartialOrd, AssertLtFail); | |
cfi_assert_macro!(cfi_assert_ge, >=, Ord, PartialOrd, AssertGeFail); | ||
cfi_assert_macro!(cfi_assert_le, <=, Ord, PartialOrd, AssertLeFail); | ||
|
||
// special case for bool assert | ||
#[inline(always)] | ||
#[allow(unused)] | ||
pub fn cfi_assert_bool(cond: bool) { | ||
if cfg!(feature = "cfi") { | ||
CfiCounter::delay(); | ||
if !cond { | ||
cfi_panic(CfiPanicInfo::AssertEqFail); | ||
} | ||
|
||
// Second check for glitch protection | ||
CfiCounter::delay(); | ||
if !cfi_launder(cond) { | ||
cfi_panic(CfiPanicInfo::AssertEqFail); | ||
} | ||
} | ||
} | ||
|
||
#[macro_export] | ||
macro_rules! cfi_assert { | ||
($cond: expr) => { | ||
cfi_assert_eq($cond, true) | ||
cfi_assert_bool($cond) | ||
}; | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These Rust type acrobatics are so that we can have special, smaller implementations for register-sized but default to
core::hint::black_box
for everything else.I'm open to suggestions on how to make this simpler or avoid a
derive
.