use std::collections::HashMap;
use crate::callgraph::{CallGraph, FuncId, Sccs};
use crate::ir::{CallTarget, SsaCfg, SsaTerminator, Stmt, VarId};
use crate::smt_explore::{AbiSlot, SinkSpec, SourceSpec, DEFAULT_SINKS, DEFAULT_SOURCES};
#[derive(Debug, Clone)]
pub struct SinkInvocation {
pub sink: SinkSpec,
pub call_site: u64,
pub tainted_caller_slots: Vec<AbiSlot>,
}
#[derive(Debug, Clone)]
pub struct SourceEmission {
pub source: SourceSpec,
pub call_site: u64,
pub tainted_caller_slots: Vec<AbiSlot>,
}
#[derive(Debug, Clone)]
pub struct FunctionSummary {
pub func: FuncId,
pub sinks: Vec<SinkInvocation>,
pub sources: Vec<SourceEmission>,
}
impl FunctionSummary {
pub fn is_leaf(&self) -> bool {
self.sinks.is_empty() && self.sources.is_empty()
}
}
pub fn build_function_summary(
func: FuncId,
ssa: &SsaCfg,
imports: &HashMap<u64, String>,
function_arg_vars: &HashMap<u8, VarId>,
) -> FunctionSummary {
let mut summary = FunctionSummary {
func,
sinks: Vec::new(),
sources: Vec::new(),
};
let regions = crate::region::infer_regions(ssa);
let has_null_terminator_loop = function_has_null_terminator_loop(ssa);
for block in &ssa.blocks {
for stmt in &block.stmts {
if let Stmt::Call { target, args, .. } = stmt {
process_call(
target,
args,
block.addr,
ssa,
imports,
function_arg_vars,
&mut summary,
);
}
if has_null_terminator_loop {
if let Stmt::Store { addr, val } = stmt {
if let Some(slots) = detect_tainted_store(*addr, *val, ssa, ®ions) {
summary.sinks.push(SinkInvocation {
sink: crate::smt_explore::STORE_SINK_SPEC,
call_site: block.addr,
tainted_caller_slots: slots,
});
}
}
}
}
if let SsaTerminator::Call { target, args, .. } = &block.terminator {
process_call(
target,
args,
block.addr,
ssa,
imports,
function_arg_vars,
&mut summary,
);
}
}
summary
}
fn detect_tainted_store(
addr: VarId,
val: VarId,
ssa: &crate::ir::SsaCfg,
regions: &crate::region::RegionMap,
) -> Option<Vec<crate::smt_explore::AbiSlot>> {
use crate::smt_explore::AbiSlot;
let arg_vars = arg_vars_from_ssa(ssa);
let mut dst_slots = arg_slots_for_var(addr, ssa, &arg_vars);
let addr_region = regions.region_of(addr);
if let Some(crate::region::AllocSite::Param(n)) = regions.site_of(addr_region) {
let cs = AbiSlot::Arg(*n);
if !dst_slots.contains(&cs) {
dst_slots.push(cs);
}
}
if dst_slots.is_empty() {
let param_slots = collect_param_bearing_slots(ssa, &arg_vars);
if let Some(slots) = addr_loads_from_param_slot(addr, ssa, ¶m_slots) {
for s in slots {
if !dst_slots.contains(&s) {
dst_slots.push(s);
}
}
}
}
dst_slots.retain(|s| matches!(s, AbiSlot::Arg(_)));
if dst_slots.is_empty() {
return None;
}
let src_slots = find_load_src_param_slots(val, ssa, &arg_vars, regions);
let mut out: Vec<AbiSlot> = src_slots
.into_iter()
.filter(|s| !dst_slots.contains(s))
.collect();
out.dedup();
if !out.is_empty() {
return Some(out);
}
if !val_involves_load(val, ssa, &mut std::collections::HashSet::new(), 0) {
return None;
}
Some(dst_slots)
}
fn find_load_src_param_slots(
val: VarId,
ssa: &crate::ir::SsaCfg,
arg_vars: &HashMap<u8, VarId>,
regions: &crate::region::RegionMap,
) -> Vec<crate::smt_explore::AbiSlot> {
use crate::smt_explore::AbiSlot;
let param_slots = collect_param_bearing_slots(ssa, arg_vars);
let mut out: Vec<AbiSlot> = Vec::new();
let mut visited: std::collections::HashSet<u32> =
std::collections::HashSet::new();
let mut stack = vec![val];
let mut classify_addr = |addr: VarId, sink: &mut Vec<AbiSlot>| {
for cs in arg_slots_for_var(addr, ssa, arg_vars) {
if !sink.contains(&cs) {
sink.push(cs);
}
}
let r = regions.region_of(addr);
if let Some(crate::region::AllocSite::Param(n)) = regions.site_of(r) {
let cs = AbiSlot::Arg(*n);
if !sink.contains(&cs) {
sink.push(cs);
}
}
if let Some(slots) = addr_loads_from_param_slot(addr, ssa, ¶m_slots) {
for s in slots {
if !sink.contains(&s) {
sink.push(s);
}
}
}
};
while let Some(cur) = stack.pop() {
if !visited.insert(cur.0) || visited.len() > 64 {
continue;
}
let Some(def) = ssa.vars.get(cur.0 as usize) else { continue };
match &def.expr {
crate::ir::Expr::Var(inner) => stack.push(*inner),
crate::ir::Expr::Load(addr) => {
classify_addr(*addr, &mut out);
stack.push(*addr);
}
crate::ir::Expr::FieldAccess(base, offset) => {
let key = field_access_canon(*base, *offset, &ssa.vars);
if let Some(slots) = param_slots.get(&key) {
for s in slots {
if !out.contains(s) {
out.push(*s);
}
}
}
classify_addr(*base, &mut out);
stack.push(*base);
}
crate::ir::Expr::UnaryOp(_, a) => stack.push(*a),
crate::ir::Expr::BinOp(_, a, b) => {
stack.push(*a);
stack.push(*b);
}
crate::ir::Expr::Phi(inputs) => {
for v in inputs {
stack.push(*v);
}
}
_ => {}
}
}
out.retain(|s| matches!(s, AbiSlot::Arg(_)));
out
}
fn function_has_null_terminator_loop(ssa: &crate::ir::SsaCfg) -> bool {
use crate::ir::SsaTerminator;
for block in &ssa.blocks {
if let SsaTerminator::CBranch { cond, .. } = &block.terminator {
if cond_is_null_check(*cond, ssa, &mut std::collections::HashSet::new(), 0) {
return true;
}
}
}
false
}
fn cond_is_null_check(
v: VarId,
ssa: &crate::ir::SsaCfg,
visited: &mut std::collections::HashSet<u32>,
depth: u32,
) -> bool {
use crate::ir::{BinOpKind, Expr};
if depth > 16 || !visited.insert(v.0) {
return false;
}
let Some(def) = ssa.vars.get(v.0 as usize) else { return false };
match &def.expr {
Expr::Var(inner) => cond_is_null_check(*inner, ssa, visited, depth + 1),
Expr::UnaryOp(_, a) => cond_is_null_check(*a, ssa, visited, depth + 1),
Expr::BinOp(BinOpKind::Eq | BinOpKind::NotEq, a, b) => {
let traces_load = |v: VarId| -> bool {
let mut vv: std::collections::HashSet<u32> =
std::collections::HashSet::new();
let mut st = vec![v];
while let Some(c) = st.pop() {
if !vv.insert(c.0) || vv.len() > 16 {
continue;
}
let Some(d) = ssa.vars.get(c.0 as usize) else { continue };
match &d.expr {
Expr::Load(_) | Expr::FieldAccess(_, _) => return true,
Expr::Var(inner) => st.push(*inner),
Expr::UnaryOp(_, x) => st.push(*x),
_ => {}
}
}
false
};
let zero_const = |v: VarId| {
matches!(
ssa.vars.get(v.0 as usize).map(|d| &d.expr),
Some(Expr::Const(0, _))
)
};
(traces_load(*a) && zero_const(*b)) || (traces_load(*b) && zero_const(*a))
}
Expr::BinOp(_, a, b) => {
cond_is_null_check(*a, ssa, visited, depth + 1)
|| cond_is_null_check(*b, ssa, visited, depth + 1)
}
_ => false,
}
}
fn store_addr_uses_bounded_index(addr: VarId, ssa: &crate::ir::SsaCfg) -> bool {
use crate::ir::{BinOpKind, Expr};
let mut visited: std::collections::HashSet<u32> =
std::collections::HashSet::new();
let mut stack = vec![addr];
while let Some(cur) = stack.pop() {
if !visited.insert(cur.0) || visited.len() > 32 {
continue;
}
let Some(def) = ssa.vars.get(cur.0 as usize) else { continue };
match &def.expr {
Expr::Phi(inputs) => {
let has_const = inputs.iter().any(|v| {
matches!(
ssa.vars.get(v.0 as usize).map(|d| &d.expr),
Some(Expr::Const(_, _))
)
});
let has_inc = inputs.iter().any(|v| {
let Some(d) = ssa.vars.get(v.0 as usize) else {
return false;
};
if let Expr::BinOp(BinOpKind::Add, a, b) = &d.expr {
let a_const = matches!(
ssa.vars.get(a.0 as usize).map(|d| &d.expr),
Some(Expr::Const(_, _))
);
let b_const = matches!(
ssa.vars.get(b.0 as usize).map(|d| &d.expr),
Some(Expr::Const(_, _))
);
return a_const || b_const;
}
false
});
if has_const && has_inc {
return true;
}
for v in inputs {
stack.push(*v);
}
}
Expr::BinOp(_, a, b) => {
stack.push(*a);
stack.push(*b);
}
Expr::Var(inner) => stack.push(*inner),
Expr::UnaryOp(_, a) => stack.push(*a),
Expr::Load(addr) => stack.push(*addr),
Expr::FieldAccess(base, _) => stack.push(*base),
_ => {}
}
}
false
}
fn collect_param_bearing_slots(
ssa: &crate::ir::SsaCfg,
arg_vars: &HashMap<u8, VarId>,
) -> HashMap<String, Vec<crate::smt_explore::AbiSlot>> {
use crate::smt_explore::AbiSlot;
let mut m: HashMap<String, Vec<AbiSlot>> = HashMap::new();
let arg_set: HashMap<VarId, u8> = arg_vars.iter().map(|(k, v)| (*v, *k)).collect();
for block in &ssa.blocks {
for stmt in &block.stmts {
if let Stmt::Store { addr, val } = stmt {
let Some(key) = addr_canon(*addr, &ssa.vars) else {
continue;
};
let mut visited: std::collections::HashSet<u32> =
std::collections::HashSet::new();
let mut cur = *val;
for _ in 0..16 {
if !visited.insert(cur.0) {
break;
}
if let Some(slot) = arg_set.get(&cur) {
m.entry(key.clone())
.or_default()
.push(AbiSlot::Arg(*slot));
break;
}
let Some(def) = ssa.vars.get(cur.0 as usize) else { break };
match &def.expr {
crate::ir::Expr::Var(inner) => cur = *inner,
_ => break,
}
}
}
}
}
m
}
fn addr_loads_from_param_slot(
addr: VarId,
ssa: &crate::ir::SsaCfg,
param_slots: &HashMap<String, Vec<crate::smt_explore::AbiSlot>>,
) -> Option<Vec<crate::smt_explore::AbiSlot>> {
let mut visited: std::collections::HashSet<u32> =
std::collections::HashSet::new();
let mut stack = vec![addr];
while let Some(cur) = stack.pop() {
if !visited.insert(cur.0) || visited.len() > 32 {
continue;
}
let Some(def) = ssa.vars.get(cur.0 as usize) else { continue };
match &def.expr {
crate::ir::Expr::Var(inner) => stack.push(*inner),
crate::ir::Expr::Load(load_addr) => {
if let Some(key) = addr_canon(*load_addr, &ssa.vars) {
if let Some(slots) = param_slots.get(&key) {
return Some(slots.clone());
}
}
}
crate::ir::Expr::BinOp(_, a, b) => {
stack.push(*a);
stack.push(*b);
}
crate::ir::Expr::UnaryOp(_, a) => stack.push(*a),
crate::ir::Expr::Phi(inputs) => {
for v in inputs {
stack.push(*v);
}
}
_ => {}
}
}
None
}
fn val_involves_load(
v: VarId,
ssa: &crate::ir::SsaCfg,
visited: &mut std::collections::HashSet<u32>,
depth: u32,
) -> bool {
if depth > 16 || !visited.insert(v.0) {
return false;
}
let Some(def) = ssa.vars.get(v.0 as usize) else {
return false;
};
match &def.expr {
crate::ir::Expr::Load(_) => true,
crate::ir::Expr::FieldAccess(_, _) => true,
crate::ir::Expr::Var(inner) => val_involves_load(*inner, ssa, visited, depth + 1),
crate::ir::Expr::UnaryOp(_, a) => val_involves_load(*a, ssa, visited, depth + 1),
crate::ir::Expr::BinOp(_, a, b) => {
val_involves_load(*a, ssa, visited, depth + 1)
|| val_involves_load(*b, ssa, visited, depth + 1)
}
crate::ir::Expr::Phi(inputs) => inputs
.iter()
.any(|x| val_involves_load(*x, ssa, visited, depth + 1)),
_ => false,
}
}
fn process_call(
target: &crate::ir::CallTarget,
args: &[VarId],
call_site: u64,
ssa: &SsaCfg,
imports: &HashMap<u64, String>,
function_arg_vars: &HashMap<u8, VarId>,
summary: &mut FunctionSummary,
) {
let addr = match target {
crate::ir::CallTarget::Direct(a) => *a,
crate::ir::CallTarget::Indirect(_) => return,
};
let Some(raw_name) = imports.get(&addr) else {
return;
};
let name = normalise_libc_name(raw_name);
if let Some(spec) = DEFAULT_SOURCES.iter().find(|s| s.name == name) {
let slot_idx = match spec.tainted {
AbiSlot::Arg(n) => Some(n as usize),
AbiSlot::Ret => None,
AbiSlot::Global(_) => None,
};
let tainted_caller_slots = if let Some(idx) = slot_idx {
args.get(idx)
.map(|v| arg_slots_for_var(*v, ssa, function_arg_vars))
.unwrap_or_default()
} else {
Vec::new()
};
summary.sources.push(SourceEmission {
source: *spec,
call_site,
tainted_caller_slots,
});
return;
}
if let Some(spec) = DEFAULT_SINKS.iter().find(|s| s.name == name) {
let slot_idx = match spec.watched {
AbiSlot::Arg(n) => Some(n as usize),
AbiSlot::Ret => None,
AbiSlot::Global(_) => None,
};
let tainted_caller_slots = if let Some(idx) = slot_idx {
args.get(idx)
.map(|v| arg_slots_for_var(*v, ssa, function_arg_vars))
.unwrap_or_default()
} else {
Vec::new()
};
summary.sinks.push(SinkInvocation {
sink: *spec,
call_site,
tainted_caller_slots,
});
}
}
fn arg_slots_for_var(
var: VarId,
ssa: &SsaCfg,
function_arg_vars: &HashMap<u8, VarId>,
) -> Vec<AbiSlot> {
let mem = build_store_map(ssa);
let mut out = Vec::new();
let mut visited: std::collections::HashSet<u32> =
std::collections::HashSet::new();
let mut stack = vec![var];
while let Some(cur) = stack.pop() {
if !visited.insert(cur.0) || visited.len() > 64 {
continue;
}
for (slot, arg_var) in function_arg_vars {
if *arg_var == cur && !out.contains(&AbiSlot::Arg(*slot)) {
out.push(AbiSlot::Arg(*slot));
}
}
if let Some(def) = ssa.vars.get(cur.0 as usize) {
match &def.expr {
crate::ir::Expr::Var(inner) => stack.push(*inner),
crate::ir::Expr::Const(va, _) if is_global_va(*va) && out.len() < 4 => {
let slot = AbiSlot::Global(*va);
if !out.contains(&slot) {
out.push(slot);
}
}
crate::ir::Expr::Load(addr) => {
stack.push(*addr);
if let Some(key) = addr_canon(*addr, &ssa.vars) {
if let Some(stored) = mem.get(&key).copied() {
stack.push(stored);
}
}
}
crate::ir::Expr::FieldAccess(base, offset) => {
let key = field_access_canon(*base, *offset, &ssa.vars);
if let Some(stored) = mem.get(&key).copied() {
stack.push(stored);
}
stack.push(*base);
}
crate::ir::Expr::BinOp(_op, a, b) => {
stack.push(*a);
stack.push(*b);
}
crate::ir::Expr::Phi(inputs) => {
for v in inputs {
stack.push(*v);
}
}
crate::ir::Expr::UnaryOp(_op, a) => {
stack.push(*a);
}
_ => {}
}
}
}
out
}
fn is_global_va(va: u64) -> bool {
va >= 0x1000 && va < 0xffff_0000_0000_0000
}
fn build_store_map(ssa: &SsaCfg) -> HashMap<String, VarId> {
let mut m: HashMap<String, VarId> = HashMap::new();
for block in &ssa.blocks {
for stmt in &block.stmts {
if let Stmt::Store { addr, val } = stmt {
if let Some(key) = addr_canon(*addr, &ssa.vars) {
m.insert(key, *val);
}
}
}
}
m
}
fn field_access_canon(
base: VarId,
offset: u64,
vars: &[crate::ir::VarDef],
) -> String {
let kb = addr_canon(base, vars).unwrap_or_else(|| "?".to_string());
let kc = format!("C{}.8", offset);
format!("BAdd({},{})", kb, kc)
}
fn addr_canon(var: VarId, vars: &[crate::ir::VarDef]) -> Option<String> {
fn rec(var: VarId, vars: &[crate::ir::VarDef], depth: u32) -> Option<String> {
if depth > 16 {
return None;
}
let def = vars.get(var.0 as usize)?;
Some(match &def.expr {
crate::ir::Expr::Var(inner) => rec(*inner, vars, depth + 1)?,
crate::ir::Expr::Const(c, sz) => format!("C{}.{}", c, sz),
crate::ir::Expr::BinOp(op, a, b) => {
let ka = rec(*a, vars, depth + 1).unwrap_or_else(|| "?".to_string());
let kb = rec(*b, vars, depth + 1).unwrap_or_else(|| "?".to_string());
format!("B{:?}({},{})", op, ka, kb)
}
crate::ir::Expr::UnaryOp(op, a) => {
let ka = rec(*a, vars, depth + 1).unwrap_or_else(|| "?".to_string());
format!("U{:?}({})", op, ka)
}
_ => format!(
"V{:?}/{}/{}",
def.varnode.space, def.varnode.offset, def.varnode.size
),
})
}
rec(var, vars, 0)
}
pub struct FunctionContext<'a> {
pub ssa: &'a SsaCfg,
pub arg_vars: &'a HashMap<u8, VarId>,
}
pub fn build_summaries_bottom_up(
graph: &CallGraph,
sccs: &Sccs,
contexts: &HashMap<FuncId, FunctionContext<'_>>,
imports: &HashMap<u64, String>,
) -> HashMap<FuncId, FunctionSummary> {
let mut summaries: HashMap<FuncId, FunctionSummary> = HashMap::new();
for scc in &sccs.components {
let scc_is_recursive = scc.len() > 1
|| scc
.first()
.map(|fid| sccs.is_recursive(*fid, graph))
.unwrap_or(false);
for &fid in scc {
let Some(ctx) = contexts.get(&fid) else {
continue;
};
let mut s = build_function_summary(fid, ctx.ssa, imports, ctx.arg_vars);
if !scc_is_recursive {
propagate_callee_summaries(ctx, imports, &summaries, &mut s);
}
summaries.insert(fid, s);
}
}
summaries
}
fn propagate_callee_summaries(
ctx: &FunctionContext<'_>,
imports: &HashMap<u64, String>,
summaries: &HashMap<FuncId, FunctionSummary>,
out: &mut FunctionSummary,
) {
for block in &ctx.ssa.blocks {
for stmt in &block.stmts {
if let Stmt::Call { target, args, .. } = stmt {
propagate_one(target, args, block.addr, ctx, imports, summaries, out);
}
}
if let SsaTerminator::Call { target, args, .. } = &block.terminator {
propagate_one(target, args, block.addr, ctx, imports, summaries, out);
}
}
}
fn propagate_one(
target: &CallTarget,
args: &[VarId],
call_site: u64,
ctx: &FunctionContext<'_>,
imports: &HashMap<u64, String>,
summaries: &HashMap<FuncId, FunctionSummary>,
out: &mut FunctionSummary,
) {
let addr = match target {
CallTarget::Direct(a) => *a,
CallTarget::Indirect(_) => return,
};
if imports.contains_key(&addr) {
return; }
let Some(callee_sum) = summaries.get(&FuncId(addr)) else {
return;
};
for sink in &callee_sum.sinks {
let caller_slots = remap_slots(&sink.tainted_caller_slots, args, ctx);
out.sinks.push(SinkInvocation {
sink: sink.sink,
call_site,
tainted_caller_slots: caller_slots,
});
}
for src in &callee_sum.sources {
let caller_slots = remap_slots(&src.tainted_caller_slots, args, ctx);
out.sources.push(SourceEmission {
source: src.source,
call_site,
tainted_caller_slots: caller_slots,
});
}
}
fn remap_slots(
callee_slots: &[AbiSlot],
caller_args: &[VarId],
ctx: &FunctionContext<'_>,
) -> Vec<AbiSlot> {
let mut out = Vec::new();
for slot in callee_slots {
match slot {
AbiSlot::Arg(n) => {
let Some(arg_var) = caller_args.get(*n as usize) else {
continue;
};
for cs in arg_slots_for_var(*arg_var, ctx.ssa, ctx.arg_vars) {
if !out.contains(&cs) {
out.push(cs);
}
}
}
AbiSlot::Global(va) => {
let cs = AbiSlot::Global(*va);
if !out.contains(&cs) {
out.push(cs);
}
}
AbiSlot::Ret => {
}
}
}
out
}
pub fn arg_vars_from_ssa(ssa: &SsaCfg) -> HashMap<u8, VarId> {
let mut out = HashMap::new();
for v in &ssa.vars {
if let Some(name) = &v.param_name {
if let Some(rest) = name.strip_prefix("param_") {
if let Ok(n) = rest.parse::<u8>() {
out.entry(n).or_insert(v.id);
}
}
}
}
out
}
fn normalise_libc_name(raw: &str) -> &str {
let stripped = raw.split('@').next().unwrap_or(raw);
let unprefixed = stripped.trim_start_matches('_');
unprefixed.strip_suffix("_chk").unwrap_or(unprefixed)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ir::{
BlockId, CallTarget, Diagnostic, Expr, InferredType, SsaBlock, SsaTerminator,
VarDef,
};
use pcode_ir::Varnode;
fn mk_var(id: u32, expr: Expr) -> VarDef {
VarDef {
id: VarId(id),
varnode: Varnode::constant(0, 8),
expr,
size: 8,
use_count: 1,
param_name: None,
call_return: false,
inferred_type: InferredType::Unknown,
display_type: None,
}
}
#[test]
fn empty_function_yields_empty_summary() {
let ssa = SsaCfg {
blocks: vec![SsaBlock {
id: BlockId(0),
addr: 0,
stmts: vec![],
terminator: SsaTerminator::Return(None),
}],
vars: vec![mk_var(0, Expr::Const(0, 8))],
entry: BlockId(0),
diagnostics: Vec::<Diagnostic>::new(),
};
let imports: HashMap<u64, String> = HashMap::new();
let arg_vars: HashMap<u8, VarId> = HashMap::new();
let s = build_function_summary(FuncId(0x1000), &ssa, &imports, &arg_vars);
assert!(s.is_leaf());
}
#[test]
fn function_invoking_strcpy_records_sink_with_caller_slot() {
let vars = vec![
mk_var(0, Expr::Const(0, 8)),
mk_var(1, Expr::Const(0, 8)),
];
let ssa = SsaCfg {
blocks: vec![SsaBlock {
id: BlockId(0),
addr: 0x1000,
stmts: vec![Stmt::Call {
target: CallTarget::Direct(0x125d8),
args: vec![VarId(0), VarId(1)],
out: None,
}],
terminator: SsaTerminator::Return(None),
}],
vars,
entry: BlockId(0),
diagnostics: Vec::<Diagnostic>::new(),
};
let mut imports = HashMap::new();
imports.insert(0x125d8u64, "strcpy".to_string());
let mut arg_vars = HashMap::new();
arg_vars.insert(0u8, VarId(0));
arg_vars.insert(1u8, VarId(1));
let s = build_function_summary(FuncId(0x1000), &ssa, &imports, &arg_vars);
assert_eq!(s.sinks.len(), 1);
assert_eq!(s.sinks[0].sink.name, "strcpy");
assert_eq!(s.sinks[0].tainted_caller_slots, vec![AbiSlot::Arg(1)]);
assert!(s.sources.is_empty());
}
#[test]
fn function_invoking_recv_records_source_with_caller_slot() {
let vars = vec![
mk_var(0, Expr::Const(0, 8)), mk_var(1, Expr::Const(0, 8)), mk_var(2, Expr::Const(256, 8)), mk_var(3, Expr::Const(0, 8)), ];
let ssa = SsaCfg {
blocks: vec![SsaBlock {
id: BlockId(0),
addr: 0x1000,
stmts: vec![Stmt::Call {
target: CallTarget::Direct(0x125d8),
args: vec![VarId(1), VarId(0), VarId(2), VarId(3)],
out: None,
}],
terminator: SsaTerminator::Return(None),
}],
vars,
entry: BlockId(0),
diagnostics: Vec::<Diagnostic>::new(),
};
let mut imports = HashMap::new();
imports.insert(0x125d8u64, "recv".to_string());
let mut arg_vars = HashMap::new();
arg_vars.insert(0u8, VarId(0));
let s = build_function_summary(FuncId(0x1000), &ssa, &imports, &arg_vars);
assert_eq!(s.sources.len(), 1);
assert_eq!(s.sources[0].source.name, "recv");
assert_eq!(s.sources[0].tainted_caller_slots, vec![AbiSlot::Arg(0)]);
}
#[test]
fn unrelated_args_yield_no_caller_slots() {
let vars = vec![
mk_var(0, Expr::Const(0x10, 8)),
mk_var(1, Expr::Const(0x20, 8)),
];
let ssa = SsaCfg {
blocks: vec![SsaBlock {
id: BlockId(0),
addr: 0x1000,
stmts: vec![Stmt::Call {
target: CallTarget::Direct(0x125d8),
args: vec![VarId(0), VarId(1)],
out: None,
}],
terminator: SsaTerminator::Return(None),
}],
vars,
entry: BlockId(0),
diagnostics: Vec::<Diagnostic>::new(),
};
let mut imports = HashMap::new();
imports.insert(0x125d8u64, "strcpy".to_string());
let arg_vars: HashMap<u8, VarId> = HashMap::new();
let s = build_function_summary(FuncId(0x1000), &ssa, &imports, &arg_vars);
assert_eq!(s.sinks.len(), 1);
assert!(s.sinks[0].tainted_caller_slots.is_empty());
}
#[test]
fn bottom_up_lifts_callee_sink_into_caller_slot() {
use crate::callgraph::{build_call_graph, tarjan_sccs};
let helper_vars = vec![
mk_var(0, Expr::Const(0, 8)),
mk_var(1, Expr::Const(0, 8)),
];
let helper_ssa = SsaCfg {
blocks: vec![SsaBlock {
id: BlockId(0),
addr: 0x2000,
stmts: vec![Stmt::Call {
target: CallTarget::Direct(0x125d8),
args: vec![VarId(0), VarId(1)],
out: None,
}],
terminator: SsaTerminator::Return(None),
}],
vars: helper_vars,
entry: BlockId(0),
diagnostics: Vec::<Diagnostic>::new(),
};
let mut helper_args = HashMap::new();
helper_args.insert(0u8, VarId(0));
helper_args.insert(1u8, VarId(1));
let outer_vars = vec![
mk_var(0, Expr::Const(0, 8)),
mk_var(1, Expr::Const(0, 8)),
];
let outer_ssa = SsaCfg {
blocks: vec![SsaBlock {
id: BlockId(0),
addr: 0x1000,
stmts: vec![Stmt::Call {
target: CallTarget::Direct(0x2000),
args: vec![VarId(0), VarId(1)],
out: None,
}],
terminator: SsaTerminator::Return(None),
}],
vars: outer_vars,
entry: BlockId(0),
diagnostics: Vec::<Diagnostic>::new(),
};
let mut outer_args = HashMap::new();
outer_args.insert(0u8, VarId(0));
outer_args.insert(1u8, VarId(1));
let mut imports = HashMap::new();
imports.insert(0x125d8u64, "strcpy".to_string());
let funcs: Vec<(FuncId, &SsaCfg)> = vec![
(FuncId(0x2000), &helper_ssa),
(FuncId(0x1000), &outer_ssa),
];
let graph = build_call_graph(&funcs, &imports);
let sccs = tarjan_sccs(&graph);
let mut contexts: HashMap<FuncId, FunctionContext<'_>> = HashMap::new();
contexts.insert(
FuncId(0x2000),
FunctionContext {
ssa: &helper_ssa,
arg_vars: &helper_args,
},
);
contexts.insert(
FuncId(0x1000),
FunctionContext {
ssa: &outer_ssa,
arg_vars: &outer_args,
},
);
let summaries =
build_summaries_bottom_up(&graph, &sccs, &contexts, &imports);
let helper_sum = summaries.get(&FuncId(0x2000)).unwrap();
assert_eq!(helper_sum.sinks.len(), 1);
assert_eq!(
helper_sum.sinks[0].tainted_caller_slots,
vec![AbiSlot::Arg(1)]
);
let outer_sum = summaries.get(&FuncId(0x1000)).unwrap();
assert_eq!(outer_sum.sinks.len(), 1);
assert_eq!(outer_sum.sinks[0].sink.name, "strcpy");
assert_eq!(
outer_sum.sinks[0].tainted_caller_slots,
vec![AbiSlot::Arg(1)]
);
assert_eq!(outer_sum.sinks[0].call_site, 0x1000);
}
#[test]
fn bottom_up_three_hop_wrapping_chain() {
use crate::callgraph::{build_call_graph, tarjan_sccs};
let mk_pass_through = |callee_addr: u64, addr: u64| -> SsaCfg {
SsaCfg {
blocks: vec![SsaBlock {
id: BlockId(0),
addr,
stmts: vec![Stmt::Call {
target: CallTarget::Direct(callee_addr),
args: vec![VarId(0), VarId(1)],
out: None,
}],
terminator: SsaTerminator::Return(None),
}],
vars: vec![mk_var(0, Expr::Const(0, 8)), mk_var(1, Expr::Const(0, 8))],
entry: BlockId(0),
diagnostics: Vec::<Diagnostic>::new(),
}
};
let inner_ssa = SsaCfg {
blocks: vec![SsaBlock {
id: BlockId(0),
addr: 0x3000,
stmts: vec![Stmt::Call {
target: CallTarget::Direct(0x125d8),
args: vec![VarId(0), VarId(1)],
out: None,
}],
terminator: SsaTerminator::Return(None),
}],
vars: vec![mk_var(0, Expr::Const(0, 8)), mk_var(1, Expr::Const(0, 8))],
entry: BlockId(0),
diagnostics: Vec::<Diagnostic>::new(),
};
let mid_ssa = mk_pass_through(0x3000, 0x2000);
let outer_ssa = mk_pass_through(0x2000, 0x1000);
let mut imports = HashMap::new();
imports.insert(0x125d8u64, "strcpy".to_string());
let funcs: Vec<(FuncId, &SsaCfg)> = vec![
(FuncId(0x3000), &inner_ssa),
(FuncId(0x2000), &mid_ssa),
(FuncId(0x1000), &outer_ssa),
];
let graph = build_call_graph(&funcs, &imports);
let sccs = tarjan_sccs(&graph);
let mut arg_vars = HashMap::new();
arg_vars.insert(0u8, VarId(0));
arg_vars.insert(1u8, VarId(1));
let mut contexts: HashMap<FuncId, FunctionContext<'_>> = HashMap::new();
for (fid, ssa) in [
(FuncId(0x3000), &inner_ssa),
(FuncId(0x2000), &mid_ssa),
(FuncId(0x1000), &outer_ssa),
] {
contexts.insert(
fid,
FunctionContext {
ssa,
arg_vars: &arg_vars,
},
);
}
let summaries =
build_summaries_bottom_up(&graph, &sccs, &contexts, &imports);
for (fid, addr) in [
(FuncId(0x3000), 0x3000u64),
(FuncId(0x2000), 0x2000u64),
(FuncId(0x1000), 0x1000u64),
] {
let s = summaries.get(&fid).unwrap();
assert_eq!(s.sinks.len(), 1, "fid {:#x}", fid.0);
assert_eq!(s.sinks[0].sink.name, "strcpy");
assert_eq!(
s.sinks[0].tainted_caller_slots,
vec![AbiSlot::Arg(1)],
"fid {:#x}",
fid.0
);
assert_eq!(s.sinks[0].call_site, addr, "fid {:#x}", fid.0);
}
}
#[test]
fn bottom_up_pass_through_with_slot_swap() {
use crate::callgraph::{build_call_graph, tarjan_sccs};
let helper_ssa = SsaCfg {
blocks: vec![SsaBlock {
id: BlockId(0),
addr: 0x2000,
stmts: vec![Stmt::Call {
target: CallTarget::Direct(0x125d8),
args: vec![VarId(1), VarId(0)], out: None,
}],
terminator: SsaTerminator::Return(None),
}],
vars: vec![mk_var(0, Expr::Const(0, 8)), mk_var(1, Expr::Const(0, 8))],
entry: BlockId(0),
diagnostics: Vec::<Diagnostic>::new(),
};
let outer_ssa = SsaCfg {
blocks: vec![SsaBlock {
id: BlockId(0),
addr: 0x1000,
stmts: vec![Stmt::Call {
target: CallTarget::Direct(0x2000),
args: vec![VarId(0), VarId(1)],
out: None,
}],
terminator: SsaTerminator::Return(None),
}],
vars: vec![mk_var(0, Expr::Const(0, 8)), mk_var(1, Expr::Const(0, 8))],
entry: BlockId(0),
diagnostics: Vec::<Diagnostic>::new(),
};
let mut imports = HashMap::new();
imports.insert(0x125d8u64, "strcpy".to_string());
let funcs: Vec<(FuncId, &SsaCfg)> = vec![
(FuncId(0x2000), &helper_ssa),
(FuncId(0x1000), &outer_ssa),
];
let graph = build_call_graph(&funcs, &imports);
let sccs = tarjan_sccs(&graph);
let mut arg_vars = HashMap::new();
arg_vars.insert(0u8, VarId(0));
arg_vars.insert(1u8, VarId(1));
let mut contexts: HashMap<FuncId, FunctionContext<'_>> = HashMap::new();
contexts.insert(
FuncId(0x2000),
FunctionContext {
ssa: &helper_ssa,
arg_vars: &arg_vars,
},
);
contexts.insert(
FuncId(0x1000),
FunctionContext {
ssa: &outer_ssa,
arg_vars: &arg_vars,
},
);
let summaries =
build_summaries_bottom_up(&graph, &sccs, &contexts, &imports);
let helper_sum = summaries.get(&FuncId(0x2000)).unwrap();
assert_eq!(
helper_sum.sinks[0].tainted_caller_slots,
vec![AbiSlot::Arg(0)]
);
let outer_sum = summaries.get(&FuncId(0x1000)).unwrap();
assert_eq!(outer_sum.sinks.len(), 1);
assert_eq!(
outer_sum.sinks[0].tainted_caller_slots,
vec![AbiSlot::Arg(0)]
);
}
#[test]
fn bottom_up_recursive_scc_is_opaque() {
use crate::callgraph::{build_call_graph, tarjan_sccs};
let make_caller_ssa = |callee_addr: u64| SsaCfg {
blocks: vec![SsaBlock {
id: BlockId(0),
addr: 0,
stmts: vec![Stmt::Call {
target: CallTarget::Direct(callee_addr),
args: vec![],
out: None,
}],
terminator: SsaTerminator::Return(None),
}],
vars: vec![mk_var(0, Expr::Const(0, 8))],
entry: BlockId(0),
diagnostics: Vec::<Diagnostic>::new(),
};
let a_ssa = make_caller_ssa(0xB000);
let b_ssa = make_caller_ssa(0xA000);
let imports: HashMap<u64, String> = HashMap::new();
let arg_vars: HashMap<u8, VarId> = HashMap::new();
let funcs: Vec<(FuncId, &SsaCfg)> =
vec![(FuncId(0xA000), &a_ssa), (FuncId(0xB000), &b_ssa)];
let graph = build_call_graph(&funcs, &imports);
let sccs = tarjan_sccs(&graph);
assert!(sccs.is_recursive(FuncId(0xA000), &graph));
let mut contexts: HashMap<FuncId, FunctionContext<'_>> = HashMap::new();
contexts.insert(
FuncId(0xA000),
FunctionContext {
ssa: &a_ssa,
arg_vars: &arg_vars,
},
);
contexts.insert(
FuncId(0xB000),
FunctionContext {
ssa: &b_ssa,
arg_vars: &arg_vars,
},
);
let summaries =
build_summaries_bottom_up(&graph, &sccs, &contexts, &imports);
assert!(summaries.get(&FuncId(0xA000)).unwrap().is_leaf());
assert!(summaries.get(&FuncId(0xB000)).unwrap().is_leaf());
}
#[test]
fn libc_name_normalisation_matches_macho_underscore() {
let vars = vec![mk_var(0, Expr::Const(0, 8))];
let ssa = SsaCfg {
blocks: vec![SsaBlock {
id: BlockId(0),
addr: 0x1000,
stmts: vec![Stmt::Call {
target: CallTarget::Direct(0x125d8),
args: vec![VarId(0), VarId(0)],
out: None,
}],
terminator: SsaTerminator::Return(None),
}],
vars,
entry: BlockId(0),
diagnostics: Vec::<Diagnostic>::new(),
};
let mut imports = HashMap::new();
imports.insert(0x125d8u64, "_strcpy".to_string());
let arg_vars: HashMap<u8, VarId> = HashMap::new();
let s = build_function_summary(FuncId(0x1000), &ssa, &imports, &arg_vars);
assert_eq!(s.sinks.len(), 1);
assert_eq!(s.sinks[0].sink.name, "strcpy");
}
}