use std::time::Instant;
use probe_rs::{Core, MemoryInterface, RegisterId};
use crate::{
registers::PC,
target_info::{StackInfo, TargetInfo},
Elf, TIMEOUT,
};
const CANARY_U8: u8 = 0xAA;
const CANARY_U32: u32 = u32::from_le_bytes([CANARY_U8, CANARY_U8, CANARY_U8, CANARY_U8]);
#[derive(Clone, Copy)]
pub struct Canary {
addr: u32,
data_below_stack: bool,
size: u32,
size_kb: f64,
}
impl Canary {
pub fn install(
core: &mut Core,
elf: &Elf,
target_info: &TargetInfo,
) -> anyhow::Result<Option<Self>> {
let canary = match Self::prepare(elf, &target_info.stack_info) {
Some(canary) => canary,
None => return Ok(None),
};
let start = Instant::now();
paint_subroutine::execute(core, canary.addr, canary.size)?;
let seconds = start.elapsed().as_secs_f64();
canary.log_time("painting", seconds);
Ok(Some(canary))
}
pub fn measure(self, core: &mut Core, elf: &Elf) -> anyhow::Result<bool> {
let start = Instant::now();
let touched_address = measure_subroutine::execute(core, self.addr, self.size)?;
let seconds = start.elapsed().as_secs_f64();
self.log_time("reading", seconds);
let min_stack_usage = match touched_address {
Some(touched_address) => {
log::debug!("stack was touched at {touched_address:#010X}");
elf.vector_table.initial_stack_pointer - touched_address
}
None => {
log::warn!("stack was not used at all");
0
}
};
let used_kb = min_stack_usage as f64 / 1024.0;
let pct = used_kb / self.size_kb * 100.0;
let msg = format!(
"program has used at least {used_kb:.2}/{:.2} KiB ({pct:.1}%) of stack space",
self.size_kb
);
if pct > 90.0 {
log::warn!("{}", msg);
if self.data_below_stack {
log::warn!("data segments might be corrupted due to stack overflow");
}
Ok(true)
} else {
log::info!("{}", msg);
Ok(false)
}
}
fn prepare(elf: &Elf, stack_info: &Option<StackInfo>) -> Option<Self> {
let stack_info = match stack_info {
Some(stack_info) => stack_info,
None => {
log::debug!("couldn't find valid stack range, not placing stack canary");
return None;
}
};
if elf.program_uses_heap() {
log::debug!("heap in use, not placing stack canary");
return None;
}
let stack_addr = *stack_info.range.start();
let stack_size = *stack_info.range.end() - stack_addr;
log::debug!(
"{stack_size} bytes of stack available ({stack_addr:#010X} ..= {:#010X})",
stack_info.range.end(),
);
Self::assert_subroutines(stack_addr, stack_size)?;
Some(Canary {
addr: stack_addr,
data_below_stack: stack_info.data_below_stack,
size: stack_size,
size_kb: stack_size as f64 / 1024.0,
})
}
fn log_time(&self, action: &str, seconds: f64) {
log::debug!(
"{action} {:.2} KiB of RAM took {seconds:.3}s ({:.2} KiB/s)",
self.size_kb,
self.size_kb / seconds
)
}
fn assert_subroutines(stack_addr: u32, stack_size: u32) -> Option<()> {
assert_eq!(stack_addr % 4, 0, "low_addr needs to be 4-byte-aligned");
assert_eq!(stack_size % 4, 0, "stack_size needs to be 4-byte-aligned");
assert_eq!(
paint_subroutine::size() % 4,
0,
"paint subroutine needs to be 4-byte-aligned"
);
assert_eq!(
measure_subroutine::size() % 4,
0,
"measure subroutine needs to be 4-byte-aligned"
);
if (stack_size < paint_subroutine::size()) || (stack_size < measure_subroutine::size()) {
log::warn!("subroutines do not fit in stack; not placing stack canary");
None
} else {
Some(())
}
}
}
mod paint_subroutine {
use super::*;
pub fn execute(core: &mut Core, low_addr: u32, stack_size: u32) -> Result<(), probe_rs::Error> {
super::execute_subroutine(core, low_addr, stack_size, self::SUBROUTINE)?;
self::overwrite_subroutine(core, low_addr)?;
Ok(())
}
fn overwrite_subroutine(core: &mut Core, low_addr: u32) -> Result<(), probe_rs::Error> {
core.write_8(low_addr as u64, &[CANARY_U8; self::SUBROUTINE.len()])
}
const SUBROUTINE: [u8; 12] = [
0x88, 0x42, 0x01, 0xd8, 0x04, 0xc0, 0xfb, 0xe7, 0x00, 0xbe, 0x00, 0xbe, ];
pub const fn size() -> u32 {
self::SUBROUTINE.len() as _
}
}
mod measure_subroutine {
use super::*;
pub fn execute(
core: &mut Core,
low_addr: u32,
stack_size: u32,
) -> Result<Option<u32>, probe_rs::Error> {
if let Some(addr) = self::search_with_probe(core, low_addr)? {
return Ok(Some(addr)); }
super::execute_subroutine(core, low_addr, stack_size, self::SUBROUTINE)?;
self::get_result(core)
}
fn search_with_probe(core: &mut Core, low_addr: u32) -> Result<Option<u32>, probe_rs::Error> {
let mut buf = [0; self::SUBROUTINE.len()];
core.read_8(low_addr as u64, &mut buf)?;
match buf.into_iter().position(|b| b != CANARY_U8) {
Some(pos) => Ok(Some(low_addr + pos as u32)),
None => Ok(None),
}
}
fn get_result(core: &mut Core) -> Result<Option<u32>, probe_rs::Error> {
let word_addr = match core.read_core_reg(RegisterId(0))? {
0 => return Ok(None),
n => n,
};
let offset = core
.read_word_32(word_addr as u64)?
.to_le_bytes()
.into_iter()
.position(|b| b != CANARY_U8)
.expect("some byte has to be touched, if `word_addr != 0`");
Ok(Some(word_addr + offset as u32))
}
const SUBROUTINE: [u8; 20] = [
0x88, 0x42, 0x04, 0xd2, 0x03, 0x68, 0x93, 0x42, 0x02, 0xd1, 0x00, 0x1d, 0xf8, 0xe7, 0x00, 0x20, 0x00, 0xbe, 0x00, 0xbe, ];
pub const fn size() -> u32 {
self::SUBROUTINE.len() as _
}
}
fn execute_subroutine<const N: usize>(
core: &mut Core,
low_addr: u32,
stack_size: u32,
subroutine: [u8; N],
) -> Result<(), probe_rs::Error> {
let subroutine_size = N as u32;
let high_addr = low_addr + stack_size;
core.write_core_reg(RegisterId(0), low_addr + subroutine_size)?;
core.write_core_reg(RegisterId(1), high_addr)?;
core.write_core_reg(RegisterId(2), CANARY_U32)?;
core.write_8(low_addr as u64, &subroutine)?;
let previous_pc = core.read_core_reg(PC)?;
core.write_core_reg(PC, low_addr)?;
core.run()?;
core.wait_for_core_halted(TIMEOUT)?;
core.write_core_reg::<u32>(PC, previous_pc)?;
Ok(())
}