use crate::ir::{VarDef, VarId};
#[cfg(feature = "smt")]
use crate::ir::Expr;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SmtVerdict {
Tautology,
Contradiction,
Satisfiable,
Unsupported,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LowerPolicy {
Symbolic,
RejectUnsupported,
}
#[cfg(feature = "smt")]
pub fn verify_branch(cond: VarId, vars: &[VarDef]) -> SmtVerdict {
use std::collections::HashMap;
use z3::ast::{Ast, Bool, BV};
use z3::{Config, Context, SatResult, Solver};
let mut cfg = Config::new();
cfg.set_timeout_msec(5_000);
let ctx = Context::new(&cfg);
let solver = Solver::new(&ctx);
let mut env: HashMap<u32, BV> = HashMap::new();
let Some(c) = build(&ctx, cond, vars, &mut env, LowerPolicy::Symbolic) else {
return SmtVerdict::Unsupported;
};
let zero = BV::from_u64(&ctx, 0, c.get_size());
let truthy = c._eq(&zero).not();
solver.push();
solver.assert(&truthy.clone().not());
let taut = matches!(solver.check(), SatResult::Unsat);
solver.pop(1);
if taut {
return SmtVerdict::Tautology;
}
solver.push();
solver.assert(&truthy);
let contra = matches!(solver.check(), SatResult::Unsat);
solver.pop(1);
if contra {
return SmtVerdict::Contradiction;
}
SmtVerdict::Satisfiable
}
#[cfg(not(feature = "smt"))]
pub fn verify_branch(_cond: VarId, _vars: &[VarDef]) -> SmtVerdict {
SmtVerdict::Unsupported
}
#[cfg(feature = "smt")]
pub fn lower<'c>(
ctx: &'c z3::Context,
v: VarId,
vars: &[VarDef],
env: &mut std::collections::HashMap<u32, z3::ast::BV<'c>>,
policy: LowerPolicy,
) -> Option<z3::ast::BV<'c>> {
build(ctx, v, vars, env, policy)
}
#[cfg(feature = "smt")]
fn build<'c>(
ctx: &'c z3::Context,
v: VarId,
vars: &[VarDef],
env: &mut std::collections::HashMap<u32, z3::ast::BV<'c>>,
policy: LowerPolicy,
) -> Option<z3::ast::BV<'c>> {
use crate::ir::{BinOpKind, UnaryOpKind};
use z3::ast::{Ast, BV};
if let Some(bv) = env.get(&v.0) {
return Some(bv.clone());
}
let def = vars.get(v.0 as usize)?;
let bits = bits_for(def.size);
let bv = match &def.expr {
Expr::Const(c, _) => BV::from_u64(ctx, *c, bits),
Expr::Var(inner) => build(ctx, *inner, vars, env, policy)?,
Expr::BinOp(kind, l, r) => {
let a = build(ctx, *l, vars, env, policy)?;
let b = build(ctx, *r, vars, env, policy)?;
let (a, b) = align(a, b);
match kind {
BinOpKind::Add => a.bvadd(&b),
BinOpKind::Sub => a.bvsub(&b),
BinOpKind::Mult => a.bvmul(&b),
BinOpKind::And => a.bvand(&b),
BinOpKind::Or => a.bvor(&b),
BinOpKind::Xor => a.bvxor(&b),
BinOpKind::Lsl => a.bvshl(&b),
BinOpKind::Lsr => a.bvlshr(&b),
BinOpKind::Asr => a.bvashr(&b),
BinOpKind::Div => a.bvudiv(&b),
BinOpKind::SDiv => a.bvsdiv(&b),
BinOpKind::Rem => a.bvurem(&b),
BinOpKind::SRem => a.bvsrem(&b),
BinOpKind::Eq => bool_to_bv(ctx, &a._eq(&b)),
BinOpKind::NotEq => bool_to_bv(ctx, &a._eq(&b).not()),
BinOpKind::Less => bool_to_bv(ctx, &a.bvult(&b)),
BinOpKind::LessEq => bool_to_bv(ctx, &a.bvule(&b)),
BinOpKind::SLess => bool_to_bv(ctx, &a.bvslt(&b)),
BinOpKind::SLessEq => bool_to_bv(ctx, &a.bvsle(&b)),
_ => return None,
}
}
Expr::UnaryOp(kind, inner) => {
let a = build(ctx, *inner, vars, env, policy)?;
match kind {
UnaryOpKind::Neg => a.bvneg(),
UnaryOpKind::Not => a.bvnot(),
UnaryOpKind::BoolNot => bool_to_bv(ctx, &a._eq(&BV::from_u64(ctx, 0, a.get_size()))),
UnaryOpKind::Zext => a.zero_ext(bits.saturating_sub(a.get_size())),
UnaryOpKind::Sext => a.sign_ext(bits.saturating_sub(a.get_size())),
UnaryOpKind::Trunc => a.extract(bits.saturating_sub(1), 0),
_ => return None,
}
}
_ => match policy {
LowerPolicy::Symbolic => {
let fresh = BV::fresh_const(ctx, "v", bits);
env.insert(v.0, fresh.clone());
fresh
}
LowerPolicy::RejectUnsupported => return None,
},
};
Some(bv)
}
#[cfg(feature = "smt")]
fn align<'c>(a: z3::ast::BV<'c>, b: z3::ast::BV<'c>) -> (z3::ast::BV<'c>, z3::ast::BV<'c>) {
let (sa, sb) = (a.get_size(), b.get_size());
if sa == sb {
(a, b)
} else if sa < sb {
(a.zero_ext(sb - sa), b)
} else {
(a, b.zero_ext(sa - sb))
}
}
#[cfg(feature = "smt")]
fn bool_to_bv<'c>(ctx: &'c z3::Context, b: &z3::ast::Bool<'c>) -> z3::ast::BV<'c> {
use z3::ast::{Ast, BV};
b.ite(&BV::from_u64(ctx, 1, 1), &BV::from_u64(ctx, 0, 1))
}
#[cfg_attr(not(feature = "smt"), allow(dead_code))]
fn bits_for(size: u32) -> u32 {
match size {
0 => 64,
s if s >= 8 => 64,
s => s * 8,
}
}
#[cfg(all(test, feature = "smt"))]
mod tests {
use super::*;
use crate::ir::{BinOpKind, Expr, InferredType, VarDef, VarId};
use pcode_ir::Varnode;
fn mk(exprs: Vec<(Expr, u32)>) -> Vec<VarDef> {
exprs
.into_iter()
.enumerate()
.map(|(i, (e, size))| VarDef {
id: VarId(i as u32),
varnode: Varnode::constant(0, size),
expr: e,
size,
use_count: 1,
param_name: None,
call_return: false,
inferred_type: InferredType::Unknown,
display_type: None,
})
.collect()
}
#[test]
fn z3_proves_themida_identity() {
let vars = mk(vec![
(Expr::Unknown, 8),
(Expr::BinOp(BinOpKind::Mult, VarId(0), VarId(0)), 8),
(Expr::Const(1, 8), 8),
(Expr::BinOp(BinOpKind::Sub, VarId(0), VarId(2)), 8),
(Expr::BinOp(BinOpKind::Mult, VarId(0), VarId(3)), 8),
(Expr::BinOp(BinOpKind::Sub, VarId(1), VarId(4)), 8),
(Expr::BinOp(BinOpKind::Sub, VarId(5), VarId(0)), 8),
(Expr::Const(0, 8), 8),
(Expr::BinOp(BinOpKind::Eq, VarId(6), VarId(7)), 1),
]);
assert_eq!(verify_branch(VarId(8), &vars), SmtVerdict::Tautology);
}
#[test]
fn z3_rejects_real_branch() {
let vars = mk(vec![
(Expr::Unknown, 8),
(Expr::Const(0, 8), 8),
(Expr::BinOp(BinOpKind::Eq, VarId(0), VarId(1)), 1),
]);
assert_eq!(verify_branch(VarId(2), &vars), SmtVerdict::Satisfiable);
}
#[test]
fn strict_policy_rejects_unknown() {
let vars = mk(vec![(Expr::Unknown, 8)]);
let mut cfg = z3::Config::new();
cfg.set_timeout_msec(1_000);
let ctx = z3::Context::new(&cfg);
let mut env = std::collections::HashMap::new();
let r = lower(&ctx, VarId(0), &vars, &mut env, LowerPolicy::RejectUnsupported);
assert!(r.is_none(), "Unknown leaked through strict lowering");
}
#[test]
fn symbolic_policy_passes_unknown() {
let vars = mk(vec![(Expr::Unknown, 8)]);
let mut cfg = z3::Config::new();
cfg.set_timeout_msec(1_000);
let ctx = z3::Context::new(&cfg);
let mut env = std::collections::HashMap::new();
let r = lower(&ctx, VarId(0), &vars, &mut env, LowerPolicy::Symbolic);
assert!(r.is_some(), "Symbolic policy regressed on Unknown");
}
#[test]
fn strict_policy_rejects_load() {
let vars = mk(vec![
(Expr::Const(0x1000, 8), 8),
(Expr::Load(VarId(0)), 8),
]);
let mut cfg = z3::Config::new();
cfg.set_timeout_msec(1_000);
let ctx = z3::Context::new(&cfg);
let mut env = std::collections::HashMap::new();
let r = lower(&ctx, VarId(1), &vars, &mut env, LowerPolicy::RejectUnsupported);
assert!(r.is_none(), "Load leaked through strict lowering");
}
}