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,
Global(u64),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SinkKind {
StackBuffer,
FormatArg,
Command,
LengthArg,
TaintedStore,
CStringRead,
}
#[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 },
SinkSpec { name: "strlen", watched: AbiSlot::Arg(0), kind: SinkKind::CStringRead },
SinkSpec { name: "strnlen", watched: AbiSlot::Arg(0), kind: SinkKind::CStringRead },
SinkSpec { name: "strcmp", watched: AbiSlot::Arg(0), kind: SinkKind::CStringRead },
SinkSpec { name: "strncmp", watched: AbiSlot::Arg(0), kind: SinkKind::CStringRead },
SinkSpec { name: "strcasecmp", watched: AbiSlot::Arg(0), kind: SinkKind::CStringRead },
SinkSpec { name: "strncasecmp", watched: AbiSlot::Arg(0), kind: SinkKind::CStringRead },
SinkSpec { name: "strchr", watched: AbiSlot::Arg(0), kind: SinkKind::CStringRead },
SinkSpec { name: "strrchr", watched: AbiSlot::Arg(0), kind: SinkKind::CStringRead },
SinkSpec { name: "strstr", watched: AbiSlot::Arg(0), kind: SinkKind::CStringRead },
SinkSpec { name: "strcasestr", watched: AbiSlot::Arg(0), kind: SinkKind::CStringRead },
];
pub const STORE_SINK_SPEC: SinkSpec = SinkSpec {
name: "<tainted_store>",
watched: AbiSlot::Arg(0),
kind: SinkKind::TaintedStore,
};
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);
let unprefixed = stripped.trim_start_matches('_');
unprefixed.strip_suffix("_chk").unwrap_or(unprefixed)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SmtFinding {
Reachable {
input_bytes: Vec<(usize, u8)>,
call_chain: Vec<u64>,
},
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>,
call_chain: Vec<u64>,
},
SinkCall {
spec: &'a SinkSpec,
args: Vec<VarId>,
out: Option<VarId>,
call_chain: Vec<u64>,
},
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 = 64;
pub const MAX_WORKLIST_SIZE: usize = 16384;
pub const MAX_PATHS_PER_FN: usize = 8192;
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 empty: HashMap<crate::callgraph::FuncId, crate::function_summary::FunctionSummary> =
HashMap::new();
collect_paths_with_summaries(ssa, imports, &empty)
}
pub fn collect_paths_with_summaries<'a>(
ssa: &'a SsaCfg,
imports: &HashMap<u64, String>,
summaries: &HashMap<crate::callgraph::FuncId, crate::function_summary::FunctionSummary>,
) -> Result<Vec<TaintPath<'a>>, PathRejection> {
collect_paths_with_summaries_named(ssa, imports, summaries, None)
}
pub fn collect_paths_with_summaries_named<'a>(
ssa: &'a SsaCfg,
imports: &HashMap<u64, String>,
summaries: &HashMap<crate::callgraph::FuncId, crate::function_summary::FunctionSummary>,
func_name: Option<&str>,
) -> Result<Vec<TaintPath<'a>>, PathRejection> {
let mut initial_events: Vec<TaintEvent<'a>> = Vec::new();
if let Some(name) = func_name {
let trimmed = name.trim_start_matches('_');
if trimmed == "main" {
for v in &ssa.vars {
if v.param_name.as_deref() == Some("param_1") {
let argv_spec = DEFAULT_SOURCES
.iter()
.find(|s| s.name == "argv")
.expect("argv spec missing from DEFAULT_SOURCES");
initial_events.push(TaintEvent {
stmt_index: 0,
kind: TaintEventKind::SourceCall {
spec: argv_spec,
args: vec![VarId(0), v.id],
out: None,
call_chain: Vec::new(),
},
});
break;
}
}
}
}
let initial = WalkState {
current: ssa.entry,
events: initial_events,
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, &ssa.vars) {
Ok(ev) => {
state.events.push(ev);
synthesize_summary_events(
idx,
target,
args,
block.addr,
imports,
&ssa.vars,
summaries,
&mut state.events,
);
}
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, &ssa.vars) {
Ok(ev) => {
state.events.push(ev);
synthesize_summary_events(
term_idx,
target,
args,
block.addr,
imports,
&ssa.vars,
summaries,
&mut state.events,
);
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;
if worklist.len() < MAX_WORKLIST_SIZE {
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);
} else {
last_reject =
Some(PathRejection::UnsupportedTerminator("worklist cap"));
}
state.current = *taken;
state.branch_decisions.push(BranchDecision {
block_addr,
cond: *cond,
taken: true,
});
}
SsaTerminator::Indirect(_) => {
completed.push(state);
keep_walking = false;
break;
}
}
}
}
let mut paths = Vec::new();
for state in completed {
let mut sources: Vec<(usize, &'a SourceSpec)> = Vec::new();
for (i, ev) in state.events.iter().enumerate() {
match &ev.kind {
TaintEventKind::SourceCall { spec, .. } => {
sources.push((i, spec));
}
TaintEventKind::SinkCall { spec, .. } => {
for (src_i, src_spec) in &sources {
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.len() > MAX_PATHS_PER_FN {
paths.truncate(MAX_PATHS_PER_FN);
}
if paths.is_empty() {
return Err(last_reject.unwrap_or(PathRejection::NoSinkFound));
}
Ok(paths)
}
fn resolve_indirect_target(
target_vn: &pcode_ir::Varnode,
vars: &[crate::ir::VarDef],
imports: &HashMap<u64, String>,
) -> Option<u64> {
let mut visited: std::collections::HashSet<u32> =
std::collections::HashSet::new();
let mut stack: Vec<u32> = vars
.iter()
.rev()
.filter(|d| d.varnode == *target_vn)
.map(|d| d.id.0)
.collect();
while let Some(id) = stack.pop() {
if !visited.insert(id) {
continue;
}
if visited.len() > 32 {
break;
}
let Some(def) = vars.get(id as usize) else {
continue;
};
match &def.expr {
crate::ir::Expr::Const(c, _) => {
let addr = *c & 0x0FFF_FFFF;
if imports.contains_key(&addr) || imports.contains_key(c) {
return Some(if imports.contains_key(c) { *c } else { addr });
}
}
crate::ir::Expr::Var(inner) => stack.push(inner.0),
crate::ir::Expr::Load(addr_var) => {
if let Some(addr_def) = vars.get(addr_var.0 as usize) {
if let crate::ir::Expr::Const(slot, _) = addr_def.expr {
let candidates = [slot, slot & 0x0FFF_FFFF];
for c in candidates {
if imports.contains_key(&c) {
return Some(c);
}
}
}
}
}
_ => {}
}
}
None
}
fn classify_call<'a>(
stmt_index: usize,
target: &CallTarget,
args: &[VarId],
out: Option<VarId>,
imports: &HashMap<u64, String>,
vars: &[crate::ir::VarDef],
) -> Result<TaintEvent<'a>, PathRejection> {
let direct_addr = match target {
CallTarget::Direct(a) => Some(*a),
CallTarget::Indirect(vn) => resolve_indirect_target(vn, vars, imports),
};
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,
call_chain: Vec::new(),
}
}
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,
call_chain: Vec::new(),
}
}
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 })
}
fn synthesize_summary_events<'a>(
stmt_index: usize,
target: &CallTarget,
caller_args: &[VarId],
caller_addr: u64,
imports: &HashMap<u64, String>,
vars: &[crate::ir::VarDef],
summaries: &HashMap<crate::callgraph::FuncId, crate::function_summary::FunctionSummary>,
events: &mut Vec<TaintEvent<'a>>,
) {
let direct_addr = match target {
CallTarget::Direct(a) => Some(*a),
CallTarget::Indirect(vn) => resolve_indirect_target(vn, vars, imports),
};
let Some(addr) = direct_addr else {
return;
};
let callee_sum = match summaries.get(&crate::callgraph::FuncId(addr)) {
Some(s) => s,
None => return,
};
for src in &callee_sum.sources {
let Some(var) =
synth_pick_caller_var(&src.tainted_caller_slots, caller_args, vars)
else {
continue;
};
let watched_idx = match src.source.tainted {
AbiSlot::Arg(n) => n as usize,
AbiSlot::Ret => continue, AbiSlot::Global(_) => continue, };
let mut args_vec = vec![VarId(0); watched_idx + 1];
args_vec[watched_idx] = var;
let spec = DEFAULT_SOURCES
.iter()
.find(|sp| sp.name == src.source.name)
.expect("summary source not in DEFAULT_SOURCES");
events.push(TaintEvent {
stmt_index,
kind: TaintEventKind::SourceCall {
spec,
args: args_vec,
out: None,
call_chain: vec![caller_addr, src.call_site],
},
});
}
for snk in &callee_sum.sinks {
let Some(var) =
synth_pick_caller_var(&snk.tainted_caller_slots, caller_args, vars)
else {
continue;
};
let watched_idx = match snk.sink.watched {
AbiSlot::Arg(n) => n as usize,
AbiSlot::Ret => continue,
AbiSlot::Global(_) => continue,
};
let mut args_vec = vec![VarId(0); watched_idx + 1];
args_vec[watched_idx] = var;
let spec: &SinkSpec = if snk.sink.name == STORE_SINK_SPEC.name {
&STORE_SINK_SPEC
} else {
DEFAULT_SINKS
.iter()
.find(|sp| sp.name == snk.sink.name)
.expect("summary sink not in DEFAULT_SINKS")
};
events.push(TaintEvent {
stmt_index,
kind: TaintEventKind::SinkCall {
spec,
args: args_vec,
out: None,
call_chain: vec![caller_addr, snk.call_site],
},
});
}
}
fn synth_pick_caller_var(
tainted_slots: &[AbiSlot],
caller_args: &[VarId],
caller_vars: &[crate::ir::VarDef],
) -> Option<VarId> {
for slot in tainted_slots {
match slot {
AbiSlot::Arg(n) => {
if let Some(v) = caller_args.get(*n as usize) {
return Some(*v);
}
}
AbiSlot::Global(va) => {
for vd in caller_vars {
match &vd.expr {
crate::ir::Expr::Const(c, _) if *c == *va => {
return Some(vd.id);
}
crate::ir::Expr::Load(addr) => {
if let Some(addr_def) = caller_vars.get(addr.0 as usize) {
if let crate::ir::Expr::Const(c, _) = addr_def.expr {
if c == *va {
return Some(vd.id);
}
}
}
}
_ => {}
}
}
}
AbiSlot::Ret => {}
}
}
None
}
type MemMap = HashMap<(crate::region::Region, crate::region::OffsetClass), VarId>;
fn build_mem_map(
events: &[TaintEvent<'_>],
vars: &[crate::ir::VarDef],
regions: &crate::region::RegionMap,
) -> MemMap {
let mut m = MemMap::new();
for ev in events {
if let TaintEventKind::Store { addr, val } = ev.kind {
let key = mem_key(addr, vars, regions);
m.insert(key, val);
}
}
m
}
fn mem_key(
addr: VarId,
vars: &[crate::ir::VarDef],
regions: &crate::region::RegionMap,
) -> (crate::region::Region, crate::region::OffsetClass) {
let region = regions.region_of(addr);
let offset = classify_offset(addr, vars);
(region, offset)
}
fn classify_offset(addr: VarId, vars: &[crate::ir::VarDef]) -> crate::region::OffsetClass {
use crate::ir::{BinOpKind, Expr};
use crate::region::OffsetClass;
let Some(def) = vars.get(addr.0 as usize) else {
return OffsetClass::ConstOffset(0);
};
match &def.expr {
Expr::FieldAccess(_, off) => OffsetClass::ConstOffset(*off as i64),
Expr::BinOp(BinOpKind::Add, a, b) => {
if let Some(c) = const_value(*a, vars) {
return OffsetClass::ConstOffset(c);
}
if let Some(c) = const_value(*b, vars) {
return OffsetClass::ConstOffset(c);
}
OffsetClass::Symbolic
}
Expr::BinOp(BinOpKind::Sub, a, b) => {
if let Some(c) = const_value(*b, vars) {
if let Some(ca) = const_value(*a, vars) {
return OffsetClass::ConstOffset(ca.wrapping_sub(c));
}
return OffsetClass::ConstOffset(-c);
}
OffsetClass::Symbolic
}
Expr::Var(inner) => classify_offset(*inner, vars),
Expr::Const(c, _) => OffsetClass::ConstOffset(*c as i64),
_ => OffsetClass::ConstOffset(0),
}
}
fn const_value(v: VarId, vars: &[crate::ir::VarDef]) -> Option<i64> {
let mut cur = v;
for _ in 0..16 {
let def = vars.get(cur.0 as usize)?;
match &def.expr {
crate::ir::Expr::Const(c, _) => return Some(*c as i64),
crate::ir::Expr::Var(inner) => cur = *inner,
_ => return None,
}
}
None
}
type CallReturnMap = HashMap<VarId, Vec<VarId>>;
const LENGTH_BOUNDING_WRAPPERS: &[&str] = &[
"strlen", "strnlen", "wcslen", "wcsnlen",
"snprintf", "vsnprintf",
"read", "recv", "recvfrom", "recvmsg", "fread", "fgets",
];
fn build_bounded_returns_set(
ssa: &crate::ir::SsaCfg,
imports: &HashMap<u64, String>,
) -> std::collections::HashSet<VarId> {
let mut out = std::collections::HashSet::new();
let wrapper_kind = |target: &CallTarget| -> Option<&'static str> {
if let CallTarget::Direct(addr) = target {
if let Some(raw) = imports.get(addr) {
let n = normalise_name(raw);
if let Some(name) = LENGTH_BOUNDING_WRAPPERS
.iter()
.find(|w| **w == n)
.copied()
{
return Some(name);
}
}
}
None
};
let mut consider = |target: &CallTarget, args: &[VarId], o: VarId| {
let Some(name) = wrapper_kind(target) else { return };
let count_idx: Option<usize> = match name {
"read" | "recv" | "recvfrom" | "recvmsg" => Some(2),
"fread" => Some(2),
"fgets" => Some(1),
_ => None,
};
if let Some(idx) = count_idx {
if !arg_resolves_to_const(args.get(idx).copied(), &ssa.vars) {
return;
}
}
out.insert(o);
};
for block in &ssa.blocks {
for stmt in &block.stmts {
if let crate::ir::Stmt::Call {
target,
args,
out: Some(o),
..
} = stmt
{
consider(target, args, *o);
}
}
if let crate::ir::SsaTerminator::Call {
target,
args,
out: Some(o),
..
} = &block.terminator
{
consider(target, args, *o);
}
}
out
}
fn arg_resolves_to_const(var: Option<VarId>, vars: &[crate::ir::VarDef]) -> bool {
let Some(start) = var else { return false };
let mut visited: std::collections::HashSet<u32> = std::collections::HashSet::new();
let mut stack = vec![start];
let mut steps = 0usize;
while let Some(cur) = stack.pop() {
if !visited.insert(cur.0) {
continue;
}
steps += 1;
if steps > 64 {
return false;
}
let Some(def) = vars.get(cur.0 as usize) else { return false };
match &def.expr {
crate::ir::Expr::Var(inner) => stack.push(*inner),
crate::ir::Expr::Const(_, _) => {}
crate::ir::Expr::Phi(inputs) => {
for v in inputs {
stack.push(*v);
}
}
_ => return false,
}
}
true
}
fn build_call_return_map(ssa: &crate::ir::SsaCfg) -> CallReturnMap {
let mut m = CallReturnMap::new();
for block in &ssa.blocks {
for stmt in &block.stmts {
if let crate::ir::Stmt::Call {
args,
out: Some(o),
..
} = stmt
{
m.insert(*o, args.clone());
}
}
if let crate::ir::SsaTerminator::Call {
args,
out: Some(o),
..
} = &block.terminator
{
m.insert(*o, args.clone());
}
}
m
}
fn varid_lineage_eq(
a: VarId,
b: VarId,
vars: &[crate::ir::VarDef],
mem: &MemMap,
calls: &CallReturnMap,
regions: &crate::region::RegionMap,
) -> bool {
if a == b {
return true;
}
let chain_a = chain_varnodes(a, vars, mem, calls, regions);
let chain_b = chain_varnodes(b, vars, mem, calls, regions);
for vn_a in &chain_a {
if chain_b.iter().any(|vn_b| vn_a == vn_b) {
return true;
}
}
false
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum AliasKey {
Vn(pcode_ir::Varnode),
Region(crate::region::Region, crate::region::OffsetClass),
}
fn chain_varnodes(
start: VarId,
vars: &[crate::ir::VarDef],
mem: &MemMap,
calls: &CallReturnMap,
regions: &crate::region::RegionMap,
) -> Vec<AliasKey> {
chain_varnodes_with_bound(start, vars, mem, calls, regions, None)
}
fn chain_varnodes_with_bound(
start: VarId,
vars: &[crate::ir::VarDef],
mem: &MemMap,
calls: &CallReturnMap,
regions: &crate::region::RegionMap,
bounded_outs: Option<&std::collections::HashSet<VarId>>,
) -> Vec<AliasKey> {
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;
}
if let Some(args) = calls.get(¤t) {
let is_bounded = bounded_outs
.map(|s| s.contains(¤t))
.unwrap_or(false);
if !is_bounded {
for a in args {
stack.push(*a);
}
}
}
let Some(def) = vars.get(current.0 as usize) else {
continue;
};
let space_aliases = !matches!(
def.varnode.space,
pcode_ir::AddressSpaceId::Register
) && !def.call_return;
if space_aliases {
out.push(AliasKey::Vn(def.varnode));
}
let region = regions.region_of(current);
if let Some(site) = regions.site_of(region) {
if !matches!(site, crate::region::AllocSite::Unknown(_)) {
let off = classify_offset(current, vars);
out.push(AliasKey::Region(region, off));
}
}
match &def.expr {
crate::ir::Expr::Var(inner) => stack.push(*inner),
crate::ir::Expr::Load(addr) => {
let key = mem_key(*addr, vars, regions);
if let Some(stored) = mem.get(&key).copied() {
stack.push(stored);
} else {
let sym_key = (
key.0,
crate::region::OffsetClass::Symbolic,
);
if let Some(stored) = mem.get(&sym_key).copied() {
stack.push(stored);
}
}
}
crate::ir::Expr::BinOp(_, a, b) => {
stack.push(*a);
stack.push(*b);
}
crate::ir::Expr::UnaryOp(_, a) => stack.push(*a),
crate::ir::Expr::FieldAccess(base, _off) => stack.push(*base),
crate::ir::Expr::Phi(args) => {
for a in args {
stack.push(*a);
}
}
_ => {}
}
}
out
}
#[cfg(feature = "smt")]
pub fn solve(path: &TaintPath, ssa: &crate::ir::SsaCfg) -> SmtFinding {
solve_with_imports(path, ssa, &HashMap::new())
}
#[cfg(feature = "smt")]
pub fn solve_with_imports(
path: &TaintPath,
ssa: &crate::ir::SsaCfg,
imports: &HashMap<u64, String>,
) -> SmtFinding {
solve_diag(path, ssa, imports, &mut Vec::new())
}
#[cfg(feature = "smt")]
pub fn solve_diag(
path: &TaintPath,
ssa: &crate::ir::SsaCfg,
imports: &HashMap<u64, String>,
reason_log: &mut Vec<String>,
) -> 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 {
reason_log.push("source/sink slot missing".into());
return SmtFinding::Unsupported("source/sink slot missing");
};
let regions = crate::region::infer_regions(ssa);
let mem = build_mem_map(&path.events, &ssa.vars, ®ions);
let calls = build_call_return_map(ssa);
let lineage_ok = if path.source.name == "argv" {
let src_region = regions.region_of(src);
let chain_snk = chain_varnodes(snk, &ssa.vars, &mem, &calls, ®ions);
chain_snk.iter().any(|k| matches!(k, AliasKey::Region(r, _) if *r == src_region))
|| varid_lineage_eq(snk, src, &ssa.vars, &mem, &calls, ®ions)
} else {
varid_lineage_eq(snk, src, &ssa.vars, &mem, &calls, ®ions)
};
if !lineage_ok {
reason_log.push("lineage_eq failed (no shared alias key)".into());
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::TaintedStore => {
for b in &bytes {
let nz = b._eq(&BV::from_u64(&ctx, 0, 8)).not();
solver.assert(&nz);
}
}
SinkKind::CStringRead => {
for b in &bytes {
let nz = b._eq(&BV::from_u64(&ctx, 0, 8)).not();
solver.assert(&nz);
}
}
SinkKind::LengthArg => {
let bounded = build_bounded_returns_set(ssa, imports);
let chain_a = chain_varnodes_with_bound(
snk, &ssa.vars, &mem, &calls, ®ions, Some(&bounded),
);
let chain_b = chain_varnodes_with_bound(
src, &ssa.vars, &mem, &calls, ®ions, Some(&bounded),
);
let src_region = regions.region_of(src);
let region_eq = matches!(regions.site_of(src_region),
Some(s) if !matches!(s, crate::region::AllocSite::Unknown(_)))
&& chain_a.iter().any(|k| {
matches!(k, AliasKey::Region(r, _) if *r == src_region)
})
&& chain_b.iter().any(|k| {
matches!(k, AliasKey::Region(r, _) if *r == src_region)
});
let unbounded_eq = chain_a.iter().any(|k_a| {
matches!(k_a, AliasKey::Vn(_)) && chain_b.contains(k_a)
}) || region_eq;
if !unbounded_eq {
reason_log.push(format!(
"LengthArg lineage bounded by wrapper return ({} bounded VarIds: {:?})",
bounded.len(),
bounded.iter().map(|v| v.0).take(8).collect::<Vec<_>>()
));
return SmtFinding::NotReachable;
}
let dst_var = match &sink_event.kind {
TaintEventKind::SinkCall { args, .. } => args.first().copied(),
_ => None,
};
let dst_is_stack = dst_var
.map(|v| {
let r = regions.region_of(v);
matches!(
regions.site_of(r),
Some(crate::region::AllocSite::StackFrame)
)
})
.unwrap_or(false);
if !dst_is_stack {
let region_label = dst_var
.map(|v| {
let r = regions.region_of(v);
format!("{:?}", regions.site_of(r))
})
.unwrap_or_else(|| "(no dst var)".into());
reason_log.push(format!(
"LengthArg dst region not StackFrame: {}",
region_label
));
return SmtFinding::NotReachable;
}
let len = bytes[0]
.concat(&bytes[1])
.concat(&bytes[2])
.concat(&bytes[3]);
let threshold = BV::from_u64(&ctx, 0xFFFF, 32);
solver.assert(&len.bvugt(&threshold));
}
}
match solver.check() {
z3::SatResult::Sat => {
let m = match solver.get_model() {
Some(m) => m,
None => {
reason_log.push("Z3 SAT but model unavailable".into());
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));
}
}
}
let call_chain = match &path.events[path.sink_event].kind {
TaintEventKind::SinkCall { call_chain, .. } => call_chain.clone(),
_ => Vec::new(),
};
SmtFinding::Reachable { input_bytes, call_chain }
}
z3::SatResult::Unsat => {
reason_log.push("Z3 unsat under sink-kind constraint".into());
SmtFinding::NotReachable
}
z3::SatResult::Unknown => {
reason_log.push("Z3 returned Unknown / timeout".into());
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(not(feature = "smt"))]
pub fn solve_with_imports(
_path: &TaintPath,
_ssa: &crate::ir::SsaCfg,
_imports: &HashMap<u64, String>,
) -> SmtFinding {
SmtFinding::Unsupported("smt feature not enabled at build time")
}
#[cfg(not(feature = "smt"))]
pub fn solve_diag(
_path: &TaintPath,
_ssa: &crate::ir::SsaCfg,
_imports: &HashMap<u64, String>,
reason_log: &mut Vec<String>,
) -> SmtFinding {
reason_log.push("smt feature not enabled at build time".into());
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", "strlen"] {
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);
let strlen = DEFAULT_SINKS.iter().find(|s| s.name == "strlen").unwrap();
assert_eq!(strlen.watched, AbiSlot::Arg(0));
assert_eq!(strlen.kind, SinkKind::CStringRead);
}
#[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(id as u64, 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 v8_inter_procedural_summary_synthesizes_reachable_path() {
use crate::callgraph::FuncId;
use crate::function_summary::{FunctionSummary, SinkInvocation, SourceEmission};
let vars = vec![
mk_var(0, Expr::Const(0x4000, 8)), ];
let outer = SsaCfg {
blocks: vec![SsaBlock {
id: BlockId(0),
addr: 0x1000,
stmts: vec![Stmt::Call {
target: CallTarget::Direct(0xBEEF),
args: vec![VarId(0)],
out: None,
}],
terminator: SsaTerminator::Return(None),
}],
vars,
entry: BlockId(0),
diagnostics: Vec::<Diagnostic>::new(),
};
let imports: HashMap<u64, String> = HashMap::new();
let recv_spec = DEFAULT_SOURCES.iter().find(|s| s.name == "recv").copied().unwrap();
let strcpy_spec = DEFAULT_SINKS.iter().find(|s| s.name == "strcpy").copied().unwrap();
let helper_summary = FunctionSummary {
func: FuncId(0xBEEF),
sources: vec![SourceEmission {
source: recv_spec,
call_site: 0xBEEF + 4,
tainted_caller_slots: vec![AbiSlot::Arg(0)],
}],
sinks: vec![SinkInvocation {
sink: strcpy_spec,
call_site: 0xBEEF + 8,
tainted_caller_slots: vec![AbiSlot::Arg(0)],
}],
};
let mut summaries = HashMap::new();
summaries.insert(FuncId(0xBEEF), helper_summary);
let paths = collect_paths_with_summaries(&outer, &imports, &summaries)
.expect("V8 should synthesize Source/Sink events from helper's summary");
assert_eq!(paths.len(), 1);
let path = &paths[0];
assert_eq!(path.source.name, "recv");
assert_eq!(path.sink.name, "strcpy");
match &path.events[path.sink_event].kind {
TaintEventKind::SinkCall { call_chain, .. } => {
assert!(!call_chain.is_empty(), "sink call_chain should be populated");
}
other => panic!("expected SinkCall, got {other:?}"),
}
match solve(path, &outer) {
SmtFinding::Reachable { call_chain, .. } => {
assert!(
!call_chain.is_empty(),
"Reachable.call_chain should propagate from synthesized event"
);
}
other => panic!("expected Reachable via summary synthesis, 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();
let calls = CallReturnMap::new();
let regions = crate::region::RegionMap::default();
assert!(varid_lineage_eq(VarId(5), VarId(2), &vars, &mem, &calls, ®ions));
assert!(!varid_lineage_eq(VarId(5), VarId(0), &vars, &mem, &calls, ®ions));
}
#[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 regions = crate::region::RegionMap::default();
let mut mem = MemMap::new();
let key = mem_key(VarId(0), &vars, ®ions);
mem.insert(key, VarId(1));
let calls = CallReturnMap::new();
assert!(!varid_lineage_eq(
VarId(2),
VarId(1),
&vars,
&MemMap::new(),
&calls,
®ions,
));
assert!(varid_lineage_eq(VarId(2), VarId(1), &vars, &mem, &calls, ®ions));
}
#[cfg(feature = "smt")]
#[test]
fn lineage_eq_follows_call_return_pass_through() {
let vars = vec![
mk_var(0, Expr::Const(0x1000, 8)),
mk_var(1, Expr::Const(0xdead, 8)), mk_var(2, Expr::Const(0xbeef, 8)), ];
let mem = MemMap::new();
let mut calls = CallReturnMap::new();
let regions = crate::region::RegionMap::default();
assert!(!varid_lineage_eq(VarId(2), VarId(1), &vars, &mem, &calls, ®ions));
calls.insert(VarId(2), vec![VarId(1)]);
assert!(varid_lineage_eq(VarId(2), VarId(1), &vars, &mem, &calls, ®ions));
assert!(!varid_lineage_eq(VarId(2), VarId(0), &vars, &mem, &calls, ®ions));
}
#[cfg(feature = "smt")]
#[test]
fn region_keyed_mem_map_collides_distinct_unique_addrs_on_same_offset() {
let vars = vec![
mk_var(0, Expr::Const(8, 8)), mk_var(1, Expr::Const(8, 8)), mk_var(2, Expr::Const(0xDEAD, 8)), mk_var(3, Expr::Const(0xBEEF, 8)), ];
let regions = crate::region::RegionMap::default();
let mut mem = MemMap::new();
let key0 = mem_key(VarId(0), &vars, ®ions);
let key1 = mem_key(VarId(1), &vars, ®ions);
assert_eq!(key0, key1, "same const-offset addrs must share MemMap key");
mem.insert(key0.clone(), VarId(2));
mem.insert(key1, VarId(3));
assert_eq!(mem.get(&key0).copied(), Some(VarId(3)));
assert_eq!(mem.len(), 1);
}
#[cfg(feature = "smt")]
#[test]
fn build_call_return_map_captures_stmt_and_terminator_calls() {
let vars = vec![
mk_var(0, Expr::Const(0, 8)),
mk_var(1, Expr::Const(0, 8)),
mk_var(2, Expr::Const(0, 8)),
mk_var(3, Expr::Const(0, 8)),
];
let block = SsaBlock {
id: BlockId(0),
addr: 0,
stmts: vec![Stmt::Call {
target: CallTarget::Direct(0x1000),
args: vec![VarId(0)],
out: Some(VarId(2)),
}],
terminator: SsaTerminator::Call {
target: CallTarget::Direct(0x2000),
args: vec![VarId(1)],
out: Some(VarId(3)),
fallthrough: BlockId(1),
},
};
let block1 = SsaBlock {
id: BlockId(1),
addr: 4,
stmts: vec![],
terminator: SsaTerminator::Return(None),
};
let ssa = SsaCfg {
blocks: vec![block, block1],
vars,
entry: BlockId(0),
diagnostics: Vec::<Diagnostic>::new(),
};
let calls = build_call_return_map(&ssa);
assert_eq!(calls.get(&VarId(2)).map(|a| a.as_slice()), Some(&[VarId(0)][..]));
assert_eq!(calls.get(&VarId(3)).map(|a| a.as_slice()), Some(&[VarId(1)][..]));
}
#[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");
}
}