#![allow(clippy::collapsible_if, clippy::unnecessary_map_or)]
use std::collections::{HashMap, HashSet, VecDeque};
use smallvec::SmallVec;
use crate::cfg::Cfg;
use crate::constraint;
use crate::evidence::{SymbolicVerdict, Verdict};
use crate::ssa::const_prop::ConstLattice;
use crate::ssa::ir::{BlockId, SsaBody, SsaValue, Terminator};
use crate::taint::Finding;
use super::SymexContext;
use super::loops::LoopInfo;
use super::state::{PathConstraint, SymbolicState};
use super::transfer::{self, SymexHeapCtx, SymexSummaryCtx};
use super::value::SymbolicValue;
const MAX_FORKS_PER_FINDING: usize = 3;
const MAX_PATHS_PER_FINDING: usize = 8;
const MAX_TOTAL_STEPS: usize = 500;
struct ExplorationState {
sym_state: SymbolicState,
env: constraint::PathEnv,
current_block: BlockId,
predecessor: Option<BlockId>,
forks_used: usize,
steps_taken: usize,
constraints_checked: u32,
visit_counts: HashMap<BlockId, u8>,
exception_context: Option<SymbolicValue>,
}
pub(super) struct PathOutcome {
verdict: Verdict,
constraints_checked: u32,
witness: Option<String>,
}
pub(super) struct ExplorationResult {
pub paths_completed: Vec<PathOutcome>,
#[allow(dead_code)]
pub paths_pruned: usize,
#[allow(dead_code)]
pub total_steps: usize,
pub search_exhausted: bool,
pub interproc_findings: Vec<super::interproc::InternalSinkFinding>,
pub interproc_cutoffs: Vec<super::interproc::CutoffReason>,
}
fn compute_source_sink_reachable(
ssa: &SsaBody,
source_block: BlockId,
sink_block: BlockId,
exception_succs: &HashMap<BlockId, SmallVec<[BlockId; 2]>>,
exception_preds: &HashMap<BlockId, SmallVec<[BlockId; 2]>>,
) -> HashSet<BlockId> {
let mut forward = HashSet::new();
let mut queue = VecDeque::new();
forward.insert(source_block);
queue.push_back(source_block);
while let Some(bid) = queue.pop_front() {
if let Some(block) = ssa.blocks.get(bid.0 as usize) {
for &succ in &block.succs {
if forward.insert(succ) {
queue.push_back(succ);
}
}
}
if let Some(catches) = exception_succs.get(&bid) {
for &catch in catches {
if forward.insert(catch) {
queue.push_back(catch);
}
}
}
}
let mut backward = HashSet::new();
backward.insert(sink_block);
queue.push_back(sink_block);
while let Some(bid) = queue.pop_front() {
if let Some(block) = ssa.blocks.get(bid.0 as usize) {
for &pred in &block.preds {
if backward.insert(pred) {
queue.push_back(pred);
}
}
}
if let Some(srcs) = exception_preds.get(&bid) {
for &src in srcs {
if backward.insert(src) {
queue.push_back(src);
}
}
}
}
forward.intersection(&backward).copied().collect()
}
pub(super) fn explore_finding(finding: &Finding, ctx: &SymexContext) -> ExplorationResult {
let ssa = ctx.ssa;
let cfg = ctx.cfg;
let const_values = ctx.const_values;
let type_facts = ctx.type_facts;
let path_blocks = super::extract_path_blocks(finding, ssa);
if path_blocks.len() < 2 {
return ExplorationResult {
paths_completed: vec![PathOutcome {
verdict: Verdict::Inconclusive,
constraints_checked: 0,
witness: None,
}],
paths_pruned: 0,
total_steps: 0,
search_exhausted: true,
interproc_findings: Vec::new(),
interproc_cutoffs: Vec::new(),
};
}
let source_block = path_blocks[0];
let sink_block = path_blocks[path_blocks.len() - 1];
let exception_succs: HashMap<BlockId, SmallVec<[BlockId; 2]>> = {
let mut map: HashMap<BlockId, SmallVec<[BlockId; 2]>> = HashMap::new();
for &(src, catch) in &ssa.exception_edges {
let entry = map.entry(src).or_default();
if !entry.contains(&catch) {
entry.push(catch);
}
}
map
};
let exception_preds: HashMap<BlockId, SmallVec<[BlockId; 2]>> = {
let mut map: HashMap<BlockId, SmallVec<[BlockId; 2]>> = HashMap::new();
for &(src, catch) in &ssa.exception_edges {
let entry = map.entry(catch).or_default();
if !entry.contains(&src) {
entry.push(src);
}
}
map
};
let reachable = compute_source_sink_reachable(
ssa,
source_block,
sink_block,
&exception_succs,
&exception_preds,
);
let on_path: HashSet<BlockId> = path_blocks.iter().copied().collect();
let loop_info = super::loops::analyse_loops(ssa);
let mut sym_state = SymbolicState::new();
sym_state.seed_from_const_values(const_values);
for step in &finding.flow_steps {
if matches!(step.op_kind, crate::evidence::FlowStepKind::Source)
&& let Some(&ssa_val) = ssa.cfg_node_map.get(&step.cfg_node)
{
sym_state.mark_tainted(ssa_val);
sym_state.set(ssa_val, SymbolicValue::Symbol(ssa_val));
}
}
let mut env = constraint::PathEnv::empty();
env.seed_from_optimization(const_values, type_facts);
let initial = ExplorationState {
sym_state,
env,
current_block: source_block,
predecessor: None,
forks_used: 0,
steps_taken: 0,
constraints_checked: 0,
visit_counts: HashMap::new(),
exception_context: None,
};
let mut work_queue: VecDeque<ExplorationState> = VecDeque::new();
work_queue.push_back(initial);
let mut outcomes: Vec<PathOutcome> = Vec::new();
let mut paths_pruned: usize = 0;
let mut total_steps: usize = 0;
let mut search_exhausted = true;
let summary_ctx = ctx.global_summaries.map(|gs| SymexSummaryCtx {
global_summaries: gs,
lang: ctx.lang,
namespace: ctx.namespace,
type_facts: Some(ctx.type_facts),
});
let summary_ctx_ref = summary_ctx.as_ref();
let heap_ctx = ctx.points_to.map(|pts| SymexHeapCtx {
points_to: pts,
ssa,
lang: ctx.lang,
const_values: ctx.const_values,
});
let heap_ctx_ref = heap_ctx.as_ref();
let interproc_budget = std::cell::Cell::new(super::interproc::InterprocBudget::new());
let interproc_cache = std::cell::RefCell::new(std::collections::HashMap::new());
let interproc_reentry = std::cell::RefCell::new(std::collections::HashMap::new());
let interproc_stats = std::cell::Cell::new(super::interproc::InterprocStats::default());
let interproc_ctx = ctx
.callee_bodies
.map(|bodies| super::interproc::InterprocCtx {
callee_bodies: bodies,
cfg,
lang: ctx.lang,
max_depth: super::interproc::DEFAULT_MAX_DEPTH,
budget: &interproc_budget,
cache: &interproc_cache,
reentry_counts: &interproc_reentry,
max_reentry_per_func: super::interproc::DEFAULT_MAX_REENTRY_PER_FUNC,
scc_membership: ctx.scc_membership,
max_scc_reentry: super::interproc::DEFAULT_MAX_SCC_REENTRY,
stats: &interproc_stats,
cross_file_bodies: ctx.cross_file_bodies,
cross_file_depth: 0,
caller_namespace: ctx.namespace,
});
let interproc_ctx_ref = interproc_ctx.as_ref();
#[cfg(feature = "smt")]
let mut smt_ctx = if super::smt_enabled() {
Some(super::smt::SmtContext::new())
} else {
None
};
while let Some(mut state) = work_queue.pop_front() {
if outcomes.len() >= MAX_PATHS_PER_FINDING {
paths_pruned += 1;
search_exhausted = false;
continue;
}
if total_steps >= MAX_TOTAL_STEPS {
paths_pruned += 1;
search_exhausted = false;
continue;
}
let outcome = run_path(
&mut state,
ssa,
cfg,
const_values,
&reachable,
&on_path,
&loop_info,
&exception_succs,
&mut work_queue,
&mut outcomes,
&mut total_steps,
&mut search_exhausted,
finding,
summary_ctx_ref,
heap_ctx_ref,
interproc_ctx_ref,
#[cfg(feature = "smt")]
&mut smt_ctx,
);
if let Some(outcome) = outcome {
outcomes.push(outcome);
}
}
ExplorationResult {
paths_completed: outcomes,
paths_pruned,
total_steps,
search_exhausted,
interproc_findings: Vec::new(),
interproc_cutoffs: Vec::new(),
}
}
#[allow(clippy::too_many_arguments)]
fn run_path(
state: &mut ExplorationState,
ssa: &SsaBody,
cfg: &Cfg,
const_values: &HashMap<SsaValue, ConstLattice>,
reachable: &HashSet<BlockId>,
on_path: &HashSet<BlockId>,
loop_info: &LoopInfo,
exception_succs: &HashMap<BlockId, SmallVec<[BlockId; 2]>>,
work_queue: &mut VecDeque<ExplorationState>,
outcomes: &mut Vec<PathOutcome>,
total_steps: &mut usize,
search_exhausted: &mut bool,
finding: &Finding,
summary_ctx: Option<&SymexSummaryCtx>,
heap_ctx: Option<&SymexHeapCtx>,
interproc_ctx: Option<&super::interproc::InterprocCtx>,
#[cfg(feature = "smt")] smt_ctx: &mut Option<super::smt::SmtContext>,
) -> Option<PathOutcome> {
loop {
if *total_steps >= MAX_TOTAL_STEPS {
*search_exhausted = false;
return Some(record_outcome(state, finding, ssa, cfg));
}
let block_id = state.current_block;
let block = match ssa.blocks.get(block_id.0 as usize) {
Some(b) => b,
None => {
let witness = try_extract_witness(state, finding, ssa, cfg);
return Some(PathOutcome {
verdict: Verdict::Inconclusive,
constraints_checked: state.constraints_checked,
witness,
});
}
};
let visit_count = {
let count = state.visit_counts.entry(block_id).or_insert(0);
*count = count.saturating_add(1);
*count
};
if loop_info.loop_heads.contains(&block_id) && visit_count > super::loops::MAX_LOOP_UNROLL {
state.sym_state.widen_at_loop_head(block_id, ssa);
if let Some(exit_blk) = loop_info.loop_exit_successor(ssa, block_id) {
if reachable.contains(&exit_blk) {
state.predecessor = Some(block_id);
state.current_block = exit_blk;
continue;
}
}
if let Terminator::Branch {
true_blk,
false_blk,
..
} = &block.terminator
{
let body = loop_info.loop_bodies.get(&block_id);
let candidates = [*true_blk, *false_blk];
let exit = candidates
.iter()
.find(|blk| on_path.contains(blk) && body.map_or(true, |b| !b.contains(blk)));
if let Some(&exit_blk) = exit {
state.predecessor = Some(block_id);
state.current_block = exit_blk;
continue;
}
}
return Some(record_outcome(state, finding, ssa, cfg));
}
if let Some(exc_val) = state.exception_context.take() {
state.sym_state.set_exception_context(exc_val);
}
let lang = summary_ctx.map(|c| c.lang).or(heap_ctx.map(|c| c.lang));
transfer::transfer_block_with_predecessor(
&mut state.sym_state,
block,
cfg,
ssa,
state.predecessor,
summary_ctx,
heap_ctx,
interproc_ctx,
lang,
None, );
if loop_info.loop_heads.contains(&block_id) && visit_count > 1 {
for phi in &block.phis {
if loop_info.induction_vars.contains(&phi.value) {
state.sym_state.set(phi.value, SymbolicValue::Unknown);
}
}
}
let step_count = block.phis.len() + block.body.len();
state.steps_taken += step_count;
*total_steps += step_count;
if let Some(catch_blocks) = exception_succs.get(&block_id) {
for &catch_blk in catch_blocks {
if !reachable.contains(&catch_blk) {
continue;
}
let can_fork = state.forks_used < MAX_FORKS_PER_FINDING
&& outcomes.len() + work_queue.len() + 1 < MAX_PATHS_PER_FINDING
&& *total_steps < MAX_TOTAL_STEPS;
if can_fork {
let exc_state = ExplorationState {
sym_state: state.sym_state.clone(),
env: constraint::PathEnv::empty(),
current_block: catch_blk,
predecessor: Some(block_id),
forks_used: state.forks_used + 1,
steps_taken: state.steps_taken,
constraints_checked: state.constraints_checked,
visit_counts: state.visit_counts.clone(),
exception_context: Some(SymbolicValue::Unknown),
};
work_queue.push_back(exc_state);
} else {
*search_exhausted = false;
}
}
}
match &block.terminator {
Terminator::Branch {
cond,
true_blk,
false_blk,
condition,
} => {
let true_reachable = reachable.contains(true_blk);
let false_reachable = reachable.contains(false_blk);
match (true_reachable, false_reachable) {
(false, false) => {
let witness = try_extract_witness(state, finding, ssa, cfg);
return Some(PathOutcome {
verdict: Verdict::Inconclusive,
constraints_checked: state.constraints_checked,
witness,
});
}
(true, false) => {
if let Some(outcome) = apply_branch_constraint(
state,
cfg,
ssa,
const_values,
block_id,
*cond,
condition,
true,
#[cfg(feature = "smt")]
smt_ctx,
) {
return Some(outcome);
}
state.predecessor = Some(block_id);
state.current_block = *true_blk;
}
(false, true) => {
if let Some(outcome) = apply_branch_constraint(
state,
cfg,
ssa,
const_values,
block_id,
*cond,
condition,
false,
#[cfg(feature = "smt")]
smt_ctx,
) {
return Some(outcome);
}
state.predecessor = Some(block_id);
state.current_block = *false_blk;
}
(true, true) => {
let can_fork = state.forks_used < MAX_FORKS_PER_FINDING
&& outcomes.len() + work_queue.len() + 1 < MAX_PATHS_PER_FINDING
&& *total_steps < MAX_TOTAL_STEPS;
if can_fork {
return fork_at_branch(
state,
cfg,
ssa,
const_values,
block_id,
*cond,
condition,
*true_blk,
*false_blk,
work_queue,
outcomes,
#[cfg(feature = "smt")]
smt_ctx,
);
} else {
*search_exhausted = false;
let preferred_polarity = if on_path.contains(true_blk) {
true
} else if on_path.contains(false_blk) {
false
} else {
true };
let target = if preferred_polarity {
*true_blk
} else {
*false_blk
};
if let Some(outcome) = apply_branch_constraint(
state,
cfg,
ssa,
const_values,
block_id,
*cond,
condition,
preferred_polarity,
#[cfg(feature = "smt")]
smt_ctx,
) {
return Some(outcome);
}
state.predecessor = Some(block_id);
state.current_block = target;
}
}
}
}
Terminator::Goto(target) => {
if !reachable.contains(target) {
let witness = try_extract_witness(state, finding, ssa, cfg);
return Some(PathOutcome {
verdict: Verdict::Inconclusive,
constraints_checked: state.constraints_checked,
witness,
});
}
state.predecessor = Some(block_id);
state.current_block = *target;
}
Terminator::Switch {
scrutinee,
targets,
default,
case_values,
} => {
if let Some(outcome) = step_switch(
state,
*scrutinee,
targets,
*default,
case_values,
block_id,
reachable,
work_queue,
outcomes,
search_exhausted,
finding,
ssa,
cfg,
) {
return Some(outcome);
}
}
Terminator::Return(_) | Terminator::Unreachable => {
return Some(record_outcome(state, finding, ssa, cfg));
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn step_switch(
state: &mut ExplorationState,
scrutinee: SsaValue,
targets: &smallvec::SmallVec<[BlockId; 4]>,
default: BlockId,
case_values: &smallvec::SmallVec<[Option<crate::constraint::domain::ConstValue>; 4]>,
block_id: BlockId,
reachable: &HashSet<BlockId>,
work_queue: &mut VecDeque<ExplorationState>,
outcomes: &mut Vec<PathOutcome>,
search_exhausted: &mut bool,
finding: &Finding,
ssa: &SsaBody,
cfg: &Cfg,
) -> Option<PathOutcome> {
use crate::constraint::lower::{CompOp, ConditionExpr, Operand};
let known_cases: Vec<(crate::constraint::domain::ConstValue, BlockId)> = targets
.iter()
.enumerate()
.filter_map(|(i, &tgt)| {
if !reachable.contains(&tgt) {
return None;
}
case_values
.get(i)
.and_then(|cv| cv.clone())
.map(|lit| (lit, tgt))
})
.collect();
let default_reachable = reachable.contains(&default);
if known_cases.is_empty() {
let next = std::iter::once(&default)
.chain(targets.iter())
.find(|s| reachable.contains(s))
.copied();
match next {
Some(target) => {
state.predecessor = Some(block_id);
state.current_block = target;
return None;
}
None => {
let witness = try_extract_witness(state, finding, ssa, cfg);
return Some(PathOutcome {
verdict: Verdict::Inconclusive,
constraints_checked: state.constraints_checked,
witness,
});
}
}
}
let mk_eq = |lit: &crate::constraint::domain::ConstValue| ConditionExpr::Comparison {
lhs: Operand::Value(scrutinee),
op: CompOp::Eq,
rhs: Operand::Const(lit.clone()),
};
let total_paths_planned = known_cases.len() + if default_reachable { 1 } else { 0 };
let can_fork_per_path = |outcomes_len: usize, queue_len: usize, planned: usize| -> bool {
state.forks_used < MAX_FORKS_PER_FINDING
&& outcomes_len + queue_len + planned + 1 < MAX_PATHS_PER_FINDING
};
let mut planned_remaining = total_paths_planned;
let mut any_enqueued = false;
for (lit, target) in &known_cases {
if !can_fork_per_path(outcomes.len(), work_queue.len(), planned_remaining) {
*search_exhausted = false;
break;
}
planned_remaining = planned_remaining.saturating_sub(1);
let mut case_state = ExplorationState {
sym_state: state.sym_state.clone(),
env: state.env.clone(),
current_block: *target,
predecessor: Some(block_id),
forks_used: state.forks_used + 1,
steps_taken: state.steps_taken,
constraints_checked: state.constraints_checked,
visit_counts: state.visit_counts.clone(),
exception_context: None,
};
let cond = mk_eq(lit);
case_state.sym_state.add_constraint(PathConstraint {
block: block_id,
condition: cond.clone(),
polarity: true,
});
case_state.env = constraint::refine_env(&case_state.env, &cond, true);
case_state.constraints_checked += 1;
if case_state.env.is_unsat() {
outcomes.push(PathOutcome {
verdict: Verdict::Infeasible,
constraints_checked: case_state.constraints_checked,
witness: None,
});
} else {
work_queue.push_back(case_state);
any_enqueued = true;
}
}
if default_reachable && can_fork_per_path(outcomes.len(), work_queue.len(), planned_remaining) {
planned_remaining = planned_remaining.saturating_sub(1);
let mut default_state = ExplorationState {
sym_state: state.sym_state.clone(),
env: state.env.clone(),
current_block: default,
predecessor: Some(block_id),
forks_used: state.forks_used + 1,
steps_taken: state.steps_taken,
constraints_checked: state.constraints_checked,
visit_counts: state.visit_counts.clone(),
exception_context: None,
};
for (lit, _) in &known_cases {
let cond = mk_eq(lit);
default_state.sym_state.add_constraint(PathConstraint {
block: block_id,
condition: cond.clone(),
polarity: false,
});
default_state.env = constraint::refine_env(&default_state.env, &cond, false);
default_state.constraints_checked += 1;
}
if default_state.env.is_unsat() {
outcomes.push(PathOutcome {
verdict: Verdict::Infeasible,
constraints_checked: default_state.constraints_checked,
witness: None,
});
} else {
work_queue.push_back(default_state);
any_enqueued = true;
}
} else if default_reachable {
*search_exhausted = false;
}
let _ = planned_remaining;
if !any_enqueued {
let witness = try_extract_witness(state, finding, ssa, cfg);
return Some(PathOutcome {
verdict: Verdict::Infeasible,
constraints_checked: state.constraints_checked,
witness,
});
}
None
}
#[allow(clippy::too_many_arguments)]
fn apply_branch_constraint(
state: &mut ExplorationState,
cfg: &Cfg,
ssa: &SsaBody,
const_values: &HashMap<SsaValue, ConstLattice>,
block_id: BlockId,
cond: petgraph::graph::NodeIndex,
pre_lowered: &Option<Box<constraint::ConditionExpr>>,
polarity: bool,
#[cfg(feature = "smt")] smt_ctx: &mut Option<super::smt::SmtContext>,
) -> Option<PathOutcome> {
let cond_expr = if let Some(pre) = pre_lowered {
(**pre).clone()
} else {
constraint::lower_condition(&cfg[cond], ssa, block_id, Some(const_values))
};
if matches!(cond_expr, constraint::ConditionExpr::Unknown) {
return None;
}
state.sym_state.add_constraint(PathConstraint {
block: block_id,
condition: cond_expr.clone(),
polarity,
});
state.env = constraint::refine_env(&state.env, &cond_expr, polarity);
state.constraints_checked += 1;
if state.env.is_unsat() {
return Some(PathOutcome {
verdict: Verdict::Infeasible,
constraints_checked: state.constraints_checked,
witness: None,
});
}
#[cfg(feature = "smt")]
if let Some(smt) = smt_ctx {
if super::smt::should_escalate(state.sym_state.path_constraints()) && smt.has_budget() {
if let super::smt::SmtResult::Unsat = smt.check_path_feasibility(
state.sym_state.path_constraints(),
&state.sym_state,
&state.env,
) {
return Some(PathOutcome {
verdict: Verdict::Infeasible,
constraints_checked: state.constraints_checked,
witness: None,
});
}
}
}
None
}
#[allow(clippy::too_many_arguments)]
fn fork_at_branch(
state: &mut ExplorationState,
cfg: &Cfg,
ssa: &SsaBody,
const_values: &HashMap<SsaValue, ConstLattice>,
block_id: BlockId,
cond: petgraph::graph::NodeIndex,
pre_lowered: &Option<Box<constraint::ConditionExpr>>,
true_blk: BlockId,
false_blk: BlockId,
work_queue: &mut VecDeque<ExplorationState>,
outcomes: &mut Vec<PathOutcome>,
#[cfg(feature = "smt")] smt_ctx: &mut Option<super::smt::SmtContext>,
) -> Option<PathOutcome> {
let cond_expr = if let Some(pre) = pre_lowered {
(**pre).clone()
} else {
constraint::lower_condition(&cfg[cond], ssa, block_id, Some(const_values))
};
let is_unknown = matches!(cond_expr, constraint::ConditionExpr::Unknown);
let mut true_state = ExplorationState {
sym_state: state.sym_state.clone(),
env: state.env.clone(),
current_block: true_blk,
predecessor: Some(block_id),
forks_used: state.forks_used + 1,
steps_taken: state.steps_taken,
constraints_checked: state.constraints_checked,
visit_counts: state.visit_counts.clone(),
exception_context: None,
};
if !is_unknown {
true_state.sym_state.add_constraint(PathConstraint {
block: block_id,
condition: cond_expr.clone(),
polarity: true,
});
true_state.env = constraint::refine_env(&true_state.env, &cond_expr, true);
true_state.constraints_checked += 1;
}
let mut false_state = ExplorationState {
sym_state: state.sym_state.clone(),
env: state.env.clone(),
current_block: false_blk,
predecessor: Some(block_id),
forks_used: state.forks_used + 1,
steps_taken: state.steps_taken,
constraints_checked: state.constraints_checked,
visit_counts: state.visit_counts.clone(),
exception_context: None,
};
if !is_unknown {
false_state.sym_state.add_constraint(PathConstraint {
block: block_id,
condition: cond_expr.clone(),
polarity: false,
});
false_state.env = constraint::refine_env(&false_state.env, &cond_expr, false);
false_state.constraints_checked += 1;
}
let true_infeasible = true_state.env.is_unsat() || {
#[cfg(feature = "smt")]
{
smt_check_infeasible(smt_ctx, &true_state)
}
#[cfg(not(feature = "smt"))]
false
};
if true_infeasible {
outcomes.push(PathOutcome {
verdict: Verdict::Infeasible,
constraints_checked: true_state.constraints_checked,
witness: None,
});
} else {
work_queue.push_back(true_state);
}
let false_infeasible = false_state.env.is_unsat() || {
#[cfg(feature = "smt")]
{
smt_check_infeasible(smt_ctx, &false_state)
}
#[cfg(not(feature = "smt"))]
false
};
if false_infeasible {
outcomes.push(PathOutcome {
verdict: Verdict::Infeasible,
constraints_checked: false_state.constraints_checked,
witness: None,
});
} else {
work_queue.push_back(false_state);
}
None
}
#[cfg(feature = "smt")]
fn smt_check_infeasible(
smt_ctx: &mut Option<super::smt::SmtContext>,
state: &ExplorationState,
) -> bool {
if let Some(smt) = smt_ctx {
if super::smt::should_escalate(state.sym_state.path_constraints()) && smt.has_budget() {
return matches!(
smt.check_path_feasibility(
state.sym_state.path_constraints(),
&state.sym_state,
&state.env,
),
super::smt::SmtResult::Unsat
);
}
}
false
}
fn record_outcome(
state: &ExplorationState,
finding: &Finding,
ssa: &SsaBody,
cfg: &Cfg,
) -> PathOutcome {
let witness = try_extract_witness(state, finding, ssa, cfg);
let verdict = Verdict::Confirmed;
PathOutcome {
verdict,
constraints_checked: state.constraints_checked,
witness,
}
}
fn try_extract_witness(
state: &ExplorationState,
finding: &Finding,
ssa: &SsaBody,
cfg: &Cfg,
) -> Option<String> {
super::witness::extract_witness(&state.sym_state, finding, ssa, cfg)
.or_else(|| state.sym_state.get_sink_witness(finding, ssa))
}
impl ExplorationResult {
pub fn aggregate_verdict(&self) -> SymbolicVerdict {
let paths_explored = self.paths_completed.len() as u32;
let constraints_checked: u32 = self
.paths_completed
.iter()
.map(|p| p.constraints_checked)
.sum();
let has_confirmed = self
.paths_completed
.iter()
.any(|p| p.verdict == Verdict::Confirmed);
let all_infeasible = !self.paths_completed.is_empty()
&& self
.paths_completed
.iter()
.all(|p| p.verdict == Verdict::Infeasible);
let verdict = if has_confirmed {
Verdict::Confirmed
} else if all_infeasible && self.search_exhausted {
Verdict::Infeasible
} else {
Verdict::Inconclusive
};
let witness = self
.paths_completed
.iter()
.filter(|p| p.verdict == Verdict::Confirmed)
.find_map(|p| p.witness.clone())
.or_else(|| self.paths_completed.iter().find_map(|p| p.witness.clone()));
let mut interproc_call_chains: Vec<Vec<String>> = Vec::new();
let mut seen_chains: std::collections::HashSet<String> = std::collections::HashSet::new();
for finding in &self.interproc_findings {
let key = finding.call_chain.join(" → ");
if seen_chains.insert(key) {
interproc_call_chains.push(finding.call_chain.clone());
}
}
let mut cutoff_notes: Vec<String> = Vec::new();
let mut seen_notes: std::collections::HashSet<String> = std::collections::HashSet::new();
for reason in &self.interproc_cutoffs {
let note = format!("{}", reason);
if seen_notes.insert(note.clone()) {
cutoff_notes.push(note);
}
}
let enriched_witness =
append_interproc_context(witness, &interproc_call_chains, &cutoff_notes);
SymbolicVerdict {
verdict,
constraints_checked,
paths_explored,
witness: enriched_witness,
interproc_call_chains,
cutoff_notes,
}
}
}
fn append_interproc_context(
witness: Option<String>,
call_chains: &[Vec<String>],
cutoff_notes: &[String],
) -> Option<String> {
if call_chains.is_empty() && cutoff_notes.is_empty() {
return witness;
}
let mut result = witness.unwrap_or_default();
if !call_chains.is_empty() {
for chain in call_chains {
if !chain.is_empty() {
if !result.is_empty() {
result.push(' ');
}
result.push_str("[via ");
result.push_str(&chain.join(" → "));
result.push(']');
}
}
}
if !cutoff_notes.is_empty() {
if !result.is_empty() {
result.push(' ');
}
result.push_str("[cutoff: ");
result.push_str(&cutoff_notes.join("; "));
result.push(']');
}
if result.is_empty() {
None
} else {
Some(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ssa::ir::{SsaBlock, SsaInst, SsaOp, SsaValue, Terminator, ValueDef};
use crate::ssa::type_facts::TypeFactResult;
use crate::taint::FlowStepRaw;
use petgraph::graph::NodeIndex;
use smallvec::smallvec;
fn empty_type_facts() -> TypeFactResult {
TypeFactResult {
facts: HashMap::new(),
}
}
fn make_value_def(block: BlockId, cfg_node: NodeIndex) -> ValueDef {
ValueDef {
var_name: None,
cfg_node,
block,
}
}
fn make_finding(n0: NodeIndex, n1: NodeIndex) -> Finding {
Finding {
body_id: crate::cfg::BodyId(0),
sink: n1,
source: n0,
path: vec![n0, n1],
source_kind: crate::labels::SourceKind::UserInput,
path_validated: false,
guard_kind: None,
hop_count: 1,
cap_specificity: 1,
uses_summary: false,
flow_steps: vec![
FlowStepRaw {
cfg_node: n0,
var_name: Some("x".into()),
op_kind: crate::evidence::FlowStepKind::Source,
},
FlowStepRaw {
cfg_node: n1,
var_name: Some("x".into()),
op_kind: crate::evidence::FlowStepKind::Sink,
},
],
symbolic: None,
source_span: None,
primary_location: None,
engine_notes: smallvec::SmallVec::new(),
path_hash: 0,
finding_id: String::new(),
alternative_finding_ids: smallvec::SmallVec::new(),
effective_sink_caps: crate::labels::Cap::empty(),
}
}
#[test]
fn reachable_diamond_excludes_dead_end() {
let b0 = BlockId(0);
let b1 = BlockId(1);
let b2 = BlockId(2);
let b3 = BlockId(3);
let ssa = SsaBody {
blocks: vec![
SsaBlock {
id: b0,
phis: vec![],
body: vec![],
terminator: Terminator::Goto(b1),
preds: smallvec![],
succs: smallvec![b1, b2],
},
SsaBlock {
id: b1,
phis: vec![],
body: vec![],
terminator: Terminator::Goto(b3),
preds: smallvec![b0],
succs: smallvec![b3],
},
SsaBlock {
id: b2,
phis: vec![],
body: vec![],
terminator: Terminator::Return(None),
preds: smallvec![b0],
succs: smallvec![],
},
SsaBlock {
id: b3,
phis: vec![],
body: vec![],
terminator: Terminator::Return(None),
preds: smallvec![b1],
succs: smallvec![],
},
],
entry: b0,
value_defs: vec![],
cfg_node_map: HashMap::new(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let empty_succs = HashMap::new();
let empty_preds = HashMap::new();
let reachable = compute_source_sink_reachable(&ssa, b0, b3, &empty_succs, &empty_preds);
assert!(reachable.contains(&b0));
assert!(reachable.contains(&b1));
assert!(reachable.contains(&b3));
assert!(!reachable.contains(&b2), "dead-end B2 should be excluded");
}
#[test]
fn reachable_diamond_includes_both_branches() {
let b0 = BlockId(0);
let b1 = BlockId(1);
let b2 = BlockId(2);
let b3 = BlockId(3);
let ssa = SsaBody {
blocks: vec![
SsaBlock {
id: b0,
phis: vec![],
body: vec![],
terminator: Terminator::Goto(b1),
preds: smallvec![],
succs: smallvec![b1, b2],
},
SsaBlock {
id: b1,
phis: vec![],
body: vec![],
terminator: Terminator::Goto(b3),
preds: smallvec![b0],
succs: smallvec![b3],
},
SsaBlock {
id: b2,
phis: vec![],
body: vec![],
terminator: Terminator::Goto(b3),
preds: smallvec![b0],
succs: smallvec![b3],
},
SsaBlock {
id: b3,
phis: vec![],
body: vec![],
terminator: Terminator::Return(None),
preds: smallvec![b1, b2],
succs: smallvec![],
},
],
entry: b0,
value_defs: vec![],
cfg_node_map: HashMap::new(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let empty_succs = HashMap::new();
let empty_preds = HashMap::new();
let reachable = compute_source_sink_reachable(&ssa, b0, b3, &empty_succs, &empty_preds);
assert_eq!(reachable.len(), 4);
assert!(reachable.contains(&b1));
assert!(reachable.contains(&b2));
}
#[test]
fn aggregate_any_confirmed_wins() {
let result = ExplorationResult {
paths_completed: vec![
PathOutcome {
verdict: Verdict::Infeasible,
constraints_checked: 1,
witness: None,
},
PathOutcome {
verdict: Verdict::Confirmed,
constraints_checked: 1,
witness: Some("sym(v0)".into()),
},
],
paths_pruned: 0,
total_steps: 10,
search_exhausted: true,
interproc_findings: Vec::new(),
interproc_cutoffs: Vec::new(),
};
let v = result.aggregate_verdict();
assert_eq!(v.verdict, Verdict::Confirmed);
assert_eq!(v.paths_explored, 2);
assert_eq!(v.witness, Some("sym(v0)".into()));
}
#[test]
fn aggregate_all_infeasible_exhausted() {
let result = ExplorationResult {
paths_completed: vec![
PathOutcome {
verdict: Verdict::Infeasible,
constraints_checked: 1,
witness: None,
},
PathOutcome {
verdict: Verdict::Infeasible,
constraints_checked: 2,
witness: None,
},
],
paths_pruned: 0,
total_steps: 10,
search_exhausted: true,
interproc_findings: Vec::new(),
interproc_cutoffs: Vec::new(),
};
let v = result.aggregate_verdict();
assert_eq!(v.verdict, Verdict::Infeasible);
assert_eq!(v.constraints_checked, 3);
}
#[test]
fn aggregate_all_infeasible_but_budget_hit_is_inconclusive() {
let result = ExplorationResult {
paths_completed: vec![PathOutcome {
verdict: Verdict::Infeasible,
constraints_checked: 1,
witness: None,
}],
paths_pruned: 2,
total_steps: 500,
search_exhausted: false, interproc_findings: Vec::new(),
interproc_cutoffs: Vec::new(),
};
let v = result.aggregate_verdict();
assert_eq!(v.verdict, Verdict::Inconclusive);
}
#[test]
fn aggregate_empty_is_inconclusive() {
let result = ExplorationResult {
paths_completed: vec![],
paths_pruned: 0,
total_steps: 0,
search_exhausted: true,
interproc_findings: Vec::new(),
interproc_cutoffs: Vec::new(),
};
let v = result.aggregate_verdict();
assert_eq!(v.verdict, Verdict::Inconclusive);
}
#[test]
fn explore_linear_no_fork() {
let n0 = NodeIndex::new(0);
let n1 = NodeIndex::new(1);
let b0 = BlockId(0);
let b1 = BlockId(1);
let ssa = SsaBody {
blocks: vec![
SsaBlock {
id: b0,
phis: vec![],
body: vec![],
terminator: Terminator::Goto(b1),
preds: smallvec![],
succs: smallvec![b1],
},
SsaBlock {
id: b1,
phis: vec![],
body: vec![],
terminator: Terminator::Return(None),
preds: smallvec![b0],
succs: smallvec![],
},
],
entry: b0,
value_defs: vec![make_value_def(b0, n0), make_value_def(b1, n1)],
cfg_node_map: [(n0, SsaValue(0)), (n1, SsaValue(1))].into_iter().collect(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let finding = make_finding(n0, n1);
let ctx = super::SymexContext {
ssa: &ssa,
cfg: &Cfg::new(),
const_values: &HashMap::new(),
type_facts: &empty_type_facts(),
global_summaries: None,
lang: crate::symbol::Lang::JavaScript,
namespace: "test.js",
points_to: None,
callee_bodies: None,
scc_membership: None,
cross_file_bodies: None,
};
let result = explore_finding(&finding, &ctx);
assert_eq!(result.paths_completed.len(), 1);
assert_eq!(result.paths_completed[0].verdict, Verdict::Confirmed);
assert!(result.search_exhausted);
let v = result.aggregate_verdict();
assert_eq!(v.verdict, Verdict::Confirmed);
assert_eq!(v.paths_explored, 1);
}
#[test]
fn explore_diamond_both_feasible() {
let n0 = NodeIndex::new(0);
let n1 = NodeIndex::new(1);
let n2 = NodeIndex::new(2);
let n3 = NodeIndex::new(3);
let n_cond = NodeIndex::new(0);
let b0 = BlockId(0);
let b1 = BlockId(1);
let b2 = BlockId(2);
let b3 = BlockId(3);
let mut cfg_graph = Cfg::new();
let _c = cfg_graph.add_node(crate::cfg::NodeInfo {
kind: crate::cfg::StmtKind::Seq,
..Default::default()
});
let ssa = SsaBody {
blocks: vec![
SsaBlock {
id: b0,
phis: vec![],
body: vec![],
terminator: Terminator::Branch {
cond: n_cond,
true_blk: b1,
false_blk: b2,
condition: None,
},
preds: smallvec![],
succs: smallvec![b1, b2],
},
SsaBlock {
id: b1,
phis: vec![],
body: vec![],
terminator: Terminator::Goto(b3),
preds: smallvec![b0],
succs: smallvec![b3],
},
SsaBlock {
id: b2,
phis: vec![],
body: vec![],
terminator: Terminator::Goto(b3),
preds: smallvec![b0],
succs: smallvec![b3],
},
SsaBlock {
id: b3,
phis: vec![],
body: vec![],
terminator: Terminator::Return(None),
preds: smallvec![b1, b2],
succs: smallvec![],
},
],
entry: b0,
value_defs: vec![
make_value_def(b0, n0),
make_value_def(b1, n1),
make_value_def(b2, n2),
make_value_def(b3, n3),
],
cfg_node_map: [
(n0, SsaValue(0)),
(n1, SsaValue(1)),
(n2, SsaValue(2)),
(n3, SsaValue(3)),
]
.into_iter()
.collect(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let finding = Finding {
body_id: crate::cfg::BodyId(0),
sink: n3,
source: n0,
path: vec![n0, n1, n3],
source_kind: crate::labels::SourceKind::UserInput,
path_validated: false,
guard_kind: None,
hop_count: 2,
cap_specificity: 1,
uses_summary: false,
flow_steps: vec![
FlowStepRaw {
cfg_node: n0,
var_name: Some("x".into()),
op_kind: crate::evidence::FlowStepKind::Source,
},
FlowStepRaw {
cfg_node: n1,
var_name: Some("x".into()),
op_kind: crate::evidence::FlowStepKind::Assignment,
},
FlowStepRaw {
cfg_node: n3,
var_name: Some("x".into()),
op_kind: crate::evidence::FlowStepKind::Sink,
},
],
symbolic: None,
source_span: None,
primary_location: None,
engine_notes: smallvec::SmallVec::new(),
path_hash: 0,
finding_id: String::new(),
alternative_finding_ids: smallvec::SmallVec::new(),
effective_sink_caps: crate::labels::Cap::empty(),
};
let ctx = super::SymexContext {
ssa: &ssa,
cfg: &cfg_graph,
const_values: &HashMap::new(),
type_facts: &empty_type_facts(),
global_summaries: None,
lang: crate::symbol::Lang::JavaScript,
namespace: "test.js",
points_to: None,
callee_bodies: None,
scc_membership: None,
cross_file_bodies: None,
};
let result = explore_finding(&finding, &ctx);
assert!(
result.paths_completed.len() >= 2,
"expected >= 2 paths, got {}",
result.paths_completed.len()
);
assert!(result.search_exhausted);
let v = result.aggregate_verdict();
assert_eq!(v.verdict, Verdict::Confirmed);
assert!(v.paths_explored >= 2);
}
#[test]
fn explore_branch_single_reachable_no_fork() {
let n0 = NodeIndex::new(0);
let n1 = NodeIndex::new(1);
let n3 = NodeIndex::new(3);
let n_cond = NodeIndex::new(0);
let b0 = BlockId(0);
let b1 = BlockId(1);
let b2 = BlockId(2);
let b3 = BlockId(3);
let mut cfg_graph = Cfg::new();
cfg_graph.add_node(crate::cfg::NodeInfo {
kind: crate::cfg::StmtKind::Seq,
..Default::default()
});
let ssa = SsaBody {
blocks: vec![
SsaBlock {
id: b0,
phis: vec![],
body: vec![],
terminator: Terminator::Branch {
cond: n_cond,
true_blk: b1,
false_blk: b2,
condition: None,
},
preds: smallvec![],
succs: smallvec![b1, b2],
},
SsaBlock {
id: b1,
phis: vec![],
body: vec![],
terminator: Terminator::Goto(b3),
preds: smallvec![b0],
succs: smallvec![b3],
},
SsaBlock {
id: b2,
phis: vec![],
body: vec![],
terminator: Terminator::Return(None), preds: smallvec![b0],
succs: smallvec![],
},
SsaBlock {
id: b3,
phis: vec![],
body: vec![],
terminator: Terminator::Return(None),
preds: smallvec![b1],
succs: smallvec![],
},
],
entry: b0,
value_defs: vec![
make_value_def(b0, n0),
make_value_def(b1, n1),
ValueDef {
var_name: None,
cfg_node: NodeIndex::new(2),
block: b2,
},
make_value_def(b3, n3),
],
cfg_node_map: [(n0, SsaValue(0)), (n1, SsaValue(1)), (n3, SsaValue(3))]
.into_iter()
.collect(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let finding = Finding {
body_id: crate::cfg::BodyId(0),
sink: n3,
source: n0,
path: vec![n0, n1, n3],
source_kind: crate::labels::SourceKind::UserInput,
path_validated: false,
guard_kind: None,
hop_count: 2,
cap_specificity: 1,
uses_summary: false,
flow_steps: vec![
FlowStepRaw {
cfg_node: n0,
var_name: Some("x".into()),
op_kind: crate::evidence::FlowStepKind::Source,
},
FlowStepRaw {
cfg_node: n1,
var_name: Some("x".into()),
op_kind: crate::evidence::FlowStepKind::Assignment,
},
FlowStepRaw {
cfg_node: n3,
var_name: Some("x".into()),
op_kind: crate::evidence::FlowStepKind::Sink,
},
],
symbolic: None,
source_span: None,
primary_location: None,
engine_notes: smallvec::SmallVec::new(),
path_hash: 0,
finding_id: String::new(),
alternative_finding_ids: smallvec::SmallVec::new(),
effective_sink_caps: crate::labels::Cap::empty(),
};
let ctx = super::SymexContext {
ssa: &ssa,
cfg: &cfg_graph,
const_values: &HashMap::new(),
type_facts: &empty_type_facts(),
global_summaries: None,
lang: crate::symbol::Lang::JavaScript,
namespace: "test.js",
points_to: None,
callee_bodies: None,
scc_membership: None,
cross_file_bodies: None,
};
let result = explore_finding(&finding, &ctx);
assert_eq!(result.paths_completed.len(), 1);
assert_eq!(result.paths_completed[0].verdict, Verdict::Confirmed);
assert!(result.search_exhausted);
}
#[test]
fn reachable_includes_exception_edges() {
let b0 = BlockId(0);
let b1 = BlockId(1);
let b2 = BlockId(2);
let b3 = BlockId(3);
let ssa = SsaBody {
blocks: vec![
SsaBlock {
id: b0,
phis: vec![],
body: vec![],
terminator: Terminator::Goto(b1),
preds: smallvec![],
succs: smallvec![b1],
},
SsaBlock {
id: b1,
phis: vec![],
body: vec![],
terminator: Terminator::Return(None),
preds: smallvec![b0],
succs: smallvec![],
},
SsaBlock {
id: b2,
phis: vec![],
body: vec![],
terminator: Terminator::Goto(b3),
preds: smallvec![], succs: smallvec![b3],
},
SsaBlock {
id: b3,
phis: vec![],
body: vec![],
terminator: Terminator::Return(None),
preds: smallvec![b2],
succs: smallvec![],
},
],
entry: b0,
value_defs: vec![],
cfg_node_map: HashMap::new(),
exception_edges: vec![(b0, b2)],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let mut exc_succs: HashMap<BlockId, SmallVec<[BlockId; 2]>> = HashMap::new();
exc_succs.insert(b0, smallvec![b2]);
let mut exc_preds: HashMap<BlockId, SmallVec<[BlockId; 2]>> = HashMap::new();
exc_preds.insert(b2, smallvec![b0]);
let reachable = compute_source_sink_reachable(&ssa, b0, b3, &exc_succs, &exc_preds);
assert!(reachable.contains(&b0), "source should be reachable");
assert!(
reachable.contains(&b2),
"catch block should be reachable via exception edge"
);
assert!(reachable.contains(&b3), "sink should be reachable");
assert!(!reachable.contains(&b1), "B1 is NOT on any path to B3");
}
#[test]
fn reachable_exception_backward() {
let b0 = BlockId(0);
let b1 = BlockId(1);
let b2 = BlockId(2);
let b3 = BlockId(3);
let ssa = SsaBody {
blocks: vec![
SsaBlock {
id: b0,
phis: vec![],
body: vec![],
terminator: Terminator::Goto(b1),
preds: smallvec![],
succs: smallvec![b1],
},
SsaBlock {
id: b1,
phis: vec![],
body: vec![],
terminator: Terminator::Return(None),
preds: smallvec![b0],
succs: smallvec![],
},
SsaBlock {
id: b2,
phis: vec![],
body: vec![],
terminator: Terminator::Goto(b3),
preds: smallvec![],
succs: smallvec![b3],
},
SsaBlock {
id: b3,
phis: vec![],
body: vec![],
terminator: Terminator::Return(None),
preds: smallvec![b2],
succs: smallvec![],
},
],
entry: b0,
value_defs: vec![],
cfg_node_map: HashMap::new(),
exception_edges: vec![(b0, b2)],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let mut exc_succs: HashMap<BlockId, SmallVec<[BlockId; 2]>> = HashMap::new();
exc_succs.insert(b0, smallvec![b2]);
let mut exc_preds: HashMap<BlockId, SmallVec<[BlockId; 2]>> = HashMap::new();
exc_preds.insert(b2, smallvec![b0]);
let reachable = compute_source_sink_reachable(&ssa, b0, b3, &exc_succs, &exc_preds);
assert!(reachable.contains(&b0));
assert!(reachable.contains(&b2));
assert!(reachable.contains(&b3));
assert!(!reachable.contains(&b1));
}
#[test]
fn exception_fork_catch_param_tainted() {
let b0 = BlockId(0);
let b1 = BlockId(1);
let b2 = BlockId(2);
let b3 = BlockId(3);
let n0 = NodeIndex::new(0);
let n1 = NodeIndex::new(1);
let n2 = NodeIndex::new(2);
let n3 = NodeIndex::new(3);
let ssa = SsaBody {
blocks: vec![
SsaBlock {
id: b0,
phis: vec![],
body: vec![SsaInst {
value: SsaValue(0),
op: SsaOp::Source,
cfg_node: n0,
var_name: Some("x".into()),
span: (0, 0),
}],
terminator: Terminator::Goto(b1),
preds: smallvec![],
succs: smallvec![b1],
},
SsaBlock {
id: b1,
phis: vec![],
body: vec![SsaInst {
value: SsaValue(1),
op: SsaOp::Call {
callee: "JSON.parse".into(),
callee_text: None,
args: vec![smallvec![SsaValue(0)]],
receiver: None,
},
cfg_node: n1,
var_name: None,
span: (0, 0),
}],
terminator: Terminator::Goto(b3),
preds: smallvec![b0],
succs: smallvec![b3],
},
SsaBlock {
id: b2,
phis: vec![],
body: vec![SsaInst {
value: SsaValue(2),
op: SsaOp::CatchParam,
cfg_node: n2,
var_name: Some("e".into()),
span: (0, 0),
}],
terminator: Terminator::Goto(b3),
preds: smallvec![], succs: smallvec![b3],
},
SsaBlock {
id: b3,
phis: vec![],
body: vec![],
terminator: Terminator::Return(None),
preds: smallvec![b1, b2],
succs: smallvec![],
},
],
entry: b0,
value_defs: vec![
make_value_def(b0, n0),
make_value_def(b1, n1),
make_value_def(b2, n2),
make_value_def(b3, n3),
],
cfg_node_map: [
(n0, SsaValue(0)),
(n1, SsaValue(1)),
(n2, SsaValue(2)),
(n3, SsaValue(3)),
]
.into_iter()
.collect(),
exception_edges: vec![(b1, b2)],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let finding = Finding {
body_id: crate::cfg::BodyId(0),
sink: n3,
source: n0,
path: vec![n0, n1, n3],
source_kind: crate::labels::SourceKind::UserInput,
path_validated: false,
guard_kind: None,
hop_count: 2,
cap_specificity: 1,
uses_summary: false,
flow_steps: vec![
FlowStepRaw {
cfg_node: n0,
var_name: Some("x".into()),
op_kind: crate::evidence::FlowStepKind::Source,
},
FlowStepRaw {
cfg_node: n3,
var_name: Some("x".into()),
op_kind: crate::evidence::FlowStepKind::Sink,
},
],
symbolic: None,
source_span: None,
primary_location: None,
engine_notes: smallvec::SmallVec::new(),
path_hash: 0,
finding_id: String::new(),
alternative_finding_ids: smallvec::SmallVec::new(),
effective_sink_caps: crate::labels::Cap::empty(),
};
let cfg_graph = crate::cfg::Cfg::new();
let ctx = super::SymexContext {
ssa: &ssa,
cfg: &cfg_graph,
const_values: &HashMap::new(),
type_facts: &empty_type_facts(),
global_summaries: None,
lang: crate::symbol::Lang::JavaScript,
namespace: "test.js",
points_to: None,
callee_bodies: None,
scc_membership: None,
cross_file_bodies: None,
};
let result = explore_finding(&finding, &ctx);
assert!(
result.paths_completed.len() >= 2,
"Expected at least 2 paths (normal + exception), got {}",
result.paths_completed.len()
);
assert!(
result
.paths_completed
.iter()
.any(|p| p.verdict == Verdict::Confirmed),
"Expected at least one Confirmed path via exception fork"
);
}
}