use std::collections::BTreeMap;
use crate::{
analysis::{
constraints::Constraint,
memory::{MemoryLocation, MemoryState},
symbolic::{SymbolicExpr, SymbolicOp},
},
ir::{
function::SsaFunction,
ops::{CmpKind, SsaOp},
value::ConstValue,
variable::{SsaVarId, VariableOrigin},
},
target::Target,
PointerSize,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ControlFlow {
Continue(usize),
Terminal,
Unknown,
}
impl ControlFlow {
#[must_use]
pub fn target(&self) -> Option<usize> {
match self {
Self::Continue(block) => Some(*block),
_ => None,
}
}
#[must_use]
pub fn is_terminal(&self) -> bool {
matches!(self, Self::Terminal)
}
#[must_use]
pub fn is_unknown(&self) -> bool {
matches!(self, Self::Unknown)
}
}
#[derive(Debug, Clone, Default)]
pub struct EvaluatorConfig {
pub track_path: bool,
pub track_memory: bool,
pub strict_phi: bool,
}
impl EvaluatorConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn path_aware() -> Self {
Self {
track_path: true,
track_memory: true,
strict_phi: true,
}
}
#[must_use]
pub fn with_memory() -> Self {
Self {
track_path: false,
track_memory: true,
strict_phi: false,
}
}
#[must_use]
pub fn with_path_tracking(mut self) -> Self {
self.track_path = true;
self
}
#[must_use]
pub fn with_memory_tracking(mut self) -> Self {
self.track_memory = true;
self
}
#[must_use]
pub fn with_strict_phi(mut self) -> Self {
self.strict_phi = true;
self
}
}
#[derive(Debug, Clone)]
pub struct ExecutionTrace<T: Target> {
blocks: Vec<usize>,
states: Vec<Option<ConstValue<T>>>,
completed: bool,
limit: usize,
}
impl<T: Target> ExecutionTrace<T> {
#[must_use]
pub fn new(limit: usize) -> Self {
Self {
blocks: Vec::new(),
states: Vec::new(),
completed: false,
limit,
}
}
#[must_use]
pub fn blocks(&self) -> &[usize] {
&self.blocks
}
#[must_use]
pub fn states(&self) -> &[Option<ConstValue<T>>] {
&self.states
}
#[must_use]
pub fn is_complete(&self) -> bool {
self.completed
}
#[must_use]
pub fn len(&self) -> usize {
self.blocks.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.blocks.is_empty()
}
#[must_use]
pub fn last_block(&self) -> Option<usize> {
self.blocks.last().copied()
}
#[must_use]
pub fn hit_limit(&self) -> bool {
self.blocks.len() >= self.limit
}
fn record_block(&mut self, block_idx: usize, state: Option<ConstValue<T>>) {
self.blocks.push(block_idx);
self.states.push(state);
}
fn mark_complete(&mut self) {
self.completed = true;
}
}
#[derive(Debug, Clone)]
pub struct SsaEvaluator<'a, T: Target> {
ssa: &'a SsaFunction<T>,
values: BTreeMap<SsaVarId, SymbolicExpr<T>>,
predecessor: Option<usize>,
constraints: BTreeMap<SsaVarId, Vec<Constraint<T>>>,
config: EvaluatorConfig,
path: Vec<usize>,
memory: MemoryState<T>,
pointer_size: PointerSize,
local_state: BTreeMap<u16, SymbolicExpr<T>>,
arg_state: BTreeMap<u16, SymbolicExpr<T>>,
}
impl<'a, T: Target> SsaEvaluator<'a, T> {
#[must_use]
pub fn new(ssa: &'a SsaFunction<T>, ptr_size: PointerSize) -> Self {
Self::with_config(ssa, EvaluatorConfig::default(), ptr_size)
}
#[must_use]
pub fn with_config(
ssa: &'a SsaFunction<T>,
config: EvaluatorConfig,
ptr_size: PointerSize,
) -> Self {
Self {
ssa,
values: BTreeMap::new(),
predecessor: None,
constraints: BTreeMap::new(),
config,
path: Vec::new(),
memory: MemoryState::new(),
pointer_size: ptr_size,
local_state: BTreeMap::new(),
arg_state: BTreeMap::new(),
}
}
#[must_use]
pub fn path_aware(ssa: &'a SsaFunction<T>, ptr_size: PointerSize) -> Self {
Self::with_config(ssa, EvaluatorConfig::path_aware(), ptr_size)
}
#[must_use]
pub fn with_values(
ssa: &'a SsaFunction<T>,
values: BTreeMap<SsaVarId, ConstValue<T>>,
ptr_size: PointerSize,
) -> Self {
let exprs = values
.into_iter()
.map(|(k, v)| (k, SymbolicExpr::constant(v)))
.collect();
Self {
ssa,
values: exprs,
predecessor: None,
constraints: BTreeMap::new(),
config: EvaluatorConfig::default(),
path: Vec::new(),
memory: MemoryState::new(),
pointer_size: ptr_size,
local_state: BTreeMap::new(),
arg_state: BTreeMap::new(),
}
}
#[must_use]
pub fn pointer_size(&self) -> PointerSize {
self.pointer_size
}
#[must_use]
pub fn ssa(&self) -> &SsaFunction<T> {
self.ssa
}
#[must_use]
pub fn config(&self) -> &EvaluatorConfig {
&self.config
}
#[must_use]
pub fn path(&self) -> &[usize] {
&self.path
}
pub fn clear_path(&mut self) {
self.path.clear();
}
#[must_use]
pub fn memory_tracking_enabled(&self) -> bool {
self.config.track_memory
}
pub fn set_concrete(&mut self, var: SsaVarId, value: ConstValue<T>) {
let expr = SymbolicExpr::constant(value);
self.values.insert(var, expr.clone());
self.track_origin_state(var, &expr);
}
pub fn set_symbolic(&mut self, var: SsaVarId, name: impl Into<String>) {
let expr = SymbolicExpr::named(name);
self.values.insert(var, expr.clone());
self.track_origin_state(var, &expr);
}
pub fn set_symbolic_expr(&mut self, var: SsaVarId, expr: SymbolicExpr<T>) {
self.values.insert(var, expr.clone());
self.track_origin_state(var, &expr);
}
pub fn set_unknown(&mut self, var: SsaVarId) {
self.values.remove(&var);
}
pub fn set(&mut self, var: SsaVarId, value: SymbolicExpr<T>) {
self.values.insert(var, value.clone());
self.track_origin_state(var, &value);
}
#[must_use]
pub fn get(&self, var: SsaVarId) -> Option<&SymbolicExpr<T>> {
self.values.get(&var)
}
#[must_use]
pub fn get_concrete(&self, var: SsaVarId) -> Option<&ConstValue<T>> {
self.values.get(&var).and_then(SymbolicExpr::as_constant)
}
#[must_use]
pub fn get_symbolic(&self, var: SsaVarId) -> Option<&SymbolicExpr<T>> {
self.values.get(&var).filter(|e| !e.is_constant())
}
#[must_use]
pub fn is_concrete(&self, var: SsaVarId) -> bool {
self.values.get(&var).is_some_and(SymbolicExpr::is_constant)
}
#[must_use]
pub fn is_symbolic(&self, var: SsaVarId) -> bool {
self.values.get(&var).is_some_and(|e| !e.is_constant())
}
#[must_use]
pub fn is_unknown(&self, var: SsaVarId) -> bool {
!self.values.contains_key(&var)
}
#[must_use]
pub fn values(&self) -> &BTreeMap<SsaVarId, SymbolicExpr<T>> {
&self.values
}
#[must_use]
pub fn concrete_values(&self) -> BTreeMap<SsaVarId, i64> {
self.values
.iter()
.filter_map(|(k, v)| v.as_i64().map(|c| (*k, c)))
.collect()
}
#[must_use]
pub fn const_values(&self) -> BTreeMap<SsaVarId, ConstValue<T>> {
self.values
.iter()
.filter_map(|(k, v)| v.as_constant().map(|c| (*k, c.clone())))
.collect()
}
pub fn clear(&mut self) {
self.values.clear();
self.predecessor = None;
self.constraints.clear();
}
pub fn add_constraint(&mut self, var: SsaVarId, constraint: Constraint<T>) {
if let Constraint::Equal(ref v) = constraint {
let expr = SymbolicExpr::constant(v.clone());
self.values.insert(var, expr.clone());
self.track_origin_state(var, &expr);
}
self.constraints.entry(var).or_default().push(constraint);
}
#[must_use]
pub fn constraints(&self, var: SsaVarId) -> &[Constraint<T>] {
self.constraints.get(&var).map_or(&[], |v| v.as_slice())
}
#[must_use]
pub fn has_constraints(&self, var: SsaVarId) -> bool {
self.constraints.get(&var).is_some_and(|v| !v.is_empty())
}
pub fn clear_constraints(&mut self, var: SsaVarId) {
self.constraints.remove(&var);
}
pub fn apply_branch_constraint(&mut self, condition: SsaVarId, took_true_branch: bool) -> bool {
let Some(ssa_var) = self.ssa.variable(condition) else {
return false;
};
let def_site = ssa_var.def_site();
let Some(block) = self.ssa.block(def_site.block) else {
return false;
};
let Some(instr_idx) = def_site.instruction else {
return false;
};
let Some(instr) = block.instruction(instr_idx) else {
return false;
};
self.derive_constraints_from_comparison(instr.op(), took_true_branch)
}
fn derive_constraints_from_comparison(
&mut self,
op: &SsaOp<T>,
took_true_branch: bool,
) -> bool {
match op {
SsaOp::Ceq { left, right, .. } => {
let left_val = self.get(*left).cloned();
let right_val = self.get(*right).cloned();
if took_true_branch {
match (&left_val, &right_val) {
(Some(l), None) => {
if let Some(v) = l.as_constant() {
self.add_constraint(*right, Constraint::Equal(v.clone()));
true
} else {
false
}
}
(None, Some(r)) => {
if let Some(v) = r.as_constant() {
self.add_constraint(*left, Constraint::Equal(v.clone()));
true
} else {
false
}
}
(Some(l), Some(r)) if l.as_constant() == r.as_constant() => {
true
}
_ => false,
}
} else {
match (&left_val, &right_val) {
(Some(l), None) => {
if let Some(v) = l.as_constant() {
self.add_constraint(*right, Constraint::NotEqual(v.clone()));
true
} else {
false
}
}
(None, Some(r)) => {
if let Some(v) = r.as_constant() {
self.add_constraint(*left, Constraint::NotEqual(v.clone()));
true
} else {
false
}
}
_ => false,
}
}
}
SsaOp::Cgt {
left,
right,
unsigned,
..
} => {
let right_val = self.get(*right).and_then(|e| e.as_constant().cloned());
if took_true_branch {
if let Some(v) = right_val {
if *unsigned {
self.add_constraint(*left, Constraint::GreaterThanUnsigned(v));
} else {
self.add_constraint(*left, Constraint::GreaterThan(v));
}
return true;
}
} else {
if let Some(v) = right_val {
self.add_constraint(*left, Constraint::LessOrEqual(v));
return true;
}
}
false
}
SsaOp::Clt {
left,
right,
unsigned,
..
} => {
let right_val = self.get(*right).and_then(|e| e.as_constant().cloned());
if took_true_branch {
if let Some(v) = right_val {
if *unsigned {
self.add_constraint(*left, Constraint::LessThanUnsigned(v));
} else {
self.add_constraint(*left, Constraint::LessThan(v));
}
return true;
}
} else {
if let Some(v) = right_val {
self.add_constraint(*left, Constraint::GreaterOrEqual(v));
return true;
}
}
false
}
_ => false,
}
}
#[must_use]
pub fn evaluate_condition_with_constraints(&self, condition: SsaVarId) -> Option<bool> {
if let Some(v) = self.get_concrete(condition) {
return Some(!v.is_zero());
}
let ssa_var = self.ssa.variable(condition)?;
let def_site = ssa_var.def_site();
let block = self.ssa.block(def_site.block)?;
let instr_idx = def_site.instruction?;
let instr = block.instruction(instr_idx)?;
self.check_condition_against_constraints(instr.op())
}
fn check_condition_against_constraints(&self, op: &SsaOp<T>) -> Option<bool> {
match op {
SsaOp::Ceq { left, right, .. } => {
let left_constraints = self.constraints(*left);
let right_val = self.get_concrete(*right)?;
for constraint in left_constraints {
match constraint {
Constraint::Equal(v) => {
return Some(v.ceq(right_val).is_some_and(|r| !r.is_zero()));
}
Constraint::NotEqual(v)
if v.ceq(right_val).is_some_and(|r| !r.is_zero()) =>
{
return Some(false);
}
Constraint::GreaterThan(v)
if right_val.cgt(v).is_none_or(|r| r.is_zero()) =>
{
return Some(false);
}
Constraint::LessThan(v)
if right_val.clt(v).is_none_or(|r| r.is_zero()) =>
{
return Some(false);
}
_ => {}
}
}
None
}
SsaOp::Cgt { left, right, .. } => {
let left_constraints = self.constraints(*left);
let right_val = self.get_concrete(*right)?;
for constraint in left_constraints {
match constraint {
Constraint::GreaterThan(v)
if v.clt(right_val).is_none_or(|r| r.is_zero()) =>
{
return Some(true);
}
Constraint::LessOrEqual(v)
if v.cgt(right_val).is_none_or(|r| r.is_zero()) =>
{
return Some(false);
}
Constraint::LessThan(v)
if v.cgt(right_val).is_none_or(|r| r.is_zero()) =>
{
return Some(false);
}
Constraint::Equal(v) => {
return Some(v.cgt(right_val).is_some_and(|r| !r.is_zero()));
}
_ => {}
}
}
None
}
SsaOp::Clt { left, right, .. } => {
let left_constraints = self.constraints(*left);
let right_val = self.get_concrete(*right)?;
for constraint in left_constraints {
match constraint {
Constraint::LessThan(v)
if v.cgt(right_val).is_none_or(|r| r.is_zero()) =>
{
return Some(true);
}
Constraint::GreaterOrEqual(v)
if v.clt(right_val).is_none_or(|r| r.is_zero()) =>
{
return Some(false);
}
Constraint::GreaterThan(v)
if v.clt(right_val).is_none_or(|r| r.is_zero()) =>
{
return Some(false);
}
Constraint::Equal(v) => {
return Some(v.clt(right_val).is_some_and(|r| !r.is_zero()));
}
_ => {}
}
}
None
}
_ => None,
}
}
pub fn set_predecessor(&mut self, pred: Option<usize>) {
self.predecessor = pred;
}
#[must_use]
pub fn predecessor(&self) -> Option<usize> {
self.predecessor
}
pub fn evaluate_phis(&mut self, block_idx: usize) {
let Some(block) = self.ssa.block(block_idx) else {
return;
};
let phi_results: Vec<(SsaVarId, Option<SymbolicExpr<T>>)> = block
.phi_nodes()
.iter()
.map(|phi| {
let result = phi.result();
let value = self.predecessor.and_then(|pred| {
phi.operands()
.iter()
.find(|op| op.predecessor() == pred)
.and_then(|op| self.values.get(&op.value()).cloned())
});
(result, value)
})
.collect();
for (result, value) in phi_results {
if let Some(v) = value {
self.values.insert(result, v.clone());
self.track_origin_state(result, &v);
} else {
self.values.remove(&result);
}
}
}
pub fn evaluate_block(&mut self, block_idx: usize) {
if self.config.track_path {
self.path.push(block_idx);
}
self.evaluate_phis(block_idx);
let Some(block) = self.ssa.block(block_idx) else {
return;
};
for instr in block.instructions() {
self.evaluate_op(instr.op());
}
}
pub fn evaluate_blocks(&mut self, block_indices: &[usize]) {
for &block_idx in block_indices {
self.evaluate_block(block_idx);
}
}
pub fn evaluate_path(&mut self, path: &[usize]) {
for (i, &block_idx) in path.iter().enumerate() {
if i > 0 {
if let Some(&prev) = path.get(i.saturating_sub(1)) {
self.set_predecessor(Some(prev));
}
}
self.evaluate_block(block_idx);
}
}
pub fn evaluate_loop_to_fixpoint(
&mut self,
loop_blocks: &[usize],
max_iterations: usize,
) -> usize {
if loop_blocks.is_empty() {
return 0;
}
for iteration in 0..max_iterations {
let snapshot: BTreeMap<SsaVarId, SymbolicExpr<T>> = self.values.clone();
for (i, &block_idx) in loop_blocks.iter().enumerate() {
if i > 0 {
if let Some(&prev) = loop_blocks.get(i.saturating_sub(1)) {
self.set_predecessor(Some(prev));
}
} else if loop_blocks.len() > 1 {
if let Some(&last) = loop_blocks.last() {
self.set_predecessor(Some(last));
}
}
self.evaluate_block(block_idx);
}
if self.values_match(&snapshot) {
return iteration.saturating_add(1);
}
}
self.widen_unstable_values(loop_blocks);
max_iterations
}
fn values_match(&self, snapshot: &BTreeMap<SsaVarId, SymbolicExpr<T>>) -> bool {
self.values == *snapshot
}
fn widen_unstable_values(&mut self, loop_blocks: &[usize]) {
for &block_idx in loop_blocks {
let Some(block) = self.ssa.block(block_idx) else {
continue;
};
for phi in block.phi_nodes() {
self.values.remove(&phi.result());
}
for instr in block.instructions() {
if let Some(dest) = instr.op().dest() {
if let Some(expr) = self.values.get(&dest) {
if !expr.is_constant() {
self.values.remove(&dest);
}
}
}
}
}
}
pub fn evaluate_loop_iterations(&mut self, loop_blocks: &[usize], iterations: usize) {
for _ in 0..iterations {
for (i, &block_idx) in loop_blocks.iter().enumerate() {
if i > 0 {
if let Some(&prev) = loop_blocks.get(i.saturating_sub(1)) {
self.set_predecessor(Some(prev));
}
}
self.evaluate_block(block_idx);
}
}
}
pub fn evaluate_op(&mut self, op: &SsaOp<T>) -> Option<SymbolicExpr<T>> {
match op {
SsaOp::Const { dest, value } => {
let expr = SymbolicExpr::constant(value.clone());
self.values.insert(*dest, expr.clone());
self.track_origin_state(*dest, &expr);
Some(expr)
}
SsaOp::Copy { dest, src } => {
let value = self.values.get(src).cloned();
if let Some(v) = value {
self.values.insert(*dest, v.clone());
self.track_origin_state(*dest, &v);
Some(v)
} else {
self.values.remove(dest);
None
}
}
SsaOp::LoadLocal { dest, local_index } => {
let value = self.local_state.get(local_index).cloned();
if let Some(v) = value {
self.values.insert(*dest, v.clone());
Some(v)
} else {
self.values.remove(dest);
None
}
}
SsaOp::LoadArg { dest, arg_index } => {
let value = self.arg_state.get(arg_index).cloned();
if let Some(v) = value {
self.values.insert(*dest, v.clone());
Some(v)
} else {
self.values.remove(dest);
None
}
}
SsaOp::Add {
dest, left, right, ..
} => self.eval_binary_op(*dest, *left, *right, SymbolicOp::Add),
SsaOp::Sub {
dest, left, right, ..
} => self.eval_binary_op(*dest, *left, *right, SymbolicOp::Sub),
SsaOp::Mul {
dest, left, right, ..
} => self.eval_binary_op(*dest, *left, *right, SymbolicOp::Mul),
SsaOp::Div {
dest,
left,
right,
unsigned,
..
} => {
let op = if *unsigned {
SymbolicOp::DivU
} else {
SymbolicOp::DivS
};
self.eval_binary_op(*dest, *left, *right, op)
}
SsaOp::Rem {
dest,
left,
right,
unsigned,
..
} => {
let op = if *unsigned {
SymbolicOp::RemU
} else {
SymbolicOp::RemS
};
self.eval_binary_op(*dest, *left, *right, op)
}
SsaOp::Xor {
dest, left, right, ..
} => self.eval_binary_op(*dest, *left, *right, SymbolicOp::Xor),
SsaOp::And {
dest, left, right, ..
} => self.eval_binary_op(*dest, *left, *right, SymbolicOp::And),
SsaOp::Or {
dest, left, right, ..
} => self.eval_binary_op(*dest, *left, *right, SymbolicOp::Or),
SsaOp::Shl {
dest,
value,
amount,
..
} => self.eval_binary_op(*dest, *value, *amount, SymbolicOp::Shl),
SsaOp::Shr {
dest,
value,
amount,
unsigned,
..
} => {
let op = if *unsigned {
SymbolicOp::ShrU
} else {
SymbolicOp::ShrS
};
self.eval_binary_op(*dest, *value, *amount, op)
}
SsaOp::Neg { dest, operand, .. } => {
self.eval_unary_op(*dest, *operand, SymbolicOp::Neg)
}
SsaOp::Not { dest, operand, .. } => {
self.eval_unary_op(*dest, *operand, SymbolicOp::Not)
}
SsaOp::Rol {
dest,
value,
amount,
} => {
let v = self.values.get(value).and_then(|e| e.as_i64());
let a = self.values.get(amount).and_then(|e| e.as_i64());
match (v, a) {
(Some(v), Some(a)) => {
let shift = (a & 0x1f) as u32;
let v32 = v as u32;
let result = v32.rotate_left(shift);
let expr = SymbolicExpr::constant(ConstValue::I32(result as i32));
self.values.insert(*dest, expr.clone());
self.track_origin_state(*dest, &expr);
Some(expr)
}
_ => {
self.values.remove(dest);
None
}
}
}
SsaOp::Ror {
dest,
value,
amount,
} => {
let v = self.values.get(value).and_then(|e| e.as_i64());
let a = self.values.get(amount).and_then(|e| e.as_i64());
match (v, a) {
(Some(v), Some(a)) => {
let shift = (a & 0x1f) as u32;
let v32 = v as u32;
let result = v32.rotate_right(shift);
let expr = SymbolicExpr::constant(ConstValue::I32(result as i32));
self.values.insert(*dest, expr.clone());
self.track_origin_state(*dest, &expr);
Some(expr)
}
_ => {
self.values.remove(dest);
None
}
}
}
SsaOp::Rcl {
dest,
value,
amount,
} => {
let v = self.values.get(value).and_then(|e| e.as_i64());
let a = self.values.get(amount).and_then(|e| e.as_i64());
match (v, a) {
(Some(v), Some(a)) => {
let shift = (a & 0x1f) as u32;
let v32 = v as u32;
let result = v32.rotate_left(shift);
let expr = SymbolicExpr::constant(ConstValue::I32(result as i32));
self.values.insert(*dest, expr.clone());
self.track_origin_state(*dest, &expr);
Some(expr)
}
_ => {
self.values.remove(dest);
None
}
}
}
SsaOp::Rcr {
dest,
value,
amount,
} => {
let v = self.values.get(value).and_then(|e| e.as_i64());
let a = self.values.get(amount).and_then(|e| e.as_i64());
match (v, a) {
(Some(v), Some(a)) => {
let shift = (a & 0x1f) as u32;
let v32 = v as u32;
let result = v32.rotate_right(shift);
let expr = SymbolicExpr::constant(ConstValue::I32(result as i32));
self.values.insert(*dest, expr.clone());
self.track_origin_state(*dest, &expr);
Some(expr)
}
_ => {
self.values.remove(dest);
None
}
}
}
SsaOp::BSwap { dest, src } => {
let v = self.values.get(src).and_then(|e| e.as_i64());
match v {
Some(v) => {
let result = (v as u32).swap_bytes();
let expr = SymbolicExpr::constant(ConstValue::I32(result as i32));
self.values.insert(*dest, expr.clone());
self.track_origin_state(*dest, &expr);
Some(expr)
}
_ => {
self.values.remove(dest);
None
}
}
}
SsaOp::BRev { dest, src } => {
let v = self.values.get(src).and_then(|e| e.as_i64());
match v {
Some(v) => {
let result = (v as u32).reverse_bits();
let expr = SymbolicExpr::constant(ConstValue::I32(result as i32));
self.values.insert(*dest, expr.clone());
self.track_origin_state(*dest, &expr);
Some(expr)
}
_ => {
self.values.remove(dest);
None
}
}
}
SsaOp::BitScanForward { dest, src } => {
let v = self.values.get(src).and_then(|e| e.as_i64());
match v {
Some(v) => {
let v32 = v as u32;
let result = if v32 == 0 {
32
} else {
v32.trailing_zeros() as i32
};
let expr = SymbolicExpr::constant(ConstValue::I32(result));
self.values.insert(*dest, expr.clone());
self.track_origin_state(*dest, &expr);
Some(expr)
}
_ => {
self.values.remove(dest);
None
}
}
}
SsaOp::BitScanReverse { dest, src } => {
let v = self.values.get(src).and_then(|e| e.as_i64());
match v {
Some(v) => {
let v32 = v as u32;
let result = if v32 == 0 {
32
} else {
31u32.saturating_sub(v32.leading_zeros()) as i32
};
let expr = SymbolicExpr::constant(ConstValue::I32(result));
self.values.insert(*dest, expr.clone());
self.track_origin_state(*dest, &expr);
Some(expr)
}
_ => {
self.values.remove(dest);
None
}
}
}
SsaOp::Popcount { dest, src } => {
let v = self.values.get(src).and_then(|e| e.as_i64());
match v {
Some(v) => {
let result = (v as u32).count_ones() as i32;
let expr = SymbolicExpr::constant(ConstValue::I32(result));
self.values.insert(*dest, expr.clone());
self.track_origin_state(*dest, &expr);
Some(expr)
}
_ => {
self.values.remove(dest);
None
}
}
}
SsaOp::Parity { dest, src } => {
let v = self.values.get(src).and_then(|e| e.as_i64());
match v {
Some(v) => {
let result = if (v as u32).count_ones() % 2 == 1 {
1
} else {
0
};
let expr = SymbolicExpr::constant(ConstValue::I32(result));
self.values.insert(*dest, expr.clone());
self.track_origin_state(*dest, &expr);
Some(expr)
}
_ => {
self.values.remove(dest);
None
}
}
}
SsaOp::Select {
dest,
condition,
true_val,
false_val,
} => {
let cond = self.values.get(condition).and_then(|e| e.as_i64());
match cond {
Some(c) if c != 0 => {
if let Some(expr) = self.values.get(true_val).cloned() {
self.values.insert(*dest, expr.clone());
self.track_origin_state(*dest, &expr);
Some(expr)
} else {
self.values.remove(dest);
None
}
}
Some(_) => {
if let Some(expr) = self.values.get(false_val).cloned() {
self.values.insert(*dest, expr.clone());
self.track_origin_state(*dest, &expr);
Some(expr)
} else {
self.values.remove(dest);
None
}
}
None => {
self.values.remove(dest);
None
}
}
}
SsaOp::Ceq { dest, left, right } => {
self.eval_binary_op(*dest, *left, *right, SymbolicOp::Eq)
}
SsaOp::Cgt {
dest,
left,
right,
unsigned,
} => {
let op = if *unsigned {
SymbolicOp::GtU
} else {
SymbolicOp::GtS
};
self.eval_binary_op(*dest, *left, *right, op)
}
SsaOp::Clt {
dest,
left,
right,
unsigned,
} => {
let op = if *unsigned {
SymbolicOp::LtU
} else {
SymbolicOp::LtS
};
self.eval_binary_op(*dest, *left, *right, op)
}
SsaOp::Conv {
dest,
operand,
target,
unsigned,
..
} => {
let value = self.values.get(operand).cloned();
if let Some(expr) = value {
if let Some(v) = expr.as_i64() {
let ptr_bytes = self.pointer_size.bytes() as u32;
let converted = T::evaluate_int_conv(v, target, *unsigned, ptr_bytes)
.unwrap_or(ConstValue::I64(v));
let result = SymbolicExpr::constant(converted);
self.values.insert(*dest, result.clone());
self.track_origin_state(*dest, &result);
Some(result)
} else {
self.values.insert(*dest, expr.clone());
self.track_origin_state(*dest, &expr);
Some(expr)
}
} else {
self.values.remove(dest);
None
}
}
SsaOp::Call { dest, .. }
| SsaOp::CallVirt { dest, .. }
| SsaOp::CallIndirect { dest, .. } => {
if let Some(d) = dest {
self.values.remove(d);
}
None
}
SsaOp::LoadStaticField { dest, field } => {
if self.config.track_memory {
let location = MemoryLocation::StaticField(field.clone());
if let Some(stored_var) = self.memory.load(&location) {
if let Some(expr) = self.values.get(&stored_var).cloned() {
self.values.insert(*dest, expr.clone());
return Some(expr);
}
self.values
.insert(*dest, SymbolicExpr::variable(stored_var));
return Some(SymbolicExpr::variable(stored_var));
}
}
self.values.remove(dest);
None
}
SsaOp::StoreStaticField { value, field } => {
if self.config.track_memory {
let location = MemoryLocation::StaticField(field.clone());
self.memory.store(location, *value, 0);
}
None
}
SsaOp::LoadField {
dest,
object,
field,
} => {
if self.config.track_memory {
let location = MemoryLocation::InstanceField(*object, field.clone());
if let Some(stored_var) = self.memory.load(&location) {
if let Some(expr) = self.values.get(&stored_var).cloned() {
self.values.insert(*dest, expr.clone());
return Some(expr);
}
self.values
.insert(*dest, SymbolicExpr::variable(stored_var));
return Some(SymbolicExpr::variable(stored_var));
}
}
self.values.remove(dest);
None
}
SsaOp::StoreField {
object,
field,
value,
} => {
if self.config.track_memory {
let location = MemoryLocation::InstanceField(*object, field.clone());
self.memory.store(location, *value, 0);
}
None
}
SsaOp::LoadLocalAddr {
dest, local_index, ..
} => {
self.values.remove(dest);
self.local_state.remove(local_index);
None
}
SsaOp::LoadArgAddr {
dest, arg_index, ..
} => {
self.values.remove(dest);
self.arg_state.remove(arg_index);
None
}
SsaOp::NewObj { dest, .. }
| SsaOp::NewArr { dest, .. }
| SsaOp::LoadElement { dest, .. }
| SsaOp::LoadIndirect { dest, .. }
| SsaOp::Box { dest, .. }
| SsaOp::Unbox { dest, .. }
| SsaOp::UnboxAny { dest, .. }
| SsaOp::CastClass { dest, .. }
| SsaOp::IsInst { dest, .. }
| SsaOp::ArrayLength { dest, .. }
| SsaOp::LoadToken { dest, .. }
| SsaOp::SizeOf { dest, .. }
| SsaOp::Ckfinite { dest, .. }
| SsaOp::LocalAlloc { dest, .. }
| SsaOp::LoadFunctionPtr { dest, .. }
| SsaOp::LoadVirtFunctionPtr { dest, .. }
| SsaOp::LoadFieldAddr { dest, .. }
| SsaOp::LoadStaticFieldAddr { dest, .. }
| SsaOp::LoadElementAddr { dest, .. }
| SsaOp::LoadObj { dest, .. } => {
self.values.remove(dest);
None
}
_ => None,
}
}
fn eval_binary_op(
&mut self,
dest: SsaVarId,
left: SsaVarId,
right: SsaVarId,
op: SymbolicOp,
) -> Option<SymbolicExpr<T>> {
let left_expr = self.values.get(&left)?;
let right_expr = self.values.get(&right)?;
let result = SymbolicExpr::binary(op, left_expr.clone(), right_expr.clone())
.simplify(self.pointer_size);
let result = self.mask_symbolic_native(result);
self.values.insert(dest, result.clone());
self.track_origin_state(dest, &result);
Some(result)
}
fn eval_unary_op(
&mut self,
dest: SsaVarId,
operand: SsaVarId,
op: SymbolicOp,
) -> Option<SymbolicExpr<T>> {
let operand_expr = self.values.get(&operand)?;
let result = SymbolicExpr::unary(op, operand_expr.clone()).simplify(self.pointer_size);
let result = self.mask_symbolic_native(result);
self.values.insert(dest, result.clone());
self.track_origin_state(dest, &result);
Some(result)
}
fn track_origin_state(&mut self, var: SsaVarId, value: &SymbolicExpr<T>) {
if let Some(ssa_var) = self.ssa.variable(var) {
match ssa_var.origin() {
VariableOrigin::Local(idx) => {
self.local_state.insert(idx, value.clone());
}
VariableOrigin::Argument(idx) => {
self.arg_state.insert(idx, value.clone());
}
_ => {}
}
}
}
fn mask_symbolic_native(&self, expr: SymbolicExpr<T>) -> SymbolicExpr<T> {
if let Some(cv) = expr.as_constant() {
match cv {
ConstValue::NativeInt(_) | ConstValue::NativeUInt(_) => {
SymbolicExpr::constant(cv.clone().mask_native(self.pointer_size))
}
_ => expr,
}
} else {
expr
}
}
pub fn resolve_with_trace(
&mut self,
var: SsaVarId,
max_depth: usize,
) -> Option<SymbolicExpr<T>> {
if let Some(v) = self.values.get(&var) {
return Some(v.clone());
}
if max_depth == 0 {
return None;
}
let ssa_var = self.ssa.variable(var)?;
let def_site = ssa_var.def_site();
let block = self.ssa.block(def_site.block)?;
let instr_idx = def_site.instruction?;
let instr = block.instruction(instr_idx)?;
let op = instr.op();
for operand in op.uses() {
if !self.values.contains_key(&operand) {
if let Some(resolved) =
self.resolve_with_trace(operand, max_depth.saturating_sub(1))
{
self.values.insert(operand, resolved);
}
}
}
self.evaluate_op(op)
}
pub fn evaluate_with_trace(&mut self, var: SsaVarId, max_depth: usize) -> Option<i64> {
self.resolve_with_trace(var, max_depth)
.and_then(|e| e.as_i64())
}
#[must_use]
pub fn next_block(&self, block_idx: usize) -> ControlFlow {
let Some(block) = self.ssa.block(block_idx) else {
return ControlFlow::Unknown;
};
let terminator = block
.instructions()
.iter()
.rev()
.find(|instr| instr.op().is_terminator());
let Some(instr) = terminator else {
let next_idx = block_idx.saturating_add(1);
if next_idx < self.ssa.block_count() {
return ControlFlow::Continue(next_idx);
}
return ControlFlow::Unknown;
};
self.evaluate_control_flow(instr.op())
}
fn evaluate_control_flow(&self, op: &SsaOp<T>) -> ControlFlow {
match op {
SsaOp::Jump { target } | SsaOp::Leave { target } => ControlFlow::Continue(*target),
SsaOp::Branch {
condition,
true_target,
false_target,
} => match self.get_concrete(*condition) {
Some(v) => {
if v.is_zero() {
ControlFlow::Continue(*false_target)
} else {
ControlFlow::Continue(*true_target)
}
}
None => ControlFlow::Unknown,
},
SsaOp::BranchCmp {
left,
right,
cmp,
unsigned,
true_target,
false_target,
} => {
let left_val = self.get_concrete(*left);
let right_val = self.get_concrete(*right);
match (left_val, right_val) {
(Some(l), Some(r)) => {
let result = Self::evaluate_comparison(l, r, *cmp, *unsigned);
if result {
ControlFlow::Continue(*true_target)
} else {
ControlFlow::Continue(*false_target)
}
}
_ => ControlFlow::Unknown,
}
}
SsaOp::Switch {
value,
targets,
default,
} => match self.get_concrete(*value).and_then(ConstValue::as_u64) {
Some(v) => {
#[allow(clippy::cast_possible_truncation)]
let idx = v as usize;
if let Some(&target) = targets.get(idx) {
ControlFlow::Continue(target)
} else {
ControlFlow::Continue(*default)
}
}
None => ControlFlow::Unknown,
},
SsaOp::Return { .. }
| SsaOp::Throw { .. }
| SsaOp::Rethrow
| SsaOp::EndFinally
| SsaOp::EndFilter { .. }
| SsaOp::InterruptReturn
| SsaOp::Unreachable => ControlFlow::Terminal,
_ => ControlFlow::Unknown,
}
}
pub fn evaluate_comparison(
left: &ConstValue<T>,
right: &ConstValue<T>,
cmp: CmpKind,
unsigned: bool,
) -> bool {
match cmp {
CmpKind::Eq => left.ceq(right).is_some_and(|v| !v.is_zero()),
CmpKind::Ne => left.ceq(right).is_some_and(|v| v.is_zero()),
CmpKind::Lt => if unsigned {
left.clt_un(right)
} else {
left.clt(right)
}
.is_some_and(|v| !v.is_zero()),
CmpKind::Le => {
if unsigned {
left.cgt_un(right)
} else {
left.cgt(right)
}
.is_some_and(|v| v.is_zero())
}
CmpKind::Gt => if unsigned {
left.cgt_un(right)
} else {
left.cgt(right)
}
.is_some_and(|v| !v.is_zero()),
CmpKind::Ge => {
if unsigned {
left.clt_un(right)
} else {
left.clt(right)
}
.is_some_and(|v| v.is_zero())
}
}
}
pub fn execute(
&mut self,
start_block: usize,
state_var: Option<SsaVarId>,
max_steps: usize,
) -> ExecutionTrace<T> {
let mut trace = ExecutionTrace::new(max_steps);
let mut current_block = start_block;
loop {
if trace.hit_limit() {
break;
}
let state = state_var.and_then(|v| self.get_concrete(v).cloned());
trace.record_block(current_block, state);
if let Some(prev) = trace.blocks().iter().rev().nth(1) {
self.set_predecessor(Some(*prev));
}
self.evaluate_block(current_block);
match self.next_block(current_block) {
ControlFlow::Continue(next) => {
current_block = next;
}
ControlFlow::Terminal => {
trace.mark_complete();
break;
}
ControlFlow::Unknown => {
break;
}
}
}
trace
}
pub fn execute_from_entry(&mut self, max_steps: usize) -> ExecutionTrace<T> {
self.execute(0, None, max_steps)
}
}