use std::path::Path;
use anyhow::{Context, Result};
use r2smt_common::{Address, Arch};
use r2smt_core::{
Finding, classify_finding_with_pretty, classify_lowered_upstream, dump_program, prepare_ssa,
reconcile_folded,
};
use r2smt_ir::BinaryProvider;
use r2smt_ir::Decompiler;
use r2smt_ir::NameHints;
use r2smt_ir::program::{Function, Program};
use r2smt_r2pipe::{AnalysisLevel, R2PipeProvider};
use r2smt_slicer::{BranchCandidate, SliceLimits, collect_branches, collect_function_branches};
use r2smt_smt::SolveOptions;
use crate::args::SolverArg;
use crate::render::truncate_on_char_boundary;
pub(crate) fn analysis_level(deep: bool) -> AnalysisLevel {
if deep {
AnalysisLevel::Deep
} else {
AnalysisLevel::Standard
}
}
pub(crate) fn open_provider(file: &Path, deep: bool) -> Result<R2PipeProvider> {
R2PipeProvider::open_with_analysis(file, false, analysis_level(deep))
.with_context(|| format!("opening {} with radare2", file.display()))
}
pub(crate) fn open_provider_writable(file: &Path, deep: bool) -> Result<R2PipeProvider> {
R2PipeProvider::open_with_analysis(file, true, analysis_level(deep))
.with_context(|| format!("opening {} with radare2 (-w)", file.display()))
}
pub(crate) struct AnalysisContext {
pub(crate) program: Program,
pub(crate) extra_functions: Vec<Function>,
}
impl AnalysisContext {
pub(crate) fn new(program: Program) -> Self {
Self {
program,
extra_functions: Vec::new(),
}
}
pub(crate) fn find_function(&self, address: Address) -> Option<&Function> {
self.program
.functions
.iter()
.find(|f| f.address == address)
.or_else(|| self.extra_functions.iter().find(|f| f.address == address))
}
pub(crate) fn all_functions(&self) -> impl Iterator<Item = &Function> {
self.program
.functions
.iter()
.chain(self.extra_functions.iter())
}
}
pub(crate) fn resolve_targets(
provider: &mut R2PipeProvider,
file: &Path,
program: Program,
at: Option<&str>,
function_filter: Option<&str>,
) -> Result<(AnalysisContext, Vec<BranchCandidate>)> {
let mut ctx = AnalysisContext::new(program);
let candidates: Vec<BranchCandidate> = match function_filter {
None => collect_branches(&ctx.program),
Some(raw) => {
let address: Address = raw
.parse()
.with_context(|| format!("parsing --function value '{raw}'"))?;
let function = ctx
.program
.functions
.iter()
.find(|f| f.address == address)
.ok_or_else(|| anyhow::anyhow!("no function at {address} in {}", file.display()))?;
collect_function_branches(function, ctx.program.arch)
}
};
let filtered: Vec<BranchCandidate> = if let Some(at_raw) = at {
let target: Address = at_raw
.parse()
.with_context(|| format!("parsing --at value '{at_raw}'"))?;
if let Some(found) = candidates.into_iter().find(|c| c.address == target) {
vec![found]
} else {
let func = provider
.load_block_at(target)
.with_context(|| format!("synthesising block at {target}"))?;
let synth_candidates = collect_function_branches(&func, ctx.program.arch);
let candidate = synth_candidates
.into_iter()
.find(|c| c.address == target)
.ok_or_else(|| {
anyhow::anyhow!(
"no candidate at {target} (program had no match; synthetic block at {} had no conditional branch at the requested address)",
func.address,
)
})?;
ctx.extra_functions.push(func);
vec![candidate]
}
} else {
candidates
};
Ok((ctx, filtered))
}
pub(crate) fn difflift_scope(
ctx: &AnalysisContext,
at: Option<&str>,
function_filter: Option<&str>,
candidates: &[BranchCandidate],
) -> Vec<Function> {
let target = function_filter
.and_then(|raw| raw.parse::<Address>().ok())
.or_else(|| {
candidates
.first()
.map(|c| c.function)
.filter(|_| at.is_some())
});
match target.and_then(|addr| ctx.find_function(addr)) {
Some(function) => vec![function.clone()],
None => ctx.all_functions().cloned().collect(),
}
}
const MAX_PSEUDOCODE_BYTES: usize = 16 * 1024;
pub(crate) fn attach_pseudocode(provider: &mut R2PipeProvider, findings: &mut [Finding]) {
let mut cache: std::collections::BTreeMap<Address, Option<String>> =
std::collections::BTreeMap::new();
for f in findings.iter_mut() {
let entry = cache.entry(f.function).or_insert_with(|| {
provider
.pseudocode(f.function)
.ok()
.flatten()
.map(|s| truncate_on_char_boundary(&s, MAX_PSEUDOCODE_BYTES))
});
f.pseudocode = entry.clone();
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn compute_findings(
file: &Path,
deep: bool,
at: Option<&str>,
function_filter: Option<&str>,
limits: &SliceLimits,
options: SolveOptions,
solver: SolverArg,
with_decompiler: bool,
ir_pcode: bool,
) -> Result<(Arch, Vec<Finding>)> {
let mut provider = open_provider(file, deep)?;
provider.set_attach_pcode(ir_pcode);
let program = dump_program(&mut provider)
.with_context(|| format!("loading program from {}", file.display()))?;
let arch = program.arch;
let (ctx, filtered) = resolve_targets(&mut provider, file, program, at, function_filter)?;
let mut hint_cache: std::collections::BTreeMap<Address, NameHints> =
std::collections::BTreeMap::new();
let mut findings: Vec<Finding> = Vec::with_capacity(filtered.len());
for cand in &filtered {
if let Some(finding) = resolve_folded_branch(
&mut provider,
cand,
ctx.program.arch,
limits,
solver,
options,
)? {
findings.push(finding);
continue;
}
let Some(function) = ctx.find_function(cand.function) else {
continue;
};
let ssa = prepare_ssa(function, cand, limits, ctx.program.arch);
let (verdict, z3_pretty) = dispatch_solver(solver, &ssa, options)?;
let hints = hint_cache
.entry(cand.function)
.or_insert_with(|| provider.name_hints(cand.function).unwrap_or_default());
findings.push(classify_finding_with_pretty(
&ssa, verdict, z3_pretty, hints,
));
}
if with_decompiler {
attach_pseudocode(&mut provider, &mut findings);
}
Ok((arch, findings))
}
pub(crate) fn compute_slices(
file: &Path,
deep: bool,
at: Option<&str>,
function_filter: Option<&str>,
limits: &SliceLimits,
ir_pcode: bool,
) -> Result<(Arch, Vec<r2smt_ssa::SsaLiftedSlice>)> {
let mut provider = open_provider(file, deep)?;
provider.set_attach_pcode(ir_pcode);
let program = dump_program(&mut provider)
.with_context(|| format!("loading program from {}", file.display()))?;
let arch = program.arch;
let (ctx, filtered) = resolve_targets(&mut provider, file, program, at, function_filter)?;
let mut slices = Vec::with_capacity(filtered.len());
for cand in &filtered {
if let Some(function) = ctx.find_function(cand.function) {
slices.push(prepare_ssa(function, cand, limits, ctx.program.arch));
}
}
Ok((arch, slices))
}
pub(crate) fn dispatch_solver(
solver: SolverArg,
slice: &r2smt_ssa::SsaLiftedSlice,
options: SolveOptions,
) -> Result<(r2smt_common::smt::SmtResult, Option<String>)> {
let backend = build_solver(solver);
match backend.solve(slice, options) {
Ok(outcome) => Ok((outcome.verdict, outcome.formula_pretty)),
Err(err) => Err(anyhow::anyhow!(
"{} backend: {}",
backend.name(),
err.detail()
)),
}
}
fn build_solver(solver: SolverArg) -> Box<dyn r2smt_solver_port::Solver> {
match solver {
SolverArg::Z3 => Box::new(r2smt_smt::Z3Solver),
SolverArg::Cvc5 => Box::new(r2smt_smt::Cvc5Solver),
SolverArg::Bitwuzla => Box::new(r2smt_smt::BitwuzlaSolver),
}
}
pub(crate) fn resolve_folded_branch(
provider: &mut R2PipeProvider,
cand: &BranchCandidate,
arch: r2smt_common::Arch,
limits: &SliceLimits,
solver: SolverArg,
options: SolveOptions,
) -> Result<Option<Finding>> {
if cand.upstream_resolved.is_none() {
return Ok(None);
}
let rederived = match provider.load_block_at(cand.address) {
Ok(synth) => collect_function_branches(&synth, arch)
.iter()
.find(|c| c.address == cand.address)
.map(|scand| {
let ssa = prepare_ssa(&synth, scand, limits, arch);
let (verdict, z3) = dispatch_solver(solver, &ssa, options)?;
Ok::<_, anyhow::Error>(classify_finding_with_pretty(
&ssa,
verdict,
z3,
&NameHints::default(),
))
})
.transpose()?,
Err(_) => None,
};
Ok(reconcile_folded(rederived, classify_lowered_upstream(cand)))
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::panic)]
use super::{AnalysisContext, difflift_scope};
use r2smt_common::{Address, Arch};
use r2smt_ir::program::{Function, Program};
use r2smt_slicer::{BranchCandidate, BranchCondition, BranchKind};
fn function(address: u64) -> Function {
Function {
address: Address::new(address),
name: None,
blocks: Vec::new(),
is_thumb: false,
}
}
fn ctx() -> AnalysisContext {
AnalysisContext::new(Program {
arch: Arch::X86_64,
bits: 64,
entry: None,
functions: vec![function(0x1000), function(0x2000)],
})
}
fn candidate_in(function: u64) -> BranchCandidate {
BranchCandidate {
address: Address::new(function + 4),
function: Address::new(function),
block: Address::new(function),
kind: BranchKind::Jcc,
mnemonic: "je".into(),
condition: BranchCondition::Equal,
formula: "ZF".into(),
taken_target: None,
fallthrough_target: None,
compare_register: None,
bit_index: None,
upstream_resolved: None,
operand_raws: Vec::new(),
is_thumb: false,
}
}
#[test]
fn test_difflift_scope_unfiltered_covers_the_whole_program() {
let scope = difflift_scope(&ctx(), None, None, &[]);
assert_eq!(scope.len(), 2);
}
#[test]
fn test_difflift_scope_function_filter_keeps_only_that_function() {
let scope = difflift_scope(&ctx(), None, Some("0x2000"), &[]);
assert_eq!(
scope.iter().map(|f| f.address).collect::<Vec<_>>(),
vec![Address::new(0x2000)]
);
}
#[test]
fn test_difflift_scope_function_filter_holds_without_any_candidate() {
let scope = difflift_scope(&ctx(), None, Some("0x1000"), &[]);
assert_eq!(scope.len(), 1);
}
#[test]
fn test_difflift_scope_at_keeps_the_function_containing_the_address() {
let scope = difflift_scope(&ctx(), Some("0x2004"), None, &[candidate_in(0x2000)]);
assert_eq!(
scope.iter().map(|f| f.address).collect::<Vec<_>>(),
vec![Address::new(0x2000)]
);
}
#[test]
fn test_difflift_scope_falls_back_to_the_program_when_the_function_is_absent() {
let scope = difflift_scope(&ctx(), None, Some("0x9999"), &[]);
assert_eq!(scope.len(), 2);
}
}