use std::collections::HashMap;
use crate::ir::{CallTarget, SsaCfg, SsaTerminator, Stmt, VarId};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AbiSlot {
Arg(u8),
Ret,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SinkKind {
StackBuffer,
FormatArg,
Command,
LengthArg,
}
#[derive(Debug, Clone, Copy)]
pub struct SourceSpec {
pub name: &'static str,
pub tainted: AbiSlot,
}
#[derive(Debug, Clone, Copy)]
pub struct SinkSpec {
pub name: &'static str,
pub watched: AbiSlot,
pub kind: SinkKind,
}
pub const DEFAULT_SOURCES: &[SourceSpec] = &[
SourceSpec { name: "recv", tainted: AbiSlot::Arg(1) },
SourceSpec { name: "recvfrom", tainted: AbiSlot::Arg(1) },
SourceSpec { name: "recvmsg", tainted: AbiSlot::Arg(1) },
SourceSpec { name: "read", tainted: AbiSlot::Arg(1) },
SourceSpec { name: "fread", tainted: AbiSlot::Arg(0) },
SourceSpec { name: "fgets", tainted: AbiSlot::Arg(0) },
SourceSpec { name: "gets", tainted: AbiSlot::Arg(0) },
SourceSpec { name: "scanf", tainted: AbiSlot::Arg(1) },
SourceSpec { name: "sscanf", tainted: AbiSlot::Arg(2) },
SourceSpec { name: "fscanf", tainted: AbiSlot::Arg(2) },
SourceSpec { name: "getenv", tainted: AbiSlot::Ret },
SourceSpec { name: "argv", tainted: AbiSlot::Arg(1) },
];
pub const DEFAULT_SINKS: &[SinkSpec] = &[
SinkSpec { name: "strcpy", watched: AbiSlot::Arg(1), kind: SinkKind::StackBuffer },
SinkSpec { name: "strcat", watched: AbiSlot::Arg(1), kind: SinkKind::StackBuffer },
SinkSpec { name: "sprintf", watched: AbiSlot::Arg(1), kind: SinkKind::FormatArg },
SinkSpec { name: "vsprintf",watched: AbiSlot::Arg(1), kind: SinkKind::FormatArg },
SinkSpec { name: "printf", watched: AbiSlot::Arg(0), kind: SinkKind::FormatArg },
SinkSpec { name: "fprintf", watched: AbiSlot::Arg(1), kind: SinkKind::FormatArg },
SinkSpec { name: "memcpy", watched: AbiSlot::Arg(2), kind: SinkKind::LengthArg },
SinkSpec { name: "memmove", watched: AbiSlot::Arg(2), kind: SinkKind::LengthArg },
SinkSpec { name: "strncpy", watched: AbiSlot::Arg(2), kind: SinkKind::LengthArg },
SinkSpec { name: "strncat", watched: AbiSlot::Arg(2), kind: SinkKind::LengthArg },
SinkSpec { name: "system", watched: AbiSlot::Arg(0), kind: SinkKind::Command },
SinkSpec { name: "popen", watched: AbiSlot::Arg(0), kind: SinkKind::Command },
SinkSpec { name: "execve", watched: AbiSlot::Arg(0), kind: SinkKind::Command },
SinkSpec { name: "execlp", watched: AbiSlot::Arg(0), kind: SinkKind::Command },
SinkSpec { name: "execvp", watched: AbiSlot::Arg(0), kind: SinkKind::Command },
];
pub fn resolve_call(
target_addr: u64,
imports: &HashMap<u64, String>,
) -> Option<SpecRef> {
let raw = imports.get(&target_addr)?;
let normalised = normalise_name(raw);
if let Some(spec) = DEFAULT_SOURCES.iter().find(|s| s.name == normalised) {
return Some(SpecRef::Source(*spec));
}
if let Some(spec) = DEFAULT_SINKS.iter().find(|s| s.name == normalised) {
return Some(SpecRef::Sink(*spec));
}
None
}
#[derive(Debug, Clone, Copy)]
pub enum SpecRef {
Source(SourceSpec),
Sink(SinkSpec),
}
fn normalise_name(raw: &str) -> &str {
let stripped = raw.split('@').next().unwrap_or(raw);
stripped
.trim_start_matches('_')
.trim_start_matches('_')
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SmtFinding {
Reachable { input_bytes: Vec<(usize, u8)> },
NotReachable,
Unsupported(&'static str),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PathRejection {
UnsupportedTerminator(&'static str),
PhiInPath,
IndirectCall,
NoSinkFound,
}
#[derive(Debug, Clone)]
pub struct TaintEvent<'a> {
pub stmt_index: usize,
pub kind: TaintEventKind<'a>,
}
#[derive(Debug, Clone)]
pub enum TaintEventKind<'a> {
Assign(VarId),
Store { addr: VarId, val: VarId },
SourceCall {
spec: &'a SourceSpec,
args: Vec<VarId>,
out: Option<VarId>,
},
SinkCall {
spec: &'a SinkSpec,
args: Vec<VarId>,
out: Option<VarId>,
},
OtherCall {
target_addr: Option<u64>,
args: Vec<VarId>,
out: Option<VarId>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BranchDecision {
pub block_addr: u64,
pub cond: VarId,
pub taken: bool,
}
#[derive(Debug, Clone)]
pub struct TaintPath<'a> {
pub source: &'a SourceSpec,
pub source_event: usize,
pub sink: &'a SinkSpec,
pub sink_event: usize,
pub events: Vec<TaintEvent<'a>>,
pub branch_decisions: Vec<BranchDecision>,
}
pub const MAX_BRANCH_DEPTH: u32 = 4;
struct WalkState<'a> {
current: crate::ir::BlockId,
events: Vec<TaintEvent<'a>>,
visited: std::collections::HashSet<crate::ir::BlockId>,
branch_decisions: Vec<BranchDecision>,
}
pub fn collect_paths<'a>(
ssa: &'a SsaCfg,
imports: &HashMap<u64, String>,
) -> Result<Vec<TaintPath<'a>>, PathRejection> {
let initial = WalkState {
current: ssa.entry,
events: Vec::new(),
visited: std::collections::HashSet::new(),
branch_decisions: Vec::new(),
};
let mut worklist: Vec<WalkState<'a>> = vec![initial];
let mut completed: Vec<WalkState<'a>> = Vec::new();
let mut last_reject: Option<PathRejection> = None;
while let Some(mut state) = worklist.pop() {
if state.branch_decisions.len() as u32 > MAX_BRANCH_DEPTH {
last_reject = Some(PathRejection::UnsupportedTerminator("depth limit"));
continue;
}
let mut keep_walking = true;
while keep_walking {
if !state.visited.insert(state.current) {
last_reject = Some(PathRejection::UnsupportedTerminator("loop back-edge"));
keep_walking = false;
break;
}
let block = match ssa.blocks.iter().find(|b| b.id == state.current) {
Some(b) => b,
None => {
last_reject =
Some(PathRejection::UnsupportedTerminator("dangling block id"));
keep_walking = false;
break;
}
};
let mut phi_or_indirect = false;
for (idx, stmt) in block.stmts.iter().enumerate() {
match stmt {
Stmt::Assign(v) => {
if matches!(
ssa.vars.get(v.0 as usize).map(|d| &d.expr),
Some(crate::ir::Expr::Phi(_))
) {
continue;
}
state.events.push(TaintEvent {
stmt_index: idx,
kind: TaintEventKind::Assign(*v),
});
}
Stmt::Store { addr, val } => {
state.events.push(TaintEvent {
stmt_index: idx,
kind: TaintEventKind::Store {
addr: *addr,
val: *val,
},
});
}
Stmt::Call { target, args, out } => {
match classify_call(idx, target, args, *out, imports) {
Ok(ev) => state.events.push(ev),
Err(e) => {
last_reject = Some(e);
phi_or_indirect = true;
break;
}
}
}
}
}
if phi_or_indirect {
keep_walking = false;
break;
}
let term_idx = block.stmts.len();
match &block.terminator {
SsaTerminator::Call {
target,
args,
out,
fallthrough,
} => match classify_call(term_idx, target, args, *out, imports) {
Ok(ev) => {
state.events.push(ev);
state.current = *fallthrough;
}
Err(e) => {
last_reject = Some(e);
keep_walking = false;
}
},
SsaTerminator::Fallthrough(next) => {
state.current = *next;
}
SsaTerminator::Return(_) => {
completed.push(state);
keep_walking = false;
break;
}
SsaTerminator::Branch(next) => {
state.current = *next;
}
SsaTerminator::CBranch {
cond,
taken,
fallthrough,
} => {
if (state.branch_decisions.len() as u32) >= MAX_BRANCH_DEPTH {
last_reject =
Some(PathRejection::UnsupportedTerminator("depth limit"));
keep_walking = false;
break;
}
let block_addr = block.addr;
let mut alt = WalkState {
current: *fallthrough,
events: state.events.clone(),
visited: state.visited.clone(),
branch_decisions: state.branch_decisions.clone(),
};
alt.branch_decisions.push(BranchDecision {
block_addr,
cond: *cond,
taken: false,
});
worklist.push(alt);
state.current = *taken;
state.branch_decisions.push(BranchDecision {
block_addr,
cond: *cond,
taken: true,
});
}
SsaTerminator::Indirect(_) => {
last_reject =
Some(PathRejection::UnsupportedTerminator("Indirect"));
keep_walking = false;
break;
}
}
}
}
let mut paths = Vec::new();
for state in completed {
let mut last_source: Option<(usize, &'a SourceSpec)> = None;
for (i, ev) in state.events.iter().enumerate() {
match &ev.kind {
TaintEventKind::SourceCall { spec, .. } => {
last_source = Some((i, spec));
}
TaintEventKind::SinkCall { spec, .. } => {
if let Some((src_i, src_spec)) = last_source.take() {
paths.push(TaintPath {
source: src_spec,
source_event: src_i,
sink: spec,
sink_event: i,
events: state.events.clone(),
branch_decisions: state.branch_decisions.clone(),
});
}
}
_ => {}
}
}
}
if paths.is_empty() {
return Err(last_reject.unwrap_or(PathRejection::NoSinkFound));
}
Ok(paths)
}
fn classify_call<'a>(
stmt_index: usize,
target: &CallTarget,
args: &[VarId],
out: Option<VarId>,
imports: &HashMap<u64, String>,
) -> Result<TaintEvent<'a>, PathRejection> {
let direct_addr = match target {
CallTarget::Direct(a) => Some(*a),
CallTarget::Indirect(_) => None,
};
let kind = match direct_addr.and_then(|a| resolve_call(a, imports)) {
Some(SpecRef::Source(s)) => {
let spec = DEFAULT_SOURCES
.iter()
.find(|sp| sp.name == s.name)
.expect("resolve_call returned a SourceSpec not in DEFAULT_SOURCES");
TaintEventKind::SourceCall { spec, args: args.to_vec(), out }
}
Some(SpecRef::Sink(s)) => {
let spec = DEFAULT_SINKS
.iter()
.find(|sp| sp.name == s.name)
.expect("resolve_call returned a SinkSpec not in DEFAULT_SINKS");
TaintEventKind::SinkCall { spec, args: args.to_vec(), out }
}
None => {
if direct_addr.is_none() {
return Err(PathRejection::IndirectCall);
}
TaintEventKind::OtherCall {
target_addr: direct_addr,
args: args.to_vec(),
out,
}
}
};
Ok(TaintEvent { stmt_index, kind })
}
type MemMap = HashMap<pcode_ir::Varnode, VarId>;
fn build_mem_map(events: &[TaintEvent<'_>], vars: &[crate::ir::VarDef]) -> MemMap {
let mut m = MemMap::new();
for ev in events {
if let TaintEventKind::Store { addr, val } = ev.kind {
if let Some(addr_vn) = vars.get(addr.0 as usize).map(|d| d.varnode) {
m.insert(addr_vn, val);
}
}
}
m
}
fn varid_lineage_eq(
a: VarId,
b: VarId,
vars: &[crate::ir::VarDef],
mem: &MemMap,
) -> bool {
if a == b {
return true;
}
let chain_a = chain_varnodes(a, vars, mem);
let chain_b = chain_varnodes(b, vars, mem);
for vn_a in &chain_a {
if chain_b.iter().any(|vn_b| vn_a == vn_b) {
return true;
}
}
false
}
fn chain_varnodes(
start: VarId,
vars: &[crate::ir::VarDef],
mem: &MemMap,
) -> Vec<pcode_ir::Varnode> {
let mut out = Vec::new();
let mut visited: std::collections::HashSet<u32> = std::collections::HashSet::new();
let mut stack = vec![start];
while let Some(current) = stack.pop() {
if !visited.insert(current.0) {
continue;
}
if visited.len() > 64 {
break;
}
let Some(def) = vars.get(current.0 as usize) else {
continue;
};
out.push(def.varnode);
match &def.expr {
crate::ir::Expr::Var(inner) => stack.push(*inner),
crate::ir::Expr::Load(addr) => {
if let Some(addr_vn) = vars.get(addr.0 as usize).map(|d| d.varnode) {
if let Some(stored) = mem.get(&addr_vn).copied() {
stack.push(stored);
}
}
}
_ => {}
}
}
out
}
#[cfg(feature = "smt")]
pub fn solve(path: &TaintPath, ssa: &crate::ir::SsaCfg) -> SmtFinding {
use z3::ast::{Ast, BV};
let source_event = &path.events[path.source_event];
let sink_event = &path.events[path.sink_event];
let source_var = match (&source_event.kind, path.source.tainted) {
(TaintEventKind::SourceCall { args, .. }, AbiSlot::Arg(n)) => {
args.get(n as usize).copied()
}
(TaintEventKind::SourceCall { out, .. }, AbiSlot::Ret) => *out,
_ => None,
};
let sink_var = match (&sink_event.kind, path.sink.watched) {
(TaintEventKind::SinkCall { args, .. }, AbiSlot::Arg(n)) => {
args.get(n as usize).copied()
}
(TaintEventKind::SinkCall { out, .. }, AbiSlot::Ret) => *out,
_ => None,
};
let (Some(src), Some(snk)) = (source_var, sink_var) else {
return SmtFinding::Unsupported("source/sink slot missing");
};
let mem = build_mem_map(&path.events, &ssa.vars);
if !varid_lineage_eq(snk, src, &ssa.vars, &mem) {
return SmtFinding::NotReachable;
}
let z3_cfg = z3::Config::new();
let ctx = z3::Context::new(&z3_cfg);
let solver = z3::Solver::new(&ctx);
const INPUT_LEN: usize = 32;
let bytes: Vec<BV> = (0..INPUT_LEN)
.map(|i| BV::new_const(&ctx, format!("in_{i}"), 8))
.collect();
match path.sink.kind {
SinkKind::Command => {
let mut acc = z3::ast::Bool::from_bool(&ctx, false);
for b in &bytes {
let semi = b._eq(&BV::from_u64(&ctx, b';' as u64, 8));
let amp = b._eq(&BV::from_u64(&ctx, b'&' as u64, 8));
let pipe = b._eq(&BV::from_u64(&ctx, b'|' as u64, 8));
let any = z3::ast::Bool::or(&ctx, &[&semi, &, &pipe]);
acc = z3::ast::Bool::or(&ctx, &[&acc, &any]);
}
solver.assert(&acc);
}
SinkKind::FormatArg => {
let mut acc = z3::ast::Bool::from_bool(&ctx, false);
for b in &bytes {
let pct = b._eq(&BV::from_u64(&ctx, b'%' as u64, 8));
acc = z3::ast::Bool::or(&ctx, &[&acc, &pct]);
}
solver.assert(&acc);
}
SinkKind::StackBuffer => {
for b in &bytes {
let nz = b._eq(&BV::from_u64(&ctx, 0, 8)).not();
solver.assert(&nz);
}
}
SinkKind::LengthArg => {
return SmtFinding::Unsupported("LengthArg sink not modeled in v0");
}
}
match solver.check() {
z3::SatResult::Sat => {
let m = match solver.get_model() {
Some(m) => m,
None => return SmtFinding::Unsupported("SAT but no model returned"),
};
let mut input_bytes = Vec::new();
for (i, b) in bytes.iter().enumerate() {
let evaluated = z3::Model::eval(&m, b, true);
if let Some(v_bv) = evaluated {
if let Some(v) = v_bv.as_u64() {
input_bytes.push((i, v as u8));
}
}
}
SmtFinding::Reachable { input_bytes }
}
z3::SatResult::Unsat => SmtFinding::NotReachable,
z3::SatResult::Unknown => SmtFinding::Unsupported("solver Unknown / timeout"),
}
}
#[cfg(not(feature = "smt"))]
pub fn solve(_path: &TaintPath, _ssa: &crate::ir::SsaCfg) -> SmtFinding {
SmtFinding::Unsupported("smt feature not enabled at build time")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_tables_non_empty() {
assert!(!DEFAULT_SOURCES.is_empty());
assert!(!DEFAULT_SINKS.is_empty());
}
#[test]
fn covers_canonical_apis() {
let src_names: Vec<_> = DEFAULT_SOURCES.iter().map(|s| s.name).collect();
for must in &["recv", "read", "fgets", "scanf", "argv"] {
assert!(src_names.contains(must), "missing source `{must}`");
}
let sink_names: Vec<_> = DEFAULT_SINKS.iter().map(|s| s.name).collect();
for must in &["strcpy", "sprintf", "memcpy", "system", "popen", "execve"] {
assert!(sink_names.contains(must), "missing sink `{must}`");
}
}
#[test]
fn argument_slots_match_real_abi() {
let recv = DEFAULT_SOURCES.iter().find(|s| s.name == "recv").unwrap();
assert_eq!(recv.tainted, AbiSlot::Arg(1));
let gets = DEFAULT_SOURCES.iter().find(|s| s.name == "gets").unwrap();
assert_eq!(gets.tainted, AbiSlot::Arg(0));
let memcpy = DEFAULT_SINKS.iter().find(|s| s.name == "memcpy").unwrap();
assert_eq!(memcpy.watched, AbiSlot::Arg(2));
assert_eq!(memcpy.kind, SinkKind::LengthArg);
let system = DEFAULT_SINKS.iter().find(|s| s.name == "system").unwrap();
assert_eq!(system.watched, AbiSlot::Arg(0));
assert_eq!(system.kind, SinkKind::Command);
}
#[test]
fn resolves_plain_libc_name() {
let mut imports = HashMap::new();
imports.insert(0x1000, "recv".to_string());
let r = resolve_call(0x1000, &imports).expect("recv resolved");
match r {
SpecRef::Source(s) => assert_eq!(s.name, "recv"),
_ => panic!("expected source"),
}
}
#[test]
fn strips_plt_suffix() {
let mut imports = HashMap::new();
imports.insert(0x2000, "strcpy@plt".to_string());
let r = resolve_call(0x2000, &imports).expect("strcpy@plt resolved");
match r {
SpecRef::Sink(s) => assert_eq!(s.name, "strcpy"),
_ => panic!("expected sink"),
}
}
#[test]
fn strips_macho_underscore() {
let mut imports = HashMap::new();
imports.insert(0x3000, "_system".to_string());
let r = resolve_call(0x3000, &imports).expect("_system resolved");
match r {
SpecRef::Sink(s) => assert_eq!(s.name, "system"),
_ => panic!("expected sink"),
}
}
#[test]
fn strips_versioned_suffix() {
let mut imports = HashMap::new();
imports.insert(0x4000, "memcpy@@GLIBC_2.14".to_string());
let r = resolve_call(0x4000, &imports).expect("versioned memcpy");
match r {
SpecRef::Sink(s) => {
assert_eq!(s.name, "memcpy");
assert_eq!(s.kind, SinkKind::LengthArg);
}
_ => panic!("expected sink"),
}
}
#[test]
fn unknown_name_is_none() {
let mut imports = HashMap::new();
imports.insert(0x5000, "fancy_app_helper".to_string());
assert!(resolve_call(0x5000, &imports).is_none());
}
#[test]
fn missing_addr_is_none() {
let imports: HashMap<u64, String> = HashMap::new();
assert!(resolve_call(0xdead_beef, &imports).is_none());
}
use crate::ir::{
BlockId, Diagnostic, Expr, InferredType, SsaBlock, SsaCfg, SsaTerminator,
Stmt, 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,
}
}
fn block_with_term(stmts: Vec<Stmt>, term: SsaTerminator) -> SsaBlock {
SsaBlock {
id: BlockId(0),
addr: 0,
stmts,
terminator: term,
}
}
fn cfg(vars: Vec<VarDef>, block: SsaBlock) -> SsaCfg {
SsaCfg {
blocks: vec![block],
vars,
entry: BlockId(0),
diagnostics: Vec::<Diagnostic>::new(),
}
}
fn imports_with(entries: &[(u64, &str)]) -> HashMap<u64, String> {
entries
.iter()
.map(|(a, n)| (*a, n.to_string()))
.collect()
}
#[test]
fn accepts_recv_then_strcpy_in_same_block() {
let vars = vec![
mk_var(0, Expr::Const(0, 8)), mk_var(1, Expr::Const(0x4000, 8)), mk_var(2, Expr::Const(0x100, 8)), mk_var(3, Expr::Const(0, 8)), mk_var(4, Expr::Const(0x5000, 8)), ];
let stmts = vec![
Stmt::Call {
target: CallTarget::Direct(0x1000),
args: vec![VarId(0), VarId(1), VarId(2), VarId(3)],
out: None,
},
Stmt::Call {
target: CallTarget::Direct(0x2000),
args: vec![VarId(4), VarId(1)],
out: None,
},
];
let block = block_with_term(stmts, SsaTerminator::Return(None));
let ssa = cfg(vars, block);
let imports = imports_with(&[(0x1000, "recv"), (0x2000, "strcpy")]);
let paths = collect_paths(&ssa, &imports).expect("should accept");
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].source.name, "recv");
assert_eq!(paths[0].sink.name, "strcpy");
assert!(paths[0].source_event < paths[0].sink_event);
}
#[test]
fn cbranch_with_no_arm_blocks_falls_through_to_dangling() {
let vars = vec![mk_var(0, Expr::Const(0, 1))];
let block = block_with_term(
vec![],
SsaTerminator::CBranch {
cond: VarId(0),
taken: BlockId(1),
fallthrough: BlockId(2),
},
);
let ssa = cfg(vars, block);
let imports: HashMap<u64, String> = HashMap::new();
match collect_paths(&ssa, &imports) {
Err(PathRejection::UnsupportedTerminator(reason)) => {
assert!(
reason == "dangling block id" || reason == "Branch",
"unexpected rejection reason: {reason}"
);
}
Err(PathRejection::NoSinkFound) => {}
other => panic!("expected dangling/NoSinkFound, got {other:?}"),
}
}
#[test]
fn cbranch_explores_both_arms_for_source_sink_pair() {
let vars = vec![
mk_var(0, Expr::Const(0, 1)), mk_var(1, Expr::Const(0, 8)), mk_var(2, Expr::Const(0x4000, 8)), mk_var(3, Expr::Const(0x100, 8)),
mk_var(4, Expr::Const(0, 8)),
mk_var(5, Expr::Const(0x5000, 8)), ];
let block0 = SsaBlock {
id: BlockId(0),
addr: 0x1000,
stmts: vec![Stmt::Call {
target: CallTarget::Direct(0x10),
args: vec![VarId(1), VarId(2), VarId(3), VarId(4)],
out: None,
}],
terminator: SsaTerminator::CBranch {
cond: VarId(0),
taken: BlockId(1),
fallthrough: BlockId(2),
},
};
let block1 = SsaBlock {
id: BlockId(1),
addr: 0x1010,
stmts: vec![],
terminator: SsaTerminator::Call {
target: CallTarget::Direct(0x20),
args: vec![VarId(5), VarId(2)],
out: None,
fallthrough: BlockId(3),
},
};
let block2 = SsaBlock {
id: BlockId(2),
addr: 0x1020,
stmts: vec![],
terminator: SsaTerminator::Return(None),
};
let block3 = SsaBlock {
id: BlockId(3),
addr: 0x1030,
stmts: vec![],
terminator: SsaTerminator::Return(None),
};
let ssa = SsaCfg {
blocks: vec![block0, block1, block2, block3],
vars,
entry: BlockId(0),
diagnostics: Vec::<Diagnostic>::new(),
};
let imports = imports_with(&[(0x10, "recv"), (0x20, "strcpy")]);
let paths =
collect_paths(&ssa, &imports).expect("v1 should explore CBranch arms");
assert_eq!(paths.len(), 1, "expected single recv→strcpy path, got {}", paths.len());
assert_eq!(paths[0].source.name, "recv");
assert_eq!(paths[0].sink.name, "strcpy");
assert_eq!(paths[0].branch_decisions.len(), 1);
assert_eq!(paths[0].branch_decisions[0].block_addr, 0x1000);
assert!(paths[0].branch_decisions[0].taken, "should have taken the sink-bearing arm");
}
#[test]
fn cbranch_depth_limit_caps_walks() {
let mut vars = Vec::new();
let mut blocks = Vec::new();
let depth = (MAX_BRANCH_DEPTH + 2) as usize;
vars.push(mk_var(0, Expr::Const(0, 1))); for i in 0..depth {
blocks.push(SsaBlock {
id: BlockId(i),
addr: 0x1000 + i as u64 * 0x10,
stmts: vec![],
terminator: SsaTerminator::CBranch {
cond: VarId(0),
taken: BlockId(i + 1),
fallthrough: BlockId(depth + 1),
},
});
}
blocks.push(SsaBlock {
id: BlockId(depth),
addr: 0x2000,
stmts: vec![],
terminator: SsaTerminator::Return(None),
});
blocks.push(SsaBlock {
id: BlockId(depth + 1),
addr: 0x2010,
stmts: vec![],
terminator: SsaTerminator::Return(None),
});
let ssa = SsaCfg {
blocks,
vars,
entry: BlockId(0),
diagnostics: Vec::<Diagnostic>::new(),
};
let imports: HashMap<u64, String> = HashMap::new();
let result = collect_paths(&ssa, &imports);
match result {
Err(PathRejection::UnsupportedTerminator("depth limit"))
| Err(PathRejection::NoSinkFound)
| Err(PathRejection::UnsupportedTerminator("Branch")) => {}
other => panic!("expected depth-limit/NoSink rejection, got {other:?}"),
}
}
#[test]
fn phi_assignment_is_skipped_not_rejected() {
let vars = vec![
mk_var(0, Expr::Const(0, 8)),
mk_var(1, Expr::Const(0, 8)),
mk_var(2, Expr::Phi(vec![VarId(0), VarId(1)])),
];
let block = block_with_term(
vec![Stmt::Assign(VarId(2))],
SsaTerminator::Return(None),
);
let ssa = cfg(vars, block);
let imports: HashMap<u64, String> = HashMap::new();
match collect_paths(&ssa, &imports) {
Err(PathRejection::NoSinkFound) => {}
other => panic!("expected NoSinkFound (Phi skipped), got {other:?}"),
}
}
#[test]
fn rejects_indirect_call() {
let vars = vec![mk_var(0, Expr::Const(0, 8))];
let block = block_with_term(
vec![Stmt::Call {
target: CallTarget::Indirect(Varnode::constant(0, 8)),
args: vec![],
out: None,
}],
SsaTerminator::Return(None),
);
let ssa = cfg(vars, block);
let imports: HashMap<u64, String> = HashMap::new();
assert_eq!(collect_paths(&ssa, &imports).unwrap_err(), PathRejection::IndirectCall);
}
#[test]
fn no_sink_found() {
let vars = vec![mk_var(0, Expr::Const(0, 8))];
let block = block_with_term(
vec![Stmt::Call {
target: CallTarget::Direct(0x1000),
args: vec![],
out: None,
}],
SsaTerminator::Return(None),
);
let ssa = cfg(vars, block);
let imports = imports_with(&[(0x1000, "recv")]);
assert_eq!(collect_paths(&ssa, &imports).unwrap_err(), PathRejection::NoSinkFound);
}
#[test]
fn source_after_sink_yields_no_path() {
let vars = vec![mk_var(0, Expr::Const(0, 8))];
let block = block_with_term(
vec![
Stmt::Call {
target: CallTarget::Direct(0x2000),
args: vec![],
out: None,
},
Stmt::Call {
target: CallTarget::Direct(0x1000),
args: vec![],
out: None,
},
],
SsaTerminator::Return(None),
);
let ssa = cfg(vars, block);
let imports = imports_with(&[(0x1000, "recv"), (0x2000, "strcpy")]);
assert_eq!(collect_paths(&ssa, &imports).unwrap_err(), PathRejection::NoSinkFound);
}
#[cfg(feature = "smt")]
fn one_call_pair_cfg(
source_addr: u64, source_args: Vec<VarId>,
sink_addr: u64, sink_args: Vec<VarId>,
vars: Vec<VarDef>,
) -> SsaCfg {
let stmts = vec![
Stmt::Call {
target: CallTarget::Direct(source_addr),
args: source_args,
out: None,
},
Stmt::Call {
target: CallTarget::Direct(sink_addr),
args: sink_args,
out: None,
},
];
cfg(vars, block_with_term(stmts, SsaTerminator::Return(None)))
}
#[cfg(feature = "smt")]
#[test]
fn sat_recv_to_strcpy_is_reachable() {
let vars = vec![
mk_var(0, Expr::Const(0, 8)), mk_var(1, Expr::Const(0x4000, 8)), mk_var(2, Expr::Const(0x100, 8)),
mk_var(3, Expr::Const(0, 8)),
mk_var(4, Expr::Const(0x5000, 8)), ];
let ssa = one_call_pair_cfg(
0x1000, vec![VarId(0), VarId(1), VarId(2), VarId(3)],
0x2000, vec![VarId(4), VarId(1)],
vars,
);
let imports = imports_with(&[(0x1000, "recv"), (0x2000, "strcpy")]);
let paths = collect_paths(&ssa, &imports).expect("v0 path collection");
match solve(&paths[0], &ssa) {
SmtFinding::Reachable { input_bytes } => {
assert_eq!(input_bytes.len(), 32);
assert!(input_bytes.iter().all(|(_, b)| *b != 0));
}
other => panic!("expected Reachable, got {other:?}"),
}
}
#[cfg(feature = "smt")]
#[test]
fn sat_recv_to_printf_is_reachable() {
let vars = vec![
mk_var(0, Expr::Const(0, 8)),
mk_var(1, Expr::Const(0x4000, 8)),
mk_var(2, Expr::Const(0x100, 8)),
mk_var(3, Expr::Const(0, 8)),
];
let ssa = one_call_pair_cfg(
0x1000, vec![VarId(0), VarId(1), VarId(2), VarId(3)],
0x2000, vec![VarId(1)],
vars,
);
let imports = imports_with(&[(0x1000, "recv"), (0x2000, "printf")]);
let paths = collect_paths(&ssa, &imports).expect("v0 path collection");
match solve(&paths[0], &ssa) {
SmtFinding::Reachable { input_bytes } => {
assert!(input_bytes.iter().any(|(_, b)| *b == b'%'));
}
other => panic!("expected Reachable with `%`, got {other:?}"),
}
}
#[cfg(feature = "smt")]
#[test]
fn sat_argv_to_system_is_reachable() {
let vars = vec![
mk_var(0, Expr::Const(0, 8)), mk_var(1, Expr::Const(0x4000, 8)), ];
let ssa = one_call_pair_cfg(
0x1000, vec![VarId(0), VarId(1)],
0x2000, vec![VarId(1)],
vars,
);
let imports = imports_with(&[(0x1000, "argv"), (0x2000, "system")]);
let paths = collect_paths(&ssa, &imports).expect("v0 path collection");
match solve(&paths[0], &ssa) {
SmtFinding::Reachable { input_bytes } => {
assert!(input_bytes
.iter()
.any(|(_, b)| matches!(*b, b';' | b'&' | b'|')));
}
other => panic!("expected Reachable with shell metachar, got {other:?}"),
}
}
#[cfg(feature = "smt")]
#[test]
fn unsat_recv_into_unrelated_strcpy_dst() {
let vars = vec![
mk_var(0, Expr::Const(0, 8)),
mk_var(1, Expr::Const(0x4000, 8)),
mk_var(2, Expr::Const(0x100, 8)),
mk_var(3, Expr::Const(0, 8)),
mk_var(4, Expr::Const(0x5000, 8)),
mk_var(5, Expr::Const(0, 8)),
mk_var(6, Expr::Const(0, 8)),
mk_var(7, Expr::Const(0, 8)),
mk_var(8, Expr::Const(0, 8)),
mk_var(9, Expr::Const(0x6000, 8)), ];
let ssa = one_call_pair_cfg(
0x1000, vec![VarId(0), VarId(1), VarId(2), VarId(3)],
0x2000, vec![VarId(4), VarId(9)],
vars,
);
let imports = imports_with(&[(0x1000, "recv"), (0x2000, "strcpy")]);
let paths = collect_paths(&ssa, &imports).expect("v0 path collection");
assert_eq!(solve(&paths[0], &ssa), SmtFinding::NotReachable);
}
#[cfg(feature = "smt")]
#[test]
fn lineage_eq_follows_var_chain() {
let vars = vec![
mk_var(0, Expr::Const(0, 8)),
mk_var(1, Expr::Const(0, 8)),
mk_var(2, Expr::Const(0x4000, 8)),
mk_var(3, Expr::Var(VarId(2))),
mk_var(4, Expr::Var(VarId(3))),
mk_var(5, Expr::Var(VarId(4))),
];
let mem = MemMap::new();
assert!(varid_lineage_eq(VarId(5), VarId(2), &vars, &mem));
assert!(!varid_lineage_eq(VarId(5), VarId(0), &vars, &mem));
}
#[cfg(feature = "smt")]
#[test]
fn lineage_eq_follows_store_then_load() {
let vars = vec![
mk_var(0, Expr::Const(0x1000, 8)), mk_var(1, Expr::Const(0xdeadbeef, 8)), mk_var(2, Expr::Load(VarId(0))), ];
let mut mem = MemMap::new();
mem.insert(vars[0].varnode, VarId(1));
assert!(!varid_lineage_eq(VarId(2), VarId(1), &vars, &MemMap::new()));
assert!(varid_lineage_eq(VarId(2), VarId(1), &vars, &mem));
}
#[test]
fn sink_in_terminator_call_slot() {
let vars = vec![
mk_var(0, Expr::Const(0, 8)),
mk_var(1, Expr::Const(0x4000, 8)),
mk_var(2, Expr::Const(0x5000, 8)),
];
let block0 = SsaBlock {
id: BlockId(0),
addr: 0,
stmts: vec![Stmt::Call {
target: CallTarget::Direct(0x1000),
args: vec![VarId(0), VarId(1), VarId(0), VarId(0)],
out: None,
}],
terminator: SsaTerminator::Call {
target: CallTarget::Direct(0x2000),
args: vec![VarId(2), VarId(1)],
out: None,
fallthrough: BlockId(1),
},
};
let block1 = SsaBlock {
id: BlockId(1),
addr: 0x10,
stmts: vec![],
terminator: SsaTerminator::Return(None),
};
let ssa = SsaCfg {
blocks: vec![block0, block1],
vars,
entry: BlockId(0),
diagnostics: Vec::<Diagnostic>::new(),
};
let imports = imports_with(&[(0x1000, "recv"), (0x2000, "strcpy")]);
let paths = collect_paths(&ssa, &imports).expect("should accept terminator-Call sink");
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].sink.name, "strcpy");
}
}