use std::path::Path;
use anyhow::{Context, Result};
use r2smt_common::Address;
use r2smt_explore::{ExploreBudget, ExploreRequest};
use r2smt_slicer::SliceLimits;
use r2smt_ssa::SsaLiftedSlice;
use r2smt_taint::{SourceId, TaintSeeds, TaintSet, propagate};
use crate::render::render_explore_result;
use crate::support::compute_slices;
pub(crate) struct TaintCli<'a> {
pub(crate) addr: &'a str,
pub(crate) sources: &'a [String],
pub(crate) concretise: bool,
pub(crate) wall_clock_ms: u64,
pub(crate) max_paths: u64,
}
pub(crate) struct TaintReport {
pub(crate) source_names: Vec<String>,
pub(crate) tainted: Vec<(String, Vec<u32>)>,
pub(crate) opaque: bool,
}
pub(crate) fn taint(
file: &Path,
deep: bool,
limits: &SliceLimits,
ir_pcode: bool,
cli: &TaintCli<'_>,
) -> Result<()> {
if !file.exists() {
anyhow::bail!("input file does not exist: {}", file.display());
}
let (arch, slices) = compute_slices(file, deep, Some(cli.addr), None, limits, ir_pcode)?;
let Some(ssa) = slices.first() else {
println!("r2smt: no analysable slice at {addr}", addr = cli.addr);
return Ok(());
};
let report = build_taint_report(ssa, cli.sources);
print_taint_report(cli.addr, &report);
if cli.concretise && report.opaque {
let target: Address = cli
.addr
.parse()
.with_context(|| format!("invalid target address: {}", cli.addr))?;
let request = ExploreRequest {
binary_path: file.to_path_buf(),
target,
arch,
};
let budget = ExploreBudget {
wall_clock_ms: cli.wall_clock_ms,
max_paths: cli.max_paths,
};
let result = r2smt_explore::explore(&request, budget);
println!("{}", render_explore_result(target, &result));
}
Ok(())
}
pub(crate) fn build_taint_report(ssa: &SsaLiftedSlice, sources: &[String]) -> TaintReport {
let (seeds, source_names) = build_seeds(ssa, sources);
let Some(outcome) = propagate(ssa, &seeds) else {
return TaintReport {
source_names,
tainted: Vec::new(),
opaque: true,
};
};
let mut tainted = Vec::new();
for def in &ssa.defs {
let set = outcome.taint_of(&def.name);
if !set.is_untainted() {
tainted.push((def.name.clone(), set.sources().map(|s| s.0).collect()));
}
}
TaintReport {
source_names,
tainted,
opaque: outcome.is_opaque(),
}
}
fn build_seeds(ssa: &SsaLiftedSlice, sources: &[String]) -> (TaintSeeds, Vec<String>) {
let mut seeds = TaintSeeds::new();
if sources.is_empty() {
let set = TaintSet::source(SourceId::new(0));
for input in &ssa.inputs {
seeds.insert(input.name.clone(), set.clone());
}
return (seeds, vec!["<any input>".to_string()]);
}
for (i, src) in sources.iter().enumerate() {
let set = TaintSet::source(SourceId::new(u32::try_from(i).unwrap_or(u32::MAX)));
for input in &ssa.inputs {
if input_matches(&input.name, src) {
seeds.insert(input.name.clone(), set.clone());
}
}
}
(seeds, sources.to_vec())
}
fn input_matches(input_name: &str, src: &str) -> bool {
input_name == src
|| input_name
.strip_prefix(src)
.is_some_and(|rest| rest.starts_with('#'))
}
fn print_taint_report(addr: &str, report: &TaintReport) {
println!(
"taint @ {addr} sources: {srcs}",
srcs = report.source_names.join(", ")
);
if report.tainted.is_empty() {
println!(" no tainted values reached this slice");
} else {
for (name, srcs) in &report.tainted {
let ids: Vec<String> = srcs.iter().map(u32::to_string).collect();
println!(" {name} <- source(s) {}", ids.join(","));
}
}
if report.opaque {
println!(
" note: outcome is OPAQUE (unmodelled node) — taint may be incomplete; \
re-run with --concretise to search for a witness"
);
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::panic)]
use super::{build_taint_report, input_matches};
use r2smt_common::{Address, Arch};
use r2smt_ir::expr::{Expr, Var};
use r2smt_ir::stmt::IrStmt;
use r2smt_slicer::{BranchCandidate, BranchCondition, BranchKind, SliceStatus};
use r2smt_ssa::SsaLiftedSlice;
fn slice(statements: Vec<IrStmt>, inputs: Vec<Var>, defs: Vec<Var>) -> SsaLiftedSlice {
let z = Address::new(0x1000);
SsaLiftedSlice {
branch: BranchCandidate {
address: z,
function: z,
block: z,
kind: BranchKind::Jcc,
mnemonic: "t".into(),
condition: BranchCondition::NotEqual,
formula: "t".into(),
taken_target: None,
fallthrough_target: None,
compare_register: None,
bit_index: None,
upstream_resolved: None,
operand_raws: Vec::new(),
is_thumb: false,
},
statements,
condition: Expr::konst(0, 1),
status: SliceStatus::Complete,
treat_truncation_as_inputs: false,
inputs,
defs,
arch: Arch::X86_64,
}
}
#[test]
fn test_build_taint_report_flags_flow_from_seeded_input() {
let ssa = slice(
vec![IrStmt::Assign {
dst: Var::new("rsi", 64),
src: Expr::Var(Var::new("rdi", 64)),
}],
vec![Var::new("rdi", 64)],
vec![Var::new("rsi", 64)],
);
let report = build_taint_report(&ssa, &[]);
assert!(report.tainted.iter().any(|(n, _)| n == "rsi"));
assert!(!report.opaque);
}
#[test]
fn test_build_taint_report_marks_unknown_opaque() {
let ssa = slice(
vec![IrStmt::Assign {
dst: Var::new("rsi", 64),
src: Expr::Unknown("x".into()),
}],
vec![Var::new("rdi", 64)],
vec![Var::new("rsi", 64)],
);
let report = build_taint_report(&ssa, &[]);
assert!(report.opaque);
}
#[test]
fn test_input_matches_exact_and_versioned() {
assert!(input_matches("rdi", "rdi"));
assert!(input_matches("rdi#0", "rdi"));
assert!(!input_matches("rdix", "rdi"));
assert!(!input_matches("rsi", "rdi"));
}
}