#![deny(missing_docs)]
#![allow(clippy::print_stdout, clippy::print_stderr)]
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use std::process::ExitCode;
use anyhow::{Context, Result};
use clap::Parser;
use r2smt_core::{
Confidence, Finding, FindingKind, classify_finding_with_pretty, dump_program, prepare_ssa,
};
use r2smt_explore::{ExploreBudget, ExploreRequest};
use r2smt_ir::Annotator;
use r2smt_ir::NameHints;
use r2smt_ir::program::Function;
use r2smt_report::Report;
use r2smt_slicer::SliceLimits;
use r2smt_smt::SolveOptions;
use tracing::{debug, error, warn};
use tracing_subscriber::EnvFilter;
mod args;
use args::{Cli, Command, SolverArg};
mod render;
use render::{print_annotation_preview, print_findings_summary};
mod support;
use support::{
attach_pseudocode, compute_findings, difflift_scope, dispatch_solver, open_provider,
resolve_folded_branch, resolve_targets,
};
mod commands;
use commands::batch::batch;
use commands::inspect::{analyze, branches, lift, slice, ssa};
use commands::patch::{PatchCli, patch};
use commands::taint::{TaintCli, taint};
fn main() -> ExitCode {
let cli = Cli::parse();
if let Err(e) = init_tracing(cli.verbose) {
eprintln!("r2smt: failed to initialise tracing: {e:#}");
return ExitCode::FAILURE;
}
match run(cli) {
Ok(()) => ExitCode::SUCCESS,
Err(err) => {
error!(target: "r2smt::cli", "{err:#}");
eprintln!("r2smt: {err:#}");
ExitCode::FAILURE
}
}
}
fn init_tracing(verbosity: u8) -> Result<()> {
let default_level = match verbosity {
0 => "info",
1 => "debug",
_ => "trace",
};
let filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default_level));
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_target(false)
.try_init()
.map_err(|e| anyhow::anyhow!("tracing init failed: {e}"))?;
Ok(())
}
#[allow(clippy::fn_params_excessive_bools)]
fn build_slice_limits(
esil_flags: bool,
max_instructions: Option<usize>,
allow_memory: bool,
allow_calls: bool,
unknowns_on_truncation: bool,
allow_join_merge: bool,
max_blocks: Option<u32>,
) -> SliceLimits {
let mut limits = SliceLimits {
esil_flags,
..SliceLimits::default()
};
if let Some(n) = max_instructions {
limits.max_instructions = n;
}
limits.allow_memory = allow_memory;
limits.allow_calls = allow_calls;
limits.unknowns_on_truncation = unknowns_on_truncation;
limits.allow_join_merge = allow_join_merge;
if let Some(n) = max_blocks {
limits.max_basic_blocks = n;
}
limits
}
fn build_solve_options(timeout_ms: Option<u32>, rlimit: Option<u32>) -> SolveOptions {
SolveOptions {
timeout_ms: timeout_ms.unwrap_or(SolveOptions::default().timeout_ms),
rlimit: rlimit.unwrap_or(SolveOptions::default().rlimit),
..SolveOptions::default()
}
}
#[allow(clippy::too_many_lines)]
fn run(cli: Cli) -> Result<()> {
let deep = cli.deep_analysis;
let ir_pcode = cli.ir.wants_pcode();
let esil_flags = !cli.no_esil_flags;
match cli.command {
Command::Version => {
println!("r2smt {}", env!("CARGO_PKG_VERSION"));
Ok(())
}
Command::Why {
file,
addr,
timeout_ms,
max_paths,
} => why_command(&file, deep, &addr, timeout_ms, max_paths),
Command::Taint {
file,
addr,
sources,
concretise,
max_instructions,
allow_memory,
allow_calls,
timeout_ms,
max_paths,
} => {
let limits = build_slice_limits(
esil_flags,
max_instructions,
allow_memory,
allow_calls,
false,
false,
None,
);
taint(
&file,
deep,
&limits,
ir_pcode,
&TaintCli {
addr: &addr,
sources: &sources,
concretise,
wall_clock_ms: timeout_ms.unwrap_or(30_000),
max_paths: max_paths.unwrap_or(10_000),
},
)
}
Command::Analyze {
file,
dump_program: dump_flag,
json,
} => analyze(&file, deep, dump_flag, json.as_deref()),
Command::Branches {
file,
function,
json,
} => branches(&file, deep, function.as_deref(), json.as_deref()),
Command::Slice {
file,
at,
function,
max_instructions,
allow_memory,
allow_calls,
unknowns_on_truncation,
solver: _,
max_blocks,
json,
} => {
let limits = build_slice_limits(
esil_flags,
max_instructions,
allow_memory,
allow_calls,
unknowns_on_truncation,
false,
max_blocks,
);
slice(
&file,
deep,
at.as_deref(),
function.as_deref(),
&limits,
json.as_deref(),
)
}
Command::Lift {
file,
at,
function,
max_instructions,
allow_memory,
allow_calls,
unknowns_on_truncation,
solver: _,
max_blocks,
json,
} => {
let limits = build_slice_limits(
esil_flags,
max_instructions,
allow_memory,
allow_calls,
unknowns_on_truncation,
false,
max_blocks,
);
lift(
&file,
deep,
at.as_deref(),
function.as_deref(),
&limits,
json.as_deref(),
)
}
Command::Annotate {
file,
at,
function,
max_instructions,
timeout_ms,
rlimit,
allow_memory,
allow_calls,
unknowns_on_truncation,
solver,
max_blocks,
min_confidence,
dry_run,
save_project,
} => {
let limits = build_slice_limits(
esil_flags,
max_instructions,
allow_memory,
allow_calls,
unknowns_on_truncation,
false,
max_blocks,
);
let options = build_solve_options(timeout_ms, rlimit);
let plan = AnnotatePlan {
min_confidence: min_confidence.to_confidence(),
dry_run,
save_project: save_project.as_deref(),
};
annotate(
&file,
deep,
at.as_deref(),
function.as_deref(),
&limits,
options,
&plan,
solver,
ir_pcode,
)
}
Command::Patch {
file,
at,
function,
max_instructions,
timeout_ms,
rlimit,
allow_memory,
allow_calls,
unknowns_on_truncation,
solver,
max_blocks,
min_confidence,
apply,
backup,
manifest,
rollback,
} => {
let limits = build_slice_limits(
esil_flags,
max_instructions,
allow_memory,
allow_calls,
unknowns_on_truncation,
false,
max_blocks,
);
let options = build_solve_options(timeout_ms, rlimit);
let cfg = PatchCli {
min_confidence: min_confidence.to_confidence(),
apply,
backup: backup.as_deref(),
manifest: manifest.as_deref(),
rollback,
solver,
};
patch(
&file,
deep,
at.as_deref(),
function.as_deref(),
&limits,
options,
&cfg,
ir_pcode,
)
}
Command::Solve {
file,
at,
function,
max_instructions,
timeout_ms,
rlimit,
allow_memory,
allow_calls,
unknowns_on_truncation,
solver,
max_blocks,
min_confidence,
include_real,
include_suspicious,
json,
markdown,
r2_script,
with_decompiler,
allow_join_merge,
differential_lift,
max_difflift_comparisons,
} => {
let limits = build_slice_limits(
esil_flags,
max_instructions,
allow_memory,
allow_calls,
unknowns_on_truncation,
allow_join_merge,
max_blocks,
);
let options = build_solve_options(timeout_ms, rlimit);
let filters = SolveFilters {
min_confidence: min_confidence.to_confidence(),
include_real,
include_suspicious,
};
let outputs = SolveOutputs {
json: json.as_deref(),
markdown: markdown.as_deref(),
r2_script: r2_script.as_deref(),
};
let difflift = DiffLiftOptions {
enabled: differential_lift,
max_comparisons: max_difflift_comparisons
.unwrap_or(DEFAULT_MAX_DIFFLIFT_COMPARISONS),
};
solve(
&file,
deep,
at.as_deref(),
function.as_deref(),
&limits,
options,
&filters,
&outputs,
solver,
with_decompiler,
ir_pcode,
&difflift,
)
}
Command::Batch {
dir,
threads,
max_instructions,
timeout_ms,
rlimit,
allow_memory,
allow_calls,
unknowns_on_truncation,
solver,
max_blocks,
json,
markdown,
with_decompiler,
allow_join_merge,
} => {
let limits = build_slice_limits(
esil_flags,
max_instructions,
allow_memory,
allow_calls,
unknowns_on_truncation,
allow_join_merge,
max_blocks,
);
let options = build_solve_options(timeout_ms, rlimit);
batch(
&dir,
deep,
threads,
&limits,
options,
solver,
with_decompiler,
ir_pcode,
json.as_deref(),
markdown.as_deref(),
)
}
Command::At {
file,
addr,
patch: do_patch,
timeout_ms,
rlimit,
max_instructions,
allow_memory,
allow_calls,
solver,
with_decompiler,
quiet,
explain,
allow_join_merge,
} => {
let limits = build_slice_limits(
esil_flags,
max_instructions,
allow_memory,
allow_calls,
false,
allow_join_merge,
None,
);
let options = build_solve_options(timeout_ms, rlimit);
let verbosity = if quiet {
AtVerbosity::Quiet
} else if explain {
AtVerbosity::Explain
} else {
AtVerbosity::Normal
};
at_command(
&file,
deep,
&addr,
&limits,
options,
solver,
&AtOptions {
do_patch,
with_decompiler,
ir_pcode,
verbosity,
},
)
}
Command::Ssa {
file,
at,
function,
max_instructions,
allow_memory,
allow_calls,
unknowns_on_truncation,
solver: _,
max_blocks,
json,
} => {
let limits = build_slice_limits(
esil_flags,
max_instructions,
allow_memory,
allow_calls,
unknowns_on_truncation,
false,
max_blocks,
);
ssa(
&file,
deep,
at.as_deref(),
function.as_deref(),
&limits,
json.as_deref(),
)
}
}
}
struct SolveFilters {
min_confidence: Confidence,
include_real: bool,
include_suspicious: bool,
}
struct AnnotatePlan<'a> {
min_confidence: Confidence,
dry_run: bool,
save_project: Option<&'a str>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum AtVerbosity {
Quiet,
Normal,
Explain,
}
struct AtOptions {
do_patch: bool,
with_decompiler: bool,
ir_pcode: bool,
verbosity: AtVerbosity,
}
fn why_command(
file: &Path,
deep: bool,
addr: &str,
timeout_ms: Option<u64>,
max_paths: Option<u64>,
) -> Result<()> {
if !file.exists() {
anyhow::bail!("input file does not exist: {}", file.display());
}
let target: r2smt_common::Address = addr
.parse()
.with_context(|| format!("invalid target address: {addr}"))?;
let mut provider = open_provider(file, deep)?;
let program = dump_program(&mut provider)
.with_context(|| format!("loading program from {}", file.display()))?;
let request = ExploreRequest {
binary_path: file.to_path_buf(),
target,
arch: program.arch,
};
let budget = ExploreBudget {
wall_clock_ms: timeout_ms.unwrap_or(30_000),
max_paths: max_paths.unwrap_or(10_000),
};
let result = r2smt_explore::explore(&request, budget);
println!("{}", render::render_explore_result(target, &result));
Ok(())
}
fn at_command(
file: &Path,
deep: bool,
addr: &str,
limits: &SliceLimits,
options: SolveOptions,
solver: SolverArg,
opts: &AtOptions,
) -> Result<()> {
if !file.exists() {
anyhow::bail!("input file does not exist: {}", file.display());
}
let (_arch, findings) = compute_findings(
file,
deep,
Some(addr),
None,
limits,
options,
solver,
opts.with_decompiler,
opts.ir_pcode,
)?;
let Some(finding) = findings.first() else {
println!("r2smt: no conditional branch at {addr}");
return Ok(());
};
println!(
"@ {addr} {mnem} {verdict:?} {kind:?}/{conf:?} {formula}",
addr = finding.address,
mnem = finding.mnemonic,
verdict = finding.verdict,
kind = finding.kind,
conf = finding.confidence,
formula = finding.formula_pretty,
);
if opts.verbosity != AtVerbosity::Quiet {
if opts.verbosity == AtVerbosity::Explain {
if let Some(z3) = &finding.formula_z3_pretty
&& !z3.is_empty()
&& z3.as_str() != finding.formula_pretty
{
println!(" solver-simplified: {z3}");
}
if !finding.evidence.inputs.is_empty() {
println!(" free inputs: {}", finding.evidence.inputs.join(", "));
}
println!(
" IR statements: {stmt}, unknowns: {unk}",
stmt = finding.evidence.statement_count,
unk = finding.evidence.unknown_count,
);
}
if let Some(code) = &finding.pseudocode {
println!("--- decompiled context ---");
println!("{code}");
}
}
if !opts.do_patch {
return Ok(());
}
if !(finding.is_actionable() && finding.confidence == Confidence::High) {
println!(
" not patched: needs an actionable verdict at high confidence (got {kind:?}/{conf:?})",
kind = finding.kind,
conf = finding.confidence,
);
return Ok(());
}
let backup = file.with_extension("r2smt.bak");
let manifest = file.with_extension("r2smt.manifest.json");
let cfg = PatchCli {
min_confidence: Confidence::High,
apply: true,
backup: Some(backup.as_path()),
manifest: Some(manifest.as_path()),
rollback: false,
solver,
};
patch(
file,
deep,
Some(addr),
None,
limits,
options,
&cfg,
opts.ir_pcode,
)
}
#[allow(clippy::too_many_arguments)]
fn annotate(
file: &Path,
deep: bool,
at: Option<&str>,
function_filter: Option<&str>,
limits: &SliceLimits,
options: SolveOptions,
plan: &AnnotatePlan<'_>,
solver: SolverArg,
ir_pcode: bool,
) -> Result<()> {
if !file.exists() {
anyhow::bail!("input file does not exist: {}", file.display());
}
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 bits = program.bits;
let function_count = program.functions.len();
let (ctx, filtered) = resolve_targets(&mut provider, file, program, at, function_filter)?;
let merged_functions: Vec<Function> = ctx.all_functions().cloned().collect();
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)?;
findings.push(classify_finding_with_pretty(
&ssa,
verdict,
z3_pretty,
&NameHints::default(),
));
}
let actionable: Vec<Finding> = findings
.into_iter()
.filter(|f| {
f.is_actionable()
&& matches!(
f.kind,
FindingKind::OpaquePredicate
| FindingKind::DeadBranch
| FindingKind::ConstantCondition
)
&& f.confidence <= plan.min_confidence
})
.collect();
let report = Report::from_findings(
env!("CARGO_PKG_VERSION"),
file.display().to_string(),
arch,
bits,
function_count,
actionable.clone(),
);
let annotations = report.annotations(&merged_functions);
println!(
"annotations: {n} (from {act} actionable findings, min_confidence={mc:?})",
n = annotations.len(),
act = actionable.len(),
mc = plan.min_confidence,
);
print_annotation_preview(&annotations, &merged_functions, &actionable);
if plan.dry_run {
println!();
println!("dry-run: no comments applied");
return Ok(());
}
let mut applied = 0usize;
for ann in &annotations {
provider
.set_comment(ann.address, &ann.text)
.with_context(|| format!("setting comment at {addr}", addr = ann.address))?;
applied += 1;
}
println!();
println!("applied: {applied} CCu comments");
if let Some(name) = plan.save_project {
provider
.save_project(name)
.with_context(|| format!("saving r2 project '{name}'"))?;
println!("saved r2 project: {name}");
}
Ok(())
}
struct SolveOutputs<'a> {
json: Option<&'a Path>,
markdown: Option<&'a Path>,
r2_script: Option<&'a Path>,
}
struct DiffLiftOptions {
enabled: bool,
max_comparisons: usize,
}
#[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)]
fn solve(
file: &Path,
deep: bool,
at: Option<&str>,
function_filter: Option<&str>,
limits: &SliceLimits,
options: SolveOptions,
filters: &SolveFilters,
outputs: &SolveOutputs<'_>,
solver: SolverArg,
with_decompiler: bool,
ir_pcode: bool,
difflift: &DiffLiftOptions,
) -> Result<()> {
if !file.exists() {
anyhow::bail!("input file does not exist: {}", file.display());
}
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 bits = program.bits;
let function_count = program.functions.len();
let (ctx, filtered) = resolve_targets(&mut provider, file, program, at, function_filter)?;
let merged_functions: Vec<Function> = ctx.all_functions().cloned().collect();
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)?;
findings.push(classify_finding_with_pretty(
&ssa,
verdict,
z3_pretty,
&NameHints::default(),
));
}
if difflift.enabled {
let scope = difflift_scope(&ctx, at, function_filter, &filtered);
let dl = run_differential_lift(
&scope,
ctx.program.arch,
solver,
options,
difflift.max_comparisons,
);
print_lifter_agreement(&dl);
findings.extend(dl.findings);
}
if with_decompiler {
attach_pseudocode(&mut provider, &mut findings);
}
let displayed: Vec<&Finding> = findings
.iter()
.filter(|f| keep_finding(f, filters))
.collect();
let any_file =
outputs.json.is_some() || outputs.markdown.is_some() || outputs.r2_script.is_some();
if any_file {
let report = Report::from_findings(
env!("CARGO_PKG_VERSION"),
file.display().to_string(),
arch,
bits,
function_count,
findings.clone(),
);
if let Some(path) = outputs.json {
let json = report.render_json().context("serialising report to JSON")?;
fs::write(path, json).with_context(|| format!("writing JSON to {}", path.display()))?;
}
if let Some(path) = outputs.markdown {
let md = report.render_markdown(&merged_functions);
fs::write(path, md)
.with_context(|| format!("writing Markdown to {}", path.display()))?;
}
if let Some(path) = outputs.r2_script {
let script = report.render_r2_script(&merged_functions);
fs::write(path, script)
.with_context(|| format!("writing r2 script to {}", path.display()))?;
}
} else {
print_findings_summary(&findings, &displayed, at.is_some(), &merged_functions);
}
Ok(())
}
const DEFAULT_MAX_DIFFLIFT_COMPARISONS: usize = 500_000;
const MAX_DIFFLIFT_MEMO_ENTRIES: usize = 50_000;
fn canonical_temp_names(rendered: &str) -> String {
let bytes = rendered.as_bytes();
let mut out = String::with_capacity(rendered.len());
let (mut copied, mut i) = (0usize, 0usize);
while i + 2 <= bytes.len() {
let starts_name = i == 0 || !(bytes[i - 1].is_ascii_alphanumeric() || bytes[i - 1] == b'_');
if !(starts_name && bytes[i] == b't' && bytes[i + 1] == b'_') {
i += 1;
continue;
}
let hex_start = i + 2;
let hex_end = hex_start
+ bytes[hex_start..]
.iter()
.take_while(|b| b.is_ascii_hexdigit())
.count();
if hex_end > hex_start && bytes.get(hex_end) == Some(&b'_') {
out.push_str(&rendered[copied..i]);
out.push_str("t_$_");
i = hex_end + 1;
copied = i;
} else {
i += 1;
}
}
out.push_str(&rendered[copied..]);
out
}
fn difflift_memo_key(a: &[r2smt_ir::IrStmt], b: &[r2smt_ir::IrStmt]) -> String {
canonical_temp_names(&format!("{a:?}\u{1}{b:?}"))
}
struct DiffLiftRun {
findings: Vec<Finding>,
stats: r2smt_difflift::AgreementStats,
compared: usize,
queries: usize,
truncated: bool,
cap: usize,
}
fn run_differential_lift(
functions: &[Function],
arch: r2smt_common::Arch,
solver: SolverArg,
options: SolveOptions,
cap: usize,
) -> DiffLiftRun {
let mut stats = r2smt_difflift::AgreementStats::default();
let mut findings: Vec<Finding> = Vec::new();
let mut compared = 0usize;
let mut queries = 0usize;
let mut truncated = false;
let mut memo: HashMap<String, r2smt_difflift::DiffVerdict> = HashMap::new();
'outer: for func in functions {
for block in &func.blocks {
for insn in &block.instructions {
if compared >= cap {
truncated = true;
break 'outer;
}
let lowerings = r2smt_difflift::lower_all(insn, arch);
let bodies: Vec<(r2smt_difflift::Lowering, &[r2smt_ir::IrStmt])> =
lowerings.available().collect();
let mut disagreeing: Vec<String> = Vec::new();
for (i, (_, sa)) in bodies.iter().enumerate() {
for (lb, sb) in &bodies[i + 1..] {
compared += 1;
let key = difflift_memo_key(sa, sb);
let verdict = memo.get(&key).copied().unwrap_or_else(|| {
queries += 1;
let fresh = compare_lowerings(sa, sb, arch, solver, options);
if memo.len() < MAX_DIFFLIFT_MEMO_ENTRIES {
memo.insert(key, fresh);
}
fresh
});
stats.record(verdict);
if verdict == r2smt_difflift::DiffVerdict::Disagree {
dump_disagreement(insn, bodies[i].0, sa, *lb, sb);
disagreeing.push(format!(
"{a} vs {b}",
a = bodies[i].0.as_str(),
b = lb.as_str(),
));
}
}
}
if !disagreeing.is_empty() {
findings.push(r2smt_core::lifter_disagreement_finding(
insn.address,
func.address,
insn.mnemonic.clone(),
format!(
"lifter disagreement on `{mnem}`: {pairs}",
mnem = insn.mnemonic,
pairs = disagreeing.join(", "),
),
));
}
}
}
}
if truncated {
warn!(
target: "r2smt::difflift",
compared,
queries,
cap,
"comparison budget exhausted — the rest of the program was not cross-checked"
);
}
DiffLiftRun {
findings,
stats,
compared,
queries,
truncated,
cap,
}
}
fn dump_disagreement(
insn: &r2smt_ir::program::Instruction,
lowering_a: r2smt_difflift::Lowering,
a: &[r2smt_ir::IrStmt],
lowering_b: r2smt_difflift::Lowering,
b: &[r2smt_ir::IrStmt],
) {
debug!(
target: "r2smt::difflift",
address = %insn.address,
mnemonic = %insn.mnemonic,
thumb = insn.is_thumb,
esil = insn.esil.as_deref().unwrap_or("<none>"),
"{a_name}:\n{a_body}\n{b_name}:\n{b_body}",
a_name = lowering_a.as_str(),
a_body = render_lowering(a),
b_name = lowering_b.as_str(),
b_body = render_lowering(b),
);
}
fn render_lowering(statements: &[r2smt_ir::IrStmt]) -> String {
statements
.iter()
.map(|s| format!(" {s}"))
.collect::<Vec<_>>()
.join("\n")
}
fn compare_lowerings(
a: &[r2smt_ir::IrStmt],
b: &[r2smt_ir::IrStmt],
arch: r2smt_common::Arch,
solver: SolverArg,
options: SolveOptions,
) -> r2smt_difflift::DiffVerdict {
match r2smt_difflift::build_equivalence_query(a, b, arch) {
None => r2smt_difflift::DiffVerdict::Inconclusive,
Some(query) => match dispatch_solver(solver, &query, options) {
Ok((verdict, _)) => r2smt_difflift::classify_equivalence(verdict),
Err(_) => r2smt_difflift::DiffVerdict::Inconclusive,
},
}
}
fn print_lifter_agreement(run: &DiffLiftRun) {
let rate = run
.stats
.agreement_rate()
.map_or_else(|| "n/a".to_string(), |r| format!("{:.2}%", r * 100.0));
let scope = if run.truncated {
format!(
" — TRUNCATED at the {cap} budget, the rest of the program was not compared",
cap = run.cap,
)
} else {
String::new()
};
println!(
"lifter-agreement: {rate} (agree={a} disagree={d} inconclusive={i}) over {compared} comparisons, {queries} solver queries{scope}",
a = run.stats.agree,
d = run.stats.disagree,
i = run.stats.inconclusive,
compared = run.compared,
queries = run.queries,
);
}
fn keep_finding(finding: &Finding, filters: &SolveFilters) -> bool {
match finding.kind {
FindingKind::RealBranch => filters.include_real,
FindingKind::OpaquePredicate | FindingKind::DeadBranch | FindingKind::ConstantCondition => {
finding.confidence <= filters.min_confidence
}
_ => filters.include_suspicious,
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::panic)]
use super::{canonical_temp_names, difflift_memo_key};
use r2smt_ir::expr::{Expr, Var};
use r2smt_ir::stmt::IrStmt;
fn assign(name: &str, value: u128) -> Vec<IrStmt> {
vec![IrStmt::Assign {
dst: Var::new(name, 64),
src: Expr::konst(value, 64),
}]
}
#[test]
fn test_the_same_instruction_at_two_addresses_shares_a_memo_key() {
assert_eq!(
difflift_memo_key(&assign("t_1000_0", 5), &assign("rax", 5)),
difflift_memo_key(&assign("t_2000_0", 5), &assign("rax", 5)),
);
}
#[test]
fn test_a_pc_relative_lowering_keeps_one_key_per_address() {
assert_ne!(
difflift_memo_key(&assign("t_1000_0", 0x1008), &assign("rax", 0x1008)),
difflift_memo_key(&assign("t_2000_0", 0x2008), &assign("rax", 0x2008)),
);
}
#[test]
fn test_two_temporaries_of_one_instruction_stay_distinct() {
assert_ne!(
canonical_temp_names("t_1000_0"),
canonical_temp_names("t_1000_1"),
);
}
#[test]
fn test_the_canonical_name_cannot_alias_a_real_variable() {
assert_eq!(canonical_temp_names("t_1000_0"), "t_$_0");
}
#[test]
fn test_a_name_without_the_temporary_shape_is_left_alone() {
assert_eq!(canonical_temp_names("t_zz_0"), "t_zz_0");
assert_eq!(canonical_temp_names("__esil_old_0"), "__esil_old_0");
}
#[test]
fn test_the_two_sides_of_a_key_cannot_straddle_the_separator() {
assert_ne!(
difflift_memo_key(&assign("rax", 1), &assign("rbx", 2)),
difflift_memo_key(&assign("rbx", 2), &assign("rax", 1)),
);
}
}