#![doc(html_root_url = "https://docs.rs/rucc-safety/0.5.0")]
use rucc_ir::{Extra, Func, Inst, InstData, Opcode, Type, Value};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Counts {
pub checked: usize,
pub skipped: usize,
}
pub fn insert(func: &mut Func) -> Counts {
let mut counts = Counts::default();
let accesses: Vec<Inst> = func
.blocks()
.flat_map(|block| func.insts(block).collect::<Vec<_>>())
.filter(|&inst| matches!(func[inst].opcode, Opcode::Load | Opcode::Store))
.collect();
for access in accesses {
match pointer_of(func, access) {
Some(pointer) => {
check(func, access, pointer);
counts.checked += 1;
}
None => counts.skipped += 1,
}
}
counts
}
fn pointer_of(func: &Func, access: Inst) -> Option<Value> {
let args = &func[func[access].args];
let at = match func[access].opcode {
Opcode::Load => 0,
Opcode::Store => 1,
_ => return None,
};
let &value = args.get(at)?;
func[value].ty.is_ptr().then_some(value)
}
fn check(func: &mut Func, access: Inst, pointer: Value) {
let span = func.span(access);
let Extra::Mem(info) = func[access].extra else { return };
let info = func[info];
let args = func.push_values(&[pointer]);
let cap =
func.create_inst(InstData { args, ..InstData::new(Opcode::CapOf) }, &[Type::CAP], span);
func.insert_before(cap, access);
let capability = func[cap].results().next().expect("cap_of produces one value");
let args = func.push_values(&[capability, pointer]);
let extra = Extra::Mem(func.add_mem(info));
let check =
func.create_inst(InstData { args, extra, ..InstData::new(Opcode::CheckBounds) }, &[], span);
func.insert_before(check, access);
}
#[cfg(test)]
mod tests {
use rucc_base::Interner;
use rucc_ir::{
Builder, MemInfo, MemOrder, Module, Restrict, Signature, print_func, verify_func,
};
use rucc_target::{Arch, Env, Os, TargetInfo, Triple};
use super::*;
fn target() -> TargetInfo {
TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu))
}
fn one_of_each(names: &mut Interner) -> Func {
let i32_ = Type::int(32);
let mut func = Func::new(
names.intern("both"),
Signature::new().with_params(&[Type::PTR]).with_returns(&[i32_]),
);
let entry = func.create_block();
let p = func.append_param(entry, Type::PTR);
let info = MemInfo {
size: 4,
align: 4,
order: MemOrder::NotAtomic,
tbaa: None,
restrict: Restrict::NONE,
};
let mut b = Builder::new(&mut func, entry);
let args = b.func().push_values(&[p]);
let extra = Extra::Mem(b.func().add_mem(info));
let loaded = b.value(InstData { args, extra, ..InstData::new(Opcode::Load) }, i32_);
let args = b.func().push_values(&[loaded, p]);
let extra = Extra::Mem(b.func().add_mem(info));
b.inst(InstData { args, extra, ..InstData::new(Opcode::Store) }, &[]);
b.ret(&[loaded]);
func
}
#[test]
fn every_access_gets_a_bounds_check() {
let mut names = Interner::new();
let mut func = one_of_each(&mut names);
assert_eq!(insert(&mut func), Counts { checked: 2, skipped: 0 });
let module = Module::new(names.intern("both.c"), &target());
assert_eq!(
print_func(&module, &func, &names),
"func @both(ptr) -> i32, linkage(external) {\n\
block0(%0: ptr):\n \
%1 = cap_of %0\n \
check_bounds %1, %0, size 4, align 4\n \
%2 = load.i32 %0, size 4, align 4\n \
%3 = cap_of %0\n \
check_bounds %3, %0, size 4, align 4\n \
store %2 -> %0, size 4, align 4\n \
return %2\n\
}\n"
);
}
#[test]
fn what_it_produces_is_a_function_the_verifier_believes() {
let mut names = Interner::new();
let mut func = one_of_each(&mut names);
insert(&mut func);
let module = Module::new(names.intern("both.c"), &target());
if let Err(errors) = verify_func(&module, &func, &names) {
panic!("that was expected to be believed: {errors:#?}");
}
}
#[test]
fn a_function_with_no_accesses_is_left_alone() {
let mut names = Interner::new();
let i32_ = Type::int(32);
let mut func = Func::new(names.intern("nothing"), Signature::new().with_returns(&[i32_]));
let entry = func.create_block();
let mut b = Builder::new(&mut func, entry);
let zero = b.iconst(i32_, 0);
b.ret(&[zero]);
let before = func.counts();
assert_eq!(insert(&mut func), Counts::default());
assert_eq!(func.counts(), before);
}
}