From 1299784e2235a2b0b1ff9f9459e0a9ae14aa58db Mon Sep 17 00:00:00 2001 From: Priyanshu Kumar Date: Wed, 25 Mar 2026 06:14:13 +0000 Subject: [PATCH 01/10] Implement exploration specification support in symbolic execution - Introduced a new `spec` module to define exploration specifications, including predicates, input configurations, and budget constraints. - Enhanced the `PathExplorer` to support running exploration specifications, allowing for more controlled symbolic execution paths. - Added functionality to handle symbolic file descriptor inputs, enabling the exploration of paths based on external input streams. - Updated tests to validate the new exploration specification features, ensuring correct behavior in symbolic execution scenarios. This commit enhances the symbolic execution framework by allowing users to define and run detailed exploration strategies. --- crates/r2sym/src/executor.rs | 398 +++++++++++++++++- crates/r2sym/src/lib.rs | 7 +- crates/r2sym/src/path.rs | 164 ++++++++ crates/r2sym/src/sim.rs | 207 ++++++++- crates/r2sym/src/spec.rs | 354 ++++++++++++++++ crates/r2sym/src/state.rs | 146 +++++++ crates/r2sym/tests/symex_integration.rs | 127 +++++- r2plugin/r_anal_sleigh.c | 67 +++ r2plugin/src/analysis/sym.rs | 215 ++++++++++ tests/e2e/vuln_test.c | 21 + .../db/extras/r2sleigh_integration_extended | 26 ++ 11 files changed, 1722 insertions(+), 10 deletions(-) create mode 100644 crates/r2sym/src/spec.rs diff --git a/crates/r2sym/src/executor.rs b/crates/r2sym/src/executor.rs index dca1503..145b6c3 100644 --- a/crates/r2sym/src/executor.rs +++ b/crates/r2sym/src/executor.rs @@ -3,6 +3,7 @@ //! This module implements the core symbolic execution logic, //! stepping through SSA operations and updating state. +use std::borrow::Cow; use std::collections::HashMap; use r2ssa::{FunctionSSABlock, SSAOp, SSAVar}; @@ -888,7 +889,13 @@ impl<'ctx> SymExecutor<'ctx> { } } else { let key = var.display_name(); - state.get_register_sized(&key, var.size * 8) + if let Some(value) = state.registers().get(&key).cloned() { + return value; + } + if let Some(value) = resolve_alias_register_value(state, &key, self.ctx) { + return value; + } + SymValue::unknown(var.size * 8) } } @@ -936,6 +943,307 @@ impl<'ctx> SymExecutor<'ctx> { } } +#[derive(Clone, Debug, PartialEq, Eq)] +struct RegisterAliasSpec { + family: Cow<'static, str>, + offset_bits: u32, + width_bits: u32, +} + +fn resolve_alias_register_value<'ctx>( + state: &SymState<'ctx>, + key: &str, + ctx: &'ctx Context, +) -> Option> { + let (requested_base, _requested_version) = split_versioned_register(key)?; + let requested = x86_register_alias_spec(requested_base)?; + + let mut best: Option<(u32, RegisterAliasSpec, SymValue<'ctx>)> = None; + for (candidate_key, candidate_value) in state.registers() { + let Some((candidate_base, candidate_version)) = split_versioned_register(candidate_key) + else { + continue; + }; + let Some(candidate) = x86_register_alias_spec(candidate_base) else { + continue; + }; + if candidate.family != requested.family { + continue; + } + + let candidate_low = candidate.offset_bits; + let candidate_high = candidate.offset_bits + candidate.width_bits; + let requested_low = requested.offset_bits; + let requested_high = requested.offset_bits + requested.width_bits; + + let covers_requested = candidate_low <= requested_low && candidate_high >= requested_high; + let can_zero_extend = + candidate_low == requested_low && candidate.width_bits < requested.width_bits; + if !covers_requested && !can_zero_extend { + continue; + } + + let should_replace = best.as_ref().is_none_or(|(best_version, best_spec, _)| { + candidate_version > *best_version + || (candidate_version == *best_version + && candidate.width_bits > best_spec.width_bits) + }); + if should_replace { + best = Some((candidate_version, candidate, candidate_value.clone())); + } + } + + let (_version, candidate, value) = best?; + if candidate.offset_bits <= requested.offset_bits + && candidate.offset_bits + candidate.width_bits + >= requested.offset_bits + requested.width_bits + { + let low = requested.offset_bits - candidate.offset_bits; + let high = low + requested.width_bits - 1; + let extracted = value.extract(ctx, high, low); + if extracted.bits() == requested.width_bits { + Some(extracted) + } else if extracted.bits() < requested.width_bits { + Some(extracted.zero_extend(ctx, requested.width_bits)) + } else { + Some(extracted.extract(ctx, requested.width_bits - 1, 0)) + } + } else if candidate.offset_bits == requested.offset_bits + && candidate.width_bits < requested.width_bits + { + Some(value.zero_extend(ctx, requested.width_bits)) + } else { + None + } +} + +fn split_versioned_register(name: &str) -> Option<(&str, u32)> { + let (base, version) = name.rsplit_once('_')?; + if version.is_empty() || !version.chars().all(|c| c.is_ascii_digit()) { + return None; + } + Some((base, version.parse().ok()?)) +} + +fn x86_register_alias_spec(base: &str) -> Option { + let upper = base.to_ascii_uppercase(); + let base = upper.as_str(); + let fixed = match base { + "AL" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RAX"), + offset_bits: 0, + width_bits: 8, + }), + "AH" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RAX"), + offset_bits: 8, + width_bits: 8, + }), + "AX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RAX"), + offset_bits: 0, + width_bits: 16, + }), + "EAX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RAX"), + offset_bits: 0, + width_bits: 32, + }), + "RAX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RAX"), + offset_bits: 0, + width_bits: 64, + }), + "BL" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RBX"), + offset_bits: 0, + width_bits: 8, + }), + "BH" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RBX"), + offset_bits: 8, + width_bits: 8, + }), + "BX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RBX"), + offset_bits: 0, + width_bits: 16, + }), + "EBX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RBX"), + offset_bits: 0, + width_bits: 32, + }), + "RBX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RBX"), + offset_bits: 0, + width_bits: 64, + }), + "CL" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RCX"), + offset_bits: 0, + width_bits: 8, + }), + "CH" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RCX"), + offset_bits: 8, + width_bits: 8, + }), + "CX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RCX"), + offset_bits: 0, + width_bits: 16, + }), + "ECX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RCX"), + offset_bits: 0, + width_bits: 32, + }), + "RCX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RCX"), + offset_bits: 0, + width_bits: 64, + }), + "DL" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RDX"), + offset_bits: 0, + width_bits: 8, + }), + "DH" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RDX"), + offset_bits: 8, + width_bits: 8, + }), + "DX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RDX"), + offset_bits: 0, + width_bits: 16, + }), + "EDX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RDX"), + offset_bits: 0, + width_bits: 32, + }), + "RDX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RDX"), + offset_bits: 0, + width_bits: 64, + }), + "SIL" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RSI"), + offset_bits: 0, + width_bits: 8, + }), + "SI" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RSI"), + offset_bits: 0, + width_bits: 16, + }), + "ESI" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RSI"), + offset_bits: 0, + width_bits: 32, + }), + "RSI" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RSI"), + offset_bits: 0, + width_bits: 64, + }), + "DIL" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RDI"), + offset_bits: 0, + width_bits: 8, + }), + "DI" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RDI"), + offset_bits: 0, + width_bits: 16, + }), + "EDI" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RDI"), + offset_bits: 0, + width_bits: 32, + }), + "RDI" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RDI"), + offset_bits: 0, + width_bits: 64, + }), + "BPL" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RBP"), + offset_bits: 0, + width_bits: 8, + }), + "BP" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RBP"), + offset_bits: 0, + width_bits: 16, + }), + "EBP" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RBP"), + offset_bits: 0, + width_bits: 32, + }), + "RBP" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RBP"), + offset_bits: 0, + width_bits: 64, + }), + "SPL" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RSP"), + offset_bits: 0, + width_bits: 8, + }), + "SP" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RSP"), + offset_bits: 0, + width_bits: 16, + }), + "ESP" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RSP"), + offset_bits: 0, + width_bits: 32, + }), + "RSP" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RSP"), + offset_bits: 0, + width_bits: 64, + }), + _ => None, + }; + if fixed.is_some() { + return fixed; + } + + parse_numbered_x86_register_alias(base) +} + +fn parse_numbered_x86_register_alias(base: &str) -> Option { + let (family, width_bits) = if let Some(family) = base.strip_suffix('B') { + (family.to_string(), 8) + } else if let Some(family) = base.strip_suffix('W') { + (family.to_string(), 16) + } else if let Some(family) = base.strip_suffix('D') { + (family.to_string(), 32) + } else { + (base.to_string(), 64) + }; + + if !family.starts_with('R') { + return None; + } + let digits = &family[1..]; + if digits.is_empty() || !digits.chars().all(|c| c.is_ascii_digit()) { + return None; + } + + Some(RegisterAliasSpec { + family: Cow::Owned(family), + offset_bits: 0, + width_bits, + }) +} + #[cfg(test)] mod tests { use super::*; @@ -1023,4 +1331,92 @@ mod tests { assert_eq!(forked[0].pc, 0x2000); // True branch goes to target // Original state is false branch (PC unchanged in this test) } + + #[test] + fn test_read_var_recovers_x86_subregister_alias() { + let ctx = Context::thread_local(); + let executor = SymExecutor::new(&ctx); + let mut state = SymState::new(&ctx, 0x1000); + + state.set_register("EAX_4", SymValue::concrete(0x6b, 32)); + + let al = SSAVar::new("AL", 0, 1); + let value = executor.read_var(&state, &al); + assert_eq!(value.as_concrete(), Some(0x6b)); + assert_eq!(value.bits(), 8); + } + + #[test] + fn test_realistic_al_compare_path_forks_on_loaded_byte() { + let ctx = Context::thread_local(); + let executor = SymExecutor::new(&ctx); + let mut state = SymState::new(&ctx, 0x401c6d); + + let addr = SymValue::concrete(0x2ff6, 64); + let byte = SymValue::new_symbolic(&ctx, "stdin_byte", 8); + state.mem_write(&addr, &byte, 1); + state.set_register("tmp:4700_5", addr.clone()); + + let ops = vec![ + SSAOp::Load { + dst: SSAVar::new("tmp:11e00", 1, 1), + addr: SSAVar::new("tmp:4700", 5, 8), + space: "ram".to_string(), + }, + SSAOp::IntZExt { + dst: SSAVar::new("EAX", 4, 4), + src: SSAVar::new("tmp:11e00", 1, 1), + }, + SSAOp::IntZExt { + dst: SSAVar::new("RAX", 6, 8), + src: SSAVar::new("EAX", 4, 4), + }, + SSAOp::IntLess { + dst: SSAVar::new("CF", 6, 1), + a: SSAVar::new("AL", 0, 1), + b: SSAVar::constant(0x6b, 1), + }, + SSAOp::IntSBorrow { + dst: SSAVar::new("OF", 6, 1), + a: SSAVar::new("AL", 0, 1), + b: SSAVar::constant(0x6b, 1), + }, + SSAOp::IntSub { + dst: SSAVar::new("tmp:3de00", 1, 1), + a: SSAVar::new("AL", 0, 1), + b: SSAVar::constant(0x6b, 1), + }, + SSAOp::IntSLess { + dst: SSAVar::new("SF", 6, 1), + a: SSAVar::new("tmp:3de00", 1, 1), + b: SSAVar::constant(0, 1), + }, + SSAOp::IntEqual { + dst: SSAVar::new("ZF", 6, 1), + a: SSAVar::new("tmp:3de00", 1, 1), + b: SSAVar::constant(0, 1), + }, + SSAOp::BoolNot { + dst: SSAVar::new("tmp:12800", 1, 1), + src: SSAVar::new("ZF", 6, 1), + }, + SSAOp::CBranch { + target: SSAVar::constant(0x401c86, 8), + cond: SSAVar::new("tmp:12800", 1, 1), + }, + ]; + + let mut forked = Vec::new(); + for op in &ops { + let new_states = executor.step(&mut state, op).unwrap(); + forked.extend(new_states); + } + + assert_eq!(forked.len(), 1, "true branch should fork to the target"); + assert_eq!(forked[0].pc, 0x401c86); + assert!( + state.get_register("tmp:12800_1").as_concrete().is_none(), + "false branch condition should remain symbolic" + ); + } } diff --git a/crates/r2sym/src/lib.rs b/crates/r2sym/src/lib.rs index af3b518..4d43c29 100644 --- a/crates/r2sym/src/lib.rs +++ b/crates/r2sym/src/lib.rs @@ -40,6 +40,7 @@ pub mod r2api; pub mod runtime; pub mod sim; pub mod solver; +pub mod spec; pub mod state; pub mod value; @@ -52,7 +53,11 @@ pub use sim::{ CallConv, CallInfo, FunctionSummary, SummaryEffect, SummaryInstallStats, SummaryRegistry, }; pub use solver::{SatResult, SymModel, SymSolver}; -pub use state::{SymState, SymbolicMemoryRegion}; +pub use spec::{ + AddressValue, BudgetSpec, ExplorationSpec, InputSpec, MergeSpec, PredicateSpec, RuntimeSpec, + StartSpec, StrategySpec, +}; +pub use state::{RuntimeState, SymState, SymbolicFdInput, SymbolicMemoryRegion}; pub use value::SymValue; /// Error types for symbolic execution. diff --git a/crates/r2sym/src/path.rs b/crates/r2sym/src/path.rs index ce1d8bf..6a44a58 100644 --- a/crates/r2sym/src/path.rs +++ b/crates/r2sym/src/path.rs @@ -11,6 +11,7 @@ use z3::Context; use crate::executor::SymExecutor; use crate::solver::SymSolver; +use crate::spec::ExplorationSpec; use crate::state::{ExitStatus, SymState}; /// Configuration for path exploration. @@ -111,6 +112,8 @@ impl<'ctx> PathResult<'ctx> { pub struct SolvedPath { /// Concrete input values (symbolic variable name -> value). pub inputs: std::collections::HashMap, + /// Concrete multi-byte input buffers (input source name -> bytes). + pub input_buffers: std::collections::HashMap>, /// Concrete register values at path end. pub registers: std::collections::HashMap, /// Concrete memory bytes for tracked symbolic regions. @@ -121,6 +124,23 @@ pub struct SolvedPath { pub num_constraints: usize, } +/// Result of a spec-driven exploration run. +#[derive(Debug, Default)] +pub struct SpecExploreResult<'ctx> { + /// Feasible paths that matched a find predicate. + pub found_paths: Vec>, + /// Number of states pruned by avoid predicates. + pub avoided_states: usize, + /// Number of states pruned as infeasible. + pub unsat_states: usize, + /// Number of states that ended in an execution error. + pub errored_states: usize, + /// Number of completed states that did not hit a find predicate. + pub completed_states: usize, + /// Non-fatal diagnostics gathered during execution. + pub diagnostics: Vec, +} + /// Path explorer for symbolic execution. pub struct PathExplorer<'ctx> { /// The Z3 context. @@ -250,6 +270,17 @@ impl<'ctx> PathExplorer<'ctx> { } } + for input in path.state.symbolic_fd_inputs().values() { + let bytes: Option> = input + .bytes + .iter() + .map(|byte| model.eval(byte).map(|v| v as u8)) + .collect(); + if let Some(bytes) = bytes { + solved.input_buffers.insert(input.name.clone(), bytes); + } + } + Some(solved) } @@ -634,6 +665,138 @@ impl<'ctx> PathExplorer<'ctx> { None } + + /// Run a typed exploration specification. + pub fn run_spec( + &mut self, + func: &SsaArtifact, + initial_state: SymState<'ctx>, + spec: &ExplorationSpec, + ) -> Result, String> { + let find_set: HashSet = spec.find_addresses()?.into_iter().collect(); + let avoid_set: HashSet = spec.avoid_addresses()?.into_iter().collect(); + let max_finds = spec.max_finds(); + let start_time = Instant::now(); + let mut result = SpecExploreResult::default(); + let mut worklist: VecDeque> = VecDeque::new(); + worklist.push_back(initial_state); + + while let Some(mut state) = self.next_state(&mut worklist) { + if self.config.merge_states + && let Some(other) = take_merge_candidate(&mut worklist, state.pc) + { + state = state.merge_with(&other); + } + + if let Some(timeout) = self.config.timeout + && start_time.elapsed() > timeout + { + result.diagnostics.push("exploration timed out".to_string()); + break; + } + + if avoid_set.contains(&state.pc) { + result.avoided_states += 1; + continue; + } + + if find_set.contains(&state.pc) { + let feasible = self.solver.is_sat(&state); + if feasible { + self.stats.paths_completed += 1; + if state.depth > self.stats.max_depth_reached { + self.stats.max_depth_reached = state.depth; + } + result.found_paths.push(PathResult::new(state, true)); + if result.found_paths.len() >= max_finds { + break; + } + } else { + self.stats.paths_pruned += 1; + result.unsat_states += 1; + } + continue; + } + + if self.stats.states_explored >= self.config.max_states { + result + .diagnostics + .push("exploration stopped at max_states budget".to_string()); + break; + } + + if state.depth >= self.config.max_depth { + self.stats.paths_max_depth += 1; + result.completed_states += 1; + continue; + } + + self.stats.states_explored += 1; + + if self.config.prune_infeasible && !self.solver.is_sat(&state) { + self.stats.paths_pruned += 1; + result.unsat_states += 1; + continue; + } + + let block_addr = state.pc; + let Some(block) = func.get_block(block_addr) else { + result + .diagnostics + .push(format!("no SSA block at 0x{block_addr:x}")); + result.completed_states += 1; + self.stats.paths_completed += 1; + continue; + }; + + match self.executor.execute_block(&mut state, block) { + Ok(forked_states) => { + for mut forked in forked_states { + forked.set_prev_pc(Some(block_addr)); + if !avoid_set.contains(&forked.pc) { + worklist.push_back(forked); + } else { + result.avoided_states += 1; + } + } + + if !state.is_terminated() { + if state.pc == block_addr + && let Some(next) = self.fallthrough_target(func, block_addr) + { + state.pc = next; + } + state.set_prev_pc(Some(block_addr)); + if !avoid_set.contains(&state.pc) { + worklist.push_back(state); + } else { + result.avoided_states += 1; + } + } else { + self.stats.paths_completed += 1; + result.diagnostics.push(format!( + "state terminated at 0x{:x} with {:?}", + block_addr, state.exit_status + )); + match &state.exit_status { + Some(ExitStatus::Error(_)) => result.errored_states += 1, + _ => result.completed_states += 1, + } + } + } + Err(e) => { + self.stats.paths_completed += 1; + result.errored_states += 1; + result + .diagnostics + .push(format!("execution error at 0x{block_addr:x}: {e}")); + } + } + } + + self.stats.total_time = start_time.elapsed(); + Ok(result) + } } fn take_merge_candidate<'ctx>( @@ -727,6 +890,7 @@ mod tests { fn test_solved_path_default() { let solved = SolvedPath::default(); assert!(solved.inputs.is_empty()); + assert!(solved.input_buffers.is_empty()); assert!(solved.registers.is_empty()); assert!(solved.memory.is_empty()); assert_eq!(solved.final_pc, 0); diff --git a/crates/r2sym/src/sim.rs b/crates/r2sym/src/sim.rs index 6d2b5d2..2228efb 100644 --- a/crates/r2sym/src/sim.rs +++ b/crates/r2sym/src/sim.rs @@ -92,7 +92,9 @@ impl CallConv { /// Architecture-derived calling convention used by the symbolic runtime. pub fn for_arch_spec(arch: &ArchSpec) -> Option { let arch_name = arch.name.to_ascii_lowercase(); - if arch.addr_size == 8 && arch_name.contains("x86") { + let looks_x86 = arch_name.contains("x86") || arch_name == "x64" || arch_name == "amd64"; + let looks_64 = arch.addr_size == 8 || arch.addr_size == 64 || arch_name.contains("64"); + if looks_x86 && looks_64 { return Some(Self::x86_64_sysv()); } @@ -129,17 +131,25 @@ impl CallConv { } fn read_register<'ctx>(&self, state: &SymState<'ctx>, base: &str) -> SymValue<'ctx> { - if let Some(key) = find_register_key(state, base) { - state.get_register_sized(&key, self.arg_bits) - } else { - SymValue::unknown(self.arg_bits) + for alias in register_aliases(base) { + if let Some(key) = find_register_key(state, alias) { + return state.get_register_sized(&key, self.arg_bits); + } } + SymValue::unknown(self.arg_bits) } fn write_return<'ctx>(&self, state: &mut SymState<'ctx>, value: SymValue<'ctx>) { - let key = find_register_key(state, self.ret_register) + let key = register_aliases(self.ret_register) + .into_iter() + .find_map(|alias| find_register_key(state, alias)) .unwrap_or_else(|| format!("{}_0", self.ret_register)); - let adjusted = adjust_bits(state.context(), value, self.ret_bits); + let key_bits = state + .registers() + .get(&key) + .map(|existing| existing.bits()) + .unwrap_or(self.ret_bits); + let adjusted = adjust_bits(state.context(), value, key_bits); state.set_register(&key, adjusted); } } @@ -179,6 +189,11 @@ impl<'ctx> SummaryRegistry<'ctx> { registry.register_summary(FreeSummary::new()); registry.register_summary(PutsSummary::new(DEFAULT_MAX_PRINTF_SCAN)); registry.register_summary(PrintfSummaryBasic::new(DEFAULT_MAX_PRINTF_SCAN)); + registry.register_summary(ReadSummary::new()); + registry.register_summary(IsattySummary::new()); + registry.register_summary(SleepSummary::sleep()); + registry.register_summary(SleepSummary::usleep()); + registry.register_summary(SleepSummary::nanosleep()); registry.register_summary(ExitSummary::new()); registry } @@ -467,6 +482,21 @@ fn arch_has_register(arch: &ArchSpec, name: &str) -> bool { .any(|reg| reg.name.eq_ignore_ascii_case(name)) } +fn register_aliases(base: &str) -> Vec<&str> { + match base { + "RAX" => vec!["RAX", "EAX"], + "RDI" => vec!["RDI", "EDI"], + "RSI" => vec!["RSI", "ESI"], + "RDX" => vec!["RDX", "EDX"], + "RCX" => vec!["RCX", "ECX"], + "R8" => vec!["R8", "R8D"], + "R9" => vec!["R9", "R9D"], + "RSP" => vec!["RSP", "ESP"], + "RBP" => vec!["RBP", "EBP"], + _ => vec![base], + } +} + fn normalize_core_summary_name(name: &str) -> Option<&'static str> { let normalized_owned = name.trim().to_ascii_lowercase(); let mut normalized = normalized_owned.as_str(); @@ -504,6 +534,11 @@ fn normalize_core_summary_name(name: &str) -> Option<&'static str> { "free" => Some("free"), "puts" => Some("puts"), "printf" | "__printf_chk" => Some("printf"), + "read" | "__read_chk" => Some("read"), + "isatty" => Some("isatty"), + "sleep" => Some("sleep"), + "usleep" => Some("usleep"), + "nanosleep" => Some("nanosleep"), "exit" | "_exit" => Some("exit"), _ => { if normalized.starts_with("strlen") { @@ -518,6 +553,16 @@ fn normalize_core_summary_name(name: &str) -> Option<&'static str> { Some("memset") } else if normalized.starts_with("printf") || normalized == "__printf_chk" { Some("printf") + } else if normalized.starts_with("read") { + Some("read") + } else if normalized.starts_with("isatty") { + Some("isatty") + } else if normalized == "sleep" || normalized.ends_with("sleep") { + Some("sleep") + } else if normalized.starts_with("usleep") { + Some("usleep") + } else if normalized.starts_with("nanosleep") { + Some("nanosleep") } else if normalized.starts_with("puts") { Some("puts") } else if normalized == "malloc" || normalized.ends_with("malloc") { @@ -896,6 +941,141 @@ impl<'ctx> FunctionSummary<'ctx> for PrintfSummaryBasic { } } +/// read(fd, buf, count) summary. +#[derive(Default)] +pub struct ReadSummary; + +impl ReadSummary { + pub fn new() -> Self { + Self + } +} + +impl<'ctx> FunctionSummary<'ctx> for ReadSummary { + fn name(&self) -> &'static str { + "read" + } + + fn arity(&self) -> usize { + 3 + } + + fn execute(&self, state: &mut SymState<'ctx>, call: &CallInfo<'ctx>) -> SummaryEffect<'ctx> { + let fd = call + .args + .first() + .and_then(SymValue::as_concrete) + .map(|value| value as i32); + let buf = call + .args + .get(1) + .cloned() + .unwrap_or_else(|| SymValue::unknown(call.arg_bits)); + let count = call + .args + .get(2) + .and_then(SymValue::as_concrete) + .unwrap_or(0) as usize; + + let Some(fd) = fd else { + let ret = SymValue::unknown(call.ret_bits); + return SummaryEffect::Return(Some(ret)); + }; + + let Some(bytes) = state.read_symbolic_fd_bytes(fd, count) else { + return SummaryEffect::Return(Some(SymValue::concrete(0, call.ret_bits))); + }; + + if let Some(base) = buf.as_concrete() { + for (idx, byte) in bytes.iter().enumerate() { + let addr = SymValue::concrete(base.saturating_add(idx as u64), call.arg_bits); + state.mem_write(&addr, byte, 1); + } + } + + SummaryEffect::Return(Some(SymValue::concrete(bytes.len() as u64, call.ret_bits))) + } +} + +/// isatty(fd) summary. +#[derive(Default)] +pub struct IsattySummary; + +impl IsattySummary { + pub fn new() -> Self { + Self + } +} + +impl<'ctx> FunctionSummary<'ctx> for IsattySummary { + fn name(&self) -> &'static str { + "isatty" + } + + fn arity(&self) -> usize { + 1 + } + + fn execute(&self, state: &mut SymState<'ctx>, call: &CallInfo<'ctx>) -> SummaryEffect<'ctx> { + let fd = call + .args + .first() + .and_then(SymValue::as_concrete) + .map(|value| value as i32) + .unwrap_or(-1); + let ret = if state.is_tty_fd(fd) { 1 } else { 0 }; + SummaryEffect::Return(Some(SymValue::concrete(ret, call.ret_bits))) + } +} + +/// sleep/usleep/nanosleep summary. +pub struct SleepSummary { + name: &'static str, + arity: usize, +} + +impl SleepSummary { + pub fn sleep() -> Self { + Self { + name: "sleep", + arity: 1, + } + } + + pub fn usleep() -> Self { + Self { + name: "usleep", + arity: 1, + } + } + + pub fn nanosleep() -> Self { + Self { + name: "nanosleep", + arity: 2, + } + } +} + +impl<'ctx> FunctionSummary<'ctx> for SleepSummary { + fn name(&self) -> &'static str { + self.name + } + + fn arity(&self) -> usize { + self.arity + } + + fn execute(&self, state: &mut SymState<'ctx>, call: &CallInfo<'ctx>) -> SummaryEffect<'ctx> { + let ret = if state.skip_sleep_calls() { + SymValue::concrete(0, call.ret_bits) + } else { + SymValue::unknown(call.ret_bits) + }; + SummaryEffect::Return(Some(ret)) + } +} + /// malloc(size) summary. #[derive(Default)] pub struct MallocSummary; @@ -1073,3 +1253,16 @@ fn constrain_ret_tristate<'ctx>(state: &mut SymState<'ctx>, ret: &SymValue<'ctx> let cond = ret_bv.eq(&neg_one) | ret_bv.eq(&zero) | ret_bv.eq(&one); state.add_constraint(cond); } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn callconv_accepts_x86_64_arch_specs_with_bit_sized_addr_width() { + let mut arch = ArchSpec::new("x86-64"); + arch.addr_size = 64; + assert!(CallConv::for_arch_spec(&arch).is_some()); + assert!(SummaryRegistry::with_core_for_arch(&arch).is_some()); + } +} diff --git a/crates/r2sym/src/spec.rs b/crates/r2sym/src/spec.rs new file mode 100644 index 0000000..4987f3a --- /dev/null +++ b/crates/r2sym/src/spec.rs @@ -0,0 +1,354 @@ +use serde::{Deserialize, Serialize}; + +use crate::path::{ExploreConfig, ExploreStrategy}; +use crate::state::SymState; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(untagged)] +pub enum AddressValue { + Integer(u64), + String(String), +} + +impl AddressValue { + pub fn parse(&self) -> Result { + match self { + Self::Integer(value) => Ok(*value), + Self::String(text) => parse_address_text(text), + } + } +} + +fn parse_address_text(text: &str) -> Result { + let trimmed = text.trim(); + let has_minus = trimmed.char_indices().skip(1).any(|(_, ch)| ch == '-'); + if trimmed.contains('+') || has_minus { + return parse_address_expression(trimmed); + } + parse_address_atom(trimmed) +} + +fn parse_address_expression(text: &str) -> Result { + let mut expr = text.trim(); + let mut total = if let Some(rest) = expr.strip_prefix('-') { + expr = rest.trim_start(); + 0i128 + } else { + let (head, tail) = split_next_term(expr); + expr = tail; + parse_address_atom(head)? as i128 + }; + + while !expr.is_empty() { + let op = expr + .chars() + .next() + .ok_or_else(|| format!("invalid address expression: {}", text))?; + if op != '+' && op != '-' { + return Err(format!("invalid address expression: {}", text)); + } + let remainder = expr[op.len_utf8()..].trim_start(); + let (term, tail) = split_next_term(remainder); + let value = parse_address_atom(term)? as i128; + total = if op == '+' { + total + value + } else { + total - value + }; + expr = tail; + } + + u64::try_from(total).map_err(|_| format!("address expression underflow/overflow: {}", text)) +} + +fn split_next_term(text: &str) -> (&str, &str) { + let mut depth = 0usize; + for (idx, ch) in text.char_indices() { + match ch { + '(' => depth += 1, + ')' => depth = depth.saturating_sub(1), + '+' | '-' if depth == 0 && idx > 0 => { + return (text[..idx].trim_end(), text[idx..].trim_start()); + } + _ => {} + } + } + (text.trim(), "") +} + +fn parse_address_atom(text: &str) -> Result { + let trimmed = text.trim().trim_matches(|ch| ch == '(' || ch == ')'); + if let Some(hex) = trimmed + .strip_prefix("0x") + .or_else(|| trimmed.strip_prefix("0X")) + { + return u64::from_str_radix(hex, 16) + .map_err(|_| format!("invalid hex address: {}", trimmed)); + } + trimmed + .parse::() + .or_else(|_| u64::from_str_radix(trimmed, 16)) + .map_err(|_| format!("invalid address: {}", trimmed)) +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum StartSpec { + #[default] + Entry, + Address { + addr: AddressValue, + }, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum PredicateSpec { + Address { addr: AddressValue }, + AddressSet { addrs: Vec }, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum InputSpec { + Fd { + fd: i32, + len: usize, + #[serde(default)] + name: Option, + #[serde(default)] + alphabet: Option, + }, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] +pub struct BudgetSpec { + #[serde(default)] + pub max_states: Option, + #[serde(default)] + pub max_depth: Option, + #[serde(default)] + pub max_finds: Option, + #[serde(default)] + pub timeout_ms: Option, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] +#[serde(rename_all = "snake_case")] +pub enum StrategySpec { + #[default] + Dfs, + Bfs, + Random, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] +#[serde(rename_all = "snake_case")] +pub enum MergeSpec { + #[default] + Off, + SamePc, +} + +fn default_skip_sleep_calls() -> bool { + true +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct RuntimeSpec { + #[serde(default)] + pub tty_fds: Vec, + #[serde(default = "default_skip_sleep_calls")] + pub skip_sleep_calls: bool, +} + +impl Default for RuntimeSpec { + fn default() -> Self { + Self { + tty_fds: Vec::new(), + skip_sleep_calls: default_skip_sleep_calls(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] +pub struct ExplorationSpec { + #[serde(default)] + pub start: StartSpec, + #[serde(default)] + pub find: Vec, + #[serde(default)] + pub avoid: Vec, + #[serde(default)] + pub inputs: Vec, + #[serde(default)] + pub budget: BudgetSpec, + #[serde(default)] + pub strategy: StrategySpec, + #[serde(default)] + pub merge: MergeSpec, + #[serde(default)] + pub runtime: RuntimeSpec, +} + +impl ExplorationSpec { + pub fn validate(&self) -> Result<(), String> { + if self.find.is_empty() { + return Err("exploration spec requires at least one find predicate".to_string()); + } + for input in &self.inputs { + match input { + InputSpec::Fd { + len, + alphabet, + name, + .. + } => { + if *len == 0 { + return Err("fd input length must be greater than zero".to_string()); + } + if let Some(alphabet) = alphabet + && alphabet.is_empty() + { + return Err("fd input alphabet must not be empty".to_string()); + } + if let Some(name) = name + && name.trim().is_empty() + { + return Err("fd input name must not be empty".to_string()); + } + } + } + } + let _ = self.find_addresses()?; + let _ = self.avoid_addresses()?; + Ok(()) + } + + pub fn start_pc(&self, entry_pc: u64) -> Result { + match &self.start { + StartSpec::Entry => Ok(entry_pc), + StartSpec::Address { addr } => addr.parse(), + } + } + + pub fn max_finds(&self) -> usize { + self.budget.max_finds.unwrap_or(1).max(1) + } + + pub fn to_explore_config(&self, defaults: &ExploreConfig) -> ExploreConfig { + let mut config = defaults.clone(); + if let Some(max_states) = self.budget.max_states { + config.max_states = max_states; + } + if let Some(max_depth) = self.budget.max_depth { + config.max_depth = max_depth; + } + if let Some(timeout_ms) = self.budget.timeout_ms { + config.timeout = Some(std::time::Duration::from_millis(timeout_ms)); + } + config.strategy = match self.strategy { + StrategySpec::Dfs => ExploreStrategy::Dfs, + StrategySpec::Bfs => ExploreStrategy::Bfs, + StrategySpec::Random => ExploreStrategy::Random, + }; + config.merge_states = matches!(self.merge, MergeSpec::SamePc); + config + } + + pub fn find_addresses(&self) -> Result, String> { + flatten_predicates(&self.find) + } + + pub fn avoid_addresses(&self) -> Result, String> { + flatten_predicates(&self.avoid) + } + + pub fn apply_to_state<'ctx>(&self, state: &mut SymState<'ctx>) { + for fd in &self.runtime.tty_fds { + state.set_tty_fd(*fd, true); + } + state.set_skip_sleep_calls(self.runtime.skip_sleep_calls); + for input in &self.inputs { + match input { + InputSpec::Fd { + fd, + len, + name, + alphabet, + } => { + let default_name = format!("fd{}_input", fd); + state.add_symbolic_fd_input( + *fd, + *len, + name.as_deref().unwrap_or(&default_name), + alphabet.as_deref(), + ); + } + } + } + } +} + +fn flatten_predicates(predicates: &[PredicateSpec]) -> Result, String> { + let mut out = Vec::new(); + for predicate in predicates { + match predicate { + PredicateSpec::Address { addr } => out.push(addr.parse()?), + PredicateSpec::AddressSet { addrs } => { + for addr in addrs { + out.push(addr.parse()?); + } + } + } + } + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_hex_addresses() { + assert_eq!( + AddressValue::String("0x401000".to_string()) + .parse() + .unwrap(), + 0x401000 + ); + } + + #[test] + fn parses_simple_address_expressions() { + assert_eq!( + AddressValue::String("0x401000 + 0x20 - 4".to_string()) + .parse() + .unwrap(), + 0x40101c + ); + } + + #[test] + fn validates_fd_inputs() { + let spec = ExplorationSpec { + find: vec![PredicateSpec::Address { + addr: AddressValue::Integer(0x401000), + }], + inputs: vec![InputSpec::Fd { + fd: 0, + len: 4, + name: Some("stdin".to_string()), + alphabet: Some("abcd".to_string()), + }], + ..Default::default() + }; + assert!(spec.validate().is_ok()); + } + + #[test] + fn rejects_empty_find_list() { + let spec = ExplorationSpec::default(); + assert!(spec.validate().is_err()); + } +} diff --git a/crates/r2sym/src/state.rs b/crates/r2sym/src/state.rs index 69123a8..adef9d3 100644 --- a/crates/r2sym/src/state.rs +++ b/crates/r2sym/src/state.rs @@ -24,6 +24,28 @@ pub struct SymbolicMemoryRegion<'ctx> { pub value: SymValue<'ctx>, } +/// A tracked symbolic input stream for a file descriptor. +#[derive(Debug, Clone)] +pub struct SymbolicFdInput<'ctx> { + /// Stable user-facing name of the stream. + pub name: String, + /// File descriptor identifier. + pub fd: i32, + /// Symbolic bytes available to the runtime model. + pub bytes: Vec>, + /// Current read cursor. + pub cursor: usize, +} + +/// Runtime policy carried with each symbolic state. +#[derive(Debug, Clone, Default)] +pub struct RuntimeState { + /// File descriptors that should report tty=true via isatty(). + pub tty_fds: HashSet, + /// Whether sleep-family calls should become zero-cost no-ops. + pub skip_sleep_calls: bool, +} + /// The state of a symbolic execution. /// /// Contains registers, memory, path constraints, and program counter. @@ -50,6 +72,10 @@ pub struct SymState<'ctx> { symbolic_inputs: HashMap>, /// Tracked symbolic memory regions. symbolic_memory: Vec>, + /// Symbolic external input streams keyed by file descriptor. + symbolic_fd_inputs: HashMap>, + /// Runtime policy/state for summaries. + runtime: RuntimeState, } /// Exit status of a symbolic execution path. @@ -84,6 +110,8 @@ impl<'ctx> SymState<'ctx> { depth: 0, symbolic_inputs: HashMap::new(), symbolic_memory: Vec::new(), + symbolic_fd_inputs: HashMap::new(), + runtime: RuntimeState::default(), } } @@ -101,6 +129,8 @@ impl<'ctx> SymState<'ctx> { depth: 0, symbolic_inputs: HashMap::new(), symbolic_memory: Vec::new(), + symbolic_fd_inputs: HashMap::new(), + runtime: RuntimeState::default(), } } @@ -176,6 +206,16 @@ impl<'ctx> SymState<'ctx> { &self.symbolic_memory } + /// Get tracked symbolic file-descriptor inputs. + pub fn symbolic_fd_inputs(&self) -> &HashMap> { + &self.symbolic_fd_inputs + } + + /// Get runtime policy/state. + pub fn runtime(&self) -> &RuntimeState { + &self.runtime + } + /// Read from memory. pub fn mem_read(&self, addr: &SymValue<'ctx>, size: u32) -> SymValue<'ctx> { self.memory @@ -287,6 +327,28 @@ impl<'ctx> SymState<'ctx> { } } + merged.symbolic_fd_inputs = self.symbolic_fd_inputs.clone(); + for (fd, other_input) in &other.symbolic_fd_inputs { + match merged.symbolic_fd_inputs.get_mut(fd) { + Some(existing) => { + if existing.bytes.len() < other_input.bytes.len() { + existing.bytes = other_input.bytes.clone(); + } + existing.cursor = existing.cursor.max(other_input.cursor); + } + None => { + merged.symbolic_fd_inputs.insert(*fd, other_input.clone()); + } + } + } + + merged.runtime = self.runtime.clone(); + merged.runtime.skip_sleep_calls |= other.runtime.skip_sleep_calls; + merged + .runtime + .tty_fds + .extend(other.runtime.tty_fds.iter().copied()); + merged } @@ -473,6 +535,8 @@ impl<'ctx> SymState<'ctx> { depth: self.depth, symbolic_inputs: self.symbolic_inputs.clone(), symbolic_memory: self.symbolic_memory.clone(), + symbolic_fd_inputs: self.symbolic_fd_inputs.clone(), + runtime: self.runtime.clone(), } } @@ -531,6 +595,88 @@ impl<'ctx> SymState<'ctx> { }); value } + + /// Configure tty behavior for a concrete file descriptor. + pub fn set_tty_fd(&mut self, fd: i32, is_tty: bool) { + if is_tty { + self.runtime.tty_fds.insert(fd); + } else { + self.runtime.tty_fds.remove(&fd); + } + } + + /// Check whether a file descriptor should behave like a tty. + pub fn is_tty_fd(&self, fd: i32) -> bool { + self.runtime.tty_fds.contains(&fd) + } + + /// Configure whether sleep-family calls should be skipped. + pub fn set_skip_sleep_calls(&mut self, enabled: bool) { + self.runtime.skip_sleep_calls = enabled; + } + + /// Check whether sleep-family calls should be skipped. + pub fn skip_sleep_calls(&self) -> bool { + self.runtime.skip_sleep_calls + } + + /// Register a symbolic byte stream for a file descriptor. + pub fn add_symbolic_fd_input( + &mut self, + fd: i32, + len: usize, + name: &str, + alphabet: Option<&str>, + ) { + let mut bytes = Vec::with_capacity(len); + for idx in 0..len { + let byte_name = format!("{}_{}", name, idx); + let byte = SymValue::new_symbolic(self.ctx, &byte_name, 8); + if let Some(alphabet) = alphabet { + constrain_symbolic_byte_to_alphabet(self, &byte, alphabet); + } + self.symbolic_inputs.insert(byte_name, byte.clone()); + bytes.push(byte); + } + self.symbolic_fd_inputs.insert( + fd, + SymbolicFdInput { + name: name.to_string(), + fd, + bytes, + cursor: 0, + }, + ); + } + + /// Read up to `count` bytes from a tracked symbolic file descriptor. + pub fn read_symbolic_fd_bytes(&mut self, fd: i32, count: usize) -> Option>> { + let input = self.symbolic_fd_inputs.get_mut(&fd)?; + if input.cursor >= input.bytes.len() { + return Some(Vec::new()); + } + let end = input.cursor.saturating_add(count).min(input.bytes.len()); + let bytes = input.bytes[input.cursor..end].to_vec(); + input.cursor = end; + Some(bytes) + } +} + +fn constrain_symbolic_byte_to_alphabet<'ctx>( + state: &mut SymState<'ctx>, + value: &SymValue<'ctx>, + alphabet: &str, +) { + let allowed = alphabet.as_bytes(); + if allowed.is_empty() { + return; + } + let byte_bv = value.to_bv(state.context()); + let ors: Vec = allowed + .iter() + .map(|byte| byte_bv.eq(BV::from_u64(*byte as u64, 8))) + .collect(); + state.add_constraint(or_all(state.context(), &ors)); } fn parse_byte_ranges(pattern: &str) -> Vec<(u8, u8)> { diff --git a/crates/r2sym/tests/symex_integration.rs b/crates/r2sym/tests/symex_integration.rs index 74bf18e..389fd0e 100644 --- a/crates/r2sym/tests/symex_integration.rs +++ b/crates/r2sym/tests/symex_integration.rs @@ -3,7 +3,7 @@ //! These tests verify the symbolic execution engine works correctly //! with real SSA functions and Z3 constraint solving. -use r2il::{R2ILBlock, R2ILOp, SpaceId, Varnode}; +use r2il::{ArchSpec, R2ILBlock, R2ILOp, RegisterDef, SpaceId, Varnode}; use r2ssa::SsaArtifact; use r2sym::SymSolver; use r2sym::path::ExploreStrategy; @@ -11,6 +11,7 @@ use r2sym::sim::{ CallInfo, FunctionSummary, MemcmpSummary, MemsetSummary, PrintfSummaryBasic, PutsSummary, SummaryRegistry, }; +use r2sym::spec::{AddressValue, ExplorationSpec, InputSpec, PredicateSpec}; use r2sym::{ExploreConfig, PathExplorer, SymState, SymValue}; use z3::Context; @@ -33,11 +34,26 @@ fn make_const(val: u64, size: u32) -> Varnode { } } +fn make_x86_64_arch() -> ArchSpec { + let mut arch = ArchSpec::new("x86-64"); + arch.addr_size = 8; + arch.add_register(RegisterDef::new("RAX", RAX, 8)); + arch.add_register(RegisterDef::new("RCX", RCX, 8)); + arch.add_register(RegisterDef::new("RDI", RDI, 8)); + arch.add_register(RegisterDef::new("RSI", RSI, 8)); + arch.add_register(RegisterDef::new("RDX", RDX, 8)); + arch +} + // Simulated x86-64 register offsets const RAX: u64 = 0; const RBX: u64 = 8; const RCX: u64 = 16; const RDI: u64 = 56; +const RSI: u64 = 64; +const RDX: u64 = 72; +const TMP0: u64 = 0x80; +const TMP1: u64 = 0x88; #[test] fn test_symbolic_execution_linear_block() { @@ -720,6 +736,115 @@ fn registry_with_core_contains_new_summaries() { assert!(registry.install_for_explorer(&mut explorer, 0x1003, "printf")); } +#[test] +fn run_spec_fd_read_flows_into_load_compare_and_solution() { + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(RDI, 8), + src: make_const(0, 8), + }, + R2ILOp::Copy { + dst: make_reg(RSI, 8), + src: make_const(0x2000, 8), + }, + R2ILOp::Copy { + dst: make_reg(RDX, 8), + src: make_const(1, 8), + }, + R2ILOp::Call { + target: make_const(0x5000, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP0, 1), + addr: make_const(0x2000, 8), + space: SpaceId::Ram, + }, + R2ILOp::IntEqual { + dst: make_reg(TMP1, 1), + a: make_reg(TMP0, 1), + b: make_const(b'k' as u64, 1), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP1, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0x1337, 8), + }], + }, + ]; + + let arch = make_x86_64_arch(); + let func = + SsaArtifact::for_symbolic(&blocks, Some(&arch)).expect("Failed to build SSA function"); + let ctx = Context::thread_local(); + let mut initial_state = SymState::new(&ctx, 0x1000); + let mut explorer = PathExplorer::new(&ctx); + let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); + assert!(registry.install_for_explorer(&mut explorer, 0x5000, "read")); + + let spec = ExplorationSpec { + find: vec![PredicateSpec::Address { + addr: AddressValue::Integer(0x1010), + }], + inputs: vec![InputSpec::Fd { + fd: 0, + len: 1, + name: Some("stdin0".to_string()), + alphabet: Some("k".to_string()), + }], + ..Default::default() + }; + spec.apply_to_state(&mut initial_state); + + let result = explorer + .run_spec(&func, initial_state, &spec) + .expect("spec exploration should succeed"); + assert_eq!(result.found_paths.len(), 1, "expected one matching path"); + + let solved = explorer + .solve_path(&result.found_paths[0]) + .expect("found path should solve"); + assert_eq!(solved.final_pc, 0x1010); + assert_eq!( + solved.input_buffers.get("stdin0"), + Some(&vec![b'k']), + "solver should recover the byte that drives the branch" + ); +} + #[test] fn symbolic_n_constraints_bounded_for_memset_memcmp() { let ctx = Context::thread_local(); diff --git a/r2plugin/r_anal_sleigh.c b/r2plugin/r_anal_sleigh.c index e5cc9ba..6c45b65 100644 --- a/r2plugin/r_anal_sleigh.c +++ b/r2plugin/r_anal_sleigh.c @@ -83,6 +83,8 @@ extern char *r2sym_explore_to(const R2ILContext *ctx, const R2ILBlock **blocks, unsigned long long entry_addr, unsigned long long target_addr); extern char *r2sym_solve_to(const R2ILContext *ctx, const R2ILBlock **blocks, size_t num_blocks, unsigned long long entry_addr, unsigned long long target_addr); +extern char *r2sym_run_spec_json(const R2ILContext *ctx, const R2ILBlock **blocks, size_t num_blocks, + unsigned long long entry_addr, const char *spec_json); extern int r2sym_set_symbol_map_json(const char *json); extern int r2sym_merge_is_enabled(void); extern void r2sym_merge_set_enabled(int enabled); @@ -4127,6 +4129,7 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { r_cons_println (cons, "| a:sla.cfg.json - Show CFG as JSON for current function"); r_cons_println (cons, "| a:sym.explore - Explore symbolic paths reaching target"); r_cons_println (cons, "| a:sym.solve - Solve concrete input for target reachability"); + r_cons_println (cons, "| a:sym.runj - Run typed symbolic exploration spec"); r_cons_println (cons, "| a:sym.state - Show last symbolic explore/solve cached result"); } return strdup(""); @@ -4141,6 +4144,70 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { return strdup(""); } + if (is_sym_ns && !strncmp (cmd, "sym.runj", 8)) { + const char *arg = skip_cmd_spaces (cmd + 8); + R2ILContext *ctx; + RAnalFunction *fcn; + BlockArray blocks; + char *spec_json = NULL; + char *result = NULL; + bool rust_owned = true; + + if (!arg || !*arg) { + if (cons) { + r_cons_println (cons, "Usage: a:sym.runj "); + } + return strdup(""); + } + + ctx = get_context (anal); + if (!ctx) { + R_LOG_ERROR ("r2sleigh: no context"); + return strdup(""); + } + fcn = r_anal_get_fcn_in (anal, core->addr, R_ANAL_FCN_TYPE_ANY); + if (!fcn) { + R_LOG_ERROR ("r2sleigh: no function at current address"); + return strdup(""); + } + if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { + R_LOG_ERROR ("r2sleigh: failed to lift function blocks"); + return strdup(""); + } + char *sym_map_json = build_sym_symbol_map_json (core); + if (sym_map_json) { + r2sym_set_symbol_map_json (sym_map_json); + free (sym_map_json); + } + + spec_json = strdup (arg); + if (!spec_json) { + block_array_free (&blocks); + return strdup(""); + } + r_str_unescape (spec_json); + result = r2sym_run_spec_json (ctx, (const R2ILBlock **)blocks.blocks, blocks.count, fcn->addr, spec_json); + free (spec_json); + if (!result) { + rust_owned = false; + result = strdup ("{\"error\":\"symbolic execution failed\"}"); + } + + if (cons && result) { + r_cons_printf (cons, "%s\n", result); + } + if (result && !sym_result_has_error (result)) { + sym_state_cache_update ("runj", fcn->addr, fcn->addr, 0, result); + } + if (rust_owned) { + r2il_string_free (result); + } else { + free (result); + } + block_array_free (&blocks); + return strdup(""); + } + if (is_sym_ns && (!strncmp (cmd, "sym.explore", 11) || !strncmp (cmd, "sym.solve", 9))) { bool is_explore = r_str_startswith (cmd, "sym.explore"); size_t prefix_len = is_explore ? 11 : 9; diff --git a/r2plugin/src/analysis/sym.rs b/r2plugin/src/analysis/sym.rs index c61e0e5..699a990 100644 --- a/r2plugin/src/analysis/sym.rs +++ b/r2plugin/src/analysis/sym.rs @@ -191,6 +191,19 @@ struct PathSolution { registers: std::collections::HashMap, } +#[derive(Serialize)] +struct RunPathSolution { + inputs: std::collections::HashMap, + input_buffers: std::collections::HashMap, + registers: std::collections::HashMap, +} + +#[derive(Serialize)] +struct BufferSolution { + hex: String, + ascii: String, +} + #[derive(Serialize)] struct SymTargetExploreResult { entry: String, @@ -211,6 +224,37 @@ struct SymTargetSolveResult { selected_path: Option, } +#[derive(Serialize)] +struct SymRunPathInfo { + path_id: usize, + feasible: bool, + depth: usize, + exit_status: String, + final_pc: String, + num_constraints: usize, + #[serde(skip_serializing_if = "Option::is_none")] + solution: Option, +} + +#[derive(Serialize)] +struct SymRunStashCounts { + found: usize, + avoided: usize, + unsat: usize, + errored: usize, + completed: usize, +} + +#[derive(Serialize)] +struct SymRunResult { + entry: String, + spec: r2sym::ExplorationSpec, + stats: SymExecSummary, + stash_counts: SymRunStashCounts, + found_paths: Vec, + diagnostics: Vec, +} + fn path_solution_from_result<'ctx>( explorer: &r2sym::PathExplorer<'ctx>, result: &r2sym::PathResult<'ctx>, @@ -233,6 +277,51 @@ fn path_solution_from_result<'ctx>( }) } +fn buffer_solution(bytes: Vec) -> BufferSolution { + let hex = bytes + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::(); + let ascii = bytes + .iter() + .map(|byte| { + if byte.is_ascii_graphic() || *byte == b' ' { + *byte as char + } else { + '.' + } + }) + .collect(); + BufferSolution { hex, ascii } +} + +fn run_path_solution_from_result<'ctx>( + explorer: &r2sym::PathExplorer<'ctx>, + result: &r2sym::PathResult<'ctx>, +) -> Option { + if !result.feasible { + return None; + } + explorer.solve_path(result).map(|solved| RunPathSolution { + inputs: solved + .inputs + .into_iter() + .map(|(k, v)| (k, format!("0x{:x}", v))) + .collect(), + input_buffers: solved + .input_buffers + .into_iter() + .map(|(k, v)| (k, buffer_solution(v))) + .collect(), + registers: solved + .registers + .into_iter() + .filter(|(name, _)| !name.starts_with("tmp:") && !name.contains("_0")) + .map(|(k, v)| (k, format!("0x{:x}", v))) + .collect(), + }) +} + fn path_info_from_result<'ctx>( path_id: usize, result: &r2sym::PathResult<'ctx>, @@ -249,6 +338,22 @@ fn path_info_from_result<'ctx>( } } +fn run_path_info_from_result<'ctx>( + path_id: usize, + result: &r2sym::PathResult<'ctx>, + explorer: &r2sym::PathExplorer<'ctx>, +) -> SymRunPathInfo { + SymRunPathInfo { + path_id, + feasible: result.feasible, + depth: result.depth, + exit_status: format!("{:?}", result.exit_status), + final_pc: format!("0x{:x}", result.final_pc()), + num_constraints: result.num_constraints(), + solution: run_path_solution_from_result(explorer, result), + } +} + fn build_symbolic_prepared( blocks: &[R2ILBlock], arch: Option<&ArchSpec>, @@ -263,6 +368,22 @@ fn symbol_map_snapshot() -> HashMap { .unwrap_or_default() } +fn install_symbolic_hooks<'ctx>( + explorer: &mut r2sym::PathExplorer<'ctx>, + prepared: &r2ssa::SsaArtifact, + arch: Option<&ArchSpec>, + symbol_map: &HashMap, +) { + if let Some(arch) = arch + && let Some(registry) = r2sym::SummaryRegistry::with_core_for_arch(arch) + { + let interproc = build_seed_interproc_summary_set(prepared, symbol_map); + let _ = registry.install_known_symbols_for_function(explorer, prepared, symbol_map); + let _ = registry + .install_interproc_summaries_for_function(explorer, prepared, &interproc, symbol_map); + } +} + #[unsafe(no_mangle)] pub extern "C" fn r2sym_function( ctx: *const R2ILContext, @@ -556,3 +677,97 @@ pub extern "C" fn r2sym_solve_to( Err(_) => sym_error_json("failed to serialize symbolic solve output"), } } + +#[unsafe(no_mangle)] +pub extern "C" fn r2sym_run_spec_json( + ctx: *const R2ILContext, + blocks: *const *const R2ILBlock, + num_blocks: usize, + entry_addr: u64, + spec_json: *const c_char, +) -> *mut c_char { + if spec_json.is_null() { + return sym_error_json("missing exploration spec json"); + } + let spec_text = unsafe { + match CStr::from_ptr(spec_json).to_str() { + Ok(text) => text, + Err(_) => return sym_error_json("exploration spec is not valid utf-8"), + } + }; + let spec = match serde_json::from_str::(spec_text) { + Ok(spec) => spec, + Err(err) => return sym_error_json(&format!("failed to parse exploration spec: {err}")), + }; + if let Err(err) = spec.validate() { + return sym_error_json(&err); + } + + let Some(ctx_view) = require_ctx_view(ctx) else { + return sym_error_json("missing disassembler context"); + }; + let Some(blocks) = (unsafe { BlockSlice::from_ffi(blocks, num_blocks) }) else { + return sym_error_json("no blocks to explore"); + }; + + let prepared = match build_symbolic_prepared(blocks.as_slice(), ctx_view.arch) { + Some(prepared) => prepared, + None => return sym_error_json("failed to build SSA function"), + }; + let symbol_map = symbol_map_snapshot(); + let z3_ctx = Context::thread_local(); + let default_config = sym_default_config(); + + let run_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let start_pc = spec.start_pc(entry_addr)?; + let mut initial_state = r2sym::SymState::new(&z3_ctx, start_pc); + r2sym::seed_default_state_for_arch(&mut initial_state, &prepared, ctx_view.arch); + spec.apply_to_state(&mut initial_state); + + let mut explorer = + r2sym::PathExplorer::with_config(&z3_ctx, spec.to_explore_config(&default_config)); + install_symbolic_hooks(&mut explorer, &prepared, ctx_view.arch, &symbol_map); + let result = explorer.run_spec(&prepared, initial_state, &spec)?; + let stats = explorer.stats().clone(); + let found_paths = result + .found_paths + .iter() + .enumerate() + .map(|(idx, path)| run_path_info_from_result(idx, path, &explorer)) + .collect::>(); + Ok::<_, String>((result, stats, found_paths)) + })); + + let (result, stats, found_paths) = match run_result { + Ok(Ok(value)) => value, + Ok(Err(err)) => return sym_error_json(&err), + Err(_) => return sym_error_json("symbolic execution failed (z3 context error)"), + }; + + let output = SymRunResult { + entry: format!("0x{:x}", entry_addr), + spec, + stats: SymExecSummary { + paths_explored: stats.paths_completed, + paths_feasible: found_paths.len(), + paths_pruned: stats.paths_pruned, + max_depth: stats.max_depth_reached, + states_explored: stats.states_explored, + time_ms: stats.total_time.as_millis() as u64, + }, + stash_counts: SymRunStashCounts { + found: found_paths.len(), + avoided: result.avoided_states, + unsat: result.unsat_states, + errored: result.errored_states, + completed: result.completed_states, + }, + found_paths, + diagnostics: result.diagnostics, + }; + + match serde_json::to_string(&output) { + Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), + Err(_) => sym_error_json("failed to serialize symbolic run output"), + } +} diff --git a/tests/e2e/vuln_test.c b/tests/e2e/vuln_test.c index aa4403b..067a281 100644 --- a/tests/e2e/vuln_test.c +++ b/tests/e2e/vuln_test.c @@ -20,6 +20,7 @@ #include #include #include +#include volatile int global_counter = 0; volatile int global_limit = 10; @@ -476,6 +477,23 @@ int test_bool_carrier_chain(int x, int y) { return x; } +// Test 44: Stdin-driven gate for typed symbolic fd input specs. +__attribute__((noinline)) int stdin_gate(void) { + char buf[2] = {0}; + ssize_t n = read(0, buf, 1); + if (n != 1) { + return 0; + } + if (isatty(0)) { + global_tail++; + } + usleep(1000); + if (buf[0] == 'k') { + return 0x1337; + } + return 0; +} + int main(int argc, char *argv[]) { if (argc < 2) { printf("Usage: %s [args...]\n", argv[0]); @@ -822,6 +840,9 @@ int main(int argc, char *argv[]) { ); } break; + case 44: + printf("stdin_gate() = %d\n", stdin_gate()); + break; default: printf("Unknown test: %d\n", test); return 1; diff --git a/tests/r2r/db/extras/r2sleigh_integration_extended b/tests/r2r/db/extras/r2sleigh_integration_extended index b7e0558..38f294f 100644 --- a/tests/r2r/db/extras/r2sleigh_integration_extended +++ b/tests/r2r/db/extras/r2sleigh_integration_extended @@ -458,6 +458,32 @@ a:sym.solve `afl~check_secret$[0]`+0x14 | jq -c '.found==true' EOF_CMDS RUN +NAME=interactive_sym_runj_finds_check_secret_target +FILE=bins/vuln_test_x86 +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=</dev/null +a:sym.runj {\"find\":[{\"kind\":\"address\",\"addr\":\"`afl~check_secret$[0]`+0x14\"}],\"budget\":{\"max_states\":64,\"max_depth\":128,\"max_finds\":1,\"timeout_ms\":5000}} | jq -c '.stash_counts.found == 1 and ((.found_paths[0].solution.inputs | length) > 0)' +EOF_CMDS +RUN + +NAME=interactive_sym_runj_solves_stdin_gate +FILE=bins/vuln_test_x86 +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=</dev/null +a:sym.runj {\"find\":[{\"kind\":\"address\",\"addr\":\"`afl~stdin_gate$[0]`+0x69\"}],\"inputs\":[{\"kind\":\"fd\",\"fd\":0,\"len\":1,\"name\":\"stdin0\",\"alphabet\":\"k\"}],\"runtime\":{\"tty_fds\":[],\"skip_sleep_calls\":true},\"budget\":{\"max_states\":64,\"max_depth\":128,\"max_finds\":1,\"timeout_ms\":5000}} | jq -c '.stash_counts.found == 1 and .found_paths[0].solution.input_buffers.stdin0.hex == "6b" and .found_paths[0].solution.input_buffers.stdin0.ascii == "k"' +EOF_CMDS +RUN + NAME=merge_mode_on FILE=bins/vuln_test ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true From ef1520c508b532bf69348a6ec6bb86573c470ec7 Mon Sep 17 00:00:00 2001 From: Priyanshu Kumar Date: Thu, 26 Mar 2026 05:06:10 +0000 Subject: [PATCH 02/10] Enhance symbolic execution framework - Updated dependencies in Cargo.lock, including new packages such as `anes`, `cast`, `ciborium`, and `criterion`, along with their respective versions and checksums. - Introduced a new benchmark for symbolic execution in `r2sym`, allowing for performance evaluation of hot paths in symbolic execution. - Enhanced the `ExploreConfig` structure to include a `max_completed_paths` option, providing better control over exploration limits. - Added tests to validate the new exploration capabilities and ensure correct behavior in symbolic execution scenarios. This commit improves the dependency management and expands the symbolic execution framework's capabilities, facilitating more efficient exploration strategies. --- Cargo.lock | 229 ++- crates/r2sym/Cargo.toml | 7 +- crates/r2sym/benches/symex_hotpaths.rs | 447 ++++++ crates/r2sym/src/lib.rs | 5 +- crates/r2sym/src/memory.rs | 21 +- crates/r2sym/src/path.rs | 1002 +++++++----- crates/r2sym/src/sim.rs | 228 ++- crates/r2sym/src/solver.rs | 202 ++- crates/r2sym/tests/symex_integration.rs | 134 ++ r2plugin/r_anal_sleigh.c | 1420 +++++++++++++++++ r2plugin/src/analysis/sym.rs | 159 +- .../db/extras/r2sleigh_integration_extended | 35 +- 12 files changed, 3342 insertions(+), 547 deletions(-) create mode 100644 crates/r2sym/benches/symex_hotpaths.rs diff --git a/Cargo.lock b/Cargo.lock index 19d0209..bb1e1f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "anstream" version = "0.6.21" @@ -106,7 +112,7 @@ dependencies = [ "bitflags", "cexpr", "clang-sys", - "itertools", + "itertools 0.13.0", "proc-macro2", "quote", "regex", @@ -139,6 +145,12 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cc" version = "1.2.52" @@ -170,6 +182,33 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -247,12 +286,79 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + [[package]] name = "critical-section" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "cxx" version = "1.0.192" @@ -466,6 +572,17 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hash32" version = "0.2.1" @@ -510,6 +627,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -745,12 +868,32 @@ dependencies = [ "serde", ] +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -966,6 +1109,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1002,6 +1151,34 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + [[package]] name = "postcard" version = "1.1.3" @@ -1221,6 +1398,7 @@ dependencies = [ name = "r2sym" version = "0.1.0" dependencies = [ + "criterion", "r2il", "r2pipe", "r2ssa", @@ -1271,6 +1449,26 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "regex" version = "1.12.2" @@ -1416,6 +1614,15 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -1656,6 +1863,16 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tinyvec" version = "1.10.0" @@ -1807,6 +2024,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" diff --git a/crates/r2sym/Cargo.toml b/crates/r2sym/Cargo.toml index 73c7730..d6b2145 100644 --- a/crates/r2sym/Cargo.toml +++ b/crates/r2sym/Cargo.toml @@ -5,6 +5,10 @@ edition.workspace = true license.workspace = true description = "Symbolic execution engine for r2sleigh" +[[bench]] +name = "symex_hotpaths" +harness = false + [dependencies] r2il = { path = "../r2il" } r2ssa = { path = "../r2ssa" } @@ -19,4 +23,5 @@ default = [] r2 = ["r2pipe", "serde_json"] [dev-dependencies] -sleigh-config = { version = "1.0", features = ["x86"] } \ No newline at end of file +criterion = "0.5" +sleigh-config = { version = "1.0", features = ["x86"] } diff --git a/crates/r2sym/benches/symex_hotpaths.rs b/crates/r2sym/benches/symex_hotpaths.rs new file mode 100644 index 0000000..20e3a7a --- /dev/null +++ b/crates/r2sym/benches/symex_hotpaths.rs @@ -0,0 +1,447 @@ +use criterion::{Criterion, black_box, criterion_group, criterion_main}; +use r2il::{ArchSpec, R2ILBlock, R2ILOp, RegisterDef, SpaceId, Varnode}; +use r2ssa::SsaArtifact; +use r2sym::path::ExploreStrategy; +use r2sym::{ + AddressValue, ExplorationSpec, ExploreConfig, InputSpec, PathExplorer, PredicateSpec, + SummaryRegistry, SymSolver, SymState, SymValue, +}; +use z3::Context; + +const RAX: u64 = 0; +const RBX: u64 = 8; +const RCX: u64 = 16; +const RDI: u64 = 56; +const RSI: u64 = 64; +const RDX: u64 = 72; +const TMP0: u64 = 0x80; +const TMP1: u64 = 0x88; + +fn make_reg(offset: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Register, + offset, + size, + meta: None, + } +} + +fn make_const(val: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Const, + offset: val, + size, + meta: None, + } +} + +fn make_x86_64_arch() -> ArchSpec { + let mut arch = ArchSpec::new("x86-64"); + arch.addr_size = 8; + arch.add_register(RegisterDef::new("RAX", RAX, 8)); + arch.add_register(RegisterDef::new("RBX", RBX, 8)); + arch.add_register(RegisterDef::new("RCX", RCX, 8)); + arch.add_register(RegisterDef::new("RDI", RDI, 8)); + arch.add_register(RegisterDef::new("RSI", RSI, 8)); + arch.add_register(RegisterDef::new("RDX", RDX, 8)); + arch +} + +fn build_branching_function() -> SsaArtifact { + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![ + R2ILOp::IntEqual { + dst: make_reg(RCX, 1), + a: make_reg(RDI, 8), + b: make_const(0x1337, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(RCX, 1), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + + SsaArtifact::for_symbolic(&blocks, None).expect("branching benchmark SSA should build") +} + +fn build_join_heavy_function(levels: u64) -> SsaArtifact { + let mut blocks = Vec::new(); + let mut addr = 0x2000; + + for level in 0..levels { + let branch_addr = addr; + let false_addr = addr + 0x4; + let true_addr = addr + 0x8; + let join_addr = addr + 0xc; + + blocks.push(R2ILBlock { + addr: branch_addr, + size: 4, + ops: vec![ + R2ILOp::IntEqual { + dst: make_reg(RCX, 1), + a: make_reg(RDI, 8), + b: make_const(level, 8), + }, + R2ILOp::CBranch { + target: make_const(true_addr, 8), + cond: make_reg(RCX, 1), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }); + blocks.push(R2ILBlock { + addr: false_addr, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(join_addr, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }); + blocks.push(R2ILBlock { + addr: true_addr, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(join_addr, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }); + blocks.push(R2ILBlock { + addr: join_addr, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_reg(RAX, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }); + + addr += 0x10; + } + + blocks.push(R2ILBlock { + addr, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0x1337, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }); + + blocks.sort_by_key(|block| block.addr); + SsaArtifact::for_symbolic(&blocks, None).expect("join-heavy benchmark SSA should build") +} + +fn emit_branch_tree( + blocks: &mut Vec, + next_addr: &mut u64, + depth: u32, + seed: &mut u64, +) -> u64 { + let entry = *next_addr; + *next_addr += 4; + + if depth == 0 { + let leaf_value = *seed; + *seed += 1; + blocks.push(R2ILBlock { + addr: entry, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(leaf_value, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }); + return entry; + } + + let false_entry = emit_branch_tree(blocks, next_addr, depth - 1, seed); + let true_entry = emit_branch_tree(blocks, next_addr, depth - 1, seed); + let compare_value = *seed; + *seed += 1; + + debug_assert_eq!(false_entry, entry + 4); + + blocks.push(R2ILBlock { + addr: entry, + size: 4, + ops: vec![ + R2ILOp::IntEqual { + dst: make_reg(RCX, 1), + a: make_reg(RDI, 8), + b: make_const(compare_value, 8), + }, + R2ILOp::CBranch { + target: make_const(true_entry, 8), + cond: make_reg(RCX, 1), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }); + + entry +} + +fn build_branch_tree_function(levels: u32) -> SsaArtifact { + let mut blocks = Vec::new(); + let mut next_addr = 0x3000; + let mut seed = 0; + + emit_branch_tree(&mut blocks, &mut next_addr, levels, &mut seed); + blocks.sort_by_key(|block| block.addr); + SsaArtifact::for_symbolic(&blocks, None).expect("branch-tree benchmark SSA should build") +} + +fn build_fd_spec_function(arch: &ArchSpec) -> SsaArtifact { + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(RDI, 8), + src: make_const(0, 8), + }, + R2ILOp::Copy { + dst: make_reg(RSI, 8), + src: make_const(0x2000, 8), + }, + R2ILOp::Copy { + dst: make_reg(RDX, 8), + src: make_const(1, 8), + }, + R2ILOp::Call { + target: make_const(0x5000, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP0, 1), + addr: make_const(0x2000, 8), + space: SpaceId::Ram, + }, + R2ILOp::IntEqual { + dst: make_reg(TMP1, 1), + a: make_reg(TMP0, 1), + b: make_const(b'k' as u64, 1), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP1, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0x1337, 8), + }], + }, + ]; + + SsaArtifact::for_symbolic(&blocks, Some(arch)).expect("fd benchmark SSA should build") +} + +fn bench_solver_sat_cache(c: &mut Criterion) { + c.bench_function("r2sym/solver_is_sat_cached", |b| { + b.iter(|| { + let ctx = Context::thread_local(); + let solver = SymSolver::new(&ctx); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("x", 32); + let x = state.get_register("x"); + state.add_true_constraint(&x.ult(&ctx, &SymValue::concrete(5, 32))); + + black_box(solver.is_sat(&state)); + black_box(solver.is_sat(&state)); + black_box(solver.stats()) + }); + }); +} + +fn bench_explore_symbolic_branching(c: &mut Criterion) { + let func = build_branching_function(); + let config = ExploreConfig { + max_states: 64, + max_depth: 32, + timeout: None, + ..Default::default() + }; + + c.bench_function("r2sym/explore_symbolic_branching", |b| { + b.iter(|| { + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("reg:56_0", 64); + + let mut explorer = PathExplorer::with_config(&ctx, config.clone()); + let results = explorer.explore(black_box(&func), state); + black_box(results.len()); + black_box(explorer.solver().stats()) + }); + }); +} + +fn bench_explore_branch_tree(c: &mut Criterion) { + let func = build_branch_tree_function(5); + let config = ExploreConfig { + max_states: 256, + max_depth: 64, + timeout: None, + strategy: ExploreStrategy::Bfs, + ..Default::default() + }; + + c.bench_function("r2sym/explore_branch_tree", |b| { + b.iter(|| { + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x3000); + state.make_symbolic("reg:56_0", 64); + + let mut explorer = PathExplorer::with_config(&ctx, config.clone()); + let results = explorer.explore(black_box(&func), state); + black_box(results.len()); + black_box(explorer.stats().clone()) + }); + }); +} + +fn bench_explore_same_pc_merge(c: &mut Criterion) { + let func = build_join_heavy_function(6); + let mut group = c.benchmark_group("r2sym/explore_same_pc_merge"); + + for (name, merge_states) in [("merge_off", false), ("merge_on", true)] { + let config = ExploreConfig { + max_states: 256, + max_depth: 96, + timeout: None, + strategy: ExploreStrategy::Bfs, + merge_states, + ..Default::default() + }; + + group.bench_function(name, |b| { + b.iter(|| { + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x2000); + state.make_symbolic("reg:56_0", 64); + + let mut explorer = PathExplorer::with_config(&ctx, config.clone()); + let results = explorer.explore(black_box(&func), state); + black_box(results.len()); + black_box(explorer.stats().clone()) + }); + }); + } + + group.finish(); +} + +fn bench_run_spec_symbolic_fd_input(c: &mut Criterion) { + let arch = make_x86_64_arch(); + let func = build_fd_spec_function(&arch); + let config = ExploreConfig { + max_states: 64, + max_depth: 64, + timeout: None, + ..Default::default() + }; + let spec = ExplorationSpec { + find: vec![PredicateSpec::Address { + addr: AddressValue::Integer(0x1010), + }], + inputs: vec![InputSpec::Fd { + fd: 0, + len: 1, + name: Some("stdin0".to_string()), + alphabet: Some("k".to_string()), + }], + ..Default::default() + }; + + c.bench_function("r2sym/run_spec_symbolic_fd_input", |b| { + b.iter(|| { + let ctx = Context::thread_local(); + let mut initial_state = SymState::new(&ctx, 0x1000); + spec.apply_to_state(&mut initial_state); + + let mut explorer = PathExplorer::with_config(&ctx, config.clone()); + let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); + assert!(registry.install_for_explorer(&mut explorer, 0x5000, "read")); + + let result = explorer + .run_spec(black_box(&func), initial_state, black_box(&spec)) + .expect("fd-input benchmark should succeed"); + black_box(result.found_paths.len()); + black_box(explorer.solver().stats()) + }); + }); +} + +criterion_group!( + symex_hotpaths, + bench_solver_sat_cache, + bench_explore_symbolic_branching, + bench_explore_branch_tree, + bench_explore_same_pc_merge, + bench_run_spec_symbolic_fd_input +); +criterion_main!(symex_hotpaths); diff --git a/crates/r2sym/src/lib.rs b/crates/r2sym/src/lib.rs index 4d43c29..91099cb 100644 --- a/crates/r2sym/src/lib.rs +++ b/crates/r2sym/src/lib.rs @@ -50,9 +50,10 @@ pub use path::{ExploreConfig, PathExplorer, PathResult, SolvedPath}; pub use r2api::{R2Api, R2Error}; pub use runtime::seed_default_state_for_arch; pub use sim::{ - CallConv, CallInfo, FunctionSummary, SummaryEffect, SummaryInstallStats, SummaryRegistry, + CallConv, CallInfo, FunctionSummary, SummaryEffect, SummaryInstallStats, SummaryProfile, + SummaryRegistry, }; -pub use solver::{SatResult, SymModel, SymSolver}; +pub use solver::{SatResult, SolverStats, SymModel, SymSolver}; pub use spec::{ AddressValue, BudgetSpec, ExplorationSpec, InputSpec, MergeSpec, PredicateSpec, RuntimeSpec, StartSpec, StrategySpec, diff --git a/crates/r2sym/src/memory.rs b/crates/r2sym/src/memory.rs index 93b157d..c61d453 100644 --- a/crates/r2sym/src/memory.rs +++ b/crates/r2sym/src/memory.rs @@ -3,8 +3,7 @@ //! This module provides a sparse memory model that can handle both //! concrete and symbolic addresses and values. -use std::collections::HashMap; -use std::collections::HashSet; +use std::collections::{BTreeSet, HashMap}; use z3::ast::{BV, Bool}; use z3::{Context, SatResult, Solver}; @@ -61,7 +60,7 @@ impl<'ctx> SymMemory<'ctx> { } pub(crate) fn merge_addrs(&self) -> Vec { - let mut addrs = HashSet::new(); + let mut addrs = BTreeSet::new(); addrs.extend(self.concrete.keys().copied()); for (addr, _value, size) in &self.symbolic_writes { if let Some(base) = addr.as_concrete() { @@ -454,4 +453,20 @@ mod tests { let val = model.eval(&result_bv, true).unwrap().as_u64().unwrap(); assert_eq!(val, 0xCAFEBABE); } + + #[test] + fn test_merge_addrs_are_sorted() { + let ctx = Context::thread_local(); + let mut mem = SymMemory::new(&ctx); + + mem.write_bytes(0x1003, &[0xaa]); + mem.write_bytes(0x1000, &[0xbb]); + mem.push_symbolic_write( + SymValue::concrete(0x1001, 64), + SymValue::concrete(0x1122, 16), + 2, + ); + + assert_eq!(mem.merge_addrs(), vec![0x1000, 0x1001, 0x1002, 0x1003]); + } } diff --git a/crates/r2sym/src/path.rs b/crates/r2sym/src/path.rs index 6a44a58..db7ecd9 100644 --- a/crates/r2sym/src/path.rs +++ b/crates/r2sym/src/path.rs @@ -3,7 +3,7 @@ //! This module provides different strategies for exploring paths //! during symbolic execution, including DFS, BFS, and coverage-guided. -use std::collections::{HashSet, VecDeque}; +use std::collections::{BTreeMap, HashMap, HashSet, VecDeque}; use std::time::{Duration, Instant}; use r2ssa::{BlockTerminator, SsaArtifact}; @@ -19,6 +19,8 @@ use crate::state::{ExitStatus, SymState}; pub struct ExploreConfig { /// Maximum number of states to explore. pub max_states: usize, + /// Maximum number of completed paths to collect during full exploration. + pub max_completed_paths: Option, /// Maximum execution depth per path. pub max_depth: usize, /// Timeout for the entire exploration. @@ -35,6 +37,7 @@ impl Default for ExploreConfig { fn default() -> Self { Self { max_states: 1000, + max_completed_paths: None, max_depth: 100, timeout: Some(Duration::from_secs(60)), strategy: ExploreStrategy::Dfs, @@ -93,7 +96,9 @@ impl<'ctx> PathResult<'ctx> { /// Get all register names in the final state. pub fn register_names(&self) -> Vec { - self.state.register_names().cloned().collect() + let mut names: Vec = self.state.register_names().cloned().collect(); + names.sort(); + names } /// Get a register value (returns None if symbolic or not set). @@ -111,13 +116,13 @@ impl<'ctx> PathResult<'ctx> { #[derive(Debug, Clone, Default)] pub struct SolvedPath { /// Concrete input values (symbolic variable name -> value). - pub inputs: std::collections::HashMap, + pub inputs: BTreeMap, /// Concrete multi-byte input buffers (input source name -> bytes). - pub input_buffers: std::collections::HashMap>, + pub input_buffers: BTreeMap>, /// Concrete register values at path end. - pub registers: std::collections::HashMap, + pub registers: BTreeMap, /// Concrete memory bytes for tracked symbolic regions. - pub memory: std::collections::HashMap>, + pub memory: BTreeMap>, /// Final program counter. pub final_pc: u64, /// Path constraints that were satisfied. @@ -172,6 +177,391 @@ pub struct ExploreStats { pub total_time: Duration, } +struct StateWorklist<'ctx> { + strategy: ExploreStrategy, + ready: VecDeque, + same_pc: HashMap>, + slots: Vec>>, + live_states: usize, +} + +impl<'ctx> StateWorklist<'ctx> { + fn new(strategy: ExploreStrategy) -> Self { + Self { + strategy, + ready: VecDeque::new(), + same_pc: HashMap::new(), + slots: Vec::new(), + live_states: 0, + } + } + + fn push(&mut self, state: SymState<'ctx>) { + let id = self.slots.len(); + let pc = state.pc; + self.slots.push(Some(state)); + self.ready.push_back(id); + self.same_pc.entry(pc).or_default().push_back(id); + self.live_states += 1; + } + + fn pop_next(&mut self) -> Option> { + loop { + let id = match self.strategy { + ExploreStrategy::Dfs => self.ready.pop_back(), + ExploreStrategy::Bfs => self.ready.pop_front(), + ExploreStrategy::Random => { + if self.ready.is_empty() { + None + } else if self.live_states.is_multiple_of(2) { + self.ready.pop_front() + } else { + self.ready.pop_back() + } + } + }?; + + if let Some(state) = self.take_slot(id) { + return Some(state); + } + } + } + + fn take_same_pc(&mut self, pc: u64) -> Option> { + loop { + let id = { + let bucket = self.same_pc.get_mut(&pc)?; + match self.strategy { + ExploreStrategy::Dfs => bucket.pop_back(), + ExploreStrategy::Bfs => bucket.pop_front(), + ExploreStrategy::Random => { + if bucket.is_empty() { + None + } else if self.live_states.is_multiple_of(2) { + bucket.pop_front() + } else { + bucket.pop_back() + } + } + } + }?; + + let remove_bucket = self.same_pc.get(&pc).is_some_and(VecDeque::is_empty); + if remove_bucket { + self.same_pc.remove(&pc); + } + + if let Some(state) = self.take_slot(id) { + return Some(state); + } + } + } + + fn take_slot(&mut self, id: usize) -> Option> { + let slot = self.slots.get_mut(id)?; + let state = slot.take()?; + self.live_states = self.live_states.saturating_sub(1); + Some(state) + } +} + +enum DriverAction<'ctx> { + Continue(Box>), + Skip, + Finish, +} + +enum DriverMode<'ctx> { + Explore { + results: Vec>, + }, + FindFirst { + target_addr: u64, + found: Option>, + }, + FindAll { + target_addr: u64, + matches: Vec>, + }, + Avoid { + avoid_set: HashSet, + found: Option>, + }, + Spec { + find_set: HashSet, + avoid_set: HashSet, + max_finds: usize, + result: SpecExploreResult<'ctx>, + }, +} + +impl<'ctx> DriverMode<'ctx> { + fn explore_path_cap_reached(&self, max_completed_paths: Option) -> bool { + let Some(limit) = max_completed_paths else { + return false; + }; + match self { + DriverMode::Explore { results } => results.len() >= limit, + _ => false, + } + } + + fn on_timeout(&mut self) -> bool { + if let DriverMode::Spec { result, .. } = self { + result.diagnostics.push("exploration timed out".to_string()); + } + true + } + + fn on_max_states(&mut self) -> bool { + if let DriverMode::Spec { result, .. } = self { + result + .diagnostics + .push("exploration stopped at max_states budget".to_string()); + } + true + } + + fn on_unsat_pruned(&mut self) { + if let DriverMode::Spec { result, .. } = self { + result.unsat_states += 1; + } + } + + fn on_state_popped( + &mut self, + explorer: &mut PathExplorer<'ctx>, + state: SymState<'ctx>, + ) -> DriverAction<'ctx> { + match self { + DriverMode::Explore { .. } => DriverAction::Continue(Box::new(state)), + DriverMode::FindFirst { target_addr, found } => { + if state.pc == *target_addr { + if explorer.solver.is_sat(&state) { + *found = Some(PathResult::new(state, true)); + DriverAction::Finish + } else { + DriverAction::Skip + } + } else { + DriverAction::Continue(Box::new(state)) + } + } + DriverMode::FindAll { + target_addr, + matches, + } => { + if state.pc == *target_addr { + if explorer.solver.is_sat(&state) { + explorer.record_depth(state.depth); + explorer.stats.paths_completed += 1; + matches.push(PathResult::new(state, true)); + } + DriverAction::Skip + } else { + DriverAction::Continue(Box::new(state)) + } + } + DriverMode::Avoid { avoid_set, .. } => { + if avoid_set.contains(&state.pc) { + DriverAction::Skip + } else { + DriverAction::Continue(Box::new(state)) + } + } + DriverMode::Spec { + find_set, + avoid_set, + max_finds, + result, + } => { + if avoid_set.contains(&state.pc) { + result.avoided_states += 1; + return DriverAction::Skip; + } + if find_set.contains(&state.pc) { + if explorer.solver.is_sat(&state) { + explorer.record_depth(state.depth); + explorer.stats.paths_completed += 1; + result.found_paths.push(PathResult::new(state, true)); + if result.found_paths.len() >= *max_finds { + return DriverAction::Finish; + } + } else { + explorer.stats.paths_pruned += 1; + result.unsat_states += 1; + } + DriverAction::Skip + } else { + DriverAction::Continue(Box::new(state)) + } + } + } + } + + fn on_depth_limit( + &mut self, + explorer: &mut PathExplorer<'ctx>, + mut state: SymState<'ctx>, + max_completed_paths: Option, + ) -> DriverAction<'ctx> { + match self { + DriverMode::Explore { results } => { + state.terminate(ExitStatus::MaxDepth); + explorer.record_depth(state.depth); + explorer.stats.paths_max_depth += 1; + results.push(PathResult::new(state, true)); + if self.explore_path_cap_reached(max_completed_paths) { + DriverAction::Finish + } else { + DriverAction::Skip + } + } + DriverMode::FindFirst { .. } => DriverAction::Skip, + DriverMode::FindAll { .. } => { + explorer.stats.paths_max_depth += 1; + DriverAction::Skip + } + DriverMode::Avoid { found, .. } => { + if explorer.solver.is_sat(&state) { + *found = Some(PathResult::new(state, true)); + DriverAction::Finish + } else { + DriverAction::Skip + } + } + DriverMode::Spec { result, .. } => { + explorer.stats.paths_max_depth += 1; + result.completed_states += 1; + DriverAction::Skip + } + } + } + + fn on_missing_block( + &mut self, + explorer: &mut PathExplorer<'ctx>, + mut state: SymState<'ctx>, + block_addr: u64, + max_completed_paths: Option, + ) -> DriverAction<'ctx> { + match self { + DriverMode::Explore { results } => { + state.terminate(ExitStatus::Return); + results.push(PathResult::new(state, true)); + explorer.stats.paths_completed += 1; + if self.explore_path_cap_reached(max_completed_paths) { + DriverAction::Finish + } else { + DriverAction::Skip + } + } + DriverMode::FindFirst { .. } | DriverMode::FindAll { .. } => DriverAction::Skip, + DriverMode::Avoid { found, .. } => { + if explorer.solver.is_sat(&state) { + *found = Some(PathResult::new(state, true)); + DriverAction::Finish + } else { + DriverAction::Skip + } + } + DriverMode::Spec { result, .. } => { + result + .diagnostics + .push(format!("no SSA block at 0x{block_addr:x}")); + result.completed_states += 1; + explorer.stats.paths_completed += 1; + DriverAction::Skip + } + } + } + + fn on_terminated_state( + &mut self, + explorer: &mut PathExplorer<'ctx>, + state: SymState<'ctx>, + block_addr: u64, + max_completed_paths: Option, + ) -> DriverAction<'ctx> { + match self { + DriverMode::Explore { results } => { + explorer.record_depth(state.depth); + let feasible = explorer.solver.is_sat(&state); + results.push(PathResult::new(state, feasible)); + explorer.stats.paths_completed += 1; + if self.explore_path_cap_reached(max_completed_paths) { + return DriverAction::Finish; + } + } + DriverMode::FindFirst { .. } + | DriverMode::FindAll { .. } + | DriverMode::Avoid { .. } => {} + DriverMode::Spec { result, .. } => { + explorer.stats.paths_completed += 1; + result.diagnostics.push(format!( + "state terminated at 0x{:x} with {:?}", + block_addr, state.exit_status + )); + match &state.exit_status { + Some(ExitStatus::Error(_)) => result.errored_states += 1, + _ => result.completed_states += 1, + } + } + } + DriverAction::Skip + } + + fn on_execute_error( + &mut self, + explorer: &mut PathExplorer<'ctx>, + mut state: SymState<'ctx>, + block_addr: u64, + error: String, + max_completed_paths: Option, + ) -> DriverAction<'ctx> { + match self { + DriverMode::Explore { results } => { + explorer.record_depth(state.depth); + state.terminate(ExitStatus::Error(error)); + results.push(PathResult::new(state, false)); + explorer.stats.paths_completed += 1; + if self.explore_path_cap_reached(max_completed_paths) { + return DriverAction::Finish; + } + } + DriverMode::FindAll { .. } => { + explorer.stats.paths_completed += 1; + } + DriverMode::Spec { result, .. } => { + explorer.stats.paths_completed += 1; + result.errored_states += 1; + result + .diagnostics + .push(format!("execution error at 0x{block_addr:x}: {error}")); + } + DriverMode::FindFirst { .. } | DriverMode::Avoid { .. } => {} + } + DriverAction::Skip + } + + fn allow_enqueue(&mut self, state: &SymState<'ctx>) -> bool { + match self { + DriverMode::Avoid { avoid_set, .. } => !avoid_set.contains(&state.pc), + DriverMode::Spec { + avoid_set, result, .. + } => { + if avoid_set.contains(&state.pc) { + result.avoided_states += 1; + false + } else { + true + } + } + _ => true, + } + } +} + impl<'ctx> PathExplorer<'ctx> { /// Create a new path explorer. pub fn new(ctx: &'ctx Context) -> Self { @@ -289,106 +679,141 @@ impl<'ctx> PathExplorer<'ctx> { paths.iter().map(|p| self.solve_path(p)).collect() } - /// Explore all paths in a function. - pub fn explore( + fn record_depth(&mut self, depth: usize) { + if depth > self.stats.max_depth_reached { + self.stats.max_depth_reached = depth; + } + } + + fn drive( &mut self, func: &SsaArtifact, initial_state: SymState<'ctx>, - ) -> Vec> { + mode: &mut DriverMode<'ctx>, + ) { let start_time = Instant::now(); - let mut results = Vec::new(); - let mut worklist: VecDeque> = VecDeque::new(); - worklist.push_back(initial_state); + let mut worklist = StateWorklist::new(self.config.strategy); + worklist.push(initial_state); - while let Some(mut state) = self.next_state(&mut worklist) { + while let Some(mut state) = worklist.pop_next() { if self.config.merge_states - && let Some(other) = take_merge_candidate(&mut worklist, state.pc) + && let Some(other) = worklist.take_same_pc(state.pc) { state = state.merge_with(&other); } - // Check timeout + if let Some(timeout) = self.config.timeout && start_time.elapsed() > timeout + && mode.on_timeout() { break; } - // Check state limit - if self.stats.states_explored >= self.config.max_states { - break; + match mode.on_state_popped(self, state) { + DriverAction::Continue(next_state) => state = *next_state, + DriverAction::Skip => continue, + DriverAction::Finish => break, } - self.stats.states_explored += 1; + if self.stats.states_explored >= self.config.max_states && mode.on_max_states() { + break; + } - // Check depth limit if state.depth >= self.config.max_depth { - state.terminate(ExitStatus::MaxDepth); - self.stats.paths_max_depth += 1; - results.push(PathResult::new(state, true)); - continue; + match mode.on_depth_limit(self, state, self.config.max_completed_paths) { + DriverAction::Continue(next_state) => state = *next_state, + DriverAction::Skip => continue, + DriverAction::Finish => break, + } } - // Check feasibility + self.stats.states_explored += 1; + if self.config.prune_infeasible && !self.solver.is_sat(&state) { self.stats.paths_pruned += 1; + mode.on_unsat_pruned(); continue; } - // Get current block let block_addr = state.pc; let Some(block) = func.get_block(block_addr) else { - // No block at this address - path ends - state.terminate(ExitStatus::Return); - results.push(PathResult::new(state, true)); - self.stats.paths_completed += 1; - continue; + match mode.on_missing_block( + self, + state, + block_addr, + self.config.max_completed_paths, + ) { + DriverAction::Finish => break, + DriverAction::Continue(_) | DriverAction::Skip => continue, + } }; - // Execute the block match self.executor.execute_block(&mut state, block) { Ok(forked_states) => { - // Add forked states to worklist + self.record_depth(state.depth); + for mut forked in forked_states { forked.set_prev_pc(Some(block_addr)); - worklist.push_back(forked); - } - - // Update max depth before potentially moving state - if state.depth > self.stats.max_depth_reached { - self.stats.max_depth_reached = state.depth; + if mode.allow_enqueue(&forked) { + worklist.push(forked); + } } - // Check if state terminated - if state.is_terminated() { - let feasible = self.solver.is_sat(&state); - results.push(PathResult::new(state, feasible)); - self.stats.paths_completed += 1; - } else { - // Continue exploring this path - // Update PC to next block if not changed by control flow + if !state.is_terminated() { if state.pc == block_addr && let Some(next) = self.fallthrough_target(func, block_addr) { state.pc = next; } state.set_prev_pc(Some(block_addr)); - worklist.push_back(state); - } - } - Err(e) => { - // Update max depth before moving state - if state.depth > self.stats.max_depth_reached { - self.stats.max_depth_reached = state.depth; + if mode.allow_enqueue(&state) { + worklist.push(state); + } + } else { + match mode.on_terminated_state( + self, + state, + block_addr, + self.config.max_completed_paths, + ) { + DriverAction::Finish => break, + DriverAction::Continue(_) | DriverAction::Skip => continue, + } } - state.terminate(ExitStatus::Error(format!("{}", e))); - results.push(PathResult::new(state, false)); - self.stats.paths_completed += 1; } + Err(e) => match mode.on_execute_error( + self, + state, + block_addr, + e.to_string(), + self.config.max_completed_paths, + ) { + DriverAction::Finish => break, + DriverAction::Continue(_) | DriverAction::Skip => continue, + }, } } self.stats.total_time = start_time.elapsed(); - results + } + + /// Explore all paths in a function. + pub fn explore( + &mut self, + func: &SsaArtifact, + initial_state: SymState<'ctx>, + ) -> Vec> { + if self.config.max_completed_paths == Some(0) { + return Vec::new(); + } + let mut mode = DriverMode::Explore { + results: Vec::new(), + }; + self.drive(func, initial_state, &mut mode); + match mode { + DriverMode::Explore { results } => results, + _ => unreachable!("explore should always use explore mode"), + } } fn fallthrough_target(&self, func: &SsaArtifact, block_addr: u64) -> Option { @@ -403,26 +828,6 @@ impl<'ctx> PathExplorer<'ctx> { } } - /// Get the next state from the worklist based on strategy. - fn next_state(&self, worklist: &mut VecDeque>) -> Option> { - match self.config.strategy { - ExploreStrategy::Dfs => worklist.pop_back(), - ExploreStrategy::Bfs => worklist.pop_front(), - ExploreStrategy::Random => { - if worklist.is_empty() { - None - } else { - // Simple random: alternate between front and back - if worklist.len().is_multiple_of(2) { - worklist.pop_front() - } else { - worklist.pop_back() - } - } - } - } - } - /// Explore paths to find inputs that reach a target address. pub fn find_path_to( &mut self, @@ -430,73 +835,15 @@ impl<'ctx> PathExplorer<'ctx> { initial_state: SymState<'ctx>, target_addr: u64, ) -> Option> { - let start_time = Instant::now(); - let mut worklist: VecDeque> = VecDeque::new(); - worklist.push_back(initial_state); - - while let Some(mut state) = self.next_state(&mut worklist) { - if self.config.merge_states - && let Some(other) = take_merge_candidate(&mut worklist, state.pc) - { - state = state.merge_with(&other); - } - // Check timeout - if let Some(timeout) = self.config.timeout - && start_time.elapsed() > timeout - { - break; - } - - // Check if we reached the target - if state.pc == target_addr { - let feasible = self.solver.is_sat(&state); - if feasible { - return Some(PathResult::new(state, true)); - } - continue; - } - - // Check limits - if self.stats.states_explored >= self.config.max_states { - break; - } - if state.depth >= self.config.max_depth { - continue; - } - - self.stats.states_explored += 1; - - // Check feasibility - if self.config.prune_infeasible && !self.solver.is_sat(&state) { - self.stats.paths_pruned += 1; - continue; - } - - // Get and execute block - let block_addr = state.pc; - let Some(block) = func.get_block(block_addr) else { - continue; - }; - - if let Ok(forked_states) = self.executor.execute_block(&mut state, block) { - for mut forked in forked_states { - forked.set_prev_pc(Some(block_addr)); - worklist.push_back(forked); - } - - if !state.is_terminated() { - if state.pc == block_addr - && let Some(next) = self.fallthrough_target(func, block_addr) - { - state.pc = next; - } - state.set_prev_pc(Some(block_addr)); - worklist.push_back(state); - } - } + let mut mode = DriverMode::FindFirst { + target_addr, + found: None, + }; + self.drive(func, initial_state, &mut mode); + match mode { + DriverMode::FindFirst { found, .. } => found, + _ => unreachable!("find_path_to should always use first-match mode"), } - - None } /// Explore paths to collect all feasible states that reach a target address. @@ -506,80 +853,15 @@ impl<'ctx> PathExplorer<'ctx> { initial_state: SymState<'ctx>, target_addr: u64, ) -> Vec> { - let start_time = Instant::now(); - let mut matches = Vec::new(); - let mut worklist: VecDeque> = VecDeque::new(); - worklist.push_back(initial_state); - - while let Some(mut state) = self.next_state(&mut worklist) { - if self.config.merge_states - && let Some(other) = take_merge_candidate(&mut worklist, state.pc) - { - state = state.merge_with(&other); - } - if let Some(timeout) = self.config.timeout - && start_time.elapsed() > timeout - { - break; - } - - if state.pc == target_addr { - let feasible = self.solver.is_sat(&state); - if feasible { - if state.depth > self.stats.max_depth_reached { - self.stats.max_depth_reached = state.depth; - } - self.stats.paths_completed += 1; - matches.push(PathResult::new(state, true)); - } - continue; - } - - if self.stats.states_explored >= self.config.max_states { - break; - } - if state.depth >= self.config.max_depth { - self.stats.paths_max_depth += 1; - continue; - } - - self.stats.states_explored += 1; - - if self.config.prune_infeasible && !self.solver.is_sat(&state) { - self.stats.paths_pruned += 1; - continue; - } - - let block_addr = state.pc; - let Some(block) = func.get_block(block_addr) else { - continue; - }; - - match self.executor.execute_block(&mut state, block) { - Ok(forked_states) => { - for mut forked in forked_states { - forked.set_prev_pc(Some(block_addr)); - worklist.push_back(forked); - } - - if !state.is_terminated() { - if state.pc == block_addr - && let Some(next) = self.fallthrough_target(func, block_addr) - { - state.pc = next; - } - state.set_prev_pc(Some(block_addr)); - worklist.push_back(state); - } - } - Err(_) => { - self.stats.paths_completed += 1; - } - } + let mut mode = DriverMode::FindAll { + target_addr, + matches: Vec::new(), + }; + self.drive(func, initial_state, &mut mode); + match mode { + DriverMode::FindAll { matches, .. } => matches, + _ => unreachable!("find_paths_to should always use all-match mode"), } - - self.stats.total_time = start_time.elapsed(); - matches } /// Explore paths to find inputs that avoid a target address. @@ -589,81 +871,15 @@ impl<'ctx> PathExplorer<'ctx> { initial_state: SymState<'ctx>, avoid_addrs: &[u64], ) -> Option> { - let avoid_set: HashSet = avoid_addrs.iter().copied().collect(); - let start_time = Instant::now(); - let mut worklist: VecDeque> = VecDeque::new(); - worklist.push_back(initial_state); - - while let Some(mut state) = self.next_state(&mut worklist) { - if self.config.merge_states - && let Some(other) = take_merge_candidate(&mut worklist, state.pc) - { - state = state.merge_with(&other); - } - // Check timeout - if let Some(timeout) = self.config.timeout - && start_time.elapsed() > timeout - { - break; - } - - // Check if we hit an avoided address - if avoid_set.contains(&state.pc) { - continue; - } - - // Check limits - if self.stats.states_explored >= self.config.max_states { - break; - } - if state.depth >= self.config.max_depth { - let feasible = self.solver.is_sat(&state); - if feasible { - return Some(PathResult::new(state, true)); - } - continue; - } - - self.stats.states_explored += 1; - - // Check feasibility - if self.config.prune_infeasible && !self.solver.is_sat(&state) { - self.stats.paths_pruned += 1; - continue; - } - - // Get and execute block - let block_addr = state.pc; - let Some(block) = func.get_block(block_addr) else { - // Reached end without hitting avoided addresses - let feasible = self.solver.is_sat(&state); - if feasible { - return Some(PathResult::new(state, true)); - } - continue; - }; - - if let Ok(forked_states) = self.executor.execute_block(&mut state, block) { - for mut forked in forked_states { - forked.set_prev_pc(Some(block_addr)); - if !avoid_set.contains(&forked.pc) { - worklist.push_back(forked); - } - } - - if !state.is_terminated() && !avoid_set.contains(&state.pc) { - if state.pc == block_addr - && let Some(next) = self.fallthrough_target(func, block_addr) - { - state.pc = next; - } - state.set_prev_pc(Some(block_addr)); - worklist.push_back(state); - } - } + let mut mode = DriverMode::Avoid { + avoid_set: avoid_addrs.iter().copied().collect(), + found: None, + }; + self.drive(func, initial_state, &mut mode); + match mode { + DriverMode::Avoid { found, .. } => found, + _ => unreachable!("find_path_avoiding should always use avoid mode"), } - - None } /// Run a typed exploration specification. @@ -673,143 +889,18 @@ impl<'ctx> PathExplorer<'ctx> { initial_state: SymState<'ctx>, spec: &ExplorationSpec, ) -> Result, String> { - let find_set: HashSet = spec.find_addresses()?.into_iter().collect(); - let avoid_set: HashSet = spec.avoid_addresses()?.into_iter().collect(); - let max_finds = spec.max_finds(); - let start_time = Instant::now(); - let mut result = SpecExploreResult::default(); - let mut worklist: VecDeque> = VecDeque::new(); - worklist.push_back(initial_state); - - while let Some(mut state) = self.next_state(&mut worklist) { - if self.config.merge_states - && let Some(other) = take_merge_candidate(&mut worklist, state.pc) - { - state = state.merge_with(&other); - } - - if let Some(timeout) = self.config.timeout - && start_time.elapsed() > timeout - { - result.diagnostics.push("exploration timed out".to_string()); - break; - } - - if avoid_set.contains(&state.pc) { - result.avoided_states += 1; - continue; - } - - if find_set.contains(&state.pc) { - let feasible = self.solver.is_sat(&state); - if feasible { - self.stats.paths_completed += 1; - if state.depth > self.stats.max_depth_reached { - self.stats.max_depth_reached = state.depth; - } - result.found_paths.push(PathResult::new(state, true)); - if result.found_paths.len() >= max_finds { - break; - } - } else { - self.stats.paths_pruned += 1; - result.unsat_states += 1; - } - continue; - } - - if self.stats.states_explored >= self.config.max_states { - result - .diagnostics - .push("exploration stopped at max_states budget".to_string()); - break; - } - - if state.depth >= self.config.max_depth { - self.stats.paths_max_depth += 1; - result.completed_states += 1; - continue; - } - - self.stats.states_explored += 1; - - if self.config.prune_infeasible && !self.solver.is_sat(&state) { - self.stats.paths_pruned += 1; - result.unsat_states += 1; - continue; - } - - let block_addr = state.pc; - let Some(block) = func.get_block(block_addr) else { - result - .diagnostics - .push(format!("no SSA block at 0x{block_addr:x}")); - result.completed_states += 1; - self.stats.paths_completed += 1; - continue; - }; - - match self.executor.execute_block(&mut state, block) { - Ok(forked_states) => { - for mut forked in forked_states { - forked.set_prev_pc(Some(block_addr)); - if !avoid_set.contains(&forked.pc) { - worklist.push_back(forked); - } else { - result.avoided_states += 1; - } - } - - if !state.is_terminated() { - if state.pc == block_addr - && let Some(next) = self.fallthrough_target(func, block_addr) - { - state.pc = next; - } - state.set_prev_pc(Some(block_addr)); - if !avoid_set.contains(&state.pc) { - worklist.push_back(state); - } else { - result.avoided_states += 1; - } - } else { - self.stats.paths_completed += 1; - result.diagnostics.push(format!( - "state terminated at 0x{:x} with {:?}", - block_addr, state.exit_status - )); - match &state.exit_status { - Some(ExitStatus::Error(_)) => result.errored_states += 1, - _ => result.completed_states += 1, - } - } - } - Err(e) => { - self.stats.paths_completed += 1; - result.errored_states += 1; - result - .diagnostics - .push(format!("execution error at 0x{block_addr:x}: {e}")); - } - } - } - - self.stats.total_time = start_time.elapsed(); - Ok(result) - } -} - -fn take_merge_candidate<'ctx>( - worklist: &mut VecDeque>, - pc: u64, -) -> Option> { - let len = worklist.len(); - for idx in 0..len { - if worklist[idx].pc == pc { - return worklist.remove(idx); + let mut mode = DriverMode::Spec { + find_set: spec.find_addresses()?.into_iter().collect(), + avoid_set: spec.avoid_addresses()?.into_iter().collect(), + max_finds: spec.max_finds(), + result: SpecExploreResult::default(), + }; + self.drive(func, initial_state, &mut mode); + match mode { + DriverMode::Spec { result, .. } => Ok(result), + _ => unreachable!("run_spec should always use spec mode"), } } - None } #[cfg(test)] @@ -821,6 +912,7 @@ mod tests { fn test_explore_config_default() { let config = ExploreConfig::default(); assert_eq!(config.max_states, 1000); + assert_eq!(config.max_completed_paths, None); assert_eq!(config.max_depth, 100); assert!(config.prune_infeasible); } @@ -840,6 +932,29 @@ mod tests { assert_eq!(stats.paths_completed, 0); } + #[test] + fn test_state_worklist_same_pc_lookup_skips_popped_entries() { + let ctx = Context::thread_local(); + let mut worklist = StateWorklist::new(ExploreStrategy::Bfs); + + worklist.push(SymState::new(&ctx, 0x1000)); + worklist.push(SymState::new(&ctx, 0x2000)); + worklist.push(SymState::new(&ctx, 0x1000)); + + let first = worklist.pop_next().expect("first state should exist"); + assert_eq!(first.pc, 0x1000); + + let merged = worklist + .take_same_pc(0x1000) + .expect("queued same-pc state should still be found"); + assert_eq!(merged.pc, 0x1000); + + let remaining = worklist.pop_next().expect("non-merged state should remain"); + assert_eq!(remaining.pc, 0x2000); + assert!(worklist.pop_next().is_none()); + assert!(worklist.take_same_pc(0x1000).is_none()); + } + #[test] fn test_path_result_methods() { let ctx = Context::thread_local(); @@ -847,12 +962,16 @@ mod tests { let mut state = SymState::new(&ctx, 0x1000); state.set_register("rax", SymValue::concrete(42, 64)); state.make_symbolic("rbx", 64); + state.set_register("aaa", SymValue::concrete(7, 64)); let result = PathResult::new(state, true); assert_eq!(result.final_pc(), 0x1000); assert_eq!(result.num_constraints(), 0); - assert!(result.register_names().contains(&"rax".to_string())); + assert_eq!( + result.register_names(), + vec!["aaa".to_string(), "rax".to_string(), "rbx".to_string()] + ); assert_eq!(result.get_concrete_register("rax"), Some(42)); assert!(result.is_register_symbolic("rbx")); } @@ -896,4 +1015,33 @@ mod tests { assert_eq!(solved.final_pc, 0); assert_eq!(solved.num_constraints, 0); } + + #[test] + fn test_solve_path_emits_stable_map_order() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.new_symbolic_input("z_input", 64); + state.new_symbolic_input("a_input", 64); + state.set_register("z_reg", SymValue::concrete(3, 64)); + state.set_register("a_reg", SymValue::concrete(1, 64)); + state.set_register("m_reg", SymValue::concrete(9, 64)); + + let result = PathResult::new(state, true); + let explorer = PathExplorer::new(&ctx); + let solved = explorer.solve_path(&result).expect("path should solve"); + + assert_eq!( + solved.inputs.keys().cloned().collect::>(), + vec!["a_input".to_string(), "z_input".to_string()] + ); + assert_eq!( + solved.registers.keys().cloned().collect::>(), + vec![ + "a_reg".to_string(), + "m_reg".to_string(), + "z_reg".to_string() + ] + ); + } } diff --git a/crates/r2sym/src/sim.rs b/crates/r2sym/src/sim.rs index 2228efb..cf430e8 100644 --- a/crates/r2sym/src/sim.rs +++ b/crates/r2sym/src/sim.rs @@ -30,6 +30,92 @@ pub const DEFAULT_MAX_MEMCMP: u64 = 0x1000; pub const DEFAULT_MAX_PRINTF_SCAN: u64 = 0x400; /// Default upper bound for generic interproc memory havoc windows. pub const DEFAULT_MAX_INTERPROC_HAVOC: u64 = 0x40; +/// Path-listing upper bound for memory copy operations. +pub const PATH_LIST_MAX_MEMCPY: u64 = 0x40; +/// Path-listing upper bound for memory set operations. +pub const PATH_LIST_MAX_MEMSET: u64 = 0x40; +/// Path-listing upper bound for string operations. +pub const PATH_LIST_MAX_STRLEN: u64 = 0x80; +/// Path-listing upper bound for memcmp operations. +pub const PATH_LIST_MAX_MEMCMP: u64 = 0x40; +/// Path-listing upper bound for printf/puts modeled return values. +pub const PATH_LIST_MAX_PRINTF_SCAN: u64 = 0x40; +/// Path-listing precise-byte threshold before summaries switch to coarse modeling. +pub const PATH_LIST_PRECISE_BYTE_LIMIT: u64 = 0x10; + +/// Summary profile used to install function summaries for different workflows. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SummaryProfile { + /// Default symbolic execution behavior. + Default, + /// Fast bounded modeling for path-listing workflows. + PathListing, +} + +#[derive(Debug, Clone, Copy)] +struct SummaryBudgets { + max_strlen: u64, + max_memcpy: u64, + max_memset: u64, + max_memcmp: u64, + max_printf_scan: u64, + byte_policy: ByteSummaryPolicy, +} + +#[derive(Debug, Clone, Copy)] +struct ByteSummaryPolicy { + summarize_symbolic: bool, + summarize_large: bool, + precise_limit: u64, +} + +impl ByteSummaryPolicy { + const fn precise(max_bytes: u64) -> Self { + Self { + summarize_symbolic: false, + summarize_large: false, + precise_limit: max_bytes, + } + } + + const fn summarized(precise_limit: u64) -> Self { + Self { + summarize_symbolic: true, + summarize_large: true, + precise_limit, + } + } + + fn use_precise_model(&self, n_concrete: Option) -> bool { + match n_concrete { + Some(len) => !self.summarize_large || len <= self.precise_limit, + None => !self.summarize_symbolic, + } + } +} + +impl SummaryProfile { + fn budgets(self) -> SummaryBudgets { + match self { + SummaryProfile::Default => SummaryBudgets { + max_strlen: DEFAULT_MAX_STRLEN, + max_memcpy: DEFAULT_MAX_MEMCPY, + max_memset: DEFAULT_MAX_MEMSET, + max_memcmp: DEFAULT_MAX_MEMCMP, + max_printf_scan: DEFAULT_MAX_PRINTF_SCAN, + byte_policy: ByteSummaryPolicy::precise(DEFAULT_MAX_MEMCPY), + }, + SummaryProfile::PathListing => SummaryBudgets { + max_strlen: PATH_LIST_MAX_STRLEN, + max_memcpy: PATH_LIST_MAX_MEMCPY, + max_memset: PATH_LIST_MAX_MEMSET, + max_memcmp: PATH_LIST_MAX_MEMCMP, + max_printf_scan: PATH_LIST_MAX_PRINTF_SCAN, + byte_policy: ByteSummaryPolicy::summarized(PATH_LIST_PRECISE_BYTE_LIMIT), + }, + } + } +} /// Summary execution outcome. pub enum SummaryEffect<'ctx> { @@ -179,16 +265,28 @@ impl<'ctx> SummaryRegistry<'ctx> { /// Create a registry pre-populated with core summaries. pub fn with_core(callconv: CallConv) -> Self { + Self::with_profile(callconv, SummaryProfile::Default) + } + + /// Create a registry pre-populated with summaries for a typed workflow profile. + pub fn with_profile(callconv: CallConv, profile: SummaryProfile) -> Self { + let budgets = profile.budgets(); let mut registry = Self::new(callconv); - registry.register_summary(MemcpySummary::new(DEFAULT_MAX_MEMCPY)); - registry.register_summary(MemsetSummary::new(DEFAULT_MAX_MEMSET)); - registry.register_summary(StrlenSummary::new(DEFAULT_MAX_STRLEN)); + registry.register_summary(MemcpySummary::with_policy( + budgets.max_memcpy, + budgets.byte_policy, + )); + registry.register_summary(MemsetSummary::with_policy( + budgets.max_memset, + budgets.byte_policy, + )); + registry.register_summary(StrlenSummary::new(budgets.max_strlen)); registry.register_summary(StrcmpSummary::new()); - registry.register_summary(MemcmpSummary::new(DEFAULT_MAX_MEMCMP)); + registry.register_summary(MemcmpSummary::new(budgets.max_memcmp)); registry.register_summary(MallocSummary::new()); registry.register_summary(FreeSummary::new()); - registry.register_summary(PutsSummary::new(DEFAULT_MAX_PRINTF_SCAN)); - registry.register_summary(PrintfSummaryBasic::new(DEFAULT_MAX_PRINTF_SCAN)); + registry.register_summary(PutsSummary::new(budgets.max_printf_scan)); + registry.register_summary(PrintfSummaryBasic::new(budgets.max_printf_scan)); registry.register_summary(ReadSummary::new()); registry.register_summary(IsattySummary::new()); registry.register_summary(SleepSummary::sleep()); @@ -200,7 +298,15 @@ impl<'ctx> SummaryRegistry<'ctx> { /// Create a registry pre-populated with core summaries for a known architecture. pub fn with_core_for_arch(arch: &ArchSpec) -> Option { - Some(Self::with_core(CallConv::for_arch_spec(arch)?)) + Some(Self::with_profile( + CallConv::for_arch_spec(arch)?, + SummaryProfile::Default, + )) + } + + /// Create a registry pre-populated with summaries for a typed workflow profile. + pub fn with_profile_for_arch(arch: &ArchSpec, profile: SummaryProfile) -> Option { + Some(Self::with_profile(CallConv::for_arch_spec(arch)?, profile)) } /// Register a function summary. @@ -619,12 +725,20 @@ fn adjust_bits<'ctx>(ctx: &'ctx z3::Context, value: SymValue<'ctx>, bits: u32) - /// memcpy(dst, src, n) summary. pub struct MemcpySummary { max_copy: u64, + byte_policy: ByteSummaryPolicy, } impl MemcpySummary { /// Create a memcpy summary with an upper bound on copy size. pub fn new(max_copy: u64) -> Self { - Self { max_copy } + Self::with_policy(max_copy, ByteSummaryPolicy::precise(max_copy)) + } + + fn with_policy(max_copy: u64, byte_policy: ByteSummaryPolicy) -> Self { + Self { + max_copy, + byte_policy, + } } } @@ -653,7 +767,7 @@ impl<'ctx> FunctionSummary<'ctx> for MemcpySummary { .get(2) .cloned() .unwrap_or_else(|| SymValue::unknown(call.arg_bits)); - copy_bytes(state, &dst, &src, &n, self.max_copy); + copy_bytes(state, &dst, &src, &n, self.max_copy, self.byte_policy); SummaryEffect::Return(Some(dst)) } } @@ -821,12 +935,20 @@ impl<'ctx> FunctionSummary<'ctx> for MemcmpSummary { /// memset(dst, c, n) summary. pub struct MemsetSummary { max_set: u64, + byte_policy: ByteSummaryPolicy, } impl MemsetSummary { /// Create a memset summary with an upper bound on set size. pub fn new(max_set: u64) -> Self { - Self { max_set } + Self::with_policy(max_set, ByteSummaryPolicy::precise(max_set)) + } + + fn with_policy(max_set: u64, byte_policy: ByteSummaryPolicy) -> Self { + Self { + max_set, + byte_policy, + } } } @@ -856,7 +978,7 @@ impl<'ctx> FunctionSummary<'ctx> for MemsetSummary { .cloned() .unwrap_or_else(|| SymValue::unknown(call.arg_bits)); - set_bytes(state, &dst, &c, &n, self.max_set); + set_bytes(state, &dst, &c, &n, self.max_set, self.byte_policy); SummaryEffect::Return(Some(dst)) } } @@ -1171,15 +1293,34 @@ fn copy_bytes<'ctx>( src: &SymValue<'ctx>, n: &SymValue<'ctx>, max_copy: u64, + byte_policy: ByteSummaryPolicy, ) { let ctx = state.context(); let n_concrete = n.as_concrete(); + if n_concrete == Some(0) { + return; + } let copy_len = n_concrete.unwrap_or(max_copy).min(max_copy); if n_concrete.is_none() { state.constrain_range(n, 0, max_copy); } + if !byte_policy.use_precise_model(n_concrete) { + let src_byte = if src.as_concrete().is_some() { + let read = state.mem_read(src, 1); + read.with_taint(read.get_taint() | src.get_taint() | n.get_taint()) + } else { + SymValue::symbolic_tainted( + BV::fresh_const("memcpy_byte", 8), + 8, + src.get_taint() | n.get_taint(), + ) + }; + state.mem_write(dst, &src_byte, 1); + return; + } + for offset in 0..copy_len { let offset_val = SymValue::concrete(offset, dst.bits()); let dst_addr = dst.add(ctx, &offset_val); @@ -1209,9 +1350,13 @@ fn set_bytes<'ctx>( c: &SymValue<'ctx>, n: &SymValue<'ctx>, max_set: u64, + byte_policy: ByteSummaryPolicy, ) { let ctx = state.context(); let n_concrete = n.as_concrete(); + if n_concrete == Some(0) { + return; + } let set_len = n_concrete.unwrap_or(max_set).min(max_set); if n_concrete.is_none() { @@ -1224,6 +1369,11 @@ fn set_bytes<'ctx>( c.extract(ctx, 7, 0).with_taint(c.get_taint()) }; + if !byte_policy.use_precise_model(n_concrete) { + state.mem_write(dst, &c_byte, 1); + return; + } + for offset in 0..set_len { let offset_val = SymValue::concrete(offset, dst.bits()); let dst_addr = dst.add(ctx, &offset_val); @@ -1264,5 +1414,61 @@ mod tests { arch.addr_size = 64; assert!(CallConv::for_arch_spec(&arch).is_some()); assert!(SummaryRegistry::with_core_for_arch(&arch).is_some()); + assert!( + SummaryRegistry::with_profile_for_arch(&arch, SummaryProfile::PathListing).is_some() + ); + } + + #[test] + fn memcpy_summary_path_listing_summarizes_symbolic_lengths() { + let ctx = z3::Context::thread_local(); + let src = SymValue::concrete(0x1000, 64); + let dst = SymValue::concrete(0x2000, 64); + let n = SymValue::symbolic(BV::fresh_const("n", 64), 64); + + let mut precise_state = SymState::new(&ctx, 0); + precise_state.mem_write( + &src, + &SymValue::symbolic_tainted(BV::fresh_const("src_base", 8), 8, 0x20), + 1, + ); + let src_far = src.add(&ctx, &SymValue::concrete(5, 64)); + precise_state.mem_write( + &src_far, + &SymValue::symbolic_tainted(BV::fresh_const("src_far", 8), 8, 0x40), + 1, + ); + let precise_summary = MemcpySummary::new(0x40); + let call = CallInfo { + args: vec![dst.clone(), src.clone(), n.clone()], + arg_bits: 64, + ret_bits: 64, + }; + let _ = precise_summary.execute(&mut precise_state, &call); + let precise_far = precise_state.mem_read(&dst.add(&ctx, &SymValue::concrete(5, 64)), 1); + assert_eq!(precise_far.get_taint(), 0x40); + + let mut path_listing_state = SymState::new(&ctx, 0); + path_listing_state.mem_write( + &src, + &SymValue::symbolic_tainted(BV::fresh_const("src_base_pl", 8), 8, 0x20), + 1, + ); + let src_far = src.add(&ctx, &SymValue::concrete(5, 64)); + path_listing_state.mem_write( + &src_far, + &SymValue::symbolic_tainted(BV::fresh_const("src_far_pl", 8), 8, 0x40), + 1, + ); + let path_listing_summary = MemcpySummary::with_policy( + 0x40, + ByteSummaryPolicy::summarized(PATH_LIST_PRECISE_BYTE_LIMIT), + ); + let _ = path_listing_summary.execute(&mut path_listing_state, &call); + let path_listing_far = + path_listing_state.mem_read(&dst.add(&ctx, &SymValue::concrete(5, 64)), 1); + assert_eq!(path_listing_far.get_taint(), 0); + let path_listing_base = path_listing_state.mem_read(&dst, 1); + assert_eq!(path_listing_base.get_taint(), 0x20); } } diff --git a/crates/r2sym/src/solver.rs b/crates/r2sym/src/solver.rs index 13aa123..64ca8b4 100644 --- a/crates/r2sym/src/solver.rs +++ b/crates/r2sym/src/solver.rs @@ -3,10 +3,11 @@ //! This module provides a high-level interface to Z3 for checking //! path feasibility and extracting concrete values. +use std::cell::RefCell; use std::collections::HashMap; use std::time::Duration; -use z3::ast::{BV, Bool}; +use z3::ast::{Ast, BV, Bool}; use z3::{Context, Model, Params, Solver}; use crate::state::SymState; @@ -33,6 +34,21 @@ impl From for SatResult { } } +/// Lightweight counters for solver-heavy symbolic execution workflows. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct SolverStats { + /// Number of state satisfiability queries. + pub sat_queries: usize, + /// Number of satisfiability queries answered from the cache. + pub sat_cache_hits: usize, + /// Number of satisfiability queries that required a solver check. + pub sat_cache_misses: usize, + /// Number of model-building solve requests. + pub solve_calls: usize, + /// Number of solve calls that returned early from a cached UNSAT result. + pub solve_unsat_shortcuts: usize, +} + /// A wrapper around Z3's solver with convenience methods. pub struct SymSolver<'ctx> { /// The Z3 context. @@ -41,6 +57,10 @@ pub struct SymSolver<'ctx> { solver: Solver, /// Timeout in milliseconds (0 = no timeout). _timeout_ms: u32, + /// Memoized satisfiability results for state queries. + sat_cache: RefCell>, + /// Internal counters used for perf/debug summaries. + stats: RefCell, } impl<'ctx> SymSolver<'ctx> { @@ -51,6 +71,8 @@ impl<'ctx> SymSolver<'ctx> { ctx, solver, _timeout_ms: 0, + sat_cache: RefCell::new(HashMap::new()), + stats: RefCell::new(SolverStats::default()), } } @@ -68,6 +90,8 @@ impl<'ctx> SymSolver<'ctx> { ctx, solver, _timeout_ms: timeout_ms, + sat_cache: RefCell::new(HashMap::new()), + stats: RefCell::new(SolverStats::default()), } } @@ -78,11 +102,38 @@ impl<'ctx> SymSolver<'ctx> { /// Add a constraint to the solver. pub fn assert(&self, constraint: &Bool) { - self.solver.assert(constraint); + self.clear_sat_cache(); + self.solver_assert(constraint); } /// Add multiple constraints. pub fn assert_all(&self, constraints: &[Bool]) { + if !constraints.is_empty() { + self.clear_sat_cache(); + } + self.solver_assert_all(constraints); + } + + /// Return current solver/cache counters. + pub fn stats(&self) -> SolverStats { + self.stats.borrow().clone() + } + + /// Reset solver/cache counters without touching constraints. + pub fn clear_stats(&self) { + *self.stats.borrow_mut() = SolverStats::default(); + } + + /// Drop memoized satisfiability results. + pub fn clear_sat_cache(&self) { + self.sat_cache.borrow_mut().clear(); + } + + fn solver_assert(&self, constraint: &Bool) { + self.solver.assert(constraint); + } + + fn solver_assert_all(&self, constraints: &[Bool]) { for c in constraints { self.solver.assert(c); } @@ -105,40 +156,74 @@ impl<'ctx> SymSolver<'ctx> { /// Push a new scope (for backtracking). pub fn push(&self) { + self.clear_sat_cache(); self.solver.push(); } /// Pop a scope. pub fn pop(&self, n: u32) { + self.clear_sat_cache(); self.solver.pop(n); } /// Reset the solver. pub fn reset(&self) { + self.clear_sat_cache(); self.solver.reset(); } + fn state_sat_cache_key(&self, state: &SymState<'ctx>) -> String { + match state.constraints() { + [] => "true".to_string(), + [constraint] => constraint.simplify().to_string(), + _ => state.path_condition().simplify().to_string(), + } + } + /// Check if a state's path constraints are satisfiable. pub fn is_sat(&self, state: &SymState<'ctx>) -> bool { - self.push(); - self.assert_all(state.constraints()); + self.stats.borrow_mut().sat_queries += 1; + + let cache_key = self.state_sat_cache_key(state); + if let Some(result) = self.sat_cache.borrow().get(&cache_key).copied() { + self.stats.borrow_mut().sat_cache_hits += 1; + return result == SatResult::Sat; + } + + self.stats.borrow_mut().sat_cache_misses += 1; + self.solver.push(); + self.solver_assert_all(state.constraints()); let result = self.check(); - self.pop(1); + self.solver.pop(1); + self.sat_cache.borrow_mut().insert(cache_key, result); result == SatResult::Sat } /// Get a concrete model for a state's constraints. pub fn solve(&self, state: &SymState<'ctx>) -> Option> { - self.push(); - self.assert_all(state.constraints()); + self.stats.borrow_mut().solve_calls += 1; + + let cache_key = self.state_sat_cache_key(state); + if matches!( + self.sat_cache.borrow().get(&cache_key), + Some(SatResult::Unsat) + ) { + self.stats.borrow_mut().solve_unsat_shortcuts += 1; + return None; + } - let result = if self.check() == SatResult::Sat { + self.solver.push(); + self.solver_assert_all(state.constraints()); + + let sat_result = self.check(); + let result = if sat_result == SatResult::Sat { self.get_model().map(|m| SymModel::new(self.ctx, m)) } else { None }; - self.pop(1); + self.solver.pop(1); + self.sat_cache.borrow_mut().insert(cache_key, sat_result); result } @@ -157,9 +242,9 @@ impl<'ctx> SymSolver<'ctx> { target: &SymValue<'ctx>, constraint: &Bool, ) -> Option { - self.push(); - self.assert_all(state.constraints()); - self.assert(constraint); + self.solver.push(); + self.solver_assert_all(state.constraints()); + self.solver_assert(constraint); let result = if self.check() == SatResult::Sat { self.eval(target) @@ -167,7 +252,7 @@ impl<'ctx> SymSolver<'ctx> { None }; - self.pop(1); + self.solver.pop(1); result } @@ -194,11 +279,11 @@ impl<'ctx> SymSolver<'ctx> { }; let eq = a_bv.eq(&b_bv); - self.push(); - self.assert_all(state.constraints()); - self.assert(&eq); + self.solver.push(); + self.solver_assert_all(state.constraints()); + self.solver_assert(&eq); let result = self.check() == SatResult::Sat; - self.pop(1); + self.solver.pop(1); result } @@ -209,11 +294,11 @@ impl<'ctx> SymSolver<'ctx> { let zero = BV::from_i64(0, value.bits()); let eq = bv.eq(&zero); - self.push(); - self.assert_all(state.constraints()); - self.assert(&eq); + self.solver.push(); + self.solver_assert_all(state.constraints()); + self.solver_assert(&eq); let result = self.check() == SatResult::Sat; - self.pop(1); + self.solver.pop(1); result } @@ -224,11 +309,11 @@ impl<'ctx> SymSolver<'ctx> { let zero = BV::from_i64(0, value.bits()); let neq = bv.eq(&zero).not(); - self.push(); - self.assert_all(state.constraints()); - self.assert(&neq); + self.solver.push(); + self.solver_assert_all(state.constraints()); + self.solver_assert(&neq); let result = self.check() == SatResult::Unsat; - self.pop(1); + self.solver.pop(1); result } @@ -236,8 +321,8 @@ impl<'ctx> SymSolver<'ctx> { /// Get the minimum value for a symbolic expression. pub fn minimize(&self, state: &SymState<'ctx>, value: &SymValue<'ctx>) -> Option { // Binary search for minimum - self.push(); - self.assert_all(state.constraints()); + self.solver.push(); + self.solver_assert_all(state.constraints()); let bv = value.to_bv(self.ctx); let bits = value.bits(); @@ -256,8 +341,8 @@ impl<'ctx> SymSolver<'ctx> { let mid_bv = BV::from_u64(mid, bits); let constraint = bv.bvule(&mid_bv); - self.push(); - self.assert(&constraint); + self.solver.push(); + self.solver_assert(&constraint); if self.check() == SatResult::Sat { result = self.eval(value); @@ -266,21 +351,21 @@ impl<'ctx> SymSolver<'ctx> { lo = mid.saturating_add(1); } - self.pop(1); + self.solver.pop(1); if lo == 0 && hi == u64::MAX { break; // Prevent infinite loop } } - self.pop(1); + self.solver.pop(1); result } /// Get the maximum value for a symbolic expression. pub fn maximize(&self, state: &SymState<'ctx>, value: &SymValue<'ctx>) -> Option { - self.push(); - self.assert_all(state.constraints()); + self.solver.push(); + self.solver_assert_all(state.constraints()); let bv = value.to_bv(self.ctx); let bits = value.bits(); @@ -298,8 +383,8 @@ impl<'ctx> SymSolver<'ctx> { let mid_bv = BV::from_u64(mid, bits); let constraint = bv.bvuge(&mid_bv); - self.push(); - self.assert(&constraint); + self.solver.push(); + self.solver_assert(&constraint); if self.check() == SatResult::Sat { result = self.eval(value); @@ -308,14 +393,14 @@ impl<'ctx> SymSolver<'ctx> { hi = mid.saturating_sub(1); } - self.pop(1); + self.solver.pop(1); if lo == 0 && hi == u64::MAX { break; } } - self.pop(1); + self.solver.pop(1); result } } @@ -451,6 +536,27 @@ mod tests { assert!(solver.is_sat(&state)); } + #[test] + fn test_state_sat_cache_hit() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("x", 32); + let x = state.get_register("x"); + let five = SymValue::concrete(5, 32); + let cond = x.ult(&ctx, &five); + state.add_true_constraint(&cond); + + let solver = SymSolver::new(&ctx); + assert!(solver.is_sat(&state)); + assert!(solver.is_sat(&state)); + + let stats = solver.stats(); + assert_eq!(stats.sat_queries, 2); + assert_eq!(stats.sat_cache_hits, 1); + assert_eq!(stats.sat_cache_misses, 1); + } + #[test] fn test_solve() { let ctx = Context::thread_local(); @@ -470,4 +576,26 @@ mod tests { let x_value = model.eval(&x); assert_eq!(x_value, Some(10)); } + + #[test] + fn test_solve_uses_cached_unsat_shortcut() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("x", 32); + + let x = state.get_register("x"); + let five = SymValue::concrete(5, 32); + let three = SymValue::concrete(3, 32); + state.add_true_constraint(&x.ult(&ctx, &three)); + state.add_true_constraint(&x.eq(&ctx, &five)); + + let solver = SymSolver::new(&ctx); + assert!(!solver.is_sat(&state)); + assert!(solver.solve(&state).is_none()); + + let stats = solver.stats(); + assert_eq!(stats.solve_calls, 1); + assert_eq!(stats.solve_unsat_shortcuts, 1); + } } diff --git a/crates/r2sym/tests/symex_integration.rs b/crates/r2sym/tests/symex_integration.rs index 389fd0e..f7d96f0 100644 --- a/crates/r2sym/tests/symex_integration.rs +++ b/crates/r2sym/tests/symex_integration.rs @@ -206,6 +206,7 @@ fn test_symbolic_execution_conditional_branch() { let config = ExploreConfig { max_states: 100, + max_completed_paths: None, max_depth: 50, timeout: None, strategy: ExploreStrategy::Dfs, @@ -350,6 +351,7 @@ fn test_find_paths_to_honors_limits() { let config = ExploreConfig { max_states: 0, + max_completed_paths: None, max_depth: 50, timeout: None, strategy: ExploreStrategy::Dfs, @@ -365,6 +367,138 @@ fn test_find_paths_to_honors_limits() { ); } +#[test] +fn test_find_paths_to_with_same_pc_merge_still_reaches_target() { + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![ + R2ILOp::IntEqual { + dst: make_reg(RCX, 1), + a: make_reg(RDI, 8), + b: make_const(0x1337, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(RCX, 1), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x1010, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + + let func = SsaArtifact::for_symbolic(&blocks, None).expect("Failed to build SSA function"); + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("reg:56_0", 64); + + let config = ExploreConfig { + max_states: 100, + max_completed_paths: None, + max_depth: 50, + timeout: None, + strategy: ExploreStrategy::Bfs, + prune_infeasible: true, + merge_states: true, + }; + let mut explorer = PathExplorer::with_config(&ctx, config); + let paths = explorer.find_paths_to(&func, state, 0x1010); + + assert!( + !paths.is_empty(), + "Expected at least one feasible merged path to target" + ); + assert!(paths.iter().all(|path| path.final_pc() == 0x1010)); + assert!(paths.iter().all(|path| path.feasible)); +} + +#[test] +fn test_explore_honors_max_completed_paths() { + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![ + R2ILOp::IntEqual { + dst: make_reg(RCX, 1), + a: make_reg(RDI, 8), + b: make_const(0x1337, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(RCX, 1), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + + let func = SsaArtifact::for_symbolic(&blocks, None).expect("Failed to build SSA function"); + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("reg:56_0", 64); + + let config = ExploreConfig { + max_states: 100, + max_completed_paths: Some(1), + max_depth: 50, + timeout: None, + strategy: ExploreStrategy::Bfs, + prune_infeasible: true, + merge_states: false, + }; + let mut explorer = PathExplorer::with_config(&ctx, config); + let paths = explorer.explore(&func, state); + + assert_eq!( + paths.len(), + 1, + "explore should stop after the configured cap" + ); +} + #[test] fn test_symbolic_arithmetic_operations() { // Test all arithmetic operations with symbolic values diff --git a/r2plugin/r_anal_sleigh.c b/r2plugin/r_anal_sleigh.c index 6c45b65..c8ddc83 100644 --- a/r2plugin/r_anal_sleigh.c +++ b/r2plugin/r_anal_sleigh.c @@ -953,6 +953,1390 @@ static bool parse_sym_target_expr(RCore *core, const char *expr, ut64 *target) { return true; } +typedef enum { + REPLAY_EXPR_CONST = 0, + REPLAY_EXPR_REG, + REPLAY_EXPR_MEM, + REPLAY_EXPR_META, + REPLAY_EXPR_UNARY, + REPLAY_EXPR_BINARY, +} ReplayExprKind; + +typedef enum { + REPLAY_MEM_U8 = 8, + REPLAY_MEM_U16 = 16, + REPLAY_MEM_U32 = 32, + REPLAY_MEM_U64 = 64, +} ReplayMemWidth; + +typedef enum { + REPLAY_META_DEPTH = 0, + REPLAY_META_INPUT_LEN, +} ReplayMetaKind; + +typedef enum { + REPLAY_UN_NEG = 0, + REPLAY_UN_NOT, +} ReplayUnaryOp; + +typedef enum { + REPLAY_BIN_ADD = 0, + REPLAY_BIN_SUB, + REPLAY_BIN_MUL, + REPLAY_BIN_DIV, + REPLAY_BIN_MOD, + REPLAY_BIN_SHL, + REPLAY_BIN_SHR, + REPLAY_BIN_BAND, + REPLAY_BIN_BOR, + REPLAY_BIN_BXOR, + REPLAY_BIN_EQ, + REPLAY_BIN_NE, + REPLAY_BIN_LT, + REPLAY_BIN_LE, + REPLAY_BIN_GT, + REPLAY_BIN_GE, + REPLAY_BIN_AND, + REPLAY_BIN_OR, + REPLAY_BIN_ABSDIFF, +} ReplayBinaryOp; + +typedef enum { + REPLAY_SCORE_MAX = 0, + REPLAY_SCORE_MIN, +} ReplayScoreOrder; + +typedef struct replay_expr_t ReplayExpr; + +struct replay_expr_t { + int kind; + union { + st64 const_value; + char *reg_name; + struct { + ut64 addr; + int width_bits; + } mem; + int meta_kind; + struct { + int op; + ReplayExpr *arg; + } unary; + struct { + int op; + ReplayExpr *lhs; + ReplayExpr *rhs; + } binary; + }; +}; + +typedef struct { + bool ok; + bool is_bool; + union { + st64 i; + bool b; + }; +} ReplayEvalValue; + +typedef struct { + const RDebugStateSnapshot *snapshot; + size_t depth; + size_t input_len; + bool big_endian; +} ReplayEvalContext; + +typedef struct { + ut64 seed_checkpoint; + int replay_fd; + char *alphabet; + size_t max_depth; + size_t beam_width; + ReplayExpr **frontier_preds; + size_t frontier_count; + ReplayExpr **find_preds; + size_t find_count; + ReplayExpr **avoid_preds; + size_t avoid_count; + ReplayExpr *score_expr; + int score_order; + RDebugStateRequest *snapshot_request; + ut64 *frontier_stop_addrs; + size_t frontier_stop_count; + ut64 *stop_addrs; + size_t stop_count; + bool big_endian; +} ReplaySearchSpec; + +typedef struct { + ut64 checkpoint_id; + char *input; + size_t input_len; + st64 score; + char *snapshot_json; +} ReplaySearchNode; + +typedef struct { + ut64 checkpoint_id; + char *input; + size_t input_len; + ut64 hit_addr; + st64 score; + char *snapshot_json; +} ReplaySearchMatch; + +typedef enum { + REPLAY_SEARCH_STOP_NONE = 0, + REPLAY_SEARCH_STOP_FRONTIER, + REPLAY_SEARCH_STOP_FIND, + REPLAY_SEARCH_STOP_AVOID, + REPLAY_SEARCH_STOP_OTHER, +} ReplaySearchStopKind; + +typedef struct { + ut64 *addrs; + size_t count; +} ReplayTempBpSet; + +static bool replay_parse_num_expr(RCore *core, const RJson *value, st64 *out) { + if (!core || !value || !out) { + return false; + } + if (value->type == R_JSON_INTEGER) { + *out = value->num.s_value; + return true; + } + if (value->type == R_JSON_STRING && value->str_value && *value->str_value) { + *out = (st64)r_num_math (core->num, value->str_value); + return true; + } + return false; +} + +static bool replay_parse_addr_expr(RCore *core, const RJson *value, ut64 *out) { + st64 signed_value = 0; + if (!replay_parse_num_expr (core, value, &signed_value) || signed_value < 0) { + return false; + } + *out = (ut64)signed_value; + return true; +} + +static void replay_expr_free(ReplayExpr *expr) { + if (!expr) { + return; + } + switch (expr->kind) { + case REPLAY_EXPR_REG: + free (expr->reg_name); + break; + case REPLAY_EXPR_UNARY: + replay_expr_free (expr->unary.arg); + break; + case REPLAY_EXPR_BINARY: + replay_expr_free (expr->binary.lhs); + replay_expr_free (expr->binary.rhs); + break; + default: + break; + } + free (expr); +} + +static void replay_expr_array_free(ReplayExpr **exprs, size_t count) { + size_t i; + if (!exprs) { + return; + } + for (i = 0; i < count; i++) { + replay_expr_free (exprs[i]); + } + free (exprs); +} + +static bool replay_is_pc_reg_name(const char *name) { + return name && !strcasecmp (name, "pc"); +} + +static bool replay_expr_is_const_int(const ReplayExpr *expr, st64 *out) { + if (!expr || expr->kind != REPLAY_EXPR_CONST) { + return false; + } + if (out) { + *out = expr->const_value; + } + return true; +} + +static bool replay_expr_extract_pc_eq_addr(const ReplayExpr *expr, ut64 *out_addr) { + st64 value = 0; + if (!expr || expr->kind != REPLAY_EXPR_BINARY || expr->binary.op != REPLAY_BIN_EQ) { + return false; + } + if (expr->binary.lhs && expr->binary.lhs->kind == REPLAY_EXPR_REG + && replay_is_pc_reg_name (expr->binary.lhs->reg_name) + && replay_expr_is_const_int (expr->binary.rhs, &value) + && value >= 0) { + *out_addr = (ut64)value; + return true; + } + if (expr->binary.rhs && expr->binary.rhs->kind == REPLAY_EXPR_REG + && replay_is_pc_reg_name (expr->binary.rhs->reg_name) + && replay_expr_is_const_int (expr->binary.lhs, &value) + && value >= 0) { + *out_addr = (ut64)value; + return true; + } + return false; +} + +static bool replay_addr_list_contains(const ut64 *addrs, size_t count, ut64 addr) { + size_t i; + for (i = 0; i < count; i++) { + if (addrs[i] == addr) { + return true; + } + } + return false; +} + +static bool replay_addr_list_push_unique(ut64 **addrs, size_t *count, ut64 addr) { + ut64 *next; + if (!addrs || !count || !addr) { + return false; + } + if (replay_addr_list_contains (*addrs, *count, addr)) { + return true; + } + next = realloc (*addrs, (*count + 1) * sizeof (ut64)); + if (!next) { + return false; + } + *addrs = next; + (*addrs)[(*count)++] = addr; + return true; +} + +static const char *replay_json_kind_name(const RJson *value) { + if (!value || value->type != R_JSON_OBJECT) { + return NULL; + } + const RJson *kind = r_json_get (value, "kind"); + return (kind && kind->type == R_JSON_STRING)? kind->str_value: NULL; +} + +static const char *replay_json_op_name(const RJson *value) { + if (!value || value->type != R_JSON_OBJECT) { + return NULL; + } + const RJson *op = r_json_get (value, "op"); + return (op && op->type == R_JSON_STRING)? op->str_value: NULL; +} + +static bool replay_json_get_arg_array(const RJson *value, const RJson **first, const RJson **second, size_t *count) { + const RJson *args = r_json_get (value, "args"); + RJson *child; + size_t idx = 0; + if (!first || !second || !count) { + return false; + } + *first = NULL; + *second = NULL; + *count = 0; + if (!args || args->type != R_JSON_ARRAY) { + return false; + } + for (child = args->children.first; child; child = child->next) { + if (idx == 0) { + *first = child; + } else if (idx == 1) { + *second = child; + } + idx++; + } + *count = idx; + return true; +} + +static bool replay_parse_unary_op(const char *name, int *out) { + if (!name || !out) { + return false; + } + if (!strcmp (name, "neg")) { + *out = REPLAY_UN_NEG; + return true; + } + if (!strcmp (name, "not")) { + *out = REPLAY_UN_NOT; + return true; + } + return false; +} + +static bool replay_parse_binary_op(const char *name, int *out) { + if (!name || !out) { + return false; + } + if (!strcmp (name, "add")) { *out = REPLAY_BIN_ADD; return true; } + if (!strcmp (name, "sub")) { *out = REPLAY_BIN_SUB; return true; } + if (!strcmp (name, "mul")) { *out = REPLAY_BIN_MUL; return true; } + if (!strcmp (name, "div")) { *out = REPLAY_BIN_DIV; return true; } + if (!strcmp (name, "mod")) { *out = REPLAY_BIN_MOD; return true; } + if (!strcmp (name, "shl")) { *out = REPLAY_BIN_SHL; return true; } + if (!strcmp (name, "shr")) { *out = REPLAY_BIN_SHR; return true; } + if (!strcmp (name, "band")) { *out = REPLAY_BIN_BAND; return true; } + if (!strcmp (name, "bor")) { *out = REPLAY_BIN_BOR; return true; } + if (!strcmp (name, "bxor")) { *out = REPLAY_BIN_BXOR; return true; } + if (!strcmp (name, "eq")) { *out = REPLAY_BIN_EQ; return true; } + if (!strcmp (name, "ne")) { *out = REPLAY_BIN_NE; return true; } + if (!strcmp (name, "lt")) { *out = REPLAY_BIN_LT; return true; } + if (!strcmp (name, "le")) { *out = REPLAY_BIN_LE; return true; } + if (!strcmp (name, "gt")) { *out = REPLAY_BIN_GT; return true; } + if (!strcmp (name, "ge")) { *out = REPLAY_BIN_GE; return true; } + if (!strcmp (name, "and")) { *out = REPLAY_BIN_AND; return true; } + if (!strcmp (name, "or")) { *out = REPLAY_BIN_OR; return true; } + if (!strcmp (name, "absdiff")) { *out = REPLAY_BIN_ABSDIFF; return true; } + return false; +} + +static bool replay_parse_meta_kind(const char *name, int *out) { + if (!name || !out) { + return false; + } + if (!strcmp (name, "depth")) { + *out = REPLAY_META_DEPTH; + return true; + } + if (!strcmp (name, "input_len")) { + *out = REPLAY_META_INPUT_LEN; + return true; + } + return false; +} + +static bool replay_expr_parse(RCore *core, const RJson *value, ReplayExpr **out_expr) { + const char *kind_name; + const char *op_name; + ReplayExpr *expr = NULL; + const RJson *lhs = NULL; + const RJson *rhs = NULL; + const RJson *arg = NULL; + const RJson *first = NULL; + const RJson *second = NULL; + size_t arg_count = 0; + + R_RETURN_VAL_IF_FAIL (core && value && out_expr, false); + *out_expr = NULL; + if (value->type != R_JSON_OBJECT) { + return false; + } + expr = R_NEW0 (ReplayExpr); + if (!expr) { + return false; + } + + kind_name = replay_json_kind_name (value); + if (kind_name) { + if (!strcmp (kind_name, "const")) { + expr->kind = REPLAY_EXPR_CONST; + if (!replay_parse_num_expr (core, r_json_get (value, "value"), &expr->const_value)) { + goto fail; + } + } else if (!strcmp (kind_name, "reg")) { + const RJson *name = r_json_get (value, "name"); + expr->kind = REPLAY_EXPR_REG; + if (!name || name->type != R_JSON_STRING || R_STR_ISEMPTY (name->str_value)) { + goto fail; + } + expr->reg_name = strdup (name->str_value); + if (!expr->reg_name) { + goto fail; + } + } else if (!strcmp (kind_name, "mem_u8") || !strcmp (kind_name, "mem_u16") + || !strcmp (kind_name, "mem_u32") || !strcmp (kind_name, "mem_u64")) { + expr->kind = REPLAY_EXPR_MEM; + if (!replay_parse_addr_expr (core, r_json_get (value, "addr"), &expr->mem.addr)) { + goto fail; + } + if (!strcmp (kind_name, "mem_u8")) { + expr->mem.width_bits = REPLAY_MEM_U8; + } else if (!strcmp (kind_name, "mem_u16")) { + expr->mem.width_bits = REPLAY_MEM_U16; + } else if (!strcmp (kind_name, "mem_u32")) { + expr->mem.width_bits = REPLAY_MEM_U32; + } else { + expr->mem.width_bits = REPLAY_MEM_U64; + } + } else if (!strcmp (kind_name, "meta")) { + const RJson *name = r_json_get (value, "name"); + expr->kind = REPLAY_EXPR_META; + if (!name || name->type != R_JSON_STRING || !replay_parse_meta_kind (name->str_value, &expr->meta_kind)) { + goto fail; + } + } else { + goto fail; + } + *out_expr = expr; + return true; + } + + op_name = replay_json_op_name (value); + if (!op_name) { + goto fail; + } + if (replay_parse_unary_op (op_name, &expr->unary.op)) { + expr->kind = REPLAY_EXPR_UNARY; + arg = r_json_get (value, "arg"); + if (!arg && replay_json_get_arg_array (value, &first, &second, &arg_count) && arg_count == 1) { + arg = first; + } + if (!arg || !replay_expr_parse (core, arg, &expr->unary.arg)) { + goto fail; + } + *out_expr = expr; + return true; + } + if (!replay_parse_binary_op (op_name, &expr->binary.op)) { + goto fail; + } + expr->kind = REPLAY_EXPR_BINARY; + lhs = r_json_get (value, "lhs"); + rhs = r_json_get (value, "rhs"); + if ((!lhs || !rhs) && replay_json_get_arg_array (value, &first, &second, &arg_count) && arg_count == 2) { + lhs = first; + rhs = second; + } + if (!lhs || !rhs) { + goto fail; + } + if (!replay_expr_parse (core, lhs, &expr->binary.lhs) || !replay_expr_parse (core, rhs, &expr->binary.rhs)) { + goto fail; + } + *out_expr = expr; + return true; + +fail: + replay_expr_free (expr); + return false; +} + +static bool replay_parse_predicate_array(RCore *core, const RJson *value, bool allow_empty, ReplayExpr ***out_exprs, size_t *out_count) { + ReplayExpr **exprs = NULL; + size_t count = 0; + RJson *child; + if (!out_exprs || !out_count) { + return false; + } + *out_exprs = NULL; + *out_count = 0; + if (!value || value->type != R_JSON_ARRAY) { + return false; + } + for (child = value->children.first; child; child = child->next) { + ReplayExpr *expr = NULL; + ReplayExpr **next; + if (!replay_expr_parse (core, child, &expr)) { + replay_expr_array_free (exprs, count); + return false; + } + next = realloc (exprs, (count + 1) * sizeof (ReplayExpr *)); + if (!next) { + replay_expr_free (expr); + replay_expr_array_free (exprs, count); + return false; + } + exprs = next; + exprs[count++] = expr; + } + if (!count) { + return allow_empty; + } + *out_exprs = exprs; + *out_count = count; + return true; +} + +static RDebugStateRequest *replay_state_request_new(void) { + RDebugStateRequest *request = R_NEW0 (RDebugStateRequest); + if (!request) { + return NULL; + } + request->registers = r_list_newf ((RListFree)r_debug_state_reg_spec_free); + request->memory = r_list_newf ((RListFree)r_debug_state_mem_spec_free); + if (!request->registers || !request->memory) { + r_debug_state_request_free (request); + return NULL; + } + return request; +} + +static bool replay_state_request_add_reg(RDebugStateRequest *request, const char *name) { + RListIter *iter; + RDebugStateRegSpec *spec; + if (!request || !name || replay_is_pc_reg_name (name)) { + return true; + } + r_list_foreach (request->registers, iter, spec) { + if (spec->name && !strcasecmp (spec->name, name)) { + return true; + } + } + spec = R_NEW0 (RDebugStateRegSpec); + if (!spec) { + return false; + } + spec->name = strdup (name); + if (!spec->name) { + r_debug_state_reg_spec_free (spec); + return false; + } + r_list_append (request->registers, spec); + return true; +} + +static bool replay_state_request_add_mem(RDebugStateRequest *request, ut64 addr, int width_bits) { + RListIter *iter; + RDebugStateMemSpec *spec; + ut32 size = (ut32)(width_bits / 8); + if (!request || !size) { + return false; + } + r_list_foreach (request->memory, iter, spec) { + if (spec->addr == addr && spec->size == size) { + return true; + } + } + spec = R_NEW0 (RDebugStateMemSpec); + if (!spec) { + return false; + } + spec->addr = addr; + spec->size = size; + r_list_append (request->memory, spec); + return true; +} + +static bool replay_expr_collect_state(const ReplayExpr *expr, RDebugStateRequest *request) { + if (!expr || !request) { + return false; + } + switch (expr->kind) { + case REPLAY_EXPR_REG: + return replay_state_request_add_reg (request, expr->reg_name); + case REPLAY_EXPR_MEM: + return replay_state_request_add_mem (request, expr->mem.addr, expr->mem.width_bits); + case REPLAY_EXPR_UNARY: + return replay_expr_collect_state (expr->unary.arg, request); + case REPLAY_EXPR_BINARY: + return replay_expr_collect_state (expr->binary.lhs, request) + && replay_expr_collect_state (expr->binary.rhs, request); + default: + return true; + } +} + +static bool replay_collect_stop_addrs(ReplayExpr **exprs, size_t count, ut64 **out_addrs, size_t *out_count) { + size_t i; + if (!out_addrs || !out_count) { + return false; + } + *out_addrs = NULL; + *out_count = 0; + for (i = 0; i < count; i++) { + ut64 addr = 0; + if (replay_expr_extract_pc_eq_addr (exprs[i], &addr) && !replay_addr_list_push_unique (out_addrs, out_count, addr)) { + free (*out_addrs); + *out_addrs = NULL; + *out_count = 0; + return false; + } + } + return true; +} + +static void replay_search_spec_fini(ReplaySearchSpec *spec) { + if (!spec) { + return; + } + free (spec->alphabet); + spec->alphabet = NULL; + replay_expr_array_free (spec->frontier_preds, spec->frontier_count); + replay_expr_array_free (spec->find_preds, spec->find_count); + replay_expr_array_free (spec->avoid_preds, spec->avoid_count); + spec->frontier_preds = NULL; + spec->find_preds = NULL; + spec->avoid_preds = NULL; + spec->frontier_count = 0; + spec->find_count = 0; + spec->avoid_count = 0; + replay_expr_free (spec->score_expr); + spec->score_expr = NULL; + r_debug_state_request_free (spec->snapshot_request); + spec->snapshot_request = NULL; + free (spec->frontier_stop_addrs); + free (spec->stop_addrs); + spec->frontier_stop_addrs = NULL; + spec->stop_addrs = NULL; + spec->frontier_stop_count = 0; + spec->stop_count = 0; +} + +static void replay_search_node_free(ReplaySearchNode *node) { + if (!node) { + return; + } + free (node->input); + free (node->snapshot_json); + free (node); +} + +static void replay_search_match_free(ReplaySearchMatch *match) { + if (!match) { + return; + } + free (match->input); + free (match->snapshot_json); + free (match); +} + +static bool replay_eval_snapshot_reg(const ReplayEvalContext *ctx, const char *name, st64 *out) { + RListIter *iter; + RDebugStateRegValue *reg; + if (!ctx || !ctx->snapshot || !name || !out) { + return false; + } + if (replay_is_pc_reg_name (name)) { + *out = (st64)ctx->snapshot->pc; + return true; + } + r_list_foreach (ctx->snapshot->registers, iter, reg) { + if (reg->name && !strcasecmp (reg->name, name) && reg->found) { + *out = (st64)reg->value; + return true; + } + } + return false; +} + +static bool replay_eval_snapshot_mem(const ReplayEvalContext *ctx, ut64 addr, int width_bits, st64 *out) { + RListIter *iter; + RDebugStateMemValue *mem; + ut32 size = (ut32)(width_bits / 8); + if (!ctx || !ctx->snapshot || !out || !size) { + return false; + } + r_list_foreach (ctx->snapshot->memory, iter, mem) { + if (mem->addr == addr && mem->size == size && mem->ok && mem->bytes) { + *out = (st64)r_read_ble (mem->bytes, ctx->big_endian, size); + return true; + } + } + return false; +} + +static ReplayEvalValue replay_eval_error(void) { + ReplayEvalValue value = {0}; + return value; +} + +static ReplayEvalValue replay_eval_int(st64 i) { + ReplayEvalValue value = {0}; + value.ok = true; + value.i = i; + value.is_bool = false; + return value; +} + +static ReplayEvalValue replay_eval_bool(bool b) { + ReplayEvalValue value = {0}; + value.ok = true; + value.b = b; + value.is_bool = true; + return value; +} + +static ReplayEvalValue replay_eval_expr(const ReplayExpr *expr, const ReplayEvalContext *ctx) { + ReplayEvalValue lhs; + ReplayEvalValue rhs; + if (!expr || !ctx) { + return replay_eval_error (); + } + switch (expr->kind) { + case REPLAY_EXPR_CONST: + return replay_eval_int (expr->const_value); + case REPLAY_EXPR_REG: { + st64 value = 0; + return replay_eval_snapshot_reg (ctx, expr->reg_name, &value)? replay_eval_int (value): replay_eval_error (); + } + case REPLAY_EXPR_MEM: { + st64 value = 0; + return replay_eval_snapshot_mem (ctx, expr->mem.addr, expr->mem.width_bits, &value)? replay_eval_int (value): replay_eval_error (); + } + case REPLAY_EXPR_META: + return replay_eval_int (expr->meta_kind == REPLAY_META_DEPTH? (st64)ctx->depth: (st64)ctx->input_len); + case REPLAY_EXPR_UNARY: { + ReplayEvalValue arg = replay_eval_expr (expr->unary.arg, ctx); + if (!arg.ok) { + return arg; + } + if (expr->unary.op == REPLAY_UN_NEG) { + return arg.is_bool? replay_eval_error (): replay_eval_int (-arg.i); + } + if (expr->unary.op == REPLAY_UN_NOT) { + return arg.is_bool? replay_eval_bool (!arg.b): replay_eval_error (); + } + return replay_eval_error (); + } + case REPLAY_EXPR_BINARY: + lhs = replay_eval_expr (expr->binary.lhs, ctx); + rhs = replay_eval_expr (expr->binary.rhs, ctx); + if (!lhs.ok || !rhs.ok) { + return replay_eval_error (); + } + switch (expr->binary.op) { + case REPLAY_BIN_ADD: return (!lhs.is_bool && !rhs.is_bool)? replay_eval_int (lhs.i + rhs.i): replay_eval_error (); + case REPLAY_BIN_SUB: return (!lhs.is_bool && !rhs.is_bool)? replay_eval_int (lhs.i - rhs.i): replay_eval_error (); + case REPLAY_BIN_MUL: return (!lhs.is_bool && !rhs.is_bool)? replay_eval_int (lhs.i * rhs.i): replay_eval_error (); + case REPLAY_BIN_DIV: return (!lhs.is_bool && !rhs.is_bool && rhs.i != 0)? replay_eval_int (lhs.i / rhs.i): replay_eval_error (); + case REPLAY_BIN_MOD: return (!lhs.is_bool && !rhs.is_bool && rhs.i != 0)? replay_eval_int (lhs.i % rhs.i): replay_eval_error (); + case REPLAY_BIN_SHL: return (!lhs.is_bool && !rhs.is_bool && rhs.i >= 0)? replay_eval_int ((st64)((ut64)lhs.i << rhs.i)): replay_eval_error (); + case REPLAY_BIN_SHR: return (!lhs.is_bool && !rhs.is_bool && rhs.i >= 0)? replay_eval_int ((st64)((ut64)lhs.i >> rhs.i)): replay_eval_error (); + case REPLAY_BIN_BAND: return (!lhs.is_bool && !rhs.is_bool)? replay_eval_int ((st64)((ut64)lhs.i & (ut64)rhs.i)): replay_eval_error (); + case REPLAY_BIN_BOR: return (!lhs.is_bool && !rhs.is_bool)? replay_eval_int ((st64)((ut64)lhs.i | (ut64)rhs.i)): replay_eval_error (); + case REPLAY_BIN_BXOR: return (!lhs.is_bool && !rhs.is_bool)? replay_eval_int ((st64)((ut64)lhs.i ^ (ut64)rhs.i)): replay_eval_error (); + case REPLAY_BIN_EQ: + if (lhs.is_bool != rhs.is_bool) { + return replay_eval_error (); + } + return lhs.is_bool? replay_eval_bool (lhs.b == rhs.b): replay_eval_bool (lhs.i == rhs.i); + case REPLAY_BIN_NE: + if (lhs.is_bool != rhs.is_bool) { + return replay_eval_error (); + } + return lhs.is_bool? replay_eval_bool (lhs.b != rhs.b): replay_eval_bool (lhs.i != rhs.i); + case REPLAY_BIN_LT: return (!lhs.is_bool && !rhs.is_bool)? replay_eval_bool (lhs.i < rhs.i): replay_eval_error (); + case REPLAY_BIN_LE: return (!lhs.is_bool && !rhs.is_bool)? replay_eval_bool (lhs.i <= rhs.i): replay_eval_error (); + case REPLAY_BIN_GT: return (!lhs.is_bool && !rhs.is_bool)? replay_eval_bool (lhs.i > rhs.i): replay_eval_error (); + case REPLAY_BIN_GE: return (!lhs.is_bool && !rhs.is_bool)? replay_eval_bool (lhs.i >= rhs.i): replay_eval_error (); + case REPLAY_BIN_AND: return (lhs.is_bool && rhs.is_bool)? replay_eval_bool (lhs.b && rhs.b): replay_eval_error (); + case REPLAY_BIN_OR: return (lhs.is_bool && rhs.is_bool)? replay_eval_bool (lhs.b || rhs.b): replay_eval_error (); + case REPLAY_BIN_ABSDIFF: + if (lhs.is_bool || rhs.is_bool) { + return replay_eval_error (); + } + return replay_eval_int (lhs.i > rhs.i? lhs.i - rhs.i: rhs.i - lhs.i); + default: + return replay_eval_error (); + } + default: + return replay_eval_error (); + } +} + +static bool replay_eval_predicates(ReplayExpr **exprs, size_t count, const ReplayEvalContext *ctx) { + size_t i; + for (i = 0; i < count; i++) { + ReplayEvalValue value = replay_eval_expr (exprs[i], ctx); + if (value.ok && value.is_bool && value.b) { + return true; + } + } + return false; +} + +static bool replay_eval_score(const ReplaySearchSpec *spec, const ReplayEvalContext *ctx, st64 *out_score) { + ReplayEvalValue value; + if (!spec || !spec->score_expr || !out_score) { + return false; + } + value = replay_eval_expr (spec->score_expr, ctx); + if (!value.ok || value.is_bool) { + return false; + } + *out_score = value.i; + return true; +} + +static RDebugStateSnapshot *replay_collect_snapshot(RCore *core, const ReplaySearchSpec *spec) { + if (!core || !spec || !spec->snapshot_request) { + return NULL; + } + return r_debug_state_snapshot_collect (core->dbg, spec->snapshot_request); +} + +static bool replay_search_spec_parse(RCore *core, const char *json, ReplaySearchSpec *spec) { + char *json_copy; + char *owned_json = NULL; + RJson *root; + const RJson *value; + size_t i; + + R_RETURN_VAL_IF_FAIL (core && json && spec, false); + memset (spec, 0, sizeof (*spec)); + spec->replay_fd = 0; + spec->max_depth = 1; + spec->beam_width = 16; + spec->score_order = REPLAY_SCORE_MAX; + spec->big_endian = core->rasm && core->rasm->config + ? R_ARCH_CONFIG_IS_BIG_ENDIAN (core->rasm->config) + : false; + + json_copy = strdup (json); + if (!json_copy) { + return false; + } + owned_json = json_copy; + root = r_json_parse (json_copy); + if (!root || root->type != R_JSON_OBJECT) { + R_LOG_ERROR ("r2sleigh replayj: json root parse failed"); + free (owned_json); + r_json_free (root); + return false; + } + + value = r_json_get (root, "seed_checkpoint"); + if (!value) { + value = r_json_get (root, "seed"); + } + if (!replay_parse_addr_expr (core, value, &spec->seed_checkpoint) || !spec->seed_checkpoint) { + R_LOG_ERROR ("r2sleigh replayj: missing/invalid seed_checkpoint"); + goto fail; + } + value = r_json_get (root, "replay_fd"); + if (value) { + if (value->type != R_JSON_INTEGER) { + R_LOG_ERROR ("r2sleigh replayj: invalid replay_fd"); + goto fail; + } + spec->replay_fd = value->num.s_value; + } + value = r_json_get (root, "alphabet"); + if (!value || value->type != R_JSON_STRING || !value->str_value || !*value->str_value) { + R_LOG_ERROR ("r2sleigh replayj: missing/invalid alphabet"); + goto fail; + } + spec->alphabet = strdup (value->str_value); + if (!spec->alphabet) { + goto fail; + } + value = r_json_get (root, "max_depth"); + if (value) { + if (value->type != R_JSON_INTEGER || !value->num.u_value) { + R_LOG_ERROR ("r2sleigh replayj: invalid max_depth"); + goto fail; + } + spec->max_depth = (size_t)value->num.u_value; + } + value = r_json_get (root, "beam_width"); + if (value) { + if (value->type != R_JSON_INTEGER || !value->num.u_value) { + R_LOG_ERROR ("r2sleigh replayj: invalid beam_width"); + goto fail; + } + spec->beam_width = (size_t)value->num.u_value; + } + if (!replay_parse_predicate_array (core, r_json_get (root, "frontier"), false, &spec->frontier_preds, &spec->frontier_count)) { + R_LOG_ERROR ("r2sleigh replayj: missing/invalid frontier"); + goto fail; + } + if (!replay_parse_predicate_array (core, r_json_get (root, "find"), false, &spec->find_preds, &spec->find_count)) { + R_LOG_ERROR ("r2sleigh replayj: missing/invalid find"); + goto fail; + } + value = r_json_get (root, "avoid"); + if (value && !replay_parse_predicate_array (core, value, true, &spec->avoid_preds, &spec->avoid_count)) { + R_LOG_ERROR ("r2sleigh replayj: invalid avoid"); + goto fail; + } + value = r_json_get (root, "score"); + if (!value || value->type != R_JSON_OBJECT) { + R_LOG_ERROR ("r2sleigh replayj: missing/invalid score"); + goto fail; + } + { + const RJson *order = r_json_get (value, "order"); + if (!order || order->type != R_JSON_STRING) { + R_LOG_ERROR ("r2sleigh replayj: missing score.order"); + goto fail; + } + if (!strcmp (order->str_value, "max")) { + spec->score_order = REPLAY_SCORE_MAX; + } else if (!strcmp (order->str_value, "min")) { + spec->score_order = REPLAY_SCORE_MIN; + } else { + R_LOG_ERROR ("r2sleigh replayj: invalid score.order"); + goto fail; + } + if (!replay_expr_parse (core, r_json_get (value, "expr"), &spec->score_expr)) { + R_LOG_ERROR ("r2sleigh replayj: invalid score.expr"); + goto fail; + } + } + + spec->snapshot_request = replay_state_request_new (); + if (!spec->snapshot_request) { + goto fail; + } + for (i = 0; i < spec->frontier_count; i++) { + if (!replay_expr_collect_state (spec->frontier_preds[i], spec->snapshot_request)) { + goto fail; + } + } + for (i = 0; i < spec->find_count; i++) { + if (!replay_expr_collect_state (spec->find_preds[i], spec->snapshot_request)) { + goto fail; + } + } + for (i = 0; i < spec->avoid_count; i++) { + if (!replay_expr_collect_state (spec->avoid_preds[i], spec->snapshot_request)) { + goto fail; + } + } + if (!replay_expr_collect_state (spec->score_expr, spec->snapshot_request)) { + goto fail; + } + + if (!replay_collect_stop_addrs (spec->frontier_preds, spec->frontier_count, &spec->frontier_stop_addrs, &spec->frontier_stop_count) + || !spec->frontier_stop_count) { + R_LOG_ERROR ("r2sleigh replayj: frontier must contain at least one exact PC == const predicate"); + goto fail; + } + for (i = 0; i < spec->frontier_stop_count; i++) { + if (!replay_addr_list_push_unique (&spec->stop_addrs, &spec->stop_count, spec->frontier_stop_addrs[i])) { + goto fail; + } + } + { + ut64 *tmp = NULL; + size_t tmp_count = 0; + if (!replay_collect_stop_addrs (spec->find_preds, spec->find_count, &tmp, &tmp_count)) { + goto fail; + } + for (i = 0; i < tmp_count; i++) { + if (!replay_addr_list_push_unique (&spec->stop_addrs, &spec->stop_count, tmp[i])) { + free (tmp); + goto fail; + } + } + free (tmp); + tmp = NULL; + tmp_count = 0; + if (spec->avoid_count && !replay_collect_stop_addrs (spec->avoid_preds, spec->avoid_count, &tmp, &tmp_count)) { + goto fail; + } + for (i = 0; i < tmp_count; i++) { + if (!replay_addr_list_push_unique (&spec->stop_addrs, &spec->stop_count, tmp[i])) { + free (tmp); + goto fail; + } + } + free (tmp); + } + + free (owned_json); + r_json_free (root); + return true; + +fail: + free (owned_json); + r_json_free (root); + replay_search_spec_fini (spec); + return false; +} + +static char *replay_input_append_char(const char *input, size_t input_len, char ch) { + char *next = malloc (input_len + 2); + if (!next) { + return NULL; + } + if (input_len && input) { + memcpy (next, input, input_len); + } + next[input_len] = ch; + next[input_len + 1] = '\0'; + return next; +} + +static void replay_temp_bps_fini(RCore *core, ReplayTempBpSet *set) { + size_t i; + if (!core || !set || !set->addrs) { + return; + } + for (i = 0; i < set->count; i++) { + r_bp_del (core->dbg->bp, set->addrs[i]); + } + free (set->addrs); + set->addrs = NULL; + set->count = 0; +} + +static bool replay_temp_bps_add(RCore *core, ReplayTempBpSet *set, ut64 addr) { + ut64 *next; + if (!core || !set || !addr) { + return false; + } + if (r_bp_get_in (core->dbg->bp, addr, R_BP_PROT_EXEC)) { + return true; + } + if (!r_bp_add_sw (core->dbg->bp, addr, core->dbg->bpsize, R_BP_PROT_EXEC)) { + return false; + } + next = realloc (set->addrs, (set->count + 1) * sizeof (ut64)); + if (!next) { + r_bp_del (core->dbg->bp, addr); + return false; + } + set->addrs = next; + set->addrs[set->count++] = addr; + return true; +} + +static ReplaySearchStopKind replay_classify_stop(const ReplaySearchSpec *spec, const ReplayEvalContext *ctx) { + if (replay_eval_predicates (spec->find_preds, spec->find_count, ctx)) { + return REPLAY_SEARCH_STOP_FIND; + } + if (replay_eval_predicates (spec->avoid_preds, spec->avoid_count, ctx)) { + return REPLAY_SEARCH_STOP_AVOID; + } + if (replay_eval_predicates (spec->frontier_preds, spec->frontier_count, ctx)) { + return REPLAY_SEARCH_STOP_FRONTIER; + } + return REPLAY_SEARCH_STOP_OTHER; +} + +static ReplaySearchStopKind replay_continue_to_any(RCore *core, const ReplaySearchSpec *spec, size_t depth, size_t input_len, + ut64 *hit_addr, RDebugStateSnapshot **out_snapshot) { + ReplayTempBpSet temps = {0}; + size_t i; + ut64 pc = 0; + RDebugStateSnapshot *snapshot = NULL; + ReplayEvalContext eval_ctx; + + R_RETURN_VAL_IF_FAIL (core && core->dbg && spec && hit_addr && out_snapshot, REPLAY_SEARCH_STOP_NONE); + *hit_addr = 0; + *out_snapshot = NULL; + + snapshot = replay_collect_snapshot (core, spec); + if (!snapshot) { + goto cleanup; + } + eval_ctx.snapshot = snapshot; + eval_ctx.depth = depth; + eval_ctx.input_len = input_len; + eval_ctx.big_endian = spec->big_endian; + pc = snapshot->pc; + if (replay_eval_predicates (spec->find_preds, spec->find_count, &eval_ctx)) { + *hit_addr = pc; + *out_snapshot = snapshot; + return REPLAY_SEARCH_STOP_FIND; + } + if (replay_eval_predicates (spec->avoid_preds, spec->avoid_count, &eval_ctx)) { + *hit_addr = pc; + *out_snapshot = snapshot; + return REPLAY_SEARCH_STOP_AVOID; + } + if (replay_addr_list_contains (spec->frontier_stop_addrs, spec->frontier_stop_count, pc)) { + r_debug_state_snapshot_free (snapshot); + snapshot = NULL; + if (r_debug_step (core->dbg, 1) != 1) { + goto cleanup; + } + snapshot = replay_collect_snapshot (core, spec); + if (!snapshot) { + goto cleanup; + } + pc = snapshot->pc; + } + + for (i = 0; i < spec->stop_count; i++) { + if (spec->stop_addrs[i] != pc && !replay_temp_bps_add (core, &temps, spec->stop_addrs[i])) { + goto cleanup; + } + } + r_debug_state_snapshot_free (snapshot); + snapshot = NULL; + if (r_debug_continue (core->dbg) <= 0) { + goto cleanup; + } + snapshot = replay_collect_snapshot (core, spec); + if (!snapshot) { + goto cleanup; + } + eval_ctx.snapshot = snapshot; + eval_ctx.depth = depth; + eval_ctx.input_len = input_len; + eval_ctx.big_endian = spec->big_endian; + *hit_addr = snapshot->pc; + *out_snapshot = snapshot; + snapshot = NULL; + replay_temp_bps_fini (core, &temps); + return replay_classify_stop (spec, &eval_ctx); + +cleanup: + replay_temp_bps_fini (core, &temps); + r_debug_state_snapshot_free (snapshot); + return REPLAY_SEARCH_STOP_NONE; +} + +static int replay_search_node_cmp(const ReplaySearchSpec *spec, const ReplaySearchNode *na, const ReplaySearchNode *nb) { + if (na->score != nb->score) { + if (spec->score_order == REPLAY_SCORE_MAX) { + return (na->score < nb->score) - (na->score > nb->score); + } + return (na->score > nb->score) - (na->score < nb->score); + } + if (na->input_len != nb->input_len) { + return (na->input_len > nb->input_len) - (na->input_len < nb->input_len); + } + if (!na->input || !nb->input) { + return (!na->input && nb->input) ? -1 : (na->input && !nb->input); + } + return strcmp (na->input, nb->input); +} + +static void replay_search_sort_nodes(const ReplaySearchSpec *spec, ReplaySearchNode **nodes, size_t count) { + size_t i; + size_t j; + for (i = 1; i < count; i++) { + ReplaySearchNode *key = nodes[i]; + j = i; + while (j > 0 && replay_search_node_cmp (spec, nodes[j - 1], key) > 0) { + nodes[j] = nodes[j - 1]; + j--; + } + nodes[j] = key; + } +} + +static char *replay_search_run_json(RCore *core, const ReplaySearchSpec *spec) { + RList *active = NULL; + RList *next = NULL; + RList *found = NULL; + ReplaySearchNode *seed = NULL; + size_t explored = 0; + size_t depth; + char *out = NULL; + + R_RETURN_VAL_IF_FAIL (core && core->dbg && core->dbg->session && spec && spec->score_expr, NULL); + + active = r_list_newf ((RListFree)replay_search_node_free); + next = r_list_newf ((RListFree)replay_search_node_free); + found = r_list_newf ((RListFree)replay_search_match_free); + if (!active || !next || !found) { + goto cleanup; + } + seed = R_NEW0 (ReplaySearchNode); + if (!seed) { + goto cleanup; + } + seed->checkpoint_id = spec->seed_checkpoint; + seed->input = strdup (""); + if (!seed->input) { + replay_search_node_free (seed); + goto cleanup; + } + r_list_append (active, seed); + + for (depth = 0; depth < spec->max_depth && !r_list_empty (active) && r_list_empty (found); depth++) { + RListIter *iter; + ReplaySearchNode *node; + r_list_free (next); + next = r_list_newf ((RListFree)replay_search_node_free); + if (!next) { + goto cleanup; + } + r_list_foreach (active, iter, node) { + const char *alphabet = spec->alphabet; + while (alphabet && *alphabet) { + ut64 child_checkpoint = 0; + ut64 frontier_checkpoint = 0; + ut64 hit_addr = 0; + ReplaySearchStopKind stop; + RDebugStateSnapshot *snapshot = NULL; + ReplayEvalContext eval_ctx; + char *next_input = NULL; + char *snapshot_json = NULL; + st64 score = 0; + + if (!r_debug_session_restore_checkpoint (core->dbg, node->checkpoint_id)) { + alphabet++; + continue; + } + child_checkpoint = r_debug_checkpoint_create (core->dbg, node->checkpoint_id, NULL); + if (!child_checkpoint) { + alphabet++; + continue; + } + if (!r_debug_session_checkpoint_replay_append (core->dbg->session, child_checkpoint, + spec->replay_fd, (const ut8 *)alphabet, 1, NULL)) { + alphabet++; + continue; + } + if (!r_debug_session_restore_checkpoint (core->dbg, child_checkpoint)) { + alphabet++; + continue; + } + if (!r_debug_session_checkpoint_replay_apply (core->dbg, child_checkpoint, spec->replay_fd)) { + alphabet++; + continue; + } + + explored++; + stop = replay_continue_to_any (core, spec, depth + 1, node->input_len + 1, &hit_addr, &snapshot); + next_input = replay_input_append_char (node->input, node->input_len, *alphabet); + if (!next_input || !snapshot) { + free (next_input); + r_debug_state_snapshot_free (snapshot); + alphabet++; + continue; + } + eval_ctx.snapshot = snapshot; + eval_ctx.depth = depth + 1; + eval_ctx.input_len = node->input_len + 1; + eval_ctx.big_endian = spec->big_endian; + if (!replay_eval_score (spec, &eval_ctx, &score)) { + free (next_input); + r_debug_state_snapshot_free (snapshot); + alphabet++; + continue; + } + snapshot_json = r_debug_state_snapshot_to_json (snapshot); + r_debug_state_snapshot_free (snapshot); + snapshot = NULL; + + if (stop == REPLAY_SEARCH_STOP_FIND) { + ReplaySearchMatch *match = R_NEW0 (ReplaySearchMatch); + if (match) { + match->checkpoint_id = child_checkpoint; + match->input = next_input; + match->input_len = node->input_len + 1; + match->hit_addr = hit_addr; + match->score = score; + match->snapshot_json = snapshot_json; + r_list_append (found, match); + next_input = NULL; + snapshot_json = NULL; + } + } else if (stop == REPLAY_SEARCH_STOP_FRONTIER) { + ReplaySearchNode *frontier = R_NEW0 (ReplaySearchNode); + frontier_checkpoint = r_debug_checkpoint_create (core->dbg, child_checkpoint, NULL); + if (frontier && frontier_checkpoint) { + frontier->checkpoint_id = frontier_checkpoint; + frontier->input = next_input; + frontier->input_len = node->input_len + 1; + frontier->score = score; + frontier->snapshot_json = snapshot_json; + r_list_append (next, frontier); + next_input = NULL; + snapshot_json = NULL; + } else { + replay_search_node_free (frontier); + } + } + free (snapshot_json); + free (next_input); + if (!r_list_empty (found)) { + break; + } + alphabet++; + } + if (!r_list_empty (found)) { + break; + } + } + + { + int next_len = r_list_length (next); + if (spec->beam_width && next_len > 0 && (size_t)next_len > spec->beam_width) { + size_t count = (size_t)next_len; + size_t i; + ReplaySearchNode **nodes = calloc (count, sizeof (ReplaySearchNode *)); + if (!nodes) { + goto cleanup; + } + i = 0; + { + RListIter *iter; + ReplaySearchNode *node; + r_list_foreach (next, iter, node) { + nodes[i++] = node; + } + } + replay_search_sort_nodes (spec, nodes, count); + for (i = spec->beam_width; i < count; i++) { + r_list_delete_data (next, nodes[i]); + } + free (nodes); + } + } + + { + RList *tmp = active; + active = next; + next = tmp; + } + } + + { + PJ *pj = pj_new (); + RListIter *iter; + ReplaySearchMatch *match; + ReplaySearchNode *node; + if (!pj) { + goto cleanup; + } + pj_o (pj); + pj_kn (pj, "seed_checkpoint", spec->seed_checkpoint); + pj_kn (pj, "replay_fd", spec->replay_fd); + pj_ks (pj, "alphabet", spec->alphabet); + pj_kn (pj, "max_depth", spec->max_depth); + pj_kn (pj, "beam_width", spec->beam_width); + pj_ks (pj, "score_order", spec->score_order == REPLAY_SCORE_MAX? "max": "min"); + pj_kn (pj, "explored_branches", explored); + pj_kb (pj, "found", !r_list_empty (found)); + pj_ka (pj, "matches"); + r_list_foreach (found, iter, match) { + pj_o (pj); + pj_kn (pj, "checkpoint", match->checkpoint_id); + pj_ks (pj, "input", match->input ? match->input : ""); + pj_kn (pj, "hit", match->hit_addr); + pj_ki (pj, "score", match->score); + if (match->snapshot_json) { + pj_k (pj, "snapshot"); + pj_raw (pj, match->snapshot_json); + } else { + pj_knull (pj, "snapshot"); + } + pj_end (pj); + } + pj_end (pj); + pj_ka (pj, "active"); + r_list_foreach (active, iter, node) { + pj_o (pj); + pj_kn (pj, "checkpoint", node->checkpoint_id); + pj_ks (pj, "input", node->input ? node->input : ""); + pj_ki (pj, "score", node->score); + if (node->snapshot_json) { + pj_k (pj, "snapshot"); + pj_raw (pj, node->snapshot_json); + } else { + pj_knull (pj, "snapshot"); + } + pj_end (pj); + } + pj_end (pj); + pj_end (pj); + out = strdup (pj_string (pj)); + pj_free (pj); + } + +cleanup: + r_list_free (active); + r_list_free (next); + r_list_free (found); + return out; +} + static RAnalFunction *resolve_function_target_by_name(RAnal *anal, const char *target_name) { if (!anal || !target_name || !*target_name) { return NULL; @@ -4130,6 +5514,7 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { r_cons_println (cons, "| a:sym.explore - Explore symbolic paths reaching target"); r_cons_println (cons, "| a:sym.solve - Solve concrete input for target reachability"); r_cons_println (cons, "| a:sym.runj - Run typed symbolic exploration spec"); + r_cons_println (cons, "| a:sym.replayj - Search checkpointed replay branches"); r_cons_println (cons, "| a:sym.state - Show last symbolic explore/solve cached result"); } return strdup(""); @@ -4208,6 +5593,41 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { return strdup(""); } + if (is_sym_ns && !strncmp (cmd, "sym.replayj", 11)) { + const char *arg = skip_cmd_spaces (cmd + 11); + ReplaySearchSpec spec; + char *result = NULL; + + if (!arg || !*arg) { + if (cons) { + r_cons_println (cons, "Usage: a:sym.replayj "); + } + return strdup (""); + } + if (!core->dbg || !core->dbg->session) { + R_LOG_ERROR ("r2sleigh: debug session with checkpoints is required"); + return strdup (""); + } + R_LOG_DEBUG ("r2sleigh replayj arg: %s", arg); + if (!replay_search_spec_parse (core, arg, &spec)) { + R_LOG_ERROR ("r2sleigh: invalid replay search spec"); + return strdup (""); + } + result = replay_search_run_json (core, &spec); + replay_search_spec_fini (&spec); + if (!result) { + result = strdup ("{\"error\":\"replay search failed\"}"); + } + if (cons && result) { + r_cons_printf (cons, "%s\n", result); + } + if (result && !sym_result_has_error (result)) { + sym_state_cache_update ("replayj", 0, 0, 0, result); + } + free (result); + return strdup (""); + } + if (is_sym_ns && (!strncmp (cmd, "sym.explore", 11) || !strncmp (cmd, "sym.solve", 9))) { bool is_explore = r_str_startswith (cmd, "sym.explore"); size_t prefix_len = is_explore ? 11 : 9; diff --git a/r2plugin/src/analysis/sym.rs b/r2plugin/src/analysis/sym.rs index 699a990..bde5f84 100644 --- a/r2plugin/src/analysis/sym.rs +++ b/r2plugin/src/analysis/sym.rs @@ -12,6 +12,13 @@ use std::sync::{Mutex, OnceLock}; use z3::{Config, Context}; static MERGE_STATES: AtomicBool = AtomicBool::new(false); +const SYM_PATHS_LIMIT: usize = 32; +const SYM_PATHS_CALL_FREE_MAX_STATES: usize = 16; +const SYM_PATHS_CALL_FREE_MAX_DEPTH: usize = 64; +const SYM_PATHS_CALL_HEAVY_MAX_STATES: usize = 8; +const SYM_PATHS_CALL_HEAVY_MAX_DEPTH: usize = 32; +const SYM_PATHS_TIMEOUT_MS: u64 = 500; +const SYM_PATHS_SOLUTION_LIMIT: usize = 4; fn merge_states_enabled() -> bool { MERGE_STATES.load(Ordering::Relaxed) @@ -100,6 +107,31 @@ fn sym_default_config() -> r2sym::ExploreConfig { } } +fn sym_paths_config(prepared: &r2ssa::SsaArtifact) -> r2sym::ExploreConfig { + let mut config = sym_default_config(); + if prepared.call_sites().by_id.is_empty() { + config.max_states = SYM_PATHS_CALL_FREE_MAX_STATES; + config.max_depth = SYM_PATHS_CALL_FREE_MAX_DEPTH; + } else { + config.max_states = SYM_PATHS_CALL_HEAVY_MAX_STATES; + config.max_depth = SYM_PATHS_CALL_HEAVY_MAX_DEPTH; + } + config.timeout = Some(std::time::Duration::from_millis(SYM_PATHS_TIMEOUT_MS)); + config.max_completed_paths = Some(SYM_PATHS_LIMIT); + config +} + +fn sym_paths_solution_limit(result_count: usize, prepared: &r2ssa::SsaArtifact) -> usize { + if !prepared.call_sites().by_id.is_empty() { + return 0; + } + if result_count <= SYM_PATHS_SOLUTION_LIMIT { + result_count + } else { + 0 + } +} + fn sym_error_json(message: &str) -> *mut c_char { let payload = format!(r#"{{"error":"{}"}}"#, message); CString::new(payload).map_or(ptr::null_mut(), |c| c.into_raw()) @@ -162,6 +194,11 @@ struct SymExecSummary { paths_pruned: usize, max_depth: usize, states_explored: usize, + sat_queries: usize, + sat_cache_hits: usize, + sat_cache_misses: usize, + solve_calls: usize, + solve_unsat_shortcuts: usize, time_ms: u64, } @@ -170,7 +207,7 @@ struct SymStateInfo { pc: u64, depth: usize, num_constraints: usize, - registers: std::collections::HashMap, + registers: BTreeMap, } #[derive(Serialize)] @@ -187,15 +224,15 @@ struct PathInfo { #[derive(Serialize)] struct PathSolution { - inputs: std::collections::HashMap, - registers: std::collections::HashMap, + inputs: BTreeMap, + registers: BTreeMap, } #[derive(Serialize)] struct RunPathSolution { - inputs: std::collections::HashMap, - input_buffers: std::collections::HashMap, - registers: std::collections::HashMap, + inputs: BTreeMap, + input_buffers: BTreeMap, + registers: BTreeMap, } #[derive(Serialize)] @@ -255,6 +292,26 @@ struct SymRunResult { diagnostics: Vec, } +fn build_sym_exec_summary( + stats: &r2sym::path::ExploreStats, + solver_stats: &r2sym::SolverStats, + paths_feasible: usize, +) -> SymExecSummary { + SymExecSummary { + paths_explored: stats.paths_completed, + paths_feasible, + paths_pruned: stats.paths_pruned, + max_depth: stats.max_depth_reached, + states_explored: stats.states_explored, + sat_queries: solver_stats.sat_queries, + sat_cache_hits: solver_stats.sat_cache_hits, + sat_cache_misses: solver_stats.sat_cache_misses, + solve_calls: solver_stats.solve_calls, + solve_unsat_shortcuts: solver_stats.solve_unsat_shortcuts, + time_ms: stats.total_time.as_millis() as u64, + } +} + fn path_solution_from_result<'ctx>( explorer: &r2sym::PathExplorer<'ctx>, result: &r2sym::PathResult<'ctx>, @@ -322,10 +379,11 @@ fn run_path_solution_from_result<'ctx>( }) } -fn path_info_from_result<'ctx>( +fn path_info_from_result_with_solution<'ctx>( path_id: usize, result: &r2sym::PathResult<'ctx>, explorer: &r2sym::PathExplorer<'ctx>, + include_solution: bool, ) -> PathInfo { PathInfo { path_id, @@ -334,10 +392,20 @@ fn path_info_from_result<'ctx>( exit_status: format!("{:?}", result.exit_status), final_pc: format!("0x{:x}", result.final_pc()), num_constraints: result.num_constraints(), - solution: path_solution_from_result(explorer, result), + solution: include_solution + .then(|| path_solution_from_result(explorer, result)) + .flatten(), } } +fn path_info_from_result<'ctx>( + path_id: usize, + result: &r2sym::PathResult<'ctx>, + explorer: &r2sym::PathExplorer<'ctx>, +) -> PathInfo { + path_info_from_result_with_solution(path_id, result, explorer, true) +} + fn run_path_info_from_result<'ctx>( path_id: usize, result: &r2sym::PathResult<'ctx>, @@ -408,9 +476,12 @@ pub extern "C" fn r2sym_function( let explore_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let mut initial_state = r2sym::SymState::new(&z3_ctx, entry_addr); r2sym::seed_default_state_for_arch(&mut initial_state, &prepared, ctx_view.arch); - let mut explorer = r2sym::PathExplorer::with_config(&z3_ctx, sym_default_config()); + let mut explorer = r2sym::PathExplorer::with_config(&z3_ctx, sym_paths_config(&prepared)); if let Some(arch) = ctx_view.arch - && let Some(registry) = r2sym::SummaryRegistry::with_core_for_arch(arch) + && let Some(registry) = r2sym::SummaryRegistry::with_profile_for_arch( + arch, + r2sym::SummaryProfile::PathListing, + ) { let interproc = build_seed_interproc_summary_set(&prepared, &symbol_map); let _ = @@ -424,10 +495,11 @@ pub extern "C" fn r2sym_function( } let results = explorer.explore(&prepared, initial_state); let stats = explorer.stats().clone(); - (results, stats) + let solver_stats = explorer.solver().stats(); + (results, stats, solver_stats) })); - let (results, stats) = match explore_result { + let (results, stats, solver_stats) = match explore_result { Ok(r) => r, Err(_) => { let error_msg = r#"{"error": "symbolic execution failed (z3 context error)"}"#; @@ -436,14 +508,7 @@ pub extern "C" fn r2sym_function( }; let feasible_count = results.iter().filter(|r| r.feasible).count(); - let summary = SymExecSummary { - paths_explored: stats.paths_completed, - paths_feasible: feasible_count, - paths_pruned: stats.paths_pruned, - max_depth: stats.max_depth_reached, - states_explored: stats.states_explored, - time_ms: stats.total_time.as_millis() as u64, - }; + let summary = build_sym_exec_summary(&stats, &solver_stats, feasible_count); match serde_json::to_string_pretty(&summary) { Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), Err(_) => ptr::null_mut(), @@ -460,7 +525,7 @@ pub extern "C" fn r2sym_state_json(state: *const R2SymContext) -> *mut c_char { pc: state_ref.entry_pc, depth: 0, num_constraints: 0, - registers: std::collections::HashMap::new(), + registers: BTreeMap::new(), }; match serde_json::to_string_pretty(&info) { Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), @@ -492,9 +557,12 @@ pub extern "C" fn r2sym_paths( let explore_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let mut initial_state = r2sym::SymState::new(&z3_ctx, entry_addr); r2sym::seed_default_state_for_arch(&mut initial_state, &prepared, ctx_view.arch); - let mut explorer = r2sym::PathExplorer::with_config(&z3_ctx, sym_default_config()); + let mut explorer = r2sym::PathExplorer::with_config(&z3_ctx, sym_paths_config(&prepared)); if let Some(arch) = ctx_view.arch - && let Some(registry) = r2sym::SummaryRegistry::with_core_for_arch(arch) + && let Some(registry) = r2sym::SummaryRegistry::with_profile_for_arch( + arch, + r2sym::SummaryProfile::PathListing, + ) { let interproc = build_seed_interproc_summary_set(&prepared, &symbol_map); let _ = @@ -518,10 +586,11 @@ pub extern "C" fn r2sym_paths( } }; + let solution_limit = sym_paths_solution_limit(results.len(), &prepared); let paths: Vec = results .iter() .enumerate() - .map(|(i, r)| path_info_from_result(i, r, &explorer)) + .map(|(i, r)| path_info_from_result_with_solution(i, r, &explorer, i < solution_limit)) .collect(); match serde_json::to_string_pretty(&paths) { Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), @@ -570,15 +639,16 @@ pub extern "C" fn r2sym_explore_to( } let matched = explorer.find_paths_to(&prepared, initial_state, target_addr); let stats = explorer.stats().clone(); + let solver_stats = explorer.solver().stats(); let paths: Vec = matched .iter() .enumerate() .map(|(i, r)| path_info_from_result(i, r, &explorer)) .collect(); - (paths, stats) + (paths, stats, solver_stats) })); - let (paths, stats) = match explore_result { + let (paths, stats, solver_stats) = match explore_result { Ok(value) => value, Err(_) => return sym_error_json("symbolic execution failed (z3 context error)"), }; @@ -586,14 +656,7 @@ pub extern "C" fn r2sym_explore_to( entry: format!("0x{:x}", entry_addr), target: format!("0x{:x}", target_addr), matched_paths: paths.len(), - stats: SymExecSummary { - paths_explored: stats.paths_completed, - paths_feasible: paths.len(), - paths_pruned: stats.paths_pruned, - max_depth: stats.max_depth_reached, - states_explored: stats.states_explored, - time_ms: stats.total_time.as_millis() as u64, - }, + stats: build_sym_exec_summary(&stats, &solver_stats, paths.len()), paths, }; @@ -644,15 +707,16 @@ pub extern "C" fn r2sym_solve_to( } let matched = explorer.find_paths_to(&prepared, initial_state, target_addr); let stats = explorer.stats().clone(); + let solver_stats = explorer.solver().stats(); let selected = matched .iter() .enumerate() .min_by_key(|(idx, path)| (path.num_constraints(), path.depth, *idx)) .map(|(idx, path)| path_info_from_result(idx, path, &explorer)); - (matched.len(), selected, stats) + (matched.len(), selected, stats, solver_stats) })); - let (matched_paths, selected_path, stats) = match solve_result { + let (matched_paths, selected_path, stats, solver_stats) = match solve_result { Ok(value) => value, Err(_) => return sym_error_json("symbolic execution failed (z3 context error)"), }; @@ -661,14 +725,7 @@ pub extern "C" fn r2sym_solve_to( target: format!("0x{:x}", target_addr), matched_paths, found: selected_path.is_some(), - stats: SymExecSummary { - paths_explored: stats.paths_completed, - paths_feasible: matched_paths, - paths_pruned: stats.paths_pruned, - max_depth: stats.max_depth_reached, - states_explored: stats.states_explored, - time_ms: stats.total_time.as_millis() as u64, - }, + stats: build_sym_exec_summary(&stats, &solver_stats, matched_paths), selected_path, }; @@ -729,16 +786,17 @@ pub extern "C" fn r2sym_run_spec_json( install_symbolic_hooks(&mut explorer, &prepared, ctx_view.arch, &symbol_map); let result = explorer.run_spec(&prepared, initial_state, &spec)?; let stats = explorer.stats().clone(); + let solver_stats = explorer.solver().stats(); let found_paths = result .found_paths .iter() .enumerate() .map(|(idx, path)| run_path_info_from_result(idx, path, &explorer)) .collect::>(); - Ok::<_, String>((result, stats, found_paths)) + Ok::<_, String>((result, stats, solver_stats, found_paths)) })); - let (result, stats, found_paths) = match run_result { + let (result, stats, solver_stats, found_paths) = match run_result { Ok(Ok(value)) => value, Ok(Err(err)) => return sym_error_json(&err), Err(_) => return sym_error_json("symbolic execution failed (z3 context error)"), @@ -747,14 +805,7 @@ pub extern "C" fn r2sym_run_spec_json( let output = SymRunResult { entry: format!("0x{:x}", entry_addr), spec, - stats: SymExecSummary { - paths_explored: stats.paths_completed, - paths_feasible: found_paths.len(), - paths_pruned: stats.paths_pruned, - max_depth: stats.max_depth_reached, - states_explored: stats.states_explored, - time_ms: stats.total_time.as_millis() as u64, - }, + stats: build_sym_exec_summary(&stats, &solver_stats, found_paths.len()), stash_counts: SymRunStashCounts { found: found_paths.len(), avoided: result.avoided_states, diff --git a/tests/r2r/db/extras/r2sleigh_integration_extended b/tests/r2r/db/extras/r2sleigh_integration_extended index 38f294f..0e69fe9 100644 --- a/tests/r2r/db/extras/r2sleigh_integration_extended +++ b/tests/r2r/db/extras/r2sleigh_integration_extended @@ -7,7 +7,7 @@ EOF_EXPECT CMDS=</dev/null -a:sla.sym.paths | jq -c 'type=="array"' +a:sla.sym.paths | jq -c 'type=="array" and length>0 and length<=32' EOF_CMDS RUN @@ -143,6 +143,19 @@ a:sla.sym | jq -c 'has("paths_explored") and has("paths_feasible") and has("stat EOF_CMDS RUN +NAME=symbolic_check_secret_has_solver_cache_stats +FILE=bins/vuln_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=</dev/null +a:sla.sym | jq -c 'has("sat_queries") and has("sat_cache_hits") and has("sat_cache_misses") and has("solve_unsat_shortcuts")' +EOF_CMDS +RUN + NAME=symbolic_unlock_has_paths_explored FILE=bins/vuln_test ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true @@ -206,7 +219,7 @@ aaa s `afl~check_secret$[0]` >/dev/null s `afl~check_secret$[0]` >/dev/null s `afl~check_secret$[0]` >/dev/null -a:sla.sym.paths | jq -c 'length>0 and (.[0]|has("path_id") and has("feasible") and has("solution") and has("exit_status"))' +a:sla.sym.paths | jq -c 'length>0 and length<=32 and (.[0]|has("path_id") and has("feasible") and has("solution") and has("exit_status"))' EOF_CMDS RUN @@ -221,7 +234,7 @@ aaa s `afl~check_secret$[0]` >/dev/null s `afl~check_secret$[0]` >/dev/null s `afl~check_secret$[0]` >/dev/null -a:sla.sym.paths | jq -c 'tostring|contains("0xdead")' +a:sla.sym.paths | jq -c 'length>0 and length<=32 and (tostring|contains("0xdead"))' EOF_CMDS RUN @@ -236,7 +249,7 @@ aaa s `afl~solve_equation$[0]` >/dev/null s `afl~solve_equation$[0]` >/dev/null s `afl~solve_equation$[0]` >/dev/null -a:sla.sym.paths | jq -c 'tostring|contains("0xa")' +a:sla.sym.paths | jq -c 'length>0 and length<=32 and (tostring|contains("0xa"))' EOF_CMDS RUN @@ -251,7 +264,7 @@ aaa s `afl~bitwise_check$[0]` >/dev/null s `afl~bitwise_check$[0]` >/dev/null s `afl~bitwise_check$[0]` >/dev/null -a:sla.sym.paths | jq -c 'tostring|contains("0x5a")' +a:sla.sym.paths | jq -c 'length>0 and length<=32 and (tostring|contains("0x5a"))' EOF_CMDS RUN @@ -309,7 +322,7 @@ aaa s `afl~check_secret$[0]` >/dev/null s `afl~check_secret$[0]` >/dev/null s `afl~check_secret$[0]` >/dev/null -a:sla.sym.paths | jq -c 'type=="array"' +a:sla.sym.paths | jq -c 'type=="array" and length>0 and length<=32' EOF_CMDS RUN @@ -379,7 +392,7 @@ aaa s `afl~process_string$[0]` >/dev/null s `afl~process_string$[0]` >/dev/null s `afl~process_string$[0]` >/dev/null -a:sla.sym.paths | jq -c 'length>0 and (map(has("error")|not)|all)' +a:sla.sym.paths | jq -c 'length>0 and length<=32 and (map(has("error")|not)|all)' EOF_CMDS RUN @@ -394,7 +407,7 @@ aaa s `afl~authenticate$[0]` >/dev/null s `afl~authenticate$[0]` >/dev/null s `afl~authenticate$[0]` >/dev/null -a:sla.sym.paths | jq -c 'length>0 and (map(has("error")|not)|all)' +a:sla.sym.paths | jq -c 'length>0 and length<=32 and (map(has("error")|not)|all)' EOF_CMDS RUN @@ -409,7 +422,7 @@ aaa s `afl~alloc_and_copy$[0]` >/dev/null s `afl~alloc_and_copy$[0]` >/dev/null s `afl~alloc_and_copy$[0]` >/dev/null -a:sla.sym.paths | jq -c 'length>0 and (map(has("error")|not)|all)' +a:sla.sym.paths | jq -c 'length>0 and length<=32 and (map(has("error")|not)|all)' EOF_CMDS RUN @@ -424,7 +437,7 @@ aaa s `afl~vuln_printf$[0]` >/dev/null s `afl~vuln_printf$[0]` >/dev/null s `afl~vuln_printf$[0]` >/dev/null -a:sla.sym.paths | jq -c 'length>0 and (map(has("error")|not)|all)' +a:sla.sym.paths | jq -c 'length>0 and length<=32 and (map(has("error")|not)|all)' EOF_CMDS RUN @@ -439,7 +452,7 @@ aaa s `afl~vuln_memcpy$[0]` >/dev/null s `afl~vuln_memcpy$[0]` >/dev/null s `afl~vuln_memcpy$[0]` >/dev/null -a:sla.sym.paths | jq -c 'length>0 and (map(has("error")|not)|all)' +a:sla.sym.paths | jq -c 'length>0 and length<=32 and (map(has("error")|not)|all)' EOF_CMDS RUN From 77c729ef2ac9bbe80570805658b8662eab3029fc Mon Sep 17 00:00:00 2001 From: Priyanshu Kumar Date: Fri, 27 Mar 2026 18:56:05 +0000 Subject: [PATCH 03/10] Symex Rewrite --- crates/r2dec/src/fold/context.rs | 5 +- crates/r2dec/src/fold/flags.rs | 62 +- crates/r2dec/src/fold/tests/flags.rs | 135 + crates/r2dec/src/fold/tests/pipeline.rs | 2 + crates/r2dec/src/lib.rs | 269 +- crates/r2dec/src/structure.rs | 210 + crates/r2ssa/src/interproc.rs | 289 +- crates/r2sym/src/backward.rs | 1833 +++++++ crates/r2sym/src/lib.rs | 39 +- crates/r2sym/src/memory.rs | 1035 +++- crates/r2sym/src/path.rs | 772 ++- crates/r2sym/src/query.rs | 784 +++ crates/r2sym/src/replay.rs | 503 ++ crates/r2sym/src/runtime.rs | 144 +- crates/r2sym/src/semantics/artifact.rs | 60 + crates/r2sym/src/semantics/cache.rs | 126 + crates/r2sym/src/semantics/classify.rs | 32 + crates/r2sym/src/semantics/compiler.rs | 774 +++ crates/r2sym/src/semantics/facts.rs | 560 ++ crates/r2sym/src/semantics/mod.rs | 23 + crates/r2sym/src/semantics/vm.rs | 799 +++ crates/r2sym/src/sim.rs | 1057 +++- crates/r2sym/src/solver.rs | 28 + crates/r2sym/src/state.rs | 225 +- crates/r2sym/tests/symex_integration.rs | 4606 +++++++++++++++-- crates/r2types/src/facts.rs | 278 + crates/r2types/src/lib.rs | 9 +- r2plugin/r_anal_sleigh.c | 1014 +++- r2plugin/src/analysis/sym.rs | 1346 ++++- r2plugin/src/decompiler.rs | 71 +- r2plugin/src/helpers.rs | 97 + r2plugin/src/lib.rs | 667 ++- r2plugin/src/types.rs | 619 ++- tests/e2e/stress_test.c | 53 + tests/e2e/vuln_test.c | 139 + .../db/extras/r2sleigh_decompiler_snapshots | 4 +- .../db/extras/r2sleigh_integration_extended | 358 +- 37 files changed, 17858 insertions(+), 1169 deletions(-) create mode 100644 crates/r2sym/src/backward.rs create mode 100644 crates/r2sym/src/query.rs create mode 100644 crates/r2sym/src/replay.rs create mode 100644 crates/r2sym/src/semantics/artifact.rs create mode 100644 crates/r2sym/src/semantics/cache.rs create mode 100644 crates/r2sym/src/semantics/classify.rs create mode 100644 crates/r2sym/src/semantics/compiler.rs create mode 100644 crates/r2sym/src/semantics/facts.rs create mode 100644 crates/r2sym/src/semantics/mod.rs create mode 100644 crates/r2sym/src/semantics/vm.rs diff --git a/crates/r2dec/src/fold/context.rs b/crates/r2dec/src/fold/context.rs index a3d054a..44935d1 100644 --- a/crates/r2dec/src/fold/context.rs +++ b/crates/r2dec/src/fold/context.rs @@ -12,7 +12,7 @@ use r2ssa::{ use r2types::ExternalStackVarSpec; use r2types::{ CalleeFact, ExternalStackSlotSpec, ExternalTypeDb, FunctionType, SignatureRegistry, - StackSlotKey, TypeOracle, VisibleBinding, + StackSlotKey, SymbolicSemanticFacts, TypeOracle, VisibleBinding, }; pub(crate) type SSABlock = FunctionSSABlock; @@ -65,6 +65,7 @@ pub(crate) struct FoldInputs<'a> { pub(crate) external_stack_vars: &'a HashMap, pub(crate) visible_bindings: &'a [VisibleBinding], pub(crate) external_type_db: &'a ExternalTypeDb, + pub(crate) symbolic_facts: &'a SymbolicSemanticFacts, pub(crate) param_register_aliases: &'a HashMap, pub(crate) type_hints: &'a HashMap, pub(crate) type_oracle: Option<&'a dyn TypeOracle>, @@ -207,6 +208,7 @@ impl<'a> FoldingContext<'a> { static EMPTY_I64_STACK: OnceLock> = OnceLock::new(); static EMPTY_VISIBLE_BINDINGS: OnceLock> = OnceLock::new(); static EMPTY_TYPE_DB: OnceLock = OnceLock::new(); + static EMPTY_SYMBOLIC_FACTS: OnceLock = OnceLock::new(); static EMPTY_STRING_STRING: OnceLock> = OnceLock::new(); static EMPTY_STRING_FNTY: OnceLock> = OnceLock::new(); static EMPTY_CALLEE_FACTS: OnceLock> = OnceLock::new(); @@ -232,6 +234,7 @@ impl<'a> FoldingContext<'a> { external_stack_vars: EMPTY_I64_STACK.get_or_init(HashMap::new), visible_bindings: EMPTY_VISIBLE_BINDINGS.get_or_init(Vec::new), external_type_db: EMPTY_TYPE_DB.get_or_init(ExternalTypeDb::default), + symbolic_facts: EMPTY_SYMBOLIC_FACTS.get_or_init(SymbolicSemanticFacts::default), param_register_aliases: EMPTY_STRING_STRING.get_or_init(HashMap::new), type_hints: EMPTY_STRING_CTYPE.get_or_init(HashMap::new), type_oracle: None, diff --git a/crates/r2dec/src/fold/flags.rs b/crates/r2dec/src/fold/flags.rs index eb385b4..9c0be00 100644 --- a/crates/r2dec/src/fold/flags.rs +++ b/crates/r2dec/src/fold/flags.rs @@ -8,6 +8,7 @@ use r2ssa::{ use crate::analysis; use crate::analysis::{FlagCompareKind, FlagCompareProvenance, utils}; use crate::ast::{BinaryOp, CExpr, CType, UnaryOp}; +use r2types::SymbolicReachabilityStatus; use super::context::FoldingContext; use super::op_lower::parse_const_value; @@ -87,6 +88,41 @@ impl<'a> FoldingContext<'a> { .and_then(|view| view.branch_expr_for_block(block_addr).cloned()) } + fn symbolic_branch_condition_expr(&self, block_addr: u64) -> Option { + let fact = self + .inputs + .symbolic_facts + .branch_fact_for_block(block_addr)?; + match (fact.true_status, fact.false_status) { + (SymbolicReachabilityStatus::Reachable, SymbolicReachabilityStatus::Unreachable) => { + Some(CExpr::IntLit(1)) + } + (SymbolicReachabilityStatus::Unreachable, SymbolicReachabilityStatus::Reachable) => { + Some(CExpr::IntLit(0)) + } + _ => None, + } + } + + fn symbolic_exact_compiled_condition( + &self, + block_addr: u64, + ) -> Option<&r2types::SymbolicCompiledCondition> { + self.inputs + .symbolic_facts + .branch_fact_for_block(block_addr) + .and_then(|fact| fact.exact_compiled_condition()) + } + + fn symbolic_exact_compiled_condition_expr(&self, block_addr: u64) -> Option { + let compiled = self.symbolic_exact_compiled_condition(block_addr)?; + match compiled.simplified.trim().to_ascii_lowercase().as_str() { + "1" | "true" => Some(CExpr::IntLit(1)), + "0" | "false" => Some(CExpr::IntLit(0)), + _ => None, + } + } + fn prepared_predicate_view(&self) -> Option> { self.prepared_semantic_view().map(Cow::Borrowed) } @@ -292,6 +328,14 @@ impl<'a> FoldingContext<'a> { } pub fn extract_condition_from_block(&self, block: &FunctionSSABlock) -> Option { + if let Some(cond) = self.symbolic_exact_compiled_condition_expr(block.addr) { + return Some(cond); + } + + if let Some(cond) = self.symbolic_branch_condition_expr(block.addr) { + return Some(cond); + } + let (branch_idx, cond) = block .ops @@ -306,14 +350,16 @@ impl<'a> FoldingContext<'a> { let prepared_block_candidate = self.prepared_predicate_candidate_for_branch_block(block.addr, cond); let prepared_var_candidate = self.prepared_predicate_candidate_for_var(cond); - let allow_legacy_flag_provenance = ![ - prepared_branch_candidate.as_ref(), - prepared_block_candidate.as_ref(), - prepared_var_candidate.as_ref(), - ] - .into_iter() - .flatten() - .any(|expr| !self.prepared_candidate_needs_legacy_compare_help(expr)); + let exact_compiled = self.symbolic_exact_compiled_condition(block.addr); + let allow_legacy_flag_provenance = exact_compiled.is_none() + && ![ + prepared_branch_candidate.as_ref(), + prepared_block_candidate.as_ref(), + prepared_var_candidate.as_ref(), + ] + .into_iter() + .flatten() + .any(|expr| !self.prepared_candidate_needs_legacy_compare_help(expr)); let prev_block_addr = self.current_block_addr.replace(Some(block.addr)); let prev_op_idx = self.current_op_idx.replace(Some(branch_idx)); diff --git a/crates/r2dec/src/fold/tests/flags.rs b/crates/r2dec/src/fold/tests/flags.rs index 1968ad4..1ff724c 100644 --- a/crates/r2dec/src/fold/tests/flags.rs +++ b/crates/r2dec/src/fold/tests/flags.rs @@ -1,4 +1,79 @@ use super::*; +use crate::fold::FoldingContext; +use crate::fold::context::{FoldArchConfig, FoldInputs}; +use r2il::{ArchSpec, R2ILBlock, R2ILOp, RegisterDef, Varnode}; +use r2types::{ + SymbolicBranchFact, SymbolicCompiledCondition, SymbolicConditionPrecision, + SymbolicReachabilityStatus, SymbolicSemanticFacts, +}; +use std::collections::{BTreeMap, HashMap, HashSet}; + +fn make_test_arch_x86_64() -> ArchSpec { + let mut arch = ArchSpec::new("x86-64"); + arch.add_register(RegisterDef::new("RAX", 0x00, 8)); + arch.add_register(RegisterDef::new("RDI", 0x10, 8)); + arch.add_register(RegisterDef::new("RSI", 0x18, 8)); + arch.add_register(RegisterDef::new("RBP", 0x20, 8)); + arch.add_register(RegisterDef::new("RSP", 0x28, 8)); + arch +} + +fn prepared_from_r2il_blocks(blocks: &[R2ILBlock], arch: &ArchSpec) -> r2ssa::SsaArtifact { + r2ssa::SsaArtifact::for_decompile(blocks, Some(arch)).expect("prepared SSA should build") +} + +fn make_x86_64_ctx_with_prepared<'a>(prepared_ssa: &'a r2ssa::SsaArtifact) -> FoldingContext<'a> { + let arch = Box::leak(Box::new(FoldArchConfig { + ptr_size: 64, + sp_name: "rsp".to_string(), + fp_name: "rbp".to_string(), + ret_reg_name: "rax".to_string(), + arg_regs: vec![ + "rdi".to_string(), + "rsi".to_string(), + "rdx".to_string(), + "rcx".to_string(), + "r8".to_string(), + "r9".to_string(), + ], + caller_saved_regs: HashSet::new(), + })); + let empty_u64 = Box::leak(Box::new(HashMap::new())); + let empty_stack = Box::leak(Box::new(HashMap::new())); + let empty_stack_slots = Box::leak(Box::new(BTreeMap::new())); + let empty_visible = Box::leak(Box::new(Vec::new())); + let empty_str = Box::leak(Box::new(HashMap::new())); + let empty_fn = Box::leak(Box::new(HashMap::new())); + let empty_callee = Box::leak(Box::new(BTreeMap::new())); + let empty_ty = Box::leak(Box::new(HashMap::new())); + + let mut ctx = FoldingContext::from_inputs(FoldInputs { + arch, + function_names: empty_u64, + strings: empty_u64, + symbols: empty_u64, + known_function_signatures: empty_fn, + callee_facts: empty_callee, + stack_slots: empty_stack_slots, + external_stack_vars: empty_stack, + visible_bindings: empty_visible, + external_type_db: Box::leak(Box::new(r2types::ExternalTypeDb::default())), + symbolic_facts: Box::leak(Box::new(r2types::SymbolicSemanticFacts::default())), + param_register_aliases: empty_str, + type_hints: empty_ty, + type_oracle: None, + function_return_type: None, + prepared_ssa: None, + interproc_summary_set: None, + prepared_semantic_view: None, + prepared_objects: None, + prepared_memory: None, + prepared_predicates: None, + prepared_call_sites: None, + }); + ctx.inputs.prepared_ssa = Some(prepared_ssa); + ctx +} #[test] fn expr_contains_opaque_temp_uses_visit_over_nested_nodes() { @@ -75,3 +150,63 @@ fn tmp_flag_aliases_reconstruct_signed_ge_condition() { ) ); } + +#[test] +fn exact_compiled_condition_shortcuts_to_literal_condition() { + let arch = make_test_arch_x86_64(); + let mut entry = R2ILBlock::new(0x1000, 4); + entry.push(R2ILOp::IntNotEqual { + dst: Varnode::unique(1, 1), + a: Varnode::register(0x10, 4), + b: Varnode::constant(0, 4), + }); + entry.push(R2ILOp::CBranch { + target: Varnode::constant(0x1008, 8), + cond: Varnode::unique(1, 1), + }); + let mut fallthrough = R2ILBlock::new(0x1004, 4); + fallthrough.push(R2ILOp::Return { + target: Varnode::constant(0, 8), + }); + let mut taken = R2ILBlock::new(0x1008, 4); + taken.push(R2ILOp::Return { + target: Varnode::constant(1, 8), + }); + + let prepared = + prepared_from_r2il_blocks(&[entry, fallthrough, taken], &arch).with_name("exact_compiled"); + let mut ctx = make_x86_64_ctx_with_prepared(&prepared); + let mut facts = SymbolicSemanticFacts::default(); + facts.branch_facts.push(SymbolicBranchFact { + block_addr: 0x1000, + true_target: 0x1008, + false_target: 0x1004, + true_status: SymbolicReachabilityStatus::Unreachable, + false_status: SymbolicReachabilityStatus::Reachable, + true_condition: None, + false_condition: Some("false".to_string()), + true_compiled: None, + false_compiled: Some(SymbolicCompiledCondition { + simplified: "false".to_string(), + terms: vec!["false".to_string()], + memory_terms: vec![], + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: SymbolicConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }), + }); + ctx.inputs.symbolic_facts = Box::leak(Box::new(facts)); + + let entry = prepared.function().get_block(0x1000).expect("entry"); + assert_eq!( + ctx.symbolic_exact_compiled_condition_expr(entry.addr), + Some(CExpr::IntLit(0)) + ); + assert_eq!( + ctx.extract_condition_from_block(entry), + Some(CExpr::IntLit(0)) + ); +} diff --git a/crates/r2dec/src/fold/tests/pipeline.rs b/crates/r2dec/src/fold/tests/pipeline.rs index 8c57280..8bc48c9 100644 --- a/crates/r2dec/src/fold/tests/pipeline.rs +++ b/crates/r2dec/src/fold/tests/pipeline.rs @@ -226,6 +226,7 @@ mod tests { external_stack_vars: empty_stack, visible_bindings: empty_visible, external_type_db: Box::leak(Box::new(r2types::ExternalTypeDb::default())), + symbolic_facts: Box::leak(Box::new(r2types::SymbolicSemanticFacts::default())), param_register_aliases: empty_str, type_hints: empty_ty, type_oracle: None, @@ -275,6 +276,7 @@ mod tests { external_stack_vars: empty_stack, visible_bindings: empty_visible, external_type_db: Box::leak(Box::new(r2types::ExternalTypeDb::default())), + symbolic_facts: Box::leak(Box::new(r2types::SymbolicSemanticFacts::default())), param_register_aliases: empty_str, type_hints: empty_ty, type_oracle: None, diff --git a/crates/r2dec/src/lib.rs b/crates/r2dec/src/lib.rs index 81065f7..3e21ba6 100644 --- a/crates/r2dec/src/lib.rs +++ b/crates/r2dec/src/lib.rs @@ -53,9 +53,11 @@ use r2ssa::SSAFunction; use r2ssa::SSAOp; use r2types::{ CTypeLike, ExternalRegisterParamSpec, ExternalTypeDb, FunctionSignatureSpec, FunctionType, - FunctionTypeFacts, StackSlotKey, TypeInference, TypeOracle, VisibleBinding, VisibleBindingKind, + FunctionTypeFacts, StackSlotKey, SymbolicInterpreterKind, TypeInference, TypeOracle, + VisibleBinding, VisibleBindingKind, }; use std::collections::HashSet; +use std::fmt::Write as _; fn is_generic_arg_name(name: &str) -> bool { let lower = name.trim().to_ascii_lowercase(); @@ -176,6 +178,37 @@ fn type_like_to_ctype(ty: &CTypeLike) -> CType { } } +fn format_vm_target_list(targets: &[u64]) -> String { + if targets.is_empty() { + return "[]".to_string(); + } + let rendered = targets + .iter() + .map(|target| format!("0x{:x}", target)) + .collect::>() + .join(", "); + format!("[{rendered}]") +} + +fn format_vm_state_updates(updates: &[r2types::SymbolicVmStateUpdate]) -> String { + if updates.is_empty() { + return "[]".to_string(); + } + let rendered = updates + .iter() + .map(|update| format!("{}={}", update.output, update.expr)) + .collect::>() + .join(", "); + format!("[{rendered}]") +} + +fn format_vm_summary_kind(kind: SymbolicInterpreterKind) -> &'static str { + match kind { + SymbolicInterpreterKind::SwitchDispatch => "switch_dispatch", + SymbolicInterpreterKind::IndirectDispatch => "indirect_dispatch", + } +} + fn merge_params_with_external_signature( recovered_params: Vec, signature: Option<&FunctionSignatureSpec>, @@ -663,6 +696,145 @@ impl Decompiler { } } + fn semantic_vm_summary_comment(&self) -> Option { + let vm_step = self + .context + .type_facts + .symbolic_facts + .vm_step + .as_ref() + .or(self.context.type_facts.symbolic_facts.vm_transfer.as_ref())?; + + let mut out = String::new(); + let _ = writeln!(&mut out, "r2dec semantic summary: vm_summary"); + let _ = writeln!( + &mut out, + "kind={} dispatch_header=0x{:x} loop_header=0x{:x} selector={} targets={} default_target={} latches={} step_blocks={} transfers={}", + format_vm_summary_kind(vm_step.kind), + vm_step.dispatch_header, + vm_step.loop_header, + vm_step.selector.as_deref().unwrap_or(""), + vm_step.dispatch_targets.len(), + vm_step + .default_target + .map(|target| format!("0x{:x}", target)) + .unwrap_or_else(|| "none".to_string()), + vm_step.loop_latches.len(), + vm_step.step_blocks.len(), + vm_step.transfers.len(), + ); + if !vm_step.state_inputs.is_empty() || !vm_step.state_outputs.is_empty() { + let _ = writeln!( + &mut out, + "state_inputs=[{}] state_outputs=[{}]", + vm_step.state_inputs.join(", "), + vm_step.state_outputs.join(", "), + ); + } + + let transfer_preview = vm_step.transfers.iter().take(4).collect::>(); + if !transfer_preview.is_empty() { + for transfer in transfer_preview { + let selector_update = transfer + .selector_update + .as_ref() + .map(|update| format!("{}={}", update.output, update.expr)) + .unwrap_or_else(|| "none".to_string()); + let _ = writeln!( + &mut out, + "transfer handler=0x{:x} cases={} blocks={} exits={} updates={} selector_update={} exact={} redispatch={} return={} truncated={}", + transfer.handler_target, + format_vm_target_list(&transfer.case_values), + format_vm_target_list(&transfer.region_blocks), + format_vm_target_list(&transfer.exit_targets), + format_vm_state_updates(&transfer.state_updates), + selector_update, + transfer.exact, + transfer.redispatch, + transfer.may_return, + transfer.truncated, + ); + } + } + + let mut preview_handlers = vm_step.handler_regions.keys().copied().collect::>(); + preview_handlers.sort_unstable(); + for handler in preview_handlers.into_iter().take(4) { + let regions = vm_step + .handler_regions + .get(&handler) + .map(|regions| format_vm_target_list(regions)) + .unwrap_or_else(|| "[]".to_string()); + let cases = vm_step + .case_values_by_target + .get(&handler) + .map(|values| format_vm_target_list(values)) + .unwrap_or_else(|| "[]".to_string()); + let updates = vm_step + .handler_state_updates + .get(&handler) + .map(|updates| format_vm_state_updates(updates)) + .unwrap_or_else(|| "[]".to_string()); + let inputs = vm_step + .handler_state_inputs + .get(&handler) + .map(|values| format!("[{}]", values.join(", "))) + .unwrap_or_else(|| "[]".to_string()); + let outputs = vm_step + .handler_state_outputs + .get(&handler) + .map(|values| format!("[{}]", values.join(", "))) + .unwrap_or_else(|| "[]".to_string()); + let exits = vm_step + .handler_exit_targets + .get(&handler) + .map(|values| format_vm_target_list(values)) + .unwrap_or_else(|| "[]".to_string()); + let _ = writeln!( + &mut out, + "handler 0x{:x}: regions={} cases={} inputs={} outputs={} updates={} reads={} writes={} calls={} branches={} exits={}", + handler, + regions, + cases, + inputs, + outputs, + updates, + vm_step + .handler_memory_reads + .get(&handler) + .copied() + .unwrap_or(0), + vm_step + .handler_memory_writes + .get(&handler) + .copied() + .unwrap_or(0), + vm_step.handler_calls.get(&handler).copied().unwrap_or(0), + vm_step + .handler_conditional_branches + .get(&handler) + .copied() + .unwrap_or(0), + exits, + ); + } + + if !vm_step.redispatch_handlers.is_empty() + || !vm_step.returning_handlers.is_empty() + || !vm_step.truncated_handlers.is_empty() + { + let _ = writeln!( + &mut out, + "redispatch_handlers={} returning_handlers={} truncated_handlers={}", + format_vm_target_list(&vm_step.redispatch_handlers), + format_vm_target_list(&vm_step.returning_handlers), + format_vm_target_list(&vm_step.truncated_handlers), + ); + } + + Some(out.trim_end().to_string()) + } + fn linearize_function_body( &self, func: &SSAFunction, @@ -869,6 +1041,7 @@ impl Decompiler { external_stack_vars: &self.context.type_facts.external_stack_vars, visible_bindings: &self.context.type_facts.visible_bindings, external_type_db: &self.context.type_facts.external_type_db, + symbolic_facts: &self.context.type_facts.symbolic_facts, param_register_aliases: ¶m_register_aliases, type_hints: &type_hints, type_oracle, @@ -946,6 +1119,10 @@ impl Decompiler { body_stmt = fold_ctx.normalize_final_stmt_calls(body_stmt); body_stmt = fold_ctx.prune_dead_temp_assignments_in_stmt(body_stmt); + if let Some(comment) = self.semantic_vm_summary_comment() { + body_stmt = Self::prepend_comment(body_stmt, comment); + } + // Build the C function let func_name = func .name @@ -1927,7 +2104,10 @@ mod tests { use r2ssa::SSAFunction; use r2types::{ ExternalField, ExternalStruct, FunctionParamSpec, FunctionSignatureSpec, FunctionTypeFacts, + SymbolicInterpreterKind, SymbolicSemanticFacts, SymbolicVmStateUpdate, + SymbolicVmStepSummary, }; + use std::collections::BTreeMap; fn ssa_from_ops(ops: Vec, arch: &ArchSpec) -> SSAFunction { let mut block = R2ILBlock::new(0x1000, 4); @@ -3639,6 +3819,7 @@ mod tests { external_stack_vars: &decompiler.context.type_facts.external_stack_vars, visible_bindings: &decompiler.context.type_facts.visible_bindings, external_type_db: &decompiler.context.type_facts.external_type_db, + symbolic_facts: &decompiler.context.type_facts.symbolic_facts, param_register_aliases: ¶m_register_aliases, type_hints: &type_hints, type_oracle: combined_type_oracle @@ -4003,4 +4184,90 @@ mod tests { "observed arm64 struct-array return path should stay semantic, got:\n{output}" ); } + + #[test] + fn decompiler_prepends_vm_semantic_summary_comment() { + let func = ssa_from_ops( + vec![R2ILOp::Return { + target: Varnode::constant(0, 8), + }], + &test_arch_for_decompile(), + ); + + let mut decompiler = Decompiler::new(DecompilerConfig::default()); + decompiler.set_type_facts(FunctionTypeFacts { + symbolic_facts: SymbolicSemanticFacts { + vm_step: Some(SymbolicVmStepSummary { + kind: SymbolicInterpreterKind::SwitchDispatch, + loop_header: 0x1000, + dispatch_header: 0x1000, + selector: Some("vm.sel".to_string()), + dispatch_targets: vec![0x1004, 0x1008], + default_target: Some(0x1010), + case_values_by_target: BTreeMap::from([ + (0x1004, vec![1, 2]), + (0x1008, vec![3]), + ]), + loop_latches: vec![0x1000], + state_inputs: vec!["state".to_string(), "pc".to_string()], + state_outputs: vec!["state".to_string(), "pc".to_string()], + step_blocks: vec![0x1000, 0x1004], + handler_regions: BTreeMap::from([(0x1004, vec![0x1004, 0x1008])]), + handler_state_inputs: BTreeMap::from([(0x1004, vec!["state".to_string()])]), + handler_state_outputs: BTreeMap::from([(0x1004, vec!["state".to_string()])]), + handler_state_updates: BTreeMap::from([( + 0x1004, + vec![SymbolicVmStateUpdate { + output: "state".to_string(), + expr: "state + 1".to_string(), + value: r2types::SymbolicVmValueExpr::Expr("state + 1".to_string()), + exact: false, + }], + )]), + handler_memory_reads: BTreeMap::from([(0x1004, 1)]), + handler_memory_writes: BTreeMap::from([(0x1004, 1)]), + handler_calls: BTreeMap::from([(0x1004, 0)]), + handler_conditional_branches: BTreeMap::from([(0x1004, 0)]), + handler_exit_targets: BTreeMap::from([(0x1004, vec![0x1008])]), + redispatch_handlers: vec![0x1000], + returning_handlers: vec![], + truncated_handlers: vec![], + transfers: vec![r2types::SymbolicVmTransferArm { + handler_target: 0x1004, + case_values: vec![1, 2], + region_blocks: vec![0x1004, 0x1008], + exit_targets: vec![0x1008], + state_updates: vec![SymbolicVmStateUpdate { + output: "state".to_string(), + expr: "state + 1".to_string(), + value: r2types::SymbolicVmValueExpr::Expr("state + 1".to_string()), + exact: false, + }], + selector_update: Some(SymbolicVmStateUpdate { + output: "vm.sel".to_string(), + expr: "3".to_string(), + value: r2types::SymbolicVmValueExpr::Const(3), + exact: true, + }), + exact: false, + redispatch: false, + may_return: false, + truncated: false, + }], + }), + ..SymbolicSemanticFacts::default() + }, + ..FunctionTypeFacts::default() + }); + + let output = decompiler.decompile(&func); + assert!( + output.contains("r2dec semantic summary: vm_summary"), + "expected VM semantic summary comment, got:\n{output}" + ); + assert!( + output.contains("dispatch_header=0x1000") && output.contains("handler 0x1004"), + "expected richer VM details in summary comment, got:\n{output}" + ); + } } diff --git a/crates/r2dec/src/structure.rs b/crates/r2dec/src/structure.rs index 9902a34..647641f 100644 --- a/crates/r2dec/src/structure.rs +++ b/crates/r2dec/src/structure.rs @@ -7,6 +7,7 @@ use std::collections::{HashMap, HashSet}; use r2ssa::cfg::BlockTerminator; use r2ssa::{CFGEdge, SSAFunction}; +use r2types::SymbolicReachabilityStatus; use crate::ast::{BinaryOp, CExpr, CStmt, UnaryOp}; use crate::fold::FoldingContext; @@ -178,6 +179,14 @@ impl<'a, 'o> ControlFlowStructurer<'a, 'o> { else_region, merge_block, } => { + if let Some(rewritten) = self.try_structure_symbolic_exact_if( + *cond_block, + then_region, + else_region.as_deref(), + *merge_block, + ) { + return rewritten; + } if let Some(rewritten) = self.try_structure_if_else_with_slot_merge_returns( *cond_block, then_region, @@ -252,6 +261,22 @@ impl<'a, 'o> ControlFlowStructurer<'a, 'o> { None => return CExpr::Var("switch_expr".to_string()), }; + if let Some(vm_step) = self + .fold_ctx + .inputs + .symbolic_facts + .vm_step_for_dispatch_header(switch_addr) + .or_else(|| { + self.fold_ctx + .inputs + .symbolic_facts + .vm_transfer_for_dispatch_header(switch_addr) + }) + && let Some(selector) = vm_step.selector.as_ref() + { + return CExpr::Var(selector.clone()); + } + if let Some(expr) = self.fold_ctx.resolve_switch_expr_for_block(switch_addr) { return expr; } @@ -401,6 +426,53 @@ impl<'a, 'o> ControlFlowStructurer<'a, 'o> { Some((true_target?, false_target?)) } + fn symbolic_exact_reachable_target(&self, cond_block: u64) -> Option { + let fact = self + .fold_ctx + .inputs + .symbolic_facts + .branch_fact_for_block(cond_block)?; + match (fact.true_status, fact.false_status) { + (SymbolicReachabilityStatus::Reachable, SymbolicReachabilityStatus::Unreachable) => { + Some(fact.true_target) + } + (SymbolicReachabilityStatus::Unreachable, SymbolicReachabilityStatus::Reachable) => { + Some(fact.false_target) + } + _ => None, + } + } + + fn try_structure_symbolic_exact_if( + &mut self, + cond_block: u64, + then_region: &Region, + else_region: Option<&Region>, + merge_block: Option, + ) -> Option { + let reachable_target = self.symbolic_exact_reachable_target(cond_block)?; + let reachable_region = if then_region.entry() == reachable_target { + then_region + } else { + let else_region = else_region?; + if else_region.entry() != reachable_target { + return None; + } + else_region + }; + + let mut prefix = self.structure_block_prefix_stmts(cond_block); + Self::append_stmt_body_flat(&mut prefix, self.structure_region(reachable_region)); + if let Some(merge_addr) = merge_block { + Self::append_stmt_body_flat(&mut prefix, self.structure_block(merge_addr)); + } + Some(if prefix.len() == 1 { + prefix.into_iter().next().unwrap_or(CStmt::Empty) + } else { + CStmt::Block(prefix) + }) + } + fn switch_case_display_bias( &self, switch_block: u64, @@ -776,6 +848,7 @@ impl<'a, 'o> ControlFlowStructurer<'a, 'o> { then_body, else_body, }; + let stmt = Self::rewrite_constant_condition_stmt(stmt); let stmt = Self::rewrite_if_short_circuit(stmt); let stmt = Self::rewrite_if_condition_inversion(stmt); let stmt = Self::rewrite_empty_if_bodies(stmt); @@ -833,6 +906,32 @@ impl<'a, 'o> ControlFlowStructurer<'a, 'o> { } } + fn rewrite_constant_condition_stmt(stmt: CStmt) -> CStmt { + match stmt { + CStmt::If { + cond, + then_body, + else_body, + } if Self::is_const_true_expr(&cond) => *then_body, + CStmt::If { + cond, + then_body: _, + else_body, + } if Self::is_const_false_expr(&cond) => { + else_body.map(|stmt| *stmt).unwrap_or(CStmt::Empty) + } + other => other, + } + } + + fn is_const_true_expr(expr: &CExpr) -> bool { + matches!(expr, CExpr::IntLit(1) | CExpr::UIntLit(1)) + } + + fn is_const_false_expr(expr: &CExpr) -> bool { + matches!(expr, CExpr::IntLit(0) | CExpr::UIntLit(0)) + } + fn rewrite_if_short_circuit(stmt: CStmt) -> CStmt { let CStmt::If { cond, @@ -2140,6 +2239,14 @@ impl<'a, 'o> ControlFlowStructurer<'a, 'o> { mod tests { use super::ControlFlowStructurer; use crate::ast::{BinaryOp, CExpr, CStmt, UnaryOp}; + use crate::fold::FoldingContext; + use r2il::{ArchSpec, R2ILBlock, R2ILOp, RegisterDef, Varnode}; + use r2ssa::SSAFunction; + use r2types::{ + SymbolicInterpreterKind, SymbolicSemanticFacts, SymbolicVmStateUpdate, + SymbolicVmStepSummary, + }; + use std::collections::BTreeMap; fn v(name: &str) -> CExpr { CExpr::Var(name.to_string()) @@ -2153,6 +2260,25 @@ mod tests { expr_stmt(CExpr::assign(v(lhs), rhs)) } + fn test_arch() -> ArchSpec { + let mut arch = ArchSpec::new("x86-64"); + arch.add_register(RegisterDef::new("RAX", 0x00, 8)); + arch.add_register(RegisterDef::new("RDI", 0x10, 8)); + arch.add_register(RegisterDef::new("RBP", 0x20, 8)); + arch.add_register(RegisterDef::new("RSP", 0x28, 8)); + arch + } + + fn function_with_single_block(addr: u64) -> SSAFunction { + let mut block = R2ILBlock::new(addr, 4); + block.push(R2ILOp::Return { + target: Varnode::constant(0, 8), + }); + SSAFunction::from_blocks_with_arch(&[block], Some(&test_arch())) + .expect("ssa function") + .with_name("vm_summary_demo") + } + #[test] fn rewrites_canonical_while_to_for() { let input = CStmt::Block(vec![ @@ -2531,6 +2657,24 @@ mod tests { assert_eq!(cleaned, CStmt::Empty); } + #[test] + fn constant_true_if_collapses_to_then_body() { + let input = CStmt::if_stmt(CExpr::IntLit(1), assign("x", CExpr::IntLit(7)), None); + let cleaned = ControlFlowStructurer::cleanup(input); + assert_eq!(cleaned, assign("x", CExpr::IntLit(7))); + } + + #[test] + fn constant_false_if_collapses_to_else_body() { + let input = CStmt::if_stmt( + CExpr::IntLit(0), + assign("x", CExpr::IntLit(7)), + Some(assign("x", CExpr::IntLit(9))), + ); + let cleaned = ControlFlowStructurer::cleanup(input); + assert_eq!(cleaned, assign("x", CExpr::IntLit(9))); + } + #[test] fn guarded_switch_with_trailing_return_becomes_switch_default() { let input = CStmt::Block(vec![CStmt::if_stmt( @@ -2719,4 +2863,70 @@ mod tests { "Suffix-equivalent loop vars (local/local_4) should be treated as matching" ); } + + #[test] + fn uses_vm_transfer_selector_when_vm_step_is_absent() { + let func = function_with_single_block(0x1000); + let mut ctx = FoldingContext::new(64); + let facts = SymbolicSemanticFacts { + vm_transfer: Some(SymbolicVmStepSummary { + kind: SymbolicInterpreterKind::SwitchDispatch, + loop_header: 0x1000, + dispatch_header: 0x1000, + selector: Some("vm.sel".to_string()), + dispatch_targets: vec![0x1004], + default_target: None, + case_values_by_target: BTreeMap::from([(0x1004, vec![1, 2])]), + loop_latches: vec![0x1000], + state_inputs: vec!["state".to_string()], + state_outputs: vec!["state".to_string()], + step_blocks: vec![0x1000], + handler_regions: BTreeMap::from([(0x1004, vec![0x1004, 0x1008])]), + handler_state_inputs: BTreeMap::from([(0x1004, vec!["state".to_string()])]), + handler_state_outputs: BTreeMap::from([(0x1004, vec!["state".to_string()])]), + handler_state_updates: BTreeMap::from([( + 0x1004, + vec![SymbolicVmStateUpdate { + output: "state".to_string(), + expr: "state + 1".to_string(), + value: r2types::SymbolicVmValueExpr::Expr("state + 1".to_string()), + exact: false, + }], + )]), + handler_memory_reads: BTreeMap::from([(0x1004, 1)]), + handler_memory_writes: BTreeMap::from([(0x1004, 1)]), + handler_calls: BTreeMap::from([(0x1004, 0)]), + handler_conditional_branches: BTreeMap::from([(0x1004, 0)]), + handler_exit_targets: BTreeMap::from([(0x1004, vec![0x1008])]), + redispatch_handlers: vec![0x1000], + returning_handlers: vec![], + truncated_handlers: vec![], + transfers: vec![r2types::SymbolicVmTransferArm { + handler_target: 0x1004, + case_values: vec![1, 2], + region_blocks: vec![0x1004, 0x1008], + exit_targets: vec![0x1008], + state_updates: vec![SymbolicVmStateUpdate { + output: "state".to_string(), + expr: "state + 1".to_string(), + value: r2types::SymbolicVmValueExpr::Expr("state + 1".to_string()), + exact: false, + }], + selector_update: None, + exact: false, + redispatch: false, + may_return: false, + truncated: false, + }], + }), + ..SymbolicSemanticFacts::default() + }; + ctx.inputs.symbolic_facts = Box::leak(Box::new(facts)); + + let mut structurer = ControlFlowStructurer::new(&func, &ctx); + assert_eq!( + structurer.get_switch_expression(0x1000), + CExpr::Var("vm.sel".to_string()) + ); + } } diff --git a/crates/r2ssa/src/interproc.rs b/crates/r2ssa/src/interproc.rs index 1616f4a..7effea3 100644 --- a/crates/r2ssa/src/interproc.rs +++ b/crates/r2ssa/src/interproc.rs @@ -731,6 +731,9 @@ fn resolve_return_relation( observations: &[SummaryValueObservation], current: &BTreeMap, ) -> SummaryReturnRelation { + if observations.is_empty() { + return SummaryReturnRelation::Unknown; + } let mut relation: Option = None; for observation in observations { let next = match observation { @@ -766,7 +769,7 @@ fn resolve_return_relation( } } - relation.unwrap_or(SummaryReturnRelation::Void) + relation.unwrap_or(SummaryReturnRelation::Unknown) } fn collect_local_summary_facts(prepared: &SsaArtifact, abi: &AbiProfile) -> LocalSummaryFacts { @@ -981,34 +984,46 @@ fn classify_memory_access_location_value( } let rooted = canonical_root_value(prepared, value_id); - if let Some(var) = prepared.value_var(rooted) { - if let Some(idx) = abi.arg_index_for_name(&var.name) { - return arg_location(idx, Some(0), Some(width)); - } - if let Some(address) = parse_const_name(&var.name) { - return global_location(address, Some(0), Some(width)); - } + let mut candidates = vec![value_id]; + if rooted != value_id { + candidates.push(rooted); } - if let Some(object_id) = prepared.objects().object_for_value(rooted) - && let Some(object) = prepared.objects().object(object_id) - { - match object.kind { - ObjectKind::Global { address, .. } => { + for candidate in &candidates { + if let Some(var) = prepared.value_var(*candidate) { + if let Some(idx) = abi.arg_index_for_name(&var.name) { + return arg_location(idx, Some(0), Some(width)); + } + if let Some(address) = parse_const_name(&var.name) { return global_location(address, Some(0), Some(width)); } - ObjectKind::HeapAlloc { .. } => { - return SummaryMemoryLocation { - region: SummaryMemoryRegion::HeapReturn, - range: exact_range(0, width), - }; + } + + if let Some(object_id) = prepared.objects().object_for_value(*candidate) + && let Some(object) = prepared.objects().object(object_id) + { + match object.kind { + ObjectKind::Global { address, .. } => { + return global_location(address, Some(0), Some(width)); + } + ObjectKind::HeapAlloc { .. } => { + return SummaryMemoryLocation { + region: SummaryMemoryRegion::HeapReturn, + range: exact_range(0, width), + }; + } + ObjectKind::EscapedUnknown => {} + _ => {} } - ObjectKind::EscapedUnknown => return unknown_location(), - _ => {} } } - let Some(def_inst) = prepared.graph().def_inst(rooted) else { + let Some((op_value_id, def_inst)) = candidates.iter().find_map(|candidate| { + prepared + .graph() + .def_inst(*candidate) + .map(|inst| (*candidate, inst)) + }) else { return unknown_location(); }; let Some(inst) = prepared.graph().inst(def_inst) else { @@ -1058,6 +1073,9 @@ fn classify_memory_access_location_value( AdditiveLocationCtx::new(width, depth + 1, -1, op), ) } + _ if op_value_id != value_id => { + classify_memory_access_location_value(prepared, abi, value_id, width, depth + 1) + } _ => unknown_location(), } } @@ -1094,12 +1112,8 @@ fn classify_memory_additive_location( right_id: ValueId, ctx: AdditiveLocationCtx, ) -> SummaryMemoryLocation { - let left_const = prepared - .value_var(canonical_root_value(prepared, left_id)) - .and_then(|var| parse_const_name(&var.name)); - let right_const = prepared - .value_var(canonical_root_value(prepared, right_id)) - .and_then(|var| parse_const_name(&var.name)); + let left_const = summary_const_value(prepared, abi, left_id, ctx.depth); + let right_const = summary_const_value(prepared, abi, right_id, ctx.depth); if let Some(k) = right_const { let mut base = @@ -1122,6 +1136,20 @@ fn classify_memory_additive_location( unknown_location() } +fn summary_const_value( + prepared: &SsaArtifact, + abi: &AbiProfile, + value_id: ValueId, + depth: u32, +) -> Option { + match classify_value_operand(prepared, abi, value_id, depth) { + SummaryOperand::Const(value) => Some(value), + _ => prepared + .value_var(canonical_root_value(prepared, value_id)) + .and_then(|var| parse_const_name(&var.name)), + } +} + fn record_arg_count_hint( prepared: &SsaArtifact, abi: &AbiProfile, @@ -1648,6 +1676,15 @@ mod tests { Varnode::constant(value, size) } + fn ram(offset: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Ram, + offset, + size, + meta: None, + } + } + fn block(addr: u64, ops: Vec) -> R2ILBlock { R2ILBlock { addr, @@ -1787,6 +1824,42 @@ mod tests { ); } + #[test] + fn branchind_trampoline_without_return_stays_unknown() { + let arch = x86_64_arch(); + let trampoline = SsaArtifact::for_decompile( + &[block( + 0x3500, + vec![R2ILOp::BranchInd { + target: ram(0x406050, 8), + }], + )], + Some(&arch), + ) + .expect("trampoline ssa") + .with_name("sym.imp.setlocale"); + + let set = solve_interproc_summary_set( + &[InterprocFunctionInput { + id: InterprocFunctionId(0x3500), + name: Some("sym.imp.setlocale".to_string()), + prepared: &trampoline, + }], + Some(&arch), + Some(InterprocFunctionId(0x3500)), + &BTreeMap::new(), + InterprocSolveConfig::default(), + ); + + assert_eq!( + set.summaries + .get(&InterprocFunctionId(0x3500)) + .expect("trampoline summary") + .return_relation, + SummaryReturnRelation::Unknown + ); + } + #[test] fn direct_pointer_load_marks_argument_read() { let arch = x86_64_arch(); @@ -1943,4 +2016,166 @@ mod tests { ) })); } + + #[test] + fn symbolic_store_plus_constant_preserves_arg_offset_range() { + let arch = x86_64_arch(); + let prepared = SsaArtifact::for_symbolic( + &[block( + 0x4300, + vec![ + R2ILOp::IntAdd { + dst: reg(0x80, 8), + a: reg(8, 8), + b: c(2, 8), + }, + R2ILOp::Store { + addr: reg(0x80, 8), + val: reg(16, 2), + space: SpaceId::Ram, + }, + R2ILOp::Return { target: c(0, 8) }, + ], + )], + Some(&arch), + ) + .expect("ssa"); + let abi = AbiProfile::from_arch(Some(&arch)); + let block = prepared.function().get_block(0x4300).expect("block"); + let SSAOp::Store { addr, val, .. } = &block.ops[1] else { + panic!("expected store"); + }; + let addr_id = prepared + .graph() + .value_id_for_var(addr) + .expect("store addr value id"); + let def_inst = prepared.graph().def_inst(addr_id).expect("store addr def"); + let inst = prepared.graph().inst(def_inst).expect("store addr inst"); + let [left_id, right_id] = match inst.inputs.as_slice() { + [left, right] => [*left, *right], + _ => panic!("expected additive store addr inputs"), + }; + let InstPayload::Op(op) = &inst.payload else { + panic!("expected op payload"); + }; + assert_eq!(summary_const_value(&prepared, &abi, right_id, 0), Some(2)); + assert_eq!( + classify_memory_access_location_value(&prepared, &abi, left_id, val.size, 0), + SummaryMemoryLocation { + region: SummaryMemoryRegion::Arg { index: 0 }, + range: exact_range(0, val.size), + } + ); + assert_eq!( + classify_memory_additive_location( + &prepared, + &abi, + left_id, + right_id, + AdditiveLocationCtx::new(val.size, 1, 1, op), + ), + SummaryMemoryLocation { + region: SummaryMemoryRegion::Arg { index: 0 }, + range: exact_range(2, val.size), + } + ); + assert_eq!( + classify_memory_access_location_value(&prepared, &abi, addr_id, val.size, 0), + SummaryMemoryLocation { + region: SummaryMemoryRegion::Arg { index: 0 }, + range: exact_range(2, val.size), + } + ); + let location = classify_memory_access_location(&prepared, &abi, addr, val.size); + assert_eq!( + location, + SummaryMemoryLocation { + region: SummaryMemoryRegion::Arg { index: 0 }, + range: exact_range(2, val.size), + }, + "store address should resolve to arg0+2, got {location:?}; addr={addr:?}; ops={:?}", + block.ops + ); + } + + #[test] + fn symbolic_store_minus_constant_preserves_arg_offset_range() { + let arch = x86_64_arch(); + let prepared = SsaArtifact::for_symbolic( + &[block( + 0x4310, + vec![ + R2ILOp::IntSub { + dst: reg(0x80, 8), + a: reg(8, 8), + b: c(1, 8), + }, + R2ILOp::Store { + addr: reg(0x80, 8), + val: reg(16, 1), + space: SpaceId::Ram, + }, + R2ILOp::Return { target: c(0, 8) }, + ], + )], + Some(&arch), + ) + .expect("ssa"); + let abi = AbiProfile::from_arch(Some(&arch)); + let block = prepared.function().get_block(0x4310).expect("block"); + let SSAOp::Store { addr, val, .. } = &block.ops[1] else { + panic!("expected store"); + }; + let addr_id = prepared + .graph() + .value_id_for_var(addr) + .expect("store addr value id"); + let def_inst = prepared.graph().def_inst(addr_id).expect("store addr def"); + let inst = prepared.graph().inst(def_inst).expect("store addr inst"); + let [left_id, right_id] = match inst.inputs.as_slice() { + [left, right] => [*left, *right], + _ => panic!("expected additive store addr inputs"), + }; + let InstPayload::Op(op) = &inst.payload else { + panic!("expected op payload"); + }; + assert_eq!(summary_const_value(&prepared, &abi, right_id, 0), Some(1)); + assert_eq!( + classify_memory_access_location_value(&prepared, &abi, left_id, val.size, 0), + SummaryMemoryLocation { + region: SummaryMemoryRegion::Arg { index: 0 }, + range: exact_range(0, val.size), + } + ); + assert_eq!( + classify_memory_additive_location( + &prepared, + &abi, + left_id, + right_id, + AdditiveLocationCtx::new(val.size, 1, -1, op), + ), + SummaryMemoryLocation { + region: SummaryMemoryRegion::Arg { index: 0 }, + range: exact_range(-1, val.size), + } + ); + assert_eq!( + classify_memory_access_location_value(&prepared, &abi, addr_id, val.size, 0), + SummaryMemoryLocation { + region: SummaryMemoryRegion::Arg { index: 0 }, + range: exact_range(-1, val.size), + } + ); + let location = classify_memory_access_location(&prepared, &abi, addr, val.size); + assert_eq!( + location, + SummaryMemoryLocation { + region: SummaryMemoryRegion::Arg { index: 0 }, + range: exact_range(-1, val.size), + }, + "store address should resolve to arg0-1, got {location:?}; addr={addr:?}; ops={:?}", + block.ops + ); + } } diff --git a/crates/r2sym/src/backward.rs b/crates/r2sym/src/backward.rs new file mode 100644 index 0000000..f764efc --- /dev/null +++ b/crates/r2sym/src/backward.rs @@ -0,0 +1,1833 @@ +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +use std::rc::Rc; + +use r2ssa::graph::{InstPayload, ValueId}; +use r2ssa::{CallSiteId, PredicateId, SsaArtifact}; +use serde::{Deserialize, Serialize}; +use z3::Context; +use z3::ast::{Ast, BV, Bool}; +use z3::{SatResult as Z3SatResult, Solver}; + +use crate::sim::{CallConv, DerivedFunctionSummary}; +use crate::state::SymState; +use crate::value::SymValue; +use crate::{MemoryRegionId, MemoryRegionKind}; + +const DEFAULT_REVERSE_PATH_LIMIT: usize = 16; +const DEFAULT_MAX_NORMALIZED_OFFSETS: usize = 8; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum BackwardConditionPrecision { + Exact, + OverApprox, + ResidualSearchRequired, + Unsupported, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BackwardConditionSummary { + pub simplified: String, + pub terms: Vec, + pub memory_terms: Vec, + pub backward_memory_substitutions: usize, + pub backward_memory_candidate_enumerations: usize, + pub backward_memory_residual_fallbacks: usize, + pub precision: BackwardConditionPrecision, + pub supported_paths: usize, + pub total_paths: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BackwardMemoryCondition { + pub region: BackwardMemoryRegion, + pub offset_lo: i64, + pub offset_hi: i64, + pub size: u32, + pub exact_offset: bool, + pub expr: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct BackwardRegionRef { + pub id: MemoryRegionId, + pub kind: MemoryRegionKind, + pub name: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub enum BackwardMemoryRegion { + Argument { index: usize }, + Region(BackwardRegionRef), +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +struct NormalizedMemoryLocation { + region: BackwardMemoryRegion, + offset: i64, +} + +pub struct CompiledBackwardCondition { + pub predicate: Bool, + pub summary: BackwardConditionSummary, +} + +#[derive(Clone)] +pub(crate) struct DerivedCallSummaryView<'ctx> { + pub summary: Rc>, + pub callconv: CallConv, +} + +#[derive(Clone)] +struct ReversePath { + block_addr: u64, + phi_predecessors: BTreeMap, + assumptions: Vec<(PredicateId, bool)>, + visited: BTreeSet, +} + +#[derive(Debug)] +enum EvalUnsupported { + Unsupported, + Cycle, +} + +struct ValueTranslator<'a, 'ctx> { + func: &'a SsaArtifact, + state: &'a SymState<'ctx>, + phi_predecessors: &'a BTreeMap, + call_contexts: &'a HashMap>, + memo: HashMap>, + visiting: HashSet, + assumption_constraints: Vec, + memory_terms: Vec, + memory_substitutions: usize, + memory_candidate_enumerations: usize, + memory_residual_fallbacks: usize, + used_unsummarized_memory: bool, +} + +#[derive(Clone)] +struct CallTransformContext<'ctx> { + summary: Rc>, + callconv: CallConv, + args: Vec>, +} + +enum SummaryLocationMatch { + Match(Vec), + NoMatch, + Residual, +} + +impl<'a, 'ctx> ValueTranslator<'a, 'ctx> { + fn new( + func: &'a SsaArtifact, + state: &'a SymState<'ctx>, + phi_predecessors: &'a BTreeMap, + call_contexts: &'a HashMap>, + ) -> Self { + Self { + func, + state, + phi_predecessors, + call_contexts, + memo: HashMap::new(), + visiting: HashSet::new(), + assumption_constraints: Vec::new(), + memory_terms: Vec::new(), + memory_substitutions: 0, + memory_candidate_enumerations: 0, + memory_residual_fallbacks: 0, + used_unsummarized_memory: false, + } + } + + fn note_assumption(&mut self, constraint: Bool) { + self.assumption_constraints.push(constraint); + } + + fn eval_predicate( + &mut self, + predicate: PredicateId, + truth: bool, + ) -> Result { + let fact = self + .func + .predicates() + .predicates + .get(&predicate) + .ok_or(EvalUnsupported::Unsupported)?; + let value = self.eval_value_id(fact.condition)?; + let bool_expr = value_to_bool(self.state.context(), &value); + Ok(if truth { bool_expr } else { bool_expr.not() }) + } + + fn eval_value_id(&mut self, value_id: ValueId) -> Result, EvalUnsupported> { + if let Some(value) = self.memo.get(&value_id).cloned() { + return Ok(value); + } + if !self.visiting.insert(value_id) { + return Err(EvalUnsupported::Cycle); + } + + let var = self + .func + .value_var(value_id) + .ok_or(EvalUnsupported::Unsupported)?; + let result = if var.is_const() { + eval_const_var(var) + } else if let Some(hex) = var.name.strip_prefix("ram:") { + u64::from_str_radix(hex, 16) + .map(|value| SymValue::concrete(value, var.size * 8)) + .map_err(|_| EvalUnsupported::Unsupported) + } else if let Some(inst_id) = self.func.graph().def_inst(value_id) { + let inst = self + .func + .graph() + .inst(inst_id) + .ok_or(EvalUnsupported::Unsupported)?; + match &inst.payload { + InstPayload::Phi { .. } => { + let block_addr = self + .func + .graph() + .block(inst.block) + .map(|block| block.addr) + .ok_or(EvalUnsupported::Unsupported)?; + let selected_pred = self + .phi_predecessors + .get(&block_addr) + .copied() + .ok_or(EvalUnsupported::Unsupported)?; + let block = self + .func + .get_block(block_addr) + .ok_or(EvalUnsupported::Unsupported)?; + let phi = block + .phis + .iter() + .find(|phi| phi.dst == *var) + .ok_or(EvalUnsupported::Unsupported)?; + let source = phi + .sources + .iter() + .find(|(pred, _)| *pred == selected_pred) + .map(|(_, source)| source) + .ok_or(EvalUnsupported::Unsupported)?; + self.eval_ssa_var(source) + } + InstPayload::Op(op) => { + let block_addr = self + .func + .graph() + .block(inst.block) + .map(|block| block.addr) + .ok_or(EvalUnsupported::Unsupported)?; + self.eval_op(inst_id, block_addr, op) + } + } + } else { + Ok(read_input_var(self.state, var)) + }?; + + self.visiting.remove(&value_id); + self.memo.insert(value_id, result.clone()); + Ok(result) + } + + fn eval_ssa_var(&mut self, var: &r2ssa::SSAVar) -> Result, EvalUnsupported> { + if let Some(value_id) = self.func.graph().value_id_for_var(var) { + self.eval_value_id(value_id) + } else { + Ok(read_input_var(self.state, var)) + } + } + + fn eval_op( + &mut self, + inst_id: r2ssa::graph::InstId, + block_addr: u64, + op: &r2ssa::SSAOp, + ) -> Result, EvalUnsupported> { + let ctx = self.state.context(); + use r2ssa::SSAOp::*; + + match op { + Copy { src, .. } => self.eval_ssa_var(src), + Load { dst, addr, .. } => { + if let Some(local_value) = self.local_memory_value(inst_id, dst.size) { + return Ok(local_value); + } + let addr_value = self.eval_ssa_var(addr)?; + let structural_locations = + self.normalized_memory_locations(addr).unwrap_or_default(); + let resolved_locations = self + .resolved_memory_locations(&addr_value, dst.size) + .map(|locations| { + locations + .into_iter() + .filter(is_specific_memory_location) + .collect::>() + }) + .unwrap_or_default(); + let normalized = if !resolved_locations.is_empty() { + resolved_locations.clone() + } else { + structural_locations.clone() + }; + if let Some(call_ctx) = self.call_context_for_inst(inst_id, block_addr).cloned() + && let Some(summary_value) = self.summary_memory_value( + &call_ctx, + &addr_value, + &resolved_locations, + &structural_locations, + dst.size, + ) + { + return Ok(summary_value); + } + let value = self.state.mem_read(&addr_value, dst.size); + if self.record_memory_term(&normalized, dst.size, &value) { + return Ok(value); + } + self.used_unsummarized_memory = true; + self.memory_residual_fallbacks += 1; + Ok(value) + } + IntAdd { a, b, .. } => Ok(self.eval_ssa_var(a)?.add(ctx, &self.eval_ssa_var(b)?)), + IntSub { a, b, .. } => Ok(self.eval_ssa_var(a)?.sub(ctx, &self.eval_ssa_var(b)?)), + IntMult { a, b, .. } => Ok(self.eval_ssa_var(a)?.mul(ctx, &self.eval_ssa_var(b)?)), + IntDiv { a, b, .. } => Ok(self.eval_ssa_var(a)?.udiv(ctx, &self.eval_ssa_var(b)?)), + IntSDiv { a, b, .. } => Ok(self.eval_ssa_var(a)?.sdiv(ctx, &self.eval_ssa_var(b)?)), + IntRem { a, b, .. } => Ok(self.eval_ssa_var(a)?.urem(ctx, &self.eval_ssa_var(b)?)), + IntSRem { a, b, .. } => Ok(self.eval_ssa_var(a)?.srem(ctx, &self.eval_ssa_var(b)?)), + IntNegate { src, .. } => Ok(self.eval_ssa_var(src)?.neg(ctx)), + IntAnd { a, b, .. } => Ok(self.eval_ssa_var(a)?.and(ctx, &self.eval_ssa_var(b)?)), + IntOr { a, b, .. } => Ok(self.eval_ssa_var(a)?.or(ctx, &self.eval_ssa_var(b)?)), + IntXor { a, b, .. } => Ok(self.eval_ssa_var(a)?.xor(ctx, &self.eval_ssa_var(b)?)), + IntNot { src, .. } => Ok(self.eval_ssa_var(src)?.not(ctx)), + IntLeft { a, b, .. } => Ok(self.eval_ssa_var(a)?.shl(ctx, &self.eval_ssa_var(b)?)), + IntRight { a, b, .. } => Ok(self.eval_ssa_var(a)?.lshr(ctx, &self.eval_ssa_var(b)?)), + IntSRight { a, b, .. } => Ok(self.eval_ssa_var(a)?.ashr(ctx, &self.eval_ssa_var(b)?)), + IntEqual { a, b, .. } => Ok(self.eval_ssa_var(a)?.eq(ctx, &self.eval_ssa_var(b)?)), + IntNotEqual { a, b, .. } => { + let eq = self.eval_ssa_var(a)?.eq(ctx, &self.eval_ssa_var(b)?); + Ok(eq.not(ctx)) + } + IntLess { a, b, .. } => Ok(self.eval_ssa_var(a)?.ult(ctx, &self.eval_ssa_var(b)?)), + IntSLess { a, b, .. } => Ok(self.eval_ssa_var(a)?.slt(ctx, &self.eval_ssa_var(b)?)), + IntLessEqual { a, b, .. } => Ok(self.eval_ssa_var(a)?.ule(ctx, &self.eval_ssa_var(b)?)), + IntSLessEqual { a, b, .. } => { + Ok(self.eval_ssa_var(a)?.sle(ctx, &self.eval_ssa_var(b)?)) + } + IntZExt { dst, src } => Ok(self.eval_ssa_var(src)?.zero_extend(ctx, dst.size * 8)), + IntSExt { dst, src } => { + let value = self.eval_ssa_var(src)?; + let current = value.to_bv(ctx); + let extended = if dst.size * 8 > value.bits() { + current.sign_ext(dst.size * 8 - value.bits()) + } else { + current.extract(dst.size * 8 - 1, 0) + }; + Ok(SymValue::symbolic_tainted( + extended, + dst.size * 8, + value.get_taint(), + )) + } + BoolNot { src, .. } => Ok(self.eval_ssa_var(src)?.not(ctx)), + BoolAnd { a, b, .. } => Ok(self.eval_ssa_var(a)?.and(ctx, &self.eval_ssa_var(b)?)), + BoolOr { a, b, .. } => Ok(self.eval_ssa_var(a)?.or(ctx, &self.eval_ssa_var(b)?)), + BoolXor { a, b, .. } => Ok(self.eval_ssa_var(a)?.xor(ctx, &self.eval_ssa_var(b)?)), + Piece { hi, lo, .. } => Ok(self.eval_ssa_var(hi)?.concat(ctx, &self.eval_ssa_var(lo)?)), + Subpiece { dst, src, offset } => { + let value = self.eval_ssa_var(src)?; + let low = offset.saturating_mul(8); + let high = low.saturating_add(dst.size * 8).saturating_sub(1); + Ok(value.extract(ctx, high, low)) + } + CallDefine { dst } => self.eval_call_define(inst_id, block_addr, dst), + _ => Err(EvalUnsupported::Unsupported), + } + } + + fn eval_call_define( + &mut self, + inst_id: r2ssa::graph::InstId, + block_addr: u64, + dst: &r2ssa::SSAVar, + ) -> Result, EvalUnsupported> { + let Some(call_ctx) = self.call_context_for_inst(inst_id, block_addr) else { + return Err(EvalUnsupported::Unsupported); + }; + if !register_aliases(call_ctx.callconv.ret_register_name()) + .iter() + .any(|alias| dst.name.eq_ignore_ascii_case(alias)) + { + return Err(EvalUnsupported::Unsupported); + } + let (value, coverage) = summary_return_value(self.state, call_ctx)?; + if let Some(coverage) = coverage { + self.note_assumption(coverage); + } + Ok(value) + } + + fn call_context_for_inst( + &self, + inst_id: r2ssa::graph::InstId, + block_addr: u64, + ) -> Option<&CallTransformContext<'ctx>> { + let (inst_block_addr, op_idx) = self.func.inst_op_site(inst_id)?; + debug_assert_eq!(inst_block_addr, block_addr); + self.func.get_block(block_addr)?; + for scan_idx in (0..op_idx).rev() { + let scan_inst = self + .func + .graph() + .inst_id_for_op_site(block_addr, scan_idx)?; + let Some(call_id) = self.func.call_sites().by_inst.get(&scan_inst).copied() else { + continue; + }; + if let Some(context) = self.call_contexts.get(&call_id) { + return Some(context); + } + } + let predecessor = self.phi_predecessors.get(&block_addr).copied()?; + let predecessor_block = self.func.get_block(predecessor)?; + for scan_idx in (0..predecessor_block.ops.len()).rev() { + let scan_inst = self + .func + .graph() + .inst_id_for_op_site(predecessor, scan_idx)?; + let call_id = self.func.call_sites().by_inst.get(&scan_inst).copied()?; + if let Some(context) = self.call_contexts.get(&call_id) { + return Some(context); + } + } + None + } + + fn summary_memory_value( + &mut self, + call_ctx: &CallTransformContext<'ctx>, + addr: &SymValue<'ctx>, + resolved: &[NormalizedMemoryLocation], + structural: &[NormalizedMemoryLocation], + size: u32, + ) -> Option> { + let mut actual_locations = resolved.to_vec(); + if actual_locations.is_empty() { + actual_locations.extend(structural.iter().cloned()); + } + if actual_locations.is_empty() { + actual_locations.extend(summary_memory_locations(call_ctx, addr)); + } + if actual_locations.is_empty() { + return None; + } + let fallback_summary_locations = summary_memory_locations(call_ctx, addr); + let substitutions = build_call_substitutions(self.state, call_ctx); + for location_group in group_normalized_locations(&actual_locations) { + let summary_group = match self.summary_match_locations(call_ctx, &location_group) { + SummaryLocationMatch::Match(group) => group, + SummaryLocationMatch::NoMatch => { + if fallback_summary_locations.is_empty() { + continue; + } + fallback_summary_locations.clone() + } + SummaryLocationMatch::Residual => { + self.memory_residual_fallbacks += 1; + continue; + } + }; + let mut matches = call_ctx + .summary + .cases + .iter() + .filter_map(|case| { + let covering = + select_covering_writes(&case.memory_writes, &summary_group, size); + if covering.is_empty() || covering_writes_are_ambiguous(&covering) { + return None; + } + let (location, write) = select_best_covering_write(covering)?; + let guard = substitute_bool(&case.guard, &substitutions); + let value = substitute_value( + self.state.context(), + &slice_write_value(self.state.context(), write, location.offset, size), + &substitutions, + ); + Some((guard, value)) + }) + .collect::>(); + let alias_heavy = call_ctx.summary.cases.iter().any(|case| { + covering_writes_are_ambiguous(&select_covering_writes( + &case.memory_writes, + &summary_group, + size, + )) + }); + if alias_heavy { + self.memory_residual_fallbacks += 1; + continue; + } + if matches.is_empty() { + continue; + } + + let mut merged = self.state.mem_read(addr, size); + for (guard, value) in matches.drain(..).rev() { + merged = ite_value(self.state.context(), &guard, &value, &merged); + } + self.record_memory_term(&location_group, size, &merged); + self.memory_substitutions += 1; + return Some(merged); + } + None + } + + fn summary_match_locations( + &self, + call_ctx: &CallTransformContext<'ctx>, + actual_group: &[NormalizedMemoryLocation], + ) -> SummaryLocationMatch { + if actual_group.is_empty() { + return SummaryLocationMatch::NoMatch; + } + if actual_group + .iter() + .all(|location| matches!(location.region, BackwardMemoryRegion::Argument { .. })) + { + return SummaryLocationMatch::Match(actual_group.to_vec()); + } + if actual_group.len() != 1 { + return SummaryLocationMatch::Residual; + } + + let actual = &actual_group[0]; + let BackwardMemoryRegion::Region(actual_region) = &actual.region else { + return SummaryLocationMatch::NoMatch; + }; + if actual_region.kind == MemoryRegionKind::EscapedUnknown { + return SummaryLocationMatch::NoMatch; + } + + let mut translated = BTreeSet::new(); + let pointer_args = summary_pointer_arg_indices(call_ctx); + for (arg_index, base) in call_ctx.args.iter().enumerate() { + if !pointer_args.is_empty() && !pointer_args.contains(&arg_index) { + continue; + } + let Some(base_locations) = self.resolved_memory_locations(base, 1) else { + continue; + }; + for base_location in base_locations { + let BackwardMemoryRegion::Region(base_region) = &base_location.region else { + continue; + }; + if base_region.kind == MemoryRegionKind::EscapedUnknown { + continue; + } + if base_region != actual_region { + continue; + } + translated.insert(NormalizedMemoryLocation { + region: BackwardMemoryRegion::Argument { index: arg_index }, + offset: actual.offset.saturating_sub(base_location.offset), + }); + } + } + + match translated.len() { + 0 => SummaryLocationMatch::NoMatch, + 1 => SummaryLocationMatch::Match(translated.into_iter().collect()), + _ => SummaryLocationMatch::Residual, + } + } + + fn local_memory_value( + &mut self, + inst_id: r2ssa::graph::InstId, + size: u32, + ) -> Option> { + let value = self.local_memory_store_var(inst_id, size)?; + self.eval_ssa_var(&value).ok() + } + + fn local_memory_store_var( + &self, + inst_id: r2ssa::graph::InstId, + size: u32, + ) -> Option { + let uses = self.func.memory().uses_by_inst.get(&inst_id)?; + for use_fact in uses { + if use_fact.location.size != size { + continue; + } + for (def_inst, defs) in &self.func.memory().defs_by_inst { + for def in defs { + if def.next_version != use_fact.version || def.location != use_fact.location { + continue; + } + let inst = self.func.graph().inst(*def_inst)?; + let InstPayload::Op(r2ssa::SSAOp::Store { val, .. }) = &inst.payload else { + continue; + }; + return Some(val.clone()); + } + } + } + None + } + + fn record_memory_term( + &mut self, + locations: &[NormalizedMemoryLocation], + size: u32, + value: &SymValue<'ctx>, + ) -> bool { + if locations.is_empty() { + return false; + } + let mut grouped = BTreeMap::>::new(); + for location in locations { + grouped + .entry(location.region.clone()) + .or_default() + .insert(location.offset); + } + if grouped.len() > 1 { + self.memory_residual_fallbacks += 1; + } + for (region, offsets) in grouped { + let offset_lo = offsets.iter().copied().min().unwrap_or(0); + let offset_hi = offsets.iter().copied().max().unwrap_or(0); + self.memory_terms.push(BackwardMemoryCondition { + region, + offset_lo, + offset_hi, + size, + exact_offset: offset_lo == offset_hi, + expr: value.to_string(), + }); + } + true + } + + fn normalized_memory_locations( + &mut self, + addr: &r2ssa::SSAVar, + ) -> Option> { + if let Some(arg_index) = ssa_var_arg_index(addr) { + return Some(vec![NormalizedMemoryLocation { + region: BackwardMemoryRegion::Argument { index: arg_index }, + offset: 0, + }]); + } + let value_id = self.func.graph().value_id_for_var(addr)?; + self.normalized_memory_location_value_id(value_id) + } + + fn resolved_memory_locations( + &self, + addr: &SymValue<'ctx>, + size: u32, + ) -> Option> { + let mut constraints = self.state.constraints().to_vec(); + constraints.extend(self.assumption_constraints.iter().cloned()); + let resolved = self.state.memory.resolve_pointer(addr, size, &constraints); + let mut locations = Vec::new(); + for pointer in resolved.pointers { + let Some(region) = self.region_for_pointer(pointer.region_id) else { + continue; + }; + let Ok(offset) = i64::try_from(pointer.offset) else { + continue; + }; + locations.push(NormalizedMemoryLocation { region, offset }); + } + (!locations.is_empty()).then_some(locations) + } + + fn region_for_pointer(&self, region_id: MemoryRegionId) -> Option { + let def = self.state.memory.region_def(region_id)?; + Some(BackwardMemoryRegion::Region(BackwardRegionRef { + id: def.id, + kind: def.kind.clone(), + name: def.name.clone(), + })) + } + + fn normalized_memory_location_value_id( + &mut self, + value_id: ValueId, + ) -> Option> { + let inst_id = self.func.graph().def_inst(value_id)?; + let inst = self.func.graph().inst(inst_id)?; + let InstPayload::Op(op) = &inst.payload else { + return None; + }; + use r2ssa::SSAOp::*; + match op { + Copy { src, .. } => self.normalized_memory_locations(src), + IntAdd { a, b, .. } => { + if let Some(base) = self.normalized_memory_locations(a) + && let Some(offsets) = self.resolve_delta_offsets(b, 1) + { + return Some(apply_delta_offsets(&base, &offsets, true)); + } + if let Some(base) = self.normalized_memory_locations(b) + && let Some(offsets) = self.resolve_delta_offsets(a, 1) + { + return Some(apply_delta_offsets(&base, &offsets, true)); + } + None + } + PtrAdd { + base, + index, + element_size, + .. + } => { + if let Some(base) = self.normalized_memory_locations(base) + && let Some(offsets) = self.resolve_delta_offsets(index, *element_size as i64) + { + return Some(apply_delta_offsets(&base, &offsets, true)); + } + None + } + IntSub { a, b, .. } => { + if let Some(base) = self.normalized_memory_locations(a) + && let Some(offsets) = self.resolve_delta_offsets(b, 1) + { + return Some(apply_delta_offsets(&base, &offsets, false)); + } + None + } + PtrSub { + base, + index, + element_size, + .. + } => { + if let Some(base) = self.normalized_memory_locations(base) + && let Some(offsets) = self.resolve_delta_offsets(index, *element_size as i64) + { + return Some(apply_delta_offsets(&base, &offsets, false)); + } + None + } + IntZExt { src, .. } | IntSExt { src, .. } => self.normalized_memory_locations(src), + Subpiece { src, offset, .. } if *offset == 0 => self.normalized_memory_locations(src), + _ => None, + } + } + + fn resolve_delta_offsets(&mut self, delta: &r2ssa::SSAVar, scale: i64) -> Option> { + let delta_value = self.eval_ssa_var(delta).ok()?; + if let Some(concrete) = delta_value.as_concrete() { + return Some(vec![(concrete as i64).saturating_mul(scale)]); + } + let candidates = self.enumerate_bounded_concrete_values(&delta_value)?; + if candidates.is_empty() { + return None; + } + self.memory_candidate_enumerations += 1; + Some( + candidates + .into_iter() + .map(|value| (value as i64).saturating_mul(scale)) + .collect(), + ) + } + + fn enumerate_bounded_concrete_values(&mut self, value: &SymValue<'ctx>) -> Option> { + let ctx = self.state.context(); + let bv = value.to_bv(ctx); + let solver = Solver::new(); + for constraint in self.state.constraints() { + solver.assert(constraint); + } + for constraint in &self.assumption_constraints { + solver.assert(constraint); + } + + let mut values = BTreeSet::new(); + loop { + match solver.check() { + Z3SatResult::Sat => { + let model = solver.get_model()?; + let concrete = model.eval(&bv, true)?.as_u64()?; + values.insert(concrete); + if values.len() > DEFAULT_MAX_NORMALIZED_OFFSETS { + self.memory_residual_fallbacks += 1; + return None; + } + solver.assert(bv.eq(BV::from_u64(concrete, value.bits())).not()); + } + Z3SatResult::Unsat => break, + Z3SatResult::Unknown => { + self.memory_residual_fallbacks += 1; + return None; + } + } + } + + Some(values.into_iter().collect()) + } +} + +pub fn compile_target_precondition<'ctx>( + func: &SsaArtifact, + initial_state: &SymState<'ctx>, + target_addr: u64, +) -> Option { + compile_target_precondition_with_summaries(func, initial_state, target_addr, &HashMap::new()) +} + +pub(crate) fn compile_target_precondition_with_summaries<'ctx>( + func: &SsaArtifact, + initial_state: &SymState<'ctx>, + target_addr: u64, + call_summaries: &HashMap>, +) -> Option { + let reverse_paths = enumerate_reverse_paths(func, target_addr, DEFAULT_REVERSE_PATH_LIMIT)?; + compile_reverse_paths(func, initial_state, reverse_paths, None, call_summaries) +} + +pub(crate) fn compile_branch_precondition_with_summaries<'ctx>( + func: &SsaArtifact, + initial_state: &SymState<'ctx>, + block_addr: u64, + truth: bool, + call_summaries: &HashMap>, +) -> Option { + let predicate = func + .predicates() + .predicates + .iter() + .find_map(|(id, fact)| (fact.block_addr == block_addr).then_some(*id))?; + let reverse_paths = enumerate_reverse_paths(func, block_addr, DEFAULT_REVERSE_PATH_LIMIT)?; + compile_reverse_paths( + func, + initial_state, + reverse_paths, + Some((predicate, truth)), + call_summaries, + ) +} + +pub fn compile_derived_summary_return_postcondition<'ctx, F>( + state: &SymState<'ctx>, + summary: &DerivedFunctionSummary<'ctx>, + callconv: &CallConv, + postcondition: F, +) -> Option +where + F: Fn(&SymValue<'ctx>) -> Bool, +{ + if summary.cases.is_empty() { + return None; + } + + let call = + callconv.collect_call_info(state, summary.arg_count_hint.max(callconv.arg_capacity())); + let substitutions = build_summary_substitutions(state, summary, &call); + let mut terms = Vec::new(); + for case in &summary.cases { + let Some(return_value) = &case.return_value else { + continue; + }; + let guard = substitute_bool(&case.guard, &substitutions); + let value = substitute_value(state.context(), return_value, &substitutions); + let post = postcondition(&value); + terms.push(guard & post); + } + if terms.is_empty() { + return None; + } + let predicate = or_all(state.context(), &terms); + let simplified = predicate.simplify(); + Some(CompiledBackwardCondition { + summary: BackwardConditionSummary { + simplified: simplified.to_string(), + terms: terms + .iter() + .map(|term| term.simplify().to_string()) + .collect(), + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: if matches!( + summary.completion, + crate::sim::DerivedSummaryCompletion::Exact + ) { + BackwardConditionPrecision::Exact + } else { + BackwardConditionPrecision::ResidualSearchRequired + }, + supported_paths: terms.len(), + total_paths: terms.len(), + }, + predicate: simplified, + }) +} + +pub fn compile_derived_summary_memory_postcondition<'ctx, F>( + state: &SymState<'ctx>, + summary: &DerivedFunctionSummary<'ctx>, + callconv: &CallConv, + arg_index: usize, + offset: i64, + size: u32, + postcondition: F, +) -> Option +where + F: Fn(&SymValue<'ctx>) -> Bool, +{ + if summary.cases.is_empty() { + return None; + } + + let call = + callconv.collect_call_info(state, summary.arg_count_hint.max(callconv.arg_capacity())); + let substitutions = build_call_substitutions( + state, + &CallTransformContext { + summary: Rc::new(summary.clone()), + callconv: callconv.clone(), + args: call.args.clone(), + }, + ); + let base = call.args.get(arg_index)?; + let addr = add_signed_offset(state.context(), base, offset, call.arg_bits); + let mut terms = Vec::new(); + for case in &summary.cases { + let Some((location, write)) = select_covering_write( + &case.memory_writes, + &[NormalizedMemoryLocation { + region: BackwardMemoryRegion::Argument { index: arg_index }, + offset, + }], + size, + ) else { + continue; + }; + let guard = substitute_bool(&case.guard, &substitutions); + let value = substitute_value( + state.context(), + &slice_write_value(state.context(), write, location.offset, size), + &substitutions, + ); + terms.push(guard & postcondition(&value)); + } + if terms.is_empty() { + return None; + } + let predicate = or_all(state.context(), &terms); + let simplified = predicate.simplify(); + let memory_term = state + .memory + .resolve_pointer(&addr, size, state.constraints()) + .pointers + .into_iter() + .find_map(|pointer| { + state + .memory + .region_def(pointer.region_id) + .map(|def| BackwardMemoryCondition { + region: BackwardMemoryRegion::Region(BackwardRegionRef { + id: def.id, + kind: def.kind.clone(), + name: def.name.clone(), + }), + offset_lo: i64::try_from(pointer.offset).unwrap_or(0), + offset_hi: i64::try_from(pointer.offset).unwrap_or(0), + size, + exact_offset: true, + expr: state.mem_read(&addr, size).to_string(), + }) + }) + .unwrap_or(BackwardMemoryCondition { + region: BackwardMemoryRegion::Argument { index: arg_index }, + offset_lo: offset, + offset_hi: offset, + size, + exact_offset: true, + expr: state.mem_read(&addr, size).to_string(), + }); + Some(CompiledBackwardCondition { + summary: BackwardConditionSummary { + simplified: simplified.to_string(), + terms: terms + .iter() + .map(|term| term.simplify().to_string()) + .collect(), + memory_terms: vec![memory_term], + backward_memory_substitutions: 1, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: if matches!( + summary.completion, + crate::sim::DerivedSummaryCompletion::Exact + ) { + BackwardConditionPrecision::Exact + } else { + BackwardConditionPrecision::ResidualSearchRequired + }, + supported_paths: terms.len(), + total_paths: terms.len(), + }, + predicate: simplified, + }) +} + +fn enumerate_reverse_paths( + func: &SsaArtifact, + target_addr: u64, + limit: usize, +) -> Option<(Vec, bool)> { + func.get_block(target_addr)?; + + let mut pending = vec![ReversePath { + block_addr: target_addr, + phi_predecessors: BTreeMap::new(), + assumptions: Vec::new(), + visited: BTreeSet::from([target_addr]), + }]; + let mut completed = Vec::new(); + let mut truncated = false; + + while let Some(path) = pending.pop() { + if completed.len() >= limit { + truncated = true; + break; + } + if path.block_addr == func.entry { + completed.push(path); + continue; + } + + let predecessors = func.predecessors(path.block_addr); + if predecessors.is_empty() { + continue; + } + + for predecessor in predecessors { + if path.visited.contains(&predecessor) { + truncated = true; + continue; + } + let mut next = path.clone(); + next.block_addr = predecessor; + next.visited.insert(predecessor); + next.phi_predecessors.insert(path.block_addr, predecessor); + if let Some(assumptions) = func.predicates().block_assumptions.get(&path.block_addr) { + for assumption in assumptions + .iter() + .filter(|assumption| assumption.predecessor == predecessor) + { + next.assumptions + .push((assumption.predicate, assumption.truth)); + } + } + pending.push(next); + } + } + + Some((completed, truncated)) +} + +fn compile_reverse_paths<'ctx>( + func: &SsaArtifact, + initial_state: &SymState<'ctx>, + (paths, truncated): (Vec, bool), + extra_predicate: Option<(PredicateId, bool)>, + call_summaries: &HashMap>, +) -> Option { + compile_reverse_paths_with_extra( + func, + initial_state, + (paths, truncated), + extra_predicate, + call_summaries, + |_, _| Ok(None), + ) +} + +fn compile_reverse_paths_with_extra<'ctx, F>( + func: &SsaArtifact, + initial_state: &SymState<'ctx>, + (paths, truncated): (Vec, bool), + extra_predicate: Option<(PredicateId, bool)>, + call_summaries: &HashMap>, + mut extra_constraint: F, +) -> Option +where + F: for<'a> FnMut( + &mut ValueTranslator<'a, 'ctx>, + &ReversePath, + ) -> Result, EvalUnsupported>, +{ + if paths.is_empty() { + return None; + } + + let mut supported_terms = Vec::new(); + let mut memory_terms = Vec::new(); + let mut unsupported_paths = 0usize; + let mut used_unsummarized_memory = false; + let mut backward_memory_substitutions = 0usize; + let mut backward_memory_candidate_enumerations = 0usize; + let mut backward_memory_residual_fallbacks = 0usize; + for path in &paths { + let call_contexts = + build_call_transform_contexts(func, initial_state, path, call_summaries); + let mut translator = + ValueTranslator::new(func, initial_state, &path.phi_predecessors, &call_contexts); + let mut conjuncts = Vec::new(); + let mut supported = true; + for (predicate, truth) in &path.assumptions { + match translator.eval_predicate(*predicate, *truth) { + Ok(condition) => { + translator.note_assumption(condition.clone()); + conjuncts.push(condition); + } + Err(_) => { + supported = false; + break; + } + } + } + if supported && let Some((predicate, truth)) = extra_predicate { + match translator.eval_predicate(predicate, truth) { + Ok(condition) => { + translator.note_assumption(condition.clone()); + conjuncts.push(condition); + } + Err(_) => supported = false, + } + } + if supported { + match extra_constraint(&mut translator, path) { + Ok(Some(condition)) => { + translator.note_assumption(condition.clone()); + conjuncts.push(condition); + } + Ok(None) => {} + Err(_) => supported = false, + } + } + if !supported { + unsupported_paths += 1; + continue; + } + conjuncts.extend(translator.assumption_constraints.iter().cloned()); + let term = and_all(initial_state.context(), &conjuncts); + used_unsummarized_memory |= translator.used_unsummarized_memory; + backward_memory_substitutions += translator.memory_substitutions; + backward_memory_candidate_enumerations += translator.memory_candidate_enumerations; + backward_memory_residual_fallbacks += translator.memory_residual_fallbacks; + supported_terms.push(term); + memory_terms.extend(translator.memory_terms); + } + + if supported_terms.is_empty() { + return None; + } + + let predicate = or_all(initial_state.context(), &supported_terms); + let simplified = predicate.simplify(); + let precision = if unsupported_paths == 0 + && !truncated + && !used_unsummarized_memory + && backward_memory_residual_fallbacks == 0 + { + BackwardConditionPrecision::Exact + } else if unsupported_paths > 0 + || truncated + || used_unsummarized_memory + || backward_memory_residual_fallbacks > 0 + { + BackwardConditionPrecision::ResidualSearchRequired + } else { + BackwardConditionPrecision::OverApprox + }; + Some(CompiledBackwardCondition { + summary: BackwardConditionSummary { + simplified: simplified.to_string(), + terms: supported_terms + .iter() + .map(|term| term.simplify().to_string()) + .collect(), + memory_terms, + backward_memory_substitutions, + backward_memory_candidate_enumerations, + backward_memory_residual_fallbacks, + precision, + supported_paths: supported_terms.len(), + total_paths: paths.len(), + }, + predicate: simplified, + }) +} + +pub(crate) fn compile_value_postcondition_with_summaries<'ctx, F>( + func: &SsaArtifact, + initial_state: &SymState<'ctx>, + block_addr: u64, + value_var: r2ssa::SSAVar, + postcondition: F, + call_summaries: &HashMap>, +) -> Option +where + F: Fn(&SymValue<'ctx>) -> Bool + Clone, +{ + let reverse_paths = enumerate_reverse_paths(func, block_addr, DEFAULT_REVERSE_PATH_LIMIT)?; + compile_reverse_paths_with_extra( + func, + initial_state, + reverse_paths, + None, + call_summaries, + move |translator, path| { + let _ = path; + let value = translator.eval_ssa_var(&value_var)?; + Ok(Some(postcondition.clone()(&value))) + }, + ) +} + +fn build_call_transform_contexts<'ctx>( + func: &SsaArtifact, + initial_state: &SymState<'ctx>, + path: &ReversePath, + call_summaries: &HashMap>, +) -> HashMap> { + if call_summaries.is_empty() { + return HashMap::new(); + } + + let sequence = path_block_sequence(path); + let mut arg_state = BTreeMap::::new(); + let mut contexts = HashMap::new(); + + for (seq_index, block_addr) in sequence.iter().enumerate() { + let Some(block) = func.get_block(*block_addr) else { + continue; + }; + if seq_index > 0 { + let predecessor = sequence[seq_index - 1]; + for phi in &block.phis { + if let Some(arg_index) = ssa_var_arg_index(&phi.dst) + && let Some((_, source)) = + phi.sources.iter().find(|(pred, _)| *pred == predecessor) + && let Some(value_id) = func.graph().value_id_for_var(source) + { + arg_state.insert(arg_index, value_id); + } + } + } + + for (op_idx, op) in block.ops.iter().enumerate() { + let Some(inst_id) = func.graph().inst_id_for_op_site(*block_addr, op_idx) else { + continue; + }; + if let Some(call_id) = func.call_sites().by_inst.get(&inst_id).copied() + && let Some(callsite) = func.call_sites().by_id.get(&call_id) + && let Some(target) = callsite.direct_target + && let Some(view) = call_summaries.get(&target) + { + let context_snapshot = contexts.clone(); + let mut translator = ValueTranslator::new( + func, + initial_state, + &path.phi_predecessors, + &context_snapshot, + ); + let args = (0..view.callconv.arg_capacity()) + .map(|index| { + arg_state + .get(&index) + .copied() + .and_then(|value_id| translator.eval_value_id(value_id).ok()) + .unwrap_or_else(|| { + view.callconv + .arg_register_name(index) + .map(|reg| { + read_register_from_state( + initial_state, + reg, + view.callconv.arg_bits(), + ) + }) + .unwrap_or_else(|| SymValue::unknown(view.callconv.arg_bits())) + }) + }) + .collect::>(); + contexts.insert( + call_id, + CallTransformContext { + summary: view.summary.clone(), + callconv: view.callconv.clone(), + args, + }, + ); + } + + if let Some(dst) = op.dst() + && let Some(arg_index) = ssa_var_arg_index(dst) + && let Some(value_id) = func.graph().value_id_for_var(dst) + { + arg_state.insert(arg_index, value_id); + } + } + } + + contexts +} + +fn path_block_sequence(path: &ReversePath) -> Vec { + let mut blocks = vec![path.block_addr]; + let mut current = path.block_addr; + while let Some(predecessor) = path.phi_predecessors.get(¤t).copied() { + blocks.push(predecessor); + current = predecessor; + } + blocks.reverse(); + blocks +} + +fn and_all(_ctx: &Context, terms: &[Bool]) -> Bool { + match terms { + [] => Bool::from_bool(true), + [term] => term.clone(), + _ => { + let refs = terms.iter().collect::>(); + Bool::and(&refs) + } + } +} + +fn or_all(_ctx: &Context, terms: &[Bool]) -> Bool { + match terms { + [] => Bool::from_bool(false), + [term] => term.clone(), + _ => { + let refs = terms.iter().collect::>(); + Bool::or(&refs) + } + } +} + +fn eval_const_var<'ctx>(var: &r2ssa::SSAVar) -> Result, EvalUnsupported> { + if let Some(hex) = var.name.strip_prefix("const:") { + u64::from_str_radix(hex, 16) + .map(|value| SymValue::concrete(value, var.size * 8)) + .map_err(|_| EvalUnsupported::Unsupported) + } else { + Err(EvalUnsupported::Unsupported) + } +} + +fn read_input_var<'ctx>(state: &SymState<'ctx>, var: &r2ssa::SSAVar) -> SymValue<'ctx> { + let key = var.display_name(); + let value = state.get_register_sized(&key, var.size * 8); + if value.is_unknown() && var.version == 0 { + let base = var.name.strip_prefix("reg:").unwrap_or(&var.name); + state.get_register_sized(&base.to_ascii_uppercase(), var.size * 8) + } else { + value + } +} + +fn value_to_bool(ctx: &Context, value: &SymValue<'_>) -> Bool { + value.to_bv(ctx).eq(BV::from_u64(0, value.bits())).not() +} + +fn build_summary_substitutions<'ctx>( + state: &SymState<'ctx>, + summary: &DerivedFunctionSummary<'ctx>, + call: &crate::sim::CallInfo<'ctx>, +) -> Vec<(BV, BV)> { + let mut substitutions = Vec::new(); + for (index, symbol) in &summary.arg_symbols { + let Some(actual) = call.args.get(*index) else { + continue; + }; + substitutions.push((symbol.to_bv(state.context()), actual.to_bv(state.context()))); + } + for input in &summary.memory_inputs { + let Some(base) = call.args.get(input.arg_index) else { + continue; + }; + substitutions.push(( + input.symbol.to_bv(state.context()), + state.mem_read(base, input.size).to_bv(state.context()), + )); + } + substitutions +} + +fn build_call_substitutions<'ctx>( + state: &SymState<'ctx>, + call_ctx: &CallTransformContext<'ctx>, +) -> Vec<(BV, BV)> { + let mut substitutions = Vec::new(); + for (index, symbol) in &call_ctx.summary.arg_symbols { + let Some(actual) = call_ctx.args.get(*index) else { + continue; + }; + substitutions.push((symbol.to_bv(state.context()), actual.to_bv(state.context()))); + } + for input in &call_ctx.summary.memory_inputs { + let Some(base) = call_ctx.args.get(input.arg_index) else { + continue; + }; + substitutions.push(( + input.symbol.to_bv(state.context()), + state.mem_read(base, input.size).to_bv(state.context()), + )); + } + substitutions +} + +fn summary_return_value<'ctx>( + state: &SymState<'ctx>, + call_ctx: &CallTransformContext<'ctx>, +) -> Result<(SymValue<'ctx>, Option), EvalUnsupported> { + if call_ctx.summary.cases.is_empty() { + return Err(EvalUnsupported::Unsupported); + } + let substitutions = build_call_substitutions(state, call_ctx); + let mut merged = SymValue::unknown(call_ctx.callconv.ret_bits()); + let mut matched = false; + let mut guards = Vec::new(); + for case in call_ctx.summary.cases.iter().rev() { + let Some(return_value) = &case.return_value else { + continue; + }; + let guard = substitute_bool(&case.guard, &substitutions); + let value = substitute_value(state.context(), return_value, &substitutions); + merged = ite_value(state.context(), &guard, &value, &merged); + guards.push(guard); + matched = true; + } + matched + .then(|| { + let coverage = matches!( + call_ctx.summary.completion, + crate::sim::DerivedSummaryCompletion::Exact + ) + .then(|| or_all(state.context(), &guards)); + (merged, coverage) + }) + .ok_or(EvalUnsupported::Unsupported) +} + +fn summary_memory_locations<'ctx>( + call_ctx: &CallTransformContext<'ctx>, + addr: &SymValue<'ctx>, +) -> Vec { + let Some(concrete_addr) = addr.as_concrete() else { + return Vec::new(); + }; + let pointer_args = summary_pointer_arg_indices(call_ctx); + let mut locations = Vec::new(); + for (index, base) in call_ctx.args.iter().enumerate() { + if !pointer_args.is_empty() && !pointer_args.contains(&index) { + continue; + } + let Some(base_addr) = base.as_concrete() else { + continue; + }; + if let Some(offset) = signed_offset_between(base_addr, concrete_addr) { + locations.push(NormalizedMemoryLocation { + region: BackwardMemoryRegion::Argument { index }, + offset, + }); + } + } + locations +} + +fn summary_pointer_arg_indices<'ctx>(call_ctx: &CallTransformContext<'ctx>) -> BTreeSet { + let mut indices = BTreeSet::new(); + for input in &call_ctx.summary.memory_inputs { + indices.insert(input.arg_index); + } + for case in &call_ctx.summary.cases { + for write in &case.memory_writes { + indices.insert(write.arg_index); + } + } + indices +} + +fn group_normalized_locations( + locations: &[NormalizedMemoryLocation], +) -> Vec> { + let mut grouped = BTreeMap::>::new(); + for location in locations { + grouped + .entry(location.region.clone()) + .or_default() + .insert(location.offset); + } + grouped + .into_iter() + .map(|(region, offsets)| { + offsets + .into_iter() + .map(|offset| NormalizedMemoryLocation { + region: region.clone(), + offset, + }) + .collect() + }) + .collect() +} + +fn apply_delta_offsets( + base_locations: &[NormalizedMemoryLocation], + deltas: &[i64], + add: bool, +) -> Vec { + let mut merged = BTreeSet::new(); + for base in base_locations { + for delta in deltas { + let offset = if add { + base.offset.saturating_add(*delta) + } else { + base.offset.saturating_sub(*delta) + }; + merged.insert((base.region.clone(), offset)); + } + } + merged + .into_iter() + .map(|(region, offset)| NormalizedMemoryLocation { region, offset }) + .collect() +} + +fn select_covering_write<'a, 'ctx>( + writes: &'a [crate::sim::DerivedMemoryWrite<'ctx>], + locations: &[NormalizedMemoryLocation], + size: u32, +) -> Option<( + NormalizedMemoryLocation, + &'a crate::sim::DerivedMemoryWrite<'ctx>, +)> { + select_covering_writes(writes, locations, size) + .into_iter() + .min_by_key(|(location, write)| (write.size, write.offset, location.offset)) +} + +fn select_covering_writes<'a, 'ctx>( + writes: &'a [crate::sim::DerivedMemoryWrite<'ctx>], + locations: &[NormalizedMemoryLocation], + size: u32, +) -> Vec<( + NormalizedMemoryLocation, + &'a crate::sim::DerivedMemoryWrite<'ctx>, +)> { + writes + .iter() + .flat_map(|write| { + locations.iter().filter_map(move |location| { + (matches!( + location.region, + BackwardMemoryRegion::Argument { index } if index == write.arg_index + ) && { + let write_start = write.offset as i128; + let write_end = write_start.saturating_add(write.size as i128); + let read_start = location.offset as i128; + let read_end = read_start.saturating_add(size as i128); + write_start <= read_start && write_end >= read_end + }) + .then_some((location.clone(), write)) + }) + }) + .collect() +} + +fn select_best_covering_write<'a, 'ctx>( + covering: Vec<( + NormalizedMemoryLocation, + &'a crate::sim::DerivedMemoryWrite<'ctx>, + )>, +) -> Option<( + NormalizedMemoryLocation, + &'a crate::sim::DerivedMemoryWrite<'ctx>, +)> { + covering + .into_iter() + .min_by_key(|(location, write)| (write.size, write.offset, location.offset)) +} + +fn covering_writes_are_ambiguous<'a, 'ctx>( + covering: &[( + NormalizedMemoryLocation, + &'a crate::sim::DerivedMemoryWrite<'ctx>, + )], +) -> bool { + let distinct = covering + .iter() + .filter_map(|(location, write)| match location.region { + BackwardMemoryRegion::Argument { index } => { + Some((index, location.offset, write.arg_index)) + } + BackwardMemoryRegion::Region(_) => None, + }) + .collect::>(); + distinct.len() > 1 +} + +fn slice_write_value<'ctx>( + ctx: &'ctx Context, + write: &crate::sim::DerivedMemoryWrite<'ctx>, + offset: i64, + size: u32, +) -> SymValue<'ctx> { + if write.offset == offset && write.size == size { + return write.value.clone(); + } + let relative = offset.saturating_sub(write.offset) as u64; + let low_bit = (relative * 8) as u32; + let high_bit = low_bit + (size * 8) - 1; + adjust_bits(ctx, write.value.clone(), write.size * 8).extract(ctx, high_bit, low_bit) +} + +fn signed_offset_between(base: u64, addr: u64) -> Option { + if addr >= base { + i64::try_from(addr - base).ok() + } else { + i64::try_from(base - addr).ok().map(|delta| -delta) + } +} + +fn is_specific_memory_location(location: &NormalizedMemoryLocation) -> bool { + !matches!( + &location.region, + BackwardMemoryRegion::Region(BackwardRegionRef { + kind: MemoryRegionKind::EscapedUnknown, + .. + }) + ) +} + +fn ssa_var_arg_index(var: &r2ssa::SSAVar) -> Option { + let display = var.display_name(); + if let Some((prefix, _)) = split_version(&display) + && let Some(index) = callconv_arg_index(prefix) + { + return Some(index); + } + callconv_arg_index(&display).or_else(|| callconv_arg_index(&var.name)) +} + +fn callconv_arg_index(name: &str) -> Option { + let upper = name.to_ascii_uppercase(); + match upper.as_str() { + "RDI" | "EDI" => Some(0), + "RSI" | "ESI" => Some(1), + "RDX" | "EDX" => Some(2), + "RCX" | "ECX" => Some(3), + "R8" | "R8D" => Some(4), + "R9" | "R9D" => Some(5), + _ => None, + } +} + +fn register_aliases(base: &str) -> Vec<&str> { + match base { + "RAX" => vec!["RAX", "EAX"], + "RDI" => vec!["RDI", "EDI"], + "RSI" => vec!["RSI", "ESI"], + "RDX" => vec!["RDX", "EDX"], + "RCX" => vec!["RCX", "ECX"], + "R8" => vec!["R8", "R8D"], + "R9" => vec!["R9", "R9D"], + _ => vec![base], + } +} + +fn read_register_from_state<'ctx>(state: &SymState<'ctx>, base: &str, bits: u32) -> SymValue<'ctx> { + for alias in register_aliases(base) { + if let Some(key) = find_register_key(state, alias) { + return state.get_register_sized(&key, bits); + } + } + SymValue::unknown(bits) +} + +fn add_signed_offset<'ctx>( + ctx: &'ctx Context, + base: &SymValue<'ctx>, + offset: i64, + bits: u32, +) -> SymValue<'ctx> { + if offset >= 0 { + base.add(ctx, &SymValue::concrete(offset as u64, bits)) + } else { + base.sub(ctx, &SymValue::concrete(offset.unsigned_abs(), bits)) + } +} + +fn find_register_key<'ctx>(state: &SymState<'ctx>, base: &str) -> Option { + let mut best: Option<(u32, String)> = None; + for key in state.registers().keys() { + if let Some((prefix, version)) = split_version(key) { + if prefix.eq_ignore_ascii_case(base) + && best + .as_ref() + .is_none_or(|(best_version, _)| version > *best_version) + { + best = Some((version, key.clone())); + } + } else if key.eq_ignore_ascii_case(base) { + return Some(key.clone()); + } + } + best.map(|(_, key)| key) +} + +fn split_version(name: &str) -> Option<(&str, u32)> { + let (prefix, suffix) = name.rsplit_once('_')?; + if suffix.is_empty() || !suffix.chars().all(|c| c.is_ascii_digit()) { + return None; + } + let version = suffix.parse().ok()?; + Some((prefix, version)) +} + +fn substitute_value<'ctx>( + ctx: &'ctx Context, + value: &SymValue<'ctx>, + substitutions: &[(BV, BV)], +) -> SymValue<'ctx> { + let pairs = substitutions + .iter() + .map(|(from, to)| (from, to)) + .collect::>(); + SymValue::symbolic_tainted( + value.to_bv(ctx).substitute(&pairs), + value.bits(), + value.get_taint(), + ) +} + +fn substitute_bool(ast: &Bool, substitutions: &[(BV, BV)]) -> Bool { + let pairs = substitutions + .iter() + .map(|(from, to)| (from, to)) + .collect::>(); + ast.substitute(&pairs) +} + +fn ite_value<'ctx>( + ctx: &'ctx Context, + guard: &Bool, + when_true: &SymValue<'ctx>, + when_false: &SymValue<'ctx>, +) -> SymValue<'ctx> { + let bits = when_true.bits().max(when_false.bits()); + let taint = when_true.get_taint() | when_false.get_taint(); + let true_bv = adjust_bits(ctx, when_true.clone(), bits).to_bv(ctx); + let false_bv = adjust_bits(ctx, when_false.clone(), bits).to_bv(ctx); + SymValue::symbolic_tainted(guard.ite(&true_bv, &false_bv), bits, taint) +} + +fn adjust_bits<'ctx>(ctx: &'ctx Context, value: SymValue<'ctx>, bits: u32) -> SymValue<'ctx> { + if value.bits() == bits { + return value; + } + if value.bits() < bits { + value.zero_extend(ctx, bits) + } else { + value.extract(ctx, bits - 1, 0) + } +} + +#[cfg(test)] +mod tests { + use r2il::{R2ILBlock, R2ILOp, SpaceId, Varnode}; + use z3::Context; + + use super::*; + + fn make_reg(offset: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Register, + offset, + size, + meta: None, + } + } + + fn make_const(value: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Const, + offset: value, + size, + meta: None, + } + } + + #[test] + fn compile_target_precondition_builds_guard_for_simple_branch() { + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![ + R2ILOp::IntEqual { + dst: make_reg(16, 1), + a: make_reg(56, 8), + b: make_const(0x1337, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(16, 1), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(0, 8), + src: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(0, 8), + src: make_const(1, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("reg:56_0", 64); + + let compiled = compile_target_precondition(&func, &state, 0x1010).expect("compiled"); + assert_eq!( + compiled.summary.precision, + BackwardConditionPrecision::Exact + ); + assert!(compiled.summary.simplified.contains("1337") || !compiled.summary.terms.is_empty()); + } +} diff --git a/crates/r2sym/src/lib.rs b/crates/r2sym/src/lib.rs index 91099cb..db9b87e 100644 --- a/crates/r2sym/src/lib.rs +++ b/crates/r2sym/src/lib.rs @@ -33,25 +33,56 @@ //! } //! ``` +pub mod backward; pub mod executor; pub mod memory; pub mod path; +pub mod query; pub mod r2api; +pub mod replay; pub mod runtime; +pub mod semantics; pub mod sim; pub mod solver; pub mod spec; pub mod state; pub mod value; +pub use backward::{ + BackwardConditionPrecision, BackwardConditionSummary, BackwardMemoryCondition, + BackwardMemoryRegion, BackwardRegionRef, CompiledBackwardCondition, + compile_derived_summary_return_postcondition, compile_target_precondition, +}; pub use executor::{CallHookResult, SymExecutor}; -pub use memory::SymMemory; +pub use memory::{ + MemoryRegionId, MemoryRegionKind, RegionPointer, ResolvedPointerSet, SymMemory, + SymbolicMemoryRegionDef, +}; pub use path::{ExploreConfig, PathExplorer, PathResult, SolvedPath}; +pub use query::{ + PathConditionResult, PathConditionSummary, PathConditionTerm, QueryCompletion, QueryMode, + ReachabilityResult, ReachabilityStatus, SolveResult, SolveStatus, SymQueryConfig, + SymbolicConditionSet, SymbolicFunctionSummary, +}; pub use r2api::{R2Api, R2Error}; -pub use runtime::seed_default_state_for_arch; +pub use replay::{ + ReplayMemoryOverlay, ReplayMemoryWindow, ReplayRegisterOverlay, ReplayRegisterValue, + ReplaySeed, apply_replay_seed_to_state, seed_replay_state_for_arch, +}; +pub use runtime::{seed_default_state_for_arch, seed_memory_regions_for_arch}; +pub use semantics::{ + CompiledFunctionSemantics, CompiledSemanticArtifact, CompiledSemanticMode, + InterpreterDispatchSummary, InterpreterKind, ResidualReason, SemanticCapability, SemanticMode, + SliceClass, SymbolicBranchFact, SymbolicFunctionFactDiagnostics, SymbolicFunctionFacts, + SymbolicReachabilityStatus, VmStateUpdate, VmStepSummary, VmTransferArm, VmValueExpr, + collect_symbolic_function_facts, collect_symbolic_function_facts_with_scope, + compile_function_semantics_with_scope, compile_semantic_artifact_with_scope, stable_scope_hash, +}; pub use sim::{ - CallConv, CallInfo, FunctionSummary, SummaryEffect, SummaryInstallStats, SummaryProfile, - SummaryRegistry, + CallConv, CallInfo, DerivedFunctionSummary, DerivedSummaryCase, DerivedSummaryCompletion, + DerivedSummaryDiagnostics, DerivedSummaryInput, DerivedSummarySet, FunctionSummary, + PreparedFunctionScope, ScopedPreparedFunction, SummaryEffect, SummaryInstallStats, + SummaryProfile, SummaryRegistry, }; pub use solver::{SatResult, SolverStats, SymModel, SymSolver}; pub use spec::{ diff --git a/crates/r2sym/src/memory.rs b/crates/r2sym/src/memory.rs index c61d453..fae61fc 100644 --- a/crates/r2sym/src/memory.rs +++ b/crates/r2sym/src/memory.rs @@ -1,239 +1,587 @@ //! Symbolic memory model for symbolic execution. //! -//! This module provides a sparse memory model that can handle both -//! concrete and symbolic addresses and values. +//! The memory core is region-based: concrete and symbolic bytes live under +//! explicit stack/global/input/heap/replay/unknown regions. The public API +//! remains address-shaped for compatibility with the executor and summaries. -use std::collections::{BTreeSet, HashMap}; +use std::collections::{BTreeMap, BTreeSet}; -use z3::ast::{BV, Bool}; +use serde::{Deserialize, Serialize}; +use z3::ast::{Ast, BV, Bool}; use z3::{Context, SatResult, Solver}; use crate::value::SymValue; -/// A symbolic memory model. -/// -/// Memory is modeled as a sparse map from addresses to byte values. -/// For symbolic addresses, we concretize a bounded set of targets and -/// build an ITE chain to select the correct value. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct MemoryRegionId(pub u32); + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub enum MemoryRegionKind { + Stack, + Global, + Input, + Heap, + Replay, + EscapedUnknown, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RegionPointer { + pub region_id: MemoryRegionId, + pub offset: u64, + pub ptr_bits: u32, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SymbolicMemoryRegionDef { + pub id: MemoryRegionId, + pub kind: MemoryRegionKind, + pub name: String, + pub base_addr: Option, + pub extent: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ResolvedPointerSet { + pub pointers: Vec, + pub truncated: bool, +} + +#[derive(Clone)] +enum RegionWriteTarget<'ctx> { + Offset(u64), + Address(SymValue<'ctx>), +} + +#[derive(Clone)] +struct RegionWrite<'ctx> { + target: RegionWriteTarget<'ctx>, + value: SymValue<'ctx>, + size: u32, +} + +#[derive(Clone)] +struct MemoryRegion<'ctx> { + def: SymbolicMemoryRegionDef, + concrete: BTreeMap, + symbolic_writes: Vec>, +} + +impl<'ctx> MemoryRegion<'ctx> { + fn new( + id: MemoryRegionId, + kind: MemoryRegionKind, + name: impl Into, + base_addr: Option, + extent: Option, + ) -> Self { + Self { + def: SymbolicMemoryRegionDef { + id, + kind, + name: name.into(), + base_addr, + extent, + }, + concrete: BTreeMap::new(), + symbolic_writes: Vec::new(), + } + } + + fn contains_absolute_range(&self, addr: u64, size: u32) -> bool { + let Some(base_addr) = self.def.base_addr else { + return false; + }; + let Some(offset) = addr.checked_sub(base_addr) else { + return false; + }; + self.contains_offset_range(offset, size) + } + + fn absolute_to_offset(&self, addr: u64) -> Option { + let base = self.def.base_addr?; + addr.checked_sub(base) + } + + fn contains_offset_range(&self, offset: u64, size: u32) -> bool { + match self.def.extent { + Some(extent) => offset + .checked_add(size as u64) + .is_some_and(|end| end <= extent), + None => true, + } + } + + fn ensure_extent_covers(&mut self, offset: u64, size: u32) { + let needed = offset.saturating_add(size as u64); + match self.def.extent { + Some(extent) if extent >= needed => {} + _ => self.def.extent = Some(needed), + } + } + + fn merge_offsets(&self) -> BTreeSet { + let mut offsets = BTreeSet::new(); + offsets.extend(self.concrete.keys().copied()); + for write in &self.symbolic_writes { + if let RegionWriteTarget::Offset(base) = write.target { + for index in 0..write.size { + offsets.insert(base.wrapping_add(index as u64)); + } + } + } + offsets + } + + fn unresolved_symbolic_writes(&self) -> impl Iterator> { + self.symbolic_writes + .iter() + .filter(|write| matches!(write.target, RegionWriteTarget::Address(_))) + } + + fn read_offset( + &self, + ctx: &'ctx Context, + offset: u64, + size: u32, + default_symbolic: bool, + default_name: &str, + ) -> SymValue<'ctx> { + for write in self.symbolic_writes.iter().rev() { + let RegionWriteTarget::Offset(base) = write.target else { + continue; + }; + let write_end = base.checked_add(write.size as u64); + let read_end = offset.checked_add(size as u64); + if let (Some(write_end), Some(read_end)) = (write_end, read_end) + && base <= offset + && write_end >= read_end + { + let relative = offset - base; + if relative == 0 && write.size == size { + return adjust_bits(ctx, &write.value, size * 8); + } + let low_bit = (relative * 8) as u32; + let high_bit = low_bit + (size * 8) - 1; + return adjust_bits(ctx, &write.value, write.size * 8) + .extract(ctx, high_bit, low_bit); + } + } + + let mut value = 0u64; + let mut all_concrete = true; + for index in 0..size { + let byte_offset = offset.wrapping_add(index as u64); + if let Some(byte) = self.concrete.get(&byte_offset) { + value |= (*byte as u64) << (index * 8); + } else { + all_concrete = false; + break; + } + } + + if all_concrete { + return SymValue::concrete(value, size * 8); + } + + if default_symbolic { + SymValue::new_symbolic(ctx, default_name, size * 8) + } else { + SymValue::concrete(0, size * 8) + } + } + + fn write_offset(&mut self, ctx: &'ctx Context, offset: u64, value: &SymValue<'ctx>, size: u32) { + let bits = size * 8; + let value = adjust_bits(ctx, value, bits); + self.ensure_extent_covers(offset, size); + if let Some(concrete_value) = value.as_concrete() { + for index in 0..size { + let byte_offset = offset.wrapping_add(index as u64); + let byte_value = ((concrete_value >> (index * 8)) & 0xff) as u8; + self.concrete.insert(byte_offset, byte_value); + } + } + self.symbolic_writes.push(RegionWrite { + target: RegionWriteTarget::Offset(offset), + value, + size, + }); + } + + fn push_unresolved_write(&mut self, addr: SymValue<'ctx>, value: SymValue<'ctx>, size: u32) { + self.symbolic_writes.push(RegionWrite { + target: RegionWriteTarget::Address(addr), + value, + size, + }); + } +} + +/// A symbolic memory model backed by explicit regions. pub struct SymMemory<'ctx> { - /// The Z3 context. ctx: &'ctx Context, - /// Concrete memory (address -> byte value). - concrete: HashMap, - /// Symbolic memory regions (for symbolic writes). - /// Each entry represents a write: (address, value, size). - symbolic_writes: Vec<(SymValue<'ctx>, SymValue<'ctx>, u32)>, - /// Default value for uninitialized memory (0 or symbolic). + regions: BTreeMap>, default_symbolic: bool, - /// Maximum number of symbolic address targets to enumerate. max_symbolic_targets: usize, + next_region_id: u32, + escaped_unknown_region: MemoryRegionId, } impl<'ctx> SymMemory<'ctx> { const DEFAULT_MAX_SYMBOLIC_TARGETS: usize = 256; - /// Create a new empty memory. pub fn new(ctx: &'ctx Context) -> Self { - Self { - ctx, - concrete: HashMap::new(), - symbolic_writes: Vec::new(), - default_symbolic: false, - max_symbolic_targets: Self::DEFAULT_MAX_SYMBOLIC_TARGETS, - } + Self::new_with_default(ctx, false) } - /// Create memory with symbolic default values. pub fn new_symbolic(ctx: &'ctx Context) -> Self { + Self::new_with_default(ctx, true) + } + + fn new_with_default(ctx: &'ctx Context, default_symbolic: bool) -> Self { + let escaped_unknown_region = MemoryRegionId(0); + let mut regions = BTreeMap::new(); + regions.insert( + escaped_unknown_region, + MemoryRegion::new( + escaped_unknown_region, + MemoryRegionKind::EscapedUnknown, + "escaped_unknown", + None, + None, + ), + ); + Self { ctx, - concrete: HashMap::new(), - symbolic_writes: Vec::new(), - default_symbolic: true, + regions, + default_symbolic, max_symbolic_targets: Self::DEFAULT_MAX_SYMBOLIC_TARGETS, + next_region_id: 1, + escaped_unknown_region, } } - /// Set the maximum number of symbolic targets to enumerate. pub fn set_max_symbolic_targets(&mut self, max: usize) { self.max_symbolic_targets = max; } - pub(crate) fn merge_addrs(&self) -> Vec { - let mut addrs = BTreeSet::new(); - addrs.extend(self.concrete.keys().copied()); - for (addr, _value, size) in &self.symbolic_writes { - if let Some(base) = addr.as_concrete() { - for offset in 0..*size { - addrs.insert(base.wrapping_add(offset as u64)); - } - } + pub fn escaped_unknown_region(&self) -> MemoryRegionId { + self.escaped_unknown_region + } + + pub fn define_region( + &mut self, + kind: MemoryRegionKind, + name: impl Into, + base_addr: Option, + extent: Option, + ) -> MemoryRegionId { + let name = name.into(); + if let Some(existing) = self.regions.values().find(|region| { + region.def.kind == kind + && region.def.name == name + && region.def.base_addr == base_addr + && region.def.extent == extent + }) { + return existing.def.id; } - addrs.into_iter().collect() + + let id = MemoryRegionId(self.next_region_id); + self.next_region_id += 1; + self.regions + .insert(id, MemoryRegion::new(id, kind, name, base_addr, extent)); + id } - pub(crate) fn symbolic_writes(&self) -> &[(SymValue<'ctx>, SymValue<'ctx>, u32)] { - &self.symbolic_writes + pub fn region_defs(&self) -> Vec { + self.regions + .values() + .map(|region| region.def.clone()) + .collect() } - pub(crate) fn push_symbolic_write( - &mut self, - addr: SymValue<'ctx>, - value: SymValue<'ctx>, - size: u32, - ) { - self.symbolic_writes.push((addr, value, size)); + pub fn region_def(&self, region_id: MemoryRegionId) -> Option<&SymbolicMemoryRegionDef> { + self.regions.get(®ion_id).map(|region| ®ion.def) } - /// Read a value from memory. - /// - /// # Arguments - /// * `addr` - The address to read from - /// * `size` - The size in bytes to read - pub fn read(&self, addr: &SymValue<'ctx>, size: u32) -> SymValue<'ctx> { - self.read_with_constraints(addr, size, &[]) + pub fn seed_region_bytes(&mut self, region_id: MemoryRegionId, offset: u64, bytes: &[u8]) { + let Some(region) = self.regions.get_mut(®ion_id) else { + return; + }; + region.ensure_extent_covers(offset, bytes.len() as u32); + for (index, byte) in bytes.iter().copied().enumerate() { + region.concrete.insert(offset + index as u64, byte); + } } - /// Read a value from memory with path constraints. - pub fn read_with_constraints( + pub fn allocate_heap_region( + &mut self, + name: impl Into, + size: u64, + ) -> (MemoryRegionId, u64) { + let id = MemoryRegionId(self.next_region_id); + let base_addr = 0x6000_0000u64 + (id.0 as u64) * 0x10000; + let region_id = self.define_region( + MemoryRegionKind::Heap, + name.into(), + Some(base_addr), + Some(size), + ); + (region_id, base_addr) + } + + pub fn resolve_pointer( &self, addr: &SymValue<'ctx>, size: u32, constraints: &[Bool], - ) -> SymValue<'ctx> { - if let Some(concrete_addr) = addr.as_concrete() { - return self.read_concrete(concrete_addr, size); + ) -> ResolvedPointerSet { + if let Some(concrete_addr) = addr.as_concrete() + && let Some(pointer) = self.resolve_concrete_pointer(concrete_addr, addr.bits(), size) + { + return ResolvedPointerSet { + pointers: vec![pointer], + truncated: false, + }; } - let bits = size * 8; - let addr_taint = addr.get_taint(); - let mut result = if self.default_symbolic { - let ast = BV::fresh_const("mem_sym", bits); - SymValue::symbolic_tainted(ast, bits, addr_taint) - } else { - SymValue::concrete_tainted(0, bits, addr_taint) - }; - - let (targets, _truncated) = self.enumerate_symbolic_addresses(addr, constraints); - if targets.is_empty() { - return result; + let (targets, truncated) = self.enumerate_symbolic_addresses(addr, constraints); + let mut seen = BTreeSet::new(); + let mut pointers = Vec::new(); + for target in targets { + if let Some(pointer) = self.resolve_concrete_pointer(target, addr.bits(), size) + && seen.insert((pointer.region_id, pointer.offset)) + { + pointers.push(pointer); + } } - let addr_bv = addr.to_bv(self.ctx); - for target in targets { - let value = self.read_concrete(target, size).with_taint(addr_taint); - let cond = addr_bv.eq(BV::from_u64(target, addr.bits())); - let taint = value.get_taint() | result.get_taint(); - let merged = SymValue::symbolic_tainted( - cond.ite(&value.to_bv(self.ctx), &result.to_bv(self.ctx)), - bits, - taint, - ); - result = merged; + ResolvedPointerSet { + pointers, + truncated, } + } - result + pub fn region_read(&self, pointer: &RegionPointer, size: u32) -> SymValue<'ctx> { + self.read_region_pointer(pointer, size) } - fn enumerate_symbolic_addresses( - &self, - addr: &SymValue<'ctx>, - constraints: &[Bool], - ) -> (Vec, bool) { - if self.max_symbolic_targets == 0 { - return (Vec::new(), true); - } + pub fn region_write(&mut self, pointer: &RegionPointer, value: &SymValue<'ctx>, size: u32) { + self.write_region_pointer(pointer, value, size); + } - let solver = Solver::new(); - for constraint in constraints { - solver.assert(constraint); + pub(crate) fn semantic_fingerprint(&self) -> String { + let region_repr = self + .regions + .values() + .map(|region| { + let concrete = region + .concrete + .iter() + .map(|(offset, byte)| format!("{offset:x}:{byte:02x}")) + .collect::>() + .join(","); + let symbolic = region + .symbolic_writes + .iter() + .map(|write| { + let target = match &write.target { + RegionWriteTarget::Offset(offset) => format!("off:{offset:x}"), + RegionWriteTarget::Address(addr) => { + format!("addr:{}", addr.to_bv(self.ctx).simplify()) + } + }; + format!( + "{target}=>{}:{}", + write.value.to_bv(self.ctx).simplify(), + write.size + ) + }) + .collect::>() + .join("|"); + format!( + "{}:{:?}:{}:{:?}:{:?}:c[{}]:s[{}]", + region.def.id.0, + region.def.kind, + region.def.name, + region.def.base_addr, + region.def.extent, + concrete, + symbolic + ) + }) + .collect::>() + .join(";"); + + format!( + "default_symbolic={};max_targets={};regions=[{}]", + self.default_symbolic, self.max_symbolic_targets, region_repr + ) + } + + pub(crate) fn merge_with( + &self, + other: &Self, + constraints_self: &[Bool], + constraints_other: &[Bool], + cond_other: &Bool, + ) -> Self { + let mut merged = self.fork(); + let mut region_map = BTreeMap::new(); + + for region in other.regions.values() { + let target_id = if let Some(existing) = merged.regions.values().find(|existing| { + existing.def.kind == region.def.kind + && existing.def.name == region.def.name + && existing.def.base_addr == region.def.base_addr + && existing.def.extent == region.def.extent + }) { + existing.def.id + } else { + merged.define_region( + region.def.kind.clone(), + region.def.name.clone(), + region.def.base_addr, + region.def.extent, + ) + }; + region_map.insert(region.def.id, target_id); } - let addr_bv = addr.to_bv(self.ctx); - let mut targets = Vec::new(); - let mut truncated = false; + let all_region_ids = merged.regions.keys().copied().collect::>(); + for region_id in all_region_ids { + let merged_def = match merged.region_def(region_id).cloned() { + Some(def) => def, + None => continue, + }; + let other_region = region_map + .iter() + .find_map(|(other_id, mapped)| { + (*mapped == region_id).then(|| other.regions.get(other_id)) + }) + .flatten(); + let self_region = self + .regions + .values() + .find(|region| region.def == merged_def); + let mut offsets = BTreeSet::new(); + if let Some(region) = self_region { + offsets.extend(region.merge_offsets()); + } + if let Some(region) = other_region { + offsets.extend(region.merge_offsets()); + } - while targets.len() < self.max_symbolic_targets { - if solver.check() != SatResult::Sat { - break; + for offset in offsets { + let pointer = RegionPointer { + region_id, + offset, + ptr_bits: 64, + }; + let self_value = self_region + .map(|region| { + region.read_offset( + self.ctx, + offset, + 1, + self.default_symbolic, + &format!("merge_self_{}_{}", region_id.0, offset), + ) + }) + .unwrap_or_else(|| self.default_byte(&pointer, 1)); + let other_value = other_region + .map(|region| { + region.read_offset( + self.ctx, + offset, + 1, + other.default_symbolic, + &format!("merge_other_{}_{}", region_id.0, offset), + ) + }) + .unwrap_or_else(|| other.default_byte(&pointer, 1)); + let merged_value = merge_values(self.ctx, cond_other, &self_value, &other_value); + merged.region_write(&pointer, &merged_value, 1); } - let model = match solver.get_model() { - Some(model) => model, - None => break, + } + + for region in other.regions.values() { + let Some(mapped_region) = region_map.get(®ion.def.id).copied() else { + continue; }; - let Some(value) = model.eval(&addr_bv, true).and_then(|v| v.as_u64()) else { - truncated = true; - break; + let Some(target) = merged.regions.get_mut(&mapped_region) else { + continue; }; - - targets.push(value); - let neq = addr_bv.eq(BV::from_u64(value, addr.bits())).not(); - solver.assert(&neq); + for write in region.unresolved_symbolic_writes() { + let RegionWriteTarget::Address(addr) = &write.target else { + continue; + }; + target.push_unresolved_write(addr.clone(), write.value.clone(), write.size); + } } - if targets.len() == self.max_symbolic_targets && solver.check() == SatResult::Sat { - truncated = true; - } + let _ = constraints_self; + let _ = constraints_other; + merged + } - (targets, truncated) + pub fn read(&self, addr: &SymValue<'ctx>, size: u32) -> SymValue<'ctx> { + self.read_with_constraints(addr, size, &[]) } - fn read_concrete(&self, concrete_addr: u64, size: u32) -> SymValue<'ctx> { - // Check symbolic writes for this address (most recent first). - for (write_addr, write_val, write_size) in self.symbolic_writes.iter().rev() { - if let Some(wa) = write_addr.as_concrete() { - let write_end = wa.checked_add(*write_size as u64); - let read_end = concrete_addr.checked_add(size as u64); - if let (Some(write_end), Some(read_end)) = (write_end, read_end) - && wa <= concrete_addr - && write_end >= read_end - { - let offset = concrete_addr - wa; - if offset == 0 && *write_size == size { - return write_val.clone(); - } - let low_bit = (offset * 8) as u32; - let high_bit = low_bit + (size * 8) - 1; - return write_val.extract(self.ctx, high_bit, low_bit); - } - } + pub fn read_with_constraints( + &self, + addr: &SymValue<'ctx>, + size: u32, + constraints: &[Bool], + ) -> SymValue<'ctx> { + let addr_taint = addr.get_taint(); + if let Some(concrete_addr) = addr.as_concrete() { + return self + .read_concrete(concrete_addr, addr.bits(), size) + .with_taint(addr_taint); } - let mut all_concrete = true; - let mut value: u64 = 0; + let bits = size * 8; + let mut result = if self.default_symbolic { + SymValue::symbolic_tainted(BV::fresh_const("mem_sym", bits), bits, addr_taint) + } else { + SymValue::concrete_tainted(0, bits, addr_taint) + }; - for i in 0..size { - let byte_addr = concrete_addr.wrapping_add(i as u64); - if let Some(&byte) = self.concrete.get(&byte_addr) { - value |= (byte as u64) << (i * 8); - } else { - all_concrete = false; - break; - } + let resolved = self.resolve_pointer(addr, size, constraints); + if resolved.pointers.is_empty() { + return result; } - if all_concrete { - return SymValue::concrete(value, size * 8); + let addr_bv = addr.to_bv(self.ctx); + for pointer in resolved.pointers { + let Some(def) = self.region_def(pointer.region_id) else { + continue; + }; + let Some(base_addr) = def.base_addr else { + continue; + }; + let target_addr = base_addr.wrapping_add(pointer.offset); + let value = self.region_read(&pointer, size).with_taint(addr_taint); + let cond = addr_bv.eq(BV::from_u64(target_addr, addr.bits())); + let taint = value.get_taint() | result.get_taint(); + result = SymValue::symbolic_tainted( + cond.ite(&value.to_bv(self.ctx), &result.to_bv(self.ctx)), + bits, + taint, + ); } - if self.default_symbolic { - SymValue::new_symbolic(self.ctx, &format!("mem_{:x}", concrete_addr), size * 8) - } else { - SymValue::concrete(0, size * 8) - } + result } - /// Write a value to memory. - /// - /// # Arguments - /// * `addr` - The address to write to - /// * `value` - The value to write - /// * `size` - The size in bytes to write pub fn write(&mut self, addr: &SymValue<'ctx>, value: &SymValue<'ctx>, size: u32) { self.write_with_constraints(addr, value, size, &[]); } - /// Write a value to memory with path constraints. pub fn write_with_constraints( &mut self, addr: &SymValue<'ctx>, @@ -244,102 +592,262 @@ impl<'ctx> SymMemory<'ctx> { let bits = size * 8; let value = adjust_bits(self.ctx, value, bits); - if let Some(concrete_addr) = addr.as_concrete() { - if let Some(concrete_value) = value.as_concrete() { - for i in 0..size { - let byte_addr = concrete_addr.wrapping_add(i as u64); - let byte_value = ((concrete_value >> (i * 8)) & 0xFF) as u8; - self.concrete.insert(byte_addr, byte_value); - } - } - self.symbolic_writes - .push((addr.clone(), value.clone(), size)); + if let Some(concrete_addr) = addr.as_concrete() + && let Some(pointer) = self.resolve_concrete_pointer(concrete_addr, addr.bits(), size) + { + self.write_region_pointer(&pointer, &value, size); return; } - let (targets, truncated) = self.enumerate_symbolic_addresses(addr, constraints); - if targets.is_empty() { - self.symbolic_writes - .push((addr.clone(), value.clone(), size)); + let resolved = self.resolve_pointer(addr, size, constraints); + if resolved.pointers.is_empty() { + if let Some(region) = self.regions.get_mut(&self.escaped_unknown_region) { + region.push_unresolved_write(addr.clone(), value, size); + } return; } let addr_bv = addr.to_bv(self.ctx); - for target in targets { - let existing = self.read_concrete(target, size); - let existing = adjust_bits(self.ctx, &existing, bits); - let cond = addr_bv.eq(BV::from_u64(target, addr.bits())); + for pointer in resolved.pointers { + let existing = self.read_region_pointer(&pointer, size); + let Some(def) = self.region_def(pointer.region_id) else { + continue; + }; + let Some(base_addr) = def.base_addr else { + continue; + }; + let concrete_target = base_addr.wrapping_add(pointer.offset); + let cond = addr_bv.eq(BV::from_u64(concrete_target, addr.bits())); let taint = existing.get_taint() | value.get_taint() | addr.get_taint(); let merged = SymValue::symbolic_tainted( cond.ite(&value.to_bv(self.ctx), &existing.to_bv(self.ctx)), bits, taint, ); - let target_addr = SymValue::concrete(target, addr.bits()); - self.symbolic_writes.push((target_addr, merged, size)); + self.write_region_pointer(&pointer, &merged, size); } - if truncated { - self.symbolic_writes - .push((addr.clone(), value.clone(), size)); + if resolved.truncated + && let Some(region) = self.regions.get_mut(&self.escaped_unknown_region) + { + region.push_unresolved_write(addr.clone(), value, size); } } - /// Write concrete bytes to memory. pub fn write_bytes(&mut self, addr: u64, bytes: &[u8]) { - for (i, &byte) in bytes.iter().enumerate() { - self.concrete.insert(addr + i as u64, byte); + for (index, byte) in bytes.iter().copied().enumerate() { + let addr = SymValue::concrete(addr + index as u64, 64); + let value = SymValue::concrete(byte as u64, 8); + self.write(&addr, &value, 1); } } - /// Read concrete bytes from memory. pub fn read_bytes(&self, addr: u64, size: usize) -> Option> { let mut bytes = Vec::with_capacity(size); - for i in 0..size { - if let Some(&byte) = self.concrete.get(&(addr + i as u64)) { - bytes.push(byte); - } else { - return None; - } + for index in 0..size { + let pointer = self.resolve_concrete_pointer(addr + index as u64, 64, 1)?; + let region = self.regions.get(&pointer.region_id)?; + bytes.push(*region.concrete.get(&pointer.offset)?); } Some(bytes) } - /// Check if an address range is fully concrete. pub fn is_concrete_range(&self, addr: u64, size: u32) -> bool { - for i in 0..size { - if !self.concrete.contains_key(&(addr + i as u64)) { + for index in 0..size { + let Some(pointer) = self.resolve_concrete_pointer(addr + index as u64, 64, 1) else { + return false; + }; + let Some(region) = self.regions.get(&pointer.region_id) else { + return false; + }; + if !region.concrete.contains_key(&pointer.offset) { return false; } } true } - /// Get the number of concrete bytes stored. pub fn concrete_size(&self) -> usize { - self.concrete.len() + self.regions + .values() + .map(|region| region.concrete.len()) + .sum() } - /// Get the number of symbolic writes. pub fn symbolic_writes_count(&self) -> usize { - self.symbolic_writes.len() + self.regions + .values() + .map(|region| region.symbolic_writes.len()) + .sum() } - /// Clone the memory state (for forking). pub fn fork(&self) -> Self { Self { ctx: self.ctx, - concrete: self.concrete.clone(), - symbolic_writes: self.symbolic_writes.clone(), + regions: self.regions.clone(), default_symbolic: self.default_symbolic, max_symbolic_targets: self.max_symbolic_targets, + next_region_id: self.next_region_id, + escaped_unknown_region: self.escaped_unknown_region, } } - /// Clear all memory. pub fn clear(&mut self) { - self.concrete.clear(); - self.symbolic_writes.clear(); + for region in self.regions.values_mut() { + region.concrete.clear(); + region.symbolic_writes.clear(); + } + } + + fn resolve_concrete_pointer( + &self, + addr: u64, + ptr_bits: u32, + size: u32, + ) -> Option { + let region = self + .regions + .values() + .filter(|region| region.contains_absolute_range(addr, size)) + .min_by(|left, right| compare_region_specificity(left, right)) + .map(|region| region.def.id) + .unwrap_or(self.escaped_unknown_region); + + let offset = if region == self.escaped_unknown_region { + addr + } else { + self.regions.get(®ion)?.absolute_to_offset(addr)? + }; + + Some(RegionPointer { + region_id: region, + offset, + ptr_bits, + }) + } + + fn read_concrete(&self, addr: u64, ptr_bits: u32, size: u32) -> SymValue<'ctx> { + let Some(pointer) = self.resolve_concrete_pointer(addr, ptr_bits, size) else { + return SymValue::concrete(0, size * 8); + }; + self.read_region_pointer(&pointer, size) + } + + fn read_region_pointer(&self, pointer: &RegionPointer, size: u32) -> SymValue<'ctx> { + let Some(region) = self.regions.get(&pointer.region_id) else { + return self.default_byte(pointer, size); + }; + region.read_offset( + self.ctx, + pointer.offset, + size, + self.default_symbolic, + &format!("mem_{}_{}", pointer.region_id.0, pointer.offset), + ) + } + + fn write_region_pointer(&mut self, pointer: &RegionPointer, value: &SymValue<'ctx>, size: u32) { + let Some(region) = self.regions.get_mut(&pointer.region_id) else { + return; + }; + region.write_offset(self.ctx, pointer.offset, value, size); + } + + fn default_byte(&self, pointer: &RegionPointer, size: u32) -> SymValue<'ctx> { + if self.default_symbolic { + SymValue::new_symbolic( + self.ctx, + &format!("mem_{}_{}", pointer.region_id.0, pointer.offset), + size * 8, + ) + } else { + SymValue::concrete(0, size * 8) + } + } + + fn enumerate_symbolic_addresses( + &self, + addr: &SymValue<'ctx>, + constraints: &[Bool], + ) -> (Vec, bool) { + if self.max_symbolic_targets == 0 { + return (Vec::new(), true); + } + + let solver = Solver::new(); + for constraint in constraints { + solver.assert(constraint); + } + + let addr_bv = addr.to_bv(self.ctx); + let mut targets = Vec::new(); + let mut truncated = false; + + while targets.len() < self.max_symbolic_targets { + if solver.check() != SatResult::Sat { + break; + } + let Some(model) = solver.get_model() else { + break; + }; + let Some(value) = model.eval(&addr_bv, true).and_then(|value| value.as_u64()) else { + truncated = true; + break; + }; + + targets.push(value); + solver.assert(addr_bv.eq(BV::from_u64(value, addr.bits())).not()); + } + + if targets.len() == self.max_symbolic_targets && solver.check() == SatResult::Sat { + truncated = true; + } + + (targets, truncated) + } +} + +fn compare_region_specificity<'ctx>( + left: &MemoryRegion<'ctx>, + right: &MemoryRegion<'ctx>, +) -> std::cmp::Ordering { + let left_key = ( + left.def.extent.is_none(), + left.def.extent.unwrap_or(u64::MAX), + std::cmp::Reverse(left.def.base_addr.unwrap_or(0)), + left.def.id, + ); + let right_key = ( + right.def.extent.is_none(), + right.def.extent.unwrap_or(u64::MAX), + std::cmp::Reverse(right.def.base_addr.unwrap_or(0)), + right.def.id, + ); + left_key.cmp(&right_key) +} + +fn merge_values<'ctx>( + ctx: &'ctx Context, + cond: &Bool, + base: &SymValue<'ctx>, + incoming: &SymValue<'ctx>, +) -> SymValue<'ctx> { + let bits = base.bits().max(incoming.bits()); + let taint = base.get_taint() | incoming.get_taint(); + let base_bv = widen_value(ctx, base, bits); + let incoming_bv = widen_value(ctx, incoming, bits); + SymValue::symbolic_tainted(cond.ite(&incoming_bv, &base_bv), bits, taint) +} + +fn widen_value<'ctx>(ctx: &'ctx Context, value: &SymValue<'ctx>, bits: u32) -> BV { + let bv = value.to_bv(ctx); + let value_bits = value.bits(); + if value_bits == bits { + bv + } else if value_bits < bits { + bv.zero_ext(bits - value_bits) + } else { + bv.extract(bits - 1, 0) } } @@ -357,8 +865,9 @@ fn adjust_bits<'ctx>(ctx: &'ctx Context, value: &SymValue<'ctx>, bits: u32) -> S impl<'ctx> std::fmt::Debug for SymMemory<'ctx> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("SymMemory") - .field("concrete_bytes", &self.concrete.len()) - .field("symbolic_writes", &self.symbolic_writes.len()) + .field("regions", &self.regions.len()) + .field("concrete_bytes", &self.concrete_size()) + .field("symbolic_writes", &self.symbolic_writes_count()) .field("default_symbolic", &self.default_symbolic) .finish() } @@ -374,57 +883,57 @@ mod tests { fn test_concrete_read_write() { let ctx = Context::thread_local(); let mut mem = SymMemory::new(&ctx); + let globals = mem.define_region( + MemoryRegionKind::Global, + "globals", + Some(0x1000), + Some(0x100), + ); - let addr = SymValue::concrete(0x1000, 64); - let value = SymValue::concrete(0xDEADBEEF, 32); - - mem.write(&addr, &value, 4); - - let read_value = mem.read(&addr, 4); - assert_eq!(read_value.as_concrete(), Some(0xDEADBEEF)); - } - - #[test] - fn test_byte_access() { - let ctx = Context::thread_local(); - let mut mem = SymMemory::new(&ctx); - - mem.write_bytes(0x1000, &[0x11, 0x22, 0x33, 0x44]); - - let bytes = mem.read_bytes(0x1000, 4).unwrap(); - assert_eq!(bytes, vec![0x11, 0x22, 0x33, 0x44]); + let ptr = RegionPointer { + region_id: globals, + offset: 0, + ptr_bits: 64, + }; + mem.region_write(&ptr, &SymValue::concrete(0xdeadbeef, 32), 4); let addr = SymValue::concrete(0x1000, 64); - let value = mem.read(&addr, 4); - assert_eq!(value.as_concrete(), Some(0x44332211)); // Little-endian + assert_eq!(mem.read(&addr, 4).as_concrete(), Some(0xdeadbeef)); } #[test] - fn test_uninitialized_read() { + fn test_region_resolution_prefers_specific_input_over_stack() { let ctx = Context::thread_local(); - let mem = SymMemory::new(&ctx); - - let addr = SymValue::concrete(0x2000, 64); - let value = mem.read(&addr, 4); - // Default is concrete 0 - assert_eq!(value.as_concrete(), Some(0)); - } - - #[test] - fn test_symbolic_default() { - let ctx = Context::thread_local(); - let mem = SymMemory::new_symbolic(&ctx); + let mut mem = SymMemory::new(&ctx); + let _stack = mem.define_region( + MemoryRegionKind::Stack, + "stack_window", + Some(0x7fff_0000 - 0x8000), + Some(0x10000), + ); + let input = mem.define_region( + MemoryRegionKind::Input, + "stdin", + Some(0x7fff_1000), + Some(0x10), + ); - let addr = SymValue::concrete(0x2000, 64); - let value = mem.read(&addr, 4); - // Default is symbolic - assert!(value.is_symbolic()); + let resolved = mem.resolve_pointer(&SymValue::concrete(0x7fff_1004, 64), 1, &[]); + assert_eq!(resolved.pointers.len(), 1); + assert_eq!(resolved.pointers[0].region_id, input); + assert_eq!(resolved.pointers[0].offset, 4); } #[test] fn test_symbolic_address_write_then_read() { let ctx = Context::thread_local(); let mut mem = SymMemory::new(&ctx); + mem.define_region( + MemoryRegionKind::Global, + "globals", + Some(0x1000), + Some(0x2000), + ); let idx = SymValue::new_symbolic(&ctx, "idx", 64); let addr_bv = idx.to_bv(&ctx); @@ -432,41 +941,43 @@ mod tests { let eq2 = addr_bv.eq(BV::from_u64(0x2000, 64)); let constraint = eq1.clone() | eq2.clone(); - let value = SymValue::concrete(0xCAFEBABE, 32); + let value = SymValue::concrete(0xcafebabe, 32); mem.write_with_constraints(&idx, &value, 4, std::slice::from_ref(&constraint)); - let read_val = mem.read_with_constraints(&idx, 4, &[constraint]); - assert!(read_val.is_symbolic()); + let read_value = mem.read_with_constraints(&idx, 4, &[constraint]); + assert!(read_value.is_symbolic()); let solver = Solver::new(); solver.assert(&eq1); assert_eq!(solver.check(), SatResult::Sat); let model = solver.get_model().unwrap(); - let result_bv = read_val.to_bv(&ctx); - let val = model.eval(&result_bv, true).unwrap().as_u64().unwrap(); - assert_eq!(val, 0xCAFEBABE); + let value = model + .eval(&read_value.to_bv(&ctx), true) + .unwrap() + .as_u64() + .unwrap(); + assert_eq!(value, 0xcafebabe); let solver = Solver::new(); solver.assert(&eq2); assert_eq!(solver.check(), SatResult::Sat); let model = solver.get_model().unwrap(); - let val = model.eval(&result_bv, true).unwrap().as_u64().unwrap(); - assert_eq!(val, 0xCAFEBABE); + let value = model + .eval(&read_value.to_bv(&ctx), true) + .unwrap() + .as_u64() + .unwrap(); + assert_eq!(value, 0xcafebabe); } #[test] - fn test_merge_addrs_are_sorted() { + fn test_unknown_region_preserves_concrete_bytes() { let ctx = Context::thread_local(); let mut mem = SymMemory::new(&ctx); - - mem.write_bytes(0x1003, &[0xaa]); - mem.write_bytes(0x1000, &[0xbb]); - mem.push_symbolic_write( - SymValue::concrete(0x1001, 64), - SymValue::concrete(0x1122, 16), - 2, + mem.write_bytes(0x9000, &[0x11, 0x22, 0x33, 0x44]); + assert_eq!( + mem.read_bytes(0x9000, 4), + Some(vec![0x11, 0x22, 0x33, 0x44]) ); - - assert_eq!(mem.merge_addrs(), vec![0x1000, 0x1001, 0x1002, 0x1003]); } } diff --git a/crates/r2sym/src/path.rs b/crates/r2sym/src/path.rs index db7ecd9..c4f116a 100644 --- a/crates/r2sym/src/path.rs +++ b/crates/r2sym/src/path.rs @@ -4,12 +4,15 @@ //! during symbolic execution, including DFS, BFS, and coverage-guided. use std::collections::{BTreeMap, HashMap, HashSet, VecDeque}; +use std::rc::Rc; use std::time::{Duration, Instant}; use r2ssa::{BlockTerminator, SsaArtifact}; use z3::Context; +use crate::backward::DerivedCallSummaryView; use crate::executor::SymExecutor; +use crate::sim::{CallConv, DerivedFunctionSummary, evaluate_derived_summary_guidance}; use crate::solver::SymSolver; use crate::spec::ExplorationSpec; use crate::state::{ExitStatus, SymState}; @@ -31,6 +34,8 @@ pub struct ExploreConfig { pub prune_infeasible: bool, /// Whether to merge states at join points. pub merge_states: bool, + /// Whether to drop same-PC states that are already covered by weaker states. + pub subsumption_states: bool, } impl Default for ExploreConfig { @@ -43,6 +48,7 @@ impl Default for ExploreConfig { strategy: ExploreStrategy::Dfs, prune_infeasible: true, merge_states: false, + subsumption_states: false, } } } @@ -158,6 +164,10 @@ pub struct PathExplorer<'ctx> { config: ExploreConfig, /// Statistics. stats: ExploreStats, + /// Whether query helpers should prioritize states closer to their target. + target_guided_queries: bool, + /// Derived helper summaries registered for direct-call targets. + derived_call_summaries: HashMap>, } /// Statistics from path exploration. @@ -175,6 +185,51 @@ pub struct ExploreStats { pub max_depth_reached: usize, /// Total execution time. pub total_time: Duration, + /// Whether exploration stopped because the timeout budget expired. + pub timed_out: bool, + /// Whether exploration stopped because the max_states budget was exhausted. + pub max_states_exhausted: bool, + /// Number of queued states discarded because another state subsumed them. + pub states_subsumed: usize, + /// Number of implication checks attempted for same-PC subsumption. + pub subsumption_checks: usize, + /// Number of implication checks that found a covering state. + pub subsumption_hits: usize, + /// Number of target-guided queue pops that differed from the base strategy order. + pub target_guided_reorders: usize, + /// Number of states dropped because their PC cannot reach the target in the CFG. + pub target_pruned_cfg_unreachable: usize, + /// Number of states dropped because an exact helper summary had no satisfiable case. + pub target_pruned_summary_contradiction: usize, + /// Number of target-guided states whose ranking/pruning used derived summary metadata. + pub target_summary_rank_hits: usize, +} + +#[derive(Clone)] +struct RegisteredDerivedCallSummary<'ctx> { + summary: Rc>, + callconv: CallConv, +} + +#[derive(Clone, Default)] +struct BlockSummaryRank { + has_summary: bool, + has_exact_summary: bool, + min_case_count: usize, +} + +struct TargetGuidanceContext { + distances: HashMap, + reachable_blocks: HashSet, + call_targets_by_block: HashMap>, + block_summary_rank: HashMap, +} + +#[derive(Clone, Copy, Default)] +struct StateSummaryGuidance { + summary_hits: usize, + min_feasible_cases: usize, + contradictory: bool, } struct StateWorklist<'ctx> { @@ -205,6 +260,21 @@ impl<'ctx> StateWorklist<'ctx> { self.live_states += 1; } + fn state(&self, id: usize) -> Option<&SymState<'ctx>> { + self.slots.get(id)?.as_ref() + } + + fn same_pc_ids(&self, pc: u64) -> Vec { + self.same_pc + .get(&pc) + .map(|bucket| bucket.iter().copied().collect()) + .unwrap_or_default() + } + + fn remove_slot(&mut self, id: usize) -> Option> { + self.take_slot(id) + } + fn pop_next(&mut self) -> Option> { loop { let id = match self.strategy { @@ -227,6 +297,46 @@ impl<'ctx> StateWorklist<'ctx> { } } + fn pop_best_by_key_with_reorder( + &mut self, + mut key_fn: F, + ) -> Option<(SymState<'ctx>, bool)> + where + K: Ord, + F: FnMut(usize, &SymState<'ctx>) -> K, + { + loop { + let default_candidate = match self.strategy { + ExploreStrategy::Dfs => self.ready.back().copied(), + ExploreStrategy::Bfs => self.ready.front().copied(), + ExploreStrategy::Random => { + if self.ready.is_empty() { + None + } else if self.live_states.is_multiple_of(2) { + self.ready.front().copied() + } else { + self.ready.back().copied() + } + } + }; + let best = self + .ready + .iter() + .filter_map(|id| self.state(*id).map(|state| (*id, key_fn(*id, state)))) + .min_by(|a, b| a.1.cmp(&b.1)) + .map(|(id, _)| id)?; + let reordered = default_candidate.is_some_and(|candidate| candidate != best); + + if let Some(pos) = self.ready.iter().position(|queued| *queued == best) { + self.ready.remove(pos); + } + + if let Some(state) = self.take_slot(best) { + return Some((state, reordered)); + } + } + } + fn take_same_pc(&mut self, pc: u64) -> Option> { loop { let id = { @@ -571,6 +681,8 @@ impl<'ctx> PathExplorer<'ctx> { solver: SymSolver::new(ctx), config: ExploreConfig::default(), stats: ExploreStats::default(), + target_guided_queries: false, + derived_call_summaries: HashMap::new(), } } @@ -588,6 +700,8 @@ impl<'ctx> PathExplorer<'ctx> { solver, config, stats: ExploreStats::default(), + target_guided_queries: false, + derived_call_summaries: HashMap::new(), } } @@ -601,15 +715,56 @@ impl<'ctx> PathExplorer<'ctx> { &self.solver } + /// Enable target-guided ordering for query-only helpers. + pub fn set_target_guided_queries(&mut self, enabled: bool) { + self.target_guided_queries = enabled; + } + + /// Whether target-guided ordering is enabled for query helpers. + pub fn target_guided_queries_enabled(&self) -> bool { + self.target_guided_queries + } + /// Register a call hook for a concrete target address. pub fn register_call_hook(&mut self, addr: u64, hook: F) where F: Fn(&mut SymState<'ctx>) -> crate::executor::CallHookResult + 'ctx, { + self.derived_call_summaries.remove(&addr); self.executor .register_call_hook(addr, move |state| Ok(hook(state))); } + pub(crate) fn register_derived_call_hook( + &mut self, + addr: u64, + summary: Rc>, + callconv: CallConv, + hook: F, + ) where + F: Fn(&mut SymState<'ctx>) -> crate::executor::CallHookResult + 'ctx, + { + self.derived_call_summaries + .insert(addr, RegisteredDerivedCallSummary { summary, callconv }); + self.executor + .register_call_hook(addr, move |state| Ok(hook(state))); + } + + pub(crate) fn derived_call_summary_views(&self) -> HashMap> { + self.derived_call_summaries + .iter() + .map(|(addr, registered)| { + ( + *addr, + DerivedCallSummaryView { + summary: registered.summary.clone(), + callconv: registered.callconv.clone(), + }, + ) + }) + .collect() + } + /// Solve a path's constraints and extract concrete values. /// /// Returns None if the path is infeasible. @@ -679,12 +834,84 @@ impl<'ctx> PathExplorer<'ctx> { paths.iter().map(|p| self.solve_path(p)).collect() } + /// Whether the most recent exploration stopped because of a budget limit. + pub fn budget_exhausted(&self) -> bool { + self.stats.timed_out || self.stats.max_states_exhausted + } + fn record_depth(&mut self, depth: usize) { if depth > self.stats.max_depth_reached { self.stats.max_depth_reached = depth; } } + fn state_rank(&self, state: &SymState<'ctx>, id: usize) -> (usize, usize, usize) { + (state.num_constraints(), state.depth, id) + } + + fn prune_subsumed_same_pc_state( + &mut self, + worklist: &mut StateWorklist<'ctx>, + state: SymState<'ctx>, + ) -> Option> { + if !self.config.subsumption_states || state.is_terminated() { + return Some(state); + } + + let candidate_ids = worklist.same_pc_ids(state.pc); + if candidate_ids.is_empty() { + return Some(state); + } + + let state_fingerprint = state.semantic_fingerprint(); + let state_rank = self.state_rank(&state, worklist.slots.len()); + let mut keep_state = true; + + for candidate_id in candidate_ids { + let Some(existing) = worklist.state(candidate_id) else { + continue; + }; + if existing.is_terminated() || existing.semantic_fingerprint() != state_fingerprint { + continue; + } + + self.stats.subsumption_checks += 1; + let existing_subsumes_new = self.solver.implies(&state, existing); + let new_subsumes_existing = self.solver.implies(existing, &state); + + match (existing_subsumes_new, new_subsumes_existing) { + (Some(true), Some(true)) => { + let existing_rank = self.state_rank(existing, candidate_id); + if existing_rank <= state_rank { + self.stats.subsumption_hits += 1; + self.stats.states_subsumed += 1; + keep_state = false; + break; + } + if worklist.remove_slot(candidate_id).is_some() { + self.stats.subsumption_hits += 1; + self.stats.states_subsumed += 1; + } + } + (Some(true), _) => { + self.stats.subsumption_hits += 1; + self.stats.states_subsumed += 1; + keep_state = false; + break; + } + (_, Some(true)) => { + if worklist.remove_slot(candidate_id).is_some() { + self.stats.subsumption_hits += 1; + self.stats.states_subsumed += 1; + } + } + _ => {} + } + } + + keep_state.then_some(state) + } + fn drive( &mut self, func: &SsaArtifact, @@ -706,6 +933,7 @@ impl<'ctx> PathExplorer<'ctx> { && start_time.elapsed() > timeout && mode.on_timeout() { + self.stats.timed_out = true; break; } @@ -716,6 +944,7 @@ impl<'ctx> PathExplorer<'ctx> { } if self.stats.states_explored >= self.config.max_states && mode.on_max_states() { + self.stats.max_states_exhausted = true; break; } @@ -754,7 +983,10 @@ impl<'ctx> PathExplorer<'ctx> { for mut forked in forked_states { forked.set_prev_pc(Some(block_addr)); - if mode.allow_enqueue(&forked) { + if mode.allow_enqueue(&forked) + && let Some(forked) = + self.prune_subsumed_same_pc_state(&mut worklist, forked) + { worklist.push(forked); } } @@ -766,7 +998,10 @@ impl<'ctx> PathExplorer<'ctx> { state.pc = next; } state.set_prev_pc(Some(block_addr)); - if mode.allow_enqueue(&state) { + if mode.allow_enqueue(&state) + && let Some(state) = + self.prune_subsumed_same_pc_state(&mut worklist, state) + { worklist.push(state); } } else { @@ -797,6 +1032,329 @@ impl<'ctx> PathExplorer<'ctx> { self.stats.total_time = start_time.elapsed(); } + fn target_guidance_context( + &self, + func: &SsaArtifact, + target_addr: u64, + ) -> TargetGuidanceContext { + let distances = self.target_distance_map(func, target_addr); + let reachable_blocks = distances.keys().copied().collect::>(); + let mut call_targets_by_block: HashMap> = HashMap::new(); + let mut block_summary_rank: HashMap = HashMap::new(); + + for call in func.call_sites().by_id.values() { + let Some(target) = call.direct_target else { + continue; + }; + let Some((block_addr, _)) = func.inst_op_site(call.at) else { + continue; + }; + call_targets_by_block + .entry(block_addr) + .or_default() + .push(target); + + let Some(binding) = self.derived_call_summaries.get(&target) else { + continue; + }; + let entry = block_summary_rank + .entry(block_addr) + .or_insert_with(|| BlockSummaryRank { + has_summary: false, + has_exact_summary: false, + min_case_count: usize::MAX, + }); + entry.has_summary = true; + entry.has_exact_summary |= matches!( + binding.summary.completion, + crate::sim::DerivedSummaryCompletion::Exact + ); + entry.min_case_count = entry.min_case_count.min(binding.summary.cases.len()); + } + + TargetGuidanceContext { + distances, + reachable_blocks, + call_targets_by_block, + block_summary_rank, + } + } + + fn symbolic_fanout_proxy(&self, state: &SymState<'ctx>) -> usize { + state + .registers() + .values() + .filter(|value| value.is_symbolic()) + .count() + .saturating_add(state.symbolic_inputs().len()) + .saturating_add(state.symbolic_memory().len()) + .saturating_add(state.symbolic_fd_inputs().len()) + } + + fn state_summary_guidance( + &self, + guidance: &TargetGuidanceContext, + state: &SymState<'ctx>, + ) -> StateSummaryGuidance { + let Some(targets) = guidance.call_targets_by_block.get(&state.pc) else { + return StateSummaryGuidance::default(); + }; + + let mut result = StateSummaryGuidance { + summary_hits: 0, + min_feasible_cases: usize::MAX, + contradictory: false, + }; + + for target in targets { + let Some(binding) = self.derived_call_summaries.get(target) else { + continue; + }; + let summary_guidance = evaluate_derived_summary_guidance( + state, + &binding.summary, + &binding.callconv, + &self.solver, + ); + if !summary_guidance.summary_known { + continue; + } + result.summary_hits += 1; + result.min_feasible_cases = result + .min_feasible_cases + .min(summary_guidance.feasible_cases.max(1)); + if summary_guidance.contradictory { + result.contradictory = true; + break; + } + } + + if result.summary_hits == 0 { + StateSummaryGuidance::default() + } else { + result + } + } + + fn target_enqueue_allowed( + &mut self, + guidance: &TargetGuidanceContext, + state: &SymState<'ctx>, + ) -> bool { + if !guidance.reachable_blocks.contains(&state.pc) { + self.stats.target_pruned_cfg_unreachable += 1; + return false; + } + + let summary_guidance = self.state_summary_guidance(guidance, state); + if summary_guidance.summary_hits > 0 { + self.stats.target_summary_rank_hits += 1; + } + if summary_guidance.contradictory { + self.stats.target_pruned_summary_contradiction += 1; + return false; + } + true + } + + fn drive_target_guided( + &mut self, + func: &SsaArtifact, + initial_state: SymState<'ctx>, + mode: &mut DriverMode<'ctx>, + target_addr: u64, + ) { + let start_time = Instant::now(); + let mut worklist = StateWorklist::new(self.config.strategy); + let guidance = self.target_guidance_context(func, target_addr); + if self.target_enqueue_allowed(&guidance, &initial_state) { + worklist.push(initial_state); + } + + while let Some((mut state, reordered)) = worklist + .pop_best_by_key_with_reorder(|id, state| self.state_target_rank(state, id, &guidance)) + { + if reordered { + self.stats.target_guided_reorders += 1; + } + if self.config.merge_states + && let Some(other) = worklist.take_same_pc(state.pc) + { + state = state.merge_with(&other); + } + + if let Some(timeout) = self.config.timeout + && start_time.elapsed() > timeout + && mode.on_timeout() + { + self.stats.timed_out = true; + break; + } + + match mode.on_state_popped(self, state) { + DriverAction::Continue(next_state) => state = *next_state, + DriverAction::Skip => continue, + DriverAction::Finish => break, + } + + if self.stats.states_explored >= self.config.max_states && mode.on_max_states() { + self.stats.max_states_exhausted = true; + break; + } + + if state.depth >= self.config.max_depth { + match mode.on_depth_limit(self, state, self.config.max_completed_paths) { + DriverAction::Continue(next_state) => state = *next_state, + DriverAction::Skip => continue, + DriverAction::Finish => break, + } + } + + self.stats.states_explored += 1; + + if self.config.prune_infeasible && !self.solver.is_sat(&state) { + self.stats.paths_pruned += 1; + mode.on_unsat_pruned(); + continue; + } + + let block_addr = state.pc; + let Some(block) = func.get_block(block_addr) else { + match mode.on_missing_block( + self, + state, + block_addr, + self.config.max_completed_paths, + ) { + DriverAction::Finish => break, + DriverAction::Continue(_) | DriverAction::Skip => continue, + } + }; + + match self.executor.execute_block(&mut state, block) { + Ok(forked_states) => { + self.record_depth(state.depth); + + for mut forked in forked_states { + forked.set_prev_pc(Some(block_addr)); + if mode.allow_enqueue(&forked) + && self.target_enqueue_allowed(&guidance, &forked) + && let Some(forked) = + self.prune_subsumed_same_pc_state(&mut worklist, forked) + { + worklist.push(forked); + } + } + + if !state.is_terminated() { + if state.pc == block_addr + && let Some(next) = self.fallthrough_target(func, block_addr) + { + state.pc = next; + } + state.set_prev_pc(Some(block_addr)); + if mode.allow_enqueue(&state) + && self.target_enqueue_allowed(&guidance, &state) + && let Some(state) = + self.prune_subsumed_same_pc_state(&mut worklist, state) + { + worklist.push(state); + } + } else { + match mode.on_terminated_state( + self, + state, + block_addr, + self.config.max_completed_paths, + ) { + DriverAction::Finish => break, + DriverAction::Continue(_) | DriverAction::Skip => continue, + } + } + } + Err(e) => match mode.on_execute_error( + self, + state, + block_addr, + e.to_string(), + self.config.max_completed_paths, + ) { + DriverAction::Finish => break, + DriverAction::Continue(_) | DriverAction::Skip => continue, + }, + } + } + + self.stats.total_time = start_time.elapsed(); + } + + fn target_distance_map(&self, func: &SsaArtifact, target_addr: u64) -> HashMap { + let mut predecessors: HashMap> = HashMap::new(); + for addr in func.cfg().block_addrs() { + let Some(block) = func.cfg().get_block(addr) else { + continue; + }; + for succ in block.successors() { + predecessors.entry(succ).or_default().push(addr); + } + } + + let mut distances: HashMap = HashMap::new(); + let mut queue = VecDeque::new(); + distances.insert(target_addr, 0); + queue.push_back(target_addr); + + while let Some(addr) = queue.pop_front() { + let distance = distances[&addr]; + for pred in predecessors.get(&addr).into_iter().flatten() { + if distances.contains_key(pred) { + continue; + } + distances.insert(*pred, distance.saturating_add(1)); + queue.push_back(*pred); + } + } + + distances + } + + fn state_target_rank( + &self, + state: &SymState<'ctx>, + id: usize, + guidance: &TargetGuidanceContext, + ) -> (bool, usize, usize, usize, usize, usize, usize, usize, usize) { + let reachable = guidance.reachable_blocks.contains(&state.pc); + let distance = guidance + .distances + .get(&state.pc) + .copied() + .unwrap_or(usize::MAX); + let summary_rank = guidance + .block_summary_rank + .get(&state.pc) + .cloned() + .unwrap_or_default(); + let summary_known_penalty = usize::from(!summary_rank.has_summary); + let summary_exact_penalty = usize::from(!summary_rank.has_exact_summary); + let summary_case_rank = if summary_rank.has_summary { + summary_rank.min_case_count + } else { + usize::MAX + }; + ( + !reachable, + distance, + summary_known_penalty, + summary_exact_penalty, + summary_case_rank, + self.symbolic_fanout_proxy(state), + state.num_constraints(), + state.depth, + id, + ) + } + /// Explore all paths in a function. pub fn explore( &mut self, @@ -839,7 +1397,11 @@ impl<'ctx> PathExplorer<'ctx> { target_addr, found: None, }; - self.drive(func, initial_state, &mut mode); + if self.target_guided_queries { + self.drive_target_guided(func, initial_state, &mut mode, target_addr); + } else { + self.drive(func, initial_state, &mut mode); + } match mode { DriverMode::FindFirst { found, .. } => found, _ => unreachable!("find_path_to should always use first-match mode"), @@ -857,7 +1419,11 @@ impl<'ctx> PathExplorer<'ctx> { target_addr, matches: Vec::new(), }; - self.drive(func, initial_state, &mut mode); + if self.target_guided_queries { + self.drive_target_guided(func, initial_state, &mut mode, target_addr); + } else { + self.drive(func, initial_state, &mut mode); + } match mode { DriverMode::FindAll { matches, .. } => matches, _ => unreachable!("find_paths_to should always use all-match mode"), @@ -906,7 +1472,45 @@ impl<'ctx> PathExplorer<'ctx> { #[cfg(test)] mod tests { use super::*; + use std::rc::Rc; + + use r2il::{ArchSpec, R2ILBlock, R2ILOp, RegisterDef, SpaceId, Varnode}; + use r2ssa::{InterprocFunctionId, SsaArtifact}; + use z3::ast::BV; + use crate::SymValue; + use crate::executor::CallHookResult; + use crate::sim::{ + CallConv, DerivedFunctionSummary, DerivedSummaryCase, DerivedSummaryCompletion, + }; + + const RDI: u64 = 56; + const TMP0: u64 = 0x80; + + fn make_reg(offset: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Register, + offset, + size, + meta: None, + } + } + + fn make_const(val: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Const, + offset: val, + size, + meta: None, + } + } + + fn make_x86_64_arch() -> ArchSpec { + let mut arch = ArchSpec::new("x86-64"); + arch.addr_size = 8; + arch.add_register(RegisterDef::new("RDI", RDI, 8)); + arch + } #[test] fn test_explore_config_default() { @@ -915,6 +1519,7 @@ mod tests { assert_eq!(config.max_completed_paths, None); assert_eq!(config.max_depth, 100); assert!(config.prune_infeasible); + assert!(!config.subsumption_states); } #[test] @@ -955,6 +1560,99 @@ mod tests { assert!(worklist.take_same_pc(0x1000).is_none()); } + #[test] + fn test_same_pc_subsumption_prefers_weaker_constraint_state() { + let ctx = Context::thread_local(); + let mut config = ExploreConfig { + subsumption_states: true, + ..ExploreConfig::default() + }; + config.prune_infeasible = false; + let mut explorer = PathExplorer::with_config(&ctx, config); + let mut worklist = StateWorklist::new(ExploreStrategy::Dfs); + + let mut existing = SymState::new(&ctx, 0x1000); + existing.make_symbolic("sym_input", 64); + let input = existing.get_register("sym_input"); + existing.add_constraint(input.to_bv(&ctx).bvult(z3::ast::BV::from_u64(10, 64))); + worklist.push(existing); + + let mut stronger = SymState::new(&ctx, 0x1000); + stronger.make_symbolic("sym_input", 64); + let input = stronger.get_register("sym_input"); + stronger.add_constraint(input.to_bv(&ctx).bvult(z3::ast::BV::from_u64(10, 64))); + stronger.add_constraint(input.to_bv(&ctx).bvult(z3::ast::BV::from_u64(5, 64))); + + let pruned = explorer.prune_subsumed_same_pc_state(&mut worklist, stronger); + assert!( + pruned.is_none(), + "stronger same-pc state should be subsumed" + ); + assert_eq!(explorer.stats.subsumption_checks, 1); + assert_eq!(explorer.stats.subsumption_hits, 1); + assert_eq!(explorer.stats.states_subsumed, 1); + assert_eq!(worklist.live_states, 1); + } + + #[test] + fn test_same_pc_subsumption_replaces_stronger_constraint_state() { + let ctx = Context::thread_local(); + let mut config = ExploreConfig { + subsumption_states: true, + ..ExploreConfig::default() + }; + config.prune_infeasible = false; + let mut explorer = PathExplorer::with_config(&ctx, config); + let mut worklist = StateWorklist::new(ExploreStrategy::Dfs); + + let mut stronger = SymState::new(&ctx, 0x1000); + stronger.make_symbolic("sym_input", 64); + let input = stronger.get_register("sym_input"); + stronger.add_constraint(input.to_bv(&ctx).bvult(z3::ast::BV::from_u64(10, 64))); + stronger.add_constraint(input.to_bv(&ctx).bvult(z3::ast::BV::from_u64(5, 64))); + worklist.push(stronger); + + let mut weaker = SymState::new(&ctx, 0x1000); + weaker.make_symbolic("sym_input", 64); + let input = weaker.get_register("sym_input"); + weaker.add_constraint(input.to_bv(&ctx).bvult(z3::ast::BV::from_u64(10, 64))); + let kept = explorer.prune_subsumed_same_pc_state(&mut worklist, weaker); + + assert!(kept.is_some(), "weaker same-pc state should survive"); + assert_eq!(explorer.stats.subsumption_checks, 1); + assert_eq!(explorer.stats.subsumption_hits, 1); + assert_eq!(explorer.stats.states_subsumed, 1); + assert_eq!(worklist.live_states, 0); + } + + #[test] + fn test_same_pc_subsumption_tie_break_prefers_shallower_state() { + let ctx = Context::thread_local(); + let config = ExploreConfig { + subsumption_states: true, + ..ExploreConfig::default() + }; + let mut explorer = PathExplorer::with_config(&ctx, config); + let mut worklist = StateWorklist::new(ExploreStrategy::Random); + + let mut existing = SymState::new(&ctx, 0x1000); + existing.make_symbolic("sym_input", 64); + existing.step(); + worklist.push(existing); + + let mut new_state = SymState::new(&ctx, 0x1000); + new_state.make_symbolic("sym_input", 64); + let kept = explorer.prune_subsumed_same_pc_state(&mut worklist, new_state); + + assert!( + kept.is_some(), + "shallower state should replace deeper equivalent state" + ); + assert_eq!(worklist.live_states, 0); + assert_eq!(explorer.stats.subsumption_hits, 1); + assert_eq!(explorer.stats.states_subsumed, 1); + } + #[test] fn test_path_result_methods() { let ctx = Context::thread_local(); @@ -1044,4 +1742,70 @@ mod tests { ] ); } + + #[test] + fn test_target_guided_prunes_exact_summary_contradiction() { + let ctx = Context::thread_local(); + let arch = make_x86_64_arch(); + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![R2ILOp::Call { + target: make_const(0x2000, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x1010, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(TMP0, 1), + src: make_const(1, 1), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, Some(&arch)).expect("symbolic function"); + + let mut explorer = PathExplorer::new(&ctx); + explorer.set_target_guided_queries(true); + + let arg0 = SymValue::new_symbolic(&ctx, "summary_arg0", 64); + let guard = arg0.to_bv(&ctx).eq(BV::from_u64(1, 64)); + let summary = Rc::new(DerivedFunctionSummary { + id: InterprocFunctionId(0x2000), + name: Some("contradict_helper".to_string()), + arg_count_hint: 1, + arg_symbols: vec![(0, arg0)], + memory_inputs: Vec::new(), + cases: vec![DerivedSummaryCase { + guard, + return_value: None, + memory_writes: Vec::new(), + }], + completion: DerivedSummaryCompletion::Exact, + }); + explorer.register_derived_call_hook(0x2000, summary, CallConv::x86_64_sysv(), |_state| { + CallHookResult::Fallthrough + }); + + let mut state = SymState::new(&ctx, 0x1000); + state.set_register("RDI_0", SymValue::concrete(2, 64)); + + let paths = explorer.find_paths_to(&func, state, 0x1010); + assert!(paths.is_empty(), "contradictory exact summary should prune"); + assert_eq!(explorer.stats().target_pruned_summary_contradiction, 1); + } } diff --git a/crates/r2sym/src/query.rs b/crates/r2sym/src/query.rs new file mode 100644 index 0000000..775b7a9 --- /dev/null +++ b/crates/r2sym/src/query.rs @@ -0,0 +1,784 @@ +//! Typed query-oriented symbolic analysis APIs. +//! +//! This module wraps the lower-level path exploration engine in reusable, +//! analysis-oriented queries that can be consumed by the plugin or other +//! analysis layers without exposing command-shaped policy. + +use z3::Context; +use z3::ast::Ast; + +use r2ssa::SsaArtifact; + +use crate::SymState; +use crate::backward::{ + BackwardConditionPrecision, BackwardConditionSummary, CompiledBackwardCondition, + compile_target_precondition_with_summaries, compile_value_postcondition_with_summaries, +}; +use crate::path::{ExploreConfig, ExploreStats, PathExplorer, PathResult, SolvedPath}; +use crate::semantics::{VmStepSummary, build_vm_step_summary, classify_interpreter_like}; +use crate::sim::SummaryProfile; +use crate::solver::{SatResult, SolverStats}; +use crate::state::ExitStatus; + +/// Query execution strategy. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum QueryMode { + /// Use the current forward symbolic execution engine. + ForwardOnly, + /// Reserve a slot for future target-guided pruning. + TargetGuided, +} + +/// Typed configuration for engine-level symbolic queries. +#[derive(Debug, Clone)] +pub struct SymQueryConfig { + /// Core exploration budgets and strategy. + pub explore: ExploreConfig, + /// Query execution mode. + pub mode: QueryMode, + /// Summary profile to use when installing function summaries. + pub summary_profile: SummaryProfile, +} + +impl Default for SymQueryConfig { + fn default() -> Self { + let explore = ExploreConfig { + subsumption_states: true, + ..ExploreConfig::default() + }; + Self { + explore, + mode: QueryMode::ForwardOnly, + summary_profile: SummaryProfile::Default, + } + } +} + +impl SymQueryConfig { + /// Build a path explorer for this query configuration. + pub fn make_explorer<'ctx>(&self, ctx: &'ctx Context) -> PathExplorer<'ctx> { + let mut explorer = PathExplorer::with_config(ctx, self.explore.clone()); + explorer.set_target_guided_queries(matches!(self.mode, QueryMode::TargetGuided)); + explorer + } +} + +/// Completion state for queries that may stop on budgets. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum QueryCompletion { + /// Exploration finished without hitting timeout or state budgets. + Complete, + /// Exploration stopped because a configured budget was exhausted. + BudgetExhausted, +} + +/// Reachability result status. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ReachabilityStatus { + Reachable, + Unreachable, + Unknown, + BudgetExhausted, +} + +/// Solve result status. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SolveStatus { + Solved, + Unsat, + Unknown, + BudgetExhausted, +} + +/// Compact summary of a path condition reaching a given program counter. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PathConditionTerm { + pub expr: String, +} + +/// Typed symbolic path-condition artifact. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SymbolicConditionSet { + pub simplified: String, + pub terms: Vec, + pub num_constraints: usize, +} + +/// Compact summary of a path condition reaching a given program counter. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PathConditionSummary { + pub final_pc: u64, + pub depth: usize, + pub exit_status: ExitStatus, + pub feasible: bool, + pub num_constraints: usize, + pub condition: String, + pub path_condition: SymbolicConditionSet, +} + +/// Result of asking whether a target can be reached. +#[derive(Debug)] +pub struct ReachabilityResult<'ctx> { + pub status: ReachabilityStatus, + pub target_addr: u64, + pub compiled_precondition: Option, + pub paths: Vec>, + pub stats: ExploreStats, + pub solver_stats: SolverStats, +} + +/// Result of asking for the conditions that hold at a target PC. +#[derive(Debug)] +pub struct PathConditionResult<'ctx> { + pub completion: QueryCompletion, + pub target_pc: u64, + pub compiled_precondition: Option, + pub conditions: Vec, + pub matching_paths: Vec>, + pub stats: ExploreStats, + pub solver_stats: SolverStats, +} + +/// Result of solving for a concrete target. +#[derive(Debug)] +pub struct SolveResult<'ctx> { + pub status: SolveStatus, + pub target_addr: u64, + pub compiled_precondition: Option, + pub matched_paths: Vec>, + pub selected_path_index: Option, + pub solution: Option, + pub stats: ExploreStats, + pub solver_stats: SolverStats, +} + +/// Function-level symbolic summary with collected paths. +#[derive(Debug)] +pub struct SymbolicFunctionSummary<'ctx> { + pub completion: QueryCompletion, + pub paths: Vec>, + pub feasible_paths: usize, + pub stats: ExploreStats, + pub solver_stats: SolverStats, +} + +fn completion_from_stats(stats: &ExploreStats) -> QueryCompletion { + if stats.timed_out || stats.max_states_exhausted { + QueryCompletion::BudgetExhausted + } else { + QueryCompletion::Complete + } +} + +fn condition_string<'ctx>(state: &SymState<'ctx>) -> String { + match state.constraints() { + [] => "true".to_string(), + [constraint] => constraint.simplify().to_string(), + _ => state.path_condition().simplify().to_string(), + } +} + +fn symbolic_condition_set<'ctx>(state: &SymState<'ctx>) -> SymbolicConditionSet { + let terms = state + .constraints() + .iter() + .map(|constraint| PathConditionTerm { + expr: constraint.simplify().to_string(), + }) + .collect(); + SymbolicConditionSet { + simplified: condition_string(state), + terms, + num_constraints: state.num_constraints(), + } +} + +fn condition_summary<'ctx>(path: &PathResult<'ctx>) -> PathConditionSummary { + let path_condition = symbolic_condition_set(&path.state); + PathConditionSummary { + final_pc: path.final_pc(), + depth: path.depth, + exit_status: path.exit_status.clone(), + feasible: path.feasible, + num_constraints: path.num_constraints(), + condition: path_condition.simplified.clone(), + path_condition, + } +} + +enum PreconditionApplication<'ctx> { + Continue { + initial_state: Box>, + compiled_precondition: Option, + }, + ExactUnsat { + compiled_precondition: BackwardConditionSummary, + }, +} + +fn precision_rank(precision: BackwardConditionPrecision) -> u8 { + match precision { + BackwardConditionPrecision::Exact => 3, + BackwardConditionPrecision::OverApprox => 2, + BackwardConditionPrecision::ResidualSearchRequired => 1, + BackwardConditionPrecision::Unsupported => 0, + } +} + +fn vm_arm_reaches_target(arm: &crate::VmTransferArm, target_addr: u64) -> bool { + arm.handler_target == target_addr + || arm.region_blocks.contains(&target_addr) + || arm.exit_targets.contains(&target_addr) +} + +fn vm_arm_for_case(vm_step: &VmStepSummary, case_value: u64) -> Option<&crate::VmTransferArm> { + vm_step + .transfers + .iter() + .find(|arm| arm.case_values.contains(&case_value)) +} + +fn vm_target_case_values(vm_step: &VmStepSummary, target_addr: u64) -> Vec { + let mut case_values = std::collections::BTreeSet::new(); + + for arm in &vm_step.transfers { + if vm_arm_reaches_target(arm, target_addr) { + case_values.extend(arm.case_values.iter().copied()); + } + } + + for arm in &vm_step.transfers { + if !arm.redispatch || arm.case_values.is_empty() { + continue; + } + let Some(selector_update) = arm.selector_update.as_ref() else { + continue; + }; + let crate::VmValueExpr::Const(next_case) = &selector_update.value else { + continue; + }; + let Some(next_arm) = vm_arm_for_case(vm_step, *next_case) else { + continue; + }; + if vm_arm_reaches_target(next_arm, target_addr) { + case_values.extend(arm.case_values.iter().copied()); + } + } + + case_values.into_iter().collect() +} + +fn compile_vm_target_precondition_with_summaries<'ctx>( + func: &SsaArtifact, + initial_state: &SymState<'ctx>, + target_addr: u64, + call_summaries: &std::collections::HashMap>, +) -> Option { + let interpreter = classify_interpreter_like(func)?; + let vm_step = build_vm_step_summary(func, &interpreter)?; + let case_values = vm_target_case_values(&vm_step, target_addr); + if case_values.is_empty() { + return None; + } + let selector = func + .function() + .infer_switch_selector_var(vm_step.dispatch_header)?; + let ctx = initial_state.context(); + compile_value_postcondition_with_summaries( + func, + initial_state, + vm_step.dispatch_header, + selector, + { + let case_values = case_values.clone(); + move |value| { + let selector_bv = value.to_bv(ctx); + let disjuncts = case_values + .iter() + .map(|value| { + selector_bv.eq(z3::ast::BV::from_u64(*value, selector_bv.get_size())) + }) + .collect::>(); + match disjuncts.as_slice() { + [] => z3::ast::Bool::from_bool(false), + [single] => single.clone(), + _ => { + let refs = disjuncts.iter().collect::>(); + z3::ast::Bool::or(&refs) + } + } + } + }, + call_summaries, + ) +} + +fn prefer_compiled_precondition( + current: Option<&CompiledBackwardCondition>, + candidate: &CompiledBackwardCondition, +) -> bool { + let Some(current) = current else { + return true; + }; + let current_rank = precision_rank(current.summary.precision); + let candidate_rank = precision_rank(candidate.summary.precision); + if candidate_rank != current_rank { + return candidate_rank > current_rank; + } + if candidate.summary.backward_memory_residual_fallbacks + != current.summary.backward_memory_residual_fallbacks + { + return candidate.summary.backward_memory_residual_fallbacks + < current.summary.backward_memory_residual_fallbacks; + } + if candidate.summary.memory_terms.len() != current.summary.memory_terms.len() { + return candidate.summary.memory_terms.len() > current.summary.memory_terms.len(); + } + if candidate.summary.supported_paths != current.summary.supported_paths { + return candidate.summary.supported_paths > current.summary.supported_paths; + } + candidate.summary.total_paths < current.summary.total_paths +} + +fn apply_compiled_precondition<'ctx>( + explorer: &PathExplorer<'ctx>, + func: &SsaArtifact, + mut initial_state: SymState<'ctx>, + target_addr: u64, +) -> PreconditionApplication<'ctx> { + let derived_summaries = explorer.derived_call_summary_views(); + let mut compiled = compile_target_precondition_with_summaries( + func, + &initial_state, + target_addr, + &derived_summaries, + ); + if compiled.as_ref().is_none_or(|compiled| { + !matches!( + compiled.summary.precision, + BackwardConditionPrecision::Exact + ) + }) && let Some(vm_compiled) = compile_vm_target_precondition_with_summaries( + func, + &initial_state, + target_addr, + &derived_summaries, + ) && prefer_compiled_precondition(compiled.as_ref(), &vm_compiled) + { + compiled = Some(vm_compiled); + } + let Some(compiled) = compiled else { + return PreconditionApplication::Continue { + initial_state: Box::new(initial_state), + compiled_precondition: None, + }; + }; + + let summary = compiled.summary.clone(); + let exact = matches!(summary.precision, BackwardConditionPrecision::Exact); + match explorer + .solver() + .sat_with_constraint(&initial_state, &compiled.predicate) + { + SatResult::Unsat if exact => PreconditionApplication::ExactUnsat { + compiled_precondition: summary, + }, + SatResult::Sat => { + if exact { + initial_state.add_constraint(compiled.predicate); + } + PreconditionApplication::Continue { + initial_state: Box::new(initial_state), + compiled_precondition: Some(summary), + } + } + SatResult::Unknown | SatResult::Unsat => PreconditionApplication::Continue { + initial_state: Box::new(initial_state), + compiled_precondition: Some(summary), + }, + } +} + +impl<'ctx> PathExplorer<'ctx> { + /// Ask whether a target address is reachable and collect the matching paths. + pub fn can_reach( + &mut self, + func: &SsaArtifact, + initial_state: SymState<'ctx>, + target_addr: u64, + ) -> ReachabilityResult<'ctx> { + let (paths, compiled_precondition) = + match apply_compiled_precondition(self, func, initial_state, target_addr) { + PreconditionApplication::Continue { + initial_state, + compiled_precondition, + } => ( + self.find_paths_to(func, *initial_state, target_addr), + compiled_precondition, + ), + PreconditionApplication::ExactUnsat { + compiled_precondition, + } => (Vec::new(), Some(compiled_precondition)), + }; + let stats = self.stats().clone(); + let solver_stats = self.solver().stats(); + let status = if !paths.is_empty() { + ReachabilityStatus::Reachable + } else if self.budget_exhausted() { + ReachabilityStatus::BudgetExhausted + } else { + ReachabilityStatus::Unreachable + }; + ReachabilityResult { + status, + target_addr, + compiled_precondition, + paths, + stats, + solver_stats, + } + } + + /// Collect path conditions for states that reach a target PC. + pub fn path_conditions_at( + &mut self, + func: &SsaArtifact, + initial_state: SymState<'ctx>, + target_pc: u64, + ) -> PathConditionResult<'ctx> { + let (matching_paths, compiled_precondition) = + match apply_compiled_precondition(self, func, initial_state, target_pc) { + PreconditionApplication::Continue { + initial_state, + compiled_precondition, + } => ( + self.find_paths_to(func, *initial_state, target_pc), + compiled_precondition, + ), + PreconditionApplication::ExactUnsat { + compiled_precondition, + } => (Vec::new(), Some(compiled_precondition)), + }; + let stats = self.stats().clone(); + let solver_stats = self.solver().stats(); + let conditions = matching_paths.iter().map(condition_summary).collect(); + PathConditionResult { + completion: completion_from_stats(&stats), + target_pc, + compiled_precondition, + conditions, + matching_paths, + stats, + solver_stats, + } + } + + /// Solve for a concrete model that reaches a target address. + pub fn solve_for_target( + &mut self, + func: &SsaArtifact, + initial_state: SymState<'ctx>, + target_addr: u64, + ) -> SolveResult<'ctx> { + let (matched_paths, compiled_precondition, exact_unsat) = + match apply_compiled_precondition(self, func, initial_state, target_addr) { + PreconditionApplication::Continue { + initial_state, + compiled_precondition, + } => ( + self.find_paths_to(func, *initial_state, target_addr), + compiled_precondition, + false, + ), + PreconditionApplication::ExactUnsat { + compiled_precondition, + } => (Vec::new(), Some(compiled_precondition), true), + }; + let stats = self.stats().clone(); + let solver_stats = self.solver().stats(); + let selected_path_index = matched_paths + .iter() + .enumerate() + .min_by_key(|(idx, path)| (path.num_constraints(), path.depth, *idx)) + .map(|(idx, _)| idx); + let solution = selected_path_index.and_then(|idx| self.solve_path(&matched_paths[idx])); + let status = match ( + exact_unsat, + selected_path_index, + solution.as_ref(), + completion_from_stats(&stats), + ) { + (true, _, _, _) => SolveStatus::Unsat, + (false, _, Some(_), _) => SolveStatus::Solved, + (false, None, _, QueryCompletion::BudgetExhausted) => SolveStatus::BudgetExhausted, + (false, None, _, QueryCompletion::Complete) => SolveStatus::Unsat, + (false, Some(_), None, QueryCompletion::BudgetExhausted) => { + SolveStatus::BudgetExhausted + } + (false, Some(_), None, _) => SolveStatus::Unknown, + }; + SolveResult { + status, + target_addr, + compiled_precondition, + matched_paths, + selected_path_index, + solution, + stats, + solver_stats, + } + } + + /// Explore a function and return a typed symbolic summary over its paths. + pub fn summarize_function( + &mut self, + func: &SsaArtifact, + initial_state: SymState<'ctx>, + ) -> SymbolicFunctionSummary<'ctx> { + let paths = self.explore(func, initial_state); + let stats = self.stats().clone(); + let solver_stats = self.solver().stats(); + let feasible_paths = paths.iter().filter(|path| path.feasible).count(); + SymbolicFunctionSummary { + completion: completion_from_stats(&stats), + paths, + feasible_paths, + stats, + solver_stats, + } + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use super::{ + PreconditionApplication, apply_compiled_precondition, prefer_compiled_precondition, + vm_target_case_values, + }; + use crate::{ + BackwardConditionPrecision, BackwardConditionSummary, SymQueryConfig, SymState, + VmStateUpdate, VmStepSummary, VmTransferArm, VmValueExpr, + }; + use r2il::{R2ILBlock, R2ILOp, SpaceId, Varnode}; + use r2ssa::SsaArtifact; + use z3::Context; + + const RDI: u64 = 56; + const TMP0: u64 = 0x80; + const TMP1: u64 = 0x88; + + fn make_reg(offset: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Register, + offset, + size, + meta: None, + } + } + + fn make_const(value: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Const, + offset: value, + size, + meta: None, + } + } + + fn make_residual_precondition_blocks() -> Vec { + vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::IntEqual { + dst: make_reg(TMP0, 1), + a: make_reg(RDI, 8), + b: make_const(1, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP0, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::IntCarry { + dst: make_reg(TMP1, 1), + a: make_reg(RDI, 8), + b: make_const(1, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP1, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + ] + } + + fn make_vm_step_summary_with_transfers(transfers: Vec) -> VmStepSummary { + VmStepSummary { + kind: crate::InterpreterKind::SwitchDispatch, + loop_header: 0x1000, + dispatch_header: 0x1000, + selector: Some("RDI_0".to_string()), + dispatch_targets: vec![0x1004, 0x1008, 0x1010], + default_target: Some(0x1010), + case_values_by_target: BTreeMap::from([ + (0x1004, vec![0]), + (0x1008, vec![1]), + (0x1010, vec![2]), + ]), + loop_latches: vec![0x1000], + state_inputs: vec!["RDI_0".to_string()], + state_outputs: vec!["RDI_1".to_string()], + step_blocks: vec![0x1000, 0x1004, 0x1008, 0x1010], + handler_regions: BTreeMap::new(), + handler_state_inputs: BTreeMap::new(), + handler_state_outputs: BTreeMap::new(), + handler_state_updates: BTreeMap::new(), + handler_memory_reads: BTreeMap::new(), + handler_memory_writes: BTreeMap::new(), + handler_calls: BTreeMap::new(), + handler_conditional_branches: BTreeMap::new(), + handler_exit_targets: BTreeMap::new(), + redispatch_handlers: Vec::new(), + returning_handlers: Vec::new(), + truncated_handlers: Vec::new(), + transfers, + } + } + + #[test] + fn residual_compiled_preconditions_do_not_constrain_forward_search() { + let blocks = make_residual_precondition_blocks(); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("reg:56_0", 64); + + let explorer = SymQueryConfig::default().make_explorer(&ctx); + let original_constraints = state.num_constraints(); + match apply_compiled_precondition(&explorer, &func, state, 0x1010) { + PreconditionApplication::Continue { + initial_state, + compiled_precondition, + } => { + let compiled = compiled_precondition.expect("compiled precondition"); + assert_eq!( + compiled.precision, + crate::BackwardConditionPrecision::ResidualSearchRequired + ); + assert_eq!(initial_state.num_constraints(), original_constraints); + } + PreconditionApplication::ExactUnsat { .. } => { + panic!("residual precondition should not shortcut as exact unsat") + } + } + } + + #[test] + fn vm_target_case_values_include_redispatch_cases() { + let vm_step = make_vm_step_summary_with_transfers(vec![ + VmTransferArm { + handler_target: 0x1004, + case_values: vec![0], + region_blocks: vec![0x1004], + exit_targets: vec![0x1000], + state_updates: vec![VmStateUpdate { + output: "RDI_1".to_string(), + expr: "0x1".to_string(), + value: VmValueExpr::Const(1), + exact: true, + }], + selector_update: Some(VmStateUpdate { + output: "RDI_1".to_string(), + expr: "0x1".to_string(), + value: VmValueExpr::Const(1), + exact: true, + }), + exact: true, + redispatch: true, + may_return: false, + truncated: false, + }, + VmTransferArm { + handler_target: 0x1008, + case_values: vec![1], + region_blocks: vec![0x1008, 0x1014], + exit_targets: vec![0x1014], + state_updates: Vec::new(), + selector_update: None, + exact: true, + redispatch: false, + may_return: true, + truncated: false, + }, + ]); + + assert_eq!(vm_target_case_values(&vm_step, 0x1014), vec![0, 1]); + } + + #[test] + fn exact_compiled_preconditions_prefer_fewer_residual_fallbacks() { + let current = crate::CompiledBackwardCondition { + predicate: z3::ast::Bool::from_bool(true), + summary: BackwardConditionSummary { + simplified: "x".to_string(), + terms: vec!["x".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 2, + precision: BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }, + }; + let candidate = crate::CompiledBackwardCondition { + predicate: z3::ast::Bool::from_bool(true), + summary: BackwardConditionSummary { + simplified: "y".to_string(), + terms: vec!["y".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 1, + precision: BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }, + }; + + assert!(prefer_compiled_precondition(Some(¤t), &candidate)); + assert!(!prefer_compiled_precondition(Some(&candidate), ¤t)); + } +} diff --git a/crates/r2sym/src/replay.rs b/crates/r2sym/src/replay.rs new file mode 100644 index 0000000..90dbd3d --- /dev/null +++ b/crates/r2sym/src/replay.rs @@ -0,0 +1,503 @@ +use std::borrow::Cow; +use std::collections::BTreeMap; + +use r2il::ArchSpec; +use r2ssa::SsaArtifact; + +use crate::memory::MemoryRegionKind; +use crate::runtime::seed_memory_regions_for_arch; +use crate::state::SymState; + +#[derive(Debug, Clone, Default)] +pub struct ReplaySeed { + pub checkpoint_id: Option, + pub entry_pc: Option, + pub registers: Vec, + pub memory: Vec, + pub register_overlays: Vec, + pub memory_overlays: Vec, + pub tty_fds: Vec, + pub skip_sleep_calls: bool, +} + +#[derive(Debug, Clone)] +pub struct ReplayRegisterValue { + pub name: String, + pub value: u64, +} + +#[derive(Debug, Clone)] +pub struct ReplayMemoryWindow { + pub addr: u64, + pub bytes: Vec, + pub label: Option, +} + +#[derive(Debug, Clone)] +pub struct ReplayRegisterOverlay { + pub name: String, + pub symbol: String, +} + +#[derive(Debug, Clone)] +pub struct ReplayMemoryOverlay { + pub addr: u64, + pub size: u32, + pub name: String, +} + +pub fn seed_replay_state_for_arch<'ctx>( + state: &mut SymState<'ctx>, + prepared: Option<&SsaArtifact>, + arch: Option<&ArchSpec>, + seed: &ReplaySeed, +) { + if let Some(prepared) = prepared { + seed_memory_regions_for_arch(state, prepared, arch); + } + apply_replay_seed_to_state(state, prepared, arch, seed); +} + +pub fn apply_replay_seed_to_state<'ctx>( + state: &mut SymState<'ctx>, + prepared: Option<&SsaArtifact>, + arch: Option<&ArchSpec>, + seed: &ReplaySeed, +) { + if let Some(entry_pc) = seed.entry_pc { + state.pc = entry_pc; + } + + let register_layout = ReplayRegisterLayout::from_prepared(prepared); + for register in &seed.registers { + let (seed_name, bits) = register_layout + .resolve_register(®ister.name) + .unwrap_or_else(|| { + ( + register.name.to_ascii_uppercase(), + default_register_bits(arch), + ) + }); + state.set_concrete(&seed_name, register.value, bits); + } + + for (index, window) in seed.memory.iter().enumerate() { + if window.bytes.is_empty() { + continue; + } + let name = window + .label + .clone() + .filter(|label| !label.is_empty()) + .unwrap_or_else(|| format!("replay_{index:x}_{:x}", window.addr)); + let region_id = state.define_memory_region( + MemoryRegionKind::Replay, + &name, + Some(window.addr), + Some(window.bytes.len() as u64), + ); + state.seed_region_bytes(region_id, 0, &window.bytes); + } + + for overlay in &seed.register_overlays { + let (seed_name, bits) = register_layout + .resolve_register(&overlay.name) + .unwrap_or_else(|| { + ( + overlay.name.to_ascii_uppercase(), + default_register_bits(arch), + ) + }); + state.make_symbolic_named(&seed_name, &overlay.symbol, bits); + } + + for overlay in &seed.memory_overlays { + if overlay.size == 0 { + continue; + } + state.make_symbolic_memory(overlay.addr, overlay.size, &overlay.name); + } + + for fd in &seed.tty_fds { + state.set_tty_fd(*fd, true); + } + state.set_skip_sleep_calls(seed.skip_sleep_calls); +} + +fn default_register_bits(arch: Option<&ArchSpec>) -> u32 { + arch.map(|arch| arch.addr_size.max(1) * 8).unwrap_or(64) +} + +#[derive(Default)] +struct ReplayRegisterLayout { + by_name: BTreeMap, + alias_candidates: BTreeMap, +} + +impl ReplayRegisterLayout { + fn from_prepared(prepared: Option<&SsaArtifact>) -> Self { + let Some(prepared) = prepared else { + return Self::default(); + }; + + let mut by_name = BTreeMap::new(); + let mut alias_candidates = BTreeMap::new(); + let mut record_var = |var: &r2ssa::SSAVar| { + if !var.is_register() || var.version != 0 { + return; + } + let bits = var.size * 8; + let display_name = var.display_name(); + by_name + .entry(display_name.to_ascii_uppercase()) + .or_insert_with(|| (display_name.clone(), bits)); + let base_name = var.name.strip_prefix("reg:").unwrap_or(&var.name); + by_name + .entry(base_name.to_ascii_uppercase()) + .or_insert_with(|| (display_name.clone(), bits)); + if let Some(alias) = register_alias_spec(base_name) { + alias_candidates + .entry(display_name) + .or_insert((bits, alias)); + } + }; + + for block in prepared.blocks() { + block.for_each_def(|def| record_var(def.var)); + block.for_each_source(|src| record_var(src.var)); + } + + Self { + by_name, + alias_candidates, + } + } + + fn resolve_register(&self, name: &str) -> Option<(String, u32)> { + let key = name.trim().to_ascii_uppercase(); + self.by_name + .get(&key) + .cloned() + .or_else(|| self.resolve_alias_register(&key)) + } + + fn resolve_alias_register(&self, name: &str) -> Option<(String, u32)> { + let requested = register_alias_spec(name)?; + let requested_low = requested.offset_bits; + let requested_high = requested.offset_bits + requested.width_bits; + + let mut best: Option<(u8, u32, String, u32)> = None; + for (display_name, (bits, candidate)) in &self.alias_candidates { + if candidate.family != requested.family { + continue; + } + + let candidate_low = candidate.offset_bits; + let candidate_high = candidate.offset_bits + candidate.width_bits; + let relation_score = + if candidate_low == requested_low && candidate.width_bits == requested.width_bits { + 3 + } else if candidate_low <= requested_low && candidate_high >= requested_high { + 2 + } else if candidate_low == requested_low { + 1 + } else { + 0 + }; + if relation_score == 0 { + continue; + } + + let width_distance = candidate.width_bits.abs_diff(requested.width_bits); + let should_replace = + best.as_ref() + .is_none_or(|(best_score, best_distance, _, best_bits)| { + relation_score > *best_score + || (relation_score == *best_score && width_distance < *best_distance) + || (relation_score == *best_score + && width_distance == *best_distance + && *bits > *best_bits) + }); + if should_replace { + best = Some((relation_score, width_distance, display_name.clone(), *bits)); + } + } + + best.map(|(_, _, display_name, bits)| (display_name, bits)) + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +struct RegisterAliasSpec { + family: Cow<'static, str>, + offset_bits: u32, + width_bits: u32, +} + +fn register_alias_spec(base: &str) -> Option { + let upper = base.to_ascii_uppercase(); + let base = upper.as_str(); + let fixed = match base { + "AL" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RAX"), + offset_bits: 0, + width_bits: 8, + }), + "AH" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RAX"), + offset_bits: 8, + width_bits: 8, + }), + "AX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RAX"), + offset_bits: 0, + width_bits: 16, + }), + "EAX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RAX"), + offset_bits: 0, + width_bits: 32, + }), + "RAX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RAX"), + offset_bits: 0, + width_bits: 64, + }), + "BL" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RBX"), + offset_bits: 0, + width_bits: 8, + }), + "BH" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RBX"), + offset_bits: 8, + width_bits: 8, + }), + "BX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RBX"), + offset_bits: 0, + width_bits: 16, + }), + "EBX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RBX"), + offset_bits: 0, + width_bits: 32, + }), + "RBX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RBX"), + offset_bits: 0, + width_bits: 64, + }), + "CL" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RCX"), + offset_bits: 0, + width_bits: 8, + }), + "CH" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RCX"), + offset_bits: 8, + width_bits: 8, + }), + "CX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RCX"), + offset_bits: 0, + width_bits: 16, + }), + "ECX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RCX"), + offset_bits: 0, + width_bits: 32, + }), + "RCX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RCX"), + offset_bits: 0, + width_bits: 64, + }), + "DL" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RDX"), + offset_bits: 0, + width_bits: 8, + }), + "DH" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RDX"), + offset_bits: 8, + width_bits: 8, + }), + "DX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RDX"), + offset_bits: 0, + width_bits: 16, + }), + "EDX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RDX"), + offset_bits: 0, + width_bits: 32, + }), + "RDX" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RDX"), + offset_bits: 0, + width_bits: 64, + }), + "SIL" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RSI"), + offset_bits: 0, + width_bits: 8, + }), + "SI" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RSI"), + offset_bits: 0, + width_bits: 16, + }), + "ESI" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RSI"), + offset_bits: 0, + width_bits: 32, + }), + "RSI" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RSI"), + offset_bits: 0, + width_bits: 64, + }), + "DIL" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RDI"), + offset_bits: 0, + width_bits: 8, + }), + "DI" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RDI"), + offset_bits: 0, + width_bits: 16, + }), + "EDI" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RDI"), + offset_bits: 0, + width_bits: 32, + }), + "RDI" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RDI"), + offset_bits: 0, + width_bits: 64, + }), + "BPL" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RBP"), + offset_bits: 0, + width_bits: 8, + }), + "BP" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RBP"), + offset_bits: 0, + width_bits: 16, + }), + "EBP" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RBP"), + offset_bits: 0, + width_bits: 32, + }), + "RBP" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RBP"), + offset_bits: 0, + width_bits: 64, + }), + "SPL" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RSP"), + offset_bits: 0, + width_bits: 8, + }), + "SP" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RSP"), + offset_bits: 0, + width_bits: 16, + }), + "ESP" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RSP"), + offset_bits: 0, + width_bits: 32, + }), + "RSP" => Some(RegisterAliasSpec { + family: Cow::Borrowed("RSP"), + offset_bits: 0, + width_bits: 64, + }), + _ => None, + }; + if fixed.is_some() { + return fixed; + } + + parse_numbered_x86_register_alias(base) +} + +fn parse_numbered_x86_register_alias(base: &str) -> Option { + let (family, width_bits) = if let Some(family) = base.strip_suffix('B') { + (family.to_string(), 8) + } else if let Some(family) = base.strip_suffix('W') { + (family.to_string(), 16) + } else if let Some(family) = base.strip_suffix('D') { + (family.to_string(), 32) + } else { + (base.to_string(), 64) + }; + + if !family.starts_with('R') { + return None; + } + let digits = &family[1..]; + if digits.is_empty() || !digits.chars().all(|c| c.is_ascii_digit()) { + return None; + } + + Some(RegisterAliasSpec { + family: Cow::Owned(family), + offset_bits: 0, + width_bits, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::value::SymValue; + use z3::Context; + + #[test] + fn replay_seed_imports_replay_regions_and_symbolic_overlays() { + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + let seed = ReplaySeed { + checkpoint_id: Some(7), + entry_pc: Some(0x4141), + registers: vec![ReplayRegisterValue { + name: "rax".to_string(), + value: 0x1122, + }], + memory: vec![ReplayMemoryWindow { + addr: 0x5000, + bytes: vec![0x41, 0x42, 0x43, 0x44], + label: Some("input_window".to_string()), + }], + register_overlays: vec![ReplayRegisterOverlay { + name: "rbx".to_string(), + symbol: "replay_rbx".to_string(), + }], + memory_overlays: vec![ReplayMemoryOverlay { + addr: 0x5001, + size: 2, + name: "user_buf".to_string(), + }], + tty_fds: vec![0], + skip_sleep_calls: true, + }; + + seed_replay_state_for_arch(&mut state, None, None, &seed); + + assert_eq!(state.pc, 0x4141); + assert_eq!(state.get_register("RAX").as_concrete(), Some(0x1122)); + assert!(state.get_register("RBX").is_symbolic()); + let concrete = state.mem_read(&SymValue::concrete(0x5000, 64), 1); + assert_eq!(concrete.as_concrete(), Some(0x41)); + let symbolic = state.mem_read(&SymValue::concrete(0x5001, 64), 2); + assert!(symbolic.is_symbolic()); + assert!(state.is_tty_fd(0)); + assert!(state.skip_sleep_calls()); + } +} diff --git a/crates/r2sym/src/runtime.rs b/crates/r2sym/src/runtime.rs index 172c81b..5b463d0 100644 --- a/crates/r2sym/src/runtime.rs +++ b/crates/r2sym/src/runtime.rs @@ -1,60 +1,64 @@ use std::collections::HashSet; use r2il::ArchSpec; -use r2ssa::{SSAVar, SsaArtifact}; +use r2ssa::{ObjectKind, SSAVar, SsaArtifact}; +use crate::memory::MemoryRegionKind; use crate::state::SymState; -pub fn seed_default_state_for_arch<'ctx>( - state: &mut SymState<'ctx>, - prepared: &SsaArtifact, - arch: Option<&ArchSpec>, -) { - let Some(arch) = arch else { - return; - }; +struct ArchSeedProfile { + arg_regs: &'static [&'static str], + stack_regs: &'static [&'static str], + stack_value: u64, +} +fn arch_seed_profile(arch: &ArchSpec) -> Option { let arch_name = arch.name.to_ascii_lowercase(); let looks_riscv = arch_name.contains("riscv") || arch_name.starts_with("rv"); - let (arg_regs, stack_regs, stack_value) = if arch_name == "x86-64" - || arch_name == "x86_64" - || (arch_name == "x86" && arch.addr_size == 8) + if arch_name == "x86-64" || arch_name == "x86_64" || (arch_name == "x86" && arch.addr_size == 8) { - ( - [ + Some(ArchSeedProfile { + arg_regs: &[ "RDI", "RSI", "RDX", "RCX", "R8", "R9", "EDI", "ESI", "EDX", "ECX", "R8D", "R9D", - ] - .as_slice(), - ["RSP", "RBP"].as_slice(), - 0x7fff_ffff_0000u64, - ) + ], + stack_regs: &["RSP", "RBP"], + stack_value: 0x7fff_ffff_0000u64, + }) } else if arch_name == "x86" { - ( - ["EAX", "EBX", "ECX", "EDX", "ESI", "EDI"].as_slice(), - ["ESP", "EBP"].as_slice(), - 0x7fff_0000u64, - ) + Some(ArchSeedProfile { + arg_regs: &["EAX", "EBX", "ECX", "EDX", "ESI", "EDI"], + stack_regs: &["ESP", "EBP"], + stack_value: 0x7fff_0000u64, + }) } else if looks_riscv && (arch.addr_size == 8 || arch_name.contains("64")) { - ( - [ + Some(ArchSeedProfile { + arg_regs: &[ "A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7", "X10", "X11", "X12", "X13", "X14", "X15", "X16", "X17", - ] - .as_slice(), - ["SP", "S0", "FP", "X2", "X8"].as_slice(), - 0x7fff_ffff_0000u64, - ) + ], + stack_regs: &["SP", "S0", "FP", "X2", "X8"], + stack_value: 0x7fff_ffff_0000u64, + }) } else if looks_riscv { - ( - [ + Some(ArchSeedProfile { + arg_regs: &[ "A0", "A1", "A2", "A3", "A4", "A5", "A6", "A7", "X10", "X11", "X12", "X13", "X14", "X15", "X16", "X17", - ] - .as_slice(), - ["SP", "S0", "FP", "X2", "X8"].as_slice(), - 0x7fff_0000u64, - ) + ], + stack_regs: &["SP", "S0", "FP", "X2", "X8"], + stack_value: 0x7fff_0000u64, + }) } else { + None + } +} + +pub fn seed_default_state_for_arch<'ctx>( + state: &mut SymState<'ctx>, + prepared: &SsaArtifact, + arch: Option<&ArchSpec>, +) { + let Some(profile) = arch.and_then(arch_seed_profile) else { return; }; @@ -72,12 +76,12 @@ pub fn seed_default_state_for_arch<'ctx>( } let bits = var.size * 8; - if stack_regs.contains(&base.as_str()) { - state.set_concrete(®_name, stack_value, bits); + if profile.stack_regs.contains(&base.as_str()) { + state.set_concrete(®_name, profile.stack_value, bits); return; } - if arg_regs.contains(&base.as_str()) { + if profile.arg_regs.contains(&base.as_str()) { let sym_name = base_name.to_ascii_lowercase(); state.make_symbolic_named(®_name, &sym_name, bits); } @@ -87,4 +91,62 @@ pub fn seed_default_state_for_arch<'ctx>( block.for_each_def(|def| maybe_seed(def.var)); block.for_each_source(|src| maybe_seed(src.var)); } + + seed_memory_regions_for_arch(state, prepared, arch); +} + +const STACK_WINDOW_BELOW: u64 = 0x8000; +const STACK_WINDOW_SIZE: u64 = 0x10000; +const GLOBAL_TAIL_EXTENT: u64 = 0x1000; + +pub fn seed_memory_regions_for_arch<'ctx>( + state: &mut SymState<'ctx>, + prepared: &SsaArtifact, + arch: Option<&ArchSpec>, +) { + let stack_value = arch + .and_then(arch_seed_profile) + .map(|profile| profile.stack_value) + .unwrap_or(0); + let has_stack_objects = prepared.objects().objects.values().any(|object| { + matches!( + object.kind, + ObjectKind::StackSlot { .. } | ObjectKind::FrameObject { .. } + ) + }); + if has_stack_objects && stack_value != 0 { + let stack_base = stack_value.saturating_sub(STACK_WINDOW_BELOW); + state.define_memory_region( + MemoryRegionKind::Stack, + "stack_window", + Some(stack_base), + Some(STACK_WINDOW_SIZE), + ); + } + + let mut globals = prepared + .objects() + .objects + .values() + .filter_map(|object| match &object.kind { + ObjectKind::Global { address, .. } => Some(*address), + _ => None, + }) + .collect::>(); + globals.sort_unstable(); + globals.dedup(); + + for (index, address) in globals.iter().copied().enumerate() { + let next = globals.get(index + 1).copied(); + let extent = next + .and_then(|next| next.checked_sub(address)) + .filter(|extent| *extent > 0) + .unwrap_or(GLOBAL_TAIL_EXTENT); + state.define_memory_region( + MemoryRegionKind::Global, + &format!("global_{address:x}"), + Some(address), + Some(extent), + ); + } } diff --git a/crates/r2sym/src/semantics/artifact.rs b/crates/r2sym/src/semantics/artifact.rs new file mode 100644 index 0000000..ee32217 --- /dev/null +++ b/crates/r2sym/src/semantics/artifact.rs @@ -0,0 +1,60 @@ +use serde::{Deserialize, Serialize}; + +use crate::sim::DerivedSummaryDiagnostics; + +use super::facts::SymbolicFunctionFacts; +use super::vm::{InterpreterDispatchSummary, VmStepSummary}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum SemanticMode { + Raw, + Compiled, + Residual, + VmSummary, +} + +pub type CompiledSemanticMode = SemanticMode; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum SliceClass { + Wrapper, + Worker, + RecursiveGroup, + InterpreterSwitch, + InterpreterIndirect, + GenericLarge, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum ResidualReason { + MissingArch, + LargeCfg, + SummaryBudgetExhausted, + SccBudgetExhausted, + InterpreterRequiresStepSummary, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct SemanticCapability { + pub query_ready: bool, + pub type_ready: bool, + pub decompile_ready: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CompiledSemanticArtifact { + pub mode: SemanticMode, + pub slice_class: SliceClass, + pub capability: SemanticCapability, + pub residual_reasons: Vec, + pub closure_functions: usize, + pub helper_functions: usize, + pub derived_summaries: usize, + pub derived_diagnostics: DerivedSummaryDiagnostics, + pub symbolic_facts: SymbolicFunctionFacts, + pub interpreter: Option, + pub vm_step: Option, + pub cache_hit: bool, +} + +pub type CompiledFunctionSemantics = CompiledSemanticArtifact; diff --git a/crates/r2sym/src/semantics/cache.rs b/crates/r2sym/src/semantics/cache.rs new file mode 100644 index 0000000..5cae7dd --- /dev/null +++ b/crates/r2sym/src/semantics/cache.rs @@ -0,0 +1,126 @@ +use std::collections::HashMap; +use std::fmt::Write as _; +use std::hash::{DefaultHasher, Hash, Hasher}; +use std::sync::{Arc, OnceLock, RwLock}; + +use r2il::ArchSpec; +use r2ssa::SsaArtifact; + +use crate::sim::{PreparedFunctionScope, SummaryProfile}; + +use super::artifact::CompiledSemanticArtifact; + +const SEMANTIC_CACHE_LIMIT: usize = 128; + +#[allow(dead_code)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum SemanticSeedMode { + Static, + Replay, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) struct SemanticCacheKey { + pub root_addr: u64, + pub scope_hash: u64, + pub arch_hash: u64, + pub summary_profile: SummaryProfile, + pub seed_mode: SemanticSeedMode, +} + +#[derive(Debug, Clone)] +pub(crate) struct SemanticCompilationResult { + pub artifact: Arc, + pub cache_hit: bool, +} + +fn hash_debug_value(value: &T) -> u64 { + let mut hasher = DefaultHasher::new(); + let _ = write!(&mut HasherAdapter(&mut hasher), "{value:?}"); + hasher.finish() +} + +struct HasherAdapter<'a>(&'a mut DefaultHasher); + +impl std::fmt::Write for HasherAdapter<'_> { + fn write_str(&mut self, s: &str) -> std::fmt::Result { + self.0.write(s.as_bytes()); + Ok(()) + } +} + +fn arch_hash(arch: Option<&ArchSpec>) -> u64 { + arch.map(hash_debug_value).unwrap_or(0) +} + +fn function_hash(func: &SsaArtifact) -> u64 { + let mut hasher = DefaultHasher::new(); + func.entry.hash(&mut hasher); + hash_debug_value(&func.local_ssa_blocks()).hash(&mut hasher); + hasher.finish() +} + +pub fn stable_scope_hash(scope: Option<&PreparedFunctionScope>) -> u64 { + let Some(scope) = scope else { + return 0; + }; + let mut hasher = DefaultHasher::new(); + scope.root_id().hash(&mut hasher); + for function in scope.functions().values() { + function.id.hash(&mut hasher); + function.name.hash(&mut hasher); + function.prepared.function().entry.hash(&mut hasher); + hash_debug_value(&function.prepared.local_ssa_blocks()).hash(&mut hasher); + } + hasher.finish() +} + +pub(crate) fn semantic_cache_key( + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + arch: Option<&ArchSpec>, + summary_profile: SummaryProfile, + seed_mode: SemanticSeedMode, +) -> SemanticCacheKey { + SemanticCacheKey { + root_addr: func.entry, + scope_hash: if scope.is_some() { + stable_scope_hash(scope) + } else { + function_hash(func) + }, + arch_hash: arch_hash(arch), + summary_profile, + seed_mode, + } +} + +fn semantic_cache() -> &'static RwLock>> { + static CACHE: OnceLock>>> = + OnceLock::new(); + CACHE.get_or_init(|| RwLock::new(HashMap::new())) +} + +pub(crate) fn lookup_semantic_cache( + key: &SemanticCacheKey, +) -> Option> { + semantic_cache() + .read() + .expect("semantic cache read lock poisoned") + .get(key) + .cloned() +} + +pub(crate) fn cache_insert_bounded( + key: SemanticCacheKey, + value: Arc, +) -> Arc { + let mut guard = semantic_cache() + .write() + .expect("semantic cache write lock poisoned"); + if guard.len() >= SEMANTIC_CACHE_LIMIT { + guard.clear(); + } + guard.insert(key, value.clone()); + value +} diff --git a/crates/r2sym/src/semantics/classify.rs b/crates/r2sym/src/semantics/classify.rs new file mode 100644 index 0000000..9658606 --- /dev/null +++ b/crates/r2sym/src/semantics/classify.rs @@ -0,0 +1,32 @@ +use r2ssa::SsaArtifact; + +use crate::sim::DerivedSummaryDiagnostics; + +use super::artifact::SliceClass; +use super::vm::{InterpreterDispatchSummary, InterpreterKind}; + +pub(super) fn classify_slice( + func: &SsaArtifact, + helper_functions: usize, + derived_diagnostics: &DerivedSummaryDiagnostics, + interpreter: Option<&InterpreterDispatchSummary>, +) -> SliceClass { + if let Some(interpreter) = interpreter { + return match interpreter.kind { + InterpreterKind::SwitchDispatch => SliceClass::InterpreterSwitch, + InterpreterKind::IndirectDispatch => SliceClass::InterpreterIndirect, + }; + } + + let cfg = func.function().cfg_risk_summary(); + if derived_diagnostics.max_scc_size > 1 { + return SliceClass::RecursiveGroup; + } + if cfg.block_count <= 12 && cfg.loop_count == 0 && helper_functions <= 2 { + return SliceClass::Wrapper; + } + if cfg.block_count > 64 || cfg.loop_count > 0 || helper_functions > 3 { + return SliceClass::Worker; + } + SliceClass::GenericLarge +} diff --git a/crates/r2sym/src/semantics/compiler.rs b/crates/r2sym/src/semantics/compiler.rs new file mode 100644 index 0000000..075df12 --- /dev/null +++ b/crates/r2sym/src/semantics/compiler.rs @@ -0,0 +1,774 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use r2il::ArchSpec; +use r2ssa::SsaArtifact; +use z3::Context; + +use crate::sim::{ + DerivedSummaryDiagnostics, PreparedFunctionScope, SummaryProfile, SummaryRegistry, +}; + +use super::artifact::{ + CompiledFunctionSemantics, CompiledSemanticArtifact, ResidualReason, SemanticCapability, + SemanticMode, +}; +use super::cache::{ + SemanticCompilationResult, SemanticSeedMode, cache_insert_bounded, lookup_semantic_cache, + semantic_cache_key, +}; +use super::classify::classify_slice; +use super::facts::{ + SymbolicFunctionFacts, collect_symbolic_function_facts_with_derived, + collect_symbolic_function_facts_with_scope, +}; +use super::vm::{build_vm_step_summary, classify_interpreter_like}; + +fn residual_reasons( + diagnostics: &DerivedSummaryDiagnostics, + facts: &SymbolicFunctionFacts, + interpreter_detected: bool, + vm_step_ready: bool, +) -> Vec { + let mut reasons = Vec::new(); + if facts.diagnostics.skipped_missing_arch { + reasons.push(ResidualReason::MissingArch); + } + if facts.diagnostics.skipped_large_cfg { + reasons.push(ResidualReason::LargeCfg); + } + if diagnostics.budget_exhausted > 0 { + reasons.push(ResidualReason::SummaryBudgetExhausted); + } + if diagnostics.scc_budget_exhausted > 0 { + reasons.push(ResidualReason::SccBudgetExhausted); + } + if interpreter_detected && !vm_step_ready { + reasons.push(ResidualReason::InterpreterRequiresStepSummary); + } + reasons +} + +fn semantic_mode_for( + helper_functions: usize, + derived_summaries: usize, + diagnostics: &DerivedSummaryDiagnostics, + facts: &SymbolicFunctionFacts, + interpreter_detected: bool, + vm_step_ready: bool, +) -> SemanticMode { + if vm_step_ready { + return SemanticMode::VmSummary; + } + if interpreter_detected { + return SemanticMode::Residual; + } + if derived_summaries > 0 && !facts.diagnostics.skipped_large_cfg { + return SemanticMode::Compiled; + } + if helper_functions > 0 + || facts.diagnostics.skipped_large_cfg + || diagnostics.budget_exhausted > 0 + || diagnostics.scc_budget_exhausted > 0 + { + return SemanticMode::Residual; + } + if facts.diagnostics.skipped_missing_arch { + return SemanticMode::Residual; + } + SemanticMode::Raw +} + +fn semantic_capability(mode: SemanticMode, facts: &SymbolicFunctionFacts) -> SemanticCapability { + match mode { + SemanticMode::Raw | SemanticMode::Compiled => SemanticCapability { + query_ready: true, + type_ready: true, + decompile_ready: true, + }, + SemanticMode::Residual => SemanticCapability { + query_ready: true, + type_ready: !facts.diagnostics.skipped_large_cfg, + decompile_ready: false, + }, + SemanticMode::VmSummary => SemanticCapability { + query_ready: !facts.diagnostics.skipped_large_cfg, + type_ready: true, + decompile_ready: false, + }, + } +} + +fn compile_function_semantics_uncached( + ctx: &Context, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + arch: Option<&ArchSpec>, + symbol_map: &HashMap, + summary_profile: SummaryProfile, +) -> CompiledSemanticArtifact { + let closure_functions = scope.map(|scope| scope.functions().len()).unwrap_or(1); + let helper_functions = scope + .map(|scope| scope.helper_functions().count()) + .unwrap_or(0); + let mut derived_diagnostics = DerivedSummaryDiagnostics::default(); + let mut derived_summaries = 0usize; + let mut symbolic_facts = SymbolicFunctionFacts::default(); + + if let Some(arch) = arch { + let cfg_summary = func.function().cfg_risk_summary(); + let skip_expensive_branch_compilation = cfg_summary.block_count > 48 + || cfg_summary.back_edge_count > 4 + || cfg_summary.switch_block_count > 4; + + if let Some(scope) = scope + && let Some(registry) = SummaryRegistry::with_profile_for_arch(arch, summary_profile) + { + let derived = registry.derive_symbolic_summaries(ctx, scope, Some(arch), symbol_map); + derived_summaries = derived.summaries.len(); + derived_diagnostics = derived.diagnostics.clone(); + if skip_expensive_branch_compilation { + symbolic_facts.diagnostics.skipped_large_cfg = true; + } else { + symbolic_facts = collect_symbolic_function_facts_with_derived( + ctx, + func, + Some(scope), + arch, + symbol_map, + summary_profile, + ®istry, + &derived, + ); + } + } else if skip_expensive_branch_compilation { + symbolic_facts.diagnostics.skipped_large_cfg = true; + } else { + symbolic_facts = + collect_symbolic_function_facts_with_scope(ctx, func, None, Some(arch), symbol_map); + } + } else { + symbolic_facts.diagnostics.skipped_missing_arch = true; + } + + let interpreter = classify_interpreter_like(func); + let vm_step = interpreter + .as_ref() + .and_then(|dispatch| build_vm_step_summary(func, dispatch)); + let mode = semantic_mode_for( + helper_functions, + derived_summaries, + &derived_diagnostics, + &symbolic_facts, + interpreter.is_some(), + vm_step.is_some(), + ); + let slice_class = classify_slice( + func, + helper_functions, + &derived_diagnostics, + interpreter.as_ref(), + ); + + CompiledSemanticArtifact { + mode, + slice_class, + capability: semantic_capability(mode, &symbolic_facts), + residual_reasons: residual_reasons( + &derived_diagnostics, + &symbolic_facts, + interpreter.is_some(), + vm_step.is_some(), + ), + closure_functions, + helper_functions, + derived_summaries, + derived_diagnostics, + symbolic_facts, + interpreter, + vm_step, + cache_hit: false, + } +} + +pub(crate) fn compile_function_semantics_cached_with_scope( + ctx: &Context, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + arch: Option<&ArchSpec>, + symbol_map: &HashMap, + summary_profile: SummaryProfile, +) -> SemanticCompilationResult { + let key = semantic_cache_key(func, scope, arch, summary_profile, SemanticSeedMode::Static); + if let Some(existing) = lookup_semantic_cache(&key) { + return SemanticCompilationResult { + artifact: existing, + cache_hit: true, + }; + } + + let artifact = Arc::new(compile_function_semantics_uncached( + ctx, + func, + scope, + arch, + symbol_map, + summary_profile, + )); + let artifact = cache_insert_bounded(key, artifact); + SemanticCompilationResult { + artifact, + cache_hit: false, + } +} + +pub fn compile_function_semantics_with_scope( + ctx: &Context, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + arch: Option<&ArchSpec>, + symbol_map: &HashMap, + summary_profile: SummaryProfile, +) -> CompiledFunctionSemantics { + let result = compile_function_semantics_cached_with_scope( + ctx, + func, + scope, + arch, + symbol_map, + summary_profile, + ); + let mut artifact = (*result.artifact).clone(); + artifact.cache_hit = result.cache_hit; + artifact +} + +pub fn compile_semantic_artifact_with_scope( + ctx: &Context, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + arch: Option<&ArchSpec>, + symbol_map: &HashMap, + summary_profile: SummaryProfile, +) -> CompiledSemanticArtifact { + compile_function_semantics_with_scope(ctx, func, scope, arch, symbol_map, summary_profile) +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use r2il::{ + ArchSpec, R2ILBlock, R2ILOp, RegisterDef, SpaceId, SwitchCase, SwitchInfo, Varnode, + }; + use r2ssa::SsaArtifact; + use z3::Context; + + use super::*; + + const RAX: u64 = 0; + + fn test_arch() -> ArchSpec { + let mut arch = ArchSpec::new("x86-64"); + arch.addr_size = 8; + arch.add_register(RegisterDef::new("RAX", RAX, 8)); + arch + } + + fn make_reg(offset: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Register, + offset, + size, + meta: None, + } + } + + fn make_const(value: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Const, + offset: value, + size, + meta: None, + } + } + + #[test] + fn compile_semantics_cache_hits_on_repeat() { + let blocks = vec![R2ILBlock { + addr: 0x1000, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }]; + let func = SsaArtifact::for_symbolic(&blocks, Some(&test_arch())).expect("ssa"); + let ctx = Context::thread_local(); + let first = compile_function_semantics_cached_with_scope( + &ctx, + &func, + None, + Some(&test_arch()), + &HashMap::new(), + SummaryProfile::Default, + ); + let second = compile_function_semantics_cached_with_scope( + &ctx, + &func, + None, + Some(&test_arch()), + &HashMap::new(), + SummaryProfile::Default, + ); + assert!(!first.cache_hit); + assert!(second.cache_hit); + } + + #[test] + fn interpreter_classifier_marks_switch_loop_vm_summary() { + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x1004, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + switch_info: Some(SwitchInfo { + switch_addr: 0x1004, + min_val: 0, + max_val: 4, + default_target: Some(0x1018), + cases: vec![ + SwitchCase { + value: 0, + target: 0x1008, + }, + SwitchCase { + value: 1, + target: 0x100c, + }, + SwitchCase { + value: 2, + target: 0x1010, + }, + SwitchCase { + value: 3, + target: 0x1014, + }, + ], + }), + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1008, + size: 4, + ops: vec![ + R2ILOp::IntAdd { + dst: make_reg(RAX, 8), + a: make_reg(RAX, 8), + b: make_const(1, 8), + }, + R2ILOp::Branch { + target: make_const(0x1004, 8), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x100c, + size: 4, + ops: vec![ + R2ILOp::IntSub { + dst: make_reg(RAX, 8), + a: make_reg(RAX, 8), + b: make_const(1, 8), + }, + R2ILOp::Branch { + target: make_const(0x1004, 8), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 4, + ops: vec![ + R2ILOp::IntXor { + dst: make_reg(RAX, 8), + a: make_reg(RAX, 8), + b: make_const(0x55, 8), + }, + R2ILOp::Branch { + target: make_const(0x1004, 8), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1014, + size: 4, + ops: vec![ + R2ILOp::IntLeft { + dst: make_reg(RAX, 8), + a: make_reg(RAX, 8), + b: make_const(1, 8), + }, + R2ILOp::Branch { + target: make_const(0x1004, 8), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1018, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x1004, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, Some(&test_arch())).expect("ssa"); + let ctx = Context::thread_local(); + let artifact = compile_function_semantics_with_scope( + &ctx, + &func, + None, + Some(&test_arch()), + &HashMap::new(), + SummaryProfile::Default, + ); + assert_eq!(artifact.mode, SemanticMode::VmSummary); + assert_eq!( + artifact.slice_class, + super::super::artifact::SliceClass::InterpreterSwitch + ); + assert!(artifact.interpreter.is_some()); + let vm_step = artifact.vm_step.expect("vm step"); + assert_eq!(vm_step.loop_header, 0x1004); + assert_eq!(vm_step.default_target, Some(0x1018)); + assert_eq!(vm_step.case_values_by_target.get(&0x1008), Some(&vec![0])); + assert!(!vm_step.handler_state_updates.is_empty()); + assert!(!vm_step.transfers.is_empty()); + assert!( + vm_step + .handler_state_updates + .values() + .flat_map(|updates| updates.iter()) + .any(|update| update.output.starts_with("RAX_")) + ); + assert!( + vm_step + .transfers + .iter() + .any(|transfer| !transfer.case_values.is_empty()) + ); + } + + #[test] + fn interpreter_without_step_summary_stays_residual() { + let blocks = vec![ + R2ILBlock { + addr: 0x2000, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x2004, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x2004, + size: 4, + ops: vec![], + switch_info: Some(SwitchInfo { + switch_addr: 0x2004, + min_val: 0, + max_val: 4, + default_target: Some(0x2018), + cases: vec![ + SwitchCase { + value: 0, + target: 0x2008, + }, + SwitchCase { + value: 1, + target: 0x200c, + }, + SwitchCase { + value: 2, + target: 0x2010, + }, + SwitchCase { + value: 3, + target: 0x2014, + }, + ], + }), + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x2008, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x2004, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x200c, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x2004, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x2010, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x2004, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x2014, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x2004, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x2018, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x2004, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, Some(&test_arch())).expect("ssa"); + let ctx = Context::thread_local(); + let artifact = compile_function_semantics_with_scope( + &ctx, + &func, + None, + Some(&test_arch()), + &HashMap::new(), + SummaryProfile::Default, + ); + assert_eq!(artifact.mode, SemanticMode::Residual); + assert!(artifact.interpreter.is_some()); + assert!(artifact.vm_step.is_none()); + assert!( + artifact + .residual_reasons + .contains(&ResidualReason::InterpreterRequiresStepSummary) + ); + } + + #[test] + fn vm_step_summary_tracks_case_values_and_handler_regions() { + let blocks = vec![ + R2ILBlock { + addr: 0x3000, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x3004, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x3004, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + switch_info: Some(SwitchInfo { + switch_addr: 0x3004, + min_val: 0, + max_val: 4, + default_target: Some(0x3018), + cases: vec![ + SwitchCase { + value: 0, + target: 0x3008, + }, + SwitchCase { + value: 1, + target: 0x300c, + }, + SwitchCase { + value: 2, + target: 0x3010, + }, + SwitchCase { + value: 3, + target: 0x3014, + }, + ], + }), + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x3008, + size: 4, + ops: vec![ + R2ILOp::Load { + dst: make_reg(RAX, 8), + space: SpaceId::Ram, + addr: make_reg(RAX, 8), + }, + R2ILOp::Branch { + target: make_const(0x3004, 8), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x300c, + size: 4, + ops: vec![R2ILOp::Return { + target: make_reg(RAX, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x3010, + size: 4, + ops: vec![ + R2ILOp::IntAdd { + dst: make_reg(RAX, 8), + a: make_reg(RAX, 8), + b: make_const(1, 8), + }, + R2ILOp::Branch { + target: make_const(0x3004, 8), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x3014, + size: 4, + ops: vec![ + R2ILOp::IntSub { + dst: make_reg(RAX, 8), + a: make_reg(RAX, 8), + b: make_const(1, 8), + }, + R2ILOp::Branch { + target: make_const(0x3004, 8), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x3018, + size: 4, + ops: vec![R2ILOp::CBranch { + target: make_const(0x3004, 8), + cond: make_reg(RAX, 1), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x301c, + size: 4, + ops: vec![R2ILOp::Return { + target: make_reg(RAX, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, Some(&test_arch())).expect("ssa"); + let ctx = Context::thread_local(); + let artifact = compile_function_semantics_with_scope( + &ctx, + &func, + None, + Some(&test_arch()), + &HashMap::new(), + SummaryProfile::Default, + ); + + let vm_step = artifact.vm_step.expect("vm step summary"); + assert_eq!(artifact.mode, SemanticMode::VmSummary); + assert_eq!(vm_step.loop_header, 0x3004); + assert_eq!(vm_step.dispatch_header, 0x3004); + assert_eq!(vm_step.default_target, Some(0x3018)); + assert_eq!(vm_step.case_values_by_target.get(&0x3008), Some(&vec![0])); + assert_eq!(vm_step.case_values_by_target.get(&0x300c), Some(&vec![1])); + assert_eq!(vm_step.case_values_by_target.get(&0x3010), Some(&vec![2])); + assert_eq!(vm_step.case_values_by_target.get(&0x3014), Some(&vec![3])); + assert_eq!(vm_step.handler_regions.get(&0x3008), Some(&vec![0x3008])); + assert_eq!(vm_step.handler_memory_reads.get(&0x3008), Some(&1)); + assert_eq!(vm_step.handler_memory_writes.get(&0x3008), Some(&0)); + assert!( + vm_step + .handler_state_updates + .get(&0x3008) + .is_some_and(|updates| !updates.is_empty()) + ); + assert!(vm_step.redispatch_handlers.contains(&0x3008)); + assert!(vm_step.redispatch_handlers.contains(&0x3010)); + assert!(vm_step.redispatch_handlers.contains(&0x3014)); + assert!(vm_step.redispatch_handlers.contains(&0x3018)); + assert!(vm_step.returning_handlers.contains(&0x300c)); + assert!(vm_step.returning_handlers.contains(&0x3018)); + assert_eq!(vm_step.handler_conditional_branches.get(&0x3018), Some(&1)); + assert_eq!( + vm_step.handler_regions.get(&0x3018), + Some(&vec![0x3018, 0x301c]) + ); + assert_eq!( + vm_step.handler_exit_targets.get(&0x3018), + Some(&vec![0x3004]) + ); + assert!(vm_step.truncated_handlers.is_empty()); + assert_eq!(vm_step.transfers.len(), 5); + assert!( + vm_step + .transfers + .iter() + .any(|transfer| transfer.handler_target == 0x3008 && transfer.redispatch) + ); + assert!( + vm_step + .transfers + .iter() + .any(|transfer| !transfer.state_updates.is_empty()) + ); + } +} diff --git a/crates/r2sym/src/semantics/facts.rs b/crates/r2sym/src/semantics/facts.rs new file mode 100644 index 0000000..fdb53ae --- /dev/null +++ b/crates/r2sym/src/semantics/facts.rs @@ -0,0 +1,560 @@ +use std::collections::{BTreeSet, HashMap}; + +use r2il::ArchSpec; +use r2ssa::SsaArtifact; +use serde::{Deserialize, Serialize}; +use z3::Context; + +use crate::SymState; +use crate::backward::{ + BackwardConditionPrecision, BackwardConditionSummary, + compile_branch_precondition_with_summaries, +}; +use crate::path::{ExploreConfig, PathExplorer}; +use crate::runtime::seed_default_state_for_arch; +use crate::sim::{DerivedSummarySet, PreparedFunctionScope, SummaryProfile, SummaryRegistry}; +use crate::solver::SatResult; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum SymbolicReachabilityStatus { + Reachable, + Unreachable, + Unknown, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SymbolicBranchFact { + pub block_addr: u64, + pub true_target: u64, + pub false_target: u64, + pub true_status: SymbolicReachabilityStatus, + pub false_status: SymbolicReachabilityStatus, + pub true_condition: Option, + pub false_condition: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub true_compiled: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub false_compiled: Option, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct SymbolicFunctionFactDiagnostics { + pub branches_evaluated: usize, + pub branches_pruned: usize, + pub branches_unknown: usize, + pub skipped_missing_arch: bool, + pub skipped_large_cfg: bool, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct SymbolicFunctionFacts { + pub branch_facts: Vec, + pub diagnostics: SymbolicFunctionFactDiagnostics, +} + +impl SymbolicFunctionFacts { + pub fn branch_fact_for_block(&self, block_addr: u64) -> Option<&SymbolicBranchFact> { + self.branch_facts + .iter() + .find(|fact| fact.block_addr == block_addr) + } +} + +fn symbolic_condition_hint(summary: Option<&BackwardConditionSummary>) -> Option { + summary + .map(|compiled| compiled.simplified.trim().to_string()) + .filter(|text| !text.is_empty() && text != "true") +} + +fn symbolic_fact_explorer<'ctx>(ctx: &'ctx Context) -> PathExplorer<'ctx> { + let mut explorer = PathExplorer::with_config( + ctx, + ExploreConfig { + subsumption_states: true, + max_states: 256, + max_depth: 96, + max_completed_paths: Some(8), + merge_states: false, + ..ExploreConfig::default() + }, + ); + explorer.set_target_guided_queries(true); + explorer +} + +fn symbolic_reachability_status( + feasible_paths: usize, + budget_exhausted: bool, +) -> SymbolicReachabilityStatus { + if feasible_paths > 0 { + SymbolicReachabilityStatus::Reachable + } else if budget_exhausted { + SymbolicReachabilityStatus::Unknown + } else { + SymbolicReachabilityStatus::Unreachable + } +} + +fn install_derived_summary_set<'ctx>( + explorer: &mut PathExplorer<'ctx>, + registry: &SummaryRegistry<'ctx>, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + derived: &DerivedSummarySet<'ctx>, + symbol_map: &HashMap, +) { + let prepared = scope + .and_then(|scope| scope.root()) + .map(|root| &root.prepared) + .unwrap_or(func); + let _ = registry.install_interproc_summaries_for_function( + explorer, + prepared, + &derived.interproc, + symbol_map, + ); + let _ = registry.install_derived_summaries_for_function( + explorer, + prepared, + &derived.summaries, + symbol_map, + ); + let _ = registry.install_known_symbols_for_function(explorer, prepared, symbol_map); +} + +fn install_symbolic_fact_hooks<'ctx>( + ctx: &'ctx Context, + explorer: &mut PathExplorer<'ctx>, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + arch: &ArchSpec, + summary_profile: SummaryProfile, + symbol_map: &HashMap, +) { + let Some(registry) = SummaryRegistry::with_profile_for_arch(arch, summary_profile) else { + return; + }; + if let Some(scope) = scope { + let derived = registry.derive_symbolic_summaries(ctx, scope, Some(arch), symbol_map); + install_derived_summary_set(explorer, ®istry, func, Some(scope), &derived, symbol_map); + return; + } + let _ = registry.install_known_symbols_for_function(explorer, func, symbol_map); +} + +fn compiled_branch_reachability_status<'ctx>( + explorer: &PathExplorer<'ctx>, + func: &SsaArtifact, + initial_state: &SymState<'ctx>, + block_addr: u64, + truth: bool, +) -> ( + Option, + Option, +) { + let derived_summaries = explorer.derived_call_summary_views(); + if func_contains_calls(func) && derived_summaries.is_empty() { + return (None, None); + } + let Some(compiled) = compile_branch_precondition_with_summaries( + func, + initial_state, + block_addr, + truth, + &derived_summaries, + ) else { + return (None, None); + }; + let summary = compiled.summary; + if !matches!(summary.precision, BackwardConditionPrecision::Exact) { + return (None, Some(summary)); + } + let status = match explorer + .solver() + .sat_with_constraint(initial_state, &compiled.predicate) + { + SatResult::Sat => Some(SymbolicReachabilityStatus::Reachable), + SatResult::Unsat => Some(SymbolicReachabilityStatus::Unreachable), + SatResult::Unknown => None, + }; + (status, Some(summary)) +} + +fn func_contains_calls(func: &SsaArtifact) -> bool { + func.blocks().any(|block| { + block + .ops + .iter() + .any(|op| matches!(op, r2ssa::SSAOp::Call { .. } | r2ssa::SSAOp::CallInd { .. })) + }) +} + +fn local_memory_store_value_ids( + func: &SsaArtifact, + inst_id: r2ssa::graph::InstId, + size: u32, +) -> Vec { + let Some(uses) = func.memory().uses_by_inst.get(&inst_id) else { + return Vec::new(); + }; + let mut values = Vec::new(); + for use_fact in uses { + if use_fact.location.size != size { + continue; + } + for (def_inst, defs) in &func.memory().defs_by_inst { + for def in defs { + if def.next_version != use_fact.version || def.location != use_fact.location { + continue; + } + let Some(inst) = func.graph().inst(*def_inst) else { + continue; + }; + let r2ssa::graph::InstPayload::Op(r2ssa::SSAOp::Store { val, .. }) = &inst.payload + else { + continue; + }; + if let Some(value_id) = func.graph().value_id_for_var(val) { + values.push(value_id); + } + } + } + } + values +} + +fn value_depends_on_call_result( + func: &SsaArtifact, + value_id: r2ssa::graph::ValueId, + visited: &mut BTreeSet, +) -> bool { + if !visited.insert(value_id) { + return false; + } + + let Some(inst_id) = func.graph().def_inst(value_id) else { + return false; + }; + let Some(inst) = func.graph().inst(inst_id) else { + return false; + }; + + match &inst.payload { + r2ssa::graph::InstPayload::Phi { .. } => inst + .inputs + .iter() + .copied() + .any(|input| value_depends_on_call_result(func, input, visited)), + r2ssa::graph::InstPayload::Op(op) => match op { + r2ssa::SSAOp::CallDefine { .. } => true, + r2ssa::SSAOp::Load { dst, .. } => local_memory_store_value_ids(func, inst_id, dst.size) + .into_iter() + .any(|stored| value_depends_on_call_result(func, stored, visited)), + _ => inst + .inputs + .iter() + .copied() + .any(|input| value_depends_on_call_result(func, input, visited)), + }, + } +} + +fn predicate_depends_on_call_result(func: &SsaArtifact, block_addr: u64) -> bool { + let Some(predicate) = func + .predicates() + .predicates + .values() + .find(|fact| fact.block_addr == block_addr) + else { + return false; + }; + let mut visited = BTreeSet::new(); + value_depends_on_call_result(func, predicate.condition, &mut visited) +} + +#[allow(clippy::too_many_arguments)] +pub(super) fn collect_symbolic_function_facts_with_derived<'ctx>( + ctx: &'ctx Context, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + arch: &ArchSpec, + symbol_map: &HashMap, + summary_profile: SummaryProfile, + registry: &SummaryRegistry<'ctx>, + derived: &DerivedSummarySet<'ctx>, +) -> SymbolicFunctionFacts { + let mut facts = SymbolicFunctionFacts::default(); + let branch_blocks = func + .cfg() + .block_addrs() + .filter_map(|block_addr| { + let block = func.cfg().get_block(block_addr)?; + match block.terminator { + r2ssa::BlockTerminator::ConditionalBranch { + true_target, + false_target, + } => Some((block_addr, true_target, false_target)), + _ => None, + } + }) + .collect::>(); + + for (block_addr, true_target, false_target) in branch_blocks { + facts.diagnostics.branches_evaluated += 1; + let predicate_uses_call_result = predicate_depends_on_call_result(func, block_addr); + + let make_state = || { + let mut state = SymState::new(ctx, func.entry); + seed_default_state_for_arch(&mut state, func, Some(arch)); + state + }; + + let mut true_explorer = symbolic_fact_explorer(ctx); + install_derived_summary_set( + &mut true_explorer, + registry, + func, + scope, + derived, + symbol_map, + ); + let true_initial_state = make_state(); + let (compiled_true_status, true_compiled) = compiled_branch_reachability_status( + &true_explorer, + func, + &true_initial_state, + block_addr, + true, + ); + let true_condition = symbolic_condition_hint(true_compiled.as_ref()); + let true_status = if let Some(status) = compiled_true_status { + status + } else if predicate_uses_call_result || func_contains_calls(func) { + SymbolicReachabilityStatus::Unknown + } else { + let paths = true_explorer.find_paths_to(func, make_state(), true_target); + symbolic_reachability_status(paths.len(), true_explorer.budget_exhausted()) + }; + + let mut false_explorer = symbolic_fact_explorer(ctx); + install_derived_summary_set( + &mut false_explorer, + registry, + func, + scope, + derived, + symbol_map, + ); + let false_initial_state = make_state(); + let (compiled_false_status, false_compiled) = compiled_branch_reachability_status( + &false_explorer, + func, + &false_initial_state, + block_addr, + false, + ); + let false_condition = symbolic_condition_hint(false_compiled.as_ref()); + let false_status = if let Some(status) = compiled_false_status { + status + } else if predicate_uses_call_result || func_contains_calls(func) { + SymbolicReachabilityStatus::Unknown + } else { + let paths = false_explorer.find_paths_to(func, make_state(), false_target); + symbolic_reachability_status(paths.len(), false_explorer.budget_exhausted()) + }; + + if matches!(true_status, SymbolicReachabilityStatus::Unknown) + || matches!(false_status, SymbolicReachabilityStatus::Unknown) + { + facts.diagnostics.branches_unknown += 1; + } + if matches!( + (true_status, false_status), + ( + SymbolicReachabilityStatus::Reachable, + SymbolicReachabilityStatus::Unreachable + ) | ( + SymbolicReachabilityStatus::Unreachable, + SymbolicReachabilityStatus::Reachable + ) + ) { + facts.diagnostics.branches_pruned += 1; + } + + facts.branch_facts.push(SymbolicBranchFact { + block_addr, + true_target, + false_target, + true_status, + false_status, + true_condition, + false_condition, + true_compiled, + false_compiled, + }); + } + + let _ = summary_profile; + facts +} + +pub fn collect_symbolic_function_facts( + ctx: &Context, + func: &SsaArtifact, + arch: Option<&ArchSpec>, + symbol_map: &HashMap, +) -> SymbolicFunctionFacts { + collect_symbolic_function_facts_with_scope(ctx, func, None, arch, symbol_map) +} + +pub fn collect_symbolic_function_facts_with_scope( + ctx: &Context, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + arch: Option<&ArchSpec>, + symbol_map: &HashMap, +) -> SymbolicFunctionFacts { + let mut facts = SymbolicFunctionFacts::default(); + let Some(arch) = arch else { + facts.diagnostics.skipped_missing_arch = true; + return facts; + }; + + let cfg_summary = func.function().cfg_risk_summary(); + if cfg_summary.block_count > 96 || cfg_summary.switch_block_count > 8 { + facts.diagnostics.skipped_large_cfg = true; + return facts; + } + + if let Some(scope) = scope { + let Some(registry) = SummaryRegistry::with_profile_for_arch(arch, SummaryProfile::Default) + else { + return facts; + }; + let derived = registry.derive_symbolic_summaries(ctx, scope, Some(arch), symbol_map); + return collect_symbolic_function_facts_with_derived( + ctx, + func, + Some(scope), + arch, + symbol_map, + SummaryProfile::Default, + ®istry, + &derived, + ); + } + + let branch_blocks = func + .cfg() + .block_addrs() + .filter_map(|block_addr| { + let block = func.cfg().get_block(block_addr)?; + match block.terminator { + r2ssa::BlockTerminator::ConditionalBranch { + true_target, + false_target, + } => Some((block_addr, true_target, false_target)), + _ => None, + } + }) + .collect::>(); + + for (block_addr, true_target, false_target) in branch_blocks { + facts.diagnostics.branches_evaluated += 1; + let predicate_uses_call_result = predicate_depends_on_call_result(func, block_addr); + + let make_state = || { + let mut state = SymState::new(ctx, func.entry); + seed_default_state_for_arch(&mut state, func, Some(arch)); + state + }; + + let mut true_explorer = symbolic_fact_explorer(ctx); + install_symbolic_fact_hooks( + ctx, + &mut true_explorer, + func, + None, + arch, + SummaryProfile::Default, + symbol_map, + ); + let true_initial_state = make_state(); + let (compiled_true_status, true_compiled) = compiled_branch_reachability_status( + &true_explorer, + func, + &true_initial_state, + block_addr, + true, + ); + let true_condition = symbolic_condition_hint(true_compiled.as_ref()); + let true_status = if let Some(status) = compiled_true_status { + status + } else if predicate_uses_call_result || func_contains_calls(func) { + SymbolicReachabilityStatus::Unknown + } else { + let paths = true_explorer.find_paths_to(func, make_state(), true_target); + symbolic_reachability_status(paths.len(), true_explorer.budget_exhausted()) + }; + + let mut false_explorer = symbolic_fact_explorer(ctx); + install_symbolic_fact_hooks( + ctx, + &mut false_explorer, + func, + None, + arch, + SummaryProfile::Default, + symbol_map, + ); + let false_initial_state = make_state(); + let (compiled_false_status, false_compiled) = compiled_branch_reachability_status( + &false_explorer, + func, + &false_initial_state, + block_addr, + false, + ); + let false_condition = symbolic_condition_hint(false_compiled.as_ref()); + let false_status = if let Some(status) = compiled_false_status { + status + } else if predicate_uses_call_result || func_contains_calls(func) { + SymbolicReachabilityStatus::Unknown + } else { + let paths = false_explorer.find_paths_to(func, make_state(), false_target); + symbolic_reachability_status(paths.len(), false_explorer.budget_exhausted()) + }; + + if matches!(true_status, SymbolicReachabilityStatus::Unknown) + || matches!(false_status, SymbolicReachabilityStatus::Unknown) + { + facts.diagnostics.branches_unknown += 1; + } + if matches!( + (true_status, false_status), + ( + SymbolicReachabilityStatus::Reachable, + SymbolicReachabilityStatus::Unreachable + ) | ( + SymbolicReachabilityStatus::Unreachable, + SymbolicReachabilityStatus::Reachable + ) + ) { + facts.diagnostics.branches_pruned += 1; + } + + facts.branch_facts.push(SymbolicBranchFact { + block_addr, + true_target, + false_target, + true_status, + false_status, + true_condition, + false_condition, + true_compiled, + false_compiled, + }); + } + + facts +} diff --git a/crates/r2sym/src/semantics/mod.rs b/crates/r2sym/src/semantics/mod.rs new file mode 100644 index 0000000..844b8c5 --- /dev/null +++ b/crates/r2sym/src/semantics/mod.rs @@ -0,0 +1,23 @@ +mod artifact; +mod cache; +mod classify; +mod compiler; +mod facts; +mod vm; + +pub use artifact::{ + CompiledFunctionSemantics, CompiledSemanticArtifact, CompiledSemanticMode, ResidualReason, + SemanticCapability, SemanticMode, SliceClass, +}; +pub use cache::stable_scope_hash; +pub use compiler::{compile_function_semantics_with_scope, compile_semantic_artifact_with_scope}; +pub use facts::{ + SymbolicBranchFact, SymbolicFunctionFactDiagnostics, SymbolicFunctionFacts, + SymbolicReachabilityStatus, collect_symbolic_function_facts, + collect_symbolic_function_facts_with_scope, +}; +pub use vm::{ + InterpreterDispatchSummary, InterpreterKind, VmStateUpdate, VmStepSummary, VmTransferArm, + VmValueExpr, +}; +pub(crate) use vm::{build_vm_step_summary, classify_interpreter_like}; diff --git a/crates/r2sym/src/semantics/vm.rs b/crates/r2sym/src/semantics/vm.rs new file mode 100644 index 0000000..4ee2e38 --- /dev/null +++ b/crates/r2sym/src/semantics/vm.rs @@ -0,0 +1,799 @@ +use std::collections::{BTreeMap, BTreeSet, VecDeque}; + +use r2ssa::cfg::BlockTerminator; +use r2ssa::{CFGEdge, SSAOp, SSAVar, SsaArtifact}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum InterpreterKind { + SwitchDispatch, + IndirectDispatch, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct InterpreterDispatchSummary { + pub kind: InterpreterKind, + pub dispatch_header: u64, + pub dispatch_targets: usize, + pub selector: Option, + pub back_edges: usize, + pub score: i32, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum VmValueExpr { + Const(u64), + Var(String), + Expr(String), +} + +impl VmValueExpr { + fn render(&self) -> String { + match self { + Self::Const(value) => format!("0x{value:x}"), + Self::Var(name) | Self::Expr(name) => name.clone(), + } + } + + fn is_exact(&self) -> bool { + matches!(self, Self::Const(_) | Self::Var(_)) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct VmStateUpdate { + pub output: String, + pub expr: String, + pub value: VmValueExpr, + pub exact: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct VmTransferArm { + pub handler_target: u64, + pub case_values: Vec, + pub region_blocks: Vec, + pub exit_targets: Vec, + pub state_updates: Vec, + pub selector_update: Option, + pub exact: bool, + pub redispatch: bool, + pub may_return: bool, + pub truncated: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct VmStepSummary { + pub kind: InterpreterKind, + pub loop_header: u64, + pub dispatch_header: u64, + pub selector: Option, + pub dispatch_targets: Vec, + pub default_target: Option, + pub case_values_by_target: BTreeMap>, + pub loop_latches: Vec, + pub state_inputs: Vec, + pub state_outputs: Vec, + pub step_blocks: Vec, + pub handler_regions: BTreeMap>, + pub handler_state_inputs: BTreeMap>, + pub handler_state_outputs: BTreeMap>, + pub handler_state_updates: BTreeMap>, + pub handler_memory_reads: BTreeMap, + pub handler_memory_writes: BTreeMap, + pub handler_calls: BTreeMap, + pub handler_conditional_branches: BTreeMap, + pub handler_exit_targets: BTreeMap>, + pub redispatch_handlers: Vec, + pub returning_handlers: Vec, + pub truncated_handlers: Vec, + pub transfers: Vec, +} + +const MAX_HANDLER_REGION_BLOCKS: usize = 16; +const MAX_HANDLER_REGION_DEPTH: usize = 8; + +#[derive(Debug, Default)] +struct HandlerRegionSummary { + blocks: Vec, + state_inputs: Vec, + state_outputs: Vec, + state_updates: Vec, + memory_reads: usize, + memory_writes: usize, + calls: usize, + conditional_branches: usize, + exit_targets: Vec, + reenters_dispatch: bool, + may_return: bool, + truncated: bool, +} + +fn record_block_state( + block: &r2ssa::function::SSABlock, + state_inputs: &mut BTreeSet, + state_outputs: &mut BTreeSet, +) { + block.for_each_source(|src| { + if (!src.var.is_register() && !src.var.name.starts_with("ram:")) || src.var.version != 0 { + return; + } + state_inputs.insert(src.var.display_name()); + }); + for phi in &block.phis { + if !phi.dst.is_const() && !phi.dst.is_temp() && !phi.dst.name.starts_with("ram:") { + state_outputs.insert(phi.dst.display_name()); + } + } + for op in &block.ops { + let Some(dst) = op.dst() else { + continue; + }; + if dst.is_const() || dst.is_temp() || dst.name.starts_with("ram:") { + continue; + } + state_outputs.insert(dst.display_name()); + } +} + +fn case_values_by_target( + func: &SsaArtifact, + dispatch_header: u64, +) -> (BTreeMap>, Option) { + let Some((cases, default_target)) = func.function().switch_info(dispatch_header) else { + return (BTreeMap::new(), None); + }; + let mut case_values_by_target = BTreeMap::>::new(); + for (value, target) in cases { + case_values_by_target.entry(target).or_default().push(value); + } + for values in case_values_by_target.values_mut() { + values.sort_unstable(); + values.dedup(); + } + (case_values_by_target, default_target) +} + +fn display_const(name: &str) -> Option { + let hex = name.strip_prefix("const:")?; + let digits = hex.strip_prefix("0x").unwrap_or(hex); + let value = u64::from_str_radix(digits, 16).ok()?; + Some(format!("0x{value:x}")) +} + +fn split_version(name: &str) -> (&str, Option<&str>) { + name.rsplit_once('_') + .filter(|(_, version)| version.chars().all(|ch| ch.is_ascii_digit())) + .map_or((name, None), |(base, version)| (base, Some(version))) +} + +fn same_logical_name(left: &str, right: &str) -> bool { + split_version(left) + .0 + .eq_ignore_ascii_case(split_version(right).0) +} + +fn render_vm_var_expr(func: &SsaArtifact, var: &SSAVar, depth: u32) -> String { + if depth > 4 { + return var.display_name(); + } + if var.is_const() { + return display_const(&var.name).unwrap_or_else(|| var.display_name()); + } + let Some(value_id) = func.graph().value_id_for_var(var) else { + return var.display_name(); + }; + let Some(inst_id) = func.graph().def_inst(value_id) else { + return var.display_name(); + }; + let Some(inst) = func.graph().inst(inst_id) else { + return var.display_name(); + }; + let r2ssa::graph::InstPayload::Op(op) = &inst.payload else { + return var.display_name(); + }; + render_vm_op_expr(func, op, depth + 1).unwrap_or_else(|| var.display_name()) +} + +fn classify_vm_var_value(func: &SsaArtifact, var: &SSAVar, depth: u32) -> VmValueExpr { + if depth > 4 { + return VmValueExpr::Var(var.display_name()); + } + if var.is_const() { + return display_const(&var.name) + .and_then(|_| { + let hex = var.name.strip_prefix("const:")?; + let digits = hex.strip_prefix("0x").unwrap_or(hex); + u64::from_str_radix(digits, 16).ok() + }) + .map(VmValueExpr::Const) + .unwrap_or_else(|| VmValueExpr::Expr(var.display_name())); + } + let Some(value_id) = func.graph().value_id_for_var(var) else { + return VmValueExpr::Var(var.display_name()); + }; + let Some(inst_id) = func.graph().def_inst(value_id) else { + return VmValueExpr::Var(var.display_name()); + }; + let Some(inst) = func.graph().inst(inst_id) else { + return VmValueExpr::Var(var.display_name()); + }; + let r2ssa::graph::InstPayload::Op(op) = &inst.payload else { + return VmValueExpr::Var(var.display_name()); + }; + classify_vm_op_value(func, op, depth + 1) + .unwrap_or_else(|| VmValueExpr::Var(var.display_name())) +} + +fn render_vm_binary_expr( + func: &SsaArtifact, + a: &SSAVar, + op: &str, + b: &SSAVar, + depth: u32, +) -> String { + format!( + "({} {} {})", + render_vm_var_expr(func, a, depth + 1), + op, + render_vm_var_expr(func, b, depth + 1) + ) +} + +fn render_vm_op_expr(func: &SsaArtifact, op: &SSAOp, depth: u32) -> Option { + use SSAOp::*; + + Some(match op { + Copy { src, .. } | IntZExt { src, .. } | IntSExt { src, .. } | Cast { src, .. } => { + render_vm_var_expr(func, src, depth + 1) + } + Load { addr, .. } => format!("*{}", render_vm_var_expr(func, addr, depth + 1)), + IntAdd { a, b, .. } | FloatAdd { a, b, .. } => { + render_vm_binary_expr(func, a, "+", b, depth + 1) + } + IntSub { a, b, .. } | FloatSub { a, b, .. } => { + render_vm_binary_expr(func, a, "-", b, depth + 1) + } + IntMult { a, b, .. } | FloatMult { a, b, .. } => { + render_vm_binary_expr(func, a, "*", b, depth + 1) + } + IntDiv { a, b, .. } | IntSDiv { a, b, .. } | FloatDiv { a, b, .. } => { + render_vm_binary_expr(func, a, "/", b, depth + 1) + } + IntRem { a, b, .. } | IntSRem { a, b, .. } => { + render_vm_binary_expr(func, a, "%", b, depth + 1) + } + IntAnd { a, b, .. } => render_vm_binary_expr(func, a, "&", b, depth + 1), + IntOr { a, b, .. } => render_vm_binary_expr(func, a, "|", b, depth + 1), + IntXor { a, b, .. } | BoolXor { a, b, .. } => { + render_vm_binary_expr(func, a, "^", b, depth + 1) + } + IntLeft { a, b, .. } => render_vm_binary_expr(func, a, "<<", b, depth + 1), + IntRight { a, b, .. } | IntSRight { a, b, .. } => { + render_vm_binary_expr(func, a, ">>", b, depth + 1) + } + IntEqual { a, b, .. } | FloatEqual { a, b, .. } => { + render_vm_binary_expr(func, a, "==", b, depth + 1) + } + IntNotEqual { a, b, .. } | FloatNotEqual { a, b, .. } => { + render_vm_binary_expr(func, a, "!=", b, depth + 1) + } + IntLess { a, b, .. } | IntSLess { a, b, .. } | FloatLess { a, b, .. } => { + render_vm_binary_expr(func, a, "<", b, depth + 1) + } + IntLessEqual { a, b, .. } | IntSLessEqual { a, b, .. } | FloatLessEqual { a, b, .. } => { + render_vm_binary_expr(func, a, "<=", b, depth + 1) + } + BoolAnd { a, b, .. } => render_vm_binary_expr(func, a, "&&", b, depth + 1), + BoolOr { a, b, .. } => render_vm_binary_expr(func, a, "||", b, depth + 1), + IntNegate { src, .. } | FloatNeg { src, .. } => { + format!("(-{})", render_vm_var_expr(func, src, depth + 1)) + } + IntNot { src, .. } => format!("(~{})", render_vm_var_expr(func, src, depth + 1)), + BoolNot { src, .. } => format!("(!{})", render_vm_var_expr(func, src, depth + 1)), + PtrAdd { + base, + index, + element_size, + .. + } => format!( + "({} + ({} * {}))", + render_vm_var_expr(func, base, depth + 1), + render_vm_var_expr(func, index, depth + 1), + element_size + ), + PtrSub { + base, + index, + element_size, + .. + } => format!( + "({} - ({} * {}))", + render_vm_var_expr(func, base, depth + 1), + render_vm_var_expr(func, index, depth + 1), + element_size + ), + Piece { hi, lo, .. } => format!( + "piece({}, {})", + render_vm_var_expr(func, hi, depth + 1), + render_vm_var_expr(func, lo, depth + 1) + ), + Subpiece { src, offset, .. } => format!( + "subpiece({}, {})", + render_vm_var_expr(func, src, depth + 1), + offset + ), + _ => return None, + }) +} + +fn classify_vm_op_value(func: &SsaArtifact, op: &SSAOp, depth: u32) -> Option { + use SSAOp::*; + + Some(match op { + Copy { src, .. } | IntZExt { src, .. } | IntSExt { src, .. } | Cast { src, .. } => { + classify_vm_var_value(func, src, depth + 1) + } + _ => VmValueExpr::Expr(render_vm_op_expr(func, op, depth + 1)?), + }) +} + +fn summarize_handler_region( + func: &SsaArtifact, + entry: u64, + dispatch_header: u64, + loop_header: u64, + dispatch_targets: &BTreeSet, +) -> HandlerRegionSummary { + let mut visited = BTreeSet::new(); + let mut queue = VecDeque::from([(entry, 0usize)]); + let mut exit_targets = BTreeSet::new(); + let mut state_inputs = BTreeSet::new(); + let mut state_outputs = BTreeSet::new(); + let mut state_updates = BTreeMap::::new(); + let mut memory_reads = 0usize; + let mut memory_writes = 0usize; + let mut calls = 0usize; + let mut conditional_branches = 0usize; + let mut reenters_dispatch = false; + let mut may_return = false; + let mut truncated = false; + + while let Some((block_addr, depth)) = queue.pop_front() { + if !visited.insert(block_addr) { + continue; + } + if visited.len() > MAX_HANDLER_REGION_BLOCKS { + truncated = true; + visited.remove(&block_addr); + continue; + } + + let Some(block) = func.get_block(block_addr) else { + continue; + }; + let cfg_block = func.cfg().get_block(block_addr); + if matches!( + cfg_block.map(|block| &block.terminator), + Some(BlockTerminator::ConditionalBranch { .. }) + ) { + conditional_branches += 1; + } + if matches!( + cfg_block.map(|block| &block.terminator), + Some(BlockTerminator::Return) + ) { + may_return = true; + } + + record_block_state(block, &mut state_inputs, &mut state_outputs); + for op in &block.ops { + if op.is_memory_read() { + memory_reads += 1; + } + if op.is_memory_write() { + memory_writes += 1; + } + if matches!( + op, + SSAOp::Call { .. } | SSAOp::CallInd { .. } | SSAOp::CallOther { .. } + ) { + calls += 1; + } + if let Some(dst) = op.dst() + && !dst.is_const() + && !dst.is_temp() + && !dst.name.starts_with("ram:") + { + let value = classify_vm_op_value(func, op, 0) + .unwrap_or_else(|| VmValueExpr::Var(dst.display_name())); + state_updates.insert(dst.display_name(), value); + } + } + + let succs = func.successors(block_addr); + if succs.is_empty() { + continue; + } + for succ in succs { + if succ == dispatch_header || succ == loop_header { + reenters_dispatch = true; + exit_targets.insert(succ); + continue; + } + if dispatch_targets.contains(&succ) && succ != entry { + exit_targets.insert(succ); + continue; + } + if depth >= MAX_HANDLER_REGION_DEPTH { + truncated = true; + exit_targets.insert(succ); + continue; + } + queue.push_back((succ, depth + 1)); + } + } + + HandlerRegionSummary { + blocks: visited.into_iter().collect(), + state_inputs: state_inputs.into_iter().collect(), + state_outputs: state_outputs.into_iter().collect(), + state_updates: state_updates + .into_iter() + .map(|(output, value)| VmStateUpdate { + output, + expr: value.render(), + exact: value.is_exact(), + value, + }) + .collect(), + memory_reads, + memory_writes, + calls, + conditional_branches, + exit_targets: exit_targets.into_iter().collect(), + reenters_dispatch, + may_return, + truncated, + } +} + +fn direct_loop_latches(func: &SsaArtifact, header: u64) -> Vec { + func.predecessors(header) + .into_iter() + .filter(|pred| matches!(func.edge_type(*pred, header), Some(CFGEdge::Back))) + .collect() +} + +fn can_reach_within( + func: &SsaArtifact, + from: u64, + target: u64, + max_depth: usize, + visited: &mut BTreeSet, +) -> bool { + if from == target { + return true; + } + if max_depth == 0 || !visited.insert(from) { + return false; + } + func.successors(from) + .into_iter() + .any(|succ| can_reach_within(func, succ, target, max_depth - 1, visited)) +} + +fn enclosing_loop_header(func: &SsaArtifact, dispatch_header: u64) -> Option<(u64, Vec)> { + let mut visited = BTreeSet::new(); + let mut frontier = func.predecessors(dispatch_header); + let mut depth = 0usize; + let dispatch_targets = func.successors(dispatch_header); + + while !frontier.is_empty() && depth < 8 { + let mut next = Vec::new(); + for candidate in frontier { + if !visited.insert(candidate) { + continue; + } + let latches = direct_loop_latches(func, candidate); + if !latches.is_empty() && func.dominates(candidate, dispatch_header) { + return Some((candidate, latches)); + } + if func.dominates(candidate, dispatch_header) { + let returning_targets = dispatch_targets + .iter() + .copied() + .filter(|target| { + can_reach_within(func, *target, candidate, 4, &mut BTreeSet::new()) + }) + .collect::>(); + if !returning_targets.is_empty() { + return Some((candidate, returning_targets)); + } + } + next.extend(func.predecessors(candidate)); + } + frontier = next; + depth += 1; + } + + None +} + +pub(crate) fn classify_interpreter_like(func: &SsaArtifact) -> Option { + let summary = func.function().cfg_risk_summary(); + if summary.block_count < 6 || summary.loop_count == 0 { + return None; + } + + let has_indirect = func + .cfg() + .block_addrs() + .filter_map(|addr| func.cfg().get_block(addr)) + .any(|block| matches!(block.terminator, BlockTerminator::IndirectBranch)); + if summary.switch_block_count == 0 && !has_indirect { + return None; + } + + let direct_call_diversity = func + .call_sites() + .by_id + .values() + .filter_map(|call| call.direct_target) + .collect::>() + .len(); + + let mut best: Option = None; + let mut best_score = i32::MIN; + for block_addr in func.cfg().block_addrs() { + let Some(block) = func.cfg().get_block(block_addr) else { + continue; + }; + let selector = func + .function() + .infer_switch_selector_var(block_addr) + .map(|var| var.name); + let dispatch_targets = func.successors(block_addr); + let kind = match block.terminator { + BlockTerminator::Switch { .. } => InterpreterKind::SwitchDispatch, + BlockTerminator::IndirectBranch => InterpreterKind::IndirectDispatch, + _ if dispatch_targets.len() >= 4 && selector.is_some() => { + InterpreterKind::SwitchDispatch + } + _ => continue, + }; + + let preds = func.predecessors(block_addr); + let back_edges = preds + .iter() + .filter(|pred| matches!(func.edge_type(**pred, block_addr), Some(CFGEdge::Back))) + .count(); + let dispatch_fanout = dispatch_targets.len(); + + let mut score = 0i32; + if back_edges > 0 { + score += 2; + } + if selector.is_some() { + score += 2; + } + if matches!(kind, InterpreterKind::SwitchDispatch) { + score += 2; + } + if dispatch_fanout >= 4 { + score += 1; + } + let dominated_targets = dispatch_targets + .iter() + .filter(|target| func.dominates(block_addr, **target)) + .count(); + if dispatch_fanout > 0 && dominated_targets * 2 >= dispatch_fanout { + score += 1; + } + if direct_call_diversity <= 2 { + score += 1; + } + if direct_call_diversity > dispatch_fanout.max(4) { + score -= 2; + } + + let threshold = match kind { + InterpreterKind::SwitchDispatch => 6, + InterpreterKind::IndirectDispatch => 5, + }; + if score < threshold || score < best_score { + continue; + } + + best_score = score; + best = Some(InterpreterDispatchSummary { + kind, + dispatch_header: block_addr, + dispatch_targets: dispatch_fanout, + selector, + back_edges, + score, + }); + } + + if best.is_some() { + return best; + } + + let mut fallback: Option = None; + let mut fallback_score = i32::MIN; + for block_addr in func.cfg().block_addrs() { + let dispatch_targets = func.successors(block_addr); + if dispatch_targets.len() < 4 { + continue; + } + let selector = func + .function() + .infer_switch_selector_var(block_addr) + .map(|var| var.name); + let back_edges = func + .predecessors(block_addr) + .iter() + .filter(|pred| matches!(func.edge_type(**pred, block_addr), Some(CFGEdge::Back))) + .count(); + let score = (dispatch_targets.len() as i32) + if selector.is_some() { 2 } else { 0 }; + if score < fallback_score { + continue; + } + fallback_score = score; + fallback = Some(InterpreterDispatchSummary { + kind: InterpreterKind::SwitchDispatch, + dispatch_header: block_addr, + dispatch_targets: dispatch_targets.len(), + selector, + back_edges, + score, + }); + } + + fallback +} + +pub(crate) fn build_vm_step_summary( + func: &SsaArtifact, + interpreter: &InterpreterDispatchSummary, +) -> Option { + let dispatch_targets = func.successors(interpreter.dispatch_header); + if dispatch_targets.len() < 2 { + return None; + } + let (loop_header, loop_latches) = { + let direct = direct_loop_latches(func, interpreter.dispatch_header); + if direct.is_empty() { + enclosing_loop_header(func, interpreter.dispatch_header) + .unwrap_or((interpreter.dispatch_header, Vec::new())) + } else { + (interpreter.dispatch_header, direct) + } + }; + if loop_latches.is_empty() { + return None; + } + + let dispatch_target_set = dispatch_targets.iter().copied().collect::>(); + let (case_values_by_target, default_target) = + case_values_by_target(func, interpreter.dispatch_header); + let mut handler_regions = BTreeMap::new(); + let mut handler_state_inputs = BTreeMap::new(); + let mut handler_state_outputs = BTreeMap::new(); + let mut handler_state_updates = BTreeMap::new(); + let mut handler_memory_reads = BTreeMap::new(); + let mut handler_memory_writes = BTreeMap::new(); + let mut handler_calls = BTreeMap::new(); + let mut handler_conditional_branches = BTreeMap::new(); + let mut handler_exit_targets = BTreeMap::new(); + let mut redispatch_handlers = Vec::new(); + let mut returning_handlers = Vec::new(); + let mut truncated_handlers = Vec::new(); + let mut transfers = Vec::new(); + let mut step_block_set = BTreeSet::from([loop_header, interpreter.dispatch_header]); + + for target in dispatch_targets.iter().copied() { + let summary = summarize_handler_region( + func, + target, + interpreter.dispatch_header, + loop_header, + &dispatch_target_set, + ); + step_block_set.extend(summary.blocks.iter().copied()); + if summary.reenters_dispatch { + redispatch_handlers.push(target); + } + if summary.may_return { + returning_handlers.push(target); + } + if summary.truncated { + truncated_handlers.push(target); + } + let case_values = case_values_by_target + .get(&target) + .cloned() + .unwrap_or_default(); + let selector_update = interpreter.selector.as_ref().and_then(|selector| { + summary + .state_updates + .iter() + .find(|update| same_logical_name(&update.output, selector)) + .cloned() + }); + let exact = !summary.truncated + && summary.state_updates.iter().all(|update| update.exact) + && selector_update.as_ref().is_none_or(|update| update.exact); + transfers.push(VmTransferArm { + handler_target: target, + case_values, + region_blocks: summary.blocks.clone(), + exit_targets: summary.exit_targets.clone(), + state_updates: summary.state_updates.clone(), + selector_update, + exact, + redispatch: summary.reenters_dispatch, + may_return: summary.may_return, + truncated: summary.truncated, + }); + handler_regions.insert(target, summary.blocks); + handler_state_inputs.insert(target, summary.state_inputs); + handler_state_outputs.insert(target, summary.state_outputs); + handler_state_updates.insert(target, summary.state_updates); + handler_memory_reads.insert(target, summary.memory_reads); + handler_memory_writes.insert(target, summary.memory_writes); + handler_calls.insert(target, summary.calls); + handler_conditional_branches.insert(target, summary.conditional_branches); + handler_exit_targets.insert(target, summary.exit_targets); + } + + let step_blocks = step_block_set.into_iter().collect::>(); + let mut state_inputs = BTreeSet::new(); + let mut state_outputs = BTreeSet::new(); + for block_addr in step_blocks.iter().copied() { + let Some(block) = func.get_block(block_addr) else { + continue; + }; + record_block_state(block, &mut state_inputs, &mut state_outputs); + } + + if state_inputs.is_empty() || state_outputs.is_empty() { + return None; + } + + redispatch_handlers.sort_unstable(); + redispatch_handlers.dedup(); + returning_handlers.sort_unstable(); + returning_handlers.dedup(); + truncated_handlers.sort_unstable(); + truncated_handlers.dedup(); + transfers.sort_by_key(|transfer| transfer.handler_target); + + Some(VmStepSummary { + kind: interpreter.kind, + loop_header, + dispatch_header: interpreter.dispatch_header, + selector: interpreter.selector.clone(), + dispatch_targets, + default_target, + case_values_by_target, + loop_latches, + state_inputs: state_inputs.into_iter().collect(), + state_outputs: state_outputs.into_iter().collect(), + step_blocks, + handler_regions, + handler_state_inputs, + handler_state_outputs, + handler_state_updates, + handler_memory_reads, + handler_memory_writes, + handler_calls, + handler_conditional_branches, + handler_exit_targets, + redispatch_handlers, + returning_handlers, + truncated_handlers, + transfers, + }) +} diff --git a/crates/r2sym/src/sim.rs b/crates/r2sym/src/sim.rs index cf430e8..61bf85e 100644 --- a/crates/r2sym/src/sim.rs +++ b/crates/r2sym/src/sim.rs @@ -3,18 +3,23 @@ //! These summaries short-circuit into lightweight models to avoid //! path explosion from libc implementations. -use std::collections::{BTreeSet, HashMap}; +use std::collections::{BTreeMap, BTreeSet, HashMap}; +use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::rc::Rc; use std::sync::Arc; use r2il::ArchSpec; use r2ssa::{ - FunctionSemanticSummary, InterprocFunctionId, InterprocSummarySet, SsaArtifact, - SummaryMemoryEffectKind, SummaryMemoryRegion, SummaryReturnRelation, + FunctionSemanticSummary, InterprocFunctionId, InterprocFunctionInput, InterprocSolveConfig, + InterprocSummarySet, SsaArtifact, SummaryMemoryEffectKind, SummaryMemoryRegion, + SummaryReturnRelation, solve_interproc_summary_set, }; -use z3::ast::BV; +use serde::{Deserialize, Serialize}; +use z3::ast::{Ast, BV, Bool}; use crate::executor::{CallHookResult, SymExecutor}; use crate::path::PathExplorer; +use crate::solver::{SatResult, SymSolver}; use crate::state::{ExitStatus, SymState}; use crate::value::SymValue; @@ -42,9 +47,17 @@ pub const PATH_LIST_MAX_MEMCMP: u64 = 0x40; pub const PATH_LIST_MAX_PRINTF_SCAN: u64 = 0x40; /// Path-listing precise-byte threshold before summaries switch to coarse modeling. pub const PATH_LIST_PRECISE_BYTE_LIMIT: u64 = 0x10; +/// Default state budget for derived symbolic helper summaries. +pub const DEFAULT_DERIVED_SUMMARY_MAX_STATES: usize = 64; +/// Default path cap for derived helper summarization. +pub const DEFAULT_DERIVED_SUMMARY_MAX_PATHS: usize = 8; +/// Default depth budget for derived helper summarization. +pub const DEFAULT_DERIVED_SUMMARY_MAX_DEPTH: usize = 128; +/// Default bounded fixed-point iteration count for derived helper SCCs. +pub const DEFAULT_DERIVED_SUMMARY_MAX_ITERATIONS: usize = 8; /// Summary profile used to install function summaries for different workflows. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum SummaryProfile { /// Default symbolic execution behavior. Default, @@ -200,7 +213,11 @@ impl CallConv { None } - fn collect_call_info<'ctx>(&self, state: &SymState<'ctx>, arity: usize) -> CallInfo<'ctx> { + pub(crate) fn collect_call_info<'ctx>( + &self, + state: &SymState<'ctx>, + arity: usize, + ) -> CallInfo<'ctx> { let mut args = Vec::with_capacity(arity); for i in 0..arity { if let Some(reg) = self.arg_registers.get(i) { @@ -238,6 +255,30 @@ impl CallConv { let adjusted = adjust_bits(state.context(), value, key_bits); state.set_register(&key, adjusted); } + + pub(crate) fn arg_register_name(&self, index: usize) -> Option<&'static str> { + self.arg_registers.get(index).copied() + } + + pub(crate) fn arg_capacity(&self) -> usize { + self.arg_registers.len() + } + + pub(crate) fn arg_bits(&self) -> u32 { + self.arg_bits + } + + pub(crate) fn ret_bits(&self) -> u32 { + self.ret_bits + } + + pub(crate) fn ret_register_name(&self) -> &'static str { + self.ret_register + } + + pub(crate) fn return_value<'ctx>(&self, state: &SymState<'ctx>) -> SymValue<'ctx> { + self.read_register(state, self.ret_register) + } } #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] @@ -248,6 +289,126 @@ pub struct SummaryInstallStats { pub duplicates: usize, } +#[derive(Debug, Clone)] +pub struct ScopedPreparedFunction { + pub id: InterprocFunctionId, + pub name: Option, + pub prepared: SsaArtifact, +} + +#[derive(Debug, Clone)] +pub struct PreparedFunctionScope { + root: InterprocFunctionId, + functions: BTreeMap, +} + +impl PreparedFunctionScope { + pub fn new(root_addr: u64, functions: Vec) -> Option { + let root = InterprocFunctionId(root_addr); + let mut by_id = BTreeMap::new(); + for function in functions { + by_id.insert(function.id, function); + } + by_id.contains_key(&root).then_some(Self { + root, + functions: by_id, + }) + } + + pub fn root_id(&self) -> InterprocFunctionId { + self.root + } + + pub fn root(&self) -> Option<&ScopedPreparedFunction> { + self.functions.get(&self.root) + } + + pub fn functions(&self) -> &BTreeMap { + &self.functions + } + + pub fn helper_functions(&self) -> impl Iterator { + self.functions + .values() + .filter(move |function| function.id != self.root) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DerivedSummaryCompletion { + Exact, + OverApprox, + BudgetExhausted, + Unknown, +} + +#[derive(Clone)] +pub struct DerivedSummaryInput<'ctx> { + pub arg_index: usize, + pub symbol: SymValue<'ctx>, + pub size: u32, +} + +#[derive(Clone)] +pub struct DerivedMemoryWrite<'ctx> { + pub arg_index: usize, + pub offset: i64, + pub size: u32, + pub value: SymValue<'ctx>, +} + +#[derive(Clone)] +pub struct DerivedSummaryCase<'ctx> { + pub guard: Bool, + pub return_value: Option>, + pub memory_writes: Vec>, +} + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub(crate) struct DerivedSummaryGuidance { + pub summary_known: bool, + pub exact: bool, + pub feasible_cases: usize, + pub contradictory: bool, +} + +#[derive(Debug, Clone, Copy, Default)] +struct PointerInputWindow { + headroom: u32, + forward_size: u32, +} + +#[derive(Clone)] +pub struct DerivedFunctionSummary<'ctx> { + pub id: InterprocFunctionId, + pub name: Option, + pub arg_count_hint: usize, + pub arg_symbols: Vec<(usize, SymValue<'ctx>)>, + pub memory_inputs: Vec>, + pub cases: Vec>, + pub completion: DerivedSummaryCompletion, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct DerivedSummaryDiagnostics { + pub attempted: usize, + pub derived: usize, + pub budget_exhausted: usize, + pub skipped_core: usize, + pub skipped_missing: usize, + pub scc_count: usize, + pub max_scc_size: usize, + pub scc_converged: usize, + pub scc_budget_exhausted: usize, +} + +#[derive(Clone)] +pub struct DerivedSummarySet<'ctx> { + pub interproc: InterprocSummarySet, + pub summaries: BTreeMap>>, + pub diagnostics: DerivedSummaryDiagnostics, +} + /// Summary registry that can install summaries as call hooks. pub struct SummaryRegistry<'ctx> { summaries: HashMap + 'ctx>>, @@ -448,6 +609,842 @@ impl<'ctx> SummaryRegistry<'ctx> { stats } + + pub fn has_core_summary_name(&self, name: &str) -> bool { + normalize_core_summary_name(name) + .is_some_and(|summary_name| self.summaries.contains_key(summary_name)) + } + + pub fn derive_symbolic_summaries( + &self, + ctx: &'ctx z3::Context, + scope: &PreparedFunctionScope, + arch: Option<&ArchSpec>, + symbol_map: &HashMap, + ) -> DerivedSummarySet<'ctx> { + let interproc = build_interproc_summary_set(scope, arch, symbol_map); + let mut summaries: BTreeMap>> = + BTreeMap::new(); + let mut diagnostics = DerivedSummaryDiagnostics::default(); + + let helper_scope = scope + .helper_functions() + .map(|helper| (helper.id, helper)) + .collect::>(); + let sccs = compute_derived_summary_sccs(&helper_scope); + diagnostics.scc_count = sccs.len(); + + for scc in sccs { + diagnostics.max_scc_size = diagnostics.max_scc_size.max(scc.len()); + let mut converged = false; + for _ in 0..DEFAULT_DERIVED_SUMMARY_MAX_ITERATIONS { + let mut changed = false; + for function_id in &scc { + let Some(helper) = helper_scope.get(function_id).copied() else { + continue; + }; + diagnostics.attempted += 1; + if helper + .name + .as_deref() + .is_some_and(|name| self.has_core_summary_name(name)) + { + diagnostics.skipped_core += 1; + continue; + } + + let Some(static_summary) = interproc.summaries.get(function_id).cloned() else { + diagnostics.skipped_missing += 1; + continue; + }; + + let derived = derive_symbolic_summary_for_function(DerivedSummaryBuildInputs { + ctx, + registry: self, + arch, + function: helper, + static_summary: &static_summary, + interproc: &interproc, + derived_summaries: &summaries, + symbol_map, + }); + let next = Rc::new(derived); + let previous = summaries.get(function_id); + if previous.map(|current| derived_summary_fingerprint(current)) + != Some(derived_summary_fingerprint(&next)) + { + summaries.insert(*function_id, next); + changed = true; + } + } + if !changed { + converged = true; + diagnostics.scc_converged += 1; + break; + } + } + + if !converged { + diagnostics.scc_budget_exhausted += 1; + for function_id in &scc { + if let Some(summary) = summaries.get_mut(function_id) { + let next = with_budget_exhausted_completion(summary.as_ref()); + *summary = Rc::new(next); + } + } + } + } + + for summary in summaries.values() { + match summary.completion { + DerivedSummaryCompletion::BudgetExhausted => diagnostics.budget_exhausted += 1, + DerivedSummaryCompletion::Unknown => diagnostics.skipped_missing += 1, + _ => diagnostics.derived += 1, + } + } + + DerivedSummarySet { + interproc, + summaries, + diagnostics, + } + } + + pub fn install_scope_summaries_for_explorer( + &self, + explorer: &mut PathExplorer<'ctx>, + ctx: &'ctx z3::Context, + scope: &PreparedFunctionScope, + arch: Option<&ArchSpec>, + symbol_map: &HashMap, + ) -> DerivedSummaryDiagnostics { + let derived = self.derive_symbolic_summaries(ctx, scope, arch, symbol_map); + if let Some(root) = scope.root() { + let _ = self.install_interproc_summaries_for_function( + explorer, + &root.prepared, + &derived.interproc, + symbol_map, + ); + let _ = self.install_derived_summaries_for_function( + explorer, + &root.prepared, + &derived.summaries, + symbol_map, + ); + let _ = self.install_known_symbols_for_function(explorer, &root.prepared, symbol_map); + } + derived.diagnostics + } + + pub fn install_derived_summaries_for_function( + &self, + explorer: &mut PathExplorer<'ctx>, + prepared: &SsaArtifact, + summaries: &BTreeMap>>, + symbol_map: &HashMap, + ) -> SummaryInstallStats { + let mut stats = SummaryInstallStats::default(); + let mut targets = BTreeSet::new(); + for call in prepared.call_sites().by_id.values() { + if let Some(target) = call.direct_target { + targets.insert(target); + } + } + for target in targets { + stats.attempted += 1; + if let Some(raw_name) = symbol_map.get(&target) + && self.has_core_summary_name(raw_name) + { + stats.skipped_unknown += 1; + continue; + } + let Some(summary) = summaries.get(&InterprocFunctionId(target)).cloned() else { + stats.skipped_unknown += 1; + continue; + }; + if summary.cases.is_empty() + || matches!( + summary.completion, + DerivedSummaryCompletion::Unknown | DerivedSummaryCompletion::BudgetExhausted + ) + { + stats.skipped_unknown += 1; + continue; + } + let callconv = self.callconv.clone(); + explorer.register_derived_call_hook( + target, + summary.clone(), + callconv.clone(), + move |state| apply_derived_summary(state, &summary, &callconv), + ); + stats.installed += 1; + } + stats + } +} + +fn compute_derived_summary_sccs( + helpers: &BTreeMap, +) -> Vec> { + let node_ids: Vec = helpers.keys().copied().collect(); + let node_set = node_ids.iter().copied().collect::>(); + let mut succs = BTreeMap::>::new(); + let mut rev = BTreeMap::>::new(); + + for node in &node_ids { + succs.entry(*node).or_default(); + rev.entry(*node).or_default(); + } + + for (id, helper) in helpers { + let mut out = helper + .prepared + .call_sites() + .by_id + .values() + .filter_map(|call| call.direct_target.map(InterprocFunctionId)) + .filter(|target| node_set.contains(target)) + .collect::>(); + out.sort_unstable(); + out.dedup(); + succs.insert(*id, out.clone()); + for succ in out { + rev.entry(succ).or_default().push(*id); + } + } + + for preds in rev.values_mut() { + preds.sort_unstable(); + preds.dedup(); + } + + let mut visited = BTreeSet::new(); + let mut order = Vec::new(); + for node in &node_ids { + dfs_summary_postorder(*node, &succs, &mut visited, &mut order); + } + + visited.clear(); + let mut sccs = Vec::new(); + while let Some(node) = order.pop() { + if visited.contains(&node) { + continue; + } + let mut component = Vec::new(); + dfs_summary_component(node, &rev, &mut visited, &mut component); + component.sort_unstable(); + sccs.push(component); + } + + sccs.reverse(); + sccs +} + +fn dfs_summary_postorder( + node: InterprocFunctionId, + succs: &BTreeMap>, + visited: &mut BTreeSet, + order: &mut Vec, +) { + if !visited.insert(node) { + return; + } + if let Some(nexts) = succs.get(&node) { + for next in nexts { + dfs_summary_postorder(*next, succs, visited, order); + } + } + order.push(node); +} + +fn dfs_summary_component( + node: InterprocFunctionId, + rev: &BTreeMap>, + visited: &mut BTreeSet, + component: &mut Vec, +) { + if !visited.insert(node) { + return; + } + component.push(node); + if let Some(preds) = rev.get(&node) { + for pred in preds { + dfs_summary_component(*pred, rev, visited, component); + } + } +} + +fn derived_summary_fingerprint(summary: &DerivedFunctionSummary<'_>) -> Vec { + let mut out = vec![ + format!("{:?}", summary.completion), + summary.arg_count_hint.to_string(), + summary.arg_symbols.len().to_string(), + summary.memory_inputs.len().to_string(), + summary.cases.len().to_string(), + ]; + out.extend( + summary + .arg_symbols + .iter() + .map(|(index, symbol)| format!("arg:{index}:{}", symbol)), + ); + out.extend( + summary + .memory_inputs + .iter() + .map(|input| format!("mem:{}:{}:{}", input.arg_index, input.size, input.symbol)), + ); + out.extend(summary.cases.iter().map(|case| { + let writes = case + .memory_writes + .iter() + .map(|write| { + format!( + "{}:{}:{}:{}", + write.arg_index, write.offset, write.size, write.value + ) + }) + .collect::>() + .join("|"); + format!( + "case:{}:{}:{}", + case.guard.simplify(), + case.return_value + .as_ref() + .map(|value| value.to_string()) + .unwrap_or_else(|| "".to_string()), + writes + ) + })); + out +} + +fn with_budget_exhausted_completion<'ctx>( + summary: &DerivedFunctionSummary<'ctx>, +) -> DerivedFunctionSummary<'ctx> { + let mut next = summary.clone(); + next.completion = DerivedSummaryCompletion::BudgetExhausted; + next +} + +fn build_interproc_summary_set( + scope: &PreparedFunctionScope, + arch: Option<&ArchSpec>, + symbol_map: &HashMap, +) -> InterprocSummarySet { + let mut inputs = Vec::new(); + let mut seeds = BTreeMap::new(); + for function in scope.functions().values() { + inputs.push(InterprocFunctionInput { + id: function.id, + name: function.name.clone(), + prepared: &function.prepared, + }); + if let Some(name) = symbol_map.get(&function.id.0) + && let Some(summary) = FunctionSemanticSummary::seed_for_name(function.id, name) + { + seeds.insert(function.id, summary); + } else if let Some(name) = function.name.as_deref() + && let Some(summary) = FunctionSemanticSummary::seed_for_name(function.id, name) + { + seeds.insert(function.id, summary); + } + } + + solve_interproc_summary_set( + &inputs, + arch, + Some(scope.root_id()), + &seeds, + InterprocSolveConfig::default(), + ) +} + +struct DerivedSummaryBuildInputs<'a, 'ctx> { + ctx: &'ctx z3::Context, + registry: &'a SummaryRegistry<'ctx>, + arch: Option<&'a ArchSpec>, + function: &'a ScopedPreparedFunction, + static_summary: &'a FunctionSemanticSummary, + interproc: &'a InterprocSummarySet, + derived_summaries: &'a BTreeMap>>, + symbol_map: &'a HashMap, +} + +fn derive_symbolic_summary_for_function<'ctx>( + inputs: DerivedSummaryBuildInputs<'_, 'ctx>, +) -> DerivedFunctionSummary<'ctx> { + let DerivedSummaryBuildInputs { + ctx, + registry, + arch, + function, + static_summary, + interproc, + derived_summaries, + symbol_map, + } = inputs; + let mut state = SymState::new(ctx, function.prepared.entry); + crate::runtime::seed_default_state_for_arch(&mut state, &function.prepared, arch); + let defines_return_value = function_defines_return_value(function, ®istry.callconv); + let opaque_return = matches!( + static_summary.return_relation, + SummaryReturnRelation::Unknown + ) && !defines_return_value; + + let mut arg_symbols = Vec::new(); + let helper_name = function + .name + .clone() + .unwrap_or_else(|| format!("sub_{:x}", function.id.0)); + let mut memory_inputs = Vec::new(); + let pointer_inputs = collect_pointer_memory_windows(static_summary); + + for index in 0..registry.callconv.arg_capacity() { + if let Some(reg) = registry.callconv.arg_register_name(index) { + let key = find_register_key(&state, reg) + .unwrap_or_else(|| format!("{}_0", reg.to_ascii_uppercase())); + let arg_bits = static_summary_arg_bits(®istry.callconv); + if let Some(window) = pointer_inputs.get(&index).copied() { + let ptr_base = helper_arg_region_base(function.id, index); + let region_start = ptr_base.saturating_sub(window.headroom as u64); + let region_size = window.headroom.saturating_add(window.forward_size.max(1)); + let _ = state.make_symbolic_memory( + region_start, + region_size.max(1), + &format!("{}_arg{}_mem", helper_name, index), + ); + let symbol = state.mem_read( + &SymValue::concrete(ptr_base, arg_bits), + window.forward_size.max(1), + ); + state.set_register(&key, SymValue::concrete(ptr_base, arg_bits)); + memory_inputs.push(DerivedSummaryInput { + arg_index: index, + symbol, + size: window.forward_size.max(1), + }); + } else { + if !state.registers().contains_key(&key) { + state.make_symbolic_named( + &key, + &format!("{}_arg{}", helper_name, index), + arg_bits, + ); + } + arg_symbols.push((index, state.get_register_sized(&key, arg_bits))); + } + } + } + + let config = crate::path::ExploreConfig { + max_states: DEFAULT_DERIVED_SUMMARY_MAX_STATES, + max_completed_paths: Some(DEFAULT_DERIVED_SUMMARY_MAX_PATHS), + max_depth: DEFAULT_DERIVED_SUMMARY_MAX_DEPTH, + timeout: None, + prune_infeasible: true, + merge_states: true, + subsumption_states: true, + ..crate::path::ExploreConfig::default() + }; + let mut explorer = PathExplorer::with_config(ctx, config); + let _ = registry.install_interproc_summaries_for_function( + &mut explorer, + &function.prepared, + interproc, + symbol_map, + ); + let _ = registry.install_derived_summaries_for_function( + &mut explorer, + &function.prepared, + derived_summaries, + symbol_map, + ); + let _ = + registry.install_known_symbols_for_function(&mut explorer, &function.prepared, symbol_map); + let summary = explorer.summarize_function(&function.prepared, state); + + let completion = if opaque_return { + DerivedSummaryCompletion::Unknown + } else if summary.stats.timed_out || summary.stats.max_states_exhausted { + DerivedSummaryCompletion::BudgetExhausted + } else if summary.paths.is_empty() { + DerivedSummaryCompletion::Unknown + } else if summary.paths.iter().all(|path| path.feasible) { + DerivedSummaryCompletion::Exact + } else { + DerivedSummaryCompletion::OverApprox + }; + + let mut cases = Vec::new(); + let mut write_locations = collect_tracked_memory_writes(static_summary); + write_locations.sort_unstable(); + write_locations.dedup(); + for path in summary.paths.iter().filter(|path| path.feasible) { + let mut memory_writes = Vec::new(); + for (arg_index, offset, size) in &write_locations { + let base_addr = helper_arg_region_base(function.id, *arg_index); + let addr = SymValue::concrete( + concrete_with_signed_offset(base_addr, *offset), + static_summary_arg_bits(®istry.callconv), + ); + let value = path.state.mem_read(&addr, *size); + memory_writes.push(DerivedMemoryWrite { + arg_index: *arg_index, + offset: *offset, + size: *size, + value, + }); + } + memory_writes = coalesce_adjacent_memory_writes(ctx, memory_writes); + + let return_value = match static_summary.return_relation { + SummaryReturnRelation::Void => None, + SummaryReturnRelation::Unknown if opaque_return => None, + _ => Some(registry.callconv.return_value(&path.state)), + }; + cases.push(DerivedSummaryCase { + guard: path.state.path_condition(), + return_value, + memory_writes, + }); + } + + DerivedFunctionSummary { + id: function.id, + name: function.name.clone(), + arg_count_hint: summary_arity(static_summary), + arg_symbols, + memory_inputs, + cases, + completion, + } +} + +fn function_defines_return_value(function: &ScopedPreparedFunction, callconv: &CallConv) -> bool { + let aliases = register_aliases(callconv.ret_register_name()); + function.prepared.blocks().any(|block| { + block.ops.iter().any(|op| { + op.dst().is_some_and(|dst| { + aliases + .iter() + .any(|alias| dst.name.eq_ignore_ascii_case(alias)) + }) + }) + }) +} + +fn coalesce_adjacent_memory_writes<'ctx>( + ctx: &'ctx z3::Context, + mut writes: Vec>, +) -> Vec> { + writes.sort_by_key(|write| (write.arg_index, write.offset, write.size)); + + let mut merged: Vec> = Vec::with_capacity(writes.len()); + for write in writes { + if let Some(last) = merged.last_mut() + && last.arg_index == write.arg_index + && last.offset.checked_add(last.size as i64) == Some(write.offset) + && let Some(new_size) = last.size.checked_add(write.size) + { + last.value = write.value.concat(ctx, &last.value); + last.size = new_size; + continue; + } + merged.push(write); + } + merged +} + +fn helper_arg_region_base(function: InterprocFunctionId, arg_index: usize) -> u64 { + 0x5000_0000u64 + .wrapping_add((function.0 & 0xffff) << 12) + .wrapping_add((arg_index as u64) << 8) + .wrapping_add(0x80) +} + +fn static_summary_arg_bits(callconv: &CallConv) -> u32 { + callconv.arg_bits +} + +fn collect_pointer_memory_windows( + summary: &FunctionSemanticSummary, +) -> BTreeMap { + let mut inputs = BTreeMap::new(); + for (index, effect) in &summary.arg_effects { + if !(effect.read || effect.write || effect.escape || effect.free) { + continue; + } + let mut window = PointerInputWindow { + headroom: 0, + forward_size: DEFAULT_MAX_INTERPROC_HAVOC as u32, + }; + let mut saw_precise_range = false; + for effect in &summary.memory_effects { + let SummaryMemoryRegion::Arg { + index: effect_index, + } = effect.location.region + else { + continue; + }; + if effect_index != *index { + continue; + } + let Some(range) = effect.location.range else { + continue; + }; + let width = range.width.unwrap_or(1).max(1); + let start = range.offset_lo.min(range.offset_hi); + let end = range.offset_hi.max(range.offset_lo); + let forward = if end >= 0 { + (end as u32).saturating_add(width) + } else { + width + }; + let headroom = if start < 0 { + start.checked_abs().unwrap_or(i64::MAX).min(u32::MAX as i64) as u32 + } else { + 0 + }; + window.forward_size = window.forward_size.max(forward.max(1)); + window.headroom = window.headroom.max(headroom); + saw_precise_range = true; + } + if !saw_precise_range { + window.forward_size = DEFAULT_MAX_INTERPROC_HAVOC as u32; + } + inputs.insert(*index, window); + } + inputs +} + +fn collect_tracked_memory_writes(summary: &FunctionSemanticSummary) -> Vec<(usize, i64, u32)> { + let mut writes = Vec::new(); + for effect in &summary.memory_effects { + if !matches!( + effect.kind, + SummaryMemoryEffectKind::Write + | SummaryMemoryEffectKind::Escape + | SummaryMemoryEffectKind::Free + ) { + continue; + } + let SummaryMemoryRegion::Arg { index } = effect.location.region else { + continue; + }; + let range = effect.location.range.unwrap_or(r2ssa::SummaryMemoryRange { + offset_lo: 0, + offset_hi: 0, + width: Some(1), + }); + let width = range.width.unwrap_or(1).max(1); + let start = range.offset_lo.min(range.offset_hi); + let end = range.offset_hi.max(range.offset_lo); + let mut offset = start; + while offset <= end { + writes.push((index, offset, width)); + let Some(next) = offset.checked_add(width as i64) else { + break; + }; + offset = next; + } + } + writes +} + +fn apply_derived_summary<'ctx>( + state: &mut SymState<'ctx>, + summary: &DerivedFunctionSummary<'ctx>, + callconv: &CallConv, +) -> CallHookResult { + let call = + callconv.collect_call_info(state, summary.arg_count_hint.max(callconv.arg_capacity())); + if summary.cases.is_empty() { + return CallHookResult::Fallthrough; + } + + let substitutions = build_summary_substitutions(state, summary, &call); + + if summary.cases.iter().any(|case| case.return_value.is_some()) { + let mut merged = SymValue::unknown(call.ret_bits); + for case in summary.cases.iter().rev() { + let Some(return_value) = &case.return_value else { + continue; + }; + let guard = substitute_bool(&case.guard, &substitutions); + let value = substitute_value(state.context(), return_value, &substitutions); + merged = ite_value(state.context(), &guard, &value, &merged); + } + callconv.write_return(state, merged); + } + + let mut writes = BTreeMap::<(usize, i64, u32), Vec<(Bool, SymValue<'ctx>)>>::new(); + for case in &summary.cases { + let guard = substitute_bool(&case.guard, &substitutions); + for write in &case.memory_writes { + let value = substitute_value(state.context(), &write.value, &substitutions); + writes + .entry((write.arg_index, write.offset, write.size)) + .or_default() + .push((guard.clone(), value)); + } + } + + for ((arg_index, offset, size), cases) in writes { + let Some(base) = call.args.get(arg_index) else { + continue; + }; + let addr = add_signed_offset(state.context(), base, offset, call.arg_bits); + let mut merged = state.mem_read(&addr, size); + for (guard, value) in cases.into_iter().rev() { + merged = ite_value(state.context(), &guard, &value, &merged); + } + state.mem_write(&addr, &merged, size); + } + + CallHookResult::Fallthrough +} + +pub(crate) fn evaluate_derived_summary_guidance<'ctx>( + state: &SymState<'ctx>, + summary: &DerivedFunctionSummary<'ctx>, + callconv: &CallConv, + solver: &SymSolver<'ctx>, +) -> DerivedSummaryGuidance { + let exact = matches!(summary.completion, DerivedSummaryCompletion::Exact); + if summary.cases.is_empty() { + return DerivedSummaryGuidance { + summary_known: true, + exact, + feasible_cases: 0, + contradictory: false, + }; + } + + let call = + callconv.collect_call_info(state, summary.arg_count_hint.max(callconv.arg_capacity())); + let substitutions = build_summary_substitutions(state, summary, &call); + let mut feasible_cases = 0; + let mut saw_unknown = false; + + for case in &summary.cases { + let Some(guard) = try_substitute_bool(&case.guard, &substitutions) else { + saw_unknown = true; + continue; + }; + match solver.sat_with_constraint(state, &guard) { + SatResult::Sat => feasible_cases += 1, + SatResult::Unknown => saw_unknown = true, + SatResult::Unsat => {} + } + } + + DerivedSummaryGuidance { + summary_known: true, + exact, + feasible_cases, + contradictory: exact && feasible_cases == 0 && !saw_unknown, + } +} + +fn concrete_with_signed_offset(base: u64, offset: i64) -> u64 { + if offset >= 0 { + base.wrapping_add(offset as u64) + } else { + base.wrapping_sub(offset.unsigned_abs()) + } +} + +fn add_signed_offset<'ctx>( + ctx: &'ctx z3::Context, + base: &SymValue<'ctx>, + offset: i64, + bits: u32, +) -> SymValue<'ctx> { + if offset >= 0 { + base.add(ctx, &SymValue::concrete(offset as u64, bits)) + } else { + base.sub(ctx, &SymValue::concrete(offset.unsigned_abs(), bits)) + } +} + +fn build_summary_substitutions<'ctx>( + state: &SymState<'ctx>, + summary: &DerivedFunctionSummary<'ctx>, + call: &CallInfo<'ctx>, +) -> Vec<(BV, BV)> { + let mut substitutions = Vec::new(); + for (index, symbol) in &summary.arg_symbols { + let Some(actual) = call.args.get(*index) else { + continue; + }; + let adjusted = adjust_bits(state.context(), actual.clone(), symbol.bits()); + substitutions.push(( + symbol.to_bv(state.context()), + adjusted.to_bv(state.context()), + )); + } + for input in &summary.memory_inputs { + let Some(base) = call.args.get(input.arg_index) else { + continue; + }; + let actual = adjust_bits( + state.context(), + state.mem_read(base, input.size), + input.symbol.bits(), + ); + substitutions.push(( + input.symbol.to_bv(state.context()), + actual.to_bv(state.context()), + )); + } + substitutions +} + +fn substitute_bool(ast: &Bool, substitutions: &[(BV, BV)]) -> Bool { + let pairs = substitutions + .iter() + .map(|(from, to)| (from, to)) + .collect::>(); + ast.substitute(&pairs) +} + +fn try_substitute_bool(ast: &Bool, substitutions: &[(BV, BV)]) -> Option { + catch_unwind(AssertUnwindSafe(|| substitute_bool(ast, substitutions))).ok() +} + +fn substitute_value<'ctx>( + _ctx: &'ctx z3::Context, + value: &SymValue<'ctx>, + substitutions: &[(BV, BV)], +) -> SymValue<'ctx> { + match value { + SymValue::Concrete { .. } | SymValue::Unknown { .. } => value.clone(), + SymValue::Symbolic { + ast, bits, taint, .. + } => { + let pairs = substitutions + .iter() + .map(|(from, to)| (from, to)) + .collect::>(); + SymValue::symbolic_tainted(ast.substitute(&pairs), *bits, *taint) + } + } +} + +fn ite_value<'ctx>( + ctx: &'ctx z3::Context, + guard: &Bool, + when_true: &SymValue<'ctx>, + when_false: &SymValue<'ctx>, +) -> SymValue<'ctx> { + let bits = when_true.bits().max(when_false.bits()); + let taint = when_true.get_taint() | when_false.get_taint(); + let true_bv = adjust_bits(ctx, when_true.clone(), bits).to_bv(ctx); + let false_bv = adjust_bits(ctx, when_false.clone(), bits).to_bv(ctx); + SymValue::symbolic_tainted(guard.ite(&true_bv, &false_bv), bits, taint) } fn apply_summary<'ctx>( @@ -512,10 +1509,14 @@ fn interproc_return_value<'ctx>( SummaryReturnRelation::Const(value) => Some(SymValue::concrete(value, call.ret_bits)), SummaryReturnRelation::Global(address) => Some(SymValue::concrete(address, call.ret_bits)), SummaryReturnRelation::HeapAlloc => { - let ret_ast = BV::fresh_const("interproc_heap_ptr", call.ret_bits); - let ret = SymValue::symbolic(ret_ast, call.ret_bits); - state.constrain_ne(&ret, 0); - Some(ret) + let size = call + .args + .first() + .and_then(SymValue::as_concrete) + .filter(|size| *size > 0) + .unwrap_or(0x100); + let (_region, base_addr) = state.allocate_heap_region("interproc_heap", size); + Some(SymValue::concrete(base_addr, call.ret_bits)) } SummaryReturnRelation::Unknown => Some(SymValue::unknown(call.ret_bits)), } @@ -1471,4 +2472,40 @@ mod tests { let path_listing_base = path_listing_state.mem_read(&dst, 1); assert_eq!(path_listing_base.get_taint(), 0x20); } + + #[test] + fn derived_summary_guidance_adjusts_arg_widths_before_substitution() { + let ctx = z3::Context::thread_local(); + let mut state = SymState::new(&ctx, 0); + state.set_register( + "RDI_0", + SymValue::symbolic(BV::fresh_const("call_arg", 64), 64), + ); + + let helper_arg = SymValue::symbolic(BV::fresh_const("helper_arg", 32), 32); + let summary = DerivedFunctionSummary { + id: InterprocFunctionId(0x401000), + name: Some("sym.helper_symbolic_zero".to_string()), + arg_count_hint: 1, + arg_symbols: vec![(0, helper_arg.clone())], + memory_inputs: Vec::new(), + cases: vec![DerivedSummaryCase { + guard: helper_arg.to_bv(&ctx).eq(helper_arg.to_bv(&ctx)), + return_value: Some(SymValue::concrete(0, 32)), + memory_writes: Vec::new(), + }], + completion: DerivedSummaryCompletion::Exact, + }; + + let guidance = evaluate_derived_summary_guidance( + &state, + &summary, + &CallConv::x86_64_sysv(), + &SymSolver::new(&ctx), + ); + + assert!(guidance.summary_known); + assert_eq!(guidance.feasible_cases, 1); + assert!(!guidance.contradictory); + } } diff --git a/crates/r2sym/src/solver.rs b/crates/r2sym/src/solver.rs index 64ca8b4..750a9d8 100644 --- a/crates/r2sym/src/solver.rs +++ b/crates/r2sym/src/solver.rs @@ -199,6 +199,16 @@ impl<'ctx> SymSolver<'ctx> { result == SatResult::Sat } + /// Check whether a state's constraints remain satisfiable with one extra constraint. + pub fn sat_with_constraint(&self, state: &SymState<'ctx>, constraint: &Bool) -> SatResult { + self.solver.push(); + self.solver_assert_all(state.constraints()); + self.solver_assert(constraint); + let result = self.check(); + self.solver.pop(1); + result + } + /// Get a concrete model for a state's constraints. pub fn solve(&self, state: &SymState<'ctx>) -> Option> { self.stats.borrow_mut().solve_calls += 1; @@ -227,6 +237,24 @@ impl<'ctx> SymSolver<'ctx> { result } + /// Check whether one state's path condition implies another's. + pub fn implies( + &self, + antecedent: &SymState<'ctx>, + consequent: &SymState<'ctx>, + ) -> Option { + self.solver.push(); + self.solver_assert_all(antecedent.constraints()); + self.solver_assert(&consequent.path_condition().not()); + let result = match self.check() { + SatResult::Unsat => Some(true), + SatResult::Sat => Some(false), + SatResult::Unknown => None, + }; + self.solver.pop(1); + result + } + /// Evaluate a symbolic value under the current model. pub fn eval(&self, value: &SymValue<'ctx>) -> Option { let model = self.get_model()?; diff --git a/crates/r2sym/src/state.rs b/crates/r2sym/src/state.rs index adef9d3..db0aabe 100644 --- a/crates/r2sym/src/state.rs +++ b/crates/r2sym/src/state.rs @@ -6,14 +6,16 @@ use std::collections::{HashMap, HashSet}; use z3::Context; -use z3::ast::{BV, Bool}; +use z3::ast::{Ast, BV, Bool}; -use crate::memory::SymMemory; +use crate::memory::{MemoryRegionId, MemoryRegionKind, SymMemory}; use crate::value::SymValue; /// A tracked symbolic memory region (usually an input buffer). #[derive(Debug, Clone)] pub struct SymbolicMemoryRegion<'ctx> { + /// Canonical region backing this symbolic buffer. + pub region_id: MemoryRegionId, /// Name of the symbolic buffer. pub name: String, /// Concrete address of the buffer. @@ -216,6 +218,114 @@ impl<'ctx> SymState<'ctx> { &self.runtime } + pub(crate) fn semantic_fingerprint(&self) -> String { + let mut registers: Vec<_> = self + .registers + .iter() + .map(|(name, value)| { + ( + name.clone(), + value.to_bv(self.ctx).simplify().to_string(), + value.get_taint(), + ) + }) + .collect(); + registers.sort_unstable_by(|a, b| a.0.cmp(&b.0)); + let registers_repr = registers + .into_iter() + .map(|(name, value, taint)| format!("{name}={value}@{taint}")) + .collect::>() + .join(","); + + let mut symbolic_inputs: Vec<_> = self + .symbolic_inputs + .iter() + .map(|(name, value)| { + ( + name.clone(), + value.to_bv(self.ctx).simplify().to_string(), + value.get_taint(), + ) + }) + .collect(); + symbolic_inputs.sort_unstable_by(|a, b| a.0.cmp(&b.0)); + let symbolic_inputs_repr = symbolic_inputs + .into_iter() + .map(|(name, value, taint)| format!("{name}={value}@{taint}")) + .collect::>() + .join(","); + + let mut symbolic_regions: Vec<_> = self + .symbolic_memory + .iter() + .map(|region| { + ( + region.region_id, + region.name.clone(), + region.addr, + region.size, + region.value.to_bv(self.ctx).simplify().to_string(), + region.value.get_taint(), + ) + }) + .collect(); + symbolic_regions.sort_unstable_by(|a, b| { + a.0.cmp(&b.0) + .then(a.1.cmp(&b.1)) + .then(a.2.cmp(&b.2)) + .then(a.3.cmp(&b.3)) + }); + let symbolic_regions_repr = symbolic_regions + .into_iter() + .map(|(region_id, name, addr, size, value, taint)| { + format!("{}:{name}@{addr:x}:{size}={value}@{taint}", region_id.0) + }) + .collect::>() + .join(","); + + let mut fd_inputs: Vec<_> = self + .symbolic_fd_inputs + .iter() + .map(|(fd, input)| { + let bytes = input + .bytes + .iter() + .map(|byte| format!("{}@{}", byte.to_bv(self.ctx).simplify(), byte.get_taint())) + .collect::>() + .join("|"); + (*fd, input.name.clone(), input.cursor, bytes) + }) + .collect(); + fd_inputs.sort_unstable_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1))); + let fd_inputs_repr = fd_inputs + .into_iter() + .map(|(fd, name, cursor, bytes)| format!("{fd}:{name}@{cursor}[{bytes}]")) + .collect::>() + .join(","); + + let mut tty_fds: Vec<_> = self.runtime.tty_fds.iter().copied().collect(); + tty_fds.sort_unstable(); + let tty_repr = tty_fds + .into_iter() + .map(|fd| fd.to_string()) + .collect::>() + .join(","); + + format!( + "pc={:x};active={};exit={:?};regs=[{}];memory=({});inputs=[{}];regions=[{}];fds=[{}];tty=[{}];skip_sleep={}", + self.pc, + self.active, + self.exit_status, + registers_repr, + self.memory.semantic_fingerprint(), + symbolic_inputs_repr, + symbolic_regions_repr, + fd_inputs_repr, + tty_repr, + self.runtime.skip_sleep_calls + ) + } + /// Read from memory. pub fn mem_read(&self, addr: &SymValue<'ctx>, size: u32) -> SymValue<'ctx> { self.memory @@ -284,29 +394,12 @@ impl<'ctx> SymState<'ctx> { } merged.registers = registers; - let mut memory = self.memory.fork(); - let mut addrs = HashSet::new(); - addrs.extend(self.memory.merge_addrs()); - addrs.extend(other.memory.merge_addrs()); - for addr in addrs { - let addr_val = SymValue::concrete(addr, 64); - let val_self = self - .memory - .read_with_constraints(&addr_val, 1, &self.constraints); - let val_other = other - .memory - .read_with_constraints(&addr_val, 1, &other.constraints); - let merged_val = merge_values(self.ctx, &cond_other, &val_self, &val_other); - memory.write(&addr_val, &merged_val, 1); - } - - for (addr, value, size) in other.memory.symbolic_writes() { - if addr.as_concrete().is_none() { - memory.push_symbolic_write(addr.clone(), value.clone(), *size); - } - } - - merged.memory = memory; + merged.memory = self.memory.merge_with( + &other.memory, + &self.constraints, + &other.constraints, + &cond_other, + ); merged.symbolic_inputs = self.symbolic_inputs.clone(); for (name, value) in &other.symbolic_inputs { @@ -318,10 +411,12 @@ impl<'ctx> SymState<'ctx> { merged.symbolic_memory = self.symbolic_memory.clone(); for region in &other.symbolic_memory { - let exists = merged - .symbolic_memory - .iter() - .any(|r| r.name == region.name && r.addr == region.addr && r.size == region.size); + let exists = merged.symbolic_memory.iter().any(|r| { + r.region_id == region.region_id + && r.name == region.name + && r.addr == region.addr + && r.size == region.size + }); if !exists { merged.symbolic_memory.push(region.clone()); } @@ -584,10 +679,13 @@ impl<'ctx> SymState<'ctx> { } else { SymValue::new_symbolic_tainted(self.ctx, name, size * 8, taint) }; + let region_id = + self.define_memory_region(MemoryRegionKind::Input, name, Some(addr), Some(size as u64)); let addr_val = SymValue::concrete(addr, 64); self.mem_write(&addr_val, &value, size); self.symbolic_inputs.insert(name.to_string(), value.clone()); self.symbolic_memory.push(SymbolicMemoryRegion { + region_id, name: name.to_string(), addr, size, @@ -596,6 +694,27 @@ impl<'ctx> SymState<'ctx> { value } + /// Define a canonical memory region for the symbolic state. + pub fn define_memory_region( + &mut self, + kind: MemoryRegionKind, + name: &str, + base_addr: Option, + extent: Option, + ) -> MemoryRegionId { + self.memory.define_region(kind, name, base_addr, extent) + } + + /// Seed concrete bytes into an existing memory region. + pub fn seed_region_bytes(&mut self, region_id: MemoryRegionId, offset: u64, bytes: &[u8]) { + self.memory.seed_region_bytes(region_id, offset, bytes); + } + + /// Allocate a deterministic heap region and return its concrete base pointer. + pub fn allocate_heap_region(&mut self, name: &str, size: u64) -> (MemoryRegionId, u64) { + self.memory.allocate_heap_region(name, size) + } + /// Configure tty behavior for a concrete file descriptor. pub fn set_tty_fd(&mut self, fd: i32, is_tty: bool) { if is_tty { @@ -911,4 +1030,52 @@ mod tests { .unwrap(); assert_eq!(val, 2); } + + #[test] + fn test_merge_memory_preserves_region_only_present_on_one_side() { + let ctx = Context::thread_local(); + + let mut state_a = SymState::new(&ctx, 0x1000); + let x_a = SymValue::new_symbolic(&ctx, "x", 32); + state_a.set_register("x", x_a.clone()); + state_a.add_constraint(x_a.to_bv(&ctx).eq(BV::from_u64(0, 32))); + + let mut state_b = SymState::new(&ctx, 0x1000); + let x_b = SymValue::new_symbolic(&ctx, "x", 32); + state_b.set_register("x", x_b.clone()); + state_b.add_constraint(x_b.to_bv(&ctx).eq(BV::from_u64(1, 32))); + state_b.define_memory_region(MemoryRegionKind::Replay, "replay", Some(0x9000), Some(0x10)); + state_b.mem_write( + &SymValue::concrete(0x9000, 64), + &SymValue::concrete(0xab, 8), + 1, + ); + + let merged = state_a.merge_with(&state_b); + let merged_byte = merged.mem_read(&SymValue::concrete(0x9000, 64), 1); + + let solver = Solver::new(); + solver.assert(merged.path_condition()); + solver.assert(x_b.to_bv(&ctx).eq(BV::from_u64(0, 32))); + assert_eq!(solver.check(), SatResult::Sat); + let model = solver.get_model().unwrap(); + let value = model + .eval(&merged_byte.to_bv(&ctx), true) + .unwrap() + .as_u64() + .unwrap(); + assert_eq!(value, 0); + + let solver = Solver::new(); + solver.assert(merged.path_condition()); + solver.assert(x_b.to_bv(&ctx).eq(BV::from_u64(1, 32))); + assert_eq!(solver.check(), SatResult::Sat); + let model = solver.get_model().unwrap(); + let value = model + .eval(&merged_byte.to_bv(&ctx), true) + .unwrap() + .as_u64() + .unwrap(); + assert_eq!(value, 0xab); + } } diff --git a/crates/r2sym/tests/symex_integration.rs b/crates/r2sym/tests/symex_integration.rs index f7d96f0..292dd8a 100644 --- a/crates/r2sym/tests/symex_integration.rs +++ b/crates/r2sym/tests/symex_integration.rs @@ -12,8 +12,16 @@ use r2sym::sim::{ SummaryRegistry, }; use r2sym::spec::{AddressValue, ExplorationSpec, InputSpec, PredicateSpec}; -use r2sym::{ExploreConfig, PathExplorer, SymState, SymValue}; +use r2sym::{ + BackwardConditionPrecision, BackwardMemoryCondition, BackwardMemoryRegion, + CompiledSemanticMode, ExploreConfig, PathExplorer, QueryCompletion, ReachabilityStatus, + ReplayRegisterOverlay, ReplayRegisterValue, ReplaySeed, SolveStatus, SymQueryConfig, SymState, + SymValue, SymbolicReachabilityStatus, collect_symbolic_function_facts, + collect_symbolic_function_facts_with_scope, compile_derived_summary_return_postcondition, + compile_function_semantics_with_scope, +}; use z3::Context; +use z3::ast::{BV, Bool}; // Helper functions for creating varnodes fn make_reg(offset: u64, size: u32) -> Varnode { @@ -34,11 +42,53 @@ fn make_const(val: u64, size: u32) -> Varnode { } } +fn assert_argument_memory_term( + term: &BackwardMemoryCondition, + index: usize, + offset_lo: i64, + offset_hi: i64, + exact_offset: bool, + size: u32, +) { + assert!(matches!( + &term.region, + BackwardMemoryRegion::Argument { index: actual } if *actual == index + )); + assert_eq!(term.offset_lo, offset_lo); + assert_eq!(term.offset_hi, offset_hi); + assert_eq!(term.exact_offset, exact_offset); + assert_eq!(term.size, size); +} + +fn assert_region_memory_term( + term: &BackwardMemoryCondition, + kind: r2sym::MemoryRegionKind, + name: &str, + offset_lo: i64, + offset_hi: i64, + exact_offset: bool, + size: u32, +) { + match &term.region { + BackwardMemoryRegion::Region(region) => { + assert_eq!(region.kind, kind); + assert_eq!(region.name, name); + } + other => panic!("expected concrete region-backed term, got {other:?}"), + } + assert_eq!(term.offset_lo, offset_lo); + assert_eq!(term.offset_hi, offset_hi); + assert_eq!(term.exact_offset, exact_offset); + assert_eq!(term.size, size); +} + fn make_x86_64_arch() -> ArchSpec { let mut arch = ArchSpec::new("x86-64"); arch.addr_size = 8; arch.add_register(RegisterDef::new("RAX", RAX, 8)); + arch.add_register(RegisterDef::new("EAX", RAX, 4)); arch.add_register(RegisterDef::new("RCX", RCX, 8)); + arch.add_register(RegisterDef::new("EDI", RDI, 4)); arch.add_register(RegisterDef::new("RDI", RDI, 8)); arch.add_register(RegisterDef::new("RSI", RSI, 8)); arch.add_register(RegisterDef::new("RDX", RDX, 8)); @@ -55,6 +105,197 @@ const RDX: u64 = 72; const TMP0: u64 = 0x80; const TMP1: u64 = 0x88; +fn make_conditional_branch_blocks() -> Vec { + vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![ + R2ILOp::IntEqual { + dst: make_reg(RCX, 1), + a: make_reg(RDI, 8), + b: make_const(0x1337, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(RCX, 1), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 6, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 6, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ] +} + +fn make_conditional_branch_blocks_low32_arg() -> Vec { + vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![ + R2ILOp::IntEqual { + dst: make_reg(RCX, 1), + a: make_reg(RDI, 4), + b: make_const(0xdead, 4), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(RCX, 1), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 6, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 6, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ] +} + +fn make_self_xor_guard_blocks() -> Vec { + vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![ + R2ILOp::IntXor { + dst: make_reg(TMP0, 8), + a: make_reg(RDI, 8), + b: make_reg(RDI, 8), + }, + R2ILOp::IntEqual { + dst: make_reg(RCX, 1), + a: make_reg(TMP0, 8), + b: make_const(0, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(RCX, 1), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ] +} + +fn make_target_guided_budget_blocks() -> Vec { + vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![ + R2ILOp::IntEqual { + dst: make_reg(RCX, 1), + a: make_reg(RDI, 8), + b: make_const(0x1337, 8), + }, + R2ILOp::CBranch { + target: make_const(0x2000, 8), + cond: make_reg(RCX, 1), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x1008, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1008, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x100c, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x100c, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x2000, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ] +} + #[test] fn test_symbolic_execution_linear_block() { // Test: Simple linear sequence of operations @@ -212,6 +453,7 @@ fn test_symbolic_execution_conditional_branch() { strategy: ExploreStrategy::Dfs, prune_infeasible: true, merge_states: false, + ..ExploreConfig::default() }; let mut explorer = PathExplorer::with_config(&ctx, config); @@ -233,221 +475,571 @@ fn test_symbolic_execution_conditional_branch() { } #[test] -fn test_find_paths_to_collects_multiple_matches() { +fn test_query_defaults_enable_subsumption() { + let config = SymQueryConfig::default(); + assert!(config.explore.subsumption_states); +} + +#[test] +fn test_query_target_guided_mode_enables_ranked_query_search() { + let ctx = Context::thread_local(); + let explorer = SymQueryConfig { + mode: r2sym::QueryMode::TargetGuided, + ..SymQueryConfig::default() + } + .make_explorer(&ctx); + assert!(explorer.target_guided_queries_enabled()); +} + +#[test] +fn test_query_can_reach_reports_reachable_and_unreachable() { + let blocks = make_conditional_branch_blocks(); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("Failed to build SSA function"); + let ctx = Context::thread_local(); + + let mut reachable_state = SymState::new(&ctx, 0x1000); + reachable_state.make_symbolic("reg:56_0", 64); + let mut explorer = SymQueryConfig { + mode: r2sym::QueryMode::TargetGuided, + ..SymQueryConfig::default() + } + .make_explorer(&ctx); + let reachable = explorer.can_reach(&func, reachable_state, 0x1010); + assert_eq!(reachable.status, ReachabilityStatus::Reachable); + assert!(!reachable.paths.is_empty()); + + let mut unreachable_state = SymState::new(&ctx, 0x1000); + unreachable_state.make_symbolic("reg:56_0", 64); + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let unreachable = explorer.can_reach(&func, unreachable_state, 0x2000); + assert_eq!(unreachable.status, ReachabilityStatus::Unreachable); + assert!(unreachable.paths.is_empty()); +} + +#[test] +fn test_query_solve_for_target_returns_solution() { + let blocks = make_conditional_branch_blocks(); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("Failed to build SSA function"); + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("reg:56_0", 64); + + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let solve = explorer.solve_for_target(&func, state, 0x1010); + assert_eq!(solve.status, SolveStatus::Solved); + assert_eq!( + solve + .compiled_precondition + .as_ref() + .map(|compiled| compiled.precision), + Some(BackwardConditionPrecision::Exact) + ); + assert!(solve.selected_path_index.is_some()); + assert!(solve.solution.is_some()); +} + +#[test] +fn test_query_path_conditions_at_collects_conditions() { + let blocks = make_conditional_branch_blocks(); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("Failed to build SSA function"); + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("reg:56_0", 64); + + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let conditions = explorer.path_conditions_at(&func, state, 0x1010); + assert_eq!( + conditions + .compiled_precondition + .as_ref() + .map(|compiled| compiled.precision), + Some(BackwardConditionPrecision::Exact) + ); + assert!(!conditions.conditions.is_empty()); + assert!( + conditions + .conditions + .iter() + .all(|condition| condition.final_pc == 0x1010) + ); + assert!( + conditions + .conditions + .iter() + .all(|condition| condition.num_constraints > 0) + ); + assert!(conditions.conditions.iter().all(|condition| { + condition.condition == condition.path_condition.simplified + && condition.path_condition.num_constraints == condition.num_constraints + && condition.path_condition.terms.len() == condition.num_constraints + })); +} + +#[test] +fn test_query_compiled_precondition_shortcuts_exact_unsat() { + let blocks = make_conditional_branch_blocks(); + let arch = make_x86_64_arch(); + let func = + SsaArtifact::for_symbolic(&blocks, Some(&arch)).expect("Failed to build SSA function"); + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic_named("RDI_0", "rdi", 64); + let rdi = state.get_register_sized("RDI_0", 64); + state.constrain_ne(&rdi, 0x1337); + + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let solve = explorer.solve_for_target(&func, state, 0x1010); + assert_eq!(solve.status, SolveStatus::Unsat); + assert!(solve.matched_paths.is_empty()); + assert_eq!( + solve + .compiled_precondition + .as_ref() + .map(|compiled| compiled.precision), + Some(BackwardConditionPrecision::Exact) + ); +} + +#[test] +fn test_query_load_dependent_precondition_compiles_through_local_store() { let blocks = vec![ R2ILBlock { addr: 0x1000, size: 4, + switch_info: None, + op_metadata: Default::default(), ops: vec![ + R2ILOp::Store { + addr: make_const(0x2000, 8), + val: make_reg(RDI, 8), + space: SpaceId::Ram, + }, + R2ILOp::Load { + dst: make_reg(TMP0, 8), + addr: make_const(0x2000, 8), + space: SpaceId::Ram, + }, R2ILOp::IntEqual { - dst: make_reg(RCX, 1), - a: make_reg(RDI, 8), + dst: make_reg(TMP1, 1), + a: make_reg(TMP0, 8), b: make_const(0x1337, 8), }, R2ILOp::CBranch { target: make_const(0x1010, 8), - cond: make_reg(RCX, 1), + cond: make_reg(TMP1, 1), }, ], - switch_info: None, - op_metadata: Default::default(), }, R2ILBlock { addr: 0x1004, - size: 4, - ops: vec![R2ILOp::Branch { - target: make_const(0x1010, 8), - }], + size: 1, switch_info: None, op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], }, R2ILBlock { addr: 0x1010, - size: 4, - ops: vec![R2ILOp::Copy { - dst: make_reg(RAX, 8), - src: make_const(1, 8), - }], + size: 1, switch_info: None, op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(1, 8), + }], }, ]; - - let func = SsaArtifact::for_symbolic(&blocks, None).expect("Failed to build SSA function"); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); state.make_symbolic("reg:56_0", 64); - let mut explorer = PathExplorer::new(&ctx); - let paths = explorer.find_paths_to(&func, state, 0x1010); - assert!( - paths.len() >= 2, - "Expected multiple target-reaching paths, got {}", - paths.len() + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let solve = explorer.solve_for_target(&func, state, 0x1010); + assert_eq!(solve.status, SolveStatus::Solved); + assert_eq!( + solve + .compiled_precondition + .as_ref() + .map(|compiled| compiled.precision), + Some(BackwardConditionPrecision::Exact) ); + assert!(!solve.matched_paths.is_empty()); + assert!(solve.solution.is_some()); } #[test] -fn test_find_paths_to_unreachable_returns_empty() { - let blocks = vec![R2ILBlock { - addr: 0x1000, - size: 4, - ops: vec![R2ILOp::Copy { - dst: make_reg(RAX, 8), - src: make_const(1, 8), - }], - switch_info: None, - op_metadata: Default::default(), - }]; - - let func = SsaArtifact::for_symbolic(&blocks, None).expect("Failed to build SSA function"); +fn test_query_load_dependent_precondition_emits_region_backed_global_term() { + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP0, 8), + addr: make_const(0x2000, 8), + space: SpaceId::Ram, + }, + R2ILOp::IntEqual { + dst: make_reg(TMP1, 1), + a: make_reg(TMP0, 8), + b: make_const(0x41, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP1, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1004, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(1, 8), + }], + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); let ctx = Context::thread_local(); - let state = SymState::new(&ctx, 0x1000); - let mut explorer = PathExplorer::new(&ctx); - let paths = explorer.find_paths_to(&func, state, 0x2000); - assert!( - paths.is_empty(), - "Expected no paths for unreachable target, got {}", - paths.len() + + let mut state = SymState::new(&ctx, 0x1000); + let region = state.define_memory_region( + r2sym::MemoryRegionKind::Global, + "global_2000", + Some(0x2000), + Some(8), + ); + state.seed_region_bytes(region, 0, &[0x41, 0, 0, 0, 0, 0, 0, 0]); + + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let conditions = explorer.path_conditions_at(&func, state, 0x1010); + let compiled = conditions + .compiled_precondition + .as_ref() + .expect("compiled precondition"); + + assert_eq!(compiled.precision, BackwardConditionPrecision::Exact); + assert!(!compiled.memory_terms.is_empty()); + assert_region_memory_term( + &compiled.memory_terms[0], + r2sym::MemoryRegionKind::Global, + "global_2000", + 0, + 0, + true, + 8, ); } #[test] -fn test_find_paths_to_honors_limits() { +fn test_query_load_dependent_precondition_emits_region_backed_stack_term() { let blocks = vec![ R2ILBlock { addr: 0x1000, size: 4, + switch_info: None, + op_metadata: Default::default(), ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP0, 8), + addr: make_const(0x7008, 8), + space: SpaceId::Ram, + }, R2ILOp::IntEqual { - dst: make_reg(RCX, 1), - a: make_reg(RDI, 8), - b: make_const(0, 8), + dst: make_reg(TMP1, 1), + a: make_reg(TMP0, 8), + b: make_const(0x41, 8), }, R2ILOp::CBranch { target: make_const(0x1010, 8), - cond: make_reg(RCX, 1), + cond: make_reg(TMP1, 1), }, ], + }, + R2ILBlock { + addr: 0x1004, + size: 1, switch_info: None, op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], }, R2ILBlock { addr: 0x1010, - size: 4, - ops: vec![R2ILOp::Copy { - dst: make_reg(RAX, 8), - src: make_const(1, 8), - }], + size: 1, switch_info: None, op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(1, 8), + }], }, ]; - - let func = SsaArtifact::for_symbolic(&blocks, None).expect("Failed to build SSA function"); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); let ctx = Context::thread_local(); - let mut state = SymState::new(&ctx, 0x1000); - state.make_symbolic("reg:56_0", 64); - let config = ExploreConfig { - max_states: 0, - max_completed_paths: None, - max_depth: 50, - timeout: None, - strategy: ExploreStrategy::Dfs, - prune_infeasible: true, - merge_states: false, - }; - let mut explorer = PathExplorer::with_config(&ctx, config); - let paths = explorer.find_paths_to(&func, state, 0x1010); - assert!( - paths.is_empty(), - "Expected no matches when max_states=0, got {}", - paths.len() + let mut state = SymState::new(&ctx, 0x1000); + let region = state.define_memory_region( + r2sym::MemoryRegionKind::Stack, + "stack_window", + Some(0x7000), + Some(0x100), + ); + state.seed_region_bytes(region, 8, &[0x41, 0, 0, 0, 0, 0, 0, 0]); + + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let conditions = explorer.path_conditions_at(&func, state, 0x1010); + let compiled = conditions + .compiled_precondition + .as_ref() + .expect("compiled precondition"); + + assert_eq!(compiled.precision, BackwardConditionPrecision::Exact); + assert!(!compiled.memory_terms.is_empty()); + assert_region_memory_term( + &compiled.memory_terms[0], + r2sym::MemoryRegionKind::Stack, + "stack_window", + 8, + 8, + true, + 8, ); } #[test] -fn test_find_paths_to_with_same_pc_merge_still_reaches_target() { +fn test_query_load_dependent_precondition_emits_region_backed_replay_term() { let blocks = vec![ R2ILBlock { addr: 0x1000, size: 4, + switch_info: None, + op_metadata: Default::default(), ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP0, 8), + addr: make_const(0x9004, 8), + space: SpaceId::Ram, + }, R2ILOp::IntEqual { - dst: make_reg(RCX, 1), - a: make_reg(RDI, 8), - b: make_const(0x1337, 8), + dst: make_reg(TMP1, 1), + a: make_reg(TMP0, 8), + b: make_const(0x41, 8), }, R2ILOp::CBranch { target: make_const(0x1010, 8), - cond: make_reg(RCX, 1), + cond: make_reg(TMP1, 1), }, ], - switch_info: None, - op_metadata: Default::default(), }, R2ILBlock { addr: 0x1004, - size: 4, - ops: vec![R2ILOp::Branch { - target: make_const(0x1010, 8), - }], + size: 1, switch_info: None, op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], }, R2ILBlock { addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(1, 8), + }], + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + let region = state.define_memory_region( + r2sym::MemoryRegionKind::Replay, + "replay_window", + Some(0x9000), + Some(0x100), + ); + state.seed_region_bytes(region, 4, &[0x41, 0, 0, 0, 0, 0, 0, 0]); + + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let conditions = explorer.path_conditions_at(&func, state, 0x1010); + let compiled = conditions + .compiled_precondition + .as_ref() + .expect("compiled precondition"); + + assert_eq!(compiled.precision, BackwardConditionPrecision::Exact); + assert!(!compiled.memory_terms.is_empty()); + assert_region_memory_term( + &compiled.memory_terms[0], + r2sym::MemoryRegionKind::Replay, + "replay_window", + 4, + 4, + true, + 8, + ); +} + +#[test] +fn test_query_load_dependent_precondition_emits_region_backed_heap_term() { + let blocks = vec![ + R2ILBlock { + addr: 0x1000, size: 4, - ops: vec![R2ILOp::Copy { - dst: make_reg(RAX, 8), - src: make_const(1, 8), + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP0, 8), + addr: make_const(0xa004, 8), + space: SpaceId::Ram, + }, + R2ILOp::IntEqual { + dst: make_reg(TMP1, 1), + a: make_reg(TMP0, 8), + b: make_const(0x41, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP1, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1004, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, switch_info: None, op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(1, 8), + }], }, ]; - - let func = SsaArtifact::for_symbolic(&blocks, None).expect("Failed to build SSA function"); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); - state.make_symbolic("reg:56_0", 64); + let region = state.define_memory_region( + r2sym::MemoryRegionKind::Heap, + "heap_window", + Some(0xa000), + Some(0x100), + ); + state.seed_region_bytes(region, 4, &[0x41, 0, 0, 0, 0, 0, 0, 0]); + + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let conditions = explorer.path_conditions_at(&func, state, 0x1010); + let compiled = conditions + .compiled_precondition + .as_ref() + .expect("compiled precondition"); + + assert_eq!(compiled.precision, BackwardConditionPrecision::Exact); + assert!(!compiled.memory_terms.is_empty()); + assert_region_memory_term( + &compiled.memory_terms[0], + r2sym::MemoryRegionKind::Heap, + "heap_window", + 4, + 4, + true, + 8, + ); +} - let config = ExploreConfig { - max_states: 100, - max_completed_paths: None, - max_depth: 50, - timeout: None, - strategy: ExploreStrategy::Bfs, - prune_infeasible: true, - merge_states: true, +#[test] +fn test_replay_seeded_query_solve_with_register_overlay() { + let blocks = make_conditional_branch_blocks(); + let arch = make_x86_64_arch(); + let func = + SsaArtifact::for_symbolic(&blocks, Some(&arch)).expect("Failed to build SSA function"); + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + let seed = ReplaySeed { + checkpoint_id: Some(2), + entry_pc: Some(0x1000), + registers: vec![ReplayRegisterValue { + name: "rdi".to_string(), + value: 0, + }], + register_overlays: vec![ReplayRegisterOverlay { + name: "rdi".to_string(), + symbol: "rdi".to_string(), + }], + ..ReplaySeed::default() }; - let mut explorer = PathExplorer::with_config(&ctx, config); - let paths = explorer.find_paths_to(&func, state, 0x1010); + r2sym::seed_replay_state_for_arch(&mut state, Some(&func), Some(&arch), &seed); - assert!( - !paths.is_empty(), - "Expected at least one feasible merged path to target" + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let solve = explorer.solve_for_target(&func, state, 0x1010); + assert_eq!(solve.status, SolveStatus::Solved); + assert_eq!( + solve + .compiled_precondition + .as_ref() + .map(|compiled| compiled.precision), + Some(BackwardConditionPrecision::Exact) ); - assert!(paths.iter().all(|path| path.final_pc() == 0x1010)); - assert!(paths.iter().all(|path| path.feasible)); + let solution = solve.solution.expect("solution"); + assert_eq!(solution.inputs.get("rdi").copied(), Some(0x1337)); } #[test] -fn test_explore_honors_max_completed_paths() { +fn test_query_compiles_bounded_indexed_memory_range() { + let arch = make_x86_64_arch(); let blocks = vec![ R2ILBlock { addr: 0x1000, size: 4, ops: vec![ - R2ILOp::IntEqual { - dst: make_reg(RCX, 1), + R2ILOp::IntAdd { + dst: make_reg(TMP0, 8), a: make_reg(RDI, 8), - b: make_const(0x1337, 8), + b: make_reg(RCX, 8), + }, + R2ILOp::Load { + dst: make_reg(TMP1, 1), + addr: make_reg(TMP0, 8), + space: SpaceId::Ram, + }, + R2ILOp::IntEqual { + dst: make_reg(RDX, 1), + a: make_reg(TMP1, 1), + b: make_const(0x41, 1), }, R2ILOp::CBranch { target: make_const(0x1010, 8), - cond: make_reg(RCX, 1), + cond: make_reg(RDX, 1), }, ], switch_info: None, @@ -455,424 +1047,3202 @@ fn test_explore_honors_max_completed_paths() { }, R2ILBlock { addr: 0x1004, - size: 4, - ops: vec![R2ILOp::Copy { - dst: make_reg(RAX, 8), - src: make_const(0, 8), + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), }], switch_info: None, op_metadata: Default::default(), }, R2ILBlock { addr: 0x1010, - size: 4, - ops: vec![R2ILOp::Copy { - dst: make_reg(RAX, 8), - src: make_const(1, 8), + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(1, 8), }], switch_info: None, op_metadata: Default::default(), }, ]; + let func = SsaArtifact::for_symbolic(&blocks, Some(&arch)).expect("ssa"); + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + r2sym::seed_default_state_for_arch(&mut state, &func, Some(&arch)); + let rcx = state.get_register_sized("RCX_0", 64); + let is_zero = rcx.to_bv(&ctx).eq(BV::from_u64(0, 64)); + let is_one = rcx.to_bv(&ctx).eq(BV::from_u64(1, 64)); + state.add_constraint(Bool::or(&[&is_zero, &is_one])); + + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let conditions = explorer.path_conditions_at(&func, state, 0x1010); + let compiled = conditions + .compiled_precondition + .as_ref() + .expect("compiled precondition"); + + assert_eq!(compiled.precision, BackwardConditionPrecision::Exact); + assert!(!compiled.memory_terms.is_empty()); + if matches!( + &compiled.memory_terms[0].region, + BackwardMemoryRegion::Argument { .. } + ) { + assert_eq!(compiled.memory_terms[0].offset_lo, 0); + assert_eq!(compiled.memory_terms[0].offset_hi, 1); + } else { + assert!(compiled.memory_terms[0].offset_hi >= compiled.memory_terms[0].offset_lo); + } + assert!(!compiled.memory_terms[0].exact_offset); + assert_eq!(compiled.memory_terms[0].size, 1); + assert!(compiled.backward_memory_candidate_enumerations > 0); + assert_eq!(compiled.backward_memory_residual_fallbacks, 0); +} + +#[test] +fn test_replay_seeded_query_solve_with_register_alias_overlay() { + let blocks = make_conditional_branch_blocks_low32_arg(); + let arch = make_x86_64_arch(); + let func = + SsaArtifact::for_symbolic(&blocks, Some(&arch)).expect("Failed to build SSA function"); + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + let seed = ReplaySeed { + checkpoint_id: Some(2), + entry_pc: Some(0x1000), + registers: vec![ReplayRegisterValue { + name: "rdi".to_string(), + value: 0, + }], + register_overlays: vec![ReplayRegisterOverlay { + name: "rdi".to_string(), + symbol: "rdi".to_string(), + }], + ..ReplaySeed::default() + }; + r2sym::seed_replay_state_for_arch(&mut state, Some(&func), Some(&arch), &seed); + + assert!(state.get_register("EDI_0").is_symbolic()); + assert!(!state.registers().contains_key("RDI")); + + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let solve = explorer.solve_for_target(&func, state, 0x1010); + assert_eq!(solve.status, SolveStatus::Solved); + let solution = solve.solution.expect("solution"); + assert_eq!(solution.inputs.get("rdi").copied(), Some(0xdead)); +} +#[test] +fn test_query_target_guided_can_reach_finds_target_under_tight_budget() { + let blocks = make_target_guided_budget_blocks(); let func = SsaArtifact::for_symbolic(&blocks, None).expect("Failed to build SSA function"); let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); state.make_symbolic("reg:56_0", 64); - let config = ExploreConfig { - max_states: 100, - max_completed_paths: Some(1), - max_depth: 50, - timeout: None, - strategy: ExploreStrategy::Bfs, - prune_infeasible: true, - merge_states: false, + let forward_config = SymQueryConfig { + explore: ExploreConfig { + max_states: 2, + ..ExploreConfig::default() + }, + mode: r2sym::QueryMode::ForwardOnly, + ..SymQueryConfig::default() }; - let mut explorer = PathExplorer::with_config(&ctx, config); - let paths = explorer.explore(&func, state); - - assert_eq!( - paths.len(), - 1, - "explore should stop after the configured cap" - ); + let mut forward = forward_config.make_explorer(&ctx); + let forward_result = forward.can_reach(&func, state.fork(), 0x2000); + assert_eq!(forward_result.status, ReachabilityStatus::BudgetExhausted); + assert!(forward_result.paths.is_empty()); + + let guided_config = SymQueryConfig { + explore: ExploreConfig { + max_states: 2, + ..ExploreConfig::default() + }, + mode: r2sym::QueryMode::TargetGuided, + ..SymQueryConfig::default() + }; + let mut guided = guided_config.make_explorer(&ctx); + let guided_result = guided.can_reach(&func, state, 0x2000); + assert_eq!(guided_result.status, ReachabilityStatus::Reachable); + assert_eq!(guided_result.paths.len(), 1); } #[test] -fn test_symbolic_arithmetic_operations() { - // Test all arithmetic operations with symbolic values +fn collect_symbolic_function_facts_prunes_self_xor_dead_branch() { + let arch = make_x86_64_arch(); + let func = SsaArtifact::for_symbolic(&make_self_xor_guard_blocks(), Some(&arch)) + .expect("symbolic ssa"); let ctx = Context::thread_local(); - let _state = SymState::new(&ctx, 0x1000); - - // Create symbolic values - let x = SymValue::new_symbolic(&ctx, "x", 64); - let y = SymValue::new_symbolic(&ctx, "y", 64); - - // Test addition - let sum = x.add(&ctx, &y); - assert!(sum.is_symbolic(), "Sum should be symbolic"); - - // Test subtraction - let diff = x.sub(&ctx, &y); - assert!(diff.is_symbolic(), "Diff should be symbolic"); - - // Test multiplication - let prod = x.mul(&ctx, &y); - assert!(prod.is_symbolic(), "Product should be symbolic"); - - // Test concrete operations - let a = SymValue::concrete(10, 64); - let b = SymValue::concrete(3, 64); - - let sum_concrete = a.add(&ctx, &b); - assert_eq!(sum_concrete.as_concrete(), Some(13)); - - let diff_concrete = a.sub(&ctx, &b); - assert_eq!(diff_concrete.as_concrete(), Some(7)); - - let prod_concrete = a.mul(&ctx, &b); - assert_eq!(prod_concrete.as_concrete(), Some(30)); - - let div_concrete = a.udiv(&ctx, &b); - assert_eq!(div_concrete.as_concrete(), Some(3)); - - let rem_concrete = a.urem(&ctx, &b); - assert_eq!(rem_concrete.as_concrete(), Some(1)); + let facts = collect_symbolic_function_facts( + &ctx, + &func, + Some(&arch), + &std::collections::HashMap::new(), + ); + let branch = facts + .branch_fact_for_block(0x1000) + .expect("entry branch fact"); + + assert_eq!(branch.true_target, 0x1010); + assert_eq!(branch.false_target, 0x1004); + assert_eq!(branch.true_status, SymbolicReachabilityStatus::Reachable); + assert_eq!(branch.false_status, SymbolicReachabilityStatus::Unreachable); + assert_eq!(facts.diagnostics.branches_pruned, 1); } #[test] -fn test_symbolic_bitwise_operations() { - let ctx = Context::thread_local(); - - // Test concrete bitwise - let a = SymValue::concrete(0b1100, 8); - let b = SymValue::concrete(0b1010, 8); - - assert_eq!(a.and(&ctx, &b).as_concrete(), Some(0b1000)); - assert_eq!(a.or(&ctx, &b).as_concrete(), Some(0b1110)); - assert_eq!(a.xor(&ctx, &b).as_concrete(), Some(0b0110)); - - // Test shifts - let amt = SymValue::concrete(2, 8); - assert_eq!(a.shl(&ctx, &amt).as_concrete(), Some(0b110000)); - assert_eq!(a.lshr(&ctx, &amt).as_concrete(), Some(0b0011)); - - // Test symbolic bitwise - let x = SymValue::new_symbolic(&ctx, "x", 64); - let y = SymValue::new_symbolic(&ctx, "y", 64); +fn collect_symbolic_function_facts_with_scope_prunes_helper_return_dead_branch() { + let arch = make_x86_64_arch(); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Call { + target: make_const(0x2000, 8), + }], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::IntNotEqual { + dst: make_reg(TMP0, 1), + a: make_reg(RAX, 8), + b: make_const(0, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP0, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + }, + ]; + let helper_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::IntXor { + dst: make_reg(RAX, 8), + a: make_reg(RDI, 8), + b: make_reg(RDI, 8), + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; - assert!(x.and(&ctx, &y).is_symbolic()); - assert!(x.or(&ctx, &y).is_symbolic()); - assert!(x.xor(&ctx, &y).is_symbolic()); -} + let root = + SsaArtifact::for_decompile(&root_blocks, Some(&arch)).expect("root decompile function"); + let helper = + SsaArtifact::for_symbolic(&helper_blocks, Some(&arch)).expect("helper symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x2000), + name: Some("helper_zero".to_string()), + prepared: helper, + }, + ], + ) + .expect("scope"); -#[test] -fn test_symbolic_comparisons() { let ctx = Context::thread_local(); + let facts = collect_symbolic_function_facts_with_scope( + &ctx, + &root, + Some(&scope), + Some(&arch), + &std::collections::HashMap::new(), + ); + let branch = facts + .branch_fact_for_block(0x1004) + .expect("post-call branch fact"); - let a = SymValue::concrete(10, 32); - let b = SymValue::concrete(20, 32); - - // Equality - assert_eq!(a.eq(&ctx, &a).as_concrete(), Some(1)); - assert_eq!(a.eq(&ctx, &b).as_concrete(), Some(0)); - - // Unsigned less than - assert_eq!(a.ult(&ctx, &b).as_concrete(), Some(1)); - assert_eq!(b.ult(&ctx, &a).as_concrete(), Some(0)); - - // Unsigned less than or equal - assert_eq!(a.ule(&ctx, &b).as_concrete(), Some(1)); - assert_eq!(a.ule(&ctx, &a).as_concrete(), Some(1)); - assert_eq!(b.ule(&ctx, &a).as_concrete(), Some(0)); + assert_eq!(branch.true_target, 0x1010); + assert_eq!(branch.false_target, 0x1008); + assert_eq!( + branch.true_status, + SymbolicReachabilityStatus::Unreachable, + "{branch:#?}" + ); + assert_eq!( + branch.false_status, + SymbolicReachabilityStatus::Reachable, + "{branch:#?}" + ); + assert_eq!(facts.diagnostics.branches_pruned, 1); } #[test] -fn test_symbolic_memory_operations() { - let ctx = Context::thread_local(); - - let mut state = SymState::new(&ctx, 0x1000); - - // Write concrete value to concrete address - let addr = SymValue::concrete(0x2000, 64); - let value = SymValue::concrete(0xDEADBEEF, 32); - state.mem_write(&addr, &value, 4); - - // Read it back - let read_value = state.mem_read(&addr, 4); - assert_eq!(read_value.as_concrete(), Some(0xDEADBEEF)); - - // Write symbolic value - let sym_value = SymValue::new_symbolic(&ctx, "mem_data", 64); - let addr2 = SymValue::concrete(0x3000, 64); - state.mem_write(&addr2, &sym_value, 8); +fn compile_function_semantics_with_scope_marks_helper_scope_compiled() { + let arch = make_x86_64_arch(); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Call { + target: make_const(0x2000, 8), + }], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::IntNotEqual { + dst: make_reg(TMP0, 1), + a: make_reg(RAX, 8), + b: make_const(0, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP0, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + }, + ]; + let helper_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::IntXor { + dst: make_reg(RAX, 8), + a: make_reg(RDI, 8), + b: make_reg(RDI, 8), + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; - // Read symbolic value - let read_sym = state.mem_read(&addr2, 8); - assert!(read_sym.is_symbolic()); -} + let root = + SsaArtifact::for_decompile(&root_blocks, Some(&arch)).expect("root decompile function"); + let helper = + SsaArtifact::for_symbolic(&helper_blocks, Some(&arch)).expect("helper symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x2000), + name: Some("helper_zero".to_string()), + prepared: helper, + }, + ], + ) + .expect("scope"); -#[test] -fn test_state_forking() { let ctx = Context::thread_local(); - - let mut state = SymState::new(&ctx, 0x1000); - state.set_concrete("rax", 42, 64); - state.make_symbolic("rbx", 64); - - // Fork the state - let forked = state.fork(); - - // Original and fork should have same values - assert_eq!(forked.pc, state.pc); - assert_eq!( - forked.get_register("rax").as_concrete(), - state.get_register("rax").as_concrete() + let compiled = compile_function_semantics_with_scope( + &ctx, + &root, + Some(&scope), + Some(&arch), + &std::collections::HashMap::new(), + r2sym::SummaryProfile::Default, ); - // Modifications to one shouldn't affect the other - state.set_concrete("rax", 100, 64); - assert_eq!(state.get_register("rax").as_concrete(), Some(100)); - assert_eq!(forked.get_register("rax").as_concrete(), Some(42)); + assert_eq!(compiled.mode, CompiledSemanticMode::Compiled); + assert_eq!(compiled.closure_functions, 2); + assert_eq!(compiled.helper_functions, 1); + assert!(compiled.derived_summaries >= 1); + assert_eq!(compiled.symbolic_facts.diagnostics.branches_pruned, 1); } #[test] -fn test_constraint_solving() { - use r2sym::SymSolver; - - let ctx = Context::thread_local(); - - let mut state = SymState::new(&ctx, 0x1000); - state.make_symbolic("x", 32); - - let x = state.get_register("x"); - - // Add constraint: x < 100 - let hundred = SymValue::concrete(100, 32); - let cond = x.ult(&ctx, &hundred); - state.add_true_constraint(&cond); +fn collect_symbolic_function_facts_with_scope_prunes_helper_return_spilled_dead_branch() { + let arch = make_x86_64_arch(); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Call { + target: make_const(0x2000, 8), + }], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Store { + space: SpaceId::Ram, + addr: Varnode { + space: SpaceId::Ram, + offset: 0x4000, + size: 8, + meta: None, + }, + val: make_reg(RAX, 8), + }, + R2ILOp::Load { + dst: make_reg(TMP0, 8), + space: SpaceId::Ram, + addr: Varnode { + space: SpaceId::Ram, + offset: 0x4000, + size: 8, + meta: None, + }, + }, + R2ILOp::IntNotEqual { + dst: make_reg(TMP1, 1), + a: make_reg(TMP0, 8), + b: make_const(0, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP1, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + }, + ]; + let helper_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::IntXor { + dst: make_reg(RAX, 8), + a: make_reg(RDI, 8), + b: make_reg(RDI, 8), + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; - // Solve - let solver = SymSolver::new(&ctx); - assert!(solver.is_sat(&state), "Constraints should be satisfiable"); + let root = + SsaArtifact::for_decompile(&root_blocks, Some(&arch)).expect("root decompile function"); + let helper = + SsaArtifact::for_symbolic(&helper_blocks, Some(&arch)).expect("helper symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x2000), + name: Some("helper_zero".to_string()), + prepared: helper, + }, + ], + ) + .expect("scope"); - let model = solver.solve(&state); - assert!(model.is_some(), "Should get a model"); + let ctx = Context::thread_local(); + let facts = collect_symbolic_function_facts_with_scope( + &ctx, + &root, + Some(&scope), + Some(&arch), + &std::collections::HashMap::new(), + ); + let branch = facts + .branch_fact_for_block(0x1004) + .expect("post-call branch fact"); - // The model should give us a value for x that is < 100 - let model = model.unwrap(); - if let Some(x_val) = model.eval(&x) { - assert!(x_val < 100, "x should be less than 100, got {}", x_val); - } + assert_eq!(branch.true_target, 0x1010); + assert_eq!(branch.false_target, 0x1008); + assert_eq!( + branch.true_status, + SymbolicReachabilityStatus::Unreachable, + "{branch:#?}" + ); + assert_eq!( + branch.false_status, + SymbolicReachabilityStatus::Reachable, + "{branch:#?}" + ); + assert_eq!(facts.diagnostics.branches_pruned, 1); } #[test] -fn test_unsatisfiable_constraints() { - use r2sym::SymSolver; - - let ctx = Context::thread_local(); - - let mut state = SymState::new(&ctx, 0x1000); - state.make_symbolic("x", 32); - - let x = state.get_register("x"); - - // Add contradictory constraints: x > 100 AND x < 50 - let hundred = SymValue::concrete(100, 32); - let fifty = SymValue::concrete(50, 32); - - // x > 100 (equivalent to 100 < x, or NOT(x <= 100)) - let gt_100 = hundred.ult(&ctx, &x); - state.add_true_constraint(>_100); +fn collect_symbolic_function_facts_with_scope_prunes_helper_return_dead_branch_via_eax_alias() { + let arch = make_x86_64_arch(); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Call { + target: make_const(0x2000, 8), + }], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::IntEqual { + dst: make_reg(TMP0, 1), + a: make_reg(RAX, 4), + b: make_const(0, 4), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP0, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0, 8), + }], + }, + ]; + let helper_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::IntXor { + dst: make_reg(RAX, 8), + a: make_reg(RDI, 8), + b: make_reg(RDI, 8), + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; - // x < 50 - let lt_50 = x.ult(&ctx, &fifty); - state.add_true_constraint(<_50); + let root = + SsaArtifact::for_decompile(&root_blocks, Some(&arch)).expect("root decompile function"); + let helper = + SsaArtifact::for_symbolic(&helper_blocks, Some(&arch)).expect("helper symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x2000), + name: Some("helper_zero".to_string()), + prepared: helper, + }, + ], + ) + .expect("scope"); - // Should be unsatisfiable - let solver = SymSolver::new(&ctx); - assert!( - !solver.is_sat(&state), - "Contradictory constraints should be unsatisfiable" + let ctx = Context::thread_local(); + let facts = collect_symbolic_function_facts_with_scope( + &ctx, + &root, + Some(&scope), + Some(&arch), + &std::collections::HashMap::new(), ); -} + let branch = facts + .branch_fact_for_block(0x1004) + .expect("post-call branch fact"); -#[test] -fn test_explore_config() { - let config = ExploreConfig::default(); - assert_eq!(config.max_states, 1000); - assert_eq!(config.max_depth, 100); - assert!(config.prune_infeasible); - assert!(!config.merge_states); - assert_eq!(config.strategy, ExploreStrategy::Dfs); + assert_eq!(branch.true_target, 0x1010); + assert_eq!(branch.false_target, 0x1008); + assert_eq!( + branch.true_status, + SymbolicReachabilityStatus::Reachable, + "{branch:#?}" + ); + assert_eq!( + branch.false_status, + SymbolicReachabilityStatus::Unreachable, + "{branch:#?}" + ); + assert_eq!(facts.diagnostics.branches_pruned, 1); } #[test] -fn test_path_result_properties() { - use r2sym::PathResult; - +fn test_query_summarize_function_marks_budget_exhausted() { + let blocks = make_conditional_branch_blocks(); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("Failed to build SSA function"); let ctx = Context::thread_local(); let mut state = SymState::new(&ctx, 0x1000); - state.set_register("rax", SymValue::concrete(42, 64)); - state.make_symbolic("rbx", 64); - - let result = PathResult::new(state, true); + state.make_symbolic("reg:56_0", 64); - assert_eq!(result.final_pc(), 0x1000); - assert_eq!(result.num_constraints(), 0); - assert!(result.register_names().contains(&"rax".to_string())); - assert_eq!(result.get_concrete_register("rax"), Some(42)); - assert!(result.is_register_symbolic("rbx")); + let query_config = SymQueryConfig { + explore: ExploreConfig { + max_states: 0, + ..ExploreConfig::default() + }, + ..SymQueryConfig::default() + }; + let mut explorer = query_config.make_explorer(&ctx); + let summary = explorer.summarize_function(&func, state); + assert_eq!(summary.completion, QueryCompletion::BudgetExhausted); } #[test] -fn test_different_bitwidth_operations() { - // Test that operations with different bit widths work correctly - let ctx = Context::thread_local(); +fn test_find_paths_to_collects_multiple_matches() { + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![ + R2ILOp::IntEqual { + dst: make_reg(RCX, 1), + a: make_reg(RDI, 8), + b: make_const(0x1337, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(RCX, 1), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x1010, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; - // 8-bit and 64-bit values - let val8 = SymValue::concrete(5, 8); - let val64 = SymValue::concrete(10, 64); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("Failed to build SSA function"); + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("reg:56_0", 64); - // Should handle mismatch gracefully - let result = val8.add(&ctx, &val64); - assert_eq!(result.as_concrete(), Some(15)); - assert_eq!(result.bits(), 64); // Result uses larger width + let mut explorer = PathExplorer::new(&ctx); + let paths = explorer.find_paths_to(&func, state, 0x1010); + assert!( + paths.len() >= 2, + "Expected multiple target-reaching paths, got {}", + paths.len() + ); +} - // Symbolic with different widths - let sym8 = SymValue::new_symbolic(&ctx, "x", 8); - let sym64 = SymValue::new_symbolic(&ctx, "y", 64); +#[test] +fn test_find_paths_to_unreachable_returns_empty() { + let blocks = vec![R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }]; - let sym_result = sym8.add(&ctx, &sym64); - assert!(sym_result.is_symbolic()); - assert_eq!(sym_result.bits(), 64); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("Failed to build SSA function"); + let ctx = Context::thread_local(); + let state = SymState::new(&ctx, 0x1000); + let mut explorer = PathExplorer::new(&ctx); + let paths = explorer.find_paths_to(&func, state, 0x2000); + assert!( + paths.is_empty(), + "Expected no paths for unreachable target, got {}", + paths.len() + ); } #[test] -fn memset_summary_writes_bounded_bytes() { +fn test_find_paths_to_honors_limits() { + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![ + R2ILOp::IntEqual { + dst: make_reg(RCX, 1), + a: make_reg(RDI, 8), + b: make_const(0, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(RCX, 1), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + + let func = SsaArtifact::for_symbolic(&blocks, None).expect("Failed to build SSA function"); let ctx = Context::thread_local(); let mut state = SymState::new(&ctx, 0x1000); - let summary = MemsetSummary::new(4); - let call = CallInfo { - args: vec![ - SymValue::concrete(0x2000, 64), - SymValue::concrete(0x41, 64), - SymValue::concrete(10, 64), - ], - arg_bits: 64, - ret_bits: 64, + state.make_symbolic("reg:56_0", 64); + + let config = ExploreConfig { + max_states: 0, + max_completed_paths: None, + max_depth: 50, + timeout: None, + strategy: ExploreStrategy::Dfs, + prune_infeasible: true, + merge_states: false, + ..ExploreConfig::default() }; + let mut explorer = PathExplorer::with_config(&ctx, config); + let paths = explorer.find_paths_to(&func, state, 0x1010); + assert!( + paths.is_empty(), + "Expected no matches when max_states=0, got {}", + paths.len() + ); +} - let effect = summary.execute(&mut state, &call); - match effect { - r2sym::SummaryEffect::Return(Some(ret)) => { - assert_eq!(ret.as_concrete(), Some(0x2000)); - } - _ => panic!("memset summary should return destination pointer"), - } +#[test] +fn test_find_paths_to_with_same_pc_merge_still_reaches_target() { + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![ + R2ILOp::IntEqual { + dst: make_reg(RCX, 1), + a: make_reg(RDI, 8), + b: make_const(0x1337, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(RCX, 1), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x1010, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + + let func = SsaArtifact::for_symbolic(&blocks, None).expect("Failed to build SSA function"); + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("reg:56_0", 64); + + let config = ExploreConfig { + max_states: 100, + max_completed_paths: None, + max_depth: 50, + timeout: None, + strategy: ExploreStrategy::Bfs, + prune_infeasible: true, + merge_states: true, + ..ExploreConfig::default() + }; + let mut explorer = PathExplorer::with_config(&ctx, config); + let paths = explorer.find_paths_to(&func, state, 0x1010); - let bytes = state - .memory - .read_bytes(0x2000, 4) - .expect("memset should materialize concrete bytes"); - assert_eq!(bytes, vec![0x41, 0x41, 0x41, 0x41]); assert!( - state.memory.read_bytes(0x2004, 1).is_none(), - "memset should be bounded by configured max" + !paths.is_empty(), + "Expected at least one feasible merged path to target" ); + assert!(paths.iter().all(|path| path.final_pc() == 0x1010)); + assert!(paths.iter().all(|path| path.feasible)); } #[test] -fn memcmp_summary_returns_trivalued_result() { +fn test_explore_honors_max_completed_paths() { + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![ + R2ILOp::IntEqual { + dst: make_reg(RCX, 1), + a: make_reg(RDI, 8), + b: make_const(0x1337, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(RCX, 1), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + + let func = SsaArtifact::for_symbolic(&blocks, None).expect("Failed to build SSA function"); let ctx = Context::thread_local(); let mut state = SymState::new(&ctx, 0x1000); - let summary = MemcmpSummary::new(32); - let call = CallInfo { - args: vec![ - SymValue::concrete(0x3000, 64), - SymValue::concrete(0x4000, 64), - SymValue::concrete(8, 64), + state.make_symbolic("reg:56_0", 64); + + let config = ExploreConfig { + max_states: 100, + max_completed_paths: Some(1), + max_depth: 50, + timeout: None, + strategy: ExploreStrategy::Bfs, + prune_infeasible: true, + merge_states: false, + ..ExploreConfig::default() + }; + let mut explorer = PathExplorer::with_config(&ctx, config); + let paths = explorer.explore(&func, state); + + assert_eq!( + paths.len(), + 1, + "explore should stop after the configured cap" + ); +} + +#[test] +fn test_symbolic_arithmetic_operations() { + // Test all arithmetic operations with symbolic values + let ctx = Context::thread_local(); + + let _state = SymState::new(&ctx, 0x1000); + + // Create symbolic values + let x = SymValue::new_symbolic(&ctx, "x", 64); + let y = SymValue::new_symbolic(&ctx, "y", 64); + + // Test addition + let sum = x.add(&ctx, &y); + assert!(sum.is_symbolic(), "Sum should be symbolic"); + + // Test subtraction + let diff = x.sub(&ctx, &y); + assert!(diff.is_symbolic(), "Diff should be symbolic"); + + // Test multiplication + let prod = x.mul(&ctx, &y); + assert!(prod.is_symbolic(), "Product should be symbolic"); + + // Test concrete operations + let a = SymValue::concrete(10, 64); + let b = SymValue::concrete(3, 64); + + let sum_concrete = a.add(&ctx, &b); + assert_eq!(sum_concrete.as_concrete(), Some(13)); + + let diff_concrete = a.sub(&ctx, &b); + assert_eq!(diff_concrete.as_concrete(), Some(7)); + + let prod_concrete = a.mul(&ctx, &b); + assert_eq!(prod_concrete.as_concrete(), Some(30)); + + let div_concrete = a.udiv(&ctx, &b); + assert_eq!(div_concrete.as_concrete(), Some(3)); + + let rem_concrete = a.urem(&ctx, &b); + assert_eq!(rem_concrete.as_concrete(), Some(1)); +} + +#[test] +fn test_symbolic_bitwise_operations() { + let ctx = Context::thread_local(); + + // Test concrete bitwise + let a = SymValue::concrete(0b1100, 8); + let b = SymValue::concrete(0b1010, 8); + + assert_eq!(a.and(&ctx, &b).as_concrete(), Some(0b1000)); + assert_eq!(a.or(&ctx, &b).as_concrete(), Some(0b1110)); + assert_eq!(a.xor(&ctx, &b).as_concrete(), Some(0b0110)); + + // Test shifts + let amt = SymValue::concrete(2, 8); + assert_eq!(a.shl(&ctx, &amt).as_concrete(), Some(0b110000)); + assert_eq!(a.lshr(&ctx, &amt).as_concrete(), Some(0b0011)); + + // Test symbolic bitwise + let x = SymValue::new_symbolic(&ctx, "x", 64); + let y = SymValue::new_symbolic(&ctx, "y", 64); + + assert!(x.and(&ctx, &y).is_symbolic()); + assert!(x.or(&ctx, &y).is_symbolic()); + assert!(x.xor(&ctx, &y).is_symbolic()); +} + +#[test] +fn test_symbolic_comparisons() { + let ctx = Context::thread_local(); + + let a = SymValue::concrete(10, 32); + let b = SymValue::concrete(20, 32); + + // Equality + assert_eq!(a.eq(&ctx, &a).as_concrete(), Some(1)); + assert_eq!(a.eq(&ctx, &b).as_concrete(), Some(0)); + + // Unsigned less than + assert_eq!(a.ult(&ctx, &b).as_concrete(), Some(1)); + assert_eq!(b.ult(&ctx, &a).as_concrete(), Some(0)); + + // Unsigned less than or equal + assert_eq!(a.ule(&ctx, &b).as_concrete(), Some(1)); + assert_eq!(a.ule(&ctx, &a).as_concrete(), Some(1)); + assert_eq!(b.ule(&ctx, &a).as_concrete(), Some(0)); +} + +#[test] +fn test_symbolic_memory_operations() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + + // Write concrete value to concrete address + let addr = SymValue::concrete(0x2000, 64); + let value = SymValue::concrete(0xDEADBEEF, 32); + state.mem_write(&addr, &value, 4); + + // Read it back + let read_value = state.mem_read(&addr, 4); + assert_eq!(read_value.as_concrete(), Some(0xDEADBEEF)); + + // Write symbolic value + let sym_value = SymValue::new_symbolic(&ctx, "mem_data", 64); + let addr2 = SymValue::concrete(0x3000, 64); + state.mem_write(&addr2, &sym_value, 8); + + // Read symbolic value + let read_sym = state.mem_read(&addr2, 8); + assert!(read_sym.is_symbolic()); +} + +#[test] +fn test_state_forking() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.set_concrete("rax", 42, 64); + state.make_symbolic("rbx", 64); + + // Fork the state + let forked = state.fork(); + + // Original and fork should have same values + assert_eq!(forked.pc, state.pc); + assert_eq!( + forked.get_register("rax").as_concrete(), + state.get_register("rax").as_concrete() + ); + + // Modifications to one shouldn't affect the other + state.set_concrete("rax", 100, 64); + assert_eq!(state.get_register("rax").as_concrete(), Some(100)); + assert_eq!(forked.get_register("rax").as_concrete(), Some(42)); +} + +#[test] +fn test_constraint_solving() { + use r2sym::SymSolver; + + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("x", 32); + + let x = state.get_register("x"); + + // Add constraint: x < 100 + let hundred = SymValue::concrete(100, 32); + let cond = x.ult(&ctx, &hundred); + state.add_true_constraint(&cond); + + // Solve + let solver = SymSolver::new(&ctx); + assert!(solver.is_sat(&state), "Constraints should be satisfiable"); + + let model = solver.solve(&state); + assert!(model.is_some(), "Should get a model"); + + // The model should give us a value for x that is < 100 + let model = model.unwrap(); + if let Some(x_val) = model.eval(&x) { + assert!(x_val < 100, "x should be less than 100, got {}", x_val); + } +} + +#[test] +fn test_unsatisfiable_constraints() { + use r2sym::SymSolver; + + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("x", 32); + + let x = state.get_register("x"); + + // Add contradictory constraints: x > 100 AND x < 50 + let hundred = SymValue::concrete(100, 32); + let fifty = SymValue::concrete(50, 32); + + // x > 100 (equivalent to 100 < x, or NOT(x <= 100)) + let gt_100 = hundred.ult(&ctx, &x); + state.add_true_constraint(>_100); + + // x < 50 + let lt_50 = x.ult(&ctx, &fifty); + state.add_true_constraint(<_50); + + // Should be unsatisfiable + let solver = SymSolver::new(&ctx); + assert!( + !solver.is_sat(&state), + "Contradictory constraints should be unsatisfiable" + ); +} + +#[test] +fn test_explore_config() { + let config = ExploreConfig::default(); + assert_eq!(config.max_states, 1000); + assert_eq!(config.max_depth, 100); + assert!(config.prune_infeasible); + assert!(!config.merge_states); + assert_eq!(config.strategy, ExploreStrategy::Dfs); +} + +#[test] +fn test_path_result_properties() { + use r2sym::PathResult; + + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.set_register("rax", SymValue::concrete(42, 64)); + state.make_symbolic("rbx", 64); + + let result = PathResult::new(state, true); + + assert_eq!(result.final_pc(), 0x1000); + assert_eq!(result.num_constraints(), 0); + assert!(result.register_names().contains(&"rax".to_string())); + assert_eq!(result.get_concrete_register("rax"), Some(42)); + assert!(result.is_register_symbolic("rbx")); +} + +#[test] +fn test_different_bitwidth_operations() { + // Test that operations with different bit widths work correctly + let ctx = Context::thread_local(); + + // 8-bit and 64-bit values + let val8 = SymValue::concrete(5, 8); + let val64 = SymValue::concrete(10, 64); + + // Should handle mismatch gracefully + let result = val8.add(&ctx, &val64); + assert_eq!(result.as_concrete(), Some(15)); + assert_eq!(result.bits(), 64); // Result uses larger width + + // Symbolic with different widths + let sym8 = SymValue::new_symbolic(&ctx, "x", 8); + let sym64 = SymValue::new_symbolic(&ctx, "y", 64); + + let sym_result = sym8.add(&ctx, &sym64); + assert!(sym_result.is_symbolic()); + assert_eq!(sym_result.bits(), 64); +} + +#[test] +fn memset_summary_writes_bounded_bytes() { + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + let summary = MemsetSummary::new(4); + let call = CallInfo { + args: vec![ + SymValue::concrete(0x2000, 64), + SymValue::concrete(0x41, 64), + SymValue::concrete(10, 64), + ], + arg_bits: 64, + ret_bits: 64, + }; + + let effect = summary.execute(&mut state, &call); + match effect { + r2sym::SummaryEffect::Return(Some(ret)) => { + assert_eq!(ret.as_concrete(), Some(0x2000)); + } + _ => panic!("memset summary should return destination pointer"), + } + + let bytes = state + .memory + .read_bytes(0x2000, 4) + .expect("memset should materialize concrete bytes"); + assert_eq!(bytes, vec![0x41, 0x41, 0x41, 0x41]); + assert!( + state.memory.read_bytes(0x2004, 1).is_none(), + "memset should be bounded by configured max" + ); +} + +#[test] +fn memcmp_summary_returns_trivalued_result() { + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + let summary = MemcmpSummary::new(32); + let call = CallInfo { + args: vec![ + SymValue::concrete(0x3000, 64), + SymValue::concrete(0x4000, 64), + SymValue::concrete(8, 64), + ], + arg_bits: 64, + ret_bits: 64, + }; + + let ret = match summary.execute(&mut state, &call) { + r2sym::SummaryEffect::Return(Some(ret)) => ret, + _ => panic!("memcmp summary should return a value"), + }; + + let solver = SymSolver::new(&ctx); + for allowed in [u64::MAX, 0, 1] { + let mut candidate = state.fork(); + candidate.constrain_eq(&ret, allowed); + assert!( + solver.is_sat(&candidate), + "memcmp return should allow value {allowed:#x}" + ); + } + + let mut disallowed = state.fork(); + disallowed.constrain_eq(&ret, 2); + assert!( + !solver.is_sat(&disallowed), + "memcmp return must be constrained to -1/0/1" + ); +} + +#[test] +fn printf_summary_does_not_terminate_path() { + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + let summary = PrintfSummaryBasic::new(32); + let call = CallInfo { + args: vec![SymValue::concrete(0x5000, 64)], + arg_bits: 64, + ret_bits: 64, + }; + + let effect = summary.execute(&mut state, &call); + assert!(matches!(effect, r2sym::SummaryEffect::Return(Some(_)))); + assert!( + state.active, + "printf summary should not terminate the state" + ); +} + +#[test] +fn puts_summary_does_not_terminate_path() { + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + let summary = PutsSummary::new(32); + let call = CallInfo { + args: vec![SymValue::concrete(0x6000, 64)], + arg_bits: 64, + ret_bits: 64, + }; + + let effect = summary.execute(&mut state, &call); + assert!(matches!(effect, r2sym::SummaryEffect::Return(Some(_)))); + assert!(state.active, "puts summary should not terminate the state"); +} + +#[test] +fn registry_with_core_contains_new_summaries() { + let ctx = Context::thread_local(); + let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); + let mut explorer = PathExplorer::new(&ctx); + + assert!(registry.install_for_explorer(&mut explorer, 0x1000, "memcmp")); + assert!(registry.install_for_explorer(&mut explorer, 0x1001, "memset")); + assert!(registry.install_for_explorer(&mut explorer, 0x1002, "puts")); + assert!(registry.install_for_explorer(&mut explorer, 0x1003, "printf")); +} + +#[test] +fn run_spec_fd_read_flows_into_load_compare_and_solution() { + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(RDI, 8), + src: make_const(0, 8), + }, + R2ILOp::Copy { + dst: make_reg(RSI, 8), + src: make_const(0x2000, 8), + }, + R2ILOp::Copy { + dst: make_reg(RDX, 8), + src: make_const(1, 8), + }, + R2ILOp::Call { + target: make_const(0x5000, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP0, 1), + addr: make_const(0x2000, 8), + space: SpaceId::Ram, + }, + R2ILOp::IntEqual { + dst: make_reg(TMP1, 1), + a: make_reg(TMP0, 1), + b: make_const(b'k' as u64, 1), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP1, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0x1337, 8), + }], + }, + ]; + + let arch = make_x86_64_arch(); + let func = + SsaArtifact::for_symbolic(&blocks, Some(&arch)).expect("Failed to build SSA function"); + let ctx = Context::thread_local(); + let mut initial_state = SymState::new(&ctx, 0x1000); + let mut explorer = PathExplorer::new(&ctx); + let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); + assert!(registry.install_for_explorer(&mut explorer, 0x5000, "read")); + + let spec = ExplorationSpec { + find: vec![PredicateSpec::Address { + addr: AddressValue::Integer(0x1010), + }], + inputs: vec![InputSpec::Fd { + fd: 0, + len: 1, + name: Some("stdin0".to_string()), + alphabet: Some("k".to_string()), + }], + ..Default::default() + }; + spec.apply_to_state(&mut initial_state); + + let result = explorer + .run_spec(&func, initial_state, &spec) + .expect("spec exploration should succeed"); + assert_eq!(result.found_paths.len(), 1, "expected one matching path"); + + let solved = explorer + .solve_path(&result.found_paths[0]) + .expect("found path should solve"); + assert_eq!(solved.final_pc, 0x1010); + assert_eq!( + solved.input_buffers.get("stdin0"), + Some(&vec![b'k']), + "solver should recover the byte that drives the branch" + ); +} + +#[test] +fn symbolic_n_constraints_bounded_for_memset_memcmp() { + let ctx = Context::thread_local(); + + let mut state_memset = SymState::new(&ctx, 0x1000); + let memset = MemsetSummary::new(8); + let n_memset = SymValue::new_symbolic(&ctx, "sym_memset_n", 64); + let call_memset = CallInfo { + args: vec![ + SymValue::concrete(0x7000, 64), + SymValue::concrete(0x7f, 64), + n_memset, + ], + arg_bits: 64, + ret_bits: 64, + }; + let before_memset = state_memset.num_constraints(); + let _ = memset.execute(&mut state_memset, &call_memset); + assert!( + state_memset.num_constraints() > before_memset, + "symbolic memset length should add bounds constraints" + ); + + let mut state_memcmp = SymState::new(&ctx, 0x1000); + let memcmp = MemcmpSummary::new(8); + let n_memcmp = SymValue::new_symbolic(&ctx, "sym_memcmp_n", 64); + let call_memcmp = CallInfo { + args: vec![ + SymValue::concrete(0x7100, 64), + SymValue::concrete(0x7200, 64), + n_memcmp, + ], + arg_bits: 64, + ret_bits: 64, + }; + let before_memcmp = state_memcmp.num_constraints(); + let _ = memcmp.execute(&mut state_memcmp, &call_memcmp); + assert!( + state_memcmp.num_constraints() > before_memcmp, + "symbolic memcmp length should add bounds constraints" + ); +} + +#[test] +fn derived_helper_summary_solves_return_transform() { + let arch = make_x86_64_arch(); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Call { + target: make_const(0x2000, 8), + }], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::IntEqual { + dst: make_reg(TMP0, 1), + a: make_reg(RAX, 8), + b: make_const(0x6a, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP0, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + }, + ]; + let helper_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::IntXor { + dst: make_reg(RAX, 8), + a: make_reg(RDI, 8), + b: make_const(0x55, 8), + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; + + let root = + SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic function"); + let helper = + SsaArtifact::for_symbolic(&helper_blocks, Some(&arch)).expect("helper symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x2000), + name: Some("helper_xor".to_string()), + prepared: helper, + }, + ], + ) + .expect("scope"); + + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + r2sym::seed_default_state_for_arch(&mut state, &root, Some(&arch)); + state.make_symbolic_named("RDI_0", "rdi", 64); + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); + let _ = registry.install_scope_summaries_for_explorer( + &mut explorer, + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + + let solve = explorer.solve_for_target(&root, state, 0x1010); + assert_eq!(solve.status, SolveStatus::Solved); + let solution = solve.solution.expect("solution"); + assert_eq!(solution.inputs.get("rdi").copied(), Some(0x3f)); +} + +#[test] +fn derived_helper_summary_solves_transitive_return_chain() { + let arch = make_x86_64_arch(); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Call { + target: make_const(0x2000, 8), + }], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::IntEqual { + dst: make_reg(TMP0, 1), + a: make_reg(RAX, 8), + b: make_const(0x6a, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP0, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(1, 8), + }], + }, + ]; + let helper1_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Call { + target: make_const(0x3000, 8), + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; + let helper2_blocks = vec![R2ILBlock { + addr: 0x3000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::IntXor { + dst: make_reg(RAX, 8), + a: make_reg(RDI, 8), + b: make_const(0x55, 8), + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; + + let root = + SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic function"); + let helper1 = + SsaArtifact::for_symbolic(&helper1_blocks, Some(&arch)).expect("helper1 symbolic function"); + let helper2 = + SsaArtifact::for_symbolic(&helper2_blocks, Some(&arch)).expect("helper2 symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x2000), + name: Some("helper_wrapper".to_string()), + prepared: helper1, + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x3000), + name: Some("helper_xor".to_string()), + prepared: helper2, + }, + ], + ) + .expect("scope"); + + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + r2sym::seed_default_state_for_arch(&mut state, &root, Some(&arch)); + state.make_symbolic_named("RDI_0", "rdi", 64); + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); + let derived = registry.derive_symbolic_summaries( + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + let wrapper_return = derived + .summaries + .get(&r2ssa::InterprocFunctionId(0x2000)) + .and_then(|summary| summary.cases.first()) + .and_then(|case| case.return_value.as_ref()) + .map(|value| value.to_bv(&ctx).to_string()) + .expect("wrapper return relation"); + assert!(wrapper_return.contains("helper_wrapper_arg0")); + assert!(wrapper_return.contains("0055")); + let diagnostics = registry.install_scope_summaries_for_explorer( + &mut explorer, + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + + let solve = explorer.solve_for_target(&root, state, 0x1010); + assert_eq!(solve.status, SolveStatus::Solved); + assert_eq!( + solve + .compiled_precondition + .as_ref() + .map(|compiled| compiled.precision), + Some(BackwardConditionPrecision::Exact) + ); + let solution = solve.solution.expect("solution"); + assert_eq!(solution.inputs.get("rdi").copied(), Some(0x3f)); + assert_eq!(diagnostics.scc_count, 2); + assert_eq!(diagnostics.max_scc_size, 1); +} + +#[test] +fn derived_helper_summary_preserves_pointer_write_value() { + let arch = make_x86_64_arch(); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(RDI, 8), + src: make_const(0x3000, 8), + }, + R2ILOp::Copy { + dst: make_reg(RSI, 1), + src: make_const(0x41, 1), + }, + R2ILOp::Call { + target: make_const(0x2000, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP0, 1), + addr: make_const(0x3000, 8), + space: SpaceId::Ram, + }, + R2ILOp::IntEqual { + dst: make_reg(TMP1, 1), + a: make_reg(TMP0, 1), + b: make_const(0x41, 1), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP1, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + ]; + let helper_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Store { + addr: make_reg(RDI, 8), + val: make_reg(RSI, 1), + space: SpaceId::Ram, + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; + + let root = + SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic function"); + let helper = + SsaArtifact::for_symbolic(&helper_blocks, Some(&arch)).expect("helper symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x2000), + name: Some("helper_store".to_string()), + prepared: helper, + }, + ], + ) + .expect("scope"); + + let ctx = Context::thread_local(); + let state = SymState::new(&ctx, 0x1000); + let mut explorer = PathExplorer::new(&ctx); + let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); + let _ = registry.install_scope_summaries_for_explorer( + &mut explorer, + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + + let paths = explorer.find_paths_to(&root, state, 0x1010); + assert_eq!(paths.len(), 1); +} + +#[test] +fn derived_helper_summary_compiles_backward_memory_terms() { + let arch = make_x86_64_arch(); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(RDI, 8), + src: make_const(0x3000, 8), + }, + R2ILOp::Copy { + dst: make_reg(RSI, 1), + src: make_const(0x41, 1), + }, + R2ILOp::Call { + target: make_const(0x2000, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP0, 1), + addr: make_const(0x3000, 8), + space: SpaceId::Ram, + }, + R2ILOp::IntEqual { + dst: make_reg(TMP1, 1), + a: make_reg(TMP0, 1), + b: make_const(0x41, 1), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP1, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + ]; + let helper_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Store { + addr: make_reg(RDI, 8), + val: make_reg(RSI, 1), + space: SpaceId::Ram, + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; + + let root = + SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic function"); + let helper = + SsaArtifact::for_symbolic(&helper_blocks, Some(&arch)).expect("helper symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x2000), + name: Some("helper_store".to_string()), + prepared: helper, + }, + ], + ) + .expect("scope"); + + let ctx = Context::thread_local(); + let state = SymState::new(&ctx, 0x1000); + let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); + let derived = registry.derive_symbolic_summaries( + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + let static_summary = derived + .interproc + .summaries + .get(&r2ssa::InterprocFunctionId(0x2000)) + .expect("static helper summary"); + assert!( + static_summary.memory_effects.iter().any(|effect| { + matches!( + effect.location.region, + r2ssa::SummaryMemoryRegion::Arg { index: 0 } + ) && effect + .location + .range + .is_some_and(|range| range.offset_lo == 0 && range.width.unwrap_or(0) >= 1) + }), + "expected static summary to record arg0 write, got {:?}", + static_summary.memory_effects + ); + let helper_summary = derived + .summaries + .get(&r2ssa::InterprocFunctionId(0x2000)) + .expect("derived helper summary"); + assert!( + helper_summary.cases.iter().any(|case| { + case.memory_writes + .iter() + .any(|write| write.arg_index == 0 && write.offset == 0 && write.size >= 1) + }), + "expected derived summary to record arg0 write, got {:?}", + helper_summary + .cases + .iter() + .flat_map(|case| case.memory_writes.iter().map(|write| ( + write.arg_index, + write.offset, + write.size + ))) + .collect::>() + ); + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let _ = registry.install_scope_summaries_for_explorer( + &mut explorer, + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + + let conditions = explorer.path_conditions_at(&root, state, 0x1010); + let compiled = conditions + .compiled_precondition + .as_ref() + .expect("compiled precondition"); + assert!(!compiled.memory_terms.is_empty()); + assert_argument_memory_term(&compiled.memory_terms[0], 0, 0, 0, true, 1); +} + +#[test] +fn derived_helper_summary_compiles_region_backed_global_memory_terms() { + let arch = make_x86_64_arch(); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(RDI, 8), + src: make_const(0x2000, 8), + }, + R2ILOp::Copy { + dst: make_reg(RSI, 1), + src: make_const(0x41, 1), + }, + R2ILOp::Call { + target: make_const(0x3000, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP0, 1), + addr: make_const(0x2000, 8), + space: SpaceId::Ram, + }, + R2ILOp::IntEqual { + dst: make_reg(TMP1, 1), + a: make_reg(TMP0, 1), + b: make_const(0x41, 1), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP1, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + ]; + let helper_blocks = vec![R2ILBlock { + addr: 0x3000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Store { + addr: make_reg(RDI, 8), + val: make_reg(RSI, 1), + space: SpaceId::Ram, + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; + + let root = + SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic function"); + let helper = + SsaArtifact::for_symbolic(&helper_blocks, Some(&arch)).expect("helper symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x3000), + name: Some("helper_store".to_string()), + prepared: helper, + }, + ], + ) + .expect("scope"); + + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.define_memory_region( + r2sym::MemoryRegionKind::Global, + "global_2000", + Some(0x2000), + Some(8), + ); + let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let _ = registry.install_scope_summaries_for_explorer( + &mut explorer, + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + + let conditions = explorer.path_conditions_at(&root, state, 0x1010); + let compiled = conditions + .compiled_precondition + .as_ref() + .expect("compiled precondition"); + assert_eq!(compiled.precision, BackwardConditionPrecision::Exact); + assert!(!compiled.memory_terms.is_empty()); + assert_region_memory_term( + &compiled.memory_terms[0], + r2sym::MemoryRegionKind::Global, + "global_2000", + 0, + 0, + true, + 1, + ); +} + +#[test] +fn derived_helper_summary_compiles_region_backed_stack_memory_terms() { + let arch = make_x86_64_arch(); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(RDI, 8), + src: make_const(0x7008, 8), + }, + R2ILOp::Copy { + dst: make_reg(RSI, 1), + src: make_const(0x41, 1), + }, + R2ILOp::Call { + target: make_const(0x3000, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP0, 1), + addr: make_const(0x7008, 8), + space: SpaceId::Ram, + }, + R2ILOp::IntEqual { + dst: make_reg(TMP1, 1), + a: make_reg(TMP0, 1), + b: make_const(0x41, 1), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP1, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + ]; + let helper_blocks = vec![R2ILBlock { + addr: 0x3000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Store { + addr: make_reg(RDI, 8), + val: make_reg(RSI, 1), + space: SpaceId::Ram, + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; + + let root = + SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic function"); + let helper = + SsaArtifact::for_symbolic(&helper_blocks, Some(&arch)).expect("helper symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x3000), + name: Some("helper_store".to_string()), + prepared: helper, + }, + ], + ) + .expect("scope"); + + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.define_memory_region( + r2sym::MemoryRegionKind::Stack, + "stack_window", + Some(0x7000), + Some(0x100), + ); + let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let _ = registry.install_scope_summaries_for_explorer( + &mut explorer, + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + + let conditions = explorer.path_conditions_at(&root, state, 0x1010); + let compiled = conditions + .compiled_precondition + .as_ref() + .expect("compiled precondition"); + assert_eq!(compiled.precision, BackwardConditionPrecision::Exact); + assert!(!compiled.memory_terms.is_empty()); + assert_region_memory_term( + &compiled.memory_terms[0], + r2sym::MemoryRegionKind::Stack, + "stack_window", + 8, + 8, + true, + 1, + ); +} + +#[test] +fn derived_helper_summary_compiles_region_backed_replay_memory_terms() { + let arch = make_x86_64_arch(); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(RDI, 8), + src: make_const(0x9004, 8), + }, + R2ILOp::Copy { + dst: make_reg(RSI, 1), + src: make_const(0x41, 1), + }, + R2ILOp::Call { + target: make_const(0x3000, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP0, 1), + addr: make_const(0x9004, 8), + space: SpaceId::Ram, + }, + R2ILOp::IntEqual { + dst: make_reg(TMP1, 1), + a: make_reg(TMP0, 1), + b: make_const(0x41, 1), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP1, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + ]; + let helper_blocks = vec![R2ILBlock { + addr: 0x3000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Store { + addr: make_reg(RDI, 8), + val: make_reg(RSI, 1), + space: SpaceId::Ram, + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; + + let root = + SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic function"); + let helper = + SsaArtifact::for_symbolic(&helper_blocks, Some(&arch)).expect("helper symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x3000), + name: Some("helper_store".to_string()), + prepared: helper, + }, + ], + ) + .expect("scope"); + + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.define_memory_region( + r2sym::MemoryRegionKind::Replay, + "replay_window", + Some(0x9000), + Some(0x100), + ); + let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let _ = registry.install_scope_summaries_for_explorer( + &mut explorer, + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + + let conditions = explorer.path_conditions_at(&root, state, 0x1010); + let compiled = conditions + .compiled_precondition + .as_ref() + .expect("compiled precondition"); + assert_eq!(compiled.precision, BackwardConditionPrecision::Exact); + assert!(!compiled.memory_terms.is_empty()); + assert_region_memory_term( + &compiled.memory_terms[0], + r2sym::MemoryRegionKind::Replay, + "replay_window", + 4, + 4, + true, + 1, + ); +} + +#[test] +fn derived_helper_summary_region_alias_falls_back_to_residual() { + let arch = make_x86_64_arch(); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(RDI, 8), + src: make_const(0x2000, 8), + }, + R2ILOp::Copy { + dst: make_reg(RSI, 8), + src: make_const(0x2000, 8), + }, + R2ILOp::Call { + target: make_const(0x3000, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP0, 1), + addr: make_const(0x2000, 8), + space: SpaceId::Ram, + }, + R2ILOp::IntEqual { + dst: make_reg(TMP1, 1), + a: make_reg(TMP0, 1), + b: make_const(0x42, 1), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP1, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + ]; + let helper_blocks = vec![R2ILBlock { + addr: 0x3000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Store { + addr: make_reg(RDI, 8), + val: make_const(0x41, 1), + space: SpaceId::Ram, + }, + R2ILOp::Store { + addr: make_reg(RSI, 8), + val: make_const(0x42, 1), + space: SpaceId::Ram, + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; + + let root = + SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic function"); + let helper = + SsaArtifact::for_symbolic(&helper_blocks, Some(&arch)).expect("helper symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x3000), + name: Some("helper_alias_store".to_string()), + prepared: helper, + }, + ], + ) + .expect("scope"); + + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.define_memory_region( + r2sym::MemoryRegionKind::Global, + "global_2000", + Some(0x2000), + Some(8), + ); + let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let _ = registry.install_scope_summaries_for_explorer( + &mut explorer, + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + + let conditions = explorer.path_conditions_at(&root, state, 0x1010); + let compiled = conditions + .compiled_precondition + .as_ref() + .expect("compiled precondition"); + assert_eq!( + compiled.precision, + BackwardConditionPrecision::ResidualSearchRequired + ); + assert!(compiled.backward_memory_residual_fallbacks > 0); + assert!(!compiled.memory_terms.is_empty()); + assert_region_memory_term( + &compiled.memory_terms[0], + r2sym::MemoryRegionKind::Global, + "global_2000", + 0, + 0, + true, + 1, + ); +} + +#[test] +fn derived_helper_summary_coalesces_adjacent_byte_writes_into_slice() { + let arch = make_x86_64_arch(); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(RDI, 8), + src: make_const(0x3000, 8), + }, + R2ILOp::Call { + target: make_const(0x2000, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP0, 2), + addr: make_const(0x3000, 8), + space: SpaceId::Ram, + }, + R2ILOp::IntEqual { + dst: make_reg(TMP1, 1), + a: make_reg(TMP0, 2), + b: make_const(0x4241, 2), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP1, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + ]; + let helper_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Store { + addr: make_reg(RDI, 8), + val: make_const(0x41, 1), + space: SpaceId::Ram, + }, + R2ILOp::IntAdd { + dst: make_reg(TMP0, 8), + a: make_reg(RDI, 8), + b: make_const(1, 8), + }, + R2ILOp::Store { + addr: make_reg(TMP0, 8), + val: make_const(0x42, 1), + space: SpaceId::Ram, + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; + + let root = + SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic function"); + let helper = + SsaArtifact::for_symbolic(&helper_blocks, Some(&arch)).expect("helper symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x2000), + name: Some("helper_store_pair".to_string()), + prepared: helper, + }, + ], + ) + .expect("scope"); + + let ctx = Context::thread_local(); + let state = SymState::new(&ctx, 0x1000); + let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); + let derived = registry.derive_symbolic_summaries( + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + let helper_summary = derived + .summaries + .get(&r2ssa::InterprocFunctionId(0x2000)) + .expect("derived helper summary"); + assert!( + helper_summary.cases.iter().any(|case| { + case.memory_writes + .iter() + .any(|write| write.arg_index == 0 && write.offset == 0 && write.size == 2) + }), + "expected derived summary to coalesce adjacent byte writes into one 2-byte slice, got {:?}", + helper_summary + .cases + .iter() + .flat_map(|case| case.memory_writes.iter().map(|write| ( + write.arg_index, + write.offset, + write.size + ))) + .collect::>() + ); + + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let _ = registry.install_scope_summaries_for_explorer( + &mut explorer, + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + + let conditions = explorer.path_conditions_at(&root, state, 0x1010); + let compiled = conditions + .compiled_precondition + .as_ref() + .expect("compiled precondition"); + assert_eq!(compiled.precision, BackwardConditionPrecision::Exact); + assert!(!compiled.memory_terms.is_empty()); + assert_argument_memory_term(&compiled.memory_terms[0], 0, 0, 0, true, 2); +} + +#[test] +fn derived_helper_summary_compiles_backward_memory_slice_at_offset() { + let arch = make_x86_64_arch(); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(RDI, 8), + src: make_const(0x3000, 8), + }, + R2ILOp::Copy { + dst: make_reg(RSI, 8), + src: make_const(0x4142, 8), + }, + R2ILOp::Call { + target: make_const(0x2000, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP1, 2), + addr: make_const(0x3002, 8), + space: SpaceId::Ram, + }, + R2ILOp::IntEqual { + dst: make_reg(RCX, 1), + a: make_reg(TMP1, 2), + b: make_const(0x4142, 2), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(RCX, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + ]; + let helper_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::IntAdd { + dst: make_reg(TMP0, 8), + a: make_reg(RDI, 8), + b: make_const(2, 8), + }, + R2ILOp::Store { + addr: make_reg(TMP0, 8), + val: make_reg(RSI, 2), + space: SpaceId::Ram, + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; + + let root = + SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic function"); + let helper = + SsaArtifact::for_symbolic(&helper_blocks, Some(&arch)).expect("helper symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x2000), + name: Some("helper_store_offset".to_string()), + prepared: helper, + }, + ], + ) + .expect("scope"); + + let ctx = Context::thread_local(); + let state = SymState::new(&ctx, 0x1000); + let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); + let derived = registry.derive_symbolic_summaries( + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + let static_summary = derived + .interproc + .summaries + .get(&r2ssa::InterprocFunctionId(0x2000)) + .expect("static helper summary"); + assert!( + static_summary.memory_effects.iter().any(|effect| { + matches!( + effect.location.region, + r2ssa::SummaryMemoryRegion::Arg { index: 0 } + ) && effect + .location + .range + .is_some_and(|range| range.offset_lo == 2 && range.width.unwrap_or(0) >= 2) + }), + "expected static summary to record arg0+2 write, got {:?}", + static_summary.memory_effects + ); + let helper_summary = derived + .summaries + .get(&r2ssa::InterprocFunctionId(0x2000)) + .expect("derived helper summary"); + assert!( + helper_summary.cases.iter().any(|case| { + case.memory_writes + .iter() + .any(|write| write.arg_index == 0 && write.offset == 2 && write.size >= 2) + }), + "expected derived summary to record arg0+2 write" + ); + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let _ = registry.install_scope_summaries_for_explorer( + &mut explorer, + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + + let conditions = explorer.path_conditions_at(&root, state, 0x1010); + let compiled = conditions + .compiled_precondition + .as_ref() + .expect("compiled precondition"); + assert_eq!(compiled.precision, BackwardConditionPrecision::Exact); + assert!(!compiled.memory_terms.is_empty()); + assert_argument_memory_term(&compiled.memory_terms[0], 0, 2, 2, true, 2); +} + +#[test] +fn derived_helper_summary_compiles_backward_memory_slice_via_ptradd() { + let arch = make_x86_64_arch(); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(RDI, 8), + src: make_const(0x3000, 8), + }, + R2ILOp::Copy { + dst: make_reg(RSI, 8), + src: make_const(0x4142, 8), + }, + R2ILOp::Call { + target: make_const(0x2000, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP1, 2), + addr: make_const(0x3002, 8), + space: SpaceId::Ram, + }, + R2ILOp::IntEqual { + dst: make_reg(RCX, 1), + a: make_reg(TMP1, 2), + b: make_const(0x4142, 2), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(RCX, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + ]; + let helper_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::PtrAdd { + dst: make_reg(TMP0, 8), + base: make_reg(RDI, 8), + index: make_const(1, 8), + element_size: 2, + }, + R2ILOp::Store { + addr: make_reg(TMP0, 8), + val: make_reg(RSI, 2), + space: SpaceId::Ram, + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; + + let root = + SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic function"); + let helper = + SsaArtifact::for_symbolic(&helper_blocks, Some(&arch)).expect("helper symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x2000), + name: Some("helper_store_ptradd".to_string()), + prepared: helper, + }, + ], + ) + .expect("scope"); + + let ctx = Context::thread_local(); + let state = SymState::new(&ctx, 0x1000); + let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); + let derived = registry.derive_symbolic_summaries( + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + let helper_summary = derived + .summaries + .get(&r2ssa::InterprocFunctionId(0x2000)) + .expect("derived helper summary"); + assert!( + helper_summary.cases.iter().any(|case| { + case.memory_writes + .iter() + .any(|write| write.arg_index == 0 && write.offset == 2 && write.size >= 2) + }), + "expected derived summary to record arg0+2 write" + ); + + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let _ = registry.install_scope_summaries_for_explorer( + &mut explorer, + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + + let conditions = explorer.path_conditions_at(&root, state, 0x1010); + let compiled = conditions + .compiled_precondition + .as_ref() + .expect("compiled precondition"); + assert_eq!(compiled.precision, BackwardConditionPrecision::Exact); + assert!(!compiled.memory_terms.is_empty()); + assert_argument_memory_term(&compiled.memory_terms[0], 0, 2, 2, true, 2); +} + +#[test] +fn derived_helper_summary_preserves_negative_pointer_write_value() { + let arch = make_x86_64_arch(); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(RDI, 8), + src: make_const(0x3001, 8), + }, + R2ILOp::Copy { + dst: make_reg(RSI, 1), + src: make_const(0x41, 1), + }, + R2ILOp::Call { + target: make_const(0x2000, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP0, 1), + addr: make_const(0x3000, 8), + space: SpaceId::Ram, + }, + R2ILOp::IntEqual { + dst: make_reg(TMP1, 1), + a: make_reg(TMP0, 1), + b: make_const(0x41, 1), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP1, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + ]; + let helper_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::IntSub { + dst: make_reg(TMP0, 8), + a: make_reg(RDI, 8), + b: make_const(1, 8), + }, + R2ILOp::Store { + addr: make_reg(TMP0, 8), + val: make_reg(RSI, 1), + space: SpaceId::Ram, + }, + R2ILOp::Return { + target: make_const(0, 8), + }, ], - arg_bits: 64, - ret_bits: 64, - }; - - let ret = match summary.execute(&mut state, &call) { - r2sym::SummaryEffect::Return(Some(ret)) => ret, - _ => panic!("memcmp summary should return a value"), - }; + }]; - let solver = SymSolver::new(&ctx); - for allowed in [u64::MAX, 0, 1] { - let mut candidate = state.fork(); - candidate.constrain_eq(&ret, allowed); - assert!( - solver.is_sat(&candidate), - "memcmp return should allow value {allowed:#x}" - ); - } + let root = + SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic function"); + let helper = + SsaArtifact::for_symbolic(&helper_blocks, Some(&arch)).expect("helper symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x2000), + name: Some("helper_store_prev".to_string()), + prepared: helper, + }, + ], + ) + .expect("scope"); - let mut disallowed = state.fork(); - disallowed.constrain_eq(&ret, 2); + let ctx = Context::thread_local(); + let state = SymState::new(&ctx, 0x1000); + let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); + let derived = registry.derive_symbolic_summaries( + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + let static_summary = derived + .interproc + .summaries + .get(&r2ssa::InterprocFunctionId(0x2000)) + .expect("static helper summary"); assert!( - !solver.is_sat(&disallowed), - "memcmp return must be constrained to -1/0/1" + static_summary.memory_effects.iter().any(|effect| { + matches!( + effect.location.region, + r2ssa::SummaryMemoryRegion::Arg { index: 0 } + ) && effect + .location + .range + .is_some_and(|range| range.offset_lo == -1 && range.width.unwrap_or(0) >= 1) + }), + "expected static summary to record arg0-1 write, got {:?}", + static_summary.memory_effects ); -} - -#[test] -fn printf_summary_does_not_terminate_path() { - let ctx = Context::thread_local(); - let mut state = SymState::new(&ctx, 0x1000); - let summary = PrintfSummaryBasic::new(32); - let call = CallInfo { - args: vec![SymValue::concrete(0x5000, 64)], - arg_bits: 64, - ret_bits: 64, - }; - - let effect = summary.execute(&mut state, &call); - assert!(matches!(effect, r2sym::SummaryEffect::Return(Some(_)))); + let helper_summary = derived + .summaries + .get(&r2ssa::InterprocFunctionId(0x2000)) + .expect("derived helper summary"); assert!( - state.active, - "printf summary should not terminate the state" + helper_summary.cases.iter().any(|case| { + case.memory_writes + .iter() + .any(|write| write.arg_index == 0 && write.offset == -1 && write.size >= 1) + }), + "expected derived summary to record arg0-1 write, got {:?}", + helper_summary + .cases + .iter() + .flat_map(|case| case.memory_writes.iter().map(|write| ( + write.arg_index, + write.offset, + write.size + ))) + .collect::>() + ); + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let _ = registry.install_scope_summaries_for_explorer( + &mut explorer, + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), ); -} - -#[test] -fn puts_summary_does_not_terminate_path() { - let ctx = Context::thread_local(); - let mut state = SymState::new(&ctx, 0x1000); - let summary = PutsSummary::new(32); - let call = CallInfo { - args: vec![SymValue::concrete(0x6000, 64)], - arg_bits: 64, - ret_bits: 64, - }; - - let effect = summary.execute(&mut state, &call); - assert!(matches!(effect, r2sym::SummaryEffect::Return(Some(_)))); - assert!(state.active, "puts summary should not terminate the state"); -} - -#[test] -fn registry_with_core_contains_new_summaries() { - let ctx = Context::thread_local(); - let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); - let mut explorer = PathExplorer::new(&ctx); - assert!(registry.install_for_explorer(&mut explorer, 0x1000, "memcmp")); - assert!(registry.install_for_explorer(&mut explorer, 0x1001, "memset")); - assert!(registry.install_for_explorer(&mut explorer, 0x1002, "puts")); - assert!(registry.install_for_explorer(&mut explorer, 0x1003, "printf")); + let conditions = explorer.path_conditions_at(&root, state, 0x1010); + let compiled = conditions + .compiled_precondition + .as_ref() + .expect("compiled precondition"); + assert_eq!(compiled.precision, BackwardConditionPrecision::Exact); + assert!(!compiled.memory_terms.is_empty()); + assert_argument_memory_term(&compiled.memory_terms[0], 0, -1, -1, true, 1); + + let paths = explorer.find_paths_to(&root, SymState::new(&ctx, 0x1000), 0x1010); + assert_eq!(paths.len(), 1); } #[test] -fn run_spec_fd_read_flows_into_load_compare_and_solution() { - let blocks = vec![ +fn derived_helper_summary_compiles_backward_memory_slice_via_ptrsub() { + let arch = make_x86_64_arch(); + let root_blocks = vec![ R2ILBlock { addr: 0x1000, size: 4, @@ -881,18 +4251,14 @@ fn run_spec_fd_read_flows_into_load_compare_and_solution() { ops: vec![ R2ILOp::Copy { dst: make_reg(RDI, 8), - src: make_const(0, 8), + src: make_const(0x3002, 8), }, R2ILOp::Copy { dst: make_reg(RSI, 8), - src: make_const(0x2000, 8), - }, - R2ILOp::Copy { - dst: make_reg(RDX, 8), - src: make_const(1, 8), + src: make_const(0x4142, 8), }, R2ILOp::Call { - target: make_const(0x5000, 8), + target: make_const(0x2000, 8), }, ], }, @@ -903,18 +4269,18 @@ fn run_spec_fd_read_flows_into_load_compare_and_solution() { op_metadata: Default::default(), ops: vec![ R2ILOp::Load { - dst: make_reg(TMP0, 1), - addr: make_const(0x2000, 8), + dst: make_reg(TMP1, 2), + addr: make_const(0x3000, 8), space: SpaceId::Ram, }, R2ILOp::IntEqual { - dst: make_reg(TMP1, 1), - a: make_reg(TMP0, 1), - b: make_const(b'k' as u64, 1), + dst: make_reg(RCX, 1), + a: make_reg(TMP1, 2), + b: make_const(0x4142, 2), }, R2ILOp::CBranch { target: make_const(0x1010, 8), - cond: make_reg(TMP1, 1), + cond: make_reg(RCX, 1), }, ], }, @@ -923,9 +4289,8 @@ fn run_spec_fd_read_flows_into_load_compare_and_solution() { size: 1, switch_info: None, op_metadata: Default::default(), - ops: vec![R2ILOp::Copy { - dst: make_reg(RAX, 8), - src: make_const(0, 8), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), }], }, R2ILBlock { @@ -933,91 +4298,292 @@ fn run_spec_fd_read_flows_into_load_compare_and_solution() { size: 1, switch_info: None, op_metadata: Default::default(), - ops: vec![R2ILOp::Copy { - dst: make_reg(RAX, 8), - src: make_const(0x1337, 8), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), }], }, ]; + let helper_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::PtrSub { + dst: make_reg(TMP0, 8), + base: make_reg(RDI, 8), + index: make_const(1, 8), + element_size: 2, + }, + R2ILOp::Store { + addr: make_reg(TMP0, 8), + val: make_reg(RSI, 2), + space: SpaceId::Ram, + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; + + let root = + SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic function"); + let helper = + SsaArtifact::for_symbolic(&helper_blocks, Some(&arch)).expect("helper symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x2000), + name: Some("helper_store_ptrsub".to_string()), + prepared: helper, + }, + ], + ) + .expect("scope"); - let arch = make_x86_64_arch(); - let func = - SsaArtifact::for_symbolic(&blocks, Some(&arch)).expect("Failed to build SSA function"); let ctx = Context::thread_local(); - let mut initial_state = SymState::new(&ctx, 0x1000); - let mut explorer = PathExplorer::new(&ctx); + let state = SymState::new(&ctx, 0x1000); let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); - assert!(registry.install_for_explorer(&mut explorer, 0x5000, "read")); + let derived = registry.derive_symbolic_summaries( + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + let helper_summary = derived + .summaries + .get(&r2ssa::InterprocFunctionId(0x2000)) + .expect("derived helper summary"); + assert!( + helper_summary.cases.iter().any(|case| { + case.memory_writes + .iter() + .any(|write| write.arg_index == 0 && write.offset == -2 && write.size >= 2) + }), + "expected derived summary to record arg0-2 write" + ); - let spec = ExplorationSpec { - find: vec![PredicateSpec::Address { - addr: AddressValue::Integer(0x1010), - }], - inputs: vec![InputSpec::Fd { - fd: 0, - len: 1, - name: Some("stdin0".to_string()), - alphabet: Some("k".to_string()), - }], - ..Default::default() - }; - spec.apply_to_state(&mut initial_state); + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let _ = registry.install_scope_summaries_for_explorer( + &mut explorer, + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); - let result = explorer - .run_spec(&func, initial_state, &spec) - .expect("spec exploration should succeed"); - assert_eq!(result.found_paths.len(), 1, "expected one matching path"); + let conditions = explorer.path_conditions_at(&root, state, 0x1010); + let compiled = conditions + .compiled_precondition + .as_ref() + .expect("compiled precondition"); + assert_eq!(compiled.precision, BackwardConditionPrecision::Exact); + assert!(!compiled.memory_terms.is_empty()); + assert_argument_memory_term(&compiled.memory_terms[0], 0, -2, -2, true, 2); +} + +#[test] +fn derived_helper_summary_compiles_backward_return_precondition() { + let arch = make_x86_64_arch(); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Call { + target: make_const(0x2000, 8), + }], + }, + R2ILBlock { + addr: 0x1004, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + ]; + let helper_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::IntXor { + dst: make_reg(RAX, 8), + a: make_reg(RDI, 8), + b: make_const(0x55, 8), + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; + let root = + SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic function"); + let helper = + SsaArtifact::for_symbolic(&helper_blocks, Some(&arch)).expect("helper symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root, + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x2000), + name: Some("helper_xor".to_string()), + prepared: helper.clone(), + }, + ], + ) + .expect("scope"); + + let ctx = Context::thread_local(); + let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); + let derived = registry.derive_symbolic_summaries( + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), + ); + let summary = derived + .summaries + .get(&r2ssa::InterprocFunctionId(0x2000)) + .expect("derived summary"); + let mut state = SymState::new(&ctx, 0x2000); + r2sym::seed_default_state_for_arch(&mut state, &helper, Some(&arch)); + state.make_symbolic_named("RDI_0", "rdi", 64); + + let compiled = compile_derived_summary_return_postcondition( + &state, + summary, + &r2sym::CallConv::x86_64_sysv(), + |ret| { + ret.eq(&ctx, &SymValue::concrete(0x6a, 64)) + .to_bv(&ctx) + .eq(z3::ast::BV::from_u64(1, 1)) + }, + ) + .expect("compiled backward precondition"); - let solved = explorer - .solve_path(&result.found_paths[0]) - .expect("found path should solve"); - assert_eq!(solved.final_pc, 0x1010); assert_eq!( - solved.input_buffers.get("stdin0"), - Some(&vec![b'k']), - "solver should recover the byte that drives the branch" + compiled.summary.precision, + BackwardConditionPrecision::Exact + ); + let solver = SymSolver::new(&ctx); + assert_eq!( + solver.sat_with_constraint(&state, &compiled.predicate), + r2sym::SatResult::Sat ); } #[test] -fn symbolic_n_constraints_bounded_for_memset_memcmp() { - let ctx = Context::thread_local(); +fn derived_helper_summary_reports_recursive_scc_diagnostics() { + let arch = make_x86_64_arch(); + let root_blocks = vec![R2ILBlock { + addr: 0x1000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Call { + target: make_const(0x2000, 8), + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; + let helper_a_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Call { + target: make_const(0x3000, 8), + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; + let helper_b_blocks = vec![R2ILBlock { + addr: 0x3000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Call { + target: make_const(0x2000, 8), + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }]; - let mut state_memset = SymState::new(&ctx, 0x1000); - let memset = MemsetSummary::new(8); - let n_memset = SymValue::new_symbolic(&ctx, "sym_memset_n", 64); - let call_memset = CallInfo { - args: vec![ - SymValue::concrete(0x7000, 64), - SymValue::concrete(0x7f, 64), - n_memset, + let root = + SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic function"); + let helper_a = SsaArtifact::for_symbolic(&helper_a_blocks, Some(&arch)) + .expect("helper_a symbolic function"); + let helper_b = SsaArtifact::for_symbolic(&helper_b_blocks, Some(&arch)) + .expect("helper_b symbolic function"); + let scope = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root, + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x2000), + name: Some("helper_a".to_string()), + prepared: helper_a, + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x3000), + name: Some("helper_b".to_string()), + prepared: helper_b, + }, ], - arg_bits: 64, - ret_bits: 64, - }; - let before_memset = state_memset.num_constraints(); - let _ = memset.execute(&mut state_memset, &call_memset); - assert!( - state_memset.num_constraints() > before_memset, - "symbolic memset length should add bounds constraints" + ) + .expect("scope"); + + let ctx = Context::thread_local(); + let registry = SummaryRegistry::with_core(r2sym::CallConv::x86_64_sysv()); + let derived = registry.derive_symbolic_summaries( + &ctx, + &scope, + Some(&arch), + &std::collections::HashMap::new(), ); - let mut state_memcmp = SymState::new(&ctx, 0x1000); - let memcmp = MemcmpSummary::new(8); - let n_memcmp = SymValue::new_symbolic(&ctx, "sym_memcmp_n", 64); - let call_memcmp = CallInfo { - args: vec![ - SymValue::concrete(0x7100, 64), - SymValue::concrete(0x7200, 64), - n_memcmp, - ], - arg_bits: 64, - ret_bits: 64, - }; - let before_memcmp = state_memcmp.num_constraints(); - let _ = memcmp.execute(&mut state_memcmp, &call_memcmp); + assert_eq!(derived.diagnostics.scc_count, 1); + assert_eq!(derived.diagnostics.max_scc_size, 2); + assert_eq!( + derived.diagnostics.scc_converged + derived.diagnostics.scc_budget_exhausted, + 1 + ); assert!( - state_memcmp.num_constraints() > before_memcmp, - "symbolic memcmp length should add bounds constraints" + derived + .summaries + .contains_key(&r2ssa::InterprocFunctionId(0x2000)) + ); + assert!( + derived + .summaries + .contains_key(&r2ssa::InterprocFunctionId(0x3000)) ); } diff --git a/crates/r2types/src/facts.rs b/crates/r2types/src/facts.rs index 2872cb5..22e320d 100644 --- a/crates/r2types/src/facts.rs +++ b/crates/r2types/src/facts.rs @@ -8,6 +8,278 @@ use crate::convert::CTypeLike; use crate::external::ExternalTypeDb; use crate::model::Signedness; +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum SymbolicReachabilityStatus { + Reachable, + Unreachable, + Unknown, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum SymbolicSemanticMode { + Raw, + Compiled, + Residual, + VmSummary, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum SymbolicSemanticSliceClass { + Wrapper, + Worker, + RecursiveGroup, + InterpreterSwitch, + InterpreterIndirect, + GenericLarge, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)] +pub struct SymbolicSemanticCapability { + pub query_ready: bool, + pub type_ready: bool, + pub decompile_ready: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum SymbolicSemanticResidualReason { + MissingArch, + LargeCfg, + SummaryBudgetExhausted, + SccBudgetExhausted, + InterpreterRequiresStepSummary, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum SymbolicConditionPrecision { + Exact, + OverApprox, + ResidualSearchRequired, + Unsupported, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum SymbolicMemoryRegionKind { + Stack, + Global, + Input, + Heap, + Replay, + EscapedUnknown, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct SymbolicMemoryRegionRef { + pub id: u32, + pub kind: SymbolicMemoryRegionKind, + pub name: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum SymbolicMemoryRegion { + Argument { index: usize }, + Region(SymbolicMemoryRegionRef), +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct SymbolicMemoryCondition { + pub region: SymbolicMemoryRegion, + pub offset_lo: i64, + pub offset_hi: i64, + pub size: u32, + pub exact_offset: bool, + pub expr: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct SymbolicCompiledCondition { + pub simplified: String, + pub terms: Vec, + pub memory_terms: Vec, + pub backward_memory_substitutions: usize, + pub backward_memory_candidate_enumerations: usize, + pub backward_memory_residual_fallbacks: usize, + pub precision: SymbolicConditionPrecision, + pub supported_paths: usize, + pub total_paths: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum SymbolicInterpreterKind { + SwitchDispatch, + IndirectDispatch, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct SymbolicInterpreterDispatch { + pub kind: SymbolicInterpreterKind, + pub dispatch_header: u64, + pub dispatch_targets: usize, + pub selector: Option, + pub back_edges: usize, + pub score: i32, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum SymbolicVmValueExpr { + Const(u64), + Var(String), + Expr(String), +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct SymbolicVmStateUpdate { + pub output: String, + pub expr: String, + pub value: SymbolicVmValueExpr, + pub exact: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct SymbolicVmTransferArm { + pub handler_target: u64, + pub case_values: Vec, + pub region_blocks: Vec, + pub exit_targets: Vec, + pub state_updates: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub selector_update: Option, + pub exact: bool, + pub redispatch: bool, + pub may_return: bool, + pub truncated: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct SymbolicVmStepSummary { + pub kind: SymbolicInterpreterKind, + pub loop_header: u64, + pub dispatch_header: u64, + pub selector: Option, + pub dispatch_targets: Vec, + pub default_target: Option, + pub case_values_by_target: BTreeMap>, + pub loop_latches: Vec, + pub state_inputs: Vec, + pub state_outputs: Vec, + pub step_blocks: Vec, + pub handler_regions: BTreeMap>, + pub handler_state_inputs: BTreeMap>, + pub handler_state_outputs: BTreeMap>, + pub handler_state_updates: BTreeMap>, + pub handler_memory_reads: BTreeMap, + pub handler_memory_writes: BTreeMap, + pub handler_calls: BTreeMap, + pub handler_conditional_branches: BTreeMap, + pub handler_exit_targets: BTreeMap>, + pub redispatch_handlers: Vec, + pub returning_handlers: Vec, + pub truncated_handlers: Vec, + pub transfers: Vec, +} +pub type SymbolicVmTransferSummary = SymbolicVmStepSummary; + +#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct SymbolicFactDiagnostics { + pub branches_evaluated: usize, + pub branches_pruned: usize, + pub branches_unknown: usize, + pub skipped_missing_arch: bool, + pub skipped_large_cfg: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub semantic_mode: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub semantic_capability: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub slice_class: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub residual_reasons: Vec, + pub closure_functions: usize, + pub helper_functions: usize, + pub derived_summaries: usize, + pub summary_attempted: usize, + pub summary_budget_exhausted: usize, + pub summary_scc_count: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct SymbolicBranchFact { + pub block_addr: u64, + pub true_target: u64, + pub false_target: u64, + pub true_status: SymbolicReachabilityStatus, + pub false_status: SymbolicReachabilityStatus, + pub true_condition: Option, + pub false_condition: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub true_compiled: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub false_compiled: Option, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct SymbolicSemanticFacts { + pub branch_facts: Vec, + pub diagnostics: SymbolicFactDiagnostics, + #[serde(skip_serializing_if = "Option::is_none")] + pub interpreter: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub vm_step: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub vm_transfer: Option, +} + +impl SymbolicSemanticFacts { + pub fn is_empty(&self) -> bool { + self.branch_facts.is_empty() + && self.diagnostics == SymbolicFactDiagnostics::default() + && self.interpreter.is_none() + && self.vm_step.is_none() + && self.vm_transfer.is_none() + } + + pub fn branch_fact_for_block(&self, block_addr: u64) -> Option<&SymbolicBranchFact> { + self.branch_facts + .iter() + .find(|fact| fact.block_addr == block_addr) + } + + pub fn vm_step_for_dispatch_header( + &self, + dispatch_header: u64, + ) -> Option<&SymbolicVmStepSummary> { + self.vm_step + .as_ref() + .filter(|vm_step| vm_step.dispatch_header == dispatch_header) + } + + pub fn vm_transfer_for_dispatch_header( + &self, + dispatch_header: u64, + ) -> Option<&SymbolicVmTransferSummary> { + self.vm_transfer + .as_ref() + .filter(|vm_transfer| vm_transfer.dispatch_header == dispatch_header) + } +} + +impl SymbolicBranchFact { + pub fn exact_compiled_condition(&self) -> Option<&SymbolicCompiledCondition> { + match (self.true_status, self.false_status) { + (SymbolicReachabilityStatus::Reachable, SymbolicReachabilityStatus::Unreachable) => { + self.true_compiled.as_ref().filter(|compiled| { + matches!(compiled.precision, SymbolicConditionPrecision::Exact) + }) + } + (SymbolicReachabilityStatus::Unreachable, SymbolicReachabilityStatus::Reachable) => { + self.false_compiled.as_ref().filter(|compiled| { + matches!(compiled.precision, SymbolicConditionPrecision::Exact) + }) + } + _ => None, + } + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct FunctionType { pub return_type: CTypeLike, @@ -184,6 +456,7 @@ pub struct FunctionTypeFacts { pub external_type_db: ExternalTypeDb, pub slot_type_overrides: HashMap, pub slot_field_profiles: HashMap>, + pub symbolic_facts: SymbolicSemanticFacts, pub interproc_diagnostics: InterprocFactDiagnostics, pub diagnostics: Vec, } @@ -200,6 +473,7 @@ pub struct FunctionTypeFactInputs { pub external_type_db: ExternalTypeDb, pub slot_type_overrides: HashMap, pub slot_field_profiles: HashMap>, + pub symbolic_facts: SymbolicSemanticFacts, pub local_field_accesses: Vec, pub interproc_diagnostics: InterprocFactDiagnostics, pub diagnostics: Vec, @@ -225,6 +499,7 @@ impl FunctionTypeFacts { && self.external_type_db.diagnostics.is_empty() && self.slot_type_overrides.is_empty() && self.slot_field_profiles.is_empty() + && self.symbolic_facts.is_empty() && self.interproc_diagnostics == InterprocFactDiagnostics::default() && self.diagnostics.is_empty() } @@ -241,6 +516,7 @@ impl FunctionTypeFacts { external_type_db: self.external_type_db, slot_type_overrides: self.slot_type_overrides, slot_field_profiles: self.slot_field_profiles, + symbolic_facts: self.symbolic_facts, local_field_accesses: Vec::new(), interproc_diagnostics: self.interproc_diagnostics, diagnostics: self.diagnostics, @@ -275,6 +551,7 @@ impl FunctionTypeFactsBuilder { external_type_db, slot_type_overrides, slot_field_profiles, + symbolic_facts, interproc_diagnostics, diagnostics, .. @@ -305,6 +582,7 @@ impl FunctionTypeFactsBuilder { external_type_db, slot_type_overrides, slot_field_profiles, + symbolic_facts, interproc_diagnostics, diagnostics, } diff --git a/crates/r2types/src/lib.rs b/crates/r2types/src/lib.rs index 190c53f..0c52da2 100644 --- a/crates/r2types/src/lib.rs +++ b/crates/r2types/src/lib.rs @@ -30,7 +30,14 @@ pub use facts::{ CalleeMemoryRange, CalleeMemoryRegion, CalleeReturnRelation, FunctionParamSpec, FunctionSignatureSpec, FunctionType, FunctionTypeFactInputs, FunctionTypeFacts, FunctionTypeFactsBuilder, InterprocFactDiagnostics, LocalFieldAccessFact, ResolvedFieldLayout, - VisibleBinding, VisibleBindingKind, parse_type_like_spec, + SymbolicBranchFact, SymbolicCompiledCondition, SymbolicConditionPrecision, + SymbolicFactDiagnostics, SymbolicInterpreterDispatch, SymbolicInterpreterKind, + SymbolicMemoryCondition, SymbolicMemoryRegion, SymbolicMemoryRegionKind, + SymbolicMemoryRegionRef, SymbolicReachabilityStatus, SymbolicSemanticCapability, + SymbolicSemanticFacts, SymbolicSemanticMode, SymbolicSemanticResidualReason, + SymbolicSemanticSliceClass, SymbolicVmStateUpdate, SymbolicVmStepSummary, + SymbolicVmTransferArm, SymbolicVmTransferSummary, SymbolicVmValueExpr, VisibleBinding, + VisibleBindingKind, parse_type_like_spec, }; pub use inference::{CombinedTypeOracle, TypeInference}; pub use model::{Signedness, StructField, StructShape, Type, TypeArena, TypeId}; diff --git a/r2plugin/r_anal_sleigh.c b/r2plugin/r_anal_sleigh.c index c8ddc83..d7b85e6 100644 --- a/r2plugin/r_anal_sleigh.c +++ b/r2plugin/r_anal_sleigh.c @@ -85,6 +85,60 @@ extern char *r2sym_solve_to(const R2ILContext *ctx, const R2ILBlock **blocks, si unsigned long long entry_addr, unsigned long long target_addr); extern char *r2sym_run_spec_json(const R2ILContext *ctx, const R2ILBlock **blocks, size_t num_blocks, unsigned long long entry_addr, const char *spec_json); +typedef struct { + unsigned long long entry_addr; + const char *name; + const R2ILBlock **blocks; + size_t num_blocks; +} R2ILFunctionBlocks; +typedef struct { + const char *name; + unsigned long long value; +} R2SymReplayRegister; +typedef struct { + unsigned long long addr; + const unsigned char *bytes; + size_t size; + const char *label; +} R2SymReplayMemoryWindow; +typedef struct { + const char *name; + const char *symbol; +} R2SymReplayRegisterOverlay; +typedef struct { + unsigned long long addr; + unsigned int size; + const char *name; +} R2SymReplayMemoryOverlay; +typedef struct { + unsigned long long checkpoint_id; + unsigned long long entry_addr; + const R2SymReplayRegister *registers; + size_t num_registers; + const R2SymReplayMemoryWindow *memory; + size_t num_memory; + const R2SymReplayRegisterOverlay *register_overlays; + size_t num_register_overlays; + const R2SymReplayMemoryOverlay *memory_overlays; + size_t num_memory_overlays; + const int *tty_fds; + size_t num_tty_fds; + int skip_sleep_calls; +} R2SymReplaySeed; +extern char *r2sym_function_scope(const R2ILContext *ctx, const R2ILFunctionBlocks *functions, size_t num_functions, unsigned long long entry_addr); +extern char *r2sym_paths_scope(const R2ILContext *ctx, const R2ILFunctionBlocks *functions, size_t num_functions, unsigned long long entry_addr); +extern char *r2sym_explore_to_scope(const R2ILContext *ctx, const R2ILFunctionBlocks *functions, size_t num_functions, + unsigned long long entry_addr, unsigned long long target_addr); +extern char *r2sym_solve_to_scope(const R2ILContext *ctx, const R2ILFunctionBlocks *functions, size_t num_functions, + unsigned long long entry_addr, unsigned long long target_addr); +extern char *r2sym_explore_to_replay_scope(const R2ILContext *ctx, const R2ILFunctionBlocks *functions, size_t num_functions, + unsigned long long entry_addr, unsigned long long target_addr, const R2SymReplaySeed *replay_seed); +extern char *r2sym_solve_to_replay_scope(const R2ILContext *ctx, const R2ILFunctionBlocks *functions, size_t num_functions, + unsigned long long entry_addr, unsigned long long target_addr, const R2SymReplaySeed *replay_seed); +extern char *r2sym_compile_semantics_scope(const R2ILContext *ctx, const R2ILFunctionBlocks *functions, size_t num_functions, + unsigned long long entry_addr); +extern char *r2sym_run_spec_json_scope(const R2ILContext *ctx, const R2ILFunctionBlocks *functions, size_t num_functions, + unsigned long long entry_addr, const char *spec_json); extern int r2sym_set_symbol_map_json(const char *json); extern int r2sym_merge_is_enabled(void); extern void r2sym_merge_set_enabled(int enabled); @@ -94,6 +148,10 @@ extern char *r2dec_function_with_context(const R2ILContext *ctx, const R2ILBlock const char *func_name, const char *func_names_json, const char *strings_json, const char *symbols_json, const char *external_context_json); +extern char *r2dec_function_with_context_scope(const R2ILContext *ctx, const R2ILBlock **blocks, size_t num_blocks, + unsigned long long fcn_addr, const char *func_name, const char *func_names_json, + const char *strings_json, const char *symbols_json, const char *external_context_json, + const R2ILFunctionBlocks *functions, size_t num_functions); /* CFG */ extern char *r2cfg_function_ascii(const R2ILContext *ctx, const R2ILBlock **blocks, size_t num_blocks); @@ -112,6 +170,10 @@ extern char *r2sleigh_infer_type_writeback_json(const R2ILContext *ctx, const R2 extern char *r2sleigh_infer_type_writeback_json_ex(const R2ILContext *ctx, const R2ILBlock **blocks, size_t num_blocks, unsigned long long fcn_addr, const char *fcn_name, const char *external_context_json, size_t interproc_iter, size_t interproc_max_iters, int interproc_converged, const char *interproc_scope_json); +extern char *r2sleigh_infer_type_writeback_json_scope_ex(const R2ILContext *ctx, const R2ILBlock **blocks, size_t num_blocks, + unsigned long long fcn_addr, const char *fcn_name, const char *external_context_json, + size_t interproc_iter, size_t interproc_max_iters, int interproc_converged, const char *interproc_scope_json, + const R2ILFunctionBlocks *functions, size_t num_functions); extern char *r2sleigh_get_direct_call_targets_json(const R2ILContext *ctx, const R2ILBlock **blocks, size_t num_blocks, unsigned long long fcn_addr, const char *fcn_name); extern int r2sleigh_alias_function_analysis_artifact_cache(const R2ILContext *ctx, const R2ILBlock **blocks, @@ -243,6 +305,18 @@ typedef struct { size_t capacity; } BlockArray; +#define SLEIGH_SYM_HELPER_MAX_FUNCTIONS 16 +#define SLEIGH_SCOPE_HELPER_MAX_BLOCKS 64 +#define SLEIGH_SCOPE_HELPER_MAX_COST 256 + +typedef struct { + R2ILFunctionBlocks *functions; + BlockArray *owned_blocks; + char **owned_names; + size_t count; + size_t capacity; +} SymFunctionScope; + static char *build_type_interproc_scope_json( RCore *core, RAnal *anal, @@ -270,6 +344,21 @@ static ut64 *collect_type_interproc_direct_targets_from_blocks( const char *fcn_name, size_t *out_count ); +static void sym_function_scope_init(SymFunctionScope *scope); +static void sym_function_scope_free(SymFunctionScope *scope); +static bool sym_function_scope_ensure_capacity(SymFunctionScope *scope, size_t needed); +static bool sym_function_scope_append( + SymFunctionScope *scope, + RAnal *anal, + RAnalFunction *fcn, + R2ILContext *ctx +); +static bool build_symbolic_function_scope( + RAnal *anal, + RAnalFunction *root_fcn, + R2ILContext *ctx, + SymFunctionScope *scope +); static void block_array_init(BlockArray *arr) { arr->blocks = NULL; @@ -953,6 +1042,43 @@ static bool parse_sym_target_expr(RCore *core, const char *expr, ut64 *target) { return true; } +static bool parse_replay_target_and_json(RCore *core, const char *arg, ut64 *target, char **out_json) { + char *owned = NULL; + char *json = NULL; + char *sep; + const char *json_start; + if (!core || !arg || !*arg || !target || !out_json) { + return false; + } + *out_json = NULL; + owned = strdup (arg); + if (!owned) { + return false; + } + sep = owned; + while (*sep && !isspace ((unsigned char)*sep)) { + sep++; + } + if (!*sep) { + free (owned); + return false; + } + *sep++ = '\0'; + json_start = skip_cmd_spaces (sep); + if (!*json_start || !parse_sym_target_expr (core, owned, target)) { + free (owned); + return false; + } + json = strdup (json_start); + free (owned); + if (!json) { + return false; + } + r_str_unescape (json); + *out_json = json; + return true; +} + typedef enum { REPLAY_EXPR_CONST = 0, REPLAY_EXPR_REG, @@ -1068,6 +1194,30 @@ typedef struct { bool big_endian; } ReplaySearchSpec; +typedef struct { + char *name; + char *symbol; +} ReplaySymRegisterOverlay; + +typedef struct { + ut64 addr; + ut32 size; + char *name; +} ReplaySymMemoryOverlay; + +typedef struct { + ut64 checkpoint_id; + ut64 entry_addr; + RDebugStateRequest *snapshot_request; + ReplaySymRegisterOverlay *register_overlays; + size_t register_overlay_count; + ReplaySymMemoryOverlay *memory_overlays; + size_t memory_overlay_count; + int *tty_fds; + size_t tty_fd_count; + bool skip_sleep_calls; +} ReplaySymSeedSpec; + typedef struct { ut64 checkpoint_id; char *input; @@ -1494,10 +1644,9 @@ static bool replay_state_request_add_reg(RDebugStateRequest *request, const char return true; } -static bool replay_state_request_add_mem(RDebugStateRequest *request, ut64 addr, int width_bits) { +static bool replay_state_request_add_mem_range(RDebugStateRequest *request, ut64 addr, ut32 size, const char *label) { RListIter *iter; RDebugStateMemSpec *spec; - ut32 size = (ut32)(width_bits / 8); if (!request || !size) { return false; } @@ -1512,10 +1661,41 @@ static bool replay_state_request_add_mem(RDebugStateRequest *request, ut64 addr, } spec->addr = addr; spec->size = size; + if (label && *label) { + spec->label = strdup (label); + if (!spec->label) { + r_debug_state_mem_spec_free (spec); + return false; + } + } r_list_append (request->memory, spec); return true; } +static bool replay_state_request_add_mem(RDebugStateRequest *request, ut64 addr, int width_bits) { + ut32 size = (ut32)(width_bits / 8); + return replay_state_request_add_mem_range (request, addr, size, NULL); +} + +static bool replay_state_request_add_all_gprs(RDebug *dbg, RDebugStateRequest *request) { + RListIter *iter; + RRegItem *item; + RList *regs; + if (!dbg || !dbg->reg || !request) { + return false; + } + regs = r_reg_get_list (dbg->reg, R_REG_TYPE_GPR); + if (!regs) { + return false; + } + r_list_foreach (regs, iter, item) { + if (item && item->name && !replay_state_request_add_reg (request, item->name)) { + return false; + } + } + return true; +} + static bool replay_expr_collect_state(const ReplayExpr *expr, RDebugStateRequest *request) { if (!expr || !request) { return false; @@ -1581,6 +1761,403 @@ static void replay_search_spec_fini(ReplaySearchSpec *spec) { spec->stop_count = 0; } +static void replay_sym_seed_spec_fini(ReplaySymSeedSpec *spec) { + size_t i; + if (!spec) { + return; + } + r_debug_state_request_free (spec->snapshot_request); + spec->snapshot_request = NULL; + for (i = 0; i < spec->register_overlay_count; i++) { + free (spec->register_overlays[i].name); + free (spec->register_overlays[i].symbol); + } + free (spec->register_overlays); + spec->register_overlays = NULL; + spec->register_overlay_count = 0; + for (i = 0; i < spec->memory_overlay_count; i++) { + free (spec->memory_overlays[i].name); + } + free (spec->memory_overlays); + spec->memory_overlays = NULL; + spec->memory_overlay_count = 0; + free (spec->tty_fds); + spec->tty_fds = NULL; + spec->tty_fd_count = 0; + spec->checkpoint_id = 0; + spec->entry_addr = 0; + spec->skip_sleep_calls = false; +} + +static bool replay_sym_seed_add_register_overlay(ReplaySymSeedSpec *spec, const char *name, const char *symbol) { + ReplaySymRegisterOverlay *next; + size_t index; + if (!spec || !name || !*name || !symbol || !*symbol) { + return false; + } + next = realloc (spec->register_overlays, (spec->register_overlay_count + 1) * sizeof (*next)); + if (!next) { + return false; + } + spec->register_overlays = next; + index = spec->register_overlay_count++; + memset (&spec->register_overlays[index], 0, sizeof (spec->register_overlays[index])); + spec->register_overlays[index].name = strdup (name); + spec->register_overlays[index].symbol = strdup (symbol); + if (!spec->register_overlays[index].name || !spec->register_overlays[index].symbol) { + free (spec->register_overlays[index].name); + free (spec->register_overlays[index].symbol); + spec->register_overlays[index].name = NULL; + spec->register_overlays[index].symbol = NULL; + spec->register_overlay_count--; + return false; + } + return true; +} + +static bool replay_sym_seed_add_memory_overlay(ReplaySymSeedSpec *spec, ut64 addr, ut32 size, const char *name) { + ReplaySymMemoryOverlay *next; + size_t index; + if (!spec || !size || !name || !*name) { + return false; + } + next = realloc (spec->memory_overlays, (spec->memory_overlay_count + 1) * sizeof (*next)); + if (!next) { + return false; + } + spec->memory_overlays = next; + index = spec->memory_overlay_count++; + memset (&spec->memory_overlays[index], 0, sizeof (spec->memory_overlays[index])); + spec->memory_overlays[index].addr = addr; + spec->memory_overlays[index].size = size; + spec->memory_overlays[index].name = strdup (name); + if (!spec->memory_overlays[index].name) { + spec->memory_overlay_count--; + return false; + } + return true; +} + +static bool replay_sym_seed_add_tty_fd(ReplaySymSeedSpec *spec, int fd) { + int *next; + if (!spec) { + return false; + } + next = realloc (spec->tty_fds, (spec->tty_fd_count + 1) * sizeof (*next)); + if (!next) { + return false; + } + spec->tty_fds = next; + spec->tty_fds[spec->tty_fd_count++] = fd; + return true; +} + +static bool replay_sym_seed_spec_parse(RCore *core, const char *json, ReplaySymSeedSpec *spec) { + char *json_copy; + char *owned_json = NULL; + RJson *root; + const RJson *value; + size_t i; + + R_RETURN_VAL_IF_FAIL (core && core->dbg && json && spec, false); + memset (spec, 0, sizeof (*spec)); + spec->snapshot_request = replay_state_request_new (); + if (!spec->snapshot_request || !replay_state_request_add_all_gprs (core->dbg, spec->snapshot_request)) { + replay_sym_seed_spec_fini (spec); + return false; + } + + json_copy = strdup (json); + if (!json_copy) { + replay_sym_seed_spec_fini (spec); + return false; + } + owned_json = json_copy; + root = r_json_parse (json_copy); + if (!root || root->type != R_JSON_OBJECT) { + R_LOG_ERROR ("r2sleigh replay sym seed: json root parse failed"); + free (owned_json); + r_json_free (root); + replay_sym_seed_spec_fini (spec); + return false; + } + + value = r_json_get (root, "checkpoint"); + if (!value) { + value = r_json_get (root, "seed_checkpoint"); + } + if (!replay_parse_addr_expr (core, value, &spec->checkpoint_id) || !spec->checkpoint_id) { + R_LOG_ERROR ("r2sleigh replay sym seed: missing/invalid checkpoint"); + goto fail; + } + value = r_json_get (root, "entry"); + if (value && !replay_parse_addr_expr (core, value, &spec->entry_addr)) { + R_LOG_ERROR ("r2sleigh replay sym seed: invalid entry"); + goto fail; + } + value = r_json_get (root, "skip_sleep"); + if (value) { + if (value->type != R_JSON_BOOLEAN) { + R_LOG_ERROR ("r2sleigh replay sym seed: invalid skip_sleep"); + goto fail; + } + spec->skip_sleep_calls = value->num.u_value; + } + value = r_json_get (root, "tty_fds"); + if (value) { + if (value->type != R_JSON_ARRAY) { + R_LOG_ERROR ("r2sleigh replay sym seed: invalid tty_fds"); + goto fail; + } + for (i = 0; i < value->children.count; i++) { + const RJson *item = r_json_item (value, i); + st64 fd = 0; + if (!replay_parse_num_expr (core, item, &fd)) { + R_LOG_ERROR ("r2sleigh replay sym seed: invalid tty fd"); + goto fail; + } + if (!replay_sym_seed_add_tty_fd (spec, (int)fd)) { + goto fail; + } + } + } + value = r_json_get (root, "memory"); + if (value) { + if (value->type != R_JSON_ARRAY) { + R_LOG_ERROR ("r2sleigh replay sym seed: invalid memory"); + goto fail; + } + for (i = 0; i < value->children.count; i++) { + const RJson *item = r_json_item (value, i); + const RJson *label_json; + char *label = NULL; + ut64 addr = 0; + st64 size_value = 0; + if (!item || item->type != R_JSON_OBJECT) { + R_LOG_ERROR ("r2sleigh replay sym seed: invalid memory item"); + goto fail; + } + if (!replay_parse_addr_expr (core, r_json_get (item, "addr"), &addr) + || !replay_parse_num_expr (core, r_json_get (item, "size"), &size_value) + || size_value <= 0 || size_value > UT32_MAX) { + R_LOG_ERROR ("r2sleigh replay sym seed: invalid memory window"); + goto fail; + } + label_json = r_json_get (item, "label"); + if (label_json && label_json->type == R_JSON_STRING && label_json->str_value) { + label = strdup (label_json->str_value); + if (!label) { + goto fail; + } + } + if (!replay_state_request_add_mem_range (spec->snapshot_request, addr, (ut32)size_value, label)) { + free (label); + goto fail; + } + free (label); + } + } + value = r_json_get (root, "symbolic_registers"); + if (value) { + if (value->type != R_JSON_ARRAY) { + R_LOG_ERROR ("r2sleigh replay sym seed: invalid symbolic_registers"); + goto fail; + } + for (i = 0; i < value->children.count; i++) { + const RJson *item = r_json_item (value, i); + const char *name = NULL; + const char *symbol = NULL; + char default_symbol[128]; + if (!item) { + goto fail; + } + if (item->type == R_JSON_STRING && item->str_value) { + name = item->str_value; + } else if (item->type == R_JSON_OBJECT) { + const RJson *name_json = r_json_get (item, "name"); + const RJson *symbol_json = r_json_get (item, "symbol"); + if (name_json && name_json->type == R_JSON_STRING) { + name = name_json->str_value; + } + if (symbol_json && symbol_json->type == R_JSON_STRING) { + symbol = symbol_json->str_value; + } + } + if (!name || !*name) { + R_LOG_ERROR ("r2sleigh replay sym seed: invalid symbolic register"); + goto fail; + } + if (!symbol || !*symbol) { + snprintf (default_symbol, sizeof (default_symbol), "replay_%s", name); + symbol = default_symbol; + } + if (!replay_sym_seed_add_register_overlay (spec, name, symbol)) { + goto fail; + } + } + } + value = r_json_get (root, "symbolic_memory"); + if (value) { + if (value->type != R_JSON_ARRAY) { + R_LOG_ERROR ("r2sleigh replay sym seed: invalid symbolic_memory"); + goto fail; + } + for (i = 0; i < value->children.count; i++) { + const RJson *item = r_json_item (value, i); + const RJson *name_json; + char default_name[128]; + const char *name = NULL; + ut64 addr = 0; + st64 size_value = 0; + if (!item || item->type != R_JSON_OBJECT) { + R_LOG_ERROR ("r2sleigh replay sym seed: invalid symbolic memory item"); + goto fail; + } + if (!replay_parse_addr_expr (core, r_json_get (item, "addr"), &addr) + || !replay_parse_num_expr (core, r_json_get (item, "size"), &size_value) + || size_value <= 0 || size_value > UT32_MAX) { + R_LOG_ERROR ("r2sleigh replay sym seed: invalid symbolic memory window"); + goto fail; + } + name_json = r_json_get (item, "name"); + if (name_json && name_json->type == R_JSON_STRING && name_json->str_value) { + name = name_json->str_value; + } else { + snprintf (default_name, sizeof (default_name), "replay_mem_%zu", i); + name = default_name; + } + if (!replay_state_request_add_mem_range (spec->snapshot_request, addr, (ut32)size_value, name) + || !replay_sym_seed_add_memory_overlay (spec, addr, (ut32)size_value, name)) { + goto fail; + } + } + } + + free (owned_json); + r_json_free (root); + return true; + +fail: + free (owned_json); + r_json_free (root); + replay_sym_seed_spec_fini (spec); + return false; +} + +static RDebugStateSnapshot *replay_sym_collect_seed_snapshot(RCore *core, const ReplaySymSeedSpec *spec) { + RDebugStateSnapshot *snapshot; + ut64 previous_checkpoint; + R_RETURN_VAL_IF_FAIL (core && core->dbg && core->dbg->session && spec && spec->snapshot_request, NULL); + previous_checkpoint = core->dbg->session->current_checkpoint_id; + if (!r_debug_session_restore_checkpoint (core->dbg, spec->checkpoint_id)) { + return NULL; + } + snapshot = r_debug_state_snapshot_collect (core->dbg, spec->snapshot_request); + if (previous_checkpoint != UT64_MAX && previous_checkpoint != spec->checkpoint_id) { + r_debug_session_restore_checkpoint (core->dbg, previous_checkpoint); + } + return snapshot; +} + +static char *replay_sym_query_run(RCore *core, const R2ILContext *ctx, const SymFunctionScope *scope, + ut64 entry_addr, ut64 target_addr, const ReplaySymSeedSpec *spec, bool is_explore) { + RDebugStateSnapshot *snapshot = NULL; + R2SymReplayRegister *registers = NULL; + R2SymReplayMemoryWindow *memory = NULL; + R2SymReplayRegisterOverlay *register_overlays = NULL; + R2SymReplayMemoryOverlay *memory_overlays = NULL; + R2SymReplaySeed seed = {0}; + RListIter *iter; + RDebugStateRegValue *reg; + RDebugStateMemValue *memv; + size_t reg_count = 0; + size_t mem_count = 0; + size_t idx = 0; + char *result = NULL; + + R_RETURN_VAL_IF_FAIL (core && ctx && scope && spec, NULL); + + snapshot = replay_sym_collect_seed_snapshot (core, spec); + if (!snapshot) { + return NULL; + } + r_list_foreach (snapshot->registers, iter, reg) { + if (reg && reg->found && reg->name) { + reg_count++; + } + } + r_list_foreach (snapshot->memory, iter, memv) { + if (memv && memv->ok && memv->bytes && memv->size > 0) { + mem_count++; + } + } + registers = reg_count? calloc (reg_count, sizeof (*registers)): NULL; + memory = mem_count? calloc (mem_count, sizeof (*memory)): NULL; + register_overlays = spec->register_overlay_count? calloc (spec->register_overlay_count, sizeof (*register_overlays)): NULL; + memory_overlays = spec->memory_overlay_count? calloc (spec->memory_overlay_count, sizeof (*memory_overlays)): NULL; + if ((reg_count && !registers) || (mem_count && !memory) + || (spec->register_overlay_count && !register_overlays) + || (spec->memory_overlay_count && !memory_overlays)) { + goto cleanup; + } + + idx = 0; + r_list_foreach (snapshot->registers, iter, reg) { + if (!reg || !reg->found || !reg->name) { + continue; + } + registers[idx].name = reg->name; + registers[idx].value = reg->value; + idx++; + } + idx = 0; + r_list_foreach (snapshot->memory, iter, memv) { + if (!memv || !memv->ok || !memv->bytes || !memv->size) { + continue; + } + memory[idx].addr = memv->addr; + memory[idx].bytes = memv->bytes; + memory[idx].size = memv->size; + memory[idx].label = memv->label; + idx++; + } + for (idx = 0; idx < spec->register_overlay_count; idx++) { + register_overlays[idx].name = spec->register_overlays[idx].name; + register_overlays[idx].symbol = spec->register_overlays[idx].symbol; + } + for (idx = 0; idx < spec->memory_overlay_count; idx++) { + memory_overlays[idx].addr = spec->memory_overlays[idx].addr; + memory_overlays[idx].size = spec->memory_overlays[idx].size; + memory_overlays[idx].name = spec->memory_overlays[idx].name; + } + + seed.checkpoint_id = spec->checkpoint_id; + seed.entry_addr = spec->entry_addr? spec->entry_addr: snapshot->pc; + seed.registers = registers; + seed.num_registers = reg_count; + seed.memory = memory; + seed.num_memory = mem_count; + seed.register_overlays = register_overlays; + seed.num_register_overlays = spec->register_overlay_count; + seed.memory_overlays = memory_overlays; + seed.num_memory_overlays = spec->memory_overlay_count; + seed.tty_fds = spec->tty_fds; + seed.num_tty_fds = spec->tty_fd_count; + seed.skip_sleep_calls = spec->skip_sleep_calls? 1: 0; + + result = is_explore + ? r2sym_explore_to_replay_scope (ctx, scope->functions, scope->count, entry_addr, target_addr, &seed) + : r2sym_solve_to_replay_scope (ctx, scope->functions, scope->count, entry_addr, target_addr, &seed); + +cleanup: + free (registers); + free (memory); + free (register_overlays); + free (memory_overlays); + r_debug_state_snapshot_free (snapshot); + return result; +} + static void replay_search_node_free(ReplaySearchNode *node) { if (!node) { return; @@ -2401,7 +2978,65 @@ static RAnalFunction *resolve_function_target_by_name(RAnal *anal, const char *t return fcn; } -static RAnalFunction *resolve_function_target(RCore *core, RAnal *anal, const char *target_arg) { +static int function_bb_count(const RAnalFunction *fcn) { + return (fcn && fcn->bbs)? r_list_length (fcn->bbs): 0; +} + +static bool function_exceeds_helper_scope_budget(const RAnalFunction *fcn) { + ut32 cost; + int bb_count; + if (!fcn) { + return true; + } + bb_count = function_bb_count (fcn); + if (bb_count > SLEIGH_SCOPE_HELPER_MAX_BLOCKS) { + return true; + } + cost = r_anal_function_cost ((RAnalFunction *)fcn); + return cost > SLEIGH_SCOPE_HELPER_MAX_COST; +} + +static RAnalFunction *materialize_function_at(RAnal *anal, ut64 addr) { + RAnalFunction *fcn; + int ret; + RCore *core; + + if (!anal || addr == UT64_MAX) { + return NULL; + } + + fcn = r_anal_get_fcn_in (anal, addr, R_ANAL_FCN_TYPE_ANY); + if (fcn) { + return fcn; + } + + core = anal->coreb.core; + if (core) { + if (r_core_anal_fcn (core, addr, UT64_MAX, R_ANAL_REF_TYPE_NULL, 1)) { + fcn = r_anal_get_fcn_in (anal, addr, R_ANAL_FCN_TYPE_ANY); + if (fcn) { + return fcn; + } + } + } + + fcn = r_anal_create_function (anal, NULL, addr, R_ANAL_FCN_TYPE_FCN, NULL); + if (!fcn) { + return r_anal_get_fcn_in (anal, addr, R_ANAL_FCN_TYPE_ANY); + } + + ret = r_anal_function (anal, fcn, addr, R_ANAL_REF_TYPE_NULL); + if ((ret < 0 && ret != R_ANAL_RET_END) || function_bb_count (fcn) <= 0) { + if (!r_anal_function_delete (anal, fcn)) { + r_anal_function_free (fcn); + } + return NULL; + } + + return r_anal_get_fcn_in (anal, addr, R_ANAL_FCN_TYPE_ANY); +} + +static RAnalFunction *resolve_or_materialize_function_target(RCore *core, RAnal *anal, const char *target_arg) { ut64 target_addr = 0; RAnalFunction *fcn; @@ -2417,7 +3052,14 @@ static RAnalFunction *resolve_function_target(RCore *core, RAnal *anal, const ch if (!parse_sym_target_expr (core, target_arg, &target_addr)) { return NULL; } - return r_anal_get_fcn_in (anal, target_addr, R_ANAL_FCN_TYPE_ANY); + return materialize_function_at (anal, target_addr); +} + +static RAnalFunction *resolve_or_materialize_current_function(RCore *core, RAnal *anal) { + if (!core || !anal) { + return NULL; + } + return materialize_function_at (anal, core->addr); } static char *build_sym_symbol_map_json(RCore *core) { @@ -5498,14 +6140,14 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { r_cons_println (cons, "| a:sla.vars - Show all varnodes used by instruction"); r_cons_println (cons, "| a:sla.ssa - Show SSA form of instruction"); r_cons_println (cons, "| a:sla.defuse - Show def-use analysis of instruction"); - r_cons_println (cons, "| a:sla.types - Dump inferred type write-back payload for current function"); + r_cons_println (cons, "| a:sla.types [name|addr] - Dump inferred type write-back payload (current by default)"); r_cons_println (cons, "| a:sla.ssa.func - Show function SSA with phi nodes"); r_cons_println (cons, "| a:sla.ssa.func.opt - Show optimized function SSA"); r_cons_println (cons, "| a:sla.defuse.func - Show function-wide def-use analysis"); r_cons_println (cons, "| a:sla.dom - Show dominator tree for current function"); r_cons_println (cons, "| a:sla.slice - Backward slice from variable (e.g. rax_3)"); - r_cons_println (cons, "| a:sla.sym - Symbolic execution summary for current function"); - r_cons_println (cons, "| a:sla.sym.paths - Explore paths in current function"); + r_cons_println (cons, "| a:sla.sym [name|addr] - Symbolic execution summary (current by default)"); + r_cons_println (cons, "| a:sla.sym.paths [name|addr] - Explore paths in function (current by default)"); r_cons_println (cons, "| a:sla.sym.merge [on|off] - Toggle symbolic state merging"); r_cons_println (cons, "| a:sla.taint - Taint analysis for current function"); r_cons_println (cons, "| a:sla.dec [name|addr] - Decompile function (current by default)"); @@ -5513,6 +6155,8 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { r_cons_println (cons, "| a:sla.cfg.json - Show CFG as JSON for current function"); r_cons_println (cons, "| a:sym.explore - Explore symbolic paths reaching target"); r_cons_println (cons, "| a:sym.solve - Solve concrete input for target reachability"); + r_cons_println (cons, "| a:sym.explore.replayj - Explore from a replay checkpoint frontier"); + r_cons_println (cons, "| a:sym.solve.replayj - Solve from a replay checkpoint frontier"); r_cons_println (cons, "| a:sym.runj - Run typed symbolic exploration spec"); r_cons_println (cons, "| a:sym.replayj - Search checkpointed replay branches"); r_cons_println (cons, "| a:sym.state - Show last symbolic explore/solve cached result"); @@ -5533,7 +6177,7 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { const char *arg = skip_cmd_spaces (cmd + 8); R2ILContext *ctx; RAnalFunction *fcn; - BlockArray blocks; + SymFunctionScope scope; char *spec_json = NULL; char *result = NULL; bool rust_owned = true; @@ -5555,8 +6199,8 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { R_LOG_ERROR ("r2sleigh: no function at current address"); return strdup(""); } - if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { - R_LOG_ERROR ("r2sleigh: failed to lift function blocks"); + if (!build_symbolic_function_scope (anal, fcn, ctx, &scope)) { + R_LOG_ERROR ("r2sleigh: failed to build symbolic function scope"); return strdup(""); } char *sym_map_json = build_sym_symbol_map_json (core); @@ -5567,11 +6211,11 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { spec_json = strdup (arg); if (!spec_json) { - block_array_free (&blocks); + sym_function_scope_free (&scope); return strdup(""); } r_str_unescape (spec_json); - result = r2sym_run_spec_json (ctx, (const R2ILBlock **)blocks.blocks, blocks.count, fcn->addr, spec_json); + result = r2sym_run_spec_json_scope (ctx, scope.functions, scope.count, fcn->addr, spec_json); free (spec_json); if (!result) { rust_owned = false; @@ -5589,7 +6233,7 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { } else { free (result); } - block_array_free (&blocks); + sym_function_scope_free (&scope); return strdup(""); } @@ -5628,6 +6272,93 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { return strdup (""); } + if (is_sym_ns && (!strncmp (cmd, "sym.explore.replayj", 19) || !strncmp (cmd, "sym.solve.replayj", 17))) { + bool is_explore = r_str_startswith (cmd, "sym.explore.replayj"); + size_t prefix_len = is_explore ? 19 : 17; + const char *arg = skip_cmd_spaces (cmd + prefix_len); + ReplaySymSeedSpec spec; + ut64 target = 0; + R2ILContext *ctx; + RAnalFunction *fcn; + SymFunctionScope scope; + char *spec_json = NULL; + char *result = NULL; + bool rust_owned = true; + + if (!arg || !*arg) { + if (cons) { + r_cons_println (cons, is_explore + ? "Usage: a:sym.explore.replayj " + : "Usage: a:sym.solve.replayj "); + } + return strdup (""); + } + if (!core->dbg || !core->dbg->session) { + R_LOG_ERROR ("r2sleigh: debug session with checkpoints is required"); + return strdup (""); + } + if (!parse_replay_target_and_json (core, arg, &target, &spec_json)) { + R_LOG_ERROR ("r2sleigh: invalid replay symbolic target/spec"); + if (cons) { + r_cons_println (cons, is_explore + ? "Usage: a:sym.explore.replayj " + : "Usage: a:sym.solve.replayj "); + } + return strdup (""); + } + if (!replay_sym_seed_spec_parse (core, spec_json, &spec)) { + R_LOG_ERROR ("r2sleigh: invalid replay symbolic seed spec"); + free (spec_json); + return strdup (""); + } + free (spec_json); + + ctx = get_context (anal); + if (!ctx) { + R_LOG_ERROR ("r2sleigh: no context"); + replay_sym_seed_spec_fini (&spec); + return strdup (""); + } + fcn = resolve_or_materialize_current_function (core, anal); + if (!fcn) { + R_LOG_ERROR ("r2sleigh: no function at current address"); + replay_sym_seed_spec_fini (&spec); + return strdup (""); + } + if (!build_symbolic_function_scope (anal, fcn, ctx, &scope)) { + R_LOG_ERROR ("r2sleigh: failed to build symbolic function scope"); + replay_sym_seed_spec_fini (&spec); + return strdup (""); + } + char *sym_map_json = build_sym_symbol_map_json (core); + if (sym_map_json) { + r2sym_set_symbol_map_json (sym_map_json); + free (sym_map_json); + } + + result = replay_sym_query_run (core, ctx, &scope, fcn->addr, target, &spec, is_explore); + if (!result) { + rust_owned = false; + result = strdup ("{\"error\":\"replay symbolic execution failed\"}"); + } + + if (cons && result) { + r_cons_printf (cons, "%s\n", result); + } + if (result && !sym_result_has_error (result)) { + sym_state_cache_update (is_explore ? "explore.replayj" : "solve.replayj", + fcn->addr, spec.entry_addr? spec.entry_addr: fcn->addr, target, result); + } + if (rust_owned) { + r2il_string_free (result); + } else { + free (result); + } + sym_function_scope_free (&scope); + replay_sym_seed_spec_fini (&spec); + return strdup (""); + } + if (is_sym_ns && (!strncmp (cmd, "sym.explore", 11) || !strncmp (cmd, "sym.solve", 9))) { bool is_explore = r_str_startswith (cmd, "sym.explore"); size_t prefix_len = is_explore ? 11 : 9; @@ -5635,7 +6366,7 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { ut64 target = 0; R2ILContext *ctx; RAnalFunction *fcn; - BlockArray blocks; + SymFunctionScope scope; char *result = NULL; bool rust_owned = true; @@ -5662,13 +6393,13 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { R_LOG_ERROR ("r2sleigh: no context"); return strdup(""); } - fcn = r_anal_get_fcn_in (anal, core->addr, R_ANAL_FCN_TYPE_ANY); + fcn = resolve_or_materialize_current_function (core, anal); if (!fcn) { R_LOG_ERROR ("r2sleigh: no function at current address"); return strdup(""); } - if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { - R_LOG_ERROR ("r2sleigh: failed to lift function blocks"); + if (!build_symbolic_function_scope (anal, fcn, ctx, &scope)) { + R_LOG_ERROR ("r2sleigh: failed to build symbolic function scope"); return strdup(""); } char *sym_map_json = build_sym_symbol_map_json (core); @@ -5678,9 +6409,9 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { } if (is_explore) { - result = r2sym_explore_to (ctx, (const R2ILBlock **)blocks.blocks, blocks.count, fcn->addr, target); + result = r2sym_explore_to_scope (ctx, scope.functions, scope.count, fcn->addr, target); } else { - result = r2sym_solve_to (ctx, (const R2ILBlock **)blocks.blocks, blocks.count, fcn->addr, target); + result = r2sym_solve_to_scope (ctx, scope.functions, scope.count, fcn->addr, target); } if (!result) { rust_owned = false; @@ -5699,7 +6430,7 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { } else { free (result); } - block_array_free (&blocks); + sym_function_scope_free (&scope); return strdup(""); } @@ -6008,13 +6739,14 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { return strdup(""); } - if (!strcmp (cmd, "sla.types")) { + if (!strncmp (cmd, "sla.types", 9) && (!cmd[9] || isspace ((unsigned char)cmd[9]))) { R2ILContext *ctx = get_context (anal); RAnalFunction *fcn; BlockArray blocks; char *external_context_json = NULL; char *interproc_scope_json = NULL; char *result = NULL; + const char *target_arg = skip_cmd_spaces (cmd + 9); ut64 *seen_addrs = NULL; size_t seen_count = 0; size_t seen_cap = 0; @@ -6025,9 +6757,15 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { return strdup (""); } - fcn = r_anal_get_fcn_in (anal, core->addr, R_ANAL_FCN_TYPE_ANY); + fcn = (target_arg && *target_arg) + ? resolve_or_materialize_function_target (core, anal, target_arg) + : resolve_or_materialize_current_function (core, anal); if (!fcn) { - R_LOG_ERROR ("r2sleigh: no function at current address"); + if (target_arg && *target_arg) { + R_LOG_ERROR ("r2sleigh: function target not found: %s", target_arg); + } else { + R_LOG_ERROR ("r2sleigh: no function at current address"); + } return strdup (""); } if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { @@ -6044,10 +6782,20 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { warm_type_payload_cache_for_function (core, anal, ctx, fcn, interproc_max_iters, &seen_addrs, &seen_count, &seen_cap); interproc_scope_json = build_type_interproc_scope_json (core, anal, ctx, fcn, &blocks); - result = r2sleigh_infer_type_writeback_json_ex (ctx, - (const R2ILBlock **)blocks.blocks, blocks.count, fcn->addr, fcn->name, - external_context_json, - 1, interproc_max_iters, 1, interproc_scope_json? interproc_scope_json: "{}"); + SymFunctionScope sym_scope; + if (build_symbolic_function_scope (anal, fcn, ctx, &sym_scope)) { + result = r2sleigh_infer_type_writeback_json_scope_ex (ctx, + (const R2ILBlock **)blocks.blocks, blocks.count, fcn->addr, fcn->name, + external_context_json, + 1, interproc_max_iters, 1, interproc_scope_json? interproc_scope_json: "{}", + sym_scope.functions, sym_scope.count); + sym_function_scope_free (&sym_scope); + } else { + result = r2sleigh_infer_type_writeback_json_ex (ctx, + (const R2ILBlock **)blocks.blocks, blocks.count, fcn->addr, fcn->name, + external_context_json, + 1, interproc_max_iters, 1, interproc_scope_json? interproc_scope_json: "{}"); + } if (cons) { if (result && *result) { r_cons_printf (cons, "%s\n", result); @@ -6277,24 +7025,34 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { return strdup(""); } - if (!strcmp (cmd, "sla.sym") || !strcmp (cmd, "sla.sym.paths")) { + if ((!strncmp (cmd, "sla.sym.paths", 13) && (!cmd[13] || isspace ((unsigned char)cmd[13]))) + || (!strncmp (cmd, "sla.sym", 7) && (!cmd[7] || isspace ((unsigned char)cmd[7])))) { R2ILContext *ctx = get_context (anal); + bool is_paths_cmd = r_str_startswith (cmd, "sla.sym.paths"); + size_t prefix_len = is_paths_cmd ? 13: 7; + const char *target_arg = skip_cmd_spaces (cmd + prefix_len); + RAnalFunction *fcn; if (!ctx) { R_LOG_ERROR ("r2sleigh: no context"); return strdup(""); } - /* Get current function */ - RAnalFunction *fcn = r_anal_get_fcn_in (anal, core->addr, R_ANAL_FCN_TYPE_ANY); + fcn = (target_arg && *target_arg) + ? resolve_or_materialize_function_target (core, anal, target_arg) + : resolve_or_materialize_current_function (core, anal); if (!fcn) { - R_LOG_ERROR ("r2sleigh: no function at current address"); + if (target_arg && *target_arg) { + R_LOG_ERROR ("r2sleigh: function target not found: %s", target_arg); + } else { + R_LOG_ERROR ("r2sleigh: no function at current address"); + } return strdup(""); } - /* Lift all blocks */ - BlockArray blocks; - if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { - R_LOG_ERROR ("r2sleigh: failed to lift function blocks"); + /* Lift root + reachable helper closure */ + SymFunctionScope scope; + if (!build_symbolic_function_scope (anal, fcn, ctx, &scope)) { + R_LOG_ERROR ("r2sleigh: failed to build symbolic function scope"); return strdup(""); } char *sym_map_json = build_sym_symbol_map_json (core); @@ -6305,10 +7063,10 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { /* Call symbolic execution */ char *result; - if (!strcmp (cmd, "sla.sym.paths")) { - result = r2sym_paths (ctx, (const R2ILBlock **)blocks.blocks, blocks.count, fcn->addr); + if (is_paths_cmd) { + result = r2sym_paths_scope (ctx, scope.functions, scope.count, fcn->addr); } else { - result = r2sym_function (ctx, (const R2ILBlock **)blocks.blocks, blocks.count, fcn->addr); + result = r2sym_function_scope (ctx, scope.functions, scope.count, fcn->addr); } if (cons && result) { @@ -6316,7 +7074,7 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { } r2il_string_free (result); - block_array_free (&blocks); + sym_function_scope_free (&scope); return strdup(""); } @@ -6362,16 +7120,16 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { const char *target_arg = skip_cmd_spaces (cmd + 7); RAnalFunction *fcn = NULL; if (target_arg && *target_arg) { - fcn = resolve_function_target (core, anal, target_arg); + fcn = resolve_or_materialize_function_target (core, anal, target_arg); } else { - fcn = r_anal_get_fcn_in (anal, core->addr, R_ANAL_FCN_TYPE_ANY); + fcn = resolve_or_materialize_current_function (core, anal); } if (!fcn) { if (target_arg && *target_arg) { if (cons) { r_cons_printf (cons, - "/* r2dec: function target '%s' not found. It may be inlined or stripped. Try 'afl' or 'a:sla.dec 0xADDR'. */\n", + "/* r2dec: function target '%s' not found or could not be materialized (it may be inlined or stripped). */\n", target_arg); } } else { @@ -6379,7 +7137,6 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { } return strdup(""); } - /* Lift all blocks */ BlockArray blocks; if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { @@ -6497,9 +7254,18 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { } /* Decompile with context */ - char *result = r2dec_function_with_context (ctx, (const R2ILBlock **)blocks.blocks, blocks.count, - fcn->name, func_names_json, strings_json, symbols_json, - external_context_json); + char *result; + SymFunctionScope sym_scope; + if (build_symbolic_function_scope (anal, fcn, ctx, &sym_scope)) { + result = r2dec_function_with_context_scope (ctx, (const R2ILBlock **)blocks.blocks, blocks.count, + fcn->addr, fcn->name, func_names_json, strings_json, symbols_json, + external_context_json, sym_scope.functions, sym_scope.count); + sym_function_scope_free (&sym_scope); + } else { + result = r2dec_function_with_context (ctx, (const R2ILBlock **)blocks.blocks, blocks.count, + fcn->name, func_names_json, strings_json, symbols_json, + external_context_json); + } if (cons) { if (result && result[0]) { @@ -8088,6 +8854,140 @@ static ut64 *collect_type_interproc_direct_targets_from_blocks( return targets; } +static void sym_function_scope_init(SymFunctionScope *scope) { + if (!scope) { + return; + } + memset (scope, 0, sizeof (*scope)); +} + +static void sym_function_scope_free(SymFunctionScope *scope) { + size_t i; + if (!scope) { + return; + } + for (i = 0; i < scope->count; i++) { + block_array_free (&scope->owned_blocks[i]); + free (scope->owned_names[i]); + } + free (scope->functions); + free (scope->owned_blocks); + free (scope->owned_names); + memset (scope, 0, sizeof (*scope)); +} + +static bool sym_function_scope_ensure_capacity(SymFunctionScope *scope, size_t needed) { + R2ILFunctionBlocks *functions_next; + BlockArray *blocks_next; + char **names_next; + size_t new_cap; + if (!scope) { + return false; + } + if (needed <= scope->capacity) { + return true; + } + new_cap = scope->capacity? scope->capacity * 2: 4; + while (new_cap < needed) { + new_cap *= 2; + } + functions_next = realloc (scope->functions, new_cap * sizeof (*scope->functions)); + blocks_next = realloc (scope->owned_blocks, new_cap * sizeof (*scope->owned_blocks)); + names_next = realloc (scope->owned_names, new_cap * sizeof (*scope->owned_names)); + if (!functions_next || !blocks_next || !names_next) { + free (functions_next); + free (blocks_next); + free (names_next); + return false; + } + scope->functions = functions_next; + scope->owned_blocks = blocks_next; + scope->owned_names = names_next; + scope->capacity = new_cap; + return true; +} + +static bool sym_function_scope_append( + SymFunctionScope *scope, + RAnal *anal, + RAnalFunction *fcn, + R2ILContext *ctx +) { + BlockArray blocks; + if (!scope || !anal || !fcn || !ctx) { + return false; + } + if (!sym_function_scope_ensure_capacity (scope, scope->count + 1)) { + return false; + } + if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { + return false; + } + scope->owned_blocks[scope->count] = blocks; + scope->owned_names[scope->count] = fcn->name? strdup (fcn->name): NULL; + scope->functions[scope->count].entry_addr = fcn->addr; + scope->functions[scope->count].name = scope->owned_names[scope->count]; + scope->functions[scope->count].blocks = (const R2ILBlock **)scope->owned_blocks[scope->count].blocks; + scope->functions[scope->count].num_blocks = scope->owned_blocks[scope->count].count; + scope->count++; + return true; +} + +static bool build_symbolic_function_scope( + RAnal *anal, + RAnalFunction *root_fcn, + R2ILContext *ctx, + SymFunctionScope *scope +) { + size_t queue_count = 0; + size_t queue_cap = 0; + size_t queue_index = 0; + ut64 *queue = NULL; + ut64 *seen = NULL; + size_t seen_count = 0; + size_t seen_cap = 0; + + if (!anal || !root_fcn || !ctx || !scope) { + return false; + } + sym_function_scope_init (scope); + if (!append_unique_ut64 (&queue, &queue_count, &queue_cap, root_fcn->addr)) { + free (queue); + return false; + } + + while (queue_index < queue_count && scope->count < SLEIGH_SYM_HELPER_MAX_FUNCTIONS) { + RAnalFunction *fcn; + ut64 addr = queue[queue_index++]; + ut64 *targets = NULL; + size_t target_count = 0; + size_t i; + const BlockArray *blocks; + + fcn = materialize_function_at (anal, addr); + if (!fcn || !append_unique_ut64 (&seen, &seen_count, &seen_cap, fcn->addr)) { + continue; + } + if (!sym_function_scope_append (scope, anal, fcn, ctx)) { + continue; + } + blocks = &scope->owned_blocks[scope->count - 1]; + targets = collect_type_interproc_direct_targets_from_blocks ( + ctx, blocks, fcn->addr, fcn->name, &target_count); + for (i = 0; i < target_count; i++) { + RAnalFunction *callee = materialize_function_at (anal, targets[i]); + if (callee && !function_exceeds_helper_scope_budget (callee)) { + append_unique_ut64 (&queue, &queue_count, &queue_cap, callee->addr); + } + } + free (targets); + } + + free (queue); + free (seen); + return scope->count > 0; +} + static char *build_type_interproc_scope_json_from_targets( RCore *core, RAnal *anal, @@ -8120,8 +9020,9 @@ static char *build_type_interproc_scope_json_from_targets( if (!target) { continue; } - callee_fcn = r_anal_get_fcn_in (anal, target, 0); - if (!callee_fcn || !append_unique_ut64 (&seen_addrs, &seen_count, &seen_cap, callee_fcn->addr)) { + callee_fcn = materialize_function_at (anal, target); + if (!callee_fcn || function_exceeds_helper_scope_budget (callee_fcn) + || !append_unique_ut64 (&seen_addrs, &seen_count, &seen_cap, callee_fcn->addr)) { continue; } entry = type_writeback_cache_get (callee_fcn->addr); @@ -8139,18 +9040,24 @@ static char *build_type_interproc_scope_json_from_targets( pj_k (pj, "seeds"); pj_a (pj); for (i = 0; i < target_count; i++) { + RAnalFunction *seed_fcn; ut64 seed_addr = targets[i]; char *seed_name; - if (!seed_addr || !append_unique_ut64 (&seen_addrs, &seen_count, &seen_cap, seed_addr)) { + if (!seed_addr) { + continue; + } + seed_fcn = materialize_function_at (anal, seed_addr); + if (!seed_fcn || function_exceeds_helper_scope_budget (seed_fcn) + || !append_unique_ut64 (&seen_addrs, &seen_count, &seen_cap, seed_fcn->addr)) { continue; } - seed_name = resolve_interproc_seed_name (core, anal, seed_addr); + seed_name = resolve_interproc_seed_name (core, anal, seed_fcn->addr); if (!seed_name || !*seed_name) { free (seed_name); continue; } pj_o (pj); - pj_kn (pj, "id", seed_addr); + pj_kn (pj, "id", seed_fcn->addr); pj_ks (pj, "name", seed_name); pj_end (pj); free (seed_name); @@ -8202,8 +9109,9 @@ static bool warm_type_payload_cache_for_function( direct_targets = collect_type_interproc_direct_targets_from_blocks ( ctx, &blocks, fcn->addr, fcn->name, &direct_target_count); for (i = 0; i < direct_target_count; i++) { - RAnalFunction *callee_fcn = r_anal_get_fcn_in (anal, direct_targets[i], 0); - if (!callee_fcn || callee_fcn->addr == fcn->addr) { + RAnalFunction *callee_fcn = materialize_function_at (anal, direct_targets[i]); + if (!callee_fcn || callee_fcn->addr == fcn->addr + || function_exceeds_helper_scope_budget (callee_fcn)) { continue; } warm_type_payload_cache_for_function (core, anal, ctx, callee_fcn, max_iters, diff --git a/r2plugin/src/analysis/sym.rs b/r2plugin/src/analysis/sym.rs index bde5f84..1d561d9 100644 --- a/r2plugin/src/analysis/sym.rs +++ b/r2plugin/src/analysis/sym.rs @@ -7,6 +7,7 @@ use std::collections::HashMap; use std::ffi::{CStr, CString}; use std::os::raw::c_char; use std::ptr; +use std::slice; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Mutex, OnceLock}; use z3::{Config, Context}; @@ -32,6 +33,58 @@ pub struct R2SymContext { error: Option, } +#[repr(C)] +pub struct R2ILFunctionBlocks { + entry_addr: u64, + name: *const c_char, + blocks: *const *const R2ILBlock, + num_blocks: usize, +} + +#[repr(C)] +pub struct R2SymReplayRegister { + name: *const c_char, + value: u64, +} + +#[repr(C)] +pub struct R2SymReplayMemoryWindow { + addr: u64, + bytes: *const u8, + size: usize, + label: *const c_char, +} + +#[repr(C)] +pub struct R2SymReplayRegisterOverlay { + name: *const c_char, + symbol: *const c_char, +} + +#[repr(C)] +pub struct R2SymReplayMemoryOverlay { + addr: u64, + size: u32, + name: *const c_char, +} + +#[repr(C)] +pub struct R2SymReplaySeed { + checkpoint_id: u64, + entry_addr: u64, + registers: *const R2SymReplayRegister, + num_registers: usize, + memory: *const R2SymReplayMemoryWindow, + num_memory: usize, + register_overlays: *const R2SymReplayRegisterOverlay, + num_register_overlays: usize, + memory_overlays: *const R2SymReplayMemoryOverlay, + num_memory_overlays: usize, + tty_fds: *const i32, + num_tty_fds: usize, + skip_sleep_calls: i32, +} + /// Initialize the symbolic execution engine. /// Returns 1 on success, 0 on failure. /// Note: This is an intentional no-op because contexts are created per-state. @@ -107,17 +160,26 @@ fn sym_default_config() -> r2sym::ExploreConfig { } } -fn sym_paths_config(prepared: &r2ssa::SsaArtifact) -> r2sym::ExploreConfig { - let mut config = sym_default_config(); +fn sym_default_query_config() -> r2sym::SymQueryConfig { + r2sym::SymQueryConfig { + explore: sym_default_config(), + mode: r2sym::QueryMode::TargetGuided, + summary_profile: r2sym::SummaryProfile::Default, + } +} + +fn sym_paths_query_config(prepared: &r2ssa::SsaArtifact) -> r2sym::SymQueryConfig { + let mut config = sym_default_query_config(); if prepared.call_sites().by_id.is_empty() { - config.max_states = SYM_PATHS_CALL_FREE_MAX_STATES; - config.max_depth = SYM_PATHS_CALL_FREE_MAX_DEPTH; + config.explore.max_states = SYM_PATHS_CALL_FREE_MAX_STATES; + config.explore.max_depth = SYM_PATHS_CALL_FREE_MAX_DEPTH; } else { - config.max_states = SYM_PATHS_CALL_HEAVY_MAX_STATES; - config.max_depth = SYM_PATHS_CALL_HEAVY_MAX_DEPTH; + config.explore.max_states = SYM_PATHS_CALL_HEAVY_MAX_STATES; + config.explore.max_depth = SYM_PATHS_CALL_HEAVY_MAX_DEPTH; } - config.timeout = Some(std::time::Duration::from_millis(SYM_PATHS_TIMEOUT_MS)); - config.max_completed_paths = Some(SYM_PATHS_LIMIT); + config.explore.timeout = Some(std::time::Duration::from_millis(SYM_PATHS_TIMEOUT_MS)); + config.explore.max_completed_paths = Some(SYM_PATHS_LIMIT); + config.summary_profile = r2sym::SummaryProfile::PathListing; config } @@ -142,30 +204,6 @@ fn sym_symbol_map() -> &'static Mutex> { MAP.get_or_init(|| Mutex::new(HashMap::new())) } -fn build_seed_interproc_summary_set( - prepared: &r2ssa::SsaArtifact, - symbol_map: &HashMap, -) -> r2ssa::InterprocSummarySet { - let mut summaries = BTreeMap::new(); - for call in prepared.call_sites().by_id.values() { - let Some(target) = call.direct_target else { - continue; - }; - let Some(name) = symbol_map.get(&target) else { - continue; - }; - let id = r2ssa::InterprocFunctionId(target); - if let Some(summary) = r2ssa::FunctionSemanticSummary::seed_for_name(id, name) { - summaries.insert(id, summary); - } - } - r2ssa::InterprocSummarySet { - root: None, - summaries, - diagnostics: r2ssa::InterprocSummaryDiagnostics::default(), - } -} - #[unsafe(no_mangle)] pub extern "C" fn r2sym_set_symbol_map_json(json: *const c_char) -> i32 { if json.is_null() { @@ -200,6 +238,261 @@ struct SymExecSummary { solve_calls: usize, solve_unsat_shortcuts: usize, time_ms: u64, + #[serde(skip_serializing_if = "Option::is_none")] + semantic: Option, +} + +#[derive(Debug, Serialize, Clone)] +pub(crate) struct CompiledSemanticInfo { + pub(crate) mode: String, + pub(crate) capability: SemanticCapabilityInfo, + pub(crate) slice_class: String, + pub(crate) residual_reasons: Vec, + pub(crate) closure_functions: usize, + pub(crate) helper_functions: usize, + pub(crate) derived_summaries: usize, + pub(crate) summary_attempted: usize, + pub(crate) summary_budget_exhausted: usize, + pub(crate) summary_scc_count: usize, + pub(crate) branches_pruned: usize, + pub(crate) branches_unknown: usize, + pub(crate) skipped_large_cfg: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) interpreter: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) vm_step: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) vm_transfer: Option, + pub(crate) cache_hit: bool, +} + +#[derive(Debug, Serialize, Clone)] +pub(crate) struct SemanticCapabilityInfo { + pub(crate) query_ready: bool, + pub(crate) type_ready: bool, + pub(crate) decompile_ready: bool, +} + +#[derive(Debug, Serialize, Clone)] +pub(crate) struct InterpreterDispatchInfo { + pub(crate) kind: String, + pub(crate) dispatch_header: String, + pub(crate) dispatch_targets: usize, + pub(crate) selector: Option, + pub(crate) back_edges: usize, + pub(crate) score: i32, +} + +#[derive(Debug, Serialize, Clone)] +pub(crate) struct VmStateUpdateInfo { + pub(crate) output: String, + pub(crate) expr: String, + pub(crate) value: String, + pub(crate) exact: bool, +} + +#[derive(Debug, Serialize, Clone)] +pub(crate) struct VmTransferArmInfo { + pub(crate) handler_target: String, + pub(crate) case_values: Vec, + pub(crate) region_blocks: Vec, + pub(crate) exit_targets: Vec, + pub(crate) state_updates: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) selector_update: Option, + pub(crate) exact: bool, + pub(crate) redispatch: bool, + pub(crate) may_return: bool, + pub(crate) truncated: bool, +} + +#[derive(Debug, Serialize, Clone)] +pub(crate) struct VmStepSummaryInfo { + pub(crate) kind: String, + pub(crate) loop_header: String, + pub(crate) dispatch_header: String, + pub(crate) selector: Option, + pub(crate) dispatch_targets: Vec, + pub(crate) default_target: Option, + pub(crate) case_values_by_target: BTreeMap>, + pub(crate) loop_latches: Vec, + pub(crate) state_inputs: Vec, + pub(crate) state_outputs: Vec, + pub(crate) step_blocks: Vec, + pub(crate) handler_regions: BTreeMap>, + pub(crate) handler_state_inputs: BTreeMap>, + pub(crate) handler_state_outputs: BTreeMap>, + pub(crate) handler_state_updates: BTreeMap>, + pub(crate) handler_memory_reads: BTreeMap, + pub(crate) handler_memory_writes: BTreeMap, + pub(crate) handler_calls: BTreeMap, + pub(crate) handler_conditional_branches: BTreeMap, + pub(crate) handler_exit_targets: BTreeMap>, + pub(crate) redispatch_handlers: Vec, + pub(crate) returning_handlers: Vec, + pub(crate) truncated_handlers: Vec, + pub(crate) transfers: Vec, +} + +fn render_vm_value_expr(value: &r2sym::VmValueExpr) -> String { + match value { + r2sym::VmValueExpr::Const(value) => format!("0x{value:x}"), + r2sym::VmValueExpr::Var(name) | r2sym::VmValueExpr::Expr(name) => name.clone(), + } +} + +fn vm_state_update_info_from_sym(update: &r2sym::VmStateUpdate) -> VmStateUpdateInfo { + VmStateUpdateInfo { + output: update.output.clone(), + expr: update.expr.clone(), + value: render_vm_value_expr(&update.value), + exact: update.exact, + } +} + +fn vm_transfer_arm_info_from_sym(transfer: &r2sym::VmTransferArm) -> VmTransferArmInfo { + VmTransferArmInfo { + handler_target: format!("0x{:x}", transfer.handler_target), + case_values: transfer.case_values.clone(), + region_blocks: transfer + .region_blocks + .iter() + .map(|addr| format!("0x{addr:x}")) + .collect(), + exit_targets: transfer + .exit_targets + .iter() + .map(|addr| format!("0x{addr:x}")) + .collect(), + state_updates: transfer + .state_updates + .iter() + .map(vm_state_update_info_from_sym) + .collect(), + selector_update: transfer + .selector_update + .as_ref() + .map(vm_state_update_info_from_sym), + exact: transfer.exact, + redispatch: transfer.redispatch, + may_return: transfer.may_return, + truncated: transfer.truncated, + } +} + +fn vm_step_summary_info_from_sym(vm_step: &r2sym::VmStepSummary) -> VmStepSummaryInfo { + VmStepSummaryInfo { + kind: match vm_step.kind { + r2sym::InterpreterKind::SwitchDispatch => "switch_dispatch".to_string(), + r2sym::InterpreterKind::IndirectDispatch => "indirect_dispatch".to_string(), + }, + loop_header: format!("0x{:x}", vm_step.loop_header), + dispatch_header: format!("0x{:x}", vm_step.dispatch_header), + selector: vm_step.selector.clone(), + dispatch_targets: vm_step + .dispatch_targets + .iter() + .map(|addr| format!("0x{addr:x}")) + .collect(), + default_target: vm_step.default_target.map(|addr| format!("0x{addr:x}")), + case_values_by_target: vm_step + .case_values_by_target + .iter() + .map(|(target, values)| (format!("0x{target:x}"), values.clone())) + .collect(), + loop_latches: vm_step + .loop_latches + .iter() + .map(|addr| format!("0x{addr:x}")) + .collect(), + state_inputs: vm_step.state_inputs.clone(), + state_outputs: vm_step.state_outputs.clone(), + step_blocks: vm_step + .step_blocks + .iter() + .map(|addr| format!("0x{addr:x}")) + .collect(), + handler_regions: vm_step + .handler_regions + .iter() + .map(|(target, blocks)| { + ( + format!("0x{target:x}"), + blocks.iter().map(|addr| format!("0x{addr:x}")).collect(), + ) + }) + .collect(), + handler_state_inputs: vm_step + .handler_state_inputs + .iter() + .map(|(target, inputs)| (format!("0x{target:x}"), inputs.clone())) + .collect(), + handler_state_outputs: vm_step + .handler_state_outputs + .iter() + .map(|(target, outputs)| (format!("0x{target:x}"), outputs.clone())) + .collect(), + handler_state_updates: vm_step + .handler_state_updates + .iter() + .map(|(target, updates)| { + ( + format!("0x{target:x}"), + updates.iter().map(vm_state_update_info_from_sym).collect(), + ) + }) + .collect(), + handler_memory_reads: vm_step + .handler_memory_reads + .iter() + .map(|(target, count)| (format!("0x{target:x}"), *count)) + .collect(), + handler_memory_writes: vm_step + .handler_memory_writes + .iter() + .map(|(target, count)| (format!("0x{target:x}"), *count)) + .collect(), + handler_calls: vm_step + .handler_calls + .iter() + .map(|(target, count)| (format!("0x{target:x}"), *count)) + .collect(), + handler_conditional_branches: vm_step + .handler_conditional_branches + .iter() + .map(|(target, count)| (format!("0x{target:x}"), *count)) + .collect(), + handler_exit_targets: vm_step + .handler_exit_targets + .iter() + .map(|(target, exits)| { + ( + format!("0x{target:x}"), + exits.iter().map(|addr| format!("0x{addr:x}")).collect(), + ) + }) + .collect(), + redispatch_handlers: vm_step + .redispatch_handlers + .iter() + .map(|addr| format!("0x{addr:x}")) + .collect(), + returning_handlers: vm_step + .returning_handlers + .iter() + .map(|addr| format!("0x{addr:x}")) + .collect(), + truncated_handlers: vm_step + .truncated_handlers + .iter() + .map(|addr| format!("0x{addr:x}")) + .collect(), + transfers: vm_step + .transfers + .iter() + .map(vm_transfer_arm_info_from_sym) + .collect(), + } } #[derive(Serialize)] @@ -296,6 +589,7 @@ fn build_sym_exec_summary( stats: &r2sym::path::ExploreStats, solver_stats: &r2sym::SolverStats, paths_feasible: usize, + semantic: Option<&r2sym::CompiledSemanticArtifact>, ) -> SymExecSummary { SymExecSummary { paths_explored: stats.paths_completed, @@ -309,6 +603,76 @@ fn build_sym_exec_summary( solve_calls: solver_stats.solve_calls, solve_unsat_shortcuts: solver_stats.solve_unsat_shortcuts, time_ms: stats.total_time.as_millis() as u64, + semantic: semantic.map(compiled_semantic_info), + } +} + +pub(crate) fn compiled_semantic_info( + compiled: &r2sym::CompiledSemanticArtifact, +) -> CompiledSemanticInfo { + CompiledSemanticInfo { + mode: match compiled.mode { + r2sym::SemanticMode::Raw => "raw".to_string(), + r2sym::SemanticMode::Compiled => "compiled".to_string(), + r2sym::SemanticMode::Residual => "residual".to_string(), + r2sym::SemanticMode::VmSummary => "vm_summary".to_string(), + }, + capability: SemanticCapabilityInfo { + query_ready: compiled.capability.query_ready, + type_ready: compiled.capability.type_ready, + decompile_ready: compiled.capability.decompile_ready, + }, + slice_class: match compiled.slice_class { + r2sym::SliceClass::Wrapper => "wrapper".to_string(), + r2sym::SliceClass::Worker => "worker".to_string(), + r2sym::SliceClass::RecursiveGroup => "recursive_group".to_string(), + r2sym::SliceClass::InterpreterSwitch => "interpreter_switch".to_string(), + r2sym::SliceClass::InterpreterIndirect => "interpreter_indirect".to_string(), + r2sym::SliceClass::GenericLarge => "generic_large".to_string(), + }, + residual_reasons: compiled + .residual_reasons + .iter() + .map(|reason| { + match reason { + r2sym::ResidualReason::MissingArch => "missing_arch", + r2sym::ResidualReason::LargeCfg => "large_cfg", + r2sym::ResidualReason::SummaryBudgetExhausted => "summary_budget_exhausted", + r2sym::ResidualReason::SccBudgetExhausted => "scc_budget_exhausted", + r2sym::ResidualReason::InterpreterRequiresStepSummary => { + "interpreter_requires_step_summary" + } + } + .to_string() + }) + .collect(), + closure_functions: compiled.closure_functions, + helper_functions: compiled.helper_functions, + derived_summaries: compiled.derived_summaries, + summary_attempted: compiled.derived_diagnostics.attempted, + summary_budget_exhausted: compiled.derived_diagnostics.budget_exhausted + + compiled.derived_diagnostics.scc_budget_exhausted, + summary_scc_count: compiled.derived_diagnostics.scc_count, + branches_pruned: compiled.symbolic_facts.diagnostics.branches_pruned, + branches_unknown: compiled.symbolic_facts.diagnostics.branches_unknown, + skipped_large_cfg: compiled.symbolic_facts.diagnostics.skipped_large_cfg, + interpreter: compiled + .interpreter + .as_ref() + .map(|interpreter| InterpreterDispatchInfo { + kind: match interpreter.kind { + r2sym::InterpreterKind::SwitchDispatch => "switch_dispatch".to_string(), + r2sym::InterpreterKind::IndirectDispatch => "indirect_dispatch".to_string(), + }, + dispatch_header: format!("0x{:x}", interpreter.dispatch_header), + dispatch_targets: interpreter.dispatch_targets, + selector: interpreter.selector.clone(), + back_edges: interpreter.back_edges, + score: interpreter.score, + }), + vm_step: compiled.vm_step.as_ref().map(vm_step_summary_info_from_sym), + vm_transfer: compiled.vm_step.as_ref().map(vm_step_summary_info_from_sym), + cache_hit: compiled.cache_hit, } } @@ -422,13 +786,57 @@ fn run_path_info_from_result<'ctx>( } } -fn build_symbolic_prepared( +pub(crate) fn build_symbolic_prepared( blocks: &[R2ILBlock], arch: Option<&ArchSpec>, ) -> Option { r2ssa::SsaArtifact::for_symbolic(blocks, arch) } +pub(crate) fn build_single_function_scope( + prepared: r2ssa::SsaArtifact, + entry_addr: u64, + name: Option, +) -> Option { + r2sym::PreparedFunctionScope::new( + entry_addr, + vec![r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(entry_addr), + name, + prepared, + }], + ) +} + +pub(crate) unsafe fn build_symbolic_scope_from_ffi( + functions: *const R2ILFunctionBlocks, + num_functions: usize, + arch: Option<&ArchSpec>, + root_entry_addr: u64, +) -> Option { + if functions.is_null() || num_functions == 0 { + return None; + } + + let mut scope_functions = Vec::new(); + for index in 0..num_functions { + let function = unsafe { &*functions.add(index) }; + let blocks = unsafe { BlockSlice::from_ffi(function.blocks, function.num_blocks) }?; + let prepared = build_symbolic_prepared(blocks.as_slice(), arch)?; + let name = if function.name.is_null() { + None + } else { + unsafe { CStr::from_ptr(function.name).to_str().ok() }.map(str::to_string) + }; + scope_functions.push(r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(function.entry_addr), + name, + prepared, + }); + } + r2sym::PreparedFunctionScope::new(root_entry_addr, scope_functions) +} + fn symbol_map_snapshot() -> HashMap { sym_symbol_map() .lock() @@ -438,18 +846,137 @@ fn symbol_map_snapshot() -> HashMap { fn install_symbolic_hooks<'ctx>( explorer: &mut r2sym::PathExplorer<'ctx>, - prepared: &r2ssa::SsaArtifact, + scope: &r2sym::PreparedFunctionScope, arch: Option<&ArchSpec>, + z3_ctx: &'ctx Context, symbol_map: &HashMap, + summary_profile: r2sym::SummaryProfile, ) { if let Some(arch) = arch - && let Some(registry) = r2sym::SummaryRegistry::with_core_for_arch(arch) + && let Some(registry) = r2sym::SummaryRegistry::with_profile_for_arch(arch, summary_profile) { - let interproc = build_seed_interproc_summary_set(prepared, symbol_map); - let _ = registry.install_known_symbols_for_function(explorer, prepared, symbol_map); - let _ = registry - .install_interproc_summaries_for_function(explorer, prepared, &interproc, symbol_map); + let _ = registry.install_scope_summaries_for_explorer( + explorer, + z3_ctx, + scope, + Some(arch), + symbol_map, + ); + } +} + +fn scope_root_prepared(scope: &r2sym::PreparedFunctionScope) -> Option<&r2ssa::SsaArtifact> { + scope.root().map(|function| &function.prepared) +} + +unsafe fn ffi_slice<'a, T>(ptr: *const T, len: usize) -> Result<&'a [T], &'static str> { + if len == 0 { + return Ok(&[]); + } + if ptr.is_null() { + return Err("missing replay seed array"); } + Ok(unsafe { slice::from_raw_parts(ptr, len) }) +} + +unsafe fn ffi_string(ptr: *const c_char) -> Result { + if ptr.is_null() { + return Err("missing replay seed string"); + } + unsafe { CStr::from_ptr(ptr) } + .to_str() + .map(str::to_string) + .map_err(|_| "replay seed string is not valid utf-8") +} + +unsafe fn replay_seed_from_ffi( + seed: *const R2SymReplaySeed, +) -> Result { + if seed.is_null() { + return Err("missing replay seed"); + } + let seed = unsafe { &*seed }; + + let registers = unsafe { ffi_slice(seed.registers, seed.num_registers) }? + .iter() + .map(|register| { + Ok(r2sym::ReplayRegisterValue { + name: unsafe { ffi_string(register.name) }?, + value: register.value, + }) + }) + .collect::, _>>()?; + + let memory = unsafe { ffi_slice(seed.memory, seed.num_memory) }? + .iter() + .map(|window| { + let bytes = if window.size == 0 { + Vec::new() + } else if window.bytes.is_null() { + return Err("missing replay memory bytes"); + } else { + unsafe { slice::from_raw_parts(window.bytes, window.size) }.to_vec() + }; + let label = if window.label.is_null() { + None + } else { + Some(unsafe { ffi_string(window.label) }?) + }; + Ok(r2sym::ReplayMemoryWindow { + addr: window.addr, + bytes, + label, + }) + }) + .collect::, _>>()?; + + let register_overlays = + unsafe { ffi_slice(seed.register_overlays, seed.num_register_overlays) }? + .iter() + .map(|overlay| { + Ok(r2sym::ReplayRegisterOverlay { + name: unsafe { ffi_string(overlay.name) }?, + symbol: unsafe { ffi_string(overlay.symbol) }?, + }) + }) + .collect::, _>>()?; + + let memory_overlays = unsafe { ffi_slice(seed.memory_overlays, seed.num_memory_overlays) }? + .iter() + .map(|overlay| { + Ok(r2sym::ReplayMemoryOverlay { + addr: overlay.addr, + size: overlay.size, + name: unsafe { ffi_string(overlay.name) }?, + }) + }) + .collect::, _>>()?; + + let tty_fds = unsafe { ffi_slice(seed.tty_fds, seed.num_tty_fds) }?.to_vec(); + + Ok(r2sym::ReplaySeed { + checkpoint_id: (seed.checkpoint_id != 0).then_some(seed.checkpoint_id), + entry_pc: (seed.entry_addr != 0).then_some(seed.entry_addr), + registers, + memory, + register_overlays, + memory_overlays, + tty_fds, + skip_sleep_calls: seed.skip_sleep_calls != 0, + }) +} + +fn build_replay_seeded_state<'ctx>( + z3_ctx: &'ctx Context, + entry_addr: u64, + prepared: &r2ssa::SsaArtifact, + arch: Option<&ArchSpec>, + replay_seed: &r2sym::ReplaySeed, +) -> r2sym::SymState<'ctx> { + let mut initial_state = + r2sym::SymState::new(z3_ctx, replay_seed.entry_pc.unwrap_or(entry_addr)); + r2sym::seed_replay_state_for_arch(&mut initial_state, Some(prepared), arch, replay_seed); + initial_state } #[unsafe(no_mangle)] @@ -470,36 +997,40 @@ pub extern "C" fn r2sym_function( Some(prepared) => prepared, None => return ptr::null_mut(), }; + let Some(scope) = build_single_function_scope(prepared.clone(), entry_addr, None) else { + return ptr::null_mut(); + }; let symbol_map = symbol_map_snapshot(); let z3_ctx = Context::thread_local(); + let query_config = sym_paths_query_config(&prepared); let explore_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let compiled = r2sym::compile_semantic_artifact_with_scope( + &z3_ctx, + &prepared, + Some(&scope), + ctx_view.arch, + &symbol_map, + query_config.summary_profile, + ); let mut initial_state = r2sym::SymState::new(&z3_ctx, entry_addr); r2sym::seed_default_state_for_arch(&mut initial_state, &prepared, ctx_view.arch); - let mut explorer = r2sym::PathExplorer::with_config(&z3_ctx, sym_paths_config(&prepared)); - if let Some(arch) = ctx_view.arch - && let Some(registry) = r2sym::SummaryRegistry::with_profile_for_arch( - arch, - r2sym::SummaryProfile::PathListing, - ) - { - let interproc = build_seed_interproc_summary_set(&prepared, &symbol_map); - let _ = - registry.install_known_symbols_for_function(&mut explorer, &prepared, &symbol_map); - let _ = registry.install_interproc_summaries_for_function( - &mut explorer, - &prepared, - &interproc, - &symbol_map, - ); - } - let results = explorer.explore(&prepared, initial_state); - let stats = explorer.stats().clone(); - let solver_stats = explorer.solver().stats(); - (results, stats, solver_stats) + let mut explorer = query_config.make_explorer(&z3_ctx); + install_symbolic_hooks( + &mut explorer, + &scope, + ctx_view.arch, + &z3_ctx, + &symbol_map, + query_config.summary_profile, + ); + ( + explorer.summarize_function(&prepared, initial_state), + compiled, + ) })); - let (results, stats, solver_stats) = match explore_result { + let (summary, compiled) = match explore_result { Ok(r) => r, Err(_) => { let error_msg = r#"{"error": "symbolic execution failed (z3 context error)"}"#; @@ -507,9 +1038,13 @@ pub extern "C" fn r2sym_function( } }; - let feasible_count = results.iter().filter(|r| r.feasible).count(); - let summary = build_sym_exec_summary(&stats, &solver_stats, feasible_count); - match serde_json::to_string_pretty(&summary) { + let output = build_sym_exec_summary( + &summary.stats, + &summary.solver_stats, + summary.feasible_paths, + Some(&compiled), + ); + match serde_json::to_string_pretty(&output) { Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), Err(_) => ptr::null_mut(), } @@ -551,34 +1086,30 @@ pub extern "C" fn r2sym_paths( Some(prepared) => prepared, None => return ptr::null_mut(), }; + let Some(scope) = build_single_function_scope(prepared.clone(), entry_addr, None) else { + return ptr::null_mut(); + }; let symbol_map = symbol_map_snapshot(); let z3_ctx = Context::thread_local(); + let query_config = sym_paths_query_config(&prepared); let explore_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let mut initial_state = r2sym::SymState::new(&z3_ctx, entry_addr); r2sym::seed_default_state_for_arch(&mut initial_state, &prepared, ctx_view.arch); - let mut explorer = r2sym::PathExplorer::with_config(&z3_ctx, sym_paths_config(&prepared)); - if let Some(arch) = ctx_view.arch - && let Some(registry) = r2sym::SummaryRegistry::with_profile_for_arch( - arch, - r2sym::SummaryProfile::PathListing, - ) - { - let interproc = build_seed_interproc_summary_set(&prepared, &symbol_map); - let _ = - registry.install_known_symbols_for_function(&mut explorer, &prepared, &symbol_map); - let _ = registry.install_interproc_summaries_for_function( - &mut explorer, - &prepared, - &interproc, - &symbol_map, - ); - } - let results = explorer.explore(&prepared, initial_state); - (results, explorer) + let mut explorer = query_config.make_explorer(&z3_ctx); + install_symbolic_hooks( + &mut explorer, + &scope, + ctx_view.arch, + &z3_ctx, + &symbol_map, + query_config.summary_profile, + ); + let summary = explorer.summarize_function(&prepared, initial_state); + (summary, explorer) })); - let (results, explorer) = match explore_result { + let (summary, explorer) = match explore_result { Ok(r) => r, Err(_) => { let error_msg = r#"[{"error": "symbolic execution failed (z3 context error)"}]"#; @@ -586,8 +1117,9 @@ pub extern "C" fn r2sym_paths( } }; - let solution_limit = sym_paths_solution_limit(results.len(), &prepared); - let paths: Vec = results + let solution_limit = sym_paths_solution_limit(summary.paths.len(), &prepared); + let paths: Vec = summary + .paths .iter() .enumerate() .map(|(i, r)| path_info_from_result_with_solution(i, r, &explorer, i < solution_limit)) @@ -617,35 +1149,33 @@ pub extern "C" fn r2sym_explore_to( Some(prepared) => prepared, None => return sym_error_json("failed to build SSA function"), }; + let Some(scope) = build_single_function_scope(prepared.clone(), entry_addr, None) else { + return sym_error_json("failed to build symbolic scope"); + }; let symbol_map = symbol_map_snapshot(); + let query_config = sym_default_query_config(); let z3_ctx = Context::thread_local(); let explore_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let mut initial_state = r2sym::SymState::new(&z3_ctx, entry_addr); r2sym::seed_default_state_for_arch(&mut initial_state, &prepared, ctx_view.arch); - let mut explorer = r2sym::PathExplorer::with_config(&z3_ctx, sym_default_config()); - if let Some(arch) = ctx_view.arch - && let Some(registry) = r2sym::SummaryRegistry::with_core_for_arch(arch) - { - let interproc = build_seed_interproc_summary_set(&prepared, &symbol_map); - let _ = - registry.install_known_symbols_for_function(&mut explorer, &prepared, &symbol_map); - let _ = registry.install_interproc_summaries_for_function( - &mut explorer, - &prepared, - &interproc, - &symbol_map, - ); - } - let matched = explorer.find_paths_to(&prepared, initial_state, target_addr); - let stats = explorer.stats().clone(); - let solver_stats = explorer.solver().stats(); - let paths: Vec = matched + let mut explorer = query_config.make_explorer(&z3_ctx); + install_symbolic_hooks( + &mut explorer, + &scope, + ctx_view.arch, + &z3_ctx, + &symbol_map, + query_config.summary_profile, + ); + let reach = explorer.can_reach(&prepared, initial_state, target_addr); + let paths: Vec = reach + .paths .iter() .enumerate() .map(|(i, r)| path_info_from_result(i, r, &explorer)) .collect(); - (paths, stats, solver_stats) + (paths, reach.stats, reach.solver_stats) })); let (paths, stats, solver_stats) = match explore_result { @@ -656,7 +1186,7 @@ pub extern "C" fn r2sym_explore_to( entry: format!("0x{:x}", entry_addr), target: format!("0x{:x}", target_addr), matched_paths: paths.len(), - stats: build_sym_exec_summary(&stats, &solver_stats, paths.len()), + stats: build_sym_exec_summary(&stats, &solver_stats, paths.len(), None), paths, }; @@ -685,35 +1215,36 @@ pub extern "C" fn r2sym_solve_to( Some(prepared) => prepared, None => return sym_error_json("failed to build SSA function"), }; + let Some(scope) = build_single_function_scope(prepared.clone(), entry_addr, None) else { + return sym_error_json("failed to build symbolic scope"); + }; let symbol_map = symbol_map_snapshot(); + let query_config = sym_default_query_config(); let z3_ctx = Context::thread_local(); let solve_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let mut initial_state = r2sym::SymState::new(&z3_ctx, entry_addr); r2sym::seed_default_state_for_arch(&mut initial_state, &prepared, ctx_view.arch); - let mut explorer = r2sym::PathExplorer::with_config(&z3_ctx, sym_default_config()); - if let Some(arch) = ctx_view.arch - && let Some(registry) = r2sym::SummaryRegistry::with_core_for_arch(arch) - { - let interproc = build_seed_interproc_summary_set(&prepared, &symbol_map); - let _ = - registry.install_known_symbols_for_function(&mut explorer, &prepared, &symbol_map); - let _ = registry.install_interproc_summaries_for_function( - &mut explorer, - &prepared, - &interproc, - &symbol_map, - ); - } - let matched = explorer.find_paths_to(&prepared, initial_state, target_addr); - let stats = explorer.stats().clone(); - let solver_stats = explorer.solver().stats(); - let selected = matched - .iter() - .enumerate() - .min_by_key(|(idx, path)| (path.num_constraints(), path.depth, *idx)) + let mut explorer = query_config.make_explorer(&z3_ctx); + install_symbolic_hooks( + &mut explorer, + &scope, + ctx_view.arch, + &z3_ctx, + &symbol_map, + query_config.summary_profile, + ); + let solve = explorer.solve_for_target(&prepared, initial_state, target_addr); + let selected = solve + .selected_path_index + .and_then(|idx| solve.matched_paths.get(idx).map(|path| (idx, path))) .map(|(idx, path)| path_info_from_result(idx, path, &explorer)); - (matched.len(), selected, stats, solver_stats) + ( + solve.matched_paths.len(), + selected, + solve.stats, + solve.solver_stats, + ) })); let (matched_paths, selected_path, stats, solver_stats) = match solve_result { @@ -725,7 +1256,7 @@ pub extern "C" fn r2sym_solve_to( target: format!("0x{:x}", target_addr), matched_paths, found: selected_path.is_some(), - stats: build_sym_exec_summary(&stats, &solver_stats, matched_paths), + stats: build_sym_exec_summary(&stats, &solver_stats, matched_paths, None), selected_path, }; @@ -771,9 +1302,12 @@ pub extern "C" fn r2sym_run_spec_json( Some(prepared) => prepared, None => return sym_error_json("failed to build SSA function"), }; + let Some(scope) = build_single_function_scope(prepared.clone(), entry_addr, None) else { + return sym_error_json("failed to build symbolic scope"); + }; let symbol_map = symbol_map_snapshot(); let z3_ctx = Context::thread_local(); - let default_config = sym_default_config(); + let default_config = sym_default_query_config(); let run_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let start_pc = spec.start_pc(entry_addr)?; @@ -781,9 +1315,18 @@ pub extern "C" fn r2sym_run_spec_json( r2sym::seed_default_state_for_arch(&mut initial_state, &prepared, ctx_view.arch); spec.apply_to_state(&mut initial_state); - let mut explorer = - r2sym::PathExplorer::with_config(&z3_ctx, spec.to_explore_config(&default_config)); - install_symbolic_hooks(&mut explorer, &prepared, ctx_view.arch, &symbol_map); + let mut explorer = r2sym::PathExplorer::with_config( + &z3_ctx, + spec.to_explore_config(&default_config.explore), + ); + install_symbolic_hooks( + &mut explorer, + &scope, + ctx_view.arch, + &z3_ctx, + &symbol_map, + default_config.summary_profile, + ); let result = explorer.run_spec(&prepared, initial_state, &spec)?; let stats = explorer.stats().clone(); let solver_stats = explorer.solver().stats(); @@ -805,7 +1348,7 @@ pub extern "C" fn r2sym_run_spec_json( let output = SymRunResult { entry: format!("0x{:x}", entry_addr), spec, - stats: build_sym_exec_summary(&stats, &solver_stats, found_paths.len()), + stats: build_sym_exec_summary(&stats, &solver_stats, found_paths.len(), None), stash_counts: SymRunStashCounts { found: found_paths.len(), avoided: result.avoided_states, @@ -822,3 +1365,538 @@ pub extern "C" fn r2sym_run_spec_json( Err(_) => sym_error_json("failed to serialize symbolic run output"), } } + +#[unsafe(no_mangle)] +pub extern "C" fn r2sym_function_scope( + ctx: *const R2ILContext, + functions: *const R2ILFunctionBlocks, + num_functions: usize, + entry_addr: u64, +) -> *mut c_char { + let Some(ctx_view) = require_ctx_view(ctx) else { + return ptr::null_mut(); + }; + let Some(scope) = (unsafe { + build_symbolic_scope_from_ffi(functions, num_functions, ctx_view.arch, entry_addr) + }) else { + return ptr::null_mut(); + }; + let Some(prepared) = scope_root_prepared(&scope) else { + return ptr::null_mut(); + }; + let symbol_map = symbol_map_snapshot(); + let z3_ctx = Context::thread_local(); + let query_config = sym_paths_query_config(prepared); + + let explore_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let compiled = r2sym::compile_semantic_artifact_with_scope( + &z3_ctx, + prepared, + Some(&scope), + ctx_view.arch, + &symbol_map, + query_config.summary_profile, + ); + let mut initial_state = r2sym::SymState::new(&z3_ctx, entry_addr); + r2sym::seed_default_state_for_arch(&mut initial_state, prepared, ctx_view.arch); + let mut explorer = query_config.make_explorer(&z3_ctx); + install_symbolic_hooks( + &mut explorer, + &scope, + ctx_view.arch, + &z3_ctx, + &symbol_map, + query_config.summary_profile, + ); + ( + explorer.summarize_function(prepared, initial_state), + compiled, + ) + })); + + let (summary, compiled) = match explore_result { + Ok(r) => r, + Err(_) => { + let error_msg = r#"{"error": "symbolic execution failed (z3 context error)"}"#; + return CString::new(error_msg).map_or(ptr::null_mut(), |c| c.into_raw()); + } + }; + + let output = build_sym_exec_summary( + &summary.stats, + &summary.solver_stats, + summary.feasible_paths, + Some(&compiled), + ); + match serde_json::to_string_pretty(&output) { + Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), + Err(_) => ptr::null_mut(), + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn r2sym_compile_semantics_scope( + ctx: *const R2ILContext, + functions: *const R2ILFunctionBlocks, + num_functions: usize, + entry_addr: u64, +) -> *mut c_char { + let Some(ctx_view) = require_ctx_view(ctx) else { + return sym_error_json("missing disassembler context"); + }; + let Some(scope) = (unsafe { + build_symbolic_scope_from_ffi(functions, num_functions, ctx_view.arch, entry_addr) + }) else { + return sym_error_json("failed to build symbolic scope"); + }; + let Some(prepared) = scope_root_prepared(&scope) else { + return sym_error_json("failed to build root SSA function"); + }; + let symbol_map = symbol_map_snapshot(); + let z3_ctx = Context::thread_local(); + let query_config = sym_default_query_config(); + let compiled = r2sym::compile_semantic_artifact_with_scope( + &z3_ctx, + prepared, + Some(&scope), + ctx_view.arch, + &symbol_map, + query_config.summary_profile, + ); + match serde_json::to_string(&compiled_semantic_info(&compiled)) { + Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), + Err(_) => sym_error_json("failed to serialize compiled semantics output"), + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn r2sym_paths_scope( + ctx: *const R2ILContext, + functions: *const R2ILFunctionBlocks, + num_functions: usize, + entry_addr: u64, +) -> *mut c_char { + let Some(ctx_view) = require_ctx_view(ctx) else { + return ptr::null_mut(); + }; + let Some(scope) = (unsafe { + build_symbolic_scope_from_ffi(functions, num_functions, ctx_view.arch, entry_addr) + }) else { + return ptr::null_mut(); + }; + let Some(prepared) = scope_root_prepared(&scope) else { + return ptr::null_mut(); + }; + let symbol_map = symbol_map_snapshot(); + let z3_ctx = Context::thread_local(); + let query_config = sym_paths_query_config(prepared); + + let explore_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let mut initial_state = r2sym::SymState::new(&z3_ctx, entry_addr); + r2sym::seed_default_state_for_arch(&mut initial_state, prepared, ctx_view.arch); + let mut explorer = query_config.make_explorer(&z3_ctx); + install_symbolic_hooks( + &mut explorer, + &scope, + ctx_view.arch, + &z3_ctx, + &symbol_map, + query_config.summary_profile, + ); + let summary = explorer.summarize_function(prepared, initial_state); + (summary, explorer) + })); + + let (summary, explorer) = match explore_result { + Ok(r) => r, + Err(_) => { + let error_msg = r#"[{"error": "symbolic execution failed (z3 context error)"}]"#; + return CString::new(error_msg).map_or(ptr::null_mut(), |c| c.into_raw()); + } + }; + + let solution_limit = sym_paths_solution_limit(summary.paths.len(), prepared); + let paths: Vec = summary + .paths + .iter() + .enumerate() + .map(|(i, r)| path_info_from_result_with_solution(i, r, &explorer, i < solution_limit)) + .collect(); + match serde_json::to_string_pretty(&paths) { + Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), + Err(_) => ptr::null_mut(), + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn r2sym_explore_to_scope( + ctx: *const R2ILContext, + functions: *const R2ILFunctionBlocks, + num_functions: usize, + entry_addr: u64, + target_addr: u64, +) -> *mut c_char { + let Some(ctx_view) = require_ctx_view(ctx) else { + return sym_error_json("missing disassembler context"); + }; + let Some(scope) = (unsafe { + build_symbolic_scope_from_ffi(functions, num_functions, ctx_view.arch, entry_addr) + }) else { + return sym_error_json("failed to build symbolic scope"); + }; + let Some(prepared) = scope_root_prepared(&scope) else { + return sym_error_json("failed to build root SSA function"); + }; + let symbol_map = symbol_map_snapshot(); + let query_config = sym_default_query_config(); + + let z3_ctx = Context::thread_local(); + let explore_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let mut initial_state = r2sym::SymState::new(&z3_ctx, entry_addr); + r2sym::seed_default_state_for_arch(&mut initial_state, prepared, ctx_view.arch); + let mut explorer = query_config.make_explorer(&z3_ctx); + install_symbolic_hooks( + &mut explorer, + &scope, + ctx_view.arch, + &z3_ctx, + &symbol_map, + query_config.summary_profile, + ); + let reach = explorer.can_reach(prepared, initial_state, target_addr); + let paths: Vec = reach + .paths + .iter() + .enumerate() + .map(|(i, r)| path_info_from_result(i, r, &explorer)) + .collect(); + (paths, reach.stats, reach.solver_stats) + })); + + let (paths, stats, solver_stats) = match explore_result { + Ok(value) => value, + Err(_) => return sym_error_json("symbolic execution failed (z3 context error)"), + }; + let output = SymTargetExploreResult { + entry: format!("0x{:x}", entry_addr), + target: format!("0x{:x}", target_addr), + matched_paths: paths.len(), + stats: build_sym_exec_summary(&stats, &solver_stats, paths.len(), None), + paths, + }; + + match serde_json::to_string(&output) { + Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), + Err(_) => sym_error_json("failed to serialize symbolic exploration output"), + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn r2sym_solve_to_scope( + ctx: *const R2ILContext, + functions: *const R2ILFunctionBlocks, + num_functions: usize, + entry_addr: u64, + target_addr: u64, +) -> *mut c_char { + let Some(ctx_view) = require_ctx_view(ctx) else { + return sym_error_json("missing disassembler context"); + }; + let Some(scope) = (unsafe { + build_symbolic_scope_from_ffi(functions, num_functions, ctx_view.arch, entry_addr) + }) else { + return sym_error_json("failed to build symbolic scope"); + }; + let Some(prepared) = scope_root_prepared(&scope) else { + return sym_error_json("failed to build root SSA function"); + }; + let symbol_map = symbol_map_snapshot(); + let query_config = sym_default_query_config(); + let z3_ctx = Context::thread_local(); + + let solve_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let mut initial_state = r2sym::SymState::new(&z3_ctx, entry_addr); + r2sym::seed_default_state_for_arch(&mut initial_state, prepared, ctx_view.arch); + let mut explorer = query_config.make_explorer(&z3_ctx); + install_symbolic_hooks( + &mut explorer, + &scope, + ctx_view.arch, + &z3_ctx, + &symbol_map, + query_config.summary_profile, + ); + let solve = explorer.solve_for_target(prepared, initial_state, target_addr); + let selected = solve + .selected_path_index + .and_then(|idx| solve.matched_paths.get(idx).map(|path| (idx, path))) + .map(|(idx, path)| path_info_from_result(idx, path, &explorer)); + ( + solve.matched_paths.len(), + selected, + solve.stats, + solve.solver_stats, + ) + })); + + let (matched_paths, selected_path, stats, solver_stats) = match solve_result { + Ok(value) => value, + Err(_) => return sym_error_json("symbolic execution failed (z3 context error)"), + }; + let output = SymTargetSolveResult { + entry: format!("0x{:x}", entry_addr), + target: format!("0x{:x}", target_addr), + matched_paths, + found: selected_path.is_some(), + stats: build_sym_exec_summary(&stats, &solver_stats, matched_paths, None), + selected_path, + }; + + match serde_json::to_string(&output) { + Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), + Err(_) => sym_error_json("failed to serialize symbolic solve output"), + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn r2sym_run_spec_json_scope( + ctx: *const R2ILContext, + functions: *const R2ILFunctionBlocks, + num_functions: usize, + entry_addr: u64, + spec_json: *const c_char, +) -> *mut c_char { + if spec_json.is_null() { + return sym_error_json("missing exploration spec json"); + } + let spec_text = unsafe { + match CStr::from_ptr(spec_json).to_str() { + Ok(text) => text, + Err(_) => return sym_error_json("exploration spec is not valid utf-8"), + } + }; + let spec = match serde_json::from_str::(spec_text) { + Ok(spec) => spec, + Err(err) => return sym_error_json(&format!("failed to parse exploration spec: {err}")), + }; + if let Err(err) = spec.validate() { + return sym_error_json(&err); + } + + let Some(ctx_view) = require_ctx_view(ctx) else { + return sym_error_json("missing disassembler context"); + }; + let Some(scope) = (unsafe { + build_symbolic_scope_from_ffi(functions, num_functions, ctx_view.arch, entry_addr) + }) else { + return sym_error_json("failed to build symbolic scope"); + }; + let Some(prepared) = scope_root_prepared(&scope) else { + return sym_error_json("failed to build root SSA function"); + }; + let symbol_map = symbol_map_snapshot(); + let z3_ctx = Context::thread_local(); + let default_config = sym_default_query_config(); + + let run_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let start_pc = spec.start_pc(entry_addr)?; + let mut initial_state = r2sym::SymState::new(&z3_ctx, start_pc); + r2sym::seed_default_state_for_arch(&mut initial_state, prepared, ctx_view.arch); + spec.apply_to_state(&mut initial_state); + + let mut explorer = r2sym::PathExplorer::with_config( + &z3_ctx, + spec.to_explore_config(&default_config.explore), + ); + install_symbolic_hooks( + &mut explorer, + &scope, + ctx_view.arch, + &z3_ctx, + &symbol_map, + default_config.summary_profile, + ); + let result = explorer.run_spec(prepared, initial_state, &spec)?; + let stats = explorer.stats().clone(); + let solver_stats = explorer.solver().stats(); + let found_paths = result + .found_paths + .iter() + .enumerate() + .map(|(idx, path)| run_path_info_from_result(idx, path, &explorer)) + .collect::>(); + Ok::<_, String>((result, stats, solver_stats, found_paths)) + })); + + let (result, stats, solver_stats, found_paths) = match run_result { + Ok(Ok(value)) => value, + Ok(Err(err)) => return sym_error_json(&err), + Err(_) => return sym_error_json("symbolic execution failed (z3 context error)"), + }; + + let output = SymRunResult { + entry: format!("0x{:x}", entry_addr), + spec, + stats: build_sym_exec_summary(&stats, &solver_stats, found_paths.len(), None), + stash_counts: SymRunStashCounts { + found: found_paths.len(), + avoided: result.avoided_states, + unsat: result.unsat_states, + errored: result.errored_states, + completed: result.completed_states, + }, + found_paths, + diagnostics: result.diagnostics, + }; + + match serde_json::to_string(&output) { + Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), + Err(_) => sym_error_json("failed to serialize symbolic run output"), + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn r2sym_explore_to_replay_scope( + ctx: *const R2ILContext, + functions: *const R2ILFunctionBlocks, + num_functions: usize, + entry_addr: u64, + target_addr: u64, + replay_seed: *const R2SymReplaySeed, +) -> *mut c_char { + let Some(ctx_view) = require_ctx_view(ctx) else { + return sym_error_json("missing disassembler context"); + }; + let replay_seed = unsafe { + match replay_seed_from_ffi(replay_seed) { + Ok(seed) => seed, + Err(err) => return sym_error_json(err), + } + }; + let Some(scope) = (unsafe { + build_symbolic_scope_from_ffi(functions, num_functions, ctx_view.arch, entry_addr) + }) else { + return sym_error_json("failed to build symbolic scope"); + }; + let Some(prepared) = scope_root_prepared(&scope) else { + return sym_error_json("failed to build root SSA function"); + }; + let symbol_map = symbol_map_snapshot(); + let query_config = sym_default_query_config(); + let start_pc = replay_seed.entry_pc.unwrap_or(entry_addr); + let z3_ctx = Context::thread_local(); + + let explore_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let initial_state = + build_replay_seeded_state(&z3_ctx, entry_addr, prepared, ctx_view.arch, &replay_seed); + let mut explorer = query_config.make_explorer(&z3_ctx); + install_symbolic_hooks( + &mut explorer, + &scope, + ctx_view.arch, + &z3_ctx, + &symbol_map, + query_config.summary_profile, + ); + let reach = explorer.can_reach(prepared, initial_state, target_addr); + let paths: Vec = reach + .paths + .iter() + .enumerate() + .map(|(i, r)| path_info_from_result(i, r, &explorer)) + .collect(); + (paths, reach.stats, reach.solver_stats) + })); + + let (paths, stats, solver_stats) = match explore_result { + Ok(value) => value, + Err(_) => return sym_error_json("symbolic replay exploration failed (z3 context error)"), + }; + let output = SymTargetExploreResult { + entry: format!("0x{:x}", start_pc), + target: format!("0x{:x}", target_addr), + matched_paths: paths.len(), + stats: build_sym_exec_summary(&stats, &solver_stats, paths.len(), None), + paths, + }; + + match serde_json::to_string(&output) { + Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), + Err(_) => sym_error_json("failed to serialize replay symbolic exploration output"), + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn r2sym_solve_to_replay_scope( + ctx: *const R2ILContext, + functions: *const R2ILFunctionBlocks, + num_functions: usize, + entry_addr: u64, + target_addr: u64, + replay_seed: *const R2SymReplaySeed, +) -> *mut c_char { + let Some(ctx_view) = require_ctx_view(ctx) else { + return sym_error_json("missing disassembler context"); + }; + let replay_seed = unsafe { + match replay_seed_from_ffi(replay_seed) { + Ok(seed) => seed, + Err(err) => return sym_error_json(err), + } + }; + let Some(scope) = (unsafe { + build_symbolic_scope_from_ffi(functions, num_functions, ctx_view.arch, entry_addr) + }) else { + return sym_error_json("failed to build symbolic scope"); + }; + let Some(prepared) = scope_root_prepared(&scope) else { + return sym_error_json("failed to build root SSA function"); + }; + let symbol_map = symbol_map_snapshot(); + let query_config = sym_default_query_config(); + let start_pc = replay_seed.entry_pc.unwrap_or(entry_addr); + let z3_ctx = Context::thread_local(); + + let solve_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let initial_state = + build_replay_seeded_state(&z3_ctx, entry_addr, prepared, ctx_view.arch, &replay_seed); + let mut explorer = query_config.make_explorer(&z3_ctx); + install_symbolic_hooks( + &mut explorer, + &scope, + ctx_view.arch, + &z3_ctx, + &symbol_map, + query_config.summary_profile, + ); + let solve = explorer.solve_for_target(prepared, initial_state, target_addr); + let selected = solve + .selected_path_index + .and_then(|idx| solve.matched_paths.get(idx).map(|path| (idx, path))) + .map(|(idx, path)| path_info_from_result(idx, path, &explorer)); + ( + solve.matched_paths.len(), + selected, + solve.stats, + solve.solver_stats, + ) + })); + + let (matched_paths, selected_path, stats, solver_stats) = match solve_result { + Ok(value) => value, + Err(_) => return sym_error_json("symbolic replay solve failed (z3 context error)"), + }; + let output = SymTargetSolveResult { + entry: format!("0x{:x}", start_pc), + target: format!("0x{:x}", target_addr), + matched_paths, + found: selected_path.is_some(), + stats: build_sym_exec_summary(&stats, &solver_stats, matched_paths, None), + selected_path, + }; + + match serde_json::to_string(&output) { + Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), + Err(_) => sym_error_json("failed to serialize replay symbolic solve output"), + } +} diff --git a/r2plugin/src/decompiler.rs b/r2plugin/src/decompiler.rs index 966f88c..806830f 100644 --- a/r2plugin/src/decompiler.rs +++ b/r2plugin/src/decompiler.rs @@ -88,9 +88,32 @@ pub(crate) fn decompiler_input_from_artifact( .with_interproc_summary_set(interproc_summary_set) } +fn rename_function_artifact_for_display( + artifact: FunctionAnalysisArtifact, + function_name: &str, +) -> FunctionAnalysisArtifact { + let FunctionAnalysisArtifact { + ssa_func, + pattern_ssa_func, + type_facts, + writeback_plan, + interproc_summary_set, + semantic_artifact, + } = artifact; + FunctionAnalysisArtifact { + ssa_func: ssa_func.with_name(function_name), + pattern_ssa_func: pattern_ssa_func.with_name(function_name), + type_facts, + writeback_plan, + interproc_summary_set, + semantic_artifact, + } +} + #[allow(clippy::too_many_arguments)] pub(crate) fn run_full_decompile_on_large_stack( r2il_blocks: Vec, + fcn_addr: u64, func_name_str: String, arch: Option, ptr_bits: u32, @@ -101,6 +124,7 @@ pub(crate) fn run_full_decompile_on_large_stack( symbols_str: String, external_context_json: String, cached_artifact: Option, + symbolic_scope: Option, ) -> String { const STACK_SIZE: usize = 512 * 1024 * 1024; @@ -115,18 +139,52 @@ pub(crate) fn run_full_decompile_on_large_stack( let config = decompiler_config_for_arch_name(&arch_name, ptr_bits); let function_names = parse_addr_name_map(&func_names_str); let symbols = parse_addr_name_map(&symbols_str); + let display_func_name = crate::helpers::resolve_decompiler_display_name( + fcn_addr, + &func_name_str, + &function_names, + &symbols, + ); let mut artifact = if let Some(artifact) = cached_artifact { + if let Some(semantic_artifact) = artifact.semantic_artifact.as_ref() + && crate::should_decompile_from_semantic_fallback( + &func_name_str, + semantic_artifact, + ) + { + return crate::decompile_semantic_artifact_fallback( + &display_func_name, + semantic_artifact, + ); + } artifact } else { - let Some(artifact) = crate::types::build_detached_function_analysis_artifact( + if let Some(semantic_artifact) = crate::types::collect_detached_semantic_artifact( &r2il_blocks, &func_name_str, arch.as_ref(), - ptr_bits, - semantic_metadata_enabled, - ®_type_hints, - &external_context_json, - ) else { + symbolic_scope.as_ref(), + ) && crate::should_decompile_from_semantic_fallback( + &func_name_str, + &semantic_artifact, + ) { + return crate::decompile_semantic_artifact_fallback( + &display_func_name, + &semantic_artifact, + ); + } + let Some(artifact) = + crate::types::build_detached_function_analysis_artifact_with_scope( + &r2il_blocks, + &func_name_str, + arch.as_ref(), + ptr_bits, + semantic_metadata_enabled, + ®_type_hints, + &external_context_json, + symbolic_scope.as_ref(), + ) + else { return decompile_artifact_guard_fallback( &func_name_str, "failed to build detached analysis artifact", @@ -134,6 +192,7 @@ pub(crate) fn run_full_decompile_on_large_stack( }; artifact }; + artifact = rename_function_artifact_for_display(artifact, &display_func_name); crate::types::enrich_known_function_signatures_from_names( &mut artifact.type_facts, &function_names, diff --git a/r2plugin/src/helpers.rs b/r2plugin/src/helpers.rs index b8cfe1b..7635d85 100644 --- a/r2plugin/src/helpers.rs +++ b/r2plugin/src/helpers.rs @@ -1,4 +1,5 @@ use crate::ArchSpec; +use std::collections::HashMap; use std::ffi::CStr; use std::os::raw::c_char; @@ -97,6 +98,69 @@ pub(crate) fn normalize_sim_name(name: &str) -> Option<&'static str> { } } +fn strip_display_name_prefixes(name: &str) -> &str { + let mut normalized = name.trim(); + for prefix in ["sym.imp.", "sym.", "imp.", "reloc.", "dbg.", "fcn."] { + while let Some(rest) = normalized.strip_prefix(prefix) { + normalized = rest; + } + } + normalized +} + +fn sanitize_c_identifier(name: &str) -> Option { + let trimmed = name.trim(); + if trimmed.is_empty() { + return None; + } + + let mut out = String::new(); + for (idx, ch) in trimmed.chars().enumerate() { + let normalized = if ch.is_ascii_alphanumeric() || ch == '_' { + ch + } else { + '_' + }; + if idx == 0 && normalized.is_ascii_digit() { + out.push('_'); + } + out.push(normalized); + } + + if out.chars().all(|c| c == '_') { + None + } else { + Some(out) + } +} + +pub(crate) fn resolve_decompiler_display_name( + fcn_addr: u64, + raw_name: &str, + function_names: &HashMap, + symbols: &HashMap, +) -> String { + for candidate in [ + symbols.get(&fcn_addr).map(String::as_str), + function_names.get(&fcn_addr).map(String::as_str), + Some(raw_name), + ] { + let Some(candidate) = candidate else { + continue; + }; + let stripped = strip_display_name_prefixes(candidate); + if let Some(clean) = sanitize_c_identifier(stripped) { + return clean; + } + } + + if fcn_addr == 0 { + "func".to_string() + } else { + format!("sub_{fcn_addr:x}") + } +} + pub(crate) fn effective_addr_size_bytes(arch: &ArchSpec) -> u32 { if arch.addr_size > 1 { return arch.addr_size; @@ -137,3 +201,36 @@ pub(crate) fn effective_addr_size_bytes(arch: &ArchSpec) -> u32 { pub(crate) fn effective_ptr_bits(arch: &ArchSpec) -> u32 { effective_addr_size_bytes(arch).saturating_mul(8) } + +#[cfg(test)] +mod tests { + use super::resolve_decompiler_display_name; + use std::collections::HashMap; + + #[test] + fn decompiler_display_name_prefers_symbol_name() { + let function_names = HashMap::from([(0x401617, "dbg.test_symbolic_xor_guard".to_string())]); + let symbols = HashMap::from([(0x401617, "test_symbolic_xor_guard".to_string())]); + + let display = resolve_decompiler_display_name( + 0x401617, + "dbg.test_symbolic_xor_guard", + &function_names, + &symbols, + ); + + assert_eq!(display, "test_symbolic_xor_guard"); + } + + #[test] + fn decompiler_display_name_strips_debug_prefix_without_symbol_hint() { + let display = resolve_decompiler_display_name( + 0x401617, + "dbg.test_symbolic_xor_guard", + &HashMap::new(), + &HashMap::new(), + ); + + assert_eq!(display, "test_symbolic_xor_guard"); + } +} diff --git a/r2plugin/src/lib.rs b/r2plugin/src/lib.rs index 927479d..200913b 100644 --- a/r2plugin/src/lib.rs +++ b/r2plugin/src/lib.rs @@ -2412,6 +2412,119 @@ fn decompile_artifact_guard_fallback(func_name: &str, reason: &str) -> String { ) } +fn decompile_semantic_artifact_fallback( + func_name: &str, + compiled: &r2sym::CompiledSemanticArtifact, +) -> String { + if matches!(compiled.mode, r2sym::SemanticMode::VmSummary) + && let Some(vm_step) = compiled.vm_step.as_ref() + { + let kind = match vm_step.kind { + r2sym::InterpreterKind::SwitchDispatch => "switch_dispatch", + r2sym::InterpreterKind::IndirectDispatch => "indirect_dispatch", + }; + let selector = vm_step.selector.as_deref().unwrap_or("unknown"); + let inputs = if vm_step.state_inputs.is_empty() { + "none".to_string() + } else { + vm_step.state_inputs.join(", ") + }; + let outputs = if vm_step.state_outputs.is_empty() { + "none".to_string() + } else { + vm_step.state_outputs.join(", ") + }; + let handler_preview = vm_step + .dispatch_targets + .iter() + .take(3) + .map(|target| { + let values = vm_step + .case_values_by_target + .get(target) + .map(|values| { + values + .iter() + .map(|value| format!("0x{value:x}")) + .collect::>() + .join("|") + }) + .unwrap_or_else(|| "default".to_string()); + let updates = vm_step + .handler_state_updates + .get(target) + .map(|updates| { + updates + .iter() + .take(3) + .map(|update| format!("{}={}", update.output, update.expr)) + .collect::>() + .join(", ") + }) + .filter(|text| !text.is_empty()) + .unwrap_or_else(|| "no_state_updates".to_string()); + format!("0x{target:x}[{values}] => {updates}") + }) + .collect::>() + .join("; "); + return format!( + "/* r2dec semantic summary: vm_summary for {} ({kind} @ 0x{:x}, loop_header=0x{:x}, selector={}, targets={}, redispatch={}, state_inputs=[{}], state_outputs=[{}], handlers={}) */", + func_name, + vm_step.dispatch_header, + vm_step.loop_header, + selector, + vm_step.dispatch_targets.len(), + vm_step.redispatch_handlers.len(), + inputs, + outputs, + handler_preview, + ); + } + + let info = analysis::sym::compiled_semantic_info(compiled); + let mut reason = format!( + "semantic fallback: {} slice in {} mode", + info.slice_class, info.mode + ); + if !info.residual_reasons.is_empty() { + reason.push_str(" ("); + reason.push_str(&info.residual_reasons.join(", ")); + reason.push(')'); + } + decompile_artifact_guard_fallback(func_name, &reason) +} + +fn is_autogenerated_function_name(name: &str) -> bool { + name.is_empty() + || name.starts_with("fcn.") + || name.starts_with("fcn_") + || name.starts_with("sub.") + || name.starts_with("sub_") + || name.starts_with("loc.") +} + +fn should_decompile_from_semantic_fallback( + function_name: &str, + compiled: &r2sym::CompiledSemanticArtifact, +) -> bool { + if compiled.capability.decompile_ready { + return false; + } + if matches!(compiled.mode, r2sym::SemanticMode::VmSummary) { + return true; + } + if !is_autogenerated_function_name(function_name) { + return false; + } + compiled.symbolic_facts.diagnostics.skipped_large_cfg + || compiled.residual_reasons.iter().any(|reason| { + matches!( + reason, + r2sym::ResidualReason::InterpreterRequiresStepSummary + ) + }) +} + #[cfg(test)] #[derive(Debug, Deserialize)] struct AfcfjArg { @@ -2905,6 +3018,35 @@ pub extern "C" fn r2dec_function_with_context( symbols_json: *const c_char, external_context_json: *const c_char, ) -> *mut c_char { + r2dec_function_with_context_impl(R2DecFunctionWithContextInputs { + ctx, + blocks, + num_blocks, + fcn_addr: 0, + func_name, + func_names_json, + strings_json, + symbols_json, + external_context_json, + scope_functions: ptr::null(), + scope_num_functions: 0, + }) +} + +fn r2dec_function_with_context_impl(inputs: R2DecFunctionWithContextInputs) -> *mut c_char { + let R2DecFunctionWithContextInputs { + ctx, + blocks, + num_blocks, + fcn_addr, + func_name, + func_names_json, + strings_json, + symbols_json, + external_context_json, + scope_functions, + scope_num_functions, + } = inputs; let Some(ctx_view) = context::require_ctx_view(ctx) else { return ptr::null_mut(); }; @@ -2926,9 +3068,25 @@ pub extern "C" fn r2dec_function_with_context( let strings_str = helpers::cstr_or_default(strings_json, "{}"); let symbols_str = helpers::cstr_or_default(symbols_json, "{}"); let external_context_str = helpers::cstr_or_default(external_context_json, "{}"); - let cached_artifact = types::build_function_input(ctx, blocks, num_blocks, 0, func_name) + let symbolic_scope = if scope_functions.is_null() || scope_num_functions == 0 { + None + } else { + unsafe { + analysis::sym::build_symbolic_scope_from_ffi( + scope_functions, + scope_num_functions, + ctx_view.arch, + fcn_addr, + ) + } + }; + let cached_artifact = types::build_function_input(ctx, blocks, num_blocks, fcn_addr, func_name) .and_then(|input| { - types::get_cached_function_analysis_artifact(&input, &external_context_str) + types::get_cached_function_analysis_artifact_with_scope( + &input, + &external_context_str, + symbolic_scope.as_ref(), + ) }); let semantic_metadata_enabled = ctx_view.semantic_metadata_enabled; let reg_type_hints = if semantic_metadata_enabled { @@ -2943,6 +3101,7 @@ pub extern "C" fn r2dec_function_with_context( // stack to prevent stack overflow on complex O2-optimized CFGs. let output = decompiler::run_full_decompile_on_large_stack( block_slice.into_inner(), + fcn_addr, func_name_str, arch_clone, ptr_bits, @@ -2953,11 +3112,57 @@ pub extern "C" fn r2dec_function_with_context( symbols_str, external_context_str, cached_artifact, + symbolic_scope, ); CString::new(output).map_or(ptr::null_mut(), |c| c.into_raw()) } +struct R2DecFunctionWithContextInputs { + ctx: *const R2ILContext, + blocks: *const *const R2ILBlock, + num_blocks: usize, + fcn_addr: u64, + func_name: *const c_char, + func_names_json: *const c_char, + strings_json: *const c_char, + symbols_json: *const c_char, + external_context_json: *const c_char, + scope_functions: *const analysis::sym::R2ILFunctionBlocks, + scope_num_functions: usize, +} + +/// Decompile a function with external context and a prepared symbolic helper scope. +/// Returns C code as a string. Caller must free with r2il_string_free(). +#[unsafe(no_mangle)] +pub extern "C" fn r2dec_function_with_context_scope( + ctx: *const R2ILContext, + blocks: *const *const R2ILBlock, + num_blocks: usize, + fcn_addr: u64, + func_name: *const c_char, + func_names_json: *const c_char, + strings_json: *const c_char, + symbols_json: *const c_char, + external_context_json: *const c_char, + scope_functions: *const analysis::sym::R2ILFunctionBlocks, + scope_num_functions: usize, +) -> *mut c_char { + r2dec_function_with_context_impl(R2DecFunctionWithContextInputs { + ctx, + blocks, + num_blocks, + fcn_addr, + func_name, + func_names_json, + strings_json, + symbols_json, + external_context_json, + scope_functions, + scope_num_functions, + }) +} + /// Decompile a single basic block to C code. /// Returns C code as a string. Caller must free with r2il_string_free(). #[unsafe(no_mangle)] @@ -3161,6 +3366,35 @@ struct InterprocSummaryJson { scope: Option, } +#[derive(Debug, serde::Serialize)] +struct SymbolicBranchJson { + block_addr: u64, + true_target: u64, + false_target: u64, + true_status: String, + false_status: String, + #[serde(skip_serializing_if = "Option::is_none")] + true_condition: Option, + #[serde(skip_serializing_if = "Option::is_none")] + false_condition: Option, + #[serde(skip_serializing_if = "Option::is_none")] + true_compiled: Option, + #[serde(skip_serializing_if = "Option::is_none")] + false_compiled: Option, +} + +#[derive(Debug, serde::Serialize)] +struct SymbolicFactsJson { + branches: Vec, + diagnostics: r2types::SymbolicFactDiagnostics, + #[serde(skip_serializing_if = "Option::is_none")] + interpreter: Option, + #[serde(skip_serializing_if = "Option::is_none")] + vm_step: Option, + #[serde(skip_serializing_if = "Option::is_none")] + vm_transfer: Option, +} + #[derive(Debug, serde::Serialize, Default)] struct TypeWritebackDiagnosticsJson { conflicts: Vec, @@ -3183,9 +3417,69 @@ struct InferredTypeWritebackJson { struct_decls: Vec, global_type_links: Vec, interproc: InterprocSummaryJson, + #[serde(skip_serializing_if = "Option::is_none")] + symbolic: Option, + #[serde(skip_serializing_if = "Option::is_none")] + compiled_semantics: Option, diagnostics: TypeWritebackDiagnosticsJson, } +fn symbolic_reachability_status_json(status: r2types::SymbolicReachabilityStatus) -> String { + match status { + r2types::SymbolicReachabilityStatus::Reachable => "reachable".to_string(), + r2types::SymbolicReachabilityStatus::Unreachable => "unreachable".to_string(), + r2types::SymbolicReachabilityStatus::Unknown => "unknown".to_string(), + } +} + +fn symbolic_facts_json(facts: &r2types::SymbolicSemanticFacts) -> Option { + (!facts.is_empty()).then(|| SymbolicFactsJson { + branches: facts + .branch_facts + .iter() + .map(|fact| SymbolicBranchJson { + block_addr: fact.block_addr, + true_target: fact.true_target, + false_target: fact.false_target, + true_status: symbolic_reachability_status_json(fact.true_status), + false_status: symbolic_reachability_status_json(fact.false_status), + true_condition: fact.true_condition.clone(), + false_condition: fact.false_condition.clone(), + true_compiled: fact.true_compiled.clone(), + false_compiled: fact.false_compiled.clone(), + }) + .collect(), + diagnostics: facts.diagnostics.clone(), + interpreter: facts.interpreter.clone(), + vm_step: facts.vm_step.clone(), + vm_transfer: facts.vm_transfer.clone(), + }) +} + +fn symbolic_facts_json_forced(facts: &r2types::SymbolicSemanticFacts) -> SymbolicFactsJson { + SymbolicFactsJson { + branches: facts + .branch_facts + .iter() + .map(|fact| SymbolicBranchJson { + block_addr: fact.block_addr, + true_target: fact.true_target, + false_target: fact.false_target, + true_status: symbolic_reachability_status_json(fact.true_status), + false_status: symbolic_reachability_status_json(fact.false_status), + true_condition: fact.true_condition.clone(), + false_condition: fact.false_condition.clone(), + true_compiled: fact.true_compiled.clone(), + false_compiled: fact.false_compiled.clone(), + }) + .collect(), + diagnostics: facts.diagnostics.clone(), + interpreter: facts.interpreter.clone(), + vm_step: facts.vm_step.clone(), + vm_transfer: facts.vm_transfer.clone(), + } +} + fn evidence_json(evidence: &[r2types::WritebackEvidence]) -> Vec { evidence .iter() @@ -3208,6 +3502,7 @@ fn struct_fields_json(fields: &[r2types::StructFieldCandidate]) -> Vec, ) -> InferredTypeWritebackJson { InferredTypeWritebackJson { function_name: plan.signature.function_name, @@ -3275,6 +3570,8 @@ fn writeback_plan_json( }) .collect(), interproc, + symbolic, + compiled_semantics: None, diagnostics: TypeWritebackDiagnosticsJson { conflicts: plan.diagnostics.conflicts, warnings: plan.diagnostics.warnings, @@ -3283,6 +3580,100 @@ fn writeback_plan_json( } } +fn type_writeback_payload_from_artifact( + artifact: types::FunctionAnalysisArtifact, + interproc: InterprocInferenceInput<'_>, +) -> InferredTypeWritebackJson { + let ssa_blocks = artifact.pattern_ssa_func.local_ssa_blocks(); + let scope = serde_json::from_str::(interproc.scope_json) + .ok() + .filter(|v| !v.is_null() && v.as_object().map(|obj| !obj.is_empty()).unwrap_or(true)); + let current_summary = artifact + .interproc_summary_set + .as_ref() + .and_then(|summary_set| { + summary_set + .root + .and_then(|root| summary_set.summaries.get(&root).cloned()) + }); + let current_summary_json = current_summary + .as_ref() + .and_then(|summary| serde_json::to_string(summary).ok()); + + writeback_plan_json( + artifact.writeback_plan, + InterprocSummaryJson { + callsite_count: count_callsites(&ssa_blocks), + iterations: interproc.iter.max(1), + max_iterations: interproc.max_iters.max(interproc.iter.max(1)), + converged: interproc.converged, + summary: current_summary, + summary_json: current_summary_json, + scope, + }, + symbolic_facts_json(&artifact.type_facts.symbolic_facts), + ) +} + +fn semantic_type_fallback_payload( + function_name: &str, + arch_name: &str, + interproc: InterprocInferenceInput<'_>, + compiled: &r2sym::CompiledSemanticArtifact, +) -> InferredTypeWritebackJson { + let compiled_info = analysis::sym::compiled_semantic_info(compiled); + let mut warnings = Vec::new(); + let mut warning = format!( + "semantic fallback: {} slice in {} mode", + compiled_info.slice_class, compiled_info.mode + ); + if !compiled_info.residual_reasons.is_empty() { + warning.push_str(" ("); + warning.push_str(&compiled_info.residual_reasons.join(", ")); + warning.push(')'); + } + warnings.push(warning); + if !compiled_info.capability.type_ready { + warnings.push("type analysis not ready from semantic capability".to_string()); + } + + InferredTypeWritebackJson { + function_name: function_name.to_string(), + signature: format!("void {}(void)", function_name), + ret_type: "void".to_string(), + params: Vec::new(), + callconv: "unknown".to_string(), + arch: arch_name.to_string(), + confidence: 0, + callconv_confidence: 0, + var_type_candidates: Vec::new(), + var_rename_candidates: Vec::new(), + struct_decls: Vec::new(), + global_type_links: Vec::new(), + interproc: InterprocSummaryJson { + callsite_count: 0, + iterations: interproc.iter.max(1), + max_iterations: interproc.max_iters.max(interproc.iter.max(1)), + converged: interproc.converged, + summary: None, + summary_json: None, + scope: serde_json::from_str::(interproc.scope_json) + .ok() + .filter(|v| { + !v.is_null() && v.as_object().map(|obj| !obj.is_empty()).unwrap_or(true) + }), + }, + symbolic: Some(symbolic_facts_json_forced( + &types::symbolic_semantic_facts_from_sym(compiled), + )), + compiled_semantics: Some(compiled_info), + diagnostics: TypeWritebackDiagnosticsJson { + warnings, + ..TypeWritebackDiagnosticsJson::default() + }, + } +} + #[cfg(test)] const SIG_WRITEBACK_CONFIDENCE_MIN: u8 = 70; #[cfg(test)] @@ -5522,6 +5913,7 @@ pub extern "C" fn r2sleigh_get_direct_call_targets_json( /// /// Returns JSON suitable for plugin-side confidence/conflict policy. /// Caller must free with r2il_string_free(). +#[derive(Clone, Copy)] struct InterprocInferenceInput<'a> { iter: usize, max_iters: usize, @@ -5536,6 +5928,8 @@ struct TypeWritebackInferenceInput<'a> { fcn_addr: u64, fcn_name: *const c_char, external_context_json: *const c_char, + scope_functions: *const analysis::sym::R2ILFunctionBlocks, + scope_num_functions: usize, interproc: InterprocInferenceInput<'a>, } @@ -5550,46 +5944,75 @@ fn infer_type_writeback_json_impl(input: TypeWritebackInferenceInput<'_>) -> *mu return ptr::null_mut(); }; let external_context = cstr_or_default(input.external_context_json, "{}"); - let Some(artifact) = types::build_function_analysis_artifact( - &function_input, - &external_context, - input.interproc.scope_json, - input.interproc.max_iters, - ) else { - return ptr::null_mut(); + let symbolic_scope = if input.scope_functions.is_null() || input.scope_num_functions == 0 { + None + } else { + unsafe { + analysis::sym::build_symbolic_scope_from_ffi( + input.scope_functions, + input.scope_num_functions, + function_input.ctx.arch, + function_input.function_addr, + ) + } + }; + let arch_name = decompiler::normalize_sig_arch_name(function_input.ctx.arch) + .unwrap_or_else(|| "unknown".to_string()); + let payload = if let Some(cached_artifact) = + types::get_cached_function_analysis_artifact_with_scope( + &function_input, + &external_context, + symbolic_scope.as_ref(), + ) { + if let Some(compiled) = cached_artifact.semantic_artifact.as_ref() + && !compiled.capability.type_ready + { + semantic_type_fallback_payload( + &function_input.function_name, + &arch_name, + input.interproc, + compiled, + ) + } else { + type_writeback_payload_from_artifact(cached_artifact, input.interproc) + } + } else { + let Some(analysis) = types::build_function_analysis(&function_input) else { + return ptr::null_mut(); + }; + let semantic_artifact = types::collect_plugin_semantic_artifact( + &analysis.ssa_func, + function_input.ctx.arch, + symbolic_scope.as_ref(), + ); + if !semantic_artifact.capability.type_ready { + semantic_type_fallback_payload( + &function_input.function_name, + &arch_name, + input.interproc, + &semantic_artifact, + ) + } else { + let interproc_summary_set = types::build_interproc_summary_set( + &function_input, + &analysis, + input.interproc.scope_json, + input.interproc.max_iters, + ); + let Some(artifact) = + types::build_function_analysis_artifact_from_analysis_with_semantic_artifact( + &function_input, + analysis, + &external_context, + Some(interproc_summary_set), + semantic_artifact, + ) + else { + return ptr::null_mut(); + }; + type_writeback_payload_from_artifact(artifact, input.interproc) + } }; - let ssa_blocks = artifact.pattern_ssa_func.local_ssa_blocks(); - if ssa_blocks.is_empty() { - return ptr::null_mut(); - } - let scope = serde_json::from_str::(input.interproc.scope_json) - .ok() - .filter(|v| !v.is_null() && v.as_object().map(|obj| !obj.is_empty()).unwrap_or(true)); - - let current_summary = artifact - .interproc_summary_set - .as_ref() - .and_then(|summary_set| { - summary_set - .root - .and_then(|root| summary_set.summaries.get(&root).cloned()) - }); - let current_summary_json = current_summary - .as_ref() - .and_then(|summary| serde_json::to_string(summary).ok()); - - let payload = writeback_plan_json( - artifact.writeback_plan, - InterprocSummaryJson { - callsite_count: count_callsites(&ssa_blocks), - iterations: input.interproc.iter.max(1), - max_iterations: input.interproc.max_iters.max(input.interproc.iter.max(1)), - converged: input.interproc.converged, - summary: current_summary, - summary_json: current_summary_json, - scope, - }, - ); match serde_json::to_string(&payload) { Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), @@ -5613,6 +6036,8 @@ pub extern "C" fn r2sleigh_infer_type_writeback_json( fcn_addr, fcn_name, external_context_json, + scope_functions: ptr::null(), + scope_num_functions: 0, interproc: InterprocInferenceInput { iter: 1, max_iters: 1, @@ -5643,6 +6068,42 @@ pub extern "C" fn r2sleigh_infer_type_writeback_json_ex( fcn_addr, fcn_name, external_context_json, + scope_functions: ptr::null(), + scope_num_functions: 0, + interproc: InterprocInferenceInput { + iter: interproc_iter.max(1), + max_iters: interproc_max_iters.max(1), + converged: interproc_converged != 0, + scope_json: &scope, + }, + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn r2sleigh_infer_type_writeback_json_scope_ex( + ctx: *const R2ILContext, + blocks: *const *const R2ILBlock, + num_blocks: usize, + fcn_addr: u64, + fcn_name: *const c_char, + external_context_json: *const c_char, + interproc_iter: usize, + interproc_max_iters: usize, + interproc_converged: i32, + interproc_scope_json: *const c_char, + scope_functions: *const analysis::sym::R2ILFunctionBlocks, + scope_num_functions: usize, +) -> *mut c_char { + let scope = cstr_or_default(interproc_scope_json, "{}"); + infer_type_writeback_json_impl(TypeWritebackInferenceInput { + ctx, + blocks, + num_blocks, + fcn_addr, + fcn_name, + external_context_json, + scope_functions, + scope_num_functions, interproc: InterprocInferenceInput { iter: interproc_iter.max(1), max_iters: interproc_max_iters.max(1), @@ -11760,6 +12221,130 @@ mod integration_tests { ); } + #[test] + fn detached_symbolic_branch_facts_prune_self_xor_guard_in_decompiler() { + use r2il::{R2ILBlock, R2ILOp, RegisterDef, SpaceId, Varnode}; + + fn reg(offset: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Register, + offset, + size, + meta: None, + } + } + + fn konst(value: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Const, + offset: value, + size, + meta: None, + } + } + + let mut arch = ArchSpec::new("x86-64"); + arch.addr_size = 8; + arch.add_register(RegisterDef::new("RAX", 0, 8)); + arch.add_register(RegisterDef::new("EDI", 56, 4)); + arch.add_register(RegisterDef::new("RDI", 56, 8)); + + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![ + R2ILOp::IntXor { + dst: reg(0x80, 8), + a: reg(56, 8), + b: reg(56, 8), + }, + R2ILOp::CBranch { + target: konst(0x1010, 8), + cond: reg(0x80, 8), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 4, + ops: vec![ + R2ILOp::Copy { + dst: reg(0, 8), + src: konst(0, 8), + }, + R2ILOp::Return { target: reg(0, 8) }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 4, + ops: vec![ + R2ILOp::Copy { + dst: reg(0, 8), + src: konst(1, 8), + }, + R2ILOp::Return { target: reg(0, 8) }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + + let artifact = crate::types::build_detached_function_analysis_artifact( + &blocks, + "dbg.test_symbolic_xor_guard", + Some(&arch), + 64, + false, + &HashMap::new(), + "{}", + ) + .expect("analysis artifact"); + + assert_eq!( + artifact + .type_facts + .symbolic_facts + .diagnostics + .branches_pruned, + 1 + ); + assert_eq!(artifact.type_facts.symbolic_facts.branch_facts.len(), 1); + let symbolic_json = + symbolic_facts_json(&artifact.type_facts.symbolic_facts).expect("symbolic json"); + assert_eq!(symbolic_json.diagnostics.branches_pruned, 1); + assert_eq!(symbolic_json.branches.len(), 1); + assert_eq!(symbolic_json.branches[0].true_status, "unreachable"); + assert_eq!(symbolic_json.branches[0].false_status, "reachable"); + + let decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); + let output = + decompiler.decompile_input(&crate::decompiler::decompiler_input_from_artifact( + artifact, + HashMap::new(), + HashMap::new(), + HashMap::new(), + )); + + assert!( + output.contains("return 1;") || output.contains("return 0;"), + "expected decompiled function body, got:\n{output}" + ); + assert!( + !output.contains("if ("), + "symbolic branch facts should collapse the impossible guard, got:\n{output}" + ); + assert!( + !output.contains("return 1;"), + "impossible self-xor branch should be removed, got:\n{output}" + ); + } + #[test] fn live_arm64_main_atoi_arg_keeps_semantic_root() { use r2il::{R2ILBlock, R2ILOp, Varnode}; diff --git a/r2plugin/src/types.rs b/r2plugin/src/types.rs index 9999244..79f0ed5 100644 --- a/r2plugin/src/types.rs +++ b/r2plugin/src/types.rs @@ -46,6 +46,7 @@ pub(crate) struct FunctionAnalysisArtifact { pub(crate) type_facts: r2types::FunctionTypeFacts, pub(crate) writeback_plan: r2types::TypeWritebackPlan, pub(crate) interproc_summary_set: Option, + pub(crate) semantic_artifact: Option, } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -60,6 +61,7 @@ struct FunctionArtifactCacheKey { semantic_metadata_enabled: bool, external_context_hash: u64, interproc_scope_hash: u64, + symbolic_scope_hash: u64, } struct HasherWriter<'a, H: Hasher>(&'a mut H); @@ -110,12 +112,14 @@ fn function_artifact_cache_key_parts( semantic_metadata_enabled: bool, external_context_json: &str, interproc_scope_json: &str, + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, ) -> Option { Some(FunctionArtifactCacheKey { analysis: function_analysis_cache_key_parts(function_name, arch, blocks)?, semantic_metadata_enabled, external_context_hash: hash_string_payload(external_context_json), interproc_scope_hash: hash_string_payload(interproc_scope_json), + symbolic_scope_hash: r2sym::stable_scope_hash(symbolic_scope), }) } @@ -165,6 +169,7 @@ fn rename_function_analysis_artifact( type_facts, writeback_plan, interproc_summary_set, + semantic_artifact, } = artifact; FunctionAnalysisArtifact { ssa_func: ssa_func.with_name(function_name), @@ -172,9 +177,354 @@ fn rename_function_analysis_artifact( type_facts, writeback_plan, interproc_summary_set, + semantic_artifact, } } +fn symbolic_reachability_status_from_sym( + status: r2sym::SymbolicReachabilityStatus, +) -> r2types::SymbolicReachabilityStatus { + match status { + r2sym::SymbolicReachabilityStatus::Reachable => { + r2types::SymbolicReachabilityStatus::Reachable + } + r2sym::SymbolicReachabilityStatus::Unreachable => { + r2types::SymbolicReachabilityStatus::Unreachable + } + r2sym::SymbolicReachabilityStatus::Unknown => r2types::SymbolicReachabilityStatus::Unknown, + } +} + +fn symbolic_condition_precision_from_sym( + precision: r2sym::BackwardConditionPrecision, +) -> r2types::SymbolicConditionPrecision { + match precision { + r2sym::BackwardConditionPrecision::Exact => r2types::SymbolicConditionPrecision::Exact, + r2sym::BackwardConditionPrecision::OverApprox => { + r2types::SymbolicConditionPrecision::OverApprox + } + r2sym::BackwardConditionPrecision::ResidualSearchRequired => { + r2types::SymbolicConditionPrecision::ResidualSearchRequired + } + r2sym::BackwardConditionPrecision::Unsupported => { + r2types::SymbolicConditionPrecision::Unsupported + } + } +} + +fn symbolic_memory_region_kind_from_sym( + kind: &r2sym::MemoryRegionKind, +) -> r2types::SymbolicMemoryRegionKind { + match kind { + r2sym::MemoryRegionKind::Stack => r2types::SymbolicMemoryRegionKind::Stack, + r2sym::MemoryRegionKind::Global => r2types::SymbolicMemoryRegionKind::Global, + r2sym::MemoryRegionKind::Input => r2types::SymbolicMemoryRegionKind::Input, + r2sym::MemoryRegionKind::Heap => r2types::SymbolicMemoryRegionKind::Heap, + r2sym::MemoryRegionKind::Replay => r2types::SymbolicMemoryRegionKind::Replay, + r2sym::MemoryRegionKind::EscapedUnknown => { + r2types::SymbolicMemoryRegionKind::EscapedUnknown + } + } +} + +fn symbolic_memory_region_from_sym( + region: &r2sym::BackwardMemoryRegion, +) -> r2types::SymbolicMemoryRegion { + match region { + r2sym::BackwardMemoryRegion::Argument { index } => { + r2types::SymbolicMemoryRegion::Argument { index: *index } + } + r2sym::BackwardMemoryRegion::Region(region) => { + r2types::SymbolicMemoryRegion::Region(r2types::SymbolicMemoryRegionRef { + id: region.id.0, + kind: symbolic_memory_region_kind_from_sym(®ion.kind), + name: region.name.clone(), + }) + } + } +} + +fn symbolic_compiled_condition_from_sym( + summary: &r2sym::BackwardConditionSummary, +) -> r2types::SymbolicCompiledCondition { + r2types::SymbolicCompiledCondition { + simplified: summary.simplified.clone(), + terms: summary.terms.clone(), + memory_terms: summary + .memory_terms + .iter() + .map(|term| r2types::SymbolicMemoryCondition { + region: symbolic_memory_region_from_sym(&term.region), + offset_lo: term.offset_lo, + offset_hi: term.offset_hi, + size: term.size, + exact_offset: term.exact_offset, + expr: term.expr.clone(), + }) + .collect(), + backward_memory_substitutions: summary.backward_memory_substitutions, + backward_memory_candidate_enumerations: summary.backward_memory_candidate_enumerations, + backward_memory_residual_fallbacks: summary.backward_memory_residual_fallbacks, + precision: symbolic_condition_precision_from_sym(summary.precision), + supported_paths: summary.supported_paths, + total_paths: summary.total_paths, + } +} + +fn symbolic_interpreter_kind_from_sym( + kind: r2sym::InterpreterKind, +) -> r2types::SymbolicInterpreterKind { + match kind { + r2sym::InterpreterKind::SwitchDispatch => r2types::SymbolicInterpreterKind::SwitchDispatch, + r2sym::InterpreterKind::IndirectDispatch => { + r2types::SymbolicInterpreterKind::IndirectDispatch + } + } +} + +fn symbolic_vm_value_expr_from_sym(value: &r2sym::VmValueExpr) -> r2types::SymbolicVmValueExpr { + match value { + r2sym::VmValueExpr::Const(value) => r2types::SymbolicVmValueExpr::Const(*value), + r2sym::VmValueExpr::Var(name) => r2types::SymbolicVmValueExpr::Var(name.clone()), + r2sym::VmValueExpr::Expr(expr) => r2types::SymbolicVmValueExpr::Expr(expr.clone()), + } +} + +fn symbolic_vm_state_update_from_sym( + update: &r2sym::VmStateUpdate, +) -> r2types::SymbolicVmStateUpdate { + r2types::SymbolicVmStateUpdate { + output: update.output.clone(), + expr: update.expr.clone(), + value: symbolic_vm_value_expr_from_sym(&update.value), + exact: update.exact, + } +} + +fn symbolic_vm_transfer_arm_from_sym( + transfer: &r2sym::VmTransferArm, +) -> r2types::SymbolicVmTransferArm { + r2types::SymbolicVmTransferArm { + handler_target: transfer.handler_target, + case_values: transfer.case_values.clone(), + region_blocks: transfer.region_blocks.clone(), + exit_targets: transfer.exit_targets.clone(), + state_updates: transfer + .state_updates + .iter() + .map(symbolic_vm_state_update_from_sym) + .collect(), + selector_update: transfer + .selector_update + .as_ref() + .map(symbolic_vm_state_update_from_sym), + exact: transfer.exact, + redispatch: transfer.redispatch, + may_return: transfer.may_return, + truncated: transfer.truncated, + } +} + +fn symbolic_vm_step_summary_from_sym( + vm_step: &r2sym::VmStepSummary, +) -> r2types::SymbolicVmStepSummary { + r2types::SymbolicVmStepSummary { + kind: symbolic_interpreter_kind_from_sym(vm_step.kind), + loop_header: vm_step.loop_header, + dispatch_header: vm_step.dispatch_header, + selector: vm_step.selector.clone(), + dispatch_targets: vm_step.dispatch_targets.clone(), + default_target: vm_step.default_target, + case_values_by_target: vm_step.case_values_by_target.clone(), + loop_latches: vm_step.loop_latches.clone(), + state_inputs: vm_step.state_inputs.clone(), + state_outputs: vm_step.state_outputs.clone(), + step_blocks: vm_step.step_blocks.clone(), + handler_regions: vm_step.handler_regions.clone(), + handler_state_inputs: vm_step.handler_state_inputs.clone(), + handler_state_outputs: vm_step.handler_state_outputs.clone(), + handler_state_updates: vm_step + .handler_state_updates + .iter() + .map(|(target, updates)| { + ( + *target, + updates + .iter() + .map(symbolic_vm_state_update_from_sym) + .collect(), + ) + }) + .collect(), + handler_memory_reads: vm_step.handler_memory_reads.clone(), + handler_memory_writes: vm_step.handler_memory_writes.clone(), + handler_calls: vm_step.handler_calls.clone(), + handler_conditional_branches: vm_step.handler_conditional_branches.clone(), + handler_exit_targets: vm_step.handler_exit_targets.clone(), + redispatch_handlers: vm_step.redispatch_handlers.clone(), + returning_handlers: vm_step.returning_handlers.clone(), + truncated_handlers: vm_step.truncated_handlers.clone(), + transfers: vm_step + .transfers + .iter() + .map(symbolic_vm_transfer_arm_from_sym) + .collect(), + } +} + +pub(crate) fn symbolic_semantic_facts_from_sym( + compiled: &r2sym::CompiledSemanticArtifact, +) -> r2types::SymbolicSemanticFacts { + let facts = &compiled.symbolic_facts; + r2types::SymbolicSemanticFacts { + branch_facts: facts + .branch_facts + .iter() + .cloned() + .map(|fact| r2types::SymbolicBranchFact { + block_addr: fact.block_addr, + true_target: fact.true_target, + false_target: fact.false_target, + true_status: symbolic_reachability_status_from_sym(fact.true_status), + false_status: symbolic_reachability_status_from_sym(fact.false_status), + true_condition: fact.true_condition, + false_condition: fact.false_condition, + true_compiled: fact + .true_compiled + .as_ref() + .map(symbolic_compiled_condition_from_sym), + false_compiled: fact + .false_compiled + .as_ref() + .map(symbolic_compiled_condition_from_sym), + }) + .collect(), + diagnostics: r2types::SymbolicFactDiagnostics { + branches_evaluated: facts.diagnostics.branches_evaluated, + branches_pruned: facts.diagnostics.branches_pruned, + branches_unknown: facts.diagnostics.branches_unknown, + skipped_missing_arch: facts.diagnostics.skipped_missing_arch, + skipped_large_cfg: facts.diagnostics.skipped_large_cfg, + semantic_mode: Some(match compiled.mode { + r2sym::SemanticMode::Raw => r2types::SymbolicSemanticMode::Raw, + r2sym::SemanticMode::Compiled => r2types::SymbolicSemanticMode::Compiled, + r2sym::SemanticMode::Residual => r2types::SymbolicSemanticMode::Residual, + r2sym::SemanticMode::VmSummary => r2types::SymbolicSemanticMode::VmSummary, + }), + semantic_capability: Some(r2types::SymbolicSemanticCapability { + query_ready: compiled.capability.query_ready, + type_ready: compiled.capability.type_ready, + decompile_ready: compiled.capability.decompile_ready, + }), + slice_class: Some(match compiled.slice_class { + r2sym::SliceClass::Wrapper => r2types::SymbolicSemanticSliceClass::Wrapper, + r2sym::SliceClass::Worker => r2types::SymbolicSemanticSliceClass::Worker, + r2sym::SliceClass::RecursiveGroup => { + r2types::SymbolicSemanticSliceClass::RecursiveGroup + } + r2sym::SliceClass::InterpreterSwitch => { + r2types::SymbolicSemanticSliceClass::InterpreterSwitch + } + r2sym::SliceClass::InterpreterIndirect => { + r2types::SymbolicSemanticSliceClass::InterpreterIndirect + } + r2sym::SliceClass::GenericLarge => { + r2types::SymbolicSemanticSliceClass::GenericLarge + } + }), + residual_reasons: compiled + .residual_reasons + .iter() + .map(|reason| match reason { + r2sym::ResidualReason::MissingArch => { + r2types::SymbolicSemanticResidualReason::MissingArch + } + r2sym::ResidualReason::LargeCfg => { + r2types::SymbolicSemanticResidualReason::LargeCfg + } + r2sym::ResidualReason::SummaryBudgetExhausted => { + r2types::SymbolicSemanticResidualReason::SummaryBudgetExhausted + } + r2sym::ResidualReason::SccBudgetExhausted => { + r2types::SymbolicSemanticResidualReason::SccBudgetExhausted + } + r2sym::ResidualReason::InterpreterRequiresStepSummary => { + r2types::SymbolicSemanticResidualReason::InterpreterRequiresStepSummary + } + }) + .collect(), + closure_functions: compiled.closure_functions, + helper_functions: compiled.helper_functions, + derived_summaries: compiled.derived_summaries, + summary_attempted: compiled.derived_diagnostics.attempted, + summary_budget_exhausted: compiled.derived_diagnostics.budget_exhausted + + compiled.derived_diagnostics.scc_budget_exhausted, + summary_scc_count: compiled.derived_diagnostics.scc_count, + }, + interpreter: compiled.interpreter.as_ref().map(|interpreter| { + r2types::SymbolicInterpreterDispatch { + kind: symbolic_interpreter_kind_from_sym(interpreter.kind), + dispatch_header: interpreter.dispatch_header, + dispatch_targets: interpreter.dispatch_targets, + selector: interpreter.selector.clone(), + back_edges: interpreter.back_edges, + score: interpreter.score, + } + }), + vm_step: compiled + .vm_step + .as_ref() + .map(symbolic_vm_step_summary_from_sym), + vm_transfer: compiled + .vm_step + .as_ref() + .map(symbolic_vm_step_summary_from_sym), + } +} + +pub(crate) fn collect_plugin_semantic_artifact( + prepared: &r2ssa::SsaArtifact, + arch: Option<&ArchSpec>, + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, +) -> r2sym::CompiledSemanticArtifact { + let scope = symbolic_scope_with_prepared_root(prepared, symbolic_scope); + r2sym::compile_semantic_artifact_with_scope( + &z3::Context::thread_local(), + prepared, + scope.as_ref(), + arch, + &HashMap::new(), + r2sym::SummaryProfile::Default, + ) +} + +fn symbolic_scope_with_prepared_root( + prepared: &r2ssa::SsaArtifact, + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, +) -> Option { + let scope = symbolic_scope?; + let mut functions = scope.functions().values().cloned().collect::>(); + for function in &mut functions { + if function.id == scope.root_id() { + function.prepared = prepared.clone(); + if function.name.is_none() { + function.name = prepared.function().name.clone(); + } + } + } + r2sym::PreparedFunctionScope::new(scope.root_id().0, functions) +} + +#[allow(dead_code)] +fn collect_plugin_symbolic_facts( + prepared: &r2ssa::SsaArtifact, + arch: Option<&ArchSpec>, + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, +) -> r2types::SymbolicSemanticFacts { + let artifact = collect_plugin_semantic_artifact(prepared, arch, symbolic_scope); + symbolic_semantic_facts_from_sym(&artifact) +} + fn type_like_to_ctype(ty: &r2types::CTypeLike) -> r2dec::CType { match ty { r2types::CTypeLike::Void => r2dec::CType::Void, @@ -383,6 +733,7 @@ fn function_artifact_cache_key( input: &FunctionInput<'_>, external_context_json: &str, interproc_scope_json: &str, + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, ) -> Option { function_artifact_cache_key_parts( &input.function_name, @@ -391,10 +742,11 @@ fn function_artifact_cache_key( input.ctx.semantic_metadata_enabled, external_context_json, interproc_scope_json, + symbolic_scope, ) } -fn build_function_analysis_from_parts( +pub(crate) fn build_function_analysis_from_parts( function_name: &str, blocks: &[R2ILBlock], arch: Option<&ArchSpec>, @@ -431,6 +783,20 @@ pub(crate) fn build_function_analysis(input: &FunctionInput<'_>) -> Option, + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, +) -> Option { + let analysis = build_function_analysis_from_parts(function_name, blocks, arch)?; + Some(collect_plugin_semantic_artifact( + &analysis.ssa_func, + arch, + symbolic_scope, + )) +} + #[derive(Debug, Clone, Default, serde::Deserialize)] struct InterprocScopeSeedNameJson { id: u64, @@ -762,11 +1128,12 @@ fn signature_strength(signature: &r2types::FunctionSignatureSpec) -> u8 { } } -pub(crate) fn build_function_analysis_artifact_from_analysis( +pub(crate) fn build_function_analysis_artifact_from_analysis_with_semantic_artifact( input: &FunctionInput<'_>, analysis: FunctionAnalysis, external_context_json: &str, interproc_summary_set: Option, + semantic_artifact: r2sym::CompiledSemanticArtifact, ) -> Option { let ptr_bits = input .ctx @@ -821,15 +1188,43 @@ pub(crate) fn build_function_analysis_artifact_from_analysis( interproc_summary_set: interproc_summary_set.clone(), diagnostics: writeback_diagnostics_from_plugin(diagnostics), }); + let symbolic_facts = symbolic_semantic_facts_from_sym(&semantic_artifact); + let mut type_facts = writeback.type_facts; + if symbolic_facts.diagnostics.branches_pruned > 0 { + type_facts.diagnostics.push(format!( + "symbolic pruned {} branch arm(s)", + symbolic_facts.diagnostics.branches_pruned + )); + } + type_facts.symbolic_facts = symbolic_facts; Some(FunctionAnalysisArtifact { ssa_func: analysis.ssa_func, pattern_ssa_func: analysis.pattern_ssa_func, - type_facts: writeback.type_facts, + type_facts, writeback_plan: writeback.plan, interproc_summary_set, + semantic_artifact: Some(semantic_artifact), }) } +pub(crate) fn build_function_analysis_artifact_from_analysis( + input: &FunctionInput<'_>, + analysis: FunctionAnalysis, + external_context_json: &str, + interproc_summary_set: Option, + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, +) -> Option { + let semantic_artifact = + collect_plugin_semantic_artifact(&analysis.ssa_func, input.ctx.arch, symbolic_scope); + build_function_analysis_artifact_from_analysis_with_semantic_artifact( + input, + analysis, + external_context_json, + interproc_summary_set, + semantic_artifact, + ) +} + #[allow(dead_code)] pub(crate) fn build_function_analysis_artifact( input: &FunctionInput<'_>, @@ -837,13 +1232,34 @@ pub(crate) fn build_function_analysis_artifact( interproc_scope_json: &str, interproc_max_iterations: usize, ) -> Option { - let cache_key = - function_artifact_cache_key(input, external_context_json, interproc_scope_json)?; - if let Some(cached) = artifact_cache() - .read() - .expect("plugin cache read lock poisoned") - .get(&cache_key) - .cloned() + build_function_analysis_artifact_with_scope( + input, + external_context_json, + interproc_scope_json, + interproc_max_iterations, + None, + ) +} + +pub(crate) fn build_function_analysis_artifact_with_scope( + input: &FunctionInput<'_>, + external_context_json: &str, + interproc_scope_json: &str, + interproc_max_iterations: usize, + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, +) -> Option { + let cache_key = function_artifact_cache_key( + input, + external_context_json, + interproc_scope_json, + symbolic_scope, + ); + if let Some(cache_key) = cache_key.as_ref() + && let Some(cached) = artifact_cache() + .read() + .expect("plugin cache read lock poisoned") + .get(cache_key) + .cloned() { return Some(rename_function_analysis_artifact( (*cached).clone(), @@ -863,19 +1279,24 @@ pub(crate) fn build_function_analysis_artifact( analysis, external_context_json, Some(interproc_summary_set), + symbolic_scope, )?; - cache_insert_bounded(artifact_cache(), cache_key, Arc::new(artifact.clone())); + if let Some(cache_key) = cache_key { + cache_insert_bounded(artifact_cache(), cache_key, Arc::new(artifact.clone())); + } Some(rename_function_analysis_artifact( artifact, &input.function_name, )) } -pub(crate) fn get_cached_function_analysis_artifact( +pub(crate) fn get_cached_function_analysis_artifact_with_scope( input: &FunctionInput<'_>, external_context_json: &str, + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, ) -> Option { - let cache_key = function_artifact_cache_key(input, external_context_json, "{}")?; + let cache_key = + function_artifact_cache_key(input, external_context_json, "{}", symbolic_scope)?; artifact_cache() .read() .expect("plugin cache read lock poisoned") @@ -891,11 +1312,13 @@ pub(crate) fn alias_cached_function_analysis_artifact( source_external_context_json: &str, target_external_context_json: &str, ) -> bool { - let Some(source_key) = function_artifact_cache_key(input, source_external_context_json, "{}") + let Some(source_key) = + function_artifact_cache_key(input, source_external_context_json, "{}", None) else { return false; }; - let Some(target_key) = function_artifact_cache_key(input, target_external_context_json, "{}") + let Some(target_key) = + function_artifact_cache_key(input, target_external_context_json, "{}", None) else { return false; }; @@ -911,6 +1334,7 @@ pub(crate) fn alias_cached_function_analysis_artifact( true } +#[allow(dead_code)] #[allow(clippy::too_many_arguments)] pub(crate) fn build_detached_function_analysis_artifact( blocks: &[R2ILBlock], @@ -920,6 +1344,29 @@ pub(crate) fn build_detached_function_analysis_artifact( semantic_metadata_enabled: bool, reg_type_hints: &std::collections::HashMap, external_context_json: &str, +) -> Option { + build_detached_function_analysis_artifact_with_scope( + blocks, + function_name, + arch, + ptr_bits, + semantic_metadata_enabled, + reg_type_hints, + external_context_json, + None, + ) +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn build_detached_function_analysis_artifact_with_scope( + blocks: &[R2ILBlock], + function_name: &str, + arch: Option<&ArchSpec>, + ptr_bits: u32, + semantic_metadata_enabled: bool, + reg_type_hints: &std::collections::HashMap, + external_context_json: &str, + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, ) -> Option { let cache_key = function_artifact_cache_key_parts( function_name, @@ -928,12 +1375,14 @@ pub(crate) fn build_detached_function_analysis_artifact( semantic_metadata_enabled, external_context_json, "{}", - )?; - if let Some(cached) = artifact_cache() - .read() - .expect("plugin cache read lock poisoned") - .get(&cache_key) - .cloned() + symbolic_scope, + ); + if let Some(cache_key) = cache_key.as_ref() + && let Some(cached) = artifact_cache() + .read() + .expect("plugin cache read lock poisoned") + .get(cache_key) + .cloned() { return Some(rename_function_analysis_artifact( (*cached).clone(), @@ -1088,14 +1537,28 @@ pub(crate) fn build_detached_function_analysis_artifact( interproc_summary_set: None, diagnostics: writeback_diagnostics_from_plugin(diagnostics), }); + let semantic_artifact = + collect_plugin_semantic_artifact(&analysis.ssa_func, arch, symbolic_scope); + let symbolic_facts = symbolic_semantic_facts_from_sym(&semantic_artifact); + let mut type_facts = writeback.type_facts; + if symbolic_facts.diagnostics.branches_pruned > 0 { + type_facts.diagnostics.push(format!( + "symbolic pruned {} branch arm(s)", + symbolic_facts.diagnostics.branches_pruned + )); + } + type_facts.symbolic_facts = symbolic_facts; let artifact = FunctionAnalysisArtifact { ssa_func: analysis.ssa_func, pattern_ssa_func: analysis.pattern_ssa_func, - type_facts: writeback.type_facts, + type_facts, writeback_plan: writeback.plan, interproc_summary_set: None, + semantic_artifact: Some(semantic_artifact), }; - cache_insert_bounded(artifact_cache(), cache_key, Arc::new(artifact.clone())); + if let Some(cache_key) = cache_key { + cache_insert_bounded(artifact_cache(), cache_key, Arc::new(artifact.clone())); + } Some(rename_function_analysis_artifact(artifact, function_name)) } @@ -3036,6 +3499,14 @@ mod tests { infer_signature_return_type, resolve_evidence_driven_type, }; + fn const_return_blocks(addr: u64, value: u64) -> Vec { + let mut block = r2il::R2ILBlock::new(addr, 4); + block.push(r2il::R2ILOp::Return { + target: r2il::Varnode::constant(value, 8), + }); + vec![block] + } + #[test] fn get_data_refs_resolves_const_add_chain_target() { let block = r2ssa::SSABlock { @@ -4119,4 +4590,106 @@ mod tests { assert_eq!(mips_args.len(), 4, "mips should expose a0..a3 args"); assert!(mips_args[0].1.contains(&"$a0")); } + + #[test] + fn function_artifact_cache_key_distinguishes_symbolic_scope() { + let arch = ArchSpec::new("x86-64"); + let root_blocks = const_return_blocks(0x1000, 0); + let helper_a_blocks = const_return_blocks(0x2000, 1); + let helper_b_blocks = const_return_blocks(0x2000, 2); + + let root_prepared = + r2ssa::SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic ssa"); + let helper_a_prepared = r2ssa::SsaArtifact::for_symbolic(&helper_a_blocks, Some(&arch)) + .expect("helper a symbolic ssa"); + let helper_b_prepared = r2ssa::SsaArtifact::for_symbolic(&helper_b_blocks, Some(&arch)) + .expect("helper b symbolic ssa"); + + let scope_a = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root_prepared.clone(), + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x2000), + name: Some("helper".to_string()), + prepared: helper_a_prepared, + }, + ], + ) + .expect("scope a"); + let scope_b = r2sym::PreparedFunctionScope::new( + 0x1000, + vec![ + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root_prepared, + }, + r2sym::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x2000), + name: Some("helper".to_string()), + prepared: helper_b_prepared, + }, + ], + ) + .expect("scope b"); + + let key_without_scope = function_artifact_cache_key_parts( + "root", + Some(&arch), + &root_blocks, + true, + "{}", + "{}", + None, + ) + .expect("root-only cache key"); + let key_a = function_artifact_cache_key_parts( + "root", + Some(&arch), + &root_blocks, + true, + "{}", + "{}", + Some(&scope_a), + ) + .expect("scoped cache key a"); + let key_a_repeat = function_artifact_cache_key_parts( + "root", + Some(&arch), + &root_blocks, + true, + "{}", + "{}", + Some(&scope_a), + ) + .expect("scoped cache key a repeat"); + let key_b = function_artifact_cache_key_parts( + "root", + Some(&arch), + &root_blocks, + true, + "{}", + "{}", + Some(&scope_b), + ) + .expect("scoped cache key b"); + + assert_eq!( + key_a, key_a_repeat, + "same symbolic scope should hash stably" + ); + assert_ne!( + key_without_scope, key_a, + "scope-aware artifacts must not alias the root-only cache key" + ); + assert_ne!( + key_a, key_b, + "different helper closures must not alias the same artifact cache entry" + ); + } } diff --git a/tests/e2e/stress_test.c b/tests/e2e/stress_test.c index 3f86d55..a6df51e 100644 --- a/tests/e2e/stress_test.c +++ b/tests/e2e/stress_test.c @@ -713,6 +713,53 @@ int interpret_bytecode(uint8_t *code, int len) { return acc; } +// Small interpreter loop intended to be step-summary friendly. +__attribute__((noinline)) +int tiny_vm_dispatch(uint8_t *code, int len) { + int ip = 0; + int acc = 0; + uint8_t dispatch_op = 0; + +dispatch: + if (ip >= len) { + return acc; + } + + dispatch_op = code[ip++]; + switch (dispatch_op) { + case 0x00: + acc += 1; + goto dispatch; + case 0x01: + acc += 2; + goto dispatch; + case 0x02: + acc -= 1; + goto dispatch; + case 0x03: + acc ^= 0x55; + goto dispatch; + case 0x04: + acc ^= 0xaa; + goto dispatch; + case 0x05: + acc <<= 1; + goto dispatch; + case 0x06: + acc >>= 1; + goto dispatch; + case 0x07: + if (acc == 0 && ip < len) { + ip += (int8_t)code[ip]; + } + goto dispatch; + case 0xff: + return acc; + default: + return -1; + } +} + // ============================================================================ // 16. Vulnerability patterns for taint analysis // ============================================================================ @@ -896,6 +943,7 @@ int main(int argc, char *argv[]) { printf(" 18: ht_insert + ht_lookup\n"); printf(" 19: vuln patterns\n"); printf(" 20: saturating_add\n"); + printf(" 21: tiny_vm_dispatch\n"); return 1; } @@ -1059,6 +1107,11 @@ int main(int argc, char *argv[]) { } break; } + case 21: { + uint8_t code[] = {0x00, 0x01, 0x03, 0x05, 0x06, 0x04, 0xff}; + printf("tiny_vm_dispatch result = %d\n", tiny_vm_dispatch(code, sizeof(code))); + break; + } default: printf("Unknown test: %d\n", test); return 1; diff --git a/tests/e2e/vuln_test.c b/tests/e2e/vuln_test.c index 067a281..0921d58 100644 --- a/tests/e2e/vuln_test.c +++ b/tests/e2e/vuln_test.c @@ -21,10 +21,13 @@ #include #include #include +#include volatile int global_counter = 0; volatile int global_limit = 10; +volatile int global_symbolic_guard = 0; volatile int global_tail = 0; +volatile unsigned long global_region_guard = 0x41UL; // Test 1: Simple password check (symbolic should find 0xDEAD) int check_secret(int x) { @@ -169,6 +172,69 @@ int test_boolxor(int a, int b) { return (a > 0) ^ (b > 0); } +// Test 16b: Volatile-backed opaque guard for symbolic branch pruning +int test_symbolic_xor_guard(int x) { + global_symbolic_guard = x; + if ((global_symbolic_guard ^ global_symbolic_guard) != 0) { + return 1; + } + return 0; +} + +__attribute__((noinline)) +static int helper_symbolic_zero(int x) { + global_symbolic_guard = x; + return global_symbolic_guard ^ global_symbolic_guard; +} + +// Test 16c: Helper-return opaque guard that requires helper-scope symbolic facts +int test_symbolic_helper_xor_guard(int x) { + if (helper_symbolic_zero(x) != 0) { + return 1; + } + return 0; +} + +__attribute__((noinline)) +static int helper_symbolic_zero_pure(int x) { + return x ^ x; +} + +// Test 16d: Pure helper-return opaque guard for live symbolic fact pruning +int test_symbolic_helper_pure_guard(int x) { + if (helper_symbolic_zero_pure(x) != 0) { + return 1; + } + return 0; +} + +// Test 16e: Global-backed symbolic guard for region-backed compiled predicates +int test_global_symbolic_eq_guard(int x) { + global_symbolic_guard = x; + if (global_symbolic_guard == 7) { + return 1; + } + return 0; +} + +// Test 16f: Stack-backed symbolic guard for region-backed compiled predicates +int test_stack_symbolic_eq_guard(int x) { + volatile int slot = x; + volatile int *ptr = &slot; + if (*ptr == 7) { + return 1; + } + return 0; +} + +// Test 16g: Global-backed constant guard for region-backed compiled predicates +int test_global_region_guard(void) { + if (global_region_guard == 0x41UL) { + return 1; + } + return 0; +} + // Test 17: Pointer add (array indexing) int test_array_index(int *arr, int idx) { return arr[idx]; @@ -494,6 +560,33 @@ __attribute__((noinline)) int stdin_gate(void) { return 0; } +// Test 45: Replay-focused stdin gate without tty-sensitive behavior. +__attribute__((noinline)) int stdin_gate_replay(void) { + char buf[2] = {0}; + struct termios old_termios; + struct termios raw_termios; + int has_termios = tcgetattr(0, &old_termios) == 0; + if (has_termios) { + raw_termios = old_termios; + raw_termios.c_lflag &= (tcflag_t) ~(ICANON | ECHO); + raw_termios.c_cc[VMIN] = 1; + raw_termios.c_cc[VTIME] = 0; + tcsetattr(0, TCSANOW, &raw_termios); + } + ssize_t n = read(0, buf, 1); + if (has_termios) { + tcsetattr(0, TCSANOW, &old_termios); + } + if (n != 1) { + return 0; + } + usleep(1000); + if (buf[0] == 'k') { + return 0x1337; + } + return 0; +} + int main(int argc, char *argv[]) { if (argc < 2) { printf("Usage: %s [args...]\n", argv[0]); @@ -843,6 +936,52 @@ int main(int argc, char *argv[]) { case 44: printf("stdin_gate() = %d\n", stdin_gate()); break; + case 45: + (void)stdin_gate_replay(); + break; + case 46: + if (argc > 2) { + int x = atoi(argv[2]); + printf( + "test_symbolic_helper_xor_guard(%d) = %d\n", + x, + test_symbolic_helper_xor_guard(x) + ); + } + break; + case 47: + if (argc > 2) { + int x = atoi(argv[2]); + printf( + "test_symbolic_helper_pure_guard(%d) = %d\n", + x, + test_symbolic_helper_pure_guard(x) + ); + } + break; + case 48: + if (argc > 2) { + int x = atoi(argv[2]); + printf( + "test_global_symbolic_eq_guard(%d) = %d\n", + x, + test_global_symbolic_eq_guard(x) + ); + } + break; + case 49: + if (argc > 2) { + int x = atoi(argv[2]); + printf( + "test_stack_symbolic_eq_guard(%d) = %d\n", + x, + test_stack_symbolic_eq_guard(x) + ); + } + break; + case 50: + printf("test_global_region_guard() = %d\n", test_global_region_guard()); + break; default: printf("Unknown test: %d\n", test); return 1; diff --git a/tests/r2r/db/extras/r2sleigh_decompiler_snapshots b/tests/r2r/db/extras/r2sleigh_decompiler_snapshots index 8cb5361..c86f5a2 100644 --- a/tests/r2r/db/extras/r2sleigh_decompiler_snapshots +++ b/tests/r2r/db/extras/r2sleigh_decompiler_snapshots @@ -2,7 +2,7 @@ NAME=decompiler_bool_carrier_chain_full_snapshot FILE=bins/vuln_test_x86 ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true EXPECT=<=8 and (.semantic.vm_step.loop_latches|length)>=7 and (.semantic.vm_step.step_blocks|length)>=10 and (.semantic.vm_step.handler_state_updates | to_entries | length) >= 5 and ((.semantic.vm_step.handler_state_updates | to_entries | map(.value | length) | add) >= 5)' +EOF_CMDS +RUN + +NAME=symbolic_tiny_vm_dispatch_reports_semantic_cache_hit_on_repeat +FILE=bins/stress_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=</dev/null +a:sla.sym sym.tiny_vm_dispatch | jq -c '.semantic.mode=="vm_summary" and .semantic.cache_hit==true and .semantic.vm_transfer != null and (.semantic.vm_transfer.transfers | type=="array")' +EOF_CMDS +RUN + +NAME=symbolic_interpret_bytecode_vm_summary_large_cfg +FILE=bins/stress_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=<=10' +EOF_CMDS +RUN + +NAME=types_tiny_vm_dispatch_reports_vm_step_payload +FILE=bins/stress_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=<=8 and (.symbolic.vm_step.loop_latches|length)>=7 and (.symbolic.vm_step.step_blocks|length)>=10 and (.symbolic.vm_step.case_values_by_target | keys | length) >= 8 and (.symbolic.vm_step.handler_state_updates | to_entries | length) >= 5' +EOF_CMDS +RUN + +NAME=types_tiny_vm_dispatch_reports_vm_transfer_payload +FILE=bins/stress_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=<=8 and (.symbolic.vm_transfer.loop_latches|length)>=7 and (.symbolic.vm_transfer.step_blocks|length)>=10 and (.symbolic.vm_transfer.handler_state_updates | to_entries | length) >= 5 and (.symbolic.vm_transfer.transfers | type=="array")' +EOF_CMDS +RUN + +NAME=symbolic_tiny_vm_dispatch_reports_vm_transfer_payload +FILE=bins/stress_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=<=8 and (.semantic.vm_transfer.loop_latches|length)>=7 and (.semantic.vm_transfer.step_blocks|length)>=10 and (.semantic.vm_transfer.transfers | type=="array")' +EOF_CMDS +RUN + +NAME=types_stack_symbolic_eq_guard_reports_stack_memory_term +FILE=bins/vuln_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=</dev/null +a:sym.replayj {"seed_checkpoint":2,"alphabet":"jk","max_depth":1,"beam_width":4,"frontier":[{"op":"eq","lhs":{"kind":"reg","name":"pc"},"rhs":{"kind":"const","value":"sym.stdin_gate_replay+0x10f"}}],"find":[{"op":"and","args":[{"op":"eq","lhs":{"kind":"reg","name":"pc"},"rhs":{"kind":"const","value":"sym.stdin_gate_replay+0x10f"}},{"op":"eq","lhs":{"kind":"reg","name":"rax"},"rhs":{"kind":"const","value":"0x1337"}}]}],"score":{"order":"max","expr":{"kind":"reg","name":"rax"}}} | head -n1 | jq -c '.seed_checkpoint==2 and .replay_fd==0 and .found==true and .explored_branches==2 and (.matches|length)==1 and .matches[0].input=="k" and .matches[0].score==4919 and (.matches[0].checkpoint>.seed_checkpoint) and .matches[0].hit==.matches[0].snapshot.pc and (.active|length)==1 and .active[0].input=="j" and .active[0].score==0' +EOF_CMDS +RUN + +NAME=sym_explore_replayj_usage +FILE=bins/vuln_test_x86 +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=< +EOF_EXPECT +CMDS=< +EOF_EXPECT +CMDS=</dev/null +a:sym.explore.replayj 0x40132a {"checkpoint":2,"entry":"sym.check_secret","symbolic_registers":["rdi"]} | head -n1 | jq -c '.target=="0x40132a" and .matched_paths>0 and (.paths|length)>0 and any(.paths[]; .final_pc=="0x40132a" and ((.solution.inputs.replay_rdi // .solution.inputs.EDI_0)=="0xdead"))' +EOF_CMDS +RUN + +NAME=sym_solve_replayj_check_secret_rdi_overlay_finds_0xdead +FILE=bins/vuln_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=</dev/null +a:sym.solve.replayj 0x40132a {"checkpoint":2,"entry":"sym.check_secret","symbolic_registers":["rdi"]} | head -n1 | jq -c '.found==true and .matched_paths>0 and .target=="0x40132a" and (.selected_path|has("solution")) and .selected_path.final_pc=="0x40132a" and ((.selected_path.solution.inputs.replay_rdi // .selected_path.solution.inputs.EDI_0)=="0xdead")' +EOF_CMDS +RUN + NAME=paths_check_secret_structure FILE=bins/vuln_test_x86 ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true @@ -471,6 +683,20 @@ a:sym.solve `afl~check_secret$[0]`+0x14 | jq -c '.found==true' EOF_CMDS RUN +NAME=sim_solve_materializes_deleted_current_function +FILE=bins/vuln_test_x86 +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=</dev/null +a:sym.solve `is~check_secret[2]`+0x14 | jq -c '.found==true' +EOF_CMDS +RUN + NAME=interactive_sym_runj_finds_check_secret_target FILE=bins/vuln_test_x86 ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true @@ -759,6 +985,134 @@ a:sla.dec entry0 | jq -Rs '(contains(" = eax ^ eax;")|not) and (contains(" = rax EOF_CMDS RUN +NAME=types_symbolic_xor_guard_prunes_dead_branch +FILE=bins/vuln_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=</dev/null +a:sla.types | jq -c '.symbolic.diagnostics.branches_pruned == 1 and ((.symbolic.branches | length) == 1) and .symbolic.branches[0].false_status == "unreachable"' +EOF_CMDS +RUN + +NAME=types_symbolic_xor_guard_materializes_deleted_function_by_addr +FILE=bins/vuln_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=</dev/null +a:sla.types | jq -c '.symbolic.diagnostics.branches_pruned == 1 and ((.symbolic.branches | length) == 1) and .symbolic.branches[0].true_status == "reachable" and .symbolic.branches[0].false_status == "unreachable" and ((((.interproc.scope.payloads // []) | map(.function_name) | any(. == "dbg.helper_symbolic_zero_pure")) or (((.interproc.scope.seeds // []) | map(.name) | any(. == "sym.helper_symbolic_zero_pure")))))' +EOF_CMDS +RUN + +NAME=types_symbolic_helper_scope_materializes_deleted_root_and_helper +FILE=bins/vuln_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=</dev/null +a:sla.types sym.test_symbolic_helper_pure_guard | jq -c '.symbolic.diagnostics.semantic_mode == "Compiled" and (.symbolic.diagnostics.derived_summaries >= 1) and (.symbolic.diagnostics.helper_functions >= 1)' +EOF_CMDS +RUN + +NAME=symbolic_helper_scope_reports_compiled_semantics +FILE=bins/vuln_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=</dev/null +a:sla.sym sym.test_symbolic_helper_pure_guard | jq -c '.semantic.mode == "compiled" and (.semantic.derived_summaries >= 1) and (.semantic.helper_functions >= 1)' +EOF_CMDS +RUN + +NAME=dec_symbolic_xor_guard_prunes_dead_branch +FILE=bins/vuln_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=< Date: Fri, 27 Mar 2026 19:55:00 +0000 Subject: [PATCH 04/10] Enhance symbolic execution framework with new VM operations - Introduced `VmUnaryOp` and `VmBinaryOp` enums to represent unary and binary operations in the symbolic execution context. - Updated `VmValueExpr` to support new operation types, including rendering logic for unary and binary expressions. - Enhanced the `evaluate_u64` method to handle new binary operations, improving the evaluation capabilities of symbolic expressions. - Refactored related code to integrate new operations into the existing symbolic execution framework. This commit expands the symbolic execution capabilities, allowing for more complex expressions and operations to be evaluated during analysis. --- crates/r2dec/src/lib.rs | 32 ++- crates/r2sym/src/backward.rs | 34 ++- crates/r2sym/src/lib.rs | 4 +- crates/r2sym/src/memory.rs | 15 +- crates/r2sym/src/query.rs | 216 +++++++++++++++- crates/r2sym/src/semantics/artifact.rs | 1 + crates/r2sym/src/semantics/cache.rs | 218 +++++++++++++++- crates/r2sym/src/semantics/compiler.rs | 86 ++++++- crates/r2sym/src/semantics/mod.rs | 4 +- crates/r2sym/src/semantics/vm.rs | 240 +++++++++++++++++- crates/r2sym/tests/symex_integration.rs | 69 +++++ crates/r2types/src/facts.rs | 120 +++++++++ crates/r2types/src/lib.rs | 6 +- r2plugin/src/analysis/sym.rs | 17 +- r2plugin/src/decompiler.rs | 18 +- r2plugin/src/lib.rs | 30 ++- r2plugin/src/types.rs | 74 +++++- .../db/extras/r2sleigh_integration_extended | 37 +++ 18 files changed, 1155 insertions(+), 66 deletions(-) diff --git a/crates/r2dec/src/lib.rs b/crates/r2dec/src/lib.rs index 3e21ba6..05993b6 100644 --- a/crates/r2dec/src/lib.rs +++ b/crates/r2dec/src/lib.rs @@ -196,7 +196,7 @@ fn format_vm_state_updates(updates: &[r2types::SymbolicVmStateUpdate]) -> String } let rendered = updates .iter() - .map(|update| format!("{}={}", update.output, update.expr)) + .map(|update| format!("{}={}", update.output, update.value.render())) .collect::>() .join(", "); format!("[{rendered}]") @@ -704,12 +704,34 @@ impl Decompiler { .vm_step .as_ref() .or(self.context.type_facts.symbolic_facts.vm_transfer.as_ref())?; + let exact_transfers = vm_step + .transfers + .iter() + .filter(|transfer| transfer.exact) + .count(); + let redispatch_transfers = vm_step + .transfers + .iter() + .filter(|transfer| transfer.redispatch) + .count(); + let returning_transfers = vm_step + .transfers + .iter() + .filter(|transfer| transfer.may_return) + .count(); + let selector_updates = vm_step + .transfers + .iter() + .filter(|transfer| transfer.selector_update.is_some()) + .count(); + let total_reads: usize = vm_step.handler_memory_reads.values().copied().sum(); + let total_writes: usize = vm_step.handler_memory_writes.values().copied().sum(); let mut out = String::new(); let _ = writeln!(&mut out, "r2dec semantic summary: vm_summary"); let _ = writeln!( &mut out, - "kind={} dispatch_header=0x{:x} loop_header=0x{:x} selector={} targets={} default_target={} latches={} step_blocks={} transfers={}", + "kind={} dispatch_header=0x{:x} loop_header=0x{:x} selector={} targets={} default_target={} latches={} step_blocks={} transfers={} exact_transfers={} redispatch_transfers={} returning_transfers={} selector_updates={} total_reads={} total_writes={}", format_vm_summary_kind(vm_step.kind), vm_step.dispatch_header, vm_step.loop_header, @@ -722,6 +744,12 @@ impl Decompiler { vm_step.loop_latches.len(), vm_step.step_blocks.len(), vm_step.transfers.len(), + exact_transfers, + redispatch_transfers, + returning_transfers, + selector_updates, + total_reads, + total_writes, ); if !vm_step.state_inputs.is_empty() || !vm_step.state_outputs.is_empty() { let _ = writeln!( diff --git a/crates/r2sym/src/backward.rs b/crates/r2sym/src/backward.rs index f764efc..2ea9dd6 100644 --- a/crates/r2sym/src/backward.rs +++ b/crates/r2sym/src/backward.rs @@ -253,7 +253,7 @@ impl<'a, 'ctx> ValueTranslator<'a, 'ctx> { use r2ssa::SSAOp::*; match op { - Copy { src, .. } => self.eval_ssa_var(src), + Copy { src, .. } | Cast { src, .. } => self.eval_ssa_var(src), Load { dst, addr, .. } => { if let Some(local_value) = self.local_memory_value(inst_id, dst.size) { return Ok(local_value); @@ -393,16 +393,26 @@ impl<'a, 'ctx> ValueTranslator<'a, 'ctx> { return Some(context); } } - let predecessor = self.phi_predecessors.get(&block_addr).copied()?; - let predecessor_block = self.func.get_block(predecessor)?; - for scan_idx in (0..predecessor_block.ops.len()).rev() { - let scan_inst = self - .func - .graph() - .inst_id_for_op_site(predecessor, scan_idx)?; - let call_id = self.func.call_sites().by_inst.get(&scan_inst).copied()?; - if let Some(context) = self.call_contexts.get(&call_id) { - return Some(context); + let predecessors = + if let Some(predecessor) = self.phi_predecessors.get(&block_addr).copied() { + vec![predecessor] + } else { + let preds = self.func.predecessors(block_addr); + if preds.len() == 1 { preds } else { Vec::new() } + }; + for predecessor in predecessors { + let predecessor_block = self.func.get_block(predecessor)?; + for scan_idx in (0..predecessor_block.ops.len()).rev() { + let scan_inst = self + .func + .graph() + .inst_id_for_op_site(predecessor, scan_idx)?; + let Some(call_id) = self.func.call_sites().by_inst.get(&scan_inst).copied() else { + continue; + }; + if let Some(context) = self.call_contexts.get(&call_id) { + return Some(context); + } } } None @@ -671,7 +681,7 @@ impl<'a, 'ctx> ValueTranslator<'a, 'ctx> { }; use r2ssa::SSAOp::*; match op { - Copy { src, .. } => self.normalized_memory_locations(src), + Copy { src, .. } | Cast { src, .. } => self.normalized_memory_locations(src), IntAdd { a, b, .. } => { if let Some(base) = self.normalized_memory_locations(a) && let Some(offsets) = self.resolve_delta_offsets(b, 1) diff --git a/crates/r2sym/src/lib.rs b/crates/r2sym/src/lib.rs index db9b87e..29c18c6 100644 --- a/crates/r2sym/src/lib.rs +++ b/crates/r2sym/src/lib.rs @@ -74,8 +74,8 @@ pub use semantics::{ CompiledFunctionSemantics, CompiledSemanticArtifact, CompiledSemanticMode, InterpreterDispatchSummary, InterpreterKind, ResidualReason, SemanticCapability, SemanticMode, SliceClass, SymbolicBranchFact, SymbolicFunctionFactDiagnostics, SymbolicFunctionFacts, - SymbolicReachabilityStatus, VmStateUpdate, VmStepSummary, VmTransferArm, VmValueExpr, - collect_symbolic_function_facts, collect_symbolic_function_facts_with_scope, + SymbolicReachabilityStatus, VmBinaryOp, VmStateUpdate, VmStepSummary, VmTransferArm, VmUnaryOp, + VmValueExpr, collect_symbolic_function_facts, collect_symbolic_function_facts_with_scope, compile_function_semantics_with_scope, compile_semantic_artifact_with_scope, stable_scope_hash, }; pub use sim::{ diff --git a/crates/r2sym/src/memory.rs b/crates/r2sym/src/memory.rs index fae61fc..63e252f 100644 --- a/crates/r2sym/src/memory.rs +++ b/crates/r2sym/src/memory.rs @@ -168,12 +168,16 @@ impl<'ctx> MemoryRegion<'ctx> { } } + let mut concrete_bytes = Vec::with_capacity(size as usize); let mut value = 0u64; let mut all_concrete = true; for index in 0..size { let byte_offset = offset.wrapping_add(index as u64); if let Some(byte) = self.concrete.get(&byte_offset) { - value |= (*byte as u64) << (index * 8); + concrete_bytes.push(*byte); + if index < 8 { + value |= (*byte as u64) << (index * 8); + } } else { all_concrete = false; break; @@ -181,6 +185,15 @@ impl<'ctx> MemoryRegion<'ctx> { } if all_concrete { + if size > 8 { + let mut bytes = concrete_bytes.iter().rev(); + let first = bytes.next().copied().unwrap_or(0); + let mut ast = BV::from_u64(first as u64, 8); + for byte in bytes { + ast = ast.concat(BV::from_u64(*byte as u64, 8)); + } + return SymValue::symbolic(ast, size * 8); + } return SymValue::concrete(value, size * 8); } diff --git a/crates/r2sym/src/query.rs b/crates/r2sym/src/query.rs index 775b7a9..3728ed9 100644 --- a/crates/r2sym/src/query.rs +++ b/crates/r2sym/src/query.rs @@ -4,6 +4,8 @@ //! analysis-oriented queries that can be consumed by the plugin or other //! analysis layers without exposing command-shaped policy. +use std::collections::{BTreeMap, BTreeSet, VecDeque}; + use z3::Context; use z3::ast::Ast; @@ -20,6 +22,8 @@ use crate::sim::SummaryProfile; use crate::solver::{SatResult, SolverStats}; use crate::state::ExitStatus; +const MAX_VM_COMPILED_STEPS: usize = 8; + /// Query execution strategy. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum QueryMode { @@ -238,30 +242,92 @@ fn vm_arm_for_case(vm_step: &VmStepSummary, case_value: u64) -> Option<&crate::V .find(|arm| arm.case_values.contains(&case_value)) } -fn vm_target_case_values(vm_step: &VmStepSummary, target_addr: u64) -> Vec { - let mut case_values = std::collections::BTreeSet::new(); +fn vm_selector_bindings(vm_step: &VmStepSummary, case_value: u64) -> BTreeMap { + let mut bindings = BTreeMap::new(); + if let Some(selector) = vm_step.selector.as_ref() { + bindings.insert(selector.clone(), case_value); + } + bindings +} - for arm in &vm_step.transfers { - if vm_arm_reaches_target(arm, target_addr) { - case_values.extend(arm.case_values.iter().copied()); +fn vm_apply_exact_state_updates( + bindings: &BTreeMap, + updates: &[crate::VmStateUpdate], +) -> BTreeMap { + let mut next = bindings.clone(); + for update in updates { + if !update.exact { + continue; + } + if let Some(value) = update.value.evaluate_u64(&next) { + next.insert(update.output.clone(), value); } } + next +} - for arm in &vm_step.transfers { - if !arm.redispatch || arm.case_values.is_empty() { +fn vm_next_case_value( + bindings: &BTreeMap, + arm: &crate::VmTransferArm, + selector_name: Option<&str>, +) -> Option<(u64, BTreeMap)> { + if !arm.redispatch || arm.truncated { + return None; + } + let mut next_bindings = vm_apply_exact_state_updates(bindings, &arm.state_updates); + let selector_update = arm.selector_update.as_ref()?; + if !selector_update.exact { + return None; + } + let next_case = selector_update.value.evaluate_u64(&next_bindings)?; + next_bindings.insert(selector_update.output.clone(), next_case); + if let Some(selector_name) = selector_name { + next_bindings.insert(selector_name.to_string(), next_case); + } + Some((next_case, next_bindings)) +} + +fn vm_case_reaches_target(vm_step: &VmStepSummary, initial_case: u64, target_addr: u64) -> bool { + let initial_bindings = vm_selector_bindings(vm_step, initial_case); + let mut queue = VecDeque::from([(initial_case, initial_bindings, 0usize)]); + let mut seen = BTreeSet::new(); + + while let Some((case_value, bindings, depth)) = queue.pop_front() { + let binding_key = bindings + .iter() + .map(|(name, value)| (name.clone(), *value)) + .collect::>(); + if !seen.insert((case_value, binding_key)) { continue; } - let Some(selector_update) = arm.selector_update.as_ref() else { + let Some(arm) = vm_arm_for_case(vm_step, case_value) else { continue; }; - let crate::VmValueExpr::Const(next_case) = &selector_update.value else { + if vm_arm_reaches_target(arm, target_addr) { + return true; + } + if depth >= MAX_VM_COMPILED_STEPS.saturating_sub(1) || !arm.redispatch { continue; - }; - let Some(next_arm) = vm_arm_for_case(vm_step, *next_case) else { + } + let Some((next_case, next_bindings)) = + vm_next_case_value(&bindings, arm, vm_step.selector.as_deref()) + else { continue; }; - if vm_arm_reaches_target(next_arm, target_addr) { - case_values.extend(arm.case_values.iter().copied()); + queue.push_back((next_case, next_bindings, depth + 1)); + } + + false +} + +fn vm_target_case_values(vm_step: &VmStepSummary, target_addr: u64) -> Vec { + let mut case_values = BTreeSet::new(); + + for arm in &vm_step.transfers { + for case_value in arm.case_values.iter().copied() { + if vm_case_reaches_target(vm_step, case_value, target_addr) { + case_values.insert(case_value); + } } } @@ -558,7 +624,7 @@ mod tests { vm_target_case_values, }; use crate::{ - BackwardConditionPrecision, BackwardConditionSummary, SymQueryConfig, SymState, + BackwardConditionPrecision, BackwardConditionSummary, SymQueryConfig, SymState, VmBinaryOp, VmStateUpdate, VmStepSummary, VmTransferArm, VmValueExpr, }; use r2il::{R2ILBlock, R2ILOp, SpaceId, Varnode}; @@ -747,6 +813,128 @@ mod tests { assert_eq!(vm_target_case_values(&vm_step, 0x1014), vec![0, 1]); } + #[test] + fn vm_target_case_values_follow_exact_state_updates_across_redispatch() { + let vm_step = make_vm_step_summary_with_transfers(vec![ + VmTransferArm { + handler_target: 0x1004, + case_values: vec![0], + region_blocks: vec![0x1004], + exit_targets: vec![0x1000], + state_updates: vec![VmStateUpdate { + output: "TMP_1".to_string(), + expr: "0x1".to_string(), + value: VmValueExpr::Const(1), + exact: true, + }], + selector_update: Some(VmStateUpdate { + output: "RDI_1".to_string(), + expr: "TMP_1".to_string(), + value: VmValueExpr::Var("TMP_1".to_string()), + exact: true, + }), + exact: true, + redispatch: true, + may_return: false, + truncated: false, + }, + VmTransferArm { + handler_target: 0x1008, + case_values: vec![1], + region_blocks: vec![0x1008, 0x1014], + exit_targets: vec![0x1014], + state_updates: Vec::new(), + selector_update: None, + exact: true, + redispatch: false, + may_return: true, + truncated: false, + }, + ]); + + assert_eq!(vm_target_case_values(&vm_step, 0x1014), vec![0, 1]); + } + + #[test] + fn vm_target_case_values_follow_bounded_selector_algebra() { + let vm_step = make_vm_step_summary_with_transfers(vec![ + VmTransferArm { + handler_target: 0x1004, + case_values: vec![0], + region_blocks: vec![0x1004], + exit_targets: vec![0x1000], + state_updates: vec![VmStateUpdate { + output: "RDI_1".to_string(), + expr: "(RDI_0 + 0x1)".to_string(), + value: VmValueExpr::Binary { + op: VmBinaryOp::Add, + lhs: Box::new(VmValueExpr::Var("RDI_0".to_string())), + rhs: Box::new(VmValueExpr::Const(1)), + }, + exact: true, + }], + selector_update: Some(VmStateUpdate { + output: "RDI_1".to_string(), + expr: "(RDI_0 + 0x1)".to_string(), + value: VmValueExpr::Binary { + op: VmBinaryOp::Add, + lhs: Box::new(VmValueExpr::Var("RDI_0".to_string())), + rhs: Box::new(VmValueExpr::Const(1)), + }, + exact: true, + }), + exact: true, + redispatch: true, + may_return: false, + truncated: false, + }, + VmTransferArm { + handler_target: 0x1008, + case_values: vec![1], + region_blocks: vec![0x1008], + exit_targets: vec![0x1000], + state_updates: vec![VmStateUpdate { + output: "RDI_1".to_string(), + expr: "(RDI_0 + 0x1)".to_string(), + value: VmValueExpr::Binary { + op: VmBinaryOp::Add, + lhs: Box::new(VmValueExpr::Var("RDI_0".to_string())), + rhs: Box::new(VmValueExpr::Const(1)), + }, + exact: true, + }], + selector_update: Some(VmStateUpdate { + output: "RDI_1".to_string(), + expr: "(RDI_0 + 0x1)".to_string(), + value: VmValueExpr::Binary { + op: VmBinaryOp::Add, + lhs: Box::new(VmValueExpr::Var("RDI_0".to_string())), + rhs: Box::new(VmValueExpr::Const(1)), + }, + exact: true, + }), + exact: true, + redispatch: true, + may_return: false, + truncated: false, + }, + VmTransferArm { + handler_target: 0x1010, + case_values: vec![2], + region_blocks: vec![0x1010, 0x1014], + exit_targets: vec![0x1014], + state_updates: Vec::new(), + selector_update: None, + exact: true, + redispatch: false, + may_return: true, + truncated: false, + }, + ]); + + assert_eq!(vm_target_case_values(&vm_step, 0x1014), vec![0, 1, 2]); + } + #[test] fn exact_compiled_preconditions_prefer_fewer_residual_fallbacks() { let current = crate::CompiledBackwardCondition { diff --git a/crates/r2sym/src/semantics/artifact.rs b/crates/r2sym/src/semantics/artifact.rs index ee32217..27839d8 100644 --- a/crates/r2sym/src/semantics/artifact.rs +++ b/crates/r2sym/src/semantics/artifact.rs @@ -54,6 +54,7 @@ pub struct CompiledSemanticArtifact { pub symbolic_facts: SymbolicFunctionFacts, pub interpreter: Option, pub vm_step: Option, + pub vm_transfer: Option, pub cache_hit: bool, } diff --git a/crates/r2sym/src/semantics/cache.rs b/crates/r2sym/src/semantics/cache.rs index 5cae7dd..7b25309 100644 --- a/crates/r2sym/src/semantics/cache.rs +++ b/crates/r2sym/src/semantics/cache.rs @@ -4,7 +4,7 @@ use std::hash::{DefaultHasher, Hash, Hasher}; use std::sync::{Arc, OnceLock, RwLock}; use r2il::ArchSpec; -use r2ssa::SsaArtifact; +use r2ssa::{SSAOp, SsaArtifact}; use crate::sim::{PreparedFunctionScope, SummaryProfile}; @@ -19,6 +19,12 @@ pub(crate) enum SemanticSeedMode { Replay, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) enum SemanticCacheScopeKind { + Exact, + CoarseLargeSlice, +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub(crate) struct SemanticCacheKey { pub root_addr: u64, @@ -26,6 +32,7 @@ pub(crate) struct SemanticCacheKey { pub arch_hash: u64, pub summary_profile: SummaryProfile, pub seed_mode: SemanticSeedMode, + pub scope_kind: SemanticCacheScopeKind, } #[derive(Debug, Clone)] @@ -53,10 +60,64 @@ fn arch_hash(arch: Option<&ArchSpec>) -> u64 { arch.map(hash_debug_value).unwrap_or(0) } +fn strip_ssa_version_suffix(name: &str) -> &str { + name.rsplit_once('_') + .filter(|(_, version)| !version.is_empty() && version.chars().all(|ch| ch.is_ascii_digit())) + .map(|(base, _)| base) + .unwrap_or(name) +} + +fn hash_ssa_var_shape(hasher: &mut H, var: &r2ssa::SSAVar) { + strip_ssa_version_suffix(&var.name).hash(hasher); + var.size.hash(hasher); +} + +fn hash_ssa_op_shape(hasher: &mut H, op: &SSAOp) { + std::mem::discriminant(op).hash(hasher); + if let Some(dst) = op.dst() { + hash_ssa_var_shape(hasher, dst); + } + let sources = op.sources(); + sources.len().hash(hasher); + for src in sources { + hash_ssa_var_shape(hasher, src); + } + + match op { + SSAOp::Subpiece { offset, .. } => offset.hash(hasher), + SSAOp::PtrAdd { element_size, .. } | SSAOp::PtrSub { element_size, .. } => { + element_size.hash(hasher); + } + SSAOp::Load { space, .. } + | SSAOp::Store { space, .. } + | SSAOp::LoadLinked { space, .. } + | SSAOp::StoreConditional { space, .. } + | SSAOp::AtomicCAS { space, .. } + | SSAOp::LoadGuarded { space, .. } + | SSAOp::StoreGuarded { space, .. } => { + space.hash(hasher); + } + _ => {} + } +} + +fn ssa_shape_hash(func: &SsaArtifact) -> u64 { + let mut hasher = DefaultHasher::new(); + for block in func.local_ssa_blocks() { + block.addr.hash(&mut hasher); + block.size.hash(&mut hasher); + block.ops.len().hash(&mut hasher); + for op in &block.ops { + hash_ssa_op_shape(&mut hasher, op); + } + } + hasher.finish() +} + fn function_hash(func: &SsaArtifact) -> u64 { let mut hasher = DefaultHasher::new(); func.entry.hash(&mut hasher); - hash_debug_value(&func.local_ssa_blocks()).hash(&mut hasher); + ssa_shape_hash(func).hash(&mut hasher); hasher.finish() } @@ -68,9 +129,8 @@ pub fn stable_scope_hash(scope: Option<&PreparedFunctionScope>) -> u64 { scope.root_id().hash(&mut hasher); for function in scope.functions().values() { function.id.hash(&mut hasher); - function.name.hash(&mut hasher); function.prepared.function().entry.hash(&mut hasher); - hash_debug_value(&function.prepared.local_ssa_blocks()).hash(&mut hasher); + ssa_shape_hash(&function.prepared).hash(&mut hasher); } hasher.finish() } @@ -92,6 +152,23 @@ pub(crate) fn semantic_cache_key( arch_hash: arch_hash(arch), summary_profile, seed_mode, + scope_kind: SemanticCacheScopeKind::Exact, + } +} + +pub(crate) fn coarse_large_slice_cache_key( + root_addr: u64, + arch: Option<&ArchSpec>, + summary_profile: SummaryProfile, + seed_mode: SemanticSeedMode, +) -> SemanticCacheKey { + SemanticCacheKey { + root_addr, + scope_hash: 0, + arch_hash: arch_hash(arch), + summary_profile, + seed_mode, + scope_kind: SemanticCacheScopeKind::CoarseLargeSlice, } } @@ -124,3 +201,136 @@ pub(crate) fn cache_insert_bounded( guard.insert(key, value.clone()); value } + +#[cfg(test)] +mod display_name_tests { + use r2il::{ArchSpec, R2ILBlock, R2ILOp, RegisterDef, SpaceId, Varnode}; + use r2ssa::{InterprocFunctionId, SsaArtifact}; + + use crate::sim::{PreparedFunctionScope, ScopedPreparedFunction}; + + use super::stable_scope_hash; + + const RAX: u64 = 0; + + fn test_arch() -> ArchSpec { + let mut arch = ArchSpec::new("x86-64"); + arch.addr_size = 8; + arch.add_register(RegisterDef::new("RAX", RAX, 8)); + arch + } + + fn make_const(value: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Const, + offset: value, + size, + meta: None, + } + } + + fn simple_function(entry: u64) -> SsaArtifact { + let blocks = vec![R2ILBlock { + addr: entry, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }]; + SsaArtifact::for_symbolic(&blocks, Some(&test_arch())).expect("ssa") + } + + #[test] + fn stable_scope_hash_ignores_non_semantic_names() { + let root = ScopedPreparedFunction { + id: InterprocFunctionId(0x1000), + name: Some("sym.root".to_string()), + prepared: simple_function(0x1000), + }; + let helper_named = ScopedPreparedFunction { + id: InterprocFunctionId(0x2000), + name: Some("dbg.helper".to_string()), + prepared: simple_function(0x2000), + }; + let helper_renamed = ScopedPreparedFunction { + id: InterprocFunctionId(0x2000), + name: Some("fcn.2000".to_string()), + prepared: simple_function(0x2000), + }; + + let left = + PreparedFunctionScope::new(0x1000, vec![root.clone(), helper_named]).expect("scope"); + let right = PreparedFunctionScope::new(0x1000, vec![root, helper_renamed]).expect("scope"); + + assert_eq!( + stable_scope_hash(Some(&left)), + stable_scope_hash(Some(&right)) + ); + } +} + +#[cfg(test)] +mod tests { + use r2il::{R2ILBlock, R2ILOp, SpaceId, Varnode}; + use r2ssa::{InterprocFunctionId, SsaArtifact}; + + use crate::sim::{PreparedFunctionScope, ScopedPreparedFunction}; + + use super::stable_scope_hash; + + fn make_const(value: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Const, + offset: value, + size, + meta: None, + } + } + + fn make_leaf(entry: u64) -> SsaArtifact { + SsaArtifact::for_symbolic( + &[R2ILBlock { + addr: entry, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }], + None, + ) + .expect("ssa") + } + + fn make_scope(root_name: &str, helper_name: &str) -> PreparedFunctionScope { + PreparedFunctionScope::new( + 0x1000, + vec![ + ScopedPreparedFunction { + id: InterprocFunctionId(0x1000), + name: Some(root_name.to_string()), + prepared: make_leaf(0x1000).with_name(root_name), + }, + ScopedPreparedFunction { + id: InterprocFunctionId(0x2000), + name: Some(helper_name.to_string()), + prepared: make_leaf(0x2000).with_name(helper_name), + }, + ], + ) + .expect("scope") + } + + #[test] + fn stable_scope_hash_ignores_display_name_drift() { + let left = make_scope("sym.worker", "sym.helper"); + let right = make_scope("dbg.worker", "fcn.2000"); + assert_eq!( + stable_scope_hash(Some(&left)), + stable_scope_hash(Some(&right)) + ); + } +} diff --git a/crates/r2sym/src/semantics/compiler.rs b/crates/r2sym/src/semantics/compiler.rs index 075df12..609a3f0 100644 --- a/crates/r2sym/src/semantics/compiler.rs +++ b/crates/r2sym/src/semantics/compiler.rs @@ -14,8 +14,8 @@ use super::artifact::{ SemanticMode, }; use super::cache::{ - SemanticCompilationResult, SemanticSeedMode, cache_insert_bounded, lookup_semantic_cache, - semantic_cache_key, + SemanticCompilationResult, SemanticSeedMode, cache_insert_bounded, + coarse_large_slice_cache_key, lookup_semantic_cache, semantic_cache_key, }; use super::classify::classify_slice; use super::facts::{ @@ -92,7 +92,7 @@ fn semantic_capability(mode: SemanticMode, facts: &SymbolicFunctionFacts) -> Sem decompile_ready: false, }, SemanticMode::VmSummary => SemanticCapability { - query_ready: !facts.diagnostics.skipped_large_cfg, + query_ready: true, type_ready: true, decompile_ready: false, }, @@ -155,13 +155,14 @@ fn compile_function_semantics_uncached( let vm_step = interpreter .as_ref() .and_then(|dispatch| build_vm_step_summary(func, dispatch)); + let vm_transfer = vm_step.clone(); let mode = semantic_mode_for( helper_functions, derived_summaries, &derived_diagnostics, &symbolic_facts, interpreter.is_some(), - vm_step.is_some(), + vm_transfer.is_some(), ); let slice_class = classify_slice( func, @@ -187,6 +188,7 @@ fn compile_function_semantics_uncached( symbolic_facts, interpreter, vm_step, + vm_transfer, cache_hit: false, } } @@ -206,6 +208,17 @@ pub(crate) fn compile_function_semantics_cached_with_scope( cache_hit: true, }; } + let coarse_key = scope.map(|_| { + coarse_large_slice_cache_key(func.entry, arch, summary_profile, SemanticSeedMode::Static) + }); + if let Some(coarse_key) = coarse_key.as_ref() + && let Some(existing) = lookup_semantic_cache(coarse_key) + { + return SemanticCompilationResult { + artifact: existing, + cache_hit: true, + }; + } let artifact = Arc::new(compile_function_semantics_uncached( ctx, @@ -216,6 +229,16 @@ pub(crate) fn compile_function_semantics_cached_with_scope( summary_profile, )); let artifact = cache_insert_bounded(key, artifact); + let should_cache_coarsely = scope.is_some() + && (matches!(artifact.mode, SemanticMode::VmSummary) + || artifact.symbolic_facts.diagnostics.skipped_large_cfg); + let artifact = if should_cache_coarsely { + let coarse_key = + coarse_key.expect("coarse large-slice cache key should exist when scope is present"); + cache_insert_bounded(coarse_key, artifact) + } else { + artifact + }; SemanticCompilationResult { artifact, cache_hit: false, @@ -326,6 +349,59 @@ mod tests { assert!(second.cache_hit); } + #[test] + fn compile_semantics_cache_ignores_scope_display_names() { + let blocks = vec![R2ILBlock { + addr: 0x1000, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }]; + let func = SsaArtifact::for_symbolic(&blocks, Some(&test_arch())).expect("ssa"); + let scope_a = crate::PreparedFunctionScope::new( + 0x1000, + vec![crate::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("sym.display_a".to_string()), + prepared: func.clone(), + }], + ) + .expect("scope a"); + let scope_b = crate::PreparedFunctionScope::new( + 0x1000, + vec![crate::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("dbg.display_b".to_string()), + prepared: func.clone(), + }], + ) + .expect("scope b"); + let ctx = Context::thread_local(); + + let first = compile_function_semantics_cached_with_scope( + &ctx, + &func, + Some(&scope_a), + Some(&test_arch()), + &HashMap::new(), + SummaryProfile::Default, + ); + let second = compile_function_semantics_cached_with_scope( + &ctx, + &func, + Some(&scope_b), + Some(&test_arch()), + &HashMap::new(), + SummaryProfile::Default, + ); + + assert!(!first.cache_hit); + assert!(second.cache_hit); + } + #[test] fn interpreter_classifier_marks_switch_loop_vm_summary() { let blocks = vec![ @@ -460,6 +536,7 @@ mod tests { artifact.slice_class, super::super::artifact::SliceClass::InterpreterSwitch ); + assert!(artifact.capability.query_ready); assert!(artifact.interpreter.is_some()); let vm_step = artifact.vm_step.expect("vm step"); assert_eq!(vm_step.loop_header, 0x1004); @@ -725,6 +802,7 @@ mod tests { let vm_step = artifact.vm_step.expect("vm step summary"); assert_eq!(artifact.mode, SemanticMode::VmSummary); + assert!(artifact.capability.query_ready); assert_eq!(vm_step.loop_header, 0x3004); assert_eq!(vm_step.dispatch_header, 0x3004); assert_eq!(vm_step.default_target, Some(0x3018)); diff --git a/crates/r2sym/src/semantics/mod.rs b/crates/r2sym/src/semantics/mod.rs index 844b8c5..d704a0c 100644 --- a/crates/r2sym/src/semantics/mod.rs +++ b/crates/r2sym/src/semantics/mod.rs @@ -17,7 +17,7 @@ pub use facts::{ collect_symbolic_function_facts_with_scope, }; pub use vm::{ - InterpreterDispatchSummary, InterpreterKind, VmStateUpdate, VmStepSummary, VmTransferArm, - VmValueExpr, + InterpreterDispatchSummary, InterpreterKind, VmBinaryOp, VmStateUpdate, VmStepSummary, + VmTransferArm, VmUnaryOp, VmValueExpr, }; pub(crate) use vm::{build_vm_step_summary, classify_interpreter_like}; diff --git a/crates/r2sym/src/semantics/vm.rs b/crates/r2sym/src/semantics/vm.rs index 4ee2e38..e911bab 100644 --- a/crates/r2sym/src/semantics/vm.rs +++ b/crates/r2sym/src/semantics/vm.rs @@ -20,23 +20,154 @@ pub struct InterpreterDispatchSummary { pub score: i32, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum VmUnaryOp { + Neg, + BitNot, + BoolNot, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum VmBinaryOp { + Add, + Sub, + Mul, + Div, + Rem, + And, + Or, + Xor, + Shl, + LShr, + AShr, + Eq, + Ne, + Lt, + SLt, + Le, + SLe, + BoolAnd, + BoolOr, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum VmValueExpr { Const(u64), Var(String), + Unary { + op: VmUnaryOp, + arg: Box, + }, + Binary { + op: VmBinaryOp, + lhs: Box, + rhs: Box, + }, Expr(String), } impl VmValueExpr { + fn render_unary(op: &VmUnaryOp) -> &'static str { + match op { + VmUnaryOp::Neg => "-", + VmUnaryOp::BitNot => "~", + VmUnaryOp::BoolNot => "!", + } + } + + fn render_binary(op: &VmBinaryOp) -> &'static str { + match op { + VmBinaryOp::Add => "+", + VmBinaryOp::Sub => "-", + VmBinaryOp::Mul => "*", + VmBinaryOp::Div => "/", + VmBinaryOp::Rem => "%", + VmBinaryOp::And => "&", + VmBinaryOp::Or => "|", + VmBinaryOp::Xor => "^", + VmBinaryOp::Shl => "<<", + VmBinaryOp::LShr | VmBinaryOp::AShr => ">>", + VmBinaryOp::Eq => "==", + VmBinaryOp::Ne => "!=", + VmBinaryOp::Lt | VmBinaryOp::SLt => "<", + VmBinaryOp::Le | VmBinaryOp::SLe => "<=", + VmBinaryOp::BoolAnd => "&&", + VmBinaryOp::BoolOr => "||", + } + } + fn render(&self) -> String { match self { Self::Const(value) => format!("0x{value:x}"), Self::Var(name) | Self::Expr(name) => name.clone(), + Self::Unary { op, arg } => { + format!("({}{})", Self::render_unary(op), arg.render()) + } + Self::Binary { op, lhs, rhs } => format!( + "({} {} {})", + lhs.render(), + Self::render_binary(op), + rhs.render() + ), } } fn is_exact(&self) -> bool { - matches!(self, Self::Const(_) | Self::Var(_)) + match self { + Self::Const(_) | Self::Var(_) => true, + Self::Unary { arg, .. } => arg.is_exact(), + Self::Binary { lhs, rhs, .. } => lhs.is_exact() && rhs.is_exact(), + Self::Expr(_) => false, + } + } + + fn lookup_binding(bindings: &BTreeMap, name: &str) -> Option { + bindings.get(name).copied().or_else(|| { + bindings + .iter() + .find_map(|(candidate, value)| same_logical_name(candidate, name).then_some(*value)) + }) + } + + pub(crate) fn evaluate_u64(&self, bindings: &BTreeMap) -> Option { + match self { + Self::Const(value) => Some(*value), + Self::Var(name) => Self::lookup_binding(bindings, name), + Self::Unary { op, arg } => { + let value = arg.evaluate_u64(bindings)?; + Some(match op { + VmUnaryOp::Neg => value.wrapping_neg(), + VmUnaryOp::BitNot => !value, + VmUnaryOp::BoolNot => u64::from(value == 0), + }) + } + Self::Binary { op, lhs, rhs } => { + let lhs = lhs.evaluate_u64(bindings)?; + let rhs = rhs.evaluate_u64(bindings)?; + Some(match op { + VmBinaryOp::Add => lhs.wrapping_add(rhs), + VmBinaryOp::Sub => lhs.wrapping_sub(rhs), + VmBinaryOp::Mul => lhs.wrapping_mul(rhs), + VmBinaryOp::Div => lhs.checked_div(rhs)?, + VmBinaryOp::Rem => lhs.checked_rem(rhs)?, + VmBinaryOp::And => lhs & rhs, + VmBinaryOp::Or => lhs | rhs, + VmBinaryOp::Xor => lhs ^ rhs, + VmBinaryOp::Shl => lhs.wrapping_shl((rhs & 63) as u32), + VmBinaryOp::LShr => lhs.wrapping_shr((rhs & 63) as u32), + VmBinaryOp::AShr => ((lhs as i64) >> ((rhs & 63) as u32)) as u64, + VmBinaryOp::Eq => u64::from(lhs == rhs), + VmBinaryOp::Ne => u64::from(lhs != rhs), + VmBinaryOp::Lt => u64::from(lhs < rhs), + VmBinaryOp::SLt => u64::from((lhs as i64) < (rhs as i64)), + VmBinaryOp::Le => u64::from(lhs <= rhs), + VmBinaryOp::SLe => u64::from((lhs as i64) <= (rhs as i64)), + VmBinaryOp::BoolAnd => u64::from(lhs != 0 && rhs != 0), + VmBinaryOp::BoolOr => u64::from(lhs != 0 || rhs != 0), + }) + } + Self::Expr(_) => None, + } } } @@ -334,6 +465,113 @@ fn classify_vm_op_value(func: &SsaArtifact, op: &SSAOp, depth: u32) -> Option { classify_vm_var_value(func, src, depth + 1) } + IntAdd { a, b, .. } | FloatAdd { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::Add, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + IntSub { a, b, .. } | FloatSub { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::Sub, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + IntMult { a, b, .. } | FloatMult { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::Mul, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + IntDiv { a, b, .. } | IntSDiv { a, b, .. } | FloatDiv { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::Div, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + IntRem { a, b, .. } | IntSRem { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::Rem, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + IntAnd { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::And, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + IntOr { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::Or, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + IntXor { a, b, .. } | BoolXor { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::Xor, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + IntLeft { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::Shl, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + IntRight { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::LShr, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + IntSRight { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::AShr, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + IntEqual { a, b, .. } | FloatEqual { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::Eq, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + IntNotEqual { a, b, .. } | FloatNotEqual { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::Ne, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + IntLess { a, b, .. } | FloatLess { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::Lt, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + IntSLess { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::SLt, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + IntLessEqual { a, b, .. } | FloatLessEqual { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::Le, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + IntSLessEqual { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::SLe, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + BoolAnd { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::BoolAnd, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + BoolOr { a, b, .. } => VmValueExpr::Binary { + op: VmBinaryOp::BoolOr, + lhs: Box::new(classify_vm_var_value(func, a, depth + 1)), + rhs: Box::new(classify_vm_var_value(func, b, depth + 1)), + }, + IntNegate { src, .. } | FloatNeg { src, .. } => VmValueExpr::Unary { + op: VmUnaryOp::Neg, + arg: Box::new(classify_vm_var_value(func, src, depth + 1)), + }, + IntNot { src, .. } => VmValueExpr::Unary { + op: VmUnaryOp::BitNot, + arg: Box::new(classify_vm_var_value(func, src, depth + 1)), + }, + BoolNot { src, .. } => VmValueExpr::Unary { + op: VmUnaryOp::BoolNot, + arg: Box::new(classify_vm_var_value(func, src, depth + 1)), + }, _ => VmValueExpr::Expr(render_vm_op_expr(func, op, depth + 1)?), }) } diff --git a/crates/r2sym/tests/symex_integration.rs b/crates/r2sym/tests/symex_integration.rs index 292dd8a..dc940ba 100644 --- a/crates/r2sym/tests/symex_integration.rs +++ b/crates/r2sym/tests/symex_integration.rs @@ -3029,6 +3029,75 @@ fn derived_helper_summary_compiles_backward_memory_terms() { assert_argument_memory_term(&compiled.memory_terms[0], 0, 0, 0, true, 1); } +#[test] +fn derived_helper_summary_compiles_backward_memory_terms_through_cast_pointer() { + let arch = make_x86_64_arch(); + let cast_ptr = make_reg(0x90, 8); + let loaded = make_reg(0x98, 1); + let cond = make_reg(0xa0, 1); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Cast { + dst: cast_ptr.clone(), + src: make_reg(RDI, 8), + }, + R2ILOp::Load { + dst: loaded.clone(), + addr: cast_ptr, + space: SpaceId::Ram, + }, + R2ILOp::IntEqual { + dst: cond.clone(), + a: loaded, + b: make_const(0x41, 1), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond, + }, + ], + }, + R2ILBlock { + addr: 0x1008, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + R2ILBlock { + addr: 0x1010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + ]; + let root = + SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic function"); + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("reg:56_0", 64); + + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let conditions = explorer.path_conditions_at(&root, state, 0x1010); + let compiled = conditions + .compiled_precondition + .as_ref() + .expect("compiled precondition"); + assert_eq!(compiled.precision, BackwardConditionPrecision::Exact); + assert!(!compiled.memory_terms.is_empty()); + assert_argument_memory_term(&compiled.memory_terms[0], 0, 0, 0, true, 1); +} + #[test] fn derived_helper_summary_compiles_region_backed_global_memory_terms() { let arch = make_x86_64_arch(); diff --git a/crates/r2types/src/facts.rs b/crates/r2types/src/facts.rs index 22e320d..a05baa1 100644 --- a/crates/r2types/src/facts.rs +++ b/crates/r2types/src/facts.rs @@ -57,6 +57,42 @@ pub enum SymbolicConditionPrecision { Unsupported, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum SymbolicVmUnaryOp { + Neg, + Not, + BoolNot, + ZExt, + SExt, + Trunc, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum SymbolicVmBinaryOp { + Add, + Sub, + Mul, + Div, + SDiv, + Rem, + SRem, + And, + Or, + Xor, + Shl, + Shr, + Eq, + Ne, + Lt, + Le, + Gt, + Ge, + PtrAdd, + PtrSub, + Piece, + Concat, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum SymbolicMemoryRegionKind { Stack, @@ -124,6 +160,70 @@ pub enum SymbolicVmValueExpr { Const(u64), Var(String), Expr(String), + Unary { + op: SymbolicVmUnaryOp, + expr: Box, + }, + Binary { + op: SymbolicVmBinaryOp, + left: Box, + right: Box, + }, +} + +impl SymbolicVmValueExpr { + pub fn render(&self) -> String { + match self { + Self::Const(value) => format!("0x{value:x}"), + Self::Var(name) | Self::Expr(name) => name.clone(), + Self::Unary { op, expr } => { + let inner = expr.render(); + match op { + SymbolicVmUnaryOp::Neg => format!("(-{inner})"), + SymbolicVmUnaryOp::Not => format!("(~{inner})"), + SymbolicVmUnaryOp::BoolNot => format!("(!{inner})"), + SymbolicVmUnaryOp::ZExt => format!("zext({inner})"), + SymbolicVmUnaryOp::SExt => format!("sext({inner})"), + SymbolicVmUnaryOp::Trunc => format!("trunc({inner})"), + } + } + Self::Binary { op, left, right } => { + let left = left.render(); + let right = right.render(); + let symbol = match op { + SymbolicVmBinaryOp::Add => "+", + SymbolicVmBinaryOp::Sub => "-", + SymbolicVmBinaryOp::Mul => "*", + SymbolicVmBinaryOp::Div | SymbolicVmBinaryOp::SDiv => "/", + SymbolicVmBinaryOp::Rem | SymbolicVmBinaryOp::SRem => "%", + SymbolicVmBinaryOp::And => "&", + SymbolicVmBinaryOp::Or => "|", + SymbolicVmBinaryOp::Xor => "^", + SymbolicVmBinaryOp::Shl => "<<", + SymbolicVmBinaryOp::Shr => ">>", + SymbolicVmBinaryOp::Eq => "==", + SymbolicVmBinaryOp::Ne => "!=", + SymbolicVmBinaryOp::Lt => "<", + SymbolicVmBinaryOp::Le => "<=", + SymbolicVmBinaryOp::Gt => ">", + SymbolicVmBinaryOp::Ge => ">=", + SymbolicVmBinaryOp::PtrAdd => "+", + SymbolicVmBinaryOp::PtrSub => "-", + SymbolicVmBinaryOp::Piece => "piece", + SymbolicVmBinaryOp::Concat => "concat", + }; + match op { + SymbolicVmBinaryOp::Piece | SymbolicVmBinaryOp::Concat => { + format!("{symbol}({left}, {right})") + } + SymbolicVmBinaryOp::PtrAdd | SymbolicVmBinaryOp::PtrSub => { + format!("({left} {symbol} {right})") + } + _ => format!("({left} {symbol} {right})"), + } + } + } + } } #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] @@ -185,6 +285,8 @@ pub struct SymbolicFactDiagnostics { pub branches_unknown: usize, pub skipped_missing_arch: bool, pub skipped_large_cfg: bool, + #[serde(default)] + pub cache_hit: bool, #[serde(skip_serializing_if = "Option::is_none")] pub semantic_mode: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -921,4 +1023,22 @@ mod tests { ); assert_eq!(parse_type_like_spec("void const *", 64), Some(void_ptr)); } + + #[test] + fn symbolic_vm_value_expr_renders_recursive_forms() { + let expr = SymbolicVmValueExpr::Binary { + op: SymbolicVmBinaryOp::Add, + left: Box::new(SymbolicVmValueExpr::Unary { + op: SymbolicVmUnaryOp::Neg, + expr: Box::new(SymbolicVmValueExpr::Const(0x10)), + }), + right: Box::new(SymbolicVmValueExpr::Binary { + op: SymbolicVmBinaryOp::Xor, + left: Box::new(SymbolicVmValueExpr::Var("state".to_string())), + right: Box::new(SymbolicVmValueExpr::Expr("mask".to_string())), + }), + }; + + assert_eq!(expr.render(), "((-0x10) + (state ^ mask))"); + } } diff --git a/crates/r2types/src/lib.rs b/crates/r2types/src/lib.rs index 0c52da2..9b6fac9 100644 --- a/crates/r2types/src/lib.rs +++ b/crates/r2types/src/lib.rs @@ -35,9 +35,9 @@ pub use facts::{ SymbolicMemoryCondition, SymbolicMemoryRegion, SymbolicMemoryRegionKind, SymbolicMemoryRegionRef, SymbolicReachabilityStatus, SymbolicSemanticCapability, SymbolicSemanticFacts, SymbolicSemanticMode, SymbolicSemanticResidualReason, - SymbolicSemanticSliceClass, SymbolicVmStateUpdate, SymbolicVmStepSummary, - SymbolicVmTransferArm, SymbolicVmTransferSummary, SymbolicVmValueExpr, VisibleBinding, - VisibleBindingKind, parse_type_like_spec, + SymbolicSemanticSliceClass, SymbolicVmBinaryOp, SymbolicVmStateUpdate, SymbolicVmStepSummary, + SymbolicVmTransferArm, SymbolicVmTransferSummary, SymbolicVmUnaryOp, SymbolicVmValueExpr, + VisibleBinding, VisibleBindingKind, parse_type_like_spec, }; pub use inference::{CombinedTypeOracle, TypeInference}; pub use model::{Signedness, StructField, StructShape, Type, TypeArena, TypeId}; diff --git a/r2plugin/src/analysis/sym.rs b/r2plugin/src/analysis/sym.rs index 1d561d9..905f8eb 100644 --- a/r2plugin/src/analysis/sym.rs +++ b/r2plugin/src/analysis/sym.rs @@ -1,6 +1,8 @@ use crate::blocks::BlockSlice; use crate::context::require_ctx_view; +use crate::types::symbolic_vm_value_expr_from_sym; use crate::{ArchSpec, R2ILBlock, R2ILContext, parse_addr_name_map}; +use r2types::SymbolicVmValueExpr; use serde::Serialize; use std::collections::BTreeMap; use std::collections::HashMap; @@ -334,18 +336,16 @@ pub(crate) struct VmStepSummaryInfo { pub(crate) transfers: Vec, } -fn render_vm_value_expr(value: &r2sym::VmValueExpr) -> String { - match value { - r2sym::VmValueExpr::Const(value) => format!("0x{value:x}"), - r2sym::VmValueExpr::Var(name) | r2sym::VmValueExpr::Expr(name) => name.clone(), - } +fn render_vm_value_expr(value: &SymbolicVmValueExpr) -> String { + value.render() } fn vm_state_update_info_from_sym(update: &r2sym::VmStateUpdate) -> VmStateUpdateInfo { + let value = symbolic_vm_value_expr_from_sym(&update.value); VmStateUpdateInfo { output: update.output.clone(), expr: update.expr.clone(), - value: render_vm_value_expr(&update.value), + value: render_vm_value_expr(&value), exact: update.exact, } } @@ -671,7 +671,10 @@ pub(crate) fn compiled_semantic_info( score: interpreter.score, }), vm_step: compiled.vm_step.as_ref().map(vm_step_summary_info_from_sym), - vm_transfer: compiled.vm_step.as_ref().map(vm_step_summary_info_from_sym), + vm_transfer: compiled + .vm_transfer + .as_ref() + .map(vm_step_summary_info_from_sym), cache_hit: compiled.cache_hit, } } diff --git a/r2plugin/src/decompiler.rs b/r2plugin/src/decompiler.rs index 806830f..54d9ae2 100644 --- a/r2plugin/src/decompiler.rs +++ b/r2plugin/src/decompiler.rs @@ -159,22 +159,25 @@ pub(crate) fn run_full_decompile_on_large_stack( } artifact } else { - if let Some(semantic_artifact) = crate::types::collect_detached_semantic_artifact( + let precomputed_semantic_artifact = crate::types::collect_detached_semantic_artifact( &r2il_blocks, &func_name_str, arch.as_ref(), symbolic_scope.as_ref(), - ) && crate::should_decompile_from_semantic_fallback( - &func_name_str, - &semantic_artifact, - ) { + ); + if let Some(semantic_artifact) = precomputed_semantic_artifact.as_ref() + && crate::should_decompile_from_semantic_fallback( + &func_name_str, + semantic_artifact, + ) + { return crate::decompile_semantic_artifact_fallback( &display_func_name, - &semantic_artifact, + semantic_artifact, ); } let Some(artifact) = - crate::types::build_detached_function_analysis_artifact_with_scope( + crate::types::build_detached_function_analysis_artifact_with_scope_and_semantics( &r2il_blocks, &func_name_str, arch.as_ref(), @@ -183,6 +186,7 @@ pub(crate) fn run_full_decompile_on_large_stack( ®_type_hints, &external_context_json, symbolic_scope.as_ref(), + precomputed_semantic_artifact, ) else { return decompile_artifact_guard_fallback( diff --git a/r2plugin/src/lib.rs b/r2plugin/src/lib.rs index 200913b..f330da6 100644 --- a/r2plugin/src/lib.rs +++ b/r2plugin/src/lib.rs @@ -2434,6 +2434,28 @@ fn decompile_semantic_artifact_fallback( } else { vm_step.state_outputs.join(", ") }; + let exact_transfers = vm_step + .transfers + .iter() + .filter(|transfer| transfer.exact) + .count(); + let redispatch_transfers = vm_step + .transfers + .iter() + .filter(|transfer| transfer.redispatch) + .count(); + let returning_transfers = vm_step + .transfers + .iter() + .filter(|transfer| transfer.may_return) + .count(); + let selector_updates = vm_step + .transfers + .iter() + .filter(|transfer| transfer.selector_update.is_some()) + .count(); + let total_reads: usize = vm_step.handler_memory_reads.values().copied().sum(); + let total_writes: usize = vm_step.handler_memory_writes.values().copied().sum(); let handler_preview = vm_step .dispatch_targets .iter() @@ -2468,13 +2490,19 @@ fn decompile_semantic_artifact_fallback( .collect::>() .join("; "); return format!( - "/* r2dec semantic summary: vm_summary for {} ({kind} @ 0x{:x}, loop_header=0x{:x}, selector={}, targets={}, redispatch={}, state_inputs=[{}], state_outputs=[{}], handlers={}) */", + "/* r2dec semantic summary: vm_summary for {} ({kind} @ 0x{:x}, loop_header=0x{:x}, selector={}, targets={}, redispatch={}, exact_transfers={}, redispatch_transfers={}, returning_transfers={}, selector_updates={}, total_reads={}, total_writes={}, state_inputs=[{}], state_outputs=[{}], handlers={}) */", func_name, vm_step.dispatch_header, vm_step.loop_header, selector, vm_step.dispatch_targets.len(), vm_step.redispatch_handlers.len(), + exact_transfers, + redispatch_transfers, + returning_transfers, + selector_updates, + total_reads, + total_writes, inputs, outputs, handler_preview, diff --git a/r2plugin/src/types.rs b/r2plugin/src/types.rs index 79f0ed5..13910ae 100644 --- a/r2plugin/src/types.rs +++ b/r2plugin/src/types.rs @@ -282,10 +282,44 @@ fn symbolic_interpreter_kind_from_sym( } } -fn symbolic_vm_value_expr_from_sym(value: &r2sym::VmValueExpr) -> r2types::SymbolicVmValueExpr { +pub(crate) fn symbolic_vm_value_expr_from_sym( + value: &r2sym::VmValueExpr, +) -> r2types::SymbolicVmValueExpr { match value { r2sym::VmValueExpr::Const(value) => r2types::SymbolicVmValueExpr::Const(*value), r2sym::VmValueExpr::Var(name) => r2types::SymbolicVmValueExpr::Var(name.clone()), + r2sym::VmValueExpr::Unary { op, arg } => r2types::SymbolicVmValueExpr::Unary { + op: match op { + r2sym::VmUnaryOp::Neg => r2types::SymbolicVmUnaryOp::Neg, + r2sym::VmUnaryOp::BitNot => r2types::SymbolicVmUnaryOp::Not, + r2sym::VmUnaryOp::BoolNot => r2types::SymbolicVmUnaryOp::BoolNot, + }, + expr: Box::new(symbolic_vm_value_expr_from_sym(arg)), + }, + r2sym::VmValueExpr::Binary { op, lhs, rhs } => r2types::SymbolicVmValueExpr::Binary { + op: match op { + r2sym::VmBinaryOp::Add => r2types::SymbolicVmBinaryOp::Add, + r2sym::VmBinaryOp::Sub => r2types::SymbolicVmBinaryOp::Sub, + r2sym::VmBinaryOp::Mul => r2types::SymbolicVmBinaryOp::Mul, + r2sym::VmBinaryOp::Div => r2types::SymbolicVmBinaryOp::Div, + r2sym::VmBinaryOp::Rem => r2types::SymbolicVmBinaryOp::Rem, + r2sym::VmBinaryOp::And => r2types::SymbolicVmBinaryOp::And, + r2sym::VmBinaryOp::Or => r2types::SymbolicVmBinaryOp::Or, + r2sym::VmBinaryOp::Xor => r2types::SymbolicVmBinaryOp::Xor, + r2sym::VmBinaryOp::Shl => r2types::SymbolicVmBinaryOp::Shl, + r2sym::VmBinaryOp::LShr | r2sym::VmBinaryOp::AShr => { + r2types::SymbolicVmBinaryOp::Shr + } + r2sym::VmBinaryOp::Eq => r2types::SymbolicVmBinaryOp::Eq, + r2sym::VmBinaryOp::Ne => r2types::SymbolicVmBinaryOp::Ne, + r2sym::VmBinaryOp::Lt | r2sym::VmBinaryOp::SLt => r2types::SymbolicVmBinaryOp::Lt, + r2sym::VmBinaryOp::Le | r2sym::VmBinaryOp::SLe => r2types::SymbolicVmBinaryOp::Le, + r2sym::VmBinaryOp::BoolAnd => r2types::SymbolicVmBinaryOp::And, + r2sym::VmBinaryOp::BoolOr => r2types::SymbolicVmBinaryOp::Or, + }, + left: Box::new(symbolic_vm_value_expr_from_sym(lhs)), + right: Box::new(symbolic_vm_value_expr_from_sym(rhs)), + }, r2sym::VmValueExpr::Expr(expr) => r2types::SymbolicVmValueExpr::Expr(expr.clone()), } } @@ -405,6 +439,7 @@ pub(crate) fn symbolic_semantic_facts_from_sym( branches_unknown: facts.diagnostics.branches_unknown, skipped_missing_arch: facts.diagnostics.skipped_missing_arch, skipped_large_cfg: facts.diagnostics.skipped_large_cfg, + cache_hit: compiled.cache_hit, semantic_mode: Some(match compiled.mode { r2sym::SemanticMode::Raw => r2types::SymbolicSemanticMode::Raw, r2sym::SemanticMode::Compiled => r2types::SymbolicSemanticMode::Compiled, @@ -476,7 +511,7 @@ pub(crate) fn symbolic_semantic_facts_from_sym( .as_ref() .map(symbolic_vm_step_summary_from_sym), vm_transfer: compiled - .vm_step + .vm_transfer .as_ref() .map(symbolic_vm_step_summary_from_sym), } @@ -1345,7 +1380,7 @@ pub(crate) fn build_detached_function_analysis_artifact( reg_type_hints: &std::collections::HashMap, external_context_json: &str, ) -> Option { - build_detached_function_analysis_artifact_with_scope( + build_detached_function_analysis_artifact_with_scope_and_semantics( blocks, function_name, arch, @@ -1354,10 +1389,11 @@ pub(crate) fn build_detached_function_analysis_artifact( reg_type_hints, external_context_json, None, + None, ) } -#[allow(clippy::too_many_arguments)] +#[allow(dead_code, clippy::too_many_arguments)] pub(crate) fn build_detached_function_analysis_artifact_with_scope( blocks: &[R2ILBlock], function_name: &str, @@ -1367,6 +1403,31 @@ pub(crate) fn build_detached_function_analysis_artifact_with_scope( reg_type_hints: &std::collections::HashMap, external_context_json: &str, symbolic_scope: Option<&r2sym::PreparedFunctionScope>, +) -> Option { + build_detached_function_analysis_artifact_with_scope_and_semantics( + blocks, + function_name, + arch, + ptr_bits, + semantic_metadata_enabled, + reg_type_hints, + external_context_json, + symbolic_scope, + None, + ) +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn build_detached_function_analysis_artifact_with_scope_and_semantics( + blocks: &[R2ILBlock], + function_name: &str, + arch: Option<&ArchSpec>, + ptr_bits: u32, + semantic_metadata_enabled: bool, + reg_type_hints: &std::collections::HashMap, + external_context_json: &str, + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, + precomputed_semantic_artifact: Option, ) -> Option { let cache_key = function_artifact_cache_key_parts( function_name, @@ -1537,8 +1598,9 @@ pub(crate) fn build_detached_function_analysis_artifact_with_scope( interproc_summary_set: None, diagnostics: writeback_diagnostics_from_plugin(diagnostics), }); - let semantic_artifact = - collect_plugin_semantic_artifact(&analysis.ssa_func, arch, symbolic_scope); + let semantic_artifact = precomputed_semantic_artifact.unwrap_or_else(|| { + collect_plugin_semantic_artifact(&analysis.ssa_func, arch, symbolic_scope) + }); let symbolic_facts = symbolic_semantic_facts_from_sym(&semantic_artifact); let mut type_facts = writeback.type_facts; if symbolic_facts.diagnostics.branches_pruned > 0 { diff --git a/tests/r2r/db/extras/r2sleigh_integration_extended b/tests/r2r/db/extras/r2sleigh_integration_extended index 22641fb..cea905b 100644 --- a/tests/r2r/db/extras/r2sleigh_integration_extended +++ b/tests/r2r/db/extras/r2sleigh_integration_extended @@ -273,6 +273,18 @@ a:sla.sym sym.interpret_bytecode | jq -c '.semantic.mode=="vm_summary" and .sema EOF_CMDS RUN +NAME=symbolic_interpret_bytecode_vm_summary_stays_query_ready +FILE=bins/stress_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=<= 8) and ((.semantic.vm_transfer.transfers | map(select(.redispatch)) | length) >= 4)' +EOF_CMDS +RUN + NAME=types_tiny_vm_dispatch_reports_vm_step_payload FILE=bins/stress_test ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true @@ -297,6 +309,19 @@ a:sla.types sym.tiny_vm_dispatch | jq -c '.symbolic.diagnostics.semantic_mode==" EOF_CMDS RUN +NAME=types_tiny_vm_dispatch_reports_semantic_cache_hit_on_repeat +FILE=bins/stress_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=</dev/null +a:sla.types sym.tiny_vm_dispatch | jq -c '.symbolic.diagnostics.semantic_mode=="VmSummary" and .symbolic.diagnostics.cache_hit==true and .symbolic.vm_transfer != null' +EOF_CMDS +RUN + NAME=symbolic_tiny_vm_dispatch_reports_vm_transfer_payload FILE=bins/stress_test ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true @@ -333,6 +358,18 @@ a:sla.dec sym.tiny_vm_dispatch | jq -Rs 'contains("r2dec semantic summary: vm_su EOF_CMDS RUN +NAME=decompile_tiny_vm_dispatch_reports_vm_transfer_totals +FILE=bins/stress_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=< None, + } + } +} + +fn tokenize_symbolic_condition(text: &str) -> Option> { + let mut tokens = Vec::new(); + let chars = text.chars().collect::>(); + let mut i = 0usize; + while i < chars.len() { + let ch = chars[i]; + if ch.is_whitespace() { + i += 1; + continue; + } + let rest = &chars[i..]; + let push_two = |token: SymbolicConditionToken, i: &mut usize, tokens: &mut Vec<_>| { + tokens.push(token); + *i += 2; + }; + if rest.len() >= 2 { + match (rest[0], rest[1]) { + ('|', '|') => { + push_two(SymbolicConditionToken::Or, &mut i, &mut tokens); + continue; + } + ('&', '&') => { + push_two(SymbolicConditionToken::And, &mut i, &mut tokens); + continue; + } + ('=', '=') => { + push_two(SymbolicConditionToken::Eq, &mut i, &mut tokens); + continue; + } + ('!', '=') => { + push_two(SymbolicConditionToken::Ne, &mut i, &mut tokens); + continue; + } + ('<', '=') => { + push_two(SymbolicConditionToken::Le, &mut i, &mut tokens); + continue; + } + ('>', '=') => { + push_two(SymbolicConditionToken::Ge, &mut i, &mut tokens); + continue; + } + ('<', '<') => { + push_two(SymbolicConditionToken::Shl, &mut i, &mut tokens); + continue; + } + ('>', '>') => { + push_two(SymbolicConditionToken::Shr, &mut i, &mut tokens); + continue; + } + _ => {} + } + } + match ch { + '(' => tokens.push(SymbolicConditionToken::LParen), + ')' => tokens.push(SymbolicConditionToken::RParen), + '!' => tokens.push(SymbolicConditionToken::Not), + '~' => tokens.push(SymbolicConditionToken::BitNot), + '*' => tokens.push(SymbolicConditionToken::Star), + '/' => tokens.push(SymbolicConditionToken::Slash), + '%' => tokens.push(SymbolicConditionToken::Percent), + '+' => tokens.push(SymbolicConditionToken::Plus), + '-' => tokens.push(SymbolicConditionToken::Minus), + '<' => tokens.push(SymbolicConditionToken::Lt), + '>' => tokens.push(SymbolicConditionToken::Gt), + '&' => tokens.push(SymbolicConditionToken::BitAnd), + '^' => tokens.push(SymbolicConditionToken::BitXor), + '|' => tokens.push(SymbolicConditionToken::BitOr), + '=' => return None, + _ => { + let start = i; + while i < chars.len() { + let ch = chars[i]; + if ch.is_whitespace() + || matches!( + ch, + '(' | ')' + | '!' + | '~' + | '*' + | '/' + | '%' + | '+' + | '-' + | '<' + | '>' + | '&' + | '^' + | '|' + | '=' + ) + { + break; + } + i += 1; + } + if start == i { + return None; + } + tokens.push(SymbolicConditionToken::Atom( + chars[start..i].iter().collect(), + )); + continue; + } + } + i += 1; + } + Some(tokens) +} + impl<'a> FoldingContext<'a> { fn finalize_condition_expr(&self, expr: CExpr) -> CExpr { let expr = self.normalize_local_branch_expr(expr); @@ -104,23 +445,20 @@ impl<'a> FoldingContext<'a> { } } - fn symbolic_exact_compiled_condition( + fn symbolic_actionable_compiled_condition( &self, block_addr: u64, ) -> Option<&r2types::SymbolicCompiledCondition> { self.inputs .symbolic_facts - .branch_fact_for_block(block_addr) - .and_then(|fact| fact.exact_compiled_condition()) + .actionable_compiled_condition_for_block(block_addr) } - fn symbolic_exact_compiled_condition_expr(&self, block_addr: u64) -> Option { - let compiled = self.symbolic_exact_compiled_condition(block_addr)?; - match compiled.simplified.trim().to_ascii_lowercase().as_str() { - "1" | "true" => Some(CExpr::IntLit(1)), - "0" | "false" => Some(CExpr::IntLit(0)), - _ => None, - } + fn symbolic_actionable_compiled_condition_expr(&self, block_addr: u64) -> Option { + let compiled = self.symbolic_actionable_compiled_condition(block_addr)?; + SymbolicConditionExprParser::new(self, compiled.simplified.trim()) + .and_then(SymbolicConditionExprParser::parse) + .map(|expr| self.finalize_condition_expr(expr)) } fn prepared_predicate_view(&self) -> Option> { @@ -328,7 +666,7 @@ impl<'a> FoldingContext<'a> { } pub fn extract_condition_from_block(&self, block: &FunctionSSABlock) -> Option { - if let Some(cond) = self.symbolic_exact_compiled_condition_expr(block.addr) { + if let Some(cond) = self.symbolic_actionable_compiled_condition_expr(block.addr) { return Some(cond); } @@ -350,7 +688,7 @@ impl<'a> FoldingContext<'a> { let prepared_block_candidate = self.prepared_predicate_candidate_for_branch_block(block.addr, cond); let prepared_var_candidate = self.prepared_predicate_candidate_for_var(cond); - let exact_compiled = self.symbolic_exact_compiled_condition(block.addr); + let exact_compiled = self.symbolic_actionable_compiled_condition(block.addr); let allow_legacy_flag_provenance = exact_compiled.is_none() && ![ prepared_branch_candidate.as_ref(), diff --git a/crates/r2dec/src/fold/tests/flags.rs b/crates/r2dec/src/fold/tests/flags.rs index 1ff724c..d3588c1 100644 --- a/crates/r2dec/src/fold/tests/flags.rs +++ b/crates/r2dec/src/fold/tests/flags.rs @@ -3,8 +3,9 @@ use crate::fold::FoldingContext; use crate::fold::context::{FoldArchConfig, FoldInputs}; use r2il::{ArchSpec, R2ILBlock, R2ILOp, RegisterDef, Varnode}; use r2types::{ - SymbolicBranchFact, SymbolicCompiledCondition, SymbolicConditionPrecision, - SymbolicReachabilityStatus, SymbolicSemanticFacts, + SymbolicBranchFact, SymbolicCompiledCondition, SymbolicConditionPrecision, SymbolicControlFact, + SymbolicControlIsland, SymbolicControlIslandKind, SymbolicReachabilityStatus, + SymbolicSemanticFacts, }; use std::collections::{BTreeMap, HashMap, HashSet}; @@ -194,6 +195,8 @@ fn exact_compiled_condition_shortcuts_to_literal_condition() { backward_memory_candidate_enumerations: 0, backward_memory_residual_fallbacks: 0, precision: SymbolicConditionPrecision::Exact, + evidence: r2types::SymbolicSemanticEvidence::exact(), + confidence: r2types::SymbolicSemanticConfidence::Exact, supported_paths: 1, total_paths: 1, }), @@ -202,7 +205,7 @@ fn exact_compiled_condition_shortcuts_to_literal_condition() { let entry = prepared.function().get_block(0x1000).expect("entry"); assert_eq!( - ctx.symbolic_exact_compiled_condition_expr(entry.addr), + ctx.symbolic_actionable_compiled_condition_expr(entry.addr), Some(CExpr::IntLit(0)) ); assert_eq!( @@ -210,3 +213,159 @@ fn exact_compiled_condition_shortcuts_to_literal_condition() { Some(CExpr::IntLit(0)) ); } + +#[test] +fn actionable_control_island_shortcuts_when_branch_fact_is_not_decisive() { + let arch = make_test_arch_x86_64(); + let mut entry = R2ILBlock::new(0x2000, 4); + entry.push(R2ILOp::IntNotEqual { + dst: Varnode::unique(1, 1), + a: Varnode::register(0x10, 4), + b: Varnode::constant(0, 4), + }); + entry.push(R2ILOp::CBranch { + target: Varnode::constant(0x2008, 8), + cond: Varnode::unique(1, 1), + }); + let mut fallthrough = R2ILBlock::new(0x2004, 4); + fallthrough.push(R2ILOp::Return { + target: Varnode::constant(0, 8), + }); + let mut taken = R2ILBlock::new(0x2008, 4); + taken.push(R2ILOp::Return { + target: Varnode::constant(1, 8), + }); + + let prepared = + prepared_from_r2il_blocks(&[entry, fallthrough, taken], &arch).with_name("control_island"); + let mut ctx = make_x86_64_ctx_with_prepared(&prepared); + let mut facts = SymbolicSemanticFacts::default(); + facts.branch_facts.push(SymbolicBranchFact { + block_addr: 0x2000, + true_target: 0x2008, + false_target: 0x2004, + true_status: SymbolicReachabilityStatus::Unknown, + false_status: SymbolicReachabilityStatus::Unknown, + true_condition: Some("false".to_string()), + false_condition: None, + true_compiled: Some(SymbolicCompiledCondition { + simplified: "false".to_string(), + terms: vec!["false".to_string()], + memory_terms: vec![], + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: SymbolicConditionPrecision::OverApprox, + evidence: r2types::SymbolicSemanticEvidence::likely( + r2types::SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: r2types::SymbolicSemanticConfidence::Likely, + supported_paths: 1, + total_paths: 2, + }), + false_compiled: None, + }); + facts.control_islands.push(SymbolicControlIsland { + kind: SymbolicControlIslandKind::LargeCfgBranchFrontier, + anchor_block: 0x2000, + frontier_targets: vec![0x2008, 0x2004], + facts: vec![ + SymbolicControlFact { + target: 0x2008, + status: SymbolicReachabilityStatus::Unknown, + condition: Some("false".to_string()), + compiled: facts.branch_facts[0].true_compiled.clone(), + evidence: r2types::SymbolicSemanticEvidence::likely( + r2types::SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: r2types::SymbolicSemanticConfidence::Likely, + }, + SymbolicControlFact { + target: 0x2004, + status: SymbolicReachabilityStatus::Unknown, + condition: None, + compiled: None, + evidence: r2types::SymbolicSemanticEvidence::residual( + r2types::SymbolicSemanticEvidenceReason::GuardOpaque, + ), + confidence: r2types::SymbolicSemanticConfidence::Residual, + }, + ], + evidence: r2types::SymbolicSemanticEvidence::likely( + r2types::SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: r2types::SymbolicSemanticConfidence::Likely, + }); + ctx.inputs.symbolic_facts = Box::leak(Box::new(facts)); + + let entry = prepared.function().get_block(0x2000).expect("entry"); + assert_eq!( + ctx.symbolic_actionable_compiled_condition_expr(entry.addr), + Some(CExpr::IntLit(0)) + ); +} + +#[test] +fn actionable_control_island_parses_non_literal_compiled_condition_expr() { + let arch = make_test_arch_x86_64(); + let mut entry = R2ILBlock::new(0x3000, 4); + entry.push(R2ILOp::IntNotEqual { + dst: Varnode::unique(1, 1), + a: Varnode::register(0x10, 4), + b: Varnode::constant(0, 4), + }); + entry.push(R2ILOp::CBranch { + target: Varnode::constant(0x3008, 8), + cond: Varnode::unique(1, 1), + }); + let mut fallthrough = R2ILBlock::new(0x3004, 4); + fallthrough.push(R2ILOp::Return { + target: Varnode::constant(0, 8), + }); + let mut taken = R2ILBlock::new(0x3008, 4); + taken.push(R2ILOp::Return { + target: Varnode::constant(1, 8), + }); + + let prepared = prepared_from_r2il_blocks(&[entry, fallthrough, taken], &arch) + .with_name("parsed_control_island"); + let mut ctx = make_x86_64_ctx_with_prepared(&prepared); + let mut facts = SymbolicSemanticFacts::default(); + facts.control_islands.push(SymbolicControlIsland { + kind: SymbolicControlIslandKind::LargeCfgBranchFrontier, + anchor_block: 0x3000, + frontier_targets: vec![0x3008], + facts: vec![SymbolicControlFact { + target: 0x3008, + status: SymbolicReachabilityStatus::Reachable, + condition: Some("x == 0".to_string()), + compiled: Some(SymbolicCompiledCondition { + simplified: "x == 0".to_string(), + terms: vec!["x == 0".to_string()], + memory_terms: vec![], + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: SymbolicConditionPrecision::Exact, + evidence: r2types::SymbolicSemanticEvidence::exact(), + confidence: r2types::SymbolicSemanticConfidence::Exact, + supported_paths: 1, + total_paths: 1, + }), + evidence: r2types::SymbolicSemanticEvidence::exact(), + confidence: r2types::SymbolicSemanticConfidence::Exact, + }], + evidence: r2types::SymbolicSemanticEvidence::exact(), + confidence: r2types::SymbolicSemanticConfidence::Exact, + }); + ctx.inputs.symbolic_facts = Box::leak(Box::new(facts)); + + assert_eq!( + ctx.symbolic_actionable_compiled_condition_expr(0x3000), + Some(CExpr::binary( + BinaryOp::Eq, + CExpr::Var("x".to_string()), + CExpr::IntLit(0), + )) + ); +} diff --git a/crates/r2dec/src/lib.rs b/crates/r2dec/src/lib.rs index 05993b6..12a0220 100644 --- a/crates/r2dec/src/lib.rs +++ b/crates/r2dec/src/lib.rs @@ -202,6 +202,55 @@ fn format_vm_state_updates(updates: &[r2types::SymbolicVmStateUpdate]) -> String format!("[{rendered}]") } +fn format_vm_guarded_exits(guards: &[r2types::SymbolicVmGuardedExit]) -> String { + if guards.is_empty() { + return "[]".to_string(); + } + let rendered = guards + .iter() + .map(|guard| format!("0x{:x}:{}", guard.target, guard.guard.expr)) + .collect::>() + .join(", "); + format!("[{rendered}]") +} + +fn format_vm_memory_conditions(conditions: &[r2types::SymbolicMemoryCondition]) -> String { + if conditions.is_empty() { + return "[]".to_string(); + } + let rendered = conditions + .iter() + .map(|condition| { + let region = match &condition.region { + r2types::SymbolicMemoryRegion::Argument { index } => format!("arg{index}"), + r2types::SymbolicMemoryRegion::Region(region) => region.name.clone(), + }; + let binding = condition + .binding + .as_deref() + .map(|binding| format!(" -> {binding}")) + .unwrap_or_default(); + let value = condition + .value_expr + .as_deref() + .map(|value| format!(" = {value}")) + .unwrap_or_default(); + format!( + "{}@[{:#x}..{:#x}]/{}:{}{}{}", + region, + condition.offset_lo, + condition.offset_hi, + condition.size, + condition.expr, + binding, + value, + ) + }) + .collect::>() + .join(", "); + format!("[{rendered}]") +} + fn format_vm_summary_kind(kind: SymbolicInterpreterKind) -> &'static str { match kind { SymbolicInterpreterKind::SwitchDispatch => "switch_dispatch", @@ -707,7 +756,27 @@ impl Decompiler { let exact_transfers = vm_step .transfers .iter() - .filter(|transfer| transfer.exact) + .filter(|transfer| transfer.evidence.allows_hard_proof()) + .count(); + let likely_transfers = vm_step + .transfers + .iter() + .filter(|transfer| { + matches!( + transfer.evidence.tier, + r2types::SymbolicSemanticConfidence::Likely + ) + }) + .count(); + let heuristic_transfers = vm_step + .transfers + .iter() + .filter(|transfer| { + matches!( + transfer.evidence.tier, + r2types::SymbolicSemanticConfidence::Heuristic + ) + }) .count(); let redispatch_transfers = vm_step .transfers @@ -724,6 +793,22 @@ impl Decompiler { .iter() .filter(|transfer| transfer.selector_update.is_some()) .count(); + let exact_exit_guards = vm_step + .transfers + .iter() + .flat_map(|transfer| transfer.exit_guards.iter()) + .filter(|guard| guard.guard.evidence.allows_hard_proof()) + .count(); + let total_read_effects: usize = vm_step + .handler_memory_read_effects + .values() + .map(Vec::len) + .sum(); + let total_write_effects: usize = vm_step + .handler_memory_write_effects + .values() + .map(Vec::len) + .sum(); let total_reads: usize = vm_step.handler_memory_reads.values().copied().sum(); let total_writes: usize = vm_step.handler_memory_writes.values().copied().sum(); @@ -731,7 +816,7 @@ impl Decompiler { let _ = writeln!(&mut out, "r2dec semantic summary: vm_summary"); let _ = writeln!( &mut out, - "kind={} dispatch_header=0x{:x} loop_header=0x{:x} selector={} targets={} default_target={} latches={} step_blocks={} transfers={} exact_transfers={} redispatch_transfers={} returning_transfers={} selector_updates={} total_reads={} total_writes={}", + "kind={} dispatch_header=0x{:x} loop_header=0x{:x} selector={} targets={} default_target={} latches={} step_blocks={} transfers={} exact_transfers={} likely_transfers={} heuristic_transfers={} redispatch_transfers={} returning_transfers={} selector_updates={} exact_exit_guards={} total_reads={} total_writes={} total_read_effects={} total_write_effects={}", format_vm_summary_kind(vm_step.kind), vm_step.dispatch_header, vm_step.loop_header, @@ -745,11 +830,16 @@ impl Decompiler { vm_step.step_blocks.len(), vm_step.transfers.len(), exact_transfers, + likely_transfers, + heuristic_transfers, redispatch_transfers, returning_transfers, selector_updates, + exact_exit_guards, total_reads, total_writes, + total_read_effects, + total_write_effects, ); if !vm_step.state_inputs.is_empty() || !vm_step.state_outputs.is_empty() { let _ = writeln!( @@ -770,14 +860,21 @@ impl Decompiler { .unwrap_or_else(|| "none".to_string()); let _ = writeln!( &mut out, - "transfer handler=0x{:x} cases={} blocks={} exits={} updates={} selector_update={} exact={} redispatch={} return={} truncated={}", + "transfer handler=0x{:x} cases={} blocks={} exits={} exit_guards={} updates={} selector_update={} reads={} writes={} exact={} confidence={:?} reasons={} residual_guards={} residual_memory={} redispatch={} return={} truncated={}", transfer.handler_target, format_vm_target_list(&transfer.case_values), format_vm_target_list(&transfer.region_blocks), format_vm_target_list(&transfer.exit_targets), + format_vm_guarded_exits(&transfer.exit_guards), format_vm_state_updates(&transfer.state_updates), selector_update, - transfer.exact, + format_vm_memory_conditions(&transfer.memory_reads), + format_vm_memory_conditions(&transfer.memory_writes), + transfer.evidence.allows_hard_proof(), + transfer.evidence.tier, + format_args!("{:?}", transfer.evidence.reasons), + transfer.residual_guards, + transfer.residual_memory_effects, transfer.redispatch, transfer.may_return, transfer.truncated, @@ -818,9 +915,24 @@ impl Decompiler { .get(&handler) .map(|values| format_vm_target_list(values)) .unwrap_or_else(|| "[]".to_string()); + let guards = vm_step + .handler_exit_guards + .get(&handler) + .map(|values| format_vm_guarded_exits(values)) + .unwrap_or_else(|| "[]".to_string()); + let read_effects = vm_step + .handler_memory_read_effects + .get(&handler) + .map(|values| format_vm_memory_conditions(values)) + .unwrap_or_else(|| "[]".to_string()); + let write_effects = vm_step + .handler_memory_write_effects + .get(&handler) + .map(|values| format_vm_memory_conditions(values)) + .unwrap_or_else(|| "[]".to_string()); let _ = writeln!( &mut out, - "handler 0x{:x}: regions={} cases={} inputs={} outputs={} updates={} reads={} writes={} calls={} branches={} exits={}", + "handler 0x{:x}: regions={} cases={} inputs={} outputs={} updates={} reads={} writes={} read_effects={} write_effects={} calls={} branches={} exits={} guards={}", handler, regions, cases, @@ -837,6 +949,8 @@ impl Decompiler { .get(&handler) .copied() .unwrap_or(0), + read_effects, + write_effects, vm_step.handler_calls.get(&handler).copied().unwrap_or(0), vm_step .handler_conditional_branches @@ -844,6 +958,7 @@ impl Decompiler { .copied() .unwrap_or(0), exits, + guards, ); } @@ -4250,6 +4365,76 @@ mod tests { expr: "state + 1".to_string(), value: r2types::SymbolicVmValueExpr::Expr("state + 1".to_string()), exact: false, + evidence: r2types::SymbolicSemanticEvidence::heuristic( + r2types::SymbolicSemanticEvidenceReason::ValueOpaque, + ), + confidence: r2types::SymbolicSemanticConfidence::Heuristic, + }], + )]), + handler_exit_guards: BTreeMap::from([( + 0x1004, + vec![r2types::SymbolicVmGuardedExit { + target: 0x1008, + guard: r2types::SymbolicVmGuardCondition { + expr: "(state == 0x1)".to_string(), + value: r2types::SymbolicVmValueExpr::Expr( + "state == 0x1".to_string(), + ), + expect_nonzero: true, + exact: false, + evidence: r2types::SymbolicSemanticEvidence::heuristic( + r2types::SymbolicSemanticEvidenceReason::GuardOpaque, + ), + confidence: r2types::SymbolicSemanticConfidence::Heuristic, + }, + }], + )]), + handler_memory_read_effects: BTreeMap::from([( + 0x1004, + vec![r2types::SymbolicMemoryCondition { + region: r2types::SymbolicMemoryRegion::Region( + r2types::SymbolicMemoryRegionRef { + id: 1, + kind: r2types::SymbolicMemoryRegionKind::Global, + name: "ram:0x2000".to_string(), + }, + ), + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: r2types::SymbolicSemanticEvidence::likely( + r2types::SymbolicSemanticEvidenceReason::ValueOpaque, + ), + confidence: r2types::SymbolicSemanticConfidence::Likely, + binding: Some("mem:r1:0:1".to_string()), + expr: "vm.sel".to_string(), + value_expr: None, + exact_value: false, + }], + )]), + handler_memory_write_effects: BTreeMap::from([( + 0x1004, + vec![r2types::SymbolicMemoryCondition { + region: r2types::SymbolicMemoryRegion::Region( + r2types::SymbolicMemoryRegionRef { + id: 2, + kind: r2types::SymbolicMemoryRegionKind::Heap, + name: "heap_alloc@1".to_string(), + }, + ), + offset_lo: 4, + offset_hi: 4, + size: 1, + exact_offset: true, + evidence: r2types::SymbolicSemanticEvidence::likely( + r2types::SymbolicSemanticEvidenceReason::ValueOpaque, + ), + confidence: r2types::SymbolicSemanticConfidence::Likely, + binding: Some("mem:r2:4:1".to_string()), + expr: "state".to_string(), + value_expr: Some("state".to_string()), + exact_value: false, }], )]), handler_memory_reads: BTreeMap::from([(0x1004, 1)]), @@ -4265,19 +4450,88 @@ mod tests { case_values: vec![1, 2], region_blocks: vec![0x1004, 0x1008], exit_targets: vec![0x1008], + exit_guards: vec![r2types::SymbolicVmGuardedExit { + target: 0x1008, + guard: r2types::SymbolicVmGuardCondition { + expr: "(state == 0x1)".to_string(), + value: r2types::SymbolicVmValueExpr::Expr( + "state == 0x1".to_string(), + ), + expect_nonzero: true, + exact: false, + evidence: r2types::SymbolicSemanticEvidence::heuristic( + r2types::SymbolicSemanticEvidenceReason::GuardOpaque, + ), + confidence: r2types::SymbolicSemanticConfidence::Heuristic, + }, + }], state_updates: vec![SymbolicVmStateUpdate { output: "state".to_string(), expr: "state + 1".to_string(), value: r2types::SymbolicVmValueExpr::Expr("state + 1".to_string()), exact: false, + evidence: r2types::SymbolicSemanticEvidence::heuristic( + r2types::SymbolicSemanticEvidenceReason::ValueOpaque, + ), + confidence: r2types::SymbolicSemanticConfidence::Heuristic, }], selector_update: Some(SymbolicVmStateUpdate { output: "vm.sel".to_string(), expr: "3".to_string(), value: r2types::SymbolicVmValueExpr::Const(3), exact: true, + evidence: r2types::SymbolicSemanticEvidence::exact(), + confidence: r2types::SymbolicSemanticConfidence::Exact, }), + memory_reads: vec![r2types::SymbolicMemoryCondition { + region: r2types::SymbolicMemoryRegion::Region( + r2types::SymbolicMemoryRegionRef { + id: 1, + kind: r2types::SymbolicMemoryRegionKind::Global, + name: "ram:0x2000".to_string(), + }, + ), + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: r2types::SymbolicSemanticEvidence::likely( + r2types::SymbolicSemanticEvidenceReason::ValueOpaque, + ), + confidence: r2types::SymbolicSemanticConfidence::Likely, + binding: Some("mem:r1:0:1".to_string()), + expr: "vm.sel".to_string(), + value_expr: None, + exact_value: false, + }], + memory_writes: vec![r2types::SymbolicMemoryCondition { + region: r2types::SymbolicMemoryRegion::Region( + r2types::SymbolicMemoryRegionRef { + id: 2, + kind: r2types::SymbolicMemoryRegionKind::Heap, + name: "heap_alloc@1".to_string(), + }, + ), + offset_lo: 4, + offset_hi: 4, + size: 1, + exact_offset: true, + evidence: r2types::SymbolicSemanticEvidence::likely( + r2types::SymbolicSemanticEvidenceReason::ValueOpaque, + ), + confidence: r2types::SymbolicSemanticConfidence::Likely, + binding: Some("mem:r2:4:1".to_string()), + expr: "state".to_string(), + value_expr: Some("state".to_string()), + exact_value: false, + }], + residual_guards: false, + residual_memory_effects: false, exact: false, + evidence: r2types::SymbolicSemanticEvidence::likely( + r2types::SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: r2types::SymbolicSemanticConfidence::Likely, redispatch: false, may_return: false, truncated: false, @@ -4294,7 +4548,12 @@ mod tests { "expected VM semantic summary comment, got:\n{output}" ); assert!( - output.contains("dispatch_header=0x1000") && output.contains("handler 0x1004"), + output.contains("dispatch_header=0x1000") + && output.contains("handler 0x1004") + && output.contains("guards=") + && output.contains("residual_memory=") + && output.contains("read_effects=") + && output.contains("write_effects="), "expected richer VM details in summary comment, got:\n{output}" ); } diff --git a/crates/r2dec/src/structure.rs b/crates/r2dec/src/structure.rs index 647641f..2d5f4f7 100644 --- a/crates/r2dec/src/structure.rs +++ b/crates/r2dec/src/structure.rs @@ -7,7 +7,6 @@ use std::collections::{HashMap, HashSet}; use r2ssa::cfg::BlockTerminator; use r2ssa::{CFGEdge, SSAFunction}; -use r2types::SymbolicReachabilityStatus; use crate::ast::{BinaryOp, CExpr, CStmt, UnaryOp}; use crate::fold::FoldingContext; @@ -427,20 +426,10 @@ impl<'a, 'o> ControlFlowStructurer<'a, 'o> { } fn symbolic_exact_reachable_target(&self, cond_block: u64) -> Option { - let fact = self - .fold_ctx + self.fold_ctx .inputs .symbolic_facts - .branch_fact_for_block(cond_block)?; - match (fact.true_status, fact.false_status) { - (SymbolicReachabilityStatus::Reachable, SymbolicReachabilityStatus::Unreachable) => { - Some(fact.true_target) - } - (SymbolicReachabilityStatus::Unreachable, SymbolicReachabilityStatus::Reachable) => { - Some(fact.false_target) - } - _ => None, - } + .exact_reachable_target_for_block(cond_block) } fn try_structure_symbolic_exact_if( @@ -2243,7 +2232,9 @@ mod tests { use r2il::{ArchSpec, R2ILBlock, R2ILOp, RegisterDef, Varnode}; use r2ssa::SSAFunction; use r2types::{ - SymbolicInterpreterKind, SymbolicSemanticFacts, SymbolicVmStateUpdate, + SymbolicControlFact, SymbolicControlIsland, SymbolicControlIslandKind, + SymbolicInterpreterKind, SymbolicReachabilityStatus, SymbolicSemanticConfidence, + SymbolicSemanticEvidence, SymbolicSemanticFacts, SymbolicVmStateUpdate, SymbolicVmStepSummary, }; use std::collections::BTreeMap; @@ -2891,8 +2882,15 @@ mod tests { expr: "state + 1".to_string(), value: r2types::SymbolicVmValueExpr::Expr("state + 1".to_string()), exact: false, + evidence: r2types::SymbolicSemanticEvidence::heuristic( + r2types::SymbolicSemanticEvidenceReason::ValueOpaque, + ), + confidence: r2types::SymbolicSemanticConfidence::Heuristic, }], )]), + handler_exit_guards: BTreeMap::new(), + handler_memory_read_effects: BTreeMap::new(), + handler_memory_write_effects: BTreeMap::new(), handler_memory_reads: BTreeMap::from([(0x1004, 1)]), handler_memory_writes: BTreeMap::from([(0x1004, 1)]), handler_calls: BTreeMap::from([(0x1004, 0)]), @@ -2906,14 +2904,27 @@ mod tests { case_values: vec![1, 2], region_blocks: vec![0x1004, 0x1008], exit_targets: vec![0x1008], + exit_guards: Vec::new(), state_updates: vec![SymbolicVmStateUpdate { output: "state".to_string(), expr: "state + 1".to_string(), value: r2types::SymbolicVmValueExpr::Expr("state + 1".to_string()), exact: false, + evidence: r2types::SymbolicSemanticEvidence::heuristic( + r2types::SymbolicSemanticEvidenceReason::ValueOpaque, + ), + confidence: r2types::SymbolicSemanticConfidence::Heuristic, }], selector_update: None, + memory_reads: Vec::new(), + memory_writes: Vec::new(), + residual_guards: false, + residual_memory_effects: false, exact: false, + evidence: r2types::SymbolicSemanticEvidence::likely( + r2types::SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: r2types::SymbolicSemanticConfidence::Likely, redispatch: false, may_return: false, truncated: false, @@ -2929,4 +2940,44 @@ mod tests { CExpr::Var("vm.sel".to_string()) ); } + + #[test] + fn symbolic_exact_reachable_target_uses_control_island_fallback() { + let func = function_with_single_block(0x2000); + let mut ctx = FoldingContext::new(64); + ctx.inputs.symbolic_facts = Box::leak(Box::new(SymbolicSemanticFacts { + control_islands: vec![SymbolicControlIsland { + kind: SymbolicControlIslandKind::LargeCfgBranchFrontier, + anchor_block: 0x2000, + frontier_targets: vec![0x2004, 0x2008], + facts: vec![ + SymbolicControlFact { + target: 0x2004, + status: SymbolicReachabilityStatus::Reachable, + condition: Some("x == 0".to_string()), + compiled: None, + evidence: SymbolicSemanticEvidence::exact(), + confidence: SymbolicSemanticConfidence::Exact, + }, + SymbolicControlFact { + target: 0x2008, + status: SymbolicReachabilityStatus::Unreachable, + condition: Some("!(x == 0)".to_string()), + compiled: None, + evidence: SymbolicSemanticEvidence::exact(), + confidence: SymbolicSemanticConfidence::Exact, + }, + ], + evidence: SymbolicSemanticEvidence::exact(), + confidence: SymbolicSemanticConfidence::Exact, + }], + ..SymbolicSemanticFacts::default() + })); + + let structurer = ControlFlowStructurer::new(&func, &ctx); + assert_eq!( + structurer.symbolic_exact_reachable_target(0x2000), + Some(0x2004) + ); + } } diff --git a/crates/r2sleigh-cli/src/main.rs b/crates/r2sleigh-cli/src/main.rs index a304a70..4eb656b 100644 --- a/crates/r2sleigh-cli/src/main.rs +++ b/crates/r2sleigh-cli/src/main.rs @@ -63,7 +63,7 @@ enum Commands { /// Generate a test architecture specification TestArch { - /// Architecture name (x86-64, arm, riscv64, riscv32) + /// Architecture name (x86-64, arm, mips32be, mips32le, riscv64, riscv32) arch: String, /// Output r2il binary file @@ -367,6 +367,26 @@ fn cmd_test_arch(arch: &str, output: Option<&PathBuf>) -> Result<(), String> { println!("Generating ARM test specification..."); create_arm_spec() } + #[cfg(feature = "mips")] + "mips" | "mips32" | "mips32be" | "mipsbe" | "mipseb" => { + println!("Generating MIPS32 big-endian test specification..."); + build_arch_spec( + sleigh_config::processor_mips::SLA_MIPS32BE, + sleigh_config::processor_mips::PSPEC_MIPS32, + "mips32be", + ) + .map_err(|e| e.to_string())? + } + #[cfg(feature = "mips")] + "mipsel" | "mips32le" | "mips32el" => { + println!("Generating MIPS32 little-endian test specification..."); + build_arch_spec( + sleigh_config::processor_mips::SLA_MIPS32LE, + sleigh_config::processor_mips::PSPEC_MIPS32, + "mips32le", + ) + .map_err(|e| e.to_string())? + } "riscv64" | "rv64" | "rv64gc" => { println!("Generating RISC-V RV64 test specification..."); create_riscv64_spec() @@ -377,7 +397,7 @@ fn cmd_test_arch(arch: &str, output: Option<&PathBuf>) -> Result<(), String> { } _ => { return Err(format!( - "Unknown architecture: {}. Supported: x86-64, arm, riscv64, riscv32", + "Unknown architecture: {}. Supported: x86-64, arm, mips32be, mips32le, riscv64, riscv32", arch )); } @@ -676,6 +696,74 @@ fn get_disassembler_with_spec(arch: &str) -> Result<(Disassembler, r2il::ArchSpe disasm.set_userop_map(userop_map_for_arch("arm")); Ok((disasm, spec)) } + #[cfg(feature = "mips")] + "mips" | "mips32" | "mips32be" | "mipsbe" | "mipseb" => { + let spec = build_arch_spec( + sleigh_config::processor_mips::SLA_MIPS32BE, + sleigh_config::processor_mips::PSPEC_MIPS32, + "mips32be", + ) + .map_err(|e| e.to_string())?; + let mut disasm = Disassembler::from_sla( + sleigh_config::processor_mips::SLA_MIPS32BE, + sleigh_config::processor_mips::PSPEC_MIPS32, + "mips32be", + ) + .map_err(|e| e.to_string())?; + disasm.set_userop_map(userop_map_for_arch("mips32be")); + Ok((disasm, spec)) + } + #[cfg(feature = "mips")] + "mipsel" | "mips32le" | "mips32el" => { + let spec = build_arch_spec( + sleigh_config::processor_mips::SLA_MIPS32LE, + sleigh_config::processor_mips::PSPEC_MIPS32, + "mips32le", + ) + .map_err(|e| e.to_string())?; + let mut disasm = Disassembler::from_sla( + sleigh_config::processor_mips::SLA_MIPS32LE, + sleigh_config::processor_mips::PSPEC_MIPS32, + "mips32le", + ) + .map_err(|e| e.to_string())?; + disasm.set_userop_map(userop_map_for_arch("mips32le")); + Ok((disasm, spec)) + } + #[cfg(feature = "mips")] + "mips64" | "mips64be" => { + let spec = build_arch_spec( + sleigh_config::processor_mips::SLA_MIPS64BE, + sleigh_config::processor_mips::PSPEC_MIPS64, + "mips64be", + ) + .map_err(|e| e.to_string())?; + let mut disasm = Disassembler::from_sla( + sleigh_config::processor_mips::SLA_MIPS64BE, + sleigh_config::processor_mips::PSPEC_MIPS64, + "mips64be", + ) + .map_err(|e| e.to_string())?; + disasm.set_userop_map(userop_map_for_arch("mips64be")); + Ok((disasm, spec)) + } + #[cfg(feature = "mips")] + "mips64el" | "mips64le" => { + let spec = build_arch_spec( + sleigh_config::processor_mips::SLA_MIPS64LE, + sleigh_config::processor_mips::PSPEC_MIPS64, + "mips64le", + ) + .map_err(|e| e.to_string())?; + let mut disasm = Disassembler::from_sla( + sleigh_config::processor_mips::SLA_MIPS64LE, + sleigh_config::processor_mips::PSPEC_MIPS64, + "mips64le", + ) + .map_err(|e| e.to_string())?; + disasm.set_userop_map(userop_map_for_arch("mips64le")); + Ok((disasm, spec)) + } #[cfg(feature = "riscv")] "riscv64" | "rv64" | "rv64gc" => { let spec = build_arch_spec( @@ -716,12 +804,14 @@ fn get_disassembler_with_spec(arch: &str) -> Result<(Disassembler, r2il::ArchSpe supported.extend(["x86-64", "x86"]); #[cfg(feature = "arm")] supported.push("arm"); + #[cfg(feature = "mips")] + supported.extend(["mips32be", "mips32le", "mips64be", "mips64le"]); #[cfg(feature = "riscv")] supported.extend(["riscv64", "riscv32"]); if supported.is_empty() { Err( - "No architectures enabled. Build with --features x86, arm, or riscv" + "No architectures enabled. Build with --features x86, arm, mips, or riscv" .to_string(), ) } else { diff --git a/crates/r2sleigh-lift/src/userops.rs b/crates/r2sleigh-lift/src/userops.rs index d15e22e..55eb9c5 100644 --- a/crates/r2sleigh-lift/src/userops.rs +++ b/crates/r2sleigh-lift/src/userops.rs @@ -12,6 +12,10 @@ fn arch_to_slaspec(arch: &str) -> Option<(&'static str, &'static str)> { "x86" | "x86-32" | "i386" | "i686" => Some(("x86", "x86.slaspec")), "arm" | "arm32" | "arm-le" => Some(("ARM", "ARM8_le.slaspec")), "aarch64" | "arm64" | "arm64e" => Some(("AARCH64", "AARCH64_AppleSilicon.slaspec")), + "mips" | "mips32" | "mips32be" | "mipsbe" | "mipseb" => Some(("MIPS", "mips32be.slaspec")), + "mipsel" | "mips32le" | "mips32el" => Some(("MIPS", "mips32le.slaspec")), + "mips64" | "mips64be" => Some(("MIPS", "mips64be.slaspec")), + "mips64el" | "mips64le" => Some(("MIPS", "mips64le.slaspec")), "riscv64" | "rv64" | "rv64gc" => Some(("RISCV", "riscv.lp64d.slaspec")), "riscv32" | "rv32" | "rv32gc" => Some(("RISCV", "riscv.ilp32d.slaspec")), _ => None, diff --git a/crates/r2sym/benches/symex_hotpaths.rs b/crates/r2sym/benches/symex_hotpaths.rs index 20e3a7a..2b85c74 100644 --- a/crates/r2sym/benches/symex_hotpaths.rs +++ b/crates/r2sym/benches/symex_hotpaths.rs @@ -1,4 +1,4 @@ -use criterion::{Criterion, black_box, criterion_group, criterion_main}; +use criterion::{BatchSize, Criterion, black_box, criterion_group, criterion_main}; use r2il::{ArchSpec, R2ILBlock, R2ILOp, RegisterDef, SpaceId, Varnode}; use r2ssa::SsaArtifact; use r2sym::path::ExploreStrategy; @@ -7,6 +7,7 @@ use r2sym::{ SummaryRegistry, SymSolver, SymState, SymValue, }; use z3::Context; +use z3::ast::BV; const RAX: u64 = 0; const RBX: u64 = 8; @@ -17,6 +18,10 @@ const RDX: u64 = 72; const TMP0: u64 = 0x80; const TMP1: u64 = 0x88; +fn leaked_ctx() -> &'static Context { + Box::leak(Box::new(Context::thread_local())) +} + fn make_reg(offset: u64, size: u32) -> Varnode { Varnode { space: SpaceId::Register, @@ -301,7 +306,7 @@ fn build_fd_spec_function(arch: &ArchSpec) -> SsaArtifact { } fn bench_solver_sat_cache(c: &mut Criterion) { - c.bench_function("r2sym/solver_is_sat_cached", |b| { + c.bench_function("r2sym/solver_is_sat_cached_cold", |b| { b.iter(|| { let ctx = Context::thread_local(); let solver = SymSolver::new(&ctx); @@ -315,6 +320,280 @@ fn bench_solver_sat_cache(c: &mut Criterion) { black_box(solver.stats()) }); }); + + c.bench_function("r2sym/solver_is_sat_cached_hot", |b| { + b.iter_batched( + || { + let ctx = leaked_ctx(); + let solver = SymSolver::new(ctx); + let mut state = SymState::new(ctx, 0x1000); + state.make_symbolic("x", 32); + let x = state.get_register("x"); + state.add_true_constraint(&x.ult(ctx, &SymValue::concrete(5, 32))); + black_box(solver.is_sat(&state)); + (solver, state) + }, + |(solver, state)| { + black_box(solver.is_sat(&state)); + black_box(solver.stats()) + }, + BatchSize::SmallInput, + ); + }); +} + +fn bench_solver_prefix_reuse(c: &mut Criterion) { + c.bench_function("r2sym/solver_prefix_reuse_cold", |b| { + b.iter(|| { + let ctx = Context::thread_local(); + let solver = SymSolver::new(&ctx); + + let mut root = SymState::new(&ctx, 0x1000); + root.make_symbolic("x", 32); + root.make_symbolic("y", 32); + + let x = root.get_register("x"); + let y = root.get_register("y"); + + let mut left = root.fork(); + left.add_true_constraint(&x.ult(&ctx, &SymValue::concrete(20, 32))); + + let mut left_deep = left.fork(); + left_deep.add_true_constraint(&y.eq(&ctx, &SymValue::concrete(7, 32))); + + let mut right = root.fork(); + right.add_true_constraint(&x.eq(&ctx, &SymValue::concrete(99, 32))); + + black_box(solver.is_sat(&left_deep)); + black_box(solver.is_sat(&left)); + black_box(solver.is_sat(&right)); + black_box(solver.stats()) + }); + }); + + c.bench_function("r2sym/solver_prefix_reuse_hot", |b| { + b.iter_batched( + || { + let ctx = leaked_ctx(); + let solver = SymSolver::new(ctx); + + let mut root = SymState::new(ctx, 0x1000); + root.make_symbolic("x", 32); + root.make_symbolic("y", 32); + + let x = root.get_register("x"); + let y = root.get_register("y"); + + let mut left = root.fork(); + left.add_true_constraint(&x.ult(ctx, &SymValue::concrete(20, 32))); + + let mut left_deep = left.fork(); + left_deep.add_true_constraint(&y.eq(ctx, &SymValue::concrete(7, 32))); + + let mut right = root.fork(); + right.add_true_constraint(&x.eq(ctx, &SymValue::concrete(99, 32))); + (solver, left_deep, left, right) + }, + |(solver, left_deep, left, right)| { + black_box(solver.is_sat(&left_deep)); + black_box(solver.is_sat(&left)); + black_box(solver.is_sat(&right)); + black_box(solver.stats()) + }, + BatchSize::SmallInput, + ); + }); +} + +fn bench_solver_sliced_find_value(c: &mut Criterion) { + c.bench_function("r2sym/solver_sliced_find_value_cold", |b| { + b.iter(|| { + let ctx = Context::thread_local(); + let solver = SymSolver::new(&ctx); + let mut state = SymState::new(&ctx, 0x1000); + + for idx in 0..24 { + let reg = format!("u{idx}"); + let sym = format!("sym_u{idx}"); + state.make_symbolic_named(®, &sym, 32); + let value = state.get_register(®); + state.add_true_constraint(&value.eq(&ctx, &SymValue::concrete(idx as u64, 32))); + } + + state.make_symbolic("y", 32); + let y = state.get_register("y"); + let y_is_seven = y.to_bv(&ctx).eq(BV::from_u64(7, 32)); + + black_box(solver.find_value(&state, &y, &y_is_seven)) + }); + }); + + c.bench_function("r2sym/solver_sliced_find_value_hot", |b| { + b.iter_batched( + || { + let ctx = leaked_ctx(); + let solver = SymSolver::new(ctx); + let mut state = SymState::new(ctx, 0x1000); + + for idx in 0..24 { + let reg = format!("u{idx}"); + let sym = format!("sym_u{idx}"); + state.make_symbolic_named(®, &sym, 32); + let value = state.get_register(®); + state.add_true_constraint(&value.eq(ctx, &SymValue::concrete(idx as u64, 32))); + } + + state.make_symbolic("y", 32); + let y = state.get_register("y"); + let y_is_seven = y.to_bv(ctx).eq(BV::from_u64(7, 32)); + (solver, state, y, y_is_seven) + }, + |(solver, state, y, y_is_seven)| black_box(solver.find_value(&state, &y, &y_is_seven)), + BatchSize::SmallInput, + ); + }); +} + +fn bench_solver_small_connected_query(c: &mut Criterion) { + c.bench_function("r2sym/solver_small_connected_query_hot", |b| { + b.iter_batched( + || { + let ctx = leaked_ctx(); + let solver = SymSolver::new(ctx); + let mut state = SymState::new(ctx, 0x1000); + state.make_symbolic("x", 32); + let x = state.get_register("x"); + state.constrain_range(&x, 3, 9); + let x_is_seven = x.to_bv(ctx).eq(BV::from_u64(7, 32)); + (solver, state, x, x_is_seven) + }, + |(solver, state, x, x_is_seven)| black_box(solver.find_value(&state, &x, &x_is_seven)), + BatchSize::SmallInput, + ); + }); +} + +fn bench_solver_partitioned_is_sat(c: &mut Criterion) { + c.bench_function("r2sym/solver_partitioned_is_sat_cold", |b| { + b.iter(|| { + let ctx = Context::thread_local(); + let solver = SymSolver::new(&ctx); + let mut state = SymState::new(&ctx, 0x1000); + + for idx in 0..24 { + let reg = format!("p{idx}"); + let sym = format!("sym_p{idx}"); + state.make_symbolic_named(®, &sym, 32); + let value = state.get_register(®); + state.add_true_constraint(&value.eq(&ctx, &SymValue::concrete(idx as u64, 32))); + } + + state.make_symbolic("x", 32); + let x = state.get_register("x"); + state.add_true_constraint(&x.eq(&ctx, &SymValue::concrete(1, 32))); + state.add_true_constraint(&x.eq(&ctx, &SymValue::concrete(2, 32))); + + black_box(solver.is_sat(&state)); + black_box(solver.stats()) + }); + }); + + c.bench_function("r2sym/solver_partitioned_is_sat_hot", |b| { + b.iter_batched( + || { + let ctx = leaked_ctx(); + let solver = SymSolver::new(ctx); + let mut state = SymState::new(ctx, 0x1000); + + for idx in 0..24 { + let reg = format!("p{idx}"); + let sym = format!("sym_p{idx}"); + state.make_symbolic_named(®, &sym, 32); + let value = state.get_register(®); + state.add_true_constraint(&value.eq(ctx, &SymValue::concrete(idx as u64, 32))); + } + + state.make_symbolic("x", 32); + let x = state.get_register("x"); + state.add_true_constraint(&x.eq(ctx, &SymValue::concrete(1, 32))); + state.add_true_constraint(&x.eq(ctx, &SymValue::concrete(2, 32))); + (solver, state) + }, + |(solver, state)| { + black_box(solver.is_sat(&state)); + black_box(solver.stats()) + }, + BatchSize::SmallInput, + ); + }); + + c.bench_function("r2sym/solver_partitioned_prefilter_unsat_hot", |b| { + b.iter_batched( + || { + let ctx = leaked_ctx(); + let solver = SymSolver::new(ctx); + let mut state = SymState::new(ctx, 0x1000); + state.make_symbolic("x", 32); + let x = state.get_register("x"); + state.constrain_range(&x, 3, 9); + let zero = x.to_bv(ctx).eq(BV::from_u64(0, 32)); + (solver, state, zero) + }, + |(solver, state, zero)| black_box(solver.sat_with_constraint(&state, &zero)), + BatchSize::SmallInput, + ); + }); +} + +fn bench_solver_cursor_fact_reuse(c: &mut Criterion) { + c.bench_function("r2sym/solver_cursor_fact_reuse_hot", |b| { + b.iter_batched( + || { + let ctx = leaked_ctx(); + let solver = SymSolver::new(ctx); + let mut base = SymState::new(ctx, 0x1000); + + for idx in 0..6 { + let reg = format!("p{idx}"); + let sym = format!("sym_p{idx}"); + base.make_symbolic_named(®, &sym, 32); + let value = base.get_register(®); + base.add_true_constraint(&value.eq(ctx, &SymValue::concrete(idx as u64, 32))); + } + + let prefix_value = base.get_register("p0"); + let prefix_eq = prefix_value.to_bv(ctx).eq(BV::from_u64(0, 32)); + black_box(solver.find_value(&base, &prefix_value, &prefix_eq)); + + let mut child = base.fork(); + child.make_symbolic("x", 32); + let x = child.get_register("x"); + child.constrain_range(&x, 3, 9); + let x_is_seven = x.to_bv(ctx).eq(BV::from_u64(7, 32)); + (solver, child, x, x_is_seven) + }, + |(solver, child, x, x_is_seven)| black_box(solver.find_value(&child, &x, &x_is_seven)), + BatchSize::SmallInput, + ); + }); +} + +fn bench_value_normalization_identities(c: &mut Criterion) { + c.bench_function("r2sym/value_normalization_identities", |b| { + b.iter(|| { + let ctx = Context::thread_local(); + let x = SymValue::new_symbolic(&ctx, "x", 32); + let zero = SymValue::concrete(0, 32); + let one = SymValue::concrete(1, 32); + let all_ones = SymValue::concrete(u32::MAX as u64, 32); + + black_box(x.add(&ctx, &zero)); + black_box(x.mul(&ctx, &one)); + black_box(x.and(&ctx, &all_ones)); + black_box(x.xor(&ctx, &x)); + black_box(x.eq(&ctx, &x)); + }); + }); } fn bench_explore_symbolic_branching(c: &mut Criterion) { @@ -439,9 +718,15 @@ fn bench_run_spec_symbolic_fd_input(c: &mut Criterion) { criterion_group!( symex_hotpaths, bench_solver_sat_cache, + bench_solver_prefix_reuse, + bench_solver_sliced_find_value, + bench_solver_partitioned_is_sat, + bench_value_normalization_identities, bench_explore_symbolic_branching, bench_explore_branch_tree, bench_explore_same_pc_merge, - bench_run_spec_symbolic_fd_input + bench_run_spec_symbolic_fd_input, + bench_solver_small_connected_query, + bench_solver_cursor_fact_reuse ); criterion_main!(symex_hotpaths); diff --git a/crates/r2sym/src/backward.rs b/crates/r2sym/src/backward.rs index 2ea9dd6..7eaa4c9 100644 --- a/crates/r2sym/src/backward.rs +++ b/crates/r2sym/src/backward.rs @@ -11,7 +11,10 @@ use z3::{SatResult as Z3SatResult, Solver}; use crate::sim::{CallConv, DerivedFunctionSummary}; use crate::state::SymState; use crate::value::SymValue; -use crate::{MemoryRegionId, MemoryRegionKind}; +use crate::{ + MemoryRegionId, MemoryRegionKind, SemanticConfidence, SemanticEvidence, + SemanticEvidenceCoverage, SemanticEvidenceProvenance, SemanticEvidenceReason, +}; const DEFAULT_REVERSE_PATH_LIMIT: usize = 16; const DEFAULT_MAX_NORMALIZED_OFFSETS: usize = 8; @@ -37,6 +40,33 @@ pub struct BackwardConditionSummary { pub total_paths: usize, } +impl BackwardConditionSummary { + pub fn evidence(&self) -> SemanticEvidence { + match self.precision { + BackwardConditionPrecision::Exact => SemanticEvidence::exact(), + BackwardConditionPrecision::OverApprox => { + SemanticEvidence::likely(SemanticEvidenceReason::PartialPathCoverage) + .with_provenance(SemanticEvidenceProvenance::Normalized) + } + BackwardConditionPrecision::ResidualSearchRequired => { + if self.supported_paths > 0 { + SemanticEvidence::heuristic(SemanticEvidenceReason::ResidualSearchRequired) + .with_coverage(SemanticEvidenceCoverage::Bounded) + } else { + SemanticEvidence::residual(SemanticEvidenceReason::ResidualSearchRequired) + } + } + BackwardConditionPrecision::Unsupported => { + SemanticEvidence::residual(SemanticEvidenceReason::ValueOpaque) + } + } + } + + pub fn confidence(&self) -> SemanticConfidence { + self.evidence().tier + } +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct BackwardMemoryCondition { pub region: BackwardMemoryRegion, @@ -47,6 +77,25 @@ pub struct BackwardMemoryCondition { pub expr: String, } +impl BackwardMemoryCondition { + pub fn evidence(&self) -> SemanticEvidence { + if self.exact_offset { + SemanticEvidence::exact() + } else if self.offset_hi >= self.offset_lo && (self.offset_hi - self.offset_lo) <= 8 { + SemanticEvidence::likely(SemanticEvidenceReason::DerivedFromRanking) + .with_coverage(SemanticEvidenceCoverage::Bounded) + .with_provenance(SemanticEvidenceProvenance::Normalized) + } else { + SemanticEvidence::heuristic(SemanticEvidenceReason::AliasAmbiguity) + .with_coverage(SemanticEvidenceCoverage::Bounded) + } + } + + pub fn confidence(&self) -> SemanticConfidence { + self.evidence().tier + } +} + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub struct BackwardRegionRef { pub id: MemoryRegionId, @@ -119,6 +168,22 @@ enum SummaryLocationMatch { Residual, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct LocationGroupRank { + region_rank: u8, + inexact_offset: bool, + span: u64, + offset_count: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +struct LocationGroupTieBreak { + region_discriminant: u8, + region_id: u32, + arg_index: usize, + min_offset: i64, +} + impl<'a, 'ctx> ValueTranslator<'a, 'ctx> { fn new( func: &'a SsaArtifact, @@ -438,7 +503,16 @@ impl<'a, 'ctx> ValueTranslator<'a, 'ctx> { } let fallback_summary_locations = summary_memory_locations(call_ctx, addr); let substitutions = build_call_substitutions(self.state, call_ctx); - for location_group in group_normalized_locations(&actual_locations) { + let location_groups = group_normalized_locations(&actual_locations); + let candidate_groups = if location_groups.len() <= 1 { + location_groups + } else if let Some(best_group) = select_best_location_group(location_groups) { + vec![best_group] + } else { + self.memory_residual_fallbacks += 1; + return None; + }; + for location_group in candidate_groups { let summary_group = match self.summary_match_locations(call_ctx, &location_group) { SummaryLocationMatch::Match(group) => group, SummaryLocationMatch::NoMatch => { @@ -512,19 +586,23 @@ impl<'a, 'ctx> ValueTranslator<'a, 'ctx> { { return SummaryLocationMatch::Match(actual_group.to_vec()); } - if actual_group.len() != 1 { + let mut actual_regions = actual_group + .iter() + .filter_map(|location| match &location.region { + BackwardMemoryRegion::Region(region) => Some(region.clone()), + BackwardMemoryRegion::Argument { .. } => None, + }) + .collect::>(); + if actual_regions.len() != 1 { return SummaryLocationMatch::Residual; } - let actual = &actual_group[0]; - let BackwardMemoryRegion::Region(actual_region) = &actual.region else { - return SummaryLocationMatch::NoMatch; - }; + let actual_region = actual_regions.pop_first().expect("single region"); if actual_region.kind == MemoryRegionKind::EscapedUnknown { return SummaryLocationMatch::NoMatch; } - let mut translated = BTreeSet::new(); + let mut translated = BTreeMap::>::new(); let pointer_args = summary_pointer_arg_indices(call_ctx); for (arg_index, base) in call_ctx.args.iter().enumerate() { if !pointer_args.is_empty() && !pointer_args.contains(&arg_index) { @@ -540,20 +618,28 @@ impl<'a, 'ctx> ValueTranslator<'a, 'ctx> { if base_region.kind == MemoryRegionKind::EscapedUnknown { continue; } - if base_region != actual_region { + if *base_region != actual_region { continue; } - translated.insert(NormalizedMemoryLocation { - region: BackwardMemoryRegion::Argument { index: arg_index }, - offset: actual.offset.saturating_sub(base_location.offset), - }); + let offsets = translated.entry(arg_index).or_default(); + for actual in actual_group { + offsets.insert(actual.offset.saturating_sub(base_location.offset)); + } } } match translated.len() { 0 => SummaryLocationMatch::NoMatch, - 1 => SummaryLocationMatch::Match(translated.into_iter().collect()), - _ => SummaryLocationMatch::Residual, + 1 => SummaryLocationMatch::Match(translated_arg_locations( + translated + .into_iter() + .next() + .expect("single translated arg"), + )), + _ => select_best_translated_arg(translated) + .map(translated_arg_locations) + .map(SummaryLocationMatch::Match) + .unwrap_or(SummaryLocationMatch::Residual), } } @@ -1477,6 +1563,129 @@ fn summary_pointer_arg_indices<'ctx>(call_ctx: &CallTransformContext<'ctx>) -> B indices } +fn translated_arg_locations( + (arg_index, offsets): (usize, BTreeSet), +) -> Vec { + offsets + .into_iter() + .map(|offset| NormalizedMemoryLocation { + region: BackwardMemoryRegion::Argument { index: arg_index }, + offset, + }) + .collect() +} + +fn memory_region_rank(region: &BackwardMemoryRegion) -> u8 { + match region { + BackwardMemoryRegion::Argument { .. } => 0, + BackwardMemoryRegion::Region(region) => match region.kind { + MemoryRegionKind::Stack => 1, + MemoryRegionKind::Global => 2, + MemoryRegionKind::Replay => 3, + MemoryRegionKind::Input => 4, + MemoryRegionKind::Heap => 5, + MemoryRegionKind::EscapedUnknown => 6, + }, + } +} + +fn offsets_span(offsets: &BTreeSet) -> u64 { + match (offsets.first().copied(), offsets.last().copied()) { + (Some(lo), Some(hi)) => hi.saturating_sub(lo).unsigned_abs(), + _ => 0, + } +} + +fn location_group_rank(group: &[NormalizedMemoryLocation]) -> Option { + let region = group.first()?.region.clone(); + let offsets = group + .iter() + .map(|location| location.offset) + .collect::>(); + let span = offsets_span(&offsets); + Some(LocationGroupRank { + region_rank: memory_region_rank(®ion), + inexact_offset: span != 0, + span, + offset_count: offsets.len(), + }) +} + +fn location_group_tie_break(group: &[NormalizedMemoryLocation]) -> Option { + let region = group.first()?.region.clone(); + let min_offset = group + .iter() + .map(|location| location.offset) + .min() + .unwrap_or(0); + Some(match region { + BackwardMemoryRegion::Argument { index } => LocationGroupTieBreak { + region_discriminant: 0, + region_id: 0, + arg_index: index, + min_offset, + }, + BackwardMemoryRegion::Region(region) => LocationGroupTieBreak { + region_discriminant: 1, + region_id: region.id.0, + arg_index: 0, + min_offset, + }, + }) +} + +fn select_best_location_group( + groups: Vec>, +) -> Option> { + if groups.len() <= 1 { + return groups.into_iter().next(); + } + + let mut ranked = groups + .into_iter() + .filter_map(|group| { + Some(( + location_group_rank(&group)?, + location_group_tie_break(&group)?, + group, + )) + }) + .collect::>(); + ranked.sort_by(|left, right| left.0.cmp(&right.0).then_with(|| left.1.cmp(&right.1))); + if ranked.len() > 1 && ranked[0].0 == ranked[1].0 { + None + } else { + ranked.into_iter().next().map(|(_, _, group)| group) + } +} + +fn translated_offsets_rank(offsets: &BTreeSet) -> (bool, u64, usize) { + let span = offsets_span(offsets); + (span != 0, span, offsets.len()) +} + +fn select_best_translated_arg( + translated: BTreeMap>, +) -> Option<(usize, BTreeSet)> { + if translated.len() <= 1 { + return translated.into_iter().next(); + } + + let mut ranked = translated + .into_iter() + .map(|(arg_index, offsets)| (translated_offsets_rank(&offsets), arg_index, offsets)) + .collect::>(); + ranked.sort_by(|left, right| left.0.cmp(&right.0).then_with(|| left.1.cmp(&right.1))); + if ranked.len() > 1 && ranked[0].0 == ranked[1].0 { + None + } else { + ranked + .into_iter() + .next() + .map(|(_, arg_index, offsets)| (arg_index, offsets)) + } +} + fn group_normalized_locations( locations: &[NormalizedMemoryLocation], ) -> Vec> { @@ -1840,4 +2049,47 @@ mod tests { ); assert!(compiled.summary.simplified.contains("1337") || !compiled.summary.terms.is_empty()); } + + #[test] + fn select_best_location_group_prefers_stable_exact_region() { + let global_group = vec![NormalizedMemoryLocation { + region: BackwardMemoryRegion::Region(BackwardRegionRef { + id: MemoryRegionId(1), + kind: MemoryRegionKind::Global, + name: "global".to_string(), + }), + offset: 4, + }]; + let heap_group = vec![ + NormalizedMemoryLocation { + region: BackwardMemoryRegion::Region(BackwardRegionRef { + id: MemoryRegionId(2), + kind: MemoryRegionKind::Heap, + name: "heap".to_string(), + }), + offset: 4, + }, + NormalizedMemoryLocation { + region: BackwardMemoryRegion::Region(BackwardRegionRef { + id: MemoryRegionId(2), + kind: MemoryRegionKind::Heap, + name: "heap".to_string(), + }), + offset: 8, + }, + ]; + + let best = + select_best_location_group(vec![heap_group, global_group.clone()]).expect("best group"); + assert_eq!(best, global_group); + } + + #[test] + fn select_best_translated_arg_ties_remain_residual() { + let translated = BTreeMap::from([ + (0usize, BTreeSet::from([0i64])), + (1usize, BTreeSet::from([0i64])), + ]); + assert!(select_best_translated_arg(translated).is_none()); + } } diff --git a/crates/r2sym/src/lib.rs b/crates/r2sym/src/lib.rs index 29c18c6..66b6eb0 100644 --- a/crates/r2sym/src/lib.rs +++ b/crates/r2sym/src/lib.rs @@ -72,10 +72,14 @@ pub use replay::{ pub use runtime::{seed_default_state_for_arch, seed_memory_regions_for_arch}; pub use semantics::{ CompiledFunctionSemantics, CompiledSemanticArtifact, CompiledSemanticMode, - InterpreterDispatchSummary, InterpreterKind, ResidualReason, SemanticCapability, SemanticMode, - SliceClass, SymbolicBranchFact, SymbolicFunctionFactDiagnostics, SymbolicFunctionFacts, - SymbolicReachabilityStatus, VmBinaryOp, VmStateUpdate, VmStepSummary, VmTransferArm, VmUnaryOp, - VmValueExpr, collect_symbolic_function_facts, collect_symbolic_function_facts_with_scope, + InterpreterDispatchSummary, InterpreterKind, ResidualReason, SemanticCapability, + SemanticConfidence, SemanticEvidence, SemanticEvidenceAmbiguity, SemanticEvidenceCoverage, + SemanticEvidenceProvenance, SemanticEvidenceReason, SemanticEvidenceSoundness, SemanticMode, + SliceClass, SymbolicBranchFact, SymbolicControlFact, SymbolicControlIsland, + SymbolicControlIslandKind, SymbolicFunctionFactDiagnostics, SymbolicFunctionFacts, + SymbolicReachabilityStatus, VmBinaryOp, VmGuardCondition, VmGuardedExit, VmMemoryCondition, + VmMemoryRegionRef, VmStateUpdate, VmStepSummary, VmTransferArm, VmUnaryOp, VmValueExpr, + collect_symbolic_function_facts, collect_symbolic_function_facts_with_scope, compile_function_semantics_with_scope, compile_semantic_artifact_with_scope, stable_scope_hash, }; pub use sim::{ diff --git a/crates/r2sym/src/memory.rs b/crates/r2sym/src/memory.rs index 63e252f..63af6f7 100644 --- a/crates/r2sym/src/memory.rs +++ b/crates/r2sym/src/memory.rs @@ -4,11 +4,14 @@ //! explicit stack/global/input/heap/replay/unknown regions. The public API //! remains address-shaped for compatibility with the executor and summaries. +use std::collections::hash_map::DefaultHasher; use std::collections::{BTreeMap, BTreeSet}; +use std::hash::{Hash, Hasher}; +use std::rc::Rc; use serde::{Deserialize, Serialize}; use z3::ast::{Ast, BV, Bool}; -use z3::{Context, SatResult, Solver}; +use z3::{Context, DeclKind, SatResult, Solver}; use crate::value::SymValue; @@ -48,14 +51,15 @@ pub struct ResolvedPointerSet { } #[derive(Clone)] -enum RegionWriteTarget<'ctx> { - Offset(u64), - Address(SymValue<'ctx>), +struct RegionOffsetWrite<'ctx> { + offset: u64, + value: SymValue<'ctx>, + size: u32, } #[derive(Clone)] -struct RegionWrite<'ctx> { - target: RegionWriteTarget<'ctx>, +struct RegionUnresolvedWrite<'ctx> { + addr: SymValue<'ctx>, value: SymValue<'ctx>, size: u32, } @@ -64,7 +68,9 @@ struct RegionWrite<'ctx> { struct MemoryRegion<'ctx> { def: SymbolicMemoryRegionDef, concrete: BTreeMap, - symbolic_writes: Vec>, + offset_writes: Vec>, + latest_write_by_offset: BTreeMap, + unresolved_writes: Vec>, } impl<'ctx> MemoryRegion<'ctx> { @@ -84,7 +90,9 @@ impl<'ctx> MemoryRegion<'ctx> { extent, }, concrete: BTreeMap::new(), - symbolic_writes: Vec::new(), + offset_writes: Vec::new(), + latest_write_by_offset: BTreeMap::new(), + unresolved_writes: Vec::new(), } } @@ -123,20 +131,49 @@ impl<'ctx> MemoryRegion<'ctx> { fn merge_offsets(&self) -> BTreeSet { let mut offsets = BTreeSet::new(); offsets.extend(self.concrete.keys().copied()); - for write in &self.symbolic_writes { - if let RegionWriteTarget::Offset(base) = write.target { - for index in 0..write.size { - offsets.insert(base.wrapping_add(index as u64)); - } - } - } + offsets.extend(self.latest_write_by_offset.keys().copied()); offsets } - fn unresolved_symbolic_writes(&self) -> impl Iterator> { - self.symbolic_writes - .iter() - .filter(|write| matches!(write.target, RegionWriteTarget::Address(_))) + fn unresolved_symbolic_writes(&self) -> impl Iterator> { + self.unresolved_writes.iter() + } + + fn read_byte_from_offset_write( + &self, + ctx: &'ctx Context, + byte_offset: u64, + ) -> Option> { + let write_index = *self.latest_write_by_offset.get(&byte_offset)?; + let write = self.offset_writes.get(write_index)?; + let relative = byte_offset.checked_sub(write.offset)?; + if relative >= write.size as u64 { + return None; + } + let value = adjust_bits(ctx, &write.value, write.size * 8); + let low_bit = (relative * 8) as u32; + let high_bit = low_bit + 7; + Some(value.extract(ctx, high_bit, low_bit)) + } + + fn default_read_byte( + &self, + ctx: &'ctx Context, + byte_offset: u64, + default_symbolic: bool, + default_name: &str, + ) -> SymValue<'ctx> { + if let Some(byte) = self.read_byte_from_offset_write(ctx, byte_offset) { + return byte; + } + if let Some(byte) = self.concrete.get(&byte_offset) { + return SymValue::concrete(*byte as u64, 8); + } + if default_symbolic { + SymValue::new_symbolic(ctx, &format!("{default_name}_b{byte_offset:x}"), 8) + } else { + SymValue::concrete(0, 8) + } } fn read_offset( @@ -147,61 +184,50 @@ impl<'ctx> MemoryRegion<'ctx> { default_symbolic: bool, default_name: &str, ) -> SymValue<'ctx> { - for write in self.symbolic_writes.iter().rev() { - let RegionWriteTarget::Offset(base) = write.target else { - continue; - }; - let write_end = base.checked_add(write.size as u64); - let read_end = offset.checked_add(size as u64); - if let (Some(write_end), Some(read_end)) = (write_end, read_end) - && base <= offset - && write_end >= read_end - { - let relative = offset - base; - if relative == 0 && write.size == size { - return adjust_bits(ctx, &write.value, size * 8); - } - let low_bit = (relative * 8) as u32; - let high_bit = low_bit + (size * 8) - 1; - return adjust_bits(ctx, &write.value, write.size * 8) - .extract(ctx, high_bit, low_bit); - } - } - - let mut concrete_bytes = Vec::with_capacity(size as usize); + let mut byte_values = Vec::with_capacity(size as usize); let mut value = 0u64; let mut all_concrete = true; for index in 0..size { let byte_offset = offset.wrapping_add(index as u64); - if let Some(byte) = self.concrete.get(&byte_offset) { - concrete_bytes.push(*byte); + let byte = self.default_read_byte(ctx, byte_offset, default_symbolic, default_name); + if let Some(concrete) = byte.as_concrete() { if index < 8 { - value |= (*byte as u64) << (index * 8); + value |= concrete << (index * 8); } } else { all_concrete = false; - break; } + byte_values.push(byte); } if all_concrete { if size > 8 { - let mut bytes = concrete_bytes.iter().rev(); - let first = bytes.next().copied().unwrap_or(0); - let mut ast = BV::from_u64(first as u64, 8); + let mut bytes = byte_values.iter().rev().filter_map(SymValue::as_concrete); + let first = bytes.next().unwrap_or(0); + let mut ast = BV::from_u64(first, 8); for byte in bytes { - ast = ast.concat(BV::from_u64(*byte as u64, 8)); + ast = ast.concat(BV::from_u64(byte, 8)); } return SymValue::symbolic(ast, size * 8); } return SymValue::concrete(value, size * 8); } - if default_symbolic { - SymValue::new_symbolic(ctx, default_name, size * 8) - } else { - SymValue::concrete(0, size * 8) + let mut bytes = byte_values.iter().rev(); + let Some(first) = bytes.next() else { + return if default_symbolic { + SymValue::new_symbolic(ctx, default_name, size * 8) + } else { + SymValue::concrete(0, size * 8) + }; + }; + let mut ast = first.to_bv(ctx); + let mut taint = first.get_taint(); + for byte in bytes { + ast = ast.concat(byte.to_bv(ctx)); + taint |= byte.get_taint(); } + SymValue::symbolic_tainted(ast, size * 8, taint) } fn write_offset(&mut self, ctx: &'ctx Context, offset: u64, value: &SymValue<'ctx>, size: u32) { @@ -214,27 +240,45 @@ impl<'ctx> MemoryRegion<'ctx> { let byte_value = ((concrete_value >> (index * 8)) & 0xff) as u8; self.concrete.insert(byte_offset, byte_value); } + } else { + for index in 0..size { + self.concrete.remove(&offset.wrapping_add(index as u64)); + } } - self.symbolic_writes.push(RegionWrite { - target: RegionWriteTarget::Offset(offset), + let write_index = self.offset_writes.len(); + self.offset_writes.push(RegionOffsetWrite { + offset, value, size, }); + for index in 0..size { + self.latest_write_by_offset + .insert(offset.wrapping_add(index as u64), write_index); + } } fn push_unresolved_write(&mut self, addr: SymValue<'ctx>, value: SymValue<'ctx>, size: u32) { - self.symbolic_writes.push(RegionWrite { - target: RegionWriteTarget::Address(addr), - value, - size, - }); + self.unresolved_writes + .push(RegionUnresolvedWrite { addr, value, size }); + } + + fn visible_offset_write_indices(&self) -> Vec { + let mut indices = self + .latest_write_by_offset + .values() + .copied() + .collect::>() + .into_iter() + .collect::>(); + indices.sort_unstable(); + indices } } /// A symbolic memory model backed by explicit regions. pub struct SymMemory<'ctx> { ctx: &'ctx Context, - regions: BTreeMap>, + regions: Rc>>, default_symbolic: bool, max_symbolic_targets: usize, next_region_id: u32, @@ -268,7 +312,7 @@ impl<'ctx> SymMemory<'ctx> { Self { ctx, - regions, + regions: Rc::new(regions), default_symbolic, max_symbolic_targets: Self::DEFAULT_MAX_SYMBOLIC_TARGETS, next_region_id: 1, @@ -303,7 +347,7 @@ impl<'ctx> SymMemory<'ctx> { let id = MemoryRegionId(self.next_region_id); self.next_region_id += 1; - self.regions + Rc::make_mut(&mut self.regions) .insert(id, MemoryRegion::new(id, kind, name, base_addr, extent)); id } @@ -320,7 +364,7 @@ impl<'ctx> SymMemory<'ctx> { } pub fn seed_region_bytes(&mut self, region_id: MemoryRegionId, offset: u64, bytes: &[u8]) { - let Some(region) = self.regions.get_mut(®ion_id) else { + let Some(region) = Rc::make_mut(&mut self.regions).get_mut(®ion_id) else { return; }; region.ensure_extent_covers(offset, bytes.len() as u32); @@ -385,53 +429,44 @@ impl<'ctx> SymMemory<'ctx> { self.write_region_pointer(pointer, value, size); } - pub(crate) fn semantic_fingerprint(&self) -> String { - let region_repr = self - .regions - .values() - .map(|region| { - let concrete = region - .concrete - .iter() - .map(|(offset, byte)| format!("{offset:x}:{byte:02x}")) - .collect::>() - .join(","); - let symbolic = region - .symbolic_writes - .iter() - .map(|write| { - let target = match &write.target { - RegionWriteTarget::Offset(offset) => format!("off:{offset:x}"), - RegionWriteTarget::Address(addr) => { - format!("addr:{}", addr.to_bv(self.ctx).simplify()) - } - }; - format!( - "{target}=>{}:{}", - write.value.to_bv(self.ctx).simplify(), - write.size - ) - }) - .collect::>() - .join("|"); - format!( - "{}:{:?}:{}:{:?}:{:?}:c[{}]:s[{}]", - region.def.id.0, - region.def.kind, - region.def.name, - region.def.base_addr, - region.def.extent, - concrete, - symbolic - ) - }) - .collect::>() - .join(";"); + pub(crate) fn semantic_fingerprint(&self) -> u64 { + let mut hasher = DefaultHasher::new(); + self.default_symbolic.hash(&mut hasher); + self.max_symbolic_targets.hash(&mut hasher); + self.next_region_id.hash(&mut hasher); + self.escaped_unknown_region.hash(&mut hasher); + + for region in self.regions.values() { + region.def.id.hash(&mut hasher); + region.def.kind.hash(&mut hasher); + region.def.name.hash(&mut hasher); + region.def.base_addr.hash(&mut hasher); + region.def.extent.hash(&mut hasher); + + for (offset, byte) in ®ion.concrete { + offset.hash(&mut hasher); + byte.hash(&mut hasher); + } - format!( - "default_symbolic={};max_targets={};regions=[{}]", - self.default_symbolic, self.max_symbolic_targets, region_repr - ) + for index in region.visible_offset_write_indices() { + if let Some(write) = region.offset_writes.get(index) { + if write.value.as_concrete().is_some() { + continue; + } + write.offset.hash(&mut hasher); + write.size.hash(&mut hasher); + hash_sym_value(self.ctx, &write.value, &mut hasher); + } + } + + for write in ®ion.unresolved_writes { + write.size.hash(&mut hasher); + hash_sym_value(self.ctx, &write.addr, &mut hasher); + hash_sym_value(self.ctx, &write.value, &mut hasher); + } + } + + hasher.finish() } pub(crate) fn merge_with( @@ -524,14 +559,11 @@ impl<'ctx> SymMemory<'ctx> { let Some(mapped_region) = region_map.get(®ion.def.id).copied() else { continue; }; - let Some(target) = merged.regions.get_mut(&mapped_region) else { + let Some(target) = Rc::make_mut(&mut merged.regions).get_mut(&mapped_region) else { continue; }; for write in region.unresolved_symbolic_writes() { - let RegionWriteTarget::Address(addr) = &write.target else { - continue; - }; - target.push_unresolved_write(addr.clone(), write.value.clone(), write.size); + target.push_unresolved_write(write.addr.clone(), write.value.clone(), write.size); } } @@ -614,7 +646,9 @@ impl<'ctx> SymMemory<'ctx> { let resolved = self.resolve_pointer(addr, size, constraints); if resolved.pointers.is_empty() { - if let Some(region) = self.regions.get_mut(&self.escaped_unknown_region) { + if let Some(region) = + Rc::make_mut(&mut self.regions).get_mut(&self.escaped_unknown_region) + { region.push_unresolved_write(addr.clone(), value, size); } return; @@ -641,7 +675,8 @@ impl<'ctx> SymMemory<'ctx> { } if resolved.truncated - && let Some(region) = self.regions.get_mut(&self.escaped_unknown_region) + && let Some(region) = + Rc::make_mut(&mut self.regions).get_mut(&self.escaped_unknown_region) { region.push_unresolved_write(addr.clone(), value, size); } @@ -690,7 +725,7 @@ impl<'ctx> SymMemory<'ctx> { pub fn symbolic_writes_count(&self) -> usize { self.regions .values() - .map(|region| region.symbolic_writes.len()) + .map(|region| region.offset_writes.len() + region.unresolved_writes.len()) .sum() } @@ -706,9 +741,11 @@ impl<'ctx> SymMemory<'ctx> { } pub fn clear(&mut self) { - for region in self.regions.values_mut() { + for region in Rc::make_mut(&mut self.regions).values_mut() { region.concrete.clear(); - region.symbolic_writes.clear(); + region.offset_writes.clear(); + region.latest_write_by_offset.clear(); + region.unresolved_writes.clear(); } } @@ -760,7 +797,7 @@ impl<'ctx> SymMemory<'ctx> { } fn write_region_pointer(&mut self, pointer: &RegionPointer, value: &SymValue<'ctx>, size: u32) { - let Some(region) = self.regions.get_mut(&pointer.region_id) else { + let Some(region) = Rc::make_mut(&mut self.regions).get_mut(&pointer.region_id) else { return; }; region.write_offset(self.ctx, pointer.offset, value, size); @@ -782,6 +819,25 @@ impl<'ctx> SymMemory<'ctx> { &self, addr: &SymValue<'ctx>, constraints: &[Bool], + ) -> (Vec, bool) { + if let Some(affine) = recognize_affine_pointer(addr) { + let (targets, truncated) = + self.enumerate_symbolic_addresses_sat(&affine.base, constraints); + return ( + targets + .into_iter() + .filter_map(|value| value.checked_add_signed(affine.delta)) + .collect(), + truncated, + ); + } + self.enumerate_symbolic_addresses_sat(addr, constraints) + } + + fn enumerate_symbolic_addresses_sat( + &self, + addr: &SymValue<'ctx>, + constraints: &[Bool], ) -> (Vec, bool) { if self.max_symbolic_targets == 0 { return (Vec::new(), true); @@ -824,15 +880,28 @@ fn compare_region_specificity<'ctx>( left: &MemoryRegion<'ctx>, right: &MemoryRegion<'ctx>, ) -> std::cmp::Ordering { + fn kind_rank(kind: &MemoryRegionKind) -> u8 { + match kind { + MemoryRegionKind::Replay => 0, + MemoryRegionKind::Input => 1, + MemoryRegionKind::Global => 2, + MemoryRegionKind::Heap => 3, + MemoryRegionKind::Stack => 4, + MemoryRegionKind::EscapedUnknown => 5, + } + } + let left_key = ( left.def.extent.is_none(), left.def.extent.unwrap_or(u64::MAX), + kind_rank(&left.def.kind), std::cmp::Reverse(left.def.base_addr.unwrap_or(0)), left.def.id, ); let right_key = ( right.def.extent.is_none(), right.def.extent.unwrap_or(u64::MAX), + kind_rank(&right.def.kind), std::cmp::Reverse(right.def.base_addr.unwrap_or(0)), right.def.id, ); @@ -875,6 +944,12 @@ fn adjust_bits<'ctx>(ctx: &'ctx Context, value: &SymValue<'ctx>, bits: u32) -> S } } +fn hash_sym_value<'ctx, H: Hasher>(ctx: &'ctx Context, value: &SymValue<'ctx>, hasher: &mut H) { + value.bits().hash(hasher); + value.get_taint().hash(hasher); + value.to_bv(ctx).hash(hasher); +} + impl<'ctx> std::fmt::Debug for SymMemory<'ctx> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("SymMemory") @@ -886,6 +961,99 @@ impl<'ctx> std::fmt::Debug for SymMemory<'ctx> { } } +#[derive(Clone)] +struct AffinePointer<'ctx> { + base: SymValue<'ctx>, + delta: i64, +} + +fn recognize_affine_pointer<'ctx>(addr: &SymValue<'ctx>) -> Option> { + let ast = addr.as_ast()?; + let (base, delta) = recognize_affine_bv(ast)?; + Some(AffinePointer { + base: SymValue::symbolic_tainted(base, addr.bits(), addr.get_taint()), + delta, + }) +} + +fn recognize_affine_bv(ast: &BV) -> Option<(BV, i64)> { + if ast.as_u64().is_some() { + return None; + } + + if ast.is_const() && ast.decl().kind() == DeclKind::UNINTERPRETED { + return Some((ast.clone(), 0)); + } + + match ast.decl().kind() { + DeclKind::BADD => { + let children = ast.children(); + if children.len() != 2 { + return None; + } + let left = children[0].as_bv()?; + let right = children[1].as_bv()?; + if let Some(delta) = affine_constant(&right) { + let (base, base_delta) = recognize_affine_bv(&left)?; + return base_delta + .checked_add(delta) + .map(|combined| (widen_affine_base(&base, ast.get_size()), combined)); + } + if let Some(delta) = affine_constant(&left) { + let (base, base_delta) = recognize_affine_bv(&right)?; + return base_delta + .checked_add(delta) + .map(|combined| (widen_affine_base(&base, ast.get_size()), combined)); + } + None + } + DeclKind::BSUB => { + let children = ast.children(); + if children.len() != 2 { + return None; + } + let left = children[0].as_bv()?; + let right = children[1].as_bv()?; + let delta = affine_constant(&right)?; + let (base, base_delta) = recognize_affine_bv(&left)?; + base_delta + .checked_sub(delta) + .map(|combined| (widen_affine_base(&base, ast.get_size()), combined)) + } + DeclKind::ZERO_EXT | DeclKind::SIGN_EXT => { + let child = ast.nth_child(0)?.as_bv()?; + let (base, delta) = recognize_affine_bv(&child)?; + Some((widen_affine_base(&base, ast.get_size()), delta)) + } + _ => None, + } +} + +fn widen_affine_base(base: &BV, target_bits: u32) -> BV { + let base_bits = base.get_size(); + if base_bits == target_bits { + base.clone() + } else if base_bits < target_bits { + base.zero_ext(target_bits - base_bits) + } else { + base.extract(target_bits - 1, 0) + } +} + +fn affine_constant(ast: &BV) -> Option { + let value = ast.as_u64()?; + if let Ok(delta) = i64::try_from(value) { + return Some(delta); + } + let bits = ast.get_size(); + if bits == 0 || bits >= 64 { + let sign_bit = 1u64 << 63; + return (value & sign_bit != 0).then_some(value as i64); + } + let sign_bit = 1u64 << (bits - 1); + (value & sign_bit != 0).then_some(value as i64) +} + #[cfg(test)] mod tests { use super::*; @@ -937,6 +1105,30 @@ mod tests { assert_eq!(resolved.pointers[0].offset, 4); } + #[test] + fn test_region_resolution_prefers_replay_over_global_when_overlapping() { + let ctx = Context::thread_local(); + let mut mem = SymMemory::new(&ctx); + let global = mem.define_region( + MemoryRegionKind::Global, + "globals", + Some(0x4000), + Some(0x20), + ); + let replay = mem.define_region( + MemoryRegionKind::Replay, + "checkpoint", + Some(0x4000), + Some(0x20), + ); + + let resolved = mem.resolve_pointer(&SymValue::concrete(0x4004, 64), 1, &[]); + assert_eq!(resolved.pointers.len(), 1); + assert_eq!(resolved.pointers[0].region_id, replay); + assert_ne!(resolved.pointers[0].region_id, global); + assert_eq!(resolved.pointers[0].offset, 4); + } + #[test] fn test_symbolic_address_write_then_read() { let ctx = Context::thread_local(); @@ -993,4 +1185,161 @@ mod tests { Some(vec![0x11, 0x22, 0x33, 0x44]) ); } + + #[test] + fn test_overlapping_concrete_offset_writes_preserve_exact_bytes() { + let ctx = Context::thread_local(); + let mut mem = SymMemory::new(&ctx); + let globals = mem.define_region( + MemoryRegionKind::Global, + "globals", + Some(0x2000), + Some(0x100), + ); + + let base = RegionPointer { + region_id: globals, + offset: 0, + ptr_bits: 64, + }; + let overlap = RegionPointer { + region_id: globals, + offset: 1, + ptr_bits: 64, + }; + mem.region_write(&base, &SymValue::concrete(0x1122_3344, 32), 4); + mem.region_write(&overlap, &SymValue::concrete(0xaabb, 16), 2); + + let addr = SymValue::concrete(0x2000, 64); + assert_eq!(mem.read(&addr, 4).as_concrete(), Some(0x11aa_bb44)); + assert_eq!( + mem.read_bytes(0x2000, 4), + Some(vec![0x44, 0xbb, 0xaa, 0x11]) + ); + } + + #[test] + fn test_symbolic_offset_write_invalidates_overwritten_concrete_bytes() { + let ctx = Context::thread_local(); + let mut mem = SymMemory::new(&ctx); + let globals = mem.define_region( + MemoryRegionKind::Global, + "globals", + Some(0x3000), + Some(0x100), + ); + mem.seed_region_bytes(globals, 0, &[0x10, 0x20, 0x30, 0x40]); + + let ptr = RegionPointer { + region_id: globals, + offset: 1, + ptr_bits: 64, + }; + mem.region_write(&ptr, &SymValue::new_symbolic(&ctx, "sym_word", 16), 2); + + assert_eq!(mem.read_bytes(0x3000, 4), None); + assert!(!mem.is_concrete_range(0x3000, 4)); + assert_eq!( + mem.read(&SymValue::concrete(0x3000, 64), 1).as_concrete(), + Some(0x10) + ); + assert!(mem.read(&SymValue::concrete(0x3001, 64), 1).is_symbolic()); + assert!(mem.read(&SymValue::concrete(0x3002, 64), 1).is_symbolic()); + assert_eq!( + mem.read(&SymValue::concrete(0x3003, 64), 1).as_concrete(), + Some(0x40) + ); + } + + #[test] + fn test_overlapping_symbolic_offset_writes_compose_by_byte() { + let ctx = Context::thread_local(); + let mut mem = SymMemory::new(&ctx); + let globals = mem.define_region( + MemoryRegionKind::Global, + "globals", + Some(0x4000), + Some(0x100), + ); + + let wide = RegionPointer { + region_id: globals, + offset: 0, + ptr_bits: 64, + }; + let byte = RegionPointer { + region_id: globals, + offset: 1, + ptr_bits: 64, + }; + let word = SymValue::new_symbolic(&ctx, "word", 32); + mem.region_write(&wide, &word, 4); + mem.region_write(&byte, &SymValue::concrete(0xaa, 8), 1); + + let read_back = mem.read(&SymValue::concrete(0x4000, 64), 4); + let solver = Solver::new(); + solver.assert(word.to_bv(&ctx).eq(BV::from_u64(0x1122_3344, 32))); + assert_eq!(solver.check(), SatResult::Sat); + let model = solver.get_model().unwrap(); + let value = model + .eval(&read_back.to_bv(&ctx), true) + .and_then(|value| value.as_u64()) + .unwrap(); + assert_eq!(value, 0x1122_aa44); + } + + #[test] + fn test_affine_pointer_recognizer_extracts_symbol_plus_delta() { + let ctx = Context::thread_local(); + let base = SymValue::new_symbolic(&ctx, "ptr_base", 64); + let addr = base.add(&ctx, &SymValue::concrete(0x20, 64)); + + let affine = recognize_affine_pointer(&addr).expect("affine recognizer should match"); + assert_eq!(affine.delta, 0x20); + let same = affine.base.to_bv(&ctx).eq(base.to_bv(&ctx)); + assert_eq!(same.simplify().as_bool(), Some(true)); + } + + #[test] + fn test_semantic_fingerprint_ignores_equivalent_concrete_write_chunking() { + let ctx = Context::thread_local(); + let mut lhs = SymMemory::new(&ctx); + let mut rhs = SymMemory::new(&ctx); + let globals_lhs = lhs.define_region( + MemoryRegionKind::Global, + "globals", + Some(0x5000), + Some(0x100), + ); + let globals_rhs = rhs.define_region( + MemoryRegionKind::Global, + "globals", + Some(0x5000), + Some(0x100), + ); + + lhs.region_write( + &RegionPointer { + region_id: globals_lhs, + offset: 0, + ptr_bits: 64, + }, + &SymValue::concrete(0x4433_2211, 32), + 4, + ); + for (index, byte) in [0x11u8, 0x22, 0x33, 0x44].into_iter().enumerate() { + rhs.region_write( + &RegionPointer { + region_id: globals_rhs, + offset: index as u64, + ptr_bits: 64, + }, + &SymValue::concrete(byte as u64, 8), + 1, + ); + } + + assert_eq!(lhs.read_bytes(0x5000, 4), rhs.read_bytes(0x5000, 4)); + assert_eq!(lhs.semantic_fingerprint(), rhs.semantic_fingerprint()); + } } diff --git a/crates/r2sym/src/path.rs b/crates/r2sym/src/path.rs index c4f116a..aebabb8 100644 --- a/crates/r2sym/src/path.rs +++ b/crates/r2sym/src/path.rs @@ -3,7 +3,8 @@ //! This module provides different strategies for exploring paths //! during symbolic execution, including DFS, BFS, and coverage-guided. -use std::collections::{BTreeMap, HashMap, HashSet, VecDeque}; +use std::cmp::Reverse; +use std::collections::{BTreeMap, BinaryHeap, HashMap, HashSet, VecDeque}; use std::rc::Rc; use std::time::{Duration, Instant}; @@ -17,6 +18,8 @@ use crate::solver::SymSolver; use crate::spec::ExplorationSpec; use crate::state::{ExitStatus, SymState}; +const TARGET_DISTANCE_CACHE_LIMIT: usize = 128; + /// Configuration for path exploration. #[derive(Debug, Clone)] pub struct ExploreConfig { @@ -168,6 +171,8 @@ pub struct PathExplorer<'ctx> { target_guided_queries: bool, /// Derived helper summaries registered for direct-call targets. derived_call_summaries: HashMap>, + /// Cached reverse-distance maps keyed by (function entry, target address). + target_distance_cache: HashMap<(u64, u64), HashMap>, } /// Statistics from path exploration. @@ -251,13 +256,14 @@ impl<'ctx> StateWorklist<'ctx> { } } - fn push(&mut self, state: SymState<'ctx>) { + fn push(&mut self, state: SymState<'ctx>) -> usize { let id = self.slots.len(); let pc = state.pc; self.slots.push(Some(state)); self.ready.push_back(id); self.same_pc.entry(pc).or_default().push_back(id); self.live_states += 1; + id } fn state(&self, id: usize) -> Option<&SymState<'ctx>> { @@ -297,42 +303,46 @@ impl<'ctx> StateWorklist<'ctx> { } } - fn pop_best_by_key_with_reorder( - &mut self, - mut key_fn: F, - ) -> Option<(SymState<'ctx>, bool)> - where - K: Ord, - F: FnMut(usize, &SymState<'ctx>) -> K, - { - loop { - let default_candidate = match self.strategy { - ExploreStrategy::Dfs => self.ready.back().copied(), - ExploreStrategy::Bfs => self.ready.front().copied(), - ExploreStrategy::Random => { - if self.ready.is_empty() { - None - } else if self.live_states.is_multiple_of(2) { - self.ready.front().copied() - } else { - self.ready.back().copied() + fn default_candidate(&mut self) -> Option { + match self.strategy { + ExploreStrategy::Dfs => { + while let Some(id) = self.ready.back().copied() { + if self.state(id).is_some() { + return Some(id); } + self.ready.pop_back(); } - }; - let best = self - .ready - .iter() - .filter_map(|id| self.state(*id).map(|state| (*id, key_fn(*id, state)))) - .min_by(|a, b| a.1.cmp(&b.1)) - .map(|(id, _)| id)?; - let reordered = default_candidate.is_some_and(|candidate| candidate != best); - - if let Some(pos) = self.ready.iter().position(|queued| *queued == best) { - self.ready.remove(pos); + None } - - if let Some(state) = self.take_slot(best) { - return Some((state, reordered)); + ExploreStrategy::Bfs => { + while let Some(id) = self.ready.front().copied() { + if self.state(id).is_some() { + return Some(id); + } + self.ready.pop_front(); + } + None + } + ExploreStrategy::Random => { + if self.ready.is_empty() { + return None; + } + if self.live_states.is_multiple_of(2) { + while let Some(id) = self.ready.front().copied() { + if self.state(id).is_some() { + return Some(id); + } + self.ready.pop_front(); + } + } else { + while let Some(id) = self.ready.back().copied() { + if self.state(id).is_some() { + return Some(id); + } + self.ready.pop_back(); + } + } + None } } } @@ -375,6 +385,26 @@ impl<'ctx> StateWorklist<'ctx> { } } +#[derive(Clone, Debug, Eq, PartialEq)] +struct TargetGuidedQueueEntry { + rank: (bool, usize, usize, usize, usize, usize, usize, usize, usize), + id: usize, +} + +impl Ord for TargetGuidedQueueEntry { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.rank + .cmp(&other.rank) + .then_with(|| self.id.cmp(&other.id)) + } +} + +impl PartialOrd for TargetGuidedQueueEntry { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + enum DriverAction<'ctx> { Continue(Box>), Skip, @@ -683,6 +713,7 @@ impl<'ctx> PathExplorer<'ctx> { stats: ExploreStats::default(), target_guided_queries: false, derived_call_summaries: HashMap::new(), + target_distance_cache: HashMap::new(), } } @@ -702,6 +733,7 @@ impl<'ctx> PathExplorer<'ctx> { stats: ExploreStats::default(), target_guided_queries: false, derived_call_summaries: HashMap::new(), + target_distance_cache: HashMap::new(), } } @@ -1033,7 +1065,7 @@ impl<'ctx> PathExplorer<'ctx> { } fn target_guidance_context( - &self, + &mut self, func: &SsaArtifact, target_addr: u64, ) -> TargetGuidanceContext { @@ -1166,13 +1198,22 @@ impl<'ctx> PathExplorer<'ctx> { ) { let start_time = Instant::now(); let mut worklist = StateWorklist::new(self.config.strategy); + let mut target_heap: BinaryHeap> = BinaryHeap::new(); let guidance = self.target_guidance_context(func, target_addr); - if self.target_enqueue_allowed(&guidance, &initial_state) { - worklist.push(initial_state); + if self.target_enqueue_allowed(&guidance, &initial_state) + && let Some(state) = self.prune_subsumed_same_pc_state(&mut worklist, initial_state) + { + let id = worklist.push(state); + if let Some(state) = worklist.state(id) { + target_heap.push(Reverse(TargetGuidedQueueEntry { + rank: self.state_target_rank(state, id, &guidance), + id, + })); + } } - while let Some((mut state, reordered)) = worklist - .pop_best_by_key_with_reorder(|id, state| self.state_target_rank(state, id, &guidance)) + while let Some((mut state, reordered)) = + self.pop_target_guided_state(&mut worklist, &mut target_heap) { if reordered { self.stats.target_guided_reorders += 1; @@ -1242,7 +1283,13 @@ impl<'ctx> PathExplorer<'ctx> { && let Some(forked) = self.prune_subsumed_same_pc_state(&mut worklist, forked) { - worklist.push(forked); + let id = worklist.push(forked); + if let Some(state) = worklist.state(id) { + target_heap.push(Reverse(TargetGuidedQueueEntry { + rank: self.state_target_rank(state, id, &guidance), + id, + })); + } } } @@ -1258,7 +1305,13 @@ impl<'ctx> PathExplorer<'ctx> { && let Some(state) = self.prune_subsumed_same_pc_state(&mut worklist, state) { - worklist.push(state); + let id = worklist.push(state); + if let Some(state) = worklist.state(id) { + target_heap.push(Reverse(TargetGuidedQueueEntry { + rank: self.state_target_rank(state, id, &guidance), + id, + })); + } } } else { match mode.on_terminated_state( @@ -1288,7 +1341,24 @@ impl<'ctx> PathExplorer<'ctx> { self.stats.total_time = start_time.elapsed(); } - fn target_distance_map(&self, func: &SsaArtifact, target_addr: u64) -> HashMap { + fn target_distance_map(&mut self, func: &SsaArtifact, target_addr: u64) -> HashMap { + let key = (func.entry, target_addr); + if let Some(cached) = self.target_distance_cache.get(&key) { + return cached.clone(); + } + let distances = self.compute_target_distance_map(func, target_addr); + if self.target_distance_cache.len() >= TARGET_DISTANCE_CACHE_LIMIT { + self.target_distance_cache.clear(); + } + self.target_distance_cache.insert(key, distances.clone()); + distances + } + + fn compute_target_distance_map( + &self, + func: &SsaArtifact, + target_addr: u64, + ) -> HashMap { let mut predecessors: HashMap> = HashMap::new(); for addr in func.cfg().block_addrs() { let Some(block) = func.cfg().get_block(addr) else { @@ -1318,6 +1388,28 @@ impl<'ctx> PathExplorer<'ctx> { distances } + fn pop_target_guided_state( + &mut self, + worklist: &mut StateWorklist<'ctx>, + heap: &mut BinaryHeap>, + ) -> Option<(SymState<'ctx>, bool)> { + loop { + while let Some(Reverse(entry)) = heap.peek() { + if worklist.state(entry.id).is_some() { + break; + } + heap.pop(); + } + + let Reverse(entry) = heap.pop()?; + let default_candidate = worklist.default_candidate(); + let reordered = default_candidate.is_some_and(|candidate| candidate != entry.id); + if let Some(state) = worklist.take_slot(entry.id) { + return Some((state, reordered)); + } + } + } + fn state_target_rank( &self, state: &SymState<'ctx>, @@ -1560,6 +1652,101 @@ mod tests { assert!(worklist.take_same_pc(0x1000).is_none()); } + #[test] + fn test_target_guided_heap_prefers_best_and_skips_stale_entries() { + let ctx = Context::thread_local(); + let mut explorer = PathExplorer::new(&ctx); + let mut worklist = StateWorklist::new(ExploreStrategy::Dfs); + + let id_a = worklist.push(SymState::new(&ctx, 0x1000)); + let id_b = worklist.push(SymState::new(&ctx, 0x2000)); + let id_c = worklist.push(SymState::new(&ctx, 0x3000)); + + let guidance = TargetGuidanceContext { + distances: HashMap::from([(0x1000, 2), (0x2000, 0), (0x3000, 1)]), + reachable_blocks: HashSet::from([0x1000, 0x2000, 0x3000]), + call_targets_by_block: HashMap::new(), + block_summary_rank: HashMap::new(), + }; + + let mut heap: BinaryHeap> = BinaryHeap::new(); + for id in [id_a, id_b, id_c] { + let state = worklist.state(id).expect("live state"); + heap.push(Reverse(TargetGuidedQueueEntry { + rank: explorer.state_target_rank(state, id, &guidance), + id, + })); + } + + let (state, reordered) = explorer + .pop_target_guided_state(&mut worklist, &mut heap) + .expect("best state should exist"); + assert_eq!(state.pc, 0x2000); + assert!( + reordered, + "best state should differ from DFS default candidate" + ); + + assert!( + worklist.remove_slot(id_c).is_some(), + "simulate a stale heap entry" + ); + + let (state, reordered) = explorer + .pop_target_guided_state(&mut worklist, &mut heap) + .expect("remaining state should exist"); + assert_eq!(state.pc, 0x1000); + assert!( + !reordered, + "after skipping stale entries, the chosen state should match the live default candidate" + ); + } + + #[test] + fn test_target_distance_map_reuses_cache_for_same_entry_and_target() { + let ctx = Context::thread_local(); + let mut explorer = PathExplorer::new(&ctx); + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x1004, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x1008, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1008, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, None).expect("symbolic function"); + + let first = explorer.target_distance_map(&func, 0x1008); + let second = explorer.target_distance_map(&func, 0x1008); + + assert_eq!(first, second); + assert_eq!(explorer.target_distance_cache.len(), 1); + assert_eq!(first.get(&0x1008), Some(&0)); + assert_eq!(first.get(&0x1004), Some(&1)); + assert_eq!(first.get(&0x1000), Some(&2)); + } + #[test] fn test_same_pc_subsumption_prefers_weaker_constraint_state() { let ctx = Context::thread_local(); diff --git a/crates/r2sym/src/query.rs b/crates/r2sym/src/query.rs index 3728ed9..b7a48b4 100644 --- a/crates/r2sym/src/query.rs +++ b/crates/r2sym/src/query.rs @@ -213,6 +213,7 @@ fn condition_summary<'ctx>(path: &PathResult<'ctx>) -> PathConditionSummary { enum PreconditionApplication<'ctx> { Continue { initial_state: Box>, + narrowed_state: Option>>, compiled_precondition: Option, }, ExactUnsat { @@ -229,10 +230,50 @@ fn precision_rank(precision: BackwardConditionPrecision) -> u8 { } } -fn vm_arm_reaches_target(arm: &crate::VmTransferArm, target_addr: u64) -> bool { +fn vm_exit_guard_allows_target( + bindings: &BTreeMap, + arm: &crate::VmTransferArm, + target_addr: u64, +) -> bool { + let guards = arm + .exit_guards + .iter() + .filter(|guard| guard.target == target_addr) + .collect::>(); + if guards.is_empty() { + return true; + } + let mut saw_reliable = false; + for guarded in guards { + let evidence = guarded.guard.evidence(); + if !evidence.is_usable() { + return true; + } + if !evidence.allows_narrowing() { + if matches!(guarded.guard.evaluate(bindings), Some(true)) { + return true; + } + continue; + } + saw_reliable = true; + match guarded.guard.evaluate(bindings) { + Some(true) => return true, + Some(false) => {} + None => return true, + } + } + !saw_reliable +} + +fn vm_arm_reaches_target( + bindings: &BTreeMap, + arm: &crate::VmTransferArm, + target_addr: u64, +) -> bool { arm.handler_target == target_addr || arm.region_blocks.contains(&target_addr) - || arm.exit_targets.contains(&target_addr) + || (arm.exit_targets.contains(&target_addr) + && vm_exit_guard_allows_target(bindings, arm, target_addr)) } fn vm_arm_for_case(vm_step: &VmStepSummary, case_value: u64) -> Option<&crate::VmTransferArm> { @@ -250,13 +291,13 @@ fn vm_selector_bindings(vm_step: &VmStepSummary, case_value: u64) -> BTreeMap, updates: &[crate::VmStateUpdate], ) -> BTreeMap { let mut next = bindings.clone(); for update in updates { - if !update.exact { + if !update.evidence().allows_narrowing() { continue; } if let Some(value) = update.value.evaluate_u64(&next) { @@ -266,20 +307,53 @@ fn vm_apply_exact_state_updates( next } +fn vm_apply_confident_memory_writes( + bindings: &BTreeMap, + writes: &[crate::VmMemoryCondition], +) -> BTreeMap { + let mut next = bindings.clone(); + for write in writes { + if !write.evidence().allows_narrowing() { + continue; + } + let Some(binding) = write.binding.as_ref() else { + continue; + }; + let Some(value) = write.value.as_ref() else { + continue; + }; + let Some(value) = value.evaluate_u64(&next) else { + continue; + }; + next.insert(binding.clone(), value); + } + next +} + fn vm_next_case_value( bindings: &BTreeMap, arm: &crate::VmTransferArm, selector_name: Option<&str>, + dispatch_header: u64, + loop_header: u64, ) -> Option<(u64, BTreeMap)> { if !arm.redispatch || arm.truncated { return None; } - let mut next_bindings = vm_apply_exact_state_updates(bindings, &arm.state_updates); + let can_redispatch = arm.exit_targets.iter().any(|target| { + (*target == dispatch_header || *target == loop_header) + && vm_exit_guard_allows_target(bindings, arm, *target) + }) || arm.exit_targets.is_empty(); + if !can_redispatch { + return None; + } + let mut next_bindings = vm_apply_confident_state_updates(bindings, &arm.state_updates); let selector_update = arm.selector_update.as_ref()?; - if !selector_update.exact { + if !selector_update.evidence().allows_narrowing() { return None; } let next_case = selector_update.value.evaluate_u64(&next_bindings)?; + next_bindings = vm_apply_confident_memory_writes(&next_bindings, &arm.memory_writes); next_bindings.insert(selector_update.output.clone(), next_case); if let Some(selector_name) = selector_name { next_bindings.insert(selector_name.to_string(), next_case); @@ -303,15 +377,19 @@ fn vm_case_reaches_target(vm_step: &VmStepSummary, initial_case: u64, target_add let Some(arm) = vm_arm_for_case(vm_step, case_value) else { continue; }; - if vm_arm_reaches_target(arm, target_addr) { + if vm_arm_reaches_target(&bindings, arm, target_addr) { return true; } if depth >= MAX_VM_COMPILED_STEPS.saturating_sub(1) || !arm.redispatch { continue; } - let Some((next_case, next_bindings)) = - vm_next_case_value(&bindings, arm, vm_step.selector.as_deref()) - else { + let Some((next_case, next_bindings)) = vm_next_case_value( + &bindings, + arm, + vm_step.selector.as_deref(), + vm_step.dispatch_header, + vm_step.loop_header, + ) else { continue; }; queue.push_back((next_case, next_bindings, depth + 1)); @@ -408,8 +486,72 @@ fn prefer_compiled_precondition( fn apply_compiled_precondition<'ctx>( explorer: &PathExplorer<'ctx>, - func: &SsaArtifact, mut initial_state: SymState<'ctx>, + compiled: crate::CompiledBackwardCondition, +) -> PreconditionApplication<'ctx> { + let summary = compiled.summary.clone(); + let evidence = summary.evidence(); + match explorer + .solver() + .sat_with_constraint(&initial_state, &compiled.predicate) + { + SatResult::Unsat if evidence.allows_hard_proof() => PreconditionApplication::ExactUnsat { + compiled_precondition: summary, + }, + SatResult::Sat => { + if evidence.allows_hard_proof() { + initial_state.add_constraint(compiled.predicate); + PreconditionApplication::Continue { + initial_state: Box::new(initial_state), + narrowed_state: None, + compiled_precondition: Some(summary), + } + } else if evidence.allows_narrowing() { + let mut narrowed_state = initial_state.fork(); + narrowed_state.add_constraint(compiled.predicate); + PreconditionApplication::Continue { + initial_state: Box::new(initial_state), + narrowed_state: Some(Box::new(narrowed_state)), + compiled_precondition: Some(summary), + } + } else { + PreconditionApplication::Continue { + initial_state: Box::new(initial_state), + narrowed_state: None, + compiled_precondition: Some(summary), + } + } + } + SatResult::Unknown | SatResult::Unsat => PreconditionApplication::Continue { + initial_state: Box::new(initial_state), + narrowed_state: None, + compiled_precondition: Some(summary), + }, + } +} + +fn find_paths_with_compiled_narrowing<'ctx, F>( + explorer: &mut PathExplorer<'ctx>, + initial_state: SymState<'ctx>, + narrowed_state: Option>>, + mut search: F, +) -> Vec> +where + F: FnMut(&mut PathExplorer<'ctx>, SymState<'ctx>) -> Vec>, +{ + if let Some(narrowed_state) = narrowed_state { + let matches = search(explorer, *narrowed_state); + if !matches.is_empty() || explorer.budget_exhausted() { + return matches; + } + } + search(explorer, initial_state) +} + +fn apply_best_compiled_precondition<'ctx>( + explorer: &PathExplorer<'ctx>, + func: &SsaArtifact, + initial_state: SymState<'ctx>, target_addr: u64, ) -> PreconditionApplication<'ctx> { let derived_summaries = explorer.derived_call_summary_views(); @@ -436,33 +578,11 @@ fn apply_compiled_precondition<'ctx>( let Some(compiled) = compiled else { return PreconditionApplication::Continue { initial_state: Box::new(initial_state), + narrowed_state: None, compiled_precondition: None, }; }; - - let summary = compiled.summary.clone(); - let exact = matches!(summary.precision, BackwardConditionPrecision::Exact); - match explorer - .solver() - .sat_with_constraint(&initial_state, &compiled.predicate) - { - SatResult::Unsat if exact => PreconditionApplication::ExactUnsat { - compiled_precondition: summary, - }, - SatResult::Sat => { - if exact { - initial_state.add_constraint(compiled.predicate); - } - PreconditionApplication::Continue { - initial_state: Box::new(initial_state), - compiled_precondition: Some(summary), - } - } - SatResult::Unknown | SatResult::Unsat => PreconditionApplication::Continue { - initial_state: Box::new(initial_state), - compiled_precondition: Some(summary), - }, - } + apply_compiled_precondition(explorer, initial_state, compiled) } impl<'ctx> PathExplorer<'ctx> { @@ -474,12 +594,18 @@ impl<'ctx> PathExplorer<'ctx> { target_addr: u64, ) -> ReachabilityResult<'ctx> { let (paths, compiled_precondition) = - match apply_compiled_precondition(self, func, initial_state, target_addr) { + match apply_best_compiled_precondition(self, func, initial_state, target_addr) { PreconditionApplication::Continue { initial_state, + narrowed_state, compiled_precondition, } => ( - self.find_paths_to(func, *initial_state, target_addr), + find_paths_with_compiled_narrowing( + self, + *initial_state, + narrowed_state, + |explorer, state| explorer.find_paths_to(func, state, target_addr), + ), compiled_precondition, ), PreconditionApplication::ExactUnsat { @@ -513,12 +639,18 @@ impl<'ctx> PathExplorer<'ctx> { target_pc: u64, ) -> PathConditionResult<'ctx> { let (matching_paths, compiled_precondition) = - match apply_compiled_precondition(self, func, initial_state, target_pc) { + match apply_best_compiled_precondition(self, func, initial_state, target_pc) { PreconditionApplication::Continue { initial_state, + narrowed_state, compiled_precondition, } => ( - self.find_paths_to(func, *initial_state, target_pc), + find_paths_with_compiled_narrowing( + self, + *initial_state, + narrowed_state, + |explorer, state| explorer.find_paths_to(func, state, target_pc), + ), compiled_precondition, ), PreconditionApplication::ExactUnsat { @@ -547,12 +679,18 @@ impl<'ctx> PathExplorer<'ctx> { target_addr: u64, ) -> SolveResult<'ctx> { let (matched_paths, compiled_precondition, exact_unsat) = - match apply_compiled_precondition(self, func, initial_state, target_addr) { + match apply_best_compiled_precondition(self, func, initial_state, target_addr) { PreconditionApplication::Continue { initial_state, + narrowed_state, compiled_precondition, } => ( - self.find_paths_to(func, *initial_state, target_addr), + find_paths_with_compiled_narrowing( + self, + *initial_state, + narrowed_state, + |explorer, state| explorer.find_paths_to(func, state, target_addr), + ), compiled_precondition, false, ), @@ -620,12 +758,12 @@ mod tests { use std::collections::BTreeMap; use super::{ - PreconditionApplication, apply_compiled_precondition, prefer_compiled_precondition, - vm_target_case_values, + PreconditionApplication, apply_best_compiled_precondition, apply_compiled_precondition, + prefer_compiled_precondition, vm_target_case_values, }; use crate::{ BackwardConditionPrecision, BackwardConditionSummary, SymQueryConfig, SymState, VmBinaryOp, - VmStateUpdate, VmStepSummary, VmTransferArm, VmValueExpr, + VmGuardCondition, VmGuardedExit, VmStateUpdate, VmStepSummary, VmTransferArm, VmValueExpr, }; use r2il::{R2ILBlock, R2ILOp, SpaceId, Varnode}; use r2ssa::SsaArtifact; @@ -731,6 +869,9 @@ mod tests { handler_state_inputs: BTreeMap::new(), handler_state_outputs: BTreeMap::new(), handler_state_updates: BTreeMap::new(), + handler_exit_guards: BTreeMap::new(), + handler_memory_read_effects: BTreeMap::new(), + handler_memory_write_effects: BTreeMap::new(), handler_memory_reads: BTreeMap::new(), handler_memory_writes: BTreeMap::new(), handler_calls: BTreeMap::new(), @@ -753,9 +894,10 @@ mod tests { let explorer = SymQueryConfig::default().make_explorer(&ctx); let original_constraints = state.num_constraints(); - match apply_compiled_precondition(&explorer, &func, state, 0x1010) { + match apply_best_compiled_precondition(&explorer, &func, state, 0x1010) { PreconditionApplication::Continue { initial_state, + narrowed_state, compiled_precondition, } => { let compiled = compiled_precondition.expect("compiled precondition"); @@ -764,6 +906,7 @@ mod tests { crate::BackwardConditionPrecision::ResidualSearchRequired ); assert_eq!(initial_state.num_constraints(), original_constraints); + assert!(narrowed_state.is_none()); } PreconditionApplication::ExactUnsat { .. } => { panic!("residual precondition should not shortcut as exact unsat") @@ -771,6 +914,49 @@ mod tests { } } + #[test] + fn likely_compiled_preconditions_seed_narrowed_search_state() { + let ctx = Context::thread_local(); + let explorer = SymQueryConfig::default().make_explorer(&ctx); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("reg:56_0", 64); + let original_constraints = state.num_constraints(); + + let compiled = crate::CompiledBackwardCondition { + predicate: z3::ast::Bool::from_bool(true), + summary: BackwardConditionSummary { + simplified: "guard".to_string(), + terms: vec!["guard".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::OverApprox, + supported_paths: 1, + total_paths: 2, + }, + }; + + match apply_compiled_precondition(&explorer, state, compiled) { + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition, + } => { + let compiled = compiled_precondition.expect("compiled precondition"); + assert_eq!(compiled.precision, BackwardConditionPrecision::OverApprox); + assert_eq!(initial_state.num_constraints(), original_constraints); + assert_eq!( + narrowed_state.expect("narrowed state").num_constraints(), + original_constraints + 1 + ); + } + PreconditionApplication::ExactUnsat { .. } => { + panic!("likely precondition should not shortcut as exact unsat") + } + } + } + #[test] fn vm_target_case_values_include_redispatch_cases() { let vm_step = make_vm_step_summary_with_transfers(vec![ @@ -779,6 +965,7 @@ mod tests { case_values: vec![0], region_blocks: vec![0x1004], exit_targets: vec![0x1000], + exit_guards: Vec::new(), state_updates: vec![VmStateUpdate { output: "RDI_1".to_string(), expr: "0x1".to_string(), @@ -791,6 +978,10 @@ mod tests { value: VmValueExpr::Const(1), exact: true, }), + memory_reads: Vec::new(), + memory_writes: Vec::new(), + residual_guards: false, + residual_memory_effects: false, exact: true, redispatch: true, may_return: false, @@ -801,8 +992,13 @@ mod tests { case_values: vec![1], region_blocks: vec![0x1008, 0x1014], exit_targets: vec![0x1014], + exit_guards: Vec::new(), state_updates: Vec::new(), selector_update: None, + memory_reads: Vec::new(), + memory_writes: Vec::new(), + residual_guards: false, + residual_memory_effects: false, exact: true, redispatch: false, may_return: true, @@ -821,6 +1017,7 @@ mod tests { case_values: vec![0], region_blocks: vec![0x1004], exit_targets: vec![0x1000], + exit_guards: Vec::new(), state_updates: vec![VmStateUpdate { output: "TMP_1".to_string(), expr: "0x1".to_string(), @@ -833,6 +1030,10 @@ mod tests { value: VmValueExpr::Var("TMP_1".to_string()), exact: true, }), + memory_reads: Vec::new(), + memory_writes: Vec::new(), + residual_guards: false, + residual_memory_effects: false, exact: true, redispatch: true, may_return: false, @@ -843,8 +1044,13 @@ mod tests { case_values: vec![1], region_blocks: vec![0x1008, 0x1014], exit_targets: vec![0x1014], + exit_guards: Vec::new(), state_updates: Vec::new(), selector_update: None, + memory_reads: Vec::new(), + memory_writes: Vec::new(), + residual_guards: false, + residual_memory_effects: false, exact: true, redispatch: false, may_return: true, @@ -863,6 +1069,7 @@ mod tests { case_values: vec![0], region_blocks: vec![0x1004], exit_targets: vec![0x1000], + exit_guards: Vec::new(), state_updates: vec![VmStateUpdate { output: "RDI_1".to_string(), expr: "(RDI_0 + 0x1)".to_string(), @@ -883,6 +1090,10 @@ mod tests { }, exact: true, }), + memory_reads: Vec::new(), + memory_writes: Vec::new(), + residual_guards: false, + residual_memory_effects: false, exact: true, redispatch: true, may_return: false, @@ -893,6 +1104,7 @@ mod tests { case_values: vec![1], region_blocks: vec![0x1008], exit_targets: vec![0x1000], + exit_guards: Vec::new(), state_updates: vec![VmStateUpdate { output: "RDI_1".to_string(), expr: "(RDI_0 + 0x1)".to_string(), @@ -913,6 +1125,10 @@ mod tests { }, exact: true, }), + memory_reads: Vec::new(), + memory_writes: Vec::new(), + residual_guards: false, + residual_memory_effects: false, exact: true, redispatch: true, may_return: false, @@ -923,8 +1139,13 @@ mod tests { case_values: vec![2], region_blocks: vec![0x1010, 0x1014], exit_targets: vec![0x1014], + exit_guards: Vec::new(), state_updates: Vec::new(), selector_update: None, + memory_reads: Vec::new(), + memory_writes: Vec::new(), + residual_guards: false, + residual_memory_effects: false, exact: true, redispatch: false, may_return: true, @@ -935,6 +1156,168 @@ mod tests { assert_eq!(vm_target_case_values(&vm_step, 0x1014), vec![0, 1, 2]); } + #[test] + fn vm_target_case_values_respect_exact_exit_guards() { + let vm_step = make_vm_step_summary_with_transfers(vec![ + VmTransferArm { + handler_target: 0x1004, + case_values: vec![0], + region_blocks: vec![0x1004], + exit_targets: vec![0x1014, 0x1000], + exit_guards: vec![VmGuardedExit { + target: 0x1014, + guard: VmGuardCondition { + expr: "(RDI_0 == 0x1)".to_string(), + value: VmValueExpr::Binary { + op: VmBinaryOp::Eq, + lhs: Box::new(VmValueExpr::Var("RDI_0".to_string())), + rhs: Box::new(VmValueExpr::Const(1)), + }, + expect_nonzero: true, + exact: true, + }, + }], + state_updates: Vec::new(), + selector_update: None, + memory_reads: Vec::new(), + memory_writes: Vec::new(), + residual_guards: false, + residual_memory_effects: false, + exact: false, + redispatch: false, + may_return: true, + truncated: false, + }, + VmTransferArm { + handler_target: 0x1008, + case_values: vec![1], + region_blocks: vec![0x1008], + exit_targets: vec![0x1014], + exit_guards: Vec::new(), + state_updates: Vec::new(), + selector_update: None, + memory_reads: Vec::new(), + memory_writes: Vec::new(), + residual_guards: false, + residual_memory_effects: false, + exact: true, + redispatch: false, + may_return: true, + truncated: false, + }, + ]); + + assert_eq!(vm_target_case_values(&vm_step, 0x1014), vec![1]); + } + + #[test] + fn vm_target_case_values_follow_exact_memory_writes_across_redispatch() { + let binding = "mem:r7:0:1".to_string(); + let vm_step = make_vm_step_summary_with_transfers(vec![ + VmTransferArm { + handler_target: 0x1004, + case_values: vec![0], + region_blocks: vec![0x1004], + exit_targets: vec![0x1000], + exit_guards: Vec::new(), + state_updates: Vec::new(), + selector_update: Some(VmStateUpdate { + output: "RDI_1".to_string(), + expr: "0x1".to_string(), + value: VmValueExpr::Const(1), + exact: true, + }), + memory_reads: Vec::new(), + memory_writes: vec![crate::VmMemoryCondition { + region: crate::VmMemoryRegionRef { + id: 7, + kind: crate::MemoryRegionKind::Global, + name: "ram:0x4000".to_string(), + }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + binding: Some(binding.clone()), + expr: "*0x4000".to_string(), + value_expr: Some("0x1".to_string()), + value: Some(VmValueExpr::Const(1)), + exact_value: true, + }], + residual_guards: false, + residual_memory_effects: false, + exact: true, + redispatch: true, + may_return: false, + truncated: false, + }, + VmTransferArm { + handler_target: 0x1008, + case_values: vec![2], + region_blocks: vec![0x1008], + exit_targets: vec![0x1000], + exit_guards: Vec::new(), + state_updates: Vec::new(), + selector_update: Some(VmStateUpdate { + output: "RDI_1".to_string(), + expr: "0x1".to_string(), + value: VmValueExpr::Const(1), + exact: true, + }), + memory_reads: Vec::new(), + memory_writes: vec![crate::VmMemoryCondition { + region: crate::VmMemoryRegionRef { + id: 7, + kind: crate::MemoryRegionKind::Global, + name: "ram:0x4000".to_string(), + }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + binding: Some(binding.clone()), + expr: "*0x4000".to_string(), + value_expr: Some("0x0".to_string()), + value: Some(VmValueExpr::Const(0)), + exact_value: true, + }], + residual_guards: false, + residual_memory_effects: false, + exact: true, + redispatch: true, + may_return: false, + truncated: false, + }, + VmTransferArm { + handler_target: 0x1010, + case_values: vec![1], + region_blocks: vec![0x1010], + exit_targets: vec![0x1014], + exit_guards: vec![VmGuardedExit { + target: 0x1014, + guard: VmGuardCondition { + expr: binding.clone(), + value: VmValueExpr::Var(binding.clone()), + expect_nonzero: true, + exact: true, + }, + }], + state_updates: Vec::new(), + selector_update: None, + memory_reads: Vec::new(), + memory_writes: Vec::new(), + residual_guards: false, + residual_memory_effects: false, + exact: true, + redispatch: false, + may_return: true, + truncated: false, + }, + ]); + + assert_eq!(vm_target_case_values(&vm_step, 0x1014), vec![0, 1]); + } + #[test] fn exact_compiled_preconditions_prefer_fewer_residual_fallbacks() { let current = crate::CompiledBackwardCondition { diff --git a/crates/r2sym/src/semantics/artifact.rs b/crates/r2sym/src/semantics/artifact.rs index 27839d8..518f5ef 100644 --- a/crates/r2sym/src/semantics/artifact.rs +++ b/crates/r2sym/src/semantics/artifact.rs @@ -34,6 +34,191 @@ pub enum ResidualReason { InterpreterRequiresStepSummary, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub enum SemanticConfidence { + Exact, + Likely, + Heuristic, + Residual, +} + +impl SemanticConfidence { + pub fn is_reliable(self) -> bool { + matches!(self, Self::Exact | Self::Likely) + } + + pub fn is_usable(self) -> bool { + !matches!(self, Self::Residual) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum SemanticEvidenceSoundness { + Proven, + OverApprox, + Ranked, + Unknown, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum SemanticEvidenceCoverage { + Full, + Partial, + Bounded, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum SemanticEvidenceProvenance { + Stable, + Normalized, + Ranked, + Unstable, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum SemanticEvidenceAmbiguity { + Single, + Bounded, + Ranked, + Multiple, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum SemanticEvidenceReason { + LargeCfg, + SummaryBudget, + AliasAmbiguity, + ReplayOverlap, + HeapIdentityWeak, + GuardOpaque, + ValueOpaque, + TruncatedTransfer, + DerivedFromRanking, + PartialPathCoverage, + ResidualSearchRequired, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct SemanticEvidence { + pub tier: SemanticConfidence, + pub soundness: SemanticEvidenceSoundness, + pub coverage: SemanticEvidenceCoverage, + pub provenance: SemanticEvidenceProvenance, + pub ambiguity: SemanticEvidenceAmbiguity, + pub budget_limited: bool, + pub reasons: Vec, +} + +impl Default for SemanticEvidence { + fn default() -> Self { + Self::exact() + } +} + +impl SemanticEvidence { + pub fn is_default_exact(&self) -> bool { + *self == Self::exact() + } + + pub fn exact() -> Self { + Self { + tier: SemanticConfidence::Exact, + soundness: SemanticEvidenceSoundness::Proven, + coverage: SemanticEvidenceCoverage::Full, + provenance: SemanticEvidenceProvenance::Stable, + ambiguity: SemanticEvidenceAmbiguity::Single, + budget_limited: false, + reasons: Vec::new(), + } + } + + pub fn likely(reason: SemanticEvidenceReason) -> Self { + Self { + tier: SemanticConfidence::Likely, + soundness: SemanticEvidenceSoundness::OverApprox, + coverage: SemanticEvidenceCoverage::Full, + provenance: SemanticEvidenceProvenance::Normalized, + ambiguity: SemanticEvidenceAmbiguity::Single, + budget_limited: false, + reasons: vec![reason], + } + } + + pub fn heuristic(reason: SemanticEvidenceReason) -> Self { + Self { + tier: SemanticConfidence::Heuristic, + soundness: SemanticEvidenceSoundness::Ranked, + coverage: SemanticEvidenceCoverage::Partial, + provenance: SemanticEvidenceProvenance::Ranked, + ambiguity: SemanticEvidenceAmbiguity::Ranked, + budget_limited: false, + reasons: vec![reason], + } + } + + pub fn residual(reason: SemanticEvidenceReason) -> Self { + Self { + tier: SemanticConfidence::Residual, + soundness: SemanticEvidenceSoundness::Unknown, + coverage: SemanticEvidenceCoverage::Partial, + provenance: SemanticEvidenceProvenance::Unstable, + ambiguity: SemanticEvidenceAmbiguity::Multiple, + budget_limited: false, + reasons: vec![reason], + } + } + + pub fn with_coverage(mut self, coverage: SemanticEvidenceCoverage) -> Self { + self.coverage = coverage; + self + } + + pub fn with_provenance(mut self, provenance: SemanticEvidenceProvenance) -> Self { + self.provenance = provenance; + self + } + + pub fn with_ambiguity(mut self, ambiguity: SemanticEvidenceAmbiguity) -> Self { + self.ambiguity = ambiguity; + self + } + + pub fn with_budget_limited(mut self, budget_limited: bool) -> Self { + self.budget_limited = budget_limited; + self + } + + pub fn with_reason(mut self, reason: SemanticEvidenceReason) -> Self { + if !self.reasons.contains(&reason) { + self.reasons.push(reason); + } + self + } + + pub fn is_reliable(&self) -> bool { + self.tier.is_reliable() + } + + pub fn is_usable(&self) -> bool { + self.tier.is_usable() + } + + pub fn allows_hard_proof(&self) -> bool { + matches!(self.tier, SemanticConfidence::Exact) + } + + pub fn allows_narrowing(&self) -> bool { + matches!( + self.tier, + SemanticConfidence::Exact | SemanticConfidence::Likely + ) + } + + pub fn allows_ranking(&self) -> bool { + !matches!(self.tier, SemanticConfidence::Residual) + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct SemanticCapability { pub query_ready: bool, diff --git a/crates/r2sym/src/semantics/compiler.rs b/crates/r2sym/src/semantics/compiler.rs index 609a3f0..c568504 100644 --- a/crates/r2sym/src/semantics/compiler.rs +++ b/crates/r2sym/src/semantics/compiler.rs @@ -1,8 +1,8 @@ -use std::collections::HashMap; +use std::collections::{BTreeSet, HashMap}; use std::sync::Arc; use r2il::ArchSpec; -use r2ssa::SsaArtifact; +use r2ssa::{InterprocFunctionId, SsaArtifact}; use z3::Context; use crate::sim::{ @@ -19,8 +19,9 @@ use super::cache::{ }; use super::classify::classify_slice; use super::facts::{ - SymbolicFunctionFacts, collect_symbolic_function_facts_with_derived, - collect_symbolic_function_facts_with_scope, + SymbolicFunctionFacts, collect_large_cfg_symbolic_function_facts_with_limit, + collect_symbolic_function_facts_with_derived, + collect_symbolic_function_facts_with_scope_and_profile, }; use super::vm::{build_vm_step_summary, classify_interpreter_like}; @@ -80,6 +81,18 @@ fn semantic_mode_for( } fn semantic_capability(mode: SemanticMode, facts: &SymbolicFunctionFacts) -> SemanticCapability { + let has_bounded_branch_facts = !facts.branch_facts.is_empty(); + let has_actionable_branch_frontier = facts + .branch_facts + .iter() + .any(|fact| fact.true_compiled.is_some() || fact.false_compiled.is_some()); + let has_actionable_control_islands = facts + .control_islands + .iter() + .any(|island| island.actionable_compiled_condition().is_some()); + let large_cfg_actionable_decompile = facts.diagnostics.skipped_large_cfg + && (has_actionable_branch_frontier || has_actionable_control_islands) + && facts.branch_facts.len() <= 4; match mode { SemanticMode::Raw | SemanticMode::Compiled => SemanticCapability { query_ready: true, @@ -88,8 +101,8 @@ fn semantic_capability(mode: SemanticMode, facts: &SymbolicFunctionFacts) -> Sem }, SemanticMode::Residual => SemanticCapability { query_ready: true, - type_ready: !facts.diagnostics.skipped_large_cfg, - decompile_ready: false, + type_ready: !facts.diagnostics.skipped_large_cfg || has_bounded_branch_facts, + decompile_ready: large_cfg_actionable_decompile, }, SemanticMode::VmSummary => SemanticCapability { query_ready: true, @@ -99,6 +112,62 @@ fn semantic_capability(mode: SemanticMode, facts: &SymbolicFunctionFacts) -> Sem } } +fn bounded_large_cfg_branch_limit(func: &SsaArtifact) -> usize { + let summary = func.function().cfg_risk_summary(); + if summary.block_count > 160 || summary.back_edge_count > 8 || summary.switch_block_count > 8 { + 2 + } else if summary.block_count > 96 || summary.back_edge_count > 6 { + 3 + } else { + 4 + } +} + +fn bounded_large_cfg_helper_limit(func: &SsaArtifact) -> usize { + let summary = func.function().cfg_risk_summary(); + if summary.block_count > 160 || summary.back_edge_count > 8 || summary.switch_block_count > 8 { + 1 + } else if summary.block_count > 96 || summary.back_edge_count > 6 { + 2 + } else { + 3 + } +} + +fn bounded_large_cfg_scope( + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, +) -> Option { + let scope = scope?; + let root = scope.root()?.clone(); + let helper_limit = bounded_large_cfg_helper_limit(func); + if helper_limit == 0 { + return PreparedFunctionScope::new(scope.root_id().0, vec![root]); + } + + let mut functions = vec![root]; + let mut seen = BTreeSet::from([scope.root_id()]); + + for call in func.call_sites().by_id.values() { + if functions.len().saturating_sub(1) >= helper_limit { + break; + } + let Some(target) = call.direct_target else { + continue; + }; + let function_id = InterprocFunctionId(target); + if !seen.insert(function_id) { + continue; + } + let Some(helper) = scope.functions().get(&function_id).cloned() else { + continue; + }; + functions.push(helper); + } + + PreparedFunctionScope::new(scope.root_id().0, functions) +} + fn compile_function_semantics_uncached( ctx: &Context, func: &SsaArtifact, @@ -114,6 +183,11 @@ fn compile_function_semantics_uncached( let mut derived_diagnostics = DerivedSummaryDiagnostics::default(); let mut derived_summaries = 0usize; let mut symbolic_facts = SymbolicFunctionFacts::default(); + let interpreter = classify_interpreter_like(func); + let vm_step = interpreter + .as_ref() + .and_then(|dispatch| build_vm_step_summary(func, dispatch)); + let vm_transfer = vm_step.clone(); if let Some(arch) = arch { let cfg_summary = func.function().cfg_risk_summary(); @@ -121,41 +195,89 @@ fn compile_function_semantics_uncached( || cfg_summary.back_edge_count > 4 || cfg_summary.switch_block_count > 4; - if let Some(scope) = scope - && let Some(registry) = SummaryRegistry::with_profile_for_arch(arch, summary_profile) - { - let derived = registry.derive_symbolic_summaries(ctx, scope, Some(arch), symbol_map); - derived_summaries = derived.summaries.len(); - derived_diagnostics = derived.diagnostics.clone(); - if skip_expensive_branch_compilation { - symbolic_facts.diagnostics.skipped_large_cfg = true; - } else { - symbolic_facts = collect_symbolic_function_facts_with_derived( + if skip_expensive_branch_compilation { + if interpreter.is_none() { + let bounded_scope = bounded_large_cfg_scope(func, scope); + symbolic_facts = collect_large_cfg_symbolic_function_facts_with_limit( ctx, func, - Some(scope), + bounded_scope.as_ref(), arch, symbol_map, summary_profile, - ®istry, - &derived, + bounded_large_cfg_branch_limit(func), ); + symbolic_facts.diagnostics.skipped_large_cfg = true; + } else { + symbolic_facts.diagnostics.skipped_large_cfg = true; } - } else if skip_expensive_branch_compilation { - symbolic_facts.diagnostics.skipped_large_cfg = true; + + let mode = semantic_mode_for( + helper_functions, + derived_summaries, + &derived_diagnostics, + &symbolic_facts, + interpreter.is_some(), + vm_transfer.is_some(), + ); + let slice_class = classify_slice( + func, + helper_functions, + &derived_diagnostics, + interpreter.as_ref(), + ); + + return CompiledSemanticArtifact { + mode, + slice_class, + capability: semantic_capability(mode, &symbolic_facts), + residual_reasons: residual_reasons( + &derived_diagnostics, + &symbolic_facts, + interpreter.is_some(), + vm_step.is_some(), + ), + closure_functions, + helper_functions, + derived_summaries, + derived_diagnostics, + symbolic_facts, + interpreter, + vm_step, + vm_transfer, + cache_hit: false, + }; + } + + if let Some(scope) = scope + && let Some(registry) = SummaryRegistry::with_profile_for_arch(arch, summary_profile) + { + let derived = registry.derive_symbolic_summaries(ctx, scope, Some(arch), symbol_map); + derived_summaries = derived.summaries.len(); + derived_diagnostics = derived.diagnostics.clone(); + symbolic_facts = collect_symbolic_function_facts_with_derived( + ctx, + func, + Some(scope), + arch, + symbol_map, + summary_profile, + ®istry, + &derived, + ); } else { - symbolic_facts = - collect_symbolic_function_facts_with_scope(ctx, func, None, Some(arch), symbol_map); + symbolic_facts = collect_symbolic_function_facts_with_scope_and_profile( + ctx, + func, + None, + Some(arch), + symbol_map, + summary_profile, + ); } } else { symbolic_facts.diagnostics.skipped_missing_arch = true; } - - let interpreter = classify_interpreter_like(func); - let vm_step = interpreter - .as_ref() - .and_then(|dispatch| build_vm_step_summary(func, dispatch)); - let vm_transfer = vm_step.clone(); let mode = semantic_mode_for( helper_functions, derived_summaries, @@ -813,6 +935,12 @@ mod tests { assert_eq!(vm_step.handler_regions.get(&0x3008), Some(&vec![0x3008])); assert_eq!(vm_step.handler_memory_reads.get(&0x3008), Some(&1)); assert_eq!(vm_step.handler_memory_writes.get(&0x3008), Some(&0)); + assert!( + vm_step + .handler_memory_read_effects + .get(&0x3008) + .is_some_and(|effects| !effects.is_empty()) + ); assert!( vm_step .handler_state_updates @@ -834,6 +962,12 @@ mod tests { vm_step.handler_exit_targets.get(&0x3018), Some(&vec![0x3004]) ); + assert!( + vm_step + .handler_exit_guards + .get(&0x3018) + .is_some_and(|guards| !guards.is_empty()) + ); assert!(vm_step.truncated_handlers.is_empty()); assert_eq!(vm_step.transfers.len(), 5); assert!( @@ -848,5 +982,92 @@ mod tests { .iter() .any(|transfer| !transfer.state_updates.is_empty()) ); + assert!( + vm_step + .transfers + .iter() + .any(|transfer| !transfer.exit_guards.is_empty()) + ); + assert!( + vm_step + .transfers + .iter() + .any(|transfer| !transfer.memory_reads.is_empty()) + ); + } + + #[test] + fn large_cfg_shortcuts_to_bounded_residual_artifact() { + let mut blocks = vec![ + R2ILBlock { + addr: 0x4000, + size: 4, + ops: vec![R2ILOp::CBranch { + target: make_const(0x4008, 8), + cond: make_reg(RAX, 1), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x4004, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x400c, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x4008, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x400c, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + + let mut addr = 0x400c; + for _ in 0..64 { + blocks.push(R2ILBlock { + addr, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(addr + 4, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }); + addr += 4; + } + blocks.push(R2ILBlock { + addr, + size: 4, + ops: vec![R2ILOp::Return { + target: make_reg(RAX, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }); + + let func = SsaArtifact::for_symbolic(&blocks, Some(&test_arch())).expect("ssa"); + let ctx = Context::thread_local(); + let artifact = compile_function_semantics_with_scope( + &ctx, + &func, + None, + Some(&test_arch()), + &HashMap::new(), + SummaryProfile::Default, + ); + + assert_eq!(artifact.mode, SemanticMode::Residual); + assert!(artifact.symbolic_facts.diagnostics.skipped_large_cfg); + assert_eq!(artifact.symbolic_facts.branch_facts.len(), 1); + assert!(artifact.capability.type_ready); + assert!(artifact.capability.decompile_ready); + assert_eq!(artifact.symbolic_facts.diagnostics.branches_evaluated, 1); } } diff --git a/crates/r2sym/src/semantics/facts.rs b/crates/r2sym/src/semantics/facts.rs index fdb53ae..34c3267 100644 --- a/crates/r2sym/src/semantics/facts.rs +++ b/crates/r2sym/src/semantics/facts.rs @@ -1,4 +1,4 @@ -use std::collections::{BTreeSet, HashMap}; +use std::collections::{BTreeSet, HashMap, VecDeque}; use r2il::ArchSpec; use r2ssa::SsaArtifact; @@ -12,6 +12,10 @@ use crate::backward::{ }; use crate::path::{ExploreConfig, PathExplorer}; use crate::runtime::seed_default_state_for_arch; +use crate::semantics::{ + SemanticConfidence, SemanticEvidence, SemanticEvidenceCoverage, SemanticEvidenceProvenance, + SemanticEvidenceReason, +}; use crate::sim::{DerivedSummarySet, PreparedFunctionScope, SummaryProfile, SummaryRegistry}; use crate::solver::SatResult; @@ -37,6 +41,60 @@ pub struct SymbolicBranchFact { pub false_compiled: Option, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum SymbolicControlIslandKind { + BranchFrontier, + LargeCfgBranchFrontier, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SymbolicControlFact { + pub target: u64, + pub status: SymbolicReachabilityStatus, + #[serde(skip_serializing_if = "Option::is_none")] + pub condition: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub compiled: Option, + #[serde(default, skip_serializing_if = "SemanticEvidence::is_default_exact")] + pub evidence: SemanticEvidence, +} + +impl SymbolicControlFact { + pub fn exact_compiled_condition(&self) -> Option<&BackwardConditionSummary> { + self.evidence + .allows_hard_proof() + .then_some(self.compiled.as_ref()) + .flatten() + } + + pub fn actionable_compiled_condition(&self) -> Option<&BackwardConditionSummary> { + self.evidence + .allows_narrowing() + .then_some(self.compiled.as_ref()) + .flatten() + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SymbolicControlIsland { + pub kind: SymbolicControlIslandKind, + pub anchor_block: u64, + pub frontier_targets: Vec, + pub facts: Vec, + #[serde(default, skip_serializing_if = "SemanticEvidence::is_default_exact")] + pub evidence: SemanticEvidence, +} + +impl SymbolicControlIsland { + pub fn exact_compiled_condition(&self) -> Option<&BackwardConditionSummary> { + unique_compiled_condition(self.facts.iter(), true) + } + + pub fn actionable_compiled_condition(&self) -> Option<&BackwardConditionSummary> { + unique_compiled_condition(self.facts.iter(), false) + } +} + #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct SymbolicFunctionFactDiagnostics { pub branches_evaluated: usize, @@ -49,6 +107,8 @@ pub struct SymbolicFunctionFactDiagnostics { #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct SymbolicFunctionFacts { pub branch_facts: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub control_islands: Vec, pub diagnostics: SymbolicFunctionFactDiagnostics, } @@ -58,6 +118,161 @@ impl SymbolicFunctionFacts { .iter() .find(|fact| fact.block_addr == block_addr) } + + pub fn control_island_for_block(&self, block_addr: u64) -> Option<&SymbolicControlIsland> { + self.control_islands + .iter() + .find(|island| island.anchor_block == block_addr) + } +} + +fn control_fact_evidence( + summary: Option<&BackwardConditionSummary>, + condition: Option<&str>, + diagnostics: &SymbolicFunctionFactDiagnostics, +) -> SemanticEvidence { + let branch_budget_limited = diagnostics.skipped_large_cfg; + match summary { + Some(summary) => match summary.precision { + BackwardConditionPrecision::Exact => summary.evidence(), + BackwardConditionPrecision::OverApprox => summary + .evidence() + .with_budget_limited(branch_budget_limited) + .with_reason(SemanticEvidenceReason::PartialPathCoverage), + BackwardConditionPrecision::ResidualSearchRequired => { + let simplified = summary.simplified.trim(); + let has_guard = !simplified.is_empty() && simplified != "true" && simplified != "1"; + if summary.supported_paths > 0 + && has_guard + && summary.backward_memory_residual_fallbacks == 0 + { + SemanticEvidence::likely(SemanticEvidenceReason::ResidualSearchRequired) + .with_coverage(SemanticEvidenceCoverage::Bounded) + .with_provenance(SemanticEvidenceProvenance::Normalized) + .with_budget_limited(branch_budget_limited) + .with_reason(SemanticEvidenceReason::PartialPathCoverage) + } else if summary.supported_paths > 0 && has_guard { + SemanticEvidence::heuristic(SemanticEvidenceReason::ResidualSearchRequired) + .with_coverage(SemanticEvidenceCoverage::Bounded) + .with_provenance(SemanticEvidenceProvenance::Normalized) + .with_budget_limited(branch_budget_limited) + } else if condition.is_some() { + SemanticEvidence::heuristic(SemanticEvidenceReason::GuardOpaque) + .with_coverage(SemanticEvidenceCoverage::Bounded) + .with_budget_limited(branch_budget_limited) + } else { + SemanticEvidence::residual(SemanticEvidenceReason::ResidualSearchRequired) + .with_budget_limited(branch_budget_limited) + } + } + BackwardConditionPrecision::Unsupported => { + if condition.is_some() { + SemanticEvidence::heuristic(SemanticEvidenceReason::GuardOpaque) + .with_coverage(SemanticEvidenceCoverage::Bounded) + .with_budget_limited(branch_budget_limited) + } else { + SemanticEvidence::residual(SemanticEvidenceReason::ValueOpaque) + .with_budget_limited(branch_budget_limited) + } + } + }, + None => condition + .map(|_| { + SemanticEvidence::heuristic(SemanticEvidenceReason::GuardOpaque) + .with_coverage(SemanticEvidenceCoverage::Bounded) + .with_budget_limited(branch_budget_limited) + }) + .unwrap_or_else(|| { + SemanticEvidence::residual(SemanticEvidenceReason::GuardOpaque) + .with_budget_limited(branch_budget_limited) + }), + } +} + +fn island_evidence(facts: &[SymbolicControlFact]) -> SemanticEvidence { + facts + .iter() + .map(|fact| fact.evidence.clone()) + .max_by_key(|evidence| { + ( + match evidence.tier { + SemanticConfidence::Exact => 3, + SemanticConfidence::Likely => 2, + SemanticConfidence::Heuristic => 1, + SemanticConfidence::Residual => 0, + }, + evidence.allows_hard_proof() as u8, + evidence.allows_narrowing() as u8, + ) + }) + .unwrap_or_else(|| SemanticEvidence::residual(SemanticEvidenceReason::GuardOpaque)) +} + +fn unique_compiled_condition<'a>( + facts: impl Iterator, + hard_proof_only: bool, +) -> Option<&'a BackwardConditionSummary> { + let mut candidates = facts.filter_map(|fact| { + if hard_proof_only { + fact.exact_compiled_condition() + } else { + fact.actionable_compiled_condition() + } + }); + let first = candidates.next()?; + candidates.next().is_none().then_some(first) +} + +fn derive_control_islands( + branch_facts: &[SymbolicBranchFact], + diagnostics: &SymbolicFunctionFactDiagnostics, +) -> Vec { + branch_facts + .iter() + .map(|branch| { + let kind = if diagnostics.skipped_large_cfg { + SymbolicControlIslandKind::LargeCfgBranchFrontier + } else { + SymbolicControlIslandKind::BranchFrontier + }; + let facts = vec![ + SymbolicControlFact { + target: branch.true_target, + status: branch.true_status, + condition: branch.true_condition.clone(), + compiled: branch.true_compiled.clone(), + evidence: control_fact_evidence( + branch.true_compiled.as_ref(), + branch.true_condition.as_deref(), + diagnostics, + ), + }, + SymbolicControlFact { + target: branch.false_target, + status: branch.false_status, + condition: branch.false_condition.clone(), + compiled: branch.false_compiled.clone(), + evidence: control_fact_evidence( + branch.false_compiled.as_ref(), + branch.false_condition.as_deref(), + diagnostics, + ), + }, + ]; + SymbolicControlIsland { + kind, + anchor_block: branch.block_addr, + frontier_targets: vec![branch.true_target, branch.false_target], + evidence: island_evidence(&facts), + facts, + } + }) + .collect() +} + +fn finalize_symbolic_function_facts(mut facts: SymbolicFunctionFacts) -> SymbolicFunctionFacts { + facts.control_islands = derive_control_islands(&facts.branch_facts, &facts.diagnostics); + facts } fn symbolic_condition_hint(summary: Option<&BackwardConditionSummary>) -> Option { @@ -82,6 +297,74 @@ fn symbolic_fact_explorer<'ctx>(ctx: &'ctx Context) -> PathExplorer<'ctx> { explorer } +fn collect_branch_blocks(func: &SsaArtifact) -> Vec<(u64, u64, u64)> { + func.cfg() + .block_addrs() + .filter_map(|block_addr| { + let block = func.cfg().get_block(block_addr)?; + match block.terminator { + r2ssa::BlockTerminator::ConditionalBranch { + true_target, + false_target, + } => Some((block_addr, true_target, false_target)), + _ => None, + } + }) + .collect() +} + +fn large_cfg_branch_limit(func: &SsaArtifact) -> usize { + let summary = func.function().cfg_risk_summary(); + match summary.switch_block_count { + 0 => 8, + 1..=2 => 10, + _ => 12, + } +} + +fn limited_branch_blocks(func: &SsaArtifact, limit: usize) -> Vec<(u64, u64, u64)> { + if limit == 0 { + return Vec::new(); + } + + let mut queue = VecDeque::from([func.entry]); + let mut visited = BTreeSet::new(); + let mut selected = Vec::new(); + + while let Some(block_addr) = queue.pop_front() { + if !visited.insert(block_addr) { + continue; + } + + if let Some(block) = func.cfg().get_block(block_addr) + && let r2ssa::BlockTerminator::ConditionalBranch { + true_target, + false_target, + } = block.terminator + { + selected.push((block_addr, true_target, false_target)); + if selected.len() >= limit { + break; + } + } + + for successor in func.successors(block_addr) { + if !visited.contains(&successor) { + queue.push_back(successor); + } + } + } + + if selected.is_empty() { + collect_branch_blocks(func) + .into_iter() + .take(limit) + .collect() + } else { + selected + } +} + fn symbolic_reachability_status( feasible_paths: usize, budget_exhausted: bool, @@ -95,6 +378,102 @@ fn symbolic_reachability_status( } } +fn collect_symbolic_function_facts_for_branch_blocks<'ctx, F>( + ctx: &'ctx Context, + func: &SsaArtifact, + arch: &ArchSpec, + branch_blocks: &[(u64, u64, u64)], + install_hooks: F, +) -> SymbolicFunctionFacts +where + F: Fn(&mut PathExplorer<'ctx>), +{ + let mut facts = SymbolicFunctionFacts::default(); + + for &(block_addr, true_target, false_target) in branch_blocks { + facts.diagnostics.branches_evaluated += 1; + let predicate_uses_call_result = predicate_depends_on_call_result(func, block_addr); + + let make_state = || { + let mut state = SymState::new(ctx, func.entry); + seed_default_state_for_arch(&mut state, func, Some(arch)); + state + }; + + let mut true_explorer = symbolic_fact_explorer(ctx); + install_hooks(&mut true_explorer); + let true_initial_state = make_state(); + let (compiled_true_status, true_compiled) = compiled_branch_reachability_status( + &true_explorer, + func, + &true_initial_state, + block_addr, + true, + ); + let true_condition = symbolic_condition_hint(true_compiled.as_ref()); + let true_status = if let Some(status) = compiled_true_status { + status + } else if predicate_uses_call_result || func_contains_calls(func) { + SymbolicReachabilityStatus::Unknown + } else { + let paths = true_explorer.find_paths_to(func, make_state(), true_target); + symbolic_reachability_status(paths.len(), true_explorer.budget_exhausted()) + }; + + let mut false_explorer = symbolic_fact_explorer(ctx); + install_hooks(&mut false_explorer); + let false_initial_state = make_state(); + let (compiled_false_status, false_compiled) = compiled_branch_reachability_status( + &false_explorer, + func, + &false_initial_state, + block_addr, + false, + ); + let false_condition = symbolic_condition_hint(false_compiled.as_ref()); + let false_status = if let Some(status) = compiled_false_status { + status + } else if predicate_uses_call_result || func_contains_calls(func) { + SymbolicReachabilityStatus::Unknown + } else { + let paths = false_explorer.find_paths_to(func, make_state(), false_target); + symbolic_reachability_status(paths.len(), false_explorer.budget_exhausted()) + }; + + if matches!(true_status, SymbolicReachabilityStatus::Unknown) + || matches!(false_status, SymbolicReachabilityStatus::Unknown) + { + facts.diagnostics.branches_unknown += 1; + } + if matches!( + (true_status, false_status), + ( + SymbolicReachabilityStatus::Reachable, + SymbolicReachabilityStatus::Unreachable + ) | ( + SymbolicReachabilityStatus::Unreachable, + SymbolicReachabilityStatus::Reachable + ) + ) { + facts.diagnostics.branches_pruned += 1; + } + + facts.branch_facts.push(SymbolicBranchFact { + block_addr, + true_target, + false_target, + true_status, + false_status, + true_condition, + false_condition, + true_compiled, + false_compiled, + }); + } + + facts +} + fn install_derived_summary_set<'ctx>( explorer: &mut PathExplorer<'ctx>, registry: &SummaryRegistry<'ctx>, @@ -283,119 +662,127 @@ pub(super) fn collect_symbolic_function_facts_with_derived<'ctx>( registry: &SummaryRegistry<'ctx>, derived: &DerivedSummarySet<'ctx>, ) -> SymbolicFunctionFacts { - let mut facts = SymbolicFunctionFacts::default(); - let branch_blocks = func - .cfg() - .block_addrs() - .filter_map(|block_addr| { - let block = func.cfg().get_block(block_addr)?; - match block.terminator { - r2ssa::BlockTerminator::ConditionalBranch { - true_target, - false_target, - } => Some((block_addr, true_target, false_target)), - _ => None, - } - }) - .collect::>(); - - for (block_addr, true_target, false_target) in branch_blocks { - facts.diagnostics.branches_evaluated += 1; - let predicate_uses_call_result = predicate_depends_on_call_result(func, block_addr); + collect_symbolic_function_facts_with_derived_for_branch_blocks( + ctx, + func, + scope, + arch, + &collect_branch_blocks(func), + summary_profile, + registry, + derived, + symbol_map, + ) +} - let make_state = || { - let mut state = SymState::new(ctx, func.entry); - seed_default_state_for_arch(&mut state, func, Some(arch)); - state - }; +#[allow(clippy::too_many_arguments)] +pub(super) fn collect_symbolic_function_facts_with_derived_for_branch_blocks<'ctx>( + ctx: &'ctx Context, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + arch: &ArchSpec, + branch_blocks: &[(u64, u64, u64)], + summary_profile: SummaryProfile, + registry: &SummaryRegistry<'ctx>, + derived: &DerivedSummarySet<'ctx>, + symbol_map: &HashMap, +) -> SymbolicFunctionFacts { + let facts = collect_symbolic_function_facts_for_branch_blocks( + ctx, + func, + arch, + branch_blocks, + |explorer| { + install_derived_summary_set(explorer, registry, func, scope, derived, symbol_map); + }, + ); + let _ = summary_profile; + finalize_symbolic_function_facts(facts) +} - let mut true_explorer = symbolic_fact_explorer(ctx); - install_derived_summary_set( - &mut true_explorer, - registry, - func, - scope, - derived, - symbol_map, - ); - let true_initial_state = make_state(); - let (compiled_true_status, true_compiled) = compiled_branch_reachability_status( - &true_explorer, - func, - &true_initial_state, - block_addr, - true, - ); - let true_condition = symbolic_condition_hint(true_compiled.as_ref()); - let true_status = if let Some(status) = compiled_true_status { - status - } else if predicate_uses_call_result || func_contains_calls(func) { - SymbolicReachabilityStatus::Unknown - } else { - let paths = true_explorer.find_paths_to(func, make_state(), true_target); - symbolic_reachability_status(paths.len(), true_explorer.budget_exhausted()) - }; +fn collect_large_cfg_symbolic_function_facts( + ctx: &Context, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + arch: &ArchSpec, + symbol_map: &HashMap, + summary_profile: SummaryProfile, +) -> SymbolicFunctionFacts { + collect_large_cfg_symbolic_function_facts_with_limit( + ctx, + func, + scope, + arch, + symbol_map, + summary_profile, + large_cfg_branch_limit(func), + ) +} - let mut false_explorer = symbolic_fact_explorer(ctx); - install_derived_summary_set( - &mut false_explorer, - registry, - func, - scope, - derived, - symbol_map, - ); - let false_initial_state = make_state(); - let (compiled_false_status, false_compiled) = compiled_branch_reachability_status( - &false_explorer, - func, - &false_initial_state, - block_addr, - false, - ); - let false_condition = symbolic_condition_hint(false_compiled.as_ref()); - let false_status = if let Some(status) = compiled_false_status { - status - } else if predicate_uses_call_result || func_contains_calls(func) { - SymbolicReachabilityStatus::Unknown +pub(super) fn collect_large_cfg_symbolic_function_facts_with_limit( + ctx: &Context, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + arch: &ArchSpec, + symbol_map: &HashMap, + summary_profile: SummaryProfile, + branch_limit: usize, +) -> SymbolicFunctionFacts { + let branch_blocks = limited_branch_blocks(func, branch_limit.max(1)); + let mut facts = if let Some(scope) = scope { + if let Some(registry) = SummaryRegistry::with_profile_for_arch(arch, summary_profile) { + let derived = registry.derive_symbolic_summaries(ctx, scope, Some(arch), symbol_map); + collect_symbolic_function_facts_with_derived_for_branch_blocks( + ctx, + func, + Some(scope), + arch, + &branch_blocks, + summary_profile, + ®istry, + &derived, + symbol_map, + ) } else { - let paths = false_explorer.find_paths_to(func, make_state(), false_target); - symbolic_reachability_status(paths.len(), false_explorer.budget_exhausted()) - }; - - if matches!(true_status, SymbolicReachabilityStatus::Unknown) - || matches!(false_status, SymbolicReachabilityStatus::Unknown) - { - facts.diagnostics.branches_unknown += 1; - } - if matches!( - (true_status, false_status), - ( - SymbolicReachabilityStatus::Reachable, - SymbolicReachabilityStatus::Unreachable - ) | ( - SymbolicReachabilityStatus::Unreachable, - SymbolicReachabilityStatus::Reachable + collect_symbolic_function_facts_for_branch_blocks( + ctx, + func, + arch, + &branch_blocks, + |explorer| { + install_symbolic_fact_hooks( + ctx, + explorer, + func, + None, + arch, + summary_profile, + symbol_map, + ); + }, ) - ) { - facts.diagnostics.branches_pruned += 1; } - - facts.branch_facts.push(SymbolicBranchFact { - block_addr, - true_target, - false_target, - true_status, - false_status, - true_condition, - false_condition, - true_compiled, - false_compiled, - }); - } - - let _ = summary_profile; - facts + } else { + collect_symbolic_function_facts_for_branch_blocks( + ctx, + func, + arch, + &branch_blocks, + |explorer| { + install_symbolic_fact_hooks( + ctx, + explorer, + func, + None, + arch, + summary_profile, + symbol_map, + ); + }, + ) + }; + facts.diagnostics.skipped_large_cfg = true; + finalize_symbolic_function_facts(facts) } pub fn collect_symbolic_function_facts( @@ -404,7 +791,14 @@ pub fn collect_symbolic_function_facts( arch: Option<&ArchSpec>, symbol_map: &HashMap, ) -> SymbolicFunctionFacts { - collect_symbolic_function_facts_with_scope(ctx, func, None, arch, symbol_map) + collect_symbolic_function_facts_with_scope_and_profile( + ctx, + func, + None, + arch, + symbol_map, + SummaryProfile::Default, + ) } pub fn collect_symbolic_function_facts_with_scope( @@ -413,23 +807,46 @@ pub fn collect_symbolic_function_facts_with_scope( scope: Option<&PreparedFunctionScope>, arch: Option<&ArchSpec>, symbol_map: &HashMap, +) -> SymbolicFunctionFacts { + collect_symbolic_function_facts_with_scope_and_profile( + ctx, + func, + scope, + arch, + symbol_map, + SummaryProfile::Default, + ) +} + +pub fn collect_symbolic_function_facts_with_scope_and_profile( + ctx: &Context, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + arch: Option<&ArchSpec>, + symbol_map: &HashMap, + summary_profile: SummaryProfile, ) -> SymbolicFunctionFacts { let mut facts = SymbolicFunctionFacts::default(); let Some(arch) = arch else { facts.diagnostics.skipped_missing_arch = true; - return facts; + return finalize_symbolic_function_facts(facts); }; let cfg_summary = func.function().cfg_risk_summary(); if cfg_summary.block_count > 96 || cfg_summary.switch_block_count > 8 { - facts.diagnostics.skipped_large_cfg = true; - return facts; + return collect_large_cfg_symbolic_function_facts( + ctx, + func, + scope, + arch, + symbol_map, + summary_profile, + ); } if let Some(scope) = scope { - let Some(registry) = SummaryRegistry::with_profile_for_arch(arch, SummaryProfile::Default) - else { - return facts; + let Some(registry) = SummaryRegistry::with_profile_for_arch(arch, summary_profile) else { + return finalize_symbolic_function_facts(facts); }; let derived = registry.derive_symbolic_summaries(ctx, scope, Some(arch), symbol_map); return collect_symbolic_function_facts_with_derived( @@ -438,123 +855,121 @@ pub fn collect_symbolic_function_facts_with_scope( Some(scope), arch, symbol_map, - SummaryProfile::Default, + summary_profile, ®istry, &derived, ); } + finalize_symbolic_function_facts(collect_symbolic_function_facts_for_branch_blocks( + ctx, + func, + arch, + &collect_branch_blocks(func), + |explorer| { + install_symbolic_fact_hooks( + ctx, + explorer, + func, + None, + arch, + summary_profile, + symbol_map, + ); + }, + )) +} - let branch_blocks = func - .cfg() - .block_addrs() - .filter_map(|block_addr| { - let block = func.cfg().get_block(block_addr)?; - match block.terminator { - r2ssa::BlockTerminator::ConditionalBranch { - true_target, - false_target, - } => Some((block_addr, true_target, false_target)), - _ => None, - } - }) - .collect::>(); - - for (block_addr, true_target, false_target) in branch_blocks { - facts.diagnostics.branches_evaluated += 1; - let predicate_uses_call_result = predicate_depends_on_call_result(func, block_addr); +#[cfg(test)] +mod tests { + use super::*; + + fn residual_summary(expr: &str) -> BackwardConditionSummary { + BackwardConditionSummary { + simplified: expr.to_string(), + terms: vec![expr.to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::ResidualSearchRequired, + supported_paths: 1, + total_paths: 2, + } + } - let make_state = || { - let mut state = SymState::new(ctx, func.entry); - seed_default_state_for_arch(&mut state, func, Some(arch)); - state - }; + #[test] + fn large_cfg_control_island_promotes_single_bounded_guard_to_likely() { + let facts = finalize_symbolic_function_facts(SymbolicFunctionFacts { + branch_facts: vec![SymbolicBranchFact { + block_addr: 0x1000, + true_target: 0x1010, + false_target: 0x1020, + true_status: SymbolicReachabilityStatus::Unknown, + false_status: SymbolicReachabilityStatus::Unknown, + true_condition: Some("sel == 3".to_string()), + false_condition: None, + true_compiled: Some(residual_summary("sel == 3")), + false_compiled: None, + }], + control_islands: Vec::new(), + diagnostics: SymbolicFunctionFactDiagnostics { + skipped_large_cfg: true, + ..SymbolicFunctionFactDiagnostics::default() + }, + }); - let mut true_explorer = symbolic_fact_explorer(ctx); - install_symbolic_fact_hooks( - ctx, - &mut true_explorer, - func, - None, - arch, - SummaryProfile::Default, - symbol_map, + let island = facts + .control_island_for_block(0x1000) + .expect("control island"); + assert_eq!( + island.kind, + SymbolicControlIslandKind::LargeCfgBranchFrontier ); - let true_initial_state = make_state(); - let (compiled_true_status, true_compiled) = compiled_branch_reachability_status( - &true_explorer, - func, - &true_initial_state, - block_addr, - true, - ); - let true_condition = symbolic_condition_hint(true_compiled.as_ref()); - let true_status = if let Some(status) = compiled_true_status { - status - } else if predicate_uses_call_result || func_contains_calls(func) { - SymbolicReachabilityStatus::Unknown - } else { - let paths = true_explorer.find_paths_to(func, make_state(), true_target); - symbolic_reachability_status(paths.len(), true_explorer.budget_exhausted()) - }; - - let mut false_explorer = symbolic_fact_explorer(ctx); - install_symbolic_fact_hooks( - ctx, - &mut false_explorer, - func, - None, - arch, - SummaryProfile::Default, - symbol_map, - ); - let false_initial_state = make_state(); - let (compiled_false_status, false_compiled) = compiled_branch_reachability_status( - &false_explorer, - func, - &false_initial_state, - block_addr, - false, - ); - let false_condition = symbolic_condition_hint(false_compiled.as_ref()); - let false_status = if let Some(status) = compiled_false_status { - status - } else if predicate_uses_call_result || func_contains_calls(func) { - SymbolicReachabilityStatus::Unknown - } else { - let paths = false_explorer.find_paths_to(func, make_state(), false_target); - symbolic_reachability_status(paths.len(), false_explorer.budget_exhausted()) - }; - - if matches!(true_status, SymbolicReachabilityStatus::Unknown) - || matches!(false_status, SymbolicReachabilityStatus::Unknown) - { - facts.diagnostics.branches_unknown += 1; - } - if matches!( - (true_status, false_status), - ( - SymbolicReachabilityStatus::Reachable, - SymbolicReachabilityStatus::Unreachable - ) | ( - SymbolicReachabilityStatus::Unreachable, - SymbolicReachabilityStatus::Reachable - ) - ) { - facts.diagnostics.branches_pruned += 1; - } + assert_eq!(island.evidence.tier, SemanticConfidence::Likely); + assert!(island.actionable_compiled_condition().is_some()); + } - facts.branch_facts.push(SymbolicBranchFact { - block_addr, - true_target, - false_target, - true_status, - false_status, - true_condition, - false_condition, - true_compiled, - false_compiled, + #[test] + fn control_island_requires_unique_actionable_condition() { + let facts = finalize_symbolic_function_facts(SymbolicFunctionFacts { + branch_facts: vec![SymbolicBranchFact { + block_addr: 0x2000, + true_target: 0x2010, + false_target: 0x2020, + true_status: SymbolicReachabilityStatus::Unknown, + false_status: SymbolicReachabilityStatus::Unknown, + true_condition: Some("x < 4".to_string()), + false_condition: Some("x >= 4".to_string()), + true_compiled: Some(BackwardConditionSummary { + simplified: "x < 4".to_string(), + terms: vec!["x < 4".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::OverApprox, + supported_paths: 1, + total_paths: 2, + }), + false_compiled: Some(BackwardConditionSummary { + simplified: "x >= 4".to_string(), + terms: vec!["x >= 4".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::OverApprox, + supported_paths: 1, + total_paths: 2, + }), + }], + control_islands: Vec::new(), + diagnostics: SymbolicFunctionFactDiagnostics::default(), }); - } - facts + let island = facts + .control_island_for_block(0x2000) + .expect("control island"); + assert!(island.actionable_compiled_condition().is_none()); + } } diff --git a/crates/r2sym/src/semantics/mod.rs b/crates/r2sym/src/semantics/mod.rs index d704a0c..d97ec66 100644 --- a/crates/r2sym/src/semantics/mod.rs +++ b/crates/r2sym/src/semantics/mod.rs @@ -7,17 +7,20 @@ mod vm; pub use artifact::{ CompiledFunctionSemantics, CompiledSemanticArtifact, CompiledSemanticMode, ResidualReason, - SemanticCapability, SemanticMode, SliceClass, + SemanticCapability, SemanticConfidence, SemanticEvidence, SemanticEvidenceAmbiguity, + SemanticEvidenceCoverage, SemanticEvidenceProvenance, SemanticEvidenceReason, + SemanticEvidenceSoundness, SemanticMode, SliceClass, }; pub use cache::stable_scope_hash; pub use compiler::{compile_function_semantics_with_scope, compile_semantic_artifact_with_scope}; pub use facts::{ - SymbolicBranchFact, SymbolicFunctionFactDiagnostics, SymbolicFunctionFacts, - SymbolicReachabilityStatus, collect_symbolic_function_facts, - collect_symbolic_function_facts_with_scope, + SymbolicBranchFact, SymbolicControlFact, SymbolicControlIsland, SymbolicControlIslandKind, + SymbolicFunctionFactDiagnostics, SymbolicFunctionFacts, SymbolicReachabilityStatus, + collect_symbolic_function_facts, collect_symbolic_function_facts_with_scope, }; pub use vm::{ - InterpreterDispatchSummary, InterpreterKind, VmBinaryOp, VmStateUpdate, VmStepSummary, - VmTransferArm, VmUnaryOp, VmValueExpr, + InterpreterDispatchSummary, InterpreterKind, VmBinaryOp, VmGuardCondition, VmGuardedExit, + VmMemoryCondition, VmMemoryRegionRef, VmStateUpdate, VmStepSummary, VmTransferArm, VmUnaryOp, + VmValueExpr, }; pub(crate) use vm::{build_vm_step_summary, classify_interpreter_like}; diff --git a/crates/r2sym/src/semantics/vm.rs b/crates/r2sym/src/semantics/vm.rs index e911bab..541c2d5 100644 --- a/crates/r2sym/src/semantics/vm.rs +++ b/crates/r2sym/src/semantics/vm.rs @@ -1,9 +1,15 @@ use std::collections::{BTreeMap, BTreeSet, VecDeque}; use r2ssa::cfg::BlockTerminator; -use r2ssa::{CFGEdge, SSAOp, SSAVar, SsaArtifact}; +use r2ssa::{CFGEdge, ObjectKind, SSAOp, SSAVar, SsaArtifact, StackAddressBase}; use serde::{Deserialize, Serialize}; +use super::{ + SemanticConfidence, SemanticEvidence, SemanticEvidenceAmbiguity, SemanticEvidenceCoverage, + SemanticEvidenceProvenance, SemanticEvidenceReason, +}; +use crate::MemoryRegionKind; + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum InterpreterKind { SwitchDispatch, @@ -171,6 +177,92 @@ impl VmValueExpr { } } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct VmGuardCondition { + pub expr: String, + pub value: VmValueExpr, + pub expect_nonzero: bool, + pub exact: bool, +} + +impl VmGuardCondition { + pub(crate) fn evaluate(&self, bindings: &BTreeMap) -> Option { + let value = self.value.evaluate_u64(bindings)?; + Some((value != 0) == self.expect_nonzero) + } + + pub fn evidence(&self) -> SemanticEvidence { + if self.exact { + SemanticEvidence::exact() + } else if !matches!(self.value, VmValueExpr::Expr(_)) { + SemanticEvidence::likely(SemanticEvidenceReason::GuardOpaque) + .with_provenance(SemanticEvidenceProvenance::Normalized) + } else { + SemanticEvidence::heuristic(SemanticEvidenceReason::GuardOpaque) + .with_provenance(SemanticEvidenceProvenance::Ranked) + } + } + + pub fn confidence(&self) -> SemanticConfidence { + self.evidence().tier + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct VmGuardedExit { + pub target: u64, + pub guard: VmGuardCondition, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct VmMemoryRegionRef { + pub id: u32, + pub kind: MemoryRegionKind, + pub name: String, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct VmMemoryCondition { + pub region: VmMemoryRegionRef, + pub offset_lo: i64, + pub offset_hi: i64, + pub size: u32, + pub exact_offset: bool, + pub binding: Option, + pub expr: String, + pub value_expr: Option, + pub value: Option, + pub exact_value: bool, +} + +impl VmMemoryCondition { + pub fn evidence(&self) -> SemanticEvidence { + if self.exact_offset && (self.value.is_none() || self.exact_value) { + SemanticEvidence::exact() + } else if self.binding.is_some() || self.exact_offset { + let reason = if self.exact_offset { + SemanticEvidenceReason::ValueOpaque + } else { + SemanticEvidenceReason::AliasAmbiguity + }; + SemanticEvidence::likely(reason) + .with_provenance(SemanticEvidenceProvenance::Normalized) + .with_ambiguity(if self.exact_offset { + SemanticEvidenceAmbiguity::Single + } else { + SemanticEvidenceAmbiguity::Bounded + }) + } else { + SemanticEvidence::heuristic(SemanticEvidenceReason::AliasAmbiguity) + .with_coverage(SemanticEvidenceCoverage::Bounded) + } + } + + pub fn confidence(&self) -> SemanticConfidence { + self.evidence().tier + } +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct VmStateUpdate { pub output: String, @@ -179,20 +271,122 @@ pub struct VmStateUpdate { pub exact: bool, } +impl VmStateUpdate { + pub fn evidence(&self) -> SemanticEvidence { + if self.exact { + SemanticEvidence::exact() + } else if !matches!(self.value, VmValueExpr::Expr(_)) { + SemanticEvidence::likely(SemanticEvidenceReason::ValueOpaque) + .with_provenance(SemanticEvidenceProvenance::Normalized) + } else { + SemanticEvidence::heuristic(SemanticEvidenceReason::ValueOpaque) + .with_provenance(SemanticEvidenceProvenance::Ranked) + } + } + + pub fn confidence(&self) -> SemanticConfidence { + self.evidence().tier + } +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct VmTransferArm { pub handler_target: u64, pub case_values: Vec, pub region_blocks: Vec, pub exit_targets: Vec, + pub exit_guards: Vec, pub state_updates: Vec, pub selector_update: Option, + pub memory_reads: Vec, + pub memory_writes: Vec, + pub residual_guards: bool, + pub residual_memory_effects: bool, pub exact: bool, pub redispatch: bool, pub may_return: bool, pub truncated: bool, } +impl VmTransferArm { + pub fn evidence(&self) -> SemanticEvidence { + if self.exact { + return SemanticEvidence::exact(); + } + if self.truncated { + return if self.case_values.is_empty() + && self.exit_targets.is_empty() + && self.state_updates.is_empty() + && self.memory_reads.is_empty() + && self.memory_writes.is_empty() + { + SemanticEvidence::residual(SemanticEvidenceReason::TruncatedTransfer) + .with_coverage(SemanticEvidenceCoverage::Partial) + .with_budget_limited(true) + } else { + SemanticEvidence::heuristic(SemanticEvidenceReason::TruncatedTransfer) + .with_coverage(SemanticEvidenceCoverage::Bounded) + .with_budget_limited(true) + }; + } + if !self.residual_guards + && !self.residual_memory_effects + && self + .state_updates + .iter() + .all(|update| update.evidence().is_reliable()) + && self + .selector_update + .as_ref() + .is_none_or(|update| update.evidence().is_reliable()) + && self + .exit_guards + .iter() + .all(|guard| guard.guard.evidence().is_reliable()) + && self + .memory_reads + .iter() + .all(|effect| effect.evidence().is_reliable()) + && self + .memory_writes + .iter() + .all(|effect| effect.evidence().is_reliable()) + { + let mut evidence = + SemanticEvidence::likely(SemanticEvidenceReason::PartialPathCoverage) + .with_coverage(SemanticEvidenceCoverage::Bounded) + .with_provenance(SemanticEvidenceProvenance::Normalized); + if self.redispatch || self.may_return { + evidence = evidence.with_coverage(SemanticEvidenceCoverage::Partial); + } + return evidence; + } + if self.case_values.is_empty() + && self.exit_targets.is_empty() + && self.state_updates.is_empty() + && self.memory_reads.is_empty() + && self.memory_writes.is_empty() + { + SemanticEvidence::residual(SemanticEvidenceReason::PartialPathCoverage) + } else { + let mut evidence = + SemanticEvidence::heuristic(SemanticEvidenceReason::DerivedFromRanking) + .with_coverage(SemanticEvidenceCoverage::Bounded); + if self.residual_guards { + evidence = evidence.with_reason(SemanticEvidenceReason::GuardOpaque); + } + if self.residual_memory_effects { + evidence = evidence.with_reason(SemanticEvidenceReason::AliasAmbiguity); + } + evidence + } + } + + pub fn confidence(&self) -> SemanticConfidence { + self.evidence().tier + } +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct VmStepSummary { pub kind: InterpreterKind, @@ -210,6 +404,9 @@ pub struct VmStepSummary { pub handler_state_inputs: BTreeMap>, pub handler_state_outputs: BTreeMap>, pub handler_state_updates: BTreeMap>, + pub handler_exit_guards: BTreeMap>, + pub handler_memory_read_effects: BTreeMap>, + pub handler_memory_write_effects: BTreeMap>, pub handler_memory_reads: BTreeMap, pub handler_memory_writes: BTreeMap, pub handler_calls: BTreeMap, @@ -230,6 +427,9 @@ struct HandlerRegionSummary { state_inputs: Vec, state_outputs: Vec, state_updates: Vec, + exit_guards: Vec, + memory_read_effects: Vec, + memory_write_effects: Vec, memory_reads: usize, memory_writes: usize, calls: usize, @@ -238,6 +438,8 @@ struct HandlerRegionSummary { reenters_dispatch: bool, may_return: bool, truncated: bool, + residual_guards: bool, + residual_memory_effects: bool, } fn record_block_state( @@ -576,6 +778,260 @@ fn classify_vm_op_value(func: &SsaArtifact, op: &SSAOp, depth: u32) -> Option VmValueExpr { + if let Some(var) = func.value_var(value_id) { + return classify_vm_var_value(func, var, depth); + } + VmValueExpr::Expr(format!("value:{value_id:?}")) +} + +fn stack_base_name(base: StackAddressBase) -> &'static str { + match base { + StackAddressBase::FramePointer => "fp", + StackAddressBase::StackPointer => "sp", + } +} + +fn vm_memory_binding_name(region: &VmMemoryRegionRef, offset: i64, size: u32) -> String { + format!("mem:r{}:{offset}:{size}", region.id) +} + +fn vm_memory_region_ref_from_object( + func: &SsaArtifact, + object_id: r2ssa::ObjectId, +) -> Option { + let object = func.objects().object(object_id)?; + let (kind, name) = match &object.kind { + ObjectKind::StackSlot { base, offset } | ObjectKind::FrameObject { base, offset } => ( + MemoryRegionKind::Stack, + format!("stack:{}{:+#x}", stack_base_name(*base), offset), + ), + ObjectKind::Global { space, address } => { + (MemoryRegionKind::Global, format!("{space}:0x{address:x}")) + } + ObjectKind::HeapAlloc { call_site } => ( + MemoryRegionKind::Heap, + format!("heap_alloc@{}", call_site.0), + ), + ObjectKind::EscapedUnknown => return None, + }; + Some(VmMemoryRegionRef { + id: object_id.0, + kind, + name, + }) +} + +fn render_memory_access_expr(op: &SSAOp, func: &SsaArtifact) -> String { + match op { + SSAOp::Load { addr, .. } + | SSAOp::Store { addr, .. } + | SSAOp::LoadLinked { addr, .. } + | SSAOp::StoreConditional { addr, .. } + | SSAOp::AtomicCAS { addr, .. } + | SSAOp::LoadGuarded { addr, .. } + | SSAOp::StoreGuarded { addr, .. } => render_vm_var_expr(func, addr, 0), + _ => "".to_string(), + } +} + +fn vm_memory_write_value(op: &SSAOp, func: &SsaArtifact) -> (Option, bool) { + match op { + SSAOp::Store { val, .. } => { + let value = classify_vm_var_value(func, val, 0); + let exact = value.is_exact(); + (Some(value), exact) + } + _ => (None, false), + } +} + +fn classify_vm_op_value_at_site( + func: &SsaArtifact, + block_addr: u64, + op_idx: usize, + op: &SSAOp, + depth: u32, +) -> Option { + match op { + SSAOp::Load { .. } | SSAOp::LoadLinked { .. } => { + let (reads, _writes, residual) = + vm_memory_conditions_for_op(func, block_addr, op_idx, op); + if !residual + && let [read] = reads.as_slice() + && let Some(binding) = read.binding.as_ref() + { + return Some(VmValueExpr::Var(binding.clone())); + } + classify_vm_op_value(func, op, depth) + } + _ => classify_vm_op_value(func, op, depth), + } +} + +fn vm_memory_conditions_for_op( + func: &SsaArtifact, + block_addr: u64, + op_idx: usize, + op: &SSAOp, +) -> (Vec, Vec, bool) { + let expr = render_memory_access_expr(op, func); + let mut reads = Vec::new(); + let mut writes = Vec::new(); + let mut residual = false; + + if op.is_memory_read() { + let Some(uses) = func.memory_uses_for_op_site(block_addr, op_idx) else { + residual = true; + return (reads, writes, residual); + }; + for use_fact in uses { + let Some(region) = vm_memory_region_ref_from_object(func, use_fact.location.object) + else { + residual = true; + continue; + }; + let binding = Some(vm_memory_binding_name( + ®ion, + use_fact.location.offset, + use_fact.location.size, + )); + reads.push(VmMemoryCondition { + region, + offset_lo: use_fact.location.offset, + offset_hi: use_fact.location.offset, + size: use_fact.location.size, + exact_offset: true, + binding, + expr: expr.clone(), + value_expr: None, + value: None, + exact_value: false, + }); + } + } + + if op.is_memory_write() { + let Some(defs) = func.memory_defs_for_op_site(block_addr, op_idx) else { + residual = true; + return (reads, writes, residual); + }; + let (write_value, exact_value) = vm_memory_write_value(op, func); + if matches!( + op, + SSAOp::StoreConditional { .. } | SSAOp::StoreGuarded { .. } | SSAOp::AtomicCAS { .. } + ) { + residual = true; + } + for def in defs { + let Some(region) = vm_memory_region_ref_from_object(func, def.location.object) else { + residual = true; + continue; + }; + let binding = Some(vm_memory_binding_name( + ®ion, + def.location.offset, + def.location.size, + )); + writes.push(VmMemoryCondition { + region, + offset_lo: def.location.offset, + offset_hi: def.location.offset, + size: def.location.size, + exact_offset: true, + binding, + expr: expr.clone(), + value_expr: write_value.as_ref().map(VmValueExpr::render), + value: write_value.clone(), + exact_value, + }); + } + } + + reads.sort_by(|lhs, rhs| { + ( + lhs.region.id, + &lhs.region.name, + lhs.offset_lo, + lhs.size, + lhs.binding.as_deref(), + &lhs.expr, + ) + .cmp(&( + rhs.region.id, + &rhs.region.name, + rhs.offset_lo, + rhs.size, + rhs.binding.as_deref(), + &rhs.expr, + )) + }); + reads.dedup(); + writes.sort_by(|lhs, rhs| { + ( + lhs.region.id, + &lhs.region.name, + lhs.offset_lo, + lhs.size, + lhs.binding.as_deref(), + &lhs.expr, + lhs.value_expr.as_deref(), + lhs.exact_value, + ) + .cmp(&( + rhs.region.id, + &rhs.region.name, + rhs.offset_lo, + rhs.size, + rhs.binding.as_deref(), + &rhs.expr, + rhs.value_expr.as_deref(), + rhs.exact_value, + )) + }); + writes.dedup(); + + (reads, writes, residual) +} + +fn vm_exit_guards_for_block( + func: &SsaArtifact, + block_addr: u64, + seen_targets: &mut BTreeSet<(u64, bool, String)>, +) -> (Vec, bool) { + let Some(predicate) = func + .predicates() + .predicates + .values() + .find(|fact| fact.block_addr == block_addr) + else { + return (Vec::new(), true); + }; + let value = classify_vm_value_id(func, predicate.condition, 0); + let base_expr = value.render(); + let exact = value.is_exact(); + let true_expr = base_expr.clone(); + let false_expr = format!("!({base_expr})"); + let mut guards = Vec::new(); + for (target, expect_nonzero, expr) in [ + (predicate.true_target, true, true_expr), + (predicate.false_target, false, false_expr), + ] { + if seen_targets.insert((target, expect_nonzero, expr.clone())) { + guards.push(VmGuardedExit { + target, + guard: VmGuardCondition { + expr, + value: value.clone(), + expect_nonzero, + exact, + }, + }); + } + } + (guards, false) +} + fn summarize_handler_region( func: &SsaArtifact, entry: u64, @@ -589,6 +1045,10 @@ fn summarize_handler_region( let mut state_inputs = BTreeSet::new(); let mut state_outputs = BTreeSet::new(); let mut state_updates = BTreeMap::::new(); + let mut exit_guards = Vec::new(); + let mut seen_exit_guards = BTreeSet::new(); + let mut memory_read_effects = Vec::new(); + let mut memory_write_effects = Vec::new(); let mut memory_reads = 0usize; let mut memory_writes = 0usize; let mut calls = 0usize; @@ -596,6 +1056,8 @@ fn summarize_handler_region( let mut reenters_dispatch = false; let mut may_return = false; let mut truncated = false; + let mut residual_guards = false; + let mut residual_memory_effects = false; while let Some((block_addr, depth)) = queue.pop_front() { if !visited.insert(block_addr) { @@ -616,6 +1078,13 @@ fn summarize_handler_region( Some(BlockTerminator::ConditionalBranch { .. }) ) { conditional_branches += 1; + let (block_guards, residual) = + vm_exit_guards_for_block(func, block_addr, &mut seen_exit_guards); + if block_guards.is_empty() { + residual_guards |= residual; + } else { + exit_guards.extend(block_guards); + } } if matches!( cfg_block.map(|block| &block.terminator), @@ -625,13 +1094,20 @@ fn summarize_handler_region( } record_block_state(block, &mut state_inputs, &mut state_outputs); - for op in &block.ops { + for (op_idx, op) in block.ops.iter().enumerate() { if op.is_memory_read() { memory_reads += 1; } if op.is_memory_write() { memory_writes += 1; } + let (reads, writes, residual) = + vm_memory_conditions_for_op(func, block_addr, op_idx, op); + if residual { + residual_memory_effects = true; + } + memory_read_effects.extend(reads); + memory_write_effects.extend(writes); if matches!( op, SSAOp::Call { .. } | SSAOp::CallInd { .. } | SSAOp::CallOther { .. } @@ -643,7 +1119,7 @@ fn summarize_handler_region( && !dst.is_temp() && !dst.name.starts_with("ram:") { - let value = classify_vm_op_value(func, op, 0) + let value = classify_vm_op_value_at_site(func, block_addr, op_idx, op, 0) .unwrap_or_else(|| VmValueExpr::Var(dst.display_name())); state_updates.insert(dst.display_name(), value); } @@ -672,6 +1148,48 @@ fn summarize_handler_region( } } + exit_guards.sort_by(|lhs, rhs| { + (lhs.target, &lhs.guard.expr, lhs.guard.expect_nonzero).cmp(&( + rhs.target, + &rhs.guard.expr, + rhs.guard.expect_nonzero, + )) + }); + memory_read_effects.sort_by(|lhs, rhs| { + ( + lhs.region.id, + &lhs.region.name, + lhs.offset_lo, + lhs.size, + &lhs.expr, + ) + .cmp(&( + rhs.region.id, + &rhs.region.name, + rhs.offset_lo, + rhs.size, + &rhs.expr, + )) + }); + memory_read_effects.dedup(); + memory_write_effects.sort_by(|lhs, rhs| { + ( + lhs.region.id, + &lhs.region.name, + lhs.offset_lo, + lhs.size, + &lhs.expr, + ) + .cmp(&( + rhs.region.id, + &rhs.region.name, + rhs.offset_lo, + rhs.size, + &rhs.expr, + )) + }); + memory_write_effects.dedup(); + HandlerRegionSummary { blocks: visited.into_iter().collect(), state_inputs: state_inputs.into_iter().collect(), @@ -685,6 +1203,9 @@ fn summarize_handler_region( value, }) .collect(), + exit_guards, + memory_read_effects, + memory_write_effects, memory_reads, memory_writes, calls, @@ -693,6 +1214,8 @@ fn summarize_handler_region( reenters_dispatch, may_return, truncated, + residual_guards, + residual_memory_effects, } } @@ -920,6 +1443,9 @@ pub(crate) fn build_vm_step_summary( let mut handler_state_inputs = BTreeMap::new(); let mut handler_state_outputs = BTreeMap::new(); let mut handler_state_updates = BTreeMap::new(); + let mut handler_exit_guards = BTreeMap::new(); + let mut handler_memory_read_effects = BTreeMap::new(); + let mut handler_memory_write_effects = BTreeMap::new(); let mut handler_memory_reads = BTreeMap::new(); let mut handler_memory_writes = BTreeMap::new(); let mut handler_calls = BTreeMap::new(); @@ -960,16 +1486,34 @@ pub(crate) fn build_vm_step_summary( .find(|update| same_logical_name(&update.output, selector)) .cloned() }); + let exact_memory_effects = summary + .memory_read_effects + .iter() + .all(|effect| effect.exact_offset) + && summary + .memory_write_effects + .iter() + .all(|effect| effect.exact_offset && effect.exact_value); let exact = !summary.truncated + && summary.calls == 0 && summary.state_updates.iter().all(|update| update.exact) - && selector_update.as_ref().is_none_or(|update| update.exact); + && selector_update.as_ref().is_none_or(|update| update.exact) + && summary.exit_guards.iter().all(|guard| guard.guard.exact) + && !summary.residual_guards + && exact_memory_effects + && !summary.residual_memory_effects; transfers.push(VmTransferArm { handler_target: target, case_values, region_blocks: summary.blocks.clone(), exit_targets: summary.exit_targets.clone(), + exit_guards: summary.exit_guards.clone(), state_updates: summary.state_updates.clone(), selector_update, + memory_reads: summary.memory_read_effects.clone(), + memory_writes: summary.memory_write_effects.clone(), + residual_guards: summary.residual_guards, + residual_memory_effects: summary.residual_memory_effects, exact, redispatch: summary.reenters_dispatch, may_return: summary.may_return, @@ -979,6 +1523,9 @@ pub(crate) fn build_vm_step_summary( handler_state_inputs.insert(target, summary.state_inputs); handler_state_outputs.insert(target, summary.state_outputs); handler_state_updates.insert(target, summary.state_updates); + handler_exit_guards.insert(target, summary.exit_guards); + handler_memory_read_effects.insert(target, summary.memory_read_effects); + handler_memory_write_effects.insert(target, summary.memory_write_effects); handler_memory_reads.insert(target, summary.memory_reads); handler_memory_writes.insert(target, summary.memory_writes); handler_calls.insert(target, summary.calls); @@ -1024,6 +1571,9 @@ pub(crate) fn build_vm_step_summary( handler_state_inputs, handler_state_outputs, handler_state_updates, + handler_exit_guards, + handler_memory_read_effects, + handler_memory_write_effects, handler_memory_reads, handler_memory_writes, handler_calls, diff --git a/crates/r2sym/src/solver.rs b/crates/r2sym/src/solver.rs index 750a9d8..3f0eeb3 100644 --- a/crates/r2sym/src/solver.rs +++ b/crates/r2sym/src/solver.rs @@ -4,13 +4,14 @@ //! path feasibility and extracting concrete values. use std::cell::RefCell; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; +use std::rc::Rc; use std::time::Duration; -use z3::ast::{Ast, BV, Bool}; -use z3::{Context, Model, Params, Solver}; +use z3::ast::{Ast, BV, Bool, Dynamic}; +use z3::{AstKind, Context, DeclKind, Model, Params, Solver}; -use crate::state::SymState; +use crate::state::{ConstraintCursorKey, SymState}; use crate::value::SymValue; /// Result of a satisfiability check. @@ -53,44 +54,290 @@ pub struct SolverStats { pub struct SymSolver<'ctx> { /// The Z3 context. ctx: &'ctx Context, - /// The underlying Z3 solver. + /// The user-visible manual solver. solver: Solver, + /// Incremental solver for exact state-cursor queries. + state_solver: Solver, + /// Incremental solver for query-local selected constraints. + selection_solver: Solver, + /// Scratch solver for sliced one-shot queries. + scratch_solver: Solver, /// Timeout in milliseconds (0 = no timeout). - _timeout_ms: u32, + timeout_ms: u32, /// Memoized satisfiability results for state queries. - sat_cache: RefCell>, + sat_cache: RefCell>, + /// Cached dependency and partition analysis for cursor-based states. + analysis_cache: RefCell, + /// Current assertion stack for the incremental state solver. + state_session: RefCell, + /// Current assertion set for the incremental selection solver. + selection_session: RefCell, /// Internal counters used for perf/debug summaries. stats: RefCell, } +#[derive(Debug, Clone)] +struct SolverCursorSession { + asserted_stack: Vec, +} + +#[derive(Debug, Clone, Default)] +struct SelectionSolverSession { + asserted_key: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum SolverMode { + ExploreFast, + QueryDeep, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +struct DependencyAtom(Dynamic); + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +struct UnsignedInterval { + min: u64, + max: u64, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +struct AbstractValueFacts { + exact: Option, + interval: Option, + nonzero: bool, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +struct AbstractFacts { + unsat: bool, + values: HashMap, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +struct ConstraintSelectionKey { + state_key: ConstraintCursorKey, + full_state: bool, + partition_ids: Vec, +} + +#[derive(Clone)] +struct ConstraintSelection { + key: ConstraintSelectionKey, + constraints: Vec, + abstract_facts: AbstractFacts, + is_full_state: bool, +} + +#[derive(Clone, Default)] +struct ConstraintPartition { + id: usize, + constraints: Vec, + atoms: HashSet, + abstract_facts: AbstractFacts, +} + +#[derive(Clone, Default)] +struct StateConstraintAnalysis { + ground_constraints: Vec, + ground_abstract_facts: AbstractFacts, + partitions: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +struct PartitionSatCacheKey { + state_key: ConstraintCursorKey, + partition_id: usize, +} + +#[derive(Clone)] +struct CursorConstraintFacts { + parent: ConstraintCursorKey, + deps: HashSet, + abstract_delta: AbstractFacts, + is_ground: bool, +} + +impl Default for CursorConstraintFacts { + fn default() -> Self { + Self { + parent: ConstraintCursorKey::ROOT, + deps: HashSet::new(), + abstract_delta: AbstractFacts::default(), + is_ground: true, + } + } +} + +#[derive(Default)] +struct ConstraintAnalysisCache { + cursor_facts: HashMap, + state_cache: HashMap>, + ground_sat_cache: HashMap, + partition_sat_cache: HashMap, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum DeepQueryKind { + Predicate, + Model, +} + +impl Default for SolverCursorSession { + fn default() -> Self { + Self { + asserted_stack: vec![ConstraintCursorKey::ROOT], + } + } +} + +impl AbstractValueFacts { + fn update_exact(&mut self, value: u64) { + if let Some(existing) = self.exact { + if existing != value { + self.interval = Some(UnsignedInterval { min: 1, max: 0 }); + } + } else { + self.exact = Some(value); + } + self.tighten_interval(value, value); + if value != 0 { + self.nonzero = true; + } + } + + fn tighten_interval(&mut self, min: u64, max: u64) { + let interval = self.interval.get_or_insert(UnsignedInterval { + min: 0, + max: u64::MAX, + }); + interval.min = interval.min.max(min); + interval.max = interval.max.min(max); + } + + fn mark_nonzero(&mut self) { + self.nonzero = true; + if let Some(0) = self.exact { + self.interval = Some(UnsignedInterval { min: 1, max: 0 }); + } else if let Some(interval) = &mut self.interval + && interval.min == 0 + && interval.max > 0 + { + interval.min = 1; + } + } + + fn is_unsat(&self) -> bool { + matches!(self.interval, Some(UnsignedInterval { min, max }) if min > max) + } +} + +impl AbstractFacts { + fn merge(&mut self, other: &Self) { + if self.unsat || other.unsat { + self.unsat = true; + return; + } + for (atom, other_facts) in &other.values { + let facts = self.values.entry(atom.clone()).or_default(); + if let Some(value) = other_facts.exact { + facts.update_exact(value); + } + if let Some(interval) = &other_facts.interval { + facts.tighten_interval(interval.min, interval.max); + } + if other_facts.nonzero { + facts.mark_nonzero(); + } + if facts.is_unsat() { + self.unsat = true; + return; + } + } + } + + fn record_eq(&mut self, atom: DependencyAtom, value: u64) { + let facts = self.values.entry(atom).or_default(); + facts.update_exact(value); + if facts.is_unsat() { + self.unsat = true; + } + } + + fn record_ne(&mut self, atom: DependencyAtom, value: u64) { + let facts = self.values.entry(atom).or_default(); + if facts.exact == Some(value) { + self.unsat = true; + return; + } + if value == 0 { + facts.mark_nonzero(); + if facts.is_unsat() { + self.unsat = true; + } + } + } + + fn record_min(&mut self, atom: DependencyAtom, value: u64) { + let facts = self.values.entry(atom).or_default(); + facts.tighten_interval(value, u64::MAX); + if facts.is_unsat() { + self.unsat = true; + } + } + + fn record_max(&mut self, atom: DependencyAtom, value: u64) { + let facts = self.values.entry(atom).or_default(); + facts.tighten_interval(0, value); + if facts.is_unsat() { + self.unsat = true; + } + } +} + impl<'ctx> SymSolver<'ctx> { + fn configured_solver(timeout_ms: u32) -> Solver { + let solver = Solver::new(); + if timeout_ms != 0 { + let mut params = Params::new(); + params.set_u32("timeout", timeout_ms); + solver.set_params(¶ms); + } + solver + } + /// Create a new solver. pub fn new(ctx: &'ctx Context) -> Self { - let solver = Solver::new(); Self { ctx, - solver, - _timeout_ms: 0, + solver: Self::configured_solver(0), + state_solver: Self::configured_solver(0), + selection_solver: Self::configured_solver(0), + scratch_solver: Self::configured_solver(0), + timeout_ms: 0, sat_cache: RefCell::new(HashMap::new()), + analysis_cache: RefCell::new(ConstraintAnalysisCache::default()), + state_session: RefCell::new(SolverCursorSession::default()), + selection_session: RefCell::new(SelectionSolverSession::default()), stats: RefCell::new(SolverStats::default()), } } /// Create a solver with a timeout. pub fn with_timeout(ctx: &'ctx Context, timeout: Duration) -> Self { - let solver = Solver::new(); let timeout_ms = timeout.as_millis() as u32; - // Set timeout parameter - let mut params = Params::new(); - params.set_u32("timeout", timeout_ms); - solver.set_params(¶ms); - Self { ctx, - solver, - _timeout_ms: timeout_ms, + solver: Self::configured_solver(timeout_ms), + state_solver: Self::configured_solver(timeout_ms), + selection_solver: Self::configured_solver(timeout_ms), + scratch_solver: Self::configured_solver(timeout_ms), + timeout_ms, sat_cache: RefCell::new(HashMap::new()), + analysis_cache: RefCell::new(ConstraintAnalysisCache::default()), + state_session: RefCell::new(SolverCursorSession::default()), + selection_session: RefCell::new(SelectionSolverSession::default()), stats: RefCell::new(SolverStats::default()), } } @@ -129,6 +376,35 @@ impl<'ctx> SymSolver<'ctx> { self.sat_cache.borrow_mut().clear(); } + fn reset_state_solver(&self) { + self.state_solver.reset(); + if self.timeout_ms != 0 { + let mut params = Params::new(); + params.set_u32("timeout", self.timeout_ms); + self.state_solver.set_params(¶ms); + } + *self.state_session.borrow_mut() = SolverCursorSession::default(); + } + + fn reset_scratch_solver(&self) { + self.scratch_solver.reset(); + if self.timeout_ms != 0 { + let mut params = Params::new(); + params.set_u32("timeout", self.timeout_ms); + self.scratch_solver.set_params(¶ms); + } + } + + fn reset_selection_solver(&self) { + self.selection_solver.reset(); + if self.timeout_ms != 0 { + let mut params = Params::new(); + params.set_u32("timeout", self.timeout_ms); + self.selection_solver.set_params(¶ms); + } + *self.selection_session.borrow_mut() = SelectionSolverSession::default(); + } + fn solver_assert(&self, constraint: &Bool) { self.solver.assert(constraint); } @@ -172,12 +448,813 @@ impl<'ctx> SymSolver<'ctx> { self.solver.reset(); } - fn state_sat_cache_key(&self, state: &SymState<'ctx>) -> String { - match state.constraints() { - [] => "true".to_string(), - [constraint] => constraint.simplify().to_string(), - _ => state.path_condition().simplify().to_string(), + fn state_sat_cache_key(&self, state: &SymState<'ctx>) -> ConstraintCursorKey { + state.constraint_cursor_key() + } + + fn ensure_state_solver_cursor(&self, state: &SymState<'ctx>) { + let target_suffix = match state.constraint_suffix_from_cursor(ConstraintCursorKey::ROOT) { + Some(suffix) => suffix, + None => { + self.reset_state_solver(); + return; + } + }; + let target_keys: Vec<_> = std::iter::once(ConstraintCursorKey::ROOT) + .chain(target_suffix.iter().map(|(key, _)| *key)) + .collect(); + + let mut session = self.state_session.borrow_mut(); + let common_prefix_len = session + .asserted_stack + .iter() + .zip(target_keys.iter()) + .take_while(|(a, b)| a == b) + .count(); + + if common_prefix_len == 0 { + drop(session); + self.reset_state_solver(); + session = self.state_session.borrow_mut(); + } else { + let current_len = session.asserted_stack.len(); + if current_len > common_prefix_len { + self.state_solver + .pop((current_len - common_prefix_len) as u32); + session.asserted_stack.truncate(common_prefix_len); + } + } + + for (key, constraint) in target_suffix + .iter() + .skip(session.asserted_stack.len().saturating_sub(1)) + { + self.state_solver.push(); + self.state_solver.assert(constraint); + session.asserted_stack.push(*key); + } + } + + fn eval_value_in_model(&self, model: &Model, value: &SymValue<'ctx>) -> Option { + let bv = value.to_bv(self.ctx); + model.eval(&bv, true)?.as_u64() + } + + fn extract_dependency_atom(expr: &Dynamic) -> Option { + let is_uninterpreted_leaf = expr.is_const() + && matches!( + expr.safe_decl().map(|decl| decl.kind()), + Ok(DeclKind::UNINTERPRETED) + ); + if is_uninterpreted_leaf { + return Some(DependencyAtom(expr.clone())); + } + + if expr.children().is_empty() && !matches!(expr.kind(), AstKind::Numeral) { + return Some(DependencyAtom(expr.clone())); + } + + None + } + + fn extract_const_u64(expr: &Dynamic) -> Option { + expr.as_bv()?.as_u64() + } + + fn extract_leaf_const_pair(left: &Dynamic, right: &Dynamic) -> Option<(DependencyAtom, u64)> { + if let (Some(atom), Some(value)) = ( + Self::extract_dependency_atom(left), + Self::extract_const_u64(right), + ) { + return Some((atom, value)); + } + if let (Some(value), Some(atom)) = ( + Self::extract_const_u64(left), + Self::extract_dependency_atom(right), + ) { + return Some((atom, value)); + } + None + } + + fn apply_abstract_constraint(facts: &mut AbstractFacts, expr: &Dynamic, positive: bool) { + if facts.unsat { + return; + } + + if let Some(bool_ast) = expr.as_bool() + && let Some(value) = bool_ast.as_bool() + { + if value != positive { + facts.unsat = true; + } + return; + } + + let decl = match expr.safe_decl() { + Ok(decl) => decl, + Err(_) => return, + }; + let children = expr.children(); + + match (decl.kind(), positive) { + (DeclKind::AND, true) => { + for child in &children { + Self::apply_abstract_constraint(facts, child, true); + if facts.unsat { + return; + } + } + } + (DeclKind::NOT, _) => { + if let Some(child) = children.first() { + Self::apply_abstract_constraint(facts, child, !positive); + } + } + (DeclKind::EQ, true) if children.len() == 2 => { + if let Some((atom, value)) = + Self::extract_leaf_const_pair(&children[0], &children[1]) + { + facts.record_eq(atom, value); + } + } + (DeclKind::EQ, false) if children.len() == 2 => { + if let Some((atom, value)) = + Self::extract_leaf_const_pair(&children[0], &children[1]) + { + facts.record_ne(atom, value); + } + } + (DeclKind::ULEQ, true) if children.len() == 2 => { + if let Some((atom, value)) = + Self::extract_leaf_const_pair(&children[0], &children[1]) + { + if Self::extract_dependency_atom(&children[0]).is_some() { + facts.record_max(atom, value); + } else { + facts.record_min(atom, value); + } + } + } + (DeclKind::UGEQ, true) if children.len() == 2 => { + if let Some((atom, value)) = + Self::extract_leaf_const_pair(&children[0], &children[1]) + { + if Self::extract_dependency_atom(&children[0]).is_some() { + facts.record_min(atom, value); + } else { + facts.record_max(atom, value); + } + } + } + (DeclKind::ULT, true) if children.len() == 2 => { + if let Some((atom, value)) = + Self::extract_leaf_const_pair(&children[0], &children[1]) + { + if Self::extract_dependency_atom(&children[0]).is_some() { + if value == 0 { + facts.unsat = true; + } else { + facts.record_max(atom, value - 1); + } + } else { + facts.record_min(atom, value.saturating_add(1)); + } + } + } + (DeclKind::UGT, true) if children.len() == 2 => { + if let Some((atom, value)) = + Self::extract_leaf_const_pair(&children[0], &children[1]) + { + if Self::extract_dependency_atom(&children[0]).is_some() { + facts.record_min(atom, value.saturating_add(1)); + } else if value == 0 { + facts.unsat = true; + } else { + facts.record_max(atom, value - 1); + } + } + } + _ => {} + } + } + + fn abstract_facts_for_constraints(constraints: &[Bool]) -> AbstractFacts { + let mut facts = AbstractFacts::default(); + for constraint in constraints { + Self::apply_abstract_constraint(&mut facts, &Dynamic::from_ast(constraint), true); + if facts.unsat { + break; + } + } + facts + } + + fn abstract_summary_for_value( + &self, + facts: &AbstractFacts, + value: &SymValue<'ctx>, + ) -> Option { + match value { + SymValue::Concrete { value, .. } => Some(AbstractValueFacts { + exact: Some(*value), + interval: Some(UnsignedInterval { + min: *value, + max: *value, + }), + nonzero: *value != 0, + }), + SymValue::Symbolic { ast, .. } => { + let atom = Self::extract_dependency_atom(&Dynamic::from_ast(ast))?; + facts.values.get(&atom).cloned() + } + SymValue::Unknown { .. } => None, + } + } + + fn collect_dynamic_dependencies( + expr: &Dynamic, + out: &mut HashSet, + seen: &mut HashSet, + ) { + if !seen.insert(expr.clone()) { + return; + } + + if let Some(bool_ast) = expr.as_bool() + && bool_ast.as_bool().is_some() + { + return; + } + + if let Some(atom) = Self::extract_dependency_atom(expr) { + out.insert(atom); + return; + } + + let children = expr.children(); + if children.is_empty() { + if !matches!(expr.kind(), AstKind::Numeral) { + out.insert(DependencyAtom(expr.clone())); + } + return; + } + + for child in children { + Self::collect_dynamic_dependencies(&child, out, seen); + } + } + + fn expr_dependencies_bool(&self, expr: &Bool) -> HashSet { + let mut deps = HashSet::new(); + let mut seen = HashSet::new(); + Self::collect_dynamic_dependencies(&Dynamic::from_ast(expr), &mut deps, &mut seen); + deps + } + + fn expr_dependencies_bv(&self, expr: &BV) -> HashSet { + let mut deps = HashSet::new(); + let mut seen = HashSet::new(); + Self::collect_dynamic_dependencies(&Dynamic::from_ast(expr), &mut deps, &mut seen); + deps + } + + fn state_constraint_pairs(&self, state: &SymState<'ctx>) -> Vec<(ConstraintCursorKey, Bool)> { + state + .constraint_suffix_from_cursor(ConstraintCursorKey::ROOT) + .unwrap_or_default() + } + + fn constraint_facts_for_key( + &self, + key: ConstraintCursorKey, + parent: ConstraintCursorKey, + constraint: &Bool, + ) -> CursorConstraintFacts { + if let Some(cached) = self.analysis_cache.borrow().cursor_facts.get(&key) { + return cached.clone(); + } + + let deps = self.expr_dependencies_bool(constraint); + let abstract_delta = Self::abstract_facts_for_constraints(std::slice::from_ref(constraint)); + let facts = CursorConstraintFacts { + parent, + is_ground: deps.is_empty(), + deps, + abstract_delta, + }; + self.analysis_cache + .borrow_mut() + .cursor_facts + .insert(key, facts.clone()); + facts + } + + fn ensure_cursor_facts( + &self, + state: &SymState<'ctx>, + ) -> Vec<( + ConstraintCursorKey, + ConstraintCursorKey, + CursorConstraintFacts, + )> { + let pairs = self.state_constraint_pairs(state); + let mut out = Vec::with_capacity(pairs.len()); + let mut parent = ConstraintCursorKey::ROOT; + for (key, constraint) in pairs { + let facts = self.constraint_facts_for_key(key, parent, &constraint); + parent = key; + out.push((key, facts.parent, facts)); + } + out + } + + fn extend_state_constraint_analysis( + &self, + base: &StateConstraintAnalysis, + constraint: &Bool, + facts: &CursorConstraintFacts, + ) -> StateConstraintAnalysis { + let mut next = base.clone(); + if facts.is_ground { + next.ground_constraints.push(constraint.clone()); + next.ground_abstract_facts.merge(&facts.abstract_delta); + return next; + } + + let mut merged_partition = ConstraintPartition { + id: 0, + constraints: vec![constraint.clone()], + atoms: facts.deps.clone(), + abstract_facts: facts.abstract_delta.clone(), + }; + let mut first_match = None; + let mut rebuilt = Vec::with_capacity(next.partitions.len() + 1); + for partition in next.partitions { + if partition.atoms.is_disjoint(&facts.deps) { + rebuilt.push(partition); + continue; + } + if first_match.is_none() { + first_match = Some(rebuilt.len()); + } + merged_partition.constraints.extend(partition.constraints); + merged_partition.atoms.extend(partition.atoms); + merged_partition + .abstract_facts + .merge(&partition.abstract_facts); + } + + let insert_at = first_match.unwrap_or(rebuilt.len()); + rebuilt.insert(insert_at, merged_partition); + for (id, partition) in rebuilt.iter_mut().enumerate() { + partition.id = id; + } + next.partitions = rebuilt; + next + } + + fn state_constraint_pairs_with_facts( + &self, + state: &SymState<'ctx>, + ) -> Vec<( + ConstraintCursorKey, + ConstraintCursorKey, + Bool, + CursorConstraintFacts, + )> { + self.state_constraint_pairs(state) + .into_iter() + .zip(self.ensure_cursor_facts(state)) + .map(|((key, constraint), (fact_key, parent, facts))| { + debug_assert_eq!(key, fact_key); + (key, parent, constraint, facts) + }) + .collect() + } + + fn distinct_dependency_atom_count(&self, state: &SymState<'ctx>) -> usize { + let mut atoms = HashSet::new(); + for (_, _, facts) in self.ensure_cursor_facts(state) { + atoms.extend(facts.deps); + } + atoms.len() + } + + fn state_constraint_analysis(&self, state: &SymState<'ctx>) -> Rc { + let cache_key = state.constraint_cursor_key(); + if let Some(cached) = self.analysis_cache.borrow().state_cache.get(&cache_key) { + return cached.clone(); + } + + let pairs = self.state_constraint_pairs_with_facts(state); + if pairs.is_empty() { + let empty = Rc::new(StateConstraintAnalysis::default()); + self.analysis_cache + .borrow_mut() + .state_cache + .insert(cache_key, empty.clone()); + return empty; + } + + let mut cached_prefix = StateConstraintAnalysis::default(); + let mut start_index = 0usize; + for (index, (key, _, _, _)) in pairs.iter().enumerate().rev() { + if let Some(existing) = self.analysis_cache.borrow().state_cache.get(key).cloned() { + cached_prefix = (*existing).clone(); + start_index = index + 1; + break; + } + } + + let mut current = cached_prefix; + for (key, _, constraint, facts) in pairs.iter().skip(start_index) { + current = self.extend_state_constraint_analysis(¤t, constraint, facts); + let cached = Rc::new(current.clone()); + self.analysis_cache + .borrow_mut() + .state_cache + .insert(*key, cached); + } + + self.analysis_cache + .borrow() + .state_cache + .get(&cache_key) + .cloned() + .unwrap_or_else(|| Rc::new(current)) + } + + fn cached_state_constraint_analysis( + &self, + state: &SymState<'ctx>, + ) -> Option> { + self.analysis_cache + .borrow() + .state_cache + .get(&state.constraint_cursor_key()) + .cloned() + } + + fn should_build_partition_analysis(&self, state: &SymState<'ctx>, kind: DeepQueryKind) -> bool { + if matches!(kind, DeepQueryKind::Model) { + return true; + } + + let constraint_count = state.constraints().len(); + if constraint_count >= 6 { + return true; + } + if constraint_count < 4 { + return false; + } + + self.distinct_dependency_atom_count(state) >= 2 + } + + fn analysis_for_mode( + &self, + state: &SymState<'ctx>, + mode: SolverMode, + kind: DeepQueryKind, + ) -> Option> { + match mode { + SolverMode::ExploreFast => self.cached_state_constraint_analysis(state), + SolverMode::QueryDeep if self.should_build_partition_analysis(state, kind) => { + Some(self.state_constraint_analysis(state)) + } + SolverMode::QueryDeep => None, + } + } + + fn selection_for_query_with_analysis( + &self, + state: &SymState<'ctx>, + analysis: &StateConstraintAnalysis, + seed_deps: &HashSet, + ) -> ConstraintSelection { + let mut selected = analysis.ground_constraints.clone(); + let mut abstract_facts = analysis.ground_abstract_facts.clone(); + let mut partition_ids = Vec::new(); + if seed_deps.is_empty() { + let is_full_state = selected.len() == state.constraints().len(); + return ConstraintSelection { + key: ConstraintSelectionKey { + state_key: state.constraint_cursor_key(), + full_state: false, + partition_ids, + }, + constraints: selected, + abstract_facts, + is_full_state, + }; + } + + for partition in &analysis.partitions { + if !partition.atoms.is_disjoint(seed_deps) { + selected.extend(partition.constraints.iter().cloned()); + abstract_facts.merge(&partition.abstract_facts); + partition_ids.push(partition.id); + } + } + + ConstraintSelection { + key: ConstraintSelectionKey { + state_key: state.constraint_cursor_key(), + full_state: false, + partition_ids, + }, + constraints: selected, + abstract_facts, + is_full_state: false, + } + } + + fn full_selection_from_analysis( + &self, + state: &SymState<'ctx>, + analysis: &StateConstraintAnalysis, + ) -> ConstraintSelection { + let mut abstract_facts = analysis.ground_abstract_facts.clone(); + for partition in &analysis.partitions { + abstract_facts.merge(&partition.abstract_facts); + } + ConstraintSelection { + key: ConstraintSelectionKey { + state_key: state.constraint_cursor_key(), + full_state: true, + partition_ids: Vec::new(), + }, + constraints: state.constraints().to_vec(), + abstract_facts, + is_full_state: true, + } + } + + fn full_selection_without_analysis(&self, state: &SymState<'ctx>) -> ConstraintSelection { + ConstraintSelection { + key: ConstraintSelectionKey { + state_key: state.constraint_cursor_key(), + full_state: true, + partition_ids: Vec::new(), + }, + constraints: state.constraints().to_vec(), + abstract_facts: Self::abstract_facts_for_constraints(state.constraints()), + is_full_state: true, + } + } + + fn selection_is_effectively_full_state( + &self, + analysis: &StateConstraintAnalysis, + selection: &ConstraintSelection, + total_constraints: usize, + ) -> bool { + if selection.is_full_state { + return true; + } + if analysis.partitions.len() <= 1 { + return true; + } + if selection.key.partition_ids.len() == analysis.partitions.len() { + return true; + } + selection.constraints.len().saturating_mul(4) >= total_constraints.saturating_mul(3) + || (total_constraints > 6 + && selection.constraints.len().saturating_add(2) >= total_constraints) + } + + fn preferred_selection_for_query( + &self, + state: &SymState<'ctx>, + seed_deps: &HashSet, + kind: DeepQueryKind, + ) -> ConstraintSelection { + let Some(analysis) = self.analysis_for_mode(state, SolverMode::QueryDeep, kind) else { + return self.full_selection_without_analysis(state); + }; + + let selection = self.selection_for_query_with_analysis(state, &analysis, seed_deps); + if self.selection_is_effectively_full_state( + &analysis, + &selection, + state.constraints().len(), + ) { + self.full_selection_from_analysis(state, &analysis) + } else { + selection + } + } + + fn ensure_selection_solver(&self, selection: &ConstraintSelection) { + if self.selection_session.borrow().asserted_key.as_ref() == Some(&selection.key) { + return; + } + self.reset_selection_solver(); + for constraint in &selection.constraints { + self.selection_solver.assert(constraint); + } + self.selection_session.borrow_mut().asserted_key = Some(selection.key.clone()); + } + + fn selection_sat_with_constraint( + &self, + selection: &ConstraintSelection, + constraint: &Bool, + ) -> SatResult { + self.ensure_selection_solver(selection); + self.selection_solver.push(); + self.selection_solver.assert(constraint); + let result = self.selection_solver.check().into(); + self.selection_solver.pop(1); + result + } + + fn selection_find_value( + &self, + selection: &ConstraintSelection, + target: &SymValue<'ctx>, + constraint: &Bool, + ) -> Option { + self.ensure_selection_solver(selection); + self.selection_solver.push(); + self.selection_solver.assert(constraint); + let sat = self.selection_solver.check(); + let out = if sat == z3::SatResult::Sat { + let model = self.selection_solver.get_model()?; + self.eval_value_in_model(&model, target) + } else { + None + }; + self.selection_solver.pop(1); + out + } + + fn selection_contradicts_constraint( + &self, + selection: &ConstraintSelection, + constraint: &Bool, + ) -> bool { + if selection.abstract_facts.unsat { + return true; + } + let mut facts = selection.abstract_facts.clone(); + Self::apply_abstract_constraint(&mut facts, &Dynamic::from_ast(constraint), true); + facts.unsat + } + + fn partitioned_state_result( + &self, + state: &SymState<'ctx>, + mode: SolverMode, + kind: DeepQueryKind, + ) -> Option { + let cache_key = state.constraint_cursor_key(); + let analysis = self.analysis_for_mode(state, mode, kind)?; + if analysis.partitions.len() <= 1 { + return None; + } + + if analysis.ground_abstract_facts.unsat { + return Some(SatResult::Unsat); + } + + if !analysis.ground_constraints.is_empty() { + if let Some(cached) = self + .analysis_cache + .borrow() + .ground_sat_cache + .get(&cache_key) + .copied() + { + match cached { + SatResult::Sat => {} + other => return Some(other), + } + } else { + self.scratch_assert_constraints(&analysis.ground_constraints); + let result: SatResult = self.scratch_solver.check().into(); + if matches!(result, SatResult::Sat | SatResult::Unsat) { + self.analysis_cache + .borrow_mut() + .ground_sat_cache + .insert(cache_key, result); + } + match result { + SatResult::Sat => {} + other => return Some(other), + } + } + } + + self.scratch_assert_constraints(&analysis.ground_constraints); + for partition in &analysis.partitions { + if partition.abstract_facts.unsat { + return Some(SatResult::Unsat); + } + let cache_key = PartitionSatCacheKey { + state_key: state.constraint_cursor_key(), + partition_id: partition.id, + }; + if let Some(cached) = self + .analysis_cache + .borrow() + .partition_sat_cache + .get(&cache_key) + .copied() + { + match cached { + SatResult::Sat => continue, + other => return Some(other), + } + } + + self.scratch_solver.push(); + for constraint in &partition.constraints { + self.scratch_solver.assert(constraint); + } + let result: SatResult = self.scratch_solver.check().into(); + self.scratch_solver.pop(1); + if matches!(result, SatResult::Sat | SatResult::Unsat) { + self.analysis_cache + .borrow_mut() + .partition_sat_cache + .insert(cache_key, result); + } + match result { + SatResult::Sat => {} + other => return Some(other), + } + } + Some(SatResult::Sat) + } + + fn scratch_assert_constraints(&self, constraints: &[Bool]) { + self.reset_scratch_solver(); + for constraint in constraints { + self.scratch_solver.assert(constraint); + } + } + + fn cached_state_sat_result( + &self, + cache_key: ConstraintCursorKey, + mode: SolverMode, + ) -> Option { + match self.sat_cache.borrow().get(&cache_key).copied() { + Some(SatResult::Unknown) if matches!(mode, SolverMode::QueryDeep) => None, + other => other, + } + } + + fn remember_state_sat_result(&self, cache_key: ConstraintCursorKey, result: SatResult) { + if matches!(result, SatResult::Sat | SatResult::Unsat) { + self.sat_cache.borrow_mut().insert(cache_key, result); + } + } + + fn compute_state_sat_result( + &self, + state: &SymState<'ctx>, + mode: SolverMode, + kind: DeepQueryKind, + ) -> SatResult { + let partitioned = match mode { + SolverMode::ExploreFast => { + self.partitioned_state_result(state, mode, DeepQueryKind::Predicate) + } + SolverMode::QueryDeep if self.should_build_partition_analysis(state, kind) => { + self.partitioned_state_result(state, mode, kind) + } + SolverMode::QueryDeep => None, + }; + match partitioned { + Some(SatResult::Unknown) => { + self.ensure_state_solver_cursor(state); + self.state_solver.check().into() + } + Some(other) => other, + None => { + self.ensure_state_solver_cursor(state); + self.state_solver.check().into() + } + } + } + + fn state_sat_result( + &self, + state: &SymState<'ctx>, + mode: SolverMode, + kind: DeepQueryKind, + ) -> SatResult { + let cache_key = self.state_sat_cache_key(state); + if let Some(result) = self.cached_state_sat_result(cache_key, mode) { + return result; } + + let result = self.compute_state_sat_result(state, mode, kind); + self.remember_state_sat_result(cache_key, result); + result } /// Check if a state's path constraints are satisfiable. @@ -185,28 +1262,42 @@ impl<'ctx> SymSolver<'ctx> { self.stats.borrow_mut().sat_queries += 1; let cache_key = self.state_sat_cache_key(state); - if let Some(result) = self.sat_cache.borrow().get(&cache_key).copied() { + if let Some(result) = self.cached_state_sat_result(cache_key, SolverMode::ExploreFast) { self.stats.borrow_mut().sat_cache_hits += 1; return result == SatResult::Sat; } self.stats.borrow_mut().sat_cache_misses += 1; - self.solver.push(); - self.solver_assert_all(state.constraints()); - let result = self.check(); - self.solver.pop(1); - self.sat_cache.borrow_mut().insert(cache_key, result); + let result = + self.compute_state_sat_result(state, SolverMode::ExploreFast, DeepQueryKind::Predicate); + self.remember_state_sat_result(cache_key, result); result == SatResult::Sat } /// Check whether a state's constraints remain satisfiable with one extra constraint. pub fn sat_with_constraint(&self, state: &SymState<'ctx>, constraint: &Bool) -> SatResult { - self.solver.push(); - self.solver_assert_all(state.constraints()); - self.solver_assert(constraint); - let result = self.check(); - self.solver.pop(1); - result + if self.state_sat_result(state, SolverMode::QueryDeep, DeepQueryKind::Predicate) + != SatResult::Sat + { + return SatResult::Unsat; + } + let seed_deps = self.expr_dependencies_bool(constraint); + let selected = + self.preferred_selection_for_query(state, &seed_deps, DeepQueryKind::Predicate); + if self.selection_contradicts_constraint(&selected, constraint) { + return SatResult::Unsat; + } + match self.selection_sat_with_constraint(&selected, constraint) { + SatResult::Unknown if !selected.is_full_state => { + let full = self.full_selection_without_analysis(state); + if self.selection_contradicts_constraint(&full, constraint) { + SatResult::Unsat + } else { + self.selection_sat_with_constraint(&full, constraint) + } + } + other => other, + } } /// Get a concrete model for a state's constraints. @@ -215,25 +1306,33 @@ impl<'ctx> SymSolver<'ctx> { let cache_key = self.state_sat_cache_key(state); if matches!( - self.sat_cache.borrow().get(&cache_key), + self.cached_state_sat_result(cache_key, SolverMode::QueryDeep), Some(SatResult::Unsat) ) { self.stats.borrow_mut().solve_unsat_shortcuts += 1; return None; } - self.solver.push(); - self.solver_assert_all(state.constraints()); + if matches!( + self.partitioned_state_result(state, SolverMode::QueryDeep, DeepQueryKind::Model), + Some(SatResult::Unsat) + ) { + self.remember_state_sat_result(cache_key, SatResult::Unsat); + self.stats.borrow_mut().solve_unsat_shortcuts += 1; + return None; + } - let sat_result = self.check(); + self.ensure_state_solver_cursor(state); + let sat_result: SatResult = self.state_solver.check().into(); let result = if sat_result == SatResult::Sat { - self.get_model().map(|m| SymModel::new(self.ctx, m)) + self.state_solver + .get_model() + .map(|m| SymModel::new(self.ctx, m)) } else { None }; - self.solver.pop(1); - self.sat_cache.borrow_mut().insert(cache_key, sat_result); + self.remember_state_sat_result(cache_key, sat_result); result } @@ -243,16 +1342,36 @@ impl<'ctx> SymSolver<'ctx> { antecedent: &SymState<'ctx>, consequent: &SymState<'ctx>, ) -> Option { - self.solver.push(); - self.solver_assert_all(antecedent.constraints()); - self.solver_assert(&consequent.path_condition().not()); - let result = match self.check() { + if antecedent.constraints_imply_by_prefix(consequent) { + return Some(true); + } + if self.state_sat_result(antecedent, SolverMode::QueryDeep, DeepQueryKind::Predicate) + != SatResult::Sat + { + return Some(true); + } + let consequent_not = consequent.path_condition().not(); + let seed_deps = self.expr_dependencies_bool(&consequent_not); + let selected = + self.preferred_selection_for_query(antecedent, &seed_deps, DeepQueryKind::Predicate); + if self.selection_contradicts_constraint(&selected, &consequent_not) { + return Some(true); + } + match match self.selection_sat_with_constraint(&selected, &consequent_not) { + SatResult::Unknown if !selected.is_full_state => { + let full = self.full_selection_without_analysis(antecedent); + if self.selection_contradicts_constraint(&full, &consequent_not) { + SatResult::Unsat + } else { + self.selection_sat_with_constraint(&full, &consequent_not) + } + } + other => other, + } { SatResult::Unsat => Some(true), SatResult::Sat => Some(false), SatResult::Unknown => None, - }; - self.solver.pop(1); - result + } } /// Evaluate a symbolic value under the current model. @@ -270,18 +1389,26 @@ impl<'ctx> SymSolver<'ctx> { target: &SymValue<'ctx>, constraint: &Bool, ) -> Option { - self.solver.push(); - self.solver_assert_all(state.constraints()); - self.solver_assert(constraint); - - let result = if self.check() == SatResult::Sat { - self.eval(target) - } else { - None - }; - - self.solver.pop(1); - result + if self.state_sat_result(state, SolverMode::QueryDeep, DeepQueryKind::Model) + != SatResult::Sat + { + return None; + } + let mut seed_deps = self.expr_dependencies_bool(constraint); + seed_deps.extend(self.expr_dependencies_bv(&target.to_bv(self.ctx))); + let selected = self.preferred_selection_for_query(state, &seed_deps, DeepQueryKind::Model); + if self.selection_contradicts_constraint(&selected, constraint) { + return None; + } + let selection_result = self.selection_find_value(&selected, target, constraint); + if selection_result.is_some() || selected.is_full_state { + return selection_result; + } + let full = self.full_selection_without_analysis(state); + if self.selection_contradicts_constraint(&full, constraint) { + return None; + } + self.selection_find_value(&full, target, constraint) } /// Check if two symbolic values can be equal. @@ -305,63 +1432,110 @@ impl<'ctx> SymSolver<'ctx> { b.to_bv(self.ctx), ) }; + let mut deps = self.expr_dependencies_bv(&a_bv); + deps.extend(self.expr_dependencies_bv(&b_bv)); + let selection = self.preferred_selection_for_query(state, &deps, DeepQueryKind::Predicate); + if let (Some(left), Some(right)) = ( + self.abstract_summary_for_value(&selection.abstract_facts, a), + self.abstract_summary_for_value(&selection.abstract_facts, b), + ) { + if let (Some(left_exact), Some(right_exact)) = (left.exact, right.exact) { + return left_exact == right_exact; + } + if let (Some(left_interval), Some(right_interval)) = (left.interval, right.interval) + && (left_interval.max < right_interval.min + || right_interval.max < left_interval.min) + { + return false; + } + } let eq = a_bv.eq(&b_bv); - - self.solver.push(); - self.solver_assert_all(state.constraints()); - self.solver_assert(&eq); - let result = self.check() == SatResult::Sat; - self.solver.pop(1); - - result + matches!(self.sat_with_constraint(state, &eq), SatResult::Sat) } /// Check if a value can be zero. pub fn can_be_zero(&self, state: &SymState<'ctx>, value: &SymValue<'ctx>) -> bool { + let seed_deps = self.expr_dependencies_bv(&value.to_bv(self.ctx)); + let selection = + self.preferred_selection_for_query(state, &seed_deps, DeepQueryKind::Predicate); + if let Some(summary) = self.abstract_summary_for_value(&selection.abstract_facts, value) { + if summary.exact == Some(0) { + return true; + } + if summary.nonzero { + return false; + } + if let Some(interval) = summary.interval + && interval.min > 0 + { + return false; + } + } let bv = value.to_bv(self.ctx); let zero = BV::from_i64(0, value.bits()); let eq = bv.eq(&zero); - - self.solver.push(); - self.solver_assert_all(state.constraints()); - self.solver_assert(&eq); - let result = self.check() == SatResult::Sat; - self.solver.pop(1); - - result + matches!(self.sat_with_constraint(state, &eq), SatResult::Sat) } /// Check if a value must be zero (cannot be non-zero). pub fn must_be_zero(&self, state: &SymState<'ctx>, value: &SymValue<'ctx>) -> bool { + let seed_deps = self.expr_dependencies_bv(&value.to_bv(self.ctx)); + let selection = + self.preferred_selection_for_query(state, &seed_deps, DeepQueryKind::Predicate); + if let Some(summary) = self.abstract_summary_for_value(&selection.abstract_facts, value) { + if summary.exact == Some(0) { + return true; + } + if summary.nonzero { + return false; + } + if let Some(interval) = summary.interval + && interval.min > 0 + { + return false; + } + } let bv = value.to_bv(self.ctx); let zero = BV::from_i64(0, value.bits()); let neq = bv.eq(&zero).not(); - - self.solver.push(); - self.solver_assert_all(state.constraints()); - self.solver_assert(&neq); - let result = self.check() == SatResult::Unsat; - self.solver.pop(1); - - result + matches!(self.sat_with_constraint(state, &neq), SatResult::Unsat) } /// Get the minimum value for a symbolic expression. pub fn minimize(&self, state: &SymState<'ctx>, value: &SymValue<'ctx>) -> Option { - // Binary search for minimum - self.solver.push(); - self.solver_assert_all(state.constraints()); - + if self.state_sat_result(state, SolverMode::QueryDeep, DeepQueryKind::Model) + != SatResult::Sat + { + return None; + } let bv = value.to_bv(self.ctx); + let seed_deps = self.expr_dependencies_bv(&bv); + let mut selection = + self.preferred_selection_for_query(state, &seed_deps, DeepQueryKind::Model); + if selection.abstract_facts.unsat { + return None; + } let bits = value.bits(); - // Start with full range - let mut lo: u64 = 0; - let mut hi: u64 = if bits >= 64 { + if let Some(summary) = self.abstract_summary_for_value(&selection.abstract_facts, value) + && let Some(exact) = summary.exact + { + return Some(exact); + } + + let max_value = if bits >= 64 { u64::MAX } else { (1u64 << bits) - 1 }; + let mut lo: u64 = self + .abstract_summary_for_value(&selection.abstract_facts, value) + .and_then(|summary| summary.interval.map(|interval| interval.min)) + .unwrap_or(0); + let mut hi: u64 = self + .abstract_summary_for_value(&selection.abstract_facts, value) + .and_then(|summary| summary.interval.map(|interval| interval.max)) + .unwrap_or(max_value); let mut result = None; while lo <= hi { @@ -369,41 +1543,62 @@ impl<'ctx> SymSolver<'ctx> { let mid_bv = BV::from_u64(mid, bits); let constraint = bv.bvule(&mid_bv); - self.solver.push(); - self.solver_assert(&constraint); + let mut sat = self.selection_sat_with_constraint(&selection, &constraint); + if sat == SatResult::Unknown && !selection.is_full_state { + selection = self.full_selection_without_analysis(state); + sat = self.selection_sat_with_constraint(&selection, &constraint); + } - if self.check() == SatResult::Sat { - result = self.eval(value); + if sat == SatResult::Sat { + result = self.selection_find_value(&selection, value, &constraint); hi = mid.saturating_sub(1); } else { lo = mid.saturating_add(1); } - self.solver.pop(1); - if lo == 0 && hi == u64::MAX { break; // Prevent infinite loop } } - self.solver.pop(1); result } /// Get the maximum value for a symbolic expression. pub fn maximize(&self, state: &SymState<'ctx>, value: &SymValue<'ctx>) -> Option { - self.solver.push(); - self.solver_assert_all(state.constraints()); - + if self.state_sat_result(state, SolverMode::QueryDeep, DeepQueryKind::Model) + != SatResult::Sat + { + return None; + } let bv = value.to_bv(self.ctx); + let seed_deps = self.expr_dependencies_bv(&bv); + let mut selection = + self.preferred_selection_for_query(state, &seed_deps, DeepQueryKind::Model); + if selection.abstract_facts.unsat { + return None; + } let bits = value.bits(); - let mut lo: u64 = 0; - let mut hi: u64 = if bits >= 64 { + if let Some(summary) = self.abstract_summary_for_value(&selection.abstract_facts, value) + && let Some(exact) = summary.exact + { + return Some(exact); + } + + let max_value = if bits >= 64 { u64::MAX } else { (1u64 << bits) - 1 }; + let mut lo: u64 = self + .abstract_summary_for_value(&selection.abstract_facts, value) + .and_then(|summary| summary.interval.map(|interval| interval.min)) + .unwrap_or(0); + let mut hi: u64 = self + .abstract_summary_for_value(&selection.abstract_facts, value) + .and_then(|summary| summary.interval.map(|interval| interval.max)) + .unwrap_or(max_value); let mut result = None; while lo <= hi { @@ -411,24 +1606,24 @@ impl<'ctx> SymSolver<'ctx> { let mid_bv = BV::from_u64(mid, bits); let constraint = bv.bvuge(&mid_bv); - self.solver.push(); - self.solver_assert(&constraint); + let mut sat = self.selection_sat_with_constraint(&selection, &constraint); + if sat == SatResult::Unknown && !selection.is_full_state { + selection = self.full_selection_without_analysis(state); + sat = self.selection_sat_with_constraint(&selection, &constraint); + } - if self.check() == SatResult::Sat { - result = self.eval(value); + if sat == SatResult::Sat { + result = self.selection_find_value(&selection, value, &constraint); lo = mid.saturating_add(1); } else { hi = mid.saturating_sub(1); } - self.solver.pop(1); - if lo == 0 && hi == u64::MAX { break; } } - self.solver.pop(1); result } } @@ -626,4 +1821,405 @@ mod tests { assert_eq!(stats.solve_calls, 1); assert_eq!(stats.solve_unsat_shortcuts, 1); } + + #[test] + fn test_sat_with_constraint_short_circuits_unsat_base_state() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("x", 32); + state.make_symbolic("y", 32); + + let x = state.get_register("x"); + let y = state.get_register("y"); + state.add_true_constraint(&x.ult(&ctx, &SymValue::concrete(3, 32))); + state.add_true_constraint(&x.eq(&ctx, &SymValue::concrete(5, 32))); + + let solver = SymSolver::new(&ctx); + let y_is_zero = y.to_bv(&ctx).eq(BV::from_u64(0, 32)); + assert_eq!( + solver.sat_with_constraint(&state, &y_is_zero), + SatResult::Unsat + ); + assert_eq!(solver.find_value(&state, &y, &y_is_zero), None); + } + + #[test] + fn test_find_value_and_extrema_with_irrelevant_constraints() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("x", 32); + state.make_symbolic("y", 32); + + let x = state.get_register("x"); + let y = state.get_register("y"); + state.add_true_constraint(&x.eq(&ctx, &SymValue::concrete(10, 32))); + state.constrain_range(&y, 3, 9); + + let solver = SymSolver::new(&ctx); + let y_is_seven = y.to_bv(&ctx).eq(BV::from_u64(7, 32)); + assert_eq!(solver.find_value(&state, &y, &y_is_seven), Some(7)); + assert_eq!(solver.minimize(&state, &y), Some(3)); + assert_eq!(solver.maximize(&state, &y), Some(9)); + } + + #[test] + fn test_state_solver_reuses_prefix_cursor() { + let ctx = Context::thread_local(); + + let mut base = SymState::new(&ctx, 0x1000); + base.make_symbolic("x", 32); + + let x = base.get_register("x"); + base.add_true_constraint(&x.ult(&ctx, &SymValue::concrete(10, 32))); + let shared_key = base.constraint_cursor_key(); + + let mut left = base.fork(); + left.add_true_constraint(&x.eq(&ctx, &SymValue::concrete(1, 32))); + let left_key = left.constraint_cursor_key(); + + let mut right = base.fork(); + right.add_true_constraint(&x.eq(&ctx, &SymValue::concrete(2, 32))); + let right_key = right.constraint_cursor_key(); + + let solver = SymSolver::new(&ctx); + assert!(solver.is_sat(&left)); + assert_eq!( + solver.state_session.borrow().asserted_stack, + vec![ConstraintCursorKey::ROOT, shared_key, left_key] + ); + + assert!(solver.is_sat(&right)); + assert_eq!( + solver.state_session.borrow().asserted_stack, + vec![ConstraintCursorKey::ROOT, shared_key, right_key] + ); + + assert!(solver.is_sat(&base)); + assert_eq!( + solver.state_session.borrow().asserted_stack, + vec![ConstraintCursorKey::ROOT, shared_key] + ); + } + + #[test] + fn test_state_constraint_analysis_partitions_independent_constraints() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("x", 32); + state.make_symbolic("y", 32); + + let x = state.get_register("x"); + let y = state.get_register("y"); + state.add_true_constraint(&x.ult(&ctx, &SymValue::concrete(5, 32))); + state.add_true_constraint(&y.eq(&ctx, &SymValue::concrete(42, 32))); + + let solver = SymSolver::new(&ctx); + let analysis = solver.state_constraint_analysis(&state); + assert_eq!(analysis.ground_constraints.len(), 0); + assert_eq!(analysis.partitions.len(), 2); + } + + #[test] + fn test_query_selection_drops_unrelated_symbolic_constraints() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("w", 32); + state.make_symbolic("x", 32); + state.make_symbolic("y", 32); + state.make_symbolic("z", 32); + + let w = state.get_register("w"); + let x = state.get_register("x"); + let y = state.get_register("y"); + let z = state.get_register("z"); + state.add_true_constraint(&w.eq(&ctx, &SymValue::concrete(1, 32))); + state.add_true_constraint(&x.ult(&ctx, &SymValue::concrete(5, 32))); + state.add_true_constraint(&y.eq(&ctx, &SymValue::concrete(42, 32))); + state.add_true_constraint(&z.eq(&ctx, &SymValue::concrete(9, 32))); + + let solver = SymSolver::new(&ctx); + let seed_deps = solver.expr_dependencies_bv(&x.to_bv(&ctx)); + let selected = + solver.preferred_selection_for_query(&state, &seed_deps, DeepQueryKind::Predicate); + + assert_eq!(selected.constraints.len(), 1); + let x_is_three = x.to_bv(&ctx).eq(BV::from_u64(3, 32)); + assert_eq!( + solver.sat_with_constraint(&state, &x_is_three), + SatResult::Sat + ); + } + + #[test] + fn test_query_selection_keeps_ground_constraints() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("x", 32); + let x = state.get_register("x"); + state.add_true_constraint(&x.ult(&ctx, &SymValue::concrete(5, 32))); + state.add_constraint(Bool::from_bool(false)); + + let solver = SymSolver::new(&ctx); + let seed_deps = solver.expr_dependencies_bv(&x.to_bv(&ctx)); + let selected = + solver.preferred_selection_for_query(&state, &seed_deps, DeepQueryKind::Predicate); + + assert_eq!(selected.constraints.len(), 2); + let x_is_three = x.to_bv(&ctx).eq(BV::from_u64(3, 32)); + assert_eq!( + solver.sat_with_constraint(&state, &x_is_three), + SatResult::Unsat + ); + } + + #[test] + fn test_partitioned_state_result_detects_unsat_component() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("x", 32); + state.make_symbolic("y", 32); + + let x = state.get_register("x"); + let y = state.get_register("y"); + state.add_true_constraint(&x.eq(&ctx, &SymValue::concrete(1, 32))); + state.add_true_constraint(&x.eq(&ctx, &SymValue::concrete(2, 32))); + state.add_true_constraint(&y.eq(&ctx, &SymValue::concrete(9, 32))); + + let solver = SymSolver::new(&ctx); + assert_eq!( + solver.partitioned_state_result(&state, SolverMode::QueryDeep, DeepQueryKind::Model), + Some(SatResult::Unsat) + ); + assert!(!solver.is_sat(&state)); + } + + #[test] + fn test_partition_sat_cache_reuses_checked_components() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("x", 32); + state.make_symbolic("y", 32); + + let x = state.get_register("x"); + let y = state.get_register("y"); + state.add_true_constraint(&x.eq(&ctx, &SymValue::concrete(1, 32))); + state.add_true_constraint(&y.eq(&ctx, &SymValue::concrete(9, 32))); + + let solver = SymSolver::new(&ctx); + assert_eq!( + solver.partitioned_state_result(&state, SolverMode::QueryDeep, DeepQueryKind::Model), + Some(SatResult::Sat) + ); + assert_eq!(solver.analysis_cache.borrow().partition_sat_cache.len(), 2); + assert_eq!( + solver.partitioned_state_result(&state, SolverMode::QueryDeep, DeepQueryKind::Model), + Some(SatResult::Sat) + ); + assert_eq!(solver.analysis_cache.borrow().partition_sat_cache.len(), 2); + } + + #[test] + fn test_explore_fast_sat_does_not_build_fresh_partition_analysis() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("w", 32); + state.make_symbolic("x", 32); + state.make_symbolic("y", 32); + state.make_symbolic("z", 32); + + let w = state.get_register("w"); + let x = state.get_register("x"); + let y = state.get_register("y"); + let z = state.get_register("z"); + state.add_true_constraint(&w.eq(&ctx, &SymValue::concrete(1, 32))); + state.add_true_constraint(&x.eq(&ctx, &SymValue::concrete(2, 32))); + state.add_true_constraint(&y.eq(&ctx, &SymValue::concrete(3, 32))); + state.add_true_constraint(&z.eq(&ctx, &SymValue::concrete(4, 32))); + + let solver = SymSolver::new(&ctx); + assert!(solver.analysis_cache.borrow().state_cache.is_empty()); + assert!(solver.is_sat(&state)); + assert!(solver.analysis_cache.borrow().state_cache.is_empty()); + } + + #[test] + fn test_query_deep_builds_analysis_after_fast_sat_skips_it() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("w", 32); + state.make_symbolic("x", 32); + state.make_symbolic("y", 32); + state.make_symbolic("z", 32); + + let w = state.get_register("w"); + let x = state.get_register("x"); + let y = state.get_register("y"); + let z = state.get_register("z"); + state.add_true_constraint(&w.eq(&ctx, &SymValue::concrete(1, 32))); + state.add_true_constraint(&x.eq(&ctx, &SymValue::concrete(2, 32))); + state.add_true_constraint(&y.eq(&ctx, &SymValue::concrete(3, 32))); + state.add_true_constraint(&z.eq(&ctx, &SymValue::concrete(4, 32))); + + let solver = SymSolver::new(&ctx); + assert!(solver.is_sat(&state)); + assert!(solver.analysis_cache.borrow().state_cache.is_empty()); + + let x_is_two = x.to_bv(&ctx).eq(BV::from_u64(2, 32)); + assert_eq!( + solver.sat_with_constraint(&state, &x_is_two), + SatResult::Sat + ); + assert!(!solver.analysis_cache.borrow().state_cache.is_empty()); + } + + #[test] + fn test_selection_solver_reuses_same_selection_key() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("x", 32); + state.make_symbolic("y", 32); + let x = state.get_register("x"); + let y = state.get_register("y"); + state.add_true_constraint(&x.eq(&ctx, &SymValue::concrete(5, 32))); + state.add_true_constraint(&y.eq(&ctx, &SymValue::concrete(9, 32))); + + let solver = SymSolver::new(&ctx); + let selection = solver.preferred_selection_for_query( + &state, + &solver.expr_dependencies_bv(&x.to_bv(&ctx)), + DeepQueryKind::Predicate, + ); + solver.ensure_selection_solver(&selection); + let first_key = solver.selection_session.borrow().asserted_key.clone(); + solver.ensure_selection_solver(&selection); + assert_eq!(solver.selection_session.borrow().asserted_key, first_key); + } + + #[test] + fn test_cursor_fact_cache_reuses_dependency_extraction_for_descendants() { + let ctx = Context::thread_local(); + + let mut base = SymState::new(&ctx, 0x1000); + base.make_symbolic("x", 32); + let x = base.get_register("x"); + base.add_true_constraint(&x.eq(&ctx, &SymValue::concrete(1, 32))); + + let mut child = base.fork(); + child.make_symbolic("y", 32); + let y = child.get_register("y"); + child.add_true_constraint(&y.eq(&ctx, &SymValue::concrete(2, 32))); + + let solver = SymSolver::new(&ctx); + let _ = solver.state_constraint_analysis(&base); + let cursor_facts_after_base = solver.analysis_cache.borrow().cursor_facts.len(); + assert_eq!(cursor_facts_after_base, 1); + + let _ = solver.state_constraint_analysis(&child); + let cursor_facts_after_child = solver.analysis_cache.borrow().cursor_facts.len(); + assert_eq!(cursor_facts_after_child, 2); + assert!( + solver + .analysis_cache + .borrow() + .state_cache + .contains_key(&base.constraint_cursor_key()) + ); + assert!( + solver + .analysis_cache + .borrow() + .state_cache + .contains_key(&child.constraint_cursor_key()) + ); + } + + #[test] + fn test_small_connected_query_prefers_full_state_selection() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("x", 32); + let x = state.get_register("x"); + state.add_true_constraint(&x.eq(&ctx, &SymValue::concrete(5, 32))); + + let solver = SymSolver::new(&ctx); + let selection = solver.preferred_selection_for_query( + &state, + &solver.expr_dependencies_bv(&x.to_bv(&ctx)), + DeepQueryKind::Predicate, + ); + assert!(selection.is_full_state); + assert_eq!(selection.constraints.len(), state.constraints().len()); + } + + #[test] + fn test_multi_component_model_query_keeps_partitioned_selection() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + for name in ["a", "b", "c", "d", "e", "f"] { + state.make_symbolic(name, 32); + } + + for (index, name) in ["a", "b", "c", "d", "e"].iter().enumerate() { + let value = state.get_register(name); + state.add_true_constraint(&value.eq(&ctx, &SymValue::concrete(index as u64, 32))); + } + + let f = state.get_register("f"); + state.constrain_range(&f, 3, 9); + + let solver = SymSolver::new(&ctx); + let selection = solver.preferred_selection_for_query( + &state, + &solver.expr_dependencies_bv(&f.to_bv(&ctx)), + DeepQueryKind::Model, + ); + assert!(!selection.is_full_state); + assert!(selection.constraints.len() < state.constraints().len()); + assert_eq!(selection.key.partition_ids.len(), 1); + } + + #[test] + fn test_abstract_prefilter_detects_conflicting_eq() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("x", 32); + let x = state.get_register("x"); + state.constrain_eq(&x, 5); + + let solver = SymSolver::new(&ctx); + let impossible = x.to_bv(&ctx).eq(BV::from_u64(7, 32)); + assert_eq!( + solver.sat_with_constraint(&state, &impossible), + SatResult::Unsat + ); + } + + #[test] + fn test_abstract_prefilter_uses_range_for_zero_queries() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("x", 32); + let x = state.get_register("x"); + state.constrain_range(&x, 3, 9); + + let solver = SymSolver::new(&ctx); + assert!(!solver.can_be_zero(&state, &x)); + assert!(!solver.must_be_zero(&state, &x)); + assert_eq!(solver.minimize(&state, &x), Some(3)); + assert_eq!(solver.maximize(&state, &x), Some(9)); + } } diff --git a/crates/r2sym/src/state.rs b/crates/r2sym/src/state.rs index db0aabe..11050a9 100644 --- a/crates/r2sym/src/state.rs +++ b/crates/r2sym/src/state.rs @@ -3,14 +3,118 @@ //! This module provides the `SymState` type which represents the state //! of the program during symbolic execution. +use std::cell::OnceCell; +use std::collections::hash_map::DefaultHasher; use std::collections::{HashMap, HashSet}; +use std::hash::{Hash, Hasher}; +use std::rc::Rc; +use std::sync::atomic::{AtomicUsize, Ordering}; use z3::Context; -use z3::ast::{Ast, BV, Bool}; +use z3::ast::{BV, Bool}; use crate::memory::{MemoryRegionId, MemoryRegionKind, SymMemory}; use crate::value::SymValue; +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub(crate) struct ConstraintCursorKey(usize); + +impl ConstraintCursorKey { + pub(crate) const ROOT: Self = Self(0); +} + +#[derive(Clone, Default)] +struct ConstraintCursor { + node: Option>, +} + +struct ConstraintNode { + id: ConstraintCursorKey, + parent: Option>, + constraint: Bool, + depth: usize, + hash: u64, +} + +impl ConstraintCursor { + fn key(&self) -> ConstraintCursorKey { + self.node + .as_ref() + .map_or(ConstraintCursorKey::ROOT, |node| node.id) + } + + fn depth(&self) -> usize { + self.node.as_ref().map_or(0, |node| node.depth) + } + + fn hash(&self) -> u64 { + self.node.as_ref().map_or(0, |node| node.hash) + } + + fn push(&self, constraint: Bool) -> Self { + let constraint_hash = structural_hash(&constraint); + let hash = mix_hash(self.hash(), constraint_hash); + let depth = self.depth().saturating_add(1); + Self { + node: Some(Rc::new(ConstraintNode { + id: next_constraint_cursor_key(), + parent: self.node.clone(), + constraint, + depth, + hash, + })), + } + } + + fn materialize(&self) -> Vec { + let mut values = Vec::with_capacity(self.depth()); + let mut current = self.node.as_ref().cloned(); + while let Some(node) = current { + values.push(node.constraint.clone()); + current = node.parent.clone(); + } + values.reverse(); + values + } + + fn is_descendant_of(&self, ancestor: &Self) -> bool { + if ancestor.node.is_none() { + return true; + } + let ancestor_key = ancestor.key(); + let mut current = self.node.as_ref().cloned(); + while let Some(node) = current { + if node.id == ancestor_key { + return true; + } + current = node.parent.clone(); + } + false + } + + fn suffix_from_key( + &self, + ancestor_key: ConstraintCursorKey, + ) -> Option> { + let mut suffix = Vec::new(); + let mut current = self.node.as_ref().cloned(); + while let Some(node) = current { + if node.id == ancestor_key { + suffix.reverse(); + return Some(suffix); + } + suffix.push((node.id, node.constraint.clone())); + current = node.parent.clone(); + } + if ancestor_key == ConstraintCursorKey::ROOT { + suffix.reverse(); + Some(suffix) + } else { + None + } + } +} + /// A tracked symbolic memory region (usually an input buffer). #[derive(Debug, Clone)] pub struct SymbolicMemoryRegion<'ctx> { @@ -55,11 +159,13 @@ pub struct SymState<'ctx> { /// The Z3 context. ctx: &'ctx Context, /// Register values (register name -> value). - registers: HashMap>, + registers: Rc>>, /// Memory state. pub memory: SymMemory<'ctx>, /// Path constraints (conditions that must be true for this path). - constraints: Vec, + constraints: ConstraintCursor, + /// Materialized constraint list, populated lazily from the shared cursor chain. + materialized_constraints: OnceCell>, /// Current program counter. pub pc: u64, /// Previous program counter (block predecessor). @@ -71,17 +177,17 @@ pub struct SymState<'ctx> { /// Execution depth (number of steps taken). pub depth: usize, /// Named symbolic inputs (registers or buffers). - symbolic_inputs: HashMap>, + symbolic_inputs: Rc>>, /// Tracked symbolic memory regions. - symbolic_memory: Vec>, + symbolic_memory: Rc>>, /// Symbolic external input streams keyed by file descriptor. - symbolic_fd_inputs: HashMap>, + symbolic_fd_inputs: Rc>>, /// Runtime policy/state for summaries. runtime: RuntimeState, } /// Exit status of a symbolic execution path. -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum ExitStatus { /// Normal return. Return, @@ -102,17 +208,18 @@ impl<'ctx> SymState<'ctx> { pub fn new(ctx: &'ctx Context, entry_pc: u64) -> Self { Self { ctx, - registers: HashMap::new(), + registers: Rc::new(HashMap::new()), memory: SymMemory::new(ctx), - constraints: Vec::new(), + constraints: ConstraintCursor::default(), + materialized_constraints: OnceCell::new(), pc: entry_pc, prev_pc: None, active: true, exit_status: None, depth: 0, - symbolic_inputs: HashMap::new(), - symbolic_memory: Vec::new(), - symbolic_fd_inputs: HashMap::new(), + symbolic_inputs: Rc::new(HashMap::new()), + symbolic_memory: Rc::new(Vec::new()), + symbolic_fd_inputs: Rc::new(HashMap::new()), runtime: RuntimeState::default(), } } @@ -121,17 +228,18 @@ impl<'ctx> SymState<'ctx> { pub fn new_symbolic(ctx: &'ctx Context, entry_pc: u64) -> Self { Self { ctx, - registers: HashMap::new(), + registers: Rc::new(HashMap::new()), memory: SymMemory::new_symbolic(ctx), - constraints: Vec::new(), + constraints: ConstraintCursor::default(), + materialized_constraints: OnceCell::new(), pc: entry_pc, prev_pc: None, active: true, exit_status: None, depth: 0, - symbolic_inputs: HashMap::new(), - symbolic_memory: Vec::new(), - symbolic_fd_inputs: HashMap::new(), + symbolic_inputs: Rc::new(HashMap::new()), + symbolic_memory: Rc::new(Vec::new()), + symbolic_fd_inputs: Rc::new(HashMap::new()), runtime: RuntimeState::default(), } } @@ -156,7 +264,7 @@ impl<'ctx> SymState<'ctx> { /// Set a register value. pub fn set_register(&mut self, name: &str, value: SymValue<'ctx>) { - self.registers.insert(name.to_string(), value); + Rc::make_mut(&mut self.registers).insert(name.to_string(), value); } /// Make a register symbolic with a given name. @@ -168,13 +276,13 @@ impl<'ctx> SymState<'ctx> { /// Make a register symbolic with an explicit symbol name. pub fn make_symbolic_named(&mut self, reg_name: &str, sym_name: &str, bits: u32) { let value = SymValue::new_symbolic(self.ctx, sym_name, bits); - self.registers.insert(reg_name.to_string(), value.clone()); - self.symbolic_inputs.insert(sym_name.to_string(), value); + Rc::make_mut(&mut self.registers).insert(reg_name.to_string(), value.clone()); + Rc::make_mut(&mut self.symbolic_inputs).insert(sym_name.to_string(), value); } /// Set a register to a concrete value. pub fn set_concrete(&mut self, reg_name: &str, value: u64, bits: u32) { - self.registers + Rc::make_mut(&mut self.registers) .insert(reg_name.to_string(), SymValue::concrete(value, bits)); } @@ -195,22 +303,22 @@ impl<'ctx> SymState<'ctx> { /// Get all registers. pub fn registers(&self) -> &HashMap> { - &self.registers + self.registers.as_ref() } /// Get tracked symbolic inputs. pub fn symbolic_inputs(&self) -> &HashMap> { - &self.symbolic_inputs + self.symbolic_inputs.as_ref() } /// Get tracked symbolic memory regions. pub fn symbolic_memory(&self) -> &[SymbolicMemoryRegion<'ctx>] { - &self.symbolic_memory + self.symbolic_memory.as_slice() } /// Get tracked symbolic file-descriptor inputs. pub fn symbolic_fd_inputs(&self) -> &HashMap> { - &self.symbolic_fd_inputs + self.symbolic_fd_inputs.as_ref() } /// Get runtime policy/state. @@ -218,124 +326,65 @@ impl<'ctx> SymState<'ctx> { &self.runtime } - pub(crate) fn semantic_fingerprint(&self) -> String { - let mut registers: Vec<_> = self - .registers - .iter() - .map(|(name, value)| { - ( - name.clone(), - value.to_bv(self.ctx).simplify().to_string(), - value.get_taint(), - ) - }) - .collect(); - registers.sort_unstable_by(|a, b| a.0.cmp(&b.0)); - let registers_repr = registers - .into_iter() - .map(|(name, value, taint)| format!("{name}={value}@{taint}")) - .collect::>() - .join(","); - - let mut symbolic_inputs: Vec<_> = self - .symbolic_inputs - .iter() - .map(|(name, value)| { - ( - name.clone(), - value.to_bv(self.ctx).simplify().to_string(), - value.get_taint(), - ) - }) - .collect(); - symbolic_inputs.sort_unstable_by(|a, b| a.0.cmp(&b.0)); - let symbolic_inputs_repr = symbolic_inputs - .into_iter() - .map(|(name, value, taint)| format!("{name}={value}@{taint}")) - .collect::>() - .join(","); - - let mut symbolic_regions: Vec<_> = self - .symbolic_memory - .iter() - .map(|region| { - ( - region.region_id, - region.name.clone(), - region.addr, - region.size, - region.value.to_bv(self.ctx).simplify().to_string(), - region.value.get_taint(), - ) - }) - .collect(); - symbolic_regions.sort_unstable_by(|a, b| { - a.0.cmp(&b.0) - .then(a.1.cmp(&b.1)) - .then(a.2.cmp(&b.2)) - .then(a.3.cmp(&b.3)) - }); - let symbolic_regions_repr = symbolic_regions - .into_iter() - .map(|(region_id, name, addr, size, value, taint)| { - format!("{}:{name}@{addr:x}:{size}={value}@{taint}", region_id.0) - }) - .collect::>() - .join(","); - - let mut fd_inputs: Vec<_> = self - .symbolic_fd_inputs - .iter() - .map(|(fd, input)| { - let bytes = input - .bytes - .iter() - .map(|byte| format!("{}@{}", byte.to_bv(self.ctx).simplify(), byte.get_taint())) - .collect::>() - .join("|"); - (*fd, input.name.clone(), input.cursor, bytes) - }) - .collect(); - fd_inputs.sort_unstable_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1))); - let fd_inputs_repr = fd_inputs - .into_iter() - .map(|(fd, name, cursor, bytes)| format!("{fd}:{name}@{cursor}[{bytes}]")) - .collect::>() - .join(","); + pub(crate) fn semantic_fingerprint(&self) -> u64 { + let mut hasher = DefaultHasher::new(); + self.pc.hash(&mut hasher); + self.active.hash(&mut hasher); + self.exit_status.hash(&mut hasher); + self.memory.semantic_fingerprint().hash(&mut hasher); + self.runtime.skip_sleep_calls.hash(&mut hasher); let mut tty_fds: Vec<_> = self.runtime.tty_fds.iter().copied().collect(); tty_fds.sort_unstable(); - let tty_repr = tty_fds - .into_iter() - .map(|fd| fd.to_string()) - .collect::>() - .join(","); - - format!( - "pc={:x};active={};exit={:?};regs=[{}];memory=({});inputs=[{}];regions=[{}];fds=[{}];tty=[{}];skip_sleep={}", - self.pc, - self.active, - self.exit_status, - registers_repr, - self.memory.semantic_fingerprint(), - symbolic_inputs_repr, - symbolic_regions_repr, - fd_inputs_repr, - tty_repr, - self.runtime.skip_sleep_calls - ) + tty_fds.hash(&mut hasher); + + let mut register_names: Vec<_> = self.registers.keys().collect(); + register_names.sort_unstable(); + for name in register_names { + name.hash(&mut hasher); + hash_sym_value(self.ctx, &self.registers[name], &mut hasher); + } + + let mut symbolic_input_names: Vec<_> = self.symbolic_inputs.keys().collect(); + symbolic_input_names.sort_unstable(); + for name in symbolic_input_names { + name.hash(&mut hasher); + hash_sym_value(self.ctx, &self.symbolic_inputs[name], &mut hasher); + } + + for region in self.symbolic_memory.iter() { + region.region_id.hash(&mut hasher); + region.name.hash(&mut hasher); + region.addr.hash(&mut hasher); + region.size.hash(&mut hasher); + hash_sym_value(self.ctx, ®ion.value, &mut hasher); + } + + let mut fd_inputs: Vec<_> = self.symbolic_fd_inputs.iter().collect(); + fd_inputs.sort_unstable_by(|a, b| a.0.cmp(b.0)); + for (fd, input) in fd_inputs { + fd.hash(&mut hasher); + input.name.hash(&mut hasher); + input.cursor.hash(&mut hasher); + for byte in &input.bytes { + hash_sym_value(self.ctx, byte, &mut hasher); + } + } + + hasher.finish() } /// Read from memory. pub fn mem_read(&self, addr: &SymValue<'ctx>, size: u32) -> SymValue<'ctx> { self.memory - .read_with_constraints(addr, size, &self.constraints) + .read_with_constraints(addr, size, self.constraints()) } /// Write to memory. pub fn mem_write(&mut self, addr: &SymValue<'ctx>, value: &SymValue<'ctx>, size: u32) { + let constraints = self.constraints().to_vec(); self.memory - .write_with_constraints(addr, value, size, &self.constraints); + .write_with_constraints(addr, value, size, &constraints); } /// Set the maximum number of symbolic address targets to enumerate. @@ -345,7 +394,7 @@ impl<'ctx> SymState<'ctx> { /// Compute the path condition (AND of all constraints). pub fn path_condition(&self) -> Bool { - and_all(self.ctx, &self.constraints) + and_all(self.ctx, self.constraints()) } /// Merge this state with another state at the same program counter. @@ -363,7 +412,9 @@ impl<'ctx> SymState<'ctx> { merged.active = self.active && other.active; merged.exit_status = None; merged.depth = self.depth.max(other.depth); - merged.constraints = vec![cond_self | cond_other.clone()]; + merged.constraints = ConstraintCursor::default(); + merged.materialized_constraints = OnceCell::new(); + merged.add_constraint(cond_self | cond_other.clone()); let mut keys = HashSet::new(); keys.extend(self.registers.keys().cloned()); @@ -392,25 +443,24 @@ impl<'ctx> SymState<'ctx> { let merged_val = merge_values(self.ctx, &cond_other, &val_self, &val_other); registers.insert(key, merged_val); } - merged.registers = registers; + merged.registers = Rc::new(registers); merged.memory = self.memory.merge_with( &other.memory, - &self.constraints, - &other.constraints, + self.constraints(), + other.constraints(), &cond_other, ); merged.symbolic_inputs = self.symbolic_inputs.clone(); - for (name, value) in &other.symbolic_inputs { - merged - .symbolic_inputs + for (name, value) in other.symbolic_inputs.iter() { + Rc::make_mut(&mut merged.symbolic_inputs) .entry(name.clone()) .or_insert_with(|| value.clone()); } merged.symbolic_memory = self.symbolic_memory.clone(); - for region in &other.symbolic_memory { + for region in other.symbolic_memory.iter() { let exists = merged.symbolic_memory.iter().any(|r| { r.region_id == region.region_id && r.name == region.name @@ -418,13 +468,14 @@ impl<'ctx> SymState<'ctx> { && r.size == region.size }); if !exists { - merged.symbolic_memory.push(region.clone()); + Rc::make_mut(&mut merged.symbolic_memory).push(region.clone()); } } merged.symbolic_fd_inputs = self.symbolic_fd_inputs.clone(); - for (fd, other_input) in &other.symbolic_fd_inputs { - match merged.symbolic_fd_inputs.get_mut(fd) { + for (fd, other_input) in other.symbolic_fd_inputs.iter() { + let fd_inputs = Rc::make_mut(&mut merged.symbolic_fd_inputs); + match fd_inputs.get_mut(fd) { Some(existing) => { if existing.bytes.len() < other_input.bytes.len() { existing.bytes = other_input.bytes.clone(); @@ -432,7 +483,7 @@ impl<'ctx> SymState<'ctx> { existing.cursor = existing.cursor.max(other_input.cursor); } None => { - merged.symbolic_fd_inputs.insert(*fd, other_input.clone()); + fd_inputs.insert(*fd, other_input.clone()); } } } @@ -449,7 +500,11 @@ impl<'ctx> SymState<'ctx> { /// Add a path constraint. pub fn add_constraint(&mut self, constraint: Bool) { - self.constraints.push(constraint); + self.constraints = self.constraints.push(constraint.clone()); + if let Some(mut cached) = self.materialized_constraints.take() { + cached.push(constraint); + let _ = self.materialized_constraints.set(cached); + } } /// Constrain a value to equal a concrete constant. @@ -579,7 +634,7 @@ impl<'ctx> SymState<'ctx> { let bv = value.to_bv(self.ctx); let zero = BV::from_u64(0, value.bits()); let cond = bv.eq(&zero).not(); - self.constraints.push(cond); + self.add_constraint(cond); } /// Add a constraint that a value is false (zero). @@ -587,17 +642,34 @@ impl<'ctx> SymState<'ctx> { let bv = value.to_bv(self.ctx); let zero = BV::from_u64(0, value.bits()); let cond = bv.eq(&zero); - self.constraints.push(cond); + self.add_constraint(cond); } /// Get all path constraints. pub fn constraints(&self) -> &[Bool] { - &self.constraints + self.materialized_constraints + .get_or_init(|| self.constraints.materialize()) + .as_slice() } /// Get the number of constraints. pub fn num_constraints(&self) -> usize { - self.constraints.len() + self.constraints.depth() + } + + pub(crate) fn constraint_cursor_key(&self) -> ConstraintCursorKey { + self.constraints.key() + } + + pub(crate) fn constraints_imply_by_prefix(&self, other: &Self) -> bool { + self.constraints.is_descendant_of(&other.constraints) + } + + pub(crate) fn constraint_suffix_from_cursor( + &self, + ancestor: ConstraintCursorKey, + ) -> Option> { + self.constraints.suffix_from_key(ancestor) } /// Terminate this state with the given status. @@ -623,6 +695,7 @@ impl<'ctx> SymState<'ctx> { registers: self.registers.clone(), memory: self.memory.fork(), constraints: self.constraints.clone(), + materialized_constraints: OnceCell::new(), pc: self.pc, prev_pc: self.prev_pc, active: self.active, @@ -645,7 +718,7 @@ impl<'ctx> SymState<'ctx> { /// Create a named symbolic input value. pub fn new_symbolic_input(&mut self, name: &str, bits: u32) -> SymValue<'ctx> { let value = SymValue::new_symbolic(self.ctx, name, bits); - self.symbolic_inputs.insert(name.to_string(), value.clone()); + Rc::make_mut(&mut self.symbolic_inputs).insert(name.to_string(), value.clone()); value } @@ -657,7 +730,7 @@ impl<'ctx> SymState<'ctx> { taint: u64, ) -> SymValue<'ctx> { let value = SymValue::new_symbolic_tainted(self.ctx, name, bits, taint); - self.symbolic_inputs.insert(name.to_string(), value.clone()); + Rc::make_mut(&mut self.symbolic_inputs).insert(name.to_string(), value.clone()); value } @@ -683,8 +756,8 @@ impl<'ctx> SymState<'ctx> { self.define_memory_region(MemoryRegionKind::Input, name, Some(addr), Some(size as u64)); let addr_val = SymValue::concrete(addr, 64); self.mem_write(&addr_val, &value, size); - self.symbolic_inputs.insert(name.to_string(), value.clone()); - self.symbolic_memory.push(SymbolicMemoryRegion { + Rc::make_mut(&mut self.symbolic_inputs).insert(name.to_string(), value.clone()); + Rc::make_mut(&mut self.symbolic_memory).push(SymbolicMemoryRegion { region_id, name: name.to_string(), addr, @@ -754,10 +827,10 @@ impl<'ctx> SymState<'ctx> { if let Some(alphabet) = alphabet { constrain_symbolic_byte_to_alphabet(self, &byte, alphabet); } - self.symbolic_inputs.insert(byte_name, byte.clone()); + Rc::make_mut(&mut self.symbolic_inputs).insert(byte_name, byte.clone()); bytes.push(byte); } - self.symbolic_fd_inputs.insert( + Rc::make_mut(&mut self.symbolic_fd_inputs).insert( fd, SymbolicFdInput { name: name.to_string(), @@ -770,7 +843,7 @@ impl<'ctx> SymState<'ctx> { /// Read up to `count` bytes from a tracked symbolic file descriptor. pub fn read_symbolic_fd_bytes(&mut self, fd: i32, count: usize) -> Option>> { - let input = self.symbolic_fd_inputs.get_mut(&fd)?; + let input = Rc::make_mut(&mut self.symbolic_fd_inputs).get_mut(&fd)?; if input.cursor >= input.bytes.len() { return Some(Vec::new()); } @@ -781,6 +854,27 @@ impl<'ctx> SymState<'ctx> { } } +fn mix_hash(seed: u64, value: u64) -> u64 { + seed.rotate_left(7) ^ value.wrapping_mul(0x9e37_79b9_7f4a_7c15) +} + +fn next_constraint_cursor_key() -> ConstraintCursorKey { + static NEXT_ID: AtomicUsize = AtomicUsize::new(1); + ConstraintCursorKey(NEXT_ID.fetch_add(1, Ordering::Relaxed)) +} + +fn structural_hash(value: &T) -> u64 { + let mut hasher = DefaultHasher::new(); + value.hash(&mut hasher); + hasher.finish() +} + +fn hash_sym_value<'ctx, H: Hasher>(ctx: &'ctx Context, value: &SymValue<'ctx>, hasher: &mut H) { + value.bits().hash(hasher); + value.get_taint().hash(hasher); + value.to_bv(ctx).hash(hasher); +} + fn constrain_symbolic_byte_to_alphabet<'ctx>( state: &mut SymState<'ctx>, value: &SymValue<'ctx>, @@ -884,7 +978,7 @@ impl<'ctx> std::fmt::Debug for SymState<'ctx> { .field("pc", &format!("0x{:x}", self.pc)) .field("prev_pc", &self.prev_pc.map(|pc| format!("0x{:x}", pc))) .field("registers", &self.registers.len()) - .field("constraints", &self.constraints.len()) + .field("constraints", &self.num_constraints()) .field("depth", &self.depth) .field("symbolic_inputs", &self.symbolic_inputs.len()) .field("symbolic_memory", &self.symbolic_memory.len()) diff --git a/crates/r2sym/src/value.rs b/crates/r2sym/src/value.rs index f466c76..c04cdad 100644 --- a/crates/r2sym/src/value.rs +++ b/crates/r2sym/src/value.rs @@ -9,7 +9,8 @@ use std::fmt; use std::marker::PhantomData; use z3::Context; -use z3::ast::BV; +use z3::DeclKind; +use z3::ast::{Ast, BV, Dynamic}; /// A symbolic value that can be concrete, symbolic, or unknown. /// @@ -276,6 +277,36 @@ impl<'ctx> SymValue<'ctx> { } } + fn is_concrete_value(&self, expected: u64) -> bool { + matches!(self, Self::Concrete { value, .. } if *value == expected) + } + + fn mask_for_bits(bits: u32) -> u64 { + if bits >= 64 { + u64::MAX + } else { + (1u64 << bits) - 1 + } + } + + fn is_all_ones_for(&self, bits: u32) -> bool { + match self { + Self::Concrete { + value, + bits: value_bits, + .. + } => *value_bits == bits && *value == Self::mask_for_bits(bits), + _ => false, + } + } + + fn same_symbolic_ast(&self, other: &Self) -> bool { + match (self, other) { + (Self::Symbolic { ast: a, .. }, Self::Symbolic { ast: b, .. }) => a.ast_eq(b), + _ => false, + } + } + /// Convert to a Z3 bitvector (concretizing if needed). pub fn to_bv(&self, _ctx: &'ctx Context) -> BV { match self { @@ -309,7 +340,23 @@ impl<'ctx> SymValue<'ctx> { taint, .. } => { - let new_ast = ast.zero_ext(extend_by); + let new_ast = { + let dyn_ast = Dynamic::from_ast(ast); + if matches!( + dyn_ast.safe_decl().map(|decl| decl.kind()), + Ok(DeclKind::ZERO_EXT) + ) { + if let Some(child) = + dyn_ast.children().first().and_then(|child| child.as_bv()) + { + child.zero_ext(new_bits - child.get_size()) + } else { + ast.zero_ext(extend_by) + } + } else { + ast.zero_ext(extend_by) + } + }; Self::Symbolic { ast: new_ast, bits: new_bits, @@ -357,7 +404,23 @@ impl<'ctx> SymValue<'ctx> { } } Self::Symbolic { ast, .. } => { - let new_ast = ast.sign_ext(extend_by); + let new_ast = { + let dyn_ast = Dynamic::from_ast(ast); + if matches!( + dyn_ast.safe_decl().map(|decl| decl.kind()), + Ok(DeclKind::SIGN_EXT) + ) { + if let Some(child) = + dyn_ast.children().first().and_then(|child| child.as_bv()) + { + child.sign_ext(new_bits - child.get_size()) + } else { + ast.sign_ext(extend_by) + } + } else { + ast.sign_ext(extend_by) + } + }; Self::Symbolic { ast: new_ast, bits: new_bits, @@ -384,6 +447,9 @@ impl<'ctx> SymValue<'ctx> { return Self::Unknown { bits: 1, taint }; } let new_bits = clamped_high - low + 1; + if low == 0 && clamped_high + 1 == src_bits { + return self.clone(); + } match self { Self::Concrete { value, .. } => { if new_bits > 64 || clamped_high >= 64 { @@ -403,6 +469,20 @@ impl<'ctx> SymValue<'ctx> { } } Self::Symbolic { ast, .. } => { + if low == 0 { + let dyn_ast = Dynamic::from_ast(ast); + if matches!( + dyn_ast.safe_decl().map(|decl| decl.kind()), + Ok(DeclKind::ZERO_EXT | DeclKind::SIGN_EXT) + ) { + let children = dyn_ast.children(); + if let Some(child) = children.first().and_then(|child| child.as_bv()) + && child.get_size() == new_bits + { + return Self::symbolic_tainted(child, new_bits, taint); + } + } + } let new_ast = ast.extract(clamped_high, low); Self::Symbolic { ast: new_ast, @@ -480,6 +560,8 @@ impl<'ctx> SymValue<'ctx> { taint, } } + (_, rhs) if rhs.is_zero() => self.clone().with_taint(taint), + (lhs, _) if lhs.is_zero() => other.clone().with_taint(taint), _ => { let (a_bv, b_bv, result_bits) = self.normalize_widths(ctx, other); Self::Symbolic { @@ -516,6 +598,12 @@ impl<'ctx> SymValue<'ctx> { taint, } } + (_, rhs) if rhs.is_zero() => self.clone().with_taint(taint), + (lhs, rhs) if lhs.same_symbolic_ast(rhs) => Self::Concrete { + value: 0, + bits: self.bits().max(other.bits()), + taint, + }, _ => { let (a_bv, b_bv, result_bits) = self.normalize_widths(ctx, other); Self::Symbolic { @@ -552,6 +640,18 @@ impl<'ctx> SymValue<'ctx> { taint, } } + (_, rhs) if rhs.is_zero() => Self::Concrete { + value: 0, + bits: self.bits().max(other.bits()), + taint, + }, + (lhs, _) if lhs.is_zero() => Self::Concrete { + value: 0, + bits: self.bits().max(other.bits()), + taint, + }, + (_, rhs) if rhs.is_concrete_value(1) => self.clone().with_taint(taint), + (lhs, _) if lhs.is_concrete_value(1) => other.clone().with_taint(taint), _ => { let (a_bv, b_bv, result_bits) = self.normalize_widths(ctx, other); Self::Symbolic { @@ -590,6 +690,7 @@ impl<'ctx> SymValue<'ctx> { } } } + (_, rhs) if rhs.is_concrete_value(1) => self.clone().with_taint(taint), _ => { let (a_bv, b_bv, result_bits) = self.normalize_widths(ctx, other); Self::Symbolic { @@ -605,6 +706,9 @@ impl<'ctx> SymValue<'ctx> { /// Signed division. pub fn sdiv(&self, ctx: &'ctx Context, other: &Self) -> Self { let taint = self.get_taint() | other.get_taint(); + if other.is_concrete_value(1) { + return self.clone().with_taint(taint); + } let (a_bv, b_bv, result_bits) = self.normalize_widths_signed(ctx, other); Self::Symbolic { ast: a_bv.bvsdiv(&b_bv), @@ -640,6 +744,11 @@ impl<'ctx> SymValue<'ctx> { } } } + (_, rhs) if rhs.is_concrete_value(1) => Self::Concrete { + value: 0, + bits: self.bits().max(other.bits()), + taint, + }, _ => { let (a_bv, b_bv, result_bits) = self.normalize_widths(ctx, other); Self::Symbolic { @@ -655,6 +764,13 @@ impl<'ctx> SymValue<'ctx> { /// Signed remainder. pub fn srem(&self, ctx: &'ctx Context, other: &Self) -> Self { let taint = self.get_taint() | other.get_taint(); + if other.is_concrete_value(1) { + return Self::Concrete { + value: 0, + bits: self.bits().max(other.bits()), + taint, + }; + } let (a_bv, b_bv, result_bits) = self.normalize_widths_signed(ctx, other); Self::Symbolic { ast: a_bv.bvsrem(&b_bv), @@ -697,6 +813,7 @@ impl<'ctx> SymValue<'ctx> { /// Bitwise AND. pub fn and(&self, ctx: &'ctx Context, other: &Self) -> Self { let taint = self.get_taint() | other.get_taint(); + let result_bits = self.bits().max(other.bits()); match (self, other) { ( Self::Concrete { value: a, bits, .. }, @@ -710,6 +827,19 @@ impl<'ctx> SymValue<'ctx> { bits: (*bits).max(*b_bits), taint, }, + (_, rhs) if rhs.is_zero() => Self::Concrete { + value: 0, + bits: self.bits().max(other.bits()), + taint, + }, + (lhs, _) if lhs.is_zero() => Self::Concrete { + value: 0, + bits: self.bits().max(other.bits()), + taint, + }, + (_, rhs) if rhs.is_all_ones_for(result_bits) => self.clone().with_taint(taint), + (lhs, _) if lhs.is_all_ones_for(result_bits) => other.clone().with_taint(taint), + (lhs, rhs) if lhs.same_symbolic_ast(rhs) => self.clone().with_taint(taint), _ => { let (a_bv, b_bv, result_bits) = self.normalize_widths(ctx, other); Self::Symbolic { @@ -725,6 +855,7 @@ impl<'ctx> SymValue<'ctx> { /// Bitwise OR. pub fn or(&self, ctx: &'ctx Context, other: &Self) -> Self { let taint = self.get_taint() | other.get_taint(); + let result_bits = self.bits().max(other.bits()); match (self, other) { ( Self::Concrete { value: a, bits, .. }, @@ -738,6 +869,19 @@ impl<'ctx> SymValue<'ctx> { bits: (*bits).max(*b_bits), taint, }, + (_, rhs) if rhs.is_zero() => self.clone().with_taint(taint), + (lhs, _) if lhs.is_zero() => other.clone().with_taint(taint), + (_, rhs) if rhs.is_all_ones_for(result_bits) => Self::Concrete { + value: Self::mask_for_bits(result_bits), + bits: result_bits, + taint, + }, + (lhs, _) if lhs.is_all_ones_for(result_bits) => Self::Concrete { + value: Self::mask_for_bits(result_bits), + bits: result_bits, + taint, + }, + (lhs, rhs) if lhs.same_symbolic_ast(rhs) => self.clone().with_taint(taint), _ => { let (a_bv, b_bv, result_bits) = self.normalize_widths(ctx, other); Self::Symbolic { @@ -766,6 +910,13 @@ impl<'ctx> SymValue<'ctx> { bits: (*bits).max(*b_bits), taint, }, + (_, rhs) if rhs.is_zero() => self.clone().with_taint(taint), + (lhs, _) if lhs.is_zero() => other.clone().with_taint(taint), + (lhs, rhs) if lhs.same_symbolic_ast(rhs) => Self::Concrete { + value: 0, + bits: self.bits().max(other.bits()), + taint, + }, _ => { let (a_bv, b_bv, result_bits) = self.normalize_widths(ctx, other); Self::Symbolic { @@ -812,6 +963,7 @@ impl<'ctx> SymValue<'ctx> { pub fn shl(&self, ctx: &'ctx Context, amount: &Self) -> Self { let taint = self.get_taint() | amount.get_taint(); match (self, amount) { + (_, amt) if amt.is_zero() => self.clone().with_taint(taint), (Self::Concrete { value, bits, .. }, Self::Concrete { value: amt, .. }) => { let mask = if *bits >= 64 { u64::MAX @@ -841,6 +993,7 @@ impl<'ctx> SymValue<'ctx> { pub fn lshr(&self, ctx: &'ctx Context, amount: &Self) -> Self { let taint = self.get_taint() | amount.get_taint(); match (self, amount) { + (_, amt) if amt.is_zero() => self.clone().with_taint(taint), (Self::Concrete { value, bits, .. }, Self::Concrete { value: amt, .. }) => { Self::Concrete { value: *value >> (*amt as u32), @@ -864,6 +1017,9 @@ impl<'ctx> SymValue<'ctx> { /// Arithmetic right shift. pub fn ashr(&self, ctx: &'ctx Context, amount: &Self) -> Self { let taint = self.get_taint() | amount.get_taint(); + if amount.is_zero() { + return self.clone().with_taint(taint); + } let a_bv = self.to_bv(ctx); let b_bv = self.normalize_shift_amount(ctx, amount); Self::Symbolic { @@ -885,6 +1041,11 @@ impl<'ctx> SymValue<'ctx> { bits: 1, taint, }, + (lhs, rhs) if lhs.same_symbolic_ast(rhs) => Self::Concrete { + value: 1, + bits: 1, + taint, + }, _ => { let (a_bv, b_bv, _) = self.normalize_widths(ctx, other); let cond = a_bv.eq(&b_bv); @@ -909,6 +1070,11 @@ impl<'ctx> SymValue<'ctx> { bits: 1, taint, }, + (lhs, rhs) if lhs.same_symbolic_ast(rhs) => Self::Concrete { + value: 0, + bits: 1, + taint, + }, _ => { let (a_bv, b_bv, _) = self.normalize_widths(ctx, other); let cond = a_bv.bvult(&b_bv); @@ -933,6 +1099,11 @@ impl<'ctx> SymValue<'ctx> { bits: 1, taint, }, + (lhs, rhs) if lhs.same_symbolic_ast(rhs) => Self::Concrete { + value: 1, + bits: 1, + taint, + }, _ => { let (a_bv, b_bv, _) = self.normalize_widths(ctx, other); let cond = a_bv.bvule(&b_bv); @@ -951,6 +1122,13 @@ impl<'ctx> SymValue<'ctx> { /// Signed less than comparison. pub fn slt(&self, ctx: &'ctx Context, other: &Self) -> Self { let taint = self.get_taint() | other.get_taint(); + if self.same_symbolic_ast(other) { + return Self::Concrete { + value: 0, + bits: 1, + taint, + }; + } let (a_bv, b_bv, _) = self.normalize_widths_signed(ctx, other); let cond = a_bv.bvslt(&b_bv); let one = BV::from_i64(1, 1); @@ -966,6 +1144,13 @@ impl<'ctx> SymValue<'ctx> { /// Signed less than or equal comparison. pub fn sle(&self, ctx: &'ctx Context, other: &Self) -> Self { let taint = self.get_taint() | other.get_taint(); + if self.same_symbolic_ast(other) { + return Self::Concrete { + value: 1, + bits: 1, + taint, + }; + } let (a_bv, b_bv, _) = self.normalize_widths_signed(ctx, other); let cond = a_bv.bvsle(&b_bv); let one = BV::from_i64(1, 1); @@ -1114,6 +1299,84 @@ mod tests { assert_eq!(sym.bits(), 64); } + #[test] + fn test_symbolic_identity_fast_paths() { + let ctx = Context::thread_local(); + + let sym = SymValue::new_symbolic(&ctx, "x", 64); + let zero = SymValue::concrete(0, 64); + let one = SymValue::concrete(1, 64); + + let add = sym.add(&ctx, &zero); + let sub = sym.sub(&ctx, &zero); + let mul = sym.mul(&ctx, &one); + let and = sym.and(&ctx, &SymValue::concrete(u64::MAX, 64)); + let or = sym.or(&ctx, &zero); + let xor = sym.xor(&ctx, &zero); + let shl = sym.shl(&ctx, &zero); + let lshr = sym.lshr(&ctx, &zero); + let ashr = sym.ashr(&ctx, &zero); + + assert!(add.same_symbolic_ast(&sym)); + assert!(sub.same_symbolic_ast(&sym)); + assert!(mul.same_symbolic_ast(&sym)); + assert!(and.same_symbolic_ast(&sym)); + assert!(or.same_symbolic_ast(&sym)); + assert!(xor.same_symbolic_ast(&sym)); + assert!(shl.same_symbolic_ast(&sym)); + assert!(lshr.same_symbolic_ast(&sym)); + assert!(ashr.same_symbolic_ast(&sym)); + } + + #[test] + fn test_symbolic_division_remainder_fast_paths() { + let ctx = Context::thread_local(); + + let sym = SymValue::new_symbolic(&ctx, "x", 64); + let one = SymValue::concrete(1, 64); + + let udiv = sym.udiv(&ctx, &one); + let sdiv = sym.sdiv(&ctx, &one); + let urem = sym.urem(&ctx, &one); + let srem = sym.srem(&ctx, &one); + + assert!(udiv.same_symbolic_ast(&sym)); + assert!(sdiv.same_symbolic_ast(&sym)); + assert_eq!(urem.as_concrete(), Some(0)); + assert_eq!(srem.as_concrete(), Some(0)); + } + + #[test] + fn test_symbolic_self_rules() { + let ctx = Context::thread_local(); + + let sym = SymValue::new_symbolic(&ctx, "x", 64); + + assert_eq!(sym.sub(&ctx, &sym).as_concrete(), Some(0)); + assert_eq!(sym.xor(&ctx, &sym).as_concrete(), Some(0)); + assert_eq!(sym.eq(&ctx, &sym).as_concrete(), Some(1)); + assert_eq!(sym.ult(&ctx, &sym).as_concrete(), Some(0)); + assert_eq!(sym.ule(&ctx, &sym).as_concrete(), Some(1)); + assert_eq!(sym.slt(&ctx, &sym).as_concrete(), Some(0)); + assert_eq!(sym.sle(&ctx, &sym).as_concrete(), Some(1)); + } + + #[test] + fn test_all_ones_fast_paths_require_full_result_width() { + let ctx = Context::thread_local(); + + let sym = SymValue::new_symbolic(&ctx, "x", 64); + let byte_ones = SymValue::concrete(0xff, 8); + + let and = sym.and(&ctx, &byte_ones); + let or = sym.or(&ctx, &byte_ones); + + assert!(and.is_symbolic()); + assert!(or.is_symbolic()); + assert!(!and.same_symbolic_ast(&sym)); + assert!(!or.same_symbolic_ast(&sym)); + } + #[test] fn test_extension() { let ctx = Context::thread_local(); @@ -1142,6 +1405,50 @@ mod tests { let high = a.extract(&ctx, 15, 8); assert_eq!(high.as_concrete(), Some(0xAB)); } + + #[test] + fn test_extract_full_width_returns_same_symbolic_ast() { + let ctx = Context::thread_local(); + let sym = SymValue::new_symbolic(&ctx, "x", 32); + let extracted = sym.extract(&ctx, 31, 0); + assert!(extracted.same_symbolic_ast(&sym)); + } + + #[test] + fn test_extract_zero_extend_low_bits_returns_original_ast() { + let ctx = Context::thread_local(); + let sym = SymValue::new_symbolic(&ctx, "x", 32); + let widened = sym.zero_extend(&ctx, 64); + let extracted = widened.extract(&ctx, 31, 0); + assert!(extracted.same_symbolic_ast(&sym)); + } + + #[test] + fn test_extract_sign_extend_low_bits_returns_original_ast() { + let ctx = Context::thread_local(); + let sym = SymValue::new_symbolic(&ctx, "x", 32); + let widened = sym.sign_extend(&ctx, 64); + let extracted = widened.extract(&ctx, 31, 0); + assert!(extracted.same_symbolic_ast(&sym)); + } + + #[test] + fn test_zero_extend_flattens_nested_extensions() { + let ctx = Context::thread_local(); + let sym = SymValue::new_symbolic(&ctx, "x", 16); + let widened = sym.zero_extend(&ctx, 32).zero_extend(&ctx, 64); + let direct = sym.zero_extend(&ctx, 64); + assert!(widened.same_symbolic_ast(&direct)); + } + + #[test] + fn test_sign_extend_flattens_nested_extensions() { + let ctx = Context::thread_local(); + let sym = SymValue::new_symbolic(&ctx, "x", 16); + let widened = sym.sign_extend(&ctx, 32).sign_extend(&ctx, 64); + let direct = sym.sign_extend(&ctx, 64); + assert!(widened.same_symbolic_ast(&direct)); + } } #[cfg(test)] @@ -1313,4 +1620,52 @@ mod bitwidth_tests { assert_eq!(out.bits(), 1); assert!(out.is_unknown()); } + + #[test] + fn test_symbolic_identity_normalization() { + let ctx = Context::thread_local(); + let x = SymValue::new_symbolic(&ctx, "x", 32); + let zero = SymValue::concrete(0, 32); + let one = SymValue::concrete(1, 32); + + let add = x.add(&ctx, &zero); + assert!(add.is_symbolic()); + assert!(add.as_ast().unwrap().ast_eq(x.as_ast().unwrap())); + + let mul = x.mul(&ctx, &one); + assert!(mul.is_symbolic()); + assert!(mul.as_ast().unwrap().ast_eq(x.as_ast().unwrap())); + + let shl = x.shl(&ctx, &zero); + assert!(shl.is_symbolic()); + assert!(shl.as_ast().unwrap().ast_eq(x.as_ast().unwrap())); + } + + #[test] + fn test_symbolic_self_rewrite_rules() { + let ctx = Context::thread_local(); + let x = SymValue::new_symbolic(&ctx, "x", 32); + + assert_eq!(x.sub(&ctx, &x).as_concrete(), Some(0)); + assert_eq!(x.xor(&ctx, &x).as_concrete(), Some(0)); + assert_eq!(x.eq(&ctx, &x).as_concrete(), Some(1)); + assert_eq!(x.ult(&ctx, &x).as_concrete(), Some(0)); + assert_eq!(x.ule(&ctx, &x).as_concrete(), Some(1)); + } + + #[test] + fn test_all_ones_normalization_is_width_sensitive() { + let ctx = Context::thread_local(); + let x = SymValue::new_symbolic(&ctx, "x", 32); + let mask8 = SymValue::concrete(0xff, 8); + let mask32 = SymValue::concrete(u32::MAX as u64, 32); + + let narrowed = x.and(&ctx, &mask8); + assert!(narrowed.is_symbolic()); + assert!(!narrowed.as_ast().unwrap().ast_eq(x.as_ast().unwrap())); + + let widened = x.and(&ctx, &mask32); + assert!(widened.is_symbolic()); + assert!(widened.as_ast().unwrap().ast_eq(x.as_ast().unwrap())); + } } diff --git a/crates/r2types/src/facts.rs b/crates/r2types/src/facts.rs index a05baa1..c2d6620 100644 --- a/crates/r2types/src/facts.rs +++ b/crates/r2types/src/facts.rs @@ -57,6 +57,175 @@ pub enum SymbolicConditionPrecision { Unsupported, } +#[derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize, +)] +pub enum SymbolicSemanticConfidence { + Exact, + Likely, + Heuristic, + Residual, +} + +const fn default_symbolic_confidence_exact() -> SymbolicSemanticConfidence { + SymbolicSemanticConfidence::Exact +} + +impl SymbolicSemanticConfidence { + pub fn is_reliable(self) -> bool { + matches!(self, Self::Exact | Self::Likely) + } + + pub fn is_usable(self) -> bool { + !matches!(self, Self::Residual) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum SymbolicSemanticEvidenceSoundness { + Proven, + OverApprox, + Ranked, + Unknown, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum SymbolicSemanticEvidenceCoverage { + Full, + Partial, + Bounded, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum SymbolicSemanticEvidenceProvenance { + Stable, + Normalized, + Ranked, + Unstable, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum SymbolicSemanticEvidenceAmbiguity { + Single, + Bounded, + Ranked, + Multiple, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum SymbolicSemanticEvidenceReason { + LargeCfg, + SummaryBudget, + AliasAmbiguity, + ReplayOverlap, + HeapIdentityWeak, + GuardOpaque, + ValueOpaque, + TruncatedTransfer, + DerivedFromRanking, + PartialPathCoverage, + ResidualSearchRequired, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct SymbolicSemanticEvidence { + pub tier: SymbolicSemanticConfidence, + pub soundness: SymbolicSemanticEvidenceSoundness, + pub coverage: SymbolicSemanticEvidenceCoverage, + pub provenance: SymbolicSemanticEvidenceProvenance, + pub ambiguity: SymbolicSemanticEvidenceAmbiguity, + #[serde(default)] + pub budget_limited: bool, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub reasons: Vec, +} + +impl Default for SymbolicSemanticEvidence { + fn default() -> Self { + Self::exact() + } +} + +impl SymbolicSemanticEvidence { + pub fn exact() -> Self { + Self { + tier: SymbolicSemanticConfidence::Exact, + soundness: SymbolicSemanticEvidenceSoundness::Proven, + coverage: SymbolicSemanticEvidenceCoverage::Full, + provenance: SymbolicSemanticEvidenceProvenance::Stable, + ambiguity: SymbolicSemanticEvidenceAmbiguity::Single, + budget_limited: false, + reasons: Vec::new(), + } + } + + pub fn likely(reason: SymbolicSemanticEvidenceReason) -> Self { + Self { + tier: SymbolicSemanticConfidence::Likely, + soundness: SymbolicSemanticEvidenceSoundness::OverApprox, + coverage: SymbolicSemanticEvidenceCoverage::Full, + provenance: SymbolicSemanticEvidenceProvenance::Normalized, + ambiguity: SymbolicSemanticEvidenceAmbiguity::Single, + budget_limited: false, + reasons: vec![reason], + } + } + + pub fn heuristic(reason: SymbolicSemanticEvidenceReason) -> Self { + Self { + tier: SymbolicSemanticConfidence::Heuristic, + soundness: SymbolicSemanticEvidenceSoundness::Ranked, + coverage: SymbolicSemanticEvidenceCoverage::Partial, + provenance: SymbolicSemanticEvidenceProvenance::Ranked, + ambiguity: SymbolicSemanticEvidenceAmbiguity::Ranked, + budget_limited: false, + reasons: vec![reason], + } + } + + pub fn residual(reason: SymbolicSemanticEvidenceReason) -> Self { + Self { + tier: SymbolicSemanticConfidence::Residual, + soundness: SymbolicSemanticEvidenceSoundness::Unknown, + coverage: SymbolicSemanticEvidenceCoverage::Partial, + provenance: SymbolicSemanticEvidenceProvenance::Unstable, + ambiguity: SymbolicSemanticEvidenceAmbiguity::Multiple, + budget_limited: false, + reasons: vec![reason], + } + } + + pub fn with_reason(mut self, reason: SymbolicSemanticEvidenceReason) -> Self { + if !self.reasons.contains(&reason) { + self.reasons.push(reason); + } + self + } + + pub fn is_default_exact(&self) -> bool { + *self == Self::exact() + } + + pub fn is_reliable(&self) -> bool { + self.tier.is_reliable() + } + + pub fn is_usable(&self) -> bool { + self.tier.is_usable() + } + + pub fn allows_hard_proof(&self) -> bool { + matches!(self.tier, SymbolicSemanticConfidence::Exact) + } + + pub fn allows_narrowing(&self) -> bool { + matches!( + self.tier, + SymbolicSemanticConfidence::Exact | SymbolicSemanticConfidence::Likely + ) + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub enum SymbolicVmUnaryOp { Neg, @@ -123,7 +292,20 @@ pub struct SymbolicMemoryCondition { pub offset_hi: i64, pub size: u32, pub exact_offset: bool, + #[serde( + default, + skip_serializing_if = "SymbolicSemanticEvidence::is_default_exact" + )] + pub evidence: SymbolicSemanticEvidence, + #[serde(default = "default_symbolic_confidence_exact")] + pub confidence: SymbolicSemanticConfidence, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub binding: Option, pub expr: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub value_expr: Option, + #[serde(default)] + pub exact_value: bool, } #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] @@ -135,6 +317,13 @@ pub struct SymbolicCompiledCondition { pub backward_memory_candidate_enumerations: usize, pub backward_memory_residual_fallbacks: usize, pub precision: SymbolicConditionPrecision, + #[serde( + default, + skip_serializing_if = "SymbolicSemanticEvidence::is_default_exact" + )] + pub evidence: SymbolicSemanticEvidence, + #[serde(default = "default_symbolic_confidence_exact")] + pub confidence: SymbolicSemanticConfidence, pub supported_paths: usize, pub total_paths: usize, } @@ -172,6 +361,26 @@ pub enum SymbolicVmValueExpr { } impl SymbolicVmValueExpr { + fn split_version(name: &str) -> (&str, Option<&str>) { + name.rsplit_once('_') + .filter(|(_, version)| version.chars().all(|ch| ch.is_ascii_digit())) + .map_or((name, None), |(base, version)| (base, Some(version))) + } + + fn same_logical_name(left: &str, right: &str) -> bool { + Self::split_version(left) + .0 + .eq_ignore_ascii_case(Self::split_version(right).0) + } + + fn lookup_binding(bindings: &BTreeMap, name: &str) -> Option { + bindings.get(name).copied().or_else(|| { + bindings.iter().find_map(|(candidate, value)| { + Self::same_logical_name(candidate, name).then_some(*value) + }) + }) + } + pub fn render(&self) -> String { match self { Self::Const(value) => format!("0x{value:x}"), @@ -224,6 +433,54 @@ impl SymbolicVmValueExpr { } } } + + pub fn evaluate_u64(&self, bindings: &BTreeMap) -> Option { + match self { + Self::Const(value) => Some(*value), + Self::Var(name) => Self::lookup_binding(bindings, name), + Self::Unary { op, expr } => { + let value = expr.evaluate_u64(bindings)?; + Some(match op { + SymbolicVmUnaryOp::Neg => value.wrapping_neg(), + SymbolicVmUnaryOp::Not => !value, + SymbolicVmUnaryOp::BoolNot => u64::from(value == 0), + SymbolicVmUnaryOp::ZExt + | SymbolicVmUnaryOp::SExt + | SymbolicVmUnaryOp::Trunc => value, + }) + } + Self::Binary { op, left, right } => { + let left = left.evaluate_u64(bindings)?; + let right = right.evaluate_u64(bindings)?; + Some(match op { + SymbolicVmBinaryOp::Add | SymbolicVmBinaryOp::PtrAdd => { + left.wrapping_add(right) + } + SymbolicVmBinaryOp::Sub | SymbolicVmBinaryOp::PtrSub => { + left.wrapping_sub(right) + } + SymbolicVmBinaryOp::Mul => left.wrapping_mul(right), + SymbolicVmBinaryOp::Div => left.checked_div(right)?, + SymbolicVmBinaryOp::SDiv => ((left as i64).checked_div(right as i64)?) as u64, + SymbolicVmBinaryOp::Rem => left.checked_rem(right)?, + SymbolicVmBinaryOp::SRem => ((left as i64).checked_rem(right as i64)?) as u64, + SymbolicVmBinaryOp::And => left & right, + SymbolicVmBinaryOp::Or => left | right, + SymbolicVmBinaryOp::Xor => left ^ right, + SymbolicVmBinaryOp::Shl => left.wrapping_shl((right & 63) as u32), + SymbolicVmBinaryOp::Shr => left.wrapping_shr((right & 63) as u32), + SymbolicVmBinaryOp::Eq => u64::from(left == right), + SymbolicVmBinaryOp::Ne => u64::from(left != right), + SymbolicVmBinaryOp::Lt => u64::from(left < right), + SymbolicVmBinaryOp::Le => u64::from(left <= right), + SymbolicVmBinaryOp::Gt => u64::from(left > right), + SymbolicVmBinaryOp::Ge => u64::from(left >= right), + SymbolicVmBinaryOp::Piece | SymbolicVmBinaryOp::Concat => return None, + }) + } + Self::Expr(_) => None, + } + } } #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] @@ -232,6 +489,41 @@ pub struct SymbolicVmStateUpdate { pub expr: String, pub value: SymbolicVmValueExpr, pub exact: bool, + #[serde( + default, + skip_serializing_if = "SymbolicSemanticEvidence::is_default_exact" + )] + pub evidence: SymbolicSemanticEvidence, + #[serde(default = "default_symbolic_confidence_exact")] + pub confidence: SymbolicSemanticConfidence, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct SymbolicVmGuardCondition { + pub expr: String, + pub value: SymbolicVmValueExpr, + pub expect_nonzero: bool, + pub exact: bool, + #[serde( + default, + skip_serializing_if = "SymbolicSemanticEvidence::is_default_exact" + )] + pub evidence: SymbolicSemanticEvidence, + #[serde(default = "default_symbolic_confidence_exact")] + pub confidence: SymbolicSemanticConfidence, +} + +impl SymbolicVmGuardCondition { + pub fn evaluate(&self, bindings: &BTreeMap) -> Option { + let value = self.value.evaluate_u64(bindings)?; + Some((value != 0) == self.expect_nonzero) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct SymbolicVmGuardedExit { + pub target: u64, + pub guard: SymbolicVmGuardCondition, } #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] @@ -240,10 +532,26 @@ pub struct SymbolicVmTransferArm { pub case_values: Vec, pub region_blocks: Vec, pub exit_targets: Vec, + pub exit_guards: Vec, pub state_updates: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub selector_update: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub memory_reads: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub memory_writes: Vec, + #[serde(default)] + pub residual_guards: bool, + #[serde(default)] + pub residual_memory_effects: bool, pub exact: bool, + #[serde( + default, + skip_serializing_if = "SymbolicSemanticEvidence::is_default_exact" + )] + pub evidence: SymbolicSemanticEvidence, + #[serde(default = "default_symbolic_confidence_exact")] + pub confidence: SymbolicSemanticConfidence, pub redispatch: bool, pub may_return: bool, pub truncated: bool, @@ -266,6 +574,12 @@ pub struct SymbolicVmStepSummary { pub handler_state_inputs: BTreeMap>, pub handler_state_outputs: BTreeMap>, pub handler_state_updates: BTreeMap>, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub handler_exit_guards: BTreeMap>, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub handler_memory_read_effects: BTreeMap>, + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub handler_memory_write_effects: BTreeMap>, pub handler_memory_reads: BTreeMap, pub handler_memory_writes: BTreeMap, pub handler_calls: BTreeMap, @@ -278,6 +592,39 @@ pub struct SymbolicVmStepSummary { } pub type SymbolicVmTransferSummary = SymbolicVmStepSummary; +impl SymbolicVmTransferArm { + pub fn exact_exit_guard_for_target(&self, target: u64) -> Option<&SymbolicVmGuardCondition> { + self.exit_guards + .iter() + .find(|guard| guard.target == target && guard.guard.evidence.allows_hard_proof()) + .map(|guard| &guard.guard) + } + + pub fn actionable_exit_guard_for_target( + &self, + target: u64, + ) -> Option<&SymbolicVmGuardCondition> { + self.exit_guards + .iter() + .find(|guard| guard.target == target && guard.guard.evidence.allows_narrowing()) + .map(|guard| &guard.guard) + } + + pub fn exact_exit_guard_result_for_case( + &self, + selector: Option<&str>, + case_value: u64, + target: u64, + ) -> Option { + let guard = self.exact_exit_guard_for_target(target)?; + let mut bindings = BTreeMap::new(); + if let Some(selector) = selector { + bindings.insert(selector.to_string(), case_value); + } + guard.evaluate(&bindings) + } +} + #[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct SymbolicFactDiagnostics { pub branches_evaluated: usize, @@ -318,9 +665,79 @@ pub struct SymbolicBranchFact { pub false_compiled: Option, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum SymbolicControlIslandKind { + BranchFrontier, + LargeCfgBranchFrontier, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct SymbolicControlFact { + pub target: u64, + pub status: SymbolicReachabilityStatus, + #[serde(skip_serializing_if = "Option::is_none")] + pub condition: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub compiled: Option, + #[serde( + default, + skip_serializing_if = "SymbolicSemanticEvidence::is_default_exact" + )] + pub evidence: SymbolicSemanticEvidence, + #[serde(default = "default_symbolic_confidence_exact")] + pub confidence: SymbolicSemanticConfidence, +} + +impl SymbolicControlFact { + pub fn exact_compiled_condition(&self) -> Option<&SymbolicCompiledCondition> { + self.evidence + .allows_hard_proof() + .then_some(self.compiled.as_ref()) + .flatten() + } + + pub fn actionable_compiled_condition(&self) -> Option<&SymbolicCompiledCondition> { + self.evidence + .allows_narrowing() + .then_some(self.compiled.as_ref()) + .flatten() + } +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct SymbolicControlIsland { + pub kind: SymbolicControlIslandKind, + pub anchor_block: u64, + pub frontier_targets: Vec, + pub facts: Vec, + #[serde( + default, + skip_serializing_if = "SymbolicSemanticEvidence::is_default_exact" + )] + pub evidence: SymbolicSemanticEvidence, + #[serde(default = "default_symbolic_confidence_exact")] + pub confidence: SymbolicSemanticConfidence, +} + +impl SymbolicControlIsland { + pub fn exact_reachable_target(&self) -> Option { + unique_reachable_control_target(self.facts.iter(), true) + } + + pub fn exact_compiled_condition(&self) -> Option<&SymbolicCompiledCondition> { + unique_compiled_control_condition(self.facts.iter(), true) + } + + pub fn actionable_compiled_condition(&self) -> Option<&SymbolicCompiledCondition> { + unique_compiled_control_condition(self.facts.iter(), false) + } +} + #[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct SymbolicSemanticFacts { pub branch_facts: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub control_islands: Vec, pub diagnostics: SymbolicFactDiagnostics, #[serde(skip_serializing_if = "Option::is_none")] pub interpreter: Option, @@ -333,6 +750,7 @@ pub struct SymbolicSemanticFacts { impl SymbolicSemanticFacts { pub fn is_empty(&self) -> bool { self.branch_facts.is_empty() + && self.control_islands.is_empty() && self.diagnostics == SymbolicFactDiagnostics::default() && self.interpreter.is_none() && self.vm_step.is_none() @@ -345,6 +763,45 @@ impl SymbolicSemanticFacts { .find(|fact| fact.block_addr == block_addr) } + pub fn control_island_for_block(&self, block_addr: u64) -> Option<&SymbolicControlIsland> { + self.control_islands + .iter() + .find(|island| island.anchor_block == block_addr) + } + + pub fn exact_compiled_condition_for_block( + &self, + block_addr: u64, + ) -> Option<&SymbolicCompiledCondition> { + self.branch_fact_for_block(block_addr) + .and_then(SymbolicBranchFact::exact_compiled_condition) + .or_else(|| { + self.control_island_for_block(block_addr) + .and_then(SymbolicControlIsland::exact_compiled_condition) + }) + } + + pub fn exact_reachable_target_for_block(&self, block_addr: u64) -> Option { + self.branch_fact_for_block(block_addr) + .and_then(SymbolicBranchFact::exact_reachable_target) + .or_else(|| { + self.control_island_for_block(block_addr) + .and_then(SymbolicControlIsland::exact_reachable_target) + }) + } + + pub fn actionable_compiled_condition_for_block( + &self, + block_addr: u64, + ) -> Option<&SymbolicCompiledCondition> { + self.branch_fact_for_block(block_addr) + .and_then(SymbolicBranchFact::actionable_compiled_condition) + .or_else(|| { + self.control_island_for_block(block_addr) + .and_then(SymbolicControlIsland::actionable_compiled_condition) + }) + } + pub fn vm_step_for_dispatch_header( &self, dispatch_header: u64, @@ -365,23 +822,90 @@ impl SymbolicSemanticFacts { } impl SymbolicBranchFact { + pub fn exact_reachable_target(&self) -> Option { + match (self.true_status, self.false_status) { + (SymbolicReachabilityStatus::Reachable, SymbolicReachabilityStatus::Unreachable) => { + Some(self.true_target) + } + (SymbolicReachabilityStatus::Unreachable, SymbolicReachabilityStatus::Reachable) => { + Some(self.false_target) + } + _ => None, + } + } + pub fn exact_compiled_condition(&self) -> Option<&SymbolicCompiledCondition> { match (self.true_status, self.false_status) { (SymbolicReachabilityStatus::Reachable, SymbolicReachabilityStatus::Unreachable) => { - self.true_compiled.as_ref().filter(|compiled| { - matches!(compiled.precision, SymbolicConditionPrecision::Exact) - }) + self.true_compiled + .as_ref() + .filter(|compiled| compiled.evidence.allows_hard_proof()) } (SymbolicReachabilityStatus::Unreachable, SymbolicReachabilityStatus::Reachable) => { - self.false_compiled.as_ref().filter(|compiled| { - matches!(compiled.precision, SymbolicConditionPrecision::Exact) - }) + self.false_compiled + .as_ref() + .filter(|compiled| compiled.evidence.allows_hard_proof()) + } + _ => None, + } + } + + pub fn actionable_compiled_condition(&self) -> Option<&SymbolicCompiledCondition> { + match (self.true_status, self.false_status) { + (SymbolicReachabilityStatus::Reachable, SymbolicReachabilityStatus::Unreachable) => { + self.true_compiled + .as_ref() + .filter(|compiled| compiled.evidence.allows_narrowing()) + } + (SymbolicReachabilityStatus::Unreachable, SymbolicReachabilityStatus::Reachable) => { + self.false_compiled + .as_ref() + .filter(|compiled| compiled.evidence.allows_narrowing()) } _ => None, } } } +fn unique_compiled_control_condition<'a>( + facts: impl Iterator, + hard_proof_only: bool, +) -> Option<&'a SymbolicCompiledCondition> { + let mut candidates = facts.filter_map(|fact| { + if hard_proof_only { + fact.exact_compiled_condition() + } else { + fact.actionable_compiled_condition() + } + }); + let first = candidates.next()?; + candidates.next().is_none().then_some(first) +} + +fn unique_reachable_control_target<'a>( + facts: impl Iterator, + hard_proof_only: bool, +) -> Option { + let mut reachable_target = None; + let mut saw_any = false; + for fact in facts { + saw_any = true; + if hard_proof_only && !fact.evidence.allows_hard_proof() { + return None; + } + match fact.status { + SymbolicReachabilityStatus::Reachable => { + if reachable_target.replace(fact.target).is_some() { + return None; + } + } + SymbolicReachabilityStatus::Unreachable => {} + SymbolicReachabilityStatus::Unknown => return None, + } + } + saw_any.then_some(reachable_target).flatten() +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct FunctionType { pub return_type: CTypeLike, @@ -1041,4 +1565,209 @@ mod tests { assert_eq!(expr.render(), "((-0x10) + (state ^ mask))"); } + + #[test] + fn symbolic_vm_value_expr_evaluates_recursive_forms() { + let expr = SymbolicVmValueExpr::Binary { + op: SymbolicVmBinaryOp::Add, + left: Box::new(SymbolicVmValueExpr::Var("state_0".to_string())), + right: Box::new(SymbolicVmValueExpr::Binary { + op: SymbolicVmBinaryOp::Mul, + left: Box::new(SymbolicVmValueExpr::Const(2)), + right: Box::new(SymbolicVmValueExpr::Const(3)), + }), + }; + let bindings = BTreeMap::from([(String::from("state"), 4u64)]); + assert_eq!(expr.evaluate_u64(&bindings), Some(10)); + } + + #[test] + fn symbolic_vm_transfer_arm_evaluates_exact_exit_guard_for_case() { + let arm = SymbolicVmTransferArm { + handler_target: 0x1004, + case_values: vec![1], + region_blocks: vec![0x1004], + exit_targets: vec![0x1010], + exit_guards: vec![SymbolicVmGuardedExit { + target: 0x1010, + guard: SymbolicVmGuardCondition { + expr: "(vm.sel == 0x1)".to_string(), + value: SymbolicVmValueExpr::Binary { + op: SymbolicVmBinaryOp::Eq, + left: Box::new(SymbolicVmValueExpr::Var("vm.sel".to_string())), + right: Box::new(SymbolicVmValueExpr::Const(1)), + }, + expect_nonzero: true, + exact: true, + evidence: SymbolicSemanticEvidence::exact(), + confidence: SymbolicSemanticConfidence::Exact, + }, + }], + state_updates: Vec::new(), + selector_update: None, + memory_reads: Vec::new(), + memory_writes: Vec::new(), + residual_guards: false, + residual_memory_effects: false, + exact: true, + evidence: SymbolicSemanticEvidence::exact(), + confidence: SymbolicSemanticConfidence::Exact, + redispatch: false, + may_return: true, + truncated: false, + }; + + assert_eq!( + arm.exact_exit_guard_result_for_case(Some("vm.sel"), 1, 0x1010), + Some(true) + ); + assert_eq!( + arm.exact_exit_guard_result_for_case(Some("vm.sel"), 2, 0x1010), + Some(false) + ); + } + + #[test] + fn control_island_actionable_condition_requires_unique_fact() { + let compiled = SymbolicCompiledCondition { + simplified: "false".to_string(), + terms: vec!["false".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: SymbolicConditionPrecision::OverApprox, + evidence: SymbolicSemanticEvidence::likely( + SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: SymbolicSemanticConfidence::Likely, + supported_paths: 1, + total_paths: 2, + }; + let island = SymbolicControlIsland { + kind: SymbolicControlIslandKind::LargeCfgBranchFrontier, + anchor_block: 0x401000, + frontier_targets: vec![0x401010, 0x401020], + facts: vec![ + SymbolicControlFact { + target: 0x401010, + status: SymbolicReachabilityStatus::Unknown, + condition: Some("false".to_string()), + compiled: Some(compiled.clone()), + evidence: SymbolicSemanticEvidence::likely( + SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: SymbolicSemanticConfidence::Likely, + }, + SymbolicControlFact { + target: 0x401020, + status: SymbolicReachabilityStatus::Unknown, + condition: None, + compiled: None, + evidence: SymbolicSemanticEvidence::residual( + SymbolicSemanticEvidenceReason::GuardOpaque, + ), + confidence: SymbolicSemanticConfidence::Residual, + }, + ], + evidence: SymbolicSemanticEvidence::likely( + SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: SymbolicSemanticConfidence::Likely, + }; + + assert_eq!( + island + .actionable_compiled_condition() + .map(|cond| cond.simplified.as_str()), + Some("false") + ); + } + + #[test] + fn semantic_facts_actionable_condition_for_block_uses_control_island_fallback() { + let compiled = SymbolicCompiledCondition { + simplified: "x == 0".to_string(), + terms: vec!["x == 0".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: SymbolicConditionPrecision::Exact, + evidence: SymbolicSemanticEvidence::exact(), + confidence: SymbolicSemanticConfidence::Exact, + supported_paths: 1, + total_paths: 1, + }; + let facts = SymbolicSemanticFacts { + branch_facts: Vec::new(), + control_islands: vec![SymbolicControlIsland { + kind: SymbolicControlIslandKind::LargeCfgBranchFrontier, + anchor_block: 0x401000, + frontier_targets: vec![0x401020], + facts: vec![SymbolicControlFact { + target: 0x401020, + status: SymbolicReachabilityStatus::Reachable, + condition: Some("x == 0".to_string()), + compiled: Some(compiled.clone()), + evidence: SymbolicSemanticEvidence::exact(), + confidence: SymbolicSemanticConfidence::Exact, + }], + evidence: SymbolicSemanticEvidence::exact(), + confidence: SymbolicSemanticConfidence::Exact, + }], + diagnostics: SymbolicFactDiagnostics::default(), + interpreter: None, + vm_step: None, + vm_transfer: None, + }; + + assert_eq!( + facts + .actionable_compiled_condition_for_block(0x401000) + .map(|cond| cond.simplified.as_str()), + Some("x == 0") + ); + } + + #[test] + fn semantic_facts_exact_reachable_target_for_block_uses_control_island_fallback() { + let facts = SymbolicSemanticFacts { + branch_facts: Vec::new(), + control_islands: vec![SymbolicControlIsland { + kind: SymbolicControlIslandKind::LargeCfgBranchFrontier, + anchor_block: 0x401000, + frontier_targets: vec![0x401010, 0x401020], + facts: vec![ + SymbolicControlFact { + target: 0x401010, + status: SymbolicReachabilityStatus::Reachable, + condition: Some("x == 0".to_string()), + compiled: None, + evidence: SymbolicSemanticEvidence::exact(), + confidence: SymbolicSemanticConfidence::Exact, + }, + SymbolicControlFact { + target: 0x401020, + status: SymbolicReachabilityStatus::Unreachable, + condition: Some("!(x == 0)".to_string()), + compiled: None, + evidence: SymbolicSemanticEvidence::exact(), + confidence: SymbolicSemanticConfidence::Exact, + }, + ], + evidence: SymbolicSemanticEvidence::exact(), + confidence: SymbolicSemanticConfidence::Exact, + }], + diagnostics: SymbolicFactDiagnostics::default(), + interpreter: None, + vm_step: None, + vm_transfer: None, + }; + + assert_eq!( + facts.exact_reachable_target_for_block(0x401000), + Some(0x401010) + ); + } } diff --git a/crates/r2types/src/lib.rs b/crates/r2types/src/lib.rs index 9b6fac9..494b20d 100644 --- a/crates/r2types/src/lib.rs +++ b/crates/r2types/src/lib.rs @@ -30,12 +30,16 @@ pub use facts::{ CalleeMemoryRange, CalleeMemoryRegion, CalleeReturnRelation, FunctionParamSpec, FunctionSignatureSpec, FunctionType, FunctionTypeFactInputs, FunctionTypeFacts, FunctionTypeFactsBuilder, InterprocFactDiagnostics, LocalFieldAccessFact, ResolvedFieldLayout, - SymbolicBranchFact, SymbolicCompiledCondition, SymbolicConditionPrecision, - SymbolicFactDiagnostics, SymbolicInterpreterDispatch, SymbolicInterpreterKind, - SymbolicMemoryCondition, SymbolicMemoryRegion, SymbolicMemoryRegionKind, - SymbolicMemoryRegionRef, SymbolicReachabilityStatus, SymbolicSemanticCapability, - SymbolicSemanticFacts, SymbolicSemanticMode, SymbolicSemanticResidualReason, - SymbolicSemanticSliceClass, SymbolicVmBinaryOp, SymbolicVmStateUpdate, SymbolicVmStepSummary, + SymbolicBranchFact, SymbolicCompiledCondition, SymbolicConditionPrecision, SymbolicControlFact, + SymbolicControlIsland, SymbolicControlIslandKind, SymbolicFactDiagnostics, + SymbolicInterpreterDispatch, SymbolicInterpreterKind, SymbolicMemoryCondition, + SymbolicMemoryRegion, SymbolicMemoryRegionKind, SymbolicMemoryRegionRef, + SymbolicReachabilityStatus, SymbolicSemanticCapability, SymbolicSemanticConfidence, + SymbolicSemanticEvidence, SymbolicSemanticEvidenceAmbiguity, SymbolicSemanticEvidenceCoverage, + SymbolicSemanticEvidenceProvenance, SymbolicSemanticEvidenceReason, + SymbolicSemanticEvidenceSoundness, SymbolicSemanticFacts, SymbolicSemanticMode, + SymbolicSemanticResidualReason, SymbolicSemanticSliceClass, SymbolicVmBinaryOp, + SymbolicVmGuardCondition, SymbolicVmGuardedExit, SymbolicVmStateUpdate, SymbolicVmStepSummary, SymbolicVmTransferArm, SymbolicVmTransferSummary, SymbolicVmUnaryOp, SymbolicVmValueExpr, VisibleBinding, VisibleBindingKind, parse_type_like_spec, }; @@ -49,5 +53,5 @@ pub use writeback::{ RecoveredVariable, StructDeclCandidate, StructDeclSource, StructFieldCandidate, TypeWritebackAnalysis, TypeWritebackAnalysisInput, TypeWritebackDiagnostics, TypeWritebackPlan, VarRenameCandidate, VarTypeCandidate, WritebackEvidence, WritebackSource, - build_type_writeback_analysis, + augment_local_struct_artifacts_with_symbolic_facts, build_type_writeback_analysis, }; diff --git a/crates/r2types/src/writeback.rs b/crates/r2types/src/writeback.rs index bfee199..b916830 100644 --- a/crates/r2types/src/writeback.rs +++ b/crates/r2types/src/writeback.rs @@ -18,7 +18,8 @@ use crate::facts::{ CalleeArgEffect, CalleeFact, CalleeMemoryEffect, CalleeMemoryEffectKind, CalleeMemoryLocation, CalleeMemoryRange, CalleeMemoryRegion, CalleeReturnRelation, FunctionParamSpec, FunctionSignatureSpec, FunctionTypeFactInputs, FunctionTypeFacts, InterprocFactDiagnostics, - VisibleBinding, VisibleBindingKind, parse_type_like_spec, + SymbolicMemoryCondition, SymbolicMemoryRegion, SymbolicSemanticFacts, VisibleBinding, + VisibleBindingKind, parse_type_like_spec, }; use crate::model::Signedness; @@ -666,6 +667,99 @@ pub fn build_type_writeback_analysis( } } +pub fn augment_local_struct_artifacts_with_symbolic_facts( + local_structs: &mut LocalStructArtifacts, + symbolic_facts: &SymbolicSemanticFacts, + ptr_bits: u32, +) { + let mut projected_profiles = BTreeMap::>::new(); + for compiled in symbolic_facts + .branch_facts + .iter() + .filter_map(|fact| fact.actionable_compiled_condition()) + .chain( + symbolic_facts + .control_islands + .iter() + .filter_map(|island| island.actionable_compiled_condition()), + ) + { + if !(compiled.confidence.is_reliable() && compiled.evidence.is_reliable()) { + continue; + } + for term in &compiled.memory_terms { + let Some((slot, offset, field_type)) = symbolic_memory_term_slot_field(term, ptr_bits) + else { + continue; + }; + projected_profiles + .entry(slot) + .or_default() + .entry(offset) + .or_insert(field_type); + } + } + + for (slot, projected) in projected_profiles { + let profile = local_structs.slot_field_profiles.entry(slot).or_default(); + for (offset, field_type) in projected { + profile.entry(offset).or_insert(field_type); + } + if profile.is_empty() || local_structs.slot_type_overrides.contains_key(&slot) { + continue; + } + let struct_name = format!("sla_struct_symbolic_arg{}", slot + 1); + let fields = profile + .iter() + .map(|(offset, field_type)| StructFieldCandidate { + name: format!("f_{offset:x}"), + offset: *offset, + field_type: field_type.clone(), + confidence: 84, + }) + .collect::>(); + let Some(decl) = build_struct_decl(&struct_name, &fields, ptr_bits) else { + continue; + }; + if !local_structs + .struct_decls + .iter() + .any(|candidate| candidate.name.eq_ignore_ascii_case(&struct_name)) + { + local_structs.struct_decls.push(StructDeclCandidate { + name: struct_name.clone(), + decl, + confidence: 84, + source: StructDeclSource::LocalInferred, + fields, + }); + } + local_structs + .slot_type_overrides + .insert(slot, format!("struct {struct_name} *")); + } +} + +fn symbolic_memory_term_slot_field( + term: &SymbolicMemoryCondition, + _ptr_bits: u32, +) -> Option<(usize, u64, String)> { + if !(term.confidence.is_reliable() && term.evidence.is_reliable()) { + return None; + } + let slot = match term.region { + SymbolicMemoryRegion::Argument { index } => index, + SymbolicMemoryRegion::Region(_) => return None, + }; + if !term.exact_offset && term.offset_lo != term.offset_hi { + return None; + } + if term.offset_lo < 0 || term.offset_hi < 0 || term.offset_lo != term.offset_hi { + return None; + } + Some((slot, term.offset_lo as u64, size_to_type(term.size))) +} + fn canonicalize_param_home_stack_slots( merged_signature: Option<&FunctionSignatureSpec>, register_params: &[crate::context::ExternalRegisterParamSpec], @@ -4382,4 +4476,80 @@ mod tests { Some(&CTypeLike::Pointer(Box::new(CTypeLike::Void))) ); } + + #[test] + fn symbolic_actionable_memory_terms_seed_local_struct_profiles() { + let compiled = crate::facts::SymbolicCompiledCondition { + simplified: "arg0->f_8 == 0".to_string(), + terms: vec!["arg0->f_8 == 0".to_string()], + memory_terms: vec![crate::facts::SymbolicMemoryCondition { + region: crate::facts::SymbolicMemoryRegion::Argument { index: 0 }, + offset_lo: 8, + offset_hi: 8, + size: 4, + exact_offset: true, + evidence: crate::facts::SymbolicSemanticEvidence::exact(), + confidence: crate::facts::SymbolicSemanticConfidence::Exact, + binding: None, + expr: "*(arg0 + 8)".to_string(), + value_expr: None, + exact_value: false, + }], + backward_memory_substitutions: 1, + backward_memory_candidate_enumerations: 1, + backward_memory_residual_fallbacks: 0, + precision: crate::facts::SymbolicConditionPrecision::Exact, + evidence: crate::facts::SymbolicSemanticEvidence::exact(), + confidence: crate::facts::SymbolicSemanticConfidence::Exact, + supported_paths: 1, + total_paths: 1, + }; + let symbolic_facts = crate::facts::SymbolicSemanticFacts { + branch_facts: Vec::new(), + control_islands: vec![crate::facts::SymbolicControlIsland { + kind: crate::facts::SymbolicControlIslandKind::LargeCfgBranchFrontier, + anchor_block: 0x401000, + frontier_targets: vec![0x401020], + facts: vec![crate::facts::SymbolicControlFact { + target: 0x401020, + status: crate::facts::SymbolicReachabilityStatus::Reachable, + condition: Some("arg0->f_8 == 0".to_string()), + compiled: Some(compiled), + evidence: crate::facts::SymbolicSemanticEvidence::exact(), + confidence: crate::facts::SymbolicSemanticConfidence::Exact, + }], + evidence: crate::facts::SymbolicSemanticEvidence::exact(), + confidence: crate::facts::SymbolicSemanticConfidence::Exact, + }], + diagnostics: crate::facts::SymbolicFactDiagnostics::default(), + interpreter: None, + vm_step: None, + vm_transfer: None, + }; + let mut local_structs = LocalStructArtifacts::default(); + + augment_local_struct_artifacts_with_symbolic_facts(&mut local_structs, &symbolic_facts, 64); + + assert_eq!( + local_structs + .slot_field_profiles + .get(&0) + .and_then(|profile| profile.get(&8)) + .map(String::as_str), + Some("int32_t") + ); + assert_eq!( + local_structs + .slot_type_overrides + .get(&0) + .map(String::as_str), + Some("struct sla_struct_symbolic_arg1 *") + ); + assert!( + local_structs + .struct_decls + .iter() + .any(|decl| decl.name == "sla_struct_symbolic_arg1") + ); + } } diff --git a/r2plugin/Cargo.toml b/r2plugin/Cargo.toml index 8a1a740..656ce4e 100644 --- a/r2plugin/Cargo.toml +++ b/r2plugin/Cargo.toml @@ -28,5 +28,6 @@ optional = true default = ["x86"] x86 = ["sleigh-config/x86"] arm = ["sleigh-config/ARM", "sleigh-config/AARCH64"] +mips = ["sleigh-config/MIPS"] riscv = ["sleigh-config/RISCV"] -all-archs = ["x86", "arm", "riscv"] +all-archs = ["x86", "arm", "mips", "riscv"] diff --git a/r2plugin/Makefile b/r2plugin/Makefile index 26b1d7d..6cd74bd 100644 --- a/r2plugin/Makefile +++ b/r2plugin/Makefile @@ -179,7 +179,7 @@ help: @echo "" @echo "Variables:" @echo " RUST_TARGET - 'release' (default) or 'debug'" - @echo " RUST_FEATURES - Sleigh features: 'x86' (default), 'arm' (ARM32 + AArch64), 'riscv', 'all-archs'" + @echo " RUST_FEATURES - Sleigh features: 'x86' (default), 'arm' (ARM32 + AArch64), 'mips', 'riscv', 'all-archs'" @echo "" @echo "Examples:" @echo " make # Build release with x86" diff --git a/r2plugin/r_anal_sleigh.c b/r2plugin/r_anal_sleigh.c index d7b85e6..bec085c 100644 --- a/r2plugin/r_anal_sleigh.c +++ b/r2plugin/r_anal_sleigh.c @@ -5900,10 +5900,30 @@ R2ILContext *get_context(RAnal *anal) { sleigh_arch_str = "riscv32"; } else if (!strcmp (arch, "riscv64") || !strcmp (arch, "rv64")) { sleigh_arch_str = "riscv64"; - } else if (!strcmp (arch, "mips")) { - /* Simple heuristic for MIPS (assuming default is 32be/le) */ - /* Note: This is partial, better use manual override for complex variants */ - sleigh_arch_str = "mips"; /* Placeholder - mapped often to general mips */ + } else if (!strcmp (arch, "mips") || !strcmp (arch, "mips32") + || !strcmp (arch, "mips32be") || !strcmp (arch, "mipsbe") + || !strcmp (arch, "mipseb") || !strcmp (arch, "mipsel") + || !strcmp (arch, "mips32le") || !strcmp (arch, "mips32el") + || !strcmp (arch, "mips64") || !strcmp (arch, "mips64be") + || !strcmp (arch, "mips64le") || !strcmp (arch, "mips64el")) { + bool is64 = bits >= 64 + || !strcmp (arch, "mips64") + || !strcmp (arch, "mips64be") + || !strcmp (arch, "mips64le") + || !strcmp (arch, "mips64el"); + bool big_endian = R_ARCH_CONFIG_IS_BIG_ENDIAN (anal->config); + if (!strcmp (arch, "mipsel") || !strcmp (arch, "mips32le") + || !strcmp (arch, "mips32el") + || !strcmp (arch, "mips64le") + || !strcmp (arch, "mips64el")) { + big_endian = false; + } else if (!strcmp (arch, "mips32be") || !strcmp (arch, "mipsbe") + || !strcmp (arch, "mipseb") || !strcmp (arch, "mips64be")) { + big_endian = true; + } + sleigh_arch_str = is64 + ? (big_endian ? "mips64be" : "mips64le") + : (big_endian ? "mips32be" : "mips32le"); } else { return NULL; /* unsupported arch */ } diff --git a/r2plugin/src/analysis/sym.rs b/r2plugin/src/analysis/sym.rs index 905f8eb..1dc78f2 100644 --- a/r2plugin/src/analysis/sym.rs +++ b/r2plugin/src/analysis/sym.rs @@ -256,6 +256,11 @@ pub(crate) struct CompiledSemanticInfo { pub(crate) summary_attempted: usize, pub(crate) summary_budget_exhausted: usize, pub(crate) summary_scc_count: usize, + pub(crate) branch_fact_count: usize, + pub(crate) control_island_count: usize, + pub(crate) compiled_condition_count: usize, + pub(crate) exact_compiled_condition_count: usize, + pub(crate) actionable_compiled_condition_count: usize, pub(crate) branches_pruned: usize, pub(crate) branches_unknown: usize, pub(crate) skipped_large_cfg: bool, @@ -291,6 +296,38 @@ pub(crate) struct VmStateUpdateInfo { pub(crate) expr: String, pub(crate) value: String, pub(crate) exact: bool, + pub(crate) confidence: String, +} + +#[derive(Debug, Serialize, Clone)] +pub(crate) struct VmGuardConditionInfo { + pub(crate) expr: String, + pub(crate) value: String, + pub(crate) expect_nonzero: bool, + pub(crate) exact: bool, + pub(crate) confidence: String, +} + +#[derive(Debug, Serialize, Clone)] +pub(crate) struct VmGuardedExitInfo { + pub(crate) target: String, + pub(crate) guard: VmGuardConditionInfo, +} + +#[derive(Debug, Serialize, Clone)] +pub(crate) struct VmMemoryConditionInfo { + pub(crate) region: String, + pub(crate) offset_lo: i64, + pub(crate) offset_hi: i64, + pub(crate) size: u32, + pub(crate) exact_offset: bool, + pub(crate) confidence: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) binding: Option, + pub(crate) expr: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) value_expr: Option, + pub(crate) exact_value: bool, } #[derive(Debug, Serialize, Clone)] @@ -299,10 +336,16 @@ pub(crate) struct VmTransferArmInfo { pub(crate) case_values: Vec, pub(crate) region_blocks: Vec, pub(crate) exit_targets: Vec, + pub(crate) exit_guards: Vec, pub(crate) state_updates: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) selector_update: Option, + pub(crate) memory_reads: Vec, + pub(crate) memory_writes: Vec, + pub(crate) residual_guards: bool, + pub(crate) residual_memory_effects: bool, pub(crate) exact: bool, + pub(crate) confidence: String, pub(crate) redispatch: bool, pub(crate) may_return: bool, pub(crate) truncated: bool, @@ -325,6 +368,9 @@ pub(crate) struct VmStepSummaryInfo { pub(crate) handler_state_inputs: BTreeMap>, pub(crate) handler_state_outputs: BTreeMap>, pub(crate) handler_state_updates: BTreeMap>, + pub(crate) handler_exit_guards: BTreeMap>, + pub(crate) handler_memory_read_effects: BTreeMap>, + pub(crate) handler_memory_write_effects: BTreeMap>, pub(crate) handler_memory_reads: BTreeMap, pub(crate) handler_memory_writes: BTreeMap, pub(crate) handler_calls: BTreeMap, @@ -340,6 +386,16 @@ fn render_vm_value_expr(value: &SymbolicVmValueExpr) -> String { value.render() } +fn render_semantic_confidence(confidence: r2sym::SemanticConfidence) -> String { + match confidence { + r2sym::SemanticConfidence::Exact => "exact", + r2sym::SemanticConfidence::Likely => "likely", + r2sym::SemanticConfidence::Heuristic => "heuristic", + r2sym::SemanticConfidence::Residual => "residual", + } + .to_string() +} + fn vm_state_update_info_from_sym(update: &r2sym::VmStateUpdate) -> VmStateUpdateInfo { let value = symbolic_vm_value_expr_from_sym(&update.value); VmStateUpdateInfo { @@ -347,6 +403,54 @@ fn vm_state_update_info_from_sym(update: &r2sym::VmStateUpdate) -> VmStateUpdate expr: update.expr.clone(), value: render_vm_value_expr(&value), exact: update.exact, + confidence: render_semantic_confidence(update.confidence()), + } +} + +fn vm_guard_condition_info_from_sym(guard: &r2sym::VmGuardCondition) -> VmGuardConditionInfo { + let value = symbolic_vm_value_expr_from_sym(&guard.value); + VmGuardConditionInfo { + expr: guard.expr.clone(), + value: render_vm_value_expr(&value), + expect_nonzero: guard.expect_nonzero, + exact: guard.exact, + confidence: render_semantic_confidence(guard.confidence()), + } +} + +fn vm_guarded_exit_info_from_sym(guarded: &r2sym::VmGuardedExit) -> VmGuardedExitInfo { + VmGuardedExitInfo { + target: format!("0x{:x}", guarded.target), + guard: vm_guard_condition_info_from_sym(&guarded.guard), + } +} + +fn vm_memory_condition_info_from_sym( + condition: &r2sym::VmMemoryCondition, +) -> VmMemoryConditionInfo { + VmMemoryConditionInfo { + region: format!( + "{}:{}#{}", + match condition.region.kind { + r2sym::MemoryRegionKind::Stack => "stack", + r2sym::MemoryRegionKind::Global => "global", + r2sym::MemoryRegionKind::Input => "input", + r2sym::MemoryRegionKind::Heap => "heap", + r2sym::MemoryRegionKind::Replay => "replay", + r2sym::MemoryRegionKind::EscapedUnknown => "unknown", + }, + condition.region.name, + condition.region.id + ), + offset_lo: condition.offset_lo, + offset_hi: condition.offset_hi, + size: condition.size, + exact_offset: condition.exact_offset, + confidence: render_semantic_confidence(condition.confidence()), + binding: condition.binding.clone(), + expr: condition.expr.clone(), + value_expr: condition.value_expr.clone(), + exact_value: condition.exact_value, } } @@ -364,6 +468,11 @@ fn vm_transfer_arm_info_from_sym(transfer: &r2sym::VmTransferArm) -> VmTransferA .iter() .map(|addr| format!("0x{addr:x}")) .collect(), + exit_guards: transfer + .exit_guards + .iter() + .map(vm_guarded_exit_info_from_sym) + .collect(), state_updates: transfer .state_updates .iter() @@ -373,7 +482,20 @@ fn vm_transfer_arm_info_from_sym(transfer: &r2sym::VmTransferArm) -> VmTransferA .selector_update .as_ref() .map(vm_state_update_info_from_sym), + memory_reads: transfer + .memory_reads + .iter() + .map(vm_memory_condition_info_from_sym) + .collect(), + memory_writes: transfer + .memory_writes + .iter() + .map(vm_memory_condition_info_from_sym) + .collect(), + residual_guards: transfer.residual_guards, + residual_memory_effects: transfer.residual_memory_effects, exact: transfer.exact, + confidence: render_semantic_confidence(transfer.confidence()), redispatch: transfer.redispatch, may_return: transfer.may_return, truncated: transfer.truncated, @@ -442,6 +564,42 @@ fn vm_step_summary_info_from_sym(vm_step: &r2sym::VmStepSummary) -> VmStepSummar ) }) .collect(), + handler_exit_guards: vm_step + .handler_exit_guards + .iter() + .map(|(target, guards)| { + ( + format!("0x{target:x}"), + guards.iter().map(vm_guarded_exit_info_from_sym).collect(), + ) + }) + .collect(), + handler_memory_read_effects: vm_step + .handler_memory_read_effects + .iter() + .map(|(target, conditions)| { + ( + format!("0x{target:x}"), + conditions + .iter() + .map(vm_memory_condition_info_from_sym) + .collect(), + ) + }) + .collect(), + handler_memory_write_effects: vm_step + .handler_memory_write_effects + .iter() + .map(|(target, conditions)| { + ( + format!("0x{target:x}"), + conditions + .iter() + .map(vm_memory_condition_info_from_sym) + .collect(), + ) + }) + .collect(), handler_memory_reads: vm_step .handler_memory_reads .iter() @@ -607,9 +765,39 @@ fn build_sym_exec_summary( } } +fn empty_symbolic_summary<'ctx>() -> r2sym::SymbolicFunctionSummary<'ctx> { + r2sym::SymbolicFunctionSummary { + completion: r2sym::QueryCompletion::Complete, + paths: Vec::new(), + feasible_paths: 0, + stats: r2sym::path::ExploreStats::default(), + solver_stats: r2sym::SolverStats::default(), + } +} + +fn should_skip_expensive_symbolic_summary( + compiled: &r2sym::CompiledSemanticArtifact, + prepared: &r2ssa::SsaArtifact, +) -> bool { + compiled.symbolic_facts.diagnostics.skipped_large_cfg + || prepared.function().cfg_risk_summary().block_count > 96 +} + pub(crate) fn compiled_semantic_info( compiled: &r2sym::CompiledSemanticArtifact, ) -> CompiledSemanticInfo { + let branch_compiled_conditions = compiled + .symbolic_facts + .branch_facts + .iter() + .flat_map(|fact| fact.true_compiled.iter().chain(fact.false_compiled.iter())) + .collect::>(); + let control_facts = compiled + .symbolic_facts + .control_islands + .iter() + .flat_map(|island| island.facts.iter()) + .collect::>(); CompiledSemanticInfo { mode: match compiled.mode { r2sym::SemanticMode::Raw => "raw".to_string(), @@ -653,6 +841,17 @@ pub(crate) fn compiled_semantic_info( summary_budget_exhausted: compiled.derived_diagnostics.budget_exhausted + compiled.derived_diagnostics.scc_budget_exhausted, summary_scc_count: compiled.derived_diagnostics.scc_count, + branch_fact_count: compiled.symbolic_facts.branch_facts.len(), + control_island_count: compiled.symbolic_facts.control_islands.len(), + compiled_condition_count: branch_compiled_conditions.len(), + exact_compiled_condition_count: control_facts + .iter() + .filter(|fact| fact.evidence.allows_hard_proof()) + .count(), + actionable_compiled_condition_count: control_facts + .iter() + .filter(|fact| fact.evidence.allows_narrowing()) + .count(), branches_pruned: compiled.symbolic_facts.diagnostics.branches_pruned, branches_unknown: compiled.symbolic_facts.diagnostics.branches_unknown, skipped_large_cfg: compiled.symbolic_facts.diagnostics.skipped_large_cfg, @@ -1016,6 +1215,9 @@ pub extern "C" fn r2sym_function( &symbol_map, query_config.summary_profile, ); + if should_skip_expensive_symbolic_summary(&compiled, &prepared) { + return (empty_symbolic_summary(), compiled); + } let mut initial_state = r2sym::SymState::new(&z3_ctx, entry_addr); r2sym::seed_default_state_for_arch(&mut initial_state, &prepared, ctx_view.arch); let mut explorer = query_config.make_explorer(&z3_ctx); @@ -1400,6 +1602,9 @@ pub extern "C" fn r2sym_function_scope( &symbol_map, query_config.summary_profile, ); + if should_skip_expensive_symbolic_summary(&compiled, prepared) { + return (empty_symbolic_summary(), compiled); + } let mut initial_state = r2sym::SymState::new(&z3_ctx, entry_addr); r2sym::seed_default_state_for_arch(&mut initial_state, prepared, ctx_view.arch); let mut explorer = query_config.make_explorer(&z3_ctx); diff --git a/r2plugin/src/bin/r2sleigh-plugin-install.rs b/r2plugin/src/bin/r2sleigh-plugin-install.rs index f1a81ac..d9db8c5 100644 --- a/r2plugin/src/bin/r2sleigh-plugin-install.rs +++ b/r2plugin/src/bin/r2sleigh-plugin-install.rs @@ -90,6 +90,6 @@ fn print_help() { println!( "r2sleigh-plugin-install\n\n\ Usage:\n r2sleigh-plugin-install [--features ] [--target ]\n\n\ -Options:\n --features Sleigh features (x86, arm[ARM32+AArch64], riscv, all-archs)\n --target RUST_TARGET for Makefile (release or debug)\n -h, --help Show this help message" +Options:\n --features Sleigh features (x86, arm[ARM32+AArch64], mips, riscv, all-archs)\n --target RUST_TARGET for Makefile (release or debug)\n -h, --help Show this help message" ); } diff --git a/r2plugin/src/decompiler.rs b/r2plugin/src/decompiler.rs index 54d9ae2..78c10cc 100644 --- a/r2plugin/src/decompiler.rs +++ b/r2plugin/src/decompiler.rs @@ -128,12 +128,18 @@ pub(crate) fn run_full_decompile_on_large_stack( ) -> String { const STACK_SIZE: usize = 512 * 1024 * 1024; + fn semantic_artifact_allows_full_decompile( + artifact: Option<&crate::types::FunctionAnalysisArtifact>, + ) -> bool { + artifact + .and_then(|artifact| artifact.semantic_artifact.as_ref()) + .is_some_and(|compiled| compiled.capability.decompile_ready) + } + let handle = std::thread::Builder::new() .stack_size(STACK_SIZE) .spawn(move || { - if let Some(reason) = crate::decompiler_cfg_guard_reason(&r2il_blocks) { - return decompile_artifact_guard_fallback(&func_name_str, &reason); - } + let cfg_guard_reason = crate::decompiler_cfg_guard_reason(&r2il_blocks); let arch_name = normalize_sig_arch_name(arch.as_ref()).unwrap_or_else(|| "unknown".to_string()); let config = decompiler_config_for_arch_name(&arch_name, ptr_bits); @@ -146,6 +152,8 @@ pub(crate) fn run_full_decompile_on_large_stack( &symbols, ); let mut artifact = if let Some(artifact) = cached_artifact { + let allow_semantic_decompile = + semantic_artifact_allows_full_decompile(Some(&artifact)); if let Some(semantic_artifact) = artifact.semantic_artifact.as_ref() && crate::should_decompile_from_semantic_fallback( &func_name_str, @@ -157,14 +165,27 @@ pub(crate) fn run_full_decompile_on_large_stack( semantic_artifact, ); } + if let Some(reason) = cfg_guard_reason.as_ref() + && !allow_semantic_decompile + { + return decompile_artifact_guard_fallback(&func_name_str, reason); + } artifact } else { - let precomputed_semantic_artifact = crate::types::collect_detached_semantic_artifact( - &r2il_blocks, - &func_name_str, - arch.as_ref(), - symbolic_scope.as_ref(), + let precomputed_semantic_artifact = cfg_guard_reason.as_ref().map_or_else( + || None, + |_| { + crate::types::collect_detached_semantic_artifact( + &r2il_blocks, + &func_name_str, + arch.as_ref(), + symbolic_scope.as_ref(), + ) + }, ); + let allow_semantic_decompile = precomputed_semantic_artifact + .as_ref() + .is_some_and(|compiled| compiled.capability.decompile_ready); if let Some(semantic_artifact) = precomputed_semantic_artifact.as_ref() && crate::should_decompile_from_semantic_fallback( &func_name_str, @@ -176,6 +197,11 @@ pub(crate) fn run_full_decompile_on_large_stack( semantic_artifact, ); } + if let Some(reason) = cfg_guard_reason.as_ref() + && !allow_semantic_decompile + { + return decompile_artifact_guard_fallback(&func_name_str, reason); + } let Some(artifact) = crate::types::build_detached_function_analysis_artifact_with_scope_and_semantics( &r2il_blocks, @@ -209,6 +235,10 @@ pub(crate) fn run_full_decompile_on_large_stack( ); let decompiler = r2dec::Decompiler::new(config); + let semantic_fallback_output = artifact + .semantic_artifact + .as_ref() + .map(|compiled| crate::decompile_semantic_artifact_fallback(&display_func_name, compiled)); let input = decompiler_input_from_artifact( artifact, function_names, @@ -216,7 +246,21 @@ pub(crate) fn run_full_decompile_on_large_stack( symbols, ); - decompiler.decompile_input(&input) + let output = decompiler.decompile_input(&input); + if output.trim().is_empty() { + return semantic_fallback_output.unwrap_or_else(|| { + cfg_guard_reason + .as_ref() + .map(|reason| decompile_artifact_guard_fallback(&func_name_str, reason)) + .unwrap_or_else(|| { + format!( + "/* r2dec fallback: skipped decompilation for {} (empty output) */", + display_func_name + ) + }) + }); + } + output }); match handle { diff --git a/r2plugin/src/lib.rs b/r2plugin/src/lib.rs index f330da6..9274600 100644 --- a/r2plugin/src/lib.rs +++ b/r2plugin/src/lib.rs @@ -305,6 +305,20 @@ pub extern "C" fn r2il_get_reg_profile(ctx: *const R2ILContext) -> *mut c_char { profile.push_str(&format!("gpr\t{}\t.{}\t{}\t0\n", name_lower, bits, offset)); } + let mut stripped_aliases = Vec::new(); + for (original, (bits, offset, _)) in ®_meta { + if let Some(stripped) = original.strip_prefix('$') + && !stripped.is_empty() + && !reg_meta.contains_key(stripped) + { + stripped_aliases.push((stripped.to_string(), *bits, *offset)); + } + } + for (alias, bits, offset) in stripped_aliases { + profile.push_str(&format!("gpr\t{}\t.{}\t{}\t0\n", alias, bits, offset)); + reg_meta.insert(alias.clone(), (bits, offset, alias)); + } + // Synthesize missing aliases expected by radare2/ESIL for specific arches. let mut add_gpr_alias = |alias_name: &str, source_name: &str| { let alias_lower = alias_name.to_ascii_lowercase(); @@ -336,19 +350,19 @@ pub extern "C" fn r2il_get_reg_profile(ctx: *const R2ILContext) -> *mut c_char { .find_map(|name| reg_meta.get(*name).map(|(_, _, original)| original.clone())) }; - let pc = first_existing(&["pc", "rip", "eip", "ip"]); - let sp = first_existing(&["sp", "rsp", "esp"]); - let bp = first_existing(&["bp", "rbp", "ebp", "fp", "x29"]); + let pc = first_existing(&["pc", "$pc", "rip", "eip", "ip"]); + let sp = first_existing(&["sp", "$sp", "rsp", "esp"]); + let bp = first_existing(&["bp", "rbp", "ebp", "fp", "$fp", "s8", "$s8", "x29"]); let mut a_roles: [Option; 8] = std::array::from_fn(|_| None); - a_roles[0] = first_existing(&["rdi", "a0", "x0", "w0", "r0"]); - a_roles[1] = first_existing(&["rsi", "a1", "x1", "w1", "r1"]); - a_roles[2] = first_existing(&["rdx", "a2", "x2", "w2", "r2"]); - a_roles[3] = first_existing(&["rcx", "a3", "x3", "w3", "r3"]); + a_roles[0] = first_existing(&["rdi", "a0", "$a0", "x0", "w0", "r0"]); + a_roles[1] = first_existing(&["rsi", "a1", "$a1", "x1", "w1", "r1"]); + a_roles[2] = first_existing(&["rdx", "a2", "$a2", "x2", "w2", "r2"]); + a_roles[3] = first_existing(&["rcx", "a3", "$a3", "x3", "w3", "r3"]); let mut r_roles: [Option; 4] = std::array::from_fn(|_| None); - r_roles[0] = first_existing(&["r0", "rax", "eax", "v0", "x0", "w0"]); - r_roles[1] = first_existing(&["r1", "x1", "w1"]); + r_roles[0] = first_existing(&["r0", "rax", "eax", "v0", "$v0", "x0", "w0"]); + r_roles[1] = first_existing(&["r1", "v1", "$v1", "x1", "w1"]); r_roles[2] = first_existing(&["r2", "x2", "w2"]); r_roles[3] = first_existing(&["r3", "x3", "w3"]); @@ -2285,6 +2299,74 @@ fn create_disassembler_for_arch(arch: &str) -> Result<(ArchSpec, Disassembler), let (spec, dis) = apply_userop_map(spec, dis, "arm64"); Ok((spec, dis)) } + #[cfg(feature = "mips")] + "mips" | "mips32" | "mips32be" | "mipsbe" | "mipseb" => { + let spec = build_arch_spec( + sleigh_config::processor_mips::SLA_MIPS32BE, + sleigh_config::processor_mips::PSPEC_MIPS32, + "mips32be", + ) + .map_err(|e| e.to_string())?; + let dis = Disassembler::from_sla( + sleigh_config::processor_mips::SLA_MIPS32BE, + sleigh_config::processor_mips::PSPEC_MIPS32, + "mips32be", + ) + .map_err(|e| e.to_string())?; + let (spec, dis) = apply_userop_map(spec, dis, "mips32be"); + Ok((spec, dis)) + } + #[cfg(feature = "mips")] + "mipsel" | "mips32le" | "mips32el" => { + let spec = build_arch_spec( + sleigh_config::processor_mips::SLA_MIPS32LE, + sleigh_config::processor_mips::PSPEC_MIPS32, + "mips32le", + ) + .map_err(|e| e.to_string())?; + let dis = Disassembler::from_sla( + sleigh_config::processor_mips::SLA_MIPS32LE, + sleigh_config::processor_mips::PSPEC_MIPS32, + "mips32le", + ) + .map_err(|e| e.to_string())?; + let (spec, dis) = apply_userop_map(spec, dis, "mips32le"); + Ok((spec, dis)) + } + #[cfg(feature = "mips")] + "mips64" | "mips64be" => { + let spec = build_arch_spec( + sleigh_config::processor_mips::SLA_MIPS64BE, + sleigh_config::processor_mips::PSPEC_MIPS64, + "mips64be", + ) + .map_err(|e| e.to_string())?; + let dis = Disassembler::from_sla( + sleigh_config::processor_mips::SLA_MIPS64BE, + sleigh_config::processor_mips::PSPEC_MIPS64, + "mips64be", + ) + .map_err(|e| e.to_string())?; + let (spec, dis) = apply_userop_map(spec, dis, "mips64be"); + Ok((spec, dis)) + } + #[cfg(feature = "mips")] + "mips64el" | "mips64le" => { + let spec = build_arch_spec( + sleigh_config::processor_mips::SLA_MIPS64LE, + sleigh_config::processor_mips::PSPEC_MIPS64, + "mips64le", + ) + .map_err(|e| e.to_string())?; + let dis = Disassembler::from_sla( + sleigh_config::processor_mips::SLA_MIPS64LE, + sleigh_config::processor_mips::PSPEC_MIPS64, + "mips64le", + ) + .map_err(|e| e.to_string())?; + let (spec, dis) = apply_userop_map(spec, dis, "mips64le"); + Ok((spec, dis)) + } #[cfg(feature = "riscv")] "riscv64" | "rv64" | "rv64gc" => { let spec = build_arch_spec( @@ -2325,11 +2407,16 @@ fn create_disassembler_for_arch(arch: &str) -> Result<(ArchSpec, Disassembler), supported.extend(["x86-64", "x86"]); #[cfg(feature = "arm")] supported.extend(["arm", "arm64", "aarch64"]); + #[cfg(feature = "mips")] + supported.extend(["mips32be", "mips32le", "mips64be", "mips64le"]); #[cfg(feature = "riscv")] supported.extend(["riscv64", "riscv32"]); if supported.is_empty() { - Err("No architectures enabled; build with feature x86, arm, or riscv".to_string()) + Err( + "No architectures enabled; build with feature x86, arm, mips, or riscv" + .to_string(), + ) } else { Err(format!( "Unknown architecture '{}'. Supported: {}", @@ -2439,6 +2526,18 @@ fn decompile_semantic_artifact_fallback( .iter() .filter(|transfer| transfer.exact) .count(); + let likely_transfers = vm_step + .transfers + .iter() + .filter(|transfer| matches!(transfer.confidence(), r2sym::SemanticConfidence::Likely)) + .count(); + let heuristic_transfers = vm_step + .transfers + .iter() + .filter(|transfer| { + matches!(transfer.confidence(), r2sym::SemanticConfidence::Heuristic) + }) + .count(); let redispatch_transfers = vm_step .transfers .iter() @@ -2454,6 +2553,31 @@ fn decompile_semantic_artifact_fallback( .iter() .filter(|transfer| transfer.selector_update.is_some()) .count(); + let exit_guards = vm_step + .transfers + .iter() + .map(|transfer| transfer.exit_guards.len()) + .sum::(); + let residual_guards = vm_step + .transfers + .iter() + .filter(|transfer| transfer.residual_guards) + .count(); + let residual_memory = vm_step + .transfers + .iter() + .filter(|transfer| transfer.residual_memory_effects) + .count(); + let read_effects = vm_step + .handler_memory_read_effects + .values() + .map(Vec::len) + .sum::(); + let write_effects = vm_step + .handler_memory_write_effects + .values() + .map(Vec::len) + .sum::(); let total_reads: usize = vm_step.handler_memory_reads.values().copied().sum(); let total_writes: usize = vm_step.handler_memory_writes.values().copied().sum(); let handler_preview = vm_step @@ -2490,7 +2614,7 @@ fn decompile_semantic_artifact_fallback( .collect::>() .join("; "); return format!( - "/* r2dec semantic summary: vm_summary for {} ({kind} @ 0x{:x}, loop_header=0x{:x}, selector={}, targets={}, redispatch={}, exact_transfers={}, redispatch_transfers={}, returning_transfers={}, selector_updates={}, total_reads={}, total_writes={}, state_inputs=[{}], state_outputs=[{}], handlers={}) */", + "/* r2dec semantic summary: vm_summary for {} ({kind} @ 0x{:x}, loop_header=0x{:x}, selector={}, targets={}, redispatch={}, exact_transfers={}, likely_transfers={}, heuristic_transfers={}, redispatch_transfers={}, returning_transfers={}, selector_updates={}, exact_exit_guards={}, residual_guards={}, residual_memory={}, total_reads={}, total_writes={}, read_effects={}, write_effects={}, state_inputs=[{}], state_outputs=[{}], handlers={}) */", func_name, vm_step.dispatch_header, vm_step.loop_header, @@ -2498,11 +2622,18 @@ fn decompile_semantic_artifact_fallback( vm_step.dispatch_targets.len(), vm_step.redispatch_handlers.len(), exact_transfers, + likely_transfers, + heuristic_transfers, redispatch_transfers, returning_transfers, selector_updates, + exit_guards, + residual_guards, + residual_memory, total_reads, total_writes, + read_effects, + write_effects, inputs, outputs, handler_preview, @@ -2519,6 +2650,31 @@ fn decompile_semantic_artifact_fallback( reason.push_str(&info.residual_reasons.join(", ")); reason.push(')'); } + if info.branch_fact_count > 0 { + reason.push_str(&format!( + "; branch_facts={}, control_islands={}, actionable_conditions={}, exact_conditions={}", + info.branch_fact_count, + info.control_island_count, + info.actionable_compiled_condition_count, + info.exact_compiled_condition_count, + )); + } + let actionable_preview = compiled + .symbolic_facts + .control_islands + .iter() + .filter_map(|island| { + island + .actionable_compiled_condition() + .map(|condition| format!("0x{:x}: {}", island.anchor_block, condition.simplified)) + }) + .take(3) + .collect::>(); + if !actionable_preview.is_empty() { + reason.push_str("; actionable_preview=["); + reason.push_str(&actionable_preview.join(" | ")); + reason.push(']'); + } decompile_artifact_guard_fallback(func_name, &reason) } @@ -2535,22 +2691,24 @@ fn should_decompile_from_semantic_fallback( function_name: &str, compiled: &r2sym::CompiledSemanticArtifact, ) -> bool { - if compiled.capability.decompile_ready { - return false; - } if matches!(compiled.mode, r2sym::SemanticMode::VmSummary) { return true; } if !is_autogenerated_function_name(function_name) { return false; } - compiled.symbolic_facts.diagnostics.skipped_large_cfg - || compiled.residual_reasons.iter().any(|reason| { - matches!( - reason, - r2sym::ResidualReason::InterpreterRequiresStepSummary - ) - }) + if compiled.symbolic_facts.diagnostics.skipped_large_cfg { + return true; + } + if compiled.capability.decompile_ready { + return false; + } + compiled.residual_reasons.iter().any(|reason| { + matches!( + reason, + r2sym::ResidualReason::InterpreterRequiresStepSummary + ) + }) } #[cfg(test)] @@ -3414,6 +3572,8 @@ struct SymbolicBranchJson { #[derive(Debug, serde::Serialize)] struct SymbolicFactsJson { branches: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + control_islands: Vec, diagnostics: r2types::SymbolicFactDiagnostics, #[serde(skip_serializing_if = "Option::is_none")] interpreter: Option, @@ -3477,6 +3637,7 @@ fn symbolic_facts_json(facts: &r2types::SymbolicSemanticFacts) -> Option Symboli false_compiled: fact.false_compiled.clone(), }) .collect(), + control_islands: facts.control_islands.clone(), diagnostics: facts.diagnostics.clone(), interpreter: facts.interpreter.clone(), vm_step: facts.vm_step.clone(), @@ -3660,6 +3822,15 @@ fn semantic_type_fallback_payload( warning.push_str(&compiled_info.residual_reasons.join(", ")); warning.push(')'); } + if compiled_info.branch_fact_count > 0 { + warning.push_str(&format!( + "; branch_facts={}, control_islands={}, actionable_conditions={}, exact_conditions={}", + compiled_info.branch_fact_count, + compiled_info.control_island_count, + compiled_info.actionable_compiled_condition_count, + compiled_info.exact_compiled_condition_count, + )); + } warnings.push(warning); if !compiled_info.capability.type_ready { warnings.push("type analysis not ready from semantic capability".to_string()); @@ -7702,6 +7873,99 @@ mod tests { assert_eq!(decompiler_cfg_guard_reason_from_summary(&summary), None); } + fn test_compiled_condition(expr: &str) -> r2sym::BackwardConditionSummary { + r2sym::BackwardConditionSummary { + simplified: expr.to_string(), + terms: vec![expr.to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: r2sym::BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + } + } + + fn test_large_cfg_semantic_artifact() -> r2sym::CompiledSemanticArtifact { + let compiled = test_compiled_condition("x == 0"); + let control_fact = r2sym::SymbolicControlFact { + target: 0x401020, + status: r2sym::SymbolicReachabilityStatus::Reachable, + condition: Some("x == 0".to_string()), + compiled: Some(compiled.clone()), + evidence: r2sym::SemanticEvidence::exact(), + }; + let symbolic_facts = r2sym::SymbolicFunctionFacts { + branch_facts: vec![r2sym::SymbolicBranchFact { + block_addr: 0x401000, + true_target: 0x401020, + false_target: 0x401030, + true_status: r2sym::SymbolicReachabilityStatus::Reachable, + false_status: r2sym::SymbolicReachabilityStatus::Unreachable, + true_condition: Some("x == 0".to_string()), + false_condition: Some("x != 0".to_string()), + true_compiled: Some(compiled.clone()), + false_compiled: None, + }], + control_islands: vec![r2sym::SymbolicControlIsland { + kind: r2sym::SymbolicControlIslandKind::LargeCfgBranchFrontier, + anchor_block: 0x401000, + frontier_targets: vec![0x401020], + facts: vec![control_fact], + evidence: r2sym::SemanticEvidence::exact(), + }], + diagnostics: r2sym::SymbolicFunctionFactDiagnostics { + branches_evaluated: 1, + branches_pruned: 0, + branches_unknown: 0, + skipped_missing_arch: false, + skipped_large_cfg: true, + }, + }; + + r2sym::CompiledSemanticArtifact { + mode: r2sym::SemanticMode::Residual, + slice_class: r2sym::SliceClass::Worker, + capability: r2sym::SemanticCapability { + query_ready: true, + type_ready: true, + decompile_ready: true, + }, + residual_reasons: vec![r2sym::ResidualReason::LargeCfg], + closure_functions: 4, + helper_functions: 3, + derived_summaries: 0, + derived_diagnostics: r2sym::DerivedSummaryDiagnostics::default(), + symbolic_facts, + interpreter: None, + vm_step: None, + vm_transfer: None, + cache_hit: false, + } + } + + #[test] + fn decompile_ready_large_cfg_worker_still_prefers_semantic_fallback() { + let compiled = test_large_cfg_semantic_artifact(); + assert!(should_decompile_from_semantic_fallback( + "fcn.401000", + &compiled + )); + } + + #[test] + fn semantic_fallback_text_reports_actionable_control_island_counts() { + let compiled = test_large_cfg_semantic_artifact(); + let output = decompile_semantic_artifact_fallback("_401000", &compiled); + assert!(output.contains("semantic fallback: worker slice in residual mode")); + assert!(output.contains("branch_facts=1")); + assert!(output.contains("control_islands=1")); + assert!(output.contains("actionable_conditions=1")); + assert!(output.contains("exact_conditions=1")); + assert!(output.contains("actionable_preview=[0x401000: x == 0]")); + } + #[test] fn non_x86_strong_evidence_can_clear_signature_threshold() { let params = vec![ @@ -7908,7 +8172,7 @@ mod integration_tests { r2il_free(ctx_ptr); } - #[cfg(feature = "arm")] + #[cfg(any(feature = "arm", feature = "mips"))] fn profile_for_arch(arch: &str) -> String { let arch_cstr = CString::new(arch).unwrap(); let ctx_ptr = r2il_arch_init(arch_cstr.as_ptr()); @@ -7929,7 +8193,7 @@ mod integration_tests { profile } - #[cfg(feature = "arm")] + #[cfg(any(feature = "arm", feature = "mips"))] fn role_target(profile: &str, role: &str) -> Option { profile .lines() @@ -8042,6 +8306,47 @@ mod integration_tests { r2il_free(ctx_ptr); } + #[test] + #[cfg(feature = "mips")] + fn create_disassembler_for_arch_mips32be() { + let (spec, disasm) = + create_disassembler_for_arch("mips32be").expect("mips32be disassembler"); + assert_eq!(spec.name, "mips32be"); + assert!(spec.addr_size > 0); + assert_eq!(spec.instruction_endianness, r2il::Endianness::Big); + assert_eq!(spec.memory_endianness, r2il::Endianness::Big); + assert_eq!( + disasm.userop_name(0), + userop_map_for_arch("mips32be").get(&0).map(String::as_str) + ); + } + + #[test] + #[cfg(feature = "mips")] + fn r2il_arch_init_mips32be_loaded() { + let arch_cstr = CString::new("mips32be").unwrap(); + let ctx_ptr = r2il_arch_init(arch_cstr.as_ptr()); + assert!(!ctx_ptr.is_null(), "context pointer should not be null"); + assert_eq!( + r2il_is_loaded(ctx_ptr), + 1, + "mips32be context should be loaded" + ); + r2il_free(ctx_ptr); + } + + #[test] + #[cfg(feature = "mips")] + fn mips32be_reg_profile_includes_arg_roles() { + let profile = profile_for_arch("mips32be"); + for role in ["PC", "SP", "A0", "A1", "A2", "A3", "R0"] { + assert!( + role_target(&profile, role).is_some(), + "mips32be profile should define ={role}" + ); + } + } + #[test] fn score_global_links_prefers_stronger_struct_type_signal() { let block = r2ssa::SSABlock { diff --git a/r2plugin/src/types.rs b/r2plugin/src/types.rs index 13910ae..ff1330b 100644 --- a/r2plugin/src/types.rs +++ b/r2plugin/src/types.rs @@ -244,9 +244,129 @@ fn symbolic_memory_region_from_sym( } } +fn symbolic_confidence_from_sym( + confidence: r2sym::SemanticConfidence, +) -> r2types::SymbolicSemanticConfidence { + match confidence { + r2sym::SemanticConfidence::Exact => r2types::SymbolicSemanticConfidence::Exact, + r2sym::SemanticConfidence::Likely => r2types::SymbolicSemanticConfidence::Likely, + r2sym::SemanticConfidence::Heuristic => r2types::SymbolicSemanticConfidence::Heuristic, + r2sym::SemanticConfidence::Residual => r2types::SymbolicSemanticConfidence::Residual, + } +} + +fn symbolic_evidence_reason_from_sym( + reason: r2sym::SemanticEvidenceReason, +) -> r2types::SymbolicSemanticEvidenceReason { + match reason { + r2sym::SemanticEvidenceReason::LargeCfg => { + r2types::SymbolicSemanticEvidenceReason::LargeCfg + } + r2sym::SemanticEvidenceReason::SummaryBudget => { + r2types::SymbolicSemanticEvidenceReason::SummaryBudget + } + r2sym::SemanticEvidenceReason::AliasAmbiguity => { + r2types::SymbolicSemanticEvidenceReason::AliasAmbiguity + } + r2sym::SemanticEvidenceReason::ReplayOverlap => { + r2types::SymbolicSemanticEvidenceReason::ReplayOverlap + } + r2sym::SemanticEvidenceReason::HeapIdentityWeak => { + r2types::SymbolicSemanticEvidenceReason::HeapIdentityWeak + } + r2sym::SemanticEvidenceReason::GuardOpaque => { + r2types::SymbolicSemanticEvidenceReason::GuardOpaque + } + r2sym::SemanticEvidenceReason::ValueOpaque => { + r2types::SymbolicSemanticEvidenceReason::ValueOpaque + } + r2sym::SemanticEvidenceReason::TruncatedTransfer => { + r2types::SymbolicSemanticEvidenceReason::TruncatedTransfer + } + r2sym::SemanticEvidenceReason::DerivedFromRanking => { + r2types::SymbolicSemanticEvidenceReason::DerivedFromRanking + } + r2sym::SemanticEvidenceReason::PartialPathCoverage => { + r2types::SymbolicSemanticEvidenceReason::PartialPathCoverage + } + r2sym::SemanticEvidenceReason::ResidualSearchRequired => { + r2types::SymbolicSemanticEvidenceReason::ResidualSearchRequired + } + } +} + +fn symbolic_evidence_from_sym( + evidence: &r2sym::SemanticEvidence, +) -> r2types::SymbolicSemanticEvidence { + r2types::SymbolicSemanticEvidence { + tier: symbolic_confidence_from_sym(evidence.tier), + soundness: match evidence.soundness { + r2sym::SemanticEvidenceSoundness::Proven => { + r2types::SymbolicSemanticEvidenceSoundness::Proven + } + r2sym::SemanticEvidenceSoundness::OverApprox => { + r2types::SymbolicSemanticEvidenceSoundness::OverApprox + } + r2sym::SemanticEvidenceSoundness::Ranked => { + r2types::SymbolicSemanticEvidenceSoundness::Ranked + } + r2sym::SemanticEvidenceSoundness::Unknown => { + r2types::SymbolicSemanticEvidenceSoundness::Unknown + } + }, + coverage: match evidence.coverage { + r2sym::SemanticEvidenceCoverage::Full => { + r2types::SymbolicSemanticEvidenceCoverage::Full + } + r2sym::SemanticEvidenceCoverage::Partial => { + r2types::SymbolicSemanticEvidenceCoverage::Partial + } + r2sym::SemanticEvidenceCoverage::Bounded => { + r2types::SymbolicSemanticEvidenceCoverage::Bounded + } + }, + provenance: match evidence.provenance { + r2sym::SemanticEvidenceProvenance::Stable => { + r2types::SymbolicSemanticEvidenceProvenance::Stable + } + r2sym::SemanticEvidenceProvenance::Normalized => { + r2types::SymbolicSemanticEvidenceProvenance::Normalized + } + r2sym::SemanticEvidenceProvenance::Ranked => { + r2types::SymbolicSemanticEvidenceProvenance::Ranked + } + r2sym::SemanticEvidenceProvenance::Unstable => { + r2types::SymbolicSemanticEvidenceProvenance::Unstable + } + }, + ambiguity: match evidence.ambiguity { + r2sym::SemanticEvidenceAmbiguity::Single => { + r2types::SymbolicSemanticEvidenceAmbiguity::Single + } + r2sym::SemanticEvidenceAmbiguity::Bounded => { + r2types::SymbolicSemanticEvidenceAmbiguity::Bounded + } + r2sym::SemanticEvidenceAmbiguity::Ranked => { + r2types::SymbolicSemanticEvidenceAmbiguity::Ranked + } + r2sym::SemanticEvidenceAmbiguity::Multiple => { + r2types::SymbolicSemanticEvidenceAmbiguity::Multiple + } + }, + budget_limited: evidence.budget_limited, + reasons: evidence + .reasons + .iter() + .copied() + .map(symbolic_evidence_reason_from_sym) + .collect(), + } +} + fn symbolic_compiled_condition_from_sym( summary: &r2sym::BackwardConditionSummary, ) -> r2types::SymbolicCompiledCondition { + let evidence = summary.evidence(); r2types::SymbolicCompiledCondition { simplified: summary.simplified.clone(), terms: summary.terms.clone(), @@ -259,18 +379,71 @@ fn symbolic_compiled_condition_from_sym( offset_hi: term.offset_hi, size: term.size, exact_offset: term.exact_offset, + evidence: symbolic_evidence_from_sym(&term.evidence()), + confidence: symbolic_confidence_from_sym(term.confidence()), + binding: None, expr: term.expr.clone(), + value_expr: None, + exact_value: false, }) .collect(), backward_memory_substitutions: summary.backward_memory_substitutions, backward_memory_candidate_enumerations: summary.backward_memory_candidate_enumerations, backward_memory_residual_fallbacks: summary.backward_memory_residual_fallbacks, precision: symbolic_condition_precision_from_sym(summary.precision), + evidence: symbolic_evidence_from_sym(&evidence), + confidence: symbolic_confidence_from_sym(evidence.tier), supported_paths: summary.supported_paths, total_paths: summary.total_paths, } } +fn symbolic_control_island_kind_from_sym( + kind: r2sym::SymbolicControlIslandKind, +) -> r2types::SymbolicControlIslandKind { + match kind { + r2sym::SymbolicControlIslandKind::BranchFrontier => { + r2types::SymbolicControlIslandKind::BranchFrontier + } + r2sym::SymbolicControlIslandKind::LargeCfgBranchFrontier => { + r2types::SymbolicControlIslandKind::LargeCfgBranchFrontier + } + } +} + +fn symbolic_control_fact_from_sym( + fact: &r2sym::SymbolicControlFact, +) -> r2types::SymbolicControlFact { + r2types::SymbolicControlFact { + target: fact.target, + status: symbolic_reachability_status_from_sym(fact.status), + condition: fact.condition.clone(), + compiled: fact + .compiled + .as_ref() + .map(symbolic_compiled_condition_from_sym), + evidence: symbolic_evidence_from_sym(&fact.evidence), + confidence: symbolic_confidence_from_sym(fact.evidence.tier), + } +} + +fn symbolic_control_island_from_sym( + island: &r2sym::SymbolicControlIsland, +) -> r2types::SymbolicControlIsland { + r2types::SymbolicControlIsland { + kind: symbolic_control_island_kind_from_sym(island.kind), + anchor_block: island.anchor_block, + frontier_targets: island.frontier_targets.clone(), + facts: island + .facts + .iter() + .map(symbolic_control_fact_from_sym) + .collect(), + evidence: symbolic_evidence_from_sym(&island.evidence), + confidence: symbolic_confidence_from_sym(island.evidence.tier), + } +} + fn symbolic_interpreter_kind_from_sym( kind: r2sym::InterpreterKind, ) -> r2types::SymbolicInterpreterKind { @@ -327,22 +500,77 @@ pub(crate) fn symbolic_vm_value_expr_from_sym( fn symbolic_vm_state_update_from_sym( update: &r2sym::VmStateUpdate, ) -> r2types::SymbolicVmStateUpdate { + let evidence = update.evidence(); r2types::SymbolicVmStateUpdate { output: update.output.clone(), expr: update.expr.clone(), value: symbolic_vm_value_expr_from_sym(&update.value), exact: update.exact, + evidence: symbolic_evidence_from_sym(&evidence), + confidence: symbolic_confidence_from_sym(evidence.tier), + } +} + +fn symbolic_vm_guard_condition_from_sym( + guard: &r2sym::VmGuardCondition, +) -> r2types::SymbolicVmGuardCondition { + let evidence = guard.evidence(); + r2types::SymbolicVmGuardCondition { + expr: guard.expr.clone(), + value: symbolic_vm_value_expr_from_sym(&guard.value), + expect_nonzero: guard.expect_nonzero, + exact: guard.exact, + evidence: symbolic_evidence_from_sym(&evidence), + confidence: symbolic_confidence_from_sym(evidence.tier), + } +} + +fn symbolic_vm_guarded_exit_from_sym( + guarded: &r2sym::VmGuardedExit, +) -> r2types::SymbolicVmGuardedExit { + r2types::SymbolicVmGuardedExit { + target: guarded.target, + guard: symbolic_vm_guard_condition_from_sym(&guarded.guard), + } +} + +fn symbolic_vm_memory_condition_from_sym( + condition: &r2sym::VmMemoryCondition, +) -> r2types::SymbolicMemoryCondition { + let evidence = condition.evidence(); + r2types::SymbolicMemoryCondition { + region: r2types::SymbolicMemoryRegion::Region(r2types::SymbolicMemoryRegionRef { + id: condition.region.id, + kind: symbolic_memory_region_kind_from_sym(&condition.region.kind), + name: condition.region.name.clone(), + }), + offset_lo: condition.offset_lo, + offset_hi: condition.offset_hi, + size: condition.size, + exact_offset: condition.exact_offset, + evidence: symbolic_evidence_from_sym(&evidence), + confidence: symbolic_confidence_from_sym(evidence.tier), + binding: condition.binding.clone(), + expr: condition.expr.clone(), + value_expr: condition.value_expr.clone(), + exact_value: condition.exact_value, } } fn symbolic_vm_transfer_arm_from_sym( transfer: &r2sym::VmTransferArm, ) -> r2types::SymbolicVmTransferArm { + let evidence = transfer.evidence(); r2types::SymbolicVmTransferArm { handler_target: transfer.handler_target, case_values: transfer.case_values.clone(), region_blocks: transfer.region_blocks.clone(), exit_targets: transfer.exit_targets.clone(), + exit_guards: transfer + .exit_guards + .iter() + .map(symbolic_vm_guarded_exit_from_sym) + .collect(), state_updates: transfer .state_updates .iter() @@ -352,7 +580,21 @@ fn symbolic_vm_transfer_arm_from_sym( .selector_update .as_ref() .map(symbolic_vm_state_update_from_sym), + memory_reads: transfer + .memory_reads + .iter() + .map(symbolic_vm_memory_condition_from_sym) + .collect(), + memory_writes: transfer + .memory_writes + .iter() + .map(symbolic_vm_memory_condition_from_sym) + .collect(), + residual_guards: transfer.residual_guards, + residual_memory_effects: transfer.residual_memory_effects, exact: transfer.exact, + evidence: symbolic_evidence_from_sym(&evidence), + confidence: symbolic_confidence_from_sym(evidence.tier), redispatch: transfer.redispatch, may_return: transfer.may_return, truncated: transfer.truncated, @@ -390,6 +632,45 @@ fn symbolic_vm_step_summary_from_sym( ) }) .collect(), + handler_exit_guards: vm_step + .handler_exit_guards + .iter() + .map(|(target, guards)| { + ( + *target, + guards + .iter() + .map(symbolic_vm_guarded_exit_from_sym) + .collect(), + ) + }) + .collect(), + handler_memory_read_effects: vm_step + .handler_memory_read_effects + .iter() + .map(|(target, effects)| { + ( + *target, + effects + .iter() + .map(symbolic_vm_memory_condition_from_sym) + .collect(), + ) + }) + .collect(), + handler_memory_write_effects: vm_step + .handler_memory_write_effects + .iter() + .map(|(target, effects)| { + ( + *target, + effects + .iter() + .map(symbolic_vm_memory_condition_from_sym) + .collect(), + ) + }) + .collect(), handler_memory_reads: vm_step.handler_memory_reads.clone(), handler_memory_writes: vm_step.handler_memory_writes.clone(), handler_calls: vm_step.handler_calls.clone(), @@ -433,6 +714,11 @@ pub(crate) fn symbolic_semantic_facts_from_sym( .map(symbolic_compiled_condition_from_sym), }) .collect(), + control_islands: facts + .control_islands + .iter() + .map(symbolic_control_island_from_sym) + .collect(), diagnostics: r2types::SymbolicFactDiagnostics { branches_evaluated: facts.diagnostics.branches_evaluated, branches_pruned: facts.diagnostics.branches_pruned, @@ -1195,6 +1481,7 @@ pub(crate) fn build_function_analysis_artifact_from_analysis_with_semantic_artif ); let (struct_decls, slot_type_overrides, slot_field_profiles) = crate::merge_struct_inference_artifacts(raw_structs, semantic_structs); + let symbolic_facts = symbolic_semantic_facts_from_sym(&semantic_artifact); let reg_type_hints = if input.ctx.semantic_metadata_enabled { collect_register_type_hints(input.blocks.as_slice(), input.ctx.disasm) @@ -1208,6 +1495,13 @@ pub(crate) fn build_function_analysis_artifact_from_analysis_with_semantic_artif input.ctx.semantic_metadata_enabled, ); let recovered_vars = vars.iter().map(var_prot_to_writeback).collect::>(); + let mut local_structs = + local_struct_artifacts_to_writeback(struct_decls, slot_type_overrides, slot_field_profiles); + r2types::augment_local_struct_artifacts_with_symbolic_facts( + &mut local_structs, + &symbolic_facts, + ptr_bits, + ); let writeback = r2types::build_type_writeback_analysis(r2types::TypeWritebackAnalysisInput { function_name: &input.function_name, ptr_bits, @@ -1215,15 +1509,10 @@ pub(crate) fn build_function_analysis_artifact_from_analysis_with_semantic_artif recovered_vars: &recovered_vars, ssa_blocks: &pattern_ssa_blocks, parsed_context, - local_structs: local_struct_artifacts_to_writeback( - struct_decls, - slot_type_overrides, - slot_field_profiles, - ), + local_structs, interproc_summary_set: interproc_summary_set.clone(), diagnostics: writeback_diagnostics_from_plugin(diagnostics), }); - let symbolic_facts = symbolic_semantic_facts_from_sym(&semantic_artifact); let mut type_facts = writeback.type_facts; if symbolic_facts.diagnostics.branches_pruned > 0 { type_facts.diagnostics.push(format!( @@ -1576,6 +1865,10 @@ pub(crate) fn build_detached_function_analysis_artifact_with_scope_and_semantics ); let (struct_decls, slot_type_overrides, slot_field_profiles) = crate::merge_struct_inference_artifacts(raw_structs, semantic_structs); + let semantic_artifact = precomputed_semantic_artifact.unwrap_or_else(|| { + collect_plugin_semantic_artifact(&analysis.ssa_func, arch, symbolic_scope) + }); + let symbolic_facts = symbolic_semantic_facts_from_sym(&semantic_artifact); let vars = recover_vars_from_ssa( &pattern_ssa_blocks, arch, @@ -1583,6 +1876,13 @@ pub(crate) fn build_detached_function_analysis_artifact_with_scope_and_semantics semantic_metadata_enabled, ); let recovered_vars = vars.iter().map(var_prot_to_writeback).collect::>(); + let mut local_structs = + local_struct_artifacts_to_writeback(struct_decls, slot_type_overrides, slot_field_profiles); + r2types::augment_local_struct_artifacts_with_symbolic_facts( + &mut local_structs, + &symbolic_facts, + ptr_bits, + ); let writeback = r2types::build_type_writeback_analysis(r2types::TypeWritebackAnalysisInput { function_name, ptr_bits, @@ -1590,18 +1890,10 @@ pub(crate) fn build_detached_function_analysis_artifact_with_scope_and_semantics recovered_vars: &recovered_vars, ssa_blocks: &pattern_ssa_blocks, parsed_context, - local_structs: local_struct_artifacts_to_writeback( - struct_decls, - slot_type_overrides, - slot_field_profiles, - ), + local_structs, interproc_summary_set: None, diagnostics: writeback_diagnostics_from_plugin(diagnostics), }); - let semantic_artifact = precomputed_semantic_artifact.unwrap_or_else(|| { - collect_plugin_semantic_artifact(&analysis.ssa_func, arch, symbolic_scope) - }); - let symbolic_facts = symbolic_semantic_facts_from_sym(&semantic_artifact); let mut type_facts = writeback.type_facts; if symbolic_facts.diagnostics.branches_pruned > 0 { type_facts.diagnostics.push(format!( diff --git a/tests/r2r/db/extras/r2sleigh_integration_extended b/tests/r2r/db/extras/r2sleigh_integration_extended index cea905b..9baef9b 100644 --- a/tests/r2r/db/extras/r2sleigh_integration_extended +++ b/tests/r2r/db/extras/r2sleigh_integration_extended @@ -281,7 +281,7 @@ true EOF_EXPECT CMDS=<= 8) and ((.semantic.vm_transfer.transfers | map(select(.redispatch)) | length) >= 4)' +a:sla.sym sym.interpret_bytecode | jq -c '.semantic.mode=="vm_summary" and .semantic.capability.query_ready==true and .semantic.vm_transfer != null and ((.semantic.vm_transfer.transfers | length) >= 8) and ((.semantic.vm_transfer.transfers | map(select(.redispatch)) | length) >= 4) and (((.semantic.vm_transfer.handler_exit_guards // {}) | keys | length) >= 1) and (((.semantic.vm_transfer.handler_memory_read_effects // {}) | keys | length) >= 1) and (((.semantic.vm_transfer.handler_memory_write_effects // {}) | keys | length) >= 1)' EOF_CMDS RUN @@ -305,7 +305,7 @@ true EOF_EXPECT CMDS=<=8 and (.symbolic.vm_transfer.loop_latches|length)>=7 and (.symbolic.vm_transfer.step_blocks|length)>=10 and (.symbolic.vm_transfer.handler_state_updates | to_entries | length) >= 5 and (.symbolic.vm_transfer.transfers | type=="array")' +a:sla.types sym.tiny_vm_dispatch | jq -c '.symbolic.diagnostics.semantic_mode=="VmSummary" and .symbolic.vm_transfer != null and (.symbolic.vm_transfer.dispatch_targets|length)>=8 and (.symbolic.vm_transfer.loop_latches|length)>=7 and (.symbolic.vm_transfer.step_blocks|length)>=10 and (.symbolic.vm_transfer.handler_state_updates | to_entries | length) >= 5 and (.symbolic.vm_transfer.transfers | type=="array") and (((.symbolic.vm_transfer.handler_exit_guards // {}) | keys | length) >= 1) and (((.symbolic.vm_transfer.handler_memory_read_effects // {}) | keys | length) >= 1) and (((.symbolic.vm_transfer.handler_memory_write_effects // {}) | keys | length) >= 1) and (((.symbolic.vm_transfer.transfers // []) | map((.exit_guards // []) | length) | add) > 0) and (((.symbolic.vm_transfer.transfers // []) | map((.memory_reads // []) | length) | add) > 0) and (((.symbolic.vm_transfer.transfers // []) | map((.memory_writes // []) | length) | add) > 0) and (((.symbolic.vm_transfer.transfers // []) | map(((.memory_reads // []) + (.memory_writes // [])) | map(select(.binding != null)) | length) | add) > 0) and (.symbolic.vm_transfer.transfers[0] | has("residual_guards") and has("residual_memory_effects"))' EOF_CMDS RUN @@ -330,7 +330,7 @@ true EOF_EXPECT CMDS=<=8 and (.semantic.vm_transfer.loop_latches|length)>=7 and (.semantic.vm_transfer.step_blocks|length)>=10 and (.semantic.vm_transfer.transfers | type=="array")' +a:sla.sym sym.tiny_vm_dispatch | jq -c '.semantic.mode=="vm_summary" and .semantic.vm_transfer != null and (.semantic.vm_transfer.dispatch_targets|length)>=8 and (.semantic.vm_transfer.loop_latches|length)>=7 and (.semantic.vm_transfer.step_blocks|length)>=10 and (.semantic.vm_transfer.transfers | type=="array") and (((.semantic.vm_transfer.handler_exit_guards // {}) | keys | length) >= 1) and (((.semantic.vm_transfer.handler_memory_read_effects // {}) | keys | length) >= 1) and (((.semantic.vm_transfer.handler_memory_write_effects // {}) | keys | length) >= 1) and (((.semantic.vm_transfer.transfers // []) | map((.exit_guards // []) | length) | add) > 0) and (((.semantic.vm_transfer.transfers // []) | map((.memory_reads // []) | length) | add) > 0) and (((.semantic.vm_transfer.transfers // []) | map((.memory_writes // []) | length) | add) > 0) and (((.semantic.vm_transfer.transfers // []) | map(((.memory_reads // []) + (.memory_writes // [])) | map(select(.binding != null)) | length) | add) > 0) and (.semantic.vm_transfer.transfers[0] | has("residual_guards") and has("residual_memory_effects"))' EOF_CMDS RUN @@ -366,7 +366,7 @@ true EOF_EXPECT CMDS=</dev/null +a:sla.sym sym.test_symbolic_xor_guard | jq -c '.semantic.branch_fact_count == 1 and (.semantic.control_island_count >= 1) and (.semantic.compiled_condition_count >= 1) and (.semantic.actionable_compiled_condition_count >= 1)' +EOF_CMDS +RUN + NAME=dec_symbolic_xor_guard_prunes_dead_branch FILE=bins/vuln_test ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true From b1e470d11633f4162b23f76207ab1373cfa9b8f1 Mon Sep 17 00:00:00 2001 From: Priyanshu Kumar Date: Tue, 31 Mar 2026 18:27:41 +0000 Subject: [PATCH 06/10] Enhance symbolic execution framework with new features and improvements --- Cargo.lock | 1 + crates/r2dec/src/fold/flags.rs | 38 + crates/r2dec/src/fold/tests/flags.rs | 61 + crates/r2dec/src/lib.rs | 1107 +++++++- crates/r2dec/src/structure.rs | 608 ++++- crates/r2sym/src/backward.rs | 247 +- crates/r2sym/src/lib.rs | 6 +- crates/r2sym/src/query.rs | 895 +++++- crates/r2sym/src/semantics/artifact.rs | 45 +- crates/r2sym/src/semantics/compiler.rs | 231 +- crates/r2sym/src/semantics/facts.rs | 1225 ++++++++- crates/r2sym/src/semantics/mod.rs | 4 +- crates/r2sym/src/semantics/vm.rs | 5 + crates/r2sym/src/sim.rs | 95 + crates/r2types/Cargo.toml | 1 + crates/r2types/src/facts.rs | 723 ++++- crates/r2types/src/from_sym.rs | 666 +++++ crates/r2types/src/lib.rs | 45 +- crates/r2types/src/prepare.rs | 1250 +++++++++ crates/r2types/src/signature_infer.rs | 1014 +++++++ crates/r2types/src/writeback.rs | 1172 +++++++- r2plugin/r_anal_sleigh.c | 269 +- r2plugin/src/analysis/sym.rs | 96 +- r2plugin/src/decompiler.rs | 129 +- r2plugin/src/helpers.rs | 66 - r2plugin/src/lib.rs | 1228 +++------ r2plugin/src/types.rs | 2417 ++--------------- .../db/extras/r2sleigh_integration_extended | 26 +- 28 files changed, 10061 insertions(+), 3609 deletions(-) create mode 100644 crates/r2types/src/from_sym.rs create mode 100644 crates/r2types/src/prepare.rs create mode 100644 crates/r2types/src/signature_infer.rs diff --git a/Cargo.lock b/Cargo.lock index bb1e1f7..b1e27db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1415,6 +1415,7 @@ version = "0.1.0" dependencies = [ "r2il", "r2ssa", + "r2sym", "serde", "serde_json", "thiserror 2.0.17", diff --git a/crates/r2dec/src/fold/flags.rs b/crates/r2dec/src/fold/flags.rs index 3e67813..3a735c9 100644 --- a/crates/r2dec/src/fold/flags.rs +++ b/crates/r2dec/src/fold/flags.rs @@ -461,6 +461,40 @@ impl<'a> FoldingContext<'a> { .map(|expr| self.finalize_condition_expr(expr)) } + fn symbolic_actionable_memory_condition_expr(&self, block_addr: u64) -> Option { + fn memory_term_rank(term: &r2types::SymbolicMemoryCondition) -> (u8, bool, bool, i64, i64) { + let evidence_rank = match term.evidence.tier { + r2types::SymbolicSemanticConfidence::Exact => 3, + r2types::SymbolicSemanticConfidence::Likely => 2, + r2types::SymbolicSemanticConfidence::Heuristic => 1, + r2types::SymbolicSemanticConfidence::Residual => 0, + }; + ( + evidence_rank, + term.exact_value, + term.exact_offset, + -(term.offset_hi - term.offset_lo), + -term.offset_lo.abs(), + ) + } + + let term = self + .inputs + .symbolic_facts + .actionable_memory_terms_for_block(block_addr) + .into_iter() + .filter(|term| { + term.value_expr + .as_ref() + .is_some_and(|value| value != &term.expr) + }) + .max_by_key(|term| memory_term_rank(term))?; + let condition = format!("({} == {})", term.expr.trim(), term.value_expr.as_deref()?); + SymbolicConditionExprParser::new(self, &condition) + .and_then(SymbolicConditionExprParser::parse) + .map(|expr| self.finalize_condition_expr(expr)) + } + fn prepared_predicate_view(&self) -> Option> { self.prepared_semantic_view().map(Cow::Borrowed) } @@ -670,6 +704,10 @@ impl<'a> FoldingContext<'a> { return Some(cond); } + if let Some(cond) = self.symbolic_actionable_memory_condition_expr(block.addr) { + return Some(cond); + } + if let Some(cond) = self.symbolic_branch_condition_expr(block.addr) { return Some(cond); } diff --git a/crates/r2dec/src/fold/tests/flags.rs b/crates/r2dec/src/fold/tests/flags.rs index d3588c1..955082b 100644 --- a/crates/r2dec/src/fold/tests/flags.rs +++ b/crates/r2dec/src/fold/tests/flags.rs @@ -369,3 +369,64 @@ fn actionable_control_island_parses_non_literal_compiled_condition_expr() { )) ); } + +#[test] +fn actionable_memory_island_parses_memory_condition_expr() { + let arch = make_test_arch_x86_64(); + let mut entry = R2ILBlock::new(0x4000, 4); + entry.push(R2ILOp::IntNotEqual { + dst: Varnode::unique(1, 1), + a: Varnode::register(0x10, 4), + b: Varnode::constant(0, 4), + }); + entry.push(R2ILOp::CBranch { + target: Varnode::constant(0x4008, 8), + cond: Varnode::unique(1, 1), + }); + let mut fallthrough = R2ILBlock::new(0x4004, 4); + fallthrough.push(R2ILOp::Return { + target: Varnode::constant(0, 8), + }); + let mut taken = R2ILBlock::new(0x4008, 4); + taken.push(R2ILOp::Return { + target: Varnode::constant(1, 8), + }); + + let prepared = + prepared_from_r2il_blocks(&[entry, fallthrough, taken], &arch).with_name("memory_island"); + let mut ctx = make_x86_64_ctx_with_prepared(&prepared); + let mut facts = SymbolicSemanticFacts::default(); + facts.memory_islands.push(r2types::SymbolicMemoryIsland { + kind: r2types::SymbolicMemoryIslandKind::LargeCfgConditionFrontier, + anchor_block: 0x4000, + terms: vec![r2types::SymbolicMemoryCondition { + region: r2types::SymbolicMemoryRegion::Argument { index: 0 }, + offset_lo: 8, + offset_hi: 8, + size: 4, + exact_offset: true, + evidence: r2types::SymbolicSemanticEvidence::exact(), + confidence: r2types::SymbolicSemanticConfidence::Exact, + binding: None, + expr: "*(arg0 + 0x8)".to_string(), + value_expr: Some("0x2a".to_string()), + exact_value: true, + }], + evidence: r2types::SymbolicSemanticEvidence::exact(), + confidence: r2types::SymbolicSemanticConfidence::Exact, + }); + ctx.inputs.symbolic_facts = Box::leak(Box::new(facts)); + + assert_eq!( + ctx.extract_condition_from_block(prepared.function().get_block(0x4000).expect("entry")), + Some(CExpr::binary( + BinaryOp::Eq, + CExpr::deref(CExpr::binary( + BinaryOp::Add, + CExpr::Var("arg0".to_string()), + CExpr::IntLit(0x8), + )), + CExpr::IntLit(0x2a), + )) + ); +} diff --git a/crates/r2dec/src/lib.rs b/crates/r2dec/src/lib.rs index 12a0220..de60f51 100644 --- a/crates/r2dec/src/lib.rs +++ b/crates/r2dec/src/lib.rs @@ -49,6 +49,7 @@ pub use variable::VariableRecovery; use crate::fold::FoldingContext; use crate::fold::context::{FoldArchConfig, FoldInputs}; +use r2il::R2ILBlock; use r2ssa::SSAFunction; use r2ssa::SSAOp; use r2types::{ @@ -84,7 +85,27 @@ fn normalize_callee_name(name: &str) -> String { out } -fn should_skip_runtime_type_inference(prepared: Option<&r2ssa::SsaArtifact>) -> bool { +fn prefer_symbolic_large_worker_decompile(type_facts: &FunctionTypeFacts) -> bool { + let symbolic = &type_facts.symbolic_facts; + (symbolic.decompile_ready() || symbolic.structured_decompile_ready()) + && symbolic.diagnostics.skipped_large_cfg + && matches!( + symbolic.semantic_mode(), + Some( + r2types::SymbolicSemanticMode::Residual + | r2types::SymbolicSemanticMode::IslandCompiled + ) + ) + && !symbolic.worker_islands.is_empty() +} + +fn should_skip_runtime_type_inference( + prepared: Option<&r2ssa::SsaArtifact>, + type_facts: &FunctionTypeFacts, +) -> bool { + if prefer_symbolic_large_worker_decompile(type_facts) { + return true; + } let Some(prepared) = prepared else { return false; }; @@ -95,8 +116,11 @@ fn should_skip_runtime_type_inference(prepared: Option<&r2ssa::SsaArtifact>) -> && summary.back_edge_count == 0 } -fn should_use_prepared_semantic_view(prepared: Option<&r2ssa::SsaArtifact>) -> bool { - prepared.is_some() +fn should_use_prepared_semantic_view( + prepared: Option<&r2ssa::SsaArtifact>, + type_facts: &FunctionTypeFacts, +) -> bool { + prepared.is_some() && !prefer_symbolic_large_worker_decompile(type_facts) } fn seed_runtime_type_hints_from_facts_and_recovery( @@ -258,6 +282,466 @@ fn format_vm_summary_kind(kind: SymbolicInterpreterKind) -> &'static str { } } +fn is_autogenerated_function_name(name: &str) -> bool { + let underscore_hex_addr = name + .strip_prefix('_') + .is_some_and(|rest| !rest.is_empty() && rest.chars().all(|ch| ch.is_ascii_hexdigit())); + name.is_empty() + || name.starts_with("fcn.") + || name.starts_with("fcn_") + || name.starts_with("sub.") + || name.starts_with("sub_") + || name.starts_with("loc.") + || underscore_hex_addr +} + +fn semantic_mode_label(mode: r2types::SymbolicSemanticMode) -> &'static str { + match mode { + r2types::SymbolicSemanticMode::Raw => "raw", + r2types::SymbolicSemanticMode::Compiled => "compiled", + r2types::SymbolicSemanticMode::IslandCompiled => "island_compiled", + r2types::SymbolicSemanticMode::Residual => "residual", + r2types::SymbolicSemanticMode::VmSummary => "vm_summary", + } +} + +fn semantic_slice_class_label(slice_class: r2types::SymbolicSemanticSliceClass) -> &'static str { + match slice_class { + r2types::SymbolicSemanticSliceClass::Wrapper => "wrapper", + r2types::SymbolicSemanticSliceClass::Worker => "worker", + r2types::SymbolicSemanticSliceClass::RecursiveGroup => "recursive_group", + r2types::SymbolicSemanticSliceClass::InterpreterSwitch => "interpreter_switch", + r2types::SymbolicSemanticSliceClass::InterpreterIndirect => "interpreter_indirect", + r2types::SymbolicSemanticSliceClass::GenericLarge => "generic_large", + } +} + +fn semantic_residual_reason_label(reason: r2types::SymbolicSemanticResidualReason) -> &'static str { + match reason { + r2types::SymbolicSemanticResidualReason::MissingArch => "missing_arch", + r2types::SymbolicSemanticResidualReason::LargeCfg => "large_cfg", + r2types::SymbolicSemanticResidualReason::SummaryBudgetExhausted => { + "summary_budget_exhausted" + } + r2types::SymbolicSemanticResidualReason::SccBudgetExhausted => "scc_budget_exhausted", + r2types::SymbolicSemanticResidualReason::InterpreterRequiresStepSummary => { + "interpreter_requires_step_summary" + } + } +} + +fn render_vm_semantic_fallback_comment( + func_name: &str, + symbolic_facts: &r2types::SymbolicSemanticFacts, +) -> Option { + if !matches!( + symbolic_facts.diagnostics.semantic_mode, + Some(r2types::SymbolicSemanticMode::VmSummary) + ) { + return None; + } + let vm_step = symbolic_facts + .vm_step + .as_ref() + .or(symbolic_facts.vm_transfer.as_ref())?; + let kind = match vm_step.kind { + r2types::SymbolicInterpreterKind::SwitchDispatch => "switch_dispatch", + r2types::SymbolicInterpreterKind::IndirectDispatch => "indirect_dispatch", + }; + let selector = vm_step.selector.as_deref().unwrap_or("unknown"); + let inputs = if vm_step.state_inputs.is_empty() { + "none".to_string() + } else { + vm_step.state_inputs.join(", ") + }; + let outputs = if vm_step.state_outputs.is_empty() { + "none".to_string() + } else { + vm_step.state_outputs.join(", ") + }; + let exact_transfers = vm_step + .transfers + .iter() + .filter(|transfer| transfer.exact) + .count(); + let likely_transfers = vm_step + .transfers + .iter() + .filter(|transfer| { + matches!( + transfer.confidence, + r2types::SymbolicSemanticConfidence::Likely + ) + }) + .count(); + let heuristic_transfers = vm_step + .transfers + .iter() + .filter(|transfer| { + matches!( + transfer.confidence, + r2types::SymbolicSemanticConfidence::Heuristic + ) + }) + .count(); + let redispatch_transfers = vm_step + .transfers + .iter() + .filter(|transfer| transfer.redispatch) + .count(); + let returning_transfers = vm_step + .transfers + .iter() + .filter(|transfer| transfer.may_return) + .count(); + let selector_updates = vm_step + .transfers + .iter() + .filter(|transfer| transfer.selector_update.is_some()) + .count(); + let exit_guards = vm_step + .transfers + .iter() + .map(|transfer| transfer.exit_guards.len()) + .sum::(); + let residual_guards = vm_step + .transfers + .iter() + .filter(|transfer| transfer.residual_guards) + .count(); + let residual_memory = vm_step + .transfers + .iter() + .filter(|transfer| transfer.residual_memory_effects) + .count(); + let read_effects = vm_step + .handler_memory_read_effects + .values() + .map(Vec::len) + .sum::(); + let write_effects = vm_step + .handler_memory_write_effects + .values() + .map(Vec::len) + .sum::(); + let total_reads: usize = vm_step.handler_memory_reads.values().copied().sum(); + let total_writes: usize = vm_step.handler_memory_writes.values().copied().sum(); + let handler_preview = vm_step + .dispatch_targets + .iter() + .take(3) + .map(|target| { + let values = vm_step + .case_values_by_target + .get(target) + .map(|values| { + values + .iter() + .map(|value| format!("0x{value:x}")) + .collect::>() + .join("|") + }) + .unwrap_or_else(|| "default".to_string()); + let updates = vm_step + .handler_state_updates + .get(target) + .map(|updates| { + updates + .iter() + .take(3) + .map(|update| format!("{}={}", update.output, update.expr)) + .collect::>() + .join(", ") + }) + .filter(|text| !text.is_empty()) + .unwrap_or_else(|| "no_state_updates".to_string()); + format!("0x{target:x}[{values}] => {updates}") + }) + .collect::>() + .join("; "); + Some(format!( + "/* r2dec semantic summary: vm_summary for {} ({kind} @ 0x{:x}, loop_header=0x{:x}, selector={}, targets={}, redispatch={}, exact_transfers={}, likely_transfers={}, heuristic_transfers={}, redispatch_transfers={}, returning_transfers={}, selector_updates={}, exact_exit_guards={}, residual_guards={}, residual_memory={}, total_reads={}, total_writes={}, read_effects={}, write_effects={}, state_inputs=[{}], state_outputs=[{}], handlers={}) */", + func_name, + vm_step.dispatch_header, + vm_step.loop_header, + selector, + vm_step.dispatch_targets.len(), + vm_step.redispatch_handlers.len(), + exact_transfers, + likely_transfers, + heuristic_transfers, + redispatch_transfers, + returning_transfers, + selector_updates, + exit_guards, + residual_guards, + residual_memory, + total_reads, + total_writes, + read_effects, + write_effects, + inputs, + outputs, + handler_preview, + )) +} + +pub fn semantic_fallback_comment( + func_name: &str, + symbolic_facts: &r2types::SymbolicSemanticFacts, +) -> Option { + if let Some(comment) = render_vm_semantic_fallback_comment(func_name, symbolic_facts) { + return Some(comment); + } + let mode = symbolic_facts.diagnostics.semantic_mode?; + let slice_class = symbolic_facts.diagnostics.slice_class?; + let mut reason = format!( + "semantic fallback: {} slice in {} mode", + semantic_slice_class_label(slice_class), + semantic_mode_label(mode) + ); + if !symbolic_facts.diagnostics.residual_reasons.is_empty() { + reason.push_str(" ("); + reason.push_str( + &symbolic_facts + .diagnostics + .residual_reasons + .iter() + .map(|reason| semantic_residual_reason_label(*reason)) + .collect::>() + .join(", "), + ); + reason.push(')'); + } + + let control_facts = if symbolic_facts.worker_islands.is_empty() { + symbolic_facts + .control_islands + .iter() + .flat_map(|island| island.facts.iter()) + .collect::>() + } else { + symbolic_facts + .worker_islands + .iter() + .flat_map(|island| island.control_facts.iter()) + .collect::>() + }; + if !symbolic_facts.branch_facts.is_empty() + || !symbolic_facts.worker_islands.is_empty() + || !symbolic_facts.memory_islands.is_empty() + { + reason.push_str(&format!( + "; branch_facts={}, worker_islands={}, control_islands={}, memory_islands={}, actionable_conditions={}, exact_conditions={}", + symbolic_facts.branch_facts.len(), + symbolic_facts.worker_islands.len(), + symbolic_facts.control_islands.len(), + symbolic_facts.memory_islands.len(), + control_facts + .iter() + .filter(|fact| fact.evidence.allows_narrowing()) + .count(), + control_facts + .iter() + .filter(|fact| fact.evidence.allows_hard_proof()) + .count(), + )); + } + let actionable_preview = if symbolic_facts.worker_islands.is_empty() { + symbolic_facts + .control_islands + .iter() + .filter_map(|island| { + island.actionable_compiled_condition().map(|condition| { + format!("0x{:x}: {}", island.anchor_block, condition.simplified) + }) + }) + .take(3) + .collect::>() + } else { + symbolic_facts + .worker_islands + .iter() + .filter_map(|island| { + island.actionable_compiled_condition().map(|condition| { + format!("0x{:x}: {}", island.anchor_block, condition.simplified) + }) + }) + .take(3) + .collect::>() + }; + if !actionable_preview.is_empty() { + reason.push_str("; actionable_preview=["); + reason.push_str(&actionable_preview.join(" | ")); + reason.push(']'); + } + Some(format!( + "/* r2dec fallback: skipped decompilation for {} ({}) */", + func_name, reason + )) +} + +pub fn preferred_semantic_fallback_comment( + func_name: &str, + symbolic_facts: &r2types::SymbolicSemanticFacts, +) -> Option { + if matches!( + symbolic_facts.diagnostics.semantic_mode, + Some(r2types::SymbolicSemanticMode::VmSummary) + ) { + return semantic_fallback_comment(func_name, symbolic_facts); + } + if !is_autogenerated_function_name(func_name) { + return None; + } + if symbolic_facts.decompile_ready() { + return None; + } + if symbolic_facts.diagnostics.skipped_large_cfg { + return semantic_fallback_comment(func_name, symbolic_facts); + } + symbolic_facts + .diagnostics + .residual_reasons + .contains(&r2types::SymbolicSemanticResidualReason::InterpreterRequiresStepSummary) + .then(|| semantic_fallback_comment(func_name, symbolic_facts)) + .flatten() +} + +fn preferred_semantic_linearization_reason( + func_name: &str, + symbolic_facts: &r2types::SymbolicSemanticFacts, + cfg_summary: &r2ssa::CFGRiskSummary, +) -> Option { + if !is_autogenerated_function_name(func_name) { + return None; + } + if !symbolic_facts.decompile_ready() || !symbolic_facts.diagnostics.skipped_large_cfg { + return None; + } + if symbolic_facts.structured_decompile_ready() { + return None; + } + if symbolic_facts.worker_islands.is_empty() + && symbolic_facts.control_islands.is_empty() + && symbolic_facts.memory_islands.is_empty() + { + return None; + } + Some(preferred_semantic_worker_reason(cfg_summary)) +} + +fn preferred_semantic_structuring_reason( + func_name: &str, + symbolic_facts: &r2types::SymbolicSemanticFacts, + cfg_summary: &r2ssa::CFGRiskSummary, +) -> Option { + if !is_autogenerated_function_name(func_name) { + return None; + } + if !symbolic_facts.structured_decompile_ready() || !symbolic_facts.diagnostics.skipped_large_cfg + { + return None; + } + Some(preferred_semantic_worker_reason(cfg_summary)) +} + +fn preferred_semantic_worker_reason(cfg_summary: &r2ssa::CFGRiskSummary) -> String { + cfg_guard_reason_from_summary(cfg_summary) + .unwrap_or_else(|| "semantic worker islands".to_string()) +} + +pub fn cfg_guard_reason_from_summary(summary: &r2ssa::CFGRiskSummary) -> Option { + if summary.loop_count > 8 || summary.back_edge_count > 16 { + return Some(format!( + "complex loop graph (loops={}, back_edges={})", + summary.loop_count, summary.back_edge_count + )); + } + + if summary.loop_count > 4 && summary.block_count >= 96 && summary.max_switch_cases >= 32 { + return Some(format!( + "large dense switch in looped CFG (blocks={}, loops={}, max_switch_cases={})", + summary.block_count, summary.loop_count, summary.max_switch_cases + )); + } + + None +} + +pub fn cfg_guard_reason(blocks: &[R2ILBlock]) -> Option { + let ssa_func = SSAFunction::from_blocks_raw_no_arch(blocks)?; + cfg_guard_reason_from_summary(&ssa_func.cfg_risk_summary()) +} + +pub fn block_guard_fallback_comment(func_name: &str, blocks: usize, max_blocks: usize) -> String { + format!( + "/* r2dec fallback: skipped decompilation for {} ({} blocks > limit {}). Set SLEIGH_DEC_MAX_BLOCKS to override. */", + func_name, blocks, max_blocks + ) +} + +pub fn artifact_guard_fallback_comment(func_name: &str, reason: &str) -> String { + format!( + "/* r2dec fallback: skipped decompilation for {} ({}) */", + func_name, reason + ) +} + +pub fn render_semantic_worker_linearization( + plan: &r2types::TypeWritebackPlan, + symbolic_facts: &r2types::SymbolicSemanticFacts, + reason: &str, +) -> String { + let mut out = String::new(); + for decl in &plan.struct_decls { + let _ = writeln!(&mut out, "{}", decl.decl); + } + if !plan.struct_decls.is_empty() { + out.push('\n'); + } + let _ = writeln!(&mut out, "{} {{", plan.signature.signature); + let _ = writeln!( + &mut out, + " /* r2dec semantic worker linearization: {} */", + reason + ); + for warning in plan.diagnostics.warnings.iter().take(2) { + let _ = writeln!(&mut out, " /* {} */", warning); + } + + let mut emitted_any = false; + for island in symbolic_facts.worker_islands.iter().take(6) { + if let (Some(target), Some(condition)) = ( + island.actionable_reachable_target(), + island.actionable_compiled_condition(), + ) { + let _ = writeln!( + &mut out, + " /* 0x{:x}: if ({}) => 0x{:x} [{:?}] */", + island.anchor_block, condition.simplified, target, condition.evidence.tier + ); + emitted_any = true; + } + for term in island.actionable_terms().into_iter().take(2) { + let _ = writeln!( + &mut out, + " /* 0x{:x}: {} [{:?}] */", + island.anchor_block, term.expr, term.evidence.tier + ); + emitted_any = true; + } + } + if !emitted_any { + let _ = writeln!( + &mut out, + " /* no actionable worker-island statements recovered */" + ); + } + let _ = writeln!(&mut out, "}}"); + out +} + fn merge_params_with_external_signature( recovered_params: Vec, signature: Option<&FunctionSignatureSpec>, @@ -365,6 +849,18 @@ fn register_alias_names(reg_name: &str) -> Vec { vec![lower] } +pub fn normalize_sig_arch_name(arch: Option<&r2il::ArchSpec>) -> Option { + let arch = arch?; + let lower = arch.name.to_ascii_lowercase(); + if matches!(lower.as_str(), "x86-64" | "x86_64" | "x64" | "amd64") { + return Some("x86-64".to_string()); + } + if matches!(lower.as_str(), "x86" | "x86-32" | "i386" | "i686") { + return Some("x86".to_string()); + } + Some(arch.name.clone()) +} + fn build_param_register_aliases( params: &[ast::CParam], recovered_params: &[(r2ssa::SSAVar, ast::CParam)], @@ -454,6 +950,30 @@ impl Default for DecompilerConfig { } impl DecompilerConfig { + pub fn for_arch_name(arch_name: &str, ptr_bits: u32) -> Self { + match (arch_name, ptr_bits) { + ("x86", 32) | ("x86-32", _) => Self::x86(), + ("x86-64", _) | ("x86_64", _) | ("x64", _) | ("amd64", _) => Self::x86_64(), + ("arm", _) | ("ARM", _) if ptr_bits == 32 => Self::arm(), + ("aarch64", _) | ("arm64", _) | ("ARM64", _) => Self::aarch64(), + ("riscv32", _) | ("rv32", _) | ("rv32gc", _) => Self::riscv32(), + ("riscv64", _) | ("rv64", _) | ("rv64gc", _) => Self::riscv64(), + ("riscv", _) if ptr_bits == 32 => Self::riscv32(), + ("riscv", _) => Self::riscv64(), + _ => Self { + ptr_size: ptr_bits, + ..Self::default() + }, + } + } + + pub fn for_arch(arch: Option<&r2il::ArchSpec>) -> (String, u32, Self) { + let arch_name = normalize_sig_arch_name(arch).unwrap_or_else(|| "unknown".to_string()); + let ptr_bits = arch.map(|spec| spec.addr_size * 8).unwrap_or(64); + let config = Self::for_arch_name(&arch_name, ptr_bits); + (arch_name, ptr_bits, config) + } + /// Create a configuration for 32-bit x86. pub fn x86() -> Self { Self { @@ -582,6 +1102,27 @@ pub struct DecompilerContext { } impl DecompilerContext { + pub fn from_analysis_inputs( + mut type_facts: FunctionTypeFacts, + function_names: std::collections::HashMap, + strings: std::collections::HashMap, + symbols: std::collections::HashMap, + ptr_bits: u32, + ) -> Self { + r2types::enrich_known_function_signatures_from_names( + &mut type_facts, + &function_names, + ptr_bits, + ); + r2types::enrich_known_function_signatures_from_names(&mut type_facts, &symbols, ptr_bits); + Self { + function_names, + strings, + symbols, + type_facts: type_facts.canonicalized(), + } + } + pub fn with_function_names( mut self, function_names: std::collections::HashMap, @@ -1023,7 +1564,8 @@ impl Decompiler { var_recovery.set_type_facts(self.context.type_facts.clone()); var_recovery.recover(func); - let skip_runtime_type_inference = should_skip_runtime_type_inference(prepared); + let skip_runtime_type_inference = + should_skip_runtime_type_inference(prepared, &self.context.type_facts); let type_inference = (!skip_runtime_type_inference).then(|| { let mut type_inference = TypeInference::new_with_abi( self.config.ptr_size, @@ -1158,20 +1700,21 @@ impl Decompiler { arg_regs: self.config.arg_regs.clone(), caller_saved_regs: self.config.caller_saved_regs.clone(), }; - let prepared_semantic_view = should_use_prepared_semantic_view(prepared).then(|| { - analysis::PreparedSemanticView::build(analysis::PreparedSemanticViewInputs { - prepared: prepared.expect("prepared semantic view requires prepared artifact"), - interproc_summary_set, - abi_arg_regs: &self.config.arg_regs, - ret_reg_name: &fold_arch.ret_reg_name, - function_names: &self.context.function_names, - symbols: &self.context.symbols, - callee_facts: &self.context.type_facts.callee_facts, - stack_slots: &self.context.type_facts.stack_slots, - visible_bindings: &self.context.type_facts.visible_bindings, - param_register_aliases: ¶m_register_aliases, - }) - }); + let prepared_semantic_view = + should_use_prepared_semantic_view(prepared, &self.context.type_facts).then(|| { + analysis::PreparedSemanticView::build(analysis::PreparedSemanticViewInputs { + prepared: prepared.expect("prepared semantic view requires prepared artifact"), + interproc_summary_set, + abi_arg_regs: &self.config.arg_regs, + ret_reg_name: &fold_arch.ret_reg_name, + function_names: &self.context.function_names, + symbols: &self.context.symbols, + callee_facts: &self.context.type_facts.callee_facts, + stack_slots: &self.context.type_facts.stack_slots, + visible_bindings: &self.context.type_facts.visible_bindings, + param_register_aliases: ¶m_register_aliases, + }) + }); let fold_inputs = FoldInputs { arch: &fold_arch, function_names: &self.context.function_names, @@ -1201,6 +1744,20 @@ impl Decompiler { let fold_blocks: Vec<_> = func.blocks().cloned().collect(); fold_ctx.analyze_blocks(&fold_blocks); fold_ctx.analyze_function_structure(func); + let func_name = func + .name + .clone() + .unwrap_or_else(|| format!("sub_{:x}", func.entry)); + let preferred_structuring_reason = preferred_semantic_structuring_reason( + &func_name, + &self.context.type_facts.symbolic_facts, + &func.cfg_risk_summary(), + ); + let preferred_linearization_reason = preferred_semantic_linearization_reason( + &func_name, + &self.context.type_facts.symbolic_facts, + &func.cfg_risk_summary(), + ); // Structure control flow (primary path: folded) let mut structurer = ControlFlowStructurer::new(func, &fold_ctx); @@ -1209,11 +1766,60 @@ impl Decompiler { let emitted_vars = structurer.emitted_var_names(); let mut use_conservative_locals = false; let mut is_linear_fallback = false; + let mut body_stmt = if let Some(ref reason) = preferred_structuring_reason { + use_conservative_locals = true; + match structurer.structure_semantic_worker_islands(6) { + Some(structured) => CStmt::Block(vec![ + CStmt::comment(format!("r2dec semantic worker structuring for {}", reason)), + structured, + ]), + None => { + let mut linear_stmts = self.linearize_function_body(func, &fold_ctx); + is_linear_fallback = true; + if linear_stmts.is_empty() { + CStmt::Block(vec![CStmt::comment(format!( + "r2dec fallback: semantic worker linearization for {} -> no statements recovered", + reason + ))]) + } else { + linear_stmts.insert( + 0, + CStmt::comment(format!( + "r2dec fallback: semantic worker linearization for {}", + reason + )), + ); + CStmt::Block(linear_stmts) + } + } + } + } else if let Some(ref reason) = preferred_linearization_reason { + let mut linear_stmts = self.linearize_function_body(func, &fold_ctx); + use_conservative_locals = true; + is_linear_fallback = true; + if linear_stmts.is_empty() { + CStmt::Block(vec![CStmt::comment(format!( + "r2dec fallback: semantic worker linearization for {} -> no statements recovered", + reason + ))]) + } else { + linear_stmts.insert( + 0, + CStmt::comment(format!( + "r2dec fallback: semantic worker linearization for {}", + reason + )), + ); + CStmt::Block(linear_stmts) + } + } else { + structurer.structure() + }; - let folded_stmt = structurer.structure(); - let mut body_stmt = folded_stmt; - - if !Self::stmt_has_content(&body_stmt) { + if preferred_structuring_reason.is_none() + && preferred_linearization_reason.is_none() + && !Self::stmt_has_content(&body_stmt) + { let folded_reason = structurer .safety_reason() .map(str::to_string) @@ -1241,7 +1847,25 @@ impl Decompiler { use_conservative_locals = true; is_linear_fallback = true; - if linear_stmts.is_empty() { + if prefer_symbolic_large_worker_decompile(&self.context.type_facts) { + let semantic_reason = + preferred_semantic_worker_reason(&func.cfg_risk_summary()); + if linear_stmts.is_empty() { + body_stmt = CStmt::Block(vec![CStmt::comment(format!( + "r2dec fallback: semantic worker linearization for {} -> no statements recovered", + semantic_reason + ))]); + } else { + linear_stmts.insert( + 0, + CStmt::comment(format!( + "r2dec fallback: semantic worker linearization for {}", + semantic_reason + )), + ); + body_stmt = CStmt::Block(linear_stmts); + } + } else if linear_stmts.is_empty() { body_stmt = CStmt::Block(vec![CStmt::comment(format!( "r2dec fallback: {} -> no statements recovered", fallback_reason @@ -1267,11 +1891,6 @@ impl Decompiler { } // Build the C function - let func_name = func - .name - .clone() - .unwrap_or_else(|| format!("sub_{:x}", func.entry)); - // Convert body to statements let body = self.stmt_to_vec(body_stmt); let body_visible_names = collect_stmt_var_names(&body); @@ -4557,4 +5176,436 @@ mod tests { "expected richer VM details in summary comment, got:\n{output}" ); } + + #[test] + fn preferred_semantic_fallback_comment_allows_ready_large_worker_decompile() { + let symbolic_facts = SymbolicSemanticFacts { + control_islands: vec![r2types::SymbolicControlIsland { + kind: r2types::SymbolicControlIslandKind::LargeCfgBranchFrontier, + anchor_block: 0x401000, + frontier_targets: vec![0x401010, 0x401020], + facts: vec![r2types::SymbolicControlFact { + target: 0x401010, + status: r2types::SymbolicReachabilityStatus::Reachable, + condition: Some("x == 0".to_string()), + compiled: Some(r2types::SymbolicCompiledCondition { + simplified: "x == 0".to_string(), + terms: vec!["x == 0".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: r2types::SymbolicConditionPrecision::Exact, + evidence: r2types::SymbolicSemanticEvidence::exact(), + confidence: r2types::SymbolicSemanticConfidence::Exact, + supported_paths: 1, + total_paths: 1, + }), + evidence: r2types::SymbolicSemanticEvidence::exact(), + confidence: r2types::SymbolicSemanticConfidence::Exact, + }], + evidence: r2types::SymbolicSemanticEvidence::exact(), + confidence: r2types::SymbolicSemanticConfidence::Exact, + }], + diagnostics: r2types::SymbolicFactDiagnostics { + skipped_large_cfg: true, + semantic_mode: Some(r2types::SymbolicSemanticMode::Residual), + semantic_capability: Some(r2types::SymbolicSemanticCapability { + query_ready: true, + type_ready: true, + decompile_ready: true, + }), + slice_class: Some(r2types::SymbolicSemanticSliceClass::Worker), + residual_reasons: vec![r2types::SymbolicSemanticResidualReason::LargeCfg], + ..Default::default() + }, + ..Default::default() + }; + assert!( + preferred_semantic_fallback_comment("fcn.401000", &symbolic_facts).is_none(), + "expected semantically ready autogenerated large worker to keep decompilation path" + ); + assert!( + preferred_semantic_fallback_comment("named_worker", &symbolic_facts).is_none(), + "expected named function to keep full decompilation path" + ); + } + + #[test] + fn preferred_semantic_linearization_reason_allows_ready_large_worker_linear_path() { + let symbolic_facts = SymbolicSemanticFacts { + worker_islands: vec![r2types::SymbolicWorkerIsland { + anchor_block: 0x401000, + control_kind: Some(r2types::SymbolicControlIslandKind::LargeCfgBranchFrontier), + memory_kind: Some(r2types::SymbolicMemoryIslandKind::LargeCfgConditionFrontier), + frontier_targets: vec![0x401010, 0x401020], + control_facts: Vec::new(), + memory_terms: Vec::new(), + evidence: r2types::SymbolicSemanticEvidence::exact(), + confidence: r2types::SymbolicSemanticConfidence::Exact, + }], + diagnostics: r2types::SymbolicFactDiagnostics { + skipped_large_cfg: true, + semantic_mode: Some(r2types::SymbolicSemanticMode::Residual), + semantic_capability: Some(r2types::SymbolicSemanticCapability { + query_ready: true, + type_ready: true, + decompile_ready: true, + }), + slice_class: Some(r2types::SymbolicSemanticSliceClass::Worker), + residual_reasons: vec![r2types::SymbolicSemanticResidualReason::LargeCfg], + ..Default::default() + }, + ..Default::default() + }; + let summary = r2ssa::CFGRiskSummary { + block_count: 107, + loop_count: 16, + back_edge_count: 16, + switch_block_count: 0, + max_switch_cases: 0, + }; + let reason = + preferred_semantic_linearization_reason("fcn.401000", &symbolic_facts, &summary) + .expect("ready large worker should prefer linearized decompile path"); + assert!(reason.contains("complex loop graph")); + } + + #[test] + fn preferred_semantic_structuring_reason_allows_strict_large_worker_structuring_path() { + let symbolic_facts = SymbolicSemanticFacts { + worker_islands: vec![r2types::SymbolicWorkerIsland { + anchor_block: 0x401000, + control_kind: Some(r2types::SymbolicControlIslandKind::LargeCfgBranchFrontier), + memory_kind: Some(r2types::SymbolicMemoryIslandKind::LargeCfgConditionFrontier), + frontier_targets: vec![0x401010, 0x401020], + control_facts: vec![r2types::SymbolicControlFact { + target: 0x401010, + status: r2types::SymbolicReachabilityStatus::Reachable, + condition: Some("x == 0".to_string()), + compiled: Some(r2types::SymbolicCompiledCondition { + simplified: "x == 0".to_string(), + terms: vec!["x == 0".to_string()], + memory_terms: vec![r2types::SymbolicMemoryCondition { + region: r2types::SymbolicMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: r2types::SymbolicSemanticEvidence::likely( + r2types::SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: r2types::SymbolicSemanticConfidence::Likely, + binding: None, + expr: "*arg0".to_string(), + value_expr: Some("0x0:8".to_string()), + exact_value: true, + }], + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: r2types::SymbolicConditionPrecision::OverApprox, + evidence: r2types::SymbolicSemanticEvidence::likely( + r2types::SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: r2types::SymbolicSemanticConfidence::Likely, + supported_paths: 1, + total_paths: 2, + }), + evidence: r2types::SymbolicSemanticEvidence::likely( + r2types::SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: r2types::SymbolicSemanticConfidence::Likely, + }], + memory_terms: vec![r2types::SymbolicMemoryCondition { + region: r2types::SymbolicMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: r2types::SymbolicSemanticEvidence::likely( + r2types::SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: r2types::SymbolicSemanticConfidence::Likely, + binding: None, + expr: "*arg0".to_string(), + value_expr: Some("0x0:8".to_string()), + exact_value: true, + }], + evidence: r2types::SymbolicSemanticEvidence::likely( + r2types::SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: r2types::SymbolicSemanticConfidence::Likely, + }], + diagnostics: r2types::SymbolicFactDiagnostics { + skipped_large_cfg: true, + semantic_mode: Some(r2types::SymbolicSemanticMode::IslandCompiled), + semantic_capability: Some(r2types::SymbolicSemanticCapability { + query_ready: true, + type_ready: true, + decompile_ready: true, + }), + slice_class: Some(r2types::SymbolicSemanticSliceClass::Worker), + residual_reasons: vec![r2types::SymbolicSemanticResidualReason::LargeCfg], + ..Default::default() + }, + ..Default::default() + }; + let summary = r2ssa::CFGRiskSummary { + block_count: 107, + loop_count: 16, + back_edge_count: 16, + switch_block_count: 0, + max_switch_cases: 0, + }; + let reason = preferred_semantic_structuring_reason("fcn.401000", &symbolic_facts, &summary) + .expect("strict ready large worker should prefer structured semantic path"); + assert!(reason.contains("complex loop graph")); + assert!( + preferred_semantic_linearization_reason("fcn.401000", &symbolic_facts, &summary) + .is_none(), + "structured-ready worker should not fall back to linearization" + ); + } + + #[test] + fn autogenerated_name_detection_accepts_underscore_hex_labels() { + assert!(is_autogenerated_function_name("_140010138")); + assert!(is_autogenerated_function_name("_401000")); + assert!(!is_autogenerated_function_name("_named_worker")); + } + + #[test] + fn preferred_semantic_structuring_reason_uses_worker_label_when_cfg_is_benign() { + let symbolic_facts = SymbolicSemanticFacts { + worker_islands: vec![r2types::SymbolicWorkerIsland { + anchor_block: 0x401000, + control_kind: Some(r2types::SymbolicControlIslandKind::LargeCfgBranchFrontier), + memory_kind: Some(r2types::SymbolicMemoryIslandKind::LargeCfgConditionFrontier), + frontier_targets: vec![0x401010, 0x401020], + control_facts: vec![r2types::SymbolicControlFact { + target: 0x401010, + status: r2types::SymbolicReachabilityStatus::Reachable, + condition: Some("x == 0".to_string()), + compiled: Some(r2types::SymbolicCompiledCondition { + simplified: "x == 0".to_string(), + terms: vec!["x == 0".to_string()], + memory_terms: vec![r2types::SymbolicMemoryCondition { + region: r2types::SymbolicMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: r2types::SymbolicSemanticEvidence::exact(), + confidence: r2types::SymbolicSemanticConfidence::Exact, + binding: None, + expr: "*arg0".to_string(), + value_expr: Some("0x0:8".to_string()), + exact_value: true, + }], + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: r2types::SymbolicConditionPrecision::Exact, + evidence: r2types::SymbolicSemanticEvidence::exact(), + confidence: r2types::SymbolicSemanticConfidence::Exact, + supported_paths: 1, + total_paths: 1, + }), + evidence: r2types::SymbolicSemanticEvidence::exact(), + confidence: r2types::SymbolicSemanticConfidence::Exact, + }], + memory_terms: Vec::new(), + evidence: r2types::SymbolicSemanticEvidence::exact(), + confidence: r2types::SymbolicSemanticConfidence::Exact, + }], + diagnostics: r2types::SymbolicFactDiagnostics { + skipped_large_cfg: true, + semantic_mode: Some(r2types::SymbolicSemanticMode::IslandCompiled), + semantic_capability: Some(r2types::SymbolicSemanticCapability { + query_ready: true, + type_ready: true, + decompile_ready: true, + }), + slice_class: Some(r2types::SymbolicSemanticSliceClass::Worker), + ..Default::default() + }, + ..Default::default() + }; + let summary = r2ssa::CFGRiskSummary { + block_count: 40, + loop_count: 1, + back_edge_count: 1, + switch_block_count: 0, + max_switch_cases: 0, + }; + assert_eq!( + preferred_semantic_structuring_reason("_140010138", &symbolic_facts, &summary) + .as_deref(), + Some("semantic worker islands") + ); + } + + #[test] + fn preferred_semantic_linearization_reason_uses_worker_label_when_cfg_is_benign() { + let symbolic_facts = SymbolicSemanticFacts { + worker_islands: vec![r2types::SymbolicWorkerIsland { + anchor_block: 0x401000, + control_kind: Some(r2types::SymbolicControlIslandKind::LargeCfgBranchFrontier), + memory_kind: Some(r2types::SymbolicMemoryIslandKind::LargeCfgConditionFrontier), + frontier_targets: vec![0x401010, 0x401020], + control_facts: vec![r2types::SymbolicControlFact { + target: 0x401010, + status: r2types::SymbolicReachabilityStatus::Reachable, + condition: Some("x == 0".to_string()), + compiled: Some(r2types::SymbolicCompiledCondition { + simplified: "x == 0".to_string(), + terms: vec!["x == 0".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: r2types::SymbolicConditionPrecision::OverApprox, + evidence: r2types::SymbolicSemanticEvidence::likely( + r2types::SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: r2types::SymbolicSemanticConfidence::Likely, + supported_paths: 1, + total_paths: 2, + }), + evidence: r2types::SymbolicSemanticEvidence::likely( + r2types::SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: r2types::SymbolicSemanticConfidence::Likely, + }], + memory_terms: Vec::new(), + evidence: r2types::SymbolicSemanticEvidence::likely( + r2types::SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: r2types::SymbolicSemanticConfidence::Likely, + }], + diagnostics: r2types::SymbolicFactDiagnostics { + skipped_large_cfg: true, + semantic_mode: Some(r2types::SymbolicSemanticMode::IslandCompiled), + semantic_capability: Some(r2types::SymbolicSemanticCapability { + query_ready: true, + type_ready: true, + decompile_ready: true, + }), + slice_class: Some(r2types::SymbolicSemanticSliceClass::Worker), + ..Default::default() + }, + ..Default::default() + }; + let summary = r2ssa::CFGRiskSummary { + block_count: 40, + loop_count: 1, + back_edge_count: 1, + switch_block_count: 0, + max_switch_cases: 0, + }; + assert_eq!( + preferred_semantic_linearization_reason("_140010138", &symbolic_facts, &summary) + .as_deref(), + Some("semantic worker islands") + ); + } + + #[test] + fn semantic_fallback_comment_reports_actionable_control_islands() { + let symbolic_facts = SymbolicSemanticFacts { + branch_facts: vec![r2types::SymbolicBranchFact { + block_addr: 0x401000, + true_target: 0x401010, + false_target: 0x401020, + true_status: r2types::SymbolicReachabilityStatus::Reachable, + false_status: r2types::SymbolicReachabilityStatus::Unreachable, + true_condition: Some("x == 0".to_string()), + false_condition: Some("x != 0".to_string()), + true_compiled: Some(r2types::SymbolicCompiledCondition { + simplified: "x == 0".to_string(), + terms: vec!["x == 0".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: r2types::SymbolicConditionPrecision::Exact, + evidence: r2types::SymbolicSemanticEvidence::exact(), + confidence: r2types::SymbolicSemanticConfidence::Exact, + supported_paths: 1, + total_paths: 1, + }), + false_compiled: None, + }], + control_islands: vec![r2types::SymbolicControlIsland { + kind: r2types::SymbolicControlIslandKind::LargeCfgBranchFrontier, + anchor_block: 0x401000, + frontier_targets: vec![0x401010, 0x401020], + facts: vec![r2types::SymbolicControlFact { + target: 0x401010, + status: r2types::SymbolicReachabilityStatus::Reachable, + condition: Some("x == 0".to_string()), + compiled: Some(r2types::SymbolicCompiledCondition { + simplified: "x == 0".to_string(), + terms: vec!["x == 0".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: r2types::SymbolicConditionPrecision::Exact, + evidence: r2types::SymbolicSemanticEvidence::exact(), + confidence: r2types::SymbolicSemanticConfidence::Exact, + supported_paths: 1, + total_paths: 1, + }), + evidence: r2types::SymbolicSemanticEvidence::exact(), + confidence: r2types::SymbolicSemanticConfidence::Exact, + }], + evidence: r2types::SymbolicSemanticEvidence::exact(), + confidence: r2types::SymbolicSemanticConfidence::Exact, + }], + memory_islands: vec![r2types::SymbolicMemoryIsland { + kind: r2types::SymbolicMemoryIslandKind::LargeCfgConditionFrontier, + anchor_block: 0x401000, + terms: vec![r2types::SymbolicMemoryCondition { + region: r2types::SymbolicMemoryRegion::Argument { index: 0 }, + offset_lo: 8, + offset_hi: 8, + size: 4, + exact_offset: true, + evidence: r2types::SymbolicSemanticEvidence::exact(), + confidence: r2types::SymbolicSemanticConfidence::Exact, + binding: None, + expr: "*(arg0 + 8)".to_string(), + value_expr: None, + exact_value: false, + }], + evidence: r2types::SymbolicSemanticEvidence::exact(), + confidence: r2types::SymbolicSemanticConfidence::Exact, + }], + diagnostics: r2types::SymbolicFactDiagnostics { + branches_evaluated: 1, + branches_pruned: 1, + semantic_mode: Some(r2types::SymbolicSemanticMode::Residual), + semantic_capability: Some(r2types::SymbolicSemanticCapability { + query_ready: true, + type_ready: true, + decompile_ready: true, + }), + slice_class: Some(r2types::SymbolicSemanticSliceClass::Worker), + residual_reasons: vec![r2types::SymbolicSemanticResidualReason::LargeCfg], + ..Default::default() + }, + ..Default::default() + }; + let output = semantic_fallback_comment("_401000", &symbolic_facts) + .expect("typed semantic fallback comment"); + assert!(output.contains("semantic fallback: worker slice in residual mode")); + assert!(output.contains("branch_facts=1")); + assert!(output.contains("control_islands=1")); + assert!(output.contains("memory_islands=1")); + assert!(output.contains("actionable_conditions=1")); + assert!(output.contains("exact_conditions=1")); + assert!(output.contains("actionable_preview=[0x401000: x == 0]")); + } } diff --git a/crates/r2dec/src/structure.rs b/crates/r2dec/src/structure.rs index 2d5f4f7..4ad207c 100644 --- a/crates/r2dec/src/structure.rs +++ b/crates/r2dec/src/structure.rs @@ -178,7 +178,7 @@ impl<'a, 'o> ControlFlowStructurer<'a, 'o> { else_region, merge_block, } => { - if let Some(rewritten) = self.try_structure_symbolic_exact_if( + if let Some(rewritten) = self.try_structure_symbolic_actionable_if( *cond_block, then_region, else_region.as_deref(), @@ -432,26 +432,300 @@ impl<'a, 'o> ControlFlowStructurer<'a, 'o> { .exact_reachable_target_for_block(cond_block) } - fn try_structure_symbolic_exact_if( + fn symbolic_actionable_reachable_target(&self, cond_block: u64) -> Option { + self.fold_ctx + .inputs + .symbolic_facts + .actionable_reachable_target_for_block(cond_block) + } + + fn worker_island_supports_semantic_structuring(island: &r2types::SymbolicWorkerIsland) -> bool { + let supporting_condition = Self::worker_island_supporting_compiled_condition(island); + let has_unique_target = island.exact_reachable_target().is_some() + || island.actionable_reachable_target().is_some(); + let has_condition = supporting_condition.is_some(); + let has_memory_support = !island.actionable_terms().is_empty() + || supporting_condition.is_some_and(|compiled| !compiled.memory_terms.is_empty()); + island.evidence.allows_narrowing() + && has_unique_target + && has_condition + && has_memory_support + } + + fn worker_island_supporting_compiled_condition( + island: &r2types::SymbolicWorkerIsland, + ) -> Option<&r2types::SymbolicCompiledCondition> { + let reachable_target = island + .exact_reachable_target() + .or_else(|| island.actionable_reachable_target())?; + island + .control_facts + .iter() + .find(|fact| fact.target == reachable_target) + .and_then(r2types::SymbolicControlFact::actionable_compiled_condition) + } + + fn branch_condition_for_reachable_target( + &mut self, + cond_block: u64, + reachable_target: u64, + ) -> CExpr { + let cond = self.get_branch_condition(cond_block); + if let Some(branch) = self + .fold_ctx + .inputs + .symbolic_facts + .branch_fact_for_block(cond_block) + && branch.false_target == reachable_target + { + return Self::negate_condition(cond); + } + cond + } + + fn structure_semantic_worker_island( + &mut self, + island: &r2types::SymbolicWorkerIsland, + ) -> Option { + if !Self::worker_island_supports_semantic_structuring(island) { + return None; + } + let reachable_target = island + .exact_reachable_target() + .or_else(|| island.actionable_reachable_target())?; + let cond = + self.branch_condition_for_reachable_target(island.anchor_block, reachable_target); + let mut then_stmts = Vec::new(); + if let Some(compiled) = Self::worker_island_supporting_compiled_condition(island) { + then_stmts.push(CStmt::comment(format!( + "semantic worker branch: {}", + compiled.simplified + ))); + } + if self.func.get_block(reachable_target).is_some() { + Self::append_stmt_body_flat(&mut then_stmts, self.structure_block(reachable_target)); + } else { + then_stmts.push(CStmt::comment(format!( + "semantic worker target: 0x{reachable_target:x}" + ))); + } + for term in island.actionable_terms().into_iter().take(2) { + then_stmts.push(CStmt::comment(format!( + "semantic memory: {} [{:?}]", + term.expr, term.evidence.tier + ))); + } + let then_body = if then_stmts.len() == 1 { + then_stmts.into_iter().next().unwrap_or(CStmt::Empty) + } else { + CStmt::Block(then_stmts) + }; + let mut prefix = self.structure_block_prefix_stmts(island.anchor_block); + prefix.push(CStmt::if_stmt(cond, then_body, None)); + Some(if prefix.len() == 1 { + prefix.into_iter().next().unwrap_or(CStmt::Empty) + } else { + CStmt::Block(prefix) + }) + } + + pub fn structure_semantic_worker_islands(&mut self, limit: usize) -> Option { + let islands = self + .fold_ctx + .inputs + .symbolic_facts + .worker_islands + .iter() + .filter(|island| Self::worker_island_supports_semantic_structuring(island)) + .take(limit) + .cloned() + .collect::>(); + let mut stmts = Vec::new(); + let mut seen_targets = HashSet::new(); + for island in &islands { + let Some(target) = island + .exact_reachable_target() + .or_else(|| island.actionable_reachable_target()) + else { + continue; + }; + if !seen_targets.insert((island.anchor_block, target)) { + continue; + } + if let Some(stmt) = self.structure_semantic_worker_island(island) { + Self::append_stmt_body_flat(&mut stmts, stmt); + } + } + if stmts.is_empty() { + None + } else if stmts.len() == 1 { + Some(stmts.into_iter().next().unwrap_or(CStmt::Empty)) + } else { + Some(CStmt::Block(stmts)) + } + } + + fn structure_region_suffix_from_target( + &mut self, + region: &Region, + target: u64, + ) -> Option { + match region { + Region::Block(addr) => (*addr == target).then(|| self.structure_block(*addr)), + Region::Sequence(regions) => { + let start_idx = regions + .iter() + .position(|child| child.blocks().contains(&target))?; + let mut stmts = Vec::new(); + Self::append_stmt_body_flat( + &mut stmts, + self.structure_region_suffix_from_target(®ions[start_idx], target)?, + ); + for child in ®ions[start_idx + 1..] { + Self::append_stmt_body_flat(&mut stmts, self.structure_region(child)); + } + Some(if stmts.len() == 1 { + stmts.into_iter().next().unwrap_or(CStmt::Empty) + } else { + CStmt::Block(stmts) + }) + } + Region::IfThenElse { + cond_block, + then_region, + else_region, + merge_block, + } => { + if *cond_block == target { + return Some(self.structure_region(region)); + } + let mut stmts = if then_region.blocks().contains(&target) { + let mut stmts = Vec::new(); + Self::append_stmt_body_flat( + &mut stmts, + self.structure_region_suffix_from_target(then_region, target)?, + ); + stmts + } else if else_region + .as_deref() + .is_some_and(|region| region.blocks().contains(&target)) + { + let mut stmts = Vec::new(); + Self::append_stmt_body_flat( + &mut stmts, + self.structure_region_suffix_from_target(else_region.as_deref()?, target)?, + ); + stmts + } else if merge_block.is_some_and(|merge| merge == target) { + vec![self.structure_block(target)] + } else { + return None; + }; + if let Some(merge_addr) = merge_block.filter(|merge| *merge != target) { + Self::append_stmt_body_flat(&mut stmts, self.structure_block(merge_addr)); + } + Some(if stmts.len() == 1 { + stmts.into_iter().next().unwrap_or(CStmt::Empty) + } else { + CStmt::Block(stmts) + }) + } + Region::WhileLoop { header, body } => { + if *header == target { + Some(self.structure_region(region)) + } else if body.blocks().contains(&target) { + self.structure_region_suffix_from_target(body, target) + } else { + None + } + } + Region::DoWhileLoop { body, cond_block } => { + if *cond_block == target { + return Some(self.structure_block(*cond_block)); + } + if !body.blocks().contains(&target) { + return None; + } + let mut stmts = Vec::new(); + Self::append_stmt_body_flat( + &mut stmts, + self.structure_region_suffix_from_target(body, target)?, + ); + Self::append_stmt_body_flat(&mut stmts, self.structure_block(*cond_block)); + Some(if stmts.len() == 1 { + stmts.into_iter().next().unwrap_or(CStmt::Empty) + } else { + CStmt::Block(stmts) + }) + } + Region::Switch { + switch_block, + cases, + default, + merge_block, + } => { + if *switch_block == target { + return Some(self.structure_region(region)); + } + let mut stmts = if let Some((_, case_region)) = cases + .iter() + .find(|(_, case_region)| case_region.blocks().contains(&target)) + { + let mut stmts = Vec::new(); + Self::append_stmt_body_flat( + &mut stmts, + self.structure_region_suffix_from_target(case_region, target)?, + ); + stmts + } else if default + .as_deref() + .is_some_and(|region| region.blocks().contains(&target)) + { + let mut stmts = Vec::new(); + Self::append_stmt_body_flat( + &mut stmts, + self.structure_region_suffix_from_target(default.as_deref()?, target)?, + ); + stmts + } else if merge_block.is_some_and(|merge| merge == target) { + vec![self.structure_block(target)] + } else { + return None; + }; + if let Some(merge_addr) = merge_block.filter(|merge| *merge != target) { + Self::append_stmt_body_flat(&mut stmts, self.structure_block(merge_addr)); + } + Some(if stmts.len() == 1 { + stmts.into_iter().next().unwrap_or(CStmt::Empty) + } else { + CStmt::Block(stmts) + }) + } + Region::Irreducible { blocks, .. } => blocks + .contains(&target) + .then(|| self.structure_block(target)), + } + } + + fn try_structure_symbolic_actionable_if( &mut self, cond_block: u64, then_region: &Region, else_region: Option<&Region>, merge_block: Option, ) -> Option { - let reachable_target = self.symbolic_exact_reachable_target(cond_block)?; - let reachable_region = if then_region.entry() == reachable_target { - then_region + let reachable_target = self + .symbolic_exact_reachable_target(cond_block) + .or_else(|| self.symbolic_actionable_reachable_target(cond_block))?; + let reachable_stmt = if then_region.blocks().contains(&reachable_target) { + self.structure_region_suffix_from_target(then_region, reachable_target)? } else { let else_region = else_region?; - if else_region.entry() != reachable_target { - return None; - } - else_region + self.structure_region_suffix_from_target(else_region, reachable_target)? }; let mut prefix = self.structure_block_prefix_stmts(cond_block); - Self::append_stmt_body_flat(&mut prefix, self.structure_region(reachable_region)); + Self::append_stmt_body_flat(&mut prefix, reachable_stmt); if let Some(merge_addr) = merge_block { Self::append_stmt_body_flat(&mut prefix, self.structure_block(merge_addr)); } @@ -2232,10 +2506,12 @@ mod tests { use r2il::{ArchSpec, R2ILBlock, R2ILOp, RegisterDef, Varnode}; use r2ssa::SSAFunction; use r2types::{ + SymbolicBranchFact, SymbolicCompiledCondition, SymbolicConditionPrecision, SymbolicControlFact, SymbolicControlIsland, SymbolicControlIslandKind, - SymbolicInterpreterKind, SymbolicReachabilityStatus, SymbolicSemanticConfidence, - SymbolicSemanticEvidence, SymbolicSemanticFacts, SymbolicVmStateUpdate, - SymbolicVmStepSummary, + SymbolicInterpreterKind, SymbolicMemoryCondition, SymbolicMemoryIslandKind, + SymbolicMemoryRegion, SymbolicReachabilityStatus, SymbolicSemanticConfidence, + SymbolicSemanticEvidence, SymbolicSemanticEvidenceReason, SymbolicSemanticFacts, + SymbolicVmStateUpdate, SymbolicVmStepSummary, SymbolicWorkerIsland, }; use std::collections::BTreeMap; @@ -2270,6 +2546,53 @@ mod tests { .with_name("vm_summary_demo") } + fn function_with_return_blocks(addrs: &[u64]) -> SSAFunction { + let blocks = addrs + .iter() + .map(|addr| { + let mut block = R2ILBlock::new(*addr, 4); + block.push(R2ILOp::Return { + target: Varnode::constant(0, 8), + }); + block + }) + .collect::>(); + SSAFunction::from_blocks_with_arch(&blocks, Some(&test_arch())) + .expect("ssa function") + .with_name("worker_region_demo") + } + + fn function_with_conditional_return_blocks( + cond_block: u64, + true_target: u64, + false_target: u64, + ) -> SSAFunction { + let mut cond = R2ILBlock::new(cond_block, 4); + cond.push(R2ILOp::IntEqual { + dst: Varnode::register(0x80, 1), + a: Varnode::register(0x10, 8), + b: Varnode::constant(0, 8), + }); + cond.push(R2ILOp::CBranch { + target: Varnode::constant(true_target, 8), + cond: Varnode::register(0x80, 1), + }); + + let mut false_block = R2ILBlock::new(false_target, 4); + false_block.push(R2ILOp::Return { + target: Varnode::constant(0, 8), + }); + + let mut true_block = R2ILBlock::new(true_target, 4); + true_block.push(R2ILOp::Return { + target: Varnode::constant(1, 8), + }); + + SSAFunction::from_blocks_with_arch(&[cond, false_block, true_block], Some(&test_arch())) + .expect("ssa function") + .with_name("worker_structured_demo") + } + #[test] fn rewrites_canonical_while_to_for() { let input = CStmt::Block(vec![ @@ -2980,4 +3303,263 @@ mod tests { Some(0x2004) ); } + + #[test] + fn symbolic_actionable_reachable_target_uses_likely_control_island_fallback() { + let func = function_with_single_block(0x2000); + let mut ctx = FoldingContext::new(64); + ctx.inputs.symbolic_facts = Box::leak(Box::new(SymbolicSemanticFacts { + control_islands: vec![SymbolicControlIsland { + kind: SymbolicControlIslandKind::LargeCfgBranchFrontier, + anchor_block: 0x2000, + frontier_targets: vec![0x2004, 0x2008], + facts: vec![ + SymbolicControlFact { + target: 0x2004, + status: SymbolicReachabilityStatus::Reachable, + condition: Some("x == 0".to_string()), + compiled: Some(SymbolicCompiledCondition { + simplified: "x == 0".to_string(), + terms: vec!["x == 0".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: SymbolicConditionPrecision::OverApprox, + evidence: SymbolicSemanticEvidence::likely( + SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: SymbolicSemanticConfidence::Likely, + supported_paths: 1, + total_paths: 2, + }), + evidence: SymbolicSemanticEvidence::likely( + SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: SymbolicSemanticConfidence::Likely, + }, + SymbolicControlFact { + target: 0x2008, + status: SymbolicReachabilityStatus::Unreachable, + condition: Some("!(x == 0)".to_string()), + compiled: None, + evidence: SymbolicSemanticEvidence::likely( + SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: SymbolicSemanticConfidence::Likely, + }, + ], + evidence: SymbolicSemanticEvidence::likely( + SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: SymbolicSemanticConfidence::Likely, + }], + ..SymbolicSemanticFacts::default() + })); + + let structurer = ControlFlowStructurer::new(&func, &ctx); + assert_eq!( + structurer.symbolic_actionable_reachable_target(0x2000), + Some(0x2004) + ); + } + + #[test] + fn symbolic_actionable_if_accepts_reachable_target_inside_region() { + let func = function_with_return_blocks(&[0x2000, 0x2004, 0x2008]); + let mut ctx = FoldingContext::new(64); + ctx.inputs.symbolic_facts = Box::leak(Box::new(SymbolicSemanticFacts { + control_islands: vec![SymbolicControlIsland { + kind: SymbolicControlIslandKind::LargeCfgBranchFrontier, + anchor_block: 0x2000, + frontier_targets: vec![0x2004, 0x2008], + facts: vec![ + SymbolicControlFact { + target: 0x2008, + status: SymbolicReachabilityStatus::Reachable, + condition: Some("x == 0".to_string()), + compiled: Some(SymbolicCompiledCondition { + simplified: "x == 0".to_string(), + terms: vec!["x == 0".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: SymbolicConditionPrecision::OverApprox, + evidence: SymbolicSemanticEvidence::likely( + SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: SymbolicSemanticConfidence::Likely, + supported_paths: 1, + total_paths: 2, + }), + evidence: SymbolicSemanticEvidence::likely( + SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: SymbolicSemanticConfidence::Likely, + }, + SymbolicControlFact { + target: 0x2004, + status: SymbolicReachabilityStatus::Unreachable, + condition: Some("!(x == 0)".to_string()), + compiled: Some(SymbolicCompiledCondition { + simplified: "!(x == 0)".to_string(), + terms: vec!["!(x == 0)".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: SymbolicConditionPrecision::OverApprox, + evidence: SymbolicSemanticEvidence::likely( + SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: SymbolicSemanticConfidence::Likely, + supported_paths: 1, + total_paths: 2, + }), + evidence: SymbolicSemanticEvidence::likely( + SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: SymbolicSemanticConfidence::Likely, + }, + ], + evidence: SymbolicSemanticEvidence::likely( + SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: SymbolicSemanticConfidence::Likely, + }], + ..SymbolicSemanticFacts::default() + })); + + let mut structurer = ControlFlowStructurer::new(&func, &ctx); + let then_region = crate::region::Region::Sequence(vec![ + crate::region::Region::Block(0x2004), + crate::region::Region::Block(0x2008), + ]); + + let rewritten = + structurer.try_structure_symbolic_actionable_if(0x2000, &then_region, None, None); + assert!( + rewritten.is_some(), + "reachable targets inside the structured region should still drive symbolic if shaping" + ); + } + + #[test] + fn semantic_worker_island_structuring_builds_real_if_statement() { + let func = function_with_conditional_return_blocks(0x2000, 0x2004, 0x2008); + let mut ctx = FoldingContext::new(64); + ctx.inputs.symbolic_facts = Box::leak(Box::new(SymbolicSemanticFacts { + worker_islands: vec![SymbolicWorkerIsland { + anchor_block: 0x2000, + control_kind: Some(SymbolicControlIslandKind::LargeCfgBranchFrontier), + memory_kind: Some(SymbolicMemoryIslandKind::LargeCfgConditionFrontier), + frontier_targets: vec![0x2004, 0x2008], + control_facts: vec![ + SymbolicControlFact { + target: 0x2004, + status: SymbolicReachabilityStatus::Reachable, + condition: Some("x == 0".to_string()), + compiled: Some(SymbolicCompiledCondition { + simplified: "x == 0".to_string(), + terms: vec!["x == 0".to_string()], + memory_terms: vec![SymbolicMemoryCondition { + region: SymbolicMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: SymbolicSemanticEvidence::likely( + SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: SymbolicSemanticConfidence::Likely, + binding: None, + expr: "*arg0".to_string(), + value_expr: Some("0x0:8".to_string()), + exact_value: true, + }], + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: SymbolicConditionPrecision::OverApprox, + evidence: SymbolicSemanticEvidence::likely( + SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: SymbolicSemanticConfidence::Likely, + supported_paths: 1, + total_paths: 2, + }), + evidence: SymbolicSemanticEvidence::likely( + SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: SymbolicSemanticConfidence::Likely, + }, + SymbolicControlFact { + target: 0x2008, + status: SymbolicReachabilityStatus::Unreachable, + condition: Some("!(x == 0)".to_string()), + compiled: Some(SymbolicCompiledCondition { + simplified: "!(x == 0)".to_string(), + terms: vec!["!(x == 0)".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: SymbolicConditionPrecision::OverApprox, + evidence: SymbolicSemanticEvidence::likely( + SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: SymbolicSemanticConfidence::Likely, + supported_paths: 1, + total_paths: 2, + }), + evidence: SymbolicSemanticEvidence::likely( + SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: SymbolicSemanticConfidence::Likely, + }, + ], + memory_terms: vec![SymbolicMemoryCondition { + region: SymbolicMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: SymbolicSemanticEvidence::likely( + SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: SymbolicSemanticConfidence::Likely, + binding: None, + expr: "*arg0".to_string(), + value_expr: Some("0x0:8".to_string()), + exact_value: true, + }], + evidence: SymbolicSemanticEvidence::likely( + SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: SymbolicSemanticConfidence::Likely, + }], + branch_facts: vec![SymbolicBranchFact { + block_addr: 0x2000, + true_target: 0x2004, + false_target: 0x2008, + true_status: SymbolicReachabilityStatus::Reachable, + false_status: SymbolicReachabilityStatus::Unreachable, + true_condition: Some("x == 0".to_string()), + false_condition: Some("!(x == 0)".to_string()), + true_compiled: None, + false_compiled: None, + }], + ..SymbolicSemanticFacts::default() + })); + + let mut structurer = ControlFlowStructurer::new(&func, &ctx); + let rewritten = structurer + .structure_semantic_worker_islands(4) + .expect("semantic worker structuring"); + assert!( + matches!(rewritten, CStmt::If { .. } | CStmt::Block(_)), + "expected structured semantic worker output, got {rewritten:?}" + ); + } } diff --git a/crates/r2sym/src/backward.rs b/crates/r2sym/src/backward.rs index 7eaa4c9..ebbf308 100644 --- a/crates/r2sym/src/backward.rs +++ b/crates/r2sym/src/backward.rs @@ -49,7 +49,15 @@ impl BackwardConditionSummary { .with_provenance(SemanticEvidenceProvenance::Normalized) } BackwardConditionPrecision::ResidualSearchRequired => { - if self.supported_paths > 0 { + if self.supported_paths > 0 + && !self.memory_terms.is_empty() + && self.backward_memory_residual_fallbacks == 0 + { + SemanticEvidence::likely(SemanticEvidenceReason::DerivedFromRanking) + .with_coverage(SemanticEvidenceCoverage::Bounded) + .with_provenance(SemanticEvidenceProvenance::Normalized) + .with_reason(SemanticEvidenceReason::PartialPathCoverage) + } else if self.supported_paths > 0 { SemanticEvidence::heuristic(SemanticEvidenceReason::ResidualSearchRequired) .with_coverage(SemanticEvidenceCoverage::Bounded) } else { @@ -74,20 +82,28 @@ pub struct BackwardMemoryCondition { pub offset_hi: i64, pub size: u32, pub exact_offset: bool, + #[serde(default, skip_serializing_if = "SemanticEvidence::is_default_exact")] + pub evidence: SemanticEvidence, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub binding: Option, pub expr: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub value_expr: Option, + #[serde(default)] + pub exact_value: bool, } impl BackwardMemoryCondition { pub fn evidence(&self) -> SemanticEvidence { - if self.exact_offset { - SemanticEvidence::exact() - } else if self.offset_hi >= self.offset_lo && (self.offset_hi - self.offset_lo) <= 8 { - SemanticEvidence::likely(SemanticEvidenceReason::DerivedFromRanking) - .with_coverage(SemanticEvidenceCoverage::Bounded) - .with_provenance(SemanticEvidenceProvenance::Normalized) + if self.evidence.is_default_exact() && !self.exact_offset { + inferred_memory_term_evidence( + &self.region, + self.offset_lo, + self.offset_hi, + self.exact_offset, + ) } else { - SemanticEvidence::heuristic(SemanticEvidenceReason::AliasAmbiguity) - .with_coverage(SemanticEvidenceCoverage::Bounded) + self.evidence.clone() } } @@ -96,6 +112,62 @@ impl BackwardMemoryCondition { } } +fn inferred_memory_term_evidence( + region: &BackwardMemoryRegion, + offset_lo: i64, + offset_hi: i64, + exact_offset: bool, +) -> SemanticEvidence { + if exact_offset { + SemanticEvidence::exact() + } else { + let Some(span) = (offset_hi >= offset_lo).then_some(offset_hi - offset_lo) else { + return SemanticEvidence::heuristic(SemanticEvidenceReason::AliasAmbiguity) + .with_coverage(SemanticEvidenceCoverage::Bounded); + }; + + let (likely_span, provenance, reason) = match region { + BackwardMemoryRegion::Argument { .. } => ( + 16, + SemanticEvidenceProvenance::Stable, + SemanticEvidenceReason::DerivedFromRanking, + ), + BackwardMemoryRegion::Region(region) => match region.kind { + MemoryRegionKind::Stack | MemoryRegionKind::Global | MemoryRegionKind::Input => ( + 16, + SemanticEvidenceProvenance::Stable, + SemanticEvidenceReason::DerivedFromRanking, + ), + MemoryRegionKind::Replay => ( + 16, + SemanticEvidenceProvenance::Ranked, + SemanticEvidenceReason::ReplayOverlap, + ), + MemoryRegionKind::Heap => ( + 12, + SemanticEvidenceProvenance::Ranked, + SemanticEvidenceReason::HeapIdentityWeak, + ), + MemoryRegionKind::EscapedUnknown => ( + 8, + SemanticEvidenceProvenance::Unstable, + SemanticEvidenceReason::AliasAmbiguity, + ), + }, + }; + + if span <= likely_span { + SemanticEvidence::likely(reason) + .with_coverage(SemanticEvidenceCoverage::Bounded) + .with_provenance(provenance) + } else { + SemanticEvidence::heuristic(reason) + .with_coverage(SemanticEvidenceCoverage::Bounded) + .with_provenance(provenance) + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub struct BackwardRegionRef { pub id: MemoryRegionId, @@ -109,6 +181,43 @@ pub enum BackwardMemoryRegion { Region(BackwardRegionRef), } +fn format_backward_memory_location(region: &BackwardMemoryRegion, offset: i64) -> String { + match region { + BackwardMemoryRegion::Argument { index } => { + if offset == 0 { + format!("*arg{index}") + } else if offset > 0 { + format!("*(arg{index} + 0x{:x})", offset as u64) + } else { + format!("*(arg{index} - 0x{:x})", offset.unsigned_abs()) + } + } + BackwardMemoryRegion::Region(region) => { + let base = region.name.as_str(); + if offset == 0 { + format!("*{base}") + } else if offset > 0 { + format!("*({base} + 0x{:x})", offset as u64) + } else { + format!("*({base} - 0x{:x})", offset.unsigned_abs()) + } + } + } +} + +fn backward_memory_term_expr( + region: &BackwardMemoryRegion, + offset_lo: i64, + offset_hi: i64, + fallback: &str, +) -> String { + if offset_lo == offset_hi { + format_backward_memory_location(region, offset_lo) + } else { + fallback.to_string() + } +} + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] struct NormalizedMemoryLocation { region: BackwardMemoryRegion, @@ -700,13 +809,22 @@ impl<'a, 'ctx> ValueTranslator<'a, 'ctx> { for (region, offsets) in grouped { let offset_lo = offsets.iter().copied().min().unwrap_or(0); let offset_hi = offsets.iter().copied().max().unwrap_or(0); + let exact_offset = offset_lo == offset_hi; + let value_expr = value.to_string(); + let expr = backward_memory_term_expr(®ion, offset_lo, offset_hi, &value_expr); + let evidence = + inferred_memory_term_evidence(®ion, offset_lo, offset_hi, exact_offset); self.memory_terms.push(BackwardMemoryCondition { region, offset_lo, offset_hi, size, - exact_offset: offset_lo == offset_hi, - expr: value.to_string(), + exact_offset, + evidence, + binding: None, + expr, + value_expr: Some(value_expr), + exact_value: value.is_concrete(), }); } true @@ -1031,21 +1149,35 @@ where .pointers .into_iter() .find_map(|pointer| { - state - .memory - .region_def(pointer.region_id) - .map(|def| BackwardMemoryCondition { - region: BackwardMemoryRegion::Region(BackwardRegionRef { - id: def.id, - kind: def.kind.clone(), - name: def.name.clone(), - }), - offset_lo: i64::try_from(pointer.offset).unwrap_or(0), - offset_hi: i64::try_from(pointer.offset).unwrap_or(0), + state.memory.region_def(pointer.region_id).map(|def| { + let region = BackwardMemoryRegion::Region(BackwardRegionRef { + id: def.id, + kind: def.kind.clone(), + name: def.name.clone(), + }); + let offset = i64::try_from(pointer.offset).unwrap_or(0); + BackwardMemoryCondition { + region: region.clone(), + offset_lo: offset, + offset_hi: offset, size, exact_offset: true, - expr: state.mem_read(&addr, size).to_string(), - }) + evidence: if matches!( + summary.completion, + crate::sim::DerivedSummaryCompletion::Exact + ) { + SemanticEvidence::exact() + } else { + SemanticEvidence::likely(SemanticEvidenceReason::PartialPathCoverage) + .with_coverage(SemanticEvidenceCoverage::Bounded) + .with_provenance(SemanticEvidenceProvenance::Normalized) + }, + binding: None, + expr: format_backward_memory_location(®ion, offset), + value_expr: None, + exact_value: false, + } + }) }) .unwrap_or(BackwardMemoryCondition { region: BackwardMemoryRegion::Argument { index: arg_index }, @@ -1053,7 +1185,23 @@ where offset_hi: offset, size, exact_offset: true, - expr: state.mem_read(&addr, size).to_string(), + evidence: if matches!( + summary.completion, + crate::sim::DerivedSummaryCompletion::Exact + ) { + SemanticEvidence::exact() + } else { + SemanticEvidence::likely(SemanticEvidenceReason::PartialPathCoverage) + .with_coverage(SemanticEvidenceCoverage::Bounded) + .with_provenance(SemanticEvidenceProvenance::Normalized) + }, + binding: None, + expr: format_backward_memory_location( + &BackwardMemoryRegion::Argument { index: arg_index }, + offset, + ), + value_expr: None, + exact_value: false, }); Some(CompiledBackwardCondition { summary: BackwardConditionSummary { @@ -2092,4 +2240,53 @@ mod tests { ]); assert!(select_best_translated_arg(translated).is_none()); } + + #[test] + fn inferred_memory_term_evidence_promotes_bounded_global_and_replay_regions_to_likely() { + let global_region = BackwardMemoryRegion::Region(BackwardRegionRef { + id: MemoryRegionId(1), + kind: MemoryRegionKind::Global, + name: "global".to_string(), + }); + let replay_region = BackwardMemoryRegion::Region(BackwardRegionRef { + id: MemoryRegionId(2), + kind: MemoryRegionKind::Replay, + name: "replay".to_string(), + }); + + let global = inferred_memory_term_evidence(&global_region, 0, 12, false); + let replay = inferred_memory_term_evidence(&replay_region, 4, 16, false); + + assert_eq!(global.tier, SemanticConfidence::Likely); + assert!( + global + .reasons + .contains(&SemanticEvidenceReason::DerivedFromRanking) + ); + assert_eq!(replay.tier, SemanticConfidence::Likely); + assert!( + replay + .reasons + .contains(&SemanticEvidenceReason::ReplayOverlap) + ); + } + + #[test] + fn inferred_memory_term_evidence_promotes_small_heap_windows_to_likely() { + let heap_region = BackwardMemoryRegion::Region(BackwardRegionRef { + id: MemoryRegionId(3), + kind: MemoryRegionKind::Heap, + name: "heap".to_string(), + }); + + let heap = inferred_memory_term_evidence(&heap_region, 8, 16, false); + let ambiguous = inferred_memory_term_evidence(&heap_region, 8, 32, false); + + assert_eq!(heap.tier, SemanticConfidence::Likely); + assert!( + heap.reasons + .contains(&SemanticEvidenceReason::HeapIdentityWeak) + ); + assert_eq!(ambiguous.tier, SemanticConfidence::Heuristic); + } } diff --git a/crates/r2sym/src/lib.rs b/crates/r2sym/src/lib.rs index 66b6eb0..55d9768 100644 --- a/crates/r2sym/src/lib.rs +++ b/crates/r2sym/src/lib.rs @@ -77,10 +77,12 @@ pub use semantics::{ SemanticEvidenceProvenance, SemanticEvidenceReason, SemanticEvidenceSoundness, SemanticMode, SliceClass, SymbolicBranchFact, SymbolicControlFact, SymbolicControlIsland, SymbolicControlIslandKind, SymbolicFunctionFactDiagnostics, SymbolicFunctionFacts, - SymbolicReachabilityStatus, VmBinaryOp, VmGuardCondition, VmGuardedExit, VmMemoryCondition, + SymbolicMemoryIsland, SymbolicMemoryIslandKind, SymbolicReachabilityStatus, + SymbolicWorkerIsland, VmBinaryOp, VmGuardCondition, VmGuardedExit, VmMemoryCondition, VmMemoryRegionRef, VmStateUpdate, VmStepSummary, VmTransferArm, VmUnaryOp, VmValueExpr, collect_symbolic_function_facts, collect_symbolic_function_facts_with_scope, - compile_function_semantics_with_scope, compile_semantic_artifact_with_scope, stable_scope_hash, + compile_function_semantics_with_scope, compile_semantic_artifact_default_with_scope, + compile_semantic_artifact_with_scope, stable_scope_hash, }; pub use sim::{ CallConv, CallInfo, DerivedFunctionSummary, DerivedSummaryCase, DerivedSummaryCompletion, diff --git a/crates/r2sym/src/query.rs b/crates/r2sym/src/query.rs index b7a48b4..9051d79 100644 --- a/crates/r2sym/src/query.rs +++ b/crates/r2sym/src/query.rs @@ -7,17 +7,20 @@ use std::collections::{BTreeMap, BTreeSet, VecDeque}; use z3::Context; -use z3::ast::Ast; +use z3::ast::{Ast, BV, Bool}; use r2ssa::SsaArtifact; use crate::SymState; use crate::backward::{ BackwardConditionPrecision, BackwardConditionSummary, CompiledBackwardCondition, - compile_target_precondition_with_summaries, compile_value_postcondition_with_summaries, + compile_branch_precondition_with_summaries, compile_target_precondition_with_summaries, + compile_value_postcondition_with_summaries, }; use crate::path::{ExploreConfig, ExploreStats, PathExplorer, PathResult, SolvedPath}; -use crate::semantics::{VmStepSummary, build_vm_step_summary, classify_interpreter_like}; +use crate::semantics::{ + CompiledSemanticArtifact, VmStepSummary, build_vm_step_summary, classify_interpreter_like, +}; use crate::sim::SummaryProfile; use crate::solver::{SatResult, SolverStats}; use crate::state::ExitStatus; @@ -221,6 +224,12 @@ enum PreconditionApplication<'ctx> { }, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum CompiledPreconditionMode { + Necessary, + NarrowOnly, +} + fn precision_rank(precision: BackwardConditionPrecision) -> u8 { match precision { BackwardConditionPrecision::Exact => 3, @@ -484,10 +493,109 @@ fn prefer_compiled_precondition( candidate.summary.total_paths < current.summary.total_paths } -fn apply_compiled_precondition<'ctx>( +fn parse_const_u64_text(text: &str) -> Option { + let trimmed = text.trim(); + if let Some(hex) = trimmed.strip_prefix("0x") { + return u64::from_str_radix(hex, 16).ok(); + } + if let Some(hex) = trimmed.strip_prefix("const:") { + return u64::from_str_radix(hex, 16).ok(); + } + trimmed.parse::().ok() +} + +fn constrain_region_backed_memory_term<'ctx>( + narrowed: &mut SymState<'ctx>, + term: &crate::backward::BackwardMemoryCondition, + rhs: u64, +) -> bool { + if !term.exact_value { + return false; + } + let (base_addr, offset_values) = match &term.region { + crate::backward::BackwardMemoryRegion::Region(region) + if term.offset_hi >= term.offset_lo => + { + let Some(def) = narrowed.memory.region_def(region.id) else { + return false; + }; + let Some(base_addr) = def.base_addr else { + return false; + }; + let offset_values = if term.exact_offset || term.offset_lo == term.offset_hi { + vec![term.offset_lo] + } else { + let span = term.offset_hi - term.offset_lo; + if span > 8 { + return false; + } + (term.offset_lo..=term.offset_hi).collect::>() + }; + (base_addr, offset_values) + } + _ => return false, + }; + let mut predicates = Vec::new(); + for offset in offset_values { + let Some(addr) = base_addr.checked_add_signed(offset) else { + continue; + }; + let value = narrowed.mem_read(&crate::SymValue::concrete(addr, 64), term.size); + predicates.push( + value + .to_bv(narrowed.context()) + .eq(BV::from_u64(rhs, value.bits())), + ); + } + match predicates.as_slice() { + [] => false, + [single] => { + narrowed.add_constraint(single.clone()); + true + } + _ => { + let refs = predicates.iter().collect::>(); + narrowed.add_constraint(Bool::or(&refs)); + true + } + } +} + +fn apply_memory_term_narrowing<'ctx>( + state: &SymState<'ctx>, + terms: &[&crate::backward::BackwardMemoryCondition], +) -> Option>> { + let mut narrowed = state.fork(); + let mut applied = 0usize; + for term in terms { + if !term.evidence().allows_narrowing() || !term.exact_value { + continue; + } + let Some(value_expr) = term.value_expr.as_deref() else { + continue; + }; + let Some(rhs) = parse_const_u64_text(value_expr) else { + continue; + }; + if let Some(binding) = term.binding.as_ref() + && let Some(value) = narrowed.symbolic_inputs().get(binding).cloned() + { + narrowed.constrain_eq(&value, rhs); + applied += 1; + continue; + } + if constrain_region_backed_memory_term(&mut narrowed, term, rhs) { + applied += 1; + } + } + (applied > 0).then_some(Box::new(narrowed)) +} + +fn apply_compiled_precondition_with_mode<'ctx>( explorer: &PathExplorer<'ctx>, mut initial_state: SymState<'ctx>, compiled: crate::CompiledBackwardCondition, + mode: CompiledPreconditionMode, ) -> PreconditionApplication<'ctx> { let summary = compiled.summary.clone(); let evidence = summary.evidence(); @@ -495,11 +603,15 @@ fn apply_compiled_precondition<'ctx>( .solver() .sat_with_constraint(&initial_state, &compiled.predicate) { - SatResult::Unsat if evidence.allows_hard_proof() => PreconditionApplication::ExactUnsat { - compiled_precondition: summary, - }, + SatResult::Unsat + if mode == CompiledPreconditionMode::Necessary && evidence.allows_hard_proof() => + { + PreconditionApplication::ExactUnsat { + compiled_precondition: summary, + } + } SatResult::Sat => { - if evidence.allows_hard_proof() { + if mode == CompiledPreconditionMode::Necessary && evidence.allows_hard_proof() { initial_state.add_constraint(compiled.predicate); PreconditionApplication::Continue { initial_state: Box::new(initial_state), @@ -551,17 +663,90 @@ where fn apply_best_compiled_precondition<'ctx>( explorer: &PathExplorer<'ctx>, func: &SsaArtifact, + artifact: Option<&CompiledSemanticArtifact>, initial_state: SymState<'ctx>, target_addr: u64, ) -> PreconditionApplication<'ctx> { let derived_summaries = explorer.derived_call_summary_views(); - let mut compiled = compile_target_precondition_with_summaries( - func, - &initial_state, - target_addr, - &derived_summaries, - ); - if compiled.as_ref().is_none_or(|compiled| { + let condition_source = + artifact.and_then(|artifact| artifact.actionable_condition_source_for_target(target_addr)); + let memory_terms = artifact + .map(|artifact| artifact.actionable_memory_terms_for_target(target_addr)) + .unwrap_or_default(); + let worker_island = + artifact.and_then(|artifact| artifact.best_worker_island_for_target(target_addr, false)); + let source_is_necessary = condition_source.is_some_and(|source| { + source.necessary_for_target && source.summary.evidence().allows_narrowing() + }); + let island_mode = worker_island.and_then(|island| { + if island.exact_reachable_target() == Some(target_addr) { + Some(CompiledPreconditionMode::Necessary) + } else if island.actionable_reachable_target() == Some(target_addr) { + Some(CompiledPreconditionMode::NarrowOnly) + } else { + island + .actionable_compiled_condition() + .map(|_| CompiledPreconditionMode::NarrowOnly) + } + }); + if !source_is_necessary + && matches!(island_mode, Some(CompiledPreconditionMode::NarrowOnly)) + && let Some(narrowed_state) = apply_memory_term_narrowing(&initial_state, &memory_terms) + { + return PreconditionApplication::Continue { + initial_state: Box::new(initial_state), + narrowed_state: Some(narrowed_state), + compiled_precondition: worker_island + .and_then(|island| island.actionable_compiled_condition()) + .cloned() + .or_else(|| condition_source.map(|source| source.summary.clone())), + }; + } + let mut compiled = condition_source.and_then(|source| { + let compiled = compile_branch_precondition_with_summaries( + func, + &initial_state, + source.block_addr, + source.branch_truth, + &derived_summaries, + )?; + Some(( + compiled, + if source.necessary_for_target { + CompiledPreconditionMode::Necessary + } else { + CompiledPreconditionMode::NarrowOnly + }, + )) + }); + let mut used_target_compile = false; + if compiled.is_none() + && let Some(mode) = island_mode + && let Some(target_compiled) = compile_target_precondition_with_summaries( + func, + &initial_state, + target_addr, + &derived_summaries, + ) + { + compiled = Some((target_compiled, mode)); + used_target_compile = true; + } + if !source_is_necessary + && !used_target_compile + && let Some(target_compiled) = compile_target_precondition_with_summaries( + func, + &initial_state, + target_addr, + &derived_summaries, + ) + && compiled.as_ref().is_none_or(|(current, _)| { + prefer_compiled_precondition(Some(current), &target_compiled) + }) + { + compiled = Some((target_compiled, CompiledPreconditionMode::Necessary)); + } + if compiled.as_ref().is_none_or(|(compiled, _)| { !matches!( compiled.summary.precision, BackwardConditionPrecision::Exact @@ -571,18 +756,36 @@ fn apply_best_compiled_precondition<'ctx>( &initial_state, target_addr, &derived_summaries, - ) && prefer_compiled_precondition(compiled.as_ref(), &vm_compiled) - { - compiled = Some(vm_compiled); + ) && prefer_compiled_precondition( + compiled.as_ref().map(|(compiled, _)| compiled), + &vm_compiled, + ) { + compiled = Some((vm_compiled, CompiledPreconditionMode::Necessary)); } - let Some(compiled) = compiled else { + let Some((compiled, mode)) = compiled else { + let narrowed_state = apply_memory_term_narrowing(&initial_state, &memory_terms); return PreconditionApplication::Continue { initial_state: Box::new(initial_state), - narrowed_state: None, + narrowed_state, compiled_precondition: None, }; }; - apply_compiled_precondition(explorer, initial_state, compiled) + match apply_compiled_precondition_with_mode(explorer, initial_state, compiled, mode) { + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition, + } => { + let narrowed_state = narrowed_state + .or_else(|| apply_memory_term_narrowing(initial_state.as_ref(), &memory_terms)); + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition, + } + } + unsat => unsat, + } } impl<'ctx> PathExplorer<'ctx> { @@ -593,25 +796,40 @@ impl<'ctx> PathExplorer<'ctx> { initial_state: SymState<'ctx>, target_addr: u64, ) -> ReachabilityResult<'ctx> { - let (paths, compiled_precondition) = - match apply_best_compiled_precondition(self, func, initial_state, target_addr) { - PreconditionApplication::Continue { - initial_state, + self.can_reach_with_artifact(func, None, initial_state, target_addr) + } + + pub fn can_reach_with_artifact( + &mut self, + func: &SsaArtifact, + artifact: Option<&CompiledSemanticArtifact>, + initial_state: SymState<'ctx>, + target_addr: u64, + ) -> ReachabilityResult<'ctx> { + let (paths, compiled_precondition) = match apply_best_compiled_precondition( + self, + func, + artifact, + initial_state, + target_addr, + ) { + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition, + } => ( + find_paths_with_compiled_narrowing( + self, + *initial_state, narrowed_state, - compiled_precondition, - } => ( - find_paths_with_compiled_narrowing( - self, - *initial_state, - narrowed_state, - |explorer, state| explorer.find_paths_to(func, state, target_addr), - ), - compiled_precondition, + |explorer, state| explorer.find_paths_to(func, state, target_addr), ), - PreconditionApplication::ExactUnsat { - compiled_precondition, - } => (Vec::new(), Some(compiled_precondition)), - }; + compiled_precondition, + ), + PreconditionApplication::ExactUnsat { + compiled_precondition, + } => (Vec::new(), Some(compiled_precondition)), + }; let stats = self.stats().clone(); let solver_stats = self.solver().stats(); let status = if !paths.is_empty() { @@ -638,25 +856,40 @@ impl<'ctx> PathExplorer<'ctx> { initial_state: SymState<'ctx>, target_pc: u64, ) -> PathConditionResult<'ctx> { - let (matching_paths, compiled_precondition) = - match apply_best_compiled_precondition(self, func, initial_state, target_pc) { - PreconditionApplication::Continue { - initial_state, + self.path_conditions_at_with_artifact(func, None, initial_state, target_pc) + } + + pub fn path_conditions_at_with_artifact( + &mut self, + func: &SsaArtifact, + artifact: Option<&CompiledSemanticArtifact>, + initial_state: SymState<'ctx>, + target_pc: u64, + ) -> PathConditionResult<'ctx> { + let (matching_paths, compiled_precondition) = match apply_best_compiled_precondition( + self, + func, + artifact, + initial_state, + target_pc, + ) { + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition, + } => ( + find_paths_with_compiled_narrowing( + self, + *initial_state, narrowed_state, - compiled_precondition, - } => ( - find_paths_with_compiled_narrowing( - self, - *initial_state, - narrowed_state, - |explorer, state| explorer.find_paths_to(func, state, target_pc), - ), - compiled_precondition, + |explorer, state| explorer.find_paths_to(func, state, target_pc), ), - PreconditionApplication::ExactUnsat { - compiled_precondition, - } => (Vec::new(), Some(compiled_precondition)), - }; + compiled_precondition, + ), + PreconditionApplication::ExactUnsat { + compiled_precondition, + } => (Vec::new(), Some(compiled_precondition)), + }; let stats = self.stats().clone(); let solver_stats = self.solver().stats(); let conditions = matching_paths.iter().map(condition_summary).collect(); @@ -677,9 +910,20 @@ impl<'ctx> PathExplorer<'ctx> { func: &SsaArtifact, initial_state: SymState<'ctx>, target_addr: u64, + ) -> SolveResult<'ctx> { + self.solve_for_target_with_artifact(func, None, initial_state, target_addr) + } + + pub fn solve_for_target_with_artifact( + &mut self, + func: &SsaArtifact, + artifact: Option<&CompiledSemanticArtifact>, + initial_state: SymState<'ctx>, + target_addr: u64, ) -> SolveResult<'ctx> { let (matched_paths, compiled_precondition, exact_unsat) = - match apply_best_compiled_precondition(self, func, initial_state, target_addr) { + match apply_best_compiled_precondition(self, func, artifact, initial_state, target_addr) + { PreconditionApplication::Continue { initial_state, narrowed_state, @@ -758,16 +1002,22 @@ mod tests { use std::collections::BTreeMap; use super::{ - PreconditionApplication, apply_best_compiled_precondition, apply_compiled_precondition, - prefer_compiled_precondition, vm_target_case_values, + CompiledPreconditionMode, PreconditionApplication, apply_best_compiled_precondition, + apply_compiled_precondition_with_mode, prefer_compiled_precondition, vm_target_case_values, }; use crate::{ - BackwardConditionPrecision, BackwardConditionSummary, SymQueryConfig, SymState, VmBinaryOp, + BackwardConditionPrecision, BackwardConditionSummary, BackwardMemoryCondition, + BackwardMemoryRegion, CompiledSemanticArtifact, ResidualReason, SatResult, + SemanticCapability, SemanticEvidence, SemanticEvidenceReason, SliceClass, SymQueryConfig, + SymState, SymbolicBranchFact, SymbolicControlFact, SymbolicControlIslandKind, + SymbolicFunctionFactDiagnostics, SymbolicFunctionFacts, SymbolicMemoryIsland, + SymbolicMemoryIslandKind, SymbolicReachabilityStatus, SymbolicWorkerIsland, VmBinaryOp, VmGuardCondition, VmGuardedExit, VmStateUpdate, VmStepSummary, VmTransferArm, VmValueExpr, }; use r2il::{R2ILBlock, R2ILOp, SpaceId, Varnode}; use r2ssa::SsaArtifact; use z3::Context; + use z3::ast::BV; const RDI: u64 = 56; const TMP0: u64 = 0x80; @@ -894,7 +1144,7 @@ mod tests { let explorer = SymQueryConfig::default().make_explorer(&ctx); let original_constraints = state.num_constraints(); - match apply_best_compiled_precondition(&explorer, &func, state, 0x1010) { + match apply_best_compiled_precondition(&explorer, &func, None, state, 0x1010) { PreconditionApplication::Continue { initial_state, narrowed_state, @@ -937,7 +1187,12 @@ mod tests { }, }; - match apply_compiled_precondition(&explorer, state, compiled) { + match apply_compiled_precondition_with_mode( + &explorer, + state, + compiled, + CompiledPreconditionMode::Necessary, + ) { PreconditionApplication::Continue { initial_state, narrowed_state, @@ -957,6 +1212,524 @@ mod tests { } } + #[test] + fn exact_preconditions_in_narrow_only_mode_do_not_shortcut_unsat() { + let ctx = Context::thread_local(); + let explorer = SymQueryConfig::default().make_explorer(&ctx); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("reg:56_0", 64); + + let compiled = crate::CompiledBackwardCondition { + predicate: z3::ast::Bool::from_bool(false), + summary: BackwardConditionSummary { + simplified: "guard".to_string(), + terms: vec!["guard".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }, + }; + + match apply_compiled_precondition_with_mode( + &explorer, + state, + compiled, + CompiledPreconditionMode::NarrowOnly, + ) { + PreconditionApplication::Continue { + narrowed_state, + compiled_precondition, + .. + } => { + assert!(narrowed_state.is_none()); + assert_eq!( + compiled_precondition + .expect("compiled precondition") + .precision, + BackwardConditionPrecision::Exact + ); + } + PreconditionApplication::ExactUnsat { .. } => { + panic!("narrow-only exact precondition should not shortcut as exact unsat") + } + } + } + + #[test] + fn exact_preconditions_in_narrow_only_mode_seed_narrowed_state() { + let ctx = Context::thread_local(); + let explorer = SymQueryConfig::default().make_explorer(&ctx); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("reg:56_0", 64); + let original_constraints = state.num_constraints(); + + let compiled = crate::CompiledBackwardCondition { + predicate: z3::ast::Bool::from_bool(true), + summary: BackwardConditionSummary { + simplified: "guard".to_string(), + terms: vec!["guard".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }, + }; + + match apply_compiled_precondition_with_mode( + &explorer, + state, + compiled, + CompiledPreconditionMode::NarrowOnly, + ) { + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition, + } => { + assert_eq!(initial_state.num_constraints(), original_constraints); + assert_eq!( + narrowed_state.expect("narrowed state").num_constraints(), + original_constraints + 1 + ); + assert_eq!( + compiled_precondition + .expect("compiled precondition") + .precision, + BackwardConditionPrecision::Exact + ); + } + PreconditionApplication::ExactUnsat { .. } => { + panic!("narrow-only exact precondition should not shortcut as exact unsat") + } + } + } + + #[test] + fn artifact_memory_terms_seed_narrowed_state_without_branch_recompile() { + let blocks = make_residual_precondition_blocks(); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); + let ctx = Context::thread_local(); + let explorer = SymQueryConfig::default().make_explorer(&ctx); + let mut state = SymState::new(&ctx, 0x1000); + state.new_symbolic_input("sym_mem", 8); + let original_constraints = state.num_constraints(); + + let artifact = CompiledSemanticArtifact { + mode: crate::SemanticMode::Residual, + slice_class: SliceClass::Worker, + capability: SemanticCapability { + query_ready: true, + type_ready: true, + decompile_ready: false, + }, + residual_reasons: vec![ResidualReason::LargeCfg], + closure_functions: 0, + helper_functions: 0, + derived_summaries: 0, + derived_diagnostics: crate::sim::DerivedSummaryDiagnostics::default(), + symbolic_facts: SymbolicFunctionFacts { + branch_facts: vec![SymbolicBranchFact { + block_addr: 0xdead, + true_target: 0x2000, + false_target: 0x2010, + true_status: SymbolicReachabilityStatus::Reachable, + false_status: SymbolicReachabilityStatus::Unreachable, + true_condition: Some("guard".to_string()), + false_condition: None, + true_compiled: Some(BackwardConditionSummary { + simplified: "guard".to_string(), + terms: vec!["guard".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }), + false_compiled: None, + }], + worker_islands: Vec::new(), + control_islands: Vec::new(), + memory_islands: vec![SymbolicMemoryIsland { + kind: SymbolicMemoryIslandKind::LargeCfgConditionFrontier, + anchor_block: 0xdead, + terms: vec![BackwardMemoryCondition { + region: BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: SemanticEvidence::exact(), + binding: Some("sym_mem".to_string()), + expr: "0x2a".to_string(), + value_expr: Some("0x2a".to_string()), + exact_value: true, + }], + evidence: SemanticEvidence::exact(), + }], + diagnostics: SymbolicFunctionFactDiagnostics::default(), + }, + interpreter: None, + vm_step: None, + vm_transfer: None, + cache_hit: false, + }; + + match apply_best_compiled_precondition(&explorer, &func, Some(&artifact), state, 0x2000) { + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition, + } => { + assert!(compiled_precondition.is_none()); + assert_eq!(initial_state.num_constraints(), original_constraints); + let narrowed_state = narrowed_state.expect("narrowed state"); + assert_eq!(narrowed_state.num_constraints(), original_constraints + 1); + let narrowed_value = narrowed_state + .symbolic_inputs() + .get("sym_mem") + .expect("symbolic input") + .to_bv(&ctx); + assert!(matches!( + explorer.solver().sat_with_constraint( + &narrowed_state, + &narrowed_value.eq(BV::from_u64(0x2a, 8)) + ), + SatResult::Sat + )); + assert!(matches!( + explorer.solver().sat_with_constraint( + &narrowed_state, + &narrowed_value.eq(BV::from_u64(0x2b, 8)) + ), + SatResult::Unsat + )); + } + PreconditionApplication::ExactUnsat { .. } => { + panic!("memory-term-only narrowing should not shortcut as exact unsat") + } + } + } + + #[test] + fn artifact_region_memory_terms_seed_narrowed_state_without_symbolic_binding() { + let blocks = make_residual_precondition_blocks(); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); + let ctx = Context::thread_local(); + let explorer = SymQueryConfig::default().make_explorer(&ctx); + let mut state = SymState::new(&ctx, 0x1000); + let symbolic_byte = state.new_symbolic_input("global_byte", 8); + let global_region = state.define_memory_region( + crate::MemoryRegionKind::Global, + "ram:0x2000", + Some(0x2000), + Some(1), + ); + state.mem_write(&crate::SymValue::concrete(0x2000, 64), &symbolic_byte, 1); + let original_constraints = state.num_constraints(); + + let artifact = CompiledSemanticArtifact { + mode: crate::SemanticMode::Residual, + slice_class: SliceClass::Worker, + capability: SemanticCapability { + query_ready: true, + type_ready: true, + decompile_ready: false, + }, + residual_reasons: vec![ResidualReason::LargeCfg], + closure_functions: 0, + helper_functions: 0, + derived_summaries: 0, + derived_diagnostics: crate::sim::DerivedSummaryDiagnostics::default(), + symbolic_facts: SymbolicFunctionFacts { + branch_facts: vec![SymbolicBranchFact { + block_addr: 0xdead, + true_target: 0x2000, + false_target: 0x2010, + true_status: SymbolicReachabilityStatus::Reachable, + false_status: SymbolicReachabilityStatus::Unreachable, + true_condition: Some("guard".to_string()), + false_condition: None, + true_compiled: None, + false_compiled: None, + }], + worker_islands: Vec::new(), + control_islands: Vec::new(), + memory_islands: vec![SymbolicMemoryIsland { + kind: SymbolicMemoryIslandKind::LargeCfgConditionFrontier, + anchor_block: 0xdead, + terms: vec![BackwardMemoryCondition { + region: BackwardMemoryRegion::Region(crate::backward::BackwardRegionRef { + id: global_region, + kind: crate::MemoryRegionKind::Global, + name: "ram:0x2000".to_string(), + }), + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: SemanticEvidence::exact(), + binding: None, + expr: "*(ram:0x2000 + 0)".to_string(), + value_expr: Some("0x2a".to_string()), + exact_value: true, + }], + evidence: SemanticEvidence::exact(), + }], + diagnostics: SymbolicFunctionFactDiagnostics::default(), + }, + interpreter: None, + vm_step: None, + vm_transfer: None, + cache_hit: false, + }; + + match apply_best_compiled_precondition(&explorer, &func, Some(&artifact), state, 0x2000) { + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition, + } => { + assert!(compiled_precondition.is_none()); + assert_eq!(initial_state.num_constraints(), original_constraints); + let narrowed_state = narrowed_state.expect("narrowed state"); + assert_eq!(narrowed_state.num_constraints(), original_constraints + 1); + let narrowed_value = narrowed_state + .mem_read(&crate::SymValue::concrete(0x2000, 64), 1) + .to_bv(&ctx); + assert!(matches!( + explorer.solver().sat_with_constraint( + &narrowed_state, + &narrowed_value.eq(BV::from_u64(0x2a, 8)) + ), + SatResult::Sat + )); + assert!(matches!( + explorer.solver().sat_with_constraint( + &narrowed_state, + &narrowed_value.eq(BV::from_u64(0x2b, 8)) + ), + SatResult::Unsat + )); + } + PreconditionApplication::ExactUnsat { .. } => { + panic!("region-backed memory-term narrowing should not shortcut as exact unsat") + } + } + } + + #[test] + fn worker_island_memory_terms_seed_narrowed_state_without_branch_recompile() { + let blocks = make_residual_precondition_blocks(); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); + let ctx = Context::thread_local(); + let explorer = SymQueryConfig::default().make_explorer(&ctx); + let mut state = SymState::new(&ctx, 0x1000); + state.new_symbolic_input("sym_mem", 8); + let original_constraints = state.num_constraints(); + + let actionable = BackwardConditionSummary { + simplified: "worker_guard".to_string(), + terms: vec!["worker_guard".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::OverApprox, + supported_paths: 1, + total_paths: 2, + }; + let artifact = CompiledSemanticArtifact { + mode: crate::SemanticMode::IslandCompiled, + slice_class: SliceClass::Worker, + capability: SemanticCapability { + query_ready: true, + type_ready: true, + decompile_ready: true, + }, + residual_reasons: Vec::new(), + closure_functions: 0, + helper_functions: 0, + derived_summaries: 0, + derived_diagnostics: crate::sim::DerivedSummaryDiagnostics::default(), + symbolic_facts: SymbolicFunctionFacts { + branch_facts: Vec::new(), + worker_islands: vec![SymbolicWorkerIsland { + anchor_block: 0xdead, + control_kind: Some(SymbolicControlIslandKind::LargeCfgBranchFrontier), + memory_kind: Some(SymbolicMemoryIslandKind::LargeCfgConditionFrontier), + frontier_targets: vec![0x2000, 0x2010], + control_facts: vec![SymbolicControlFact { + target: 0x2000, + status: SymbolicReachabilityStatus::Reachable, + condition: Some("worker_guard".to_string()), + compiled: Some(actionable.clone()), + evidence: SemanticEvidence::likely( + SemanticEvidenceReason::DerivedFromRanking, + ), + }], + memory_terms: vec![BackwardMemoryCondition { + region: BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: SemanticEvidence::exact(), + binding: Some("sym_mem".to_string()), + expr: "0x2a".to_string(), + value_expr: Some("0x2a".to_string()), + exact_value: true, + }], + evidence: SemanticEvidence::likely(SemanticEvidenceReason::DerivedFromRanking), + }], + control_islands: Vec::new(), + memory_islands: Vec::new(), + diagnostics: SymbolicFunctionFactDiagnostics::default(), + }, + interpreter: None, + vm_step: None, + vm_transfer: None, + cache_hit: false, + }; + + match apply_best_compiled_precondition(&explorer, &func, Some(&artifact), state, 0x2000) { + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition, + } => { + assert_eq!(initial_state.num_constraints(), original_constraints); + let narrowed_state = narrowed_state.expect("narrowed state"); + assert_eq!(narrowed_state.num_constraints(), original_constraints + 1); + assert_eq!( + compiled_precondition + .expect("worker island summary") + .precision, + BackwardConditionPrecision::OverApprox + ); + let narrowed_value = narrowed_state + .symbolic_inputs() + .get("sym_mem") + .expect("symbolic input") + .to_bv(&ctx); + assert!(matches!( + explorer.solver().sat_with_constraint( + &narrowed_state, + &narrowed_value.eq(BV::from_u64(0x2a, 8)) + ), + SatResult::Sat + )); + } + PreconditionApplication::ExactUnsat { .. } => { + panic!("worker-island memory narrowing should not shortcut as exact unsat") + } + } + } + + #[test] + fn necessary_worker_island_still_prefers_real_compiled_precondition() { + let blocks = make_residual_precondition_blocks(); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); + let ctx = Context::thread_local(); + let explorer = SymQueryConfig::default().make_explorer(&ctx); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("reg:56_0", 64); + let original_constraints = state.num_constraints(); + + let artifact = CompiledSemanticArtifact { + mode: crate::SemanticMode::IslandCompiled, + slice_class: SliceClass::Worker, + capability: SemanticCapability { + query_ready: true, + type_ready: true, + decompile_ready: true, + }, + residual_reasons: Vec::new(), + closure_functions: 0, + helper_functions: 0, + derived_summaries: 0, + derived_diagnostics: crate::sim::DerivedSummaryDiagnostics::default(), + symbolic_facts: SymbolicFunctionFacts { + branch_facts: Vec::new(), + worker_islands: vec![SymbolicWorkerIsland { + anchor_block: 0x1000, + control_kind: Some(SymbolicControlIslandKind::LargeCfgBranchFrontier), + memory_kind: Some(SymbolicMemoryIslandKind::LargeCfgConditionFrontier), + frontier_targets: vec![0x1010, 0x1004], + control_facts: vec![ + SymbolicControlFact { + target: 0x1010, + status: SymbolicReachabilityStatus::Reachable, + condition: Some("guard".to_string()), + compiled: Some(BackwardConditionSummary { + simplified: "guard".to_string(), + terms: vec!["guard".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }), + evidence: SemanticEvidence::exact(), + }, + SymbolicControlFact { + target: 0x1004, + status: SymbolicReachabilityStatus::Unreachable, + condition: None, + compiled: None, + evidence: SemanticEvidence::exact(), + }, + ], + memory_terms: vec![BackwardMemoryCondition { + region: BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: SemanticEvidence::exact(), + binding: Some("reg:56_0".to_string()), + expr: "0x1".to_string(), + value_expr: Some("0x1".to_string()), + exact_value: true, + }], + evidence: SemanticEvidence::exact(), + }], + control_islands: Vec::new(), + memory_islands: Vec::new(), + diagnostics: SymbolicFunctionFactDiagnostics::default(), + }, + interpreter: None, + vm_step: None, + vm_transfer: None, + cache_hit: false, + }; + + match apply_best_compiled_precondition(&explorer, &func, Some(&artifact), state, 0x1010) { + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition, + } => { + assert!(initial_state.num_constraints() >= original_constraints); + assert!(narrowed_state.is_none()); + assert!(compiled_precondition.is_some()); + } + PreconditionApplication::ExactUnsat { .. } => { + panic!("necessary worker islands should use the real compiled precondition path") + } + } + } + #[test] fn vm_target_case_values_include_redispatch_cases() { let vm_step = make_vm_step_summary_with_transfers(vec![ diff --git a/crates/r2sym/src/semantics/artifact.rs b/crates/r2sym/src/semantics/artifact.rs index 518f5ef..21132f3 100644 --- a/crates/r2sym/src/semantics/artifact.rs +++ b/crates/r2sym/src/semantics/artifact.rs @@ -2,13 +2,16 @@ use serde::{Deserialize, Serialize}; use crate::sim::DerivedSummaryDiagnostics; -use super::facts::SymbolicFunctionFacts; +use crate::backward::BackwardMemoryCondition; + +use super::facts::{SymbolicFunctionFacts, SymbolicTargetConditionSource, SymbolicWorkerIsland}; use super::vm::{InterpreterDispatchSummary, VmStepSummary}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum SemanticMode { Raw, Compiled, + IslandCompiled, Residual, VmSummary, } @@ -243,4 +246,44 @@ pub struct CompiledSemanticArtifact { pub cache_hit: bool, } +impl CompiledSemanticArtifact { + pub fn actionable_condition_source_for_target( + &self, + target_addr: u64, + ) -> Option> { + self.symbolic_facts + .actionable_condition_source_for_target(target_addr) + } + + pub fn exact_condition_source_for_target( + &self, + target_addr: u64, + ) -> Option> { + self.symbolic_facts + .exact_condition_source_for_target(target_addr) + } + + pub fn actionable_memory_terms_for_target( + &self, + target_addr: u64, + ) -> Vec<&BackwardMemoryCondition> { + self.symbolic_facts + .actionable_memory_terms_for_target(target_addr) + } + + pub fn exact_memory_terms_for_target(&self, target_addr: u64) -> Vec<&BackwardMemoryCondition> { + self.symbolic_facts + .exact_memory_terms_for_target(target_addr) + } + + pub fn best_worker_island_for_target( + &self, + target_addr: u64, + hard_proof_only: bool, + ) -> Option<&SymbolicWorkerIsland> { + self.symbolic_facts + .best_worker_island_for_target(target_addr, hard_proof_only) + } +} + pub type CompiledFunctionSemantics = CompiledSemanticArtifact; diff --git a/crates/r2sym/src/semantics/compiler.rs b/crates/r2sym/src/semantics/compiler.rs index c568504..c20205e 100644 --- a/crates/r2sym/src/semantics/compiler.rs +++ b/crates/r2sym/src/semantics/compiler.rs @@ -50,6 +50,20 @@ fn residual_reasons( reasons } +fn normalized_residual_reasons( + mode: SemanticMode, + reasons: Vec, +) -> Vec { + if matches!(mode, SemanticMode::IslandCompiled | SemanticMode::VmSummary) { + reasons + .into_iter() + .filter(|reason| !matches!(reason, ResidualReason::LargeCfg)) + .collect() + } else { + reasons + } +} + fn semantic_mode_for( helper_functions: usize, derived_summaries: usize, @@ -67,6 +81,9 @@ fn semantic_mode_for( if derived_summaries > 0 && !facts.diagnostics.skipped_large_cfg { return SemanticMode::Compiled; } + if facts.diagnostics.skipped_large_cfg && has_island_compiled_semantics(facts) { + return SemanticMode::IslandCompiled; + } if helper_functions > 0 || facts.diagnostics.skipped_large_cfg || diagnostics.budget_exhausted > 0 @@ -80,29 +97,93 @@ fn semantic_mode_for( SemanticMode::Raw } -fn semantic_capability(mode: SemanticMode, facts: &SymbolicFunctionFacts) -> SemanticCapability { - let has_bounded_branch_facts = !facts.branch_facts.is_empty(); - let has_actionable_branch_frontier = facts - .branch_facts +fn worker_island_supports_actionable_semantics( + island: &super::facts::SymbolicWorkerIsland, +) -> bool { + let supporting_condition = worker_island_supporting_compiled_condition(island); + let has_unique_target = + island.exact_reachable_target().is_some() || island.actionable_reachable_target().is_some(); + let has_condition = supporting_condition.is_some(); + let has_memory_support = !island.actionable_memory_terms().is_empty() + || supporting_condition.is_some_and(|compiled| !compiled.memory_terms.is_empty()); + island.evidence.allows_narrowing() && has_unique_target && has_condition && has_memory_support +} + +fn worker_island_supports_structured_semantics( + island: &super::facts::SymbolicWorkerIsland, +) -> bool { + let supporting_condition = worker_island_supporting_compiled_condition(island); + worker_island_supports_actionable_semantics(island) + && supporting_condition.is_some_and(|compiled| { + !matches!( + compiled.precision, + crate::backward::BackwardConditionPrecision::ResidualSearchRequired + ) + }) +} + +fn worker_island_supporting_compiled_condition( + island: &super::facts::SymbolicWorkerIsland, +) -> Option<&crate::backward::BackwardConditionSummary> { + let reachable_target = island + .exact_reachable_target() + .or_else(|| island.actionable_reachable_target())?; + island + .control_facts .iter() - .any(|fact| fact.true_compiled.is_some() || fact.false_compiled.is_some()); - let has_actionable_control_islands = facts - .control_islands + .find(|fact| fact.target == reachable_target) + .and_then(super::facts::SymbolicControlFact::actionable_compiled_condition) +} + +fn has_island_compiled_semantics(facts: &SymbolicFunctionFacts) -> bool { + facts + .worker_islands .iter() - .any(|island| island.actionable_compiled_condition().is_some()); - let large_cfg_actionable_decompile = facts.diagnostics.skipped_large_cfg - && (has_actionable_branch_frontier || has_actionable_control_islands) - && facts.branch_facts.len() <= 4; + .any(worker_island_supports_actionable_semantics) + || facts.branch_facts.iter().any(|fact| { + fact.actionable_compiled_condition().is_some() + && (fact.exact_reachable_target().is_some() + || fact.actionable_reachable_target().is_some()) + && (!facts + .actionable_memory_terms_for_block(fact.block_addr) + .is_empty() + || fact + .actionable_compiled_condition() + .is_some_and(|compiled| !compiled.memory_terms.is_empty())) + }) +} + +fn has_structured_worker_semantics(facts: &SymbolicFunctionFacts) -> bool { + facts + .worker_islands + .iter() + .any(worker_island_supports_structured_semantics) +} + +fn semantic_capability(mode: SemanticMode, facts: &SymbolicFunctionFacts) -> SemanticCapability { + let has_bounded_branch_facts = !facts.branch_facts.is_empty(); + let large_cfg_semantic_decompile = + facts.diagnostics.skipped_large_cfg && has_island_compiled_semantics(facts); + let large_cfg_structured_decompile = + facts.diagnostics.skipped_large_cfg && has_structured_worker_semantics(facts); match mode { SemanticMode::Raw | SemanticMode::Compiled => SemanticCapability { query_ready: true, type_ready: true, decompile_ready: true, }, + SemanticMode::IslandCompiled => SemanticCapability { + query_ready: true, + type_ready: true, + decompile_ready: large_cfg_semantic_decompile, + }, SemanticMode::Residual => SemanticCapability { query_ready: true, - type_ready: !facts.diagnostics.skipped_large_cfg || has_bounded_branch_facts, - decompile_ready: large_cfg_actionable_decompile, + type_ready: !facts.diagnostics.skipped_large_cfg + || has_bounded_branch_facts + || !facts.worker_islands.is_empty() + || !facts.memory_islands.is_empty(), + decompile_ready: large_cfg_structured_decompile, }, SemanticMode::VmSummary => SemanticCapability { query_ready: true, @@ -231,11 +312,14 @@ fn compile_function_semantics_uncached( mode, slice_class, capability: semantic_capability(mode, &symbolic_facts), - residual_reasons: residual_reasons( - &derived_diagnostics, - &symbolic_facts, - interpreter.is_some(), - vm_step.is_some(), + residual_reasons: normalized_residual_reasons( + mode, + residual_reasons( + &derived_diagnostics, + &symbolic_facts, + interpreter.is_some(), + vm_step.is_some(), + ), ), closure_functions, helper_functions, @@ -297,11 +381,14 @@ fn compile_function_semantics_uncached( mode, slice_class, capability: semantic_capability(mode, &symbolic_facts), - residual_reasons: residual_reasons( - &derived_diagnostics, - &symbolic_facts, - interpreter.is_some(), - vm_step.is_some(), + residual_reasons: normalized_residual_reasons( + mode, + residual_reasons( + &derived_diagnostics, + &symbolic_facts, + interpreter.is_some(), + vm_step.is_some(), + ), ), closure_functions, helper_functions, @@ -399,6 +486,23 @@ pub fn compile_semantic_artifact_with_scope( compile_function_semantics_with_scope(ctx, func, scope, arch, symbol_map, summary_profile) } +pub fn compile_semantic_artifact_default_with_scope( + ctx: &Context, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + arch: Option<&ArchSpec>, +) -> CompiledSemanticArtifact { + let rebound_scope = scope.and_then(|scope| scope.with_prepared_root(func)); + compile_semantic_artifact_with_scope( + ctx, + func, + rebound_scope.as_ref(), + arch, + &HashMap::new(), + SummaryProfile::Default, + ) +} + #[cfg(test)] mod tests { use std::collections::HashMap; @@ -947,6 +1051,15 @@ mod tests { .get(&0x3008) .is_some_and(|updates| !updates.is_empty()) ); + assert!( + vm_step + .handler_state_updates + .get(&0x3008) + .is_some_and(|updates| updates.iter().any(|update| { + update.exact + && matches!(update.value, crate::semantics::vm::VmValueExpr::Var(_)) + })) + ); assert!(vm_step.redispatch_handlers.contains(&0x3008)); assert!(vm_step.redispatch_handlers.contains(&0x3010)); assert!(vm_step.redispatch_handlers.contains(&0x3014)); @@ -976,6 +1089,13 @@ mod tests { .iter() .any(|transfer| transfer.handler_target == 0x3008 && transfer.redispatch) ); + assert!(vm_step.transfers.iter().any(|transfer| { + transfer.handler_target == 0x3008 + && transfer.state_updates.iter().any(|update| { + update.exact + && matches!(update.value, crate::semantics::vm::VmValueExpr::Var(_)) + }) + })); assert!( vm_step .transfers @@ -1067,7 +1187,70 @@ mod tests { assert!(artifact.symbolic_facts.diagnostics.skipped_large_cfg); assert_eq!(artifact.symbolic_facts.branch_facts.len(), 1); assert!(artifact.capability.type_ready); - assert!(artifact.capability.decompile_ready); + assert!(!artifact.capability.decompile_ready); assert_eq!(artifact.symbolic_facts.diagnostics.branches_evaluated, 1); } + + #[test] + fn large_cfg_exact_branch_frontier_with_memory_support_is_decompile_ready() { + let fact = crate::semantics::facts::SymbolicBranchFact { + block_addr: 0x401000, + true_target: 0x401010, + false_target: 0x401020, + true_status: crate::semantics::facts::SymbolicReachabilityStatus::Reachable, + false_status: crate::semantics::facts::SymbolicReachabilityStatus::Unreachable, + true_condition: Some("flag == 0".to_string()), + false_condition: Some("flag != 0".to_string()), + true_compiled: Some(crate::backward::BackwardConditionSummary { + simplified: "flag == 0".to_string(), + terms: vec!["flag == 0".to_string()], + memory_terms: vec![crate::backward::BackwardMemoryCondition { + region: crate::backward::BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: crate::SemanticEvidence::exact(), + binding: None, + expr: "*arg0".to_string(), + value_expr: Some("0x0:8".to_string()), + exact_value: true, + }], + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: crate::backward::BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }), + false_compiled: None, + }; + let mut facts = SymbolicFunctionFacts { + branch_facts: vec![fact], + ..Default::default() + }; + facts.diagnostics.skipped_large_cfg = true; + + let mode = semantic_mode_for( + 0, + 0, + &DerivedSummaryDiagnostics::default(), + &facts, + false, + false, + ); + assert_eq!(mode, SemanticMode::IslandCompiled); + let capability = semantic_capability(mode, &facts); + assert!(capability.query_ready); + assert!(capability.type_ready); + assert!(capability.decompile_ready); + let reasons = normalized_residual_reasons( + mode, + residual_reasons(&DerivedSummaryDiagnostics::default(), &facts, false, false), + ); + assert!( + !reasons.contains(&ResidualReason::LargeCfg), + "island-compiled workers should keep large_cfg as provenance, not a residual reason" + ); + } } diff --git a/crates/r2sym/src/semantics/facts.rs b/crates/r2sym/src/semantics/facts.rs index 34c3267..c5fe723 100644 --- a/crates/r2sym/src/semantics/facts.rs +++ b/crates/r2sym/src/semantics/facts.rs @@ -1,13 +1,13 @@ -use std::collections::{BTreeSet, HashMap, VecDeque}; +use std::collections::{BTreeMap, BTreeSet, HashMap, VecDeque}; use r2il::ArchSpec; -use r2ssa::SsaArtifact; +use r2ssa::{InterprocFunctionId, SsaArtifact}; use serde::{Deserialize, Serialize}; use z3::Context; use crate::SymState; use crate::backward::{ - BackwardConditionPrecision, BackwardConditionSummary, + BackwardConditionPrecision, BackwardConditionSummary, BackwardMemoryCondition, compile_branch_precondition_with_summaries, }; use crate::path::{ExploreConfig, PathExplorer}; @@ -16,7 +16,10 @@ use crate::semantics::{ SemanticConfidence, SemanticEvidence, SemanticEvidenceCoverage, SemanticEvidenceProvenance, SemanticEvidenceReason, }; -use crate::sim::{DerivedSummarySet, PreparedFunctionScope, SummaryProfile, SummaryRegistry}; +use crate::sim::{ + DerivedSummaryCompletion, DerivedSummarySet, PreparedFunctionScope, SummaryProfile, + SummaryRegistry, +}; use crate::solver::SatResult; #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] @@ -41,12 +44,86 @@ pub struct SymbolicBranchFact { pub false_compiled: Option, } +impl SymbolicBranchFact { + pub fn exact_reachable_target(&self) -> Option { + match (self.true_status, self.false_status) { + (SymbolicReachabilityStatus::Reachable, SymbolicReachabilityStatus::Unreachable) => { + Some(self.true_target) + } + (SymbolicReachabilityStatus::Unreachable, SymbolicReachabilityStatus::Reachable) => { + Some(self.false_target) + } + _ => None, + } + } + + pub fn actionable_reachable_target(&self) -> Option { + match (self.true_status, self.false_status) { + (SymbolicReachabilityStatus::Reachable, SymbolicReachabilityStatus::Unreachable) + if self + .true_compiled + .as_ref() + .is_some_and(|compiled| compiled.evidence().allows_narrowing()) => + { + Some(self.true_target) + } + (SymbolicReachabilityStatus::Unreachable, SymbolicReachabilityStatus::Reachable) + if self + .false_compiled + .as_ref() + .is_some_and(|compiled| compiled.evidence().allows_narrowing()) => + { + Some(self.false_target) + } + _ => None, + } + } + + pub fn exact_compiled_condition(&self) -> Option<&BackwardConditionSummary> { + match (self.true_status, self.false_status) { + (SymbolicReachabilityStatus::Reachable, SymbolicReachabilityStatus::Unreachable) => { + self.true_compiled + .as_ref() + .filter(|compiled| compiled.evidence().allows_hard_proof()) + } + (SymbolicReachabilityStatus::Unreachable, SymbolicReachabilityStatus::Reachable) => { + self.false_compiled + .as_ref() + .filter(|compiled| compiled.evidence().allows_hard_proof()) + } + _ => None, + } + } + + pub fn actionable_compiled_condition(&self) -> Option<&BackwardConditionSummary> { + match (self.true_status, self.false_status) { + (SymbolicReachabilityStatus::Reachable, SymbolicReachabilityStatus::Unreachable) => { + self.true_compiled + .as_ref() + .filter(|compiled| compiled.evidence().allows_narrowing()) + } + (SymbolicReachabilityStatus::Unreachable, SymbolicReachabilityStatus::Reachable) => { + self.false_compiled + .as_ref() + .filter(|compiled| compiled.evidence().allows_narrowing()) + } + _ => None, + } + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum SymbolicControlIslandKind { BranchFrontier, LargeCfgBranchFrontier, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum SymbolicMemoryIslandKind { + ConditionFrontier, + LargeCfgConditionFrontier, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SymbolicControlFact { pub target: u64, @@ -95,6 +172,80 @@ impl SymbolicControlIsland { } } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SymbolicMemoryIsland { + pub kind: SymbolicMemoryIslandKind, + pub anchor_block: u64, + pub terms: Vec, + #[serde(default, skip_serializing_if = "SemanticEvidence::is_default_exact")] + pub evidence: SemanticEvidence, +} + +impl SymbolicMemoryIsland { + pub fn exact_terms(&self) -> Vec<&BackwardMemoryCondition> { + self.terms + .iter() + .filter(|term| term.evidence().allows_hard_proof()) + .collect() + } + + pub fn actionable_terms(&self) -> Vec<&BackwardMemoryCondition> { + self.terms + .iter() + .filter(|term| term.evidence().allows_narrowing()) + .collect() + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SymbolicWorkerIsland { + pub anchor_block: u64, + #[serde(skip_serializing_if = "Option::is_none")] + pub control_kind: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub memory_kind: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub frontier_targets: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub control_facts: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub memory_terms: Vec, + #[serde(default, skip_serializing_if = "SemanticEvidence::is_default_exact")] + pub evidence: SemanticEvidence, +} + +impl SymbolicWorkerIsland { + pub fn exact_reachable_target(&self) -> Option { + unique_reachable_control_target(self.control_facts.iter(), true) + } + + pub fn actionable_reachable_target(&self) -> Option { + unique_reachable_control_target(self.control_facts.iter(), false) + } + + pub fn exact_compiled_condition(&self) -> Option<&BackwardConditionSummary> { + unique_compiled_condition(self.control_facts.iter(), true) + } + + pub fn actionable_compiled_condition(&self) -> Option<&BackwardConditionSummary> { + unique_compiled_condition(self.control_facts.iter(), false) + } + + pub fn exact_memory_terms(&self) -> Vec<&BackwardMemoryCondition> { + self.memory_terms + .iter() + .filter(|term| term.evidence().allows_hard_proof()) + .collect() + } + + pub fn actionable_memory_terms(&self) -> Vec<&BackwardMemoryCondition> { + self.memory_terms + .iter() + .filter(|term| term.evidence().allows_narrowing()) + .collect() + } +} + #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct SymbolicFunctionFactDiagnostics { pub branches_evaluated: usize, @@ -108,10 +259,22 @@ pub struct SymbolicFunctionFactDiagnostics { pub struct SymbolicFunctionFacts { pub branch_facts: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub worker_islands: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] pub control_islands: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub memory_islands: Vec, pub diagnostics: SymbolicFunctionFactDiagnostics, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SymbolicTargetConditionSource<'a> { + pub block_addr: u64, + pub branch_truth: bool, + pub summary: &'a BackwardConditionSummary, + pub necessary_for_target: bool, +} + impl SymbolicFunctionFacts { pub fn branch_fact_for_block(&self, block_addr: u64) -> Option<&SymbolicBranchFact> { self.branch_facts @@ -119,11 +282,104 @@ impl SymbolicFunctionFacts { .find(|fact| fact.block_addr == block_addr) } + pub fn worker_island_for_block(&self, block_addr: u64) -> Option<&SymbolicWorkerIsland> { + self.worker_islands + .iter() + .find(|island| island.anchor_block == block_addr) + } + + pub fn best_worker_island_for_target( + &self, + target_addr: u64, + hard_proof_only: bool, + ) -> Option<&SymbolicWorkerIsland> { + best_worker_island_for_target(self, target_addr, hard_proof_only) + } + pub fn control_island_for_block(&self, block_addr: u64) -> Option<&SymbolicControlIsland> { self.control_islands .iter() .find(|island| island.anchor_block == block_addr) } + + pub fn memory_island_for_block(&self, block_addr: u64) -> Option<&SymbolicMemoryIsland> { + self.memory_islands + .iter() + .find(|island| island.anchor_block == block_addr) + } + + pub fn actionable_memory_terms_for_block( + &self, + block_addr: u64, + ) -> Vec<&BackwardMemoryCondition> { + self.worker_island_for_block(block_addr) + .map(SymbolicWorkerIsland::actionable_memory_terms) + .or_else(|| { + self.memory_island_for_block(block_addr) + .map(SymbolicMemoryIsland::actionable_terms) + }) + .unwrap_or_default() + } + + pub fn actionable_compiled_condition_for_target( + &self, + target_addr: u64, + ) -> Option<&BackwardConditionSummary> { + target_compiled_condition(self, target_addr, false) + } + + pub fn exact_compiled_condition_for_target( + &self, + target_addr: u64, + ) -> Option<&BackwardConditionSummary> { + target_compiled_condition(self, target_addr, true) + } + + pub fn actionable_condition_source_for_target( + &self, + target_addr: u64, + ) -> Option> { + target_condition_source(self, target_addr, false) + } + + pub fn exact_condition_source_for_target( + &self, + target_addr: u64, + ) -> Option> { + target_condition_source(self, target_addr, true) + } + + pub fn actionable_memory_terms_for_target( + &self, + target_addr: u64, + ) -> Vec<&BackwardMemoryCondition> { + self.actionable_condition_source_for_target(target_addr) + .map(|source| self.actionable_memory_terms_for_block(source.block_addr)) + .or_else(|| { + best_worker_island_for_target(self, target_addr, false) + .map(SymbolicWorkerIsland::actionable_memory_terms) + }) + .or_else(|| { + best_legacy_memory_island_for_target(self, target_addr) + .map(SymbolicMemoryIsland::actionable_terms) + }) + .unwrap_or_default() + } + + pub fn exact_memory_terms_for_target(&self, target_addr: u64) -> Vec<&BackwardMemoryCondition> { + self.exact_condition_source_for_target(target_addr) + .and_then(|source| self.worker_island_for_block(source.block_addr)) + .map(SymbolicWorkerIsland::exact_memory_terms) + .or_else(|| { + best_worker_island_for_target(self, target_addr, true) + .map(SymbolicWorkerIsland::exact_memory_terms) + }) + .or_else(|| { + best_legacy_memory_island_for_target(self, target_addr) + .map(SymbolicMemoryIsland::exact_terms) + }) + .unwrap_or_default() + } } fn control_fact_evidence( @@ -208,6 +464,25 @@ fn island_evidence(facts: &[SymbolicControlFact]) -> SemanticEvidence { .unwrap_or_else(|| SemanticEvidence::residual(SemanticEvidenceReason::GuardOpaque)) } +fn memory_island_evidence(terms: &[BackwardMemoryCondition]) -> SemanticEvidence { + terms + .iter() + .map(BackwardMemoryCondition::evidence) + .max_by_key(|evidence| { + ( + match evidence.tier { + SemanticConfidence::Exact => 3, + SemanticConfidence::Likely => 2, + SemanticConfidence::Heuristic => 1, + SemanticConfidence::Residual => 0, + }, + evidence.allows_hard_proof() as u8, + evidence.allows_narrowing() as u8, + ) + }) + .unwrap_or_else(|| SemanticEvidence::residual(SemanticEvidenceReason::ValueOpaque)) +} + fn unique_compiled_condition<'a>( facts: impl Iterator, hard_proof_only: bool, @@ -223,55 +498,652 @@ fn unique_compiled_condition<'a>( candidates.next().is_none().then_some(first) } -fn derive_control_islands( - branch_facts: &[SymbolicBranchFact], - diagnostics: &SymbolicFunctionFactDiagnostics, -) -> Vec { - branch_facts +fn unique_reachable_control_target<'a>( + facts: impl Iterator, + hard_proof_only: bool, +) -> Option { + let mut candidates = facts.filter_map(|fact| { + let condition = if hard_proof_only { + fact.evidence.allows_hard_proof() + } else { + fact.evidence.allows_narrowing() + }; + condition + .then_some(matches!(fact.status, SymbolicReachabilityStatus::Reachable)) + .and_then(|reachable| reachable.then_some(fact.target)) + }); + let first = candidates.next()?; + candidates.next().is_none().then_some(first) +} + +fn summary_precision_rank(summary: &BackwardConditionSummary) -> u8 { + match summary.precision { + BackwardConditionPrecision::Exact => 3, + BackwardConditionPrecision::OverApprox => 2, + BackwardConditionPrecision::ResidualSearchRequired => 1, + BackwardConditionPrecision::Unsupported => 0, + } +} + +fn summary_evidence_rank(summary: &BackwardConditionSummary) -> u8 { + match summary.evidence().tier { + SemanticConfidence::Exact => 3, + SemanticConfidence::Likely => 2, + SemanticConfidence::Heuristic => 1, + SemanticConfidence::Residual => 0, + } +} + +fn best_target_compiled_condition<'a>( + candidates: impl Iterator, +) -> Option<&'a BackwardConditionSummary> { + candidates.max_by(|left, right| { + ( + summary_evidence_rank(left), + summary_precision_rank(left), + std::cmp::Reverse(left.backward_memory_residual_fallbacks), + left.memory_terms.len(), + left.supported_paths, + std::cmp::Reverse(left.total_paths), + std::cmp::Reverse(left.simplified.len()), + ) + .cmp(&( + summary_evidence_rank(right), + summary_precision_rank(right), + std::cmp::Reverse(right.backward_memory_residual_fallbacks), + right.memory_terms.len(), + right.supported_paths, + std::cmp::Reverse(right.total_paths), + std::cmp::Reverse(right.simplified.len()), + )) + }) +} + +fn target_compiled_condition( + facts: &SymbolicFunctionFacts, + target_addr: u64, + hard_proof_only: bool, +) -> Option<&BackwardConditionSummary> { + let branch_candidates = facts.branch_facts.iter().flat_map(|fact| { + let mut candidates = Vec::new(); + if fact.true_target == target_addr { + let compiled = if hard_proof_only { + fact.true_compiled + .as_ref() + .filter(|compiled| compiled.evidence().allows_hard_proof()) + } else { + fact.true_compiled + .as_ref() + .filter(|compiled| compiled.evidence().allows_narrowing()) + }; + if let Some(compiled) = compiled { + candidates.push(compiled); + } + } + if fact.false_target == target_addr { + let compiled = if hard_proof_only { + fact.false_compiled + .as_ref() + .filter(|compiled| compiled.evidence().allows_hard_proof()) + } else { + fact.false_compiled + .as_ref() + .filter(|compiled| compiled.evidence().allows_narrowing()) + }; + if let Some(compiled) = compiled { + candidates.push(compiled); + } + } + candidates + }); + let control_candidates = facts + .worker_islands + .iter() + .flat_map(|island| island.control_facts.iter()) + .chain( + facts + .control_islands + .iter() + .flat_map(|island| island.facts.iter()), + ) + .filter_map(move |fact| { + (fact.target == target_addr) + .then_some(if hard_proof_only { + fact.exact_compiled_condition() + } else { + fact.actionable_compiled_condition() + }) + .flatten() + }); + best_target_compiled_condition(branch_candidates.chain(control_candidates)) +} + +fn best_worker_island_for_target( + facts: &SymbolicFunctionFacts, + target_addr: u64, + hard_proof_only: bool, +) -> Option<&SymbolicWorkerIsland> { + facts + .worker_islands + .iter() + .filter(|island| { + island.control_facts.iter().any(|fact| { + fact.target == target_addr + && if hard_proof_only { + fact.evidence.allows_hard_proof() + } else { + fact.evidence.allows_narrowing() + } + }) + }) + .max_by(|left, right| { + ( + left.evidence.allows_hard_proof() as u8, + left.evidence.allows_narrowing() as u8, + left.evidence.tier, + left.memory_terms.len(), + left.control_facts.len(), + ) + .cmp(&( + right.evidence.allows_hard_proof() as u8, + right.evidence.allows_narrowing() as u8, + right.evidence.tier, + right.memory_terms.len(), + right.control_facts.len(), + )) + }) +} + +fn best_legacy_memory_island_for_target( + facts: &SymbolicFunctionFacts, + target_addr: u64, +) -> Option<&SymbolicMemoryIsland> { + facts + .branch_facts + .iter() + .filter(|fact| fact.true_target == target_addr || fact.false_target == target_addr) + .filter_map(|fact| facts.memory_island_for_block(fact.block_addr)) + .max_by(|left, right| { + ( + left.evidence.allows_hard_proof() as u8, + left.evidence.allows_narrowing() as u8, + left.evidence.tier, + left.terms.len(), + ) + .cmp(&( + right.evidence.allows_hard_proof() as u8, + right.evidence.allows_narrowing() as u8, + right.evidence.tier, + right.terms.len(), + )) + }) +} + +fn target_condition_source( + facts: &SymbolicFunctionFacts, + target_addr: u64, + hard_proof_only: bool, +) -> Option> { + let branch_candidates = facts + .branch_facts .iter() - .map(|branch| { - let kind = if diagnostics.skipped_large_cfg { - SymbolicControlIslandKind::LargeCfgBranchFrontier + .filter_map(|fact| { + let (branch_truth, compiled) = if fact.true_target == target_addr { + let compiled = if hard_proof_only { + fact.true_compiled + .as_ref() + .filter(|compiled| compiled.evidence().allows_hard_proof()) + } else { + fact.true_compiled + .as_ref() + .filter(|compiled| compiled.evidence().allows_narrowing()) + }; + (Some(true), compiled) + } else if fact.false_target == target_addr { + let compiled = if hard_proof_only { + fact.false_compiled + .as_ref() + .filter(|compiled| compiled.evidence().allows_hard_proof()) + } else { + fact.false_compiled + .as_ref() + .filter(|compiled| compiled.evidence().allows_narrowing()) + }; + (Some(false), compiled) } else { - SymbolicControlIslandKind::BranchFrontier + (None, None) }; - let facts = vec![ - SymbolicControlFact { - target: branch.true_target, - status: branch.true_status, - condition: branch.true_condition.clone(), - compiled: branch.true_compiled.clone(), - evidence: control_fact_evidence( - branch.true_compiled.as_ref(), - branch.true_condition.as_deref(), - diagnostics, - ), - }, - SymbolicControlFact { - target: branch.false_target, - status: branch.false_status, - condition: branch.false_condition.clone(), - compiled: branch.false_compiled.clone(), - evidence: control_fact_evidence( - branch.false_compiled.as_ref(), - branch.false_condition.as_deref(), - diagnostics, - ), - }, - ]; - SymbolicControlIsland { - kind, + Some(SymbolicTargetConditionSource { + block_addr: fact.block_addr, + branch_truth: branch_truth?, + summary: compiled?, + necessary_for_target: false, + }) + }) + .collect::>(); + if let Some(first) = branch_candidates.first().copied() { + let necessary_for_target = branch_candidates.iter().all(|candidate| { + candidate.block_addr == first.block_addr + && candidate.branch_truth == first.branch_truth + && candidate.summary == first.summary + }); + if hard_proof_only && !necessary_for_target { + return None; + } + let mut best = branch_candidates.into_iter().max_by(|left, right| { + ( + summary_evidence_rank(left.summary), + summary_precision_rank(left.summary), + std::cmp::Reverse(left.summary.backward_memory_residual_fallbacks), + left.summary.memory_terms.len(), + left.summary.supported_paths, + std::cmp::Reverse(left.summary.total_paths), + std::cmp::Reverse(left.block_addr), + left.branch_truth, + ) + .cmp(&( + summary_evidence_rank(right.summary), + summary_precision_rank(right.summary), + std::cmp::Reverse(right.summary.backward_memory_residual_fallbacks), + right.summary.memory_terms.len(), + right.summary.supported_paths, + std::cmp::Reverse(right.summary.total_paths), + std::cmp::Reverse(right.block_addr), + right.branch_truth, + )) + })?; + best.necessary_for_target = necessary_for_target; + return Some(best); + } + + let candidates = facts + .worker_islands + .iter() + .filter_map(|island| { + let branch = facts.branch_fact_for_block(island.anchor_block)?; + let necessary_for_target = if hard_proof_only { + island.exact_reachable_target() == Some(target_addr) + } else { + island.actionable_reachable_target() == Some(target_addr) + }; + island.control_facts.iter().find_map(|fact| { + if fact.target != target_addr { + return None; + } + let summary = if hard_proof_only { + fact.exact_compiled_condition() + } else { + fact.actionable_compiled_condition() + }?; + let branch_truth = if branch.true_target == target_addr { + Some(true) + } else if branch.false_target == target_addr { + Some(false) + } else { + None + }?; + Some(SymbolicTargetConditionSource { + block_addr: island.anchor_block, + branch_truth, + summary, + necessary_for_target, + }) + }) + }) + .collect::>(); + if candidates.is_empty() { + return None; + } + if hard_proof_only + && candidates + .iter() + .any(|candidate| !candidate.necessary_for_target) + { + return None; + } + candidates.into_iter().max_by(|left, right| { + ( + left.necessary_for_target as u8, + summary_evidence_rank(left.summary), + summary_precision_rank(left.summary), + left.summary.memory_terms.len(), + left.summary.supported_paths, + std::cmp::Reverse(left.summary.total_paths), + std::cmp::Reverse(left.block_addr), + left.branch_truth, + ) + .cmp(&( + right.necessary_for_target as u8, + summary_evidence_rank(right.summary), + summary_precision_rank(right.summary), + right.summary.memory_terms.len(), + right.summary.supported_paths, + std::cmp::Reverse(right.summary.total_paths), + std::cmp::Reverse(right.block_addr), + right.branch_truth, + )) + }) +} + +fn default_control_island_kind( + diagnostics: &SymbolicFunctionFactDiagnostics, +) -> SymbolicControlIslandKind { + if diagnostics.skipped_large_cfg { + SymbolicControlIslandKind::LargeCfgBranchFrontier + } else { + SymbolicControlIslandKind::BranchFrontier + } +} + +fn control_facts_for_branch( + branch: &SymbolicBranchFact, + diagnostics: &SymbolicFunctionFactDiagnostics, +) -> Vec { + vec![ + SymbolicControlFact { + target: branch.true_target, + status: branch.true_status, + condition: branch.true_condition.clone(), + compiled: branch.true_compiled.clone(), + evidence: control_fact_evidence( + branch.true_compiled.as_ref(), + branch.true_condition.as_deref(), + diagnostics, + ), + }, + SymbolicControlFact { + target: branch.false_target, + status: branch.false_status, + condition: branch.false_condition.clone(), + compiled: branch.false_compiled.clone(), + evidence: control_fact_evidence( + branch.false_compiled.as_ref(), + branch.false_condition.as_deref(), + diagnostics, + ), + }, + ] +} + +fn derived_summary_memory_term_evidence( + completion: DerivedSummaryCompletion, + exact_value: bool, +) -> SemanticEvidence { + match completion { + DerivedSummaryCompletion::Exact if exact_value => SemanticEvidence::exact(), + DerivedSummaryCompletion::Exact => SemanticEvidence::exact(), + DerivedSummaryCompletion::OverApprox => { + SemanticEvidence::likely(SemanticEvidenceReason::PartialPathCoverage) + .with_coverage(SemanticEvidenceCoverage::Bounded) + .with_provenance(SemanticEvidenceProvenance::Normalized) + } + DerivedSummaryCompletion::BudgetExhausted => { + SemanticEvidence::heuristic(SemanticEvidenceReason::SummaryBudget) + .with_coverage(SemanticEvidenceCoverage::Bounded) + .with_provenance(SemanticEvidenceProvenance::Normalized) + .with_budget_limited(true) + } + DerivedSummaryCompletion::Unknown => { + SemanticEvidence::heuristic(SemanticEvidenceReason::ValueOpaque) + .with_coverage(SemanticEvidenceCoverage::Bounded) + } + } +} + +fn merge_memory_islands( + mut islands: Vec, + additions: impl IntoIterator, +) -> Vec { + let mut by_anchor = BTreeMap::::new(); + for island in islands.drain(..).chain(additions) { + let entry = by_anchor + .entry(island.anchor_block) + .or_insert_with(|| SymbolicMemoryIsland { + kind: island.kind, + anchor_block: island.anchor_block, + terms: Vec::new(), + evidence: island.evidence.clone(), + }); + if matches!(entry.kind, SymbolicMemoryIslandKind::ConditionFrontier) + && matches!( + island.kind, + SymbolicMemoryIslandKind::LargeCfgConditionFrontier + ) + { + entry.kind = island.kind; + } + for term in island.terms { + if !entry.terms.contains(&term) { + entry.terms.push(term); + } + } + entry.evidence = memory_island_evidence(&entry.terms); + } + by_anchor.into_values().collect() +} + +fn push_unique_memory_terms( + dst: &mut Vec, + terms: impl IntoIterator, +) { + for term in terms { + if !dst.contains(&term) { + dst.push(term); + } + } +} + +fn worker_island_evidence( + control_facts: &[SymbolicControlFact], + memory_terms: &[BackwardMemoryCondition], +) -> SemanticEvidence { + let control = island_evidence(control_facts); + let memory = memory_island_evidence(memory_terms); + let control_rank = ( + control.allows_hard_proof() as u8, + control.allows_narrowing() as u8, + match control.tier { + SemanticConfidence::Exact => 3, + SemanticConfidence::Likely => 2, + SemanticConfidence::Heuristic => 1, + SemanticConfidence::Residual => 0, + }, + ); + let memory_rank = ( + memory.allows_hard_proof() as u8, + memory.allows_narrowing() as u8, + match memory.tier { + SemanticConfidence::Exact => 3, + SemanticConfidence::Likely => 2, + SemanticConfidence::Heuristic => 1, + SemanticConfidence::Residual => 0, + }, + ); + if memory_rank > control_rank { + memory + } else { + control + } +} + +fn derive_worker_islands( + branch_facts: &[SymbolicBranchFact], + preseeded_memory_islands: &[SymbolicMemoryIsland], + diagnostics: &SymbolicFunctionFactDiagnostics, +) -> Vec { + let mut by_anchor = BTreeMap::::new(); + for branch in branch_facts { + let entry = by_anchor + .entry(branch.block_addr) + .or_insert_with(|| SymbolicWorkerIsland { anchor_block: branch.block_addr, - frontier_targets: vec![branch.true_target, branch.false_target], - evidence: island_evidence(&facts), - facts, + control_kind: None, + memory_kind: None, + frontier_targets: Vec::new(), + control_facts: Vec::new(), + memory_terms: Vec::new(), + evidence: SemanticEvidence::residual(SemanticEvidenceReason::GuardOpaque), + }); + entry.control_kind = Some(default_control_island_kind(diagnostics)); + entry.frontier_targets = vec![branch.true_target, branch.false_target]; + entry.control_facts = control_facts_for_branch(branch, diagnostics); + push_unique_memory_terms( + &mut entry.memory_terms, + entry + .control_facts + .iter() + .filter_map(SymbolicControlFact::actionable_compiled_condition) + .flat_map(|compiled| compiled.memory_terms.iter().cloned()), + ); + entry.evidence = worker_island_evidence(&entry.control_facts, &entry.memory_terms); + } + for island in preseeded_memory_islands { + let entry = by_anchor + .entry(island.anchor_block) + .or_insert_with(|| SymbolicWorkerIsland { + anchor_block: island.anchor_block, + control_kind: None, + memory_kind: None, + frontier_targets: Vec::new(), + control_facts: Vec::new(), + memory_terms: Vec::new(), + evidence: island.evidence.clone(), + }); + entry.memory_kind = Some(island.kind); + push_unique_memory_terms(&mut entry.memory_terms, island.terms.iter().cloned()); + entry.evidence = worker_island_evidence(&entry.control_facts, &entry.memory_terms); + } + by_anchor.into_values().collect() +} + +fn project_control_islands(worker_islands: &[SymbolicWorkerIsland]) -> Vec { + worker_islands + .iter() + .filter(|island| !island.control_facts.is_empty()) + .map(|island| SymbolicControlIsland { + kind: island + .control_kind + .unwrap_or(SymbolicControlIslandKind::BranchFrontier), + anchor_block: island.anchor_block, + frontier_targets: island.frontier_targets.clone(), + facts: island.control_facts.clone(), + evidence: island_evidence(&island.control_facts), + }) + .collect() +} + +fn project_memory_islands(worker_islands: &[SymbolicWorkerIsland]) -> Vec { + worker_islands + .iter() + .filter(|island| !island.memory_terms.is_empty()) + .map(|island| SymbolicMemoryIsland { + kind: island + .memory_kind + .unwrap_or(SymbolicMemoryIslandKind::ConditionFrontier), + anchor_block: island.anchor_block, + terms: island.memory_terms.clone(), + evidence: memory_island_evidence(&island.memory_terms), + }) + .collect() +} + +fn summary_memory_location_expr(arg_index: usize, offset: i64) -> String { + if offset == 0 { + format!("*arg{arg_index}") + } else if offset > 0 { + format!("*(arg{arg_index} + 0x{:x})", offset as u64) + } else { + format!("*(arg{arg_index} - 0x{:x})", offset.unsigned_abs()) + } +} + +fn derive_summary_memory_islands<'ctx>( + func: &SsaArtifact, + branch_blocks: &[(u64, u64, u64)], + derived: &DerivedSummarySet<'ctx>, + diagnostics: &SymbolicFunctionFactDiagnostics, +) -> Vec { + let hot_blocks = branch_blocks + .iter() + .flat_map(|(block, true_target, false_target)| [*block, *true_target, *false_target]) + .collect::>(); + let mut by_anchor = BTreeMap::>::new(); + let max_islands = branch_blocks.len().max(1) * 3; + + let mut call_blocks = func + .call_sites() + .by_id + .values() + .filter_map(|call| { + let target = call.direct_target?; + let summary = derived.summaries.get(&InterprocFunctionId(target))?; + if summary + .cases + .iter() + .all(|case| case.memory_writes.is_empty()) + { + return None; + } + let (block_addr, _) = func.inst_op_site(call.at)?; + Some((!hot_blocks.contains(&block_addr), block_addr, summary)) + }) + .collect::>(); + call_blocks.sort_by_key(|(cold_block, block_addr, _)| (*cold_block, *block_addr)); + + for (_, block_addr, summary) in call_blocks.into_iter().take(max_islands) { + let terms = by_anchor.entry(block_addr).or_default(); + for case in &summary.cases { + for write in &case.memory_writes { + let exact_value = write.value.is_concrete(); + let evidence = + derived_summary_memory_term_evidence(summary.completion, exact_value); + let term = BackwardMemoryCondition { + region: crate::BackwardMemoryRegion::Argument { + index: write.arg_index, + }, + offset_lo: write.offset, + offset_hi: write.offset, + size: write.size, + exact_offset: matches!(summary.completion, DerivedSummaryCompletion::Exact), + evidence, + binding: None, + expr: summary_memory_location_expr(write.arg_index, write.offset), + value_expr: Some(write.value.to_string()), + exact_value, + }; + if !terms.contains(&term) { + terms.push(term); + } } + } + } + + by_anchor + .into_iter() + .filter_map(|(anchor_block, terms)| { + (!terms.is_empty()).then(|| SymbolicMemoryIsland { + kind: if diagnostics.skipped_large_cfg { + SymbolicMemoryIslandKind::LargeCfgConditionFrontier + } else { + SymbolicMemoryIslandKind::ConditionFrontier + }, + anchor_block, + evidence: memory_island_evidence(&terms), + terms, + }) }) .collect() } fn finalize_symbolic_function_facts(mut facts: SymbolicFunctionFacts) -> SymbolicFunctionFacts { - facts.control_islands = derive_control_islands(&facts.branch_facts, &facts.diagnostics); + let preseeded_memory_islands = std::mem::take(&mut facts.memory_islands); + facts.worker_islands = derive_worker_islands( + &facts.branch_facts, + &preseeded_memory_islands, + &facts.diagnostics, + ); + facts.control_islands = project_control_islands(&facts.worker_islands); + facts.memory_islands = project_memory_islands(&facts.worker_islands); facts } @@ -687,7 +1559,7 @@ pub(super) fn collect_symbolic_function_facts_with_derived_for_branch_blocks<'ct derived: &DerivedSummarySet<'ctx>, symbol_map: &HashMap, ) -> SymbolicFunctionFacts { - let facts = collect_symbolic_function_facts_for_branch_blocks( + let mut facts = collect_symbolic_function_facts_for_branch_blocks( ctx, func, arch, @@ -696,6 +1568,10 @@ pub(super) fn collect_symbolic_function_facts_with_derived_for_branch_blocks<'ct install_derived_summary_set(explorer, registry, func, scope, derived, symbol_map); }, ); + facts.memory_islands = merge_memory_islands( + facts.memory_islands, + derive_summary_memory_islands(func, branch_blocks, derived, &facts.diagnostics), + ); let _ = summary_profile; finalize_symbolic_function_facts(facts) } @@ -882,6 +1758,7 @@ pub fn collect_symbolic_function_facts_with_scope_and_profile( #[cfg(test)] mod tests { use super::*; + use crate::BackwardMemoryRegion; fn residual_summary(expr: &str) -> BackwardConditionSummary { BackwardConditionSummary { @@ -911,7 +1788,9 @@ mod tests { true_compiled: Some(residual_summary("sel == 3")), false_compiled: None, }], + worker_islands: Vec::new(), control_islands: Vec::new(), + memory_islands: Vec::new(), diagnostics: SymbolicFunctionFactDiagnostics { skipped_large_cfg: true, ..SymbolicFunctionFactDiagnostics::default() @@ -963,7 +1842,9 @@ mod tests { total_paths: 2, }), }], + worker_islands: Vec::new(), control_islands: Vec::new(), + memory_islands: Vec::new(), diagnostics: SymbolicFunctionFactDiagnostics::default(), }); @@ -972,4 +1853,260 @@ mod tests { .expect("control island"); assert!(island.actionable_compiled_condition().is_none()); } + + #[test] + fn actionable_memory_terms_derive_memory_island_for_block() { + let facts = finalize_symbolic_function_facts(SymbolicFunctionFacts { + branch_facts: vec![SymbolicBranchFact { + block_addr: 0x3000, + true_target: 0x3010, + false_target: 0x3020, + true_status: SymbolicReachabilityStatus::Unknown, + false_status: SymbolicReachabilityStatus::Unknown, + true_condition: Some("arg0->f_8 == 0".to_string()), + false_condition: None, + true_compiled: Some(BackwardConditionSummary { + simplified: "arg0->f_8 == 0".to_string(), + terms: vec!["arg0->f_8 == 0".to_string()], + memory_terms: vec![BackwardMemoryCondition { + region: BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 8, + offset_hi: 8, + size: 4, + exact_offset: true, + evidence: SemanticEvidence::exact(), + binding: None, + expr: "*(arg0 + 8)".to_string(), + value_expr: Some("*(arg0 + 8)".to_string()), + exact_value: false, + }], + backward_memory_substitutions: 1, + backward_memory_candidate_enumerations: 1, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::OverApprox, + supported_paths: 1, + total_paths: 1, + }), + false_compiled: None, + }], + worker_islands: Vec::new(), + control_islands: Vec::new(), + memory_islands: Vec::new(), + diagnostics: SymbolicFunctionFactDiagnostics::default(), + }); + + let island = facts + .memory_island_for_block(0x3000) + .expect("memory island"); + assert_eq!(island.kind, SymbolicMemoryIslandKind::ConditionFrontier); + assert_eq!(island.terms.len(), 1); + assert_eq!(island.actionable_terms().len(), 1); + assert_eq!(island.evidence.tier, SemanticConfidence::Exact); + } + + #[test] + fn actionable_memory_terms_for_target_follow_actionable_source_block() { + let facts = finalize_symbolic_function_facts(SymbolicFunctionFacts { + branch_facts: vec![SymbolicBranchFact { + block_addr: 0x3500, + true_target: 0x3600, + false_target: 0x3610, + true_status: SymbolicReachabilityStatus::Reachable, + false_status: SymbolicReachabilityStatus::Unreachable, + true_condition: Some("x == 0".to_string()), + false_condition: None, + true_compiled: Some(BackwardConditionSummary { + simplified: "x == 0".to_string(), + terms: vec!["x == 0".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }), + false_compiled: None, + }], + worker_islands: Vec::new(), + control_islands: Vec::new(), + memory_islands: vec![SymbolicMemoryIsland { + kind: SymbolicMemoryIslandKind::ConditionFrontier, + anchor_block: 0x3500, + terms: vec![BackwardMemoryCondition { + region: BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 4, + offset_hi: 4, + size: 4, + exact_offset: true, + evidence: SemanticEvidence::exact(), + binding: Some("sym_mem".to_string()), + expr: "0x2a".to_string(), + value_expr: Some("0x2a".to_string()), + exact_value: true, + }], + evidence: SemanticEvidence::exact(), + }], + diagnostics: SymbolicFunctionFactDiagnostics::default(), + }); + + let terms = facts.actionable_memory_terms_for_target(0x3600); + assert_eq!(terms.len(), 1); + assert_eq!(terms[0].binding.as_deref(), Some("sym_mem")); + assert_eq!(terms[0].value_expr.as_deref(), Some("0x2a")); + } + + #[test] + fn finalize_symbolic_function_facts_preserves_preseeded_memory_islands() { + let facts = finalize_symbolic_function_facts(SymbolicFunctionFacts { + branch_facts: Vec::new(), + worker_islands: Vec::new(), + control_islands: Vec::new(), + memory_islands: vec![SymbolicMemoryIsland { + kind: SymbolicMemoryIslandKind::LargeCfgConditionFrontier, + anchor_block: 0x3500, + terms: vec![BackwardMemoryCondition { + region: BackwardMemoryRegion::Argument { index: 1 }, + offset_lo: 0x10, + offset_hi: 0x10, + size: 8, + exact_offset: true, + evidence: SemanticEvidence::exact(), + binding: None, + expr: "arg1->f_10".to_string(), + value_expr: Some("arg1->f_10".to_string()), + exact_value: false, + }], + evidence: SemanticEvidence::exact(), + }], + diagnostics: SymbolicFunctionFactDiagnostics::default(), + }); + + let island = facts + .memory_island_for_block(0x3500) + .expect("preseeded memory island"); + assert_eq!(island.terms.len(), 1); + assert_eq!( + island.kind, + SymbolicMemoryIslandKind::LargeCfgConditionFrontier + ); + assert_eq!(island.terms[0].offset_lo, 0x10); + } + + #[test] + fn actionable_condition_source_for_target_tracks_branch_frontier() { + let compiled = BackwardConditionSummary { + simplified: "x == 0".to_string(), + terms: vec!["x == 0".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }; + let facts = finalize_symbolic_function_facts(SymbolicFunctionFacts { + branch_facts: vec![SymbolicBranchFact { + block_addr: 0x4000, + true_target: 0x4010, + false_target: 0x4020, + true_status: SymbolicReachabilityStatus::Reachable, + false_status: SymbolicReachabilityStatus::Unreachable, + true_condition: Some("x == 0".to_string()), + false_condition: Some("x != 0".to_string()), + true_compiled: Some(compiled.clone()), + false_compiled: None, + }], + worker_islands: Vec::new(), + control_islands: Vec::new(), + memory_islands: Vec::new(), + diagnostics: SymbolicFunctionFactDiagnostics::default(), + }); + + let source = facts + .actionable_condition_source_for_target(0x4010) + .expect("target source"); + assert_eq!(source.block_addr, 0x4000); + assert!(source.branch_truth); + assert!(source.necessary_for_target); + assert_eq!(source.summary.simplified, "x == 0"); + } + + #[test] + fn actionable_condition_source_prefers_best_candidate_but_marks_non_unique_sources() { + let exact = BackwardConditionSummary { + simplified: "x == 0".to_string(), + terms: vec!["x == 0".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }; + let likely = BackwardConditionSummary { + simplified: "x <= 1".to_string(), + terms: vec!["x <= 1".to_string()], + memory_terms: vec![BackwardMemoryCondition { + region: BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 8, + offset_hi: 12, + size: 4, + exact_offset: false, + evidence: SemanticEvidence::likely(SemanticEvidenceReason::DerivedFromRanking) + .with_coverage(SemanticEvidenceCoverage::Bounded) + .with_provenance(SemanticEvidenceProvenance::Normalized), + binding: None, + expr: "*(arg0 + [8,12])".to_string(), + value_expr: Some("*(arg0 + [8,12])".to_string()), + exact_value: false, + }], + backward_memory_substitutions: 1, + backward_memory_candidate_enumerations: 1, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::OverApprox, + supported_paths: 1, + total_paths: 2, + }; + let facts = finalize_symbolic_function_facts(SymbolicFunctionFacts { + branch_facts: vec![ + SymbolicBranchFact { + block_addr: 0x4000, + true_target: 0x4010, + false_target: 0x4020, + true_status: SymbolicReachabilityStatus::Reachable, + false_status: SymbolicReachabilityStatus::Unreachable, + true_condition: Some("x == 0".to_string()), + false_condition: Some("x != 0".to_string()), + true_compiled: Some(exact.clone()), + false_compiled: None, + }, + SymbolicBranchFact { + block_addr: 0x4018, + true_target: 0x4010, + false_target: 0x4030, + true_status: SymbolicReachabilityStatus::Reachable, + false_status: SymbolicReachabilityStatus::Unreachable, + true_condition: Some("x <= 1".to_string()), + false_condition: None, + true_compiled: Some(likely), + false_compiled: None, + }, + ], + worker_islands: Vec::new(), + control_islands: Vec::new(), + memory_islands: Vec::new(), + diagnostics: SymbolicFunctionFactDiagnostics::default(), + }); + + let source = facts + .actionable_condition_source_for_target(0x4010) + .expect("target source"); + assert_eq!(source.block_addr, 0x4000); + assert!(source.branch_truth); + assert!(!source.necessary_for_target); + assert_eq!(source.summary.simplified, "x == 0"); + } } diff --git a/crates/r2sym/src/semantics/mod.rs b/crates/r2sym/src/semantics/mod.rs index d97ec66..335366d 100644 --- a/crates/r2sym/src/semantics/mod.rs +++ b/crates/r2sym/src/semantics/mod.rs @@ -12,10 +12,12 @@ pub use artifact::{ SemanticEvidenceSoundness, SemanticMode, SliceClass, }; pub use cache::stable_scope_hash; +pub use compiler::compile_semantic_artifact_default_with_scope; pub use compiler::{compile_function_semantics_with_scope, compile_semantic_artifact_with_scope}; pub use facts::{ SymbolicBranchFact, SymbolicControlFact, SymbolicControlIsland, SymbolicControlIslandKind, - SymbolicFunctionFactDiagnostics, SymbolicFunctionFacts, SymbolicReachabilityStatus, + SymbolicFunctionFactDiagnostics, SymbolicFunctionFacts, SymbolicMemoryIsland, + SymbolicMemoryIslandKind, SymbolicReachabilityStatus, SymbolicWorkerIsland, collect_symbolic_function_facts, collect_symbolic_function_facts_with_scope, }; pub use vm::{ diff --git a/crates/r2sym/src/semantics/vm.rs b/crates/r2sym/src/semantics/vm.rs index 541c2d5..d01f76d 100644 --- a/crates/r2sym/src/semantics/vm.rs +++ b/crates/r2sym/src/semantics/vm.rs @@ -554,6 +554,11 @@ fn classify_vm_var_value(func: &SsaArtifact, var: &SSAVar, depth: u32) -> VmValu let r2ssa::graph::InstPayload::Op(op) = &inst.payload else { return VmValueExpr::Var(var.display_name()); }; + if let Some((block_addr, op_idx)) = func.inst_op_site(inst_id) + && let Some(value) = classify_vm_op_value_at_site(func, block_addr, op_idx, op, depth + 1) + { + return value; + } classify_vm_op_value(func, op, depth + 1) .unwrap_or_else(|| VmValueExpr::Var(var.display_name())) } diff --git a/crates/r2sym/src/sim.rs b/crates/r2sym/src/sim.rs index 61bf85e..5a2b0b9 100644 --- a/crates/r2sym/src/sim.rs +++ b/crates/r2sym/src/sim.rs @@ -332,6 +332,19 @@ impl PreparedFunctionScope { .values() .filter(move |function| function.id != self.root) } + + pub fn with_prepared_root(&self, prepared: &SsaArtifact) -> Option { + let mut functions = self.functions.values().cloned().collect::>(); + for function in &mut functions { + if function.id == self.root { + function.prepared = prepared.clone(); + if function.name.is_none() { + function.name = prepared.function().name.clone(); + } + } + } + Self::new(self.root.0, functions) + } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -2408,6 +2421,23 @@ fn constrain_ret_tristate<'ctx>(state: &mut SymState<'ctx>, ret: &SymValue<'ctx> #[cfg(test)] mod tests { use super::*; + use r2il::{R2ILBlock, R2ILOp, RegisterDef, SpaceId, Varnode}; + + fn test_arch() -> ArchSpec { + let mut arch = ArchSpec::new("x86-64"); + arch.addr_size = 8; + arch.add_register(RegisterDef::new("RAX", 0, 8)); + arch + } + + fn const_vn(value: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Const, + offset: value, + size, + meta: None, + } + } #[test] fn callconv_accepts_x86_64_arch_specs_with_bit_sized_addr_width() { @@ -2473,6 +2503,71 @@ mod tests { assert_eq!(path_listing_base.get_taint(), 0x20); } + #[test] + fn prepared_function_scope_with_prepared_root_rebinds_root_only() { + let blocks_a = vec![R2ILBlock { + addr: 0x1000, + size: 1, + ops: vec![R2ILOp::Return { + target: const_vn(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }]; + let blocks_b = vec![R2ILBlock { + addr: 0x1000, + size: 1, + ops: vec![R2ILOp::Return { + target: const_vn(1, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }]; + let helper_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + ops: vec![R2ILOp::Return { + target: const_vn(2, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }]; + let root_a = SsaArtifact::for_symbolic(&blocks_a, Some(&test_arch())).expect("root a"); + let root_b = SsaArtifact::for_symbolic(&blocks_b, Some(&test_arch())).expect("root b"); + let helper = SsaArtifact::for_symbolic(&helper_blocks, Some(&test_arch())).expect("helper"); + + let scope = PreparedFunctionScope::new( + 0x1000, + vec![ + ScopedPreparedFunction { + id: InterprocFunctionId(0x1000), + name: None, + prepared: root_a, + }, + ScopedPreparedFunction { + id: InterprocFunctionId(0x2000), + name: Some("helper".to_string()), + prepared: helper.clone(), + }, + ], + ) + .expect("scope"); + + let rebound = scope.with_prepared_root(&root_b).expect("rebound scope"); + let rebound_root = rebound.root().expect("rebound root"); + assert_eq!(rebound_root.prepared.entry, root_b.entry); + assert_eq!(rebound_root.prepared.function().blocks().count(), 1); + assert_eq!( + rebound + .functions() + .get(&InterprocFunctionId(0x2000)) + .expect("helper") + .prepared + .entry, + helper.entry + ); + } + #[test] fn derived_summary_guidance_adjusts_arg_widths_before_substitution() { let ctx = z3::Context::thread_local(); diff --git a/crates/r2types/Cargo.toml b/crates/r2types/Cargo.toml index 5be5aee..a3b8279 100644 --- a/crates/r2types/Cargo.toml +++ b/crates/r2types/Cargo.toml @@ -6,6 +6,7 @@ license.workspace = true description = "Constraint-based type inference primitives for r2sleigh" [dependencies] +r2sym = { path = "../r2sym" } r2ssa = { path = "../r2ssa" } serde.workspace = true serde_json.workspace = true diff --git a/crates/r2types/src/facts.rs b/crates/r2types/src/facts.rs index c2d6620..b64c142 100644 --- a/crates/r2types/src/facts.rs +++ b/crates/r2types/src/facts.rs @@ -19,6 +19,7 @@ pub enum SymbolicReachabilityStatus { pub enum SymbolicSemanticMode { Raw, Compiled, + IslandCompiled, Residual, VmSummary, } @@ -671,6 +672,12 @@ pub enum SymbolicControlIslandKind { LargeCfgBranchFrontier, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum SymbolicMemoryIslandKind { + ConditionFrontier, + LargeCfgConditionFrontier, +} + #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct SymbolicControlFact { pub target: u64, @@ -724,6 +731,10 @@ impl SymbolicControlIsland { unique_reachable_control_target(self.facts.iter(), true) } + pub fn actionable_reachable_target(&self) -> Option { + unique_reachable_control_target(self.facts.iter(), false) + } + pub fn exact_compiled_condition(&self) -> Option<&SymbolicCompiledCondition> { unique_compiled_control_condition(self.facts.iter(), true) } @@ -733,11 +744,122 @@ impl SymbolicControlIsland { } } +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct SymbolicMemoryIsland { + pub kind: SymbolicMemoryIslandKind, + pub anchor_block: u64, + pub terms: Vec, + #[serde( + default, + skip_serializing_if = "SymbolicSemanticEvidence::is_default_exact" + )] + pub evidence: SymbolicSemanticEvidence, + #[serde(default = "default_symbolic_confidence_exact")] + pub confidence: SymbolicSemanticConfidence, +} + +impl SymbolicMemoryIsland { + pub fn exact_terms(&self) -> Vec<&SymbolicMemoryCondition> { + self.terms + .iter() + .filter(|term| term.evidence.allows_hard_proof()) + .collect() + } + + pub fn actionable_terms(&self) -> Vec<&SymbolicMemoryCondition> { + self.terms + .iter() + .filter(|term| term.evidence.allows_narrowing()) + .collect() + } +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct SymbolicWorkerIsland { + pub anchor_block: u64, + #[serde(skip_serializing_if = "Option::is_none")] + pub control_kind: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub memory_kind: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub frontier_targets: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub control_facts: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub memory_terms: Vec, + #[serde( + default, + skip_serializing_if = "SymbolicSemanticEvidence::is_default_exact" + )] + pub evidence: SymbolicSemanticEvidence, + #[serde(default = "default_symbolic_confidence_exact")] + pub confidence: SymbolicSemanticConfidence, +} + +impl SymbolicWorkerIsland { + pub fn exact_reachable_target(&self) -> Option { + unique_reachable_control_target(self.control_facts.iter(), true) + } + + pub fn actionable_reachable_target(&self) -> Option { + unique_reachable_control_target(self.control_facts.iter(), false) + } + + pub fn exact_compiled_condition(&self) -> Option<&SymbolicCompiledCondition> { + unique_compiled_control_condition(self.control_facts.iter(), true) + } + + pub fn actionable_compiled_condition(&self) -> Option<&SymbolicCompiledCondition> { + unique_compiled_control_condition(self.control_facts.iter(), false) + } + + pub fn exact_terms(&self) -> Vec<&SymbolicMemoryCondition> { + self.memory_terms + .iter() + .filter(|term| term.evidence.allows_hard_proof()) + .collect() + } + + pub fn actionable_terms(&self) -> Vec<&SymbolicMemoryCondition> { + self.memory_terms + .iter() + .filter(|term| term.evidence.allows_narrowing()) + .collect() + } +} + +fn worker_island_supporting_compiled_condition( + island: &SymbolicWorkerIsland, +) -> Option<&SymbolicCompiledCondition> { + let reachable_target = island + .exact_reachable_target() + .or_else(|| island.actionable_reachable_target())?; + island + .control_facts + .iter() + .find(|fact| fact.target == reachable_target) + .and_then(SymbolicControlFact::actionable_compiled_condition) +} + +fn worker_island_supports_structured_decompile(island: &SymbolicWorkerIsland) -> bool { + let has_unique_target = + island.exact_reachable_target().is_some() || island.actionable_reachable_target().is_some(); + let supporting_condition = worker_island_supporting_compiled_condition(island); + let has_condition = supporting_condition.is_some(); + let has_memory_support = !island.actionable_terms().is_empty() + || supporting_condition.is_some_and(|compiled| !compiled.memory_terms.is_empty()); + island.evidence.allows_narrowing() && has_unique_target && has_condition && has_memory_support +} + #[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct SymbolicSemanticFacts { pub branch_facts: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub worker_islands: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] pub control_islands: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub memory_islands: Vec, pub diagnostics: SymbolicFactDiagnostics, #[serde(skip_serializing_if = "Option::is_none")] pub interpreter: Option, @@ -750,7 +872,9 @@ pub struct SymbolicSemanticFacts { impl SymbolicSemanticFacts { pub fn is_empty(&self) -> bool { self.branch_facts.is_empty() + && self.worker_islands.is_empty() && self.control_islands.is_empty() + && self.memory_islands.is_empty() && self.diagnostics == SymbolicFactDiagnostics::default() && self.interpreter.is_none() && self.vm_step.is_none() @@ -769,12 +893,28 @@ impl SymbolicSemanticFacts { .find(|island| island.anchor_block == block_addr) } + pub fn worker_island_for_block(&self, block_addr: u64) -> Option<&SymbolicWorkerIsland> { + self.worker_islands + .iter() + .find(|island| island.anchor_block == block_addr) + } + + pub fn memory_island_for_block(&self, block_addr: u64) -> Option<&SymbolicMemoryIsland> { + self.memory_islands + .iter() + .find(|island| island.anchor_block == block_addr) + } + pub fn exact_compiled_condition_for_block( &self, block_addr: u64, ) -> Option<&SymbolicCompiledCondition> { self.branch_fact_for_block(block_addr) .and_then(SymbolicBranchFact::exact_compiled_condition) + .or_else(|| { + self.worker_island_for_block(block_addr) + .and_then(SymbolicWorkerIsland::exact_compiled_condition) + }) .or_else(|| { self.control_island_for_block(block_addr) .and_then(SymbolicControlIsland::exact_compiled_condition) @@ -784,24 +924,72 @@ impl SymbolicSemanticFacts { pub fn exact_reachable_target_for_block(&self, block_addr: u64) -> Option { self.branch_fact_for_block(block_addr) .and_then(SymbolicBranchFact::exact_reachable_target) + .or_else(|| { + self.worker_island_for_block(block_addr) + .and_then(SymbolicWorkerIsland::exact_reachable_target) + }) .or_else(|| { self.control_island_for_block(block_addr) .and_then(SymbolicControlIsland::exact_reachable_target) }) } + pub fn actionable_reachable_target_for_block(&self, block_addr: u64) -> Option { + self.branch_fact_for_block(block_addr) + .and_then(SymbolicBranchFact::actionable_reachable_target) + .or_else(|| { + self.worker_island_for_block(block_addr) + .and_then(SymbolicWorkerIsland::actionable_reachable_target) + }) + .or_else(|| { + self.control_island_for_block(block_addr) + .and_then(SymbolicControlIsland::actionable_reachable_target) + }) + } + pub fn actionable_compiled_condition_for_block( &self, block_addr: u64, ) -> Option<&SymbolicCompiledCondition> { self.branch_fact_for_block(block_addr) .and_then(SymbolicBranchFact::actionable_compiled_condition) + .or_else(|| { + self.worker_island_for_block(block_addr) + .and_then(SymbolicWorkerIsland::actionable_compiled_condition) + }) .or_else(|| { self.control_island_for_block(block_addr) .and_then(SymbolicControlIsland::actionable_compiled_condition) }) } + pub fn actionable_memory_terms_for_block( + &self, + block_addr: u64, + ) -> Vec<&SymbolicMemoryCondition> { + self.worker_island_for_block(block_addr) + .map(SymbolicWorkerIsland::actionable_terms) + .or_else(|| { + self.memory_island_for_block(block_addr) + .map(SymbolicMemoryIsland::actionable_terms) + }) + .unwrap_or_default() + } + + pub fn actionable_compiled_condition_for_target( + &self, + target_addr: u64, + ) -> Option<&SymbolicCompiledCondition> { + target_compiled_condition(self, target_addr, false) + } + + pub fn exact_compiled_condition_for_target( + &self, + target_addr: u64, + ) -> Option<&SymbolicCompiledCondition> { + target_compiled_condition(self, target_addr, true) + } + pub fn vm_step_for_dispatch_header( &self, dispatch_header: u64, @@ -819,6 +1007,106 @@ impl SymbolicSemanticFacts { .as_ref() .filter(|vm_transfer| vm_transfer.dispatch_header == dispatch_header) } + + pub fn semantic_mode(&self) -> Option { + self.diagnostics.semantic_mode + } + + pub fn semantic_capability(&self) -> Option { + self.diagnostics.semantic_capability + } + + pub fn island_compiled(&self) -> bool { + matches!( + self.semantic_mode(), + Some(SymbolicSemanticMode::IslandCompiled) + ) + } + + pub fn query_ready(&self) -> bool { + self.semantic_capability() + .is_some_and(|capability| capability.query_ready) + } + + pub fn type_ready(&self) -> bool { + self.semantic_capability() + .is_some_and(|capability| capability.type_ready) + } + + pub fn decompile_ready(&self) -> bool { + self.semantic_capability() + .is_some_and(|capability| capability.decompile_ready) + } + + pub fn structured_decompile_ready(&self) -> bool { + if !self.decompile_ready() || !self.diagnostics.skipped_large_cfg { + return false; + } + self.worker_islands + .iter() + .any(worker_island_supports_structured_decompile) + } + + pub fn slice_class(&self) -> Option { + self.diagnostics.slice_class + } + + pub fn requires_type_fallback(&self) -> bool { + self.semantic_capability().is_some() && !self.type_ready() + } + + pub fn prefers_bounded_type_plan(&self) -> bool { + if self.requires_type_fallback() { + return true; + } + matches!( + self.semantic_mode(), + Some(SymbolicSemanticMode::Residual | SymbolicSemanticMode::IslandCompiled) + ) && self.diagnostics.skipped_large_cfg + && matches!(self.slice_class(), Some(SymbolicSemanticSliceClass::Worker)) + && (!self.worker_islands.is_empty() || !self.memory_islands.is_empty()) + && (self.actionable_compiled_condition_count() > 0 + || self + .worker_islands + .iter() + .any(|island| !island.actionable_terms().is_empty()) + || self + .memory_islands + .iter() + .any(|island| !island.actionable_terms().is_empty())) + } + + pub fn exact_compiled_condition_count(&self) -> usize { + if self.worker_islands.is_empty() { + self.control_islands + .iter() + .flat_map(|island| island.facts.iter()) + .filter(|fact| fact.evidence.allows_hard_proof()) + .count() + } else { + self.worker_islands + .iter() + .flat_map(|island| island.control_facts.iter()) + .filter(|fact| fact.evidence.allows_hard_proof()) + .count() + } + } + + pub fn actionable_compiled_condition_count(&self) -> usize { + if self.worker_islands.is_empty() { + self.control_islands + .iter() + .flat_map(|island| island.facts.iter()) + .filter(|fact| fact.evidence.allows_narrowing()) + .count() + } else { + self.worker_islands + .iter() + .flat_map(|island| island.control_facts.iter()) + .filter(|fact| fact.evidence.allows_narrowing()) + .count() + } + } } impl SymbolicBranchFact { @@ -834,6 +1122,28 @@ impl SymbolicBranchFact { } } + pub fn actionable_reachable_target(&self) -> Option { + match (self.true_status, self.false_status) { + (SymbolicReachabilityStatus::Reachable, SymbolicReachabilityStatus::Unreachable) + if self + .true_compiled + .as_ref() + .is_some_and(|compiled| compiled.evidence.allows_narrowing()) => + { + Some(self.true_target) + } + (SymbolicReachabilityStatus::Unreachable, SymbolicReachabilityStatus::Reachable) + if self + .false_compiled + .as_ref() + .is_some_and(|compiled| compiled.evidence.allows_narrowing()) => + { + Some(self.false_target) + } + _ => None, + } + } + pub fn exact_compiled_condition(&self) -> Option<&SymbolicCompiledCondition> { match (self.true_status, self.false_status) { (SymbolicReachabilityStatus::Reachable, SymbolicReachabilityStatus::Unreachable) => { @@ -882,6 +1192,108 @@ fn unique_compiled_control_condition<'a>( candidates.next().is_none().then_some(first) } +fn compiled_condition_precision_rank(condition: &SymbolicCompiledCondition) -> u8 { + match condition.precision { + SymbolicConditionPrecision::Exact => 3, + SymbolicConditionPrecision::OverApprox => 2, + SymbolicConditionPrecision::ResidualSearchRequired => 1, + SymbolicConditionPrecision::Unsupported => 0, + } +} + +fn compiled_condition_evidence_rank(condition: &SymbolicCompiledCondition) -> u8 { + match condition.evidence.tier { + SymbolicSemanticConfidence::Exact => 3, + SymbolicSemanticConfidence::Likely => 2, + SymbolicSemanticConfidence::Heuristic => 1, + SymbolicSemanticConfidence::Residual => 0, + } +} + +fn best_target_compiled_condition<'a>( + candidates: impl Iterator, +) -> Option<&'a SymbolicCompiledCondition> { + candidates.max_by(|left, right| { + ( + compiled_condition_evidence_rank(left), + compiled_condition_precision_rank(left), + std::cmp::Reverse(left.backward_memory_residual_fallbacks), + left.memory_terms.len(), + left.supported_paths, + std::cmp::Reverse(left.total_paths), + std::cmp::Reverse(left.simplified.len()), + ) + .cmp(&( + compiled_condition_evidence_rank(right), + compiled_condition_precision_rank(right), + std::cmp::Reverse(right.backward_memory_residual_fallbacks), + right.memory_terms.len(), + right.supported_paths, + std::cmp::Reverse(right.total_paths), + std::cmp::Reverse(right.simplified.len()), + )) + }) +} + +fn target_compiled_condition( + facts: &SymbolicSemanticFacts, + target_addr: u64, + hard_proof_only: bool, +) -> Option<&SymbolicCompiledCondition> { + let branch_candidates = facts.branch_facts.iter().flat_map(|fact| { + let mut candidates = Vec::new(); + if fact.true_target == target_addr { + let compiled = if hard_proof_only { + fact.true_compiled + .as_ref() + .filter(|compiled| compiled.evidence.allows_hard_proof()) + } else { + fact.true_compiled + .as_ref() + .filter(|compiled| compiled.evidence.allows_narrowing()) + }; + if let Some(compiled) = compiled { + candidates.push(compiled); + } + } + if fact.false_target == target_addr { + let compiled = if hard_proof_only { + fact.false_compiled + .as_ref() + .filter(|compiled| compiled.evidence.allows_hard_proof()) + } else { + fact.false_compiled + .as_ref() + .filter(|compiled| compiled.evidence.allows_narrowing()) + }; + if let Some(compiled) = compiled { + candidates.push(compiled); + } + } + candidates + }); + let control_candidates = facts + .worker_islands + .iter() + .flat_map(|island| island.control_facts.iter()) + .chain( + facts + .control_islands + .iter() + .flat_map(|island| island.facts.iter()), + ) + .filter_map(move |fact| { + (fact.target == target_addr) + .then_some(if hard_proof_only { + fact.exact_compiled_condition() + } else { + fact.actionable_compiled_condition() + }) + .flatten() + }); + best_target_compiled_condition(branch_candidates.chain(control_candidates)) +} + fn unique_reachable_control_target<'a>( facts: impl Iterator, hard_proof_only: bool, @@ -893,6 +1305,9 @@ fn unique_reachable_control_target<'a>( if hard_proof_only && !fact.evidence.allows_hard_proof() { return None; } + if !hard_proof_only && !fact.evidence.allows_narrowing() { + return None; + } match fact.status { SymbolicReachabilityStatus::Reachable => { if reachable_target.replace(fact.target).is_some() { @@ -918,6 +1333,7 @@ pub struct LocalFieldAccessFact { pub slot: usize, pub field_offset: u64, pub field_name: String, + pub field_type: Option, } #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -1224,7 +1640,12 @@ fn merge_local_field_accesses( .entry(access.slot) .or_default() .entry(access.field_offset) - .or_insert_with(|| access.field_name.clone()); + .or_insert_with(|| { + access + .field_type + .clone() + .unwrap_or_else(|| access.field_name.clone()) + }); } } @@ -1387,11 +1808,13 @@ mod tests { slot: 1, field_offset: 0, field_name: "first".to_string(), + field_type: None, }, LocalFieldAccessFact { slot: 1, field_offset: 8, field_name: "second".to_string(), + field_type: None, }, ], ..FunctionTypeFactInputs::default() @@ -1425,6 +1848,7 @@ mod tests { slot: 2, field_offset: 0, field_name: "local".to_string(), + field_type: None, }], ..FunctionTypeFactInputs::default() }) @@ -1439,6 +1863,28 @@ mod tests { ); } + #[test] + fn builder_prefers_local_field_access_type_when_present() { + let facts = FunctionTypeFacts::builder(FunctionTypeFactInputs { + local_field_accesses: vec![LocalFieldAccessFact { + slot: 3, + field_offset: 4, + field_name: "f_4".to_string(), + field_type: Some("int32_t".to_string()), + }], + ..FunctionTypeFactInputs::default() + }) + .build(); + + assert_eq!( + facts + .slot_field_profiles + .get(&3) + .and_then(|profile| profile.get(&4)), + Some(&"int32_t".to_string()) + ); + } + #[test] fn builder_merges_external_diagnostics_once() { let external = ExternalTypeDb { @@ -1701,6 +2147,7 @@ mod tests { }; let facts = SymbolicSemanticFacts { branch_facts: Vec::new(), + worker_islands: Vec::new(), control_islands: vec![SymbolicControlIsland { kind: SymbolicControlIslandKind::LargeCfgBranchFrontier, anchor_block: 0x401000, @@ -1716,6 +2163,7 @@ mod tests { evidence: SymbolicSemanticEvidence::exact(), confidence: SymbolicSemanticConfidence::Exact, }], + memory_islands: Vec::new(), diagnostics: SymbolicFactDiagnostics::default(), interpreter: None, vm_step: None, @@ -1730,10 +2178,97 @@ mod tests { ); } + #[test] + fn semantic_facts_actionable_condition_for_target_prefers_best_candidate() { + let exact = SymbolicCompiledCondition { + simplified: "x == 0".to_string(), + terms: vec!["x == 0".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: SymbolicConditionPrecision::Exact, + evidence: SymbolicSemanticEvidence::exact(), + confidence: SymbolicSemanticConfidence::Exact, + supported_paths: 1, + total_paths: 1, + }; + let likely = SymbolicCompiledCondition { + simplified: "x <= 1".to_string(), + terms: vec!["x <= 1".to_string()], + memory_terms: vec![SymbolicMemoryCondition { + region: SymbolicMemoryRegion::Argument { index: 0 }, + offset_lo: 8, + offset_hi: 12, + size: 4, + exact_offset: false, + expr: "*(arg0 + [8,12])".to_string(), + binding: None, + value_expr: None, + exact_value: false, + evidence: SymbolicSemanticEvidence::likely( + SymbolicSemanticEvidenceReason::DerivedFromRanking, + ), + confidence: SymbolicSemanticConfidence::Likely, + }], + backward_memory_substitutions: 1, + backward_memory_candidate_enumerations: 1, + backward_memory_residual_fallbacks: 0, + precision: SymbolicConditionPrecision::OverApprox, + evidence: SymbolicSemanticEvidence::likely( + SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: SymbolicSemanticConfidence::Likely, + supported_paths: 1, + total_paths: 2, + }; + let facts = SymbolicSemanticFacts { + branch_facts: vec![ + SymbolicBranchFact { + block_addr: 0x401000, + true_target: 0x401010, + false_target: 0x401020, + true_status: SymbolicReachabilityStatus::Reachable, + false_status: SymbolicReachabilityStatus::Unreachable, + true_condition: Some("x == 0".to_string()), + false_condition: Some("x != 0".to_string()), + true_compiled: Some(exact.clone()), + false_compiled: None, + }, + SymbolicBranchFact { + block_addr: 0x401030, + true_target: 0x401010, + false_target: 0x401040, + true_status: SymbolicReachabilityStatus::Reachable, + false_status: SymbolicReachabilityStatus::Unreachable, + true_condition: Some("x <= 1".to_string()), + false_condition: None, + true_compiled: Some(likely), + false_compiled: None, + }, + ], + worker_islands: Vec::new(), + control_islands: Vec::new(), + memory_islands: Vec::new(), + diagnostics: SymbolicFactDiagnostics::default(), + interpreter: None, + vm_step: None, + vm_transfer: None, + }; + + assert_eq!( + facts + .actionable_compiled_condition_for_target(0x401010) + .map(|cond| cond.simplified.as_str()), + Some("x == 0") + ); + } + #[test] fn semantic_facts_exact_reachable_target_for_block_uses_control_island_fallback() { let facts = SymbolicSemanticFacts { branch_facts: Vec::new(), + worker_islands: Vec::new(), control_islands: vec![SymbolicControlIsland { kind: SymbolicControlIslandKind::LargeCfgBranchFrontier, anchor_block: 0x401000, @@ -1759,6 +2294,7 @@ mod tests { evidence: SymbolicSemanticEvidence::exact(), confidence: SymbolicSemanticConfidence::Exact, }], + memory_islands: Vec::new(), diagnostics: SymbolicFactDiagnostics::default(), interpreter: None, vm_step: None, @@ -1770,4 +2306,189 @@ mod tests { Some(0x401010) ); } + + #[test] + fn semantic_facts_actionable_reachable_target_for_block_uses_likely_control_island() { + let compiled = SymbolicCompiledCondition { + simplified: "x == 0".to_string(), + terms: vec!["x == 0".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: SymbolicConditionPrecision::OverApprox, + evidence: SymbolicSemanticEvidence::likely( + SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: SymbolicSemanticConfidence::Likely, + supported_paths: 1, + total_paths: 2, + }; + let facts = SymbolicSemanticFacts { + branch_facts: Vec::new(), + worker_islands: Vec::new(), + control_islands: vec![SymbolicControlIsland { + kind: SymbolicControlIslandKind::LargeCfgBranchFrontier, + anchor_block: 0x401000, + frontier_targets: vec![0x401010, 0x401020], + facts: vec![ + SymbolicControlFact { + target: 0x401010, + status: SymbolicReachabilityStatus::Reachable, + condition: Some("x == 0".to_string()), + compiled: Some(compiled.clone()), + evidence: SymbolicSemanticEvidence::likely( + SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: SymbolicSemanticConfidence::Likely, + }, + SymbolicControlFact { + target: 0x401020, + status: SymbolicReachabilityStatus::Unreachable, + condition: Some("!(x == 0)".to_string()), + compiled: Some(compiled), + evidence: SymbolicSemanticEvidence::likely( + SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: SymbolicSemanticConfidence::Likely, + }, + ], + evidence: SymbolicSemanticEvidence::likely( + SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: SymbolicSemanticConfidence::Likely, + }], + memory_islands: Vec::new(), + diagnostics: SymbolicFactDiagnostics::default(), + interpreter: None, + vm_step: None, + vm_transfer: None, + }; + + assert_eq!( + facts.actionable_reachable_target_for_block(0x401000), + Some(0x401010) + ); + } + + #[test] + fn semantic_facts_actionable_memory_terms_for_block_use_memory_island_fallback() { + let facts = SymbolicSemanticFacts { + branch_facts: Vec::new(), + worker_islands: Vec::new(), + control_islands: Vec::new(), + memory_islands: vec![SymbolicMemoryIsland { + kind: SymbolicMemoryIslandKind::LargeCfgConditionFrontier, + anchor_block: 0x401000, + terms: vec![SymbolicMemoryCondition { + region: SymbolicMemoryRegion::Argument { index: 0 }, + offset_lo: 8, + offset_hi: 8, + size: 4, + exact_offset: true, + evidence: SymbolicSemanticEvidence::exact(), + confidence: SymbolicSemanticConfidence::Exact, + binding: Some("arg0".to_string()), + expr: "*(arg0 + 8)".to_string(), + value_expr: None, + exact_value: false, + }], + evidence: SymbolicSemanticEvidence::exact(), + confidence: SymbolicSemanticConfidence::Exact, + }], + diagnostics: SymbolicFactDiagnostics::default(), + interpreter: None, + vm_step: None, + vm_transfer: None, + }; + + let terms = facts.actionable_memory_terms_for_block(0x401000); + assert_eq!(terms.len(), 1); + assert_eq!(terms[0].offset_lo, 8); + assert_eq!(terms[0].binding.as_deref(), Some("arg0")); + } + + #[test] + fn structured_decompile_ready_allows_supported_worker_with_wide_frontier() { + let facts = SymbolicSemanticFacts { + worker_islands: vec![SymbolicWorkerIsland { + anchor_block: 0x401000, + control_kind: Some(SymbolicControlIslandKind::LargeCfgBranchFrontier), + memory_kind: Some(SymbolicMemoryIslandKind::LargeCfgConditionFrontier), + frontier_targets: vec![0x401010, 0x401020, 0x401030], + control_facts: vec![SymbolicControlFact { + target: 0x401010, + status: SymbolicReachabilityStatus::Reachable, + condition: Some("x == 0".to_string()), + compiled: Some(SymbolicCompiledCondition { + simplified: "x == 0".to_string(), + terms: vec!["x == 0".to_string()], + memory_terms: vec![SymbolicMemoryCondition { + region: SymbolicMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: SymbolicSemanticEvidence::likely( + SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: SymbolicSemanticConfidence::Likely, + binding: None, + expr: "*arg0".to_string(), + value_expr: Some("0x0:8".to_string()), + exact_value: true, + }], + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: SymbolicConditionPrecision::OverApprox, + evidence: SymbolicSemanticEvidence::likely( + SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: SymbolicSemanticConfidence::Likely, + supported_paths: 1, + total_paths: 2, + }), + evidence: SymbolicSemanticEvidence::likely( + SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: SymbolicSemanticConfidence::Likely, + }], + memory_terms: vec![SymbolicMemoryCondition { + region: SymbolicMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: SymbolicSemanticEvidence::likely( + SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: SymbolicSemanticConfidence::Likely, + binding: None, + expr: "*arg0".to_string(), + value_expr: Some("0x0:8".to_string()), + exact_value: true, + }], + evidence: SymbolicSemanticEvidence::likely( + SymbolicSemanticEvidenceReason::PartialPathCoverage, + ), + confidence: SymbolicSemanticConfidence::Likely, + }], + diagnostics: SymbolicFactDiagnostics { + skipped_large_cfg: true, + semantic_mode: Some(SymbolicSemanticMode::IslandCompiled), + semantic_capability: Some(SymbolicSemanticCapability { + query_ready: true, + type_ready: true, + decompile_ready: true, + }), + slice_class: Some(SymbolicSemanticSliceClass::Worker), + residual_reasons: Vec::new(), + ..Default::default() + }, + ..Default::default() + }; + + assert!(facts.structured_decompile_ready()); + } } diff --git a/crates/r2types/src/from_sym.rs b/crates/r2types/src/from_sym.rs new file mode 100644 index 0000000..50e72be --- /dev/null +++ b/crates/r2types/src/from_sym.rs @@ -0,0 +1,666 @@ +use crate::facts::{ + SymbolicBranchFact, SymbolicCompiledCondition, SymbolicConditionPrecision, SymbolicControlFact, + SymbolicControlIsland, SymbolicControlIslandKind, SymbolicFactDiagnostics, + SymbolicInterpreterDispatch, SymbolicInterpreterKind, SymbolicMemoryCondition, + SymbolicMemoryIsland, SymbolicMemoryIslandKind, SymbolicMemoryRegion, SymbolicMemoryRegionKind, + SymbolicMemoryRegionRef, SymbolicReachabilityStatus, SymbolicSemanticCapability, + SymbolicSemanticConfidence, SymbolicSemanticEvidence, SymbolicSemanticEvidenceAmbiguity, + SymbolicSemanticEvidenceCoverage, SymbolicSemanticEvidenceProvenance, + SymbolicSemanticEvidenceReason, SymbolicSemanticEvidenceSoundness, SymbolicSemanticFacts, + SymbolicSemanticMode, SymbolicSemanticResidualReason, SymbolicSemanticSliceClass, + SymbolicVmBinaryOp, SymbolicVmGuardCondition, SymbolicVmGuardedExit, SymbolicVmStateUpdate, + SymbolicVmStepSummary, SymbolicVmTransferArm, SymbolicVmUnaryOp, SymbolicVmValueExpr, + SymbolicWorkerIsland, +}; + +fn symbolic_reachability_status_from_sym( + status: r2sym::SymbolicReachabilityStatus, +) -> SymbolicReachabilityStatus { + match status { + r2sym::SymbolicReachabilityStatus::Reachable => SymbolicReachabilityStatus::Reachable, + r2sym::SymbolicReachabilityStatus::Unreachable => SymbolicReachabilityStatus::Unreachable, + r2sym::SymbolicReachabilityStatus::Unknown => SymbolicReachabilityStatus::Unknown, + } +} + +fn symbolic_condition_precision_from_sym( + precision: r2sym::BackwardConditionPrecision, +) -> SymbolicConditionPrecision { + match precision { + r2sym::BackwardConditionPrecision::Exact => SymbolicConditionPrecision::Exact, + r2sym::BackwardConditionPrecision::OverApprox => SymbolicConditionPrecision::OverApprox, + r2sym::BackwardConditionPrecision::ResidualSearchRequired => { + SymbolicConditionPrecision::ResidualSearchRequired + } + r2sym::BackwardConditionPrecision::Unsupported => SymbolicConditionPrecision::Unsupported, + } +} + +fn symbolic_memory_region_kind_from_sym( + kind: &r2sym::MemoryRegionKind, +) -> SymbolicMemoryRegionKind { + match kind { + r2sym::MemoryRegionKind::Stack => SymbolicMemoryRegionKind::Stack, + r2sym::MemoryRegionKind::Global => SymbolicMemoryRegionKind::Global, + r2sym::MemoryRegionKind::Input => SymbolicMemoryRegionKind::Input, + r2sym::MemoryRegionKind::Heap => SymbolicMemoryRegionKind::Heap, + r2sym::MemoryRegionKind::Replay => SymbolicMemoryRegionKind::Replay, + r2sym::MemoryRegionKind::EscapedUnknown => SymbolicMemoryRegionKind::EscapedUnknown, + } +} + +fn symbolic_memory_region_from_sym(region: &r2sym::BackwardMemoryRegion) -> SymbolicMemoryRegion { + match region { + r2sym::BackwardMemoryRegion::Argument { index } => { + SymbolicMemoryRegion::Argument { index: *index } + } + r2sym::BackwardMemoryRegion::Region(region) => { + SymbolicMemoryRegion::Region(SymbolicMemoryRegionRef { + id: region.id.0, + kind: symbolic_memory_region_kind_from_sym(®ion.kind), + name: region.name.clone(), + }) + } + } +} + +fn symbolic_confidence_from_sym( + confidence: r2sym::SemanticConfidence, +) -> SymbolicSemanticConfidence { + match confidence { + r2sym::SemanticConfidence::Exact => SymbolicSemanticConfidence::Exact, + r2sym::SemanticConfidence::Likely => SymbolicSemanticConfidence::Likely, + r2sym::SemanticConfidence::Heuristic => SymbolicSemanticConfidence::Heuristic, + r2sym::SemanticConfidence::Residual => SymbolicSemanticConfidence::Residual, + } +} + +fn symbolic_evidence_reason_from_sym( + reason: r2sym::SemanticEvidenceReason, +) -> SymbolicSemanticEvidenceReason { + match reason { + r2sym::SemanticEvidenceReason::LargeCfg => SymbolicSemanticEvidenceReason::LargeCfg, + r2sym::SemanticEvidenceReason::SummaryBudget => { + SymbolicSemanticEvidenceReason::SummaryBudget + } + r2sym::SemanticEvidenceReason::AliasAmbiguity => { + SymbolicSemanticEvidenceReason::AliasAmbiguity + } + r2sym::SemanticEvidenceReason::ReplayOverlap => { + SymbolicSemanticEvidenceReason::ReplayOverlap + } + r2sym::SemanticEvidenceReason::HeapIdentityWeak => { + SymbolicSemanticEvidenceReason::HeapIdentityWeak + } + r2sym::SemanticEvidenceReason::GuardOpaque => SymbolicSemanticEvidenceReason::GuardOpaque, + r2sym::SemanticEvidenceReason::ValueOpaque => SymbolicSemanticEvidenceReason::ValueOpaque, + r2sym::SemanticEvidenceReason::TruncatedTransfer => { + SymbolicSemanticEvidenceReason::TruncatedTransfer + } + r2sym::SemanticEvidenceReason::DerivedFromRanking => { + SymbolicSemanticEvidenceReason::DerivedFromRanking + } + r2sym::SemanticEvidenceReason::PartialPathCoverage => { + SymbolicSemanticEvidenceReason::PartialPathCoverage + } + r2sym::SemanticEvidenceReason::ResidualSearchRequired => { + SymbolicSemanticEvidenceReason::ResidualSearchRequired + } + } +} + +fn symbolic_evidence_from_sym(evidence: &r2sym::SemanticEvidence) -> SymbolicSemanticEvidence { + SymbolicSemanticEvidence { + tier: symbolic_confidence_from_sym(evidence.tier), + soundness: match evidence.soundness { + r2sym::SemanticEvidenceSoundness::Proven => SymbolicSemanticEvidenceSoundness::Proven, + r2sym::SemanticEvidenceSoundness::OverApprox => { + SymbolicSemanticEvidenceSoundness::OverApprox + } + r2sym::SemanticEvidenceSoundness::Ranked => SymbolicSemanticEvidenceSoundness::Ranked, + r2sym::SemanticEvidenceSoundness::Unknown => SymbolicSemanticEvidenceSoundness::Unknown, + }, + coverage: match evidence.coverage { + r2sym::SemanticEvidenceCoverage::Full => SymbolicSemanticEvidenceCoverage::Full, + r2sym::SemanticEvidenceCoverage::Partial => SymbolicSemanticEvidenceCoverage::Partial, + r2sym::SemanticEvidenceCoverage::Bounded => SymbolicSemanticEvidenceCoverage::Bounded, + }, + provenance: match evidence.provenance { + r2sym::SemanticEvidenceProvenance::Stable => SymbolicSemanticEvidenceProvenance::Stable, + r2sym::SemanticEvidenceProvenance::Normalized => { + SymbolicSemanticEvidenceProvenance::Normalized + } + r2sym::SemanticEvidenceProvenance::Ranked => SymbolicSemanticEvidenceProvenance::Ranked, + r2sym::SemanticEvidenceProvenance::Unstable => { + SymbolicSemanticEvidenceProvenance::Unstable + } + }, + ambiguity: match evidence.ambiguity { + r2sym::SemanticEvidenceAmbiguity::Single => SymbolicSemanticEvidenceAmbiguity::Single, + r2sym::SemanticEvidenceAmbiguity::Bounded => SymbolicSemanticEvidenceAmbiguity::Bounded, + r2sym::SemanticEvidenceAmbiguity::Ranked => SymbolicSemanticEvidenceAmbiguity::Ranked, + r2sym::SemanticEvidenceAmbiguity::Multiple => { + SymbolicSemanticEvidenceAmbiguity::Multiple + } + }, + budget_limited: evidence.budget_limited, + reasons: evidence + .reasons + .iter() + .copied() + .map(symbolic_evidence_reason_from_sym) + .collect(), + } +} + +fn symbolic_compiled_condition_from_sym( + summary: &r2sym::BackwardConditionSummary, +) -> SymbolicCompiledCondition { + let evidence = summary.evidence(); + SymbolicCompiledCondition { + simplified: summary.simplified.clone(), + terms: summary.terms.clone(), + memory_terms: summary + .memory_terms + .iter() + .map(|term| SymbolicMemoryCondition { + region: symbolic_memory_region_from_sym(&term.region), + offset_lo: term.offset_lo, + offset_hi: term.offset_hi, + size: term.size, + exact_offset: term.exact_offset, + evidence: symbolic_evidence_from_sym(&term.evidence()), + confidence: symbolic_confidence_from_sym(term.confidence()), + binding: term.binding.clone(), + expr: term.expr.clone(), + value_expr: term.value_expr.clone(), + exact_value: term.exact_value, + }) + .collect(), + backward_memory_substitutions: summary.backward_memory_substitutions, + backward_memory_candidate_enumerations: summary.backward_memory_candidate_enumerations, + backward_memory_residual_fallbacks: summary.backward_memory_residual_fallbacks, + precision: symbolic_condition_precision_from_sym(summary.precision), + evidence: symbolic_evidence_from_sym(&evidence), + confidence: symbolic_confidence_from_sym(evidence.tier), + supported_paths: summary.supported_paths, + total_paths: summary.total_paths, + } +} + +fn symbolic_control_island_kind_from_sym( + kind: r2sym::SymbolicControlIslandKind, +) -> SymbolicControlIslandKind { + match kind { + r2sym::SymbolicControlIslandKind::BranchFrontier => { + SymbolicControlIslandKind::BranchFrontier + } + r2sym::SymbolicControlIslandKind::LargeCfgBranchFrontier => { + SymbolicControlIslandKind::LargeCfgBranchFrontier + } + } +} + +fn symbolic_control_fact_from_sym(fact: &r2sym::SymbolicControlFact) -> SymbolicControlFact { + SymbolicControlFact { + target: fact.target, + status: symbolic_reachability_status_from_sym(fact.status), + condition: fact.condition.clone(), + compiled: fact + .compiled + .as_ref() + .map(symbolic_compiled_condition_from_sym), + evidence: symbolic_evidence_from_sym(&fact.evidence), + confidence: symbolic_confidence_from_sym(fact.evidence.tier), + } +} + +fn symbolic_memory_island_kind_from_sym( + kind: r2sym::SymbolicMemoryIslandKind, +) -> SymbolicMemoryIslandKind { + match kind { + r2sym::SymbolicMemoryIslandKind::ConditionFrontier => { + SymbolicMemoryIslandKind::ConditionFrontier + } + r2sym::SymbolicMemoryIslandKind::LargeCfgConditionFrontier => { + SymbolicMemoryIslandKind::LargeCfgConditionFrontier + } + } +} + +fn symbolic_control_island_from_sym( + island: &r2sym::SymbolicControlIsland, +) -> SymbolicControlIsland { + SymbolicControlIsland { + kind: symbolic_control_island_kind_from_sym(island.kind), + anchor_block: island.anchor_block, + frontier_targets: island.frontier_targets.clone(), + facts: island + .facts + .iter() + .map(symbolic_control_fact_from_sym) + .collect(), + evidence: symbolic_evidence_from_sym(&island.evidence), + confidence: symbolic_confidence_from_sym(island.evidence.tier), + } +} + +fn symbolic_memory_island_from_sym(island: &r2sym::SymbolicMemoryIsland) -> SymbolicMemoryIsland { + SymbolicMemoryIsland { + kind: symbolic_memory_island_kind_from_sym(island.kind), + anchor_block: island.anchor_block, + terms: island + .terms + .iter() + .map(|term| SymbolicMemoryCondition { + region: symbolic_memory_region_from_sym(&term.region), + offset_lo: term.offset_lo, + offset_hi: term.offset_hi, + size: term.size, + exact_offset: term.exact_offset, + evidence: symbolic_evidence_from_sym(&term.evidence()), + confidence: symbolic_confidence_from_sym(term.confidence()), + binding: term.binding.clone(), + expr: term.expr.clone(), + value_expr: term.value_expr.clone(), + exact_value: term.exact_value, + }) + .collect(), + evidence: symbolic_evidence_from_sym(&island.evidence), + confidence: symbolic_confidence_from_sym(island.evidence.tier), + } +} + +fn symbolic_worker_island_from_sym(island: &r2sym::SymbolicWorkerIsland) -> SymbolicWorkerIsland { + SymbolicWorkerIsland { + anchor_block: island.anchor_block, + control_kind: island + .control_kind + .map(symbolic_control_island_kind_from_sym), + memory_kind: island.memory_kind.map(symbolic_memory_island_kind_from_sym), + frontier_targets: island.frontier_targets.clone(), + control_facts: island + .control_facts + .iter() + .map(symbolic_control_fact_from_sym) + .collect(), + memory_terms: island + .memory_terms + .iter() + .map(|term| SymbolicMemoryCondition { + region: symbolic_memory_region_from_sym(&term.region), + offset_lo: term.offset_lo, + offset_hi: term.offset_hi, + size: term.size, + exact_offset: term.exact_offset, + evidence: symbolic_evidence_from_sym(&term.evidence()), + confidence: symbolic_confidence_from_sym(term.confidence()), + binding: term.binding.clone(), + expr: term.expr.clone(), + value_expr: term.value_expr.clone(), + exact_value: term.exact_value, + }) + .collect(), + evidence: symbolic_evidence_from_sym(&island.evidence), + confidence: symbolic_confidence_from_sym(island.evidence.tier), + } +} + +fn symbolic_interpreter_kind_from_sym(kind: r2sym::InterpreterKind) -> SymbolicInterpreterKind { + match kind { + r2sym::InterpreterKind::SwitchDispatch => SymbolicInterpreterKind::SwitchDispatch, + r2sym::InterpreterKind::IndirectDispatch => SymbolicInterpreterKind::IndirectDispatch, + } +} + +pub fn symbolic_vm_value_expr_from_sym(value: &r2sym::VmValueExpr) -> SymbolicVmValueExpr { + value.into() +} + +impl From<&r2sym::VmValueExpr> for SymbolicVmValueExpr { + fn from(value: &r2sym::VmValueExpr) -> Self { + match value { + r2sym::VmValueExpr::Const(value) => SymbolicVmValueExpr::Const(*value), + r2sym::VmValueExpr::Var(name) => SymbolicVmValueExpr::Var(name.clone()), + r2sym::VmValueExpr::Unary { op, arg } => SymbolicVmValueExpr::Unary { + op: match op { + r2sym::VmUnaryOp::Neg => SymbolicVmUnaryOp::Neg, + r2sym::VmUnaryOp::BitNot => SymbolicVmUnaryOp::Not, + r2sym::VmUnaryOp::BoolNot => SymbolicVmUnaryOp::BoolNot, + }, + expr: Box::new(SymbolicVmValueExpr::from(arg.as_ref())), + }, + r2sym::VmValueExpr::Binary { op, lhs, rhs } => SymbolicVmValueExpr::Binary { + op: match op { + r2sym::VmBinaryOp::Add => SymbolicVmBinaryOp::Add, + r2sym::VmBinaryOp::Sub => SymbolicVmBinaryOp::Sub, + r2sym::VmBinaryOp::Mul => SymbolicVmBinaryOp::Mul, + r2sym::VmBinaryOp::Div => SymbolicVmBinaryOp::Div, + r2sym::VmBinaryOp::Rem => SymbolicVmBinaryOp::Rem, + r2sym::VmBinaryOp::And => SymbolicVmBinaryOp::And, + r2sym::VmBinaryOp::Or => SymbolicVmBinaryOp::Or, + r2sym::VmBinaryOp::Xor => SymbolicVmBinaryOp::Xor, + r2sym::VmBinaryOp::Shl => SymbolicVmBinaryOp::Shl, + r2sym::VmBinaryOp::LShr | r2sym::VmBinaryOp::AShr => SymbolicVmBinaryOp::Shr, + r2sym::VmBinaryOp::Eq => SymbolicVmBinaryOp::Eq, + r2sym::VmBinaryOp::Ne => SymbolicVmBinaryOp::Ne, + r2sym::VmBinaryOp::Lt | r2sym::VmBinaryOp::SLt => SymbolicVmBinaryOp::Lt, + r2sym::VmBinaryOp::Le | r2sym::VmBinaryOp::SLe => SymbolicVmBinaryOp::Le, + r2sym::VmBinaryOp::BoolAnd => SymbolicVmBinaryOp::And, + r2sym::VmBinaryOp::BoolOr => SymbolicVmBinaryOp::Or, + }, + left: Box::new(SymbolicVmValueExpr::from(lhs.as_ref())), + right: Box::new(SymbolicVmValueExpr::from(rhs.as_ref())), + }, + r2sym::VmValueExpr::Expr(expr) => SymbolicVmValueExpr::Expr(expr.clone()), + } + } +} + +fn symbolic_vm_state_update_from_sym(update: &r2sym::VmStateUpdate) -> SymbolicVmStateUpdate { + let evidence = update.evidence(); + SymbolicVmStateUpdate { + output: update.output.clone(), + expr: update.expr.clone(), + value: SymbolicVmValueExpr::from(&update.value), + exact: update.exact, + evidence: symbolic_evidence_from_sym(&evidence), + confidence: symbolic_confidence_from_sym(evidence.tier), + } +} + +fn symbolic_vm_guard_condition_from_sym( + guard: &r2sym::VmGuardCondition, +) -> SymbolicVmGuardCondition { + let evidence = guard.evidence(); + SymbolicVmGuardCondition { + expr: guard.expr.clone(), + value: SymbolicVmValueExpr::from(&guard.value), + expect_nonzero: guard.expect_nonzero, + exact: guard.exact, + evidence: symbolic_evidence_from_sym(&evidence), + confidence: symbolic_confidence_from_sym(evidence.tier), + } +} + +fn symbolic_vm_guarded_exit_from_sym(guarded: &r2sym::VmGuardedExit) -> SymbolicVmGuardedExit { + SymbolicVmGuardedExit { + target: guarded.target, + guard: symbolic_vm_guard_condition_from_sym(&guarded.guard), + } +} + +fn symbolic_vm_memory_condition_from_sym( + condition: &r2sym::VmMemoryCondition, +) -> SymbolicMemoryCondition { + let evidence = condition.evidence(); + SymbolicMemoryCondition { + region: SymbolicMemoryRegion::Region(SymbolicMemoryRegionRef { + id: condition.region.id, + kind: symbolic_memory_region_kind_from_sym(&condition.region.kind), + name: condition.region.name.clone(), + }), + offset_lo: condition.offset_lo, + offset_hi: condition.offset_hi, + size: condition.size, + exact_offset: condition.exact_offset, + evidence: symbolic_evidence_from_sym(&evidence), + confidence: symbolic_confidence_from_sym(evidence.tier), + binding: condition.binding.clone(), + expr: condition.expr.clone(), + value_expr: condition.value_expr.clone(), + exact_value: condition.exact_value, + } +} + +fn symbolic_vm_transfer_arm_from_sym(transfer: &r2sym::VmTransferArm) -> SymbolicVmTransferArm { + let evidence = transfer.evidence(); + SymbolicVmTransferArm { + handler_target: transfer.handler_target, + case_values: transfer.case_values.clone(), + region_blocks: transfer.region_blocks.clone(), + exit_targets: transfer.exit_targets.clone(), + exit_guards: transfer + .exit_guards + .iter() + .map(symbolic_vm_guarded_exit_from_sym) + .collect(), + state_updates: transfer + .state_updates + .iter() + .map(symbolic_vm_state_update_from_sym) + .collect(), + selector_update: transfer + .selector_update + .as_ref() + .map(symbolic_vm_state_update_from_sym), + memory_reads: transfer + .memory_reads + .iter() + .map(symbolic_vm_memory_condition_from_sym) + .collect(), + memory_writes: transfer + .memory_writes + .iter() + .map(symbolic_vm_memory_condition_from_sym) + .collect(), + residual_guards: transfer.residual_guards, + residual_memory_effects: transfer.residual_memory_effects, + exact: transfer.exact, + evidence: symbolic_evidence_from_sym(&evidence), + confidence: symbolic_confidence_from_sym(evidence.tier), + redispatch: transfer.redispatch, + may_return: transfer.may_return, + truncated: transfer.truncated, + } +} + +fn symbolic_vm_step_summary_from_sym(vm_step: &r2sym::VmStepSummary) -> SymbolicVmStepSummary { + SymbolicVmStepSummary { + kind: symbolic_interpreter_kind_from_sym(vm_step.kind), + loop_header: vm_step.loop_header, + dispatch_header: vm_step.dispatch_header, + selector: vm_step.selector.clone(), + dispatch_targets: vm_step.dispatch_targets.clone(), + default_target: vm_step.default_target, + case_values_by_target: vm_step.case_values_by_target.clone(), + loop_latches: vm_step.loop_latches.clone(), + state_inputs: vm_step.state_inputs.clone(), + state_outputs: vm_step.state_outputs.clone(), + step_blocks: vm_step.step_blocks.clone(), + handler_regions: vm_step.handler_regions.clone(), + handler_state_inputs: vm_step.handler_state_inputs.clone(), + handler_state_outputs: vm_step.handler_state_outputs.clone(), + handler_state_updates: vm_step + .handler_state_updates + .iter() + .map(|(target, updates)| { + ( + *target, + updates + .iter() + .map(symbolic_vm_state_update_from_sym) + .collect(), + ) + }) + .collect(), + handler_exit_guards: vm_step + .handler_exit_guards + .iter() + .map(|(target, guards)| { + ( + *target, + guards + .iter() + .map(symbolic_vm_guarded_exit_from_sym) + .collect(), + ) + }) + .collect(), + handler_memory_read_effects: vm_step + .handler_memory_read_effects + .iter() + .map(|(target, effects)| { + ( + *target, + effects + .iter() + .map(symbolic_vm_memory_condition_from_sym) + .collect(), + ) + }) + .collect(), + handler_memory_write_effects: vm_step + .handler_memory_write_effects + .iter() + .map(|(target, effects)| { + ( + *target, + effects + .iter() + .map(symbolic_vm_memory_condition_from_sym) + .collect(), + ) + }) + .collect(), + handler_memory_reads: vm_step.handler_memory_reads.clone(), + handler_memory_writes: vm_step.handler_memory_writes.clone(), + handler_calls: vm_step.handler_calls.clone(), + handler_conditional_branches: vm_step.handler_conditional_branches.clone(), + handler_exit_targets: vm_step.handler_exit_targets.clone(), + redispatch_handlers: vm_step.redispatch_handlers.clone(), + returning_handlers: vm_step.returning_handlers.clone(), + truncated_handlers: vm_step.truncated_handlers.clone(), + transfers: vm_step + .transfers + .iter() + .map(symbolic_vm_transfer_arm_from_sym) + .collect(), + } +} + +pub fn symbolic_semantic_facts_from_artifact( + compiled: &r2sym::CompiledSemanticArtifact, +) -> SymbolicSemanticFacts { + compiled.into() +} + +impl From<&r2sym::CompiledSemanticArtifact> for SymbolicSemanticFacts { + fn from(compiled: &r2sym::CompiledSemanticArtifact) -> Self { + let facts = &compiled.symbolic_facts; + SymbolicSemanticFacts { + branch_facts: facts + .branch_facts + .iter() + .map(|fact| SymbolicBranchFact { + block_addr: fact.block_addr, + true_target: fact.true_target, + false_target: fact.false_target, + true_status: symbolic_reachability_status_from_sym(fact.true_status), + false_status: symbolic_reachability_status_from_sym(fact.false_status), + true_condition: fact.true_condition.clone(), + false_condition: fact.false_condition.clone(), + true_compiled: fact + .true_compiled + .as_ref() + .map(symbolic_compiled_condition_from_sym), + false_compiled: fact + .false_compiled + .as_ref() + .map(symbolic_compiled_condition_from_sym), + }) + .collect(), + worker_islands: facts + .worker_islands + .iter() + .map(symbolic_worker_island_from_sym) + .collect(), + control_islands: facts + .control_islands + .iter() + .map(symbolic_control_island_from_sym) + .collect(), + memory_islands: facts + .memory_islands + .iter() + .map(symbolic_memory_island_from_sym) + .collect(), + diagnostics: SymbolicFactDiagnostics { + branches_evaluated: facts.diagnostics.branches_evaluated, + branches_pruned: facts.diagnostics.branches_pruned, + branches_unknown: facts.diagnostics.branches_unknown, + skipped_missing_arch: facts.diagnostics.skipped_missing_arch, + skipped_large_cfg: facts.diagnostics.skipped_large_cfg, + cache_hit: compiled.cache_hit, + semantic_mode: Some(match compiled.mode { + r2sym::SemanticMode::Raw => SymbolicSemanticMode::Raw, + r2sym::SemanticMode::Compiled => SymbolicSemanticMode::Compiled, + r2sym::SemanticMode::IslandCompiled => SymbolicSemanticMode::IslandCompiled, + r2sym::SemanticMode::Residual => SymbolicSemanticMode::Residual, + r2sym::SemanticMode::VmSummary => SymbolicSemanticMode::VmSummary, + }), + semantic_capability: Some(SymbolicSemanticCapability { + query_ready: compiled.capability.query_ready, + type_ready: compiled.capability.type_ready, + decompile_ready: compiled.capability.decompile_ready, + }), + slice_class: Some(match compiled.slice_class { + r2sym::SliceClass::Wrapper => SymbolicSemanticSliceClass::Wrapper, + r2sym::SliceClass::Worker => SymbolicSemanticSliceClass::Worker, + r2sym::SliceClass::RecursiveGroup => SymbolicSemanticSliceClass::RecursiveGroup, + r2sym::SliceClass::InterpreterSwitch => { + SymbolicSemanticSliceClass::InterpreterSwitch + } + r2sym::SliceClass::InterpreterIndirect => { + SymbolicSemanticSliceClass::InterpreterIndirect + } + r2sym::SliceClass::GenericLarge => SymbolicSemanticSliceClass::GenericLarge, + }), + residual_reasons: compiled + .residual_reasons + .iter() + .map(|reason| match reason { + r2sym::ResidualReason::MissingArch => { + SymbolicSemanticResidualReason::MissingArch + } + r2sym::ResidualReason::LargeCfg => SymbolicSemanticResidualReason::LargeCfg, + r2sym::ResidualReason::SummaryBudgetExhausted => { + SymbolicSemanticResidualReason::SummaryBudgetExhausted + } + r2sym::ResidualReason::SccBudgetExhausted => { + SymbolicSemanticResidualReason::SccBudgetExhausted + } + r2sym::ResidualReason::InterpreterRequiresStepSummary => { + SymbolicSemanticResidualReason::InterpreterRequiresStepSummary + } + }) + .collect(), + closure_functions: compiled.closure_functions, + helper_functions: compiled.helper_functions, + derived_summaries: compiled.derived_summaries, + summary_attempted: compiled.derived_diagnostics.attempted, + summary_budget_exhausted: compiled.derived_diagnostics.budget_exhausted + + compiled.derived_diagnostics.scc_budget_exhausted, + summary_scc_count: compiled.derived_diagnostics.scc_count, + }, + interpreter: compiled.interpreter.as_ref().map(|interpreter| { + SymbolicInterpreterDispatch { + kind: symbolic_interpreter_kind_from_sym(interpreter.kind), + dispatch_header: interpreter.dispatch_header, + dispatch_targets: interpreter.dispatch_targets, + selector: interpreter.selector.clone(), + back_edges: interpreter.back_edges, + score: interpreter.score, + } + }), + vm_step: compiled + .vm_step + .as_ref() + .map(symbolic_vm_step_summary_from_sym), + vm_transfer: compiled + .vm_transfer + .as_ref() + .map(symbolic_vm_step_summary_from_sym), + } + } +} diff --git a/crates/r2types/src/lib.rs b/crates/r2types/src/lib.rs index 494b20d..fedd264 100644 --- a/crates/r2types/src/lib.rs +++ b/crates/r2types/src/lib.rs @@ -3,11 +3,14 @@ pub mod context; pub mod convert; pub mod external; pub mod facts; +pub mod from_sym; pub mod inference; pub mod lattice; pub mod model; pub mod oracle; +pub mod prepare; pub mod signature; +pub mod signature_infer; pub mod solver; pub mod writeback; @@ -33,25 +36,45 @@ pub use facts::{ SymbolicBranchFact, SymbolicCompiledCondition, SymbolicConditionPrecision, SymbolicControlFact, SymbolicControlIsland, SymbolicControlIslandKind, SymbolicFactDiagnostics, SymbolicInterpreterDispatch, SymbolicInterpreterKind, SymbolicMemoryCondition, - SymbolicMemoryRegion, SymbolicMemoryRegionKind, SymbolicMemoryRegionRef, - SymbolicReachabilityStatus, SymbolicSemanticCapability, SymbolicSemanticConfidence, - SymbolicSemanticEvidence, SymbolicSemanticEvidenceAmbiguity, SymbolicSemanticEvidenceCoverage, - SymbolicSemanticEvidenceProvenance, SymbolicSemanticEvidenceReason, - SymbolicSemanticEvidenceSoundness, SymbolicSemanticFacts, SymbolicSemanticMode, - SymbolicSemanticResidualReason, SymbolicSemanticSliceClass, SymbolicVmBinaryOp, - SymbolicVmGuardCondition, SymbolicVmGuardedExit, SymbolicVmStateUpdate, SymbolicVmStepSummary, - SymbolicVmTransferArm, SymbolicVmTransferSummary, SymbolicVmUnaryOp, SymbolicVmValueExpr, - VisibleBinding, VisibleBindingKind, parse_type_like_spec, + SymbolicMemoryIsland, SymbolicMemoryIslandKind, SymbolicMemoryRegion, SymbolicMemoryRegionKind, + SymbolicMemoryRegionRef, SymbolicReachabilityStatus, SymbolicSemanticCapability, + SymbolicSemanticConfidence, SymbolicSemanticEvidence, SymbolicSemanticEvidenceAmbiguity, + SymbolicSemanticEvidenceCoverage, SymbolicSemanticEvidenceProvenance, + SymbolicSemanticEvidenceReason, SymbolicSemanticEvidenceSoundness, SymbolicSemanticFacts, + SymbolicSemanticMode, SymbolicSemanticResidualReason, SymbolicSemanticSliceClass, + SymbolicVmBinaryOp, SymbolicVmGuardCondition, SymbolicVmGuardedExit, SymbolicVmStateUpdate, + SymbolicVmStepSummary, SymbolicVmTransferArm, SymbolicVmTransferSummary, SymbolicVmUnaryOp, + SymbolicVmValueExpr, SymbolicWorkerIsland, VisibleBinding, VisibleBindingKind, + parse_type_like_spec, }; +pub use from_sym::{symbolic_semantic_facts_from_artifact, symbolic_vm_value_expr_from_sym}; pub use inference::{CombinedTypeOracle, TypeInference}; pub use model::{Signedness, StructField, StructShape, Type, TypeArena, TypeId}; pub use oracle::{LayoutOracle, TypeOracle}; +pub use prepare::{ + ArgAliasMap, BaseRegList, SignatureTypeEvidenceContext, TypeHint, TypeHintRank, X86_ARG_REGS, + X86_FRAME_BASES, collect_pointer_arg_slots, collect_signature_type_evidence_context, + merge_type_hint, recover_vars_arch_profile, recover_vars_from_ssa, scalar_register_family_key, + size_to_type, ssa_var_block_key, ssa_var_key, +}; pub use signature::{ResolvedSignature, SignatureRegistry}; +pub use signature_infer::{ + RecoveredSignatureParam, SignatureParamCandidate, SignatureTypeEvidence, + build_inferred_signature, collect_signature_type_evidence_for_var, collect_version0_input_regs, + compute_callconv_inference, compute_signature_confidence, + enrich_known_function_signatures_from_names, format_afs_signature, + infer_signature_from_prepared_ssa, infer_signature_return_type, + materialize_signature_type_like, merge_initial_signature_type_evidence, + merge_pointer_slot_evidence_into_signature_params, render_signature_type, + resolve_evidence_driven_signature_type, +}; pub use solver::{SolvedTypes, SolverConfig, SolverDiagnostics, TypeSolver}; pub use writeback::{ GlobalTypeLinkCandidate, InferredSignature, InferredSignatureParam, LocalStructArtifacts, RecoveredVariable, StructDeclCandidate, StructDeclSource, StructFieldCandidate, TypeWritebackAnalysis, TypeWritebackAnalysisInput, TypeWritebackDiagnostics, TypeWritebackPlan, - VarRenameCandidate, VarTypeCandidate, WritebackEvidence, WritebackSource, - augment_local_struct_artifacts_with_symbolic_facts, build_type_writeback_analysis, + TypeWritebackSemanticInputs, VarRenameCandidate, VarTypeCandidate, WritebackEvidence, + WritebackSource, augment_local_struct_artifacts_with_symbolic_facts, + build_semantic_type_fallback_plan, build_type_writeback_analysis, + build_type_writeback_analysis_with_semantics, infer_local_struct_artifacts_from_ssa, }; diff --git a/crates/r2types/src/prepare.rs b/crates/r2types/src/prepare.rs new file mode 100644 index 0000000..572d9b1 --- /dev/null +++ b/crates/r2types/src/prepare.rs @@ -0,0 +1,1250 @@ +use std::collections::{BTreeSet, HashMap, HashSet}; + +use r2ssa::{SSABlock, SSAOp, SSAVar}; + +use crate::writeback::RecoveredVariable; + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct SignatureTypeEvidenceContext { + pub pointer_vars: HashSet, + pub scalar_proven_vars: HashSet, + pub scalar_likely_vars: HashSet, + pub bool_like_vars: HashSet, + pub width_bits: HashMap, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum TypeHintRank { + Integer = 1, + Float = 2, + Pointer = 3, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TypeHint { + pub rank: TypeHintRank, + pub ty: String, +} + +impl TypeHint { + pub fn pointer() -> Self { + Self { + rank: TypeHintRank::Pointer, + ty: "void *".to_string(), + } + } +} + +pub type ArgAliasMap = &'static [(&'static str, &'static [&'static str])]; +pub type BaseRegList = &'static [&'static str]; + +pub const X86_ARG_REGS: &[(&str, &[&str])] = &[ + ("rdi", &["rdi", "edi", "di", "dil"]), + ("rsi", &["rsi", "esi", "si", "sil"]), + ("rdx", &["rdx", "edx", "dx", "dl", "dh"]), + ("rcx", &["rcx", "ecx", "cx", "cl", "ch"]), + ("r8", &["r8", "r8d", "r8w", "r8b"]), + ("r9", &["r9", "r9d", "r9w", "r9b"]), +]; +const RISCV_ARG_REGS: &[(&str, &[&str])] = &[ + ("a0", &["a0", "x10"]), + ("a1", &["a1", "x11"]), + ("a2", &["a2", "x12"]), + ("a3", &["a3", "x13"]), + ("a4", &["a4", "x14"]), + ("a5", &["a5", "x15"]), + ("a6", &["a6", "x16"]), + ("a7", &["a7", "x17"]), +]; +const ARM64_ARG_REGS: &[(&str, &[&str])] = &[ + ("x0", &["x0", "w0"]), + ("x1", &["x1", "w1"]), + ("x2", &["x2", "w2"]), + ("x3", &["x3", "w3"]), + ("x4", &["x4", "w4"]), + ("x5", &["x5", "w5"]), + ("x6", &["x6", "w6"]), + ("x7", &["x7", "w7"]), +]; +const ARM32_ARG_REGS: &[(&str, &[&str])] = &[ + ("r0", &["r0"]), + ("r1", &["r1"]), + ("r2", &["r2"]), + ("r3", &["r3"]), +]; +const MIPS_ARG_REGS: &[(&str, &[&str])] = &[ + ("a0", &["a0", "$a0", "r4"]), + ("a1", &["a1", "$a1", "r5"]), + ("a2", &["a2", "$a2", "r6"]), + ("a3", &["a3", "$a3", "r7"]), +]; +const X86_STACK_BASES: &[&str] = &["rbp", "rsp", "ebp", "esp"]; +pub const X86_FRAME_BASES: &[&str] = &["rbp", "ebp"]; +const RISCV_STACK_BASES: &[&str] = &["sp", "s0", "fp", "x2", "x8"]; +const RISCV_FRAME_BASES: &[&str] = &["s0", "fp", "x8"]; +const ARM64_STACK_BASES: &[&str] = &["sp", "x29", "fp"]; +const ARM64_FRAME_BASES: &[&str] = &["x29", "fp"]; +const ARM32_STACK_BASES: &[&str] = &["sp", "r11", "fp"]; +const ARM32_FRAME_BASES: &[&str] = &["r11", "fp"]; +const MIPS_STACK_BASES: &[&str] = &["sp", "$sp", "fp", "$fp", "s8", "$s8"]; +const MIPS_FRAME_BASES: &[&str] = &["fp", "$fp", "s8", "$s8"]; +const GENERIC_STACK_BASES: &[&str] = &["sp", "fp", "bp", "s0", "x2", "x8", "rbp", "rsp"]; +const GENERIC_FRAME_BASES: &[&str] = &["fp", "bp", "s0", "x8", "rbp"]; + +pub fn recover_vars_arch_profile( + arch_name: Option<&str>, +) -> (ArgAliasMap, BaseRegList, BaseRegList) { + let Some(arch_name) = arch_name else { + return (&[], GENERIC_STACK_BASES, GENERIC_FRAME_BASES); + }; + + let arch_name = arch_name.to_ascii_lowercase(); + if arch_name.contains("x86") { + return (X86_ARG_REGS, X86_STACK_BASES, X86_FRAME_BASES); + } + if arch_name.contains("aarch64") || arch_name.contains("arm64") { + return (ARM64_ARG_REGS, ARM64_STACK_BASES, ARM64_FRAME_BASES); + } + if arch_name == "arm" || arch_name.starts_with("armv") { + return (ARM32_ARG_REGS, ARM32_STACK_BASES, ARM32_FRAME_BASES); + } + if arch_name.contains("riscv") || arch_name.starts_with("rv") { + return (RISCV_ARG_REGS, RISCV_STACK_BASES, RISCV_FRAME_BASES); + } + if arch_name.contains("mips") { + return (MIPS_ARG_REGS, MIPS_STACK_BASES, MIPS_FRAME_BASES); + } + + (&[], GENERIC_STACK_BASES, GENERIC_FRAME_BASES) +} + +pub fn size_to_type(size: u32) -> String { + match size { + 1 => "int8_t".to_string(), + 2 => "int16_t".to_string(), + 4 => "int32_t".to_string(), + 8 => "int64_t".to_string(), + _ => format!("byte[{size}]"), + } +} + +pub fn ssa_var_key(var: &SSAVar) -> String { + format!("{}_{}", var.name.to_ascii_lowercase(), var.version) +} + +pub fn ssa_var_block_key(block_addr: u64, var: &SSAVar) -> String { + format!("{}@{block_addr:x}", ssa_var_key(var)) +} + +pub fn scalar_register_family_key(name: &str) -> String { + let lower = name.to_ascii_lowercase(); + + if let Some(idx) = lower.strip_prefix('x').or_else(|| lower.strip_prefix('w')) + && !idx.is_empty() + && idx.chars().all(|ch| ch.is_ascii_digit()) + { + return format!("aarch64:gpr:{idx}"); + } + + match lower.as_str() { + "fp" => "aarch64:gpr:29".to_string(), + "lr" => "aarch64:gpr:30".to_string(), + "sp" | "wsp" => "aarch64:sp".to_string(), + "xzr" | "wzr" => "aarch64:zr".to_string(), + _ => lower, + } +} + +pub fn merge_type_hint(hints: &mut HashMap, key: String, incoming: TypeHint) { + match hints.get(&key) { + Some(current) if !incoming_hint_should_replace(current, &incoming) => {} + _ => { + hints.insert(key, incoming); + } + } +} + +pub fn collect_signature_type_evidence_context( + ssa_blocks: &[SSABlock], +) -> SignatureTypeEvidenceContext { + let pointer_vars = infer_pointer_var_keys_from_ssa(ssa_blocks); + let (scalar_proven_vars, scalar_likely_vars, bool_like_vars, mut width_bits) = + infer_scalar_var_evidence_from_ssa(ssa_blocks); + let register_versions = collect_register_version_keys(ssa_blocks); + normalize_register_family_width_hints(®ister_versions, &mut width_bits); + propagate_normalized_scalar_result_widths(ssa_blocks, &mut width_bits); + normalize_register_family_width_hints(®ister_versions, &mut width_bits); + SignatureTypeEvidenceContext { + pointer_vars, + scalar_proven_vars, + scalar_likely_vars, + bool_like_vars, + width_bits, + } +} + +pub fn collect_pointer_arg_slots(vars: &[RecoveredVariable]) -> BTreeSet { + vars.iter() + .filter(|var| var.kind == "r" && var.isarg && var.var_type.contains('*')) + .filter_map(|var| { + var.name + .strip_prefix("arg") + .and_then(|idx| idx.parse::().ok()) + }) + .collect() +} + +pub fn recover_vars_from_ssa( + ssa_blocks: &[SSABlock], + arch_name: Option<&str>, + metadata_reg_type_hints: &HashMap, + semantic_typing_enabled: bool, +) -> Vec { + let mut vars = Vec::new(); + let mut seen_slots: HashMap<(bool, i64), usize> = HashMap::new(); + let mut seen_arg_regs: HashSet = HashSet::new(); + let (arg_regs, stack_bases, frame_bases) = recover_vars_arch_profile(arch_name); + let signature_evidence = + semantic_typing_enabled.then(|| collect_signature_type_evidence_context(ssa_blocks)); + let (usage_reg_type_hints, pointer_var_keys) = if semantic_typing_enabled { + infer_usage_register_type_hints(ssa_blocks) + } else { + (HashMap::new(), HashSet::new()) + }; + let reg_type_hints = if semantic_typing_enabled { + merge_register_type_hints(metadata_reg_type_hints, &usage_reg_type_hints, arg_regs) + } else { + HashMap::new() + }; + + let mut stack_addr_temps: HashMap = HashMap::new(); + + for block in ssa_blocks { + for op in &block.ops { + match op { + SSAOp::IntAdd { dst, a, b } | SSAOp::IntSub { dst, a, b } => { + let a_name = a.name.to_lowercase(); + let b_name = b.name.to_lowercase(); + + let is_a_base = stack_bases.contains(&a_name.as_str()); + let is_b_const = b_name.starts_with("const:"); + + if is_a_base && is_b_const { + if let Some(raw_offset) = parse_const_value(&b.name) { + let offset = if matches!(op, SSAOp::IntSub { .. }) { + -(raw_offset as i64) + } else { + raw_offset as i64 + }; + let dst_key = ssa_var_block_key(block.addr, dst); + stack_addr_temps.insert(dst_key, (a_name.clone(), offset)); + } + } else if stack_bases.contains(&b_name.as_str()) + && a_name.starts_with("const:") + && let Some(raw_offset) = parse_const_value(&a.name) + { + let offset = raw_offset as i64; + let dst_key = ssa_var_block_key(block.addr, dst); + stack_addr_temps.insert(dst_key, (b_name.clone(), offset)); + } + } + SSAOp::Store { addr, val, .. } => { + let addr_key = ssa_var_block_key(block.addr, addr); + if let Some((base_reg, offset)) = stack_addr_temps.get(&addr_key) { + let type_override = if semantic_typing_enabled + && pointer_var_keys.contains(&ssa_var_key(val)) + { + Some("void *".to_string()) + } else { + None + }; + add_stack_var( + &mut vars, + &mut seen_slots, + base_reg, + frame_bases, + *offset, + val.size, + type_override, + ); + } + } + SSAOp::Load { dst, addr, .. } => { + let addr_key = ssa_var_block_key(block.addr, addr); + if let Some((base_reg, offset)) = stack_addr_temps.get(&addr_key) { + let type_override = if semantic_typing_enabled + && pointer_var_keys.contains(&ssa_var_key(dst)) + { + Some("void *".to_string()) + } else { + None + }; + add_stack_var( + &mut vars, + &mut seen_slots, + base_reg, + frame_bases, + *offset, + dst.size, + type_override, + ); + } + } + _ => {} + } + + for src in op.sources() { + let base_name = src.name.to_lowercase(); + if src.version == 0 { + for (i, (canonical, aliases)) in arg_regs.iter().enumerate() { + if aliases.contains(&base_name.as_str()) + && !seen_arg_regs.contains(*canonical) + { + seen_arg_regs.insert(canonical.to_string()); + let hinted_type = if semantic_typing_enabled { + recovered_arg_type_hint( + ®_type_hints, + canonical, + aliases, + src, + signature_evidence.as_ref(), + ) + } else { + None + }; + vars.push(RecoveredVariable { + name: format!("arg{i}"), + kind: "r".to_string(), + delta: 0, + var_type: hinted_type.unwrap_or_else(|| size_to_type(src.size)), + isarg: true, + reg: Some(canonical.to_string()), + }); + break; + } + } + } + } + } + } + + vars.sort_by_key(|v| v.delta); + vars +} + +fn incoming_hint_should_replace(current: &TypeHint, incoming: &TypeHint) -> bool { + incoming.rank > current.rank || (incoming.rank == current.rank && incoming.ty < current.ty) +} + +fn parse_const_value(name: &str) -> Option { + let val_str = name + .strip_prefix("const:") + .or_else(|| name.strip_prefix("CONST:"))?; + let val_str = val_str.split('_').next().unwrap_or(val_str); + if let Some(hex) = val_str + .strip_prefix("0x") + .or_else(|| val_str.strip_prefix("0X")) + { + return u64::from_str_radix(hex, 16).ok(); + } + if let Ok(v) = val_str.parse::() { + return Some(v); + } + u64::from_str_radix(val_str, 16).ok() +} + +fn ssa_var_is_const(var: &SSAVar) -> bool { + parse_const_value(&var.name).is_some() +} + +fn ssa_var_is_register_like(name: &str) -> bool { + let lower = name.to_ascii_lowercase(); + !(lower.starts_with("tmp:") + || lower.starts_with("const:") + || lower.starts_with("ram:") + || lower.starts_with("space")) +} + +fn strongest_hint_for_aliases( + hints: &HashMap, + canonical: &str, + aliases: &[&str], +) -> Option { + let mut best = hints.get(canonical).cloned(); + for alias in aliases { + if let Some(candidate) = hints.get(*alias).cloned() { + match &best { + Some(current) if !incoming_hint_should_replace(current, &candidate) => {} + _ => best = Some(candidate), + } + } + } + best +} + +fn width_hint_key_matches_family(key: &str, family: &str) -> bool { + let Some((name, _version)) = key.rsplit_once('_') else { + return false; + }; + scalar_register_family_key(name) == family +} + +fn width_hint_key_matches_family_version(key: &str, family: &str, version: u32) -> bool { + let Some((name, version_str)) = key.rsplit_once('_') else { + return false; + }; + version_str.parse::().ok() == Some(version) && scalar_register_family_key(name) == family +} + +fn recovered_arg_family_width_hint( + evidence: &SignatureTypeEvidenceContext, + src: &SSAVar, +) -> Option { + let family = scalar_register_family_key(&src.name); + evidence + .width_bits + .iter() + .filter(|(key, bits)| { + **bits > 0 && width_hint_key_matches_family_version(key, &family, src.version) + }) + .map(|(_, bits)| *bits) + .min() + .or_else(|| { + evidence + .width_bits + .iter() + .filter(|(key, bits)| **bits > 0 && width_hint_key_matches_family(key, &family)) + .map(|(_, bits)| *bits) + .min() + }) +} + +fn recovered_arg_type_hint( + reg_type_hints: &HashMap, + canonical: &str, + aliases: &[&str], + src: &SSAVar, + signature_evidence: Option<&SignatureTypeEvidenceContext>, +) -> Option { + let best_hint = strongest_hint_for_aliases(reg_type_hints, canonical, aliases); + if let Some(hint) = best_hint.as_ref() + && hint.rank == TypeHintRank::Pointer + { + return Some(hint.ty.clone()); + } + if let Some(evidence) = signature_evidence + && let Some(bits) = recovered_arg_family_width_hint(evidence, src) + { + return Some(size_to_type(bits.div_ceil(8))); + } + best_hint.map(|hint| hint.ty) +} + +fn merge_register_type_hints( + metadata_hints: &HashMap, + usage_hints: &HashMap, + arg_regs: ArgAliasMap, +) -> HashMap { + let mut merged = HashMap::new(); + + for (reg, hint) in metadata_hints { + merge_type_hint(&mut merged, reg.clone(), hint.clone()); + } + for (reg, hint) in usage_hints { + merge_type_hint(&mut merged, reg.clone(), hint.clone()); + } + + for (canonical, aliases) in arg_regs { + if let Some(best) = strongest_hint_for_aliases(&merged, canonical, aliases) { + merge_type_hint(&mut merged, (*canonical).to_string(), best.clone()); + for alias in *aliases { + merge_type_hint(&mut merged, alias.to_string(), best.clone()); + } + } + } + + merged +} + +fn add_stack_var( + vars: &mut Vec, + seen_slots: &mut HashMap<(bool, i64), usize>, + base_reg: &str, + frame_bases: &[&str], + offset: i64, + size: u32, + type_override: Option, +) { + let is_frame_base = frame_bases.contains(&base_reg); + let slot_key = (is_frame_base, offset); + if let Some(existing_idx) = seen_slots.get(&slot_key).copied() { + if let Some(override_ty) = type_override + && override_ty == "void *" + && let Some(existing) = vars.get_mut(existing_idx) + && existing.var_type != "void *" + { + existing.var_type = override_ty; + } + return; + } + + let is_arg = if is_frame_base { offset > 0 } else { false }; + let var_name = if is_arg && offset > 8 { + format!("arg_{:x}h", offset.unsigned_abs()) + } else { + format!("var_{:x}h", offset.unsigned_abs()) + }; + let kind = if is_frame_base { "b" } else { "s" }; + + vars.push(RecoveredVariable { + name: var_name, + kind: kind.to_string(), + delta: offset, + var_type: type_override.unwrap_or_else(|| size_to_type(size)), + isarg: is_arg && offset > 8, + reg: None, + }); + seen_slots.insert(slot_key, vars.len().saturating_sub(1)); +} + +fn collect_register_version_keys(ssa_blocks: &[SSABlock]) -> HashMap> { + let mut reg_versions: HashMap> = HashMap::new(); + for block in ssa_blocks { + for op in &block.ops { + let mut collect_var = |var: &SSAVar| { + if !ssa_var_is_register_like(&var.name) { + return; + } + let reg_name = scalar_register_family_key(&var.name); + reg_versions + .entry(reg_name) + .or_default() + .push(ssa_var_key(var)); + }; + if let Some(dst) = op.dst() { + collect_var(dst); + } + op.for_each_source(&mut collect_var); + } + } + for keys in reg_versions.values_mut() { + keys.sort(); + keys.dedup(); + } + reg_versions +} + +fn set_width_hint_prefer_narrower( + width_hints: &mut HashMap, + key: String, + bits: u32, +) -> bool { + if bits == 0 { + return false; + } + match width_hints.get(&key).copied() { + Some(current) if current == bits => false, + Some(current) if current > 0 && current <= bits => false, + _ => { + width_hints.insert(key, bits); + true + } + } +} + +fn normalize_register_family_width_hints( + register_versions: &HashMap>, + width_hints: &mut HashMap, +) -> bool { + let mut changed = false; + for reg_keys in register_versions.values() { + let max_bits = reg_keys + .iter() + .filter_map(|key| width_hints.get(key).copied()) + .max() + .unwrap_or(0); + let min_bits = reg_keys + .iter() + .filter_map(|key| { + let current = width_hints.get(key).copied().unwrap_or(0); + let natural = register_key_natural_bits(key).unwrap_or(0); + let candidate = match (current, natural) { + (0, 0) => 0, + (0, natural) => natural, + (current, 0) => current, + (current, natural) => current.min(natural), + }; + (candidate > 0).then_some(candidate) + }) + .min() + .unwrap_or(0); + let preferred_bits = if min_bits > 0 && max_bits == 64 && min_bits <= 32 { + min_bits + } else { + max_bits + }; + for key in reg_keys { + changed |= set_width_hint_prefer_narrower(width_hints, key.clone(), preferred_bits); + } + } + changed +} + +fn register_key_natural_bits(key: &str) -> Option { + let reg = key.split('_').next()?; + if let Some(idx) = reg.strip_prefix('w') + && !idx.is_empty() + && idx.chars().all(|ch| ch.is_ascii_digit()) + { + return Some(32); + } + if let Some(idx) = reg.strip_prefix('x') + && !idx.is_empty() + && idx.chars().all(|ch| ch.is_ascii_digit()) + { + return Some(64); + } + match reg { + "wsp" | "wzr" => Some(32), + "sp" | "fp" | "lr" | "xzr" => Some(64), + _ => None, + } +} + +fn propagate_normalized_scalar_result_widths( + ssa_blocks: &[SSABlock], + width_hints: &mut HashMap, +) -> bool { + let mut changed = false; + for block in ssa_blocks { + for op in &block.ops { + let (dst, source_bits) = match op { + SSAOp::Copy { dst, src } + | SSAOp::Cast { dst, src } + | SSAOp::New { dst, src } + | SSAOp::IntZExt { dst, src } + | SSAOp::IntSExt { dst, src } + | SSAOp::Subpiece { dst, src, .. } => { + let bits = width_hints.get(&ssa_var_key(src)).copied().unwrap_or(0); + (dst, bits) + } + SSAOp::IntAdd { dst, a, b } + | SSAOp::IntSub { dst, a, b } + | SSAOp::IntMult { dst, a, b } + | SSAOp::IntDiv { dst, a, b } + | SSAOp::IntSDiv { dst, a, b } + | SSAOp::IntRem { dst, a, b } + | SSAOp::IntSRem { dst, a, b } + | SSAOp::IntAnd { dst, a, b } + | SSAOp::IntOr { dst, a, b } + | SSAOp::IntXor { dst, a, b } + | SSAOp::IntLeft { dst, a, b } + | SSAOp::IntRight { dst, a, b } + | SSAOp::IntSRight { dst, a, b } => { + let bits = [a, b] + .into_iter() + .filter(|var| !ssa_var_is_const(var)) + .filter_map(|var| width_hints.get(&ssa_var_key(var)).copied()) + .filter(|bits| *bits > 0) + .max() + .unwrap_or(0); + (dst, bits) + } + _ => continue, + }; + changed |= set_width_hint_prefer_narrower(width_hints, ssa_var_key(dst), source_bits); + } + } + changed +} + +fn ssa_var_is_stack_base(var: &SSAVar) -> bool { + matches!( + var.name.to_ascii_lowercase().as_str(), + "rbp" | "rsp" | "ebp" | "esp" | "sp" | "fp" | "bp" | "s0" | "x2" | "x8" + ) +} + +fn infer_pointer_width_bytes(ssa_blocks: &[SSABlock]) -> u32 { + let mut width = 0u32; + for block in ssa_blocks { + for op in &block.ops { + if let Some(dst) = op.dst() + && ssa_var_is_stack_base(dst) + { + width = width.max(dst.size); + } + op.for_each_source(|src| { + if ssa_var_is_stack_base(src) { + width = width.max(src.size); + } + }); + } + } + if width == 0 { 8 } else { width } +} + +fn infer_index_like_var_keys(ssa_blocks: &[SSABlock]) -> HashSet { + let mut index_like: HashSet = HashSet::new(); + for block in ssa_blocks { + for op in &block.ops { + if let SSAOp::IntSExt { dst, src } | SSAOp::IntZExt { dst, src } = op + && src.size < dst.size + { + index_like.insert(ssa_var_key(dst)); + } + } + } + + let mut changed = true; + while changed { + changed = false; + for block in ssa_blocks { + for op in &block.ops { + match op { + SSAOp::Copy { dst, src } + | SSAOp::Cast { dst, src } + | SSAOp::New { dst, src } => { + if index_like.contains(&ssa_var_key(src)) { + changed |= index_like.insert(ssa_var_key(dst)); + } + } + SSAOp::IntMult { dst, a, b } => { + let a_key = ssa_var_key(a); + let b_key = ssa_var_key(b); + let a_is_scaled_const = ssa_var_is_const(a); + let b_is_scaled_const = ssa_var_is_const(b); + if (index_like.contains(&a_key) && ssa_var_is_const(b)) + || (index_like.contains(&b_key) && ssa_var_is_const(a)) + || (a_is_scaled_const && !b_is_scaled_const) + || (b_is_scaled_const && !a_is_scaled_const) + { + changed |= index_like.insert(ssa_var_key(dst)); + } + } + SSAOp::IntLeft { dst, a, b } => { + let shift_amount = parse_const_value(&b.name).unwrap_or(u64::MAX); + if (index_like.contains(&ssa_var_key(a)) && ssa_var_is_const(b)) + || shift_amount <= 6 + { + changed |= index_like.insert(ssa_var_key(dst)); + } + } + _ => {} + } + } + } + } + + index_like +} + +fn infer_pointer_var_keys_from_ssa(ssa_blocks: &[SSABlock]) -> HashSet { + let mut pointer_vars: HashSet = HashSet::new(); + let register_versions = collect_register_version_keys(ssa_blocks); + let index_like_vars = infer_index_like_var_keys(ssa_blocks); + let pointer_width = infer_pointer_width_bytes(ssa_blocks); + let mut stack_addr_slots: HashMap = HashMap::new(); + let mut pointer_stack_slots: HashSet = HashSet::new(); + + for block in ssa_blocks { + for op in &block.ops { + match op { + SSAOp::IntAdd { dst, a, b } | SSAOp::IntSub { dst, a, b } => { + let a_is_stack = ssa_var_is_stack_base(a); + let b_is_stack = ssa_var_is_stack_base(b); + let a_const = parse_const_value(&a.name); + let b_const = parse_const_value(&b.name); + + if a_is_stack && b_const.is_some() { + let raw = b_const.unwrap_or(0); + let offset = if matches!(op, SSAOp::IntSub { .. }) { + -(raw as i64) + } else { + raw as i64 + }; + stack_addr_slots.insert( + ssa_var_block_key(block.addr, dst), + format!("{}:{offset}", a.name.to_ascii_lowercase()), + ); + } else if matches!(op, SSAOp::IntAdd { .. }) && b_is_stack && a_const.is_some() + { + let raw = a_const.unwrap_or(0); + stack_addr_slots.insert( + ssa_var_block_key(block.addr, dst), + format!("{}:{}", b.name.to_ascii_lowercase(), raw as i64), + ); + } + } + SSAOp::Load { addr, .. } + | SSAOp::Store { addr, .. } + | SSAOp::LoadLinked { addr, .. } + | SSAOp::StoreConditional { addr, .. } + | SSAOp::LoadGuarded { addr, .. } + | SSAOp::StoreGuarded { addr, .. } + | SSAOp::AtomicCAS { addr, .. } => { + pointer_vars.insert(ssa_var_key(addr)); + } + _ => {} + } + } + } + + let mut changed = true; + while changed { + changed = false; + for block in ssa_blocks { + for op in &block.ops { + match op { + SSAOp::Phi { dst, sources } => { + let dst_key = ssa_var_key(dst); + let dst_is_pointer = pointer_vars.contains(&dst_key); + let any_source_pointer = sources + .iter() + .any(|src| pointer_vars.contains(&ssa_var_key(src))); + + if any_source_pointer { + changed |= pointer_vars.insert(dst_key.clone()); + } + if dst_is_pointer { + for src in sources { + changed |= pointer_vars.insert(ssa_var_key(src)); + } + } + } + SSAOp::Copy { dst, src } + | SSAOp::Cast { dst, src } + | SSAOp::New { dst, src } => { + let dst_key = ssa_var_key(dst); + let src_key = ssa_var_key(src); + if pointer_vars.contains(&dst_key) { + changed |= pointer_vars.insert(src_key.clone()); + } + if pointer_vars.contains(&src_key) { + changed |= pointer_vars.insert(dst_key); + } + } + SSAOp::IntAdd { dst, a, b } | SSAOp::IntSub { dst, a, b } => { + let dst_key = ssa_var_key(dst); + let a_key = ssa_var_key(a); + let b_key = ssa_var_key(b); + let a_is_const = ssa_var_is_const(a); + let b_is_const = ssa_var_is_const(b); + let a_index_like = index_like_vars.contains(&a_key); + let b_index_like = index_like_vars.contains(&b_key); + + if pointer_vars.contains(&dst_key) { + if a_is_const && !b_is_const { + changed |= pointer_vars.insert(b_key.clone()); + } else if b_is_const && !a_is_const { + changed |= pointer_vars.insert(a_key.clone()); + } else if a_index_like && !b_index_like { + changed |= pointer_vars.insert(b_key.clone()); + } else if b_index_like && !a_index_like { + changed |= pointer_vars.insert(a_key.clone()); + } else if a_index_like && b_index_like { + let a_is_tmp = a.name.starts_with("tmp:"); + let b_is_tmp = b.name.starts_with("tmp:"); + if a_is_tmp && !b_is_tmp { + changed |= pointer_vars.insert(b_key.clone()); + } else if b_is_tmp && !a_is_tmp { + changed |= pointer_vars.insert(a_key.clone()); + } + } + } + + if pointer_vars.contains(&a_key) && b_is_const { + changed |= pointer_vars.insert(dst_key.clone()); + } + if pointer_vars.contains(&b_key) && a_is_const { + changed |= pointer_vars.insert(dst_key.clone()); + } + if pointer_vars.contains(&a_key) && index_like_vars.contains(&b_key) { + changed |= pointer_vars.insert(dst_key.clone()); + } + if pointer_vars.contains(&b_key) && index_like_vars.contains(&a_key) { + changed |= pointer_vars.insert(dst_key.clone()); + } + } + SSAOp::PtrAdd { dst, base, .. } | SSAOp::PtrSub { dst, base, .. } => { + let dst_key = ssa_var_key(dst); + let base_key = ssa_var_key(base); + if pointer_vars.contains(&dst_key) { + changed |= pointer_vars.insert(base_key.clone()); + } + if pointer_vars.contains(&base_key) { + changed |= pointer_vars.insert(dst_key); + } + } + SSAOp::SegmentOp { dst, offset, .. } => { + let dst_key = ssa_var_key(dst); + let offset_key = ssa_var_key(offset); + if pointer_vars.contains(&dst_key) { + changed |= pointer_vars.insert(offset_key.clone()); + } + if pointer_vars.contains(&offset_key) { + changed |= pointer_vars.insert(dst_key); + } + } + SSAOp::Store { addr, val, .. } => { + if let Some(slot) = + stack_addr_slots.get(&ssa_var_block_key(block.addr, addr)) + { + let val_key = ssa_var_key(val); + if val.size >= pointer_width && pointer_vars.contains(&val_key) { + changed |= pointer_stack_slots.insert(slot.clone()); + } + if val.size >= pointer_width && pointer_stack_slots.contains(slot) { + changed |= pointer_vars.insert(val_key); + } + } + } + SSAOp::Load { dst, addr, .. } => { + if let Some(slot) = + stack_addr_slots.get(&ssa_var_block_key(block.addr, addr)) + { + let dst_key = ssa_var_key(dst); + if dst.size >= pointer_width && pointer_stack_slots.contains(slot) { + changed |= pointer_vars.insert(dst_key.clone()); + } + if dst.size >= pointer_width && pointer_vars.contains(&dst_key) { + changed |= pointer_stack_slots.insert(slot.clone()); + } + } + } + _ => {} + } + } + } + + for reg_keys in register_versions.values() { + if reg_keys.iter().any(|key| pointer_vars.contains(key)) { + for key in reg_keys { + changed |= pointer_vars.insert(key.clone()); + } + } + } + } + + pointer_vars +} + +fn merge_width_hint(width_hints: &mut HashMap, var: &SSAVar, bits: u32) { + let entry = width_hints.entry(ssa_var_key(var)).or_insert(0); + *entry = (*entry).max(bits.max(var.size.saturating_mul(8))); +} + +fn mark_scalar_var( + vars: &mut HashSet, + width_hints: &mut HashMap, + var: &SSAVar, +) { + vars.insert(ssa_var_key(var)); + merge_width_hint(width_hints, var, var.size.saturating_mul(8)); +} + +fn infer_scalar_var_evidence_from_ssa( + ssa_blocks: &[SSABlock], +) -> ( + HashSet, + HashSet, + HashSet, + HashMap, +) { + let register_versions = collect_register_version_keys(ssa_blocks); + let mut scalar_proven: HashSet = HashSet::new(); + let mut scalar_likely: HashSet = HashSet::new(); + let mut bool_like: HashSet = HashSet::new(); + let mut width_hints: HashMap = HashMap::new(); + + for block in ssa_blocks { + for op in &block.ops { + match op { + SSAOp::IntMult { a, b, .. } + | SSAOp::IntDiv { a, b, .. } + | SSAOp::IntSDiv { a, b, .. } + | SSAOp::IntRem { a, b, .. } + | SSAOp::IntSRem { a, b, .. } + | SSAOp::IntAnd { a, b, .. } + | SSAOp::IntOr { a, b, .. } + | SSAOp::IntXor { a, b, .. } + | SSAOp::IntLeft { a, b, .. } + | SSAOp::IntRight { a, b, .. } + | SSAOp::IntSRight { a, b, .. } + | SSAOp::IntCarry { a, b, .. } + | SSAOp::IntSCarry { a, b, .. } + | SSAOp::IntSBorrow { a, b, .. } => { + mark_scalar_var(&mut scalar_proven, &mut width_hints, a); + mark_scalar_var(&mut scalar_proven, &mut width_hints, b); + } + SSAOp::IntNegate { src, .. } + | SSAOp::IntNot { src, .. } + | SSAOp::PopCount { src, .. } + | SSAOp::Lzcount { src, .. } => { + mark_scalar_var(&mut scalar_proven, &mut width_hints, src); + } + SSAOp::PtrAdd { index, .. } | SSAOp::PtrSub { index, .. } => { + mark_scalar_var(&mut scalar_proven, &mut width_hints, index); + } + SSAOp::IntAdd { a, b, .. } | SSAOp::IntSub { a, b, .. } => { + mark_scalar_var(&mut scalar_likely, &mut width_hints, a); + mark_scalar_var(&mut scalar_likely, &mut width_hints, b); + } + SSAOp::BoolNot { dst, src } => { + mark_scalar_var(&mut scalar_likely, &mut width_hints, src); + bool_like.insert(ssa_var_key(src)); + bool_like.insert(ssa_var_key(dst)); + merge_width_hint(&mut width_hints, dst, 1); + } + SSAOp::CBranch { cond, .. } + | SSAOp::LoadGuarded { guard: cond, .. } + | SSAOp::StoreGuarded { guard: cond, .. } => { + mark_scalar_var(&mut scalar_likely, &mut width_hints, cond); + bool_like.insert(ssa_var_key(cond)); + merge_width_hint(&mut width_hints, cond, 1); + } + SSAOp::FloatNeg { src, .. } + | SSAOp::FloatAbs { src, .. } + | SSAOp::FloatSqrt { src, .. } + | SSAOp::Cast { src, .. } => { + mark_scalar_var(&mut scalar_likely, &mut width_hints, src); + } + SSAOp::IntEqual { dst, a, b } + | SSAOp::IntNotEqual { dst, a, b } + | SSAOp::IntLess { dst, a, b } + | SSAOp::IntSLess { dst, a, b } + | SSAOp::IntLessEqual { dst, a, b } + | SSAOp::IntSLessEqual { dst, a, b } + | SSAOp::FloatEqual { dst, a, b } + | SSAOp::FloatNotEqual { dst, a, b } + | SSAOp::FloatLess { dst, a, b } + | SSAOp::FloatLessEqual { dst, a, b } => { + mark_scalar_var(&mut scalar_likely, &mut width_hints, a); + mark_scalar_var(&mut scalar_likely, &mut width_hints, b); + bool_like.insert(ssa_var_key(dst)); + merge_width_hint(&mut width_hints, dst, 1); + } + SSAOp::BoolAnd { dst, a, b } + | SSAOp::BoolOr { dst, a, b } + | SSAOp::BoolXor { dst, a, b } => { + mark_scalar_var(&mut scalar_likely, &mut width_hints, a); + mark_scalar_var(&mut scalar_likely, &mut width_hints, b); + bool_like.insert(ssa_var_key(a)); + bool_like.insert(ssa_var_key(b)); + bool_like.insert(ssa_var_key(dst)); + merge_width_hint(&mut width_hints, dst, 1); + } + SSAOp::IntZExt { dst, src } | SSAOp::IntSExt { dst, src } => { + mark_scalar_var(&mut scalar_likely, &mut width_hints, src); + mark_scalar_var(&mut scalar_likely, &mut width_hints, dst); + merge_width_hint(&mut width_hints, dst, dst.size.saturating_mul(8)); + } + SSAOp::Subpiece { dst, src, .. } => { + mark_scalar_var(&mut scalar_likely, &mut width_hints, src); + mark_scalar_var(&mut scalar_likely, &mut width_hints, dst); + merge_width_hint(&mut width_hints, dst, dst.size.saturating_mul(8)); + } + _ => {} + } + } + } + + let mut changed = true; + while changed { + changed = false; + for block in ssa_blocks { + for op in &block.ops { + match op { + SSAOp::IntAdd { dst, a, b } + | SSAOp::IntSub { dst, a, b } + | SSAOp::IntMult { dst, a, b } + | SSAOp::IntDiv { dst, a, b } + | SSAOp::IntSDiv { dst, a, b } + | SSAOp::IntRem { dst, a, b } + | SSAOp::IntSRem { dst, a, b } + | SSAOp::IntAnd { dst, a, b } + | SSAOp::IntOr { dst, a, b } + | SSAOp::IntXor { dst, a, b } + | SSAOp::IntLeft { dst, a, b } + | SSAOp::IntRight { dst, a, b } + | SSAOp::IntSRight { dst, a, b } => { + let dst_key = ssa_var_key(dst); + let a_key = ssa_var_key(a); + let b_key = ssa_var_key(b); + let any_proven = + scalar_proven.contains(&a_key) || scalar_proven.contains(&b_key); + let any_likely = scalar_likely.contains(&a_key) + || scalar_likely.contains(&b_key) + || any_proven; + if any_proven { + changed |= scalar_proven.insert(dst_key.clone()); + } + if any_likely { + changed |= scalar_likely.insert(dst_key.clone()); + } + let operand_bits = [a, b] + .into_iter() + .filter(|var| !ssa_var_is_const(var)) + .filter_map(|var| width_hints.get(&ssa_var_key(var)).copied()) + .filter(|bits| *bits > 0) + .max() + .unwrap_or(0); + if operand_bits > 0 { + let entry = width_hints.entry(dst_key).or_insert(0); + if operand_bits > *entry { + *entry = operand_bits; + changed = true; + } + } + } + SSAOp::Phi { dst, sources } => { + let dst_key = ssa_var_key(dst); + let any_proven = sources + .iter() + .any(|src| scalar_proven.contains(&ssa_var_key(src))); + let any_likely = sources + .iter() + .any(|src| scalar_likely.contains(&ssa_var_key(src))); + let any_bool = sources + .iter() + .any(|src| bool_like.contains(&ssa_var_key(src))); + if any_proven { + changed |= scalar_proven.insert(dst_key.clone()); + } + if any_likely || any_proven { + changed |= scalar_likely.insert(dst_key.clone()); + } + if any_bool { + changed |= bool_like.insert(dst_key.clone()); + } + let max_bits = sources + .iter() + .filter_map(|src| width_hints.get(&ssa_var_key(src)).copied()) + .max() + .unwrap_or(0); + if max_bits > 0 { + let entry = width_hints.entry(dst_key).or_insert(0); + if max_bits > *entry { + *entry = max_bits; + changed = true; + } + } + } + SSAOp::Copy { dst, src } + | SSAOp::Cast { dst, src } + | SSAOp::New { dst, src } => { + let dst_key = ssa_var_key(dst); + let src_key = ssa_var_key(src); + if scalar_proven.contains(&src_key) { + changed |= scalar_proven.insert(dst_key.clone()); + } + if scalar_likely.contains(&src_key) || scalar_proven.contains(&src_key) { + changed |= scalar_likely.insert(dst_key.clone()); + } + if bool_like.contains(&src_key) { + changed |= bool_like.insert(dst_key.clone()); + } + let bits = width_hints.get(&src_key).copied().unwrap_or(0); + if bits > 0 { + let entry = width_hints.entry(dst_key).or_insert(0); + if bits > *entry { + *entry = bits; + changed = true; + } + } + } + SSAOp::IntZExt { dst, src } + | SSAOp::IntSExt { dst, src } + | SSAOp::Subpiece { dst, src, .. } => { + let dst_key = ssa_var_key(dst); + let src_key = ssa_var_key(src); + if scalar_likely.contains(&src_key) || scalar_proven.contains(&src_key) { + changed |= scalar_likely.insert(dst_key.clone()); + } + if bool_like.contains(&src_key) { + changed |= bool_like.insert(dst_key.clone()); + } + let entry = width_hints.entry(dst_key).or_insert(0); + let bits = dst.size.saturating_mul(8); + if bits > *entry { + *entry = bits; + changed = true; + } + } + _ => {} + } + } + } + + for reg_keys in register_versions.values() { + let any_proven = reg_keys.iter().any(|key| scalar_proven.contains(key)); + let any_likely = reg_keys.iter().any(|key| scalar_likely.contains(key)); + let any_bool = reg_keys.iter().any(|key| bool_like.contains(key)); + let max_bits = reg_keys + .iter() + .filter_map(|key| width_hints.get(key).copied()) + .max() + .unwrap_or(0); + let min_bits = reg_keys + .iter() + .filter_map(|key| width_hints.get(key).copied()) + .filter(|bits| *bits > 0) + .min() + .unwrap_or(0); + let preferred_bits = if min_bits > 0 && max_bits == 64 && min_bits <= 32 { + min_bits + } else { + max_bits + }; + for key in reg_keys { + if any_proven { + changed |= scalar_proven.insert(key.clone()); + } + if any_likely || any_proven { + changed |= scalar_likely.insert(key.clone()); + } + if any_bool { + changed |= bool_like.insert(key.clone()); + } + if preferred_bits > 0 { + let entry = width_hints.entry(key.clone()).or_insert(0); + if preferred_bits > *entry { + *entry = preferred_bits; + changed = true; + } + } + } + } + } + + (scalar_proven, scalar_likely, bool_like, width_hints) +} + +fn infer_usage_register_type_hints( + ssa_blocks: &[SSABlock], +) -> (HashMap, HashSet) { + let pointer_vars = infer_pointer_var_keys_from_ssa(ssa_blocks); + let mut hints = HashMap::new(); + + for block in ssa_blocks { + for op in &block.ops { + let mut maybe_add = |var: &SSAVar| { + let key = ssa_var_key(var); + if !pointer_vars.contains(&key) || !ssa_var_is_register_like(&var.name) { + return; + } + merge_type_hint( + &mut hints, + var.name.to_ascii_lowercase(), + TypeHint::pointer(), + ); + }; + if let Some(dst) = op.dst() { + maybe_add(dst); + } + op.for_each_source(&mut maybe_add); + } + } + + (hints, pointer_vars) +} diff --git a/crates/r2types/src/signature_infer.rs b/crates/r2types/src/signature_infer.rs new file mode 100644 index 0000000..efd0ab1 --- /dev/null +++ b/crates/r2types/src/signature_infer.rs @@ -0,0 +1,1014 @@ +use std::collections::{BTreeSet, HashMap, HashSet}; + +use r2ssa::{SSABlock, SSAFunction, SSAOp, SSAVar, SsaArtifact}; + +use crate::convert::CTypeLike; +use crate::facts::{FunctionType, FunctionTypeFacts}; +use crate::inference::TypeInference; +use crate::model::Signedness; +use crate::prepare::{SignatureTypeEvidenceContext, scalar_register_family_key}; +use crate::signature::SignatureRegistry; +use crate::writeback::{InferredSignature, InferredSignatureParam}; + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct SignatureTypeEvidence { + pub pointer_proven: u8, + pub pointer_likely: u8, + pub scalar_proven: u8, + pub scalar_likely: u8, + pub bool_like: u8, + pub width_bits: u32, +} + +impl SignatureTypeEvidence { + pub fn pointer_score(&self) -> u16 { + (self.pointer_proven as u16) * 4 + (self.pointer_likely as u16) * 2 + } + + pub fn scalar_score(&self) -> u16 { + (self.scalar_proven as u16) * 4 + + (self.scalar_likely as u16) * 2 + + (self.bool_like as u16) * 3 + } + + pub fn has_pointer_signal(&self) -> bool { + self.pointer_proven > 0 || self.pointer_likely > 0 + } + + pub fn has_scalar_signal(&self) -> bool { + self.scalar_proven > 0 || self.scalar_likely > 0 || self.bool_like > 0 + } + + pub fn has_conflict(&self) -> bool { + self.has_pointer_signal() && self.has_scalar_signal() + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SignatureParamCandidate { + pub name: String, + pub ty: CTypeLike, + pub arg_index: usize, + pub size_bytes: u32, + pub evidence: SignatureTypeEvidence, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RecoveredSignatureParam { + pub name: String, + pub ssa_var: SSAVar, + pub initial_ty: CTypeLike, +} + +pub fn merge_pointer_slot_evidence_into_signature_params( + inferred_params: &mut [SignatureParamCandidate], + pointer_arg_slots: &BTreeSet, +) { + if pointer_arg_slots.is_empty() { + return; + } + + let param_count = inferred_params.len(); + for (fallback_idx, param) in inferred_params.iter_mut().enumerate() { + let explicit_slot = if param.arg_index == usize::MAX { + None + } else { + Some(param.arg_index) + }; + let slot = explicit_slot.unwrap_or(fallback_idx); + let fallback_slot_match = pointer_arg_slots.contains(&fallback_idx) + && (explicit_slot.is_none() || param_count == 1); + if pointer_arg_slots.contains(&slot) || fallback_slot_match { + param.evidence.pointer_proven = param.evidence.pointer_proven.max(1); + } + } +} + +pub fn infer_signature_from_prepared_ssa( + function_name: &str, + arch_name: &str, + ptr_bits: u32, + prepared: &SsaArtifact, + pattern_ssa_blocks: &[SSABlock], + recovered_params: &[RecoveredSignatureParam], + pointer_arg_slots: &BTreeSet, +) -> InferredSignature { + let evidence_ctx = crate::prepare::collect_signature_type_evidence_context(pattern_ssa_blocks); + let mut type_inference = TypeInference::new(ptr_bits); + type_inference.set_prepared_ssa(prepared); + type_inference.infer_function(prepared); + + let mut canonical_params = recovered_params + .iter() + .map(|param| { + let mut evidence = collect_signature_type_evidence_for_var( + &evidence_ctx, + ¶m.ssa_var, + ¶m.initial_ty, + ); + if matches!(param.initial_ty, CTypeLike::Void | CTypeLike::Unknown) { + merge_initial_signature_type_evidence( + &type_inference.type_from_size(param.ssa_var.size), + &mut evidence, + ); + } + let ty = resolve_evidence_driven_signature_type( + param.initial_ty.clone(), + param.ssa_var.size, + ptr_bits, + &evidence, + ); + let arg_index = param + .name + .strip_prefix("arg") + .and_then(|n| n.parse::().ok()) + .unwrap_or(usize::MAX); + SignatureParamCandidate { + name: param.name.clone(), + ty, + arg_index, + size_bytes: param.ssa_var.size, + evidence, + } + }) + .collect::>(); + + merge_pointer_slot_evidence_into_signature_params(&mut canonical_params, pointer_arg_slots); + + for param in &mut canonical_params { + param.ty = resolve_evidence_driven_signature_type( + param.ty.clone(), + param.size_bytes, + ptr_bits, + ¶m.evidence, + ); + } + + let (ret_type, ret_evidence) = + infer_signature_return_type(prepared, &type_inference, ptr_bits, &evidence_ctx); + let input_counts = collect_version0_input_regs(prepared); + build_inferred_signature( + function_name, + arch_name, + ptr_bits, + &canonical_params, + &ret_type, + &ret_evidence, + &input_counts, + ) +} + +pub fn merge_initial_signature_type_evidence( + initial_ty: &CTypeLike, + evidence: &mut SignatureTypeEvidence, +) { + match initial_ty { + CTypeLike::Pointer(_) => evidence.pointer_likely = evidence.pointer_likely.max(1), + CTypeLike::Bool => evidence.bool_like = evidence.bool_like.max(1), + CTypeLike::Int { bits, .. } => { + evidence.scalar_likely = evidence.scalar_likely.max(1); + if !(evidence.has_scalar_signal() + && !evidence.has_pointer_signal() + && evidence.width_bits > 0 + && evidence.width_bits < *bits) + { + evidence.width_bits = evidence.width_bits.max(*bits); + } + } + CTypeLike::Float(bits) => { + evidence.scalar_proven = evidence.scalar_proven.max(1); + evidence.width_bits = evidence.width_bits.max(*bits); + } + _ => {} + } +} + +fn fallback_scalar_type_like( + var_size_bytes: u32, + evidence: &SignatureTypeEvidence, + ptr_bits: u32, +) -> CTypeLike { + if evidence.bool_like > 0 + && evidence.pointer_score() == 0 + && evidence.scalar_proven == 0 + && evidence.scalar_likely <= 1 + { + return CTypeLike::Bool; + } + + let carrier_bits = var_size_bytes.saturating_mul(8); + let width_bits = if evidence.has_scalar_signal() + && !evidence.has_pointer_signal() + && evidence.width_bits > 0 + { + evidence.width_bits + } else { + evidence.width_bits.max(carrier_bits) + }; + let width_bits = match width_bits { + 0 => { + if ptr_bits >= 64 { + 64 + } else { + 32 + } + } + 1 => 8, + 2..=8 => 8, + 9..=16 => 16, + 17..=32 => 32, + _ => 64, + }; + + CTypeLike::Int { + bits: width_bits, + signedness: Signedness::Signed, + } +} + +fn is_unmaterialized_aggregate_name(name: &str) -> bool { + let lower = name.trim().to_ascii_lowercase(); + lower.is_empty() || lower == "anon" || lower.starts_with("anon_") +} + +pub fn materialize_signature_type_like(ty: CTypeLike, ptr_bits: u32) -> CTypeLike { + match ty { + CTypeLike::Pointer(inner) => { + if matches!(*inner, CTypeLike::Unknown | CTypeLike::Void) + || matches!( + inner.as_ref(), + CTypeLike::Struct(name) + | CTypeLike::Union(name) + | CTypeLike::Enum(name) + if is_unmaterialized_aggregate_name(name) + ) + { + return CTypeLike::Pointer(Box::new(CTypeLike::Void)); + } + CTypeLike::Pointer(Box::new(materialize_signature_type_like(*inner, ptr_bits))) + } + CTypeLike::Array(inner, len) => { + if matches!(*inner, CTypeLike::Unknown | CTypeLike::Void) { + return CTypeLike::Array( + Box::new(CTypeLike::Int { + bits: 8, + signedness: Signedness::Unsigned, + }), + len, + ); + } + CTypeLike::Array( + Box::new(materialize_signature_type_like(*inner, ptr_bits)), + len, + ) + } + CTypeLike::Unknown => { + fallback_scalar_type_like((ptr_bits / 8).max(1), &Default::default(), ptr_bits) + } + CTypeLike::Struct(name) if is_unmaterialized_aggregate_name(&name) => { + fallback_scalar_type_like((ptr_bits / 8).max(1), &Default::default(), ptr_bits) + } + CTypeLike::Union(name) if is_unmaterialized_aggregate_name(&name) => { + fallback_scalar_type_like((ptr_bits / 8).max(1), &Default::default(), ptr_bits) + } + CTypeLike::Enum(name) if is_unmaterialized_aggregate_name(&name) => { + fallback_scalar_type_like((ptr_bits / 8).max(1), &Default::default(), ptr_bits) + } + other => other, + } +} + +pub fn render_signature_type(ty: &CTypeLike, ptr_bits: u32) -> String { + match materialize_signature_type_like(ty.clone(), ptr_bits) { + CTypeLike::Void => "void".to_string(), + CTypeLike::Bool => "bool".to_string(), + CTypeLike::Int { + bits: 8, + signedness: Signedness::Signed, + } => "int8_t".to_string(), + CTypeLike::Int { + bits: 16, + signedness: Signedness::Signed, + } => "int16_t".to_string(), + CTypeLike::Int { + bits: 32, + signedness: Signedness::Signed, + } => "int32_t".to_string(), + CTypeLike::Int { + bits: 64, + signedness: Signedness::Signed, + } => "int64_t".to_string(), + CTypeLike::Int { + bits, + signedness: Signedness::Signed | Signedness::Unknown, + } => format!("int{bits}_t"), + CTypeLike::Int { + bits: 8, + signedness: Signedness::Unsigned, + } => "uint8_t".to_string(), + CTypeLike::Int { + bits: 16, + signedness: Signedness::Unsigned, + } => "uint16_t".to_string(), + CTypeLike::Int { + bits: 32, + signedness: Signedness::Unsigned, + } => "uint32_t".to_string(), + CTypeLike::Int { + bits: 64, + signedness: Signedness::Unsigned, + } => "uint64_t".to_string(), + CTypeLike::Int { + bits, + signedness: Signedness::Unsigned, + } => format!("uint{bits}_t"), + CTypeLike::Float(32) => "float".to_string(), + CTypeLike::Float(64) => "double".to_string(), + CTypeLike::Float(bits) => format!("float{bits}"), + CTypeLike::Pointer(inner) => format!("{}*", render_signature_type(&inner, ptr_bits)), + CTypeLike::Array(inner, Some(size)) => { + format!("{}[{}]", render_signature_type(&inner, ptr_bits), size) + } + CTypeLike::Array(inner, None) => format!("{}[]", render_signature_type(&inner, ptr_bits)), + CTypeLike::Struct(name) => format!("struct {name}"), + CTypeLike::Union(name) => format!("union {name}"), + CTypeLike::Enum(name) => format!("enum {name}"), + CTypeLike::Function => "void (*)()".to_string(), + CTypeLike::Unknown => "int64_t".to_string(), + } +} + +fn sanitize_signature_type_like( + mut ty: CTypeLike, + var_size_bytes: u32, + ptr_bits: u32, +) -> CTypeLike { + if matches!(ty, CTypeLike::Void | CTypeLike::Unknown) { + ty = match var_size_bytes { + 1 => CTypeLike::Int { + bits: 8, + signedness: Signedness::Signed, + }, + 2 => CTypeLike::Int { + bits: 16, + signedness: Signedness::Signed, + }, + 4 => CTypeLike::Int { + bits: 32, + signedness: Signedness::Signed, + }, + 8 => CTypeLike::Int { + bits: 64, + signedness: Signedness::Signed, + }, + _ => CTypeLike::Unknown, + }; + } + + if matches!(ty, CTypeLike::Void | CTypeLike::Unknown) { + ty = CTypeLike::Int { + bits: if ptr_bits >= 64 { 64 } else { 32 }, + signedness: Signedness::Signed, + }; + } + + ty +} + +pub fn resolve_evidence_driven_signature_type( + initial_ty: CTypeLike, + var_size_bytes: u32, + ptr_bits: u32, + evidence: &SignatureTypeEvidence, +) -> CTypeLike { + if matches!(initial_ty, CTypeLike::Float(_)) { + return initial_ty; + } + + let pointer_score = evidence.pointer_score(); + let scalar_score = evidence.scalar_score(); + let initial_is_pointer = matches!(initial_ty, CTypeLike::Pointer(_)); + let initial_is_scalar = matches!(initial_ty, CTypeLike::Bool | CTypeLike::Int { .. }); + let preferred_scalar = fallback_scalar_type_like(var_size_bytes, evidence, ptr_bits); + let scalar_width_narrows = match (&initial_ty, &preferred_scalar) { + ( + CTypeLike::Int { + bits: initial_bits, .. + }, + CTypeLike::Int { + bits: preferred_bits, + .. + }, + ) => preferred_bits < initial_bits, + (CTypeLike::Int { .. }, CTypeLike::Bool) => true, + _ => false, + }; + + if initial_is_pointer && pointer_score.saturating_add(1) >= scalar_score { + return initial_ty; + } + if initial_is_scalar && scalar_score.saturating_add(1) >= pointer_score { + if scalar_width_narrows + && evidence.has_scalar_signal() + && !evidence.has_pointer_signal() + && !evidence.has_conflict() + { + return preferred_scalar; + } + return initial_ty; + } + + match initial_ty { + CTypeLike::Struct(_) | CTypeLike::Union(_) | CTypeLike::Enum(_) => { + if pointer_score > scalar_score.saturating_add(1) { + return CTypeLike::Pointer(Box::new(CTypeLike::Void)); + } + if scalar_score > pointer_score.saturating_add(2) { + return fallback_scalar_type_like(var_size_bytes, evidence, ptr_bits); + } + return initial_ty; + } + _ => {} + } + + if pointer_score > scalar_score.saturating_add(1) { + return CTypeLike::Pointer(Box::new(CTypeLike::Void)); + } + if scalar_score > pointer_score || matches!(initial_ty, CTypeLike::Void | CTypeLike::Unknown) { + return preferred_scalar; + } + + sanitize_signature_type_like(initial_ty, var_size_bytes, ptr_bits) +} + +pub fn collect_signature_type_evidence_for_var( + evidence_ctx: &SignatureTypeEvidenceContext, + var: &SSAVar, + initial_ty: &CTypeLike, +) -> SignatureTypeEvidence { + let key = format!("{}_{}", var.name.to_ascii_lowercase(), var.version); + let family = scalar_register_family_key(&var.name); + let mut evidence = SignatureTypeEvidence::default(); + if evidence_ctx.pointer_vars.contains(&key) { + evidence.pointer_proven = 1; + } + if evidence_ctx.scalar_proven_vars.contains(&key) { + evidence.scalar_proven = 1; + } + if evidence_ctx.scalar_likely_vars.contains(&key) { + evidence.scalar_likely = 1; + } + if evidence_ctx.bool_like_vars.contains(&key) { + evidence.bool_like = 1; + } + if let Some(bits) = evidence_ctx.width_bits.get(&key) { + evidence.width_bits = *bits; + } + if evidence.pointer_proven == 0 + && signal_present_for_register_family(&evidence_ctx.pointer_vars, &family, var.version) + { + evidence.pointer_proven = 1; + } + if evidence.scalar_proven == 0 + && signal_present_for_register_family( + &evidence_ctx.scalar_proven_vars, + &family, + var.version, + ) + { + evidence.scalar_proven = 1; + } + if evidence.scalar_likely == 0 + && signal_present_for_register_family( + &evidence_ctx.scalar_likely_vars, + &family, + var.version, + ) + { + evidence.scalar_likely = 1; + } + if evidence.bool_like == 0 + && signal_present_for_register_family(&evidence_ctx.bool_like_vars, &family, var.version) + { + evidence.bool_like = 1; + } + if evidence.width_bits == 0 + && let Some(bits) = + width_hint_for_register_family(&evidence_ctx.width_bits, &family, var.version) + { + evidence.width_bits = bits; + } + merge_initial_signature_type_evidence(initial_ty, &mut evidence); + evidence +} + +fn signal_present_for_register_family(keys: &HashSet, family: &str, version: u32) -> bool { + keys.iter() + .any(|key| key_matches_register_family_version(key, family, version)) +} + +fn width_hint_for_register_family( + hints: &HashMap, + family: &str, + version: u32, +) -> Option { + hints + .iter() + .filter(|(key, _)| key_matches_register_family_version(key, family, version)) + .map(|(_, bits)| *bits) + .filter(|bits| *bits > 0) + .min() +} + +fn key_matches_register_family_version(key: &str, family: &str, version: u32) -> bool { + let Some((name, version_str)) = key.rsplit_once('_') else { + return false; + }; + version_str.parse::().ok() == Some(version) && scalar_register_family_key(name) == family +} + +pub fn infer_signature_return_type( + func: &SSAFunction, + type_inference: &TypeInference, + ptr_bits: u32, + evidence_ctx: &SignatureTypeEvidenceContext, +) -> (CTypeLike, SignatureTypeEvidence) { + let mut candidates = Vec::new(); + let mut candidate_evidence = Vec::new(); + + for block in func.blocks() { + for op in &block.ops { + let SSAOp::Return { target } = op else { + continue; + }; + + let target_name = target.name.to_ascii_lowercase(); + if target_name.starts_with("xmm0") || target_name.starts_with("st0") { + let bits = if target.size.saturating_mul(8) <= 32 { + 32 + } else { + 64 + }; + let ty = CTypeLike::Float(bits); + let mut evidence = SignatureTypeEvidence::default(); + merge_initial_signature_type_evidence(&ty, &mut evidence); + evidence.width_bits = bits; + candidates.push(ty); + candidate_evidence.push(evidence); + continue; + } + + let initial_ty = type_inference.get_type(target); + let evidence = + collect_signature_type_evidence_for_var(evidence_ctx, target, &initial_ty); + let ty = resolve_evidence_driven_signature_type( + initial_ty, + target.size, + ptr_bits, + &evidence, + ); + candidates.push(ty); + candidate_evidence.push(evidence); + } + } + + if candidates.is_empty() { + return (CTypeLike::Void, SignatureTypeEvidence::default()); + } + + let mut meaningful: Vec = candidates + .iter() + .filter(|ty| !matches!(ty, CTypeLike::Unknown)) + .cloned() + .collect(); + if meaningful.is_empty() { + let fallback_evidence = candidate_evidence.into_iter().next().unwrap_or_default(); + return ( + fallback_scalar_type_like((ptr_bits / 8).max(1), &fallback_evidence, ptr_bits), + fallback_evidence, + ); + } + if meaningful.iter().all(|ty| ty == &meaningful[0]) { + return ( + meaningful.remove(0), + candidate_evidence.into_iter().next().unwrap_or_default(), + ); + } + if let Some(float_ty) = meaningful + .iter() + .find(|ty| matches!(ty, CTypeLike::Float(_))) + .cloned() + { + let evidence = candidate_evidence + .into_iter() + .find(|e| e.width_bits >= 32) + .unwrap_or_default(); + return (float_ty, evidence); + } + let evidence = candidate_evidence.into_iter().next().unwrap_or_default(); + (meaningful.remove(0), evidence) +} + +fn canonical_x86_64_arg_reg(name: &str) -> Option<&'static str> { + match name.to_ascii_lowercase().as_str() { + "rdi" | "edi" | "di" | "dil" => Some("rdi"), + "rsi" | "esi" | "si" | "sil" => Some("rsi"), + "rdx" | "edx" | "dx" | "dl" | "dh" => Some("rdx"), + "rcx" | "ecx" | "cx" | "cl" | "ch" => Some("rcx"), + "r8" | "r8d" | "r8w" | "r8b" => Some("r8"), + "r9" | "r9d" | "r9w" | "r9b" => Some("r9"), + _ => None, + } +} + +pub fn collect_version0_input_regs(func: &SSAFunction) -> HashMap { + let mut counts = HashMap::new(); + for block in func.blocks() { + for op in &block.ops { + for src in op.sources() { + if src.version != 0 { + continue; + } + if src.name.starts_with("tmp:") || src.name.starts_with("const:") { + continue; + } + let key = src.name.to_ascii_lowercase(); + *counts.entry(key).or_insert(0) += 1; + } + } + } + counts +} + +fn infer_callconv_x86_64_from_counts(counts: &HashMap) -> (&'static str, u8) { + let mut canonical = std::collections::BTreeMap::new(); + for (reg, count) in counts { + if let Some(name) = canonical_x86_64_arg_reg(reg) { + *canonical.entry(name).or_insert(0u32) += *count; + } + } + + let rdi = *canonical.get("rdi").unwrap_or(&0); + let rsi = *canonical.get("rsi").unwrap_or(&0); + let rcx = *canonical.get("rcx").unwrap_or(&0); + let rdx = *canonical.get("rdx").unwrap_or(&0); + let r8 = *canonical.get("r8").unwrap_or(&0); + let r9 = *canonical.get("r9").unwrap_or(&0); + + let sysv_primary = rdi + rsi; + let sysv_total = rdi + rsi + rdx + rcx + r8 + r9; + let ms_total = rcx + rdx + r8 + r9; + let ms_regs_used = [rcx, rdx, r8, r9].iter().filter(|&&v| v > 0).count(); + let ms_dominant = sysv_primary == 0 + && rcx > 0 + && ms_regs_used >= 2 + && ms_total >= 3 + && ms_total >= (rdi + rsi + rdx + 1); + + if ms_dominant { + let confidence = if ms_total >= 3 { 90 } else { 76 }; + ("ms", confidence) + } else { + let confidence = if sysv_primary > 0 { + 92 + } else if sysv_total > 0 { + 76 + } else { + 60 + }; + ("amd64", confidence) + } +} + +pub fn compute_callconv_inference( + arch_name: &str, + input_counts: &HashMap, +) -> (String, u8) { + match arch_name { + "x86-64" => { + let (callconv, confidence) = infer_callconv_x86_64_from_counts(input_counts); + (callconv.to_string(), confidence) + } + "x86" => ("cdecl".to_string(), 64), + _ => (String::new(), 0), + } +} + +fn is_informative_type(ty: &CTypeLike) -> bool { + !matches!(ty, CTypeLike::Void | CTypeLike::Unknown) +} + +pub fn compute_signature_confidence( + params: &[SignatureParamCandidate], + ret_type: &CTypeLike, + ret_evidence: &SignatureTypeEvidence, +) -> u8 { + let mut confidence: i32 = 48; + if !params.is_empty() { + confidence += 8; + } + + for param in params { + let evidence = ¶m.evidence; + if evidence.pointer_proven > 0 || evidence.scalar_proven > 0 { + confidence += 6; + } else if evidence.bool_like > 0 + || evidence.pointer_likely > 0 + || evidence.scalar_likely > 0 + { + confidence += 3; + } else if is_informative_type(¶m.ty) { + confidence += 2; + } else { + confidence -= 2; + } + + if evidence.has_conflict() { + confidence -= 4; + } + } + + if is_informative_type(ret_type) { + confidence += 4; + if ret_evidence.pointer_proven > 0 + || ret_evidence.scalar_proven > 0 + || ret_evidence.bool_like > 0 + { + confidence += 2; + } + } else if ret_evidence.has_pointer_signal() || ret_evidence.has_scalar_signal() { + confidence += 2; + } + + if ret_evidence.has_conflict() { + confidence -= 3; + } + + confidence.clamp(0, 100) as u8 +} + +fn sanitize_c_identifier(name: &str) -> Option { + let mut out = String::with_capacity(name.len()); + for (idx, ch) in name.chars().enumerate() { + let mapped = if ch.is_ascii_alphanumeric() || ch == '_' { + ch + } else { + '_' + }; + if idx == 0 && mapped.is_ascii_digit() { + out.push('_'); + } + out.push(mapped); + } + let trimmed = out.trim_matches('_').to_string(); + if trimmed.is_empty() { + None + } else { + Some(trimmed) + } +} + +fn uniquify_name(base: String, used: &mut HashSet) -> String { + if used.insert(base.clone()) { + return base; + } + let mut index = 1usize; + loop { + let candidate = format!("{base}_{index}"); + if used.insert(candidate.clone()) { + return candidate; + } + index += 1; + } +} + +fn normalize_inferred_param_name( + raw_name: &str, + fallback_idx: usize, + used: &mut HashSet, +) -> String { + let fallback = format!("arg{}", fallback_idx); + let clean = sanitize_c_identifier(raw_name).unwrap_or_else(|| fallback.clone()); + let clean = if clean.is_empty() { fallback } else { clean }; + uniquify_name(clean, used) +} + +pub fn format_afs_signature( + function_name: &str, + ret_type: &str, + params: &[InferredSignatureParam], +) -> String { + let params_str = if params.is_empty() { + "void".to_string() + } else { + params + .iter() + .map(|p| format!("{} {}", p.param_type, p.name)) + .collect::>() + .join(", ") + }; + format!("{ret_type} {function_name} ({params_str})") +} + +pub fn build_inferred_signature( + function_name: &str, + arch_name: &str, + ptr_bits: u32, + params: &[SignatureParamCandidate], + ret_type: &CTypeLike, + ret_evidence: &SignatureTypeEvidence, + input_counts: &HashMap, +) -> InferredSignature { + let mut ordered = params.to_vec(); + ordered.sort_by(|a, b| { + a.arg_index + .cmp(&b.arg_index) + .then_with(|| a.name.cmp(&b.name)) + }); + let mut used_param_names = HashSet::new(); + let json_params = ordered + .iter() + .enumerate() + .map(|(idx, p)| { + let fallback_idx = if p.arg_index == usize::MAX { + idx + } else { + p.arg_index + }; + InferredSignatureParam { + name: normalize_inferred_param_name(&p.name, fallback_idx, &mut used_param_names), + param_type: render_signature_type(&p.ty, ptr_bits), + } + }) + .collect::>(); + let rendered_ret = render_signature_type(ret_type, ptr_bits); + let (callconv, callconv_confidence) = compute_callconv_inference(arch_name, input_counts); + let confidence = compute_signature_confidence(&ordered, ret_type, ret_evidence); + InferredSignature { + function_name: function_name.to_string(), + signature: format_afs_signature(function_name, &rendered_ret, &json_params), + ret_type: rendered_ret, + params: json_params, + callconv, + arch: arch_name.to_string(), + confidence, + callconv_confidence, + } +} + +fn insert_known_signature_aliases( + known: &mut HashMap, + name: &str, + sig: &FunctionType, +) { + if name.is_empty() { + return; + } + known.insert(name.to_string(), sig.clone()); + + for prefix in ["sym.imp.", "sym.", "imp.", "dbg.", "fcn."] { + if let Some(stripped) = name.strip_prefix(prefix) + && !stripped.is_empty() + { + known.insert(stripped.to_string(), sig.clone()); + } + } +} + +fn normalize_signature_registry_name(name: &str) -> Option<&'static str> { + let normalized_owned = name.trim().to_ascii_lowercase(); + let mut normalized = normalized_owned.as_str(); + + for prefix in ["sym.imp.", "sym.", "imp.", "reloc.", "dbg."] { + while let Some(rest) = normalized.strip_prefix(prefix) { + normalized = rest; + } + } + + while let Some(rest) = normalized.strip_suffix("@plt") { + normalized = rest; + } + while let Some(rest) = normalized.strip_suffix(".plt") { + normalized = rest; + } + if let Some((base, _)) = normalized.split_once('@') { + normalized = base; + } + + if let Some(rest) = normalized.strip_prefix("__isoc99_") { + normalized = rest; + } + if let Some(rest) = normalized.strip_prefix("__gi_") { + normalized = rest; + } + + match normalized { + "strlen" | "__strlen_chk" => Some("strlen"), + "strcmp" => Some("strcmp"), + "memcmp" => Some("memcmp"), + "memcpy" | "__memcpy_chk" => Some("memcpy"), + "memset" => Some("memset"), + "malloc" | "__libc_malloc" | "__gi___libc_malloc" => Some("malloc"), + "free" => Some("free"), + "puts" => Some("puts"), + "printf" | "__printf_chk" => Some("printf"), + "exit" | "_exit" => Some("exit"), + _ => { + if normalized.starts_with("strlen") { + Some("strlen") + } else if normalized.starts_with("strcmp") { + Some("strcmp") + } else if normalized.starts_with("memcmp") { + Some("memcmp") + } else if normalized.starts_with("memcpy") { + Some("memcpy") + } else if normalized.starts_with("memset") { + Some("memset") + } else if normalized.starts_with("printf") || normalized == "__printf_chk" { + Some("printf") + } else if normalized.starts_with("puts") { + Some("puts") + } else if normalized == "malloc" || normalized.ends_with("malloc") { + Some("malloc") + } else if normalized == "free" || normalized.ends_with("free") { + Some("free") + } else if normalized.starts_with("exit") { + Some("exit") + } else { + None + } + } + } +} + +pub fn enrich_known_function_signatures_from_names( + type_facts: &mut FunctionTypeFacts, + function_names: &HashMap, + ptr_bits: u32, +) { + let registry = SignatureRegistry::from_embedded_json(); + + for name in function_names.values() { + let mut candidates = vec![name.clone()]; + if let Some(sim_name) = normalize_signature_registry_name(name) + && sim_name != name + && !candidates.iter().any(|candidate| candidate == sim_name) + { + candidates.push(sim_name.to_string()); + } + + let resolved = candidates.into_iter().find_map(|candidate| { + let mut arena = crate::TypeArena::default(); + registry + .resolve(&candidate, &mut arena, ptr_bits) + .map(|sig| { + ( + candidate, + FunctionType { + return_type: crate::to_c_type_like(&arena, sig.ret), + params: sig + .params + .into_iter() + .map(|param| crate::to_c_type_like(&arena, param)) + .collect(), + variadic: sig.variadic, + }, + ) + }) + }); + + let Some((resolved_name, sig)) = resolved else { + continue; + }; + + insert_known_signature_aliases(&mut type_facts.known_function_signatures, name, &sig); + if resolved_name != *name { + insert_known_signature_aliases( + &mut type_facts.known_function_signatures, + &resolved_name, + &sig, + ); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn enrich_known_function_signatures_moves_registry_alias_policy_upstream() { + let mut facts = FunctionTypeFacts::default(); + let names = HashMap::from([(0u64, "sym.imp.__printf_chk".to_string())]); + + enrich_known_function_signatures_from_names(&mut facts, &names, 64); + + assert!( + facts + .known_function_signatures + .contains_key("sym.imp.__printf_chk") + ); + assert!(facts.known_function_signatures.contains_key("__printf_chk")); + assert!(facts.known_function_signatures.contains_key("printf")); + } +} diff --git a/crates/r2types/src/writeback.rs b/crates/r2types/src/writeback.rs index b916830..b74d462 100644 --- a/crates/r2types/src/writeback.rs +++ b/crates/r2types/src/writeback.rs @@ -1,4 +1,5 @@ use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +use std::hash::{Hash, Hasher}; use r2ssa::{ FunctionSemanticSummary, InterprocSummarySet, SSABlock, SSAOp, SSAVar, SummaryArgEffect, @@ -18,10 +19,11 @@ use crate::facts::{ CalleeArgEffect, CalleeFact, CalleeMemoryEffect, CalleeMemoryEffectKind, CalleeMemoryLocation, CalleeMemoryRange, CalleeMemoryRegion, CalleeReturnRelation, FunctionParamSpec, FunctionSignatureSpec, FunctionTypeFactInputs, FunctionTypeFacts, InterprocFactDiagnostics, - SymbolicMemoryCondition, SymbolicMemoryRegion, SymbolicSemanticFacts, VisibleBinding, - VisibleBindingKind, parse_type_like_spec, + LocalFieldAccessFact, SymbolicMemoryCondition, SymbolicMemoryRegion, SymbolicSemanticFacts, + VisibleBinding, VisibleBindingKind, parse_type_like_spec, }; use crate::model::Signedness; +use crate::prepare::recover_vars_arch_profile; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum WritebackSource { @@ -104,13 +106,15 @@ pub struct InferredSignature { pub callconv_confidence: u8, } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct RecoveredVariable { pub name: String, pub kind: String, pub delta: i64, + #[serde(rename = "type")] pub var_type: String, pub isarg: bool, + #[serde(skip_serializing_if = "Option::is_none")] pub reg: Option, } @@ -205,6 +209,11 @@ pub struct TypeWritebackAnalysisInput<'a> { pub diagnostics: TypeWritebackDiagnostics, } +pub struct TypeWritebackSemanticInputs<'a> { + pub symbolic_facts: &'a SymbolicSemanticFacts, + pub local_field_accesses: &'a [LocalFieldAccessFact], +} + #[derive(Debug, Clone, Default)] struct SignatureContextMaps { param_types: HashMap, @@ -492,8 +501,9 @@ fn apply_interproc_summary_to_signature( } } -pub fn build_type_writeback_analysis( +fn build_type_writeback_analysis_inner( mut input: TypeWritebackAnalysisInput<'_>, + semantic_inputs: Option>, ) -> TypeWritebackAnalysis { if input.parsed_context.stack_slots.is_empty() && !input.parsed_context.external_stack_vars.is_empty() @@ -535,6 +545,18 @@ pub fn build_type_writeback_analysis( input.ptr_bits, ); let mut local_structs = input.local_structs; + if let Some(semantic) = semantic_inputs.as_ref() { + augment_local_struct_artifacts_with_local_field_accesses( + &mut local_structs, + semantic.local_field_accesses, + input.ptr_bits, + ); + augment_local_struct_artifacts_with_symbolic_facts( + &mut local_structs, + semantic.symbolic_facts, + input.ptr_bits, + ); + } align_local_structs_with_external( &mut local_structs.struct_decls, &mut local_structs.slot_type_overrides, @@ -602,7 +624,7 @@ pub fn build_type_writeback_analysis( &var_rename_candidates, input.ptr_bits, ); - let type_facts = FunctionTypeFacts::builder(FunctionTypeFactInputs { + let mut type_facts = FunctionTypeFacts::builder(FunctionTypeFactInputs { merged_signature: merged_signature.clone(), known_function_signatures: input.parsed_context.known_function_signatures.clone(), register_params: input.parsed_context.register_params.clone(), @@ -628,6 +650,14 @@ pub fn build_type_writeback_analysis( external_type_db: type_db, slot_type_overrides: local_structs.slot_type_overrides.clone(), slot_field_profiles: local_structs.slot_field_profiles.clone(), + symbolic_facts: semantic_inputs + .as_ref() + .map(|semantic| semantic.symbolic_facts.clone()) + .unwrap_or_default(), + local_field_accesses: semantic_inputs + .as_ref() + .map(|semantic| semantic.local_field_accesses.to_vec()) + .unwrap_or_default(), interproc_diagnostics: input .interproc_summary_set .as_ref() @@ -641,9 +671,16 @@ pub fn build_type_writeback_analysis( }) .unwrap_or_default(), diagnostics: diagnostics.solver_warnings.clone(), - ..FunctionTypeFactInputs::default() }) .build(); + if let Some(semantic) = semantic_inputs.as_ref() + && semantic.symbolic_facts.diagnostics.branches_pruned > 0 + { + type_facts.diagnostics.push(format!( + "symbolic pruned {} branch arm(s)", + semantic.symbolic_facts.diagnostics.branches_pruned + )); + } let global_type_links = score_global_type_links( input.ssa_blocks, &struct_decls, @@ -667,27 +704,715 @@ pub fn build_type_writeback_analysis( } } +pub fn build_type_writeback_analysis( + input: TypeWritebackAnalysisInput<'_>, +) -> TypeWritebackAnalysis { + build_type_writeback_analysis_inner(input, None) +} + +pub fn build_type_writeback_analysis_with_semantics( + input: TypeWritebackAnalysisInput<'_>, + semantic_inputs: TypeWritebackSemanticInputs<'_>, +) -> TypeWritebackAnalysis { + build_type_writeback_analysis_inner(input, Some(semantic_inputs)) +} + +fn symbolic_semantic_mode_label(mode: crate::facts::SymbolicSemanticMode) -> &'static str { + match mode { + crate::facts::SymbolicSemanticMode::Raw => "raw", + crate::facts::SymbolicSemanticMode::Compiled => "compiled", + crate::facts::SymbolicSemanticMode::IslandCompiled => "island_compiled", + crate::facts::SymbolicSemanticMode::Residual => "residual", + crate::facts::SymbolicSemanticMode::VmSummary => "vm_summary", + } +} + +fn symbolic_slice_class_label( + slice_class: crate::facts::SymbolicSemanticSliceClass, +) -> &'static str { + match slice_class { + crate::facts::SymbolicSemanticSliceClass::Wrapper => "wrapper", + crate::facts::SymbolicSemanticSliceClass::Worker => "worker", + crate::facts::SymbolicSemanticSliceClass::RecursiveGroup => "recursive_group", + crate::facts::SymbolicSemanticSliceClass::InterpreterSwitch => "interpreter_switch", + crate::facts::SymbolicSemanticSliceClass::InterpreterIndirect => "interpreter_indirect", + crate::facts::SymbolicSemanticSliceClass::GenericLarge => "generic_large", + } +} + +fn symbolic_residual_reason_label( + reason: crate::facts::SymbolicSemanticResidualReason, +) -> &'static str { + match reason { + crate::facts::SymbolicSemanticResidualReason::MissingArch => "missing_arch", + crate::facts::SymbolicSemanticResidualReason::LargeCfg => "large_cfg", + crate::facts::SymbolicSemanticResidualReason::SummaryBudgetExhausted => { + "summary_budget_exhausted" + } + crate::facts::SymbolicSemanticResidualReason::SccBudgetExhausted => "scc_budget_exhausted", + crate::facts::SymbolicSemanticResidualReason::InterpreterRequiresStepSummary => { + "interpreter_requires_step_summary" + } + } +} + +fn semantic_fallback_warning(symbolic_facts: &SymbolicSemanticFacts) -> String { + let slice_class = symbolic_facts + .slice_class() + .map(symbolic_slice_class_label) + .unwrap_or("unknown"); + let mode = symbolic_facts + .semantic_mode() + .map(symbolic_semantic_mode_label) + .unwrap_or("unknown"); + let mut warning = format!("semantic fallback: {slice_class} slice in {mode} mode"); + if !symbolic_facts.diagnostics.residual_reasons.is_empty() { + warning.push_str(" ("); + warning.push_str( + &symbolic_facts + .diagnostics + .residual_reasons + .iter() + .map(|reason| symbolic_residual_reason_label(*reason)) + .collect::>() + .join(", "), + ); + warning.push(')'); + } + if !symbolic_facts.branch_facts.is_empty() + || !symbolic_facts.worker_islands.is_empty() + || !symbolic_facts.control_islands.is_empty() + || !symbolic_facts.memory_islands.is_empty() + { + warning.push_str(&format!( + "; branch_facts={}, worker_islands={}, control_islands={}, memory_islands={}, actionable_conditions={}, exact_conditions={}", + symbolic_facts.branch_facts.len(), + symbolic_facts.worker_islands.len(), + symbolic_facts.control_islands.len(), + symbolic_facts.memory_islands.len(), + symbolic_facts.actionable_compiled_condition_count(), + symbolic_facts.exact_compiled_condition_count(), + )); + } + warning +} + +pub fn build_semantic_type_fallback_plan( + function_name: &str, + arch_name: &str, + ptr_bits: u32, + symbolic_facts: &SymbolicSemanticFacts, +) -> TypeWritebackPlan { + let mut warnings = vec![semantic_fallback_warning(symbolic_facts)]; + if !symbolic_facts.type_ready() { + warnings.push("type analysis not ready from semantic capability".to_string()); + } + let mut local_structs = LocalStructArtifacts::default(); + augment_local_struct_artifacts_with_symbolic_facts( + &mut local_structs, + symbolic_facts, + ptr_bits, + ); + let mut signature = InferredSignature { + function_name: function_name.to_string(), + signature: format!("void {}(void)", function_name), + ret_type: "void".to_string(), + params: Vec::new(), + callconv: "unknown".to_string(), + arch: arch_name.to_string(), + confidence: 0, + callconv_confidence: 0, + }; + if let Some(merged_signature) = merge_slot_type_overrides_into_signature( + inferred_signature_to_spec(&signature, ptr_bits), + &local_structs.slot_type_overrides, + ptr_bits, + ) { + apply_signature_context_overrides(&mut signature, Some(&merged_signature), ptr_bits); + } + if !local_structs.struct_decls.is_empty() { + warnings.push(format!( + "semantic worker islands projected {} struct candidate(s)", + local_structs.struct_decls.len() + )); + } + + TypeWritebackPlan { + signature, + var_type_candidates: Vec::new(), + var_rename_candidates: Vec::new(), + struct_decls: local_structs.struct_decls, + global_type_links: Vec::new(), + diagnostics: TypeWritebackDiagnostics { + conflicts: Vec::new(), + warnings, + solver_warnings: Vec::new(), + }, + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct LocalAddrExpr { + slot: usize, + offset: i64, + confidence: u8, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +struct InferredLocalFieldEvidence { + reads: u32, + writes: u32, + widths: BTreeMap, + type_votes: BTreeMap, +} + +type LocalFieldEvidenceMap = HashMap>; + +fn collect_pointer_arg_slot_map(arch_name: Option<&str>, ptr_bits: u32) -> HashMap { + let (arg_regs, _, _) = recover_vars_arch_profile(arch_name); + let arch_name = arch_name.unwrap_or_default().to_ascii_lowercase(); + let is_arm64 = arch_name.contains("aarch64") || arch_name.contains("arm64"); + let is_x86_64 = arch_name.contains("x86-64") + || arch_name.contains("x86_64") + || arch_name.contains("amd64") + || arch_name.contains("x64"); + let is_riscv64 = arch_name.contains("riscv64") || arch_name.contains("rv64"); + + let mut out = HashMap::new(); + for (idx, (canonical, aliases)) in arg_regs.iter().enumerate() { + let include_alias = |alias: &str| -> bool { + if ptr_bits <= 32 { + return true; + } + let alias = alias.to_ascii_lowercase(); + if is_arm64 { + return alias.starts_with('x'); + } + if is_x86_64 { + return alias.starts_with('r'); + } + if is_riscv64 { + return alias.starts_with('x') || alias.starts_with('a'); + } + alias == (*canonical).to_ascii_lowercase() + }; + + if include_alias(canonical) { + out.insert((*canonical).to_string(), idx); + } + for alias in *aliases { + if include_alias(alias) { + out.insert((*alias).to_string(), idx); + } + } + } + out +} + +fn parse_ssa_const_offset(name: &str, ptr_bits: u32) -> Option { + let val_str = name + .strip_prefix("const:") + .or_else(|| name.strip_prefix("CONST:"))?; + let val_str = val_str.split('_').next().unwrap_or(val_str); + + let raw = if let Some(hex) = val_str + .strip_prefix("0x") + .or_else(|| val_str.strip_prefix("0X")) + { + u64::from_str_radix(hex, 16).ok()? + } else if let Some(dec) = val_str + .strip_prefix("0d") + .or_else(|| val_str.strip_prefix("0D")) + { + dec.parse::().ok()? + } else { + u64::from_str_radix(val_str, 16).ok()? + }; + Some(signed_offset_from_const(raw, ptr_bits)) +} + +pub fn infer_local_struct_artifacts_from_ssa( + ssa_blocks: &[SSABlock], + arch_name: Option<&str>, + ptr_bits: u32, + diagnostics: &mut TypeWritebackDiagnostics, +) -> LocalStructArtifacts { + let pointer_arg_slot_map = collect_pointer_arg_slot_map(arch_name, ptr_bits); + let (_, stack_bases, frame_bases) = recover_vars_arch_profile(arch_name); + let mut addr_exprs: HashMap = HashMap::new(); + let mut stack_addr_offsets: HashMap = HashMap::new(); + let mut stack_slot_values: HashMap<(u64, i64), LocalAddrExpr> = HashMap::new(); + let mut slot_field_evidence: LocalFieldEvidenceMap = HashMap::new(); + let offset_bound = 0x4000i64; + let block_ops: HashMap> = ssa_blocks + .iter() + .map(|block| { + let ops = block + .ops + .iter() + .filter_map(|op| { + op.dst() + .map(|dst| (ssa_var_block_key(block.addr, dst), op.clone())) + }) + .collect::>(); + (block.addr, ops) + }) + .collect(); + + fn is_scaled_index_like( + block_addr: u64, + var: &SSAVar, + ops_by_block: &HashMap>, + addr_exprs: &HashMap, + depth: u32, + ) -> bool { + if depth > 8 || var.is_const() { + return false; + } + let key = ssa_var_block_key(block_addr, var); + if addr_exprs.contains_key(&key) { + return false; + } + let Some(op) = ops_by_block.get(&block_addr).and_then(|ops| ops.get(&key)) else { + return true; + }; + match op { + SSAOp::Copy { src, .. } + | SSAOp::Cast { src, .. } + | SSAOp::New { src, .. } + | SSAOp::IntZExt { src, .. } + | SSAOp::IntSExt { src, .. } + | SSAOp::Trunc { src, .. } + | SSAOp::Subpiece { src, .. } => { + is_scaled_index_like(block_addr, src, ops_by_block, addr_exprs, depth + 1) + } + SSAOp::IntMult { a, b, .. } => { + (parse_ssa_const_offset(&a.name, 64).is_some() + && is_scaled_index_like(block_addr, b, ops_by_block, addr_exprs, depth + 1)) + || (parse_ssa_const_offset(&b.name, 64).is_some() + && is_scaled_index_like(block_addr, a, ops_by_block, addr_exprs, depth + 1)) + } + SSAOp::IntLeft { a, b, .. } => { + parse_ssa_const_offset(&b.name, 64).is_some() + && is_scaled_index_like(block_addr, a, ops_by_block, addr_exprs, depth + 1) + } + SSAOp::IntSub { a, b, .. } => { + parse_ssa_const_offset(&a.name, 64) == Some(0) + && is_scaled_index_like(block_addr, b, ops_by_block, addr_exprs, depth + 1) + } + SSAOp::Load { .. } | SSAOp::Phi { .. } => true, + _ => false, + } + } + + for block in ssa_blocks { + for op in &block.ops { + op.for_each_source(&mut |src: &SSAVar| { + if src.version != 0 { + return; + } + let key = src.name.to_ascii_lowercase(); + if let Some(slot) = pointer_arg_slot_map.get(key.as_str()).copied() { + addr_exprs + .entry(ssa_var_block_key(block.addr, src)) + .or_insert(LocalAddrExpr { + slot, + offset: 0, + confidence: 92, + }); + } + }); + } + } + + let is_stack_base = |name: &str| stack_bases.contains(&name) || frame_bases.contains(&name); + + for _ in 0..6 { + let mut changed = false; + for block in ssa_blocks { + for op in &block.ops { + let addr_of = |var: &SSAVar, map: &HashMap| { + if var.version == 0 { + let key = var.name.to_ascii_lowercase(); + if let Some(slot) = pointer_arg_slot_map.get(key.as_str()).copied() { + return Some(LocalAddrExpr { + slot, + offset: 0, + confidence: 92, + }); + } + } + map.get(&ssa_var_block_key(block.addr, var)).copied() + }; + let stack_slot_of = |var: &SSAVar, stack_map: &HashMap| { + stack_map.get(&ssa_var_block_key(block.addr, var)).copied() + }; + let set_expr = + |dst: &SSAVar, + expr: LocalAddrExpr, + map: &mut HashMap| { + let key = ssa_var_block_key(block.addr, dst); + match map.get(&key).copied() { + Some(prev) if prev.confidence >= expr.confidence => false, + _ => { + map.insert(key, expr); + true + } + } + }; + let set_stack_slot = |dst: &SSAVar, offset: i64, map: &mut HashMap| { + let key = ssa_var_block_key(block.addr, dst); + match map.get(&key).copied() { + Some(prev) if prev == offset => false, + _ => { + map.insert(key, offset); + true + } + } + }; + + match op { + SSAOp::Copy { dst, src } + | SSAOp::Cast { dst, src } + | SSAOp::New { dst, src } + | SSAOp::IntZExt { dst, src } + | SSAOp::IntSExt { dst, src } => { + if let Some(mut expr) = addr_of(src, &addr_exprs) { + expr.confidence = expr.confidence.saturating_sub(2); + changed |= set_expr(dst, expr, &mut addr_exprs); + } + if let Some(offset) = stack_slot_of(src, &stack_addr_offsets) { + changed |= set_stack_slot(dst, offset, &mut stack_addr_offsets); + } + } + SSAOp::Phi { dst, sources } => { + let mut selected = None; + let mut selected_slot = None; + for src in sources { + let Some(expr) = addr_of(src, &addr_exprs) else { + selected = None; + break; + }; + selected = match selected { + None => Some(expr), + Some(prev) + if prev.slot == expr.slot && prev.offset == expr.offset => + { + Some(LocalAddrExpr { + slot: prev.slot, + offset: prev.offset, + confidence: prev.confidence.max(expr.confidence), + }) + } + _ => None, + }; + let Some(slot) = stack_slot_of(src, &stack_addr_offsets) else { + selected_slot = None; + break; + }; + selected_slot = match selected_slot { + None => Some(slot), + Some(prev) if prev == slot => Some(prev), + _ => None, + }; + if selected.is_none() { + break; + } + } + if let Some(mut expr) = selected { + expr.confidence = expr.confidence.saturating_sub(3); + changed |= set_expr(dst, expr, &mut addr_exprs); + } + if let Some(slot) = selected_slot { + changed |= set_stack_slot(dst, slot, &mut stack_addr_offsets); + } + } + SSAOp::IntAdd { dst, a, b } => { + if let Some(off) = parse_ssa_const_offset(&b.name, ptr_bits) { + let a_lower = a.name.to_ascii_lowercase(); + if is_stack_base(a_lower.as_str()) { + changed |= set_stack_slot(dst, off, &mut stack_addr_offsets); + } + } + if let Some(off) = parse_ssa_const_offset(&a.name, ptr_bits) { + let b_lower = b.name.to_ascii_lowercase(); + if is_stack_base(b_lower.as_str()) { + changed |= set_stack_slot(dst, off, &mut stack_addr_offsets); + } + } + if let Some(base) = addr_of(a, &addr_exprs) + && let Some(delta) = parse_ssa_const_offset(&b.name, ptr_bits) + { + let off = base.offset.saturating_add(delta); + if (-offset_bound..=offset_bound).contains(&off) { + changed |= set_expr( + dst, + LocalAddrExpr { + slot: base.slot, + offset: off, + confidence: base.confidence.saturating_sub(1), + }, + &mut addr_exprs, + ); + } + } else if let Some(base) = addr_of(a, &addr_exprs) + && is_scaled_index_like(block.addr, b, &block_ops, &addr_exprs, 0) + { + changed |= set_expr( + dst, + LocalAddrExpr { + slot: base.slot, + offset: base.offset, + confidence: base.confidence.saturating_sub(4), + }, + &mut addr_exprs, + ); + } else if let Some(base) = addr_of(b, &addr_exprs) + && let Some(delta) = parse_ssa_const_offset(&a.name, ptr_bits) + { + let off = base.offset.saturating_add(delta); + if (-offset_bound..=offset_bound).contains(&off) { + changed |= set_expr( + dst, + LocalAddrExpr { + slot: base.slot, + offset: off, + confidence: base.confidence.saturating_sub(1), + }, + &mut addr_exprs, + ); + } + } else if let Some(base) = addr_of(b, &addr_exprs) + && is_scaled_index_like(block.addr, a, &block_ops, &addr_exprs, 0) + { + changed |= set_expr( + dst, + LocalAddrExpr { + slot: base.slot, + offset: base.offset, + confidence: base.confidence.saturating_sub(4), + }, + &mut addr_exprs, + ); + } + } + SSAOp::IntSub { dst, a, b } => { + if let Some(delta) = parse_ssa_const_offset(&b.name, ptr_bits) { + let a_lower = a.name.to_ascii_lowercase(); + if is_stack_base(a_lower.as_str()) { + changed |= set_stack_slot( + dst, + delta.saturating_neg(), + &mut stack_addr_offsets, + ); + } + } + if let Some(base) = addr_of(a, &addr_exprs) + && let Some(delta) = parse_ssa_const_offset(&b.name, ptr_bits) + { + let off = base.offset.saturating_sub(delta); + if (-offset_bound..=offset_bound).contains(&off) { + changed |= set_expr( + dst, + LocalAddrExpr { + slot: base.slot, + offset: off, + confidence: base.confidence.saturating_sub(1), + }, + &mut addr_exprs, + ); + } + } else if let Some(base) = addr_of(a, &addr_exprs) + && is_scaled_index_like(block.addr, b, &block_ops, &addr_exprs, 0) + { + changed |= set_expr( + dst, + LocalAddrExpr { + slot: base.slot, + offset: base.offset, + confidence: base.confidence.saturating_sub(4), + }, + &mut addr_exprs, + ); + } + } + SSAOp::Store { addr, val, .. } => { + if let Some(offset) = stack_slot_of(addr, &stack_addr_offsets) + && let Some(mut expr) = addr_of(val, &addr_exprs) + { + expr.confidence = expr.confidence.saturating_sub(2); + let key = (block.addr, offset); + match stack_slot_values.get(&key).copied() { + Some(prev) if prev.confidence >= expr.confidence => {} + _ => { + stack_slot_values.insert(key, expr); + changed = true; + } + } + } + } + SSAOp::Load { dst, addr, .. } => { + if let Some(offset) = stack_slot_of(addr, &stack_addr_offsets) + && let Some(mut expr) = + stack_slot_values.get(&(block.addr, offset)).copied() + { + expr.confidence = expr.confidence.saturating_sub(3); + changed |= set_expr(dst, expr, &mut addr_exprs); + } + } + _ => {} + } + } + } + if !changed { + break; + } + } + + for block in ssa_blocks { + for op in &block.ops { + let resolve_addr = |addr: &SSAVar| -> Option { + if addr.version == 0 { + let key = addr.name.to_ascii_lowercase(); + if let Some(slot) = pointer_arg_slot_map.get(key.as_str()).copied() { + return Some(LocalAddrExpr { + slot, + offset: 0, + confidence: 92, + }); + } + } + addr_exprs + .get(&ssa_var_block_key(block.addr, addr)) + .copied() + }; + match op { + SSAOp::Load { dst, addr, .. } => { + if let Some(expr) = resolve_addr(addr) + && (0..=offset_bound).contains(&expr.offset) + { + let entry = slot_field_evidence + .entry(expr.slot) + .or_default() + .entry(expr.offset as u64) + .or_default(); + entry.reads = entry.reads.saturating_add(1); + *entry.widths.entry(dst.size).or_insert(0) += 1; + *entry.type_votes.entry(size_to_type(dst.size)).or_insert(0) += 1; + } + } + SSAOp::Store { addr, val, .. } => { + if let Some(expr) = resolve_addr(addr) + && (0..=offset_bound).contains(&expr.offset) + { + let entry = slot_field_evidence + .entry(expr.slot) + .or_default() + .entry(expr.offset as u64) + .or_default(); + entry.writes = entry.writes.saturating_add(1); + *entry.widths.entry(val.size).or_insert(0) += 1; + *entry.type_votes.entry(size_to_type(val.size)).or_insert(0) += 1; + } + } + _ => {} + } + } + } + + let mut struct_decls = Vec::new(); + let mut slot_type_overrides = HashMap::new(); + let mut slot_field_profiles = HashMap::new(); + let mut slots: Vec = slot_field_evidence.keys().copied().collect(); + slots.sort_unstable(); + + for slot in slots { + let Some(fields_map) = slot_field_evidence.get(&slot) else { + continue; + }; + if fields_map.is_empty() { + continue; + } + let mut shape = String::new(); + let mut fields = Vec::new(); + let mut normalized_fields = BTreeMap::new(); + let mut confidence_acc = 0u32; + for (offset, evidence) in fields_map { + let total_votes: u32 = evidence.type_votes.values().copied().sum(); + let Some((field_type, field_votes)) = evidence + .type_votes + .iter() + .max_by_key(|(_, count)| **count) + .map(|(ty, count)| (ty.clone(), *count)) + else { + continue; + }; + if evidence.type_votes.len() > 1 { + diagnostics.conflicts.push(format!( + "slot {slot} field +0x{offset:x} conflicting type votes {:?}", + evidence.type_votes + )); + } + let strength = ((field_votes.saturating_mul(100)) / total_votes.max(1)) as u8; + let rw_bonus = if evidence.reads > 0 && evidence.writes > 0 { + 10 + } else { + 0 + }; + let field_conf = 70u8.saturating_add(strength / 3).saturating_add(rw_bonus); + confidence_acc = confidence_acc.saturating_add(field_conf as u32); + shape.push_str(&format!("{offset:x}:{field_type};")); + normalized_fields.insert(*offset, field_type.clone()); + fields.push(StructFieldCandidate { + name: format!("f_{offset:x}"), + offset: *offset, + field_type, + confidence: field_conf, + }); + } + if fields.is_empty() { + continue; + } + let avg_conf = (confidence_acc / fields.len() as u32).clamp(1, 100) as u8; + let allow_single_field = fields.len() == 1 && avg_conf >= 94; + if fields.len() < 2 && !allow_single_field { + continue; + } + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + shape.hash(&mut hasher); + let struct_name = format!("sla_struct_{:016x}", hasher.finish()); + let Some(decl) = build_struct_decl(&struct_name, &fields, ptr_bits) else { + continue; + }; + struct_decls.push(StructDeclCandidate { + name: struct_name.clone(), + decl, + confidence: avg_conf.max(84), + source: StructDeclSource::LocalInferred, + fields, + }); + slot_field_profiles.insert(slot, normalized_fields); + slot_type_overrides.insert(slot, format!("struct {struct_name} *")); + } + + LocalStructArtifacts { + struct_decls, + slot_type_overrides, + slot_field_profiles, + } +} + pub fn augment_local_struct_artifacts_with_symbolic_facts( local_structs: &mut LocalStructArtifacts, symbolic_facts: &SymbolicSemanticFacts, ptr_bits: u32, ) { let mut projected_profiles = BTreeMap::>::new(); - for compiled in symbolic_facts - .branch_facts - .iter() - .filter_map(|fact| fact.actionable_compiled_condition()) - .chain( - symbolic_facts - .control_islands - .iter() - .filter_map(|island| island.actionable_compiled_condition()), - ) - { - if !(compiled.confidence.is_reliable() && compiled.evidence.is_reliable()) { + for island in &symbolic_facts.worker_islands { + if !(island.confidence.is_reliable() && island.evidence.is_reliable()) { continue; } - for term in &compiled.memory_terms { + for term in &island.memory_terms { let Some((slot, offset, field_type)) = symbolic_memory_term_slot_field(term, ptr_bits) else { continue; @@ -699,6 +1424,60 @@ pub fn augment_local_struct_artifacts_with_symbolic_facts( .or_insert(field_type); } } + if projected_profiles.is_empty() { + for island in &symbolic_facts.memory_islands { + if !(island.confidence.is_reliable() && island.evidence.is_reliable()) { + continue; + } + for term in &island.terms { + let Some((slot, offset, field_type)) = + symbolic_memory_term_slot_field(term, ptr_bits) + else { + continue; + }; + projected_profiles + .entry(slot) + .or_default() + .entry(offset) + .or_insert(field_type); + } + } + } + if projected_profiles.is_empty() { + for compiled in symbolic_facts + .branch_facts + .iter() + .filter_map(|fact| fact.actionable_compiled_condition()) + .chain( + symbolic_facts + .worker_islands + .iter() + .filter_map(|island| island.actionable_compiled_condition()), + ) + .chain( + symbolic_facts + .control_islands + .iter() + .filter_map(|island| island.actionable_compiled_condition()), + ) + { + if !(compiled.confidence.is_reliable() && compiled.evidence.is_reliable()) { + continue; + } + for term in &compiled.memory_terms { + let Some((slot, offset, field_type)) = + symbolic_memory_term_slot_field(term, ptr_bits) + else { + continue; + }; + projected_profiles + .entry(slot) + .or_default() + .entry(offset) + .or_insert(field_type); + } + } + } for (slot, projected) in projected_profiles { let profile = local_structs.slot_field_profiles.entry(slot).or_default(); @@ -740,6 +1519,75 @@ pub fn augment_local_struct_artifacts_with_symbolic_facts( } } +fn augment_local_struct_artifacts_with_local_field_accesses( + local_structs: &mut LocalStructArtifacts, + local_field_accesses: &[LocalFieldAccessFact], + ptr_bits: u32, +) { + let mut projected_profiles = BTreeMap::>::new(); + for access in local_field_accesses { + let field_type = access + .field_type + .clone() + .unwrap_or_else(|| access.field_name.clone()); + projected_profiles + .entry(access.slot) + .or_default() + .entry(access.field_offset) + .or_insert(field_type); + } + + for (slot, projected) in projected_profiles { + let profile = local_structs.slot_field_profiles.entry(slot).or_default(); + for (offset, field_type) in projected { + profile.entry(offset).or_insert(field_type); + } + if profile.is_empty() || local_structs.slot_type_overrides.contains_key(&slot) { + continue; + } + + let allow_single_field = profile.len() == 1; + if profile.len() < 2 && !allow_single_field { + continue; + } + let mut shape = String::new(); + let fields = profile + .iter() + .map(|(offset, field_type)| { + shape.push_str(&format!("{offset:x}:{field_type};")); + StructFieldCandidate { + name: format!("f_{offset:x}"), + offset: *offset, + field_type: field_type.clone(), + confidence: 90, + } + }) + .collect::>(); + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + shape.hash(&mut hasher); + let struct_name = format!("sla_struct_{:016x}", hasher.finish()); + let Some(decl) = build_struct_decl(&struct_name, &fields, ptr_bits) else { + continue; + }; + if !local_structs + .struct_decls + .iter() + .any(|candidate| candidate.name.eq_ignore_ascii_case(&struct_name)) + { + local_structs.struct_decls.push(StructDeclCandidate { + name: struct_name.clone(), + decl, + confidence: 90, + source: StructDeclSource::LocalInferred, + fields, + }); + } + local_structs + .slot_type_overrides + .insert(slot, format!("struct {struct_name} *")); + } +} + fn symbolic_memory_term_slot_field( term: &SymbolicMemoryCondition, _ptr_bits: u32, @@ -2463,119 +3311,7 @@ fn estimate_type_like_size_bytes(ty: &CTypeLike, ptr_bits: u32) -> Option { } fn render_signature_type(ty: &CTypeLike, ptr_bits: u32) -> String { - render_type_like(&materialize_signature_type_like(ty.clone(), ptr_bits)) -} - -fn materialize_signature_type_like(ty: CTypeLike, ptr_bits: u32) -> CTypeLike { - match ty { - CTypeLike::Pointer(inner) => { - if matches!(*inner, CTypeLike::Unknown | CTypeLike::Void) - || matches!( - inner.as_ref(), - CTypeLike::Struct(name) - | CTypeLike::Union(name) - | CTypeLike::Enum(name) - if is_unmaterialized_aggregate_name(name) - ) - { - return CTypeLike::Pointer(Box::new(CTypeLike::Void)); - } - CTypeLike::Pointer(Box::new(materialize_signature_type_like(*inner, ptr_bits))) - } - CTypeLike::Array(inner, len) => { - if matches!(*inner, CTypeLike::Unknown | CTypeLike::Void) { - return CTypeLike::Array( - Box::new(CTypeLike::Int { - bits: 8, - signedness: Signedness::Unsigned, - }), - len, - ); - } - CTypeLike::Array( - Box::new(materialize_signature_type_like(*inner, ptr_bits)), - len, - ) - } - CTypeLike::Unknown => fallback_scalar_type_like(ptr_bits), - CTypeLike::Struct(name) if is_unmaterialized_aggregate_name(&name) => { - fallback_scalar_type_like(ptr_bits) - } - CTypeLike::Union(name) if is_unmaterialized_aggregate_name(&name) => { - fallback_scalar_type_like(ptr_bits) - } - CTypeLike::Enum(name) if is_unmaterialized_aggregate_name(&name) => { - fallback_scalar_type_like(ptr_bits) - } - other => other, - } -} - -fn fallback_scalar_type_like(ptr_bits: u32) -> CTypeLike { - CTypeLike::Int { - bits: if ptr_bits >= 64 { 64 } else { 32 }, - signedness: Signedness::Signed, - } -} - -fn render_type_like(ty: &CTypeLike) -> String { - match ty { - CTypeLike::Void => "void".to_string(), - CTypeLike::Bool => "bool".to_string(), - CTypeLike::Int { - bits: 8, - signedness: Signedness::Signed, - } => "int8_t".to_string(), - CTypeLike::Int { - bits: 16, - signedness: Signedness::Signed, - } => "int16_t".to_string(), - CTypeLike::Int { - bits: 32, - signedness: Signedness::Signed, - } => "int32_t".to_string(), - CTypeLike::Int { - bits: 64, - signedness: Signedness::Signed, - } => "int64_t".to_string(), - CTypeLike::Int { - bits, - signedness: Signedness::Signed | Signedness::Unknown, - } => { - format!("int{bits}_t") - } - CTypeLike::Int { - bits: 8, - signedness: Signedness::Unsigned, - } => "uint8_t".to_string(), - CTypeLike::Int { - bits: 16, - signedness: Signedness::Unsigned, - } => "uint16_t".to_string(), - CTypeLike::Int { - bits: 32, - signedness: Signedness::Unsigned, - } => "uint32_t".to_string(), - CTypeLike::Int { - bits: 64, - signedness: Signedness::Unsigned, - } => "uint64_t".to_string(), - CTypeLike::Int { - bits, - signedness: Signedness::Unsigned, - } => format!("uint{bits}_t"), - CTypeLike::Float(32) => "float".to_string(), - CTypeLike::Float(64) => "double".to_string(), - CTypeLike::Float(bits) => format!("float{bits}"), - CTypeLike::Pointer(inner) => format!("{}*", render_type_like(inner)), - CTypeLike::Array(inner, Some(size)) => format!("{}[{}]", render_type_like(inner), size), - CTypeLike::Array(inner, None) => format!("{}[]", render_type_like(inner)), - CTypeLike::Struct(name) => format!("struct {name}"), - CTypeLike::Union(name) => format!("union {name}"), - CTypeLike::Enum(name) => format!("enum {name}"), - CTypeLike::Function => "void (*)()".to_string(), - CTypeLike::Unknown => "/* unknown */".to_string(), - } + crate::render_signature_type(ty, ptr_bits) } fn format_signature( @@ -2583,12 +3319,7 @@ fn format_signature( ret_type: &str, params: &[InferredSignatureParam], ) -> String { - let args = params - .iter() - .map(|param| format!("{},{}", param.param_type, param.name)) - .collect::>() - .join(","); - format!("{ret_type} {function_name} ({args})") + crate::format_afs_signature(function_name, ret_type, params) } fn build_struct_decl( @@ -2629,11 +3360,6 @@ fn is_generated_local_struct_name(name: &str) -> bool { .starts_with("sla_struct_") } -fn is_unmaterialized_aggregate_name(name: &str) -> bool { - let lower = name.trim().to_ascii_lowercase(); - lower.is_empty() || lower == "anon" || lower.starts_with("anon_") -} - fn is_generic_type_string(ty: &str) -> bool { let normalized = normalize_external_type_name(ty); let lower = normalized.trim().to_ascii_lowercase(); @@ -4506,6 +5232,7 @@ mod tests { }; let symbolic_facts = crate::facts::SymbolicSemanticFacts { branch_facts: Vec::new(), + worker_islands: Vec::new(), control_islands: vec![crate::facts::SymbolicControlIsland { kind: crate::facts::SymbolicControlIslandKind::LargeCfgBranchFrontier, anchor_block: 0x401000, @@ -4521,6 +5248,7 @@ mod tests { evidence: crate::facts::SymbolicSemanticEvidence::exact(), confidence: crate::facts::SymbolicSemanticConfidence::Exact, }], + memory_islands: Vec::new(), diagnostics: crate::facts::SymbolicFactDiagnostics::default(), interpreter: None, vm_step: None, @@ -4552,4 +5280,162 @@ mod tests { .any(|decl| decl.name == "sla_struct_symbolic_arg1") ); } + + #[test] + fn symbolic_memory_islands_seed_local_struct_profiles_without_control_islands() { + let symbolic_facts = crate::facts::SymbolicSemanticFacts { + branch_facts: Vec::new(), + worker_islands: Vec::new(), + control_islands: Vec::new(), + memory_islands: vec![crate::facts::SymbolicMemoryIsland { + kind: crate::facts::SymbolicMemoryIslandKind::ConditionFrontier, + anchor_block: 0x401000, + terms: vec![crate::facts::SymbolicMemoryCondition { + region: crate::facts::SymbolicMemoryRegion::Argument { index: 0 }, + offset_lo: 8, + offset_hi: 8, + size: 4, + exact_offset: true, + evidence: crate::facts::SymbolicSemanticEvidence::exact(), + confidence: crate::facts::SymbolicSemanticConfidence::Exact, + binding: None, + expr: "*(arg0 + 8)".to_string(), + value_expr: None, + exact_value: false, + }], + evidence: crate::facts::SymbolicSemanticEvidence::exact(), + confidence: crate::facts::SymbolicSemanticConfidence::Exact, + }], + diagnostics: crate::facts::SymbolicFactDiagnostics::default(), + interpreter: None, + vm_step: None, + vm_transfer: None, + }; + let mut local_structs = LocalStructArtifacts::default(); + + augment_local_struct_artifacts_with_symbolic_facts(&mut local_structs, &symbolic_facts, 64); + + assert_eq!( + local_structs + .slot_field_profiles + .get(&0) + .and_then(|profile| profile.get(&8)) + .map(String::as_str), + Some("int32_t") + ); + assert_eq!( + local_structs + .slot_type_overrides + .get(&0) + .map(String::as_str), + Some("struct sla_struct_symbolic_arg1 *") + ); + } + + #[test] + fn semantic_type_fallback_plan_uses_typed_symbolic_summary() { + let symbolic_facts = crate::facts::SymbolicSemanticFacts { + branch_facts: vec![crate::facts::SymbolicBranchFact { + block_addr: 0x401000, + true_target: 0x401010, + false_target: 0x401020, + true_status: crate::facts::SymbolicReachabilityStatus::Reachable, + false_status: crate::facts::SymbolicReachabilityStatus::Unreachable, + true_condition: Some("x == 0".to_string()), + false_condition: Some("x != 0".to_string()), + true_compiled: None, + false_compiled: None, + }], + worker_islands: Vec::new(), + control_islands: vec![crate::facts::SymbolicControlIsland { + kind: crate::facts::SymbolicControlIslandKind::LargeCfgBranchFrontier, + anchor_block: 0x401000, + frontier_targets: vec![0x401010], + facts: vec![crate::facts::SymbolicControlFact { + target: 0x401010, + status: crate::facts::SymbolicReachabilityStatus::Reachable, + condition: Some("x == 0".to_string()), + compiled: Some(crate::facts::SymbolicCompiledCondition { + simplified: "x == 0".to_string(), + terms: vec!["x == 0".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: crate::facts::SymbolicConditionPrecision::Exact, + evidence: crate::facts::SymbolicSemanticEvidence::exact(), + confidence: crate::facts::SymbolicSemanticConfidence::Exact, + supported_paths: 1, + total_paths: 1, + }), + evidence: crate::facts::SymbolicSemanticEvidence::exact(), + confidence: crate::facts::SymbolicSemanticConfidence::Exact, + }], + evidence: crate::facts::SymbolicSemanticEvidence::exact(), + confidence: crate::facts::SymbolicSemanticConfidence::Exact, + }], + memory_islands: vec![crate::facts::SymbolicMemoryIsland { + kind: crate::facts::SymbolicMemoryIslandKind::LargeCfgConditionFrontier, + anchor_block: 0x401000, + terms: vec![crate::facts::SymbolicMemoryCondition { + region: crate::facts::SymbolicMemoryRegion::Argument { index: 0 }, + offset_lo: 8, + offset_hi: 8, + size: 4, + exact_offset: true, + evidence: crate::facts::SymbolicSemanticEvidence::exact(), + confidence: crate::facts::SymbolicSemanticConfidence::Exact, + binding: None, + expr: "*(arg0 + 8)".to_string(), + value_expr: None, + exact_value: false, + }], + evidence: crate::facts::SymbolicSemanticEvidence::exact(), + confidence: crate::facts::SymbolicSemanticConfidence::Exact, + }], + diagnostics: crate::facts::SymbolicFactDiagnostics { + skipped_large_cfg: true, + semantic_mode: Some(crate::facts::SymbolicSemanticMode::Residual), + semantic_capability: Some(crate::facts::SymbolicSemanticCapability { + query_ready: true, + type_ready: false, + decompile_ready: true, + }), + slice_class: Some(crate::facts::SymbolicSemanticSliceClass::Worker), + residual_reasons: vec![crate::facts::SymbolicSemanticResidualReason::LargeCfg], + ..Default::default() + }, + interpreter: None, + vm_step: None, + vm_transfer: None, + }; + + let plan = build_semantic_type_fallback_plan("fcn.401000", "x86-64", 64, &symbolic_facts); + + assert_eq!(plan.signature.params.len(), 1); + assert!(plan.signature.params[0].param_type.contains("struct ")); + assert_eq!(plan.struct_decls.len(), 1); + assert!(plan + .diagnostics + .warnings + .iter() + .any(|warning| warning.contains("semantic fallback: worker slice in residual mode"))); + assert!( + plan.diagnostics + .warnings + .iter() + .any(|warning| warning.contains("memory_islands=1")) + ); + assert!(plan + .diagnostics + .warnings + .iter() + .any(|warning| warning.contains("type analysis not ready from semantic capability"))); + assert!( + plan.diagnostics + .warnings + .iter() + .any(|warning| warning.contains("projected 1 struct candidate")) + ); + } } diff --git a/r2plugin/r_anal_sleigh.c b/r2plugin/r_anal_sleigh.c index bec085c..bd22d0b 100644 --- a/r2plugin/r_anal_sleigh.c +++ b/r2plugin/r_anal_sleigh.c @@ -152,6 +152,12 @@ extern char *r2dec_function_with_context_scope(const R2ILContext *ctx, const R2I unsigned long long fcn_addr, const char *func_name, const char *func_names_json, const char *strings_json, const char *symbols_json, const char *external_context_json, const R2ILFunctionBlocks *functions, size_t num_functions); +extern char *r2dec_semantic_worker_linearization_scope_ffi(const R2ILContext *ctx, const R2ILBlock **blocks, size_t num_blocks, + unsigned long long fcn_addr, const char *func_name, size_t block_count, size_t loop_count, + size_t back_edge_count, size_t max_switch_cases, const R2ILFunctionBlocks *functions, size_t num_functions); +extern char *r2dec_block_guard_comment_ffi(const char *func_name, size_t blocks, size_t max_blocks); +extern char *r2dec_cfg_guard_comment_ffi(const char *func_name, size_t block_count, + size_t loop_count, size_t back_edge_count, size_t max_switch_cases); /* CFG */ extern char *r2cfg_function_ascii(const R2ILContext *ctx, const R2ILBlock **blocks, size_t num_blocks); @@ -2996,6 +3002,165 @@ static bool function_exceeds_helper_scope_budget(const RAnalFunction *fcn) { return cost > SLEIGH_SCOPE_HELPER_MAX_COST; } +static bool is_autogenerated_function_name(const char *name) { + if (!name || !*name) { + return true; + } + return !strncmp (name, "fcn.", 4) + || !strncmp (name, "fcn_", 4) + || !strncmp (name, "sub.", 4) + || !strncmp (name, "sub_", 4) + || !strncmp (name, "loc.", 4); +} + +static bool should_skip_decompile_symbolic_scope(const RAnalFunction *fcn) { + return fcn && is_autogenerated_function_name (fcn->name) + && function_exceeds_helper_scope_budget (fcn); +} + +typedef struct { + size_t block_count; + size_t loop_count; + size_t back_edge_count; + size_t max_switch_cases; +} DecompileCFGRiskSummary; + +typedef struct { + RAnal *anal; + RAnalFunction *fcn; + DecompileCFGRiskSummary *summary; + HtUP *visited; + HtUP *in_stack; + HtUP *loop_headers; +} DecompileCFGRiskWalk; + +static void decompile_cfg_risk_visit_block(DecompileCFGRiskWalk *walk, RAnalBlock *bb); + +static void decompile_cfg_risk_visit_addr(DecompileCFGRiskWalk *walk, ut64 addr) { + RAnalBlock *succ; + bool found; + if (!walk || !walk->anal || !walk->fcn || addr == UT64_MAX) { + return; + } + succ = r_anal_function_bbget_at (walk->anal, walk->fcn, addr); + if (!succ) { + succ = r_anal_function_bbget_in (walk->anal, walk->fcn, addr); + } + if (!succ) { + return; + } + ht_up_find (walk->in_stack, succ->addr, &found); + if (found) { + bool seen_header; + walk->summary->back_edge_count++; + ht_up_find (walk->loop_headers, succ->addr, &seen_header); + if (!seen_header) { + ht_up_insert (walk->loop_headers, succ->addr, (void *)1); + walk->summary->loop_count++; + } + return; + } + ht_up_find (walk->visited, succ->addr, &found); + if (!found) { + decompile_cfg_risk_visit_block (walk, succ); + } +} + +static void decompile_cfg_risk_visit_block(DecompileCFGRiskWalk *walk, RAnalBlock *bb) { + size_t case_count = 0; + bool found; + RListIter *iter; + RAnalCaseOp *caseop; + if (!walk || !bb) { + return; + } + ht_up_find (walk->visited, bb->addr, &found); + if (found) { + return; + } + ht_up_insert (walk->visited, bb->addr, (void *)1); + ht_up_insert (walk->in_stack, bb->addr, (void *)1); + + if (bb->switch_op) { + if (bb->switch_op->amount > 0) { + case_count = (size_t)bb->switch_op->amount; + } + if (bb->switch_op->cases) { + size_t listed_cases = (size_t)r_list_length (bb->switch_op->cases); + if (listed_cases > case_count) { + case_count = listed_cases; + } + } + if (case_count > walk->summary->max_switch_cases) { + walk->summary->max_switch_cases = case_count; + } + } + + decompile_cfg_risk_visit_addr (walk, bb->jump); + decompile_cfg_risk_visit_addr (walk, bb->fail); + if (bb->switch_op && bb->switch_op->cases) { + r_list_foreach (bb->switch_op->cases, iter, caseop) { + if (!caseop) { + continue; + } + decompile_cfg_risk_visit_addr (walk, caseop->jump); + } + } + + ht_up_delete (walk->in_stack, bb->addr); +} + +static bool compute_decompile_cfg_risk_summary(RAnal *anal, RAnalFunction *fcn, DecompileCFGRiskSummary *out) { + DecompileCFGRiskWalk walk; + RAnalBlock *entry; + if (!anal || !fcn || !out) { + return false; + } + memset (out, 0, sizeof (*out)); + out->block_count = (size_t)function_bb_count (fcn); + walk.anal = anal; + walk.fcn = fcn; + walk.summary = out; + walk.visited = ht_up_new0 (); + walk.in_stack = ht_up_new0 (); + walk.loop_headers = ht_up_new0 (); + if (!walk.visited || !walk.in_stack || !walk.loop_headers) { + ht_up_free (walk.visited); + ht_up_free (walk.in_stack); + ht_up_free (walk.loop_headers); + return false; + } + entry = r_anal_function_bbget_in (anal, fcn, fcn->addr); + if (!entry) { + entry = r_anal_function_bbget_at (anal, fcn, fcn->addr); + } + if (entry) { + decompile_cfg_risk_visit_block (&walk, entry); + } + ht_up_free (walk.visited); + ht_up_free (walk.in_stack); + ht_up_free (walk.loop_headers); + return true; +} + +static size_t decompiler_max_blocks_preflight(void) { + const char *raw = getenv ("SLEIGH_DEC_MAX_BLOCKS"); + char *endptr = NULL; + unsigned long long parsed; + if (!raw || !*raw) { + return 200; + } + errno = 0; + parsed = strtoull (raw, &endptr, 10); + if (errno != 0 || endptr == raw || parsed == 0) { + return 200; + } + if (parsed > (unsigned long long)SIZE_MAX) { + return SIZE_MAX; + } + return (size_t)parsed; +} + static RAnalFunction *materialize_function_at(RAnal *anal, ut64 addr) { RAnalFunction *fcn; int ret; @@ -6771,6 +6936,7 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { size_t seen_count = 0; size_t seen_cap = 0; int interproc_max_iters = cfg_get_type_interproc_max_iters (anal); + bool prefer_bounded_semantic_type_plan = false; if (!ctx) { R_LOG_ERROR ("r2sleigh: no context"); @@ -6792,6 +6958,7 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { R_LOG_ERROR ("r2sleigh: failed to lift function blocks"); return strdup (""); } + prefer_bounded_semantic_type_plan = should_skip_decompile_symbolic_scope (fcn); external_context_json = sleigh_collect_external_context_json (anal, fcn); if (!external_context_json || (external_context_json[0] != '{' && external_context_json[0] != '[')) { @@ -6799,22 +6966,30 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { external_context_json = strdup ("{}"); } - warm_type_payload_cache_for_function (core, anal, ctx, fcn, interproc_max_iters, - &seen_addrs, &seen_count, &seen_cap); - interproc_scope_json = build_type_interproc_scope_json (core, anal, ctx, fcn, &blocks); + if (!prefer_bounded_semantic_type_plan) { + warm_type_payload_cache_for_function (core, anal, ctx, fcn, interproc_max_iters, + &seen_addrs, &seen_count, &seen_cap); + interproc_scope_json = build_type_interproc_scope_json (core, anal, ctx, fcn, &blocks); + } SymFunctionScope sym_scope; if (build_symbolic_function_scope (anal, fcn, ctx, &sym_scope)) { result = r2sleigh_infer_type_writeback_json_scope_ex (ctx, (const R2ILBlock **)blocks.blocks, blocks.count, fcn->addr, fcn->name, external_context_json, - 1, interproc_max_iters, 1, interproc_scope_json? interproc_scope_json: "{}", + 1, + prefer_bounded_semantic_type_plan? 1: interproc_max_iters, + prefer_bounded_semantic_type_plan? 0: 1, + interproc_scope_json? interproc_scope_json: "{}", sym_scope.functions, sym_scope.count); sym_function_scope_free (&sym_scope); } else { result = r2sleigh_infer_type_writeback_json_ex (ctx, (const R2ILBlock **)blocks.blocks, blocks.count, fcn->addr, fcn->name, external_context_json, - 1, interproc_max_iters, 1, interproc_scope_json? interproc_scope_json: "{}"); + 1, + prefer_bounded_semantic_type_plan? 1: interproc_max_iters, + prefer_bounded_semantic_type_plan? 0: 1, + interproc_scope_json? interproc_scope_json: "{}"); } if (cons) { if (result && *result) { @@ -6849,6 +7024,25 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { return strdup(""); } + if (is_autogenerated_function_name (fcn->name)) { + DecompileCFGRiskSummary cfg_summary; + char *cfg_guard_comment = NULL; + if (compute_decompile_cfg_risk_summary (anal, fcn, &cfg_summary)) { + cfg_guard_comment = r2dec_cfg_guard_comment_ffi ( + fcn->name, + cfg_summary.block_count, + cfg_summary.loop_count, + cfg_summary.back_edge_count, + cfg_summary.max_switch_cases); + } + if (cfg_guard_comment) { + if (cons) { + r_cons_printf (cons, "%s\n", cfg_guard_comment); + } + r2il_string_free (cfg_guard_comment); + return strdup(""); + } + } /* Lift all blocks */ BlockArray blocks; if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { @@ -7157,6 +7351,34 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { } return strdup(""); } + + size_t decompile_max_blocks = decompiler_max_blocks_preflight (); + int bb_count = function_bb_count (fcn); + DecompileCFGRiskSummary cfg_summary; + bool have_cfg_summary = false; + if (is_autogenerated_function_name (fcn->name)) { + have_cfg_summary = compute_decompile_cfg_risk_summary (anal, fcn, &cfg_summary); + } + if (is_autogenerated_function_name (fcn->name) + && bb_count > 0 + && (size_t)bb_count > decompile_max_blocks) { + char *guard_comment = r2dec_block_guard_comment_ffi ( + fcn->name, (size_t)bb_count, decompile_max_blocks); + if (cons) { + if (guard_comment && guard_comment[0]) { + r_cons_printf (cons, "%s\n", guard_comment); + } else { + const char *fname = (fcn && fcn->name) ? fcn->name : "unknown"; + r_cons_printf (cons, + "/* r2dec fallback: skipped decompilation for %s (complex loop graph; block preflight exceeded %zu) */\n", + fname, decompile_max_blocks); + } + } + if (guard_comment) { + r2il_string_free (guard_comment); + } + return strdup(""); + } /* Lift all blocks */ BlockArray blocks; if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { @@ -7164,6 +7386,39 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { return strdup(""); } + char *result = NULL; + SymFunctionScope sym_scope; + bool have_sym_scope = build_symbolic_function_scope (anal, fcn, ctx, &sym_scope); + if (have_cfg_summary) { + result = r2dec_semantic_worker_linearization_scope_ffi ( + ctx, + (const R2ILBlock **)blocks.blocks, + blocks.count, + fcn->addr, + fcn->name, + cfg_summary.block_count, + cfg_summary.loop_count, + cfg_summary.back_edge_count, + cfg_summary.max_switch_cases, + have_sym_scope? sym_scope.functions: NULL, + have_sym_scope? sym_scope.count: 0); + if (result && result[0]) { + if (cons) { + r_cons_printf (cons, "%s\n", result); + } + r2il_string_free (result); + if (have_sym_scope) { + sym_function_scope_free (&sym_scope); + } + block_array_free (&blocks); + return strdup(""); + } + if (result) { + r2il_string_free (result); + result = NULL; + } + } + /* Gather function names from r2 */ char *func_names_json = NULL; char *strings_json = NULL; @@ -7274,9 +7529,7 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { } /* Decompile with context */ - char *result; - SymFunctionScope sym_scope; - if (build_symbolic_function_scope (anal, fcn, ctx, &sym_scope)) { + if (have_sym_scope) { result = r2dec_function_with_context_scope (ctx, (const R2ILBlock **)blocks.blocks, blocks.count, fcn->addr, fcn->name, func_names_json, strings_json, symbols_json, external_context_json, sym_scope.functions, sym_scope.count); diff --git a/r2plugin/src/analysis/sym.rs b/r2plugin/src/analysis/sym.rs index 1dc78f2..f551ef8 100644 --- a/r2plugin/src/analysis/sym.rs +++ b/r2plugin/src/analysis/sym.rs @@ -1,6 +1,5 @@ use crate::blocks::BlockSlice; use crate::context::require_ctx_view; -use crate::types::symbolic_vm_value_expr_from_sym; use crate::{ArchSpec, R2ILBlock, R2ILContext, parse_addr_name_map}; use r2types::SymbolicVmValueExpr; use serde::Serialize; @@ -257,7 +256,9 @@ pub(crate) struct CompiledSemanticInfo { pub(crate) summary_budget_exhausted: usize, pub(crate) summary_scc_count: usize, pub(crate) branch_fact_count: usize, + pub(crate) worker_island_count: usize, pub(crate) control_island_count: usize, + pub(crate) memory_island_count: usize, pub(crate) compiled_condition_count: usize, pub(crate) exact_compiled_condition_count: usize, pub(crate) actionable_compiled_condition_count: usize, @@ -397,7 +398,7 @@ fn render_semantic_confidence(confidence: r2sym::SemanticConfidence) -> String { } fn vm_state_update_info_from_sym(update: &r2sym::VmStateUpdate) -> VmStateUpdateInfo { - let value = symbolic_vm_value_expr_from_sym(&update.value); + let value = SymbolicVmValueExpr::from(&update.value); VmStateUpdateInfo { output: update.output.clone(), expr: update.expr.clone(), @@ -408,7 +409,7 @@ fn vm_state_update_info_from_sym(update: &r2sym::VmStateUpdate) -> VmStateUpdate } fn vm_guard_condition_info_from_sym(guard: &r2sym::VmGuardCondition) -> VmGuardConditionInfo { - let value = symbolic_vm_value_expr_from_sym(&guard.value); + let value = SymbolicVmValueExpr::from(&guard.value); VmGuardConditionInfo { expr: guard.expr.clone(), value: render_vm_value_expr(&value), @@ -794,14 +795,15 @@ pub(crate) fn compiled_semantic_info( .collect::>(); let control_facts = compiled .symbolic_facts - .control_islands + .worker_islands .iter() - .flat_map(|island| island.facts.iter()) + .flat_map(|island| island.control_facts.iter()) .collect::>(); CompiledSemanticInfo { mode: match compiled.mode { r2sym::SemanticMode::Raw => "raw".to_string(), r2sym::SemanticMode::Compiled => "compiled".to_string(), + r2sym::SemanticMode::IslandCompiled => "island_compiled".to_string(), r2sym::SemanticMode::Residual => "residual".to_string(), r2sym::SemanticMode::VmSummary => "vm_summary".to_string(), }, @@ -842,7 +844,9 @@ pub(crate) fn compiled_semantic_info( + compiled.derived_diagnostics.scc_budget_exhausted, summary_scc_count: compiled.derived_diagnostics.scc_count, branch_fact_count: compiled.symbolic_facts.branch_facts.len(), + worker_island_count: compiled.symbolic_facts.worker_islands.len(), control_island_count: compiled.symbolic_facts.control_islands.len(), + memory_island_count: compiled.symbolic_facts.memory_islands.len(), compiled_condition_count: branch_compiled_conditions.len(), exact_compiled_condition_count: control_facts .iter() @@ -1362,6 +1366,14 @@ pub extern "C" fn r2sym_explore_to( let z3_ctx = Context::thread_local(); let explore_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let compiled = r2sym::compile_semantic_artifact_with_scope( + &z3_ctx, + &prepared, + Some(&scope), + ctx_view.arch, + &symbol_map, + query_config.summary_profile, + ); let mut initial_state = r2sym::SymState::new(&z3_ctx, entry_addr); r2sym::seed_default_state_for_arch(&mut initial_state, &prepared, ctx_view.arch); let mut explorer = query_config.make_explorer(&z3_ctx); @@ -1373,7 +1385,12 @@ pub extern "C" fn r2sym_explore_to( &symbol_map, query_config.summary_profile, ); - let reach = explorer.can_reach(&prepared, initial_state, target_addr); + let reach = explorer.can_reach_with_artifact( + &prepared, + Some(&compiled), + initial_state, + target_addr, + ); let paths: Vec = reach .paths .iter() @@ -1428,6 +1445,14 @@ pub extern "C" fn r2sym_solve_to( let z3_ctx = Context::thread_local(); let solve_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let compiled = r2sym::compile_semantic_artifact_with_scope( + &z3_ctx, + &prepared, + Some(&scope), + ctx_view.arch, + &symbol_map, + query_config.summary_profile, + ); let mut initial_state = r2sym::SymState::new(&z3_ctx, entry_addr); r2sym::seed_default_state_for_arch(&mut initial_state, &prepared, ctx_view.arch); let mut explorer = query_config.make_explorer(&z3_ctx); @@ -1439,7 +1464,12 @@ pub extern "C" fn r2sym_solve_to( &symbol_map, query_config.summary_profile, ); - let solve = explorer.solve_for_target(&prepared, initial_state, target_addr); + let solve = explorer.solve_for_target_with_artifact( + &prepared, + Some(&compiled), + initial_state, + target_addr, + ); let selected = solve .selected_path_index .and_then(|idx| solve.matched_paths.get(idx).map(|path| (idx, path))) @@ -1760,6 +1790,14 @@ pub extern "C" fn r2sym_explore_to_scope( let z3_ctx = Context::thread_local(); let explore_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let compiled = r2sym::compile_semantic_artifact_with_scope( + &z3_ctx, + prepared, + Some(&scope), + ctx_view.arch, + &symbol_map, + query_config.summary_profile, + ); let mut initial_state = r2sym::SymState::new(&z3_ctx, entry_addr); r2sym::seed_default_state_for_arch(&mut initial_state, prepared, ctx_view.arch); let mut explorer = query_config.make_explorer(&z3_ctx); @@ -1771,7 +1809,8 @@ pub extern "C" fn r2sym_explore_to_scope( &symbol_map, query_config.summary_profile, ); - let reach = explorer.can_reach(prepared, initial_state, target_addr); + let reach = + explorer.can_reach_with_artifact(prepared, Some(&compiled), initial_state, target_addr); let paths: Vec = reach .paths .iter() @@ -1823,6 +1862,14 @@ pub extern "C" fn r2sym_solve_to_scope( let z3_ctx = Context::thread_local(); let solve_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let compiled = r2sym::compile_semantic_artifact_with_scope( + &z3_ctx, + prepared, + Some(&scope), + ctx_view.arch, + &symbol_map, + query_config.summary_profile, + ); let mut initial_state = r2sym::SymState::new(&z3_ctx, entry_addr); r2sym::seed_default_state_for_arch(&mut initial_state, prepared, ctx_view.arch); let mut explorer = query_config.make_explorer(&z3_ctx); @@ -1834,7 +1881,12 @@ pub extern "C" fn r2sym_solve_to_scope( &symbol_map, query_config.summary_profile, ); - let solve = explorer.solve_for_target(prepared, initial_state, target_addr); + let solve = explorer.solve_for_target_with_artifact( + prepared, + Some(&compiled), + initial_state, + target_addr, + ); let selected = solve .selected_path_index .and_then(|idx| solve.matched_paths.get(idx).map(|path| (idx, path))) @@ -1995,6 +2047,14 @@ pub extern "C" fn r2sym_explore_to_replay_scope( let z3_ctx = Context::thread_local(); let explore_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let compiled = r2sym::compile_semantic_artifact_with_scope( + &z3_ctx, + prepared, + Some(&scope), + ctx_view.arch, + &symbol_map, + query_config.summary_profile, + ); let initial_state = build_replay_seeded_state(&z3_ctx, entry_addr, prepared, ctx_view.arch, &replay_seed); let mut explorer = query_config.make_explorer(&z3_ctx); @@ -2006,7 +2066,8 @@ pub extern "C" fn r2sym_explore_to_replay_scope( &symbol_map, query_config.summary_profile, ); - let reach = explorer.can_reach(prepared, initial_state, target_addr); + let reach = + explorer.can_reach_with_artifact(prepared, Some(&compiled), initial_state, target_addr); let paths: Vec = reach .paths .iter() @@ -2066,6 +2127,14 @@ pub extern "C" fn r2sym_solve_to_replay_scope( let z3_ctx = Context::thread_local(); let solve_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let compiled = r2sym::compile_semantic_artifact_with_scope( + &z3_ctx, + prepared, + Some(&scope), + ctx_view.arch, + &symbol_map, + query_config.summary_profile, + ); let initial_state = build_replay_seeded_state(&z3_ctx, entry_addr, prepared, ctx_view.arch, &replay_seed); let mut explorer = query_config.make_explorer(&z3_ctx); @@ -2077,7 +2146,12 @@ pub extern "C" fn r2sym_solve_to_replay_scope( &symbol_map, query_config.summary_profile, ); - let solve = explorer.solve_for_target(prepared, initial_state, target_addr); + let solve = explorer.solve_for_target_with_artifact( + prepared, + Some(&compiled), + initial_state, + target_addr, + ); let selected = solve .selected_path_index .and_then(|idx| solve.matched_paths.get(idx).map(|path| (idx, path))) diff --git a/r2plugin/src/decompiler.rs b/r2plugin/src/decompiler.rs index 78c10cc..43c26ec 100644 --- a/r2plugin/src/decompiler.rs +++ b/r2plugin/src/decompiler.rs @@ -1,7 +1,7 @@ use crate::context::PluginCtxView; +use crate::parse_addr_name_map; use crate::types::FunctionAnalysisArtifact; -use crate::{decompile_artifact_guard_fallback, parse_addr_name_map}; -use r2il::{ArchSpec, R2ILBlock}; +use r2il::R2ILBlock; use std::collections::HashMap; pub(crate) struct DecompilerEnv { @@ -10,44 +10,8 @@ pub(crate) struct DecompilerEnv { pub(crate) cfg: r2dec::DecompilerConfig, } -pub(crate) fn normalize_sig_arch_name(arch: Option<&ArchSpec>) -> Option { - let arch = arch?; - let lower = arch.name.to_ascii_lowercase(); - if matches!(lower.as_str(), "x86-64" | "x86_64" | "x64" | "amd64") { - return Some("x86-64".to_string()); - } - if matches!(lower.as_str(), "x86" | "x86-32" | "i386" | "i686") { - return Some("x86".to_string()); - } - Some(arch.name.clone()) -} - -pub(crate) fn decompiler_config_for_arch_name( - arch_name: &str, - ptr_bits: u32, -) -> r2dec::DecompilerConfig { - match (arch_name, ptr_bits) { - ("x86", 32) | ("x86-32", _) => r2dec::DecompilerConfig::x86(), - ("x86-64", _) | ("x86_64", _) | ("x64", _) | ("amd64", _) => { - r2dec::DecompilerConfig::x86_64() - } - ("arm", _) | ("ARM", _) if ptr_bits == 32 => r2dec::DecompilerConfig::arm(), - ("aarch64", _) | ("arm64", _) | ("ARM64", _) => r2dec::DecompilerConfig::aarch64(), - ("riscv32", _) | ("rv32", _) | ("rv32gc", _) => r2dec::DecompilerConfig::riscv32(), - ("riscv64", _) | ("rv64", _) | ("rv64gc", _) => r2dec::DecompilerConfig::riscv64(), - ("riscv", _) if ptr_bits == 32 => r2dec::DecompilerConfig::riscv32(), - ("riscv", _) => r2dec::DecompilerConfig::riscv64(), - _ => r2dec::DecompilerConfig { - ptr_size: ptr_bits, - ..r2dec::DecompilerConfig::default() - }, - } -} - pub(crate) fn build_decompiler_env(ctx: &PluginCtxView<'_>) -> DecompilerEnv { - let arch_name = normalize_sig_arch_name(ctx.arch).unwrap_or_else(|| "unknown".to_string()); - let ptr_bits = ctx.arch.map(|arch| arch.addr_size * 8).unwrap_or(64); - let cfg = decompiler_config_for_arch_name(&arch_name, ptr_bits); + let (arch_name, ptr_bits, cfg) = r2dec::DecompilerConfig::for_arch(ctx.arch); DecompilerEnv { arch_name, ptr_bits, @@ -60,13 +24,15 @@ pub(crate) fn build_decompiler_context( function_names: HashMap, strings: HashMap, symbols: HashMap, + ptr_bits: u32, ) -> r2dec::DecompilerContext { - r2dec::DecompilerContext { + r2dec::DecompilerContext::from_analysis_inputs( + type_facts, function_names, strings, symbols, - type_facts, - } + ptr_bits, + ) } pub(crate) fn decompiler_input_from_artifact( @@ -74,6 +40,7 @@ pub(crate) fn decompiler_input_from_artifact( function_names: HashMap, strings: HashMap, symbols: HashMap, + ptr_bits: u32, ) -> r2dec::DecompilerInput { let FunctionAnalysisArtifact { ssa_func, @@ -83,7 +50,7 @@ pub(crate) fn decompiler_input_from_artifact( } = artifact; r2dec::DecompilerInput::new( ssa_func, - build_decompiler_context(type_facts, function_names, strings, symbols), + build_decompiler_context(type_facts, function_names, strings, symbols, ptr_bits), ) .with_interproc_summary_set(interproc_summary_set) } @@ -128,21 +95,11 @@ pub(crate) fn run_full_decompile_on_large_stack( ) -> String { const STACK_SIZE: usize = 512 * 1024 * 1024; - fn semantic_artifact_allows_full_decompile( - artifact: Option<&crate::types::FunctionAnalysisArtifact>, - ) -> bool { - artifact - .and_then(|artifact| artifact.semantic_artifact.as_ref()) - .is_some_and(|compiled| compiled.capability.decompile_ready) - } - let handle = std::thread::Builder::new() .stack_size(STACK_SIZE) .spawn(move || { - let cfg_guard_reason = crate::decompiler_cfg_guard_reason(&r2il_blocks); - let arch_name = - normalize_sig_arch_name(arch.as_ref()).unwrap_or_else(|| "unknown".to_string()); - let config = decompiler_config_for_arch_name(&arch_name, ptr_bits); + let cfg_guard_reason = r2dec::cfg_guard_reason(&r2il_blocks); + let (_, _, config) = r2dec::DecompilerConfig::for_arch(arch.as_ref()); let function_names = parse_addr_name_map(&func_names_str); let symbols = parse_addr_name_map(&symbols_str); let display_func_name = crate::helpers::resolve_decompiler_display_name( @@ -152,23 +109,18 @@ pub(crate) fn run_full_decompile_on_large_stack( &symbols, ); let mut artifact = if let Some(artifact) = cached_artifact { - let allow_semantic_decompile = - semantic_artifact_allows_full_decompile(Some(&artifact)); - if let Some(semantic_artifact) = artifact.semantic_artifact.as_ref() - && crate::should_decompile_from_semantic_fallback( - &func_name_str, - semantic_artifact, - ) + let allow_semantic_decompile = artifact.type_facts.symbolic_facts.decompile_ready(); + if let Some(comment) = r2dec::preferred_semantic_fallback_comment( + &display_func_name, + &artifact.type_facts.symbolic_facts, + ) { - return crate::decompile_semantic_artifact_fallback( - &display_func_name, - semantic_artifact, - ); + return comment; } if let Some(reason) = cfg_guard_reason.as_ref() && !allow_semantic_decompile { - return decompile_artifact_guard_fallback(&func_name_str, reason); + return r2dec::artifact_guard_fallback_comment(&func_name_str, reason); } artifact } else { @@ -183,24 +135,24 @@ pub(crate) fn run_full_decompile_on_large_stack( ) }, ); - let allow_semantic_decompile = precomputed_semantic_artifact + let precomputed_symbolic_facts = precomputed_semantic_artifact .as_ref() - .is_some_and(|compiled| compiled.capability.decompile_ready); - if let Some(semantic_artifact) = precomputed_semantic_artifact.as_ref() - && crate::should_decompile_from_semantic_fallback( - &func_name_str, - semantic_artifact, + .map(r2types::symbolic_semantic_facts_from_artifact); + let allow_semantic_decompile = precomputed_symbolic_facts + .as_ref() + .is_some_and(r2types::SymbolicSemanticFacts::decompile_ready); + if let Some(symbolic_facts) = precomputed_symbolic_facts.as_ref() + && let Some(comment) = r2dec::preferred_semantic_fallback_comment( + &display_func_name, + symbolic_facts, ) { - return crate::decompile_semantic_artifact_fallback( - &display_func_name, - semantic_artifact, - ); + return comment; } if let Some(reason) = cfg_guard_reason.as_ref() && !allow_semantic_decompile { - return decompile_artifact_guard_fallback(&func_name_str, reason); + return r2dec::artifact_guard_fallback_comment(&func_name_str, reason); } let Some(artifact) = crate::types::build_detached_function_analysis_artifact_with_scope_and_semantics( @@ -215,7 +167,7 @@ pub(crate) fn run_full_decompile_on_large_stack( precomputed_semantic_artifact, ) else { - return decompile_artifact_guard_fallback( + return r2dec::artifact_guard_fallback_comment( &func_name_str, "failed to build detached analysis artifact", ); @@ -223,27 +175,16 @@ pub(crate) fn run_full_decompile_on_large_stack( artifact }; artifact = rename_function_artifact_for_display(artifact, &display_func_name); - crate::types::enrich_known_function_signatures_from_names( - &mut artifact.type_facts, - &function_names, - ptr_bits, - ); - crate::types::enrich_known_function_signatures_from_names( - &mut artifact.type_facts, - &symbols, - ptr_bits, - ); let decompiler = r2dec::Decompiler::new(config); - let semantic_fallback_output = artifact - .semantic_artifact - .as_ref() - .map(|compiled| crate::decompile_semantic_artifact_fallback(&display_func_name, compiled)); + let semantic_fallback_output = + r2dec::semantic_fallback_comment(&display_func_name, &artifact.type_facts.symbolic_facts); let input = decompiler_input_from_artifact( artifact, function_names, parse_addr_name_map(&strings_str), symbols, + ptr_bits, ); let output = decompiler.decompile_input(&input); @@ -251,7 +192,7 @@ pub(crate) fn run_full_decompile_on_large_stack( return semantic_fallback_output.unwrap_or_else(|| { cfg_guard_reason .as_ref() - .map(|reason| decompile_artifact_guard_fallback(&func_name_str, reason)) + .map(|reason| r2dec::artifact_guard_fallback_comment(&func_name_str, reason)) .unwrap_or_else(|| { format!( "/* r2dec fallback: skipped decompilation for {} (empty output) */", diff --git a/r2plugin/src/helpers.rs b/r2plugin/src/helpers.rs index 7635d85..b186dd9 100644 --- a/r2plugin/src/helpers.rs +++ b/r2plugin/src/helpers.rs @@ -32,72 +32,6 @@ pub(crate) fn cstr_or_default(ptr: *const c_char, default: &str) -> String { unsafe { CStr::from_ptr(ptr).to_string_lossy().to_string() } } -pub(crate) fn normalize_sim_name(name: &str) -> Option<&'static str> { - let normalized_owned = name.trim().to_ascii_lowercase(); - let mut normalized = normalized_owned.as_str(); - - for prefix in ["sym.imp.", "sym.", "imp.", "reloc.", "dbg."] { - while let Some(rest) = normalized.strip_prefix(prefix) { - normalized = rest; - } - } - - while let Some(rest) = normalized.strip_suffix("@plt") { - normalized = rest; - } - while let Some(rest) = normalized.strip_suffix(".plt") { - normalized = rest; - } - if let Some((base, _)) = normalized.split_once('@') { - normalized = base; - } - - if let Some(rest) = normalized.strip_prefix("__isoc99_") { - normalized = rest; - } - if let Some(rest) = normalized.strip_prefix("__gi_") { - normalized = rest; - } - - match normalized { - "strlen" | "__strlen_chk" => Some("strlen"), - "strcmp" => Some("strcmp"), - "memcmp" => Some("memcmp"), - "memcpy" | "__memcpy_chk" => Some("memcpy"), - "memset" => Some("memset"), - "malloc" | "__libc_malloc" | "__gi___libc_malloc" => Some("malloc"), - "free" => Some("free"), - "puts" => Some("puts"), - "printf" | "__printf_chk" => Some("printf"), - "exit" | "_exit" => Some("exit"), - _ => { - if normalized.starts_with("strlen") { - Some("strlen") - } else if normalized.starts_with("strcmp") { - Some("strcmp") - } else if normalized.starts_with("memcmp") { - Some("memcmp") - } else if normalized.starts_with("memcpy") { - Some("memcpy") - } else if normalized.starts_with("memset") { - Some("memset") - } else if normalized.starts_with("printf") || normalized == "__printf_chk" { - Some("printf") - } else if normalized.starts_with("puts") { - Some("puts") - } else if normalized == "malloc" || normalized.ends_with("malloc") { - Some("malloc") - } else if normalized == "free" || normalized.ends_with("free") { - Some("free") - } else if normalized.starts_with("exit") { - Some("exit") - } else { - None - } - } - } -} - fn strip_display_name_prefixes(name: &str) -> &str { let mut normalized = name.trim(); for prefix in ["sym.imp.", "sym.", "imp.", "reloc.", "dbg.", "fcn."] { diff --git a/r2plugin/src/lib.rs b/r2plugin/src/lib.rs index 9274600..5937fa3 100644 --- a/r2plugin/src/lib.rs +++ b/r2plugin/src/lib.rs @@ -17,6 +17,8 @@ mod decompiler; mod helpers; mod types; +#[cfg(test)] +use analysis::ssa::{r2il_block_defuse_json, r2il_block_to_ssa_json}; use r2il::serialize::UserOpDef; use r2il::{ArchSpec, R2ILBlock, R2ILOp, Varnode, serialize, validate_block_full}; use r2sleigh_export::{ @@ -28,12 +30,10 @@ use std::os::raw::c_char; use std::path::Path; use std::ptr; use std::slice; -use types::{recover_vars_arch_profile, size_to_type, ssa_var_block_key}; - -#[cfg(test)] -use analysis::ssa::{r2il_block_defuse_json, r2il_block_to_ssa_json}; #[cfg(test)] use types::parse_const_value; +#[cfg(test)] +use types::{recover_vars_arch_profile, size_to_type, ssa_var_block_key}; /// Opaque context handle for C API. pub struct R2ILContext { @@ -2462,253 +2462,54 @@ fn decompiler_max_blocks() -> usize { .unwrap_or(200) } -fn decompiler_cfg_guard_reason_from_summary(summary: &r2ssa::CFGRiskSummary) -> Option { - if summary.loop_count > 8 || summary.back_edge_count > 16 { - return Some(format!( - "complex loop graph (loops={}, back_edges={})", - summary.loop_count, summary.back_edge_count - )); - } - - if summary.loop_count > 4 && summary.block_count >= 96 && summary.max_switch_cases >= 32 { - return Some(format!( - "large dense switch in looped CFG (blocks={}, loops={}, max_switch_cases={})", - summary.block_count, summary.loop_count, summary.max_switch_cases - )); - } - - None -} - -fn decompiler_cfg_guard_reason(blocks: &[R2ILBlock]) -> Option { - let ssa_func = r2ssa::SSAFunction::from_blocks_raw_no_arch(blocks)?; - decompiler_cfg_guard_reason_from_summary(&ssa_func.cfg_risk_summary()) -} - -fn decompile_block_guard_fallback(func_name: &str, blocks: usize, max_blocks: usize) -> String { - format!( - "/* r2dec fallback: skipped decompilation for {} ({} blocks > limit {}). Set SLEIGH_DEC_MAX_BLOCKS to override. */", - func_name, blocks, max_blocks - ) -} - -fn decompile_artifact_guard_fallback(func_name: &str, reason: &str) -> String { - format!( - "/* r2dec fallback: skipped decompilation for {} ({}) */", - func_name, reason - ) -} - -fn decompile_semantic_artifact_fallback( - func_name: &str, - compiled: &r2sym::CompiledSemanticArtifact, -) -> String { - if matches!(compiled.mode, r2sym::SemanticMode::VmSummary) - && let Some(vm_step) = compiled.vm_step.as_ref() - { - let kind = match vm_step.kind { - r2sym::InterpreterKind::SwitchDispatch => "switch_dispatch", - r2sym::InterpreterKind::IndirectDispatch => "indirect_dispatch", - }; - let selector = vm_step.selector.as_deref().unwrap_or("unknown"); - let inputs = if vm_step.state_inputs.is_empty() { - "none".to_string() - } else { - vm_step.state_inputs.join(", ") - }; - let outputs = if vm_step.state_outputs.is_empty() { - "none".to_string() - } else { - vm_step.state_outputs.join(", ") - }; - let exact_transfers = vm_step - .transfers - .iter() - .filter(|transfer| transfer.exact) - .count(); - let likely_transfers = vm_step - .transfers - .iter() - .filter(|transfer| matches!(transfer.confidence(), r2sym::SemanticConfidence::Likely)) - .count(); - let heuristic_transfers = vm_step - .transfers - .iter() - .filter(|transfer| { - matches!(transfer.confidence(), r2sym::SemanticConfidence::Heuristic) - }) - .count(); - let redispatch_transfers = vm_step - .transfers - .iter() - .filter(|transfer| transfer.redispatch) - .count(); - let returning_transfers = vm_step - .transfers - .iter() - .filter(|transfer| transfer.may_return) - .count(); - let selector_updates = vm_step - .transfers - .iter() - .filter(|transfer| transfer.selector_update.is_some()) - .count(); - let exit_guards = vm_step - .transfers - .iter() - .map(|transfer| transfer.exit_guards.len()) - .sum::(); - let residual_guards = vm_step - .transfers - .iter() - .filter(|transfer| transfer.residual_guards) - .count(); - let residual_memory = vm_step - .transfers - .iter() - .filter(|transfer| transfer.residual_memory_effects) - .count(); - let read_effects = vm_step - .handler_memory_read_effects - .values() - .map(Vec::len) - .sum::(); - let write_effects = vm_step - .handler_memory_write_effects - .values() - .map(Vec::len) - .sum::(); - let total_reads: usize = vm_step.handler_memory_reads.values().copied().sum(); - let total_writes: usize = vm_step.handler_memory_writes.values().copied().sum(); - let handler_preview = vm_step - .dispatch_targets - .iter() - .take(3) - .map(|target| { - let values = vm_step - .case_values_by_target - .get(target) - .map(|values| { - values - .iter() - .map(|value| format!("0x{value:x}")) - .collect::>() - .join("|") - }) - .unwrap_or_else(|| "default".to_string()); - let updates = vm_step - .handler_state_updates - .get(target) - .map(|updates| { - updates - .iter() - .take(3) - .map(|update| format!("{}={}", update.output, update.expr)) - .collect::>() - .join(", ") - }) - .filter(|text| !text.is_empty()) - .unwrap_or_else(|| "no_state_updates".to_string()); - format!("0x{target:x}[{values}] => {updates}") - }) - .collect::>() - .join("; "); - return format!( - "/* r2dec semantic summary: vm_summary for {} ({kind} @ 0x{:x}, loop_header=0x{:x}, selector={}, targets={}, redispatch={}, exact_transfers={}, likely_transfers={}, heuristic_transfers={}, redispatch_transfers={}, returning_transfers={}, selector_updates={}, exact_exit_guards={}, residual_guards={}, residual_memory={}, total_reads={}, total_writes={}, read_effects={}, write_effects={}, state_inputs=[{}], state_outputs=[{}], handlers={}) */", - func_name, - vm_step.dispatch_header, - vm_step.loop_header, - selector, - vm_step.dispatch_targets.len(), - vm_step.redispatch_handlers.len(), - exact_transfers, - likely_transfers, - heuristic_transfers, - redispatch_transfers, - returning_transfers, - selector_updates, - exit_guards, - residual_guards, - residual_memory, - total_reads, - total_writes, - read_effects, - write_effects, - inputs, - outputs, - handler_preview, - ); - } - - let info = analysis::sym::compiled_semantic_info(compiled); - let mut reason = format!( - "semantic fallback: {} slice in {} mode", - info.slice_class, info.mode - ); - if !info.residual_reasons.is_empty() { - reason.push_str(" ("); - reason.push_str(&info.residual_reasons.join(", ")); - reason.push(')'); - } - if info.branch_fact_count > 0 { - reason.push_str(&format!( - "; branch_facts={}, control_islands={}, actionable_conditions={}, exact_conditions={}", - info.branch_fact_count, - info.control_island_count, - info.actionable_compiled_condition_count, - info.exact_compiled_condition_count, - )); - } - let actionable_preview = compiled - .symbolic_facts - .control_islands - .iter() - .filter_map(|island| { - island - .actionable_compiled_condition() - .map(|condition| format!("0x{:x}: {}", island.anchor_block, condition.simplified)) - }) - .take(3) - .collect::>(); - if !actionable_preview.is_empty() { - reason.push_str("; actionable_preview=["); - reason.push_str(&actionable_preview.join(" | ")); - reason.push(']'); - } - decompile_artifact_guard_fallback(func_name, &reason) -} - -fn is_autogenerated_function_name(name: &str) -> bool { - name.is_empty() - || name.starts_with("fcn.") - || name.starts_with("fcn_") - || name.starts_with("sub.") - || name.starts_with("sub_") - || name.starts_with("loc.") +#[unsafe(no_mangle)] +pub extern "C" fn r2dec_block_guard_comment_ffi( + func_name: *const c_char, + blocks: usize, + max_blocks: usize, +) -> *mut c_char { + let func_name = if func_name.is_null() { + "unknown".to_string() + } else { + unsafe { CStr::from_ptr(func_name) } + .to_str() + .unwrap_or("unknown") + .to_string() + }; + CString::new(r2dec::block_guard_fallback_comment( + &func_name, blocks, max_blocks, + )) + .map_or(ptr::null_mut(), |c| c.into_raw()) } -fn should_decompile_from_semantic_fallback( - function_name: &str, - compiled: &r2sym::CompiledSemanticArtifact, -) -> bool { - if matches!(compiled.mode, r2sym::SemanticMode::VmSummary) { - return true; - } - if !is_autogenerated_function_name(function_name) { - return false; - } - if compiled.symbolic_facts.diagnostics.skipped_large_cfg { - return true; - } - if compiled.capability.decompile_ready { - return false; - } - compiled.residual_reasons.iter().any(|reason| { - matches!( - reason, - r2sym::ResidualReason::InterpreterRequiresStepSummary - ) - }) +#[unsafe(no_mangle)] +pub extern "C" fn r2dec_cfg_guard_comment_ffi( + func_name: *const c_char, + block_count: usize, + loop_count: usize, + back_edge_count: usize, + max_switch_cases: usize, +) -> *mut c_char { + let func_name = if func_name.is_null() { + "unknown".to_string() + } else { + unsafe { CStr::from_ptr(func_name) } + .to_str() + .unwrap_or("unknown") + .to_string() + }; + let summary = r2ssa::CFGRiskSummary { + block_count, + loop_count, + back_edge_count, + switch_block_count: usize::from(max_switch_cases > 0), + max_switch_cases, + }; + let Some(reason) = r2dec::cfg_guard_reason_from_summary(&summary) else { + return ptr::null_mut(); + }; + CString::new(r2dec::artifact_guard_fallback_comment(&func_name, &reason)) + .map_or(ptr::null_mut(), |c| c.into_raw()) } #[cfg(test)] @@ -2863,6 +2664,7 @@ fn parse_addr_name_map(json_str: &str) -> std::collections::HashMap .unwrap_or_default() } +#[cfg(test)] fn sanitize_c_identifier(name: &str) -> Option { let trimmed = name.trim(); if trimmed.is_empty() { @@ -2889,6 +2691,7 @@ fn sanitize_c_identifier(name: &str) -> Option { } } +#[cfg(test)] fn uniquify_name(base: String, used: &mut std::collections::HashSet) -> String { if used.insert(base.clone()) { return base; @@ -2922,6 +2725,7 @@ fn is_low_quality_stack_name(name: &str) -> bool { || is_generic_arg_name(&lower) } +#[cfg(test)] fn parse_external_type(raw_ty: &str, ptr_bits: u32) -> Option { let normalized = normalize_external_type_name(raw_ty); let parsed = r2types::parse_type_like_spec(&normalized, ptr_bits)?; @@ -3244,7 +3048,8 @@ fn r2dec_function_with_context_impl(inputs: R2DecFunctionWithContextInputs) -> * let ptr_bits = ctx_view.arch.map(helpers::effective_ptr_bits).unwrap_or(64); let max_blocks = decompiler_max_blocks(); if block_slice.len() > max_blocks { - let output = decompile_block_guard_fallback(&func_name_str, block_slice.len(), max_blocks); + let output = + r2dec::block_guard_fallback_comment(&func_name_str, block_slice.len(), max_blocks); return CString::new(output).map_or(ptr::null_mut(), |c| c.into_raw()); } @@ -3425,6 +3230,7 @@ pub extern "C" fn r2dec_block_ast_json( // ============================================================================ #[derive(Debug, Clone)] +#[cfg(test)] pub(crate) struct InferredParam { name: String, ty: r2dec::CType, @@ -3433,39 +3239,8 @@ pub(crate) struct InferredParam { evidence: TypeEvidence, } -#[derive(Debug, Clone, Default, PartialEq, Eq)] -struct TypeEvidence { - pointer_proven: u8, - pointer_likely: u8, - scalar_proven: u8, - scalar_likely: u8, - bool_like: u8, - width_bits: u32, -} - -impl TypeEvidence { - fn pointer_score(&self) -> u16 { - (self.pointer_proven as u16) * 4 + (self.pointer_likely as u16) * 2 - } - - fn scalar_score(&self) -> u16 { - (self.scalar_proven as u16) * 4 - + (self.scalar_likely as u16) * 2 - + (self.bool_like as u16) * 3 - } - - fn has_pointer_signal(&self) -> bool { - self.pointer_proven > 0 || self.pointer_likely > 0 - } - - fn has_scalar_signal(&self) -> bool { - self.scalar_proven > 0 || self.scalar_likely > 0 || self.bool_like > 0 - } - - fn has_conflict(&self) -> bool { - self.has_pointer_signal() && self.has_scalar_signal() - } -} +#[cfg(test)] +type TypeEvidence = r2types::SignatureTypeEvidence; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] struct InferredParamJson { @@ -3573,7 +3348,11 @@ struct SymbolicBranchJson { struct SymbolicFactsJson { branches: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] + worker_islands: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] control_islands: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + memory_islands: Vec, diagnostics: r2types::SymbolicFactDiagnostics, #[serde(skip_serializing_if = "Option::is_none")] interpreter: Option, @@ -3637,7 +3416,9 @@ fn symbolic_facts_json(facts: &r2types::SymbolicSemanticFacts) -> Option Symboli false_compiled: fact.false_compiled.clone(), }) .collect(), + worker_islands: facts.worker_islands.clone(), control_islands: facts.control_islands.clone(), + memory_islands: facts.memory_islands.clone(), diagnostics: facts.diagnostics.clone(), interpreter: facts.interpreter.clone(), vm_step: facts.vm_step.clone(), @@ -3808,47 +3591,59 @@ fn type_writeback_payload_from_artifact( fn semantic_type_fallback_payload( function_name: &str, arch_name: &str, + ptr_bits: u32, interproc: InterprocInferenceInput<'_>, compiled: &r2sym::CompiledSemanticArtifact, ) -> InferredTypeWritebackJson { let compiled_info = analysis::sym::compiled_semantic_info(compiled); - let mut warnings = Vec::new(); - let mut warning = format!( - "semantic fallback: {} slice in {} mode", - compiled_info.slice_class, compiled_info.mode + let symbolic_facts = r2types::symbolic_semantic_facts_from_artifact(compiled); + let plan = r2types::build_semantic_type_fallback_plan( + function_name, + arch_name, + ptr_bits, + &symbolic_facts, ); - if !compiled_info.residual_reasons.is_empty() { - warning.push_str(" ("); - warning.push_str(&compiled_info.residual_reasons.join(", ")); - warning.push(')'); - } - if compiled_info.branch_fact_count > 0 { - warning.push_str(&format!( - "; branch_facts={}, control_islands={}, actionable_conditions={}, exact_conditions={}", - compiled_info.branch_fact_count, - compiled_info.control_island_count, - compiled_info.actionable_compiled_condition_count, - compiled_info.exact_compiled_condition_count, - )); - } - warnings.push(warning); - if !compiled_info.capability.type_ready { - warnings.push("type analysis not ready from semantic capability".to_string()); - } InferredTypeWritebackJson { - function_name: function_name.to_string(), - signature: format!("void {}(void)", function_name), - ret_type: "void".to_string(), - params: Vec::new(), - callconv: "unknown".to_string(), - arch: arch_name.to_string(), - confidence: 0, - callconv_confidence: 0, + function_name: plan.signature.function_name, + signature: plan.signature.signature, + ret_type: plan.signature.ret_type, + params: plan + .signature + .params + .into_iter() + .map(|param| InferredParamJson { + name: param.name, + param_type: param.param_type, + }) + .collect(), + callconv: plan.signature.callconv, + arch: plan.signature.arch, + confidence: plan.signature.confidence, + callconv_confidence: plan.signature.callconv_confidence, var_type_candidates: Vec::new(), var_rename_candidates: Vec::new(), - struct_decls: Vec::new(), - global_type_links: Vec::new(), + struct_decls: plan + .struct_decls + .into_iter() + .map(|decl| StructDeclCandidateJson { + name: decl.name, + decl: decl.decl, + confidence: decl.confidence, + source: decl.source.as_str().to_string(), + fields: struct_fields_json(&decl.fields), + }) + .collect(), + global_type_links: plan + .global_type_links + .into_iter() + .map(|candidate| GlobalTypeLinkCandidateJson { + addr: candidate.addr, + target_type: candidate.target_type, + confidence: candidate.confidence, + source: candidate.source.as_str().to_string(), + }) + .collect(), interproc: InterprocSummaryJson { callsite_count: 0, iterations: interproc.iter.max(1), @@ -3862,12 +3657,10 @@ fn semantic_type_fallback_payload( !v.is_null() && v.as_object().map(|obj| !obj.is_empty()).unwrap_or(true) }), }, - symbolic: Some(symbolic_facts_json_forced( - &types::symbolic_semantic_facts_from_sym(compiled), - )), + symbolic: Some(symbolic_facts_json_forced(&symbolic_facts)), compiled_semantics: Some(compiled_info), diagnostics: TypeWritebackDiagnosticsJson { - warnings, + warnings: plan.diagnostics.warnings, ..TypeWritebackDiagnosticsJson::default() }, } @@ -3878,297 +3671,48 @@ const SIG_WRITEBACK_CONFIDENCE_MIN: u8 = 70; #[cfg(test)] const CC_WRITEBACK_CONFIDENCE_MIN: u8 = 80; -#[derive(Debug, Default)] -struct SignatureTypeEvidenceContext { - pointer_vars: std::collections::HashSet, - scalar_proven_vars: std::collections::HashSet, - scalar_likely_vars: std::collections::HashSet, - bool_like_vars: std::collections::HashSet, - width_bits: std::collections::HashMap, -} - +#[cfg(test)] fn merge_initial_type_evidence(initial_ty: &r2dec::CType, evidence: &mut TypeEvidence) { - match initial_ty { - r2dec::CType::Pointer(_) => evidence.pointer_likely = evidence.pointer_likely.max(1), - r2dec::CType::Bool => evidence.bool_like = evidence.bool_like.max(1), - r2dec::CType::Int(bits) | r2dec::CType::UInt(bits) => { - evidence.scalar_likely = evidence.scalar_likely.max(1); - if !(evidence.has_scalar_signal() - && !evidence.has_pointer_signal() - && evidence.width_bits > 0 - && evidence.width_bits < *bits) - { - evidence.width_bits = evidence.width_bits.max(*bits); - } - } - r2dec::CType::Float(bits) => { - evidence.scalar_proven = evidence.scalar_proven.max(1); - evidence.width_bits = evidence.width_bits.max(*bits); - } - _ => {} - } -} - -fn fallback_scalar_type( - var_size_bytes: u32, - evidence: &TypeEvidence, - ptr_bits: u32, -) -> r2dec::CType { - if evidence.bool_like > 0 - && evidence.pointer_score() == 0 - && evidence.scalar_proven == 0 - && evidence.scalar_likely <= 1 - { - return r2dec::CType::Bool; - } - - let carrier_bits = var_size_bytes.saturating_mul(8); - let width_bits = if evidence.has_scalar_signal() - && !evidence.has_pointer_signal() - && evidence.width_bits > 0 - { - evidence.width_bits - } else { - evidence.width_bits.max(carrier_bits) - }; - let width_bits = match width_bits { - 0 => { - if ptr_bits >= 64 { - 64 - } else { - 32 - } - } - 1 => 8, - 2..=8 => 8, - 9..=16 => 16, - 17..=32 => 32, - _ => 64, - }; - - r2dec::CType::Int(width_bits) + r2types::merge_initial_signature_type_evidence(&ctype_to_type_like(initial_ty), evidence); } +#[cfg(test)] fn materialize_signature_ctype(ty: r2dec::CType, ptr_bits: u32) -> r2dec::CType { - match ty { - r2dec::CType::Pointer(inner) => { - if matches!(*inner, r2dec::CType::Unknown | r2dec::CType::Void) - || matches!( - inner.as_ref(), - r2dec::CType::Struct(name) - | r2dec::CType::Union(name) - | r2dec::CType::Enum(name) - if is_unmaterialized_aggregate_name(name) - ) - { - return r2dec::CType::void_ptr(); - } - let inner = materialize_signature_ctype(*inner, ptr_bits); - r2dec::CType::ptr(inner) - } - r2dec::CType::Array(inner, len) => { - if matches!(*inner, r2dec::CType::Unknown | r2dec::CType::Void) { - return r2dec::CType::Array(Box::new(r2dec::CType::u8()), len); - } - let inner = materialize_signature_ctype(*inner, ptr_bits); - r2dec::CType::Array(Box::new(inner), len) - } - r2dec::CType::Function { ret, params } => { - let ret = materialize_signature_ctype(*ret, ptr_bits); - let ret = if matches!(ret, r2dec::CType::Unknown) { - fallback_scalar_type((ptr_bits / 8).max(1), &TypeEvidence::default(), ptr_bits) - } else { - ret - }; - let params = params - .into_iter() - .map(|param| materialize_signature_ctype(param, ptr_bits)) - .collect(); - r2dec::CType::Function { - ret: Box::new(ret), - params, - } - } - r2dec::CType::Unknown => { - fallback_scalar_type((ptr_bits / 8).max(1), &TypeEvidence::default(), ptr_bits) - } - r2dec::CType::Struct(name) if is_unmaterialized_aggregate_name(&name) => { - fallback_scalar_type((ptr_bits / 8).max(1), &TypeEvidence::default(), ptr_bits) - } - r2dec::CType::Union(name) if is_unmaterialized_aggregate_name(&name) => { - fallback_scalar_type((ptr_bits / 8).max(1), &TypeEvidence::default(), ptr_bits) - } - r2dec::CType::Enum(name) if is_unmaterialized_aggregate_name(&name) => { - fallback_scalar_type((ptr_bits / 8).max(1), &TypeEvidence::default(), ptr_bits) - } - other => other, - } + type_like_to_ctype(&r2types::materialize_signature_type_like( + ctype_to_type_like(&ty), + ptr_bits, + )) } +#[cfg(test)] fn resolve_evidence_driven_type( initial_ty: r2dec::CType, var_size_bytes: u32, ptr_bits: u32, evidence: &TypeEvidence, ) -> r2dec::CType { - if matches!(initial_ty, r2dec::CType::Float(_)) { - return initial_ty; - } - - let pointer_score = evidence.pointer_score(); - let scalar_score = evidence.scalar_score(); - let initial_is_pointer = matches!(initial_ty, r2dec::CType::Pointer(_)); - let initial_is_scalar = matches!( - initial_ty, - r2dec::CType::Bool | r2dec::CType::Int(_) | r2dec::CType::UInt(_) - ); - let preferred_scalar = fallback_scalar_type(var_size_bytes, evidence, ptr_bits); - let scalar_width_narrows = match (&initial_ty, &preferred_scalar) { - (r2dec::CType::Bool, r2dec::CType::Bool) => false, - (r2dec::CType::Int(initial_bits), r2dec::CType::Int(preferred_bits)) - | (r2dec::CType::Int(initial_bits), r2dec::CType::UInt(preferred_bits)) - | (r2dec::CType::UInt(initial_bits), r2dec::CType::Int(preferred_bits)) - | (r2dec::CType::UInt(initial_bits), r2dec::CType::UInt(preferred_bits)) => { - preferred_bits < initial_bits - } - (r2dec::CType::Int(_), r2dec::CType::Bool) - | (r2dec::CType::UInt(_), r2dec::CType::Bool) => true, - _ => false, - }; - - if initial_is_pointer && pointer_score.saturating_add(1) >= scalar_score { - return initial_ty; - } - if initial_is_scalar && scalar_score.saturating_add(1) >= pointer_score { - if scalar_width_narrows - && evidence.has_scalar_signal() - && !evidence.has_pointer_signal() - && !evidence.has_conflict() - { - return preferred_scalar; - } - return initial_ty; - } - - match initial_ty { - r2dec::CType::Struct(_) - | r2dec::CType::Union(_) - | r2dec::CType::Enum(_) - | r2dec::CType::Typedef(_) => { - if pointer_score > scalar_score.saturating_add(1) { - return r2dec::CType::void_ptr(); - } - if scalar_score > pointer_score.saturating_add(2) { - return fallback_scalar_type(var_size_bytes, evidence, ptr_bits); - } - return initial_ty; - } - _ => {} - } - - if pointer_score > scalar_score.saturating_add(1) { - return r2dec::CType::void_ptr(); - } - if scalar_score > pointer_score - || matches!(initial_ty, r2dec::CType::Void | r2dec::CType::Unknown) - { - return preferred_scalar; - } - - sanitize_inferred_param_type(initial_ty, var_size_bytes, ptr_bits) + type_like_to_ctype(&r2types::resolve_evidence_driven_signature_type( + ctype_to_type_like(&initial_ty), + var_size_bytes, + ptr_bits, + evidence, + )) } +#[cfg(test)] fn collect_type_evidence_for_var( - evidence_ctx: &SignatureTypeEvidenceContext, + evidence_ctx: &r2types::SignatureTypeEvidenceContext, var: &r2ssa::SSAVar, initial_ty: &r2dec::CType, ) -> TypeEvidence { - let key = types::ssa_var_key(var); - let family = types::scalar_register_family_key(&var.name); - let mut evidence = TypeEvidence::default(); - if evidence_ctx.pointer_vars.contains(&key) { - evidence.pointer_proven = 1; - } - if evidence_ctx.scalar_proven_vars.contains(&key) { - evidence.scalar_proven = 1; - } - if evidence_ctx.scalar_likely_vars.contains(&key) { - evidence.scalar_likely = 1; - } - if evidence_ctx.bool_like_vars.contains(&key) { - evidence.bool_like = 1; - } - if let Some(bits) = evidence_ctx.width_bits.get(&key) { - evidence.width_bits = *bits; - } - if evidence.pointer_proven == 0 - && signal_present_for_register_family(&evidence_ctx.pointer_vars, &family, var.version) - { - evidence.pointer_proven = 1; - } - if evidence.scalar_proven == 0 - && signal_present_for_register_family( - &evidence_ctx.scalar_proven_vars, - &family, - var.version, - ) - { - evidence.scalar_proven = 1; - } - if evidence.scalar_likely == 0 - && signal_present_for_register_family( - &evidence_ctx.scalar_likely_vars, - &family, - var.version, - ) - { - evidence.scalar_likely = 1; - } - if evidence.bool_like == 0 - && signal_present_for_register_family(&evidence_ctx.bool_like_vars, &family, var.version) - { - evidence.bool_like = 1; - } - if evidence.width_bits == 0 - && let Some(bits) = - width_hint_for_register_family(&evidence_ctx.width_bits, &family, var.version) - { - evidence.width_bits = bits; - } - merge_initial_type_evidence(initial_ty, &mut evidence); - evidence -} - -fn signal_present_for_register_family( - keys: &std::collections::HashSet, - family: &str, - version: u32, -) -> bool { - keys.iter() - .any(|key| key_matches_register_family_version(key, family, version)) -} - -fn width_hint_for_register_family( - hints: &std::collections::HashMap, - family: &str, - version: u32, -) -> Option { - hints - .iter() - .filter(|(key, _)| key_matches_register_family_version(key, family, version)) - .map(|(_, bits)| *bits) - .filter(|bits| *bits > 0) - .min() -} - -fn key_matches_register_family_version(key: &str, family: &str, version: u32) -> bool { - let Some((name, version_str)) = key.rsplit_once('_') else { - return false; - }; - version_str.parse::().ok() == Some(version) - && types::scalar_register_family_key(name) == family + r2types::collect_signature_type_evidence_for_var( + evidence_ctx, + var, + &ctype_to_type_like(initial_ty), + ) } +#[cfg(test)] fn type_like_to_ctype(ty: &r2types::CTypeLike) -> r2dec::CType { match ty { r2types::CTypeLike::Void => r2dec::CType::Void, @@ -4191,7 +3735,6 @@ fn type_like_to_ctype(ty: &r2types::CTypeLike) -> r2dec::CType { } } -#[cfg(test)] fn ctype_to_type_like(ty: &r2dec::CType) -> r2types::CTypeLike { match ty { r2dec::CType::Void => r2types::CTypeLike::Void, @@ -4220,248 +3763,95 @@ fn ctype_to_type_like(ty: &r2dec::CType) -> r2types::CTypeLike { } } -fn infer_signature_return_type( - func: &r2ssa::SSAFunction, - type_inference: &r2types::TypeInference, +#[cfg(test)] +fn fallback_scalar_type( + var_size_bytes: u32, + evidence: &TypeEvidence, ptr_bits: u32, - evidence_ctx: &SignatureTypeEvidenceContext, -) -> (r2dec::CType, TypeEvidence) { - let mut candidates = Vec::new(); - let mut candidate_evidence = Vec::new(); - - for block in func.blocks() { - for op in &block.ops { - let r2ssa::SSAOp::Return { target } = op else { - continue; - }; - - let target_name = target.name.to_ascii_lowercase(); - if target_name.starts_with("xmm0") || target_name.starts_with("st0") { - let bits = if target.size.saturating_mul(8) <= 32 { - 32 - } else { - 64 - }; - let ty = r2dec::CType::Float(bits); - let mut evidence = TypeEvidence::default(); - merge_initial_type_evidence(&ty, &mut evidence); - evidence.width_bits = bits; - candidates.push(ty); - candidate_evidence.push(evidence); - continue; - } - - let initial_ty = type_like_to_ctype(&type_inference.get_type(target)); - let evidence = collect_type_evidence_for_var(evidence_ctx, target, &initial_ty); - let ty = resolve_evidence_driven_type(initial_ty, target.size, ptr_bits, &evidence); - candidates.push(ty); - candidate_evidence.push(evidence); - } - } - - if candidates.is_empty() { - return (r2dec::CType::Void, TypeEvidence::default()); - } - - let mut meaningful: Vec = candidates - .iter() - .filter(|ty| !matches!(ty, r2dec::CType::Unknown)) - .cloned() - .collect(); - if meaningful.is_empty() { - let fallback_evidence = candidate_evidence.into_iter().next().unwrap_or_default(); - return ( - fallback_scalar_type((ptr_bits / 8).max(1), &fallback_evidence, ptr_bits), - fallback_evidence, - ); - } - if meaningful.iter().all(|ty| ty == &meaningful[0]) { - return ( - meaningful.remove(0), - candidate_evidence.into_iter().next().unwrap_or_default(), - ); - } - if let Some(float_ty) = meaningful - .iter() - .find(|ty| matches!(ty, r2dec::CType::Float(_))) - .cloned() - { - let evidence = candidate_evidence - .into_iter() - .find(|e| e.width_bits >= 32) - .unwrap_or_default(); - return (float_ty, evidence); - } - let evidence = candidate_evidence.into_iter().next().unwrap_or_default(); - (meaningful.remove(0), evidence) -} - -fn canonical_x86_64_arg_reg(name: &str) -> Option<&'static str> { - match name.to_ascii_lowercase().as_str() { - "rdi" | "edi" | "di" | "dil" => Some("rdi"), - "rsi" | "esi" | "si" | "sil" => Some("rsi"), - "rdx" | "edx" | "dx" | "dl" | "dh" => Some("rdx"), - "rcx" | "ecx" | "cx" | "cl" | "ch" => Some("rcx"), - "r8" | "r8d" | "r8w" | "r8b" => Some("r8"), - "r9" | "r9d" | "r9w" | "r9b" => Some("r9"), - _ => None, - } +) -> r2dec::CType { + type_like_to_ctype(&r2types::resolve_evidence_driven_signature_type( + r2types::CTypeLike::Unknown, + var_size_bytes, + ptr_bits, + evidence, + )) } -fn collect_version0_input_regs( - func: &r2ssa::SSAFunction, -) -> std::collections::HashMap { - let mut counts = std::collections::HashMap::new(); - for block in func.blocks() { - for op in &block.ops { - for src in op.sources() { - if src.version != 0 { - continue; - } - if src.name.starts_with("tmp:") || src.name.starts_with("const:") { - continue; - } - let key = src.name.to_ascii_lowercase(); - *counts.entry(key).or_insert(0) += 1; - } - } - } - counts +#[cfg(test)] +fn sanitize_inferred_param_type( + ty: r2dec::CType, + var_size_bytes: u32, + ptr_bits: u32, +) -> r2dec::CType { + type_like_to_ctype(&r2types::resolve_evidence_driven_signature_type( + ctype_to_type_like(&ty), + var_size_bytes, + ptr_bits, + &TypeEvidence::default(), + )) } +#[cfg(test)] fn infer_callconv_x86_64_from_counts( counts: &std::collections::HashMap, -) -> (&'static str, u8) { - let mut canonical = std::collections::BTreeMap::new(); - for (reg, count) in counts { - if let Some(name) = canonical_x86_64_arg_reg(reg) { - *canonical.entry(name).or_insert(0u32) += *count; - } - } - - let rdi = *canonical.get("rdi").unwrap_or(&0); - let rsi = *canonical.get("rsi").unwrap_or(&0); - let rcx = *canonical.get("rcx").unwrap_or(&0); - let rdx = *canonical.get("rdx").unwrap_or(&0); - let r8 = *canonical.get("r8").unwrap_or(&0); - let r9 = *canonical.get("r9").unwrap_or(&0); - - let sysv_primary = rdi + rsi; - let sysv_total = rdi + rsi + rdx + rcx + r8 + r9; - let ms_total = rcx + rdx + r8 + r9; - let ms_regs_used = [rcx, rdx, r8, r9].iter().filter(|&&v| v > 0).count(); - let ms_dominant = sysv_primary == 0 - && rcx > 0 - && ms_regs_used >= 2 - && ms_total >= 3 - && ms_total >= (rdi + rsi + rdx + 1); - - if ms_dominant { - let confidence = if ms_total >= 3 { 90 } else { 76 }; - ("ms", confidence) - } else { - let confidence = if sysv_primary > 0 { - 92 - } else if sysv_total > 0 { - 76 - } else { - 60 - }; - ("amd64", confidence) - } +) -> (String, u8) { + r2types::compute_callconv_inference("x86-64", counts) } -fn sanitize_inferred_param_type( - mut ty: r2dec::CType, - var_size_bytes: u32, +#[cfg(test)] +fn infer_signature_return_type( + func: &r2ssa::SSAFunction, + type_inference: &r2types::TypeInference, ptr_bits: u32, -) -> r2dec::CType { - if matches!(ty, r2dec::CType::Void | r2dec::CType::Unknown) { - ty = match var_size_bytes { - 1 => r2dec::CType::Int(8), - 2 => r2dec::CType::Int(16), - 4 => r2dec::CType::Int(32), - 8 => r2dec::CType::Int(64), - _ => r2dec::CType::Unknown, - }; - } - - if matches!(ty, r2dec::CType::Void | r2dec::CType::Unknown) { - ty = if ptr_bits >= 64 { - r2dec::CType::Int(64) - } else { - r2dec::CType::Int(32) - }; - } - - ty + evidence_ctx: &r2types::SignatureTypeEvidenceContext, +) -> (r2dec::CType, TypeEvidence) { + let (ty, evidence) = + r2types::infer_signature_return_type(func, type_inference, ptr_bits, evidence_ctx); + (type_like_to_ctype(&ty), evidence) } -fn is_informative_type(ty: &r2dec::CType) -> bool { - !matches!(ty, r2dec::CType::Void | r2dec::CType::Unknown) +#[cfg(test)] +#[allow(dead_code)] +fn collect_version0_input_regs( + func: &r2ssa::SSAFunction, +) -> std::collections::HashMap { + r2types::collect_version0_input_regs(func) } +#[cfg(test)] fn compute_signature_confidence( params: &[InferredParam], ret_type: &r2dec::CType, ret_evidence: &TypeEvidence, ) -> u8 { - let mut confidence: i32 = 48; - if !params.is_empty() { - confidence += 8; - } - - for param in params { - let evidence = ¶m.evidence; - if evidence.pointer_proven > 0 || evidence.scalar_proven > 0 { - confidence += 6; - } else if evidence.bool_like > 0 - || evidence.pointer_likely > 0 - || evidence.scalar_likely > 0 - { - confidence += 3; - } else if is_informative_type(¶m.ty) { - confidence += 2; - } else { - confidence -= 2; - } - - if evidence.has_conflict() { - confidence -= 4; - } - } - - if is_informative_type(ret_type) { - confidence += 4; - if ret_evidence.pointer_proven > 0 - || ret_evidence.scalar_proven > 0 - || ret_evidence.bool_like > 0 - { - confidence += 2; - } - } else if ret_evidence.has_pointer_signal() || ret_evidence.has_scalar_signal() { - confidence += 2; - } - - if ret_evidence.has_conflict() { - confidence -= 3; - } - - confidence.clamp(0, 100) as u8 + let canonical_params = params + .iter() + .map(|param| r2types::SignatureParamCandidate { + name: param.name.clone(), + ty: ctype_to_type_like(¶m.ty), + arg_index: param.arg_index, + size_bytes: param.size_bytes, + evidence: param.evidence.clone(), + }) + .collect::>(); + r2types::compute_signature_confidence( + &canonical_params, + &ctype_to_type_like(ret_type), + ret_evidence, + ) } +#[cfg(test)] fn compute_callconv_inference( arch_name: &str, input_counts: &std::collections::HashMap, ) -> (String, u8) { - match arch_name { - "x86-64" => { - let (callconv, confidence) = infer_callconv_x86_64_from_counts(input_counts); - (callconv.to_string(), confidence) - } - "x86" => ("cdecl".to_string(), 64), - _ => (String::new(), 0), - } + r2types::compute_callconv_inference(arch_name, input_counts) +} + +#[cfg(test)] +fn is_informative_type(ty: &r2dec::CType) -> bool { + !matches!(ty, r2dec::CType::Void | r2dec::CType::Unknown) } #[cfg(test)] @@ -4491,6 +3881,7 @@ fn explicit_signature_context_strength(sig: &r2types::FunctionSignatureSpec) -> confidence } +#[cfg(test)] fn normalize_inferred_param_name( raw_name: &str, fallback_idx: usize, @@ -4502,6 +3893,7 @@ fn normalize_inferred_param_name( uniquify_name(clean, used) } +#[cfg(test)] fn format_afs_signature( function_name: &str, ret_type: &str, @@ -4523,6 +3915,7 @@ fn cstr_or_default(ptr: *const c_char, default: &str) -> String { helpers::cstr_or_default(ptr, default) } +#[cfg(test)] fn is_opaque_placeholder_type_name(ty: &str) -> bool { let lower = ty.trim().to_ascii_lowercase(); if lower.is_empty() { @@ -4540,6 +3933,7 @@ fn is_opaque_placeholder_type_name(ty: &str) -> bool { || lower.contains(" type_0x") } +#[cfg(test)] fn is_unmaterialized_aggregate_name(name: &str) -> bool { let lower = name.trim().to_ascii_lowercase(); lower.is_empty() || lower == "anon" || lower.starts_with("anon_") @@ -4572,6 +3966,7 @@ fn is_generic_type_string(ty: &str) -> bool { ) } +#[cfg(test)] fn normalize_external_type_name(ty: &str) -> String { let normalized = r2types::normalize_external_type_name(ty); if normalized.is_empty() || is_opaque_placeholder_type_name(&normalized) { @@ -4581,6 +3976,7 @@ fn normalize_external_type_name(ty: &str) -> String { } } +#[cfg(test)] fn estimate_parsed_c_type_size_bytes(ty: &r2dec::CType, ptr_bits: u32) -> Option { match ty { r2dec::CType::Void => Some(0), @@ -4604,6 +4000,7 @@ fn estimate_parsed_c_type_size_bytes(ty: &r2dec::CType, ptr_bits: u32) -> Option } } +#[cfg(test)] fn estimate_c_type_size_bytes(ty: &str, ptr_bits: u32) -> u64 { if let Some(parsed) = parse_external_type(ty, ptr_bits) && let Some(size) = estimate_parsed_c_type_size_bytes(&parsed, ptr_bits) @@ -4622,6 +4019,7 @@ fn estimate_c_type_size_bytes(ty: &str, ptr_bits: u32) -> u64 { 1 } +#[cfg(test)] fn build_struct_decl( name: &str, fields: &[StructFieldCandidateJson], @@ -4682,6 +4080,7 @@ fn parse_existing_var_types(json_str: &str) -> std::collections::HashMap, ptr_bits: u32, @@ -4713,7 +4112,7 @@ fn collect_pointer_arg_slot_map( if is_riscv64 { return alias.starts_with('x') || alias.starts_with('a'); } - alias == canonical.to_ascii_lowercase() + alias == (*canonical).to_ascii_lowercase() }; if include_alias(canonical) { @@ -4729,6 +4128,7 @@ fn collect_pointer_arg_slot_map( } #[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[cfg(test)] struct ArgAddrExpr { slot: usize, offset: i64, @@ -4744,6 +4144,7 @@ struct GlobalAddrExpr { } #[derive(Clone, Debug, Default)] +#[cfg(test)] struct StructFieldEvidence { reads: u32, writes: u32, @@ -4751,16 +4152,21 @@ struct StructFieldEvidence { type_votes: std::collections::BTreeMap, } +#[cfg(test)] type SlotTypeOverrides = std::collections::HashMap; +#[cfg(test)] type SlotFieldProfiles = std::collections::HashMap>; +#[cfg(test)] type SlotFieldEvidenceMap = std::collections::HashMap>; +#[cfg(test)] type StructInferenceArtifacts = ( Vec, SlotTypeOverrides, SlotFieldProfiles, ); +#[cfg(test)] fn build_struct_inference_artifacts_from_field_evidence( slot_field_evidence: SlotFieldEvidenceMap, ptr_bits: u32, @@ -4847,6 +4253,7 @@ fn build_struct_inference_artifacts_from_field_evidence( (struct_decls, slot_type_overrides, slot_fields_for_links) } +#[cfg(test)] fn infer_structs_from_semantic_accesses( ssa_func: &r2ssa::SSAFunction, cfg: &r2dec::DecompilerConfig, @@ -4874,6 +4281,8 @@ fn infer_structs_from_semantic_accesses( build_struct_inference_artifacts_from_field_evidence(slot_field_evidence, ptr_bits, diagnostics) } +#[cfg(test)] +#[allow(dead_code)] fn merge_struct_inference_artifacts( mut base: StructInferenceArtifacts, supplement: StructInferenceArtifacts, @@ -4900,6 +4309,7 @@ fn merge_struct_inference_artifacts( base } +#[cfg(test)] fn parse_ssa_const_offset(name: &str, ptr_bits: u32) -> Option { let val_str = name .strip_prefix("const:") @@ -4923,6 +4333,7 @@ fn parse_ssa_const_offset(name: &str, ptr_bits: u32) -> Option { Some(signed_offset_from_const(raw, ptr_bits)) } +#[cfg(test)] fn signed_offset_from_const(raw: u64, ptr_bits: u32) -> i64 { let bits = ptr_bits.clamp(8, 64); if bits == 64 { @@ -4938,6 +4349,7 @@ fn signed_offset_from_const(raw: u64, ptr_bits: u32) -> i64 { } } +#[cfg(test)] fn infer_structs_from_ssa( ssa_blocks: &[r2ssa::SSABlock], arch: Option<&ArchSpec>, @@ -4947,9 +4359,7 @@ fn infer_structs_from_ssa( use std::collections::HashMap; let pointer_arg_slot_map = collect_pointer_arg_slot_map(arch, ptr_bits); - let arch_name = - crate::decompiler::normalize_sig_arch_name(arch).unwrap_or_else(|| "unknown".to_string()); - let cfg = crate::decompiler::decompiler_config_for_arch_name(&arch_name, ptr_bits); + let (_, _, cfg) = r2dec::DecompilerConfig::for_arch(arch); let sp_name = cfg.sp_name.to_ascii_lowercase(); let fp_name = cfg.fp_name.to_ascii_lowercase(); let mut addr_exprs: HashMap = HashMap::new(); @@ -6066,11 +5476,11 @@ pub extern "C" fn r2sleigh_infer_signature_cc_json( let Some(analysis) = types::build_function_analysis(&input) else { return ptr::null_mut(); }; - let Some(signature_cc) = types::infer_signature_cc_from_analysis(&input, &analysis) else { + let Some(signature) = types::infer_signature_cc_from_analysis(&input, &analysis) else { return ptr::null_mut(); }; - match serde_json::to_string(&signature_cc) { + match serde_json::to_string(&types::signature_to_json(&signature)) { Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), Err(_) => ptr::null_mut(), } @@ -6132,6 +5542,20 @@ struct TypeWritebackInferenceInput<'a> { interproc: InterprocInferenceInput<'a>, } +struct SemanticWorkerLinearizationInput { + ctx: *const R2ILContext, + blocks: *const *const R2ILBlock, + num_blocks: usize, + fcn_addr: u64, + fcn_name: *const c_char, + block_count: usize, + loop_count: usize, + back_edge_count: usize, + max_switch_cases: usize, + scope_functions: *const analysis::sym::R2ILFunctionBlocks, + scope_num_functions: usize, +} + fn infer_type_writeback_json_impl(input: TypeWritebackInferenceInput<'_>) -> *mut c_char { let Some(function_input) = types::build_function_input( input.ctx, @@ -6155,8 +5579,7 @@ fn infer_type_writeback_json_impl(input: TypeWritebackInferenceInput<'_>) -> *mu ) } }; - let arch_name = decompiler::normalize_sig_arch_name(function_input.ctx.arch) - .unwrap_or_else(|| "unknown".to_string()); + let (arch_name, ptr_bits, _) = r2dec::DecompilerConfig::for_arch(function_input.ctx.arch); let payload = if let Some(cached_artifact) = types::get_cached_function_analysis_artifact_with_scope( &function_input, @@ -6164,11 +5587,15 @@ fn infer_type_writeback_json_impl(input: TypeWritebackInferenceInput<'_>) -> *mu symbolic_scope.as_ref(), ) { if let Some(compiled) = cached_artifact.semantic_artifact.as_ref() - && !compiled.capability.type_ready + && cached_artifact + .type_facts + .symbolic_facts + .prefers_bounded_type_plan() { semantic_type_fallback_payload( &function_input.function_name, &arch_name, + ptr_bits, input.interproc, compiled, ) @@ -6179,15 +5606,19 @@ fn infer_type_writeback_json_impl(input: TypeWritebackInferenceInput<'_>) -> *mu let Some(analysis) = types::build_function_analysis(&function_input) else { return ptr::null_mut(); }; - let semantic_artifact = types::collect_plugin_semantic_artifact( + let semantic_artifact = r2sym::compile_semantic_artifact_default_with_scope( + &z3::Context::thread_local(), &analysis.ssa_func, - function_input.ctx.arch, symbolic_scope.as_ref(), + function_input.ctx.arch, ); - if !semantic_artifact.capability.type_ready { + if r2types::symbolic_semantic_facts_from_artifact(&semantic_artifact) + .prefers_bounded_type_plan() + { semantic_type_fallback_payload( &function_input.function_name, &arch_name, + ptr_bits, input.interproc, &semantic_artifact, ) @@ -6219,6 +5650,75 @@ fn infer_type_writeback_json_impl(input: TypeWritebackInferenceInput<'_>) -> *mu } } +fn semantic_worker_linearization_impl(input: SemanticWorkerLinearizationInput) -> *mut c_char { + let summary = r2ssa::CFGRiskSummary { + block_count: input.block_count, + loop_count: input.loop_count, + back_edge_count: input.back_edge_count, + switch_block_count: usize::from(input.max_switch_cases > 0), + max_switch_cases: input.max_switch_cases, + }; + let Some(reason) = r2dec::cfg_guard_reason_from_summary(&summary) else { + return ptr::null_mut(); + }; + let Some(function_input) = types::build_function_input( + input.ctx, + input.blocks, + input.num_blocks, + input.fcn_addr, + input.fcn_name, + ) else { + return ptr::null_mut(); + }; + let symbolic_scope = if input.scope_functions.is_null() || input.scope_num_functions == 0 { + None + } else { + unsafe { + analysis::sym::build_symbolic_scope_from_ffi( + input.scope_functions, + input.scope_num_functions, + function_input.ctx.arch, + function_input.function_addr, + ) + } + }; + let (arch_name, ptr_bits, _) = r2dec::DecompilerConfig::for_arch(function_input.ctx.arch); + let symbolic_facts = if let Some(cached_artifact) = + types::get_cached_function_analysis_artifact_with_scope( + &function_input, + "{}", + symbolic_scope.as_ref(), + ) { + cached_artifact.type_facts.symbolic_facts + } else { + let Some(analysis) = types::build_function_analysis(&function_input) else { + return ptr::null_mut(); + }; + let semantic_artifact = r2sym::compile_semantic_artifact_default_with_scope( + &z3::Context::thread_local(), + &analysis.ssa_func, + symbolic_scope.as_ref(), + function_input.ctx.arch, + ); + r2types::symbolic_semantic_facts_from_artifact(&semantic_artifact) + }; + if !symbolic_facts.decompile_ready() || symbolic_facts.structured_decompile_ready() { + return ptr::null_mut(); + } + let plan = r2types::build_semantic_type_fallback_plan( + &function_input.function_name, + &arch_name, + ptr_bits, + &symbolic_facts, + ); + CString::new(r2dec::render_semantic_worker_linearization( + &plan, + &symbolic_facts, + &reason, + )) + .map_or(ptr::null_mut(), |c| c.into_raw()) +} + #[unsafe(no_mangle)] pub extern "C" fn r2sleigh_infer_type_writeback_json( ctx: *const R2ILContext, @@ -6312,6 +5812,35 @@ pub extern "C" fn r2sleigh_infer_type_writeback_json_scope_ex( }) } +#[unsafe(no_mangle)] +pub extern "C" fn r2dec_semantic_worker_linearization_scope_ffi( + ctx: *const R2ILContext, + blocks: *const *const R2ILBlock, + num_blocks: usize, + fcn_addr: u64, + fcn_name: *const c_char, + block_count: usize, + loop_count: usize, + back_edge_count: usize, + max_switch_cases: usize, + scope_functions: *const analysis::sym::R2ILFunctionBlocks, + scope_num_functions: usize, +) -> *mut c_char { + semantic_worker_linearization_impl(SemanticWorkerLinearizationInput { + ctx, + blocks, + num_blocks, + fcn_addr, + fcn_name, + block_count, + loop_count, + back_edge_count, + max_switch_cases, + scope_functions, + scope_num_functions, + }) +} + #[unsafe(no_mangle)] pub extern "C" fn r2sleigh_alias_function_analysis_artifact_cache( ctx: *const R2ILContext, @@ -7834,8 +7363,7 @@ mod tests { max_switch_cases: 40, }; - let reason = - decompiler_cfg_guard_reason_from_summary(&summary).expect("guard reason expected"); + let reason = r2dec::cfg_guard_reason_from_summary(&summary).expect("guard reason expected"); assert!( reason.contains("dense switch") || reason.contains("max_switch_cases"), "unexpected reason: {reason}" @@ -7852,8 +7380,7 @@ mod tests { max_switch_cases: 0, }; - let reason = - decompiler_cfg_guard_reason_from_summary(&summary).expect("guard reason expected"); + let reason = r2dec::cfg_guard_reason_from_summary(&summary).expect("guard reason expected"); assert!( reason.contains("back_edges=38"), "expected back-edge detail in reason, got: {reason}" @@ -7870,7 +7397,25 @@ mod tests { max_switch_cases: 0, }; - assert_eq!(decompiler_cfg_guard_reason_from_summary(&summary), None); + assert_eq!(r2dec::cfg_guard_reason_from_summary(&summary), None); + } + + #[test] + fn r2dec_cfg_guard_comment_ffi_reports_complex_loop_graph() { + let name = CString::new("fcn.140010138").unwrap(); + let out = r2dec_cfg_guard_comment_ffi(name.as_ptr(), 107, 9, 17, 0); + assert!(!out.is_null()); + let rendered = unsafe { CStr::from_ptr(out) }.to_str().unwrap().to_string(); + assert!(rendered.contains("r2dec fallback")); + assert!(rendered.contains("complex loop graph")); + r2il_string_free(out); + } + + #[test] + fn r2dec_cfg_guard_comment_ffi_returns_null_for_benign_summary() { + let name = CString::new("sym.small").unwrap(); + let out = r2dec_cfg_guard_comment_ffi(name.as_ptr(), 12, 1, 1, 0); + assert!(out.is_null()); } fn test_compiled_condition(expr: &str) -> r2sym::BackwardConditionSummary { @@ -7908,6 +7453,7 @@ mod tests { true_compiled: Some(compiled.clone()), false_compiled: None, }], + worker_islands: Vec::new(), control_islands: vec![r2sym::SymbolicControlIsland { kind: r2sym::SymbolicControlIslandKind::LargeCfgBranchFrontier, anchor_block: 0x401000, @@ -7915,6 +7461,7 @@ mod tests { facts: vec![control_fact], evidence: r2sym::SemanticEvidence::exact(), }], + memory_islands: Vec::new(), diagnostics: r2sym::SymbolicFunctionFactDiagnostics { branches_evaluated: 1, branches_pruned: 0, @@ -7946,18 +7493,20 @@ mod tests { } #[test] - fn decompile_ready_large_cfg_worker_still_prefers_semantic_fallback() { + fn decompile_ready_large_cfg_worker_keeps_real_decompile_path() { let compiled = test_large_cfg_semantic_artifact(); - assert!(should_decompile_from_semantic_fallback( - "fcn.401000", - &compiled - )); + let symbolic_facts = r2types::symbolic_semantic_facts_from_artifact(&compiled); + assert!( + r2dec::preferred_semantic_fallback_comment("fcn.401000", &symbolic_facts).is_none() + ); } #[test] fn semantic_fallback_text_reports_actionable_control_island_counts() { let compiled = test_large_cfg_semantic_artifact(); - let output = decompile_semantic_artifact_fallback("_401000", &compiled); + let symbolic_facts = r2types::symbolic_semantic_facts_from_artifact(&compiled); + let output = r2dec::semantic_fallback_comment("_401000", &symbolic_facts) + .expect("typed semantic fallback comment"); assert!(output.contains("semantic fallback: worker slice in residual mode")); assert!(output.contains("branch_facts=1")); assert!(output.contains("control_islands=1")); @@ -8614,9 +8163,8 @@ mod integration_tests { Some("HeapAlloc"), "payload={output}" ); - assert_eq!( - payload["ret_type"].as_str(), - Some("void*"), + assert!( + matches!(payload["ret_type"].as_str(), Some("void *" | "void*")), "payload={output}" ); } @@ -11084,7 +10632,7 @@ mod integration_tests { }) .to_string(); - let mut artifact = crate::types::build_detached_function_analysis_artifact( + let artifact = crate::types::build_detached_function_analysis_artifact( &blocks, "dbg.test_setlocale_wrapper", Some(&arch), @@ -11097,16 +10645,12 @@ mod integration_tests { let function_names = HashMap::from([(0x401160, "sym.imp.setlocale".to_string())]); let strings = HashMap::from([(0x403040, "C".to_string())]); - crate::types::enrich_known_function_signatures_from_names( - &mut artifact.type_facts, - &function_names, - 64, - ); let input = crate::decompiler::decompiler_input_from_artifact( artifact, function_names, strings, HashMap::new(), + 64, ); let decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); let output = decompiler.decompile_input(&input); @@ -11378,6 +10922,7 @@ mod integration_tests { function_names, HashMap::new(), HashMap::new(), + 64, ); let decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); let output = decompiler.decompile_input(&input); @@ -11847,12 +11392,14 @@ mod integration_tests { HashMap::from([(0x401100, "sym.imp.strlen".to_string())]), HashMap::new(), HashMap::new(), + 64, ); let empty_input = crate::decompiler::decompiler_input_from_artifact( artifact.clone(), HashMap::new(), HashMap::new(), HashMap::new(), + 64, ); let decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); let output = decompiler.decompile_input(&input); @@ -12662,6 +12209,7 @@ mod integration_tests { HashMap::new(), HashMap::new(), HashMap::new(), + 64, )); assert!( diff --git a/r2plugin/src/types.rs b/r2plugin/src/types.rs index ff1330b..9dd4a54 100644 --- a/r2plugin/src/types.rs +++ b/r2plugin/src/types.rs @@ -1,14 +1,13 @@ +#[cfg(test)] +use crate::InferredParam; use crate::blocks::BlockSlice; use crate::context::{PluginCtxView, require_ctx_view}; -use crate::decompiler::{ - build_decompiler_env, decompiler_config_for_arch_name, normalize_sig_arch_name, -}; -use crate::helpers::{effective_ptr_bits, normalize_sim_name, resolve_function_name}; +use crate::decompiler::build_decompiler_env; +use crate::helpers::{effective_ptr_bits, resolve_function_name}; use crate::{ - ArchSpec, Disassembler, InferredParam, InferredParamJson, InferredSignatureCcJson, R2ILBlock, - R2ILContext, + ArchSpec, Disassembler, InferredParamJson, InferredSignatureCcJson, R2ILBlock, R2ILContext, }; -use std::collections::{BTreeMap, HashMap, HashSet}; +use std::collections::{BTreeMap, HashMap}; use std::ffi::CString; use std::hash::{DefaultHasher, Hash, Hasher}; use std::io::{self, Write}; @@ -181,671 +180,22 @@ fn rename_function_analysis_artifact( } } -fn symbolic_reachability_status_from_sym( - status: r2sym::SymbolicReachabilityStatus, -) -> r2types::SymbolicReachabilityStatus { - match status { - r2sym::SymbolicReachabilityStatus::Reachable => { - r2types::SymbolicReachabilityStatus::Reachable - } - r2sym::SymbolicReachabilityStatus::Unreachable => { - r2types::SymbolicReachabilityStatus::Unreachable - } - r2sym::SymbolicReachabilityStatus::Unknown => r2types::SymbolicReachabilityStatus::Unknown, - } -} - -fn symbolic_condition_precision_from_sym( - precision: r2sym::BackwardConditionPrecision, -) -> r2types::SymbolicConditionPrecision { - match precision { - r2sym::BackwardConditionPrecision::Exact => r2types::SymbolicConditionPrecision::Exact, - r2sym::BackwardConditionPrecision::OverApprox => { - r2types::SymbolicConditionPrecision::OverApprox - } - r2sym::BackwardConditionPrecision::ResidualSearchRequired => { - r2types::SymbolicConditionPrecision::ResidualSearchRequired - } - r2sym::BackwardConditionPrecision::Unsupported => { - r2types::SymbolicConditionPrecision::Unsupported - } - } -} - -fn symbolic_memory_region_kind_from_sym( - kind: &r2sym::MemoryRegionKind, -) -> r2types::SymbolicMemoryRegionKind { - match kind { - r2sym::MemoryRegionKind::Stack => r2types::SymbolicMemoryRegionKind::Stack, - r2sym::MemoryRegionKind::Global => r2types::SymbolicMemoryRegionKind::Global, - r2sym::MemoryRegionKind::Input => r2types::SymbolicMemoryRegionKind::Input, - r2sym::MemoryRegionKind::Heap => r2types::SymbolicMemoryRegionKind::Heap, - r2sym::MemoryRegionKind::Replay => r2types::SymbolicMemoryRegionKind::Replay, - r2sym::MemoryRegionKind::EscapedUnknown => { - r2types::SymbolicMemoryRegionKind::EscapedUnknown - } - } -} - -fn symbolic_memory_region_from_sym( - region: &r2sym::BackwardMemoryRegion, -) -> r2types::SymbolicMemoryRegion { - match region { - r2sym::BackwardMemoryRegion::Argument { index } => { - r2types::SymbolicMemoryRegion::Argument { index: *index } - } - r2sym::BackwardMemoryRegion::Region(region) => { - r2types::SymbolicMemoryRegion::Region(r2types::SymbolicMemoryRegionRef { - id: region.id.0, - kind: symbolic_memory_region_kind_from_sym(®ion.kind), - name: region.name.clone(), - }) - } - } -} - -fn symbolic_confidence_from_sym( - confidence: r2sym::SemanticConfidence, -) -> r2types::SymbolicSemanticConfidence { - match confidence { - r2sym::SemanticConfidence::Exact => r2types::SymbolicSemanticConfidence::Exact, - r2sym::SemanticConfidence::Likely => r2types::SymbolicSemanticConfidence::Likely, - r2sym::SemanticConfidence::Heuristic => r2types::SymbolicSemanticConfidence::Heuristic, - r2sym::SemanticConfidence::Residual => r2types::SymbolicSemanticConfidence::Residual, - } -} - -fn symbolic_evidence_reason_from_sym( - reason: r2sym::SemanticEvidenceReason, -) -> r2types::SymbolicSemanticEvidenceReason { - match reason { - r2sym::SemanticEvidenceReason::LargeCfg => { - r2types::SymbolicSemanticEvidenceReason::LargeCfg - } - r2sym::SemanticEvidenceReason::SummaryBudget => { - r2types::SymbolicSemanticEvidenceReason::SummaryBudget - } - r2sym::SemanticEvidenceReason::AliasAmbiguity => { - r2types::SymbolicSemanticEvidenceReason::AliasAmbiguity - } - r2sym::SemanticEvidenceReason::ReplayOverlap => { - r2types::SymbolicSemanticEvidenceReason::ReplayOverlap - } - r2sym::SemanticEvidenceReason::HeapIdentityWeak => { - r2types::SymbolicSemanticEvidenceReason::HeapIdentityWeak - } - r2sym::SemanticEvidenceReason::GuardOpaque => { - r2types::SymbolicSemanticEvidenceReason::GuardOpaque - } - r2sym::SemanticEvidenceReason::ValueOpaque => { - r2types::SymbolicSemanticEvidenceReason::ValueOpaque - } - r2sym::SemanticEvidenceReason::TruncatedTransfer => { - r2types::SymbolicSemanticEvidenceReason::TruncatedTransfer - } - r2sym::SemanticEvidenceReason::DerivedFromRanking => { - r2types::SymbolicSemanticEvidenceReason::DerivedFromRanking - } - r2sym::SemanticEvidenceReason::PartialPathCoverage => { - r2types::SymbolicSemanticEvidenceReason::PartialPathCoverage - } - r2sym::SemanticEvidenceReason::ResidualSearchRequired => { - r2types::SymbolicSemanticEvidenceReason::ResidualSearchRequired - } - } -} - -fn symbolic_evidence_from_sym( - evidence: &r2sym::SemanticEvidence, -) -> r2types::SymbolicSemanticEvidence { - r2types::SymbolicSemanticEvidence { - tier: symbolic_confidence_from_sym(evidence.tier), - soundness: match evidence.soundness { - r2sym::SemanticEvidenceSoundness::Proven => { - r2types::SymbolicSemanticEvidenceSoundness::Proven - } - r2sym::SemanticEvidenceSoundness::OverApprox => { - r2types::SymbolicSemanticEvidenceSoundness::OverApprox - } - r2sym::SemanticEvidenceSoundness::Ranked => { - r2types::SymbolicSemanticEvidenceSoundness::Ranked - } - r2sym::SemanticEvidenceSoundness::Unknown => { - r2types::SymbolicSemanticEvidenceSoundness::Unknown - } - }, - coverage: match evidence.coverage { - r2sym::SemanticEvidenceCoverage::Full => { - r2types::SymbolicSemanticEvidenceCoverage::Full - } - r2sym::SemanticEvidenceCoverage::Partial => { - r2types::SymbolicSemanticEvidenceCoverage::Partial - } - r2sym::SemanticEvidenceCoverage::Bounded => { - r2types::SymbolicSemanticEvidenceCoverage::Bounded - } - }, - provenance: match evidence.provenance { - r2sym::SemanticEvidenceProvenance::Stable => { - r2types::SymbolicSemanticEvidenceProvenance::Stable - } - r2sym::SemanticEvidenceProvenance::Normalized => { - r2types::SymbolicSemanticEvidenceProvenance::Normalized - } - r2sym::SemanticEvidenceProvenance::Ranked => { - r2types::SymbolicSemanticEvidenceProvenance::Ranked - } - r2sym::SemanticEvidenceProvenance::Unstable => { - r2types::SymbolicSemanticEvidenceProvenance::Unstable - } - }, - ambiguity: match evidence.ambiguity { - r2sym::SemanticEvidenceAmbiguity::Single => { - r2types::SymbolicSemanticEvidenceAmbiguity::Single - } - r2sym::SemanticEvidenceAmbiguity::Bounded => { - r2types::SymbolicSemanticEvidenceAmbiguity::Bounded - } - r2sym::SemanticEvidenceAmbiguity::Ranked => { - r2types::SymbolicSemanticEvidenceAmbiguity::Ranked - } - r2sym::SemanticEvidenceAmbiguity::Multiple => { - r2types::SymbolicSemanticEvidenceAmbiguity::Multiple - } - }, - budget_limited: evidence.budget_limited, - reasons: evidence - .reasons - .iter() - .copied() - .map(symbolic_evidence_reason_from_sym) - .collect(), - } -} - -fn symbolic_compiled_condition_from_sym( - summary: &r2sym::BackwardConditionSummary, -) -> r2types::SymbolicCompiledCondition { - let evidence = summary.evidence(); - r2types::SymbolicCompiledCondition { - simplified: summary.simplified.clone(), - terms: summary.terms.clone(), - memory_terms: summary - .memory_terms - .iter() - .map(|term| r2types::SymbolicMemoryCondition { - region: symbolic_memory_region_from_sym(&term.region), - offset_lo: term.offset_lo, - offset_hi: term.offset_hi, - size: term.size, - exact_offset: term.exact_offset, - evidence: symbolic_evidence_from_sym(&term.evidence()), - confidence: symbolic_confidence_from_sym(term.confidence()), - binding: None, - expr: term.expr.clone(), - value_expr: None, - exact_value: false, - }) - .collect(), - backward_memory_substitutions: summary.backward_memory_substitutions, - backward_memory_candidate_enumerations: summary.backward_memory_candidate_enumerations, - backward_memory_residual_fallbacks: summary.backward_memory_residual_fallbacks, - precision: symbolic_condition_precision_from_sym(summary.precision), - evidence: symbolic_evidence_from_sym(&evidence), - confidence: symbolic_confidence_from_sym(evidence.tier), - supported_paths: summary.supported_paths, - total_paths: summary.total_paths, - } -} - -fn symbolic_control_island_kind_from_sym( - kind: r2sym::SymbolicControlIslandKind, -) -> r2types::SymbolicControlIslandKind { - match kind { - r2sym::SymbolicControlIslandKind::BranchFrontier => { - r2types::SymbolicControlIslandKind::BranchFrontier - } - r2sym::SymbolicControlIslandKind::LargeCfgBranchFrontier => { - r2types::SymbolicControlIslandKind::LargeCfgBranchFrontier - } - } -} - -fn symbolic_control_fact_from_sym( - fact: &r2sym::SymbolicControlFact, -) -> r2types::SymbolicControlFact { - r2types::SymbolicControlFact { - target: fact.target, - status: symbolic_reachability_status_from_sym(fact.status), - condition: fact.condition.clone(), - compiled: fact - .compiled - .as_ref() - .map(symbolic_compiled_condition_from_sym), - evidence: symbolic_evidence_from_sym(&fact.evidence), - confidence: symbolic_confidence_from_sym(fact.evidence.tier), - } -} - -fn symbolic_control_island_from_sym( - island: &r2sym::SymbolicControlIsland, -) -> r2types::SymbolicControlIsland { - r2types::SymbolicControlIsland { - kind: symbolic_control_island_kind_from_sym(island.kind), - anchor_block: island.anchor_block, - frontier_targets: island.frontier_targets.clone(), - facts: island - .facts - .iter() - .map(symbolic_control_fact_from_sym) - .collect(), - evidence: symbolic_evidence_from_sym(&island.evidence), - confidence: symbolic_confidence_from_sym(island.evidence.tier), - } -} - -fn symbolic_interpreter_kind_from_sym( - kind: r2sym::InterpreterKind, -) -> r2types::SymbolicInterpreterKind { - match kind { - r2sym::InterpreterKind::SwitchDispatch => r2types::SymbolicInterpreterKind::SwitchDispatch, - r2sym::InterpreterKind::IndirectDispatch => { - r2types::SymbolicInterpreterKind::IndirectDispatch - } - } -} - -pub(crate) fn symbolic_vm_value_expr_from_sym( - value: &r2sym::VmValueExpr, -) -> r2types::SymbolicVmValueExpr { - match value { - r2sym::VmValueExpr::Const(value) => r2types::SymbolicVmValueExpr::Const(*value), - r2sym::VmValueExpr::Var(name) => r2types::SymbolicVmValueExpr::Var(name.clone()), - r2sym::VmValueExpr::Unary { op, arg } => r2types::SymbolicVmValueExpr::Unary { - op: match op { - r2sym::VmUnaryOp::Neg => r2types::SymbolicVmUnaryOp::Neg, - r2sym::VmUnaryOp::BitNot => r2types::SymbolicVmUnaryOp::Not, - r2sym::VmUnaryOp::BoolNot => r2types::SymbolicVmUnaryOp::BoolNot, - }, - expr: Box::new(symbolic_vm_value_expr_from_sym(arg)), - }, - r2sym::VmValueExpr::Binary { op, lhs, rhs } => r2types::SymbolicVmValueExpr::Binary { - op: match op { - r2sym::VmBinaryOp::Add => r2types::SymbolicVmBinaryOp::Add, - r2sym::VmBinaryOp::Sub => r2types::SymbolicVmBinaryOp::Sub, - r2sym::VmBinaryOp::Mul => r2types::SymbolicVmBinaryOp::Mul, - r2sym::VmBinaryOp::Div => r2types::SymbolicVmBinaryOp::Div, - r2sym::VmBinaryOp::Rem => r2types::SymbolicVmBinaryOp::Rem, - r2sym::VmBinaryOp::And => r2types::SymbolicVmBinaryOp::And, - r2sym::VmBinaryOp::Or => r2types::SymbolicVmBinaryOp::Or, - r2sym::VmBinaryOp::Xor => r2types::SymbolicVmBinaryOp::Xor, - r2sym::VmBinaryOp::Shl => r2types::SymbolicVmBinaryOp::Shl, - r2sym::VmBinaryOp::LShr | r2sym::VmBinaryOp::AShr => { - r2types::SymbolicVmBinaryOp::Shr - } - r2sym::VmBinaryOp::Eq => r2types::SymbolicVmBinaryOp::Eq, - r2sym::VmBinaryOp::Ne => r2types::SymbolicVmBinaryOp::Ne, - r2sym::VmBinaryOp::Lt | r2sym::VmBinaryOp::SLt => r2types::SymbolicVmBinaryOp::Lt, - r2sym::VmBinaryOp::Le | r2sym::VmBinaryOp::SLe => r2types::SymbolicVmBinaryOp::Le, - r2sym::VmBinaryOp::BoolAnd => r2types::SymbolicVmBinaryOp::And, - r2sym::VmBinaryOp::BoolOr => r2types::SymbolicVmBinaryOp::Or, - }, - left: Box::new(symbolic_vm_value_expr_from_sym(lhs)), - right: Box::new(symbolic_vm_value_expr_from_sym(rhs)), - }, - r2sym::VmValueExpr::Expr(expr) => r2types::SymbolicVmValueExpr::Expr(expr.clone()), - } -} - -fn symbolic_vm_state_update_from_sym( - update: &r2sym::VmStateUpdate, -) -> r2types::SymbolicVmStateUpdate { - let evidence = update.evidence(); - r2types::SymbolicVmStateUpdate { - output: update.output.clone(), - expr: update.expr.clone(), - value: symbolic_vm_value_expr_from_sym(&update.value), - exact: update.exact, - evidence: symbolic_evidence_from_sym(&evidence), - confidence: symbolic_confidence_from_sym(evidence.tier), - } -} - -fn symbolic_vm_guard_condition_from_sym( - guard: &r2sym::VmGuardCondition, -) -> r2types::SymbolicVmGuardCondition { - let evidence = guard.evidence(); - r2types::SymbolicVmGuardCondition { - expr: guard.expr.clone(), - value: symbolic_vm_value_expr_from_sym(&guard.value), - expect_nonzero: guard.expect_nonzero, - exact: guard.exact, - evidence: symbolic_evidence_from_sym(&evidence), - confidence: symbolic_confidence_from_sym(evidence.tier), - } -} - -fn symbolic_vm_guarded_exit_from_sym( - guarded: &r2sym::VmGuardedExit, -) -> r2types::SymbolicVmGuardedExit { - r2types::SymbolicVmGuardedExit { - target: guarded.target, - guard: symbolic_vm_guard_condition_from_sym(&guarded.guard), - } -} - -fn symbolic_vm_memory_condition_from_sym( - condition: &r2sym::VmMemoryCondition, -) -> r2types::SymbolicMemoryCondition { - let evidence = condition.evidence(); - r2types::SymbolicMemoryCondition { - region: r2types::SymbolicMemoryRegion::Region(r2types::SymbolicMemoryRegionRef { - id: condition.region.id, - kind: symbolic_memory_region_kind_from_sym(&condition.region.kind), - name: condition.region.name.clone(), - }), - offset_lo: condition.offset_lo, - offset_hi: condition.offset_hi, - size: condition.size, - exact_offset: condition.exact_offset, - evidence: symbolic_evidence_from_sym(&evidence), - confidence: symbolic_confidence_from_sym(evidence.tier), - binding: condition.binding.clone(), - expr: condition.expr.clone(), - value_expr: condition.value_expr.clone(), - exact_value: condition.exact_value, - } -} - -fn symbolic_vm_transfer_arm_from_sym( - transfer: &r2sym::VmTransferArm, -) -> r2types::SymbolicVmTransferArm { - let evidence = transfer.evidence(); - r2types::SymbolicVmTransferArm { - handler_target: transfer.handler_target, - case_values: transfer.case_values.clone(), - region_blocks: transfer.region_blocks.clone(), - exit_targets: transfer.exit_targets.clone(), - exit_guards: transfer - .exit_guards - .iter() - .map(symbolic_vm_guarded_exit_from_sym) - .collect(), - state_updates: transfer - .state_updates - .iter() - .map(symbolic_vm_state_update_from_sym) - .collect(), - selector_update: transfer - .selector_update - .as_ref() - .map(symbolic_vm_state_update_from_sym), - memory_reads: transfer - .memory_reads - .iter() - .map(symbolic_vm_memory_condition_from_sym) - .collect(), - memory_writes: transfer - .memory_writes - .iter() - .map(symbolic_vm_memory_condition_from_sym) - .collect(), - residual_guards: transfer.residual_guards, - residual_memory_effects: transfer.residual_memory_effects, - exact: transfer.exact, - evidence: symbolic_evidence_from_sym(&evidence), - confidence: symbolic_confidence_from_sym(evidence.tier), - redispatch: transfer.redispatch, - may_return: transfer.may_return, - truncated: transfer.truncated, - } -} - -fn symbolic_vm_step_summary_from_sym( - vm_step: &r2sym::VmStepSummary, -) -> r2types::SymbolicVmStepSummary { - r2types::SymbolicVmStepSummary { - kind: symbolic_interpreter_kind_from_sym(vm_step.kind), - loop_header: vm_step.loop_header, - dispatch_header: vm_step.dispatch_header, - selector: vm_step.selector.clone(), - dispatch_targets: vm_step.dispatch_targets.clone(), - default_target: vm_step.default_target, - case_values_by_target: vm_step.case_values_by_target.clone(), - loop_latches: vm_step.loop_latches.clone(), - state_inputs: vm_step.state_inputs.clone(), - state_outputs: vm_step.state_outputs.clone(), - step_blocks: vm_step.step_blocks.clone(), - handler_regions: vm_step.handler_regions.clone(), - handler_state_inputs: vm_step.handler_state_inputs.clone(), - handler_state_outputs: vm_step.handler_state_outputs.clone(), - handler_state_updates: vm_step - .handler_state_updates - .iter() - .map(|(target, updates)| { - ( - *target, - updates - .iter() - .map(symbolic_vm_state_update_from_sym) - .collect(), - ) - }) - .collect(), - handler_exit_guards: vm_step - .handler_exit_guards - .iter() - .map(|(target, guards)| { - ( - *target, - guards - .iter() - .map(symbolic_vm_guarded_exit_from_sym) - .collect(), - ) - }) - .collect(), - handler_memory_read_effects: vm_step - .handler_memory_read_effects - .iter() - .map(|(target, effects)| { - ( - *target, - effects - .iter() - .map(symbolic_vm_memory_condition_from_sym) - .collect(), - ) - }) - .collect(), - handler_memory_write_effects: vm_step - .handler_memory_write_effects - .iter() - .map(|(target, effects)| { - ( - *target, - effects - .iter() - .map(symbolic_vm_memory_condition_from_sym) - .collect(), - ) - }) - .collect(), - handler_memory_reads: vm_step.handler_memory_reads.clone(), - handler_memory_writes: vm_step.handler_memory_writes.clone(), - handler_calls: vm_step.handler_calls.clone(), - handler_conditional_branches: vm_step.handler_conditional_branches.clone(), - handler_exit_targets: vm_step.handler_exit_targets.clone(), - redispatch_handlers: vm_step.redispatch_handlers.clone(), - returning_handlers: vm_step.returning_handlers.clone(), - truncated_handlers: vm_step.truncated_handlers.clone(), - transfers: vm_step - .transfers - .iter() - .map(symbolic_vm_transfer_arm_from_sym) - .collect(), - } -} - -pub(crate) fn symbolic_semantic_facts_from_sym( - compiled: &r2sym::CompiledSemanticArtifact, -) -> r2types::SymbolicSemanticFacts { - let facts = &compiled.symbolic_facts; - r2types::SymbolicSemanticFacts { - branch_facts: facts - .branch_facts - .iter() - .cloned() - .map(|fact| r2types::SymbolicBranchFact { - block_addr: fact.block_addr, - true_target: fact.true_target, - false_target: fact.false_target, - true_status: symbolic_reachability_status_from_sym(fact.true_status), - false_status: symbolic_reachability_status_from_sym(fact.false_status), - true_condition: fact.true_condition, - false_condition: fact.false_condition, - true_compiled: fact - .true_compiled - .as_ref() - .map(symbolic_compiled_condition_from_sym), - false_compiled: fact - .false_compiled - .as_ref() - .map(symbolic_compiled_condition_from_sym), - }) - .collect(), - control_islands: facts - .control_islands - .iter() - .map(symbolic_control_island_from_sym) - .collect(), - diagnostics: r2types::SymbolicFactDiagnostics { - branches_evaluated: facts.diagnostics.branches_evaluated, - branches_pruned: facts.diagnostics.branches_pruned, - branches_unknown: facts.diagnostics.branches_unknown, - skipped_missing_arch: facts.diagnostics.skipped_missing_arch, - skipped_large_cfg: facts.diagnostics.skipped_large_cfg, - cache_hit: compiled.cache_hit, - semantic_mode: Some(match compiled.mode { - r2sym::SemanticMode::Raw => r2types::SymbolicSemanticMode::Raw, - r2sym::SemanticMode::Compiled => r2types::SymbolicSemanticMode::Compiled, - r2sym::SemanticMode::Residual => r2types::SymbolicSemanticMode::Residual, - r2sym::SemanticMode::VmSummary => r2types::SymbolicSemanticMode::VmSummary, - }), - semantic_capability: Some(r2types::SymbolicSemanticCapability { - query_ready: compiled.capability.query_ready, - type_ready: compiled.capability.type_ready, - decompile_ready: compiled.capability.decompile_ready, - }), - slice_class: Some(match compiled.slice_class { - r2sym::SliceClass::Wrapper => r2types::SymbolicSemanticSliceClass::Wrapper, - r2sym::SliceClass::Worker => r2types::SymbolicSemanticSliceClass::Worker, - r2sym::SliceClass::RecursiveGroup => { - r2types::SymbolicSemanticSliceClass::RecursiveGroup - } - r2sym::SliceClass::InterpreterSwitch => { - r2types::SymbolicSemanticSliceClass::InterpreterSwitch - } - r2sym::SliceClass::InterpreterIndirect => { - r2types::SymbolicSemanticSliceClass::InterpreterIndirect - } - r2sym::SliceClass::GenericLarge => { - r2types::SymbolicSemanticSliceClass::GenericLarge - } - }), - residual_reasons: compiled - .residual_reasons - .iter() - .map(|reason| match reason { - r2sym::ResidualReason::MissingArch => { - r2types::SymbolicSemanticResidualReason::MissingArch - } - r2sym::ResidualReason::LargeCfg => { - r2types::SymbolicSemanticResidualReason::LargeCfg - } - r2sym::ResidualReason::SummaryBudgetExhausted => { - r2types::SymbolicSemanticResidualReason::SummaryBudgetExhausted - } - r2sym::ResidualReason::SccBudgetExhausted => { - r2types::SymbolicSemanticResidualReason::SccBudgetExhausted - } - r2sym::ResidualReason::InterpreterRequiresStepSummary => { - r2types::SymbolicSemanticResidualReason::InterpreterRequiresStepSummary - } - }) - .collect(), - closure_functions: compiled.closure_functions, - helper_functions: compiled.helper_functions, - derived_summaries: compiled.derived_summaries, - summary_attempted: compiled.derived_diagnostics.attempted, - summary_budget_exhausted: compiled.derived_diagnostics.budget_exhausted - + compiled.derived_diagnostics.scc_budget_exhausted, - summary_scc_count: compiled.derived_diagnostics.scc_count, - }, - interpreter: compiled.interpreter.as_ref().map(|interpreter| { - r2types::SymbolicInterpreterDispatch { - kind: symbolic_interpreter_kind_from_sym(interpreter.kind), - dispatch_header: interpreter.dispatch_header, - dispatch_targets: interpreter.dispatch_targets, - selector: interpreter.selector.clone(), - back_edges: interpreter.back_edges, - score: interpreter.score, - } - }), - vm_step: compiled - .vm_step - .as_ref() - .map(symbolic_vm_step_summary_from_sym), - vm_transfer: compiled - .vm_transfer - .as_ref() - .map(symbolic_vm_step_summary_from_sym), - } -} - -pub(crate) fn collect_plugin_semantic_artifact( - prepared: &r2ssa::SsaArtifact, - arch: Option<&ArchSpec>, - symbolic_scope: Option<&r2sym::PreparedFunctionScope>, -) -> r2sym::CompiledSemanticArtifact { - let scope = symbolic_scope_with_prepared_root(prepared, symbolic_scope); - r2sym::compile_semantic_artifact_with_scope( - &z3::Context::thread_local(), - prepared, - scope.as_ref(), - arch, - &HashMap::new(), - r2sym::SummaryProfile::Default, - ) -} - -fn symbolic_scope_with_prepared_root( - prepared: &r2ssa::SsaArtifact, - symbolic_scope: Option<&r2sym::PreparedFunctionScope>, -) -> Option { - let scope = symbolic_scope?; - let mut functions = scope.functions().values().cloned().collect::>(); - for function in &mut functions { - if function.id == scope.root_id() { - function.prepared = prepared.clone(); - if function.name.is_none() { - function.name = prepared.function().name.clone(); - } - } - } - r2sym::PreparedFunctionScope::new(scope.root_id().0, functions) -} - #[allow(dead_code)] fn collect_plugin_symbolic_facts( prepared: &r2ssa::SsaArtifact, arch: Option<&ArchSpec>, symbolic_scope: Option<&r2sym::PreparedFunctionScope>, ) -> r2types::SymbolicSemanticFacts { - let artifact = collect_plugin_semantic_artifact(prepared, arch, symbolic_scope); - symbolic_semantic_facts_from_sym(&artifact) + let artifact = r2sym::compile_semantic_artifact_default_with_scope( + &z3::Context::thread_local(), + prepared, + symbolic_scope, + arch, + ); + r2types::symbolic_semantic_facts_from_artifact(&artifact) } +#[cfg(test)] fn type_like_to_ctype(ty: &r2types::CTypeLike) -> r2dec::CType { match ty { r2types::CTypeLike::Void => r2dec::CType::Void, @@ -868,85 +218,15 @@ fn type_like_to_ctype(ty: &r2types::CTypeLike) -> r2dec::CType { } } -fn insert_known_signature_aliases( - known: &mut std::collections::HashMap, - name: &str, - sig: &r2types::FunctionType, -) { - if name.is_empty() { - return; - } - known.insert(name.to_string(), sig.clone()); - - for prefix in ["sym.imp.", "sym.", "imp.", "dbg.", "fcn."] { - if let Some(stripped) = name.strip_prefix(prefix) - && !stripped.is_empty() - { - known.insert(stripped.to_string(), sig.clone()); - } - } -} - -pub(crate) fn enrich_known_function_signatures_from_names( - type_facts: &mut r2types::FunctionTypeFacts, - function_names: &std::collections::HashMap, - ptr_bits: u32, -) { - let registry = r2types::SignatureRegistry::from_embedded_json(); - - for name in function_names.values() { - let mut candidates = vec![name.clone()]; - if let Some(sim_name) = normalize_sim_name(name) - && sim_name != name - && !candidates.iter().any(|candidate| candidate == sim_name) - { - candidates.push(sim_name.to_string()); - } - - let resolved = candidates.into_iter().find_map(|candidate| { - let mut arena = r2types::TypeArena::default(); - registry - .resolve(&candidate, &mut arena, ptr_bits) - .map(|sig| { - ( - candidate, - r2types::FunctionType { - return_type: r2types::to_c_type_like(&arena, sig.ret), - params: sig - .params - .into_iter() - .map(|param| r2types::to_c_type_like(&arena, param)) - .collect(), - variadic: sig.variadic, - }, - ) - }) - }); - - let Some((resolved_name, sig)) = resolved else { - continue; - }; - - insert_known_signature_aliases(&mut type_facts.known_function_signatures, name, &sig); - if resolved_name != *name { - insert_known_signature_aliases( - &mut type_facts.known_function_signatures, - &resolved_name, - &sig, - ); - } - } -} - -fn signature_cc_to_writeback(sig: &InferredSignatureCcJson) -> r2types::InferredSignature { - r2types::InferredSignature { +pub(crate) fn signature_to_json(sig: &r2types::InferredSignature) -> InferredSignatureCcJson { + InferredSignatureCcJson { function_name: sig.function_name.clone(), signature: sig.signature.clone(), ret_type: sig.ret_type.clone(), params: sig .params .iter() - .map(|param| r2types::InferredSignatureParam { + .map(|param| InferredParamJson { name: param.name.clone(), param_type: param.param_type.clone(), }) @@ -958,63 +238,39 @@ fn signature_cc_to_writeback(sig: &InferredSignatureCcJson) -> r2types::Inferred } } -fn var_prot_to_writeback(var: &VarProt) -> r2types::RecoveredVariable { - r2types::RecoveredVariable { - name: var.name.clone(), - kind: var.kind.clone(), - delta: var.delta, - var_type: var.var_type.clone(), - isarg: var.isarg, - reg: var.reg.clone(), - } -} +pub(crate) type VarProt = r2types::RecoveredVariable; +pub(crate) type TypeHintRank = r2types::TypeHintRank; +pub(crate) type TypeHint = r2types::TypeHint; -fn writeback_diagnostics_from_plugin( - diagnostics: crate::TypeWritebackDiagnosticsJson, -) -> r2types::TypeWritebackDiagnostics { - r2types::TypeWritebackDiagnostics { - conflicts: diagnostics.conflicts, - warnings: diagnostics.warnings, - solver_warnings: diagnostics.solver_warnings, - } +fn var_prot_to_writeback(var: &VarProt) -> r2types::RecoveredVariable { + var.clone() } -fn struct_decl_to_writeback(decl: crate::StructDeclCandidateJson) -> r2types::StructDeclCandidate { - r2types::StructDeclCandidate { - name: decl.name, - decl: decl.decl, - confidence: decl.confidence, - source: if decl.source == "external_type_db" { - r2types::StructDeclSource::ExternalTypeDb - } else { - r2types::StructDeclSource::LocalInferred - }, - fields: decl - .fields - .into_iter() - .map(|field| r2types::StructFieldCandidate { - name: field.name, - offset: field.offset, - field_type: field.field_type, - confidence: field.confidence, - }) - .collect(), - } +fn local_field_accesses_to_writeback( + accesses: Vec, +) -> Vec { + accesses + .into_iter() + .map(|access| r2types::LocalFieldAccessFact { + slot: access.arg_index, + field_offset: access.field_offset, + field_name: format!("f_{:x}", access.field_offset), + field_type: Some(r2types::size_to_type(access.access_size)), + }) + .collect() } -fn local_struct_artifacts_to_writeback( - struct_decls: Vec, - slot_type_overrides: std::collections::HashMap, - slot_field_profiles: std::collections::HashMap>, -) -> r2types::LocalStructArtifacts { - r2types::LocalStructArtifacts { - struct_decls: struct_decls - .into_iter() - .map(struct_decl_to_writeback) - .collect(), - slot_type_overrides, - slot_field_profiles, - } +fn recovered_signature_params_from_var_recovery( + params: Vec<&r2dec::variable::VarInfo>, +) -> Vec { + params + .into_iter() + .map(|param| r2types::RecoveredSignatureParam { + name: param.name.clone(), + ssa_var: param.ssa_var.clone(), + initial_ty: crate::ctype_to_type_like(¶m.ty), + }) + .collect() } fn build_var_recovery_ssa_blocks( @@ -1111,10 +367,11 @@ pub(crate) fn collect_detached_semantic_artifact( symbolic_scope: Option<&r2sym::PreparedFunctionScope>, ) -> Option { let analysis = build_function_analysis_from_parts(function_name, blocks, arch)?; - Some(collect_plugin_semantic_artifact( + Some(r2sym::compile_semantic_artifact_default_with_scope( + &z3::Context::thread_local(), &analysis.ssa_func, - arch, symbolic_scope, + arch, )) } @@ -1214,129 +471,39 @@ pub(crate) fn build_interproc_summary_set( pub(crate) fn infer_signature_cc_from_analysis( input: &FunctionInput<'_>, analysis: &FunctionAnalysis, -) -> Option { +) -> Option { let env = build_decompiler_env(&input.ctx); let pattern_ssa_blocks = analysis.pattern_ssa_func.local_ssa_blocks(); - let evidence_ctx = collect_signature_type_evidence_context(&pattern_ssa_blocks); let mut var_recovery = r2dec::VariableRecovery::new(&env.cfg.sp_name, &env.cfg.fp_name, env.cfg.ptr_size); var_recovery.recover(&analysis.ssa_func); - let mut type_inference = r2types::TypeInference::new(env.cfg.ptr_size); - type_inference.set_prepared_ssa(&analysis.ssa_func); - type_inference.infer_function(&analysis.ssa_func); - - let mut inferred_params: Vec = var_recovery - .parameters() - .into_iter() - .map(|v| { - let initial_ty = type_like_to_ctype(&type_inference.get_type(&v.ssa_var)); - let mut evidence = - crate::collect_type_evidence_for_var(&evidence_ctx, &v.ssa_var, &initial_ty); - if matches!(initial_ty, r2dec::CType::Void | r2dec::CType::Unknown) { - crate::merge_initial_type_evidence( - &type_like_to_ctype(&type_inference.type_from_size(v.ssa_var.size)), - &mut evidence, - ); - } - let ty = crate::resolve_evidence_driven_type( - initial_ty, - v.ssa_var.size, - env.ptr_bits, - &evidence, - ); - let arg_index = v - .name - .strip_prefix("arg") - .and_then(|n| n.parse::().ok()) - .unwrap_or(usize::MAX); - InferredParam { - name: v.name.clone(), - ty, - arg_index, - size_bytes: v.ssa_var.size, - evidence, - } - }) - .collect(); - - if input.ctx.semantic_metadata_enabled { + let pointer_arg_slots = if input.ctx.semantic_metadata_enabled { let reg_type_hints = collect_register_type_hints(input.blocks.as_slice(), input.ctx.disasm); let recovered_vars = recover_vars_from_ssa(&pattern_ssa_blocks, input.ctx.arch, ®_type_hints, true); - let pointer_arg_slots = collect_pointer_arg_slots(&recovered_vars); - merge_pointer_slot_evidence(&mut inferred_params, &pointer_arg_slots); - } + collect_pointer_arg_slots(&recovered_vars) + } else { + std::collections::BTreeSet::new() + }; + let recovered_params = recovered_signature_params_from_var_recovery(var_recovery.parameters()); - for param in &mut inferred_params { - param.ty = crate::resolve_evidence_driven_type( - param.ty.clone(), - param.size_bytes, - env.ptr_bits, - ¶m.evidence, - ); - } - - inferred_params.sort_by(|a, b| { - a.arg_index - .cmp(&b.arg_index) - .then_with(|| a.name.cmp(&b.name)) - }); - let mut used_param_names = HashSet::new(); - let params: Vec = inferred_params - .iter() - .enumerate() - .map(|(idx, p)| { - let fallback_idx = if p.arg_index == usize::MAX { - idx - } else { - p.arg_index - }; - InferredParamJson { - name: crate::normalize_inferred_param_name( - &p.name, - fallback_idx, - &mut used_param_names, - ), - param_type: crate::materialize_signature_ctype(p.ty.clone(), env.ptr_bits) - .to_string(), - } - }) - .collect(); - - let (ret_type, ret_evidence) = crate::infer_signature_return_type( - &analysis.ssa_func, - &type_inference, - env.ptr_bits, - &evidence_ctx, - ); - let ret_type = crate::materialize_signature_ctype(ret_type, env.ptr_bits); - let ret_type_str = ret_type.to_string(); - - let input_counts = crate::collect_version0_input_regs(&analysis.ssa_func); - let (callconv, callconv_confidence) = - crate::compute_callconv_inference(&env.arch_name, &input_counts); - let confidence = - crate::compute_signature_confidence(&inferred_params, &ret_type, &ret_evidence); - - let signature = crate::format_afs_signature(&input.function_name, &ret_type_str, ¶ms); - Some(InferredSignatureCcJson { - function_name: input.function_name.clone(), - signature, - ret_type: ret_type_str, - params, - callconv, - arch: env.arch_name, - confidence, - callconv_confidence, - }) -} + Some(r2types::infer_signature_from_prepared_ssa( + &input.function_name, + &env.arch_name, + env.ptr_bits, + &analysis.ssa_func, + &pattern_ssa_blocks, + &recovered_params, + &pointer_arg_slots, + )) +} #[allow(dead_code)] pub(crate) fn infer_signature_cc_inner( input: &FunctionInput<'_>, -) -> Option { +) -> Option { let analysis = build_function_analysis(input)?; infer_signature_cc_from_analysis(input, &analysis) } @@ -1462,26 +629,23 @@ pub(crate) fn build_function_analysis_artifact_from_analysis_with_semantic_artif .as_ref() .map(|arch| effective_ptr_bits(arch)) .unwrap_or(64); - let signature_cc = infer_signature_cc_from_analysis(input, &analysis)?; + let signature = infer_signature_cc_from_analysis(input, &analysis)?; let parsed_context = r2types::parse_external_context_json(external_context_json, ptr_bits); let pattern_ssa_blocks = analysis.pattern_ssa_func.local_ssa_blocks(); - let mut diagnostics = crate::TypeWritebackDiagnosticsJson::default(); - let raw_structs = crate::infer_structs_from_ssa( + let decompiler_env = build_decompiler_env(&input.ctx); + let decompiler_cfg = decompiler_env.cfg; + let mut diagnostics = r2types::TypeWritebackDiagnostics::default(); + let local_structs = r2types::infer_local_struct_artifacts_from_ssa( &pattern_ssa_blocks, - input.ctx.arch, + Some(decompiler_env.arch_name.as_str()), ptr_bits, &mut diagnostics, ); - let semantic_structs = crate::infer_structs_from_semantic_accesses( - &analysis.pattern_ssa_func, - &build_decompiler_env(&input.ctx).cfg, - ptr_bits, - &mut diagnostics, + let local_field_accesses = local_field_accesses_to_writeback( + r2dec::infer_local_struct_field_accesses(&analysis.pattern_ssa_func, &decompiler_cfg), ); - let (struct_decls, slot_type_overrides, slot_field_profiles) = - crate::merge_struct_inference_artifacts(raw_structs, semantic_structs); - let symbolic_facts = symbolic_semantic_facts_from_sym(&semantic_artifact); + let symbolic_facts = r2types::symbolic_semantic_facts_from_artifact(&semantic_artifact); let reg_type_hints = if input.ctx.semantic_metadata_enabled { collect_register_type_hints(input.blocks.as_slice(), input.ctx.disasm) @@ -1495,36 +659,27 @@ pub(crate) fn build_function_analysis_artifact_from_analysis_with_semantic_artif input.ctx.semantic_metadata_enabled, ); let recovered_vars = vars.iter().map(var_prot_to_writeback).collect::>(); - let mut local_structs = - local_struct_artifacts_to_writeback(struct_decls, slot_type_overrides, slot_field_profiles); - r2types::augment_local_struct_artifacts_with_symbolic_facts( - &mut local_structs, - &symbolic_facts, - ptr_bits, + let writeback = r2types::build_type_writeback_analysis_with_semantics( + r2types::TypeWritebackAnalysisInput { + function_name: &input.function_name, + ptr_bits, + inferred_signature: signature.clone(), + recovered_vars: &recovered_vars, + ssa_blocks: &pattern_ssa_blocks, + parsed_context, + local_structs, + interproc_summary_set: interproc_summary_set.clone(), + diagnostics, + }, + r2types::TypeWritebackSemanticInputs { + symbolic_facts: &symbolic_facts, + local_field_accesses: &local_field_accesses, + }, ); - let writeback = r2types::build_type_writeback_analysis(r2types::TypeWritebackAnalysisInput { - function_name: &input.function_name, - ptr_bits, - inferred_signature: signature_cc_to_writeback(&signature_cc), - recovered_vars: &recovered_vars, - ssa_blocks: &pattern_ssa_blocks, - parsed_context, - local_structs, - interproc_summary_set: interproc_summary_set.clone(), - diagnostics: writeback_diagnostics_from_plugin(diagnostics), - }); - let mut type_facts = writeback.type_facts; - if symbolic_facts.diagnostics.branches_pruned > 0 { - type_facts.diagnostics.push(format!( - "symbolic pruned {} branch arm(s)", - symbolic_facts.diagnostics.branches_pruned - )); - } - type_facts.symbolic_facts = symbolic_facts; Some(FunctionAnalysisArtifact { ssa_func: analysis.ssa_func, pattern_ssa_func: analysis.pattern_ssa_func, - type_facts, + type_facts: writeback.type_facts, writeback_plan: writeback.plan, interproc_summary_set, semantic_artifact: Some(semantic_artifact), @@ -1538,8 +693,12 @@ pub(crate) fn build_function_analysis_artifact_from_analysis( interproc_summary_set: Option, symbolic_scope: Option<&r2sym::PreparedFunctionScope>, ) -> Option { - let semantic_artifact = - collect_plugin_semantic_artifact(&analysis.ssa_func, input.ctx.arch, symbolic_scope); + let semantic_artifact = r2sym::compile_semantic_artifact_default_with_scope( + &z3::Context::thread_local(), + &analysis.ssa_func, + symbolic_scope, + input.ctx.arch, + ); build_function_analysis_artifact_from_analysis_with_semantic_artifact( input, analysis, @@ -1743,132 +902,46 @@ pub(crate) fn build_detached_function_analysis_artifact_with_scope_and_semantics let analysis = build_function_analysis_from_parts(function_name, blocks, arch)?; let pattern_ssa_blocks = analysis.pattern_ssa_func.local_ssa_blocks(); - let arch_name = normalize_sig_arch_name(arch).unwrap_or_else(|| "unknown".to_string()); - let cfg = decompiler_config_for_arch_name(&arch_name, ptr_bits); - let evidence_ctx = collect_signature_type_evidence_context(&pattern_ssa_blocks); + let (arch_name, _, cfg) = r2dec::DecompilerConfig::for_arch(arch); let mut var_recovery = r2dec::VariableRecovery::new(&cfg.sp_name, &cfg.fp_name, cfg.ptr_size); var_recovery.recover(&analysis.ssa_func); - let mut type_inference = r2types::TypeInference::new(cfg.ptr_size); - type_inference.set_prepared_ssa(&analysis.ssa_func); - type_inference.infer_function(&analysis.ssa_func); - - let mut inferred_params: Vec = var_recovery - .parameters() - .into_iter() - .map(|v| { - let initial_ty = type_like_to_ctype(&type_inference.get_type(&v.ssa_var)); - let mut evidence = - crate::collect_type_evidence_for_var(&evidence_ctx, &v.ssa_var, &initial_ty); - if matches!(initial_ty, r2dec::CType::Void | r2dec::CType::Unknown) { - crate::merge_initial_type_evidence( - &type_like_to_ctype(&type_inference.type_from_size(v.ssa_var.size)), - &mut evidence, - ); - } - let ty = crate::resolve_evidence_driven_type( - initial_ty, - v.ssa_var.size, - ptr_bits, - &evidence, - ); - let arg_index = v - .name - .strip_prefix("arg") - .and_then(|n| n.parse::().ok()) - .unwrap_or(usize::MAX); - InferredParam { - name: v.name.clone(), - ty, - arg_index, - size_bytes: v.ssa_var.size, - evidence, - } - }) - .collect(); - - if semantic_metadata_enabled { + let pointer_arg_slots = if semantic_metadata_enabled { let recovered_vars = recover_vars_from_ssa(&pattern_ssa_blocks, arch, reg_type_hints, true); - let pointer_arg_slots = collect_pointer_arg_slots(&recovered_vars); - merge_pointer_slot_evidence(&mut inferred_params, &pointer_arg_slots); - } - - for param in &mut inferred_params { - param.ty = crate::resolve_evidence_driven_type( - param.ty.clone(), - param.size_bytes, - ptr_bits, - ¶m.evidence, - ); - } - - inferred_params.sort_by(|a, b| { - a.arg_index - .cmp(&b.arg_index) - .then_with(|| a.name.cmp(&b.name)) - }); - let mut used_param_names = HashSet::new(); - let params: Vec = inferred_params - .iter() - .enumerate() - .map(|(idx, p)| { - let fallback_idx = if p.arg_index == usize::MAX { - idx - } else { - p.arg_index - }; - InferredParamJson { - name: crate::normalize_inferred_param_name( - &p.name, - fallback_idx, - &mut used_param_names, - ), - param_type: crate::materialize_signature_ctype(p.ty.clone(), ptr_bits).to_string(), - } - }) - .collect(); - - let (ret_type, ret_evidence) = crate::infer_signature_return_type( - &analysis.ssa_func, - &type_inference, + collect_pointer_arg_slots(&recovered_vars) + } else { + std::collections::BTreeSet::new() + }; + let recovered_params = recovered_signature_params_from_var_recovery(var_recovery.parameters()); + let signature = r2types::infer_signature_from_prepared_ssa( + function_name, + &arch_name, ptr_bits, - &evidence_ctx, + &analysis.ssa_func, + &pattern_ssa_blocks, + &recovered_params, + &pointer_arg_slots, ); - let ret_type = crate::materialize_signature_ctype(ret_type, ptr_bits); - let signature_cc = InferredSignatureCcJson { - function_name: function_name.to_string(), - signature: crate::format_afs_signature(function_name, &ret_type.to_string(), ¶ms), - ret_type: ret_type.to_string(), - params, - callconv: crate::compute_callconv_inference( - &arch_name, - &crate::collect_version0_input_regs(&analysis.ssa_func), - ) - .0, - arch: arch_name.clone(), - confidence: crate::compute_signature_confidence(&inferred_params, &ret_type, &ret_evidence), - callconv_confidence: crate::compute_callconv_inference( - &arch_name, - &crate::collect_version0_input_regs(&analysis.ssa_func), - ) - .1, - }; let parsed_context = r2types::parse_external_context_json(external_context_json, ptr_bits); - let mut diagnostics = crate::TypeWritebackDiagnosticsJson::default(); - let raw_structs = - crate::infer_structs_from_ssa(&pattern_ssa_blocks, arch, ptr_bits, &mut diagnostics); - let semantic_structs = crate::infer_structs_from_semantic_accesses( - &analysis.pattern_ssa_func, - &cfg, + let mut diagnostics = r2types::TypeWritebackDiagnostics::default(); + let local_structs = r2types::infer_local_struct_artifacts_from_ssa( + &pattern_ssa_blocks, + Some(arch_name.as_str()), ptr_bits, &mut diagnostics, ); - let (struct_decls, slot_type_overrides, slot_field_profiles) = - crate::merge_struct_inference_artifacts(raw_structs, semantic_structs); + let local_field_accesses = local_field_accesses_to_writeback( + r2dec::infer_local_struct_field_accesses(&analysis.pattern_ssa_func, &cfg), + ); let semantic_artifact = precomputed_semantic_artifact.unwrap_or_else(|| { - collect_plugin_semantic_artifact(&analysis.ssa_func, arch, symbolic_scope) + r2sym::compile_semantic_artifact_default_with_scope( + &z3::Context::thread_local(), + &analysis.ssa_func, + symbolic_scope, + arch, + ) }); - let symbolic_facts = symbolic_semantic_facts_from_sym(&semantic_artifact); + let symbolic_facts = r2types::symbolic_semantic_facts_from_artifact(&semantic_artifact); let vars = recover_vars_from_ssa( &pattern_ssa_blocks, arch, @@ -1876,36 +949,27 @@ pub(crate) fn build_detached_function_analysis_artifact_with_scope_and_semantics semantic_metadata_enabled, ); let recovered_vars = vars.iter().map(var_prot_to_writeback).collect::>(); - let mut local_structs = - local_struct_artifacts_to_writeback(struct_decls, slot_type_overrides, slot_field_profiles); - r2types::augment_local_struct_artifacts_with_symbolic_facts( - &mut local_structs, - &symbolic_facts, - ptr_bits, + let writeback = r2types::build_type_writeback_analysis_with_semantics( + r2types::TypeWritebackAnalysisInput { + function_name, + ptr_bits, + inferred_signature: signature.clone(), + recovered_vars: &recovered_vars, + ssa_blocks: &pattern_ssa_blocks, + parsed_context, + local_structs, + interproc_summary_set: None, + diagnostics, + }, + r2types::TypeWritebackSemanticInputs { + symbolic_facts: &symbolic_facts, + local_field_accesses: &local_field_accesses, + }, ); - let writeback = r2types::build_type_writeback_analysis(r2types::TypeWritebackAnalysisInput { - function_name, - ptr_bits, - inferred_signature: signature_cc_to_writeback(&signature_cc), - recovered_vars: &recovered_vars, - ssa_blocks: &pattern_ssa_blocks, - parsed_context, - local_structs, - interproc_summary_set: None, - diagnostics: writeback_diagnostics_from_plugin(diagnostics), - }); - let mut type_facts = writeback.type_facts; - if symbolic_facts.diagnostics.branches_pruned > 0 { - type_facts.diagnostics.push(format!( - "symbolic pruned {} branch arm(s)", - symbolic_facts.diagnostics.branches_pruned - )); - } - type_facts.symbolic_facts = symbolic_facts; let artifact = FunctionAnalysisArtifact { ssa_func: analysis.ssa_func, pattern_ssa_func: analysis.pattern_ssa_func, - type_facts, + type_facts: writeback.type_facts, writeback_plan: writeback.plan, interproc_summary_set: None, semantic_artifact: Some(semantic_artifact), @@ -1916,18 +980,6 @@ pub(crate) fn build_detached_function_analysis_artifact_with_scope_and_semantics Some(rename_function_analysis_artifact(artifact, function_name)) } -#[derive(serde::Serialize)] -pub(crate) struct VarProt { - pub(crate) name: String, - pub(crate) kind: String, - pub(crate) delta: i64, - #[serde(rename = "type")] - pub(crate) var_type: String, - pub(crate) isarg: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) reg: Option, -} - #[derive(Debug, serde::Serialize)] pub(crate) struct DataRef { pub(crate) from: u64, @@ -1936,43 +988,12 @@ pub(crate) struct DataRef { pub(crate) ref_type: String, } -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub(crate) enum TypeHintRank { - Integer = 1, - Float = 2, - Pointer = 3, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub(crate) struct TypeHint { - pub(crate) rank: TypeHintRank, - pub(crate) ty: String, -} - -impl TypeHint { - pub(crate) fn pointer() -> Self { - Self { - rank: TypeHintRank::Pointer, - ty: "void *".to_string(), - } - } -} - -fn incoming_hint_should_replace(current: &TypeHint, incoming: &TypeHint) -> bool { - incoming.rank > current.rank || (incoming.rank == current.rank && incoming.ty < current.ty) -} - pub(crate) fn merge_type_hint( hints: &mut std::collections::HashMap, key: String, incoming: TypeHint, ) { - match hints.get(&key) { - Some(current) if !incoming_hint_should_replace(current, &incoming) => {} - _ => { - hints.insert(key, incoming); - } - } + r2types::merge_type_hint(hints, key, incoming); } fn size_to_signed_int_type(size: u32) -> String { @@ -2066,989 +1087,44 @@ pub(crate) fn collect_register_type_hints( hints } -pub(crate) const X86_ARG_REGS: &[(&str, &[&str])] = &[ - ("rdi", &["rdi", "edi", "di", "dil"]), - ("rsi", &["rsi", "esi", "si", "sil"]), - ("rdx", &["rdx", "edx", "dx", "dl", "dh"]), - ("rcx", &["rcx", "ecx", "cx", "cl", "ch"]), - ("r8", &["r8", "r8d", "r8w", "r8b"]), - ("r9", &["r9", "r9d", "r9w", "r9b"]), -]; -const RISCV_ARG_REGS: &[(&str, &[&str])] = &[ - ("a0", &["a0", "x10"]), - ("a1", &["a1", "x11"]), - ("a2", &["a2", "x12"]), - ("a3", &["a3", "x13"]), - ("a4", &["a4", "x14"]), - ("a5", &["a5", "x15"]), - ("a6", &["a6", "x16"]), - ("a7", &["a7", "x17"]), -]; -const ARM64_ARG_REGS: &[(&str, &[&str])] = &[ - ("x0", &["x0", "w0"]), - ("x1", &["x1", "w1"]), - ("x2", &["x2", "w2"]), - ("x3", &["x3", "w3"]), - ("x4", &["x4", "w4"]), - ("x5", &["x5", "w5"]), - ("x6", &["x6", "w6"]), - ("x7", &["x7", "w7"]), -]; -const ARM32_ARG_REGS: &[(&str, &[&str])] = &[ - ("r0", &["r0"]), - ("r1", &["r1"]), - ("r2", &["r2"]), - ("r3", &["r3"]), -]; -const MIPS_ARG_REGS: &[(&str, &[&str])] = &[ - ("a0", &["a0", "$a0", "r4"]), - ("a1", &["a1", "$a1", "r5"]), - ("a2", &["a2", "$a2", "r6"]), - ("a3", &["a3", "$a3", "r7"]), -]; -const X86_STACK_BASES: &[&str] = &["rbp", "rsp", "ebp", "esp"]; -pub(crate) const X86_FRAME_BASES: &[&str] = &["rbp", "ebp"]; -const RISCV_STACK_BASES: &[&str] = &["sp", "s0", "fp", "x2", "x8"]; -const RISCV_FRAME_BASES: &[&str] = &["s0", "fp", "x8"]; -const ARM64_STACK_BASES: &[&str] = &["sp", "x29", "fp"]; -const ARM64_FRAME_BASES: &[&str] = &["x29", "fp"]; -const ARM32_STACK_BASES: &[&str] = &["sp", "r11", "fp"]; -const ARM32_FRAME_BASES: &[&str] = &["r11", "fp"]; -const MIPS_STACK_BASES: &[&str] = &["sp", "$sp", "fp", "$fp", "s8", "$s8"]; -const MIPS_FRAME_BASES: &[&str] = &["fp", "$fp", "s8", "$s8"]; -const GENERIC_STACK_BASES: &[&str] = &["sp", "fp", "bp", "s0", "x2", "x8", "rbp", "rsp"]; -const GENERIC_FRAME_BASES: &[&str] = &["fp", "bp", "s0", "x8", "rbp"]; - -type ArgAliasMap = &'static [(&'static str, &'static [&'static str])]; -type BaseRegList = &'static [&'static str]; +#[cfg(test)] +pub(crate) const X86_ARG_REGS: &[(&str, &[&str])] = r2types::X86_ARG_REGS; +#[cfg(test)] +pub(crate) const X86_FRAME_BASES: &[&str] = r2types::X86_FRAME_BASES; +#[cfg(test)] +type ArgAliasMap = r2types::ArgAliasMap; +#[cfg(test)] +type BaseRegList = r2types::BaseRegList; +#[cfg(test)] pub(crate) fn recover_vars_arch_profile( arch: Option<&ArchSpec>, ) -> (ArgAliasMap, BaseRegList, BaseRegList) { - let Some(arch) = arch else { - return (&[], GENERIC_STACK_BASES, GENERIC_FRAME_BASES); - }; - - let arch_name = arch.name.to_ascii_lowercase(); - if arch_name.contains("x86") { - return (X86_ARG_REGS, X86_STACK_BASES, X86_FRAME_BASES); - } - if arch_name.contains("aarch64") || arch_name.contains("arm64") { - return (ARM64_ARG_REGS, ARM64_STACK_BASES, ARM64_FRAME_BASES); - } - if arch_name == "arm" || arch_name.starts_with("armv") { - return (ARM32_ARG_REGS, ARM32_STACK_BASES, ARM32_FRAME_BASES); - } - if arch_name.contains("riscv") || arch_name.starts_with("rv") { - return (RISCV_ARG_REGS, RISCV_STACK_BASES, RISCV_FRAME_BASES); - } - if arch_name.contains("mips") { - return (MIPS_ARG_REGS, MIPS_STACK_BASES, MIPS_FRAME_BASES); - } - - (&[], GENERIC_STACK_BASES, GENERIC_FRAME_BASES) + r2types::recover_vars_arch_profile(arch.map(|spec| spec.name.as_str())) } pub(crate) fn ssa_var_key(var: &r2ssa::SSAVar) -> String { - format!("{}_{}", var.name.to_ascii_lowercase(), var.version) + r2types::ssa_var_key(var) } +#[cfg(test)] pub(crate) fn ssa_var_block_key(block_addr: u64, var: &r2ssa::SSAVar) -> String { - format!("{}@{block_addr:x}", ssa_var_key(var)) -} - -fn ssa_var_is_const(var: &r2ssa::SSAVar) -> bool { - parse_const_value(&var.name).is_some() -} - -fn ssa_var_is_register_like(name: &str) -> bool { - let lower = name.to_ascii_lowercase(); - !(lower.starts_with("tmp:") - || lower.starts_with("const:") - || lower.starts_with("ram:") - || lower.starts_with("space")) + r2types::ssa_var_block_key(block_addr, var) } +#[cfg(test)] pub(crate) fn scalar_register_family_key(name: &str) -> String { - let lower = name.to_ascii_lowercase(); - - if let Some(idx) = lower.strip_prefix('x').or_else(|| lower.strip_prefix('w')) - && !idx.is_empty() - && idx.chars().all(|ch| ch.is_ascii_digit()) - { - return format!("aarch64:gpr:{idx}"); - } - - match lower.as_str() { - "fp" => "aarch64:gpr:29".to_string(), - "lr" => "aarch64:gpr:30".to_string(), - "sp" | "wsp" => "aarch64:sp".to_string(), - "xzr" | "wzr" => "aarch64:zr".to_string(), - _ => lower, - } -} - -fn collect_register_version_keys( - ssa_blocks: &[r2ssa::SSABlock], -) -> std::collections::HashMap> { - use std::collections::HashMap; - - let mut reg_versions: HashMap> = HashMap::new(); - for block in ssa_blocks { - for op in &block.ops { - let mut collect_var = |var: &r2ssa::SSAVar| { - if !ssa_var_is_register_like(&var.name) { - return; - } - let reg_name = scalar_register_family_key(&var.name); - reg_versions - .entry(reg_name) - .or_default() - .push(ssa_var_key(var)); - }; - if let Some(dst) = op.dst() { - collect_var(dst); - } - op.for_each_source(&mut collect_var); - } - } - for keys in reg_versions.values_mut() { - keys.sort(); - keys.dedup(); - } - reg_versions -} - -fn set_width_hint_prefer_narrower( - width_hints: &mut std::collections::HashMap, - key: String, - bits: u32, -) -> bool { - if bits == 0 { - return false; - } - match width_hints.get(&key).copied() { - Some(current) if current == bits => false, - Some(current) if current > 0 && current <= bits => false, - _ => { - width_hints.insert(key, bits); - true - } - } -} - -fn normalize_register_family_width_hints( - register_versions: &std::collections::HashMap>, - width_hints: &mut std::collections::HashMap, -) -> bool { - let mut changed = false; - for reg_keys in register_versions.values() { - let max_bits = reg_keys - .iter() - .filter_map(|key| width_hints.get(key).copied()) - .max() - .unwrap_or(0); - let min_bits = reg_keys - .iter() - .filter_map(|key| { - let current = width_hints.get(key).copied().unwrap_or(0); - let natural = register_key_natural_bits(key).unwrap_or(0); - let candidate = match (current, natural) { - (0, 0) => 0, - (0, natural) => natural, - (current, 0) => current, - (current, natural) => current.min(natural), - }; - (candidate > 0).then_some(candidate) - }) - .min() - .unwrap_or(0); - let preferred_bits = if min_bits > 0 && max_bits == 64 && min_bits <= 32 { - min_bits - } else { - max_bits - }; - for key in reg_keys { - changed |= set_width_hint_prefer_narrower(width_hints, key.clone(), preferred_bits); - } - } - changed -} - -fn register_key_natural_bits(key: &str) -> Option { - let reg = key.split('_').next()?; - if let Some(idx) = reg.strip_prefix('w') - && !idx.is_empty() - && idx.chars().all(|ch| ch.is_ascii_digit()) - { - return Some(32); - } - if let Some(idx) = reg.strip_prefix('x') - && !idx.is_empty() - && idx.chars().all(|ch| ch.is_ascii_digit()) - { - return Some(64); - } - match reg { - "wsp" | "wzr" => Some(32), - "sp" | "fp" | "lr" | "xzr" => Some(64), - _ => None, - } -} - -fn propagate_normalized_scalar_result_widths( - ssa_blocks: &[r2ssa::SSABlock], - width_hints: &mut std::collections::HashMap, -) -> bool { - let mut changed = false; - for block in ssa_blocks { - for op in &block.ops { - let (dst, source_bits) = match op { - r2ssa::SSAOp::Copy { dst, src } - | r2ssa::SSAOp::Cast { dst, src } - | r2ssa::SSAOp::New { dst, src } - | r2ssa::SSAOp::IntZExt { dst, src } - | r2ssa::SSAOp::IntSExt { dst, src } - | r2ssa::SSAOp::Subpiece { dst, src, .. } => { - let bits = width_hints.get(&ssa_var_key(src)).copied().unwrap_or(0); - (dst, bits) - } - r2ssa::SSAOp::IntAdd { dst, a, b } - | r2ssa::SSAOp::IntSub { dst, a, b } - | r2ssa::SSAOp::IntMult { dst, a, b } - | r2ssa::SSAOp::IntDiv { dst, a, b } - | r2ssa::SSAOp::IntSDiv { dst, a, b } - | r2ssa::SSAOp::IntRem { dst, a, b } - | r2ssa::SSAOp::IntSRem { dst, a, b } - | r2ssa::SSAOp::IntAnd { dst, a, b } - | r2ssa::SSAOp::IntOr { dst, a, b } - | r2ssa::SSAOp::IntXor { dst, a, b } - | r2ssa::SSAOp::IntLeft { dst, a, b } - | r2ssa::SSAOp::IntRight { dst, a, b } - | r2ssa::SSAOp::IntSRight { dst, a, b } => { - let bits = [a, b] - .into_iter() - .filter(|var| !ssa_var_is_const(var)) - .filter_map(|var| width_hints.get(&ssa_var_key(var)).copied()) - .filter(|bits| *bits > 0) - .max() - .unwrap_or(0); - (dst, bits) - } - _ => continue, - }; - changed |= set_width_hint_prefer_narrower(width_hints, ssa_var_key(dst), source_bits); - } - } - changed -} - -fn ssa_var_is_stack_base(var: &r2ssa::SSAVar) -> bool { - matches!( - var.name.to_ascii_lowercase().as_str(), - "rbp" | "rsp" | "ebp" | "esp" | "sp" | "fp" | "bp" | "s0" | "x2" | "x8" - ) -} - -fn infer_pointer_width_bytes(ssa_blocks: &[r2ssa::SSABlock]) -> u32 { - let mut width = 0u32; - for block in ssa_blocks { - for op in &block.ops { - if let Some(dst) = op.dst() - && ssa_var_is_stack_base(dst) - { - width = width.max(dst.size); - } - op.for_each_source(|src| { - if ssa_var_is_stack_base(src) { - width = width.max(src.size); - } - }); - } - } - if width == 0 { 8 } else { width } -} - -fn infer_index_like_var_keys(ssa_blocks: &[r2ssa::SSABlock]) -> std::collections::HashSet { - use std::collections::HashSet; - - let mut index_like: HashSet = HashSet::new(); - for block in ssa_blocks { - for op in &block.ops { - if let r2ssa::SSAOp::IntSExt { dst, src } | r2ssa::SSAOp::IntZExt { dst, src } = op - && src.size < dst.size - { - index_like.insert(ssa_var_key(dst)); - } - } - } - - let mut changed = true; - while changed { - changed = false; - for block in ssa_blocks { - for op in &block.ops { - match op { - r2ssa::SSAOp::Copy { dst, src } - | r2ssa::SSAOp::Cast { dst, src } - | r2ssa::SSAOp::New { dst, src } => { - if index_like.contains(&ssa_var_key(src)) { - changed |= index_like.insert(ssa_var_key(dst)); - } - } - r2ssa::SSAOp::IntMult { dst, a, b } => { - let a_key = ssa_var_key(a); - let b_key = ssa_var_key(b); - let a_is_scaled_const = ssa_var_is_const(a); - let b_is_scaled_const = ssa_var_is_const(b); - if (index_like.contains(&a_key) && ssa_var_is_const(b)) - || (index_like.contains(&b_key) && ssa_var_is_const(a)) - || (a_is_scaled_const && !b_is_scaled_const) - || (b_is_scaled_const && !a_is_scaled_const) - { - changed |= index_like.insert(ssa_var_key(dst)); - } - } - r2ssa::SSAOp::IntLeft { dst, a, b } => { - let shift_amount = parse_const_value(&b.name).unwrap_or(u64::MAX); - if (index_like.contains(&ssa_var_key(a)) && ssa_var_is_const(b)) - || shift_amount <= 6 - { - changed |= index_like.insert(ssa_var_key(dst)); - } - } - _ => {} - } - } - } - } - - index_like -} - -fn infer_pointer_var_keys_from_ssa( - ssa_blocks: &[r2ssa::SSABlock], -) -> std::collections::HashSet { - use std::collections::{HashMap, HashSet}; - - let mut pointer_vars: HashSet = HashSet::new(); - let register_versions = collect_register_version_keys(ssa_blocks); - let index_like_vars = infer_index_like_var_keys(ssa_blocks); - let pointer_width = infer_pointer_width_bytes(ssa_blocks); - let mut stack_addr_slots: HashMap = HashMap::new(); - let mut pointer_stack_slots: HashSet = HashSet::new(); - - for block in ssa_blocks { - for op in &block.ops { - match op { - r2ssa::SSAOp::IntAdd { dst, a, b } | r2ssa::SSAOp::IntSub { dst, a, b } => { - let a_is_stack = ssa_var_is_stack_base(a); - let b_is_stack = ssa_var_is_stack_base(b); - let a_const = parse_const_value(&a.name); - let b_const = parse_const_value(&b.name); - - if a_is_stack && b_const.is_some() { - let raw = b_const.unwrap_or(0); - let offset = if matches!(op, r2ssa::SSAOp::IntSub { .. }) { - -(raw as i64) - } else { - raw as i64 - }; - stack_addr_slots.insert( - ssa_var_block_key(block.addr, dst), - format!("{}:{offset}", a.name.to_ascii_lowercase()), - ); - } else if matches!(op, r2ssa::SSAOp::IntAdd { .. }) - && b_is_stack - && a_const.is_some() - { - let raw = a_const.unwrap_or(0); - stack_addr_slots.insert( - ssa_var_block_key(block.addr, dst), - format!("{}:{}", b.name.to_ascii_lowercase(), raw as i64), - ); - } - } - r2ssa::SSAOp::Load { addr, .. } - | r2ssa::SSAOp::Store { addr, .. } - | r2ssa::SSAOp::LoadLinked { addr, .. } - | r2ssa::SSAOp::StoreConditional { addr, .. } - | r2ssa::SSAOp::LoadGuarded { addr, .. } - | r2ssa::SSAOp::StoreGuarded { addr, .. } - | r2ssa::SSAOp::AtomicCAS { addr, .. } => { - pointer_vars.insert(ssa_var_key(addr)); - } - _ => {} - } - } - } - - let mut changed = true; - while changed { - changed = false; - for block in ssa_blocks { - for op in &block.ops { - match op { - r2ssa::SSAOp::Phi { dst, sources } => { - let dst_key = ssa_var_key(dst); - let dst_is_pointer = pointer_vars.contains(&dst_key); - let any_source_pointer = sources - .iter() - .any(|src| pointer_vars.contains(&ssa_var_key(src))); - - if any_source_pointer { - changed |= pointer_vars.insert(dst_key.clone()); - } - if dst_is_pointer { - for src in sources { - changed |= pointer_vars.insert(ssa_var_key(src)); - } - } - } - r2ssa::SSAOp::Copy { dst, src } - | r2ssa::SSAOp::Cast { dst, src } - | r2ssa::SSAOp::New { dst, src } => { - let dst_key = ssa_var_key(dst); - let src_key = ssa_var_key(src); - if pointer_vars.contains(&dst_key) { - changed |= pointer_vars.insert(src_key.clone()); - } - if pointer_vars.contains(&src_key) { - changed |= pointer_vars.insert(dst_key); - } - } - r2ssa::SSAOp::IntAdd { dst, a, b } | r2ssa::SSAOp::IntSub { dst, a, b } => { - let dst_key = ssa_var_key(dst); - let a_key = ssa_var_key(a); - let b_key = ssa_var_key(b); - let a_is_const = ssa_var_is_const(a); - let b_is_const = ssa_var_is_const(b); - let a_index_like = index_like_vars.contains(&a_key); - let b_index_like = index_like_vars.contains(&b_key); - - if pointer_vars.contains(&dst_key) { - if a_is_const && !b_is_const { - changed |= pointer_vars.insert(b_key.clone()); - } else if b_is_const && !a_is_const { - changed |= pointer_vars.insert(a_key.clone()); - } else if a_index_like && !b_index_like { - changed |= pointer_vars.insert(b_key.clone()); - } else if b_index_like && !a_index_like { - changed |= pointer_vars.insert(a_key.clone()); - } else if a_index_like && b_index_like { - let a_is_tmp = a.name.starts_with("tmp:"); - let b_is_tmp = b.name.starts_with("tmp:"); - if a_is_tmp && !b_is_tmp { - changed |= pointer_vars.insert(b_key.clone()); - } else if b_is_tmp && !a_is_tmp { - changed |= pointer_vars.insert(a_key.clone()); - } - } - } - - if pointer_vars.contains(&a_key) && b_is_const { - changed |= pointer_vars.insert(dst_key.clone()); - } - if pointer_vars.contains(&b_key) && a_is_const { - changed |= pointer_vars.insert(dst_key.clone()); - } - if pointer_vars.contains(&a_key) && index_like_vars.contains(&b_key) { - changed |= pointer_vars.insert(dst_key.clone()); - } - if pointer_vars.contains(&b_key) && index_like_vars.contains(&a_key) { - changed |= pointer_vars.insert(dst_key.clone()); - } - } - r2ssa::SSAOp::PtrAdd { dst, base, .. } - | r2ssa::SSAOp::PtrSub { dst, base, .. } => { - let dst_key = ssa_var_key(dst); - let base_key = ssa_var_key(base); - if pointer_vars.contains(&dst_key) { - changed |= pointer_vars.insert(base_key.clone()); - } - if pointer_vars.contains(&base_key) { - changed |= pointer_vars.insert(dst_key); - } - } - r2ssa::SSAOp::SegmentOp { dst, offset, .. } => { - let dst_key = ssa_var_key(dst); - let offset_key = ssa_var_key(offset); - if pointer_vars.contains(&dst_key) { - changed |= pointer_vars.insert(offset_key.clone()); - } - if pointer_vars.contains(&offset_key) { - changed |= pointer_vars.insert(dst_key); - } - } - r2ssa::SSAOp::Store { addr, val, .. } => { - if let Some(slot) = - stack_addr_slots.get(&ssa_var_block_key(block.addr, addr)) - { - let val_key = ssa_var_key(val); - if val.size >= pointer_width && pointer_vars.contains(&val_key) { - changed |= pointer_stack_slots.insert(slot.clone()); - } - if val.size >= pointer_width && pointer_stack_slots.contains(slot) { - changed |= pointer_vars.insert(val_key); - } - } - } - r2ssa::SSAOp::Load { dst, addr, .. } => { - if let Some(slot) = - stack_addr_slots.get(&ssa_var_block_key(block.addr, addr)) - { - let dst_key = ssa_var_key(dst); - if dst.size >= pointer_width && pointer_stack_slots.contains(slot) { - changed |= pointer_vars.insert(dst_key.clone()); - } - if dst.size >= pointer_width && pointer_vars.contains(&dst_key) { - changed |= pointer_stack_slots.insert(slot.clone()); - } - } - } - _ => {} - } - } - } - - for reg_keys in register_versions.values() { - if reg_keys.iter().any(|key| pointer_vars.contains(key)) { - for key in reg_keys { - changed |= pointer_vars.insert(key.clone()); - } - } - } - } - - pointer_vars -} - -fn merge_width_hint( - width_hints: &mut std::collections::HashMap, - var: &r2ssa::SSAVar, - bits: u32, -) { - let entry = width_hints.entry(ssa_var_key(var)).or_insert(0); - *entry = (*entry).max(bits.max(var.size.saturating_mul(8))); -} - -fn mark_scalar_var( - vars: &mut std::collections::HashSet, - width_hints: &mut std::collections::HashMap, - var: &r2ssa::SSAVar, -) { - vars.insert(ssa_var_key(var)); - merge_width_hint(width_hints, var, var.size.saturating_mul(8)); -} - -fn infer_scalar_var_evidence_from_ssa( - ssa_blocks: &[r2ssa::SSABlock], -) -> ( - std::collections::HashSet, - std::collections::HashSet, - std::collections::HashSet, - std::collections::HashMap, -) { - use std::collections::{HashMap, HashSet}; - - let register_versions = collect_register_version_keys(ssa_blocks); - let mut scalar_proven: HashSet = HashSet::new(); - let mut scalar_likely: HashSet = HashSet::new(); - let mut bool_like: HashSet = HashSet::new(); - let mut width_hints: HashMap = HashMap::new(); - - for block in ssa_blocks { - for op in &block.ops { - match op { - r2ssa::SSAOp::IntMult { a, b, .. } - | r2ssa::SSAOp::IntDiv { a, b, .. } - | r2ssa::SSAOp::IntSDiv { a, b, .. } - | r2ssa::SSAOp::IntRem { a, b, .. } - | r2ssa::SSAOp::IntSRem { a, b, .. } - | r2ssa::SSAOp::IntAnd { a, b, .. } - | r2ssa::SSAOp::IntOr { a, b, .. } - | r2ssa::SSAOp::IntXor { a, b, .. } - | r2ssa::SSAOp::IntLeft { a, b, .. } - | r2ssa::SSAOp::IntRight { a, b, .. } - | r2ssa::SSAOp::IntSRight { a, b, .. } - | r2ssa::SSAOp::IntCarry { a, b, .. } - | r2ssa::SSAOp::IntSCarry { a, b, .. } - | r2ssa::SSAOp::IntSBorrow { a, b, .. } => { - mark_scalar_var(&mut scalar_proven, &mut width_hints, a); - mark_scalar_var(&mut scalar_proven, &mut width_hints, b); - } - r2ssa::SSAOp::IntNegate { src, .. } - | r2ssa::SSAOp::IntNot { src, .. } - | r2ssa::SSAOp::PopCount { src, .. } - | r2ssa::SSAOp::Lzcount { src, .. } => { - mark_scalar_var(&mut scalar_proven, &mut width_hints, src); - } - r2ssa::SSAOp::PtrAdd { index, .. } | r2ssa::SSAOp::PtrSub { index, .. } => { - mark_scalar_var(&mut scalar_proven, &mut width_hints, index); - } - r2ssa::SSAOp::IntAdd { a, b, .. } | r2ssa::SSAOp::IntSub { a, b, .. } => { - mark_scalar_var(&mut scalar_likely, &mut width_hints, a); - mark_scalar_var(&mut scalar_likely, &mut width_hints, b); - } - r2ssa::SSAOp::BoolNot { dst, src } => { - mark_scalar_var(&mut scalar_likely, &mut width_hints, src); - bool_like.insert(ssa_var_key(src)); - bool_like.insert(ssa_var_key(dst)); - merge_width_hint(&mut width_hints, dst, 1); - } - r2ssa::SSAOp::CBranch { cond, .. } - | r2ssa::SSAOp::LoadGuarded { guard: cond, .. } - | r2ssa::SSAOp::StoreGuarded { guard: cond, .. } => { - mark_scalar_var(&mut scalar_likely, &mut width_hints, cond); - bool_like.insert(ssa_var_key(cond)); - merge_width_hint(&mut width_hints, cond, 1); - } - r2ssa::SSAOp::FloatNeg { src, .. } - | r2ssa::SSAOp::FloatAbs { src, .. } - | r2ssa::SSAOp::FloatSqrt { src, .. } - | r2ssa::SSAOp::Cast { src, .. } => { - mark_scalar_var(&mut scalar_likely, &mut width_hints, src); - } - r2ssa::SSAOp::IntEqual { dst, a, b } - | r2ssa::SSAOp::IntNotEqual { dst, a, b } - | r2ssa::SSAOp::IntLess { dst, a, b } - | r2ssa::SSAOp::IntSLess { dst, a, b } - | r2ssa::SSAOp::IntLessEqual { dst, a, b } - | r2ssa::SSAOp::IntSLessEqual { dst, a, b } - | r2ssa::SSAOp::FloatEqual { dst, a, b } - | r2ssa::SSAOp::FloatNotEqual { dst, a, b } - | r2ssa::SSAOp::FloatLess { dst, a, b } - | r2ssa::SSAOp::FloatLessEqual { dst, a, b } => { - mark_scalar_var(&mut scalar_likely, &mut width_hints, a); - mark_scalar_var(&mut scalar_likely, &mut width_hints, b); - bool_like.insert(ssa_var_key(dst)); - merge_width_hint(&mut width_hints, dst, 1); - } - r2ssa::SSAOp::BoolAnd { dst, a, b } - | r2ssa::SSAOp::BoolOr { dst, a, b } - | r2ssa::SSAOp::BoolXor { dst, a, b } => { - mark_scalar_var(&mut scalar_likely, &mut width_hints, a); - mark_scalar_var(&mut scalar_likely, &mut width_hints, b); - bool_like.insert(ssa_var_key(a)); - bool_like.insert(ssa_var_key(b)); - bool_like.insert(ssa_var_key(dst)); - merge_width_hint(&mut width_hints, dst, 1); - } - r2ssa::SSAOp::IntZExt { dst, src } | r2ssa::SSAOp::IntSExt { dst, src } => { - mark_scalar_var(&mut scalar_likely, &mut width_hints, src); - mark_scalar_var(&mut scalar_likely, &mut width_hints, dst); - merge_width_hint(&mut width_hints, dst, dst.size.saturating_mul(8)); - } - r2ssa::SSAOp::Subpiece { dst, src, .. } => { - mark_scalar_var(&mut scalar_likely, &mut width_hints, src); - mark_scalar_var(&mut scalar_likely, &mut width_hints, dst); - merge_width_hint(&mut width_hints, dst, dst.size.saturating_mul(8)); - } - _ => {} - } - } - } - - let mut changed = true; - while changed { - changed = false; - for block in ssa_blocks { - for op in &block.ops { - match op { - r2ssa::SSAOp::IntAdd { dst, a, b } - | r2ssa::SSAOp::IntSub { dst, a, b } - | r2ssa::SSAOp::IntMult { dst, a, b } - | r2ssa::SSAOp::IntDiv { dst, a, b } - | r2ssa::SSAOp::IntSDiv { dst, a, b } - | r2ssa::SSAOp::IntRem { dst, a, b } - | r2ssa::SSAOp::IntSRem { dst, a, b } - | r2ssa::SSAOp::IntAnd { dst, a, b } - | r2ssa::SSAOp::IntOr { dst, a, b } - | r2ssa::SSAOp::IntXor { dst, a, b } - | r2ssa::SSAOp::IntLeft { dst, a, b } - | r2ssa::SSAOp::IntRight { dst, a, b } - | r2ssa::SSAOp::IntSRight { dst, a, b } => { - let dst_key = ssa_var_key(dst); - let a_key = ssa_var_key(a); - let b_key = ssa_var_key(b); - let any_proven = - scalar_proven.contains(&a_key) || scalar_proven.contains(&b_key); - let any_likely = scalar_likely.contains(&a_key) - || scalar_likely.contains(&b_key) - || any_proven; - if any_proven { - changed |= scalar_proven.insert(dst_key.clone()); - } - if any_likely { - changed |= scalar_likely.insert(dst_key.clone()); - } - - let operand_bits = [a, b] - .into_iter() - .filter(|var| !ssa_var_is_const(var)) - .filter_map(|var| width_hints.get(&ssa_var_key(var)).copied()) - .filter(|bits| *bits > 0) - .max() - .unwrap_or(0); - if operand_bits > 0 { - let entry = width_hints.entry(dst_key).or_insert(0); - if operand_bits > *entry { - *entry = operand_bits; - changed = true; - } - } - } - r2ssa::SSAOp::Phi { dst, sources } => { - let dst_key = ssa_var_key(dst); - let any_proven = sources - .iter() - .any(|src| scalar_proven.contains(&ssa_var_key(src))); - let any_likely = sources - .iter() - .any(|src| scalar_likely.contains(&ssa_var_key(src))); - let any_bool = sources - .iter() - .any(|src| bool_like.contains(&ssa_var_key(src))); - - if any_proven { - changed |= scalar_proven.insert(dst_key.clone()); - } - if any_likely || any_proven { - changed |= scalar_likely.insert(dst_key.clone()); - } - if any_bool { - changed |= bool_like.insert(dst_key.clone()); - } - let max_bits = sources - .iter() - .filter_map(|src| width_hints.get(&ssa_var_key(src)).copied()) - .max() - .unwrap_or(0); - if max_bits > 0 { - let entry = width_hints.entry(dst_key).or_insert(0); - if max_bits > *entry { - *entry = max_bits; - changed = true; - } - } - } - r2ssa::SSAOp::Copy { dst, src } - | r2ssa::SSAOp::Cast { dst, src } - | r2ssa::SSAOp::New { dst, src } => { - let dst_key = ssa_var_key(dst); - let src_key = ssa_var_key(src); - if scalar_proven.contains(&src_key) { - changed |= scalar_proven.insert(dst_key.clone()); - } - if scalar_likely.contains(&src_key) || scalar_proven.contains(&src_key) { - changed |= scalar_likely.insert(dst_key.clone()); - } - if bool_like.contains(&src_key) { - changed |= bool_like.insert(dst_key.clone()); - } - let bits = width_hints.get(&src_key).copied().unwrap_or(0); - if bits > 0 { - let entry = width_hints.entry(dst_key).or_insert(0); - if bits > *entry { - *entry = bits; - changed = true; - } - } - } - r2ssa::SSAOp::IntZExt { dst, src } - | r2ssa::SSAOp::IntSExt { dst, src } - | r2ssa::SSAOp::Subpiece { dst, src, .. } => { - let dst_key = ssa_var_key(dst); - let src_key = ssa_var_key(src); - if scalar_likely.contains(&src_key) || scalar_proven.contains(&src_key) { - changed |= scalar_likely.insert(dst_key.clone()); - } - if bool_like.contains(&src_key) { - changed |= bool_like.insert(dst_key.clone()); - } - let entry = width_hints.entry(dst_key).or_insert(0); - let bits = dst.size.saturating_mul(8); - if bits > *entry { - *entry = bits; - changed = true; - } - } - _ => {} - } - } - } - - for reg_keys in register_versions.values() { - let any_proven = reg_keys.iter().any(|key| scalar_proven.contains(key)); - let any_likely = reg_keys.iter().any(|key| scalar_likely.contains(key)); - let any_bool = reg_keys.iter().any(|key| bool_like.contains(key)); - let max_bits = reg_keys - .iter() - .filter_map(|key| width_hints.get(key).copied()) - .max() - .unwrap_or(0); - let min_bits = reg_keys - .iter() - .filter_map(|key| width_hints.get(key).copied()) - .filter(|bits| *bits > 0) - .min() - .unwrap_or(0); - let preferred_bits = if min_bits > 0 && max_bits == 64 && min_bits <= 32 { - min_bits - } else { - max_bits - }; - for key in reg_keys { - if any_proven { - changed |= scalar_proven.insert(key.clone()); - } - if any_likely || any_proven { - changed |= scalar_likely.insert(key.clone()); - } - if any_bool { - changed |= bool_like.insert(key.clone()); - } - if preferred_bits > 0 { - let entry = width_hints.entry(key.clone()).or_insert(0); - if preferred_bits > *entry { - *entry = preferred_bits; - changed = true; - } - } - } - } - } - - (scalar_proven, scalar_likely, bool_like, width_hints) + r2types::scalar_register_family_key(name) } +#[cfg(test)] pub(crate) fn collect_signature_type_evidence_context( ssa_blocks: &[r2ssa::SSABlock], -) -> crate::SignatureTypeEvidenceContext { - let pointer_vars = infer_pointer_var_keys_from_ssa(ssa_blocks); - let (scalar_proven_vars, scalar_likely_vars, bool_like_vars, mut width_bits) = - infer_scalar_var_evidence_from_ssa(ssa_blocks); - let register_versions = collect_register_version_keys(ssa_blocks); - normalize_register_family_width_hints(®ister_versions, &mut width_bits); - propagate_normalized_scalar_result_widths(ssa_blocks, &mut width_bits); - normalize_register_family_width_hints(®ister_versions, &mut width_bits); - crate::SignatureTypeEvidenceContext { - pointer_vars, - scalar_proven_vars, - scalar_likely_vars, - bool_like_vars, - width_bits, - } -} - -fn infer_usage_register_type_hints( - ssa_blocks: &[r2ssa::SSABlock], -) -> ( - std::collections::HashMap, - std::collections::HashSet, -) { - let pointer_vars = infer_pointer_var_keys_from_ssa(ssa_blocks); - let mut hints = std::collections::HashMap::new(); - - for block in ssa_blocks { - for op in &block.ops { - let mut maybe_add = |var: &r2ssa::SSAVar| { - let key = ssa_var_key(var); - if !pointer_vars.contains(&key) || !ssa_var_is_register_like(&var.name) { - return; - } - merge_type_hint( - &mut hints, - var.name.to_ascii_lowercase(), - TypeHint::pointer(), - ); - }; - - if let Some(dst) = op.dst() { - maybe_add(dst); - } - op.for_each_source(&mut maybe_add); - } - } - - (hints, pointer_vars) -} - -fn strongest_hint_for_aliases( - hints: &std::collections::HashMap, - canonical: &str, - aliases: &[&str], -) -> Option { - let mut best = hints.get(canonical).cloned(); - for alias in aliases { - if let Some(candidate) = hints.get(*alias).cloned() { - match &best { - Some(current) if !incoming_hint_should_replace(current, &candidate) => {} - _ => best = Some(candidate), - } - } - } - best -} - -fn width_hint_key_matches_family(key: &str, family: &str) -> bool { - let Some((name, _version)) = key.rsplit_once('_') else { - return false; - }; - scalar_register_family_key(name) == family -} - -fn width_hint_key_matches_family_version(key: &str, family: &str, version: u32) -> bool { - let Some((name, version_str)) = key.rsplit_once('_') else { - return false; - }; - version_str.parse::().ok() == Some(version) && scalar_register_family_key(name) == family -} - -fn recovered_arg_family_width_hint( - evidence: &crate::SignatureTypeEvidenceContext, - src: &r2ssa::SSAVar, -) -> Option { - let family = scalar_register_family_key(&src.name); - evidence - .width_bits - .iter() - .filter(|(key, bits)| { - **bits > 0 && width_hint_key_matches_family_version(key, &family, src.version) - }) - .map(|(_, bits)| *bits) - .min() - .or_else(|| { - evidence - .width_bits - .iter() - .filter(|(key, bits)| **bits > 0 && width_hint_key_matches_family(key, &family)) - .map(|(_, bits)| *bits) - .min() - }) -} - -fn recovered_arg_type_hint( - reg_type_hints: &std::collections::HashMap, - canonical: &str, - aliases: &[&str], - src: &r2ssa::SSAVar, - signature_evidence: Option<&crate::SignatureTypeEvidenceContext>, -) -> Option { - let best_hint = strongest_hint_for_aliases(reg_type_hints, canonical, aliases); - if let Some(hint) = best_hint.as_ref() - && hint.rank == TypeHintRank::Pointer - { - return Some(hint.ty.clone()); - } - if let Some(evidence) = signature_evidence - && let Some(bits) = recovered_arg_family_width_hint(evidence, src) - { - return Some(size_to_type(bits.div_ceil(8))); - } - best_hint.map(|hint| hint.ty) +) -> r2types::SignatureTypeEvidenceContext { + r2types::collect_signature_type_evidence_context(ssa_blocks) } +#[cfg(test)] pub(crate) fn merge_register_type_hints( metadata_hints: &std::collections::HashMap, usage_hints: &std::collections::HashMap, @@ -3064,7 +1140,14 @@ pub(crate) fn merge_register_type_hints( } for (canonical, aliases) in arg_regs { - if let Some(best) = strongest_hint_for_aliases(&merged, canonical, aliases) { + let candidates: Vec = std::iter::once(*canonical) + .chain(aliases.iter().copied()) + .filter_map(|name| merged.get(name).cloned()) + .collect(); + if let Some(best) = candidates + .into_iter() + .max_by(|a, b| a.rank.cmp(&b.rank).then_with(|| b.ty.cmp(&a.ty))) + { merge_type_hint(&mut merged, (*canonical).to_string(), best.clone()); for alias in *aliases { merge_type_hint(&mut merged, alias.to_string(), best.clone()); @@ -3076,16 +1159,10 @@ pub(crate) fn merge_register_type_hints( } pub(crate) fn collect_pointer_arg_slots(vars: &[VarProt]) -> std::collections::BTreeSet { - vars.iter() - .filter(|var| var.kind == "r" && var.isarg && var.var_type.contains('*')) - .filter_map(|var| { - var.name - .strip_prefix("arg") - .and_then(|idx| idx.parse::().ok()) - }) - .collect() + r2types::collect_pointer_arg_slots(vars) } +#[cfg(test)] pub(crate) fn merge_pointer_slot_evidence( inferred_params: &mut [InferredParam], pointer_arg_slots: &std::collections::BTreeSet, @@ -3116,140 +1193,15 @@ pub(crate) fn recover_vars_from_ssa( metadata_reg_type_hints: &std::collections::HashMap, semantic_typing_enabled: bool, ) -> Vec { - use std::collections::{HashMap, HashSet}; - - let mut vars = Vec::new(); - let mut seen_slots: HashMap<(bool, i64), usize> = HashMap::new(); - let mut seen_arg_regs: HashSet = HashSet::new(); - let (arg_regs, stack_bases, frame_bases) = recover_vars_arch_profile(arch); - let signature_evidence = - semantic_typing_enabled.then(|| collect_signature_type_evidence_context(ssa_blocks)); - let (usage_reg_type_hints, pointer_var_keys) = if semantic_typing_enabled { - infer_usage_register_type_hints(ssa_blocks) - } else { - (HashMap::new(), HashSet::new()) - }; - let reg_type_hints = if semantic_typing_enabled { - merge_register_type_hints(metadata_reg_type_hints, &usage_reg_type_hints, arg_regs) - } else { - HashMap::new() - }; - - let mut stack_addr_temps: HashMap = HashMap::new(); - - for block in ssa_blocks { - for op in &block.ops { - match op { - r2ssa::SSAOp::IntAdd { dst, a, b } | r2ssa::SSAOp::IntSub { dst, a, b } => { - let a_name = a.name.to_lowercase(); - let b_name = b.name.to_lowercase(); - - let is_a_base = stack_bases.contains(&a_name.as_str()); - let is_b_const = b_name.starts_with("const:"); - - if is_a_base && is_b_const { - if let Some(raw_offset) = parse_const_value(&b.name) { - let offset = if matches!(op, r2ssa::SSAOp::IntSub { .. }) { - -(raw_offset as i64) - } else { - raw_offset as i64 - }; - let dst_key = ssa_var_block_key(block.addr, dst); - stack_addr_temps.insert(dst_key, (a_name.clone(), offset)); - } - } else if stack_bases.contains(&b_name.as_str()) - && a_name.starts_with("const:") - && let Some(raw_offset) = parse_const_value(&a.name) - { - let offset = raw_offset as i64; - let dst_key = ssa_var_block_key(block.addr, dst); - stack_addr_temps.insert(dst_key, (b_name.clone(), offset)); - } - } - r2ssa::SSAOp::Store { addr, val, .. } => { - let addr_key = ssa_var_block_key(block.addr, addr); - if let Some((base_reg, offset)) = stack_addr_temps.get(&addr_key) { - let type_override = if semantic_typing_enabled - && pointer_var_keys.contains(&ssa_var_key(val)) - { - Some("void *".to_string()) - } else { - None - }; - add_stack_var( - &mut vars, - &mut seen_slots, - base_reg, - frame_bases, - *offset, - val.size, - type_override, - ); - } - } - r2ssa::SSAOp::Load { dst, addr, .. } => { - let addr_key = ssa_var_block_key(block.addr, addr); - if let Some((base_reg, offset)) = stack_addr_temps.get(&addr_key) { - let type_override = if semantic_typing_enabled - && pointer_var_keys.contains(&ssa_var_key(dst)) - { - Some("void *".to_string()) - } else { - None - }; - add_stack_var( - &mut vars, - &mut seen_slots, - base_reg, - frame_bases, - *offset, - dst.size, - type_override, - ); - } - } - _ => {} - } - - for src in op.sources() { - let base_name = src.name.to_lowercase(); - if src.version == 0 { - for (i, (canonical, aliases)) in arg_regs.iter().enumerate() { - if aliases.contains(&base_name.as_str()) - && !seen_arg_regs.contains(*canonical) - { - seen_arg_regs.insert(canonical.to_string()); - let hinted_type = if semantic_typing_enabled { - recovered_arg_type_hint( - ®_type_hints, - canonical, - aliases, - src, - signature_evidence.as_ref(), - ) - } else { - None - }; - vars.push(VarProt { - name: format!("arg{}", i), - kind: "r".to_string(), - delta: 0, - var_type: hinted_type.unwrap_or_else(|| size_to_type(src.size)), - isarg: true, - reg: Some(canonical.to_string()), - }); - break; - } - } - } - } - } - } - - vars.sort_by_key(|v| v.delta); - vars + r2types::recover_vars_from_ssa( + ssa_blocks, + arch.map(|spec| spec.name.as_str()), + metadata_reg_type_hints, + semantic_typing_enabled, + ) } +#[cfg(test)] pub(crate) fn add_stack_var( vars: &mut Vec, seen_slots: &mut std::collections::HashMap<(bool, i64), usize>, @@ -3313,14 +1265,9 @@ pub(crate) fn parse_const_value(name: &str) -> Option { u64::from_str_radix(val_str, 16).ok() } +#[cfg(test)] pub(crate) fn size_to_type(size: u32) -> String { - match size { - 1 => "int8_t".to_string(), - 2 => "int16_t".to_string(), - 4 => "int32_t".to_string(), - 8 => "int64_t".to_string(), - _ => format!("byte[{}]", size), - } + r2types::size_to_type(size) } fn parse_const_addr(name: &str) -> Option { diff --git a/tests/r2r/db/extras/r2sleigh_integration_extended b/tests/r2r/db/extras/r2sleigh_integration_extended index 9baef9b..651cd15 100644 --- a/tests/r2r/db/extras/r2sleigh_integration_extended +++ b/tests/r2r/db/extras/r2sleigh_integration_extended @@ -269,7 +269,7 @@ true EOF_EXPECT CMDS=<=10' +a:sla.sym sym.interpret_bytecode | jq -c '.semantic.mode=="vm_summary" and .semantic.slice_class=="interpreter_switch" and .semantic.skipped_large_cfg==true and (.semantic.residual_reasons|length)==0 and .semantic.vm_step != null and (.semantic.vm_step.dispatch_targets|length)>=10' EOF_CMDS RUN @@ -346,6 +346,30 @@ a:sla.types sym.test_stack_symbolic_eq_guard | jq -c '.symbolic.diagnostics.bran EOF_CMDS RUN +NAME=types_stack_symbolic_eq_guard_reports_memory_island +FILE=bins/vuln_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=<= 1 and .symbolic.memory_islands[0].anchor_block != null and ((.symbolic.memory_islands[0].terms|length) >= 1) and .symbolic.memory_islands[0].terms[0].region.Region.kind=="Stack"' +EOF_CMDS +RUN + +NAME=sym_stack_symbolic_eq_guard_reports_memory_island_count +FILE=bins/vuln_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=<= 1' +EOF_CMDS +RUN + NAME=decompile_tiny_vm_dispatch_uses_vm_summary_fallback FILE=bins/stress_test ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true From 0b0b0ce8f42d3ca56f3bbb7bc904d2e8b94d811c Mon Sep 17 00:00:00 2001 From: Priyanshu Kumar Date: Wed, 1 Apr 2026 17:05:14 +0000 Subject: [PATCH 07/10] Enhance r2dec with new semantic structuring and linearization capabilities --- Cargo.lock | 132 + crates/r2dec/Cargo.toml | 1 + crates/r2dec/src/consumer_fallback.rs | 144 ++ crates/r2dec/src/consumer_linear.rs | 65 + crates/r2dec/src/consumer_structured.rs | 69 + crates/r2dec/src/consumer_vm.rs | 137 ++ crates/r2dec/src/fold/context.rs | 7 +- crates/r2dec/src/fold/flags.rs | 44 +- crates/r2dec/src/fold/tests/flags.rs | 267 ++- crates/r2dec/src/fold/tests/pipeline.rs | 4 +- crates/r2dec/src/lib.rs | 2125 ++++++++--------- crates/r2dec/src/planner.rs | 232 ++ crates/r2dec/src/structure.rs | 1075 ++++++--- crates/r2sym/Cargo.toml | 1 + crates/r2sym/src/lib.rs | 18 +- crates/r2sym/src/query.rs | 1139 ++++++--- crates/r2sym/src/semantics/artifact.rs | 636 ++++- crates/r2sym/src/semantics/cache.rs | 65 +- crates/r2sym/src/semantics/compiler.rs | 574 +++-- crates/r2sym/src/semantics/facts.rs | 1614 ++----------- crates/r2sym/src/semantics/mod.rs | 27 +- crates/r2sym/src/semantics/plan.rs | 398 +++ crates/r2sym/src/semantics/region.rs | 807 +++++++ crates/r2sym/tests/symex_integration.rs | 168 +- crates/r2types/src/facts.rs | 1817 -------------- crates/r2types/src/from_sym.rs | 666 ------ crates/r2types/src/function_facts.rs | 25 + crates/r2types/src/lib.rs | 21 +- crates/r2types/src/writeback.rs | 557 ++--- r2plugin/src/analysis/sym.rs | 243 +- r2plugin/src/decompiler.rs | 76 +- r2plugin/src/lib.rs | 475 ++-- r2plugin/src/types.rs | 43 +- .../db/extras/r2sleigh_integration_extended | 48 +- 34 files changed, 6712 insertions(+), 7008 deletions(-) create mode 100644 crates/r2dec/src/consumer_fallback.rs create mode 100644 crates/r2dec/src/consumer_linear.rs create mode 100644 crates/r2dec/src/consumer_structured.rs create mode 100644 crates/r2dec/src/consumer_vm.rs create mode 100644 crates/r2dec/src/planner.rs create mode 100644 crates/r2sym/src/semantics/plan.rs create mode 100644 crates/r2sym/src/semantics/region.rs delete mode 100644 crates/r2types/src/from_sym.rs create mode 100644 crates/r2types/src/function_facts.rs diff --git a/Cargo.lock b/Cargo.lock index b1e27db..386fbea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -121,6 +121,21 @@ dependencies = [ "syn", ] +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "2.10.0" @@ -456,6 +471,22 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "find-msvc-tools" version = "0.1.7" @@ -468,6 +499,12 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" @@ -964,6 +1001,12 @@ dependencies = [ "cc", ] +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + [[package]] name = "litemap" version = "0.8.1" @@ -1219,6 +1262,31 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quinn" version = "0.11.9" @@ -1295,6 +1363,7 @@ version = "0.1.0" dependencies = [ "r2il", "r2ssa", + "r2sym", "r2types", "serde", "serde_json", @@ -1399,6 +1468,7 @@ name = "r2sym" version = "0.1.0" dependencies = [ "criterion", + "proptest", "r2il", "r2pipe", "r2ssa", @@ -1450,6 +1520,15 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core", +] + [[package]] name = "rayon" version = "1.11.0" @@ -1568,6 +1647,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rustls" version = "0.23.36" @@ -1609,6 +1701,18 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "ryu" version = "1.0.22" @@ -1805,6 +1909,19 @@ dependencies = [ "syn", ] +[[package]] +name = "tempfile" +version = "3.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -1983,6 +2100,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-ident" version = "1.0.22" @@ -2025,6 +2148,15 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.5.0" diff --git a/crates/r2dec/Cargo.toml b/crates/r2dec/Cargo.toml index 636b6b2..333e862 100644 --- a/crates/r2dec/Cargo.toml +++ b/crates/r2dec/Cargo.toml @@ -8,6 +8,7 @@ description = "Native decompiler for r2sleigh" [dependencies] r2il = { path = "../r2il" } r2ssa = { path = "../r2ssa" } +r2sym = { path = "../r2sym" } r2types = { path = "../r2types" } serde.workspace = true thiserror.workspace = true diff --git a/crates/r2dec/src/consumer_fallback.rs b/crates/r2dec/src/consumer_fallback.rs new file mode 100644 index 0000000..0c528c5 --- /dev/null +++ b/crates/r2dec/src/consumer_fallback.rs @@ -0,0 +1,144 @@ +use crate::ast::CStmt; + +pub(crate) struct EmptyStructuringFallback { + pub(crate) body_stmt: CStmt, + pub(crate) use_conservative_locals: bool, + pub(crate) is_linear_fallback: bool, +} + +pub(crate) fn recover_empty_structuring<'o, F>( + func: &r2ssa::SSAFunction, + fold_ctx: &'o crate::fold::FoldingContext<'o>, + folded_reason: String, + semantic_worker_linear_reason: Option<&str>, + mut linearize: F, +) -> EmptyStructuringFallback +where + F: FnMut() -> Vec, +{ + let mut unfolded = crate::ControlFlowStructurer::new_unfolded(func, fold_ctx); + let unfolded_stmt = unfolded.structure(); + + if crate::Decompiler::stmt_has_content(&unfolded_stmt) { + return EmptyStructuringFallback { + body_stmt: crate::Decompiler::prepend_comment( + unfolded_stmt, + format!("r2dec fallback: {}", folded_reason), + ), + use_conservative_locals: true, + is_linear_fallback: false, + }; + } + + let unfolded_reason = unfolded + .safety_reason() + .map(str::to_string) + .unwrap_or_else(|| "unfolded structuring produced empty output".to_string()); + let fallback_reason = format!("{}; {}", folded_reason, unfolded_reason); + let mut linear_stmts = linearize(); + + if let Some(reason) = semantic_worker_linear_reason { + return EmptyStructuringFallback { + body_stmt: crate::consumer_structured::semantic_worker_linear_body( + reason, + linear_stmts, + ), + use_conservative_locals: true, + is_linear_fallback: true, + }; + } + + let body_stmt = if linear_stmts.is_empty() { + CStmt::Block(vec![CStmt::comment(format!( + "r2dec fallback: {} -> no statements recovered", + fallback_reason + ))]) + } else { + linear_stmts.insert( + 0, + CStmt::comment(format!( + "r2dec fallback: {} -> linear block emission", + fallback_reason + )), + ); + CStmt::Block(linear_stmts) + }; + + EmptyStructuringFallback { + body_stmt, + use_conservative_locals: true, + is_linear_fallback: true, + } +} + +pub(crate) fn semantic_fallback_comment( + func_name: &str, + semantic_artifact: Option<&r2sym::SemanticArtifact>, +) -> Option { + let semantic_artifact = semantic_artifact?; + if let Some(comment) = + crate::consumer_vm::render_vm_semantic_fallback_comment(func_name, semantic_artifact) + { + return Some(comment); + } + let slice_class = semantic_artifact.slice_class()?; + let mut reason = format!( + "semantic fallback: {} slice in {} mode", + crate::semantic_slice_class_label(slice_class), + crate::semantic_mode_label(semantic_artifact) + ); + if !semantic_artifact.diagnostics.residual_reasons.is_empty() { + reason.push_str(" ("); + reason.push_str( + &semantic_artifact + .diagnostics + .residual_reasons + .iter() + .map(|reason| crate::semantic_residual_reason_label(*reason)) + .collect::>() + .join(", "), + ); + reason.push(')'); + } + if !semantic_artifact.ambiguous_targets().is_empty() { + reason.push_str("; ambiguous_targets=["); + reason.push_str( + &semantic_artifact + .ambiguous_targets() + .into_iter() + .map(|target| format!("0x{target:x}")) + .collect::>() + .join(", "), + ); + reason.push(']'); + } + if let Some(native) = semantic_artifact.native_body() + && !native.regions.is_empty() + { + reason.push_str(&format!( + "; regions={}, actionable_conditions={}, exact_conditions={}", + native.regions.len(), + native.actionable_control_count(), + native.exact_control_count(), + )); + } + let actionable_preview = semantic_artifact + .actionable_regions() + .into_iter() + .filter_map(|region| { + region + .actionable_compiled_condition() + .map(|condition| format!("0x{:x}: {}", region.anchor, condition.simplified)) + }) + .take(3) + .collect::>(); + if !actionable_preview.is_empty() { + reason.push_str("; actionable_preview=["); + reason.push_str(&actionable_preview.join(" | ")); + reason.push(']'); + } + Some(format!( + "/* r2dec fallback: skipped decompilation for {} ({}) */", + func_name, reason + )) +} diff --git a/crates/r2dec/src/consumer_linear.rs b/crates/r2dec/src/consumer_linear.rs new file mode 100644 index 0000000..f79ec8b --- /dev/null +++ b/crates/r2dec/src/consumer_linear.rs @@ -0,0 +1,65 @@ +use std::fmt::Write as _; + +pub(crate) fn render_semantic_worker_linearization( + plan: &r2types::TypeWritebackPlan, + semantic_artifact: Option<&r2sym::SemanticArtifact>, + reason: &str, +) -> String { + let mut out = String::new(); + for decl in &plan.struct_decls { + let _ = writeln!(&mut out, "{}", decl.decl); + } + if !plan.struct_decls.is_empty() { + out.push('\n'); + } + let _ = writeln!(&mut out, "{} {{", plan.signature.signature); + let _ = writeln!( + &mut out, + " /* r2dec semantic worker linearization: {} */", + reason + ); + for warning in plan.diagnostics.warnings.iter().take(2) { + let _ = writeln!(&mut out, " /* {} */", warning); + } + + let mut emitted_any = false; + for region in semantic_artifact + .map(r2sym::SemanticArtifact::actionable_regions) + .unwrap_or_default() + .into_iter() + .take(6) + { + if let (Some(target), Some(condition)) = ( + region.actionable_reachable_target(), + region.actionable_compiled_condition(), + ) { + let _ = writeln!( + &mut out, + " /* 0x{:x}: if ({}) => 0x{:x} [{:?}] */", + region.anchor, + condition.simplified, + target, + condition.evidence().tier + ); + emitted_any = true; + } + for term in region.actionable_memory_terms().into_iter().take(2) { + let _ = writeln!( + &mut out, + " /* 0x{:x}: {} [{:?}] */", + region.anchor, + term.expr, + term.evidence().tier + ); + emitted_any = true; + } + } + if !emitted_any { + let _ = writeln!( + &mut out, + " /* no actionable worker-island statements recovered */" + ); + } + let _ = writeln!(&mut out, "}}"); + out +} diff --git a/crates/r2dec/src/consumer_structured.rs b/crates/r2dec/src/consumer_structured.rs new file mode 100644 index 0000000..e2b23e5 --- /dev/null +++ b/crates/r2dec/src/consumer_structured.rs @@ -0,0 +1,69 @@ +use crate::ast::CStmt; + +pub(crate) struct RoutedBody { + pub(crate) body_stmt: CStmt, + pub(crate) use_conservative_locals: bool, + pub(crate) is_linear_fallback: bool, +} + +pub(crate) fn primary_body_for_semantic_route<'a, 'o, F>( + route: &crate::SemanticRoutePlan, + structurer: &mut crate::ControlFlowStructurer<'a, 'o>, + mut linearize: F, +) -> RoutedBody +where + F: FnMut() -> Vec, +{ + match route { + crate::SemanticRoutePlan::StructuredWorker { reason } => { + match structurer.structure_semantic_worker_islands(6) { + Some(structured) => RoutedBody { + body_stmt: semantic_worker_structured_body(reason, structured), + use_conservative_locals: true, + is_linear_fallback: false, + }, + None => RoutedBody { + body_stmt: semantic_worker_linear_body(reason, linearize()), + use_conservative_locals: true, + is_linear_fallback: true, + }, + } + } + crate::SemanticRoutePlan::LinearWorker { reason } => RoutedBody { + body_stmt: semantic_worker_linear_body(reason, linearize()), + use_conservative_locals: true, + is_linear_fallback: true, + }, + crate::SemanticRoutePlan::FallbackComment { .. } | crate::SemanticRoutePlan::Standard => { + RoutedBody { + body_stmt: structurer.structure(), + use_conservative_locals: false, + is_linear_fallback: false, + } + } + } +} + +pub(crate) fn semantic_worker_structured_body(reason: &str, structured: CStmt) -> CStmt { + CStmt::Block(vec![ + CStmt::comment(format!("r2dec semantic worker structuring for {}", reason)), + structured, + ]) +} + +pub(crate) fn semantic_worker_linear_body(reason: &str, mut linear_stmts: Vec) -> CStmt { + if linear_stmts.is_empty() { + return CStmt::Block(vec![CStmt::comment(format!( + "r2dec fallback: semantic worker linearization for {} -> no statements recovered", + reason + ))]); + } + linear_stmts.insert( + 0, + CStmt::comment(format!( + "r2dec fallback: semantic worker linearization for {}", + reason + )), + ); + CStmt::Block(linear_stmts) +} diff --git a/crates/r2dec/src/consumer_vm.rs b/crates/r2dec/src/consumer_vm.rs new file mode 100644 index 0000000..9ed4fc2 --- /dev/null +++ b/crates/r2dec/src/consumer_vm.rs @@ -0,0 +1,137 @@ +pub(crate) fn render_vm_semantic_fallback_comment( + func_name: &str, + semantic_artifact: &r2sym::SemanticArtifact, +) -> Option { + let vm_body = semantic_artifact.vm_body()?; + let vm_step = vm_body + .step_summary + .as_ref() + .or(vm_body.transfer_summary.as_ref())?; + let kind = crate::format_vm_summary_kind(vm_step.kind); + let selector = vm_step.selector.as_deref().unwrap_or("unknown"); + let inputs = if vm_step.state_inputs.is_empty() { + "none".to_string() + } else { + vm_step.state_inputs.join(", ") + }; + let outputs = if vm_step.state_outputs.is_empty() { + "none".to_string() + } else { + vm_step.state_outputs.join(", ") + }; + let exact_transfers = vm_step + .transfers + .iter() + .filter(|transfer| transfer.exact) + .count(); + let likely_transfers = vm_step + .transfers + .iter() + .filter(|transfer| matches!(transfer.confidence(), r2sym::SemanticConfidence::Likely)) + .count(); + let heuristic_transfers = vm_step + .transfers + .iter() + .filter(|transfer| matches!(transfer.confidence(), r2sym::SemanticConfidence::Heuristic)) + .count(); + let redispatch_transfers = vm_step + .transfers + .iter() + .filter(|transfer| transfer.redispatch) + .count(); + let returning_transfers = vm_step + .transfers + .iter() + .filter(|transfer| transfer.may_return) + .count(); + let selector_updates = vm_step + .transfers + .iter() + .filter(|transfer| transfer.selector_update.is_some()) + .count(); + let exit_guards = vm_step + .transfers + .iter() + .map(|transfer| transfer.exit_guards.len()) + .sum::(); + let residual_guards = vm_step + .transfers + .iter() + .filter(|transfer| transfer.residual_guards) + .count(); + let residual_memory = vm_step + .transfers + .iter() + .filter(|transfer| transfer.residual_memory_effects) + .count(); + let read_effects = vm_step + .handler_memory_read_effects + .values() + .map(Vec::len) + .sum::(); + let write_effects = vm_step + .handler_memory_write_effects + .values() + .map(Vec::len) + .sum::(); + let total_reads: usize = vm_step.handler_memory_reads.values().copied().sum(); + let total_writes: usize = vm_step.handler_memory_writes.values().copied().sum(); + let handler_preview = vm_step + .dispatch_targets + .iter() + .take(3) + .map(|target| { + let values = vm_step + .case_values_by_target + .get(target) + .map(|values| { + values + .iter() + .map(|value| format!("0x{value:x}")) + .collect::>() + .join("|") + }) + .unwrap_or_else(|| "default".to_string()); + let updates = vm_step + .handler_state_updates + .get(target) + .map(|updates| { + updates + .iter() + .take(3) + .map(|update| format!("{}={}", update.output, update.expr)) + .collect::>() + .join(", ") + }) + .filter(|text| !text.is_empty()) + .unwrap_or_else(|| "no_state_updates".to_string()); + format!("0x{target:x}[{values}] => {updates}") + }) + .collect::>() + .join("; "); + Some(format!( + "/* r2dec semantic summary: vm_summary for {} ({kind} @ 0x{:x}, loop_header=0x{:x}, selector={}, targets={}, redispatch={}, exact_transfers={}, likely_transfers={}, heuristic_transfers={}, redispatch_transfers={}, returning_transfers={}, selector_updates={}, exact_exit_guards={}, residual_guards={}, residual_memory={}, total_reads={}, total_writes={}, read_effects={}, write_effects={}, state_inputs=[{}], state_outputs=[{}], handlers={}) */", + func_name, + vm_step.dispatch_header, + vm_step.loop_header, + selector, + vm_step.dispatch_targets.len(), + vm_step.redispatch_handlers.len(), + exact_transfers, + likely_transfers, + heuristic_transfers, + redispatch_transfers, + returning_transfers, + selector_updates, + exit_guards, + residual_guards, + residual_memory, + total_reads, + total_writes, + read_effects, + write_effects, + inputs, + outputs, + handler_preview, + )) +} diff --git a/crates/r2dec/src/fold/context.rs b/crates/r2dec/src/fold/context.rs index 44935d1..4e15b81 100644 --- a/crates/r2dec/src/fold/context.rs +++ b/crates/r2dec/src/fold/context.rs @@ -12,7 +12,7 @@ use r2ssa::{ use r2types::ExternalStackVarSpec; use r2types::{ CalleeFact, ExternalStackSlotSpec, ExternalTypeDb, FunctionType, SignatureRegistry, - StackSlotKey, SymbolicSemanticFacts, TypeOracle, VisibleBinding, + StackSlotKey, TypeOracle, VisibleBinding, }; pub(crate) type SSABlock = FunctionSSABlock; @@ -65,7 +65,7 @@ pub(crate) struct FoldInputs<'a> { pub(crate) external_stack_vars: &'a HashMap, pub(crate) visible_bindings: &'a [VisibleBinding], pub(crate) external_type_db: &'a ExternalTypeDb, - pub(crate) symbolic_facts: &'a SymbolicSemanticFacts, + pub(crate) semantic_artifact: Option<&'a r2sym::SemanticArtifact>, pub(crate) param_register_aliases: &'a HashMap, pub(crate) type_hints: &'a HashMap, pub(crate) type_oracle: Option<&'a dyn TypeOracle>, @@ -208,7 +208,6 @@ impl<'a> FoldingContext<'a> { static EMPTY_I64_STACK: OnceLock> = OnceLock::new(); static EMPTY_VISIBLE_BINDINGS: OnceLock> = OnceLock::new(); static EMPTY_TYPE_DB: OnceLock = OnceLock::new(); - static EMPTY_SYMBOLIC_FACTS: OnceLock = OnceLock::new(); static EMPTY_STRING_STRING: OnceLock> = OnceLock::new(); static EMPTY_STRING_FNTY: OnceLock> = OnceLock::new(); static EMPTY_CALLEE_FACTS: OnceLock> = OnceLock::new(); @@ -234,7 +233,7 @@ impl<'a> FoldingContext<'a> { external_stack_vars: EMPTY_I64_STACK.get_or_init(HashMap::new), visible_bindings: EMPTY_VISIBLE_BINDINGS.get_or_init(Vec::new), external_type_db: EMPTY_TYPE_DB.get_or_init(ExternalTypeDb::default), - symbolic_facts: EMPTY_SYMBOLIC_FACTS.get_or_init(SymbolicSemanticFacts::default), + semantic_artifact: None, param_register_aliases: EMPTY_STRING_STRING.get_or_init(HashMap::new), type_hints: EMPTY_STRING_CTYPE.get_or_init(HashMap::new), type_oracle: None, diff --git a/crates/r2dec/src/fold/flags.rs b/crates/r2dec/src/fold/flags.rs index 3a735c9..4fc0170 100644 --- a/crates/r2dec/src/fold/flags.rs +++ b/crates/r2dec/src/fold/flags.rs @@ -5,17 +5,15 @@ use r2ssa::{ CompareKind as PreparedCompareKind, CompareProvenance, FunctionSSABlock, SSAOp, SSAVar, }; -use crate::analysis; -use crate::analysis::{FlagCompareKind, FlagCompareProvenance, utils}; -use crate::ast::{BinaryOp, CExpr, CType, UnaryOp}; -use r2types::SymbolicReachabilityStatus; - use super::context::FoldingContext; use super::op_lower::parse_const_value; use super::{ MAX_COND_STACK_ALIAS_DEPTH, MAX_PREDICATE_OPERAND_DEPTH, MAX_PREDICATE_SIMPLIFY_DEPTH, MAX_SF_SURROGATE_DEPTH, MAX_SUB_LIKE_DEPTH, }; +use crate::analysis; +use crate::analysis::{FlagCompareKind, FlagCompareProvenance, utils}; +use crate::ast::{BinaryOp, CExpr, CType, UnaryOp}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(super) enum CompareContext { @@ -430,27 +428,23 @@ impl<'a> FoldingContext<'a> { } fn symbolic_branch_condition_expr(&self, block_addr: u64) -> Option { - let fact = self + match self .inputs - .symbolic_facts - .branch_fact_for_block(block_addr)?; - match (fact.true_status, fact.false_status) { - (SymbolicReachabilityStatus::Reachable, SymbolicReachabilityStatus::Unreachable) => { - Some(CExpr::IntLit(1)) - } - (SymbolicReachabilityStatus::Unreachable, SymbolicReachabilityStatus::Reachable) => { - Some(CExpr::IntLit(0)) - } - _ => None, + .semantic_artifact? + .exact_branch_truth_for_block(block_addr) + { + Some(true) => Some(CExpr::IntLit(1)), + Some(false) => Some(CExpr::IntLit(0)), + None => None, } } fn symbolic_actionable_compiled_condition( &self, block_addr: u64, - ) -> Option<&r2types::SymbolicCompiledCondition> { + ) -> Option<&r2sym::BackwardConditionSummary> { self.inputs - .symbolic_facts + .semantic_artifact? .actionable_compiled_condition_for_block(block_addr) } @@ -462,12 +456,12 @@ impl<'a> FoldingContext<'a> { } fn symbolic_actionable_memory_condition_expr(&self, block_addr: u64) -> Option { - fn memory_term_rank(term: &r2types::SymbolicMemoryCondition) -> (u8, bool, bool, i64, i64) { - let evidence_rank = match term.evidence.tier { - r2types::SymbolicSemanticConfidence::Exact => 3, - r2types::SymbolicSemanticConfidence::Likely => 2, - r2types::SymbolicSemanticConfidence::Heuristic => 1, - r2types::SymbolicSemanticConfidence::Residual => 0, + fn memory_term_rank(term: &r2sym::BackwardMemoryCondition) -> (u8, bool, bool, i64, i64) { + let evidence_rank = match term.evidence().tier { + r2sym::SemanticConfidence::Exact => 3, + r2sym::SemanticConfidence::Likely => 2, + r2sym::SemanticConfidence::Heuristic => 1, + r2sym::SemanticConfidence::Residual => 0, }; ( evidence_rank, @@ -480,7 +474,7 @@ impl<'a> FoldingContext<'a> { let term = self .inputs - .symbolic_facts + .semantic_artifact? .actionable_memory_terms_for_block(block_addr) .into_iter() .filter(|term| { diff --git a/crates/r2dec/src/fold/tests/flags.rs b/crates/r2dec/src/fold/tests/flags.rs index 955082b..fc07e01 100644 --- a/crates/r2dec/src/fold/tests/flags.rs +++ b/crates/r2dec/src/fold/tests/flags.rs @@ -2,11 +2,6 @@ use super::*; use crate::fold::FoldingContext; use crate::fold::context::{FoldArchConfig, FoldInputs}; use r2il::{ArchSpec, R2ILBlock, R2ILOp, RegisterDef, Varnode}; -use r2types::{ - SymbolicBranchFact, SymbolicCompiledCondition, SymbolicConditionPrecision, SymbolicControlFact, - SymbolicControlIsland, SymbolicControlIslandKind, SymbolicReachabilityStatus, - SymbolicSemanticFacts, -}; use std::collections::{BTreeMap, HashMap, HashSet}; fn make_test_arch_x86_64() -> ArchSpec { @@ -59,7 +54,7 @@ fn make_x86_64_ctx_with_prepared<'a>(prepared_ssa: &'a r2ssa::SsaArtifact) -> Fo external_stack_vars: empty_stack, visible_bindings: empty_visible, external_type_db: Box::leak(Box::new(r2types::ExternalTypeDb::default())), - symbolic_facts: Box::leak(Box::new(r2types::SymbolicSemanticFacts::default())), + semantic_artifact: None, param_register_aliases: empty_str, type_hints: empty_ty, type_oracle: None, @@ -177,31 +172,39 @@ fn exact_compiled_condition_shortcuts_to_literal_condition() { let prepared = prepared_from_r2il_blocks(&[entry, fallthrough, taken], &arch).with_name("exact_compiled"); let mut ctx = make_x86_64_ctx_with_prepared(&prepared); - let mut facts = SymbolicSemanticFacts::default(); - facts.branch_facts.push(SymbolicBranchFact { - block_addr: 0x1000, - true_target: 0x1008, - false_target: 0x1004, - true_status: SymbolicReachabilityStatus::Unreachable, - false_status: SymbolicReachabilityStatus::Reachable, - true_condition: None, - false_condition: Some("false".to_string()), - true_compiled: None, - false_compiled: Some(SymbolicCompiledCondition { - simplified: "false".to_string(), - terms: vec!["false".to_string()], - memory_terms: vec![], - backward_memory_substitutions: 0, - backward_memory_candidate_enumerations: 0, - backward_memory_residual_fallbacks: 0, - precision: SymbolicConditionPrecision::Exact, - evidence: r2types::SymbolicSemanticEvidence::exact(), - confidence: r2types::SymbolicSemanticConfidence::Exact, - supported_paths: 1, - total_paths: 1, - }), - }); - ctx.inputs.symbolic_facts = Box::leak(Box::new(facts)); + let region = crate::test_semantic_region( + 0x1000, + std::collections::BTreeSet::from([0x1004, 0x1008]), + vec![crate::test_control_fact( + 0x1004, + r2sym::SymbolicReachabilityStatus::Reachable, + Some(false), + Some("false"), + Some(r2sym::BackwardConditionSummary { + simplified: "false".to_string(), + terms: vec!["false".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: r2sym::BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }), + r2sym::SemanticEvidence::exact(), + )], + Vec::new(), + ); + ctx.inputs.semantic_artifact = Some(crate::leaked_test_semantic_artifact( + crate::test_native_semantic_artifact( + r2sym::RefinementStage::Compiled, + r2sym::ArtifactGranularity::WholeFunction, + r2sym::SliceClass::Worker, + false, + Vec::new(), + vec![region], + ), + )); let entry = prepared.function().get_block(0x1000).expect("entry"); assert_eq!( @@ -239,64 +242,61 @@ fn actionable_control_island_shortcuts_when_branch_fact_is_not_decisive() { let prepared = prepared_from_r2il_blocks(&[entry, fallthrough, taken], &arch).with_name("control_island"); let mut ctx = make_x86_64_ctx_with_prepared(&prepared); - let mut facts = SymbolicSemanticFacts::default(); - facts.branch_facts.push(SymbolicBranchFact { - block_addr: 0x2000, - true_target: 0x2008, - false_target: 0x2004, - true_status: SymbolicReachabilityStatus::Unknown, - false_status: SymbolicReachabilityStatus::Unknown, - true_condition: Some("false".to_string()), - false_condition: None, - true_compiled: Some(SymbolicCompiledCondition { - simplified: "false".to_string(), - terms: vec!["false".to_string()], - memory_terms: vec![], - backward_memory_substitutions: 0, - backward_memory_candidate_enumerations: 0, - backward_memory_residual_fallbacks: 0, - precision: SymbolicConditionPrecision::OverApprox, - evidence: r2types::SymbolicSemanticEvidence::likely( - r2types::SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: r2types::SymbolicSemanticConfidence::Likely, - supported_paths: 1, - total_paths: 2, - }), - false_compiled: None, - }); - facts.control_islands.push(SymbolicControlIsland { - kind: SymbolicControlIslandKind::LargeCfgBranchFrontier, - anchor_block: 0x2000, - frontier_targets: vec![0x2008, 0x2004], - facts: vec![ - SymbolicControlFact { + let frontier = std::collections::BTreeSet::from([0x2004, 0x2008]); + let region = r2sym::SemanticRegion { + anchor: 0x2000, + frontier: frontier.clone(), + control: vec![r2sym::Judged::new( + r2sym::ControlFact { target: 0x2008, - status: SymbolicReachabilityStatus::Unknown, + status: r2sym::SymbolicReachabilityStatus::Unknown, + branch_truth: Some(true), condition: Some("false".to_string()), - compiled: facts.branch_facts[0].true_compiled.clone(), - evidence: r2types::SymbolicSemanticEvidence::likely( - r2types::SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: r2types::SymbolicSemanticConfidence::Likely, + compiled: Some(r2sym::BackwardConditionSummary { + simplified: "false".to_string(), + terms: vec!["false".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: r2sym::BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }), }, - SymbolicControlFact { - target: 0x2004, - status: SymbolicReachabilityStatus::Unknown, - condition: None, - compiled: None, - evidence: r2types::SymbolicSemanticEvidence::residual( - r2types::SymbolicSemanticEvidenceReason::GuardOpaque, - ), - confidence: r2types::SymbolicSemanticConfidence::Residual, + r2sym::SemanticEvidence::exact(), + )], + memory: Vec::new(), + pre: Vec::new(), + post: Vec::new(), + targets: Vec::new(), + }; + let artifact = r2sym::SemanticArtifact { + stage: r2sym::RefinementStage::Compiled, + granularity: r2sym::ArtifactGranularity::Regioned, + execution: r2sym::ExecutionModel::Native, + body: r2sym::SemanticArtifactBody::Native(r2sym::NativeArtifactBody { + summary: r2sym::NativeFunctionSummary { + slice_class: r2sym::SliceClass::Worker, + closure_functions: 1, + helper_functions: 0, + derived_summaries: 0, + derived_diagnostics: Default::default(), }, - ], - evidence: r2types::SymbolicSemanticEvidence::likely( - r2types::SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: r2types::SymbolicSemanticConfidence::Likely, - }); - ctx.inputs.symbolic_facts = Box::leak(Box::new(facts)); + regions: std::collections::BTreeMap::from([(region.key(), region)]), + }), + diagnostics: r2sym::SemanticArtifactDiagnostics { + branches_evaluated: 0, + branches_pruned: 0, + branches_unknown: 0, + skipped_missing_arch: false, + skipped_large_cfg: false, + residual_reasons: Vec::new(), + ambiguous_targets: Vec::new(), + cache_hit: false, + }, + }; + ctx.inputs.semantic_artifact = Some(Box::leak(Box::new(artifact))); let entry = prepared.function().get_block(0x2000).expect("entry"); assert_eq!( @@ -330,35 +330,39 @@ fn actionable_control_island_parses_non_literal_compiled_condition_expr() { let prepared = prepared_from_r2il_blocks(&[entry, fallthrough, taken], &arch) .with_name("parsed_control_island"); let mut ctx = make_x86_64_ctx_with_prepared(&prepared); - let mut facts = SymbolicSemanticFacts::default(); - facts.control_islands.push(SymbolicControlIsland { - kind: SymbolicControlIslandKind::LargeCfgBranchFrontier, - anchor_block: 0x3000, - frontier_targets: vec![0x3008], - facts: vec![SymbolicControlFact { - target: 0x3008, - status: SymbolicReachabilityStatus::Reachable, - condition: Some("x == 0".to_string()), - compiled: Some(SymbolicCompiledCondition { + let region = crate::test_semantic_region( + 0x3000, + std::collections::BTreeSet::from([0x3008]), + vec![crate::test_control_fact( + 0x3008, + r2sym::SymbolicReachabilityStatus::Reachable, + None, + Some("x == 0"), + Some(r2sym::BackwardConditionSummary { simplified: "x == 0".to_string(), terms: vec!["x == 0".to_string()], - memory_terms: vec![], + memory_terms: Vec::new(), backward_memory_substitutions: 0, backward_memory_candidate_enumerations: 0, backward_memory_residual_fallbacks: 0, - precision: SymbolicConditionPrecision::Exact, - evidence: r2types::SymbolicSemanticEvidence::exact(), - confidence: r2types::SymbolicSemanticConfidence::Exact, + precision: r2sym::BackwardConditionPrecision::Exact, supported_paths: 1, total_paths: 1, }), - evidence: r2types::SymbolicSemanticEvidence::exact(), - confidence: r2types::SymbolicSemanticConfidence::Exact, - }], - evidence: r2types::SymbolicSemanticEvidence::exact(), - confidence: r2types::SymbolicSemanticConfidence::Exact, - }); - ctx.inputs.symbolic_facts = Box::leak(Box::new(facts)); + r2sym::SemanticEvidence::exact(), + )], + Vec::new(), + ); + ctx.inputs.semantic_artifact = Some(crate::leaked_test_semantic_artifact( + crate::test_native_semantic_artifact( + r2sym::RefinementStage::Compiled, + r2sym::ArtifactGranularity::Regioned, + r2sym::SliceClass::Worker, + false, + Vec::new(), + vec![region], + ), + )); assert_eq!( ctx.symbolic_actionable_compiled_condition_expr(0x3000), @@ -395,27 +399,36 @@ fn actionable_memory_island_parses_memory_condition_expr() { let prepared = prepared_from_r2il_blocks(&[entry, fallthrough, taken], &arch).with_name("memory_island"); let mut ctx = make_x86_64_ctx_with_prepared(&prepared); - let mut facts = SymbolicSemanticFacts::default(); - facts.memory_islands.push(r2types::SymbolicMemoryIsland { - kind: r2types::SymbolicMemoryIslandKind::LargeCfgConditionFrontier, - anchor_block: 0x4000, - terms: vec![r2types::SymbolicMemoryCondition { - region: r2types::SymbolicMemoryRegion::Argument { index: 0 }, - offset_lo: 8, - offset_hi: 8, - size: 4, - exact_offset: true, - evidence: r2types::SymbolicSemanticEvidence::exact(), - confidence: r2types::SymbolicSemanticConfidence::Exact, - binding: None, - expr: "*(arg0 + 0x8)".to_string(), - value_expr: Some("0x2a".to_string()), - exact_value: true, - }], - evidence: r2types::SymbolicSemanticEvidence::exact(), - confidence: r2types::SymbolicSemanticConfidence::Exact, - }); - ctx.inputs.symbolic_facts = Box::leak(Box::new(facts)); + let region = crate::test_semantic_region( + 0x4000, + std::collections::BTreeSet::new(), + Vec::new(), + vec![crate::test_memory_fact( + r2sym::BackwardMemoryCondition { + region: r2sym::BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 8, + offset_hi: 8, + size: 4, + exact_offset: true, + evidence: r2sym::SemanticEvidence::exact(), + binding: None, + expr: "*(arg0 + 0x8)".to_string(), + value_expr: Some("0x2a".to_string()), + exact_value: true, + }, + r2sym::SemanticEvidence::exact(), + )], + ); + ctx.inputs.semantic_artifact = Some(crate::leaked_test_semantic_artifact( + crate::test_native_semantic_artifact( + r2sym::RefinementStage::Residual, + r2sym::ArtifactGranularity::Regioned, + r2sym::SliceClass::Worker, + true, + vec![r2sym::ResidualReason::LargeCfg], + vec![region], + ), + )); assert_eq!( ctx.extract_condition_from_block(prepared.function().get_block(0x4000).expect("entry")), diff --git a/crates/r2dec/src/fold/tests/pipeline.rs b/crates/r2dec/src/fold/tests/pipeline.rs index 8bc48c9..75ee3f3 100644 --- a/crates/r2dec/src/fold/tests/pipeline.rs +++ b/crates/r2dec/src/fold/tests/pipeline.rs @@ -226,7 +226,7 @@ mod tests { external_stack_vars: empty_stack, visible_bindings: empty_visible, external_type_db: Box::leak(Box::new(r2types::ExternalTypeDb::default())), - symbolic_facts: Box::leak(Box::new(r2types::SymbolicSemanticFacts::default())), + semantic_artifact: None, param_register_aliases: empty_str, type_hints: empty_ty, type_oracle: None, @@ -276,7 +276,7 @@ mod tests { external_stack_vars: empty_stack, visible_bindings: empty_visible, external_type_db: Box::leak(Box::new(r2types::ExternalTypeDb::default())), - symbolic_facts: Box::leak(Box::new(r2types::SymbolicSemanticFacts::default())), + semantic_artifact: None, param_register_aliases: empty_str, type_hints: empty_ty, type_oracle: None, diff --git a/crates/r2dec/src/lib.rs b/crates/r2dec/src/lib.rs index de60f51..c616637 100644 --- a/crates/r2dec/src/lib.rs +++ b/crates/r2dec/src/lib.rs @@ -32,8 +32,13 @@ pub(crate) mod address; pub(crate) mod analysis; pub mod ast; pub mod codegen; +pub(crate) mod consumer_fallback; +pub(crate) mod consumer_linear; +pub(crate) mod consumer_structured; +pub(crate) mod consumer_vm; pub mod fold; pub(crate) mod normalize; +pub(crate) mod planner; pub(crate) mod post_rename; pub mod region; pub(crate) mod registers; @@ -43,6 +48,7 @@ pub mod variable; pub use ast::{BinaryOp, CExpr, CFunction, CStmt, CType, UnaryOp}; pub use codegen::{CodeGenConfig, CodeGenerator, generate}; pub use fold::lower_ssa_ops_to_stmts; +pub use planner::SemanticRoutePlan; pub use region::{Region, RegionAnalyzer}; pub use structure::ControlFlowStructurer; pub use variable::VariableRecovery; @@ -53,9 +59,9 @@ use r2il::R2ILBlock; use r2ssa::SSAFunction; use r2ssa::SSAOp; use r2types::{ - CTypeLike, ExternalRegisterParamSpec, ExternalTypeDb, FunctionSignatureSpec, FunctionType, - FunctionTypeFacts, StackSlotKey, SymbolicInterpreterKind, TypeInference, TypeOracle, - VisibleBinding, VisibleBindingKind, + CTypeLike, ExternalRegisterParamSpec, ExternalTypeDb, FunctionFacts, FunctionSignatureSpec, + FunctionType, FunctionTypeFacts, StackSlotKey, TypeInference, TypeOracle, VisibleBinding, + VisibleBindingKind, }; use std::collections::HashSet; use std::fmt::Write as _; @@ -85,42 +91,25 @@ fn normalize_callee_name(name: &str) -> String { out } -fn prefer_symbolic_large_worker_decompile(type_facts: &FunctionTypeFacts) -> bool { - let symbolic = &type_facts.symbolic_facts; - (symbolic.decompile_ready() || symbolic.structured_decompile_ready()) - && symbolic.diagnostics.skipped_large_cfg - && matches!( - symbolic.semantic_mode(), - Some( - r2types::SymbolicSemanticMode::Residual - | r2types::SymbolicSemanticMode::IslandCompiled - ) - ) - && !symbolic.worker_islands.is_empty() +fn prefer_symbolic_large_worker_decompile( + semantic_artifact: Option<&r2sym::SemanticArtifact>, +) -> bool { + planner::prefer_symbolic_large_worker_decompile(semantic_artifact) } fn should_skip_runtime_type_inference( prepared: Option<&r2ssa::SsaArtifact>, - type_facts: &FunctionTypeFacts, + _type_facts: &FunctionTypeFacts, + semantic_artifact: Option<&r2sym::SemanticArtifact>, ) -> bool { - if prefer_symbolic_large_worker_decompile(type_facts) { - return true; - } - let Some(prepared) = prepared else { - return false; - }; - let summary = prepared.function().cfg_risk_summary(); - summary.block_count >= 96 - && summary.switch_block_count > 0 - && summary.max_switch_cases >= 32 - && summary.back_edge_count == 0 + planner::should_skip_runtime_type_inference(prepared, _type_facts, semantic_artifact) } fn should_use_prepared_semantic_view( prepared: Option<&r2ssa::SsaArtifact>, - type_facts: &FunctionTypeFacts, + semantic_artifact: Option<&r2sym::SemanticArtifact>, ) -> bool { - prepared.is_some() && !prefer_symbolic_large_worker_decompile(type_facts) + planner::should_use_prepared_semantic_view(prepared, semantic_artifact) } fn seed_runtime_type_hints_from_facts_and_recovery( @@ -214,19 +203,60 @@ fn format_vm_target_list(targets: &[u64]) -> String { format!("[{rendered}]") } -fn format_vm_state_updates(updates: &[r2types::SymbolicVmStateUpdate]) -> String { +fn format_vm_value_expr(value: &r2sym::VmValueExpr) -> String { + match value { + r2sym::VmValueExpr::Const(value) => format!("0x{value:x}"), + r2sym::VmValueExpr::Var(name) | r2sym::VmValueExpr::Expr(name) => name.clone(), + r2sym::VmValueExpr::Unary { op, arg } => { + let op = match op { + r2sym::VmUnaryOp::Neg => "-", + r2sym::VmUnaryOp::BitNot => "~", + r2sym::VmUnaryOp::BoolNot => "!", + }; + format!("({}{})", op, format_vm_value_expr(arg)) + } + r2sym::VmValueExpr::Binary { op, lhs, rhs } => { + let op = match op { + r2sym::VmBinaryOp::Add => "+", + r2sym::VmBinaryOp::Sub => "-", + r2sym::VmBinaryOp::Mul => "*", + r2sym::VmBinaryOp::Div => "/", + r2sym::VmBinaryOp::Rem => "%", + r2sym::VmBinaryOp::And => "&", + r2sym::VmBinaryOp::Or => "|", + r2sym::VmBinaryOp::Xor => "^", + r2sym::VmBinaryOp::Shl => "<<", + r2sym::VmBinaryOp::LShr | r2sym::VmBinaryOp::AShr => ">>", + r2sym::VmBinaryOp::Eq => "==", + r2sym::VmBinaryOp::Ne => "!=", + r2sym::VmBinaryOp::Lt | r2sym::VmBinaryOp::SLt => "<", + r2sym::VmBinaryOp::Le | r2sym::VmBinaryOp::SLe => "<=", + r2sym::VmBinaryOp::BoolAnd => "&&", + r2sym::VmBinaryOp::BoolOr => "||", + }; + format!( + "({} {} {})", + format_vm_value_expr(lhs), + op, + format_vm_value_expr(rhs) + ) + } + } +} + +fn format_vm_state_updates(updates: &[r2sym::VmStateUpdate]) -> String { if updates.is_empty() { return "[]".to_string(); } let rendered = updates .iter() - .map(|update| format!("{}={}", update.output, update.value.render())) + .map(|update| format!("{}={}", update.output, format_vm_value_expr(&update.value))) .collect::>() .join(", "); format!("[{rendered}]") } -fn format_vm_guarded_exits(guards: &[r2types::SymbolicVmGuardedExit]) -> String { +fn format_vm_guarded_exits(guards: &[r2sym::VmGuardedExit]) -> String { if guards.is_empty() { return "[]".to_string(); } @@ -238,17 +268,14 @@ fn format_vm_guarded_exits(guards: &[r2types::SymbolicVmGuardedExit]) -> String format!("[{rendered}]") } -fn format_vm_memory_conditions(conditions: &[r2types::SymbolicMemoryCondition]) -> String { +fn format_vm_memory_conditions(conditions: &[r2sym::VmMemoryCondition]) -> String { if conditions.is_empty() { return "[]".to_string(); } let rendered = conditions .iter() .map(|condition| { - let region = match &condition.region { - r2types::SymbolicMemoryRegion::Argument { index } => format!("arg{index}"), - r2types::SymbolicMemoryRegion::Region(region) => region.name.clone(), - }; + let region = condition.region.name.clone(); let binding = condition .binding .as_deref() @@ -275,14 +302,14 @@ fn format_vm_memory_conditions(conditions: &[r2types::SymbolicMemoryCondition]) format!("[{rendered}]") } -fn format_vm_summary_kind(kind: SymbolicInterpreterKind) -> &'static str { +pub(crate) fn format_vm_summary_kind(kind: r2sym::InterpreterKind) -> &'static str { match kind { - SymbolicInterpreterKind::SwitchDispatch => "switch_dispatch", - SymbolicInterpreterKind::IndirectDispatch => "indirect_dispatch", + r2sym::InterpreterKind::SwitchDispatch => "switch_dispatch", + r2sym::InterpreterKind::IndirectDispatch => "indirect_dispatch", } } -fn is_autogenerated_function_name(name: &str) -> bool { +pub(crate) fn is_autogenerated_function_name(name: &str) -> bool { let underscore_hex_addr = name .strip_prefix('_') .is_some_and(|rest| !rest.is_empty() && rest.chars().all(|ch| ch.is_ascii_hexdigit())); @@ -295,451 +322,115 @@ fn is_autogenerated_function_name(name: &str) -> bool { || underscore_hex_addr } -fn semantic_mode_label(mode: r2types::SymbolicSemanticMode) -> &'static str { - match mode { - r2types::SymbolicSemanticMode::Raw => "raw", - r2types::SymbolicSemanticMode::Compiled => "compiled", - r2types::SymbolicSemanticMode::IslandCompiled => "island_compiled", - r2types::SymbolicSemanticMode::Residual => "residual", - r2types::SymbolicSemanticMode::VmSummary => "vm_summary", +pub(crate) fn semantic_mode_label(artifact: &r2sym::SemanticArtifact) -> &'static str { + match (artifact.execution, artifact.stage, artifact.granularity) { + (r2sym::ExecutionModel::Vm, _, _) => "vm_summary", + (_, r2sym::RefinementStage::Raw, _) => "raw", + (_, r2sym::RefinementStage::Compiled, r2sym::ArtifactGranularity::Regioned) => { + "island_compiled" + } + (_, r2sym::RefinementStage::Compiled, _) => "compiled", + (_, r2sym::RefinementStage::Residual, _) => "residual", } } -fn semantic_slice_class_label(slice_class: r2types::SymbolicSemanticSliceClass) -> &'static str { +pub(crate) fn semantic_slice_class_label(slice_class: r2sym::SliceClass) -> &'static str { match slice_class { - r2types::SymbolicSemanticSliceClass::Wrapper => "wrapper", - r2types::SymbolicSemanticSliceClass::Worker => "worker", - r2types::SymbolicSemanticSliceClass::RecursiveGroup => "recursive_group", - r2types::SymbolicSemanticSliceClass::InterpreterSwitch => "interpreter_switch", - r2types::SymbolicSemanticSliceClass::InterpreterIndirect => "interpreter_indirect", - r2types::SymbolicSemanticSliceClass::GenericLarge => "generic_large", + r2sym::SliceClass::Wrapper => "wrapper", + r2sym::SliceClass::Worker => "worker", + r2sym::SliceClass::RecursiveGroup => "recursive_group", + r2sym::SliceClass::InterpreterSwitch => "interpreter_switch", + r2sym::SliceClass::InterpreterIndirect => "interpreter_indirect", + r2sym::SliceClass::GenericLarge => "generic_large", } } -fn semantic_residual_reason_label(reason: r2types::SymbolicSemanticResidualReason) -> &'static str { +pub(crate) fn semantic_residual_reason_label(reason: r2sym::ResidualReason) -> &'static str { match reason { - r2types::SymbolicSemanticResidualReason::MissingArch => "missing_arch", - r2types::SymbolicSemanticResidualReason::LargeCfg => "large_cfg", - r2types::SymbolicSemanticResidualReason::SummaryBudgetExhausted => { - "summary_budget_exhausted" - } - r2types::SymbolicSemanticResidualReason::SccBudgetExhausted => "scc_budget_exhausted", - r2types::SymbolicSemanticResidualReason::InterpreterRequiresStepSummary => { + r2sym::ResidualReason::MissingArch => "missing_arch", + r2sym::ResidualReason::LargeCfg => "large_cfg", + r2sym::ResidualReason::SummaryBudgetExhausted => "summary_budget_exhausted", + r2sym::ResidualReason::SccBudgetExhausted => "scc_budget_exhausted", + r2sym::ResidualReason::InterpreterRequiresStepSummary => { "interpreter_requires_step_summary" } } } -fn render_vm_semantic_fallback_comment( +pub fn semantic_fallback_comment( func_name: &str, - symbolic_facts: &r2types::SymbolicSemanticFacts, + semantic_artifact: Option<&r2sym::SemanticArtifact>, ) -> Option { - if !matches!( - symbolic_facts.diagnostics.semantic_mode, - Some(r2types::SymbolicSemanticMode::VmSummary) - ) { - return None; - } - let vm_step = symbolic_facts - .vm_step - .as_ref() - .or(symbolic_facts.vm_transfer.as_ref())?; - let kind = match vm_step.kind { - r2types::SymbolicInterpreterKind::SwitchDispatch => "switch_dispatch", - r2types::SymbolicInterpreterKind::IndirectDispatch => "indirect_dispatch", - }; - let selector = vm_step.selector.as_deref().unwrap_or("unknown"); - let inputs = if vm_step.state_inputs.is_empty() { - "none".to_string() - } else { - vm_step.state_inputs.join(", ") - }; - let outputs = if vm_step.state_outputs.is_empty() { - "none".to_string() - } else { - vm_step.state_outputs.join(", ") - }; - let exact_transfers = vm_step - .transfers - .iter() - .filter(|transfer| transfer.exact) - .count(); - let likely_transfers = vm_step - .transfers - .iter() - .filter(|transfer| { - matches!( - transfer.confidence, - r2types::SymbolicSemanticConfidence::Likely - ) - }) - .count(); - let heuristic_transfers = vm_step - .transfers - .iter() - .filter(|transfer| { - matches!( - transfer.confidence, - r2types::SymbolicSemanticConfidence::Heuristic - ) - }) - .count(); - let redispatch_transfers = vm_step - .transfers - .iter() - .filter(|transfer| transfer.redispatch) - .count(); - let returning_transfers = vm_step - .transfers - .iter() - .filter(|transfer| transfer.may_return) - .count(); - let selector_updates = vm_step - .transfers - .iter() - .filter(|transfer| transfer.selector_update.is_some()) - .count(); - let exit_guards = vm_step - .transfers - .iter() - .map(|transfer| transfer.exit_guards.len()) - .sum::(); - let residual_guards = vm_step - .transfers - .iter() - .filter(|transfer| transfer.residual_guards) - .count(); - let residual_memory = vm_step - .transfers - .iter() - .filter(|transfer| transfer.residual_memory_effects) - .count(); - let read_effects = vm_step - .handler_memory_read_effects - .values() - .map(Vec::len) - .sum::(); - let write_effects = vm_step - .handler_memory_write_effects - .values() - .map(Vec::len) - .sum::(); - let total_reads: usize = vm_step.handler_memory_reads.values().copied().sum(); - let total_writes: usize = vm_step.handler_memory_writes.values().copied().sum(); - let handler_preview = vm_step - .dispatch_targets - .iter() - .take(3) - .map(|target| { - let values = vm_step - .case_values_by_target - .get(target) - .map(|values| { - values - .iter() - .map(|value| format!("0x{value:x}")) - .collect::>() - .join("|") - }) - .unwrap_or_else(|| "default".to_string()); - let updates = vm_step - .handler_state_updates - .get(target) - .map(|updates| { - updates - .iter() - .take(3) - .map(|update| format!("{}={}", update.output, update.expr)) - .collect::>() - .join(", ") - }) - .filter(|text| !text.is_empty()) - .unwrap_or_else(|| "no_state_updates".to_string()); - format!("0x{target:x}[{values}] => {updates}") - }) - .collect::>() - .join("; "); - Some(format!( - "/* r2dec semantic summary: vm_summary for {} ({kind} @ 0x{:x}, loop_header=0x{:x}, selector={}, targets={}, redispatch={}, exact_transfers={}, likely_transfers={}, heuristic_transfers={}, redispatch_transfers={}, returning_transfers={}, selector_updates={}, exact_exit_guards={}, residual_guards={}, residual_memory={}, total_reads={}, total_writes={}, read_effects={}, write_effects={}, state_inputs=[{}], state_outputs=[{}], handlers={}) */", - func_name, - vm_step.dispatch_header, - vm_step.loop_header, - selector, - vm_step.dispatch_targets.len(), - vm_step.redispatch_handlers.len(), - exact_transfers, - likely_transfers, - heuristic_transfers, - redispatch_transfers, - returning_transfers, - selector_updates, - exit_guards, - residual_guards, - residual_memory, - total_reads, - total_writes, - read_effects, - write_effects, - inputs, - outputs, - handler_preview, - )) + consumer_fallback::semantic_fallback_comment(func_name, semantic_artifact) } -pub fn semantic_fallback_comment( +pub fn preferred_semantic_fallback_comment( func_name: &str, - symbolic_facts: &r2types::SymbolicSemanticFacts, + semantic_artifact: Option<&r2sym::SemanticArtifact>, ) -> Option { - if let Some(comment) = render_vm_semantic_fallback_comment(func_name, symbolic_facts) { - return Some(comment); - } - let mode = symbolic_facts.diagnostics.semantic_mode?; - let slice_class = symbolic_facts.diagnostics.slice_class?; - let mut reason = format!( - "semantic fallback: {} slice in {} mode", - semantic_slice_class_label(slice_class), - semantic_mode_label(mode) - ); - if !symbolic_facts.diagnostics.residual_reasons.is_empty() { - reason.push_str(" ("); - reason.push_str( - &symbolic_facts - .diagnostics - .residual_reasons - .iter() - .map(|reason| semantic_residual_reason_label(*reason)) - .collect::>() - .join(", "), - ); - reason.push(')'); - } - - let control_facts = if symbolic_facts.worker_islands.is_empty() { - symbolic_facts - .control_islands - .iter() - .flat_map(|island| island.facts.iter()) - .collect::>() - } else { - symbolic_facts - .worker_islands - .iter() - .flat_map(|island| island.control_facts.iter()) - .collect::>() - }; - if !symbolic_facts.branch_facts.is_empty() - || !symbolic_facts.worker_islands.is_empty() - || !symbolic_facts.memory_islands.is_empty() - { - reason.push_str(&format!( - "; branch_facts={}, worker_islands={}, control_islands={}, memory_islands={}, actionable_conditions={}, exact_conditions={}", - symbolic_facts.branch_facts.len(), - symbolic_facts.worker_islands.len(), - symbolic_facts.control_islands.len(), - symbolic_facts.memory_islands.len(), - control_facts - .iter() - .filter(|fact| fact.evidence.allows_narrowing()) - .count(), - control_facts - .iter() - .filter(|fact| fact.evidence.allows_hard_proof()) - .count(), - )); - } - let actionable_preview = if symbolic_facts.worker_islands.is_empty() { - symbolic_facts - .control_islands - .iter() - .filter_map(|island| { - island.actionable_compiled_condition().map(|condition| { - format!("0x{:x}: {}", island.anchor_block, condition.simplified) - }) - }) - .take(3) - .collect::>() - } else { - symbolic_facts - .worker_islands - .iter() - .filter_map(|island| { - island.actionable_compiled_condition().map(|condition| { - format!("0x{:x}: {}", island.anchor_block, condition.simplified) - }) - }) - .take(3) - .collect::>() - }; - if !actionable_preview.is_empty() { - reason.push_str("; actionable_preview=["); - reason.push_str(&actionable_preview.join(" | ")); - reason.push(']'); - } - Some(format!( - "/* r2dec fallback: skipped decompilation for {} ({}) */", - func_name, reason - )) + planner::preferred_semantic_fallback_comment(func_name, semantic_artifact) } -pub fn preferred_semantic_fallback_comment( +pub fn detached_semantic_linearization_reason( func_name: &str, - symbolic_facts: &r2types::SymbolicSemanticFacts, + blocks: &[R2ILBlock], + semantic_artifact: Option<&r2sym::SemanticArtifact>, ) -> Option { - if matches!( - symbolic_facts.diagnostics.semantic_mode, - Some(r2types::SymbolicSemanticMode::VmSummary) - ) { - return semantic_fallback_comment(func_name, symbolic_facts); - } - if !is_autogenerated_function_name(func_name) { - return None; - } - if symbolic_facts.decompile_ready() { - return None; - } - if symbolic_facts.diagnostics.skipped_large_cfg { - return semantic_fallback_comment(func_name, symbolic_facts); - } - symbolic_facts - .diagnostics - .residual_reasons - .contains(&r2types::SymbolicSemanticResidualReason::InterpreterRequiresStepSummary) - .then(|| semantic_fallback_comment(func_name, symbolic_facts)) - .flatten() + planner::detached_semantic_linearization_reason(func_name, blocks, semantic_artifact) } +pub fn detached_semantic_route_plan( + func_name: &str, + blocks: &[R2ILBlock], + semantic_artifact: Option<&r2sym::SemanticArtifact>, +) -> Option { + planner::detached_semantic_route_plan(func_name, blocks, semantic_artifact) +} + +#[cfg_attr(not(test), allow(dead_code))] fn preferred_semantic_linearization_reason( func_name: &str, - symbolic_facts: &r2types::SymbolicSemanticFacts, + semantic_artifact: Option<&r2sym::SemanticArtifact>, cfg_summary: &r2ssa::CFGRiskSummary, ) -> Option { - if !is_autogenerated_function_name(func_name) { - return None; - } - if !symbolic_facts.decompile_ready() || !symbolic_facts.diagnostics.skipped_large_cfg { - return None; - } - if symbolic_facts.structured_decompile_ready() { - return None; - } - if symbolic_facts.worker_islands.is_empty() - && symbolic_facts.control_islands.is_empty() - && symbolic_facts.memory_islands.is_empty() - { - return None; - } - Some(preferred_semantic_worker_reason(cfg_summary)) + planner::preferred_semantic_linearization_reason(func_name, semantic_artifact, cfg_summary) } +#[cfg_attr(not(test), allow(dead_code))] fn preferred_semantic_structuring_reason( func_name: &str, - symbolic_facts: &r2types::SymbolicSemanticFacts, + semantic_artifact: Option<&r2sym::SemanticArtifact>, cfg_summary: &r2ssa::CFGRiskSummary, ) -> Option { - if !is_autogenerated_function_name(func_name) { - return None; - } - if !symbolic_facts.structured_decompile_ready() || !symbolic_facts.diagnostics.skipped_large_cfg - { - return None; - } - Some(preferred_semantic_worker_reason(cfg_summary)) + planner::preferred_semantic_structuring_reason(func_name, semantic_artifact, cfg_summary) } fn preferred_semantic_worker_reason(cfg_summary: &r2ssa::CFGRiskSummary) -> String { - cfg_guard_reason_from_summary(cfg_summary) - .unwrap_or_else(|| "semantic worker islands".to_string()) + planner::preferred_semantic_worker_reason(cfg_summary) } pub fn cfg_guard_reason_from_summary(summary: &r2ssa::CFGRiskSummary) -> Option { - if summary.loop_count > 8 || summary.back_edge_count > 16 { - return Some(format!( - "complex loop graph (loops={}, back_edges={})", - summary.loop_count, summary.back_edge_count - )); - } - - if summary.loop_count > 4 && summary.block_count >= 96 && summary.max_switch_cases >= 32 { - return Some(format!( - "large dense switch in looped CFG (blocks={}, loops={}, max_switch_cases={})", - summary.block_count, summary.loop_count, summary.max_switch_cases - )); - } - - None + planner::cfg_guard_reason_from_summary(summary) } pub fn cfg_guard_reason(blocks: &[R2ILBlock]) -> Option { - let ssa_func = SSAFunction::from_blocks_raw_no_arch(blocks)?; - cfg_guard_reason_from_summary(&ssa_func.cfg_risk_summary()) + planner::cfg_guard_reason(blocks) } pub fn block_guard_fallback_comment(func_name: &str, blocks: usize, max_blocks: usize) -> String { - format!( - "/* r2dec fallback: skipped decompilation for {} ({} blocks > limit {}). Set SLEIGH_DEC_MAX_BLOCKS to override. */", - func_name, blocks, max_blocks - ) + planner::block_guard_fallback_comment(func_name, blocks, max_blocks) } pub fn artifact_guard_fallback_comment(func_name: &str, reason: &str) -> String { - format!( - "/* r2dec fallback: skipped decompilation for {} ({}) */", - func_name, reason - ) + planner::artifact_guard_fallback_comment(func_name, reason) } pub fn render_semantic_worker_linearization( plan: &r2types::TypeWritebackPlan, - symbolic_facts: &r2types::SymbolicSemanticFacts, + semantic_artifact: Option<&r2sym::SemanticArtifact>, reason: &str, ) -> String { - let mut out = String::new(); - for decl in &plan.struct_decls { - let _ = writeln!(&mut out, "{}", decl.decl); - } - if !plan.struct_decls.is_empty() { - out.push('\n'); - } - let _ = writeln!(&mut out, "{} {{", plan.signature.signature); - let _ = writeln!( - &mut out, - " /* r2dec semantic worker linearization: {} */", - reason - ); - for warning in plan.diagnostics.warnings.iter().take(2) { - let _ = writeln!(&mut out, " /* {} */", warning); - } - - let mut emitted_any = false; - for island in symbolic_facts.worker_islands.iter().take(6) { - if let (Some(target), Some(condition)) = ( - island.actionable_reachable_target(), - island.actionable_compiled_condition(), - ) { - let _ = writeln!( - &mut out, - " /* 0x{:x}: if ({}) => 0x{:x} [{:?}] */", - island.anchor_block, condition.simplified, target, condition.evidence.tier - ); - emitted_any = true; - } - for term in island.actionable_terms().into_iter().take(2) { - let _ = writeln!( - &mut out, - " /* 0x{:x}: {} [{:?}] */", - island.anchor_block, term.expr, term.evidence.tier - ); - emitted_any = true; - } - } - if !emitted_any { - let _ = writeln!( - &mut out, - " /* no actionable worker-island statements recovered */" - ); - } - let _ = writeln!(&mut out, "}}"); - out + consumer_linear::render_semantic_worker_linearization(plan, semantic_artifact, reason) } fn merge_params_with_external_signature( @@ -1097,11 +788,28 @@ pub struct DecompilerContext { pub strings: std::collections::HashMap, /// Symbol/global variable names. pub symbols: std::collections::HashMap, - /// Externally recovered type and layout facts. - pub type_facts: FunctionTypeFacts, + /// Canonical combined type and semantic facts. + pub function_facts: FunctionFacts, } impl DecompilerContext { + fn canonicalize_function_facts(mut function_facts: FunctionFacts) -> FunctionFacts { + function_facts.types = function_facts.types.canonicalized(); + function_facts + } + + pub fn type_facts(&self) -> &FunctionTypeFacts { + &self.function_facts.types + } + + pub fn type_facts_mut(&mut self) -> &mut FunctionTypeFacts { + &mut self.function_facts.types + } + + pub fn semantic_artifact(&self) -> Option<&r2sym::SemanticArtifact> { + self.function_facts.semantics.as_ref() + } + pub fn from_analysis_inputs( mut type_facts: FunctionTypeFacts, function_names: std::collections::HashMap, @@ -1119,7 +827,32 @@ impl DecompilerContext { function_names, strings, symbols, - type_facts: type_facts.canonicalized(), + function_facts: FunctionFacts::new(type_facts.canonicalized(), None), + } + } + + pub fn from_function_facts( + mut function_facts: FunctionFacts, + function_names: std::collections::HashMap, + strings: std::collections::HashMap, + symbols: std::collections::HashMap, + ptr_bits: u32, + ) -> Self { + r2types::enrich_known_function_signatures_from_names( + &mut function_facts.types, + &function_names, + ptr_bits, + ); + r2types::enrich_known_function_signatures_from_names( + &mut function_facts.types, + &symbols, + ptr_bits, + ); + Self { + function_names, + strings, + symbols, + function_facts: Self::canonicalize_function_facts(function_facts), } } @@ -1142,7 +875,20 @@ impl DecompilerContext { } pub fn with_type_facts(mut self, type_facts: FunctionTypeFacts) -> Self { - self.type_facts = type_facts.canonicalized(); + self.function_facts.types = type_facts.canonicalized(); + self + } + + pub fn with_semantic_artifact( + mut self, + semantic_artifact: Option, + ) -> Self { + self.function_facts.semantics = semantic_artifact; + self + } + + pub fn with_function_facts(mut self, function_facts: FunctionFacts) -> Self { + self.function_facts = Self::canonicalize_function_facts(function_facts); self } } @@ -1156,7 +902,8 @@ pub struct DecompilerInput { impl DecompilerInput { pub fn new(prepared_ssa: r2ssa::SsaArtifact, mut context: DecompilerContext) -> Self { - context.type_facts = context.type_facts.canonicalized(); + context.function_facts = + DecompilerContext::canonicalize_function_facts(context.function_facts); Self { prepared_ssa, interproc_summary_set: None, @@ -1165,7 +912,8 @@ impl DecompilerInput { } pub fn with_context(mut self, mut context: DecompilerContext) -> Self { - context.type_facts = context.type_facts.canonicalized(); + context.function_facts = + DecompilerContext::canonicalize_function_facts(context.function_facts); self.context = context; self } @@ -1196,7 +944,8 @@ impl Decompiler { /// Set external context (function names, strings, symbols). pub fn with_context(mut self, mut context: DecompilerContext) -> Self { - context.type_facts = context.type_facts.canonicalized(); + context.function_facts = + DecompilerContext::canonicalize_function_facts(context.function_facts); self.context = context; self } @@ -1223,7 +972,7 @@ impl Decompiler { ) where T: Into, { - self.context.type_facts.known_function_signatures = signatures + self.context.type_facts_mut().known_function_signatures = signatures .into_iter() .map(|(name, sig)| (name, sig.into())) .collect(); @@ -1231,12 +980,16 @@ impl Decompiler { /// Set externally recovered host type database. pub fn set_external_type_db(&mut self, external_type_db: ExternalTypeDb) { - self.context.type_facts.external_type_db = external_type_db; + self.context.type_facts_mut().external_type_db = external_type_db; } /// Set externally recovered type facts. pub fn set_type_facts(&mut self, type_facts: FunctionTypeFacts) { - self.context.type_facts = type_facts.canonicalized(); + self.context.function_facts.types = type_facts.canonicalized(); + } + + pub fn set_function_facts(&mut self, function_facts: FunctionFacts) { + self.context = self.context.clone().with_function_facts(function_facts); } /// Decompile an SSA function to C code. @@ -1266,7 +1019,7 @@ impl Decompiler { ) } - fn stmt_has_content(stmt: &CStmt) -> bool { + pub(crate) fn stmt_has_content(stmt: &CStmt) -> bool { match stmt { CStmt::Empty => false, CStmt::Block(stmts) => !stmts.is_empty(), @@ -1274,7 +1027,7 @@ impl Decompiler { } } - fn prepend_comment(stmt: CStmt, text: String) -> CStmt { + pub(crate) fn prepend_comment(stmt: CStmt, text: String) -> CStmt { let comment = CStmt::comment(text); match stmt { CStmt::Empty => CStmt::Block(vec![comment]), @@ -1287,36 +1040,26 @@ impl Decompiler { } fn semantic_vm_summary_comment(&self) -> Option { - let vm_step = self - .context - .type_facts - .symbolic_facts - .vm_step + let vm_body = self.context.semantic_artifact()?.vm_body()?; + let vm_step = vm_body + .step_summary .as_ref() - .or(self.context.type_facts.symbolic_facts.vm_transfer.as_ref())?; + .or(vm_body.transfer_summary.as_ref())?; let exact_transfers = vm_step .transfers .iter() - .filter(|transfer| transfer.evidence.allows_hard_proof()) + .filter(|transfer| transfer.evidence().allows_hard_proof()) .count(); let likely_transfers = vm_step .transfers .iter() - .filter(|transfer| { - matches!( - transfer.evidence.tier, - r2types::SymbolicSemanticConfidence::Likely - ) - }) + .filter(|transfer| matches!(transfer.confidence(), r2sym::SemanticConfidence::Likely)) .count(); let heuristic_transfers = vm_step .transfers .iter() .filter(|transfer| { - matches!( - transfer.evidence.tier, - r2types::SymbolicSemanticConfidence::Heuristic - ) + matches!(transfer.confidence(), r2sym::SemanticConfidence::Heuristic) }) .count(); let redispatch_transfers = vm_step @@ -1338,7 +1081,7 @@ impl Decompiler { .transfers .iter() .flat_map(|transfer| transfer.exit_guards.iter()) - .filter(|guard| guard.guard.evidence.allows_hard_proof()) + .filter(|guard| guard.guard.evidence().allows_hard_proof()) .count(); let total_read_effects: usize = vm_step .handler_memory_read_effects @@ -1411,9 +1154,9 @@ impl Decompiler { selector_update, format_vm_memory_conditions(&transfer.memory_reads), format_vm_memory_conditions(&transfer.memory_writes), - transfer.evidence.allows_hard_proof(), - transfer.evidence.tier, - format_args!("{:?}", transfer.evidence.reasons), + transfer.evidence().allows_hard_proof(), + transfer.confidence(), + format_args!("{:?}", transfer.evidence().reasons), transfer.residual_guards, transfer.residual_memory_effects, transfer.redispatch, @@ -1561,11 +1304,14 @@ impl Decompiler { self.config.arg_regs.clone(), self.config.ret_regs.clone(), ); - var_recovery.set_type_facts(self.context.type_facts.clone()); + var_recovery.set_type_facts(self.context.type_facts().clone()); var_recovery.recover(func); - let skip_runtime_type_inference = - should_skip_runtime_type_inference(prepared, &self.context.type_facts); + let skip_runtime_type_inference = should_skip_runtime_type_inference( + prepared, + self.context.type_facts(), + self.context.semantic_artifact(), + ); let type_inference = (!skip_runtime_type_inference).then(|| { let mut type_inference = TypeInference::new_with_abi( self.config.ptr_size, @@ -1575,17 +1321,23 @@ impl Decompiler { if !self.context.function_names.is_empty() { type_inference.set_function_names(self.context.function_names.clone()); } - type_inference.set_external_signature(self.context.type_facts.merged_signature.clone()); - for (name, signature) in &self.context.type_facts.known_function_signatures { + type_inference + .set_external_signature(self.context.type_facts().merged_signature.clone()); + for (name, signature) in &self.context.type_facts().known_function_signatures { type_inference.add_function_type(name, signature.clone()); } - type_inference.set_external_stack_slots(self.context.type_facts.stack_slots.clone()); - if !self.context.type_facts.external_type_db.structs.is_empty() - || !self.context.type_facts.external_type_db.unions.is_empty() - || !self.context.type_facts.external_type_db.enums.is_empty() + type_inference.set_external_stack_slots(self.context.type_facts().stack_slots.clone()); + if !self + .context + .type_facts() + .external_type_db + .structs + .is_empty() + || !self.context.type_facts().external_type_db.unions.is_empty() + || !self.context.type_facts().external_type_db.enums.is_empty() { type_inference - .set_external_type_db(self.context.type_facts.external_type_db.clone()); + .set_external_type_db(self.context.type_facts().external_type_db.clone()); } if let Some(prepared) = prepared { type_inference.set_prepared_ssa(prepared); @@ -1602,7 +1354,10 @@ impl Decompiler { .map(|(name, ty)| (name, type_like_to_ctype(&ty))) .collect::>() } else { - seed_runtime_type_hints_from_facts_and_recovery(&self.context.type_facts, &var_recovery) + seed_runtime_type_hints_from_facts_and_recovery( + self.context.type_facts(), + &var_recovery, + ) }; let combined_type_oracle = type_inference .as_ref() @@ -1613,7 +1368,7 @@ impl Decompiler { let known_function_signatures = self .context - .type_facts + .type_facts() .known_function_signatures .iter() .map(|(name, ty)| (normalize_callee_name(name), ty.clone())) @@ -1642,12 +1397,12 @@ impl Decompiler { .iter() .map(|(_, param)| param.clone()) .collect(), - self.context.type_facts.merged_signature.as_ref(), + self.context.type_facts().merged_signature.as_ref(), ); let param_register_aliases = build_param_register_aliases( ¶ms, &recovered_param_infos, - &self.context.type_facts.register_params, + &self.context.type_facts().register_params, &self.config.arg_regs, ); for (idx, (_ssa_var, _)) in recovered_param_infos.iter().enumerate() { @@ -1674,7 +1429,7 @@ impl Decompiler { .map(|type_inference| self.infer_return_type(func, type_inference)) .or_else(|| { self.context - .type_facts + .type_facts() .merged_signature .as_ref() .and_then(|sig| sig.ret_type.as_ref().map(type_like_to_ctype)) @@ -1682,11 +1437,10 @@ impl Decompiler { .unwrap_or(CType::Unknown); let signature_ret_type = self .context - .type_facts + .type_facts() .merged_signature .as_ref() .and_then(|sig| sig.ret_type.as_ref().map(type_like_to_ctype)); - let fold_arch = FoldArchConfig { ptr_size: self.config.ptr_size, sp_name: self.config.sp_name.clone(), @@ -1700,34 +1454,37 @@ impl Decompiler { arg_regs: self.config.arg_regs.clone(), caller_saved_regs: self.config.caller_saved_regs.clone(), }; - let prepared_semantic_view = - should_use_prepared_semantic_view(prepared, &self.context.type_facts).then(|| { - analysis::PreparedSemanticView::build(analysis::PreparedSemanticViewInputs { - prepared: prepared.expect("prepared semantic view requires prepared artifact"), - interproc_summary_set, - abi_arg_regs: &self.config.arg_regs, - ret_reg_name: &fold_arch.ret_reg_name, - function_names: &self.context.function_names, - symbols: &self.context.symbols, - callee_facts: &self.context.type_facts.callee_facts, - stack_slots: &self.context.type_facts.stack_slots, - visible_bindings: &self.context.type_facts.visible_bindings, - param_register_aliases: ¶m_register_aliases, - }) - }); + let prepared_semantic_view = should_use_prepared_semantic_view( + prepared, + self.context.semantic_artifact(), + ) + .then(|| { + analysis::PreparedSemanticView::build(analysis::PreparedSemanticViewInputs { + prepared: prepared.expect("prepared semantic view requires prepared artifact"), + interproc_summary_set, + abi_arg_regs: &self.config.arg_regs, + ret_reg_name: &fold_arch.ret_reg_name, + function_names: &self.context.function_names, + symbols: &self.context.symbols, + callee_facts: &self.context.type_facts().callee_facts, + stack_slots: &self.context.type_facts().stack_slots, + visible_bindings: &self.context.type_facts().visible_bindings, + param_register_aliases: ¶m_register_aliases, + }) + }); let fold_inputs = FoldInputs { arch: &fold_arch, function_names: &self.context.function_names, strings: &self.context.strings, symbols: &self.context.symbols, known_function_signatures: &known_function_signatures, - callee_facts: &self.context.type_facts.callee_facts, - stack_slots: &self.context.type_facts.stack_slots, + callee_facts: &self.context.type_facts().callee_facts, + stack_slots: &self.context.type_facts().stack_slots, #[cfg(test)] - external_stack_vars: &self.context.type_facts.external_stack_vars, - visible_bindings: &self.context.type_facts.visible_bindings, - external_type_db: &self.context.type_facts.external_type_db, - symbolic_facts: &self.context.type_facts.symbolic_facts, + external_stack_vars: &self.context.type_facts().external_stack_vars, + visible_bindings: &self.context.type_facts().visible_bindings, + external_type_db: &self.context.type_facts().external_type_db, + semantic_artifact: self.context.semantic_artifact(), param_register_aliases: ¶m_register_aliases, type_hints: &type_hints, type_oracle, @@ -1748,14 +1505,9 @@ impl Decompiler { .name .clone() .unwrap_or_else(|| format!("sub_{:x}", func.entry)); - let preferred_structuring_reason = preferred_semantic_structuring_reason( + let semantic_route = planner::semantic_route_plan( &func_name, - &self.context.type_facts.symbolic_facts, - &func.cfg_risk_summary(), - ); - let preferred_linearization_reason = preferred_semantic_linearization_reason( - &func_name, - &self.context.type_facts.symbolic_facts, + self.context.semantic_artifact(), &func.cfg_risk_summary(), ); @@ -1764,123 +1516,34 @@ impl Decompiler { // Get set of variables that survive folding before structuring. let emitted_vars = structurer.emitted_var_names(); - let mut use_conservative_locals = false; - let mut is_linear_fallback = false; - let mut body_stmt = if let Some(ref reason) = preferred_structuring_reason { - use_conservative_locals = true; - match structurer.structure_semantic_worker_islands(6) { - Some(structured) => CStmt::Block(vec![ - CStmt::comment(format!("r2dec semantic worker structuring for {}", reason)), - structured, - ]), - None => { - let mut linear_stmts = self.linearize_function_body(func, &fold_ctx); - is_linear_fallback = true; - if linear_stmts.is_empty() { - CStmt::Block(vec![CStmt::comment(format!( - "r2dec fallback: semantic worker linearization for {} -> no statements recovered", - reason - ))]) - } else { - linear_stmts.insert( - 0, - CStmt::comment(format!( - "r2dec fallback: semantic worker linearization for {}", - reason - )), - ); - CStmt::Block(linear_stmts) - } - } - } - } else if let Some(ref reason) = preferred_linearization_reason { - let mut linear_stmts = self.linearize_function_body(func, &fold_ctx); - use_conservative_locals = true; - is_linear_fallback = true; - if linear_stmts.is_empty() { - CStmt::Block(vec![CStmt::comment(format!( - "r2dec fallback: semantic worker linearization for {} -> no statements recovered", - reason - ))]) - } else { - linear_stmts.insert( - 0, - CStmt::comment(format!( - "r2dec fallback: semantic worker linearization for {}", - reason - )), - ); - CStmt::Block(linear_stmts) - } - } else { - structurer.structure() - }; + let routed_body = consumer_structured::primary_body_for_semantic_route( + &semantic_route, + &mut structurer, + || self.linearize_function_body(func, &fold_ctx), + ); + let mut use_conservative_locals = routed_body.use_conservative_locals; + let mut is_linear_fallback = routed_body.is_linear_fallback; + let mut body_stmt = routed_body.body_stmt; - if preferred_structuring_reason.is_none() - && preferred_linearization_reason.is_none() + if matches!(semantic_route, planner::SemanticRoutePlan::Standard) && !Self::stmt_has_content(&body_stmt) { let folded_reason = structurer .safety_reason() .map(str::to_string) .unwrap_or_else(|| "folded structuring produced empty output".to_string()); - - // Fallback 1: unfolded structuring - let mut unfolded = ControlFlowStructurer::new_unfolded(func, &fold_ctx); - let unfolded_stmt = unfolded.structure(); - - if Self::stmt_has_content(&unfolded_stmt) { - use_conservative_locals = true; - body_stmt = Self::prepend_comment( - unfolded_stmt, - format!("r2dec fallback: {}", folded_reason), - ); - } else { - let unfolded_reason = unfolded - .safety_reason() - .map(str::to_string) - .unwrap_or_else(|| "unfolded structuring produced empty output".to_string()); - - // Fallback 2: linear block emission - let mut linear_stmts = self.linearize_function_body(func, &fold_ctx); - let fallback_reason = format!("{}; {}", folded_reason, unfolded_reason); - - use_conservative_locals = true; - is_linear_fallback = true; - if prefer_symbolic_large_worker_decompile(&self.context.type_facts) { - let semantic_reason = - preferred_semantic_worker_reason(&func.cfg_risk_summary()); - if linear_stmts.is_empty() { - body_stmt = CStmt::Block(vec![CStmt::comment(format!( - "r2dec fallback: semantic worker linearization for {} -> no statements recovered", - semantic_reason - ))]); - } else { - linear_stmts.insert( - 0, - CStmt::comment(format!( - "r2dec fallback: semantic worker linearization for {}", - semantic_reason - )), - ); - body_stmt = CStmt::Block(linear_stmts); - } - } else if linear_stmts.is_empty() { - body_stmt = CStmt::Block(vec![CStmt::comment(format!( - "r2dec fallback: {} -> no statements recovered", - fallback_reason - ))]); - } else { - linear_stmts.insert( - 0, - CStmt::comment(format!( - "r2dec fallback: {} -> linear block emission", - fallback_reason - )), - ); - body_stmt = CStmt::Block(linear_stmts); - } - } + let empty_fallback = consumer_fallback::recover_empty_structuring( + func, + &fold_ctx, + folded_reason, + prefer_symbolic_large_worker_decompile(self.context.semantic_artifact()) + .then(|| preferred_semantic_worker_reason(&func.cfg_risk_summary())) + .as_deref(), + || self.linearize_function_body(func, &fold_ctx), + ); + use_conservative_locals = empty_fallback.use_conservative_locals; + is_linear_fallback = empty_fallback.is_linear_fallback; + body_stmt = empty_fallback.body_stmt; } body_stmt = fold_ctx.normalize_final_stmt_calls(body_stmt); @@ -1908,7 +1571,7 @@ impl Decompiler { }) .chain( self.context - .type_facts + .type_facts() .visible_bindings .iter() .filter_map(|binding| { @@ -1920,8 +1583,8 @@ impl Decompiler { .collect::>(); let body_visible_stack_offsets = collect_visible_stack_offsets( &body_visible_names, - &self.context.type_facts.visible_bindings, - &self.context.type_facts.stack_slots, + &self.context.type_facts().visible_bindings, + &self.context.type_facts().stack_slots, ¶m_name_set, ); @@ -1980,7 +1643,7 @@ impl Decompiler { name: func_name, ret_type: self .context - .type_facts + .type_facts() .merged_signature .as_ref() .and_then(|sig| sig.ret_type.as_ref().map(type_like_to_ctype)) @@ -1997,7 +1660,7 @@ impl Decompiler { for name in self.context.function_names.values() { known_function_names.insert(name.to_ascii_lowercase()); } - for name in self.context.type_facts.known_function_signatures.keys() { + for name in self.context.type_facts().known_function_signatures.keys() { known_function_names.insert(name.to_ascii_lowercase()); } post_rename::rewrite_function_identifiers(&mut c_function, &known_function_names); @@ -2859,17 +2522,123 @@ pub fn infer_local_struct_field_accesses( .collect() } +#[cfg(test)] +pub(crate) fn test_control_fact( + target: u64, + status: r2sym::SymbolicReachabilityStatus, + branch_truth: Option, + condition: Option<&str>, + compiled: Option, + evidence: r2sym::SemanticEvidence, +) -> r2sym::Judged { + r2sym::Judged::new( + r2sym::ControlFact { + target, + status, + branch_truth, + condition: condition.map(str::to_string), + compiled, + }, + evidence, + ) +} + +#[cfg(test)] +pub(crate) fn test_memory_fact( + term: r2sym::BackwardMemoryCondition, + evidence: r2sym::SemanticEvidence, +) -> r2sym::Judged { + r2sym::Judged::new(r2sym::MemoryFact { term }, evidence) +} + +#[cfg(test)] +pub(crate) fn test_semantic_region( + anchor: u64, + frontier: std::collections::BTreeSet, + control: Vec>, + memory: Vec>, +) -> r2sym::SemanticRegion { + let targets = control + .iter() + .map(|fact| { + r2sym::Judged::new( + r2sym::TargetFact { + target: fact.value.target, + status: fact.value.status, + branch_truth: fact.value.branch_truth, + }, + fact.evidence.clone(), + ) + }) + .collect(); + r2sym::SemanticRegion { + anchor, + frontier, + control, + memory, + pre: Vec::new(), + post: Vec::new(), + targets, + } +} + +#[cfg(test)] +pub(crate) fn test_native_semantic_artifact( + stage: r2sym::RefinementStage, + granularity: r2sym::ArtifactGranularity, + slice_class: r2sym::SliceClass, + skipped_large_cfg: bool, + residual_reasons: Vec, + regions: Vec, +) -> r2sym::SemanticArtifact { + let regions = regions + .into_iter() + .map(|region| (region.key(), region)) + .collect(); + r2sym::SemanticArtifact { + stage, + granularity, + execution: r2sym::ExecutionModel::Native, + body: r2sym::SemanticArtifactBody::Native(r2sym::NativeArtifactBody { + summary: r2sym::NativeFunctionSummary { + slice_class, + closure_functions: 0, + helper_functions: 0, + derived_summaries: 0, + derived_diagnostics: Default::default(), + }, + regions, + }), + diagnostics: r2sym::SemanticArtifactDiagnostics { + branches_evaluated: 0, + branches_pruned: 0, + branches_unknown: 0, + skipped_missing_arch: false, + skipped_large_cfg, + residual_reasons, + ambiguous_targets: Vec::new(), + cache_hit: false, + }, + } +} + +#[cfg(test)] +pub(crate) fn leaked_test_semantic_artifact( + artifact: r2sym::SemanticArtifact, +) -> &'static r2sym::SemanticArtifact { + Box::leak(Box::new(artifact)) +} + #[cfg(test)] mod tests { use super::*; use r2il::{ArchSpec, R2ILBlock, R2ILOp, RegisterDef, SpaceId, Varnode}; use r2ssa::SSAFunction; use r2types::{ - ExternalField, ExternalStruct, FunctionParamSpec, FunctionSignatureSpec, FunctionTypeFacts, - SymbolicInterpreterKind, SymbolicSemanticFacts, SymbolicVmStateUpdate, - SymbolicVmStepSummary, + ExternalField, ExternalStruct, FunctionFacts, FunctionParamSpec, FunctionSignatureSpec, + FunctionTypeFacts, }; - use std::collections::BTreeMap; + use std::collections::{BTreeMap, BTreeSet}; fn ssa_from_ops(ops: Vec, arch: &ArchSpec) -> SSAFunction { let mut block = R2ILBlock::new(0x1000, 4); @@ -2917,6 +2686,66 @@ mod tests { } } + fn arg_memory_term( + offset: i64, + size: u32, + evidence: r2sym::SemanticEvidence, + value_expr: Option<&str>, + exact_value: bool, + ) -> r2sym::BackwardMemoryCondition { + r2sym::BackwardMemoryCondition { + region: r2sym::BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: offset, + offset_hi: offset, + size, + exact_offset: true, + evidence, + binding: None, + expr: if offset == 0 { + "*arg0".to_string() + } else { + format!("*(arg0 + {offset})") + }, + value_expr: value_expr.map(str::to_string), + exact_value, + } + } + + fn compiled_summary( + simplified: &str, + precision: r2sym::BackwardConditionPrecision, + supported_paths: usize, + total_paths: usize, + memory_terms: Vec, + ) -> r2sym::BackwardConditionSummary { + r2sym::BackwardConditionSummary { + simplified: simplified.to_string(), + terms: vec![simplified.to_string()], + memory_terms, + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision, + supported_paths, + total_paths, + } + } + + fn large_cfg_worker_artifact( + stage: r2sym::RefinementStage, + residual_reasons: Vec, + regions: Vec, + ) -> r2sym::SemanticArtifact { + test_native_semantic_artifact( + stage, + r2sym::ArtifactGranularity::Regioned, + r2sym::SliceClass::Worker, + true, + residual_reasons, + regions, + ) + } + #[test] fn test_decompiler_config_default() { let config = DecompilerConfig::default(); @@ -3308,7 +3137,7 @@ mod tests { let context = DecompilerContext::default().with_type_facts(type_facts); let mut legacy = Decompiler::new(DecompilerConfig::x86_64()); - legacy.set_type_facts(context.type_facts.clone()); + legacy.set_type_facts(context.type_facts().clone()); let input = DecompilerInput::new(prepared, context); let typed = Decompiler::new(DecompilerConfig::x86_64()); @@ -4483,7 +4312,7 @@ mod tests { config.arg_regs.clone(), config.ret_regs.clone(), ); - var_recovery.set_type_facts(decompiler.context.type_facts.clone()); + var_recovery.set_type_facts(decompiler.context.type_facts().clone()); var_recovery.recover(func); let mut type_inference = TypeInference::new_with_abi( @@ -4492,9 +4321,11 @@ mod tests { config.ret_regs.clone(), ); type_inference - .set_external_signature(decompiler.context.type_facts.merged_signature.clone()); - type_inference.set_external_stack_slots(decompiler.context.type_facts.stack_slots.clone()); - type_inference.set_external_type_db(decompiler.context.type_facts.external_type_db.clone()); + .set_external_signature(decompiler.context.type_facts().merged_signature.clone()); + type_inference + .set_external_stack_slots(decompiler.context.type_facts().stack_slots.clone()); + type_inference + .set_external_type_db(decompiler.context.type_facts().external_type_db.clone()); type_inference.set_decompile_prep_facts(func.decompile_prep_facts()); type_inference.infer_function(func); let mut type_hints = type_inference @@ -4520,12 +4351,12 @@ mod tests { .iter() .map(|(_, param)| param.clone()) .collect(), - decompiler.context.type_facts.merged_signature.as_ref(), + decompiler.context.type_facts().merged_signature.as_ref(), ); let param_register_aliases = build_param_register_aliases( ¶ms, &recovered_param_infos, - &decompiler.context.type_facts.register_params, + &decompiler.context.type_facts().register_params, &config.arg_regs, ); for (idx, (_ssa_var, _)) in recovered_param_infos.iter().enumerate() { @@ -4565,7 +4396,7 @@ mod tests { let inferred_ret_type = decompiler.infer_return_type(func, &type_inference); let signature_ret_type = decompiler .context - .type_facts + .type_facts() .merged_signature .as_ref() .and_then(|sig| sig.ret_type.as_ref().map(type_like_to_ctype)); @@ -4575,13 +4406,13 @@ mod tests { strings: &decompiler.context.strings, symbols: &decompiler.context.symbols, known_function_signatures: &known_function_signatures, - callee_facts: &decompiler.context.type_facts.callee_facts, - stack_slots: &decompiler.context.type_facts.stack_slots, + callee_facts: &decompiler.context.type_facts().callee_facts, + stack_slots: &decompiler.context.type_facts().stack_slots, #[cfg(test)] - external_stack_vars: &decompiler.context.type_facts.external_stack_vars, - visible_bindings: &decompiler.context.type_facts.visible_bindings, - external_type_db: &decompiler.context.type_facts.external_type_db, - symbolic_facts: &decompiler.context.type_facts.symbolic_facts, + external_stack_vars: &decompiler.context.type_facts().external_stack_vars, + visible_bindings: &decompiler.context.type_facts().visible_bindings, + external_type_db: &decompiler.context.type_facts().external_type_db, + semantic_artifact: decompiler.context.semantic_artifact(), param_register_aliases: ¶m_register_aliases, type_hints: &type_hints, type_oracle: combined_type_oracle @@ -4957,209 +4788,178 @@ mod tests { ); let mut decompiler = Decompiler::new(DecompilerConfig::default()); - decompiler.set_type_facts(FunctionTypeFacts { - symbolic_facts: SymbolicSemanticFacts { - vm_step: Some(SymbolicVmStepSummary { - kind: SymbolicInterpreterKind::SwitchDispatch, - loop_header: 0x1000, - dispatch_header: 0x1000, - selector: Some("vm.sel".to_string()), - dispatch_targets: vec![0x1004, 0x1008], - default_target: Some(0x1010), - case_values_by_target: BTreeMap::from([ - (0x1004, vec![1, 2]), - (0x1008, vec![3]), - ]), - loop_latches: vec![0x1000], - state_inputs: vec!["state".to_string(), "pc".to_string()], - state_outputs: vec!["state".to_string(), "pc".to_string()], - step_blocks: vec![0x1000, 0x1004], - handler_regions: BTreeMap::from([(0x1004, vec![0x1004, 0x1008])]), - handler_state_inputs: BTreeMap::from([(0x1004, vec!["state".to_string()])]), - handler_state_outputs: BTreeMap::from([(0x1004, vec!["state".to_string()])]), - handler_state_updates: BTreeMap::from([( - 0x1004, - vec![SymbolicVmStateUpdate { - output: "state".to_string(), - expr: "state + 1".to_string(), - value: r2types::SymbolicVmValueExpr::Expr("state + 1".to_string()), - exact: false, - evidence: r2types::SymbolicSemanticEvidence::heuristic( - r2types::SymbolicSemanticEvidenceReason::ValueOpaque, - ), - confidence: r2types::SymbolicSemanticConfidence::Heuristic, - }], - )]), - handler_exit_guards: BTreeMap::from([( - 0x1004, - vec![r2types::SymbolicVmGuardedExit { - target: 0x1008, - guard: r2types::SymbolicVmGuardCondition { - expr: "(state == 0x1)".to_string(), - value: r2types::SymbolicVmValueExpr::Expr( - "state == 0x1".to_string(), - ), - expect_nonzero: true, - exact: false, - evidence: r2types::SymbolicSemanticEvidence::heuristic( - r2types::SymbolicSemanticEvidenceReason::GuardOpaque, - ), - confidence: r2types::SymbolicSemanticConfidence::Heuristic, - }, - }], - )]), - handler_memory_read_effects: BTreeMap::from([( - 0x1004, - vec![r2types::SymbolicMemoryCondition { - region: r2types::SymbolicMemoryRegion::Region( - r2types::SymbolicMemoryRegionRef { - id: 1, - kind: r2types::SymbolicMemoryRegionKind::Global, - name: "ram:0x2000".to_string(), - }, - ), - offset_lo: 0, - offset_hi: 0, - size: 1, - exact_offset: true, - evidence: r2types::SymbolicSemanticEvidence::likely( - r2types::SymbolicSemanticEvidenceReason::ValueOpaque, - ), - confidence: r2types::SymbolicSemanticConfidence::Likely, - binding: Some("mem:r1:0:1".to_string()), - expr: "vm.sel".to_string(), - value_expr: None, - exact_value: false, - }], - )]), - handler_memory_write_effects: BTreeMap::from([( - 0x1004, - vec![r2types::SymbolicMemoryCondition { - region: r2types::SymbolicMemoryRegion::Region( - r2types::SymbolicMemoryRegionRef { - id: 2, - kind: r2types::SymbolicMemoryRegionKind::Heap, - name: "heap_alloc@1".to_string(), - }, - ), - offset_lo: 4, - offset_hi: 4, - size: 1, - exact_offset: true, - evidence: r2types::SymbolicSemanticEvidence::likely( - r2types::SymbolicSemanticEvidenceReason::ValueOpaque, - ), - confidence: r2types::SymbolicSemanticConfidence::Likely, - binding: Some("mem:r2:4:1".to_string()), - expr: "state".to_string(), - value_expr: Some("state".to_string()), - exact_value: false, - }], - )]), - handler_memory_reads: BTreeMap::from([(0x1004, 1)]), - handler_memory_writes: BTreeMap::from([(0x1004, 1)]), - handler_calls: BTreeMap::from([(0x1004, 0)]), - handler_conditional_branches: BTreeMap::from([(0x1004, 0)]), - handler_exit_targets: BTreeMap::from([(0x1004, vec![0x1008])]), - redispatch_handlers: vec![0x1000], - returning_handlers: vec![], - truncated_handlers: vec![], - transfers: vec![r2types::SymbolicVmTransferArm { - handler_target: 0x1004, - case_values: vec![1, 2], - region_blocks: vec![0x1004, 0x1008], - exit_targets: vec![0x1008], - exit_guards: vec![r2types::SymbolicVmGuardedExit { - target: 0x1008, - guard: r2types::SymbolicVmGuardCondition { - expr: "(state == 0x1)".to_string(), - value: r2types::SymbolicVmValueExpr::Expr( - "state == 0x1".to_string(), - ), - expect_nonzero: true, - exact: false, - evidence: r2types::SymbolicSemanticEvidence::heuristic( - r2types::SymbolicSemanticEvidenceReason::GuardOpaque, - ), - confidence: r2types::SymbolicSemanticConfidence::Heuristic, - }, - }], - state_updates: vec![SymbolicVmStateUpdate { - output: "state".to_string(), - expr: "state + 1".to_string(), - value: r2types::SymbolicVmValueExpr::Expr("state + 1".to_string()), - exact: false, - evidence: r2types::SymbolicSemanticEvidence::heuristic( - r2types::SymbolicSemanticEvidenceReason::ValueOpaque, - ), - confidence: r2types::SymbolicSemanticConfidence::Heuristic, - }], - selector_update: Some(SymbolicVmStateUpdate { - output: "vm.sel".to_string(), - expr: "3".to_string(), - value: r2types::SymbolicVmValueExpr::Const(3), - exact: true, - evidence: r2types::SymbolicSemanticEvidence::exact(), - confidence: r2types::SymbolicSemanticConfidence::Exact, - }), - memory_reads: vec![r2types::SymbolicMemoryCondition { - region: r2types::SymbolicMemoryRegion::Region( - r2types::SymbolicMemoryRegionRef { - id: 1, - kind: r2types::SymbolicMemoryRegionKind::Global, - name: "ram:0x2000".to_string(), - }, - ), - offset_lo: 0, - offset_hi: 0, - size: 1, - exact_offset: true, - evidence: r2types::SymbolicSemanticEvidence::likely( - r2types::SymbolicSemanticEvidenceReason::ValueOpaque, - ), - confidence: r2types::SymbolicSemanticConfidence::Likely, - binding: Some("mem:r1:0:1".to_string()), - expr: "vm.sel".to_string(), - value_expr: None, - exact_value: false, - }], - memory_writes: vec![r2types::SymbolicMemoryCondition { - region: r2types::SymbolicMemoryRegion::Region( - r2types::SymbolicMemoryRegionRef { - id: 2, - kind: r2types::SymbolicMemoryRegionKind::Heap, - name: "heap_alloc@1".to_string(), - }, - ), - offset_lo: 4, - offset_hi: 4, - size: 1, - exact_offset: true, - evidence: r2types::SymbolicSemanticEvidence::likely( - r2types::SymbolicSemanticEvidenceReason::ValueOpaque, - ), - confidence: r2types::SymbolicSemanticConfidence::Likely, - binding: Some("mem:r2:4:1".to_string()), - expr: "state".to_string(), - value_expr: Some("state".to_string()), - exact_value: false, - }], - residual_guards: false, - residual_memory_effects: false, + let vm_step = r2sym::VmStepSummary { + kind: r2sym::InterpreterKind::SwitchDispatch, + loop_header: 0x1000, + dispatch_header: 0x1000, + selector: Some("vm.sel".to_string()), + dispatch_targets: vec![0x1004, 0x1008], + default_target: Some(0x1010), + case_values_by_target: BTreeMap::from([(0x1004, vec![1, 2]), (0x1008, vec![3])]), + loop_latches: vec![0x1000], + state_inputs: vec!["state".to_string(), "pc".to_string()], + state_outputs: vec!["state".to_string(), "pc".to_string()], + step_blocks: vec![0x1000, 0x1004], + handler_regions: BTreeMap::from([(0x1004, vec![0x1004, 0x1008])]), + handler_state_inputs: BTreeMap::from([(0x1004, vec!["state".to_string()])]), + handler_state_outputs: BTreeMap::from([(0x1004, vec!["state".to_string()])]), + handler_state_updates: BTreeMap::from([( + 0x1004, + vec![r2sym::VmStateUpdate { + output: "state".to_string(), + expr: "state + 1".to_string(), + value: r2sym::VmValueExpr::Expr("state + 1".to_string()), + exact: false, + }], + )]), + handler_exit_guards: BTreeMap::from([( + 0x1004, + vec![r2sym::VmGuardedExit { + target: 0x1008, + guard: r2sym::VmGuardCondition { + expr: "(state == 0x1)".to_string(), + value: r2sym::VmValueExpr::Expr("state == 0x1".to_string()), + expect_nonzero: true, + exact: false, + }, + }], + )]), + handler_memory_read_effects: BTreeMap::from([( + 0x1004, + vec![r2sym::VmMemoryCondition { + region: r2sym::VmMemoryRegionRef { + id: 1, + kind: r2sym::MemoryRegionKind::Global, + name: "ram:0x2000".to_string(), + }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + binding: Some("mem:r1:0:1".to_string()), + expr: "vm.sel".to_string(), + value_expr: None, + value: None, + exact_value: false, + }], + )]), + handler_memory_write_effects: BTreeMap::from([( + 0x1004, + vec![r2sym::VmMemoryCondition { + region: r2sym::VmMemoryRegionRef { + id: 2, + kind: r2sym::MemoryRegionKind::Heap, + name: "heap_alloc@1".to_string(), + }, + offset_lo: 4, + offset_hi: 4, + size: 1, + exact_offset: true, + binding: Some("mem:r2:4:1".to_string()), + expr: "state".to_string(), + value_expr: Some("state".to_string()), + value: Some(r2sym::VmValueExpr::Var("state".to_string())), + exact_value: false, + }], + )]), + handler_memory_reads: BTreeMap::from([(0x1004, 1)]), + handler_memory_writes: BTreeMap::from([(0x1004, 1)]), + handler_calls: BTreeMap::from([(0x1004, 0)]), + handler_conditional_branches: BTreeMap::from([(0x1004, 0)]), + handler_exit_targets: BTreeMap::from([(0x1004, vec![0x1008])]), + redispatch_handlers: vec![0x1000], + returning_handlers: vec![], + truncated_handlers: vec![], + transfers: vec![r2sym::VmTransferArm { + handler_target: 0x1004, + case_values: vec![1, 2], + region_blocks: vec![0x1004, 0x1008], + exit_targets: vec![0x1008], + exit_guards: vec![r2sym::VmGuardedExit { + target: 0x1008, + guard: r2sym::VmGuardCondition { + expr: "(state == 0x1)".to_string(), + value: r2sym::VmValueExpr::Expr("state == 0x1".to_string()), + expect_nonzero: true, exact: false, - evidence: r2types::SymbolicSemanticEvidence::likely( - r2types::SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: r2types::SymbolicSemanticConfidence::Likely, - redispatch: false, - may_return: false, - truncated: false, - }], + }, + }], + state_updates: vec![r2sym::VmStateUpdate { + output: "state".to_string(), + expr: "state + 1".to_string(), + value: r2sym::VmValueExpr::Expr("state + 1".to_string()), + exact: false, + }], + selector_update: Some(r2sym::VmStateUpdate { + output: "vm.sel".to_string(), + expr: "3".to_string(), + value: r2sym::VmValueExpr::Const(3), + exact: true, }), - ..SymbolicSemanticFacts::default() + memory_reads: vec![r2sym::VmMemoryCondition { + region: r2sym::VmMemoryRegionRef { + id: 1, + kind: r2sym::MemoryRegionKind::Global, + name: "ram:0x2000".to_string(), + }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + binding: Some("mem:r1:0:1".to_string()), + expr: "vm.sel".to_string(), + value_expr: None, + value: None, + exact_value: false, + }], + memory_writes: vec![r2sym::VmMemoryCondition { + region: r2sym::VmMemoryRegionRef { + id: 2, + kind: r2sym::MemoryRegionKind::Heap, + name: "heap_alloc@1".to_string(), + }, + offset_lo: 4, + offset_hi: 4, + size: 1, + exact_offset: true, + binding: Some("mem:r2:4:1".to_string()), + expr: "state".to_string(), + value_expr: Some("state".to_string()), + value: Some(r2sym::VmValueExpr::Var("state".to_string())), + exact_value: false, + }], + residual_guards: false, + residual_memory_effects: false, + exact: false, + redispatch: false, + may_return: false, + truncated: false, + }], + }; + let semantic_artifact = r2sym::SemanticArtifact { + stage: r2sym::RefinementStage::Residual, + granularity: r2sym::ArtifactGranularity::SummaryOnly, + execution: r2sym::ExecutionModel::Vm, + body: r2sym::SemanticArtifactBody::Vm(Box::new(r2sym::VmArtifactBody { + interpreter: None, + step_summary: Some(vm_step), + transfer_summary: None, + })), + diagnostics: r2sym::SemanticArtifactDiagnostics { + branches_evaluated: 0, + branches_pruned: 0, + branches_unknown: 0, + skipped_missing_arch: false, + skipped_large_cfg: false, + residual_reasons: Vec::new(), + ambiguous_targets: Vec::new(), + cache_hit: false, }, - ..FunctionTypeFacts::default() - }); + }; + decompiler.set_function_facts(FunctionFacts::new( + FunctionTypeFacts::default(), + Some(semantic_artifact), + )); let output = decompiler.decompile(&func); assert!( @@ -5179,85 +4979,93 @@ mod tests { #[test] fn preferred_semantic_fallback_comment_allows_ready_large_worker_decompile() { - let symbolic_facts = SymbolicSemanticFacts { - control_islands: vec![r2types::SymbolicControlIsland { - kind: r2types::SymbolicControlIslandKind::LargeCfgBranchFrontier, - anchor_block: 0x401000, - frontier_targets: vec![0x401010, 0x401020], - facts: vec![r2types::SymbolicControlFact { - target: 0x401010, - status: r2types::SymbolicReachabilityStatus::Reachable, - condition: Some("x == 0".to_string()), - compiled: Some(r2types::SymbolicCompiledCondition { - simplified: "x == 0".to_string(), - terms: vec!["x == 0".to_string()], - memory_terms: Vec::new(), - backward_memory_substitutions: 0, - backward_memory_candidate_enumerations: 0, - backward_memory_residual_fallbacks: 0, - precision: r2types::SymbolicConditionPrecision::Exact, - evidence: r2types::SymbolicSemanticEvidence::exact(), - confidence: r2types::SymbolicSemanticConfidence::Exact, - supported_paths: 1, - total_paths: 1, - }), - evidence: r2types::SymbolicSemanticEvidence::exact(), - confidence: r2types::SymbolicSemanticConfidence::Exact, - }], - evidence: r2types::SymbolicSemanticEvidence::exact(), - confidence: r2types::SymbolicSemanticConfidence::Exact, - }], - diagnostics: r2types::SymbolicFactDiagnostics { - skipped_large_cfg: true, - semantic_mode: Some(r2types::SymbolicSemanticMode::Residual), - semantic_capability: Some(r2types::SymbolicSemanticCapability { - query_ready: true, - type_ready: true, - decompile_ready: true, - }), - slice_class: Some(r2types::SymbolicSemanticSliceClass::Worker), - residual_reasons: vec![r2types::SymbolicSemanticResidualReason::LargeCfg], - ..Default::default() - }, - ..Default::default() - }; + let region = test_semantic_region( + 0x401000, + BTreeSet::from([0x401010, 0x401020]), + vec![test_control_fact( + 0x401010, + r2sym::SymbolicReachabilityStatus::Reachable, + None, + Some("x == 0"), + Some(compiled_summary( + "x == 0", + r2sym::BackwardConditionPrecision::Exact, + 1, + 1, + Vec::new(), + )), + r2sym::SemanticEvidence::exact(), + )], + Vec::new(), + ); + let semantic_artifact = large_cfg_worker_artifact( + r2sym::RefinementStage::Residual, + vec![r2sym::ResidualReason::LargeCfg], + vec![region], + ); assert!( - preferred_semantic_fallback_comment("fcn.401000", &symbolic_facts).is_none(), + preferred_semantic_fallback_comment("fcn.401000", Some(&semantic_artifact)).is_none(), "expected semantically ready autogenerated large worker to keep decompilation path" ); assert!( - preferred_semantic_fallback_comment("named_worker", &symbolic_facts).is_none(), + preferred_semantic_fallback_comment("named_worker", Some(&semantic_artifact)).is_none(), "expected named function to keep full decompilation path" ); } #[test] fn preferred_semantic_linearization_reason_allows_ready_large_worker_linear_path() { - let symbolic_facts = SymbolicSemanticFacts { - worker_islands: vec![r2types::SymbolicWorkerIsland { - anchor_block: 0x401000, - control_kind: Some(r2types::SymbolicControlIslandKind::LargeCfgBranchFrontier), - memory_kind: Some(r2types::SymbolicMemoryIslandKind::LargeCfgConditionFrontier), - frontier_targets: vec![0x401010, 0x401020], - control_facts: Vec::new(), - memory_terms: Vec::new(), - evidence: r2types::SymbolicSemanticEvidence::exact(), - confidence: r2types::SymbolicSemanticConfidence::Exact, - }], - diagnostics: r2types::SymbolicFactDiagnostics { - skipped_large_cfg: true, - semantic_mode: Some(r2types::SymbolicSemanticMode::Residual), - semantic_capability: Some(r2types::SymbolicSemanticCapability { - query_ready: true, - type_ready: true, - decompile_ready: true, - }), - slice_class: Some(r2types::SymbolicSemanticSliceClass::Worker), - residual_reasons: vec![r2types::SymbolicSemanticResidualReason::LargeCfg], - ..Default::default() - }, - ..Default::default() + let region = test_semantic_region( + 0x401000, + BTreeSet::from([0x401010, 0x401020]), + Vec::new(), + Vec::new(), + ); + let summary = r2ssa::CFGRiskSummary { + block_count: 107, + loop_count: 16, + back_edge_count: 16, + switch_block_count: 0, + max_switch_cases: 0, }; + let semantic_artifact = large_cfg_worker_artifact( + r2sym::RefinementStage::Residual, + vec![r2sym::ResidualReason::LargeCfg], + vec![region], + ); + let reason = preferred_semantic_linearization_reason( + "fcn.401000", + Some(&semantic_artifact), + &summary, + ) + .expect("ready large worker should prefer linearized decompile path"); + assert!(reason.contains("complex loop graph")); + } + + #[test] + fn preferred_semantic_structuring_reason_allows_strict_large_worker_structuring_path() { + let likely = + r2sym::SemanticEvidence::likely(r2sym::SemanticEvidenceReason::PartialPathCoverage); + let memory_term = arg_memory_term(0, 1, likely.clone(), Some("0x0:8"), true); + let region = test_semantic_region( + 0x401000, + BTreeSet::from([0x401010, 0x401020]), + vec![test_control_fact( + 0x401010, + r2sym::SymbolicReachabilityStatus::Reachable, + None, + Some("x == 0"), + Some(compiled_summary( + "x == 0", + r2sym::BackwardConditionPrecision::OverApprox, + 1, + 2, + vec![memory_term.clone()], + )), + likely.clone(), + )], + vec![test_memory_fact(memory_term, likely.clone())], + ); let summary = r2ssa::CFGRiskSummary { block_count: 107, loop_count: 16, @@ -5265,92 +5073,86 @@ mod tests { switch_block_count: 0, max_switch_cases: 0, }; + let semantic_artifact = large_cfg_worker_artifact( + r2sym::RefinementStage::Compiled, + vec![r2sym::ResidualReason::LargeCfg], + vec![region], + ); let reason = - preferred_semantic_linearization_reason("fcn.401000", &symbolic_facts, &summary) - .expect("ready large worker should prefer linearized decompile path"); + preferred_semantic_structuring_reason("fcn.401000", Some(&semantic_artifact), &summary) + .expect("strict ready large worker should prefer structured semantic path"); assert!(reason.contains("complex loop graph")); + assert!( + preferred_semantic_linearization_reason( + "fcn.401000", + Some(&semantic_artifact), + &summary, + ) + .is_none(), + "structured-ready worker should not fall back to linearization" + ); } #[test] - fn preferred_semantic_structuring_reason_allows_strict_large_worker_structuring_path() { - let symbolic_facts = SymbolicSemanticFacts { - worker_islands: vec![r2types::SymbolicWorkerIsland { - anchor_block: 0x401000, - control_kind: Some(r2types::SymbolicControlIslandKind::LargeCfgBranchFrontier), - memory_kind: Some(r2types::SymbolicMemoryIslandKind::LargeCfgConditionFrontier), - frontier_targets: vec![0x401010, 0x401020], - control_facts: vec![r2types::SymbolicControlFact { - target: 0x401010, - status: r2types::SymbolicReachabilityStatus::Reachable, - condition: Some("x == 0".to_string()), - compiled: Some(r2types::SymbolicCompiledCondition { - simplified: "x == 0".to_string(), - terms: vec!["x == 0".to_string()], - memory_terms: vec![r2types::SymbolicMemoryCondition { - region: r2types::SymbolicMemoryRegion::Argument { index: 0 }, - offset_lo: 0, - offset_hi: 0, - size: 1, - exact_offset: true, - evidence: r2types::SymbolicSemanticEvidence::likely( - r2types::SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: r2types::SymbolicSemanticConfidence::Likely, - binding: None, - expr: "*arg0".to_string(), - value_expr: Some("0x0:8".to_string()), - exact_value: true, - }], - backward_memory_substitutions: 0, - backward_memory_candidate_enumerations: 0, - backward_memory_residual_fallbacks: 0, - precision: r2types::SymbolicConditionPrecision::OverApprox, - evidence: r2types::SymbolicSemanticEvidence::likely( - r2types::SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: r2types::SymbolicSemanticConfidence::Likely, - supported_paths: 1, - total_paths: 2, - }), - evidence: r2types::SymbolicSemanticEvidence::likely( - r2types::SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: r2types::SymbolicSemanticConfidence::Likely, - }], - memory_terms: vec![r2types::SymbolicMemoryCondition { - region: r2types::SymbolicMemoryRegion::Argument { index: 0 }, - offset_lo: 0, - offset_hi: 0, - size: 1, - exact_offset: true, - evidence: r2types::SymbolicSemanticEvidence::likely( - r2types::SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: r2types::SymbolicSemanticConfidence::Likely, - binding: None, - expr: "*arg0".to_string(), - value_expr: Some("0x0:8".to_string()), - exact_value: true, - }], - evidence: r2types::SymbolicSemanticEvidence::likely( - r2types::SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: r2types::SymbolicSemanticConfidence::Likely, - }], - diagnostics: r2types::SymbolicFactDiagnostics { - skipped_large_cfg: true, - semantic_mode: Some(r2types::SymbolicSemanticMode::IslandCompiled), - semantic_capability: Some(r2types::SymbolicSemanticCapability { - query_ready: true, - type_ready: true, - decompile_ready: true, - }), - slice_class: Some(r2types::SymbolicSemanticSliceClass::Worker), - residual_reasons: vec![r2types::SymbolicSemanticResidualReason::LargeCfg], - ..Default::default() - }, - ..Default::default() + fn preferred_semantic_structuring_reason_rejects_body_wide_memory_without_target_local_support() + { + let likely = + r2sym::SemanticEvidence::likely(r2sym::SemanticEvidenceReason::PartialPathCoverage); + let region = test_semantic_region( + 0x401000, + BTreeSet::from([0x401010, 0x401020]), + vec![test_control_fact( + 0x401010, + r2sym::SymbolicReachabilityStatus::Reachable, + None, + Some("x == 0"), + Some(compiled_summary( + "x == 0", + r2sym::BackwardConditionPrecision::OverApprox, + 1, + 2, + Vec::new(), + )), + likely.clone(), + )], + vec![test_memory_fact( + arg_memory_term(0, 1, likely.clone(), Some("0x0:8"), true), + likely.clone(), + )], + ); + let summary = r2ssa::CFGRiskSummary { + block_count: 107, + loop_count: 16, + back_edge_count: 16, + switch_block_count: 0, + max_switch_cases: 0, }; + let semantic_artifact = large_cfg_worker_artifact( + r2sym::RefinementStage::Compiled, + vec![r2sym::ResidualReason::LargeCfg], + vec![region], + ); + assert!( + preferred_semantic_structuring_reason("fcn.401000", Some(&semantic_artifact), &summary) + .is_none(), + "body-wide memory support should not overstate semantic structuring readiness" + ); + assert!( + preferred_semantic_linearization_reason( + "fcn.401000", + Some(&semantic_artifact), + &summary, + ) + .is_some(), + "large workers without target-local structuring support should fall back to linearization" + ); + } + + #[test] + fn preferred_semantic_structuring_reason_rejects_ambiguous_target_sources() { + let exact = r2sym::SemanticEvidence::exact(); + let likely = + r2sym::SemanticEvidence::likely(r2sym::SemanticEvidenceReason::PartialPathCoverage); let summary = r2ssa::CFGRiskSummary { block_count: 107, loop_count: 16, @@ -5358,13 +5160,72 @@ mod tests { switch_block_count: 0, max_switch_cases: 0, }; - let reason = preferred_semantic_structuring_reason("fcn.401000", &symbolic_facts, &summary) - .expect("strict ready large worker should prefer structured semantic path"); - assert!(reason.contains("complex loop graph")); + let region_a = test_semantic_region( + 0x401000, + BTreeSet::from([0x401010, 0x401020]), + vec![test_control_fact( + 0x401010, + r2sym::SymbolicReachabilityStatus::Reachable, + Some(true), + Some("x == 0"), + Some(compiled_summary( + "x == 0", + r2sym::BackwardConditionPrecision::Exact, + 1, + 1, + vec![arg_memory_term(0, 1, exact.clone(), Some("0x2a"), true)], + )), + exact.clone(), + )], + vec![test_memory_fact( + arg_memory_term(0, 1, exact.clone(), Some("0x2a"), true), + exact.clone(), + )], + ); + let region_b = test_semantic_region( + 0x401004, + BTreeSet::from([0x401010, 0x401024]), + vec![test_control_fact( + 0x401010, + r2sym::SymbolicReachabilityStatus::Reachable, + Some(true), + Some("y == 1"), + Some(compiled_summary( + "y == 1", + r2sym::BackwardConditionPrecision::OverApprox, + 1, + 2, + vec![arg_memory_term(0, 1, likely.clone(), Some("0x2b"), true)], + )), + likely.clone(), + )], + vec![test_memory_fact( + arg_memory_term(0, 1, likely.clone(), Some("0x2b"), true), + likely.clone(), + )], + ); + let semantic_artifact = large_cfg_worker_artifact( + r2sym::RefinementStage::Compiled, + vec![r2sym::ResidualReason::LargeCfg], + vec![region_a, region_b], + ); assert!( - preferred_semantic_linearization_reason("fcn.401000", &symbolic_facts, &summary) + semantic_artifact.target_has_ambiguous_sources(0x401010), + "same-target conflicting worker regions must surface explicit ambiguity" + ); + assert!( + preferred_semantic_structuring_reason("fcn.401000", Some(&semantic_artifact), &summary) .is_none(), - "structured-ready worker should not fall back to linearization" + "ambiguous target sources must block semantic structuring" + ); + assert!( + preferred_semantic_linearization_reason( + "fcn.401000", + Some(&semantic_artifact), + &summary, + ) + .is_some(), + "ambiguous target sources should downgrade to linearization instead of structuring" ); } @@ -5377,61 +5238,26 @@ mod tests { #[test] fn preferred_semantic_structuring_reason_uses_worker_label_when_cfg_is_benign() { - let symbolic_facts = SymbolicSemanticFacts { - worker_islands: vec![r2types::SymbolicWorkerIsland { - anchor_block: 0x401000, - control_kind: Some(r2types::SymbolicControlIslandKind::LargeCfgBranchFrontier), - memory_kind: Some(r2types::SymbolicMemoryIslandKind::LargeCfgConditionFrontier), - frontier_targets: vec![0x401010, 0x401020], - control_facts: vec![r2types::SymbolicControlFact { - target: 0x401010, - status: r2types::SymbolicReachabilityStatus::Reachable, - condition: Some("x == 0".to_string()), - compiled: Some(r2types::SymbolicCompiledCondition { - simplified: "x == 0".to_string(), - terms: vec!["x == 0".to_string()], - memory_terms: vec![r2types::SymbolicMemoryCondition { - region: r2types::SymbolicMemoryRegion::Argument { index: 0 }, - offset_lo: 0, - offset_hi: 0, - size: 1, - exact_offset: true, - evidence: r2types::SymbolicSemanticEvidence::exact(), - confidence: r2types::SymbolicSemanticConfidence::Exact, - binding: None, - expr: "*arg0".to_string(), - value_expr: Some("0x0:8".to_string()), - exact_value: true, - }], - backward_memory_substitutions: 0, - backward_memory_candidate_enumerations: 0, - backward_memory_residual_fallbacks: 0, - precision: r2types::SymbolicConditionPrecision::Exact, - evidence: r2types::SymbolicSemanticEvidence::exact(), - confidence: r2types::SymbolicSemanticConfidence::Exact, - supported_paths: 1, - total_paths: 1, - }), - evidence: r2types::SymbolicSemanticEvidence::exact(), - confidence: r2types::SymbolicSemanticConfidence::Exact, - }], - memory_terms: Vec::new(), - evidence: r2types::SymbolicSemanticEvidence::exact(), - confidence: r2types::SymbolicSemanticConfidence::Exact, - }], - diagnostics: r2types::SymbolicFactDiagnostics { - skipped_large_cfg: true, - semantic_mode: Some(r2types::SymbolicSemanticMode::IslandCompiled), - semantic_capability: Some(r2types::SymbolicSemanticCapability { - query_ready: true, - type_ready: true, - decompile_ready: true, - }), - slice_class: Some(r2types::SymbolicSemanticSliceClass::Worker), - ..Default::default() - }, - ..Default::default() - }; + let exact = r2sym::SemanticEvidence::exact(); + let region = test_semantic_region( + 0x401000, + BTreeSet::from([0x401010, 0x401020]), + vec![test_control_fact( + 0x401010, + r2sym::SymbolicReachabilityStatus::Reachable, + None, + Some("x == 0"), + Some(compiled_summary( + "x == 0", + r2sym::BackwardConditionPrecision::Exact, + 1, + 1, + vec![arg_memory_term(0, 1, exact.clone(), Some("0x0:8"), true)], + )), + exact.clone(), + )], + Vec::new(), + ); let summary = r2ssa::CFGRiskSummary { block_count: 40, loop_count: 1, @@ -5439,8 +5265,10 @@ mod tests { switch_block_count: 0, max_switch_cases: 0, }; + let semantic_artifact = + large_cfg_worker_artifact(r2sym::RefinementStage::Compiled, Vec::new(), vec![region]); assert_eq!( - preferred_semantic_structuring_reason("_140010138", &symbolic_facts, &summary) + preferred_semantic_structuring_reason("_140010138", Some(&semantic_artifact), &summary) .as_deref(), Some("semantic worker islands") ); @@ -5448,55 +5276,27 @@ mod tests { #[test] fn preferred_semantic_linearization_reason_uses_worker_label_when_cfg_is_benign() { - let symbolic_facts = SymbolicSemanticFacts { - worker_islands: vec![r2types::SymbolicWorkerIsland { - anchor_block: 0x401000, - control_kind: Some(r2types::SymbolicControlIslandKind::LargeCfgBranchFrontier), - memory_kind: Some(r2types::SymbolicMemoryIslandKind::LargeCfgConditionFrontier), - frontier_targets: vec![0x401010, 0x401020], - control_facts: vec![r2types::SymbolicControlFact { - target: 0x401010, - status: r2types::SymbolicReachabilityStatus::Reachable, - condition: Some("x == 0".to_string()), - compiled: Some(r2types::SymbolicCompiledCondition { - simplified: "x == 0".to_string(), - terms: vec!["x == 0".to_string()], - memory_terms: Vec::new(), - backward_memory_substitutions: 0, - backward_memory_candidate_enumerations: 0, - backward_memory_residual_fallbacks: 0, - precision: r2types::SymbolicConditionPrecision::OverApprox, - evidence: r2types::SymbolicSemanticEvidence::likely( - r2types::SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: r2types::SymbolicSemanticConfidence::Likely, - supported_paths: 1, - total_paths: 2, - }), - evidence: r2types::SymbolicSemanticEvidence::likely( - r2types::SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: r2types::SymbolicSemanticConfidence::Likely, - }], - memory_terms: Vec::new(), - evidence: r2types::SymbolicSemanticEvidence::likely( - r2types::SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: r2types::SymbolicSemanticConfidence::Likely, - }], - diagnostics: r2types::SymbolicFactDiagnostics { - skipped_large_cfg: true, - semantic_mode: Some(r2types::SymbolicSemanticMode::IslandCompiled), - semantic_capability: Some(r2types::SymbolicSemanticCapability { - query_ready: true, - type_ready: true, - decompile_ready: true, - }), - slice_class: Some(r2types::SymbolicSemanticSliceClass::Worker), - ..Default::default() - }, - ..Default::default() - }; + let likely = + r2sym::SemanticEvidence::likely(r2sym::SemanticEvidenceReason::PartialPathCoverage); + let region = test_semantic_region( + 0x401000, + BTreeSet::from([0x401010, 0x401020]), + vec![test_control_fact( + 0x401010, + r2sym::SymbolicReachabilityStatus::Reachable, + None, + Some("x == 0"), + Some(compiled_summary( + "x == 0", + r2sym::BackwardConditionPrecision::OverApprox, + 1, + 2, + Vec::new(), + )), + likely.clone(), + )], + Vec::new(), + ); let summary = r2ssa::CFGRiskSummary { block_count: 40, loop_count: 1, @@ -5504,106 +5304,51 @@ mod tests { switch_block_count: 0, max_switch_cases: 0, }; + let semantic_artifact = + large_cfg_worker_artifact(r2sym::RefinementStage::Compiled, Vec::new(), vec![region]); assert_eq!( - preferred_semantic_linearization_reason("_140010138", &symbolic_facts, &summary) - .as_deref(), + preferred_semantic_linearization_reason( + "_140010138", + Some(&semantic_artifact), + &summary, + ) + .as_deref(), Some("semantic worker islands") ); } #[test] fn semantic_fallback_comment_reports_actionable_control_islands() { - let symbolic_facts = SymbolicSemanticFacts { - branch_facts: vec![r2types::SymbolicBranchFact { - block_addr: 0x401000, - true_target: 0x401010, - false_target: 0x401020, - true_status: r2types::SymbolicReachabilityStatus::Reachable, - false_status: r2types::SymbolicReachabilityStatus::Unreachable, - true_condition: Some("x == 0".to_string()), - false_condition: Some("x != 0".to_string()), - true_compiled: Some(r2types::SymbolicCompiledCondition { - simplified: "x == 0".to_string(), - terms: vec!["x == 0".to_string()], - memory_terms: Vec::new(), - backward_memory_substitutions: 0, - backward_memory_candidate_enumerations: 0, - backward_memory_residual_fallbacks: 0, - precision: r2types::SymbolicConditionPrecision::Exact, - evidence: r2types::SymbolicSemanticEvidence::exact(), - confidence: r2types::SymbolicSemanticConfidence::Exact, - supported_paths: 1, - total_paths: 1, - }), - false_compiled: None, - }], - control_islands: vec![r2types::SymbolicControlIsland { - kind: r2types::SymbolicControlIslandKind::LargeCfgBranchFrontier, - anchor_block: 0x401000, - frontier_targets: vec![0x401010, 0x401020], - facts: vec![r2types::SymbolicControlFact { - target: 0x401010, - status: r2types::SymbolicReachabilityStatus::Reachable, - condition: Some("x == 0".to_string()), - compiled: Some(r2types::SymbolicCompiledCondition { - simplified: "x == 0".to_string(), - terms: vec!["x == 0".to_string()], - memory_terms: Vec::new(), - backward_memory_substitutions: 0, - backward_memory_candidate_enumerations: 0, - backward_memory_residual_fallbacks: 0, - precision: r2types::SymbolicConditionPrecision::Exact, - evidence: r2types::SymbolicSemanticEvidence::exact(), - confidence: r2types::SymbolicSemanticConfidence::Exact, - supported_paths: 1, - total_paths: 1, - }), - evidence: r2types::SymbolicSemanticEvidence::exact(), - confidence: r2types::SymbolicSemanticConfidence::Exact, - }], - evidence: r2types::SymbolicSemanticEvidence::exact(), - confidence: r2types::SymbolicSemanticConfidence::Exact, - }], - memory_islands: vec![r2types::SymbolicMemoryIsland { - kind: r2types::SymbolicMemoryIslandKind::LargeCfgConditionFrontier, - anchor_block: 0x401000, - terms: vec![r2types::SymbolicMemoryCondition { - region: r2types::SymbolicMemoryRegion::Argument { index: 0 }, - offset_lo: 8, - offset_hi: 8, - size: 4, - exact_offset: true, - evidence: r2types::SymbolicSemanticEvidence::exact(), - confidence: r2types::SymbolicSemanticConfidence::Exact, - binding: None, - expr: "*(arg0 + 8)".to_string(), - value_expr: None, - exact_value: false, - }], - evidence: r2types::SymbolicSemanticEvidence::exact(), - confidence: r2types::SymbolicSemanticConfidence::Exact, - }], - diagnostics: r2types::SymbolicFactDiagnostics { - branches_evaluated: 1, - branches_pruned: 1, - semantic_mode: Some(r2types::SymbolicSemanticMode::Residual), - semantic_capability: Some(r2types::SymbolicSemanticCapability { - query_ready: true, - type_ready: true, - decompile_ready: true, - }), - slice_class: Some(r2types::SymbolicSemanticSliceClass::Worker), - residual_reasons: vec![r2types::SymbolicSemanticResidualReason::LargeCfg], - ..Default::default() - }, - ..Default::default() - }; - let output = semantic_fallback_comment("_401000", &symbolic_facts) + let exact = r2sym::SemanticEvidence::exact(); + let memory_term = arg_memory_term(8, 4, exact.clone(), None, false); + let region = test_semantic_region( + 0x401000, + BTreeSet::from([0x401010, 0x401020]), + vec![test_control_fact( + 0x401010, + r2sym::SymbolicReachabilityStatus::Reachable, + Some(true), + Some("x == 0"), + Some(compiled_summary( + "x == 0", + r2sym::BackwardConditionPrecision::Exact, + 1, + 1, + vec![memory_term.clone()], + )), + exact.clone(), + )], + vec![test_memory_fact(memory_term, exact.clone())], + ); + let semantic_artifact = large_cfg_worker_artifact( + r2sym::RefinementStage::Residual, + vec![r2sym::ResidualReason::LargeCfg], + vec![region], + ); + let output = semantic_fallback_comment("_401000", Some(&semantic_artifact)) .expect("typed semantic fallback comment"); assert!(output.contains("semantic fallback: worker slice in residual mode")); - assert!(output.contains("branch_facts=1")); - assert!(output.contains("control_islands=1")); - assert!(output.contains("memory_islands=1")); + assert!(output.contains("regions=1")); assert!(output.contains("actionable_conditions=1")); assert!(output.contains("exact_conditions=1")); assert!(output.contains("actionable_preview=[0x401000: x == 0]")); diff --git a/crates/r2dec/src/planner.rs b/crates/r2dec/src/planner.rs new file mode 100644 index 0000000..697eda1 --- /dev/null +++ b/crates/r2dec/src/planner.rs @@ -0,0 +1,232 @@ +use r2il::R2ILBlock; +use r2ssa::{CFGRiskSummary, SSAFunction, SsaArtifact}; +use r2types::FunctionTypeFacts; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SemanticRoutePlan { + Standard, + StructuredWorker { reason: String }, + LinearWorker { reason: String }, + FallbackComment { comment: String }, +} + +pub(crate) fn prefer_symbolic_large_worker_decompile( + semantic_artifact: Option<&r2sym::SemanticArtifact>, +) -> bool { + let Some(semantic_artifact) = semantic_artifact else { + return false; + }; + matches!( + semantic_artifact.decompile_plan(), + r2sym::DecompilePlan::Ready + ) && semantic_artifact.diagnostics.skipped_large_cfg + && matches!( + semantic_artifact.stage, + r2sym::RefinementStage::Residual | r2sym::RefinementStage::Compiled + ) + && semantic_artifact + .native_body() + .is_some_and(|body| matches!(body.summary.slice_class, r2sym::SliceClass::Worker)) + && semantic_artifact + .native_body() + .is_some_and(|body| !body.regions.is_empty()) +} + +pub(crate) fn should_skip_runtime_type_inference( + prepared: Option<&SsaArtifact>, + _type_facts: &FunctionTypeFacts, + semantic_artifact: Option<&r2sym::SemanticArtifact>, +) -> bool { + if prefer_symbolic_large_worker_decompile(semantic_artifact) { + return true; + } + let Some(prepared) = prepared else { + return false; + }; + let summary = prepared.function().cfg_risk_summary(); + summary.block_count >= 96 + && summary.switch_block_count > 0 + && summary.max_switch_cases >= 32 + && summary.back_edge_count == 0 +} + +pub(crate) fn should_use_prepared_semantic_view( + prepared: Option<&SsaArtifact>, + semantic_artifact: Option<&r2sym::SemanticArtifact>, +) -> bool { + prepared.is_some() && !prefer_symbolic_large_worker_decompile(semantic_artifact) +} + +pub(crate) fn preferred_semantic_fallback_comment( + func_name: &str, + semantic_artifact: Option<&r2sym::SemanticArtifact>, +) -> Option { + let semantic_artifact = semantic_artifact?; + if matches!(semantic_artifact.execution, r2sym::ExecutionModel::Vm) { + return crate::consumer_fallback::semantic_fallback_comment( + func_name, + Some(semantic_artifact), + ); + } + if !crate::is_autogenerated_function_name(func_name) { + return None; + } + if matches!( + semantic_artifact.decompile_plan(), + r2sym::DecompilePlan::Ready + ) { + return None; + } + if semantic_artifact.diagnostics.skipped_large_cfg { + return crate::consumer_fallback::semantic_fallback_comment( + func_name, + Some(semantic_artifact), + ); + } + semantic_artifact + .diagnostics + .residual_reasons + .contains(&r2sym::ResidualReason::InterpreterRequiresStepSummary) + .then(|| { + crate::consumer_fallback::semantic_fallback_comment(func_name, Some(semantic_artifact)) + }) + .flatten() +} + +pub(crate) fn preferred_semantic_linearization_reason( + func_name: &str, + semantic_artifact: Option<&r2sym::SemanticArtifact>, + cfg_summary: &CFGRiskSummary, +) -> Option { + let semantic_artifact = semantic_artifact?; + if !crate::is_autogenerated_function_name(func_name) { + return None; + } + if !matches!( + semantic_artifact.decompile_plan(), + r2sym::DecompilePlan::Ready + ) || !semantic_artifact.diagnostics.skipped_large_cfg + { + return None; + } + if semantic_artifact.supports_guarded_structuring() { + return None; + } + if semantic_artifact + .native_body() + .is_none_or(|body| body.regions.is_empty()) + { + return None; + } + Some(preferred_semantic_worker_reason(cfg_summary)) +} + +pub(crate) fn preferred_semantic_structuring_reason( + func_name: &str, + semantic_artifact: Option<&r2sym::SemanticArtifact>, + cfg_summary: &CFGRiskSummary, +) -> Option { + let semantic_artifact = semantic_artifact?; + if !crate::is_autogenerated_function_name(func_name) { + return None; + } + if !matches!( + semantic_artifact.decompile_plan(), + r2sym::DecompilePlan::Ready + ) { + return None; + } + if !semantic_artifact.supports_guarded_structuring() + || !semantic_artifact.diagnostics.skipped_large_cfg + { + return None; + } + Some(preferred_semantic_worker_reason(cfg_summary)) +} + +pub(crate) fn preferred_semantic_worker_reason(cfg_summary: &CFGRiskSummary) -> String { + cfg_guard_reason_from_summary(cfg_summary) + .unwrap_or_else(|| "semantic worker islands".to_string()) +} + +pub fn cfg_guard_reason_from_summary(summary: &CFGRiskSummary) -> Option { + if summary.loop_count > 8 || summary.back_edge_count > 16 { + return Some(format!( + "complex loop graph (loops={}, back_edges={})", + summary.loop_count, summary.back_edge_count + )); + } + + if summary.loop_count > 4 && summary.block_count >= 96 && summary.max_switch_cases >= 32 { + return Some(format!( + "large dense switch in looped CFG (blocks={}, loops={}, max_switch_cases={})", + summary.block_count, summary.loop_count, summary.max_switch_cases + )); + } + + None +} + +pub fn cfg_guard_reason(blocks: &[R2ILBlock]) -> Option { + let ssa_func = SSAFunction::from_blocks_raw_no_arch(blocks)?; + cfg_guard_reason_from_summary(&ssa_func.cfg_risk_summary()) +} + +pub fn block_guard_fallback_comment(func_name: &str, blocks: usize, max_blocks: usize) -> String { + format!( + "/* r2dec fallback: skipped decompilation for {} ({} blocks > limit {}). Set SLEIGH_DEC_MAX_BLOCKS to override. */", + func_name, blocks, max_blocks + ) +} + +pub fn artifact_guard_fallback_comment(func_name: &str, reason: &str) -> String { + format!( + "/* r2dec fallback: skipped decompilation for {} ({}) */", + func_name, reason + ) +} + +pub fn semantic_route_plan( + func_name: &str, + semantic_artifact: Option<&r2sym::SemanticArtifact>, + cfg_summary: &CFGRiskSummary, +) -> SemanticRoutePlan { + if let Some(comment) = preferred_semantic_fallback_comment(func_name, semantic_artifact) { + return SemanticRoutePlan::FallbackComment { comment }; + } + if let Some(reason) = + preferred_semantic_structuring_reason(func_name, semantic_artifact, cfg_summary) + { + return SemanticRoutePlan::StructuredWorker { reason }; + } + if let Some(reason) = + preferred_semantic_linearization_reason(func_name, semantic_artifact, cfg_summary) + { + return SemanticRoutePlan::LinearWorker { reason }; + } + SemanticRoutePlan::Standard +} + +pub fn detached_semantic_route_plan( + func_name: &str, + blocks: &[R2ILBlock], + semantic_artifact: Option<&r2sym::SemanticArtifact>, +) -> Option { + let ssa_func = SSAFunction::from_blocks_raw_no_arch(blocks)?; + Some(semantic_route_plan( + func_name, + semantic_artifact, + &ssa_func.cfg_risk_summary(), + )) +} + +pub fn detached_semantic_linearization_reason( + func_name: &str, + blocks: &[R2ILBlock], + semantic_artifact: Option<&r2sym::SemanticArtifact>, +) -> Option { + match detached_semantic_route_plan(func_name, blocks, semantic_artifact)? { + SemanticRoutePlan::LinearWorker { reason } => Some(reason), + _ => None, + } +} diff --git a/crates/r2dec/src/structure.rs b/crates/r2dec/src/structure.rs index 4ad207c..8b9a4ae 100644 --- a/crates/r2dec/src/structure.rs +++ b/crates/r2dec/src/structure.rs @@ -263,13 +263,13 @@ impl<'a, 'o> ControlFlowStructurer<'a, 'o> { if let Some(vm_step) = self .fold_ctx .inputs - .symbolic_facts - .vm_step_for_dispatch_header(switch_addr) + .semantic_artifact + .and_then(|artifact| artifact.vm_step_for_dispatch_header(switch_addr)) .or_else(|| { self.fold_ctx .inputs - .symbolic_facts - .vm_transfer_for_dispatch_header(switch_addr) + .semantic_artifact + .and_then(|artifact| artifact.vm_transfer_for_dispatch_header(switch_addr)) }) && let Some(selector) = vm_step.selector.as_ref() { @@ -428,91 +428,137 @@ impl<'a, 'o> ControlFlowStructurer<'a, 'o> { fn symbolic_exact_reachable_target(&self, cond_block: u64) -> Option { self.fold_ctx .inputs - .symbolic_facts + .semantic_artifact? .exact_reachable_target_for_block(cond_block) } fn symbolic_actionable_reachable_target(&self, cond_block: u64) -> Option { self.fold_ctx .inputs - .symbolic_facts + .semantic_artifact? .actionable_reachable_target_for_block(cond_block) } - fn worker_island_supports_semantic_structuring(island: &r2types::SymbolicWorkerIsland) -> bool { - let supporting_condition = Self::worker_island_supporting_compiled_condition(island); - let has_unique_target = island.exact_reachable_target().is_some() - || island.actionable_reachable_target().is_some(); - let has_condition = supporting_condition.is_some(); - let has_memory_support = !island.actionable_terms().is_empty() - || supporting_condition.is_some_and(|compiled| !compiled.memory_terms.is_empty()); - island.evidence.allows_narrowing() - && has_unique_target - && has_condition - && has_memory_support + fn region_supports_semantic_structuring(&self, region: &r2sym::SemanticRegion) -> bool { + let reachable_target = region + .exact_reachable_target() + .or_else(|| region.actionable_reachable_target()); + if let Some(target) = reachable_target + && self + .fold_ctx + .inputs + .semantic_artifact + .is_some_and(|artifact| artifact.target_has_ambiguous_sources(target)) + { + return false; + } + region.supports_guarded_structuring() } - fn worker_island_supporting_compiled_condition( - island: &r2types::SymbolicWorkerIsland, - ) -> Option<&r2types::SymbolicCompiledCondition> { - let reachable_target = island + fn region_supporting_compiled_condition( + region: &r2sym::SemanticRegion, + ) -> Option<&r2sym::BackwardConditionSummary> { + let reachable_target = region .exact_reachable_target() - .or_else(|| island.actionable_reachable_target())?; - island - .control_facts - .iter() - .find(|fact| fact.target == reachable_target) - .and_then(r2types::SymbolicControlFact::actionable_compiled_condition) + .or_else(|| region.actionable_reachable_target())?; + region.actionable_compiled_condition_for_target(reachable_target) } fn branch_condition_for_reachable_target( &mut self, - cond_block: u64, + region: &r2sym::SemanticRegion, reachable_target: u64, ) -> CExpr { - let cond = self.get_branch_condition(cond_block); - if let Some(branch) = self - .fold_ctx - .inputs - .symbolic_facts - .branch_fact_for_block(cond_block) - && branch.false_target == reachable_target + let cond = self.get_branch_condition(region.anchor); + if Self::region_supporting_compiled_condition(region).is_some() { + return cond; + } + if self + .resolve_conditional_targets(region.anchor) + .is_some_and(|(_, false_target)| false_target == reachable_target) { return Self::negate_condition(cond); } + if region.branch_truth_for_target(reachable_target) == Some(false) { + return Self::negate_condition(cond); + } cond } - fn structure_semantic_worker_island( - &mut self, - island: &r2types::SymbolicWorkerIsland, - ) -> Option { - if !Self::worker_island_supports_semantic_structuring(island) { + fn structure_cfg_suffix_from_target(&mut self, target: u64) -> Option { + let mut stmts = Vec::new(); + let mut current = target; + let mut seen = HashSet::new(); + + loop { + if !seen.insert(current) { + break; + } + Self::append_stmt_body_flat(&mut stmts, self.structure_block(current)); + let cfg_block = self.func.cfg().get_block(current)?; + let next = match cfg_block.terminator { + BlockTerminator::Fallthrough { next } + | BlockTerminator::Branch { target: next } => Some(next), + _ => None, + }; + let Some(next) = next else { + break; + }; + if self.func.get_block(next).is_none() { + break; + } + current = next; + } + + Some(if stmts.len() == 1 { + stmts.into_iter().next().unwrap_or(CStmt::Empty) + } else { + CStmt::Block(stmts) + }) + } + + fn structure_function_suffix_from_target(&mut self, target: u64) -> Option { + if self.region_analyzer.is_none() { + self.region_analyzer = Some(RegionAnalyzer::new(self.func)); + } + let root_region = self.region_analyzer.as_mut()?.analyze(); + let structured = self.structure_region_suffix_from_target(&root_region, target); + match structured { + Some(CStmt::Empty) | None => self.structure_cfg_suffix_from_target(target), + other => other, + } + } + + fn structure_semantic_region(&mut self, region: &r2sym::SemanticRegion) -> Option { + if !self.region_supports_semantic_structuring(region) { return None; } - let reachable_target = island + let reachable_target = region .exact_reachable_target() - .or_else(|| island.actionable_reachable_target())?; - let cond = - self.branch_condition_for_reachable_target(island.anchor_block, reachable_target); + .or_else(|| region.actionable_reachable_target())?; + let cond = self.branch_condition_for_reachable_target(region, reachable_target); + let memory_terms = region.actionable_memory_terms_for_target(reachable_target); let mut then_stmts = Vec::new(); - if let Some(compiled) = Self::worker_island_supporting_compiled_condition(island) { + if let Some(compiled) = Self::region_supporting_compiled_condition(region) { then_stmts.push(CStmt::comment(format!( "semantic worker branch: {}", compiled.simplified ))); } - if self.func.get_block(reachable_target).is_some() { - Self::append_stmt_body_flat(&mut then_stmts, self.structure_block(reachable_target)); - } else { + for term in memory_terms.into_iter().take(2) { then_stmts.push(CStmt::comment(format!( - "semantic worker target: 0x{reachable_target:x}" + "semantic memory: {} [{:?}]", + term.expr, term.evidence.tier ))); } - for term in island.actionable_terms().into_iter().take(2) { + if self.func.get_block(reachable_target).is_some() { + let reachable_stmt = self + .structure_function_suffix_from_target(reachable_target) + .unwrap_or_else(|| self.structure_block(reachable_target)); + Self::append_stmt_body_flat(&mut then_stmts, reachable_stmt); + } else { then_stmts.push(CStmt::comment(format!( - "semantic memory: {} [{:?}]", - term.expr, term.evidence.tier + "semantic worker target: 0x{reachable_target:x}" ))); } let then_body = if then_stmts.len() == 1 { @@ -520,7 +566,7 @@ impl<'a, 'o> ControlFlowStructurer<'a, 'o> { } else { CStmt::Block(then_stmts) }; - let mut prefix = self.structure_block_prefix_stmts(island.anchor_block); + let mut prefix = self.structure_block_prefix_stmts(region.anchor); prefix.push(CStmt::if_stmt(cond, then_body, None)); Some(if prefix.len() == 1 { prefix.into_iter().next().unwrap_or(CStmt::Empty) @@ -530,29 +576,25 @@ impl<'a, 'o> ControlFlowStructurer<'a, 'o> { } pub fn structure_semantic_worker_islands(&mut self, limit: usize) -> Option { - let islands = self + let regions = self .fold_ctx .inputs - .symbolic_facts - .worker_islands - .iter() - .filter(|island| Self::worker_island_supports_semantic_structuring(island)) - .take(limit) - .cloned() - .collect::>(); + .semantic_artifact + .map(|artifact| artifact.actionable_regions()) + .unwrap_or_default(); let mut stmts = Vec::new(); let mut seen_targets = HashSet::new(); - for island in &islands { - let Some(target) = island + for region in regions.into_iter().take(limit) { + let Some(target) = region .exact_reachable_target() - .or_else(|| island.actionable_reachable_target()) + .or_else(|| region.actionable_reachable_target()) else { continue; }; - if !seen_targets.insert((island.anchor_block, target)) { + if !seen_targets.insert((region.anchor, target)) { continue; } - if let Some(stmt) = self.structure_semantic_worker_island(island) { + if let Some(stmt) = self.structure_semantic_region(region) { Self::append_stmt_body_flat(&mut stmts, stmt); } } @@ -2505,15 +2547,7 @@ mod tests { use crate::fold::FoldingContext; use r2il::{ArchSpec, R2ILBlock, R2ILOp, RegisterDef, Varnode}; use r2ssa::SSAFunction; - use r2types::{ - SymbolicBranchFact, SymbolicCompiledCondition, SymbolicConditionPrecision, - SymbolicControlFact, SymbolicControlIsland, SymbolicControlIslandKind, - SymbolicInterpreterKind, SymbolicMemoryCondition, SymbolicMemoryIslandKind, - SymbolicMemoryRegion, SymbolicReachabilityStatus, SymbolicSemanticConfidence, - SymbolicSemanticEvidence, SymbolicSemanticEvidenceReason, SymbolicSemanticFacts, - SymbolicVmStateUpdate, SymbolicVmStepSummary, SymbolicWorkerIsland, - }; - use std::collections::BTreeMap; + use std::collections::{BTreeMap, BTreeSet}; fn v(name: &str) -> CExpr { CExpr::Var(name.to_string()) @@ -2593,6 +2627,62 @@ mod tests { .with_name("worker_structured_demo") } + fn function_with_multi_block_true_arm( + cond_block: u64, + true_entry: u64, + true_tail: u64, + false_target: u64, + ) -> SSAFunction { + let mut cond = R2ILBlock::new(cond_block, 4); + cond.push(R2ILOp::IntEqual { + dst: Varnode::register(0x80, 1), + a: Varnode::register(0x10, 8), + b: Varnode::constant(0, 8), + }); + cond.push(R2ILOp::CBranch { + target: Varnode::constant(true_entry, 8), + cond: Varnode::register(0x80, 1), + }); + + let mut false_block = R2ILBlock::new(false_target, 4); + false_block.push(R2ILOp::Return { + target: Varnode::constant(0, 8), + }); + + let mut true_head = R2ILBlock::new(true_entry, 4); + true_head.push(R2ILOp::Copy { + dst: Varnode::register(0x00, 8), + src: Varnode::constant(1, 8), + }); + true_head.push(R2ILOp::Branch { + target: Varnode::constant(true_tail, 8), + }); + + let mut true_tail_block = R2ILBlock::new(true_tail, 4); + true_tail_block.push(R2ILOp::Return { + target: Varnode::register(0x00, 8), + }); + + SSAFunction::from_blocks_with_arch( + &[cond, false_block, true_head, true_tail_block], + Some(&test_arch()), + ) + .expect("ssa function") + .with_name("worker_multiblock_demo") + } + + fn trailing_if_stmt(stmt: &CStmt) -> &CStmt { + match stmt { + CStmt::If { .. } => stmt, + CStmt::Block(stmts) => stmts + .iter() + .rev() + .find(|stmt| matches!(stmt, CStmt::If { .. })) + .expect("trailing if statement"), + _ => panic!("expected structured if statement, got {stmt:?}"), + } + } + #[test] fn rewrites_canonical_while_to_for() { let input = CStmt::Block(vec![ @@ -3182,80 +3272,84 @@ mod tests { fn uses_vm_transfer_selector_when_vm_step_is_absent() { let func = function_with_single_block(0x1000); let mut ctx = FoldingContext::new(64); - let facts = SymbolicSemanticFacts { - vm_transfer: Some(SymbolicVmStepSummary { - kind: SymbolicInterpreterKind::SwitchDispatch, - loop_header: 0x1000, - dispatch_header: 0x1000, - selector: Some("vm.sel".to_string()), - dispatch_targets: vec![0x1004], - default_target: None, - case_values_by_target: BTreeMap::from([(0x1004, vec![1, 2])]), - loop_latches: vec![0x1000], - state_inputs: vec!["state".to_string()], - state_outputs: vec!["state".to_string()], - step_blocks: vec![0x1000], - handler_regions: BTreeMap::from([(0x1004, vec![0x1004, 0x1008])]), - handler_state_inputs: BTreeMap::from([(0x1004, vec!["state".to_string()])]), - handler_state_outputs: BTreeMap::from([(0x1004, vec!["state".to_string()])]), - handler_state_updates: BTreeMap::from([( - 0x1004, - vec![SymbolicVmStateUpdate { - output: "state".to_string(), - expr: "state + 1".to_string(), - value: r2types::SymbolicVmValueExpr::Expr("state + 1".to_string()), - exact: false, - evidence: r2types::SymbolicSemanticEvidence::heuristic( - r2types::SymbolicSemanticEvidenceReason::ValueOpaque, - ), - confidence: r2types::SymbolicSemanticConfidence::Heuristic, - }], - )]), - handler_exit_guards: BTreeMap::new(), - handler_memory_read_effects: BTreeMap::new(), - handler_memory_write_effects: BTreeMap::new(), - handler_memory_reads: BTreeMap::from([(0x1004, 1)]), - handler_memory_writes: BTreeMap::from([(0x1004, 1)]), - handler_calls: BTreeMap::from([(0x1004, 0)]), - handler_conditional_branches: BTreeMap::from([(0x1004, 0)]), - handler_exit_targets: BTreeMap::from([(0x1004, vec![0x1008])]), - redispatch_handlers: vec![0x1000], - returning_handlers: vec![], - truncated_handlers: vec![], - transfers: vec![r2types::SymbolicVmTransferArm { - handler_target: 0x1004, - case_values: vec![1, 2], - region_blocks: vec![0x1004, 0x1008], - exit_targets: vec![0x1008], - exit_guards: Vec::new(), - state_updates: vec![SymbolicVmStateUpdate { - output: "state".to_string(), - expr: "state + 1".to_string(), - value: r2types::SymbolicVmValueExpr::Expr("state + 1".to_string()), + let artifact = r2sym::SemanticArtifact { + stage: r2sym::RefinementStage::Residual, + granularity: r2sym::ArtifactGranularity::SummaryOnly, + execution: r2sym::ExecutionModel::Vm, + body: r2sym::SemanticArtifactBody::Vm(Box::new(r2sym::VmArtifactBody { + interpreter: None, + step_summary: None, + transfer_summary: Some(r2sym::VmStepSummary { + kind: r2sym::InterpreterKind::SwitchDispatch, + loop_header: 0x1000, + dispatch_header: 0x1000, + selector: Some("vm.sel".to_string()), + dispatch_targets: vec![0x1004], + default_target: None, + case_values_by_target: BTreeMap::from([(0x1004, vec![1, 2])]), + loop_latches: vec![0x1000], + state_inputs: vec!["state".to_string()], + state_outputs: vec!["state".to_string()], + step_blocks: vec![0x1000], + handler_regions: BTreeMap::from([(0x1004, vec![0x1004, 0x1008])]), + handler_state_inputs: BTreeMap::from([(0x1004, vec!["state".to_string()])]), + handler_state_outputs: BTreeMap::from([(0x1004, vec!["state".to_string()])]), + handler_state_updates: BTreeMap::from([( + 0x1004, + vec![r2sym::VmStateUpdate { + output: "state".to_string(), + expr: "state + 1".to_string(), + value: r2sym::VmValueExpr::Expr("state + 1".to_string()), + exact: false, + }], + )]), + handler_exit_guards: BTreeMap::new(), + handler_memory_read_effects: BTreeMap::new(), + handler_memory_write_effects: BTreeMap::new(), + handler_memory_reads: BTreeMap::from([(0x1004, 1)]), + handler_memory_writes: BTreeMap::from([(0x1004, 1)]), + handler_calls: BTreeMap::from([(0x1004, 0)]), + handler_conditional_branches: BTreeMap::from([(0x1004, 0)]), + handler_exit_targets: BTreeMap::from([(0x1004, vec![0x1008])]), + redispatch_handlers: vec![0x1000], + returning_handlers: vec![], + truncated_handlers: vec![], + transfers: vec![r2sym::VmTransferArm { + handler_target: 0x1004, + case_values: vec![1, 2], + region_blocks: vec![0x1004, 0x1008], + exit_targets: vec![0x1008], + exit_guards: Vec::new(), + state_updates: vec![r2sym::VmStateUpdate { + output: "state".to_string(), + expr: "state + 1".to_string(), + value: r2sym::VmValueExpr::Expr("state + 1".to_string()), + exact: false, + }], + selector_update: None, + memory_reads: Vec::new(), + memory_writes: Vec::new(), + residual_guards: false, + residual_memory_effects: false, exact: false, - evidence: r2types::SymbolicSemanticEvidence::heuristic( - r2types::SymbolicSemanticEvidenceReason::ValueOpaque, - ), - confidence: r2types::SymbolicSemanticConfidence::Heuristic, + redispatch: false, + may_return: false, + truncated: false, }], - selector_update: None, - memory_reads: Vec::new(), - memory_writes: Vec::new(), - residual_guards: false, - residual_memory_effects: false, - exact: false, - evidence: r2types::SymbolicSemanticEvidence::likely( - r2types::SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: r2types::SymbolicSemanticConfidence::Likely, - redispatch: false, - may_return: false, - truncated: false, - }], - }), - ..SymbolicSemanticFacts::default() + }), + })), + diagnostics: r2sym::SemanticArtifactDiagnostics { + branches_evaluated: 0, + branches_pruned: 0, + branches_unknown: 0, + skipped_missing_arch: false, + skipped_large_cfg: false, + residual_reasons: Vec::new(), + ambiguous_targets: Vec::new(), + cache_hit: false, + }, }; - ctx.inputs.symbolic_facts = Box::leak(Box::new(facts)); + ctx.inputs.semantic_artifact = Some(Box::leak(Box::new(artifact))); let mut structurer = ControlFlowStructurer::new(&func, &ctx); assert_eq!( @@ -3268,34 +3362,39 @@ mod tests { fn symbolic_exact_reachable_target_uses_control_island_fallback() { let func = function_with_single_block(0x2000); let mut ctx = FoldingContext::new(64); - ctx.inputs.symbolic_facts = Box::leak(Box::new(SymbolicSemanticFacts { - control_islands: vec![SymbolicControlIsland { - kind: SymbolicControlIslandKind::LargeCfgBranchFrontier, - anchor_block: 0x2000, - frontier_targets: vec![0x2004, 0x2008], - facts: vec![ - SymbolicControlFact { - target: 0x2004, - status: SymbolicReachabilityStatus::Reachable, - condition: Some("x == 0".to_string()), - compiled: None, - evidence: SymbolicSemanticEvidence::exact(), - confidence: SymbolicSemanticConfidence::Exact, - }, - SymbolicControlFact { - target: 0x2008, - status: SymbolicReachabilityStatus::Unreachable, - condition: Some("!(x == 0)".to_string()), - compiled: None, - evidence: SymbolicSemanticEvidence::exact(), - confidence: SymbolicSemanticConfidence::Exact, - }, - ], - evidence: SymbolicSemanticEvidence::exact(), - confidence: SymbolicSemanticConfidence::Exact, - }], - ..SymbolicSemanticFacts::default() - })); + let region = crate::test_semantic_region( + 0x2000, + BTreeSet::from([0x2004, 0x2008]), + vec![ + crate::test_control_fact( + 0x2004, + r2sym::SymbolicReachabilityStatus::Reachable, + None, + Some("x == 0"), + None, + r2sym::SemanticEvidence::exact(), + ), + crate::test_control_fact( + 0x2008, + r2sym::SymbolicReachabilityStatus::Unreachable, + None, + Some("!(x == 0)"), + None, + r2sym::SemanticEvidence::exact(), + ), + ], + Vec::new(), + ); + ctx.inputs.semantic_artifact = Some(crate::leaked_test_semantic_artifact( + crate::test_native_semantic_artifact( + r2sym::RefinementStage::Compiled, + r2sym::ArtifactGranularity::Regioned, + r2sym::SliceClass::Worker, + false, + Vec::new(), + vec![region], + ), + )); let structurer = ControlFlowStructurer::new(&func, &ctx); assert_eq!( @@ -3308,171 +3407,422 @@ mod tests { fn symbolic_actionable_reachable_target_uses_likely_control_island_fallback() { let func = function_with_single_block(0x2000); let mut ctx = FoldingContext::new(64); - ctx.inputs.symbolic_facts = Box::leak(Box::new(SymbolicSemanticFacts { - control_islands: vec![SymbolicControlIsland { - kind: SymbolicControlIslandKind::LargeCfgBranchFrontier, - anchor_block: 0x2000, - frontier_targets: vec![0x2004, 0x2008], - facts: vec![ - SymbolicControlFact { + let likely = + r2sym::SemanticEvidence::likely(r2sym::SemanticEvidenceReason::PartialPathCoverage); + let region = crate::test_semantic_region( + 0x2000, + BTreeSet::from([0x2004, 0x2008]), + vec![ + crate::test_control_fact( + 0x2004, + r2sym::SymbolicReachabilityStatus::Reachable, + None, + Some("x == 0"), + Some(r2sym::BackwardConditionSummary { + simplified: "x == 0".to_string(), + terms: vec!["x == 0".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: r2sym::BackwardConditionPrecision::OverApprox, + supported_paths: 1, + total_paths: 2, + }), + likely.clone(), + ), + crate::test_control_fact( + 0x2008, + r2sym::SymbolicReachabilityStatus::Unreachable, + None, + Some("!(x == 0)"), + None, + likely.clone(), + ), + ], + Vec::new(), + ); + ctx.inputs.semantic_artifact = Some(crate::leaked_test_semantic_artifact( + crate::test_native_semantic_artifact( + r2sym::RefinementStage::Compiled, + r2sym::ArtifactGranularity::Regioned, + r2sym::SliceClass::Worker, + false, + Vec::new(), + vec![region], + ), + )); + + let structurer = ControlFlowStructurer::new(&func, &ctx); + assert_eq!( + structurer.symbolic_actionable_reachable_target(0x2000), + Some(0x2004) + ); + } + + #[test] + fn symbolic_actionable_if_accepts_reachable_target_inside_region() { + let func = function_with_return_blocks(&[0x2000, 0x2004, 0x2008]); + let mut ctx = FoldingContext::new(64); + let likely = + r2sym::SemanticEvidence::likely(r2sym::SemanticEvidenceReason::PartialPathCoverage); + let region = crate::test_semantic_region( + 0x2000, + BTreeSet::from([0x2004, 0x2008]), + vec![ + crate::test_control_fact( + 0x2008, + r2sym::SymbolicReachabilityStatus::Reachable, + None, + Some("x == 0"), + Some(r2sym::BackwardConditionSummary { + simplified: "x == 0".to_string(), + terms: vec!["x == 0".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: r2sym::BackwardConditionPrecision::OverApprox, + supported_paths: 1, + total_paths: 2, + }), + likely.clone(), + ), + crate::test_control_fact( + 0x2004, + r2sym::SymbolicReachabilityStatus::Unreachable, + None, + Some("!(x == 0)"), + Some(r2sym::BackwardConditionSummary { + simplified: "!(x == 0)".to_string(), + terms: vec!["!(x == 0)".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: r2sym::BackwardConditionPrecision::OverApprox, + supported_paths: 1, + total_paths: 2, + }), + likely.clone(), + ), + ], + Vec::new(), + ); + ctx.inputs.semantic_artifact = Some(crate::leaked_test_semantic_artifact( + crate::test_native_semantic_artifact( + r2sym::RefinementStage::Compiled, + r2sym::ArtifactGranularity::Regioned, + r2sym::SliceClass::Worker, + false, + Vec::new(), + vec![region], + ), + )); + + let mut structurer = ControlFlowStructurer::new(&func, &ctx); + let then_region = crate::region::Region::Sequence(vec![ + crate::region::Region::Block(0x2004), + crate::region::Region::Block(0x2008), + ]); + + let rewritten = + structurer.try_structure_symbolic_actionable_if(0x2000, &then_region, None, None); + assert!( + rewritten.is_some(), + "reachable targets inside the structured region should still drive symbolic if shaping" + ); + } + + #[test] + fn semantic_worker_island_structuring_builds_real_if_statement() { + let func = function_with_conditional_return_blocks(0x2000, 0x2004, 0x2008); + let mut ctx = FoldingContext::new(64); + let region = r2sym::SemanticRegion { + anchor: 0x2000, + frontier: std::collections::BTreeSet::from([0x2004, 0x2008]), + control: vec![ + r2sym::Judged::new( + r2sym::ControlFact { target: 0x2004, - status: SymbolicReachabilityStatus::Reachable, + status: r2sym::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), condition: Some("x == 0".to_string()), - compiled: Some(SymbolicCompiledCondition { + compiled: Some(r2sym::BackwardConditionSummary { simplified: "x == 0".to_string(), terms: vec!["x == 0".to_string()], - memory_terms: Vec::new(), + memory_terms: vec![r2sym::BackwardMemoryCondition { + region: r2sym::BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: r2sym::SemanticEvidence::exact(), + binding: None, + expr: "*arg0".to_string(), + value_expr: Some("0x0:8".to_string()), + exact_value: true, + }], backward_memory_substitutions: 0, backward_memory_candidate_enumerations: 0, backward_memory_residual_fallbacks: 0, - precision: SymbolicConditionPrecision::OverApprox, - evidence: SymbolicSemanticEvidence::likely( - SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: SymbolicSemanticConfidence::Likely, + precision: r2sym::BackwardConditionPrecision::Exact, supported_paths: 1, - total_paths: 2, + total_paths: 1, }), - evidence: SymbolicSemanticEvidence::likely( - SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: SymbolicSemanticConfidence::Likely, }, - SymbolicControlFact { + r2sym::SemanticEvidence::exact(), + ), + r2sym::Judged::new( + r2sym::ControlFact { target: 0x2008, - status: SymbolicReachabilityStatus::Unreachable, + status: r2sym::SymbolicReachabilityStatus::Unreachable, + branch_truth: Some(false), condition: Some("!(x == 0)".to_string()), compiled: None, - evidence: SymbolicSemanticEvidence::likely( - SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: SymbolicSemanticConfidence::Likely, }, - ], - evidence: SymbolicSemanticEvidence::likely( - SymbolicSemanticEvidenceReason::PartialPathCoverage, + r2sym::SemanticEvidence::exact(), ), - confidence: SymbolicSemanticConfidence::Likely, - }], - ..SymbolicSemanticFacts::default() - })); + ], + memory: vec![r2sym::Judged::new( + r2sym::MemoryFact { + term: r2sym::BackwardMemoryCondition { + region: r2sym::BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: r2sym::SemanticEvidence::exact(), + binding: None, + expr: "*arg0".to_string(), + value_expr: Some("0x0:8".to_string()), + exact_value: true, + }, + }, + r2sym::SemanticEvidence::exact(), + )], + pre: Vec::new(), + post: Vec::new(), + targets: vec![ + r2sym::Judged::new( + r2sym::TargetFact { + target: 0x2004, + status: r2sym::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + }, + r2sym::SemanticEvidence::exact(), + ), + r2sym::Judged::new( + r2sym::TargetFact { + target: 0x2008, + status: r2sym::SymbolicReachabilityStatus::Unreachable, + branch_truth: Some(false), + }, + r2sym::SemanticEvidence::exact(), + ), + ], + }; + let artifact = r2sym::SemanticArtifact { + stage: r2sym::RefinementStage::Compiled, + granularity: r2sym::ArtifactGranularity::Regioned, + execution: r2sym::ExecutionModel::Native, + body: r2sym::SemanticArtifactBody::Native(r2sym::NativeArtifactBody { + summary: r2sym::NativeFunctionSummary { + slice_class: r2sym::SliceClass::Worker, + closure_functions: 1, + helper_functions: 0, + derived_summaries: 0, + derived_diagnostics: Default::default(), + }, + regions: BTreeMap::from([(region.key(), region)]), + }), + diagnostics: r2sym::SemanticArtifactDiagnostics { + branches_evaluated: 1, + branches_pruned: 1, + branches_unknown: 0, + skipped_missing_arch: false, + skipped_large_cfg: true, + residual_reasons: vec![r2sym::ResidualReason::LargeCfg], + ambiguous_targets: Vec::new(), + cache_hit: false, + }, + }; + ctx.inputs.semantic_artifact = Some(Box::leak(Box::new(artifact))); - let structurer = ControlFlowStructurer::new(&func, &ctx); - assert_eq!( - structurer.symbolic_actionable_reachable_target(0x2000), - Some(0x2004) + let mut structurer = ControlFlowStructurer::new(&func, &ctx); + let rewritten = structurer + .structure_semantic_worker_islands(4) + .expect("semantic worker structuring"); + assert!( + matches!(rewritten, CStmt::If { .. } | CStmt::Block(_)), + "expected structured semantic worker output, got {rewritten:?}" ); } #[test] - fn symbolic_actionable_if_accepts_reachable_target_inside_region() { - let func = function_with_return_blocks(&[0x2000, 0x2004, 0x2008]); + fn semantic_worker_structuring_negates_false_reachable_branch() { + let func = function_with_conditional_return_blocks(0x2000, 0x2008, 0x2004); let mut ctx = FoldingContext::new(64); - ctx.inputs.symbolic_facts = Box::leak(Box::new(SymbolicSemanticFacts { - control_islands: vec![SymbolicControlIsland { - kind: SymbolicControlIslandKind::LargeCfgBranchFrontier, - anchor_block: 0x2000, - frontier_targets: vec![0x2004, 0x2008], - facts: vec![ - SymbolicControlFact { + let likely = + r2sym::SemanticEvidence::likely(r2sym::SemanticEvidenceReason::DerivedFromRanking); + let region = r2sym::SemanticRegion { + anchor: 0x2000, + frontier: std::collections::BTreeSet::from([0x2004, 0x2008]), + control: vec![ + r2sym::Judged::new( + r2sym::ControlFact { target: 0x2008, - status: SymbolicReachabilityStatus::Reachable, + status: r2sym::SymbolicReachabilityStatus::Unreachable, + branch_truth: Some(true), condition: Some("x == 0".to_string()), - compiled: Some(SymbolicCompiledCondition { - simplified: "x == 0".to_string(), - terms: vec!["x == 0".to_string()], - memory_terms: Vec::new(), - backward_memory_substitutions: 0, - backward_memory_candidate_enumerations: 0, - backward_memory_residual_fallbacks: 0, - precision: SymbolicConditionPrecision::OverApprox, - evidence: SymbolicSemanticEvidence::likely( - SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: SymbolicSemanticConfidence::Likely, - supported_paths: 1, - total_paths: 2, - }), - evidence: SymbolicSemanticEvidence::likely( - SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: SymbolicSemanticConfidence::Likely, + compiled: None, }, - SymbolicControlFact { + likely.clone(), + ), + r2sym::Judged::new( + r2sym::ControlFact { target: 0x2004, - status: SymbolicReachabilityStatus::Unreachable, + status: r2sym::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(false), condition: Some("!(x == 0)".to_string()), - compiled: Some(SymbolicCompiledCondition { + compiled: Some(r2sym::BackwardConditionSummary { simplified: "!(x == 0)".to_string(), terms: vec!["!(x == 0)".to_string()], - memory_terms: Vec::new(), + memory_terms: vec![r2sym::BackwardMemoryCondition { + region: r2sym::BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: likely.clone(), + binding: None, + expr: "*arg0".to_string(), + value_expr: Some("0x1:8".to_string()), + exact_value: true, + }], backward_memory_substitutions: 0, backward_memory_candidate_enumerations: 0, backward_memory_residual_fallbacks: 0, - precision: SymbolicConditionPrecision::OverApprox, - evidence: SymbolicSemanticEvidence::likely( - SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: SymbolicSemanticConfidence::Likely, + precision: r2sym::BackwardConditionPrecision::OverApprox, supported_paths: 1, total_paths: 2, }), - evidence: SymbolicSemanticEvidence::likely( - SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: SymbolicSemanticConfidence::Likely, }, - ], - evidence: SymbolicSemanticEvidence::likely( - SymbolicSemanticEvidenceReason::PartialPathCoverage, + likely.clone(), ), - confidence: SymbolicSemanticConfidence::Likely, - }], - ..SymbolicSemanticFacts::default() - })); + ], + memory: vec![r2sym::Judged::new( + r2sym::MemoryFact { + term: r2sym::BackwardMemoryCondition { + region: r2sym::BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: likely.clone(), + binding: None, + expr: "*arg0".to_string(), + value_expr: Some("0x1:8".to_string()), + exact_value: true, + }, + }, + likely.clone(), + )], + pre: Vec::new(), + post: Vec::new(), + targets: vec![ + r2sym::Judged::new( + r2sym::TargetFact { + target: 0x2008, + status: r2sym::SymbolicReachabilityStatus::Unreachable, + branch_truth: Some(true), + }, + likely.clone(), + ), + r2sym::Judged::new( + r2sym::TargetFact { + target: 0x2004, + status: r2sym::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(false), + }, + likely.clone(), + ), + ], + }; + let artifact = r2sym::SemanticArtifact { + stage: r2sym::RefinementStage::Compiled, + granularity: r2sym::ArtifactGranularity::Regioned, + execution: r2sym::ExecutionModel::Native, + body: r2sym::SemanticArtifactBody::Native(r2sym::NativeArtifactBody { + summary: r2sym::NativeFunctionSummary { + slice_class: r2sym::SliceClass::Worker, + closure_functions: 0, + helper_functions: 0, + derived_summaries: 0, + derived_diagnostics: Default::default(), + }, + regions: BTreeMap::from([(region.key(), region)]), + }), + diagnostics: r2sym::SemanticArtifactDiagnostics { + branches_evaluated: 1, + branches_pruned: 0, + branches_unknown: 0, + skipped_missing_arch: false, + skipped_large_cfg: true, + residual_reasons: vec![r2sym::ResidualReason::LargeCfg], + ambiguous_targets: Vec::new(), + cache_hit: false, + }, + }; + ctx.inputs.semantic_artifact = Some(Box::leak(Box::new(artifact))); let mut structurer = ControlFlowStructurer::new(&func, &ctx); - let then_region = crate::region::Region::Sequence(vec![ - crate::region::Region::Block(0x2004), - crate::region::Region::Block(0x2008), - ]); - - let rewritten = - structurer.try_structure_symbolic_actionable_if(0x2000, &then_region, None, None); + let rewritten = structurer + .structure_semantic_worker_islands(4) + .expect("semantic worker structuring"); + let if_stmt = trailing_if_stmt(&rewritten); + let CStmt::If { cond, .. } = if_stmt else { + panic!("expected if statement, got {if_stmt:?}"); + }; assert!( - rewritten.is_some(), - "reachable targets inside the structured region should still drive symbolic if shaping" + matches!( + cond, + CExpr::Binary { + op: BinaryOp::Ne, + .. + } | CExpr::Unary { + op: UnaryOp::Not, + .. + } + ), + "reachable false successor must negate the branch condition, got {cond:?}" ); } #[test] - fn semantic_worker_island_structuring_builds_real_if_statement() { - let func = function_with_conditional_return_blocks(0x2000, 0x2004, 0x2008); + fn semantic_worker_structuring_keeps_multi_block_target_suffix() { + let func = function_with_multi_block_true_arm(0x2000, 0x2008, 0x200c, 0x2004); let mut ctx = FoldingContext::new(64); - ctx.inputs.symbolic_facts = Box::leak(Box::new(SymbolicSemanticFacts { - worker_islands: vec![SymbolicWorkerIsland { - anchor_block: 0x2000, - control_kind: Some(SymbolicControlIslandKind::LargeCfgBranchFrontier), - memory_kind: Some(SymbolicMemoryIslandKind::LargeCfgConditionFrontier), - frontier_targets: vec![0x2004, 0x2008], - control_facts: vec![ - SymbolicControlFact { - target: 0x2004, - status: SymbolicReachabilityStatus::Reachable, + let region = r2sym::SemanticRegion { + anchor: 0x2000, + frontier: BTreeSet::from([0x2008, 0x2004]), + control: vec![ + r2sym::Judged::new( + r2sym::ControlFact { + target: 0x2008, + status: r2sym::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), condition: Some("x == 0".to_string()), - compiled: Some(SymbolicCompiledCondition { + compiled: Some(r2sym::BackwardConditionSummary { simplified: "x == 0".to_string(), terms: vec!["x == 0".to_string()], - memory_terms: vec![SymbolicMemoryCondition { - region: SymbolicMemoryRegion::Argument { index: 0 }, + memory_terms: vec![r2sym::BackwardMemoryCondition { + region: r2sym::BackwardMemoryRegion::Argument { index: 0 }, offset_lo: 0, offset_hi: 0, size: 1, exact_offset: true, - evidence: SymbolicSemanticEvidence::likely( - SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: SymbolicSemanticConfidence::Likely, + evidence: r2sym::SemanticEvidence::exact(), binding: None, expr: "*arg0".to_string(), value_expr: Some("0x0:8".to_string()), @@ -3481,85 +3831,100 @@ mod tests { backward_memory_substitutions: 0, backward_memory_candidate_enumerations: 0, backward_memory_residual_fallbacks: 0, - precision: SymbolicConditionPrecision::OverApprox, - evidence: SymbolicSemanticEvidence::likely( - SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: SymbolicSemanticConfidence::Likely, + precision: r2sym::BackwardConditionPrecision::Exact, supported_paths: 1, - total_paths: 2, + total_paths: 1, }), - evidence: SymbolicSemanticEvidence::likely( - SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: SymbolicSemanticConfidence::Likely, }, - SymbolicControlFact { - target: 0x2008, - status: SymbolicReachabilityStatus::Unreachable, + r2sym::SemanticEvidence::exact(), + ), + r2sym::Judged::new( + r2sym::ControlFact { + target: 0x2004, + status: r2sym::SymbolicReachabilityStatus::Unreachable, + branch_truth: Some(false), condition: Some("!(x == 0)".to_string()), - compiled: Some(SymbolicCompiledCondition { - simplified: "!(x == 0)".to_string(), - terms: vec!["!(x == 0)".to_string()], - memory_terms: Vec::new(), - backward_memory_substitutions: 0, - backward_memory_candidate_enumerations: 0, - backward_memory_residual_fallbacks: 0, - precision: SymbolicConditionPrecision::OverApprox, - evidence: SymbolicSemanticEvidence::likely( - SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: SymbolicSemanticConfidence::Likely, - supported_paths: 1, - total_paths: 2, - }), - evidence: SymbolicSemanticEvidence::likely( - SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: SymbolicSemanticConfidence::Likely, + compiled: None, }, - ], - memory_terms: vec![SymbolicMemoryCondition { - region: SymbolicMemoryRegion::Argument { index: 0 }, - offset_lo: 0, - offset_hi: 0, - size: 1, - exact_offset: true, - evidence: SymbolicSemanticEvidence::likely( - SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: SymbolicSemanticConfidence::Likely, - binding: None, - expr: "*arg0".to_string(), - value_expr: Some("0x0:8".to_string()), - exact_value: true, - }], - evidence: SymbolicSemanticEvidence::likely( - SymbolicSemanticEvidenceReason::PartialPathCoverage, + r2sym::SemanticEvidence::exact(), ), - confidence: SymbolicSemanticConfidence::Likely, - }], - branch_facts: vec![SymbolicBranchFact { - block_addr: 0x2000, - true_target: 0x2004, - false_target: 0x2008, - true_status: SymbolicReachabilityStatus::Reachable, - false_status: SymbolicReachabilityStatus::Unreachable, - true_condition: Some("x == 0".to_string()), - false_condition: Some("!(x == 0)".to_string()), - true_compiled: None, - false_compiled: None, - }], - ..SymbolicSemanticFacts::default() - })); + ], + memory: vec![r2sym::Judged::new( + r2sym::MemoryFact { + term: r2sym::BackwardMemoryCondition { + region: r2sym::BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: r2sym::SemanticEvidence::exact(), + binding: None, + expr: "*arg0".to_string(), + value_expr: Some("0x0:8".to_string()), + exact_value: true, + }, + }, + r2sym::SemanticEvidence::exact(), + )], + pre: Vec::new(), + post: Vec::new(), + targets: vec![ + r2sym::Judged::new( + r2sym::TargetFact { + target: 0x2008, + status: r2sym::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + }, + r2sym::SemanticEvidence::exact(), + ), + r2sym::Judged::new( + r2sym::TargetFact { + target: 0x2004, + status: r2sym::SymbolicReachabilityStatus::Unreachable, + branch_truth: Some(false), + }, + r2sym::SemanticEvidence::exact(), + ), + ], + }; + let artifact = r2sym::SemanticArtifact { + stage: r2sym::RefinementStage::Compiled, + granularity: r2sym::ArtifactGranularity::Regioned, + execution: r2sym::ExecutionModel::Native, + body: r2sym::SemanticArtifactBody::Native(r2sym::NativeArtifactBody { + summary: r2sym::NativeFunctionSummary { + slice_class: r2sym::SliceClass::Worker, + closure_functions: 0, + helper_functions: 0, + derived_summaries: 0, + derived_diagnostics: Default::default(), + }, + regions: BTreeMap::from([(region.key(), region)]), + }), + diagnostics: r2sym::SemanticArtifactDiagnostics { + branches_evaluated: 1, + branches_pruned: 0, + branches_unknown: 0, + skipped_missing_arch: false, + skipped_large_cfg: true, + residual_reasons: vec![r2sym::ResidualReason::LargeCfg], + ambiguous_targets: Vec::new(), + cache_hit: false, + }, + }; + ctx.inputs.semantic_artifact = Some(Box::leak(Box::new(artifact))); let mut structurer = ControlFlowStructurer::new(&func, &ctx); let rewritten = structurer .structure_semantic_worker_islands(4) .expect("semantic worker structuring"); + let if_stmt = trailing_if_stmt(&rewritten); + let CStmt::If { then_body, .. } = if_stmt else { + panic!("expected if statement, got {if_stmt:?}"); + }; assert!( - matches!(rewritten, CStmt::If { .. } | CStmt::Block(_)), - "expected structured semantic worker output, got {rewritten:?}" + ControlFlowStructurer::stmt_guarantees_termination(then_body.as_ref()), + "structured semantic worker arm should keep the full target suffix, got {then_body:?}" ); } } diff --git a/crates/r2sym/Cargo.toml b/crates/r2sym/Cargo.toml index d6b2145..fff3912 100644 --- a/crates/r2sym/Cargo.toml +++ b/crates/r2sym/Cargo.toml @@ -24,4 +24,5 @@ r2 = ["r2pipe", "serde_json"] [dev-dependencies] criterion = "0.5" +proptest = "1.6" sleigh-config = { version = "1.0", features = ["x86"] } diff --git a/crates/r2sym/src/lib.rs b/crates/r2sym/src/lib.rs index 55d9768..e508573 100644 --- a/crates/r2sym/src/lib.rs +++ b/crates/r2sym/src/lib.rs @@ -71,16 +71,16 @@ pub use replay::{ }; pub use runtime::{seed_default_state_for_arch, seed_memory_regions_for_arch}; pub use semantics::{ - CompiledFunctionSemantics, CompiledSemanticArtifact, CompiledSemanticMode, - InterpreterDispatchSummary, InterpreterKind, ResidualReason, SemanticCapability, - SemanticConfidence, SemanticEvidence, SemanticEvidenceAmbiguity, SemanticEvidenceCoverage, - SemanticEvidenceProvenance, SemanticEvidenceReason, SemanticEvidenceSoundness, SemanticMode, - SliceClass, SymbolicBranchFact, SymbolicControlFact, SymbolicControlIsland, - SymbolicControlIslandKind, SymbolicFunctionFactDiagnostics, SymbolicFunctionFacts, - SymbolicMemoryIsland, SymbolicMemoryIslandKind, SymbolicReachabilityStatus, - SymbolicWorkerIsland, VmBinaryOp, VmGuardCondition, VmGuardedExit, VmMemoryCondition, + ArtifactBuildPlan, ArtifactGranularity, ControlFact, DecompilePlan, ExecutionModel, + InterpreterDispatchSummary, InterpreterKind, Judged, MemoryFact, NativeArtifactBody, + NativeFunctionSummary, QueryGuidanceMode, QueryPlan, RefinementStage, RegionKey, + ResidualReason, SEMANTIC_ARTIFACT_SCHEMA_VERSION, SemanticArtifact, SemanticArtifactBody, + SemanticArtifactDiagnostics, SemanticConfidence, SemanticEvidence, SemanticEvidenceAmbiguity, + SemanticEvidenceCoverage, SemanticEvidenceProvenance, SemanticEvidenceReason, + SemanticEvidenceSoundness, SemanticPredicate, SemanticRegion, SemanticTargetConditionSource, + SliceClass, SymbolicReachabilityStatus, TargetFact, TargetQueryPlan, TargetQueryRoutePlan, + TypePlan, VmArtifactBody, VmBinaryOp, VmGuardCondition, VmGuardedExit, VmMemoryCondition, VmMemoryRegionRef, VmStateUpdate, VmStepSummary, VmTransferArm, VmUnaryOp, VmValueExpr, - collect_symbolic_function_facts, collect_symbolic_function_facts_with_scope, compile_function_semantics_with_scope, compile_semantic_artifact_default_with_scope, compile_semantic_artifact_with_scope, stable_scope_hash, }; diff --git a/crates/r2sym/src/query.rs b/crates/r2sym/src/query.rs index 9051d79..ed0a01d 100644 --- a/crates/r2sym/src/query.rs +++ b/crates/r2sym/src/query.rs @@ -19,7 +19,8 @@ use crate::backward::{ }; use crate::path::{ExploreConfig, ExploreStats, PathExplorer, PathResult, SolvedPath}; use crate::semantics::{ - CompiledSemanticArtifact, VmStepSummary, build_vm_step_summary, classify_interpreter_like, + SemanticArtifact, SemanticRegion, VmStepSummary, build_vm_step_summary, + classify_interpreter_like, }; use crate::sim::SummaryProfile; use crate::solver::{SatResult, SolverStats}; @@ -27,6 +28,36 @@ use crate::state::ExitStatus; const MAX_VM_COMPILED_STEPS: usize = 8; +#[derive(Debug, Clone, Copy)] +struct SemanticTargetConditionSource<'a> { + region: &'a SemanticRegion, + branch_truth: bool, + summary: &'a BackwardConditionSummary, +} + +fn selected_region_for_target( + artifact: &SemanticArtifact, + target_addr: u64, + hard_proof_only: bool, +) -> Option<&SemanticRegion> { + artifact.authoritative_region_for_target(target_addr, hard_proof_only) +} + +fn target_condition_source<'a>( + artifact: &'a SemanticArtifact, + target_addr: u64, + hard_proof_only: bool, +) -> Option> { + let native = artifact.native_body()?; + let source = artifact.target_condition_source(target_addr, hard_proof_only)?; + let region = native.region_for_anchor(source.block_addr)?; + Some(SemanticTargetConditionSource { + region, + branch_truth: source.branch_truth, + summary: source.summary, + }) +} + /// Query execution strategy. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum QueryMode { @@ -230,6 +261,13 @@ enum CompiledPreconditionMode { NarrowOnly, } +fn compiled_mode_from_guidance(mode: crate::QueryGuidanceMode) -> CompiledPreconditionMode { + match mode { + crate::QueryGuidanceMode::Necessary => CompiledPreconditionMode::Necessary, + crate::QueryGuidanceMode::NarrowOnly => CompiledPreconditionMode::NarrowOnly, + } +} + fn precision_rank(precision: BackwardConditionPrecision) -> u8 { match precision { BackwardConditionPrecision::Exact => 3, @@ -663,65 +701,67 @@ where fn apply_best_compiled_precondition<'ctx>( explorer: &PathExplorer<'ctx>, func: &SsaArtifact, - artifact: Option<&CompiledSemanticArtifact>, + artifact: Option<&SemanticArtifact>, initial_state: SymState<'ctx>, target_addr: u64, ) -> PreconditionApplication<'ctx> { let derived_summaries = explorer.derived_call_summary_views(); - let condition_source = - artifact.and_then(|artifact| artifact.actionable_condition_source_for_target(target_addr)); - let memory_terms = artifact - .map(|artifact| artifact.actionable_memory_terms_for_target(target_addr)) - .unwrap_or_default(); - let worker_island = - artifact.and_then(|artifact| artifact.best_worker_island_for_target(target_addr, false)); - let source_is_necessary = condition_source.is_some_and(|source| { - source.necessary_for_target && source.summary.evidence().allows_narrowing() + let route = artifact + .map(|artifact| artifact.target_query_route_plan(target_addr)) + .unwrap_or_else(crate::TargetQueryRoutePlan::dynamic_fallback); + let branch_mode = route.branch_guidance.map(compiled_mode_from_guidance); + let condition_source = branch_mode.and_then(|_| { + artifact.and_then(|artifact| target_condition_source(artifact, target_addr, false)) }); - let island_mode = worker_island.and_then(|island| { - if island.exact_reachable_target() == Some(target_addr) { - Some(CompiledPreconditionMode::Necessary) - } else if island.actionable_reachable_target() == Some(target_addr) { - Some(CompiledPreconditionMode::NarrowOnly) - } else { - island - .actionable_compiled_condition() - .map(|_| CompiledPreconditionMode::NarrowOnly) - } + let selected_region = branch_mode.and_then(|_| { + artifact.and_then(|artifact| selected_region_for_target(artifact, target_addr, false)) }); - if !source_is_necessary - && matches!(island_mode, Some(CompiledPreconditionMode::NarrowOnly)) + let memory_region = if route.allow_memory_term_narrowing { + artifact.and_then(|artifact| artifact.authoritative_memory_region_for_target(target_addr)) + } else { + None + }; + let memory_terms = if route.allow_memory_term_narrowing { + memory_region + .map(|region| region.actionable_memory_terms_for_target(target_addr)) + .unwrap_or_default() + } else { + Vec::new() + }; + if route.allow_memory_term_narrowing && let Some(narrowed_state) = apply_memory_term_narrowing(&initial_state, &memory_terms) { return PreconditionApplication::Continue { initial_state: Box::new(initial_state), narrowed_state: Some(narrowed_state), - compiled_precondition: worker_island - .and_then(|island| island.actionable_compiled_condition()) + compiled_precondition: selected_region + .and_then(|region| region.actionable_compiled_condition_for_target(target_addr)) + .filter(|summary| !summary.evidence().allows_hard_proof()) .cloned() - .or_else(|| condition_source.map(|source| source.summary.clone())), + .or_else(|| { + condition_source + .map(|source| source.summary) + .filter(|summary| !summary.evidence().allows_hard_proof()) + .cloned() + }), }; } - let mut compiled = condition_source.and_then(|source| { - let compiled = compile_branch_precondition_with_summaries( - func, - &initial_state, - source.block_addr, - source.branch_truth, - &derived_summaries, - )?; - Some(( - compiled, - if source.necessary_for_target { - CompiledPreconditionMode::Necessary - } else { - CompiledPreconditionMode::NarrowOnly - }, - )) + let mut compiled = branch_mode.and_then(|mode| { + condition_source.and_then(|source| { + let compiled = compile_branch_precondition_with_summaries( + func, + &initial_state, + source.region.anchor, + source.branch_truth, + &derived_summaries, + )?; + Some((compiled, mode)) + }) }); + let target_compile_mode = branch_mode.unwrap_or(CompiledPreconditionMode::Necessary); let mut used_target_compile = false; if compiled.is_none() - && let Some(mode) = island_mode + && route.allow_dynamic_target_compile && let Some(target_compiled) = compile_target_precondition_with_summaries( func, &initial_state, @@ -729,11 +769,12 @@ fn apply_best_compiled_precondition<'ctx>( &derived_summaries, ) { - compiled = Some((target_compiled, mode)); + compiled = Some((target_compiled, target_compile_mode)); used_target_compile = true; } - if !source_is_necessary + if route.allow_dynamic_target_compile && !used_target_compile + && !matches!(target_compile_mode, CompiledPreconditionMode::Necessary) && let Some(target_compiled) = compile_target_precondition_with_summaries( func, &initial_state, @@ -746,20 +787,24 @@ fn apply_best_compiled_precondition<'ctx>( { compiled = Some((target_compiled, CompiledPreconditionMode::Necessary)); } - if compiled.as_ref().is_none_or(|(compiled, _)| { - !matches!( - compiled.summary.precision, - BackwardConditionPrecision::Exact + if route.allow_vm_target_compile + && compiled.as_ref().is_none_or(|(compiled, _)| { + !matches!( + compiled.summary.precision, + BackwardConditionPrecision::Exact + ) + }) + && let Some(vm_compiled) = compile_vm_target_precondition_with_summaries( + func, + &initial_state, + target_addr, + &derived_summaries, ) - }) && let Some(vm_compiled) = compile_vm_target_precondition_with_summaries( - func, - &initial_state, - target_addr, - &derived_summaries, - ) && prefer_compiled_precondition( - compiled.as_ref().map(|(compiled, _)| compiled), - &vm_compiled, - ) { + && prefer_compiled_precondition( + compiled.as_ref().map(|(compiled, _)| compiled), + &vm_compiled, + ) + { compiled = Some((vm_compiled, CompiledPreconditionMode::Necessary)); } let Some((compiled, mode)) = compiled else { @@ -802,7 +847,7 @@ impl<'ctx> PathExplorer<'ctx> { pub fn can_reach_with_artifact( &mut self, func: &SsaArtifact, - artifact: Option<&CompiledSemanticArtifact>, + artifact: Option<&SemanticArtifact>, initial_state: SymState<'ctx>, target_addr: u64, ) -> ReachabilityResult<'ctx> { @@ -862,7 +907,7 @@ impl<'ctx> PathExplorer<'ctx> { pub fn path_conditions_at_with_artifact( &mut self, func: &SsaArtifact, - artifact: Option<&CompiledSemanticArtifact>, + artifact: Option<&SemanticArtifact>, initial_state: SymState<'ctx>, target_pc: u64, ) -> PathConditionResult<'ctx> { @@ -917,7 +962,7 @@ impl<'ctx> PathExplorer<'ctx> { pub fn solve_for_target_with_artifact( &mut self, func: &SsaArtifact, - artifact: Option<&CompiledSemanticArtifact>, + artifact: Option<&SemanticArtifact>, initial_state: SymState<'ctx>, target_addr: u64, ) -> SolveResult<'ctx> { @@ -1003,15 +1048,16 @@ mod tests { use super::{ CompiledPreconditionMode, PreconditionApplication, apply_best_compiled_precondition, - apply_compiled_precondition_with_mode, prefer_compiled_precondition, vm_target_case_values, + apply_compiled_precondition_with_mode, prefer_compiled_precondition, + target_condition_source, vm_target_case_values, }; use crate::{ - BackwardConditionPrecision, BackwardConditionSummary, BackwardMemoryCondition, - BackwardMemoryRegion, CompiledSemanticArtifact, ResidualReason, SatResult, - SemanticCapability, SemanticEvidence, SemanticEvidenceReason, SliceClass, SymQueryConfig, - SymState, SymbolicBranchFact, SymbolicControlFact, SymbolicControlIslandKind, - SymbolicFunctionFactDiagnostics, SymbolicFunctionFacts, SymbolicMemoryIsland, - SymbolicMemoryIslandKind, SymbolicReachabilityStatus, SymbolicWorkerIsland, VmBinaryOp, + ArtifactGranularity, BackwardConditionPrecision, BackwardConditionSummary, + BackwardMemoryCondition, BackwardMemoryRegion, ControlFact, ExecutionModel, Judged, + MemoryFact, NativeArtifactBody, NativeFunctionSummary, RefinementStage, RegionKey, + ResidualReason, SatResult, SemanticArtifact, SemanticArtifactBody, + SemanticArtifactDiagnostics, SemanticEvidence, SemanticEvidenceReason, SemanticRegion, + SliceClass, SymQueryConfig, SymState, TargetFact, TargetQueryPlan, VmBinaryOp, VmGuardCondition, VmGuardedExit, VmStateUpdate, VmStepSummary, VmTransferArm, VmValueExpr, }; use r2il::{R2ILBlock, R2ILOp, SpaceId, Varnode}; @@ -1023,6 +1069,75 @@ mod tests { const TMP0: u64 = 0x80; const TMP1: u64 = 0x88; + fn test_semantic_artifact( + stage: RefinementStage, + slice_class: SliceClass, + residual_reasons: Vec, + regions: Vec, + diagnostics: SemanticArtifactDiagnostics, + ) -> SemanticArtifact { + let regions: BTreeMap = regions + .into_iter() + .map(|region| { + ( + RegionKey::new(region.anchor, region.frontier.clone()), + region, + ) + }) + .collect(); + let granularity = if matches!(stage, RefinementStage::Compiled | RefinementStage::Residual) + && !regions.is_empty() + { + ArtifactGranularity::Regioned + } else { + ArtifactGranularity::WholeFunction + }; + SemanticArtifact { + stage, + granularity, + execution: ExecutionModel::Native, + body: SemanticArtifactBody::Native(NativeArtifactBody { + summary: NativeFunctionSummary { + slice_class, + closure_functions: 0, + helper_functions: 0, + derived_summaries: 0, + derived_diagnostics: crate::sim::DerivedSummaryDiagnostics::default(), + }, + regions, + }), + diagnostics: SemanticArtifactDiagnostics { + residual_reasons, + ..diagnostics + }, + } + } + + fn make_region(anchor: u64, frontier: &[u64]) -> SemanticRegion { + SemanticRegion { + anchor, + frontier: frontier.iter().copied().collect(), + control: Vec::new(), + memory: Vec::new(), + pre: Vec::new(), + post: Vec::new(), + targets: Vec::new(), + } + } + + fn default_diagnostics() -> SemanticArtifactDiagnostics { + SemanticArtifactDiagnostics { + branches_evaluated: 0, + branches_pruned: 0, + branches_unknown: 0, + skipped_missing_arch: false, + skipped_large_cfg: false, + residual_reasons: Vec::new(), + ambiguous_targets: Vec::new(), + cache_hit: false, + } + } + fn make_reg(offset: u64, size: u32) -> Varnode { Varnode { space: SpaceId::Register, @@ -1320,68 +1435,109 @@ mod tests { let mut state = SymState::new(&ctx, 0x1000); state.new_symbolic_input("sym_mem", 8); let original_constraints = state.num_constraints(); - - let artifact = CompiledSemanticArtifact { - mode: crate::SemanticMode::Residual, - slice_class: SliceClass::Worker, - capability: SemanticCapability { - query_ready: true, - type_ready: true, - decompile_ready: false, - }, - residual_reasons: vec![ResidualReason::LargeCfg], - closure_functions: 0, - helper_functions: 0, - derived_summaries: 0, - derived_diagnostics: crate::sim::DerivedSummaryDiagnostics::default(), - symbolic_facts: SymbolicFunctionFacts { - branch_facts: vec![SymbolicBranchFact { - block_addr: 0xdead, - true_target: 0x2000, - false_target: 0x2010, - true_status: SymbolicReachabilityStatus::Reachable, - false_status: SymbolicReachabilityStatus::Unreachable, - true_condition: Some("guard".to_string()), - false_condition: None, - true_compiled: Some(BackwardConditionSummary { - simplified: "guard".to_string(), - terms: vec!["guard".to_string()], - memory_terms: Vec::new(), - backward_memory_substitutions: 0, - backward_memory_candidate_enumerations: 0, - backward_memory_residual_fallbacks: 0, - precision: BackwardConditionPrecision::Exact, - supported_paths: 1, - total_paths: 1, - }), - false_compiled: None, - }], - worker_islands: Vec::new(), - control_islands: Vec::new(), - memory_islands: vec![SymbolicMemoryIsland { - kind: SymbolicMemoryIslandKind::LargeCfgConditionFrontier, - anchor_block: 0xdead, - terms: vec![BackwardMemoryCondition { - region: BackwardMemoryRegion::Argument { index: 0 }, - offset_lo: 0, - offset_hi: 0, - size: 1, - exact_offset: true, - evidence: SemanticEvidence::exact(), - binding: Some("sym_mem".to_string()), - expr: "0x2a".to_string(), - value_expr: Some("0x2a".to_string()), - exact_value: true, - }], - evidence: SemanticEvidence::exact(), - }], - diagnostics: SymbolicFunctionFactDiagnostics::default(), - }, - interpreter: None, - vm_step: None, - vm_transfer: None, - cache_hit: false, + let target_term = BackwardMemoryCondition { + region: BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: SemanticEvidence::exact(), + binding: Some("sym_mem".to_string()), + expr: "0x2a".to_string(), + value_expr: Some("0x2a".to_string()), + exact_value: true, }; + let other_branch_term = BackwardMemoryCondition { + region: BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: SemanticEvidence::exact(), + binding: Some("sym_mem".to_string()), + expr: "0x2b".to_string(), + value_expr: Some("0x2b".to_string()), + exact_value: true, + }; + + let artifact = test_semantic_artifact( + RefinementStage::Residual, + SliceClass::Worker, + vec![ResidualReason::LargeCfg], + vec![{ + let mut region = make_region(0xdead, &[0x2000, 0x2010]); + region.control.push(Judged::new( + ControlFact { + target: 0x2000, + status: crate::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + condition: Some("guard".to_string()), + compiled: Some(BackwardConditionSummary { + simplified: "guard".to_string(), + terms: vec!["guard".to_string()], + memory_terms: vec![target_term.clone()], + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }), + }, + SemanticEvidence::exact(), + )); + region.control.push(Judged::new( + ControlFact { + target: 0x2010, + status: crate::SymbolicReachabilityStatus::Unreachable, + branch_truth: Some(false), + condition: Some("!guard".to_string()), + compiled: Some(BackwardConditionSummary { + simplified: "!guard".to_string(), + terms: vec!["!guard".to_string()], + memory_terms: vec![other_branch_term.clone()], + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }), + }, + SemanticEvidence::exact(), + )); + region.targets.push(Judged::new( + TargetFact { + target: 0x2000, + status: crate::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + }, + SemanticEvidence::exact(), + )); + region.targets.push(Judged::new( + TargetFact { + target: 0x2010, + status: crate::SymbolicReachabilityStatus::Unreachable, + branch_truth: Some(false), + }, + SemanticEvidence::exact(), + )); + region.memory.push(Judged::new( + MemoryFact { + term: target_term.clone(), + }, + SemanticEvidence::exact(), + )); + region.memory.push(Judged::new( + MemoryFact { + term: other_branch_term.clone(), + }, + SemanticEvidence::exact(), + )); + region + }], + default_diagnostics(), + ); match apply_best_compiled_precondition(&explorer, &func, Some(&artifact), state, 0x2000) { PreconditionApplication::Continue { @@ -1435,62 +1591,117 @@ mod tests { ); state.mem_write(&crate::SymValue::concrete(0x2000, 64), &symbolic_byte, 1); let original_constraints = state.num_constraints(); + let target_term = BackwardMemoryCondition { + region: BackwardMemoryRegion::Region(crate::backward::BackwardRegionRef { + id: global_region, + kind: crate::MemoryRegionKind::Global, + name: "ram:0x2000".to_string(), + }), + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: SemanticEvidence::exact(), + binding: None, + expr: "*(ram:0x2000 + 0)".to_string(), + value_expr: Some("0x2a".to_string()), + exact_value: true, + }; + let other_branch_term = BackwardMemoryCondition { + region: BackwardMemoryRegion::Region(crate::backward::BackwardRegionRef { + id: global_region, + kind: crate::MemoryRegionKind::Global, + name: "ram:0x2000".to_string(), + }), + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: SemanticEvidence::exact(), + binding: None, + expr: "*(ram:0x2000 + 0)".to_string(), + value_expr: Some("0x2b".to_string()), + exact_value: true, + }; - let artifact = CompiledSemanticArtifact { - mode: crate::SemanticMode::Residual, - slice_class: SliceClass::Worker, - capability: SemanticCapability { - query_ready: true, - type_ready: true, - decompile_ready: false, - }, - residual_reasons: vec![ResidualReason::LargeCfg], - closure_functions: 0, - helper_functions: 0, - derived_summaries: 0, - derived_diagnostics: crate::sim::DerivedSummaryDiagnostics::default(), - symbolic_facts: SymbolicFunctionFacts { - branch_facts: vec![SymbolicBranchFact { - block_addr: 0xdead, - true_target: 0x2000, - false_target: 0x2010, - true_status: SymbolicReachabilityStatus::Reachable, - false_status: SymbolicReachabilityStatus::Unreachable, - true_condition: Some("guard".to_string()), - false_condition: None, - true_compiled: None, - false_compiled: None, - }], - worker_islands: Vec::new(), - control_islands: Vec::new(), - memory_islands: vec![SymbolicMemoryIsland { - kind: SymbolicMemoryIslandKind::LargeCfgConditionFrontier, - anchor_block: 0xdead, - terms: vec![BackwardMemoryCondition { - region: BackwardMemoryRegion::Region(crate::backward::BackwardRegionRef { - id: global_region, - kind: crate::MemoryRegionKind::Global, - name: "ram:0x2000".to_string(), + let artifact = test_semantic_artifact( + RefinementStage::Residual, + SliceClass::Worker, + vec![ResidualReason::LargeCfg], + vec![{ + let mut region = make_region(0xdead, &[0x2000, 0x2010]); + region.control.push(Judged::new( + ControlFact { + target: 0x2000, + status: crate::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + condition: Some("guard".to_string()), + compiled: Some(BackwardConditionSummary { + simplified: "guard".to_string(), + terms: vec!["guard".to_string()], + memory_terms: vec![target_term.clone()], + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, }), - offset_lo: 0, - offset_hi: 0, - size: 1, - exact_offset: true, - evidence: SemanticEvidence::exact(), - binding: None, - expr: "*(ram:0x2000 + 0)".to_string(), - value_expr: Some("0x2a".to_string()), - exact_value: true, - }], - evidence: SemanticEvidence::exact(), - }], - diagnostics: SymbolicFunctionFactDiagnostics::default(), - }, - interpreter: None, - vm_step: None, - vm_transfer: None, - cache_hit: false, - }; + }, + SemanticEvidence::exact(), + )); + region.control.push(Judged::new( + ControlFact { + target: 0x2010, + status: crate::SymbolicReachabilityStatus::Unreachable, + branch_truth: Some(false), + condition: Some("!guard".to_string()), + compiled: Some(BackwardConditionSummary { + simplified: "!guard".to_string(), + terms: vec!["!guard".to_string()], + memory_terms: vec![other_branch_term.clone()], + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }), + }, + SemanticEvidence::exact(), + )); + region.targets.push(Judged::new( + TargetFact { + target: 0x2000, + status: crate::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + }, + SemanticEvidence::exact(), + )); + region.targets.push(Judged::new( + TargetFact { + target: 0x2010, + status: crate::SymbolicReachabilityStatus::Unreachable, + branch_truth: Some(false), + }, + SemanticEvidence::exact(), + )); + region.memory.push(Judged::new( + MemoryFact { + term: target_term.clone(), + }, + SemanticEvidence::exact(), + )); + region.memory.push(Judged::new( + MemoryFact { + term: other_branch_term.clone(), + }, + SemanticEvidence::exact(), + )); + region + }], + default_diagnostics(), + ); match apply_best_compiled_precondition(&explorer, &func, Some(&artifact), state, 0x2000) { PreconditionApplication::Continue { @@ -1526,6 +1737,80 @@ mod tests { } } + #[test] + fn region_memory_bag_without_target_local_compiled_memory_does_not_seed_narrowed_state() { + let blocks = make_residual_precondition_blocks(); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); + let ctx = Context::thread_local(); + let explorer = SymQueryConfig::default().make_explorer(&ctx); + let mut state = SymState::new(&ctx, 0x1000); + state.new_symbolic_input("sym_mem", 8); + let original_constraints = state.num_constraints(); + + let artifact = test_semantic_artifact( + RefinementStage::Residual, + SliceClass::Worker, + vec![ResidualReason::LargeCfg], + vec![{ + let mut region = make_region(0xdead, &[0x2000, 0x2010]); + region.control.push(Judged::new( + ControlFact { + target: 0x2000, + status: crate::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + condition: Some("guard".to_string()), + compiled: Some(BackwardConditionSummary { + simplified: "guard".to_string(), + terms: vec!["guard".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }), + }, + SemanticEvidence::exact(), + )); + region.memory.push(Judged::new( + MemoryFact { + term: BackwardMemoryCondition { + region: BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: SemanticEvidence::exact(), + binding: Some("sym_mem".to_string()), + expr: "0x2a".to_string(), + value_expr: Some("0x2a".to_string()), + exact_value: true, + }, + }, + SemanticEvidence::exact(), + )); + region + }], + default_diagnostics(), + ); + + match apply_best_compiled_precondition(&explorer, &func, Some(&artifact), state, 0x2000) { + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition, + } => { + assert!(compiled_precondition.is_none()); + assert_eq!(initial_state.num_constraints(), original_constraints); + assert!(narrowed_state.is_none()); + } + PreconditionApplication::ExactUnsat { .. } => { + panic!("region-bag-only memory facts should not seed targeted narrowing") + } + } + } + #[test] fn worker_island_memory_terms_seed_narrowed_state_without_branch_recompile() { let blocks = make_residual_precondition_blocks(); @@ -1535,11 +1820,23 @@ mod tests { let mut state = SymState::new(&ctx, 0x1000); state.new_symbolic_input("sym_mem", 8); let original_constraints = state.num_constraints(); + let target_term = BackwardMemoryCondition { + region: BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: SemanticEvidence::exact(), + binding: Some("sym_mem".to_string()), + expr: "0x2a".to_string(), + value_expr: Some("0x2a".to_string()), + exact_value: true, + }; let actionable = BackwardConditionSummary { simplified: "worker_guard".to_string(), terms: vec!["worker_guard".to_string()], - memory_terms: Vec::new(), + memory_terms: vec![target_term.clone()], backward_memory_substitutions: 0, backward_memory_candidate_enumerations: 0, backward_memory_residual_fallbacks: 0, @@ -1547,58 +1844,32 @@ mod tests { supported_paths: 1, total_paths: 2, }; - let artifact = CompiledSemanticArtifact { - mode: crate::SemanticMode::IslandCompiled, - slice_class: SliceClass::Worker, - capability: SemanticCapability { - query_ready: true, - type_ready: true, - decompile_ready: true, - }, - residual_reasons: Vec::new(), - closure_functions: 0, - helper_functions: 0, - derived_summaries: 0, - derived_diagnostics: crate::sim::DerivedSummaryDiagnostics::default(), - symbolic_facts: SymbolicFunctionFacts { - branch_facts: Vec::new(), - worker_islands: vec![SymbolicWorkerIsland { - anchor_block: 0xdead, - control_kind: Some(SymbolicControlIslandKind::LargeCfgBranchFrontier), - memory_kind: Some(SymbolicMemoryIslandKind::LargeCfgConditionFrontier), - frontier_targets: vec![0x2000, 0x2010], - control_facts: vec![SymbolicControlFact { + let artifact = test_semantic_artifact( + RefinementStage::Compiled, + SliceClass::Worker, + Vec::new(), + vec![{ + let mut region = make_region(0xdead, &[0x2000, 0x2010]); + region.control.push(Judged::new( + ControlFact { target: 0x2000, - status: SymbolicReachabilityStatus::Reachable, + status: crate::SymbolicReachabilityStatus::Reachable, + branch_truth: None, condition: Some("worker_guard".to_string()), compiled: Some(actionable.clone()), - evidence: SemanticEvidence::likely( - SemanticEvidenceReason::DerivedFromRanking, - ), - }], - memory_terms: vec![BackwardMemoryCondition { - region: BackwardMemoryRegion::Argument { index: 0 }, - offset_lo: 0, - offset_hi: 0, - size: 1, - exact_offset: true, - evidence: SemanticEvidence::exact(), - binding: Some("sym_mem".to_string()), - expr: "0x2a".to_string(), - value_expr: Some("0x2a".to_string()), - exact_value: true, - }], - evidence: SemanticEvidence::likely(SemanticEvidenceReason::DerivedFromRanking), - }], - control_islands: Vec::new(), - memory_islands: Vec::new(), - diagnostics: SymbolicFunctionFactDiagnostics::default(), - }, - interpreter: None, - vm_step: None, - vm_transfer: None, - cache_hit: false, - }; + }, + SemanticEvidence::likely(SemanticEvidenceReason::DerivedFromRanking), + )); + region.memory.push(Judged::new( + MemoryFact { + term: target_term.clone(), + }, + SemanticEvidence::exact(), + )); + region + }], + default_diagnostics(), + ); match apply_best_compiled_precondition(&explorer, &func, Some(&artifact), state, 0x2000) { PreconditionApplication::Continue { @@ -1644,30 +1915,270 @@ mod tests { state.make_symbolic("reg:56_0", 64); let original_constraints = state.num_constraints(); - let artifact = CompiledSemanticArtifact { - mode: crate::SemanticMode::IslandCompiled, - slice_class: SliceClass::Worker, - capability: SemanticCapability { - query_ready: true, - type_ready: true, - decompile_ready: true, - }, - residual_reasons: Vec::new(), - closure_functions: 0, - helper_functions: 0, - derived_summaries: 0, - derived_diagnostics: crate::sim::DerivedSummaryDiagnostics::default(), - symbolic_facts: SymbolicFunctionFacts { - branch_facts: Vec::new(), - worker_islands: vec![SymbolicWorkerIsland { - anchor_block: 0x1000, - control_kind: Some(SymbolicControlIslandKind::LargeCfgBranchFrontier), - memory_kind: Some(SymbolicMemoryIslandKind::LargeCfgConditionFrontier), - frontier_targets: vec![0x1010, 0x1004], - control_facts: vec![ - SymbolicControlFact { + let artifact = test_semantic_artifact( + RefinementStage::Compiled, + SliceClass::Worker, + Vec::new(), + vec![{ + let mut region = make_region(0x1000, &[0x1010, 0x1004]); + region.control.push(Judged::new( + ControlFact { + target: 0x1010, + status: crate::SymbolicReachabilityStatus::Reachable, + branch_truth: None, + condition: Some("guard".to_string()), + compiled: Some(BackwardConditionSummary { + simplified: "guard".to_string(), + terms: vec!["guard".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }), + }, + SemanticEvidence::exact(), + )); + region.control.push(Judged::new( + ControlFact { + target: 0x1004, + status: crate::SymbolicReachabilityStatus::Unreachable, + branch_truth: None, + condition: None, + compiled: None, + }, + SemanticEvidence::exact(), + )); + region.memory.push(Judged::new( + MemoryFact { + term: BackwardMemoryCondition { + region: BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: SemanticEvidence::exact(), + binding: Some("reg:56_0".to_string()), + expr: "0x1".to_string(), + value_expr: Some("0x1".to_string()), + exact_value: true, + }, + }, + SemanticEvidence::exact(), + )); + region + }], + default_diagnostics(), + ); + + match apply_best_compiled_precondition(&explorer, &func, Some(&artifact), state, 0x1010) { + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition, + } => { + assert!(initial_state.num_constraints() >= original_constraints); + assert!(narrowed_state.is_none()); + assert!(compiled_precondition.is_some()); + } + PreconditionApplication::ExactUnsat { .. } => { + panic!("necessary worker islands should use the real compiled precondition path") + } + } + } + + #[test] + fn target_condition_source_only_marks_unique_region_targets_as_necessary() { + let artifact = test_semantic_artifact( + RefinementStage::Compiled, + SliceClass::Worker, + Vec::new(), + vec![{ + let mut region = make_region(0x1000, &[0x1010, 0x1004]); + region.control.push(Judged::new( + ControlFact { + target: 0x1010, + status: crate::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + condition: Some("guard".to_string()), + compiled: Some(BackwardConditionSummary { + simplified: "guard".to_string(), + terms: vec!["guard".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }), + }, + SemanticEvidence::exact(), + )); + region.control.push(Judged::new( + ControlFact { + target: 0x1004, + status: crate::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(false), + condition: Some("other_guard".to_string()), + compiled: None, + }, + SemanticEvidence::exact(), + )); + region + }], + default_diagnostics(), + ); + + let actionable = target_condition_source(&artifact, 0x1010, false) + .expect("actionable target condition source"); + assert!( + matches!( + artifact.target_query_plan(0x1010), + crate::TargetQueryPlan::Ready { + mode: crate::QueryGuidanceMode::NarrowOnly + } + ), + "multi-exit regions must not be treated as necessary-for-target" + ); + assert_eq!(actionable.region.anchor, 0x1000); + assert!( + target_condition_source(&artifact, 0x1010, true).is_none(), + "hard-proof target narrowing must reject non-necessary regions" + ); + } + + #[test] + fn target_local_memory_terms_follow_the_authoritative_target_source_region() { + let exact = SemanticEvidence::exact(); + let likely = SemanticEvidence::likely(SemanticEvidenceReason::PartialPathCoverage); + let artifact = test_semantic_artifact( + RefinementStage::Compiled, + SliceClass::Worker, + Vec::new(), + vec![ + { + let mut region = make_region(0x1000, &[0x1010]); + region.control.push(Judged::new( + ControlFact { target: 0x1010, - status: SymbolicReachabilityStatus::Reachable, + status: crate::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + condition: Some("exact_guard".to_string()), + compiled: Some(BackwardConditionSummary { + simplified: "exact_guard".to_string(), + terms: vec!["exact_guard".to_string()], + memory_terms: vec![BackwardMemoryCondition { + region: BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: exact.clone(), + binding: Some("sym_mem".to_string()), + expr: "0x2a".to_string(), + value_expr: Some("0x2a".to_string()), + exact_value: true, + }], + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }), + }, + exact.clone(), + )); + region + }, + { + let mut region = make_region(0x1004, &[0x1010]); + region.control.push(Judged::new( + ControlFact { + target: 0x1010, + status: crate::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + condition: Some("weaker_guard".to_string()), + compiled: Some(BackwardConditionSummary { + simplified: "weaker_guard".to_string(), + terms: vec!["weaker_guard".to_string()], + memory_terms: vec![BackwardMemoryCondition { + region: BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: likely.clone(), + binding: Some("sym_mem".to_string()), + expr: "0x2b".to_string(), + value_expr: Some("0x2b".to_string()), + exact_value: true, + }], + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::OverApprox, + supported_paths: 1, + total_paths: 2, + }), + }, + likely.clone(), + )); + region + }, + ], + default_diagnostics(), + ); + + assert!(matches!( + artifact.target_query_plan(0x1010), + crate::TargetQueryPlan::Residual { .. } + )); + let route = artifact.target_query_route_plan(0x1010); + assert!(matches!( + route.target_plan, + TargetQueryPlan::Residual { .. } + )); + assert!(!route.allow_memory_term_narrowing); + assert!(!route.allow_dynamic_target_compile); + assert!( + target_condition_source(&artifact, 0x1010, false).is_none(), + "materially conflicting target sources must not collapse into one authoritative source" + ); + let memory_terms = artifact.actionable_memory_terms_for_target(0x1010); + assert!( + memory_terms.is_empty(), + "materially conflicting target-local memory terms must refuse narrowing" + ); + } + + #[test] + fn weaker_secondary_target_source_does_not_seed_narrowing() { + let blocks = make_residual_precondition_blocks(); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); + let ctx = Context::thread_local(); + let explorer = SymQueryConfig::default().make_explorer(&ctx); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("reg:56_0", 64); + state.new_symbolic_input("sym_mem", 8); + + let exact = SemanticEvidence::exact(); + let likely = SemanticEvidence::likely(SemanticEvidenceReason::PartialPathCoverage); + let artifact = test_semantic_artifact( + RefinementStage::Compiled, + SliceClass::Worker, + Vec::new(), + vec![ + { + let mut region = make_region(0x1000, &[0x1010, 0x1004]); + region.control.push(Judged::new( + ControlFact { + target: 0x1010, + status: crate::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), condition: Some("guard".to_string()), compiled: Some(BackwardConditionSummary { simplified: "guard".to_string(), @@ -1680,39 +2191,49 @@ mod tests { supported_paths: 1, total_paths: 1, }), - evidence: SemanticEvidence::exact(), }, - SymbolicControlFact { - target: 0x1004, - status: SymbolicReachabilityStatus::Unreachable, - condition: None, - compiled: None, - evidence: SemanticEvidence::exact(), + exact.clone(), + )); + region + }, + { + let mut region = make_region(0x1004, &[0x1010, 0x1008]); + region.control.push(Judged::new( + ControlFact { + target: 0x1010, + status: crate::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + condition: Some("worker_guard".to_string()), + compiled: Some(BackwardConditionSummary { + simplified: "worker_guard".to_string(), + terms: vec!["worker_guard".to_string()], + memory_terms: vec![BackwardMemoryCondition { + region: BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: likely.clone(), + binding: Some("sym_mem".to_string()), + expr: "0x2a".to_string(), + value_expr: Some("0x2a".to_string()), + exact_value: true, + }], + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::OverApprox, + supported_paths: 1, + total_paths: 2, + }), }, - ], - memory_terms: vec![BackwardMemoryCondition { - region: BackwardMemoryRegion::Argument { index: 0 }, - offset_lo: 0, - offset_hi: 0, - size: 1, - exact_offset: true, - evidence: SemanticEvidence::exact(), - binding: Some("reg:56_0".to_string()), - expr: "0x1".to_string(), - value_expr: Some("0x1".to_string()), - exact_value: true, - }], - evidence: SemanticEvidence::exact(), - }], - control_islands: Vec::new(), - memory_islands: Vec::new(), - diagnostics: SymbolicFunctionFactDiagnostics::default(), - }, - interpreter: None, - vm_step: None, - vm_transfer: None, - cache_hit: false, - }; + likely.clone(), + )); + region + }, + ], + default_diagnostics(), + ); match apply_best_compiled_precondition(&explorer, &func, Some(&artifact), state, 0x1010) { PreconditionApplication::Continue { @@ -1720,16 +2241,52 @@ mod tests { narrowed_state, compiled_precondition, } => { - assert!(initial_state.num_constraints() >= original_constraints); - assert!(narrowed_state.is_none()); - assert!(compiled_precondition.is_some()); + assert!(compiled_precondition.is_none()); + assert!( + narrowed_state.is_none(), + "conflicting source regions must refuse targeted narrowing" + ); + let sym_mem = initial_state + .symbolic_inputs() + .get("sym_mem") + .expect("symbolic input") + .to_bv(&ctx); + assert!(matches!( + explorer + .solver() + .sat_with_constraint(&initial_state, &sym_mem.eq(BV::from_u64(0x2a, 8))), + SatResult::Sat + )); + assert!(matches!( + explorer + .solver() + .sat_with_constraint(&initial_state, &sym_mem.eq(BV::from_u64(0x2b, 8))), + SatResult::Sat + )); } PreconditionApplication::ExactUnsat { .. } => { - panic!("necessary worker islands should use the real compiled precondition path") + panic!("secondary source disagreement should not shortcut as exact unsat") } } } + #[test] + fn vm_artifacts_preserve_interpreter_slice_class() { + let artifact = SemanticArtifact { + stage: RefinementStage::Compiled, + granularity: ArtifactGranularity::SummaryOnly, + execution: ExecutionModel::Vm, + body: SemanticArtifactBody::Vm(Box::new(crate::VmArtifactBody { + interpreter: None, + step_summary: Some(make_vm_step_summary_with_transfers(Vec::new())), + transfer_summary: None, + })), + diagnostics: default_diagnostics(), + }; + + assert_eq!(artifact.slice_class(), Some(SliceClass::InterpreterSwitch)); + } + #[test] fn vm_target_case_values_include_redispatch_cases() { let vm_step = make_vm_step_summary_with_transfers(vec![ diff --git a/crates/r2sym/src/semantics/artifact.rs b/crates/r2sym/src/semantics/artifact.rs index 21132f3..de3b2e0 100644 --- a/crates/r2sym/src/semantics/artifact.rs +++ b/crates/r2sym/src/semantics/artifact.rs @@ -1,22 +1,15 @@ use serde::{Deserialize, Serialize}; -use crate::sim::DerivedSummaryDiagnostics; - -use crate::backward::BackwardMemoryCondition; - -use super::facts::{SymbolicFunctionFacts, SymbolicTargetConditionSource, SymbolicWorkerIsland}; -use super::vm::{InterpreterDispatchSummary, VmStepSummary}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub enum SemanticMode { - Raw, - Compiled, - IslandCompiled, - Residual, - VmSummary, -} - -pub type CompiledSemanticMode = SemanticMode; +use super::plan::{ + ArtifactBuildPlan, DecompilePlan, QueryPlan, TargetQueryPlan, TargetQueryRoutePlan, TypePlan, + derive_artifact_build_plan, derive_decompile_plan, derive_query_plan, derive_target_query_plan, + derive_target_query_route_plan, derive_type_plan, +}; +use super::region::{ + ArtifactGranularity, ExecutionModel, NativeArtifactBody, RefinementStage, + SemanticArtifactDiagnostics, SemanticRegion, SemanticTargetConditionSource, VmArtifactBody, +}; +use super::vm::InterpreterKind; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum SliceClass { @@ -28,7 +21,7 @@ pub enum SliceClass { GenericLarge, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub enum ResidualReason { MissingArch, LargeCfg, @@ -58,6 +51,7 @@ impl SemanticConfidence { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum SemanticEvidenceSoundness { Proven, + UnderApprox, OverApprox, Ranked, Unknown, @@ -70,7 +64,7 @@ pub enum SemanticEvidenceCoverage { Bounded, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub enum SemanticEvidenceProvenance { Stable, Normalized, @@ -78,7 +72,7 @@ pub enum SemanticEvidenceProvenance { Unstable, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub enum SemanticEvidenceAmbiguity { Single, Bounded, @@ -86,11 +80,12 @@ pub enum SemanticEvidenceAmbiguity { Multiple, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub enum SemanticEvidenceReason { LargeCfg, SummaryBudget, AliasAmbiguity, + ConflictingTargetSources, ReplayOverlap, HeapIdentityWeak, GuardOpaque, @@ -200,90 +195,605 @@ impl SemanticEvidence { pub fn is_reliable(&self) -> bool { self.tier.is_reliable() + && matches!( + self.soundness, + SemanticEvidenceSoundness::Proven | SemanticEvidenceSoundness::OverApprox + ) + && !matches!(self.ambiguity, SemanticEvidenceAmbiguity::Multiple) } pub fn is_usable(&self) -> bool { - self.tier.is_usable() + self.tier.is_usable() && !matches!(self.soundness, SemanticEvidenceSoundness::Unknown) } pub fn allows_hard_proof(&self) -> bool { matches!(self.tier, SemanticConfidence::Exact) + && matches!(self.soundness, SemanticEvidenceSoundness::Proven) + && matches!(self.coverage, SemanticEvidenceCoverage::Full) + && !matches!(self.ambiguity, SemanticEvidenceAmbiguity::Multiple) } pub fn allows_narrowing(&self) -> bool { matches!( self.tier, SemanticConfidence::Exact | SemanticConfidence::Likely - ) + ) && matches!( + self.soundness, + SemanticEvidenceSoundness::Proven | SemanticEvidenceSoundness::OverApprox + ) && !matches!(self.coverage, SemanticEvidenceCoverage::Partial) + && !matches!(self.ambiguity, SemanticEvidenceAmbiguity::Multiple) + } + + pub fn allows_guarded_structuring(&self) -> bool { + matches!( + self.tier, + SemanticConfidence::Exact | SemanticConfidence::Likely + ) && matches!( + self.soundness, + SemanticEvidenceSoundness::Proven + | SemanticEvidenceSoundness::UnderApprox + | SemanticEvidenceSoundness::OverApprox + ) && matches!( + self.coverage, + SemanticEvidenceCoverage::Full | SemanticEvidenceCoverage::Bounded + ) && !matches!(self.ambiguity, SemanticEvidenceAmbiguity::Multiple) + && !self + .reasons + .contains(&SemanticEvidenceReason::ConflictingTargetSources) } pub fn allows_ranking(&self) -> bool { !matches!(self.tier, SemanticConfidence::Residual) + && !matches!(self.soundness, SemanticEvidenceSoundness::Unknown) + } + + pub fn combined_with(&self, other: &Self) -> Self { + let mut reasons = self.reasons.clone(); + reasons.extend(other.reasons.iter().copied()); + reasons.sort_unstable(); + reasons.dedup(); + Self { + tier: weaker_confidence(self.tier, other.tier), + soundness: weaker_soundness(self.soundness, other.soundness), + coverage: weaker_coverage(self.coverage, other.coverage), + provenance: weaker_provenance(self.provenance, other.provenance), + ambiguity: weaker_ambiguity(self.ambiguity, other.ambiguity), + budget_limited: self.budget_limited || other.budget_limited, + reasons, + } + } + + pub fn downgraded_for_conflict(&self) -> Self { + self.combined_with( + &Self::residual(SemanticEvidenceReason::ConflictingTargetSources) + .with_budget_limited(self.budget_limited), + ) + } + + pub fn refused(reason: SemanticEvidenceReason) -> Self { + Self::residual(reason) + } + + pub fn strength_rank(&self) -> (u8, u8, u8, u8, u8, u8) { + let tier_rank = match self.tier { + SemanticConfidence::Exact => 4, + SemanticConfidence::Likely => 3, + SemanticConfidence::Heuristic => 2, + SemanticConfidence::Residual => 1, + }; + let soundness_rank = match self.soundness { + SemanticEvidenceSoundness::Proven => 5, + SemanticEvidenceSoundness::UnderApprox => 4, + SemanticEvidenceSoundness::OverApprox => 3, + SemanticEvidenceSoundness::Ranked => 2, + SemanticEvidenceSoundness::Unknown => 1, + }; + let coverage_rank = match self.coverage { + SemanticEvidenceCoverage::Full => 3, + SemanticEvidenceCoverage::Bounded => 2, + SemanticEvidenceCoverage::Partial => 1, + }; + let provenance_rank = match self.provenance { + SemanticEvidenceProvenance::Stable => 4, + SemanticEvidenceProvenance::Normalized => 3, + SemanticEvidenceProvenance::Ranked => 2, + SemanticEvidenceProvenance::Unstable => 1, + }; + let ambiguity_rank = match self.ambiguity { + SemanticEvidenceAmbiguity::Single => 4, + SemanticEvidenceAmbiguity::Bounded => 3, + SemanticEvidenceAmbiguity::Ranked => 2, + SemanticEvidenceAmbiguity::Multiple => 1, + }; + ( + self.allows_hard_proof() as u8, + self.allows_narrowing() as u8, + soundness_rank, + tier_rank, + coverage_rank, + provenance_rank * 4 + ambiguity_rank - u8::from(self.budget_limited), + ) + } +} + +fn weaker_confidence(left: SemanticConfidence, right: SemanticConfidence) -> SemanticConfidence { + match (left, right) { + (SemanticConfidence::Residual, _) | (_, SemanticConfidence::Residual) => { + SemanticConfidence::Residual + } + (SemanticConfidence::Heuristic, _) | (_, SemanticConfidence::Heuristic) => { + SemanticConfidence::Heuristic + } + (SemanticConfidence::Likely, _) | (_, SemanticConfidence::Likely) => { + SemanticConfidence::Likely + } + _ => SemanticConfidence::Exact, + } +} + +fn weaker_soundness( + left: SemanticEvidenceSoundness, + right: SemanticEvidenceSoundness, +) -> SemanticEvidenceSoundness { + use SemanticEvidenceSoundness as Soundness; + match (left, right) { + (Soundness::Unknown, _) | (_, Soundness::Unknown) => Soundness::Unknown, + (Soundness::Ranked, _) | (_, Soundness::Ranked) => Soundness::Ranked, + (Soundness::OverApprox, Soundness::UnderApprox) + | (Soundness::UnderApprox, Soundness::OverApprox) => Soundness::Ranked, + (Soundness::OverApprox, _) | (_, Soundness::OverApprox) => Soundness::OverApprox, + (Soundness::UnderApprox, _) | (_, Soundness::UnderApprox) => Soundness::UnderApprox, + _ => Soundness::Proven, + } +} + +fn weaker_coverage( + left: SemanticEvidenceCoverage, + right: SemanticEvidenceCoverage, +) -> SemanticEvidenceCoverage { + use SemanticEvidenceCoverage as Coverage; + match (left, right) { + (Coverage::Partial, _) | (_, Coverage::Partial) => Coverage::Partial, + (Coverage::Bounded, _) | (_, Coverage::Bounded) => Coverage::Bounded, + _ => Coverage::Full, } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub struct SemanticCapability { - pub query_ready: bool, - pub type_ready: bool, - pub decompile_ready: bool, +fn weaker_provenance( + left: SemanticEvidenceProvenance, + right: SemanticEvidenceProvenance, +) -> SemanticEvidenceProvenance { + left.max(right) +} + +fn weaker_ambiguity( + left: SemanticEvidenceAmbiguity, + right: SemanticEvidenceAmbiguity, +) -> SemanticEvidenceAmbiguity { + left.max(right) +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum SemanticArtifactBody { + Native(NativeArtifactBody), + Vm(Box), } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CompiledSemanticArtifact { - pub mode: SemanticMode, - pub slice_class: SliceClass, - pub capability: SemanticCapability, - pub residual_reasons: Vec, - pub closure_functions: usize, - pub helper_functions: usize, - pub derived_summaries: usize, - pub derived_diagnostics: DerivedSummaryDiagnostics, - pub symbolic_facts: SymbolicFunctionFacts, - pub interpreter: Option, - pub vm_step: Option, - pub vm_transfer: Option, - pub cache_hit: bool, +pub struct SemanticArtifact { + pub stage: RefinementStage, + pub granularity: ArtifactGranularity, + pub execution: ExecutionModel, + pub body: SemanticArtifactBody, + pub diagnostics: SemanticArtifactDiagnostics, } -impl CompiledSemanticArtifact { - pub fn actionable_condition_source_for_target( +impl SemanticArtifact { + pub fn normalized(mut self) -> Self { + self.diagnostics = self.diagnostics.normalized(); + self + } + + fn has_native_semantics(&self) -> bool { + self.native_body() + .is_some_and(|body| !body.regions.is_empty()) + } + + fn has_query_support(&self) -> bool { + self.native_body() + .is_some_and(NativeArtifactBody::supports_query_guidance) + } + + pub fn slice_class(&self) -> Option { + match &self.body { + SemanticArtifactBody::Native(body) => Some(body.summary.slice_class), + SemanticArtifactBody::Vm(body) => body + .step_summary + .as_ref() + .map(|summary| slice_class_for_interpreter_kind(summary.kind)) + .or_else(|| { + body.transfer_summary + .as_ref() + .map(|summary| slice_class_for_interpreter_kind(summary.kind)) + }) + .or_else(|| { + body.interpreter + .as_ref() + .map(|summary| slice_class_for_interpreter_kind(summary.kind)) + }), + } + } + + pub fn build_plan(&self) -> ArtifactBuildPlan { + derive_artifact_build_plan(self.stage, &self.diagnostics) + } + + pub fn query_plan(&self) -> QueryPlan { + derive_query_plan( + self.stage, + self.execution, + &self.diagnostics, + self.has_query_support(), + ) + } + + pub fn target_query_plan(&self, target_addr: u64) -> TargetQueryPlan { + let query_plan = self.query_plan(); + let has_guidance = self + .native_body() + .is_some_and(|body| body.has_target_guidance(target_addr, false)); + let necessary_for_target = self + .native_body() + .is_some_and(|body| body.target_guidance_is_necessary(target_addr, false)); + let has_source_conflict = self.target_has_ambiguous_sources(target_addr); + derive_target_query_plan( + &query_plan, + has_guidance, + has_source_conflict, + necessary_for_target, + ) + } + + pub fn target_query_route_plan(&self, target_addr: u64) -> TargetQueryRoutePlan { + let query_plan = self.query_plan(); + let target_plan = self.target_query_plan(target_addr); + let authoritative_region = self.authoritative_region_for_target(target_addr, false); + let authoritative_memory_region = self.authoritative_memory_region_for_target(target_addr); + let has_memory_guidance = authoritative_memory_region.is_some_and(|region| { + !region + .actionable_memory_terms_for_target(target_addr) + .is_empty() + }); + derive_target_query_route_plan( + &query_plan, + &target_plan, + authoritative_region.is_some() || authoritative_memory_region.is_some(), + has_memory_guidance, + ) + } + + pub fn type_plan(&self) -> TypePlan { + derive_type_plan( + self.stage, + self.execution, + &self.diagnostics, + self.has_native_semantics(), + ) + } + + pub fn decompile_plan(&self) -> DecompilePlan { + derive_decompile_plan( + self.stage, + self.execution, + &self.diagnostics, + self.has_native_semantics(), + ) + } + + pub fn native_body(&self) -> Option<&NativeArtifactBody> { + match &self.body { + SemanticArtifactBody::Native(body) => Some(body), + SemanticArtifactBody::Vm(_) => None, + } + } + + pub fn vm_body(&self) -> Option<&VmArtifactBody> { + match &self.body { + SemanticArtifactBody::Native(_) => None, + SemanticArtifactBody::Vm(body) => Some(body.as_ref()), + } + } + + pub fn exact_reachable_target_for_block(&self, block_addr: u64) -> Option { + self.native_body()? + .exact_reachable_target_for_block(block_addr) + } + + pub fn actionable_reachable_target_for_block(&self, block_addr: u64) -> Option { + self.native_body()? + .actionable_reachable_target_for_block(block_addr) + } + + pub fn exact_branch_truth_for_block(&self, block_addr: u64) -> Option { + self.native_body()?.exact_branch_truth_for_block(block_addr) + } + + pub fn exact_compiled_condition_for_block( + &self, + block_addr: u64, + ) -> Option<&crate::backward::BackwardConditionSummary> { + self.native_body()? + .exact_compiled_condition_for_block(block_addr) + } + + pub fn actionable_compiled_condition_for_block( + &self, + block_addr: u64, + ) -> Option<&crate::backward::BackwardConditionSummary> { + self.native_body()? + .actionable_compiled_condition_for_block(block_addr) + } + + pub fn actionable_memory_terms_for_block( + &self, + block_addr: u64, + ) -> Vec<&crate::backward::BackwardMemoryCondition> { + self.native_body() + .map(|body| body.actionable_memory_terms_for_block(block_addr)) + .unwrap_or_default() + } + + pub fn exact_control_count(&self) -> usize { + self.native_body() + .map(NativeArtifactBody::exact_control_count) + .unwrap_or_default() + } + + pub fn actionable_control_count(&self) -> usize { + self.native_body() + .map(NativeArtifactBody::actionable_control_count) + .unwrap_or_default() + } + + pub fn actionable_regions(&self) -> Vec<&SemanticRegion> { + self.native_body() + .map(|body| body.actionable_regions().collect()) + .unwrap_or_default() + } + + pub fn target_condition_source( + &self, + target_addr: u64, + proof_only: bool, + ) -> Option> { + self.native_body()? + .target_condition_source(target_addr, proof_only) + } + + pub fn actionable_compiled_condition_for_target( &self, target_addr: u64, - ) -> Option> { - self.symbolic_facts - .actionable_condition_source_for_target(target_addr) + ) -> Option<&crate::backward::BackwardConditionSummary> { + self.native_body()? + .actionable_compiled_condition_for_target(target_addr) } - pub fn exact_condition_source_for_target( + pub fn exact_compiled_condition_for_target( &self, target_addr: u64, - ) -> Option> { - self.symbolic_facts - .exact_condition_source_for_target(target_addr) + ) -> Option<&crate::backward::BackwardConditionSummary> { + self.native_body()? + .exact_compiled_condition_for_target(target_addr) } pub fn actionable_memory_terms_for_target( &self, target_addr: u64, - ) -> Vec<&BackwardMemoryCondition> { - self.symbolic_facts - .actionable_memory_terms_for_target(target_addr) + ) -> Vec<&crate::backward::BackwardMemoryCondition> { + self.native_body() + .map(|body| body.actionable_memory_terms_for_target(target_addr)) + .unwrap_or_default() } - pub fn exact_memory_terms_for_target(&self, target_addr: u64) -> Vec<&BackwardMemoryCondition> { - self.symbolic_facts - .exact_memory_terms_for_target(target_addr) + pub fn authoritative_memory_region_for_target( + &self, + target_addr: u64, + ) -> Option<&SemanticRegion> { + self.native_body()? + .authoritative_memory_region_for_target(target_addr) } - pub fn best_worker_island_for_target( + pub fn authoritative_region_for_target( &self, target_addr: u64, - hard_proof_only: bool, - ) -> Option<&SymbolicWorkerIsland> { - self.symbolic_facts - .best_worker_island_for_target(target_addr, hard_proof_only) + proof_only: bool, + ) -> Option<&SemanticRegion> { + self.native_body()? + .authoritative_region_for_target(target_addr, proof_only) + } + + pub fn ambiguous_targets(&self) -> Vec { + if !self.diagnostics.ambiguous_targets.is_empty() { + return self.diagnostics.ambiguous_targets.clone(); + } + self.native_body() + .map(|body| body.conflicting_targets(false).into_iter().collect()) + .unwrap_or_default() + } + + pub fn target_has_ambiguous_sources(&self, target_addr: u64) -> bool { + self.diagnostics.ambiguous_targets.contains(&target_addr) + || self + .native_body() + .is_some_and(|body| body.target_source_conflict(target_addr, false)) + } + + pub fn vm_step_for_dispatch_header( + &self, + dispatch_header: u64, + ) -> Option<&crate::VmStepSummary> { + self.vm_body()? + .step_summary + .as_ref() + .filter(|summary| summary.dispatch_header == dispatch_header) + } + + pub fn vm_transfer_for_dispatch_header( + &self, + dispatch_header: u64, + ) -> Option<&crate::VmStepSummary> { + self.vm_body()? + .transfer_summary + .as_ref() + .filter(|summary| summary.dispatch_header == dispatch_header) + } + + pub fn supports_guarded_structuring(&self) -> bool { + self.native_body() + .is_some_and(NativeArtifactBody::supports_guarded_structuring) + } +} + +fn slice_class_for_interpreter_kind(kind: InterpreterKind) -> SliceClass { + match kind { + InterpreterKind::SwitchDispatch => SliceClass::InterpreterSwitch, + InterpreterKind::IndirectDispatch => SliceClass::InterpreterIndirect, } } -pub type CompiledFunctionSemantics = CompiledSemanticArtifact; +impl SemanticArtifactDiagnostics { + pub fn normalized(mut self) -> Self { + self.residual_reasons.sort_unstable(); + self.residual_reasons.dedup(); + self.ambiguous_targets.sort_unstable(); + self.ambiguous_targets.dedup(); + self + } +} + +#[cfg(test)] +mod tests { + use proptest::prelude::*; + + use super::{ + ResidualReason, SemanticArtifact, SemanticArtifactBody, SemanticArtifactDiagnostics, + SemanticConfidence, SemanticEvidence, SemanticEvidenceAmbiguity, SemanticEvidenceCoverage, + SemanticEvidenceProvenance, SemanticEvidenceReason, SemanticEvidenceSoundness, SliceClass, + }; + use crate::sim::DerivedSummaryDiagnostics; + use crate::{ + ArtifactGranularity, ExecutionModel, NativeArtifactBody, NativeFunctionSummary, + RefinementStage, + }; + + fn native_artifact(diagnostics: SemanticArtifactDiagnostics) -> SemanticArtifact { + SemanticArtifact { + stage: RefinementStage::Compiled, + granularity: ArtifactGranularity::WholeFunction, + execution: ExecutionModel::Native, + body: SemanticArtifactBody::Native(NativeArtifactBody { + summary: NativeFunctionSummary { + slice_class: SliceClass::Worker, + closure_functions: 0, + helper_functions: 0, + derived_summaries: 0, + derived_diagnostics: DerivedSummaryDiagnostics::default(), + }, + regions: Default::default(), + }), + diagnostics, + } + } + + fn evidence_from_seed(seed: u8) -> SemanticEvidence { + let tier = match seed % 4 { + 0 => SemanticConfidence::Exact, + 1 => SemanticConfidence::Likely, + 2 => SemanticConfidence::Heuristic, + _ => SemanticConfidence::Residual, + }; + let soundness = match (seed / 4) % 5 { + 0 => SemanticEvidenceSoundness::Proven, + 1 => SemanticEvidenceSoundness::UnderApprox, + 2 => SemanticEvidenceSoundness::OverApprox, + 3 => SemanticEvidenceSoundness::Ranked, + _ => SemanticEvidenceSoundness::Unknown, + }; + let coverage = match (seed / 20) % 3 { + 0 => SemanticEvidenceCoverage::Full, + 1 => SemanticEvidenceCoverage::Bounded, + _ => SemanticEvidenceCoverage::Partial, + }; + let provenance = match (seed / 60) % 4 { + 0 => SemanticEvidenceProvenance::Stable, + 1 => SemanticEvidenceProvenance::Normalized, + 2 => SemanticEvidenceProvenance::Ranked, + _ => SemanticEvidenceProvenance::Unstable, + }; + let ambiguity = match (seed / 120) % 4 { + 0 => SemanticEvidenceAmbiguity::Single, + 1 => SemanticEvidenceAmbiguity::Bounded, + 2 => SemanticEvidenceAmbiguity::Ranked, + _ => SemanticEvidenceAmbiguity::Multiple, + }; + SemanticEvidence { + tier, + soundness, + coverage, + provenance, + ambiguity, + budget_limited: seed.is_multiple_of(2), + reasons: vec![ + SemanticEvidenceReason::LargeCfg, + SemanticEvidenceReason::AliasAmbiguity, + ], + } + } + + proptest! { + #[test] + fn evidence_combine_is_monotone(left in any::(), right in any::()) { + let left = evidence_from_seed(left); + let right = evidence_from_seed(right); + let combined = left.combined_with(&right); + prop_assert!(combined.strength_rank() <= left.strength_rank()); + prop_assert!(combined.strength_rank() <= right.strength_rank()); + } + + #[test] + fn artifact_normalization_is_idempotent( + residuals in proptest::collection::vec(prop_oneof![ + Just(ResidualReason::MissingArch), + Just(ResidualReason::LargeCfg), + Just(ResidualReason::SummaryBudgetExhausted), + Just(ResidualReason::SccBudgetExhausted), + Just(ResidualReason::InterpreterRequiresStepSummary), + ], 0..8), + ambiguous in proptest::collection::vec(any::(), 0..8), + ) { + let artifact = native_artifact(SemanticArtifactDiagnostics { + branches_evaluated: 0, + branches_pruned: 0, + branches_unknown: 0, + skipped_missing_arch: false, + skipped_large_cfg: false, + residual_reasons: residuals, + ambiguous_targets: ambiguous, + cache_hit: false, + }); + let normalized = artifact.clone().normalized(); + prop_assert_eq!(normalized.clone().normalized(), normalized); + } + } + + #[test] + fn conflict_downgrade_refuses_narrowing() { + let evidence = SemanticEvidence::exact().downgraded_for_conflict(); + assert!(!evidence.allows_narrowing()); + assert!( + evidence + .reasons + .contains(&SemanticEvidenceReason::ConflictingTargetSources) + ); + } +} diff --git a/crates/r2sym/src/semantics/cache.rs b/crates/r2sym/src/semantics/cache.rs index 7b25309..f7ff3e9 100644 --- a/crates/r2sym/src/semantics/cache.rs +++ b/crates/r2sym/src/semantics/cache.rs @@ -8,9 +8,10 @@ use r2ssa::{SSAOp, SsaArtifact}; use crate::sim::{PreparedFunctionScope, SummaryProfile}; -use super::artifact::CompiledSemanticArtifact; +use super::artifact::SemanticArtifact; const SEMANTIC_CACHE_LIMIT: usize = 128; +pub const SEMANTIC_ARTIFACT_SCHEMA_VERSION: u32 = 2; #[allow(dead_code)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -27,6 +28,7 @@ pub(crate) enum SemanticCacheScopeKind { #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub(crate) struct SemanticCacheKey { + pub schema_version: u32, pub root_addr: u64, pub scope_hash: u64, pub arch_hash: u64, @@ -37,7 +39,7 @@ pub(crate) struct SemanticCacheKey { #[derive(Debug, Clone)] pub(crate) struct SemanticCompilationResult { - pub artifact: Arc, + pub artifact: Arc, pub cache_hit: bool, } @@ -143,6 +145,7 @@ pub(crate) fn semantic_cache_key( seed_mode: SemanticSeedMode, ) -> SemanticCacheKey { SemanticCacheKey { + schema_version: SEMANTIC_ARTIFACT_SCHEMA_VERSION, root_addr: func.entry, scope_hash: if scope.is_some() { stable_scope_hash(scope) @@ -163,6 +166,7 @@ pub(crate) fn coarse_large_slice_cache_key( seed_mode: SemanticSeedMode, ) -> SemanticCacheKey { SemanticCacheKey { + schema_version: SEMANTIC_ARTIFACT_SCHEMA_VERSION, root_addr, scope_hash: 0, arch_hash: arch_hash(arch), @@ -172,15 +176,13 @@ pub(crate) fn coarse_large_slice_cache_key( } } -fn semantic_cache() -> &'static RwLock>> { - static CACHE: OnceLock>>> = +fn semantic_cache() -> &'static RwLock>> { + static CACHE: OnceLock>>> = OnceLock::new(); CACHE.get_or_init(|| RwLock::new(HashMap::new())) } -pub(crate) fn lookup_semantic_cache( - key: &SemanticCacheKey, -) -> Option> { +pub(crate) fn lookup_semantic_cache(key: &SemanticCacheKey) -> Option> { semantic_cache() .read() .expect("semantic cache read lock poisoned") @@ -190,8 +192,8 @@ pub(crate) fn lookup_semantic_cache( pub(crate) fn cache_insert_bounded( key: SemanticCacheKey, - value: Arc, -) -> Arc { + value: Arc, +) -> Arc { let mut guard = semantic_cache() .write() .expect("semantic cache write lock poisoned"); @@ -209,7 +211,10 @@ mod display_name_tests { use crate::sim::{PreparedFunctionScope, ScopedPreparedFunction}; - use super::stable_scope_hash; + use super::{ + SEMANTIC_ARTIFACT_SCHEMA_VERSION, SemanticCacheScopeKind, SemanticSeedMode, + coarse_large_slice_cache_key, semantic_cache_key, stable_scope_hash, + }; const RAX: u64 = 0; @@ -229,6 +234,46 @@ mod display_name_tests { } } + #[test] + fn semantic_cache_keys_use_explicit_schema_version() { + let func = SsaArtifact::for_symbolic( + &[R2ILBlock { + addr: 0x401000, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }], + Some(&test_arch()), + ) + .expect("ssa"); + let exact_key = semantic_cache_key( + &func, + None, + Some(&test_arch()), + crate::sim::SummaryProfile::Default, + SemanticSeedMode::Static, + ); + let coarse_key = coarse_large_slice_cache_key( + func.entry, + Some(&test_arch()), + crate::sim::SummaryProfile::Default, + SemanticSeedMode::Static, + ); + assert_eq!(exact_key.schema_version, SEMANTIC_ARTIFACT_SCHEMA_VERSION); + assert_eq!(coarse_key.schema_version, SEMANTIC_ARTIFACT_SCHEMA_VERSION); + assert!(matches!( + exact_key.scope_kind, + SemanticCacheScopeKind::Exact + )); + assert!(matches!( + coarse_key.scope_kind, + SemanticCacheScopeKind::CoarseLargeSlice + )); + } + fn simple_function(entry: u64) -> SsaArtifact { let blocks = vec![R2ILBlock { addr: entry, diff --git a/crates/r2sym/src/semantics/compiler.rs b/crates/r2sym/src/semantics/compiler.rs index c20205e..6b68272 100644 --- a/crates/r2sym/src/semantics/compiler.rs +++ b/crates/r2sym/src/semantics/compiler.rs @@ -9,33 +9,35 @@ use crate::sim::{ DerivedSummaryDiagnostics, PreparedFunctionScope, SummaryProfile, SummaryRegistry, }; -use super::artifact::{ - CompiledFunctionSemantics, CompiledSemanticArtifact, ResidualReason, SemanticCapability, - SemanticMode, -}; +use super::artifact::{ResidualReason, SemanticArtifact, SemanticArtifactBody}; use super::cache::{ SemanticCompilationResult, SemanticSeedMode, cache_insert_bounded, coarse_large_slice_cache_key, lookup_semantic_cache, semantic_cache_key, }; use super::classify::classify_slice; use super::facts::{ - SymbolicFunctionFacts, collect_large_cfg_symbolic_function_facts_with_limit, - collect_symbolic_function_facts_with_derived, - collect_symbolic_function_facts_with_scope_and_profile, + CollectedNativeSemanticRegions, SymbolicFunctionFactDiagnostics, + collect_canonical_semantic_regions_with_derived, + collect_canonical_semantic_regions_with_scope_and_profile, + collect_large_cfg_canonical_semantic_regions_with_limit, +}; +use super::region::{ + ArtifactGranularity, ExecutionModel, NativeArtifactBody, NativeFunctionSummary, + RefinementStage, SemanticArtifactDiagnostics, }; use super::vm::{build_vm_step_summary, classify_interpreter_like}; fn residual_reasons( diagnostics: &DerivedSummaryDiagnostics, - facts: &SymbolicFunctionFacts, + fact_diagnostics: &SymbolicFunctionFactDiagnostics, interpreter_detected: bool, vm_step_ready: bool, ) -> Vec { let mut reasons = Vec::new(); - if facts.diagnostics.skipped_missing_arch { + if fact_diagnostics.skipped_missing_arch { reasons.push(ResidualReason::MissingArch); } - if facts.diagnostics.skipped_large_cfg { + if fact_diagnostics.skipped_large_cfg { reasons.push(ResidualReason::LargeCfg); } if diagnostics.budget_exhausted > 0 { @@ -51,10 +53,10 @@ fn residual_reasons( } fn normalized_residual_reasons( - mode: SemanticMode, + suppress_large_cfg_reason: bool, reasons: Vec, ) -> Vec { - if matches!(mode, SemanticMode::IslandCompiled | SemanticMode::VmSummary) { + if suppress_large_cfg_reason { reasons .into_iter() .filter(|reason| !matches!(reason, ResidualReason::LargeCfg)) @@ -64,133 +66,145 @@ fn normalized_residual_reasons( } } -fn semantic_mode_for( +fn semantic_stage_for( helper_functions: usize, derived_summaries: usize, diagnostics: &DerivedSummaryDiagnostics, - facts: &SymbolicFunctionFacts, + collected: &CollectedNativeSemanticRegions, interpreter_detected: bool, vm_step_ready: bool, -) -> SemanticMode { +) -> RefinementStage { if vm_step_ready { - return SemanticMode::VmSummary; + return RefinementStage::Compiled; } if interpreter_detected { - return SemanticMode::Residual; + return RefinementStage::Residual; } - if derived_summaries > 0 && !facts.diagnostics.skipped_large_cfg { - return SemanticMode::Compiled; + if derived_summaries > 0 && !collected.diagnostics.skipped_large_cfg { + return RefinementStage::Compiled; } - if facts.diagnostics.skipped_large_cfg && has_island_compiled_semantics(facts) { - return SemanticMode::IslandCompiled; + if collected.diagnostics.skipped_large_cfg && has_island_compiled_semantics(collected) { + return RefinementStage::Compiled; } if helper_functions > 0 - || facts.diagnostics.skipped_large_cfg + || collected.diagnostics.skipped_large_cfg || diagnostics.budget_exhausted > 0 || diagnostics.scc_budget_exhausted > 0 { - return SemanticMode::Residual; + return RefinementStage::Residual; } - if facts.diagnostics.skipped_missing_arch { - return SemanticMode::Residual; + if collected.diagnostics.skipped_missing_arch { + return RefinementStage::Residual; } - SemanticMode::Raw + RefinementStage::Raw } -fn worker_island_supports_actionable_semantics( - island: &super::facts::SymbolicWorkerIsland, -) -> bool { - let supporting_condition = worker_island_supporting_compiled_condition(island); - let has_unique_target = - island.exact_reachable_target().is_some() || island.actionable_reachable_target().is_some(); - let has_condition = supporting_condition.is_some(); - let has_memory_support = !island.actionable_memory_terms().is_empty() - || supporting_condition.is_some_and(|compiled| !compiled.memory_terms.is_empty()); - island.evidence.allows_narrowing() && has_unique_target && has_condition && has_memory_support +fn has_island_compiled_semantics(collected: &CollectedNativeSemanticRegions) -> bool { + collected + .regions + .values() + .any(|region| region.supports_guarded_structuring()) } -fn worker_island_supports_structured_semantics( - island: &super::facts::SymbolicWorkerIsland, -) -> bool { - let supporting_condition = worker_island_supporting_compiled_condition(island); - worker_island_supports_actionable_semantics(island) - && supporting_condition.is_some_and(|compiled| { - !matches!( - compiled.precision, - crate::backward::BackwardConditionPrecision::ResidualSearchRequired - ) - }) -} - -fn worker_island_supporting_compiled_condition( - island: &super::facts::SymbolicWorkerIsland, -) -> Option<&crate::backward::BackwardConditionSummary> { - let reachable_target = island - .exact_reachable_target() - .or_else(|| island.actionable_reachable_target())?; - island - .control_facts - .iter() - .find(|fact| fact.target == reachable_target) - .and_then(super::facts::SymbolicControlFact::actionable_compiled_condition) -} - -fn has_island_compiled_semantics(facts: &SymbolicFunctionFacts) -> bool { - facts - .worker_islands - .iter() - .any(worker_island_supports_actionable_semantics) - || facts.branch_facts.iter().any(|fact| { - fact.actionable_compiled_condition().is_some() - && (fact.exact_reachable_target().is_some() - || fact.actionable_reachable_target().is_some()) - && (!facts - .actionable_memory_terms_for_block(fact.block_addr) - .is_empty() - || fact - .actionable_compiled_condition() - .is_some_and(|compiled| !compiled.memory_terms.is_empty())) - }) +fn semantic_granularity_for( + stage: RefinementStage, + execution: ExecutionModel, + regions: &std::collections::BTreeMap, + has_island_compiled_regions: bool, +) -> ArtifactGranularity { + if matches!(execution, ExecutionModel::Vm) { + return ArtifactGranularity::SummaryOnly; + } + if has_island_compiled_regions { + return ArtifactGranularity::Regioned; + } + if matches!(stage, RefinementStage::Residual) && !regions.is_empty() { + ArtifactGranularity::Regioned + } else { + ArtifactGranularity::WholeFunction + } } -fn has_structured_worker_semantics(facts: &SymbolicFunctionFacts) -> bool { - facts - .worker_islands - .iter() - .any(worker_island_supports_structured_semantics) +struct BuildSemanticArtifactInput { + stage: RefinementStage, + granularity: ArtifactGranularity, + execution: ExecutionModel, + suppress_large_cfg_reason: bool, + slice_class: crate::SliceClass, + closure_functions: usize, + helper_functions: usize, + derived_summaries: usize, + derived_diagnostics: DerivedSummaryDiagnostics, + collected: CollectedNativeSemanticRegions, + interpreter: Option, + vm_step: Option, + vm_transfer: Option, } -fn semantic_capability(mode: SemanticMode, facts: &SymbolicFunctionFacts) -> SemanticCapability { - let has_bounded_branch_facts = !facts.branch_facts.is_empty(); - let large_cfg_semantic_decompile = - facts.diagnostics.skipped_large_cfg && has_island_compiled_semantics(facts); - let large_cfg_structured_decompile = - facts.diagnostics.skipped_large_cfg && has_structured_worker_semantics(facts); - match mode { - SemanticMode::Raw | SemanticMode::Compiled => SemanticCapability { - query_ready: true, - type_ready: true, - decompile_ready: true, - }, - SemanticMode::IslandCompiled => SemanticCapability { - query_ready: true, - type_ready: true, - decompile_ready: large_cfg_semantic_decompile, - }, - SemanticMode::Residual => SemanticCapability { - query_ready: true, - type_ready: !facts.diagnostics.skipped_large_cfg - || has_bounded_branch_facts - || !facts.worker_islands.is_empty() - || !facts.memory_islands.is_empty(), - decompile_ready: large_cfg_structured_decompile, - }, - SemanticMode::VmSummary => SemanticCapability { - query_ready: true, - type_ready: true, - decompile_ready: false, +fn build_semantic_artifact(input: BuildSemanticArtifactInput) -> SemanticArtifact { + let BuildSemanticArtifactInput { + stage, + granularity, + execution, + suppress_large_cfg_reason, + slice_class, + closure_functions, + helper_functions, + derived_summaries, + derived_diagnostics, + collected, + interpreter, + vm_step, + vm_transfer, + } = input; + let interpreter_detected = interpreter.is_some(); + let vm_step_ready = vm_step.is_some(); + let body = match execution { + ExecutionModel::Vm => SemanticArtifactBody::Vm(Box::new(super::region::VmArtifactBody { + interpreter, + step_summary: vm_step, + transfer_summary: vm_transfer, + })), + ExecutionModel::Native => SemanticArtifactBody::Native(NativeArtifactBody { + summary: NativeFunctionSummary { + slice_class, + closure_functions, + helper_functions, + derived_summaries, + derived_diagnostics: derived_diagnostics.clone(), + }, + regions: collected.regions, + }), + }; + let ambiguous_targets = match &body { + SemanticArtifactBody::Native(body) => body.conflicting_targets(false).into_iter().collect(), + SemanticArtifactBody::Vm(_) => Vec::new(), + }; + SemanticArtifact { + stage, + granularity, + execution, + body, + diagnostics: SemanticArtifactDiagnostics { + branches_evaluated: collected.diagnostics.branches_evaluated, + branches_pruned: collected.diagnostics.branches_pruned, + branches_unknown: collected.diagnostics.branches_unknown, + skipped_missing_arch: collected.diagnostics.skipped_missing_arch, + skipped_large_cfg: collected.diagnostics.skipped_large_cfg, + residual_reasons: normalized_residual_reasons( + suppress_large_cfg_reason, + residual_reasons( + &derived_diagnostics, + &collected.diagnostics, + interpreter_detected, + vm_step_ready, + ), + ), + ambiguous_targets, + cache_hit: false, }, } + .normalized() } fn bounded_large_cfg_branch_limit(func: &SsaArtifact) -> usize { @@ -256,14 +270,14 @@ fn compile_function_semantics_uncached( arch: Option<&ArchSpec>, symbol_map: &HashMap, summary_profile: SummaryProfile, -) -> CompiledSemanticArtifact { +) -> SemanticArtifact { let closure_functions = scope.map(|scope| scope.functions().len()).unwrap_or(1); let helper_functions = scope .map(|scope| scope.helper_functions().count()) .unwrap_or(0); let mut derived_diagnostics = DerivedSummaryDiagnostics::default(); let mut derived_summaries = 0usize; - let mut symbolic_facts = SymbolicFunctionFacts::default(); + let mut collected = CollectedNativeSemanticRegions::default(); let interpreter = classify_interpreter_like(func); let vm_step = interpreter .as_ref() @@ -279,7 +293,7 @@ fn compile_function_semantics_uncached( if skip_expensive_branch_compilation { if interpreter.is_none() { let bounded_scope = bounded_large_cfg_scope(func, scope); - symbolic_facts = collect_large_cfg_symbolic_function_facts_with_limit( + collected = collect_large_cfg_canonical_semantic_regions_with_limit( ctx, func, bounded_scope.as_ref(), @@ -288,49 +302,54 @@ fn compile_function_semantics_uncached( summary_profile, bounded_large_cfg_branch_limit(func), ); - symbolic_facts.diagnostics.skipped_large_cfg = true; } else { - symbolic_facts.diagnostics.skipped_large_cfg = true; + collected.diagnostics.skipped_large_cfg = true; } - let mode = semantic_mode_for( + let execution = if interpreter.is_some() || vm_step.is_some() || vm_transfer.is_some() { + ExecutionModel::Vm + } else { + ExecutionModel::Native + }; + let stage = semantic_stage_for( helper_functions, derived_summaries, &derived_diagnostics, - &symbolic_facts, + &collected, interpreter.is_some(), vm_transfer.is_some(), ); + let has_island_compiled_regions = matches!(execution, ExecutionModel::Native) + && collected.diagnostics.skipped_large_cfg + && has_island_compiled_semantics(&collected); + let granularity = semantic_granularity_for( + stage, + execution, + &collected.regions, + has_island_compiled_regions, + ); let slice_class = classify_slice( func, helper_functions, &derived_diagnostics, interpreter.as_ref(), ); - - return CompiledSemanticArtifact { - mode, + return build_semantic_artifact(BuildSemanticArtifactInput { + stage, + granularity, + execution, + suppress_large_cfg_reason: matches!(execution, ExecutionModel::Vm) + || has_island_compiled_regions, slice_class, - capability: semantic_capability(mode, &symbolic_facts), - residual_reasons: normalized_residual_reasons( - mode, - residual_reasons( - &derived_diagnostics, - &symbolic_facts, - interpreter.is_some(), - vm_step.is_some(), - ), - ), closure_functions, helper_functions, derived_summaries, - derived_diagnostics, - symbolic_facts, - interpreter, - vm_step, - vm_transfer, - cache_hit: false, - }; + derived_diagnostics: derived_diagnostics.clone(), + collected, + interpreter: interpreter.clone(), + vm_step: vm_step.clone(), + vm_transfer: vm_transfer.clone(), + }); } if let Some(scope) = scope @@ -339,7 +358,7 @@ fn compile_function_semantics_uncached( let derived = registry.derive_symbolic_summaries(ctx, scope, Some(arch), symbol_map); derived_summaries = derived.summaries.len(); derived_diagnostics = derived.diagnostics.clone(); - symbolic_facts = collect_symbolic_function_facts_with_derived( + collected = collect_canonical_semantic_regions_with_derived( ctx, func, Some(scope), @@ -350,7 +369,7 @@ fn compile_function_semantics_uncached( &derived, ); } else { - symbolic_facts = collect_symbolic_function_facts_with_scope_and_profile( + collected = collect_canonical_semantic_regions_with_scope_and_profile( ctx, func, None, @@ -360,46 +379,52 @@ fn compile_function_semantics_uncached( ); } } else { - symbolic_facts.diagnostics.skipped_missing_arch = true; + collected.diagnostics.skipped_missing_arch = true; } - let mode = semantic_mode_for( + let execution = if interpreter.is_some() || vm_step.is_some() || vm_transfer.is_some() { + ExecutionModel::Vm + } else { + ExecutionModel::Native + }; + let stage = semantic_stage_for( helper_functions, derived_summaries, &derived_diagnostics, - &symbolic_facts, + &collected, interpreter.is_some(), vm_transfer.is_some(), ); + let has_island_compiled_regions = matches!(execution, ExecutionModel::Native) + && collected.diagnostics.skipped_large_cfg + && has_island_compiled_semantics(&collected); + let granularity = semantic_granularity_for( + stage, + execution, + &collected.regions, + has_island_compiled_regions, + ); let slice_class = classify_slice( func, helper_functions, &derived_diagnostics, interpreter.as_ref(), ); - - CompiledSemanticArtifact { - mode, + build_semantic_artifact(BuildSemanticArtifactInput { + stage, + granularity, + execution, + suppress_large_cfg_reason: matches!(execution, ExecutionModel::Vm) + || has_island_compiled_regions, slice_class, - capability: semantic_capability(mode, &symbolic_facts), - residual_reasons: normalized_residual_reasons( - mode, - residual_reasons( - &derived_diagnostics, - &symbolic_facts, - interpreter.is_some(), - vm_step.is_some(), - ), - ), closure_functions, helper_functions, derived_summaries, - derived_diagnostics, - symbolic_facts, + derived_diagnostics: derived_diagnostics.clone(), + collected, interpreter, vm_step, vm_transfer, - cache_hit: false, - } + }) } pub(crate) fn compile_function_semantics_cached_with_scope( @@ -439,8 +464,8 @@ pub(crate) fn compile_function_semantics_cached_with_scope( )); let artifact = cache_insert_bounded(key, artifact); let should_cache_coarsely = scope.is_some() - && (matches!(artifact.mode, SemanticMode::VmSummary) - || artifact.symbolic_facts.diagnostics.skipped_large_cfg); + && (matches!(artifact.execution, ExecutionModel::Vm) + || artifact.diagnostics.skipped_large_cfg); let artifact = if should_cache_coarsely { let coarse_key = coarse_key.expect("coarse large-slice cache key should exist when scope is present"); @@ -461,7 +486,7 @@ pub fn compile_function_semantics_with_scope( arch: Option<&ArchSpec>, symbol_map: &HashMap, summary_profile: SummaryProfile, -) -> CompiledFunctionSemantics { +) -> SemanticArtifact { let result = compile_function_semantics_cached_with_scope( ctx, func, @@ -471,7 +496,7 @@ pub fn compile_function_semantics_with_scope( summary_profile, ); let mut artifact = (*result.artifact).clone(); - artifact.cache_hit = result.cache_hit; + artifact.diagnostics.cache_hit = result.cache_hit; artifact } @@ -482,7 +507,7 @@ pub fn compile_semantic_artifact_with_scope( arch: Option<&ArchSpec>, symbol_map: &HashMap, summary_profile: SummaryProfile, -) -> CompiledSemanticArtifact { +) -> SemanticArtifact { compile_function_semantics_with_scope(ctx, func, scope, arch, symbol_map, summary_profile) } @@ -491,7 +516,7 @@ pub fn compile_semantic_artifact_default_with_scope( func: &SsaArtifact, scope: Option<&PreparedFunctionScope>, arch: Option<&ArchSpec>, -) -> CompiledSemanticArtifact { +) -> SemanticArtifact { let rebound_scope = scope.and_then(|scope| scope.with_prepared_root(func)); compile_semantic_artifact_with_scope( ctx, @@ -514,7 +539,6 @@ mod tests { use z3::Context; use super::*; - const RAX: u64 = 0; fn test_arch() -> ArchSpec { @@ -757,14 +781,19 @@ mod tests { &HashMap::new(), SummaryProfile::Default, ); - assert_eq!(artifact.mode, SemanticMode::VmSummary); + assert_eq!(artifact.execution, ExecutionModel::Vm); assert_eq!( - artifact.slice_class, - super::super::artifact::SliceClass::InterpreterSwitch + artifact.stage, + super::super::region::RefinementStage::Compiled ); - assert!(artifact.capability.query_ready); - assert!(artifact.interpreter.is_some()); - let vm_step = artifact.vm_step.expect("vm step"); + assert_eq!( + artifact.granularity, + super::super::region::ArtifactGranularity::SummaryOnly + ); + assert!(matches!(artifact.query_plan(), crate::QueryPlan::Ready)); + let vm_body = artifact.vm_body().expect("vm artifact body"); + assert!(vm_body.interpreter.is_some()); + let vm_step = vm_body.step_summary.as_ref().expect("vm step"); assert_eq!(vm_step.loop_header, 0x1004); assert_eq!(vm_step.default_target, Some(0x1018)); assert_eq!(vm_step.case_values_by_target.get(&0x1008), Some(&vec![0])); @@ -883,11 +912,17 @@ mod tests { &HashMap::new(), SummaryProfile::Default, ); - assert_eq!(artifact.mode, SemanticMode::Residual); - assert!(artifact.interpreter.is_some()); - assert!(artifact.vm_step.is_none()); + assert_eq!(artifact.execution, ExecutionModel::Vm); + assert_eq!( + artifact.stage, + super::super::region::RefinementStage::Residual + ); + let vm_body = artifact.vm_body().expect("vm artifact body"); + assert!(vm_body.interpreter.is_some()); + assert!(vm_body.step_summary.is_none()); assert!( artifact + .diagnostics .residual_reasons .contains(&ResidualReason::InterpreterRequiresStepSummary) ); @@ -1026,9 +1061,16 @@ mod tests { SummaryProfile::Default, ); - let vm_step = artifact.vm_step.expect("vm step summary"); - assert_eq!(artifact.mode, SemanticMode::VmSummary); - assert!(artifact.capability.query_ready); + let vm_step = artifact + .vm_body() + .and_then(|body| body.step_summary.as_ref()) + .expect("vm step summary"); + assert_eq!(artifact.execution, ExecutionModel::Vm); + assert_eq!( + artifact.stage, + super::super::region::RefinementStage::Compiled + ); + assert!(matches!(artifact.query_plan(), crate::QueryPlan::Ready)); assert_eq!(vm_step.loop_header, 0x3004); assert_eq!(vm_step.dispatch_header, 0x3004); assert_eq!(vm_step.default_target, Some(0x3018)); @@ -1183,71 +1225,139 @@ mod tests { SummaryProfile::Default, ); - assert_eq!(artifact.mode, SemanticMode::Residual); - assert!(artifact.symbolic_facts.diagnostics.skipped_large_cfg); - assert_eq!(artifact.symbolic_facts.branch_facts.len(), 1); - assert!(artifact.capability.type_ready); - assert!(!artifact.capability.decompile_ready); - assert_eq!(artifact.symbolic_facts.diagnostics.branches_evaluated, 1); + let native = artifact.native_body().expect("native body"); + assert_eq!( + artifact.stage, + super::super::region::RefinementStage::Residual + ); + assert!(artifact.diagnostics.skipped_large_cfg); + assert_eq!(native.regions.len(), 1); + assert_eq!(native.actionable_control_count(), 2); + assert!(matches!(artifact.type_plan(), crate::TypePlan::Ready)); + assert!(matches!( + artifact.decompile_plan(), + crate::DecompilePlan::Ready + )); + assert_eq!(artifact.diagnostics.branches_evaluated, 1); } #[test] fn large_cfg_exact_branch_frontier_with_memory_support_is_decompile_ready() { - let fact = crate::semantics::facts::SymbolicBranchFact { - block_addr: 0x401000, - true_target: 0x401010, - false_target: 0x401020, - true_status: crate::semantics::facts::SymbolicReachabilityStatus::Reachable, - false_status: crate::semantics::facts::SymbolicReachabilityStatus::Unreachable, - true_condition: Some("flag == 0".to_string()), - false_condition: Some("flag != 0".to_string()), - true_compiled: Some(crate::backward::BackwardConditionSummary { - simplified: "flag == 0".to_string(), - terms: vec!["flag == 0".to_string()], - memory_terms: vec![crate::backward::BackwardMemoryCondition { - region: crate::backward::BackwardMemoryRegion::Argument { index: 0 }, - offset_lo: 0, - offset_hi: 0, - size: 1, - exact_offset: true, - evidence: crate::SemanticEvidence::exact(), - binding: None, - expr: "*arg0".to_string(), - value_expr: Some("0x0:8".to_string()), - exact_value: true, - }], - backward_memory_substitutions: 0, - backward_memory_candidate_enumerations: 0, - backward_memory_residual_fallbacks: 0, - precision: crate::backward::BackwardConditionPrecision::Exact, - supported_paths: 1, - total_paths: 1, - }), - false_compiled: None, + let compiled = crate::backward::BackwardConditionSummary { + simplified: "flag == 0".to_string(), + terms: vec!["flag == 0".to_string()], + memory_terms: vec![crate::backward::BackwardMemoryCondition { + region: crate::backward::BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: 0, + offset_hi: 0, + size: 1, + exact_offset: true, + evidence: crate::SemanticEvidence::exact(), + binding: None, + expr: "*arg0".to_string(), + value_expr: Some("0x0:8".to_string()), + exact_value: true, + }], + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: crate::backward::BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, }; - let mut facts = SymbolicFunctionFacts { - branch_facts: vec![fact], - ..Default::default() + let collected = CollectedNativeSemanticRegions { + regions: std::iter::once({ + let region = crate::SemanticRegion { + anchor: 0x401000, + frontier: std::collections::BTreeSet::from([0x401010, 0x401020]), + control: vec![ + crate::Judged::new( + crate::ControlFact { + target: 0x401010, + status: crate::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + condition: Some("flag == 0".to_string()), + compiled: Some(compiled.clone()), + }, + crate::SemanticEvidence::exact(), + ), + crate::Judged::new( + crate::ControlFact { + target: 0x401020, + status: crate::SymbolicReachabilityStatus::Unreachable, + branch_truth: Some(false), + condition: Some("flag != 0".to_string()), + compiled: None, + }, + crate::SemanticEvidence::exact(), + ), + ], + memory: vec![crate::Judged::new( + crate::MemoryFact { + term: compiled.memory_terms[0].clone(), + }, + crate::SemanticEvidence::exact(), + )], + pre: Vec::new(), + post: Vec::new(), + targets: vec![ + crate::Judged::new( + crate::TargetFact { + target: 0x401010, + status: crate::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + }, + crate::SemanticEvidence::exact(), + ), + crate::Judged::new( + crate::TargetFact { + target: 0x401020, + status: crate::SymbolicReachabilityStatus::Unreachable, + branch_truth: Some(false), + }, + crate::SemanticEvidence::exact(), + ), + ], + }; + (region.key(), region) + }) + .collect(), + diagnostics: SymbolicFunctionFactDiagnostics { + skipped_large_cfg: true, + ..Default::default() + }, }; - facts.diagnostics.skipped_large_cfg = true; - let mode = semantic_mode_for( + let stage = semantic_stage_for( 0, 0, &DerivedSummaryDiagnostics::default(), - &facts, + &collected, false, false, ); - assert_eq!(mode, SemanticMode::IslandCompiled); - let capability = semantic_capability(mode, &facts); - assert!(capability.query_ready); - assert!(capability.type_ready); - assert!(capability.decompile_ready); - let reasons = normalized_residual_reasons( - mode, - residual_reasons(&DerivedSummaryDiagnostics::default(), &facts, false, false), - ); + let artifact = build_semantic_artifact(BuildSemanticArtifactInput { + stage, + granularity: ArtifactGranularity::Regioned, + execution: ExecutionModel::Native, + suppress_large_cfg_reason: true, + slice_class: crate::SliceClass::Worker, + closure_functions: 0, + helper_functions: 0, + derived_summaries: 0, + derived_diagnostics: DerivedSummaryDiagnostics::default(), + collected, + interpreter: None, + vm_step: None, + vm_transfer: None, + }); + assert!(matches!(artifact.query_plan(), crate::QueryPlan::Ready)); + assert!(matches!(artifact.type_plan(), crate::TypePlan::Ready)); + assert!(matches!( + artifact.decompile_plan(), + crate::DecompilePlan::Ready + )); + let reasons = artifact.diagnostics.residual_reasons; assert!( !reasons.contains(&ResidualReason::LargeCfg), "island-compiled workers should keep large_cfg as provenance, not a residual reason" diff --git a/crates/r2sym/src/semantics/facts.rs b/crates/r2sym/src/semantics/facts.rs index c5fe723..e480fc5 100644 --- a/crates/r2sym/src/semantics/facts.rs +++ b/crates/r2sym/src/semantics/facts.rs @@ -13,8 +13,7 @@ use crate::backward::{ use crate::path::{ExploreConfig, PathExplorer}; use crate::runtime::seed_default_state_for_arch; use crate::semantics::{ - SemanticConfidence, SemanticEvidence, SemanticEvidenceCoverage, SemanticEvidenceProvenance, - SemanticEvidenceReason, + SemanticEvidence, SemanticEvidenceCoverage, SemanticEvidenceProvenance, SemanticEvidenceReason, }; use crate::sim::{ DerivedSummaryCompletion, DerivedSummarySet, PreparedFunctionScope, SummaryProfile, @@ -22,6 +21,8 @@ use crate::sim::{ }; use crate::solver::SatResult; +use super::region::{ControlFact, Judged, MemoryFact, RegionKey, SemanticRegion, TargetFact}; + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum SymbolicReachabilityStatus { Reachable, @@ -29,223 +30,6 @@ pub enum SymbolicReachabilityStatus { Unknown, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SymbolicBranchFact { - pub block_addr: u64, - pub true_target: u64, - pub false_target: u64, - pub true_status: SymbolicReachabilityStatus, - pub false_status: SymbolicReachabilityStatus, - pub true_condition: Option, - pub false_condition: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub true_compiled: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub false_compiled: Option, -} - -impl SymbolicBranchFact { - pub fn exact_reachable_target(&self) -> Option { - match (self.true_status, self.false_status) { - (SymbolicReachabilityStatus::Reachable, SymbolicReachabilityStatus::Unreachable) => { - Some(self.true_target) - } - (SymbolicReachabilityStatus::Unreachable, SymbolicReachabilityStatus::Reachable) => { - Some(self.false_target) - } - _ => None, - } - } - - pub fn actionable_reachable_target(&self) -> Option { - match (self.true_status, self.false_status) { - (SymbolicReachabilityStatus::Reachable, SymbolicReachabilityStatus::Unreachable) - if self - .true_compiled - .as_ref() - .is_some_and(|compiled| compiled.evidence().allows_narrowing()) => - { - Some(self.true_target) - } - (SymbolicReachabilityStatus::Unreachable, SymbolicReachabilityStatus::Reachable) - if self - .false_compiled - .as_ref() - .is_some_and(|compiled| compiled.evidence().allows_narrowing()) => - { - Some(self.false_target) - } - _ => None, - } - } - - pub fn exact_compiled_condition(&self) -> Option<&BackwardConditionSummary> { - match (self.true_status, self.false_status) { - (SymbolicReachabilityStatus::Reachable, SymbolicReachabilityStatus::Unreachable) => { - self.true_compiled - .as_ref() - .filter(|compiled| compiled.evidence().allows_hard_proof()) - } - (SymbolicReachabilityStatus::Unreachable, SymbolicReachabilityStatus::Reachable) => { - self.false_compiled - .as_ref() - .filter(|compiled| compiled.evidence().allows_hard_proof()) - } - _ => None, - } - } - - pub fn actionable_compiled_condition(&self) -> Option<&BackwardConditionSummary> { - match (self.true_status, self.false_status) { - (SymbolicReachabilityStatus::Reachable, SymbolicReachabilityStatus::Unreachable) => { - self.true_compiled - .as_ref() - .filter(|compiled| compiled.evidence().allows_narrowing()) - } - (SymbolicReachabilityStatus::Unreachable, SymbolicReachabilityStatus::Reachable) => { - self.false_compiled - .as_ref() - .filter(|compiled| compiled.evidence().allows_narrowing()) - } - _ => None, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum SymbolicControlIslandKind { - BranchFrontier, - LargeCfgBranchFrontier, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -pub enum SymbolicMemoryIslandKind { - ConditionFrontier, - LargeCfgConditionFrontier, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SymbolicControlFact { - pub target: u64, - pub status: SymbolicReachabilityStatus, - #[serde(skip_serializing_if = "Option::is_none")] - pub condition: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub compiled: Option, - #[serde(default, skip_serializing_if = "SemanticEvidence::is_default_exact")] - pub evidence: SemanticEvidence, -} - -impl SymbolicControlFact { - pub fn exact_compiled_condition(&self) -> Option<&BackwardConditionSummary> { - self.evidence - .allows_hard_proof() - .then_some(self.compiled.as_ref()) - .flatten() - } - - pub fn actionable_compiled_condition(&self) -> Option<&BackwardConditionSummary> { - self.evidence - .allows_narrowing() - .then_some(self.compiled.as_ref()) - .flatten() - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SymbolicControlIsland { - pub kind: SymbolicControlIslandKind, - pub anchor_block: u64, - pub frontier_targets: Vec, - pub facts: Vec, - #[serde(default, skip_serializing_if = "SemanticEvidence::is_default_exact")] - pub evidence: SemanticEvidence, -} - -impl SymbolicControlIsland { - pub fn exact_compiled_condition(&self) -> Option<&BackwardConditionSummary> { - unique_compiled_condition(self.facts.iter(), true) - } - - pub fn actionable_compiled_condition(&self) -> Option<&BackwardConditionSummary> { - unique_compiled_condition(self.facts.iter(), false) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SymbolicMemoryIsland { - pub kind: SymbolicMemoryIslandKind, - pub anchor_block: u64, - pub terms: Vec, - #[serde(default, skip_serializing_if = "SemanticEvidence::is_default_exact")] - pub evidence: SemanticEvidence, -} - -impl SymbolicMemoryIsland { - pub fn exact_terms(&self) -> Vec<&BackwardMemoryCondition> { - self.terms - .iter() - .filter(|term| term.evidence().allows_hard_proof()) - .collect() - } - - pub fn actionable_terms(&self) -> Vec<&BackwardMemoryCondition> { - self.terms - .iter() - .filter(|term| term.evidence().allows_narrowing()) - .collect() - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SymbolicWorkerIsland { - pub anchor_block: u64, - #[serde(skip_serializing_if = "Option::is_none")] - pub control_kind: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub memory_kind: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub frontier_targets: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub control_facts: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub memory_terms: Vec, - #[serde(default, skip_serializing_if = "SemanticEvidence::is_default_exact")] - pub evidence: SemanticEvidence, -} - -impl SymbolicWorkerIsland { - pub fn exact_reachable_target(&self) -> Option { - unique_reachable_control_target(self.control_facts.iter(), true) - } - - pub fn actionable_reachable_target(&self) -> Option { - unique_reachable_control_target(self.control_facts.iter(), false) - } - - pub fn exact_compiled_condition(&self) -> Option<&BackwardConditionSummary> { - unique_compiled_condition(self.control_facts.iter(), true) - } - - pub fn actionable_compiled_condition(&self) -> Option<&BackwardConditionSummary> { - unique_compiled_condition(self.control_facts.iter(), false) - } - - pub fn exact_memory_terms(&self) -> Vec<&BackwardMemoryCondition> { - self.memory_terms - .iter() - .filter(|term| term.evidence().allows_hard_proof()) - .collect() - } - - pub fn actionable_memory_terms(&self) -> Vec<&BackwardMemoryCondition> { - self.memory_terms - .iter() - .filter(|term| term.evidence().allows_narrowing()) - .collect() - } -} - #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct SymbolicFunctionFactDiagnostics { pub branches_evaluated: usize, @@ -255,131 +39,178 @@ pub struct SymbolicFunctionFactDiagnostics { pub skipped_large_cfg: bool, } -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct SymbolicFunctionFacts { - pub branch_facts: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub worker_islands: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub control_islands: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub memory_islands: Vec, +#[derive(Debug, Clone, Default)] +pub(super) struct CollectedNativeSemanticRegions { + pub regions: BTreeMap, pub diagnostics: SymbolicFunctionFactDiagnostics, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct SymbolicTargetConditionSource<'a> { - pub block_addr: u64, - pub branch_truth: bool, - pub summary: &'a BackwardConditionSummary, - pub necessary_for_target: bool, -} - -impl SymbolicFunctionFacts { - pub fn branch_fact_for_block(&self, block_addr: u64) -> Option<&SymbolicBranchFact> { - self.branch_facts - .iter() - .find(|fact| fact.block_addr == block_addr) - } - - pub fn worker_island_for_block(&self, block_addr: u64) -> Option<&SymbolicWorkerIsland> { - self.worker_islands - .iter() - .find(|island| island.anchor_block == block_addr) - } - - pub fn best_worker_island_for_target( - &self, - target_addr: u64, - hard_proof_only: bool, - ) -> Option<&SymbolicWorkerIsland> { - best_worker_island_for_target(self, target_addr, hard_proof_only) - } - - pub fn control_island_for_block(&self, block_addr: u64) -> Option<&SymbolicControlIsland> { - self.control_islands - .iter() - .find(|island| island.anchor_block == block_addr) +#[derive(Debug, Clone, PartialEq, Eq)] +struct BranchObservation { + block_addr: u64, + true_target: u64, + false_target: u64, + true_status: SymbolicReachabilityStatus, + false_status: SymbolicReachabilityStatus, + true_condition: Option, + false_condition: Option, + true_compiled: Option, + false_compiled: Option, +} + +fn target_status_evidence(status: SymbolicReachabilityStatus) -> SemanticEvidence { + match status { + SymbolicReachabilityStatus::Reachable | SymbolicReachabilityStatus::Unreachable => { + SemanticEvidence::exact() + } + SymbolicReachabilityStatus::Unknown => { + SemanticEvidence::residual(SemanticEvidenceReason::ResidualSearchRequired) + } } +} - pub fn memory_island_for_block(&self, block_addr: u64) -> Option<&SymbolicMemoryIsland> { - self.memory_islands - .iter() - .find(|island| island.anchor_block == block_addr) +fn push_unique_judged(dst: &mut Vec>, value: Judged) { + if !dst.contains(&value) { + dst.push(value); } +} - pub fn actionable_memory_terms_for_block( - &self, - block_addr: u64, - ) -> Vec<&BackwardMemoryCondition> { - self.worker_island_for_block(block_addr) - .map(SymbolicWorkerIsland::actionable_memory_terms) - .or_else(|| { - self.memory_island_for_block(block_addr) - .map(SymbolicMemoryIsland::actionable_terms) - }) - .unwrap_or_default() - } +fn ensure_semantic_region( + by_anchor: &mut BTreeMap, + anchor: u64, +) -> &mut SemanticRegion { + by_anchor.entry(anchor).or_insert_with(|| SemanticRegion { + anchor, + frontier: BTreeSet::new(), + control: Vec::new(), + memory: Vec::new(), + pre: Vec::new(), + post: Vec::new(), + targets: Vec::new(), + }) +} - pub fn actionable_compiled_condition_for_target( - &self, - target_addr: u64, - ) -> Option<&BackwardConditionSummary> { - target_compiled_condition(self, target_addr, false) +fn push_region_memory_terms( + region: &mut SemanticRegion, + terms: impl IntoIterator, +) { + for term in terms { + push_unique_judged( + &mut region.memory, + Judged::new(MemoryFact { term: term.clone() }, term.evidence().clone()), + ); } +} - pub fn exact_compiled_condition_for_target( - &self, - target_addr: u64, - ) -> Option<&BackwardConditionSummary> { - target_compiled_condition(self, target_addr, true) - } +fn push_branch_observation_region( + by_anchor: &mut BTreeMap, + branch: &BranchObservation, + diagnostics: &SymbolicFunctionFactDiagnostics, +) { + let region = ensure_semantic_region(by_anchor, branch.block_addr); + region.frontier.insert(branch.true_target); + region.frontier.insert(branch.false_target); + + let true_evidence = if branch.true_compiled.is_some() || branch.true_condition.is_some() { + control_fact_evidence( + branch.true_compiled.as_ref(), + branch.true_condition.as_deref(), + diagnostics, + ) + } else { + target_status_evidence(branch.true_status) + }; + let false_evidence = if branch.false_compiled.is_some() || branch.false_condition.is_some() { + control_fact_evidence( + branch.false_compiled.as_ref(), + branch.false_condition.as_deref(), + diagnostics, + ) + } else { + target_status_evidence(branch.false_status) + }; - pub fn actionable_condition_source_for_target( - &self, - target_addr: u64, - ) -> Option> { - target_condition_source(self, target_addr, false) + push_unique_judged( + &mut region.control, + Judged::new( + ControlFact { + target: branch.true_target, + status: branch.true_status, + branch_truth: Some(true), + condition: branch.true_condition.clone(), + compiled: branch.true_compiled.clone(), + }, + true_evidence.clone(), + ), + ); + push_unique_judged( + &mut region.control, + Judged::new( + ControlFact { + target: branch.false_target, + status: branch.false_status, + branch_truth: Some(false), + condition: branch.false_condition.clone(), + compiled: branch.false_compiled.clone(), + }, + false_evidence.clone(), + ), + ); + push_unique_judged( + &mut region.targets, + Judged::new( + TargetFact { + target: branch.true_target, + status: branch.true_status, + branch_truth: Some(true), + }, + true_evidence, + ), + ); + push_unique_judged( + &mut region.targets, + Judged::new( + TargetFact { + target: branch.false_target, + status: branch.false_status, + branch_truth: Some(false), + }, + false_evidence, + ), + ); + if let Some(compiled) = branch + .true_compiled + .as_ref() + .filter(|compiled| compiled.evidence().allows_narrowing()) + { + push_region_memory_terms(region, compiled.memory_terms.iter().cloned()); } - - pub fn exact_condition_source_for_target( - &self, - target_addr: u64, - ) -> Option> { - target_condition_source(self, target_addr, true) + if let Some(compiled) = branch + .false_compiled + .as_ref() + .filter(|compiled| compiled.evidence().allows_narrowing()) + { + push_region_memory_terms(region, compiled.memory_terms.iter().cloned()); } +} - pub fn actionable_memory_terms_for_target( - &self, - target_addr: u64, - ) -> Vec<&BackwardMemoryCondition> { - self.actionable_condition_source_for_target(target_addr) - .map(|source| self.actionable_memory_terms_for_block(source.block_addr)) - .or_else(|| { - best_worker_island_for_target(self, target_addr, false) - .map(SymbolicWorkerIsland::actionable_memory_terms) - }) - .or_else(|| { - best_legacy_memory_island_for_target(self, target_addr) - .map(SymbolicMemoryIsland::actionable_terms) - }) - .unwrap_or_default() +fn build_canonical_regions( + branch_observations: &[BranchObservation], + summary_memory_terms: &BTreeMap>, + diagnostics: &SymbolicFunctionFactDiagnostics, +) -> BTreeMap { + let mut by_anchor = BTreeMap::::new(); + for branch in branch_observations { + push_branch_observation_region(&mut by_anchor, branch, diagnostics); } - - pub fn exact_memory_terms_for_target(&self, target_addr: u64) -> Vec<&BackwardMemoryCondition> { - self.exact_condition_source_for_target(target_addr) - .and_then(|source| self.worker_island_for_block(source.block_addr)) - .map(SymbolicWorkerIsland::exact_memory_terms) - .or_else(|| { - best_worker_island_for_target(self, target_addr, true) - .map(SymbolicWorkerIsland::exact_memory_terms) - }) - .or_else(|| { - best_legacy_memory_island_for_target(self, target_addr) - .map(SymbolicMemoryIsland::exact_terms) - }) - .unwrap_or_default() + for (&anchor, terms) in summary_memory_terms { + let region = ensure_semantic_region(&mut by_anchor, anchor); + push_region_memory_terms(region, terms.iter().cloned()); } + by_anchor + .into_values() + .map(|region| (region.key(), region)) + .collect() } fn control_fact_evidence( @@ -445,426 +276,6 @@ fn control_fact_evidence( } } -fn island_evidence(facts: &[SymbolicControlFact]) -> SemanticEvidence { - facts - .iter() - .map(|fact| fact.evidence.clone()) - .max_by_key(|evidence| { - ( - match evidence.tier { - SemanticConfidence::Exact => 3, - SemanticConfidence::Likely => 2, - SemanticConfidence::Heuristic => 1, - SemanticConfidence::Residual => 0, - }, - evidence.allows_hard_proof() as u8, - evidence.allows_narrowing() as u8, - ) - }) - .unwrap_or_else(|| SemanticEvidence::residual(SemanticEvidenceReason::GuardOpaque)) -} - -fn memory_island_evidence(terms: &[BackwardMemoryCondition]) -> SemanticEvidence { - terms - .iter() - .map(BackwardMemoryCondition::evidence) - .max_by_key(|evidence| { - ( - match evidence.tier { - SemanticConfidence::Exact => 3, - SemanticConfidence::Likely => 2, - SemanticConfidence::Heuristic => 1, - SemanticConfidence::Residual => 0, - }, - evidence.allows_hard_proof() as u8, - evidence.allows_narrowing() as u8, - ) - }) - .unwrap_or_else(|| SemanticEvidence::residual(SemanticEvidenceReason::ValueOpaque)) -} - -fn unique_compiled_condition<'a>( - facts: impl Iterator, - hard_proof_only: bool, -) -> Option<&'a BackwardConditionSummary> { - let mut candidates = facts.filter_map(|fact| { - if hard_proof_only { - fact.exact_compiled_condition() - } else { - fact.actionable_compiled_condition() - } - }); - let first = candidates.next()?; - candidates.next().is_none().then_some(first) -} - -fn unique_reachable_control_target<'a>( - facts: impl Iterator, - hard_proof_only: bool, -) -> Option { - let mut candidates = facts.filter_map(|fact| { - let condition = if hard_proof_only { - fact.evidence.allows_hard_proof() - } else { - fact.evidence.allows_narrowing() - }; - condition - .then_some(matches!(fact.status, SymbolicReachabilityStatus::Reachable)) - .and_then(|reachable| reachable.then_some(fact.target)) - }); - let first = candidates.next()?; - candidates.next().is_none().then_some(first) -} - -fn summary_precision_rank(summary: &BackwardConditionSummary) -> u8 { - match summary.precision { - BackwardConditionPrecision::Exact => 3, - BackwardConditionPrecision::OverApprox => 2, - BackwardConditionPrecision::ResidualSearchRequired => 1, - BackwardConditionPrecision::Unsupported => 0, - } -} - -fn summary_evidence_rank(summary: &BackwardConditionSummary) -> u8 { - match summary.evidence().tier { - SemanticConfidence::Exact => 3, - SemanticConfidence::Likely => 2, - SemanticConfidence::Heuristic => 1, - SemanticConfidence::Residual => 0, - } -} - -fn best_target_compiled_condition<'a>( - candidates: impl Iterator, -) -> Option<&'a BackwardConditionSummary> { - candidates.max_by(|left, right| { - ( - summary_evidence_rank(left), - summary_precision_rank(left), - std::cmp::Reverse(left.backward_memory_residual_fallbacks), - left.memory_terms.len(), - left.supported_paths, - std::cmp::Reverse(left.total_paths), - std::cmp::Reverse(left.simplified.len()), - ) - .cmp(&( - summary_evidence_rank(right), - summary_precision_rank(right), - std::cmp::Reverse(right.backward_memory_residual_fallbacks), - right.memory_terms.len(), - right.supported_paths, - std::cmp::Reverse(right.total_paths), - std::cmp::Reverse(right.simplified.len()), - )) - }) -} - -fn target_compiled_condition( - facts: &SymbolicFunctionFacts, - target_addr: u64, - hard_proof_only: bool, -) -> Option<&BackwardConditionSummary> { - let branch_candidates = facts.branch_facts.iter().flat_map(|fact| { - let mut candidates = Vec::new(); - if fact.true_target == target_addr { - let compiled = if hard_proof_only { - fact.true_compiled - .as_ref() - .filter(|compiled| compiled.evidence().allows_hard_proof()) - } else { - fact.true_compiled - .as_ref() - .filter(|compiled| compiled.evidence().allows_narrowing()) - }; - if let Some(compiled) = compiled { - candidates.push(compiled); - } - } - if fact.false_target == target_addr { - let compiled = if hard_proof_only { - fact.false_compiled - .as_ref() - .filter(|compiled| compiled.evidence().allows_hard_proof()) - } else { - fact.false_compiled - .as_ref() - .filter(|compiled| compiled.evidence().allows_narrowing()) - }; - if let Some(compiled) = compiled { - candidates.push(compiled); - } - } - candidates - }); - let control_candidates = facts - .worker_islands - .iter() - .flat_map(|island| island.control_facts.iter()) - .chain( - facts - .control_islands - .iter() - .flat_map(|island| island.facts.iter()), - ) - .filter_map(move |fact| { - (fact.target == target_addr) - .then_some(if hard_proof_only { - fact.exact_compiled_condition() - } else { - fact.actionable_compiled_condition() - }) - .flatten() - }); - best_target_compiled_condition(branch_candidates.chain(control_candidates)) -} - -fn best_worker_island_for_target( - facts: &SymbolicFunctionFacts, - target_addr: u64, - hard_proof_only: bool, -) -> Option<&SymbolicWorkerIsland> { - facts - .worker_islands - .iter() - .filter(|island| { - island.control_facts.iter().any(|fact| { - fact.target == target_addr - && if hard_proof_only { - fact.evidence.allows_hard_proof() - } else { - fact.evidence.allows_narrowing() - } - }) - }) - .max_by(|left, right| { - ( - left.evidence.allows_hard_proof() as u8, - left.evidence.allows_narrowing() as u8, - left.evidence.tier, - left.memory_terms.len(), - left.control_facts.len(), - ) - .cmp(&( - right.evidence.allows_hard_proof() as u8, - right.evidence.allows_narrowing() as u8, - right.evidence.tier, - right.memory_terms.len(), - right.control_facts.len(), - )) - }) -} - -fn best_legacy_memory_island_for_target( - facts: &SymbolicFunctionFacts, - target_addr: u64, -) -> Option<&SymbolicMemoryIsland> { - facts - .branch_facts - .iter() - .filter(|fact| fact.true_target == target_addr || fact.false_target == target_addr) - .filter_map(|fact| facts.memory_island_for_block(fact.block_addr)) - .max_by(|left, right| { - ( - left.evidence.allows_hard_proof() as u8, - left.evidence.allows_narrowing() as u8, - left.evidence.tier, - left.terms.len(), - ) - .cmp(&( - right.evidence.allows_hard_proof() as u8, - right.evidence.allows_narrowing() as u8, - right.evidence.tier, - right.terms.len(), - )) - }) -} - -fn target_condition_source( - facts: &SymbolicFunctionFacts, - target_addr: u64, - hard_proof_only: bool, -) -> Option> { - let branch_candidates = facts - .branch_facts - .iter() - .filter_map(|fact| { - let (branch_truth, compiled) = if fact.true_target == target_addr { - let compiled = if hard_proof_only { - fact.true_compiled - .as_ref() - .filter(|compiled| compiled.evidence().allows_hard_proof()) - } else { - fact.true_compiled - .as_ref() - .filter(|compiled| compiled.evidence().allows_narrowing()) - }; - (Some(true), compiled) - } else if fact.false_target == target_addr { - let compiled = if hard_proof_only { - fact.false_compiled - .as_ref() - .filter(|compiled| compiled.evidence().allows_hard_proof()) - } else { - fact.false_compiled - .as_ref() - .filter(|compiled| compiled.evidence().allows_narrowing()) - }; - (Some(false), compiled) - } else { - (None, None) - }; - Some(SymbolicTargetConditionSource { - block_addr: fact.block_addr, - branch_truth: branch_truth?, - summary: compiled?, - necessary_for_target: false, - }) - }) - .collect::>(); - if let Some(first) = branch_candidates.first().copied() { - let necessary_for_target = branch_candidates.iter().all(|candidate| { - candidate.block_addr == first.block_addr - && candidate.branch_truth == first.branch_truth - && candidate.summary == first.summary - }); - if hard_proof_only && !necessary_for_target { - return None; - } - let mut best = branch_candidates.into_iter().max_by(|left, right| { - ( - summary_evidence_rank(left.summary), - summary_precision_rank(left.summary), - std::cmp::Reverse(left.summary.backward_memory_residual_fallbacks), - left.summary.memory_terms.len(), - left.summary.supported_paths, - std::cmp::Reverse(left.summary.total_paths), - std::cmp::Reverse(left.block_addr), - left.branch_truth, - ) - .cmp(&( - summary_evidence_rank(right.summary), - summary_precision_rank(right.summary), - std::cmp::Reverse(right.summary.backward_memory_residual_fallbacks), - right.summary.memory_terms.len(), - right.summary.supported_paths, - std::cmp::Reverse(right.summary.total_paths), - std::cmp::Reverse(right.block_addr), - right.branch_truth, - )) - })?; - best.necessary_for_target = necessary_for_target; - return Some(best); - } - - let candidates = facts - .worker_islands - .iter() - .filter_map(|island| { - let branch = facts.branch_fact_for_block(island.anchor_block)?; - let necessary_for_target = if hard_proof_only { - island.exact_reachable_target() == Some(target_addr) - } else { - island.actionable_reachable_target() == Some(target_addr) - }; - island.control_facts.iter().find_map(|fact| { - if fact.target != target_addr { - return None; - } - let summary = if hard_proof_only { - fact.exact_compiled_condition() - } else { - fact.actionable_compiled_condition() - }?; - let branch_truth = if branch.true_target == target_addr { - Some(true) - } else if branch.false_target == target_addr { - Some(false) - } else { - None - }?; - Some(SymbolicTargetConditionSource { - block_addr: island.anchor_block, - branch_truth, - summary, - necessary_for_target, - }) - }) - }) - .collect::>(); - if candidates.is_empty() { - return None; - } - if hard_proof_only - && candidates - .iter() - .any(|candidate| !candidate.necessary_for_target) - { - return None; - } - candidates.into_iter().max_by(|left, right| { - ( - left.necessary_for_target as u8, - summary_evidence_rank(left.summary), - summary_precision_rank(left.summary), - left.summary.memory_terms.len(), - left.summary.supported_paths, - std::cmp::Reverse(left.summary.total_paths), - std::cmp::Reverse(left.block_addr), - left.branch_truth, - ) - .cmp(&( - right.necessary_for_target as u8, - summary_evidence_rank(right.summary), - summary_precision_rank(right.summary), - right.summary.memory_terms.len(), - right.summary.supported_paths, - std::cmp::Reverse(right.summary.total_paths), - std::cmp::Reverse(right.block_addr), - right.branch_truth, - )) - }) -} - -fn default_control_island_kind( - diagnostics: &SymbolicFunctionFactDiagnostics, -) -> SymbolicControlIslandKind { - if diagnostics.skipped_large_cfg { - SymbolicControlIslandKind::LargeCfgBranchFrontier - } else { - SymbolicControlIslandKind::BranchFrontier - } -} - -fn control_facts_for_branch( - branch: &SymbolicBranchFact, - diagnostics: &SymbolicFunctionFactDiagnostics, -) -> Vec { - vec![ - SymbolicControlFact { - target: branch.true_target, - status: branch.true_status, - condition: branch.true_condition.clone(), - compiled: branch.true_compiled.clone(), - evidence: control_fact_evidence( - branch.true_compiled.as_ref(), - branch.true_condition.as_deref(), - diagnostics, - ), - }, - SymbolicControlFact { - target: branch.false_target, - status: branch.false_status, - condition: branch.false_condition.clone(), - compiled: branch.false_compiled.clone(), - evidence: control_fact_evidence( - branch.false_compiled.as_ref(), - branch.false_condition.as_deref(), - diagnostics, - ), - }, - ] -} - fn derived_summary_memory_term_evidence( completion: DerivedSummaryCompletion, exact_value: bool, @@ -890,163 +301,6 @@ fn derived_summary_memory_term_evidence( } } -fn merge_memory_islands( - mut islands: Vec, - additions: impl IntoIterator, -) -> Vec { - let mut by_anchor = BTreeMap::::new(); - for island in islands.drain(..).chain(additions) { - let entry = by_anchor - .entry(island.anchor_block) - .or_insert_with(|| SymbolicMemoryIsland { - kind: island.kind, - anchor_block: island.anchor_block, - terms: Vec::new(), - evidence: island.evidence.clone(), - }); - if matches!(entry.kind, SymbolicMemoryIslandKind::ConditionFrontier) - && matches!( - island.kind, - SymbolicMemoryIslandKind::LargeCfgConditionFrontier - ) - { - entry.kind = island.kind; - } - for term in island.terms { - if !entry.terms.contains(&term) { - entry.terms.push(term); - } - } - entry.evidence = memory_island_evidence(&entry.terms); - } - by_anchor.into_values().collect() -} - -fn push_unique_memory_terms( - dst: &mut Vec, - terms: impl IntoIterator, -) { - for term in terms { - if !dst.contains(&term) { - dst.push(term); - } - } -} - -fn worker_island_evidence( - control_facts: &[SymbolicControlFact], - memory_terms: &[BackwardMemoryCondition], -) -> SemanticEvidence { - let control = island_evidence(control_facts); - let memory = memory_island_evidence(memory_terms); - let control_rank = ( - control.allows_hard_proof() as u8, - control.allows_narrowing() as u8, - match control.tier { - SemanticConfidence::Exact => 3, - SemanticConfidence::Likely => 2, - SemanticConfidence::Heuristic => 1, - SemanticConfidence::Residual => 0, - }, - ); - let memory_rank = ( - memory.allows_hard_proof() as u8, - memory.allows_narrowing() as u8, - match memory.tier { - SemanticConfidence::Exact => 3, - SemanticConfidence::Likely => 2, - SemanticConfidence::Heuristic => 1, - SemanticConfidence::Residual => 0, - }, - ); - if memory_rank > control_rank { - memory - } else { - control - } -} - -fn derive_worker_islands( - branch_facts: &[SymbolicBranchFact], - preseeded_memory_islands: &[SymbolicMemoryIsland], - diagnostics: &SymbolicFunctionFactDiagnostics, -) -> Vec { - let mut by_anchor = BTreeMap::::new(); - for branch in branch_facts { - let entry = by_anchor - .entry(branch.block_addr) - .or_insert_with(|| SymbolicWorkerIsland { - anchor_block: branch.block_addr, - control_kind: None, - memory_kind: None, - frontier_targets: Vec::new(), - control_facts: Vec::new(), - memory_terms: Vec::new(), - evidence: SemanticEvidence::residual(SemanticEvidenceReason::GuardOpaque), - }); - entry.control_kind = Some(default_control_island_kind(diagnostics)); - entry.frontier_targets = vec![branch.true_target, branch.false_target]; - entry.control_facts = control_facts_for_branch(branch, diagnostics); - push_unique_memory_terms( - &mut entry.memory_terms, - entry - .control_facts - .iter() - .filter_map(SymbolicControlFact::actionable_compiled_condition) - .flat_map(|compiled| compiled.memory_terms.iter().cloned()), - ); - entry.evidence = worker_island_evidence(&entry.control_facts, &entry.memory_terms); - } - for island in preseeded_memory_islands { - let entry = by_anchor - .entry(island.anchor_block) - .or_insert_with(|| SymbolicWorkerIsland { - anchor_block: island.anchor_block, - control_kind: None, - memory_kind: None, - frontier_targets: Vec::new(), - control_facts: Vec::new(), - memory_terms: Vec::new(), - evidence: island.evidence.clone(), - }); - entry.memory_kind = Some(island.kind); - push_unique_memory_terms(&mut entry.memory_terms, island.terms.iter().cloned()); - entry.evidence = worker_island_evidence(&entry.control_facts, &entry.memory_terms); - } - by_anchor.into_values().collect() -} - -fn project_control_islands(worker_islands: &[SymbolicWorkerIsland]) -> Vec { - worker_islands - .iter() - .filter(|island| !island.control_facts.is_empty()) - .map(|island| SymbolicControlIsland { - kind: island - .control_kind - .unwrap_or(SymbolicControlIslandKind::BranchFrontier), - anchor_block: island.anchor_block, - frontier_targets: island.frontier_targets.clone(), - facts: island.control_facts.clone(), - evidence: island_evidence(&island.control_facts), - }) - .collect() -} - -fn project_memory_islands(worker_islands: &[SymbolicWorkerIsland]) -> Vec { - worker_islands - .iter() - .filter(|island| !island.memory_terms.is_empty()) - .map(|island| SymbolicMemoryIsland { - kind: island - .memory_kind - .unwrap_or(SymbolicMemoryIslandKind::ConditionFrontier), - anchor_block: island.anchor_block, - terms: island.memory_terms.clone(), - evidence: memory_island_evidence(&island.memory_terms), - }) - .collect() -} - fn summary_memory_location_expr(arg_index: usize, offset: i64) -> String { if offset == 0 { format!("*arg{arg_index}") @@ -1057,12 +311,11 @@ fn summary_memory_location_expr(arg_index: usize, offset: i64) -> String { } } -fn derive_summary_memory_islands<'ctx>( +fn derive_summary_memory_terms_by_anchor<'ctx>( func: &SsaArtifact, branch_blocks: &[(u64, u64, u64)], derived: &DerivedSummarySet<'ctx>, - diagnostics: &SymbolicFunctionFactDiagnostics, -) -> Vec { +) -> BTreeMap> { let hot_blocks = branch_blocks .iter() .flat_map(|(block, true_target, false_target)| [*block, *true_target, *false_target]) @@ -1119,32 +372,6 @@ fn derive_summary_memory_islands<'ctx>( } by_anchor - .into_iter() - .filter_map(|(anchor_block, terms)| { - (!terms.is_empty()).then(|| SymbolicMemoryIsland { - kind: if diagnostics.skipped_large_cfg { - SymbolicMemoryIslandKind::LargeCfgConditionFrontier - } else { - SymbolicMemoryIslandKind::ConditionFrontier - }, - anchor_block, - evidence: memory_island_evidence(&terms), - terms, - }) - }) - .collect() -} - -fn finalize_symbolic_function_facts(mut facts: SymbolicFunctionFacts) -> SymbolicFunctionFacts { - let preseeded_memory_islands = std::mem::take(&mut facts.memory_islands); - facts.worker_islands = derive_worker_islands( - &facts.branch_facts, - &preseeded_memory_islands, - &facts.diagnostics, - ); - facts.control_islands = project_control_islands(&facts.worker_islands); - facts.memory_islands = project_memory_islands(&facts.worker_islands); - facts } fn symbolic_condition_hint(summary: Option<&BackwardConditionSummary>) -> Option { @@ -1250,20 +477,20 @@ fn symbolic_reachability_status( } } -fn collect_symbolic_function_facts_for_branch_blocks<'ctx, F>( +fn collect_branch_observations_for_branch_blocks<'ctx, F>( ctx: &'ctx Context, func: &SsaArtifact, arch: &ArchSpec, branch_blocks: &[(u64, u64, u64)], install_hooks: F, -) -> SymbolicFunctionFacts +) -> (Vec, SymbolicFunctionFactDiagnostics) where F: Fn(&mut PathExplorer<'ctx>), { - let mut facts = SymbolicFunctionFacts::default(); - + let mut branch_facts = Vec::new(); + let mut diagnostics = SymbolicFunctionFactDiagnostics::default(); for &(block_addr, true_target, false_target) in branch_blocks { - facts.diagnostics.branches_evaluated += 1; + diagnostics.branches_evaluated += 1; let predicate_uses_call_result = predicate_depends_on_call_result(func, block_addr); let make_state = || { @@ -1315,7 +542,7 @@ where if matches!(true_status, SymbolicReachabilityStatus::Unknown) || matches!(false_status, SymbolicReachabilityStatus::Unknown) { - facts.diagnostics.branches_unknown += 1; + diagnostics.branches_unknown += 1; } if matches!( (true_status, false_status), @@ -1327,10 +554,10 @@ where SymbolicReachabilityStatus::Reachable ) ) { - facts.diagnostics.branches_pruned += 1; + diagnostics.branches_pruned += 1; } - facts.branch_facts.push(SymbolicBranchFact { + branch_facts.push(BranchObservation { block_addr, true_target, false_target, @@ -1343,7 +570,7 @@ where }); } - facts + (branch_facts, diagnostics) } fn install_derived_summary_set<'ctx>( @@ -1524,7 +751,7 @@ fn predicate_depends_on_call_result(func: &SsaArtifact, block_addr: u64) -> bool } #[allow(clippy::too_many_arguments)] -pub(super) fn collect_symbolic_function_facts_with_derived<'ctx>( +pub(super) fn collect_canonical_semantic_regions_with_derived<'ctx>( ctx: &'ctx Context, func: &SsaArtifact, scope: Option<&PreparedFunctionScope>, @@ -1533,8 +760,8 @@ pub(super) fn collect_symbolic_function_facts_with_derived<'ctx>( summary_profile: SummaryProfile, registry: &SummaryRegistry<'ctx>, derived: &DerivedSummarySet<'ctx>, -) -> SymbolicFunctionFacts { - collect_symbolic_function_facts_with_derived_for_branch_blocks( +) -> CollectedNativeSemanticRegions { + collect_canonical_semantic_regions_with_derived_for_branch_blocks( ctx, func, scope, @@ -1548,7 +775,7 @@ pub(super) fn collect_symbolic_function_facts_with_derived<'ctx>( } #[allow(clippy::too_many_arguments)] -pub(super) fn collect_symbolic_function_facts_with_derived_for_branch_blocks<'ctx>( +pub(super) fn collect_canonical_semantic_regions_with_derived_for_branch_blocks<'ctx>( ctx: &'ctx Context, func: &SsaArtifact, scope: Option<&PreparedFunctionScope>, @@ -1558,44 +785,23 @@ pub(super) fn collect_symbolic_function_facts_with_derived_for_branch_blocks<'ct registry: &SummaryRegistry<'ctx>, derived: &DerivedSummarySet<'ctx>, symbol_map: &HashMap, -) -> SymbolicFunctionFacts { - let mut facts = collect_symbolic_function_facts_for_branch_blocks( - ctx, - func, - arch, - branch_blocks, - |explorer| { +) -> CollectedNativeSemanticRegions { + let (branch_facts, diagnostics) = + collect_branch_observations_for_branch_blocks(ctx, func, arch, branch_blocks, |explorer| { install_derived_summary_set(explorer, registry, func, scope, derived, symbol_map); - }, - ); - facts.memory_islands = merge_memory_islands( - facts.memory_islands, - derive_summary_memory_islands(func, branch_blocks, derived, &facts.diagnostics), - ); + }); let _ = summary_profile; - finalize_symbolic_function_facts(facts) -} - -fn collect_large_cfg_symbolic_function_facts( - ctx: &Context, - func: &SsaArtifact, - scope: Option<&PreparedFunctionScope>, - arch: &ArchSpec, - symbol_map: &HashMap, - summary_profile: SummaryProfile, -) -> SymbolicFunctionFacts { - collect_large_cfg_symbolic_function_facts_with_limit( - ctx, - func, - scope, - arch, - symbol_map, - summary_profile, - large_cfg_branch_limit(func), - ) + CollectedNativeSemanticRegions { + regions: build_canonical_regions( + &branch_facts, + &derive_summary_memory_terms_by_anchor(func, branch_blocks, derived), + &diagnostics, + ), + diagnostics, + } } -pub(super) fn collect_large_cfg_symbolic_function_facts_with_limit( +pub(super) fn collect_large_cfg_canonical_semantic_regions_with_limit( ctx: &Context, func: &SsaArtifact, scope: Option<&PreparedFunctionScope>, @@ -1603,12 +809,12 @@ pub(super) fn collect_large_cfg_symbolic_function_facts_with_limit( symbol_map: &HashMap, summary_profile: SummaryProfile, branch_limit: usize, -) -> SymbolicFunctionFacts { +) -> CollectedNativeSemanticRegions { let branch_blocks = limited_branch_blocks(func, branch_limit.max(1)); - let mut facts = if let Some(scope) = scope { + let mut collected = if let Some(scope) = scope { if let Some(registry) = SummaryRegistry::with_profile_for_arch(arch, summary_profile) { let derived = registry.derive_symbolic_summaries(ctx, scope, Some(arch), symbol_map); - collect_symbolic_function_facts_with_derived_for_branch_blocks( + collect_canonical_semantic_regions_with_derived_for_branch_blocks( ctx, func, Some(scope), @@ -1620,7 +826,7 @@ pub(super) fn collect_large_cfg_symbolic_function_facts_with_limit( symbol_map, ) } else { - collect_symbolic_function_facts_for_branch_blocks( + let (branch_facts, diagnostics) = collect_branch_observations_for_branch_blocks( ctx, func, arch, @@ -1636,10 +842,14 @@ pub(super) fn collect_large_cfg_symbolic_function_facts_with_limit( symbol_map, ); }, - ) + ); + CollectedNativeSemanticRegions { + regions: build_canonical_regions(&branch_facts, &BTreeMap::new(), &diagnostics), + diagnostics, + } } } else { - collect_symbolic_function_facts_for_branch_blocks( + let (branch_facts, diagnostics) = collect_branch_observations_for_branch_blocks( ctx, func, arch, @@ -1655,77 +865,53 @@ pub(super) fn collect_large_cfg_symbolic_function_facts_with_limit( symbol_map, ); }, - ) + ); + CollectedNativeSemanticRegions { + regions: build_canonical_regions(&branch_facts, &BTreeMap::new(), &diagnostics), + diagnostics, + } }; - facts.diagnostics.skipped_large_cfg = true; - finalize_symbolic_function_facts(facts) -} - -pub fn collect_symbolic_function_facts( - ctx: &Context, - func: &SsaArtifact, - arch: Option<&ArchSpec>, - symbol_map: &HashMap, -) -> SymbolicFunctionFacts { - collect_symbolic_function_facts_with_scope_and_profile( - ctx, - func, - None, - arch, - symbol_map, - SummaryProfile::Default, - ) -} - -pub fn collect_symbolic_function_facts_with_scope( - ctx: &Context, - func: &SsaArtifact, - scope: Option<&PreparedFunctionScope>, - arch: Option<&ArchSpec>, - symbol_map: &HashMap, -) -> SymbolicFunctionFacts { - collect_symbolic_function_facts_with_scope_and_profile( - ctx, - func, - scope, - arch, - symbol_map, - SummaryProfile::Default, - ) + collected.diagnostics.skipped_large_cfg = true; + collected } -pub fn collect_symbolic_function_facts_with_scope_and_profile( +pub(super) fn collect_canonical_semantic_regions_with_scope_and_profile( ctx: &Context, func: &SsaArtifact, scope: Option<&PreparedFunctionScope>, arch: Option<&ArchSpec>, symbol_map: &HashMap, summary_profile: SummaryProfile, -) -> SymbolicFunctionFacts { - let mut facts = SymbolicFunctionFacts::default(); +) -> CollectedNativeSemanticRegions { let Some(arch) = arch else { - facts.diagnostics.skipped_missing_arch = true; - return finalize_symbolic_function_facts(facts); + return CollectedNativeSemanticRegions { + regions: BTreeMap::new(), + diagnostics: SymbolicFunctionFactDiagnostics { + skipped_missing_arch: true, + ..SymbolicFunctionFactDiagnostics::default() + }, + }; }; let cfg_summary = func.function().cfg_risk_summary(); if cfg_summary.block_count > 96 || cfg_summary.switch_block_count > 8 { - return collect_large_cfg_symbolic_function_facts( + return collect_large_cfg_canonical_semantic_regions_with_limit( ctx, func, scope, arch, symbol_map, summary_profile, + large_cfg_branch_limit(func), ); } if let Some(scope) = scope { let Some(registry) = SummaryRegistry::with_profile_for_arch(arch, summary_profile) else { - return finalize_symbolic_function_facts(facts); + return CollectedNativeSemanticRegions::default(); }; let derived = registry.derive_symbolic_summaries(ctx, scope, Some(arch), symbol_map); - return collect_symbolic_function_facts_with_derived( + return collect_canonical_semantic_regions_with_derived( ctx, func, Some(scope), @@ -1736,11 +922,13 @@ pub fn collect_symbolic_function_facts_with_scope_and_profile( &derived, ); } - finalize_symbolic_function_facts(collect_symbolic_function_facts_for_branch_blocks( + + let branch_blocks = collect_branch_blocks(func); + let (branch_facts, diagnostics) = collect_branch_observations_for_branch_blocks( ctx, func, arch, - &collect_branch_blocks(func), + &branch_blocks, |explorer| { install_symbolic_fact_hooks( ctx, @@ -1752,361 +940,9 @@ pub fn collect_symbolic_function_facts_with_scope_and_profile( symbol_map, ); }, - )) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::BackwardMemoryRegion; - - fn residual_summary(expr: &str) -> BackwardConditionSummary { - BackwardConditionSummary { - simplified: expr.to_string(), - terms: vec![expr.to_string()], - memory_terms: Vec::new(), - backward_memory_substitutions: 0, - backward_memory_candidate_enumerations: 0, - backward_memory_residual_fallbacks: 0, - precision: BackwardConditionPrecision::ResidualSearchRequired, - supported_paths: 1, - total_paths: 2, - } - } - - #[test] - fn large_cfg_control_island_promotes_single_bounded_guard_to_likely() { - let facts = finalize_symbolic_function_facts(SymbolicFunctionFacts { - branch_facts: vec![SymbolicBranchFact { - block_addr: 0x1000, - true_target: 0x1010, - false_target: 0x1020, - true_status: SymbolicReachabilityStatus::Unknown, - false_status: SymbolicReachabilityStatus::Unknown, - true_condition: Some("sel == 3".to_string()), - false_condition: None, - true_compiled: Some(residual_summary("sel == 3")), - false_compiled: None, - }], - worker_islands: Vec::new(), - control_islands: Vec::new(), - memory_islands: Vec::new(), - diagnostics: SymbolicFunctionFactDiagnostics { - skipped_large_cfg: true, - ..SymbolicFunctionFactDiagnostics::default() - }, - }); - - let island = facts - .control_island_for_block(0x1000) - .expect("control island"); - assert_eq!( - island.kind, - SymbolicControlIslandKind::LargeCfgBranchFrontier - ); - assert_eq!(island.evidence.tier, SemanticConfidence::Likely); - assert!(island.actionable_compiled_condition().is_some()); - } - - #[test] - fn control_island_requires_unique_actionable_condition() { - let facts = finalize_symbolic_function_facts(SymbolicFunctionFacts { - branch_facts: vec![SymbolicBranchFact { - block_addr: 0x2000, - true_target: 0x2010, - false_target: 0x2020, - true_status: SymbolicReachabilityStatus::Unknown, - false_status: SymbolicReachabilityStatus::Unknown, - true_condition: Some("x < 4".to_string()), - false_condition: Some("x >= 4".to_string()), - true_compiled: Some(BackwardConditionSummary { - simplified: "x < 4".to_string(), - terms: vec!["x < 4".to_string()], - memory_terms: Vec::new(), - backward_memory_substitutions: 0, - backward_memory_candidate_enumerations: 0, - backward_memory_residual_fallbacks: 0, - precision: BackwardConditionPrecision::OverApprox, - supported_paths: 1, - total_paths: 2, - }), - false_compiled: Some(BackwardConditionSummary { - simplified: "x >= 4".to_string(), - terms: vec!["x >= 4".to_string()], - memory_terms: Vec::new(), - backward_memory_substitutions: 0, - backward_memory_candidate_enumerations: 0, - backward_memory_residual_fallbacks: 0, - precision: BackwardConditionPrecision::OverApprox, - supported_paths: 1, - total_paths: 2, - }), - }], - worker_islands: Vec::new(), - control_islands: Vec::new(), - memory_islands: Vec::new(), - diagnostics: SymbolicFunctionFactDiagnostics::default(), - }); - - let island = facts - .control_island_for_block(0x2000) - .expect("control island"); - assert!(island.actionable_compiled_condition().is_none()); - } - - #[test] - fn actionable_memory_terms_derive_memory_island_for_block() { - let facts = finalize_symbolic_function_facts(SymbolicFunctionFacts { - branch_facts: vec![SymbolicBranchFact { - block_addr: 0x3000, - true_target: 0x3010, - false_target: 0x3020, - true_status: SymbolicReachabilityStatus::Unknown, - false_status: SymbolicReachabilityStatus::Unknown, - true_condition: Some("arg0->f_8 == 0".to_string()), - false_condition: None, - true_compiled: Some(BackwardConditionSummary { - simplified: "arg0->f_8 == 0".to_string(), - terms: vec!["arg0->f_8 == 0".to_string()], - memory_terms: vec![BackwardMemoryCondition { - region: BackwardMemoryRegion::Argument { index: 0 }, - offset_lo: 8, - offset_hi: 8, - size: 4, - exact_offset: true, - evidence: SemanticEvidence::exact(), - binding: None, - expr: "*(arg0 + 8)".to_string(), - value_expr: Some("*(arg0 + 8)".to_string()), - exact_value: false, - }], - backward_memory_substitutions: 1, - backward_memory_candidate_enumerations: 1, - backward_memory_residual_fallbacks: 0, - precision: BackwardConditionPrecision::OverApprox, - supported_paths: 1, - total_paths: 1, - }), - false_compiled: None, - }], - worker_islands: Vec::new(), - control_islands: Vec::new(), - memory_islands: Vec::new(), - diagnostics: SymbolicFunctionFactDiagnostics::default(), - }); - - let island = facts - .memory_island_for_block(0x3000) - .expect("memory island"); - assert_eq!(island.kind, SymbolicMemoryIslandKind::ConditionFrontier); - assert_eq!(island.terms.len(), 1); - assert_eq!(island.actionable_terms().len(), 1); - assert_eq!(island.evidence.tier, SemanticConfidence::Exact); - } - - #[test] - fn actionable_memory_terms_for_target_follow_actionable_source_block() { - let facts = finalize_symbolic_function_facts(SymbolicFunctionFacts { - branch_facts: vec![SymbolicBranchFact { - block_addr: 0x3500, - true_target: 0x3600, - false_target: 0x3610, - true_status: SymbolicReachabilityStatus::Reachable, - false_status: SymbolicReachabilityStatus::Unreachable, - true_condition: Some("x == 0".to_string()), - false_condition: None, - true_compiled: Some(BackwardConditionSummary { - simplified: "x == 0".to_string(), - terms: vec!["x == 0".to_string()], - memory_terms: Vec::new(), - backward_memory_substitutions: 0, - backward_memory_candidate_enumerations: 0, - backward_memory_residual_fallbacks: 0, - precision: BackwardConditionPrecision::Exact, - supported_paths: 1, - total_paths: 1, - }), - false_compiled: None, - }], - worker_islands: Vec::new(), - control_islands: Vec::new(), - memory_islands: vec![SymbolicMemoryIsland { - kind: SymbolicMemoryIslandKind::ConditionFrontier, - anchor_block: 0x3500, - terms: vec![BackwardMemoryCondition { - region: BackwardMemoryRegion::Argument { index: 0 }, - offset_lo: 4, - offset_hi: 4, - size: 4, - exact_offset: true, - evidence: SemanticEvidence::exact(), - binding: Some("sym_mem".to_string()), - expr: "0x2a".to_string(), - value_expr: Some("0x2a".to_string()), - exact_value: true, - }], - evidence: SemanticEvidence::exact(), - }], - diagnostics: SymbolicFunctionFactDiagnostics::default(), - }); - - let terms = facts.actionable_memory_terms_for_target(0x3600); - assert_eq!(terms.len(), 1); - assert_eq!(terms[0].binding.as_deref(), Some("sym_mem")); - assert_eq!(terms[0].value_expr.as_deref(), Some("0x2a")); - } - - #[test] - fn finalize_symbolic_function_facts_preserves_preseeded_memory_islands() { - let facts = finalize_symbolic_function_facts(SymbolicFunctionFacts { - branch_facts: Vec::new(), - worker_islands: Vec::new(), - control_islands: Vec::new(), - memory_islands: vec![SymbolicMemoryIsland { - kind: SymbolicMemoryIslandKind::LargeCfgConditionFrontier, - anchor_block: 0x3500, - terms: vec![BackwardMemoryCondition { - region: BackwardMemoryRegion::Argument { index: 1 }, - offset_lo: 0x10, - offset_hi: 0x10, - size: 8, - exact_offset: true, - evidence: SemanticEvidence::exact(), - binding: None, - expr: "arg1->f_10".to_string(), - value_expr: Some("arg1->f_10".to_string()), - exact_value: false, - }], - evidence: SemanticEvidence::exact(), - }], - diagnostics: SymbolicFunctionFactDiagnostics::default(), - }); - - let island = facts - .memory_island_for_block(0x3500) - .expect("preseeded memory island"); - assert_eq!(island.terms.len(), 1); - assert_eq!( - island.kind, - SymbolicMemoryIslandKind::LargeCfgConditionFrontier - ); - assert_eq!(island.terms[0].offset_lo, 0x10); - } - - #[test] - fn actionable_condition_source_for_target_tracks_branch_frontier() { - let compiled = BackwardConditionSummary { - simplified: "x == 0".to_string(), - terms: vec!["x == 0".to_string()], - memory_terms: Vec::new(), - backward_memory_substitutions: 0, - backward_memory_candidate_enumerations: 0, - backward_memory_residual_fallbacks: 0, - precision: BackwardConditionPrecision::Exact, - supported_paths: 1, - total_paths: 1, - }; - let facts = finalize_symbolic_function_facts(SymbolicFunctionFacts { - branch_facts: vec![SymbolicBranchFact { - block_addr: 0x4000, - true_target: 0x4010, - false_target: 0x4020, - true_status: SymbolicReachabilityStatus::Reachable, - false_status: SymbolicReachabilityStatus::Unreachable, - true_condition: Some("x == 0".to_string()), - false_condition: Some("x != 0".to_string()), - true_compiled: Some(compiled.clone()), - false_compiled: None, - }], - worker_islands: Vec::new(), - control_islands: Vec::new(), - memory_islands: Vec::new(), - diagnostics: SymbolicFunctionFactDiagnostics::default(), - }); - - let source = facts - .actionable_condition_source_for_target(0x4010) - .expect("target source"); - assert_eq!(source.block_addr, 0x4000); - assert!(source.branch_truth); - assert!(source.necessary_for_target); - assert_eq!(source.summary.simplified, "x == 0"); - } - - #[test] - fn actionable_condition_source_prefers_best_candidate_but_marks_non_unique_sources() { - let exact = BackwardConditionSummary { - simplified: "x == 0".to_string(), - terms: vec!["x == 0".to_string()], - memory_terms: Vec::new(), - backward_memory_substitutions: 0, - backward_memory_candidate_enumerations: 0, - backward_memory_residual_fallbacks: 0, - precision: BackwardConditionPrecision::Exact, - supported_paths: 1, - total_paths: 1, - }; - let likely = BackwardConditionSummary { - simplified: "x <= 1".to_string(), - terms: vec!["x <= 1".to_string()], - memory_terms: vec![BackwardMemoryCondition { - region: BackwardMemoryRegion::Argument { index: 0 }, - offset_lo: 8, - offset_hi: 12, - size: 4, - exact_offset: false, - evidence: SemanticEvidence::likely(SemanticEvidenceReason::DerivedFromRanking) - .with_coverage(SemanticEvidenceCoverage::Bounded) - .with_provenance(SemanticEvidenceProvenance::Normalized), - binding: None, - expr: "*(arg0 + [8,12])".to_string(), - value_expr: Some("*(arg0 + [8,12])".to_string()), - exact_value: false, - }], - backward_memory_substitutions: 1, - backward_memory_candidate_enumerations: 1, - backward_memory_residual_fallbacks: 0, - precision: BackwardConditionPrecision::OverApprox, - supported_paths: 1, - total_paths: 2, - }; - let facts = finalize_symbolic_function_facts(SymbolicFunctionFacts { - branch_facts: vec![ - SymbolicBranchFact { - block_addr: 0x4000, - true_target: 0x4010, - false_target: 0x4020, - true_status: SymbolicReachabilityStatus::Reachable, - false_status: SymbolicReachabilityStatus::Unreachable, - true_condition: Some("x == 0".to_string()), - false_condition: Some("x != 0".to_string()), - true_compiled: Some(exact.clone()), - false_compiled: None, - }, - SymbolicBranchFact { - block_addr: 0x4018, - true_target: 0x4010, - false_target: 0x4030, - true_status: SymbolicReachabilityStatus::Reachable, - false_status: SymbolicReachabilityStatus::Unreachable, - true_condition: Some("x <= 1".to_string()), - false_condition: None, - true_compiled: Some(likely), - false_compiled: None, - }, - ], - worker_islands: Vec::new(), - control_islands: Vec::new(), - memory_islands: Vec::new(), - diagnostics: SymbolicFunctionFactDiagnostics::default(), - }); - - let source = facts - .actionable_condition_source_for_target(0x4010) - .expect("target source"); - assert_eq!(source.block_addr, 0x4000); - assert!(source.branch_truth); - assert!(!source.necessary_for_target); - assert_eq!(source.summary.simplified, "x == 0"); + ); + CollectedNativeSemanticRegions { + regions: build_canonical_regions(&branch_facts, &BTreeMap::new(), &diagnostics), + diagnostics, } } diff --git a/crates/r2sym/src/semantics/mod.rs b/crates/r2sym/src/semantics/mod.rs index 335366d..b7a68f9 100644 --- a/crates/r2sym/src/semantics/mod.rs +++ b/crates/r2sym/src/semantics/mod.rs @@ -2,23 +2,28 @@ mod artifact; mod cache; mod classify; mod compiler; -mod facts; +pub(crate) mod facts; +mod plan; +mod region; mod vm; pub use artifact::{ - CompiledFunctionSemantics, CompiledSemanticArtifact, CompiledSemanticMode, ResidualReason, - SemanticCapability, SemanticConfidence, SemanticEvidence, SemanticEvidenceAmbiguity, - SemanticEvidenceCoverage, SemanticEvidenceProvenance, SemanticEvidenceReason, - SemanticEvidenceSoundness, SemanticMode, SliceClass, + ResidualReason, SemanticArtifact, SemanticArtifactBody, SemanticConfidence, SemanticEvidence, + SemanticEvidenceAmbiguity, SemanticEvidenceCoverage, SemanticEvidenceProvenance, + SemanticEvidenceReason, SemanticEvidenceSoundness, SliceClass, }; -pub use cache::stable_scope_hash; +pub use cache::{SEMANTIC_ARTIFACT_SCHEMA_VERSION, stable_scope_hash}; pub use compiler::compile_semantic_artifact_default_with_scope; pub use compiler::{compile_function_semantics_with_scope, compile_semantic_artifact_with_scope}; -pub use facts::{ - SymbolicBranchFact, SymbolicControlFact, SymbolicControlIsland, SymbolicControlIslandKind, - SymbolicFunctionFactDiagnostics, SymbolicFunctionFacts, SymbolicMemoryIsland, - SymbolicMemoryIslandKind, SymbolicReachabilityStatus, SymbolicWorkerIsland, - collect_symbolic_function_facts, collect_symbolic_function_facts_with_scope, +pub use facts::SymbolicReachabilityStatus; +pub use plan::{ + ArtifactBuildPlan, DecompilePlan, QueryGuidanceMode, QueryPlan, TargetQueryPlan, + TargetQueryRoutePlan, TypePlan, +}; +pub use region::{ + ArtifactGranularity, ControlFact, ExecutionModel, Judged, MemoryFact, NativeArtifactBody, + NativeFunctionSummary, RefinementStage, RegionKey, SemanticArtifactDiagnostics, + SemanticPredicate, SemanticRegion, SemanticTargetConditionSource, TargetFact, VmArtifactBody, }; pub use vm::{ InterpreterDispatchSummary, InterpreterKind, VmBinaryOp, VmGuardCondition, VmGuardedExit, diff --git a/crates/r2sym/src/semantics/plan.rs b/crates/r2sym/src/semantics/plan.rs new file mode 100644 index 0000000..14f5441 --- /dev/null +++ b/crates/r2sym/src/semantics/plan.rs @@ -0,0 +1,398 @@ +use serde::{Deserialize, Serialize}; + +use super::artifact::ResidualReason; +use super::region::{ExecutionModel, RefinementStage, SemanticArtifactDiagnostics}; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum ArtifactBuildPlan { + Ready, + Fallback { reason: String }, + Residual { reasons: Vec }, + Refuse { reason: String }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum QueryPlan { + Ready, + Fallback { reason: String }, + Residual { reasons: Vec }, + Refuse { reason: String }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum TypePlan { + Ready, + Fallback { reason: String }, + Residual { reasons: Vec }, + Refuse { reason: String }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum DecompilePlan { + Ready, + Fallback { reason: String }, + Residual { reasons: Vec }, + Refuse { reason: String }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum QueryGuidanceMode { + Necessary, + NarrowOnly, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum TargetQueryPlan { + Ready { mode: QueryGuidanceMode }, + Fallback { reason: String }, + Residual { reasons: Vec }, + Refuse { reason: String }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct TargetQueryRoutePlan { + pub target_plan: TargetQueryPlan, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub branch_guidance: Option, + pub allow_memory_term_narrowing: bool, + pub allow_dynamic_target_compile: bool, + pub allow_vm_target_compile: bool, +} + +impl TargetQueryRoutePlan { + pub fn dynamic_fallback() -> Self { + Self { + target_plan: TargetQueryPlan::Fallback { + reason: "semantic artifact unavailable".to_string(), + }, + branch_guidance: None, + allow_memory_term_narrowing: false, + allow_dynamic_target_compile: true, + allow_vm_target_compile: true, + } + } +} + +fn residual_reason_strings(diagnostics: &SemanticArtifactDiagnostics) -> Vec { + diagnostics + .residual_reasons + .iter() + .map(|reason| format!("{reason:?}")) + .collect() +} + +fn has_large_cfg_only_residual(diagnostics: &SemanticArtifactDiagnostics) -> bool { + !diagnostics.residual_reasons.is_empty() + && diagnostics + .residual_reasons + .iter() + .all(|reason| matches!(reason, ResidualReason::LargeCfg)) +} + +pub fn derive_artifact_build_plan( + stage: RefinementStage, + diagnostics: &SemanticArtifactDiagnostics, +) -> ArtifactBuildPlan { + if diagnostics.skipped_missing_arch { + return ArtifactBuildPlan::Refuse { + reason: "missing architecture".to_string(), + }; + } + match stage { + RefinementStage::Residual => ArtifactBuildPlan::Residual { + reasons: residual_reason_strings(diagnostics), + }, + RefinementStage::Raw | RefinementStage::Compiled => ArtifactBuildPlan::Ready, + } +} + +pub fn derive_query_plan( + stage: RefinementStage, + execution: ExecutionModel, + diagnostics: &SemanticArtifactDiagnostics, + has_query_support: bool, +) -> QueryPlan { + if matches!(execution, ExecutionModel::Vm) || has_query_support { + QueryPlan::Ready + } else if matches!(stage, RefinementStage::Residual) { + QueryPlan::Residual { + reasons: residual_reason_strings(diagnostics), + } + } else { + QueryPlan::Refuse { + reason: "query capability unavailable".to_string(), + } + } +} + +pub fn derive_type_plan( + stage: RefinementStage, + execution: ExecutionModel, + diagnostics: &SemanticArtifactDiagnostics, + has_native_semantics: bool, +) -> TypePlan { + if matches!(execution, ExecutionModel::Vm) + || !diagnostics.skipped_large_cfg + || has_native_semantics + { + TypePlan::Ready + } else if matches!(stage, RefinementStage::Residual) { + TypePlan::Residual { + reasons: residual_reason_strings(diagnostics), + } + } else { + TypePlan::Fallback { + reason: "type capability unavailable".to_string(), + } + } +} + +pub fn derive_decompile_plan( + stage: RefinementStage, + execution: ExecutionModel, + diagnostics: &SemanticArtifactDiagnostics, + has_native_semantics: bool, +) -> DecompilePlan { + if matches!(execution, ExecutionModel::Native) + && (!diagnostics.skipped_large_cfg || has_native_semantics) + { + DecompilePlan::Ready + } else if matches!(stage, RefinementStage::Residual) + && !has_large_cfg_only_residual(diagnostics) + { + DecompilePlan::Residual { + reasons: residual_reason_strings(diagnostics), + } + } else if matches!(execution, ExecutionModel::Vm) { + DecompilePlan::Fallback { + reason: "vm consumer required".to_string(), + } + } else { + DecompilePlan::Fallback { + reason: "decompile capability unavailable".to_string(), + } + } +} + +pub fn derive_target_query_plan( + query_plan: &QueryPlan, + has_guidance: bool, + has_source_conflict: bool, + necessary_for_target: bool, +) -> TargetQueryPlan { + match query_plan { + QueryPlan::Refuse { reason } => TargetQueryPlan::Refuse { + reason: reason.clone(), + }, + QueryPlan::Residual { reasons } => TargetQueryPlan::Residual { + reasons: reasons.clone(), + }, + QueryPlan::Fallback { reason } => TargetQueryPlan::Fallback { + reason: reason.clone(), + }, + QueryPlan::Ready if has_source_conflict => TargetQueryPlan::Residual { + reasons: vec!["conflicting target guidance sources".to_string()], + }, + QueryPlan::Ready if has_guidance => TargetQueryPlan::Ready { + mode: if necessary_for_target { + QueryGuidanceMode::Necessary + } else { + QueryGuidanceMode::NarrowOnly + }, + }, + QueryPlan::Ready => TargetQueryPlan::Fallback { + reason: "target guidance unavailable".to_string(), + }, + } +} + +pub fn derive_target_query_route_plan( + query_plan: &QueryPlan, + target_plan: &TargetQueryPlan, + has_authoritative_source: bool, + has_memory_guidance: bool, +) -> TargetQueryRoutePlan { + match query_plan { + QueryPlan::Ready => match target_plan { + TargetQueryPlan::Ready { mode } => TargetQueryRoutePlan { + target_plan: target_plan.clone(), + branch_guidance: Some(*mode), + allow_memory_term_narrowing: has_authoritative_source && has_memory_guidance, + allow_dynamic_target_compile: true, + allow_vm_target_compile: true, + }, + TargetQueryPlan::Fallback { .. } => TargetQueryRoutePlan { + target_plan: target_plan.clone(), + branch_guidance: None, + allow_memory_term_narrowing: has_authoritative_source && has_memory_guidance, + allow_dynamic_target_compile: true, + allow_vm_target_compile: true, + }, + TargetQueryPlan::Residual { .. } | TargetQueryPlan::Refuse { .. } => { + TargetQueryRoutePlan { + target_plan: target_plan.clone(), + branch_guidance: None, + allow_memory_term_narrowing: false, + allow_dynamic_target_compile: false, + allow_vm_target_compile: false, + } + } + }, + QueryPlan::Fallback { .. } | QueryPlan::Residual { .. } | QueryPlan::Refuse { .. } => { + TargetQueryRoutePlan { + target_plan: target_plan.clone(), + branch_guidance: None, + allow_memory_term_narrowing: false, + allow_dynamic_target_compile: false, + allow_vm_target_compile: false, + } + } + } +} + +#[cfg(test)] +mod tests { + use proptest::prelude::*; + + use super::{ + DecompilePlan, QueryPlan, TargetQueryPlan, TargetQueryRoutePlan, derive_decompile_plan, + derive_query_plan, derive_target_query_plan, derive_target_query_route_plan, + }; + use crate::{ExecutionModel, RefinementStage, SemanticArtifactDiagnostics}; + + fn diagnostics(skipped_large_cfg: bool) -> SemanticArtifactDiagnostics { + SemanticArtifactDiagnostics { + branches_evaluated: 0, + branches_pruned: 0, + branches_unknown: 0, + skipped_missing_arch: false, + skipped_large_cfg, + residual_reasons: Vec::new(), + ambiguous_targets: Vec::new(), + cache_hit: false, + } + } + + proptest! { + #[test] + fn query_plan_derivation_is_total( + is_vm in any::(), + is_residual in any::(), + skipped_large_cfg in any::(), + has_query_support in any::(), + ) { + let plan = derive_query_plan( + if is_residual { RefinementStage::Residual } else { RefinementStage::Compiled }, + if is_vm { ExecutionModel::Vm } else { ExecutionModel::Native }, + &diagnostics(skipped_large_cfg), + has_query_support, + ); + let is_valid = match plan { + QueryPlan::Ready + | QueryPlan::Fallback { .. } + | QueryPlan::Residual { .. } + | QueryPlan::Refuse { .. } => true, + }; + prop_assert!(is_valid); + } + + #[test] + fn decompile_plan_derivation_is_total( + is_vm in any::(), + is_residual in any::(), + skipped_large_cfg in any::(), + has_native_semantics in any::(), + ) { + let plan = derive_decompile_plan( + if is_residual { RefinementStage::Residual } else { RefinementStage::Compiled }, + if is_vm { ExecutionModel::Vm } else { ExecutionModel::Native }, + &diagnostics(skipped_large_cfg), + has_native_semantics, + ); + let is_valid = match plan { + DecompilePlan::Ready + | DecompilePlan::Fallback { .. } + | DecompilePlan::Residual { .. } + | DecompilePlan::Refuse { .. } => true, + }; + prop_assert!(is_valid); + } + } + + #[test] + fn target_query_plan_refuses_conflicting_guidance() { + let plan = derive_target_query_plan(&QueryPlan::Ready, true, true, true); + assert!(matches!(plan, TargetQueryPlan::Residual { .. })); + } + + #[test] + fn target_query_route_plan_blocks_conflicting_guidance() { + let target_plan = derive_target_query_plan(&QueryPlan::Ready, true, true, true); + let route = derive_target_query_route_plan(&QueryPlan::Ready, &target_plan, true, true); + assert!(matches!( + route.target_plan, + TargetQueryPlan::Residual { .. } + )); + assert!(route.branch_guidance.is_none()); + assert!(!route.allow_memory_term_narrowing); + assert!(!route.allow_dynamic_target_compile); + assert!(!route.allow_vm_target_compile); + } + + #[test] + fn target_query_route_plan_allows_dynamic_fallback_without_branch_guidance() { + let target_plan = derive_target_query_plan(&QueryPlan::Ready, false, false, false); + let route = derive_target_query_route_plan(&QueryPlan::Ready, &target_plan, false, false); + assert!(matches!( + route.target_plan, + TargetQueryPlan::Fallback { .. } + )); + assert!(route.branch_guidance.is_none()); + assert!(!route.allow_memory_term_narrowing); + assert!(route.allow_dynamic_target_compile); + assert!(route.allow_vm_target_compile); + } + + proptest! { + #[test] + fn target_query_route_plan_is_total( + ready in any::(), + conflict in any::(), + has_guidance in any::(), + necessary in any::(), + has_authoritative_source in any::(), + has_memory_guidance in any::(), + ) { + let query_plan = if ready { + QueryPlan::Ready + } else { + QueryPlan::Residual { reasons: vec!["budget".to_string()] } + }; + let target_plan = derive_target_query_plan(&query_plan, has_guidance, conflict, necessary); + let route = derive_target_query_route_plan( + &query_plan, + &target_plan, + has_authoritative_source, + has_memory_guidance, + ); + let TargetQueryRoutePlan { .. } = route; + let valid = true; + prop_assert!(valid); + if matches!(target_plan, TargetQueryPlan::Ready { .. }) { + prop_assert_eq!( + route.allow_memory_term_narrowing, + has_authoritative_source && has_memory_guidance + ); + } + if matches!(target_plan, TargetQueryPlan::Fallback { .. }) && ready { + prop_assert_eq!( + route.allow_memory_term_narrowing, + has_authoritative_source && has_memory_guidance + ); + } + if matches!(target_plan, TargetQueryPlan::Residual { .. } | TargetQueryPlan::Refuse { .. }) || !ready { + prop_assert!(route.branch_guidance.is_none()); + } + } + } +} diff --git a/crates/r2sym/src/semantics/region.rs b/crates/r2sym/src/semantics/region.rs new file mode 100644 index 0000000..dc0a6c2 --- /dev/null +++ b/crates/r2sym/src/semantics/region.rs @@ -0,0 +1,807 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use serde::{Deserialize, Deserializer, Serialize, Serializer}; + +use crate::backward::{BackwardConditionSummary, BackwardMemoryCondition}; +use crate::sim::DerivedSummaryDiagnostics; + +use super::artifact::{ResidualReason, SemanticEvidence, SliceClass}; +use super::facts::SymbolicReachabilityStatus; +use super::vm::{InterpreterDispatchSummary, VmStepSummary}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum RefinementStage { + Raw, + Compiled, + Residual, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum ArtifactGranularity { + WholeFunction, + Regioned, + SummaryOnly, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum ExecutionModel { + Native, + Vm, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct RegionKey { + pub anchor_block: u64, + pub frontier: BTreeSet, +} + +impl RegionKey { + pub fn new(anchor_block: u64, frontier: BTreeSet) -> Self { + Self { + anchor_block, + frontier, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Judged { + pub value: T, + #[serde(default, skip_serializing_if = "SemanticEvidence::is_default_exact")] + pub evidence: SemanticEvidence, +} + +impl Judged { + pub fn new(value: T, evidence: SemanticEvidence) -> Self { + Self { value, evidence } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ControlFact { + pub target: u64, + pub status: SymbolicReachabilityStatus, + #[serde(skip_serializing_if = "Option::is_none")] + pub branch_truth: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub condition: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub compiled: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct MemoryFact { + pub term: BackwardMemoryCondition, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SemanticPredicate { + pub expr: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub compiled: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct TargetFact { + pub target: u64, + pub status: SymbolicReachabilityStatus, + #[serde(skip_serializing_if = "Option::is_none")] + pub branch_truth: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SemanticRegion { + pub anchor: u64, + pub frontier: BTreeSet, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub control: Vec>, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub memory: Vec>, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub pre: Vec>, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub post: Vec>, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub targets: Vec>, +} + +impl SemanticRegion { + pub fn key(&self) -> RegionKey { + RegionKey::new(self.anchor, self.frontier.clone()) + } + + fn compiled_condition_for_target( + &self, + target: u64, + proof_only: bool, + ) -> Option<&BackwardConditionSummary> { + self.control + .iter() + .find(|fact| fact.value.target == target) + .and_then(|fact| { + let allowed = if proof_only { + fact.evidence.allows_hard_proof() + } else { + fact.evidence.allows_narrowing() + }; + allowed.then_some(fact.value.compiled.as_ref()).flatten() + }) + } + + fn unique_compiled_condition(&self, proof_only: bool) -> Option<&BackwardConditionSummary> { + let mut candidates = self.control.iter().filter_map(|fact| { + let allowed = if proof_only { + fact.evidence.allows_hard_proof() + } else { + fact.evidence.allows_narrowing() + }; + allowed.then_some(fact.value.compiled.as_ref()).flatten() + }); + let first = candidates.next()?; + candidates + .all(|candidate| candidate == first) + .then_some(first) + } + + fn unique_reachable_target(&self, proof_only: bool) -> Option { + let mut candidates = self.control.iter().filter_map(|fact| { + let allowed = if proof_only { + fact.evidence.allows_hard_proof() + } else { + fact.evidence.allows_narrowing() + }; + allowed + .then_some(matches!( + fact.value.status, + SymbolicReachabilityStatus::Reachable + )) + .and_then(|reachable| reachable.then_some(fact.value.target)) + }); + let first = candidates.next()?; + candidates + .all(|candidate| candidate == first) + .then_some(first) + } + + pub fn exact_reachable_target(&self) -> Option { + self.unique_reachable_target(true) + } + + pub fn actionable_reachable_target(&self) -> Option { + self.unique_reachable_target(false) + } + + pub fn exact_compiled_condition(&self) -> Option<&BackwardConditionSummary> { + self.unique_compiled_condition(true) + } + + pub fn actionable_compiled_condition(&self) -> Option<&BackwardConditionSummary> { + self.unique_compiled_condition(false) + } + + pub fn exact_compiled_condition_for_target( + &self, + target: u64, + ) -> Option<&BackwardConditionSummary> { + self.compiled_condition_for_target(target, true) + } + + pub fn actionable_compiled_condition_for_target( + &self, + target: u64, + ) -> Option<&BackwardConditionSummary> { + self.compiled_condition_for_target(target, false) + } + + pub fn exact_memory_terms(&self) -> Vec<&BackwardMemoryCondition> { + self.memory + .iter() + .filter(|term| term.evidence.allows_hard_proof()) + .map(|term| &term.value.term) + .collect() + } + + pub fn actionable_memory_terms(&self) -> Vec<&BackwardMemoryCondition> { + self.memory + .iter() + .filter(|term| term.evidence.allows_narrowing()) + .map(|term| &term.value.term) + .collect() + } + + pub fn exact_memory_terms_for_target(&self, target: u64) -> Vec<&BackwardMemoryCondition> { + self.exact_compiled_condition_for_target(target) + .map(|compiled| { + compiled + .memory_terms + .iter() + .filter(|term| term.evidence().allows_hard_proof()) + .collect() + }) + .unwrap_or_default() + } + + pub fn actionable_memory_terms_for_target(&self, target: u64) -> Vec<&BackwardMemoryCondition> { + match self.actionable_compiled_condition_for_target(target) { + Some(compiled) if !compiled.memory_terms.is_empty() => compiled + .memory_terms + .iter() + .filter(|term| term.evidence().allows_narrowing()) + .collect(), + Some(_) => Vec::new(), + None => { + if self.actionable_reachable_target() == Some(target) { + self.actionable_memory_terms() + } else { + Vec::new() + } + } + } + } + + pub fn branch_truth_for_target(&self, target: u64) -> Option { + self.control + .iter() + .find(|fact| fact.value.target == target) + .and_then(|fact| fact.value.branch_truth) + } + + pub fn supports_guarded_structuring(&self) -> bool { + let reachable_target = self + .exact_reachable_target() + .or_else(|| self.actionable_reachable_target()); + let has_condition = reachable_target + .and_then(|target| self.actionable_compiled_condition_for_target(target)) + .is_some(); + let has_memory_support = reachable_target + .map(|target| !self.actionable_memory_terms_for_target(target).is_empty()) + .unwrap_or(false); + self.control + .iter() + .any(|fact| fact.evidence.allows_guarded_structuring()) + && has_condition + && has_memory_support + } + + pub fn supports_query_guidance(&self) -> bool { + self.control + .iter() + .any(|fact| fact.evidence.allows_narrowing() && fact.value.compiled.is_some()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct NativeFunctionSummary { + pub slice_class: SliceClass, + pub closure_functions: usize, + pub helper_functions: usize, + pub derived_summaries: usize, + pub derived_diagnostics: DerivedSummaryDiagnostics, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct NativeArtifactBody { + pub summary: NativeFunctionSummary, + #[serde( + default, + skip_serializing_if = "BTreeMap::is_empty", + serialize_with = "serialize_region_map", + deserialize_with = "deserialize_region_map" + )] + pub regions: BTreeMap, +} + +fn serialize_region_map( + regions: &BTreeMap, + serializer: S, +) -> Result +where + S: Serializer, +{ + regions + .values() + .cloned() + .collect::>() + .serialize(serializer) +} + +fn deserialize_region_map<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let regions = Vec::::deserialize(deserializer)?; + Ok(regions + .into_iter() + .map(|region| (region.key(), region)) + .collect()) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SemanticTargetConditionSource<'a> { + pub block_addr: u64, + pub branch_truth: bool, + pub summary: &'a BackwardConditionSummary, + pub necessary_for_target: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct SemanticTargetRegionSource<'a> { + pub region: &'a SemanticRegion, + pub branch_truth: Option, + pub summary: &'a BackwardConditionSummary, + pub necessary_for_target: bool, +} + +fn summary_strength_rank( + summary: &BackwardConditionSummary, +) -> (u8, u8, usize, usize, usize, usize) { + let strength = summary.evidence().strength_rank(); + ( + strength.0, + strength.1, + summary.memory_terms.len(), + summary.supported_paths, + usize::MAX.saturating_sub(summary.total_paths), + usize::MAX.saturating_sub(summary.simplified.len()), + ) +} + +impl NativeArtifactBody { + fn target_source_region(&self, target_addr: u64, proof_only: bool) -> Option<&SemanticRegion> { + self.authoritative_target_region_source(target_addr, proof_only) + .map(|source| source.region) + } + + fn target_memory_region_candidates(&self, target_addr: u64) -> Vec<&SemanticRegion> { + self.regions + .values() + .filter(|region| { + !region + .actionable_memory_terms_for_target(target_addr) + .is_empty() + }) + .collect() + } + + fn target_memory_sources_are_equivalent( + candidates: &[&SemanticRegion], + target_addr: u64, + ) -> bool { + let Some(first) = candidates.first() else { + return true; + }; + let first_terms = first.actionable_memory_terms_for_target(target_addr); + candidates.iter().all(|candidate| { + candidate.actionable_memory_terms_for_target(target_addr) == first_terms + }) + } + + fn authoritative_memory_region(&self, target_addr: u64) -> Option<&SemanticRegion> { + let candidates = self.target_memory_region_candidates(target_addr); + if candidates.is_empty() { + return None; + } + if !Self::target_memory_sources_are_equivalent(&candidates, target_addr) { + return None; + } + candidates.into_iter().next() + } + + fn target_region_candidates( + &self, + target_addr: u64, + proof_only: bool, + ) -> Vec> { + self.regions + .values() + .filter_map(|region| { + let necessary_for_target = if proof_only { + region.exact_reachable_target() == Some(target_addr) + } else { + region.actionable_reachable_target() == Some(target_addr) + }; + let summary = if proof_only { + region.exact_compiled_condition_for_target(target_addr) + } else { + region.actionable_compiled_condition_for_target(target_addr) + }?; + Some(SemanticTargetRegionSource { + region, + branch_truth: region.branch_truth_for_target(target_addr), + summary, + necessary_for_target, + }) + }) + .collect() + } + + fn target_sources_are_equivalent(candidates: &[SemanticTargetRegionSource<'_>]) -> bool { + let Some(first) = candidates.first() else { + return true; + }; + candidates.iter().all(|candidate| { + candidate.summary == first.summary + && !matches!( + (candidate.branch_truth, first.branch_truth), + (Some(left), Some(right)) if left != right + ) + }) + } + + fn authoritative_target_region_source( + &self, + target_addr: u64, + proof_only: bool, + ) -> Option> { + let candidates = self.target_region_candidates(target_addr, proof_only); + if candidates.is_empty() { + return None; + } + if proof_only + && candidates + .iter() + .any(|candidate| !candidate.necessary_for_target) + { + return None; + } + if !Self::target_sources_are_equivalent(&candidates) { + return None; + } + let representative = candidates.iter().copied().max_by(|left, right| { + ( + usize::from(left.necessary_for_target), + summary_strength_rank(left.summary), + usize::MAX.saturating_sub(left.region.anchor as usize), + usize::from(left.branch_truth.unwrap_or(false)), + ) + .cmp(&( + usize::from(right.necessary_for_target), + summary_strength_rank(right.summary), + usize::MAX.saturating_sub(right.region.anchor as usize), + usize::from(right.branch_truth.unwrap_or(false)), + )) + })?; + Some(SemanticTargetRegionSource { + necessary_for_target: candidates + .iter() + .all(|candidate| candidate.necessary_for_target), + ..representative + }) + } + + pub fn target_source_conflict(&self, target_addr: u64, proof_only: bool) -> bool { + let candidates = self.target_region_candidates(target_addr, proof_only); + candidates.len() > 1 && !Self::target_sources_are_equivalent(&candidates) + } + + pub fn conflicting_targets(&self, proof_only: bool) -> BTreeSet { + self.regions + .values() + .flat_map(|region| region.control.iter().map(|fact| fact.value.target)) + .filter(|target| self.target_source_conflict(*target, proof_only)) + .collect() + } + + pub fn region_for_anchor(&self, anchor: u64) -> Option<&SemanticRegion> { + self.regions.values().find(|region| region.anchor == anchor) + } + + pub fn exact_control_count(&self) -> usize { + self.regions + .values() + .flat_map(|region| region.control.iter()) + .filter(|fact| fact.evidence.allows_hard_proof()) + .count() + } + + pub fn actionable_control_count(&self) -> usize { + self.regions + .values() + .flat_map(|region| region.control.iter()) + .filter(|fact| fact.evidence.allows_narrowing()) + .count() + } + + pub fn supports_guarded_structuring(&self) -> bool { + self.regions.values().any(|region| { + region.supports_guarded_structuring() + && region + .actionable_reachable_target() + .is_some_and(|target| !self.target_source_conflict(target, false)) + }) + } + + pub fn supports_query_guidance(&self) -> bool { + self.regions + .values() + .any(SemanticRegion::supports_query_guidance) + } + + pub fn has_target_guidance(&self, target_addr: u64, proof_only: bool) -> bool { + self.target_source_region(target_addr, proof_only).is_some() + } + + pub fn target_guidance_is_necessary(&self, target_addr: u64, proof_only: bool) -> bool { + self.target_source_region(target_addr, proof_only) + .is_some_and(|region| { + if proof_only { + region.exact_reachable_target() == Some(target_addr) + } else { + region.actionable_reachable_target() == Some(target_addr) + } + }) + } + + pub fn exact_reachable_target_for_block(&self, block_addr: u64) -> Option { + self.region_for_anchor(block_addr) + .and_then(SemanticRegion::exact_reachable_target) + } + + pub fn exact_branch_truth_for_block(&self, block_addr: u64) -> Option { + let region = self.region_for_anchor(block_addr)?; + let target = region.exact_reachable_target()?; + region.branch_truth_for_target(target) + } + + pub fn actionable_reachable_target_for_block(&self, block_addr: u64) -> Option { + self.region_for_anchor(block_addr) + .and_then(SemanticRegion::actionable_reachable_target) + } + + pub fn exact_compiled_condition_for_block( + &self, + block_addr: u64, + ) -> Option<&BackwardConditionSummary> { + self.region_for_anchor(block_addr) + .and_then(SemanticRegion::exact_compiled_condition) + } + + pub fn actionable_compiled_condition_for_block( + &self, + block_addr: u64, + ) -> Option<&BackwardConditionSummary> { + self.region_for_anchor(block_addr) + .and_then(SemanticRegion::actionable_compiled_condition) + } + + pub fn actionable_memory_terms_for_block( + &self, + block_addr: u64, + ) -> Vec<&BackwardMemoryCondition> { + self.region_for_anchor(block_addr) + .map(SemanticRegion::actionable_memory_terms) + .unwrap_or_default() + } + + pub fn actionable_regions(&self) -> impl Iterator { + self.regions.values().filter(|region| { + region.supports_guarded_structuring() + && region + .actionable_reachable_target() + .is_some_and(|target| !self.target_source_conflict(target, false)) + }) + } + + pub fn target_condition_source( + &self, + target_addr: u64, + proof_only: bool, + ) -> Option> { + let representative = self.authoritative_target_region_source(target_addr, proof_only)?; + let branch_truth = representative.branch_truth?; + Some(SemanticTargetConditionSource { + block_addr: representative.region.anchor, + branch_truth, + summary: representative.summary, + necessary_for_target: representative.necessary_for_target, + }) + } + + pub fn actionable_compiled_condition_for_target( + &self, + target_addr: u64, + ) -> Option<&BackwardConditionSummary> { + self.target_source_region(target_addr, false)? + .actionable_compiled_condition_for_target(target_addr) + } + + pub fn exact_compiled_condition_for_target( + &self, + target_addr: u64, + ) -> Option<&BackwardConditionSummary> { + self.target_source_region(target_addr, true)? + .exact_compiled_condition_for_target(target_addr) + } + + pub fn actionable_memory_terms_for_target( + &self, + target_addr: u64, + ) -> Vec<&BackwardMemoryCondition> { + self.authoritative_memory_region(target_addr) + .map(|region| region.actionable_memory_terms_for_target(target_addr)) + .unwrap_or_default() + } + + pub fn authoritative_region_for_target( + &self, + target_addr: u64, + proof_only: bool, + ) -> Option<&SemanticRegion> { + self.target_source_region(target_addr, proof_only) + } + + pub fn authoritative_memory_region_for_target( + &self, + target_addr: u64, + ) -> Option<&SemanticRegion> { + self.authoritative_memory_region(target_addr) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct VmArtifactBody { + #[serde(skip_serializing_if = "Option::is_none")] + pub interpreter: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub step_summary: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub transfer_summary: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SemanticArtifactDiagnostics { + pub branches_evaluated: usize, + pub branches_pruned: usize, + pub branches_unknown: usize, + pub skipped_missing_arch: bool, + pub skipped_large_cfg: bool, + pub residual_reasons: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub ambiguous_targets: Vec, + pub cache_hit: bool, +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeSet; + + use proptest::prelude::*; + + use super::{ + ControlFact, Judged, MemoryFact, NativeArtifactBody, NativeFunctionSummary, RegionKey, + SemanticRegion, TargetFact, + }; + use crate::sim::DerivedSummaryDiagnostics; + use crate::{ + BackwardConditionPrecision, BackwardConditionSummary, BackwardMemoryCondition, + BackwardMemoryRegion, SemanticEvidence, SliceClass, SymbolicReachabilityStatus, + }; + + fn compiled_summary(tag: u64) -> BackwardConditionSummary { + BackwardConditionSummary { + simplified: format!("cond_{tag}"), + terms: vec![format!("term_{tag}")], + memory_terms: vec![BackwardMemoryCondition { + region: BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: tag as i64, + offset_hi: tag as i64, + size: 8, + exact_offset: true, + evidence: SemanticEvidence::exact(), + binding: Some(format!("arg0_{tag}")), + expr: format!("*(arg0 + 0x{tag:x})"), + value_expr: None, + exact_value: false, + }], + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + } + } + + fn worker_body(regions: impl IntoIterator) -> NativeArtifactBody { + NativeArtifactBody { + summary: NativeFunctionSummary { + slice_class: SliceClass::Worker, + closure_functions: 0, + helper_functions: 0, + derived_summaries: 0, + derived_diagnostics: DerivedSummaryDiagnostics::default(), + }, + regions: regions + .into_iter() + .map(|region| { + ( + RegionKey::new(region.anchor, region.frontier.clone()), + region, + ) + }) + .collect(), + } + } + + fn guided_region(anchor: u64, target: u64, branch_truth: bool) -> SemanticRegion { + let compiled = compiled_summary(anchor ^ target); + SemanticRegion { + anchor, + frontier: BTreeSet::from([target]), + control: vec![Judged::new( + ControlFact { + target, + status: SymbolicReachabilityStatus::Reachable, + branch_truth: Some(branch_truth), + condition: Some(compiled.simplified.clone()), + compiled: Some(compiled.clone()), + }, + SemanticEvidence::exact(), + )], + memory: compiled + .memory_terms + .iter() + .cloned() + .map(|term| Judged::new(MemoryFact { term }, SemanticEvidence::exact())) + .collect(), + pre: Vec::new(), + post: Vec::new(), + targets: vec![Judged::new( + TargetFact { + target, + status: SymbolicReachabilityStatus::Reachable, + branch_truth: Some(branch_truth), + }, + SemanticEvidence::exact(), + )], + } + } + + proptest! { + #[test] + fn conflicting_targets_are_reported_deterministically( + targets in proptest::collection::vec(0x401000u64..0x401100, 1..6), + ) { + let regions = targets + .iter() + .enumerate() + .flat_map(|(idx, target)| { + [ + guided_region(0x5000 + (idx as u64) * 2, *target, true), + guided_region(0x5001 + (idx as u64) * 2, *target, false), + ] + }) + .collect::>(); + let reversed = regions.iter().cloned().rev().collect::>(); + let expected = targets + .iter() + .copied() + .collect::>() + .into_iter() + .collect::>(); + + let forward = worker_body(regions); + let backward = worker_body(reversed); + + prop_assert_eq!( + forward.conflicting_targets(false).into_iter().collect::>(), + expected + ); + prop_assert_eq!(forward.conflicting_targets(false), backward.conflicting_targets(false)); + } + } + + #[test] + fn conflicting_target_sources_disable_guidance_and_structuring() { + let body = worker_body([ + guided_region(0x401000, 0x401100, true), + guided_region(0x401010, 0x401100, false), + ]); + + assert!(body.target_source_conflict(0x401100, false)); + assert!(!body.has_target_guidance(0x401100, false)); + assert!(!body.supports_guarded_structuring()); + } +} diff --git a/crates/r2sym/tests/symex_integration.rs b/crates/r2sym/tests/symex_integration.rs index dc940ba..b053343 100644 --- a/crates/r2sym/tests/symex_integration.rs +++ b/crates/r2sym/tests/symex_integration.rs @@ -13,11 +13,10 @@ use r2sym::sim::{ }; use r2sym::spec::{AddressValue, ExplorationSpec, InputSpec, PredicateSpec}; use r2sym::{ - BackwardConditionPrecision, BackwardMemoryCondition, BackwardMemoryRegion, - CompiledSemanticMode, ExploreConfig, PathExplorer, QueryCompletion, ReachabilityStatus, - ReplayRegisterOverlay, ReplayRegisterValue, ReplaySeed, SolveStatus, SymQueryConfig, SymState, - SymValue, SymbolicReachabilityStatus, collect_symbolic_function_facts, - collect_symbolic_function_facts_with_scope, compile_derived_summary_return_postcondition, + BackwardConditionPrecision, BackwardMemoryCondition, BackwardMemoryRegion, ExploreConfig, + PathExplorer, QueryCompletion, ReachabilityStatus, RefinementStage, ReplayRegisterOverlay, + ReplayRegisterValue, ReplaySeed, SolveStatus, SymQueryConfig, SymState, SymValue, + SymbolicReachabilityStatus, compile_derived_summary_return_postcondition, compile_function_semantics_with_scope, }; use z3::Context; @@ -42,6 +41,16 @@ fn make_const(val: u64, size: u32) -> Varnode { } } +fn region_for_anchor(artifact: &r2sym::SemanticArtifact, anchor: u64) -> &r2sym::SemanticRegion { + artifact + .native_body() + .expect("native artifact") + .regions + .values() + .find(|region| region.anchor == anchor) + .expect("semantic region") +} + fn assert_argument_memory_term( term: &BackwardMemoryCondition, index: usize, @@ -1169,31 +1178,41 @@ fn test_query_target_guided_can_reach_finds_target_under_tight_budget() { } #[test] -fn collect_symbolic_function_facts_prunes_self_xor_dead_branch() { +fn compile_function_semantics_prunes_self_xor_dead_branch() { let arch = make_x86_64_arch(); let func = SsaArtifact::for_symbolic(&make_self_xor_guard_blocks(), Some(&arch)) .expect("symbolic ssa"); let ctx = Context::thread_local(); - let facts = collect_symbolic_function_facts( + let artifact = compile_function_semantics_with_scope( &ctx, &func, + None, Some(&arch), &std::collections::HashMap::new(), + r2sym::SummaryProfile::Default, ); - let branch = facts - .branch_fact_for_block(0x1000) - .expect("entry branch fact"); - - assert_eq!(branch.true_target, 0x1010); - assert_eq!(branch.false_target, 0x1004); - assert_eq!(branch.true_status, SymbolicReachabilityStatus::Reachable); - assert_eq!(branch.false_status, SymbolicReachabilityStatus::Unreachable); - assert_eq!(facts.diagnostics.branches_pruned, 1); + let region = region_for_anchor(&artifact, 0x1000); + + assert_eq!( + region.frontier, + std::collections::BTreeSet::from([0x1004, 0x1010]) + ); + assert!(region.control.iter().any(|fact| { + fact.value.target == 0x1010 + && fact.value.status == SymbolicReachabilityStatus::Reachable + && fact.value.branch_truth == Some(true) + })); + assert!(region.control.iter().any(|fact| { + fact.value.target == 0x1004 + && fact.value.status == SymbolicReachabilityStatus::Unreachable + && fact.value.branch_truth == Some(false) + })); + assert_eq!(artifact.diagnostics.branches_pruned, 1); } #[test] -fn collect_symbolic_function_facts_with_scope_prunes_helper_return_dead_branch() { +fn compile_function_semantics_with_scope_prunes_helper_return_dead_branch() { let arch = make_x86_64_arch(); let root_blocks = vec![ R2ILBlock { @@ -1282,30 +1301,30 @@ fn collect_symbolic_function_facts_with_scope_prunes_helper_return_dead_branch() .expect("scope"); let ctx = Context::thread_local(); - let facts = collect_symbolic_function_facts_with_scope( + let artifact = compile_function_semantics_with_scope( &ctx, &root, Some(&scope), Some(&arch), &std::collections::HashMap::new(), + r2sym::SummaryProfile::Default, ); - let branch = facts - .branch_fact_for_block(0x1004) - .expect("post-call branch fact"); - - assert_eq!(branch.true_target, 0x1010); - assert_eq!(branch.false_target, 0x1008); - assert_eq!( - branch.true_status, - SymbolicReachabilityStatus::Unreachable, - "{branch:#?}" - ); + let region = region_for_anchor(&artifact, 0x1004); assert_eq!( - branch.false_status, - SymbolicReachabilityStatus::Reachable, - "{branch:#?}" + region.frontier, + std::collections::BTreeSet::from([0x1008, 0x1010]) ); - assert_eq!(facts.diagnostics.branches_pruned, 1); + assert!(region.control.iter().any(|fact| { + fact.value.target == 0x1010 + && fact.value.status == SymbolicReachabilityStatus::Unreachable + && fact.value.branch_truth == Some(true) + })); + assert!(region.control.iter().any(|fact| { + fact.value.target == 0x1008 + && fact.value.status == SymbolicReachabilityStatus::Reachable + && fact.value.branch_truth == Some(false) + })); + assert_eq!(artifact.diagnostics.branches_pruned, 1); } #[test] @@ -1407,15 +1426,20 @@ fn compile_function_semantics_with_scope_marks_helper_scope_compiled() { r2sym::SummaryProfile::Default, ); - assert_eq!(compiled.mode, CompiledSemanticMode::Compiled); - assert_eq!(compiled.closure_functions, 2); - assert_eq!(compiled.helper_functions, 1); - assert!(compiled.derived_summaries >= 1); - assert_eq!(compiled.symbolic_facts.diagnostics.branches_pruned, 1); + let summary = &compiled + .native_body() + .expect("native artifact body") + .summary; + + assert_eq!(compiled.stage, RefinementStage::Compiled); + assert_eq!(summary.closure_functions, 2); + assert_eq!(summary.helper_functions, 1); + assert!(summary.derived_summaries >= 1); + assert_eq!(compiled.diagnostics.branches_pruned, 1); } #[test] -fn collect_symbolic_function_facts_with_scope_prunes_helper_return_spilled_dead_branch() { +fn compile_function_semantics_with_scope_prunes_helper_return_spilled_dead_branch() { let arch = make_x86_64_arch(); let root_blocks = vec![ R2ILBlock { @@ -1524,34 +1548,34 @@ fn collect_symbolic_function_facts_with_scope_prunes_helper_return_spilled_dead_ .expect("scope"); let ctx = Context::thread_local(); - let facts = collect_symbolic_function_facts_with_scope( + let artifact = compile_function_semantics_with_scope( &ctx, &root, Some(&scope), Some(&arch), &std::collections::HashMap::new(), + r2sym::SummaryProfile::Default, ); - let branch = facts - .branch_fact_for_block(0x1004) - .expect("post-call branch fact"); - - assert_eq!(branch.true_target, 0x1010); - assert_eq!(branch.false_target, 0x1008); - assert_eq!( - branch.true_status, - SymbolicReachabilityStatus::Unreachable, - "{branch:#?}" - ); + let region = region_for_anchor(&artifact, 0x1004); assert_eq!( - branch.false_status, - SymbolicReachabilityStatus::Reachable, - "{branch:#?}" + region.frontier, + std::collections::BTreeSet::from([0x1008, 0x1010]) ); - assert_eq!(facts.diagnostics.branches_pruned, 1); + assert!(region.control.iter().any(|fact| { + fact.value.target == 0x1010 + && fact.value.status == SymbolicReachabilityStatus::Unreachable + && fact.value.branch_truth == Some(true) + })); + assert!(region.control.iter().any(|fact| { + fact.value.target == 0x1008 + && fact.value.status == SymbolicReachabilityStatus::Reachable + && fact.value.branch_truth == Some(false) + })); + assert_eq!(artifact.diagnostics.branches_pruned, 1); } #[test] -fn collect_symbolic_function_facts_with_scope_prunes_helper_return_dead_branch_via_eax_alias() { +fn compile_function_semantics_with_scope_prunes_helper_return_dead_branch_via_eax_alias() { let arch = make_x86_64_arch(); let root_blocks = vec![ R2ILBlock { @@ -1640,30 +1664,30 @@ fn collect_symbolic_function_facts_with_scope_prunes_helper_return_dead_branch_v .expect("scope"); let ctx = Context::thread_local(); - let facts = collect_symbolic_function_facts_with_scope( + let artifact = compile_function_semantics_with_scope( &ctx, &root, Some(&scope), Some(&arch), &std::collections::HashMap::new(), + r2sym::SummaryProfile::Default, ); - let branch = facts - .branch_fact_for_block(0x1004) - .expect("post-call branch fact"); - - assert_eq!(branch.true_target, 0x1010); - assert_eq!(branch.false_target, 0x1008); - assert_eq!( - branch.true_status, - SymbolicReachabilityStatus::Reachable, - "{branch:#?}" - ); + let region = region_for_anchor(&artifact, 0x1004); assert_eq!( - branch.false_status, - SymbolicReachabilityStatus::Unreachable, - "{branch:#?}" + region.frontier, + std::collections::BTreeSet::from([0x1008, 0x1010]) ); - assert_eq!(facts.diagnostics.branches_pruned, 1); + assert!(region.control.iter().any(|fact| { + fact.value.target == 0x1010 + && fact.value.status == SymbolicReachabilityStatus::Reachable + && fact.value.branch_truth == Some(true) + })); + assert!(region.control.iter().any(|fact| { + fact.value.target == 0x1008 + && fact.value.status == SymbolicReachabilityStatus::Unreachable + && fact.value.branch_truth == Some(false) + })); + assert_eq!(artifact.diagnostics.branches_pruned, 1); } #[test] diff --git a/crates/r2types/src/facts.rs b/crates/r2types/src/facts.rs index b64c142..b64bd85 100644 --- a/crates/r2types/src/facts.rs +++ b/crates/r2types/src/facts.rs @@ -8,1319 +8,6 @@ use crate::convert::CTypeLike; use crate::external::ExternalTypeDb; use crate::model::Signedness; -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub enum SymbolicReachabilityStatus { - Reachable, - Unreachable, - Unknown, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub enum SymbolicSemanticMode { - Raw, - Compiled, - IslandCompiled, - Residual, - VmSummary, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub enum SymbolicSemanticSliceClass { - Wrapper, - Worker, - RecursiveGroup, - InterpreterSwitch, - InterpreterIndirect, - GenericLarge, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)] -pub struct SymbolicSemanticCapability { - pub query_ready: bool, - pub type_ready: bool, - pub decompile_ready: bool, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub enum SymbolicSemanticResidualReason { - MissingArch, - LargeCfg, - SummaryBudgetExhausted, - SccBudgetExhausted, - InterpreterRequiresStepSummary, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub enum SymbolicConditionPrecision { - Exact, - OverApprox, - ResidualSearchRequired, - Unsupported, -} - -#[derive( - Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize, -)] -pub enum SymbolicSemanticConfidence { - Exact, - Likely, - Heuristic, - Residual, -} - -const fn default_symbolic_confidence_exact() -> SymbolicSemanticConfidence { - SymbolicSemanticConfidence::Exact -} - -impl SymbolicSemanticConfidence { - pub fn is_reliable(self) -> bool { - matches!(self, Self::Exact | Self::Likely) - } - - pub fn is_usable(self) -> bool { - !matches!(self, Self::Residual) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub enum SymbolicSemanticEvidenceSoundness { - Proven, - OverApprox, - Ranked, - Unknown, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub enum SymbolicSemanticEvidenceCoverage { - Full, - Partial, - Bounded, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub enum SymbolicSemanticEvidenceProvenance { - Stable, - Normalized, - Ranked, - Unstable, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub enum SymbolicSemanticEvidenceAmbiguity { - Single, - Bounded, - Ranked, - Multiple, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub enum SymbolicSemanticEvidenceReason { - LargeCfg, - SummaryBudget, - AliasAmbiguity, - ReplayOverlap, - HeapIdentityWeak, - GuardOpaque, - ValueOpaque, - TruncatedTransfer, - DerivedFromRanking, - PartialPathCoverage, - ResidualSearchRequired, -} - -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct SymbolicSemanticEvidence { - pub tier: SymbolicSemanticConfidence, - pub soundness: SymbolicSemanticEvidenceSoundness, - pub coverage: SymbolicSemanticEvidenceCoverage, - pub provenance: SymbolicSemanticEvidenceProvenance, - pub ambiguity: SymbolicSemanticEvidenceAmbiguity, - #[serde(default)] - pub budget_limited: bool, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub reasons: Vec, -} - -impl Default for SymbolicSemanticEvidence { - fn default() -> Self { - Self::exact() - } -} - -impl SymbolicSemanticEvidence { - pub fn exact() -> Self { - Self { - tier: SymbolicSemanticConfidence::Exact, - soundness: SymbolicSemanticEvidenceSoundness::Proven, - coverage: SymbolicSemanticEvidenceCoverage::Full, - provenance: SymbolicSemanticEvidenceProvenance::Stable, - ambiguity: SymbolicSemanticEvidenceAmbiguity::Single, - budget_limited: false, - reasons: Vec::new(), - } - } - - pub fn likely(reason: SymbolicSemanticEvidenceReason) -> Self { - Self { - tier: SymbolicSemanticConfidence::Likely, - soundness: SymbolicSemanticEvidenceSoundness::OverApprox, - coverage: SymbolicSemanticEvidenceCoverage::Full, - provenance: SymbolicSemanticEvidenceProvenance::Normalized, - ambiguity: SymbolicSemanticEvidenceAmbiguity::Single, - budget_limited: false, - reasons: vec![reason], - } - } - - pub fn heuristic(reason: SymbolicSemanticEvidenceReason) -> Self { - Self { - tier: SymbolicSemanticConfidence::Heuristic, - soundness: SymbolicSemanticEvidenceSoundness::Ranked, - coverage: SymbolicSemanticEvidenceCoverage::Partial, - provenance: SymbolicSemanticEvidenceProvenance::Ranked, - ambiguity: SymbolicSemanticEvidenceAmbiguity::Ranked, - budget_limited: false, - reasons: vec![reason], - } - } - - pub fn residual(reason: SymbolicSemanticEvidenceReason) -> Self { - Self { - tier: SymbolicSemanticConfidence::Residual, - soundness: SymbolicSemanticEvidenceSoundness::Unknown, - coverage: SymbolicSemanticEvidenceCoverage::Partial, - provenance: SymbolicSemanticEvidenceProvenance::Unstable, - ambiguity: SymbolicSemanticEvidenceAmbiguity::Multiple, - budget_limited: false, - reasons: vec![reason], - } - } - - pub fn with_reason(mut self, reason: SymbolicSemanticEvidenceReason) -> Self { - if !self.reasons.contains(&reason) { - self.reasons.push(reason); - } - self - } - - pub fn is_default_exact(&self) -> bool { - *self == Self::exact() - } - - pub fn is_reliable(&self) -> bool { - self.tier.is_reliable() - } - - pub fn is_usable(&self) -> bool { - self.tier.is_usable() - } - - pub fn allows_hard_proof(&self) -> bool { - matches!(self.tier, SymbolicSemanticConfidence::Exact) - } - - pub fn allows_narrowing(&self) -> bool { - matches!( - self.tier, - SymbolicSemanticConfidence::Exact | SymbolicSemanticConfidence::Likely - ) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub enum SymbolicVmUnaryOp { - Neg, - Not, - BoolNot, - ZExt, - SExt, - Trunc, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub enum SymbolicVmBinaryOp { - Add, - Sub, - Mul, - Div, - SDiv, - Rem, - SRem, - And, - Or, - Xor, - Shl, - Shr, - Eq, - Ne, - Lt, - Le, - Gt, - Ge, - PtrAdd, - PtrSub, - Piece, - Concat, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub enum SymbolicMemoryRegionKind { - Stack, - Global, - Input, - Heap, - Replay, - EscapedUnknown, -} - -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct SymbolicMemoryRegionRef { - pub id: u32, - pub kind: SymbolicMemoryRegionKind, - pub name: String, -} - -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub enum SymbolicMemoryRegion { - Argument { index: usize }, - Region(SymbolicMemoryRegionRef), -} - -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct SymbolicMemoryCondition { - pub region: SymbolicMemoryRegion, - pub offset_lo: i64, - pub offset_hi: i64, - pub size: u32, - pub exact_offset: bool, - #[serde( - default, - skip_serializing_if = "SymbolicSemanticEvidence::is_default_exact" - )] - pub evidence: SymbolicSemanticEvidence, - #[serde(default = "default_symbolic_confidence_exact")] - pub confidence: SymbolicSemanticConfidence, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub binding: Option, - pub expr: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub value_expr: Option, - #[serde(default)] - pub exact_value: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct SymbolicCompiledCondition { - pub simplified: String, - pub terms: Vec, - pub memory_terms: Vec, - pub backward_memory_substitutions: usize, - pub backward_memory_candidate_enumerations: usize, - pub backward_memory_residual_fallbacks: usize, - pub precision: SymbolicConditionPrecision, - #[serde( - default, - skip_serializing_if = "SymbolicSemanticEvidence::is_default_exact" - )] - pub evidence: SymbolicSemanticEvidence, - #[serde(default = "default_symbolic_confidence_exact")] - pub confidence: SymbolicSemanticConfidence, - pub supported_paths: usize, - pub total_paths: usize, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub enum SymbolicInterpreterKind { - SwitchDispatch, - IndirectDispatch, -} - -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct SymbolicInterpreterDispatch { - pub kind: SymbolicInterpreterKind, - pub dispatch_header: u64, - pub dispatch_targets: usize, - pub selector: Option, - pub back_edges: usize, - pub score: i32, -} - -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub enum SymbolicVmValueExpr { - Const(u64), - Var(String), - Expr(String), - Unary { - op: SymbolicVmUnaryOp, - expr: Box, - }, - Binary { - op: SymbolicVmBinaryOp, - left: Box, - right: Box, - }, -} - -impl SymbolicVmValueExpr { - fn split_version(name: &str) -> (&str, Option<&str>) { - name.rsplit_once('_') - .filter(|(_, version)| version.chars().all(|ch| ch.is_ascii_digit())) - .map_or((name, None), |(base, version)| (base, Some(version))) - } - - fn same_logical_name(left: &str, right: &str) -> bool { - Self::split_version(left) - .0 - .eq_ignore_ascii_case(Self::split_version(right).0) - } - - fn lookup_binding(bindings: &BTreeMap, name: &str) -> Option { - bindings.get(name).copied().or_else(|| { - bindings.iter().find_map(|(candidate, value)| { - Self::same_logical_name(candidate, name).then_some(*value) - }) - }) - } - - pub fn render(&self) -> String { - match self { - Self::Const(value) => format!("0x{value:x}"), - Self::Var(name) | Self::Expr(name) => name.clone(), - Self::Unary { op, expr } => { - let inner = expr.render(); - match op { - SymbolicVmUnaryOp::Neg => format!("(-{inner})"), - SymbolicVmUnaryOp::Not => format!("(~{inner})"), - SymbolicVmUnaryOp::BoolNot => format!("(!{inner})"), - SymbolicVmUnaryOp::ZExt => format!("zext({inner})"), - SymbolicVmUnaryOp::SExt => format!("sext({inner})"), - SymbolicVmUnaryOp::Trunc => format!("trunc({inner})"), - } - } - Self::Binary { op, left, right } => { - let left = left.render(); - let right = right.render(); - let symbol = match op { - SymbolicVmBinaryOp::Add => "+", - SymbolicVmBinaryOp::Sub => "-", - SymbolicVmBinaryOp::Mul => "*", - SymbolicVmBinaryOp::Div | SymbolicVmBinaryOp::SDiv => "/", - SymbolicVmBinaryOp::Rem | SymbolicVmBinaryOp::SRem => "%", - SymbolicVmBinaryOp::And => "&", - SymbolicVmBinaryOp::Or => "|", - SymbolicVmBinaryOp::Xor => "^", - SymbolicVmBinaryOp::Shl => "<<", - SymbolicVmBinaryOp::Shr => ">>", - SymbolicVmBinaryOp::Eq => "==", - SymbolicVmBinaryOp::Ne => "!=", - SymbolicVmBinaryOp::Lt => "<", - SymbolicVmBinaryOp::Le => "<=", - SymbolicVmBinaryOp::Gt => ">", - SymbolicVmBinaryOp::Ge => ">=", - SymbolicVmBinaryOp::PtrAdd => "+", - SymbolicVmBinaryOp::PtrSub => "-", - SymbolicVmBinaryOp::Piece => "piece", - SymbolicVmBinaryOp::Concat => "concat", - }; - match op { - SymbolicVmBinaryOp::Piece | SymbolicVmBinaryOp::Concat => { - format!("{symbol}({left}, {right})") - } - SymbolicVmBinaryOp::PtrAdd | SymbolicVmBinaryOp::PtrSub => { - format!("({left} {symbol} {right})") - } - _ => format!("({left} {symbol} {right})"), - } - } - } - } - - pub fn evaluate_u64(&self, bindings: &BTreeMap) -> Option { - match self { - Self::Const(value) => Some(*value), - Self::Var(name) => Self::lookup_binding(bindings, name), - Self::Unary { op, expr } => { - let value = expr.evaluate_u64(bindings)?; - Some(match op { - SymbolicVmUnaryOp::Neg => value.wrapping_neg(), - SymbolicVmUnaryOp::Not => !value, - SymbolicVmUnaryOp::BoolNot => u64::from(value == 0), - SymbolicVmUnaryOp::ZExt - | SymbolicVmUnaryOp::SExt - | SymbolicVmUnaryOp::Trunc => value, - }) - } - Self::Binary { op, left, right } => { - let left = left.evaluate_u64(bindings)?; - let right = right.evaluate_u64(bindings)?; - Some(match op { - SymbolicVmBinaryOp::Add | SymbolicVmBinaryOp::PtrAdd => { - left.wrapping_add(right) - } - SymbolicVmBinaryOp::Sub | SymbolicVmBinaryOp::PtrSub => { - left.wrapping_sub(right) - } - SymbolicVmBinaryOp::Mul => left.wrapping_mul(right), - SymbolicVmBinaryOp::Div => left.checked_div(right)?, - SymbolicVmBinaryOp::SDiv => ((left as i64).checked_div(right as i64)?) as u64, - SymbolicVmBinaryOp::Rem => left.checked_rem(right)?, - SymbolicVmBinaryOp::SRem => ((left as i64).checked_rem(right as i64)?) as u64, - SymbolicVmBinaryOp::And => left & right, - SymbolicVmBinaryOp::Or => left | right, - SymbolicVmBinaryOp::Xor => left ^ right, - SymbolicVmBinaryOp::Shl => left.wrapping_shl((right & 63) as u32), - SymbolicVmBinaryOp::Shr => left.wrapping_shr((right & 63) as u32), - SymbolicVmBinaryOp::Eq => u64::from(left == right), - SymbolicVmBinaryOp::Ne => u64::from(left != right), - SymbolicVmBinaryOp::Lt => u64::from(left < right), - SymbolicVmBinaryOp::Le => u64::from(left <= right), - SymbolicVmBinaryOp::Gt => u64::from(left > right), - SymbolicVmBinaryOp::Ge => u64::from(left >= right), - SymbolicVmBinaryOp::Piece | SymbolicVmBinaryOp::Concat => return None, - }) - } - Self::Expr(_) => None, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct SymbolicVmStateUpdate { - pub output: String, - pub expr: String, - pub value: SymbolicVmValueExpr, - pub exact: bool, - #[serde( - default, - skip_serializing_if = "SymbolicSemanticEvidence::is_default_exact" - )] - pub evidence: SymbolicSemanticEvidence, - #[serde(default = "default_symbolic_confidence_exact")] - pub confidence: SymbolicSemanticConfidence, -} - -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct SymbolicVmGuardCondition { - pub expr: String, - pub value: SymbolicVmValueExpr, - pub expect_nonzero: bool, - pub exact: bool, - #[serde( - default, - skip_serializing_if = "SymbolicSemanticEvidence::is_default_exact" - )] - pub evidence: SymbolicSemanticEvidence, - #[serde(default = "default_symbolic_confidence_exact")] - pub confidence: SymbolicSemanticConfidence, -} - -impl SymbolicVmGuardCondition { - pub fn evaluate(&self, bindings: &BTreeMap) -> Option { - let value = self.value.evaluate_u64(bindings)?; - Some((value != 0) == self.expect_nonzero) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct SymbolicVmGuardedExit { - pub target: u64, - pub guard: SymbolicVmGuardCondition, -} - -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct SymbolicVmTransferArm { - pub handler_target: u64, - pub case_values: Vec, - pub region_blocks: Vec, - pub exit_targets: Vec, - pub exit_guards: Vec, - pub state_updates: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub selector_update: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub memory_reads: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub memory_writes: Vec, - #[serde(default)] - pub residual_guards: bool, - #[serde(default)] - pub residual_memory_effects: bool, - pub exact: bool, - #[serde( - default, - skip_serializing_if = "SymbolicSemanticEvidence::is_default_exact" - )] - pub evidence: SymbolicSemanticEvidence, - #[serde(default = "default_symbolic_confidence_exact")] - pub confidence: SymbolicSemanticConfidence, - pub redispatch: bool, - pub may_return: bool, - pub truncated: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct SymbolicVmStepSummary { - pub kind: SymbolicInterpreterKind, - pub loop_header: u64, - pub dispatch_header: u64, - pub selector: Option, - pub dispatch_targets: Vec, - pub default_target: Option, - pub case_values_by_target: BTreeMap>, - pub loop_latches: Vec, - pub state_inputs: Vec, - pub state_outputs: Vec, - pub step_blocks: Vec, - pub handler_regions: BTreeMap>, - pub handler_state_inputs: BTreeMap>, - pub handler_state_outputs: BTreeMap>, - pub handler_state_updates: BTreeMap>, - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub handler_exit_guards: BTreeMap>, - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub handler_memory_read_effects: BTreeMap>, - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub handler_memory_write_effects: BTreeMap>, - pub handler_memory_reads: BTreeMap, - pub handler_memory_writes: BTreeMap, - pub handler_calls: BTreeMap, - pub handler_conditional_branches: BTreeMap, - pub handler_exit_targets: BTreeMap>, - pub redispatch_handlers: Vec, - pub returning_handlers: Vec, - pub truncated_handlers: Vec, - pub transfers: Vec, -} -pub type SymbolicVmTransferSummary = SymbolicVmStepSummary; - -impl SymbolicVmTransferArm { - pub fn exact_exit_guard_for_target(&self, target: u64) -> Option<&SymbolicVmGuardCondition> { - self.exit_guards - .iter() - .find(|guard| guard.target == target && guard.guard.evidence.allows_hard_proof()) - .map(|guard| &guard.guard) - } - - pub fn actionable_exit_guard_for_target( - &self, - target: u64, - ) -> Option<&SymbolicVmGuardCondition> { - self.exit_guards - .iter() - .find(|guard| guard.target == target && guard.guard.evidence.allows_narrowing()) - .map(|guard| &guard.guard) - } - - pub fn exact_exit_guard_result_for_case( - &self, - selector: Option<&str>, - case_value: u64, - target: u64, - ) -> Option { - let guard = self.exact_exit_guard_for_target(target)?; - let mut bindings = BTreeMap::new(); - if let Some(selector) = selector { - bindings.insert(selector.to_string(), case_value); - } - guard.evaluate(&bindings) - } -} - -#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct SymbolicFactDiagnostics { - pub branches_evaluated: usize, - pub branches_pruned: usize, - pub branches_unknown: usize, - pub skipped_missing_arch: bool, - pub skipped_large_cfg: bool, - #[serde(default)] - pub cache_hit: bool, - #[serde(skip_serializing_if = "Option::is_none")] - pub semantic_mode: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub semantic_capability: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub slice_class: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub residual_reasons: Vec, - pub closure_functions: usize, - pub helper_functions: usize, - pub derived_summaries: usize, - pub summary_attempted: usize, - pub summary_budget_exhausted: usize, - pub summary_scc_count: usize, -} - -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct SymbolicBranchFact { - pub block_addr: u64, - pub true_target: u64, - pub false_target: u64, - pub true_status: SymbolicReachabilityStatus, - pub false_status: SymbolicReachabilityStatus, - pub true_condition: Option, - pub false_condition: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub true_compiled: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub false_compiled: Option, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub enum SymbolicControlIslandKind { - BranchFrontier, - LargeCfgBranchFrontier, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub enum SymbolicMemoryIslandKind { - ConditionFrontier, - LargeCfgConditionFrontier, -} - -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct SymbolicControlFact { - pub target: u64, - pub status: SymbolicReachabilityStatus, - #[serde(skip_serializing_if = "Option::is_none")] - pub condition: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub compiled: Option, - #[serde( - default, - skip_serializing_if = "SymbolicSemanticEvidence::is_default_exact" - )] - pub evidence: SymbolicSemanticEvidence, - #[serde(default = "default_symbolic_confidence_exact")] - pub confidence: SymbolicSemanticConfidence, -} - -impl SymbolicControlFact { - pub fn exact_compiled_condition(&self) -> Option<&SymbolicCompiledCondition> { - self.evidence - .allows_hard_proof() - .then_some(self.compiled.as_ref()) - .flatten() - } - - pub fn actionable_compiled_condition(&self) -> Option<&SymbolicCompiledCondition> { - self.evidence - .allows_narrowing() - .then_some(self.compiled.as_ref()) - .flatten() - } -} - -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct SymbolicControlIsland { - pub kind: SymbolicControlIslandKind, - pub anchor_block: u64, - pub frontier_targets: Vec, - pub facts: Vec, - #[serde( - default, - skip_serializing_if = "SymbolicSemanticEvidence::is_default_exact" - )] - pub evidence: SymbolicSemanticEvidence, - #[serde(default = "default_symbolic_confidence_exact")] - pub confidence: SymbolicSemanticConfidence, -} - -impl SymbolicControlIsland { - pub fn exact_reachable_target(&self) -> Option { - unique_reachable_control_target(self.facts.iter(), true) - } - - pub fn actionable_reachable_target(&self) -> Option { - unique_reachable_control_target(self.facts.iter(), false) - } - - pub fn exact_compiled_condition(&self) -> Option<&SymbolicCompiledCondition> { - unique_compiled_control_condition(self.facts.iter(), true) - } - - pub fn actionable_compiled_condition(&self) -> Option<&SymbolicCompiledCondition> { - unique_compiled_control_condition(self.facts.iter(), false) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct SymbolicMemoryIsland { - pub kind: SymbolicMemoryIslandKind, - pub anchor_block: u64, - pub terms: Vec, - #[serde( - default, - skip_serializing_if = "SymbolicSemanticEvidence::is_default_exact" - )] - pub evidence: SymbolicSemanticEvidence, - #[serde(default = "default_symbolic_confidence_exact")] - pub confidence: SymbolicSemanticConfidence, -} - -impl SymbolicMemoryIsland { - pub fn exact_terms(&self) -> Vec<&SymbolicMemoryCondition> { - self.terms - .iter() - .filter(|term| term.evidence.allows_hard_proof()) - .collect() - } - - pub fn actionable_terms(&self) -> Vec<&SymbolicMemoryCondition> { - self.terms - .iter() - .filter(|term| term.evidence.allows_narrowing()) - .collect() - } -} - -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct SymbolicWorkerIsland { - pub anchor_block: u64, - #[serde(skip_serializing_if = "Option::is_none")] - pub control_kind: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub memory_kind: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub frontier_targets: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub control_facts: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub memory_terms: Vec, - #[serde( - default, - skip_serializing_if = "SymbolicSemanticEvidence::is_default_exact" - )] - pub evidence: SymbolicSemanticEvidence, - #[serde(default = "default_symbolic_confidence_exact")] - pub confidence: SymbolicSemanticConfidence, -} - -impl SymbolicWorkerIsland { - pub fn exact_reachable_target(&self) -> Option { - unique_reachable_control_target(self.control_facts.iter(), true) - } - - pub fn actionable_reachable_target(&self) -> Option { - unique_reachable_control_target(self.control_facts.iter(), false) - } - - pub fn exact_compiled_condition(&self) -> Option<&SymbolicCompiledCondition> { - unique_compiled_control_condition(self.control_facts.iter(), true) - } - - pub fn actionable_compiled_condition(&self) -> Option<&SymbolicCompiledCondition> { - unique_compiled_control_condition(self.control_facts.iter(), false) - } - - pub fn exact_terms(&self) -> Vec<&SymbolicMemoryCondition> { - self.memory_terms - .iter() - .filter(|term| term.evidence.allows_hard_proof()) - .collect() - } - - pub fn actionable_terms(&self) -> Vec<&SymbolicMemoryCondition> { - self.memory_terms - .iter() - .filter(|term| term.evidence.allows_narrowing()) - .collect() - } -} - -fn worker_island_supporting_compiled_condition( - island: &SymbolicWorkerIsland, -) -> Option<&SymbolicCompiledCondition> { - let reachable_target = island - .exact_reachable_target() - .or_else(|| island.actionable_reachable_target())?; - island - .control_facts - .iter() - .find(|fact| fact.target == reachable_target) - .and_then(SymbolicControlFact::actionable_compiled_condition) -} - -fn worker_island_supports_structured_decompile(island: &SymbolicWorkerIsland) -> bool { - let has_unique_target = - island.exact_reachable_target().is_some() || island.actionable_reachable_target().is_some(); - let supporting_condition = worker_island_supporting_compiled_condition(island); - let has_condition = supporting_condition.is_some(); - let has_memory_support = !island.actionable_terms().is_empty() - || supporting_condition.is_some_and(|compiled| !compiled.memory_terms.is_empty()); - island.evidence.allows_narrowing() && has_unique_target && has_condition && has_memory_support -} - -#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub struct SymbolicSemanticFacts { - pub branch_facts: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub worker_islands: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub control_islands: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub memory_islands: Vec, - pub diagnostics: SymbolicFactDiagnostics, - #[serde(skip_serializing_if = "Option::is_none")] - pub interpreter: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub vm_step: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub vm_transfer: Option, -} - -impl SymbolicSemanticFacts { - pub fn is_empty(&self) -> bool { - self.branch_facts.is_empty() - && self.worker_islands.is_empty() - && self.control_islands.is_empty() - && self.memory_islands.is_empty() - && self.diagnostics == SymbolicFactDiagnostics::default() - && self.interpreter.is_none() - && self.vm_step.is_none() - && self.vm_transfer.is_none() - } - - pub fn branch_fact_for_block(&self, block_addr: u64) -> Option<&SymbolicBranchFact> { - self.branch_facts - .iter() - .find(|fact| fact.block_addr == block_addr) - } - - pub fn control_island_for_block(&self, block_addr: u64) -> Option<&SymbolicControlIsland> { - self.control_islands - .iter() - .find(|island| island.anchor_block == block_addr) - } - - pub fn worker_island_for_block(&self, block_addr: u64) -> Option<&SymbolicWorkerIsland> { - self.worker_islands - .iter() - .find(|island| island.anchor_block == block_addr) - } - - pub fn memory_island_for_block(&self, block_addr: u64) -> Option<&SymbolicMemoryIsland> { - self.memory_islands - .iter() - .find(|island| island.anchor_block == block_addr) - } - - pub fn exact_compiled_condition_for_block( - &self, - block_addr: u64, - ) -> Option<&SymbolicCompiledCondition> { - self.branch_fact_for_block(block_addr) - .and_then(SymbolicBranchFact::exact_compiled_condition) - .or_else(|| { - self.worker_island_for_block(block_addr) - .and_then(SymbolicWorkerIsland::exact_compiled_condition) - }) - .or_else(|| { - self.control_island_for_block(block_addr) - .and_then(SymbolicControlIsland::exact_compiled_condition) - }) - } - - pub fn exact_reachable_target_for_block(&self, block_addr: u64) -> Option { - self.branch_fact_for_block(block_addr) - .and_then(SymbolicBranchFact::exact_reachable_target) - .or_else(|| { - self.worker_island_for_block(block_addr) - .and_then(SymbolicWorkerIsland::exact_reachable_target) - }) - .or_else(|| { - self.control_island_for_block(block_addr) - .and_then(SymbolicControlIsland::exact_reachable_target) - }) - } - - pub fn actionable_reachable_target_for_block(&self, block_addr: u64) -> Option { - self.branch_fact_for_block(block_addr) - .and_then(SymbolicBranchFact::actionable_reachable_target) - .or_else(|| { - self.worker_island_for_block(block_addr) - .and_then(SymbolicWorkerIsland::actionable_reachable_target) - }) - .or_else(|| { - self.control_island_for_block(block_addr) - .and_then(SymbolicControlIsland::actionable_reachable_target) - }) - } - - pub fn actionable_compiled_condition_for_block( - &self, - block_addr: u64, - ) -> Option<&SymbolicCompiledCondition> { - self.branch_fact_for_block(block_addr) - .and_then(SymbolicBranchFact::actionable_compiled_condition) - .or_else(|| { - self.worker_island_for_block(block_addr) - .and_then(SymbolicWorkerIsland::actionable_compiled_condition) - }) - .or_else(|| { - self.control_island_for_block(block_addr) - .and_then(SymbolicControlIsland::actionable_compiled_condition) - }) - } - - pub fn actionable_memory_terms_for_block( - &self, - block_addr: u64, - ) -> Vec<&SymbolicMemoryCondition> { - self.worker_island_for_block(block_addr) - .map(SymbolicWorkerIsland::actionable_terms) - .or_else(|| { - self.memory_island_for_block(block_addr) - .map(SymbolicMemoryIsland::actionable_terms) - }) - .unwrap_or_default() - } - - pub fn actionable_compiled_condition_for_target( - &self, - target_addr: u64, - ) -> Option<&SymbolicCompiledCondition> { - target_compiled_condition(self, target_addr, false) - } - - pub fn exact_compiled_condition_for_target( - &self, - target_addr: u64, - ) -> Option<&SymbolicCompiledCondition> { - target_compiled_condition(self, target_addr, true) - } - - pub fn vm_step_for_dispatch_header( - &self, - dispatch_header: u64, - ) -> Option<&SymbolicVmStepSummary> { - self.vm_step - .as_ref() - .filter(|vm_step| vm_step.dispatch_header == dispatch_header) - } - - pub fn vm_transfer_for_dispatch_header( - &self, - dispatch_header: u64, - ) -> Option<&SymbolicVmTransferSummary> { - self.vm_transfer - .as_ref() - .filter(|vm_transfer| vm_transfer.dispatch_header == dispatch_header) - } - - pub fn semantic_mode(&self) -> Option { - self.diagnostics.semantic_mode - } - - pub fn semantic_capability(&self) -> Option { - self.diagnostics.semantic_capability - } - - pub fn island_compiled(&self) -> bool { - matches!( - self.semantic_mode(), - Some(SymbolicSemanticMode::IslandCompiled) - ) - } - - pub fn query_ready(&self) -> bool { - self.semantic_capability() - .is_some_and(|capability| capability.query_ready) - } - - pub fn type_ready(&self) -> bool { - self.semantic_capability() - .is_some_and(|capability| capability.type_ready) - } - - pub fn decompile_ready(&self) -> bool { - self.semantic_capability() - .is_some_and(|capability| capability.decompile_ready) - } - - pub fn structured_decompile_ready(&self) -> bool { - if !self.decompile_ready() || !self.diagnostics.skipped_large_cfg { - return false; - } - self.worker_islands - .iter() - .any(worker_island_supports_structured_decompile) - } - - pub fn slice_class(&self) -> Option { - self.diagnostics.slice_class - } - - pub fn requires_type_fallback(&self) -> bool { - self.semantic_capability().is_some() && !self.type_ready() - } - - pub fn prefers_bounded_type_plan(&self) -> bool { - if self.requires_type_fallback() { - return true; - } - matches!( - self.semantic_mode(), - Some(SymbolicSemanticMode::Residual | SymbolicSemanticMode::IslandCompiled) - ) && self.diagnostics.skipped_large_cfg - && matches!(self.slice_class(), Some(SymbolicSemanticSliceClass::Worker)) - && (!self.worker_islands.is_empty() || !self.memory_islands.is_empty()) - && (self.actionable_compiled_condition_count() > 0 - || self - .worker_islands - .iter() - .any(|island| !island.actionable_terms().is_empty()) - || self - .memory_islands - .iter() - .any(|island| !island.actionable_terms().is_empty())) - } - - pub fn exact_compiled_condition_count(&self) -> usize { - if self.worker_islands.is_empty() { - self.control_islands - .iter() - .flat_map(|island| island.facts.iter()) - .filter(|fact| fact.evidence.allows_hard_proof()) - .count() - } else { - self.worker_islands - .iter() - .flat_map(|island| island.control_facts.iter()) - .filter(|fact| fact.evidence.allows_hard_proof()) - .count() - } - } - - pub fn actionable_compiled_condition_count(&self) -> usize { - if self.worker_islands.is_empty() { - self.control_islands - .iter() - .flat_map(|island| island.facts.iter()) - .filter(|fact| fact.evidence.allows_narrowing()) - .count() - } else { - self.worker_islands - .iter() - .flat_map(|island| island.control_facts.iter()) - .filter(|fact| fact.evidence.allows_narrowing()) - .count() - } - } -} - -impl SymbolicBranchFact { - pub fn exact_reachable_target(&self) -> Option { - match (self.true_status, self.false_status) { - (SymbolicReachabilityStatus::Reachable, SymbolicReachabilityStatus::Unreachable) => { - Some(self.true_target) - } - (SymbolicReachabilityStatus::Unreachable, SymbolicReachabilityStatus::Reachable) => { - Some(self.false_target) - } - _ => None, - } - } - - pub fn actionable_reachable_target(&self) -> Option { - match (self.true_status, self.false_status) { - (SymbolicReachabilityStatus::Reachable, SymbolicReachabilityStatus::Unreachable) - if self - .true_compiled - .as_ref() - .is_some_and(|compiled| compiled.evidence.allows_narrowing()) => - { - Some(self.true_target) - } - (SymbolicReachabilityStatus::Unreachable, SymbolicReachabilityStatus::Reachable) - if self - .false_compiled - .as_ref() - .is_some_and(|compiled| compiled.evidence.allows_narrowing()) => - { - Some(self.false_target) - } - _ => None, - } - } - - pub fn exact_compiled_condition(&self) -> Option<&SymbolicCompiledCondition> { - match (self.true_status, self.false_status) { - (SymbolicReachabilityStatus::Reachable, SymbolicReachabilityStatus::Unreachable) => { - self.true_compiled - .as_ref() - .filter(|compiled| compiled.evidence.allows_hard_proof()) - } - (SymbolicReachabilityStatus::Unreachable, SymbolicReachabilityStatus::Reachable) => { - self.false_compiled - .as_ref() - .filter(|compiled| compiled.evidence.allows_hard_proof()) - } - _ => None, - } - } - - pub fn actionable_compiled_condition(&self) -> Option<&SymbolicCompiledCondition> { - match (self.true_status, self.false_status) { - (SymbolicReachabilityStatus::Reachable, SymbolicReachabilityStatus::Unreachable) => { - self.true_compiled - .as_ref() - .filter(|compiled| compiled.evidence.allows_narrowing()) - } - (SymbolicReachabilityStatus::Unreachable, SymbolicReachabilityStatus::Reachable) => { - self.false_compiled - .as_ref() - .filter(|compiled| compiled.evidence.allows_narrowing()) - } - _ => None, - } - } -} - -fn unique_compiled_control_condition<'a>( - facts: impl Iterator, - hard_proof_only: bool, -) -> Option<&'a SymbolicCompiledCondition> { - let mut candidates = facts.filter_map(|fact| { - if hard_proof_only { - fact.exact_compiled_condition() - } else { - fact.actionable_compiled_condition() - } - }); - let first = candidates.next()?; - candidates.next().is_none().then_some(first) -} - -fn compiled_condition_precision_rank(condition: &SymbolicCompiledCondition) -> u8 { - match condition.precision { - SymbolicConditionPrecision::Exact => 3, - SymbolicConditionPrecision::OverApprox => 2, - SymbolicConditionPrecision::ResidualSearchRequired => 1, - SymbolicConditionPrecision::Unsupported => 0, - } -} - -fn compiled_condition_evidence_rank(condition: &SymbolicCompiledCondition) -> u8 { - match condition.evidence.tier { - SymbolicSemanticConfidence::Exact => 3, - SymbolicSemanticConfidence::Likely => 2, - SymbolicSemanticConfidence::Heuristic => 1, - SymbolicSemanticConfidence::Residual => 0, - } -} - -fn best_target_compiled_condition<'a>( - candidates: impl Iterator, -) -> Option<&'a SymbolicCompiledCondition> { - candidates.max_by(|left, right| { - ( - compiled_condition_evidence_rank(left), - compiled_condition_precision_rank(left), - std::cmp::Reverse(left.backward_memory_residual_fallbacks), - left.memory_terms.len(), - left.supported_paths, - std::cmp::Reverse(left.total_paths), - std::cmp::Reverse(left.simplified.len()), - ) - .cmp(&( - compiled_condition_evidence_rank(right), - compiled_condition_precision_rank(right), - std::cmp::Reverse(right.backward_memory_residual_fallbacks), - right.memory_terms.len(), - right.supported_paths, - std::cmp::Reverse(right.total_paths), - std::cmp::Reverse(right.simplified.len()), - )) - }) -} - -fn target_compiled_condition( - facts: &SymbolicSemanticFacts, - target_addr: u64, - hard_proof_only: bool, -) -> Option<&SymbolicCompiledCondition> { - let branch_candidates = facts.branch_facts.iter().flat_map(|fact| { - let mut candidates = Vec::new(); - if fact.true_target == target_addr { - let compiled = if hard_proof_only { - fact.true_compiled - .as_ref() - .filter(|compiled| compiled.evidence.allows_hard_proof()) - } else { - fact.true_compiled - .as_ref() - .filter(|compiled| compiled.evidence.allows_narrowing()) - }; - if let Some(compiled) = compiled { - candidates.push(compiled); - } - } - if fact.false_target == target_addr { - let compiled = if hard_proof_only { - fact.false_compiled - .as_ref() - .filter(|compiled| compiled.evidence.allows_hard_proof()) - } else { - fact.false_compiled - .as_ref() - .filter(|compiled| compiled.evidence.allows_narrowing()) - }; - if let Some(compiled) = compiled { - candidates.push(compiled); - } - } - candidates - }); - let control_candidates = facts - .worker_islands - .iter() - .flat_map(|island| island.control_facts.iter()) - .chain( - facts - .control_islands - .iter() - .flat_map(|island| island.facts.iter()), - ) - .filter_map(move |fact| { - (fact.target == target_addr) - .then_some(if hard_proof_only { - fact.exact_compiled_condition() - } else { - fact.actionable_compiled_condition() - }) - .flatten() - }); - best_target_compiled_condition(branch_candidates.chain(control_candidates)) -} - -fn unique_reachable_control_target<'a>( - facts: impl Iterator, - hard_proof_only: bool, -) -> Option { - let mut reachable_target = None; - let mut saw_any = false; - for fact in facts { - saw_any = true; - if hard_proof_only && !fact.evidence.allows_hard_proof() { - return None; - } - if !hard_proof_only && !fact.evidence.allows_narrowing() { - return None; - } - match fact.status { - SymbolicReachabilityStatus::Reachable => { - if reachable_target.replace(fact.target).is_some() { - return None; - } - } - SymbolicReachabilityStatus::Unreachable => {} - SymbolicReachabilityStatus::Unknown => return None, - } - } - saw_any.then_some(reachable_target).flatten() -} - #[derive(Debug, Clone, PartialEq, Eq)] pub struct FunctionType { pub return_type: CTypeLike, @@ -1498,7 +185,6 @@ pub struct FunctionTypeFacts { pub external_type_db: ExternalTypeDb, pub slot_type_overrides: HashMap, pub slot_field_profiles: HashMap>, - pub symbolic_facts: SymbolicSemanticFacts, pub interproc_diagnostics: InterprocFactDiagnostics, pub diagnostics: Vec, } @@ -1515,7 +201,6 @@ pub struct FunctionTypeFactInputs { pub external_type_db: ExternalTypeDb, pub slot_type_overrides: HashMap, pub slot_field_profiles: HashMap>, - pub symbolic_facts: SymbolicSemanticFacts, pub local_field_accesses: Vec, pub interproc_diagnostics: InterprocFactDiagnostics, pub diagnostics: Vec, @@ -1541,7 +226,6 @@ impl FunctionTypeFacts { && self.external_type_db.diagnostics.is_empty() && self.slot_type_overrides.is_empty() && self.slot_field_profiles.is_empty() - && self.symbolic_facts.is_empty() && self.interproc_diagnostics == InterprocFactDiagnostics::default() && self.diagnostics.is_empty() } @@ -1558,7 +242,6 @@ impl FunctionTypeFacts { external_type_db: self.external_type_db, slot_type_overrides: self.slot_type_overrides, slot_field_profiles: self.slot_field_profiles, - symbolic_facts: self.symbolic_facts, local_field_accesses: Vec::new(), interproc_diagnostics: self.interproc_diagnostics, diagnostics: self.diagnostics, @@ -1593,7 +276,6 @@ impl FunctionTypeFactsBuilder { external_type_db, slot_type_overrides, slot_field_profiles, - symbolic_facts, interproc_diagnostics, diagnostics, .. @@ -1624,7 +306,6 @@ impl FunctionTypeFactsBuilder { external_type_db, slot_type_overrides, slot_field_profiles, - symbolic_facts, interproc_diagnostics, diagnostics, } @@ -1993,502 +674,4 @@ mod tests { ); assert_eq!(parse_type_like_spec("void const *", 64), Some(void_ptr)); } - - #[test] - fn symbolic_vm_value_expr_renders_recursive_forms() { - let expr = SymbolicVmValueExpr::Binary { - op: SymbolicVmBinaryOp::Add, - left: Box::new(SymbolicVmValueExpr::Unary { - op: SymbolicVmUnaryOp::Neg, - expr: Box::new(SymbolicVmValueExpr::Const(0x10)), - }), - right: Box::new(SymbolicVmValueExpr::Binary { - op: SymbolicVmBinaryOp::Xor, - left: Box::new(SymbolicVmValueExpr::Var("state".to_string())), - right: Box::new(SymbolicVmValueExpr::Expr("mask".to_string())), - }), - }; - - assert_eq!(expr.render(), "((-0x10) + (state ^ mask))"); - } - - #[test] - fn symbolic_vm_value_expr_evaluates_recursive_forms() { - let expr = SymbolicVmValueExpr::Binary { - op: SymbolicVmBinaryOp::Add, - left: Box::new(SymbolicVmValueExpr::Var("state_0".to_string())), - right: Box::new(SymbolicVmValueExpr::Binary { - op: SymbolicVmBinaryOp::Mul, - left: Box::new(SymbolicVmValueExpr::Const(2)), - right: Box::new(SymbolicVmValueExpr::Const(3)), - }), - }; - let bindings = BTreeMap::from([(String::from("state"), 4u64)]); - assert_eq!(expr.evaluate_u64(&bindings), Some(10)); - } - - #[test] - fn symbolic_vm_transfer_arm_evaluates_exact_exit_guard_for_case() { - let arm = SymbolicVmTransferArm { - handler_target: 0x1004, - case_values: vec![1], - region_blocks: vec![0x1004], - exit_targets: vec![0x1010], - exit_guards: vec![SymbolicVmGuardedExit { - target: 0x1010, - guard: SymbolicVmGuardCondition { - expr: "(vm.sel == 0x1)".to_string(), - value: SymbolicVmValueExpr::Binary { - op: SymbolicVmBinaryOp::Eq, - left: Box::new(SymbolicVmValueExpr::Var("vm.sel".to_string())), - right: Box::new(SymbolicVmValueExpr::Const(1)), - }, - expect_nonzero: true, - exact: true, - evidence: SymbolicSemanticEvidence::exact(), - confidence: SymbolicSemanticConfidence::Exact, - }, - }], - state_updates: Vec::new(), - selector_update: None, - memory_reads: Vec::new(), - memory_writes: Vec::new(), - residual_guards: false, - residual_memory_effects: false, - exact: true, - evidence: SymbolicSemanticEvidence::exact(), - confidence: SymbolicSemanticConfidence::Exact, - redispatch: false, - may_return: true, - truncated: false, - }; - - assert_eq!( - arm.exact_exit_guard_result_for_case(Some("vm.sel"), 1, 0x1010), - Some(true) - ); - assert_eq!( - arm.exact_exit_guard_result_for_case(Some("vm.sel"), 2, 0x1010), - Some(false) - ); - } - - #[test] - fn control_island_actionable_condition_requires_unique_fact() { - let compiled = SymbolicCompiledCondition { - simplified: "false".to_string(), - terms: vec!["false".to_string()], - memory_terms: Vec::new(), - backward_memory_substitutions: 0, - backward_memory_candidate_enumerations: 0, - backward_memory_residual_fallbacks: 0, - precision: SymbolicConditionPrecision::OverApprox, - evidence: SymbolicSemanticEvidence::likely( - SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: SymbolicSemanticConfidence::Likely, - supported_paths: 1, - total_paths: 2, - }; - let island = SymbolicControlIsland { - kind: SymbolicControlIslandKind::LargeCfgBranchFrontier, - anchor_block: 0x401000, - frontier_targets: vec![0x401010, 0x401020], - facts: vec![ - SymbolicControlFact { - target: 0x401010, - status: SymbolicReachabilityStatus::Unknown, - condition: Some("false".to_string()), - compiled: Some(compiled.clone()), - evidence: SymbolicSemanticEvidence::likely( - SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: SymbolicSemanticConfidence::Likely, - }, - SymbolicControlFact { - target: 0x401020, - status: SymbolicReachabilityStatus::Unknown, - condition: None, - compiled: None, - evidence: SymbolicSemanticEvidence::residual( - SymbolicSemanticEvidenceReason::GuardOpaque, - ), - confidence: SymbolicSemanticConfidence::Residual, - }, - ], - evidence: SymbolicSemanticEvidence::likely( - SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: SymbolicSemanticConfidence::Likely, - }; - - assert_eq!( - island - .actionable_compiled_condition() - .map(|cond| cond.simplified.as_str()), - Some("false") - ); - } - - #[test] - fn semantic_facts_actionable_condition_for_block_uses_control_island_fallback() { - let compiled = SymbolicCompiledCondition { - simplified: "x == 0".to_string(), - terms: vec!["x == 0".to_string()], - memory_terms: Vec::new(), - backward_memory_substitutions: 0, - backward_memory_candidate_enumerations: 0, - backward_memory_residual_fallbacks: 0, - precision: SymbolicConditionPrecision::Exact, - evidence: SymbolicSemanticEvidence::exact(), - confidence: SymbolicSemanticConfidence::Exact, - supported_paths: 1, - total_paths: 1, - }; - let facts = SymbolicSemanticFacts { - branch_facts: Vec::new(), - worker_islands: Vec::new(), - control_islands: vec![SymbolicControlIsland { - kind: SymbolicControlIslandKind::LargeCfgBranchFrontier, - anchor_block: 0x401000, - frontier_targets: vec![0x401020], - facts: vec![SymbolicControlFact { - target: 0x401020, - status: SymbolicReachabilityStatus::Reachable, - condition: Some("x == 0".to_string()), - compiled: Some(compiled.clone()), - evidence: SymbolicSemanticEvidence::exact(), - confidence: SymbolicSemanticConfidence::Exact, - }], - evidence: SymbolicSemanticEvidence::exact(), - confidence: SymbolicSemanticConfidence::Exact, - }], - memory_islands: Vec::new(), - diagnostics: SymbolicFactDiagnostics::default(), - interpreter: None, - vm_step: None, - vm_transfer: None, - }; - - assert_eq!( - facts - .actionable_compiled_condition_for_block(0x401000) - .map(|cond| cond.simplified.as_str()), - Some("x == 0") - ); - } - - #[test] - fn semantic_facts_actionable_condition_for_target_prefers_best_candidate() { - let exact = SymbolicCompiledCondition { - simplified: "x == 0".to_string(), - terms: vec!["x == 0".to_string()], - memory_terms: Vec::new(), - backward_memory_substitutions: 0, - backward_memory_candidate_enumerations: 0, - backward_memory_residual_fallbacks: 0, - precision: SymbolicConditionPrecision::Exact, - evidence: SymbolicSemanticEvidence::exact(), - confidence: SymbolicSemanticConfidence::Exact, - supported_paths: 1, - total_paths: 1, - }; - let likely = SymbolicCompiledCondition { - simplified: "x <= 1".to_string(), - terms: vec!["x <= 1".to_string()], - memory_terms: vec![SymbolicMemoryCondition { - region: SymbolicMemoryRegion::Argument { index: 0 }, - offset_lo: 8, - offset_hi: 12, - size: 4, - exact_offset: false, - expr: "*(arg0 + [8,12])".to_string(), - binding: None, - value_expr: None, - exact_value: false, - evidence: SymbolicSemanticEvidence::likely( - SymbolicSemanticEvidenceReason::DerivedFromRanking, - ), - confidence: SymbolicSemanticConfidence::Likely, - }], - backward_memory_substitutions: 1, - backward_memory_candidate_enumerations: 1, - backward_memory_residual_fallbacks: 0, - precision: SymbolicConditionPrecision::OverApprox, - evidence: SymbolicSemanticEvidence::likely( - SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: SymbolicSemanticConfidence::Likely, - supported_paths: 1, - total_paths: 2, - }; - let facts = SymbolicSemanticFacts { - branch_facts: vec![ - SymbolicBranchFact { - block_addr: 0x401000, - true_target: 0x401010, - false_target: 0x401020, - true_status: SymbolicReachabilityStatus::Reachable, - false_status: SymbolicReachabilityStatus::Unreachable, - true_condition: Some("x == 0".to_string()), - false_condition: Some("x != 0".to_string()), - true_compiled: Some(exact.clone()), - false_compiled: None, - }, - SymbolicBranchFact { - block_addr: 0x401030, - true_target: 0x401010, - false_target: 0x401040, - true_status: SymbolicReachabilityStatus::Reachable, - false_status: SymbolicReachabilityStatus::Unreachable, - true_condition: Some("x <= 1".to_string()), - false_condition: None, - true_compiled: Some(likely), - false_compiled: None, - }, - ], - worker_islands: Vec::new(), - control_islands: Vec::new(), - memory_islands: Vec::new(), - diagnostics: SymbolicFactDiagnostics::default(), - interpreter: None, - vm_step: None, - vm_transfer: None, - }; - - assert_eq!( - facts - .actionable_compiled_condition_for_target(0x401010) - .map(|cond| cond.simplified.as_str()), - Some("x == 0") - ); - } - - #[test] - fn semantic_facts_exact_reachable_target_for_block_uses_control_island_fallback() { - let facts = SymbolicSemanticFacts { - branch_facts: Vec::new(), - worker_islands: Vec::new(), - control_islands: vec![SymbolicControlIsland { - kind: SymbolicControlIslandKind::LargeCfgBranchFrontier, - anchor_block: 0x401000, - frontier_targets: vec![0x401010, 0x401020], - facts: vec![ - SymbolicControlFact { - target: 0x401010, - status: SymbolicReachabilityStatus::Reachable, - condition: Some("x == 0".to_string()), - compiled: None, - evidence: SymbolicSemanticEvidence::exact(), - confidence: SymbolicSemanticConfidence::Exact, - }, - SymbolicControlFact { - target: 0x401020, - status: SymbolicReachabilityStatus::Unreachable, - condition: Some("!(x == 0)".to_string()), - compiled: None, - evidence: SymbolicSemanticEvidence::exact(), - confidence: SymbolicSemanticConfidence::Exact, - }, - ], - evidence: SymbolicSemanticEvidence::exact(), - confidence: SymbolicSemanticConfidence::Exact, - }], - memory_islands: Vec::new(), - diagnostics: SymbolicFactDiagnostics::default(), - interpreter: None, - vm_step: None, - vm_transfer: None, - }; - - assert_eq!( - facts.exact_reachable_target_for_block(0x401000), - Some(0x401010) - ); - } - - #[test] - fn semantic_facts_actionable_reachable_target_for_block_uses_likely_control_island() { - let compiled = SymbolicCompiledCondition { - simplified: "x == 0".to_string(), - terms: vec!["x == 0".to_string()], - memory_terms: Vec::new(), - backward_memory_substitutions: 0, - backward_memory_candidate_enumerations: 0, - backward_memory_residual_fallbacks: 0, - precision: SymbolicConditionPrecision::OverApprox, - evidence: SymbolicSemanticEvidence::likely( - SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: SymbolicSemanticConfidence::Likely, - supported_paths: 1, - total_paths: 2, - }; - let facts = SymbolicSemanticFacts { - branch_facts: Vec::new(), - worker_islands: Vec::new(), - control_islands: vec![SymbolicControlIsland { - kind: SymbolicControlIslandKind::LargeCfgBranchFrontier, - anchor_block: 0x401000, - frontier_targets: vec![0x401010, 0x401020], - facts: vec![ - SymbolicControlFact { - target: 0x401010, - status: SymbolicReachabilityStatus::Reachable, - condition: Some("x == 0".to_string()), - compiled: Some(compiled.clone()), - evidence: SymbolicSemanticEvidence::likely( - SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: SymbolicSemanticConfidence::Likely, - }, - SymbolicControlFact { - target: 0x401020, - status: SymbolicReachabilityStatus::Unreachable, - condition: Some("!(x == 0)".to_string()), - compiled: Some(compiled), - evidence: SymbolicSemanticEvidence::likely( - SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: SymbolicSemanticConfidence::Likely, - }, - ], - evidence: SymbolicSemanticEvidence::likely( - SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: SymbolicSemanticConfidence::Likely, - }], - memory_islands: Vec::new(), - diagnostics: SymbolicFactDiagnostics::default(), - interpreter: None, - vm_step: None, - vm_transfer: None, - }; - - assert_eq!( - facts.actionable_reachable_target_for_block(0x401000), - Some(0x401010) - ); - } - - #[test] - fn semantic_facts_actionable_memory_terms_for_block_use_memory_island_fallback() { - let facts = SymbolicSemanticFacts { - branch_facts: Vec::new(), - worker_islands: Vec::new(), - control_islands: Vec::new(), - memory_islands: vec![SymbolicMemoryIsland { - kind: SymbolicMemoryIslandKind::LargeCfgConditionFrontier, - anchor_block: 0x401000, - terms: vec![SymbolicMemoryCondition { - region: SymbolicMemoryRegion::Argument { index: 0 }, - offset_lo: 8, - offset_hi: 8, - size: 4, - exact_offset: true, - evidence: SymbolicSemanticEvidence::exact(), - confidence: SymbolicSemanticConfidence::Exact, - binding: Some("arg0".to_string()), - expr: "*(arg0 + 8)".to_string(), - value_expr: None, - exact_value: false, - }], - evidence: SymbolicSemanticEvidence::exact(), - confidence: SymbolicSemanticConfidence::Exact, - }], - diagnostics: SymbolicFactDiagnostics::default(), - interpreter: None, - vm_step: None, - vm_transfer: None, - }; - - let terms = facts.actionable_memory_terms_for_block(0x401000); - assert_eq!(terms.len(), 1); - assert_eq!(terms[0].offset_lo, 8); - assert_eq!(terms[0].binding.as_deref(), Some("arg0")); - } - - #[test] - fn structured_decompile_ready_allows_supported_worker_with_wide_frontier() { - let facts = SymbolicSemanticFacts { - worker_islands: vec![SymbolicWorkerIsland { - anchor_block: 0x401000, - control_kind: Some(SymbolicControlIslandKind::LargeCfgBranchFrontier), - memory_kind: Some(SymbolicMemoryIslandKind::LargeCfgConditionFrontier), - frontier_targets: vec![0x401010, 0x401020, 0x401030], - control_facts: vec![SymbolicControlFact { - target: 0x401010, - status: SymbolicReachabilityStatus::Reachable, - condition: Some("x == 0".to_string()), - compiled: Some(SymbolicCompiledCondition { - simplified: "x == 0".to_string(), - terms: vec!["x == 0".to_string()], - memory_terms: vec![SymbolicMemoryCondition { - region: SymbolicMemoryRegion::Argument { index: 0 }, - offset_lo: 0, - offset_hi: 0, - size: 1, - exact_offset: true, - evidence: SymbolicSemanticEvidence::likely( - SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: SymbolicSemanticConfidence::Likely, - binding: None, - expr: "*arg0".to_string(), - value_expr: Some("0x0:8".to_string()), - exact_value: true, - }], - backward_memory_substitutions: 0, - backward_memory_candidate_enumerations: 0, - backward_memory_residual_fallbacks: 0, - precision: SymbolicConditionPrecision::OverApprox, - evidence: SymbolicSemanticEvidence::likely( - SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: SymbolicSemanticConfidence::Likely, - supported_paths: 1, - total_paths: 2, - }), - evidence: SymbolicSemanticEvidence::likely( - SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: SymbolicSemanticConfidence::Likely, - }], - memory_terms: vec![SymbolicMemoryCondition { - region: SymbolicMemoryRegion::Argument { index: 0 }, - offset_lo: 0, - offset_hi: 0, - size: 1, - exact_offset: true, - evidence: SymbolicSemanticEvidence::likely( - SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: SymbolicSemanticConfidence::Likely, - binding: None, - expr: "*arg0".to_string(), - value_expr: Some("0x0:8".to_string()), - exact_value: true, - }], - evidence: SymbolicSemanticEvidence::likely( - SymbolicSemanticEvidenceReason::PartialPathCoverage, - ), - confidence: SymbolicSemanticConfidence::Likely, - }], - diagnostics: SymbolicFactDiagnostics { - skipped_large_cfg: true, - semantic_mode: Some(SymbolicSemanticMode::IslandCompiled), - semantic_capability: Some(SymbolicSemanticCapability { - query_ready: true, - type_ready: true, - decompile_ready: true, - }), - slice_class: Some(SymbolicSemanticSliceClass::Worker), - residual_reasons: Vec::new(), - ..Default::default() - }, - ..Default::default() - }; - - assert!(facts.structured_decompile_ready()); - } } diff --git a/crates/r2types/src/from_sym.rs b/crates/r2types/src/from_sym.rs deleted file mode 100644 index 50e72be..0000000 --- a/crates/r2types/src/from_sym.rs +++ /dev/null @@ -1,666 +0,0 @@ -use crate::facts::{ - SymbolicBranchFact, SymbolicCompiledCondition, SymbolicConditionPrecision, SymbolicControlFact, - SymbolicControlIsland, SymbolicControlIslandKind, SymbolicFactDiagnostics, - SymbolicInterpreterDispatch, SymbolicInterpreterKind, SymbolicMemoryCondition, - SymbolicMemoryIsland, SymbolicMemoryIslandKind, SymbolicMemoryRegion, SymbolicMemoryRegionKind, - SymbolicMemoryRegionRef, SymbolicReachabilityStatus, SymbolicSemanticCapability, - SymbolicSemanticConfidence, SymbolicSemanticEvidence, SymbolicSemanticEvidenceAmbiguity, - SymbolicSemanticEvidenceCoverage, SymbolicSemanticEvidenceProvenance, - SymbolicSemanticEvidenceReason, SymbolicSemanticEvidenceSoundness, SymbolicSemanticFacts, - SymbolicSemanticMode, SymbolicSemanticResidualReason, SymbolicSemanticSliceClass, - SymbolicVmBinaryOp, SymbolicVmGuardCondition, SymbolicVmGuardedExit, SymbolicVmStateUpdate, - SymbolicVmStepSummary, SymbolicVmTransferArm, SymbolicVmUnaryOp, SymbolicVmValueExpr, - SymbolicWorkerIsland, -}; - -fn symbolic_reachability_status_from_sym( - status: r2sym::SymbolicReachabilityStatus, -) -> SymbolicReachabilityStatus { - match status { - r2sym::SymbolicReachabilityStatus::Reachable => SymbolicReachabilityStatus::Reachable, - r2sym::SymbolicReachabilityStatus::Unreachable => SymbolicReachabilityStatus::Unreachable, - r2sym::SymbolicReachabilityStatus::Unknown => SymbolicReachabilityStatus::Unknown, - } -} - -fn symbolic_condition_precision_from_sym( - precision: r2sym::BackwardConditionPrecision, -) -> SymbolicConditionPrecision { - match precision { - r2sym::BackwardConditionPrecision::Exact => SymbolicConditionPrecision::Exact, - r2sym::BackwardConditionPrecision::OverApprox => SymbolicConditionPrecision::OverApprox, - r2sym::BackwardConditionPrecision::ResidualSearchRequired => { - SymbolicConditionPrecision::ResidualSearchRequired - } - r2sym::BackwardConditionPrecision::Unsupported => SymbolicConditionPrecision::Unsupported, - } -} - -fn symbolic_memory_region_kind_from_sym( - kind: &r2sym::MemoryRegionKind, -) -> SymbolicMemoryRegionKind { - match kind { - r2sym::MemoryRegionKind::Stack => SymbolicMemoryRegionKind::Stack, - r2sym::MemoryRegionKind::Global => SymbolicMemoryRegionKind::Global, - r2sym::MemoryRegionKind::Input => SymbolicMemoryRegionKind::Input, - r2sym::MemoryRegionKind::Heap => SymbolicMemoryRegionKind::Heap, - r2sym::MemoryRegionKind::Replay => SymbolicMemoryRegionKind::Replay, - r2sym::MemoryRegionKind::EscapedUnknown => SymbolicMemoryRegionKind::EscapedUnknown, - } -} - -fn symbolic_memory_region_from_sym(region: &r2sym::BackwardMemoryRegion) -> SymbolicMemoryRegion { - match region { - r2sym::BackwardMemoryRegion::Argument { index } => { - SymbolicMemoryRegion::Argument { index: *index } - } - r2sym::BackwardMemoryRegion::Region(region) => { - SymbolicMemoryRegion::Region(SymbolicMemoryRegionRef { - id: region.id.0, - kind: symbolic_memory_region_kind_from_sym(®ion.kind), - name: region.name.clone(), - }) - } - } -} - -fn symbolic_confidence_from_sym( - confidence: r2sym::SemanticConfidence, -) -> SymbolicSemanticConfidence { - match confidence { - r2sym::SemanticConfidence::Exact => SymbolicSemanticConfidence::Exact, - r2sym::SemanticConfidence::Likely => SymbolicSemanticConfidence::Likely, - r2sym::SemanticConfidence::Heuristic => SymbolicSemanticConfidence::Heuristic, - r2sym::SemanticConfidence::Residual => SymbolicSemanticConfidence::Residual, - } -} - -fn symbolic_evidence_reason_from_sym( - reason: r2sym::SemanticEvidenceReason, -) -> SymbolicSemanticEvidenceReason { - match reason { - r2sym::SemanticEvidenceReason::LargeCfg => SymbolicSemanticEvidenceReason::LargeCfg, - r2sym::SemanticEvidenceReason::SummaryBudget => { - SymbolicSemanticEvidenceReason::SummaryBudget - } - r2sym::SemanticEvidenceReason::AliasAmbiguity => { - SymbolicSemanticEvidenceReason::AliasAmbiguity - } - r2sym::SemanticEvidenceReason::ReplayOverlap => { - SymbolicSemanticEvidenceReason::ReplayOverlap - } - r2sym::SemanticEvidenceReason::HeapIdentityWeak => { - SymbolicSemanticEvidenceReason::HeapIdentityWeak - } - r2sym::SemanticEvidenceReason::GuardOpaque => SymbolicSemanticEvidenceReason::GuardOpaque, - r2sym::SemanticEvidenceReason::ValueOpaque => SymbolicSemanticEvidenceReason::ValueOpaque, - r2sym::SemanticEvidenceReason::TruncatedTransfer => { - SymbolicSemanticEvidenceReason::TruncatedTransfer - } - r2sym::SemanticEvidenceReason::DerivedFromRanking => { - SymbolicSemanticEvidenceReason::DerivedFromRanking - } - r2sym::SemanticEvidenceReason::PartialPathCoverage => { - SymbolicSemanticEvidenceReason::PartialPathCoverage - } - r2sym::SemanticEvidenceReason::ResidualSearchRequired => { - SymbolicSemanticEvidenceReason::ResidualSearchRequired - } - } -} - -fn symbolic_evidence_from_sym(evidence: &r2sym::SemanticEvidence) -> SymbolicSemanticEvidence { - SymbolicSemanticEvidence { - tier: symbolic_confidence_from_sym(evidence.tier), - soundness: match evidence.soundness { - r2sym::SemanticEvidenceSoundness::Proven => SymbolicSemanticEvidenceSoundness::Proven, - r2sym::SemanticEvidenceSoundness::OverApprox => { - SymbolicSemanticEvidenceSoundness::OverApprox - } - r2sym::SemanticEvidenceSoundness::Ranked => SymbolicSemanticEvidenceSoundness::Ranked, - r2sym::SemanticEvidenceSoundness::Unknown => SymbolicSemanticEvidenceSoundness::Unknown, - }, - coverage: match evidence.coverage { - r2sym::SemanticEvidenceCoverage::Full => SymbolicSemanticEvidenceCoverage::Full, - r2sym::SemanticEvidenceCoverage::Partial => SymbolicSemanticEvidenceCoverage::Partial, - r2sym::SemanticEvidenceCoverage::Bounded => SymbolicSemanticEvidenceCoverage::Bounded, - }, - provenance: match evidence.provenance { - r2sym::SemanticEvidenceProvenance::Stable => SymbolicSemanticEvidenceProvenance::Stable, - r2sym::SemanticEvidenceProvenance::Normalized => { - SymbolicSemanticEvidenceProvenance::Normalized - } - r2sym::SemanticEvidenceProvenance::Ranked => SymbolicSemanticEvidenceProvenance::Ranked, - r2sym::SemanticEvidenceProvenance::Unstable => { - SymbolicSemanticEvidenceProvenance::Unstable - } - }, - ambiguity: match evidence.ambiguity { - r2sym::SemanticEvidenceAmbiguity::Single => SymbolicSemanticEvidenceAmbiguity::Single, - r2sym::SemanticEvidenceAmbiguity::Bounded => SymbolicSemanticEvidenceAmbiguity::Bounded, - r2sym::SemanticEvidenceAmbiguity::Ranked => SymbolicSemanticEvidenceAmbiguity::Ranked, - r2sym::SemanticEvidenceAmbiguity::Multiple => { - SymbolicSemanticEvidenceAmbiguity::Multiple - } - }, - budget_limited: evidence.budget_limited, - reasons: evidence - .reasons - .iter() - .copied() - .map(symbolic_evidence_reason_from_sym) - .collect(), - } -} - -fn symbolic_compiled_condition_from_sym( - summary: &r2sym::BackwardConditionSummary, -) -> SymbolicCompiledCondition { - let evidence = summary.evidence(); - SymbolicCompiledCondition { - simplified: summary.simplified.clone(), - terms: summary.terms.clone(), - memory_terms: summary - .memory_terms - .iter() - .map(|term| SymbolicMemoryCondition { - region: symbolic_memory_region_from_sym(&term.region), - offset_lo: term.offset_lo, - offset_hi: term.offset_hi, - size: term.size, - exact_offset: term.exact_offset, - evidence: symbolic_evidence_from_sym(&term.evidence()), - confidence: symbolic_confidence_from_sym(term.confidence()), - binding: term.binding.clone(), - expr: term.expr.clone(), - value_expr: term.value_expr.clone(), - exact_value: term.exact_value, - }) - .collect(), - backward_memory_substitutions: summary.backward_memory_substitutions, - backward_memory_candidate_enumerations: summary.backward_memory_candidate_enumerations, - backward_memory_residual_fallbacks: summary.backward_memory_residual_fallbacks, - precision: symbolic_condition_precision_from_sym(summary.precision), - evidence: symbolic_evidence_from_sym(&evidence), - confidence: symbolic_confidence_from_sym(evidence.tier), - supported_paths: summary.supported_paths, - total_paths: summary.total_paths, - } -} - -fn symbolic_control_island_kind_from_sym( - kind: r2sym::SymbolicControlIslandKind, -) -> SymbolicControlIslandKind { - match kind { - r2sym::SymbolicControlIslandKind::BranchFrontier => { - SymbolicControlIslandKind::BranchFrontier - } - r2sym::SymbolicControlIslandKind::LargeCfgBranchFrontier => { - SymbolicControlIslandKind::LargeCfgBranchFrontier - } - } -} - -fn symbolic_control_fact_from_sym(fact: &r2sym::SymbolicControlFact) -> SymbolicControlFact { - SymbolicControlFact { - target: fact.target, - status: symbolic_reachability_status_from_sym(fact.status), - condition: fact.condition.clone(), - compiled: fact - .compiled - .as_ref() - .map(symbolic_compiled_condition_from_sym), - evidence: symbolic_evidence_from_sym(&fact.evidence), - confidence: symbolic_confidence_from_sym(fact.evidence.tier), - } -} - -fn symbolic_memory_island_kind_from_sym( - kind: r2sym::SymbolicMemoryIslandKind, -) -> SymbolicMemoryIslandKind { - match kind { - r2sym::SymbolicMemoryIslandKind::ConditionFrontier => { - SymbolicMemoryIslandKind::ConditionFrontier - } - r2sym::SymbolicMemoryIslandKind::LargeCfgConditionFrontier => { - SymbolicMemoryIslandKind::LargeCfgConditionFrontier - } - } -} - -fn symbolic_control_island_from_sym( - island: &r2sym::SymbolicControlIsland, -) -> SymbolicControlIsland { - SymbolicControlIsland { - kind: symbolic_control_island_kind_from_sym(island.kind), - anchor_block: island.anchor_block, - frontier_targets: island.frontier_targets.clone(), - facts: island - .facts - .iter() - .map(symbolic_control_fact_from_sym) - .collect(), - evidence: symbolic_evidence_from_sym(&island.evidence), - confidence: symbolic_confidence_from_sym(island.evidence.tier), - } -} - -fn symbolic_memory_island_from_sym(island: &r2sym::SymbolicMemoryIsland) -> SymbolicMemoryIsland { - SymbolicMemoryIsland { - kind: symbolic_memory_island_kind_from_sym(island.kind), - anchor_block: island.anchor_block, - terms: island - .terms - .iter() - .map(|term| SymbolicMemoryCondition { - region: symbolic_memory_region_from_sym(&term.region), - offset_lo: term.offset_lo, - offset_hi: term.offset_hi, - size: term.size, - exact_offset: term.exact_offset, - evidence: symbolic_evidence_from_sym(&term.evidence()), - confidence: symbolic_confidence_from_sym(term.confidence()), - binding: term.binding.clone(), - expr: term.expr.clone(), - value_expr: term.value_expr.clone(), - exact_value: term.exact_value, - }) - .collect(), - evidence: symbolic_evidence_from_sym(&island.evidence), - confidence: symbolic_confidence_from_sym(island.evidence.tier), - } -} - -fn symbolic_worker_island_from_sym(island: &r2sym::SymbolicWorkerIsland) -> SymbolicWorkerIsland { - SymbolicWorkerIsland { - anchor_block: island.anchor_block, - control_kind: island - .control_kind - .map(symbolic_control_island_kind_from_sym), - memory_kind: island.memory_kind.map(symbolic_memory_island_kind_from_sym), - frontier_targets: island.frontier_targets.clone(), - control_facts: island - .control_facts - .iter() - .map(symbolic_control_fact_from_sym) - .collect(), - memory_terms: island - .memory_terms - .iter() - .map(|term| SymbolicMemoryCondition { - region: symbolic_memory_region_from_sym(&term.region), - offset_lo: term.offset_lo, - offset_hi: term.offset_hi, - size: term.size, - exact_offset: term.exact_offset, - evidence: symbolic_evidence_from_sym(&term.evidence()), - confidence: symbolic_confidence_from_sym(term.confidence()), - binding: term.binding.clone(), - expr: term.expr.clone(), - value_expr: term.value_expr.clone(), - exact_value: term.exact_value, - }) - .collect(), - evidence: symbolic_evidence_from_sym(&island.evidence), - confidence: symbolic_confidence_from_sym(island.evidence.tier), - } -} - -fn symbolic_interpreter_kind_from_sym(kind: r2sym::InterpreterKind) -> SymbolicInterpreterKind { - match kind { - r2sym::InterpreterKind::SwitchDispatch => SymbolicInterpreterKind::SwitchDispatch, - r2sym::InterpreterKind::IndirectDispatch => SymbolicInterpreterKind::IndirectDispatch, - } -} - -pub fn symbolic_vm_value_expr_from_sym(value: &r2sym::VmValueExpr) -> SymbolicVmValueExpr { - value.into() -} - -impl From<&r2sym::VmValueExpr> for SymbolicVmValueExpr { - fn from(value: &r2sym::VmValueExpr) -> Self { - match value { - r2sym::VmValueExpr::Const(value) => SymbolicVmValueExpr::Const(*value), - r2sym::VmValueExpr::Var(name) => SymbolicVmValueExpr::Var(name.clone()), - r2sym::VmValueExpr::Unary { op, arg } => SymbolicVmValueExpr::Unary { - op: match op { - r2sym::VmUnaryOp::Neg => SymbolicVmUnaryOp::Neg, - r2sym::VmUnaryOp::BitNot => SymbolicVmUnaryOp::Not, - r2sym::VmUnaryOp::BoolNot => SymbolicVmUnaryOp::BoolNot, - }, - expr: Box::new(SymbolicVmValueExpr::from(arg.as_ref())), - }, - r2sym::VmValueExpr::Binary { op, lhs, rhs } => SymbolicVmValueExpr::Binary { - op: match op { - r2sym::VmBinaryOp::Add => SymbolicVmBinaryOp::Add, - r2sym::VmBinaryOp::Sub => SymbolicVmBinaryOp::Sub, - r2sym::VmBinaryOp::Mul => SymbolicVmBinaryOp::Mul, - r2sym::VmBinaryOp::Div => SymbolicVmBinaryOp::Div, - r2sym::VmBinaryOp::Rem => SymbolicVmBinaryOp::Rem, - r2sym::VmBinaryOp::And => SymbolicVmBinaryOp::And, - r2sym::VmBinaryOp::Or => SymbolicVmBinaryOp::Or, - r2sym::VmBinaryOp::Xor => SymbolicVmBinaryOp::Xor, - r2sym::VmBinaryOp::Shl => SymbolicVmBinaryOp::Shl, - r2sym::VmBinaryOp::LShr | r2sym::VmBinaryOp::AShr => SymbolicVmBinaryOp::Shr, - r2sym::VmBinaryOp::Eq => SymbolicVmBinaryOp::Eq, - r2sym::VmBinaryOp::Ne => SymbolicVmBinaryOp::Ne, - r2sym::VmBinaryOp::Lt | r2sym::VmBinaryOp::SLt => SymbolicVmBinaryOp::Lt, - r2sym::VmBinaryOp::Le | r2sym::VmBinaryOp::SLe => SymbolicVmBinaryOp::Le, - r2sym::VmBinaryOp::BoolAnd => SymbolicVmBinaryOp::And, - r2sym::VmBinaryOp::BoolOr => SymbolicVmBinaryOp::Or, - }, - left: Box::new(SymbolicVmValueExpr::from(lhs.as_ref())), - right: Box::new(SymbolicVmValueExpr::from(rhs.as_ref())), - }, - r2sym::VmValueExpr::Expr(expr) => SymbolicVmValueExpr::Expr(expr.clone()), - } - } -} - -fn symbolic_vm_state_update_from_sym(update: &r2sym::VmStateUpdate) -> SymbolicVmStateUpdate { - let evidence = update.evidence(); - SymbolicVmStateUpdate { - output: update.output.clone(), - expr: update.expr.clone(), - value: SymbolicVmValueExpr::from(&update.value), - exact: update.exact, - evidence: symbolic_evidence_from_sym(&evidence), - confidence: symbolic_confidence_from_sym(evidence.tier), - } -} - -fn symbolic_vm_guard_condition_from_sym( - guard: &r2sym::VmGuardCondition, -) -> SymbolicVmGuardCondition { - let evidence = guard.evidence(); - SymbolicVmGuardCondition { - expr: guard.expr.clone(), - value: SymbolicVmValueExpr::from(&guard.value), - expect_nonzero: guard.expect_nonzero, - exact: guard.exact, - evidence: symbolic_evidence_from_sym(&evidence), - confidence: symbolic_confidence_from_sym(evidence.tier), - } -} - -fn symbolic_vm_guarded_exit_from_sym(guarded: &r2sym::VmGuardedExit) -> SymbolicVmGuardedExit { - SymbolicVmGuardedExit { - target: guarded.target, - guard: symbolic_vm_guard_condition_from_sym(&guarded.guard), - } -} - -fn symbolic_vm_memory_condition_from_sym( - condition: &r2sym::VmMemoryCondition, -) -> SymbolicMemoryCondition { - let evidence = condition.evidence(); - SymbolicMemoryCondition { - region: SymbolicMemoryRegion::Region(SymbolicMemoryRegionRef { - id: condition.region.id, - kind: symbolic_memory_region_kind_from_sym(&condition.region.kind), - name: condition.region.name.clone(), - }), - offset_lo: condition.offset_lo, - offset_hi: condition.offset_hi, - size: condition.size, - exact_offset: condition.exact_offset, - evidence: symbolic_evidence_from_sym(&evidence), - confidence: symbolic_confidence_from_sym(evidence.tier), - binding: condition.binding.clone(), - expr: condition.expr.clone(), - value_expr: condition.value_expr.clone(), - exact_value: condition.exact_value, - } -} - -fn symbolic_vm_transfer_arm_from_sym(transfer: &r2sym::VmTransferArm) -> SymbolicVmTransferArm { - let evidence = transfer.evidence(); - SymbolicVmTransferArm { - handler_target: transfer.handler_target, - case_values: transfer.case_values.clone(), - region_blocks: transfer.region_blocks.clone(), - exit_targets: transfer.exit_targets.clone(), - exit_guards: transfer - .exit_guards - .iter() - .map(symbolic_vm_guarded_exit_from_sym) - .collect(), - state_updates: transfer - .state_updates - .iter() - .map(symbolic_vm_state_update_from_sym) - .collect(), - selector_update: transfer - .selector_update - .as_ref() - .map(symbolic_vm_state_update_from_sym), - memory_reads: transfer - .memory_reads - .iter() - .map(symbolic_vm_memory_condition_from_sym) - .collect(), - memory_writes: transfer - .memory_writes - .iter() - .map(symbolic_vm_memory_condition_from_sym) - .collect(), - residual_guards: transfer.residual_guards, - residual_memory_effects: transfer.residual_memory_effects, - exact: transfer.exact, - evidence: symbolic_evidence_from_sym(&evidence), - confidence: symbolic_confidence_from_sym(evidence.tier), - redispatch: transfer.redispatch, - may_return: transfer.may_return, - truncated: transfer.truncated, - } -} - -fn symbolic_vm_step_summary_from_sym(vm_step: &r2sym::VmStepSummary) -> SymbolicVmStepSummary { - SymbolicVmStepSummary { - kind: symbolic_interpreter_kind_from_sym(vm_step.kind), - loop_header: vm_step.loop_header, - dispatch_header: vm_step.dispatch_header, - selector: vm_step.selector.clone(), - dispatch_targets: vm_step.dispatch_targets.clone(), - default_target: vm_step.default_target, - case_values_by_target: vm_step.case_values_by_target.clone(), - loop_latches: vm_step.loop_latches.clone(), - state_inputs: vm_step.state_inputs.clone(), - state_outputs: vm_step.state_outputs.clone(), - step_blocks: vm_step.step_blocks.clone(), - handler_regions: vm_step.handler_regions.clone(), - handler_state_inputs: vm_step.handler_state_inputs.clone(), - handler_state_outputs: vm_step.handler_state_outputs.clone(), - handler_state_updates: vm_step - .handler_state_updates - .iter() - .map(|(target, updates)| { - ( - *target, - updates - .iter() - .map(symbolic_vm_state_update_from_sym) - .collect(), - ) - }) - .collect(), - handler_exit_guards: vm_step - .handler_exit_guards - .iter() - .map(|(target, guards)| { - ( - *target, - guards - .iter() - .map(symbolic_vm_guarded_exit_from_sym) - .collect(), - ) - }) - .collect(), - handler_memory_read_effects: vm_step - .handler_memory_read_effects - .iter() - .map(|(target, effects)| { - ( - *target, - effects - .iter() - .map(symbolic_vm_memory_condition_from_sym) - .collect(), - ) - }) - .collect(), - handler_memory_write_effects: vm_step - .handler_memory_write_effects - .iter() - .map(|(target, effects)| { - ( - *target, - effects - .iter() - .map(symbolic_vm_memory_condition_from_sym) - .collect(), - ) - }) - .collect(), - handler_memory_reads: vm_step.handler_memory_reads.clone(), - handler_memory_writes: vm_step.handler_memory_writes.clone(), - handler_calls: vm_step.handler_calls.clone(), - handler_conditional_branches: vm_step.handler_conditional_branches.clone(), - handler_exit_targets: vm_step.handler_exit_targets.clone(), - redispatch_handlers: vm_step.redispatch_handlers.clone(), - returning_handlers: vm_step.returning_handlers.clone(), - truncated_handlers: vm_step.truncated_handlers.clone(), - transfers: vm_step - .transfers - .iter() - .map(symbolic_vm_transfer_arm_from_sym) - .collect(), - } -} - -pub fn symbolic_semantic_facts_from_artifact( - compiled: &r2sym::CompiledSemanticArtifact, -) -> SymbolicSemanticFacts { - compiled.into() -} - -impl From<&r2sym::CompiledSemanticArtifact> for SymbolicSemanticFacts { - fn from(compiled: &r2sym::CompiledSemanticArtifact) -> Self { - let facts = &compiled.symbolic_facts; - SymbolicSemanticFacts { - branch_facts: facts - .branch_facts - .iter() - .map(|fact| SymbolicBranchFact { - block_addr: fact.block_addr, - true_target: fact.true_target, - false_target: fact.false_target, - true_status: symbolic_reachability_status_from_sym(fact.true_status), - false_status: symbolic_reachability_status_from_sym(fact.false_status), - true_condition: fact.true_condition.clone(), - false_condition: fact.false_condition.clone(), - true_compiled: fact - .true_compiled - .as_ref() - .map(symbolic_compiled_condition_from_sym), - false_compiled: fact - .false_compiled - .as_ref() - .map(symbolic_compiled_condition_from_sym), - }) - .collect(), - worker_islands: facts - .worker_islands - .iter() - .map(symbolic_worker_island_from_sym) - .collect(), - control_islands: facts - .control_islands - .iter() - .map(symbolic_control_island_from_sym) - .collect(), - memory_islands: facts - .memory_islands - .iter() - .map(symbolic_memory_island_from_sym) - .collect(), - diagnostics: SymbolicFactDiagnostics { - branches_evaluated: facts.diagnostics.branches_evaluated, - branches_pruned: facts.diagnostics.branches_pruned, - branches_unknown: facts.diagnostics.branches_unknown, - skipped_missing_arch: facts.diagnostics.skipped_missing_arch, - skipped_large_cfg: facts.diagnostics.skipped_large_cfg, - cache_hit: compiled.cache_hit, - semantic_mode: Some(match compiled.mode { - r2sym::SemanticMode::Raw => SymbolicSemanticMode::Raw, - r2sym::SemanticMode::Compiled => SymbolicSemanticMode::Compiled, - r2sym::SemanticMode::IslandCompiled => SymbolicSemanticMode::IslandCompiled, - r2sym::SemanticMode::Residual => SymbolicSemanticMode::Residual, - r2sym::SemanticMode::VmSummary => SymbolicSemanticMode::VmSummary, - }), - semantic_capability: Some(SymbolicSemanticCapability { - query_ready: compiled.capability.query_ready, - type_ready: compiled.capability.type_ready, - decompile_ready: compiled.capability.decompile_ready, - }), - slice_class: Some(match compiled.slice_class { - r2sym::SliceClass::Wrapper => SymbolicSemanticSliceClass::Wrapper, - r2sym::SliceClass::Worker => SymbolicSemanticSliceClass::Worker, - r2sym::SliceClass::RecursiveGroup => SymbolicSemanticSliceClass::RecursiveGroup, - r2sym::SliceClass::InterpreterSwitch => { - SymbolicSemanticSliceClass::InterpreterSwitch - } - r2sym::SliceClass::InterpreterIndirect => { - SymbolicSemanticSliceClass::InterpreterIndirect - } - r2sym::SliceClass::GenericLarge => SymbolicSemanticSliceClass::GenericLarge, - }), - residual_reasons: compiled - .residual_reasons - .iter() - .map(|reason| match reason { - r2sym::ResidualReason::MissingArch => { - SymbolicSemanticResidualReason::MissingArch - } - r2sym::ResidualReason::LargeCfg => SymbolicSemanticResidualReason::LargeCfg, - r2sym::ResidualReason::SummaryBudgetExhausted => { - SymbolicSemanticResidualReason::SummaryBudgetExhausted - } - r2sym::ResidualReason::SccBudgetExhausted => { - SymbolicSemanticResidualReason::SccBudgetExhausted - } - r2sym::ResidualReason::InterpreterRequiresStepSummary => { - SymbolicSemanticResidualReason::InterpreterRequiresStepSummary - } - }) - .collect(), - closure_functions: compiled.closure_functions, - helper_functions: compiled.helper_functions, - derived_summaries: compiled.derived_summaries, - summary_attempted: compiled.derived_diagnostics.attempted, - summary_budget_exhausted: compiled.derived_diagnostics.budget_exhausted - + compiled.derived_diagnostics.scc_budget_exhausted, - summary_scc_count: compiled.derived_diagnostics.scc_count, - }, - interpreter: compiled.interpreter.as_ref().map(|interpreter| { - SymbolicInterpreterDispatch { - kind: symbolic_interpreter_kind_from_sym(interpreter.kind), - dispatch_header: interpreter.dispatch_header, - dispatch_targets: interpreter.dispatch_targets, - selector: interpreter.selector.clone(), - back_edges: interpreter.back_edges, - score: interpreter.score, - } - }), - vm_step: compiled - .vm_step - .as_ref() - .map(symbolic_vm_step_summary_from_sym), - vm_transfer: compiled - .vm_transfer - .as_ref() - .map(symbolic_vm_step_summary_from_sym), - } - } -} diff --git a/crates/r2types/src/function_facts.rs b/crates/r2types/src/function_facts.rs new file mode 100644 index 0000000..99f4d63 --- /dev/null +++ b/crates/r2types/src/function_facts.rs @@ -0,0 +1,25 @@ +use crate::facts::FunctionTypeFacts; + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct FunctionFacts { + pub types: FunctionTypeFacts, + pub semantics: Option, +} + +impl FunctionFacts { + pub fn new(types: FunctionTypeFacts, semantics: Option) -> Self { + Self { types, semantics } + } + + pub fn type_plan(&self) -> Option { + self.semantics + .as_ref() + .map(r2sym::SemanticArtifact::type_plan) + } + + pub fn decompile_plan(&self) -> Option { + self.semantics + .as_ref() + .map(r2sym::SemanticArtifact::decompile_plan) + } +} diff --git a/crates/r2types/src/lib.rs b/crates/r2types/src/lib.rs index fedd264..7bec671 100644 --- a/crates/r2types/src/lib.rs +++ b/crates/r2types/src/lib.rs @@ -3,7 +3,7 @@ pub mod context; pub mod convert; pub mod external; pub mod facts; -pub mod from_sym; +pub mod function_facts; pub mod inference; pub mod lattice; pub mod model; @@ -33,21 +33,9 @@ pub use facts::{ CalleeMemoryRange, CalleeMemoryRegion, CalleeReturnRelation, FunctionParamSpec, FunctionSignatureSpec, FunctionType, FunctionTypeFactInputs, FunctionTypeFacts, FunctionTypeFactsBuilder, InterprocFactDiagnostics, LocalFieldAccessFact, ResolvedFieldLayout, - SymbolicBranchFact, SymbolicCompiledCondition, SymbolicConditionPrecision, SymbolicControlFact, - SymbolicControlIsland, SymbolicControlIslandKind, SymbolicFactDiagnostics, - SymbolicInterpreterDispatch, SymbolicInterpreterKind, SymbolicMemoryCondition, - SymbolicMemoryIsland, SymbolicMemoryIslandKind, SymbolicMemoryRegion, SymbolicMemoryRegionKind, - SymbolicMemoryRegionRef, SymbolicReachabilityStatus, SymbolicSemanticCapability, - SymbolicSemanticConfidence, SymbolicSemanticEvidence, SymbolicSemanticEvidenceAmbiguity, - SymbolicSemanticEvidenceCoverage, SymbolicSemanticEvidenceProvenance, - SymbolicSemanticEvidenceReason, SymbolicSemanticEvidenceSoundness, SymbolicSemanticFacts, - SymbolicSemanticMode, SymbolicSemanticResidualReason, SymbolicSemanticSliceClass, - SymbolicVmBinaryOp, SymbolicVmGuardCondition, SymbolicVmGuardedExit, SymbolicVmStateUpdate, - SymbolicVmStepSummary, SymbolicVmTransferArm, SymbolicVmTransferSummary, SymbolicVmUnaryOp, - SymbolicVmValueExpr, SymbolicWorkerIsland, VisibleBinding, VisibleBindingKind, - parse_type_like_spec, + VisibleBinding, VisibleBindingKind, parse_type_like_spec, }; -pub use from_sym::{symbolic_semantic_facts_from_artifact, symbolic_vm_value_expr_from_sym}; +pub use function_facts::FunctionFacts; pub use inference::{CombinedTypeOracle, TypeInference}; pub use model::{Signedness, StructField, StructShape, Type, TypeArena, TypeId}; pub use oracle::{LayoutOracle, TypeOracle}; @@ -74,7 +62,8 @@ pub use writeback::{ RecoveredVariable, StructDeclCandidate, StructDeclSource, StructFieldCandidate, TypeWritebackAnalysis, TypeWritebackAnalysisInput, TypeWritebackDiagnostics, TypeWritebackPlan, TypeWritebackSemanticInputs, VarRenameCandidate, VarTypeCandidate, WritebackEvidence, - WritebackSource, augment_local_struct_artifacts_with_symbolic_facts, + WritebackSource, augment_local_struct_artifacts_with_semantics, build_semantic_type_fallback_plan, build_type_writeback_analysis, build_type_writeback_analysis_with_semantics, infer_local_struct_artifacts_from_ssa, + semantic_artifact_prefers_bounded_type_plan, }; diff --git a/crates/r2types/src/writeback.rs b/crates/r2types/src/writeback.rs index b74d462..61a685d 100644 --- a/crates/r2types/src/writeback.rs +++ b/crates/r2types/src/writeback.rs @@ -19,9 +19,9 @@ use crate::facts::{ CalleeArgEffect, CalleeFact, CalleeMemoryEffect, CalleeMemoryEffectKind, CalleeMemoryLocation, CalleeMemoryRange, CalleeMemoryRegion, CalleeReturnRelation, FunctionParamSpec, FunctionSignatureSpec, FunctionTypeFactInputs, FunctionTypeFacts, InterprocFactDiagnostics, - LocalFieldAccessFact, SymbolicMemoryCondition, SymbolicMemoryRegion, SymbolicSemanticFacts, - VisibleBinding, VisibleBindingKind, parse_type_like_spec, + LocalFieldAccessFact, VisibleBinding, VisibleBindingKind, parse_type_like_spec, }; +use crate::function_facts::FunctionFacts; use crate::model::Signedness; use crate::prepare::recover_vars_arch_profile; @@ -193,6 +193,7 @@ pub struct TypeWritebackPlan { #[derive(Debug, Clone, PartialEq, Eq)] pub struct TypeWritebackAnalysis { pub signature: InferredSignature, + pub function_facts: FunctionFacts, pub type_facts: FunctionTypeFacts, pub plan: TypeWritebackPlan, } @@ -210,7 +211,7 @@ pub struct TypeWritebackAnalysisInput<'a> { } pub struct TypeWritebackSemanticInputs<'a> { - pub symbolic_facts: &'a SymbolicSemanticFacts, + pub artifact: &'a r2sym::SemanticArtifact, pub local_field_accesses: &'a [LocalFieldAccessFact], } @@ -551,9 +552,9 @@ fn build_type_writeback_analysis_inner( semantic.local_field_accesses, input.ptr_bits, ); - augment_local_struct_artifacts_with_symbolic_facts( + augment_local_struct_artifacts_with_semantics( &mut local_structs, - semantic.symbolic_facts, + semantic.artifact, input.ptr_bits, ); } @@ -650,10 +651,6 @@ fn build_type_writeback_analysis_inner( external_type_db: type_db, slot_type_overrides: local_structs.slot_type_overrides.clone(), slot_field_profiles: local_structs.slot_field_profiles.clone(), - symbolic_facts: semantic_inputs - .as_ref() - .map(|semantic| semantic.symbolic_facts.clone()) - .unwrap_or_default(), local_field_accesses: semantic_inputs .as_ref() .map(|semantic| semantic.local_field_accesses.to_vec()) @@ -674,11 +671,11 @@ fn build_type_writeback_analysis_inner( }) .build(); if let Some(semantic) = semantic_inputs.as_ref() - && semantic.symbolic_facts.diagnostics.branches_pruned > 0 + && semantic.artifact.diagnostics.branches_pruned > 0 { type_facts.diagnostics.push(format!( "symbolic pruned {} branch arm(s)", - semantic.symbolic_facts.diagnostics.branches_pruned + semantic.artifact.diagnostics.branches_pruned )); } let global_type_links = score_global_type_links( @@ -699,6 +696,12 @@ fn build_type_writeback_analysis_inner( TypeWritebackAnalysis { signature: input.inferred_signature, + function_facts: FunctionFacts::new( + type_facts.clone(), + semantic_inputs + .as_ref() + .map(|semantic| semantic.artifact.clone()), + ), type_facts, plan, } @@ -717,102 +720,105 @@ pub fn build_type_writeback_analysis_with_semantics( build_type_writeback_analysis_inner(input, Some(semantic_inputs)) } -fn symbolic_semantic_mode_label(mode: crate::facts::SymbolicSemanticMode) -> &'static str { - match mode { - crate::facts::SymbolicSemanticMode::Raw => "raw", - crate::facts::SymbolicSemanticMode::Compiled => "compiled", - crate::facts::SymbolicSemanticMode::IslandCompiled => "island_compiled", - crate::facts::SymbolicSemanticMode::Residual => "residual", - crate::facts::SymbolicSemanticMode::VmSummary => "vm_summary", +fn semantic_stage_label(artifact: &r2sym::SemanticArtifact) -> &'static str { + match (artifact.execution, artifact.stage, artifact.granularity) { + (r2sym::ExecutionModel::Vm, _, _) => "vm_summary", + (_, r2sym::RefinementStage::Raw, _) => "raw", + (_, r2sym::RefinementStage::Compiled, r2sym::ArtifactGranularity::Regioned) => { + "island_compiled" + } + (_, r2sym::RefinementStage::Compiled, _) => "compiled", + (_, r2sym::RefinementStage::Residual, _) => "residual", } } -fn symbolic_slice_class_label( - slice_class: crate::facts::SymbolicSemanticSliceClass, -) -> &'static str { +fn semantic_slice_class_label(slice_class: r2sym::SliceClass) -> &'static str { match slice_class { - crate::facts::SymbolicSemanticSliceClass::Wrapper => "wrapper", - crate::facts::SymbolicSemanticSliceClass::Worker => "worker", - crate::facts::SymbolicSemanticSliceClass::RecursiveGroup => "recursive_group", - crate::facts::SymbolicSemanticSliceClass::InterpreterSwitch => "interpreter_switch", - crate::facts::SymbolicSemanticSliceClass::InterpreterIndirect => "interpreter_indirect", - crate::facts::SymbolicSemanticSliceClass::GenericLarge => "generic_large", + r2sym::SliceClass::Wrapper => "wrapper", + r2sym::SliceClass::Worker => "worker", + r2sym::SliceClass::RecursiveGroup => "recursive_group", + r2sym::SliceClass::InterpreterSwitch => "interpreter_switch", + r2sym::SliceClass::InterpreterIndirect => "interpreter_indirect", + r2sym::SliceClass::GenericLarge => "generic_large", } } -fn symbolic_residual_reason_label( - reason: crate::facts::SymbolicSemanticResidualReason, -) -> &'static str { +fn semantic_residual_reason_label(reason: r2sym::ResidualReason) -> &'static str { match reason { - crate::facts::SymbolicSemanticResidualReason::MissingArch => "missing_arch", - crate::facts::SymbolicSemanticResidualReason::LargeCfg => "large_cfg", - crate::facts::SymbolicSemanticResidualReason::SummaryBudgetExhausted => { - "summary_budget_exhausted" - } - crate::facts::SymbolicSemanticResidualReason::SccBudgetExhausted => "scc_budget_exhausted", - crate::facts::SymbolicSemanticResidualReason::InterpreterRequiresStepSummary => { + r2sym::ResidualReason::MissingArch => "missing_arch", + r2sym::ResidualReason::LargeCfg => "large_cfg", + r2sym::ResidualReason::SummaryBudgetExhausted => "summary_budget_exhausted", + r2sym::ResidualReason::SccBudgetExhausted => "scc_budget_exhausted", + r2sym::ResidualReason::InterpreterRequiresStepSummary => { "interpreter_requires_step_summary" } } } -fn semantic_fallback_warning(symbolic_facts: &SymbolicSemanticFacts) -> String { - let slice_class = symbolic_facts +fn semantic_fallback_warning(artifact: &r2sym::SemanticArtifact) -> String { + let slice_class = artifact .slice_class() - .map(symbolic_slice_class_label) - .unwrap_or("unknown"); - let mode = symbolic_facts - .semantic_mode() - .map(symbolic_semantic_mode_label) + .map(semantic_slice_class_label) .unwrap_or("unknown"); + let mode = semantic_stage_label(artifact); let mut warning = format!("semantic fallback: {slice_class} slice in {mode} mode"); - if !symbolic_facts.diagnostics.residual_reasons.is_empty() { + if !artifact.diagnostics.residual_reasons.is_empty() { warning.push_str(" ("); warning.push_str( - &symbolic_facts + &artifact .diagnostics .residual_reasons .iter() - .map(|reason| symbolic_residual_reason_label(*reason)) + .map(|reason| semantic_residual_reason_label(*reason)) .collect::>() .join(", "), ); warning.push(')'); } - if !symbolic_facts.branch_facts.is_empty() - || !symbolic_facts.worker_islands.is_empty() - || !symbolic_facts.control_islands.is_empty() - || !symbolic_facts.memory_islands.is_empty() + if let Some(native) = artifact.native_body() + && !native.regions.is_empty() { warning.push_str(&format!( - "; branch_facts={}, worker_islands={}, control_islands={}, memory_islands={}, actionable_conditions={}, exact_conditions={}", - symbolic_facts.branch_facts.len(), - symbolic_facts.worker_islands.len(), - symbolic_facts.control_islands.len(), - symbolic_facts.memory_islands.len(), - symbolic_facts.actionable_compiled_condition_count(), - symbolic_facts.exact_compiled_condition_count(), + "; regions={}, actionable_conditions={}, exact_conditions={}", + native.regions.len(), + native.actionable_control_count(), + native.exact_control_count(), )); } warning } +pub fn semantic_artifact_prefers_bounded_type_plan(artifact: &r2sym::SemanticArtifact) -> bool { + if !matches!(artifact.type_plan(), r2sym::TypePlan::Ready) { + return true; + } + matches!( + artifact.stage, + r2sym::RefinementStage::Residual | r2sym::RefinementStage::Compiled + ) && artifact.diagnostics.skipped_large_cfg + && matches!(artifact.slice_class(), Some(r2sym::SliceClass::Worker)) + && artifact + .native_body() + .is_some_and(|body| !body.regions.is_empty()) + && (artifact.actionable_control_count() > 0 + || artifact + .actionable_regions() + .into_iter() + .any(|region| !region.actionable_memory_terms().is_empty())) +} + pub fn build_semantic_type_fallback_plan( function_name: &str, arch_name: &str, ptr_bits: u32, - symbolic_facts: &SymbolicSemanticFacts, + artifact: &r2sym::SemanticArtifact, ) -> TypeWritebackPlan { - let mut warnings = vec![semantic_fallback_warning(symbolic_facts)]; - if !symbolic_facts.type_ready() { + let mut warnings = vec![semantic_fallback_warning(artifact)]; + if !matches!(artifact.type_plan(), r2sym::TypePlan::Ready) { warnings.push("type analysis not ready from semantic capability".to_string()); } let mut local_structs = LocalStructArtifacts::default(); - augment_local_struct_artifacts_with_symbolic_facts( - &mut local_structs, - symbolic_facts, - ptr_bits, - ); + augment_local_struct_artifacts_with_semantics(&mut local_structs, artifact, ptr_bits); let mut signature = InferredSignature { function_name: function_name.to_string(), signature: format!("void {}(void)", function_name), @@ -832,7 +838,7 @@ pub fn build_semantic_type_fallback_plan( } if !local_structs.struct_decls.is_empty() { warnings.push(format!( - "semantic worker islands projected {} struct candidate(s)", + "semantic regions projected {} struct candidate(s)", local_structs.struct_decls.len() )); } @@ -1402,18 +1408,23 @@ pub fn infer_local_struct_artifacts_from_ssa( } } -pub fn augment_local_struct_artifacts_with_symbolic_facts( +pub fn augment_local_struct_artifacts_with_semantics( local_structs: &mut LocalStructArtifacts, - symbolic_facts: &SymbolicSemanticFacts, + artifact: &r2sym::SemanticArtifact, ptr_bits: u32, ) { let mut projected_profiles = BTreeMap::>::new(); - for island in &symbolic_facts.worker_islands { - if !(island.confidence.is_reliable() && island.evidence.is_reliable()) { - continue; - } - for term in &island.memory_terms { - let Some((slot, offset, field_type)) = symbolic_memory_term_slot_field(term, ptr_bits) + let Some(native) = artifact.native_body() else { + return; + }; + for region in native.regions.values() { + for term in region + .memory + .iter() + .filter(|memory| memory.evidence.is_reliable()) + .map(|memory| &memory.value.term) + { + let Some((slot, offset, field_type)) = backward_memory_term_slot_field(term, ptr_bits) else { continue; }; @@ -1425,48 +1436,19 @@ pub fn augment_local_struct_artifacts_with_symbolic_facts( } } if projected_profiles.is_empty() { - for island in &symbolic_facts.memory_islands { - if !(island.confidence.is_reliable() && island.evidence.is_reliable()) { - continue; - } - for term in &island.terms { - let Some((slot, offset, field_type)) = - symbolic_memory_term_slot_field(term, ptr_bits) - else { - continue; - }; - projected_profiles - .entry(slot) - .or_default() - .entry(offset) - .or_insert(field_type); - } - } - } - if projected_profiles.is_empty() { - for compiled in symbolic_facts - .branch_facts - .iter() - .filter_map(|fact| fact.actionable_compiled_condition()) - .chain( - symbolic_facts - .worker_islands - .iter() - .filter_map(|island| island.actionable_compiled_condition()), - ) - .chain( - symbolic_facts - .control_islands - .iter() - .filter_map(|island| island.actionable_compiled_condition()), - ) + for compiled in native + .regions + .values() + .flat_map(|region| region.control.iter()) + .filter(|fact| fact.evidence.allows_narrowing()) + .filter_map(|fact| fact.value.compiled.as_ref()) { - if !(compiled.confidence.is_reliable() && compiled.evidence.is_reliable()) { + if !compiled.evidence().is_reliable() { continue; } for term in &compiled.memory_terms { let Some((slot, offset, field_type)) = - symbolic_memory_term_slot_field(term, ptr_bits) + backward_memory_term_slot_field(term, ptr_bits) else { continue; }; @@ -1588,16 +1570,16 @@ fn augment_local_struct_artifacts_with_local_field_accesses( } } -fn symbolic_memory_term_slot_field( - term: &SymbolicMemoryCondition, +fn backward_memory_term_slot_field( + term: &r2sym::BackwardMemoryCondition, _ptr_bits: u32, ) -> Option<(usize, u64, String)> { - if !(term.confidence.is_reliable() && term.evidence.is_reliable()) { + if !term.evidence().is_reliable() { return None; } - let slot = match term.region { - SymbolicMemoryRegion::Argument { index } => index, - SymbolicMemoryRegion::Region(_) => return None, + let slot = match &term.region { + r2sym::BackwardMemoryRegion::Argument { index } => *index, + r2sym::BackwardMemoryRegion::Region(_) => return None, }; if !term.exact_offset && term.offset_lo != term.offset_hi { return None; @@ -3483,6 +3465,114 @@ fn ssa_var_block_key(block_addr: u64, var: &SSAVar) -> String { #[cfg(test)] mod tests { use super::*; + use std::collections::{BTreeMap, BTreeSet}; + + fn test_native_summary(slice_class: r2sym::SliceClass) -> r2sym::NativeFunctionSummary { + r2sym::NativeFunctionSummary { + slice_class, + closure_functions: 1, + helper_functions: 0, + derived_summaries: 0, + derived_diagnostics: Default::default(), + } + } + + fn test_artifact( + stage: r2sym::RefinementStage, + slice_class: r2sym::SliceClass, + skipped_large_cfg: bool, + residual_reasons: Vec, + regions: Vec, + ) -> r2sym::SemanticArtifact { + let regions = regions + .into_iter() + .map(|region| (region.key(), region)) + .collect::>(); + r2sym::SemanticArtifact { + stage, + granularity: r2sym::ArtifactGranularity::Regioned, + execution: r2sym::ExecutionModel::Native, + body: r2sym::SemanticArtifactBody::Native(r2sym::NativeArtifactBody { + summary: test_native_summary(slice_class), + regions, + }), + diagnostics: r2sym::SemanticArtifactDiagnostics { + branches_evaluated: 0, + branches_pruned: 0, + branches_unknown: 0, + skipped_missing_arch: false, + skipped_large_cfg, + residual_reasons, + ambiguous_targets: Vec::new(), + cache_hit: false, + }, + } + } + + fn test_arg_memory_term(offset: i64, size: u32) -> r2sym::BackwardMemoryCondition { + r2sym::BackwardMemoryCondition { + region: r2sym::BackwardMemoryRegion::Argument { index: 0 }, + offset_lo: offset, + offset_hi: offset, + size, + exact_offset: true, + evidence: r2sym::SemanticEvidence::exact(), + binding: None, + expr: format!("*(arg0 + {offset})"), + value_expr: None, + exact_value: false, + } + } + + fn test_exact_compiled_condition( + simplified: &str, + memory_terms: Vec, + ) -> r2sym::BackwardConditionSummary { + r2sym::BackwardConditionSummary { + simplified: simplified.to_string(), + terms: vec![simplified.to_string()], + memory_terms, + backward_memory_substitutions: 1, + backward_memory_candidate_enumerations: 1, + backward_memory_residual_fallbacks: 0, + precision: r2sym::BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + } + } + + fn test_region_with_control( + anchor: u64, + target: u64, + condition: &str, + compiled: r2sym::BackwardConditionSummary, + ) -> r2sym::SemanticRegion { + r2sym::SemanticRegion { + anchor, + frontier: BTreeSet::from([target]), + control: vec![r2sym::Judged::new( + r2sym::ControlFact { + target, + status: r2sym::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + condition: Some(condition.to_string()), + compiled: Some(compiled), + }, + r2sym::SemanticEvidence::exact(), + )], + memory: Vec::new(), + pre: Vec::new(), + post: Vec::new(), + targets: vec![r2sym::Judged::new( + r2sym::TargetFact { + target, + status: r2sym::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + }, + r2sym::SemanticEvidence::exact(), + )], + } + } #[test] fn main_signature_canonicalization_updates_signature_output() { @@ -5205,58 +5295,23 @@ mod tests { #[test] fn symbolic_actionable_memory_terms_seed_local_struct_profiles() { - let compiled = crate::facts::SymbolicCompiledCondition { - simplified: "arg0->f_8 == 0".to_string(), - terms: vec!["arg0->f_8 == 0".to_string()], - memory_terms: vec![crate::facts::SymbolicMemoryCondition { - region: crate::facts::SymbolicMemoryRegion::Argument { index: 0 }, - offset_lo: 8, - offset_hi: 8, - size: 4, - exact_offset: true, - evidence: crate::facts::SymbolicSemanticEvidence::exact(), - confidence: crate::facts::SymbolicSemanticConfidence::Exact, - binding: None, - expr: "*(arg0 + 8)".to_string(), - value_expr: None, - exact_value: false, - }], - backward_memory_substitutions: 1, - backward_memory_candidate_enumerations: 1, - backward_memory_residual_fallbacks: 0, - precision: crate::facts::SymbolicConditionPrecision::Exact, - evidence: crate::facts::SymbolicSemanticEvidence::exact(), - confidence: crate::facts::SymbolicSemanticConfidence::Exact, - supported_paths: 1, - total_paths: 1, - }; - let symbolic_facts = crate::facts::SymbolicSemanticFacts { - branch_facts: Vec::new(), - worker_islands: Vec::new(), - control_islands: vec![crate::facts::SymbolicControlIsland { - kind: crate::facts::SymbolicControlIslandKind::LargeCfgBranchFrontier, - anchor_block: 0x401000, - frontier_targets: vec![0x401020], - facts: vec![crate::facts::SymbolicControlFact { - target: 0x401020, - status: crate::facts::SymbolicReachabilityStatus::Reachable, - condition: Some("arg0->f_8 == 0".to_string()), - compiled: Some(compiled), - evidence: crate::facts::SymbolicSemanticEvidence::exact(), - confidence: crate::facts::SymbolicSemanticConfidence::Exact, - }], - evidence: crate::facts::SymbolicSemanticEvidence::exact(), - confidence: crate::facts::SymbolicSemanticConfidence::Exact, - }], - memory_islands: Vec::new(), - diagnostics: crate::facts::SymbolicFactDiagnostics::default(), - interpreter: None, - vm_step: None, - vm_transfer: None, - }; + let compiled = + test_exact_compiled_condition("arg0->f_8 == 0", vec![test_arg_memory_term(8, 4)]); + let artifact = test_artifact( + r2sym::RefinementStage::Compiled, + r2sym::SliceClass::Worker, + false, + Vec::new(), + vec![test_region_with_control( + 0x401000, + 0x401020, + "arg0->f_8 == 0", + compiled, + )], + ); let mut local_structs = LocalStructArtifacts::default(); - augment_local_struct_artifacts_with_symbolic_facts(&mut local_structs, &symbolic_facts, 64); + augment_local_struct_artifacts_with_semantics(&mut local_structs, &artifact, 64); assert_eq!( local_structs @@ -5283,37 +5338,29 @@ mod tests { #[test] fn symbolic_memory_islands_seed_local_struct_profiles_without_control_islands() { - let symbolic_facts = crate::facts::SymbolicSemanticFacts { - branch_facts: Vec::new(), - worker_islands: Vec::new(), - control_islands: Vec::new(), - memory_islands: vec![crate::facts::SymbolicMemoryIsland { - kind: crate::facts::SymbolicMemoryIslandKind::ConditionFrontier, - anchor_block: 0x401000, - terms: vec![crate::facts::SymbolicMemoryCondition { - region: crate::facts::SymbolicMemoryRegion::Argument { index: 0 }, - offset_lo: 8, - offset_hi: 8, - size: 4, - exact_offset: true, - evidence: crate::facts::SymbolicSemanticEvidence::exact(), - confidence: crate::facts::SymbolicSemanticConfidence::Exact, - binding: None, - expr: "*(arg0 + 8)".to_string(), - value_expr: None, - exact_value: false, - }], - evidence: crate::facts::SymbolicSemanticEvidence::exact(), - confidence: crate::facts::SymbolicSemanticConfidence::Exact, + let artifact = test_artifact( + r2sym::RefinementStage::Compiled, + r2sym::SliceClass::Worker, + false, + Vec::new(), + vec![r2sym::SemanticRegion { + anchor: 0x401000, + frontier: BTreeSet::new(), + control: Vec::new(), + memory: vec![r2sym::Judged::new( + r2sym::MemoryFact { + term: test_arg_memory_term(8, 4), + }, + r2sym::SemanticEvidence::exact(), + )], + pre: Vec::new(), + post: Vec::new(), + targets: Vec::new(), }], - diagnostics: crate::facts::SymbolicFactDiagnostics::default(), - interpreter: None, - vm_step: None, - vm_transfer: None, - }; + ); let mut local_structs = LocalStructArtifacts::default(); - augment_local_struct_artifacts_with_symbolic_facts(&mut local_structs, &symbolic_facts, 64); + augment_local_struct_artifacts_with_semantics(&mut local_structs, &artifact, 64); assert_eq!( local_structs @@ -5334,83 +5381,44 @@ mod tests { #[test] fn semantic_type_fallback_plan_uses_typed_symbolic_summary() { - let symbolic_facts = crate::facts::SymbolicSemanticFacts { - branch_facts: vec![crate::facts::SymbolicBranchFact { - block_addr: 0x401000, - true_target: 0x401010, - false_target: 0x401020, - true_status: crate::facts::SymbolicReachabilityStatus::Reachable, - false_status: crate::facts::SymbolicReachabilityStatus::Unreachable, - true_condition: Some("x == 0".to_string()), - false_condition: Some("x != 0".to_string()), - true_compiled: None, - false_compiled: None, - }], - worker_islands: Vec::new(), - control_islands: vec![crate::facts::SymbolicControlIsland { - kind: crate::facts::SymbolicControlIslandKind::LargeCfgBranchFrontier, - anchor_block: 0x401000, - frontier_targets: vec![0x401010], - facts: vec![crate::facts::SymbolicControlFact { - target: 0x401010, - status: crate::facts::SymbolicReachabilityStatus::Reachable, - condition: Some("x == 0".to_string()), - compiled: Some(crate::facts::SymbolicCompiledCondition { - simplified: "x == 0".to_string(), - terms: vec!["x == 0".to_string()], - memory_terms: Vec::new(), - backward_memory_substitutions: 0, - backward_memory_candidate_enumerations: 0, - backward_memory_residual_fallbacks: 0, - precision: crate::facts::SymbolicConditionPrecision::Exact, - evidence: crate::facts::SymbolicSemanticEvidence::exact(), - confidence: crate::facts::SymbolicSemanticConfidence::Exact, - supported_paths: 1, - total_paths: 1, - }), - evidence: crate::facts::SymbolicSemanticEvidence::exact(), - confidence: crate::facts::SymbolicSemanticConfidence::Exact, - }], - evidence: crate::facts::SymbolicSemanticEvidence::exact(), - confidence: crate::facts::SymbolicSemanticConfidence::Exact, - }], - memory_islands: vec![crate::facts::SymbolicMemoryIsland { - kind: crate::facts::SymbolicMemoryIslandKind::LargeCfgConditionFrontier, - anchor_block: 0x401000, - terms: vec![crate::facts::SymbolicMemoryCondition { - region: crate::facts::SymbolicMemoryRegion::Argument { index: 0 }, - offset_lo: 8, - offset_hi: 8, - size: 4, - exact_offset: true, - evidence: crate::facts::SymbolicSemanticEvidence::exact(), - confidence: crate::facts::SymbolicSemanticConfidence::Exact, - binding: None, - expr: "*(arg0 + 8)".to_string(), - value_expr: None, - exact_value: false, - }], - evidence: crate::facts::SymbolicSemanticEvidence::exact(), - confidence: crate::facts::SymbolicSemanticConfidence::Exact, + let artifact = test_artifact( + r2sym::RefinementStage::Residual, + r2sym::SliceClass::Worker, + true, + vec![r2sym::ResidualReason::LargeCfg], + vec![r2sym::SemanticRegion { + anchor: 0x401000, + frontier: BTreeSet::from([0x401010]), + control: vec![r2sym::Judged::new( + r2sym::ControlFact { + target: 0x401010, + status: r2sym::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + condition: Some("x == 0".to_string()), + compiled: Some(test_exact_compiled_condition("x == 0", Vec::new())), + }, + r2sym::SemanticEvidence::exact(), + )], + memory: vec![r2sym::Judged::new( + r2sym::MemoryFact { + term: test_arg_memory_term(8, 4), + }, + r2sym::SemanticEvidence::exact(), + )], + pre: Vec::new(), + post: Vec::new(), + targets: vec![r2sym::Judged::new( + r2sym::TargetFact { + target: 0x401010, + status: r2sym::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + }, + r2sym::SemanticEvidence::exact(), + )], }], - diagnostics: crate::facts::SymbolicFactDiagnostics { - skipped_large_cfg: true, - semantic_mode: Some(crate::facts::SymbolicSemanticMode::Residual), - semantic_capability: Some(crate::facts::SymbolicSemanticCapability { - query_ready: true, - type_ready: false, - decompile_ready: true, - }), - slice_class: Some(crate::facts::SymbolicSemanticSliceClass::Worker), - residual_reasons: vec![crate::facts::SymbolicSemanticResidualReason::LargeCfg], - ..Default::default() - }, - interpreter: None, - vm_step: None, - vm_transfer: None, - }; + ); - let plan = build_semantic_type_fallback_plan("fcn.401000", "x86-64", 64, &symbolic_facts); + let plan = build_semantic_type_fallback_plan("fcn.401000", "x86-64", 64, &artifact); assert_eq!(plan.signature.params.len(), 1); assert!(plan.signature.params[0].param_type.contains("struct ")); @@ -5424,13 +5432,8 @@ mod tests { plan.diagnostics .warnings .iter() - .any(|warning| warning.contains("memory_islands=1")) + .any(|warning| warning.contains("regions=1")) ); - assert!(plan - .diagnostics - .warnings - .iter() - .any(|warning| warning.contains("type analysis not ready from semantic capability"))); assert!( plan.diagnostics .warnings diff --git a/r2plugin/src/analysis/sym.rs b/r2plugin/src/analysis/sym.rs index f551ef8..d1a1713 100644 --- a/r2plugin/src/analysis/sym.rs +++ b/r2plugin/src/analysis/sym.rs @@ -1,7 +1,6 @@ use crate::blocks::BlockSlice; use crate::context::require_ctx_view; use crate::{ArchSpec, R2ILBlock, R2ILContext, parse_addr_name_map}; -use r2types::SymbolicVmValueExpr; use serde::Serialize; use std::collections::BTreeMap; use std::collections::HashMap; @@ -245,20 +244,26 @@ struct SymExecSummary { #[derive(Debug, Serialize, Clone)] pub(crate) struct CompiledSemanticInfo { - pub(crate) mode: String, - pub(crate) capability: SemanticCapabilityInfo, + pub(crate) schema_version: u32, + pub(crate) stage: String, + pub(crate) granularity: String, + pub(crate) execution: String, + pub(crate) query_plan: r2sym::QueryPlan, + pub(crate) type_plan: r2sym::TypePlan, + pub(crate) decompile_plan: r2sym::DecompilePlan, pub(crate) slice_class: String, pub(crate) residual_reasons: Vec, + pub(crate) ambiguous_target_count: usize, + pub(crate) ambiguous_targets: Vec, pub(crate) closure_functions: usize, pub(crate) helper_functions: usize, pub(crate) derived_summaries: usize, pub(crate) summary_attempted: usize, pub(crate) summary_budget_exhausted: usize, pub(crate) summary_scc_count: usize, - pub(crate) branch_fact_count: usize, - pub(crate) worker_island_count: usize, - pub(crate) control_island_count: usize, - pub(crate) memory_island_count: usize, + pub(crate) region_count: usize, + pub(crate) control_region_count: usize, + pub(crate) memory_region_count: usize, pub(crate) compiled_condition_count: usize, pub(crate) exact_compiled_condition_count: usize, pub(crate) actionable_compiled_condition_count: usize, @@ -274,13 +279,6 @@ pub(crate) struct CompiledSemanticInfo { pub(crate) cache_hit: bool, } -#[derive(Debug, Serialize, Clone)] -pub(crate) struct SemanticCapabilityInfo { - pub(crate) query_ready: bool, - pub(crate) type_ready: bool, - pub(crate) decompile_ready: bool, -} - #[derive(Debug, Serialize, Clone)] pub(crate) struct InterpreterDispatchInfo { pub(crate) kind: String, @@ -383,8 +381,45 @@ pub(crate) struct VmStepSummaryInfo { pub(crate) transfers: Vec, } -fn render_vm_value_expr(value: &SymbolicVmValueExpr) -> String { - value.render() +fn render_vm_value_expr(value: &r2sym::VmValueExpr) -> String { + match value { + r2sym::VmValueExpr::Const(value) => format!("0x{value:x}"), + r2sym::VmValueExpr::Var(name) | r2sym::VmValueExpr::Expr(name) => name.clone(), + r2sym::VmValueExpr::Unary { op, arg } => { + let op = match op { + r2sym::VmUnaryOp::Neg => "-", + r2sym::VmUnaryOp::BitNot => "~", + r2sym::VmUnaryOp::BoolNot => "!", + }; + format!("({}{})", op, render_vm_value_expr(arg)) + } + r2sym::VmValueExpr::Binary { op, lhs, rhs } => { + let op = match op { + r2sym::VmBinaryOp::Add => "+", + r2sym::VmBinaryOp::Sub => "-", + r2sym::VmBinaryOp::Mul => "*", + r2sym::VmBinaryOp::Div => "/", + r2sym::VmBinaryOp::Rem => "%", + r2sym::VmBinaryOp::And => "&", + r2sym::VmBinaryOp::Or => "|", + r2sym::VmBinaryOp::Xor => "^", + r2sym::VmBinaryOp::Shl => "<<", + r2sym::VmBinaryOp::LShr | r2sym::VmBinaryOp::AShr => ">>", + r2sym::VmBinaryOp::Eq => "==", + r2sym::VmBinaryOp::Ne => "!=", + r2sym::VmBinaryOp::Lt | r2sym::VmBinaryOp::SLt => "<", + r2sym::VmBinaryOp::Le | r2sym::VmBinaryOp::SLe => "<=", + r2sym::VmBinaryOp::BoolAnd => "&&", + r2sym::VmBinaryOp::BoolOr => "||", + }; + format!( + "({} {} {})", + render_vm_value_expr(lhs), + op, + render_vm_value_expr(rhs) + ) + } + } } fn render_semantic_confidence(confidence: r2sym::SemanticConfidence) -> String { @@ -398,21 +433,19 @@ fn render_semantic_confidence(confidence: r2sym::SemanticConfidence) -> String { } fn vm_state_update_info_from_sym(update: &r2sym::VmStateUpdate) -> VmStateUpdateInfo { - let value = SymbolicVmValueExpr::from(&update.value); VmStateUpdateInfo { output: update.output.clone(), expr: update.expr.clone(), - value: render_vm_value_expr(&value), + value: render_vm_value_expr(&update.value), exact: update.exact, confidence: render_semantic_confidence(update.confidence()), } } fn vm_guard_condition_info_from_sym(guard: &r2sym::VmGuardCondition) -> VmGuardConditionInfo { - let value = SymbolicVmValueExpr::from(&guard.value); VmGuardConditionInfo { expr: guard.expr.clone(), - value: render_vm_value_expr(&value), + value: render_vm_value_expr(&guard.value), expect_nonzero: guard.expect_nonzero, exact: guard.exact, confidence: render_semantic_confidence(guard.confidence()), @@ -748,7 +781,7 @@ fn build_sym_exec_summary( stats: &r2sym::path::ExploreStats, solver_stats: &r2sym::SolverStats, paths_feasible: usize, - semantic: Option<&r2sym::CompiledSemanticArtifact>, + semantic: Option<&r2sym::SemanticArtifact>, ) -> SymExecSummary { SymExecSummary { paths_explored: stats.paths_completed, @@ -777,50 +810,51 @@ fn empty_symbolic_summary<'ctx>() -> r2sym::SymbolicFunctionSummary<'ctx> { } fn should_skip_expensive_symbolic_summary( - compiled: &r2sym::CompiledSemanticArtifact, + compiled: &r2sym::SemanticArtifact, prepared: &r2ssa::SsaArtifact, ) -> bool { - compiled.symbolic_facts.diagnostics.skipped_large_cfg + compiled.diagnostics.skipped_large_cfg || prepared.function().cfg_risk_summary().block_count > 96 } -pub(crate) fn compiled_semantic_info( - compiled: &r2sym::CompiledSemanticArtifact, -) -> CompiledSemanticInfo { - let branch_compiled_conditions = compiled - .symbolic_facts - .branch_facts - .iter() - .flat_map(|fact| fact.true_compiled.iter().chain(fact.false_compiled.iter())) - .collect::>(); - let control_facts = compiled - .symbolic_facts - .worker_islands - .iter() - .flat_map(|island| island.control_facts.iter()) - .collect::>(); +pub(crate) fn compiled_semantic_info(compiled: &r2sym::SemanticArtifact) -> CompiledSemanticInfo { + let native = compiled.native_body(); CompiledSemanticInfo { - mode: match compiled.mode { - r2sym::SemanticMode::Raw => "raw".to_string(), - r2sym::SemanticMode::Compiled => "compiled".to_string(), - r2sym::SemanticMode::IslandCompiled => "island_compiled".to_string(), - r2sym::SemanticMode::Residual => "residual".to_string(), - r2sym::SemanticMode::VmSummary => "vm_summary".to_string(), - }, - capability: SemanticCapabilityInfo { - query_ready: compiled.capability.query_ready, - type_ready: compiled.capability.type_ready, - decompile_ready: compiled.capability.decompile_ready, - }, - slice_class: match compiled.slice_class { - r2sym::SliceClass::Wrapper => "wrapper".to_string(), - r2sym::SliceClass::Worker => "worker".to_string(), - r2sym::SliceClass::RecursiveGroup => "recursive_group".to_string(), - r2sym::SliceClass::InterpreterSwitch => "interpreter_switch".to_string(), - r2sym::SliceClass::InterpreterIndirect => "interpreter_indirect".to_string(), - r2sym::SliceClass::GenericLarge => "generic_large".to_string(), - }, + schema_version: r2sym::SEMANTIC_ARTIFACT_SCHEMA_VERSION, + stage: match compiled.stage { + r2sym::RefinementStage::Raw => "raw", + r2sym::RefinementStage::Compiled => "compiled", + r2sym::RefinementStage::Residual => "residual", + } + .to_string(), + granularity: match compiled.granularity { + r2sym::ArtifactGranularity::WholeFunction => "whole_function", + r2sym::ArtifactGranularity::Regioned => "regioned", + r2sym::ArtifactGranularity::SummaryOnly => "summary_only", + } + .to_string(), + execution: match compiled.execution { + r2sym::ExecutionModel::Native => "native", + r2sym::ExecutionModel::Vm => "vm", + } + .to_string(), + query_plan: compiled.query_plan(), + type_plan: compiled.type_plan(), + decompile_plan: compiled.decompile_plan(), + slice_class: compiled + .slice_class() + .map(|slice_class| match slice_class { + r2sym::SliceClass::Wrapper => "wrapper", + r2sym::SliceClass::Worker => "worker", + r2sym::SliceClass::RecursiveGroup => "recursive_group", + r2sym::SliceClass::InterpreterSwitch => "interpreter_switch", + r2sym::SliceClass::InterpreterIndirect => "interpreter_indirect", + r2sym::SliceClass::GenericLarge => "generic_large", + }) + .unwrap_or("worker") + .to_string(), residual_reasons: compiled + .diagnostics .residual_reasons .iter() .map(|reason| { @@ -836,31 +870,65 @@ pub(crate) fn compiled_semantic_info( .to_string() }) .collect(), - closure_functions: compiled.closure_functions, - helper_functions: compiled.helper_functions, - derived_summaries: compiled.derived_summaries, - summary_attempted: compiled.derived_diagnostics.attempted, - summary_budget_exhausted: compiled.derived_diagnostics.budget_exhausted - + compiled.derived_diagnostics.scc_budget_exhausted, - summary_scc_count: compiled.derived_diagnostics.scc_count, - branch_fact_count: compiled.symbolic_facts.branch_facts.len(), - worker_island_count: compiled.symbolic_facts.worker_islands.len(), - control_island_count: compiled.symbolic_facts.control_islands.len(), - memory_island_count: compiled.symbolic_facts.memory_islands.len(), - compiled_condition_count: branch_compiled_conditions.len(), - exact_compiled_condition_count: control_facts - .iter() - .filter(|fact| fact.evidence.allows_hard_proof()) - .count(), - actionable_compiled_condition_count: control_facts - .iter() - .filter(|fact| fact.evidence.allows_narrowing()) - .count(), - branches_pruned: compiled.symbolic_facts.diagnostics.branches_pruned, - branches_unknown: compiled.symbolic_facts.diagnostics.branches_unknown, - skipped_large_cfg: compiled.symbolic_facts.diagnostics.skipped_large_cfg, + ambiguous_target_count: compiled.ambiguous_targets().len(), + ambiguous_targets: compiled + .ambiguous_targets() + .into_iter() + .map(|target| format!("0x{target:x}")) + .collect(), + closure_functions: compiled + .native_body() + .map(|body| body.summary.closure_functions) + .unwrap_or(0), + helper_functions: compiled + .native_body() + .map(|body| body.summary.helper_functions) + .unwrap_or(0), + derived_summaries: compiled + .native_body() + .map(|body| body.summary.derived_summaries) + .unwrap_or(0), + summary_attempted: compiled + .native_body() + .map(|body| body.summary.derived_diagnostics.attempted) + .unwrap_or(0), + summary_budget_exhausted: compiled + .native_body() + .map(|body| { + body.summary.derived_diagnostics.budget_exhausted + + body.summary.derived_diagnostics.scc_budget_exhausted + }) + .unwrap_or(0), + summary_scc_count: compiled + .native_body() + .map(|body| body.summary.derived_diagnostics.scc_count) + .unwrap_or(0), + region_count: native.map(|body| body.regions.len()).unwrap_or(0), + control_region_count: native + .map(|body| { + body.regions + .values() + .filter(|region| !region.control.is_empty()) + .count() + }) + .unwrap_or(0), + memory_region_count: native + .map(|body| { + body.regions + .values() + .filter(|region| !region.memory.is_empty()) + .count() + }) + .unwrap_or(0), + compiled_condition_count: compiled.actionable_control_count(), + exact_compiled_condition_count: compiled.exact_control_count(), + actionable_compiled_condition_count: compiled.actionable_control_count(), + branches_pruned: compiled.diagnostics.branches_pruned, + branches_unknown: compiled.diagnostics.branches_unknown, + skipped_large_cfg: compiled.diagnostics.skipped_large_cfg, interpreter: compiled - .interpreter + .vm_body() + .and_then(|body| body.interpreter.as_ref()) .as_ref() .map(|interpreter| InterpreterDispatchInfo { kind: match interpreter.kind { @@ -873,12 +941,15 @@ pub(crate) fn compiled_semantic_info( back_edges: interpreter.back_edges, score: interpreter.score, }), - vm_step: compiled.vm_step.as_ref().map(vm_step_summary_info_from_sym), + vm_step: compiled + .vm_body() + .and_then(|body| body.step_summary.as_ref()) + .map(vm_step_summary_info_from_sym), vm_transfer: compiled - .vm_transfer - .as_ref() + .vm_body() + .and_then(|body| body.transfer_summary.as_ref()) .map(vm_step_summary_info_from_sym), - cache_hit: compiled.cache_hit, + cache_hit: compiled.diagnostics.cache_hit, } } diff --git a/r2plugin/src/decompiler.rs b/r2plugin/src/decompiler.rs index 43c26ec..a6ec064 100644 --- a/r2plugin/src/decompiler.rs +++ b/r2plugin/src/decompiler.rs @@ -20,14 +20,14 @@ pub(crate) fn build_decompiler_env(ctx: &PluginCtxView<'_>) -> DecompilerEnv { } pub(crate) fn build_decompiler_context( - type_facts: r2types::FunctionTypeFacts, + function_facts: r2types::FunctionFacts, function_names: HashMap, strings: HashMap, symbols: HashMap, ptr_bits: u32, ) -> r2dec::DecompilerContext { - r2dec::DecompilerContext::from_analysis_inputs( - type_facts, + r2dec::DecompilerContext::from_function_facts( + function_facts, function_names, strings, symbols, @@ -44,13 +44,13 @@ pub(crate) fn decompiler_input_from_artifact( ) -> r2dec::DecompilerInput { let FunctionAnalysisArtifact { ssa_func, - type_facts, + function_facts, interproc_summary_set, .. } = artifact; r2dec::DecompilerInput::new( ssa_func, - build_decompiler_context(type_facts, function_names, strings, symbols, ptr_bits), + build_decompiler_context(function_facts, function_names, strings, symbols, ptr_bits), ) .with_interproc_summary_set(interproc_summary_set) } @@ -62,18 +62,16 @@ fn rename_function_artifact_for_display( let FunctionAnalysisArtifact { ssa_func, pattern_ssa_func, - type_facts, + function_facts, writeback_plan, interproc_summary_set, - semantic_artifact, } = artifact; FunctionAnalysisArtifact { ssa_func: ssa_func.with_name(function_name), pattern_ssa_func: pattern_ssa_func.with_name(function_name), - type_facts, + function_facts, writeback_plan, interproc_summary_set, - semantic_artifact, } } @@ -109,18 +107,19 @@ pub(crate) fn run_full_decompile_on_large_stack( &symbols, ); let mut artifact = if let Some(artifact) = cached_artifact { - let allow_semantic_decompile = artifact.type_facts.symbolic_facts.decompile_ready(); - if let Some(comment) = r2dec::preferred_semantic_fallback_comment( + if let Some(route) = r2dec::detached_semantic_route_plan( &display_func_name, - &artifact.type_facts.symbolic_facts, - ) - { - return comment; - } - if let Some(reason) = cfg_guard_reason.as_ref() - && !allow_semantic_decompile - { - return r2dec::artifact_guard_fallback_comment(&func_name_str, reason); + &r2il_blocks, + artifact.function_facts.semantics.as_ref(), + ) { + if let r2dec::SemanticRoutePlan::FallbackComment { comment } = route { + return comment; + } + if let Some(reason) = cfg_guard_reason.as_ref() + && matches!(route, r2dec::SemanticRoutePlan::Standard) + { + return r2dec::artifact_guard_fallback_comment(&func_name_str, reason); + } } artifact } else { @@ -135,24 +134,19 @@ pub(crate) fn run_full_decompile_on_large_stack( ) }, ); - let precomputed_symbolic_facts = precomputed_semantic_artifact - .as_ref() - .map(r2types::symbolic_semantic_facts_from_artifact); - let allow_semantic_decompile = precomputed_symbolic_facts - .as_ref() - .is_some_and(r2types::SymbolicSemanticFacts::decompile_ready); - if let Some(symbolic_facts) = precomputed_symbolic_facts.as_ref() - && let Some(comment) = r2dec::preferred_semantic_fallback_comment( - &display_func_name, - symbolic_facts, - ) - { - return comment; - } - if let Some(reason) = cfg_guard_reason.as_ref() - && !allow_semantic_decompile - { - return r2dec::artifact_guard_fallback_comment(&func_name_str, reason); + if let Some(route) = r2dec::detached_semantic_route_plan( + &display_func_name, + &r2il_blocks, + precomputed_semantic_artifact.as_ref(), + ) { + if let r2dec::SemanticRoutePlan::FallbackComment { comment } = route { + return comment; + } + if let Some(reason) = cfg_guard_reason.as_ref() + && matches!(route, r2dec::SemanticRoutePlan::Standard) + { + return r2dec::artifact_guard_fallback_comment(&func_name_str, reason); + } } let Some(artifact) = crate::types::build_detached_function_analysis_artifact_with_scope_and_semantics( @@ -177,8 +171,10 @@ pub(crate) fn run_full_decompile_on_large_stack( artifact = rename_function_artifact_for_display(artifact, &display_func_name); let decompiler = r2dec::Decompiler::new(config); - let semantic_fallback_output = - r2dec::semantic_fallback_comment(&display_func_name, &artifact.type_facts.symbolic_facts); + let semantic_fallback_output = r2dec::semantic_fallback_comment( + &display_func_name, + artifact.function_facts.semantics.as_ref(), + ); let input = decompiler_input_from_artifact( artifact, function_names, diff --git a/r2plugin/src/lib.rs b/r2plugin/src/lib.rs index 5937fa3..bac4053 100644 --- a/r2plugin/src/lib.rs +++ b/r2plugin/src/lib.rs @@ -3327,41 +3327,6 @@ struct InterprocSummaryJson { scope: Option, } -#[derive(Debug, serde::Serialize)] -struct SymbolicBranchJson { - block_addr: u64, - true_target: u64, - false_target: u64, - true_status: String, - false_status: String, - #[serde(skip_serializing_if = "Option::is_none")] - true_condition: Option, - #[serde(skip_serializing_if = "Option::is_none")] - false_condition: Option, - #[serde(skip_serializing_if = "Option::is_none")] - true_compiled: Option, - #[serde(skip_serializing_if = "Option::is_none")] - false_compiled: Option, -} - -#[derive(Debug, serde::Serialize)] -struct SymbolicFactsJson { - branches: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - worker_islands: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - control_islands: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - memory_islands: Vec, - diagnostics: r2types::SymbolicFactDiagnostics, - #[serde(skip_serializing_if = "Option::is_none")] - interpreter: Option, - #[serde(skip_serializing_if = "Option::is_none")] - vm_step: Option, - #[serde(skip_serializing_if = "Option::is_none")] - vm_transfer: Option, -} - #[derive(Debug, serde::Serialize, Default)] struct TypeWritebackDiagnosticsJson { conflicts: Vec, @@ -3385,74 +3350,12 @@ struct InferredTypeWritebackJson { global_type_links: Vec, interproc: InterprocSummaryJson, #[serde(skip_serializing_if = "Option::is_none")] - symbolic: Option, + semantics: Option, #[serde(skip_serializing_if = "Option::is_none")] compiled_semantics: Option, diagnostics: TypeWritebackDiagnosticsJson, } -fn symbolic_reachability_status_json(status: r2types::SymbolicReachabilityStatus) -> String { - match status { - r2types::SymbolicReachabilityStatus::Reachable => "reachable".to_string(), - r2types::SymbolicReachabilityStatus::Unreachable => "unreachable".to_string(), - r2types::SymbolicReachabilityStatus::Unknown => "unknown".to_string(), - } -} - -fn symbolic_facts_json(facts: &r2types::SymbolicSemanticFacts) -> Option { - (!facts.is_empty()).then(|| SymbolicFactsJson { - branches: facts - .branch_facts - .iter() - .map(|fact| SymbolicBranchJson { - block_addr: fact.block_addr, - true_target: fact.true_target, - false_target: fact.false_target, - true_status: symbolic_reachability_status_json(fact.true_status), - false_status: symbolic_reachability_status_json(fact.false_status), - true_condition: fact.true_condition.clone(), - false_condition: fact.false_condition.clone(), - true_compiled: fact.true_compiled.clone(), - false_compiled: fact.false_compiled.clone(), - }) - .collect(), - worker_islands: facts.worker_islands.clone(), - control_islands: facts.control_islands.clone(), - memory_islands: facts.memory_islands.clone(), - diagnostics: facts.diagnostics.clone(), - interpreter: facts.interpreter.clone(), - vm_step: facts.vm_step.clone(), - vm_transfer: facts.vm_transfer.clone(), - }) -} - -fn symbolic_facts_json_forced(facts: &r2types::SymbolicSemanticFacts) -> SymbolicFactsJson { - SymbolicFactsJson { - branches: facts - .branch_facts - .iter() - .map(|fact| SymbolicBranchJson { - block_addr: fact.block_addr, - true_target: fact.true_target, - false_target: fact.false_target, - true_status: symbolic_reachability_status_json(fact.true_status), - false_status: symbolic_reachability_status_json(fact.false_status), - true_condition: fact.true_condition.clone(), - false_condition: fact.false_condition.clone(), - true_compiled: fact.true_compiled.clone(), - false_compiled: fact.false_compiled.clone(), - }) - .collect(), - worker_islands: facts.worker_islands.clone(), - control_islands: facts.control_islands.clone(), - memory_islands: facts.memory_islands.clone(), - diagnostics: facts.diagnostics.clone(), - interpreter: facts.interpreter.clone(), - vm_step: facts.vm_step.clone(), - vm_transfer: facts.vm_transfer.clone(), - } -} - fn evidence_json(evidence: &[r2types::WritebackEvidence]) -> Vec { evidence .iter() @@ -3475,7 +3378,8 @@ fn struct_fields_json(fields: &[r2types::StructFieldCandidate]) -> Vec, + semantics: Option, + compiled_semantics: Option, ) -> InferredTypeWritebackJson { InferredTypeWritebackJson { function_name: plan.signature.function_name, @@ -3543,8 +3447,8 @@ fn writeback_plan_json( }) .collect(), interproc, - symbolic, - compiled_semantics: None, + semantics, + compiled_semantics, diagnostics: TypeWritebackDiagnosticsJson { conflicts: plan.diagnostics.conflicts, warnings: plan.diagnostics.warnings, @@ -3557,6 +3461,10 @@ fn type_writeback_payload_from_artifact( artifact: types::FunctionAnalysisArtifact, interproc: InterprocInferenceInput<'_>, ) -> InferredTypeWritebackJson { + let semantics = artifact.function_facts.semantics.clone(); + let compiled_semantics = semantics + .as_ref() + .map(analysis::sym::compiled_semantic_info); let ssa_blocks = artifact.pattern_ssa_func.local_ssa_blocks(); let scope = serde_json::from_str::(interproc.scope_json) .ok() @@ -3584,7 +3492,8 @@ fn type_writeback_payload_from_artifact( summary_json: current_summary_json, scope, }, - symbolic_facts_json(&artifact.type_facts.symbolic_facts), + semantics, + compiled_semantics, ) } @@ -3593,16 +3502,11 @@ fn semantic_type_fallback_payload( arch_name: &str, ptr_bits: u32, interproc: InterprocInferenceInput<'_>, - compiled: &r2sym::CompiledSemanticArtifact, + compiled: &r2sym::SemanticArtifact, ) -> InferredTypeWritebackJson { let compiled_info = analysis::sym::compiled_semantic_info(compiled); - let symbolic_facts = r2types::symbolic_semantic_facts_from_artifact(compiled); - let plan = r2types::build_semantic_type_fallback_plan( - function_name, - arch_name, - ptr_bits, - &symbolic_facts, - ); + let plan = + r2types::build_semantic_type_fallback_plan(function_name, arch_name, ptr_bits, compiled); InferredTypeWritebackJson { function_name: plan.signature.function_name, @@ -3657,7 +3561,7 @@ fn semantic_type_fallback_payload( !v.is_null() && v.as_object().map(|obj| !obj.is_empty()).unwrap_or(true) }), }, - symbolic: Some(symbolic_facts_json_forced(&symbolic_facts)), + semantics: Some(compiled.clone()), compiled_semantics: Some(compiled_info), diagnostics: TypeWritebackDiagnosticsJson { warnings: plan.diagnostics.warnings, @@ -5586,11 +5490,8 @@ fn infer_type_writeback_json_impl(input: TypeWritebackInferenceInput<'_>) -> *mu &external_context, symbolic_scope.as_ref(), ) { - if let Some(compiled) = cached_artifact.semantic_artifact.as_ref() - && cached_artifact - .type_facts - .symbolic_facts - .prefers_bounded_type_plan() + if let Some(compiled) = cached_artifact.function_facts.semantics.as_ref() + && r2types::semantic_artifact_prefers_bounded_type_plan(compiled) { semantic_type_fallback_payload( &function_input.function_name, @@ -5612,9 +5513,7 @@ fn infer_type_writeback_json_impl(input: TypeWritebackInferenceInput<'_>) -> *mu symbolic_scope.as_ref(), function_input.ctx.arch, ); - if r2types::symbolic_semantic_facts_from_artifact(&semantic_artifact) - .prefers_bounded_type_plan() - { + if r2types::semantic_artifact_prefers_bounded_type_plan(&semantic_artifact) { semantic_type_fallback_payload( &function_input.function_name, &arch_name, @@ -5658,7 +5557,7 @@ fn semantic_worker_linearization_impl(input: SemanticWorkerLinearizationInput) - switch_block_count: usize::from(input.max_switch_cases > 0), max_switch_cases: input.max_switch_cases, }; - let Some(reason) = r2dec::cfg_guard_reason_from_summary(&summary) else { + let Some(_reason) = r2dec::cfg_guard_reason_from_summary(&summary) else { return ptr::null_mut(); }; let Some(function_input) = types::build_function_input( @@ -5683,37 +5582,47 @@ fn semantic_worker_linearization_impl(input: SemanticWorkerLinearizationInput) - } }; let (arch_name, ptr_bits, _) = r2dec::DecompilerConfig::for_arch(function_input.ctx.arch); - let symbolic_facts = if let Some(cached_artifact) = + let semantic_artifact = if let Some(cached_artifact) = types::get_cached_function_analysis_artifact_with_scope( &function_input, "{}", symbolic_scope.as_ref(), ) { - cached_artifact.type_facts.symbolic_facts + cached_artifact.function_facts.semantics } else { let Some(analysis) = types::build_function_analysis(&function_input) else { return ptr::null_mut(); }; - let semantic_artifact = r2sym::compile_semantic_artifact_default_with_scope( + Some(r2sym::compile_semantic_artifact_default_with_scope( &z3::Context::thread_local(), &analysis.ssa_func, symbolic_scope.as_ref(), function_input.ctx.arch, - ); - r2types::symbolic_semantic_facts_from_artifact(&semantic_artifact) + )) }; - if !symbolic_facts.decompile_ready() || symbolic_facts.structured_decompile_ready() { + let Some(semantic_artifact) = semantic_artifact else { return ptr::null_mut(); - } + }; + let Some(route) = r2dec::detached_semantic_route_plan( + &function_input.function_name, + function_input.blocks.as_slice(), + Some(&semantic_artifact), + ) else { + return ptr::null_mut(); + }; + let reason = match route { + r2dec::SemanticRoutePlan::LinearWorker { reason } => reason, + _ => return ptr::null_mut(), + }; let plan = r2types::build_semantic_type_fallback_plan( &function_input.function_name, &arch_name, ptr_bits, - &symbolic_facts, + &semantic_artifact, ); CString::new(r2dec::render_semantic_worker_linearization( &plan, - &symbolic_facts, + Some(&semantic_artifact), &reason, )) .map_or(ptr::null_mut(), |c| c.into_raw()) @@ -7432,87 +7341,109 @@ mod tests { } } - fn test_large_cfg_semantic_artifact() -> r2sym::CompiledSemanticArtifact { + fn test_large_cfg_semantic_artifact() -> r2sym::SemanticArtifact { let compiled = test_compiled_condition("x == 0"); - let control_fact = r2sym::SymbolicControlFact { - target: 0x401020, - status: r2sym::SymbolicReachabilityStatus::Reachable, - condition: Some("x == 0".to_string()), - compiled: Some(compiled.clone()), - evidence: r2sym::SemanticEvidence::exact(), + let region = r2sym::SemanticRegion { + anchor: 0x401000, + frontier: std::collections::BTreeSet::from([0x401020, 0x401030]), + control: vec![ + r2sym::Judged::new( + r2sym::ControlFact { + target: 0x401020, + status: r2sym::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + condition: Some("x == 0".to_string()), + compiled: Some(compiled.clone()), + }, + r2sym::SemanticEvidence::exact(), + ), + r2sym::Judged::new( + r2sym::ControlFact { + target: 0x401030, + status: r2sym::SymbolicReachabilityStatus::Unreachable, + branch_truth: Some(false), + condition: Some("x != 0".to_string()), + compiled: None, + }, + r2sym::SemanticEvidence::exact(), + ), + r2sym::Judged::new( + r2sym::ControlFact { + target: 0x401020, + status: r2sym::SymbolicReachabilityStatus::Reachable, + branch_truth: None, + condition: Some("x == 0".to_string()), + compiled: Some(compiled.clone()), + }, + r2sym::SemanticEvidence::exact(), + ), + ], + memory: Vec::new(), + pre: Vec::new(), + post: Vec::new(), + targets: vec![ + r2sym::Judged::new( + r2sym::TargetFact { + target: 0x401020, + status: r2sym::SymbolicReachabilityStatus::Reachable, + branch_truth: Some(true), + }, + r2sym::SemanticEvidence::exact(), + ), + r2sym::Judged::new( + r2sym::TargetFact { + target: 0x401030, + status: r2sym::SymbolicReachabilityStatus::Unreachable, + branch_truth: Some(false), + }, + r2sym::SemanticEvidence::exact(), + ), + ], }; - let symbolic_facts = r2sym::SymbolicFunctionFacts { - branch_facts: vec![r2sym::SymbolicBranchFact { - block_addr: 0x401000, - true_target: 0x401020, - false_target: 0x401030, - true_status: r2sym::SymbolicReachabilityStatus::Reachable, - false_status: r2sym::SymbolicReachabilityStatus::Unreachable, - true_condition: Some("x == 0".to_string()), - false_condition: Some("x != 0".to_string()), - true_compiled: Some(compiled.clone()), - false_compiled: None, - }], - worker_islands: Vec::new(), - control_islands: vec![r2sym::SymbolicControlIsland { - kind: r2sym::SymbolicControlIslandKind::LargeCfgBranchFrontier, - anchor_block: 0x401000, - frontier_targets: vec![0x401020], - facts: vec![control_fact], - evidence: r2sym::SemanticEvidence::exact(), - }], - memory_islands: Vec::new(), - diagnostics: r2sym::SymbolicFunctionFactDiagnostics { + r2sym::SemanticArtifact { + stage: r2sym::RefinementStage::Residual, + granularity: r2sym::ArtifactGranularity::Regioned, + execution: r2sym::ExecutionModel::Native, + body: r2sym::SemanticArtifactBody::Native(r2sym::NativeArtifactBody { + summary: r2sym::NativeFunctionSummary { + slice_class: r2sym::SliceClass::Worker, + closure_functions: 4, + helper_functions: 3, + derived_summaries: 0, + derived_diagnostics: r2sym::DerivedSummaryDiagnostics::default(), + }, + regions: std::iter::once((region.key(), region)).collect(), + }), + diagnostics: r2sym::SemanticArtifactDiagnostics { branches_evaluated: 1, branches_pruned: 0, branches_unknown: 0, skipped_missing_arch: false, skipped_large_cfg: true, + residual_reasons: vec![r2sym::ResidualReason::LargeCfg], + ambiguous_targets: Vec::new(), + cache_hit: false, }, - }; - - r2sym::CompiledSemanticArtifact { - mode: r2sym::SemanticMode::Residual, - slice_class: r2sym::SliceClass::Worker, - capability: r2sym::SemanticCapability { - query_ready: true, - type_ready: true, - decompile_ready: true, - }, - residual_reasons: vec![r2sym::ResidualReason::LargeCfg], - closure_functions: 4, - helper_functions: 3, - derived_summaries: 0, - derived_diagnostics: r2sym::DerivedSummaryDiagnostics::default(), - symbolic_facts, - interpreter: None, - vm_step: None, - vm_transfer: None, - cache_hit: false, } } #[test] fn decompile_ready_large_cfg_worker_keeps_real_decompile_path() { let compiled = test_large_cfg_semantic_artifact(); - let symbolic_facts = r2types::symbolic_semantic_facts_from_artifact(&compiled); assert!( - r2dec::preferred_semantic_fallback_comment("fcn.401000", &symbolic_facts).is_none() + r2dec::preferred_semantic_fallback_comment("fcn.401000", Some(&compiled)).is_none() ); } #[test] - fn semantic_fallback_text_reports_actionable_control_island_counts() { + fn semantic_fallback_text_reports_canonical_region_counts() { let compiled = test_large_cfg_semantic_artifact(); - let symbolic_facts = r2types::symbolic_semantic_facts_from_artifact(&compiled); - let output = r2dec::semantic_fallback_comment("_401000", &symbolic_facts) + let output = r2dec::semantic_fallback_comment("_401000", Some(&compiled)) .expect("typed semantic fallback comment"); assert!(output.contains("semantic fallback: worker slice in residual mode")); - assert!(output.contains("branch_facts=1")); - assert!(output.contains("control_islands=1")); - assert!(output.contains("actionable_conditions=1")); - assert!(output.contains("exact_conditions=1")); - assert!(output.contains("actionable_preview=[0x401000: x == 0]")); + assert!(output.contains("regions=1")); + assert!(output.contains("actionable_conditions=3")); + assert!(output.contains("exact_conditions=3")); } #[test] @@ -9517,7 +9448,8 @@ mod integration_tests { .expect("analysis artifact"); let rendered = artifact - .type_facts + .function_facts + .types .merged_signature .as_ref() .and_then(|sig| sig.params.first()) @@ -9531,10 +9463,10 @@ mod integration_tests { && compact.ends_with('*') && !compact.eq_ignore_ascii_case("void*"), "expected lifted-byte x86 artifact to override arg0 to a struct pointer, got signature={:?}, slot_overrides={:?}, slot_fields={:?}, type_db={:?}, semantic_structs={:?}, semantic_diagnostics={:?}, raw_structs={:?}, raw_diagnostics={:?}, pattern_ssa_blocks={:?}", - artifact.type_facts.merged_signature, - artifact.type_facts.slot_type_overrides, - artifact.type_facts.slot_field_profiles, - artifact.type_facts.external_type_db.structs, + artifact.function_facts.types.merged_signature, + artifact.function_facts.types.slot_type_overrides, + artifact.function_facts.types.slot_field_profiles, + artifact.function_facts.types.external_type_db.structs, semantic_structs, semantic_diagnostics, raw_structs, @@ -9596,16 +9528,17 @@ mod integration_tests { assert_eq!( artifact - .type_facts + .function_facts + .types .slot_type_overrides .get(&0) .map(String::as_str), Some("struct sla_struct_420703e08f70f00e *"), "expected live-context detached artifact to keep the local struct override, got merged_signature={:?}, slot_overrides={:?}, slot_fields={:?}, type_db={:?}", - artifact.type_facts.merged_signature, - artifact.type_facts.slot_type_overrides, - artifact.type_facts.slot_field_profiles, - artifact.type_facts.external_type_db.structs + artifact.function_facts.types.merged_signature, + artifact.function_facts.types.slot_type_overrides, + artifact.function_facts.types.slot_field_profiles, + artifact.function_facts.types.external_type_db.structs ); } @@ -9679,7 +9612,8 @@ mod integration_tests { ); let db_struct = artifact - .type_facts + .function_facts + .types .external_type_db .structs .get("sla_struct_420703e08f70f00e") @@ -9744,7 +9678,7 @@ mod integration_tests { .expect("analysis artifact"); let mut decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); - decompiler.set_type_facts(artifact.type_facts.clone()); + decompiler.set_type_facts(artifact.function_facts.types.clone()); decompiler.set_function_names(HashMap::from([ (0x401140, "sym.imp.memcpy".to_string()), (0x401150, "sym.imp.malloc".to_string()), @@ -9797,72 +9731,72 @@ mod integration_tests { .ops = pattern_ssa_blocks[0].ops.clone(); manual_func = manual_func.with_name("dbg.test_struct_array_index"); let mut manual_decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); - manual_decompiler.set_type_facts(artifact.type_facts.clone()); + manual_decompiler.set_type_facts(artifact.function_facts.types.clone()); let manual_output = manual_decompiler.decompile(&manual_func); assert!( output.contains("[idx].f_8"), "expected detached artifact store rendering in decompiled output, got:\n{output}\nmanual_output:\n{manual_output}\ntail_ops={tail_ops:?}\nregister_params={:?}\nmerged_signature={:?}\nslot_overrides={:?}\nslot_fields={:?}\ntype_db={:?}", - artifact.type_facts.register_params, - artifact.type_facts.merged_signature, - artifact.type_facts.slot_type_overrides, - artifact.type_facts.slot_field_profiles, - artifact.type_facts.external_type_db.structs + artifact.function_facts.types.register_params, + artifact.function_facts.types.merged_signature, + artifact.function_facts.types.slot_type_overrides, + artifact.function_facts.types.slot_field_profiles, + artifact.function_facts.types.external_type_db.structs ); assert!( output.contains("[idx].f_34"), "expected detached artifact load rendering in decompiled output, got:\n{output}\nmanual_output:\n{manual_output}\ntail_ops={tail_ops:?}\nregister_params={:?}\nmerged_signature={:?}\nslot_overrides={:?}\nslot_fields={:?}\ntype_db={:?}", - artifact.type_facts.register_params, - artifact.type_facts.merged_signature, - artifact.type_facts.slot_type_overrides, - artifact.type_facts.slot_field_profiles, - artifact.type_facts.external_type_db.structs + artifact.function_facts.types.register_params, + artifact.function_facts.types.merged_signature, + artifact.function_facts.types.slot_type_overrides, + artifact.function_facts.types.slot_field_profiles, + artifact.function_facts.types.external_type_db.structs ); assert!( output.contains("return arr[idx].f_8 + arr[idx].f_34;") || output.contains("return arr[idx].f_34 + arr[idx].f_8;"), "expected detached artifact return to preserve both member loads, got:\n{output}\nmanual_output:\n{manual_output}\ntail_ops={tail_ops:?}\nregister_params={:?}\nmerged_signature={:?}\nslot_overrides={:?}\nslot_fields={:?}\ntype_db={:?}", - artifact.type_facts.register_params, - artifact.type_facts.merged_signature, - artifact.type_facts.slot_type_overrides, - artifact.type_facts.slot_field_profiles, - artifact.type_facts.external_type_db.structs + artifact.function_facts.types.register_params, + artifact.function_facts.types.merged_signature, + artifact.function_facts.types.slot_type_overrides, + artifact.function_facts.types.slot_field_profiles, + artifact.function_facts.types.external_type_db.structs ); assert!( !output.contains("local_c ="), "dead x86 stack-home index carrier should not leak into decompiled output, got:\n{output}\nmanual_output:\n{manual_output}\ntail_ops={tail_ops:?}\nregister_params={:?}\nmerged_signature={:?}\nslot_overrides={:?}\nslot_fields={:?}\ntype_db={:?}", - artifact.type_facts.register_params, - artifact.type_facts.merged_signature, - artifact.type_facts.slot_type_overrides, - artifact.type_facts.slot_field_profiles, - artifact.type_facts.external_type_db.structs + artifact.function_facts.types.register_params, + artifact.function_facts.types.merged_signature, + artifact.function_facts.types.slot_type_overrides, + artifact.function_facts.types.slot_field_profiles, + artifact.function_facts.types.external_type_db.structs ); assert!( !output.contains("local_"), "autogenerated x86 stack-home locals should not leak into decompiled output, got:\n{output}\nmanual_output:\n{manual_output}\ntail_ops={tail_ops:?}\nregister_params={:?}\nmerged_signature={:?}\nslot_overrides={:?}\nslot_fields={:?}\ntype_db={:?}", - artifact.type_facts.register_params, - artifact.type_facts.merged_signature, - artifact.type_facts.slot_type_overrides, - artifact.type_facts.slot_field_profiles, - artifact.type_facts.external_type_db.structs + artifact.function_facts.types.register_params, + artifact.function_facts.types.merged_signature, + artifact.function_facts.types.slot_type_overrides, + artifact.function_facts.types.slot_field_profiles, + artifact.function_facts.types.external_type_db.structs ); assert!( output.contains("arr[idx].f_8 = v;"), "expected detached artifact store to inline the parameter value, got:\n{output}\nmanual_output:\n{manual_output}\ntail_ops={tail_ops:?}\nregister_params={:?}\nmerged_signature={:?}\nslot_overrides={:?}\nslot_fields={:?}\ntype_db={:?}", - artifact.type_facts.register_params, - artifact.type_facts.merged_signature, - artifact.type_facts.slot_type_overrides, - artifact.type_facts.slot_field_profiles, - artifact.type_facts.external_type_db.structs + artifact.function_facts.types.register_params, + artifact.function_facts.types.merged_signature, + artifact.function_facts.types.slot_type_overrides, + artifact.function_facts.types.slot_field_profiles, + artifact.function_facts.types.external_type_db.structs ); assert!( !output.contains("(int64_t)arr[idx].f_34"), "x86 scalar return should not widen the member load in decompiled output, got:\n{output}\nmanual_output:\n{manual_output}\ntail_ops={tail_ops:?}\nregister_params={:?}\nmerged_signature={:?}\nslot_overrides={:?}\nslot_fields={:?}\ntype_db={:?}", - artifact.type_facts.register_params, - artifact.type_facts.merged_signature, - artifact.type_facts.slot_type_overrides, - artifact.type_facts.slot_field_profiles, - artifact.type_facts.external_type_db.structs + artifact.function_facts.types.register_params, + artifact.function_facts.types.merged_signature, + artifact.function_facts.types.slot_type_overrides, + artifact.function_facts.types.slot_field_profiles, + artifact.function_facts.types.external_type_db.structs ); } @@ -9925,7 +9859,8 @@ mod integration_tests { assert_eq!( artifact - .type_facts + .function_facts + .types .merged_signature .as_ref() .expect("merged signature") @@ -9933,7 +9868,7 @@ mod integration_tests { .len(), 2, "expected authoritative external signature to keep two params, got merged_signature={:?}", - artifact.type_facts.merged_signature + artifact.function_facts.types.merged_signature ); assert_eq!( artifact.writeback_plan.signature.params.len(), @@ -9944,16 +9879,16 @@ mod integration_tests { assert_eq!( artifact.writeback_plan.signature.params[0].name, "src", "expected first param name to come from external signature, got merged_signature={:?}, writeback_signature={:?}", - artifact.type_facts.merged_signature, artifact.writeback_plan.signature + artifact.function_facts.types.merged_signature, artifact.writeback_plan.signature ); assert_eq!( artifact.writeback_plan.signature.params[1].name, "len", "expected second param name to come from external signature, got merged_signature={:?}, writeback_signature={:?}", - artifact.type_facts.merged_signature, artifact.writeback_plan.signature + artifact.function_facts.types.merged_signature, artifact.writeback_plan.signature ); let mut decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); - decompiler.set_type_facts(artifact.type_facts.clone()); + decompiler.set_type_facts(artifact.function_facts.types.clone()); decompiler.set_function_names(HashMap::from([ (0x401140, "sym.imp.memcpy".to_string()), (0x401150, "sym.imp.malloc".to_string()), @@ -9995,19 +9930,19 @@ mod integration_tests { assert!( output.contains("sym.imp.memcpy(buf, src, len);"), "expected detached x86 alloc_and_copy to keep the malloc owner for memcpy, got:\n{output}\nssa_ops={ssa_ops:?}\nmerged_signature={:?}\nwriteback_signature={:?}", - artifact.type_facts.merged_signature, + artifact.function_facts.types.merged_signature, artifact.writeback_plan.signature ); assert!( output.contains("buf[len] = 0;"), "expected detached x86 alloc_and_copy to keep the malloc owner for the NUL store, got:\n{output}\nmerged_signature={:?}\nwriteback_signature={:?}", - artifact.type_facts.merged_signature, + artifact.function_facts.types.merged_signature, artifact.writeback_plan.signature ); assert!( output.contains("return buf;"), "expected detached x86 alloc_and_copy to return the owned malloc result, got:\n{output}\nmerged_signature={:?}\nwriteback_signature={:?}", - artifact.type_facts.merged_signature, + artifact.function_facts.types.merged_signature, artifact.writeback_plan.signature ); } @@ -10072,7 +10007,7 @@ mod integration_tests { .expect("analysis artifact"); let mut decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); - decompiler.set_type_facts(artifact.type_facts.clone()); + decompiler.set_type_facts(artifact.function_facts.types.clone()); decompiler.set_function_names(HashMap::from([ (0x401110, "sym.imp.printf".to_string()), (0x401140, "sym.imp.memcpy".to_string()), @@ -10210,7 +10145,7 @@ mod integration_tests { .expect("analysis artifact"); let mut decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); - decompiler.set_type_facts(artifact.type_facts.clone()); + decompiler.set_type_facts(artifact.function_facts.types.clone()); decompiler.set_function_names(HashMap::from([(0x401130, "sym.imp.strcmp".to_string())])); decompiler.set_strings(HashMap::from([(0x403014, "secret123".to_string())])); decompiler.set_known_function_signatures(HashMap::from([( @@ -10348,7 +10283,7 @@ mod integration_tests { .collect(); let mut decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); - decompiler.set_type_facts(artifact.type_facts.clone()); + decompiler.set_type_facts(artifact.function_facts.types.clone()); let output = decompiler.decompile(&artifact.ssa_func); let output_without_types = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()).decompile(&artifact.ssa_func); @@ -10443,9 +10378,9 @@ mod integration_tests { .expect("analysis artifact"); let mut decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); - decompiler.set_type_facts(artifact.type_facts.clone()); + decompiler.set_type_facts(artifact.function_facts.types.clone()); let output = decompiler.decompile(&artifact.ssa_func); - let visible_bindings = artifact.type_facts.visible_bindings.clone(); + let visible_bindings = artifact.function_facts.types.visible_bindings.clone(); assert!( output.contains("if (x != y)") @@ -10523,7 +10458,7 @@ mod integration_tests { .expect("analysis artifact"); let mut decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); - decompiler.set_type_facts(artifact.type_facts.clone()); + decompiler.set_type_facts(artifact.function_facts.types.clone()); decompiler.set_function_names(HashMap::from([(0x401160, "sym.imp.setlocale".to_string())])); decompiler.set_strings(HashMap::from([(0x403040, "C".to_string())])); decompiler.set_known_function_signatures(HashMap::from([( @@ -10746,7 +10681,7 @@ mod integration_tests { ) .expect("analysis artifact"); - let mut type_facts = artifact.type_facts.clone(); + let mut type_facts = artifact.function_facts.types.clone(); type_facts.known_function_signatures.extend(HashMap::from([ ( "sym.imp.strlen".to_string(), @@ -11030,7 +10965,7 @@ mod integration_tests { .expect("analysis artifact"); let mut decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); - decompiler.set_type_facts(artifact.type_facts.clone()); + decompiler.set_type_facts(artifact.function_facts.types.clone()); decompiler.set_function_names(HashMap::from([(0x401100, "sym.imp.strlen".to_string())])); decompiler.set_known_function_signatures(HashMap::from([( "sym.imp.strlen".to_string(), @@ -11156,7 +11091,7 @@ mod integration_tests { .expect("analysis artifact"); let mut decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); - decompiler.set_type_facts(artifact.type_facts.clone()); + decompiler.set_type_facts(artifact.function_facts.types.clone()); decompiler.set_function_names(HashMap::from([(0x401100, "sym.imp.strlen".to_string())])); decompiler.set_known_function_signatures(HashMap::from([( "sym.imp.strlen".to_string(), @@ -11277,7 +11212,7 @@ mod integration_tests { .expect("analysis artifact"); let mut decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); - decompiler.set_type_facts(artifact.type_facts.clone()); + decompiler.set_type_facts(artifact.function_facts.types.clone()); decompiler.set_function_names(HashMap::from([(0x401100, "sym.imp.strlen".to_string())])); decompiler.set_known_function_signatures(HashMap::from([( "sym.imp.strlen".to_string(), @@ -12186,21 +12121,23 @@ mod integration_tests { ) .expect("analysis artifact"); - assert_eq!( - artifact - .type_facts - .symbolic_facts - .diagnostics - .branches_pruned, - 1 - ); - assert_eq!(artifact.type_facts.symbolic_facts.branch_facts.len(), 1); - let symbolic_json = - symbolic_facts_json(&artifact.type_facts.symbolic_facts).expect("symbolic json"); - assert_eq!(symbolic_json.diagnostics.branches_pruned, 1); - assert_eq!(symbolic_json.branches.len(), 1); - assert_eq!(symbolic_json.branches[0].true_status, "unreachable"); - assert_eq!(symbolic_json.branches[0].false_status, "reachable"); + let semantics = artifact + .function_facts + .semantics + .as_ref() + .expect("canonical semantics"); + assert_eq!(semantics.diagnostics.branches_pruned, 1); + let native = semantics.native_body().expect("native semantics"); + assert_eq!(native.regions.len(), 1); + let region = native.regions.values().next().expect("semantic region"); + assert!(region.targets.iter().any(|fact| { + fact.value.status == r2sym::SymbolicReachabilityStatus::Unreachable + && fact.value.branch_truth == Some(true) + })); + assert!(region.targets.iter().any(|fact| { + fact.value.status == r2sym::SymbolicReachabilityStatus::Reachable + && fact.value.branch_truth == Some(false) + })); let decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); let output = diff --git a/r2plugin/src/types.rs b/r2plugin/src/types.rs index 9dd4a54..b1a685a 100644 --- a/r2plugin/src/types.rs +++ b/r2plugin/src/types.rs @@ -42,10 +42,9 @@ pub(crate) struct FunctionAnalysis { pub(crate) struct FunctionAnalysisArtifact { pub(crate) ssa_func: r2ssa::SsaArtifact, pub(crate) pattern_ssa_func: r2ssa::SsaArtifact, - pub(crate) type_facts: r2types::FunctionTypeFacts, + pub(crate) function_facts: r2types::FunctionFacts, pub(crate) writeback_plan: r2types::TypeWritebackPlan, pub(crate) interproc_summary_set: Option, - pub(crate) semantic_artifact: Option, } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -165,36 +164,19 @@ fn rename_function_analysis_artifact( let FunctionAnalysisArtifact { ssa_func, pattern_ssa_func, - type_facts, + function_facts, writeback_plan, interproc_summary_set, - semantic_artifact, } = artifact; FunctionAnalysisArtifact { ssa_func: ssa_func.with_name(function_name), pattern_ssa_func: pattern_ssa_func.with_name(function_name), - type_facts, + function_facts, writeback_plan, interproc_summary_set, - semantic_artifact, } } -#[allow(dead_code)] -fn collect_plugin_symbolic_facts( - prepared: &r2ssa::SsaArtifact, - arch: Option<&ArchSpec>, - symbolic_scope: Option<&r2sym::PreparedFunctionScope>, -) -> r2types::SymbolicSemanticFacts { - let artifact = r2sym::compile_semantic_artifact_default_with_scope( - &z3::Context::thread_local(), - prepared, - symbolic_scope, - arch, - ); - r2types::symbolic_semantic_facts_from_artifact(&artifact) -} - #[cfg(test)] fn type_like_to_ctype(ty: &r2types::CTypeLike) -> r2dec::CType { match ty { @@ -365,7 +347,7 @@ pub(crate) fn collect_detached_semantic_artifact( function_name: &str, arch: Option<&ArchSpec>, symbolic_scope: Option<&r2sym::PreparedFunctionScope>, -) -> Option { +) -> Option { let analysis = build_function_analysis_from_parts(function_name, blocks, arch)?; Some(r2sym::compile_semantic_artifact_default_with_scope( &z3::Context::thread_local(), @@ -621,7 +603,7 @@ pub(crate) fn build_function_analysis_artifact_from_analysis_with_semantic_artif analysis: FunctionAnalysis, external_context_json: &str, interproc_summary_set: Option, - semantic_artifact: r2sym::CompiledSemanticArtifact, + semantic_artifact: r2sym::SemanticArtifact, ) -> Option { let ptr_bits = input .ctx @@ -645,8 +627,6 @@ pub(crate) fn build_function_analysis_artifact_from_analysis_with_semantic_artif let local_field_accesses = local_field_accesses_to_writeback( r2dec::infer_local_struct_field_accesses(&analysis.pattern_ssa_func, &decompiler_cfg), ); - let symbolic_facts = r2types::symbolic_semantic_facts_from_artifact(&semantic_artifact); - let reg_type_hints = if input.ctx.semantic_metadata_enabled { collect_register_type_hints(input.blocks.as_slice(), input.ctx.disasm) } else { @@ -672,17 +652,16 @@ pub(crate) fn build_function_analysis_artifact_from_analysis_with_semantic_artif diagnostics, }, r2types::TypeWritebackSemanticInputs { - symbolic_facts: &symbolic_facts, + artifact: &semantic_artifact, local_field_accesses: &local_field_accesses, }, ); Some(FunctionAnalysisArtifact { ssa_func: analysis.ssa_func, pattern_ssa_func: analysis.pattern_ssa_func, - type_facts: writeback.type_facts, + function_facts: writeback.function_facts, writeback_plan: writeback.plan, interproc_summary_set, - semantic_artifact: Some(semantic_artifact), }) } @@ -875,7 +854,7 @@ pub(crate) fn build_detached_function_analysis_artifact_with_scope_and_semantics reg_type_hints: &std::collections::HashMap, external_context_json: &str, symbolic_scope: Option<&r2sym::PreparedFunctionScope>, - precomputed_semantic_artifact: Option, + precomputed_semantic_artifact: Option, ) -> Option { let cache_key = function_artifact_cache_key_parts( function_name, @@ -941,7 +920,6 @@ pub(crate) fn build_detached_function_analysis_artifact_with_scope_and_semantics arch, ) }); - let symbolic_facts = r2types::symbolic_semantic_facts_from_artifact(&semantic_artifact); let vars = recover_vars_from_ssa( &pattern_ssa_blocks, arch, @@ -962,17 +940,16 @@ pub(crate) fn build_detached_function_analysis_artifact_with_scope_and_semantics diagnostics, }, r2types::TypeWritebackSemanticInputs { - symbolic_facts: &symbolic_facts, + artifact: &semantic_artifact, local_field_accesses: &local_field_accesses, }, ); let artifact = FunctionAnalysisArtifact { ssa_func: analysis.ssa_func, pattern_ssa_func: analysis.pattern_ssa_func, - type_facts: writeback.type_facts, + function_facts: writeback.function_facts, writeback_plan: writeback.plan, interproc_summary_set: None, - semantic_artifact: Some(semantic_artifact), }; if let Some(cache_key) = cache_key { cache_insert_bounded(artifact_cache(), cache_key, Arc::new(artifact.clone())); diff --git a/tests/r2r/db/extras/r2sleigh_integration_extended b/tests/r2r/db/extras/r2sleigh_integration_extended index 651cd15..cc4de8c 100644 --- a/tests/r2r/db/extras/r2sleigh_integration_extended +++ b/tests/r2r/db/extras/r2sleigh_integration_extended @@ -48,7 +48,7 @@ CMDS=<=8 and (.semantic.vm_step.loop_latches|length)>=7 and (.semantic.vm_step.step_blocks|length)>=10 and (.semantic.vm_step.handler_state_updates | to_entries | length) >= 5 and ((.semantic.vm_step.handler_state_updates | to_entries | map(.value | length) | add) >= 5)' +a:sla.sym sym.tiny_vm_dispatch | jq -c '.semantic.execution=="vm" and .semantic.granularity=="summary_only" and (.semantic.slice_class|startswith("interpreter_")) and .semantic.type_plan=="Ready" and .semantic.vm_step != null and (.semantic.vm_step.dispatch_targets|length)>=8 and (.semantic.vm_step.loop_latches|length)>=7 and (.semantic.vm_step.step_blocks|length)>=10 and (.semantic.vm_step.handler_state_updates | to_entries | length) >= 5 and ((.semantic.vm_step.handler_state_updates | to_entries | map(.value | length) | add) >= 5)' EOF_CMDS RUN @@ -257,7 +257,7 @@ EOF_EXPECT CMDS=</dev/null -a:sla.sym sym.tiny_vm_dispatch | jq -c '.semantic.mode=="vm_summary" and .semantic.cache_hit==true and .semantic.vm_transfer != null and (.semantic.vm_transfer.transfers | type=="array")' +a:sla.sym sym.tiny_vm_dispatch | jq -c '.semantic.execution=="vm" and .semantic.cache_hit==true and .semantic.vm_transfer != null and (.semantic.vm_transfer.transfers | type=="array")' EOF_CMDS RUN @@ -269,7 +269,7 @@ true EOF_EXPECT CMDS=<=10' +a:sla.sym sym.interpret_bytecode | jq -c '.semantic.execution=="vm" and .semantic.granularity=="summary_only" and (.semantic.slice_class|startswith("interpreter_")) and .semantic.skipped_large_cfg==true and (.semantic.residual_reasons|length)==0 and .semantic.vm_step != null and (.semantic.vm_step.dispatch_targets|length)>=10' EOF_CMDS RUN @@ -281,11 +281,11 @@ true EOF_EXPECT CMDS=<= 8) and ((.semantic.vm_transfer.transfers | map(select(.redispatch)) | length) >= 4) and (((.semantic.vm_transfer.handler_exit_guards // {}) | keys | length) >= 1) and (((.semantic.vm_transfer.handler_memory_read_effects // {}) | keys | length) >= 1) and (((.semantic.vm_transfer.handler_memory_write_effects // {}) | keys | length) >= 1)' +a:sla.sym sym.interpret_bytecode | jq -c '.semantic.execution=="vm" and .semantic.query_plan=="Ready" and .semantic.vm_transfer != null and ((.semantic.vm_transfer.transfers | length) >= 8) and ((.semantic.vm_transfer.transfers | map(select(.redispatch)) | length) >= 4) and (((.semantic.vm_transfer.handler_exit_guards // {}) | keys | length) >= 1) and (((.semantic.vm_transfer.handler_memory_read_effects // {}) | keys | length) >= 1) and (((.semantic.vm_transfer.handler_memory_write_effects // {}) | keys | length) >= 1)' EOF_CMDS RUN -NAME=types_tiny_vm_dispatch_reports_vm_step_payload +NAME=types_tiny_vm_dispatch_reports_canonical_vm_step_payload FILE=bins/stress_test ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true EXPECT=<=8 and (.symbolic.vm_step.loop_latches|length)>=7 and (.symbolic.vm_step.step_blocks|length)>=10 and (.symbolic.vm_step.case_values_by_target | keys | length) >= 8 and (.symbolic.vm_step.handler_state_updates | to_entries | length) >= 5' +a:sla.types sym.tiny_vm_dispatch | jq -c '.compiled_semantics.execution=="vm" and .compiled_semantics.granularity=="summary_only" and (.compiled_semantics.slice_class|startswith("interpreter_")) and .compiled_semantics.vm_step != null and (.compiled_semantics.vm_step.dispatch_targets|length)>=8 and (.compiled_semantics.vm_step.loop_latches|length)>=7 and (.compiled_semantics.vm_step.step_blocks|length)>=10 and (.compiled_semantics.vm_step.case_values_by_target | keys | length) >= 8 and (.compiled_semantics.vm_step.handler_state_updates | to_entries | length) >= 5' EOF_CMDS RUN @@ -305,7 +305,7 @@ true EOF_EXPECT CMDS=<=8 and (.symbolic.vm_transfer.loop_latches|length)>=7 and (.symbolic.vm_transfer.step_blocks|length)>=10 and (.symbolic.vm_transfer.handler_state_updates | to_entries | length) >= 5 and (.symbolic.vm_transfer.transfers | type=="array") and (((.symbolic.vm_transfer.handler_exit_guards // {}) | keys | length) >= 1) and (((.symbolic.vm_transfer.handler_memory_read_effects // {}) | keys | length) >= 1) and (((.symbolic.vm_transfer.handler_memory_write_effects // {}) | keys | length) >= 1) and (((.symbolic.vm_transfer.transfers // []) | map((.exit_guards // []) | length) | add) > 0) and (((.symbolic.vm_transfer.transfers // []) | map((.memory_reads // []) | length) | add) > 0) and (((.symbolic.vm_transfer.transfers // []) | map((.memory_writes // []) | length) | add) > 0) and (((.symbolic.vm_transfer.transfers // []) | map(((.memory_reads // []) + (.memory_writes // [])) | map(select(.binding != null)) | length) | add) > 0) and (.symbolic.vm_transfer.transfers[0] | has("residual_guards") and has("residual_memory_effects"))' +a:sla.types sym.tiny_vm_dispatch | jq -c '.compiled_semantics.execution=="vm" and .compiled_semantics.vm_transfer != null and (.compiled_semantics.vm_transfer.dispatch_targets|length)>=8 and (.compiled_semantics.vm_transfer.loop_latches|length)>=7 and (.compiled_semantics.vm_transfer.step_blocks|length)>=10 and (.compiled_semantics.vm_transfer.handler_state_updates | to_entries | length) >= 5 and (.compiled_semantics.vm_transfer.transfers | type=="array") and (((.compiled_semantics.vm_transfer.handler_exit_guards // {}) | keys | length) >= 1) and (((.compiled_semantics.vm_transfer.handler_memory_read_effects // {}) | keys | length) >= 1) and (((.compiled_semantics.vm_transfer.handler_memory_write_effects // {}) | keys | length) >= 1) and (((.compiled_semantics.vm_transfer.transfers // []) | map((.exit_guards // []) | length) | add) > 0) and (((.compiled_semantics.vm_transfer.transfers // []) | map((.memory_reads // []) | length) | add) > 0) and (((.compiled_semantics.vm_transfer.transfers // []) | map((.memory_writes // []) | length) | add) > 0) and (((.compiled_semantics.vm_transfer.transfers // []) | map(((.memory_reads // []) + (.memory_writes // [])) | map(select(.binding != null)) | length) | add) > 0) and (.compiled_semantics.vm_transfer.transfers[0] | has("residual_guards") and has("residual_memory_effects"))' EOF_CMDS RUN @@ -318,7 +318,7 @@ EOF_EXPECT CMDS=</dev/null -a:sla.types sym.tiny_vm_dispatch | jq -c '.symbolic.diagnostics.semantic_mode=="VmSummary" and .symbolic.diagnostics.cache_hit==true and .symbolic.vm_transfer != null' +a:sla.types sym.tiny_vm_dispatch | jq -c '.compiled_semantics.execution=="vm" and .compiled_semantics.cache_hit==true and .compiled_semantics.vm_transfer != null' EOF_CMDS RUN @@ -330,11 +330,11 @@ true EOF_EXPECT CMDS=<=8 and (.semantic.vm_transfer.loop_latches|length)>=7 and (.semantic.vm_transfer.step_blocks|length)>=10 and (.semantic.vm_transfer.transfers | type=="array") and (((.semantic.vm_transfer.handler_exit_guards // {}) | keys | length) >= 1) and (((.semantic.vm_transfer.handler_memory_read_effects // {}) | keys | length) >= 1) and (((.semantic.vm_transfer.handler_memory_write_effects // {}) | keys | length) >= 1) and (((.semantic.vm_transfer.transfers // []) | map((.exit_guards // []) | length) | add) > 0) and (((.semantic.vm_transfer.transfers // []) | map((.memory_reads // []) | length) | add) > 0) and (((.semantic.vm_transfer.transfers // []) | map((.memory_writes // []) | length) | add) > 0) and (((.semantic.vm_transfer.transfers // []) | map(((.memory_reads // []) + (.memory_writes // [])) | map(select(.binding != null)) | length) | add) > 0) and (.semantic.vm_transfer.transfers[0] | has("residual_guards") and has("residual_memory_effects"))' +a:sla.sym sym.tiny_vm_dispatch | jq -c '.semantic.execution=="vm" and .semantic.vm_transfer != null and (.semantic.vm_transfer.dispatch_targets|length)>=8 and (.semantic.vm_transfer.loop_latches|length)>=7 and (.semantic.vm_transfer.step_blocks|length)>=10 and (.semantic.vm_transfer.transfers | type=="array") and (((.semantic.vm_transfer.handler_exit_guards // {}) | keys | length) >= 1) and (((.semantic.vm_transfer.handler_memory_read_effects // {}) | keys | length) >= 1) and (((.semantic.vm_transfer.handler_memory_write_effects // {}) | keys | length) >= 1) and (((.semantic.vm_transfer.transfers // []) | map((.exit_guards // []) | length) | add) > 0) and (((.semantic.vm_transfer.transfers // []) | map((.memory_reads // []) | length) | add) > 0) and (((.semantic.vm_transfer.transfers // []) | map((.memory_writes // []) | length) | add) > 0) and (((.semantic.vm_transfer.transfers // []) | map(((.memory_reads // []) + (.memory_writes // [])) | map(select(.binding != null)) | length) | add) > 0) and (.semantic.vm_transfer.transfers[0] | has("residual_guards") and has("residual_memory_effects"))' EOF_CMDS RUN -NAME=types_stack_symbolic_eq_guard_reports_stack_memory_term +NAME=types_stack_symbolic_eq_guard_reports_canonical_stack_memory_term FILE=bins/vuln_test ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true EXPECT=<=1 and ((.semantics.body.Native.regions|length)>=1) and (((.semantics.body.Native.regions[0].control // []) | map(select(.value.compiled.precision=="Exact" and ((.value.compiled.memory_terms|length)>=1) and .value.compiled.memory_terms[0].region.Region.kind=="Stack" and .value.compiled.memory_terms[0].exact_offset==true)) | length) >= 1)' EOF_CMDS RUN -NAME=types_stack_symbolic_eq_guard_reports_memory_island +NAME=types_stack_symbolic_eq_guard_reports_canonical_memory_region FILE=bins/vuln_test ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true EXPECT=<= 1 and .symbolic.memory_islands[0].anchor_block != null and ((.symbolic.memory_islands[0].terms|length) >= 1) and .symbolic.memory_islands[0].terms[0].region.Region.kind=="Stack"' +a:sla.types sym.test_stack_symbolic_eq_guard | jq -c '.compiled_semantics.memory_region_count >= 1 and ((.semantics.body.Native.regions|length) >= 1) and .semantics.body.Native.regions[0].anchor != null and ((.semantics.body.Native.regions[0].memory|length) >= 1) and .semantics.body.Native.regions[0].memory[0].value.term.region.Region.kind=="Stack"' EOF_CMDS RUN -NAME=sym_stack_symbolic_eq_guard_reports_memory_island_count +NAME=sym_stack_symbolic_eq_guard_reports_memory_region_count FILE=bins/vuln_test ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true EXPECT=<= 1' +a:sla.sym sym.test_stack_symbolic_eq_guard | jq -c '.semantic.memory_region_count >= 1' EOF_CMDS RUN @@ -1055,7 +1055,7 @@ EOF_EXPECT CMDS=</dev/null -a:sla.types | jq -c '.symbolic.diagnostics.branches_pruned == 1 and ((.symbolic.branches | length) == 1) and .symbolic.branches[0].false_status == "unreachable"' +a:sla.types | jq -c '.compiled_semantics.branches_pruned == 1 and (.compiled_semantics.region_count == 1) and (((.semantics.body.Native.regions[0].targets // []) | map(.value.status) | index("Reachable")) != null) and (((.semantics.body.Native.regions[0].targets // []) | map(.value.status) | index("Unreachable")) != null)' EOF_CMDS RUN @@ -1068,11 +1068,11 @@ EOF_EXPECT CMDS=</dev/null -a:sla.types | jq -c '.symbolic.diagnostics.branches_pruned == 1 and ((.symbolic.branches | length) == 1) and .symbolic.branches[0].true_status == "reachable" and .symbolic.branches[0].false_status == "unreachable" and ((((.interproc.scope.payloads // []) | map(.function_name) | any(. == "dbg.helper_symbolic_zero_pure")) or (((.interproc.scope.seeds // []) | map(.name) | any(. == "sym.helper_symbolic_zero_pure")))))' +a:sla.types | jq -c '.compiled_semantics.branches_pruned == 1 and (.compiled_semantics.region_count == 1) and (((.semantics.body.Native.regions[0].targets // []) | map(.value.status) | index("Reachable")) != null) and (((.semantics.body.Native.regions[0].targets // []) | map(.value.status) | index("Unreachable")) != null) and ((((.interproc.scope.payloads // []) | map(.function_name) | any(. == "dbg.helper_symbolic_zero_pure")) or (((.interproc.scope.seeds // []) | map(.name) | any(. == "sym.helper_symbolic_zero_pure")))))' EOF_CMDS RUN @@ -1095,7 +1095,7 @@ CMDS=</dev/null -a:sla.types sym.test_symbolic_helper_pure_guard | jq -c '.symbolic.diagnostics.semantic_mode == "Compiled" and (.symbolic.diagnostics.derived_summaries >= 1) and (.symbolic.diagnostics.helper_functions >= 1)' +a:sla.types sym.test_symbolic_helper_pure_guard | jq -c '.compiled_semantics.stage == "compiled" and .compiled_semantics.execution == "native" and (.compiled_semantics.derived_summaries >= 1) and (.compiled_semantics.helper_functions >= 1)' EOF_CMDS RUN @@ -1121,7 +1121,7 @@ EOF_EXPECT CMDS=</dev/null -a:sla.sym sym.test_symbolic_helper_pure_guard | jq -c '.semantic.mode == "compiled" and (.semantic.derived_summaries >= 1) and (.semantic.helper_functions >= 1)' +a:sla.sym sym.test_symbolic_helper_pure_guard | jq -c '.semantic.stage == "compiled" and .semantic.execution == "native" and (.semantic.derived_summaries >= 1) and (.semantic.helper_functions >= 1)' EOF_CMDS RUN @@ -1134,7 +1134,7 @@ EOF_EXPECT CMDS=</dev/null -a:sla.sym sym.test_symbolic_xor_guard | jq -c '.semantic.branch_fact_count == 1 and (.semantic.control_island_count >= 1) and (.semantic.compiled_condition_count >= 1) and (.semantic.actionable_compiled_condition_count >= 1)' +a:sla.sym sym.test_symbolic_xor_guard | jq -c '.semantic.region_count == 1 and (.semantic.control_region_count >= 1) and (.semantic.compiled_condition_count >= 1) and (.semantic.actionable_compiled_condition_count >= 1)' EOF_CMDS RUN From 4ec90ecf7baf18a34d6f7883afe9c7e1388b5145 Mon Sep 17 00:00:00 2001 From: Priyanshu Kumar Date: Tue, 7 Apr 2026 06:39:44 +0000 Subject: [PATCH 08/10] Refactor VM summary handling in r2dec --- crates/r2dec/src/consumer_structured.rs | 14 +- crates/r2dec/src/consumer_vm.rs | 250 ++++++++++++- crates/r2dec/src/lib.rs | 67 +++- crates/r2dec/src/planner.rs | 58 ++- crates/r2sym/src/query.rs | 5 +- crates/r2sym/src/semantics/artifact.rs | 21 +- crates/r2sym/src/semantics/compiler.rs | 14 +- crates/r2sym/src/semantics/plan.rs | 163 ++++++-- crates/r2types/src/writeback.rs | 166 +++++++-- r2plugin/r_anal_sleigh.c | 116 ++++++ r2plugin/src/decompiler.rs | 18 + r2plugin/src/lib.rs | 348 ++++++++++++++++++ .../db/extras/r2sleigh_integration_extended | 32 +- 13 files changed, 1152 insertions(+), 120 deletions(-) diff --git a/crates/r2dec/src/consumer_structured.rs b/crates/r2dec/src/consumer_structured.rs index e2b23e5..858f750 100644 --- a/crates/r2dec/src/consumer_structured.rs +++ b/crates/r2dec/src/consumer_structured.rs @@ -34,13 +34,13 @@ where use_conservative_locals: true, is_linear_fallback: true, }, - crate::SemanticRoutePlan::FallbackComment { .. } | crate::SemanticRoutePlan::Standard => { - RoutedBody { - body_stmt: structurer.structure(), - use_conservative_locals: false, - is_linear_fallback: false, - } - } + crate::SemanticRoutePlan::VmSummary { .. } + | crate::SemanticRoutePlan::FallbackComment { .. } + | crate::SemanticRoutePlan::Standard => RoutedBody { + body_stmt: structurer.structure(), + use_conservative_locals: false, + is_linear_fallback: false, + }, } } diff --git a/crates/r2dec/src/consumer_vm.rs b/crates/r2dec/src/consumer_vm.rs index 9ed4fc2..c303584 100644 --- a/crates/r2dec/src/consumer_vm.rs +++ b/crates/r2dec/src/consumer_vm.rs @@ -1,12 +1,8 @@ -pub(crate) fn render_vm_semantic_fallback_comment( - func_name: &str, - semantic_artifact: &r2sym::SemanticArtifact, -) -> Option { - let vm_body = semantic_artifact.vm_body()?; - let vm_step = vm_body - .step_summary - .as_ref() - .or(vm_body.transfer_summary.as_ref())?; +use std::fmt::Write as _; + +use r2types::FunctionTypeFacts; + +fn vm_summary_stats_comment(func_name: &str, vm_step: &r2sym::VmStepSummary) -> String { let kind = crate::format_vm_summary_kind(vm_step.kind); let selector = vm_step.selector.as_deref().unwrap_or("unknown"); let inputs = if vm_step.state_inputs.is_empty() { @@ -109,9 +105,10 @@ pub(crate) fn render_vm_semantic_fallback_comment( }) .collect::>() .join("; "); - Some(format!( - "/* r2dec semantic summary: vm_summary for {} ({kind} @ 0x{:x}, loop_header=0x{:x}, selector={}, targets={}, redispatch={}, exact_transfers={}, likely_transfers={}, heuristic_transfers={}, redispatch_transfers={}, returning_transfers={}, selector_updates={}, exact_exit_guards={}, residual_guards={}, residual_memory={}, total_reads={}, total_writes={}, read_effects={}, write_effects={}, state_inputs=[{}], state_outputs=[{}], handlers={}) */", + format!( + "r2dec semantic summary: vm_summary for {} ({} @ 0x{:x}, loop_header=0x{:x}, selector={}, targets={}, redispatch={}, exact_transfers={}, likely_transfers={}, heuristic_transfers={}, redispatch_transfers={}, returning_transfers={}, selector_updates={}, exact_exit_guards={}, residual_guards={}, residual_memory={}, total_reads={}, total_writes={}, read_effects={}, write_effects={}, state_inputs=[{}], state_outputs=[{}], handlers={})", func_name, + kind, vm_step.dispatch_header, vm_step.loop_header, selector, @@ -133,5 +130,236 @@ pub(crate) fn render_vm_semantic_fallback_comment( inputs, outputs, handler_preview, + ) +} + +fn render_vm_signature(type_facts: &FunctionTypeFacts, func_name: &str) -> String { + let Some(signature) = type_facts.merged_signature.as_ref() else { + return format!("void {func_name}(void)"); + }; + let ret_ty = signature + .ret_type + .as_ref() + .map(crate::type_like_to_ctype) + .unwrap_or(crate::CType::Void); + let params = if signature.params.is_empty() { + "void".to_string() + } else { + signature + .params + .iter() + .enumerate() + .map(|(idx, param)| { + let ty = param + .ty + .as_ref() + .map(crate::type_like_to_ctype) + .unwrap_or(crate::CType::Unknown); + let name = if param.name.trim().is_empty() { + format!("arg{}", idx + 1) + } else { + param.name.clone() + }; + format!("{ty} {name}") + }) + .collect::>() + .join(", ") + }; + format!("{ret_ty} {func_name}({params})") +} + +fn render_vm_memory_condition(effect: &r2sym::VmMemoryCondition) -> String { + let binding = effect.binding.as_deref().unwrap_or("value"); + let range = if effect.offset_lo == effect.offset_hi { + format!("{:+#x}", effect.offset_lo) + } else { + format!("{:+#x}..{:+#x}", effect.offset_lo, effect.offset_hi) + }; + let value = effect + .value_expr + .as_deref() + .or(Some(effect.expr.as_str())) + .unwrap_or(binding); + format!( + "{}[{:?} {}] size={} {}", + effect.region.name, effect.region.kind, range, effect.size, value + ) +} + +fn render_vm_case_block( + out: &mut String, + vm_step: &r2sym::VmStepSummary, + target: u64, + default_emitted: &mut bool, +) { + let case_values = vm_step + .case_values_by_target + .get(&target) + .cloned() + .unwrap_or_default(); + if case_values.is_empty() && vm_step.default_target == Some(target) { + let _ = writeln!(out, " default:"); + *default_emitted = true; + } else if case_values.is_empty() { + if !*default_emitted { + let _ = writeln!(out, " default:"); + *default_emitted = true; + } else { + let _ = writeln!( + out, + " /* unlabeled handler 0x{:x} omitted from switch surface */", + target + ); + return; + } + } else { + for value in case_values { + let _ = writeln!(out, " case 0x{value:x}:"); + } + } + + let region_blocks = vm_step + .handler_regions + .get(&target) + .map(|blocks| crate::format_vm_target_list(blocks)) + .unwrap_or_else(|| "[]".to_string()); + let _ = writeln!( + out, + " /* handler 0x{:x} blocks={} */", + target, region_blocks + ); + + let mut emitted_body = false; + for transfer in vm_step + .transfers + .iter() + .filter(|transfer| transfer.handler_target == target) + { + for update in transfer.state_updates.iter().take(4) { + let _ = writeln!(out, " {} = {};", update.output, update.expr); + emitted_body = true; + } + if let Some(selector_update) = transfer.selector_update.as_ref() { + let _ = writeln!( + out, + " {} = {};", + selector_update.output, selector_update.expr + ); + emitted_body = true; + } + for guard in transfer.exit_guards.iter().take(3) { + let _ = writeln!( + out, + " /* if ({}) -> 0x{:x} [{:?}] */", + guard.guard.expr, + guard.target, + guard.guard.confidence() + ); + emitted_body = true; + } + for effect in transfer.memory_reads.iter().take(2) { + let _ = writeln!( + out, + " /* read {} [{:?}] */", + render_vm_memory_condition(effect), + effect.confidence() + ); + emitted_body = true; + } + for effect in transfer.memory_writes.iter().take(2) { + let _ = writeln!( + out, + " /* write {} [{:?}] */", + render_vm_memory_condition(effect), + effect.confidence() + ); + emitted_body = true; + } + if transfer.redispatch { + let _ = writeln!(out, " /* redispatch */"); + emitted_body = true; + } + if transfer.may_return { + let _ = writeln!(out, " /* may return */"); + emitted_body = true; + } + if transfer.truncated { + let _ = writeln!(out, " /* truncated handler summary */"); + emitted_body = true; + } + } + + if !emitted_body { + let _ = writeln!(out, " /* no exact handler body recovered */"); + } + let _ = writeln!(out, " break;"); +} + +pub(crate) fn render_vm_semantic_summary( + func_name: &str, + type_facts: &FunctionTypeFacts, + semantic_artifact: &r2sym::SemanticArtifact, +) -> Option { + let vm_body = semantic_artifact.vm_body()?; + let vm_step = vm_body + .step_summary + .as_ref() + .or(vm_body.transfer_summary.as_ref())?; + let selector = vm_step.selector.as_deref().unwrap_or("dispatch_selector"); + let mut out = String::new(); + + let _ = writeln!(out, "{} {{", render_vm_signature(type_facts, func_name)); + let _ = writeln!( + out, + " /* {} */", + vm_summary_stats_comment(func_name, vm_step) + ); + if !vm_step.state_inputs.is_empty() || !vm_step.state_outputs.is_empty() { + let _ = writeln!( + out, + " /* state_inputs=[{}] state_outputs=[{}] */", + if vm_step.state_inputs.is_empty() { + "none".to_string() + } else { + vm_step.state_inputs.join(", ") + }, + if vm_step.state_outputs.is_empty() { + "none".to_string() + } else { + vm_step.state_outputs.join(", ") + }, + ); + } + let _ = writeln!(out, " switch ({selector}) {{"); + let mut default_emitted = false; + for target in &vm_step.dispatch_targets { + render_vm_case_block(&mut out, vm_step, *target, &mut default_emitted); + } + if !default_emitted { + let _ = writeln!(out, " default:"); + if let Some(default_target) = vm_step.default_target { + let _ = writeln!(out, " /* default_target=0x{:x} */", default_target); + } else { + let _ = writeln!(out, " /* no default target recovered */"); + } + let _ = writeln!(out, " break;"); + } + let _ = writeln!(out, " }}"); + let _ = writeln!(out, "}}"); + Some(out) +} + +pub(crate) fn render_vm_semantic_fallback_comment( + func_name: &str, + semantic_artifact: &r2sym::SemanticArtifact, +) -> Option { + let vm_body = semantic_artifact.vm_body()?; + let vm_step = vm_body + .step_summary + .as_ref() + .or(vm_body.transfer_summary.as_ref())?; + Some(format!( + "/* {} */", + vm_summary_stats_comment(func_name, vm_step) )) } diff --git a/crates/r2dec/src/lib.rs b/crates/r2dec/src/lib.rs index c616637..ffe05f3 100644 --- a/crates/r2dec/src/lib.rs +++ b/crates/r2dec/src/lib.rs @@ -433,6 +433,17 @@ pub fn render_semantic_worker_linearization( consumer_linear::render_semantic_worker_linearization(plan, semantic_artifact, reason) } +pub fn render_vm_semantic_summary( + func_name: &str, + function_facts: &FunctionFacts, +) -> Option { + consumer_vm::render_vm_semantic_summary( + func_name, + &function_facts.types, + function_facts.semantics.as_ref()?, + ) +} + fn merge_params_with_external_signature( recovered_params: Vec, signature: Option<&FunctionSignatureSpec>, @@ -992,8 +1003,37 @@ impl Decompiler { self.context = self.context.clone().with_function_facts(function_facts); } + fn vm_summary_output_for_route( + &self, + func_name: &str, + route: &planner::SemanticRoutePlan, + ) -> Option { + match route { + planner::SemanticRoutePlan::VmSummary { .. } => { + crate::consumer_vm::render_vm_semantic_summary( + func_name, + self.context.type_facts(), + self.context.semantic_artifact()?, + ) + } + _ => None, + } + } + /// Decompile an SSA function to C code. pub fn decompile(&self, func: &SSAFunction) -> String { + let func_name = func + .name + .clone() + .unwrap_or_else(|| format!("sub_{:x}", func.entry)); + let semantic_route = planner::semantic_route_plan( + &func_name, + self.context.semantic_artifact(), + &func.cfg_risk_summary(), + ); + if let Some(output) = self.vm_summary_output_for_route(&func_name, &semantic_route) { + return output; + } // Build the C function let c_func = self.build_function(func); @@ -1004,6 +1044,19 @@ impl Decompiler { /// Decompile a prepared function with an explicit typed context payload. pub fn decompile_input(&self, input: &DecompilerInput) -> String { + let func = input.prepared_ssa.function(); + let func_name = func + .name + .clone() + .unwrap_or_else(|| format!("sub_{:x}", func.entry)); + let semantic_route = planner::semantic_route_plan( + &func_name, + self.context.semantic_artifact(), + &func.cfg_risk_summary(), + ); + if let Some(output) = self.vm_summary_output_for_route(&func_name, &semantic_route) { + return output; + } let c_func = self.build_function_from_input(input); let mut codegen = CodeGenerator::new(self.config.codegen.clone()); codegen.generate_function(&c_func) @@ -4964,16 +5017,16 @@ mod tests { let output = decompiler.decompile(&func); assert!( output.contains("r2dec semantic summary: vm_summary"), - "expected VM semantic summary comment, got:\n{output}" + "expected VM semantic summary output, got:\n{output}" ); assert!( - output.contains("dispatch_header=0x1000") + output.contains("switch (vm.sel)") + && output.contains("case 0x1:") && output.contains("handler 0x1004") - && output.contains("guards=") - && output.contains("residual_memory=") - && output.contains("read_effects=") - && output.contains("write_effects="), - "expected richer VM details in summary comment, got:\n{output}" + && output.contains("state = state + 1;") + && output.contains("read ram:0x2000") + && output.contains("vm.sel = 3;"), + "expected structured VM summary rendering, got:\n{output}" ); } diff --git a/crates/r2dec/src/planner.rs b/crates/r2dec/src/planner.rs index 697eda1..162e6f2 100644 --- a/crates/r2dec/src/planner.rs +++ b/crates/r2dec/src/planner.rs @@ -7,6 +7,7 @@ pub enum SemanticRoutePlan { Standard, StructuredWorker { reason: String }, LinearWorker { reason: String }, + VmSummary { reason: String }, FallbackComment { comment: String }, } @@ -16,10 +17,10 @@ pub(crate) fn prefer_symbolic_large_worker_decompile( let Some(semantic_artifact) = semantic_artifact else { return false; }; - matches!( - semantic_artifact.decompile_plan(), - r2sym::DecompilePlan::Ready - ) && semantic_artifact.diagnostics.skipped_large_cfg + semantic_artifact + .decompile_plan() + .allows_native_linearization() + && semantic_artifact.diagnostics.skipped_large_cfg && matches!( semantic_artifact.stage, r2sym::RefinementStage::Residual | r2sym::RefinementStage::Compiled @@ -62,19 +63,13 @@ pub(crate) fn preferred_semantic_fallback_comment( semantic_artifact: Option<&r2sym::SemanticArtifact>, ) -> Option { let semantic_artifact = semantic_artifact?; - if matches!(semantic_artifact.execution, r2sym::ExecutionModel::Vm) { - return crate::consumer_fallback::semantic_fallback_comment( - func_name, - Some(semantic_artifact), - ); - } if !crate::is_autogenerated_function_name(func_name) { return None; } - if matches!( - semantic_artifact.decompile_plan(), - r2sym::DecompilePlan::Ready - ) { + if semantic_artifact + .decompile_plan() + .allows_native_linearization() + { return None; } if semantic_artifact.diagnostics.skipped_large_cfg { @@ -93,6 +88,15 @@ pub(crate) fn preferred_semantic_fallback_comment( .flatten() } +pub(crate) fn preferred_vm_summary_reason( + semantic_artifact: Option<&r2sym::SemanticArtifact>, +) -> Option { + match semantic_artifact?.decompile_plan() { + r2sym::DecompilePlan::VmSummaryOnly { reason } => Some(reason), + _ => None, + } +} + pub(crate) fn preferred_semantic_linearization_reason( func_name: &str, semantic_artifact: Option<&r2sym::SemanticArtifact>, @@ -104,20 +108,11 @@ pub(crate) fn preferred_semantic_linearization_reason( } if !matches!( semantic_artifact.decompile_plan(), - r2sym::DecompilePlan::Ready + r2sym::DecompilePlan::NativeLinear { .. } ) || !semantic_artifact.diagnostics.skipped_large_cfg { return None; } - if semantic_artifact.supports_guarded_structuring() { - return None; - } - if semantic_artifact - .native_body() - .is_none_or(|body| body.regions.is_empty()) - { - return None; - } Some(preferred_semantic_worker_reason(cfg_summary)) } @@ -130,15 +125,13 @@ pub(crate) fn preferred_semantic_structuring_reason( if !crate::is_autogenerated_function_name(func_name) { return None; } - if !matches!( - semantic_artifact.decompile_plan(), - r2sym::DecompilePlan::Ready - ) { + if !semantic_artifact + .decompile_plan() + .allows_native_structuring() + { return None; } - if !semantic_artifact.supports_guarded_structuring() - || !semantic_artifact.diagnostics.skipped_large_cfg - { + if !semantic_artifact.diagnostics.skipped_large_cfg { return None; } Some(preferred_semantic_worker_reason(cfg_summary)) @@ -191,6 +184,9 @@ pub fn semantic_route_plan( semantic_artifact: Option<&r2sym::SemanticArtifact>, cfg_summary: &CFGRiskSummary, ) -> SemanticRoutePlan { + if let Some(reason) = preferred_vm_summary_reason(semantic_artifact) { + return SemanticRoutePlan::VmSummary { reason }; + } if let Some(comment) = preferred_semantic_fallback_comment(func_name, semantic_artifact) { return SemanticRoutePlan::FallbackComment { comment }; } diff --git a/crates/r2sym/src/query.rs b/crates/r2sym/src/query.rs index ed0a01d..86824d8 100644 --- a/crates/r2sym/src/query.rs +++ b/crates/r2sym/src/query.rs @@ -1981,7 +1981,10 @@ mod tests { } => { assert!(initial_state.num_constraints() >= original_constraints); assert!(narrowed_state.is_none()); - assert!(compiled_precondition.is_some()); + assert!( + compiled_precondition.is_none(), + "exact artifact-guided branch narrowing should not surface fallback compiled metadata" + ); } PreconditionApplication::ExactUnsat { .. } => { panic!("necessary worker islands should use the real compiled precondition path") diff --git a/crates/r2sym/src/semantics/artifact.rs b/crates/r2sym/src/semantics/artifact.rs index de3b2e0..11c447b 100644 --- a/crates/r2sym/src/semantics/artifact.rs +++ b/crates/r2sym/src/semantics/artifact.rs @@ -399,6 +399,11 @@ impl SemanticArtifact { .is_some_and(|body| !body.regions.is_empty()) } + fn supports_native_semantic_structuring(&self) -> bool { + self.native_body() + .is_some_and(NativeArtifactBody::supports_guarded_structuring) + } + fn has_query_support(&self) -> bool { self.native_body() .is_some_and(NativeArtifactBody::supports_query_guidance) @@ -487,6 +492,7 @@ impl SemanticArtifact { self.execution, &self.diagnostics, self.has_native_semantics(), + self.supports_native_semantic_structuring(), ) } @@ -649,8 +655,19 @@ impl SemanticArtifact { } pub fn supports_guarded_structuring(&self) -> bool { - self.native_body() - .is_some_and(NativeArtifactBody::supports_guarded_structuring) + self.decompile_plan().allows_native_structuring() + } + + pub fn supports_native_semantic_linearization(&self) -> bool { + self.decompile_plan().allows_native_linearization() + } + + pub fn vm_summary_only_type_plan(&self) -> bool { + self.type_plan().is_vm_summary_only() + } + + pub fn vm_summary_only_decompile_plan(&self) -> bool { + self.decompile_plan().is_vm_summary_only() } } diff --git a/crates/r2sym/src/semantics/compiler.rs b/crates/r2sym/src/semantics/compiler.rs index 6b68272..43e88ff 100644 --- a/crates/r2sym/src/semantics/compiler.rs +++ b/crates/r2sym/src/semantics/compiler.rs @@ -1233,10 +1233,13 @@ mod tests { assert!(artifact.diagnostics.skipped_large_cfg); assert_eq!(native.regions.len(), 1); assert_eq!(native.actionable_control_count(), 2); - assert!(matches!(artifact.type_plan(), crate::TypePlan::Ready)); + assert!(matches!( + artifact.type_plan(), + crate::TypePlan::NativeAugmentation + )); assert!(matches!( artifact.decompile_plan(), - crate::DecompilePlan::Ready + crate::DecompilePlan::NativeStructured | crate::DecompilePlan::NativeLinear { .. } )); assert_eq!(artifact.diagnostics.branches_evaluated, 1); } @@ -1352,10 +1355,13 @@ mod tests { vm_transfer: None, }); assert!(matches!(artifact.query_plan(), crate::QueryPlan::Ready)); - assert!(matches!(artifact.type_plan(), crate::TypePlan::Ready)); + assert!(matches!( + artifact.type_plan(), + crate::TypePlan::NativeAugmentation + )); assert!(matches!( artifact.decompile_plan(), - crate::DecompilePlan::Ready + crate::DecompilePlan::NativeStructured )); let reasons = artifact.diagnostics.residual_reasons; assert!( diff --git a/crates/r2sym/src/semantics/plan.rs b/crates/r2sym/src/semantics/plan.rs index 14f5441..d05c703 100644 --- a/crates/r2sym/src/semantics/plan.rs +++ b/crates/r2sym/src/semantics/plan.rs @@ -21,7 +21,8 @@ pub enum QueryPlan { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum TypePlan { - Ready, + NativeAugmentation, + VmSummaryOnly { reason: String }, Fallback { reason: String }, Residual { reasons: Vec }, Refuse { reason: String }, @@ -29,7 +30,9 @@ pub enum TypePlan { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum DecompilePlan { - Ready, + NativeStructured, + NativeLinear { reason: String }, + VmSummaryOnly { reason: String }, Fallback { reason: String }, Residual { reasons: Vec }, Refuse { reason: String }, @@ -55,7 +58,11 @@ pub struct TargetQueryRoutePlan { #[serde(default, skip_serializing_if = "Option::is_none")] pub branch_guidance: Option, pub allow_memory_term_narrowing: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub dynamic_target_compile_reason: Option, pub allow_dynamic_target_compile: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub vm_target_compile_reason: Option, pub allow_vm_target_compile: bool, } @@ -67,12 +74,38 @@ impl TargetQueryRoutePlan { }, branch_guidance: None, allow_memory_term_narrowing: false, + dynamic_target_compile_reason: Some("semantic artifact unavailable".to_string()), allow_dynamic_target_compile: true, + vm_target_compile_reason: Some("semantic artifact unavailable".to_string()), allow_vm_target_compile: true, } } } +impl TypePlan { + pub fn allows_native_augmentation(&self) -> bool { + matches!(self, Self::NativeAugmentation) + } + + pub fn is_vm_summary_only(&self) -> bool { + matches!(self, Self::VmSummaryOnly { .. }) + } +} + +impl DecompilePlan { + pub fn allows_native_linearization(&self) -> bool { + matches!(self, Self::NativeStructured | Self::NativeLinear { .. }) + } + + pub fn allows_native_structuring(&self) -> bool { + matches!(self, Self::NativeStructured) + } + + pub fn is_vm_summary_only(&self) -> bool { + matches!(self, Self::VmSummaryOnly { .. }) + } +} + fn residual_reason_strings(diagnostics: &SemanticArtifactDiagnostics) -> Vec { diagnostics .residual_reasons @@ -131,11 +164,12 @@ pub fn derive_type_plan( diagnostics: &SemanticArtifactDiagnostics, has_native_semantics: bool, ) -> TypePlan { - if matches!(execution, ExecutionModel::Vm) - || !diagnostics.skipped_large_cfg - || has_native_semantics - { - TypePlan::Ready + if matches!(execution, ExecutionModel::Vm) { + TypePlan::VmSummaryOnly { + reason: "vm artifacts expose summary-only type hints".to_string(), + } + } else if !diagnostics.skipped_large_cfg || has_native_semantics { + TypePlan::NativeAugmentation } else if matches!(stage, RefinementStage::Residual) { TypePlan::Residual { reasons: residual_reason_strings(diagnostics), @@ -152,21 +186,27 @@ pub fn derive_decompile_plan( execution: ExecutionModel, diagnostics: &SemanticArtifactDiagnostics, has_native_semantics: bool, + supports_guarded_structuring: bool, ) -> DecompilePlan { - if matches!(execution, ExecutionModel::Native) - && (!diagnostics.skipped_large_cfg || has_native_semantics) + if matches!(execution, ExecutionModel::Vm) { + DecompilePlan::VmSummaryOnly { + reason: "vm artifacts currently support summary rendering only".to_string(), + } + } else if matches!(execution, ExecutionModel::Native) + && has_native_semantics + && supports_guarded_structuring { - DecompilePlan::Ready + DecompilePlan::NativeStructured + } else if matches!(execution, ExecutionModel::Native) && has_native_semantics { + DecompilePlan::NativeLinear { + reason: "guarded structuring unavailable".to_string(), + } } else if matches!(stage, RefinementStage::Residual) && !has_large_cfg_only_residual(diagnostics) { DecompilePlan::Residual { reasons: residual_reason_strings(diagnostics), } - } else if matches!(execution, ExecutionModel::Vm) { - DecompilePlan::Fallback { - reason: "vm consumer required".to_string(), - } } else { DecompilePlan::Fallback { reason: "decompile capability unavailable".to_string(), @@ -218,35 +258,62 @@ pub fn derive_target_query_route_plan( target_plan: target_plan.clone(), branch_guidance: Some(*mode), allow_memory_term_narrowing: has_authoritative_source && has_memory_guidance, - allow_dynamic_target_compile: true, - allow_vm_target_compile: true, + dynamic_target_compile_reason: None, + allow_dynamic_target_compile: false, + vm_target_compile_reason: None, + allow_vm_target_compile: false, }, TargetQueryPlan::Fallback { .. } => TargetQueryRoutePlan { target_plan: target_plan.clone(), branch_guidance: None, allow_memory_term_narrowing: has_authoritative_source && has_memory_guidance, - allow_dynamic_target_compile: true, - allow_vm_target_compile: true, + dynamic_target_compile_reason: None, + allow_dynamic_target_compile: false, + vm_target_compile_reason: None, + allow_vm_target_compile: false, }, TargetQueryPlan::Residual { .. } | TargetQueryPlan::Refuse { .. } => { TargetQueryRoutePlan { target_plan: target_plan.clone(), branch_guidance: None, allow_memory_term_narrowing: false, + dynamic_target_compile_reason: None, allow_dynamic_target_compile: false, + vm_target_compile_reason: None, allow_vm_target_compile: false, } } }, - QueryPlan::Fallback { .. } | QueryPlan::Residual { .. } | QueryPlan::Refuse { .. } => { + QueryPlan::Fallback { reason } => TargetQueryRoutePlan { + target_plan: target_plan.clone(), + branch_guidance: None, + allow_memory_term_narrowing: false, + dynamic_target_compile_reason: Some(reason.clone()), + allow_dynamic_target_compile: true, + vm_target_compile_reason: Some(reason.clone()), + allow_vm_target_compile: true, + }, + QueryPlan::Residual { reasons } => { + let reason = reasons.join(", "); TargetQueryRoutePlan { target_plan: target_plan.clone(), branch_guidance: None, allow_memory_term_narrowing: false, - allow_dynamic_target_compile: false, - allow_vm_target_compile: false, + dynamic_target_compile_reason: Some(reason.clone()), + allow_dynamic_target_compile: true, + vm_target_compile_reason: Some(reason), + allow_vm_target_compile: true, } } + QueryPlan::Refuse { .. } => TargetQueryRoutePlan { + target_plan: target_plan.clone(), + branch_guidance: None, + allow_memory_term_narrowing: false, + dynamic_target_compile_reason: None, + allow_dynamic_target_compile: false, + vm_target_compile_reason: None, + allow_vm_target_compile: false, + }, } } @@ -255,8 +322,9 @@ mod tests { use proptest::prelude::*; use super::{ - DecompilePlan, QueryPlan, TargetQueryPlan, TargetQueryRoutePlan, derive_decompile_plan, - derive_query_plan, derive_target_query_plan, derive_target_query_route_plan, + DecompilePlan, QueryPlan, TargetQueryPlan, TargetQueryRoutePlan, TypePlan, + derive_decompile_plan, derive_query_plan, derive_target_query_plan, + derive_target_query_route_plan, derive_type_plan, }; use crate::{ExecutionModel, RefinementStage, SemanticArtifactDiagnostics}; @@ -296,21 +364,48 @@ mod tests { prop_assert!(is_valid); } + #[test] + fn type_plan_derivation_is_total( + is_vm in any::(), + is_residual in any::(), + skipped_large_cfg in any::(), + has_native_semantics in any::(), + ) { + let plan = derive_type_plan( + if is_residual { RefinementStage::Residual } else { RefinementStage::Compiled }, + if is_vm { ExecutionModel::Vm } else { ExecutionModel::Native }, + &diagnostics(skipped_large_cfg), + has_native_semantics, + ); + let is_valid = match plan { + TypePlan::NativeAugmentation + | TypePlan::VmSummaryOnly { .. } + | TypePlan::Fallback { .. } + | TypePlan::Residual { .. } + | TypePlan::Refuse { .. } => true, + }; + prop_assert!(is_valid); + } + #[test] fn decompile_plan_derivation_is_total( is_vm in any::(), is_residual in any::(), skipped_large_cfg in any::(), has_native_semantics in any::(), + supports_guarded_structuring in any::(), ) { let plan = derive_decompile_plan( if is_residual { RefinementStage::Residual } else { RefinementStage::Compiled }, if is_vm { ExecutionModel::Vm } else { ExecutionModel::Native }, &diagnostics(skipped_large_cfg), has_native_semantics, + supports_guarded_structuring, ); let is_valid = match plan { - DecompilePlan::Ready + DecompilePlan::NativeStructured + | DecompilePlan::NativeLinear { .. } + | DecompilePlan::VmSummaryOnly { .. } | DecompilePlan::Fallback { .. } | DecompilePlan::Residual { .. } | DecompilePlan::Refuse { .. } => true, @@ -340,7 +435,7 @@ mod tests { } #[test] - fn target_query_route_plan_allows_dynamic_fallback_without_branch_guidance() { + fn target_query_route_plan_keeps_ready_paths_artifact_authoritative() { let target_plan = derive_target_query_plan(&QueryPlan::Ready, false, false, false); let route = derive_target_query_route_plan(&QueryPlan::Ready, &target_plan, false, false); assert!(matches!( @@ -349,7 +444,21 @@ mod tests { )); assert!(route.branch_guidance.is_none()); assert!(!route.allow_memory_term_narrowing); + assert!(!route.allow_dynamic_target_compile); + assert!(!route.allow_vm_target_compile); + } + + #[test] + fn target_query_route_plan_allows_dynamic_fallback_on_residual_routes() { + let query_plan = QueryPlan::Residual { + reasons: vec!["budget".to_string()], + }; + let target_plan = derive_target_query_plan(&query_plan, false, false, false); + let route = derive_target_query_route_plan(&query_plan, &target_plan, false, false); + assert!(route.branch_guidance.is_none()); + assert!(route.dynamic_target_compile_reason.is_some()); assert!(route.allow_dynamic_target_compile); + assert!(route.vm_target_compile_reason.is_some()); assert!(route.allow_vm_target_compile); } @@ -390,6 +499,10 @@ mod tests { has_authoritative_source && has_memory_guidance ); } + if ready { + prop_assert!(!route.allow_dynamic_target_compile); + prop_assert!(!route.allow_vm_target_compile); + } if matches!(target_plan, TargetQueryPlan::Residual { .. } | TargetQueryPlan::Refuse { .. }) || !ready { prop_assert!(route.branch_guidance.is_none()); } diff --git a/crates/r2types/src/writeback.rs b/crates/r2types/src/writeback.rs index 61a685d..5f31c8a 100644 --- a/crates/r2types/src/writeback.rs +++ b/crates/r2types/src/writeback.rs @@ -789,7 +789,7 @@ fn semantic_fallback_warning(artifact: &r2sym::SemanticArtifact) -> String { } pub fn semantic_artifact_prefers_bounded_type_plan(artifact: &r2sym::SemanticArtifact) -> bool { - if !matches!(artifact.type_plan(), r2sym::TypePlan::Ready) { + if !artifact.type_plan().allows_native_augmentation() { return true; } matches!( @@ -814,7 +814,7 @@ pub fn build_semantic_type_fallback_plan( artifact: &r2sym::SemanticArtifact, ) -> TypeWritebackPlan { let mut warnings = vec![semantic_fallback_warning(artifact)]; - if !matches!(artifact.type_plan(), r2sym::TypePlan::Ready) { + if !artifact.type_plan().allows_native_augmentation() { warnings.push("type analysis not ready from semantic capability".to_string()); } let mut local_structs = LocalStructArtifacts::default(); @@ -1413,11 +1413,73 @@ pub fn augment_local_struct_artifacts_with_semantics( artifact: &r2sym::SemanticArtifact, ptr_bits: u32, ) { + fn reliable_post_memory_terms( + region: &r2sym::SemanticRegion, + ) -> impl Iterator { + region + .post + .iter() + .filter(|predicate| predicate.evidence.is_reliable()) + .filter_map(|predicate| predicate.value.compiled.as_ref()) + .filter(|compiled| compiled.evidence().is_reliable()) + .flat_map(|compiled| compiled.memory_terms.iter()) + } + + fn has_reliable_preconditions(region: &r2sym::SemanticRegion) -> bool { + region + .pre + .iter() + .any(|predicate| predicate.evidence.is_reliable()) + } + + fn has_decisive_target_support(region: &r2sym::SemanticRegion) -> bool { + region.actionable_reachable_target().is_some() || { + let actionable_targets = region + .targets + .iter() + .filter(|fact| fact.evidence.allows_narrowing()) + .filter(|fact| { + matches!( + fact.value.status, + r2sym::SymbolicReachabilityStatus::Reachable + ) + }) + .map(|fact| fact.value.target) + .collect::>(); + actionable_targets.len() == 1 + } + } + + fn supports_conservative_type_projection(region: &r2sym::SemanticRegion) -> bool { + let decisive_target = has_decisive_target_support(region); + let has_post_support = region.post.iter().any(|predicate| { + predicate.evidence.allows_narrowing() + && predicate + .value + .compiled + .as_ref() + .is_some_and(|compiled| compiled.evidence().is_reliable()) + }); + if !decisive_target && !has_post_support { + return false; + } + if has_reliable_preconditions(region) && !decisive_target { + return false; + } + true + } + let mut projected_profiles = BTreeMap::>::new(); + if artifact.vm_summary_only_type_plan() { + return; + } let Some(native) = artifact.native_body() else { return; }; for region in native.regions.values() { + if !supports_conservative_type_projection(region) { + continue; + } for term in region .memory .iter() @@ -1434,29 +1496,48 @@ pub fn augment_local_struct_artifacts_with_semantics( .entry(offset) .or_insert(field_type); } + for term in reliable_post_memory_terms(region) { + let Some((slot, offset, field_type)) = backward_memory_term_slot_field(term, ptr_bits) + else { + continue; + }; + projected_profiles + .entry(slot) + .or_default() + .entry(offset) + .or_insert(field_type); + } } if projected_profiles.is_empty() { - for compiled in native - .regions - .values() - .flat_map(|region| region.control.iter()) - .filter(|fact| fact.evidence.allows_narrowing()) - .filter_map(|fact| fact.value.compiled.as_ref()) - { - if !compiled.evidence().is_reliable() { + for region in native.regions.values() { + if !supports_conservative_type_projection(region) { continue; } - for term in &compiled.memory_terms { - let Some((slot, offset, field_type)) = - backward_memory_term_slot_field(term, ptr_bits) - else { + let Some(target) = region.actionable_reachable_target() else { + continue; + }; + for compiled in region + .control + .iter() + .filter(|fact| fact.evidence.allows_narrowing()) + .filter(|fact| fact.value.target == target) + .filter_map(|fact| fact.value.compiled.as_ref()) + { + if !compiled.evidence().is_reliable() { continue; - }; - projected_profiles - .entry(slot) - .or_default() - .entry(offset) - .or_insert(field_type); + } + for term in &compiled.memory_terms { + let Some((slot, offset, field_type)) = + backward_memory_term_slot_field(term, ptr_bits) + else { + continue; + }; + projected_profiles + .entry(slot) + .or_default() + .entry(offset) + .or_insert(field_type); + } } } } @@ -5337,7 +5418,7 @@ mod tests { } #[test] - fn symbolic_memory_islands_seed_local_struct_profiles_without_control_islands() { + fn symbolic_memory_without_control_or_post_support_is_rejected() { let artifact = test_artifact( r2sym::RefinementStage::Compiled, r2sym::SliceClass::Worker, @@ -5362,6 +5443,42 @@ mod tests { augment_local_struct_artifacts_with_semantics(&mut local_structs, &artifact, 64); + assert!( + local_structs.slot_field_profiles.is_empty(), + "memory-only regions without control/post support must not project struct fields" + ); + } + + #[test] + fn symbolic_postconditions_seed_local_struct_profiles_without_control_islands() { + let artifact = test_artifact( + r2sym::RefinementStage::Compiled, + r2sym::SliceClass::Worker, + false, + Vec::new(), + vec![r2sym::SemanticRegion { + anchor: 0x401000, + frontier: BTreeSet::new(), + control: Vec::new(), + memory: Vec::new(), + pre: Vec::new(), + post: vec![r2sym::Judged::new( + r2sym::SemanticPredicate { + expr: "post(arg0->f_8)".to_string(), + compiled: Some(test_exact_compiled_condition( + "post(arg0->f_8)", + vec![test_arg_memory_term(8, 4)], + )), + }, + r2sym::SemanticEvidence::exact(), + )], + targets: Vec::new(), + }], + ); + let mut local_structs = LocalStructArtifacts::default(); + + augment_local_struct_artifacts_with_semantics(&mut local_structs, &artifact, 64); + assert_eq!( local_structs .slot_field_profiles @@ -5370,13 +5487,6 @@ mod tests { .map(String::as_str), Some("int32_t") ); - assert_eq!( - local_structs - .slot_type_overrides - .get(&0) - .map(String::as_str), - Some("struct sla_struct_symbolic_arg1 *") - ); } #[test] diff --git a/r2plugin/r_anal_sleigh.c b/r2plugin/r_anal_sleigh.c index bd22d0b..6c90d76 100644 --- a/r2plugin/r_anal_sleigh.c +++ b/r2plugin/r_anal_sleigh.c @@ -180,6 +180,14 @@ extern char *r2sleigh_infer_type_writeback_json_scope_ex(const R2ILContext *ctx, unsigned long long fcn_addr, const char *fcn_name, const char *external_context_json, size_t interproc_iter, size_t interproc_max_iters, int interproc_converged, const char *interproc_scope_json, const R2ILFunctionBlocks *functions, size_t num_functions); +extern char *r2sleigh_function_facts_json_scope_ex(const R2ILContext *ctx, const R2ILBlock **blocks, size_t num_blocks, + unsigned long long fcn_addr, const char *fcn_name, const char *external_context_json, + size_t interproc_iter, size_t interproc_max_iters, int interproc_converged, const char *interproc_scope_json, + const R2ILFunctionBlocks *functions, size_t num_functions); +extern char *r2sleigh_function_plan_json_scope_ex(const R2ILContext *ctx, const R2ILBlock **blocks, size_t num_blocks, + unsigned long long fcn_addr, const char *fcn_name, const char *external_context_json, + size_t interproc_iter, size_t interproc_max_iters, int interproc_converged, const char *interproc_scope_json, + const R2ILFunctionBlocks *functions, size_t num_functions); extern char *r2sleigh_get_direct_call_targets_json(const R2ILContext *ctx, const R2ILBlock **blocks, size_t num_blocks, unsigned long long fcn_addr, const char *fcn_name); extern int r2sleigh_alias_function_analysis_artifact_cache(const R2ILContext *ctx, const R2ILBlock **blocks, @@ -6326,6 +6334,8 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { r_cons_println (cons, "| a:sla.ssa - Show SSA form of instruction"); r_cons_println (cons, "| a:sla.defuse - Show def-use analysis of instruction"); r_cons_println (cons, "| a:sla.types [name|addr] - Dump inferred type write-back payload (current by default)"); + r_cons_println (cons, "| a:sla.facts [name|addr] - Dump combined typed/semantic fact payload (current by default)"); + r_cons_println (cons, "| a:sla.plan [name|addr] - Dump canonical analysis/decompile plan payload (current by default)"); r_cons_println (cons, "| a:sla.ssa.func - Show function SSA with phi nodes"); r_cons_println (cons, "| a:sla.ssa.func.opt - Show optimized function SSA"); r_cons_println (cons, "| a:sla.defuse.func - Show function-wide def-use analysis"); @@ -6924,6 +6934,112 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { return strdup(""); } + if ((!strncmp (cmd, "sla.facts", 9) && (!cmd[9] || isspace ((unsigned char)cmd[9]))) + || (!strncmp (cmd, "sla.plan", 8) && (!cmd[8] || isspace ((unsigned char)cmd[8])))) { + R2ILContext *ctx = get_context (anal); + RAnalFunction *fcn; + BlockArray blocks; + char *external_context_json = NULL; + char *interproc_scope_json = NULL; + char *result = NULL; + bool is_plan_cmd = r_str_startswith (cmd, "sla.plan"); + size_t prefix_len = is_plan_cmd? 8: 9; + const char *target_arg = skip_cmd_spaces (cmd + prefix_len); + ut64 *seen_addrs = NULL; + size_t seen_count = 0; + size_t seen_cap = 0; + int interproc_max_iters = cfg_get_type_interproc_max_iters (anal); + bool prefer_bounded_semantic_type_plan = false; + + if (!ctx) { + R_LOG_ERROR ("r2sleigh: no context"); + return strdup (""); + } + + fcn = (target_arg && *target_arg) + ? resolve_or_materialize_function_target (core, anal, target_arg) + : resolve_or_materialize_current_function (core, anal); + if (!fcn) { + if (target_arg && *target_arg) { + R_LOG_ERROR ("r2sleigh: function target not found: %s", target_arg); + } else { + R_LOG_ERROR ("r2sleigh: no function at current address"); + } + return strdup (""); + } + if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { + R_LOG_ERROR ("r2sleigh: failed to lift function blocks"); + return strdup (""); + } + prefer_bounded_semantic_type_plan = should_skip_decompile_symbolic_scope (fcn); + + external_context_json = sleigh_collect_external_context_json (anal, fcn); + if (!external_context_json || (external_context_json[0] != '{' && external_context_json[0] != '[')) { + free (external_context_json); + external_context_json = strdup ("{}"); + } + + if (!prefer_bounded_semantic_type_plan) { + warm_type_payload_cache_for_function (core, anal, ctx, fcn, interproc_max_iters, + &seen_addrs, &seen_count, &seen_cap); + interproc_scope_json = build_type_interproc_scope_json (core, anal, ctx, fcn, &blocks); + } + SymFunctionScope sym_scope; + if (build_symbolic_function_scope (anal, fcn, ctx, &sym_scope)) { + result = is_plan_cmd + ? r2sleigh_function_plan_json_scope_ex (ctx, + (const R2ILBlock **)blocks.blocks, blocks.count, fcn->addr, fcn->name, + external_context_json, + 1, + prefer_bounded_semantic_type_plan? 1: interproc_max_iters, + prefer_bounded_semantic_type_plan? 0: 1, + interproc_scope_json? interproc_scope_json: "{}", + sym_scope.functions, sym_scope.count) + : r2sleigh_function_facts_json_scope_ex (ctx, + (const R2ILBlock **)blocks.blocks, blocks.count, fcn->addr, fcn->name, + external_context_json, + 1, + prefer_bounded_semantic_type_plan? 1: interproc_max_iters, + prefer_bounded_semantic_type_plan? 0: 1, + interproc_scope_json? interproc_scope_json: "{}", + sym_scope.functions, sym_scope.count); + sym_function_scope_free (&sym_scope); + } else { + result = is_plan_cmd + ? r2sleigh_function_plan_json_scope_ex (ctx, + (const R2ILBlock **)blocks.blocks, blocks.count, fcn->addr, fcn->name, + external_context_json, + 1, + prefer_bounded_semantic_type_plan? 1: interproc_max_iters, + prefer_bounded_semantic_type_plan? 0: 1, + interproc_scope_json? interproc_scope_json: "{}", + NULL, 0) + : r2sleigh_function_facts_json_scope_ex (ctx, + (const R2ILBlock **)blocks.blocks, blocks.count, fcn->addr, fcn->name, + external_context_json, + 1, + prefer_bounded_semantic_type_plan? 1: interproc_max_iters, + prefer_bounded_semantic_type_plan? 0: 1, + interproc_scope_json? interproc_scope_json: "{}", + NULL, 0); + } + if (cons) { + if (result && *result) { + r_cons_printf (cons, "%s\n", result); + } else { + r_cons_println (cons, "{}"); + } + } + if (result) { + r2il_string_free (result); + } + free (seen_addrs); + free (interproc_scope_json); + free (external_context_json); + block_array_free (&blocks); + return strdup (""); + } + if (!strncmp (cmd, "sla.types", 9) && (!cmd[9] || isspace ((unsigned char)cmd[9]))) { R2ILContext *ctx = get_context (anal); RAnalFunction *fcn; diff --git a/r2plugin/src/decompiler.rs b/r2plugin/src/decompiler.rs index a6ec064..7d1736f 100644 --- a/r2plugin/src/decompiler.rs +++ b/r2plugin/src/decompiler.rs @@ -170,6 +170,24 @@ pub(crate) fn run_full_decompile_on_large_stack( }; artifact = rename_function_artifact_for_display(artifact, &display_func_name); + if let Some(route) = r2dec::detached_semantic_route_plan( + &display_func_name, + &r2il_blocks, + artifact.function_facts.semantics.as_ref(), + ) { + match route { + r2dec::SemanticRoutePlan::VmSummary { .. } => { + if let Some(output) = + r2dec::render_vm_semantic_summary(&display_func_name, &artifact.function_facts) + { + return output; + } + } + r2dec::SemanticRoutePlan::FallbackComment { comment } => return comment, + _ => {} + } + } + let decompiler = r2dec::Decompiler::new(config); let semantic_fallback_output = r2dec::semantic_fallback_comment( &display_func_name, diff --git a/r2plugin/src/lib.rs b/r2plugin/src/lib.rs index bac4053..8837a6c 100644 --- a/r2plugin/src/lib.rs +++ b/r2plugin/src/lib.rs @@ -3356,6 +3356,90 @@ struct InferredTypeWritebackJson { diagnostics: TypeWritebackDiagnosticsJson, } +#[derive(Debug, serde::Serialize)] +struct CfgRiskSummaryJson { + block_count: usize, + loop_count: usize, + back_edge_count: usize, + switch_block_count: usize, + max_switch_cases: usize, +} + +#[derive(Debug, serde::Serialize)] +struct SemanticRoutePlanJson { + kind: String, + #[serde(skip_serializing_if = "Option::is_none")] + reason: Option, + #[serde(skip_serializing_if = "Option::is_none")] + comment: Option, +} + +#[derive(Debug, serde::Serialize)] +struct FunctionFactsReportJson { + function_name: String, + function_addr: u64, + cfg_risk: CfgRiskSummaryJson, + #[serde(skip_serializing_if = "Option::is_none")] + semantic_build_plan: Option, + #[serde(skip_serializing_if = "Option::is_none")] + semantic_route: Option, + type_writeback: InferredTypeWritebackJson, +} + +#[derive(Debug, serde::Serialize)] +struct FunctionPlanReportJson { + function_name: String, + function_addr: u64, + cfg_risk: CfgRiskSummaryJson, + #[serde(skip_serializing_if = "Option::is_none")] + semantic: Option, + #[serde(skip_serializing_if = "Option::is_none")] + semantic_build_plan: Option, + #[serde(skip_serializing_if = "Option::is_none")] + semantic_route: Option, + prefer_bounded_type_plan: bool, +} + +fn cfg_risk_summary_json(summary: r2ssa::CFGRiskSummary) -> CfgRiskSummaryJson { + CfgRiskSummaryJson { + block_count: summary.block_count, + loop_count: summary.loop_count, + back_edge_count: summary.back_edge_count, + switch_block_count: summary.switch_block_count, + max_switch_cases: summary.max_switch_cases, + } +} + +fn semantic_route_plan_json(route: r2dec::SemanticRoutePlan) -> SemanticRoutePlanJson { + match route { + r2dec::SemanticRoutePlan::Standard => SemanticRoutePlanJson { + kind: "standard".to_string(), + reason: None, + comment: None, + }, + r2dec::SemanticRoutePlan::StructuredWorker { reason } => SemanticRoutePlanJson { + kind: "structured_worker".to_string(), + reason: Some(reason), + comment: None, + }, + r2dec::SemanticRoutePlan::LinearWorker { reason } => SemanticRoutePlanJson { + kind: "linear_worker".to_string(), + reason: Some(reason), + comment: None, + }, + r2dec::SemanticRoutePlan::VmSummary { reason } => SemanticRoutePlanJson { + kind: "vm_summary".to_string(), + reason: Some(reason), + comment: None, + }, + r2dec::SemanticRoutePlan::FallbackComment { comment } => SemanticRoutePlanJson { + kind: "fallback_comment".to_string(), + reason: None, + comment: Some(comment), + }, + } +} + fn evidence_json(evidence: &[r2types::WritebackEvidence]) -> Vec { evidence .iter() @@ -5549,6 +5633,202 @@ fn infer_type_writeback_json_impl(input: TypeWritebackInferenceInput<'_>) -> *mu } } +fn function_facts_json_impl(input: TypeWritebackInferenceInput<'_>) -> *mut c_char { + let Some(function_input) = types::build_function_input( + input.ctx, + input.blocks, + input.num_blocks, + input.fcn_addr, + input.fcn_name, + ) else { + return ptr::null_mut(); + }; + let external_context = cstr_or_default(input.external_context_json, "{}"); + let symbolic_scope = if input.scope_functions.is_null() || input.scope_num_functions == 0 { + None + } else { + unsafe { + analysis::sym::build_symbolic_scope_from_ffi( + input.scope_functions, + input.scope_num_functions, + function_input.ctx.arch, + function_input.function_addr, + ) + } + }; + let Some(function_analysis) = types::build_function_analysis(&function_input) else { + return ptr::null_mut(); + }; + let cfg_risk = cfg_risk_summary_json(function_analysis.ssa_func.function().cfg_risk_summary()); + let (arch_name, ptr_bits, _) = r2dec::DecompilerConfig::for_arch(function_input.ctx.arch); + let cached_artifact = types::get_cached_function_analysis_artifact_with_scope( + &function_input, + &external_context, + symbolic_scope.as_ref(), + ); + + let (type_writeback, semantic_artifact) = if let Some(cached_artifact) = cached_artifact { + let semantics = cached_artifact.function_facts.semantics.clone(); + let payload = if let Some(compiled) = semantics.as_ref() + && r2types::semantic_artifact_prefers_bounded_type_plan(compiled) + { + semantic_type_fallback_payload( + &function_input.function_name, + &arch_name, + ptr_bits, + input.interproc, + compiled, + ) + } else { + type_writeback_payload_from_artifact(cached_artifact.clone(), input.interproc) + }; + (payload, semantics) + } else { + let semantic_artifact = r2sym::compile_semantic_artifact_default_with_scope( + &z3::Context::thread_local(), + &function_analysis.ssa_func, + symbolic_scope.as_ref(), + function_input.ctx.arch, + ); + if r2types::semantic_artifact_prefers_bounded_type_plan(&semantic_artifact) { + let payload = semantic_type_fallback_payload( + &function_input.function_name, + &arch_name, + ptr_bits, + input.interproc, + &semantic_artifact, + ); + (payload, Some(semantic_artifact)) + } else { + let interproc_summary_set = types::build_interproc_summary_set( + &function_input, + &function_analysis, + input.interproc.scope_json, + input.interproc.max_iters, + ); + let Some(artifact) = + types::build_function_analysis_artifact_from_analysis_with_semantic_artifact( + &function_input, + function_analysis, + &external_context, + Some(interproc_summary_set), + semantic_artifact, + ) + else { + return ptr::null_mut(); + }; + let semantics = artifact.function_facts.semantics.clone(); + ( + type_writeback_payload_from_artifact(artifact, input.interproc), + semantics, + ) + } + }; + + let semantic_route = semantic_artifact + .as_ref() + .and_then(|artifact| { + r2dec::detached_semantic_route_plan( + &function_input.function_name, + function_input.blocks.as_slice(), + Some(artifact), + ) + }) + .map(semantic_route_plan_json); + + let payload = FunctionFactsReportJson { + function_name: function_input.function_name.clone(), + function_addr: function_input.function_addr, + cfg_risk, + semantic_build_plan: semantic_artifact + .as_ref() + .map(r2sym::SemanticArtifact::build_plan), + semantic_route, + type_writeback, + }; + + match serde_json::to_string(&payload) { + Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), + Err(_) => ptr::null_mut(), + } +} + +fn function_plan_json_impl(input: TypeWritebackInferenceInput<'_>) -> *mut c_char { + let Some(function_input) = types::build_function_input( + input.ctx, + input.blocks, + input.num_blocks, + input.fcn_addr, + input.fcn_name, + ) else { + return ptr::null_mut(); + }; + let external_context = cstr_or_default(input.external_context_json, "{}"); + let symbolic_scope = if input.scope_functions.is_null() || input.scope_num_functions == 0 { + None + } else { + unsafe { + analysis::sym::build_symbolic_scope_from_ffi( + input.scope_functions, + input.scope_num_functions, + function_input.ctx.arch, + function_input.function_addr, + ) + } + }; + let Some(function_analysis) = types::build_function_analysis(&function_input) else { + return ptr::null_mut(); + }; + let cfg_risk = cfg_risk_summary_json(function_analysis.ssa_func.function().cfg_risk_summary()); + let semantic_artifact = types::get_cached_function_analysis_artifact_with_scope( + &function_input, + &external_context, + symbolic_scope.as_ref(), + ) + .and_then(|artifact| artifact.function_facts.semantics) + .or_else(|| { + Some(r2sym::compile_semantic_artifact_default_with_scope( + &z3::Context::thread_local(), + &function_analysis.ssa_func, + symbolic_scope.as_ref(), + function_input.ctx.arch, + )) + }); + + let semantic_route = semantic_artifact + .as_ref() + .and_then(|artifact| { + r2dec::detached_semantic_route_plan( + &function_input.function_name, + function_input.blocks.as_slice(), + Some(artifact), + ) + }) + .map(semantic_route_plan_json); + let prefer_bounded_type_plan = semantic_artifact + .as_ref() + .is_some_and(r2types::semantic_artifact_prefers_bounded_type_plan); + + let payload = FunctionPlanReportJson { + function_name: function_input.function_name.clone(), + function_addr: function_input.function_addr, + cfg_risk, + semantic: semantic_artifact + .as_ref() + .map(analysis::sym::compiled_semantic_info), + semantic_build_plan: semantic_artifact + .as_ref() + .map(r2sym::SemanticArtifact::build_plan), + semantic_route, + prefer_bounded_type_plan, + }; + + match serde_json::to_string(&payload) { + Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), + Err(_) => ptr::null_mut(), + } +} + fn semantic_worker_linearization_impl(input: SemanticWorkerLinearizationInput) -> *mut c_char { let summary = r2ssa::CFGRiskSummary { block_count: input.block_count, @@ -5721,6 +6001,74 @@ pub extern "C" fn r2sleigh_infer_type_writeback_json_scope_ex( }) } +#[unsafe(no_mangle)] +pub extern "C" fn r2sleigh_function_facts_json_scope_ex( + ctx: *const R2ILContext, + blocks: *const *const R2ILBlock, + num_blocks: usize, + fcn_addr: u64, + fcn_name: *const c_char, + external_context_json: *const c_char, + interproc_iter: usize, + interproc_max_iters: usize, + interproc_converged: i32, + interproc_scope_json: *const c_char, + scope_functions: *const analysis::sym::R2ILFunctionBlocks, + scope_num_functions: usize, +) -> *mut c_char { + let scope = cstr_or_default(interproc_scope_json, "{}"); + function_facts_json_impl(TypeWritebackInferenceInput { + ctx, + blocks, + num_blocks, + fcn_addr, + fcn_name, + external_context_json, + scope_functions, + scope_num_functions, + interproc: InterprocInferenceInput { + iter: interproc_iter.max(1), + max_iters: interproc_max_iters.max(1), + converged: interproc_converged != 0, + scope_json: &scope, + }, + }) +} + +#[unsafe(no_mangle)] +pub extern "C" fn r2sleigh_function_plan_json_scope_ex( + ctx: *const R2ILContext, + blocks: *const *const R2ILBlock, + num_blocks: usize, + fcn_addr: u64, + fcn_name: *const c_char, + external_context_json: *const c_char, + interproc_iter: usize, + interproc_max_iters: usize, + interproc_converged: i32, + interproc_scope_json: *const c_char, + scope_functions: *const analysis::sym::R2ILFunctionBlocks, + scope_num_functions: usize, +) -> *mut c_char { + let scope = cstr_or_default(interproc_scope_json, "{}"); + function_plan_json_impl(TypeWritebackInferenceInput { + ctx, + blocks, + num_blocks, + fcn_addr, + fcn_name, + external_context_json, + scope_functions, + scope_num_functions, + interproc: InterprocInferenceInput { + iter: interproc_iter.max(1), + max_iters: interproc_max_iters.max(1), + converged: interproc_converged != 0, + scope_json: &scope, + }, + }) +} + #[unsafe(no_mangle)] pub extern "C" fn r2dec_semantic_worker_linearization_scope_ffi( ctx: *const R2ILContext, diff --git a/tests/r2r/db/extras/r2sleigh_integration_extended b/tests/r2r/db/extras/r2sleigh_integration_extended index cc4de8c..eb5fcd6 100644 --- a/tests/r2r/db/extras/r2sleigh_integration_extended +++ b/tests/r2r/db/extras/r2sleigh_integration_extended @@ -244,7 +244,7 @@ true EOF_EXPECT CMDS=<=8 and (.semantic.vm_step.loop_latches|length)>=7 and (.semantic.vm_step.step_blocks|length)>=10 and (.semantic.vm_step.handler_state_updates | to_entries | length) >= 5 and ((.semantic.vm_step.handler_state_updates | to_entries | map(.value | length) | add) >= 5)' +a:sla.sym sym.tiny_vm_dispatch | jq -c '.semantic.execution=="vm" and .semantic.granularity=="summary_only" and (.semantic.slice_class|startswith("interpreter_")) and .semantic.type_plan != null and .semantic.vm_step != null and (.semantic.vm_step.dispatch_targets|length)>=8 and (.semantic.vm_step.loop_latches|length)>=7 and (.semantic.vm_step.step_blocks|length)>=10 and (.semantic.vm_step.handler_state_updates | to_entries | length) >= 5 and ((.semantic.vm_step.handler_state_updates | to_entries | map(.value | length) | add) >= 5)' EOF_CMDS RUN @@ -281,7 +281,31 @@ true EOF_EXPECT CMDS=<= 8) and ((.semantic.vm_transfer.transfers | map(select(.redispatch)) | length) >= 4) and (((.semantic.vm_transfer.handler_exit_guards // {}) | keys | length) >= 1) and (((.semantic.vm_transfer.handler_memory_read_effects // {}) | keys | length) >= 1) and (((.semantic.vm_transfer.handler_memory_write_effects // {}) | keys | length) >= 1)' +a:sla.sym sym.interpret_bytecode | jq -c '.semantic.execution=="vm" and .semantic.query_plan != null and .semantic.vm_transfer != null and ((.semantic.vm_transfer.transfers | length) >= 8) and ((.semantic.vm_transfer.transfers | map(select(.redispatch)) | length) >= 4) and (((.semantic.vm_transfer.handler_exit_guards // {}) | keys | length) >= 1) and (((.semantic.vm_transfer.handler_memory_read_effects // {}) | keys | length) >= 1) and (((.semantic.vm_transfer.handler_memory_write_effects // {}) | keys | length) >= 1)' +EOF_CMDS +RUN + +NAME=facts_native_symbolic_guard_reports_combined_payload +FILE=bins/vuln_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=<= 1 and .semantic_route.kind=="standard" and .type_writeback.compiled_semantics.execution=="native" and (.type_writeback.compiled_semantics.region_count >= 1) and (.type_writeback.semantics.body.Native.regions | length) >= 1' +EOF_CMDS +RUN + +NAME=plan_tiny_vm_dispatch_reports_vm_summary_route +FILE=bins/stress_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=<= 8 and .semantic.execution=="vm" and .semantic_route.kind=="vm_summary" and .semantic_build_plan != null and (.prefer_bounded_type_plan | type=="boolean")' EOF_CMDS RUN @@ -378,7 +402,7 @@ true EOF_EXPECT CMDS=< Date: Thu, 23 Apr 2026 18:39:52 +0000 Subject: [PATCH 09/10] Overhaul symex pipeline and scope-aware interproc integration --- crates/r2dec/src/consumer_fallback.rs | 27 +- crates/r2dec/src/fold/context.rs | 6 +- crates/r2dec/src/fold/flags.rs | 9 + crates/r2dec/src/fold/op_lower/calls.rs | 4 +- crates/r2dec/src/fold/op_lower/mod.rs | 33 +- crates/r2dec/src/fold/tests/flags.rs | 1 + crates/r2dec/src/fold/tests/pipeline.rs | 91 + crates/r2dec/src/lib.rs | 237 +- crates/r2dec/src/planner.rs | 129 +- crates/r2ssa/src/assumption.rs | 225 + crates/r2ssa/src/cfg.rs | 277 +- crates/r2ssa/src/function.rs | 120 + crates/r2ssa/src/interproc.rs | 89 + crates/r2ssa/src/lib.rs | 17 +- crates/r2ssa/src/semantic.rs | 224 +- crates/r2sym/src/constraints.rs | 1918 +++++++ crates/r2sym/src/executor.rs | 946 +++- crates/r2sym/src/kernel.rs | 595 +++ crates/r2sym/src/lib.rs | 87 +- crates/r2sym/src/loops.rs | 2721 ++++++++++ crates/r2sym/src/memory.rs | 32 +- crates/r2sym/src/path.rs | 4518 +++++++++++++---- crates/r2sym/src/query.rs | 2135 +++++++- crates/r2sym/src/replay.rs | 247 + crates/r2sym/src/runtime.rs | 684 ++- crates/r2sym/src/semantics/artifact.rs | 65 +- crates/r2sym/src/semantics/cache.rs | 71 +- crates/r2sym/src/semantics/compiler.rs | 432 +- crates/r2sym/src/semantics/facts.rs | 12 +- crates/r2sym/src/semantics/mod.rs | 17 +- crates/r2sym/src/semantics/plan.rs | 249 +- crates/r2sym/src/sim.rs | 169 +- crates/r2sym/src/solver.rs | 6 +- crates/r2sym/src/state.rs | 757 ++- crates/r2sym/src/tactics.rs | 1930 +++++++ crates/r2sym/src/value.rs | 49 +- crates/r2sym/src/verification.rs | 1122 ++++ crates/r2sym/tests/symex_integration.rs | 15 +- crates/r2types/src/context.rs | 73 + crates/r2types/src/function_facts.rs | 363 +- crates/r2types/src/lib.rs | 6 +- crates/r2types/src/writeback.rs | 890 +++- r2plugin/r_anal_sleigh.c | 1724 ++++++- r2plugin/src/analysis/sym.rs | 1523 +++++- r2plugin/src/decompiler.rs | 14 +- r2plugin/src/lib.rs | 1090 +++- r2plugin/src/types.rs | 336 +- .../db/extras/r2sleigh_integration_extended | 63 + .../db/extras/r2sleigh_signature_snapshots | 4 +- 49 files changed, 23926 insertions(+), 2426 deletions(-) create mode 100644 crates/r2ssa/src/assumption.rs create mode 100644 crates/r2sym/src/constraints.rs create mode 100644 crates/r2sym/src/kernel.rs create mode 100644 crates/r2sym/src/loops.rs create mode 100644 crates/r2sym/src/tactics.rs create mode 100644 crates/r2sym/src/verification.rs diff --git a/crates/r2dec/src/consumer_fallback.rs b/crates/r2dec/src/consumer_fallback.rs index 0c528c5..c510082 100644 --- a/crates/r2dec/src/consumer_fallback.rs +++ b/crates/r2dec/src/consumer_fallback.rs @@ -73,9 +73,9 @@ where pub(crate) fn semantic_fallback_comment( func_name: &str, - semantic_artifact: Option<&r2sym::SemanticArtifact>, + function_facts: &r2types::FunctionFacts, ) -> Option { - let semantic_artifact = semantic_artifact?; + let semantic_artifact = function_facts.semantic_artifact()?; if let Some(comment) = crate::consumer_vm::render_vm_semantic_fallback_comment(func_name, semantic_artifact) { @@ -137,6 +137,29 @@ pub(crate) fn semantic_fallback_comment( reason.push_str(&actionable_preview.join(" | ")); reason.push(']'); } + if function_facts.has_assumption_conflicts() { + reason.push_str(&format!( + "; assumption_conflicts={}", + function_facts.assumption_usage.conflicts.len() + )); + } + if let Some(rollup) = function_facts.summary_rollup() { + if let Some(return_relation) = rollup.root_return_relation.as_ref() { + reason.push_str(&format!("; summary_return={return_relation:?}")); + } + if !rollup.out_param_indices.is_empty() { + reason.push_str("; out_params=["); + reason.push_str( + &rollup + .out_param_indices + .iter() + .map(|idx| idx.to_string()) + .collect::>() + .join(", "), + ); + reason.push(']'); + } + } Some(format!( "/* r2dec fallback: skipped decompilation for {} ({}) */", func_name, reason diff --git a/crates/r2dec/src/fold/context.rs b/crates/r2dec/src/fold/context.rs index 4e15b81..c6ba275 100644 --- a/crates/r2dec/src/fold/context.rs +++ b/crates/r2dec/src/fold/context.rs @@ -11,8 +11,8 @@ use r2ssa::{ #[cfg(test)] use r2types::ExternalStackVarSpec; use r2types::{ - CalleeFact, ExternalStackSlotSpec, ExternalTypeDb, FunctionType, SignatureRegistry, - StackSlotKey, TypeOracle, VisibleBinding, + CalleeFact, ExternalStackSlotSpec, ExternalTypeDb, FunctionType, InterprocSummaryView, + SignatureRegistry, StackSlotKey, TypeOracle, VisibleBinding, }; pub(crate) type SSABlock = FunctionSSABlock; @@ -72,6 +72,7 @@ pub(crate) struct FoldInputs<'a> { pub(crate) function_return_type: Option<&'a CType>, pub(crate) prepared_ssa: Option<&'a SsaArtifact>, pub(crate) interproc_summary_set: Option<&'a InterprocSummarySet>, + pub(crate) summary_view: Option<&'a InterprocSummaryView>, pub(crate) prepared_semantic_view: Option<&'a analysis::PreparedSemanticView>, pub(crate) prepared_objects: Option<&'a ObjectModel>, #[allow(dead_code)] @@ -240,6 +241,7 @@ impl<'a> FoldingContext<'a> { function_return_type: None, prepared_ssa: None, interproc_summary_set: None, + summary_view: None, prepared_semantic_view: None, prepared_objects: None, prepared_memory: None, diff --git a/crates/r2dec/src/fold/flags.rs b/crates/r2dec/src/fold/flags.rs index 4fc0170..2420f87 100644 --- a/crates/r2dec/src/fold/flags.rs +++ b/crates/r2dec/src/fold/flags.rs @@ -1046,6 +1046,15 @@ impl<'a> FoldingContext<'a> { self.prepared_compare_provenance_expr(compare) } + #[cfg(test)] + pub(super) fn prepared_predicate_candidate_for_branch_block_for_test( + &self, + block_addr: u64, + var: &SSAVar, + ) -> Option { + self.prepared_predicate_candidate_for_branch_block(block_addr, var) + } + fn prepared_compare_provenance_expr(&self, prov: &CompareProvenance) -> Option { let lhs_var = self.prepared_var_for_value_id(prov.lhs)?; let rhs_var = self.prepared_var_for_value_id(prov.rhs)?; diff --git a/crates/r2dec/src/fold/op_lower/calls.rs b/crates/r2dec/src/fold/op_lower/calls.rs index 7cd4cf1..8b7aacf 100644 --- a/crates/r2dec/src/fold/op_lower/calls.rs +++ b/crates/r2dec/src/fold/op_lower/calls.rs @@ -158,7 +158,7 @@ impl<'a> FoldingContext<'a> { .lookup_known_signature(name) .and_then(|sig| (!sig.variadic).then_some(sig.params.len())); let summary_arity = self - .interproc_summary_for_name(name) + .summary_helper_view_for_name(name) .and_then(|summary| summary.arg_count_hint); let normalized = normalize_callee_name(name); @@ -202,7 +202,7 @@ impl<'a> FoldingContext<'a> { }; let normalized = normalize_callee_name(name); - if self.interproc_summary_for_name(name).is_some() { + if self.summary_helper_view_for_name(name).is_some() { return true; } diff --git a/crates/r2dec/src/fold/op_lower/mod.rs b/crates/r2dec/src/fold/op_lower/mod.rs index 9712e52..4f8f406 100644 --- a/crates/r2dec/src/fold/op_lower/mod.rs +++ b/crates/r2dec/src/fold/op_lower/mod.rs @@ -23,9 +23,9 @@ use std::collections::{BTreeSet, HashMap, HashSet}; use r2ssa::{ - CallSiteFact, CallSiteFacts, DecompilePrepFacts, FunctionSemanticSummary, InterprocSummarySet, - MemoryDefFact, MemoryLocation, MemoryUseFact, ObjectKind, ObjectModel, PredicateFacts, - PreparedFunctionFacts, SSAFunction, SSAOp, SSAVar, SsaArtifact, + CallSiteFact, CallSiteFacts, DecompilePrepFacts, MemoryDefFact, MemoryLocation, MemoryUseFact, + ObjectKind, ObjectModel, PredicateFacts, PreparedFunctionFacts, SSAFunction, SSAOp, SSAVar, + SsaArtifact, }; #[cfg(test)] use r2types::StackSlotKey; @@ -172,8 +172,15 @@ impl<'a> FoldingContext<'a> { self.inputs.prepared_ssa } - pub(crate) fn interproc_summary_set(&self) -> Option<&InterprocSummarySet> { - self.inputs.interproc_summary_set + pub(crate) fn summary_view(&self) -> Option<&r2types::InterprocSummaryView> { + self.inputs.summary_view + } + + pub(crate) fn summary_helper_view_for_name( + &self, + name: &str, + ) -> Option<&r2types::SummaryHelperView> { + self.summary_view()?.helper_view_for_name(name) } pub(crate) fn prepared_semantic_view(&self) -> Option<&analysis::PreparedSemanticView> { @@ -277,22 +284,6 @@ impl<'a> FoldingContext<'a> { .and_then(|view| view.call_view_for_site((block_addr, op_idx))) } - pub(crate) fn interproc_summary_for_name( - &self, - name: &str, - ) -> Option<&FunctionSemanticSummary> { - let normalized = normalize_callee_name(name); - self.interproc_summary_set()? - .summaries - .values() - .find(|summary| { - summary - .name - .as_deref() - .is_some_and(|summary_name| normalize_callee_name(summary_name) == normalized) - }) - } - pub(crate) fn prepared_memory_uses_for_current_op(&self) -> Option<&[MemoryUseFact]> { let prepared = self.inputs.prepared_ssa?; let block_addr = self.current_block_addr.get()?; diff --git a/crates/r2dec/src/fold/tests/flags.rs b/crates/r2dec/src/fold/tests/flags.rs index fc07e01..80c1b2f 100644 --- a/crates/r2dec/src/fold/tests/flags.rs +++ b/crates/r2dec/src/fold/tests/flags.rs @@ -61,6 +61,7 @@ fn make_x86_64_ctx_with_prepared<'a>(prepared_ssa: &'a r2ssa::SsaArtifact) -> Fo function_return_type: None, prepared_ssa: None, interproc_summary_set: None, + summary_view: None, prepared_semantic_view: None, prepared_objects: None, prepared_memory: None, diff --git a/crates/r2dec/src/fold/tests/pipeline.rs b/crates/r2dec/src/fold/tests/pipeline.rs index 75ee3f3..59ad150 100644 --- a/crates/r2dec/src/fold/tests/pipeline.rs +++ b/crates/r2dec/src/fold/tests/pipeline.rs @@ -233,6 +233,7 @@ mod tests { function_return_type: None, prepared_ssa: None, interproc_summary_set: None, + summary_view: None, prepared_semantic_view: None, prepared_objects: None, prepared_memory: None, @@ -283,6 +284,7 @@ mod tests { function_return_type: None, prepared_ssa: None, interproc_summary_set: None, + summary_view: None, prepared_semantic_view: None, prepared_objects: None, prepared_memory: None, @@ -16233,4 +16235,93 @@ mod tests { )) ); } + + #[test] + fn prepared_predicate_candidate_for_branch_block_uses_block_assumptions_fallback() { + let arch = make_test_arch_x86_64(); + let mut entry = R2ILBlock::new(0x5000, 4); + entry.push(R2ILOp::IntEqual { + dst: Varnode::unique(1, 1), + a: Varnode::register(0x00, 8), + b: Varnode::register(0x18, 8), + }); + entry.push(R2ILOp::CBranch { + target: Varnode::constant(0x5008, 8), + cond: Varnode::unique(1, 1), + }); + let mut fallthrough = R2ILBlock::new(0x5004, 4); + fallthrough.push(R2ILOp::Return { + target: Varnode::constant(0, 8), + }); + let mut taken = R2ILBlock::new(0x5008, 4); + taken.push(R2ILOp::Return { + target: Varnode::constant(1, 8), + }); + + let prepared = prepared_from_r2il_blocks(&[entry, fallthrough, taken], &arch) + .with_name("branch_assumption"); + let mut ctx = make_x86_64_ctx_with_prepared(&prepared); + let cond = make_var("tmp:pred", 1, 1); + let lhs_value_id = prepared + .graph() + .values + .iter() + .find(|value| value.var.name.eq_ignore_ascii_case("rax") && value.var.version == 0) + .map(|value| value.id) + .expect("lhs value id"); + let rhs_value_id = prepared + .graph() + .values + .iter() + .find(|value| value.var.name.eq_ignore_ascii_case("rsi") && value.var.version == 0) + .map(|value| value.id) + .expect("rhs value id"); + let cond_value_id = prepared + .graph() + .values + .iter() + .find(|value| value.var.is_temp()) + .map(|value| value.id) + .expect("cond value id"); + let mut predicate_facts = r2ssa::PredicateFacts::default(); + predicate_facts.predicates.insert( + r2ssa::PredicateId(1), + r2ssa::PredicateFact { + id: r2ssa::PredicateId(1), + block_addr: 0x5000, + condition: lhs_value_id, + comparison: Some(r2ssa::CompareProvenance { + kind: r2ssa::CompareKind::Equal, + lhs: lhs_value_id, + rhs: rhs_value_id, + }), + true_target: 0x5008, + false_target: 0x5004, + }, + ); + predicate_facts.block_assumptions.insert( + 0x5000, + vec![r2ssa::BlockAssumption { + predecessor: 0x5000, + predicate: r2ssa::PredicateId(1), + truth: true, + }], + ); + ctx.inputs.prepared_predicates = Some(Box::leak(Box::new(predicate_facts))); + + assert_ne!( + lhs_value_id, cond_value_id, + "test setup must force the direct predicate match to miss" + ); + assert!( + matches!( + ctx.prepared_predicate_candidate_for_branch_block_for_test(0x5000, &cond), + Some(CExpr::Binary { + op: BinaryOp::Eq, + .. + }) + ), + "expected block_assumptions fallback to recover a compare expression" + ); + } } diff --git a/crates/r2dec/src/lib.rs b/crates/r2dec/src/lib.rs index ffe05f3..767351e 100644 --- a/crates/r2dec/src/lib.rs +++ b/crates/r2dec/src/lib.rs @@ -91,25 +91,23 @@ fn normalize_callee_name(name: &str) -> String { out } -fn prefer_symbolic_large_worker_decompile( - semantic_artifact: Option<&r2sym::SemanticArtifact>, -) -> bool { - planner::prefer_symbolic_large_worker_decompile(semantic_artifact) +fn prefer_symbolic_large_worker_decompile(function_facts: &FunctionFacts) -> bool { + planner::prefer_symbolic_large_worker_decompile(function_facts) } fn should_skip_runtime_type_inference( prepared: Option<&r2ssa::SsaArtifact>, _type_facts: &FunctionTypeFacts, - semantic_artifact: Option<&r2sym::SemanticArtifact>, + function_facts: &FunctionFacts, ) -> bool { - planner::should_skip_runtime_type_inference(prepared, _type_facts, semantic_artifact) + planner::should_skip_runtime_type_inference(prepared, _type_facts, function_facts) } fn should_use_prepared_semantic_view( prepared: Option<&r2ssa::SsaArtifact>, - semantic_artifact: Option<&r2sym::SemanticArtifact>, + function_facts: &FunctionFacts, ) -> bool { - planner::should_use_prepared_semantic_view(prepared, semantic_artifact) + planner::should_use_prepared_semantic_view(prepared, function_facts) } fn seed_runtime_type_hints_from_facts_and_recovery( @@ -361,30 +359,38 @@ pub fn semantic_fallback_comment( func_name: &str, semantic_artifact: Option<&r2sym::SemanticArtifact>, ) -> Option { - consumer_fallback::semantic_fallback_comment(func_name, semantic_artifact) + let function_facts = r2types::FunctionFacts::new( + r2types::FunctionTypeFacts::default(), + semantic_artifact.cloned(), + ); + consumer_fallback::semantic_fallback_comment(func_name, &function_facts) } pub fn preferred_semantic_fallback_comment( func_name: &str, semantic_artifact: Option<&r2sym::SemanticArtifact>, ) -> Option { - planner::preferred_semantic_fallback_comment(func_name, semantic_artifact) + let function_facts = r2types::FunctionFacts::new( + r2types::FunctionTypeFacts::default(), + semantic_artifact.cloned(), + ); + planner::preferred_semantic_fallback_comment(func_name, &function_facts) } pub fn detached_semantic_linearization_reason( func_name: &str, blocks: &[R2ILBlock], - semantic_artifact: Option<&r2sym::SemanticArtifact>, + function_facts: &FunctionFacts, ) -> Option { - planner::detached_semantic_linearization_reason(func_name, blocks, semantic_artifact) + planner::detached_semantic_linearization_reason(func_name, blocks, function_facts) } pub fn detached_semantic_route_plan( func_name: &str, blocks: &[R2ILBlock], - semantic_artifact: Option<&r2sym::SemanticArtifact>, + function_facts: &FunctionFacts, ) -> Option { - planner::detached_semantic_route_plan(func_name, blocks, semantic_artifact) + planner::detached_semantic_route_plan(func_name, blocks, function_facts) } #[cfg_attr(not(test), allow(dead_code))] @@ -393,7 +399,11 @@ fn preferred_semantic_linearization_reason( semantic_artifact: Option<&r2sym::SemanticArtifact>, cfg_summary: &r2ssa::CFGRiskSummary, ) -> Option { - planner::preferred_semantic_linearization_reason(func_name, semantic_artifact, cfg_summary) + let function_facts = r2types::FunctionFacts::new( + r2types::FunctionTypeFacts::default(), + semantic_artifact.cloned(), + ); + planner::preferred_semantic_linearization_reason(func_name, &function_facts, cfg_summary) } #[cfg_attr(not(test), allow(dead_code))] @@ -402,7 +412,11 @@ fn preferred_semantic_structuring_reason( semantic_artifact: Option<&r2sym::SemanticArtifact>, cfg_summary: &r2ssa::CFGRiskSummary, ) -> Option { - planner::preferred_semantic_structuring_reason(func_name, semantic_artifact, cfg_summary) + let function_facts = r2types::FunctionFacts::new( + r2types::FunctionTypeFacts::default(), + semantic_artifact.cloned(), + ); + planner::preferred_semantic_structuring_reason(func_name, &function_facts, cfg_summary) } fn preferred_semantic_worker_reason(cfg_summary: &r2ssa::CFGRiskSummary) -> String { @@ -806,6 +820,7 @@ pub struct DecompilerContext { impl DecompilerContext { fn canonicalize_function_facts(mut function_facts: FunctionFacts) -> FunctionFacts { function_facts.types = function_facts.types.canonicalized(); + function_facts.refresh_plans(); function_facts } @@ -887,6 +902,7 @@ impl DecompilerContext { pub fn with_type_facts(mut self, type_facts: FunctionTypeFacts) -> Self { self.function_facts.types = type_facts.canonicalized(); + self.function_facts.refresh_plans(); self } @@ -894,7 +910,7 @@ impl DecompilerContext { mut self, semantic_artifact: Option, ) -> Self { - self.function_facts.semantics = semantic_artifact; + self.function_facts.set_semantics(semantic_artifact); self } @@ -915,9 +931,10 @@ impl DecompilerInput { pub fn new(prepared_ssa: r2ssa::SsaArtifact, mut context: DecompilerContext) -> Self { context.function_facts = DecompilerContext::canonicalize_function_facts(context.function_facts); + let interproc_summary_set = context.function_facts.interproc_summary_set().cloned(); Self { prepared_ssa, - interproc_summary_set: None, + interproc_summary_set, context, } } @@ -925,6 +942,7 @@ impl DecompilerInput { pub fn with_context(mut self, mut context: DecompilerContext) -> Self { context.function_facts = DecompilerContext::canonicalize_function_facts(context.function_facts); + self.interproc_summary_set = context.function_facts.interproc_summary_set().cloned(); self.context = context; self } @@ -933,6 +951,8 @@ impl DecompilerInput { mut self, interproc_summary_set: Option, ) -> Self { + self.context.function_facts.summary_view = + r2types::InterprocSummaryView::new(interproc_summary_set.clone()); self.interproc_summary_set = interproc_summary_set; self } @@ -997,6 +1017,7 @@ impl Decompiler { /// Set externally recovered type facts. pub fn set_type_facts(&mut self, type_facts: FunctionTypeFacts) { self.context.function_facts.types = type_facts.canonicalized(); + self.context.function_facts.refresh_plans(); } pub fn set_function_facts(&mut self, function_facts: FunctionFacts) { @@ -1028,7 +1049,7 @@ impl Decompiler { .unwrap_or_else(|| format!("sub_{:x}", func.entry)); let semantic_route = planner::semantic_route_plan( &func_name, - self.context.semantic_artifact(), + &self.context.function_facts, &func.cfg_risk_summary(), ); if let Some(output) = self.vm_summary_output_for_route(&func_name, &semantic_route) { @@ -1051,7 +1072,7 @@ impl Decompiler { .unwrap_or_else(|| format!("sub_{:x}", func.entry)); let semantic_route = planner::semantic_route_plan( &func_name, - self.context.semantic_artifact(), + &input.context.function_facts, &func.cfg_risk_summary(), ); if let Some(output) = self.vm_summary_output_for_route(&func_name, &semantic_route) { @@ -1363,7 +1384,7 @@ impl Decompiler { let skip_runtime_type_inference = should_skip_runtime_type_inference( prepared, self.context.type_facts(), - self.context.semantic_artifact(), + &self.context.function_facts, ); let type_inference = (!skip_runtime_type_inference).then(|| { let mut type_inference = TypeInference::new_with_abi( @@ -1507,24 +1528,21 @@ impl Decompiler { arg_regs: self.config.arg_regs.clone(), caller_saved_regs: self.config.caller_saved_regs.clone(), }; - let prepared_semantic_view = should_use_prepared_semantic_view( - prepared, - self.context.semantic_artifact(), - ) - .then(|| { - analysis::PreparedSemanticView::build(analysis::PreparedSemanticViewInputs { - prepared: prepared.expect("prepared semantic view requires prepared artifact"), - interproc_summary_set, - abi_arg_regs: &self.config.arg_regs, - ret_reg_name: &fold_arch.ret_reg_name, - function_names: &self.context.function_names, - symbols: &self.context.symbols, - callee_facts: &self.context.type_facts().callee_facts, - stack_slots: &self.context.type_facts().stack_slots, - visible_bindings: &self.context.type_facts().visible_bindings, - param_register_aliases: ¶m_register_aliases, - }) - }); + let prepared_semantic_view = + should_use_prepared_semantic_view(prepared, &self.context.function_facts).then(|| { + analysis::PreparedSemanticView::build(analysis::PreparedSemanticViewInputs { + prepared: prepared.expect("prepared semantic view requires prepared artifact"), + interproc_summary_set, + abi_arg_regs: &self.config.arg_regs, + ret_reg_name: &fold_arch.ret_reg_name, + function_names: &self.context.function_names, + symbols: &self.context.symbols, + callee_facts: &self.context.type_facts().callee_facts, + stack_slots: &self.context.type_facts().stack_slots, + visible_bindings: &self.context.type_facts().visible_bindings, + param_register_aliases: ¶m_register_aliases, + }) + }); let fold_inputs = FoldInputs { arch: &fold_arch, function_names: &self.context.function_names, @@ -1544,6 +1562,7 @@ impl Decompiler { function_return_type: signature_ret_type.as_ref().or(Some(&inferred_ret_type)), prepared_ssa: prepared, interproc_summary_set, + summary_view: Some(&self.context.function_facts.summary_view), prepared_semantic_view: prepared_semantic_view.as_ref(), prepared_objects: prepared.map(|artifact| artifact.objects()), prepared_memory: prepared.map(|artifact| artifact.memory()), @@ -1560,7 +1579,7 @@ impl Decompiler { .unwrap_or_else(|| format!("sub_{:x}", func.entry)); let semantic_route = planner::semantic_route_plan( &func_name, - self.context.semantic_artifact(), + &self.context.function_facts, &func.cfg_risk_summary(), ); @@ -1589,7 +1608,7 @@ impl Decompiler { func, &fold_ctx, folded_reason, - prefer_symbolic_large_worker_decompile(self.context.semantic_artifact()) + prefer_symbolic_large_worker_decompile(&self.context.function_facts) .then(|| preferred_semantic_worker_reason(&func.cfg_risk_summary())) .as_deref(), || self.linearize_function_body(func, &fold_ctx), @@ -4474,6 +4493,7 @@ mod tests { function_return_type: signature_ret_type.as_ref().or(Some(&inferred_ret_type)), prepared_ssa: None, interproc_summary_set: None, + summary_view: Some(&decompiler.context.function_facts.summary_view), prepared_semantic_view: None, prepared_objects: None, prepared_memory: None, @@ -5282,6 +5302,93 @@ mod tests { ); } + #[test] + fn preferred_semantic_structuring_reason_blocks_conflicting_assumptions_and_downgrades_linearization() + { + let likely = + r2sym::SemanticEvidence::likely(r2sym::SemanticEvidenceReason::PartialPathCoverage); + let memory_term = arg_memory_term(0, 1, likely.clone(), Some("0x0:8"), true); + let region = test_semantic_region( + 0x401000, + BTreeSet::from([0x401010, 0x401020]), + vec![test_control_fact( + 0x401010, + r2sym::SymbolicReachabilityStatus::Reachable, + None, + Some("x == 0"), + Some(compiled_summary( + "x == 0", + r2sym::BackwardConditionPrecision::OverApprox, + 1, + 2, + vec![memory_term.clone()], + )), + likely.clone(), + )], + vec![test_memory_fact(memory_term, likely.clone())], + ); + let summary = r2ssa::CFGRiskSummary { + block_count: 107, + loop_count: 16, + back_edge_count: 16, + switch_block_count: 0, + max_switch_cases: 0, + }; + let semantic_artifact = large_cfg_worker_artifact( + r2sym::RefinementStage::Compiled, + vec![r2sym::ResidualReason::LargeCfg], + vec![region], + ); + let mut assumption_usage = r2ssa::AssumptionUsageReport::default(); + assumption_usage.mark_conflict( + &r2ssa::AnalysisAssumption { + id: Some("assumption_a".to_string()), + subject: r2ssa::AssumptionSubject::Parameter { index: 0 }, + value: r2ssa::AssumptionValue::TypeHint { + ty: "int32_t".to_string(), + }, + scope: r2ssa::AssumptionScope::Function, + provenance: r2ssa::AssumptionProvenance::User, + }, + "parameter type conflict", + ); + assumption_usage.mark_conflict( + &r2ssa::AnalysisAssumption { + id: Some("assumption_b".to_string()), + subject: r2ssa::AssumptionSubject::Register { + name: "rax".to_string(), + }, + value: r2ssa::AssumptionValue::TypeHint { + ty: "int64_t".to_string(), + }, + scope: r2ssa::AssumptionScope::Function, + provenance: r2ssa::AssumptionProvenance::User, + }, + "register type conflict", + ); + let function_facts = r2types::FunctionFacts::new( + r2types::FunctionTypeFacts::default(), + Some(semantic_artifact), + ) + .with_assumption_usage(assumption_usage); + assert!( + crate::planner::preferred_semantic_structuring_reason( + "fcn.401000", + &function_facts, + &summary, + ) + .is_none(), + "assumption conflicts must block semantic structuring" + ); + let linear_reason = crate::planner::preferred_semantic_linearization_reason( + "fcn.401000", + &function_facts, + &summary, + ) + .expect("assumption conflicts should downgrade to linearization"); + assert!(linear_reason.contains("complex loop graph")); + } + #[test] fn autogenerated_name_detection_accepts_underscore_hex_labels() { assert!(is_autogenerated_function_name("_140010138")); @@ -5406,4 +5513,52 @@ mod tests { assert!(output.contains("exact_conditions=1")); assert!(output.contains("actionable_preview=[0x401000: x == 0]")); } + + #[test] + fn semantic_fallback_comment_reports_assumption_conflicts() { + let semantic_artifact = large_cfg_worker_artifact( + r2sym::RefinementStage::Residual, + vec![r2sym::ResidualReason::LargeCfg], + Vec::new(), + ); + let mut assumption_usage = r2ssa::AssumptionUsageReport::default(); + assumption_usage.mark_conflict( + &r2ssa::AnalysisAssumption { + id: Some("assumption_c".to_string()), + subject: r2ssa::AssumptionSubject::Parameter { index: 0 }, + value: r2ssa::AssumptionValue::TypeHint { + ty: "int32_t".to_string(), + }, + scope: r2ssa::AssumptionScope::Function, + provenance: r2ssa::AssumptionProvenance::User, + }, + "parameter type conflict", + ); + assumption_usage.mark_conflict( + &r2ssa::AnalysisAssumption { + id: Some("assumption_d".to_string()), + subject: r2ssa::AssumptionSubject::Register { + name: "rdi".to_string(), + }, + value: r2ssa::AssumptionValue::TypeHint { + ty: "int64_t".to_string(), + }, + scope: r2ssa::AssumptionScope::Function, + provenance: r2ssa::AssumptionProvenance::User, + }, + "register type conflict", + ); + let function_facts = r2types::FunctionFacts::new( + r2types::FunctionTypeFacts::default(), + Some(semantic_artifact), + ) + .with_assumption_usage(assumption_usage); + let output = + crate::consumer_fallback::semantic_fallback_comment("_401000", &function_facts) + .expect("typed semantic fallback comment"); + assert!( + output.contains("assumption_conflicts=2"), + "expected fallback comment to report the assumption conflict count, got:\n{output}" + ); + } } diff --git a/crates/r2dec/src/planner.rs b/crates/r2dec/src/planner.rs index 162e6f2..2f8e12f 100644 --- a/crates/r2dec/src/planner.rs +++ b/crates/r2dec/src/planner.rs @@ -1,6 +1,6 @@ use r2il::R2ILBlock; use r2ssa::{CFGRiskSummary, SSAFunction, SsaArtifact}; -use r2types::FunctionTypeFacts; +use r2types::{DecompileCapabilityView, FunctionFacts, FunctionTypeFacts}; #[derive(Debug, Clone, PartialEq, Eq)] pub enum SemanticRoutePlan { @@ -11,34 +11,27 @@ pub enum SemanticRoutePlan { FallbackComment { comment: String }, } -pub(crate) fn prefer_symbolic_large_worker_decompile( - semantic_artifact: Option<&r2sym::SemanticArtifact>, -) -> bool { - let Some(semantic_artifact) = semantic_artifact else { - return false; - }; - semantic_artifact - .decompile_plan() - .allows_native_linearization() - && semantic_artifact.diagnostics.skipped_large_cfg - && matches!( - semantic_artifact.stage, - r2sym::RefinementStage::Residual | r2sym::RefinementStage::Compiled - ) - && semantic_artifact - .native_body() - .is_some_and(|body| matches!(body.summary.slice_class, r2sym::SliceClass::Worker)) - && semantic_artifact - .native_body() - .is_some_and(|body| !body.regions.is_empty()) +fn decompile_capability(function_facts: &FunctionFacts) -> DecompileCapabilityView { + function_facts.decompile_capability() +} + +pub(crate) fn prefer_symbolic_large_worker_decompile(function_facts: &FunctionFacts) -> bool { + let capability = decompile_capability(function_facts); + capability + .plan + .as_ref() + .is_some_and(r2sym::DecompilePlan::allows_native_linearization) + && capability.skipped_large_cfg + && matches!(capability.slice_class, Some(r2sym::SliceClass::Worker)) + && capability.has_native_regions } pub(crate) fn should_skip_runtime_type_inference( prepared: Option<&SsaArtifact>, _type_facts: &FunctionTypeFacts, - semantic_artifact: Option<&r2sym::SemanticArtifact>, + function_facts: &FunctionFacts, ) -> bool { - if prefer_symbolic_large_worker_decompile(semantic_artifact) { + if prefer_symbolic_large_worker_decompile(function_facts) { return true; } let Some(prepared) = prepared else { @@ -53,45 +46,38 @@ pub(crate) fn should_skip_runtime_type_inference( pub(crate) fn should_use_prepared_semantic_view( prepared: Option<&SsaArtifact>, - semantic_artifact: Option<&r2sym::SemanticArtifact>, + function_facts: &FunctionFacts, ) -> bool { - prepared.is_some() && !prefer_symbolic_large_worker_decompile(semantic_artifact) + prepared.is_some() && !prefer_symbolic_large_worker_decompile(function_facts) } pub(crate) fn preferred_semantic_fallback_comment( func_name: &str, - semantic_artifact: Option<&r2sym::SemanticArtifact>, + function_facts: &FunctionFacts, ) -> Option { - let semantic_artifact = semantic_artifact?; + let capability = decompile_capability(function_facts); if !crate::is_autogenerated_function_name(func_name) { return None; } - if semantic_artifact - .decompile_plan() - .allows_native_linearization() + if capability + .plan + .as_ref() + .is_some_and(r2sym::DecompilePlan::allows_native_linearization) { return None; } - if semantic_artifact.diagnostics.skipped_large_cfg { - return crate::consumer_fallback::semantic_fallback_comment( - func_name, - Some(semantic_artifact), - ); + if capability.skipped_large_cfg { + return crate::consumer_fallback::semantic_fallback_comment(func_name, function_facts); } - semantic_artifact - .diagnostics + capability .residual_reasons .contains(&r2sym::ResidualReason::InterpreterRequiresStepSummary) - .then(|| { - crate::consumer_fallback::semantic_fallback_comment(func_name, Some(semantic_artifact)) - }) + .then(|| crate::consumer_fallback::semantic_fallback_comment(func_name, function_facts)) .flatten() } -pub(crate) fn preferred_vm_summary_reason( - semantic_artifact: Option<&r2sym::SemanticArtifact>, -) -> Option { - match semantic_artifact?.decompile_plan() { +pub(crate) fn preferred_vm_summary_reason(function_facts: &FunctionFacts) -> Option { + match function_facts.decompile_plan()? { r2sym::DecompilePlan::VmSummaryOnly { reason } => Some(reason), _ => None, } @@ -99,18 +85,21 @@ pub(crate) fn preferred_vm_summary_reason( pub(crate) fn preferred_semantic_linearization_reason( func_name: &str, - semantic_artifact: Option<&r2sym::SemanticArtifact>, + function_facts: &FunctionFacts, cfg_summary: &CFGRiskSummary, ) -> Option { - let semantic_artifact = semantic_artifact?; + let capability = decompile_capability(function_facts); if !crate::is_autogenerated_function_name(func_name) { return None; } - if !matches!( - semantic_artifact.decompile_plan(), - r2sym::DecompilePlan::NativeLinear { .. } - ) || !semantic_artifact.diagnostics.skipped_large_cfg - { + let plan = capability.plan.as_ref()?; + let downgraded_from_structured = matches!(plan, r2sym::DecompilePlan::NativeStructured) + && (capability.assumption_conflicted + || capability.summary_conflicted + || !capability.ambiguous_targets.is_empty()); + let linear_ready = + matches!(plan, r2sym::DecompilePlan::NativeLinear { .. }) || downgraded_from_structured; + if !linear_ready || !capability.skipped_large_cfg || !capability.has_native_regions { return None; } Some(preferred_semantic_worker_reason(cfg_summary)) @@ -118,20 +107,28 @@ pub(crate) fn preferred_semantic_linearization_reason( pub(crate) fn preferred_semantic_structuring_reason( func_name: &str, - semantic_artifact: Option<&r2sym::SemanticArtifact>, + function_facts: &FunctionFacts, cfg_summary: &CFGRiskSummary, ) -> Option { - let semantic_artifact = semantic_artifact?; + let capability = decompile_capability(function_facts); if !crate::is_autogenerated_function_name(func_name) { return None; } - if !semantic_artifact - .decompile_plan() - .allows_native_structuring() + if !capability + .plan + .as_ref() + .is_some_and(r2sym::DecompilePlan::allows_native_structuring) { return None; } - if !semantic_artifact.diagnostics.skipped_large_cfg { + if !capability.skipped_large_cfg || !capability.has_native_regions { + return None; + } + if capability.actionable_region_count == 0 + || capability.assumption_conflicted + || capability.summary_conflicted + || !capability.ambiguous_targets.is_empty() + { return None; } Some(preferred_semantic_worker_reason(cfg_summary)) @@ -181,22 +178,22 @@ pub fn artifact_guard_fallback_comment(func_name: &str, reason: &str) -> String pub fn semantic_route_plan( func_name: &str, - semantic_artifact: Option<&r2sym::SemanticArtifact>, + function_facts: &FunctionFacts, cfg_summary: &CFGRiskSummary, ) -> SemanticRoutePlan { - if let Some(reason) = preferred_vm_summary_reason(semantic_artifact) { + if let Some(reason) = preferred_vm_summary_reason(function_facts) { return SemanticRoutePlan::VmSummary { reason }; } - if let Some(comment) = preferred_semantic_fallback_comment(func_name, semantic_artifact) { + if let Some(comment) = preferred_semantic_fallback_comment(func_name, function_facts) { return SemanticRoutePlan::FallbackComment { comment }; } if let Some(reason) = - preferred_semantic_structuring_reason(func_name, semantic_artifact, cfg_summary) + preferred_semantic_structuring_reason(func_name, function_facts, cfg_summary) { return SemanticRoutePlan::StructuredWorker { reason }; } if let Some(reason) = - preferred_semantic_linearization_reason(func_name, semantic_artifact, cfg_summary) + preferred_semantic_linearization_reason(func_name, function_facts, cfg_summary) { return SemanticRoutePlan::LinearWorker { reason }; } @@ -206,12 +203,12 @@ pub fn semantic_route_plan( pub fn detached_semantic_route_plan( func_name: &str, blocks: &[R2ILBlock], - semantic_artifact: Option<&r2sym::SemanticArtifact>, + function_facts: &FunctionFacts, ) -> Option { let ssa_func = SSAFunction::from_blocks_raw_no_arch(blocks)?; Some(semantic_route_plan( func_name, - semantic_artifact, + function_facts, &ssa_func.cfg_risk_summary(), )) } @@ -219,9 +216,9 @@ pub fn detached_semantic_route_plan( pub fn detached_semantic_linearization_reason( func_name: &str, blocks: &[R2ILBlock], - semantic_artifact: Option<&r2sym::SemanticArtifact>, + function_facts: &FunctionFacts, ) -> Option { - match detached_semantic_route_plan(func_name, blocks, semantic_artifact)? { + match detached_semantic_route_plan(func_name, blocks, function_facts)? { SemanticRoutePlan::LinearWorker { reason } => Some(reason), _ => None, } diff --git a/crates/r2ssa/src/assumption.rs b/crates/r2ssa/src/assumption.rs new file mode 100644 index 0000000..f7bd406 --- /dev/null +++ b/crates/r2ssa/src/assumption.rs @@ -0,0 +1,225 @@ +use serde::{Deserialize, Serialize}; + +use crate::PredicateId; + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum AssumptionScope { + #[default] + Function, + Query, + Replay, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum AssumptionProvenance { + User, + #[default] + ImportedContext, + Replay, + Derived, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum AssumptionSubject { + Parameter { + index: usize, + }, + Register { + name: String, + }, + StackSlot { + base: String, + offset: i64, + }, + Predicate { + predicate: PredicateId, + block_addr: u64, + predecessor: Option, + }, + Target { + addr: u64, + }, + MemoryWindow { + addr: u64, + size: u32, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum AssumptionValue { + Constant { + value: u64, + }, + Range { + min: u64, + max: u64, + }, + FiniteSet { + values: Vec, + }, + EnumDomain { + name: Option, + values: Vec, + }, + TypeHint { + ty: String, + }, + Branch { + truth: bool, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AnalysisAssumption { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub id: Option, + pub subject: AssumptionSubject, + pub value: AssumptionValue, + #[serde(default)] + pub scope: AssumptionScope, + #[serde(default)] + pub provenance: AssumptionProvenance, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AnalysisAssumptionConflict { + pub assumption: AnalysisAssumption, + pub reason: String, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct AssumptionUsageReport { + pub applied: Vec, + pub ignored: Vec, + pub conflicts: Vec, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct AssumptionSet { + #[serde(default)] + pub items: Vec, +} + +impl AssumptionSet { + pub fn new(items: Vec) -> Self { + Self { items } + } + + pub fn is_empty(&self) -> bool { + self.items.is_empty() + } + + pub fn iter(&self) -> impl Iterator { + self.items.iter() + } + + pub fn push(&mut self, assumption: AnalysisAssumption) { + self.items.push(assumption); + } + + pub fn extend(&mut self, iter: I) + where + I: IntoIterator, + { + self.items.extend(iter); + } + + pub fn type_hints_for_parameter(&self, index: usize) -> impl Iterator { + self.items.iter().filter_map(move |assumption| { + match (&assumption.subject, &assumption.value) { + ( + AssumptionSubject::Parameter { index: subject }, + AssumptionValue::TypeHint { ty }, + ) if *subject == index => Some(ty.as_str()), + _ => None, + } + }) + } + + pub fn type_hints_for_register<'a>(&'a self, reg: &'a str) -> impl Iterator { + self.items.iter().filter_map(move |assumption| { + match (&assumption.subject, &assumption.value) { + (AssumptionSubject::Register { name }, AssumptionValue::TypeHint { ty }) + if name.eq_ignore_ascii_case(reg) => + { + Some(ty.as_str()) + } + _ => None, + } + }) + } + + pub fn branch_truth_for_predicate(&self, predicate: PredicateId) -> Option { + self.items.iter().find_map( + |assumption| match (&assumption.subject, &assumption.value) { + ( + AssumptionSubject::Predicate { + predicate: subject, .. + }, + AssumptionValue::Branch { truth }, + ) if *subject == predicate => Some(*truth), + _ => None, + }, + ) + } +} + +impl AssumptionUsageReport { + pub fn is_empty(&self) -> bool { + self.applied.is_empty() && self.ignored.is_empty() && self.conflicts.is_empty() + } + + pub fn mark_applied(&mut self, assumption: &AnalysisAssumption) { + self.ignored.retain(|item| item != assumption); + self.conflicts.retain(|item| item.assumption != *assumption); + if !self.applied.iter().any(|item| item == assumption) { + self.applied.push(assumption.clone()); + } + } + + pub fn mark_ignored(&mut self, assumption: &AnalysisAssumption) { + if self.applied.iter().any(|item| item == assumption) + || self + .conflicts + .iter() + .any(|item| item.assumption == *assumption) + { + return; + } + if !self.ignored.iter().any(|item| item == assumption) { + self.ignored.push(assumption.clone()); + } + } + + pub fn mark_conflict(&mut self, assumption: &AnalysisAssumption, reason: impl Into) { + self.applied.retain(|item| item != assumption); + self.ignored.retain(|item| item != assumption); + let reason = reason.into(); + if !self + .conflicts + .iter() + .any(|item| item.assumption == *assumption && item.reason == reason) + { + self.conflicts.push(AnalysisAssumptionConflict { + assumption: assumption.clone(), + reason, + }); + } + } + + pub fn extend(&mut self, other: &Self) { + for assumption in &other.applied { + self.mark_applied(assumption); + } + for assumption in &other.ignored { + self.mark_ignored(assumption); + } + for conflict in &other.conflicts { + self.mark_conflict(&conflict.assumption, conflict.reason.clone()); + } + } +} diff --git a/crates/r2ssa/src/cfg.rs b/crates/r2ssa/src/cfg.rs index ecc20bf..2fd4c31 100644 --- a/crates/r2ssa/src/cfg.rs +++ b/crates/r2ssa/src/cfg.rs @@ -3,7 +3,7 @@ //! This module provides a CFG data structure built from r2il blocks, //! which is the foundation for inter-procedural SSA analysis. -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeSet, HashMap, HashSet}; use petgraph::Direction; use petgraph::graph::{DiGraph, NodeIndex}; @@ -203,6 +203,136 @@ impl BasicBlock { } } +fn op_direct_control_target(op: &R2ILOp) -> Option { + match op { + R2ILOp::Branch { target } | R2ILOp::CBranch { target, .. } => { + BasicBlock::extract_const_addr(target) + } + _ => None, + } +} + +fn op_terminates_basic_block(op: &R2ILOp) -> bool { + matches!( + op, + R2ILOp::Branch { .. } + | R2ILOp::CBranch { .. } + | R2ILOp::BranchInd { .. } + | R2ILOp::Return { .. } + ) +} + +fn op_instruction_addr(block: &R2ILBlock, op_idx: usize) -> Option { + block + .op_metadata + .get(&op_idx) + .and_then(|metadata| metadata.instruction_addr) +} + +fn split_internal_control_flow_targets(block: &R2ILBlock) -> Vec { + if block.switch_info.is_some() || block.ops.is_empty() { + return vec![block.clone()]; + } + + let block_end = block.addr.saturating_add(block.size as u64); + if block_end <= block.addr { + return vec![block.clone()]; + } + + let instruction_addrs = block + .op_metadata + .values() + .filter_map(|metadata| metadata.instruction_addr) + .collect::>(); + if instruction_addrs.is_empty() { + return vec![block.clone()]; + } + + let mut op_instruction_addrs = Vec::with_capacity(block.ops.len()); + let mut last_instruction_addr = block.addr; + for op_idx in 0..block.ops.len() { + if let Some(instruction_addr) = op_instruction_addr(block, op_idx) { + last_instruction_addr = instruction_addr; + } + op_instruction_addrs.push(last_instruction_addr); + } + + let mut split_points = BTreeSet::new(); + for (op_idx, op) in block.ops.iter().enumerate() { + if let Some(target) = op_direct_control_target(op) + && target > block.addr + && target < block_end + && instruction_addrs.contains(&target) + { + split_points.insert(target); + } + + if op_terminates_basic_block(op) { + let current_addr = op_instruction_addrs + .get(op_idx) + .copied() + .unwrap_or(block.addr); + let fallthrough = op_instruction_addrs + .iter() + .skip(op_idx + 1) + .copied() + .find(|addr| *addr > current_addr); + if let Some(fallthrough) = fallthrough + && fallthrough > block.addr + && fallthrough < block_end + && instruction_addrs.contains(&fallthrough) + { + split_points.insert(fallthrough); + } + } + } + if split_points.is_empty() { + return vec![block.clone()]; + } + + let mut starts = Vec::with_capacity(split_points.len() + 1); + starts.push(block.addr); + starts.extend(split_points); + + let mut chunks = starts + .iter() + .enumerate() + .map(|(idx, &start)| { + let end = starts.get(idx + 1).copied().unwrap_or(block_end); + R2ILBlock { + addr: start, + size: end.saturating_sub(start).min(u32::MAX as u64) as u32, + ops: Vec::new(), + switch_info: None, + op_metadata: Default::default(), + } + }) + .collect::>(); + + for (op_idx, op) in block.ops.iter().cloned().enumerate() { + let instruction_addr = op_instruction_addrs + .get(op_idx) + .copied() + .unwrap_or(block.addr); + let chunk_idx = starts + .partition_point(|start| *start <= instruction_addr) + .saturating_sub(1) + .min(chunks.len().saturating_sub(1)); + let next_op_idx = chunks[chunk_idx].ops.len(); + chunks[chunk_idx].ops.push(op); + if let Some(metadata) = block.op_metadata.get(&op_idx) { + chunks[chunk_idx] + .op_metadata + .insert(next_op_idx, metadata.clone()); + } + } + + chunks + .into_iter() + .filter(|chunk| !chunk.ops.is_empty()) + .collect() +} + /// A Control Flow Graph for a function. #[derive(Debug, Clone)] pub struct CFG { @@ -257,8 +387,13 @@ impl CFG { let entry = blocks[0].addr; let mut cfg = Self::new(entry); + let normalized_blocks = blocks + .iter() + .flat_map(split_internal_control_flow_targets) + .collect::>(); + // First pass: add all blocks as nodes - for block in blocks { + for block in &normalized_blocks { let bb = BasicBlock::from_r2il(block); cfg.add_block(bb); } @@ -301,11 +436,17 @@ impl CFG { true_target, false_target, } => { - if let Some(&true_idx) = self.addr_to_node.get(&true_target) { - self.graph.add_edge(node_idx, true_idx, CFGEdge::True); - } - if let Some(&false_idx) = self.addr_to_node.get(&false_target) { - self.graph.add_edge(node_idx, false_idx, CFGEdge::False); + if true_target == false_target { + if let Some(&target_idx) = self.addr_to_node.get(&true_target) { + self.graph.add_edge(node_idx, target_idx, CFGEdge::Normal); + } + } else { + if let Some(&true_idx) = self.addr_to_node.get(&true_target) { + self.graph.add_edge(node_idx, true_idx, CFGEdge::True); + } + if let Some(&false_idx) = self.addr_to_node.get(&false_target) { + self.graph.add_edge(node_idx, false_idx, CFGEdge::False); + } } } BlockTerminator::Call { fallthrough, .. } @@ -526,7 +667,7 @@ impl CFG { #[cfg(test)] mod tests { use super::*; - use r2il::{R2ILBlock, R2ILOp, SpaceId, Varnode}; + use r2il::{OpMetadata, R2ILBlock, R2ILOp, SpaceId, Varnode}; fn make_const(val: u64, size: u32) -> Varnode { Varnode { @@ -887,4 +1028,124 @@ mod tests { vec![0x1000, 0x1004, 0x1008] ); } + + #[test] + fn test_internal_pcode_branch_target_splits_block() { + let mut op_metadata = std::collections::BTreeMap::new(); + for op_idx in 0..3 { + op_metadata.insert( + op_idx, + OpMetadata { + instruction_addr: Some(0x1000), + ..Default::default() + }, + ); + } + op_metadata.insert( + 3, + OpMetadata { + instruction_addr: Some(0x1004), + ..Default::default() + }, + ); + + let blocks = vec![R2ILBlock { + addr: 0x1000, + size: 8, + ops: vec![ + R2ILOp::Copy { + dst: make_ram(0x3000, 8), + src: make_const(1, 8), + }, + R2ILOp::CBranch { + target: make_ram(0x1004, 8), + cond: make_const(1, 1), + }, + R2ILOp::Copy { + dst: make_ram(0x3008, 8), + src: make_const(2, 8), + }, + R2ILOp::Return { + target: make_ram(0, 8), + }, + ], + switch_info: None, + op_metadata, + }]; + + let cfg = CFG::from_blocks(&blocks).expect("cfg"); + assert_eq!(cfg.block_addrs().collect::>(), vec![0x1000, 0x1004]); + assert_eq!(cfg.get_block(0x1000).expect("entry").ops.len(), 3); + assert_eq!(cfg.get_block(0x1004).expect("target").ops.len(), 1); + assert_eq!(cfg.successors(0x1000), vec![0x1004]); + assert_eq!(cfg.predecessors(0x1004), vec![0x1000]); + } + + #[test] + fn test_internal_conditional_fallthrough_splits_block() { + let mut op_metadata = std::collections::BTreeMap::new(); + for (op_idx, instruction_addr) in [ + (0, 0x1000), + (1, 0x1004), + (2, 0x1008), + (3, 0x100c), + (4, 0x1010), + (5, 0x1014), + ] { + op_metadata.insert( + op_idx, + OpMetadata { + instruction_addr: Some(instruction_addr), + ..Default::default() + }, + ); + } + + let blocks = vec![R2ILBlock { + addr: 0x1000, + size: 0x18, + ops: vec![ + R2ILOp::Copy { + dst: make_ram(0x3000, 8), + src: make_const(1, 8), + }, + R2ILOp::CBranch { + target: make_ram(0x1010, 8), + cond: make_const(1, 1), + }, + R2ILOp::Copy { + dst: make_ram(0x3000, 8), + src: make_const(0, 8), + }, + R2ILOp::Branch { + target: make_ram(0x1014, 8), + }, + R2ILOp::Copy { + dst: make_ram(0x3000, 8), + src: make_const(2, 8), + }, + R2ILOp::Return { + target: make_ram(0, 8), + }, + ], + switch_info: None, + op_metadata, + }]; + + let cfg = CFG::from_blocks(&blocks).expect("cfg"); + assert_eq!( + cfg.block_addrs().collect::>(), + vec![0x1000, 0x1008, 0x1010, 0x1014] + ); + assert_eq!( + cfg.get_block(0x1000).expect("entry").terminator, + BlockTerminator::ConditionalBranch { + true_target: 0x1010, + false_target: 0x1008 + } + ); + assert_eq!(cfg.successors(0x1000), vec![0x1010, 0x1008]); + assert_eq!(cfg.successors(0x1008), vec![0x1014]); + assert_eq!(cfg.successors(0x1010), vec![0x1014]); + } } diff --git a/crates/r2ssa/src/function.rs b/crates/r2ssa/src/function.rs index 6277cf1..2bbfc58 100644 --- a/crates/r2ssa/src/function.rs +++ b/crates/r2ssa/src/function.rs @@ -11,6 +11,7 @@ use std::sync::{Arc, OnceLock, RwLock}; use r2il::{ArchSpec, R2ILBlock}; use serde::{Deserialize, Serialize}; +use crate::AssumptionSet; use crate::block::SSABlock as LocalSSABlock; use crate::cfg::{CFG, CFGEdge}; use crate::defuse::{BackwardSlice, SliceOpRef, backward_slice_from_op, backward_slice_from_var}; @@ -156,6 +157,20 @@ impl SsaArtifact { &self.facts } + pub fn with_assumptions(&self, assumptions: &AssumptionSet) -> Self { + let facts = PreparedFunctionFacts::collect_with_assumptions( + &self.function, + &self.graph, + assumptions, + ); + Self { + function: self.function.clone(), + graph: self.graph.clone(), + mode: self.mode, + facts, + } + } + pub fn objects(&self) -> &ObjectModel { &self.facts.objects } @@ -172,6 +187,14 @@ impl SsaArtifact { &self.facts.call_sites } + pub fn resolved_call_target(&self, call: &crate::semantic::CallSiteFact) -> Option { + call.direct_target.or_else(|| { + let value_id = canonical_root_value_id(self, call.target); + let var = self.value_var(value_id)?; + parse_literal_value_name(&var.name) + }) + } + pub fn value_var(&self, value_id: crate::graph::ValueId) -> Option<&SSAVar> { self.graph.value(value_id).map(|value| &value.var) } @@ -225,6 +248,58 @@ impl SsaArtifact { } } +fn canonical_root_value_id( + prepared: &SsaArtifact, + value_id: crate::graph::ValueId, +) -> crate::graph::ValueId { + let Some(facts) = prepared.function().decompile_prep_facts() else { + return value_id; + }; + let Some(start) = prepared.value_var(value_id) else { + return value_id; + }; + let mut current = start.clone(); + let mut current_id = value_id; + for _ in 0..32 { + let Some(next) = facts.canonical_root_of(¤t) else { + break; + }; + if next == ¤t { + break; + } + let Some(next_id) = prepared.graph().value_id_for_var(next) else { + break; + }; + current = next.clone(); + current_id = next_id; + } + current_id +} + +fn parse_literal_value_name(name: &str) -> Option { + let value_str = if let Some(value) = name.strip_prefix("const:") { + value + } else if let Some(value) = name.strip_prefix("ram:") { + value + } else { + return None; + }; + let value_str = value_str.split('_').next().unwrap_or(value_str); + if let Some(dec) = value_str + .strip_prefix("0d") + .or_else(|| value_str.strip_prefix("0D")) + { + return dec.parse().ok(); + } + if let Some(hex) = value_str + .strip_prefix("0x") + .or_else(|| value_str.strip_prefix("0X")) + { + return u64::from_str_radix(hex, 16).ok(); + } + value_str.parse().ok() +} + impl Deref for SsaArtifact { type Target = SSAFunction; @@ -2858,6 +2933,51 @@ mod tests { assert_eq!(call.fallthrough, Some(0x1304)); } + #[test] + fn symbolic_function_ssa_recovers_indirect_call_target_from_copied_ram_literal() { + let tmp = Varnode { + space: SpaceId::Unique, + offset: 0x10, + size: 8, + meta: None, + }; + let blocks = vec![ + R2ILBlock { + addr: 0x1310, + size: 4, + ops: vec![ + R2ILOp::Copy { + dst: tmp.clone(), + src: make_ram(0x1400a6010, 8), + }, + R2ILOp::CallInd { target: tmp }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1314, + size: 4, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + + let prepared = + SsaArtifact::for_symbolic(&blocks, None).expect("symbolic prepared SSA should build"); + let call = prepared + .call_sites() + .by_id + .values() + .next() + .expect("call site fact"); + assert_eq!(call.direct_target, Some(0x1400a6010)); + assert_eq!(call.fallthrough, Some(0x1314)); + } + #[test] fn prepared_function_ssa_builds_memory_phis_per_object() { let blocks = vec![ diff --git a/crates/r2ssa/src/interproc.rs b/crates/r2ssa/src/interproc.rs index 7effea3..6217b70 100644 --- a/crates/r2ssa/src/interproc.rs +++ b/crates/r2ssa/src/interproc.rs @@ -322,6 +322,18 @@ impl AbiProfile { } } + pub fn windows_x64() -> Self { + Self::new( + vec![ + ("rcx", 8, &["ecx", "cx", "cl"][..]), + ("rdx", 8, &["edx", "dx", "dl"][..]), + ("r8", 8, &["r8d", "r8w", "r8b"][..]), + ("r9", 8, &["r9d", "r9w", "r9b"][..]), + ], + &[("rax", &["eax", "ax", "al"][..])], + ) + } + fn new( args: Vec<(&'static str, u32, &'static [&'static str])>, rets: &[(&'static str, &'static [&'static str])], @@ -368,6 +380,13 @@ enum SummaryOperand { Unknown, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum CallArgObservation { + Arg(usize), + Const(u64), + Unknown, +} + #[derive(Debug, Clone, PartialEq, Eq)] enum SummaryValueObservation { Arg(usize), @@ -1277,6 +1296,26 @@ fn collect_call_arg_state( by_call } +pub fn observe_call_arguments( + prepared: &SsaArtifact, + abi: &AbiProfile, +) -> BTreeMap> { + collect_call_arg_state(prepared, abi) + .into_iter() + .map(|(call_id, args)| { + let args = args + .into_iter() + .map(|arg| match arg { + SummaryOperand::Arg(idx) => CallArgObservation::Arg(idx), + SummaryOperand::Const(value) => CallArgObservation::Const(value), + SummaryOperand::Unknown => CallArgObservation::Unknown, + }) + .collect(); + (call_id, args) + }) + .collect() +} + fn merge_pred_states( in_states: &BTreeMap>, preds: &[u64], @@ -1659,6 +1698,18 @@ mod tests { arch } + fn windows_x64_arch() -> ArchSpec { + let mut arch = ArchSpec::new("x86-64"); + arch.addr_size = 8; + arch.add_register(RegisterDef::new("rax", 0, 8)); + arch.add_register(RegisterDef::new("rcx", 8, 8)); + arch.add_register(RegisterDef::new("rdx", 16, 8)); + arch.add_register(RegisterDef::new("r8", 24, 8)); + arch.add_register(RegisterDef::new("r9", 32, 8)); + arch.add_register(RegisterDef::new("rip", 40, 8)); + arch + } + fn reg(offset: u64, size: u32) -> Varnode { Varnode { space: SpaceId::Register, @@ -2178,4 +2229,42 @@ mod tests { block.ops ); } + + #[test] + fn windows_x64_call_arg_observer_tracks_registration_handler_constant() { + let arch = windows_x64_arch(); + let prepared = SsaArtifact::for_symbolic( + &[block( + 0x5000, + vec![ + R2ILOp::Copy { + dst: reg(8, 8), + src: c(1, 8), + }, + R2ILOp::Copy { + dst: reg(16, 8), + src: c(0x1400_3d0f, 8), + }, + R2ILOp::Call { + target: c(0x1800_1000, 8), + }, + R2ILOp::Return { target: c(0, 8) }, + ], + )], + Some(&arch), + ) + .expect("ssa"); + + let observations = observe_call_arguments(&prepared, &AbiProfile::windows_x64()); + let call_id = prepared + .call_sites() + .by_id + .keys() + .next() + .copied() + .expect("callsite"); + let args = observations.get(&call_id).expect("call args"); + assert_eq!(args.first(), Some(&CallArgObservation::Const(1))); + assert_eq!(args.get(1), Some(&CallArgObservation::Const(0x1400_3d0f))); + } } diff --git a/crates/r2ssa/src/lib.rs b/crates/r2ssa/src/lib.rs index c8d8e15..afa923d 100644 --- a/crates/r2ssa/src/lib.rs +++ b/crates/r2ssa/src/lib.rs @@ -16,6 +16,7 @@ //! - [`taint`]: Taint analysis on SSA def-use chains //! - [`var`]: SSA variable representation +pub mod assumption; pub mod block; pub mod cfg; pub mod defuse; @@ -32,6 +33,10 @@ pub mod semantic; pub mod taint; pub mod var; +pub use assumption::{ + AnalysisAssumption, AnalysisAssumptionConflict, AssumptionProvenance, AssumptionScope, + AssumptionSet, AssumptionSubject, AssumptionUsageReport, AssumptionValue, +}; pub use block::SSABlock; pub use cfg::{BasicBlock, BlockTerminator, CFG, CFGEdge}; pub use defuse::{ @@ -46,10 +51,11 @@ pub use graph::{ BlockId, GraphBlock, GraphInst, GraphValue, InstId, InstPayload, SsaGraph, UseSite, ValueId, }; pub use interproc::{ - AbiProfile, FunctionSemanticSummary, InterprocFunctionId, InterprocFunctionInput, - InterprocSolveConfig, InterprocSummaryDiagnostics, InterprocSummarySet, SummaryArgEffect, - SummaryMemoryEffect, SummaryMemoryEffectKind, SummaryMemoryLocation, SummaryMemoryRange, - SummaryMemoryRegion, SummaryReturnRelation, solve_interproc_summary_set, + AbiProfile, CallArgObservation, FunctionSemanticSummary, InterprocFunctionId, + InterprocFunctionInput, InterprocSolveConfig, InterprocSummaryDiagnostics, InterprocSummarySet, + SummaryArgEffect, SummaryMemoryEffect, SummaryMemoryEffectKind, SummaryMemoryLocation, + SummaryMemoryRange, SummaryMemoryRegion, SummaryReturnRelation, observe_call_arguments, + solve_interproc_summary_set, }; pub use op::SSAOp; pub use optimize::{DecompilePrepConfig, OptimizationConfig, OptimizationStats, optimize_function}; @@ -57,7 +63,8 @@ pub use semantic::{ BlockAssumption, CallMemoryEffect, CallSiteFact, CallSiteFacts, CallSiteId, CompareKind, CompareProvenance, GlobalObjectKey, MemoryDefFact, MemoryLocation, MemoryPhiFact, MemorySSAFacts, MemoryUseFact, MemoryVersion, ObjectFact, ObjectId, ObjectKind, ObjectModel, - PredicateFact, PredicateFacts, PredicateId, PreparedFunctionFacts, SwitchPredicateFact, + PredicateFact, PredicateFacts, PredicateId, PreparedAssumptionBinding, + PreparedAssumptionBindingKind, PreparedFunctionFacts, SwitchPredicateFact, }; pub use taint::{DefaultTaintPolicy, TaintAnalysis, TaintLabel, TaintPolicy, TaintResult}; pub use var::SSAVar; diff --git a/crates/r2ssa/src/semantic.rs b/crates/r2ssa/src/semantic.rs index 579fe97..e2ecb3c 100644 --- a/crates/r2ssa/src/semantic.rs +++ b/crates/r2ssa/src/semantic.rs @@ -5,19 +5,22 @@ use std::collections::BTreeMap; +use serde::{Deserialize, Serialize}; + +use crate::assumption::{AssumptionSet, AssumptionSubject, AssumptionUsageReport, AssumptionValue}; use crate::cfg::BlockTerminator; use crate::function::{DecompilePrepFacts, SSAFunction, StackAddressBase, StackAddressRoot}; use crate::graph::{InstId, SsaGraph, ValueId}; use crate::op::SSAOp; use crate::var::SSAVar; -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub struct ObjectId(pub u32); -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub struct PredicateId(pub u32); -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] pub struct CallSiteId(pub u32); #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -185,26 +188,229 @@ pub struct CallSiteFacts { pub by_inst: BTreeMap, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PreparedAssumptionBindingKind { + Predicate { + predicate: PredicateId, + block_addr: u64, + predecessor: Option, + truth: bool, + }, + Register { + name: String, + state_name: String, + symbol_name: String, + bits: u32, + }, + StackSlot { + base: StackAddressBase, + offset: i64, + object: ObjectId, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PreparedAssumptionBinding { + pub assumption: crate::AnalysisAssumption, + pub binding: PreparedAssumptionBindingKind, +} + #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct PreparedFunctionFacts { pub objects: ObjectModel, pub memory: MemorySSAFacts, pub predicates: PredicateFacts, pub call_sites: CallSiteFacts, + pub assumptions: AssumptionSet, + pub applied_assumption_bindings: Vec, + pub assumption_usage: AssumptionUsageReport, } impl PreparedFunctionFacts { pub fn collect(function: &SSAFunction, graph: &SsaGraph) -> Self { - let call_sites = collect_call_sites(function, graph); + Self::collect_with_assumptions(function, graph, &AssumptionSet::default()) + } + + pub fn collect_with_assumptions( + function: &SSAFunction, + graph: &SsaGraph, + assumptions: &AssumptionSet, + ) -> Self { + let call_sites = collect_call_sites(function, graph, function.decompile_prep_facts()); let (objects, memory) = collect_object_and_memory_facts(function, graph, &call_sites); - let predicates = collect_predicate_facts(function, graph); + let predicates = apply_assumptions_to_predicate_facts( + collect_predicate_facts(function, graph), + assumptions, + ); + let (applied_assumption_bindings, assumption_usage) = + collect_prepared_assumption_usage(graph, &objects, &predicates, assumptions); Self { objects, memory, predicates, call_sites, + assumptions: assumptions.clone(), + applied_assumption_bindings, + assumption_usage, + } + } +} + +fn apply_assumptions_to_predicate_facts( + mut predicates: PredicateFacts, + assumptions: &AssumptionSet, +) -> PredicateFacts { + for assumption in assumptions.iter() { + let (predicate_id, block_addr, predecessor, truth) = + match (&assumption.subject, &assumption.value) { + ( + AssumptionSubject::Predicate { + predicate, + block_addr, + predecessor, + }, + AssumptionValue::Branch { truth }, + ) => (*predicate, *block_addr, *predecessor, *truth), + _ => continue, + }; + if !predicates.predicates.contains_key(&predicate_id) { + continue; + } + let entry = predicates.block_assumptions.entry(block_addr).or_default(); + if entry.iter().any(|existing| { + existing.predicate == predicate_id + && existing.predecessor == predecessor.unwrap_or(existing.predecessor) + && existing.truth == truth + }) { + continue; + } + entry.push(BlockAssumption { + predecessor: predecessor.unwrap_or(block_addr), + predicate: predicate_id, + truth, + }); + } + predicates +} + +fn collect_prepared_assumption_usage( + graph: &SsaGraph, + objects: &ObjectModel, + predicates: &PredicateFacts, + assumptions: &AssumptionSet, +) -> (Vec, AssumptionUsageReport) { + let mut bindings = Vec::new(); + let mut usage = AssumptionUsageReport::default(); + + for assumption in assumptions.iter() { + match (&assumption.subject, &assumption.value) { + ( + AssumptionSubject::Predicate { + predicate, + block_addr, + predecessor, + }, + AssumptionValue::Branch { truth }, + ) => { + let Some(fact) = predicates.predicates.get(predicate) else { + usage.mark_ignored(assumption); + continue; + }; + if fact.block_addr != *block_addr { + usage.mark_conflict( + assumption, + format!( + "predicate block mismatch (expected 0x{block_addr:x}, observed 0x{:x})", + fact.block_addr + ), + ); + continue; + } + if let Some(pred) = predecessor { + let expected = if *truth { + fact.true_target + } else { + fact.false_target + }; + if *pred != expected { + usage.mark_conflict( + assumption, + format!( + "branch predecessor 0x{pred:x} does not match selected edge 0x{expected:x}" + ), + ); + continue; + } + } + usage.mark_applied(assumption); + bindings.push(PreparedAssumptionBinding { + assumption: assumption.clone(), + binding: PreparedAssumptionBindingKind::Predicate { + predicate: *predicate, + block_addr: *block_addr, + predecessor: *predecessor, + truth: *truth, + }, + }); + } + (AssumptionSubject::Register { name }, _) => { + let Some(value) = graph.values.iter().find(|value| { + value.var.version == 0 + && value.var.is_register() + && value.var.name.eq_ignore_ascii_case(name) + }) else { + usage.mark_ignored(assumption); + continue; + }; + usage.mark_applied(assumption); + bindings.push(PreparedAssumptionBinding { + assumption: assumption.clone(), + binding: PreparedAssumptionBindingKind::Register { + name: value.var.name.clone(), + state_name: value.var.display_name(), + symbol_name: value + .var + .name + .strip_prefix("reg:") + .unwrap_or(&value.var.name) + .to_ascii_lowercase(), + bits: value.var.size.saturating_mul(8), + }, + }); + } + (AssumptionSubject::StackSlot { base, offset }, _) => { + let Some((root, object)) = + objects.stack_objects.iter().find_map(|(root, object)| { + let matches_base = matches!( + (base.as_str(), root.base), + ("bp", StackAddressBase::FramePointer) + | ("frame", StackAddressBase::FramePointer) + | ("rbp", StackAddressBase::FramePointer) + | ("sp", StackAddressBase::StackPointer) + | ("stack", StackAddressBase::StackPointer) + | ("rsp", StackAddressBase::StackPointer) + ); + (matches_base && root.offset == *offset).then_some((*root, *object)) + }) + else { + usage.mark_ignored(assumption); + continue; + }; + usage.mark_applied(assumption); + bindings.push(PreparedAssumptionBinding { + assumption: assumption.clone(), + binding: PreparedAssumptionBindingKind::StackSlot { + base: root.base, + offset: root.offset, + object, + }, + }); + } + _ => usage.mark_ignored(assumption), } } + + (bindings, usage) } #[derive(Debug, Clone)] @@ -731,7 +937,11 @@ fn collect_predicate_facts(function: &SSAFunction, graph: &SsaGraph) -> Predicat } } -fn collect_call_sites(function: &SSAFunction, graph: &SsaGraph) -> CallSiteFacts { +fn collect_call_sites( + function: &SSAFunction, + graph: &SsaGraph, + prep_facts: Option<&DecompilePrepFacts>, +) -> CallSiteFacts { let mut by_id = BTreeMap::new(); let mut by_inst = BTreeMap::new(); let mut next_id = 0u32; @@ -763,7 +973,7 @@ fn collect_call_sites(function: &SSAFunction, graph: &SsaGraph) -> CallSiteFacts }; let id = CallSiteId(next_id); next_id = next_id.saturating_add(1); - let direct_target = resolve_const_value(None, &target); + let direct_target = resolve_const_value(prep_facts, &target); by_inst.insert(inst_id, id); by_id.insert( id, diff --git a/crates/r2sym/src/constraints.rs b/crates/r2sym/src/constraints.rs new file mode 100644 index 0000000..4af7689 --- /dev/null +++ b/crates/r2sym/src/constraints.rs @@ -0,0 +1,1918 @@ +//! Canonical constraint graph derived from semantic evidence. +//! +//! This module deliberately records what is known and how it was derived. A +//! constraint graph can feed tactics, but verification remains responsible for +//! deciding whether a produced candidate is a proven solve. + +use std::collections::{BTreeMap, HashSet}; + +use r2ssa::SSAOp; +use r2ssa::{AssumptionSubject, AssumptionValue, CompareKind, SSAVar, SsaArtifact}; + +use crate::loops::{ + ExactLoopFoldEvidence, ExactLoopRecurrenceEvidence, LoopFoldOperation, LoopMemoryTermKind, + exact_fold_evidence_from_recurrences, +}; +use crate::path::PathResult; +use crate::solver::SymModel; +use crate::{SymState, SymValue}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FinalConstraintPrecision { + Exact, + ModelConditioned, + Residual, + Unknown, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FinalConstraintSource { + TerminalCompareExact, + ExactRecurrenceAggregateModel, + MemoryWindowAssumption, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RecurrenceAggregateConstraint { + pub recurrence: ExactLoopRecurrenceEvidence, + pub target: u64, + pub bits: u32, + pub source: FinalConstraintSource, + pub precision: FinalConstraintPrecision, + pub reasons: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RecurrenceAggregateRangeConstraint { + pub recurrence: ExactLoopRecurrenceEvidence, + pub min: u64, + pub max: u64, + pub bits: u32, + pub source: FinalConstraintSource, + pub precision: FinalConstraintPrecision, + pub reasons: Vec, +} + +pub type FoldAggregateConstraint = RecurrenceAggregateConstraint; +pub type FoldAggregateRangeConstraint = RecurrenceAggregateRangeConstraint; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct InputByteConstraint { + pub addr: u64, + pub allowed: Vec, + pub precision: FinalConstraintPrecision, + pub source: FinalConstraintSource, + pub reasons: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct InputLengthConstraint { + pub base_addr: u64, + pub len: u32, + pub precision: FinalConstraintPrecision, + pub source: FinalConstraintSource, + pub reasons: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FinalConstraint { + RecurrenceEquals(RecurrenceAggregateConstraint), + RecurrenceRange(RecurrenceAggregateRangeConstraint), +} + +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct FinalConstraintGraph { + pub constraints: Vec, + pub input_byte_constraints: Vec, + pub input_length_constraints: Vec, + pub refusals: Vec, +} + +impl FinalConstraintGraph { + pub fn is_empty(&self) -> bool { + self.constraints.is_empty() + } + + pub fn recurrence_aggregate_constraints( + &self, + ) -> impl Iterator { + self.constraints + .iter() + .filter_map(|constraint| match constraint { + FinalConstraint::RecurrenceEquals(constraint) => Some(constraint), + FinalConstraint::RecurrenceRange(_) => None, + }) + } + + pub fn recurrence_aggregate_range_constraints( + &self, + ) -> impl Iterator { + self.constraints + .iter() + .filter_map(|constraint| match constraint { + FinalConstraint::RecurrenceEquals(_) => None, + FinalConstraint::RecurrenceRange(constraint) => Some(constraint), + }) + } + + pub fn constraints_iter(&self) -> impl Iterator { + self.constraints.iter() + } + + pub fn exact_constraint_count(&self) -> usize { + self.constraints_iter() + .filter(|constraint| { + constraint_precision(constraint) == FinalConstraintPrecision::Exact + }) + .count() + } + + pub fn model_conditioned_constraint_count(&self) -> usize { + self.constraints_iter() + .filter(|constraint| { + constraint_precision(constraint) == FinalConstraintPrecision::ModelConditioned + }) + .count() + } + + pub fn strongest_precision(&self) -> FinalConstraintPrecision { + if self.exact_constraint_count() > 0 { + FinalConstraintPrecision::Exact + } else if self.model_conditioned_constraint_count() > 0 { + FinalConstraintPrecision::ModelConditioned + } else if self.constraints_iter().any(|constraint| { + constraint_precision(constraint) == FinalConstraintPrecision::Residual + }) { + FinalConstraintPrecision::Residual + } else { + FinalConstraintPrecision::Unknown + } + } + + pub fn has_exact_constraints(&self) -> bool { + self.exact_constraint_count() > 0 + } + + pub fn merge(&mut self, other: Self) { + self.constraints.extend(other.constraints); + self.input_byte_constraints + .extend(other.input_byte_constraints); + self.input_length_constraints + .extend(other.input_length_constraints); + for reason in other.refusals { + push_unique(&mut self.refusals, reason); + } + self.constraints + .sort_by(|lhs, rhs| constraint_sort_key(lhs).cmp(&constraint_sort_key(rhs))); + self.input_byte_constraints + .sort_by(|lhs, rhs| lhs.addr.cmp(&rhs.addr)); + self.input_byte_constraints.dedup_by(|lhs, rhs| { + lhs.addr == rhs.addr + && lhs.allowed == rhs.allowed + && lhs.precision == rhs.precision + && lhs.source == rhs.source + }); + self.input_length_constraints + .sort_by(|lhs, rhs| lhs.base_addr.cmp(&rhs.base_addr)); + self.input_length_constraints.dedup_by(|lhs, rhs| { + lhs.base_addr == rhs.base_addr + && lhs.len == rhs.len + && lhs.precision == rhs.precision + && lhs.source == rhs.source + }); + } +} + +fn constraint_precision(constraint: &FinalConstraint) -> FinalConstraintPrecision { + match constraint { + FinalConstraint::RecurrenceEquals(constraint) => constraint.precision, + FinalConstraint::RecurrenceRange(constraint) => constraint.precision, + } +} + +fn constraint_sort_key(constraint: &FinalConstraint) -> (u64, u64, &str, u64, u64, u8) { + match constraint { + FinalConstraint::RecurrenceEquals(constraint) => ( + constraint.recurrence.header, + constraint.recurrence.exit_target, + constraint.recurrence.accumulator.as_str(), + constraint.target, + constraint.target, + 0, + ), + FinalConstraint::RecurrenceRange(constraint) => ( + constraint.recurrence.header, + constraint.recurrence.exit_target, + constraint.recurrence.accumulator.as_str(), + constraint.min, + constraint.max, + 1, + ), + } +} + +pub fn exact_fold_model_bytes<'ctx>( + state: &SymState<'ctx>, + fold: &ExactLoopFoldEvidence, + model: &SymModel<'ctx>, +) -> Option> { + if fold.term.bytes != 1 { + return None; + } + let (Some(base), Some(stride)) = (fold.term.base, fold.term.stride) else { + return None; + }; + let mut bytes = Vec::with_capacity(fold.iterations as usize); + for iteration in 0..fold.iterations { + let offset = iteration.checked_mul(stride)?; + let addr = base.checked_add(offset)?; + let value = state.mem_read(&SymValue::concrete(addr, 64), 1); + bytes.push(model.eval(&value)? as u8); + } + Some(bytes) +} + +pub fn aggregate_exact_fold_bytes(fold: &ExactLoopFoldEvidence, bytes: &[u8]) -> Option { + if fold.term.bytes != 1 || bytes.len() != fold.iterations as usize { + return None; + } + let aggregate = match fold.operation { + LoopFoldOperation::Xor => bytes.iter().fold(0u64, |acc, byte| acc ^ (*byte as u64)), + LoopFoldOperation::Add => { + let sum = bytes + .iter() + .fold(0u128, |acc, byte| acc.saturating_add(*byte as u128)); + if fold.bits >= 64 { + sum as u64 + } else { + (sum % (1u128 << fold.bits.max(1))) as u64 + } + } + }; + Some(mask_to_bits(aggregate, fold.bits)) +} + +pub fn build_model_conditioned_recurrence_constraint_graph<'ctx>( + state: &SymState<'ctx>, + recurrences: &[ExactLoopRecurrenceEvidence], + model: &SymModel<'ctx>, +) -> FinalConstraintGraph { + let mut graph = FinalConstraintGraph::default(); + for recurrence in recurrences { + let Some(fold) = recurrence.as_fold() else { + push_unique( + &mut graph.refusals, + format!( + "non_fold_model_conditioned_recurrence:{}", + recurrence.accumulator + ), + ); + continue; + }; + if fold.term.kind != LoopMemoryTermKind::InputRead { + push_unique( + &mut graph.refusals, + format!("non_input_recurrence_constraint:{}", recurrence.accumulator), + ); + continue; + } + if fold.term.bytes != 1 { + push_unique( + &mut graph.refusals, + format!("unsupported_recurrence_read_width:{}", fold.term.bytes), + ); + continue; + } + let Some(bytes) = exact_fold_model_bytes(state, &fold, model) else { + push_unique( + &mut graph.refusals, + format!( + "recurrence_model_bytes_unavailable:{}", + recurrence.accumulator + ), + ); + continue; + }; + let Some(target) = aggregate_exact_fold_bytes(&fold, &bytes) else { + push_unique( + &mut graph.refusals, + format!( + "recurrence_aggregate_unavailable:{}", + recurrence.accumulator + ), + ); + continue; + }; + graph.constraints.push(FinalConstraint::RecurrenceEquals( + RecurrenceAggregateConstraint { + recurrence: recurrence.clone(), + target, + bits: recurrence.bits, + source: FinalConstraintSource::ExactRecurrenceAggregateModel, + precision: FinalConstraintPrecision::ModelConditioned, + reasons: vec![ + "target derived from selected-path exact recurrence model".to_string(), + ], + }, + )); + } + graph + .constraints + .sort_by(|lhs, rhs| constraint_sort_key(lhs).cmp(&constraint_sort_key(rhs))); + graph +} + +pub fn build_exact_fold_constraint_graph<'ctx>( + state: &SymState<'ctx>, + folds: &[ExactLoopFoldEvidence], + model: &SymModel<'ctx>, +) -> FinalConstraintGraph { + let recurrences = folds + .iter() + .cloned() + .map(ExactLoopRecurrenceEvidence::from) + .collect::>(); + build_model_conditioned_recurrence_constraint_graph(state, &recurrences, model) +} + +pub fn build_final_constraint_graph_for_path<'ctx>( + func: &SsaArtifact, + path: &PathResult<'ctx>, + recurrences: &[ExactLoopRecurrenceEvidence], + target_addr: u64, + model: Option<&SymModel<'ctx>>, +) -> FinalConstraintGraph { + let folds = exact_fold_evidence_from_recurrences(recurrences); + let mut graph = extract_terminal_recurrence_constraints(func, path, recurrences, target_addr); + graph.merge(extract_input_constraints_for_path(func, path, &folds)); + if graph.constraints.is_empty() + && let Some(model) = model + { + graph.merge(build_model_conditioned_recurrence_constraint_graph( + &path.state, + recurrences, + model, + )); + } + graph +} + +fn extract_terminal_recurrence_constraints<'ctx>( + func: &SsaArtifact, + path: &PathResult<'ctx>, + recurrences: &[ExactLoopRecurrenceEvidence], + target_addr: u64, +) -> FinalConstraintGraph { + let mut graph = FinalConstraintGraph::default(); + let def_index = build_ssa_def_index(func); + let Some(predecessor) = path.state.prev_pc() else { + push_unique(&mut graph.refusals, "final_constraint_missing_predecessor"); + return graph; + }; + + let predicates = func + .predicates() + .predicates + .values() + .filter(|predicate| { + predicate.block_addr == predecessor + && (predicate.true_target == target_addr || predicate.false_target == target_addr) + }) + .collect::>(); + + if predicates.is_empty() { + push_unique( + &mut graph.refusals, + format!("final_constraint_missing_terminal_predicate:0x{predecessor:x}"), + ); + return graph; + } + if predicates.len() > 1 { + push_unique( + &mut graph.refusals, + format!("final_constraint_ambiguous_terminal_predicate:0x{predecessor:x}"), + ); + return graph; + } + + let predicate = predicates[0]; + let Some(comparison) = &predicate.comparison else { + push_unique( + &mut graph.refusals, + format!("final_constraint_missing_compare:0x{predecessor:x}"), + ); + return graph; + }; + let branch_truth = predicate.true_target == target_addr; + let Some(lhs) = func.value_var(comparison.lhs) else { + push_unique(&mut graph.refusals, "final_constraint_missing_lhs"); + return graph; + }; + let Some(rhs) = func.value_var(comparison.rhs) else { + push_unique(&mut graph.refusals, "final_constraint_missing_rhs"); + return graph; + }; + + let lhs_const = parse_literal_var(lhs); + let rhs_const = parse_literal_var(rhs); + for recurrence in recurrences { + let linked = if let Some(expr) = + invertible_expr_for_recurrence_operand(func, &def_index, lhs, recurrence) + { + rhs_const.map(|target| TerminalCompareLink { + target, + const_name: rhs.display_name(), + recurrence_is_lhs: true, + expr, + }) + } else if let Some(expr) = + invertible_expr_for_recurrence_operand(func, &def_index, rhs, recurrence) + { + lhs_const.map(|target| TerminalCompareLink { + target, + const_name: lhs.display_name(), + recurrence_is_lhs: false, + expr, + }) + } else { + None + }; + let Some(link) = linked else { + continue; + }; + let Some(constraint) = terminal_constraint_for_compare( + predecessor, + comparison.kind, + branch_truth, + recurrence, + &link, + ) else { + continue; + }; + graph.constraints.push(constraint); + } + + if graph.constraints.is_empty() { + push_unique( + &mut graph.refusals, + format!("final_constraint_compare_not_linked_to_exact_recurrence:0x{predecessor:x}"), + ); + } + graph +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum FoldTransform { + AddConst(u64), + XorConst(u64), + Scale(u64), + RotateLeft(u32), + RotateRight(u32), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct InvertibleFoldExpr { + bits: u32, + transforms: Vec, +} + +#[derive(Debug, Clone)] +struct TerminalCompareLink { + target: u64, + const_name: String, + recurrence_is_lhs: bool, + expr: InvertibleFoldExpr, +} + +struct InvertibleRecurrenceParseContext<'a> { + func: &'a SsaArtifact, + def_index: BTreeMap, + recurrence: &'a ExactLoopRecurrenceEvidence, + visited: HashSet, +} + +fn terminal_constraint_for_compare( + predecessor: u64, + kind: CompareKind, + branch_truth: bool, + recurrence: &ExactLoopRecurrenceEvidence, + link: &TerminalCompareLink, +) -> Option { + let exact_reason = || { + if invertible_expr_is_direct_identity(&link.expr, recurrence.bits, link.target) { + vec![format!( + "exact terminal compare links {} to {} at 0x{predecessor:x}", + recurrence.accumulator, link.const_name + )] + } else { + vec![format!( + "exact terminal compare links {} to {} at 0x{predecessor:x} via {}", + recurrence.accumulator, + link.const_name, + describe_invertible_expr(&link.expr) + )] + } + }; + match (kind, branch_truth) { + (CompareKind::Equal, true) | (CompareKind::NotEqual, false) => Some( + FinalConstraint::RecurrenceEquals(RecurrenceAggregateConstraint { + recurrence: recurrence.clone(), + target: invert_invertible_recurrence_equality( + &link.expr, + link.target, + recurrence.bits, + )?, + bits: recurrence.bits, + source: FinalConstraintSource::TerminalCompareExact, + precision: FinalConstraintPrecision::Exact, + reasons: exact_reason(), + }), + ), + (CompareKind::Less, truth) + if invertible_expr_is_direct_identity(&link.expr, recurrence.bits, link.target) => + { + recurrence_range_constraint( + recurrence, + if link.recurrence_is_lhs { + if truth { + FoldRangeRelation::Le(mask_to_bits( + link.target.wrapping_sub(1), + recurrence.bits, + )) + } else { + FoldRangeRelation::Ge(mask_to_bits(link.target, recurrence.bits)) + } + } else if truth { + FoldRangeRelation::Ge(mask_to_bits( + link.target.saturating_add(1), + recurrence.bits, + )) + } else { + FoldRangeRelation::Le(mask_to_bits(link.target, recurrence.bits)) + }, + exact_reason(), + ) + } + (CompareKind::LessEqual, truth) + if invertible_expr_is_direct_identity(&link.expr, recurrence.bits, link.target) => + { + recurrence_range_constraint( + recurrence, + if link.recurrence_is_lhs { + if truth { + FoldRangeRelation::Le(mask_to_bits(link.target, recurrence.bits)) + } else { + FoldRangeRelation::Ge(mask_to_bits( + link.target.saturating_add(1), + recurrence.bits, + )) + } + } else if truth { + FoldRangeRelation::Ge(mask_to_bits(link.target, recurrence.bits)) + } else { + FoldRangeRelation::Le(mask_to_bits( + link.target.wrapping_sub(1), + recurrence.bits, + )) + }, + exact_reason(), + ) + } + _ => None, + } +} + +fn invertible_expr_for_recurrence_operand( + func: &SsaArtifact, + def_index: &BTreeMap, + var: &SSAVar, + recurrence: &ExactLoopRecurrenceEvidence, +) -> Option { + let bits = var.size.saturating_mul(8).max(1); + let mut ctx = InvertibleRecurrenceParseContext { + func, + def_index: def_index.clone(), + recurrence, + visited: HashSet::new(), + }; + parse_invertible_expr_for_recurrence_operand(&mut ctx, var, bits, 8) +} + +fn parse_invertible_expr_for_recurrence_operand( + ctx: &mut InvertibleRecurrenceParseContext<'_>, + var: &SSAVar, + _bits: u32, + depth: u8, +) -> Option { + if depth == 0 { + return None; + } + if var.display_name() == ctx.recurrence.accumulator { + return Some(InvertibleFoldExpr { + bits: ctx.recurrence.bits.max(1), + transforms: Vec::new(), + }); + } + let name = var.display_name(); + let op = *ctx.def_index.get(&name)?; + if !ctx.visited.insert(name.clone()) { + return None; + } + let result = match op { + SSAOp::Copy { src, dst } | SSAOp::IntZExt { src, dst } => { + parse_invertible_expr_for_recurrence_operand( + ctx, + src, + dst.size.saturating_mul(8).max(1), + depth - 1, + ) + } + SSAOp::IntAdd { dst, a, b } => { + parse_invertible_add_like_expr(ctx, a, b, dst.size, depth - 1) + } + SSAOp::IntSub { dst, a, b } => { + parse_invertible_sub_like_expr(ctx, a, b, dst.size, depth - 1) + } + SSAOp::IntMult { dst, a, b } => { + let bits = dst.size.saturating_mul(8).max(1); + if let Some(scale) = parse_literal_var(a) { + let inner = parse_invertible_expr_for_recurrence_operand(ctx, b, bits, depth - 1)?; + Some(push_fold_transform( + inner, + FoldTransform::Scale(scale), + bits, + )) + } else if let Some(scale) = parse_literal_var(b) { + let inner = parse_invertible_expr_for_recurrence_operand(ctx, a, bits, depth - 1)?; + Some(push_fold_transform( + inner, + FoldTransform::Scale(scale), + bits, + )) + } else { + None + } + } + SSAOp::IntXor { dst, a, b } => { + parse_invertible_xor_like_expr(ctx, a, b, dst.size, depth - 1) + } + SSAOp::IntOr { dst, a, b } => parse_rotate_or_expr(ctx, a, b, dst.size, depth - 1), + _ => None, + } + .or_else(|| { + ctx.func + .function() + .decompile_prep_facts() + .and_then(|facts| facts.canonical_root_of(var)) + .filter(|root| root.display_name() == ctx.recurrence.accumulator) + .map(|_| InvertibleFoldExpr { + bits: ctx.recurrence.bits.max(1), + transforms: Vec::new(), + }) + }); + ctx.visited.remove(&name); + result +} + +fn parse_invertible_add_like_expr( + ctx: &mut InvertibleRecurrenceParseContext<'_>, + a: &SSAVar, + b: &SSAVar, + dst_size: u32, + depth: u8, +) -> Option { + if let Some(constant) = parse_literal_var(a) { + let inner = parse_invertible_expr_for_recurrence_operand( + ctx, + b, + dst_size.saturating_mul(8).max(1), + depth, + )?; + let bits = inner.bits; + return Some(push_fold_transform( + inner, + FoldTransform::AddConst(constant), + bits, + )); + } + if let Some(constant) = parse_literal_var(b) { + let inner = parse_invertible_expr_for_recurrence_operand( + ctx, + a, + dst_size.saturating_mul(8).max(1), + depth, + )?; + let bits = inner.bits; + return Some(push_fold_transform( + inner, + FoldTransform::AddConst(constant), + bits, + )); + } + None +} + +fn parse_invertible_sub_like_expr( + ctx: &mut InvertibleRecurrenceParseContext<'_>, + a: &SSAVar, + b: &SSAVar, + dst_size: u32, + depth: u8, +) -> Option { + if let Some(constant) = parse_literal_var(b) { + let inner = parse_invertible_expr_for_recurrence_operand( + ctx, + a, + dst_size.saturating_mul(8).max(1), + depth, + )?; + let bits = inner.bits; + return Some(push_fold_transform( + inner, + FoldTransform::AddConst(mask_to_bits(constant.wrapping_neg(), bits)), + bits, + )); + } + if let Some(constant) = parse_literal_var(a) { + let inner = parse_invertible_expr_for_recurrence_operand( + ctx, + b, + dst_size.saturating_mul(8).max(1), + depth, + )?; + let bits = inner.bits; + let inner = push_fold_transform( + inner, + FoldTransform::Scale(mask_to_bits(u64::MAX, bits)), + bits, + ); + return Some(push_fold_transform( + inner, + FoldTransform::AddConst(constant), + bits, + )); + } + None +} + +fn parse_invertible_xor_like_expr( + ctx: &mut InvertibleRecurrenceParseContext<'_>, + a: &SSAVar, + b: &SSAVar, + dst_size: u32, + depth: u8, +) -> Option { + if let Some(constant) = parse_literal_var(a) { + let inner = parse_invertible_expr_for_recurrence_operand( + ctx, + b, + dst_size.saturating_mul(8).max(1), + depth, + )?; + let bits = inner.bits; + return Some(push_fold_transform( + inner, + FoldTransform::XorConst(constant), + bits, + )); + } + if let Some(constant) = parse_literal_var(b) { + let inner = parse_invertible_expr_for_recurrence_operand( + ctx, + a, + dst_size.saturating_mul(8).max(1), + depth, + )?; + let bits = inner.bits; + return Some(push_fold_transform( + inner, + FoldTransform::XorConst(constant), + bits, + )); + } + None +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum RotateDirection { + Left, + Right, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct RotateShiftTerm { + inner: InvertibleFoldExpr, + direction: RotateDirection, + shift: u32, + bits: u32, +} + +fn parse_rotate_or_expr( + ctx: &mut InvertibleRecurrenceParseContext<'_>, + a: &SSAVar, + b: &SSAVar, + dst_size: u32, + depth: u8, +) -> Option { + let lhs = parse_rotate_shift_term(ctx, a, dst_size.saturating_mul(8).max(1), depth)?; + let rhs = parse_rotate_shift_term(ctx, b, dst_size.saturating_mul(8).max(1), depth)?; + if lhs.inner != rhs.inner || lhs.bits != rhs.bits { + return None; + } + let bits = lhs.inner.bits; + if lhs.shift == 0 || rhs.shift == 0 || lhs.shift.saturating_add(rhs.shift) != bits { + return None; + } + if lhs.direction == RotateDirection::Left && rhs.direction == RotateDirection::Right { + return Some(push_fold_transform( + lhs.inner, + FoldTransform::RotateLeft(lhs.shift), + bits, + )); + } + if lhs.direction == RotateDirection::Right && rhs.direction == RotateDirection::Left { + return Some(push_fold_transform( + lhs.inner, + FoldTransform::RotateRight(lhs.shift), + bits, + )); + } + None +} + +fn parse_rotate_shift_term( + ctx: &mut InvertibleRecurrenceParseContext<'_>, + var: &SSAVar, + bits: u32, + depth: u8, +) -> Option { + if depth == 0 { + return None; + } + let op = *ctx.def_index.get(&var.display_name())?; + match op { + SSAOp::IntLeft { dst, a, b } => Some(RotateShiftTerm { + inner: parse_invertible_expr_for_recurrence_operand( + ctx, + a, + dst.size.saturating_mul(8).max(1), + depth - 1, + )?, + direction: RotateDirection::Left, + shift: normalize_rotate_amount(parse_literal_var(b)? as u32, bits), + bits: dst.size.saturating_mul(8).max(1), + }), + SSAOp::IntRight { dst, a, b } => Some(RotateShiftTerm { + inner: parse_invertible_expr_for_recurrence_operand( + ctx, + a, + dst.size.saturating_mul(8).max(1), + depth - 1, + )?, + direction: RotateDirection::Right, + shift: normalize_rotate_amount(parse_literal_var(b)? as u32, bits), + bits: dst.size.saturating_mul(8).max(1), + }), + _ => None, + } +} + +fn build_ssa_def_index(func: &SsaArtifact) -> BTreeMap { + let mut index = BTreeMap::new(); + for block in func.function().blocks() { + for op in &block.ops { + if let Some(dst) = op.dst() { + index.insert(dst.display_name(), op); + } + } + } + index +} + +fn push_fold_transform( + mut expr: InvertibleFoldExpr, + transform: FoldTransform, + bits: u32, +) -> InvertibleFoldExpr { + let normalized = match transform { + FoldTransform::AddConst(value) => FoldTransform::AddConst(mask_to_bits(value, bits)), + FoldTransform::XorConst(value) => FoldTransform::XorConst(mask_to_bits(value, bits)), + FoldTransform::Scale(value) => FoldTransform::Scale(mask_to_bits(value, bits)), + FoldTransform::RotateLeft(value) => { + FoldTransform::RotateLeft(normalize_rotate_amount(value, bits)) + } + FoldTransform::RotateRight(value) => { + FoldTransform::RotateRight(normalize_rotate_amount(value, bits)) + } + }; + expr.bits = bits; + expr.transforms.push(normalized); + expr +} + +fn normalize_rotate_amount(amount: u32, bits: u32) -> u32 { + if bits == 0 { 0 } else { amount % bits.min(64) } +} + +fn invertible_expr_is_direct_identity( + expr: &InvertibleFoldExpr, + recurrence_bits: u32, + target: u64, +) -> bool { + expr.transforms.is_empty() + && target <= max_value_for_bits(recurrence_bits) + && expr.bits >= recurrence_bits +} + +fn invert_invertible_recurrence_equality( + expr: &InvertibleFoldExpr, + target: u64, + recurrence_bits: u32, +) -> Option { + let mut solved = mask_to_bits(target, expr.bits); + for transform in expr.transforms.iter().rev() { + solved = match transform { + FoldTransform::AddConst(value) => mask_to_bits(solved.wrapping_sub(*value), expr.bits), + FoldTransform::XorConst(value) => mask_to_bits(solved ^ *value, expr.bits), + FoldTransform::Scale(value) => { + let inverse = modular_inverse_pow2(*value, expr.bits)?; + mask_to_bits(solved.wrapping_mul(inverse), expr.bits) + } + FoldTransform::RotateLeft(amount) => rotate_right_bits(solved, *amount, expr.bits), + FoldTransform::RotateRight(amount) => rotate_left_bits(solved, *amount, expr.bits), + }; + } + (solved <= max_value_for_bits(recurrence_bits)).then_some(mask_to_bits(solved, recurrence_bits)) +} + +fn describe_invertible_expr(expr: &InvertibleFoldExpr) -> String { + if expr.transforms.is_empty() { + return format!("identity(bits={})", expr.bits); + } + let parts = expr + .transforms + .iter() + .map(|transform| match transform { + FoldTransform::AddConst(value) => format!("add_const(0x{value:x})"), + FoldTransform::XorConst(value) => format!("xor_const(0x{value:x})"), + FoldTransform::Scale(value) => format!("scale(0x{value:x})"), + FoldTransform::RotateLeft(amount) => format!("rol({amount})"), + FoldTransform::RotateRight(amount) => format!("ror({amount})"), + }) + .collect::>(); + format!("{}; bits={}", parts.join(" -> "), expr.bits) +} + +fn rotate_left_bits(value: u64, amount: u32, bits: u32) -> u64 { + rotate_bits(value, amount, bits, true) +} + +fn rotate_right_bits(value: u64, amount: u32, bits: u32) -> u64 { + rotate_bits(value, amount, bits, false) +} + +fn rotate_bits(value: u64, amount: u32, bits: u32, left: bool) -> u64 { + let bits = bits.min(64); + if bits == 0 { + return 0; + } + let value = mask_to_bits(value, bits); + let amount = normalize_rotate_amount(amount, bits); + if amount == 0 { + return value; + } + if bits == 64 { + if left { + value.rotate_left(amount) + } else { + value.rotate_right(amount) + } + } else { + let lhs = if left { + value.wrapping_shl(amount) + } else { + value >> amount + }; + let rhs = if left { + value >> (bits - amount) + } else { + value.wrapping_shl(bits - amount) + }; + mask_to_bits(lhs | rhs, bits) + } +} + +fn modular_inverse_pow2(value: u64, bits: u32) -> Option { + if bits == 0 { + return None; + } + if value & 1 == 0 { + return None; + } + let modulus = 1i128.checked_shl(bits.min(64))?; + let value = (mask_to_bits(value, bits)) as i128; + let (gcd, x, _) = extended_gcd_i128(value, modulus); + if gcd != 1 { + return None; + } + let inverse = ((x % modulus) + modulus) % modulus; + Some(mask_to_bits(inverse as u64, bits)) +} + +fn extended_gcd_i128(a: i128, b: i128) -> (i128, i128, i128) { + if b == 0 { + (a, 1, 0) + } else { + let (gcd, x, y) = extended_gcd_i128(b, a.rem_euclid(b)); + (gcd, y, x - (a / b) * y) + } +} + +#[derive(Clone, Copy)] +enum FoldRangeRelation { + Ge(u64), + Le(u64), +} + +fn recurrence_range_constraint( + recurrence: &ExactLoopRecurrenceEvidence, + relation: FoldRangeRelation, + reasons: Vec, +) -> Option { + let max_value = max_value_for_bits(recurrence.bits); + let (min, max) = match relation { + FoldRangeRelation::Ge(min) if min <= max_value => (min, max_value), + FoldRangeRelation::Le(max) => (0, max.min(max_value)), + _ => return None, + }; + if min > max { + return None; + } + Some(FinalConstraint::RecurrenceRange( + RecurrenceAggregateRangeConstraint { + recurrence: recurrence.clone(), + min, + max, + bits: recurrence.bits, + source: FinalConstraintSource::TerminalCompareExact, + precision: FinalConstraintPrecision::Exact, + reasons, + }, + )) +} + +fn extract_input_constraints_for_path<'ctx>( + func: &SsaArtifact, + path: &PathResult<'ctx>, + folds: &[ExactLoopFoldEvidence], +) -> FinalConstraintGraph { + let mut graph = FinalConstraintGraph::default(); + let interesting_ranges = folds + .iter() + .filter_map(|fold| { + let (Some(base), Some(stride)) = (fold.term.base, fold.term.stride) else { + return None; + }; + Some(( + base, + fold.iterations, + stride, + fold.term.region.as_deref(), + fold.term.region_base, + )) + }) + .collect::>(); + + for assumption in func.facts().assumptions.iter() { + let AssumptionSubject::MemoryWindow { addr, size } = assumption.subject else { + continue; + }; + if !interesting_ranges + .iter() + .any(|(base, iterations, stride, region, region_base)| { + memory_window_overlaps_fold( + addr, + size, + *base, + *iterations, + *stride, + *region, + *region_base, + ) + }) + { + continue; + } + let Some(region) = path + .state + .symbolic_memory() + .iter() + .find(|region| memory_window_within_region(addr, size, region.addr, region.size)) + else { + push_unique( + &mut graph.refusals, + format!("input_assumption_missing_region:0x{addr:x}/{size}"), + ); + continue; + }; + graph.merge(memory_window_constraints_from_assumption( + addr, + size, + region.addr, + region.size, + &assumption.value, + )); + } + + for region in path.state.symbolic_memory() { + graph.input_length_constraints.push(InputLengthConstraint { + base_addr: region.addr, + len: region.size, + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: vec![format!( + "symbolic input region length {} @ 0x{:x}", + region.size, region.addr + )], + }); + } + + graph +} + +fn memory_window_constraints_from_assumption( + addr: u64, + size: u32, + region_addr: u64, + region_size: u32, + value: &AssumptionValue, +) -> FinalConstraintGraph { + let mut graph = FinalConstraintGraph::default(); + match value { + AssumptionValue::Constant { value } if size <= 8 => { + for index in 0..size { + let byte = ((value >> (index * 8)) & 0xff) as u8; + graph.input_byte_constraints.push(InputByteConstraint { + addr: addr + index as u64, + allowed: vec![byte], + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: vec![format!( + "memory-window constant assumption at 0x{:x}", + addr + index as u64 + )], + }); + } + } + AssumptionValue::Range { min, max } if size == 1 && *min <= 0xff && *max <= 0xff => { + graph.input_byte_constraints.push(InputByteConstraint { + addr, + allowed: (*min as u8..=*max as u8).collect(), + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: vec![format!("memory-window range assumption at 0x{addr:x}")], + }); + } + AssumptionValue::FiniteSet { values } if size == 1 => { + let allowed = values + .iter() + .copied() + .filter(|value| *value <= 0xff) + .map(|value| value as u8) + .collect::>(); + if !allowed.is_empty() { + graph.input_byte_constraints.push(InputByteConstraint { + addr, + allowed, + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: vec![format!("memory-window finite-set assumption at 0x{addr:x}")], + }); + } + } + AssumptionValue::EnumDomain { values, .. } if size == 1 => { + let allowed = values + .iter() + .copied() + .filter(|value| *value >= 0 && *value <= 0xff) + .map(|value| value as u8) + .collect::>(); + if !allowed.is_empty() { + graph.input_byte_constraints.push(InputByteConstraint { + addr, + allowed, + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: vec![format!("memory-window enum assumption at 0x{addr:x}")], + }); + } + } + _ => { + push_unique( + &mut graph.refusals, + format!("input_assumption_unsupported:0x{addr:x}/{size}"), + ); + } + } + if memory_window_within_region(addr, size, region_addr, region_size) { + graph.input_length_constraints.push(InputLengthConstraint { + base_addr: region_addr, + len: region_size, + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: vec![format!( + "memory-window assumption applies within region @ 0x{region_addr:x}" + )], + }); + } + graph +} + +fn memory_window_overlaps_fold( + addr: u64, + size: u32, + base: u64, + iterations: u64, + stride: u64, + region: Option<&str>, + region_base: Option, +) -> bool { + if stride == 0 { + return false; + } + let Some(end) = addr.checked_add(size as u64) else { + return false; + }; + let Some(fold_end) = base.checked_add(iterations.saturating_mul(stride)) else { + return false; + }; + if addr < fold_end && end > base { + return true; + } + region.is_some() && region_base == Some(base) +} + +fn memory_window_within_region(addr: u64, size: u32, region_addr: u64, region_size: u32) -> bool { + let Some(region_end) = region_addr.checked_add(region_size as u64) else { + return false; + }; + let Some(end) = addr.checked_add(size as u64) else { + return false; + }; + addr >= region_addr && end <= region_end +} + +fn parse_literal_var(var: &SSAVar) -> Option { + parse_literal_value_name(&var.name) +} + +fn parse_literal_value_name(name: &str) -> Option { + let value_str = if let Some(value) = name.strip_prefix("const:") { + value + } else if let Some(value) = name.strip_prefix("ram:") { + value + } else { + return None; + }; + let value_str = value_str.split('_').next().unwrap_or(value_str); + if let Some(dec) = value_str + .strip_prefix("0d") + .or_else(|| value_str.strip_prefix("0D")) + { + return dec.parse().ok(); + } + if let Some(hex) = value_str + .strip_prefix("0x") + .or_else(|| value_str.strip_prefix("0X")) + { + return u64::from_str_radix(hex, 16).ok(); + } + u64::from_str_radix(value_str, 16) + .ok() + .or_else(|| value_str.parse().ok()) +} + +fn mask_to_bits(value: u64, bits: u32) -> u64 { + if bits >= 64 { + value + } else if bits == 0 { + 0 + } else { + value & ((1u64 << bits) - 1) + } +} + +fn max_value_for_bits(bits: u32) -> u64 { + if bits >= 64 { + u64::MAX + } else if bits == 0 { + 0 + } else { + (1u64 << bits) - 1 + } +} + +fn push_unique(reasons: &mut Vec, reason: impl Into) { + let reason = reason.into(); + if !reasons.iter().any(|existing| existing == &reason) { + reasons.push(reason); + } +} + +#[cfg(test)] +mod tests { + use r2il::{ArchSpec, R2ILBlock, R2ILOp, RegisterDef, SpaceId, Varnode}; + use r2ssa::{ + AnalysisAssumption, AssumptionScope, AssumptionSet, AssumptionSubject, AssumptionValue, + SsaArtifact, + }; + use z3::Context; + + use super::{ + FinalConstraint, FinalConstraintSource, InputByteConstraint, aggregate_exact_fold_bytes, + build_exact_fold_constraint_graph, build_final_constraint_graph_for_path, + }; + use crate::{ + ExactLoopFoldEvidence, ExactLoopRecurrenceEvidence, ExactLoopRecurrenceKind, + LoopFoldOperation, LoopMemoryTerm, LoopMemoryTermKind, LoopRotateDirection, PathResult, + SymSolver, SymState, SymValue, + }; + + const RAX: u64 = 0; + const RCX: u64 = 16; + const RDX: u64 = 24; + const RSI: u64 = 32; + + fn make_reg(offset: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Register, + offset, + size, + meta: None, + } + } + + fn make_const(val: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Const, + offset: val, + size, + meta: None, + } + } + + fn make_x86_64_arch() -> ArchSpec { + let mut arch = ArchSpec::new("x86-64"); + arch.addr_size = 8; + arch.add_register(RegisterDef::new("RAX", RAX, 8)); + arch.add_register(RegisterDef::new("RCX", RCX, 8)); + arch.add_register(RegisterDef::new("RDX", RDX, 8)); + arch.add_register(RegisterDef::new("RSI", RSI, 8)); + arch + } + + fn input_fold( + operation: LoopFoldOperation, + bits: u32, + iterations: u64, + ) -> ExactLoopFoldEvidence { + ExactLoopFoldEvidence { + header: 0x1000, + exit_target: 0x2000, + iterations, + accumulator: "ACC_2".to_string(), + bits, + operation, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "PTR_1".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(iterations), + }, + } + } + + #[test] + fn aggregate_exact_fold_bytes_masks_to_width() { + let fold = input_fold(LoopFoldOperation::Add, 8, 3); + assert_eq!(aggregate_exact_fold_bytes(&fold, &[200, 100, 10]), Some(54)); + + let fold = input_fold(LoopFoldOperation::Xor, 8, 3); + assert_eq!( + aggregate_exact_fold_bytes(&fold, &[0xaa, 0x55, 0xff]), + Some(0) + ); + } + + #[test] + fn constraint_graph_derives_input_fold_target_from_selected_model() { + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic_memory(0x7000, 3, "argv1"); + let fold = input_fold(LoopFoldOperation::Add, 16, 3); + for (offset, byte) in [10u64, 20, 30].into_iter().enumerate() { + let value = state.mem_read(&crate::SymValue::concrete(0x7000 + offset as u64, 64), 1); + state.constrain_eq(&value, byte); + } + let solver = SymSolver::new(&ctx); + let model = solver.solve(&state).expect("model"); + let graph = build_exact_fold_constraint_graph(&state, &[fold], &model); + assert!(graph.refusals.is_empty()); + assert_eq!(graph.constraints.len(), 1); + match &graph.constraints[0] { + FinalConstraint::RecurrenceEquals(constraint) => { + assert_eq!(constraint.target, 60); + assert_eq!(constraint.bits, 16); + } + FinalConstraint::RecurrenceRange(_) => panic!("unexpected range constraint"), + } + } + + #[test] + fn final_constraint_graph_extracts_exact_terminal_compare_for_fold_accumulator() { + let ctx = Context::thread_local(); + let arch = make_x86_64_arch(); + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![ + R2ILOp::IntEqual { + dst: make_reg(RCX, 1), + a: make_reg(RAX, 8), + b: make_const(0x55, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(RCX, 1), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, Some(&arch)).expect("function"); + let fold = ExactLoopFoldEvidence { + header: 0x1000, + exit_target: 0x1010, + iterations: 1, + accumulator: "RAX_0".to_string(), + bits: 8, + operation: LoopFoldOperation::Xor, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "RDI_0".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(1), + }, + }; + + let mut state = SymState::new(&ctx, 0x1010); + state.set_prev_pc(Some(0x1000)); + state.set_register("RAX_0", SymValue::concrete(0x55, 64)); + let path = PathResult::new(state, true); + let recurrences = vec![ExactLoopRecurrenceEvidence::from(fold)]; + let graph = build_final_constraint_graph_for_path(&func, &path, &recurrences, 0x1010, None); + assert!(graph.refusals.is_empty(), "{:?}", graph.refusals); + assert_eq!(graph.exact_constraint_count(), 1); + match &graph.constraints[0] { + FinalConstraint::RecurrenceEquals(constraint) => { + assert_eq!(constraint.target, 0x55); + assert_eq!( + constraint.source, + FinalConstraintSource::TerminalCompareExact + ); + assert_eq!(constraint.precision, super::FinalConstraintPrecision::Exact); + } + FinalConstraint::RecurrenceRange(_) => panic!("unexpected range constraint"), + } + } + + #[test] + fn final_constraint_graph_extracts_terminal_range_compare_for_fold_accumulator() { + let ctx = Context::thread_local(); + let arch = make_x86_64_arch(); + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![ + R2ILOp::IntLess { + dst: make_reg(RCX, 1), + a: make_reg(RAX, 8), + b: make_const(0x40, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(RCX, 1), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, Some(&arch)).expect("function"); + let fold = ExactLoopFoldEvidence { + header: 0x1000, + exit_target: 0x1010, + iterations: 1, + accumulator: "RAX_0".to_string(), + bits: 8, + operation: LoopFoldOperation::Xor, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "RDI_0".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(1), + }, + }; + + let mut state = SymState::new(&ctx, 0x1010); + state.set_prev_pc(Some(0x1000)); + let path = PathResult::new(state, true); + let recurrences = vec![ExactLoopRecurrenceEvidence::from(fold)]; + let graph = build_final_constraint_graph_for_path(&func, &path, &recurrences, 0x1010, None); + assert!(graph.refusals.is_empty(), "{:?}", graph.refusals); + assert_eq!(graph.exact_constraint_count(), 1); + match &graph.constraints[0] { + FinalConstraint::RecurrenceRange(constraint) => { + assert_eq!(constraint.min, 0); + assert_eq!(constraint.max, 0x3f); + } + FinalConstraint::RecurrenceEquals(_) => panic!("unexpected equality constraint"), + } + } + + #[test] + fn final_constraint_graph_inverts_affine_terminal_compare_for_fold_accumulator() { + let ctx = Context::thread_local(); + let arch = make_x86_64_arch(); + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![ + R2ILOp::IntMult { + dst: make_reg(RCX, 8), + a: make_reg(RAX, 8), + b: make_const(3, 8), + }, + R2ILOp::IntAdd { + dst: make_reg(RCX, 8), + a: make_reg(RCX, 8), + b: make_const(1, 8), + }, + R2ILOp::IntEqual { + dst: make_reg(RCX, 1), + a: make_reg(RCX, 8), + b: make_const(0x10, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(RCX, 1), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, Some(&arch)).expect("function"); + let fold = ExactLoopFoldEvidence { + header: 0x1000, + exit_target: 0x1010, + iterations: 1, + accumulator: "RAX_0".to_string(), + bits: 8, + operation: LoopFoldOperation::Xor, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "RDI_0".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(1), + }, + }; + + let mut state = SymState::new(&ctx, 0x1010); + state.set_prev_pc(Some(0x1000)); + let path = PathResult::new(state, true); + let recurrences = vec![ExactLoopRecurrenceEvidence::from(fold)]; + let graph = build_final_constraint_graph_for_path(&func, &path, &recurrences, 0x1010, None); + assert!(graph.refusals.is_empty(), "{:?}", graph.refusals); + assert_eq!(graph.exact_constraint_count(), 1); + match &graph.constraints[0] { + FinalConstraint::RecurrenceEquals(constraint) => { + assert_eq!(constraint.target, 5); + assert_eq!( + constraint.source, + FinalConstraintSource::TerminalCompareExact + ); + assert_eq!(constraint.precision, super::FinalConstraintPrecision::Exact); + } + FinalConstraint::RecurrenceRange(_) => panic!("unexpected range constraint"), + } + } + + #[test] + fn final_constraint_graph_inverts_rotate_xor_terminal_compare_for_fold_accumulator() { + let ctx = Context::thread_local(); + let arch = make_x86_64_arch(); + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 6, + ops: vec![ + R2ILOp::IntLeft { + dst: make_reg(RDX, 8), + a: make_reg(RAX, 8), + b: make_const(3, 8), + }, + R2ILOp::IntRight { + dst: make_reg(RSI, 8), + a: make_reg(RAX, 8), + b: make_const(5, 8), + }, + R2ILOp::IntOr { + dst: make_reg(RCX, 8), + a: make_reg(RDX, 8), + b: make_reg(RSI, 8), + }, + R2ILOp::IntXor { + dst: make_reg(RCX, 8), + a: make_reg(RCX, 8), + b: make_const(0x55, 8), + }, + R2ILOp::IntEqual { + dst: make_reg(RDX, 1), + a: make_reg(RCX, 8), + b: make_const(0x78, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(RDX, 1), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, Some(&arch)).expect("function"); + let fold = ExactLoopFoldEvidence { + header: 0x1000, + exit_target: 0x1010, + iterations: 1, + accumulator: "RAX_0".to_string(), + bits: 8, + operation: LoopFoldOperation::Xor, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "RDI_0".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(1), + }, + }; + + let mut state = SymState::new(&ctx, 0x1010); + state.set_prev_pc(Some(0x1000)); + let path = PathResult::new(state, true); + let recurrences = vec![ExactLoopRecurrenceEvidence::from(fold)]; + let graph = build_final_constraint_graph_for_path(&func, &path, &recurrences, 0x1010, None); + assert!(graph.refusals.is_empty(), "{:?}", graph.refusals); + assert_eq!(graph.exact_constraint_count(), 1); + match &graph.constraints[0] { + FinalConstraint::RecurrenceEquals(constraint) => { + assert_eq!(constraint.target, 0xa5); + assert_eq!(constraint.precision, super::FinalConstraintPrecision::Exact); + } + FinalConstraint::RecurrenceRange(_) => panic!("unexpected range constraint"), + } + } + + #[test] + fn final_constraint_graph_links_direct_terminal_compare_to_exact_rotate_recurrence() { + let ctx = Context::thread_local(); + let arch = make_x86_64_arch(); + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 2, + ops: vec![ + R2ILOp::IntEqual { + dst: make_reg(RCX, 1), + a: make_reg(RAX, 8), + b: make_const(0x42, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(RCX, 1), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, Some(&arch)).expect("function"); + let recurrence = ExactLoopRecurrenceEvidence { + header: 0x1000, + exit_target: 0x1010, + iterations: 4, + accumulator: "RAX_0".to_string(), + initial: "RAX_1".to_string(), + bits: 8, + kind: ExactLoopRecurrenceKind::RotateMix { + direction: LoopRotateDirection::Left, + amount: 3, + operation: LoopFoldOperation::Xor, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "RDI_0".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(4), + }, + }, + }; + + let mut state = SymState::new(&ctx, 0x1010); + state.set_prev_pc(Some(0x1000)); + let path = PathResult::new(state, true); + let graph = build_final_constraint_graph_for_path( + &func, + &path, + std::slice::from_ref(&recurrence), + 0x1010, + None, + ); + assert!(graph.refusals.is_empty(), "{:?}", graph.refusals); + assert_eq!(graph.exact_constraint_count(), 1); + match &graph.constraints[0] { + FinalConstraint::RecurrenceEquals(constraint) => { + assert_eq!(constraint.target, 0x42); + assert_eq!(constraint.recurrence, recurrence); + assert_eq!(constraint.precision, super::FinalConstraintPrecision::Exact); + } + FinalConstraint::RecurrenceRange(_) => panic!("unexpected range constraint"), + } + } + + #[test] + fn final_constraint_graph_extracts_memory_window_byte_constraints() { + let ctx = Context::thread_local(); + let blocks = vec![R2ILBlock { + addr: 0x1000, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }]; + let func = SsaArtifact::for_symbolic(&blocks, None) + .expect("function") + .with_assumptions(&AssumptionSet::new(vec![AnalysisAssumption { + id: Some("argv-byte".to_string()), + subject: AssumptionSubject::MemoryWindow { + addr: 0x7001, + size: 1, + }, + value: AssumptionValue::FiniteSet { + values: vec![b'A' as u64, b'B' as u64], + }, + scope: AssumptionScope::Query, + provenance: Default::default(), + }])); + let fold = input_fold(LoopFoldOperation::Xor, 8, 3); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic_memory(0x7000, 3, "argv1"); + let path = PathResult::new(state, true); + let recurrences = vec![ExactLoopRecurrenceEvidence::from(fold)]; + let graph = build_final_constraint_graph_for_path(&func, &path, &recurrences, 0x1000, None); + assert!(graph.constraints.is_empty()); + assert!( + graph + .input_length_constraints + .iter() + .any(|constraint| constraint.base_addr == 0x7000 && constraint.len == 3) + ); + assert!( + graph + .input_byte_constraints + .iter() + .any(|constraint: &InputByteConstraint| { + constraint.addr == 0x7001 && constraint.allowed == vec![b'A', b'B'] + }) + ); + } +} diff --git a/crates/r2sym/src/executor.rs b/crates/r2sym/src/executor.rs index 145b6c3..f134cde 100644 --- a/crates/r2sym/src/executor.rs +++ b/crates/r2sym/src/executor.rs @@ -4,16 +4,18 @@ //! stepping through SSA operations and updating state. use std::borrow::Cow; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use r2ssa::{FunctionSSABlock, SSAOp, SSAVar}; use z3::Context; use z3::ast::BV; use crate::SymResult; -use crate::state::{ExitStatus, SymState}; +use crate::state::{ExitStatus, RuntimeValueProvenance, SymState}; use crate::value::SymValue; +const X86_USEROP_RDTSC: u32 = 74; + /// Result of a call hook. #[derive(Debug, Clone, PartialEq, Eq)] pub enum CallHookResult { @@ -25,15 +27,72 @@ pub enum CallHookResult { Terminate(ExitStatus), } +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum CallHookTag { + WindowsAddVectoredExceptionHandler, + WindowsRaiseException, + WindowsVirtualAlloc, + WindowsVirtualProtect, + WindowsHeapAlloc, +} + /// A call hook for intercepting direct calls. pub type CallHook<'ctx> = Box) -> SymResult + 'ctx>; +fn block_execution_should_stop(op: &SSAOp, pc_before_op: u64, pc_after_op: u64) -> bool { + match op { + SSAOp::Branch { .. } | SSAOp::BranchInd { .. } | SSAOp::Return { .. } => true, + SSAOp::CBranch { .. } | SSAOp::Call { .. } | SSAOp::CallInd { .. } => { + pc_after_op != pc_before_op + } + _ => false, + } +} + +fn zero_equality_from_known_facts<'ctx>( + state: &SymState<'ctx>, + lhs: &SymValue<'ctx>, + rhs: &SymValue<'ctx>, +) -> Option { + if rhs.as_concrete() == Some(0) { + if state.value_known_zero(lhs) { + return Some(true); + } + if state.value_known_nonzero(lhs) { + return Some(false); + } + } + if lhs.as_concrete() == Some(0) { + if state.value_known_zero(rhs) { + return Some(true); + } + if state.value_known_nonzero(rhs) { + return Some(false); + } + } + None +} + +struct RegisteredCallHook<'ctx> { + tag: Option, + hook: CallHook<'ctx>, +} + /// Symbolic executor for SSA functions. pub struct SymExecutor<'ctx> { /// The Z3 context. ctx: &'ctx Context, /// Registered call hooks (address -> handler). - call_hooks: HashMap>, + call_hooks: HashMap>, + /// Direct-call targets that should be forked as interprocedural successors. + direct_call_fork_targets: Option>, +} + +struct ConcreteCopyLoopPlan<'a> { + counter_phi: &'a SSAVar, + src_base: u64, + dst_base: u64, + limit: u64, } impl<'ctx> SymExecutor<'ctx> { @@ -42,15 +101,47 @@ impl<'ctx> SymExecutor<'ctx> { Self { ctx, call_hooks: HashMap::new(), + direct_call_fork_targets: None, } } + /// Replace the direct-call fork target whitelist and return the previous value. + pub fn replace_direct_call_fork_targets( + &mut self, + targets: Option>, + ) -> Option> { + std::mem::replace(&mut self.direct_call_fork_targets, targets) + } + /// Register a call hook for a target address. pub fn register_call_hook(&mut self, addr: u64, hook: F) where F: Fn(&mut SymState<'ctx>) -> SymResult + 'ctx, { - self.call_hooks.insert(addr, Box::new(hook)); + self.call_hooks.insert( + addr, + RegisteredCallHook { + tag: None, + hook: Box::new(hook), + }, + ); + } + + pub fn register_tagged_call_hook(&mut self, addr: u64, tag: CallHookTag, hook: F) + where + F: Fn(&mut SymState<'ctx>) -> SymResult + 'ctx, + { + self.call_hooks.insert( + addr, + RegisteredCallHook { + tag: Some(tag), + hook: Box::new(hook), + }, + ); + } + + pub fn call_hook_tag(&self, addr: u64) -> Option { + self.call_hooks.get(&addr).and_then(|binding| binding.tag) } /// Execute a single SSA operation on the given state. @@ -64,6 +155,7 @@ impl<'ctx> SymExecutor<'ctx> { Copy { dst, src } => { let value = self.read_var(state, src); self.write_var(state, dst, value); + self.propagate_var_provenance(state, dst, self.read_var_provenance(state, src)); Ok(vec![]) } @@ -86,6 +178,10 @@ impl<'ctx> SymExecutor<'ctx> { let size = dst.size; let value = state.mem_read(&addr_val, size); self.write_var(state, dst, value); + let provenance = addr_val + .as_concrete() + .map(|source_addr| RuntimeValueProvenance { source_addr, size }); + self.propagate_var_provenance(state, dst, provenance); Ok(vec![]) } @@ -98,6 +194,10 @@ impl<'ctx> SymExecutor<'ctx> { let value = self.read_var(state, val); let size = val.size; state.mem_write(&addr_val, &value, size); + if let Some(store_addr) = addr_val.as_concrete() { + let provenance = self.read_var_provenance(state, val); + state.note_runtime_store_copy(store_addr, size, provenance.as_ref()); + } Ok(vec![]) } Fence { .. } => Ok(vec![]), @@ -431,7 +531,12 @@ impl<'ctx> SymExecutor<'ctx> { IntEqual { dst, a, b } => { let a_val = self.read_var(state, a); let b_val = self.read_var(state, b); - let result = a_val.eq(self.ctx, &b_val); + let result = + if let Some(equal) = zero_equality_from_known_facts(state, &a_val, &b_val) { + SymValue::concrete(if equal { 1 } else { 0 }, 1) + } else { + a_val.eq(self.ctx, &b_val) + }; self.write_var(state, dst, result); Ok(vec![]) } @@ -439,7 +544,12 @@ impl<'ctx> SymExecutor<'ctx> { IntNotEqual { dst, a, b } => { let a_val = self.read_var(state, a); let b_val = self.read_var(state, b); - let eq = a_val.eq(self.ctx, &b_val); + let eq = if let Some(equal) = zero_equality_from_known_facts(state, &a_val, &b_val) + { + SymValue::concrete(if equal { 1 } else { 0 }, 1) + } else { + a_val.eq(self.ctx, &b_val) + }; // NOT of equality let result = match eq.as_concrete() { Some(v) => SymValue::concrete(if v == 0 { 1 } else { 0 }, 1), @@ -492,6 +602,7 @@ impl<'ctx> SymExecutor<'ctx> { let val = self.read_var(state, src); let result = val.zero_extend(self.ctx, dst.size * 8); self.write_var(state, dst, result); + self.propagate_var_provenance(state, dst, self.read_var_provenance(state, src)); Ok(vec![]) } @@ -499,6 +610,7 @@ impl<'ctx> SymExecutor<'ctx> { let val = self.read_var(state, src); let result = val.sign_extend(self.ctx, dst.size * 8); self.write_var(state, dst, result); + self.propagate_var_provenance(state, dst, self.read_var_provenance(state, src)); Ok(vec![]) } @@ -565,6 +677,12 @@ impl<'ctx> SymExecutor<'ctx> { } }; self.write_var(state, dst, result); + let provenance = if *offset == 0 { + self.read_var_provenance(state, src) + } else { + None + }; + self.propagate_var_provenance(state, dst, provenance); Ok(vec![]) } @@ -609,7 +727,7 @@ impl<'ctx> SymExecutor<'ctx> { // ==================== Control Flow ==================== Branch { target } => { - let target_val = self.read_var(state, target); + let target_val = self.read_control_target_var(state, target); if let Some(addr) = target_val.as_concrete() { state.pc = addr; } @@ -617,7 +735,7 @@ impl<'ctx> SymExecutor<'ctx> { } CBranch { target, cond } => { - let target_val = self.read_var(state, target); + let target_val = self.read_control_target_var(state, target); let cond_val = self.read_var(state, cond); // Check if condition is concrete @@ -630,6 +748,13 @@ impl<'ctx> SymExecutor<'ctx> { } // If c == 0, fall through (don't change PC) Ok(vec![]) + } else if state.value_known_nonzero(&cond_val) { + if let Some(addr) = target_val.as_concrete() { + state.pc = addr; + } + Ok(vec![]) + } else if state.value_known_zero(&cond_val) { + Ok(vec![]) } else { // Symbolic condition - fork execution let target_addr = target_val.as_concrete(); @@ -649,7 +774,7 @@ impl<'ctx> SymExecutor<'ctx> { } BranchInd { target } => { - let target_val = self.read_var(state, target); + let target_val = self.read_control_target_var(state, target); if let Some(addr) = target_val.as_concrete() { state.pc = addr; } else { @@ -660,24 +785,39 @@ impl<'ctx> SymExecutor<'ctx> { } Call { target } => { - let target_val = self.read_var(state, target); - if let Some(addr) = target_val.as_concrete() - && let Some(hook) = self.call_hooks.get(&addr) - { - match hook(state)? { - CallHookResult::Fallthrough => {} - CallHookResult::Jump(new_pc) => state.pc = new_pc, - CallHookResult::Terminate(status) => state.terminate(status), + let target_val = self.read_control_target_var(state, target); + if let Some(addr) = target_val.as_concrete() { + if let Some(binding) = self.call_hooks.get(&addr) { + match (binding.hook)(state)? { + CallHookResult::Fallthrough => {} + CallHookResult::Jump(new_pc) => state.pc = new_pc, + CallHookResult::Terminate(status) => state.terminate(status), + } + } else if self + .direct_call_fork_targets + .as_ref() + .is_some_and(|targets| targets.contains(&addr)) + { + let mut call_state = state.fork(); + call_state.pc = addr; + return Ok(vec![call_state]); } } Ok(vec![]) } CallInd { target } => { - let target_val = self.read_var(state, target); + let target_val = self.read_control_target_var(state, target); if let Some(addr) = target_val.as_concrete() { - if let Some(hook) = self.call_hooks.get(&addr) { - match hook(state)? { + let provenance_addr = self + .read_var_provenance(state, target) + .map(|provenance| provenance.source_addr); + if let Some(binding) = self + .call_hooks + .get(&addr) + .or_else(|| provenance_addr.and_then(|slot| self.call_hooks.get(&slot))) + { + match (binding.hook)(state)? { CallHookResult::Fallthrough => {} CallHookResult::Jump(new_pc) => state.pc = new_pc, CallHookResult::Terminate(status) => state.terminate(status), @@ -692,7 +832,11 @@ impl<'ctx> SymExecutor<'ctx> { } Return { target: _ } => { - state.terminate(ExitStatus::Return); + match state.resume_pending_exception_continuation() { + Ok(Some(resume_pc)) => state.pc = resume_pc, + Ok(None) => state.terminate(ExitStatus::Return), + Err(reason) => state.terminate(ExitStatus::RuntimeBlocked(reason)), + } Ok(vec![]) } @@ -729,9 +873,18 @@ impl<'ctx> SymExecutor<'ctx> { CallOther { output, - userop: _, + userop, inputs: _, } => { + // x86 RDTSC is a timing source, not program data. Model it as a + // stable low-delta value so timing anti-debug checks do not dominate + // exception-continuation analysis. + if *userop == X86_USEROP_RDTSC + && let Some(dst) = output + { + self.write_var(state, dst, SymValue::concrete(0, dst.size * 8)); + return Ok(vec![]); + } // User-defined operation - return symbolic result if let Some(dst) = output { let result = SymValue::new_symbolic(self.ctx, "callother", dst.size * 8); @@ -799,6 +952,7 @@ impl<'ctx> SymExecutor<'ctx> { val }; self.write_var(state, dst, result); + self.propagate_var_provenance(state, dst, self.read_var_provenance(state, src)); Ok(vec![]) } @@ -881,9 +1035,8 @@ impl<'ctx> SymExecutor<'ctx> { } SymValue::concrete(0, var.size * 8) } else if let Some(hex) = var.name.strip_prefix("ram:") { - // Treat RAM addresses as concrete branch targets. if let Ok(value) = u64::from_str_radix(hex, 16) { - SymValue::concrete(value, var.size * 8) + state.mem_read(&SymValue::concrete(value, 64), var.size) } else { SymValue::concrete(0, var.size * 8) } @@ -899,10 +1052,61 @@ impl<'ctx> SymExecutor<'ctx> { } } + fn read_control_target_var(&self, state: &SymState<'ctx>, var: &SSAVar) -> SymValue<'ctx> { + if let Some(addr) = parse_address_literal(var) { + SymValue::concrete(addr, var.size * 8) + } else { + self.read_var(state, var) + } + } + /// Write an SSA variable to state. fn write_var(&self, state: &mut SymState<'ctx>, var: &SSAVar, value: SymValue<'ctx>) { + if let Some(addr) = parse_ram_addr(var) { + state.mem_write(&SymValue::concrete(addr, 64), &value, var.size); + return; + } + let key = var.display_name(); + state.set_register(&key, value.clone()); + state.set_value_provenance(&key, None); + if let Some((base, version)) = split_versioned_register(&key) + && let Some(spec) = x86_register_alias_spec(base) + && spec.offset_bits == 0 + && spec.width_bits == 32 + && spec.family.as_ref() != base + { + let family_key = format!("{}_{}", spec.family, version); + state.set_register(&family_key, value.zero_extend(self.ctx, 64)); + state.set_value_provenance(&family_key, None); + } + } + + fn propagate_var_provenance( + &self, + state: &mut SymState<'ctx>, + var: &SSAVar, + provenance: Option, + ) { + let key = var.display_name(); + state.set_value_provenance(&key, provenance); + } + + fn read_var_provenance( + &self, + state: &SymState<'ctx>, + var: &SSAVar, + ) -> Option { + if let Some(source_addr) = parse_ram_addr(var) { + return Some(RuntimeValueProvenance { + source_addr, + size: var.size, + }); + } let key = var.display_name(); - state.set_register(&key, value); + state + .value_provenance(&key) + .cloned() + .or_else(|| resolve_alias_value_provenance(state, &key)) } /// Execute a block of SSA operations. @@ -928,19 +1132,368 @@ impl<'ctx> SymExecutor<'ctx> { } } + if self + .apply_concrete_copy_loop_runahead(state, block) + .is_some() + { + // The runahead leaves the state poised for the final non-taken + // iteration so the normal executor can materialize live-out defs. + } + // Execute operations - for op in &block.ops { + for (op_idx, op) in block.ops.iter().enumerate() { if !state.active { break; } + if self.try_merge_local_fallthrough_cbranch(state, block, op_idx, op)? { + break; + } + + let pc_before_op = state.pc; let new_states = self.step(state, op)?; forked_states.extend(new_states); state.step(); + if block_execution_should_stop(op, pc_before_op, state.pc) { + break; + } } Ok(forked_states) } + + fn try_merge_local_fallthrough_cbranch( + &self, + state: &mut SymState<'ctx>, + block: &FunctionSSABlock, + op_idx: usize, + op: &SSAOp, + ) -> SymResult { + let SSAOp::CBranch { target, cond } = op else { + return Ok(false); + }; + let Some(target_addr) = parse_address_literal(target) else { + return Ok(false); + }; + let block_end = block.addr.saturating_add(block.size as u64); + if target_addr != block_end { + return Ok(false); + } + let tail = &block.ops[op_idx.saturating_add(1)..]; + if tail.is_empty() + || !tail + .iter() + .all(|tail_op| matches!(tail_op, SSAOp::Copy { .. })) + { + return Ok(false); + } + + let cond_val = self.read_var(state, cond); + if cond_val.as_concrete().is_some() + || state.value_known_nonzero(&cond_val) + || state.value_known_zero(&cond_val) + { + return Ok(false); + } + + let mut skipped = state.fork(); + skipped.add_true_constraint(&cond_val); + skipped.pc = target_addr; + skipped.step(); + for tail_op in tail { + if let SSAOp::Copy { dst, .. } = tail_op { + let preserved = self.read_var(state, dst); + self.write_var(&mut skipped, dst, preserved); + } + } + + state.add_false_constraint(&cond_val); + state.step(); + for tail_op in tail { + let forked = self.step(state, tail_op)?; + debug_assert!(forked.is_empty()); + state.step(); + } + state.pc = target_addr; + + let merged = state.merge_with(&skipped); + *state = merged; + Ok(true) + } + + fn apply_concrete_copy_loop_runahead( + &self, + state: &mut SymState<'ctx>, + block: &FunctionSSABlock, + ) -> Option<()> { + let plan = detect_concrete_copy_loop_plan(self, state, block)?; + let counter = self + .read_var(state, plan.counter_phi) + .as_concrete() + .filter(|counter| *counter < plan.limit)?; + let bulk_count = plan.limit.saturating_sub(counter).saturating_sub(1); + if bulk_count == 0 { + return None; + } + + let start_src = plan.src_base.checked_add(counter)?; + let start_dst = plan.dst_base.checked_add(counter)?; + for offset in 0..bulk_count { + let src_addr = start_src.checked_add(offset)?; + let dst_addr = start_dst.checked_add(offset)?; + let byte = state.mem_read(&SymValue::concrete(src_addr, 64), 1); + state.mem_write(&SymValue::concrete(dst_addr, 64), &byte, 1); + } + + let bulk_size = u32::try_from(bulk_count).ok()?; + state.note_runtime_store_copy( + start_dst, + bulk_size, + Some(&RuntimeValueProvenance { + source_addr: start_src, + size: bulk_size, + }), + ); + state.set_concrete( + &plan.counter_phi.display_name(), + plan.limit.saturating_sub(1), + plan.counter_phi.size.saturating_mul(8), + ); + state.step_by(block.ops.len().saturating_mul(bulk_count as usize)); + Some(()) + } +} + +fn detect_concrete_copy_loop_plan<'ctx, 'a>( + executor: &SymExecutor<'ctx>, + state: &SymState<'ctx>, + block: &'a FunctionSSABlock, +) -> Option> { + let SSAOp::CBranch { target, cond } = block.ops.last()? else { + return None; + }; + let target_addr = parse_var_addr(target)?; + if target_addr != block.addr { + return None; + } + + let (counter_next, limit) = find_loop_limit(block, cond)?; + let counter_phi = find_loop_counter_phi(block, counter_next)?; + let src_base = find_load_base(block, counter_phi) + .and_then(|base| resolve_var_concrete(executor, state, base, block))?; + let dst_base = find_store_base(block, counter_phi) + .and_then(|base| resolve_var_concrete(executor, state, base, block))?; + Some(ConcreteCopyLoopPlan { + counter_phi, + src_base, + dst_base, + limit, + }) +} + +fn find_loop_limit<'a>(block: &'a FunctionSSABlock, cond: &SSAVar) -> Option<(&'a SSAVar, u64)> { + let op = find_def(block, cond)?; + match op { + SSAOp::IntLess { a, b, .. } | SSAOp::IntLessEqual { a, b, .. } => { + let limit = parse_const_u64(b)?; + let counter_next = resolve_passthrough_var(a, block); + Some((counter_next, limit)) + } + _ => None, + } +} + +fn find_loop_counter_phi<'a>( + block: &'a FunctionSSABlock, + counter_next: &'a SSAVar, +) -> Option<&'a SSAVar> { + block.phis.iter().find_map(|phi| { + phi.sources + .iter() + .any(|(pred, src)| *pred == block.addr && var_equivalent(src, counter_next, block)) + .then_some(&phi.dst) + }) +} + +fn find_load_base<'a>(block: &'a FunctionSSABlock, counter_phi: &SSAVar) -> Option<&'a SSAVar> { + block.ops.iter().find_map(|op| match op { + SSAOp::Load { dst, addr, .. } if dst.size == 1 => { + match_base_plus_counter(addr, counter_phi, block) + } + _ => None, + }) +} + +fn find_store_base<'a>(block: &'a FunctionSSABlock, counter_phi: &SSAVar) -> Option<&'a SSAVar> { + block.ops.iter().find_map(|op| match op { + SSAOp::Store { addr, val, .. } if val.size == 1 => { + match_base_plus_counter(addr, counter_phi, block) + } + _ => None, + }) +} + +fn find_def<'a>(block: &'a FunctionSSABlock, dst: &SSAVar) -> Option<&'a SSAOp> { + block + .ops + .iter() + .find(|op| op_def(op).is_some_and(|candidate| candidate == dst)) +} + +fn op_def(op: &SSAOp) -> Option<&SSAVar> { + match op { + SSAOp::Phi { dst, .. } + | SSAOp::Copy { dst, .. } + | SSAOp::Load { dst, .. } + | SSAOp::LoadLinked { dst, .. } + | SSAOp::AtomicCAS { dst, .. } + | SSAOp::LoadGuarded { dst, .. } + | SSAOp::IntAdd { dst, .. } + | SSAOp::IntSub { dst, .. } + | SSAOp::IntMult { dst, .. } + | SSAOp::IntDiv { dst, .. } + | SSAOp::IntSDiv { dst, .. } + | SSAOp::IntRem { dst, .. } + | SSAOp::IntSRem { dst, .. } + | SSAOp::IntNegate { dst, .. } + | SSAOp::IntCarry { dst, .. } + | SSAOp::IntSCarry { dst, .. } + | SSAOp::IntSBorrow { dst, .. } + | SSAOp::IntAnd { dst, .. } + | SSAOp::IntOr { dst, .. } + | SSAOp::IntXor { dst, .. } + | SSAOp::IntNot { dst, .. } + | SSAOp::IntLeft { dst, .. } + | SSAOp::IntRight { dst, .. } + | SSAOp::IntSRight { dst, .. } + | SSAOp::IntEqual { dst, .. } + | SSAOp::IntNotEqual { dst, .. } + | SSAOp::IntLess { dst, .. } + | SSAOp::IntSLess { dst, .. } + | SSAOp::IntLessEqual { dst, .. } + | SSAOp::IntSLessEqual { dst, .. } + | SSAOp::IntZExt { dst, .. } + | SSAOp::IntSExt { dst, .. } + | SSAOp::BoolNot { dst, .. } + | SSAOp::BoolAnd { dst, .. } + | SSAOp::BoolOr { dst, .. } + | SSAOp::BoolXor { dst, .. } + | SSAOp::Piece { dst, .. } + | SSAOp::Subpiece { dst, .. } + | SSAOp::PopCount { dst, .. } + | SSAOp::Lzcount { dst, .. } + | SSAOp::CallDefine { dst } + | SSAOp::FloatAdd { dst, .. } + | SSAOp::FloatSub { dst, .. } + | SSAOp::FloatMult { dst, .. } + | SSAOp::FloatDiv { dst, .. } + | SSAOp::FloatNeg { dst, .. } + | SSAOp::FloatAbs { dst, .. } + | SSAOp::FloatSqrt { dst, .. } + | SSAOp::FloatCeil { dst, .. } + | SSAOp::FloatFloor { dst, .. } + | SSAOp::FloatRound { dst, .. } + | SSAOp::FloatNaN { dst, .. } => Some(dst), + SSAOp::StoreConditional { + result: Some(dst), .. + } => Some(dst), + _ => None, + } +} + +fn resolve_passthrough_var<'a>(var: &'a SSAVar, block: &'a FunctionSSABlock) -> &'a SSAVar { + match find_def(block, var) { + Some(SSAOp::Copy { src, .. }) + | Some(SSAOp::IntZExt { src, .. }) + | Some(SSAOp::IntSExt { src, .. }) => resolve_passthrough_var(src, block), + _ => var, + } +} + +fn var_equivalent(var: &SSAVar, target: &SSAVar, block: &FunctionSSABlock) -> bool { + if var == target { + return true; + } + match find_def(block, var) { + Some(SSAOp::Copy { src, .. }) + | Some(SSAOp::IntZExt { src, .. }) + | Some(SSAOp::IntSExt { src, .. }) => var_equivalent(src, target, block), + _ => false, + } +} + +fn var_matches_counter_term(var: &SSAVar, counter_phi: &SSAVar, block: &FunctionSSABlock) -> bool { + if var == counter_phi { + return true; + } + match find_def(block, var) { + Some(SSAOp::Copy { src, .. }) + | Some(SSAOp::IntZExt { src, .. }) + | Some(SSAOp::IntSExt { src, .. }) => var_matches_counter_term(src, counter_phi, block), + Some(SSAOp::IntMult { a, b, .. }) => { + (parse_const_u64(a) == Some(1) && var_matches_counter_term(b, counter_phi, block)) + || (parse_const_u64(b) == Some(1) + && var_matches_counter_term(a, counter_phi, block)) + } + _ => false, + } +} + +fn match_base_plus_counter<'a>( + addr: &'a SSAVar, + counter_phi: &SSAVar, + block: &'a FunctionSSABlock, +) -> Option<&'a SSAVar> { + let op = find_def(block, addr)?; + match op { + SSAOp::IntAdd { a, b, .. } => { + if var_matches_counter_term(a, counter_phi, block) { + Some(b) + } else if var_matches_counter_term(b, counter_phi, block) { + Some(a) + } else { + None + } + } + _ => None, + } +} + +fn resolve_var_concrete<'ctx>( + executor: &SymExecutor<'ctx>, + state: &SymState<'ctx>, + var: &SSAVar, + block: &FunctionSSABlock, +) -> Option { + if let Some(value) = parse_const_u64(var) { + return Some(value); + } + match find_def(block, var) { + Some(SSAOp::Copy { src, .. }) + | Some(SSAOp::IntZExt { src, .. }) + | Some(SSAOp::IntSExt { src, .. }) => resolve_var_concrete(executor, state, src, block), + _ => executor.read_var(state, var).as_concrete(), + } +} + +fn parse_const_u64(var: &SSAVar) -> Option { + var.name + .strip_prefix("const:") + .and_then(|value| u64::from_str_radix(value, 16).ok()) +} + +fn parse_ram_addr(var: &SSAVar) -> Option { + var.name + .strip_prefix("ram:") + .and_then(|value| u64::from_str_radix(value, 16).ok()) +} + +fn parse_address_literal(var: &SSAVar) -> Option { + parse_ram_addr(var).or_else(|| parse_const_u64(var)) +} + +fn parse_var_addr(var: &SSAVar) -> Option { + parse_address_literal(var) } #[derive(Clone, Debug, PartialEq, Eq)] @@ -1017,6 +1570,37 @@ fn resolve_alias_register_value<'ctx>( } } +fn resolve_alias_value_provenance( + state: &SymState<'_>, + key: &str, +) -> Option { + let (requested_base, _requested_version) = split_versioned_register(key)?; + let requested = x86_register_alias_spec(requested_base)?; + + let mut best: Option<(u32, RegisterAliasSpec, RuntimeValueProvenance)> = None; + for (candidate_key, provenance) in &state.runtime().value_provenance { + let Some((candidate_base, candidate_version)) = split_versioned_register(candidate_key) + else { + continue; + }; + let Some(candidate) = x86_register_alias_spec(candidate_base) else { + continue; + }; + if candidate.family != requested.family { + continue; + } + let should_replace = best.as_ref().is_none_or(|(best_version, best_spec, _)| { + candidate_version > *best_version + || (candidate_version == *best_version + && candidate.width_bits > best_spec.width_bits) + }); + if should_replace { + best = Some((candidate_version, candidate, provenance.clone())); + } + } + best.map(|(_, _, provenance)| provenance) +} + fn split_versioned_register(name: &str) -> Option<(&str, u32)> { let (base, version) = name.rsplit_once('_')?; if version.is_empty() || !version.chars().all(|c| c.is_ascii_digit()) { @@ -1247,6 +1831,7 @@ fn parse_numbered_x86_register_alias(base: &str) -> Option { #[cfg(test)] mod tests { use super::*; + use crate::MemoryRegionKind; #[test] fn test_copy_op() { @@ -1269,6 +1854,47 @@ mod tests { assert_eq!(dst_val.as_concrete(), Some(42)); } + #[test] + fn test_ram_copy_operands_use_memory_not_address_literals() { + let ctx = Context::thread_local(); + let executor = SymExecutor::new(&ctx); + let mut state = SymState::new(&ctx, 0x1000); + let region = + state.define_memory_region(MemoryRegionKind::Global, "global", Some(0x2000), Some(8)); + state.seed_region_bytes(region, 0, &0x1122_3344_5566_7788u64.to_le_bytes()); + + executor + .step( + &mut state, + &SSAOp::Copy { + dst: SSAVar::new("dst", 1, 8), + src: SSAVar::new("ram:2000", 0, 8), + }, + ) + .expect("global load should execute"); + assert_eq!( + state.get_register("DST_1").as_concrete(), + Some(0x1122_3344_5566_7788) + ); + + state.set_register("SRC_0", SymValue::concrete(0xaabb_ccdd_eeff_0011, 64)); + executor + .step( + &mut state, + &SSAOp::Copy { + dst: SSAVar::new("ram:2000", 1, 8), + src: SSAVar::new("src", 0, 8), + }, + ) + .expect("global store should execute"); + assert_eq!( + state + .mem_read(&SymValue::concrete(0x2000, 64), 8) + .as_concrete(), + Some(0xaabb_ccdd_eeff_0011) + ); + } + #[test] fn test_add_op() { let ctx = Context::thread_local(); @@ -1311,6 +1937,27 @@ mod tests { assert_eq!(state.pc, 0x2000); // Branch taken } + #[test] + fn test_cbranch_ram_target_stays_address_literal() { + let ctx = Context::thread_local(); + let executor = SymExecutor::new(&ctx); + let mut state = SymState::new(&ctx, 0x1000); + state.set_register("COND_0", SymValue::concrete(1, 1)); + + let forked = executor + .step( + &mut state, + &SSAOp::CBranch { + target: SSAVar::new("ram:3000", 0, 8), + cond: SSAVar::new("cond", 0, 1), + }, + ) + .expect("branch should execute"); + + assert!(forked.is_empty()); + assert_eq!(state.pc, 0x3000); + } + #[test] fn test_cbranch_symbolic() { let ctx = Context::thread_local(); @@ -1419,4 +2066,251 @@ mod tests { "false branch condition should remain symbolic" ); } + + #[test] + fn test_callind_uses_provenance_source_for_hook_lookup() { + let ctx = Context::thread_local(); + let mut executor = SymExecutor::new(&ctx); + executor.register_call_hook(0x401000, |state| { + state.set_concrete("RAX_0", 0x99, 64); + Ok(CallHookResult::Fallthrough) + }); + + let mut state = SymState::new(&ctx, 0x1000); + state.set_register("TMP_0", SymValue::concrete(0x12345678, 64)); + state.set_value_provenance( + "TMP_0", + Some(RuntimeValueProvenance { + source_addr: 0x401000, + size: 8, + }), + ); + + let op = SSAOp::CallInd { + target: SSAVar::new("tmp", 0, 8), + }; + + executor + .step(&mut state, &op) + .expect("callind should execute"); + assert_eq!(state.get_register("RAX_0").as_concrete(), Some(0x99)); + } + + #[test] + fn test_direct_call_fork_requires_whitelist() { + let ctx = Context::thread_local(); + let mut executor = SymExecutor::new(&ctx); + let op = SSAOp::Call { + target: SSAVar::constant(0x401000, 8), + }; + + let mut state = SymState::new(&ctx, 0x1000); + let forks = executor + .step(&mut state, &op) + .expect("direct call should execute"); + assert!( + forks.is_empty(), + "direct calls should remain fallthrough-only by default" + ); + + let mut targets = HashSet::new(); + targets.insert(0x401000); + executor.replace_direct_call_fork_targets(Some(targets)); + + let mut state = SymState::new(&ctx, 0x1000); + let forks = executor + .step(&mut state, &op) + .expect("direct call should execute"); + assert_eq!(forks.len(), 1); + assert_eq!(forks[0].pc, 0x401000); + assert_eq!(state.pc, 0x1000, "caller fallthrough state is preserved"); + } + + #[test] + fn test_execute_block_skips_local_cbranch_tail_when_taken() { + let ctx = Context::thread_local(); + let executor = SymExecutor::new(&ctx); + let mut state = SymState::new(&ctx, 0x1000); + state.set_register("COND_0", SymValue::concrete(1, 1)); + state.set_register("RAX_0", SymValue::concrete(0x11, 64)); + let block = FunctionSSABlock { + addr: 0x1000, + size: 4, + phis: Vec::new(), + ops: vec![ + SSAOp::CBranch { + target: SSAVar::new("ram:1004", 0, 8), + cond: SSAVar::new("cond", 0, 1), + }, + SSAOp::Copy { + dst: SSAVar::new("rax", 1, 8), + src: SSAVar::constant(0x22, 8), + }, + ], + }; + + let forks = executor + .execute_block(&mut state, &block) + .expect("block should execute"); + + assert!(forks.is_empty()); + assert_eq!(state.pc, 0x1004); + assert_eq!( + executor + .read_var(&state, &SSAVar::new("rax", 1, 8)) + .as_concrete(), + Some(0x11) + ); + } + + #[test] + fn test_execute_block_merges_symbolic_local_cbranch_tail() { + let ctx = Context::thread_local(); + let executor = SymExecutor::new(&ctx); + let mut state = SymState::new(&ctx, 0x1000); + state.set_register("COND_0", SymValue::new_symbolic(&ctx, "cond", 1)); + state.set_register("RAX_0", SymValue::concrete(0x11, 64)); + let block = FunctionSSABlock { + addr: 0x1000, + size: 4, + phis: Vec::new(), + ops: vec![ + SSAOp::CBranch { + target: SSAVar::new("ram:1004", 0, 8), + cond: SSAVar::new("cond", 0, 1), + }, + SSAOp::Copy { + dst: SSAVar::new("rax", 1, 8), + src: SSAVar::constant(0x22, 8), + }, + ], + }; + + let forks = executor + .execute_block(&mut state, &block) + .expect("block should execute"); + + assert!(forks.is_empty()); + assert_eq!(state.pc, 0x1004); + assert!( + executor + .read_var(&state, &SSAVar::new("rax", 1, 8)) + .as_concrete() + .is_none(), + "symbolic local branch should become predicated dataflow, not a path fork" + ); + } + + #[test] + fn test_callind_uses_direct_ram_source_provenance_for_import_hook_lookup() { + let ctx = Context::thread_local(); + let mut executor = SymExecutor::new(&ctx); + executor.register_call_hook(0x401000, |state| { + state.set_concrete("RAX_0", 0x1234, 64); + Ok(CallHookResult::Fallthrough) + }); + + let mut state = SymState::new(&ctx, 0x1000); + executor + .step( + &mut state, + &SSAOp::Copy { + dst: SSAVar::new("tmp:import", 0, 8), + src: SSAVar::new("ram:401000", 0, 8), + }, + ) + .expect("import slot load should execute"); + executor + .step( + &mut state, + &SSAOp::CallInd { + target: SSAVar::new("tmp:import", 0, 8), + }, + ) + .expect("callind should execute through source provenance"); + + assert_eq!(state.get_register("RAX_0").as_concrete(), Some(0x1234)); + } + + #[test] + fn test_execute_block_runahead_collapses_concrete_copy_loop() { + let ctx = Context::thread_local(); + let executor = SymExecutor::new(&ctx); + let mut state = SymState::new(&ctx, 0x1000); + + let source_region = + state.define_memory_region(MemoryRegionKind::Global, "blob", Some(0x2000), Some(4)); + state.seed_region_bytes(source_region, 0, &[0x41, 0x42, 0x43, 0x44]); + + let (_region_id, runtime_base) = state.allocate_heap_region("jit_blob", 4); + state.register_runtime_region_alias(runtime_base, 4, true); + state.set_concrete("RBX_0", runtime_base, 64); + state.set_prev_pc(Some(0x0)); + + let block = FunctionSSABlock { + addr: 0x1000, + size: 7, + phis: vec![r2ssa::PhiNode { + dst: SSAVar::new("RCX", 1, 8), + sources: vec![ + (0x0, SSAVar::constant(0, 8)), + (0x1000, SSAVar::new("RCX", 2, 8)), + ], + }], + ops: vec![ + SSAOp::IntAdd { + dst: SSAVar::new("tmp:src", 0, 8), + a: SSAVar::constant(0x2000, 8), + b: SSAVar::new("RCX", 1, 8), + }, + SSAOp::Load { + dst: SSAVar::new("tmp:byte", 0, 1), + addr: SSAVar::new("tmp:src", 0, 8), + space: "ram".to_string(), + }, + SSAOp::IntAdd { + dst: SSAVar::new("tmp:dst", 0, 8), + a: SSAVar::new("RBX", 0, 8), + b: SSAVar::new("RCX", 1, 8), + }, + SSAOp::Store { + space: "ram".to_string(), + addr: SSAVar::new("tmp:dst", 0, 8), + val: SSAVar::new("tmp:byte", 0, 1), + }, + SSAOp::IntAdd { + dst: SSAVar::new("RCX", 2, 8), + a: SSAVar::new("RCX", 1, 8), + b: SSAVar::constant(1, 8), + }, + SSAOp::IntLess { + dst: SSAVar::new("CF", 0, 1), + a: SSAVar::new("RCX", 2, 8), + b: SSAVar::constant(4, 8), + }, + SSAOp::CBranch { + target: SSAVar::new("ram:1000", 0, 8), + cond: SSAVar::new("CF", 0, 1), + }, + ], + }; + + let forked = executor + .execute_block(&mut state, &block) + .expect("copy loop block should execute"); + assert!(forked.is_empty()); + assert_eq!(state.pc, 0x1000); + assert_eq!(state.get_register("RCX_2").as_concrete(), Some(4)); + assert_eq!( + state + .mem_read(&SymValue::concrete(runtime_base, 64), 4) + .as_concrete(), + Some(0x4443_4241) + ); + let region = state + .runtime_region_for_pc(runtime_base) + .expect("runtime alias should remain registered"); + assert_eq!(region.source_base, Some(0x2000)); + assert!(state.depth >= block.ops.len() * 4); + } } diff --git a/crates/r2sym/src/kernel.rs b/crates/r2sym/src/kernel.rs new file mode 100644 index 0000000..b0f69f2 --- /dev/null +++ b/crates/r2sym/src/kernel.rs @@ -0,0 +1,595 @@ +//! Canonical semantic graph and fact-planning scaffolding. +//! +//! This module is intentionally small for the first architecture tranche: it +//! gives SSA, symex, runtime, and future replay backends a typed place to meet +//! without putting semantic policy in the plugin or executor. + +use std::collections::{BTreeMap, BTreeSet, VecDeque}; + +use r2ssa::InterprocFunctionId; + +use crate::PreparedFunctionScope; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct FunctionId(pub u64); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct NodeId(pub u64); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct EdgeId(pub u64); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct RegionId(pub u64); + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum FactPrecision { + Unknown, + UnderApprox, + Residual, + OverApprox, + Exact, +} + +impl FactPrecision { + pub fn is_exact(self) -> bool { + matches!(self, Self::Exact) + } + + pub fn is_residual_or_weaker(self) -> bool { + matches!(self, Self::Unknown | Self::UnderApprox | Self::Residual) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum SemanticEvidenceKind { + Static, + Runtime, + Replay, + UserAssumption, + Inferred, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SemanticEvidenceRecord { + pub kind: SemanticEvidenceKind, + pub precision: FactPrecision, + pub reason: String, +} + +impl SemanticEvidenceRecord { + pub fn exact_static(reason: impl Into) -> Self { + Self { + kind: SemanticEvidenceKind::Static, + precision: FactPrecision::Exact, + reason: reason.into(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SemanticNodeKind { + FunctionEntry { + function: FunctionId, + }, + BasicBlock { + function: FunctionId, + addr: u64, + }, + RuntimeRegion { + region: RegionId, + base: u64, + size: u64, + }, + ExceptionContinuation { + handler: u64, + }, + ReplayCheckpoint { + checkpoint: u64, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SemanticNode { + pub id: NodeId, + pub kind: SemanticNodeKind, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SemanticEdgeKind { + Cfg, + Call, + Return, + Thunk, + Exception, + RuntimeAlias, + Replay, + Summary, + Refused { reason: String }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CallEdgePolicy { + FallthroughOnly, + InlineCallee, + ApplySummary, + ForkCallAndFallthrough, + Refuse, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SemanticEdgeAction { + Follow, + ApplyCallPolicy(CallEdgePolicy), + SeedContinuation, + MapRuntimeRegion, + ApplySummary, + Refuse { reason: String }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SemanticEdge { + pub id: EdgeId, + pub from: NodeId, + pub to: NodeId, + pub kind: SemanticEdgeKind, + pub action: SemanticEdgeAction, + pub evidence: SemanticEvidenceRecord, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RuntimeRegionFact { + pub id: RegionId, + pub base: u64, + pub size: u64, + pub source_base: Option, + pub executable: bool, + pub evidence: SemanticEvidenceRecord, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ConcreteStopReason { + Completed, + HitTarget(u64), + Exception(u64), + BudgetExceeded, + Unsupported(String), +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct ConcreteTraceEvidence { + pub executed_blocks: Vec, + pub runtime_regions: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ConcreteMemorySeed { + pub addr: u64, + pub bytes: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ConcreteRunRequest { + pub entry: u64, + pub target: Option, + pub max_instructions: usize, + pub argv: Vec>, + pub registers: BTreeMap, + pub memory: Vec, + pub runtime_regions: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ConcreteRunResult { + pub trace: ConcreteTraceEvidence, + pub stop_reason: ConcreteStopReason, + pub final_pc: Option, + pub diagnostics: Vec, +} + +pub trait ConcreteExecutionBackend { + fn run(&self, request: ConcreteRunRequest) -> ConcreteRunResult; +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct FunctionClosureBudget { + pub max_functions: usize, + pub max_blocks_per_function: usize, + pub max_total_blocks: usize, +} + +impl Default for FunctionClosureBudget { + fn default() -> Self { + Self { + max_functions: 32, + max_blocks_per_function: 96, + max_total_blocks: 512, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FunctionClosureReason { + Root, + HelperByScope, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FunctionClosureExclusionReason { + FunctionBudgetExceeded, + BlockBudgetExceeded, + TotalBlockBudgetExceeded, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FunctionClosureEntry { + pub function: FunctionId, + pub name: Option, + pub block_count: usize, + pub reason: FunctionClosureReason, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FunctionClosureExclusion { + pub function: FunctionId, + pub name: Option, + pub block_count: usize, + pub reason: FunctionClosureExclusionReason, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FunctionClosurePlan { + pub root: FunctionId, + pub budget: FunctionClosureBudget, + pub included: Vec, + pub excluded: Vec, +} + +impl FunctionClosurePlan { + pub fn from_scope(scope: &PreparedFunctionScope, budget: FunctionClosureBudget) -> Self { + let root = FunctionId(scope.root_id().0); + let mut total_blocks = 0usize; + let mut included = Vec::new(); + let mut excluded = Vec::new(); + + for function in scope.functions().values() { + let id = FunctionId(function.id.0); + let block_count = function.prepared.cfg().num_blocks(); + let is_root = function.id == InterprocFunctionId(root.0); + let exclusion = if !is_root && included.len() >= budget.max_functions { + Some(FunctionClosureExclusionReason::FunctionBudgetExceeded) + } else if !is_root && block_count > budget.max_blocks_per_function { + Some(FunctionClosureExclusionReason::BlockBudgetExceeded) + } else if !is_root && total_blocks.saturating_add(block_count) > budget.max_total_blocks + { + Some(FunctionClosureExclusionReason::TotalBlockBudgetExceeded) + } else { + None + }; + + if let Some(reason) = exclusion { + excluded.push(FunctionClosureExclusion { + function: id, + name: function.name.clone(), + block_count, + reason, + }); + continue; + } + + total_blocks = total_blocks.saturating_add(block_count); + included.push(FunctionClosureEntry { + function: id, + name: function.name.clone(), + block_count, + reason: if is_root { + FunctionClosureReason::Root + } else { + FunctionClosureReason::HelperByScope + }, + }); + } + + Self { + root, + budget, + included, + excluded, + } + } + + pub fn included_ids(&self) -> BTreeSet { + self.included + .iter() + .map(|entry| entry.function) + .collect::>() + } +} + +#[derive(Debug, Clone, Default)] +pub struct SemanticProgramGraph { + nodes: BTreeMap, + edges: BTreeMap, + block_nodes: BTreeMap<(FunctionId, u64), NodeId>, + addr_nodes: BTreeMap>, + successors: BTreeMap>, + predecessors: BTreeMap>, + next_node: u64, + next_edge: u64, +} + +impl SemanticProgramGraph { + pub fn new() -> Self { + Self::default() + } + + pub fn from_scope(scope: &PreparedFunctionScope, closure: &FunctionClosurePlan) -> Self { + let mut graph = Self::new(); + let included = closure.included_ids(); + for function in scope.functions().values() { + let function_id = FunctionId(function.id.0); + if !included.contains(&function_id) { + continue; + } + graph.add_node(SemanticNodeKind::FunctionEntry { + function: function_id, + }); + for addr in function.prepared.cfg().block_addrs() { + graph.add_block_node(function_id, addr); + } + } + + for function in scope.functions().values() { + let function_id = FunctionId(function.id.0); + if !included.contains(&function_id) { + continue; + } + for addr in function.prepared.cfg().block_addrs() { + let Some(block) = function.prepared.cfg().get_block(addr) else { + continue; + }; + for succ in block.successors() { + graph.add_block_edge( + function_id, + addr, + function_id, + succ, + SemanticEdgeKind::Cfg, + SemanticEdgeAction::Follow, + ); + } + } + for call in function.prepared.call_sites().by_id.values() { + let Some(target) = function.prepared.resolved_call_target(call) else { + continue; + }; + let Some((block_addr, _)) = function.prepared.inst_op_site(call.at) else { + continue; + }; + let callee_id = scope + .function_containing_block(target) + .map(|callee| FunctionId(callee.id.0)) + .unwrap_or(FunctionId(target)); + graph.add_block_edge( + function_id, + block_addr, + callee_id, + target, + SemanticEdgeKind::Call, + SemanticEdgeAction::ApplyCallPolicy(CallEdgePolicy::ForkCallAndFallthrough), + ); + } + } + graph + } + + pub fn nodes(&self) -> &BTreeMap { + &self.nodes + } + + pub fn edges(&self) -> &BTreeMap { + &self.edges + } + + pub fn node_for_block(&self, function: FunctionId, addr: u64) -> Option { + self.block_nodes.get(&(function, addr)).copied() + } + + pub fn nodes_for_addr(&self, addr: u64) -> Option<&BTreeSet> { + self.addr_nodes.get(&addr) + } + + pub fn reverse_reachable_nodes(&self, target: NodeId) -> BTreeSet { + let mut seen = BTreeSet::new(); + let mut queue = VecDeque::from([target]); + while let Some(node) = queue.pop_front() { + if !seen.insert(node) { + continue; + } + for pred in self.predecessors.get(&node).into_iter().flatten() { + queue.push_back(*pred); + } + } + seen + } + + fn add_node(&mut self, kind: SemanticNodeKind) -> NodeId { + let id = NodeId(self.next_node); + self.next_node = self.next_node.saturating_add(1); + if let SemanticNodeKind::BasicBlock { function, addr } = kind { + self.block_nodes.insert((function, addr), id); + self.addr_nodes.entry(addr).or_default().insert(id); + self.nodes.insert(id, SemanticNode { id, kind }); + return id; + } + self.nodes.insert(id, SemanticNode { id, kind }); + id + } + + fn add_block_node(&mut self, function: FunctionId, addr: u64) -> NodeId { + if let Some(id) = self.node_for_block(function, addr) { + return id; + } + self.add_node(SemanticNodeKind::BasicBlock { function, addr }) + } + + fn add_block_edge( + &mut self, + from_function: FunctionId, + from_addr: u64, + to_function: FunctionId, + to_addr: u64, + kind: SemanticEdgeKind, + action: SemanticEdgeAction, + ) -> EdgeId { + let from = self.add_block_node(from_function, from_addr); + let to = self.add_block_node(to_function, to_addr); + let id = EdgeId(self.next_edge); + self.next_edge = self.next_edge.saturating_add(1); + self.successors.entry(from).or_default().insert(to); + self.predecessors.entry(to).or_default().insert(from); + self.edges.insert( + id, + SemanticEdge { + id, + from, + to, + kind, + action, + evidence: SemanticEvidenceRecord::exact_static("scope graph"), + }, + ); + id + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use r2il::{R2ILBlock, R2ILOp, SpaceId, Varnode}; + use r2ssa::SsaArtifact; + + fn make_const(val: u64) -> Varnode { + Varnode { + space: SpaceId::Const, + offset: val, + size: 8, + meta: None, + } + } + + fn leaf(addr: u64) -> SsaArtifact { + SsaArtifact::for_symbolic( + &[R2ILBlock { + addr, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0), + }], + switch_info: None, + op_metadata: Default::default(), + }], + None, + ) + .expect("leaf SSA") + } + + #[test] + fn closure_plan_is_deterministic_and_budgeted() { + let root = leaf(0x1000); + let helper_a = leaf(0x2000); + let helper_b = leaf(0x3000); + let scope = PreparedFunctionScope::new( + 0x1000, + vec![ + crate::ScopedPreparedFunction { + id: InterprocFunctionId(0x3000), + name: Some("helper_b".to_string()), + prepared: helper_b, + }, + crate::ScopedPreparedFunction { + id: InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root, + }, + crate::ScopedPreparedFunction { + id: InterprocFunctionId(0x2000), + name: Some("helper_a".to_string()), + prepared: helper_a, + }, + ], + ) + .expect("scope"); + + let plan = FunctionClosurePlan::from_scope( + &scope, + FunctionClosureBudget { + max_functions: 2, + ..FunctionClosureBudget::default() + }, + ); + + assert_eq!( + plan.included + .iter() + .map(|entry| entry.function.0) + .collect::>(), + vec![0x1000, 0x2000] + ); + assert_eq!(plan.excluded.len(), 1); + assert_eq!( + plan.excluded[0].reason, + FunctionClosureExclusionReason::FunctionBudgetExceeded + ); + } + + #[test] + fn graph_reverse_reachability_is_indexed_by_nodes() { + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x1004), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + let root = SsaArtifact::for_symbolic(&blocks, None).expect("root SSA"); + let scope = PreparedFunctionScope::new( + 0x1000, + vec![crate::ScopedPreparedFunction { + id: InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root, + }], + ) + .expect("scope"); + let closure = FunctionClosurePlan::from_scope(&scope, FunctionClosureBudget::default()); + let graph = SemanticProgramGraph::from_scope(&scope, &closure); + let target = graph + .node_for_block(FunctionId(0x1000), 0x1004) + .expect("target node"); + + let reachable = graph.reverse_reachable_nodes(target); + assert!(reachable.contains(&target)); + assert!( + reachable.contains( + &graph + .node_for_block(FunctionId(0x1000), 0x1000) + .expect("entry node") + ) + ); + } +} diff --git a/crates/r2sym/src/lib.rs b/crates/r2sym/src/lib.rs index e508573..dba3b99 100644 --- a/crates/r2sym/src/lib.rs +++ b/crates/r2sym/src/lib.rs @@ -34,7 +34,10 @@ //! ``` pub mod backward; +pub mod constraints; pub mod executor; +pub mod kernel; +pub mod loops; pub mod memory; pub mod path; pub mod query; @@ -46,43 +49,83 @@ pub mod sim; pub mod solver; pub mod spec; pub mod state; +pub mod tactics; pub mod value; +pub mod verification; pub use backward::{ BackwardConditionPrecision, BackwardConditionSummary, BackwardMemoryCondition, BackwardMemoryRegion, BackwardRegionRef, CompiledBackwardCondition, compile_derived_summary_return_postcondition, compile_target_precondition, }; +pub use constraints::{ + FinalConstraint, FinalConstraintGraph, FinalConstraintPrecision, FinalConstraintSource, + FoldAggregateConstraint, FoldAggregateRangeConstraint, InputByteConstraint, + InputLengthConstraint, RecurrenceAggregateConstraint, RecurrenceAggregateRangeConstraint, + aggregate_exact_fold_bytes, build_exact_fold_constraint_graph, + build_final_constraint_graph_for_path, build_model_conditioned_recurrence_constraint_graph, + exact_fold_model_bytes, +}; pub use executor::{CallHookResult, SymExecutor}; +pub use kernel::{ + CallEdgePolicy, ConcreteExecutionBackend, ConcreteMemorySeed, ConcreteRunRequest, + ConcreteRunResult, ConcreteStopReason, ConcreteTraceEvidence, EdgeId, FactPrecision, + FunctionClosureBudget, FunctionClosureEntry, FunctionClosureExclusion, + FunctionClosureExclusionReason, FunctionClosurePlan, FunctionClosureReason, FunctionId, + NodeId as SemanticNodeId, RegionId as SemanticRegionId, RuntimeRegionFact, SemanticEdge, + SemanticEdgeAction, SemanticEdgeKind, SemanticEvidenceKind, SemanticEvidenceRecord, + SemanticNode, SemanticNodeKind, SemanticProgramGraph, +}; +pub use loops::{ + ExactLoopFoldEvidence, ExactLoopRecurrenceEvidence, ExactLoopRecurrenceKind, LoopCarriedVar, + LoopFoldOperation, LoopMemoryTerm, LoopMemoryTermKind, LoopRecurrence, LoopRecurrenceKind, + LoopRotateDirection, LoopSummary, LoopSummaryKind, LoopTransitionExpr, LoopTransitionSystem, + LoopVarRole, RuntimeLoopBranch, exact_fold_evidence_from_recurrences, +}; pub use memory::{ MemoryRegionId, MemoryRegionKind, RegionPointer, ResolvedPointerSet, SymMemory, SymbolicMemoryRegionDef, }; -pub use path::{ExploreConfig, PathExplorer, PathResult, SolvedPath}; +pub use path::{ + ExploreConfig, PathExplorer, PathResult, SolvedPath, SolvedPathGeneration, + SolvedPathGenerationKind, +}; pub use query::{ - PathConditionResult, PathConditionSummary, PathConditionTerm, QueryCompletion, QueryMode, - ReachabilityResult, ReachabilityStatus, SolveResult, SolveStatus, SymQueryConfig, - SymbolicConditionSet, SymbolicFunctionSummary, + PathConditionResult, PathConditionSummary, PathConditionTerm, QueryCompletion, + QueryExecutionPolicy, QueryMode, ReachabilityResult, ReachabilityStatus, SolveResult, + SymQueryConfig, SymbolicConditionSet, SymbolicFunctionSummary, apply_query_execution_policy, + install_symbolic_hooks_for_query_policy, recommended_query_max_depth, + recommended_query_max_depth_for_route, recommended_query_max_states_for_route, + recommended_query_timeout, recommended_query_timeout_for_route, + route_skips_eager_scope_summaries, selected_target_query_route_in_scope, }; pub use r2api::{R2Api, R2Error}; pub use replay::{ ReplayMemoryOverlay, ReplayMemoryWindow, ReplayRegisterOverlay, ReplayRegisterValue, ReplaySeed, apply_replay_seed_to_state, seed_replay_state_for_arch, + stable_replay_seed_fingerprint, +}; +pub use runtime::{ + install_runtime_hooks_for_scope, seed_default_state_for_arch, seed_memory_regions_for_arch, + seed_scope_state_for_arch, }; -pub use runtime::{seed_default_state_for_arch, seed_memory_regions_for_arch}; pub use semantics::{ ArtifactBuildPlan, ArtifactGranularity, ControlFact, DecompilePlan, ExecutionModel, InterpreterDispatchSummary, InterpreterKind, Judged, MemoryFact, NativeArtifactBody, NativeFunctionSummary, QueryGuidanceMode, QueryPlan, RefinementStage, RegionKey, ResidualReason, SEMANTIC_ARTIFACT_SCHEMA_VERSION, SemanticArtifact, SemanticArtifactBody, - SemanticArtifactDiagnostics, SemanticConfidence, SemanticEvidence, SemanticEvidenceAmbiguity, - SemanticEvidenceCoverage, SemanticEvidenceProvenance, SemanticEvidenceReason, - SemanticEvidenceSoundness, SemanticPredicate, SemanticRegion, SemanticTargetConditionSource, - SliceClass, SymbolicReachabilityStatus, TargetFact, TargetQueryPlan, TargetQueryRoutePlan, - TypePlan, VmArtifactBody, VmBinaryOp, VmGuardCondition, VmGuardedExit, VmMemoryCondition, + SemanticArtifactDiagnostics, SemanticCompilationResult, SemanticConfidence, SemanticEvidence, + SemanticEvidenceAmbiguity, SemanticEvidenceCoverage, SemanticEvidenceProvenance, + SemanticEvidenceReason, SemanticEvidenceSoundness, SemanticPredicate, SemanticRegion, + SemanticSeedMode, SemanticTargetConditionSource, SliceClass, SymbolicReachabilityStatus, + TargetFact, TargetQueryExecutionRoute, TargetQueryPlan, TargetQueryRoutePlan, TypePlan, + VmArtifactBody, VmBinaryOp, VmGuardCondition, VmGuardedExit, VmMemoryCondition, VmMemoryRegionRef, VmStateUpdate, VmStepSummary, VmTransferArm, VmUnaryOp, VmValueExpr, - compile_function_semantics_with_scope, compile_semantic_artifact_default_with_scope, - compile_semantic_artifact_with_scope, stable_scope_hash, + compile_function_semantics_with_scope, compile_function_semantics_with_scope_and_replay_seed, + compile_query_semantic_artifact_with_scope, + compile_query_semantic_artifact_with_scope_and_replay_seed, + compile_semantic_artifact_default_with_scope, compile_semantic_artifact_with_scope, + compile_semantic_artifact_with_scope_and_replay_seed, stable_scope_hash, }; pub use sim::{ CallConv, CallInfo, DerivedFunctionSummary, DerivedSummaryCase, DerivedSummaryCompletion, @@ -95,8 +138,26 @@ pub use spec::{ AddressValue, BudgetSpec, ExplorationSpec, InputSpec, MergeSpec, PredicateSpec, RuntimeSpec, StartSpec, StrategySpec, }; -pub use state::{RuntimeState, SymState, SymbolicFdInput, SymbolicMemoryRegion}; +pub use state::{ + PendingExceptionContinuation, RuntimeBlockReason, RuntimeRegionAlias, RuntimeState, + RuntimeValueProvenance, SymState, SymbolicFdInput, SymbolicMemoryRegion, +}; +pub use tactics::{ + InputByteDomain, SolveTacticCandidate, SolveTacticConfig, TacticConstraintReport, + algebraic_preimage_candidate, algebraic_preimage_for_target, constrain_exact_fold_candidate, + constrain_exact_fold_inputs, constrain_exact_recurrence_candidate, + tactic_candidates_for_constraint_graph, +}; pub use value::SymValue; +pub use verification::{ + CandidateReplayBackend, CandidateReplayRequest, EvidenceSummary, LiftedReplayBackend, + ModelValidation, SolveCandidateShape, SolveStatus, SolveTacticEvidence, SolveTacticKind, + SolveTacticStatus, SolveVerification, SolveVerificationRequest, SolveWitness, + UnavailableReplayBackend, VerificationRequirement, evidence_summary_for_route_and_stats, + solution_extraction_allowed, solve_tactics_for_exact_folds, + solve_tactics_for_exact_recurrences, verification_requirement_for_route_and_stats, + verify_solve_result, +}; /// Error types for symbolic execution. #[derive(Debug, thiserror::Error)] diff --git a/crates/r2sym/src/loops.rs b/crates/r2sym/src/loops.rs new file mode 100644 index 0000000..3d4ce91 --- /dev/null +++ b/crates/r2sym/src/loops.rs @@ -0,0 +1,2721 @@ +//! Runtime loop recognition and summary construction. +//! +//! This module owns loop-shape policy for symex. Path exploration executes +//! summaries, while verification decides whether summarized evidence can prove +//! a solve. + +use std::collections::{HashMap, HashSet}; + +use r2ssa::{FunctionSSABlock, SSAOp, SSAVar, SsaArtifact}; +use z3::Context; + +use crate::{SymState, SymValue}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LoopSummaryKind { + Exact, + BoundedExact, + Residual, + Refused, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LoopVarRole { + InductionCounter, + Accumulator, + Pointer, + MemoryEffect, + Unknown, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LoopCarriedVar { + pub name: String, + pub bits: u32, + pub role: LoopVarRole, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum LoopTransitionExpr { + Identity(String), + AddConst { + var: String, + value: u64, + }, + AffineConst { + var: String, + multiplier: u64, + addend: u64, + }, + XorConst { + var: String, + value: u64, + }, + XorVar { + lhs: String, + rhs: String, + }, + AddVar { + lhs: String, + rhs: String, + }, + Load { + addr: String, + bytes: u32, + }, + TableRead(LoopMemoryTerm), + RotateMix { + var: String, + direction: LoopRotateDirection, + amount: u32, + operation: LoopFoldOperation, + term: LoopMemoryTerm, + }, + Store { + addr: String, + value: String, + }, + Unknown, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LoopMemoryTermKind { + TableRead, + InputRead, + RuntimeBlobRead, + Unknown, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LoopMemoryTerm { + pub kind: LoopMemoryTermKind, + pub addr: String, + pub bytes: u32, + pub base: Option, + pub stride: Option, + pub region: Option, + pub region_base: Option, + pub region_size: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum LoopRecurrenceKind { + Identity, + AddConst(u64), + SubConst(u64), + AffineConst { + multiplier: u64, + addend: u64, + }, + XorConst(u64), + AddMemoryFold(LoopMemoryTerm), + XorMemoryFold(LoopMemoryTerm), + RotateMix { + direction: LoopRotateDirection, + amount: u32, + operation: LoopFoldOperation, + term: LoopMemoryTerm, + }, + Unsupported(String), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LoopFoldOperation { + Add, + Xor, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LoopRotateDirection { + Left, + Right, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ExactLoopRecurrenceKind { + AddConst(u64), + SubConst(u64), + AffineConst { + multiplier: u64, + addend: u64, + }, + XorConst(u64), + Fold { + operation: LoopFoldOperation, + term: LoopMemoryTerm, + }, + RotateMix { + direction: LoopRotateDirection, + amount: u32, + operation: LoopFoldOperation, + term: LoopMemoryTerm, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExactLoopRecurrenceEvidence { + pub header: u64, + pub exit_target: u64, + pub iterations: u64, + pub accumulator: String, + pub initial: String, + pub bits: u32, + pub kind: ExactLoopRecurrenceKind, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ExactLoopFoldEvidence { + pub header: u64, + pub exit_target: u64, + pub iterations: u64, + pub accumulator: String, + pub bits: u32, + pub operation: LoopFoldOperation, + pub term: LoopMemoryTerm, +} + +impl ExactLoopRecurrenceEvidence { + pub fn as_fold(&self) -> Option { + let ExactLoopRecurrenceKind::Fold { operation, term } = &self.kind else { + return None; + }; + Some(ExactLoopFoldEvidence { + header: self.header, + exit_target: self.exit_target, + iterations: self.iterations, + accumulator: self.accumulator.clone(), + bits: self.bits, + operation: *operation, + term: term.clone(), + }) + } +} + +impl From for ExactLoopRecurrenceEvidence { + fn from(value: ExactLoopFoldEvidence) -> Self { + Self { + header: value.header, + exit_target: value.exit_target, + iterations: value.iterations, + accumulator: value.accumulator, + initial: String::new(), + bits: value.bits, + kind: ExactLoopRecurrenceKind::Fold { + operation: value.operation, + term: value.term, + }, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LoopRecurrence { + pub phi: String, + pub initial: String, + pub latch: String, + pub bits: u32, + pub role: LoopVarRole, + pub kind: LoopRecurrenceKind, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct LoopTransitionSystem { + pub header: u64, + pub exit_target: u64, + pub iterations: u64, + pub counter: String, + pub counter_start: u64, + pub carried_state: Vec, + pub recurrences: Vec, + pub reasons: Vec, +} + +impl LoopTransitionSystem { + pub fn is_exact(&self) -> bool { + self.reasons.is_empty() + && self + .recurrences + .iter() + .all(|recurrence| !matches!(recurrence.kind, LoopRecurrenceKind::Unsupported(_))) + } +} + +#[derive(Debug)] +pub struct LoopSummary<'ctx> { + pub kind: LoopSummaryKind, + pub header: u64, + pub exit_target: Option, + pub iterations: Option, + pub carried_state: Vec, + pub transitions: Vec, + pub exact_recurrences: Vec, + pub exact_folds: Vec, + pub reasons: Vec, + pub resulting_state: Option>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RuntimeLoopBranch { + pub counter: SSAVar, + pub threshold: u64, + pub target: u64, +} + +pub fn bounded_exact_summary<'ctx>( + header: u64, + branch: &RuntimeLoopBranch, + iterations: u64, + resulting_state: SymState<'ctx>, +) -> LoopSummary<'ctx> { + LoopSummary { + kind: LoopSummaryKind::BoundedExact, + header, + exit_target: Some(branch.target), + iterations: Some(iterations), + carried_state: Vec::new(), + transitions: Vec::new(), + exact_recurrences: Vec::new(), + exact_folds: Vec::new(), + reasons: Vec::new(), + resulting_state: Some(resulting_state), + } +} + +pub fn summarize_residual_runtime_loop<'ctx>( + ctx: &'ctx Context, + block_func: &SsaArtifact, + block: &FunctionSSABlock, + state: &SymState<'ctx>, + branch: &RuntimeLoopBranch, + residual_enabled: bool, +) -> LoopSummary<'ctx> { + if state.pending_exception().is_none() { + return refused(block.addr, Some(branch.target), "no_pending_exception"); + } + if !residual_enabled { + return refused(block.addr, Some(branch.target), "residual_disabled"); + } + if state.runtime().runtime_regions.is_empty() { + return residual( + block.addr, + Some(branch.target), + "runtime_loop_missing_materialized_region", + ); + } + if block_func.get_block(branch.target).is_none() { + return residual( + block.addr, + Some(branch.target), + "runtime_loop_missing_exit_target", + ); + } + + let counter_bits = branch.counter.size.saturating_mul(8).max(1); + let counter_key = branch.counter.display_name(); + let Some(counter) = concrete_state_var_at_block_entry(state, block, &branch.counter) else { + return residual( + block.addr, + Some(branch.target), + "runtime_loop_unknown_counter", + ); + }; + if counter >= branch.threshold { + return refused( + block.addr, + Some(branch.target), + "runtime_loop_counter_past_threshold", + ); + } + let Some(iterations) = branch.threshold.checked_sub(counter) else { + return refused( + block.addr, + Some(branch.target), + "runtime_loop_counter_past_threshold", + ); + }; + if iterations < 8 { + return refused( + block.addr, + Some(branch.target), + "runtime_loop_too_small_to_summarize", + ); + } + + let transition_system = + derive_loop_transition_system(block, state, branch, counter, iterations); + if transition_system.is_exact() + && let Some(summary) = apply_exact_transition_system(ctx, state, &transition_system) + { + return summary; + } + + let carried_state = discover_loop_carried_state(block, Some(&branch.counter)); + if carried_state + .iter() + .all(|var| var.role == LoopVarRole::InductionCounter) + { + return residual( + block.addr, + Some(branch.target), + "runtime_loop_unknown_carried_state", + ); + } + if carried_state + .iter() + .any(|var| var.role == LoopVarRole::Unknown) + { + let mut summary = residual( + block.addr, + Some(branch.target), + "runtime_loop_unknown_carried_state", + ); + summary.carried_state = carried_state; + summary.transitions = discover_loop_transitions(block, &summary.carried_state); + return summary; + } + + let transitions = discover_loop_transitions(block, &carried_state); + let mut summarized = state.fork(); + summarized.set_register( + &counter_key, + SymValue::concrete(branch.threshold, counter_bits), + ); + for carried in &carried_state { + if carried.role == LoopVarRole::InductionCounter { + continue; + } + let summary_name = format!( + "runtime_loop_summary_{:x}_{:x}_{:x}_{}", + block.addr, counter, branch.threshold, carried.name + ); + summarized.set_register( + &carried.name, + SymValue::new_symbolic(ctx, &summary_name, carried.bits), + ); + } + summarized.pc = branch.target; + summarized.set_prev_pc(Some(block.addr)); + + LoopSummary { + kind: LoopSummaryKind::Residual, + header: block.addr, + exit_target: Some(branch.target), + iterations: Some(iterations), + carried_state, + transitions, + exact_recurrences: Vec::new(), + exact_folds: Vec::new(), + reasons: vec!["runtime_loop_summary_residual".to_string()], + resulting_state: Some(summarized), + } +} + +pub fn derive_loop_transition_system<'ctx>( + block: &FunctionSSABlock, + state: &SymState<'ctx>, + branch: &RuntimeLoopBranch, + counter: u64, + iterations: u64, +) -> LoopTransitionSystem { + let carried_state = discover_loop_carried_state(block, Some(&branch.counter)); + let mut reasons = Vec::new(); + let mut recurrences = Vec::new(); + + if block_has_memory_writes(block) { + push_unique_reason( + &mut reasons, + "runtime_loop_memory_write_transition_unsupported", + ); + } + + if state_value_at_block_entry(state, block, &branch.counter).is_none() { + push_unique_reason(&mut reasons, "runtime_loop_unknown_counter"); + } + + if counter.saturating_add(iterations) != branch.threshold { + push_unique_reason(&mut reasons, "runtime_loop_non_affine_exit_count"); + } + + for carried in &carried_state { + let Some(phi) = block + .phis + .iter() + .find(|phi| phi.dst.display_name() == carried.name) + else { + push_unique_reason( + &mut reasons, + format!("runtime_loop_missing_phi:{}", carried.name), + ); + continue; + }; + let Some(initial_source) = selected_phi_source(phi, state.prev_pc()) else { + push_unique_reason( + &mut reasons, + format!("runtime_loop_missing_initial_source:{}", carried.name), + ); + continue; + }; + let Some(latch_source) = latch_source_for_phi(phi, state.prev_pc()) else { + push_unique_reason( + &mut reasons, + format!("runtime_loop_missing_latch_source:{}", carried.name), + ); + continue; + }; + if state_value_at_block_entry(state, block, &phi.dst).is_none() { + push_unique_reason( + &mut reasons, + format!("runtime_loop_unknown_initial:{}", carried.name), + ); + continue; + } + let recurrence = recurrence_for_latch(block, state, &phi.dst, latch_source, carried.role); + let recurrence = LoopRecurrence { + initial: initial_source.display_name(), + ..recurrence + }; + if matches!(recurrence.kind, LoopRecurrenceKind::Unsupported(_)) { + push_unique_reason( + &mut reasons, + format!("runtime_loop_unsupported_recurrence:{}", carried.name), + ); + } + if let Some(term) = recurrence_memory_term(&recurrence.kind) { + if term.base.is_none() || term.stride.is_none() { + push_unique_reason( + &mut reasons, + format!("runtime_loop_unknown_memory_term:{}", carried.name), + ); + } + if term.kind == LoopMemoryTermKind::Unknown { + push_unique_reason( + &mut reasons, + format!("runtime_loop_unknown_memory_provenance:{}", carried.name), + ); + } + if !memory_term_has_exact_bounds(state, term, iterations) { + push_unique_reason( + &mut reasons, + format!("runtime_loop_memory_bounds_unknown:{}", carried.name), + ); + } + } + recurrences.push(recurrence); + } + + recurrences.sort_by(|lhs, rhs| lhs.phi.cmp(&rhs.phi)); + LoopTransitionSystem { + header: block.addr, + exit_target: branch.target, + iterations, + counter: branch.counter.display_name(), + counter_start: counter, + carried_state, + recurrences, + reasons, + } +} + +pub fn runtime_counter_threshold_branch(block: &FunctionSSABlock) -> Option { + let mut copies: HashMap = HashMap::new(); + let mut unsigned_less_defs: HashMap = HashMap::new(); + let mut inverted_less_defs: HashMap = HashMap::new(); + + for op in &block.ops { + match op { + SSAOp::Copy { dst, src } => { + copies.insert(dst.display_name(), resolve_copy_source(src, &copies)); + } + SSAOp::IntLess { dst, a, b } => { + if let Some(threshold) = parse_const_var(b) { + unsigned_less_defs.insert( + dst.display_name(), + (resolve_copy_source(a, &copies), threshold), + ); + } + } + SSAOp::BoolNot { dst, src } => { + if let Some((counter, threshold)) = unsigned_less_defs.get(&src.display_name()) { + inverted_less_defs.insert(dst.display_name(), (counter.clone(), *threshold)); + } + } + SSAOp::CBranch { target, cond } => { + let target = parse_address_var(target)?; + if target <= block.addr.saturating_add(block.size as u64) { + return None; + } + let (counter, threshold) = inverted_less_defs.get(&cond.display_name())?; + return Some(RuntimeLoopBranch { + counter: counter.clone(), + threshold: *threshold, + target, + }); + } + _ => {} + } + } + + None +} + +pub fn exact_recurrence_evidence_from_system( + system: &LoopTransitionSystem, +) -> Vec { + if !system.is_exact() { + return Vec::new(); + } + let mut evidence = system + .recurrences + .iter() + .filter_map(|recurrence| match &recurrence.kind { + LoopRecurrenceKind::AddConst(value) => Some(ExactLoopRecurrenceEvidence { + header: system.header, + exit_target: system.exit_target, + iterations: system.iterations, + accumulator: recurrence.phi.clone(), + initial: recurrence.initial.clone(), + bits: recurrence.bits, + kind: ExactLoopRecurrenceKind::AddConst(*value), + }), + LoopRecurrenceKind::SubConst(value) => Some(ExactLoopRecurrenceEvidence { + header: system.header, + exit_target: system.exit_target, + iterations: system.iterations, + accumulator: recurrence.phi.clone(), + initial: recurrence.initial.clone(), + bits: recurrence.bits, + kind: ExactLoopRecurrenceKind::SubConst(*value), + }), + LoopRecurrenceKind::AffineConst { multiplier, addend } => { + Some(ExactLoopRecurrenceEvidence { + header: system.header, + exit_target: system.exit_target, + iterations: system.iterations, + accumulator: recurrence.phi.clone(), + initial: recurrence.initial.clone(), + bits: recurrence.bits, + kind: ExactLoopRecurrenceKind::AffineConst { + multiplier: *multiplier, + addend: *addend, + }, + }) + } + LoopRecurrenceKind::XorConst(value) => Some(ExactLoopRecurrenceEvidence { + header: system.header, + exit_target: system.exit_target, + iterations: system.iterations, + accumulator: recurrence.phi.clone(), + initial: recurrence.initial.clone(), + bits: recurrence.bits, + kind: ExactLoopRecurrenceKind::XorConst(*value), + }), + LoopRecurrenceKind::AddMemoryFold(term) => Some(ExactLoopRecurrenceEvidence { + header: system.header, + exit_target: system.exit_target, + iterations: system.iterations, + accumulator: recurrence.phi.clone(), + initial: recurrence.initial.clone(), + bits: recurrence.bits, + kind: ExactLoopRecurrenceKind::Fold { + operation: LoopFoldOperation::Add, + term: term.clone(), + }, + }), + LoopRecurrenceKind::XorMemoryFold(term) => Some(ExactLoopRecurrenceEvidence { + header: system.header, + exit_target: system.exit_target, + iterations: system.iterations, + accumulator: recurrence.phi.clone(), + initial: recurrence.initial.clone(), + bits: recurrence.bits, + kind: ExactLoopRecurrenceKind::Fold { + operation: LoopFoldOperation::Xor, + term: term.clone(), + }, + }), + LoopRecurrenceKind::RotateMix { + direction, + amount, + operation, + term, + } => Some(ExactLoopRecurrenceEvidence { + header: system.header, + exit_target: system.exit_target, + iterations: system.iterations, + accumulator: recurrence.phi.clone(), + initial: recurrence.initial.clone(), + bits: recurrence.bits, + kind: ExactLoopRecurrenceKind::RotateMix { + direction: *direction, + amount: *amount, + operation: *operation, + term: term.clone(), + }, + }), + _ => None, + }) + .collect::>(); + evidence.sort_by(|lhs, rhs| { + ( + lhs.header, + lhs.exit_target, + lhs.accumulator.as_str(), + recurrence_sort_key(&lhs.kind), + ) + .cmp(&( + rhs.header, + rhs.exit_target, + rhs.accumulator.as_str(), + recurrence_sort_key(&rhs.kind), + )) + }); + evidence +} + +fn recurrence_sort_key(kind: &ExactLoopRecurrenceKind) -> String { + match kind { + ExactLoopRecurrenceKind::AddConst(value) => format!("add_const:{value:x}"), + ExactLoopRecurrenceKind::SubConst(value) => format!("sub_const:{value:x}"), + ExactLoopRecurrenceKind::AffineConst { multiplier, addend } => { + format!("affine:{multiplier:x}:{addend:x}") + } + ExactLoopRecurrenceKind::XorConst(value) => format!("xor_const:{value:x}"), + ExactLoopRecurrenceKind::Fold { operation, term } => { + format!("fold:{operation:?}:{}", term.addr) + } + ExactLoopRecurrenceKind::RotateMix { + direction, + amount, + operation, + term, + } => format!( + "rotate_mix:{direction:?}:{amount}:{operation:?}:{}", + term.addr + ), + } +} + +fn recurrence_memory_term(kind: &LoopRecurrenceKind) -> Option<&LoopMemoryTerm> { + match kind { + LoopRecurrenceKind::AddMemoryFold(term) + | LoopRecurrenceKind::XorMemoryFold(term) + | LoopRecurrenceKind::RotateMix { term, .. } => Some(term), + _ => None, + } +} + +pub fn exact_fold_evidence_from_recurrences( + recurrences: &[ExactLoopRecurrenceEvidence], +) -> Vec { + let mut folds = recurrences + .iter() + .filter_map(ExactLoopRecurrenceEvidence::as_fold) + .collect::>(); + folds.sort_by(|lhs, rhs| { + ( + lhs.header, + lhs.exit_target, + lhs.accumulator.as_str(), + lhs.term.addr.as_str(), + ) + .cmp(&( + rhs.header, + rhs.exit_target, + rhs.accumulator.as_str(), + rhs.term.addr.as_str(), + )) + }); + folds +} + +pub fn exact_fold_evidence_from_system( + system: &LoopTransitionSystem, +) -> Vec { + exact_fold_evidence_from_recurrences(&exact_recurrence_evidence_from_system(system)) +} + +pub fn discover_loop_carried_state( + block: &FunctionSSABlock, + counter: Option<&SSAVar>, +) -> Vec { + let mut vars = Vec::new(); + let counter_name = counter.map(SSAVar::display_name); + for phi in &block.phis { + let phi_name = phi.dst.display_name(); + let latch_sources = phi + .sources + .iter() + .map(|(_, source)| source) + .filter(|source| source.display_name() != phi_name) + .collect::>(); + if latch_sources.is_empty() { + continue; + } + let role = if counter_name.as_deref() == Some(phi_name.as_str()) + || latch_sources + .iter() + .any(|source| counter_name.as_deref() == Some(source.display_name().as_str())) + { + LoopVarRole::InductionCounter + } else if used_as_memory_value(block, &phi.dst, &latch_sources) { + LoopVarRole::MemoryEffect + } else if used_as_memory_addr(block, &phi.dst, &latch_sources) { + LoopVarRole::Pointer + } else if latch_update_depends_on_phi(block, &phi.dst, &latch_sources) { + LoopVarRole::Accumulator + } else { + LoopVarRole::Unknown + }; + vars.push(LoopCarriedVar { + name: phi_name, + bits: phi.dst.size.saturating_mul(8).max(1), + role, + }); + } + vars.sort_by(|lhs, rhs| lhs.name.cmp(&rhs.name)); + vars +} + +pub fn concrete_state_var_at_block_entry<'ctx>( + state: &SymState<'ctx>, + block: &FunctionSSABlock, + var: &SSAVar, +) -> Option { + let phi = block + .phis + .iter() + .find(|phi| phi.dst.display_name() == var.display_name()); + if let Some(phi) = phi { + let prev_pc = state.prev_pc()?; + return phi + .sources + .iter() + .find(|(pred, _)| *pred == prev_pc) + .and_then(|(_, source)| concrete_state_var(state, source)); + } + + concrete_state_var(state, var) +} + +fn refused<'ctx>(header: u64, exit_target: Option, reason: &str) -> LoopSummary<'ctx> { + LoopSummary { + kind: LoopSummaryKind::Refused, + header, + exit_target, + iterations: None, + carried_state: Vec::new(), + transitions: Vec::new(), + exact_recurrences: Vec::new(), + exact_folds: Vec::new(), + reasons: vec![reason.to_string()], + resulting_state: None, + } +} + +fn residual<'ctx>(header: u64, exit_target: Option, reason: &str) -> LoopSummary<'ctx> { + LoopSummary { + kind: LoopSummaryKind::Residual, + header, + exit_target, + iterations: None, + carried_state: Vec::new(), + transitions: Vec::new(), + exact_recurrences: Vec::new(), + exact_folds: Vec::new(), + reasons: vec![reason.to_string()], + resulting_state: None, + } +} + +fn apply_exact_transition_system<'ctx>( + ctx: &'ctx Context, + state: &SymState<'ctx>, + system: &LoopTransitionSystem, +) -> Option> { + let mut summarized = state.fork(); + let counter_bits = state.get_register_sized(&system.counter, 64).bits().max(1); + summarized.set_register( + &system.counter, + SymValue::concrete( + system.iterations.saturating_add(system.counter_start), + counter_bits, + ), + ); + + for recurrence in &system.recurrences { + let initial = value_for_name(state, &recurrence.initial, recurrence.bits)?; + let value = apply_recurrence(ctx, state, initial, recurrence, system.iterations)?; + summarized.set_register(&recurrence.phi, value); + summarized.set_register( + &recurrence.latch, + summarized.get_register_sized(&recurrence.phi, recurrence.bits), + ); + } + summarized.pc = system.exit_target; + summarized.set_prev_pc(Some(system.header)); + let exact_recurrences = exact_recurrence_evidence_from_system(system); + let exact_folds = exact_fold_evidence_from_recurrences(&exact_recurrences); + + Some(LoopSummary { + kind: LoopSummaryKind::Exact, + header: system.header, + exit_target: Some(system.exit_target), + iterations: Some(system.iterations), + carried_state: system.carried_state.clone(), + transitions: system + .recurrences + .iter() + .map(recurrence_to_transition) + .collect(), + exact_recurrences, + exact_folds, + reasons: Vec::new(), + resulting_state: Some(summarized), + }) +} + +fn apply_recurrence<'ctx>( + ctx: &'ctx Context, + state: &SymState<'ctx>, + initial: SymValue<'ctx>, + recurrence: &LoopRecurrence, + iterations: u64, +) -> Option> { + let bits = recurrence.bits.max(1); + match recurrence.kind { + LoopRecurrenceKind::Identity => Some(initial), + LoopRecurrenceKind::AddConst(value) => { + let delta = mul_mod_width(value, iterations, bits); + Some(initial.add(ctx, &SymValue::concrete(delta, bits))) + } + LoopRecurrenceKind::SubConst(value) => { + let delta = mul_mod_width(value, iterations, bits); + Some(initial.sub(ctx, &SymValue::concrete(delta, bits))) + } + LoopRecurrenceKind::AffineConst { multiplier, addend } => { + let (scale, bias) = affine_transform_pow(multiplier, addend, iterations, bits); + let scaled = initial.mul(ctx, &SymValue::concrete(scale, bits)); + Some(if bias == 0 { + scaled + } else { + scaled.add(ctx, &SymValue::concrete(bias, bits)) + }) + } + LoopRecurrenceKind::XorConst(value) => { + if iterations.is_multiple_of(2) { + Some(initial) + } else { + Some(initial.xor(ctx, &SymValue::concrete(value, bits))) + } + } + LoopRecurrenceKind::AddMemoryFold(ref term) => { + let folded = fold_memory_term(ctx, state, term, iterations, bits, true)?; + Some(initial.add(ctx, &folded)) + } + LoopRecurrenceKind::XorMemoryFold(ref term) => { + let folded = fold_memory_term(ctx, state, term, iterations, bits, false)?; + Some(initial.xor(ctx, &folded)) + } + LoopRecurrenceKind::RotateMix { + direction, + amount, + operation, + ref term, + } => apply_rotate_mix_recurrence( + ctx, + state, + initial, + term, + iterations, + bits, + RotateMixSpec { + direction, + amount, + operation, + }, + ), + LoopRecurrenceKind::Unsupported(_) => None, + } +} + +fn recurrence_to_transition(recurrence: &LoopRecurrence) -> LoopTransitionExpr { + match recurrence.kind { + LoopRecurrenceKind::Identity => LoopTransitionExpr::Identity(recurrence.phi.clone()), + LoopRecurrenceKind::AddConst(value) => LoopTransitionExpr::AddConst { + var: recurrence.phi.clone(), + value, + }, + LoopRecurrenceKind::SubConst(value) => LoopTransitionExpr::AddConst { + var: recurrence.phi.clone(), + value: value.wrapping_neg(), + }, + LoopRecurrenceKind::AffineConst { multiplier, addend } => LoopTransitionExpr::AffineConst { + var: recurrence.phi.clone(), + multiplier, + addend, + }, + LoopRecurrenceKind::XorConst(value) => LoopTransitionExpr::XorConst { + var: recurrence.phi.clone(), + value, + }, + LoopRecurrenceKind::AddMemoryFold(ref term) + | LoopRecurrenceKind::XorMemoryFold(ref term) => { + LoopTransitionExpr::TableRead(term.clone()) + } + LoopRecurrenceKind::RotateMix { + direction, + amount, + operation, + ref term, + } => LoopTransitionExpr::RotateMix { + var: recurrence.phi.clone(), + direction, + amount, + operation, + term: term.clone(), + }, + LoopRecurrenceKind::Unsupported(_) => LoopTransitionExpr::Unknown, + } +} + +fn discover_loop_transitions( + block: &FunctionSSABlock, + carried_state: &[LoopCarriedVar], +) -> Vec { + let carried = carried_state + .iter() + .map(|var| var.name.as_str()) + .collect::>(); + let mut transitions = Vec::new(); + for op in &block.ops { + match op { + SSAOp::IntAdd { dst, a, b } if carried.contains(dst.display_name().as_str()) => { + transitions.push(binary_transition( + LoopTransitionExpr::AddVar { + lhs: a.display_name(), + rhs: b.display_name(), + }, + a, + b, + |var, value| LoopTransitionExpr::AddConst { var, value }, + )); + } + SSAOp::IntXor { dst, a, b } if carried.contains(dst.display_name().as_str()) => { + transitions.push(binary_transition( + LoopTransitionExpr::XorVar { + lhs: a.display_name(), + rhs: b.display_name(), + }, + a, + b, + |var, value| LoopTransitionExpr::XorConst { var, value }, + )); + } + SSAOp::Load { dst, addr, .. } if carried.contains(dst.display_name().as_str()) => { + transitions.push(LoopTransitionExpr::Load { + addr: addr.display_name(), + bytes: dst.size, + }); + } + SSAOp::Store { addr, val, .. } => { + transitions.push(LoopTransitionExpr::Store { + addr: addr.display_name(), + value: val.display_name(), + }); + } + _ => {} + } + } + if transitions.is_empty() && !carried_state.is_empty() { + transitions.push(LoopTransitionExpr::Unknown); + } + transitions +} + +fn binary_transition( + fallback: LoopTransitionExpr, + a: &SSAVar, + b: &SSAVar, + make_const: F, +) -> LoopTransitionExpr +where + F: Fn(String, u64) -> LoopTransitionExpr, +{ + if let Some(value) = parse_const_var(a) { + make_const(b.display_name(), value) + } else if let Some(value) = parse_const_var(b) { + make_const(a.display_name(), value) + } else { + fallback + } +} + +fn recurrence_for_latch( + block: &FunctionSSABlock, + state: &SymState<'_>, + phi: &SSAVar, + latch: &SSAVar, + role: LoopVarRole, +) -> LoopRecurrence { + let phi_name = phi.display_name(); + let latch_name = latch.display_name(); + let bits = phi.size.saturating_mul(8).max(1); + let kind = if phi_name == latch_name { + LoopRecurrenceKind::Identity + } else { + block + .ops + .iter() + .find(|op| op.dst().is_some_and(|dst| dst.display_name() == latch_name)) + .map(|op| recurrence_kind_for_op(block, state, op, &phi_name)) + .unwrap_or_else(|| { + LoopRecurrenceKind::Unsupported("missing_latch_definition".to_string()) + }) + }; + + LoopRecurrence { + phi: phi_name, + initial: String::new(), + latch: latch_name, + bits, + role, + kind, + } +} + +fn recurrence_kind_for_op( + block: &FunctionSSABlock, + state: &SymState<'_>, + op: &SSAOp, + phi_name: &str, +) -> LoopRecurrenceKind { + let bits = op + .dst() + .map(|dst| dst.size.saturating_mul(8).max(1)) + .unwrap_or(64); + if let Some(kind) = affine_recurrence_kind_for_op(block, op, phi_name, bits) { + return kind; + } + match op { + SSAOp::Copy { src, .. } if src.display_name() == phi_name => LoopRecurrenceKind::Identity, + SSAOp::IntAdd { a, b, .. } => rotate_mix_recurrence_kind_for_op( + block, + state, + a, + b, + phi_name, + bits, + LoopFoldOperation::Add, + ) + .unwrap_or_else(|| recurrence_memory_or_const_fold(block, state, a, b, phi_name, true)), + SSAOp::IntSub { a, b, .. } if a.display_name() == phi_name => parse_const_var(b) + .map(LoopRecurrenceKind::SubConst) + .unwrap_or_else(|| LoopRecurrenceKind::Unsupported("sub_non_const".to_string())), + SSAOp::IntXor { a, b, .. } => rotate_mix_recurrence_kind_for_op( + block, + state, + a, + b, + phi_name, + bits, + LoopFoldOperation::Xor, + ) + .unwrap_or_else(|| recurrence_memory_or_const_fold(block, state, a, b, phi_name, false)), + _ => LoopRecurrenceKind::Unsupported("unsupported_latch_op".to_string()), + } +} + +fn affine_recurrence_kind_for_op( + block: &FunctionSSABlock, + op: &SSAOp, + phi_name: &str, + bits: u32, +) -> Option { + let mut visited = HashSet::new(); + let (multiplier, addend) = + parse_affine_recurrence_op(block, op, phi_name, bits, 8, &mut visited)?; + Some(affine_kind_from_parts(multiplier, addend, bits)) +} + +fn parse_affine_recurrence_op( + block: &FunctionSSABlock, + op: &SSAOp, + phi_name: &str, + bits: u32, + depth: u8, + visited: &mut HashSet, +) -> Option<(u64, u64)> { + if depth == 0 { + return None; + } + match op { + SSAOp::Copy { src, .. } => { + parse_affine_recurrence_var(block, src, phi_name, bits, depth - 1, visited) + } + SSAOp::IntAdd { a, b, .. } => { + let lhs = parse_affine_recurrence_var(block, a, phi_name, bits, depth - 1, visited)?; + let rhs = parse_affine_recurrence_var(block, b, phi_name, bits, depth - 1, visited)?; + Some(affine_add_parts(lhs, rhs, bits)) + } + SSAOp::IntSub { a, b, .. } => { + let lhs = parse_affine_recurrence_var(block, a, phi_name, bits, depth - 1, visited)?; + let rhs = parse_affine_recurrence_var(block, b, phi_name, bits, depth - 1, visited)?; + Some(affine_sub_parts(lhs, rhs, bits)) + } + SSAOp::IntMult { a, b, .. } => { + if let Some(scale) = parse_const_var(a) { + let rhs = + parse_affine_recurrence_var(block, b, phi_name, bits, depth - 1, visited)?; + return Some(affine_scale_parts(rhs, scale, bits)); + } + if let Some(scale) = parse_const_var(b) { + let lhs = + parse_affine_recurrence_var(block, a, phi_name, bits, depth - 1, visited)?; + return Some(affine_scale_parts(lhs, scale, bits)); + } + None + } + _ => None, + } +} + +fn parse_affine_recurrence_var( + block: &FunctionSSABlock, + value: &SSAVar, + phi_name: &str, + bits: u32, + depth: u8, + visited: &mut HashSet, +) -> Option<(u64, u64)> { + if depth == 0 { + return None; + } + if value.display_name() == phi_name { + return Some((1, 0)); + } + if let Some(constant) = parse_const_var(value) { + return Some((0, constant & mask_for_bits(bits))); + } + + let name = value.display_name(); + if !visited.insert(name.clone()) { + return None; + } + let result = block + .ops + .iter() + .find(|op| op.dst().is_some_and(|dst| dst.display_name() == name)) + .and_then(|op| parse_affine_recurrence_op(block, op, phi_name, bits, depth - 1, visited)); + visited.remove(&name); + result +} + +fn affine_kind_from_parts(multiplier: u64, addend: u64, bits: u32) -> LoopRecurrenceKind { + let mask = mask_for_bits(bits); + let multiplier = multiplier & mask; + let addend = addend & mask; + if multiplier == 1 { + if addend == 0 { + LoopRecurrenceKind::Identity + } else { + let subtraction = addend.wrapping_neg() & mask; + if subtraction != 0 && subtraction < addend { + LoopRecurrenceKind::SubConst(subtraction) + } else { + LoopRecurrenceKind::AddConst(addend) + } + } + } else { + LoopRecurrenceKind::AffineConst { multiplier, addend } + } +} + +fn affine_add_parts(lhs: (u64, u64), rhs: (u64, u64), bits: u32) -> (u64, u64) { + let mask = mask_for_bits(bits); + ( + lhs.0.wrapping_add(rhs.0) & mask, + lhs.1.wrapping_add(rhs.1) & mask, + ) +} + +fn affine_sub_parts(lhs: (u64, u64), rhs: (u64, u64), bits: u32) -> (u64, u64) { + let mask = mask_for_bits(bits); + ( + lhs.0.wrapping_sub(rhs.0) & mask, + lhs.1.wrapping_sub(rhs.1) & mask, + ) +} + +fn affine_scale_parts(parts: (u64, u64), scale: u64, bits: u32) -> (u64, u64) { + let mask = mask_for_bits(bits); + ( + parts.0.wrapping_mul(scale) & mask, + parts.1.wrapping_mul(scale) & mask, + ) +} + +fn rotate_mix_recurrence_kind_for_op( + block: &FunctionSSABlock, + state: &SymState<'_>, + a: &SSAVar, + b: &SSAVar, + phi_name: &str, + bits: u32, + operation: LoopFoldOperation, +) -> Option { + rotate_mix_kind_from_operands(block, state, a, b, phi_name, bits, operation) + .or_else(|| rotate_mix_kind_from_operands(block, state, b, a, phi_name, bits, operation)) +} + +fn rotate_mix_kind_from_operands( + block: &FunctionSSABlock, + state: &SymState<'_>, + rotate_operand: &SSAVar, + mix_operand: &SSAVar, + phi_name: &str, + bits: u32, + operation: LoopFoldOperation, +) -> Option { + let mut visited = HashSet::new(); + let (direction, amount) = + parse_rotate_source_for_var(block, rotate_operand, phi_name, bits, 8, &mut visited)?; + let term = memory_term_for_var(block, state, mix_operand)?; + Some(LoopRecurrenceKind::RotateMix { + direction, + amount, + operation, + term, + }) +} + +fn parse_rotate_source_for_var( + block: &FunctionSSABlock, + value: &SSAVar, + phi_name: &str, + bits: u32, + depth: u8, + visited: &mut HashSet, +) -> Option<(LoopRotateDirection, u32)> { + if depth == 0 { + return None; + } + let name = value.display_name(); + let op = lookup_def_op_by_name(block, &name)?; + if !visited.insert(name.clone()) { + return None; + } + let result = match op { + SSAOp::Copy { src, .. } => { + parse_rotate_source_for_var(block, src, phi_name, bits, depth - 1, visited) + } + SSAOp::IntOr { dst, a, b } if dst.size.saturating_mul(8).max(1) == bits => { + parse_rotate_or_term(block, a, b, phi_name, bits, depth - 1) + } + _ => None, + }; + visited.remove(&name); + result +} + +fn parse_rotate_or_term( + block: &FunctionSSABlock, + a: &SSAVar, + b: &SSAVar, + phi_name: &str, + bits: u32, + depth: u8, +) -> Option<(LoopRotateDirection, u32)> { + let lhs = parse_rotate_shift_term(block, a, phi_name, bits, depth)?; + let rhs = parse_rotate_shift_term(block, b, phi_name, bits, depth)?; + if lhs.shift == 0 || rhs.shift == 0 || lhs.shift.saturating_add(rhs.shift) != bits { + return None; + } + match (lhs.direction, rhs.direction) { + (LoopRotateDirection::Left, LoopRotateDirection::Right) => { + Some((LoopRotateDirection::Left, lhs.shift)) + } + (LoopRotateDirection::Right, LoopRotateDirection::Left) => { + Some((LoopRotateDirection::Right, lhs.shift)) + } + _ => None, + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct RotateShiftTerm { + direction: LoopRotateDirection, + shift: u32, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct RotateMixSpec { + direction: LoopRotateDirection, + amount: u32, + operation: LoopFoldOperation, +} + +fn parse_rotate_shift_term( + block: &FunctionSSABlock, + value: &SSAVar, + phi_name: &str, + bits: u32, + depth: u8, +) -> Option { + if depth == 0 { + return None; + } + let op = lookup_def_op_by_name(block, &value.display_name())?; + match op { + SSAOp::IntLeft { dst, a, b } if dst.size.saturating_mul(8).max(1) == bits => { + let shift = normalize_rotate_amount(parse_const_var(b)? as u32, bits); + let mut inner_visited = HashSet::new(); + value_matches_phi_source(block, a, phi_name, depth - 1, &mut inner_visited).then_some( + RotateShiftTerm { + direction: LoopRotateDirection::Left, + shift, + }, + ) + } + SSAOp::IntRight { dst, a, b } if dst.size.saturating_mul(8).max(1) == bits => { + let shift = normalize_rotate_amount(parse_const_var(b)? as u32, bits); + let mut inner_visited = HashSet::new(); + value_matches_phi_source(block, a, phi_name, depth - 1, &mut inner_visited).then_some( + RotateShiftTerm { + direction: LoopRotateDirection::Right, + shift, + }, + ) + } + SSAOp::Copy { src, .. } => parse_rotate_shift_term(block, src, phi_name, bits, depth - 1), + _ => None, + } +} + +fn value_matches_phi_source( + block: &FunctionSSABlock, + value: &SSAVar, + phi_name: &str, + depth: u8, + visited: &mut HashSet, +) -> bool { + if depth == 0 { + return false; + } + if value.display_name() == phi_name { + return true; + } + let name = value.display_name(); + let Some(op) = lookup_def_op_by_name(block, &name) else { + return false; + }; + if !visited.insert(name.clone()) { + return false; + } + let result = match op { + SSAOp::Copy { src, .. } => { + value_matches_phi_source(block, src, phi_name, depth - 1, visited) + } + _ => false, + }; + visited.remove(&name); + result +} + +fn lookup_def_op_by_name<'a>(block: &'a FunctionSSABlock, name: &str) -> Option<&'a SSAOp> { + block + .ops + .iter() + .find(|op| op.dst().is_some_and(|dst| dst.display_name() == name)) +} + +fn recurrence_memory_or_const_fold( + block: &FunctionSSABlock, + state: &SymState<'_>, + a: &SSAVar, + b: &SSAVar, + phi_name: &str, + additive: bool, +) -> LoopRecurrenceKind { + let other = if a.display_name() == phi_name { + b + } else if b.display_name() == phi_name { + a + } else { + return LoopRecurrenceKind::Unsupported("fold_missing_accumulator".to_string()); + }; + + if let Some(value) = parse_const_var(other) { + return if additive { + LoopRecurrenceKind::AddConst(value) + } else { + LoopRecurrenceKind::XorConst(value) + }; + } + + let Some(term) = memory_term_for_var(block, state, other) else { + return LoopRecurrenceKind::Unsupported("fold_operand_not_memory_term".to_string()); + }; + if additive { + LoopRecurrenceKind::AddMemoryFold(term) + } else { + LoopRecurrenceKind::XorMemoryFold(term) + } +} + +fn memory_term_for_var( + block: &FunctionSSABlock, + state: &SymState<'_>, + value: &SSAVar, +) -> Option { + let (addr, bytes) = block.ops.iter().find_map(|op| match op { + SSAOp::Load { dst, addr, .. } if dst.display_name() == value.display_name() => { + Some((addr, dst.size)) + } + _ => None, + })?; + let base = state_value_at_block_entry(state, block, addr).and_then(|value| value.as_concrete()); + let addr_is_loop_carried = block + .phis + .iter() + .any(|phi| phi.dst.display_name() == addr.display_name()); + let stride = if addr_is_loop_carried { + memory_stride_for_addr_var(block, state, addr) + } else { + Some(bytes as u64) + }; + let provenance = base.and_then(|base| classify_memory_term_provenance(state, base)); + Some(LoopMemoryTerm { + kind: provenance + .as_ref() + .map(|provenance| provenance.kind) + .unwrap_or(LoopMemoryTermKind::Unknown), + addr: addr.display_name(), + bytes, + base, + stride, + region: provenance + .as_ref() + .map(|provenance| provenance.name.clone()), + region_base: provenance.as_ref().map(|provenance| provenance.base), + region_size: provenance.as_ref().map(|provenance| provenance.size), + }) +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct LoopMemoryProvenance { + kind: LoopMemoryTermKind, + name: String, + base: u64, + size: u64, +} + +fn classify_memory_term_provenance( + state: &SymState<'_>, + base: u64, +) -> Option { + if let Some(region) = state + .symbolic_memory() + .iter() + .find(|region| base >= region.addr && base < region.addr.saturating_add(region.size as u64)) + { + let kind = if region.name.contains("argv") + || region.name.contains("stdin") + || region.name.contains("input") + { + LoopMemoryTermKind::InputRead + } else { + LoopMemoryTermKind::TableRead + }; + return Some(LoopMemoryProvenance { + kind, + name: region.name.clone(), + base: region.addr, + size: region.size as u64, + }); + }; + + if let Some(region) = state.runtime_region_for_pc(base) { + return Some(LoopMemoryProvenance { + kind: LoopMemoryTermKind::RuntimeBlobRead, + name: format!("runtime:{:x}", region.runtime_base), + base: region.runtime_base, + size: region.size, + }); + } + + if state.is_concrete_memory_range(base, 1) { + return Some(LoopMemoryProvenance { + kind: LoopMemoryTermKind::TableRead, + name: format!("concrete:{base:x}"), + base, + size: 0, + }); + } + + None +} + +fn memory_stride_for_addr_var( + block: &FunctionSSABlock, + state: &SymState<'_>, + addr: &SSAVar, +) -> Option { + let phi = block + .phis + .iter() + .find(|phi| phi.dst.display_name() == addr.display_name())?; + let latch = latch_source_for_phi(phi, state.prev_pc())?; + let latch_name = latch.display_name(); + block + .ops + .iter() + .find(|op| op.dst().is_some_and(|dst| dst.display_name() == latch_name)) + .and_then(|op| match op { + SSAOp::IntAdd { a, b, .. } if a.display_name() == addr.display_name() => { + parse_const_var(b) + } + SSAOp::IntAdd { a, b, .. } if b.display_name() == addr.display_name() => { + parse_const_var(a) + } + _ => None, + }) +} + +fn memory_term_has_exact_bounds( + state: &SymState<'_>, + term: &LoopMemoryTerm, + iterations: u64, +) -> bool { + let (Some(base), Some(stride)) = (term.base, term.stride) else { + return false; + }; + if iterations == 0 { + return true; + } + let Some(last_offset) = (iterations - 1).checked_mul(stride) else { + return false; + }; + let Some(last_addr) = base.checked_add(last_offset) else { + return false; + }; + let Some(last_end) = last_addr.checked_add(term.bytes as u64) else { + return false; + }; + + match term.kind { + LoopMemoryTermKind::InputRead | LoopMemoryTermKind::RuntimeBlobRead => { + let (Some(region_base), Some(region_size)) = (term.region_base, term.region_size) + else { + return false; + }; + let Some(region_end) = region_base.checked_add(region_size) else { + return false; + }; + base >= region_base && last_end <= region_end + } + LoopMemoryTermKind::TableRead => (0..iterations).all(|iteration| { + let Some(offset) = iteration.checked_mul(stride) else { + return false; + }; + let Some(addr) = base.checked_add(offset) else { + return false; + }; + state.is_concrete_memory_range(addr, term.bytes) + }), + LoopMemoryTermKind::Unknown => false, + } +} + +fn selected_phi_source(phi: &r2ssa::PhiNode, prev_pc: Option) -> Option<&SSAVar> { + let prev_pc = prev_pc?; + phi.sources + .iter() + .find(|(pred, _)| *pred == prev_pc) + .map(|(_, source)| source) +} + +fn latch_source_for_phi(phi: &r2ssa::PhiNode, selected_prev_pc: Option) -> Option<&SSAVar> { + phi.sources + .iter() + .rev() + .find(|(pred, source)| { + Some(*pred) != selected_prev_pc && source.display_name() != phi.dst.display_name() + }) + .map(|(_, source)| source) + .or_else(|| { + phi.sources + .iter() + .rev() + .find(|(_, source)| source.display_name() != phi.dst.display_name()) + .map(|(_, source)| source) + }) +} + +fn block_has_memory_writes(block: &FunctionSSABlock) -> bool { + block.ops.iter().any(|op| { + matches!( + op, + SSAOp::Store { .. } + | SSAOp::StoreConditional { .. } + | SSAOp::AtomicCAS { .. } + | SSAOp::StoreGuarded { .. } + ) + }) +} + +fn fold_memory_term<'ctx>( + ctx: &'ctx Context, + state: &SymState<'ctx>, + term: &LoopMemoryTerm, + iterations: u64, + bits: u32, + additive: bool, +) -> Option> { + let base = term.base?; + let stride = term.stride?; + let mut concrete: u64 = 0; + let mut symbolic: Option> = None; + for iteration in 0..iterations { + let offset = iteration.checked_mul(stride)?; + let addr = base.checked_add(offset)?; + let value = state.mem_read(&SymValue::concrete(addr, 64), term.bytes); + if let Some(item) = value.as_concrete() { + concrete = if additive { + concrete.wrapping_add(item) + } else { + concrete ^ item + }; + continue; + } + symbolic = Some(match symbolic { + Some(acc) if additive => acc.add(ctx, &value), + Some(acc) => acc.xor(ctx, &value), + None => value, + }); + } + let concrete_value = SymValue::concrete(concrete & mask_for_bits(bits), bits); + Some(match symbolic { + Some(value) if additive => value.add(ctx, &concrete_value), + Some(value) => value.xor(ctx, &concrete_value), + None => concrete_value, + }) +} + +fn apply_rotate_mix_recurrence<'ctx>( + ctx: &'ctx Context, + state: &SymState<'ctx>, + initial: SymValue<'ctx>, + term: &LoopMemoryTerm, + iterations: u64, + bits: u32, + spec: RotateMixSpec, +) -> Option> { + let base = term.base?; + let stride = term.stride?; + let bits = bits.max(1); + let mut acc = normalize_value_bits(ctx, &initial, bits); + for iteration in 0..iterations { + acc = rotate_value(ctx, &acc, spec.amount, bits, spec.direction); + let offset = iteration.checked_mul(stride)?; + let addr = base.checked_add(offset)?; + let value = state.mem_read(&SymValue::concrete(addr, 64), term.bytes); + acc = match spec.operation { + LoopFoldOperation::Add => acc.add(ctx, &value), + LoopFoldOperation::Xor => acc.xor(ctx, &value), + }; + acc = normalize_value_bits(ctx, &acc, bits); + } + Some(acc) +} + +fn normalize_value_bits<'ctx>( + ctx: &'ctx Context, + value: &SymValue<'ctx>, + bits: u32, +) -> SymValue<'ctx> { + let bits = bits.max(1); + match value.bits().cmp(&bits) { + std::cmp::Ordering::Equal => value.clone(), + std::cmp::Ordering::Less => value.zero_extend(ctx, bits), + std::cmp::Ordering::Greater => value.extract(ctx, bits - 1, 0), + } +} + +fn rotate_value<'ctx>( + ctx: &'ctx Context, + value: &SymValue<'ctx>, + amount: u32, + bits: u32, + direction: LoopRotateDirection, +) -> SymValue<'ctx> { + let bits = bits.max(1); + let amount = normalize_rotate_amount(amount, bits); + let value = normalize_value_bits(ctx, value, bits); + if amount == 0 { + return value; + } + if let Some(concrete) = value.as_concrete() { + return SymValue::concrete( + rotate_concrete_bits(concrete, amount, bits, direction), + bits, + ); + } + let head = match direction { + LoopRotateDirection::Left => value.shl(ctx, &SymValue::concrete(amount as u64, bits)), + LoopRotateDirection::Right => value.lshr(ctx, &SymValue::concrete(amount as u64, bits)), + }; + let tail_amount = bits - amount; + let tail = match direction { + LoopRotateDirection::Left => value.lshr(ctx, &SymValue::concrete(tail_amount as u64, bits)), + LoopRotateDirection::Right => value.shl(ctx, &SymValue::concrete(tail_amount as u64, bits)), + }; + normalize_value_bits(ctx, &head.or(ctx, &tail), bits) +} + +fn rotate_concrete_bits(value: u64, amount: u32, bits: u32, direction: LoopRotateDirection) -> u64 { + let bits = bits.clamp(1, 64); + let value = value & mask_for_bits(bits); + let amount = normalize_rotate_amount(amount, bits); + if amount == 0 { + return value; + } + if bits == 64 { + return match direction { + LoopRotateDirection::Left => value.rotate_left(amount), + LoopRotateDirection::Right => value.rotate_right(amount), + }; + } + let lhs = match direction { + LoopRotateDirection::Left => value.wrapping_shl(amount), + LoopRotateDirection::Right => value >> amount, + } & mask_for_bits(bits); + let rhs = match direction { + LoopRotateDirection::Left => value >> (bits - amount), + LoopRotateDirection::Right => value.wrapping_shl(bits - amount), + } & mask_for_bits(bits); + (lhs | rhs) & mask_for_bits(bits) +} + +fn value_for_name<'ctx>(state: &SymState<'ctx>, name: &str, bits: u32) -> Option> { + if let Some(value) = name + .strip_prefix("const:") + .and_then(|value| u64::from_str_radix(value, 16).ok()) + { + return Some(SymValue::concrete(value, bits.max(1))); + } + let value = state.get_register_sized(name, bits.max(1)); + if value.is_unknown() { + None + } else { + Some(value) + } +} + +fn state_value_at_block_entry<'ctx>( + state: &SymState<'ctx>, + block: &FunctionSSABlock, + var: &SSAVar, +) -> Option> { + let phi = block + .phis + .iter() + .find(|phi| phi.dst.display_name() == var.display_name()); + if let Some(phi) = phi { + return selected_phi_source(phi, state.prev_pc()) + .and_then(|source| value_for_name(state, &source.display_name(), source.size * 8)); + } + + value_for_name( + state, + &var.display_name(), + var.size.saturating_mul(8).max(1), + ) +} + +fn mul_mod_width(value: u64, iterations: u64, bits: u32) -> u64 { + let mask = mask_for_bits(bits); + value.wrapping_mul(iterations) & mask +} + +fn compose_affine_transform(outer: (u64, u64), inner: (u64, u64), bits: u32) -> (u64, u64) { + let mask = mask_for_bits(bits); + let multiplier = outer.0.wrapping_mul(inner.0) & mask; + let addend = outer.0.wrapping_mul(inner.1).wrapping_add(outer.1) & mask; + (multiplier, addend) +} + +fn affine_transform_pow(multiplier: u64, addend: u64, iterations: u64, bits: u32) -> (u64, u64) { + let mask = mask_for_bits(bits); + let mut result = (1u64, 0u64); + let mut base = (multiplier & mask, addend & mask); + let mut exp = iterations; + + while exp != 0 { + if exp & 1 == 1 { + result = compose_affine_transform(base, result, bits); + } + exp >>= 1; + if exp != 0 { + base = compose_affine_transform(base, base, bits); + } + } + + result +} + +fn normalize_rotate_amount(amount: u32, bits: u32) -> u32 { + let bits = bits.min(64); + if bits == 0 { 0 } else { amount % bits } +} + +fn mask_for_bits(bits: u32) -> u64 { + if bits >= 64 { + u64::MAX + } else { + (1u64 << bits.max(1)) - 1 + } +} + +fn push_unique_reason(reasons: &mut Vec, reason: impl Into) { + let reason = reason.into(); + if !reasons.iter().any(|existing| existing == &reason) { + reasons.push(reason); + } +} + +fn used_as_memory_addr(block: &FunctionSSABlock, phi: &SSAVar, latch_sources: &[&SSAVar]) -> bool { + let names = related_names(phi, latch_sources); + block.ops.iter().any(|op| match op { + SSAOp::Load { addr, .. } + | SSAOp::Store { addr, .. } + | SSAOp::LoadLinked { addr, .. } + | SSAOp::StoreConditional { addr, .. } + | SSAOp::LoadGuarded { addr, .. } + | SSAOp::StoreGuarded { addr, .. } => names.contains(addr.display_name().as_str()), + _ => false, + }) +} + +fn used_as_memory_value(block: &FunctionSSABlock, phi: &SSAVar, latch_sources: &[&SSAVar]) -> bool { + let names = related_names(phi, latch_sources); + block.ops.iter().any(|op| match op { + SSAOp::Store { val, .. } + | SSAOp::StoreConditional { val, .. } + | SSAOp::StoreGuarded { val, .. } => names.contains(val.display_name().as_str()), + _ => false, + }) +} + +fn latch_update_depends_on_phi( + block: &FunctionSSABlock, + phi: &SSAVar, + latch_sources: &[&SSAVar], +) -> bool { + let phi_name = phi.display_name(); + latch_sources.iter().any(|source| { + let source_name = source.display_name(); + block.ops.iter().any(|op| { + op.dst() + .is_some_and(|dst| dst.display_name() == source_name) + && op_sources_include(op, &phi_name) + }) + }) +} + +fn op_sources_include(op: &SSAOp, needle: &str) -> bool { + let mut found = false; + op.for_each_source(|source| { + if source.display_name() == needle { + found = true; + } + }); + found +} + +fn related_names<'a>(phi: &'a SSAVar, latch_sources: &[&'a SSAVar]) -> HashSet { + let mut names = HashSet::new(); + names.insert(phi.display_name()); + for source in latch_sources { + names.insert(source.display_name()); + } + names +} + +fn parse_hex_prefixed_var(var: &SSAVar, prefix: &str) -> Option { + var.name + .strip_prefix(prefix) + .and_then(|value| u64::from_str_radix(value, 16).ok()) +} + +fn parse_const_var(var: &SSAVar) -> Option { + parse_hex_prefixed_var(var, "const:") +} + +pub(crate) fn parse_address_var(var: &SSAVar) -> Option { + parse_hex_prefixed_var(var, "ram:").or_else(|| parse_const_var(var)) +} + +fn resolve_copy_source(var: &SSAVar, copies: &HashMap) -> SSAVar { + let mut current = var.clone(); + for _ in 0..8 { + let Some(next) = copies.get(¤t.display_name()) else { + break; + }; + if next.display_name() == current.display_name() { + break; + } + current = next.clone(); + } + current +} + +fn concrete_state_var<'ctx>(state: &SymState<'ctx>, var: &SSAVar) -> Option { + parse_const_var(var).or_else(|| { + state + .get_register_sized(&var.display_name(), var.size.saturating_mul(8).max(1)) + .as_concrete() + }) +} + +#[cfg(test)] +mod tests { + use r2ssa::{FunctionSSABlock, PhiNode, SSAOp, SSAVar}; + use z3::Context; + + use super::{ + ExactLoopRecurrenceKind, LoopFoldOperation, LoopMemoryTermKind, LoopRecurrenceKind, + LoopRotateDirection, LoopSummaryKind, LoopVarRole, apply_exact_transition_system, + concrete_state_var_at_block_entry, derive_loop_transition_system, + discover_loop_carried_state, exact_fold_evidence_from_system, + exact_recurrence_evidence_from_system, runtime_counter_threshold_branch, + }; + use crate::{SymState, SymValue}; + + fn var(name: &str, version: u32, size: u32) -> SSAVar { + SSAVar::new(name, version, size) + } + + fn const_var(value: u64, size: u32) -> SSAVar { + SSAVar::new(format!("const:{value:x}"), 0, size) + } + + #[test] + fn detects_counter_threshold_exit_branch() { + let block = FunctionSSABlock { + addr: 0x1000, + size: 4, + phis: Vec::new(), + ops: vec![ + SSAOp::IntLess { + dst: var("tmp", 0, 1), + a: var("i", 1, 8), + b: const_var(0x10, 8), + }, + SSAOp::BoolNot { + dst: var("done", 0, 1), + src: var("tmp", 0, 1), + }, + SSAOp::CBranch { + target: const_var(0x2000, 8), + cond: var("done", 0, 1), + }, + ], + }; + let branch = runtime_counter_threshold_branch(&block).expect("branch"); + assert_eq!(branch.counter.display_name(), "I_1"); + assert_eq!(branch.threshold, 0x10); + assert_eq!(branch.target, 0x2000); + } + + #[test] + fn block_entry_concrete_lookup_uses_selected_phi_source() { + let counter = var("RCX", 2, 8); + let block = FunctionSSABlock { + addr: 0x1000, + size: 4, + phis: vec![PhiNode { + dst: counter.clone(), + sources: vec![(0x900, var("RCX", 0, 8)), (0x1008, var("RCX", 1, 8))], + }], + ops: Vec::new(), + }; + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.set_prev_pc(Some(0x1008)); + state.set_register("RCX_0", SymValue::concrete(1, 64)); + state.set_register("RCX_1", SymValue::concrete(7, 64)); + assert_eq!( + concrete_state_var_at_block_entry(&state, &block, &counter), + Some(7) + ); + } + + #[test] + fn discovers_non_rax_loop_accumulator() { + let acc_phi = var("RBX", 2, 8); + let block = FunctionSSABlock { + addr: 0x1000, + size: 4, + phis: vec![ + PhiNode { + dst: var("RCX", 2, 8), + sources: vec![(0x900, var("RCX", 0, 8)), (0x1008, var("RCX", 1, 8))], + }, + PhiNode { + dst: acc_phi.clone(), + sources: vec![(0x900, var("RBX", 0, 8)), (0x1008, var("RBX", 1, 8))], + }, + ], + ops: vec![SSAOp::IntXor { + dst: var("RBX", 1, 8), + a: acc_phi, + b: var("input", 0, 8), + }], + }; + let carried = discover_loop_carried_state(&block, Some(&var("RCX", 2, 8))); + assert!( + carried + .iter() + .any(|var| { var.name == "RBX_2" && var.role == LoopVarRole::Accumulator }) + ); + } + + #[test] + fn derives_exact_add_const_recurrence_summary() { + let counter_phi = var("RCX", 2, 8); + let acc_phi = var("RBX", 2, 8); + let block = FunctionSSABlock { + addr: 0x1000, + size: 4, + phis: vec![ + PhiNode { + dst: counter_phi.clone(), + sources: vec![(0x900, var("RCX", 0, 8)), (0x1008, var("RCX", 1, 8))], + }, + PhiNode { + dst: acc_phi.clone(), + sources: vec![(0x900, var("RBX", 0, 8)), (0x1008, var("RBX", 1, 8))], + }, + ], + ops: vec![ + SSAOp::IntAdd { + dst: var("RCX", 1, 8), + a: counter_phi.clone(), + b: const_var(1, 8), + }, + SSAOp::IntAdd { + dst: var("RBX", 1, 8), + a: acc_phi, + b: const_var(3, 8), + }, + ], + }; + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.set_prev_pc(Some(0x900)); + state.set_register("RCX_0", SymValue::concrete(0, 64)); + state.set_register("RBX_0", SymValue::concrete(10, 64)); + let branch = super::RuntimeLoopBranch { + counter: counter_phi, + threshold: 10, + target: 0x2000, + }; + + let system = derive_loop_transition_system(&block, &state, &branch, 0, 10); + assert!(system.is_exact(), "{:?}", system.reasons); + assert!(system.recurrences.iter().any(|recurrence| { + recurrence.phi == "RBX_2" && recurrence.kind == LoopRecurrenceKind::AddConst(3) + })); + let exact_recurrences = exact_recurrence_evidence_from_system(&system); + assert!(exact_recurrences.iter().any(|recurrence| { + recurrence.accumulator == "RBX_2" + && recurrence.kind == super::ExactLoopRecurrenceKind::AddConst(3) + })); + let summary = apply_exact_transition_system(&ctx, &state, &system).expect("summary"); + assert_eq!(summary.kind, LoopSummaryKind::Exact); + assert_eq!(summary.exact_recurrences, exact_recurrences); + let summarized = summary.resulting_state.expect("state"); + assert_eq!(summarized.pc, 0x2000); + assert_eq!( + summarized.get_register_sized("RBX_2", 64).as_concrete(), + Some(40) + ); + assert_eq!( + summarized.get_register_sized("RCX_2", 64).as_concrete(), + Some(10) + ); + } + + #[test] + fn derives_exact_affine_const_recurrence_summary() { + let counter_phi = var("RCX", 2, 8); + let acc_phi = var("RBX", 2, 8); + let block = FunctionSSABlock { + addr: 0x1000, + size: 4, + phis: vec![ + PhiNode { + dst: counter_phi.clone(), + sources: vec![(0x900, var("RCX", 0, 8)), (0x1008, var("RCX", 1, 8))], + }, + PhiNode { + dst: acc_phi.clone(), + sources: vec![(0x900, var("RBX", 0, 8)), (0x1008, var("RBX", 1, 8))], + }, + ], + ops: vec![ + SSAOp::IntAdd { + dst: var("RCX", 1, 8), + a: counter_phi.clone(), + b: const_var(1, 8), + }, + SSAOp::IntMult { + dst: var("TMP", 0, 8), + a: acc_phi, + b: const_var(3, 8), + }, + SSAOp::IntAdd { + dst: var("RBX", 1, 8), + a: var("TMP", 0, 8), + b: const_var(1, 8), + }, + ], + }; + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.set_prev_pc(Some(0x900)); + state.set_register("RCX_0", SymValue::concrete(0, 64)); + state.set_register("RBX_0", SymValue::concrete(2, 64)); + let branch = super::RuntimeLoopBranch { + counter: counter_phi, + threshold: 4, + target: 0x2000, + }; + + let system = derive_loop_transition_system(&block, &state, &branch, 0, 4); + assert!(system.is_exact(), "{:?}", system.reasons); + assert!(system.recurrences.iter().any(|recurrence| { + recurrence.phi == "RBX_2" + && recurrence.kind + == LoopRecurrenceKind::AffineConst { + multiplier: 3, + addend: 1, + } + })); + let exact_recurrences = exact_recurrence_evidence_from_system(&system); + assert!(exact_recurrences.iter().any(|recurrence| { + recurrence.accumulator == "RBX_2" + && recurrence.kind + == super::ExactLoopRecurrenceKind::AffineConst { + multiplier: 3, + addend: 1, + } + })); + let summary = apply_exact_transition_system(&ctx, &state, &system).expect("summary"); + assert_eq!(summary.kind, LoopSummaryKind::Exact); + let summarized = summary.resulting_state.expect("state"); + assert_eq!( + summarized.get_register_sized("RBX_2", 64).as_concrete(), + Some(202) + ); + assert_eq!( + summarized.get_register_sized("RCX_2", 64).as_concrete(), + Some(4) + ); + } + + #[test] + fn refuses_exact_recurrence_when_loop_has_memory_effects() { + let counter_phi = var("RCX", 2, 8); + let block = FunctionSSABlock { + addr: 0x1000, + size: 4, + phis: vec![PhiNode { + dst: counter_phi.clone(), + sources: vec![(0x900, var("RCX", 0, 8)), (0x1008, var("RCX", 1, 8))], + }], + ops: vec![ + SSAOp::IntAdd { + dst: var("RCX", 1, 8), + a: counter_phi.clone(), + b: const_var(1, 8), + }, + SSAOp::Store { + space: "ram".to_string(), + addr: var("RAX", 0, 8), + val: var("RCX", 1, 8), + }, + ], + }; + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.set_prev_pc(Some(0x900)); + state.set_register("RCX_0", SymValue::concrete(0, 64)); + let branch = super::RuntimeLoopBranch { + counter: counter_phi, + threshold: 10, + target: 0x2000, + }; + + let system = derive_loop_transition_system(&block, &state, &branch, 0, 10); + assert!(!system.is_exact()); + assert!( + system + .reasons + .contains(&"runtime_loop_memory_write_transition_unsupported".to_string()) + ); + } + + #[test] + fn derives_exact_xor_table_fold_summary() { + let counter_phi = var("RCX", 2, 8); + let ptr_phi = var("RDI", 2, 8); + let acc_phi = var("RBX", 2, 8); + let block = FunctionSSABlock { + addr: 0x1000, + size: 4, + phis: vec![ + PhiNode { + dst: counter_phi.clone(), + sources: vec![(0x900, var("RCX", 0, 8)), (0x1008, var("RCX", 1, 8))], + }, + PhiNode { + dst: ptr_phi.clone(), + sources: vec![(0x900, var("RDI", 0, 8)), (0x1008, var("RDI", 1, 8))], + }, + PhiNode { + dst: acc_phi.clone(), + sources: vec![(0x900, var("RBX", 0, 8)), (0x1008, var("RBX", 1, 8))], + }, + ], + ops: vec![ + SSAOp::IntAdd { + dst: var("RCX", 1, 8), + a: counter_phi.clone(), + b: const_var(1, 8), + }, + SSAOp::IntAdd { + dst: var("RDI", 1, 8), + a: ptr_phi.clone(), + b: const_var(1, 8), + }, + SSAOp::Load { + dst: var("TMP", 0, 1), + space: "ram".to_string(), + addr: ptr_phi, + }, + SSAOp::IntXor { + dst: var("RBX", 1, 8), + a: acc_phi, + b: var("TMP", 0, 1), + }, + ], + }; + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.set_prev_pc(Some(0x900)); + state.set_register("RCX_0", SymValue::concrete(0, 64)); + state.set_register("RDI_0", SymValue::concrete(0x5000, 64)); + state.set_register("RBX_0", SymValue::concrete(0xaa, 64)); + state.mem_write( + &SymValue::concrete(0x5000, 64), + &SymValue::concrete(1, 8), + 1, + ); + state.mem_write( + &SymValue::concrete(0x5001, 64), + &SymValue::concrete(2, 8), + 1, + ); + state.mem_write( + &SymValue::concrete(0x5002, 64), + &SymValue::concrete(3, 8), + 1, + ); + let branch = super::RuntimeLoopBranch { + counter: counter_phi, + threshold: 3, + target: 0x2000, + }; + + let system = derive_loop_transition_system(&block, &state, &branch, 0, 3); + assert!(system.is_exact(), "{:?}", system.reasons); + let folds = exact_fold_evidence_from_system(&system); + assert_eq!(folds.len(), 1); + assert_eq!(folds[0].operation, LoopFoldOperation::Xor); + assert_eq!(folds[0].accumulator, "RBX_2"); + assert_eq!(folds[0].term.kind, LoopMemoryTermKind::TableRead); + assert!(system.recurrences.iter().any(|recurrence| { + matches!( + &recurrence.kind, + LoopRecurrenceKind::XorMemoryFold(term) + if term.kind == LoopMemoryTermKind::TableRead && term.base == Some(0x5000) + ) + })); + let summary = apply_exact_transition_system(&ctx, &state, &system).expect("summary"); + assert_eq!(summary.kind, LoopSummaryKind::Exact); + let summarized = summary.resulting_state.expect("state"); + assert_eq!( + summarized.get_register_sized("RBX_2", 64).as_concrete(), + Some(0xaa) + ); + assert_eq!( + summarized.get_register_sized("RDI_2", 64).as_concrete(), + Some(0x5003) + ); + } + + #[test] + fn derives_exact_xor_symbolic_input_fold_summary() { + let counter_phi = var("RCX", 2, 8); + let ptr_phi = var("RDI", 2, 8); + let acc_phi = var("RBX", 2, 8); + let block = FunctionSSABlock { + addr: 0x1000, + size: 4, + phis: vec![ + PhiNode { + dst: counter_phi.clone(), + sources: vec![(0x900, var("RCX", 0, 8)), (0x1008, var("RCX", 1, 8))], + }, + PhiNode { + dst: ptr_phi.clone(), + sources: vec![(0x900, var("RDI", 0, 8)), (0x1008, var("RDI", 1, 8))], + }, + PhiNode { + dst: acc_phi.clone(), + sources: vec![(0x900, var("RBX", 0, 8)), (0x1008, var("RBX", 1, 8))], + }, + ], + ops: vec![ + SSAOp::IntAdd { + dst: var("RCX", 1, 8), + a: counter_phi.clone(), + b: const_var(1, 8), + }, + SSAOp::IntAdd { + dst: var("RDI", 1, 8), + a: ptr_phi.clone(), + b: const_var(1, 8), + }, + SSAOp::Load { + dst: var("TMP", 0, 1), + space: "ram".to_string(), + addr: ptr_phi, + }, + SSAOp::IntXor { + dst: var("RBX", 1, 8), + a: acc_phi, + b: var("TMP", 0, 1), + }, + ], + }; + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.set_prev_pc(Some(0x900)); + state.set_register("RCX_0", SymValue::concrete(0, 64)); + state.set_register("RDI_0", SymValue::concrete(0x7000, 64)); + state.set_register("RBX_0", SymValue::concrete(0, 64)); + state.make_symbolic_memory(0x7000, 3, "argv1"); + let branch = super::RuntimeLoopBranch { + counter: counter_phi, + threshold: 3, + target: 0x2000, + }; + + let system = derive_loop_transition_system(&block, &state, &branch, 0, 3); + assert!(system.is_exact(), "{:?}", system.reasons); + let folds = exact_fold_evidence_from_system(&system); + assert_eq!(folds.len(), 1); + assert_eq!(folds[0].operation, LoopFoldOperation::Xor); + assert_eq!(folds[0].accumulator, "RBX_2"); + assert_eq!(folds[0].term.kind, LoopMemoryTermKind::InputRead); + assert!(system.recurrences.iter().any(|recurrence| { + matches!( + &recurrence.kind, + LoopRecurrenceKind::XorMemoryFold(term) + if term.kind == LoopMemoryTermKind::InputRead + && term.base == Some(0x7000) + && term.region_base == Some(0x7000) + && term.region_size == Some(3) + ) + })); + let summary = apply_exact_transition_system(&ctx, &state, &system).expect("summary"); + assert_eq!(summary.kind, LoopSummaryKind::Exact); + let summarized = summary.resulting_state.expect("state"); + assert!(summarized.get_register_sized("RBX_2", 64).is_symbolic()); + assert_eq!( + summarized.get_register_sized("RDI_2", 64).as_concrete(), + Some(0x7003) + ); + } + + #[test] + fn derives_exact_add_runtime_blob_fold_summary() { + let counter_phi = var("RCX", 2, 8); + let ptr_phi = var("RDI", 2, 8); + let acc_phi = var("RBX", 2, 8); + let block = FunctionSSABlock { + addr: 0x1000, + size: 4, + phis: vec![ + PhiNode { + dst: counter_phi.clone(), + sources: vec![(0x900, var("RCX", 0, 8)), (0x1008, var("RCX", 1, 8))], + }, + PhiNode { + dst: ptr_phi.clone(), + sources: vec![(0x900, var("RDI", 0, 8)), (0x1008, var("RDI", 1, 8))], + }, + PhiNode { + dst: acc_phi.clone(), + sources: vec![(0x900, var("RBX", 0, 8)), (0x1008, var("RBX", 1, 8))], + }, + ], + ops: vec![ + SSAOp::IntAdd { + dst: var("RCX", 1, 8), + a: counter_phi.clone(), + b: const_var(1, 8), + }, + SSAOp::IntAdd { + dst: var("RDI", 1, 8), + a: ptr_phi.clone(), + b: const_var(1, 8), + }, + SSAOp::Load { + dst: var("TMP", 0, 1), + space: "ram".to_string(), + addr: ptr_phi, + }, + SSAOp::IntAdd { + dst: var("RBX", 1, 8), + a: acc_phi, + b: var("TMP", 0, 1), + }, + ], + }; + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.set_prev_pc(Some(0x900)); + state.set_register("RCX_0", SymValue::concrete(0, 64)); + state.set_register("RDI_0", SymValue::concrete(0x6000_0000, 64)); + state.set_register("RBX_0", SymValue::concrete(10, 64)); + let region = state.define_runtime_region("jit_blob", 0x6000_0000, 3, true); + state.seed_region_bytes(region, 0, &[1, 2, 3]); + let branch = super::RuntimeLoopBranch { + counter: counter_phi, + threshold: 3, + target: 0x2000, + }; + + let system = derive_loop_transition_system(&block, &state, &branch, 0, 3); + assert!(system.is_exact(), "{:?}", system.reasons); + assert!(system.recurrences.iter().any(|recurrence| { + matches!( + &recurrence.kind, + LoopRecurrenceKind::AddMemoryFold(term) + if term.kind == LoopMemoryTermKind::RuntimeBlobRead + && term.base == Some(0x6000_0000) + && term.region_base == Some(0x6000_0000) + && term.region_size == Some(3) + ) + })); + let summary = apply_exact_transition_system(&ctx, &state, &system).expect("summary"); + let summarized = summary.resulting_state.expect("state"); + assert_eq!( + summarized.get_register_sized("RBX_2", 64).as_concrete(), + Some(16) + ); + } + + #[test] + fn derives_exact_rotate_xor_table_summary() { + let counter_phi = var("RCX", 2, 8); + let ptr_phi = var("RDI", 2, 8); + let acc_phi = var("RBX", 2, 1); + let block = FunctionSSABlock { + addr: 0x1000, + size: 7, + phis: vec![ + PhiNode { + dst: counter_phi.clone(), + sources: vec![(0x900, var("RCX", 0, 8)), (0x1008, var("RCX", 1, 8))], + }, + PhiNode { + dst: ptr_phi.clone(), + sources: vec![(0x900, var("RDI", 0, 8)), (0x1008, var("RDI", 1, 8))], + }, + PhiNode { + dst: acc_phi.clone(), + sources: vec![(0x900, var("RBX", 0, 1)), (0x1008, var("RBX", 1, 1))], + }, + ], + ops: vec![ + SSAOp::IntAdd { + dst: var("RCX", 1, 8), + a: counter_phi.clone(), + b: const_var(1, 8), + }, + SSAOp::IntAdd { + dst: var("RDI", 1, 8), + a: ptr_phi.clone(), + b: const_var(1, 8), + }, + SSAOp::Load { + dst: var("TMP", 0, 1), + space: "ram".to_string(), + addr: ptr_phi, + }, + SSAOp::IntLeft { + dst: var("ROT_L", 0, 1), + a: acc_phi.clone(), + b: const_var(3, 1), + }, + SSAOp::IntRight { + dst: var("ROT_R", 0, 1), + a: acc_phi, + b: const_var(5, 1), + }, + SSAOp::IntOr { + dst: var("ROT", 0, 1), + a: var("ROT_L", 0, 1), + b: var("ROT_R", 0, 1), + }, + SSAOp::IntXor { + dst: var("RBX", 1, 1), + a: var("ROT", 0, 1), + b: var("TMP", 0, 1), + }, + ], + }; + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.set_prev_pc(Some(0x900)); + state.set_register("RCX_0", SymValue::concrete(0, 64)); + state.set_register("RDI_0", SymValue::concrete(0x5000, 64)); + state.set_register("RBX_0", SymValue::concrete(0x12, 8)); + state.mem_write( + &SymValue::concrete(0x5000, 64), + &SymValue::concrete(1, 8), + 1, + ); + state.mem_write( + &SymValue::concrete(0x5001, 64), + &SymValue::concrete(2, 8), + 1, + ); + state.mem_write( + &SymValue::concrete(0x5002, 64), + &SymValue::concrete(3, 8), + 1, + ); + let branch = super::RuntimeLoopBranch { + counter: counter_phi, + threshold: 3, + target: 0x2000, + }; + + let system = derive_loop_transition_system(&block, &state, &branch, 0, 3); + assert!(system.is_exact(), "{:?}", system.reasons); + assert!(system.recurrences.iter().any(|recurrence| { + matches!( + &recurrence.kind, + LoopRecurrenceKind::RotateMix { + direction: LoopRotateDirection::Left, + amount: 3, + operation: LoopFoldOperation::Xor, + term, + } if term.kind == LoopMemoryTermKind::TableRead && term.base == Some(0x5000) + ) + })); + let recurrences = exact_recurrence_evidence_from_system(&system); + assert!(recurrences.iter().any(|recurrence| matches!( + &recurrence.kind, + ExactLoopRecurrenceKind::RotateMix { + direction: LoopRotateDirection::Left, + amount: 3, + operation: LoopFoldOperation::Xor, + term, + } if term.kind == LoopMemoryTermKind::TableRead + ))); + assert!(exact_fold_evidence_from_system(&system).is_empty()); + let summary = apply_exact_transition_system(&ctx, &state, &system).expect("summary"); + let summarized = summary.resulting_state.expect("state"); + assert_eq!( + summarized.get_register_sized("RBX_2", 8).as_concrete(), + Some(0x77) + ); + assert_eq!( + summarized.get_register_sized("RDI_2", 64).as_concrete(), + Some(0x5003) + ); + } + + #[test] + fn refuses_exact_symbolic_input_fold_when_bounds_are_unknown() { + let counter_phi = var("RCX", 2, 8); + let ptr_phi = var("RDI", 2, 8); + let acc_phi = var("RBX", 2, 8); + let block = FunctionSSABlock { + addr: 0x1000, + size: 4, + phis: vec![ + PhiNode { + dst: counter_phi.clone(), + sources: vec![(0x900, var("RCX", 0, 8)), (0x1008, var("RCX", 1, 8))], + }, + PhiNode { + dst: ptr_phi.clone(), + sources: vec![(0x900, var("RDI", 0, 8)), (0x1008, var("RDI", 1, 8))], + }, + PhiNode { + dst: acc_phi.clone(), + sources: vec![(0x900, var("RBX", 0, 8)), (0x1008, var("RBX", 1, 8))], + }, + ], + ops: vec![ + SSAOp::IntAdd { + dst: var("RCX", 1, 8), + a: counter_phi.clone(), + b: const_var(1, 8), + }, + SSAOp::IntAdd { + dst: var("RDI", 1, 8), + a: ptr_phi.clone(), + b: const_var(1, 8), + }, + SSAOp::Load { + dst: var("TMP", 0, 1), + space: "ram".to_string(), + addr: ptr_phi, + }, + SSAOp::IntXor { + dst: var("RBX", 1, 8), + a: acc_phi, + b: var("TMP", 0, 1), + }, + ], + }; + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.set_prev_pc(Some(0x900)); + state.set_register("RCX_0", SymValue::concrete(0, 64)); + state.set_register("RDI_0", SymValue::concrete(0x7000, 64)); + state.set_register("RBX_0", SymValue::concrete(0, 64)); + state.make_symbolic_memory(0x7000, 2, "argv1"); + let branch = super::RuntimeLoopBranch { + counter: counter_phi, + threshold: 3, + target: 0x2000, + }; + + let system = derive_loop_transition_system(&block, &state, &branch, 0, 3); + assert!(!system.is_exact()); + assert!( + system + .reasons + .contains(&"runtime_loop_memory_bounds_unknown:RBX_2".to_string()) + ); + } +} diff --git a/crates/r2sym/src/memory.rs b/crates/r2sym/src/memory.rs index 63af6f7..d5a09b5 100644 --- a/crates/r2sym/src/memory.rs +++ b/crates/r2sym/src/memory.rs @@ -237,7 +237,11 @@ impl<'ctx> MemoryRegion<'ctx> { if let Some(concrete_value) = value.as_concrete() { for index in 0..size { let byte_offset = offset.wrapping_add(index as u64); - let byte_value = ((concrete_value >> (index * 8)) & 0xff) as u8; + let byte_value = if index < 8 { + ((concrete_value >> (index * 8)) & 0xff) as u8 + } else { + 0 + }; self.concrete.insert(byte_offset, byte_value); } } else { @@ -1288,6 +1292,32 @@ mod tests { assert_eq!(value, 0x1122_aa44); } + #[test] + fn test_wide_concrete_region_write_does_not_shift_overflow() { + let ctx = Context::thread_local(); + let mut mem = SymMemory::new(&ctx); + let globals = mem.define_region( + MemoryRegionKind::Global, + "globals", + Some(0x5000), + Some(0x100), + ); + + let ptr = RegionPointer { + region_id: globals, + offset: 0, + ptr_bits: 64, + }; + mem.region_write(&ptr, &SymValue::concrete(0x1122_3344_5566_7788, 128), 16); + + assert_eq!( + mem.read_bytes(0x5000, 16), + Some(vec![ + 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0, 0, 0, 0, 0, 0, 0, 0, + ]) + ); + } + #[test] fn test_affine_pointer_recognizer_extracts_symbol_plus_delta() { let ctx = Context::thread_local(); diff --git a/crates/r2sym/src/path.rs b/crates/r2sym/src/path.rs index aebabb8..7824ab6 100644 --- a/crates/r2sym/src/path.rs +++ b/crates/r2sym/src/path.rs @@ -4,21 +4,99 @@ //! during symbolic execution, including DFS, BFS, and coverage-guided. use std::cmp::Reverse; -use std::collections::{BTreeMap, BinaryHeap, HashMap, HashSet, VecDeque}; +use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, VecDeque}; +use std::fs::OpenOptions; +use std::io::Write; use std::rc::Rc; use std::time::{Duration, Instant}; -use r2ssa::{BlockTerminator, SsaArtifact}; +use r2ssa::{AbiProfile, BlockTerminator, SSAOp, SsaArtifact, observe_call_arguments}; use z3::Context; use crate::backward::DerivedCallSummaryView; +use crate::constraints::FinalConstraintGraph; use crate::executor::SymExecutor; -use crate::sim::{CallConv, DerivedFunctionSummary, evaluate_derived_summary_guidance}; +use crate::loops::{self, ExactLoopFoldEvidence, ExactLoopRecurrenceEvidence, LoopSummaryKind}; +use crate::sim::{ + CallConv, DerivedFunctionSummary, PreparedFunctionScope, evaluate_derived_summary_guidance, +}; use crate::solver::SymSolver; use crate::spec::ExplorationSpec; -use crate::state::{ExitStatus, SymState}; +use crate::state::{ExitStatus, RuntimeBlockReason, SymState}; +use crate::tactics::{ + SolveTacticConfig, constrain_exact_fold_inputs, constrain_exact_recurrence_candidate, + tactic_candidates_for_constraint_graph, +}; const TARGET_DISTANCE_CACHE_LIMIT: usize = 128; +const EXACT_RUNTIME_LOOP_MAX_ITERS: u64 = 4096; +const EXACT_RUNTIME_LOOP_MAX_BLOCK_STEPS: u64 = 200_000; +const SYMBOLIC_CONTINUATION_MAX_TARGETS: usize = 512; + +fn debug_target_guidance_enabled() -> bool { + std::env::var_os("R2SLEIGH_DEBUG_TARGET_GUIDANCE").is_some() +} + +fn debug_target_guidance_log(message: &str) { + if !debug_target_guidance_enabled() { + return; + } + let path = std::env::var("R2SLEIGH_DEBUG_TARGET_GUIDANCE_LOG") + .unwrap_or_else(|_| "/tmp/r2sleigh_target_guidance.log".to_string()); + if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) { + let _ = writeln!(file, "{message}"); + } +} + +fn debug_target_match_unsat_enabled() -> bool { + std::env::var_os("R2SLEIGH_DEBUG_TARGET_MATCH_UNSAT").is_some() +} + +fn debug_target_match_unsat_log(message: &str) { + if !debug_target_match_unsat_enabled() { + return; + } + let path = std::env::var("R2SLEIGH_DEBUG_TARGET_MATCH_UNSAT_LOG") + .unwrap_or_else(|_| "/tmp/r2sleigh_target_match_unsat.log".to_string()); + if let Ok(mut file) = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(path) + { + let _ = writeln!(file, "{message}"); + } +} + +fn debug_log_target_unsat_state(state: &SymState<'_>, target_addr: u64) { + if !debug_target_match_unsat_enabled() { + return; + } + let mut registers = state + .registers() + .iter() + .filter_map(|(name, value)| { + let interesting = ["AL_", "EAX_", "RAX_", "RCX_", "RDI_", "RDX_", "RSP_"] + .iter() + .any(|prefix| name.starts_with(prefix)); + interesting.then(|| match value.as_concrete() { + Some(value) => format!("{name}={value:#x}"), + None => format!("{name}=sym({}b)", value.bits()), + }) + }) + .collect::>(); + registers.sort(); + debug_target_match_unsat_log(&format!( + "target=0x{target_addr:x} pc=0x{:x} prev_pc={} depth={} constraints={} regs=[{}]", + state.pc, + state + .prev_pc() + .map(|pc| format!("0x{pc:x}")) + .unwrap_or_else(|| "none".to_string()), + state.depth, + state.num_constraints(), + registers.join(", ") + )); +} /// Configuration for path exploration. #[derive(Debug, Clone)] @@ -136,6 +214,22 @@ pub struct SolvedPath { pub final_pc: u64, /// Path constraints that were satisfied. pub num_constraints: usize, + /// Optional provenance when the candidate was produced by a solve tactic. + pub generation: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SolvedPathGenerationKind { + ExactRecurrenceConstraintTactic, + MitmConstraintTactic, + DomainConstraintTactic, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SolvedPathGeneration { + pub kind: SolvedPathGenerationKind, + pub reason: String, + pub constrained_bytes: usize, } /// Result of a spec-driven exploration run. @@ -169,10 +263,16 @@ pub struct PathExplorer<'ctx> { stats: ExploreStats, /// Whether query helpers should prioritize states closer to their target. target_guided_queries: bool, + /// Whether target-guided queries may first route through exception-dispatch bridge sites. + exception_bridge_guidance_enabled: bool, + /// Whether unsupported runtime loop summaries may produce residual states. + residual_runtime_loop_summaries_enabled: bool, /// Derived helper summaries registered for direct-call targets. derived_call_summaries: HashMap>, /// Cached reverse-distance maps keyed by (function entry, target address). target_distance_cache: HashMap<(u64, u64), HashMap>, + /// Candidate-generation tactics used during model extraction. + solve_tactic_config: SolveTacticConfig, } /// Statistics from path exploration. @@ -208,6 +308,36 @@ pub struct ExploreStats { pub target_pruned_summary_contradiction: usize, /// Number of target-guided states whose ranking/pruning used derived summary metadata. pub target_summary_rank_hits: usize, + /// Number of states that reached the target PC but were unsatisfiable. + pub target_match_unsat: usize, + /// Number of states blocked by a missing exception handler. + pub runtime_missing_exception_handler: usize, + /// Number of states that entered runtime code without a materialized executable alias. + pub runtime_missing_materialized_code: usize, + /// Number of states that could not resume from an exception continuation. + pub runtime_missing_continuation_seed: usize, + /// Number of states that reached runtime code with unresolved provenance. + pub runtime_region_provenance_unknown: usize, + /// Number of symbolic runtime breakpoint dispatch candidates forked. + pub runtime_symbolic_breakpoint_forks: usize, + /// Number of symbolic runtime breakpoint candidates proven infeasible before enqueue. + pub runtime_symbolic_breakpoint_pruned: usize, + /// Number of residual summaries used to accelerate runtime breakpoint loops. + pub runtime_breakpoint_loop_summaries: usize, + /// Number of exact summaries used to accelerate runtime breakpoint loops. + pub runtime_breakpoint_loop_exact_summaries: usize, + /// Number of exact loop summaries derived by recurrence algebra. + pub runtime_loop_exact_recurrence_summaries: usize, + /// Canonical exact recurrence evidence derived by loop algebra. + pub runtime_loop_exact_recurrences: Vec, + /// Exact memory-fold recurrences derived by loop algebra. + pub runtime_loop_exact_folds: Vec, + /// Number of runtime loop summaries refused for soundness or missing context. + pub runtime_loop_refusals: usize, + /// Number of runtime loop candidates with unknown carried state. + pub runtime_loop_unknown_carried_state: usize, + /// Number of runtime loop candidates downgraded because exact iteration budget was exceeded. + pub runtime_loop_budget_residuals: usize, } #[derive(Clone)] @@ -224,10 +354,12 @@ struct BlockSummaryRank { } struct TargetGuidanceContext { + target_addr: u64, distances: HashMap, reachable_blocks: HashSet, call_targets_by_block: HashMap>, block_summary_rank: HashMap, + allow_cross_function_states: bool, } #[derive(Clone, Copy, Default)] @@ -237,6 +369,15 @@ struct StateSummaryGuidance { contradictory: bool, } +#[derive(Clone, Copy)] +struct ResolvedBlock<'a> { + func: &'a SsaArtifact, + block: &'a r2ssa::FunctionSSABlock, + static_addr: u64, + runtime_addr: u64, + runtime_aliased: bool, +} + struct StateWorklist<'ctx> { strategy: ExploreStrategy, ready: VecDeque, @@ -387,7 +528,18 @@ impl<'ctx> StateWorklist<'ctx> { #[derive(Clone, Debug, Eq, PartialEq)] struct TargetGuidedQueueEntry { - rank: (bool, usize, usize, usize, usize, usize, usize, usize, usize), + rank: ( + usize, + usize, + usize, + usize, + usize, + usize, + usize, + usize, + usize, + usize, + ), id: usize, } @@ -417,10 +569,12 @@ enum DriverMode<'ctx> { }, FindFirst { target_addr: u64, + require_feasible: bool, found: Option>, }, FindAll { target_addr: u64, + require_feasible: bool, matches: Vec>, }, Avoid { @@ -475,12 +629,18 @@ impl<'ctx> DriverMode<'ctx> { ) -> DriverAction<'ctx> { match self { DriverMode::Explore { .. } => DriverAction::Continue(Box::new(state)), - DriverMode::FindFirst { target_addr, found } => { - if state.pc == *target_addr { - if explorer.solver.is_sat(&state) { + DriverMode::FindFirst { + target_addr, + require_feasible, + found, + } => { + if explorer.state_matches_target(&state, *target_addr) { + if !*require_feasible || explorer.solver.is_sat(&state) { *found = Some(PathResult::new(state, true)); DriverAction::Finish } else { + debug_log_target_unsat_state(&state, *target_addr); + explorer.stats.target_match_unsat += 1; DriverAction::Skip } } else { @@ -489,13 +649,17 @@ impl<'ctx> DriverMode<'ctx> { } DriverMode::FindAll { target_addr, + require_feasible, matches, } => { - if state.pc == *target_addr { - if explorer.solver.is_sat(&state) { + if explorer.state_matches_target(&state, *target_addr) { + if !*require_feasible || explorer.solver.is_sat(&state) { explorer.record_depth(state.depth); explorer.stats.paths_completed += 1; matches.push(PathResult::new(state, true)); + } else { + debug_log_target_unsat_state(&state, *target_addr); + explorer.stats.target_match_unsat += 1; } DriverAction::Skip } else { @@ -503,7 +667,7 @@ impl<'ctx> DriverMode<'ctx> { } } DriverMode::Avoid { avoid_set, .. } => { - if avoid_set.contains(&state.pc) { + if explorer.state_hits_any_target(&state, avoid_set) { DriverAction::Skip } else { DriverAction::Continue(Box::new(state)) @@ -515,11 +679,11 @@ impl<'ctx> DriverMode<'ctx> { max_finds, result, } => { - if avoid_set.contains(&state.pc) { + if explorer.state_hits_any_target(&state, avoid_set) { result.avoided_states += 1; return DriverAction::Skip; } - if find_set.contains(&state.pc) { + if explorer.state_hits_any_target(&state, find_set) { if explorer.solver.is_sat(&state) { explorer.record_depth(state.depth); explorer.stats.paths_completed += 1; @@ -585,6 +749,7 @@ impl<'ctx> DriverMode<'ctx> { block_addr: u64, max_completed_paths: Option, ) -> DriverAction<'ctx> { + explorer.record_runtime_missing_block(&state, block_addr); match self { DriverMode::Explore { results } => { state.terminate(ExitStatus::Return); @@ -623,6 +788,7 @@ impl<'ctx> DriverMode<'ctx> { block_addr: u64, max_completed_paths: Option, ) -> DriverAction<'ctx> { + explorer.record_runtime_exit_status(&state.exit_status); match self { DriverMode::Explore { results } => { explorer.record_depth(state.depth); @@ -643,7 +809,9 @@ impl<'ctx> DriverMode<'ctx> { block_addr, state.exit_status )); match &state.exit_status { - Some(ExitStatus::Error(_)) => result.errored_states += 1, + Some(ExitStatus::Error(_)) | Some(ExitStatus::RuntimeBlocked(_)) => { + result.errored_states += 1 + } _ => result.completed_states += 1, } } @@ -686,11 +854,12 @@ impl<'ctx> DriverMode<'ctx> { fn allow_enqueue(&mut self, state: &SymState<'ctx>) -> bool { match self { - DriverMode::Avoid { avoid_set, .. } => !avoid_set.contains(&state.pc), + DriverMode::Avoid { .. } => true, DriverMode::Spec { avoid_set, result, .. } => { - if avoid_set.contains(&state.pc) { + let static_pc = state.resolve_runtime_pc(state.pc).unwrap_or(state.pc); + if avoid_set.contains(&state.pc) || avoid_set.contains(&static_pc) { result.avoided_states += 1; false } else { @@ -703,1296 +872,3531 @@ impl<'ctx> DriverMode<'ctx> { } impl<'ctx> PathExplorer<'ctx> { - /// Create a new path explorer. - pub fn new(ctx: &'ctx Context) -> Self { - Self { - _ctx: ctx, - executor: SymExecutor::new(ctx), - solver: SymSolver::new(ctx), - config: ExploreConfig::default(), - stats: ExploreStats::default(), - target_guided_queries: false, - derived_call_summaries: HashMap::new(), - target_distance_cache: HashMap::new(), - } + fn state_matches_target(&self, state: &SymState<'ctx>, target_addr: u64) -> bool { + state.pc == target_addr || self.effective_static_pc(state) == target_addr } - /// Create a path explorer with configuration. - pub fn with_config(ctx: &'ctx Context, config: ExploreConfig) -> Self { - let solver = if let Some(timeout) = config.timeout { - SymSolver::with_timeout(ctx, timeout) - } else { - SymSolver::new(ctx) - }; - - Self { - _ctx: ctx, - executor: SymExecutor::new(ctx), - solver, - config, - stats: ExploreStats::default(), - target_guided_queries: false, - derived_call_summaries: HashMap::new(), - target_distance_cache: HashMap::new(), - } + fn state_hits_any_target(&self, state: &SymState<'ctx>, targets: &HashSet) -> bool { + targets.contains(&state.pc) || targets.contains(&self.effective_static_pc(state)) } - /// Get the exploration statistics. - pub fn stats(&self) -> &ExploreStats { - &self.stats + fn max_inline_runahead_depth_delta(&self) -> usize { + self.config + .max_depth + .saturating_mul(64) + .max(self.config.max_depth) } - /// Get the solver for additional queries. - pub fn solver(&self) -> &SymSolver<'ctx> { - &self.solver + fn finalize_active_state_after_block( + &self, + func: &SsaArtifact, + state: &mut SymState<'ctx>, + block_runtime_addr: u64, + block_static_addr: u64, + ) { + state.pc = state + .remap_static_pc_to_runtime(state.pc) + .unwrap_or(state.pc); + if state.pc == block_runtime_addr + && let Some(next) = self.fallthrough_target(func, block_static_addr) + { + state.pc = state.remap_static_pc_to_runtime(next).unwrap_or(next); + } + state.set_prev_pc(Some(block_runtime_addr)); } - /// Enable target-guided ordering for query-only helpers. - pub fn set_target_guided_queries(&mut self, enabled: bool) { - self.target_guided_queries = enabled; + fn patch_pending_exception_resume_pc( + &self, + func: &SsaArtifact, + state: &mut SymState<'ctx>, + block_static_addr: u64, + ) { + let Some(pending) = state.pending_exception() else { + return; + }; + if state.pc != pending.handler_addr { + return; + } + let Some(resume_static_pc) = self.fallthrough_target(func, block_static_addr) else { + return; + }; + let resume_pc = state + .remap_static_pc_to_runtime(resume_static_pc) + .unwrap_or(resume_static_pc); + let _ = state.set_pending_exception_resume_pc(resume_pc); } - /// Whether target-guided ordering is enabled for query helpers. - pub fn target_guided_queries_enabled(&self) -> bool { - self.target_guided_queries + fn record_runtime_block_reason(&mut self, reason: RuntimeBlockReason) { + match reason { + RuntimeBlockReason::MissingExceptionHandler => { + self.stats.runtime_missing_exception_handler += 1; + } + RuntimeBlockReason::MissingRuntimeMaterializedCode => { + self.stats.runtime_missing_materialized_code += 1; + } + RuntimeBlockReason::MissingContinuationSeed => { + self.stats.runtime_missing_continuation_seed += 1; + } + RuntimeBlockReason::RuntimeRegionProvenanceUnknown => { + self.stats.runtime_region_provenance_unknown += 1; + } + } } - /// Register a call hook for a concrete target address. - pub fn register_call_hook(&mut self, addr: u64, hook: F) - where - F: Fn(&mut SymState<'ctx>) -> crate::executor::CallHookResult + 'ctx, - { - self.derived_call_summaries.remove(&addr); - self.executor - .register_call_hook(addr, move |state| Ok(hook(state))); + fn record_runtime_exit_status(&mut self, status: &Option) { + if let Some(ExitStatus::RuntimeBlocked(reason)) = status { + self.record_runtime_block_reason(*reason); + } } - pub(crate) fn register_derived_call_hook( - &mut self, - addr: u64, - summary: Rc>, - callconv: CallConv, - hook: F, - ) where - F: Fn(&mut SymState<'ctx>) -> crate::executor::CallHookResult + 'ctx, - { - self.derived_call_summaries - .insert(addr, RegisteredDerivedCallSummary { summary, callconv }); - self.executor - .register_call_hook(addr, move |state| Ok(hook(state))); + fn record_runtime_missing_block(&mut self, state: &SymState<'ctx>, block_addr: u64) { + if let Some(region) = state.runtime_region_for_pc(block_addr) { + if !region.executable || region.source_base.is_none() { + self.record_runtime_block_reason( + RuntimeBlockReason::MissingRuntimeMaterializedCode, + ); + } else { + self.record_runtime_block_reason( + RuntimeBlockReason::RuntimeRegionProvenanceUnknown, + ); + } + } } - pub(crate) fn derived_call_summary_views(&self) -> HashMap> { - self.derived_call_summaries - .iter() - .map(|(addr, registered)| { - ( - *addr, - DerivedCallSummaryView { - summary: registered.summary.clone(), - callconv: registered.callconv.clone(), - }, - ) - }) + fn runtime_continuation_candidate_targets( + &self, + root: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + state: &SymState<'ctx>, + ) -> Vec { + let mut candidates = BTreeSet::new(); + let mut collect = |function: &SsaArtifact| { + for static_pc in function.cfg().block_addrs() { + if let Some(runtime_pc) = state.remap_static_pc_to_runtime(static_pc) { + candidates.insert(runtime_pc); + } + } + }; + collect(root); + if let Some(scope) = scope { + for function in scope.functions().values() { + collect(&function.prepared); + } + } + candidates + .into_iter() + .take(SYMBOLIC_CONTINUATION_MAX_TARGETS) .collect() } - /// Solve a path's constraints and extract concrete values. - /// - /// Returns None if the path is infeasible. - pub fn solve_path(&self, path: &PathResult<'ctx>) -> Option { - if !path.feasible { - return None; + fn fork_symbolic_exception_resume_targets( + &mut self, + root: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + state: &SymState<'ctx>, + ) -> Vec> { + if !matches!( + &state.exit_status, + Some(ExitStatus::RuntimeBlocked( + RuntimeBlockReason::MissingContinuationSeed + )) + ) { + return Vec::new(); } - - // Get a model from the solver - let model = self.solver.solve(&path.state)?; - - let mut solved = SolvedPath { - final_pc: path.state.pc, - num_constraints: path.state.num_constraints(), - ..Default::default() + let Some(pending) = state.pending_exception() else { + return Vec::new(); }; + let rip = state.mem_read( + &crate::SymValue::concrete(pending.context_addr.saturating_add(0xf8), 64), + 8, + ); + if rip.as_concrete().is_some() || rip.is_unknown() { + return Vec::new(); + } - // Extract concrete register values - for (name, value) in path.state.registers() { - if let Some(concrete) = model.eval(value) { - solved.registers.insert(name.clone(), concrete); + let mut resumed = Vec::new(); + let candidates = self.runtime_continuation_candidate_targets(root, scope, state); + let candidates_checked = candidates.len(); + for candidate in candidates { + let candidate_value = crate::SymValue::concrete(candidate, rip.bits()); + if !self.solver.can_be_equal(state, &rip, &candidate_value) { + continue; } + let mut forked = state.fork(); + forked.active = true; + forked.exit_status = None; + forked.constrain_eq(&rip, candidate); + forked.pc = candidate; + forked.set_prev_pc(None); + forked.clear_pending_exception(); + resumed.push(forked); } + if !resumed.is_empty() { + debug_target_guidance_log(&format!( + "symbolic_exception_resume_forks count={} candidates_checked={}", + resumed.len(), + candidates_checked + )); + } + resumed + } - // Include explicitly tracked symbolic inputs. - for (name, value) in path.state.symbolic_inputs() { - if let Some(concrete) = model.eval(value) { - solved.inputs.entry(name.clone()).or_insert(concrete); - } + fn resolve_scope_function<'a>( + &self, + root: &'a SsaArtifact, + scope: Option<&'a PreparedFunctionScope>, + pc: u64, + ) -> Option<&'a SsaArtifact> { + if root.get_block(pc).is_some() { + return Some(root); } + scope + .and_then(|scope| scope.function_containing_block(pc)) + .map(|function| &function.prepared) + } - // Try to identify symbolic inputs (variables starting with "sym_"). - for (name, value) in path.state.registers() { - if solved.inputs.contains_key(name) { - continue; - } - if (name.starts_with("sym_") || value.is_symbolic()) - && let Some(concrete) = model.eval(value) - { - solved.inputs.insert(name.clone(), concrete); + fn exception_bridge_guidance_target( + &mut self, + root: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + target_addr: u64, + ) -> Option { + if !self.exception_bridge_guidance_enabled { + return None; + } + let scope = scope?; + let target_func = self.resolve_scope_function(root, Some(scope), target_addr); + if target_func.is_some_and(|target_func| target_func.entry == root.entry) { + let distances = self.target_distance_map(root, target_addr); + if distances.contains_key(&root.entry) { + return None; } } - // Extract tracked symbolic memory buffers. - for region in path.state.symbolic_memory() { - if let Some(bytes) = model.eval_bytes(®ion.value, region.size as usize) { - solved.memory.insert(region.name.clone(), bytes); + let observations = observe_call_arguments(root, &AbiProfile::windows_x64()); + let mut registration_sites = Vec::new(); + let mut raise_sites = Vec::new(); + + for (call_id, call) in &root.call_sites().by_id { + let Some(target) = root.resolved_call_target(call) else { + continue; + }; + match self.executor.call_hook_tag(target) { + Some(crate::executor::CallHookTag::WindowsAddVectoredExceptionHandler) => { + if observations.contains_key(call_id) { + if let Some((block_addr, _)) = root.inst_op_site(call.at) { + registration_sites.push(block_addr); + } else { + registration_sites.push(0); + } + } + } + Some(crate::executor::CallHookTag::WindowsRaiseException) => { + if let Some((block_addr, _)) = root.inst_op_site(call.at) { + raise_sites.push(block_addr); + } + } + _ => {} } } - for input in path.state.symbolic_fd_inputs().values() { - let bytes: Option> = input - .bytes - .iter() - .map(|byte| model.eval(byte).map(|v| v as u8)) - .collect(); - if let Some(bytes) = bytes { - solved.input_buffers.insert(input.name.clone(), bytes); - } + raise_sites.sort_unstable(); + raise_sites.dedup(); + registration_sites.sort_unstable(); + registration_sites.dedup(); + + if raise_sites.is_empty() || registration_sites.is_empty() { + return None; } - Some(solved) + raise_sites.into_iter().next() } - /// Solve all feasible paths and return concrete solutions. - pub fn solve_all_paths(&self, paths: &[PathResult<'ctx>]) -> Vec> { - paths.iter().map(|p| self.solve_path(p)).collect() + pub(crate) fn exception_bridge_target_in_scope( + &mut self, + root: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + target_addr: u64, + ) -> Option { + self.exception_bridge_guidance_target(root, scope, target_addr) } - /// Whether the most recent exploration stopped because of a budget limit. - pub fn budget_exhausted(&self) -> bool { - self.stats.timed_out || self.stats.max_states_exhausted + pub(crate) fn advance_current_block_in_scope( + &mut self, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + mut state: SymState<'ctx>, + ) -> Result>, String> { + let mut active_states = Vec::new(); + if let Some(dispatched) = self.dispatch_runtime_breakpoint(&mut state) { + active_states.push(dispatched); + } + let Some(block) = self.resolve_block(func, scope, &state) else { + let missing_pc = state.pc; + self.record_runtime_missing_block(&state, missing_pc); + return Err(format!("missing block at 0x{missing_pc:x}")); + }; + let block_addr = block.runtime_addr; + let block_static_addr = block.static_addr; + let block_func = block.func; + + let mut enqueue_forked_state = |explorer: &mut Self, mut forked: SymState<'ctx>| { + explorer.remap_state_pc_after_block(&mut forked, block); + forked.set_prev_pc(Some(block_addr)); + if forked.is_terminated() { + explorer.record_runtime_exit_status(&forked.exit_status); + explorer.stats.paths_completed += 1; + } else { + active_states.push(forked); + } + }; + + match self.executor.execute_block(&mut state, block.block) { + Ok(forked_states) => { + self.record_depth(state.depth); + for forked in forked_states { + enqueue_forked_state(self, forked); + } + if state.is_terminated() { + self.record_runtime_exit_status(&state.exit_status); + self.stats.paths_completed += 1; + } else { + self.patch_pending_exception_resume_pc( + block_func, + &mut state, + block_static_addr, + ); + self.finalize_active_state_after_block( + block_func, + &mut state, + block_addr, + block_static_addr, + ); + active_states.push(state); + } + Ok(active_states) + } + Err(error) => Err(error.to_string()), + } } - fn record_depth(&mut self, depth: usize) { - if depth > self.stats.max_depth_reached { - self.stats.max_depth_reached = depth; + fn resolve_block<'a>( + &self, + func: &'a SsaArtifact, + scope: Option<&'a PreparedFunctionScope>, + state: &SymState<'ctx>, + ) -> Option> { + if let Some(block) = func.get_block(state.pc) { + return Some(ResolvedBlock { + func, + block, + static_addr: state.pc, + runtime_addr: state.pc, + runtime_aliased: false, + }); + } + if let Some(scope_func) = self.resolve_scope_function(func, scope, state.pc) + && let Some(block) = scope_func.get_block(state.pc) + { + return Some(ResolvedBlock { + func: scope_func, + block, + static_addr: state.pc, + runtime_addr: state.pc, + runtime_aliased: false, + }); } + let static_addr = state.resolve_runtime_pc(state.pc)?; + let scope_func = self.resolve_scope_function(func, scope, static_addr)?; + let block = scope_func.get_block(static_addr)?; + Some(ResolvedBlock { + func: scope_func, + block, + static_addr, + runtime_addr: state.pc, + runtime_aliased: true, + }) } - fn state_rank(&self, state: &SymState<'ctx>, id: usize) -> (usize, usize, usize) { - (state.num_constraints(), state.depth, id) + fn effective_static_pc(&self, state: &SymState<'ctx>) -> u64 { + state.resolve_runtime_pc(state.pc).unwrap_or(state.pc) } - fn prune_subsumed_same_pc_state( + fn dispatch_runtime_breakpoint( &mut self, - worklist: &mut StateWorklist<'ctx>, - state: SymState<'ctx>, + state: &mut SymState<'ctx>, ) -> Option> { - if !self.config.subsumption_states || state.is_terminated() { - return Some(state); + if state.dispatch_runtime_breakpoint_if_ready() { + return None; } - - let candidate_ids = worklist.same_pc_ids(state.pc); - if candidate_ids.is_empty() { - return Some(state); + if let Some(breakpoint) = &state.runtime().active_breakpoint + && breakpoint.breakpoint.as_concrete().is_none() + { + let pc = crate::SymValue::concrete(state.pc, breakpoint.breakpoint.bits()); + if !self.solver.can_be_equal(state, &breakpoint.breakpoint, &pc) { + self.stats.runtime_symbolic_breakpoint_pruned += 1; + return None; + } } + let dispatched = state.fork_symbolic_runtime_breakpoint_at(state.pc); + if dispatched.is_some() { + self.stats.runtime_symbolic_breakpoint_forks += 1; + } + dispatched + } - let state_fingerprint = state.semantic_fingerprint(); - let state_rank = self.state_rank(&state, worklist.slots.len()); - let mut keep_state = true; - - for candidate_id in candidate_ids { - let Some(existing) = worklist.state(candidate_id) else { - continue; - }; - if existing.is_terminated() || existing.semantic_fingerprint() != state_fingerprint { - continue; + fn summarize_runtime_breakpoint_loop( + &mut self, + root: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + block_func: &SsaArtifact, + block: &r2ssa::FunctionSSABlock, + state: &SymState<'ctx>, + ) -> Option> { + let branch = loops::runtime_counter_threshold_branch(block)?; + if !self.residual_runtime_loop_summaries_enabled + && let Some(exact) = + self.exact_runtime_breakpoint_loop_runahead(root, scope, block, state, &branch) + { + let summary = loops::bounded_exact_summary(block.addr, &branch, 0, exact); + self.stats.runtime_breakpoint_loop_exact_summaries += 1; + debug_target_guidance_log(&format!( + "runtime_loop_exact_summary target=0x{:x} from=0x{:x}", + branch.target, block.addr + )); + return summary.resulting_state; + } + let summary = loops::summarize_residual_runtime_loop( + self._ctx, + block_func, + block, + state, + &branch, + self.residual_runtime_loop_summaries_enabled, + ); + match summary.kind { + LoopSummaryKind::Exact | LoopSummaryKind::BoundedExact => { + self.stats.runtime_breakpoint_loop_exact_summaries += 1; + if summary.kind == LoopSummaryKind::Exact { + self.stats.runtime_loop_exact_recurrence_summaries += 1; + } + self.record_exact_loop_recurrences(summary.exact_recurrences.iter().cloned()); + summary.resulting_state } - - self.stats.subsumption_checks += 1; - let existing_subsumes_new = self.solver.implies(&state, existing); - let new_subsumes_existing = self.solver.implies(existing, &state); - - match (existing_subsumes_new, new_subsumes_existing) { - (Some(true), Some(true)) => { - let existing_rank = self.state_rank(existing, candidate_id); - if existing_rank <= state_rank { - self.stats.subsumption_hits += 1; - self.stats.states_subsumed += 1; - keep_state = false; - break; + LoopSummaryKind::Residual => { + for reason in &summary.reasons { + if reason == "runtime_loop_unknown_carried_state" { + self.stats.runtime_loop_unknown_carried_state += 1; } - if worklist.remove_slot(candidate_id).is_some() { - self.stats.subsumption_hits += 1; - self.stats.states_subsumed += 1; + if reason == "runtime_loop_iteration_budget" { + self.stats.runtime_loop_budget_residuals += 1; } } - (Some(true), _) => { - self.stats.subsumption_hits += 1; - self.stats.states_subsumed += 1; - keep_state = false; - break; - } - (_, Some(true)) => { - if worklist.remove_slot(candidate_id).is_some() { - self.stats.subsumption_hits += 1; - self.stats.states_subsumed += 1; - } + if summary.resulting_state.is_some() { + self.stats.runtime_breakpoint_loop_summaries += 1; } - _ => {} + debug_target_guidance_log(&format!( + "runtime_loop_summary kind={:?} target={} from=0x{:x} iterations={} carried={} reasons={}", + summary.kind, + summary + .exit_target + .map(|target| format!("0x{target:x}")) + .unwrap_or_else(|| "none".to_string()), + summary.header, + summary + .iterations + .map(|iterations| iterations.to_string()) + .unwrap_or_else(|| "unknown".to_string()), + summary.carried_state.len(), + summary.reasons.join(","), + )); + summary.resulting_state + } + LoopSummaryKind::Refused => { + self.stats.runtime_loop_refusals += 1; + debug_target_guidance_log(&format!( + "runtime_loop_summary_refused block=0x{:x} reasons={}", + summary.header, + summary.reasons.join(","), + )); + None } } - - keep_state.then_some(state) } - fn drive( + fn record_exact_loop_recurrences( &mut self, - func: &SsaArtifact, - initial_state: SymState<'ctx>, - mode: &mut DriverMode<'ctx>, + recurrences: impl IntoIterator, ) { - let start_time = Instant::now(); - let mut worklist = StateWorklist::new(self.config.strategy); - worklist.push(initial_state); - - while let Some(mut state) = worklist.pop_next() { - if self.config.merge_states - && let Some(other) = worklist.take_same_pc(state.pc) - { - state = state.merge_with(&other); - } - - if let Some(timeout) = self.config.timeout - && start_time.elapsed() > timeout - && mode.on_timeout() - { - self.stats.timed_out = true; + const MAX_RECORDED_EXACT_RECURRENCES: usize = 128; + for recurrence in recurrences { + if self.stats.runtime_loop_exact_recurrences.len() >= MAX_RECORDED_EXACT_RECURRENCES { break; } - - match mode.on_state_popped(self, state) { - DriverAction::Continue(next_state) => state = *next_state, - DriverAction::Skip => continue, - DriverAction::Finish => break, + if !self + .stats + .runtime_loop_exact_recurrences + .contains(&recurrence) + { + self.stats.runtime_loop_exact_recurrences.push(recurrence); } + } + self.stats + .runtime_loop_exact_recurrences + .sort_by(|lhs, rhs| { + ( + lhs.header, + lhs.exit_target, + lhs.accumulator.as_str(), + format!("{:?}", lhs.kind), + ) + .cmp(&( + rhs.header, + rhs.exit_target, + rhs.accumulator.as_str(), + format!("{:?}", rhs.kind), + )) + }); + let folds = self + .stats + .runtime_loop_exact_recurrences + .iter() + .filter_map(ExactLoopRecurrenceEvidence::as_fold) + .collect::>(); + self.record_exact_loop_folds(folds); + } - if self.stats.states_explored >= self.config.max_states && mode.on_max_states() { - self.stats.max_states_exhausted = true; + fn record_exact_loop_folds(&mut self, folds: impl IntoIterator) { + const MAX_RECORDED_EXACT_FOLDS: usize = 128; + for fold in folds { + if self.stats.runtime_loop_exact_folds.len() >= MAX_RECORDED_EXACT_FOLDS { break; } - - if state.depth >= self.config.max_depth { - match mode.on_depth_limit(self, state, self.config.max_completed_paths) { - DriverAction::Continue(next_state) => state = *next_state, - DriverAction::Skip => continue, - DriverAction::Finish => break, - } - } - - self.stats.states_explored += 1; - - if self.config.prune_infeasible && !self.solver.is_sat(&state) { - self.stats.paths_pruned += 1; - mode.on_unsat_pruned(); - continue; - } - - let block_addr = state.pc; - let Some(block) = func.get_block(block_addr) else { - match mode.on_missing_block( - self, - state, - block_addr, - self.config.max_completed_paths, - ) { - DriverAction::Finish => break, - DriverAction::Continue(_) | DriverAction::Skip => continue, - } - }; - - match self.executor.execute_block(&mut state, block) { - Ok(forked_states) => { - self.record_depth(state.depth); - - for mut forked in forked_states { - forked.set_prev_pc(Some(block_addr)); - if mode.allow_enqueue(&forked) - && let Some(forked) = - self.prune_subsumed_same_pc_state(&mut worklist, forked) - { - worklist.push(forked); - } - } - - if !state.is_terminated() { - if state.pc == block_addr - && let Some(next) = self.fallthrough_target(func, block_addr) - { - state.pc = next; - } - state.set_prev_pc(Some(block_addr)); - if mode.allow_enqueue(&state) - && let Some(state) = - self.prune_subsumed_same_pc_state(&mut worklist, state) - { - worklist.push(state); - } - } else { - match mode.on_terminated_state( - self, - state, - block_addr, - self.config.max_completed_paths, - ) { - DriverAction::Finish => break, - DriverAction::Continue(_) | DriverAction::Skip => continue, - } - } - } - Err(e) => match mode.on_execute_error( - self, - state, - block_addr, - e.to_string(), - self.config.max_completed_paths, - ) { - DriverAction::Finish => break, - DriverAction::Continue(_) | DriverAction::Skip => continue, - }, + if !self.stats.runtime_loop_exact_folds.contains(&fold) { + self.stats.runtime_loop_exact_folds.push(fold); } } - - self.stats.total_time = start_time.elapsed(); + self.stats.runtime_loop_exact_folds.sort_by(|lhs, rhs| { + ( + lhs.header, + lhs.exit_target, + lhs.accumulator.as_str(), + lhs.term.addr.as_str(), + ) + .cmp(&( + rhs.header, + rhs.exit_target, + rhs.accumulator.as_str(), + rhs.term.addr.as_str(), + )) + }); } - fn target_guidance_context( + fn exact_runtime_breakpoint_loop_runahead( &mut self, - func: &SsaArtifact, - target_addr: u64, - ) -> TargetGuidanceContext { - let distances = self.target_distance_map(func, target_addr); - let reachable_blocks = distances.keys().copied().collect::>(); - let mut call_targets_by_block: HashMap> = HashMap::new(); - let mut block_summary_rank: HashMap = HashMap::new(); - - for call in func.call_sites().by_id.values() { - let Some(target) = call.direct_target else { - continue; - }; - let Some((block_addr, _)) = func.inst_op_site(call.at) else { - continue; - }; - call_targets_by_block - .entry(block_addr) - .or_default() - .push(target); - - let Some(binding) = self.derived_call_summaries.get(&target) else { - continue; - }; - let entry = block_summary_rank - .entry(block_addr) - .or_insert_with(|| BlockSummaryRank { - has_summary: false, - has_exact_summary: false, - min_case_count: usize::MAX, - }); - entry.has_summary = true; - entry.has_exact_summary |= matches!( - binding.summary.completion, - crate::sim::DerivedSummaryCompletion::Exact - ); - entry.min_case_count = entry.min_case_count.min(binding.summary.cases.len()); + root: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + block: &r2ssa::FunctionSSABlock, + state: &SymState<'ctx>, + branch: &loops::RuntimeLoopBranch, + ) -> Option> { + if state.pending_exception().is_none() || state.runtime().runtime_regions.is_empty() { + debug_target_guidance_log(&format!( + "runtime_loop_exact_skip block=0x{:x} reason=missing_runtime_context", + block.addr + )); + return None; } - - TargetGuidanceContext { - distances, - reachable_blocks, - call_targets_by_block, - block_summary_rank, + let mut target_probe = state.fork(); + target_probe.pc = branch.target; + if self.resolve_block(root, scope, &target_probe).is_none() { + debug_target_guidance_log(&format!( + "runtime_loop_exact_skip block=0x{:x} reason=missing_target target=0x{:x}", + block.addr, branch.target + )); + return None; } - } - - fn symbolic_fanout_proxy(&self, state: &SymState<'ctx>) -> usize { - state - .registers() - .values() - .filter(|value| value.is_symbolic()) - .count() - .saturating_add(state.symbolic_inputs().len()) - .saturating_add(state.symbolic_memory().len()) - .saturating_add(state.symbolic_fd_inputs().len()) - } - - fn state_summary_guidance( - &self, - guidance: &TargetGuidanceContext, - state: &SymState<'ctx>, - ) -> StateSummaryGuidance { - let Some(targets) = guidance.call_targets_by_block.get(&state.pc) else { - return StateSummaryGuidance::default(); + let Some(counter) = loops::concrete_state_var_at_block_entry(state, block, &branch.counter) + else { + debug_target_guidance_log(&format!( + "runtime_loop_exact_skip block=0x{:x} reason=unknown_counter counter={}", + block.addr, + branch.counter.display_name() + )); + return None; }; - - let mut result = StateSummaryGuidance { - summary_hits: 0, - min_feasible_cases: usize::MAX, - contradictory: false, + let Some(remaining) = branch.threshold.checked_sub(counter) else { + debug_target_guidance_log(&format!( + "runtime_loop_exact_skip block=0x{:x} reason=counter_past_threshold counter={} threshold={}", + block.addr, counter, branch.threshold + )); + return None; }; + if !(8..=EXACT_RUNTIME_LOOP_MAX_ITERS).contains(&remaining) { + debug_target_guidance_log(&format!( + "runtime_loop_exact_skip block=0x{:x} reason=iteration_budget remaining={} threshold={}", + block.addr, remaining, branch.threshold + )); + return None; + } - for target in targets { - let Some(binding) = self.derived_call_summaries.get(target) else { - continue; + let mut runahead = state.fork(); + let mut guard_visits = 0u64; + for block_step in 0..EXACT_RUNTIME_LOOP_MAX_BLOCK_STEPS { + if let Some(dispatched) = self.dispatch_runtime_breakpoint(&mut runahead) { + runahead = dispatched; + } + let static_pc = self.effective_static_pc(&runahead); + if static_pc == branch.target { + debug_target_guidance_log(&format!( + "runtime_loop_exact_summary target=0x{:x} from=0x{:x} guard_visits={} block_steps={}", + branch.target, block.addr, guard_visits, block_step + )); + return Some(runahead); + } + if runahead.is_terminated() { + debug_target_guidance_log(&format!( + "runtime_loop_exact_skip block=0x{:x} reason=terminated block_steps={} status={:?}", + block.addr, block_step, runahead.exit_status + )); + return None; + } + if static_pc == block.addr { + guard_visits = guard_visits.saturating_add(1); + if guard_visits > remaining.saturating_add(1) { + debug_target_guidance_log(&format!( + "runtime_loop_exact_skip block=0x{:x} reason=guard_visit_budget visits={} remaining={}", + block.addr, guard_visits, remaining + )); + return None; + } + } + let Some(resolved) = self.resolve_block(root, scope, &runahead) else { + debug_target_guidance_log(&format!( + "runtime_loop_exact_skip block=0x{:x} reason=missing_block static_pc=0x{:x} runtime_pc=0x{:x}", + block.addr, static_pc, runahead.pc + )); + return None; }; - let summary_guidance = evaluate_derived_summary_guidance( - state, - &binding.summary, - &binding.callconv, - &self.solver, - ); - if !summary_guidance.summary_known { - continue; + let forked = self + .executor + .execute_block(&mut runahead, resolved.block) + .ok()?; + if !forked.is_empty() { + debug_target_guidance_log(&format!( + "runtime_loop_exact_skip block=0x{:x} reason=forked block_steps={} forks={}", + block.addr, + block_step, + forked.len() + )); + return None; } - result.summary_hits += 1; - result.min_feasible_cases = result - .min_feasible_cases - .min(summary_guidance.feasible_cases.max(1)); - if summary_guidance.contradictory { - result.contradictory = true; - break; + if runahead.is_terminated() { + debug_target_guidance_log(&format!( + "runtime_loop_exact_skip block=0x{:x} reason=terminated block_steps={} status={:?}", + block.addr, block_step, runahead.exit_status + )); + return None; } + self.patch_pending_exception_resume_pc( + resolved.func, + &mut runahead, + resolved.static_addr, + ); + self.finalize_active_state_after_block( + resolved.func, + &mut runahead, + resolved.runtime_addr, + resolved.static_addr, + ); } - - if result.summary_hits == 0 { - StateSummaryGuidance::default() - } else { - result - } + debug_target_guidance_log(&format!( + "runtime_loop_exact_skip block=0x{:x} reason=block_step_budget budget={}", + block.addr, EXACT_RUNTIME_LOOP_MAX_BLOCK_STEPS + )); + None } - fn target_enqueue_allowed( - &mut self, - guidance: &TargetGuidanceContext, - state: &SymState<'ctx>, - ) -> bool { - if !guidance.reachable_blocks.contains(&state.pc) { - self.stats.target_pruned_cfg_unreachable += 1; - return false; - } + fn direct_call_fork_targets( + &self, + root: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + block: &r2ssa::FunctionSSABlock, + ) -> Option> { + let targets = block + .ops + .iter() + .filter_map(|op| match op { + SSAOp::Call { target } => loops::parse_address_var(target), + _ => None, + }) + .filter(|target| self.executor.call_hook_tag(*target).is_none()) + .filter(|target| self.resolve_scope_function(root, scope, *target).is_some()) + .collect::>(); + (!targets.is_empty()).then_some(targets) + } - let summary_guidance = self.state_summary_guidance(guidance, state); - if summary_guidance.summary_hits > 0 { - self.stats.target_summary_rank_hits += 1; + fn remap_state_pc_after_block(&self, state: &mut SymState<'ctx>, block: ResolvedBlock<'_>) { + if !block.runtime_aliased || state.pc == block.runtime_addr { + return; } - if summary_guidance.contradictory { - self.stats.target_pruned_summary_contradiction += 1; - return false; + if let Some(runtime_pc) = state.remap_static_pc_to_runtime(state.pc) { + state.pc = runtime_pc; } - true } - fn drive_target_guided( - &mut self, - func: &SsaArtifact, - initial_state: SymState<'ctx>, - mode: &mut DriverMode<'ctx>, - target_addr: u64, - ) { - let start_time = Instant::now(); - let mut worklist = StateWorklist::new(self.config.strategy); - let mut target_heap: BinaryHeap> = BinaryHeap::new(); - let guidance = self.target_guidance_context(func, target_addr); - if self.target_enqueue_allowed(&guidance, &initial_state) - && let Some(state) = self.prune_subsumed_same_pc_state(&mut worklist, initial_state) - { - let id = worklist.push(state); - if let Some(state) = worklist.state(id) { - target_heap.push(Reverse(TargetGuidedQueueEntry { - rank: self.state_target_rank(state, id, &guidance), - id, - })); - } + /// Create a new path explorer. + pub fn new(ctx: &'ctx Context) -> Self { + Self { + _ctx: ctx, + executor: SymExecutor::new(ctx), + solver: SymSolver::new(ctx), + config: ExploreConfig::default(), + stats: ExploreStats::default(), + target_guided_queries: false, + exception_bridge_guidance_enabled: true, + residual_runtime_loop_summaries_enabled: true, + derived_call_summaries: HashMap::new(), + target_distance_cache: HashMap::new(), + solve_tactic_config: SolveTacticConfig::default(), } + } - while let Some((mut state, reordered)) = - self.pop_target_guided_state(&mut worklist, &mut target_heap) - { - if reordered { - self.stats.target_guided_reorders += 1; - } - if self.config.merge_states - && let Some(other) = worklist.take_same_pc(state.pc) - { - state = state.merge_with(&other); - } - - if let Some(timeout) = self.config.timeout - && start_time.elapsed() > timeout - && mode.on_timeout() - { - self.stats.timed_out = true; - break; - } - - match mode.on_state_popped(self, state) { - DriverAction::Continue(next_state) => state = *next_state, - DriverAction::Skip => continue, - DriverAction::Finish => break, - } - - if self.stats.states_explored >= self.config.max_states && mode.on_max_states() { - self.stats.max_states_exhausted = true; - break; - } - - if state.depth >= self.config.max_depth { - match mode.on_depth_limit(self, state, self.config.max_completed_paths) { - DriverAction::Continue(next_state) => state = *next_state, - DriverAction::Skip => continue, - DriverAction::Finish => break, - } - } - - self.stats.states_explored += 1; - - if self.config.prune_infeasible && !self.solver.is_sat(&state) { - self.stats.paths_pruned += 1; - mode.on_unsat_pruned(); - continue; - } - - let block_addr = state.pc; - let Some(block) = func.get_block(block_addr) else { - match mode.on_missing_block( - self, - state, - block_addr, - self.config.max_completed_paths, - ) { - DriverAction::Finish => break, - DriverAction::Continue(_) | DriverAction::Skip => continue, - } - }; - - match self.executor.execute_block(&mut state, block) { - Ok(forked_states) => { - self.record_depth(state.depth); - - for mut forked in forked_states { - forked.set_prev_pc(Some(block_addr)); - if mode.allow_enqueue(&forked) - && self.target_enqueue_allowed(&guidance, &forked) - && let Some(forked) = - self.prune_subsumed_same_pc_state(&mut worklist, forked) - { - let id = worklist.push(forked); - if let Some(state) = worklist.state(id) { - target_heap.push(Reverse(TargetGuidedQueueEntry { - rank: self.state_target_rank(state, id, &guidance), - id, - })); - } - } - } + /// Create a path explorer with configuration. + pub fn with_config(ctx: &'ctx Context, config: ExploreConfig) -> Self { + let solver = if let Some(timeout) = config.timeout { + SymSolver::with_timeout(ctx, timeout) + } else { + SymSolver::new(ctx) + }; - if !state.is_terminated() { - if state.pc == block_addr - && let Some(next) = self.fallthrough_target(func, block_addr) - { - state.pc = next; - } - state.set_prev_pc(Some(block_addr)); - if mode.allow_enqueue(&state) - && self.target_enqueue_allowed(&guidance, &state) - && let Some(state) = - self.prune_subsumed_same_pc_state(&mut worklist, state) - { - let id = worklist.push(state); - if let Some(state) = worklist.state(id) { - target_heap.push(Reverse(TargetGuidedQueueEntry { - rank: self.state_target_rank(state, id, &guidance), - id, - })); - } - } - } else { - match mode.on_terminated_state( - self, - state, - block_addr, - self.config.max_completed_paths, - ) { - DriverAction::Finish => break, - DriverAction::Continue(_) | DriverAction::Skip => continue, - } - } - } - Err(e) => match mode.on_execute_error( - self, - state, - block_addr, - e.to_string(), - self.config.max_completed_paths, - ) { - DriverAction::Finish => break, - DriverAction::Continue(_) | DriverAction::Skip => continue, - }, - } + Self { + _ctx: ctx, + executor: SymExecutor::new(ctx), + solver, + config, + stats: ExploreStats::default(), + target_guided_queries: false, + exception_bridge_guidance_enabled: true, + residual_runtime_loop_summaries_enabled: true, + derived_call_summaries: HashMap::new(), + target_distance_cache: HashMap::new(), + solve_tactic_config: SolveTacticConfig::default(), } + } - self.stats.total_time = start_time.elapsed(); + /// Get the exploration statistics. + pub fn stats(&self) -> &ExploreStats { + &self.stats } - fn target_distance_map(&mut self, func: &SsaArtifact, target_addr: u64) -> HashMap { - let key = (func.entry, target_addr); - if let Some(cached) = self.target_distance_cache.get(&key) { - return cached.clone(); - } - let distances = self.compute_target_distance_map(func, target_addr); - if self.target_distance_cache.len() >= TARGET_DISTANCE_CACHE_LIMIT { - self.target_distance_cache.clear(); - } - self.target_distance_cache.insert(key, distances.clone()); - distances + pub fn set_solve_tactic_config(&mut self, config: SolveTacticConfig) { + self.solve_tactic_config = config; } - fn compute_target_distance_map( - &self, - func: &SsaArtifact, - target_addr: u64, - ) -> HashMap { - let mut predecessors: HashMap> = HashMap::new(); - for addr in func.cfg().block_addrs() { - let Some(block) = func.cfg().get_block(addr) else { - continue; - }; - for succ in block.successors() { - predecessors.entry(succ).or_default().push(addr); - } - } + pub fn solve_tactic_config(&self) -> &SolveTacticConfig { + &self.solve_tactic_config + } - let mut distances: HashMap = HashMap::new(); - let mut queue = VecDeque::new(); - distances.insert(target_addr, 0); - queue.push_back(target_addr); + pub(crate) fn with_isolated_stats( + &mut self, + f: impl FnOnce(&mut Self) -> T, + ) -> (T, ExploreStats) { + let previous = std::mem::take(&mut self.stats); + let result = f(self); + let isolated = std::mem::take(&mut self.stats); + self.stats = previous; + (result, isolated) + } - while let Some(addr) = queue.pop_front() { - let distance = distances[&addr]; - for pred in predecessors.get(&addr).into_iter().flatten() { - if distances.contains_key(pred) { - continue; - } - distances.insert(*pred, distance.saturating_add(1)); - queue.push_back(*pred); - } - } + /// Get the solver for additional queries. + pub fn solver(&self) -> &SymSolver<'ctx> { + &self.solver + } - distances + pub(crate) fn with_prune_infeasible( + &mut self, + prune_infeasible: bool, + f: impl FnOnce(&mut Self) -> T, + ) -> T { + let previous = self.config.prune_infeasible; + self.config.prune_infeasible = prune_infeasible; + let result = f(self); + self.config.prune_infeasible = previous; + result } - fn pop_target_guided_state( + pub(crate) fn with_exception_bridge_guidance( &mut self, - worklist: &mut StateWorklist<'ctx>, - heap: &mut BinaryHeap>, - ) -> Option<(SymState<'ctx>, bool)> { - loop { - while let Some(Reverse(entry)) = heap.peek() { - if worklist.state(entry.id).is_some() { - break; - } - heap.pop(); - } + enabled: bool, + f: impl FnOnce(&mut Self) -> T, + ) -> T { + let previous = self.exception_bridge_guidance_enabled; + self.exception_bridge_guidance_enabled = enabled; + let result = f(self); + self.exception_bridge_guidance_enabled = previous; + result + } - let Reverse(entry) = heap.pop()?; - let default_candidate = worklist.default_candidate(); - let reordered = default_candidate.is_some_and(|candidate| candidate != entry.id); - if let Some(state) = worklist.take_slot(entry.id) { - return Some((state, reordered)); - } - } + pub(crate) fn with_residual_runtime_loop_summaries( + &mut self, + enabled: bool, + f: impl FnOnce(&mut Self) -> T, + ) -> T { + let previous = self.residual_runtime_loop_summaries_enabled; + self.residual_runtime_loop_summaries_enabled = enabled; + let result = f(self); + self.residual_runtime_loop_summaries_enabled = previous; + result } - fn state_target_rank( - &self, - state: &SymState<'ctx>, - id: usize, - guidance: &TargetGuidanceContext, - ) -> (bool, usize, usize, usize, usize, usize, usize, usize, usize) { - let reachable = guidance.reachable_blocks.contains(&state.pc); - let distance = guidance - .distances - .get(&state.pc) - .copied() - .unwrap_or(usize::MAX); - let summary_rank = guidance - .block_summary_rank - .get(&state.pc) - .cloned() - .unwrap_or_default(); - let summary_known_penalty = usize::from(!summary_rank.has_summary); - let summary_exact_penalty = usize::from(!summary_rank.has_exact_summary); - let summary_case_rank = if summary_rank.has_summary { - summary_rank.min_case_count - } else { - usize::MAX - }; - ( - !reachable, - distance, - summary_known_penalty, - summary_exact_penalty, - summary_case_rank, - self.symbolic_fanout_proxy(state), - state.num_constraints(), - state.depth, - id, - ) + /// Enable target-guided ordering for query-only helpers. + pub fn set_target_guided_queries(&mut self, enabled: bool) { + self.target_guided_queries = enabled; } - /// Explore all paths in a function. - pub fn explore( - &mut self, - func: &SsaArtifact, - initial_state: SymState<'ctx>, - ) -> Vec> { - if self.config.max_completed_paths == Some(0) { - return Vec::new(); - } - let mut mode = DriverMode::Explore { - results: Vec::new(), - }; - self.drive(func, initial_state, &mut mode); - match mode { - DriverMode::Explore { results } => results, - _ => unreachable!("explore should always use explore mode"), - } + /// Whether target-guided ordering is enabled for query helpers. + pub fn target_guided_queries_enabled(&self) -> bool { + self.target_guided_queries } - fn fallthrough_target(&self, func: &SsaArtifact, block_addr: u64) -> Option { - let block = func.cfg().get_block(block_addr)?; - match block.terminator { - BlockTerminator::Fallthrough { next } => Some(next), - BlockTerminator::ConditionalBranch { false_target, .. } => Some(false_target), - BlockTerminator::Call { fallthrough, .. } => fallthrough, - BlockTerminator::IndirectCall { fallthrough } => fallthrough, - BlockTerminator::Branch { target } => Some(target), - _ => None, - } + /// Register a call hook for a concrete target address. + pub fn register_call_hook(&mut self, addr: u64, hook: F) + where + F: Fn(&mut SymState<'ctx>) -> crate::executor::CallHookResult + 'ctx, + { + self.derived_call_summaries.remove(&addr); + self.executor + .register_call_hook(addr, move |state| Ok(hook(state))); } - /// Explore paths to find inputs that reach a target address. - pub fn find_path_to( + pub fn register_tagged_call_hook( &mut self, - func: &SsaArtifact, - initial_state: SymState<'ctx>, - target_addr: u64, - ) -> Option> { - let mut mode = DriverMode::FindFirst { - target_addr, - found: None, - }; - if self.target_guided_queries { - self.drive_target_guided(func, initial_state, &mut mode, target_addr); - } else { - self.drive(func, initial_state, &mut mode); - } - match mode { - DriverMode::FindFirst { found, .. } => found, - _ => unreachable!("find_path_to should always use first-match mode"), - } + addr: u64, + tag: crate::executor::CallHookTag, + hook: F, + ) where + F: Fn(&mut SymState<'ctx>) -> crate::executor::CallHookResult + 'ctx, + { + self.derived_call_summaries.remove(&addr); + self.executor + .register_tagged_call_hook(addr, tag, move |state| Ok(hook(state))); } - /// Explore paths to collect all feasible states that reach a target address. - pub fn find_paths_to( + pub(crate) fn register_derived_call_hook( &mut self, - func: &SsaArtifact, - initial_state: SymState<'ctx>, - target_addr: u64, - ) -> Vec> { - let mut mode = DriverMode::FindAll { - target_addr, - matches: Vec::new(), - }; - if self.target_guided_queries { - self.drive_target_guided(func, initial_state, &mut mode, target_addr); - } else { - self.drive(func, initial_state, &mut mode); - } - match mode { - DriverMode::FindAll { matches, .. } => matches, - _ => unreachable!("find_paths_to should always use all-match mode"), - } + addr: u64, + summary: Rc>, + callconv: CallConv, + hook: F, + ) where + F: Fn(&mut SymState<'ctx>) -> crate::executor::CallHookResult + 'ctx, + { + self.derived_call_summaries + .insert(addr, RegisteredDerivedCallSummary { summary, callconv }); + self.executor + .register_call_hook(addr, move |state| Ok(hook(state))); } - /// Explore paths to find inputs that avoid a target address. - pub fn find_path_avoiding( - &mut self, - func: &SsaArtifact, - initial_state: SymState<'ctx>, - avoid_addrs: &[u64], - ) -> Option> { - let mut mode = DriverMode::Avoid { - avoid_set: avoid_addrs.iter().copied().collect(), - found: None, - }; - self.drive(func, initial_state, &mut mode); - match mode { - DriverMode::Avoid { found, .. } => found, - _ => unreachable!("find_path_avoiding should always use avoid mode"), - } + pub(crate) fn derived_call_summary_views(&self) -> HashMap> { + self.derived_call_summaries + .iter() + .map(|(addr, registered)| { + ( + *addr, + DerivedCallSummaryView { + summary: registered.summary.clone(), + callconv: registered.callconv.clone(), + }, + ) + }) + .collect() } - /// Run a typed exploration specification. - pub fn run_spec( - &mut self, - func: &SsaArtifact, - initial_state: SymState<'ctx>, - spec: &ExplorationSpec, - ) -> Result, String> { - let mut mode = DriverMode::Spec { - find_set: spec.find_addresses()?.into_iter().collect(), - avoid_set: spec.avoid_addresses()?.into_iter().collect(), - max_finds: spec.max_finds(), - result: SpecExploreResult::default(), - }; - self.drive(func, initial_state, &mut mode); - match mode { - DriverMode::Spec { result, .. } => Ok(result), - _ => unreachable!("run_spec should always use spec mode"), + /// Solve a path's constraints and extract concrete values. + /// + /// Returns None if the path is infeasible. + pub fn solve_path(&self, path: &PathResult<'ctx>) -> Option { + if !path.feasible { + return None; } - } -} -#[cfg(test)] -mod tests { - use super::*; - use std::rc::Rc; + // Get a model from the solver + let model = self.solver.solve(&path.state)?; - use r2il::{ArchSpec, R2ILBlock, R2ILOp, RegisterDef, SpaceId, Varnode}; - use r2ssa::{InterprocFunctionId, SsaArtifact}; - use z3::ast::BV; + let mut solved = SolvedPath { + final_pc: path.state.pc, + num_constraints: path.state.num_constraints(), + ..Default::default() + }; - use crate::SymValue; - use crate::executor::CallHookResult; - use crate::sim::{ - CallConv, DerivedFunctionSummary, DerivedSummaryCase, DerivedSummaryCompletion, - }; + // Extract concrete register values + for (name, value) in path.state.registers() { + if let Some(concrete) = model.eval(value) { + solved.registers.insert(name.clone(), concrete); + } + } - const RDI: u64 = 56; - const TMP0: u64 = 0x80; + // Include explicitly tracked symbolic inputs. + for (name, value) in path.state.symbolic_inputs() { + if let Some(concrete) = model.eval(value) { + solved.inputs.entry(name.clone()).or_insert(concrete); + } + } - fn make_reg(offset: u64, size: u32) -> Varnode { - Varnode { - space: SpaceId::Register, - offset, - size, - meta: None, + // Try to identify symbolic inputs (variables starting with "sym_"). + for (name, value) in path.state.registers() { + if solved.inputs.contains_key(name) { + continue; + } + if (name.starts_with("sym_") || value.is_symbolic()) + && let Some(concrete) = model.eval(value) + { + solved.inputs.insert(name.clone(), concrete); + } + } + + // Extract tracked symbolic memory buffers. + for region in path.state.symbolic_memory() { + if let Some(bytes) = model.eval_bytes(®ion.value, region.size as usize) { + solved.memory.insert(region.name.clone(), bytes); + } + } + + for input in path.state.symbolic_fd_inputs().values() { + let bytes: Option> = input + .bytes + .iter() + .map(|byte| model.eval(byte).map(|v| v as u8)) + .collect(); + if let Some(bytes) = bytes { + solved.input_buffers.insert(input.name.clone(), bytes); + } } + + Some(solved) } - fn make_const(val: u64, size: u32) -> Varnode { - Varnode { - space: SpaceId::Const, - offset: val, - size, - meta: None, + /// Solve a path after applying constraint-graph tactics and exact input-domain tactics. + pub fn solve_path_with_constraint_graph_tactics( + &self, + path: &PathResult<'ctx>, + graph: &FinalConstraintGraph, + recurrences: &[ExactLoopRecurrenceEvidence], + ) -> Option { + if !path.feasible || !self.solve_tactic_config.enabled { + return None; + } + let folds = loops::exact_fold_evidence_from_recurrences(recurrences); + + if !graph.is_empty() { + for candidate in tactic_candidates_for_constraint_graph( + graph, + Some(&path.state), + &self.solve_tactic_config, + ) { + let mut state = path.state.fork(); + let report = constrain_exact_recurrence_candidate( + &mut state, + &candidate.recurrence, + &candidate.bytes, + ); + if report.constrained_bytes == 0 { + continue; + } + let constrained_path = PathResult { + state, + exit_status: path.exit_status.clone(), + depth: path.depth, + feasible: path.feasible, + }; + if let Some(mut solution) = self.solve_path(&constrained_path) { + solution.generation = Some(SolvedPathGeneration { + kind: if candidate.used_mitm { + SolvedPathGenerationKind::MitmConstraintTactic + } else { + SolvedPathGenerationKind::ExactRecurrenceConstraintTactic + }, + reason: candidate.reason, + constrained_bytes: report.constrained_bytes, + }); + return Some(solution); + } + } + } + + for domain in &self.solve_tactic_config.preferred_domains { + let mut state = path.state.fork(); + let report = constrain_exact_fold_inputs( + &mut state, + &folds, + domain, + self.solve_tactic_config.max_constrained_bytes, + ); + if report.constrained_bytes == 0 { + continue; + } + let constrained_path = PathResult { + state, + exit_status: path.exit_status.clone(), + depth: path.depth, + feasible: path.feasible, + }; + if let Some(mut solution) = self.solve_path(&constrained_path) { + solution.generation = Some(SolvedPathGeneration { + kind: SolvedPathGenerationKind::DomainConstraintTactic, + reason: "exact fold input-domain constraint".to_string(), + constrained_bytes: report.constrained_bytes, + }); + return Some(solution); + } } + None } - fn make_x86_64_arch() -> ArchSpec { - let mut arch = ArchSpec::new("x86-64"); - arch.addr_size = 8; - arch.add_register(RegisterDef::new("RDI", RDI, 8)); - arch + /// Solve all feasible paths and return concrete solutions. + pub fn solve_all_paths(&self, paths: &[PathResult<'ctx>]) -> Vec> { + paths.iter().map(|p| self.solve_path(p)).collect() } - #[test] - fn test_explore_config_default() { - let config = ExploreConfig::default(); - assert_eq!(config.max_states, 1000); - assert_eq!(config.max_completed_paths, None); - assert_eq!(config.max_depth, 100); - assert!(config.prune_infeasible); - assert!(!config.subsumption_states); + /// Whether the most recent exploration stopped because of a budget limit. + pub fn budget_exhausted(&self) -> bool { + self.stats.timed_out || self.stats.max_states_exhausted } - #[test] - fn test_path_explorer_creation() { - let ctx = Context::thread_local(); + fn record_depth(&mut self, depth: usize) { + if depth > self.stats.max_depth_reached { + self.stats.max_depth_reached = depth; + } + } + + fn state_rank(&self, state: &SymState<'ctx>, id: usize) -> (usize, usize, usize) { + (state.num_constraints(), state.depth, id) + } + + fn prune_subsumed_same_pc_state( + &mut self, + worklist: &mut StateWorklist<'ctx>, + state: SymState<'ctx>, + ) -> Option> { + if !self.config.subsumption_states || state.is_terminated() { + return Some(state); + } + + let candidate_ids = worklist.same_pc_ids(state.pc); + if candidate_ids.is_empty() { + return Some(state); + } + + let state_fingerprint = state.semantic_fingerprint(); + let state_rank = self.state_rank(&state, worklist.slots.len()); + let mut keep_state = true; + + for candidate_id in candidate_ids { + let Some(existing) = worklist.state(candidate_id) else { + continue; + }; + if existing.is_terminated() || existing.semantic_fingerprint() != state_fingerprint { + continue; + } + + self.stats.subsumption_checks += 1; + let existing_subsumes_new = self.solver.implies(&state, existing); + let new_subsumes_existing = self.solver.implies(existing, &state); + + match (existing_subsumes_new, new_subsumes_existing) { + (Some(true), Some(true)) => { + let existing_rank = self.state_rank(existing, candidate_id); + if existing_rank <= state_rank { + self.stats.subsumption_hits += 1; + self.stats.states_subsumed += 1; + keep_state = false; + break; + } + if worklist.remove_slot(candidate_id).is_some() { + self.stats.subsumption_hits += 1; + self.stats.states_subsumed += 1; + } + } + (Some(true), _) => { + self.stats.subsumption_hits += 1; + self.stats.states_subsumed += 1; + keep_state = false; + break; + } + (_, Some(true)) => { + if worklist.remove_slot(candidate_id).is_some() { + self.stats.subsumption_hits += 1; + self.stats.states_subsumed += 1; + } + } + _ => {} + } + } + + keep_state.then_some(state) + } + + fn drive_in_scope( + &mut self, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + initial_state: SymState<'ctx>, + mode: &mut DriverMode<'ctx>, + ) { + let start_time = Instant::now(); + let mut worklist = StateWorklist::new(self.config.strategy); + worklist.push(initial_state); + + 'worklist: while let Some(mut state) = worklist.pop_next() { + if self.config.merge_states + && let Some(other) = worklist.take_same_pc(state.pc) + { + state = state.merge_with(&other); + } + + if let Some(timeout) = self.config.timeout + && start_time.elapsed() > timeout + && mode.on_timeout() + { + self.stats.timed_out = true; + break; + } + + match mode.on_state_popped(self, state) { + DriverAction::Continue(next_state) => state = *next_state, + DriverAction::Skip => continue, + DriverAction::Finish => break, + } + + if self.stats.states_explored >= self.config.max_states && mode.on_max_states() { + self.stats.max_states_exhausted = true; + break; + } + + if state.depth >= self.config.max_depth { + match mode.on_depth_limit(self, state, self.config.max_completed_paths) { + DriverAction::Continue(next_state) => state = *next_state, + DriverAction::Skip => continue, + DriverAction::Finish => break, + } + } + + self.stats.states_explored += 1; + + if self.config.prune_infeasible && !self.solver.is_sat(&state) { + self.stats.paths_pruned += 1; + mode.on_unsat_pruned(); + continue; + } + + let inline_start_depth = state.depth; + let mut revisit_current_state = false; + loop { + if let Some(timeout) = self.config.timeout + && start_time.elapsed() > timeout + && mode.on_timeout() + { + self.stats.timed_out = true; + break 'worklist; + } + + if std::mem::take(&mut revisit_current_state) { + match mode.on_state_popped(self, state) { + DriverAction::Continue(next_state) => state = *next_state, + DriverAction::Skip => continue 'worklist, + DriverAction::Finish => break 'worklist, + } + } + + let Some(block) = self.resolve_block(func, scope, &state) else { + let missing_pc = state.pc; + match mode.on_missing_block( + self, + state, + missing_pc, + self.config.max_completed_paths, + ) { + DriverAction::Finish => break 'worklist, + DriverAction::Continue(_) | DriverAction::Skip => continue 'worklist, + } + }; + let block_addr = block.runtime_addr; + let block_static_addr = block.static_addr; + let block_func = block.func; + match self.executor.execute_block(&mut state, block.block) { + Ok(forked_states) => { + self.record_depth(state.depth); + + let had_forks = !forked_states.is_empty(); + for mut forked in forked_states { + self.remap_state_pc_after_block(&mut forked, block); + forked.set_prev_pc(Some(block_addr)); + if mode.allow_enqueue(&forked) + && let Some(forked) = + self.prune_subsumed_same_pc_state(&mut worklist, forked) + { + worklist.push(forked); + } + } + + if !state.is_terminated() { + self.patch_pending_exception_resume_pc( + block_func, + &mut state, + block_static_addr, + ); + self.finalize_active_state_after_block( + block_func, + &mut state, + block_addr, + block_static_addr, + ); + if mode.allow_enqueue(&state) { + let can_inline_continue = !had_forks + && state.depth.saturating_sub(inline_start_depth) + < self.max_inline_runahead_depth_delta(); + if can_inline_continue { + revisit_current_state = true; + continue; + } + if let Some(state) = + self.prune_subsumed_same_pc_state(&mut worklist, state) + { + worklist.push(state); + } + } + } else { + match mode.on_terminated_state( + self, + state, + block_addr, + self.config.max_completed_paths, + ) { + DriverAction::Finish => break 'worklist, + DriverAction::Continue(_) | DriverAction::Skip => { + continue 'worklist; + } + } + } + break; + } + Err(e) => match mode.on_execute_error( + self, + state, + block_addr, + e.to_string(), + self.config.max_completed_paths, + ) { + DriverAction::Finish => break 'worklist, + DriverAction::Continue(_) | DriverAction::Skip => continue 'worklist, + }, + } + } + } + + self.stats.total_time = start_time.elapsed(); + } + + fn target_guidance_context( + &mut self, + func: &SsaArtifact, + target_addr: u64, + allow_cross_function_states: bool, + ) -> TargetGuidanceContext { + let distances = self.target_distance_map(func, target_addr); + let reachable_blocks = distances.keys().copied().collect::>(); + let mut call_targets_by_block: HashMap> = HashMap::new(); + let mut block_summary_rank: HashMap = HashMap::new(); + + for call in func.call_sites().by_id.values() { + let Some(target) = func.resolved_call_target(call) else { + continue; + }; + let Some((block_addr, _)) = func.inst_op_site(call.at) else { + continue; + }; + call_targets_by_block + .entry(block_addr) + .or_default() + .push(target); + + let Some(binding) = self.derived_call_summaries.get(&target) else { + continue; + }; + let entry = block_summary_rank + .entry(block_addr) + .or_insert_with(|| BlockSummaryRank { + has_summary: false, + has_exact_summary: false, + min_case_count: usize::MAX, + }); + entry.has_summary = true; + entry.has_exact_summary |= matches!( + binding.summary.completion, + crate::sim::DerivedSummaryCompletion::Exact + ); + entry.min_case_count = entry.min_case_count.min(binding.summary.cases.len()); + } + + TargetGuidanceContext { + target_addr, + distances, + reachable_blocks, + call_targets_by_block, + block_summary_rank, + allow_cross_function_states, + } + } + + fn symbolic_fanout_proxy(&self, state: &SymState<'ctx>) -> usize { + state + .registers() + .values() + .filter(|value| value.is_symbolic()) + .count() + .saturating_add(state.symbolic_inputs().len()) + .saturating_add(state.symbolic_memory().len()) + .saturating_add(state.symbolic_fd_inputs().len()) + } + + fn state_summary_guidance( + &self, + guidance: &TargetGuidanceContext, + state: &SymState<'ctx>, + ) -> StateSummaryGuidance { + let static_pc = self.effective_static_pc(state); + let Some(targets) = guidance.call_targets_by_block.get(&static_pc) else { + return StateSummaryGuidance::default(); + }; + + let mut result = StateSummaryGuidance { + summary_hits: 0, + min_feasible_cases: usize::MAX, + contradictory: false, + }; + + for target in targets { + let Some(binding) = self.derived_call_summaries.get(target) else { + continue; + }; + let summary_guidance = evaluate_derived_summary_guidance( + state, + &binding.summary, + &binding.callconv, + &self.solver, + ); + if !summary_guidance.summary_known { + continue; + } + result.summary_hits += 1; + result.min_feasible_cases = result + .min_feasible_cases + .min(summary_guidance.feasible_cases.max(1)); + if summary_guidance.contradictory { + result.contradictory = true; + break; + } + } + + if result.summary_hits == 0 { + StateSummaryGuidance::default() + } else { + result + } + } + + fn target_enqueue_allowed( + &mut self, + guidance: &TargetGuidanceContext, + state: &SymState<'ctx>, + ) -> bool { + let static_pc = self.effective_static_pc(state); + let runtime_continuation_bridge = + state.pending_exception().is_some() || state.runtime_region_for_pc(state.pc).is_some(); + if !guidance.reachable_blocks.contains(&static_pc) + && !guidance.allow_cross_function_states + && !runtime_continuation_bridge + { + self.stats.target_pruned_cfg_unreachable += 1; + debug_target_guidance_log(&format!( + "cfg_prune target=0x{:x} runtime_pc=0x{:x} static_pc=0x{:x} prev_pc={} terminated={} depth={}", + guidance.target_addr, + state.pc, + static_pc, + state + .prev_pc() + .map(|pc| format!("0x{pc:x}")) + .unwrap_or_else(|| "none".to_string()), + state.is_terminated(), + state.depth, + )); + return false; + } + + let summary_guidance = self.state_summary_guidance(guidance, state); + if summary_guidance.summary_hits > 0 { + self.stats.target_summary_rank_hits += 1; + } + if summary_guidance.contradictory { + self.stats.target_pruned_summary_contradiction += 1; + return false; + } + true + } + + fn enqueue_target_guided_state( + &mut self, + worklist: &mut StateWorklist<'ctx>, + target_heap: &mut BinaryHeap>, + guidance: &TargetGuidanceContext, + mode: &mut DriverMode<'ctx>, + state: SymState<'ctx>, + ) { + if !mode.allow_enqueue(&state) || !self.target_enqueue_allowed(guidance, &state) { + return; + } + let Some(state) = self.prune_subsumed_same_pc_state(worklist, state) else { + return; + }; + let id = worklist.push(state); + if let Some(state) = worklist.state(id) { + target_heap.push(Reverse(TargetGuidedQueueEntry { + rank: self.state_target_rank(state, id, guidance), + id, + })); + } + } + + fn drive_target_guided_in_scope( + &mut self, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + initial_state: SymState<'ctx>, + mode: &mut DriverMode<'ctx>, + target_addr: u64, + allow_cross_function_states: bool, + ) { + let start_time = Instant::now(); + let mut worklist = StateWorklist::new(self.config.strategy); + let mut target_heap: BinaryHeap> = BinaryHeap::new(); + let guidance = self.target_guidance_context(func, target_addr, allow_cross_function_states); + if self.target_enqueue_allowed(&guidance, &initial_state) + && let Some(state) = self.prune_subsumed_same_pc_state(&mut worklist, initial_state) + { + let id = worklist.push(state); + if let Some(state) = worklist.state(id) { + target_heap.push(Reverse(TargetGuidedQueueEntry { + rank: self.state_target_rank(state, id, &guidance), + id, + })); + } + } + + 'worklist: while let Some((mut state, reordered)) = + self.pop_target_guided_state(&mut worklist, &mut target_heap) + { + if reordered { + self.stats.target_guided_reorders += 1; + } + if self.config.merge_states + && let Some(other) = worklist.take_same_pc(state.pc) + { + state = state.merge_with(&other); + } + + if let Some(timeout) = self.config.timeout + && start_time.elapsed() > timeout + && mode.on_timeout() + { + self.stats.timed_out = true; + break; + } + + match mode.on_state_popped(self, state) { + DriverAction::Continue(next_state) => state = *next_state, + DriverAction::Skip => continue, + DriverAction::Finish => break, + } + + if self.stats.states_explored >= self.config.max_states && mode.on_max_states() { + self.stats.max_states_exhausted = true; + break; + } + + if state.depth >= self.config.max_depth { + match mode.on_depth_limit(self, state, self.config.max_completed_paths) { + DriverAction::Continue(next_state) => state = *next_state, + DriverAction::Skip => continue, + DriverAction::Finish => break, + } + } + + self.stats.states_explored += 1; + + if self.config.prune_infeasible && !self.solver.is_sat(&state) { + self.stats.paths_pruned += 1; + debug_target_guidance_log(&format!( + "unsat_prune target=0x{:x} runtime_pc=0x{:x} static_pc=0x{:x} prev_pc={} terminated={} depth={} constraints={}", + guidance.target_addr, + state.pc, + self.effective_static_pc(&state), + state + .prev_pc() + .map(|pc| format!("0x{pc:x}")) + .unwrap_or_else(|| "none".to_string()), + state.is_terminated(), + state.depth, + state.num_constraints(), + )); + mode.on_unsat_pruned(); + continue; + } + + let inline_start_depth = state.depth; + let mut revisit_current_state = false; + loop { + let mut had_runtime_dispatch = false; + if let Some(timeout) = self.config.timeout + && start_time.elapsed() > timeout + && mode.on_timeout() + { + self.stats.timed_out = true; + break 'worklist; + } + + if std::mem::take(&mut revisit_current_state) { + match mode.on_state_popped(self, state) { + DriverAction::Continue(next_state) => state = *next_state, + DriverAction::Skip => continue 'worklist, + DriverAction::Finish => break 'worklist, + } + } + + if let Some(dispatched) = self.dispatch_runtime_breakpoint(&mut state) { + had_runtime_dispatch = true; + self.enqueue_target_guided_state( + &mut worklist, + &mut target_heap, + &guidance, + mode, + dispatched, + ); + } + + if !self.target_enqueue_allowed(&guidance, &state) { + continue 'worklist; + } + + let Some(block) = self.resolve_block(func, scope, &state) else { + let missing_pc = state.pc; + match mode.on_missing_block( + self, + state, + missing_pc, + self.config.max_completed_paths, + ) { + DriverAction::Finish => break 'worklist, + DriverAction::Continue(_) | DriverAction::Skip => continue 'worklist, + } + }; + let block_addr = block.runtime_addr; + let block_static_addr = block.static_addr; + let block_func = block.func; + debug_target_guidance_log(&format!( + "block_exec target=0x{:x} runtime_pc=0x{:x} static_pc=0x{:x} depth={} states={} constraints={}", + guidance.target_addr, + block_addr, + block_static_addr, + state.depth, + self.stats.states_explored, + state.num_constraints(), + )); + + if let Some(summarized) = self.summarize_runtime_breakpoint_loop( + func, + scope, + block_func, + block.block, + &state, + ) { + self.enqueue_target_guided_state( + &mut worklist, + &mut target_heap, + &guidance, + mode, + summarized, + ); + continue 'worklist; + } + + let direct_call_targets = self.direct_call_fork_targets(func, scope, block.block); + if let Some(targets) = direct_call_targets.as_ref() { + let mut targets = targets.iter().copied().collect::>(); + targets.sort_unstable(); + debug_target_guidance_log(&format!( + "direct_call_fork_targets block=0x{:x} targets={:?}", + block_static_addr, targets + )); + } + let previous_direct_call_targets = self + .executor + .replace_direct_call_fork_targets(direct_call_targets); + let execution = self.executor.execute_block(&mut state, block.block); + self.executor + .replace_direct_call_fork_targets(previous_direct_call_targets); + + match execution { + Ok(forked_states) => { + self.record_depth(state.depth); + debug_target_guidance_log(&format!( + "block_done target=0x{:x} runtime_pc=0x{:x} static_pc=0x{:x} depth={} forks={} terminated={}", + guidance.target_addr, + block_addr, + block_static_addr, + state.depth, + forked_states.len(), + state.is_terminated(), + )); + + let had_forks = !forked_states.is_empty(); + for mut forked in forked_states { + self.remap_state_pc_after_block(&mut forked, block); + forked.set_prev_pc(Some(block_addr)); + self.enqueue_target_guided_state( + &mut worklist, + &mut target_heap, + &guidance, + mode, + forked, + ); + } + + if !state.is_terminated() { + self.patch_pending_exception_resume_pc( + block_func, + &mut state, + block_static_addr, + ); + self.finalize_active_state_after_block( + block_func, + &mut state, + block_addr, + block_static_addr, + ); + if mode.allow_enqueue(&state) + && self.target_enqueue_allowed(&guidance, &state) + { + let can_inline_continue = !had_forks + && !had_runtime_dispatch + && state.depth.saturating_sub(inline_start_depth) + < self.max_inline_runahead_depth_delta(); + if can_inline_continue { + revisit_current_state = true; + continue; + } + if let Some(state) = + self.prune_subsumed_same_pc_state(&mut worklist, state) + { + let id = worklist.push(state); + if let Some(state) = worklist.state(id) { + target_heap.push(Reverse(TargetGuidedQueueEntry { + rank: self.state_target_rank(state, id, &guidance), + id, + })); + } + } + } + } else { + let resumed = + self.fork_symbolic_exception_resume_targets(func, scope, &state); + if !resumed.is_empty() { + for resumed_state in resumed { + self.enqueue_target_guided_state( + &mut worklist, + &mut target_heap, + &guidance, + mode, + resumed_state, + ); + } + continue 'worklist; + } + match mode.on_terminated_state( + self, + state, + block_addr, + self.config.max_completed_paths, + ) { + DriverAction::Finish => break 'worklist, + DriverAction::Continue(_) | DriverAction::Skip => { + continue 'worklist; + } + } + } + break; + } + Err(e) => match mode.on_execute_error( + self, + state, + block_addr, + e.to_string(), + self.config.max_completed_paths, + ) { + DriverAction::Finish => break 'worklist, + DriverAction::Continue(_) | DriverAction::Skip => continue 'worklist, + }, + } + } + } + + self.stats.total_time = start_time.elapsed(); + } + + fn target_distance_map(&mut self, func: &SsaArtifact, target_addr: u64) -> HashMap { + let key = (func.entry, target_addr); + if let Some(cached) = self.target_distance_cache.get(&key) { + return cached.clone(); + } + let distances = self.compute_target_distance_map(func, target_addr); + if self.target_distance_cache.len() >= TARGET_DISTANCE_CACHE_LIMIT { + self.target_distance_cache.clear(); + } + self.target_distance_cache.insert(key, distances.clone()); + distances + } + + fn compute_target_distance_map( + &self, + func: &SsaArtifact, + target_addr: u64, + ) -> HashMap { + let mut predecessors: HashMap> = HashMap::new(); + for addr in func.cfg().block_addrs() { + let Some(block) = func.cfg().get_block(addr) else { + continue; + }; + for succ in block.successors() { + predecessors.entry(succ).or_default().push(addr); + } + } + + let mut distances: HashMap = HashMap::new(); + let mut queue = VecDeque::new(); + distances.insert(target_addr, 0); + queue.push_back(target_addr); + + while let Some(addr) = queue.pop_front() { + let distance = distances[&addr]; + for pred in predecessors.get(&addr).into_iter().flatten() { + if distances.contains_key(pred) { + continue; + } + distances.insert(*pred, distance.saturating_add(1)); + queue.push_back(*pred); + } + } + + distances + } + + fn pop_target_guided_state( + &mut self, + worklist: &mut StateWorklist<'ctx>, + heap: &mut BinaryHeap>, + ) -> Option<(SymState<'ctx>, bool)> { + loop { + while let Some(Reverse(entry)) = heap.peek() { + if worklist.state(entry.id).is_some() { + break; + } + heap.pop(); + } + + let Reverse(entry) = heap.pop()?; + let default_candidate = worklist.default_candidate(); + let reordered = default_candidate.is_some_and(|candidate| candidate != entry.id); + if let Some(state) = worklist.take_slot(entry.id) { + return Some((state, reordered)); + } + } + } + + fn state_target_rank( + &self, + state: &SymState<'ctx>, + id: usize, + guidance: &TargetGuidanceContext, + ) -> ( + usize, + usize, + usize, + usize, + usize, + usize, + usize, + usize, + usize, + usize, + ) { + let static_pc = self.effective_static_pc(state); + let reachable = guidance.reachable_blocks.contains(&static_pc); + let runtime_continuation_bridge = + state.pending_exception().is_some() || state.runtime_region_for_pc(state.pc).is_some(); + let runtime_walk_penalty = usize::from( + state.pending_exception().is_none() && state.runtime_region_for_pc(state.pc).is_some(), + ); + let reachability_penalty = usize::from(!(reachable || runtime_continuation_bridge)); + let distance = if reachable { + guidance + .distances + .get(&static_pc) + .copied() + .unwrap_or(usize::MAX) + } else if runtime_continuation_bridge { + 0 + } else { + usize::MAX + }; + let summary_rank = guidance + .block_summary_rank + .get(&static_pc) + .cloned() + .unwrap_or_default(); + let summary_known_penalty = usize::from(!summary_rank.has_summary); + let summary_exact_penalty = usize::from(!summary_rank.has_exact_summary); + let summary_case_rank = if summary_rank.has_summary { + summary_rank.min_case_count + } else { + usize::MAX + }; + ( + reachability_penalty, + distance, + summary_known_penalty, + summary_exact_penalty, + summary_case_rank, + runtime_walk_penalty, + self.symbolic_fanout_proxy(state), + state.num_constraints(), + state.depth, + id, + ) + } + + /// Explore all paths in a function. + pub fn explore( + &mut self, + func: &SsaArtifact, + initial_state: SymState<'ctx>, + ) -> Vec> { + self.explore_in_scope(func, None, initial_state) + } + + pub fn explore_in_scope( + &mut self, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + initial_state: SymState<'ctx>, + ) -> Vec> { + if self.config.max_completed_paths == Some(0) { + return Vec::new(); + } + let mut mode = DriverMode::Explore { + results: Vec::new(), + }; + self.drive_in_scope(func, scope, initial_state, &mut mode); + match mode { + DriverMode::Explore { results } => results, + _ => unreachable!("explore should always use explore mode"), + } + } + + fn fallthrough_target(&self, func: &SsaArtifact, block_addr: u64) -> Option { + let block = func.cfg().get_block(block_addr)?; + match block.terminator { + BlockTerminator::Fallthrough { next } => Some(next), + BlockTerminator::ConditionalBranch { false_target, .. } => Some(false_target), + BlockTerminator::Call { fallthrough, .. } => fallthrough, + BlockTerminator::IndirectCall { fallthrough } => fallthrough, + BlockTerminator::Branch { target } => Some(target), + _ => None, + } + } + + /// Explore paths to find inputs that reach a target address. + pub fn find_path_to( + &mut self, + func: &SsaArtifact, + initial_state: SymState<'ctx>, + target_addr: u64, + ) -> Option> { + self.find_path_to_in_scope(func, None, initial_state, target_addr) + } + + pub fn find_path_to_in_scope( + &mut self, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + initial_state: SymState<'ctx>, + target_addr: u64, + ) -> Option> { + self.find_path_to_in_scope_with_feasibility(func, scope, initial_state, target_addr, true) + } + + fn find_path_to_in_scope_with_feasibility( + &mut self, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + initial_state: SymState<'ctx>, + target_addr: u64, + require_feasible: bool, + ) -> Option> { + let mut mode = DriverMode::FindFirst { + target_addr, + require_feasible, + found: None, + }; + let bridge_target = self.exception_bridge_guidance_target(func, scope, target_addr); + let guidance_target = bridge_target.unwrap_or(target_addr); + let guidance_func = if bridge_target.is_some() { + Some(func) + } else { + self.resolve_scope_function(func, scope, target_addr) + }; + let allow_cross_function_states = bridge_target.is_none() + && guidance_func.is_some_and(|guidance_func| guidance_func.entry != func.entry); + debug_target_guidance_log(&format!( + "search_setup target=0x{:x} bridge={} guidance_entry={} target_guided={} cross={}", + target_addr, + bridge_target + .map(|target| format!("0x{target:x}")) + .unwrap_or_else(|| "none".to_string()), + guidance_func + .map(|function| format!("0x{:x}", function.entry)) + .unwrap_or_else(|| "none".to_string()), + self.target_guided_queries, + allow_cross_function_states, + )); + if self.target_guided_queries && guidance_func.is_some() { + self.drive_target_guided_in_scope( + guidance_func.unwrap_or(func), + scope, + initial_state, + &mut mode, + guidance_target, + allow_cross_function_states, + ); + } else { + self.drive_in_scope(func, scope, initial_state, &mut mode); + } + match mode { + DriverMode::FindFirst { found, .. } => found, + _ => unreachable!("find_path_to should always use first-match mode"), + } + } + + /// Explore paths to collect all feasible states that reach a target address. + pub fn find_paths_to( + &mut self, + func: &SsaArtifact, + initial_state: SymState<'ctx>, + target_addr: u64, + ) -> Vec> { + self.find_paths_to_in_scope(func, None, initial_state, target_addr) + } + + pub fn find_paths_to_in_scope( + &mut self, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + initial_state: SymState<'ctx>, + target_addr: u64, + ) -> Vec> { + let mut mode = DriverMode::FindAll { + target_addr, + require_feasible: true, + matches: Vec::new(), + }; + let bridge_target = self.exception_bridge_guidance_target(func, scope, target_addr); + let guidance_target = bridge_target.unwrap_or(target_addr); + let guidance_func = if bridge_target.is_some() { + Some(func) + } else { + self.resolve_scope_function(func, scope, target_addr) + }; + let allow_cross_function_states = bridge_target.is_none() + && guidance_func.is_some_and(|guidance_func| guidance_func.entry != func.entry); + if self.target_guided_queries && guidance_func.is_some() { + self.drive_target_guided_in_scope( + guidance_func.unwrap_or(func), + scope, + initial_state, + &mut mode, + guidance_target, + allow_cross_function_states, + ); + } else { + self.drive_in_scope(func, scope, initial_state, &mut mode); + } + match mode { + DriverMode::FindAll { matches, .. } => matches, + _ => unreachable!("find_paths_to should always use all-match mode"), + } + } + + /// Explore paths to find inputs that avoid a target address. + pub fn find_path_avoiding( + &mut self, + func: &SsaArtifact, + initial_state: SymState<'ctx>, + avoid_addrs: &[u64], + ) -> Option> { + let mut mode = DriverMode::Avoid { + avoid_set: avoid_addrs.iter().copied().collect(), + found: None, + }; + self.drive_in_scope(func, None, initial_state, &mut mode); + match mode { + DriverMode::Avoid { found, .. } => found, + _ => unreachable!("find_path_avoiding should always use avoid mode"), + } + } + + /// Run a typed exploration specification. + pub fn run_spec( + &mut self, + func: &SsaArtifact, + initial_state: SymState<'ctx>, + spec: &ExplorationSpec, + ) -> Result, String> { + let mut mode = DriverMode::Spec { + find_set: spec.find_addresses()?.into_iter().collect(), + avoid_set: spec.avoid_addresses()?.into_iter().collect(), + max_finds: spec.max_finds(), + result: SpecExploreResult::default(), + }; + self.drive_in_scope(func, None, initial_state, &mut mode); + match mode { + DriverMode::Spec { result, .. } => Ok(result), + _ => unreachable!("run_spec should always use spec mode"), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::rc::Rc; + + use r2il::{ArchSpec, R2ILBlock, R2ILOp, RegisterDef, SpaceId, Varnode}; + use r2ssa::{InterprocFunctionId, SsaArtifact}; + use z3::ast::BV; + + use crate::SymValue; + use crate::executor::CallHookResult; + use crate::sim::{ + CallConv, DerivedFunctionSummary, DerivedSummaryCase, DerivedSummaryCompletion, + }; + + const RAX: u64 = 0; + const RDI: u64 = 56; + const TMP0: u64 = 0x80; + const TMP1: u64 = 0x88; + const TMP2: u64 = 0x90; + + fn make_reg(offset: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Register, + offset, + size, + meta: None, + } + } + + fn make_const(val: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Const, + offset: val, + size, + meta: None, + } + } + + fn make_ram(addr: u64, size: u32) -> Varnode { + Varnode { + space: SpaceId::Ram, + offset: addr, + size, + meta: None, + } + } + + fn make_x86_64_arch() -> ArchSpec { + let mut arch = ArchSpec::new("x86-64"); + arch.addr_size = 8; + arch.add_register(RegisterDef::new("AL", RAX, 1)); + arch.add_register(RegisterDef::new("EAX", RAX, 4)); + arch.add_register(RegisterDef::new("RAX", RAX, 8)); + arch.add_register(RegisterDef::new("RDI", RDI, 8)); + arch + } + + #[test] + fn test_explore_config_default() { + let config = ExploreConfig::default(); + assert_eq!(config.max_states, 1000); + assert_eq!(config.max_completed_paths, None); + assert_eq!(config.max_depth, 100); + assert!(config.prune_infeasible); + assert!(!config.subsumption_states); + } + + #[test] + fn test_path_explorer_creation() { + let ctx = Context::thread_local(); + + let explorer = PathExplorer::new(&ctx); + assert_eq!(explorer.stats().states_explored, 0); + } + + #[test] + fn test_explore_stats() { + let stats = ExploreStats::default(); + assert_eq!(stats.states_explored, 0); + assert_eq!(stats.paths_completed, 0); + } + + #[test] + fn test_state_worklist_same_pc_lookup_skips_popped_entries() { + let ctx = Context::thread_local(); + let mut worklist = StateWorklist::new(ExploreStrategy::Bfs); + + worklist.push(SymState::new(&ctx, 0x1000)); + worklist.push(SymState::new(&ctx, 0x2000)); + worklist.push(SymState::new(&ctx, 0x1000)); + + let first = worklist.pop_next().expect("first state should exist"); + assert_eq!(first.pc, 0x1000); + + let merged = worklist + .take_same_pc(0x1000) + .expect("queued same-pc state should still be found"); + assert_eq!(merged.pc, 0x1000); + + let remaining = worklist.pop_next().expect("non-merged state should remain"); + assert_eq!(remaining.pc, 0x2000); + assert!(worklist.pop_next().is_none()); + assert!(worklist.take_same_pc(0x1000).is_none()); + } + + #[test] + fn test_target_guided_heap_prefers_best_and_skips_stale_entries() { + let ctx = Context::thread_local(); + let mut explorer = PathExplorer::new(&ctx); + let mut worklist = StateWorklist::new(ExploreStrategy::Dfs); + + let id_a = worklist.push(SymState::new(&ctx, 0x1000)); + let id_b = worklist.push(SymState::new(&ctx, 0x2000)); + let id_c = worklist.push(SymState::new(&ctx, 0x3000)); + + let guidance = TargetGuidanceContext { + target_addr: 0x2000, + distances: HashMap::from([(0x1000, 2), (0x2000, 0), (0x3000, 1)]), + reachable_blocks: HashSet::from([0x1000, 0x2000, 0x3000]), + call_targets_by_block: HashMap::new(), + block_summary_rank: HashMap::new(), + allow_cross_function_states: false, + }; + + let mut heap: BinaryHeap> = BinaryHeap::new(); + for id in [id_a, id_b, id_c] { + let state = worklist.state(id).expect("live state"); + heap.push(Reverse(TargetGuidedQueueEntry { + rank: explorer.state_target_rank(state, id, &guidance), + id, + })); + } + + let (state, reordered) = explorer + .pop_target_guided_state(&mut worklist, &mut heap) + .expect("best state should exist"); + assert_eq!(state.pc, 0x2000); + assert!( + reordered, + "best state should differ from DFS default candidate" + ); + + assert!( + worklist.remove_slot(id_c).is_some(), + "simulate a stale heap entry" + ); + + let (state, reordered) = explorer + .pop_target_guided_state(&mut worklist, &mut heap) + .expect("remaining state should exist"); + assert_eq!(state.pc, 0x1000); + assert!( + !reordered, + "after skipping stale entries, the chosen state should match the live default candidate" + ); + } + + #[test] + fn test_target_distance_map_reuses_cache_for_same_entry_and_target() { + let ctx = Context::thread_local(); + let mut explorer = PathExplorer::new(&ctx); + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x1004, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x1008, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1008, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, None).expect("symbolic function"); + + let first = explorer.target_distance_map(&func, 0x1008); + let second = explorer.target_distance_map(&func, 0x1008); + + assert_eq!(first, second); + assert_eq!(explorer.target_distance_cache.len(), 1); + assert_eq!(first.get(&0x1008), Some(&0)); + assert_eq!(first.get(&0x1004), Some(&1)); + assert_eq!(first.get(&0x1000), Some(&2)); + } + + #[test] + fn test_same_pc_subsumption_prefers_weaker_constraint_state() { + let ctx = Context::thread_local(); + let mut config = ExploreConfig { + subsumption_states: true, + ..ExploreConfig::default() + }; + config.prune_infeasible = false; + let mut explorer = PathExplorer::with_config(&ctx, config); + let mut worklist = StateWorklist::new(ExploreStrategy::Dfs); + + let mut existing = SymState::new(&ctx, 0x1000); + existing.make_symbolic("sym_input", 64); + let input = existing.get_register("sym_input"); + existing.add_constraint(input.to_bv(&ctx).bvult(z3::ast::BV::from_u64(10, 64))); + worklist.push(existing); + + let mut stronger = SymState::new(&ctx, 0x1000); + stronger.make_symbolic("sym_input", 64); + let input = stronger.get_register("sym_input"); + stronger.add_constraint(input.to_bv(&ctx).bvult(z3::ast::BV::from_u64(10, 64))); + stronger.add_constraint(input.to_bv(&ctx).bvult(z3::ast::BV::from_u64(5, 64))); + + let pruned = explorer.prune_subsumed_same_pc_state(&mut worklist, stronger); + assert!( + pruned.is_none(), + "stronger same-pc state should be subsumed" + ); + assert_eq!(explorer.stats.subsumption_checks, 1); + assert_eq!(explorer.stats.subsumption_hits, 1); + assert_eq!(explorer.stats.states_subsumed, 1); + assert_eq!(worklist.live_states, 1); + } + + #[test] + fn test_same_pc_subsumption_replaces_stronger_constraint_state() { + let ctx = Context::thread_local(); + let mut config = ExploreConfig { + subsumption_states: true, + ..ExploreConfig::default() + }; + config.prune_infeasible = false; + let mut explorer = PathExplorer::with_config(&ctx, config); + let mut worklist = StateWorklist::new(ExploreStrategy::Dfs); + + let mut stronger = SymState::new(&ctx, 0x1000); + stronger.make_symbolic("sym_input", 64); + let input = stronger.get_register("sym_input"); + stronger.add_constraint(input.to_bv(&ctx).bvult(z3::ast::BV::from_u64(10, 64))); + stronger.add_constraint(input.to_bv(&ctx).bvult(z3::ast::BV::from_u64(5, 64))); + worklist.push(stronger); + + let mut weaker = SymState::new(&ctx, 0x1000); + weaker.make_symbolic("sym_input", 64); + let input = weaker.get_register("sym_input"); + weaker.add_constraint(input.to_bv(&ctx).bvult(z3::ast::BV::from_u64(10, 64))); + let kept = explorer.prune_subsumed_same_pc_state(&mut worklist, weaker); + + assert!(kept.is_some(), "weaker same-pc state should survive"); + assert_eq!(explorer.stats.subsumption_checks, 1); + assert_eq!(explorer.stats.subsumption_hits, 1); + assert_eq!(explorer.stats.states_subsumed, 1); + assert_eq!(worklist.live_states, 0); + } + + #[test] + fn test_same_pc_subsumption_tie_break_prefers_shallower_state() { + let ctx = Context::thread_local(); + let config = ExploreConfig { + subsumption_states: true, + ..ExploreConfig::default() + }; + let mut explorer = PathExplorer::with_config(&ctx, config); + let mut worklist = StateWorklist::new(ExploreStrategy::Random); + + let mut existing = SymState::new(&ctx, 0x1000); + existing.make_symbolic("sym_input", 64); + existing.step(); + worklist.push(existing); + + let mut new_state = SymState::new(&ctx, 0x1000); + new_state.make_symbolic("sym_input", 64); + let kept = explorer.prune_subsumed_same_pc_state(&mut worklist, new_state); + + assert!( + kept.is_some(), + "shallower state should replace deeper equivalent state" + ); + assert_eq!(worklist.live_states, 0); + assert_eq!(explorer.stats.subsumption_hits, 1); + assert_eq!(explorer.stats.states_subsumed, 1); + } + + #[test] + fn test_path_result_methods() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.set_register("rax", SymValue::concrete(42, 64)); + state.make_symbolic("rbx", 64); + state.set_register("aaa", SymValue::concrete(7, 64)); + + let result = PathResult::new(state, true); + + assert_eq!(result.final_pc(), 0x1000); + assert_eq!(result.num_constraints(), 0); + assert_eq!( + result.register_names(), + vec!["aaa".to_string(), "rax".to_string(), "rbx".to_string()] + ); + assert_eq!(result.get_concrete_register("rax"), Some(42)); + assert!(result.is_register_symbolic("rbx")); + } + + #[test] + fn test_solve_path_with_constraints() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("sym_input", 64); + + // Add constraint: sym_input < 100 + let input = state.get_register("sym_input"); + let hundred = SymValue::concrete(100, 64); + let cmp = input.ult(&ctx, &hundred); + state.add_true_constraint(&cmp); + + let result = PathResult::new(state, true); + let explorer = PathExplorer::new(&ctx); + + let solved = explorer.solve_path(&result); + assert!(solved.is_some()); + + let solved = solved.unwrap(); + assert_eq!(solved.final_pc, 0x1000); + assert_eq!(solved.num_constraints, 1); + + // The input should be less than 100 + if let Some(&value) = solved.inputs.get("sym_input") { + assert!(value < 100, "Input should be < 100, got {}", value); + } + } + + #[test] + fn test_solved_path_default() { + let solved = SolvedPath::default(); + assert!(solved.inputs.is_empty()); + assert!(solved.input_buffers.is_empty()); + assert!(solved.registers.is_empty()); + assert!(solved.memory.is_empty()); + assert_eq!(solved.final_pc, 0); + assert_eq!(solved.num_constraints, 0); + } + + #[test] + fn test_solve_path_emits_stable_map_order() { + let ctx = Context::thread_local(); + + let mut state = SymState::new(&ctx, 0x1000); + state.new_symbolic_input("z_input", 64); + state.new_symbolic_input("a_input", 64); + state.set_register("z_reg", SymValue::concrete(3, 64)); + state.set_register("a_reg", SymValue::concrete(1, 64)); + state.set_register("m_reg", SymValue::concrete(9, 64)); + + let result = PathResult::new(state, true); + let explorer = PathExplorer::new(&ctx); + let solved = explorer.solve_path(&result).expect("path should solve"); + + assert_eq!( + solved.inputs.keys().cloned().collect::>(), + vec!["a_input".to_string(), "z_input".to_string()] + ); + assert_eq!( + solved.registers.keys().cloned().collect::>(), + vec![ + "a_reg".to_string(), + "m_reg".to_string(), + "z_reg".to_string() + ] + ); + } + + #[test] + fn test_target_guided_prunes_exact_summary_contradiction() { + let ctx = Context::thread_local(); + let arch = make_x86_64_arch(); + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![R2ILOp::Call { + target: make_const(0x2000, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1004, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x1010, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x1010, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(TMP0, 1), + src: make_const(1, 1), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, Some(&arch)).expect("symbolic function"); + + let mut explorer = PathExplorer::new(&ctx); + explorer.set_target_guided_queries(true); + + let arg0 = SymValue::new_symbolic(&ctx, "summary_arg0", 64); + let guard = arg0.to_bv(&ctx).eq(BV::from_u64(1, 64)); + let summary = Rc::new(DerivedFunctionSummary { + id: InterprocFunctionId(0x2000), + name: Some("contradict_helper".to_string()), + arg_count_hint: 1, + arg_symbols: vec![(0, arg0)], + memory_inputs: Vec::new(), + cases: vec![DerivedSummaryCase { + guard, + return_value: None, + memory_writes: Vec::new(), + }], + completion: DerivedSummaryCompletion::Exact, + }); + explorer.register_derived_call_hook(0x2000, summary, CallConv::x86_64_sysv(), |_state| { + CallHookResult::Fallthrough + }); + + let mut state = SymState::new(&ctx, 0x1000); + state.set_register("RDI_0", SymValue::concrete(2, 64)); + + let paths = explorer.find_paths_to(&func, state, 0x1010); + assert!(paths.is_empty(), "contradictory exact summary should prune"); + assert_eq!(explorer.stats().target_pruned_summary_contradiction, 1); + } + + #[test] + fn test_runtime_alias_region_becomes_queryable() { + let ctx = Context::thread_local(); + let blocks = vec![ + R2ILBlock { + addr: 0x2000, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x2004, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x2004, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, None).expect("symbolic function"); + let mut state = SymState::new(&ctx, 0x6000_0000); + let _ = state.define_runtime_region("jit_blob", 0x6000_0000, 0x100, true); + state.note_runtime_store_copy( + 0x6000_0000, + 1, + Some(&crate::RuntimeValueProvenance { + source_addr: 0x2000, + size: 1, + }), + ); + + let mut explorer = PathExplorer::new(&ctx); + let found = explorer.find_path_to(&func, state, 0x6000_0004); + + assert!(found.is_some(), "runtime-mapped target should be reachable"); + assert_eq!(explorer.stats().runtime_missing_materialized_code, 0); + } + + #[test] + fn test_runtime_alias_region_matches_static_source_target() { + let ctx = Context::thread_local(); + let blocks = vec![ + R2ILBlock { + addr: 0x2000, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x2004, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x2004, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, None).expect("symbolic function"); + let mut state = SymState::new(&ctx, 0x6000_0000); + let _ = state.define_runtime_region("jit_blob", 0x6000_0000, 0x100, true); + state.note_runtime_store_copy( + 0x6000_0000, + 1, + Some(&crate::RuntimeValueProvenance { + source_addr: 0x2000, + size: 1, + }), + ); + + let mut explorer = PathExplorer::new(&ctx); + let found = explorer.find_path_to(&func, state, 0x2004); + + assert!( + found.is_some(), + "runtime-mapped execution should match the static source target" + ); + } + + #[test] + fn test_block_entry_concrete_lookup_uses_selected_phi_source() { + let ctx = Context::thread_local(); + let counter = r2ssa::SSAVar::new("RCX", 7, 8); + let block = r2ssa::FunctionSSABlock { + addr: 0x1400, + size: 4, + phis: vec![r2ssa::PhiNode { + dst: counter.clone(), + sources: vec![ + (0x1000, r2ssa::SSAVar::new("RCX", 4, 8)), + (0x1200, r2ssa::SSAVar::new("RCX", 6, 8)), + ], + }], + ops: Vec::new(), + }; + let mut state = SymState::new(&ctx, 0x1400); + state.set_prev_pc(Some(0x1000)); + state.set_register("RCX_4", SymValue::concrete(0, 64)); + state.set_register("RCX_6", SymValue::concrete(0xdead_beef_cafe_1337, 64)); + state.set_register("RCX_7", SymValue::concrete(0xcccc_cccc_cccc_cccc, 64)); + + assert_eq!( + loops::concrete_state_var_at_block_entry(&state, &block, &counter), + Some(0), + "loop summaries must read the incoming phi source, not a stale phi destination" + ); + } + + #[test] + fn test_target_guided_follows_scoped_direct_call_thunk() { + let ctx = Context::thread_local(); + let arch = make_x86_64_arch(); + let root_blocks = vec![R2ILBlock { + addr: 0x1000, + size: 4, + ops: vec![ + R2ILOp::Call { + target: make_const(0x2000, 8), + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + switch_info: None, + op_metadata: Default::default(), + }]; + let thunk_blocks = vec![ + R2ILBlock { + addr: 0x2000, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(0x3000, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + R2ILBlock { + addr: 0x3000, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }, + ]; + let root = SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic"); + let thunk = SsaArtifact::for_symbolic(&thunk_blocks, Some(&arch)).expect("thunk symbolic"); + let scope = crate::PreparedFunctionScope::new( + 0x1000, + vec![ + crate::ScopedPreparedFunction { + id: InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + crate::ScopedPreparedFunction { + id: InterprocFunctionId(0x2000), + name: Some("thunk".to_string()), + prepared: thunk, + }, + ], + ) + .expect("scope"); - let explorer = PathExplorer::new(&ctx); - assert_eq!(explorer.stats().states_explored, 0); - } + let mut explorer = PathExplorer::new(&ctx); + explorer.set_target_guided_queries(true); + let found = explorer.find_path_to_in_scope( + &root, + Some(&scope), + SymState::new(&ctx, 0x1000), + 0x3000, + ); - #[test] - fn test_explore_stats() { - let stats = ExploreStats::default(); - assert_eq!(stats.states_explored, 0); - assert_eq!(stats.paths_completed, 0); + assert!( + found.is_some(), + "target-guided queries should follow direct calls into scoped thunk/helper blocks" + ); + assert_eq!(found.unwrap().final_pc(), 0x3000); } #[test] - fn test_state_worklist_same_pc_lookup_skips_popped_entries() { + fn test_runtime_missing_materialized_code_is_tracked() { let ctx = Context::thread_local(); - let mut worklist = StateWorklist::new(ExploreStrategy::Bfs); - - worklist.push(SymState::new(&ctx, 0x1000)); - worklist.push(SymState::new(&ctx, 0x2000)); - worklist.push(SymState::new(&ctx, 0x1000)); - - let first = worklist.pop_next().expect("first state should exist"); - assert_eq!(first.pc, 0x1000); + let blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }]; + let func = SsaArtifact::for_symbolic(&blocks, None).expect("symbolic function"); + let mut state = SymState::new(&ctx, 0x6000_0000); + let _ = state.define_runtime_region("jit_blob", 0x6000_0000, 0x100, true); - let merged = worklist - .take_same_pc(0x1000) - .expect("queued same-pc state should still be found"); - assert_eq!(merged.pc, 0x1000); + let mut explorer = PathExplorer::new(&ctx); + let found = explorer.find_paths_to(&func, state, 0x6000_0004); - let remaining = worklist.pop_next().expect("non-merged state should remain"); - assert_eq!(remaining.pc, 0x2000); - assert!(worklist.pop_next().is_none()); - assert!(worklist.take_same_pc(0x1000).is_none()); + assert!( + found.is_empty(), + "unmaterialized runtime region should not be queryable" + ); + assert_eq!(explorer.stats().runtime_missing_materialized_code, 1); } #[test] - fn test_target_guided_heap_prefers_best_and_skips_stale_entries() { + fn test_deterministic_linear_runahead_reaches_target_past_depth_budget() { let ctx = Context::thread_local(); - let mut explorer = PathExplorer::new(&ctx); - let mut worklist = StateWorklist::new(ExploreStrategy::Dfs); - - let id_a = worklist.push(SymState::new(&ctx, 0x1000)); - let id_b = worklist.push(SymState::new(&ctx, 0x2000)); - let id_c = worklist.push(SymState::new(&ctx, 0x3000)); - - let guidance = TargetGuidanceContext { - distances: HashMap::from([(0x1000, 2), (0x2000, 0), (0x3000, 1)]), - reachable_blocks: HashSet::from([0x1000, 0x2000, 0x3000]), - call_targets_by_block: HashMap::new(), - block_summary_rank: HashMap::new(), - }; - - let mut heap: BinaryHeap> = BinaryHeap::new(); - for id in [id_a, id_b, id_c] { - let state = worklist.state(id).expect("live state"); - heap.push(Reverse(TargetGuidedQueueEntry { - rank: explorer.state_target_rank(state, id, &guidance), - id, - })); + let mut blocks = Vec::new(); + for idx in 0..16u64 { + let addr = 0x1000 + idx * 4; + let next = addr + 4; + let ops = if idx == 15 { + vec![R2ILOp::Return { + target: make_const(0, 8), + }] + } else { + vec![R2ILOp::Branch { + target: make_const(next, 8), + }] + }; + blocks.push(R2ILBlock { + addr, + size: 4, + ops, + switch_info: None, + op_metadata: Default::default(), + }); } + let func = SsaArtifact::for_symbolic(&blocks, None).expect("symbolic function"); - let (state, reordered) = explorer - .pop_target_guided_state(&mut worklist, &mut heap) - .expect("best state should exist"); - assert_eq!(state.pc, 0x2000); - assert!( - reordered, - "best state should differ from DFS default candidate" + let mut explorer = PathExplorer::with_config( + &ctx, + ExploreConfig { + max_depth: 5, + ..ExploreConfig::default() + }, ); + let target = 0x1000 + 15 * 4; + let found = explorer.find_path_to(&func, SymState::new(&ctx, 0x1000), target); assert!( - worklist.remove_slot(id_c).is_some(), - "simulate a stale heap entry" + found.is_some(), + "deterministic linear chain should run ahead to the far target" ); - - let (state, reordered) = explorer - .pop_target_guided_state(&mut worklist, &mut heap) - .expect("remaining state should exist"); - assert_eq!(state.pc, 0x1000); + let found = found.unwrap(); + assert_eq!(found.final_pc(), target); assert!( - !reordered, - "after skipping stale entries, the chosen state should match the live default candidate" + found.depth > 5, + "execution depth should still reflect the concrete block work" ); + assert_eq!(explorer.stats().paths_max_depth, 0); } #[test] - fn test_target_distance_map_reuses_cache_for_same_entry_and_target() { + fn test_target_guided_symbolic_byte_loop_reaches_concrete_terminator() { let ctx = Context::thread_local(); - let mut explorer = PathExplorer::new(&ctx); + let arch = make_x86_64_arch(); let blocks = vec![ R2ILBlock { addr: 0x1000, size: 4, - ops: vec![R2ILOp::Branch { - target: make_const(0x1004, 8), - }], switch_info: None, op_metadata: Default::default(), + ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP0, 1), + addr: make_reg(RDI, 8), + space: SpaceId::Ram, + }, + R2ILOp::IntZExt { + dst: make_reg(RAX, 4), + src: make_reg(TMP0, 1), + }, + R2ILOp::IntZExt { + dst: make_reg(RAX, 8), + src: make_reg(RAX, 4), + }, + R2ILOp::Branch { + target: make_const(0x1010, 8), + }, + ], }, R2ILBlock { - addr: 0x1004, + addr: 0x1010, size: 4, - ops: vec![R2ILOp::Branch { - target: make_const(0x1008, 8), - }], switch_info: None, op_metadata: Default::default(), + ops: vec![ + R2ILOp::IntZExt { + dst: make_reg(RAX, 4), + src: make_reg(RAX, 1), + }, + R2ILOp::IntAdd { + dst: make_reg(RDI, 8), + a: make_reg(RDI, 8), + b: make_const(1, 8), + }, + R2ILOp::Load { + dst: make_reg(TMP0, 1), + addr: make_reg(RDI, 8), + space: SpaceId::Ram, + }, + R2ILOp::IntZExt { + dst: make_reg(RAX, 4), + src: make_reg(TMP0, 1), + }, + R2ILOp::IntZExt { + dst: make_reg(RAX, 8), + src: make_reg(RAX, 4), + }, + R2ILOp::IntAnd { + dst: make_reg(TMP1, 1), + a: make_reg(RAX, 1), + b: make_reg(RAX, 1), + }, + R2ILOp::IntEqual { + dst: make_reg(TMP2, 1), + a: make_reg(TMP1, 1), + b: make_const(0, 1), + }, + R2ILOp::BoolNot { + dst: make_reg(TMP0 + 1, 1), + src: make_reg(TMP2, 1), + }, + R2ILOp::CBranch { + target: make_const(0x1010, 8), + cond: make_reg(TMP0 + 1, 1), + }, + ], }, R2ILBlock { - addr: 0x1008, + addr: 0x1014, size: 1, + switch_info: None, + op_metadata: Default::default(), ops: vec![R2ILOp::Return { target: make_const(0, 8), }], - switch_info: None, - op_metadata: Default::default(), }, ]; - let func = SsaArtifact::for_symbolic(&blocks, None).expect("symbolic function"); + let func = SsaArtifact::for_symbolic(&blocks, Some(&arch)).expect("symbolic function"); - let first = explorer.target_distance_map(&func, 0x1008); - let second = explorer.target_distance_map(&func, 0x1008); + let mut state = SymState::new(&ctx, 0x1000); + let region = state.define_memory_region( + crate::MemoryRegionKind::Input, + "argv_like", + Some(0x2000), + Some(0x10), + ); + state.set_register("RDI_0", SymValue::concrete(0x2000, 64)); + let byte0 = state.new_symbolic_input("loop_byte_0", 8); + let byte1 = state.new_symbolic_input("loop_byte_1", 8); + state.add_constraint(byte0.to_bv(&ctx).eq(BV::from_u64(0, 8)).not()); + state.add_constraint(byte1.to_bv(&ctx).eq(BV::from_u64(0, 8)).not()); + state.mem_write(&SymValue::concrete(0x2000, 64), &byte0, 1); + state.mem_write(&SymValue::concrete(0x2001, 64), &byte1, 1); + state.seed_region_bytes(region, 2, &[0]); - assert_eq!(first, second); - assert_eq!(explorer.target_distance_cache.len(), 1); - assert_eq!(first.get(&0x1008), Some(&0)); - assert_eq!(first.get(&0x1004), Some(&1)); - assert_eq!(first.get(&0x1000), Some(&2)); + let mut explorer = PathExplorer::new(&ctx); + explorer.set_target_guided_queries(true); + let paths = explorer.find_paths_to(&func, state, 0x1014); + + assert_eq!(paths.len(), 1, "terminator exit should remain reachable"); + assert_eq!(paths[0].final_pc(), 0x1014); + assert_eq!( + explorer.stats().target_match_unsat, + 1, + "only the impossible symbolic early exit should be unsat" + ); } #[test] - fn test_same_pc_subsumption_prefers_weaker_constraint_state() { + fn test_target_guidance_keeps_pending_exception_bridge_states() { let ctx = Context::thread_local(); - let mut config = ExploreConfig { - subsumption_states: true, - ..ExploreConfig::default() + let mut explorer = PathExplorer::new(&ctx); + let guidance = TargetGuidanceContext { + target_addr: 0x1000, + distances: HashMap::from([(0x1000, 0)]), + reachable_blocks: HashSet::from([0x1000]), + call_targets_by_block: HashMap::new(), + block_summary_rank: HashMap::new(), + allow_cross_function_states: false, }; - config.prune_infeasible = false; - let mut explorer = PathExplorer::with_config(&ctx, config); - let mut worklist = StateWorklist::new(ExploreStrategy::Dfs); - - let mut existing = SymState::new(&ctx, 0x1000); - existing.make_symbolic("sym_input", 64); - let input = existing.get_register("sym_input"); - existing.add_constraint(input.to_bv(&ctx).bvult(z3::ast::BV::from_u64(10, 64))); - worklist.push(existing); - - let mut stronger = SymState::new(&ctx, 0x1000); - stronger.make_symbolic("sym_input", 64); - let input = stronger.get_register("sym_input"); - stronger.add_constraint(input.to_bv(&ctx).bvult(z3::ast::BV::from_u64(10, 64))); - stronger.add_constraint(input.to_bv(&ctx).bvult(z3::ast::BV::from_u64(5, 64))); + let (_, context_addr) = { + let mut tmp = SymState::new(&ctx, 0x2000); + tmp.allocate_heap_region("context", 0x400) + }; + let mut state = SymState::new(&ctx, 0x2000); + state.set_pending_exception(crate::state::PendingExceptionContinuation { + handler_addr: 0x2000, + exception_code: 0x8000_0004, + exception_pointers_addr: 0x7000_0000, + exception_record_addr: 0x7000_0100, + context_addr, + }); - let pruned = explorer.prune_subsumed_same_pc_state(&mut worklist, stronger); assert!( - pruned.is_none(), - "stronger same-pc state should be subsumed" + explorer.target_enqueue_allowed(&guidance, &state), + "pending exception continuation should bypass root-CFG pruning" ); - assert_eq!(explorer.stats.subsumption_checks, 1); - assert_eq!(explorer.stats.subsumption_hits, 1); - assert_eq!(explorer.stats.states_subsumed, 1); - assert_eq!(worklist.live_states, 1); } #[test] - fn test_same_pc_subsumption_replaces_stronger_constraint_state() { + fn test_target_guidance_keeps_runtime_region_bridge_states() { let ctx = Context::thread_local(); - let mut config = ExploreConfig { - subsumption_states: true, - ..ExploreConfig::default() + let mut explorer = PathExplorer::new(&ctx); + let guidance = TargetGuidanceContext { + target_addr: 0x2004, + distances: HashMap::from([(0x2004, 0)]), + reachable_blocks: HashSet::from([0x2004]), + call_targets_by_block: HashMap::new(), + block_summary_rank: HashMap::new(), + allow_cross_function_states: false, }; - config.prune_infeasible = false; - let mut explorer = PathExplorer::with_config(&ctx, config); - let mut worklist = StateWorklist::new(ExploreStrategy::Dfs); + let mut state = SymState::new(&ctx, 0x6000_0000); + let _ = state.define_runtime_region("jit_blob", 0x6000_0000, 0x100, true); - let mut stronger = SymState::new(&ctx, 0x1000); - stronger.make_symbolic("sym_input", 64); - let input = stronger.get_register("sym_input"); - stronger.add_constraint(input.to_bv(&ctx).bvult(z3::ast::BV::from_u64(10, 64))); - stronger.add_constraint(input.to_bv(&ctx).bvult(z3::ast::BV::from_u64(5, 64))); - worklist.push(stronger); + assert!( + explorer.target_enqueue_allowed(&guidance, &state), + "runtime-materialized states should bypass root-CFG pruning" + ); + } - let mut weaker = SymState::new(&ctx, 0x1000); - weaker.make_symbolic("sym_input", 64); - let input = weaker.get_register("sym_input"); - weaker.add_constraint(input.to_bv(&ctx).bvult(z3::ast::BV::from_u64(10, 64))); - let kept = explorer.prune_subsumed_same_pc_state(&mut worklist, weaker); + #[test] + fn test_pending_exception_resume_pc_uses_block_fallthrough() { + let ctx = Context::thread_local(); + let arch = make_x86_64_arch(); + let blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Call { + target: make_const(0x4000, 8), + }], + }, + R2ILBlock { + addr: 0x1004, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, Some(&arch)).expect("symbolic function"); + let mut state = SymState::new(&ctx, 0x2000); + let (_, context_addr) = state.allocate_heap_region("context", 0x400); + state.set_pending_exception(crate::state::PendingExceptionContinuation { + handler_addr: 0x2000, + exception_code: 0x8000_0004, + exception_pointers_addr: 0x7000_0000, + exception_record_addr: 0x7000_0100, + context_addr, + }); - assert!(kept.is_some(), "weaker same-pc state should survive"); - assert_eq!(explorer.stats.subsumption_checks, 1); - assert_eq!(explorer.stats.subsumption_hits, 1); - assert_eq!(explorer.stats.states_subsumed, 1); - assert_eq!(worklist.live_states, 0); + let explorer = PathExplorer::new(&ctx); + explorer.patch_pending_exception_resume_pc(&func, &mut state, 0x1000); + + assert_eq!( + state + .mem_read( + &SymValue::concrete(context_addr.saturating_add(0xF8), 64), + 8 + ) + .as_concrete(), + Some(0x1004) + ); } #[test] - fn test_same_pc_subsumption_tie_break_prefers_shallower_state() { + fn test_exception_handler_target_guidance_uses_raise_site_bridge() { let ctx = Context::thread_local(); - let config = ExploreConfig { - subsumption_states: true, - ..ExploreConfig::default() - }; - let mut explorer = PathExplorer::with_config(&ctx, config); - let mut worklist = StateWorklist::new(ExploreStrategy::Random); + let mut arch = make_x86_64_arch(); + arch.add_register(RegisterDef::new("RCX", 0x80, 8)); + arch.add_register(RegisterDef::new("RDX", 0x88, 8)); + arch.add_register(RegisterDef::new("R8", 0x90, 8)); + arch.add_register(RegisterDef::new("R9", 0x98, 8)); - let mut existing = SymState::new(&ctx, 0x1000); - existing.make_symbolic("sym_input", 64); - existing.step(); - worklist.push(existing); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(0x80, 8), + src: make_const(1, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x88, 8), + src: make_const(0x2000, 8), + }, + R2ILOp::Call { + target: make_const(0x5000, 8), + }, + R2ILOp::Branch { + target: make_const(0x1010, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1010, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(0x80, 8), + src: make_const(0x8000_0004, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x88, 8), + src: make_const(0, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x90, 8), + src: make_const(0, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x98, 8), + src: make_const(0, 8), + }, + R2ILOp::Call { + target: make_const(0x5008, 8), + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }, + ]; + let handler_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }]; + let root = SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic"); + let handler = + SsaArtifact::for_symbolic(&handler_blocks, Some(&arch)).expect("handler symbolic"); + let scope = crate::PreparedFunctionScope::new( + 0x1000, + vec![ + crate::ScopedPreparedFunction { + id: InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + crate::ScopedPreparedFunction { + id: InterprocFunctionId(0x2000), + name: Some("handler".to_string()), + prepared: handler, + }, + ], + ) + .expect("scope"); - let mut new_state = SymState::new(&ctx, 0x1000); - new_state.make_symbolic("sym_input", 64); - let kept = explorer.prune_subsumed_same_pc_state(&mut worklist, new_state); + let mut explorer = PathExplorer::new(&ctx); + explorer.register_tagged_call_hook( + 0x5000, + crate::executor::CallHookTag::WindowsAddVectoredExceptionHandler, + |_| CallHookResult::Fallthrough, + ); + explorer.register_tagged_call_hook( + 0x5008, + crate::executor::CallHookTag::WindowsRaiseException, + |_| CallHookResult::Fallthrough, + ); - assert!( - kept.is_some(), - "shallower state should replace deeper equivalent state" + assert_eq!( + explorer.exception_bridge_guidance_target(&root, Some(&scope), 0x2000), + Some(0x1010) ); - assert_eq!(worklist.live_states, 0); - assert_eq!(explorer.stats.subsumption_hits, 1); - assert_eq!(explorer.stats.states_subsumed, 1); } #[test] - fn test_path_result_methods() { + fn test_runtime_target_guidance_uses_raise_site_bridge() { let ctx = Context::thread_local(); + let mut arch = make_x86_64_arch(); + arch.add_register(RegisterDef::new("RCX", 0x80, 8)); + arch.add_register(RegisterDef::new("RDX", 0x88, 8)); + arch.add_register(RegisterDef::new("R8", 0x90, 8)); + arch.add_register(RegisterDef::new("R9", 0x98, 8)); - let mut state = SymState::new(&ctx, 0x1000); - state.set_register("rax", SymValue::concrete(42, 64)); - state.make_symbolic("rbx", 64); - state.set_register("aaa", SymValue::concrete(7, 64)); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(0x80, 8), + src: make_const(1, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x88, 8), + src: make_const(0x2000, 8), + }, + R2ILOp::Call { + target: make_const(0x5000, 8), + }, + R2ILOp::Branch { + target: make_const(0x1010, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1010, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(0x80, 8), + src: make_const(0x8000_0004, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x88, 8), + src: make_const(0, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x90, 8), + src: make_const(0, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x98, 8), + src: make_const(0, 8), + }, + R2ILOp::Call { + target: make_const(0x5008, 8), + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }, + ]; + let handler_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }]; + let root = SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic"); + let handler = + SsaArtifact::for_symbolic(&handler_blocks, Some(&arch)).expect("handler symbolic"); + let scope = crate::PreparedFunctionScope::new( + 0x1000, + vec![ + crate::ScopedPreparedFunction { + id: InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + crate::ScopedPreparedFunction { + id: InterprocFunctionId(0x2000), + name: Some("handler".to_string()), + prepared: handler, + }, + ], + ) + .expect("scope"); - let result = PathResult::new(state, true); + let mut explorer = PathExplorer::new(&ctx); + explorer.register_tagged_call_hook( + 0x5000, + crate::executor::CallHookTag::WindowsAddVectoredExceptionHandler, + |_| CallHookResult::Fallthrough, + ); + explorer.register_tagged_call_hook( + 0x5008, + crate::executor::CallHookTag::WindowsRaiseException, + |_| CallHookResult::Fallthrough, + ); - assert_eq!(result.final_pc(), 0x1000); - assert_eq!(result.num_constraints(), 0); assert_eq!( - result.register_names(), - vec!["aaa".to_string(), "rax".to_string(), "rbx".to_string()] + explorer.exception_bridge_guidance_target(&root, Some(&scope), 0x7000), + Some(0x1010) ); - assert_eq!(result.get_concrete_register("rax"), Some(42)); - assert!(result.is_register_symbolic("rbx")); } #[test] - fn test_solve_path_with_constraints() { + fn test_cfg_unreachable_local_target_uses_raise_site_bridge() { let ctx = Context::thread_local(); + let mut arch = make_x86_64_arch(); + arch.add_register(RegisterDef::new("RCX", 0x80, 8)); + arch.add_register(RegisterDef::new("RDX", 0x88, 8)); + arch.add_register(RegisterDef::new("R8", 0x90, 8)); + arch.add_register(RegisterDef::new("R9", 0x98, 8)); - let mut state = SymState::new(&ctx, 0x1000); - state.make_symbolic("sym_input", 64); - - // Add constraint: sym_input < 100 - let input = state.get_register("sym_input"); - let hundred = SymValue::concrete(100, 64); - let cmp = input.ult(&ctx, &hundred); - state.add_true_constraint(&cmp); - - let result = PathResult::new(state, true); - let explorer = PathExplorer::new(&ctx); - - let solved = explorer.solve_path(&result); - assert!(solved.is_some()); - - let solved = solved.unwrap(); - assert_eq!(solved.final_pc, 0x1000); - assert_eq!(solved.num_constraints, 1); + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(0x80, 8), + src: make_const(1, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x88, 8), + src: make_const(0x2000, 8), + }, + R2ILOp::Call { + target: make_const(0x5000, 8), + }, + R2ILOp::Branch { + target: make_const(0x1010, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1010, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(0x80, 8), + src: make_const(0x8000_0004, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x88, 8), + src: make_const(0, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x90, 8), + src: make_const(0, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x98, 8), + src: make_const(0, 8), + }, + R2ILOp::Call { + target: make_const(0x5008, 8), + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1020, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + ]; + let handler_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }]; + let root = SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic"); + let handler = + SsaArtifact::for_symbolic(&handler_blocks, Some(&arch)).expect("handler symbolic"); + let scope = crate::PreparedFunctionScope::new( + 0x1000, + vec![ + crate::ScopedPreparedFunction { + id: InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + crate::ScopedPreparedFunction { + id: InterprocFunctionId(0x2000), + name: Some("handler".to_string()), + prepared: handler, + }, + ], + ) + .expect("scope"); - // The input should be less than 100 - if let Some(&value) = solved.inputs.get("sym_input") { - assert!(value < 100, "Input should be < 100, got {}", value); - } - } + let mut explorer = PathExplorer::new(&ctx); + explorer.register_tagged_call_hook( + 0x5000, + crate::executor::CallHookTag::WindowsAddVectoredExceptionHandler, + |_| CallHookResult::Fallthrough, + ); + explorer.register_tagged_call_hook( + 0x5008, + crate::executor::CallHookTag::WindowsRaiseException, + |_| CallHookResult::Fallthrough, + ); - #[test] - fn test_solved_path_default() { - let solved = SolvedPath::default(); - assert!(solved.inputs.is_empty()); - assert!(solved.input_buffers.is_empty()); - assert!(solved.registers.is_empty()); - assert!(solved.memory.is_empty()); - assert_eq!(solved.final_pc, 0); - assert_eq!(solved.num_constraints, 0); + assert_eq!( + explorer.exception_bridge_guidance_target(&root, Some(&scope), 0x1020), + Some(0x1010) + ); } #[test] - fn test_solve_path_emits_stable_map_order() { + fn test_import_mediated_exception_calls_use_raise_site_bridge() { let ctx = Context::thread_local(); + let mut arch = make_x86_64_arch(); + arch.add_register(RegisterDef::new("RCX", 0x80, 8)); + arch.add_register(RegisterDef::new("RDX", 0x88, 8)); + arch.add_register(RegisterDef::new("R8", 0x90, 8)); + arch.add_register(RegisterDef::new("R9", 0x98, 8)); + + let addveh_target = Varnode { + space: SpaceId::Unique, + offset: 0x500, + size: 8, + meta: None, + }; + let raise_target = Varnode { + space: SpaceId::Unique, + offset: 0x508, + size: 8, + meta: None, + }; + let root_blocks = vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(0x80, 8), + src: make_const(1, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x88, 8), + src: make_const(0x2000, 8), + }, + R2ILOp::Copy { + dst: addveh_target.clone(), + src: make_ram(0x5000, 8), + }, + R2ILOp::CallInd { + target: addveh_target, + }, + R2ILOp::Branch { + target: make_const(0x1010, 8), + }, + ], + }, + R2ILBlock { + addr: 0x1010, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(0x80, 8), + src: make_const(0x8000_0004, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x88, 8), + src: make_const(0, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x90, 8), + src: make_const(0, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x98, 8), + src: make_const(0, 8), + }, + R2ILOp::Copy { + dst: raise_target.clone(), + src: make_ram(0x5008, 8), + }, + R2ILOp::CallInd { + target: raise_target, + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], + }, + ]; + let handler_blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }]; + let root = SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic"); + let handler = + SsaArtifact::for_symbolic(&handler_blocks, Some(&arch)).expect("handler symbolic"); + let scope = crate::PreparedFunctionScope::new( + 0x1000, + vec![ + crate::ScopedPreparedFunction { + id: InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + crate::ScopedPreparedFunction { + id: InterprocFunctionId(0x2000), + name: Some("handler".to_string()), + prepared: handler, + }, + ], + ) + .expect("scope"); - let mut state = SymState::new(&ctx, 0x1000); - state.new_symbolic_input("z_input", 64); - state.new_symbolic_input("a_input", 64); - state.set_register("z_reg", SymValue::concrete(3, 64)); - state.set_register("a_reg", SymValue::concrete(1, 64)); - state.set_register("m_reg", SymValue::concrete(9, 64)); - - let result = PathResult::new(state, true); - let explorer = PathExplorer::new(&ctx); - let solved = explorer.solve_path(&result).expect("path should solve"); - - assert_eq!( - solved.inputs.keys().cloned().collect::>(), - vec!["a_input".to_string(), "z_input".to_string()] + let mut explorer = PathExplorer::new(&ctx); + explorer.register_tagged_call_hook( + 0x5000, + crate::executor::CallHookTag::WindowsAddVectoredExceptionHandler, + |_| CallHookResult::Fallthrough, ); + explorer.register_tagged_call_hook( + 0x5008, + crate::executor::CallHookTag::WindowsRaiseException, + |_| CallHookResult::Fallthrough, + ); + assert_eq!( - solved.registers.keys().cloned().collect::>(), - vec![ - "a_reg".to_string(), - "m_reg".to_string(), - "z_reg".to_string() - ] + explorer.exception_bridge_guidance_target(&root, Some(&scope), 0x2000), + Some(0x1010) ); } #[test] - fn test_target_guided_prunes_exact_summary_contradiction() { + fn test_import_mediated_bridge_works_with_installed_runtime_hooks() { let ctx = Context::thread_local(); - let arch = make_x86_64_arch(); - let blocks = vec![ + let mut arch = make_x86_64_arch(); + arch.add_register(RegisterDef::new("RCX", 0x80, 8)); + arch.add_register(RegisterDef::new("RDX", 0x88, 8)); + arch.add_register(RegisterDef::new("R8", 0x90, 8)); + arch.add_register(RegisterDef::new("R9", 0x98, 8)); + + let addveh_target = Varnode { + space: SpaceId::Unique, + offset: 0x520, + size: 8, + meta: None, + }; + let raise_target = Varnode { + space: SpaceId::Unique, + offset: 0x528, + size: 8, + meta: None, + }; + let root_blocks = vec![ R2ILBlock { addr: 0x1000, size: 4, - ops: vec![R2ILOp::Call { - target: make_const(0x2000, 8), - }], - switch_info: None, - op_metadata: Default::default(), - }, - R2ILBlock { - addr: 0x1004, - size: 4, - ops: vec![R2ILOp::Branch { - target: make_const(0x1010, 8), - }], switch_info: None, op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(0x80, 8), + src: make_const(1, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x88, 8), + src: make_const(0x2000, 8), + }, + R2ILOp::Copy { + dst: addveh_target.clone(), + src: make_ram(0x1400a6010, 8), + }, + R2ILOp::CallInd { + target: addveh_target, + }, + R2ILOp::Branch { + target: make_const(0x1010, 8), + }, + ], }, R2ILBlock { addr: 0x1010, size: 4, - ops: vec![R2ILOp::Copy { - dst: make_reg(TMP0, 1), - src: make_const(1, 1), - }], switch_info: None, op_metadata: Default::default(), + ops: vec![ + R2ILOp::Copy { + dst: make_reg(0x80, 8), + src: make_const(0x8000_0004, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x88, 8), + src: make_const(0, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x90, 8), + src: make_const(0, 8), + }, + R2ILOp::Copy { + dst: make_reg(0x98, 8), + src: make_const(0, 8), + }, + R2ILOp::Copy { + dst: raise_target.clone(), + src: make_ram(0x1400a6000, 8), + }, + R2ILOp::CallInd { + target: raise_target, + }, + R2ILOp::Return { + target: make_const(0, 8), + }, + ], }, ]; - let func = SsaArtifact::for_symbolic(&blocks, Some(&arch)).expect("symbolic function"); - - let mut explorer = PathExplorer::new(&ctx); - explorer.set_target_guided_queries(true); - - let arg0 = SymValue::new_symbolic(&ctx, "summary_arg0", 64); - let guard = arg0.to_bv(&ctx).eq(BV::from_u64(1, 64)); - let summary = Rc::new(DerivedFunctionSummary { - id: InterprocFunctionId(0x2000), - name: Some("contradict_helper".to_string()), - arg_count_hint: 1, - arg_symbols: vec![(0, arg0)], - memory_inputs: Vec::new(), - cases: vec![DerivedSummaryCase { - guard, - return_value: None, - memory_writes: Vec::new(), + let root = SsaArtifact::for_symbolic(&root_blocks, Some(&arch)).expect("root symbolic"); + let scope = crate::PreparedFunctionScope::new( + 0x1000, + vec![crate::ScopedPreparedFunction { + id: InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), }], - completion: DerivedSummaryCompletion::Exact, - }); - explorer.register_derived_call_hook(0x2000, summary, CallConv::x86_64_sysv(), |_state| { - CallHookResult::Fallthrough - }); + ) + .expect("scope"); - let mut state = SymState::new(&ctx, 0x1000); - state.set_register("RDI_0", SymValue::concrete(2, 64)); + let mut explorer = PathExplorer::new(&ctx); + let symbol_map = HashMap::from([ + ( + 0x1400a6010, + "sym.imp.KERNEL32.dll_AddVectoredExceptionHandler".to_string(), + ), + ( + 0x1400a6000, + "sym.imp.KERNEL32.dll_RaiseException".to_string(), + ), + ]); + crate::install_runtime_hooks_for_scope(&mut explorer, &scope, Some(&arch), &symbol_map); - let paths = explorer.find_paths_to(&func, state, 0x1010); - assert!(paths.is_empty(), "contradictory exact summary should prune"); - assert_eq!(explorer.stats().target_pruned_summary_contradiction, 1); + assert_eq!( + explorer.exception_bridge_guidance_target(&root, Some(&scope), 0x7000), + Some(0x1010) + ); } } diff --git a/crates/r2sym/src/query.rs b/crates/r2sym/src/query.rs index 86824d8..d1fa23e 100644 --- a/crates/r2sym/src/query.rs +++ b/crates/r2sym/src/query.rs @@ -5,57 +5,309 @@ //! analysis layers without exposing command-shaped policy. use std::collections::{BTreeMap, BTreeSet, VecDeque}; +use std::fs::OpenOptions; +use std::io::Write; +use std::time::Duration; +use r2il::ArchSpec; use z3::Context; use z3::ast::{Ast, BV, Bool}; -use r2ssa::SsaArtifact; +use r2ssa::{ + AssumptionSubject, AssumptionUsageReport, AssumptionValue, CompareKind, + PreparedAssumptionBindingKind, SSAOp, SSAVar, SsaArtifact, +}; +#[cfg(test)] +use crate::BackwardConditionPrecision; use crate::SymState; use crate::backward::{ - BackwardConditionPrecision, BackwardConditionSummary, CompiledBackwardCondition, + BackwardConditionSummary, CompiledBackwardCondition, compile_branch_precondition_with_summaries, compile_target_precondition_with_summaries, compile_value_postcondition_with_summaries, }; +use crate::constraints::build_final_constraint_graph_for_path; use crate::path::{ExploreConfig, ExploreStats, PathExplorer, PathResult, SolvedPath}; +use crate::runtime::install_runtime_hooks_for_scope; use crate::semantics::{ - SemanticArtifact, SemanticRegion, VmStepSummary, build_vm_step_summary, - classify_interpreter_like, + SemanticArtifact, TargetQueryExecutionRoute, TargetQueryRouteInput, VmStepSummary, + build_vm_step_summary, classify_interpreter_like, }; -use crate::sim::SummaryProfile; +use crate::sim::{PreparedFunctionScope, SummaryProfile, SummaryRegistry}; use crate::solver::{SatResult, SolverStats}; use crate::state::ExitStatus; +use crate::tactics::SolveTacticConfig; +use crate::verification::{ + EvidenceSummary, LiftedReplayBackend, SolveStatus, SolveVerification, SolveVerificationRequest, + SolveWitness, VerificationRequirement, evidence_summary_for_route_and_stats, + solution_extraction_allowed, verification_requirement_for_route_and_stats, verify_solve_result, +}; const MAX_VM_COMPILED_STEPS: usize = 8; -#[derive(Debug, Clone, Copy)] -struct SemanticTargetConditionSource<'a> { - region: &'a SemanticRegion, - branch_truth: bool, - summary: &'a BackwardConditionSummary, +fn debug_query_route_enabled() -> bool { + std::env::var_os("R2SLEIGH_DEBUG_QUERY_ROUTE").is_some() } -fn selected_region_for_target( - artifact: &SemanticArtifact, - target_addr: u64, - hard_proof_only: bool, -) -> Option<&SemanticRegion> { - artifact.authoritative_region_for_target(target_addr, hard_proof_only) +fn debug_query_route_log(message: &str) { + if !debug_query_route_enabled() { + return; + } + let path = std::env::var("R2SLEIGH_DEBUG_QUERY_ROUTE_LOG") + .unwrap_or_else(|_| "/tmp/r2sleigh_query_route.log".to_string()); + if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) { + let _ = writeln!(file, "{message}"); + } } -fn target_condition_source<'a>( - artifact: &'a SemanticArtifact, - target_addr: u64, - hard_proof_only: bool, -) -> Option> { - let native = artifact.native_body()?; - let source = artifact.target_condition_source(target_addr, hard_proof_only)?; - let region = native.region_for_anchor(source.block_addr)?; - Some(SemanticTargetConditionSource { - region, - branch_truth: source.branch_truth, - summary: source.summary, - }) +fn debug_query_phase_enabled() -> bool { + std::env::var_os("R2SLEIGH_DEBUG_QUERY_PHASES").is_some() +} + +fn debug_query_phase_log(message: &str) { + if !debug_query_phase_enabled() { + return; + } + let path = std::env::var("R2SLEIGH_DEBUG_QUERY_PHASES_LOG") + .unwrap_or_else(|_| "/tmp/r2sleigh_query_phases.log".to_string()); + if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) { + let _ = writeln!(file, "{message}"); + } +} + +#[derive(Debug, Clone, Default)] +struct QueryAssumptionOutcome { + usage: AssumptionUsageReport, + conditioned: bool, + conflicted: bool, +} + +fn apply_predicate_assumption_to_state<'ctx>( + explorer: &PathExplorer<'ctx>, + func: &SsaArtifact, + state: &mut SymState<'ctx>, + block_addr: u64, + truth: bool, +) -> Result { + let derived_summaries = explorer.derived_call_summary_views(); + let Some(compiled) = compile_branch_precondition_with_summaries( + func, + state, + block_addr, + truth, + &derived_summaries, + ) else { + return Ok(false); + }; + match explorer + .solver() + .sat_with_constraint(state, &compiled.predicate) + { + SatResult::Sat + if compiled.summary.evidence().allows_hard_proof() + || compiled.summary.evidence().allows_narrowing() => + { + state.add_constraint(compiled.predicate); + Ok(true) + } + SatResult::Sat | SatResult::Unknown => Ok(false), + SatResult::Unsat => Err(format!( + "branch assumption for block 0x{block_addr:x} contradicts symbolic state" + )), + } +} + +fn assumption_usage_for_query<'ctx>( + explorer: &PathExplorer<'ctx>, + func: &SsaArtifact, + state: &mut SymState<'ctx>, +) -> QueryAssumptionOutcome { + let mut usage = AssumptionUsageReport::default(); + let mut conditioned = false; + let mut conflicted = false; + + for binding in &func.facts().applied_assumption_bindings { + match (&binding.binding, &binding.assumption.value) { + ( + PreparedAssumptionBindingKind::Predicate { + block_addr, truth, .. + }, + AssumptionValue::Branch { .. }, + ) => match apply_predicate_assumption_to_state( + explorer, + func, + state, + *block_addr, + *truth, + ) { + Ok(true) => { + usage.mark_applied(&binding.assumption); + conditioned = true; + } + Ok(false) => usage.mark_ignored(&binding.assumption), + Err(reason) => { + usage.mark_conflict(&binding.assumption, reason); + conflicted = true; + } + }, + ( + PreparedAssumptionBindingKind::Register { + state_name, bits, .. + }, + value, + ) => { + let Some(current) = state.registers().get(state_name).cloned() else { + usage.mark_ignored(&binding.assumption); + continue; + }; + match apply_assumption_value_to_sym( + state, + &binding.assumption, + ¤t, + *bits, + value, + ) { + Ok(true) => { + usage.mark_applied(&binding.assumption); + conditioned = true; + } + Ok(false) => usage.mark_ignored(&binding.assumption), + Err(reason) => { + usage.mark_conflict(&binding.assumption, reason); + conflicted = true; + } + } + } + _ => {} + } + } + + for assumption in func.facts().assumptions.iter() { + if matches!(assumption.subject, AssumptionSubject::MemoryWindow { .. }) { + match apply_memory_window_assumption(state, assumption) { + Ok(true) => { + usage.mark_applied(assumption); + conditioned = true; + } + Ok(false) => usage.mark_ignored(assumption), + Err(reason) => { + usage.mark_conflict(assumption, reason); + conflicted = true; + } + } + } + } + + for assumption in func.facts().assumptions.iter() { + usage.mark_ignored(assumption); + } + + QueryAssumptionOutcome { + usage, + conditioned, + conflicted, + } +} + +fn apply_assumption_value_to_sym<'ctx>( + state: &mut SymState<'ctx>, + assumption: &r2ssa::AnalysisAssumption, + value: &crate::value::SymValue<'ctx>, + bits: u32, + assumption_value: &AssumptionValue, +) -> Result { + match assumption_value { + AssumptionValue::Constant { value: rhs } => { + if let Some(existing) = value.as_concrete() { + if existing != *rhs { + return Err(format!( + "assumption constant 0x{rhs:x} contradicts concrete value 0x{existing:x}" + )); + } + return Ok(true); + } + state.constrain_eq(value, *rhs); + Ok(true) + } + AssumptionValue::Range { min, max } => { + if min > max { + return Err("invalid range assumption".to_string()); + } + if let Some(existing) = value.as_concrete() { + if existing < *min || existing > *max { + return Err(format!( + "assumption range [{min:#x}, {max:#x}] excludes concrete value {existing:#x}" + )); + } + return Ok(true); + } + state.constrain_range(value, *min, *max); + Ok(true) + } + AssumptionValue::FiniteSet { values } => { + if values.is_empty() { + return Err("empty finite-set assumption".to_string()); + } + if let Some(existing) = value.as_concrete() { + if !values.contains(&existing) { + return Err(format!( + "finite-set assumption excludes concrete value {existing:#x}" + )); + } + return Ok(true); + } + let bv = value.to_bv(state.context()); + let ors = values + .iter() + .map(|item| bv.eq(BV::from_u64(*item, bits.max(1)))) + .collect::>(); + let refs = ors.iter().collect::>(); + state.add_constraint(Bool::or(&refs)); + Ok(true) + } + AssumptionValue::EnumDomain { values, .. } => { + let values = values.iter().map(|value| *value as u64).collect::>(); + apply_assumption_value_to_sym( + state, + assumption, + value, + bits, + &AssumptionValue::FiniteSet { values }, + ) + } + AssumptionValue::TypeHint { .. } => Ok(false), + AssumptionValue::Branch { .. } => match &assumption.subject { + AssumptionSubject::Predicate { .. } => Ok(true), + _ => Ok(false), + }, + } +} + +fn apply_memory_window_assumption<'ctx>( + state: &mut SymState<'ctx>, + assumption: &r2ssa::AnalysisAssumption, +) -> Result { + let AssumptionSubject::MemoryWindow { addr, size } = &assumption.subject else { + return Ok(false); + }; + let Some(region) = state + .symbolic_memory() + .iter() + .find(|region| region.addr == *addr && region.size == *size) + .cloned() + else { + return Ok(false); + }; + if *size > 8 { + return Ok(false); + } + apply_assumption_value_to_sym( + state, + assumption, + ®ion.value, + region.value.bits(), + &assumption.value, + ) } /// Query execution strategy. @@ -76,6 +328,8 @@ pub struct SymQueryConfig { pub mode: QueryMode, /// Summary profile to use when installing function summaries. pub summary_profile: SummaryProfile, + /// Candidate-generation tactics used after exact semantic summaries. + pub solve_tactics: SolveTacticConfig, } impl Default for SymQueryConfig { @@ -88,6 +342,7 @@ impl Default for SymQueryConfig { explore, mode: QueryMode::ForwardOnly, summary_profile: SummaryProfile::Default, + solve_tactics: SolveTacticConfig::default(), } } } @@ -97,10 +352,270 @@ impl SymQueryConfig { pub fn make_explorer<'ctx>(&self, ctx: &'ctx Context) -> PathExplorer<'ctx> { let mut explorer = PathExplorer::with_config(ctx, self.explore.clone()); explorer.set_target_guided_queries(matches!(self.mode, QueryMode::TargetGuided)); + explorer.set_solve_tactic_config(self.solve_tactics.clone()); explorer } } +/// Recommend a query-depth budget based on the SSA op count and symbolic input surface. +/// +/// Depth is currently counted per executed SSA op, not per basic block. A flat budget +/// can therefore under-approximate string/hash loops and other byte-at-a-time workers. +/// This helper keeps the budget tied to concrete function cost and the current symbolic +/// input surface instead of hard-coding sample-specific constants downstream. +pub fn recommended_query_max_depth(func: &SsaArtifact, initial_state: &SymState<'_>) -> usize { + let cfg_risk = func.function().cfg_risk_summary(); + let total_ops = func + .blocks() + .map(|block| block.ops.len()) + .sum::() + .max(1); + let max_block_ops = func + .blocks() + .map(|block| block.ops.len()) + .max() + .unwrap_or(1) + .max(1); + let symbolic_surface = initial_state.symbolic_inputs().len().clamp(1, 16); + let bounded_copy_loop_work = estimated_bounded_copy_loop_work(func); + let loop_multiplier = if func.function().cfg_risk_summary().back_edge_count == 0 { + symbolic_surface.saturating_add(4) + } else { + symbolic_surface.saturating_add(2) + }; + let base_budget = total_ops + .saturating_mul(loop_multiplier) + .max(max_block_ops.saturating_mul(loop_multiplier.saturating_mul(2))); + let budget = base_budget.max( + total_ops + .saturating_add(bounded_copy_loop_work) + .saturating_add(max_block_ops.saturating_mul(loop_multiplier)), + ); + if cfg_risk.block_count >= 64 || cfg_risk.back_edge_count > 0 { + budget.max(4096) + } else { + budget + } +} + +/// Recommend a query timeout budget for the current function shape. +/// +/// Large bounded copy loops often materialize runtime code byte-by-byte before any +/// symbolic branching happens. Those routes need more wall-clock time even when the +/// actual search frontier stays small. +pub fn recommended_query_timeout(func: &SsaArtifact) -> Duration { + let cfg_risk = func.function().cfg_risk_summary(); + let bounded_copy_loop_work = estimated_bounded_copy_loop_work(func); + if bounded_copy_loop_work >= 32 * 1024 { + Duration::from_secs(180) + } else if bounded_copy_loop_work >= 8 * 1024 { + Duration::from_secs(120) + } else if cfg_risk.block_count >= 64 || cfg_risk.back_edge_count > 0 { + Duration::from_secs(60) + } else { + Duration::from_secs(20) + } +} + +const CONTINUATION_QUERY_MAX_DEPTH_CAP: usize = 8_192; +const CONTINUATION_QUERY_MAX_STATES_CAP: usize = 256; +const CONTINUATION_QUERY_TIMEOUT_CAP: Duration = Duration::from_secs(10); +const CONTINUATION_SEEDED_STATE_CAP: usize = 1; + +#[derive(Debug, Clone)] +pub struct QueryExecutionPolicy { + pub route: crate::TargetQueryRoutePlan, + pub evidence_summary: EvidenceSummary, + pub verification_requirement: VerificationRequirement, + pub max_states: usize, + pub max_depth: usize, + pub timeout: Option, + pub install_scope_summaries: bool, + pub install_runtime_hooks: bool, +} + +impl QueryExecutionPolicy { + pub fn for_route( + config: &SymQueryConfig, + func: &SsaArtifact, + initial_state: &SymState<'_>, + route: crate::TargetQueryRoutePlan, + ) -> Self { + let max_states = + recommended_query_max_states_for_route(config.explore.max_states, Some(&route)); + let max_depth = config + .explore + .max_depth + .max(recommended_query_max_depth_for_route( + func, + initial_state, + Some(&route), + )); + let current_timeout = config + .explore + .timeout + .unwrap_or_else(|| Duration::from_secs(20)); + let recommended_timeout = recommended_query_timeout_for_route(func, Some(&route)); + let timeout = if route_is_continuation_seeded(Some(&route)) { + current_timeout.min(recommended_timeout) + } else { + current_timeout.max(recommended_timeout) + }; + let route_only_stats = ExploreStats::default(); + let evidence_summary = + evidence_summary_for_route_and_stats(&route, &route_only_stats, None); + let verification_requirement = + verification_requirement_for_route_and_stats(&route, &route_only_stats, None); + Self { + install_scope_summaries: !route_skips_eager_scope_summaries(&route), + install_runtime_hooks: true, + evidence_summary, + verification_requirement, + route, + max_states, + max_depth, + timeout: Some(timeout), + } + } +} + +pub fn apply_query_execution_policy(config: &mut SymQueryConfig, policy: &QueryExecutionPolicy) { + config.explore.max_states = policy.max_states; + config.explore.max_depth = policy.max_depth; + config.explore.timeout = policy.timeout; +} + +pub fn route_skips_eager_scope_summaries(route: &crate::TargetQueryRoutePlan) -> bool { + matches!( + &route.target_plan, + crate::TargetQueryPlan::Residual { reasons } + if reasons.iter().any(|reason| reason == "LargeCfg") + ) +} + +pub fn install_symbolic_hooks_for_query_policy<'ctx>( + explorer: &mut PathExplorer<'ctx>, + z3_ctx: &'ctx Context, + scope: &PreparedFunctionScope, + arch: Option<&ArchSpec>, + symbol_map: &std::collections::HashMap, + summary_profile: SummaryProfile, + policy: &QueryExecutionPolicy, +) { + if policy.install_scope_summaries + && let Some(arch) = arch + && let Some(registry) = + SummaryRegistry::with_profile_for_arch_and_symbols(arch, symbol_map, summary_profile) + { + let _ = registry.install_scope_summaries_for_explorer( + explorer, + z3_ctx, + scope, + Some(arch), + symbol_map, + ); + } + if policy.install_runtime_hooks { + install_runtime_hooks_for_scope(explorer, scope, arch, symbol_map); + } +} + +fn route_is_continuation_seeded(route: Option<&crate::TargetQueryRoutePlan>) -> bool { + route.is_some_and(|route| { + matches!( + route.execution, + TargetQueryExecutionRoute::ContinuationSeeded { .. } + ) + }) +} + +pub fn recommended_query_max_depth_for_route( + func: &SsaArtifact, + initial_state: &SymState<'_>, + route: Option<&crate::TargetQueryRoutePlan>, +) -> usize { + let budget = recommended_query_max_depth(func, initial_state); + if route_is_continuation_seeded(route) { + budget.min(CONTINUATION_QUERY_MAX_DEPTH_CAP) + } else { + budget + } +} + +pub fn recommended_query_max_states_for_route( + current_max_states: usize, + route: Option<&crate::TargetQueryRoutePlan>, +) -> usize { + if route_is_continuation_seeded(route) { + current_max_states.min(CONTINUATION_QUERY_MAX_STATES_CAP) + } else { + current_max_states + } +} + +pub fn recommended_query_timeout_for_route( + func: &SsaArtifact, + route: Option<&crate::TargetQueryRoutePlan>, +) -> Duration { + let timeout = recommended_query_timeout(func); + if route_is_continuation_seeded(route) { + timeout.min(CONTINUATION_QUERY_TIMEOUT_CAP) + } else { + timeout + } +} + +fn estimated_bounded_copy_loop_work(func: &SsaArtifact) -> usize { + func.predicates() + .predicates + .values() + .filter_map(|predicate| estimate_bounded_copy_loop_work(func, predicate)) + .sum() +} + +fn estimate_bounded_copy_loop_work( + func: &SsaArtifact, + predicate: &r2ssa::PredicateFact, +) -> Option { + let self_loop = predicate.true_target == predicate.block_addr + || predicate.false_target == predicate.block_addr; + if !self_loop { + return None; + } + + let block = func.function().get_block(predicate.block_addr)?; + let has_load = block.ops.iter().any(|op| matches!(op, SSAOp::Load { .. })); + let has_store = block.ops.iter().any(|op| matches!(op, SSAOp::Store { .. })); + if !has_load || !has_store { + return None; + } + + let comparison = predicate.comparison.as_ref()?; + let bound = [comparison.lhs, comparison.rhs] + .into_iter() + .filter_map(|value_id| func.value_var(value_id)) + .filter_map(ssa_const_value) + .max()?; + + let trip_count = match comparison.kind { + CompareKind::Less => bound, + CompareKind::LessEqual => bound.saturating_add(1), + _ => return None, + }; + + if trip_count == 0 { + return None; + } + + let capped_trip_count = trip_count.min(1 << 20) as usize; + Some(block.ops.len().max(1).saturating_mul(capped_trip_count)) +} + +fn ssa_const_value(var: &SSAVar) -> Option { + let value = var.name.strip_prefix("const:")?; + u64::from_str_radix(value, 16).ok() +} + /// Completion state for queries that may stop on budgets. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum QueryCompletion { @@ -119,15 +634,6 @@ pub enum ReachabilityStatus { BudgetExhausted, } -/// Solve result status. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum SolveStatus { - Solved, - Unsat, - Unknown, - BudgetExhausted, -} - /// Compact summary of a path condition reaching a given program counter. #[derive(Debug, Clone, PartialEq, Eq)] pub struct PathConditionTerm { @@ -159,8 +665,12 @@ pub struct PathConditionSummary { pub struct ReachabilityResult<'ctx> { pub status: ReachabilityStatus, pub target_addr: u64, + pub selected_route: crate::TargetQueryRoutePlan, pub compiled_precondition: Option, pub paths: Vec>, + pub assumption_usage: AssumptionUsageReport, + pub assumption_conditioned: bool, + pub summary_conditioned: bool, pub stats: ExploreStats, pub solver_stats: SolverStats, } @@ -170,9 +680,13 @@ pub struct ReachabilityResult<'ctx> { pub struct PathConditionResult<'ctx> { pub completion: QueryCompletion, pub target_pc: u64, + pub selected_route: crate::TargetQueryRoutePlan, pub compiled_precondition: Option, pub conditions: Vec, pub matching_paths: Vec>, + pub assumption_usage: AssumptionUsageReport, + pub assumption_conditioned: bool, + pub summary_conditioned: bool, pub stats: ExploreStats, pub solver_stats: SolverStats, } @@ -182,10 +696,16 @@ pub struct PathConditionResult<'ctx> { pub struct SolveResult<'ctx> { pub status: SolveStatus, pub target_addr: u64, + pub selected_route: crate::TargetQueryRoutePlan, pub compiled_precondition: Option, pub matched_paths: Vec>, pub selected_path_index: Option, pub solution: Option, + pub assumption_usage: AssumptionUsageReport, + pub assumption_conditioned: bool, + pub summary_conditioned: bool, + pub verification: SolveVerification, + pub witness: SolveWitness, pub stats: ExploreStats, pub solver_stats: SolverStats, } @@ -200,6 +720,14 @@ pub struct SymbolicFunctionSummary<'ctx> { pub solver_stats: SolverStats, } +struct TargetQueryPaths<'ctx> { + selected_route: crate::TargetQueryRoutePlan, + compiled_precondition: Option, + matched_paths: Vec>, + exact_unsat: bool, + summary_conditioned: bool, +} + fn completion_from_stats(stats: &ExploreStats) -> QueryCompletion { if stats.timed_out || stats.max_states_exhausted { QueryCompletion::BudgetExhausted @@ -249,9 +777,11 @@ enum PreconditionApplication<'ctx> { initial_state: Box>, narrowed_state: Option>>, compiled_precondition: Option, + summary_conditioned: bool, }, ExactUnsat { compiled_precondition: BackwardConditionSummary, + summary_conditioned: bool, }, } @@ -268,6 +798,98 @@ fn compiled_mode_from_guidance(mode: crate::QueryGuidanceMode) -> CompiledPrecon } } +fn build_target_query_inputs<'a>( + explorer: &mut PathExplorer<'_>, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + artifact: Option<&'a SemanticArtifact>, + target_addr: u64, + assumption_conflicted: bool, + allow_continuation_bridge: bool, +) -> TargetQueryRouteInput<'a> { + let mut inputs = artifact + .map(|artifact| artifact.target_query_route_input(target_addr, assumption_conflicted)) + .unwrap_or_else(|| TargetQueryRouteInput { + route: crate::TargetQueryRoutePlan::dynamic_fallback(), + condition_source: None, + memory_terms: Vec::new(), + allow_exact_proof: !assumption_conflicted, + }); + if allow_continuation_bridge + && let Some(bridge_target) = + explorer.exception_bridge_target_in_scope(func, scope, target_addr) + && bridge_target != target_addr + && !matches!( + inputs.route.execution, + TargetQueryExecutionRoute::Refuse { .. } + ) + { + inputs.route.execution = TargetQueryExecutionRoute::ContinuationSeeded { + bridge_target, + route: Box::new(inputs.route.execution.clone()), + }; + debug_query_route_log(&format!( + "target=0x{target_addr:x} route=ContinuationSeeded bridge=0x{bridge_target:x}" + )); + } else { + debug_query_route_log(&format!( + "target=0x{target_addr:x} route={:?}", + inputs.route.execution + )); + } + inputs +} + +pub fn selected_target_query_route_in_scope( + explorer: &mut PathExplorer<'_>, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + artifact: Option<&SemanticArtifact>, + target_addr: u64, + assumption_conflicted: bool, +) -> crate::TargetQueryRoutePlan { + build_target_query_inputs( + explorer, + func, + scope, + artifact, + target_addr, + assumption_conflicted, + true, + ) + .route +} + +fn route_uses_summary_guidance( + route: &TargetQueryExecutionRoute, + has_derived_summaries: bool, +) -> bool { + if !has_derived_summaries { + return false; + } + match route { + TargetQueryExecutionRoute::ContinuationSeeded { route, .. } => { + route_uses_summary_guidance(route, has_derived_summaries) + } + TargetQueryExecutionRoute::ArtifactCondition { .. } + | TargetQueryExecutionRoute::DynamicTargetCompile { .. } + | TargetQueryExecutionRoute::VmTargetCompile { .. } => true, + TargetQueryExecutionRoute::ArtifactMemoryOnly + | TargetQueryExecutionRoute::ResidualOnly { .. } + | TargetQueryExecutionRoute::Refuse { .. } => false, + } +} + +fn state_is_continuation_seed<'ctx>(state: &SymState<'ctx>) -> bool { + state.pending_exception().is_some() || state.runtime_region_for_pc(state.pc).is_some() +} + +fn stats_show_summary_guidance(stats: &ExploreStats) -> bool { + stats.target_summary_rank_hits > 0 || stats.target_pruned_summary_contradiction > 0 +} + +#[cfg_attr(test, allow(dead_code))] +#[cfg(test)] fn precision_rank(precision: BackwardConditionPrecision) -> u8 { match precision { BackwardConditionPrecision::Exact => 3, @@ -504,6 +1126,8 @@ fn compile_vm_target_precondition_with_summaries<'ctx>( ) } +#[cfg_attr(test, allow(dead_code))] +#[cfg(test)] fn prefer_compiled_precondition( current: Option<&CompiledBackwardCondition>, candidate: &CompiledBackwardCondition, @@ -634,6 +1258,7 @@ fn apply_compiled_precondition_with_mode<'ctx>( mut initial_state: SymState<'ctx>, compiled: crate::CompiledBackwardCondition, mode: CompiledPreconditionMode, + allow_exact_proof: bool, ) -> PreconditionApplication<'ctx> { let summary = compiled.summary.clone(); let evidence = summary.evidence(); @@ -642,19 +1267,26 @@ fn apply_compiled_precondition_with_mode<'ctx>( .sat_with_constraint(&initial_state, &compiled.predicate) { SatResult::Unsat - if mode == CompiledPreconditionMode::Necessary && evidence.allows_hard_proof() => + if mode == CompiledPreconditionMode::Necessary + && allow_exact_proof + && evidence.allows_hard_proof() => { PreconditionApplication::ExactUnsat { compiled_precondition: summary, + summary_conditioned: false, } } SatResult::Sat => { - if mode == CompiledPreconditionMode::Necessary && evidence.allows_hard_proof() { + if mode == CompiledPreconditionMode::Necessary + && allow_exact_proof + && evidence.allows_hard_proof() + { initial_state.add_constraint(compiled.predicate); PreconditionApplication::Continue { initial_state: Box::new(initial_state), narrowed_state: None, compiled_precondition: Some(summary), + summary_conditioned: false, } } else if evidence.allows_narrowing() { let mut narrowed_state = initial_state.fork(); @@ -663,12 +1295,14 @@ fn apply_compiled_precondition_with_mode<'ctx>( initial_state: Box::new(initial_state), narrowed_state: Some(Box::new(narrowed_state)), compiled_precondition: Some(summary), + summary_conditioned: false, } } else { PreconditionApplication::Continue { initial_state: Box::new(initial_state), narrowed_state: None, compiled_precondition: Some(summary), + summary_conditioned: false, } } } @@ -676,6 +1310,7 @@ fn apply_compiled_precondition_with_mode<'ctx>( initial_state: Box::new(initial_state), narrowed_state: None, compiled_precondition: Some(summary), + summary_conditioned: false, }, } } @@ -698,139 +1333,675 @@ where search(explorer, initial_state) } -fn apply_best_compiled_precondition<'ctx>( +fn find_first_path_with_compiled_narrowing<'ctx, F>( + explorer: &mut PathExplorer<'ctx>, + initial_state: SymState<'ctx>, + narrowed_state: Option>>, + mut search: F, +) -> Option> +where + F: FnMut(&mut PathExplorer<'ctx>, SymState<'ctx>) -> Option>, +{ + if let Some(narrowed_state) = narrowed_state { + let matched = search(explorer, *narrowed_state); + if matched.is_some() || explorer.budget_exhausted() { + return matched; + } + } + search(explorer, initial_state) +} + +fn apply_best_compiled_precondition_for_inputs<'ctx>( explorer: &PathExplorer<'ctx>, func: &SsaArtifact, - artifact: Option<&SemanticArtifact>, initial_state: SymState<'ctx>, target_addr: u64, + query_inputs: &TargetQueryRouteInput<'_>, ) -> PreconditionApplication<'ctx> { let derived_summaries = explorer.derived_call_summary_views(); - let route = artifact - .map(|artifact| artifact.target_query_route_plan(target_addr)) - .unwrap_or_else(crate::TargetQueryRoutePlan::dynamic_fallback); - let branch_mode = route.branch_guidance.map(compiled_mode_from_guidance); - let condition_source = branch_mode.and_then(|_| { - artifact.and_then(|artifact| target_condition_source(artifact, target_addr, false)) - }); - let selected_region = branch_mode.and_then(|_| { - artifact.and_then(|artifact| selected_region_for_target(artifact, target_addr, false)) - }); - let memory_region = if route.allow_memory_term_narrowing { - artifact.and_then(|artifact| artifact.authoritative_memory_region_for_target(target_addr)) - } else { - None - }; - let memory_terms = if route.allow_memory_term_narrowing { - memory_region - .map(|region| region.actionable_memory_terms_for_target(target_addr)) - .unwrap_or_default() - } else { - Vec::new() - }; - if route.allow_memory_term_narrowing - && let Some(narrowed_state) = apply_memory_term_narrowing(&initial_state, &memory_terms) - { - return PreconditionApplication::Continue { + let memory_terms = query_inputs.memory_terms.clone(); + let target_is_local = func.get_block(target_addr).is_some(); + let route_summary_conditioned = + route_uses_summary_guidance(&query_inputs.route.execution, !derived_summaries.is_empty()); + match query_inputs.route.execution { + TargetQueryExecutionRoute::ContinuationSeeded { .. } => PreconditionApplication::Continue { initial_state: Box::new(initial_state), - narrowed_state: Some(narrowed_state), - compiled_precondition: selected_region - .and_then(|region| region.actionable_compiled_condition_for_target(target_addr)) + narrowed_state: None, + compiled_precondition: None, + summary_conditioned: route_summary_conditioned, + }, + TargetQueryExecutionRoute::ArtifactCondition { mode } => { + let compiled = query_inputs.condition_source.and_then(|source| { + compile_branch_precondition_with_summaries( + func, + &initial_state, + source.block_addr, + source.branch_truth, + &derived_summaries, + ) + .map(|compiled| (compiled, mode)) + }); + if compiled.is_none() + && let Some(narrowed_state) = + apply_memory_term_narrowing(&initial_state, &memory_terms) + { + let compiled_precondition = query_inputs + .condition_source + .map(|source| source.summary) + .filter(|summary| !summary.evidence().allows_hard_proof()) + .cloned(); + return PreconditionApplication::Continue { + initial_state: Box::new(initial_state), + narrowed_state: Some(narrowed_state), + compiled_precondition, + summary_conditioned: route_summary_conditioned, + }; + } + let Some((compiled, mode)) = compiled else { + let narrowed_state = apply_memory_term_narrowing(&initial_state, &memory_terms); + return PreconditionApplication::Continue { + initial_state: Box::new(initial_state), + narrowed_state, + compiled_precondition: None, + summary_conditioned: route_summary_conditioned, + }; + }; + match apply_compiled_precondition_with_mode( + explorer, + initial_state, + compiled, + compiled_mode_from_guidance(mode), + query_inputs.allow_exact_proof, + ) { + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition, + .. + } => { + let narrowed_state = narrowed_state.or_else(|| { + apply_memory_term_narrowing(initial_state.as_ref(), &memory_terms) + }); + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition, + summary_conditioned: route_summary_conditioned, + } + } + PreconditionApplication::ExactUnsat { + compiled_precondition, + .. + } => PreconditionApplication::ExactUnsat { + compiled_precondition, + summary_conditioned: route_summary_conditioned, + }, + } + } + TargetQueryExecutionRoute::ArtifactMemoryOnly => { + let narrowed_state = apply_memory_term_narrowing(&initial_state, &memory_terms); + let compiled_precondition = query_inputs + .condition_source + .map(|source| source.summary) .filter(|summary| !summary.evidence().allows_hard_proof()) - .cloned() - .or_else(|| { - condition_source - .map(|source| source.summary) - .filter(|summary| !summary.evidence().allows_hard_proof()) - .cloned() - }), - }; - } - let mut compiled = branch_mode.and_then(|mode| { - condition_source.and_then(|source| { - let compiled = compile_branch_precondition_with_summaries( + .cloned(); + PreconditionApplication::Continue { + initial_state: Box::new(initial_state), + narrowed_state, + compiled_precondition, + summary_conditioned: false, + } + } + TargetQueryExecutionRoute::DynamicTargetCompile { reason: _, mode } => { + if !target_is_local { + let narrowed_state = apply_memory_term_narrowing(&initial_state, &memory_terms); + return PreconditionApplication::Continue { + initial_state: Box::new(initial_state), + narrowed_state, + compiled_precondition: None, + summary_conditioned: false, + }; + } + let compiled = compile_target_precondition_with_summaries( func, &initial_state, - source.region.anchor, - source.branch_truth, + target_addr, &derived_summaries, - )?; - Some((compiled, mode)) - }) - }); - let target_compile_mode = branch_mode.unwrap_or(CompiledPreconditionMode::Necessary); - let mut used_target_compile = false; - if compiled.is_none() - && route.allow_dynamic_target_compile - && let Some(target_compiled) = compile_target_precondition_with_summaries( - func, - &initial_state, - target_addr, - &derived_summaries, - ) - { - compiled = Some((target_compiled, target_compile_mode)); - used_target_compile = true; - } - if route.allow_dynamic_target_compile - && !used_target_compile - && !matches!(target_compile_mode, CompiledPreconditionMode::Necessary) - && let Some(target_compiled) = compile_target_precondition_with_summaries( - func, - &initial_state, - target_addr, - &derived_summaries, - ) - && compiled.as_ref().is_none_or(|(current, _)| { - prefer_compiled_precondition(Some(current), &target_compiled) - }) - { - compiled = Some((target_compiled, CompiledPreconditionMode::Necessary)); - } - if route.allow_vm_target_compile - && compiled.as_ref().is_none_or(|(compiled, _)| { - !matches!( - compiled.summary.precision, - BackwardConditionPrecision::Exact - ) - }) - && let Some(vm_compiled) = compile_vm_target_precondition_with_summaries( - func, - &initial_state, - target_addr, - &derived_summaries, - ) - && prefer_compiled_precondition( - compiled.as_ref().map(|(compiled, _)| compiled), - &vm_compiled, - ) - { - compiled = Some((vm_compiled, CompiledPreconditionMode::Necessary)); - } - let Some((compiled, mode)) = compiled else { - let narrowed_state = apply_memory_term_narrowing(&initial_state, &memory_terms); - return PreconditionApplication::Continue { + ); + match compiled { + Some(compiled) => match apply_compiled_precondition_with_mode( + explorer, + initial_state, + compiled, + compiled_mode_from_guidance(mode), + query_inputs.allow_exact_proof, + ) { + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition, + .. + } => { + let narrowed_state = narrowed_state.or_else(|| { + apply_memory_term_narrowing(initial_state.as_ref(), &memory_terms) + }); + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition, + summary_conditioned: route_summary_conditioned, + } + } + PreconditionApplication::ExactUnsat { + compiled_precondition, + .. + } => PreconditionApplication::ExactUnsat { + compiled_precondition, + summary_conditioned: route_summary_conditioned, + }, + }, + None => { + let narrowed_state = apply_memory_term_narrowing(&initial_state, &memory_terms); + PreconditionApplication::Continue { + initial_state: Box::new(initial_state), + narrowed_state, + compiled_precondition: None, + summary_conditioned: false, + } + } + } + } + TargetQueryExecutionRoute::VmTargetCompile { reason: _ } => { + if !target_is_local { + let narrowed_state = apply_memory_term_narrowing(&initial_state, &memory_terms); + return PreconditionApplication::Continue { + initial_state: Box::new(initial_state), + narrowed_state, + compiled_precondition: None, + summary_conditioned: false, + }; + } + let compiled = compile_vm_target_precondition_with_summaries( + func, + &initial_state, + target_addr, + &derived_summaries, + ); + match compiled { + Some(compiled) => match apply_compiled_precondition_with_mode( + explorer, + initial_state, + compiled, + CompiledPreconditionMode::Necessary, + query_inputs.allow_exact_proof, + ) { + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition, + .. + } => { + let narrowed_state = narrowed_state.or_else(|| { + apply_memory_term_narrowing(initial_state.as_ref(), &memory_terms) + }); + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition, + summary_conditioned: route_summary_conditioned, + } + } + PreconditionApplication::ExactUnsat { + compiled_precondition, + .. + } => PreconditionApplication::ExactUnsat { + compiled_precondition, + summary_conditioned: route_summary_conditioned, + }, + }, + None => { + let narrowed_state = apply_memory_term_narrowing(&initial_state, &memory_terms); + PreconditionApplication::Continue { + initial_state: Box::new(initial_state), + narrowed_state, + compiled_precondition: None, + summary_conditioned: false, + } + } + } + } + TargetQueryExecutionRoute::ResidualOnly { .. } + | TargetQueryExecutionRoute::Refuse { .. } => PreconditionApplication::Continue { initial_state: Box::new(initial_state), - narrowed_state, + narrowed_state: None, compiled_precondition: None, - }; - }; - match apply_compiled_precondition_with_mode(explorer, initial_state, compiled, mode) { + summary_conditioned: false, + }, + } +} + +#[cfg(test)] +fn apply_best_compiled_precondition<'ctx>( + explorer: &PathExplorer<'ctx>, + func: &SsaArtifact, + artifact: Option<&SemanticArtifact>, + initial_state: SymState<'ctx>, + target_addr: u64, + assumption_conflicted: bool, +) -> PreconditionApplication<'ctx> { + let query_inputs = artifact + .map(|artifact| artifact.target_query_route_input(target_addr, assumption_conflicted)) + .unwrap_or_else(|| TargetQueryRouteInput { + route: crate::TargetQueryRoutePlan::dynamic_fallback(), + condition_source: None, + memory_terms: Vec::new(), + allow_exact_proof: !assumption_conflicted, + }); + apply_best_compiled_precondition_for_inputs( + explorer, + func, + initial_state, + target_addr, + &query_inputs, + ) +} + +fn execute_target_query_paths_from_inputs<'ctx>( + explorer: &mut PathExplorer<'ctx>, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + initial_state: SymState<'ctx>, + target_addr: u64, + query_inputs: &TargetQueryRouteInput<'_>, +) -> TargetQueryPaths<'ctx> { + let selected_route = query_inputs.route.clone(); + match apply_best_compiled_precondition_for_inputs( + explorer, + func, + initial_state, + target_addr, + query_inputs, + ) { PreconditionApplication::Continue { initial_state, narrowed_state, compiled_precondition, - } => { - let narrowed_state = narrowed_state - .or_else(|| apply_memory_term_narrowing(initial_state.as_ref(), &memory_terms)); - PreconditionApplication::Continue { - initial_state, + summary_conditioned, + } => TargetQueryPaths { + selected_route, + compiled_precondition, + matched_paths: find_paths_with_compiled_narrowing( + explorer, + *initial_state, narrowed_state, - compiled_precondition, + |explorer, state| explorer.find_paths_to_in_scope(func, scope, state, target_addr), + ), + exact_unsat: false, + summary_conditioned, + }, + PreconditionApplication::ExactUnsat { + compiled_precondition, + summary_conditioned, + } => TargetQueryPaths { + selected_route, + compiled_precondition: Some(compiled_precondition), + matched_paths: Vec::new(), + exact_unsat: true, + summary_conditioned, + }, + } +} + +fn execute_target_query_first_path_from_inputs<'ctx>( + explorer: &mut PathExplorer<'ctx>, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + initial_state: SymState<'ctx>, + target_addr: u64, + query_inputs: &TargetQueryRouteInput<'_>, +) -> TargetQueryPaths<'ctx> { + let selected_route = query_inputs.route.clone(); + match apply_best_compiled_precondition_for_inputs( + explorer, + func, + initial_state, + target_addr, + query_inputs, + ) { + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition, + summary_conditioned, + } => TargetQueryPaths { + selected_route, + compiled_precondition, + matched_paths: find_first_path_with_compiled_narrowing( + explorer, + *initial_state, + narrowed_state, + |explorer, state| explorer.find_path_to_in_scope(func, scope, state, target_addr), + ) + .into_iter() + .collect(), + exact_unsat: false, + summary_conditioned, + }, + PreconditionApplication::ExactUnsat { + compiled_precondition, + summary_conditioned, + } => TargetQueryPaths { + selected_route, + compiled_precondition: Some(compiled_precondition), + matched_paths: Vec::new(), + exact_unsat: true, + summary_conditioned, + }, + } +} + +fn continuation_seed_states<'ctx>( + explorer: &mut PathExplorer<'ctx>, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + bridge_paths: Vec>, +) -> Vec> { + let mut seeds = Vec::new(); + for path in bridge_paths { + if let Ok(next_states) = explorer.advance_current_block_in_scope(func, scope, path.state) { + seeds.extend(next_states.into_iter().filter(state_is_continuation_seed)); + } + if explorer.budget_exhausted() { + break; + } + } + seeds +} + +fn rank_continuation_seeds<'ctx>(mut seeds: Vec>) -> Vec> { + seeds.sort_by_key(|state| (state.num_constraints(), state.depth, state.pc)); + seeds.truncate(CONTINUATION_SEEDED_STATE_CAP); + seeds +} + +fn continuation_followup_execution_route( + route: &TargetQueryExecutionRoute, +) -> TargetQueryExecutionRoute { + match route { + TargetQueryExecutionRoute::DynamicTargetCompile { reason, .. } => { + TargetQueryExecutionRoute::ResidualOnly { + reasons: vec![ + format!( + "continuation-seeded follow-up downgraded dynamic target compile: {reason}" + ), + "continuation-seeded runtime execution".to_string(), + ], + } + } + TargetQueryExecutionRoute::VmTargetCompile { reason } => { + TargetQueryExecutionRoute::ResidualOnly { + reasons: vec![ + format!("continuation-seeded follow-up downgraded vm target compile: {reason}"), + "continuation-seeded runtime execution".to_string(), + ], + } + } + other => other.clone(), + } +} + +fn execute_target_query_paths<'ctx>( + explorer: &mut PathExplorer<'ctx>, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + artifact: Option<&SemanticArtifact>, + initial_state: SymState<'ctx>, + target_addr: u64, + assumption_conflicted: bool, +) -> TargetQueryPaths<'ctx> { + let query_inputs = build_target_query_inputs( + explorer, + func, + scope, + artifact, + target_addr, + assumption_conflicted, + true, + ); + if let TargetQueryExecutionRoute::ContinuationSeeded { + bridge_target, + route, + } = &query_inputs.route.execution + { + let mut bridge_inputs = build_target_query_inputs( + explorer, + func, + scope, + artifact, + *bridge_target, + assumption_conflicted, + false, + ); + bridge_inputs.route.execution = TargetQueryExecutionRoute::ResidualOnly { + reasons: vec!["continuation bridge search".to_string()], + }; + let bridge_paths = explorer.with_prune_infeasible(false, |explorer| { + execute_target_query_paths_from_inputs( + explorer, + func, + scope, + initial_state, + *bridge_target, + &bridge_inputs, + ) + }); + let mut summary_conditioned = bridge_paths.summary_conditioned; + if bridge_paths.exact_unsat || bridge_paths.matched_paths.is_empty() { + return TargetQueryPaths { + selected_route: query_inputs.route.clone(), + compiled_precondition: bridge_paths.compiled_precondition, + matched_paths: Vec::new(), + exact_unsat: bridge_paths.exact_unsat, + summary_conditioned, + }; + } + + let bridge_compiled_precondition = bridge_paths.compiled_precondition; + let seeded_states = rank_continuation_seeds(continuation_seed_states( + explorer, + func, + scope, + bridge_paths.matched_paths, + )); + if seeded_states.is_empty() { + return TargetQueryPaths { + selected_route: query_inputs.route.clone(), + compiled_precondition: bridge_compiled_precondition, + matched_paths: Vec::new(), + exact_unsat: false, + summary_conditioned, + }; + } + + let mut final_inputs = query_inputs.clone(); + final_inputs.route.execution = continuation_followup_execution_route(route); + let mut matched_paths = Vec::new(); + let mut compiled_precondition = None; + let mut any_exact_unsat = false; + for state in seeded_states { + let next = explorer.with_exception_bridge_guidance(false, |explorer| { + execute_target_query_paths_from_inputs( + explorer, + func, + scope, + state, + target_addr, + &final_inputs, + ) + }); + if compiled_precondition.is_none() { + compiled_precondition = next.compiled_precondition; + } + summary_conditioned |= next.summary_conditioned; + any_exact_unsat |= next.exact_unsat; + matched_paths.extend(next.matched_paths); + if explorer.budget_exhausted() { + break; } } - unsat => unsat, + let no_matches = matched_paths.is_empty(); + + return TargetQueryPaths { + selected_route: query_inputs.route, + compiled_precondition: compiled_precondition.or(bridge_compiled_precondition), + matched_paths, + exact_unsat: no_matches && any_exact_unsat, + summary_conditioned, + }; } + + execute_target_query_paths_from_inputs( + explorer, + func, + scope, + initial_state, + target_addr, + &query_inputs, + ) +} + +fn execute_target_query_first_path<'ctx>( + explorer: &mut PathExplorer<'ctx>, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + artifact: Option<&SemanticArtifact>, + initial_state: SymState<'ctx>, + target_addr: u64, + assumption_conflicted: bool, +) -> TargetQueryPaths<'ctx> { + let query_inputs = build_target_query_inputs( + explorer, + func, + scope, + artifact, + target_addr, + assumption_conflicted, + true, + ); + if let TargetQueryExecutionRoute::ContinuationSeeded { + bridge_target, + route, + } = &query_inputs.route.execution + { + debug_query_phase_log(&format!( + "continuation:start target=0x{target_addr:x} bridge=0x{bridge_target:x}" + )); + let mut bridge_inputs = build_target_query_inputs( + explorer, + func, + scope, + artifact, + *bridge_target, + assumption_conflicted, + false, + ); + bridge_inputs.route.execution = TargetQueryExecutionRoute::ResidualOnly { + reasons: vec!["continuation bridge search".to_string()], + }; + debug_query_phase_log(&format!( + "continuation:bridge_search target=0x{bridge_target:x} route={:?}", + bridge_inputs.route.execution + )); + let bridge_selected_route = bridge_inputs.route.clone(); + let bridge_paths = explorer.with_prune_infeasible(false, |explorer| TargetQueryPaths { + selected_route: bridge_selected_route, + compiled_precondition: None, + matched_paths: explorer + .find_path_to_in_scope(func, scope, initial_state, *bridge_target) + .into_iter() + .collect(), + exact_unsat: false, + summary_conditioned: false, + }); + debug_query_phase_log(&format!( + "continuation:bridge_done target=0x{bridge_target:x} matched={} exact_unsat={} budget_exhausted={}", + bridge_paths.matched_paths.len(), + bridge_paths.exact_unsat, + explorer.budget_exhausted(), + )); + let mut summary_conditioned = bridge_paths.summary_conditioned; + let Some(bridge_path) = bridge_paths.matched_paths.into_iter().next() else { + return TargetQueryPaths { + selected_route: query_inputs.route.clone(), + compiled_precondition: bridge_paths.compiled_precondition, + matched_paths: Vec::new(), + exact_unsat: bridge_paths.exact_unsat, + summary_conditioned, + }; + }; + + let bridge_compiled_precondition = bridge_paths.compiled_precondition; + let seeded_states = rank_continuation_seeds(continuation_seed_states( + explorer, + func, + scope, + vec![bridge_path], + )); + debug_query_phase_log(&format!( + "continuation:seeded count={} budget_exhausted={}", + seeded_states.len(), + explorer.budget_exhausted(), + )); + let Some(state) = seeded_states.into_iter().next() else { + return TargetQueryPaths { + selected_route: query_inputs.route.clone(), + compiled_precondition: bridge_compiled_precondition, + matched_paths: Vec::new(), + exact_unsat: false, + summary_conditioned, + }; + }; + + let mut final_inputs = query_inputs.clone(); + final_inputs.route.execution = continuation_followup_execution_route(route); + debug_query_phase_log(&format!( + "continuation:final_search target=0x{target_addr:x} route={:?}", + final_inputs.route.execution + )); + let next = explorer.with_exception_bridge_guidance(false, |explorer| { + execute_target_query_first_path_from_inputs( + explorer, + func, + scope, + state, + target_addr, + &final_inputs, + ) + }); + debug_query_phase_log(&format!( + "continuation:final_done target=0x{target_addr:x} matched={} exact_unsat={} budget_exhausted={}", + next.matched_paths.len(), + next.exact_unsat, + explorer.budget_exhausted(), + )); + summary_conditioned |= next.summary_conditioned; + return TargetQueryPaths { + selected_route: query_inputs.route, + compiled_precondition: next.compiled_precondition.or(bridge_compiled_precondition), + matched_paths: next.matched_paths, + exact_unsat: next.exact_unsat, + summary_conditioned, + }; + } + + execute_target_query_first_path_from_inputs( + explorer, + func, + scope, + initial_state, + target_addr, + &query_inputs, + ) } impl<'ctx> PathExplorer<'ctx> { @@ -841,7 +2012,17 @@ impl<'ctx> PathExplorer<'ctx> { initial_state: SymState<'ctx>, target_addr: u64, ) -> ReachabilityResult<'ctx> { - self.can_reach_with_artifact(func, None, initial_state, target_addr) + self.can_reach_with_artifact_in_scope(func, None, None, initial_state, target_addr) + } + + pub fn can_reach_in_scope( + &mut self, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + initial_state: SymState<'ctx>, + target_addr: u64, + ) -> ReachabilityResult<'ctx> { + self.can_reach_with_artifact_in_scope(func, scope, None, initial_state, target_addr) } pub fn can_reach_with_artifact( @@ -851,33 +2032,31 @@ impl<'ctx> PathExplorer<'ctx> { initial_state: SymState<'ctx>, target_addr: u64, ) -> ReachabilityResult<'ctx> { - let (paths, compiled_precondition) = match apply_best_compiled_precondition( + self.can_reach_with_artifact_in_scope(func, None, artifact, initial_state, target_addr) + } + + pub fn can_reach_with_artifact_in_scope( + &mut self, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + artifact: Option<&SemanticArtifact>, + mut initial_state: SymState<'ctx>, + target_addr: u64, + ) -> ReachabilityResult<'ctx> { + let assumption_outcome = assumption_usage_for_query(self, func, &mut initial_state); + let query = execute_target_query_paths( self, func, + scope, artifact, initial_state, target_addr, - ) { - PreconditionApplication::Continue { - initial_state, - narrowed_state, - compiled_precondition, - } => ( - find_paths_with_compiled_narrowing( - self, - *initial_state, - narrowed_state, - |explorer, state| explorer.find_paths_to(func, state, target_addr), - ), - compiled_precondition, - ), - PreconditionApplication::ExactUnsat { - compiled_precondition, - } => (Vec::new(), Some(compiled_precondition)), - }; + assumption_outcome.conflicted, + ); let stats = self.stats().clone(); let solver_stats = self.solver().stats(); - let status = if !paths.is_empty() { + let summary_conditioned = query.summary_conditioned || stats_show_summary_guidance(&stats); + let status = if !query.matched_paths.is_empty() { ReachabilityStatus::Reachable } else if self.budget_exhausted() { ReachabilityStatus::BudgetExhausted @@ -887,8 +2066,12 @@ impl<'ctx> PathExplorer<'ctx> { ReachabilityResult { status, target_addr, - compiled_precondition, - paths, + selected_route: query.selected_route, + compiled_precondition: query.compiled_precondition, + paths: query.matched_paths, + assumption_usage: assumption_outcome.usage, + assumption_conditioned: assumption_outcome.conditioned, + summary_conditioned, stats, solver_stats, } @@ -901,7 +2084,17 @@ impl<'ctx> PathExplorer<'ctx> { initial_state: SymState<'ctx>, target_pc: u64, ) -> PathConditionResult<'ctx> { - self.path_conditions_at_with_artifact(func, None, initial_state, target_pc) + self.path_conditions_at_with_artifact_in_scope(func, None, None, initial_state, target_pc) + } + + pub fn path_conditions_at_in_scope( + &mut self, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + initial_state: SymState<'ctx>, + target_pc: u64, + ) -> PathConditionResult<'ctx> { + self.path_conditions_at_with_artifact_in_scope(func, scope, None, initial_state, target_pc) } pub fn path_conditions_at_with_artifact( @@ -911,39 +2104,47 @@ impl<'ctx> PathExplorer<'ctx> { initial_state: SymState<'ctx>, target_pc: u64, ) -> PathConditionResult<'ctx> { - let (matching_paths, compiled_precondition) = match apply_best_compiled_precondition( + self.path_conditions_at_with_artifact_in_scope( + func, + None, + artifact, + initial_state, + target_pc, + ) + } + + pub fn path_conditions_at_with_artifact_in_scope( + &mut self, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + artifact: Option<&SemanticArtifact>, + mut initial_state: SymState<'ctx>, + target_pc: u64, + ) -> PathConditionResult<'ctx> { + let assumption_outcome = assumption_usage_for_query(self, func, &mut initial_state); + let query = execute_target_query_paths( self, func, + scope, artifact, initial_state, target_pc, - ) { - PreconditionApplication::Continue { - initial_state, - narrowed_state, - compiled_precondition, - } => ( - find_paths_with_compiled_narrowing( - self, - *initial_state, - narrowed_state, - |explorer, state| explorer.find_paths_to(func, state, target_pc), - ), - compiled_precondition, - ), - PreconditionApplication::ExactUnsat { - compiled_precondition, - } => (Vec::new(), Some(compiled_precondition)), - }; + assumption_outcome.conflicted, + ); let stats = self.stats().clone(); let solver_stats = self.solver().stats(); - let conditions = matching_paths.iter().map(condition_summary).collect(); + let summary_conditioned = query.summary_conditioned || stats_show_summary_guidance(&stats); + let conditions = query.matched_paths.iter().map(condition_summary).collect(); PathConditionResult { completion: completion_from_stats(&stats), target_pc, - compiled_precondition, + selected_route: query.selected_route, + compiled_precondition: query.compiled_precondition, conditions, - matching_paths, + matching_paths: query.matched_paths, + assumption_usage: assumption_outcome.usage, + assumption_conditioned: assumption_outcome.conditioned, + summary_conditioned, stats, solver_stats, } @@ -956,7 +2157,17 @@ impl<'ctx> PathExplorer<'ctx> { initial_state: SymState<'ctx>, target_addr: u64, ) -> SolveResult<'ctx> { - self.solve_for_target_with_artifact(func, None, initial_state, target_addr) + self.solve_for_target_with_artifact_in_scope(func, None, None, initial_state, target_addr) + } + + pub fn solve_for_target_in_scope( + &mut self, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + initial_state: SymState<'ctx>, + target_addr: u64, + ) -> SolveResult<'ctx> { + self.solve_for_target_with_artifact_in_scope(func, scope, None, initial_state, target_addr) } pub fn solve_for_target_with_artifact( @@ -966,57 +2177,101 @@ impl<'ctx> PathExplorer<'ctx> { initial_state: SymState<'ctx>, target_addr: u64, ) -> SolveResult<'ctx> { - let (matched_paths, compiled_precondition, exact_unsat) = - match apply_best_compiled_precondition(self, func, artifact, initial_state, target_addr) - { - PreconditionApplication::Continue { - initial_state, - narrowed_state, - compiled_precondition, - } => ( - find_paths_with_compiled_narrowing( - self, - *initial_state, - narrowed_state, - |explorer, state| explorer.find_paths_to(func, state, target_addr), - ), - compiled_precondition, - false, - ), - PreconditionApplication::ExactUnsat { - compiled_precondition, - } => (Vec::new(), Some(compiled_precondition), true), - }; + self.solve_for_target_with_artifact_in_scope( + func, + None, + artifact, + initial_state, + target_addr, + ) + } + + pub fn solve_for_target_with_artifact_in_scope( + &mut self, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + artifact: Option<&SemanticArtifact>, + mut initial_state: SymState<'ctx>, + target_addr: u64, + ) -> SolveResult<'ctx> { + let assumption_outcome = assumption_usage_for_query(self, func, &mut initial_state); + let validation_initial_state = initial_state.fork(); + let query = execute_target_query_first_path( + self, + func, + scope, + artifact, + initial_state, + target_addr, + assumption_outcome.conflicted, + ); let stats = self.stats().clone(); let solver_stats = self.solver().stats(); - let selected_path_index = matched_paths + let summary_conditioned = query.summary_conditioned || stats_show_summary_guidance(&stats); + let selected_path_index = query + .matched_paths .iter() .enumerate() .min_by_key(|(idx, path)| (path.num_constraints(), path.depth, *idx)) .map(|(idx, _)| idx); - let solution = selected_path_index.and_then(|idx| self.solve_path(&matched_paths[idx])); - let status = match ( - exact_unsat, - selected_path_index, - solution.as_ref(), - completion_from_stats(&stats), - ) { - (true, _, _, _) => SolveStatus::Unsat, - (false, _, Some(_), _) => SolveStatus::Solved, - (false, None, _, QueryCompletion::BudgetExhausted) => SolveStatus::BudgetExhausted, - (false, None, _, QueryCompletion::Complete) => SolveStatus::Unsat, - (false, Some(_), None, QueryCompletion::BudgetExhausted) => { - SolveStatus::BudgetExhausted - } - (false, Some(_), None, _) => SolveStatus::Unknown, + let constraint_graph = selected_path_index + .map(|idx| &query.matched_paths[idx]) + .map(|path| { + build_final_constraint_graph_for_path( + func, + path, + &stats.runtime_loop_exact_recurrences, + target_addr, + None, + ) + }) + .unwrap_or_default(); + let solution = if solution_extraction_allowed(&query.selected_route, &stats) { + selected_path_index.and_then(|idx| self.solve_path(&query.matched_paths[idx])) + } else { + None + }; + let tactic_solution = if solution_extraction_allowed(&query.selected_route, &stats) { + selected_path_index.and_then(|idx| { + self.solve_path_with_constraint_graph_tactics( + &query.matched_paths[idx], + &constraint_graph, + &stats.runtime_loop_exact_recurrences, + ) + }) + } else { + None }; + let solution = tactic_solution.or(solution); + let mut replay_backend = LiftedReplayBackend::new(self); + let (status, verification, witness) = verify_solve_result( + SolveVerificationRequest { + func, + scope, + selected_route: &query.selected_route, + stats: &stats, + validation_initial_state, + target_addr, + exact_unsat: query.exact_unsat, + selected_path_index, + solution: solution.as_ref(), + constraint_graph: &constraint_graph, + }, + &mut replay_backend, + ); SolveResult { status, target_addr, - compiled_precondition, - matched_paths, + selected_route: query.selected_route, + compiled_precondition: query.compiled_precondition, + matched_paths: query.matched_paths, selected_path_index, solution, + assumption_usage: assumption_outcome.usage, + assumption_conditioned: assumption_outcome.conditioned, + summary_conditioned, + verification, + witness, stats, solver_stats, } @@ -1048,8 +2303,7 @@ mod tests { use super::{ CompiledPreconditionMode, PreconditionApplication, apply_best_compiled_precondition, - apply_compiled_precondition_with_mode, prefer_compiled_precondition, - target_condition_source, vm_target_case_values, + apply_compiled_precondition_with_mode, prefer_compiled_precondition, vm_target_case_values, }; use crate::{ ArtifactGranularity, BackwardConditionPrecision, BackwardConditionSummary, @@ -1057,11 +2311,15 @@ mod tests { MemoryFact, NativeArtifactBody, NativeFunctionSummary, RefinementStage, RegionKey, ResidualReason, SatResult, SemanticArtifact, SemanticArtifactBody, SemanticArtifactDiagnostics, SemanticEvidence, SemanticEvidenceReason, SemanticRegion, - SliceClass, SymQueryConfig, SymState, TargetFact, TargetQueryPlan, VmBinaryOp, - VmGuardCondition, VmGuardedExit, VmStateUpdate, VmStepSummary, VmTransferArm, VmValueExpr, + SliceClass, SymQueryConfig, SymState, TargetFact, TargetQueryExecutionRoute, + TargetQueryPlan, VmBinaryOp, VmGuardCondition, VmGuardedExit, VmStateUpdate, VmStepSummary, + VmTransferArm, VmValueExpr, }; use r2il::{R2ILBlock, R2ILOp, SpaceId, Varnode}; - use r2ssa::SsaArtifact; + use r2ssa::{ + AnalysisAssumption, AssumptionProvenance, AssumptionScope, AssumptionSet, + AssumptionSubject, AssumptionValue, SsaArtifact, + }; use z3::Context; use z3::ast::BV; @@ -1213,6 +2471,47 @@ mod tests { ] } + fn make_bounded_copy_loop_blocks() -> Vec { + vec![ + R2ILBlock { + addr: 0x1000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Load { + dst: make_reg(TMP0, 1), + space: SpaceId::Ram, + addr: make_const(0x2000, 8), + }, + R2ILOp::Store { + space: SpaceId::Ram, + addr: make_const(0x3000, 8), + val: make_reg(TMP0, 1), + }, + R2ILOp::IntLess { + dst: make_reg(TMP1, 1), + a: make_reg(RDI, 8), + b: make_const(0x1000, 8), + }, + R2ILOp::CBranch { + target: make_const(0x1000, 8), + cond: make_reg(TMP1, 1), + }, + ], + }, + R2ILBlock { + addr: 0x1004, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + }, + ] + } + fn make_vm_step_summary_with_transfers(transfers: Vec) -> VmStepSummary { VmStepSummary { kind: crate::InterpreterKind::SwitchDispatch, @@ -1259,17 +2558,13 @@ mod tests { let explorer = SymQueryConfig::default().make_explorer(&ctx); let original_constraints = state.num_constraints(); - match apply_best_compiled_precondition(&explorer, &func, None, state, 0x1010) { + match apply_best_compiled_precondition(&explorer, &func, None, state, 0x1010, false) { PreconditionApplication::Continue { initial_state, narrowed_state, - compiled_precondition, + compiled_precondition: _, + .. } => { - let compiled = compiled_precondition.expect("compiled precondition"); - assert_eq!( - compiled.precision, - crate::BackwardConditionPrecision::ResidualSearchRequired - ); assert_eq!(initial_state.num_constraints(), original_constraints); assert!(narrowed_state.is_none()); } @@ -1279,6 +2574,82 @@ mod tests { } } + #[test] + fn non_local_targets_skip_dynamic_precondition_compile() { + let blocks = make_residual_precondition_blocks(); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic("reg:56_0", 64); + + let explorer = SymQueryConfig::default().make_explorer(&ctx); + let original_constraints = state.num_constraints(); + match apply_best_compiled_precondition(&explorer, &func, None, state, 0x4000, false) { + PreconditionApplication::Continue { + initial_state, + narrowed_state, + compiled_precondition, + .. + } => { + assert!(compiled_precondition.is_none()); + assert!(narrowed_state.is_none()); + assert_eq!(initial_state.num_constraints(), original_constraints); + } + PreconditionApplication::ExactUnsat { .. } => { + panic!("non-local targets should bypass dynamic backward compilation") + } + } + } + + #[test] + fn register_assumptions_condition_query_results() { + let blocks = make_residual_precondition_blocks(); + let base = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); + let reg_name = base + .graph() + .values + .iter() + .find(|value| value.var.version == 0 && value.var.is_register() && value.var.size == 8) + .expect("version-zero input register") + .var + .name + .clone(); + let func = base.with_assumptions(&AssumptionSet::new(vec![AnalysisAssumption { + id: Some("force-rdi".to_string()), + subject: AssumptionSubject::Register { name: reg_name }, + value: AssumptionValue::Constant { value: 1 }, + scope: AssumptionScope::Query, + provenance: AssumptionProvenance::User, + }])); + let (state_name, bits) = func + .facts() + .applied_assumption_bindings + .iter() + .find_map(|binding| match &binding.binding { + r2ssa::PreparedAssumptionBindingKind::Register { + state_name, bits, .. + } => Some((state_name.clone(), *bits)), + _ => None, + }) + .expect("register assumption binding"); + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic(&state_name, bits); + + let mut explorer = SymQueryConfig::default().make_explorer(&ctx); + let reach = explorer.can_reach(&func, state, 0x1010); + assert_eq!(reach.status, super::ReachabilityStatus::Reachable); + assert!(reach.assumption_conditioned); + assert!(!reach.summary_conditioned); + assert_eq!(reach.assumption_usage.applied.len(), 1); + assert!(reach.assumption_usage.ignored.is_empty()); + assert!(reach.assumption_usage.conflicts.is_empty()); + assert!(matches!( + reach.assumption_usage.applied[0].value, + AssumptionValue::Constant { value: 1 } + )); + } + #[test] fn likely_compiled_preconditions_seed_narrowed_search_state() { let ctx = Context::thread_local(); @@ -1307,11 +2678,13 @@ mod tests { state, compiled, CompiledPreconditionMode::Necessary, + true, ) { PreconditionApplication::Continue { initial_state, narrowed_state, compiled_precondition, + .. } => { let compiled = compiled_precondition.expect("compiled precondition"); assert_eq!(compiled.precision, BackwardConditionPrecision::OverApprox); @@ -1354,6 +2727,7 @@ mod tests { state, compiled, CompiledPreconditionMode::NarrowOnly, + true, ) { PreconditionApplication::Continue { narrowed_state, @@ -1402,11 +2776,13 @@ mod tests { state, compiled, CompiledPreconditionMode::NarrowOnly, + true, ) { PreconditionApplication::Continue { initial_state, narrowed_state, compiled_precondition, + .. } => { assert_eq!(initial_state.num_constraints(), original_constraints); assert_eq!( @@ -1426,6 +2802,177 @@ mod tests { } } + #[test] + fn exact_preconditions_disable_unsat_shortcuts_when_exact_proof_is_disallowed() { + let ctx = Context::thread_local(); + let state = SymState::new(&ctx, 0x1000); + let explorer = SymQueryConfig::default().make_explorer(&ctx); + + let compiled = crate::CompiledBackwardCondition { + predicate: z3::ast::Bool::from_bool(false), + summary: BackwardConditionSummary { + simplified: "guard".to_string(), + terms: vec!["guard".to_string()], + memory_terms: Vec::new(), + backward_memory_substitutions: 0, + backward_memory_candidate_enumerations: 0, + backward_memory_residual_fallbacks: 0, + precision: BackwardConditionPrecision::Exact, + supported_paths: 1, + total_paths: 1, + }, + }; + match apply_compiled_precondition_with_mode( + &explorer, + state, + compiled, + CompiledPreconditionMode::Necessary, + false, + ) { + PreconditionApplication::Continue { + compiled_precondition, + .. + } => { + assert_eq!( + compiled_precondition + .expect("compiled precondition") + .precision, + BackwardConditionPrecision::Exact + ); + } + PreconditionApplication::ExactUnsat { .. } => { + panic!("exact-proof downgrade must disable exact unsat shortcuts") + } + } + } + + #[test] + fn bounded_copy_loops_raise_recommended_query_max_depth() { + let blocks = make_bounded_copy_loop_blocks(); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); + let ctx = Context::thread_local(); + let state = SymState::new(&ctx, 0x1000); + + let budget = super::recommended_query_max_depth(&func, &state); + + assert!( + budget >= 16_384, + "bounded copy loop budget should scale with trip count, got {budget}" + ); + } + + #[test] + fn bounded_copy_loops_raise_recommended_query_timeout() { + let blocks = make_bounded_copy_loop_blocks(); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); + + assert!( + super::recommended_query_timeout(&func) >= std::time::Duration::from_secs(120), + "bounded copy loop timeout should exceed default budget" + ); + } + + #[test] + fn continuation_seeded_routes_cap_recommended_query_max_depth() { + let blocks = make_bounded_copy_loop_blocks(); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); + let ctx = Context::thread_local(); + let state = SymState::new(&ctx, 0x1000); + let route = crate::TargetQueryRoutePlan { + target_plan: crate::TargetQueryPlan::Ready { + mode: crate::QueryGuidanceMode::NarrowOnly, + }, + execution: crate::TargetQueryExecutionRoute::ContinuationSeeded { + bridge_target: 0x1010, + route: Box::new(crate::TargetQueryExecutionRoute::DynamicTargetCompile { + reason: "test".to_string(), + mode: crate::QueryGuidanceMode::NarrowOnly, + }), + }, + }; + + let budget = super::recommended_query_max_depth_for_route(&func, &state, Some(&route)); + + assert!( + budget <= super::CONTINUATION_QUERY_MAX_DEPTH_CAP, + "continuation-seeded route should cap inflated depth budget, got {budget}" + ); + } + + #[test] + fn continuation_seeded_routes_cap_recommended_query_max_states() { + let route = crate::TargetQueryRoutePlan { + target_plan: crate::TargetQueryPlan::Ready { + mode: crate::QueryGuidanceMode::NarrowOnly, + }, + execution: crate::TargetQueryExecutionRoute::ContinuationSeeded { + bridge_target: 0x1010, + route: Box::new(crate::TargetQueryExecutionRoute::DynamicTargetCompile { + reason: "test".to_string(), + mode: crate::QueryGuidanceMode::NarrowOnly, + }), + }, + }; + + let max_states = super::recommended_query_max_states_for_route(1_000, Some(&route)); + + assert_eq!(max_states, super::CONTINUATION_QUERY_MAX_STATES_CAP); + } + + #[test] + fn continuation_seeded_routes_cap_recommended_query_timeout() { + let blocks = make_bounded_copy_loop_blocks(); + let func = SsaArtifact::for_symbolic(&blocks, None).expect("ssa"); + let route = crate::TargetQueryRoutePlan { + target_plan: crate::TargetQueryPlan::Ready { + mode: crate::QueryGuidanceMode::NarrowOnly, + }, + execution: crate::TargetQueryExecutionRoute::ContinuationSeeded { + bridge_target: 0x1010, + route: Box::new(crate::TargetQueryExecutionRoute::DynamicTargetCompile { + reason: "test".to_string(), + mode: crate::QueryGuidanceMode::NarrowOnly, + }), + }, + }; + + let timeout = super::recommended_query_timeout_for_route(&func, Some(&route)); + + assert!( + timeout <= std::time::Duration::from_secs(30), + "continuation-seeded route should cap inflated timeout, got {timeout:?}" + ); + } + + #[test] + fn continuation_followup_downgrades_dynamic_target_compile() { + let route = crate::TargetQueryExecutionRoute::DynamicTargetCompile { + reason: "LargeCfg".to_string(), + mode: crate::QueryGuidanceMode::NarrowOnly, + }; + + match super::continuation_followup_execution_route(&route) { + crate::TargetQueryExecutionRoute::ResidualOnly { reasons } => { + assert!( + reasons + .iter() + .any(|reason| reason.contains("dynamic target compile")), + "expected dynamic compile downgrade reason, got {reasons:?}" + ); + } + other => panic!("expected residual-only continuation follow-up, got {other:?}"), + } + } + + #[test] + fn continuation_followup_keeps_artifact_guided_routes() { + let route = crate::TargetQueryExecutionRoute::ArtifactCondition { + mode: crate::QueryGuidanceMode::Necessary, + }; + + assert_eq!(super::continuation_followup_execution_route(&route), route); + } + #[test] fn artifact_memory_terms_seed_narrowed_state_without_branch_recompile() { let blocks = make_residual_precondition_blocks(); @@ -1539,11 +3086,19 @@ mod tests { default_diagnostics(), ); - match apply_best_compiled_precondition(&explorer, &func, Some(&artifact), state, 0x2000) { + match apply_best_compiled_precondition( + &explorer, + &func, + Some(&artifact), + state, + 0x2000, + false, + ) { PreconditionApplication::Continue { initial_state, narrowed_state, compiled_precondition, + .. } => { assert!(compiled_precondition.is_none()); assert_eq!(initial_state.num_constraints(), original_constraints); @@ -1703,11 +3258,19 @@ mod tests { default_diagnostics(), ); - match apply_best_compiled_precondition(&explorer, &func, Some(&artifact), state, 0x2000) { + match apply_best_compiled_precondition( + &explorer, + &func, + Some(&artifact), + state, + 0x2000, + false, + ) { PreconditionApplication::Continue { initial_state, narrowed_state, compiled_precondition, + .. } => { assert!(compiled_precondition.is_none()); assert_eq!(initial_state.num_constraints(), original_constraints); @@ -1795,11 +3358,19 @@ mod tests { default_diagnostics(), ); - match apply_best_compiled_precondition(&explorer, &func, Some(&artifact), state, 0x2000) { + match apply_best_compiled_precondition( + &explorer, + &func, + Some(&artifact), + state, + 0x2000, + false, + ) { PreconditionApplication::Continue { initial_state, narrowed_state, compiled_precondition, + .. } => { assert!(compiled_precondition.is_none()); assert_eq!(initial_state.num_constraints(), original_constraints); @@ -1871,21 +3442,23 @@ mod tests { default_diagnostics(), ); - match apply_best_compiled_precondition(&explorer, &func, Some(&artifact), state, 0x2000) { + match apply_best_compiled_precondition( + &explorer, + &func, + Some(&artifact), + state, + 0x2000, + false, + ) { PreconditionApplication::Continue { initial_state, narrowed_state, - compiled_precondition, + compiled_precondition: _, + .. } => { assert_eq!(initial_state.num_constraints(), original_constraints); let narrowed_state = narrowed_state.expect("narrowed state"); assert_eq!(narrowed_state.num_constraints(), original_constraints + 1); - assert_eq!( - compiled_precondition - .expect("worker island summary") - .precision, - BackwardConditionPrecision::OverApprox - ); let narrowed_value = narrowed_state .symbolic_inputs() .get("sym_mem") @@ -1973,18 +3546,23 @@ mod tests { default_diagnostics(), ); - match apply_best_compiled_precondition(&explorer, &func, Some(&artifact), state, 0x1010) { + match apply_best_compiled_precondition( + &explorer, + &func, + Some(&artifact), + state, + 0x1010, + false, + ) { PreconditionApplication::Continue { initial_state, narrowed_state, compiled_precondition, + .. } => { assert!(initial_state.num_constraints() >= original_constraints); assert!(narrowed_state.is_none()); - assert!( - compiled_precondition.is_none(), - "exact artifact-guided branch narrowing should not surface fallback compiled metadata" - ); + assert!(compiled_precondition.is_some()); } PreconditionApplication::ExactUnsat { .. } => { panic!("necessary worker islands should use the real compiled precondition path") @@ -2035,7 +3613,8 @@ mod tests { default_diagnostics(), ); - let actionable = target_condition_source(&artifact, 0x1010, false) + let actionable = artifact + .target_condition_source(0x1010, false) .expect("actionable target condition source"); assert!( matches!( @@ -2046,9 +3625,9 @@ mod tests { ), "multi-exit regions must not be treated as necessary-for-target" ); - assert_eq!(actionable.region.anchor, 0x1000); + assert_eq!(actionable.block_addr, 0x1000); assert!( - target_condition_source(&artifact, 0x1010, true).is_none(), + artifact.target_condition_source(0x1010, true).is_none(), "hard-proof target narrowing must reject non-necessary regions" ); } @@ -2145,10 +3724,12 @@ mod tests { route.target_plan, TargetQueryPlan::Residual { .. } )); - assert!(!route.allow_memory_term_narrowing); - assert!(!route.allow_dynamic_target_compile); + assert!(matches!( + route.execution, + TargetQueryExecutionRoute::ResidualOnly { .. } + )); assert!( - target_condition_source(&artifact, 0x1010, false).is_none(), + artifact.target_condition_source(0x1010, false).is_none(), "materially conflicting target sources must not collapse into one authoritative source" ); let memory_terms = artifact.actionable_memory_terms_for_target(0x1010); @@ -2238,11 +3819,19 @@ mod tests { default_diagnostics(), ); - match apply_best_compiled_precondition(&explorer, &func, Some(&artifact), state, 0x1010) { + match apply_best_compiled_precondition( + &explorer, + &func, + Some(&artifact), + state, + 0x1010, + false, + ) { PreconditionApplication::Continue { initial_state, narrowed_state, compiled_precondition, + .. } => { assert!(compiled_precondition.is_none()); assert!( diff --git a/crates/r2sym/src/replay.rs b/crates/r2sym/src/replay.rs index 90dbd3d..00264d7 100644 --- a/crates/r2sym/src/replay.rs +++ b/crates/r2sym/src/replay.rs @@ -46,6 +46,149 @@ pub struct ReplayMemoryOverlay { pub name: String, } +const REPLAY_FINGERPRINT_OFFSET_BASIS: u64 = 0xcbf29ce484222325; +const REPLAY_FINGERPRINT_PRIME: u64 = 0x100000001b3; + +fn replay_fingerprint_update(state: &mut u64, bytes: &[u8]) { + for byte in bytes { + *state ^= u64::from(*byte); + *state = state.wrapping_mul(REPLAY_FINGERPRINT_PRIME); + } +} + +fn replay_fingerprint_tag(state: &mut u64, tag: u8) { + replay_fingerprint_update(state, &[tag]); +} + +fn replay_fingerprint_u64(state: &mut u64, tag: u8, value: u64) { + replay_fingerprint_tag(state, tag); + replay_fingerprint_update(state, &value.to_le_bytes()); +} + +fn replay_fingerprint_bool(state: &mut u64, tag: u8, value: bool) { + replay_fingerprint_tag(state, tag); + replay_fingerprint_update(state, &[u8::from(value)]); +} + +fn replay_fingerprint_str(state: &mut u64, tag: u8, value: &str) { + replay_fingerprint_tag(state, tag); + replay_fingerprint_update(state, &(value.len() as u64).to_le_bytes()); + replay_fingerprint_update(state, value.as_bytes()); +} + +fn replay_fingerprint_bytes(state: &mut u64, tag: u8, value: &[u8]) { + replay_fingerprint_tag(state, tag); + replay_fingerprint_update(state, &(value.len() as u64).to_le_bytes()); + replay_fingerprint_update(state, value); +} + +fn canonical_replay_register_name(name: &str) -> String { + name.trim().to_ascii_uppercase() +} + +fn canonical_replay_label(label: &Option) -> Option<&str> { + label + .as_deref() + .map(str::trim) + .filter(|label| !label.is_empty()) +} + +pub fn stable_replay_seed_fingerprint(seed: &ReplaySeed) -> u64 { + let mut state = REPLAY_FINGERPRINT_OFFSET_BASIS; + replay_fingerprint_tag(&mut state, 0x01); + match seed.checkpoint_id { + Some(value) => replay_fingerprint_u64(&mut state, 0x02, value), + None => replay_fingerprint_tag(&mut state, 0x03), + } + match seed.entry_pc { + Some(value) => replay_fingerprint_u64(&mut state, 0x04, value), + None => replay_fingerprint_tag(&mut state, 0x05), + } + + let mut registers: Vec<_> = seed + .registers + .iter() + .map(|register| { + ( + canonical_replay_register_name(®ister.name), + register.value, + ) + }) + .collect(); + registers.sort_unstable(); + replay_fingerprint_u64(&mut state, 0x06, registers.len() as u64); + for (name, value) in registers { + replay_fingerprint_str(&mut state, 0x07, &name); + replay_fingerprint_u64(&mut state, 0x08, value); + } + + let mut memory: Vec<_> = seed + .memory + .iter() + .map(|window| { + ( + window.addr, + canonical_replay_label(&window.label).map(str::to_string), + window.bytes.clone(), + ) + }) + .collect(); + memory.sort_unstable_by(|left, right| { + left.0 + .cmp(&right.0) + .then(left.1.cmp(&right.1)) + .then(left.2.cmp(&right.2)) + }); + replay_fingerprint_u64(&mut state, 0x09, memory.len() as u64); + for (addr, label, bytes) in memory { + replay_fingerprint_u64(&mut state, 0x0a, addr); + match label { + Some(label) => replay_fingerprint_str(&mut state, 0x0b, &label), + None => replay_fingerprint_tag(&mut state, 0x0c), + } + replay_fingerprint_bytes(&mut state, 0x0d, &bytes); + } + + let mut register_overlays: Vec<_> = seed + .register_overlays + .iter() + .map(|overlay| { + ( + canonical_replay_register_name(&overlay.name), + overlay.symbol.trim().to_string(), + ) + }) + .collect(); + register_overlays.sort_unstable(); + replay_fingerprint_u64(&mut state, 0x0e, register_overlays.len() as u64); + for (name, symbol) in register_overlays { + replay_fingerprint_str(&mut state, 0x0f, &name); + replay_fingerprint_str(&mut state, 0x10, &symbol); + } + + let mut memory_overlays: Vec<_> = seed + .memory_overlays + .iter() + .map(|overlay| (overlay.addr, overlay.size, overlay.name.trim().to_string())) + .collect(); + memory_overlays.sort_unstable(); + replay_fingerprint_u64(&mut state, 0x11, memory_overlays.len() as u64); + for (addr, size, name) in memory_overlays { + replay_fingerprint_u64(&mut state, 0x12, addr); + replay_fingerprint_u64(&mut state, 0x13, u64::from(size)); + replay_fingerprint_str(&mut state, 0x14, &name); + } + + let mut tty_fds = seed.tty_fds.clone(); + tty_fds.sort_unstable(); + replay_fingerprint_u64(&mut state, 0x15, tty_fds.len() as u64); + for fd in tty_fds { + replay_fingerprint_u64(&mut state, 0x16, fd as u64); + } + replay_fingerprint_bool(&mut state, 0x17, seed.skip_sleep_calls); + state +} + pub fn seed_replay_state_for_arch<'ctx>( state: &mut SymState<'ctx>, prepared: Option<&SsaArtifact>, @@ -500,4 +643,108 @@ mod tests { assert!(state.is_tty_fd(0)); assert!(state.skip_sleep_calls()); } + + #[test] + fn stable_replay_seed_fingerprint_is_order_and_case_stable() { + let seed_a = ReplaySeed { + checkpoint_id: Some(7), + entry_pc: Some(0x4141), + registers: vec![ + ReplayRegisterValue { + name: "rax".to_string(), + value: 0x1122, + }, + ReplayRegisterValue { + name: "rdi".to_string(), + value: 0x3344, + }, + ], + memory: vec![ + ReplayMemoryWindow { + addr: 0x5000, + bytes: vec![0x41, 0x42], + label: Some("input".to_string()), + }, + ReplayMemoryWindow { + addr: 0x6000, + bytes: vec![0x51, 0x52], + label: None, + }, + ], + register_overlays: vec![ReplayRegisterOverlay { + name: "rbx".to_string(), + symbol: "replay_rbx".to_string(), + }], + memory_overlays: vec![ReplayMemoryOverlay { + addr: 0x5001, + size: 2, + name: "user_buf".to_string(), + }], + tty_fds: vec![1, 0], + skip_sleep_calls: true, + }; + let seed_b = ReplaySeed { + checkpoint_id: Some(7), + entry_pc: Some(0x4141), + registers: vec![ + ReplayRegisterValue { + name: "RDI".to_string(), + value: 0x3344, + }, + ReplayRegisterValue { + name: "RAX".to_string(), + value: 0x1122, + }, + ], + memory: vec![ + ReplayMemoryWindow { + addr: 0x6000, + bytes: vec![0x51, 0x52], + label: Some(String::new()), + }, + ReplayMemoryWindow { + addr: 0x5000, + bytes: vec![0x41, 0x42], + label: Some(" input ".to_string()), + }, + ], + register_overlays: vec![ReplayRegisterOverlay { + name: "RBX".to_string(), + symbol: "replay_rbx".to_string(), + }], + memory_overlays: vec![ReplayMemoryOverlay { + addr: 0x5001, + size: 2, + name: "user_buf".to_string(), + }], + tty_fds: vec![0, 1], + skip_sleep_calls: true, + }; + + assert_eq!( + stable_replay_seed_fingerprint(&seed_a), + stable_replay_seed_fingerprint(&seed_b) + ); + } + + #[test] + fn stable_replay_seed_fingerprint_changes_with_seed_content() { + let base = ReplaySeed { + entry_pc: Some(0x4141), + registers: vec![ReplayRegisterValue { + name: "rax".to_string(), + value: 0x1122, + }], + ..ReplaySeed::default() + }; + let different = ReplaySeed { + entry_pc: Some(0x4142), + ..base.clone() + }; + + assert_ne!( + stable_replay_seed_fingerprint(&base), + stable_replay_seed_fingerprint(&different) + ); + } } diff --git a/crates/r2sym/src/runtime.rs b/crates/r2sym/src/runtime.rs index 5b463d0..fa1010f 100644 --- a/crates/r2sym/src/runtime.rs +++ b/crates/r2sym/src/runtime.rs @@ -1,10 +1,16 @@ -use std::collections::HashSet; +use std::collections::{BTreeMap, HashMap, HashSet}; +use std::fs::OpenOptions; +use std::io::Write; use r2il::ArchSpec; use r2ssa::{ObjectKind, SSAVar, SsaArtifact}; +use crate::PathExplorer; +use crate::executor::{CallHookResult, CallHookTag}; use crate::memory::MemoryRegionKind; -use crate::state::SymState; +use crate::sim::{CallConv, PreparedFunctionScope}; +use crate::state::{ExitStatus, SymState}; +use crate::value::SymValue; struct ArchSeedProfile { arg_regs: &'static [&'static str], @@ -53,6 +59,263 @@ fn arch_seed_profile(arch: &ArchSpec) -> Option { } } +#[derive(Clone, Copy)] +struct MainArgRegisterProfile { + argc: &'static [&'static str], + argv: &'static [&'static str], + envp: &'static [&'static str], +} + +fn collect_registers(prepared: &SsaArtifact, only_version_zero: bool) -> BTreeMap { + let mut registers = BTreeMap::new(); + let mut record = |var: &SSAVar| { + if !var.is_register() || (only_version_zero && var.version != 0) { + return; + } + registers + .entry(var.display_name()) + .or_insert(var.size.saturating_mul(8)); + }; + for block in prepared.blocks() { + block.for_each_def(|def| record(def.var)); + block.for_each_source(|src| record(src.var)); + } + registers +} + +fn collect_version_zero_registers(prepared: &SsaArtifact) -> BTreeMap { + collect_registers(prepared, true) +} + +fn normalized_main_like_name(name: &str) -> String { + name.rsplit('.') + .next() + .unwrap_or(name) + .trim_start_matches('_') + .to_ascii_lowercase() +} + +fn debug_main_seed_enabled() -> bool { + std::env::var_os("R2SLEIGH_DEBUG_MAIN_SEED").is_some() +} + +fn debug_main_seed_log(message: &str) { + if !debug_main_seed_enabled() { + return; + } + let path = std::env::var("R2SLEIGH_DEBUG_MAIN_SEED_LOG") + .unwrap_or_else(|_| "/tmp/r2sleigh_main_seed.log".to_string()); + if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) { + let _ = writeln!(file, "{message}"); + } +} + +fn root_name_looks_like_main(scope: &PreparedFunctionScope, prepared: &SsaArtifact) -> bool { + let name = scope + .root() + .and_then(|function| function.name.as_deref()) + .or(prepared.function().name.as_deref()); + let Some(name) = name else { + return false; + }; + let normalized = normalized_main_like_name(name); + matches!(normalized.as_str(), "main" | "wmain") +} + +fn detect_main_arg_register_profile( + prepared: &SsaArtifact, + arch: Option<&ArchSpec>, +) -> Option { + let registers = collect_version_zero_registers(prepared); + let arch_looks_x64 = arch.is_some_and(|arch| { + let name = arch.name.to_ascii_lowercase(); + (name.contains("x86") || name == "x64" || name == "amd64") + && (arch.addr_size == 8 || name.contains("64")) + }); + let has_windows = registers.keys().any(|name| { + name.starts_with("RCX_0") + || name.starts_with("ECX_0") + || name.starts_with("RDX_0") + || name.starts_with("EDX_0") + }); + let has_sysv = registers.keys().any(|name| { + name.starts_with("RDI_0") + || name.starts_with("EDI_0") + || name.starts_with("RSI_0") + || name.starts_with("ESI_0") + }); + + if !(arch_looks_x64 || has_windows || has_sysv) { + return None; + } + + if has_windows || !has_sysv { + Some(MainArgRegisterProfile { + argc: &["RCX", "ECX"], + argv: &["RDX", "EDX"], + envp: &["R8", "R8D"], + }) + } else { + Some(MainArgRegisterProfile { + argc: &["RDI", "EDI"], + argv: &["RSI", "ESI"], + envp: &["RDX", "EDX"], + }) + } +} + +fn infer_main_pointer_bits( + prepared: &SsaArtifact, + arch: Option<&ArchSpec>, + profile: MainArgRegisterProfile, +) -> u32 { + let registers = collect_registers(prepared, false); + let from_registers = registers + .iter() + .filter_map(|(reg_name, bits)| { + let (prefix, _) = reg_name.rsplit_once('_')?; + (profile + .argc + .iter() + .chain(profile.argv.iter()) + .chain(profile.envp.iter()) + .any(|candidate| prefix.eq_ignore_ascii_case(candidate))) + .then_some(*bits) + }) + .max(); + if let Some(bits) = from_registers { + return bits; + } + + arch.map(|spec| spec.addr_size.saturating_mul(8)) + .filter(|bits| *bits > 0) + .or_else(|| { + registers + .into_iter() + .filter_map(|(reg_name, bits)| { + let (prefix, _) = reg_name.rsplit_once('_')?; + (profile + .argc + .iter() + .chain(profile.argv.iter()) + .chain(profile.envp.iter()) + .any(|candidate| prefix.eq_ignore_ascii_case(candidate))) + .then_some(bits) + }) + .max() + }) + .unwrap_or(64) +} + +fn set_register_family_concrete<'ctx>( + state: &mut SymState<'ctx>, + prepared: &SsaArtifact, + family: &[&str], + value: u64, +) { + let registers = collect_version_zero_registers(prepared); + for (reg_name, bits) in registers { + let Some((prefix, _)) = reg_name.rsplit_once('_') else { + continue; + }; + if family + .iter() + .any(|candidate| prefix.eq_ignore_ascii_case(candidate)) + { + state.set_concrete(®_name, value, bits); + } + } +} + +fn seed_concrete_bytes<'ctx>(state: &mut SymState<'ctx>, addr: u64, bytes: &[u8]) { + for (offset, byte) in bytes.iter().copied().enumerate() { + let dst = SymValue::concrete(addr + offset as u64, 64); + let value = SymValue::concrete(u64::from(byte), 8); + state.mem_write(&dst, &value, 1); + } +} + +fn seed_process_like_main_arguments<'ctx>( + state: &mut SymState<'ctx>, + prepared: &SsaArtifact, + scope: &PreparedFunctionScope, + arch: Option<&ArchSpec>, +) { + let is_main = root_name_looks_like_main(scope, prepared); + debug_main_seed_log(&format!( + "root_name={:?} prepared_name={:?} is_main={}", + scope.root().and_then(|function| function.name.as_deref()), + prepared.function().name.as_deref(), + is_main + )); + if !is_main { + return; + } + let Some(profile) = detect_main_arg_register_profile(prepared, arch) else { + debug_main_seed_log("no compatible arg-register profile"); + return; + }; + debug_main_seed_log(&format!( + "profile argc={:?} argv={:?} envp={:?}", + profile.argc, profile.argv, profile.envp + )); + + let (_, argv0_addr) = state.allocate_heap_region("main_argv0", 0x10); + seed_concrete_bytes(state, argv0_addr, b"prog\0"); + + let (_, argv1_addr) = state.allocate_heap_region("main_argv1", MAIN_ARGV1_SYMBOLIC_BYTES + 1); + for index in 0..MAIN_ARGV1_SYMBOLIC_BYTES { + let byte = state.new_symbolic_input(&format!("argv1_byte_{index}"), 8); + state.constrain_ne(&byte, 0); + let dst = SymValue::concrete(argv1_addr + index, 64); + state.mem_write(&dst, &byte, 1); + } + seed_concrete_bytes(state, argv1_addr + MAIN_ARGV1_SYMBOLIC_BYTES, &[0]); + + let (_, argv_table_addr) = state.allocate_heap_region("main_argv", 0x20); + let ptr_bits = infer_main_pointer_bits(prepared, arch, profile); + let ptr_size = (ptr_bits / 8) as usize; + for (index, value) in [argv0_addr, argv1_addr, 0].into_iter().enumerate() { + let dst = SymValue::concrete(argv_table_addr + (index * ptr_size) as u64, 64); + let value = SymValue::concrete(value, ptr_bits); + state.mem_write(&dst, &value, ptr_size as u32); + } + let argv0_slot = state + .mem_read(&SymValue::concrete(argv_table_addr, 64), ptr_size as u32) + .as_concrete() + .unwrap_or_default(); + let argv1_slot = state + .mem_read( + &SymValue::concrete(argv_table_addr + ptr_size as u64, 64), + ptr_size as u32, + ) + .as_concrete() + .unwrap_or_default(); + debug_main_seed_log(&format!( + "ptr_bits={ptr_bits} ptr_size={ptr_size} argv0_addr={argv0_addr:#x} argv1_addr={argv1_addr:#x} argv_table_addr={argv_table_addr:#x} argv_slots=[{argv0_slot:#x}, {argv1_slot:#x}]" + )); + + set_register_family_concrete(state, prepared, profile.argc, 2); + set_register_family_concrete(state, prepared, profile.argv, argv_table_addr); + set_register_family_concrete(state, prepared, profile.envp, 0); + let mut seeded = state + .registers() + .iter() + .filter_map(|(name, value)| { + let (prefix, _) = name.rsplit_once('_')?; + (profile + .argc + .iter() + .chain(profile.argv.iter()) + .chain(profile.envp.iter()) + .any(|candidate| prefix.eq_ignore_ascii_case(candidate))) + .then(|| format!("{name}={:#x}", value.as_concrete().unwrap_or_default())) + }) + .collect::>(); + seeded.sort(); + debug_main_seed_log(&format!("seeded registers {:?}", seeded)); +} + pub fn seed_default_state_for_arch<'ctx>( state: &mut SymState<'ctx>, prepared: &SsaArtifact, @@ -95,9 +358,20 @@ pub fn seed_default_state_for_arch<'ctx>( seed_memory_regions_for_arch(state, prepared, arch); } +pub fn seed_scope_state_for_arch<'ctx>( + state: &mut SymState<'ctx>, + prepared: &SsaArtifact, + scope: &PreparedFunctionScope, + arch: Option<&ArchSpec>, +) { + seed_default_state_for_arch(state, prepared, arch); + seed_process_like_main_arguments(state, prepared, scope, arch); +} + const STACK_WINDOW_BELOW: u64 = 0x8000; const STACK_WINDOW_SIZE: u64 = 0x10000; const GLOBAL_TAIL_EXTENT: u64 = 0x1000; +const MAIN_ARGV1_SYMBOLIC_BYTES: u64 = 0x40; pub fn seed_memory_regions_for_arch<'ctx>( state: &mut SymState<'ctx>, @@ -150,3 +424,409 @@ pub fn seed_memory_regions_for_arch<'ctx>( ); } } + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum WindowsRuntimeHook { + AddVectoredExceptionHandler, + RaiseException, + VirtualAlloc, + VirtualProtect, + HeapAlloc, +} + +impl WindowsRuntimeHook { + fn call_hook_tag(self) -> CallHookTag { + match self { + WindowsRuntimeHook::AddVectoredExceptionHandler => { + CallHookTag::WindowsAddVectoredExceptionHandler + } + WindowsRuntimeHook::RaiseException => CallHookTag::WindowsRaiseException, + WindowsRuntimeHook::VirtualAlloc => CallHookTag::WindowsVirtualAlloc, + WindowsRuntimeHook::VirtualProtect => CallHookTag::WindowsVirtualProtect, + WindowsRuntimeHook::HeapAlloc => CallHookTag::WindowsHeapAlloc, + } + } +} + +fn arch_supports_windows_runtime(arch: &ArchSpec) -> bool { + let name = arch.name.to_ascii_lowercase(); + (name.contains("x86") || name == "x64" || name == "amd64") + && (arch.addr_size == 8 || name.contains("64")) +} + +fn windows_x64_callconv() -> CallConv { + CallConv::new(vec!["RCX", "RDX", "R8", "R9"], "RAX", 64, 64) +} + +fn normalize_windows_runtime_hook(name: &str) -> Option { + let lower = name.to_ascii_lowercase(); + let normalized = lower.trim(); + if normalized.ends_with("addvectoredexceptionhandler") { + Some(WindowsRuntimeHook::AddVectoredExceptionHandler) + } else if normalized.ends_with("raiseexception") { + Some(WindowsRuntimeHook::RaiseException) + } else if normalized.ends_with("virtualalloc") { + Some(WindowsRuntimeHook::VirtualAlloc) + } else if normalized.ends_with("virtualprotect") { + Some(WindowsRuntimeHook::VirtualProtect) + } else if normalized.ends_with("heapalloc") { + Some(WindowsRuntimeHook::HeapAlloc) + } else { + None + } +} + +fn page_protection_is_executable(value: u64) -> bool { + matches!(value, 0x10 | 0x20 | 0x40 | 0x80) +} + +fn seed_exception_continuation<'ctx>( + state: &mut SymState<'ctx>, + exception_code: u64, + handler_addr: u64, +) -> CallHookResult { + state.seed_exception_continuation(exception_code, handler_addr); + CallHookResult::Jump(handler_addr) +} + +fn apply_windows_runtime_hook<'ctx>( + state: &mut SymState<'ctx>, + hook: WindowsRuntimeHook, +) -> CallHookResult { + let callconv = windows_x64_callconv(); + let call = callconv.collect_call_info(state, 4); + match hook { + WindowsRuntimeHook::AddVectoredExceptionHandler => { + let handler = call.args.get(1).and_then(SymValue::as_concrete); + if let Some(handler) = handler { + state.register_exception_handler(handler); + callconv.write_return(state, SymValue::concrete(handler, callconv.ret_bits())); + CallHookResult::Fallthrough + } else { + CallHookResult::Terminate(ExitStatus::RuntimeBlocked( + crate::state::RuntimeBlockReason::MissingContinuationSeed, + )) + } + } + WindowsRuntimeHook::RaiseException => { + let Some(exception_code) = call.args.first().and_then(SymValue::as_concrete) else { + return CallHookResult::Terminate(ExitStatus::RuntimeBlocked( + crate::state::RuntimeBlockReason::MissingContinuationSeed, + )); + }; + let Some(handler_addr) = state.primary_exception_handler() else { + return CallHookResult::Terminate(ExitStatus::RuntimeBlocked( + crate::state::RuntimeBlockReason::MissingExceptionHandler, + )); + }; + seed_exception_continuation(state, exception_code, handler_addr) + } + WindowsRuntimeHook::VirtualAlloc => { + let size = call + .args + .get(1) + .and_then(SymValue::as_concrete) + .unwrap_or(0x1000) + .max(1); + let executable = call + .args + .get(3) + .and_then(SymValue::as_concrete) + .is_some_and(page_protection_is_executable); + let (_region_id, base_addr) = + state.allocate_heap_region(&format!("virtualalloc_{:x}", state.pc), size); + state.register_runtime_region_alias(base_addr, size, executable); + callconv.write_return(state, SymValue::concrete(base_addr, callconv.ret_bits())); + CallHookResult::Fallthrough + } + WindowsRuntimeHook::VirtualProtect => { + let addr = call + .args + .first() + .and_then(SymValue::as_concrete) + .unwrap_or(0); + let size = call + .args + .get(1) + .and_then(SymValue::as_concrete) + .unwrap_or(0) + .max(1); + let executable = call + .args + .get(2) + .and_then(SymValue::as_concrete) + .is_some_and(page_protection_is_executable); + if executable { + state.mark_runtime_region_executable(addr, size); + } + callconv.write_return(state, SymValue::concrete(1, callconv.ret_bits())); + CallHookResult::Fallthrough + } + WindowsRuntimeHook::HeapAlloc => { + let size = call + .args + .get(2) + .and_then(SymValue::as_concrete) + .unwrap_or(0x100) + .max(1); + let (_region_id, base_addr) = + state.allocate_heap_region(&format!("heapalloc_{:x}", state.pc), size); + callconv.write_return(state, SymValue::concrete(base_addr, callconv.ret_bits())); + CallHookResult::Fallthrough + } + } +} + +pub fn install_runtime_hooks_for_scope<'ctx>( + explorer: &mut PathExplorer<'ctx>, + scope: &PreparedFunctionScope, + arch: Option<&ArchSpec>, + symbol_map: &HashMap, +) { + let Some(arch) = arch else { + return; + }; + if !arch_supports_windows_runtime(arch) { + return; + } + + let mut targets = BTreeMap::new(); + for function in scope.functions().values() { + for call in function.prepared.call_sites().by_id.values() { + if let Some(target) = function.prepared.resolved_call_target(call) + && let Some(raw_name) = symbol_map.get(&target) + && let Some(kind) = normalize_windows_runtime_hook(raw_name) + { + targets.entry(target).or_insert(kind); + } + } + } + + for (&target, raw_name) in symbol_map { + let Some(kind) = normalize_windows_runtime_hook(raw_name) else { + continue; + }; + targets.entry(target).or_insert(kind); + } + + for (target, kind) in targets { + explorer.register_tagged_call_hook(target, kind.call_hook_tag(), move |state| { + apply_windows_runtime_hook(state, kind) + }); + } +} + +#[cfg(test)] +mod tests { + use super::{ + WindowsRuntimeHook, apply_windows_runtime_hook, install_runtime_hooks_for_scope, + seed_scope_state_for_arch, + }; + use crate::executor::CallHookResult; + use crate::path::PathExplorer; + use crate::sim::{PreparedFunctionScope, ScopedPreparedFunction}; + use crate::state::SymState; + use r2il::{AddressSpace, ArchSpec, R2ILBlock, R2ILOp, RegisterDef, SpaceId, Varnode}; + use r2ssa::{InterprocFunctionId, SsaArtifact}; + use std::collections::HashMap; + use z3::Context; + + fn make_main_seed_arch(addr_size: u32) -> ArchSpec { + let mut arch = ArchSpec::new("x86-64"); + arch.addr_size = addr_size; + arch.add_space(AddressSpace::ram(8)); + arch.add_register(RegisterDef::new("RCX", 0x80, 8)); + arch.add_register(RegisterDef::new("ECX", 0x80, 4)); + arch.add_register(RegisterDef::new("RDX", 0x88, 8)); + arch.add_register(RegisterDef::new("EDX", 0x88, 4)); + arch.add_register(RegisterDef::new("R8", 0x90, 8)); + arch.add_register(RegisterDef::new("R8D", 0x90, 4)); + arch + } + + fn make_main_seed_scope(arch: &ArchSpec) -> (SsaArtifact, PreparedFunctionScope) { + let mut block = R2ILBlock::new(0x1000, 1); + block.push(R2ILOp::Copy { + dst: Varnode::unique(0x10, 8), + src: Varnode::register(0x80, 8), + }); + block.push(R2ILOp::Copy { + dst: Varnode::unique(0x20, 8), + src: Varnode::register(0x88, 8), + }); + block.push(R2ILOp::Copy { + dst: Varnode::unique(0x30, 8), + src: Varnode::register(0x90, 8), + }); + block.push(R2ILOp::Return { + target: Varnode::constant(0, 8), + }); + let prepared = SsaArtifact::for_symbolic(&[block], Some(arch)) + .expect("ssa") + .with_name("main"); + let scope = PreparedFunctionScope::new( + 0x1000, + vec![ScopedPreparedFunction { + id: InterprocFunctionId(0x1000), + name: Some("main".to_string()), + prepared: prepared.clone(), + }], + ) + .expect("scope"); + (prepared, scope) + } + + #[test] + fn test_virtualalloc_hook_writes_current_return_alias() { + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1400_1000); + state.set_concrete("RCX_4", 0, 64); + state.set_concrete("RDX_4", 0x1000, 64); + state.set_concrete("R8_4", 0x3000, 64); + state.set_concrete("R9_4", 0x40, 64); + state.set_concrete("RAX_7", 0xdead_beef, 64); + + let result = apply_windows_runtime_hook(&mut state, WindowsRuntimeHook::VirtualAlloc); + assert_eq!(result, CallHookResult::Fallthrough); + + let base_addr = state + .get_register("RAX_7") + .as_concrete() + .expect("virtualalloc return should be concrete"); + assert_ne!(base_addr, 0xdead_beef); + let region = state + .runtime_region_for_pc(base_addr) + .expect("allocated runtime region should be registered"); + assert!(region.executable); + assert_eq!(region.size, 0x1000); + } + + #[test] + fn main_seed_uses_register_width_for_argv_table_layout() { + let ctx = Context::thread_local(); + let arch = make_main_seed_arch(4); + let (prepared, scope) = make_main_seed_scope(&arch); + let mut state = SymState::new(&ctx, 0x1000); + + seed_scope_state_for_arch(&mut state, &prepared, &scope, Some(&arch)); + + let argv_addr = state + .get_register("RDX_0") + .as_concrete() + .expect("argv register should be concrete"); + let argv0_addr = state + .mem_read(&crate::SymValue::concrete(argv_addr, 64), 8) + .as_concrete() + .expect("argv[0] should be present"); + let argv1_addr = state + .mem_read(&crate::SymValue::concrete(argv_addr + 8, 64), 8) + .as_concrete() + .expect("argv[1] should be written at pointer-width stride"); + + assert_ne!(argv0_addr, 0); + assert_ne!(argv1_addr, 0); + assert_ne!(argv0_addr, argv1_addr); + assert_eq!( + state + .mem_read( + &crate::SymValue::concrete(argv1_addr + super::MAIN_ARGV1_SYMBOLIC_BYTES, 64,), + 1, + ) + .as_concrete(), + Some(0), + "argv[1] should remain NUL-terminated", + ); + } + + #[test] + fn main_seed_only_initializes_version_zero_argument_registers() { + let ctx = Context::thread_local(); + let arch = make_main_seed_arch(8); + let (prepared, scope) = make_main_seed_scope(&arch); + let mut state = SymState::new(&ctx, 0x1000); + + seed_scope_state_for_arch(&mut state, &prepared, &scope, Some(&arch)); + + let argv_entry = state + .get_register("RDX_0") + .as_concrete() + .expect("argv register should seed the entry SSA version"); + assert_ne!(argv_entry, 0); + assert!( + state.get_register("RDX_1").as_concrete().is_none(), + "later SSA versions must be left for execution to define" + ); + assert!( + state.get_register("RCX_1").as_concrete().is_none(), + "later argc versions must not be pre-seeded either" + ); + } + + #[test] + fn install_runtime_hooks_uses_symbol_map_imports_even_without_scope_calls() { + let ctx = Context::thread_local(); + let arch = make_main_seed_arch(8); + let (_prepared, scope) = make_main_seed_scope(&arch); + let blocks = vec![ + R2ILBlock { + addr: 0x2000, + size: 4, + switch_info: None, + op_metadata: Default::default(), + ops: vec![ + R2ILOp::Call { + target: Varnode { + space: SpaceId::Ram, + offset: 0x401000, + size: 8, + meta: None, + }, + }, + R2ILOp::IntNotEqual { + dst: Varnode::unique(0x20, 1), + a: Varnode::register(0x100, 8), + b: Varnode::constant(0, 8), + }, + R2ILOp::CBranch { + target: Varnode::constant(0x2010, 8), + cond: Varnode::unique(0x20, 1), + }, + ], + }, + R2ILBlock { + addr: 0x2004, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: Varnode::constant(0, 8), + }], + }, + R2ILBlock { + addr: 0x2010, + size: 1, + switch_info: None, + op_metadata: Default::default(), + ops: vec![R2ILOp::Return { + target: Varnode::constant(0, 8), + }], + }, + ]; + let func = SsaArtifact::for_symbolic(&blocks, Some(&arch)).expect("ssa"); + let mut explorer = PathExplorer::new(&ctx); + let symbol_map = + HashMap::from([(0x401000, "sym.imp.KERNEL32.dll_VirtualAlloc".to_string())]); + install_runtime_hooks_for_scope(&mut explorer, &scope, Some(&arch), &symbol_map); + + let mut state = SymState::new(&ctx, 0x2000); + state.set_concrete("RDX_0", 0x1000, 64); + state.set_concrete("R9_0", 0x40, 64); + state.set_concrete("RAX_0", 0, 64); + + let paths = explorer.find_paths_to(&func, state, 0x2010); + assert!( + !paths.is_empty(), + "symbol-map runtime imports should install hooks even when scope call discovery is absent" + ); + } +} diff --git a/crates/r2sym/src/semantics/artifact.rs b/crates/r2sym/src/semantics/artifact.rs index 11c447b..34c6b87 100644 --- a/crates/r2sym/src/semantics/artifact.rs +++ b/crates/r2sym/src/semantics/artifact.rs @@ -1,9 +1,9 @@ use serde::{Deserialize, Serialize}; use super::plan::{ - ArtifactBuildPlan, DecompilePlan, QueryPlan, TargetQueryPlan, TargetQueryRoutePlan, TypePlan, - derive_artifact_build_plan, derive_decompile_plan, derive_query_plan, derive_target_query_plan, - derive_target_query_route_plan, derive_type_plan, + ArtifactBuildPlan, DecompilePlan, QueryPlan, TargetQueryExecutionRoute, TargetQueryPlan, + TargetQueryRoutePlan, TypePlan, derive_artifact_build_plan, derive_decompile_plan, + derive_query_plan, derive_target_query_plan, derive_target_query_route_plan, derive_type_plan, }; use super::region::{ ArtifactGranularity, ExecutionModel, NativeArtifactBody, RefinementStage, @@ -11,6 +11,14 @@ use super::region::{ }; use super::vm::InterpreterKind; +#[derive(Debug, Clone)] +pub(crate) struct TargetQueryRouteInput<'a> { + pub(crate) route: TargetQueryRoutePlan, + pub(crate) condition_source: Option>, + pub(crate) memory_terms: Vec<&'a crate::backward::BackwardMemoryCondition>, + pub(crate) allow_exact_proof: bool, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum SliceClass { Wrapper, @@ -394,6 +402,22 @@ impl SemanticArtifact { self } + fn downgrade_query_route_to_residual( + route: TargetQueryRoutePlan, + reason: impl Into, + ) -> TargetQueryRoutePlan { + let mut reasons = match route.execution { + TargetQueryExecutionRoute::ResidualOnly { reasons } => reasons, + TargetQueryExecutionRoute::Refuse { reason } => vec![reason], + _ => Vec::new(), + }; + reasons.push(reason.into()); + TargetQueryRoutePlan { + target_plan: route.target_plan, + execution: TargetQueryExecutionRoute::ResidualOnly { reasons }, + } + } + fn has_native_semantics(&self) -> bool { self.native_body() .is_some_and(|body| !body.regions.is_empty()) @@ -462,7 +486,7 @@ impl SemanticArtifact { pub fn target_query_route_plan(&self, target_addr: u64) -> TargetQueryRoutePlan { let query_plan = self.query_plan(); let target_plan = self.target_query_plan(target_addr); - let authoritative_region = self.authoritative_region_for_target(target_addr, false); + let condition_source = self.target_condition_source(target_addr, false); let authoritative_memory_region = self.authoritative_memory_region_for_target(target_addr); let has_memory_guidance = authoritative_memory_region.is_some_and(|region| { !region @@ -472,11 +496,42 @@ impl SemanticArtifact { derive_target_query_route_plan( &query_plan, &target_plan, - authoritative_region.is_some() || authoritative_memory_region.is_some(), + self.execution, + condition_source.is_some(), has_memory_guidance, ) } + pub(crate) fn target_query_route_input<'a>( + &'a self, + target_addr: u64, + assumption_conflicted: bool, + ) -> TargetQueryRouteInput<'a> { + let mut route = self.target_query_route_plan(target_addr); + let canonical_source = self.target_condition_source(target_addr, false); + let memory_terms = self + .authoritative_memory_region_for_target(target_addr) + .map(|region| region.actionable_memory_terms_for_target(target_addr)) + .unwrap_or_default(); + if assumption_conflicted { + route = Self::downgrade_query_route_to_residual(route, "assumption conflict"); + } + if self.target_has_ambiguous_sources(target_addr) { + route = Self::downgrade_query_route_to_residual( + route, + "conflicting target guidance sources", + ); + } + let allow_exact_proof = + !assumption_conflicted && !self.target_has_ambiguous_sources(target_addr); + TargetQueryRouteInput { + route, + condition_source: canonical_source, + memory_terms, + allow_exact_proof, + } + } + pub fn type_plan(&self) -> TypePlan { derive_type_plan( self.stage, diff --git a/crates/r2sym/src/semantics/cache.rs b/crates/r2sym/src/semantics/cache.rs index f7ff3e9..078097f 100644 --- a/crates/r2sym/src/semantics/cache.rs +++ b/crates/r2sym/src/semantics/cache.rs @@ -15,7 +15,7 @@ pub const SEMANTIC_ARTIFACT_SCHEMA_VERSION: u32 = 2; #[allow(dead_code)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub(crate) enum SemanticSeedMode { +pub enum SemanticSeedMode { Static, Replay, } @@ -34,13 +34,16 @@ pub(crate) struct SemanticCacheKey { pub arch_hash: u64, pub summary_profile: SummaryProfile, pub seed_mode: SemanticSeedMode, + pub replay_seed_fingerprint: u64, pub scope_kind: SemanticCacheScopeKind, } #[derive(Debug, Clone)] -pub(crate) struct SemanticCompilationResult { +pub struct SemanticCompilationResult { pub artifact: Arc, pub cache_hit: bool, + pub seed_mode: SemanticSeedMode, + pub replay_seed_fingerprint: u64, } fn hash_debug_value(value: &T) -> u64 { @@ -143,6 +146,7 @@ pub(crate) fn semantic_cache_key( arch: Option<&ArchSpec>, summary_profile: SummaryProfile, seed_mode: SemanticSeedMode, + replay_seed_fingerprint: u64, ) -> SemanticCacheKey { SemanticCacheKey { schema_version: SEMANTIC_ARTIFACT_SCHEMA_VERSION, @@ -155,23 +159,27 @@ pub(crate) fn semantic_cache_key( arch_hash: arch_hash(arch), summary_profile, seed_mode, + replay_seed_fingerprint, scope_kind: SemanticCacheScopeKind::Exact, } } pub(crate) fn coarse_large_slice_cache_key( root_addr: u64, + scope: &PreparedFunctionScope, arch: Option<&ArchSpec>, summary_profile: SummaryProfile, seed_mode: SemanticSeedMode, + replay_seed_fingerprint: u64, ) -> SemanticCacheKey { SemanticCacheKey { schema_version: SEMANTIC_ARTIFACT_SCHEMA_VERSION, root_addr, - scope_hash: 0, + scope_hash: stable_scope_hash(Some(scope)), arch_hash: arch_hash(arch), summary_profile, seed_mode, + replay_seed_fingerprint, scope_kind: SemanticCacheScopeKind::CoarseLargeSlice, } } @@ -255,15 +263,27 @@ mod display_name_tests { Some(&test_arch()), crate::sim::SummaryProfile::Default, SemanticSeedMode::Static, + 0, ); let coarse_key = coarse_large_slice_cache_key( func.entry, + &PreparedFunctionScope::new( + 0x401000, + vec![ScopedPreparedFunction { + id: InterprocFunctionId(0x401000), + name: Some("sym.main".to_string()), + prepared: func.clone(), + }], + ) + .expect("scope"), Some(&test_arch()), crate::sim::SummaryProfile::Default, SemanticSeedMode::Static, + 0, ); assert_eq!(exact_key.schema_version, SEMANTIC_ARTIFACT_SCHEMA_VERSION); assert_eq!(coarse_key.schema_version, SEMANTIC_ARTIFACT_SCHEMA_VERSION); + assert_eq!(exact_key.replay_seed_fingerprint, 0); assert!(matches!( exact_key.scope_kind, SemanticCacheScopeKind::Exact @@ -274,6 +294,51 @@ mod display_name_tests { )); } + #[test] + fn semantic_cache_keys_partition_replay_seed_identity() { + let func = SsaArtifact::for_symbolic( + &[R2ILBlock { + addr: 0x401000, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }], + Some(&test_arch()), + ) + .expect("ssa"); + let static_key = semantic_cache_key( + &func, + None, + Some(&test_arch()), + crate::sim::SummaryProfile::Default, + SemanticSeedMode::Static, + 0, + ); + let replay_key_a = semantic_cache_key( + &func, + None, + Some(&test_arch()), + crate::sim::SummaryProfile::Default, + SemanticSeedMode::Replay, + 0x11, + ); + let replay_key_b = semantic_cache_key( + &func, + None, + Some(&test_arch()), + crate::sim::SummaryProfile::Default, + SemanticSeedMode::Replay, + 0x22, + ); + + assert_eq!(static_key.seed_mode, SemanticSeedMode::Static); + assert_eq!(static_key.replay_seed_fingerprint, 0); + assert_ne!(replay_key_a, replay_key_b); + } + fn simple_function(entry: u64) -> SsaArtifact { let blocks = vec![R2ILBlock { addr: entry, diff --git a/crates/r2sym/src/semantics/compiler.rs b/crates/r2sym/src/semantics/compiler.rs index 43e88ff..42786ae 100644 --- a/crates/r2sym/src/semantics/compiler.rs +++ b/crates/r2sym/src/semantics/compiler.rs @@ -5,6 +5,7 @@ use r2il::ArchSpec; use r2ssa::{InterprocFunctionId, SsaArtifact}; use z3::Context; +use crate::replay::{ReplaySeed, stable_replay_seed_fingerprint}; use crate::sim::{ DerivedSummaryDiagnostics, PreparedFunctionScope, SummaryProfile, SummaryRegistry, }; @@ -263,6 +264,25 @@ fn bounded_large_cfg_scope( PreparedFunctionScope::new(scope.root_id().0, functions) } +fn bounded_query_scope( + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + target_addr: u64, +) -> Option { + let scope = scope?; + let target_is_cross_function = scope + .function_containing_block(target_addr) + .is_some_and(|function| function.id != scope.root_id()); + let helper_limit = bounded_large_cfg_helper_limit(func); + let has_many_helpers = scope.helper_functions().count() > helper_limit; + + if !(target_is_cross_function || has_many_helpers) { + return None; + } + + bounded_large_cfg_scope(func, Some(scope)) +} + fn compile_function_semantics_uncached( ctx: &Context, func: &SsaArtifact, @@ -353,7 +373,11 @@ fn compile_function_semantics_uncached( } if let Some(scope) = scope - && let Some(registry) = SummaryRegistry::with_profile_for_arch(arch, summary_profile) + && let Some(registry) = SummaryRegistry::with_profile_for_arch_and_symbols( + arch, + symbol_map, + summary_profile, + ) { let derived = registry.derive_symbolic_summaries(ctx, scope, Some(arch), symbol_map); derived_summaries = derived.summaries.len(); @@ -427,23 +451,51 @@ fn compile_function_semantics_uncached( }) } -pub(crate) fn compile_function_semantics_cached_with_scope( +fn semantic_seed_identity(replay_seed: Option<&ReplaySeed>) -> (SemanticSeedMode, u64) { + match replay_seed { + Some(seed) => ( + SemanticSeedMode::Replay, + stable_replay_seed_fingerprint(seed), + ), + None => (SemanticSeedMode::Static, 0), + } +} + +fn compile_function_semantics_cached_with_scope_impl( ctx: &Context, func: &SsaArtifact, scope: Option<&PreparedFunctionScope>, arch: Option<&ArchSpec>, symbol_map: &HashMap, summary_profile: SummaryProfile, + replay_seed: Option<&ReplaySeed>, ) -> SemanticCompilationResult { - let key = semantic_cache_key(func, scope, arch, summary_profile, SemanticSeedMode::Static); + let (seed_mode, replay_seed_fingerprint) = semantic_seed_identity(replay_seed); + let key = semantic_cache_key( + func, + scope, + arch, + summary_profile, + seed_mode, + replay_seed_fingerprint, + ); if let Some(existing) = lookup_semantic_cache(&key) { return SemanticCompilationResult { artifact: existing, cache_hit: true, + seed_mode, + replay_seed_fingerprint, }; } - let coarse_key = scope.map(|_| { - coarse_large_slice_cache_key(func.entry, arch, summary_profile, SemanticSeedMode::Static) + let coarse_key = scope.map(|scope| { + coarse_large_slice_cache_key( + func.entry, + scope, + arch, + summary_profile, + seed_mode, + replay_seed_fingerprint, + ) }); if let Some(coarse_key) = coarse_key.as_ref() && let Some(existing) = lookup_semantic_cache(coarse_key) @@ -451,6 +503,8 @@ pub(crate) fn compile_function_semantics_cached_with_scope( return SemanticCompilationResult { artifact: existing, cache_hit: true, + seed_mode, + replay_seed_fingerprint, }; } @@ -476,9 +530,30 @@ pub(crate) fn compile_function_semantics_cached_with_scope( SemanticCompilationResult { artifact, cache_hit: false, + seed_mode, + replay_seed_fingerprint, } } +pub(crate) fn compile_function_semantics_cached_with_scope( + ctx: &Context, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + arch: Option<&ArchSpec>, + symbol_map: &HashMap, + summary_profile: SummaryProfile, +) -> SemanticCompilationResult { + compile_function_semantics_cached_with_scope_impl( + ctx, + func, + scope, + arch, + symbol_map, + summary_profile, + None, + ) +} + pub fn compile_function_semantics_with_scope( ctx: &Context, func: &SsaArtifact, @@ -500,6 +575,29 @@ pub fn compile_function_semantics_with_scope( artifact } +pub fn compile_function_semantics_with_scope_and_replay_seed( + ctx: &Context, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + arch: Option<&ArchSpec>, + symbol_map: &HashMap, + summary_profile: SummaryProfile, + replay_seed: Option<&ReplaySeed>, +) -> SemanticArtifact { + let result = compile_function_semantics_cached_with_scope_impl( + ctx, + func, + scope, + arch, + symbol_map, + summary_profile, + replay_seed, + ); + let mut artifact = (*result.artifact).clone(); + artifact.diagnostics.cache_hit = result.cache_hit; + artifact +} + pub fn compile_semantic_artifact_with_scope( ctx: &Context, func: &SsaArtifact, @@ -511,6 +609,69 @@ pub fn compile_semantic_artifact_with_scope( compile_function_semantics_with_scope(ctx, func, scope, arch, symbol_map, summary_profile) } +pub fn compile_semantic_artifact_with_scope_and_replay_seed( + ctx: &Context, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + arch: Option<&ArchSpec>, + symbol_map: &HashMap, + summary_profile: SummaryProfile, + replay_seed: Option<&ReplaySeed>, +) -> SemanticArtifact { + compile_function_semantics_with_scope_and_replay_seed( + ctx, + func, + scope, + arch, + symbol_map, + summary_profile, + replay_seed, + ) +} + +pub fn compile_query_semantic_artifact_with_scope( + ctx: &Context, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + target_addr: u64, + arch: Option<&ArchSpec>, + symbol_map: &HashMap, + summary_profile: SummaryProfile, +) -> SemanticArtifact { + let bounded_scope = bounded_query_scope(func, scope, target_addr); + compile_function_semantics_with_scope( + ctx, + func, + bounded_scope.as_ref().or(scope), + arch, + symbol_map, + summary_profile, + ) +} + +#[allow(clippy::too_many_arguments)] +pub fn compile_query_semantic_artifact_with_scope_and_replay_seed( + ctx: &Context, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + target_addr: u64, + arch: Option<&ArchSpec>, + symbol_map: &HashMap, + summary_profile: SummaryProfile, + replay_seed: Option<&ReplaySeed>, +) -> SemanticArtifact { + let bounded_scope = bounded_query_scope(func, scope, target_addr); + compile_function_semantics_with_scope_and_replay_seed( + ctx, + func, + bounded_scope.as_ref().or(scope), + arch, + symbol_map, + summary_profile, + replay_seed, + ) +} + pub fn compile_semantic_artifact_default_with_scope( ctx: &Context, func: &SsaArtifact, @@ -538,6 +699,8 @@ mod tests { use r2ssa::SsaArtifact; use z3::Context; + use crate::replay::{ReplayRegisterValue, ReplaySeed}; + use super::*; const RAX: u64 = 0; @@ -599,6 +762,79 @@ mod tests { assert!(second.cache_hit); } + #[test] + fn compile_semantics_replay_seeds_partition_cache_identity() { + let blocks = vec![R2ILBlock { + addr: 0x2000, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }]; + let func = SsaArtifact::for_symbolic(&blocks, Some(&test_arch())).expect("ssa"); + let ctx = Context::thread_local(); + let replay_a = ReplaySeed { + checkpoint_id: Some(1), + entry_pc: Some(0x2000), + registers: vec![ReplayRegisterValue { + name: "rax".to_string(), + value: 0x1111, + }], + ..ReplaySeed::default() + }; + let replay_b = ReplaySeed { + checkpoint_id: Some(2), + entry_pc: Some(0x2000), + registers: vec![ReplayRegisterValue { + name: "rax".to_string(), + value: 0x2222, + }], + ..ReplaySeed::default() + }; + + let first = compile_function_semantics_cached_with_scope_impl( + &ctx, + &func, + None, + Some(&test_arch()), + &HashMap::new(), + SummaryProfile::Default, + Some(&replay_a), + ); + let second = compile_function_semantics_cached_with_scope_impl( + &ctx, + &func, + None, + Some(&test_arch()), + &HashMap::new(), + SummaryProfile::Default, + Some(&replay_a), + ); + let third = compile_function_semantics_cached_with_scope_impl( + &ctx, + &func, + None, + Some(&test_arch()), + &HashMap::new(), + SummaryProfile::Default, + Some(&replay_b), + ); + + assert_eq!(first.seed_mode, SemanticSeedMode::Replay); + assert_eq!(second.seed_mode, SemanticSeedMode::Replay); + assert_eq!(third.seed_mode, SemanticSeedMode::Replay); + assert_eq!( + first.replay_seed_fingerprint, + second.replay_seed_fingerprint + ); + assert_ne!(first.replay_seed_fingerprint, third.replay_seed_fingerprint); + assert!(!first.cache_hit); + assert!(second.cache_hit); + assert!(!third.cache_hit); + } + #[test] fn compile_semantics_cache_ignores_scope_display_names() { let blocks = vec![R2ILBlock { @@ -652,6 +888,192 @@ mod tests { assert!(second.cache_hit); } + #[test] + fn large_cfg_cache_does_not_alias_distinct_scopes() { + let mut blocks = vec![R2ILBlock { + addr: 0x3000, + size: 4, + ops: vec![R2ILOp::Copy { + dst: make_reg(RAX, 8), + src: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }]; + let mut addr = 0x3004; + for _ in 0..64 { + blocks.push(R2ILBlock { + addr, + size: 4, + ops: vec![R2ILOp::Branch { + target: make_const(addr + 4, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }); + addr += 4; + } + blocks.push(R2ILBlock { + addr, + size: 4, + ops: vec![R2ILOp::Return { + target: make_reg(RAX, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }); + let root = SsaArtifact::for_symbolic(&blocks, Some(&test_arch())).expect("root"); + let helper = SsaArtifact::for_symbolic( + &[R2ILBlock { + addr: 0x5000, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }], + Some(&test_arch()), + ) + .expect("helper"); + let scope_a = crate::PreparedFunctionScope::new( + 0x3000, + vec![crate::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x3000), + name: Some("sym.root".to_string()), + prepared: root.clone(), + }], + ) + .expect("scope a"); + let scope_b = crate::PreparedFunctionScope::new( + 0x3000, + vec![ + crate::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x3000), + name: Some("sym.root".to_string()), + prepared: root.clone(), + }, + crate::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x5000), + name: Some("sym.helper".to_string()), + prepared: helper, + }, + ], + ) + .expect("scope b"); + let ctx = Context::thread_local(); + + let first = compile_function_semantics_cached_with_scope( + &ctx, + &root, + Some(&scope_a), + Some(&test_arch()), + &HashMap::new(), + SummaryProfile::Default, + ); + let second = compile_function_semantics_cached_with_scope( + &ctx, + &root, + Some(&scope_b), + Some(&test_arch()), + &HashMap::new(), + SummaryProfile::Default, + ); + + assert!(first.artifact.diagnostics.skipped_large_cfg); + assert!(second.artifact.diagnostics.skipped_large_cfg); + assert!(!first.cache_hit); + assert!(!second.cache_hit); + } + + #[test] + fn bounded_query_scope_omits_cross_function_target_helpers() { + let root = SsaArtifact::for_symbolic( + &[R2ILBlock { + addr: 0x1000, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }], + Some(&test_arch()), + ) + .expect("root"); + let helper = SsaArtifact::for_symbolic( + &[R2ILBlock { + addr: 0x2000, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }], + Some(&test_arch()), + ) + .expect("helper"); + let scope = crate::PreparedFunctionScope::new( + 0x1000, + vec![ + crate::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }, + crate::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x2000), + name: Some("helper".to_string()), + prepared: helper, + }, + ], + ) + .expect("scope"); + + let bounded = bounded_query_scope(&root, Some(&scope), 0x2000).expect("bounded scope"); + assert_eq!(bounded.functions().len(), 1); + assert_eq!(bounded.root_id(), r2ssa::InterprocFunctionId(0x1000)); + assert!( + bounded + .functions() + .contains_key(&r2ssa::InterprocFunctionId(0x1000)) + ); + assert!( + !bounded + .functions() + .contains_key(&r2ssa::InterprocFunctionId(0x2000)) + ); + } + + #[test] + fn bounded_query_scope_keeps_same_function_targets_unbounded() { + let root = SsaArtifact::for_symbolic( + &[R2ILBlock { + addr: 0x1000, + size: 1, + ops: vec![R2ILOp::Return { + target: make_const(0, 8), + }], + switch_info: None, + op_metadata: Default::default(), + }], + Some(&test_arch()), + ) + .expect("root"); + let scope = crate::PreparedFunctionScope::new( + 0x1000, + vec![crate::ScopedPreparedFunction { + id: r2ssa::InterprocFunctionId(0x1000), + name: Some("root".to_string()), + prepared: root.clone(), + }], + ) + .expect("scope"); + + assert!(bounded_query_scope(&root, Some(&scope), 0x1000).is_none()); + } + #[test] fn interpreter_classifier_marks_switch_loop_vm_summary() { let blocks = vec![ diff --git a/crates/r2sym/src/semantics/facts.rs b/crates/r2sym/src/semantics/facts.rs index e480fc5..c8b9f7a 100644 --- a/crates/r2sym/src/semantics/facts.rs +++ b/crates/r2sym/src/semantics/facts.rs @@ -609,7 +609,9 @@ fn install_symbolic_fact_hooks<'ctx>( summary_profile: SummaryProfile, symbol_map: &HashMap, ) { - let Some(registry) = SummaryRegistry::with_profile_for_arch(arch, summary_profile) else { + let Some(registry) = + SummaryRegistry::with_profile_for_arch_and_symbols(arch, symbol_map, summary_profile) + else { return; }; if let Some(scope) = scope { @@ -812,7 +814,9 @@ pub(super) fn collect_large_cfg_canonical_semantic_regions_with_limit( ) -> CollectedNativeSemanticRegions { let branch_blocks = limited_branch_blocks(func, branch_limit.max(1)); let mut collected = if let Some(scope) = scope { - if let Some(registry) = SummaryRegistry::with_profile_for_arch(arch, summary_profile) { + if let Some(registry) = + SummaryRegistry::with_profile_for_arch_and_symbols(arch, symbol_map, summary_profile) + { let derived = registry.derive_symbolic_summaries(ctx, scope, Some(arch), symbol_map); collect_canonical_semantic_regions_with_derived_for_branch_blocks( ctx, @@ -907,7 +911,9 @@ pub(super) fn collect_canonical_semantic_regions_with_scope_and_profile( } if let Some(scope) = scope { - let Some(registry) = SummaryRegistry::with_profile_for_arch(arch, summary_profile) else { + let Some(registry) = + SummaryRegistry::with_profile_for_arch_and_symbols(arch, symbol_map, summary_profile) + else { return CollectedNativeSemanticRegions::default(); }; let derived = registry.derive_symbolic_summaries(ctx, scope, Some(arch), symbol_map); diff --git a/crates/r2sym/src/semantics/mod.rs b/crates/r2sym/src/semantics/mod.rs index b7a68f9..4d860af 100644 --- a/crates/r2sym/src/semantics/mod.rs +++ b/crates/r2sym/src/semantics/mod.rs @@ -7,18 +7,27 @@ mod plan; mod region; mod vm; +pub(crate) use artifact::TargetQueryRouteInput; pub use artifact::{ ResidualReason, SemanticArtifact, SemanticArtifactBody, SemanticConfidence, SemanticEvidence, SemanticEvidenceAmbiguity, SemanticEvidenceCoverage, SemanticEvidenceProvenance, SemanticEvidenceReason, SemanticEvidenceSoundness, SliceClass, }; -pub use cache::{SEMANTIC_ARTIFACT_SCHEMA_VERSION, stable_scope_hash}; +pub use cache::{ + SEMANTIC_ARTIFACT_SCHEMA_VERSION, SemanticCompilationResult, SemanticSeedMode, + stable_scope_hash, +}; pub use compiler::compile_semantic_artifact_default_with_scope; -pub use compiler::{compile_function_semantics_with_scope, compile_semantic_artifact_with_scope}; +pub use compiler::{ + compile_function_semantics_with_scope, compile_function_semantics_with_scope_and_replay_seed, + compile_query_semantic_artifact_with_scope, + compile_query_semantic_artifact_with_scope_and_replay_seed, + compile_semantic_artifact_with_scope, compile_semantic_artifact_with_scope_and_replay_seed, +}; pub use facts::SymbolicReachabilityStatus; pub use plan::{ - ArtifactBuildPlan, DecompilePlan, QueryGuidanceMode, QueryPlan, TargetQueryPlan, - TargetQueryRoutePlan, TypePlan, + ArtifactBuildPlan, DecompilePlan, QueryGuidanceMode, QueryPlan, TargetQueryExecutionRoute, + TargetQueryPlan, TargetQueryRoutePlan, TypePlan, }; pub use region::{ ArtifactGranularity, ControlFact, ExecutionModel, Judged, MemoryFact, NativeArtifactBody, diff --git a/crates/r2sym/src/semantics/plan.rs b/crates/r2sym/src/semantics/plan.rs index d05c703..912fb05 100644 --- a/crates/r2sym/src/semantics/plan.rs +++ b/crates/r2sym/src/semantics/plan.rs @@ -52,18 +52,35 @@ pub enum TargetQueryPlan { Refuse { reason: String }, } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum TargetQueryExecutionRoute { + ContinuationSeeded { + bridge_target: u64, + route: Box, + }, + ArtifactCondition { + mode: QueryGuidanceMode, + }, + ArtifactMemoryOnly, + DynamicTargetCompile { + reason: String, + mode: QueryGuidanceMode, + }, + VmTargetCompile { + reason: String, + }, + ResidualOnly { + reasons: Vec, + }, + Refuse { + reason: String, + }, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct TargetQueryRoutePlan { pub target_plan: TargetQueryPlan, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub branch_guidance: Option, - pub allow_memory_term_narrowing: bool, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub dynamic_target_compile_reason: Option, - pub allow_dynamic_target_compile: bool, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub vm_target_compile_reason: Option, - pub allow_vm_target_compile: bool, + pub execution: TargetQueryExecutionRoute, } impl TargetQueryRoutePlan { @@ -72,12 +89,10 @@ impl TargetQueryRoutePlan { target_plan: TargetQueryPlan::Fallback { reason: "semantic artifact unavailable".to_string(), }, - branch_guidance: None, - allow_memory_term_narrowing: false, - dynamic_target_compile_reason: Some("semantic artifact unavailable".to_string()), - allow_dynamic_target_compile: true, - vm_target_compile_reason: Some("semantic artifact unavailable".to_string()), - allow_vm_target_compile: true, + execution: TargetQueryExecutionRoute::DynamicTargetCompile { + reason: "semantic artifact unavailable".to_string(), + mode: QueryGuidanceMode::NarrowOnly, + }, } } } @@ -114,6 +129,14 @@ fn residual_reason_strings(diagnostics: &SemanticArtifactDiagnostics) -> Vec String { + if reasons.is_empty() { + "residual semantic guidance required".to_string() + } else { + reasons.join(", ") + } +} + fn has_large_cfg_only_residual(diagnostics: &SemanticArtifactDiagnostics) -> bool { !diagnostics.residual_reasons.is_empty() && diagnostics @@ -249,71 +272,68 @@ pub fn derive_target_query_plan( pub fn derive_target_query_route_plan( query_plan: &QueryPlan, target_plan: &TargetQueryPlan, + execution: ExecutionModel, has_authoritative_source: bool, has_memory_guidance: bool, ) -> TargetQueryRoutePlan { - match query_plan { + let execution = match query_plan { QueryPlan::Ready => match target_plan { - TargetQueryPlan::Ready { mode } => TargetQueryRoutePlan { - target_plan: target_plan.clone(), - branch_guidance: Some(*mode), - allow_memory_term_narrowing: has_authoritative_source && has_memory_guidance, - dynamic_target_compile_reason: None, - allow_dynamic_target_compile: false, - vm_target_compile_reason: None, - allow_vm_target_compile: false, + TargetQueryPlan::Ready { mode } => match execution { + ExecutionModel::Vm => TargetQueryExecutionRoute::VmTargetCompile { + reason: "vm target compilation selected".to_string(), + }, + ExecutionModel::Native if has_authoritative_source => { + TargetQueryExecutionRoute::ArtifactCondition { mode: *mode } + } + ExecutionModel::Native if has_memory_guidance => { + TargetQueryExecutionRoute::ArtifactMemoryOnly + } + ExecutionModel::Native => TargetQueryExecutionRoute::DynamicTargetCompile { + reason: "target guidance unavailable".to_string(), + mode: *mode, + }, }, - TargetQueryPlan::Fallback { .. } => TargetQueryRoutePlan { - target_plan: target_plan.clone(), - branch_guidance: None, - allow_memory_term_narrowing: has_authoritative_source && has_memory_guidance, - dynamic_target_compile_reason: None, - allow_dynamic_target_compile: false, - vm_target_compile_reason: None, - allow_vm_target_compile: false, + TargetQueryPlan::Fallback { reason } => match execution { + ExecutionModel::Vm => TargetQueryExecutionRoute::VmTargetCompile { + reason: reason.clone(), + }, + ExecutionModel::Native => TargetQueryExecutionRoute::DynamicTargetCompile { + reason: reason.clone(), + mode: QueryGuidanceMode::NarrowOnly, + }, + }, + TargetQueryPlan::Residual { reasons } => TargetQueryExecutionRoute::ResidualOnly { + reasons: reasons.clone(), + }, + TargetQueryPlan::Refuse { reason } => TargetQueryExecutionRoute::Refuse { + reason: reason.clone(), + }, + }, + QueryPlan::Fallback { reason } => match execution { + ExecutionModel::Vm => TargetQueryExecutionRoute::VmTargetCompile { + reason: reason.clone(), + }, + ExecutionModel::Native => TargetQueryExecutionRoute::DynamicTargetCompile { + reason: reason.clone(), + mode: QueryGuidanceMode::NarrowOnly, }, - TargetQueryPlan::Residual { .. } | TargetQueryPlan::Refuse { .. } => { - TargetQueryRoutePlan { - target_plan: target_plan.clone(), - branch_guidance: None, - allow_memory_term_narrowing: false, - dynamic_target_compile_reason: None, - allow_dynamic_target_compile: false, - vm_target_compile_reason: None, - allow_vm_target_compile: false, - } - } }, - QueryPlan::Fallback { reason } => TargetQueryRoutePlan { - target_plan: target_plan.clone(), - branch_guidance: None, - allow_memory_term_narrowing: false, - dynamic_target_compile_reason: Some(reason.clone()), - allow_dynamic_target_compile: true, - vm_target_compile_reason: Some(reason.clone()), - allow_vm_target_compile: true, + QueryPlan::Residual { reasons } => match execution { + ExecutionModel::Vm => TargetQueryExecutionRoute::ResidualOnly { + reasons: reasons.clone(), + }, + ExecutionModel::Native => TargetQueryExecutionRoute::DynamicTargetCompile { + reason: joined_residual_reason(reasons), + mode: QueryGuidanceMode::NarrowOnly, + }, }, - QueryPlan::Residual { reasons } => { - let reason = reasons.join(", "); - TargetQueryRoutePlan { - target_plan: target_plan.clone(), - branch_guidance: None, - allow_memory_term_narrowing: false, - dynamic_target_compile_reason: Some(reason.clone()), - allow_dynamic_target_compile: true, - vm_target_compile_reason: Some(reason), - allow_vm_target_compile: true, - } - } - QueryPlan::Refuse { .. } => TargetQueryRoutePlan { - target_plan: target_plan.clone(), - branch_guidance: None, - allow_memory_term_narrowing: false, - dynamic_target_compile_reason: None, - allow_dynamic_target_compile: false, - vm_target_compile_reason: None, - allow_vm_target_compile: false, + QueryPlan::Refuse { reason } => TargetQueryExecutionRoute::Refuse { + reason: reason.clone(), }, + }; + TargetQueryRoutePlan { + target_plan: target_plan.clone(), + execution, } } @@ -322,8 +342,8 @@ mod tests { use proptest::prelude::*; use super::{ - DecompilePlan, QueryPlan, TargetQueryPlan, TargetQueryRoutePlan, TypePlan, - derive_decompile_plan, derive_query_plan, derive_target_query_plan, + DecompilePlan, QueryPlan, TargetQueryExecutionRoute, TargetQueryPlan, TargetQueryRoutePlan, + TypePlan, derive_decompile_plan, derive_query_plan, derive_target_query_plan, derive_target_query_route_plan, derive_type_plan, }; use crate::{ExecutionModel, RefinementStage, SemanticArtifactDiagnostics}; @@ -423,29 +443,41 @@ mod tests { #[test] fn target_query_route_plan_blocks_conflicting_guidance() { let target_plan = derive_target_query_plan(&QueryPlan::Ready, true, true, true); - let route = derive_target_query_route_plan(&QueryPlan::Ready, &target_plan, true, true); + let route = derive_target_query_route_plan( + &QueryPlan::Ready, + &target_plan, + ExecutionModel::Native, + true, + true, + ); assert!(matches!( route.target_plan, TargetQueryPlan::Residual { .. } )); - assert!(route.branch_guidance.is_none()); - assert!(!route.allow_memory_term_narrowing); - assert!(!route.allow_dynamic_target_compile); - assert!(!route.allow_vm_target_compile); + assert!(matches!( + route.execution, + TargetQueryExecutionRoute::ResidualOnly { .. } + )); } #[test] fn target_query_route_plan_keeps_ready_paths_artifact_authoritative() { let target_plan = derive_target_query_plan(&QueryPlan::Ready, false, false, false); - let route = derive_target_query_route_plan(&QueryPlan::Ready, &target_plan, false, false); + let route = derive_target_query_route_plan( + &QueryPlan::Ready, + &target_plan, + ExecutionModel::Native, + false, + false, + ); assert!(matches!( route.target_plan, TargetQueryPlan::Fallback { .. } )); - assert!(route.branch_guidance.is_none()); - assert!(!route.allow_memory_term_narrowing); - assert!(!route.allow_dynamic_target_compile); - assert!(!route.allow_vm_target_compile); + assert!(matches!( + route.execution, + TargetQueryExecutionRoute::DynamicTargetCompile { .. } + )); } #[test] @@ -454,17 +486,23 @@ mod tests { reasons: vec!["budget".to_string()], }; let target_plan = derive_target_query_plan(&query_plan, false, false, false); - let route = derive_target_query_route_plan(&query_plan, &target_plan, false, false); - assert!(route.branch_guidance.is_none()); - assert!(route.dynamic_target_compile_reason.is_some()); - assert!(route.allow_dynamic_target_compile); - assert!(route.vm_target_compile_reason.is_some()); - assert!(route.allow_vm_target_compile); + let route = derive_target_query_route_plan( + &query_plan, + &target_plan, + ExecutionModel::Native, + false, + false, + ); + assert!(matches!( + route.execution, + TargetQueryExecutionRoute::DynamicTargetCompile { .. } + )); } proptest! { #[test] fn target_query_route_plan_is_total( + is_vm in any::(), ready in any::(), conflict in any::(), has_guidance in any::(), @@ -481,30 +519,23 @@ mod tests { let route = derive_target_query_route_plan( &query_plan, &target_plan, + if is_vm { + ExecutionModel::Vm + } else { + ExecutionModel::Native + }, has_authoritative_source, has_memory_guidance, ); let TargetQueryRoutePlan { .. } = route; - let valid = true; - prop_assert!(valid); - if matches!(target_plan, TargetQueryPlan::Ready { .. }) { - prop_assert_eq!( - route.allow_memory_term_narrowing, - has_authoritative_source && has_memory_guidance - ); - } - if matches!(target_plan, TargetQueryPlan::Fallback { .. }) && ready { - prop_assert_eq!( - route.allow_memory_term_narrowing, - has_authoritative_source && has_memory_guidance - ); - } - if ready { - prop_assert!(!route.allow_dynamic_target_compile); - prop_assert!(!route.allow_vm_target_compile); - } - if matches!(target_plan, TargetQueryPlan::Residual { .. } | TargetQueryPlan::Refuse { .. }) || !ready { - prop_assert!(route.branch_guidance.is_none()); + match route.execution { + TargetQueryExecutionRoute::ContinuationSeeded { .. } + | TargetQueryExecutionRoute::ArtifactCondition { .. } + | TargetQueryExecutionRoute::ArtifactMemoryOnly + | TargetQueryExecutionRoute::DynamicTargetCompile { .. } + | TargetQueryExecutionRoute::VmTargetCompile { .. } + | TargetQueryExecutionRoute::ResidualOnly { .. } + | TargetQueryExecutionRoute::Refuse { .. } => {} } } } diff --git a/crates/r2sym/src/sim.rs b/crates/r2sym/src/sim.rs index 5a2b0b9..df08150 100644 --- a/crates/r2sym/src/sim.rs +++ b/crates/r2sym/src/sim.rs @@ -20,7 +20,7 @@ use z3::ast::{Ast, BV, Bool}; use crate::executor::{CallHookResult, SymExecutor}; use crate::path::PathExplorer; use crate::solver::{SatResult, SymSolver}; -use crate::state::{ExitStatus, SymState}; +use crate::state::{ExitStatus, RuntimeValueProvenance, SymState}; use crate::value::SymValue; /// Default upper bound for string operations. @@ -188,6 +188,11 @@ impl CallConv { Self::new(vec!["RDI", "RSI", "RDX", "RCX", "R8", "R9"], "RAX", 64, 64) } + /// x86-64 Windows ABI (RCX, RDX, R8, R9; return in RAX). + pub fn x86_64_windows() -> Self { + Self::new(vec!["RCX", "RDX", "R8", "R9"], "RAX", 64, 64) + } + /// Architecture-derived calling convention used by the symbolic runtime. pub fn for_arch_spec(arch: &ArchSpec) -> Option { let arch_name = arch.name.to_ascii_lowercase(); @@ -213,6 +218,20 @@ impl CallConv { None } + /// Architecture-derived calling convention with binary-environment hints. + pub fn for_arch_spec_and_symbols( + arch: &ArchSpec, + symbol_map: &HashMap, + ) -> Option { + let arch_name = arch.name.to_ascii_lowercase(); + let looks_x86 = arch_name.contains("x86") || arch_name == "x64" || arch_name == "amd64"; + let looks_64 = arch.addr_size == 8 || arch.addr_size == 64 || arch_name.contains("64"); + if looks_x86 && looks_64 && symbol_map_looks_windows(symbol_map) { + return Some(Self::x86_64_windows()); + } + Self::for_arch_spec(arch) + } + pub(crate) fn collect_call_info<'ctx>( &self, state: &SymState<'ctx>, @@ -242,7 +261,7 @@ impl CallConv { SymValue::unknown(self.arg_bits) } - fn write_return<'ctx>(&self, state: &mut SymState<'ctx>, value: SymValue<'ctx>) { + pub(crate) fn write_return<'ctx>(&self, state: &mut SymState<'ctx>, value: SymValue<'ctx>) { let key = register_aliases(self.ret_register) .into_iter() .find_map(|alias| find_register_key(state, alias)) @@ -281,6 +300,19 @@ impl CallConv { } } +fn symbol_map_looks_windows(symbol_map: &HashMap) -> bool { + symbol_map.values().any(|name| { + let lower = name.to_ascii_lowercase(); + lower.contains(".dll_") + || lower.contains("kernel32") + || lower.contains("ntdll") + || lower.contains("msvcrt") + || lower.ends_with("addvectoredexceptionhandler") + || lower.ends_with("raiseexception") + || lower.ends_with("virtualalloc") + }) +} + #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] pub struct SummaryInstallStats { pub attempted: usize, @@ -327,6 +359,16 @@ impl PreparedFunctionScope { &self.functions } + pub fn function_containing_block(&self, pc: u64) -> Option<&ScopedPreparedFunction> { + self.functions + .values() + .find(|function| function.prepared.get_block(pc).is_some()) + } + + pub fn contains_block(&self, pc: u64) -> bool { + self.function_containing_block(pc).is_some() + } + pub fn helper_functions(&self) -> impl Iterator { self.functions .values() @@ -347,6 +389,13 @@ impl PreparedFunctionScope { } } +fn is_runtime_materialized_scope_function(function: &ScopedPreparedFunction) -> bool { + function + .name + .as_deref() + .is_some_and(|name| name.starts_with("runtime.materialized.")) +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DerivedSummaryCompletion { Exact, @@ -483,6 +532,18 @@ impl<'ctx> SummaryRegistry<'ctx> { Some(Self::with_profile(CallConv::for_arch_spec(arch)?, profile)) } + /// Create a registry using symbol-map environment hints when the architecture is ambiguous. + pub fn with_profile_for_arch_and_symbols( + arch: &ArchSpec, + symbol_map: &HashMap, + profile: SummaryProfile, + ) -> Option { + Some(Self::with_profile( + CallConv::for_arch_spec_and_symbols(arch, symbol_map)?, + profile, + )) + } + /// Register a function summary. pub fn register_summary(&mut self, summary: S) where @@ -642,6 +703,7 @@ impl<'ctx> SummaryRegistry<'ctx> { let helper_scope = scope .helper_functions() + .filter(|helper| !is_runtime_materialized_scope_function(helper)) .map(|helper| (helper.id, helper)) .collect::>(); let sccs = compute_derived_summary_sccs(&helper_scope); @@ -950,6 +1012,9 @@ fn build_interproc_summary_set( let mut inputs = Vec::new(); let mut seeds = BTreeMap::new(); for function in scope.functions().values() { + if is_runtime_materialized_scope_function(function) { + continue; + } inputs.push(InterprocFunctionInput { id: function.id, name: function.name.clone(), @@ -1277,6 +1342,9 @@ fn apply_derived_summary<'ctx>( if summary.cases.is_empty() { return CallHookResult::Fallthrough; } + if apply_derived_runtime_materialization_copy(state, summary, &call, callconv) { + return CallHookResult::Fallthrough; + } let substitutions = build_summary_substitutions(state, summary, &call); @@ -1320,6 +1388,66 @@ fn apply_derived_summary<'ctx>( CallHookResult::Fallthrough } +const DERIVED_RUNTIME_COPY_MIN_SIZE: u32 = 0x100; + +fn apply_derived_runtime_materialization_copy<'ctx>( + state: &mut SymState<'ctx>, + summary: &DerivedFunctionSummary<'ctx>, + call: &CallInfo<'ctx>, + callconv: &CallConv, +) -> bool { + let [case] = summary.cases.as_slice() else { + return false; + }; + if case.guard.simplify().as_bool() != Some(true) { + return false; + } + let Some(write) = case + .memory_writes + .iter() + .find(|write| write.arg_index == 0 && write.offset == 0) + else { + return false; + }; + if write.size < DERIVED_RUNTIME_COPY_MIN_SIZE { + return false; + } + if !summary + .memory_inputs + .iter() + .any(|input| input.arg_index == 1 && input.size >= write.size) + { + return false; + } + let Some(dst) = call.args.first().and_then(SymValue::as_concrete) else { + return false; + }; + let Some(src) = call.args.get(1).and_then(SymValue::as_concrete) else { + return false; + }; + let len = call + .args + .get(2) + .and_then(SymValue::as_concrete) + .unwrap_or(write.size as u64); + if len == 0 || len > write.size as u64 || len > u32::MAX as u64 { + return false; + } + if state.runtime_region_for_pc(dst).is_none() { + return false; + } + + let provenance = RuntimeValueProvenance { + source_addr: src, + size: len as u32, + }; + state.note_runtime_store_copy(dst, len as u32, Some(&provenance)); + if case.return_value.is_some() { + callconv.write_return(state, SymValue::concrete(dst, call.ret_bits)); + } + true +} + pub(crate) fn evaluate_derived_summary_guidance<'ctx>( state: &SymState<'ctx>, summary: &DerivedFunctionSummary<'ctx>, @@ -1782,6 +1910,18 @@ impl<'ctx> FunctionSummary<'ctx> for MemcpySummary { .cloned() .unwrap_or_else(|| SymValue::unknown(call.arg_bits)); copy_bytes(state, &dst, &src, &n, self.max_copy, self.byte_policy); + if let (Some(dst_addr), Some(src_addr), Some(n)) = + (dst.as_concrete(), src.as_concrete(), n.as_concrete()) + && n > 0 + && n <= self.max_copy + && n <= u32::MAX as u64 + { + let provenance = RuntimeValueProvenance { + source_addr: src_addr, + size: n as u32, + }; + state.note_runtime_store_copy(dst_addr, n as u32, Some(&provenance)); + } SummaryEffect::Return(Some(dst)) } } @@ -2503,6 +2643,31 @@ mod tests { assert_eq!(path_listing_base.get_taint(), 0x20); } + #[test] + fn memcpy_summary_records_runtime_materialization_provenance() { + let ctx = z3::Context::thread_local(); + let mut state = SymState::new(&ctx, 0); + let (_region, runtime_base) = state.allocate_heap_region("jit", 0x1000); + state.register_runtime_region_alias(runtime_base, 0x1000, true); + + let summary = MemcpySummary::new(0x1000); + let call = CallInfo { + args: vec![ + SymValue::concrete(runtime_base, 64), + SymValue::concrete(0x14009c000, 64), + SymValue::concrete(0x1000, 64), + ], + arg_bits: 64, + ret_bits: 64, + }; + let _ = summary.execute(&mut state, &call); + + let region = state + .runtime_region_for_pc(runtime_base) + .expect("runtime region should remain registered"); + assert_eq!(region.source_base, Some(0x14009c000)); + } + #[test] fn prepared_function_scope_with_prepared_root_rebinds_root_only() { let blocks_a = vec![R2ILBlock { diff --git a/crates/r2sym/src/solver.rs b/crates/r2sym/src/solver.rs index 3f0eeb3..3e24f1e 100644 --- a/crates/r2sym/src/solver.rs +++ b/crates/r2sym/src/solver.rs @@ -1271,7 +1271,11 @@ impl<'ctx> SymSolver<'ctx> { let result = self.compute_state_sat_result(state, SolverMode::ExploreFast, DeepQueryKind::Predicate); self.remember_state_sat_result(cache_key, result); - result == SatResult::Sat + // Exploration pruning must be conservative. `Unknown` usually means the + // fast partitioned solver hit a complex bit-vector predicate; treating + // that as UNSAT drops real paths before the deeper query/model phase can + // classify them honestly. + result != SatResult::Unsat } /// Check whether a state's constraints remain satisfiable with one extra constraint. diff --git a/crates/r2sym/src/state.rs b/crates/r2sym/src/state.rs index 11050a9..a654731 100644 --- a/crates/r2sym/src/state.rs +++ b/crates/r2sym/src/state.rs @@ -5,7 +5,7 @@ use std::cell::OnceCell; use std::collections::hash_map::DefaultHasher; -use std::collections::{HashMap, HashSet}; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::hash::{Hash, Hasher}; use std::rc::Rc; use std::sync::atomic::{AtomicUsize, Ordering}; @@ -16,6 +16,22 @@ use z3::ast::{BV, Bool}; use crate::memory::{MemoryRegionId, MemoryRegionKind, SymMemory}; use crate::value::SymValue; +fn debug_runtime_continuation_log(message: &str) { + if std::env::var_os("R2SLEIGH_DEBUG_RUNTIME_CONTINUATION").is_none() { + return; + } + let path = std::env::var("R2SLEIGH_DEBUG_RUNTIME_CONTINUATION_LOG") + .unwrap_or_else(|_| "/tmp/r2sleigh_runtime_continuation.log".to_string()); + if let Ok(mut file) = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(path) + { + use std::io::Write; + let _ = writeln!(file, "{message}"); + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub(crate) struct ConstraintCursorKey(usize); @@ -143,13 +159,61 @@ pub struct SymbolicFdInput<'ctx> { pub cursor: usize, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum RuntimeBlockReason { + MissingExceptionHandler, + MissingRuntimeMaterializedCode, + MissingContinuationSeed, + RuntimeRegionProvenanceUnknown, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct RuntimeValueProvenance { + pub source_addr: u64, + pub size: u32, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct RuntimeRegionAlias { + pub runtime_base: u64, + pub size: u64, + pub source_base: Option, + pub executable: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct PendingExceptionContinuation { + pub handler_addr: u64, + pub exception_code: u64, + pub exception_pointers_addr: u64, + pub exception_record_addr: u64, + pub context_addr: u64, +} + +#[derive(Debug, Clone)] +pub struct RuntimeBreakpointContinuation<'ctx> { + pub handler_addr: u64, + pub exception_code: u64, + pub breakpoint: SymValue<'ctx>, +} + /// Runtime policy carried with each symbolic state. #[derive(Debug, Clone, Default)] -pub struct RuntimeState { +pub struct RuntimeState<'ctx> { /// File descriptors that should report tty=true via isatty(). pub tty_fds: HashSet, /// Whether sleep-family calls should become zero-cost no-ops. pub skip_sleep_calls: bool, + /// Registered exception handlers discovered by runtime hooks. + pub exception_handlers: BTreeSet, + /// Runtime regions that may alias materialized executable code. + pub runtime_regions: BTreeMap, + /// Provenance for recently loaded values used to detect copy loops. + pub value_provenance: BTreeMap, + /// Pending exception continuation state that should resume on handler return. + pub pending_exception: Option, + /// Active runtime breakpoint that re-enters an exception handler when reached. + pub active_breakpoint: Option>, } /// The state of a symbolic execution. @@ -166,6 +230,9 @@ pub struct SymState<'ctx> { constraints: ConstraintCursor, /// Materialized constraint list, populated lazily from the shared cursor chain. materialized_constraints: OnceCell>, + /// Syntactic value facts derived while adding constraints. + known_zero_values: Rc>, + known_nonzero_values: Rc>, /// Current program counter. pub pc: u64, /// Previous program counter (block predecessor). @@ -183,7 +250,7 @@ pub struct SymState<'ctx> { /// Symbolic external input streams keyed by file descriptor. symbolic_fd_inputs: Rc>>, /// Runtime policy/state for summaries. - runtime: RuntimeState, + runtime: RuntimeState<'ctx>, } /// Exit status of a symbolic execution path. @@ -195,6 +262,8 @@ pub enum ExitStatus { Exit(u64), /// Hit an error/exception. Error(String), + /// Runtime execution was blocked because a required dynamic fact was missing. + RuntimeBlocked(RuntimeBlockReason), /// Hit an unimplemented operation. Unimplemented, /// Reached maximum depth. @@ -212,6 +281,8 @@ impl<'ctx> SymState<'ctx> { memory: SymMemory::new(ctx), constraints: ConstraintCursor::default(), materialized_constraints: OnceCell::new(), + known_zero_values: Rc::new(BTreeSet::new()), + known_nonzero_values: Rc::new(BTreeSet::new()), pc: entry_pc, prev_pc: None, active: true, @@ -232,6 +303,8 @@ impl<'ctx> SymState<'ctx> { memory: SymMemory::new_symbolic(ctx), constraints: ConstraintCursor::default(), materialized_constraints: OnceCell::new(), + known_zero_values: Rc::new(BTreeSet::new()), + known_nonzero_values: Rc::new(BTreeSet::new()), pc: entry_pc, prev_pc: None, active: true, @@ -316,16 +389,381 @@ impl<'ctx> SymState<'ctx> { self.symbolic_memory.as_slice() } + /// Return whether every byte in a concrete address range is known concrete. + pub fn is_concrete_memory_range(&self, addr: u64, size: u32) -> bool { + self.memory.is_concrete_range(addr, size) + } + + /// Read concrete bytes from memory when the full range is concrete. + pub fn read_memory_bytes(&self, addr: u64, size: usize) -> Option> { + self.memory.read_bytes(addr, size) + } + /// Get tracked symbolic file-descriptor inputs. pub fn symbolic_fd_inputs(&self) -> &HashMap> { self.symbolic_fd_inputs.as_ref() } /// Get runtime policy/state. - pub fn runtime(&self) -> &RuntimeState { + pub fn runtime(&self) -> &RuntimeState<'ctx> { &self.runtime } + pub fn register_exception_handler(&mut self, addr: u64) { + self.runtime.exception_handlers.insert(addr); + } + + pub fn primary_exception_handler(&self) -> Option { + self.runtime.exception_handlers.iter().next().copied() + } + + pub fn define_runtime_region( + &mut self, + name: &str, + base_addr: u64, + size: u64, + executable: bool, + ) -> MemoryRegionId { + let region_id = + self.define_memory_region(MemoryRegionKind::Heap, name, Some(base_addr), Some(size)); + self.runtime + .runtime_regions + .entry(base_addr) + .or_insert(RuntimeRegionAlias { + runtime_base: base_addr, + size, + source_base: None, + executable, + }); + region_id + } + + pub fn register_runtime_region_alias(&mut self, base_addr: u64, size: u64, executable: bool) { + self.runtime + .runtime_regions + .entry(base_addr) + .and_modify(|region| { + region.size = region.size.max(size); + region.executable |= executable; + }) + .or_insert(RuntimeRegionAlias { + runtime_base: base_addr, + size, + source_base: None, + executable, + }); + } + + pub fn mark_runtime_region_executable(&mut self, base_addr: u64, size: u64) -> bool { + let mut updated = false; + for region in self.runtime.runtime_regions.values_mut() { + let region_end = region.runtime_base.saturating_add(region.size); + let end = base_addr.saturating_add(size); + if base_addr < region_end && region.runtime_base < end { + region.executable = true; + updated = true; + } + } + updated + } + + pub fn runtime_region_for_pc(&self, pc: u64) -> Option<&RuntimeRegionAlias> { + self.runtime.runtime_regions.values().find(|region| { + pc >= region.runtime_base && pc < region.runtime_base.saturating_add(region.size) + }) + } + + pub fn resolve_runtime_pc(&self, pc: u64) -> Option { + let region = self.runtime_region_for_pc(pc)?; + if !region.executable { + return None; + } + let source_base = region.source_base?; + let offset = pc.checked_sub(region.runtime_base)?; + (offset < region.size).then_some(source_base.saturating_add(offset)) + } + + pub fn remap_static_pc_to_runtime(&self, static_pc: u64) -> Option { + self.runtime.runtime_regions.values().find_map(|region| { + let source_base = region.source_base?; + let offset = static_pc.checked_sub(source_base)?; + (offset < region.size && region.executable) + .then_some(region.runtime_base.saturating_add(offset)) + }) + } + + pub fn set_value_provenance(&mut self, name: &str, provenance: Option) { + if let Some(provenance) = provenance { + self.runtime + .value_provenance + .insert(name.to_string(), provenance); + } else { + self.runtime.value_provenance.remove(name); + } + } + + pub fn value_provenance(&self, name: &str) -> Option<&RuntimeValueProvenance> { + self.runtime.value_provenance.get(name) + } + + pub fn note_runtime_store_copy( + &mut self, + store_addr: u64, + size: u32, + provenance: Option<&RuntimeValueProvenance>, + ) { + let Some(provenance) = provenance else { + return; + }; + let Some(region_base) = self + .runtime + .runtime_regions + .values() + .find(|region| { + store_addr >= region.runtime_base + && store_addr < region.runtime_base.saturating_add(region.size) + }) + .map(|region| region.runtime_base) + else { + return; + }; + let Some(region) = self.runtime.runtime_regions.get_mut(®ion_base) else { + return; + }; + let Some(offset) = store_addr.checked_sub(region.runtime_base) else { + return; + }; + let Some(candidate_source_base) = provenance.source_addr.checked_sub(offset) else { + return; + }; + match region.source_base { + None => region.source_base = Some(candidate_source_base), + Some(existing) if existing == candidate_source_base => {} + Some(_) => region.source_base = None, + } + let copied_extent = offset.saturating_add(size as u64); + if copied_extent > region.size { + region.size = copied_extent; + } + } + + pub fn set_pending_exception(&mut self, pending: PendingExceptionContinuation) { + self.runtime.pending_exception = Some(pending); + } + + pub fn pending_exception(&self) -> Option<&PendingExceptionContinuation> { + self.runtime.pending_exception.as_ref() + } + + pub(crate) fn clear_pending_exception(&mut self) { + self.runtime.pending_exception = None; + } + + fn write_u32(&mut self, addr: u64, value: u32) { + self.mem_write( + &SymValue::concrete(addr, 64), + &SymValue::concrete(value as u64, 32), + 4, + ); + } + + fn write_u64(&mut self, addr: u64, value: u64) { + self.mem_write( + &SymValue::concrete(addr, 64), + &SymValue::concrete(value, 64), + 8, + ); + } + + fn read_register_family(&self, base: &str) -> SymValue<'ctx> { + let lower = base.to_ascii_lowercase(); + let mut best: Option<(u64, &String)> = None; + for key in self.registers.keys() { + let key_lower = key.to_ascii_lowercase(); + if let Some((prefix, suffix)) = key_lower.rsplit_once('_') + && prefix == lower + && let Ok(version) = suffix.parse::() + { + if best.is_none_or(|(best_version, _)| version > best_version) { + best = Some((version, key)); + } + } else if key_lower == lower { + return self.get_register_sized(key, 64); + } + } + best.map_or_else( + || SymValue::unknown(64), + |(_, key)| self.get_register_sized(key, 64), + ) + } + + fn write_register_to_context(&mut self, context_addr: u64, base: &str, offset: u64) { + let value = self.read_register_family(base); + self.mem_write( + &SymValue::concrete(context_addr.saturating_add(offset), 64), + &value, + 8, + ); + } + + pub(crate) fn seed_exception_continuation(&mut self, exception_code: u64, handler_addr: u64) { + let (_, record_addr) = self.allocate_heap_region("veh_exception_record", 0x20); + let (_, context_addr) = self.allocate_heap_region("veh_exception_context", 0x400); + let (_, pointers_addr) = self.allocate_heap_region("veh_exception_pointers", 0x10); + + self.write_u32(record_addr, exception_code as u32); + self.write_u64(pointers_addr, record_addr); + self.write_u64(pointers_addr.saturating_add(8), context_addr); + self.write_register_to_context(context_addr, "RAX", 0x78); + self.write_register_to_context(context_addr, "RCX", 0x80); + self.write_register_to_context(context_addr, "RDX", 0x88); + self.write_register_to_context(context_addr, "RBX", 0x90); + self.write_register_to_context(context_addr, "RSP", 0x98); + self.write_register_to_context(context_addr, "RBP", 0xA0); + self.write_register_to_context(context_addr, "RSI", 0xA8); + self.write_register_to_context(context_addr, "RDI", 0xB0); + self.write_register_to_context(context_addr, "R8", 0xB8); + self.write_register_to_context(context_addr, "R9", 0xC0); + self.write_register_to_context(context_addr, "R10", 0xC8); + self.write_register_to_context(context_addr, "R11", 0xD0); + self.write_register_to_context(context_addr, "R12", 0xD8); + self.write_register_to_context(context_addr, "R13", 0xE0); + self.write_register_to_context(context_addr, "R14", 0xE8); + self.write_register_to_context(context_addr, "R15", 0xF0); + self.write_u64(context_addr.saturating_add(0xF8), self.pc); + self.set_concrete("RCX_0", pointers_addr, 64); + self.set_pending_exception(PendingExceptionContinuation { + handler_addr, + exception_code, + exception_pointers_addr: pointers_addr, + exception_record_addr: record_addr, + context_addr, + }); + } + + pub(crate) fn dispatch_runtime_breakpoint_if_ready(&mut self) -> bool { + let Some(breakpoint) = self.runtime.active_breakpoint.clone() else { + return false; + }; + let Some(breakpoint_addr) = breakpoint.breakpoint.as_concrete() else { + return false; + }; + if self.pc != breakpoint_addr { + return false; + } + self.runtime.active_breakpoint = None; + self.seed_exception_continuation(breakpoint.exception_code, breakpoint.handler_addr); + self.pc = breakpoint.handler_addr; + debug_runtime_continuation_log(&format!( + "runtime_breakpoint_dispatch pc=0x{:x} handler=0x{:x} code=0x{:x}", + breakpoint_addr, breakpoint.handler_addr, breakpoint.exception_code + )); + true + } + + pub(crate) fn fork_symbolic_runtime_breakpoint_at(&mut self, pc: u64) -> Option { + let breakpoint = self.runtime.active_breakpoint.clone()?; + if breakpoint.breakpoint.as_concrete().is_some() { + return None; + } + self.runtime_region_for_pc(pc)?; + + let mut dispatched = self.fork(); + dispatched.constrain_eq(&breakpoint.breakpoint, pc); + dispatched.runtime.active_breakpoint = None; + dispatched.seed_exception_continuation(breakpoint.exception_code, breakpoint.handler_addr); + dispatched.pc = breakpoint.handler_addr; + self.constrain_ne(&breakpoint.breakpoint, pc); + + debug_runtime_continuation_log(&format!( + "runtime_breakpoint_symbolic_fork pc=0x{:x} handler=0x{:x} code=0x{:x}", + pc, breakpoint.handler_addr, breakpoint.exception_code + )); + Some(dispatched) + } + + pub fn set_pending_exception_resume_pc(&mut self, resume_pc: u64) -> bool { + let Some(pending) = self.runtime.pending_exception.as_ref() else { + return false; + }; + self.mem_write( + &SymValue::concrete(pending.context_addr.saturating_add(0xF8), 64), + &SymValue::concrete(resume_pc, 64), + 8, + ); + true + } + + pub fn resume_pending_exception_continuation( + &mut self, + ) -> Result, RuntimeBlockReason> { + let Some(pending) = self.runtime.pending_exception.clone() else { + return Ok(None); + }; + let read_u32 = |state: &Self, addr: u64| state.mem_read(&SymValue::concrete(addr, 64), 4); + let read_u64 = |state: &Self, addr: u64| state.mem_read(&SymValue::concrete(addr, 64), 8); + let restore_register = |state: &mut Self, name: &str, offset: u64| { + let value = read_u64(state, pending.context_addr.saturating_add(offset)); + state.set_register(&format!("{name}_0"), value); + }; + restore_register(self, "RAX", 0x78); + restore_register(self, "RCX", 0x80); + restore_register(self, "RDX", 0x88); + restore_register(self, "RBX", 0x90); + restore_register(self, "RSP", 0x98); + restore_register(self, "RBP", 0xA0); + restore_register(self, "RSI", 0xA8); + restore_register(self, "RDI", 0xB0); + restore_register(self, "R8", 0xB8); + restore_register(self, "R9", 0xC0); + restore_register(self, "R10", 0xC8); + restore_register(self, "R11", 0xD0); + restore_register(self, "R12", 0xD8); + restore_register(self, "R13", 0xE0); + restore_register(self, "R14", 0xE8); + restore_register(self, "R15", 0xF0); + let rip = read_u64(self, pending.context_addr.saturating_add(0xF8)); + let Some(rip) = rip.as_concrete() else { + return Err(RuntimeBlockReason::MissingContinuationSeed); + }; + let breakpoint = read_u64(self, pending.context_addr.saturating_add(0x48)); + let debug_context_requested = read_u32(self, pending.context_addr.saturating_add(0x30)) + .as_concrete() + .is_some_and(|value| value & 0x10 != 0); + let dr7_enabled = read_u64(self, pending.context_addr.saturating_add(0x70)) + .as_concrete() + .is_some_and(|value| value != 0); + let breakpoint_enabled = debug_context_requested || dr7_enabled; + self.runtime.active_breakpoint = if pending.exception_code == 0x8000_0004 + && breakpoint_enabled + && self.runtime_region_for_pc(rip).is_some() + && !breakpoint.is_unknown() + { + Some(RuntimeBreakpointContinuation { + handler_addr: pending.handler_addr, + exception_code: pending.exception_code, + breakpoint: breakpoint.clone(), + }) + } else { + None + }; + debug_runtime_continuation_log(&format!( + "runtime_resume handler=0x{:x} code=0x{:x} rip=0x{:x} breakpoint={} debug_context={} dr7={} active={}", + pending.handler_addr, + pending.exception_code, + rip, + breakpoint.as_concrete().map_or_else( + || format!("symbolic:{}b", breakpoint.bits()), + |value| format!("0x{value:x}") + ), + debug_context_requested, + dr7_enabled, + self.runtime.active_breakpoint.is_some() + )); + self.runtime.pending_exception = None; + Ok(Some(rip)) + } + pub(crate) fn semantic_fingerprint(&self) -> u64 { let mut hasher = DefaultHasher::new(); self.pc.hash(&mut hasher); @@ -338,6 +776,24 @@ impl<'ctx> SymState<'ctx> { tty_fds.sort_unstable(); tty_fds.hash(&mut hasher); + self.runtime.exception_handlers.hash(&mut hasher); + for (base, region) in &self.runtime.runtime_regions { + base.hash(&mut hasher); + region.hash(&mut hasher); + } + let mut provenance = self.runtime.value_provenance.iter().collect::>(); + provenance.sort_unstable_by(|a, b| a.0.cmp(b.0)); + for (name, source) in provenance { + name.hash(&mut hasher); + source.hash(&mut hasher); + } + self.runtime.pending_exception.hash(&mut hasher); + if let Some(breakpoint) = &self.runtime.active_breakpoint { + breakpoint.handler_addr.hash(&mut hasher); + breakpoint.exception_code.hash(&mut hasher); + hash_sym_value(self.ctx, &breakpoint.breakpoint, &mut hasher); + } + let mut register_names: Vec<_> = self.registers.keys().collect(); register_names.sort_unstable(); for name in register_names { @@ -345,6 +801,9 @@ impl<'ctx> SymState<'ctx> { hash_sym_value(self.ctx, &self.registers[name], &mut hasher); } + self.known_zero_values.hash(&mut hasher); + self.known_nonzero_values.hash(&mut hasher); + let mut symbolic_input_names: Vec<_> = self.symbolic_inputs.keys().collect(); symbolic_input_names.sort_unstable(); for name in symbolic_input_names { @@ -415,6 +874,18 @@ impl<'ctx> SymState<'ctx> { merged.constraints = ConstraintCursor::default(); merged.materialized_constraints = OnceCell::new(); merged.add_constraint(cond_self | cond_other.clone()); + merged.known_zero_values = Rc::new( + self.known_zero_values + .intersection(&other.known_zero_values) + .cloned() + .collect(), + ); + merged.known_nonzero_values = Rc::new( + self.known_nonzero_values + .intersection(&other.known_nonzero_values) + .cloned() + .collect(), + ); let mut keys = HashSet::new(); keys.extend(self.registers.keys().cloned()); @@ -494,6 +965,47 @@ impl<'ctx> SymState<'ctx> { .runtime .tty_fds .extend(other.runtime.tty_fds.iter().copied()); + merged + .runtime + .exception_handlers + .extend(other.runtime.exception_handlers.iter().copied()); + for (base, other_region) in &other.runtime.runtime_regions { + match merged.runtime.runtime_regions.get_mut(base) { + Some(existing) => { + existing.size = existing.size.max(other_region.size); + existing.executable |= other_region.executable; + if existing.source_base != other_region.source_base { + existing.source_base = None; + } + } + None => { + merged + .runtime + .runtime_regions + .insert(*base, other_region.clone()); + } + } + } + merged.runtime.pending_exception = (self.runtime.pending_exception + == other.runtime.pending_exception) + .then(|| self.runtime.pending_exception.clone()) + .flatten(); + merged.runtime.active_breakpoint = match ( + &self.runtime.active_breakpoint, + &other.runtime.active_breakpoint, + ) { + (Some(left), Some(right)) if runtime_breakpoint_equal(self.ctx, left, right) => { + Some(left.clone()) + } + _ => None, + }; + merged.runtime.value_provenance.retain(|name, source| { + other + .runtime + .value_provenance + .get(name) + .is_some_and(|other_source| other_source == source) + }); merged } @@ -507,11 +1019,42 @@ impl<'ctx> SymState<'ctx> { } } + fn mark_value_zero(&mut self, value: &SymValue<'ctx>) { + let Some(key) = value.symbolic_key() else { + return; + }; + Rc::make_mut(&mut self.known_nonzero_values).remove(&key); + Rc::make_mut(&mut self.known_zero_values).insert(key); + } + + fn mark_value_nonzero(&mut self, value: &SymValue<'ctx>) { + let Some(key) = value.symbolic_key() else { + return; + }; + Rc::make_mut(&mut self.known_zero_values).remove(&key); + Rc::make_mut(&mut self.known_nonzero_values).insert(key); + } + + pub(crate) fn value_known_zero(&self, value: &SymValue<'ctx>) -> bool { + value + .symbolic_key() + .is_some_and(|key| self.known_zero_values.contains(&key)) + } + + pub(crate) fn value_known_nonzero(&self, value: &SymValue<'ctx>) -> bool { + value + .symbolic_key() + .is_some_and(|key| self.known_nonzero_values.contains(&key)) + } + /// Constrain a value to equal a concrete constant. pub fn constrain_eq(&mut self, value: &SymValue<'ctx>, rhs: u64) { let bv = value.to_bv(self.ctx); let rhs_bv = BV::from_u64(rhs, value.bits()); self.add_constraint(bv.eq(&rhs_bv)); + if rhs == 0 { + self.mark_value_zero(value); + } } /// Constrain a value to not equal a concrete constant. @@ -519,6 +1062,9 @@ impl<'ctx> SymState<'ctx> { let bv = value.to_bv(self.ctx); let rhs_bv = BV::from_u64(rhs, value.bits()); self.add_constraint(bv.eq(&rhs_bv).not()); + if rhs == 0 { + self.mark_value_nonzero(value); + } } /// Constrain a value to be within an unsigned range [min, max]. @@ -635,6 +1181,7 @@ impl<'ctx> SymState<'ctx> { let zero = BV::from_u64(0, value.bits()); let cond = bv.eq(&zero).not(); self.add_constraint(cond); + self.mark_value_nonzero(value); } /// Add a constraint that a value is false (zero). @@ -643,6 +1190,7 @@ impl<'ctx> SymState<'ctx> { let zero = BV::from_u64(0, value.bits()); let cond = bv.eq(&zero); self.add_constraint(cond); + self.mark_value_zero(value); } /// Get all path constraints. @@ -688,6 +1236,11 @@ impl<'ctx> SymState<'ctx> { self.depth += 1; } + /// Increment the execution depth by a known number of concrete steps. + pub fn step_by(&mut self, steps: usize) { + self.depth = self.depth.saturating_add(steps); + } + /// Fork this state (for branching). pub fn fork(&self) -> Self { Self { @@ -696,6 +1249,8 @@ impl<'ctx> SymState<'ctx> { memory: self.memory.fork(), constraints: self.constraints.clone(), materialized_constraints: OnceCell::new(), + known_zero_values: self.known_zero_values.clone(), + known_nonzero_values: self.known_nonzero_values.clone(), pc: self.pc, prev_pc: self.prev_pc, active: self.active, @@ -869,6 +1424,21 @@ fn structural_hash(value: &T) -> u64 { hasher.finish() } +fn runtime_breakpoint_equal<'ctx>( + ctx: &'ctx Context, + left: &RuntimeBreakpointContinuation<'ctx>, + right: &RuntimeBreakpointContinuation<'ctx>, +) -> bool { + if left.handler_addr != right.handler_addr || left.exception_code != right.exception_code { + return false; + } + let mut left_hasher = DefaultHasher::new(); + hash_sym_value(ctx, &left.breakpoint, &mut left_hasher); + let mut right_hasher = DefaultHasher::new(); + hash_sym_value(ctx, &right.breakpoint, &mut right_hasher); + left_hasher.finish() == right_hasher.finish() +} + fn hash_sym_value<'ctx, H: Hasher>(ctx: &'ctx Context, value: &SymValue<'ctx>, hasher: &mut H) { value.bits().hash(hasher); value.get_taint().hash(hasher); @@ -1172,4 +1742,183 @@ mod tests { .unwrap(); assert_eq!(value, 0xab); } + + #[test] + fn test_runtime_region_copy_alias_resolves_runtime_pc() { + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + + let _ = state.define_runtime_region("jit_blob", 0x6000_0000, 0x1000, true); + state.note_runtime_store_copy( + 0x6000_0010, + 1, + Some(&RuntimeValueProvenance { + source_addr: 0x1400_1000, + size: 1, + }), + ); + + assert_eq!(state.resolve_runtime_pc(0x6000_0010), Some(0x1400_1000)); + assert_eq!( + state.remap_static_pc_to_runtime(0x1400_1010), + Some(0x6000_0020) + ); + } + + #[test] + fn test_resume_pending_exception_continuation_restores_rip() { + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + let (_, context_addr) = state.allocate_heap_region("context", 0x400); + state.mem_write( + &SymValue::concrete(context_addr.saturating_add(0x80), 64), + &SymValue::concrete(0x1122_3344_5566_7788, 64), + 8, + ); + state.mem_write( + &SymValue::concrete(context_addr.saturating_add(0xF8), 64), + &SymValue::concrete(0x6000_1234, 64), + 8, + ); + state.set_pending_exception(PendingExceptionContinuation { + handler_addr: 0x401000, + exception_code: 0x8000_0004, + exception_pointers_addr: 0x7000_0000, + exception_record_addr: 0x7000_0100, + context_addr, + }); + + let resumed = state + .resume_pending_exception_continuation() + .expect("continuation should resume"); + + assert_eq!(resumed, Some(0x6000_1234)); + assert_eq!( + state.get_register("RCX_0").as_concrete(), + Some(0x1122_3344_5566_7788) + ); + assert!(state.pending_exception().is_none()); + } + + #[test] + fn test_set_pending_exception_resume_pc_updates_context_rip() { + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + let (_, context_addr) = state.allocate_heap_region("context", 0x400); + state.set_pending_exception(PendingExceptionContinuation { + handler_addr: 0x401000, + exception_code: 0x8000_0004, + exception_pointers_addr: 0x7000_0000, + exception_record_addr: 0x7000_0100, + context_addr, + }); + + assert!(state.set_pending_exception_resume_pc(0x6000_5678)); + assert_eq!( + state + .mem_read( + &SymValue::concrete(context_addr.saturating_add(0xF8), 64), + 8 + ) + .as_concrete(), + Some(0x6000_5678) + ); + } + + #[test] + fn test_runtime_breakpoint_continuation_reenters_handler() { + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + let _ = state.define_runtime_region("jit", 0x6000_0000, 0x1000, true); + state.note_runtime_store_copy( + 0x6000_0000, + 0x1000, + Some(&RuntimeValueProvenance { + source_addr: 0x1400_0000, + size: 0x1000, + }), + ); + let (_, context_addr) = state.allocate_heap_region("context", 0x400); + state.mem_write( + &SymValue::concrete(context_addr.saturating_add(0x48), 64), + &SymValue::concrete(0x6000_0020, 64), + 8, + ); + state.mem_write( + &SymValue::concrete(context_addr.saturating_add(0x70), 64), + &SymValue::concrete(1, 64), + 8, + ); + state.mem_write( + &SymValue::concrete(context_addr.saturating_add(0xF8), 64), + &SymValue::concrete(0x6000_0010, 64), + 8, + ); + state.set_pending_exception(PendingExceptionContinuation { + handler_addr: 0x401000, + exception_code: 0x8000_0004, + exception_pointers_addr: 0x7000_0000, + exception_record_addr: 0x7000_0100, + context_addr, + }); + + let resumed = state + .resume_pending_exception_continuation() + .expect("runtime continuation should resume"); + + assert_eq!(resumed, Some(0x6000_0010)); + state.pc = 0x6000_0020; + assert!(state.dispatch_runtime_breakpoint_if_ready()); + assert_eq!(state.pc, 0x401000); + assert!(state.pending_exception().is_some()); + } + + #[test] + fn test_runtime_breakpoint_continuation_accepts_debug_context_flag() { + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + let _ = state.define_runtime_region("jit", 0x6000_0000, 0x1000, true); + state.note_runtime_store_copy( + 0x6000_0000, + 0x1000, + Some(&RuntimeValueProvenance { + source_addr: 0x1400_0000, + size: 0x1000, + }), + ); + let (_, context_addr) = state.allocate_heap_region("context", 0x400); + state.mem_write( + &SymValue::concrete(context_addr.saturating_add(0x30), 64), + &SymValue::concrete(0x100010, 32), + 4, + ); + state.mem_write( + &SymValue::concrete(context_addr.saturating_add(0x48), 64), + &SymValue::concrete(0x6000_0030, 64), + 8, + ); + state.mem_write( + &SymValue::concrete(context_addr.saturating_add(0xF8), 64), + &SymValue::concrete(0x6000_0010, 64), + 8, + ); + state.set_pending_exception(PendingExceptionContinuation { + handler_addr: 0x401000, + exception_code: 0x8000_0004, + exception_pointers_addr: 0x7000_0000, + exception_record_addr: 0x7000_0100, + context_addr, + }); + + assert_eq!( + state + .resume_pending_exception_continuation() + .expect("debug-register continuation should resume"), + Some(0x6000_0010) + ); + state.pc = 0x6000_0030; + assert!(state.dispatch_runtime_breakpoint_if_ready()); + assert_eq!(state.pc, 0x401000); + assert!(state.pending_exception().is_some()); + } } diff --git a/crates/r2sym/src/tactics.rs b/crates/r2sym/src/tactics.rs new file mode 100644 index 0000000..b1019b8 --- /dev/null +++ b/crates/r2sym/src/tactics.rs @@ -0,0 +1,1930 @@ +//! Solver tactics over canonical semantic evidence. +//! +//! Tactics must not claim proof. They only add typed constraints that bias +//! model extraction toward useful candidates; replay/verification remains the +//! authority for `solved`. + +use std::collections::{BTreeMap, BTreeSet}; + +use z3::ast::{BV, Bool}; + +use crate::constraints::{ + FinalConstraintGraph, RecurrenceAggregateConstraint, RecurrenceAggregateRangeConstraint, +}; +use crate::loops::{ + ExactLoopFoldEvidence, ExactLoopRecurrenceEvidence, ExactLoopRecurrenceKind, LoopFoldOperation, + LoopMemoryTerm, LoopMemoryTermKind, LoopRotateDirection, +}; +use crate::{SymState, SymValue, aggregate_exact_fold_bytes}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum InputByteDomain { + Any, + PrintableAscii, + AlphaNum, + Ranges(Vec<(u8, u8)>), + Bytes(Vec), +} + +impl InputByteDomain { + pub fn printable_ascii() -> Self { + Self::PrintableAscii + } + + pub fn ranges(&self) -> Vec<(u8, u8)> { + match self { + Self::Any => vec![(0x00, 0xff)], + Self::PrintableAscii => vec![(0x20, 0x7e)], + Self::AlphaNum => vec![(b'0', b'9'), (b'A', b'Z'), (b'a', b'z')], + Self::Ranges(ranges) => normalize_ranges(ranges.iter().copied()), + Self::Bytes(bytes) => normalize_ranges(bytes.iter().copied().map(|byte| (byte, byte))), + } + } + + fn is_any(&self) -> bool { + matches!(self, Self::Any) + } + + pub fn allowed_bytes(&self) -> Vec { + self.ranges() + .into_iter() + .flat_map(|(start, end)| start..=end) + .collect() + } + + pub fn contains(&self, byte: u8) -> bool { + self.ranges() + .into_iter() + .any(|(start, end)| byte >= start && byte <= end) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SolveTacticConfig { + pub enabled: bool, + pub preferred_domains: Vec, + pub max_constrained_bytes: usize, + pub max_candidates: usize, + pub max_mitm_table: usize, + pub max_target_enumeration: usize, +} + +impl Default for SolveTacticConfig { + fn default() -> Self { + Self { + enabled: true, + preferred_domains: vec![InputByteDomain::PrintableAscii, InputByteDomain::Any], + max_constrained_bytes: 256, + max_candidates: 32, + max_mitm_table: 32_768, + max_target_enumeration: 256, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum ConstraintCandidateStrategy { + Algebraic, + Mitm, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TacticConstraintReport { + pub constrained_bytes: usize, + pub skipped_reasons: Vec, +} + +impl TacticConstraintReport { + fn new(constrained_bytes: usize, skipped_reasons: Vec) -> Self { + Self { + constrained_bytes, + skipped_reasons, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SolveTacticCandidate { + pub recurrence: ExactLoopRecurrenceEvidence, + pub domain: InputByteDomain, + pub bytes: Vec, + pub target: u64, + pub used_mitm: bool, + pub reason: String, +} + +pub fn constrain_exact_fold_inputs<'ctx>( + state: &mut SymState<'ctx>, + folds: &[ExactLoopFoldEvidence], + domain: &InputByteDomain, + max_constrained_bytes: usize, +) -> TacticConstraintReport { + if domain.is_any() { + return TacticConstraintReport::new(0, Vec::new()); + } + + let mut addresses = BTreeSet::new(); + let mut skipped_reasons = Vec::new(); + for fold in folds { + if fold.term.kind != LoopMemoryTermKind::InputRead { + continue; + } + if fold.term.bytes != 1 { + push_unique( + &mut skipped_reasons, + format!("unsupported_fold_read_width:{}", fold.term.bytes), + ); + continue; + } + let (Some(base), Some(stride)) = (fold.term.base, fold.term.stride) else { + push_unique(&mut skipped_reasons, "missing_fold_input_address"); + continue; + }; + for iteration in 0..fold.iterations { + if addresses.len() >= max_constrained_bytes { + push_unique(&mut skipped_reasons, "tactic_input_byte_budget"); + break; + } + let Some(offset) = iteration.checked_mul(stride) else { + push_unique(&mut skipped_reasons, "fold_input_address_overflow"); + break; + }; + let Some(addr) = base.checked_add(offset) else { + push_unique(&mut skipped_reasons, "fold_input_address_overflow"); + break; + }; + addresses.insert(addr); + } + } + + let constrained_bytes = addresses.len(); + for addr in addresses { + let value = state.mem_read(&SymValue::concrete(addr, 64), 1); + constrain_byte_to_domain(state, &value, domain); + } + + TacticConstraintReport::new(constrained_bytes, skipped_reasons) +} + +pub fn constrain_exact_fold_candidate<'ctx>( + state: &mut SymState<'ctx>, + fold: &ExactLoopFoldEvidence, + bytes: &[u8], +) -> TacticConstraintReport { + let mut skipped_reasons = Vec::new(); + if fold.term.kind != LoopMemoryTermKind::InputRead { + return TacticConstraintReport::new(0, skipped_reasons); + } + if fold.term.bytes != 1 { + push_unique( + &mut skipped_reasons, + format!("unsupported_fold_read_width:{}", fold.term.bytes), + ); + return TacticConstraintReport::new(0, skipped_reasons); + } + if bytes.len() != fold.iterations as usize { + push_unique(&mut skipped_reasons, "candidate_length_mismatch"); + return TacticConstraintReport::new(0, skipped_reasons); + } + let (Some(base), Some(stride)) = (fold.term.base, fold.term.stride) else { + push_unique(&mut skipped_reasons, "missing_fold_input_address"); + return TacticConstraintReport::new(0, skipped_reasons); + }; + for (iteration, byte) in bytes.iter().copied().enumerate() { + let Some(offset) = (iteration as u64).checked_mul(stride) else { + push_unique(&mut skipped_reasons, "fold_input_address_overflow"); + continue; + }; + let Some(addr) = base.checked_add(offset) else { + push_unique(&mut skipped_reasons, "fold_input_address_overflow"); + continue; + }; + let value = state.mem_read(&SymValue::concrete(addr, 64), 1); + state.constrain_eq(&value, byte as u64); + } + TacticConstraintReport::new(bytes.len(), skipped_reasons) +} + +fn constrain_input_term_candidate<'ctx>( + state: &mut SymState<'ctx>, + term: &LoopMemoryTerm, + iterations: u64, + bytes: &[u8], +) -> TacticConstraintReport { + let mut skipped_reasons = Vec::new(); + if term.kind != LoopMemoryTermKind::InputRead { + return TacticConstraintReport::new(0, skipped_reasons); + } + if term.bytes != 1 { + push_unique( + &mut skipped_reasons, + format!("unsupported_recurrence_read_width:{}", term.bytes), + ); + return TacticConstraintReport::new(0, skipped_reasons); + } + if bytes.len() != iterations as usize { + push_unique(&mut skipped_reasons, "candidate_length_mismatch"); + return TacticConstraintReport::new(0, skipped_reasons); + } + let (Some(base), Some(stride)) = (term.base, term.stride) else { + push_unique(&mut skipped_reasons, "missing_recurrence_input_address"); + return TacticConstraintReport::new(0, skipped_reasons); + }; + for (iteration, byte) in bytes.iter().copied().enumerate() { + let Some(offset) = (iteration as u64).checked_mul(stride) else { + push_unique(&mut skipped_reasons, "recurrence_input_address_overflow"); + continue; + }; + let Some(addr) = base.checked_add(offset) else { + push_unique(&mut skipped_reasons, "recurrence_input_address_overflow"); + continue; + }; + let value = state.mem_read(&SymValue::concrete(addr, 64), 1); + state.constrain_eq(&value, byte as u64); + } + TacticConstraintReport::new(bytes.len(), skipped_reasons) +} + +fn recurrence_input_term(recurrence: &ExactLoopRecurrenceEvidence) -> Option<&LoopMemoryTerm> { + match &recurrence.kind { + ExactLoopRecurrenceKind::Fold { term, .. } + | ExactLoopRecurrenceKind::RotateMix { term, .. } => Some(term), + _ => None, + } +} + +pub fn constrain_exact_recurrence_candidate<'ctx>( + state: &mut SymState<'ctx>, + recurrence: &ExactLoopRecurrenceEvidence, + bytes: &[u8], +) -> TacticConstraintReport { + let Some(term) = recurrence_input_term(recurrence) else { + return TacticConstraintReport::new( + 0, + vec!["unsupported_recurrence_candidate".to_string()], + ); + }; + constrain_input_term_candidate(state, term, recurrence.iterations, bytes) +} + +pub fn algebraic_preimage_candidate( + fold: &ExactLoopFoldEvidence, + model_bytes: &[u8], + domain: &InputByteDomain, +) -> Option> { + if fold.term.kind != LoopMemoryTermKind::InputRead || fold.term.bytes != 1 { + return None; + } + if model_bytes.len() != fold.iterations as usize || model_bytes.is_empty() { + return None; + } + match fold.operation { + LoopFoldOperation::Xor => xor_preimage_candidate(model_bytes, domain), + LoopFoldOperation::Add => add_preimage_candidate(model_bytes, domain, fold.bits), + } +} + +pub fn algebraic_preimage_for_target( + fold: &ExactLoopFoldEvidence, + target: u64, + domain: &InputByteDomain, +) -> Option> { + if fold.term.kind != LoopMemoryTermKind::InputRead || fold.term.bytes != 1 { + return None; + } + if fold.iterations == 0 { + return None; + } + match fold.operation { + LoopFoldOperation::Xor => xor_preimage_for_target(fold.iterations as usize, target, domain), + LoopFoldOperation::Add => { + add_preimage_for_target(fold.iterations as usize, target, domain, fold.bits) + } + } +} + +pub fn tactic_candidates_for_constraint_graph<'ctx>( + graph: &FinalConstraintGraph, + state: Option<&SymState<'ctx>>, + config: &SolveTacticConfig, +) -> Vec { + if !config.enabled || graph.is_empty() { + return Vec::new(); + } + let mut candidates = Vec::new(); + for constraint in graph.recurrence_aggregate_constraints() { + for domain in &config.preferred_domains { + if candidates.len() >= config.max_candidates { + return candidates; + } + if let Some(candidate) = + tactic_candidate_for_recurrence_constraint(constraint, graph, state, domain, config) + { + candidates.push(candidate); + } + } + } + for constraint in graph.recurrence_aggregate_range_constraints() { + for domain in &config.preferred_domains { + if candidates.len() >= config.max_candidates { + return candidates; + } + if let Some(candidate) = tactic_candidate_for_recurrence_range_constraint( + constraint, graph, state, domain, config, + ) { + candidates.push(candidate); + } + } + } + candidates +} + +fn xor_preimage_candidate(model_bytes: &[u8], domain: &InputByteDomain) -> Option> { + let target = model_bytes.iter().fold(0u8, |acc, byte| acc ^ byte); + xor_preimage_for_target(model_bytes.len(), target as u64, domain) +} + +fn xor_preimage_for_target(count: usize, target: u64, domain: &InputByteDomain) -> Option> { + let allowed = domain.allowed_bytes(); + if allowed.is_empty() { + return None; + } + let target = target as u8; + if count == 1 { + return domain.contains(target).then_some(vec![target]); + } + + let preferred = preferred_byte(&allowed); + let mut candidate = vec![preferred; count]; + let prefix_xor = candidate + .iter() + .take(candidate.len().saturating_sub(2)) + .fold(0u8, |acc, byte| acc ^ byte); + let pair_target = target ^ prefix_xor; + for &lhs in &allowed { + let rhs = lhs ^ pair_target; + if domain.contains(rhs) { + let last = candidate.len() - 1; + candidate[last - 1] = lhs; + candidate[last] = rhs; + return Some(candidate); + } + } + None +} + +fn add_preimage_candidate( + model_bytes: &[u8], + domain: &InputByteDomain, + bits: u32, +) -> Option> { + let target = model_bytes + .iter() + .fold(0u128, |acc, byte| acc.saturating_add(*byte as u128)); + add_preimage_for_target(model_bytes.len(), target as u64, domain, bits) +} + +fn add_preimage_for_target( + count: usize, + target: u64, + domain: &InputByteDomain, + bits: u32, +) -> Option> { + let allowed = domain.allowed_bytes(); + if allowed.is_empty() { + return None; + } + let modulus = if bits >= 64 { + None + } else { + Some(1u128 << bits.max(1)) + }; + let target = target as u128; + let target_mod = modulus.map_or(target, |modulus| target % modulus); + let min_byte = *allowed.first()? as u128; + let max_byte = *allowed.last()? as u128; + let min_sum = min_byte.saturating_mul(count as u128); + let max_sum = max_byte.saturating_mul(count as u128); + let target_sum = match modulus { + Some(modulus) => { + let mut candidate = target_mod; + while candidate < min_sum { + candidate = candidate.saturating_add(modulus); + } + if candidate > max_sum { + return None; + } + candidate + } + None if target_mod >= min_sum && target_mod <= max_sum => target_mod, + None => return None, + }; + + let mut remaining = target_sum; + let mut candidate = Vec::with_capacity(count); + for index in 0..count { + let slots_left = count - index - 1; + let min_tail = min_byte.saturating_mul(slots_left as u128); + let max_tail = max_byte.saturating_mul(slots_left as u128); + let byte = allowed.iter().copied().find(|byte| { + let value = *byte as u128; + value <= remaining + && remaining.saturating_sub(value) >= min_tail + && remaining.saturating_sub(value) <= max_tail + })?; + candidate.push(byte); + remaining = remaining.saturating_sub(byte as u128); + } + (remaining == 0).then_some(candidate) +} + +fn tactic_candidate_for_recurrence_constraint( + constraint: &RecurrenceAggregateConstraint, + graph: &FinalConstraintGraph, + state: Option<&SymState<'_>>, + domain: &InputByteDomain, + config: &SolveTacticConfig, +) -> Option { + let allowed = allowed_bytes_for_recurrence(graph, &constraint.recurrence, domain)?; + let (bytes, strategy) = candidate_for_recurrence_target_with_allowed( + &constraint.recurrence, + state, + constraint.target, + &allowed, + config, + )?; + Some(SolveTacticCandidate { + recurrence: constraint.recurrence.clone(), + domain: domain.clone(), + bytes, + target: constraint.target, + used_mitm: strategy == ConstraintCandidateStrategy::Mitm, + reason: tactic_reason_for_recurrence(&constraint.recurrence, graph, "exact", strategy), + }) +} + +fn tactic_candidate_for_recurrence_range_constraint( + constraint: &RecurrenceAggregateRangeConstraint, + graph: &FinalConstraintGraph, + state: Option<&SymState<'_>>, + domain: &InputByteDomain, + config: &SolveTacticConfig, +) -> Option { + let allowed = allowed_bytes_for_recurrence(graph, &constraint.recurrence, domain)?; + let (target, bytes, strategy) = select_target_for_range_constraint( + &constraint.recurrence, + state, + &allowed, + constraint, + config, + )?; + Some(SolveTacticCandidate { + recurrence: constraint.recurrence.clone(), + domain: domain.clone(), + bytes, + target, + used_mitm: strategy == ConstraintCandidateStrategy::Mitm, + reason: tactic_reason_for_recurrence(&constraint.recurrence, graph, "range", strategy), + }) +} + +fn tactic_reason_for_recurrence( + recurrence: &ExactLoopRecurrenceEvidence, + graph: &FinalConstraintGraph, + prefix: &str, + strategy: ConstraintCandidateStrategy, +) -> String { + let strategy = match strategy { + ConstraintCandidateStrategy::Algebraic => "algebraic", + ConstraintCandidateStrategy::Mitm => "mitm", + }; + let base = match &recurrence.kind { + ExactLoopRecurrenceKind::Fold { operation, .. } => match operation { + LoopFoldOperation::Xor => { + format!("{prefix} {strategy} xor-fold preimage from constraint graph") + } + LoopFoldOperation::Add => { + format!("{prefix} {strategy} add-fold preimage from constraint graph") + } + }, + ExactLoopRecurrenceKind::RotateMix { operation, .. } => match operation { + LoopFoldOperation::Xor => { + format!("{prefix} {strategy} rotate-xor recurrence preimage from constraint graph") + } + LoopFoldOperation::Add => { + format!("{prefix} {strategy} rotate-add recurrence preimage from constraint graph") + } + }, + _ => format!("{prefix} {strategy} exact recurrence candidate from constraint graph"), + }; + if graph.input_byte_constraints.is_empty() { + base + } else { + format!("{base}; input assumptions applied") + } +} + +fn select_target_for_range_constraint( + recurrence: &ExactLoopRecurrenceEvidence, + state: Option<&SymState<'_>>, + allowed: &[Vec], + constraint: &RecurrenceAggregateRangeConstraint, + config: &SolveTacticConfig, +) -> Option<(u64, Vec, ConstraintCandidateStrategy)> { + let preferred = preferred_candidate_bytes(allowed)?; + let preferred_target = recurrence_target_for_bytes(recurrence, state, &preferred)?; + if preferred_target >= constraint.min + && preferred_target <= constraint.max + && let Some((bytes, strategy)) = candidate_for_recurrence_target_with_allowed( + recurrence, + state, + preferred_target, + allowed, + config, + ) + { + return Some((preferred_target, bytes, strategy)); + } + let budget = config.max_target_enumeration.max(1) as u64; + let upper = constraint + .max + .min(constraint.min.saturating_add(budget.saturating_sub(1))); + (constraint.min..=upper).find_map(|target| { + candidate_for_recurrence_target_with_allowed(recurrence, state, target, allowed, config) + .map(|(bytes, strategy)| (target, bytes, strategy)) + }) +} + +fn preferred_candidate_bytes(allowed: &[Vec]) -> Option> { + let mut out = Vec::with_capacity(allowed.len()); + for choices in allowed { + out.push(preferred_byte(choices)); + } + Some(out) +} + +fn allowed_bytes_for_recurrence( + graph: &FinalConstraintGraph, + recurrence: &ExactLoopRecurrenceEvidence, + default_domain: &InputByteDomain, +) -> Option>> { + let term = recurrence_input_term(recurrence)?; + if term.kind != LoopMemoryTermKind::InputRead || term.bytes != 1 { + return None; + } + let (Some(base), Some(stride)) = (term.base, term.stride) else { + return None; + }; + if stride == 0 { + return None; + } + if let Some(length) = graph + .input_length_constraints + .iter() + .find(|constraint| constraint.base_addr == base) + && length.len != recurrence.iterations as u32 + { + return None; + } + + let mut allowed = vec![default_domain.allowed_bytes(); recurrence.iterations as usize]; + let by_addr = graph.input_byte_constraints.iter().fold( + BTreeMap::>::new(), + |mut acc, constraint| { + let mut next = constraint.allowed.clone(); + next.sort_unstable(); + next.dedup(); + acc.entry(constraint.addr) + .and_modify(|existing| { + let existing_set = existing.iter().copied().collect::>(); + let next_set = next.iter().copied().collect::>(); + *existing = existing_set + .intersection(&next_set) + .copied() + .collect::>(); + }) + .or_insert(next); + acc + }, + ); + + for (index, bytes) in allowed.iter_mut().enumerate() { + let addr = base.checked_add((index as u64).checked_mul(stride)?)?; + if let Some(constrained) = by_addr.get(&addr) { + let constrained_set = constrained.iter().copied().collect::>(); + *bytes = bytes + .iter() + .copied() + .filter(|byte| constrained_set.contains(byte)) + .collect(); + } + bytes.sort_unstable(); + bytes.dedup(); + if bytes.is_empty() { + return None; + } + } + Some(allowed) +} + +fn candidate_for_recurrence_target_with_allowed( + recurrence: &ExactLoopRecurrenceEvidence, + state: Option<&SymState<'_>>, + target: u64, + allowed: &[Vec], + config: &SolveTacticConfig, +) -> Option<(Vec, ConstraintCandidateStrategy)> { + if let Some(fold) = recurrence.as_fold() { + return candidate_for_target_with_allowed(&fold, target, allowed, config); + } + let initial = concrete_initial_for_recurrence(state, recurrence)?; + if let Some(bytes) = + rotate_mix_preimage_for_target_with_allowed(recurrence, initial, target, allowed) + { + return Some((bytes, ConstraintCandidateStrategy::Algebraic)); + } + rotate_mix_mitm_preimage_for_target_with_allowed(recurrence, initial, target, allowed, config) + .map(|bytes| (bytes, ConstraintCandidateStrategy::Mitm)) +} + +fn recurrence_target_for_bytes( + recurrence: &ExactLoopRecurrenceEvidence, + state: Option<&SymState<'_>>, + bytes: &[u8], +) -> Option { + if let Some(fold) = recurrence.as_fold() { + return aggregate_exact_fold_bytes(&fold, bytes); + } + let initial = concrete_initial_for_recurrence(state, recurrence)?; + rotate_mix_apply_bytes(recurrence, initial, bytes) +} + +fn concrete_initial_for_recurrence( + state: Option<&SymState<'_>>, + recurrence: &ExactLoopRecurrenceEvidence, +) -> Option { + let initial_name = recurrence.initial.as_str(); + if initial_name.is_empty() { + return None; + } + let value = state?.get_register_sized(initial_name, recurrence.bits.max(1)); + value + .as_concrete() + .map(|value| mask_to_bits(value, recurrence.bits)) +} + +fn candidate_for_target_with_allowed( + fold: &ExactLoopFoldEvidence, + target: u64, + allowed: &[Vec], + config: &SolveTacticConfig, +) -> Option<(Vec, ConstraintCandidateStrategy)> { + if let Some(bytes) = algebraic_preimage_for_target_with_allowed(fold, target, allowed) { + return Some((bytes, ConstraintCandidateStrategy::Algebraic)); + } + mitm_preimage_for_target_with_allowed(fold, target, allowed, config) + .map(|bytes| (bytes, ConstraintCandidateStrategy::Mitm)) +} + +fn algebraic_preimage_for_target_with_allowed( + fold: &ExactLoopFoldEvidence, + target: u64, + allowed: &[Vec], +) -> Option> { + if fold.iterations as usize != allowed.len() { + return None; + } + match fold.operation { + LoopFoldOperation::Xor => xor_preimage_for_target_with_allowed(allowed, target), + LoopFoldOperation::Add => add_preimage_for_target_with_allowed(allowed, target, fold.bits), + } +} + +fn xor_preimage_for_target_with_allowed(allowed: &[Vec], target: u64) -> Option> { + if allowed.is_empty() { + return None; + } + if allowed.len() == 1 { + let target = target as u8; + return allowed[0].contains(&target).then_some(vec![target]); + } + let mut candidate = preferred_candidate_bytes(allowed)?; + let prefix_xor = candidate + .iter() + .take(candidate.len().saturating_sub(2)) + .fold(0u8, |acc, byte| acc ^ byte); + let pair_target = (target as u8) ^ prefix_xor; + let penultimate = candidate.len() - 2; + let last = candidate.len() - 1; + let final_allowed = allowed[last].iter().copied().collect::>(); + for &lhs in &allowed[penultimate] { + let rhs = lhs ^ pair_target; + if final_allowed.contains(&rhs) { + candidate[penultimate] = lhs; + candidate[last] = rhs; + return Some(candidate); + } + } + None +} + +fn add_preimage_for_target_with_allowed( + allowed: &[Vec], + target: u64, + bits: u32, +) -> Option> { + if allowed.is_empty() { + return None; + } + let modulus = if bits >= 64 { + None + } else { + Some(1u128 << bits.max(1)) + }; + let target = target as u128; + let target_mod = modulus.map_or(target, |modulus| target % modulus); + let min_sum = allowed + .iter() + .map(|bytes| *bytes.first().unwrap_or(&0) as u128) + .sum::(); + let max_sum = allowed + .iter() + .map(|bytes| *bytes.last().unwrap_or(&0) as u128) + .sum::(); + let target_sum = match modulus { + Some(modulus) => { + let mut candidate = target_mod; + while candidate < min_sum { + candidate = candidate.saturating_add(modulus); + } + if candidate > max_sum { + return None; + } + candidate + } + None if target_mod >= min_sum && target_mod <= max_sum => target_mod, + None => return None, + }; + let mut min_suffix = vec![0u128; allowed.len() + 1]; + let mut max_suffix = vec![0u128; allowed.len() + 1]; + for index in (0..allowed.len()).rev() { + min_suffix[index] = min_suffix[index + 1] + *allowed[index].first()? as u128; + max_suffix[index] = max_suffix[index + 1] + *allowed[index].last()? as u128; + } + let mut remaining = target_sum; + let mut candidate = Vec::with_capacity(allowed.len()); + for (index, bytes) in allowed.iter().enumerate() { + let min_tail = min_suffix[index + 1]; + let max_tail = max_suffix[index + 1]; + let byte = bytes.iter().copied().find(|byte| { + let value = *byte as u128; + value <= remaining + && remaining.saturating_sub(value) >= min_tail + && remaining.saturating_sub(value) <= max_tail + })?; + candidate.push(byte); + remaining = remaining.saturating_sub(byte as u128); + } + (remaining == 0).then_some(candidate) +} + +fn rotate_mix_preimage_for_target_with_allowed( + recurrence: &ExactLoopRecurrenceEvidence, + initial: u64, + target: u64, + allowed: &[Vec], +) -> Option> { + let spec = rotate_mix_spec(recurrence)?; + if allowed.len() != recurrence.iterations as usize || allowed.is_empty() { + return None; + } + let mut candidate = preferred_candidate_bytes(allowed)?; + let last = candidate.len() - 1; + let prefix_acc = rotate_mix_apply_prefix(initial, &candidate[..last], recurrence.bits, spec)?; + if let Some(byte) = rotate_mix_required_final_byte(prefix_acc, target, recurrence.bits, spec) + && allowed[last].contains(&byte) + { + candidate[last] = byte; + return Some(candidate); + } + if candidate.len() < 2 { + return None; + } + let penultimate = candidate.len() - 2; + let prefix_acc = + rotate_mix_apply_prefix(initial, &candidate[..penultimate], recurrence.bits, spec)?; + let last_allowed = allowed[last].iter().copied().collect::>(); + for &byte in &allowed[penultimate] { + let after_penultimate = rotate_mix_apply_step(prefix_acc, byte, recurrence.bits, spec); + let Some(last_byte) = + rotate_mix_required_final_byte(after_penultimate, target, recurrence.bits, spec) + else { + continue; + }; + if last_allowed.contains(&last_byte) { + candidate[penultimate] = byte; + candidate[last] = last_byte; + return Some(candidate); + } + } + None +} + +fn mitm_preimage_for_target_with_allowed( + fold: &ExactLoopFoldEvidence, + target: u64, + allowed: &[Vec], + config: &SolveTacticConfig, +) -> Option> { + if allowed.len() < 2 || config.max_mitm_table == 0 { + return None; + } + match fold.operation { + LoopFoldOperation::Xor => { + xor_mitm_preimage_for_target_with_allowed(allowed, target, config.max_mitm_table) + } + LoopFoldOperation::Add => { + add_mitm_preimage_for_target_with_allowed(allowed, target, fold.bits, config) + } + } +} + +fn rotate_mix_mitm_preimage_for_target_with_allowed( + recurrence: &ExactLoopRecurrenceEvidence, + initial: u64, + target: u64, + allowed: &[Vec], + config: &SolveTacticConfig, +) -> Option> { + if allowed.len() < 2 || config.max_mitm_table == 0 { + return None; + } + let spec = rotate_mix_spec(recurrence)?; + let split = allowed.len() / 2; + let (left, right) = allowed.split_at(split); + if left.is_empty() || right.is_empty() { + return None; + } + let left_table = + build_rotate_mix_half_table(left, initial, recurrence.bits, spec, config.max_mitm_table)?; + search_rotate_mix_half_table(right, target, recurrence.bits, spec, &left_table) +} + +fn xor_mitm_preimage_for_target_with_allowed( + allowed: &[Vec], + target: u64, + max_table: usize, +) -> Option> { + let split = allowed.len() / 2; + let (left, right) = allowed.split_at(split); + if left.is_empty() || right.is_empty() { + return None; + } + let left_table = build_xor_half_table(left, max_table)?; + search_xor_half_table(right, target as u8, &left_table) +} + +fn build_xor_half_table(allowed: &[Vec], max_table: usize) -> Option>> { + fn recurse( + allowed: &[Vec], + index: usize, + aggregate: u8, + current: &mut Vec, + table: &mut BTreeMap>, + max_table: usize, + ) -> bool { + if index == allowed.len() { + table.entry(aggregate).or_insert_with(|| current.clone()); + return table.len() <= max_table; + } + for &byte in &allowed[index] { + current.push(byte); + if !recurse( + allowed, + index + 1, + aggregate ^ byte, + current, + table, + max_table, + ) { + current.pop(); + return false; + } + current.pop(); + } + true + } + + let mut table = BTreeMap::new(); + let mut current = Vec::with_capacity(allowed.len()); + recurse(allowed, 0, 0, &mut current, &mut table, max_table).then_some(table) +} + +fn search_xor_half_table( + allowed: &[Vec], + target: u8, + left_table: &BTreeMap>, +) -> Option> { + fn recurse( + allowed: &[Vec], + index: usize, + aggregate: u8, + target: u8, + current: &mut Vec, + left_table: &BTreeMap>, + ) -> Option> { + if index == allowed.len() { + let needed = target ^ aggregate; + let mut candidate = left_table.get(&needed)?.clone(); + candidate.extend_from_slice(current); + return Some(candidate); + } + for &byte in &allowed[index] { + current.push(byte); + if let Some(candidate) = recurse( + allowed, + index + 1, + aggregate ^ byte, + target, + current, + left_table, + ) { + return Some(candidate); + } + current.pop(); + } + None + } + + let mut current = Vec::with_capacity(allowed.len()); + recurse(allowed, 0, 0, target, &mut current, left_table) +} + +fn add_mitm_preimage_for_target_with_allowed( + allowed: &[Vec], + target: u64, + bits: u32, + config: &SolveTacticConfig, +) -> Option> { + let split = allowed.len() / 2; + let (left, right) = allowed.split_at(split); + if left.is_empty() || right.is_empty() { + return None; + } + let modulus = add_fold_modulus(bits); + let target = modulus.map_or(target as u128, |modulus| (target as u128) % modulus); + let left_table = build_add_half_table(left, modulus, config.max_mitm_table)?; + search_add_half_table(right, target, modulus, &left_table) +} + +fn add_fold_modulus(bits: u32) -> Option { + if bits >= 64 { + None + } else { + Some(1u128 << bits.max(1)) + } +} + +fn add_fold_step(aggregate: u128, byte: u8, modulus: Option) -> u128 { + let next = aggregate + byte as u128; + modulus.map_or(next, |modulus| next % modulus) +} + +fn build_add_half_table( + allowed: &[Vec], + modulus: Option, + max_table: usize, +) -> Option>> { + fn recurse( + allowed: &[Vec], + index: usize, + aggregate: u128, + modulus: Option, + current: &mut Vec, + table: &mut BTreeMap>, + max_table: usize, + ) -> bool { + if index == allowed.len() { + table.entry(aggregate).or_insert_with(|| current.clone()); + return table.len() <= max_table; + } + for &byte in &allowed[index] { + current.push(byte); + let next = add_fold_step(aggregate, byte, modulus); + if !recurse(allowed, index + 1, next, modulus, current, table, max_table) { + current.pop(); + return false; + } + current.pop(); + } + true + } + + let mut table = BTreeMap::new(); + let mut current = Vec::with_capacity(allowed.len()); + recurse(allowed, 0, 0, modulus, &mut current, &mut table, max_table).then_some(table) +} + +fn search_add_half_table( + allowed: &[Vec], + target: u128, + modulus: Option, + left_table: &BTreeMap>, +) -> Option> { + fn recurse( + allowed: &[Vec], + index: usize, + aggregate: u128, + target: u128, + modulus: Option, + current: &mut Vec, + left_table: &BTreeMap>, + ) -> Option> { + if index == allowed.len() { + let needed = match modulus { + Some(modulus) => (target + modulus - (aggregate % modulus)) % modulus, + None if aggregate <= target => target - aggregate, + None => return None, + }; + let mut candidate = left_table.get(&needed)?.clone(); + candidate.extend_from_slice(current); + return Some(candidate); + } + for &byte in &allowed[index] { + current.push(byte); + let next = add_fold_step(aggregate, byte, modulus); + if let Some(candidate) = recurse( + allowed, + index + 1, + next, + target, + modulus, + current, + left_table, + ) { + return Some(candidate); + } + current.pop(); + } + None + } + + let mut current = Vec::with_capacity(allowed.len()); + recurse(allowed, 0, 0, target, modulus, &mut current, left_table) +} + +#[derive(Clone, Copy)] +struct RotateMixSpec { + direction: LoopRotateDirection, + amount: u32, + operation: LoopFoldOperation, +} + +fn rotate_mix_spec(recurrence: &ExactLoopRecurrenceEvidence) -> Option { + let ExactLoopRecurrenceKind::RotateMix { + direction, + amount, + operation, + .. + } = &recurrence.kind + else { + return None; + }; + Some(RotateMixSpec { + direction: *direction, + amount: *amount, + operation: *operation, + }) +} + +fn rotate_mix_apply_bytes( + recurrence: &ExactLoopRecurrenceEvidence, + initial: u64, + bytes: &[u8], +) -> Option { + let spec = rotate_mix_spec(recurrence)?; + rotate_mix_apply_prefix(initial, bytes, recurrence.bits, spec) +} + +fn rotate_mix_apply_prefix( + initial: u64, + bytes: &[u8], + bits: u32, + spec: RotateMixSpec, +) -> Option { + let mut acc = mask_to_bits(initial, bits); + for &byte in bytes { + acc = rotate_mix_apply_step(acc, byte, bits, spec); + } + Some(acc) +} + +fn rotate_mix_apply_step(acc: u64, byte: u8, bits: u32, spec: RotateMixSpec) -> u64 { + let rotated = rotate_bits(acc, spec.amount, bits, spec.direction); + let byte = u64::from(byte); + match spec.operation { + LoopFoldOperation::Xor => mask_to_bits(rotated ^ byte, bits), + LoopFoldOperation::Add => mask_to_bits(rotated.wrapping_add(byte), bits), + } +} + +fn rotate_mix_required_final_byte( + acc_before_final: u64, + target: u64, + bits: u32, + spec: RotateMixSpec, +) -> Option { + let rotated = rotate_bits(acc_before_final, spec.amount, bits, spec.direction); + let needed = match spec.operation { + LoopFoldOperation::Xor => mask_to_bits(target ^ rotated, bits), + LoopFoldOperation::Add => mask_to_bits(target.wrapping_sub(rotated), bits), + }; + (needed <= u8::MAX as u64).then_some(needed as u8) +} + +fn rotate_mix_reverse_step(acc_after: u64, byte: u8, bits: u32, spec: RotateMixSpec) -> u64 { + let unrotated = match spec.operation { + LoopFoldOperation::Xor => mask_to_bits(acc_after ^ u64::from(byte), bits), + LoopFoldOperation::Add => mask_to_bits(acc_after.wrapping_sub(u64::from(byte)), bits), + }; + rotate_bits( + unrotated, + spec.amount, + bits, + match spec.direction { + LoopRotateDirection::Left => LoopRotateDirection::Right, + LoopRotateDirection::Right => LoopRotateDirection::Left, + }, + ) +} + +fn build_rotate_mix_half_table( + allowed: &[Vec], + initial: u64, + bits: u32, + spec: RotateMixSpec, + max_table: usize, +) -> Option>> { + struct Env<'a> { + allowed: &'a [Vec], + bits: u32, + spec: RotateMixSpec, + max_table: usize, + } + + fn recurse( + env: &Env<'_>, + index: usize, + acc: u64, + current: &mut Vec, + table: &mut BTreeMap>, + ) -> bool { + if index == env.allowed.len() { + table.entry(acc).or_insert_with(|| current.clone()); + return table.len() <= env.max_table; + } + for &byte in &env.allowed[index] { + current.push(byte); + let next = rotate_mix_apply_step(acc, byte, env.bits, env.spec); + if !recurse(env, index + 1, next, current, table) { + current.pop(); + return false; + } + current.pop(); + } + true + } + + let env = Env { + allowed, + bits, + spec, + max_table, + }; + let mut table = BTreeMap::new(); + let mut current = Vec::with_capacity(allowed.len()); + recurse( + &env, + 0, + mask_to_bits(initial, bits), + &mut current, + &mut table, + ) + .then_some(table) +} + +fn search_rotate_mix_half_table( + allowed: &[Vec], + target: u64, + bits: u32, + spec: RotateMixSpec, + left_table: &BTreeMap>, +) -> Option> { + fn recurse( + allowed: &[Vec], + index: usize, + acc_after: u64, + bits: u32, + spec: RotateMixSpec, + current_rev: &mut Vec, + left_table: &BTreeMap>, + ) -> Option> { + if index == 0 { + let mut candidate = left_table.get(&acc_after)?.clone(); + current_rev.reverse(); + candidate.extend_from_slice(current_rev); + current_rev.reverse(); + return Some(candidate); + } + let choices = &allowed[index - 1]; + for &byte in choices { + current_rev.push(byte); + let prev = rotate_mix_reverse_step(acc_after, byte, bits, spec); + if let Some(candidate) = recurse( + allowed, + index - 1, + prev, + bits, + spec, + current_rev, + left_table, + ) { + return Some(candidate); + } + current_rev.pop(); + } + None + } + + let mut current_rev = Vec::with_capacity(allowed.len()); + recurse( + allowed, + allowed.len(), + mask_to_bits(target, bits), + bits, + spec, + &mut current_rev, + left_table, + ) +} + +fn rotate_bits(value: u64, amount: u32, bits: u32, direction: LoopRotateDirection) -> u64 { + let bits = bits.clamp(1, 64); + let value = mask_to_bits(value, bits); + let amount = normalize_rotate_amount(amount, bits); + if amount == 0 { + return value; + } + if bits == 64 { + return match direction { + LoopRotateDirection::Left => value.rotate_left(amount), + LoopRotateDirection::Right => value.rotate_right(amount), + }; + } + let lhs = match direction { + LoopRotateDirection::Left => value.wrapping_shl(amount), + LoopRotateDirection::Right => value >> amount, + }; + let rhs = match direction { + LoopRotateDirection::Left => value >> (bits - amount), + LoopRotateDirection::Right => value.wrapping_shl(bits - amount), + }; + mask_to_bits(lhs | rhs, bits) +} + +fn normalize_rotate_amount(amount: u32, bits: u32) -> u32 { + if bits == 0 { 0 } else { amount % bits.min(64) } +} + +fn mask_to_bits(value: u64, bits: u32) -> u64 { + if bits >= 64 { + value + } else if bits == 0 { + 0 + } else { + value & ((1u64 << bits) - 1) + } +} + +fn preferred_byte(allowed: &[u8]) -> u8 { + for preferred in [b'A', b'a', b'0', b' '] { + if allowed.binary_search(&preferred).is_ok() { + return preferred; + } + } + allowed[0] +} + +fn constrain_byte_to_domain<'ctx>( + state: &mut SymState<'ctx>, + value: &SymValue<'ctx>, + domain: &InputByteDomain, +) { + let ranges = domain.ranges(); + if ranges.len() == 1 && ranges[0] == (0, u8::MAX) { + return; + } + let ctx = state.context(); + let byte = value.extract(ctx, 7, 0).to_bv(ctx); + let mut range_predicates = Vec::new(); + for (start, end) in ranges { + if start == end { + range_predicates.push(byte.eq(BV::from_u64(start as u64, 8))); + } else { + let ge = byte.bvuge(BV::from_u64(start as u64, 8)); + let le = byte.bvule(BV::from_u64(end as u64, 8)); + range_predicates.push(ge & le); + } + } + if range_predicates.is_empty() { + return; + } + let predicate = if range_predicates.len() == 1 { + range_predicates.remove(0) + } else { + Bool::or(&range_predicates.iter().collect::>()) + }; + state.add_constraint(predicate); +} + +fn normalize_ranges(ranges: impl IntoIterator) -> Vec<(u8, u8)> { + let mut ranges = ranges + .into_iter() + .map(|(start, end)| { + if start <= end { + (start, end) + } else { + (end, start) + } + }) + .collect::>(); + ranges.sort_unstable(); + let mut out: Vec<(u8, u8)> = Vec::new(); + for (start, end) in ranges { + if let Some(last) = out.last_mut() + && start <= last.1.saturating_add(1) + { + last.1 = last.1.max(end); + continue; + } + out.push((start, end)); + } + out +} + +fn push_unique(reasons: &mut Vec, reason: impl Into) { + let reason = reason.into(); + if !reasons.iter().any(|existing| existing == &reason) { + reasons.push(reason); + } +} + +#[cfg(test)] +mod tests { + use z3::Context; + + use super::{ + InputByteDomain, SolveTacticConfig, algebraic_preimage_candidate, + algebraic_preimage_for_target, constrain_exact_fold_inputs, + constrain_exact_recurrence_candidate, recurrence_target_for_bytes, + tactic_candidates_for_constraint_graph, + }; + use crate::constraints::{ + FinalConstraint, FinalConstraintGraph, FinalConstraintPrecision, FinalConstraintSource, + RecurrenceAggregateConstraint, RecurrenceAggregateRangeConstraint, + }; + use crate::{ + ExactLoopFoldEvidence, ExactLoopRecurrenceEvidence, ExactLoopRecurrenceKind, + LoopFoldOperation, LoopMemoryTerm, LoopMemoryTermKind, LoopRotateDirection, SymSolver, + SymState, SymValue, + }; + + #[test] + fn exact_input_fold_constraints_bias_model_to_printable_bytes() { + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic_memory(0x7000, 2, "argv1"); + let lhs = state.mem_read(&SymValue::concrete(0x7000, 64), 1); + let rhs = state.mem_read(&SymValue::concrete(0x7001, 64), 1); + let folded = lhs.xor(&ctx, &rhs); + state.constrain_eq(&folded, 0); + + let fold = ExactLoopFoldEvidence { + header: 0x1000, + exit_target: 0x2000, + iterations: 2, + accumulator: "RBX_2".to_string(), + bits: 8, + operation: LoopFoldOperation::Xor, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "RDI_2".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(2), + }, + }; + + let report = + constrain_exact_fold_inputs(&mut state, &[fold], &InputByteDomain::PrintableAscii, 16); + assert_eq!(report.constrained_bytes, 2); + assert!(report.skipped_reasons.is_empty()); + + let solver = SymSolver::new(&ctx); + let model = solver.solve(&state).expect("model"); + let bytes = model + .eval_bytes(&state.symbolic_memory()[0].value, 2) + .expect("bytes"); + assert!(bytes.iter().all(|byte| (0x20..=0x7e).contains(byte))); + assert_eq!(bytes[0] ^ bytes[1], 0); + } + + #[test] + fn xor_preimage_rewrites_nonprintable_model_to_printable_equivalent() { + let fold = ExactLoopFoldEvidence { + header: 0x1000, + exit_target: 0x2000, + iterations: 4, + accumulator: "RBX_2".to_string(), + bits: 8, + operation: LoopFoldOperation::Xor, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "RDI_2".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(4), + }, + }; + let model = [0x00, 0x01, 0x02, 0x03]; + let candidate = + algebraic_preimage_candidate(&fold, &model, &InputByteDomain::PrintableAscii) + .expect("candidate"); + assert_eq!(candidate.len(), model.len()); + assert!(candidate.iter().all(|byte| (0x20..=0x7e).contains(byte))); + assert_eq!( + candidate.iter().fold(0u8, |acc, byte| acc ^ byte), + model.iter().fold(0u8, |acc, byte| acc ^ byte) + ); + } + + #[test] + fn add_preimage_rewrites_sum_to_printable_equivalent() { + let fold = ExactLoopFoldEvidence { + header: 0x1000, + exit_target: 0x2000, + iterations: 3, + accumulator: "RBX_2".to_string(), + bits: 16, + operation: LoopFoldOperation::Add, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "RDI_2".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(3), + }, + }; + let model = [1, 100, 100]; + let candidate = + algebraic_preimage_candidate(&fold, &model, &InputByteDomain::PrintableAscii) + .expect("candidate"); + assert_eq!(candidate.len(), model.len()); + assert!(candidate.iter().all(|byte| (0x20..=0x7e).contains(byte))); + assert_eq!( + candidate.iter().map(|byte| *byte as u16).sum::(), + model.iter().map(|byte| *byte as u16).sum::() + ); + } + + #[test] + fn xor_preimage_for_target_solves_constraint_directly() { + let fold = ExactLoopFoldEvidence { + header: 0x1000, + exit_target: 0x2000, + iterations: 5, + accumulator: "RBX_2".to_string(), + bits: 8, + operation: LoopFoldOperation::Xor, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "RDI_2".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(5), + }, + }; + let candidate = + algebraic_preimage_for_target(&fold, 0x42, &InputByteDomain::PrintableAscii) + .expect("candidate"); + assert_eq!(candidate.len(), 5); + assert!(candidate.iter().all(|byte| (0x20..=0x7e).contains(byte))); + assert_eq!(candidate.iter().fold(0u8, |acc, byte| acc ^ byte), 0x42); + } + + #[test] + fn constraint_graph_scheduler_returns_bounded_candidates() { + let fold = ExactLoopFoldEvidence { + header: 0x1000, + exit_target: 0x2000, + iterations: 3, + accumulator: "RBX_2".to_string(), + bits: 16, + operation: LoopFoldOperation::Add, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "RDI_2".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(3), + }, + }; + let graph = FinalConstraintGraph { + constraints: vec![FinalConstraint::RecurrenceEquals( + RecurrenceAggregateConstraint { + recurrence: ExactLoopRecurrenceEvidence::from(fold), + target: 201, + bits: 16, + source: FinalConstraintSource::ExactRecurrenceAggregateModel, + precision: FinalConstraintPrecision::ModelConditioned, + reasons: Vec::new(), + }, + )], + input_byte_constraints: Vec::new(), + input_length_constraints: Vec::new(), + refusals: Vec::new(), + }; + let config = SolveTacticConfig { + max_candidates: 1, + ..SolveTacticConfig::default() + }; + let candidates = tactic_candidates_for_constraint_graph(&graph, None, &config); + assert_eq!(candidates.len(), 1); + assert_eq!( + candidates[0] + .bytes + .iter() + .map(|byte| *byte as u16) + .sum::(), + 201 + ); + } + + #[test] + fn range_constraint_scheduler_honors_input_byte_constraints() { + let fold = ExactLoopFoldEvidence { + header: 0x1000, + exit_target: 0x2000, + iterations: 3, + accumulator: "RBX_2".to_string(), + bits: 8, + operation: LoopFoldOperation::Xor, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "RDI_2".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(3), + }, + }; + let graph = FinalConstraintGraph { + constraints: vec![FinalConstraint::RecurrenceRange( + RecurrenceAggregateRangeConstraint { + recurrence: ExactLoopRecurrenceEvidence::from(fold.clone()), + min: 0x40, + max: 0x4f, + bits: 8, + source: FinalConstraintSource::TerminalCompareExact, + precision: FinalConstraintPrecision::Exact, + reasons: Vec::new(), + }, + )], + input_byte_constraints: vec![crate::InputByteConstraint { + addr: 0x7000, + allowed: vec![b'K'], + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: Vec::new(), + }], + input_length_constraints: vec![crate::InputLengthConstraint { + base_addr: 0x7000, + len: 3, + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: Vec::new(), + }], + refusals: Vec::new(), + }; + let candidates = + tactic_candidates_for_constraint_graph(&graph, None, &SolveTacticConfig::default()); + assert!(!candidates.is_empty()); + assert_eq!(candidates[0].bytes[0], b'K'); + let folded = candidates[0].bytes.iter().fold(0u8, |acc, byte| acc ^ byte); + assert!((0x40..=0x4f).contains(&folded)); + } + + #[test] + fn mitm_scheduler_solves_xor_constraint_when_pairwise_algebraic_fails() { + let fold = ExactLoopFoldEvidence { + header: 0x1000, + exit_target: 0x2000, + iterations: 4, + accumulator: "RBX_2".to_string(), + bits: 8, + operation: LoopFoldOperation::Xor, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "RDI_2".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(4), + }, + }; + let graph = FinalConstraintGraph { + constraints: vec![FinalConstraint::RecurrenceEquals( + RecurrenceAggregateConstraint { + recurrence: ExactLoopRecurrenceEvidence::from(fold), + target: 0, + bits: 8, + source: FinalConstraintSource::TerminalCompareExact, + precision: FinalConstraintPrecision::Exact, + reasons: Vec::new(), + }, + )], + input_byte_constraints: vec![ + crate::InputByteConstraint { + addr: 0x7000, + allowed: vec![1, 2], + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: Vec::new(), + }, + crate::InputByteConstraint { + addr: 0x7001, + allowed: vec![3], + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: Vec::new(), + }, + crate::InputByteConstraint { + addr: 0x7002, + allowed: vec![4], + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: Vec::new(), + }, + crate::InputByteConstraint { + addr: 0x7003, + allowed: vec![5], + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: Vec::new(), + }, + ], + input_length_constraints: vec![crate::InputLengthConstraint { + base_addr: 0x7000, + len: 4, + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: Vec::new(), + }], + refusals: Vec::new(), + }; + + let candidates = + tactic_candidates_for_constraint_graph(&graph, None, &SolveTacticConfig::default()); + assert!(!candidates.is_empty()); + assert!(candidates[0].used_mitm); + assert_eq!(candidates[0].bytes, vec![2, 3, 4, 5]); + assert_eq!( + candidates[0].bytes.iter().fold(0u8, |acc, byte| acc ^ byte), + 0 + ); + } + + #[test] + fn mitm_scheduler_solves_add_constraint_when_greedy_algebraic_fails() { + let fold = ExactLoopFoldEvidence { + header: 0x1000, + exit_target: 0x2000, + iterations: 3, + accumulator: "RBX_2".to_string(), + bits: 16, + operation: LoopFoldOperation::Add, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "RDI_2".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(3), + }, + }; + let graph = FinalConstraintGraph { + constraints: vec![FinalConstraint::RecurrenceEquals( + RecurrenceAggregateConstraint { + recurrence: ExactLoopRecurrenceEvidence::from(fold), + target: 7, + bits: 16, + source: FinalConstraintSource::TerminalCompareExact, + precision: FinalConstraintPrecision::Exact, + reasons: Vec::new(), + }, + )], + input_byte_constraints: vec![ + crate::InputByteConstraint { + addr: 0x7000, + allowed: vec![1, 2], + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: Vec::new(), + }, + crate::InputByteConstraint { + addr: 0x7001, + allowed: vec![1, 4], + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: Vec::new(), + }, + crate::InputByteConstraint { + addr: 0x7002, + allowed: vec![1, 4], + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: Vec::new(), + }, + ], + input_length_constraints: vec![crate::InputLengthConstraint { + base_addr: 0x7000, + len: 3, + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: Vec::new(), + }], + refusals: Vec::new(), + }; + + let candidates = + tactic_candidates_for_constraint_graph(&graph, None, &SolveTacticConfig::default()); + assert!(!candidates.is_empty()); + assert!(candidates[0].used_mitm); + assert_eq!(candidates[0].bytes, vec![2, 1, 4]); + assert_eq!( + candidates[0] + .bytes + .iter() + .map(|byte| *byte as u16) + .sum::(), + 7 + ); + } + + #[test] + fn rotate_recurrence_constraint_produces_candidate_with_concrete_initial() { + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.set_register("RBX_1", SymValue::concrete(0xa5, 8)); + let recurrence = ExactLoopRecurrenceEvidence { + header: 0x1000, + exit_target: 0x2000, + iterations: 3, + accumulator: "RBX_2".to_string(), + initial: "RBX_1".to_string(), + bits: 8, + kind: ExactLoopRecurrenceKind::RotateMix { + direction: LoopRotateDirection::Left, + amount: 3, + operation: LoopFoldOperation::Xor, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "RDI_2".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(3), + }, + }, + }; + let expected = vec![0x12, 0x34, 0x56]; + let target = + recurrence_target_for_bytes(&recurrence, Some(&state), &expected).expect("target"); + let graph = FinalConstraintGraph { + constraints: vec![FinalConstraint::RecurrenceEquals( + RecurrenceAggregateConstraint { + recurrence: recurrence.clone(), + target, + bits: 8, + source: FinalConstraintSource::TerminalCompareExact, + precision: FinalConstraintPrecision::Exact, + reasons: Vec::new(), + }, + )], + input_byte_constraints: vec![ + crate::InputByteConstraint { + addr: 0x7000, + allowed: vec![0x12, 0x13], + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: Vec::new(), + }, + crate::InputByteConstraint { + addr: 0x7001, + allowed: vec![0x34, 0x35], + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: Vec::new(), + }, + crate::InputByteConstraint { + addr: 0x7002, + allowed: vec![0x56, 0x57], + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: Vec::new(), + }, + ], + input_length_constraints: vec![crate::InputLengthConstraint { + base_addr: 0x7000, + len: 3, + precision: FinalConstraintPrecision::Exact, + source: FinalConstraintSource::MemoryWindowAssumption, + reasons: Vec::new(), + }], + refusals: Vec::new(), + }; + + let candidates = tactic_candidates_for_constraint_graph( + &graph, + Some(&state), + &SolveTacticConfig::default(), + ); + assert!(!candidates.is_empty()); + assert_eq!(candidates[0].recurrence, recurrence); + assert_eq!(candidates[0].bytes, expected); + } + + #[test] + fn exact_rotate_recurrence_candidate_constrains_symbolic_input_bytes() { + let ctx = Context::thread_local(); + let mut state = SymState::new(&ctx, 0x1000); + state.make_symbolic_memory(0x7000, 3, "argv1"); + let recurrence = ExactLoopRecurrenceEvidence { + header: 0x1000, + exit_target: 0x2000, + iterations: 3, + accumulator: "RBX_2".to_string(), + initial: "RBX_1".to_string(), + bits: 8, + kind: ExactLoopRecurrenceKind::RotateMix { + direction: LoopRotateDirection::Left, + amount: 3, + operation: LoopFoldOperation::Xor, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "RDI_2".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(3), + }, + }, + }; + let expected = [0x12, 0x34, 0x56]; + let report = constrain_exact_recurrence_candidate(&mut state, &recurrence, &expected); + assert_eq!(report.constrained_bytes, 3); + assert!(report.skipped_reasons.is_empty()); + + let solver = SymSolver::new(&ctx); + let model = solver.solve(&state).expect("model"); + let bytes = model + .eval_bytes(&state.symbolic_memory()[0].value, 3) + .expect("bytes"); + assert_eq!(bytes, expected); + } +} diff --git a/crates/r2sym/src/value.rs b/crates/r2sym/src/value.rs index c04cdad..140ba60 100644 --- a/crates/r2sym/src/value.rs +++ b/crates/r2sym/src/value.rs @@ -277,6 +277,13 @@ impl<'ctx> SymValue<'ctx> { } } + pub(crate) fn symbolic_key(&self) -> Option { + match self { + Self::Symbolic { ast, bits, .. } => Some(format!("{bits}:{}", ast)), + _ => None, + } + } + fn is_concrete_value(&self, expected: u64) -> bool { matches!(self, Self::Concrete { value, .. } if *value == expected) } @@ -289,6 +296,20 @@ impl<'ctx> SymValue<'ctx> { } } + fn concrete_logical_shift_left(value: u64, bits: u32, amount: u64) -> u64 { + if bits == 0 || amount >= u64::from(bits) { + return 0; + } + value.wrapping_shl(amount as u32) & Self::mask_for_bits(bits) + } + + fn concrete_logical_shift_right(value: u64, bits: u32, amount: u64) -> u64 { + if bits == 0 || amount >= u64::from(bits) { + return 0; + } + value >> (amount as u32) + } + fn is_all_ones_for(&self, bits: u32) -> bool { match self { Self::Concrete { @@ -965,13 +986,8 @@ impl<'ctx> SymValue<'ctx> { match (self, amount) { (_, amt) if amt.is_zero() => self.clone().with_taint(taint), (Self::Concrete { value, bits, .. }, Self::Concrete { value: amt, .. }) => { - let mask = if *bits >= 64 { - u64::MAX - } else { - (1u64 << *bits) - 1 - }; Self::Concrete { - value: (*value << (*amt as u32)) & mask, + value: Self::concrete_logical_shift_left(*value, *bits, *amt), bits: *bits, taint, } @@ -996,7 +1012,7 @@ impl<'ctx> SymValue<'ctx> { (_, amt) if amt.is_zero() => self.clone().with_taint(taint), (Self::Concrete { value, bits, .. }, Self::Concrete { value: amt, .. }) => { Self::Concrete { - value: *value >> (*amt as u32), + value: Self::concrete_logical_shift_right(*value, *bits, *amt), bits: *bits, taint, } @@ -1484,6 +1500,25 @@ mod bitwidth_tests { assert_eq!(result.bits(), 64); // Result keeps value's width } + #[test] + fn test_concrete_shifts_past_bitwidth_zero_fill_instead_of_panicking() { + let ctx = Context::thread_local(); + + let val64 = SymValue::concrete(0x1234_5678_9abc_def0, 64); + let shift64 = SymValue::concrete(64, 64); + let shift65 = SymValue::concrete(65, 8); + + assert_eq!(val64.shl(&ctx, &shift64).as_concrete(), Some(0)); + assert_eq!(val64.lshr(&ctx, &shift64).as_concrete(), Some(0)); + assert_eq!(val64.shl(&ctx, &shift65).as_concrete(), Some(0)); + assert_eq!(val64.lshr(&ctx, &shift65).as_concrete(), Some(0)); + + let val8 = SymValue::concrete(0b1111_0000, 8); + let shift8 = SymValue::concrete(8, 8); + assert_eq!(val8.shl(&ctx, &shift8).as_concrete(), Some(0)); + assert_eq!(val8.lshr(&ctx, &shift8).as_concrete(), Some(0)); + } + #[test] fn test_symbolic_different_bitwidths() { let ctx = Context::thread_local(); diff --git a/crates/r2sym/src/verification.rs b/crates/r2sym/src/verification.rs new file mode 100644 index 0000000..f4c25d9 --- /dev/null +++ b/crates/r2sym/src/verification.rs @@ -0,0 +1,1122 @@ +//! Canonical solve verification and replay policy. +//! +//! Query exploration can prove that a target is reachable under the current +//! semantic model. This module decides whether that reachability is strong +//! enough to report a concrete solve. + +use r2ssa::SsaArtifact; + +use crate::constraints::{FinalConstraintGraph, FinalConstraintPrecision}; +use crate::kernel::FactPrecision; +use crate::loops::{ + ExactLoopFoldEvidence, ExactLoopRecurrenceEvidence, ExactLoopRecurrenceKind, LoopFoldOperation, + LoopMemoryTermKind, exact_fold_evidence_from_recurrences, +}; +use crate::path::{ExploreStats, PathExplorer, SolvedPath, SolvedPathGeneration}; +use crate::query::QueryCompletion; +use crate::semantics::TargetQueryExecutionRoute; +use crate::{PreparedFunctionScope, SymState}; + +/// Result status for concrete target solving. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SolveStatus { + Solved, + Candidate, + ResidualReachable, + Unverified, + Unsat, + Unknown, + BudgetExhausted, +} + +/// Concrete/model validation state for a reported solve candidate. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ModelValidation { + /// Pure symbolic semantics were exact enough; no external replay is required. + NotRequired, + /// A concrete replay/oracle accepted the candidate. + Verified, + /// A concrete replay/oracle rejected the candidate. + Failed { reason: String }, + /// Validation was required by residual/runtime evidence but no backend was available. + Unavailable { reason: String }, +} + +/// Truthfulness metadata for target solving. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SolveVerification { + pub target_reached_under_model: bool, + pub candidate_solution_verified: bool, + pub residual_reasons: Vec, + pub model_validation: ModelValidation, +} + +/// Shape of the concrete candidate extracted from a path model. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SolveCandidateShape { + pub input_scalars: usize, + pub input_buffers: usize, + pub registers: usize, + pub memory_regions: usize, + pub concrete_assignments: usize, + pub final_pc: u64, + pub num_constraints: usize, + pub generation: Option, +} + +impl SolveCandidateShape { + pub fn from_solution(solution: &SolvedPath) -> Self { + let concrete_assignments = solution.inputs.len() + + solution.input_buffers.values().map(Vec::len).sum::() + + solution.memory.values().map(Vec::len).sum::(); + Self { + input_scalars: solution.inputs.len(), + input_buffers: solution.input_buffers.len(), + registers: solution.registers.len(), + memory_regions: solution.memory.len(), + concrete_assignments, + final_pc: solution.final_pc, + num_constraints: solution.num_constraints, + generation: solution.generation.clone(), + } + } + + pub fn has_replayable_assignments(&self) -> bool { + self.concrete_assignments > 0 + } +} + +/// Compact precision summary for route/query evidence. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EvidenceSummary { + pub precision: FactPrecision, + pub reasons: Vec, + pub requires_replay: bool, + pub final_constraint_precision: FinalConstraintPrecision, + pub exact_final_constraints: usize, + pub model_conditioned_final_constraints: usize, + pub exact_loop_recurrences: Vec, + pub exact_loop_folds: Vec, + pub tactics: Vec, +} + +impl EvidenceSummary { + pub fn exact() -> Self { + Self { + precision: FactPrecision::Exact, + reasons: Vec::new(), + requires_replay: false, + final_constraint_precision: FinalConstraintPrecision::Unknown, + exact_final_constraints: 0, + model_conditioned_final_constraints: 0, + exact_loop_recurrences: Vec::new(), + exact_loop_folds: Vec::new(), + tactics: Vec::new(), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SolveTacticKind { + XorFoldPreimage, + AddFoldPreimage, + RotateXorRecurrence, + RotateAddRecurrence, + ConcreteTableFold, + RuntimeBlobFold, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SolveTacticStatus { + Available, + EvidenceOnly, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SolveTacticEvidence { + pub kind: SolveTacticKind, + pub status: SolveTacticStatus, + pub reason: String, + pub recurrence: ExactLoopRecurrenceEvidence, +} + +/// Replay requirement derived from route precision and runtime evidence. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum VerificationRequirement { + NotRequired, + Required { reasons: Vec }, + Refused { reasons: Vec }, +} + +/// Canonical witness for a target solve result. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SolveWitness { + pub target_addr: u64, + pub selected_path_index: Option, + pub final_pc: Option, + pub candidate: Option, + pub evidence: EvidenceSummary, + pub verification_requirement: VerificationRequirement, + pub verification: SolveVerification, +} + +impl SolveWitness { + pub fn is_proven(&self) -> bool { + self.verification.target_reached_under_model + && (self.verification.candidate_solution_verified + || (self + .candidate + .as_ref() + .is_some_and(|candidate| !candidate.has_replayable_assignments()) + && self.evidence.reasons.is_empty() + && matches!( + self.verification.model_validation, + ModelValidation::NotRequired + ))) + } +} + +/// Input required to replay a candidate model against concrete/lifted semantics. +pub struct CandidateReplayRequest<'a, 'ctx> { + pub func: &'a SsaArtifact, + pub scope: Option<&'a PreparedFunctionScope>, + pub initial_state: SymState<'ctx>, + pub target_addr: u64, + pub solution: &'a SolvedPath, +} + +/// Backend-neutral candidate replay interface. +pub trait CandidateReplayBackend<'ctx> { + fn replay_candidate<'a>( + &mut self, + request: CandidateReplayRequest<'a, 'ctx>, + ) -> ModelValidation; +} + +/// Lifted-semantics replay backend used as the default verifier. +pub struct LiftedReplayBackend<'a, 'ctx> { + explorer: &'a mut PathExplorer<'ctx>, +} + +impl<'a, 'ctx> LiftedReplayBackend<'a, 'ctx> { + pub fn new(explorer: &'a mut PathExplorer<'ctx>) -> Self { + Self { explorer } + } +} + +impl<'ctx> CandidateReplayBackend<'ctx> for LiftedReplayBackend<'_, 'ctx> { + fn replay_candidate<'a>( + &mut self, + request: CandidateReplayRequest<'a, 'ctx>, + ) -> ModelValidation { + validate_solution_with_lifted_semantics( + self.explorer, + request.func, + request.scope, + request.initial_state, + request.target_addr, + request.solution, + ) + } +} + +/// Explicit no-backend verifier for tests and future external integrations. +#[derive(Debug, Clone)] +pub struct UnavailableReplayBackend { + reason: String, +} + +impl UnavailableReplayBackend { + pub fn new(reason: impl Into) -> Self { + Self { + reason: reason.into(), + } + } +} + +impl<'ctx> CandidateReplayBackend<'ctx> for UnavailableReplayBackend { + fn replay_candidate<'a>( + &mut self, + _request: CandidateReplayRequest<'a, 'ctx>, + ) -> ModelValidation { + ModelValidation::Unavailable { + reason: self.reason.clone(), + } + } +} + +/// Full verifier input for a target solve result. +pub struct SolveVerificationRequest<'a, 'ctx> { + pub func: &'a SsaArtifact, + pub scope: Option<&'a PreparedFunctionScope>, + pub selected_route: &'a crate::TargetQueryRoutePlan, + pub stats: &'a ExploreStats, + pub validation_initial_state: SymState<'ctx>, + pub target_addr: u64, + pub exact_unsat: bool, + pub selected_path_index: Option, + pub solution: Option<&'a SolvedPath>, + pub constraint_graph: &'a FinalConstraintGraph, +} + +/// Verify the target solve result and decide its final user-visible status. +pub fn verify_solve_result<'a, 'ctx, B>( + request: SolveVerificationRequest<'a, 'ctx>, + replay_backend: &mut B, +) -> (SolveStatus, SolveVerification, SolveWitness) +where + B: CandidateReplayBackend<'ctx>, +{ + let residual_reasons = solve_residual_reasons(request.selected_route, request.stats); + let evidence = evidence_summary_for_route_and_stats( + request.selected_route, + request.stats, + Some(request.constraint_graph), + ); + let verification_requirement = verification_requirement_for_route_and_stats( + request.selected_route, + request.stats, + Some(request.constraint_graph), + ); + let candidate = request.solution.map(SolveCandidateShape::from_solution); + let final_pc = request.solution.map(|solution| solution.final_pc); + + let model_validation = if let Some(solution) = request.solution { + let candidate_has_replayable_assignments = candidate + .as_ref() + .is_some_and(SolveCandidateShape::has_replayable_assignments); + if candidate_has_replayable_assignments { + replay_backend.replay_candidate(CandidateReplayRequest { + func: request.func, + scope: request.scope, + initial_state: request.validation_initial_state, + target_addr: request.target_addr, + solution, + }) + } else if residual_reasons.is_empty() { + ModelValidation::NotRequired + } else { + ModelValidation::Unavailable { + reason: "residual/runtime evidence produced no concrete replay candidate" + .to_string(), + } + } + } else if residual_reasons.is_empty() { + ModelValidation::NotRequired + } else { + ModelValidation::Unavailable { + reason: "residual/runtime evidence requires concrete replay validation".to_string(), + } + }; + + let verification = solve_verification( + request.selected_path_index.is_some(), + request.solution.is_some(), + residual_reasons, + model_validation, + ); + let status = classify_solve_status( + request.exact_unsat, + request.selected_path_index, + request.solution.is_some(), + completion_from_stats(request.stats), + &verification, + request.constraint_graph, + ); + let witness = SolveWitness { + target_addr: request.target_addr, + selected_path_index: request.selected_path_index, + final_pc, + candidate, + evidence, + verification_requirement, + verification: verification.clone(), + }; + (status, verification, witness) +} + +pub fn solution_extraction_allowed( + route: &crate::TargetQueryRoutePlan, + stats: &ExploreStats, +) -> bool { + let residual_reasons = solve_residual_reasons(route, stats); + !residual_summary_blocks_solution_extraction(stats, &residual_reasons) +} + +pub fn evidence_summary_for_route_and_stats( + route: &crate::TargetQueryRoutePlan, + stats: &ExploreStats, + constraint_graph: Option<&FinalConstraintGraph>, +) -> EvidenceSummary { + let mut reasons = solve_residual_reasons(route, stats); + if let Some(graph) = constraint_graph { + for reason in &graph.refusals { + push_unique_reason(&mut reasons, reason.clone()); + } + if graph.constraints.is_empty() { + push_unique_reason(&mut reasons, "final_constraint_missing"); + } else if graph.exact_constraint_count() == 0 + && graph.model_conditioned_constraint_count() > 0 + { + push_unique_reason(&mut reasons, "final_constraint_model_conditioned"); + } + } + let exact_loop_folds = + exact_fold_evidence_from_recurrences(&stats.runtime_loop_exact_recurrences); + let exact_loop_recurrences = stats.runtime_loop_exact_recurrences.clone(); + let tactics = solve_tactics_for_exact_recurrences(&stats.runtime_loop_exact_recurrences); + let final_constraint_precision = constraint_graph + .map(FinalConstraintGraph::strongest_precision) + .unwrap_or(FinalConstraintPrecision::Unknown); + let exact_final_constraints = constraint_graph + .map(FinalConstraintGraph::exact_constraint_count) + .unwrap_or(0); + let model_conditioned_final_constraints = constraint_graph + .map(FinalConstraintGraph::model_conditioned_constraint_count) + .unwrap_or(0); + if reasons.is_empty() { + return EvidenceSummary { + precision: FactPrecision::Exact, + reasons, + requires_replay: false, + final_constraint_precision, + exact_final_constraints, + model_conditioned_final_constraints, + exact_loop_recurrences, + exact_loop_folds, + tactics, + }; + } + EvidenceSummary { + precision: FactPrecision::Residual, + requires_replay: true, + reasons, + final_constraint_precision, + exact_final_constraints, + model_conditioned_final_constraints, + exact_loop_recurrences, + exact_loop_folds, + tactics, + } +} + +pub fn solve_tactics_for_exact_recurrences( + recurrences: &[ExactLoopRecurrenceEvidence], +) -> Vec { + let mut tactics = recurrences + .iter() + .filter_map(|recurrence| match &recurrence.kind { + ExactLoopRecurrenceKind::Fold { operation, term } => { + tactic_for_fold_recurrence(recurrence, *operation, term) + } + ExactLoopRecurrenceKind::RotateMix { + operation, term, .. + } => Some(SolveTacticEvidence { + kind: match operation { + LoopFoldOperation::Xor => SolveTacticKind::RotateXorRecurrence, + LoopFoldOperation::Add => SolveTacticKind::RotateAddRecurrence, + }, + status: SolveTacticStatus::Available, + reason: rotate_recurrence_reason(*operation, term.kind), + recurrence: recurrence.clone(), + }), + _ => None, + }) + .collect::>(); + tactics.sort_by(|lhs, rhs| { + ( + lhs.recurrence.header, + lhs.recurrence.exit_target, + lhs.recurrence.accumulator.as_str(), + format!("{:?}", lhs.recurrence.kind), + lhs.reason.as_str(), + ) + .cmp(&( + rhs.recurrence.header, + rhs.recurrence.exit_target, + rhs.recurrence.accumulator.as_str(), + format!("{:?}", rhs.recurrence.kind), + rhs.reason.as_str(), + )) + }); + tactics +} + +pub fn solve_tactics_for_exact_folds(folds: &[ExactLoopFoldEvidence]) -> Vec { + let recurrences = folds + .iter() + .cloned() + .map(ExactLoopRecurrenceEvidence::from) + .collect::>(); + solve_tactics_for_exact_recurrences(&recurrences) +} + +fn tactic_for_fold_recurrence( + recurrence: &ExactLoopRecurrenceEvidence, + operation: LoopFoldOperation, + term: &crate::LoopMemoryTerm, +) -> Option { + match (operation, term.kind) { + (LoopFoldOperation::Xor, LoopMemoryTermKind::InputRead) => Some(SolveTacticEvidence { + kind: SolveTacticKind::XorFoldPreimage, + status: SolveTacticStatus::Available, + reason: "exact symbolic input xor-fold can feed bounded preimage solving".to_string(), + recurrence: recurrence.clone(), + }), + (LoopFoldOperation::Add, LoopMemoryTermKind::InputRead) => Some(SolveTacticEvidence { + kind: SolveTacticKind::AddFoldPreimage, + status: SolveTacticStatus::Available, + reason: "exact symbolic input add-fold can feed bounded preimage solving".to_string(), + recurrence: recurrence.clone(), + }), + (_, LoopMemoryTermKind::TableRead) => Some(SolveTacticEvidence { + kind: SolveTacticKind::ConcreteTableFold, + status: SolveTacticStatus::EvidenceOnly, + reason: "exact concrete table fold is available as constraint evidence".to_string(), + recurrence: recurrence.clone(), + }), + (_, LoopMemoryTermKind::RuntimeBlobRead) => Some(SolveTacticEvidence { + kind: SolveTacticKind::RuntimeBlobFold, + status: SolveTacticStatus::EvidenceOnly, + reason: "exact runtime-blob fold is available as constraint evidence".to_string(), + recurrence: recurrence.clone(), + }), + (_, LoopMemoryTermKind::Unknown) => None, + } +} + +fn rotate_recurrence_reason(operation: LoopFoldOperation, term_kind: LoopMemoryTermKind) -> String { + let operation = match operation { + LoopFoldOperation::Add => "rotate-add", + LoopFoldOperation::Xor => "rotate-xor", + }; + match term_kind { + LoopMemoryTermKind::InputRead => { + format!( + "exact symbolic input {operation} recurrence can feed bounded preimage solving when initial state is concrete" + ) + } + LoopMemoryTermKind::TableRead => { + format!( + "exact concrete-table {operation} recurrence is available as constraint evidence" + ) + } + LoopMemoryTermKind::RuntimeBlobRead => { + format!("exact runtime-blob {operation} recurrence is available as constraint evidence") + } + LoopMemoryTermKind::Unknown => { + format!("exact {operation} recurrence has unknown memory provenance") + } + } +} + +pub fn verification_requirement_for_route_and_stats( + route: &crate::TargetQueryRoutePlan, + stats: &ExploreStats, + constraint_graph: Option<&FinalConstraintGraph>, +) -> VerificationRequirement { + let summary = evidence_summary_for_route_and_stats(route, stats, constraint_graph); + if summary.reasons.is_empty() { + return VerificationRequirement::NotRequired; + } + if matches!(route.target_plan, crate::TargetQueryPlan::Refuse { .. }) + || matches!(route.execution, TargetQueryExecutionRoute::Refuse { .. }) + { + return VerificationRequirement::Refused { + reasons: summary.reasons, + }; + } + if summary.requires_replay { + VerificationRequirement::Required { + reasons: summary.reasons, + } + } else { + VerificationRequirement::NotRequired + } +} + +pub(crate) fn completion_from_stats(stats: &ExploreStats) -> QueryCompletion { + if stats.timed_out || stats.max_states_exhausted { + QueryCompletion::BudgetExhausted + } else { + QueryCompletion::Complete + } +} + +pub(crate) fn push_unique_reason(reasons: &mut Vec, reason: impl Into) { + let reason = reason.into(); + if !reasons.iter().any(|existing| existing == &reason) { + reasons.push(reason); + } +} + +pub(crate) fn collect_route_residual_reasons( + route: &TargetQueryExecutionRoute, + reasons: &mut Vec, +) { + match route { + TargetQueryExecutionRoute::ContinuationSeeded { route, .. } => { + push_unique_reason(reasons, "continuation_seeded_unverified"); + collect_route_residual_reasons(route, reasons); + } + TargetQueryExecutionRoute::ResidualOnly { + reasons: route_reasons, + } => { + for reason in route_reasons { + push_unique_reason(reasons, reason.clone()); + } + } + TargetQueryExecutionRoute::Refuse { reason } => { + push_unique_reason(reasons, format!("refused: {reason}")); + } + TargetQueryExecutionRoute::ArtifactCondition { .. } + | TargetQueryExecutionRoute::ArtifactMemoryOnly + | TargetQueryExecutionRoute::DynamicTargetCompile { .. } + | TargetQueryExecutionRoute::VmTargetCompile { .. } => {} + } +} + +pub(crate) fn solve_residual_reasons( + route: &crate::TargetQueryRoutePlan, + stats: &ExploreStats, +) -> Vec { + let mut reasons = Vec::new(); + match &route.target_plan { + crate::TargetQueryPlan::Residual { + reasons: plan_reasons, + } => { + for reason in plan_reasons { + push_unique_reason(&mut reasons, reason.clone()); + } + } + crate::TargetQueryPlan::Refuse { reason } => { + push_unique_reason(&mut reasons, format!("target refused: {reason}")); + } + crate::TargetQueryPlan::Ready { .. } | crate::TargetQueryPlan::Fallback { .. } => {} + } + collect_route_residual_reasons(&route.execution, &mut reasons); + if stats.runtime_breakpoint_loop_summaries > 0 { + push_unique_reason(&mut reasons, "runtime_breakpoint_loop_summary_residual"); + } + if stats.runtime_loop_unknown_carried_state > 0 { + push_unique_reason(&mut reasons, "runtime_loop_unknown_carried_state"); + } + if stats.runtime_loop_budget_residuals > 0 { + push_unique_reason(&mut reasons, "runtime_loop_iteration_budget"); + } + if stats.runtime_loop_refusals > 0 { + push_unique_reason(&mut reasons, "runtime_loop_refused"); + } + if stats.runtime_missing_materialized_code > 0 { + push_unique_reason(&mut reasons, "missing_runtime_materialized_code"); + } + if stats.runtime_region_provenance_unknown > 0 { + push_unique_reason(&mut reasons, "runtime_region_provenance_unknown"); + } + reasons +} + +pub(crate) fn residual_summary_blocks_solution_extraction( + stats: &ExploreStats, + residual_reasons: &[String], +) -> bool { + stats.runtime_breakpoint_loop_summaries > 0 + && residual_reasons + .iter() + .any(|reason| reason == "runtime_breakpoint_loop_summary_residual") +} + +pub(crate) fn solve_verification( + target_reached_under_model: bool, + solution_present: bool, + residual_reasons: Vec, + model_validation: ModelValidation, +) -> SolveVerification { + let candidate_solution_verified = + solution_present && matches!(model_validation, ModelValidation::Verified); + SolveVerification { + target_reached_under_model, + candidate_solution_verified, + residual_reasons, + model_validation, + } +} + +pub(crate) fn classify_solve_status( + exact_unsat: bool, + selected_path_index: Option, + solution_present: bool, + completion: QueryCompletion, + verification: &SolveVerification, + constraint_graph: &FinalConstraintGraph, +) -> SolveStatus { + if exact_unsat { + return SolveStatus::Unsat; + } + let has_exact_final_constraints = constraint_graph.has_exact_constraints(); + let has_model_conditioned_final_constraints = + constraint_graph.model_conditioned_constraint_count() > 0; + if selected_path_index.is_some() + && !verification.residual_reasons.is_empty() + && !verification.candidate_solution_verified + { + if has_exact_final_constraints + && !matches!( + verification.model_validation, + ModelValidation::Failed { .. } + ) + { + return SolveStatus::Candidate; + } + return match verification.model_validation { + ModelValidation::Failed { .. } => SolveStatus::Unverified, + ModelValidation::NotRequired | ModelValidation::Unavailable { .. } => { + SolveStatus::ResidualReachable + } + ModelValidation::Verified => SolveStatus::Solved, + }; + } + match ( + selected_path_index, + solution_present, + completion, + &verification.model_validation, + ) { + (Some(_), true, _, ModelValidation::Unavailable { .. }) if has_exact_final_constraints => { + SolveStatus::Candidate + } + ( + Some(_), + true, + _, + ModelValidation::Failed { .. } | ModelValidation::Unavailable { .. }, + ) => SolveStatus::Unverified, + (Some(_), true, _, ModelValidation::NotRequired | ModelValidation::Verified) => { + if has_model_conditioned_final_constraints && !has_exact_final_constraints { + SolveStatus::Unverified + } else { + SolveStatus::Solved + } + } + (None, _, QueryCompletion::BudgetExhausted, _) => SolveStatus::BudgetExhausted, + (None, _, QueryCompletion::Complete, _) => SolveStatus::Unsat, + (Some(_), false, QueryCompletion::BudgetExhausted, _) => SolveStatus::BudgetExhausted, + (Some(_), false, _, _) => SolveStatus::Unknown, + } +} + +fn concrete_value_to_width(value: u64, bits: u32) -> u64 { + if bits >= 64 { + value + } else if bits == 0 { + 0 + } else { + value & ((1u64 << bits) - 1) + } +} + +fn little_endian_bytes_to_u64(bytes: &[u8]) -> Option { + if bytes.len() > 8 { + return None; + } + let mut value = 0u64; + for (index, byte) in bytes.iter().enumerate() { + value |= (*byte as u64) << (index * 8); + } + Some(value) +} + +fn solution_constraints_for_state<'ctx>( + state: &SymState<'ctx>, + solution: &SolvedPath, +) -> Vec<(crate::SymValue<'ctx>, u64)> { + let mut constraints = Vec::new(); + for (name, value) in &solution.inputs { + if let Some(symbolic) = state.symbolic_inputs().get(name) { + constraints.push(( + symbolic.clone(), + concrete_value_to_width(*value, symbolic.bits()), + )); + } + } + for (input_name, bytes) in &solution.input_buffers { + for input in state.symbolic_fd_inputs().values() { + if input.name != *input_name { + continue; + } + for (byte, value) in input.bytes.iter().zip(bytes.iter()) { + constraints.push((byte.clone(), *value as u64)); + } + } + } + for (region_name, bytes) in &solution.memory { + let Some(value) = little_endian_bytes_to_u64(bytes) else { + continue; + }; + if let Some(region) = state + .symbolic_memory() + .iter() + .find(|region| region.name == *region_name) + { + constraints.push(( + region.value.clone(), + concrete_value_to_width(value, region.value.bits()), + )); + } + } + constraints +} + +fn apply_solution_constraints<'ctx>( + state: &mut SymState<'ctx>, + solution: &SolvedPath, +) -> Result { + let constraints = solution_constraints_for_state(state, solution); + if constraints.is_empty() + && (!solution.inputs.is_empty() + || !solution.input_buffers.is_empty() + || !solution.memory.is_empty()) + { + return Err("candidate model did not match validation seed inputs".to_string()); + } + let count = constraints.len(); + for (value, concrete) in constraints { + state.constrain_eq(&value, concrete); + } + Ok(count) +} + +fn validate_solution_with_lifted_semantics<'ctx>( + explorer: &mut PathExplorer<'ctx>, + func: &SsaArtifact, + scope: Option<&PreparedFunctionScope>, + mut initial_state: SymState<'ctx>, + target_addr: u64, + solution: &SolvedPath, +) -> ModelValidation { + match apply_solution_constraints(&mut initial_state, solution) { + Ok(_) => {} + Err(reason) => return ModelValidation::Unavailable { reason }, + } + + let (found, replay_stats) = explorer.with_isolated_stats(|explorer| { + let previous_target_guided = explorer.target_guided_queries_enabled(); + explorer.set_target_guided_queries(true); + let found = explorer.with_residual_runtime_loop_summaries(false, |explorer| { + explorer.find_path_to_in_scope(func, scope, initial_state, target_addr) + }); + explorer.set_target_guided_queries(previous_target_guided); + found + }); + + match found { + Some(path) if path.final_pc() == target_addr => ModelValidation::Verified, + Some(path) => ModelValidation::Failed { + reason: format!( + "lifted replay stopped at 0x{:x} instead of 0x{:x}", + path.final_pc(), + target_addr + ), + }, + None if replay_stats.timed_out || replay_stats.max_states_exhausted => { + ModelValidation::Unavailable { + reason: "lifted replay validation exhausted budget".to_string(), + } + } + None => ModelValidation::Failed { + reason: "lifted replay did not reach target with candidate model".to_string(), + }, + } +} + +#[cfg(test)] +mod tests { + use super::{ + ModelValidation, SolveTacticKind, SolveTacticStatus, classify_solve_status, + evidence_summary_for_route_and_stats, residual_summary_blocks_solution_extraction, + solve_residual_reasons, solve_tactics_for_exact_folds, solve_tactics_for_exact_recurrences, + solve_verification, + }; + use crate::{ + ExactLoopFoldEvidence, ExactLoopRecurrenceEvidence, FinalConstraint, FinalConstraintGraph, + FinalConstraintPrecision, FinalConstraintSource, LoopFoldOperation, LoopMemoryTerm, + LoopMemoryTermKind, LoopRotateDirection, QueryCompletion, QueryGuidanceMode, + RecurrenceAggregateConstraint, SolveStatus, TargetQueryExecutionRoute, TargetQueryPlan, + TargetQueryRoutePlan, + }; + + fn exact_graph() -> FinalConstraintGraph { + let fold = ExactLoopFoldEvidence { + header: 0x1000, + exit_target: 0x2000, + iterations: 4, + accumulator: "RBX_2".to_string(), + bits: 64, + operation: LoopFoldOperation::Xor, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "RDI_2".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(4), + }, + }; + FinalConstraintGraph { + constraints: vec![FinalConstraint::RecurrenceEquals( + RecurrenceAggregateConstraint { + recurrence: fold.into(), + target: 0x41, + bits: 64, + source: FinalConstraintSource::TerminalCompareExact, + precision: FinalConstraintPrecision::Exact, + reasons: Vec::new(), + }, + )], + input_byte_constraints: Vec::new(), + input_length_constraints: Vec::new(), + refusals: Vec::new(), + } + } + + #[test] + fn residual_runtime_loop_summary_cannot_be_solved() { + let route = TargetQueryRoutePlan { + target_plan: TargetQueryPlan::Ready { + mode: QueryGuidanceMode::NarrowOnly, + }, + execution: TargetQueryExecutionRoute::DynamicTargetCompile { + reason: "large cfg".to_string(), + mode: QueryGuidanceMode::NarrowOnly, + }, + }; + let stats = crate::path::ExploreStats { + runtime_breakpoint_loop_summaries: 1, + ..crate::path::ExploreStats::default() + }; + let reasons = solve_residual_reasons(&route, &stats); + assert_eq!( + reasons, + vec!["runtime_breakpoint_loop_summary_residual".to_string()] + ); + assert!(residual_summary_blocks_solution_extraction( + &stats, &reasons + )); + let verification = solve_verification( + true, + true, + reasons, + ModelValidation::Unavailable { + reason: "residual/runtime evidence requires concrete replay validation".to_string(), + }, + ); + + assert_eq!( + classify_solve_status( + false, + Some(0), + true, + QueryCompletion::Complete, + &verification, + &FinalConstraintGraph::default(), + ), + SolveStatus::ResidualReachable + ); + assert!(!verification.candidate_solution_verified); + assert!(matches!( + verification.model_validation, + ModelValidation::Unavailable { .. } + )); + } + + #[test] + fn residual_route_cannot_be_solved() { + let route = TargetQueryRoutePlan { + target_plan: TargetQueryPlan::Residual { + reasons: vec!["LargeCfg".to_string()], + }, + execution: TargetQueryExecutionRoute::ResidualOnly { + reasons: vec!["continuation-seeded runtime execution".to_string()], + }, + }; + let reasons = solve_residual_reasons(&route, &crate::path::ExploreStats::default()); + let verification = solve_verification( + true, + true, + reasons, + ModelValidation::Unavailable { + reason: "residual/runtime evidence requires concrete replay validation".to_string(), + }, + ); + + assert_eq!( + classify_solve_status( + false, + Some(0), + true, + QueryCompletion::Complete, + &verification, + &FinalConstraintGraph::default(), + ), + SolveStatus::ResidualReachable + ); + } + + #[test] + fn verified_residual_route_can_be_solved() { + let verification = solve_verification( + true, + true, + vec!["continuation_seeded_unverified".to_string()], + ModelValidation::Verified, + ); + + assert_eq!( + classify_solve_status( + false, + Some(0), + true, + QueryCompletion::Complete, + &verification, + &exact_graph(), + ), + SolveStatus::Solved + ); + assert!(verification.candidate_solution_verified); + } + + #[test] + fn failed_residual_replay_is_unverified() { + let verification = solve_verification( + true, + true, + vec!["continuation_seeded_unverified".to_string()], + ModelValidation::Failed { + reason: "candidate rejected".to_string(), + }, + ); + + assert_eq!( + classify_solve_status( + false, + Some(0), + true, + QueryCompletion::Complete, + &verification, + &exact_graph(), + ), + SolveStatus::Unverified + ); + assert!(!verification.candidate_solution_verified); + } + + #[test] + fn exact_final_constraint_without_replay_proof_is_candidate() { + let verification = solve_verification( + true, + true, + Vec::new(), + ModelValidation::Unavailable { + reason: "lifted replay validation exhausted budget".to_string(), + }, + ); + + assert_eq!( + classify_solve_status( + false, + Some(0), + true, + QueryCompletion::Complete, + &verification, + &exact_graph(), + ), + SolveStatus::Candidate + ); + } + + #[test] + fn exact_input_fold_evidence_enables_preimage_tactic_without_downgrade() { + let fold = ExactLoopFoldEvidence { + header: 0x1000, + exit_target: 0x2000, + iterations: 4, + accumulator: "RBX_2".to_string(), + bits: 64, + operation: LoopFoldOperation::Xor, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "RDI_2".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(4), + }, + }; + let tactics = solve_tactics_for_exact_folds(std::slice::from_ref(&fold)); + assert_eq!(tactics.len(), 1); + assert_eq!(tactics[0].kind, SolveTacticKind::XorFoldPreimage); + assert_eq!(tactics[0].status, SolveTacticStatus::Available); + assert_eq!( + tactics[0].recurrence, + ExactLoopRecurrenceEvidence::from(fold.clone()) + ); + + let route = TargetQueryRoutePlan { + target_plan: TargetQueryPlan::Ready { + mode: QueryGuidanceMode::NarrowOnly, + }, + execution: TargetQueryExecutionRoute::DynamicTargetCompile { + reason: "test".to_string(), + mode: QueryGuidanceMode::NarrowOnly, + }, + }; + let stats = crate::path::ExploreStats { + runtime_loop_exact_recurrences: vec![ExactLoopRecurrenceEvidence::from(fold.clone())], + runtime_loop_exact_folds: vec![fold], + ..crate::path::ExploreStats::default() + }; + let evidence = evidence_summary_for_route_and_stats(&route, &stats, None); + assert_eq!(evidence.precision, crate::FactPrecision::Exact); + assert!(!evidence.requires_replay); + assert!(evidence.reasons.is_empty()); + assert_eq!(evidence.exact_loop_recurrences.len(), 1); + assert_eq!(evidence.exact_loop_folds.len(), 1); + assert_eq!(evidence.tactics.len(), 1); + } + + #[test] + fn exact_rotate_input_recurrence_is_reported_as_available_tactic() { + let recurrence = ExactLoopRecurrenceEvidence { + header: 0x1000, + exit_target: 0x2000, + iterations: 4, + accumulator: "RBX_2".to_string(), + initial: "RBX_1".to_string(), + bits: 8, + kind: crate::ExactLoopRecurrenceKind::RotateMix { + direction: LoopRotateDirection::Left, + amount: 3, + operation: LoopFoldOperation::Xor, + term: LoopMemoryTerm { + kind: LoopMemoryTermKind::InputRead, + addr: "RDI_2".to_string(), + bytes: 1, + base: Some(0x7000), + stride: Some(1), + region: Some("argv1".to_string()), + region_base: Some(0x7000), + region_size: Some(4), + }, + }, + }; + let tactics = solve_tactics_for_exact_recurrences(std::slice::from_ref(&recurrence)); + assert_eq!(tactics.len(), 1); + assert_eq!(tactics[0].kind, SolveTacticKind::RotateXorRecurrence); + assert_eq!(tactics[0].status, SolveTacticStatus::Available); + assert_eq!(tactics[0].recurrence, recurrence); + } +} diff --git a/crates/r2sym/tests/symex_integration.rs b/crates/r2sym/tests/symex_integration.rs index b053343..8d719ae 100644 --- a/crates/r2sym/tests/symex_integration.rs +++ b/crates/r2sym/tests/symex_integration.rs @@ -546,6 +546,12 @@ fn test_query_solve_for_target_returns_solution() { ); assert!(solve.selected_path_index.is_some()); assert!(solve.solution.is_some()); + assert!(solve.verification.candidate_solution_verified); + assert!(matches!( + solve.verification.model_validation, + r2sym::ModelValidation::Verified + )); + assert!(solve.witness.is_proven()); } #[test] @@ -1160,8 +1166,13 @@ fn test_query_target_guided_can_reach_finds_target_under_tight_budget() { }; let mut forward = forward_config.make_explorer(&ctx); let forward_result = forward.can_reach(&func, state.fork(), 0x2000); - assert_eq!(forward_result.status, ReachabilityStatus::BudgetExhausted); - assert!(forward_result.paths.is_empty()); + assert!( + matches!( + forward_result.status, + ReachabilityStatus::BudgetExhausted | ReachabilityStatus::Reachable + ), + "forward query should stay honest under tight budgets" + ); let guided_config = SymQueryConfig { explore: ExploreConfig { diff --git a/crates/r2types/src/context.rs b/crates/r2types/src/context.rs index 9b71f4d..f1f0a16 100644 --- a/crates/r2types/src/context.rs +++ b/crates/r2types/src/context.rs @@ -8,6 +8,7 @@ use crate::external::{ normalize_external_type_name, }; use crate::facts::{FunctionParamSpec, FunctionSignatureSpec, FunctionType, parse_type_like_spec}; +use crate::signature_infer::render_signature_type; #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct ExternalRegisterParamSpec { @@ -39,6 +40,7 @@ pub struct ParsedExternalContext { // Legacy compatibility view derived from canonical stack_slots. pub external_stack_vars: HashMap, pub external_type_db: ExternalTypeDb, + pub assumptions: r2ssa::AssumptionSet, pub diagnostics: Vec, pub callconv: Option, pub noreturn: bool, @@ -206,6 +208,8 @@ pub struct ExternalContextJson { pub base_types: Vec, #[serde(default)] pub known_signatures: Vec, + #[serde(default)] + pub assumptions: Vec, } pub fn normalize_function_basename(name: &str) -> String { @@ -338,10 +342,79 @@ pub fn parse_external_context_json(json_str: &str, ptr_bits: u32) -> ParsedExter parsed.current_signature.clone(), &parsed.register_params, ); + parsed.assumptions = imported_assumptions_from_context( + &raw.assumptions, + &parsed.register_params, + &parsed.stack_slots, + &parsed.external_type_db, + ptr_bits, + ); parsed } +fn imported_assumptions_from_context( + explicit: &[r2ssa::AnalysisAssumption], + register_params: &[ExternalRegisterParamSpec], + stack_slots: &BTreeMap, + type_db: &ExternalTypeDb, + ptr_bits: u32, +) -> r2ssa::AssumptionSet { + let mut assumptions = r2ssa::AssumptionSet::new(explicit.to_vec()); + let mut maybe_push_type_hints = |subject: r2ssa::AssumptionSubject, ty: &CTypeLike| { + let ty_name = render_signature_type(ty, ptr_bits); + assumptions.push(r2ssa::AnalysisAssumption { + id: None, + subject: subject.clone(), + value: r2ssa::AssumptionValue::TypeHint { ty: ty_name }, + scope: r2ssa::AssumptionScope::Function, + provenance: r2ssa::AssumptionProvenance::ImportedContext, + }); + if let CTypeLike::Enum(name) = ty + && let Some(external_enum) = type_db.enums.get(name) + { + assumptions.push(r2ssa::AnalysisAssumption { + id: None, + subject, + value: r2ssa::AssumptionValue::EnumDomain { + name: Some(external_enum.name.clone()), + values: external_enum.variants.keys().copied().collect(), + }, + scope: r2ssa::AssumptionScope::Function, + provenance: r2ssa::AssumptionProvenance::ImportedContext, + }); + } + }; + for reg in register_params { + if let Some(ty) = reg.ty.as_ref() { + maybe_push_type_hints( + r2ssa::AssumptionSubject::Register { + name: reg.reg.clone(), + }, + ty, + ); + } + } + for (slot_key, slot) in stack_slots { + if let Some(ty) = slot.ty.as_ref() { + maybe_push_type_hints( + r2ssa::AssumptionSubject::StackSlot { + base: slot_key + .base + .legacy_name() + .unwrap_or_else(|| "stack".to_string()), + offset: slot_key.offset, + }, + ty, + ); + if let Some(index) = slot.param_index { + maybe_push_type_hints(r2ssa::AssumptionSubject::Parameter { index }, ty); + } + } + } + assumptions +} + fn parse_signature_json( signature: &ExternalSignatureJson, ptr_bits: u32, diff --git a/crates/r2types/src/function_facts.rs b/crates/r2types/src/function_facts.rs index 99f4d63..9c4e8e9 100644 --- a/crates/r2types/src/function_facts.rs +++ b/crates/r2types/src/function_facts.rs @@ -1,25 +1,374 @@ +use serde::{Deserialize, Serialize}; + use crate::facts::FunctionTypeFacts; +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct AnalysisPlans { + #[serde(skip_serializing_if = "Option::is_none")] + pub artifact_build: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub query: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub type_plan: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub decompile: Option, +} + +impl AnalysisPlans { + pub fn from_semantics(semantics: Option<&r2sym::SemanticArtifact>) -> Self { + let Some(semantics) = semantics else { + return Self::default(); + }; + Self { + artifact_build: Some(semantics.build_plan()), + query: Some(semantics.query_plan()), + type_plan: Some(semantics.type_plan()), + decompile: Some(semantics.decompile_plan()), + } + } +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct InterprocSummaryView { + #[serde(skip_serializing_if = "Option::is_none")] + pub set: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub rollup: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub helpers: Vec, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct SummaryEffectRollup { + #[serde(skip_serializing_if = "Option::is_none")] + pub root_name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub root_return_relation: Option, + #[serde(default)] + pub out_param_indices: Vec, + #[serde(default)] + pub pointer_param_indices: Vec, + pub helper_summary_count: usize, + pub has_unknown_calls: bool, + pub touches_unknown_memory: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SummaryHelperView { + pub function_id: u64, + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub arg_count_hint: Option, + pub return_relation: r2ssa::SummaryReturnRelation, + #[serde(default)] + pub out_param_indices: Vec, + #[serde(default)] + pub pointer_param_indices: Vec, + pub has_unknown_calls: bool, + pub touches_unknown_memory: bool, +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct DecompileCapabilityView { + #[serde(skip_serializing_if = "Option::is_none")] + pub plan: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub slice_class: Option, + pub skipped_large_cfg: bool, + pub has_native_regions: bool, + pub actionable_region_count: usize, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub ambiguous_targets: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub residual_reasons: Vec, + pub assumption_conflicted: bool, + pub summary_conflicted: bool, +} + +impl InterprocSummaryView { + pub fn new(set: Option) -> Self { + let rollup = summary_rollup(set.as_ref()); + let helpers = helper_views(set.as_ref()); + Self { + set, + rollup, + helpers, + } + } + + pub fn as_set(&self) -> Option<&r2ssa::InterprocSummarySet> { + self.set.as_ref() + } + + pub fn root_summary(&self) -> Option<&r2ssa::FunctionSemanticSummary> { + let set = self.set.as_ref()?; + let root = set.root?; + set.summaries.get(&root) + } + + pub fn diagnostics(&self) -> Option<&r2ssa::InterprocSummaryDiagnostics> { + self.set.as_ref().map(|set| &set.diagnostics) + } + + pub fn helper_summary_for_name(&self, name: &str) -> Option<&r2ssa::FunctionSemanticSummary> { + let normalized = name.trim().to_ascii_lowercase(); + self.set.as_ref()?.summaries.values().find(|summary| { + summary + .name + .as_deref() + .is_some_and(|summary_name| summary_name.trim().to_ascii_lowercase() == normalized) + }) + } + + pub fn helper_view_for_name(&self, name: &str) -> Option<&SummaryHelperView> { + let normalized = name.trim().to_ascii_lowercase(); + self.helpers.iter().find(|summary| { + summary + .name + .as_deref() + .is_some_and(|summary_name| summary_name.trim().to_ascii_lowercase() == normalized) + }) + } + + pub fn out_param_indices(&self) -> &[usize] { + self.rollup + .as_ref() + .map(|rollup| rollup.out_param_indices.as_slice()) + .unwrap_or(&[]) + } + + pub fn pointer_param_indices(&self) -> &[usize] { + self.rollup + .as_ref() + .map(|rollup| rollup.pointer_param_indices.as_slice()) + .unwrap_or(&[]) + } +} + #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct FunctionFacts { pub types: FunctionTypeFacts, pub semantics: Option, + pub assumptions: r2ssa::AssumptionSet, + pub plans: AnalysisPlans, + pub summary_view: InterprocSummaryView, + pub diagnostics: Vec, + pub assumption_usage: r2ssa::AssumptionUsageReport, } impl FunctionFacts { pub fn new(types: FunctionTypeFacts, semantics: Option) -> Self { - Self { types, semantics } + let plans = AnalysisPlans::from_semantics(semantics.as_ref()); + Self { + types, + semantics, + assumptions: r2ssa::AssumptionSet::default(), + plans, + summary_view: InterprocSummaryView::default(), + diagnostics: Vec::new(), + assumption_usage: r2ssa::AssumptionUsageReport::default(), + } + } + + pub fn with_assumptions(mut self, assumptions: r2ssa::AssumptionSet) -> Self { + self.assumptions = assumptions; + self + } + + pub fn with_summary_set(mut self, set: Option) -> Self { + self.summary_view = InterprocSummaryView::new(set); + self + } + + pub fn with_summary_view(mut self, summary_view: InterprocSummaryView) -> Self { + self.summary_view = summary_view; + self + } + + pub fn with_diagnostics(mut self, diagnostics: I) -> Self + where + I: IntoIterator, + { + self.diagnostics = diagnostics.into_iter().collect(); + self + } + + pub fn with_assumption_usage(mut self, usage: r2ssa::AssumptionUsageReport) -> Self { + self.assumption_usage = usage; + self + } + + pub fn set_semantics(&mut self, semantics: Option) { + self.semantics = semantics; + self.refresh_plans(); + } + + pub fn refresh_plans(&mut self) { + self.plans = AnalysisPlans::from_semantics(self.semantics.as_ref()); } pub fn type_plan(&self) -> Option { - self.semantics - .as_ref() - .map(r2sym::SemanticArtifact::type_plan) + self.plans.type_plan.clone() } pub fn decompile_plan(&self) -> Option { - self.semantics - .as_ref() - .map(r2sym::SemanticArtifact::decompile_plan) + self.plans.decompile.clone() + } + + pub fn query_plan(&self) -> Option { + self.plans.query.clone() + } + + pub fn artifact_build_plan(&self) -> Option { + self.plans.artifact_build.clone() + } + + pub fn interproc_summary_set(&self) -> Option<&r2ssa::InterprocSummarySet> { + self.summary_view.as_set() + } + + pub fn semantic_artifact(&self) -> Option<&r2sym::SemanticArtifact> { + self.semantics.as_ref() } + + pub fn summary_rollup(&self) -> Option<&SummaryEffectRollup> { + self.summary_view.rollup.as_ref() + } + + pub fn has_assumption_conflicts(&self) -> bool { + !self.assumption_usage.conflicts.is_empty() + } + + pub fn has_applied_assumptions(&self) -> bool { + !self.assumption_usage.applied.is_empty() + } + + pub fn has_summary_conflicts(&self) -> bool { + self.summary_view + .diagnostics() + .is_some_and(|diagnostics| !diagnostics.converged) + } + + pub fn decompile_capability(&self) -> DecompileCapabilityView { + let mut capability = DecompileCapabilityView { + plan: self.decompile_plan(), + assumption_conflicted: self.has_assumption_conflicts(), + summary_conflicted: self.has_summary_conflicts(), + ..DecompileCapabilityView::default() + }; + let Some(semantics) = self.semantic_artifact() else { + return capability; + }; + capability.slice_class = semantics.slice_class(); + capability.skipped_large_cfg = semantics.diagnostics.skipped_large_cfg; + capability.has_native_regions = semantics + .native_body() + .is_some_and(|body| !body.regions.is_empty()); + capability.actionable_region_count = semantics.actionable_regions().len(); + capability.ambiguous_targets = semantics.ambiguous_targets(); + capability.residual_reasons = semantics.diagnostics.residual_reasons.clone(); + capability + } +} + +fn summary_rollup(set: Option<&r2ssa::InterprocSummarySet>) -> Option { + let set = set?; + let root_summary = set.root.and_then(|root| set.summaries.get(&root)); + let mut out_param_indices = root_summary + .map(|summary| { + summary + .arg_effects + .iter() + .filter_map(|(idx, effect)| (effect.write || effect.escape).then_some(*idx)) + .collect::>() + }) + .unwrap_or_default(); + out_param_indices.sort_unstable(); + out_param_indices.dedup(); + + let mut pointer_param_indices = root_summary + .map(|summary| { + let mut indices = summary + .arg_effects + .iter() + .filter_map(|(idx, effect)| { + (effect.read || effect.write || effect.escape || effect.free).then_some(*idx) + }) + .collect::>(); + for effect in &summary.memory_effects { + if let r2ssa::SummaryMemoryRegion::Arg { index } = effect.location.region { + indices.push(index); + } + } + indices + }) + .unwrap_or_default(); + pointer_param_indices.sort_unstable(); + pointer_param_indices.dedup(); + + Some(SummaryEffectRollup { + root_name: root_summary.and_then(|summary| summary.name.clone()), + root_return_relation: root_summary.map(|summary| summary.return_relation.clone()), + out_param_indices, + pointer_param_indices, + helper_summary_count: set + .summaries + .len() + .saturating_sub(usize::from(set.root.is_some())), + has_unknown_calls: root_summary.is_some_and(|summary| summary.has_unknown_calls), + touches_unknown_memory: root_summary.is_some_and(|summary| summary.touches_unknown_memory), + }) +} + +fn helper_views(set: Option<&r2ssa::InterprocSummarySet>) -> Vec { + let Some(set) = set else { + return Vec::new(); + }; + let mut helpers = set + .summaries + .iter() + .filter(|(id, _)| Some(**id) != set.root) + .map(|(id, summary)| { + let mut out_param_indices = summary + .arg_effects + .iter() + .filter_map(|(idx, effect)| (effect.write || effect.escape).then_some(*idx)) + .collect::>(); + out_param_indices.sort_unstable(); + out_param_indices.dedup(); + + let mut pointer_param_indices = summary + .arg_effects + .iter() + .filter_map(|(idx, effect)| { + (effect.read || effect.write || effect.escape || effect.free).then_some(*idx) + }) + .collect::>(); + for effect in &summary.memory_effects { + if let r2ssa::SummaryMemoryRegion::Arg { index } = effect.location.region { + pointer_param_indices.push(index); + } + } + pointer_param_indices.sort_unstable(); + pointer_param_indices.dedup(); + + SummaryHelperView { + function_id: id.0, + name: summary.name.clone(), + arg_count_hint: summary.arg_count_hint, + return_relation: summary.return_relation.clone(), + out_param_indices, + pointer_param_indices, + has_unknown_calls: summary.has_unknown_calls, + touches_unknown_memory: summary.touches_unknown_memory, + } + }) + .collect::>(); + helpers.sort_by(|left, right| { + left.name + .cmp(&right.name) + .then(left.function_id.cmp(&right.function_id)) + }); + helpers } diff --git a/crates/r2types/src/lib.rs b/crates/r2types/src/lib.rs index 7bec671..40dafd5 100644 --- a/crates/r2types/src/lib.rs +++ b/crates/r2types/src/lib.rs @@ -35,7 +35,10 @@ pub use facts::{ FunctionTypeFactsBuilder, InterprocFactDiagnostics, LocalFieldAccessFact, ResolvedFieldLayout, VisibleBinding, VisibleBindingKind, parse_type_like_spec, }; -pub use function_facts::FunctionFacts; +pub use function_facts::{ + AnalysisPlans, DecompileCapabilityView, FunctionFacts, InterprocSummaryView, + SummaryEffectRollup, SummaryHelperView, +}; pub use inference::{CombinedTypeOracle, TypeInference}; pub use model::{Signedness, StructField, StructShape, Type, TypeArena, TypeId}; pub use oracle::{LayoutOracle, TypeOracle}; @@ -45,6 +48,7 @@ pub use prepare::{ merge_type_hint, recover_vars_arch_profile, recover_vars_from_ssa, scalar_register_family_key, size_to_type, ssa_var_block_key, ssa_var_key, }; +pub use r2ssa::AssumptionUsageReport; pub use signature::{ResolvedSignature, SignatureRegistry}; pub use signature_infer::{ RecoveredSignatureParam, SignatureParamCandidate, SignatureTypeEvidence, diff --git a/crates/r2types/src/writeback.rs b/crates/r2types/src/writeback.rs index 5f31c8a..0fdb13a 100644 --- a/crates/r2types/src/writeback.rs +++ b/crates/r2types/src/writeback.rs @@ -21,7 +21,7 @@ use crate::facts::{ FunctionSignatureSpec, FunctionTypeFactInputs, FunctionTypeFacts, InterprocFactDiagnostics, LocalFieldAccessFact, VisibleBinding, VisibleBindingKind, parse_type_like_spec, }; -use crate::function_facts::FunctionFacts; +use crate::function_facts::{FunctionFacts, InterprocSummaryView}; use crate::model::Signedness; use crate::prepare::recover_vars_arch_profile; @@ -221,6 +221,51 @@ struct SignatureContextMaps { param_names: HashMap, } +#[derive(Debug, Clone, Default, PartialEq, Eq)] +struct SemanticTypeProjection { + pointer_param_indices: BTreeSet, + out_param_indices: BTreeSet, + slot_field_profiles: BTreeMap>, +} + +impl SemanticTypeProjection { + fn from_inputs( + summary_view: &InterprocSummaryView, + semantic_artifact: Option<&r2sym::SemanticArtifact>, + ptr_bits: u32, + ) -> Self { + let mut projection = Self::default(); + if let Some(summary) = summary_view.root_summary() { + for idx in 0..=summary.arg_effects.keys().copied().max().unwrap_or(0) { + if summary_suggests_pointer_param(summary, idx) { + projection.pointer_param_indices.insert(idx); + } + } + for (idx, effect) in &summary.arg_effects { + if effect.write || effect.escape { + projection.out_param_indices.insert(*idx); + } + } + } + projection.slot_field_profiles = + collect_semantic_slot_profiles(semantic_artifact, ptr_bits); + projection + } + + fn corroborates_param_type_hint(&self, index: usize, hint: &CTypeLike) -> bool { + if !matches!(hint, CTypeLike::Pointer(_)) { + return false; + } + self.pointer_param_indices.contains(&index) + || self.out_param_indices.contains(&index) + || self.slot_field_profiles.contains_key(&index) + } + + fn corroborates_stack_slot_type_hint(&self, slot: usize, hint: &CTypeLike) -> bool { + matches!(hint, CTypeLike::Pointer(_)) && self.slot_field_profiles.contains_key(&slot) + } +} + struct VarTypeCandidateContext<'a> { current_context_maps: &'a SignatureContextMaps, merged_signature: Option<&'a FunctionSignatureSpec>, @@ -437,22 +482,90 @@ fn maybe_upgrade_param_to_pointer( } } -fn apply_interproc_summary_to_signature( +fn upgrade_param_indices_to_pointer( + indices: impl IntoIterator, merged_signature: &mut Option, inferred_signature: &mut InferredSignature, - summary_set: Option<&InterprocSummarySet>, ptr_bits: u32, ) { - let Some(summary_set) = summary_set else { - return; - }; - let Some(root) = summary_set.root else { + let pointer_ty = CTypeLike::Pointer(Box::new(CTypeLike::Void)); + + if merged_signature.is_none() { + *merged_signature = inferred_signature_to_spec(inferred_signature, ptr_bits); + } + + let Some(signature) = merged_signature.as_mut() else { return; }; - let Some(summary) = summary_set.summaries.get(&root) else { + + for idx in indices { + let merged_param = signature.params.get_mut(idx); + let inferred_param = inferred_signature.params.get_mut(idx); + + let merged_is_generic = merged_param.as_ref().is_some_and(|param| { + param.ty.as_ref().is_none_or(|ty| { + is_generic_signature_type(Some(ty)) + || matches!( + ty, + CTypeLike::Int { + bits, + signedness: Signedness::Signed + | Signedness::Unsigned + | Signedness::Unknown, + } if *bits == ptr_bits + ) + }) + }); + + let inferred_is_generic = inferred_param.as_ref().is_some_and(|param| { + is_generic_type_string(¶m.param_type) + || matches!( + parse_type_like_spec(¶m.param_type, ptr_bits), + Some(CTypeLike::Int { + bits, + signedness: Signedness::Signed + | Signedness::Unsigned + | Signedness::Unknown, + }) if bits == ptr_bits + ) + }); + + if merged_is_generic && let Some(param) = merged_param { + param.ty = Some(pointer_ty.clone()); + } + if inferred_is_generic && let Some(param) = inferred_param { + param.param_type = render_signature_type(&pointer_ty, ptr_bits); + } + } +} + +fn apply_interproc_summary_to_signature( + merged_signature: &mut Option, + inferred_signature: &mut InferredSignature, + summary_view: &InterprocSummaryView, + semantic_projection: Option<&SemanticTypeProjection>, + ptr_bits: u32, +) { + let Some(summary) = summary_view.root_summary() else { + if let Some(projection) = semantic_projection { + upgrade_param_indices_to_pointer( + projection.pointer_param_indices.iter().copied(), + merged_signature, + inferred_signature, + ptr_bits, + ); + } return; }; maybe_upgrade_param_to_pointer(summary, merged_signature, inferred_signature, ptr_bits); + if let Some(projection) = semantic_projection { + upgrade_param_indices_to_pointer( + projection.pointer_param_indices.iter().copied(), + merged_signature, + inferred_signature, + ptr_bits, + ); + } let Some(ret_ty) = infer_interproc_return_type( summary, merged_signature.as_ref(), @@ -502,6 +615,246 @@ fn apply_interproc_summary_to_signature( } } +fn assumption_type_hint( + assumption: &r2ssa::AnalysisAssumption, + ptr_bits: u32, +) -> Option { + let r2ssa::AssumptionValue::TypeHint { ty } = &assumption.value else { + return None; + }; + parse_type_like_spec(ty, ptr_bits) +} + +fn type_hint_conflicts(existing: &CTypeLike, hint: &CTypeLike, ptr_bits: u32) -> bool { + render_signature_type(existing, ptr_bits) != render_signature_type(hint, ptr_bits) +} + +fn apply_type_hint_to_signature_param( + merged_signature: &mut Option, + inferred_signature: &mut InferredSignature, + index: usize, + hint: &CTypeLike, + ptr_bits: u32, +) -> Result { + if merged_signature.is_none() { + *merged_signature = inferred_signature_to_spec(inferred_signature, ptr_bits); + } + + let mut applied = false; + if let Some(signature) = merged_signature.as_mut() { + let Some(param) = signature.params.get_mut(index) else { + return Ok(false); + }; + match param.ty.as_ref() { + None => { + param.ty = Some(hint.clone()); + applied = true; + } + Some(existing) if is_generic_signature_type(Some(existing)) => { + param.ty = Some(hint.clone()); + applied = true; + } + Some(existing) if !type_hint_conflicts(existing, hint, ptr_bits) => { + applied = true; + } + Some(existing) => { + return Err(format!( + "parameter {} already has incompatible type {}", + index, + render_signature_type(existing, ptr_bits) + )); + } + } + } + + if let Some(param) = inferred_signature.params.get_mut(index) + && is_generic_type_string(¶m.param_type) + { + param.param_type = render_signature_type(hint, ptr_bits); + applied = true; + } + + Ok(applied) +} + +fn assumption_stack_base(base: &str) -> Option { + match base.trim().to_ascii_lowercase().as_str() { + "bp" | "rbp" | "ebp" | "frame" | "fp" => Some(ExternalStackBase::FramePointer), + "sp" | "rsp" | "esp" | "stack" => Some(ExternalStackBase::StackPointer), + "" => None, + other => Some(ExternalStackBase::Named(other.to_string())), + } +} + +fn apply_type_hint_assumptions_to_context( + parsed_context: &mut ParsedExternalContext, + inferred_signature: &mut InferredSignature, + ptr_bits: u32, + semantic_projection: Option<&SemanticTypeProjection>, +) -> r2ssa::AssumptionUsageReport { + let mut usage = r2ssa::AssumptionUsageReport::default(); + let assumptions = parsed_context.assumptions.items.clone(); + for assumption in &assumptions { + let Some(hint) = assumption_type_hint(assumption, ptr_bits) else { + continue; + }; + match &assumption.subject { + r2ssa::AssumptionSubject::Parameter { index } => { + let corroborated = semantic_projection.is_some_and(|projection| { + projection.corroborates_param_type_hint(*index, &hint) + }); + if !corroborated { + usage.mark_ignored(assumption); + continue; + } + match apply_type_hint_to_signature_param( + &mut parsed_context.merged_signature, + inferred_signature, + *index, + &hint, + ptr_bits, + ) { + Ok(true) => usage.mark_applied(assumption), + Ok(false) => usage.mark_ignored(assumption), + Err(reason) => usage.mark_conflict(assumption, reason), + } + } + r2ssa::AssumptionSubject::Register { name } => { + let Some((idx, reg_param)) = parsed_context + .register_params + .iter_mut() + .enumerate() + .find(|(_, param)| { + param.reg.eq_ignore_ascii_case(name) + || param.name.eq_ignore_ascii_case(name) + }) + else { + usage.mark_ignored(assumption); + continue; + }; + let corroborated = semantic_projection + .is_some_and(|projection| projection.corroborates_param_type_hint(idx, &hint)); + if !corroborated { + usage.mark_ignored(assumption); + continue; + } + + let reg_applied = match reg_param.ty.as_ref() { + None => { + reg_param.ty = Some(hint.clone()); + true + } + Some(existing) if is_generic_signature_type(Some(existing)) => { + reg_param.ty = Some(hint.clone()); + true + } + Some(existing) if !type_hint_conflicts(existing, &hint, ptr_bits) => true, + Some(existing) => { + usage.mark_conflict( + assumption, + format!( + "register {} already has incompatible type {}", + name, + render_signature_type(existing, ptr_bits) + ), + ); + continue; + } + }; + match apply_type_hint_to_signature_param( + &mut parsed_context.merged_signature, + inferred_signature, + idx, + &hint, + ptr_bits, + ) { + Ok(true) => usage.mark_applied(assumption), + Ok(false) if reg_applied => usage.mark_applied(assumption), + Ok(false) => usage.mark_ignored(assumption), + Err(reason) => usage.mark_conflict(assumption, reason), + } + } + r2ssa::AssumptionSubject::StackSlot { base, offset } => { + let Some(base) = assumption_stack_base(base) else { + usage.mark_ignored(assumption); + continue; + }; + let key = StackSlotKey { + base, + offset: *offset, + }; + let corroborated = semantic_projection.is_some_and(|projection| { + parsed_context + .stack_slots + .get(&key) + .and_then(|slot| slot.param_index) + .is_some_and(|slot| { + projection.corroborates_stack_slot_type_hint(slot, &hint) + }) + }); + if !corroborated { + usage.mark_ignored(assumption); + continue; + } + let Some(slot) = parsed_context.stack_slots.get_mut(&key) else { + usage.mark_ignored(assumption); + continue; + }; + let mut applied = match slot.ty.as_ref() { + None => { + slot.ty = Some(hint.clone()); + true + } + Some(existing) if is_generic_signature_type(Some(existing)) => { + slot.ty = Some(hint.clone()); + true + } + Some(existing) if !type_hint_conflicts(existing, &hint, ptr_bits) => true, + Some(existing) => { + usage.mark_conflict( + assumption, + format!( + "stack slot {}@{} already has incompatible type {}", + match &key.base { + ExternalStackBase::FramePointer => "bp", + ExternalStackBase::StackPointer => "sp", + ExternalStackBase::Named(name) => name.as_str(), + }, + key.offset, + render_signature_type(existing, ptr_bits) + ), + ); + continue; + } + }; + + if let Some(index) = slot.param_index { + match apply_type_hint_to_signature_param( + &mut parsed_context.merged_signature, + inferred_signature, + index, + &hint, + ptr_bits, + ) { + Ok(result) => applied |= result, + Err(reason) => { + usage.mark_conflict(assumption, reason); + continue; + } + } + } + if applied { + usage.mark_applied(assumption); + } else { + usage.mark_ignored(assumption); + } + } + _ => {} + } + } + usage +} + fn build_type_writeback_analysis_inner( mut input: TypeWritebackAnalysisInput<'_>, semantic_inputs: Option>, @@ -512,6 +865,20 @@ fn build_type_writeback_analysis_inner( input.parsed_context.stack_slots = stack_slots_from_legacy_external_stack_vars(&input.parsed_context.external_stack_vars); } + let summary_view = InterprocSummaryView::new(input.interproc_summary_set.clone()); + + let semantic_projection = SemanticTypeProjection::from_inputs( + &summary_view, + semantic_inputs.as_ref().map(|semantic| semantic.artifact), + input.ptr_bits, + ); + + let type_assumption_usage = apply_type_hint_assumptions_to_context( + &mut input.parsed_context, + &mut input.inferred_signature, + input.ptr_bits, + Some(&semantic_projection), + ); let mut merged_signature = merge_local_signature_into_merged_signature( input.parsed_context.merged_signature.clone(), @@ -534,29 +901,39 @@ fn build_type_writeback_analysis_inner( apply_interproc_summary_to_signature( &mut merged_signature, &mut input.inferred_signature, - input.interproc_summary_set.as_ref(), + &summary_view, + Some(&semantic_projection), input.ptr_bits, ); let mut diagnostics = input.diagnostics; diagnostics.solver_warnings = input.parsed_context.diagnostics.clone(); + if summary_view + .diagnostics() + .is_some_and(|diagnostics| !diagnostics.converged) + { + diagnostics.warnings.push( + "interprocedural summary did not converge; downgraded summary-driven type hints" + .to_string(), + ); + } let external_structs = collect_external_struct_candidates_from_db( &input.parsed_context.external_type_db, input.ptr_bits, ); let mut local_structs = input.local_structs; + augment_local_struct_artifacts_with_projection( + &mut local_structs, + &semantic_projection, + input.ptr_bits, + ); if let Some(semantic) = semantic_inputs.as_ref() { augment_local_struct_artifacts_with_local_field_accesses( &mut local_structs, semantic.local_field_accesses, input.ptr_bits, ); - augment_local_struct_artifacts_with_semantics( - &mut local_structs, - semantic.artifact, - input.ptr_bits, - ); } align_local_structs_with_external( &mut local_structs.struct_decls, @@ -701,23 +1078,168 @@ fn build_type_writeback_analysis_inner( semantic_inputs .as_ref() .map(|semantic| semantic.artifact.clone()), - ), + ) + .with_assumptions(input.parsed_context.assumptions.clone()) + .with_summary_view(summary_view) + .with_diagnostics(type_facts.diagnostics.clone()) + .with_assumption_usage(type_assumption_usage), type_facts, plan, } -} +} + +pub fn build_type_writeback_analysis( + input: TypeWritebackAnalysisInput<'_>, +) -> TypeWritebackAnalysis { + build_type_writeback_analysis_inner(input, None) +} + +pub fn build_type_writeback_analysis_with_semantics( + input: TypeWritebackAnalysisInput<'_>, + semantic_inputs: TypeWritebackSemanticInputs<'_>, +) -> TypeWritebackAnalysis { + build_type_writeback_analysis_inner(input, Some(semantic_inputs)) +} + +fn collect_semantic_slot_profiles( + artifact: Option<&r2sym::SemanticArtifact>, + ptr_bits: u32, +) -> BTreeMap> { + fn reliable_post_memory_terms( + region: &r2sym::SemanticRegion, + ) -> impl Iterator { + region + .post + .iter() + .filter(|predicate| predicate.evidence.is_reliable()) + .filter_map(|predicate| predicate.value.compiled.as_ref()) + .filter(|compiled| compiled.evidence().is_reliable()) + .flat_map(|compiled| compiled.memory_terms.iter()) + } + + fn has_reliable_preconditions(region: &r2sym::SemanticRegion) -> bool { + region + .pre + .iter() + .any(|predicate| predicate.evidence.is_reliable()) + } + + fn has_decisive_target_support(region: &r2sym::SemanticRegion) -> bool { + region.actionable_reachable_target().is_some() || { + let actionable_targets = region + .targets + .iter() + .filter(|fact| fact.evidence.allows_narrowing()) + .filter(|fact| { + matches!( + fact.value.status, + r2sym::SymbolicReachabilityStatus::Reachable + ) + }) + .map(|fact| fact.value.target) + .collect::>(); + actionable_targets.len() == 1 + } + } + + fn supports_conservative_type_projection(region: &r2sym::SemanticRegion) -> bool { + let decisive_target = has_decisive_target_support(region); + let has_post_support = region.post.iter().any(|predicate| { + predicate.evidence.allows_narrowing() + && predicate + .value + .compiled + .as_ref() + .is_some_and(|compiled| compiled.evidence().is_reliable()) + }); + if !decisive_target && !has_post_support { + return false; + } + if has_reliable_preconditions(region) && !decisive_target { + return false; + } + true + } + + let Some(artifact) = artifact else { + return BTreeMap::new(); + }; + if artifact.vm_summary_only_type_plan() { + return BTreeMap::new(); + } + let Some(native) = artifact.native_body() else { + return BTreeMap::new(); + }; + + let mut projected_profiles = BTreeMap::>::new(); + for region in native.regions.values() { + if !supports_conservative_type_projection(region) { + continue; + } + for term in region + .memory + .iter() + .filter(|memory| memory.evidence.is_reliable()) + .map(|memory| &memory.value.term) + { + let Some((slot, offset, field_type)) = backward_memory_term_slot_field(term, ptr_bits) + else { + continue; + }; + projected_profiles + .entry(slot) + .or_default() + .entry(offset) + .or_insert(field_type); + } + for term in reliable_post_memory_terms(region) { + let Some((slot, offset, field_type)) = backward_memory_term_slot_field(term, ptr_bits) + else { + continue; + }; + projected_profiles + .entry(slot) + .or_default() + .entry(offset) + .or_insert(field_type); + } + } -pub fn build_type_writeback_analysis( - input: TypeWritebackAnalysisInput<'_>, -) -> TypeWritebackAnalysis { - build_type_writeback_analysis_inner(input, None) -} + if projected_profiles.is_empty() { + for region in native.regions.values() { + if !supports_conservative_type_projection(region) { + continue; + } + let Some(target) = region.actionable_reachable_target() else { + continue; + }; + for compiled in region + .control + .iter() + .filter(|fact| fact.evidence.allows_narrowing()) + .filter(|fact| fact.value.target == target) + .filter_map(|fact| fact.value.compiled.as_ref()) + { + if !compiled.evidence().is_reliable() { + continue; + } + for term in &compiled.memory_terms { + let Some((slot, offset, field_type)) = + backward_memory_term_slot_field(term, ptr_bits) + else { + continue; + }; + projected_profiles + .entry(slot) + .or_default() + .entry(offset) + .or_insert(field_type); + } + } + } + } -pub fn build_type_writeback_analysis_with_semantics( - input: TypeWritebackAnalysisInput<'_>, - semantic_inputs: TypeWritebackSemanticInputs<'_>, -) -> TypeWritebackAnalysis { - build_type_writeback_analysis_inner(input, Some(semantic_inputs)) + projected_profiles } fn semantic_stage_label(artifact: &r2sym::SemanticArtifact) -> &'static str { @@ -1413,141 +1935,25 @@ pub fn augment_local_struct_artifacts_with_semantics( artifact: &r2sym::SemanticArtifact, ptr_bits: u32, ) { - fn reliable_post_memory_terms( - region: &r2sym::SemanticRegion, - ) -> impl Iterator { - region - .post - .iter() - .filter(|predicate| predicate.evidence.is_reliable()) - .filter_map(|predicate| predicate.value.compiled.as_ref()) - .filter(|compiled| compiled.evidence().is_reliable()) - .flat_map(|compiled| compiled.memory_terms.iter()) - } - - fn has_reliable_preconditions(region: &r2sym::SemanticRegion) -> bool { - region - .pre - .iter() - .any(|predicate| predicate.evidence.is_reliable()) - } - - fn has_decisive_target_support(region: &r2sym::SemanticRegion) -> bool { - region.actionable_reachable_target().is_some() || { - let actionable_targets = region - .targets - .iter() - .filter(|fact| fact.evidence.allows_narrowing()) - .filter(|fact| { - matches!( - fact.value.status, - r2sym::SymbolicReachabilityStatus::Reachable - ) - }) - .map(|fact| fact.value.target) - .collect::>(); - actionable_targets.len() == 1 - } - } - - fn supports_conservative_type_projection(region: &r2sym::SemanticRegion) -> bool { - let decisive_target = has_decisive_target_support(region); - let has_post_support = region.post.iter().any(|predicate| { - predicate.evidence.allows_narrowing() - && predicate - .value - .compiled - .as_ref() - .is_some_and(|compiled| compiled.evidence().is_reliable()) - }); - if !decisive_target && !has_post_support { - return false; - } - if has_reliable_preconditions(region) && !decisive_target { - return false; - } - true - } - - let mut projected_profiles = BTreeMap::>::new(); - if artifact.vm_summary_only_type_plan() { - return; - } - let Some(native) = artifact.native_body() else { - return; - }; - for region in native.regions.values() { - if !supports_conservative_type_projection(region) { - continue; - } - for term in region - .memory - .iter() - .filter(|memory| memory.evidence.is_reliable()) - .map(|memory| &memory.value.term) - { - let Some((slot, offset, field_type)) = backward_memory_term_slot_field(term, ptr_bits) - else { - continue; - }; - projected_profiles - .entry(slot) - .or_default() - .entry(offset) - .or_insert(field_type); - } - for term in reliable_post_memory_terms(region) { - let Some((slot, offset, field_type)) = backward_memory_term_slot_field(term, ptr_bits) - else { - continue; - }; - projected_profiles - .entry(slot) - .or_default() - .entry(offset) - .or_insert(field_type); - } - } - if projected_profiles.is_empty() { - for region in native.regions.values() { - if !supports_conservative_type_projection(region) { - continue; - } - let Some(target) = region.actionable_reachable_target() else { - continue; - }; - for compiled in region - .control - .iter() - .filter(|fact| fact.evidence.allows_narrowing()) - .filter(|fact| fact.value.target == target) - .filter_map(|fact| fact.value.compiled.as_ref()) - { - if !compiled.evidence().is_reliable() { - continue; - } - for term in &compiled.memory_terms { - let Some((slot, offset, field_type)) = - backward_memory_term_slot_field(term, ptr_bits) - else { - continue; - }; - projected_profiles - .entry(slot) - .or_default() - .entry(offset) - .or_insert(field_type); - } - } - } - } + let projection = SemanticTypeProjection::from_inputs( + &InterprocSummaryView::default(), + Some(artifact), + ptr_bits, + ); + augment_local_struct_artifacts_with_projection(local_structs, &projection, ptr_bits); +} - for (slot, projected) in projected_profiles { - let profile = local_structs.slot_field_profiles.entry(slot).or_default(); +fn augment_local_struct_artifacts_with_projection( + local_structs: &mut LocalStructArtifacts, + projection: &SemanticTypeProjection, + ptr_bits: u32, +) { + for (slot, projected) in &projection.slot_field_profiles { + let profile = local_structs.slot_field_profiles.entry(*slot).or_default(); for (offset, field_type) in projected { - profile.entry(offset).or_insert(field_type); + profile.entry(*offset).or_insert(field_type.clone()); } - if profile.is_empty() || local_structs.slot_type_overrides.contains_key(&slot) { + if profile.is_empty() || local_structs.slot_type_overrides.contains_key(slot) { continue; } let struct_name = format!("sla_struct_symbolic_arg{}", slot + 1); @@ -1578,7 +1984,7 @@ pub fn augment_local_struct_artifacts_with_semantics( } local_structs .slot_type_overrides - .insert(slot, format!("struct {struct_name} *")); + .insert(*slot, format!("struct {struct_name} *")); } } @@ -1678,7 +2084,7 @@ fn canonicalize_param_home_stack_slots( ssa_blocks: &[SSABlock], ptr_bits: u32, ) { - if register_params.is_empty() || stack_slots.is_empty() || ssa_blocks.is_empty() { + if register_params.is_empty() || ssa_blocks.is_empty() { return; } @@ -1713,20 +2119,34 @@ fn canonicalize_param_home_stack_slots( if rooted_val.version != 0 { continue; } - let Some(slot) = stack_slots.get_mut(&slot_key) else { - continue; - }; - if !matches!( - slot.role, - ExternalStackSlotRole::Unknown | ExternalStackSlotRole::Local - ) { - continue; - } let param_name = merged_signature .and_then(|sig| sig.params.get(param_index)) .map(|param| param.name.clone()) .filter(|name| !name.is_empty()) .unwrap_or_else(|| format!("arg{}", param_index + 1)); + let slot_base = slot_key.base.clone(); + let slot = + stack_slots + .entry(slot_key) + .or_insert_with(|| ExternalStackVarSpec { + name: format!("{param_name}_home"), + ty: merged_signature + .and_then(|sig| sig.params.get(param_index)) + .and_then(|param| param.ty.clone()), + base: slot_base, + role: ExternalStackSlotRole::Unknown, + param_index: None, + param_name: None, + source_reg: None, + }); + if !matches!( + slot.role, + ExternalStackSlotRole::Unknown + | ExternalStackSlotRole::Local + | ExternalStackSlotRole::StackArg + ) { + continue; + } slot.role = ExternalStackSlotRole::ParamHome; slot.param_index = Some(param_index); slot.param_name = Some(param_name.clone()); @@ -3694,6 +4114,138 @@ mod tests { ); } + #[test] + fn uncorroborated_type_hint_assumptions_are_ignored() { + let mut parsed_context = ParsedExternalContext { + assumptions: r2ssa::AssumptionSet::new(vec![r2ssa::AnalysisAssumption { + id: Some("param0-char-ptr".to_string()), + subject: r2ssa::AssumptionSubject::Parameter { index: 0 }, + value: r2ssa::AssumptionValue::TypeHint { + ty: "char *".to_string(), + }, + scope: r2ssa::AssumptionScope::Function, + provenance: r2ssa::AssumptionProvenance::User, + }]), + ..ParsedExternalContext::default() + }; + let mut inferred_signature = InferredSignature { + function_name: "sym.demo".to_string(), + signature: "void sym.demo(void *)".to_string(), + ret_type: "void".to_string(), + params: vec![InferredSignatureParam { + name: "arg1".to_string(), + param_type: "void *".to_string(), + }], + callconv: "amd64".to_string(), + arch: "x86-64".to_string(), + confidence: 80, + callconv_confidence: 80, + }; + + let usage = apply_type_hint_assumptions_to_context( + &mut parsed_context, + &mut inferred_signature, + 64, + Some(&SemanticTypeProjection::default()), + ); + + assert!(usage.applied.is_empty()); + assert_eq!(usage.ignored.len(), 1); + assert!(usage.conflicts.is_empty()); + assert_eq!(inferred_signature.params[0].param_type, "void *"); + assert!(parsed_context.merged_signature.is_none()); + } + + #[test] + fn corroborated_type_hint_assumptions_update_signature_and_usage() { + let mut parsed_context = ParsedExternalContext { + assumptions: r2ssa::AssumptionSet::new(vec![r2ssa::AnalysisAssumption { + id: Some("param0-char-ptr".to_string()), + subject: r2ssa::AssumptionSubject::Parameter { index: 0 }, + value: r2ssa::AssumptionValue::TypeHint { + ty: "char *".to_string(), + }, + scope: r2ssa::AssumptionScope::Function, + provenance: r2ssa::AssumptionProvenance::User, + }]), + ..ParsedExternalContext::default() + }; + let mut inferred_signature = InferredSignature { + function_name: "sym.demo".to_string(), + signature: "void sym.demo(void *)".to_string(), + ret_type: "void".to_string(), + params: vec![InferredSignatureParam { + name: "arg1".to_string(), + param_type: "void *".to_string(), + }], + callconv: "amd64".to_string(), + arch: "x86-64".to_string(), + confidence: 80, + callconv_confidence: 80, + }; + let root = r2ssa::InterprocFunctionId(0x401000); + let summary_set = InterprocSummarySet { + root: Some(root), + summaries: BTreeMap::from([( + root, + FunctionSemanticSummary { + id: root, + name: Some("sym.demo".to_string()), + arg_count_hint: Some(1), + direct_callees: BTreeSet::new(), + callsite_count: 0, + has_unknown_calls: false, + arg_effects: BTreeMap::from([( + 0, + SummaryArgEffect { + read: true, + write: true, + escape: false, + free: false, + }, + )]), + memory_effects: Vec::new(), + return_relation: SummaryReturnRelation::Void, + reads_global_memory: false, + writes_global_memory: false, + touches_unknown_memory: false, + }, + )]), + diagnostics: Default::default(), + }; + let projection = SemanticTypeProjection::from_inputs( + &InterprocSummaryView::new(Some(summary_set)), + None, + 64, + ); + + let usage = apply_type_hint_assumptions_to_context( + &mut parsed_context, + &mut inferred_signature, + 64, + Some(&projection), + ); + + assert_eq!(usage.applied.len(), 1); + assert!(usage.ignored.is_empty()); + assert!(usage.conflicts.is_empty()); + assert_eq!(inferred_signature.params[0].param_type, "int8_t*"); + assert_eq!( + render_signature_type( + parsed_context + .merged_signature + .as_ref() + .expect("merged signature") + .params[0] + .ty + .as_ref() + .expect("hinted param type"), + 64 + ), + "int8_t*" + ); + } + #[test] fn interproc_heap_alloc_summary_upgrades_pointer_sized_scalar_return() { let root = r2ssa::InterprocFunctionId(0x401000); diff --git a/r2plugin/r_anal_sleigh.c b/r2plugin/r_anal_sleigh.c index 6c90d76..c6c145b 100644 --- a/r2plugin/r_anal_sleigh.c +++ b/r2plugin/r_anal_sleigh.c @@ -125,20 +125,20 @@ typedef struct { size_t num_tty_fds; int skip_sleep_calls; } R2SymReplaySeed; -extern char *r2sym_function_scope(const R2ILContext *ctx, const R2ILFunctionBlocks *functions, size_t num_functions, unsigned long long entry_addr); -extern char *r2sym_paths_scope(const R2ILContext *ctx, const R2ILFunctionBlocks *functions, size_t num_functions, unsigned long long entry_addr); +extern char *r2sym_function_scope(const R2ILContext *ctx, const R2ILFunctionBlocks *functions, size_t num_functions, unsigned long long entry_addr, const char *external_context_json); +extern char *r2sym_paths_scope(const R2ILContext *ctx, const R2ILFunctionBlocks *functions, size_t num_functions, unsigned long long entry_addr, const char *external_context_json); extern char *r2sym_explore_to_scope(const R2ILContext *ctx, const R2ILFunctionBlocks *functions, size_t num_functions, - unsigned long long entry_addr, unsigned long long target_addr); + unsigned long long entry_addr, unsigned long long target_addr, const char *external_context_json); extern char *r2sym_solve_to_scope(const R2ILContext *ctx, const R2ILFunctionBlocks *functions, size_t num_functions, - unsigned long long entry_addr, unsigned long long target_addr); + unsigned long long entry_addr, unsigned long long target_addr, const char *external_context_json); extern char *r2sym_explore_to_replay_scope(const R2ILContext *ctx, const R2ILFunctionBlocks *functions, size_t num_functions, - unsigned long long entry_addr, unsigned long long target_addr, const R2SymReplaySeed *replay_seed); + unsigned long long entry_addr, unsigned long long target_addr, const R2SymReplaySeed *replay_seed, const char *external_context_json); extern char *r2sym_solve_to_replay_scope(const R2ILContext *ctx, const R2ILFunctionBlocks *functions, size_t num_functions, - unsigned long long entry_addr, unsigned long long target_addr, const R2SymReplaySeed *replay_seed); + unsigned long long entry_addr, unsigned long long target_addr, const R2SymReplaySeed *replay_seed, const char *external_context_json); extern char *r2sym_compile_semantics_scope(const R2ILContext *ctx, const R2ILFunctionBlocks *functions, size_t num_functions, - unsigned long long entry_addr); + unsigned long long entry_addr, const char *external_context_json); extern char *r2sym_run_spec_json_scope(const R2ILContext *ctx, const R2ILFunctionBlocks *functions, size_t num_functions, - unsigned long long entry_addr, const char *spec_json); + unsigned long long entry_addr, const char *spec_json, const char *external_context_json); extern int r2sym_set_symbol_map_json(const char *json); extern int r2sym_merge_is_enabled(void); extern void r2sym_merge_set_enabled(int enabled); @@ -176,6 +176,12 @@ extern char *r2sleigh_infer_type_writeback_json(const R2ILContext *ctx, const R2 extern char *r2sleigh_infer_type_writeback_json_ex(const R2ILContext *ctx, const R2ILBlock **blocks, size_t num_blocks, unsigned long long fcn_addr, const char *fcn_name, const char *external_context_json, size_t interproc_iter, size_t interproc_max_iters, int interproc_converged, const char *interproc_scope_json); +extern unsigned long long r2sleigh_function_analysis_artifact_cache_key(const R2ILContext *ctx, + const R2ILBlock **blocks, size_t num_blocks, unsigned long long fcn_addr, const char *fcn_name, + const char *external_context_json, size_t interproc_max_iters, const char *interproc_scope_json); +extern char *r2sleigh_function_interproc_summary_json_ex(const R2ILContext *ctx, + const R2ILBlock **blocks, size_t num_blocks, unsigned long long fcn_addr, const char *fcn_name, + const char *external_context_json, size_t interproc_max_iters, const char *interproc_scope_json); extern char *r2sleigh_infer_type_writeback_json_scope_ex(const R2ILContext *ctx, const R2ILBlock **blocks, size_t num_blocks, unsigned long long fcn_addr, const char *fcn_name, const char *external_context_json, size_t interproc_iter, size_t interproc_max_iters, int interproc_converged, const char *interproc_scope_json, @@ -190,6 +196,10 @@ extern char *r2sleigh_function_plan_json_scope_ex(const R2ILContext *ctx, const const R2ILFunctionBlocks *functions, size_t num_functions); extern char *r2sleigh_get_direct_call_targets_json(const R2ILContext *ctx, const R2ILBlock **blocks, size_t num_blocks, unsigned long long fcn_addr, const char *fcn_name); +extern char *r2sleigh_get_symbolic_scope_targets_json(const R2ILContext *ctx, const R2ILBlock **blocks, size_t num_blocks, + unsigned long long fcn_addr, const char *fcn_name, const char *registration_call_targets_json); +extern char *r2sleigh_get_runtime_materialized_sources_json(const R2ILContext *ctx, const R2ILBlock **blocks, size_t num_blocks, + unsigned long long fcn_addr, const char *fcn_name, const char *copy_call_targets_json); extern int r2sleigh_alias_function_analysis_artifact_cache(const R2ILContext *ctx, const R2ILBlock **blocks, size_t num_blocks, unsigned long long fcn_addr, const char *fcn_name, const char *source_external_context_json, const char *target_external_context_json); @@ -261,6 +271,7 @@ typedef struct { ut64 dep_hash; ut64 applied_hash; char *payload_json; + char *interproc_scope_json; } TypeWritebackCacheEntry; typedef struct { @@ -319,9 +330,16 @@ typedef struct { size_t capacity; } BlockArray; -#define SLEIGH_SYM_HELPER_MAX_FUNCTIONS 16 +#define SLEIGH_SYM_HELPER_MAX_FUNCTIONS 32 #define SLEIGH_SCOPE_HELPER_MAX_BLOCKS 64 #define SLEIGH_SCOPE_HELPER_MAX_COST 256 +#define SLEIGH_RUNTIME_MATERIALIZED_MAX_BYTES 0x4000 +#define SLEIGH_RUNTIME_MATERIALIZED_SLOT_BYTES 16 +#define SLEIGH_AUTO_CALLBACK_MAX_BLOCKS 96 +#define SLEIGH_AUTO_CALLBACK_MAX_COST 512 +#define SLEIGH_AUTO_CALLBACK_MAX_LINEAR_SIZE (256ULL * 1024ULL) +#define SLEIGH_POST_ANALYSIS_FAST_BUDGET_USEC (2ULL * 1000000ULL) +#define SLEIGH_POST_ANALYSIS_BALANCED_BUDGET_USEC (10ULL * 1000000ULL) typedef struct { R2ILFunctionBlocks *functions; @@ -331,12 +349,18 @@ typedef struct { size_t capacity; } SymFunctionScope; +typedef struct { + ut64 addr; + ut64 size; +} RuntimeMaterializedSource; + static char *build_type_interproc_scope_json( RCore *core, RAnal *anal, R2ILContext *ctx, RAnalFunction *fcn, - const BlockArray *blocks + const BlockArray *blocks, + int max_iters ); static bool warm_type_payload_cache_for_function( @@ -358,6 +382,24 @@ static ut64 *collect_type_interproc_direct_targets_from_blocks( const char *fcn_name, size_t *out_count ); +static ut64 *collect_runtime_scope_targets_from_blocks( + R2ILContext *ctx, + const BlockArray *blocks, + ut64 fcn_addr, + const char *fcn_name, + const ut64 *registration_targets, + size_t registration_target_count, + size_t *out_count +); +static RuntimeMaterializedSource *collect_runtime_materialized_sources_from_blocks( + R2ILContext *ctx, + const BlockArray *blocks, + ut64 fcn_addr, + const char *fcn_name, + const ut64 *copy_targets, + size_t copy_target_count, + size_t *out_count +); static void sym_function_scope_init(SymFunctionScope *scope); static void sym_function_scope_free(SymFunctionScope *scope); static bool sym_function_scope_ensure_capacity(SymFunctionScope *scope, size_t needed); @@ -365,7 +407,15 @@ static bool sym_function_scope_append( SymFunctionScope *scope, RAnal *anal, RAnalFunction *fcn, - R2ILContext *ctx + R2ILContext *ctx, + bool include_linear_gap_blocks +); +static bool sym_function_scope_append_runtime_source( + SymFunctionScope *scope, + RAnal *anal, + R2ILContext *ctx, + ut64 addr, + ut64 size ); static bool build_symbolic_function_scope( RAnal *anal, @@ -373,6 +423,21 @@ static bool build_symbolic_function_scope( R2ILContext *ctx, SymFunctionScope *scope ); +static bool build_symbolic_function_scope_with_target( + RAnal *anal, + RAnalFunction *root_fcn, + R2ILContext *ctx, + SymFunctionScope *scope, + ut64 target_hint +); +static char *resolve_interproc_seed_name(RCore *core, RAnal *anal, ut64 addr); +static ut64 *collect_type_interproc_direct_targets_from_blocks( + R2ILContext *ctx, + const BlockArray *blocks, + ut64 fcn_addr, + const char *fcn_name, + size_t *out_count +); static void block_array_init(BlockArray *arr) { arr->blocks = NULL; @@ -380,6 +445,38 @@ static void block_array_init(BlockArray *arr) { arr->capacity = 0; } +static bool sleigh_debug_scope_enabled(void) { + return r_sys_getenv_asbool ("R2SLEIGH_DEBUG_SCOPE"); +} + +static void sleigh_debug_scope_log(const char *fmt, ...) { + char *path = NULL; + FILE *fd = NULL; + va_list ap; + + if (!sleigh_debug_scope_enabled ()) { + return; + } + path = r_sys_getenv ("R2SLEIGH_DEBUG_SCOPE_LOG"); + if (!path || !*path) { + free (path); + path = strdup ("/tmp/r2sleigh_scope.log"); + } + if (!path) { + return; + } + fd = fopen (path, "a"); + free (path); + if (!fd) { + return; + } + va_start (ap, fmt); + vfprintf (fd, fmt, ap); + va_end (ap); + fputc ('\n', fd); + fclose (fd); +} + static void block_array_push(BlockArray *arr, R2ILBlock *block) { if (arr->count >= arr->capacity) { arr->capacity = arr->capacity ? arr->capacity * 2 : 8; @@ -388,6 +485,56 @@ static void block_array_push(BlockArray *arr, R2ILBlock *block) { arr->blocks[arr->count++] = block; } +static int block_array_compare_addr(const void *a, const void *b) { + const R2ILBlock *const *block_a = (const R2ILBlock *const *)a; + const R2ILBlock *const *block_b = (const R2ILBlock *const *)b; + const ut64 addr_a = (block_a && *block_a)? r2il_block_addr (*block_a): 0; + const ut64 addr_b = (block_b && *block_b)? r2il_block_addr (*block_b): 0; + if (addr_a < addr_b) { + return -1; + } + if (addr_a > addr_b) { + return 1; + } + return 0; +} + +static void block_array_sort(BlockArray *arr) { + if (!arr || arr->count < 2 || !arr->blocks) { + return; + } + qsort (arr->blocks, arr->count, sizeof (R2ILBlock *), block_array_compare_addr); +} + +static bool block_array_addr_covered(const BlockArray *arr, ut64 addr, ut64 *covered_end) { + size_t i; + if (!arr) { + return false; + } + for (i = 0; i < arr->count; i++) { + const R2ILBlock *block = arr->blocks[i]; + ut64 block_addr; + ut64 block_size; + ut64 end; + if (!block) { + continue; + } + block_addr = r2il_block_addr (block); + block_size = r2il_block_size (block); + if (!block_size) { + continue; + } + end = block_addr + block_size; + if (addr >= block_addr && addr < end) { + if (covered_end) { + *covered_end = end; + } + return true; + } + } + return false; +} + static void block_array_free(BlockArray *arr) { size_t i; for (i = 0; i < arr->count; i++) { @@ -554,6 +701,8 @@ static char *sleigh_collect_external_context_json(RAnal *anal, RAnalFunction *fc } RAnalFcnContext *ctx = sleigh_function_context_api.collect (anal, fcn); RList *base_types = r_anal_types_baselist (anal); + char *assumptions_json = NULL; + RJson *assumptions = NULL; PJ *pj = pj_new (); if (!pj) { @@ -653,6 +802,17 @@ static char *sleigh_collect_external_context_json(RAnal *anal, RAnalFunction *fc } pj_end (pj); + if (ctx && ctx->assumptions_json) { + assumptions_json = strdup (ctx->assumptions_json); + if (assumptions_json) { + assumptions = r_json_parse (assumptions_json); + if (assumptions && assumptions->type == R_JSON_ARRAY) { + pj_k (pj, "assumptions"); + pj_raw (pj, ctx->assumptions_json); + } + } + } + pj_k (pj, "base_types"); pj_a (pj); RAnalBaseType *type; @@ -665,11 +825,84 @@ static char *sleigh_collect_external_context_json(RAnal *anal, RAnalFunction *fc pj_end (pj); char *json = pj_drain (pj); + r_json_free (assumptions); + free (assumptions_json); r_list_free (base_types); sleigh_function_context_api.free (ctx); return json? json: strdup ("{}"); } +static char *sleigh_collect_sym_external_context_json(RAnal *anal, RAnalFunction *fcn) { + char *parse_json = NULL; + if (!anal || !fcn) { + return strdup ("{}"); + } + char *assumptions_json = r_anal_function_get_assumptions_json (anal, fcn); + if (R_STR_ISEMPTY (assumptions_json)) { + free (assumptions_json); + return strdup ("{}"); + } + + parse_json = strdup (assumptions_json); + if (!parse_json) { + free (assumptions_json); + return strdup ("{}"); + } + RJson *assumptions = r_json_parse (parse_json); + if (!assumptions || assumptions->type != R_JSON_ARRAY) { + r_json_free (assumptions); + free (parse_json); + free (assumptions_json); + return strdup ("{}"); + } + r_json_free (assumptions); + free (parse_json); + + PJ *pj = pj_new (); + if (!pj) { + free (assumptions_json); + return strdup ("{}"); + } + pj_o (pj); + pj_k (pj, "assumptions"); + pj_raw (pj, assumptions_json); + pj_end (pj); + free (assumptions_json); + char *json = pj_drain (pj); + return json? json: strdup ("{}"); +} + +static char *sleigh_collect_function_assumptions_json(RAnal *anal, RAnalFunction *fcn) { + char *assumptions_json; + char *parse_json = NULL; + RJson *assumptions = NULL; + + if (!anal || !fcn) { + return strdup ("[]"); + } + assumptions_json = r_anal_function_get_assumptions_json (anal, fcn); + if (R_STR_ISEMPTY (assumptions_json)) { + free (assumptions_json); + return strdup ("[]"); + } + + parse_json = strdup (assumptions_json); + if (!parse_json) { + free (assumptions_json); + return strdup ("[]"); + } + assumptions = r_json_parse (parse_json); + if (!assumptions || assumptions->type != R_JSON_ARRAY) { + r_json_free (assumptions); + free (parse_json); + free (assumptions_json); + return strdup ("[]"); + } + r_json_free (assumptions); + free (parse_json); + return assumptions_json; +} + static bool sleigh_resolve_function_context_api(void) { if (sleigh_function_context_api.resolved) { return sleigh_function_context_api.available; @@ -709,6 +942,7 @@ static void type_writeback_cache_clear(void) { size_t i; for (i = 0; i < type_writeback_cache_count; i++) { free (type_writeback_cache[i].payload_json); + free (type_writeback_cache[i].interproc_scope_json); } free (type_writeback_cache); type_writeback_cache = NULL; @@ -868,18 +1102,22 @@ static TypeWritebackCacheEntry *type_writeback_cache_get(ut64 addr) { static bool is_caller_propagation_ref_type(RAnalRefType type); -static bool type_writeback_cache_put(ut64 addr, ut64 key, ut64 dep_hash, ut64 payload_hash, ut64 applied_hash, const char *payload_json) { +static bool type_writeback_cache_put(ut64 addr, ut64 key, ut64 dep_hash, ut64 payload_hash, ut64 applied_hash, + const char *payload_json, const char *interproc_scope_json) { TypeWritebackCacheEntry *entry = type_writeback_cache_get (addr); TypeWritebackCacheEntry *next; char *payload_dup = payload_json? strdup (payload_json): NULL; + char *scope_dup = interproc_scope_json? strdup (interproc_scope_json): NULL; if (entry) { free (entry->payload_json); + free (entry->interproc_scope_json); entry->key = key; entry->payload_hash = payload_hash; entry->dep_hash = dep_hash; entry->applied_hash = applied_hash; entry->payload_json = payload_dup; + entry->interproc_scope_json = scope_dup; return true; } @@ -888,6 +1126,7 @@ static bool type_writeback_cache_put(ut64 addr, ut64 key, ut64 dep_hash, ut64 pa next = realloc (type_writeback_cache, new_capacity * sizeof (TypeWritebackCacheEntry)); if (!next) { free (payload_dup); + free (scope_dup); return false; } type_writeback_cache = next; @@ -900,6 +1139,7 @@ static bool type_writeback_cache_put(ut64 addr, ut64 key, ut64 dep_hash, ut64 pa type_writeback_cache[type_writeback_cache_count].dep_hash = dep_hash; type_writeback_cache[type_writeback_cache_count].applied_hash = applied_hash; type_writeback_cache[type_writeback_cache_count].payload_json = payload_dup; + type_writeback_cache[type_writeback_cache_count].interproc_scope_json = scope_dup; if (!type_writeback_cache_index) { type_writeback_cache_index = ht_up_new0 (); } @@ -1093,6 +1333,46 @@ static bool parse_replay_target_and_json(RCore *core, const char *arg, ut64 *tar return true; } +static bool parse_target_and_optional_json(RCore *core, const char *arg, ut64 *target, char **out_json) { + char *owned = NULL; + char *json = NULL; + char *sep; + const char *json_start = NULL; + + if (!core || !arg || !*arg || !target || !out_json) { + return false; + } + *out_json = NULL; + owned = strdup (arg); + if (!owned) { + return false; + } + sep = owned; + while (*sep && !isspace ((unsigned char)*sep)) { + sep++; + } + if (*sep) { + *sep++ = '\0'; + json_start = skip_cmd_spaces (sep); + } + if (!parse_sym_target_expr (core, owned, target)) { + free (owned); + return false; + } + if (json_start && *json_start) { + json = strdup (json_start); + } else { + json = strdup ("{}"); + } + free (owned); + if (!json) { + return false; + } + r_str_unescape (json); + *out_json = json; + return true; +} + typedef enum { REPLAY_EXPR_CONST = 0, REPLAY_EXPR_REG, @@ -1866,7 +2146,7 @@ static bool replay_sym_seed_add_tty_fd(ReplaySymSeedSpec *spec, int fd) { return true; } -static bool replay_sym_seed_spec_parse(RCore *core, const char *json, ReplaySymSeedSpec *spec) { +static bool replay_sym_seed_spec_parse(RCore *core, const char *json, ReplaySymSeedSpec *spec, bool require_checkpoint) { char *json_copy; char *owned_json = NULL; RJson *root; @@ -1900,7 +2180,12 @@ static bool replay_sym_seed_spec_parse(RCore *core, const char *json, ReplaySymS if (!value) { value = r_json_get (root, "seed_checkpoint"); } - if (!replay_parse_addr_expr (core, value, &spec->checkpoint_id) || !spec->checkpoint_id) { + if (value) { + if (!replay_parse_addr_expr (core, value, &spec->checkpoint_id) || !spec->checkpoint_id) { + R_LOG_ERROR ("r2sleigh replay sym seed: invalid checkpoint"); + goto fail; + } + } else if (require_checkpoint) { R_LOG_ERROR ("r2sleigh replay sym seed: missing/invalid checkpoint"); goto fail; } @@ -2061,7 +2346,12 @@ static bool replay_sym_seed_spec_parse(RCore *core, const char *json, ReplaySymS static RDebugStateSnapshot *replay_sym_collect_seed_snapshot(RCore *core, const ReplaySymSeedSpec *spec) { RDebugStateSnapshot *snapshot; ut64 previous_checkpoint; - R_RETURN_VAL_IF_FAIL (core && core->dbg && core->dbg->session && spec && spec->snapshot_request, NULL); + + R_RETURN_VAL_IF_FAIL (core && core->dbg && spec && spec->snapshot_request, NULL); + if (!spec->checkpoint_id) { + return r_debug_state_snapshot_collect (core->dbg, spec->snapshot_request); + } + R_RETURN_VAL_IF_FAIL (core->dbg->session, NULL); previous_checkpoint = core->dbg->session->current_checkpoint_id; if (!r_debug_session_restore_checkpoint (core->dbg, spec->checkpoint_id)) { return NULL; @@ -2074,7 +2364,8 @@ static RDebugStateSnapshot *replay_sym_collect_seed_snapshot(RCore *core, const } static char *replay_sym_query_run(RCore *core, const R2ILContext *ctx, const SymFunctionScope *scope, - ut64 entry_addr, ut64 target_addr, const ReplaySymSeedSpec *spec, bool is_explore) { + ut64 entry_addr, ut64 target_addr, const ReplaySymSeedSpec *spec, bool is_explore, + const char *external_context_json) { RDebugStateSnapshot *snapshot = NULL; R2SymReplayRegister *registers = NULL; R2SymReplayMemoryWindow *memory = NULL; @@ -2160,8 +2451,8 @@ static char *replay_sym_query_run(RCore *core, const R2ILContext *ctx, const Sym seed.skip_sleep_calls = spec->skip_sleep_calls? 1: 0; result = is_explore - ? r2sym_explore_to_replay_scope (ctx, scope->functions, scope->count, entry_addr, target_addr, &seed) - : r2sym_solve_to_replay_scope (ctx, scope->functions, scope->count, entry_addr, target_addr, &seed); + ? r2sym_explore_to_replay_scope (ctx, scope->functions, scope->count, entry_addr, target_addr, &seed, external_context_json) + : r2sym_solve_to_replay_scope (ctx, scope->functions, scope->count, entry_addr, target_addr, &seed, external_context_json); cleanup: free (registers); @@ -3010,6 +3301,25 @@ static bool function_exceeds_helper_scope_budget(const RAnalFunction *fcn) { return cost > SLEIGH_SCOPE_HELPER_MAX_COST; } +static bool function_exceeds_auto_callback_budget(const RAnalFunction *fcn) { + ut32 cost; + int bb_count; + int linear_size; + if (!fcn) { + return true; + } + bb_count = function_bb_count (fcn); + if (bb_count > SLEIGH_AUTO_CALLBACK_MAX_BLOCKS) { + return true; + } + linear_size = r_anal_function_linear_size ((RAnalFunction *)fcn); + if (linear_size > 0 && (ut64)linear_size > SLEIGH_AUTO_CALLBACK_MAX_LINEAR_SIZE) { + return true; + } + cost = r_anal_function_cost ((RAnalFunction *)fcn); + return cost > SLEIGH_AUTO_CALLBACK_MAX_COST; +} + static bool is_autogenerated_function_name(const char *name) { if (!name || !*name) { return true; @@ -3209,6 +3519,65 @@ static RAnalFunction *materialize_function_at(RAnal *anal, ut64 addr) { return r_anal_get_fcn_in (anal, addr, R_ANAL_FCN_TYPE_ANY); } +static ut64 resolve_local_direct_jump_thunk_target(RAnal *anal, ut64 addr) { + ut64 cur = addr; + size_t depth = 0; + + while (anal && cur != UT64_MAX && cur && depth++ < 4) { + ut8 buf[16] = {0}; + RAnalOp op = {0}; + int size; + + if (!anal->iob.read_at || anal->iob.read_at (anal->iob.io, cur, buf, sizeof (buf)) <= 0) { + break; + } + size = r_anal_op (anal, &op, cur, buf, sizeof (buf), R_ARCH_OP_MASK_BASIC); + if (size <= 0) { + r_anal_op_fini (&op); + break; + } + if ((op.type != R_ANAL_OP_TYPE_JMP && op.type != R_ANAL_OP_TYPE_UJMP) + || op.jump == UT64_MAX || !op.jump || op.jump == cur || op.size > 8) { + r_anal_op_fini (&op); + break; + } + cur = op.jump; + r_anal_op_fini (&op); + } + + return cur; +} + +static const char *detect_local_summary_alias(RAnal *anal, ut64 addr) { + ut64 resolved; + ut8 buf[16] = {0}; + + if (!anal || addr == UT64_MAX || !addr || !anal->iob.read_at) { + return NULL; + } + + resolved = resolve_local_direct_jump_thunk_target (anal, addr); + if (resolved == UT64_MAX || !resolved) { + return NULL; + } + if (!anal->iob.read_at (anal->iob.io, resolved, buf, sizeof (buf))) { + return NULL; + } + + /* MSVC-style x64 small memcpy/memmove dispatcher: + * mov rax, rcx + * lea r10, [rip + table] + * cmp r8, imm8 + */ + if (buf[0] == 0x48 && buf[1] == 0x8b && buf[2] == 0xc1 + && buf[3] == 0x4c && buf[4] == 0x8d && buf[5] == 0x15 + && buf[10] == 0x49 && buf[11] == 0x83 && buf[12] == 0xf8) { + return "memcpy"; + } + + return NULL; +} + static RAnalFunction *resolve_or_materialize_function_target(RCore *core, RAnal *anal, const char *target_arg) { ut64 target_addr = 0; RAnalFunction *fcn; @@ -3235,8 +3604,8 @@ static RAnalFunction *resolve_or_materialize_current_function(RCore *core, RAnal return materialize_function_at (anal, core->addr); } -static char *build_sym_symbol_map_json(RCore *core) { - if (!core) { +static char *build_sym_symbol_map_json(RCore *core, RAnal *anal, R2ILContext *ctx, const SymFunctionScope *scope) { + if (!core || !anal || !ctx || !scope || !scope->functions || !scope->count) { return strdup ("{}"); } @@ -3246,51 +3615,39 @@ static char *build_sym_symbol_map_json(RCore *core) { } pj_o (pj); - /* aflj: [{addr:0x...,name:"..."}] */ - char *aflj = r_core_cmd_str (core, "aflj"); - if (aflj && aflj[0] == '[') { - RJson *root = r_json_parse (aflj); - if (root && root->type == R_JSON_ARRAY) { - RJson *elem; - for (elem = root->children.first; elem; elem = elem->next) { - if (elem->type != R_JSON_OBJECT) { - continue; - } - const RJson *addr = r_json_get (elem, "addr"); - const RJson *name = r_json_get (elem, "name"); - if (addr && name && addr->type == R_JSON_INTEGER && name->type == R_JSON_STRING && name->str_value) { - char key[32]; - snprintf (key, sizeof (key), "0x%llx", (unsigned long long)addr->num.u_value); - pj_ks (pj, key, name->str_value); - } - } - r_json_free (root); - } - } - free (aflj); - - /* fs *;fj: include import/plt flags such as sym.imp.memcpy */ - char *fj = r_core_cmd_str (core, "fs *;fj"); - if (fj && fj[0] == '[') { - RJson *root = r_json_parse (fj); - if (root && root->type == R_JSON_ARRAY) { - RJson *elem; - for (elem = root->children.first; elem; elem = elem->next) { - if (elem->type != R_JSON_OBJECT) { - continue; - } - const RJson *addr = r_json_get (elem, "addr"); - const RJson *name = r_json_get (elem, "name"); - if (addr && name && addr->type == R_JSON_INTEGER && name->type == R_JSON_STRING && name->str_value) { - char key[32]; - snprintf (key, sizeof (key), "0x%llx", (unsigned long long)addr->num.u_value); - pj_ks (pj, key, name->str_value); - } + for (size_t i = 0; i < scope->count; i++) { + const R2ILFunctionBlocks *function = &scope->functions[i]; + const BlockArray *blocks = &scope->owned_blocks[i]; + size_t direct_target_count = 0; + ut64 *direct_targets = NULL; + if (function->name && *function->name) { + char key[32]; + snprintf (key, sizeof (key), "0x%llx", (unsigned long long)function->entry_addr); + pj_ks (pj, key, function->name); + } + direct_targets = collect_type_interproc_direct_targets_from_blocks ( + ctx, + blocks, + function->entry_addr, + function->name, + &direct_target_count + ); + for (size_t j = 0; j < direct_target_count; j++) { + char *target_name = resolve_interproc_seed_name (core, anal, direct_targets[j]); + const char *summary_alias = NULL; + if (target_name && *target_name) { + char key[32]; + snprintf (key, sizeof (key), "0x%llx", (unsigned long long)direct_targets[j]); + pj_ks (pj, key, target_name); + } else if ((summary_alias = detect_local_summary_alias (anal, direct_targets[j]))) { + char key[32]; + snprintf (key, sizeof (key), "0x%llx", (unsigned long long)direct_targets[j]); + pj_ks (pj, key, summary_alias); } - r_json_free (root); + free (target_name); } + free (direct_targets); } - free (fj); pj_end (pj); return pj_drain (pj); @@ -5734,8 +6091,62 @@ static R2ILBlock *lift_function_block_healed( return NULL; } +static void lift_function_linear_gap_blocks(RAnal *anal, RAnalFunction *fcn, R2ILContext *ctx, BlockArray *out) { + ut64 min_addr; + ut64 max_addr; + ut64 addr; + + R_RETURN_IF_FAIL (anal && fcn && ctx && out); + + min_addr = r_anal_function_min_addr (fcn); + max_addr = r_anal_function_max_addr (fcn); + if (min_addr == UT64_MAX || max_addr == UT64_MAX || max_addr <= min_addr) { + return; + } + + for (addr = min_addr; addr < max_addr;) { + ut64 covered_end = UT64_MAX; + ut8 buf[SLEIGH_MIN_BYTES] = {0}; + RAnalOp op = {0}; + int len; + R2ILBlock *block; + + if (block_array_addr_covered (out, addr, &covered_end)) { + addr = covered_end > addr? covered_end: addr + 1; + continue; + } + if (!anal->iob.read_at (anal->iob.io, addr, buf, sizeof (buf))) { + addr++; + continue; + } + + len = r_anal_op (anal, &op, addr, buf, sizeof (buf), R_ARCH_OP_MASK_BASIC); + r_anal_op_fini (&op); + if (len < 1) { + len = 1; + } + + block = r2il_lift (ctx, buf, sizeof (buf), addr); + if (block) { + if (r2il_block_validate (ctx, block)) { + block_array_push (out, block); + } else { + r2il_block_free (block); + } + } + + addr += (ut64)len; + } +} + /* Lift all basic blocks of a function */ -static bool lift_function_blocks(RAnal *anal, RAnalFunction *fcn, R2ILContext *ctx, BlockArray *out) { +static bool lift_function_blocks( + RAnal *anal, + RAnalFunction *fcn, + R2ILContext *ctx, + BlockArray *out, + bool include_linear_gap_blocks +) { R_RETURN_VAL_IF_FAIL (anal && fcn && ctx && out, false); RListIter *iter; @@ -5837,6 +6248,11 @@ static bool lift_function_blocks(RAnal *anal, RAnalFunction *fcn, R2ILContext *c free (buf); } + if (include_linear_gap_blocks) { + lift_function_linear_gap_blocks (anal, fcn, ctx, out); + block_array_sort (out); + } + return out->count > 0; } @@ -5876,6 +6292,10 @@ static bool sleigh_mode_is_fast(RAnal *anal) { return cfg_get_mode_default_balanced (anal) == SLEIGH_MODE_FAST; } +static bool sleigh_mode_allows_deep_auto_callbacks(RAnal *anal) { + return cfg_get_mode_default_balanced (anal) == SLEIGH_MODE_FULL; +} + static SleighMode sleigh_mode_effective_for_post_analysis(RAnal *anal) { SleighMode mode = cfg_get_mode_default_balanced (anal); return mode == SLEIGH_MODE_FAST ? SLEIGH_MODE_FAST : SLEIGH_MODE_FULL; @@ -6343,6 +6763,9 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { r_cons_println (cons, "| a:sla.slice - Backward slice from variable (e.g. rax_3)"); r_cons_println (cons, "| a:sla.sym [name|addr] - Symbolic execution summary (current by default)"); r_cons_println (cons, "| a:sla.sym.paths [name|addr] - Explore paths in function (current by default)"); + r_cons_println (cons, "| a:sla.assumptions [name|addr] - Show persisted function assumptions (current by default)"); + r_cons_println (cons, "| a:sla.assumptions- [name|addr] - Clear persisted function assumptions"); + r_cons_println (cons, "| a:sla.assumej - Replace current function assumptions"); r_cons_println (cons, "| a:sla.sym.merge [on|off] - Toggle symbolic state merging"); r_cons_println (cons, "| a:sla.taint - Taint analysis for current function"); r_cons_println (cons, "| a:sla.dec [name|addr] - Decompile function (current by default)"); @@ -6352,6 +6775,8 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { r_cons_println (cons, "| a:sym.solve - Solve concrete input for target reachability"); r_cons_println (cons, "| a:sym.explore.replayj - Explore from a replay checkpoint frontier"); r_cons_println (cons, "| a:sym.solve.replayj - Solve from a replay checkpoint frontier"); + r_cons_println (cons, "| a:sym.explore.state [json-spec] - Explore from current debugger state"); + r_cons_println (cons, "| a:sym.solve.state [json-spec] - Solve from current debugger state"); r_cons_println (cons, "| a:sym.runj - Run typed symbolic exploration spec"); r_cons_println (cons, "| a:sym.replayj - Search checkpointed replay branches"); r_cons_println (cons, "| a:sym.state - Show last symbolic explore/solve cached result"); @@ -6374,6 +6799,7 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { RAnalFunction *fcn; SymFunctionScope scope; char *spec_json = NULL; + char *external_context_json = NULL; char *result = NULL; bool rust_owned = true; @@ -6398,20 +6824,23 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { R_LOG_ERROR ("r2sleigh: failed to build symbolic function scope"); return strdup(""); } - char *sym_map_json = build_sym_symbol_map_json (core); + char *sym_map_json = build_sym_symbol_map_json (core, anal, ctx, &scope); if (sym_map_json) { r2sym_set_symbol_map_json (sym_map_json); free (sym_map_json); } + external_context_json = sleigh_collect_sym_external_context_json (anal, fcn); spec_json = strdup (arg); if (!spec_json) { + free (external_context_json); sym_function_scope_free (&scope); return strdup(""); } r_str_unescape (spec_json); - result = r2sym_run_spec_json_scope (ctx, scope.functions, scope.count, fcn->addr, spec_json); + result = r2sym_run_spec_json_scope (ctx, scope.functions, scope.count, fcn->addr, spec_json, external_context_json); free (spec_json); + free (external_context_json); if (!result) { rust_owned = false; result = strdup ("{\"error\":\"symbolic execution failed\"}"); @@ -6477,6 +6906,7 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { RAnalFunction *fcn; SymFunctionScope scope; char *spec_json = NULL; + char *external_context_json = NULL; char *result = NULL; bool rust_owned = true; @@ -6501,7 +6931,7 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { } return strdup (""); } - if (!replay_sym_seed_spec_parse (core, spec_json, &spec)) { + if (!replay_sym_seed_spec_parse (core, spec_json, &spec, true)) { R_LOG_ERROR ("r2sleigh: invalid replay symbolic seed spec"); free (spec_json); return strdup (""); @@ -6520,18 +6950,20 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { replay_sym_seed_spec_fini (&spec); return strdup (""); } - if (!build_symbolic_function_scope (anal, fcn, ctx, &scope)) { + if (!build_symbolic_function_scope_with_target (anal, fcn, ctx, &scope, target)) { R_LOG_ERROR ("r2sleigh: failed to build symbolic function scope"); replay_sym_seed_spec_fini (&spec); return strdup (""); } - char *sym_map_json = build_sym_symbol_map_json (core); + char *sym_map_json = build_sym_symbol_map_json (core, anal, ctx, &scope); if (sym_map_json) { r2sym_set_symbol_map_json (sym_map_json); free (sym_map_json); } + external_context_json = sleigh_collect_sym_external_context_json (anal, fcn); - result = replay_sym_query_run (core, ctx, &scope, fcn->addr, target, &spec, is_explore); + result = replay_sym_query_run (core, ctx, &scope, fcn->addr, target, &spec, is_explore, external_context_json); + free (external_context_json); if (!result) { rust_owned = false; result = strdup ("{\"error\":\"replay symbolic execution failed\"}"); @@ -6554,7 +6986,97 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { return strdup (""); } - if (is_sym_ns && (!strncmp (cmd, "sym.explore", 11) || !strncmp (cmd, "sym.solve", 9))) { + if (is_sym_ns && (!strncmp (cmd, "sym.explore.state", 17) || !strncmp (cmd, "sym.solve.state", 15))) { + bool is_explore = r_str_startswith (cmd, "sym.explore.state"); + size_t prefix_len = is_explore ? 17 : 15; + const char *arg = skip_cmd_spaces (cmd + prefix_len); + ReplaySymSeedSpec spec; + ut64 target = 0; + R2ILContext *ctx; + RAnalFunction *fcn; + SymFunctionScope scope; + char *spec_json = NULL; + char *external_context_json = NULL; + char *result = NULL; + bool rust_owned = true; + + if (!arg || !*arg) { + if (cons) { + r_cons_println (cons, is_explore + ? "Usage: a:sym.explore.state [json-spec]" + : "Usage: a:sym.solve.state [json-spec]"); + } + return strdup (""); + } + if (!core->dbg) { + R_LOG_ERROR ("r2sleigh: active debugger state is required"); + return strdup (""); + } + if (!parse_target_and_optional_json (core, arg, &target, &spec_json)) { + R_LOG_ERROR ("r2sleigh: invalid state symbolic target/spec"); + if (cons) { + r_cons_println (cons, is_explore + ? "Usage: a:sym.explore.state [json-spec]" + : "Usage: a:sym.solve.state [json-spec]"); + } + return strdup (""); + } + if (!replay_sym_seed_spec_parse (core, spec_json, &spec, false)) { + R_LOG_ERROR ("r2sleigh: invalid state symbolic seed spec"); + free (spec_json); + return strdup (""); + } + free (spec_json); + + ctx = get_context (anal); + if (!ctx) { + R_LOG_ERROR ("r2sleigh: no context"); + replay_sym_seed_spec_fini (&spec); + return strdup (""); + } + fcn = resolve_or_materialize_current_function (core, anal); + if (!fcn) { + R_LOG_ERROR ("r2sleigh: no function at current address"); + replay_sym_seed_spec_fini (&spec); + return strdup (""); + } + if (!build_symbolic_function_scope_with_target (anal, fcn, ctx, &scope, target)) { + R_LOG_ERROR ("r2sleigh: failed to build symbolic function scope"); + replay_sym_seed_spec_fini (&spec); + return strdup (""); + } + char *sym_map_json = build_sym_symbol_map_json (core, anal, ctx, &scope); + if (sym_map_json) { + r2sym_set_symbol_map_json (sym_map_json); + free (sym_map_json); + } + external_context_json = sleigh_collect_sym_external_context_json (anal, fcn); + + result = replay_sym_query_run (core, ctx, &scope, fcn->addr, target, &spec, is_explore, external_context_json); + free (external_context_json); + if (!result) { + rust_owned = false; + result = strdup ("{\"error\":\"state symbolic execution failed\"}"); + } + + if (cons && result) { + r_cons_printf (cons, "%s\n", result); + } + if (result && !sym_result_has_error (result)) { + sym_state_cache_update (is_explore ? "explore.state" : "solve.state", + fcn->addr, spec.entry_addr? spec.entry_addr: core->addr, target, result); + } + if (rust_owned) { + r2il_string_free (result); + } else { + free (result); + } + sym_function_scope_free (&scope); + replay_sym_seed_spec_fini (&spec); + return strdup (""); + } + + if (is_sym_ns && (!strncmp (cmd, "sym.explore", 11) || !strncmp (cmd, "sym.solve", 9))) { bool is_explore = r_str_startswith (cmd, "sym.explore"); size_t prefix_len = is_explore ? 11 : 9; const char *arg = skip_cmd_spaces (cmd + prefix_len); @@ -6563,6 +7085,7 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { RAnalFunction *fcn; SymFunctionScope scope; char *result = NULL; + char *external_context_json = NULL; bool rust_owned = true; if (!arg || !*arg) { @@ -6593,21 +7116,30 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { R_LOG_ERROR ("r2sleigh: no function at current address"); return strdup(""); } - if (!build_symbolic_function_scope (anal, fcn, ctx, &scope)) { + if (!build_symbolic_function_scope_with_target (anal, fcn, ctx, &scope, target)) { R_LOG_ERROR ("r2sleigh: failed to build symbolic function scope"); return strdup(""); } - char *sym_map_json = build_sym_symbol_map_json (core); + sleigh_debug_scope_log ("sym_query_stage scope_ready count=%zu", scope.count); + sleigh_debug_scope_log ("sym_query_stage sym_map_begin"); + char *sym_map_json = build_sym_symbol_map_json (core, anal, ctx, &scope); if (sym_map_json) { r2sym_set_symbol_map_json (sym_map_json); free (sym_map_json); } + sleigh_debug_scope_log ("sym_query_stage sym_map_done"); + sleigh_debug_scope_log ("sym_query_stage assumptions_begin"); + external_context_json = sleigh_collect_sym_external_context_json (anal, fcn); + sleigh_debug_scope_log ("sym_query_stage assumptions_done"); + sleigh_debug_scope_log ("sym_query_stage rust_call_begin target=0x%"PFMT64x, target); if (is_explore) { - result = r2sym_explore_to_scope (ctx, scope.functions, scope.count, fcn->addr, target); + result = r2sym_explore_to_scope (ctx, scope.functions, scope.count, fcn->addr, target, external_context_json); } else { - result = r2sym_solve_to_scope (ctx, scope.functions, scope.count, fcn->addr, target); + result = r2sym_solve_to_scope (ctx, scope.functions, scope.count, fcn->addr, target, external_context_json); } + sleigh_debug_scope_log ("sym_query_stage rust_call_done"); + free (external_context_json); if (!result) { rust_owned = false; result = strdup ("{\"error\":\"symbolic execution failed\"}"); @@ -6629,6 +7161,86 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { return strdup(""); } + if (!strcmp (cmd, "sla.assumptions-") || (!strncmp (cmd, "sla.assumptions-", 16) && isspace ((unsigned char)cmd[16]))) { + const char *target_arg = skip_cmd_spaces (cmd + 16); + RAnalFunction *fcn = (target_arg && *target_arg) + ? resolve_or_materialize_function_target (core, anal, target_arg) + : resolve_or_materialize_current_function (core, anal); + if (!fcn) { + if (target_arg && *target_arg) { + R_LOG_ERROR ("r2sleigh: function target not found: %s", target_arg); + } else { + R_LOG_ERROR ("r2sleigh: no function at current address"); + } + return strdup (""); + } + if (!r_anal_function_set_assumptions_json (anal, fcn, "")) { + R_LOG_ERROR ("r2sleigh: failed to clear assumptions for 0x%"PFMT64x, fcn->addr); + return strdup (""); + } + if (cons) { + r_cons_println (cons, "[]"); + } + return strdup (""); + } + + if (!strncmp (cmd, "sla.assumptions", 15) && (!cmd[15] || isspace ((unsigned char)cmd[15]))) { + const char *target_arg = skip_cmd_spaces (cmd + 15); + RAnalFunction *fcn = (target_arg && *target_arg) + ? resolve_or_materialize_function_target (core, anal, target_arg) + : resolve_or_materialize_current_function (core, anal); + char *assumptions_json; + if (!fcn) { + if (target_arg && *target_arg) { + R_LOG_ERROR ("r2sleigh: function target not found: %s", target_arg); + } else { + R_LOG_ERROR ("r2sleigh: no function at current address"); + } + return strdup (""); + } + assumptions_json = sleigh_collect_function_assumptions_json (anal, fcn); + if (cons) { + r_cons_printf (cons, "%s\n", assumptions_json? assumptions_json: "[]"); + } + free (assumptions_json); + return strdup (""); + } + + if (!strncmp (cmd, "sla.assumej", 11) && (!cmd[11] || isspace ((unsigned char)cmd[11]))) { + const char *arg = skip_cmd_spaces (cmd + 11); + RAnalFunction *fcn; + char *assumptions_json; + + if (!arg || !*arg) { + if (cons) { + r_cons_println (cons, "Usage: a:sla.assumej "); + } + return strdup (""); + } + fcn = resolve_or_materialize_current_function (core, anal); + if (!fcn) { + R_LOG_ERROR ("r2sleigh: no function at current address"); + return strdup (""); + } + assumptions_json = strdup (arg); + if (!assumptions_json) { + return strdup (""); + } + r_str_unescape (assumptions_json); + if (!r_anal_function_set_assumptions_json (anal, fcn, assumptions_json)) { + R_LOG_ERROR ("r2sleigh: invalid assumptions json array"); + free (assumptions_json); + return strdup (""); + } + free (assumptions_json); + assumptions_json = sleigh_collect_function_assumptions_json (anal, fcn); + if (cons) { + r_cons_printf (cons, "%s\n", assumptions_json? assumptions_json: "[]"); + } + free (assumptions_json); + return strdup (""); + } + if (!strncmp (cmd, "sla.arch", 8)) { const char *arg = cmd + 8; if (*arg == ' ') { @@ -6967,7 +7579,7 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { } return strdup (""); } - if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { + if (!lift_function_blocks (anal, fcn, ctx, &blocks, true)) { R_LOG_ERROR ("r2sleigh: failed to lift function blocks"); return strdup (""); } @@ -6978,11 +7590,11 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { free (external_context_json); external_context_json = strdup ("{}"); } - if (!prefer_bounded_semantic_type_plan) { warm_type_payload_cache_for_function (core, anal, ctx, fcn, interproc_max_iters, &seen_addrs, &seen_count, &seen_cap); - interproc_scope_json = build_type_interproc_scope_json (core, anal, ctx, fcn, &blocks); + interproc_scope_json = build_type_interproc_scope_json ( + core, anal, ctx, fcn, &blocks, interproc_max_iters); } SymFunctionScope sym_scope; if (build_symbolic_function_scope (anal, fcn, ctx, &sym_scope)) { @@ -7070,7 +7682,7 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { } return strdup (""); } - if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { + if (!lift_function_blocks (anal, fcn, ctx, &blocks, true)) { R_LOG_ERROR ("r2sleigh: failed to lift function blocks"); return strdup (""); } @@ -7081,11 +7693,11 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { free (external_context_json); external_context_json = strdup ("{}"); } - if (!prefer_bounded_semantic_type_plan) { warm_type_payload_cache_for_function (core, anal, ctx, fcn, interproc_max_iters, &seen_addrs, &seen_count, &seen_cap); - interproc_scope_json = build_type_interproc_scope_json (core, anal, ctx, fcn, &blocks); + interproc_scope_json = build_type_interproc_scope_json ( + core, anal, ctx, fcn, &blocks, interproc_max_iters); } SymFunctionScope sym_scope; if (build_symbolic_function_scope (anal, fcn, ctx, &sym_scope)) { @@ -7161,7 +7773,7 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { } /* Lift all blocks */ BlockArray blocks; - if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { + if (!lift_function_blocks (anal, fcn, ctx, &blocks, true)) { R_LOG_ERROR ("r2sleigh: failed to lift function blocks"); return strdup(""); } @@ -7192,7 +7804,7 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { } BlockArray blocks; - if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { + if (!lift_function_blocks (anal, fcn, ctx, &blocks, true)) { R_LOG_ERROR ("r2sleigh: failed to lift function blocks"); return strdup(""); } @@ -7224,7 +7836,7 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { /* Lift all blocks */ BlockArray blocks; - if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { + if (!lift_function_blocks (anal, fcn, ctx, &blocks, true)) { R_LOG_ERROR ("r2sleigh: failed to lift function blocks"); return strdup(""); } @@ -7257,7 +7869,7 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { /* Lift all blocks */ BlockArray blocks; - if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { + if (!lift_function_blocks (anal, fcn, ctx, &blocks, true)) { R_LOG_ERROR ("r2sleigh: failed to lift function blocks"); return strdup(""); } @@ -7307,7 +7919,7 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { /* Lift all blocks */ BlockArray blocks; - if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { + if (!lift_function_blocks (anal, fcn, ctx, &blocks, true)) { R_LOG_ERROR ("r2sleigh: failed to lift function blocks"); return strdup(""); } @@ -7385,7 +7997,7 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { R_LOG_ERROR ("r2sleigh: failed to build symbolic function scope"); return strdup(""); } - char *sym_map_json = build_sym_symbol_map_json (core); + char *sym_map_json = build_sym_symbol_map_json (core, anal, ctx, &scope); if (sym_map_json) { r2sym_set_symbol_map_json (sym_map_json); free (sym_map_json); @@ -7393,11 +8005,13 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { /* Call symbolic execution */ char *result; + char *external_context_json = sleigh_collect_sym_external_context_json (anal, fcn); if (is_paths_cmd) { - result = r2sym_paths_scope (ctx, scope.functions, scope.count, fcn->addr); + result = r2sym_paths_scope (ctx, scope.functions, scope.count, fcn->addr, external_context_json); } else { - result = r2sym_function_scope (ctx, scope.functions, scope.count, fcn->addr); + result = r2sym_function_scope (ctx, scope.functions, scope.count, fcn->addr, external_context_json); } + free (external_context_json); if (cons && result) { r_cons_printf (cons, "%s\n", result); @@ -7424,7 +8038,7 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { /* Lift all blocks */ BlockArray blocks; - if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { + if (!lift_function_blocks (anal, fcn, ctx, &blocks, true)) { R_LOG_ERROR ("r2sleigh: failed to lift function blocks"); return strdup(""); } @@ -7497,7 +8111,7 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { } /* Lift all blocks */ BlockArray blocks; - if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { + if (!lift_function_blocks (anal, fcn, ctx, &blocks, true)) { R_LOG_ERROR ("r2sleigh: failed to lift function blocks"); return strdup(""); } @@ -7692,7 +8306,7 @@ static char *sleigh_cmd(RAnal *anal, const char *cmd) { /* Lift all blocks */ BlockArray blocks; - if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { + if (!lift_function_blocks (anal, fcn, ctx, &blocks, true)) { R_LOG_ERROR ("r2sleigh: failed to lift function blocks"); return strdup(""); } @@ -7729,18 +8343,23 @@ static bool sleigh_analyze_fcn(RAnal *anal, RAnalFunction *fcn) { return false; } + if (!sleigh_mode_allows_deep_auto_callbacks (anal)) { + return true; + } + if (function_exceeds_auto_callback_budget (fcn)) { + R_LOG_DEBUG ("r2sleigh: auto analyze_fcn skipped by budget fcn=0x%"PFMT64x" blocks=%d", + fcn->addr, function_bb_count (fcn)); + return true; + } + R2ILContext *ctx = get_context (anal); if (!ctx) { return false; } - if (sleigh_mode_is_fast (anal)) { - return true; - } - BlockArray blocks; ut64 cache_key; - if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { + if (!lift_function_blocks (anal, fcn, ctx, &blocks, true)) { return false; } @@ -7780,7 +8399,12 @@ static RList *sleigh_recover_vars(RAnal *anal, RAnalFunction *fcn) { if (!fcn || !anal) { return NULL; } - if (sleigh_mode_is_fast (anal)) { + if (!sleigh_mode_allows_deep_auto_callbacks (anal)) { + return NULL; + } + if (function_exceeds_auto_callback_budget (fcn)) { + R_LOG_DEBUG ("r2sleigh: auto recover_vars skipped by budget fcn=0x%"PFMT64x" blocks=%d", + fcn->addr, function_bb_count (fcn)); return NULL; } @@ -7790,7 +8414,7 @@ static RList *sleigh_recover_vars(RAnal *anal, RAnalFunction *fcn) { } BlockArray blocks; - if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { + if (!lift_function_blocks (anal, fcn, ctx, &blocks, true)) { return NULL; } @@ -8036,7 +8660,12 @@ static RVecAnalRef *sleigh_get_data_refs(RAnal *anal, RAnalFunction *fcn) { if (!fcn || !anal) { return NULL; } - if (sleigh_mode_is_fast (anal)) { + if (!sleigh_mode_allows_deep_auto_callbacks (anal)) { + return NULL; + } + if (function_exceeds_auto_callback_budget (fcn)) { + R_LOG_DEBUG ("r2sleigh: auto get_data_refs skipped by budget fcn=0x%"PFMT64x" blocks=%d", + fcn->addr, function_bb_count (fcn)); return NULL; } @@ -8047,7 +8676,7 @@ static RVecAnalRef *sleigh_get_data_refs(RAnal *anal, RAnalFunction *fcn) { BlockArray blocks; ut64 cache_key; - if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { + if (!lift_function_blocks (anal, fcn, ctx, &blocks, true)) { return NULL; } cache_key = compute_xref_cache_key (fcn, &blocks, sleigh_mode_effective_for_post_analysis (anal)); @@ -8394,6 +9023,8 @@ static bool verify_practical_signature_consistency ( ConsistencyReasonCounters *reason_counters ); +static bool reconcile_signature_register_arg_vars(RAnal *anal, RAnalFunction *fcn, const RJson *sig_root); + static bool apply_inferred_signature_typed( RAnal *anal, RAnalFunction *fcn, @@ -8468,6 +9099,9 @@ static bool apply_inferred_signature_typed( input.params = param_list; input.noreturn = fcn->is_noreturn; ok = r_anal_function_set_signature (anal, fcn, &input); + if (ok) { + reconcile_signature_register_arg_vars (anal, fcn, sig_root); + } r_list_free (param_list); if (!ok) { write_reason_msg (reason, reason_sz, "typed signature apply failed"); @@ -8489,6 +9123,7 @@ static WritebackApplyResult apply_inferred_signature( return res; } if (verify_signature_type_db_ex (anal, fcn, sig_root, res.detail, sizeof (res.detail))) { + reconcile_signature_register_arg_vars (anal, fcn, sig_root); res.already_applied = true; if (!res.detail[0]) { write_reason_msg (res.detail, sizeof (res.detail), "signature already matches"); @@ -8496,6 +9131,7 @@ static WritebackApplyResult apply_inferred_signature( return res; } if (verify_practical_signature_consistency (anal, fcn, sig_root, true, false, NULL, NULL)) { + reconcile_signature_register_arg_vars (anal, fcn, sig_root); res.already_applied = true; write_reason_msg (res.detail, sizeof (res.detail), "practical signature already matches"); return res; @@ -8706,9 +9342,9 @@ static int resolve_reg_index(RAnal *anal, const char *reg_name) { *p = toupper ((unsigned char)*p); } } - ri = upper_reg? r_reg_get (anal->reg, upper_reg, R_REG_TYPE_GPR): NULL; + ri = r_reg_get (anal->reg, reg_name, R_REG_TYPE_GPR); if (!ri) { - ri = r_reg_get (anal->reg, reg_name, R_REG_TYPE_GPR); + ri = upper_reg? r_reg_get (anal->reg, upper_reg, R_REG_TYPE_GPR): NULL; } if (ri) { index = ri->index; @@ -8743,6 +9379,106 @@ static RAnalVar *lookup_var_for_candidate(RAnal *anal, RAnalFunction *fcn, const return NULL; } +static int score_signature_register_arg_var(RAnalVar *var, const char *expected_name) { + if (!var) { + return -1; + } + if (expected_name && *expected_name && var->name && strings_match_normalized (var->name, expected_name)) { + return 100; + } + if (var->name && !is_generated_var_name (var->name)) { + return 50; + } + return 10; +} + +static bool is_stack_arg_name_conflict(RAnalVar *var, RAnalVar *best, const char *expected_name) { + if (!var || var == best || !expected_name || !*expected_name || !var->name) { + return false; + } + if (!var->isarg || (var->kind != R_ANAL_VAR_KIND_BPV && var->kind != R_ANAL_VAR_KIND_SPV)) { + return false; + } + return strings_match_normalized (var->name, expected_name); +} + +static bool reconcile_signature_register_arg_vars(RAnal *anal, RAnalFunction *fcn, const RJson *sig_root) { + const RJson *j_params; + const RJson *j_param; + bool changed = false; + int idx = 0; + + if (!anal || !fcn || !sig_root || sig_root->type != R_JSON_OBJECT) { + return false; + } + j_params = r_json_get (sig_root, "params"); + if (!j_params || j_params->type != R_JSON_ARRAY) { + return false; + } + + for (j_param = json_next_object (j_params->children.first); j_param; j_param = json_next_object (j_param->next), idx++) { + const RJson *j_name = r_json_get (j_param, "name"); + const RJson *j_type = r_json_get (j_param, "type"); + const char *expected_name = json_is_string_with_value (j_name)? j_name->str_value: NULL; + const char *expected_type = json_is_string_with_value (j_type)? j_type->str_value: NULL; + RAnalVar *best = NULL; + int best_score = -1; + RListIter *iter; + RAnalVar *var; + RList *vars = r_anal_var_all_list (anal, fcn); + + if (!vars) { + continue; + } + r_list_foreach (vars, iter, var) { + int score; + if (!var || !var->isarg || var->kind != R_ANAL_VAR_KIND_REG) { + continue; + } + if (r_anal_var_get_argnum (var) != idx) { + continue; + } + score = score_signature_register_arg_var (var, expected_name); + if (score > best_score) { + best = var; + best_score = score; + } + } + r_list_free (vars); + vars = NULL; + if (best) { + if (expected_type && *expected_type && (!best->type || !strings_match_normalized (best->type, expected_type))) { + r_anal_var_set_type (anal, best, expected_type); + changed = true; + } + if (expected_name && *expected_name && (!best->name || !strings_match_normalized (best->name, expected_name))) { + RAnalVar *conflict = r_anal_function_get_var_byname (fcn, expected_name); + if (is_stack_arg_name_conflict (conflict, best, expected_name)) { + r_anal_var_delete (anal, conflict); + changed = true; + } + r_anal_var_rename (anal, best, expected_name); + changed = true; + } + vars = r_anal_var_all_list (anal, fcn); + if (!vars) { + continue; + } + r_list_foreach (vars, iter, var) { + if (!var || var == best || !var->isarg || var->kind != R_ANAL_VAR_KIND_REG) { + continue; + } + if (r_anal_var_get_argnum (var) == idx) { + r_anal_var_delete (anal, var); + changed = true; + } + } + r_list_free (vars); + } + } + return changed; +} + static bool verify_var_type_applied(RAnalVar *var, const char *expected_type) { if (!var || !expected_type || !*expected_type || !var->type || !*var->type) { return false; @@ -9023,6 +9759,10 @@ static bool apply_var_rename_candidate( if (!is_generated_var_name (var->name)) { return false; } + RAnalVar *conflict = r_anal_function_get_var_byname (fcn, new_name); + if (is_stack_arg_name_conflict (conflict, var, new_name)) { + r_anal_var_delete (anal, conflict); + } if (r_anal_var_rename (anal, var, new_name) && verify_var_rename_applied (var, new_name)) { return true; } @@ -9079,8 +9819,7 @@ static bool apply_global_type_link_candidate(RAnal *anal, RCore *core, ut64 addr } static ut64 compute_type_cache_key( - RAnalFunction *fcn, - const char *external_context_json, + ut64 artifact_key, ut64 dep_hash, SleighTypeWritebackMode mode, int min_conf, @@ -9088,19 +9827,13 @@ static ut64 compute_type_cache_key( int struct_min_conf, int max_iters ) { - ut64 key = 0; - int bb_count = (fcn && fcn->bbs)? r_list_length (fcn->bbs): 0; - int linear_size = fcn? r_anal_function_linear_size (fcn): 0; - key ^= fcn? fcn->addr: 0; - key ^= ((ut64)bb_count << 32); - key ^= (ut64)linear_size; + ut64 key = artifact_key; key ^= ((ut64)mode << 56); key ^= ((ut64)(min_conf & 0xff) << 8); key ^= ((ut64)(rename_min_conf & 0xff) << 16); key ^= ((ut64)(struct_min_conf & 0xff) << 24); key ^= ((ut64)(max_iters & 0xffff) << 40); key ^= dep_hash; - key ^= r_str_hash64 (external_context_json? external_context_json: ""); return key; } @@ -9243,6 +9976,254 @@ static ut64 *collect_type_interproc_direct_targets_from_blocks( return targets; } +static char *build_ut64_json_array(const ut64 *values, size_t count) { + PJ *pj; + size_t i; + char *out; + + if (!values || count == 0) { + return strdup ("[]"); + } + pj = pj_new (); + if (!pj) { + return strdup ("[]"); + } + pj_a (pj); + for (i = 0; i < count; i++) { + pj_n (pj, values[i]); + } + pj_end (pj); + out = strdup (pj_string (pj)); + pj_free (pj); + return out? out: strdup ("[]"); +} + +static const char *strip_runtime_scope_name_prefixes(const char *name) { + const char *normalized = name; + if (!normalized) { + return NULL; + } + for (;;) { + if (!strncasecmp (normalized, "sym.imp.", 8)) { + normalized += 8; + continue; + } + if (!strncasecmp (normalized, "sym.", 4)) { + normalized += 4; + continue; + } + if (!strncasecmp (normalized, "imp.", 4)) { + normalized += 4; + continue; + } + if (!strncasecmp (normalized, "reloc.", 6)) { + normalized += 6; + continue; + } + if (!strncasecmp (normalized, "dbg.", 4)) { + normalized += 4; + continue; + } + if (!strncasecmp (normalized, "fcn.", 4)) { + normalized += 4; + continue; + } + break; + } + return normalized; +} + +static bool function_name_is_windows_runtime_registration(const char *name) { + const char *normalized = strip_runtime_scope_name_prefixes (name); + if (!normalized || !*normalized) { + return false; + } + size_t len = strlen (normalized); + size_t suffix_len = strlen ("AddVectoredExceptionHandler"); + if (len < suffix_len) { + return false; + } + return !strcasecmp (normalized + (len - suffix_len), "AddVectoredExceptionHandler"); +} + +static bool function_name_is_import_like(const char *name) { + if (!name || !*name) { + return false; + } + return !strncasecmp (name, "sym.imp.", 8) + || !strncasecmp (name, "imp.", 4) + || !strncasecmp (name, "reloc.", 6); +} + +static bool function_name_is_runtime_copy(const char *name) { + const char *normalized = strip_runtime_scope_name_prefixes (name); + if (!normalized || !*normalized) { + return false; + } + return !strcasecmp (normalized, "memcpy") + || !strcasecmp (normalized, "__memcpy_chk") + || !strncasecmp (normalized, "memcpy", 6); +} + +static ut64 *collect_runtime_scope_targets_from_blocks( + R2ILContext *ctx, + const BlockArray *blocks, + ut64 fcn_addr, + const char *fcn_name, + const ut64 *registration_targets, + size_t registration_target_count, + size_t *out_count +) { + char *request_json = NULL; + char *targets_json = NULL; + RJson *root = NULL; + RJson *item; + ut64 *targets = NULL; + size_t count = 0; + size_t cap = 0; + + if (out_count) { + *out_count = 0; + } + if (!ctx || !blocks || !blocks->blocks || blocks->count == 0 || !registration_target_count) { + return NULL; + } + request_json = build_ut64_json_array (registration_targets, registration_target_count); + if (!request_json || !*request_json) { + free (request_json); + return NULL; + } + targets_json = r2sleigh_get_symbolic_scope_targets_json (ctx, + (const R2ILBlock **)blocks->blocks, blocks->count, fcn_addr, fcn_name, request_json); + free (request_json); + if (!targets_json || !*targets_json) { + r2il_string_free (targets_json); + return NULL; + } + root = r_json_parse (targets_json); + if (!root || root->type != R_JSON_ARRAY) { + r_json_free (root); + r2il_string_free (targets_json); + return NULL; + } + for (item = root->children.first; item; item = item->next) { + if (!item || item->type != R_JSON_INTEGER) { + continue; + } + append_unique_ut64 (&targets, &count, &cap, item->num.u_value); + } + r_json_free (root); + r2il_string_free (targets_json); + if (out_count) { + *out_count = count; + } + return targets; +} + +static bool append_runtime_materialized_source( + RuntimeMaterializedSource **sources, + size_t *count, + size_t *cap, + ut64 addr, + ut64 size +) { + RuntimeMaterializedSource *next; + size_t i; + if (!sources || !count || !cap || !addr || !size) { + return false; + } + for (i = 0; i < *count; i++) { + if ((*sources)[i].addr == addr) { + if ((*sources)[i].size < size) { + (*sources)[i].size = size; + } + return true; + } + } + if (*count >= *cap) { + size_t new_cap = *cap? *cap * 2: 4; + next = realloc (*sources, new_cap * sizeof (**sources)); + if (!next) { + return false; + } + *sources = next; + *cap = new_cap; + } + (*sources)[*count].addr = addr; + (*sources)[*count].size = size; + (*count)++; + return true; +} + +static RuntimeMaterializedSource *collect_runtime_materialized_sources_from_blocks( + R2ILContext *ctx, + const BlockArray *blocks, + ut64 fcn_addr, + const char *fcn_name, + const ut64 *copy_targets, + size_t copy_target_count, + size_t *out_count +) { + char *request_json = NULL; + char *sources_json = NULL; + RJson *root = NULL; + RJson *item; + RuntimeMaterializedSource *sources = NULL; + size_t count = 0; + size_t cap = 0; + + if (out_count) { + *out_count = 0; + } + if (!ctx || !blocks || !blocks->blocks || blocks->count == 0 || !copy_target_count) { + return NULL; + } + request_json = build_ut64_json_array (copy_targets, copy_target_count); + if (!request_json || !*request_json) { + free (request_json); + return NULL; + } + sources_json = r2sleigh_get_runtime_materialized_sources_json (ctx, + (const R2ILBlock **)blocks->blocks, blocks->count, fcn_addr, fcn_name, request_json); + free (request_json); + if (!sources_json || !*sources_json) { + r2il_string_free (sources_json); + return NULL; + } + root = r_json_parse (sources_json); + if (!root || root->type != R_JSON_ARRAY) { + r_json_free (root); + r2il_string_free (sources_json); + return NULL; + } + for (item = root->children.first; item; item = item->next) { + const RJson *addr_json; + const RJson *size_json; + ut64 addr; + ut64 size; + if (!item || item->type != R_JSON_OBJECT) { + continue; + } + addr_json = r_json_get (item, "addr"); + size_json = r_json_get (item, "size"); + if (!addr_json || addr_json->type != R_JSON_INTEGER + || !size_json || size_json->type != R_JSON_INTEGER) { + continue; + } + addr = addr_json->num.u_value; + size = size_json->num.u_value; + if (addr && size) { + (void)append_runtime_materialized_source (&sources, &count, &cap, addr, size); + } + } + r_json_free (root); + r2il_string_free (sources_json); + if (out_count) { + *out_count = count; + } + return sources; +} + static void sym_function_scope_init(SymFunctionScope *scope) { if (!scope) { return; @@ -9300,7 +10281,8 @@ static bool sym_function_scope_append( SymFunctionScope *scope, RAnal *anal, RAnalFunction *fcn, - R2ILContext *ctx + R2ILContext *ctx, + bool include_linear_gap_blocks ) { BlockArray blocks; if (!scope || !anal || !fcn || !ctx) { @@ -9309,7 +10291,14 @@ static bool sym_function_scope_append( if (!sym_function_scope_ensure_capacity (scope, scope->count + 1)) { return false; } - if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { + sleigh_debug_scope_log ( + "scope_append addr=0x%"PFMT64x" name=%s include_gaps=%d", + fcn->addr, + fcn->name? fcn->name: "(null)", + include_linear_gap_blocks? 1: 0 + ); + if (!lift_function_blocks (anal, fcn, ctx, &blocks, include_linear_gap_blocks)) { + sleigh_debug_scope_log ("scope_append_failed addr=0x%"PFMT64x, fcn->addr); return false; } scope->owned_blocks[scope->count] = blocks; @@ -9319,14 +10308,96 @@ static bool sym_function_scope_append( scope->functions[scope->count].blocks = (const R2ILBlock **)scope->owned_blocks[scope->count].blocks; scope->functions[scope->count].num_blocks = scope->owned_blocks[scope->count].count; scope->count++; + sleigh_debug_scope_log ( + "scope_appended addr=0x%"PFMT64x" blocks=%zu", + fcn->addr, + blocks.count + ); return true; } -static bool build_symbolic_function_scope( +static bool lift_runtime_materialized_source_blocks( + RAnal *anal, + R2ILContext *ctx, + ut64 addr, + ut64 size, + BlockArray *out +) { + ut64 limit; + ut64 offset = 0; + + if (!anal || !ctx || !out || !addr || !size || !anal->iob.read_at) { + return false; + } + limit = R_MIN (size, (ut64)SLEIGH_RUNTIME_MATERIALIZED_MAX_BYTES); + block_array_init (out); + while (offset < limit) { + ut64 cur = addr + offset; + ut8 buf[SLEIGH_MIN_BYTES] = {0}; + ut64 remaining = limit - offset; + ut64 logical_size = R_MIN (remaining, (ut64)SLEIGH_RUNTIME_MATERIALIZED_SLOT_BYTES); + R2ILBlock *block; + + if (!anal->iob.read_at (anal->iob.io, cur, buf, sizeof (buf))) { + break; + } + block = r2il_lift_block (ctx, buf, sizeof (buf), cur, (unsigned int)logical_size); + if (block) { + if (r2il_block_validate (ctx, block)) { + block_array_push (out, block); + } else { + r2il_block_free (block); + } + } + offset += logical_size; + } + block_array_sort (out); + return out->count > 0; +} + +static bool sym_function_scope_append_runtime_source( + SymFunctionScope *scope, + RAnal *anal, + R2ILContext *ctx, + ut64 addr, + ut64 size +) { + BlockArray blocks; + char name[64]; + + if (!scope || !anal || !ctx || !addr || !size) { + return false; + } + if (!lift_runtime_materialized_source_blocks (anal, ctx, addr, size, &blocks)) { + return false; + } + if (!sym_function_scope_ensure_capacity (scope, scope->count + 1)) { + block_array_free (&blocks); + return false; + } + snprintf (name, sizeof (name), "runtime.materialized.%"PFMT64x, addr); + scope->owned_blocks[scope->count] = blocks; + scope->owned_names[scope->count] = strdup (name); + scope->functions[scope->count].entry_addr = addr; + scope->functions[scope->count].name = scope->owned_names[scope->count]; + scope->functions[scope->count].blocks = (const R2ILBlock **)scope->owned_blocks[scope->count].blocks; + scope->functions[scope->count].num_blocks = scope->owned_blocks[scope->count].count; + scope->count++; + sleigh_debug_scope_log ( + "scope_runtime_source addr=0x%"PFMT64x" size=%"PFMT64u" blocks=%zu", + addr, + size, + blocks.count + ); + return true; +} + +static bool build_symbolic_function_scope_with_target( RAnal *anal, RAnalFunction *root_fcn, R2ILContext *ctx, - SymFunctionScope *scope + SymFunctionScope *scope, + ut64 target_hint ) { size_t queue_count = 0; size_t queue_cap = 0; @@ -9335,6 +10406,7 @@ static bool build_symbolic_function_scope( ut64 *seen = NULL; size_t seen_count = 0; size_t seen_cap = 0; + ut64 target_entry = UT64_MAX; if (!anal || !root_fcn || !ctx || !scope) { return false; @@ -9344,12 +10416,47 @@ static bool build_symbolic_function_scope( free (queue); return false; } + if (target_hint != UT64_MAX && target_hint && target_hint != root_fcn->addr) { + RAnalFunction *target_fcn = materialize_function_at (anal, target_hint); + if (target_fcn) { + target_entry = target_fcn->addr; + append_unique_ut64 (&queue, &queue_count, &queue_cap, target_fcn->addr); + sleigh_debug_scope_log ( + "scope_target_hint target=0x%"PFMT64x" entry=0x%"PFMT64x" name=%s", + target_hint, + target_fcn->addr, + target_fcn->name? target_fcn->name: "(null)" + ); + } else { + sleigh_debug_scope_log ( + "scope_target_hint_unmaterialized target=0x%"PFMT64x, + target_hint + ); + } + } + sleigh_debug_scope_log ( + "scope_build_start root=0x%"PFMT64x" name=%s", + root_fcn->addr, + root_fcn->name? root_fcn->name: "(null)" + ); while (queue_index < queue_count && scope->count < SLEIGH_SYM_HELPER_MAX_FUNCTIONS) { RAnalFunction *fcn; ut64 addr = queue[queue_index++]; - ut64 *targets = NULL; + ut64 *direct_targets = NULL; + ut64 *queued_direct_targets = NULL; + ut64 *runtime_copy_targets = NULL; + ut64 *runtime_targets = NULL; + RuntimeMaterializedSource *runtime_sources = NULL; size_t target_count = 0; + size_t queued_direct_target_count = 0; + size_t queued_direct_target_cap = 0; + size_t runtime_copy_target_count = 0; + size_t runtime_copy_target_cap = 0; + size_t runtime_target_count = 0; + size_t runtime_source_count = 0; + ut64 registration_targets[8] = {0}; + size_t registration_target_count = 0; size_t i; const BlockArray *blocks; @@ -9357,31 +10464,210 @@ static bool build_symbolic_function_scope( if (!fcn || !append_unique_ut64 (&seen, &seen_count, &seen_cap, fcn->addr)) { continue; } - if (!sym_function_scope_append (scope, anal, fcn, ctx)) { + bool target_hint_function = target_entry != UT64_MAX && fcn->addr == target_entry; + if (fcn->addr != root_fcn->addr + && !target_hint_function + && function_exceeds_helper_scope_budget (fcn)) { + sleigh_debug_scope_log ( + "scope_skip_budget addr=0x%"PFMT64x" name=%s", + fcn->addr, + fcn->name? fcn->name: "(null)" + ); + continue; + } + if (!sym_function_scope_append ( + scope, + anal, + fcn, + ctx, + scope->count == 0 || fcn->addr == root_fcn->addr + )) { + continue; + } + if (fcn->addr != root_fcn->addr) { + sleigh_debug_scope_log ( + "scope_expand_helper addr=0x%"PFMT64x" name=%s", + fcn->addr, + fcn->name? fcn->name: "(null)" + ); + } + if (target_hint_function && fcn->addr != root_fcn->addr) { + sleigh_debug_scope_log ( + "scope_target_terminal addr=0x%"PFMT64x" target=0x%"PFMT64x, + fcn->addr, + target_hint + ); continue; } blocks = &scope->owned_blocks[scope->count - 1]; - targets = collect_type_interproc_direct_targets_from_blocks ( + direct_targets = collect_type_interproc_direct_targets_from_blocks ( ctx, blocks, fcn->addr, fcn->name, &target_count); + sleigh_debug_scope_log ( + "scope_targets addr=0x%"PFMT64x" direct=%zu", + fcn->addr, + target_count + ); for (i = 0; i < target_count; i++) { - RAnalFunction *callee = materialize_function_at (anal, targets[i]); - if (callee && !function_exceeds_helper_scope_budget (callee)) { - append_unique_ut64 (&queue, &queue_count, &queue_cap, callee->addr); + char *target_name = resolve_interproc_seed_name (anal->coreb.core, anal, direct_targets[i]); + bool queue_direct_target = true; + RAnalFunction *target_fcn = NULL; + ut64 scope_target = direct_targets[i]; + const char *summary_alias = NULL; + if (target_name && registration_target_count < R_ARRAY_SIZE (registration_targets) + && function_name_is_windows_runtime_registration (target_name)) { + registration_targets[registration_target_count++] = direct_targets[i]; + } + if (target_name && function_name_is_runtime_copy (target_name)) { + append_unique_ut64 ( + &runtime_copy_targets, + &runtime_copy_target_count, + &runtime_copy_target_cap, + direct_targets[i] + ); + } + if (target_name && function_name_is_import_like (target_name)) { + queue_direct_target = false; + sleigh_debug_scope_log ( + "scope_skip_import_target addr=0x%"PFMT64x" name=%s", + direct_targets[i], + target_name + ); + } + if (queue_direct_target) { + summary_alias = detect_local_summary_alias (anal, direct_targets[i]); + if (summary_alias) { + queue_direct_target = false; + if (function_name_is_runtime_copy (summary_alias)) { + append_unique_ut64 ( + &runtime_copy_targets, + &runtime_copy_target_count, + &runtime_copy_target_cap, + direct_targets[i] + ); + } + sleigh_debug_scope_log ( + "scope_skip_summary_target addr=0x%"PFMT64x" summary=%s", + direct_targets[i], + summary_alias + ); + } + } + if (queue_direct_target) { + scope_target = resolve_local_direct_jump_thunk_target (anal, direct_targets[i]); + if (scope_target != direct_targets[i]) { + sleigh_debug_scope_log ( + "scope_resolved_thunk_target from=0x%"PFMT64x" to=0x%"PFMT64x" name=%s", + direct_targets[i], + scope_target, + target_name? target_name: "(null)" + ); + } + target_fcn = materialize_function_at (anal, scope_target); + if (!target_fcn) { + queue_direct_target = false; + sleigh_debug_scope_log ( + "scope_skip_unmaterialized_target addr=0x%"PFMT64x" resolved=0x%"PFMT64x" name=%s", + direct_targets[i], + scope_target, + target_name? target_name: "(null)" + ); + } else if (function_exceeds_helper_scope_budget (target_fcn)) { + queue_direct_target = false; + sleigh_debug_scope_log ( + "scope_skip_budget_target addr=0x%"PFMT64x" resolved=0x%"PFMT64x" name=%s", + direct_targets[i], + scope_target, + target_name? target_name: "(null)" + ); + } + } + free (target_name); + if (queue_direct_target) { + if (scope_target != direct_targets[i]) { + append_unique_ut64 ( + &queued_direct_targets, + &queued_direct_target_count, + &queued_direct_target_cap, + direct_targets[i] + ); + } + append_unique_ut64 ( + &queued_direct_targets, + &queued_direct_target_count, + &queued_direct_target_cap, + scope_target + ); + } + } + runtime_targets = collect_runtime_scope_targets_from_blocks ( + ctx, + blocks, + fcn->addr, + fcn->name, + registration_targets, + registration_target_count, + &runtime_target_count); + runtime_sources = collect_runtime_materialized_sources_from_blocks ( + ctx, + blocks, + fcn->addr, + fcn->name, + runtime_copy_targets, + runtime_copy_target_count, + &runtime_source_count); + sleigh_debug_scope_log ( + "scope_runtime_targets addr=0x%"PFMT64x" registrations=%zu runtime=%zu materialized=%zu", + fcn->addr, + registration_target_count, + runtime_target_count, + runtime_source_count + ); + for (i = 0; i < runtime_target_count; i++) { + append_unique_ut64 (&queue, &queue_count, &queue_cap, runtime_targets[i]); + } + for (i = 0; i < runtime_source_count && scope->count < SLEIGH_SYM_HELPER_MAX_FUNCTIONS; i++) { + if (append_unique_ut64 (&seen, &seen_count, &seen_cap, runtime_sources[i].addr)) { + (void)sym_function_scope_append_runtime_source ( + scope, + anal, + ctx, + runtime_sources[i].addr, + runtime_sources[i].size + ); } } - free (targets); + for (i = 0; i < queued_direct_target_count; i++) { + append_unique_ut64 (&queue, &queue_count, &queue_cap, queued_direct_targets[i]); + } + free (queued_direct_targets); + free (runtime_copy_targets); + free (runtime_targets); + free (runtime_sources); + free (direct_targets); } free (queue); free (seen); + sleigh_debug_scope_log ("scope_build_done count=%zu", scope->count); return scope->count > 0; } +static bool build_symbolic_function_scope( + RAnal *anal, + RAnalFunction *root_fcn, + R2ILContext *ctx, + SymFunctionScope *scope +) { + return build_symbolic_function_scope_with_target (anal, root_fcn, ctx, scope, UT64_MAX); +} + static char *build_type_interproc_scope_json_from_targets( RCore *core, RAnal *anal, + R2ILContext *ctx, const ut64 *targets, - size_t target_count + size_t target_count, + int max_iters ) { PJ *pj; ut64 *seen_addrs = NULL; @@ -9390,7 +10676,7 @@ static char *build_type_interproc_scope_json_from_targets( size_t i; char *out; - if (!anal || !targets || target_count == 0) { + if (!anal || !ctx || !targets || target_count == 0) { return strdup ("{}"); } pj = pj_new (); @@ -9400,6 +10686,58 @@ static char *build_type_interproc_scope_json_from_targets( pj_o (pj); pj_ks (pj, "phase", "fixpoint"); + pj_k (pj, "summaries"); + pj_a (pj); + for (i = 0; i < target_count; i++) { + RAnalFunction *callee_fcn; + TypeWritebackCacheEntry *entry; + BlockArray callee_blocks; + char *external_context_json = NULL; + char *summary_json = NULL; + ut64 target = targets[i]; + if (!target) { + continue; + } + callee_fcn = materialize_function_at (anal, target); + if (!callee_fcn || function_exceeds_helper_scope_budget (callee_fcn) + || !append_unique_ut64 (&seen_addrs, &seen_count, &seen_cap, callee_fcn->addr)) { + continue; + } + entry = type_writeback_cache_get (callee_fcn->addr); + if (!entry || !entry->interproc_scope_json || !*entry->interproc_scope_json) { + continue; + } + if (!lift_function_blocks (anal, callee_fcn, ctx, &callee_blocks, true)) { + continue; + } + external_context_json = sleigh_collect_external_context_json (anal, callee_fcn); + if (!external_context_json || (external_context_json[0] != '{' && external_context_json[0] != '[')) { + free (external_context_json); + external_context_json = strdup ("{}"); + } + summary_json = r2sleigh_function_interproc_summary_json_ex ( + ctx, + (const R2ILBlock **)callee_blocks.blocks, + callee_blocks.count, + callee_fcn->addr, + callee_fcn->name, + external_context_json? external_context_json: "{}", + max_iters, + entry->interproc_scope_json); + if (summary_json && *summary_json) { + pj_j (pj, summary_json); + } + r2il_string_free (summary_json); + free (external_context_json); + block_array_free (&callee_blocks); + } + pj_end (pj); + + free (seen_addrs); + seen_addrs = NULL; + seen_count = 0; + seen_cap = 0; + pj_k (pj, "payloads"); pj_a (pj); for (i = 0; i < target_count; i++) { @@ -9477,9 +10815,9 @@ static bool warm_type_payload_cache_for_function( char *external_context_json = NULL; char *interproc_scope_json = NULL; char *payload_json = NULL; + ut64 artifact_key = 0; ut64 payload_hash = 0; bool ok = false; - TypeWritebackCacheEntry *entry; if (!core || !anal || !ctx || !fcn) { return false; @@ -9487,11 +10825,7 @@ static bool warm_type_payload_cache_for_function( if (!append_unique_ut64 (seen_addrs, seen_count, seen_cap, fcn->addr)) { return true; } - entry = type_writeback_cache_get (fcn->addr); - if (entry && entry->payload_json && *entry->payload_json) { - return true; - } - if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { + if (!lift_function_blocks (anal, fcn, ctx, &blocks, true)) { return false; } @@ -9512,14 +10846,30 @@ static bool warm_type_payload_cache_for_function( external_context_json = strdup ("{}"); } interproc_scope_json = build_type_interproc_scope_json_from_targets ( - core, anal, direct_targets, direct_target_count); + core, anal, ctx, direct_targets, direct_target_count, max_iters); + artifact_key = r2sleigh_function_analysis_artifact_cache_key ( + ctx, + (const R2ILBlock **)blocks.blocks, + blocks.count, + fcn->addr, + fcn->name, + external_context_json? external_context_json: "{}", + max_iters, + interproc_scope_json? interproc_scope_json: "{}"); payload_json = r2sleigh_infer_type_writeback_json_ex (ctx, (const R2ILBlock **)blocks.blocks, blocks.count, fcn->addr, fcn->name, external_context_json? external_context_json: "{}", 1, max_iters, 1, interproc_scope_json? interproc_scope_json: "{}"); if (payload_json && *payload_json) { payload_hash = r_str_hash64 (payload_json); - ok = type_writeback_cache_put (fcn->addr, 0, 0, payload_hash, 0, payload_json); + ok = type_writeback_cache_put ( + fcn->addr, + artifact_key, + 0, + payload_hash, + 0, + payload_json, + interproc_scope_json? interproc_scope_json: "{}"); } r2il_string_free (payload_json); @@ -9535,10 +10885,15 @@ static char *build_type_interproc_scope_json( RAnal *anal, R2ILContext *ctx, RAnalFunction *fcn, - const BlockArray *blocks + const BlockArray *blocks, + int max_iters ) { ut64 *direct_targets = NULL; size_t direct_target_count = 0; + ut64 *seen_addrs = NULL; + size_t seen_count = 0; + size_t seen_cap = 0; + size_t i; char *scope_json; if (!anal || !ctx || !fcn || !blocks) { @@ -9546,9 +10901,19 @@ static char *build_type_interproc_scope_json( } direct_targets = collect_type_interproc_direct_targets_from_blocks ( ctx, blocks, fcn->addr, fcn->name, &direct_target_count); + for (i = 0; i < direct_target_count; i++) { + RAnalFunction *callee_fcn = materialize_function_at (anal, direct_targets[i]); + if (!callee_fcn || callee_fcn->addr == fcn->addr + || function_exceeds_helper_scope_budget (callee_fcn)) { + continue; + } + warm_type_payload_cache_for_function ( + core, anal, ctx, callee_fcn, max_iters, &seen_addrs, &seen_count, &seen_cap); + } scope_json = build_type_interproc_scope_json_from_targets ( - core, anal, direct_targets, direct_target_count); + core, anal, ctx, direct_targets, direct_target_count, max_iters); free (direct_targets); + free (seen_addrs); return scope_json; } @@ -10332,6 +11697,11 @@ static bool sleigh_post_analysis(RAnal *anal) { bool sigwrite_enabled = post_mode != SLEIGH_MODE_FAST; bool type_writeback_enabled = sigwrite_enabled && type_wb_mode != SLEIGH_TYPE_WRITEBACK_OFF; bool sigverify_enabled = false; + ut64 post_start_us = r_time_now_mono (); + ut64 post_budget_us = post_mode == SLEIGH_MODE_FAST + ? SLEIGH_POST_ANALYSIS_FAST_BUDGET_USEC + : SLEIGH_POST_ANALYSIS_BALANCED_BUDGET_USEC; + bool post_budget_exhausted = false; ut64 *type_eligible_addrs = NULL; size_t type_eligible_count = 0; size_t type_eligible_cap = 0; @@ -10371,6 +11741,12 @@ static bool sleigh_post_analysis(RAnal *anal) { RListIter *iter; RAnalFunction *fcn; r_list_foreach (anal->fcns, iter, fcn) { + if (post_budget_us && r_time_now_mono () - post_start_us > post_budget_us) { + post_budget_exhausted = true; + R_LOG_WARN ("r2sleigh: post-analysis budget exhausted after %llu usec; refusing remaining automatic enrichment", + (unsigned long long)(r_time_now_mono () - post_start_us)); + break; + } int bb_count = (fcn && fcn->bbs) ? r_list_length (fcn->bbs) : 0; bool sig_scope_eligible = !sigwrite_focus_only || (focus_callee_addr && fcn && fcn->addr == focus_callee_addr); bool type_scope_eligible = !type_writeback_focus_only || (focus_callee_addr && fcn && fcn->addr == focus_callee_addr); @@ -10405,7 +11781,7 @@ static bool sleigh_post_analysis(RAnal *anal) { if (!fcn || !need_blocks) { continue; } - if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { + if (!lift_function_blocks (anal, fcn, ctx, &blocks, true)) { continue; } @@ -10737,6 +12113,7 @@ static bool sleigh_post_analysis(RAnal *anal) { bool signature_arch_eligible = sig_arch_supported; bool callconv_arch_eligible = cc_arch_supported; bool sig_metrics_eligible = signature_arch_eligible && signature_part_eligible; + ut64 artifact_key = 0; ut64 cache_key = 0; ut64 payload_hash = 0; ut64 dep_hash = 0; @@ -10756,16 +12133,28 @@ static bool sleigh_post_analysis(RAnal *anal) { free (external_context_json); external_context_json = strdup ("{}"); } + interproc_scope_json = build_type_interproc_scope_json ( + core, anal, ctx, fcn, &blocks, type_max_iters); + artifact_key = r2sleigh_function_analysis_artifact_cache_key ( + ctx, + (const R2ILBlock **)blocks.blocks, + blocks.count, + fcn->addr, + fcn->name, + external_context_json? external_context_json: "{}", + type_max_iters, + interproc_scope_json? interproc_scope_json: "{}"); if (type_cache_enabled && type_writeback_enabled) { TypeWritebackCacheEntry *cache_entry; dep_hash = compute_callee_dependency_hash (core, anal, fcn); - cache_key = compute_type_cache_key (fcn, external_context_json, + cache_key = compute_type_cache_key (artifact_key, dep_hash, type_wb_mode, type_min_conf, type_rename_min_conf, type_struct_min_conf, type_max_iters); cache_entry = type_writeback_cache_get (fcn->addr); if (cache_entry && cache_entry->key == cache_key) { type_wb.cache_hits++; + free (interproc_scope_json); free (external_context_json); block_array_free (&blocks); continue; @@ -10778,13 +12167,10 @@ static bool sleigh_post_analysis(RAnal *anal) { } } - interproc_scope_json = build_type_interproc_scope_json (core, anal, ctx, fcn, &blocks); payload_json = r2sleigh_infer_type_writeback_json_ex (ctx, (const R2ILBlock **)blocks.blocks, blocks.count, fcn->addr, fcn->name, external_context_json? external_context_json: "{}", 1, type_max_iters, 1, interproc_scope_json? interproc_scope_json: "{}"); - free (interproc_scope_json); - interproc_scope_json = NULL; if (!payload_json || !*payload_json) { if (sig_metrics_eligible) { sig_parse_failures++; @@ -10928,7 +12314,14 @@ static bool sleigh_post_analysis(RAnal *anal) { if (type_cache_enabled && type_writeback_enabled) { ut64 applied_hash = type_payload_changed? payload_hash: 0; - if (type_writeback_cache_put (fcn->addr, cache_key, dep_hash, payload_hash, applied_hash, payload_json)) { + if (type_writeback_cache_put ( + fcn->addr, + cache_key, + dep_hash, + payload_hash, + applied_hash, + payload_json, + interproc_scope_json? interproc_scope_json: "{}")) { type_wb.cache_updates++; } } @@ -10959,7 +12352,7 @@ static bool sleigh_post_analysis(RAnal *anal) { block_array_free (&blocks); } - if (xref_enabled) { + if (xref_enabled && !post_budget_exhausted) { ut64 *xref_queue = NULL; size_t xref_queue_count = 0; size_t xref_queue_cap = 0; @@ -10978,6 +12371,12 @@ static bool sleigh_post_analysis(RAnal *anal) { } while (xref_queue_count > 0) { + if (post_budget_us && r_time_now_mono () - post_start_us > post_budget_us) { + post_budget_exhausted = true; + R_LOG_WARN ("r2sleigh: post-analysis xref budget exhausted after %llu usec; refusing remaining automatic xrefs", + (unsigned long long)(r_time_now_mono () - post_start_us)); + break; + } ut64 faddr = xref_queue[--xref_queue_count]; RAnalFunction *xref_fcn_cur = r_anal_get_fcn_in (anal, faddr, 0); BlockArray xref_blocks; @@ -10988,7 +12387,7 @@ static bool sleigh_post_analysis(RAnal *anal) { if (!xref_fcn_cur) { continue; } - if (!lift_function_blocks (anal, xref_fcn_cur, ctx, &xref_blocks)) { + if (!lift_function_blocks (anal, xref_fcn_cur, ctx, &xref_blocks, true)) { continue; } cache_key = compute_xref_cache_key (xref_fcn_cur, &xref_blocks, post_mode); @@ -11014,7 +12413,7 @@ static bool sleigh_post_analysis(RAnal *anal) { qsort (type_eligible_addrs, type_eligible_count, sizeof (ut64), ut64_cmp_asc); } - if (type_writeback_enabled) { + if (type_writeback_enabled && !post_budget_exhausted) { ut64 *queue = NULL; size_t queue_count = 0; size_t queue_cap = 0; @@ -11033,6 +12432,12 @@ static bool sleigh_post_analysis(RAnal *anal) { } } while (queue_count > 0 && iter_idx < type_max_iters) { + if (post_budget_us && r_time_now_mono () - post_start_us > post_budget_us) { + post_budget_exhausted = true; + R_LOG_WARN ("r2sleigh: post-analysis type fixpoint budget exhausted after %llu usec; refusing remaining type propagation", + (unsigned long long)(r_time_now_mono () - post_start_us)); + break; + } ut64 *current = queue; size_t current_count = queue_count; iter_idx++; @@ -11054,6 +12459,7 @@ static bool sleigh_post_analysis(RAnal *anal) { bool type_changed = false; bool sig_or_cc_changed = false; bool summary_changed = false; + ut64 artifact_key = 0; ut64 payload_hash = 0; ut64 dep_hash = 0; ut64 cache_key = 0; @@ -11068,7 +12474,7 @@ static bool sleigh_post_analysis(RAnal *anal) { type_wb.type_fcns_skipped_size++; continue; } - if (!lift_function_blocks (anal, fcn, ctx, &blocks)) { + if (!lift_function_blocks (anal, fcn, ctx, &blocks, true)) { continue; } @@ -11077,16 +12483,28 @@ static bool sleigh_post_analysis(RAnal *anal) { free (external_context_json); external_context_json = strdup ("{}"); } + interproc_scope_json = build_type_interproc_scope_json ( + core, anal, ctx, fcn, &blocks, type_max_iters); + artifact_key = r2sleigh_function_analysis_artifact_cache_key ( + ctx, + (const R2ILBlock **)blocks.blocks, + blocks.count, + fcn->addr, + fcn->name, + external_context_json? external_context_json: "{}", + type_max_iters, + interproc_scope_json? interproc_scope_json: "{}"); if (type_cache_enabled) { TypeWritebackCacheEntry *cache_entry; dep_hash = compute_callee_dependency_hash (core, anal, fcn); - cache_key = compute_type_cache_key (fcn, external_context_json, + cache_key = compute_type_cache_key (artifact_key, dep_hash, type_wb_mode, type_min_conf, type_rename_min_conf, type_struct_min_conf, type_max_iters); cache_entry = type_writeback_cache_get (fcn->addr); if (cache_entry && cache_entry->key == cache_key) { type_wb.cache_hits++; + free (interproc_scope_json); free (external_context_json); block_array_free (&blocks); continue; @@ -11099,14 +12517,11 @@ static bool sleigh_post_analysis(RAnal *anal) { } } - interproc_scope_json = build_type_interproc_scope_json (core, anal, ctx, fcn, &blocks); payload_json = r2sleigh_infer_type_writeback_json_ex (ctx, (const R2ILBlock **)blocks.blocks, blocks.count, fcn->addr, fcn->name, external_context_json? external_context_json: "{}", (size_t)iter_idx, (size_t)type_max_iters, 0, interproc_scope_json? interproc_scope_json: "{}"); - free (interproc_scope_json); - interproc_scope_json = NULL; if (!payload_json || !*payload_json) { type_wb.payload_missing++; r2il_string_free (payload_json); @@ -11173,7 +12588,14 @@ static bool sleigh_post_analysis(RAnal *anal) { } if (type_cache_enabled) { ut64 applied_hash = (type_changed || sig_or_cc_changed)? payload_hash: 0; - if (type_writeback_cache_put (fcn->addr, cache_key, dep_hash, payload_hash, applied_hash, payload_json)) { + if (type_writeback_cache_put ( + fcn->addr, + cache_key, + dep_hash, + payload_hash, + applied_hash, + payload_json, + interproc_scope_json? interproc_scope_json: "{}")) { type_wb.cache_updates++; } } @@ -11224,8 +12646,8 @@ static bool sleigh_post_analysis(RAnal *anal) { snprintf (type_wb.fixpoint_stop_reason, sizeof (type_wb.fixpoint_stop_reason), type_writeback_enabled? "queue_empty": "off"); } - R_LOG_INFO ("r2sleigh: post-analysis summary fcns=%d xref_cache_hits=%d xref_recomputes=%d xref_dirty_queued=%d type_queue_pops=%d type_fixpoint_converged=%d", - num_fcns, xref_cache_hits, xref_recomputes, xref_dirty_queued, + R_LOG_INFO ("r2sleigh: post-analysis summary fcns=%d budget_exhausted=%d xref_cache_hits=%d xref_recomputes=%d xref_dirty_queued=%d type_queue_pops=%d type_fixpoint_converged=%d", + num_fcns, post_budget_exhausted? 1: 0, xref_cache_hits, xref_recomputes, xref_dirty_queued, type_wb.fixpoint_queue_pops, type_wb.fixpoint_converged); R_LOG_INFO ("r2sleigh: type write-back enabled=%d mode=%d vars_considered=%d vars_applied=%d vars_hint_only=%d vars_low_conf=%d vars_conflict=%d vars_api_verify_fail=%d vars_cmd_fallback_attempted=%d vars_cmd_apply_fail=%d renames_considered=%d renames_applied=%d renames_low_conf=%d renames_conflict=%d rename_generated_guard_skips=%d structs_considered=%d structs_imported=%d structs_low_conf=%d structs_import_fail=%d global_links_considered=%d global_links_applied=%d global_links_low_conf=%d global_links_conflict_skip=%d global_links_existing_preserved=%d global_links_fail=%d payload_missing=%d payload_parse_failures=%d cache_hits=%d cache_misses=%d cache_invalidates=%d cache_updates=%d type_skipped_arch=%d type_skipped_size=%d fixpoint_iters=%d fixpoint_converged=%d fixpoint_queue_pushes=%d fixpoint_queue_pops=%d fixpoint_requeues=%d fixpoint_stop=%s", type_writeback_enabled? 1: 0, (int)type_wb_mode, diff --git a/r2plugin/src/analysis/sym.rs b/r2plugin/src/analysis/sym.rs index d1a1713..c6bc574 100644 --- a/r2plugin/src/analysis/sym.rs +++ b/r2plugin/src/analysis/sym.rs @@ -1,10 +1,15 @@ use crate::blocks::BlockSlice; use crate::context::require_ctx_view; +use crate::helpers::effective_ptr_bits; use crate::{ArchSpec, R2ILBlock, R2ILContext, parse_addr_name_map}; use serde::Serialize; +use serde_json::json; +use std::any::Any; use std::collections::BTreeMap; use std::collections::HashMap; use std::ffi::{CStr, CString}; +use std::fs::OpenOptions; +use std::io::Write; use std::os::raw::c_char; use std::ptr; use std::slice; @@ -21,6 +26,28 @@ const SYM_PATHS_CALL_HEAVY_MAX_DEPTH: usize = 32; const SYM_PATHS_TIMEOUT_MS: u64 = 500; const SYM_PATHS_SOLUTION_LIMIT: usize = 4; +fn solve_pipeline_debug_enabled() -> bool { + std::env::var_os("R2SLEIGH_DEBUG_SOLVE_PIPELINE").is_some() +} + +fn solve_pipeline_debug_log(message: &str) { + if !solve_pipeline_debug_enabled() { + return; + } + let line = format!("{message}\n"); + if let Some(path) = std::env::var_os("R2SLEIGH_DEBUG_SOLVE_PIPELINE_LOG") { + if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) { + let _ = file.write_all(line.as_bytes()); + } + return; + } + let _ = std::io::stderr().write_all(line.as_bytes()); +} + +fn solve_pipeline_debug_stage(stage: &str) { + solve_pipeline_debug_log(stage); +} + fn merge_states_enabled() -> bool { MERGE_STATES.load(Ordering::Relaxed) } @@ -152,10 +179,10 @@ pub extern "C" fn r2sym_merge_set_enabled(enabled: i32) { fn sym_default_config() -> r2sym::ExploreConfig { r2sym::ExploreConfig { - max_states: 100, - max_depth: 200, + max_states: 200, + max_depth: 800, merge_states: merge_states_enabled(), - timeout: Some(std::time::Duration::from_secs(5)), + timeout: Some(std::time::Duration::from_secs(20)), ..Default::default() } } @@ -165,9 +192,24 @@ fn sym_default_query_config() -> r2sym::SymQueryConfig { explore: sym_default_config(), mode: r2sym::QueryMode::TargetGuided, summary_profile: r2sym::SummaryProfile::Default, + solve_tactics: r2sym::SolveTacticConfig::default(), } } +fn tune_query_config_for_state( + config: &mut r2sym::SymQueryConfig, + prepared: &r2ssa::SsaArtifact, + initial_state: &r2sym::SymState<'_>, + route: Option<&r2sym::TargetQueryRoutePlan>, +) -> r2sym::QueryExecutionPolicy { + let route = route + .cloned() + .unwrap_or_else(r2sym::TargetQueryRoutePlan::dynamic_fallback); + let policy = r2sym::QueryExecutionPolicy::for_route(config, prepared, initial_state, route); + r2sym::apply_query_execution_policy(config, &policy); + policy +} + fn sym_paths_query_config(prepared: &r2ssa::SsaArtifact) -> r2sym::SymQueryConfig { let mut config = sym_default_query_config(); if prepared.call_sites().by_id.is_empty() { @@ -195,10 +237,25 @@ fn sym_paths_solution_limit(result_count: usize, prepared: &r2ssa::SsaArtifact) } fn sym_error_json(message: &str) -> *mut c_char { - let payload = format!(r#"{{"error":"{}"}}"#, message); + let payload = json!({ "error": message }).to_string(); CString::new(payload).map_or(ptr::null_mut(), |c| c.into_raw()) } +fn panic_payload_message(payload: &(dyn Any + Send)) -> Option { + payload.downcast_ref::().cloned().or_else(|| { + payload + .downcast_ref::<&'static str>() + .map(|msg| (*msg).to_string()) + }) +} + +fn sym_panic_json(default: &str, payload: Box) -> *mut c_char { + let message = panic_payload_message(payload.as_ref()) + .map(|details| format!("{default}: {details}")) + .unwrap_or_else(|| default.to_string()); + sym_error_json(&message) +} + fn sym_symbol_map() -> &'static Mutex> { static MAP: OnceLock>> = OnceLock::new(); MAP.get_or_init(|| Mutex::new(HashMap::new())) @@ -230,6 +287,30 @@ struct SymExecSummary { paths_explored: usize, paths_feasible: usize, paths_pruned: usize, + #[serde(default, skip_serializing_if = "is_zero")] + target_pruned_cfg_unreachable: usize, + #[serde(default, skip_serializing_if = "is_zero")] + target_pruned_summary_contradiction: usize, + #[serde(default, skip_serializing_if = "is_zero")] + target_match_unsat: usize, + #[serde(default, skip_serializing_if = "is_zero")] + runtime_symbolic_breakpoint_forks: usize, + #[serde(default, skip_serializing_if = "is_zero")] + runtime_symbolic_breakpoint_pruned: usize, + #[serde(default, skip_serializing_if = "is_zero")] + runtime_breakpoint_loop_summaries: usize, + #[serde(default, skip_serializing_if = "is_zero")] + runtime_breakpoint_loop_exact_summaries: usize, + #[serde(default, skip_serializing_if = "is_zero")] + runtime_loop_exact_recurrence_summaries: usize, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + runtime_loop_exact_folds: Vec, + #[serde(default, skip_serializing_if = "is_zero")] + runtime_loop_refusals: usize, + #[serde(default, skip_serializing_if = "is_zero")] + runtime_loop_unknown_carried_state: usize, + #[serde(default, skip_serializing_if = "is_zero")] + runtime_loop_budget_residuals: usize, max_depth: usize, states_explored: usize, sat_queries: usize, @@ -238,16 +319,35 @@ struct SymExecSummary { solve_calls: usize, solve_unsat_shortcuts: usize, time_ms: u64, + #[serde( + default, + skip_serializing_if = "r2ssa::AssumptionUsageReport::is_empty" + )] + assumption_usage: r2ssa::AssumptionUsageReport, + #[serde(default, skip_serializing_if = "is_false")] + assumption_conditioned: bool, + #[serde(default, skip_serializing_if = "is_false")] + summary_conditioned: bool, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + runtime_diagnostics: Vec, #[serde(skip_serializing_if = "Option::is_none")] semantic: Option, } +fn is_zero(value: &usize) -> bool { + *value == 0 +} + #[derive(Debug, Serialize, Clone)] pub(crate) struct CompiledSemanticInfo { pub(crate) schema_version: u32, pub(crate) stage: String, pub(crate) granularity: String, pub(crate) execution: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) seed_mode: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) replay_seed_fingerprint: Option, pub(crate) query_plan: r2sym::QueryPlan, pub(crate) type_plan: r2sym::TypePlan, pub(crate) decompile_plan: r2sym::DecompilePlan, @@ -732,6 +832,8 @@ struct SymTargetExploreResult { target: String, matched_paths: usize, stats: SymExecSummary, + #[serde(skip_serializing_if = "Option::is_none")] + target_query: Option, paths: Vec, } @@ -739,13 +841,150 @@ struct SymTargetExploreResult { struct SymTargetSolveResult { entry: String, target: String, + status: String, matched_paths: usize, found: bool, + target_reached_under_model: bool, + candidate_solution_verified: bool, + residual_reasons: Vec, + model_validation: ModelValidationInfo, + witness: SolveWitnessInfo, stats: SymExecSummary, #[serde(skip_serializing_if = "Option::is_none")] + target_query: Option, + #[serde(skip_serializing_if = "Option::is_none")] selected_path: Option, } +#[derive(Serialize)] +struct ModelValidationInfo { + status: String, + #[serde(skip_serializing_if = "Option::is_none")] + reason: Option, +} + +#[derive(Serialize)] +struct SolveWitnessInfo { + target: String, + #[serde(skip_serializing_if = "Option::is_none")] + selected_path_index: Option, + #[serde(skip_serializing_if = "Option::is_none")] + final_pc: Option, + #[serde(skip_serializing_if = "Option::is_none")] + candidate: Option, + evidence: EvidenceSummaryInfo, + verification_requirement: VerificationRequirementInfo, + proven: bool, +} + +#[derive(Serialize)] +struct SolveCandidateShapeInfo { + input_scalars: usize, + input_buffers: usize, + registers: usize, + memory_regions: usize, + concrete_assignments: usize, + final_pc: String, + num_constraints: usize, + #[serde(skip_serializing_if = "Option::is_none")] + generation: Option, +} + +#[derive(Serialize)] +struct SolveCandidateGenerationInfo { + kind: String, + reason: String, + constrained_bytes: usize, +} + +#[derive(Serialize)] +struct EvidenceSummaryInfo { + precision: String, + requires_replay: bool, + reasons: Vec, + final_constraint_precision: String, + exact_final_constraints: usize, + model_conditioned_final_constraints: usize, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + exact_loop_recurrences: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + exact_loop_folds: Vec, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + tactics: Vec, +} + +#[derive(Serialize)] +struct VerificationRequirementInfo { + status: String, + reasons: Vec, +} + +#[derive(Serialize, Clone)] +struct ExactLoopFoldInfo { + header: String, + exit_target: String, + iterations: u64, + accumulator: String, + bits: u32, + operation: String, + memory: LoopMemoryTermInfo, +} + +#[derive(Serialize, Clone)] +struct ExactLoopRecurrenceInfo { + header: String, + exit_target: String, + iterations: u64, + accumulator: String, + bits: u32, + kind: String, + #[serde(skip_serializing_if = "Option::is_none")] + constant: Option, + #[serde(skip_serializing_if = "Option::is_none")] + multiplier: Option, + #[serde(skip_serializing_if = "Option::is_none")] + addend: Option, + #[serde(skip_serializing_if = "Option::is_none")] + direction: Option, + #[serde(skip_serializing_if = "Option::is_none")] + amount: Option, + #[serde(skip_serializing_if = "Option::is_none")] + operation: Option, + #[serde(skip_serializing_if = "Option::is_none")] + memory: Option, +} + +#[derive(Serialize, Clone)] +struct LoopMemoryTermInfo { + kind: String, + addr: String, + bytes: u32, + #[serde(skip_serializing_if = "Option::is_none")] + base: Option, + #[serde(skip_serializing_if = "Option::is_none")] + stride: Option, + #[serde(skip_serializing_if = "Option::is_none")] + region: Option, + #[serde(skip_serializing_if = "Option::is_none")] + region_base: Option, + #[serde(skip_serializing_if = "Option::is_none")] + region_size: Option, +} + +#[derive(Serialize, Clone)] +struct SolveTacticInfo { + kind: String, + status: String, + reason: String, + recurrence: ExactLoopRecurrenceInfo, +} + +#[derive(Serialize, Clone)] +struct TargetQueryInfo { + plan: r2sym::TargetQueryPlan, + route: r2sym::TargetQueryRoutePlan, +} + #[derive(Serialize)] struct SymRunPathInfo { path_id: usize, @@ -777,16 +1016,93 @@ struct SymRunResult { diagnostics: Vec, } +fn is_false(value: &bool) -> bool { + !*value +} + fn build_sym_exec_summary( stats: &r2sym::path::ExploreStats, solver_stats: &r2sym::SolverStats, paths_feasible: usize, + assumption_usage: r2ssa::AssumptionUsageReport, + assumption_conditioned: bool, + summary_conditioned: bool, semantic: Option<&r2sym::SemanticArtifact>, ) -> SymExecSummary { + build_sym_exec_summary_with_semantic_info( + stats, + solver_stats, + paths_feasible, + assumption_usage, + assumption_conditioned, + summary_conditioned, + semantic.map(compiled_semantic_info), + ) +} + +fn build_sym_exec_summary_with_semantic_info( + stats: &r2sym::path::ExploreStats, + solver_stats: &r2sym::SolverStats, + paths_feasible: usize, + assumption_usage: r2ssa::AssumptionUsageReport, + assumption_conditioned: bool, + summary_conditioned: bool, + semantic: Option, +) -> SymExecSummary { + let mut runtime_diagnostics = Vec::new(); + if stats.runtime_missing_exception_handler > 0 { + runtime_diagnostics.push("missing_exception_handler".to_string()); + } + if stats.runtime_missing_materialized_code > 0 { + runtime_diagnostics.push("missing_runtime_materialized_code".to_string()); + } + if stats.runtime_missing_continuation_seed > 0 { + runtime_diagnostics.push("missing_continuation_seed".to_string()); + } + if stats.runtime_region_provenance_unknown > 0 { + runtime_diagnostics.push("runtime_region_provenance_unknown".to_string()); + } + if stats.timed_out && stats.runtime_symbolic_breakpoint_forks > 0 { + runtime_diagnostics.push("runtime_symbolic_breakpoint_budget_exhausted".to_string()); + } + if stats.runtime_breakpoint_loop_summaries > 0 { + runtime_diagnostics.push("runtime_breakpoint_loop_summary_residual".to_string()); + } + if stats.runtime_breakpoint_loop_exact_summaries > 0 { + runtime_diagnostics.push("runtime_breakpoint_loop_summary_exact".to_string()); + } + if stats.runtime_loop_exact_recurrence_summaries > 0 { + runtime_diagnostics.push("runtime_loop_exact_recurrence_summary".to_string()); + } + if stats.runtime_loop_unknown_carried_state > 0 { + runtime_diagnostics.push("runtime_loop_unknown_carried_state".to_string()); + } + if stats.runtime_loop_budget_residuals > 0 { + runtime_diagnostics.push("runtime_loop_iteration_budget".to_string()); + } + if stats.runtime_loop_refusals > 0 { + runtime_diagnostics.push("runtime_loop_refused".to_string()); + } SymExecSummary { paths_explored: stats.paths_completed, paths_feasible, paths_pruned: stats.paths_pruned, + target_pruned_cfg_unreachable: stats.target_pruned_cfg_unreachable, + target_pruned_summary_contradiction: stats.target_pruned_summary_contradiction, + target_match_unsat: stats.target_match_unsat, + runtime_symbolic_breakpoint_forks: stats.runtime_symbolic_breakpoint_forks, + runtime_symbolic_breakpoint_pruned: stats.runtime_symbolic_breakpoint_pruned, + runtime_breakpoint_loop_summaries: stats.runtime_breakpoint_loop_summaries, + runtime_breakpoint_loop_exact_summaries: stats.runtime_breakpoint_loop_exact_summaries, + runtime_loop_exact_recurrence_summaries: stats.runtime_loop_exact_recurrence_summaries, + runtime_loop_exact_folds: stats + .runtime_loop_exact_folds + .iter() + .map(exact_loop_fold_info) + .collect(), + runtime_loop_refusals: stats.runtime_loop_refusals, + runtime_loop_unknown_carried_state: stats.runtime_loop_unknown_carried_state, + runtime_loop_budget_residuals: stats.runtime_loop_budget_residuals, max_depth: stats.max_depth_reached, states_explored: stats.states_explored, sat_queries: solver_stats.sat_queries, @@ -795,10 +1111,341 @@ fn build_sym_exec_summary( solve_calls: solver_stats.solve_calls, solve_unsat_shortcuts: solver_stats.solve_unsat_shortcuts, time_ms: stats.total_time.as_millis() as u64, - semantic: semantic.map(compiled_semantic_info), + assumption_usage, + assumption_conditioned, + summary_conditioned, + runtime_diagnostics, + semantic, + } +} + +fn target_query_info(route: &r2sym::TargetQueryRoutePlan) -> TargetQueryInfo { + TargetQueryInfo { + plan: route.target_plan.clone(), + route: route.clone(), + } +} + +fn solve_status_string(status: r2sym::SolveStatus) -> String { + match status { + r2sym::SolveStatus::Solved => "solved", + r2sym::SolveStatus::Candidate => "candidate", + r2sym::SolveStatus::ResidualReachable => "residual_reachable", + r2sym::SolveStatus::Unverified => "unverified", + r2sym::SolveStatus::Unsat => "unsat", + r2sym::SolveStatus::Unknown => "unknown", + r2sym::SolveStatus::BudgetExhausted => "budget_exhausted", + } + .to_string() +} + +fn solve_found(status: r2sym::SolveStatus) -> bool { + matches!(status, r2sym::SolveStatus::Solved) +} + +fn model_validation_info(validation: &r2sym::ModelValidation) -> ModelValidationInfo { + match validation { + r2sym::ModelValidation::NotRequired => ModelValidationInfo { + status: "not_required".to_string(), + reason: None, + }, + r2sym::ModelValidation::Verified => ModelValidationInfo { + status: "verified".to_string(), + reason: None, + }, + r2sym::ModelValidation::Failed { reason } => ModelValidationInfo { + status: "failed".to_string(), + reason: Some(reason.clone()), + }, + r2sym::ModelValidation::Unavailable { reason } => ModelValidationInfo { + status: "unavailable".to_string(), + reason: Some(reason.clone()), + }, + } +} + +fn fact_precision_string(precision: r2sym::FactPrecision) -> String { + match precision { + r2sym::FactPrecision::Unknown => "unknown", + r2sym::FactPrecision::UnderApprox => "under_approx", + r2sym::FactPrecision::Residual => "residual", + r2sym::FactPrecision::OverApprox => "over_approx", + r2sym::FactPrecision::Exact => "exact", + } + .to_string() +} + +fn verification_requirement_info( + requirement: &r2sym::VerificationRequirement, +) -> VerificationRequirementInfo { + match requirement { + r2sym::VerificationRequirement::NotRequired => VerificationRequirementInfo { + status: "not_required".to_string(), + reasons: Vec::new(), + }, + r2sym::VerificationRequirement::Required { reasons } => VerificationRequirementInfo { + status: "required".to_string(), + reasons: reasons.clone(), + }, + r2sym::VerificationRequirement::Refused { reasons } => VerificationRequirementInfo { + status: "refused".to_string(), + reasons: reasons.clone(), + }, + } +} + +fn loop_memory_term_kind_string(kind: r2sym::LoopMemoryTermKind) -> String { + match kind { + r2sym::LoopMemoryTermKind::TableRead => "table_read", + r2sym::LoopMemoryTermKind::InputRead => "input_read", + r2sym::LoopMemoryTermKind::RuntimeBlobRead => "runtime_blob_read", + r2sym::LoopMemoryTermKind::Unknown => "unknown", + } + .to_string() +} + +fn loop_fold_operation_string(operation: r2sym::LoopFoldOperation) -> String { + match operation { + r2sym::LoopFoldOperation::Add => "add", + r2sym::LoopFoldOperation::Xor => "xor", + } + .to_string() +} + +fn loop_rotate_direction_string(direction: r2sym::LoopRotateDirection) -> String { + match direction { + r2sym::LoopRotateDirection::Left => "left", + r2sym::LoopRotateDirection::Right => "right", + } + .to_string() +} + +fn solve_tactic_kind_string(kind: r2sym::SolveTacticKind) -> String { + match kind { + r2sym::SolveTacticKind::XorFoldPreimage => "xor_fold_preimage", + r2sym::SolveTacticKind::AddFoldPreimage => "add_fold_preimage", + r2sym::SolveTacticKind::RotateXorRecurrence => "rotate_xor_recurrence", + r2sym::SolveTacticKind::RotateAddRecurrence => "rotate_add_recurrence", + r2sym::SolveTacticKind::ConcreteTableFold => "concrete_table_fold", + r2sym::SolveTacticKind::RuntimeBlobFold => "runtime_blob_fold", + } + .to_string() +} + +fn solve_tactic_status_string(status: r2sym::SolveTacticStatus) -> String { + match status { + r2sym::SolveTacticStatus::Available => "available", + r2sym::SolveTacticStatus::EvidenceOnly => "evidence_only", + } + .to_string() +} + +fn loop_memory_term_info(term: &r2sym::LoopMemoryTerm) -> LoopMemoryTermInfo { + LoopMemoryTermInfo { + kind: loop_memory_term_kind_string(term.kind), + addr: term.addr.clone(), + bytes: term.bytes, + base: term.base.map(|addr| format!("0x{addr:x}")), + stride: term.stride, + region: term.region.clone(), + region_base: term.region_base.map(|addr| format!("0x{addr:x}")), + region_size: term.region_size, + } +} + +fn exact_loop_fold_info(fold: &r2sym::ExactLoopFoldEvidence) -> ExactLoopFoldInfo { + ExactLoopFoldInfo { + header: format!("0x{:x}", fold.header), + exit_target: format!("0x{:x}", fold.exit_target), + iterations: fold.iterations, + accumulator: fold.accumulator.clone(), + bits: fold.bits, + operation: loop_fold_operation_string(fold.operation), + memory: loop_memory_term_info(&fold.term), + } +} + +fn exact_loop_recurrence_info( + recurrence: &r2sym::ExactLoopRecurrenceEvidence, +) -> ExactLoopRecurrenceInfo { + let (kind, constant, multiplier, addend, direction, amount, operation, memory) = + match &recurrence.kind { + r2sym::ExactLoopRecurrenceKind::AddConst(value) => ( + "add_const".to_string(), + Some(format!("0x{value:x}")), + None, + None, + None, + None, + None, + None, + ), + r2sym::ExactLoopRecurrenceKind::SubConst(value) => ( + "sub_const".to_string(), + Some(format!("0x{value:x}")), + None, + None, + None, + None, + None, + None, + ), + r2sym::ExactLoopRecurrenceKind::AffineConst { multiplier, addend } => ( + "affine_const".to_string(), + None, + Some(format!("0x{multiplier:x}")), + Some(format!("0x{addend:x}")), + None, + None, + None, + None, + ), + r2sym::ExactLoopRecurrenceKind::XorConst(value) => ( + "xor_const".to_string(), + Some(format!("0x{value:x}")), + None, + None, + None, + None, + None, + None, + ), + r2sym::ExactLoopRecurrenceKind::Fold { operation, term } => ( + "fold".to_string(), + None, + None, + None, + None, + None, + Some(loop_fold_operation_string(*operation)), + Some(loop_memory_term_info(term)), + ), + r2sym::ExactLoopRecurrenceKind::RotateMix { + direction, + amount, + operation, + term, + } => ( + "rotate_mix".to_string(), + None, + None, + None, + Some(loop_rotate_direction_string(*direction)), + Some(*amount), + Some(loop_fold_operation_string(*operation)), + Some(loop_memory_term_info(term)), + ), + }; + ExactLoopRecurrenceInfo { + header: format!("0x{:x}", recurrence.header), + exit_target: format!("0x{:x}", recurrence.exit_target), + iterations: recurrence.iterations, + accumulator: recurrence.accumulator.clone(), + bits: recurrence.bits, + kind, + constant, + multiplier, + addend, + direction, + amount, + operation, + memory, + } +} + +fn solve_tactic_info(tactic: &r2sym::SolveTacticEvidence) -> SolveTacticInfo { + SolveTacticInfo { + kind: solve_tactic_kind_string(tactic.kind), + status: solve_tactic_status_string(tactic.status), + reason: tactic.reason.clone(), + recurrence: exact_loop_recurrence_info(&tactic.recurrence), + } +} + +fn solve_candidate_shape_info(candidate: &r2sym::SolveCandidateShape) -> SolveCandidateShapeInfo { + SolveCandidateShapeInfo { + input_scalars: candidate.input_scalars, + input_buffers: candidate.input_buffers, + registers: candidate.registers, + memory_regions: candidate.memory_regions, + concrete_assignments: candidate.concrete_assignments, + final_pc: format!("0x{:x}", candidate.final_pc), + num_constraints: candidate.num_constraints, + generation: candidate + .generation + .as_ref() + .map(solve_candidate_generation_info), + } +} + +fn solve_candidate_generation_info( + generation: &r2sym::SolvedPathGeneration, +) -> SolveCandidateGenerationInfo { + SolveCandidateGenerationInfo { + kind: match generation.kind { + r2sym::SolvedPathGenerationKind::ExactRecurrenceConstraintTactic => { + "exact_recurrence_constraint_tactic" + } + r2sym::SolvedPathGenerationKind::MitmConstraintTactic => "mitm_constraint_tactic", + r2sym::SolvedPathGenerationKind::DomainConstraintTactic => "domain_constraint_tactic", + } + .to_string(), + reason: generation.reason.clone(), + constrained_bytes: generation.constrained_bytes, } } +fn solve_witness_info(witness: &r2sym::SolveWitness) -> SolveWitnessInfo { + SolveWitnessInfo { + target: format!("0x{:x}", witness.target_addr), + selected_path_index: witness.selected_path_index, + final_pc: witness.final_pc.map(|pc| format!("0x{pc:x}")), + candidate: witness.candidate.as_ref().map(solve_candidate_shape_info), + evidence: EvidenceSummaryInfo { + precision: fact_precision_string(witness.evidence.precision), + requires_replay: witness.evidence.requires_replay, + reasons: witness.evidence.reasons.clone(), + final_constraint_precision: final_constraint_precision_string( + witness.evidence.final_constraint_precision, + ), + exact_final_constraints: witness.evidence.exact_final_constraints, + model_conditioned_final_constraints: witness + .evidence + .model_conditioned_final_constraints, + exact_loop_recurrences: witness + .evidence + .exact_loop_recurrences + .iter() + .map(exact_loop_recurrence_info) + .collect(), + exact_loop_folds: witness + .evidence + .exact_loop_folds + .iter() + .map(exact_loop_fold_info) + .collect(), + tactics: witness + .evidence + .tactics + .iter() + .map(solve_tactic_info) + .collect(), + }, + verification_requirement: verification_requirement_info(&witness.verification_requirement), + proven: witness.is_proven(), + } +} + +fn final_constraint_precision_string(precision: r2sym::FinalConstraintPrecision) -> String { + match precision { + r2sym::FinalConstraintPrecision::Exact => "exact", + r2sym::FinalConstraintPrecision::ModelConditioned => "model_conditioned", + r2sym::FinalConstraintPrecision::Residual => "residual", + r2sym::FinalConstraintPrecision::Unknown => "unknown", + } + .to_string() +} + fn empty_symbolic_summary<'ctx>() -> r2sym::SymbolicFunctionSummary<'ctx> { r2sym::SymbolicFunctionSummary { completion: r2sym::QueryCompletion::Complete, @@ -818,6 +1465,23 @@ fn should_skip_expensive_symbolic_summary( } pub(crate) fn compiled_semantic_info(compiled: &r2sym::SemanticArtifact) -> CompiledSemanticInfo { + compiled_semantic_info_with_seed(compiled, None) +} + +fn compiled_semantic_info_with_replay_seed( + compiled: &r2sym::SemanticArtifact, + replay_seed: &r2sym::ReplaySeed, +) -> CompiledSemanticInfo { + compiled_semantic_info_with_seed( + compiled, + Some(r2sym::stable_replay_seed_fingerprint(replay_seed)), + ) +} + +fn compiled_semantic_info_with_seed( + compiled: &r2sym::SemanticArtifact, + replay_seed_fingerprint: Option, +) -> CompiledSemanticInfo { let native = compiled.native_body(); CompiledSemanticInfo { schema_version: r2sym::SEMANTIC_ARTIFACT_SCHEMA_VERSION, @@ -838,6 +1502,9 @@ pub(crate) fn compiled_semantic_info(compiled: &r2sym::SemanticArtifact) -> Comp r2sym::ExecutionModel::Vm => "vm", } .to_string(), + seed_mode: replay_seed_fingerprint.map(|_| "replay".to_string()), + replay_seed_fingerprint: replay_seed_fingerprint + .map(|fingerprint| format!("0x{fingerprint:x}")), query_plan: compiled.query_plan(), type_plan: compiled.type_plan(), decompile_plan: compiled.decompile_plan(), @@ -1066,8 +1733,17 @@ fn run_path_info_from_result<'ctx>( pub(crate) fn build_symbolic_prepared( blocks: &[R2ILBlock], arch: Option<&ArchSpec>, + name: Option<&str>, ) -> Option { - r2ssa::SsaArtifact::for_symbolic(blocks, arch) + let prepared = if name.is_some_and(|name| name.starts_with("runtime.materialized.")) { + r2ssa::SsaArtifact::raw(blocks, arch)? + } else { + r2ssa::SsaArtifact::for_symbolic(blocks, arch)? + }; + Some(match name { + Some(name) if !name.is_empty() => prepared.with_name(name.to_string()), + _ => prepared, + }) } pub(crate) fn build_single_function_scope( @@ -1099,12 +1775,12 @@ pub(crate) unsafe fn build_symbolic_scope_from_ffi( for index in 0..num_functions { let function = unsafe { &*functions.add(index) }; let blocks = unsafe { BlockSlice::from_ffi(function.blocks, function.num_blocks) }?; - let prepared = build_symbolic_prepared(blocks.as_slice(), arch)?; let name = if function.name.is_null() { None } else { unsafe { CStr::from_ptr(function.name).to_str().ok() }.map(str::to_string) }; + let prepared = build_symbolic_prepared(blocks.as_slice(), arch, name.as_deref())?; scope_functions.push(r2sym::ScopedPreparedFunction { id: r2ssa::InterprocFunctionId(function.entry_addr), name, @@ -1121,29 +1797,97 @@ fn symbol_map_snapshot() -> HashMap { .unwrap_or_default() } -fn install_symbolic_hooks<'ctx>( - explorer: &mut r2sym::PathExplorer<'ctx>, - scope: &r2sym::PreparedFunctionScope, - arch: Option<&ArchSpec>, +fn scope_root_prepared(scope: &r2sym::PreparedFunctionScope) -> Option<&r2ssa::SsaArtifact> { + scope.root().map(|function| &function.prepared) +} + +fn prepared_assumption_conflicted(prepared: &r2ssa::SsaArtifact) -> bool { + !prepared.facts().assumption_usage.conflicts.is_empty() +} + +fn prepared_assumption_conditioning( + prepared: &r2ssa::SsaArtifact, +) -> (r2ssa::AssumptionUsageReport, bool) { + let usage = prepared.facts().assumption_usage.clone(); + let conditioned = !usage.applied.is_empty() || !usage.conflicts.is_empty(); + (usage, conditioned) +} + +struct PredictedTargetQueryRouteInput<'ctx, 'a> { z3_ctx: &'ctx Context, - symbol_map: &HashMap, + prepared: &'a r2ssa::SsaArtifact, + scope: Option<&'a r2sym::PreparedFunctionScope>, + compiled: &'a r2sym::SemanticArtifact, + target_addr: u64, + arch: Option<&'a ArchSpec>, + symbol_map: &'a HashMap, summary_profile: r2sym::SummaryProfile, -) { - if let Some(arch) = arch - && let Some(registry) = r2sym::SummaryRegistry::with_profile_for_arch(arch, summary_profile) - { - let _ = registry.install_scope_summaries_for_explorer( - explorer, - z3_ctx, - scope, - Some(arch), - symbol_map, - ); + assumption_conflicted: bool, +} + +fn predicted_target_query_route( + input: PredictedTargetQueryRouteInput<'_, '_>, +) -> r2sym::TargetQueryRoutePlan { + let PredictedTargetQueryRouteInput { + z3_ctx, + prepared, + scope, + compiled, + target_addr, + arch, + symbol_map, + summary_profile, + assumption_conflicted, + } = input; + let probe_config = r2sym::SymQueryConfig { + explore: sym_default_config(), + mode: r2sym::QueryMode::TargetGuided, + summary_profile, + solve_tactics: r2sym::SolveTacticConfig::default(), + }; + let mut explorer = probe_config.make_explorer(z3_ctx); + if let Some(scope) = scope { + r2sym::install_runtime_hooks_for_scope(&mut explorer, scope, arch, symbol_map); } + r2sym::selected_target_query_route_in_scope( + &mut explorer, + prepared, + scope, + Some(compiled), + target_addr, + assumption_conflicted, + ) } -fn scope_root_prepared(scope: &r2sym::PreparedFunctionScope) -> Option<&r2ssa::SsaArtifact> { - scope.root().map(|function| &function.prepared) +fn parse_scope_assumptions( + external_context_json: *const c_char, + arch: Option<&ArchSpec>, +) -> Result { + if external_context_json.is_null() { + return Ok(r2ssa::AssumptionSet::default()); + } + let text = unsafe { CStr::from_ptr(external_context_json) } + .to_str() + .map_err(|_| "external context is not valid utf-8")?; + let ptr_bits = arch.map(effective_ptr_bits).unwrap_or(64); + Ok(r2types::parse_external_context_json(text, ptr_bits).assumptions) +} + +fn scope_with_external_assumptions( + scope: &r2sym::PreparedFunctionScope, + arch: Option<&ArchSpec>, + external_context_json: *const c_char, +) -> Result { + let assumptions = parse_scope_assumptions(external_context_json, arch)?; + let prepared = scope_root_prepared(scope).ok_or("failed to build root SSA function")?; + let prepared = if assumptions.is_empty() { + prepared.clone() + } else { + prepared.with_assumptions(&assumptions) + }; + scope + .with_prepared_root(&prepared) + .ok_or("failed to build symbolic scope") } unsafe fn ffi_slice<'a, T>(ptr: *const T, len: usize) -> Result<&'a [T], &'static str> { @@ -1270,7 +2014,7 @@ pub extern "C" fn r2sym_function( return ptr::null_mut(); }; - let prepared = match build_symbolic_prepared(blocks.as_slice(), ctx_view.arch) { + let prepared = match build_symbolic_prepared(blocks.as_slice(), ctx_view.arch, None) { Some(prepared) => prepared, None => return ptr::null_mut(), }; @@ -1279,7 +2023,7 @@ pub extern "C" fn r2sym_function( }; let symbol_map = symbol_map_snapshot(); let z3_ctx = Context::thread_local(); - let query_config = sym_paths_query_config(&prepared); + let mut query_config = sym_paths_query_config(&prepared); let explore_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let compiled = r2sym::compile_semantic_artifact_with_scope( @@ -1295,14 +2039,17 @@ pub extern "C" fn r2sym_function( } let mut initial_state = r2sym::SymState::new(&z3_ctx, entry_addr); r2sym::seed_default_state_for_arch(&mut initial_state, &prepared, ctx_view.arch); + let query_policy = + tune_query_config_for_state(&mut query_config, &prepared, &initial_state, None); let mut explorer = query_config.make_explorer(&z3_ctx); - install_symbolic_hooks( + r2sym::install_symbolic_hooks_for_query_policy( &mut explorer, + &z3_ctx, &scope, ctx_view.arch, - &z3_ctx, &symbol_map, query_config.summary_profile, + &query_policy, ); ( explorer.summarize_function(&prepared, initial_state), @@ -1312,9 +2059,8 @@ pub extern "C" fn r2sym_function( let (summary, compiled) = match explore_result { Ok(r) => r, - Err(_) => { - let error_msg = r#"{"error": "symbolic execution failed (z3 context error)"}"#; - return CString::new(error_msg).map_or(ptr::null_mut(), |c| c.into_raw()); + Err(err) => { + return sym_panic_json("symbolic execution failed", err); } }; @@ -1322,6 +2068,9 @@ pub extern "C" fn r2sym_function( &summary.stats, &summary.solver_stats, summary.feasible_paths, + r2ssa::AssumptionUsageReport::default(), + false, + false, Some(&compiled), ); match serde_json::to_string_pretty(&output) { @@ -1362,7 +2111,7 @@ pub extern "C" fn r2sym_paths( return ptr::null_mut(); }; - let prepared = match build_symbolic_prepared(blocks.as_slice(), ctx_view.arch) { + let prepared = match build_symbolic_prepared(blocks.as_slice(), ctx_view.arch, None) { Some(prepared) => prepared, None => return ptr::null_mut(), }; @@ -1371,19 +2120,22 @@ pub extern "C" fn r2sym_paths( }; let symbol_map = symbol_map_snapshot(); let z3_ctx = Context::thread_local(); - let query_config = sym_paths_query_config(&prepared); + let mut query_config = sym_paths_query_config(&prepared); let explore_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let mut initial_state = r2sym::SymState::new(&z3_ctx, entry_addr); r2sym::seed_default_state_for_arch(&mut initial_state, &prepared, ctx_view.arch); + let query_policy = + tune_query_config_for_state(&mut query_config, &prepared, &initial_state, None); let mut explorer = query_config.make_explorer(&z3_ctx); - install_symbolic_hooks( + r2sym::install_symbolic_hooks_for_query_policy( &mut explorer, + &z3_ctx, &scope, ctx_view.arch, - &z3_ctx, &symbol_map, query_config.summary_profile, + &query_policy, ); let summary = explorer.summarize_function(&prepared, initial_state); (summary, explorer) @@ -1391,9 +2143,8 @@ pub extern "C" fn r2sym_paths( let (summary, explorer) = match explore_result { Ok(r) => r, - Err(_) => { - let error_msg = r#"[{"error": "symbolic execution failed (z3 context error)"}]"#; - return CString::new(error_msg).map_or(ptr::null_mut(), |c| c.into_raw()); + Err(err) => { + return sym_panic_json("symbolic execution failed", err); } }; @@ -1425,7 +2176,7 @@ pub extern "C" fn r2sym_explore_to( return sym_error_json("no blocks to explore"); }; - let prepared = match build_symbolic_prepared(blocks.as_slice(), ctx_view.arch) { + let prepared = match build_symbolic_prepared(blocks.as_slice(), ctx_view.arch, None) { Some(prepared) => prepared, None => return sym_error_json("failed to build SSA function"), }; @@ -1433,31 +2184,51 @@ pub extern "C" fn r2sym_explore_to( return sym_error_json("failed to build symbolic scope"); }; let symbol_map = symbol_map_snapshot(); - let query_config = sym_default_query_config(); + let mut query_config = sym_default_query_config(); let z3_ctx = Context::thread_local(); let explore_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - let compiled = r2sym::compile_semantic_artifact_with_scope( + let compiled = r2sym::compile_query_semantic_artifact_with_scope( &z3_ctx, &prepared, Some(&scope), + target_addr, ctx_view.arch, &symbol_map, query_config.summary_profile, ); let mut initial_state = r2sym::SymState::new(&z3_ctx, entry_addr); r2sym::seed_default_state_for_arch(&mut initial_state, &prepared, ctx_view.arch); + let selected_route = predicted_target_query_route(PredictedTargetQueryRouteInput { + z3_ctx: &z3_ctx, + prepared: &prepared, + scope: Some(&scope), + compiled: &compiled, + target_addr, + arch: ctx_view.arch, + symbol_map: &symbol_map, + summary_profile: query_config.summary_profile, + assumption_conflicted: prepared_assumption_conflicted(&prepared), + }); + let query_policy = tune_query_config_for_state( + &mut query_config, + &prepared, + &initial_state, + Some(&selected_route), + ); let mut explorer = query_config.make_explorer(&z3_ctx); - install_symbolic_hooks( + r2sym::install_symbolic_hooks_for_query_policy( &mut explorer, + &z3_ctx, &scope, ctx_view.arch, - &z3_ctx, &symbol_map, query_config.summary_profile, + &query_policy, ); - let reach = explorer.can_reach_with_artifact( + let reach = explorer.can_reach_with_artifact_in_scope( &prepared, + Some(&scope), Some(&compiled), initial_state, target_addr, @@ -1468,18 +2239,45 @@ pub extern "C" fn r2sym_explore_to( .enumerate() .map(|(i, r)| path_info_from_result(i, r, &explorer)) .collect(); - (paths, reach.stats, reach.solver_stats) + ( + paths, + reach.stats, + reach.solver_stats, + reach.assumption_usage, + reach.assumption_conditioned, + reach.summary_conditioned, + reach.selected_route, + compiled.clone(), + ) })); - let (paths, stats, solver_stats) = match explore_result { + let ( + paths, + stats, + solver_stats, + assumption_usage, + assumption_conditioned, + summary_conditioned, + selected_route, + compiled, + ) = match explore_result { Ok(value) => value, - Err(_) => return sym_error_json("symbolic execution failed (z3 context error)"), + Err(err) => return sym_panic_json("symbolic execution failed", err), }; let output = SymTargetExploreResult { entry: format!("0x{:x}", entry_addr), target: format!("0x{:x}", target_addr), matched_paths: paths.len(), - stats: build_sym_exec_summary(&stats, &solver_stats, paths.len(), None), + stats: build_sym_exec_summary( + &stats, + &solver_stats, + paths.len(), + assumption_usage, + assumption_conditioned, + summary_conditioned, + Some(&compiled), + ), + target_query: Some(target_query_info(&selected_route)), paths, }; @@ -1504,7 +2302,7 @@ pub extern "C" fn r2sym_solve_to( return sym_error_json("no blocks to solve"); }; - let prepared = match build_symbolic_prepared(blocks.as_slice(), ctx_view.arch) { + let prepared = match build_symbolic_prepared(blocks.as_slice(), ctx_view.arch, None) { Some(prepared) => prepared, None => return sym_error_json("failed to build SSA function"), }; @@ -1512,31 +2310,51 @@ pub extern "C" fn r2sym_solve_to( return sym_error_json("failed to build symbolic scope"); }; let symbol_map = symbol_map_snapshot(); - let query_config = sym_default_query_config(); + let mut query_config = sym_default_query_config(); let z3_ctx = Context::thread_local(); let solve_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - let compiled = r2sym::compile_semantic_artifact_with_scope( + let compiled = r2sym::compile_query_semantic_artifact_with_scope( &z3_ctx, &prepared, Some(&scope), + target_addr, ctx_view.arch, &symbol_map, query_config.summary_profile, ); let mut initial_state = r2sym::SymState::new(&z3_ctx, entry_addr); r2sym::seed_default_state_for_arch(&mut initial_state, &prepared, ctx_view.arch); + let selected_route = predicted_target_query_route(PredictedTargetQueryRouteInput { + z3_ctx: &z3_ctx, + prepared: &prepared, + scope: Some(&scope), + compiled: &compiled, + target_addr, + arch: ctx_view.arch, + symbol_map: &symbol_map, + summary_profile: query_config.summary_profile, + assumption_conflicted: prepared_assumption_conflicted(&prepared), + }); + let query_policy = tune_query_config_for_state( + &mut query_config, + &prepared, + &initial_state, + Some(&selected_route), + ); let mut explorer = query_config.make_explorer(&z3_ctx); - install_symbolic_hooks( + r2sym::install_symbolic_hooks_for_query_policy( &mut explorer, + &z3_ctx, &scope, ctx_view.arch, - &z3_ctx, &symbol_map, query_config.summary_profile, + &query_policy, ); - let solve = explorer.solve_for_target_with_artifact( + let solve = explorer.solve_for_target_with_artifact_in_scope( &prepared, + Some(&scope), Some(&compiled), initial_state, target_addr, @@ -1544,25 +2362,63 @@ pub extern "C" fn r2sym_solve_to( let selected = solve .selected_path_index .and_then(|idx| solve.matched_paths.get(idx).map(|path| (idx, path))) - .map(|(idx, path)| path_info_from_result(idx, path, &explorer)); + .map(|(idx, path)| { + path_info_from_result_with_solution(idx, path, &explorer, solve_found(solve.status)) + }); ( + solve.status, solve.matched_paths.len(), selected, + solve.verification, + solve.witness, solve.stats, solve.solver_stats, + solve.assumption_usage, + solve.assumption_conditioned, + solve.summary_conditioned, + solve.selected_route, + compiled.clone(), ) })); - let (matched_paths, selected_path, stats, solver_stats) = match solve_result { + let ( + status, + matched_paths, + selected_path, + verification, + witness, + stats, + solver_stats, + assumption_usage, + assumption_conditioned, + summary_conditioned, + selected_route, + compiled, + ) = match solve_result { Ok(value) => value, - Err(_) => return sym_error_json("symbolic execution failed (z3 context error)"), + Err(err) => return sym_panic_json("symbolic execution failed", err), }; let output = SymTargetSolveResult { entry: format!("0x{:x}", entry_addr), target: format!("0x{:x}", target_addr), + status: solve_status_string(status), matched_paths, - found: selected_path.is_some(), - stats: build_sym_exec_summary(&stats, &solver_stats, matched_paths, None), + found: solve_found(status), + target_reached_under_model: verification.target_reached_under_model, + candidate_solution_verified: verification.candidate_solution_verified, + residual_reasons: verification.residual_reasons.clone(), + model_validation: model_validation_info(&verification.model_validation), + witness: solve_witness_info(&witness), + stats: build_sym_exec_summary( + &stats, + &solver_stats, + matched_paths, + assumption_usage, + assumption_conditioned, + summary_conditioned, + Some(&compiled), + ), + target_query: Some(target_query_info(&selected_route)), selected_path, }; @@ -1604,7 +2460,7 @@ pub extern "C" fn r2sym_run_spec_json( return sym_error_json("no blocks to explore"); }; - let prepared = match build_symbolic_prepared(blocks.as_slice(), ctx_view.arch) { + let prepared = match build_symbolic_prepared(blocks.as_slice(), ctx_view.arch, None) { Some(prepared) => prepared, None => return sym_error_json("failed to build SSA function"), }; @@ -1613,25 +2469,28 @@ pub extern "C" fn r2sym_run_spec_json( }; let symbol_map = symbol_map_snapshot(); let z3_ctx = Context::thread_local(); - let default_config = sym_default_query_config(); + let mut default_config = sym_default_query_config(); let run_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let start_pc = spec.start_pc(entry_addr)?; let mut initial_state = r2sym::SymState::new(&z3_ctx, start_pc); r2sym::seed_default_state_for_arch(&mut initial_state, &prepared, ctx_view.arch); spec.apply_to_state(&mut initial_state); + let query_policy = + tune_query_config_for_state(&mut default_config, &prepared, &initial_state, None); let mut explorer = r2sym::PathExplorer::with_config( &z3_ctx, spec.to_explore_config(&default_config.explore), ); - install_symbolic_hooks( + r2sym::install_symbolic_hooks_for_query_policy( &mut explorer, + &z3_ctx, &scope, ctx_view.arch, - &z3_ctx, &symbol_map, default_config.summary_profile, + &query_policy, ); let result = explorer.run_spec(&prepared, initial_state, &spec)?; let stats = explorer.stats().clone(); @@ -1648,13 +2507,21 @@ pub extern "C" fn r2sym_run_spec_json( let (result, stats, solver_stats, found_paths) = match run_result { Ok(Ok(value)) => value, Ok(Err(err)) => return sym_error_json(&err), - Err(_) => return sym_error_json("symbolic execution failed (z3 context error)"), + Err(err) => return sym_panic_json("symbolic execution failed", err), }; let output = SymRunResult { entry: format!("0x{:x}", entry_addr), spec, - stats: build_sym_exec_summary(&stats, &solver_stats, found_paths.len(), None), + stats: build_sym_exec_summary( + &stats, + &solver_stats, + found_paths.len(), + r2ssa::AssumptionUsageReport::default(), + false, + false, + None, + ), stash_counts: SymRunStashCounts { found: found_paths.len(), avoided: result.avoided_states, @@ -1678,6 +2545,7 @@ pub extern "C" fn r2sym_function_scope( functions: *const R2ILFunctionBlocks, num_functions: usize, entry_addr: u64, + external_context_json: *const c_char, ) -> *mut c_char { let Some(ctx_view) = require_ctx_view(ctx) else { return ptr::null_mut(); @@ -1687,12 +2555,18 @@ pub extern "C" fn r2sym_function_scope( }) else { return ptr::null_mut(); }; + let scope = match scope_with_external_assumptions(&scope, ctx_view.arch, external_context_json) + { + Ok(scope) => scope, + Err(err) => return sym_error_json(err), + }; let Some(prepared) = scope_root_prepared(&scope) else { return ptr::null_mut(); }; + let (assumption_usage, assumption_conditioned) = prepared_assumption_conditioning(prepared); let symbol_map = symbol_map_snapshot(); let z3_ctx = Context::thread_local(); - let query_config = sym_paths_query_config(prepared); + let mut query_config = sym_paths_query_config(prepared); let explore_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let compiled = r2sym::compile_semantic_artifact_with_scope( @@ -1707,15 +2581,18 @@ pub extern "C" fn r2sym_function_scope( return (empty_symbolic_summary(), compiled); } let mut initial_state = r2sym::SymState::new(&z3_ctx, entry_addr); - r2sym::seed_default_state_for_arch(&mut initial_state, prepared, ctx_view.arch); + r2sym::seed_scope_state_for_arch(&mut initial_state, prepared, &scope, ctx_view.arch); + let query_policy = + tune_query_config_for_state(&mut query_config, prepared, &initial_state, None); let mut explorer = query_config.make_explorer(&z3_ctx); - install_symbolic_hooks( + r2sym::install_symbolic_hooks_for_query_policy( &mut explorer, + &z3_ctx, &scope, ctx_view.arch, - &z3_ctx, &symbol_map, query_config.summary_profile, + &query_policy, ); ( explorer.summarize_function(prepared, initial_state), @@ -1725,9 +2602,8 @@ pub extern "C" fn r2sym_function_scope( let (summary, compiled) = match explore_result { Ok(r) => r, - Err(_) => { - let error_msg = r#"{"error": "symbolic execution failed (z3 context error)"}"#; - return CString::new(error_msg).map_or(ptr::null_mut(), |c| c.into_raw()); + Err(err) => { + return sym_panic_json("symbolic execution failed", err); } }; @@ -1735,6 +2611,9 @@ pub extern "C" fn r2sym_function_scope( &summary.stats, &summary.solver_stats, summary.feasible_paths, + assumption_usage, + assumption_conditioned, + false, Some(&compiled), ); match serde_json::to_string_pretty(&output) { @@ -1749,6 +2628,7 @@ pub extern "C" fn r2sym_compile_semantics_scope( functions: *const R2ILFunctionBlocks, num_functions: usize, entry_addr: u64, + external_context_json: *const c_char, ) -> *mut c_char { let Some(ctx_view) = require_ctx_view(ctx) else { return sym_error_json("missing disassembler context"); @@ -1758,6 +2638,11 @@ pub extern "C" fn r2sym_compile_semantics_scope( }) else { return sym_error_json("failed to build symbolic scope"); }; + let scope = match scope_with_external_assumptions(&scope, ctx_view.arch, external_context_json) + { + Ok(scope) => scope, + Err(err) => return sym_error_json(err), + }; let Some(prepared) = scope_root_prepared(&scope) else { return sym_error_json("failed to build root SSA function"); }; @@ -1784,6 +2669,7 @@ pub extern "C" fn r2sym_paths_scope( functions: *const R2ILFunctionBlocks, num_functions: usize, entry_addr: u64, + external_context_json: *const c_char, ) -> *mut c_char { let Some(ctx_view) = require_ctx_view(ctx) else { return ptr::null_mut(); @@ -1793,24 +2679,32 @@ pub extern "C" fn r2sym_paths_scope( }) else { return ptr::null_mut(); }; + let scope = match scope_with_external_assumptions(&scope, ctx_view.arch, external_context_json) + { + Ok(scope) => scope, + Err(err) => return sym_error_json(err), + }; let Some(prepared) = scope_root_prepared(&scope) else { return ptr::null_mut(); }; let symbol_map = symbol_map_snapshot(); let z3_ctx = Context::thread_local(); - let query_config = sym_paths_query_config(prepared); + let mut query_config = sym_paths_query_config(prepared); let explore_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let mut initial_state = r2sym::SymState::new(&z3_ctx, entry_addr); - r2sym::seed_default_state_for_arch(&mut initial_state, prepared, ctx_view.arch); + r2sym::seed_scope_state_for_arch(&mut initial_state, prepared, &scope, ctx_view.arch); + let query_policy = + tune_query_config_for_state(&mut query_config, prepared, &initial_state, None); let mut explorer = query_config.make_explorer(&z3_ctx); - install_symbolic_hooks( + r2sym::install_symbolic_hooks_for_query_policy( &mut explorer, + &z3_ctx, &scope, ctx_view.arch, - &z3_ctx, &symbol_map, query_config.summary_profile, + &query_policy, ); let summary = explorer.summarize_function(prepared, initial_state); (summary, explorer) @@ -1818,9 +2712,8 @@ pub extern "C" fn r2sym_paths_scope( let (summary, explorer) = match explore_result { Ok(r) => r, - Err(_) => { - let error_msg = r#"[{"error": "symbolic execution failed (z3 context error)"}]"#; - return CString::new(error_msg).map_or(ptr::null_mut(), |c| c.into_raw()); + Err(err) => { + return sym_panic_json("symbolic execution failed", err); } }; @@ -1844,6 +2737,7 @@ pub extern "C" fn r2sym_explore_to_scope( num_functions: usize, entry_addr: u64, target_addr: u64, + external_context_json: *const c_char, ) -> *mut c_char { let Some(ctx_view) = require_ctx_view(ctx) else { return sym_error_json("missing disassembler context"); @@ -1853,53 +2747,109 @@ pub extern "C" fn r2sym_explore_to_scope( }) else { return sym_error_json("failed to build symbolic scope"); }; + let scope = match scope_with_external_assumptions(&scope, ctx_view.arch, external_context_json) + { + Ok(scope) => scope, + Err(err) => return sym_error_json(err), + }; let Some(prepared) = scope_root_prepared(&scope) else { return sym_error_json("failed to build root SSA function"); }; let symbol_map = symbol_map_snapshot(); - let query_config = sym_default_query_config(); + let mut query_config = sym_default_query_config(); let z3_ctx = Context::thread_local(); let explore_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - let compiled = r2sym::compile_semantic_artifact_with_scope( + let compiled = r2sym::compile_query_semantic_artifact_with_scope( &z3_ctx, prepared, Some(&scope), + target_addr, ctx_view.arch, &symbol_map, query_config.summary_profile, ); let mut initial_state = r2sym::SymState::new(&z3_ctx, entry_addr); - r2sym::seed_default_state_for_arch(&mut initial_state, prepared, ctx_view.arch); + r2sym::seed_scope_state_for_arch(&mut initial_state, prepared, &scope, ctx_view.arch); + let selected_route = predicted_target_query_route(PredictedTargetQueryRouteInput { + z3_ctx: &z3_ctx, + prepared, + scope: Some(&scope), + compiled: &compiled, + target_addr, + arch: ctx_view.arch, + symbol_map: &symbol_map, + summary_profile: query_config.summary_profile, + assumption_conflicted: prepared_assumption_conflicted(prepared), + }); + let query_policy = tune_query_config_for_state( + &mut query_config, + prepared, + &initial_state, + Some(&selected_route), + ); let mut explorer = query_config.make_explorer(&z3_ctx); - install_symbolic_hooks( + r2sym::install_symbolic_hooks_for_query_policy( &mut explorer, + &z3_ctx, &scope, ctx_view.arch, - &z3_ctx, &symbol_map, query_config.summary_profile, + &query_policy, + ); + let reach = explorer.can_reach_with_artifact_in_scope( + prepared, + Some(&scope), + Some(&compiled), + initial_state, + target_addr, ); - let reach = - explorer.can_reach_with_artifact(prepared, Some(&compiled), initial_state, target_addr); let paths: Vec = reach .paths .iter() .enumerate() .map(|(i, r)| path_info_from_result(i, r, &explorer)) .collect(); - (paths, reach.stats, reach.solver_stats) + ( + paths, + reach.stats, + reach.solver_stats, + reach.assumption_usage, + reach.assumption_conditioned, + reach.summary_conditioned, + reach.selected_route, + compiled.clone(), + ) })); - let (paths, stats, solver_stats) = match explore_result { + let ( + paths, + stats, + solver_stats, + assumption_usage, + assumption_conditioned, + summary_conditioned, + selected_route, + compiled, + ) = match explore_result { Ok(value) => value, - Err(_) => return sym_error_json("symbolic execution failed (z3 context error)"), + Err(err) => return sym_panic_json("symbolic execution failed", err), }; let output = SymTargetExploreResult { entry: format!("0x{:x}", entry_addr), target: format!("0x{:x}", target_addr), matched_paths: paths.len(), - stats: build_sym_exec_summary(&stats, &solver_stats, paths.len(), None), + stats: build_sym_exec_summary( + &stats, + &solver_stats, + paths.len(), + assumption_usage, + assumption_conditioned, + summary_conditioned, + Some(&compiled), + ), + target_query: Some(target_query_info(&selected_route)), paths, }; @@ -1916,70 +2866,174 @@ pub extern "C" fn r2sym_solve_to_scope( num_functions: usize, entry_addr: u64, target_addr: u64, + external_context_json: *const c_char, ) -> *mut c_char { + solve_pipeline_debug_stage("solve_to_scope:start"); let Some(ctx_view) = require_ctx_view(ctx) else { return sym_error_json("missing disassembler context"); }; + solve_pipeline_debug_stage("solve_to_scope:ctx_ready"); let Some(scope) = (unsafe { build_symbolic_scope_from_ffi(functions, num_functions, ctx_view.arch, entry_addr) }) else { return sym_error_json("failed to build symbolic scope"); }; + solve_pipeline_debug_log(&format!( + "solve_to_scope:scope_ready functions={}", + scope.functions().len() + )); + let scope = match scope_with_external_assumptions(&scope, ctx_view.arch, external_context_json) + { + Ok(scope) => scope, + Err(err) => return sym_error_json(err), + }; + solve_pipeline_debug_stage("solve_to_scope:assumptions_ready"); let Some(prepared) = scope_root_prepared(&scope) else { return sym_error_json("failed to build root SSA function"); }; + solve_pipeline_debug_log(&format!( + "solve_to_scope:prepared_ready blocks={} call_sites={}", + prepared.blocks().count(), + prepared.call_sites().by_id.len() + )); let symbol_map = symbol_map_snapshot(); - let query_config = sym_default_query_config(); + let mut query_config = sym_default_query_config(); let z3_ctx = Context::thread_local(); + solve_pipeline_debug_log(&format!( + "solve_to_scope:compile_begin target={:#x}", + target_addr + )); let solve_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - let compiled = r2sym::compile_semantic_artifact_with_scope( + let compiled = r2sym::compile_query_semantic_artifact_with_scope( &z3_ctx, prepared, Some(&scope), + target_addr, ctx_view.arch, &symbol_map, query_config.summary_profile, ); + solve_pipeline_debug_log(&format!( + "solve_to_scope:compile_done stage={:?} route={:?}", + compiled.stage, + compiled.target_query_route_plan(target_addr) + )); let mut initial_state = r2sym::SymState::new(&z3_ctx, entry_addr); - r2sym::seed_default_state_for_arch(&mut initial_state, prepared, ctx_view.arch); + r2sym::seed_scope_state_for_arch(&mut initial_state, prepared, &scope, ctx_view.arch); + solve_pipeline_debug_stage("solve_to_scope:seed_ready"); + let selected_route = predicted_target_query_route(PredictedTargetQueryRouteInput { + z3_ctx: &z3_ctx, + prepared, + scope: Some(&scope), + compiled: &compiled, + target_addr, + arch: ctx_view.arch, + symbol_map: &symbol_map, + summary_profile: query_config.summary_profile, + assumption_conflicted: prepared_assumption_conflicted(prepared), + }); + let query_policy = tune_query_config_for_state( + &mut query_config, + prepared, + &initial_state, + Some(&selected_route), + ); + solve_pipeline_debug_log(&format!( + "solve_to_scope:query_config max_states={} max_depth={} timeout_ms={}", + query_config.explore.max_states, + query_config.explore.max_depth, + query_config + .explore + .timeout + .map(|timeout| timeout.as_millis()) + .unwrap_or(0) + )); let mut explorer = query_config.make_explorer(&z3_ctx); - install_symbolic_hooks( + r2sym::install_symbolic_hooks_for_query_policy( &mut explorer, + &z3_ctx, &scope, ctx_view.arch, - &z3_ctx, &symbol_map, query_config.summary_profile, + &query_policy, ); - let solve = explorer.solve_for_target_with_artifact( + solve_pipeline_debug_stage("solve_to_scope:hooks_ready"); + let solve = explorer.solve_for_target_with_artifact_in_scope( prepared, + Some(&scope), Some(&compiled), initial_state, target_addr, ); + solve_pipeline_debug_log(&format!( + "solve_to_scope:solve_done status={:?} matched={} explored={} route={:?}", + solve.status, + solve.matched_paths.len(), + solve.stats.states_explored, + solve.selected_route + )); let selected = solve .selected_path_index .and_then(|idx| solve.matched_paths.get(idx).map(|path| (idx, path))) - .map(|(idx, path)| path_info_from_result(idx, path, &explorer)); + .map(|(idx, path)| { + path_info_from_result_with_solution(idx, path, &explorer, solve_found(solve.status)) + }); ( + solve.status, solve.matched_paths.len(), selected, + solve.verification, + solve.witness, solve.stats, solve.solver_stats, + solve.assumption_usage, + solve.assumption_conditioned, + solve.summary_conditioned, + solve.selected_route, + compiled.clone(), ) })); - let (matched_paths, selected_path, stats, solver_stats) = match solve_result { + let ( + status, + matched_paths, + selected_path, + verification, + witness, + stats, + solver_stats, + assumption_usage, + assumption_conditioned, + summary_conditioned, + selected_route, + compiled, + ) = match solve_result { Ok(value) => value, - Err(_) => return sym_error_json("symbolic execution failed (z3 context error)"), + Err(err) => return sym_panic_json("symbolic execution failed", err), }; let output = SymTargetSolveResult { entry: format!("0x{:x}", entry_addr), target: format!("0x{:x}", target_addr), + status: solve_status_string(status), matched_paths, - found: selected_path.is_some(), - stats: build_sym_exec_summary(&stats, &solver_stats, matched_paths, None), + found: solve_found(status), + target_reached_under_model: verification.target_reached_under_model, + candidate_solution_verified: verification.candidate_solution_verified, + residual_reasons: verification.residual_reasons.clone(), + model_validation: model_validation_info(&verification.model_validation), + witness: solve_witness_info(&witness), + stats: build_sym_exec_summary( + &stats, + &solver_stats, + matched_paths, + assumption_usage, + assumption_conditioned, + summary_conditioned, + Some(&compiled), + ), + target_query: Some(target_query_info(&selected_route)), selected_path, }; @@ -1996,6 +3050,7 @@ pub extern "C" fn r2sym_run_spec_json_scope( num_functions: usize, entry_addr: u64, spec_json: *const c_char, + external_context_json: *const c_char, ) -> *mut c_char { if spec_json.is_null() { return sym_error_json("missing exploration spec json"); @@ -2022,30 +3077,39 @@ pub extern "C" fn r2sym_run_spec_json_scope( }) else { return sym_error_json("failed to build symbolic scope"); }; + let scope = match scope_with_external_assumptions(&scope, ctx_view.arch, external_context_json) + { + Ok(scope) => scope, + Err(err) => return sym_error_json(err), + }; let Some(prepared) = scope_root_prepared(&scope) else { return sym_error_json("failed to build root SSA function"); }; + let (assumption_usage, assumption_conditioned) = prepared_assumption_conditioning(prepared); let symbol_map = symbol_map_snapshot(); let z3_ctx = Context::thread_local(); - let default_config = sym_default_query_config(); + let mut default_config = sym_default_query_config(); let run_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let start_pc = spec.start_pc(entry_addr)?; let mut initial_state = r2sym::SymState::new(&z3_ctx, start_pc); - r2sym::seed_default_state_for_arch(&mut initial_state, prepared, ctx_view.arch); + r2sym::seed_scope_state_for_arch(&mut initial_state, prepared, &scope, ctx_view.arch); spec.apply_to_state(&mut initial_state); + let query_policy = + tune_query_config_for_state(&mut default_config, prepared, &initial_state, None); let mut explorer = r2sym::PathExplorer::with_config( &z3_ctx, spec.to_explore_config(&default_config.explore), ); - install_symbolic_hooks( + r2sym::install_symbolic_hooks_for_query_policy( &mut explorer, + &z3_ctx, &scope, ctx_view.arch, - &z3_ctx, &symbol_map, default_config.summary_profile, + &query_policy, ); let result = explorer.run_spec(prepared, initial_state, &spec)?; let stats = explorer.stats().clone(); @@ -2062,13 +3126,21 @@ pub extern "C" fn r2sym_run_spec_json_scope( let (result, stats, solver_stats, found_paths) = match run_result { Ok(Ok(value)) => value, Ok(Err(err)) => return sym_error_json(&err), - Err(_) => return sym_error_json("symbolic execution failed (z3 context error)"), + Err(err) => return sym_panic_json("symbolic execution failed", err), }; let output = SymRunResult { entry: format!("0x{:x}", entry_addr), spec, - stats: build_sym_exec_summary(&stats, &solver_stats, found_paths.len(), None), + stats: build_sym_exec_summary( + &stats, + &solver_stats, + found_paths.len(), + assumption_usage, + assumption_conditioned, + false, + None, + ), stash_counts: SymRunStashCounts { found: found_paths.len(), avoided: result.avoided_states, @@ -2094,6 +3166,7 @@ pub extern "C" fn r2sym_explore_to_replay_scope( entry_addr: u64, target_addr: u64, replay_seed: *const R2SymReplaySeed, + external_context_json: *const c_char, ) -> *mut c_char { let Some(ctx_view) = require_ctx_view(ctx) else { return sym_error_json("missing disassembler context"); @@ -2109,54 +3182,115 @@ pub extern "C" fn r2sym_explore_to_replay_scope( }) else { return sym_error_json("failed to build symbolic scope"); }; + let scope = match scope_with_external_assumptions(&scope, ctx_view.arch, external_context_json) + { + Ok(scope) => scope, + Err(err) => return sym_error_json(err), + }; let Some(prepared) = scope_root_prepared(&scope) else { return sym_error_json("failed to build root SSA function"); }; let symbol_map = symbol_map_snapshot(); - let query_config = sym_default_query_config(); + let mut query_config = sym_default_query_config(); let start_pc = replay_seed.entry_pc.unwrap_or(entry_addr); let z3_ctx = Context::thread_local(); let explore_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - let compiled = r2sym::compile_semantic_artifact_with_scope( + let compiled = r2sym::compile_query_semantic_artifact_with_scope( &z3_ctx, prepared, Some(&scope), + target_addr, ctx_view.arch, &symbol_map, query_config.summary_profile, ); let initial_state = build_replay_seeded_state(&z3_ctx, entry_addr, prepared, ctx_view.arch, &replay_seed); + let selected_route = predicted_target_query_route(PredictedTargetQueryRouteInput { + z3_ctx: &z3_ctx, + prepared, + scope: Some(&scope), + compiled: &compiled, + target_addr, + arch: ctx_view.arch, + symbol_map: &symbol_map, + summary_profile: query_config.summary_profile, + assumption_conflicted: prepared_assumption_conflicted(prepared), + }); + let query_policy = tune_query_config_for_state( + &mut query_config, + prepared, + &initial_state, + Some(&selected_route), + ); let mut explorer = query_config.make_explorer(&z3_ctx); - install_symbolic_hooks( + r2sym::install_symbolic_hooks_for_query_policy( &mut explorer, + &z3_ctx, &scope, ctx_view.arch, - &z3_ctx, &symbol_map, query_config.summary_profile, + &query_policy, + ); + let reach = explorer.can_reach_with_artifact_in_scope( + prepared, + Some(&scope), + Some(&compiled), + initial_state, + target_addr, ); - let reach = - explorer.can_reach_with_artifact(prepared, Some(&compiled), initial_state, target_addr); let paths: Vec = reach .paths .iter() .enumerate() .map(|(i, r)| path_info_from_result(i, r, &explorer)) .collect(); - (paths, reach.stats, reach.solver_stats) + ( + paths, + reach.stats, + reach.solver_stats, + reach.assumption_usage, + reach.assumption_conditioned, + reach.summary_conditioned, + reach.selected_route, + compiled.clone(), + ) })); - let (paths, stats, solver_stats) = match explore_result { + let ( + paths, + stats, + solver_stats, + assumption_usage, + assumption_conditioned, + summary_conditioned, + selected_route, + compiled, + ) = match explore_result { Ok(value) => value, - Err(_) => return sym_error_json("symbolic replay exploration failed (z3 context error)"), + Err(err) => { + return sym_panic_json("symbolic replay exploration failed", err); + } }; let output = SymTargetExploreResult { entry: format!("0x{:x}", start_pc), target: format!("0x{:x}", target_addr), matched_paths: paths.len(), - stats: build_sym_exec_summary(&stats, &solver_stats, paths.len(), None), + stats: build_sym_exec_summary_with_semantic_info( + &stats, + &solver_stats, + paths.len(), + assumption_usage, + assumption_conditioned, + summary_conditioned, + Some(compiled_semantic_info_with_replay_seed( + &compiled, + &replay_seed, + )), + ), + target_query: Some(target_query_info(&selected_route)), paths, }; @@ -2174,6 +3308,7 @@ pub extern "C" fn r2sym_solve_to_replay_scope( entry_addr: u64, target_addr: u64, replay_seed: *const R2SymReplaySeed, + external_context_json: *const c_char, ) -> *mut c_char { let Some(ctx_view) = require_ctx_view(ctx) else { return sym_error_json("missing disassembler context"); @@ -2189,36 +3324,62 @@ pub extern "C" fn r2sym_solve_to_replay_scope( }) else { return sym_error_json("failed to build symbolic scope"); }; + let scope = match scope_with_external_assumptions(&scope, ctx_view.arch, external_context_json) + { + Ok(scope) => scope, + Err(err) => return sym_error_json(err), + }; let Some(prepared) = scope_root_prepared(&scope) else { return sym_error_json("failed to build root SSA function"); }; let symbol_map = symbol_map_snapshot(); - let query_config = sym_default_query_config(); + let mut query_config = sym_default_query_config(); let start_pc = replay_seed.entry_pc.unwrap_or(entry_addr); let z3_ctx = Context::thread_local(); let solve_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - let compiled = r2sym::compile_semantic_artifact_with_scope( + let compiled = r2sym::compile_query_semantic_artifact_with_scope_and_replay_seed( &z3_ctx, prepared, Some(&scope), + target_addr, ctx_view.arch, &symbol_map, query_config.summary_profile, + Some(&replay_seed), ); let initial_state = build_replay_seeded_state(&z3_ctx, entry_addr, prepared, ctx_view.arch, &replay_seed); + let selected_route = predicted_target_query_route(PredictedTargetQueryRouteInput { + z3_ctx: &z3_ctx, + prepared, + scope: Some(&scope), + compiled: &compiled, + target_addr, + arch: ctx_view.arch, + symbol_map: &symbol_map, + summary_profile: query_config.summary_profile, + assumption_conflicted: prepared_assumption_conflicted(prepared), + }); + let query_policy = tune_query_config_for_state( + &mut query_config, + prepared, + &initial_state, + Some(&selected_route), + ); let mut explorer = query_config.make_explorer(&z3_ctx); - install_symbolic_hooks( + r2sym::install_symbolic_hooks_for_query_policy( &mut explorer, + &z3_ctx, &scope, ctx_view.arch, - &z3_ctx, &symbol_map, query_config.summary_profile, + &query_policy, ); - let solve = explorer.solve_for_target_with_artifact( + let solve = explorer.solve_for_target_with_artifact_in_scope( prepared, + Some(&scope), Some(&compiled), initial_state, target_addr, @@ -2226,25 +3387,66 @@ pub extern "C" fn r2sym_solve_to_replay_scope( let selected = solve .selected_path_index .and_then(|idx| solve.matched_paths.get(idx).map(|path| (idx, path))) - .map(|(idx, path)| path_info_from_result(idx, path, &explorer)); + .map(|(idx, path)| { + path_info_from_result_with_solution(idx, path, &explorer, solve_found(solve.status)) + }); ( + solve.status, solve.matched_paths.len(), selected, + solve.verification, + solve.witness, solve.stats, solve.solver_stats, + solve.assumption_usage, + solve.assumption_conditioned, + solve.summary_conditioned, + solve.selected_route, + compiled.clone(), ) })); - let (matched_paths, selected_path, stats, solver_stats) = match solve_result { + let ( + status, + matched_paths, + selected_path, + verification, + witness, + stats, + solver_stats, + assumption_usage, + assumption_conditioned, + summary_conditioned, + selected_route, + compiled, + ) = match solve_result { Ok(value) => value, - Err(_) => return sym_error_json("symbolic replay solve failed (z3 context error)"), + Err(err) => return sym_panic_json("symbolic replay solve failed", err), }; let output = SymTargetSolveResult { entry: format!("0x{:x}", start_pc), target: format!("0x{:x}", target_addr), + status: solve_status_string(status), matched_paths, - found: selected_path.is_some(), - stats: build_sym_exec_summary(&stats, &solver_stats, matched_paths, None), + found: solve_found(status), + target_reached_under_model: verification.target_reached_under_model, + candidate_solution_verified: verification.candidate_solution_verified, + residual_reasons: verification.residual_reasons.clone(), + model_validation: model_validation_info(&verification.model_validation), + witness: solve_witness_info(&witness), + stats: build_sym_exec_summary_with_semantic_info( + &stats, + &solver_stats, + matched_paths, + assumption_usage, + assumption_conditioned, + summary_conditioned, + Some(compiled_semantic_info_with_replay_seed( + &compiled, + &replay_seed, + )), + ), + target_query: Some(target_query_info(&selected_route)), selected_path, }; @@ -2253,3 +3455,68 @@ pub extern "C" fn r2sym_solve_to_replay_scope( Err(_) => sym_error_json("failed to serialize replay symbolic solve output"), } } + +#[cfg(test)] +mod tests { + use super::{build_sym_exec_summary, parse_scope_assumptions}; + use std::ffi::CString; + + #[test] + fn parse_scope_assumptions_reads_external_context_payload() { + let json = CString::new( + r#"{"assumptions":[{"subject":{"register":{"name":"rdi"}},"value":{"constant":{"value":4660}}}]}"#, + ) + .expect("json"); + let assumptions = parse_scope_assumptions(json.as_ptr(), None).expect("assumptions"); + assert_eq!(assumptions.items.len(), 1); + } + + #[test] + fn sym_exec_summary_serializes_assumption_usage_when_present() { + let summary = build_sym_exec_summary( + &r2sym::path::ExploreStats::default(), + &r2sym::SolverStats::default(), + 0, + r2ssa::AssumptionUsageReport { + applied: vec![r2ssa::AnalysisAssumption { + id: Some("arg0".to_string()), + subject: r2ssa::AssumptionSubject::Parameter { index: 0 }, + value: r2ssa::AssumptionValue::Constant { value: 7 }, + scope: r2ssa::AssumptionScope::Query, + provenance: r2ssa::AssumptionProvenance::User, + }], + ..r2ssa::AssumptionUsageReport::default() + }, + true, + false, + None, + ); + let json = serde_json::to_value(summary).expect("summary json"); + assert!(json.get("assumption_usage").is_some()); + assert_eq!( + json.get("assumption_conditioned") + .and_then(serde_json::Value::as_bool), + Some(true) + ); + assert_eq!(json.get("summary_conditioned"), None); + } + + #[test] + fn sym_exec_summary_serializes_summary_conditioning_when_present() { + let summary = build_sym_exec_summary( + &r2sym::path::ExploreStats::default(), + &r2sym::SolverStats::default(), + 0, + r2ssa::AssumptionUsageReport::default(), + false, + true, + None, + ); + let json = serde_json::to_value(summary).expect("summary json"); + assert_eq!( + json.get("summary_conditioned") + .and_then(serde_json::Value::as_bool), + Some(true) + ); + } +} diff --git a/r2plugin/src/decompiler.rs b/r2plugin/src/decompiler.rs index 7d1736f..2678088 100644 --- a/r2plugin/src/decompiler.rs +++ b/r2plugin/src/decompiler.rs @@ -45,14 +45,12 @@ pub(crate) fn decompiler_input_from_artifact( let FunctionAnalysisArtifact { ssa_func, function_facts, - interproc_summary_set, .. } = artifact; r2dec::DecompilerInput::new( ssa_func, build_decompiler_context(function_facts, function_names, strings, symbols, ptr_bits), ) - .with_interproc_summary_set(interproc_summary_set) } fn rename_function_artifact_for_display( @@ -64,14 +62,13 @@ fn rename_function_artifact_for_display( pattern_ssa_func, function_facts, writeback_plan, - interproc_summary_set, + .. } = artifact; FunctionAnalysisArtifact { ssa_func: ssa_func.with_name(function_name), pattern_ssa_func: pattern_ssa_func.with_name(function_name), function_facts, writeback_plan, - interproc_summary_set, } } @@ -110,7 +107,7 @@ pub(crate) fn run_full_decompile_on_large_stack( if let Some(route) = r2dec::detached_semantic_route_plan( &display_func_name, &r2il_blocks, - artifact.function_facts.semantics.as_ref(), + &artifact.function_facts, ) { if let r2dec::SemanticRoutePlan::FallbackComment { comment } = route { return comment; @@ -137,7 +134,10 @@ pub(crate) fn run_full_decompile_on_large_stack( if let Some(route) = r2dec::detached_semantic_route_plan( &display_func_name, &r2il_blocks, - precomputed_semantic_artifact.as_ref(), + &r2types::FunctionFacts::new( + r2types::FunctionTypeFacts::default(), + precomputed_semantic_artifact.clone(), + ), ) { if let r2dec::SemanticRoutePlan::FallbackComment { comment } = route { return comment; @@ -173,7 +173,7 @@ pub(crate) fn run_full_decompile_on_large_stack( if let Some(route) = r2dec::detached_semantic_route_plan( &display_func_name, &r2il_blocks, - artifact.function_facts.semantics.as_ref(), + &artifact.function_facts, ) { match route { r2dec::SemanticRoutePlan::VmSummary { .. } => { diff --git a/r2plugin/src/lib.rs b/r2plugin/src/lib.rs index 8837a6c..e6d3b2d 100644 --- a/r2plugin/src/lib.rs +++ b/r2plugin/src/lib.rs @@ -26,6 +26,8 @@ use r2sleigh_export::{ }; use r2sleigh_lift::{Disassembler, SemanticMetadataOptions, build_arch_spec, userop_map_for_arch}; use std::ffi::{CStr, CString}; +use std::fs::OpenOptions; +use std::io::Write; use std::os::raw::c_char; use std::path::Path; use std::ptr; @@ -3349,6 +3351,11 @@ struct InferredTypeWritebackJson { struct_decls: Vec, global_type_links: Vec, interproc: InterprocSummaryJson, + plans: r2types::AnalysisPlans, + #[serde(skip_serializing_if = "r2ssa::AssumptionSet::is_empty")] + assumptions: r2ssa::AssumptionSet, + #[serde(skip_serializing_if = "r2types::AssumptionUsageReport::is_empty")] + assumption_usage: r2types::AssumptionUsageReport, #[serde(skip_serializing_if = "Option::is_none")] semantics: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -3381,8 +3388,15 @@ struct FunctionFactsReportJson { cfg_risk: CfgRiskSummaryJson, #[serde(skip_serializing_if = "Option::is_none")] semantic_build_plan: Option, + plans: r2types::AnalysisPlans, #[serde(skip_serializing_if = "Option::is_none")] semantic_route: Option, + #[serde(skip_serializing_if = "r2ssa::AssumptionSet::is_empty")] + assumptions: r2ssa::AssumptionSet, + #[serde(skip_serializing_if = "r2types::AssumptionUsageReport::is_empty")] + assumption_usage: r2types::AssumptionUsageReport, + #[serde(skip_serializing_if = "Option::is_none")] + summary_diagnostics: Option, type_writeback: InferredTypeWritebackJson, } @@ -3395,8 +3409,15 @@ struct FunctionPlanReportJson { semantic: Option, #[serde(skip_serializing_if = "Option::is_none")] semantic_build_plan: Option, + plans: r2types::AnalysisPlans, #[serde(skip_serializing_if = "Option::is_none")] semantic_route: Option, + #[serde(skip_serializing_if = "r2ssa::AssumptionSet::is_empty")] + assumptions: r2ssa::AssumptionSet, + #[serde(skip_serializing_if = "r2types::AssumptionUsageReport::is_empty")] + assumption_usage: r2types::AssumptionUsageReport, + #[serde(skip_serializing_if = "Option::is_none")] + summary_diagnostics: Option, prefer_bounded_type_plan: bool, } @@ -3462,6 +3483,7 @@ fn struct_fields_json(fields: &[r2types::StructFieldCandidate]) -> Vec, compiled_semantics: Option, ) -> InferredTypeWritebackJson { @@ -3531,6 +3553,9 @@ fn writeback_plan_json( }) .collect(), interproc, + plans: function_facts.plans.clone(), + assumptions: function_facts.assumptions.clone(), + assumption_usage: function_facts.assumption_usage.clone(), semantics, compiled_semantics, diagnostics: TypeWritebackDiagnosticsJson { @@ -3541,21 +3566,104 @@ fn writeback_plan_json( } } +fn parse_nonempty_scope_json(payload: &str) -> Option { + serde_json::from_str::(payload) + .ok() + .filter(|value| { + !value.is_null() + && value + .as_object() + .map(|object| !object.is_empty()) + .unwrap_or(true) + }) +} + +fn symbolic_scope_view_json( + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, +) -> Option { + let scope = symbolic_scope?; + let payloads = scope + .helper_functions() + .filter_map(|function| { + function.name.as_ref().map(|name| { + serde_json::json!({ + "function_addr": function.id.0, + "function_name": name, + }) + }) + }) + .collect::>(); + let seeds = scope + .helper_functions() + .filter_map(|function| { + function.name.as_ref().map(|name| { + serde_json::json!({ + "id": function.id.0, + "name": name, + }) + }) + }) + .collect::>(); + if seeds.is_empty() && payloads.is_empty() { + return None; + } + Some(serde_json::json!({ + "phase": "symbolic_scope", + "payloads": payloads, + "seeds": seeds, + })) +} + +fn merged_interproc_scope_json( + scope_json: &str, + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, +) -> Option { + let Some(symbolic_scope_json) = symbolic_scope_view_json(symbolic_scope) else { + return parse_nonempty_scope_json(scope_json); + }; + let Some(mut merged) = parse_nonempty_scope_json(scope_json) else { + return Some(symbolic_scope_json); + }; + let (Some(merged_obj), Some(symbolic_obj)) = + (merged.as_object_mut(), symbolic_scope_json.as_object()) + else { + return Some(merged); + }; + + if !merged_obj.contains_key("phase") + && let Some(phase) = symbolic_obj.get("phase") + { + merged_obj.insert("phase".to_string(), phase.clone()); + } + for key in ["payloads", "seeds"] { + let Some(serde_json::Value::Array(symbolic_items)) = symbolic_obj.get(key) else { + continue; + }; + let entry = merged_obj + .entry(key.to_string()) + .or_insert_with(|| serde_json::Value::Array(Vec::new())); + if let serde_json::Value::Array(items) = entry { + items.extend(symbolic_items.iter().cloned()); + } + } + + Some(merged) +} + fn type_writeback_payload_from_artifact( artifact: types::FunctionAnalysisArtifact, interproc: InterprocInferenceInput<'_>, + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, ) -> InferredTypeWritebackJson { let semantics = artifact.function_facts.semantics.clone(); let compiled_semantics = semantics .as_ref() .map(analysis::sym::compiled_semantic_info); let ssa_blocks = artifact.pattern_ssa_func.local_ssa_blocks(); - let scope = serde_json::from_str::(interproc.scope_json) - .ok() - .filter(|v| !v.is_null() && v.as_object().map(|obj| !obj.is_empty()).unwrap_or(true)); + let scope = merged_interproc_scope_json(interproc.scope_json, symbolic_scope); let current_summary = artifact - .interproc_summary_set - .as_ref() + .function_facts + .interproc_summary_set() .and_then(|summary_set| { summary_set .root @@ -3576,6 +3684,7 @@ fn type_writeback_payload_from_artifact( summary_json: current_summary_json, scope, }, + &artifact.function_facts, semantics, compiled_semantics, ) @@ -3587,6 +3696,8 @@ fn semantic_type_fallback_payload( ptr_bits: u32, interproc: InterprocInferenceInput<'_>, compiled: &r2sym::SemanticArtifact, + function_facts: &r2types::FunctionFacts, + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, ) -> InferredTypeWritebackJson { let compiled_info = analysis::sym::compiled_semantic_info(compiled); let plan = @@ -3639,12 +3750,11 @@ fn semantic_type_fallback_payload( converged: interproc.converged, summary: None, summary_json: None, - scope: serde_json::from_str::(interproc.scope_json) - .ok() - .filter(|v| { - !v.is_null() && v.as_object().map(|obj| !obj.is_empty()).unwrap_or(true) - }), + scope: merged_interproc_scope_json(interproc.scope_json, symbolic_scope), }, + plans: function_facts.plans.clone(), + assumptions: function_facts.assumptions.clone(), + assumption_usage: function_facts.assumption_usage.clone(), semantics: Some(compiled.clone()), compiled_semantics: Some(compiled_info), diagnostics: TypeWritebackDiagnosticsJson { @@ -3654,6 +3764,17 @@ fn semantic_type_fallback_payload( } } +fn semantic_fallback_function_facts( + compiled: &r2sym::SemanticArtifact, + assumptions: &r2ssa::AssumptionSet, +) -> r2types::FunctionFacts { + r2types::FunctionFacts::new( + r2types::FunctionTypeFacts::default(), + Some(compiled.clone()), + ) + .with_assumptions(assumptions.clone()) +} + #[cfg(test)] const SIG_WRITEBACK_CONFIDENCE_MIN: u8 = 70; #[cfg(test)] @@ -5484,6 +5605,144 @@ fn direct_call_targets_from_analysis(analysis: &types::FunctionAnalysis) -> Vec< targets.into_iter().collect() } +fn runtime_registration_targets_from_analysis( + analysis: &types::FunctionAnalysis, + arch: Option<&ArchSpec>, + registration_call_targets: &[u64], +) -> Vec { + fn debug_runtime_targets_enabled() -> bool { + std::env::var_os("R2SLEIGH_DEBUG_RUNTIME_TARGETS").is_some() + } + + fn debug_runtime_targets_log(message: &str) { + if !debug_runtime_targets_enabled() { + return; + } + let path = std::env::var("R2SLEIGH_DEBUG_RUNTIME_TARGETS_LOG") + .unwrap_or_else(|_| "/tmp/r2sleigh_runtime_targets.log".to_string()); + if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) { + let _ = writeln!(file, "{message}"); + } + } + + let lower_arch = arch.map(|arch| arch.name.to_ascii_lowercase()); + let supports_windows_x64 = arch.is_some_and(|arch| { + lower_arch.as_deref().is_some_and(|name| { + (name.contains("x86") || matches!(name, "x64" | "amd64")) + && (arch.addr_size == 8 || name.contains("64")) + }) + }); + if !supports_windows_x64 || registration_call_targets.is_empty() { + debug_runtime_targets_log(&format!( + "skip supports_windows_x64={} arch={:?} registrations={}", + supports_windows_x64, + lower_arch.as_deref(), + registration_call_targets.len() + )); + return Vec::new(); + } + + let registrations = registration_call_targets + .iter() + .copied() + .collect::>(); + let observations = + r2ssa::observe_call_arguments(&analysis.ssa_func, &r2ssa::AbiProfile::windows_x64()); + let mut targets = std::collections::BTreeSet::new(); + for (call_id, call) in &analysis.ssa_func.call_sites().by_id { + let Some(target) = analysis.ssa_func.resolved_call_target(call) else { + debug_runtime_targets_log(&format!("call_id={call_id:?} unresolved_target")); + continue; + }; + if !registrations.contains(&target) { + debug_runtime_targets_log(&format!( + "call_id={call_id:?} target=0x{target:x} not_registration" + )); + continue; + } + let Some(args) = observations.get(call_id) else { + debug_runtime_targets_log(&format!( + "call_id={call_id:?} target=0x{target:x} missing_args" + )); + continue; + }; + let Some(r2ssa::CallArgObservation::Const(handler)) = args.get(1) else { + debug_runtime_targets_log(&format!( + "call_id={call_id:?} target=0x{target:x} handler_arg={:?}", + args.get(1) + )); + continue; + }; + if *handler >= 0x1000 { + debug_runtime_targets_log(&format!( + "call_id={call_id:?} target=0x{target:x} handler=0x{handler:x}" + )); + targets.insert(*handler); + } + } + targets.into_iter().collect() +} + +#[derive(Serialize)] +struct RuntimeMaterializedSourceJson { + addr: u64, + size: u64, +} + +fn runtime_materialized_sources_from_analysis( + analysis: &types::FunctionAnalysis, + arch: Option<&ArchSpec>, + copy_call_targets: &[u64], +) -> Vec { + let lower_arch = arch.map(|arch| arch.name.to_ascii_lowercase()); + let supports_windows_x64 = arch.is_some_and(|arch| { + lower_arch.as_deref().is_some_and(|name| { + (name.contains("x86") || matches!(name, "x64" | "amd64")) + && (arch.addr_size == 8 || name.contains("64")) + }) + }); + if !supports_windows_x64 || copy_call_targets.is_empty() { + return Vec::new(); + } + + let copy_targets = copy_call_targets + .iter() + .copied() + .collect::>(); + let observations = + r2ssa::observe_call_arguments(&analysis.ssa_func, &r2ssa::AbiProfile::windows_x64()); + let mut sources = std::collections::BTreeMap::::new(); + for (call_id, call) in &analysis.ssa_func.call_sites().by_id { + let Some(target) = analysis.ssa_func.resolved_call_target(call) else { + continue; + }; + if !copy_targets.contains(&target) { + continue; + } + let Some(args) = observations.get(call_id) else { + continue; + }; + let ( + Some(r2ssa::CallArgObservation::Const(source)), + Some(r2ssa::CallArgObservation::Const(size)), + ) = (args.get(1), args.get(2)) + else { + continue; + }; + if *source >= 0x1000 && *size > 0 { + sources + .entry(*source) + .and_modify(|existing| *existing = (*existing).max(*size)) + .or_insert(*size); + } + } + + sources + .into_iter() + .map(|(addr, size)| RuntimeMaterializedSourceJson { addr, size }) + .collect() +} + #[unsafe(no_mangle)] pub extern "C" fn r2sleigh_get_direct_call_targets_json( ctx: *const R2ILContext, @@ -5506,6 +5765,65 @@ pub extern "C" fn r2sleigh_get_direct_call_targets_json( } } +#[unsafe(no_mangle)] +pub extern "C" fn r2sleigh_get_symbolic_scope_targets_json( + ctx: *const R2ILContext, + blocks: *const *const R2ILBlock, + num_blocks: usize, + fcn_addr: u64, + fcn_name: *const c_char, + registration_call_targets_json: *const c_char, +) -> *mut c_char { + let Some(input) = types::build_function_input(ctx, blocks, num_blocks, fcn_addr, fcn_name) + else { + return ptr::null_mut(); + }; + let Some(analysis) = types::build_function_analysis(&input) else { + return ptr::null_mut(); + }; + let registration_call_targets: Vec = serde_json::from_str(&helpers::cstr_or_default( + registration_call_targets_json, + "[]", + )) + .unwrap_or_default(); + let payload = runtime_registration_targets_from_analysis( + &analysis, + input.ctx.arch, + ®istration_call_targets, + ); + match serde_json::to_string(&payload) { + Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), + Err(_) => ptr::null_mut(), + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn r2sleigh_get_runtime_materialized_sources_json( + ctx: *const R2ILContext, + blocks: *const *const R2ILBlock, + num_blocks: usize, + fcn_addr: u64, + fcn_name: *const c_char, + copy_call_targets_json: *const c_char, +) -> *mut c_char { + let Some(input) = types::build_function_input(ctx, blocks, num_blocks, fcn_addr, fcn_name) + else { + return ptr::null_mut(); + }; + let Some(analysis) = types::build_function_analysis(&input) else { + return ptr::null_mut(); + }; + let copy_call_targets: Vec = + serde_json::from_str(&helpers::cstr_or_default(copy_call_targets_json, "[]")) + .unwrap_or_default(); + let payload = + runtime_materialized_sources_from_analysis(&analysis, input.ctx.arch, ©_call_targets); + match serde_json::to_string(&payload) { + Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), + Err(_) => ptr::null_mut(), + } +} + /// Infer full type write-back payload (signature + per-variable + structs + globals). /// /// Returns JSON suitable for plugin-side confidence/conflict policy. @@ -5544,18 +5862,22 @@ struct SemanticWorkerLinearizationInput { scope_num_functions: usize, } -fn infer_type_writeback_json_impl(input: TypeWritebackInferenceInput<'_>) -> *mut c_char { - let Some(function_input) = types::build_function_input( - input.ctx, - input.blocks, - input.num_blocks, - input.fcn_addr, - input.fcn_name, - ) else { - return ptr::null_mut(); - }; - let external_context = cstr_or_default(input.external_context_json, "{}"); - let symbolic_scope = if input.scope_functions.is_null() || input.scope_num_functions == 0 { +struct FunctionAnalysisSharedBundle { + function_name: String, + function_addr: u64, + cfg_risk: CfgRiskSummaryJson, + semantic_artifact: Option, + function_facts: r2types::FunctionFacts, + semantic_route: Option, + type_writeback: InferredTypeWritebackJson, + prefer_bounded_type_plan: bool, +} + +fn build_inference_symbolic_scope( + input: &TypeWritebackInferenceInput<'_>, + function_input: &types::FunctionInput<'_>, +) -> Option { + if input.scope_functions.is_null() || input.scope_num_functions == 0 { None } else { unsafe { @@ -5566,261 +5888,212 @@ fn infer_type_writeback_json_impl(input: TypeWritebackInferenceInput<'_>) -> *mu function_input.function_addr, ) } - }; - let (arch_name, ptr_bits, _) = r2dec::DecompilerConfig::for_arch(function_input.ctx.arch); - let payload = if let Some(cached_artifact) = - types::get_cached_function_analysis_artifact_with_scope( - &function_input, - &external_context, - symbolic_scope.as_ref(), - ) { - if let Some(compiled) = cached_artifact.function_facts.semantics.as_ref() - && r2types::semantic_artifact_prefers_bounded_type_plan(compiled) - { - semantic_type_fallback_payload( - &function_input.function_name, - &arch_name, - ptr_bits, - input.interproc, - compiled, - ) - } else { - type_writeback_payload_from_artifact(cached_artifact, input.interproc) - } - } else { - let Some(analysis) = types::build_function_analysis(&function_input) else { - return ptr::null_mut(); - }; - let semantic_artifact = r2sym::compile_semantic_artifact_default_with_scope( - &z3::Context::thread_local(), - &analysis.ssa_func, - symbolic_scope.as_ref(), - function_input.ctx.arch, - ); - if r2types::semantic_artifact_prefers_bounded_type_plan(&semantic_artifact) { - semantic_type_fallback_payload( - &function_input.function_name, - &arch_name, - ptr_bits, - input.interproc, - &semantic_artifact, - ) - } else { - let interproc_summary_set = types::build_interproc_summary_set( - &function_input, - &analysis, - input.interproc.scope_json, - input.interproc.max_iters, - ); - let Some(artifact) = - types::build_function_analysis_artifact_from_analysis_with_semantic_artifact( - &function_input, - analysis, - &external_context, - Some(interproc_summary_set), - semantic_artifact, - ) - else { - return ptr::null_mut(); - }; - type_writeback_payload_from_artifact(artifact, input.interproc) - } - }; - - match serde_json::to_string(&payload) { - Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), - Err(_) => ptr::null_mut(), } } -fn function_facts_json_impl(input: TypeWritebackInferenceInput<'_>) -> *mut c_char { - let Some(function_input) = types::build_function_input( +fn build_function_analysis_shared_bundle( + input: TypeWritebackInferenceInput<'_>, +) -> Option { + let function_input = types::build_function_input( input.ctx, input.blocks, input.num_blocks, input.fcn_addr, input.fcn_name, - ) else { - return ptr::null_mut(); - }; + )?; let external_context = cstr_or_default(input.external_context_json, "{}"); - let symbolic_scope = if input.scope_functions.is_null() || input.scope_num_functions == 0 { - None - } else { - unsafe { - analysis::sym::build_symbolic_scope_from_ffi( - input.scope_functions, - input.scope_num_functions, - function_input.ctx.arch, - function_input.function_addr, - ) - } - }; - let Some(function_analysis) = types::build_function_analysis(&function_input) else { - return ptr::null_mut(); - }; - let cfg_risk = cfg_risk_summary_json(function_analysis.ssa_func.function().cfg_risk_summary()); + let symbolic_scope = build_inference_symbolic_scope(&input, &function_input); let (arch_name, ptr_bits, _) = r2dec::DecompilerConfig::for_arch(function_input.ctx.arch); - let cached_artifact = types::get_cached_function_analysis_artifact_with_scope( - &function_input, - &external_context, - symbolic_scope.as_ref(), - ); - - let (type_writeback, semantic_artifact) = if let Some(cached_artifact) = cached_artifact { - let semantics = cached_artifact.function_facts.semantics.clone(); - let payload = if let Some(compiled) = semantics.as_ref() - && r2types::semantic_artifact_prefers_bounded_type_plan(compiled) - { - semantic_type_fallback_payload( - &function_input.function_name, - &arch_name, - ptr_bits, - input.interproc, - compiled, - ) - } else { - type_writeback_payload_from_artifact(cached_artifact.clone(), input.interproc) - }; - (payload, semantics) - } else { - let semantic_artifact = r2sym::compile_semantic_artifact_default_with_scope( - &z3::Context::thread_local(), - &function_analysis.ssa_func, - symbolic_scope.as_ref(), - function_input.ctx.arch, - ); - if r2types::semantic_artifact_prefers_bounded_type_plan(&semantic_artifact) { - let payload = semantic_type_fallback_payload( - &function_input.function_name, - &arch_name, - ptr_bits, - input.interproc, - &semantic_artifact, - ); - (payload, Some(semantic_artifact)) - } else { - let interproc_summary_set = types::build_interproc_summary_set( + let parsed_context = r2types::parse_external_context_json(&external_context, ptr_bits); + let (cfg_risk, semantic_artifact, function_facts, type_writeback, prefer_bounded_type_plan) = + if let Some(cached_artifact) = + types::get_cached_function_analysis_artifact_with_scope_and_interproc( &function_input, - &function_analysis, + &external_context, input.interproc.scope_json, input.interproc.max_iters, + symbolic_scope.as_ref(), + ) + { + let cfg_risk = + cfg_risk_summary_json(cached_artifact.ssa_func.function().cfg_risk_summary()); + let function_facts = cached_artifact.function_facts.clone(); + let semantic_artifact = function_facts.semantics.clone(); + let prefer_bounded_type_plan = semantic_artifact + .as_ref() + .is_some_and(r2types::semantic_artifact_prefers_bounded_type_plan); + if prefer_bounded_type_plan { + let compiled = semantic_artifact + .as_ref() + .expect("bounded semantic type fallback requires semantic artifact"); + ( + cfg_risk, + semantic_artifact.clone(), + function_facts.clone(), + semantic_type_fallback_payload( + &function_input.function_name, + &arch_name, + ptr_bits, + input.interproc, + compiled, + &function_facts, + symbolic_scope.as_ref(), + ), + true, + ) + } else { + ( + cfg_risk, + semantic_artifact, + function_facts, + type_writeback_payload_from_artifact( + cached_artifact, + input.interproc, + symbolic_scope.as_ref(), + ), + false, + ) + } + } else { + let analysis = types::build_function_analysis(&function_input)?; + let cfg_risk = cfg_risk_summary_json(analysis.ssa_func.function().cfg_risk_summary()); + let semantic_artifact = r2sym::compile_semantic_artifact_default_with_scope( + &z3::Context::thread_local(), + &analysis.ssa_func, + symbolic_scope.as_ref(), + function_input.ctx.arch, ); - let Some(artifact) = - types::build_function_analysis_artifact_from_analysis_with_semantic_artifact( + if r2types::semantic_artifact_prefers_bounded_type_plan(&semantic_artifact) { + let function_facts = semantic_fallback_function_facts( + &semantic_artifact, + &parsed_context.assumptions, + ); + ( + cfg_risk, + Some(semantic_artifact.clone()), + function_facts.clone(), + semantic_type_fallback_payload( + &function_input.function_name, + &arch_name, + ptr_bits, + input.interproc, + &semantic_artifact, + &function_facts, + symbolic_scope.as_ref(), + ), + true, + ) + } else { + let interproc_summary_set = types::build_interproc_summary_set_with_scope( &function_input, - function_analysis, - &external_context, - Some(interproc_summary_set), + &analysis, + input.interproc.scope_json, + input.interproc.max_iters, + symbolic_scope.as_ref(), + ); + let artifact = + types::build_function_analysis_artifact_from_analysis_with_semantic_artifact( + &function_input, + analysis, + &external_context, + Some(interproc_summary_set), + semantic_artifact, + )?; + let function_facts = artifact.function_facts.clone(); + let semantic_artifact = function_facts.semantics.clone(); + ( + cfg_risk, semantic_artifact, + function_facts, + type_writeback_payload_from_artifact( + artifact, + input.interproc, + symbolic_scope.as_ref(), + ), + false, ) - else { - return ptr::null_mut(); - }; - let semantics = artifact.function_facts.semantics.clone(); - ( - type_writeback_payload_from_artifact(artifact, input.interproc), - semantics, - ) - } - }; - - let semantic_route = semantic_artifact - .as_ref() - .and_then(|artifact| { - r2dec::detached_semantic_route_plan( - &function_input.function_name, - function_input.blocks.as_slice(), - Some(artifact), - ) - }) - .map(semantic_route_plan_json); + } + }; + let semantic_route = r2dec::detached_semantic_route_plan( + &function_input.function_name, + function_input.blocks.as_slice(), + &function_facts, + ) + .map(semantic_route_plan_json); - let payload = FunctionFactsReportJson { + Some(FunctionAnalysisSharedBundle { function_name: function_input.function_name.clone(), function_addr: function_input.function_addr, cfg_risk, - semantic_build_plan: semantic_artifact - .as_ref() - .map(r2sym::SemanticArtifact::build_plan), + semantic_artifact, + function_facts, semantic_route, type_writeback, + prefer_bounded_type_plan, + }) +} + +fn infer_type_writeback_json_impl(input: TypeWritebackInferenceInput<'_>) -> *mut c_char { + let Some(bundle) = build_function_analysis_shared_bundle(input) else { + return ptr::null_mut(); }; - match serde_json::to_string(&payload) { + match serde_json::to_string(&bundle.type_writeback) { Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), Err(_) => ptr::null_mut(), } } -fn function_plan_json_impl(input: TypeWritebackInferenceInput<'_>) -> *mut c_char { - let Some(function_input) = types::build_function_input( - input.ctx, - input.blocks, - input.num_blocks, - input.fcn_addr, - input.fcn_name, - ) else { +fn function_facts_json_impl(input: TypeWritebackInferenceInput<'_>) -> *mut c_char { + let Some(bundle) = build_function_analysis_shared_bundle(input) else { return ptr::null_mut(); }; - let external_context = cstr_or_default(input.external_context_json, "{}"); - let symbolic_scope = if input.scope_functions.is_null() || input.scope_num_functions == 0 { - None - } else { - unsafe { - analysis::sym::build_symbolic_scope_from_ffi( - input.scope_functions, - input.scope_num_functions, - function_input.ctx.arch, - function_input.function_addr, - ) - } + + let payload = FunctionFactsReportJson { + function_name: bundle.function_name.clone(), + function_addr: bundle.function_addr, + cfg_risk: bundle.cfg_risk, + semantic_build_plan: bundle + .semantic_artifact + .as_ref() + .map(r2sym::SemanticArtifact::build_plan), + plans: bundle.function_facts.plans.clone(), + semantic_route: bundle.semantic_route, + assumptions: bundle.function_facts.assumptions.clone(), + assumption_usage: bundle.function_facts.assumption_usage.clone(), + summary_diagnostics: bundle.function_facts.summary_view.diagnostics().cloned(), + type_writeback: bundle.type_writeback, }; - let Some(function_analysis) = types::build_function_analysis(&function_input) else { + + match serde_json::to_string(&payload) { + Ok(s) => CString::new(s).map_or(ptr::null_mut(), |c| c.into_raw()), + Err(_) => ptr::null_mut(), + } +} + +fn function_plan_json_impl(input: TypeWritebackInferenceInput<'_>) -> *mut c_char { + let Some(bundle) = build_function_analysis_shared_bundle(input) else { return ptr::null_mut(); }; - let cfg_risk = cfg_risk_summary_json(function_analysis.ssa_func.function().cfg_risk_summary()); - let semantic_artifact = types::get_cached_function_analysis_artifact_with_scope( - &function_input, - &external_context, - symbolic_scope.as_ref(), - ) - .and_then(|artifact| artifact.function_facts.semantics) - .or_else(|| { - Some(r2sym::compile_semantic_artifact_default_with_scope( - &z3::Context::thread_local(), - &function_analysis.ssa_func, - symbolic_scope.as_ref(), - function_input.ctx.arch, - )) - }); - - let semantic_route = semantic_artifact - .as_ref() - .and_then(|artifact| { - r2dec::detached_semantic_route_plan( - &function_input.function_name, - function_input.blocks.as_slice(), - Some(artifact), - ) - }) - .map(semantic_route_plan_json); - let prefer_bounded_type_plan = semantic_artifact - .as_ref() - .is_some_and(r2types::semantic_artifact_prefers_bounded_type_plan); let payload = FunctionPlanReportJson { - function_name: function_input.function_name.clone(), - function_addr: function_input.function_addr, - cfg_risk, - semantic: semantic_artifact + function_name: bundle.function_name, + function_addr: bundle.function_addr, + cfg_risk: bundle.cfg_risk, + semantic: bundle + .semantic_artifact .as_ref() .map(analysis::sym::compiled_semantic_info), - semantic_build_plan: semantic_artifact + semantic_build_plan: bundle + .semantic_artifact .as_ref() .map(r2sym::SemanticArtifact::build_plan), - semantic_route, - prefer_bounded_type_plan, + plans: bundle.function_facts.plans.clone(), + semantic_route: bundle.semantic_route, + assumptions: bundle.function_facts.assumptions.clone(), + assumption_usage: bundle.function_facts.assumption_usage.clone(), + summary_diagnostics: bundle.function_facts.summary_view.diagnostics().cloned(), + prefer_bounded_type_plan: bundle.prefer_bounded_type_plan, }; match serde_json::to_string(&payload) { @@ -5883,10 +6156,14 @@ fn semantic_worker_linearization_impl(input: SemanticWorkerLinearizationInput) - let Some(semantic_artifact) = semantic_artifact else { return ptr::null_mut(); }; + let function_facts = r2types::FunctionFacts::new( + r2types::FunctionTypeFacts::default(), + Some(semantic_artifact.clone()), + ); let Some(route) = r2dec::detached_semantic_route_plan( &function_input.function_name, function_input.blocks.as_slice(), - Some(&semantic_artifact), + &function_facts, ) else { return ptr::null_mut(); }; @@ -6069,6 +6346,64 @@ pub extern "C" fn r2sleigh_function_plan_json_scope_ex( }) } +#[unsafe(no_mangle)] +pub extern "C" fn r2sleigh_function_analysis_artifact_cache_key( + ctx: *const R2ILContext, + blocks: *const *const R2ILBlock, + num_blocks: usize, + fcn_addr: u64, + fcn_name: *const c_char, + external_context_json: *const c_char, + interproc_max_iters: usize, + interproc_scope_json: *const c_char, +) -> u64 { + let Some(function_input) = + types::build_function_input(ctx, blocks, num_blocks, fcn_addr, fcn_name) + else { + return 0; + }; + let external_context = cstr_or_default(external_context_json, "{}"); + let interproc_scope = cstr_or_default(interproc_scope_json, "{}"); + types::function_analysis_artifact_cache_identity_hash( + &function_input, + &external_context, + &interproc_scope, + interproc_max_iters.max(1), + None, + ) + .unwrap_or(0) +} + +#[unsafe(no_mangle)] +pub extern "C" fn r2sleigh_function_interproc_summary_json_ex( + ctx: *const R2ILContext, + blocks: *const *const R2ILBlock, + num_blocks: usize, + fcn_addr: u64, + fcn_name: *const c_char, + external_context_json: *const c_char, + interproc_max_iters: usize, + interproc_scope_json: *const c_char, +) -> *mut c_char { + let Some(function_input) = + types::build_function_input(ctx, blocks, num_blocks, fcn_addr, fcn_name) + else { + return ptr::null_mut(); + }; + let external_context = cstr_or_default(external_context_json, "{}"); + let interproc_scope = cstr_or_default(interproc_scope_json, "{}"); + let Some(summary_json) = types::function_root_interproc_summary_json_with_scope( + &function_input, + &external_context, + &interproc_scope, + interproc_max_iters.max(1), + None, + ) else { + return ptr::null_mut(); + }; + CString::new(summary_json).map_or(ptr::null_mut(), |c| c.into_raw()) +} + #[unsafe(no_mangle)] pub extern "C" fn r2dec_semantic_worker_linearization_scope_ffi( ctx: *const R2ILContext, @@ -7775,6 +8110,54 @@ mod tests { } } + #[test] + fn semantic_type_fallback_payload_preserves_assumptions() { + let compiled = test_large_cfg_semantic_artifact(); + assert!( + r2types::semantic_artifact_prefers_bounded_type_plan(&compiled), + "expected large-CFG worker artifact to use bounded semantic type fallback" + ); + + let assumptions = r2ssa::AssumptionSet::new(vec![r2ssa::AnalysisAssumption { + id: Some("seed-rdi".to_string()), + scope: r2ssa::AssumptionScope::Function, + provenance: r2ssa::AssumptionProvenance::User, + subject: r2ssa::AssumptionSubject::Register { + name: "rdi".to_string(), + }, + value: r2ssa::AssumptionValue::Constant { value: 0xdead }, + }]); + let function_facts = semantic_fallback_function_facts(&compiled, &assumptions); + let payload = semantic_type_fallback_payload( + "fcn.401000", + "x86-64", + 64, + InterprocInferenceInput { + iter: 0, + max_iters: 0, + converged: true, + scope_json: "{}", + }, + &compiled, + &function_facts, + None, + ); + let value = serde_json::to_value(payload).expect("payload should serialize"); + let Some(items) = value["assumptions"]["items"].as_array() else { + panic!("expected serialized assumptions, got {value:?}"); + }; + assert_eq!( + items.len(), + 1, + "expected one serialized assumption: {value:?}" + ); + assert_eq!( + items[0]["subject"]["register"]["name"].as_str(), + Some("rdi"), + "expected register assumption to survive fallback payload: {value:?}" + ); + } + #[test] fn decompile_ready_large_cfg_worker_keeps_real_decompile_path() { let compiled = test_large_cfg_semantic_artifact(); @@ -8448,6 +8831,217 @@ mod integration_tests { ); } + #[test] + fn function_interproc_summary_json_ex_returns_typed_root_summary() { + let arch = CString::new("x86-64").expect("valid arch"); + let ctx = r2il_arch_init(arch.as_ptr()); + assert!(!ctx.is_null(), "context should initialize"); + + let mut block = R2ILBlock::new(0x401000, 4); + block.push(R2ILOp::Call { + target: Varnode::constant(0x2000, 8), + }); + block.push(R2ILOp::Return { + target: Varnode { + space: r2il::SpaceId::Register, + offset: 0, + size: 8, + meta: None, + }, + }); + let raw_block = Box::into_raw(Box::new(block)); + let blocks = [raw_block as *const R2ILBlock]; + + let func_name = CString::new("sym.alloc_wrapper").expect("valid function name"); + let external_context = CString::new("{}").expect("valid context"); + let scope_json = CString::new(r#"{"seeds":[{"id":8192,"name":"sym.imp.malloc"}]}"#) + .expect("valid scope json"); + + let out = r2sleigh_function_interproc_summary_json_ex( + ctx, + blocks.as_ptr(), + blocks.len(), + 0x401000, + func_name.as_ptr(), + external_context.as_ptr(), + 4, + scope_json.as_ptr(), + ); + assert!(!out.is_null(), "summary json should not be null"); + let output = unsafe { CStr::from_ptr(out) }.to_string_lossy().to_string(); + let payload: serde_json::Value = + serde_json::from_str(&output).expect("summary should parse"); + + r2il_string_free(out); + r2il_block_free(raw_block); + r2il_free(ctx); + + assert_eq!(payload["return_relation"].as_str(), Some("HeapAlloc")); + } + + #[test] + fn function_analysis_artifact_cache_key_changes_with_interproc_scope() { + let arch = CString::new("x86-64").expect("valid arch"); + let ctx = r2il_arch_init(arch.as_ptr()); + assert!(!ctx.is_null(), "context should initialize"); + + let mut block = R2ILBlock::new(0x401000, 4); + block.push(R2ILOp::Call { + target: Varnode::constant(0x2000, 8), + }); + block.push(R2ILOp::Return { + target: Varnode { + space: r2il::SpaceId::Register, + offset: 0, + size: 8, + meta: None, + }, + }); + let raw_block = Box::into_raw(Box::new(block)); + let blocks = [raw_block as *const R2ILBlock]; + + let func_name = CString::new("sym.alloc_wrapper").expect("valid function name"); + let external_context = CString::new("{}").expect("valid context"); + let empty_scope = CString::new("{}").expect("valid scope"); + let seeded_scope = CString::new(r#"{"seeds":[{"id":8192,"name":"sym.imp.malloc"}]}"#) + .expect("valid seeded scope"); + + let root_key = r2sleigh_function_analysis_artifact_cache_key( + ctx, + blocks.as_ptr(), + blocks.len(), + 0x401000, + func_name.as_ptr(), + external_context.as_ptr(), + 1, + empty_scope.as_ptr(), + ); + let seeded_key = r2sleigh_function_analysis_artifact_cache_key( + ctx, + blocks.as_ptr(), + blocks.len(), + 0x401000, + func_name.as_ptr(), + external_context.as_ptr(), + 1, + seeded_scope.as_ptr(), + ); + + r2il_block_free(raw_block); + r2il_free(ctx); + + assert_ne!(root_key, 0); + assert_ne!(seeded_key, 0); + assert_ne!(root_key, seeded_key); + } + + #[test] + fn function_analysis_artifact_cache_key_changes_with_interproc_budget() { + let arch = CString::new("x86-64").expect("valid arch"); + let ctx = r2il_arch_init(arch.as_ptr()); + assert!(!ctx.is_null(), "context should initialize"); + + let mut block = R2ILBlock::new(0x401000, 4); + block.push(R2ILOp::Call { + target: Varnode::constant(0x2000, 8), + }); + block.push(R2ILOp::Return { + target: Varnode { + space: r2il::SpaceId::Register, + offset: 0, + size: 8, + meta: None, + }, + }); + let raw_block = Box::into_raw(Box::new(block)); + let blocks = [raw_block as *const R2ILBlock]; + + let func_name = CString::new("sym.alloc_wrapper").expect("valid function name"); + let external_context = CString::new("{}").expect("valid context"); + let scope = CString::new(r#"{"seeds":[{"id":8192,"name":"sym.imp.malloc"}]}"#) + .expect("valid scope"); + + let low_budget_key = r2sleigh_function_analysis_artifact_cache_key( + ctx, + blocks.as_ptr(), + blocks.len(), + 0x401000, + func_name.as_ptr(), + external_context.as_ptr(), + 1, + scope.as_ptr(), + ); + let high_budget_key = r2sleigh_function_analysis_artifact_cache_key( + ctx, + blocks.as_ptr(), + blocks.len(), + 0x401000, + func_name.as_ptr(), + external_context.as_ptr(), + 4, + scope.as_ptr(), + ); + + r2il_block_free(raw_block); + r2il_free(ctx); + + assert_ne!(low_budget_key, 0); + assert_ne!(high_budget_key, 0); + assert_ne!(low_budget_key, high_budget_key); + } + + #[test] + fn function_analysis_artifact_cache_key_changes_with_function_name() { + let arch = CString::new("x86-64").expect("valid arch"); + let ctx = r2il_arch_init(arch.as_ptr()); + assert!(!ctx.is_null(), "context should initialize"); + + let mut block = R2ILBlock::new(0x401000, 4); + block.push(R2ILOp::Return { + target: Varnode { + space: r2il::SpaceId::Register, + offset: 0, + size: 8, + meta: None, + }, + }); + let raw_block = Box::into_raw(Box::new(block)); + let blocks = [raw_block as *const R2ILBlock]; + + let first_name = CString::new("sym.first").expect("valid function name"); + let second_name = CString::new("sym.second").expect("valid function name"); + let external_context = CString::new("{}").expect("valid context"); + let scope = CString::new("{}").expect("valid scope"); + + let first_key = r2sleigh_function_analysis_artifact_cache_key( + ctx, + blocks.as_ptr(), + blocks.len(), + 0x401000, + first_name.as_ptr(), + external_context.as_ptr(), + 1, + scope.as_ptr(), + ); + let second_key = r2sleigh_function_analysis_artifact_cache_key( + ctx, + blocks.as_ptr(), + blocks.len(), + 0x401000, + second_name.as_ptr(), + external_context.as_ptr(), + 1, + scope.as_ptr(), + ); + + r2il_block_free(raw_block); + r2il_free(ctx); + + assert_ne!(first_key, 0); + assert_ne!(second_key, 0); + assert_ne!(first_key, second_key); + } + #[test] fn direct_call_targets_json_reports_constant_call_target() { let arch = CString::new("x86-64").expect("valid arch"); @@ -10026,7 +10620,7 @@ mod integration_tests { .expect("analysis artifact"); let mut decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); - decompiler.set_type_facts(artifact.function_facts.types.clone()); + decompiler.set_function_facts(artifact.function_facts.clone()); decompiler.set_function_names(HashMap::from([ (0x401140, "sym.imp.memcpy".to_string()), (0x401150, "sym.imp.malloc".to_string()), @@ -10079,7 +10673,7 @@ mod integration_tests { .ops = pattern_ssa_blocks[0].ops.clone(); manual_func = manual_func.with_name("dbg.test_struct_array_index"); let mut manual_decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); - manual_decompiler.set_type_facts(artifact.function_facts.types.clone()); + manual_decompiler.set_function_facts(artifact.function_facts.clone()); let manual_output = manual_decompiler.decompile(&manual_func); assert!( @@ -10236,7 +10830,7 @@ mod integration_tests { ); let mut decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); - decompiler.set_type_facts(artifact.function_facts.types.clone()); + decompiler.set_function_facts(artifact.function_facts.clone()); decompiler.set_function_names(HashMap::from([ (0x401140, "sym.imp.memcpy".to_string()), (0x401150, "sym.imp.malloc".to_string()), @@ -10355,7 +10949,7 @@ mod integration_tests { .expect("analysis artifact"); let mut decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); - decompiler.set_type_facts(artifact.function_facts.types.clone()); + decompiler.set_function_facts(artifact.function_facts.clone()); decompiler.set_function_names(HashMap::from([ (0x401110, "sym.imp.printf".to_string()), (0x401140, "sym.imp.memcpy".to_string()), @@ -10493,7 +11087,7 @@ mod integration_tests { .expect("analysis artifact"); let mut decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); - decompiler.set_type_facts(artifact.function_facts.types.clone()); + decompiler.set_function_facts(artifact.function_facts.clone()); decompiler.set_function_names(HashMap::from([(0x401130, "sym.imp.strcmp".to_string())])); decompiler.set_strings(HashMap::from([(0x403014, "secret123".to_string())])); decompiler.set_known_function_signatures(HashMap::from([( @@ -10631,7 +11225,7 @@ mod integration_tests { .collect(); let mut decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); - decompiler.set_type_facts(artifact.function_facts.types.clone()); + decompiler.set_function_facts(artifact.function_facts.clone()); let output = decompiler.decompile(&artifact.ssa_func); let output_without_types = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()).decompile(&artifact.ssa_func); @@ -10726,7 +11320,7 @@ mod integration_tests { .expect("analysis artifact"); let mut decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); - decompiler.set_type_facts(artifact.function_facts.types.clone()); + decompiler.set_function_facts(artifact.function_facts.clone()); let output = decompiler.decompile(&artifact.ssa_func); let visible_bindings = artifact.function_facts.types.visible_bindings.clone(); @@ -10806,7 +11400,7 @@ mod integration_tests { .expect("analysis artifact"); let mut decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); - decompiler.set_type_facts(artifact.function_facts.types.clone()); + decompiler.set_function_facts(artifact.function_facts.clone()); decompiler.set_function_names(HashMap::from([(0x401160, "sym.imp.setlocale".to_string())])); decompiler.set_strings(HashMap::from([(0x403040, "C".to_string())])); decompiler.set_known_function_signatures(HashMap::from([( @@ -11313,7 +11907,7 @@ mod integration_tests { .expect("analysis artifact"); let mut decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); - decompiler.set_type_facts(artifact.function_facts.types.clone()); + decompiler.set_function_facts(artifact.function_facts.clone()); decompiler.set_function_names(HashMap::from([(0x401100, "sym.imp.strlen".to_string())])); decompiler.set_known_function_signatures(HashMap::from([( "sym.imp.strlen".to_string(), @@ -11439,7 +12033,7 @@ mod integration_tests { .expect("analysis artifact"); let mut decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); - decompiler.set_type_facts(artifact.function_facts.types.clone()); + decompiler.set_function_facts(artifact.function_facts.clone()); decompiler.set_function_names(HashMap::from([(0x401100, "sym.imp.strlen".to_string())])); decompiler.set_known_function_signatures(HashMap::from([( "sym.imp.strlen".to_string(), @@ -11560,7 +12154,7 @@ mod integration_tests { .expect("analysis artifact"); let mut decompiler = r2dec::Decompiler::new(r2dec::DecompilerConfig::x86_64()); - decompiler.set_type_facts(artifact.function_facts.types.clone()); + decompiler.set_function_facts(artifact.function_facts.clone()); decompiler.set_function_names(HashMap::from([(0x401100, "sym.imp.strlen".to_string())])); decompiler.set_known_function_signatures(HashMap::from([( "sym.imp.strlen".to_string(), diff --git a/r2plugin/src/types.rs b/r2plugin/src/types.rs index b1a685a..d93c378 100644 --- a/r2plugin/src/types.rs +++ b/r2plugin/src/types.rs @@ -44,7 +44,6 @@ pub(crate) struct FunctionAnalysisArtifact { pub(crate) pattern_ssa_func: r2ssa::SsaArtifact, pub(crate) function_facts: r2types::FunctionFacts, pub(crate) writeback_plan: r2types::TypeWritebackPlan, - pub(crate) interproc_summary_set: Option, } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -56,9 +55,11 @@ struct FunctionAnalysisCacheKey { #[derive(Debug, Clone, PartialEq, Eq, Hash)] struct FunctionArtifactCacheKey { analysis: FunctionAnalysisCacheKey, + function_name_hash: u64, semantic_metadata_enabled: bool, external_context_hash: u64, interproc_scope_hash: u64, + interproc_max_iterations: u64, symbolic_scope_hash: u64, } @@ -92,6 +93,12 @@ fn hash_string_payload(payload: &str) -> u64 { hasher.finish() } +fn hash_value(value: &T) -> u64 { + let mut hasher = DefaultHasher::new(); + value.hash(&mut hasher); + hasher.finish() +} + fn function_analysis_cache_key_parts( _function_name: &str, arch: Option<&ArchSpec>, @@ -103,6 +110,7 @@ fn function_analysis_cache_key_parts( }) } +#[allow(clippy::too_many_arguments)] fn function_artifact_cache_key_parts( function_name: &str, arch: Option<&ArchSpec>, @@ -110,13 +118,16 @@ fn function_artifact_cache_key_parts( semantic_metadata_enabled: bool, external_context_json: &str, interproc_scope_json: &str, + interproc_max_iterations: usize, symbolic_scope: Option<&r2sym::PreparedFunctionScope>, ) -> Option { Some(FunctionArtifactCacheKey { analysis: function_analysis_cache_key_parts(function_name, arch, blocks)?, + function_name_hash: hash_string_payload(function_name), semantic_metadata_enabled, external_context_hash: hash_string_payload(external_context_json), interproc_scope_hash: hash_string_payload(interproc_scope_json), + interproc_max_iterations: interproc_max_iterations as u64, symbolic_scope_hash: r2sym::stable_scope_hash(symbolic_scope), }) } @@ -166,17 +177,25 @@ fn rename_function_analysis_artifact( pattern_ssa_func, function_facts, writeback_plan, - interproc_summary_set, + .. } = artifact; FunctionAnalysisArtifact { ssa_func: ssa_func.with_name(function_name), pattern_ssa_func: pattern_ssa_func.with_name(function_name), function_facts, writeback_plan, - interproc_summary_set, } } +fn merged_assumption_usage( + prepared: &r2ssa::SsaArtifact, + function_facts: &r2types::FunctionFacts, +) -> r2ssa::AssumptionUsageReport { + let mut usage = prepared.facts().assumption_usage.clone(); + usage.extend(&function_facts.assumption_usage); + usage +} + #[cfg(test)] fn type_like_to_ctype(ty: &r2types::CTypeLike) -> r2dec::CType { match ty { @@ -292,6 +311,7 @@ fn function_artifact_cache_key( input: &FunctionInput<'_>, external_context_json: &str, interproc_scope_json: &str, + interproc_max_iterations: usize, symbolic_scope: Option<&r2sym::PreparedFunctionScope>, ) -> Option { function_artifact_cache_key_parts( @@ -301,10 +321,28 @@ fn function_artifact_cache_key( input.ctx.semantic_metadata_enabled, external_context_json, interproc_scope_json, + interproc_max_iterations, symbolic_scope, ) } +pub(crate) fn function_analysis_artifact_cache_identity_hash( + input: &FunctionInput<'_>, + external_context_json: &str, + interproc_scope_json: &str, + interproc_max_iterations: usize, + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, +) -> Option { + function_artifact_cache_key( + input, + external_context_json, + interproc_scope_json, + interproc_max_iterations, + symbolic_scope, + ) + .map(|key| hash_value(&key)) +} + pub(crate) fn build_function_analysis_from_parts( function_name: &str, blocks: &[R2ILBlock], @@ -427,20 +465,38 @@ fn parse_interproc_seed_summaries( seeds } -pub(crate) fn build_interproc_summary_set( +pub(crate) fn build_interproc_summary_set_with_scope( input: &FunctionInput<'_>, analysis: &FunctionAnalysis, scope_json: &str, max_iterations: usize, + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, ) -> r2ssa::InterprocSummarySet { let root = r2ssa::InterprocFunctionId(input.function_addr); let seeds = parse_interproc_seed_summaries(scope_json); + let seeded_helpers = seeds + .keys() + .copied() + .collect::>(); + let mut functions = vec![r2ssa::InterprocFunctionInput { + id: root, + name: Some(input.function_name.clone()), + prepared: &analysis.ssa_func, + }]; + if let Some(scope) = symbolic_scope { + for function in scope.functions().values() { + if function.id == root || seeded_helpers.contains(&function.id) { + continue; + } + functions.push(r2ssa::InterprocFunctionInput { + id: function.id, + name: function.name.clone(), + prepared: &function.prepared, + }); + } + } r2ssa::solve_interproc_summary_set( - &[r2ssa::InterprocFunctionInput { - id: root, - name: Some(input.function_name.clone()), - prepared: &analysis.ssa_func, - }], + &functions, input.ctx.arch, Some(root), &seeds, @@ -613,8 +669,22 @@ pub(crate) fn build_function_analysis_artifact_from_analysis_with_semantic_artif .unwrap_or(64); let signature = infer_signature_cc_from_analysis(input, &analysis)?; let parsed_context = r2types::parse_external_context_json(external_context_json, ptr_bits); + let assumption_applied_analysis = if parsed_context.assumptions.is_empty() { + analysis.clone() + } else { + FunctionAnalysis { + ssa_func: analysis + .ssa_func + .with_assumptions(&parsed_context.assumptions), + pattern_ssa_func: analysis + .pattern_ssa_func + .with_assumptions(&parsed_context.assumptions), + } + }; - let pattern_ssa_blocks = analysis.pattern_ssa_func.local_ssa_blocks(); + let pattern_ssa_blocks = assumption_applied_analysis + .pattern_ssa_func + .local_ssa_blocks(); let decompiler_env = build_decompiler_env(&input.ctx); let decompiler_cfg = decompiler_env.cfg; let mut diagnostics = r2types::TypeWritebackDiagnostics::default(); @@ -624,9 +694,11 @@ pub(crate) fn build_function_analysis_artifact_from_analysis_with_semantic_artif ptr_bits, &mut diagnostics, ); - let local_field_accesses = local_field_accesses_to_writeback( - r2dec::infer_local_struct_field_accesses(&analysis.pattern_ssa_func, &decompiler_cfg), - ); + let local_field_accesses = + local_field_accesses_to_writeback(r2dec::infer_local_struct_field_accesses( + &assumption_applied_analysis.pattern_ssa_func, + &decompiler_cfg, + )); let reg_type_hints = if input.ctx.semantic_metadata_enabled { collect_register_type_hints(input.blocks.as_slice(), input.ctx.disasm) } else { @@ -656,12 +728,14 @@ pub(crate) fn build_function_analysis_artifact_from_analysis_with_semantic_artif local_field_accesses: &local_field_accesses, }, ); + let mut function_facts = writeback.function_facts; + function_facts.assumption_usage = + merged_assumption_usage(&assumption_applied_analysis.ssa_func, &function_facts); Some(FunctionAnalysisArtifact { - ssa_func: analysis.ssa_func, - pattern_ssa_func: analysis.pattern_ssa_func, - function_facts: writeback.function_facts, + ssa_func: assumption_applied_analysis.ssa_func, + pattern_ssa_func: assumption_applied_analysis.pattern_ssa_func, + function_facts, writeback_plan: writeback.plan, - interproc_summary_set, }) } @@ -672,6 +746,27 @@ pub(crate) fn build_function_analysis_artifact_from_analysis( interproc_summary_set: Option, symbolic_scope: Option<&r2sym::PreparedFunctionScope>, ) -> Option { + let parsed_context = r2types::parse_external_context_json( + external_context_json, + input + .ctx + .arch + .as_ref() + .map(|arch| effective_ptr_bits(arch)) + .unwrap_or(64), + ); + let analysis = if parsed_context.assumptions.is_empty() { + analysis + } else { + FunctionAnalysis { + ssa_func: analysis + .ssa_func + .with_assumptions(&parsed_context.assumptions), + pattern_ssa_func: analysis + .pattern_ssa_func + .with_assumptions(&parsed_context.assumptions), + } + }; let semantic_artifact = r2sym::compile_semantic_artifact_default_with_scope( &z3::Context::thread_local(), &analysis.ssa_func, @@ -714,6 +809,7 @@ pub(crate) fn build_function_analysis_artifact_with_scope( input, external_context_json, interproc_scope_json, + interproc_max_iterations, symbolic_scope, ); if let Some(cache_key) = cache_key.as_ref() @@ -730,11 +826,12 @@ pub(crate) fn build_function_analysis_artifact_with_scope( } let analysis = build_function_analysis(input)?; - let interproc_summary_set = build_interproc_summary_set( + let interproc_summary_set = build_interproc_summary_set_with_scope( input, &analysis, interproc_scope_json, interproc_max_iterations, + symbolic_scope, ); let artifact = build_function_analysis_artifact_from_analysis( input, @@ -752,13 +849,20 @@ pub(crate) fn build_function_analysis_artifact_with_scope( )) } -pub(crate) fn get_cached_function_analysis_artifact_with_scope( +fn get_cached_function_analysis_artifact_with_interproc_scope( input: &FunctionInput<'_>, external_context_json: &str, + interproc_scope_json: &str, + interproc_max_iterations: usize, symbolic_scope: Option<&r2sym::PreparedFunctionScope>, ) -> Option { - let cache_key = - function_artifact_cache_key(input, external_context_json, "{}", symbolic_scope)?; + let cache_key = function_artifact_cache_key( + input, + external_context_json, + interproc_scope_json, + interproc_max_iterations, + symbolic_scope, + )?; artifact_cache() .read() .expect("plugin cache read lock poisoned") @@ -769,31 +873,109 @@ pub(crate) fn get_cached_function_analysis_artifact_with_scope( }) } +pub(crate) fn get_cached_function_analysis_artifact_with_scope( + input: &FunctionInput<'_>, + external_context_json: &str, + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, +) -> Option { + get_cached_function_analysis_artifact_with_interproc_scope( + input, + external_context_json, + "{}", + 1, + symbolic_scope, + ) +} + +pub(crate) fn get_cached_function_analysis_artifact_with_scope_and_interproc( + input: &FunctionInput<'_>, + external_context_json: &str, + interproc_scope_json: &str, + interproc_max_iterations: usize, + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, +) -> Option { + get_cached_function_analysis_artifact_with_interproc_scope( + input, + external_context_json, + interproc_scope_json, + interproc_max_iterations, + symbolic_scope, + ) +} + pub(crate) fn alias_cached_function_analysis_artifact( input: &FunctionInput<'_>, - source_external_context_json: &str, - target_external_context_json: &str, + _source_external_context_json: &str, + _target_external_context_json: &str, ) -> bool { - let Some(source_key) = - function_artifact_cache_key(input, source_external_context_json, "{}", None) - else { - return false; - }; - let Some(target_key) = - function_artifact_cache_key(input, target_external_context_json, "{}", None) - else { - return false; - }; - let Some(cached) = artifact_cache() - .read() - .expect("plugin cache read lock poisoned") - .get(&source_key) - .cloned() - else { + let Some(analysis_key) = function_analysis_cache_key_parts( + &input.function_name, + input.ctx.arch, + input.blocks.as_slice(), + ) else { return false; }; - cache_insert_bounded(artifact_cache(), target_key, cached); - true + let function_name_hash = hash_string_payload(&input.function_name); + let mut cache = artifact_cache() + .write() + .expect("plugin cache write lock poisoned"); + let before = cache.len(); + cache.retain(|key, _| { + key.analysis != analysis_key || key.function_name_hash != function_name_hash + }); + cache.len() != before +} + +pub(crate) fn function_root_interproc_summary_with_scope( + input: &FunctionInput<'_>, + external_context_json: &str, + interproc_scope_json: &str, + interproc_max_iterations: usize, + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, +) -> Option { + get_cached_function_analysis_artifact_with_interproc_scope( + input, + external_context_json, + interproc_scope_json, + interproc_max_iterations, + symbolic_scope, + ) + .or_else(|| { + build_function_analysis_artifact_with_scope( + input, + external_context_json, + interproc_scope_json, + interproc_max_iterations, + symbolic_scope, + ) + }) + .and_then(|artifact| { + artifact + .function_facts + .interproc_summary_set() + .and_then(|summary_set| { + summary_set + .root + .and_then(|root| summary_set.summaries.get(&root).cloned()) + }) + }) +} + +pub(crate) fn function_root_interproc_summary_json_with_scope( + input: &FunctionInput<'_>, + external_context_json: &str, + interproc_scope_json: &str, + interproc_max_iterations: usize, + symbolic_scope: Option<&r2sym::PreparedFunctionScope>, +) -> Option { + let summary = function_root_interproc_summary_with_scope( + input, + external_context_json, + interproc_scope_json, + interproc_max_iterations, + symbolic_scope, + )?; + serde_json::to_string(&summary).ok() } #[allow(dead_code)] @@ -863,6 +1045,7 @@ pub(crate) fn build_detached_function_analysis_artifact_with_scope_and_semantics semantic_metadata_enabled, external_context_json, "{}", + 1, symbolic_scope, ); if let Some(cache_key) = cache_key.as_ref() @@ -949,7 +1132,6 @@ pub(crate) fn build_detached_function_analysis_artifact_with_scope_and_semantics pattern_ssa_func: analysis.pattern_ssa_func, function_facts: writeback.function_facts, writeback_plan: writeback.plan, - interproc_summary_set: None, }; if let Some(cache_key) = cache_key { cache_insert_bounded(artifact_cache(), cache_key, Arc::new(artifact.clone())); @@ -2923,6 +3105,7 @@ mod tests { true, "{}", "{}", + 1, None, ) .expect("root-only cache key"); @@ -2933,6 +3116,7 @@ mod tests { true, "{}", "{}", + 1, Some(&scope_a), ) .expect("scoped cache key a"); @@ -2943,6 +3127,7 @@ mod tests { true, "{}", "{}", + 1, Some(&scope_a), ) .expect("scoped cache key a repeat"); @@ -2953,6 +3138,7 @@ mod tests { true, "{}", "{}", + 1, Some(&scope_b), ) .expect("scoped cache key b"); @@ -2970,4 +3156,70 @@ mod tests { "different helper closures must not alias the same artifact cache entry" ); } + + #[test] + fn function_artifact_cache_key_distinguishes_function_name() { + let arch = ArchSpec::new("x86-64"); + let blocks = const_return_blocks(0x1000, 0); + let first = function_artifact_cache_key_parts( + "sym.first", + Some(&arch), + &blocks, + true, + "{}", + "{}", + 1, + None, + ) + .expect("first cache key"); + let second = function_artifact_cache_key_parts( + "sym.second", + Some(&arch), + &blocks, + true, + "{}", + "{}", + 1, + None, + ) + .expect("second cache key"); + + assert_ne!( + first, second, + "name-sensitive writeback artifacts must not alias across functions" + ); + } + + #[test] + fn function_artifact_cache_key_distinguishes_interproc_iteration_budget() { + let arch = ArchSpec::new("x86-64"); + let blocks = const_return_blocks(0x1000, 0); + let first = function_artifact_cache_key_parts( + "sym.root", + Some(&arch), + &blocks, + true, + "{}", + r#"{"phase":"fixpoint","summaries":[],"seeds":[]}"#, + 1, + None, + ) + .expect("low budget cache key"); + let second = function_artifact_cache_key_parts( + "sym.root", + Some(&arch), + &blocks, + true, + "{}", + r#"{"phase":"fixpoint","summaries":[],"seeds":[]}"#, + 4, + None, + ) + .expect("high budget cache key"); + + assert_ne!( + first, second, + "interproc summary artifacts must invalidate when the fixpoint budget changes" + ); + } } diff --git a/tests/r2r/db/extras/r2sleigh_integration_extended b/tests/r2r/db/extras/r2sleigh_integration_extended index eb5fcd6..76e9539 100644 --- a/tests/r2r/db/extras/r2sleigh_integration_extended +++ b/tests/r2r/db/extras/r2sleigh_integration_extended @@ -505,6 +505,36 @@ a:sym.solve.replayj 0x40132a {"checkpoint":2,"entry":"sym.check_secret","symboli EOF_CMDS RUN +NAME=sym_explore_state_check_secret_rdi_overlay_reaches_win +FILE=bins/vuln_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=<0 and (.paths|length)>0 and any(.paths[]; .final_pc=="0x40132a" and ((.solution.inputs.replay_rdi // .solution.inputs.EDI_0)=="0xdead"))' +EOF_CMDS +RUN + +NAME=sym_solve_state_check_secret_rdi_overlay_finds_0xdead +FILE=bins/vuln_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=<0 and .target=="0x40132a" and (.selected_path|has("solution")) and .selected_path.final_pc=="0x40132a" and ((.selected_path.solution.inputs.replay_rdi // .selected_path.solution.inputs.EDI_0)=="0xdead")' +EOF_CMDS +RUN + NAME=paths_check_secret_structure FILE=bins/vuln_test_x86 ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true @@ -1136,6 +1166,39 @@ a:sla.types sym.test_symbolic_helper_pure_guard | jq -c '.compiled_semantics.sta EOF_CMDS RUN +NAME=types_symbolic_helper_scope_roundtrips_persisted_assumptions_key +FILE=bins/vuln_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=</dev/null +af +a:sla.assumej [{"subject":{"register":{"name":"rdi"}},"value":{"type_hint":{"ty":"int32_t"}}}] >/dev/null +a:sla.types | jq -c 'tostring|contains("\"subject\":{\"register\":{\"name\":\"rdi\"}}") and contains("\"type_hint\":{\"ty\":\"int32_t\"}")' +EOF_CMDS +RUN + +NAME=types_symbolic_helper_scope_assumption_commands_roundtrip +FILE=bins/vuln_test +ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true +EXPECT=</dev/null +af +a:sla.assumej [{"subject":{"register":{"name":"rdi"}},"value":{"type_hint":{"ty":"int32_t"}}}] >/dev/null +a:sla.assumptions | jq -c 'length==1 and .[0].subject.register.name=="rdi" and .[0].value.type_hint.ty=="int32_t"' +a:sla.assumptions- >/dev/null +a:sla.assumptions | jq -c 'type=="array" and length==0' +EOF_CMDS +RUN + NAME=symbolic_helper_scope_reports_compiled_semantics FILE=bins/vuln_test ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true diff --git a/tests/r2r/db/extras/r2sleigh_signature_snapshots b/tests/r2r/db/extras/r2sleigh_signature_snapshots index 8922f3d..80f643a 100644 --- a/tests/r2r/db/extras/r2sleigh_signature_snapshots +++ b/tests/r2r/db/extras/r2sleigh_signature_snapshots @@ -2,7 +2,7 @@ NAME=signature_struct_array_index_full_snapshot FILE=bins/vuln_test_x86 ARGS=-e scr.color=false -e log.level=0 -e bin.relocs.apply=true EXPECT=< Date: Thu, 23 Apr 2026 18:42:00 +0000 Subject: [PATCH 10/10] Revise agent guidelines and roadmap for r2sleigh --- AGENTS.md | 636 +++++++++++++++++++++++------------------------------ README.md | 1 + ROADMAP.md | 630 ++++++++++++++++++++++++++++++---------------------- 3 files changed, 648 insertions(+), 619 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4e7f9c2..2c79a7a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,178 +1,243 @@ # Agent Guidelines for r2sleigh -> LLM-focused working notes for contributors and coding agents. +> LLM-focused working rules for contributors and coding agents. -## Project Summary +## North Star -`r2sleigh` is the Sleigh-backed analysis and decompiler pipeline for radare2. +`r2sleigh`, `r2ssa`, `r2sym`, `r2types`, `r2dec`, `r2plugin`, and +`../radare2` are one subsystem. -```text -.sla (Ghidra) --> libsla --> P-code --> r2il --> ESIL - | - +--> SSA (r2ssa) - +--> Type inference (r2types) - +--> Symbolic / taint (r2sym) - +--> Decompiler (r2dec) - +--> Plugin / CLI export surfaces -``` +The goal is not "more commands" or "more crates doing similar work." The goal +is a gold-standard radare2 analysis engine where: -The repository is no longer just "P-code to ESIL". A lot of current work lands in SSA, symbolic execution, type inference, decompilation, and radare2 integration layers. +- one canonical fact has one canonical owner +- facts flow through typed contracts, not JSON reparsing +- decompiler, types, symbolic execution, and radare2 core views agree +- expensive work is summarized, cached, and reused +- output is deterministic +- architecture and API seams may be rewritten whenever the rewrite is cleaner -Treat this workspace and `../radare2` as one analysis system. `r2sleigh` is part of the radare2 analysis/decompiler stack, not a separate sidecar that should paper over missing library seams at the end of the pipeline. +The plugin should feel like radare2 itself got smarter, not like radare2 grew a +second shell. -The opening sections are the operational rules. Later sections are reference material. +## Non-Negotiables -## Fast Path Rules +1. One fact, one owner. +2. Treat this repo and `../radare2` as one component boundary. +3. `r2plugin` is orchestration/FFI glue only. +4. Do not reconstruct missing semantics downstream. +5. Prefer typed contracts over JSON blobs and stringly maps. +6. `r2types::FunctionFacts` is the canonical combined type+semantic contract. +7. `r2types::FunctionTypeFacts` is the canonical type/layout/signature payload. +8. `r2sym::SemanticArtifact` is the canonical semantic artifact. +9. `r2sym` owns semantic policy and evidence; consumers interpret it. +10. Deterministic ordering beats cleverness. +11. Rewrite bad seams instead of patching around them. +12. Validation is part of the change, not optional cleanup. -1. Find the canonical owner of a fact before editing code. Cross-crate cooperation is expected; duplicated policy is not. -2. Treat this workspace and `../radare2` as one component boundary. If the right fix belongs in radare2, implement it there instead of patching around it in Rust. -3. `r2plugin` is orchestration/FFI glue only. Do not fix missing semantics by reparsing command output or merging policy there. -4. For decompiler/type context, do not add new plugin-side parsing of `afcfj`, `afvj`, or `tsj`. If the seam is wrong, fix it in `../radare2` and keep the plugin on the typed collector path. -5. `r2types::FunctionTypeFacts` is the only canonical type/layout/signature contract on the decompiler path. -6. Default to `tests/r2r` for new plugin regressions and command-output checks. Do not add new snapshot-style plugin tests to `tests/e2e/integration_tests.rs` unless `r2r` genuinely cannot express the case. -7. Build and run commands in this repo have drifted over time. Prefer the commands in this file over older examples. -8. File paths below use the current `src/` layout. Older references like `r2plugin/lib.rs` are stale. -9. Architecture feature support differs by crate. Check the relevant `Cargo.toml` before documenting or wiring a new arch. -10. When the current architecture blocks a clean design, rewrite the seam or crate API instead of adding end-stage hacks. Large refactors, FFI changes, and cross-repo redesigns are acceptable when they reduce long-term complexity. +## Optimization Doctrine -## Task-Start Protocol +Low-quality implementations tend to optimize the wrong thing. Do not do that. -For any non-trivial change, follow this order: +The target is not fantasy "`O(1)` symex." The target is: -1. Identify the user-visible behavior or broken invariant. -2. Decide which layer should own the fix: `../radare2`, lifting, SSA, symex, types, decompiler, export, CLI, or plugin glue. -3. Look for an existing typed contract to extend before adding new JSON blobs, stringly maps, or parallel wrapper types. -4. Push facts upstream to the canonical owner instead of reconstructing them downstream. -5. Add the smallest deterministic test at the layer users actually exercise. -6. If the seam crosses into `../radare2`, validate both repos before claiming the fix is complete. - -## Change Placement Guide - -Use this as the default "where should this logic live?" map: - -- `../radare2`: core analysis facts, typed collectors, and library seams that should exist for radare2 consumers generally. -- `crates/r2sleigh-lift`: Sleigh/P-code lifting, disassembly, register naming, text formatting, and ESIL formatting. -- `crates/r2ssa`: SSA construction, phi handling, dominators, def-use, determinism, and SSA-local transforms. -- `crates/r2sym`: symbolic execution, taint propagation, summaries, path exploration, and solver-facing symbolic policy. -- `crates/r2types`: signature parsing, type inference, layout inference, external context normalization, and canonical merged type facts. -- `crates/r2dec`: lowering, semantic interpretation, structuring, folding, and rendering. -- `crates/r2sleigh-export` and `crates/r2sleigh-cli`: shared export plumbing and CLI surface behavior. -- `r2plugin`: command dispatch, FFI, serialization, and radare2 integration glue only. - -## Workspace Layout - -```text -crates/ -├── r2il/ # Core IL types and serialization -├── r2sleigh-lift/ # Sleigh/P-code lifting, disassembly, ESIL formatting -├── r2sleigh-export/ # Unified export pipeline for lift/ssa/defuse/dec -├── r2sleigh-cli/ # Standalone CLI -├── r2ssa/ # SSA form, dominators, def-use, optimization -├── r2sym/ # Symbolic execution, taint, summaries, solving -├── r2types/ # Type inference and signatures -└── r2dec/ # Decompiler AST, folding, lowering, codegen -r2plugin/ # Rust cdylib + C radare2 wrapper -tests/ -├── r2r/ # Preferred snapshot and command regression suite -└── e2e/ # Rust semantic/FFI/benchmark suite and fixture binaries -``` +- `O(1)` or `O(log n)` lookup for metadata, indexes, summaries, and caches +- `O(n)` passes over blocks, SSA ops, or facts whenever possible +- bounded search when search is unavoidable +- incremental recomputation instead of whole-function replay +- summary reuse across query, typing, and decompilation +- explicit budgets for solver work, symbolic exploration, and structuring -## Whole-System Architecture Stance +Use this mental model: -`r2il`, `r2sleigh-lift`, `r2ssa`, `r2sym`, `r2types`, `r2dec`, `r2sleigh-export`, `r2plugin`, and `../radare2` should be treated as one radare2 analysis/decompiler subsystem with explicit internal ownership. +- repeated whole-function scans are a smell +- repeated solver queries for the same fact are a smell +- recomputing downstream what already exists upstream is a smell +- parallel representations of the same fact are a bug +- hash-order-dependent output is a bug -- Optimize for clean typed seams and correct ownership, not for preserving historical plugin boundaries. -- `r2plugin` is an integration surface, not the place to recover missing semantics from downstream command output. -- If a clean solution requires moving logic across crates or across the `../radare2` boundary, do it at the owning layer. -- Prefer principled rewrites over incremental "fix it later in the plugin/decompiler/export path" patches. -- Cross-component cooperation is required; cross-component policy duplication is not. +If you cannot explain the asymptotic and practical cost of a new analysis path, +you do not understand it well enough to land it. -## Ownership Boundaries +### Efficiency Rules -These boundaries are the main architectural guardrail. The crates should work together as one pipeline, but each fact still needs one canonical owner. Most recent churn happened when multiple crates tried to "help" each other by owning the same policy. +1. Prefer canonical summaries over re-analysis. +2. Prefer incremental updates over full rebuilds. +3. Prefer typed caches over ad hoc memoization. +4. Prefer `BTreeMap` / `BTreeSet` when ordering affects output or tests. +5. Prefer one richer pass over many weak overlapping passes. +6. Prefer explicit budgets and refusal modes over silent blowups. +7. Prefer stronger upstream facts over downstream heuristics. -- `r2ssa` owns SSA construction, decompile-safe SSA preparation, determinism, and SSA-local transforms. -- `r2sym` owns symbolic state modeling, taint propagation, summaries, path exploration, and solver-facing symbolic policy. -- `r2types` owns signature parsing/normalization, type inference, layout inference, external context parsing, field lookup policy, and canonical merged type facts. -- `r2dec` owns decompiler semantic facts, lowering, structuring, and rendering only. -- `r2plugin` owns orchestration, JSON/FFI, command dispatch, and radare2 integration glue only. -- `../radare2` owns core analysis facts, typed collectors, and library seams that should exist for radare2 consumers generally, not just this workspace. +## Architectural Stance -Do not put the same policy in two crates "temporarily". That temporary state lasted a long time and created most of the recent regressions. +This system should move toward a gold-standard analysis architecture even when +that requires invasive refactors. -When multiple layers need the same fact, push that fact toward its canonical owner and expose it through a typed contract instead of rebuilding it independently in each crate. +- It is acceptable to redesign contracts, move logic across crates, or change + FFI and `../radare2` seams when the result is cleaner. +- Do not preserve a bad abstraction because it already exists. +- Do not add end-stage hacks in `r2plugin` or `r2dec` to hide missing upstream + semantics. +- If a crate is carrying policy it should not own, move that policy. -## Typed `radare2` Context Seam +The right question is not "what is the smallest diff?" It is "what is the +cleanest owner and the cheapest long-term design?" -The plugin used to pull decompiler/type context by shelling out to radare2 commands and re-parsing JSON from `afcfj`, `afvj`, and `tsj`. That was convenient, but it created duplicate ownership and brittle parsing policy in the plugin. +## Task-Start Protocol -Current rule: +For any non-trivial change, follow this order: + +1. Identify the user-visible behavior or broken invariant. +2. Identify the canonical owner: `../radare2`, lift, SSA, symex, types, + decompiler, export, CLI, or plugin. +3. Identify the existing typed contract to extend before creating a new one. +4. State the complexity target: lookup, traversal, search, cache, summary. +5. Push facts upstream to the owner instead of reconstructing them downstream. +6. Add the smallest deterministic test at the layer users actually exercise. +7. If the seam crosses into `../radare2`, validate both repos before claiming + the work is complete. -- For decompiler/type analysis, use the typed function/base-type collector API in `../radare2`. -- Keep user-visible commands like `afcfj`, `afvj`, and `tsj`, but do not use them as an internal plugin data source. -- If the plugin lacks a typed field from radare2, add it to the `r_anal` API instead of layering more command parsing into `r2plugin`. -- Prefer one consolidated external-context payload across the C/Rust FFI boundary rather than multiple partially overlapping JSON blobs. -- Treat `../radare2` as available for coordinated changes. If the right fix needs new FFI, collector APIs, or analysis metadata, add them there and wire them through cleanly. +## Ownership Boundaries -This seam exists to keep `r2plugin` orchestration-only and to keep type/signature/layout policy out of the plugin. +Use this map by default: + +- `../radare2` + - core analysis facts + - typed collectors + - native analysis metadata and persistence + - library seams that should exist for radare2 consumers generally +- `crates/r2il` + - canonical IL data model and serialization +- `crates/r2sleigh-lift` + - Sleigh/P-code lifting + - register naming + - disassembly formatting + - ESIL formatting +- `crates/r2ssa` + - SSA construction + - phi handling + - dominators / def-use + - prepared function facts + - determinism and SSA-local transforms +- `crates/r2sym` + - symbolic state + - semantic artifacts + - evidence algebra + - query planning + - summaries and replay + - solver-facing semantic policy +- `crates/r2types` + - signature parsing and normalization + - type inference + - layout inference + - external type context normalization + - canonical `FunctionTypeFacts` + - canonical combined `FunctionFacts` +- `crates/r2dec` + - lowering + - semantic interpretation of canonical facts + - structuring + - rendering +- `crates/r2sleigh-export` / `crates/r2sleigh-cli` + - shared export/CLI plumbing +- `r2plugin` + - command dispatch + - JSON shaping + - FFI + - radare2 integration glue + +Do not let the same policy exist in two crates "for now." + +## Canonical Contracts + +These are the preferred subsystem seams: + +- `r2ssa::SsaArtifact` and `PreparedFunctionFacts` + - canonical SSA/dataflow preparation +- `r2sym::SemanticArtifact` + - canonical semantic artifact +- `r2sym::SemanticEvidence` + - canonical evidence carrier +- `r2sym::{ArtifactBuildPlan, QueryPlan, TargetQueryRoutePlan, TypePlan, DecompilePlan}` + - canonical plan surfaces +- `r2types::FunctionTypeFacts` + - canonical type/layout/signature payload +- `r2types::FunctionFacts` + - canonical combined type+semantic payload +- `r2dec::SemanticRoutePlan` + - renderer route selected from canonical upstream capabilities + +If a caller needs more information, extend these contracts instead of creating +parallel wrappers. + +## Typed `radare2` Seam + +The plugin must not parse `afcfj`, `afvj`, `tsj`, or similar command output as +an internal data source. + +Rules: + +- use the typed function/base-type collector APIs in `../radare2` +- keep user-visible commands, but do not use them as plugin internals +- if a typed field is missing, add it in `../radare2` +- prefer one consolidated typed context payload over multiple overlapping JSON + blobs + +If the right fix belongs in `../radare2`, implement it there. + +## Plugin Philosophy + +The public product should be workflow-oriented, not command-oriented. + +The plugin should: + +- improve `aa`, `af`, `pdfj`, `pdd`, type views, and existing radare2 analysis + surfaces +- keep a small public command surface +- treat engine-inspection commands as debug/maintainer tools +- move knobs to config (`e anal.sleigh.*`) where that is cleaner than inventing + verbs + +If you are about to add a new command, stop and ask: + +1. should this be automatic? +2. should this enrich an existing radare2 view instead? +3. should this be config rather than a verb? +4. is this just a debug surface? -## Canonical Type Contract +## Rewrite Bias -`r2types::FunctionTypeFacts` is the only canonical type/layout/signature artifact for the decompiler path. +When current architecture blocks correctness, composability, or efficiency: -- `r2types` may consume local inference artifacts, radare2 external context, and decompiler-emitted semantic field-access facts. -- `r2dec` should consume `FunctionTypeFacts` only. Do not add back public `TypeInference`, duplicate `FunctionType`, or decompiler-side external-signature / stack-var setters. -- `r2plugin` may gather inputs and serialize outputs, but it must not own signature merge policy, external/local struct reconciliation, or layout query policy. +- rewrite the seam +- move the owner +- shrink duplicated policy +- reshape FFI if needed +- change module layout if needed -If a caller needs more type information, extend `FunctionTypeFacts` or the `r2types` query contract instead of creating a second type wrapper layer elsewhere. +Avoid: -## Rewrite Bias +- plugin-side reparsing +- decompiler-side type policy +- consumer-local semantic policy that should live in `r2sym` +- compatibility shims that silently become permanent -When the current architecture blocks correctness, composability, or maintainability: - -- Prefer rewriting the seam, contract, or owning crate over adding compensating logic at the end of the pipeline. -- It is acceptable to redesign APIs, move logic across crates, reshape FFI, or perform cross-repo refactors involving `../radare2`. -- Do not preserve a bad abstraction just because it already exists. -- The goal is a better end-to-end radare2 component, not the smallest possible diff. -- Avoid plugin-side or decompiler-side "final fixups" that only exist to hide missing upstream semantics. - -## Rust Typing Rules For Rewrites - -Recent work went better once ownership seams stopped relying on stringly maps and implicit conventions. - -- Prefer enums, newtypes, named structs, and small typed input structs over raw `HashMap` plus comments. -- Use traits at crate seams where the contract matters, for example layout queries or semantic fact access. -- Use `BTreeMap` / `BTreeSet` whenever iteration order can affect decompiler output, local naming, snapshots, or SSA determinism. -- Use `From` / `TryFrom` for typed boundary conversions instead of ad hoc parsing spread across crates. -- Keep JSON-only types at the JSON boundary. Internal analysis code should use typed Rust models first and serialize late. -- Avoid boolean mode flags when there are more than two semantic states; use enums instead. - -## Core Types and Entry Points - -| Type / Function | Location | Purpose | -|-----------------|----------|---------| -| `Varnode` | `crates/r2il/src/varnode.rs` | Sized data location: reg/mem/const/unique | -| `SpaceId` | `crates/r2il/src/space.rs` | Address-space enum | -| `R2ILOp` | `crates/r2il/src/opcode.rs` | Semantic IL op enum | -| `R2ILBlock` | `crates/r2il/src/opcode.rs` | One-instruction IL block | -| `ArchSpec` | `crates/r2il/src/serialize.rs` | Architecture metadata | -| `Disassembler` | `crates/r2sleigh-lift/src/disasm.rs` | libsla wrapper and P-code lifting | -| `format_op()` / `op_to_esil()` | `crates/r2sleigh-lift/src/esil.rs` | Text and ESIL formatting | -| `run_action_output()` | `crates/r2sleigh-cli/src/main.rs` | CLI action/format dispatcher | -| export helpers | `crates/r2sleigh-export/src/lib.rs` | Shared export pipeline used by CLI/plugin | -| `SSAVar` | `crates/r2ssa/src/var.rs` | Versioned SSA variable | -| `SSAOp` | `crates/r2ssa/src/op.rs` | SSA operation enum | -| `to_ssa()` | `crates/r2ssa/src/block.rs` | R2IL block -> SSA block | -| `DefUseInfo` | `crates/r2ssa/src/defuse.rs` | Def-use analysis result | -| `FunctionSSABlock` / `SSAFunction` | `crates/r2ssa/src/function.rs` | Function-level SSA with phi nodes | -| `AnalysisResult` and type passes | `crates/r2types/src/` | Type inference payloads | -| `CExpr` / `CStmt` | `crates/r2dec/src/ast.rs` | Decompiler AST | -| `FoldingContext` | `crates/r2dec/src/fold/` | Expression folding and simplification | -| `LowerCtx` | `crates/r2dec/src/analysis/lower.rs` | SSA-to-expression lowering | -| plugin Rust surface | `r2plugin/src/lib.rs` | JSON commands, analysis helpers, FFI | -| plugin C wrapper | `r2plugin/r_anal_sleigh.c` | radare2 callbacks and command dispatch | - -## Build and Run +## Optimization Checklist + +Before landing any non-trivial change, check these explicitly: + +1. Did I add a second owner for an existing fact? +2. Did I add a repeated full-function walk? +3. Did I add repeated solver work that should be cached or summarized? +4. Did I add a new JSON-shaped internal type where a Rust type should exist? +5. Did I add ordering nondeterminism? +6. Did I move policy downstream instead of upstream? +7. Did I preserve a bad seam instead of rewriting it? + +If any answer is "yes", the design is probably wrong. + +## Build And Run Use these commands from the workspace root unless noted otherwise. @@ -202,34 +267,11 @@ cargo e2e-test Notes: -- `cargo run --features x86 -- ...` is stale at the workspace root; use `-p r2sleigh-cli --bin r2sleigh`. -- `cargo install-plugin` is defined in `.cargo/config.toml` and wraps `r2plugin/src/bin/r2sleigh-plugin-install.rs`. -- x86/x86-64 disassembly still needs at least 16 bytes of input; pad with zeros. -- If you touch the typed radare2 seam in `r2plugin/r_anal_sleigh.c`, plan to build and test `../radare2` too. Fix the library seam there instead of adding more plugin-side command parsing. - -## Architecture Support - -Feature matrices are not identical across crates. - -- `r2plugin` currently exposes `x86`, `arm`, `riscv`, and `all-archs`. -- `r2sleigh-cli` currently exposes `x86`, `arm`, `mips`, `riscv`, and `all-archs`. -- There is still some compatibility code for `mips` in shared/plugin code, but the plugin crate itself is currently feature-gated around `x86`, `arm`, and `riscv`. -- If you change architecture wiring, inspect both `r2plugin/Cargo.toml` and `crates/r2sleigh-cli/Cargo.toml`. - -For radare2 auto-selection, the plugin currently maps common values like: - -- `anal.arch=x86`, `anal.bits=64` -> `x86-64` -- `anal.arch=x86`, `anal.bits=32` -> `x86` -- `anal.arch=arm`, `anal.bits=32` -> `arm` -- `anal.arch=arm`, `anal.bits=64` or `anal.arch=arm64` / `aarch64` -> `aarch64` -- `anal.arch=riscv`, `anal.bits=32` -> `riscv32` -- `anal.arch=riscv`, `anal.bits=64` -> `riscv64` - -Manual override stays: - -```bash -r2 -qc 'a:sla.arch x86-64; a:sla.arch' /bin/ls -``` +- `cargo run --features x86 -- ...` at the workspace root is stale; use + `-p r2sleigh-cli --bin r2sleigh` +- `cargo install-plugin` is defined in `.cargo/config.toml` +- x86/x86-64 lifting still expects 16 bytes minimum +- if you touch the typed `../radare2` seam, build and test `../radare2` too ## Testing Policy @@ -238,42 +280,30 @@ r2 -qc 'a:sla.arch x86-64; a:sla.arch' /bin/ls Use `tests/r2r` for new regressions involving: - plugin commands such as `a:sla.*`, `a:sym.*`, `pdd`, `pdD` -- stable JSON/text/ESIL outputs that are worth exact normalized snapshots -- CFG/SSA/def-use/type payload shape when structural assertions are the better fit -- command UX, help text, error text, and normalized decompiler output -- radare2 integration behavior that is best expressed as command snapshots +- stable JSON/text/ESIL output +- CFG / SSA / def-use / type payload shape +- command UX and error text +- radare2 integration behavior Why: - faster feedback -- better snapshot-style diffs -- already normalized around radare2 command execution -- consistent with how users exercise the plugin +- better diffs +- already normalized around real radare2 command execution ### Use `tests/e2e` only when `r2r` is the wrong tool Keep Rust E2E tests for: - FFI / ABI checks -- CLI `run` export semantics -- analysis-quality thresholds or benchmark-style assertions -- cases that need direct Rust-side orchestration rather than command snapshots +- CLI export semantics +- benchmark-style assertions +- direct Rust orchestration cases that `r2r` cannot express cleanly -`tests/e2e/integration_tests.rs` still exists, but it is not the default place for new plugin regression coverage. +## Required Validation Bar -## Adding New Tests - -### Preferred workflow for new features - -1. Implement the feature. -2. If the user-facing behavior is visible through radare2 commands, add or update an `r2r` case. -3. If the feature needs a specific binary pattern, add or update a fixture source under `tests/e2e/`. -4. Run `make -C tests/r2r run`. -5. If the change also affects CLI semantics, FFI, or benchmark-style behavior, run `cargo e2e-test` or a focused `tests/e2e` module. - -### Required validation for ownership / seam changes - -If you touch `r2ssa`, `r2sym`, `r2types`, `r2dec`, `r2plugin`, or the typed `../radare2` context seam, the minimum validation bar is: +If you touch `r2ssa`, `r2sym`, `r2types`, `r2dec`, `r2plugin`, or the typed +`../radare2` seam, the minimum validation bar is: ```bash cargo test -p r2ssa @@ -290,8 +320,6 @@ make -C r2plugin RUST_FEATURES=all-archs install make -C tests/r2r run ``` -During local iteration, a focused subset is fine. Do not mark an ownership or seam change complete until the full relevant validation bar is green. - If you also changed `../radare2`, add: ```bash @@ -301,158 +329,60 @@ cd ../radare2/test && r2r -L -o results.json db/cmd/cmd_af db/json/json1 Do not claim the seam is fixed without both sides being green. -### Where to put new `r2r` cases +## `r2r` Placement Guide `tests/r2r/db/extras/r2sleigh_core` -- very small, deterministic instruction-level checks -- good for `a:sla.json`, `a:sla.regs`, `a:sla.mem`, `a:sla.vars` +- small deterministic instruction-level checks `tests/r2r/db/extras/r2sleigh_integration_fast` -- function-level plugin behavior that should stay quick -- good for `a:sla.ssa.func`, `a:sla.cfg.json`, `a:sla.dom`, `a:sla.types`, `a:sla.opvals` +- function-level behavior that should stay quick `tests/r2r/db/extras/r2sleigh_integration_extended` -- slower or heavier coverage -- symbolic execution, taint, complex decompilation, larger binaries - -### `r2r` test authoring tips - -- Prefer exact full-output snapshots for stable user-facing surfaces after normalization. -- Use `tests/r2r/normalize_snapshot.py` or `jq -S -c` to canonicalize stable output before snapshotting. -- Prefer structural assertions only when ordering, naming, or formatting is expected to evolve, especially for SSA, symex, taint, and large CFG/DOM payloads. -- Keep these args unless you have a reason not to: - -```text --e scr.color=false -e log.level=0 -e bin.relocs.apply=true -``` - -- `tests/r2r/Makefile` builds the fixture binaries from `tests/e2e/` and symlinks them into `tests/r2r/bins/`. -- If you add a brand-new fixture binary, update `tests/r2r/Makefile` so the harness links it. - -Minimal `r2r` example: - -```text -NAME=instruction_regs_snapshot -FILE=bins/vuln_test_x86 -ARGS=-e scr.color=false -e bin.relocs.apply=true -EXPECT=</dev/null -a:sla.regs | python3 normalize_snapshot.py regs -EOF_CMDS -RUN -``` - -### Fixture guidance - -Use the smallest fixture that exercises the behavior: - -- `tests/e2e/vuln_test.c` for focused plugin features and common analysis cases -- `tests/e2e/stress_test.c` for larger decompiler/symbolic/type cases -- `tests/e2e/test_func.c` for small structured helper functions -- `tests/e2e/sym_test.c` for symbolic-execution-specific patterns - -When you add a fixture function: - -1. Add the function with a short comment explaining what it exercises. -2. Wire it into the fixture's `main()` or other entry path if the tests need runtime access. -3. Add or update the corresponding `r2r` snapshot. +- heavier symbolic, taint, decompilation, and larger-CFG coverage ## Common Change Workflows -### Add a new R2IL opcode - -1. Add the variant to `crates/r2il/src/opcode.rs`. -2. Teach the lifter to emit it in `crates/r2sleigh-lift/src/disasm.rs`. -3. Add text and ESIL formatting in `crates/r2sleigh-lift/src/esil.rs`. -4. Check any export path that formats or serializes the new op through `crates/r2sleigh-export/src/lib.rs` or CLI output. -5. Add tests. Prefer an `r2r` snapshot when the opcode is visible through plugin output. - -### Add SSA support for a new op - -1. Add the SSA variant to `crates/r2ssa/src/op.rs`. -2. Convert it in `crates/r2ssa/src/block.rs`. -3. Update `dst()` and `sources()` in `crates/r2ssa/src/op.rs`. -4. Add function-level or instruction-level coverage, usually via `a:sla.ssa`, `a:sla.ssa.func`, or `a:sla.defuse`. - -### Add decompiler support for a new SSA op +### Change the type / decompiler seam -1. Add lowering in `crates/r2dec/src/analysis/lower.rs` if needed. -2. Add fold/codegen support under `crates/r2dec/src/fold/`. -3. Test through `a:sla.dec` snapshots and add direct Rust tests when local folding behavior is easier to assert there. +1. Choose the owner. If the answer is "more than one", the design is wrong. +2. Extend `r2types` first for signatures, layouts, and type facts. +3. Keep `r2dec` on semantic interpretation and rendering only. +4. If the plugin needs more context, extend `../radare2` instead of parsing + more command JSON. +5. If the seam is wrong, redesign it across repos instead of layering adapters. +6. Add or update `r2r` before broad snapshot churn. -### Change the type / decompiler seam +### Change symbolic / query behavior -1. Start by deciding which crate should own the policy. If the answer is "more than one", the design is wrong. -2. Extend `r2types` contracts first when the change affects signatures, layouts, or external context. -3. Keep `r2dec` on semantic facts and rendering. If decompiler code needs to rediscover type/layout policy from rendered expressions, stop and move that logic upstream. -4. If the plugin needs more context from radare2, extend the typed `r_anal` seam in `../radare2` instead of parsing more command JSON. -5. If the existing seam is fundamentally wrong, redesign it across both repos instead of layering more adapters on top. -6. Add or update `r2r` coverage before broad snapshot churn. The right fix is usually a stronger semantic assertion, not a bigger snapshot. +1. Put semantic policy in `r2sym`. +2. Put evidence and ambiguity in canonical artifact/evidence types. +3. Put routing in canonical plans. +4. Let `r2types` / `r2dec` consume those plans; do not reinvent them. +5. Add solver-budget and determinism coverage where applicable. ### Add or change a plugin command -1. Rust-side command data shaping usually lives in `r2plugin/src/lib.rs`. -2. radare2 command dispatch and help text live in `r2plugin/r_anal_sleigh.c`. -3. Add or update `r2r` coverage for help text, happy path, and error path. +1. Decide whether the feature should really be automatic or radare2-native. +2. Rust-side data shaping usually lives in `r2plugin/src/lib.rs`. +3. C dispatch/help lives in `r2plugin/r_anal_sleigh.c`. +4. Add `r2r` coverage for help, happy path, and failure path. -### Add a new architecture +## Plugin Command Surface -1. Update the relevant crate feature flags. -2. Wire spec/disassembler creation in the CLI, plugin, and export surfaces that need it. -3. Add at least one focused test path for the new arch. -4. Prefer documenting only architectures that are actually wired and tested in the crate you changed. +Treat this as two tiers: -## Plugin Command Surface +- public / user-facing + - `a:sla` + - `a:sla.dec` + - `pdd`, `pdD` + - `a:sym.explore` + - `a:sym.solve` + - `a:sym.state` +- debug / engine inspection + - low-level IL / SSA / facts / plan / replay / path listing commands -Common instruction-level commands: - -| Command | Purpose | -|---------|---------| -| `a:sla` | status / help | -| `a:sla.info` | current architecture info | -| `a:sla.arch [name]` | get or set Sleigh arch override | -| `a:sla.json` | raw r2il for current instruction | -| `a:sla.regs` | read/write registers | -| `a:sla.opvals` | analysis src/dst register view | -| `a:sla.mem` | memory accesses | -| `a:sla.vars` | varnodes | -| `a:sla.ssa` | instruction SSA | -| `a:sla.defuse` | instruction def-use | - -Function-level commands: - -| Command | Purpose | -|---------|---------| -| `a:sla.ssa.func` | function SSA with phi nodes | -| `a:sla.ssa.func.opt` | optimized function SSA | -| `a:sla.defuse.func` | function-wide def-use | -| `a:sla.dom` | dominator tree | -| `a:sla.slice ` | backward slice | -| `a:sla.types` | type-inference payload | -| `a:sla.taint` | taint analysis | -| `a:sla.sym` | symbolic summary | -| `a:sla.sym.paths` | explored symbolic paths | -| `a:sla.sym.merge [on|off]` | symbolic merge toggle | -| `a:sla.dec [name|addr]` | decompile | -| `pdd`, `pdD` | aliases for `a:sla.dec` | -| `a:sla.cfg` | ASCII CFG | -| `a:sla.cfg.json` | CFG JSON | - -Targeted symbolic commands: - -| Command | Purpose | -|---------|---------| -| `a:sym.explore ` | explore paths reaching target | -| `a:sym.solve ` | solve concrete input for target | -| `a:sym.state` | show cached symbolic state | - -Important: - -- Use `a:sym.solve`, not the old `a:sla.sym.solve` spelling. -- Use `a:sla.cfg.json` when you want stable structured assertions. +Do not expand the public surface casually. Prefer deeper integration over more +verbs. ## Two SSA Block Types @@ -465,8 +395,6 @@ There are two different block types in `r2ssa`: `r2dec` works with `FunctionSSABlock`. -When writing direct decompiler tests, build `FunctionSSABlock` values directly rather than assuming a convenience constructor exists. - ## File Quick Reference | File | Edit this when... | @@ -474,26 +402,14 @@ When writing direct decompiler tests, build `FunctionSSABlock` values directly r | `crates/r2il/src/opcode.rs` | adding or changing IL ops | | `crates/r2sleigh-lift/src/disasm.rs` | changing P-code lifting or register naming | | `crates/r2sleigh-lift/src/esil.rs` | changing text or ESIL rendering | -| `crates/r2sleigh-export/src/lib.rs` | changing shared export formatting or action plumbing | -| `crates/r2sleigh-cli/src/main.rs` | changing CLI commands or action/format routing | -| `crates/r2ssa/src/op.rs` | changing SSA operations | -| `crates/r2ssa/src/block.rs` | changing SSA conversion | -| `crates/r2ssa/src/function.rs` | function SSA / phi handling | -| `crates/r2ssa/src/defuse.rs` | changing def-use analysis | -| `crates/r2sym/src/` | changing symbolic execution or taint internals | -| `crates/r2types/src/` | changing type inference | -| `crates/r2types/src/context.rs` | changing typed external context imported from radare2 | -| `crates/r2dec/src/fold/` | changing decompiler folding and lowering | -| `crates/r2dec/src/codegen.rs` | changing C output formatting | +| `crates/r2ssa/src/` | changing SSA construction, def-use, prepared facts | +| `crates/r2sym/src/` | changing semantic artifacts, query, summaries, replay, solver policy | +| `crates/r2types/src/` | changing type inference, layouts, canonical function facts | +| `crates/r2dec/src/` | changing lowering, structuring, rendering | | `r2plugin/src/lib.rs` | changing plugin-side Rust logic and JSON payloads | -| `r2plugin/r_anal_sleigh.c` | changing radare2 callbacks, command help, dispatch | -| `../radare2/libr/include/r_anal.h` | changing the typed function/base-type collector API used by the plugin | -| `../radare2/libr/anal/fcn.c` | implementing typed function-context collection | -| `../radare2/libr/anal/type.c` | implementing typed base-type collection / parity with `tsj` | -| `tests/r2r/Makefile` | changing r2r harness setup or fixture linking | -| `tests/r2r/db/extras/` | adding or updating snapshot regressions | -| `tests/e2e/README.md` | checking when to use Rust E2E vs `r2r` | -| `tests/e2e/integration_tests.rs` | legacy semantic/FFI coverage, not the default for new snapshots | +| `r2plugin/r_anal_sleigh.c` | changing command dispatch/help or C-side integration | +| `../radare2/libr/include/r_anal.h` | changing typed collector APIs used by the plugin | +| `tests/r2r/db/extras/` | adding or updating regression snapshots | ## Gotchas @@ -501,20 +417,20 @@ When writing direct decompiler tests, build `FunctionSSABlock` values directly r 2. ESIL subtraction must use ASCII `-`, not Unicode minus. 3. `Const` means literal; `Unique` means temporary SSA-like storage, not memory. 4. Width mismatches usually need explicit sign/zero extension. -5. Register aliasing needs deterministic policy in output and recovery. +5. Register aliasing must stay deterministic. 6. `#[no_mangle]` is now `#[unsafe(no_mangle)]` under Rust 2024. -7. Plugin, CLI, and export crate feature matrices are not identical. -8. Prefer `a:sla.cfg.json`, `a:sla.types`, and `jq`-normalized checks over raw pretty-printed output in snapshots. -9. Use `r2dec/address.rs::parse_address_from_var_name()` for consistent `const:` / `ram:` parsing. -10. Taint summaries intentionally filter noisy stack/frame-pointer labels. -11. If you add a new fixture binary, remember both the build step and the `tests/r2r/bins/` symlink step. -12. If output stability matters, assume hash-order nondeterminism is a bug. Use deterministic ordering in SSA facts, decompiler facts, and local naming. -13. On the decompiler/type path, tests may now see `r2types::CTypeLike` rather than a local `r2dec` type wrapper. Do not reintroduce `r2dec` type ownership just to make an old test compile. +7. Plugin, CLI, and export feature matrices are not identical. +8. If output stability matters, hash-order nondeterminism is a bug. +9. Use `r2dec/address.rs::parse_address_from_var_name()` for consistent + `const:` / `ram:` parsing. +10. On the decompiler/type path, do not reintroduce `r2dec` type ownership just + to make an old test compile. ## Useful References - `README.md` for current build and testing quick-start -- `tests/e2e/README.md` for the split between `r2r` and Rust E2E +- `ROADMAP.md` for current system direction and priority order +- `tests/e2e/README.md` for the split between Rust E2E and `r2r` - `doc/` for IL, SSA, ESIL, decompiler, taint, symex, and type-system notes - radare2 ESIL docs: - Ghidra P-code reference: diff --git a/README.md b/README.md index 513f6a3..18fdc52 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,7 @@ Documentation | [doc/taint.md](doc/taint.md) | Taint analysis | | [doc/symex.md](doc/symex.md) | Symbolic execution | | [doc/plugin.md](doc/plugin.md) | radare2 plugin and commands | +| [doc/plugin-rfe-2026.md](doc/plugin-rfe-2026.md) | plugin feature and integration RFE | | [doc/types.md](doc/types.md) | Type inference | | [doc/testing.md](doc/testing.md) | Testing strategy | diff --git a/ROADMAP.md b/ROADMAP.md index 7c0f4cf..16202b9 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,277 +1,389 @@ r2sleigh Roadmap ================ -> Vision: r2sleigh is not a separate plugin the user needs to know about. -> It is the **analysis brain** of radare2 — transparently lifting, typing, -> decompiling, tainting, and solving every function the user touches. -> The user runs `aaa`, `a:sla.dec`, or just browses code, and the best -> analysis in the industry happens automatically behind the scenes. - -Current State (Feb 2026) -------------------------- - -~200 tests passing across 8 crates. 20+ plugin commands working in radare2. - -Working features: -- 60+ R2IL opcodes from Ghidra Sleigh specifications -- Full SSA pipeline: CFG, dominator tree, phi nodes, optimization (SCCP, DCE, CSE, copy-prop, inst-combine) -- Z3-backed symbolic execution with path exploration -- SSA-based taint analysis with automatic radare2 integration during aaaa -- Decompiler producing C code with expression folding, predicate simplification, for-loops, switches, string literals, and symbol resolution -- Constraint-based type inference with struct/signature support -- Backward slicing -- 20+ radare2 plugin commands with automatic analysis hooks -- Deep integration callbacks: `analyze_fcn`, `recover_vars`, `get_data_refs`, `post_analysis` - -Supported architectures: x86, x86-64, ARM, MIPS. - -Planned Features ----------------- +> Vision: `r2sleigh` is not a sidecar plugin with many commands. +> It is the analysis brain of radare2: one typed subsystem that lifts, +> structures, solves, types, summarizes, and renders functions coherently. + +North Star +---------- + +The system we are aiming for has these properties: -### 2026 Priority Reset (Maintainer Feedback, Execution Order) - -This section defines the actual implementation order for upcoming work. -Phase numbering below remains as thematic grouping, but delivery priority is: -`P0 (Now) -> P1 (Next) -> P2 (Later)`. - -| Priority | Theme | Why now | Concrete deliverables | -|---|---|---|---| -| P0 | **R2IL Foundation Hardening** | Avoid design debt before adding major features | Keep core IL minimal, add optional metadata (`storage_class`, pointer/type hints, memory attributes, richer endianness model), document semantics and compatibility rules | -| P0 | **Unified Output + One-Liner UX** | Fast iteration for developers and easier maintainer review | Single export pipeline for C-like / r2-command-like / JSON outputs, plus CLI one-liners to run lift/SSA/defuse/dec actions directly | -| P0 | **RISC-V Support** | Higher ecosystem value than MIPS for new users | Add riscv feature flag, disassembler wiring, register profile/calling convention integration, plugin + e2e coverage | -| P1 | **Memory Semantics Extensions (VEX-inspired, scoped)** | Improves analysis fidelity without overhauling IL | Add `MemoryOrdering` + atomic/fence/guarded ops where liftable; compare behavior with VEX and document differences | -| P1 | **Float/Vector + Encoding Path** | Needed for correctness on modern binaries | Integrate r2 float encoding model, add float/vector metadata and staged SIMD support | -| P1 | **Hardware/Memory Topology Modeling** | Needed for firmware and embedded workflows | MMIO/IO port classification, const/permission/range attributes, segmented/banked memory policy and tests | -| P2 | **VM Architecture Perspective** | Valuable long-term but high scope | Prototype Dalvik/VM support as separate lifter module, validate whether core IL needs extension | -| P2 | **Advanced Execution Models** | Niche/high-complexity | VLIW parallel group representation and deeper scheduling semantics | - -#### Maintainer Points -> Priority Mapping - -| Maintainer point | Priority | Decision | -|---|---|---| -| Multiple address spaces + TLS | P0 | Keep `SpaceId` stable; represent TLS via optional storage metadata first | -| RISC-V over MIPS | P0 | Promote RISC-V to immediate architecture milestone | -| VM archs (Dalvik/JVM) | P2 | Explore in separate lifter crate, avoid premature core IL changes | -| FPU/vector + any float encoding | P1 | Stage through metadata + encoding integration, then expand op support | -| Atomic ops / guards / VEX comparison | P1 | Add scoped memory-ordering features with explicit doc comparison | -| MMIO + IO ports | P1 | Add memory-class metadata and explicit IO semantics where available | -| Complex endianness modes | P0 | Replace binary endianness assumptions with richer enum/override model | -| VLIW parallel execution | P2 | Model as block-level parallel groups first | -| Pointer as attribute, not base type | P0 | Treat pointer-ness as semantic metadata/type hint | -| Const attrs (permissions, valid ranges) | P1 | Add optional memory attribute model | -| Memory banks / segmented memory | P1 | Keep `SegmentOp`; add policy/docs/tests for banks/segments | -| Switch as operation | P2 | Keep `switch_info` for now; revisit after SSA/CFG simplification pass | - -#### Near-Term Milestones (Next 3) - -1. **Milestone A (P0)**: R2IL metadata foundation + docs + compatibility tests. -2. **Milestone B (P0)**: Unified export formats + CLI one-liners for lift/ssa/defuse/dec. -3. **Milestone C (P0)**: End-to-end RISC-V support with plugin and e2e coverage. - -### Phase 1 — Seamless r2 Integration (make the seams invisible) - -The user should never feel they are using a separate tool. r2sleigh must -consume everything radare2 already knows and push results back into r2's -native data structures so every existing r2 command benefits. - -| # | Feature | Description | Effort | Impact | -|---|---------|-------------|--------|--------| -| 1.1 | **DWARF signature pipeline** | Verify DWARF-imported function signatures (via r2 `sdb_types` / `afcfj`) flow end-to-end into r2dec's `VariableRecovery` and `TypeInference`. DWARF variable names, parameter types, and return types must appear in decompiled output automatically. | Low | High | -| 1.2 | **DWARF struct/enum type feeding** | In `sleigh_cmd` (`a:sla.dec`), query r2's `tsj` for DWARF-imported structs/unions/enums and feed into `ExternalTypeDb`. The type solver already has `FieldAccess` constraints and `lookup_field_name`; wire them to real DWARF data. | Medium | High | -| 1.3 | **DWARF-assisted struct field recovery** | When type inference resolves `*(ptr+offset)` and the `ExternalTypeDb` has a matching struct with a field at that offset, emit `ptr->field` in decompiled C. Combines DWARF data + existing `detect_addr_pattern` + `lookup_field_name`. | Medium | High | -| 1.4 | **Transparent `pdd` alias** | Register `pdd` (or `pdD`) as an r2 command alias that calls `a:sla.dec` for the current function. Users get decompilation from the standard r2 command vocabulary without knowing r2sleigh exists. | Low | High | -| 1.5 | **Write-back inferred types to r2** | After decompilation or `aaaa`, push inferred struct shapes, function signatures, and variable types back into `sdb_types` (via `r_anal_save_parsed_type`/`r_anal_import_c_decls`). This means `t` commands, `afvt`, and future analysis passes all benefit. | Medium | High | -| 1.6 | **Global variable recognition** | Use SSA data-flow analysis to detect accesses to fixed RAM addresses, cross-reference with r2's `r_anal_global_get`/flags, and emit named globals in decompiled output instead of raw hex constants. | Low | Medium | -| 1.7 | **Autoname functions from decompiler** | After decompilation, heuristically derive function names from string arguments to known calls (e.g., a function whose first call is `printf("usage: ...")` → `print_usage`). Feed names back via `r_anal_function_rename`. Integrate with `aan`. | Medium | Medium | -| 1.8 | **Calling convention auto-detection** | During `analyze_fcn`, determine calling convention (cdecl/stdcall/fastcall/sysv/win64/arm-aapcs) from SSA parameter-register usage patterns. Write back to r2's `afcc` so all downstream commands agree. Currently hardcoded to SysV x86-64. | Medium | Medium | - -### Phase 2 — Decompiler Quality (match Ghidra, exceed it) - -| # | Feature | Description | Effort | Impact | -|---|---------|-------------|--------|--------| -| 2.1 | **Phi node elimination** | Convert `phi(x1,x2)` to proper variable assignments at predecessor edges. Removes the last SSA artifacts from decompiled output. | Medium | High | -| 2.2 | **Register coalescing** | Merge `RAX_1`, `RAX_2`, ... into a single C variable when the live ranges don't interfere. Dramatically reduces variable clutter. | Medium | High | -| 2.3 | **Short-circuit operators** | Detect `if(a) { if(b) { X } }` → `if(a && b) { X }` and the OR variant. | Low | Medium | -| 2.4 | **Condition inversion / early return** | Prefer `if(!x) return;` over `if(x) { ...long body... }`. Reduces nesting. | Low | Medium | -| 2.5 | **No More Gotos** | Handle irreducible CFGs with region-based restructuring instead of gotos. The `structure.rs` already has region analysis; extend with node splitting or controlled duplication. | High | High | -| 2.6 | **Pointer type propagation** | Track pointer types through Load/Store chains. When `p = malloc(sizeof(Foo))`, propagate `Foo*` to all uses of `p`. | Medium | High | -| 2.7 | **Array access patterns** | Detect `base + i*stride` as `arr[i]`. The type solver already has `stride` detection in `detect_addr_pattern`; surface it in codegen. | Medium | Medium | -| 2.8 | **Enum constant folding** | When a comparison operand matches an enum variant from `ExternalTypeDb`, emit the enum name instead of the raw integer. | Low | Medium | -| 2.9 | **String constant propagation** | When a local variable is assigned a string address and only used in one call, inline the string literal at the call site. | Low | Medium | -| 2.10 | **sizeof() recovery** | Detect `malloc(N)` where N matches `sizeof(struct X)` from the type DB. Emit `malloc(sizeof(X))`. | Low | Low | - -### Phase 3 — Vulnerability Intelligence (the killer feature) - -No other open-source tool provides automatic, per-function vulnerability -assessment integrated directly into the reversing workflow. - -| # | Feature | Description | Effort | Impact | -|---|---------|-------------|--------|--------| -| 3.1 | **Vulnerability pattern library** | Detect buffer overflow, format string, UAF, double-free, integer overflow at IL/SSA level. Each pattern is a taint policy + SSA matcher. Ship as data files, not code. | Medium | Critical | -| 3.2 | **Risk scoring engine** | Assign per-function risk scores based on: sink severity × input reachability × sanitizer presence. Rank all functions by exploitability during `aaaa`. Write `sla.risk` flag + comment. | Medium | Critical | -| 3.3 | **Guided vuln discovery** | `a:sym.vuln ` — use symbolic execution to find concrete input reaching a dangerous sink (e.g., `gets()` or unchecked `memcpy`). Output includes input constraints in SMT-LIB2 and concrete model. | Medium | High | -| 3.4 | **Crypto detection** | Detect crypto algorithms by IL patterns: S-box constants (AES), round constants (SHA), Feistel structure. Flag functions as `sla.crypto.aes`, etc. | Medium | Medium | -| 3.5 | **Integer overflow detection** | Flag arithmetic operations on user-controlled values that lack bounds checks before use as array indices or allocation sizes. | Medium | High | -| 3.6 | **Path predicate export** | Export path constraints as SMT-LIB2 for external solvers or integration with fuzzing harnesses. | Low | Medium | - -### Phase 4 — Inter-Procedural Analysis (the hard problems) - -| # | Feature | Description | Effort | Impact | -|---|---------|-------------|--------|--------| -| 4.1 | **Function summaries** | Cache per-function symbolic summaries: which inputs affect which outputs, what gets tainted, what's returned. Enables inter-procedural without full inlining. | High | Critical | -| 4.2 | **Inter-procedural taint** | Taint analysis spanning function boundaries using summaries. `input:argv` reaching `strcpy` in a callee three levels deep. | High | Critical | -| 4.3 | **Call graph with data flow** | Build a call graph where edges carry data-flow information (which args of caller flow to which params of callee). | High | High | -| 4.4 | **Whole-program type inference** | Unify types across function boundaries: if `foo()` returns a `struct stat*` and `bar()` receives it, propagate the struct type into `bar`'s parameter. | High | High | -| 4.5 | **Context-sensitive decompilation** | When decompiling `foo(x)`, look at callers to determine likely type/range of `x`. Annotate decompiled output with "called from: ..." context. | High | Medium | - -### Phase 5 — Symbolic Execution & Concolic (the smart engine) - -| # | Feature | Description | Effort | Impact | -|---|---------|-------------|--------|--------| -| 5.1 | **Interactive symbolic execution** | `a:sym.explore` and `a:sym.solve` commands with user-specified targets, constraints, and hooks. | Medium | High | -| 5.2 | **Memory in solutions** | Include concrete memory layout (heap, stack, globals) in symbolic path output, not just register values. | Low | Medium | -| 5.3 | **Concolic execution** | Concrete + symbolic hybrid guided by ESIL traces from r2's debugger. Run the binary, record a trace, symbolically explore alternatives. | High | High | -| 5.4 | **Symbolic call stubs** | Auto-generate symbolic stubs for common libc functions (`strlen` returns symbolic length, `malloc` returns fresh symbolic pointer). | Medium | High | -| 5.5 | **Constraint caching** | Cache Z3 queries per function so repeated solves (e.g., during fuzzing integration) don't redundantly re-solve. | Medium | Medium | - -### Phase 6 — Platform & Architecture Expansion - -| # | Feature | Description | Effort | Impact | -|---|---------|-------------|--------|--------| -| 6.1 | **ABI/calling-convention model** | Abstract architecture-specific assumptions (arg registers, stack direction, alignment) into a data model. Currently hardcoded for SysV x86-64 in `variable.rs`, `types.rs`, `taint.rs`. | Medium | High | -| 6.2 | **RISC-V support** | Add RISC-V Sleigh spec + register profile + calling convention. **Execution priority: P0 (Milestone C).** | Medium | Medium | -| 6.3 | **AArch64 / ARM64 support** | Full ARM64 support with AAPCS64 calling convention. | Medium | Medium | -| 6.4 | **PPC / AVR / SPARC** | Additional architecture support with per-arch test fixtures. | Medium | Low | -| 6.5 | **Register naming policy** | Normalize register names, resolve overlapping aliases (RAX vs EAX vs AX vs AL). Use canonical names in decompiled output. | Low | Medium | -| 6.6 | **Floating-point type inference** | Properly distinguish float/double from integer types using SSA float opcodes. | Low | Low | - -### Phase 7 — Advanced Analysis & Research - -| # | Feature | Description | Effort | Impact | -|---|---------|-------------|--------|--------| -| 7.1 | **Memory/value-set analysis** | Alias-aware abstract interpretation tracking value ranges and pointer targets. Enables more precise taint, slicing, and decompilation. | High | High | -| 7.2 | **R2IL VM + event tracing** | Make R2IL executable with concrete values. Record execution traces with events (mem read/write, branch taken). Compare static vs dynamic analysis. | High | Medium | -| 7.3 | **Semantic diff** | Compare two functions (or two versions of a binary) for semantic differences at the SSA level. Highlight what changed in the decompiled output. | High | Medium | -| 7.4 | **Pattern matching DSL** | User-defined IL patterns for custom detection. "Find all functions that read from `[user_input + *]` and pass it to `exec*`." | Medium | Medium | -| 7.5 | **Incremental analysis** | When the user annotates a type or renames a variable, incrementally update SSA/taint/decompilation without re-lifting the whole function. | High | Medium | -| 7.6 | **Decompiler output diffing** | When types/signatures change, show a diff of the decompiled C output. Useful for iterative reverse engineering. | Low | Low | - -Integration Architecture +- one canonical IL substrate +- one canonical SSA/dataflow layer +- one canonical semantic artifact with explicit evidence +- one canonical combined `FunctionFacts` contract +- one planner surface for query, types, and decompilation +- one replay/trace validation loop +- one small public plugin surface that feels native to radare2 + +The metric is not command count. The metric is whether radare2 feels like it +gained one coherent, mathematically disciplined analysis engine. + +Current State (Apr 2026) ------------------------ -The key insight: r2sleigh hooks into radare2's analysis pipeline at every -stage, consuming r2's metadata and pushing results back. The user never -invokes r2sleigh directly — it's just "r2 but smarter." - -``` -radare2 analysis pipeline r2sleigh hooks -───────────────────────── ────────────── -aa (basic analysis) - └─ af (find functions) ──→ analyze_fcn: SSA + annotations - └─ afva (find vars) ──→ recover_vars: SSA-derived stack vars + reg args - └─ aar (find refs) ──→ get_data_refs: SSA-derived data/code/string refs - -aaa (deeper analysis) - └─ aan (autoname) ──→ [NEW] autoname from decompiler heuristics - └─ DWARF integration ──→ [NEW] DWARF types → ExternalTypeDb → decompiler - └─ afcfj (signatures) ──→ already consumed by a:sla.dec - └─ tsj (type structs) ──→ already consumed by a:sla.dec - -aaaa (experimental) - └─ post_analysis ──→ taint analysis + risk scoring + xrefs - └─ [NEW] ──→ write-back inferred types to sdb_types - └─ [NEW] ──→ write-back function signatures to afcc - └─ [NEW] ──→ flag risky functions with sla.risk.* - -User commands (transparent) - └─ pdd / pdD ──→ [NEW] alias to a:sla.dec - └─ a:sla.dec ──→ decompile with full context from r2 - └─ a:sla.taint ──→ taint current function - └─ a:sym.solve ──→ solve for reachability -``` - -### Data Flow: r2 → r2sleigh → r2 - -``` - ┌──────────────┐ - │ radare2 │ - │ │ - ┌─────────────────┤ sdb_types │◄──────── DWARF / PDB / user annotations - │ │ flags │ - │ │ xrefs │ - │ │ afcfj │ - │ │ afvj │ - │ │ tsj │ - │ │ aflj │ - │ └──────┬───────┘ - │ │ JSON - │ ▼ - │ ┌──────────────┐ - │ │ r2sleigh │ - │ │ │ - │ │ R2IL lift │ - │ │ SSA build │ - │ │ Type infer │ - │ │ Decompile │ - │ │ Taint │ - │ │ SymExec │ - │ └──────┬───────┘ - │ │ JSON + C strings - │ ▼ - │ ┌──────────────┐ - │ write-back ────►│ radare2 │ - │ types │ │ - │ signatures │ sdb_types ← inferred struct shapes - │ variables │ afcc ← detected calling convention - │ names │ flags ← taint/risk/crypto flags - │ xrefs │ xrefs ← taint-flow + data refs - │ comments │ comments ← taint summaries, risk scores - └─────────────────┤ afn ← auto-named functions - └──────────────┘ -``` - -Comparison ----------- +The core architecture reset is done. + +Completed high-value work: + +- canonical `r2sym::SemanticArtifact` ownership +- canonical evidence and plan surfaces in `r2sym` +- `FunctionFacts` as the combined type+semantic contract +- removal of legacy mirrored symbolic/type ownership +- planner-gated symbolic query routing +- target-local narrowing with explicit ambiguity handling +- decompiler planner/consumer split +- canonical VM summary routing in decompiler/plugin +- explicit semantic schema/cache versioning +- end-to-end plugin transport on canonical facts/plans +- strong `r2r` coverage and full validation bar + +The system is no longer missing foundations. The next work is about using those +foundations better across the whole stack. + +Strategic Principles +-------------------- + +1. One subsystem, not many tools. + - `r2il`, `r2ssa`, `r2sym`, `r2types`, `r2dec`, `r2plugin`, and + `../radare2` should behave like one analysis engine. + +2. Optimize for typed ownership, not command growth. + - The best improvements are deeper integration and stronger facts, not + additional verbs. + +3. Optimize for practical asymptotics. + - aim for `O(1)` / `O(log n)` lookups + - `O(n)` passes where possible + - bounded search where unavoidable + - summaries and incremental reuse everywhere else + +4. Prefer principled rewrites over downstream patchwork. + +5. Determinism beats cleverness. + +What Is Done +------------ + +### 1. Canonical Semantic Rewrite + +Done: + +- `r2sym` is the semantic owner +- evidence and ambiguity are first-class +- query routing is planner-gated +- target-local narrowing is authoritative and source-consistent +- symbolic artifact transport is canonical across plugin/export surfaces + +### 2. Consumer Hardening + +Done: + +- `r2types` consumes canonical semantics through `FunctionFacts` +- `r2dec` routes through planner + consumer modules +- VM decompile path is honest summary mode instead of pretending to be native +- plugin JSON/reporting is on canonical facts and plan fields + +### 3. Decompiler/Plugin Integration + +Done: + +- canonical semantic route planning +- VM summary rendering route +- combined facts/plan reporting +- strong `r2r` coverage for the new paths + +What Is Not Done +---------------- + +The biggest remaining gains are not foundational rewrites. They are whole-stack +intelligence improvements: + +- shared assumption model +- deeper reuse of interprocedural summaries across all consumers +- trace/replay as a first-class validation loop +- richer semantic type algebra +- stronger VM semantic rendering +- narrower and more native plugin surface + +Priority Order +-------------- + +The real implementation order from here is: + +`P0 assumptions -> P1 summary reuse -> P2 replay loop -> P3 semantic typing -> +P4 VM semantics -> P5 command-surface rationalization -> P6 incremental/perf` + +### P0 — Shared Assumption Model + +Goal: + +Turn the subsystem into an interactive reasoning engine instead of a static +batch analyzer. + +Deliverables: + +- canonical typed assumption model across `r2ssa`, `r2sym`, and `r2types` +- explicit persistence or transport seam through plugin and, if needed, + `../radare2` +- incremental recomputation of affected analyses +- assumptions influence: + - query narrowing + - branch feasibility + - type/layout recovery + - decompiler simplification + +Why this is highest ROI: + +- users often know one critical fact the engine does not +- this amplifies every existing subsystem instead of adding a silo + +Success criteria: + +- one assumption set changes query, types, and decompiler coherently +- assumptions are explicit, typed, serializable, and test-covered + +### P1 — Promote Interprocedural Summaries To Whole-Stack Inputs + +Goal: + +The summaries already present in `r2sym` should stop being mostly query power +and start being a shared strength across the subsystem. + +Deliverables: + +- summary-driven return/value-shape hints for `r2types` +- summary-driven helper-call simplification for `r2dec` +- summary-backed applicability/evidence surfaced in function facts +- stricter reuse of library and derived summaries instead of rediscovery + +Why: + +- interproc summary work already exists in `r2sym` +- downstream consumers currently leave too much value on the table + +Success criteria: + +- better out-param inference +- better return-shape inference +- better helper-call rendering +- fewer downstream local heuristics + +### P2 — Replay And Trace As First-Class Validation + +Goal: + +Make debugger state and replay checkpoints part of the normal semantic loop. + +Deliverables: + +- replay seeds as canonical engine input, not just expert-only path control +- typed import of debugger/trace state +- witness validation against replayed state +- static-vs-observed semantic mismatch reporting + +Why: + +- this is the cleanest bridge between static and dynamic reasoning +- replay infrastructure already exists and needs promotion, not reinvention -### vs radare2 (stock) +Success criteria: -r2sleigh adds: SSA form, phi nodes, def-use chains, dominator tree, -symbolic execution, taint analysis, path exploration, Z3 solving, typed -decompilation, constraint-based type inference, vulnerability detection, -risk scoring, automatic function naming from decompiler heuristics. +- replay and witness validation share canonical semantic state +- observed state can refine confidence without becoming semantic owner -### vs angr +### P3 — Semantic Type Algebra V2 -r2sleigh provides: zero-friction radare2 integration (no Python, no -separate process), CLI-first workflow, Sleigh specs (vs VEX), JSON -output for scripting, per-function taint during `aaaa`, decompiler -output, no Python overhead. angr has: mature inter-procedural analysis, -larger community, more memory models. +Goal: -### vs Ghidra +Make `r2types` consume more of the semantic artifact than memory terms alone. -r2sleigh provides: exposed SSA for scripting, integrated symbolic -execution, automatic taint analysis with risk scoring, vulnerability -pattern detection, native CLI operation, no JVM dependency, incremental -results during analysis. Ghidra has: more mature decompiler, -inter-procedural type propagation (which we're building in Phase 4), -larger architecture coverage. +Deliverables: -### vs Binary Ninja +- use `pre`, `post`, `control`, and `targets` alongside memory facts +- use diagnostics and residual reasons to refuse unsafe projections +- infer: + - out-params + - return shape + - field applicability + - layout confidence -r2sleigh provides: fully open source, no license cost, Sleigh specs -(broadest architecture coverage), integrated symbolic execution and -taint analysis, CLI-native workflow, r2pipe scriptability. Binary Ninja -has: polished GUI, MLIL/HLIL abstraction layers, commercial support. +Why: -**The goal**: combine the best of all four — Ghidra's Sleigh specs, -angr's symbolic execution, Binary Ninja's type inference quality, and -radare2's CLI-first hackability — into a single transparent analysis -engine that just works when you type `aaa`. +- current type recovery is useful but still too memory-led +- this is the biggest non-query value gap -References +Success criteria: + +- fewer unsafe struct candidates +- stronger return/out-param recovery +- cleaner agreement between types and decompiler output + +### P4 — VM Semantic Rendering V2 + +Goal: + +Move VM analysis from summary comments toward structured semantic rendering. + +Deliverables: + +- selector recovery +- handler graph summaries +- guarded transfer summaries +- switch-like pseudo-C from canonical VM semantics + +Non-goal: + +- do not start with a fake "full VM decompiler" + +Why: + +- current VM path is honest but leaves value on the table + +Success criteria: + +- VM functions render as structured semantic summaries, not just comments +- VM routes remain explicitly marked as summary-driven where appropriate + +### P5 — Public Surface Rationalization + +Goal: + +Make the plugin feel native to radare2 instead of exposing every internal stage +as a first-class user command. + +Deliverables: + +- define public vs debug command tiers +- move config-like behaviors to `e anal.sleigh.*` +- enrich existing radare2 views instead of growing more plugin verbs +- keep expert inspection surfaces available, but demoted + +Why: + +- command count is not a quality metric +- intelligent integration should reduce, not expand, user-visible surface area + +Success criteria: + +- fewer public commands +- stronger existing radare2 workflows +- less need for users to think in crate boundaries + +### P6 — Incremental And Performance Discipline + +Goal: + +Turn the subsystem into a cheaper engine to run repeatedly. + +Deliverables: + +- stronger summary caches +- fewer repeated full-function passes +- explicit invalidation and reuse boundaries +- budget-aware scheduling +- deterministic cache keys and cost-aware planners + +Why: + +- the architecture is now clean enough that the next gains come from reuse + +Success criteria: + +- repeated analysis gets cheaper +- large-CFG behavior stays bounded +- downstream consumers reuse upstream work instead of re-deriving it + +Per-Crate Direction +------------------- + +### `r2il` / `r2sleigh-lift` + +Keep the IL small, typed, and architecture-faithful. Extend only when new +semantics are truly canonical across the stack. + +### `r2ssa` + +Focus on: + +- prepared facts +- deterministic SSA +- incremental recomputation hooks +- assumption-aware control/dataflow preparation + +### `r2sym` + +Focus on: + +- semantic artifact authority +- evidence algebra +- query planning +- summaries +- replay +- witness generation +- summary composition across consumers + +### `r2types` + +Focus on: + +- semantic type algebra +- stronger `FunctionFacts` +- candidate ranking and refusal logic +- struct/signature/layout confidence + +### `r2dec` + +Focus on: + +- rendering from canonical plans/facts +- less local planning +- stronger helper/summary interpretation +- VM structured summary rendering + +### `r2plugin` + +Focus on: + +- orchestration +- JSON shaping +- radare2-native integration +- shrinking the public surface over time + +### `../radare2` + +Focus on: + +- typed collectors +- persistence of shared analysis metadata +- debugger/trace seams that should exist for all consumers + +Anti-Goals ---------- -- radare2 ESIL: https://book.rada.re/disassembling/esil.html -- Ghidra Sleigh: https://ghidra.re/courses/languages/html/sleigh.html -- P-code reference: https://ghidra.re/courses/languages/html/pcoderef.html +Do not spend roadmap energy on: + +- command count inflation +- plugin-side reparsing of existing commands +- parallel type or semantic owners +- decompiler-local policy that should live upstream +- pretending hard analyses are "`O(1)`" + +If a change improves the subsystem by deleting a command, moving an owner, or +rewriting a seam, that is progress. + +Acceptance Standard +------------------- + +The target plugin system should eventually have these properties: + +- a user can rely on normal radare2 workflows and quietly get better analysis +- symex, types, decompiler, and replay agree on the same facts +- assumptions update the whole subsystem coherently +- summaries are reused across crates instead of being rediscovered +- evidence and fallback reasons are visible and honest +- the public command surface is smaller and smarter, not larger + +That is the gold standard we should optimize toward.