#![allow(
clippy::collapsible_if,
clippy::if_same_then_else,
clippy::manual_flatten,
clippy::needless_range_loop,
clippy::only_used_in_recursion,
clippy::single_match,
clippy::too_many_arguments,
clippy::unnecessary_map_or
)]
mod events;
mod inline;
mod state;
mod summary_extract;
#[cfg(test)]
mod tests;
pub use events::{SsaTaintEvent, ssa_events_to_findings};
pub(crate) use inline::{ArgTaintSig, InlineCache};
use inline::{CachedInlineShape, InlineResult, MAX_INLINE_BLOCKS, ReturnShape};
pub use inline::{CalleeSsaBody, CrossFileNodeMeta, populate_node_meta, rebuild_body_graph};
#[allow(unused_imports)] pub(crate) use inline::{inline_cache_clear_epoch, inline_cache_fingerprint};
pub use state::{
BindingKey, SsaTaintState, max_worklist_iterations, origins_truncation_count,
reset_all_validated_spans, reset_origins_observability, reset_path_safe_suppressed_spans,
reset_worklist_observability, seed_lookup, set_max_origins_override, set_worklist_cap_override,
take_all_validated_spans, take_path_safe_suppressed_spans, worklist_cap_hit_count,
};
use state::{
MAX_WORKLIST_ITERATIONS, ORIGINS_TRUNCATION_COUNT, WORKLIST_CAP_HITS, effective_max_origins,
effective_worklist_cap,
};
pub(crate) use state::{
push_origin_bounded, record_engine_note, reset_body_engine_notes, take_body_engine_notes,
};
pub use summary_extract::{extract_ssa_func_summary, extract_ssa_func_summary_full};
use crate::abstract_interp::AbstractState;
use crate::callgraph::{callee_container_hint, callee_leaf_name};
use crate::cfg::{BodyId, Cfg, FuncSummaries, NodeInfo};
use crate::constraint;
use crate::interop::InteropEdge;
use crate::labels::{Cap, DataLabel, RuntimeLabelRule, SourceKind};
use crate::ssa::heap::{HeapObjectId, HeapSlot, PointsToResult, PointsToSet};
use crate::ssa::ir::*;
use crate::state::lattice::Lattice;
use crate::state::symbol::SymbolInterner;
use crate::summary::{CalleeQuery, CalleeResolution, GlobalSummaries, SinkSite};
use crate::symbol::{FuncKey, Lang};
use crate::taint::domain::{PredicateSummary, TaintOrigin, VarTaint, predicate_kind_bit};
use crate::taint::path_state::{PredicateKind, classify_condition_with_target};
use petgraph::graph::NodeIndex;
use smallvec::SmallVec;
use std::cell::RefCell;
use std::collections::{HashMap, HashSet, VecDeque};
pub struct SsaTaintTransfer<'a> {
pub lang: Lang,
pub namespace: &'a str,
pub interner: &'a SymbolInterner,
pub local_summaries: &'a FuncSummaries,
pub global_summaries: Option<&'a GlobalSummaries>,
pub interop_edges: &'a [InteropEdge],
pub owner_body_id: BodyId,
pub parent_body_id: Option<BodyId>,
pub global_seed: Option<&'a HashMap<BindingKey, VarTaint>>,
pub param_seed: Option<&'a [Option<VarTaint>]>,
pub receiver_seed: Option<&'a VarTaint>,
pub const_values: Option<&'a HashMap<SsaValue, crate::ssa::const_prop::ConstLattice>>,
pub type_facts: Option<&'a crate::ssa::type_facts::TypeFactResult>,
pub ssa_summaries: Option<&'a HashMap<FuncKey, crate::summary::ssa_summary::SsaFuncSummary>>,
pub extra_labels: Option<&'a [RuntimeLabelRule]>,
pub callee_bodies: Option<&'a HashMap<FuncKey, CalleeSsaBody>>,
pub(crate) inline_cache: Option<&'a RefCell<InlineCache>>,
pub base_aliases: Option<&'a crate::ssa::alias::BaseAliasResult>,
pub context_depth: u8,
pub callback_bindings: Option<&'a HashMap<String, FuncKey>>,
pub points_to: Option<&'a PointsToResult>,
pub dynamic_pts: Option<&'a RefCell<HashMap<SsaValue, PointsToSet>>>,
pub import_bindings: Option<&'a crate::cfg::ImportBindings>,
pub promisify_aliases: Option<&'a crate::cfg::PromisifyAliases>,
pub module_aliases: Option<&'a HashMap<SsaValue, smallvec::SmallVec<[String; 2]>>>,
pub static_map: Option<&'a crate::ssa::static_map::StaticMapResult>,
pub auto_seed_handler_params: bool,
pub cross_file_bodies: Option<&'a HashMap<FuncKey, CalleeSsaBody>>,
pub pointer_facts: Option<&'a crate::pointer::PointsToFacts>,
}
type PredStates = HashMap<(usize, usize), SsaTaintState>;
struct SsaTaintRunResult {
events: Vec<SsaTaintEvent>,
block_states: Vec<Option<SsaTaintState>>,
block_exit_states: Vec<Option<SsaTaintState>>,
}
pub fn run_ssa_taint_full(
ssa: &SsaBody,
cfg: &Cfg,
transfer: &SsaTaintTransfer,
) -> (Vec<SsaTaintEvent>, Vec<Option<SsaTaintState>>) {
let result = run_ssa_taint_internal(ssa, cfg, transfer);
(result.events, result.block_states)
}
pub fn run_ssa_taint_full_with_exits(
ssa: &SsaBody,
cfg: &Cfg,
transfer: &SsaTaintTransfer,
) -> (
Vec<SsaTaintEvent>,
Vec<Option<SsaTaintState>>,
Vec<Option<SsaTaintState>>,
) {
let result = run_ssa_taint_internal(ssa, cfg, transfer);
(result.events, result.block_states, result.block_exit_states)
}
fn run_ssa_taint_internal(
ssa: &SsaBody,
cfg: &Cfg,
transfer: &SsaTaintTransfer,
) -> SsaTaintRunResult {
let num_blocks = ssa.blocks.len();
let back_edges = detect_back_edges(ssa);
let induction_vars = detect_induction_phis(ssa, &back_edges);
let mut block_states: Vec<Option<SsaTaintState>> = vec![None; num_blocks];
let mut block_exit_states: Vec<Option<SsaTaintState>> = vec![None; num_blocks];
block_states[ssa.entry.0 as usize] = Some(SsaTaintState::initial());
if let Some(ref mut entry_state) = block_states[ssa.entry.0 as usize] {
if let Some(ref mut env) = entry_state.path_env {
if let (Some(cv), Some(tf)) = (transfer.const_values, transfer.type_facts) {
env.seed_from_optimization(cv, tf);
}
}
}
if let Some(ref mut entry_state) = block_states[ssa.entry.0 as usize] {
if let Some(ref mut abs) = entry_state.abstract_state {
if let Some(cv) = transfer.const_values {
use crate::abstract_interp::{
AbstractValue, BitFact, IntervalFact, PathFact, StringFact,
};
use crate::ssa::const_prop::ConstLattice;
for (v, cl) in cv {
match cl {
ConstLattice::Int(n) => {
abs.set(
*v,
AbstractValue {
interval: IntervalFact::exact(*n),
string: StringFact::top(),
bits: BitFact::from_const(*n),
path: PathFact::top(),
},
);
}
ConstLattice::Str(s) => {
abs.set(
*v,
AbstractValue {
interval: IntervalFact::top(),
string: StringFact::exact(s),
bits: BitFact::top(),
path: PathFact::top(),
},
);
}
_ => {}
}
}
}
}
}
let loop_heads: HashSet<usize> = back_edges
.iter()
.map(|(_, target)| target.0 as usize)
.collect();
let mut pred_states: PredStates = HashMap::new();
let mut worklist: VecDeque<usize> = VecDeque::new();
let mut in_worklist: HashSet<usize> = HashSet::new();
worklist.push_back(ssa.entry.0 as usize);
in_worklist.insert(ssa.entry.0 as usize);
for (bid, block) in ssa.blocks.iter().enumerate() {
if bid != ssa.entry.0 as usize && block.preds.is_empty() {
block_states[bid] = Some(SsaTaintState::initial());
worklist.push_back(bid);
in_worklist.insert(bid);
}
}
if !ssa.exception_edges.is_empty() {
tracing::debug!(
count = ssa.exception_edges.len(),
"SSA taint: exception edges for catch-block seeding"
);
}
let mut iterations: usize = 0;
let budget = effective_worklist_cap();
let mut worklist_capped = false;
while let Some(bid) = worklist.pop_front() {
in_worklist.remove(&bid);
iterations += 1;
if iterations >= budget {
tracing::warn!("SSA taint: worklist budget exceeded");
worklist_capped = true;
break;
}
let entry_state = match &block_states[bid] {
Some(s) => s.clone(),
None => continue,
};
let block = &ssa.blocks[bid];
let exit_state = transfer_block(
block,
cfg,
ssa,
transfer,
entry_state,
&induction_vars,
Some(&pred_states),
);
block_exit_states[bid] = Some(exit_state.clone());
let succ_states = compute_succ_states(block, cfg, ssa, transfer, &exit_state);
for &(succ_id, ref succ_state) in &succ_states {
let succ_idx = succ_id.0 as usize;
pred_states.insert((succ_idx, bid), succ_state.clone());
}
for (succ_id, succ_state) in succ_states {
let succ_idx = succ_id.0 as usize;
let new_succ_state = match &block_states[succ_idx] {
Some(existing) => {
let mut joined = existing.join(&succ_state);
if loop_heads.contains(&succ_idx) {
if let (Some(new_abs), Some(old_abs)) =
(&joined.abstract_state, &existing.abstract_state)
{
let widened = old_abs.widen(new_abs);
joined.abstract_state = Some(widened);
}
}
joined
}
None => succ_state,
};
let changed = block_states[succ_idx]
.as_ref()
.is_none_or(|existing| *existing != new_succ_state);
if changed {
block_states[succ_idx] = Some(new_succ_state);
if in_worklist.insert(succ_idx) {
worklist.push_back(succ_idx);
}
}
}
let bid_id = BlockId(bid as u32);
for &(src_blk, catch_blk) in &ssa.exception_edges {
if src_blk != bid_id {
continue;
}
let catch_idx = catch_blk.0 as usize;
let mut exc_state = exit_state.clone();
exc_state.predicates.clear();
exc_state.path_env = None;
let new_catch_state = match &block_states[catch_idx] {
Some(existing) => existing.join(&exc_state),
None => exc_state,
};
let changed = block_states[catch_idx]
.as_ref()
.is_none_or(|existing| *existing != new_catch_state);
if changed {
block_states[catch_idx] = Some(new_catch_state);
if in_worklist.insert(catch_idx) {
worklist.push_back(catch_idx);
}
}
}
}
MAX_WORKLIST_ITERATIONS.fetch_max(iterations, std::sync::atomic::Ordering::Relaxed);
if worklist_capped {
WORKLIST_CAP_HITS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
record_engine_note(crate::engine_notes::EngineNote::WorklistCapped {
iterations: iterations as u32,
});
}
let cap = effective_max_origins();
let mut saturated = 0u32;
for state in block_states.iter().flatten() {
for (_v, taint) in &state.values {
if taint.origins.len() >= cap {
saturated = saturated.saturating_add(1);
}
}
}
if saturated > 0 {
ORIGINS_TRUNCATION_COUNT
.fetch_add(saturated as usize, std::sync::atomic::Ordering::Relaxed);
record_engine_note(crate::engine_notes::EngineNote::OriginsTruncated {
dropped: saturated,
});
}
let mut events: Vec<SsaTaintEvent> = Vec::new();
for bid in 0..num_blocks {
let entry_state = match &block_states[bid] {
Some(s) => s.clone(),
None => continue,
};
let block = &ssa.blocks[bid];
collect_block_events(
block,
cfg,
ssa,
transfer,
entry_state,
&mut events,
&induction_vars,
Some(&pred_states),
);
}
SsaTaintRunResult {
events,
block_states,
block_exit_states,
}
}
pub fn run_ssa_taint(ssa: &SsaBody, cfg: &Cfg, transfer: &SsaTaintTransfer) -> Vec<SsaTaintEvent> {
run_ssa_taint_full(ssa, cfg, transfer).0
}
pub fn extract_ssa_exit_state(
block_states: &[Option<SsaTaintState>],
ssa: &SsaBody,
cfg: &Cfg,
transfer: &SsaTaintTransfer,
owner_body_id: BodyId,
) -> HashMap<BindingKey, VarTaint> {
let empty_induction = HashSet::new();
let mut joined = SsaTaintState::initial();
for (bid, entry_state) in block_states.iter().enumerate() {
if let Some(state) = entry_state {
let exit_state = transfer_block(
&ssa.blocks[bid],
cfg,
ssa,
transfer,
state.clone(),
&empty_induction,
None,
);
joined = joined.join(&exit_state);
}
}
let mut result: HashMap<BindingKey, VarTaint> = HashMap::new();
for (val, taint) in &joined.values {
let var_name = ssa
.value_defs
.get(val.0 as usize)
.and_then(|vd| vd.var_name.as_deref());
if let Some(name) = var_name {
let key = BindingKey::new(name, owner_body_id);
result
.entry(key)
.and_modify(|existing| {
existing.caps |= taint.caps;
for orig in &taint.origins {
push_origin_bounded(&mut existing.origins, *orig);
}
})
.or_insert_with(|| taint.clone());
}
}
for taint in result.values_mut() {
for origin in taint.origins.iter_mut() {
if origin.source_span.is_none() {
if let Some(info) = cfg.node_weight(origin.node) {
origin.source_span = Some(info.classification_span());
}
}
}
}
result
}
pub fn join_seed_maps(
a: &HashMap<BindingKey, VarTaint>,
b: &HashMap<BindingKey, VarTaint>,
) -> HashMap<BindingKey, VarTaint> {
let mut result = a.clone();
for (key, taint) in b {
result
.entry(key.clone())
.and_modify(|existing| {
existing.caps |= taint.caps;
for orig in &taint.origins {
push_origin_bounded(&mut existing.origins, *orig);
}
})
.or_insert_with(|| taint.clone());
}
result
}
pub fn filter_seed_to_toplevel(
seed: &HashMap<BindingKey, VarTaint>,
toplevel: &HashSet<BindingKey>,
) -> HashMap<BindingKey, VarTaint> {
let toplevel_names: HashSet<&str> = toplevel.iter().map(|k| k.name.as_str()).collect();
let mut out: HashMap<BindingKey, VarTaint> = HashMap::new();
for (key, taint) in seed.iter() {
if !toplevel_names.contains(key.name.as_str()) {
continue;
}
let rekeyed = BindingKey::new(key.name.clone(), BodyId(0));
out.entry(rekeyed)
.and_modify(|existing| {
existing.caps |= taint.caps;
for orig in &taint.origins {
push_origin_bounded(&mut existing.origins, *orig);
}
existing.uses_summary |= taint.uses_summary;
})
.or_insert_with(|| taint.clone());
}
out
}
fn detect_back_edges(ssa: &SsaBody) -> HashSet<(BlockId, BlockId)> {
let mut back_edges = HashSet::new();
for block in &ssa.blocks {
for &pred in &block.preds {
if pred.0 >= block.id.0 {
back_edges.insert((pred, block.id));
}
}
}
back_edges
}
fn is_simple_increment(ssa: &SsaBody, inc_val: SsaValue, phi_val: SsaValue) -> bool {
let def = ssa.def_of(inc_val);
let block = ssa.block(def.block);
for inst in &block.body {
if inst.value == inc_val {
if let SsaOp::Assign(ref uses) = inst.op {
if uses.len() == 2 && uses.contains(&phi_val) {
let other = if uses[0] == phi_val { uses[1] } else { uses[0] };
let other_def = ssa.def_of(other);
let other_block = ssa.block(other_def.block);
for other_inst in other_block.phis.iter().chain(other_block.body.iter()) {
if other_inst.value == other && matches!(other_inst.op, SsaOp::Const(_)) {
return true;
}
}
}
}
break;
}
}
false
}
fn detect_induction_phis(
ssa: &SsaBody,
back_edges: &HashSet<(BlockId, BlockId)>,
) -> HashSet<SsaValue> {
let mut induction_vars = HashSet::new();
for block in &ssa.blocks {
for phi in &block.phis {
if let SsaOp::Phi(ref operands) = phi.op {
if operands.len() != 2 {
continue;
}
let mut back_edge_op = None;
let mut init_op = None;
for &(pred_blk, operand_val) in operands {
if back_edges.contains(&(pred_blk, block.id)) {
back_edge_op = Some(operand_val);
} else {
init_op = Some(operand_val);
}
}
if let (Some(back_val), Some(_init_val)) = (back_edge_op, init_op) {
if is_simple_increment(ssa, back_val, phi.value) {
induction_vars.insert(phi.value);
}
}
}
}
}
induction_vars
}
pub(super) fn transfer_block(
block: &SsaBlock,
cfg: &Cfg,
ssa: &SsaBody,
transfer: &SsaTaintTransfer,
mut state: SsaTaintState,
induction_vars: &HashSet<SsaValue>,
pred_states: Option<&PredStates>,
) -> SsaTaintState {
let block_idx = block.id.0 as usize;
for phi in &block.phis {
if let SsaOp::Phi(ref operands) = phi.op {
let is_induction = induction_vars.contains(&phi.value);
let mut combined_caps = Cap::empty();
let mut combined_origins: SmallVec<[TaintOrigin; 2]> = SmallVec::new();
let mut all_tainted_validated = true;
let mut any_tainted = false;
for &(pred_blk, operand_val) in operands {
if is_induction && pred_blk.0 >= block.id.0 {
continue;
}
if let Some(ps) = pred_states {
if let Some(pred_st) = ps.get(&(block_idx, pred_blk.0 as usize)) {
if pred_st.path_env.as_ref().is_some_and(|e| e.is_unsat()) {
continue;
}
}
}
let operand_taint = if let Some(ps) = pred_states {
ps.get(&(block_idx, pred_blk.0 as usize))
.and_then(|pred_st| pred_st.get(operand_val))
} else {
None
};
let operand_taint = operand_taint.or_else(|| state.get(operand_val));
if let Some(taint) = operand_taint {
any_tainted = true;
combined_caps |= taint.caps;
for orig in &taint.origins {
push_origin_bounded(&mut combined_origins, *orig);
}
if let Some(ps) = pred_states {
if let Some(pred_st) = ps.get(&(block_idx, pred_blk.0 as usize)) {
let var_name = ssa
.value_defs
.get(operand_val.0 as usize)
.and_then(|vd| vd.var_name.as_deref());
if let Some(name) = var_name {
if let Some(sym) = transfer.interner.get(name) {
if !pred_st.validated_must.contains(sym) {
all_tainted_validated = false;
}
} else {
all_tainted_validated = false;
}
} else {
all_tainted_validated = false;
}
} else {
all_tainted_validated = false;
}
} else {
all_tainted_validated = false;
}
}
}
if combined_caps.is_empty() {
state.remove(phi.value);
} else {
state.set(
phi.value,
VarTaint {
caps: combined_caps,
origins: combined_origins,
uses_summary: false,
},
);
if any_tainted && all_tainted_validated {
if let Some(name) = ssa
.value_defs
.get(phi.value.0 as usize)
.and_then(|vd| vd.var_name.as_deref())
{
if let Some(sym) = transfer.interner.get(name) {
state.validated_may.insert(sym);
state.validated_must.insert(sym);
}
}
}
}
}
}
if state.abstract_state.is_some() {
for phi in &block.phis {
if let SsaOp::Phi(ref operands) = phi.op {
use crate::abstract_interp::AbstractValue;
let is_induction = induction_vars.contains(&phi.value);
let mut joined = AbstractValue::bottom();
let mut any_operand = false;
for &(pred_blk, operand_val) in operands {
if is_induction && pred_blk.0 >= block.id.0 {
continue;
}
if let Some(ps) = pred_states {
if let Some(pred_st) = ps.get(&(block_idx, pred_blk.0 as usize)) {
if pred_st.path_env.as_ref().is_some_and(|e| e.is_unsat()) {
continue;
}
}
}
let pred_abs = pred_states
.and_then(|ps| ps.get(&(block_idx, pred_blk.0 as usize)))
.and_then(|s| s.abstract_state.as_ref())
.map(|a| a.get(operand_val))
.unwrap_or_else(AbstractValue::top);
joined = joined.join(&pred_abs);
any_operand = true;
}
if any_operand {
if let Some(ref mut abs) = state.abstract_state {
abs.set(phi.value, joined);
}
}
}
}
}
for inst in &block.body {
transfer_inst(inst, cfg, ssa, transfer, &mut state);
}
state
}
fn compute_succ_states(
block: &SsaBlock,
cfg: &Cfg,
ssa: &SsaBody,
transfer: &SsaTaintTransfer,
exit_state: &SsaTaintState,
) -> SmallVec<[(BlockId, SsaTaintState); 2]> {
match &block.terminator {
Terminator::Branch {
cond,
true_blk,
false_blk,
condition,
} => {
let Some(cond_info) = cfg.node_weight(*cond) else {
return smallvec::smallvec![
(*true_blk, exit_state.clone()),
(*false_blk, exit_state.clone()),
];
};
if cond_info.kind == crate::cfg::StmtKind::If && !cond_info.condition_vars.is_empty() {
let cond_text = cond_info.condition_text.as_deref().unwrap_or("");
let (kind, target_var) = classify_condition_with_target(cond_text);
let effective_vars: Vec<String> = if let Some(ref target) = target_var {
if cond_info.condition_vars.iter().any(|v| v == target) {
vec![target.clone()]
} else {
cond_info.condition_vars.clone()
}
} else {
cond_info.condition_vars.clone()
};
let mut true_state = exit_state.clone();
let mut false_state = exit_state.clone();
let cond_lower = cond_text.to_ascii_lowercase();
let has_semantic_negation = (kind == PredicateKind::AllowlistCheck
&& cond_lower.contains(" not in "))
|| (kind == PredicateKind::TypeCheck
&& (cond_lower.contains("!==") || cond_lower.contains("!=")));
let effective_negated = if has_semantic_negation {
!cond_info.condition_negated
} else {
cond_info.condition_negated
};
let true_polarity = !effective_negated;
let false_polarity = effective_negated;
apply_branch_predicates(
&mut true_state,
&effective_vars,
kind,
true_polarity,
transfer.interner,
);
apply_branch_predicates(
&mut false_state,
&effective_vars,
kind,
false_polarity,
transfer.interner,
);
apply_path_fact_branch_narrowing_with_interner(
&mut true_state,
&mut false_state,
cond_text,
&effective_vars,
ssa,
Some(transfer.interner),
);
if matches!(kind, PredicateKind::ErrorCheck) {
apply_validation_err_check_narrowing(
&mut true_state,
&mut false_state,
cond_text,
&cond_info.condition_vars,
ssa,
block.id,
transfer.interner,
);
}
if true_state.path_env.is_some() || false_state.path_env.is_some() {
let cond_expr = if let Some(pre_lowered) = condition {
(**pre_lowered).clone()
} else {
constraint::lower_condition(cond_info, ssa, block.id, transfer.const_values)
};
if !matches!(cond_expr, constraint::ConditionExpr::Unknown) {
if let Some(ref mut env) = true_state.path_env {
*env = constraint::refine_env(env, &cond_expr, true);
if env.is_unsat() {
tracing::debug!(
block = ?block.id,
cond = cond_text,
"constraint: pruned true branch (unsat)"
);
}
}
if let Some(ref mut env) = false_state.path_env {
*env = constraint::refine_env(env, &cond_expr, false);
if env.is_unsat() {
tracing::debug!(
block = ?block.id,
cond = cond_text,
"constraint: pruned false branch (unsat)"
);
}
}
}
}
let true_pred_contra = true_state
.predicates
.iter()
.any(|(_, s)| s.has_contradiction());
let false_pred_contra = false_state
.predicates
.iter()
.any(|(_, s)| s.has_contradiction());
if true_pred_contra {
true_state = SsaTaintState::bot();
} else if true_state.path_env.as_ref().is_some_and(|e| e.is_unsat()) {
true_state.path_env = None;
}
if false_pred_contra {
false_state = SsaTaintState::bot();
} else if false_state.path_env.as_ref().is_some_and(|e| e.is_unsat()) {
false_state.path_env = None;
}
smallvec::smallvec![(*true_blk, true_state), (*false_blk, false_state),]
} else {
smallvec::smallvec![
(*true_blk, exit_state.clone()),
(*false_blk, exit_state.clone()),
]
}
}
Terminator::Goto(_) => {
block
.succs
.iter()
.map(|s| (*s, exit_state.clone()))
.collect()
}
Terminator::Switch { .. } => {
block
.succs
.iter()
.map(|s| (*s, exit_state.clone()))
.collect()
}
Terminator::Return(_) | Terminator::Unreachable => {
block
.succs
.iter()
.map(|s| (*s, exit_state.clone()))
.collect()
}
}
}
fn apply_branch_predicates(
state: &mut SsaTaintState,
condition_vars: &[String],
kind: PredicateKind,
polarity: bool,
interner: &SymbolInterner,
) {
if matches!(
kind,
PredicateKind::ValidationCall | PredicateKind::AllowlistCheck | PredicateKind::TypeCheck
) && polarity
{
for var in condition_vars {
if let Some(sym) = interner.get(var) {
state.validated_may.insert(sym);
state.validated_must.insert(sym);
}
}
}
if kind == PredicateKind::ShellMetaValidated && !polarity {
for var in condition_vars {
if let Some(sym) = interner.get(var) {
state.validated_may.insert(sym);
state.validated_must.insert(sym);
}
}
}
if let Some(bit_idx) = predicate_kind_bit(kind) {
for var in condition_vars {
if let Some(sym) = interner.get(var) {
let mut summary = state
.predicates
.binary_search_by_key(&sym, |(id, _)| *id)
.ok()
.map(|idx| state.predicates[idx].1)
.unwrap_or_else(PredicateSummary::empty);
if polarity {
summary.known_true |= 1 << bit_idx;
} else {
summary.known_false |= 1 << bit_idx;
}
match state.predicates.binary_search_by_key(&sym, |(id, _)| *id) {
Ok(idx) => state.predicates[idx].1 = summary,
Err(idx) => state.predicates.insert(idx, (sym, summary)),
}
}
}
}
}
fn apply_validation_err_check_narrowing(
true_state: &mut SsaTaintState,
false_state: &mut SsaTaintState,
cond_text: &str,
condition_vars: &[String],
ssa: &SsaBody,
block: BlockId,
interner: &SymbolInterner,
) {
if condition_vars.is_empty() {
return;
}
let lower = cond_text.to_ascii_lowercase();
let success_branch_is_true = lower.contains("== nil")
|| lower.contains("== none")
|| lower.contains("is none")
|| lower.contains("is_ok")
|| lower.contains("=== null")
|| lower.contains("== null");
if condition_vars.len() != 1 {
return;
}
let err_name = condition_vars[0].as_str();
let err_val = match resolve_var_to_ssa_value(err_name, ssa, block) {
Some(v) => v,
None => return,
};
let def_inst = ssa
.blocks
.iter()
.flat_map(|b| b.body.iter())
.find(|i| i.value == err_val);
let Some(def_inst) = def_inst else { return };
let SsaOp::Call {
ref callee,
ref args,
..
} = def_inst.op
else {
return;
};
if !crate::ssa::type_facts::is_int_producing_callee(callee) {
return;
}
let mut arg_names: SmallVec<[String; 2]> = SmallVec::new();
for arg_group in args {
for &v in arg_group {
if let Some(name) = ssa
.value_defs
.get(v.0 as usize)
.and_then(|vd| vd.var_name.as_deref())
{
if !arg_names.iter().any(|s: &String| s == name) {
arg_names.push(name.to_string());
}
}
}
}
if arg_names.is_empty() {
return;
}
let success_state = if success_branch_is_true {
true_state
} else {
false_state
};
for name in &arg_names {
if let Some(sym) = interner.get(name) {
success_state.validated_may.insert(sym);
success_state.validated_must.insert(sym);
}
}
}
fn resolve_var_to_ssa_value(var_name: &str, ssa: &SsaBody, block: BlockId) -> Option<SsaValue> {
let mut best_in_block: Option<SsaValue> = None;
let mut best_outside: Option<SsaValue> = None;
for (idx, vd) in ssa.value_defs.iter().enumerate() {
if vd.var_name.as_deref() != Some(var_name) {
continue;
}
let v = SsaValue(idx as u32);
if vd.block == block {
best_in_block = Some(match best_in_block {
Some(existing) if existing.0 > v.0 => existing,
_ => v,
});
} else {
best_outside = Some(match best_outside {
Some(existing) if existing.0 > v.0 => existing,
_ => v,
});
}
}
best_in_block.or(best_outside)
}
#[cfg(test)]
fn apply_path_fact_branch_narrowing(
true_state: &mut SsaTaintState,
false_state: &mut SsaTaintState,
cond_text: &str,
effective_vars: &[String],
ssa: &SsaBody,
) {
apply_path_fact_branch_narrowing_with_interner(
true_state,
false_state,
cond_text,
effective_vars,
ssa,
None,
);
}
fn apply_path_fact_branch_narrowing_with_interner(
true_state: &mut SsaTaintState,
false_state: &mut SsaTaintState,
cond_text: &str,
effective_vars: &[String],
ssa: &SsaBody,
interner: Option<&SymbolInterner>,
) {
use crate::abstract_interp::PathFact;
use crate::abstract_interp::path_domain::{
PathAssertion, PathRejection, classify_path_assertion, classify_path_rejection_axes,
};
let rejection_axes = classify_path_rejection_axes(cond_text);
let assertion = classify_path_assertion(cond_text);
if rejection_axes.is_empty() && matches!(assertion, PathAssertion::None) {
return;
}
if !rejection_axes.is_empty()
&& let Some(intern) = interner
{
for var in effective_vars {
if let Some(sym) = intern.get(var) {
false_state.validated_may.insert(sym);
false_state.validated_must.insert(sym);
}
}
}
let mut targets: smallvec::SmallVec<[SsaValue; 2]> = smallvec::SmallVec::new();
for var_name in effective_vars {
let mut latest: Option<SsaValue> = None;
for (idx, vd) in ssa.value_defs.iter().enumerate() {
if vd.var_name.as_deref() == Some(var_name.as_str()) {
latest = Some(SsaValue(idx as u32));
}
}
if let Some(v) = latest {
targets.push(v);
}
}
if targets.is_empty() {
return;
}
let narrow_false = |fact: &mut PathFact| {
for axis in rejection_axes.iter() {
match axis {
PathRejection::DotDot => {
fact.dotdot = crate::abstract_interp::Tri::No;
}
PathRejection::AbsoluteSlash | PathRejection::IsAbsolute => {
fact.absolute = crate::abstract_interp::Tri::No;
}
PathRejection::None => {}
}
}
};
let narrow_true = |fact: &mut PathFact| {
if let PathAssertion::PrefixLock(ref root) = assertion {
let updated = fact.clone().with_prefix_lock(root);
*fact = updated;
}
};
for v in &targets {
if let Some(ref mut abs) = false_state.abstract_state {
let mut av = abs.get(*v);
narrow_false(&mut av.path);
if !av.is_top() {
abs.set(*v, av);
}
}
if let Some(ref mut abs) = true_state.abstract_state {
let mut av = abs.get(*v);
narrow_true(&mut av.path);
if !av.is_top() {
abs.set(*v, av);
}
}
}
}
fn build_arg_taint_sig(
args: &[SmallVec<[SsaValue; 2]>],
receiver: &Option<SsaValue>,
state: &SsaTaintState,
) -> ArgTaintSig {
let mut sig = SmallVec::new();
if let Some(rv) = receiver {
if let Some(taint) = state.get(*rv) {
sig.push((usize::MAX, taint.caps.bits()));
}
}
for (i, arg_vals) in args.iter().enumerate() {
let mut caps = Cap::empty();
for v in arg_vals {
if let Some(taint) = state.get(*v) {
caps |= taint.caps;
}
}
if !caps.is_empty() {
sig.push((i, caps.bits()));
}
}
sig.sort_by_key(|(idx, _)| *idx);
ArgTaintSig(sig)
}
fn inline_analyse_callee(
callee: &str,
args: &[SmallVec<[SsaValue; 2]>],
receiver: &Option<SsaValue>,
state: &SsaTaintState,
transfer: &SsaTaintTransfer,
cfg: &Cfg,
caller_ssa: &SsaBody,
call_inst: &SsaInst,
) -> Option<InlineResult> {
if transfer.context_depth >= 1 {
return None;
}
let cache_ref = transfer.inline_cache?;
let normalized = callee_leaf_name(callee);
let container_raw = callee_container_hint(callee);
let container_hint = if container_raw.is_empty() {
None
} else {
Some(container_raw)
};
let intra_key = transfer.callee_bodies.and_then(|_| {
resolve_local_func_key(
transfer.local_summaries,
transfer.lang,
transfer.namespace,
normalized,
container_hint,
)
});
let intra_body = intra_key
.as_ref()
.and_then(|k| transfer.callee_bodies.and_then(|cb| cb.get(k)));
let (callee_key, callee_body) = if let (Some(k), Some(b)) = (intra_key, intra_body) {
(k, b)
} else if let Some(gs) = transfer.global_summaries {
let (namespace_qualifier, receiver_var) = split_qualifier(callee);
let caller_func = caller_ssa
.blocks
.iter()
.flat_map(|b| b.phis.iter().chain(b.body.iter()))
.filter_map(|inst| {
cfg.node_weight(inst.cfg_node)
.and_then(|info| info.ast.enclosing_func.as_deref())
})
.next()
.unwrap_or("");
let caller_container_opt = caller_container_for(transfer, caller_func);
let caller_container: Option<&str> = caller_container_opt.as_deref();
let receiver_type = receiver_type_prefix(transfer, *receiver);
let arity_hint = Some(args.len());
let query = CalleeQuery {
name: normalized,
caller_lang: transfer.lang,
caller_namespace: transfer.namespace,
caller_container,
receiver_type,
namespace_qualifier,
receiver_var,
arity: arity_hint,
};
match gs.resolve_callee(&query) {
CalleeResolution::Resolved(key) => {
let xfile_bodies = transfer.cross_file_bodies?;
let body = xfile_bodies.get(&key)?;
if body.body_graph.is_none() {
tracing::debug!(
callee = %normalized,
"cross-file inline miss: body has no body_graph and no node_meta"
);
return None;
}
tracing::debug!(
callee = %normalized,
namespace = %key.namespace,
"cross-file inline hit: using GlobalSummaries.bodies_by_key"
);
(key, body)
}
_ => return None,
}
} else {
return None;
};
if callee_body.ssa.blocks.len() > MAX_INLINE_BLOCKS {
tracing::debug!(
callee = %callee_key.name,
namespace = %callee_key.namespace,
blocks = callee_body.ssa.blocks.len(),
max = MAX_INLINE_BLOCKS,
"inline miss: body too large (budget-exceeded)"
);
return None;
}
let sig = build_arg_taint_sig(args, receiver, state);
{
let cache = cache_ref.borrow();
if let Some(cached) = cache.get(&(callee_key.clone(), sig.clone())) {
record_engine_note(crate::engine_notes::EngineNote::InlineCacheReused);
return Some(apply_cached_shape(
cached,
args,
receiver,
state,
call_inst.cfg_node,
));
}
}
let populate_span = |mut o: TaintOrigin| -> TaintOrigin {
if o.source_span.is_none() {
if let Some(info) = cfg.node_weight(o.node) {
o.source_span = Some(info.classification_span());
}
}
o
};
let combine_taint = |arg_vals: &SmallVec<[SsaValue; 2]>| -> Option<VarTaint> {
let mut combined_caps = Cap::empty();
let mut combined_origins: SmallVec<[TaintOrigin; 2]> = SmallVec::new();
for v in arg_vals {
if let Some(taint) = state.get(*v) {
combined_caps |= taint.caps;
for orig in &taint.origins {
push_origin_bounded(&mut combined_origins, populate_span(*orig));
}
}
}
if combined_caps.is_empty() {
None
} else {
Some(VarTaint {
caps: combined_caps,
origins: combined_origins,
uses_summary: false,
})
}
};
let param_seed: Vec<Option<VarTaint>> = args.iter().map(combine_taint).collect();
let receiver_seed: Option<VarTaint> = receiver.and_then(|rv| {
state.get(rv).map(|taint| VarTaint {
caps: taint.caps,
origins: taint.origins.iter().map(|o| populate_span(*o)).collect(),
uses_summary: false,
})
});
let mut callback_bindings: HashMap<String, FuncKey> = HashMap::new();
for block in &callee_body.ssa.blocks {
for inst in block.phis.iter().chain(block.body.iter()) {
if let SsaOp::Param { index } = &inst.op {
if let Some(param_name) = inst.var_name.as_ref() {
if *index < args.len() {
for v in &args[*index] {
if let Some(arg_var_name) = caller_ssa
.value_defs
.get(v.0 as usize)
.and_then(|vd| vd.var_name.as_deref())
{
let norm = callee_leaf_name(arg_var_name);
let hint_raw = callee_container_hint(arg_var_name);
let hint = if hint_raw.is_empty() {
None
} else {
Some(hint_raw)
};
if let Some(target_key) = resolve_local_func_key(
transfer.local_summaries,
transfer.lang,
transfer.namespace,
norm,
hint,
) {
if transfer
.callee_bodies
.is_some_and(|cb| cb.contains_key(&target_key))
{
callback_bindings.insert(param_name.clone(), target_key);
}
}
}
}
}
}
}
}
}
let cb_ref = if callback_bindings.is_empty() {
None
} else {
Some(&callback_bindings)
};
let param_seed_slice: Option<&[Option<VarTaint>]> = if param_seed.is_empty() {
None
} else {
Some(param_seed.as_slice())
};
let child_transfer = SsaTaintTransfer {
lang: transfer.lang,
namespace: transfer.namespace,
interner: transfer.interner,
local_summaries: transfer.local_summaries,
global_summaries: transfer.global_summaries,
interop_edges: transfer.interop_edges,
owner_body_id: BodyId(0),
parent_body_id: None,
global_seed: None,
param_seed: param_seed_slice,
receiver_seed: receiver_seed.as_ref(),
const_values: Some(&callee_body.opt.const_values),
type_facts: Some(&callee_body.opt.type_facts),
ssa_summaries: transfer.ssa_summaries,
extra_labels: transfer.extra_labels,
base_aliases: Some(&callee_body.opt.alias_result),
callee_bodies: None, inline_cache: None,
context_depth: transfer.context_depth + 1,
callback_bindings: cb_ref,
points_to: Some(&callee_body.opt.points_to),
dynamic_pts: None, import_bindings: transfer.import_bindings,
promisify_aliases: transfer.promisify_aliases,
module_aliases: None, static_map: None, auto_seed_handler_params: transfer.auto_seed_handler_params,
cross_file_bodies: transfer.cross_file_bodies,
pointer_facts: None,
};
let callee_cfg = callee_body.body_graph.as_ref().unwrap_or(cfg);
let (_, callee_block_states, callee_block_exit_states) =
run_ssa_taint_full_with_exits(&callee_body.ssa, callee_cfg, &child_transfer);
let empty_induction = HashSet::new();
let shape = extract_inline_return_taint(
&callee_body.ssa,
callee_cfg,
&child_transfer,
&callee_block_states,
&callee_block_exit_states,
&empty_induction,
);
{
let mut cache = cache_ref.borrow_mut();
cache.insert((callee_key, sig), shape.clone());
}
Some(apply_cached_shape(
&shape,
args,
receiver,
state,
call_inst.cfg_node,
))
}
#[derive(Copy, Clone, Debug, Default)]
struct CalleeParamNodeBits {
params: u64,
receiver: bool,
}
fn extract_inline_return_taint(
ssa: &SsaBody,
cfg: &Cfg,
transfer: &SsaTaintTransfer,
block_states: &[Option<SsaTaintState>],
block_exit_states: &[Option<SsaTaintState>],
induction_vars: &HashSet<SsaValue>,
) -> CachedInlineShape {
let param_values: HashSet<SsaValue> = ssa
.blocks
.iter()
.flat_map(|b| b.phis.iter().chain(b.body.iter()))
.filter(|i| matches!(i.op, SsaOp::Param { .. }))
.map(|i| i.value)
.collect();
let mut param_node_map: HashMap<NodeIndex, CalleeParamNodeBits> = HashMap::new();
for block in &ssa.blocks {
for inst in block.phis.iter().chain(block.body.iter()) {
match &inst.op {
SsaOp::Param { index } => {
let entry = param_node_map.entry(inst.cfg_node).or_default();
if *index < 64 {
entry.params |= 1u64 << *index;
}
}
SsaOp::SelfParam => {
let entry = param_node_map.entry(inst.cfg_node).or_default();
entry.receiver = true;
}
_ => {}
}
}
}
let placeholder_node = NodeIndex::end();
let prep_internal = |o: &TaintOrigin| -> TaintOrigin {
let mut out = *o;
if out.source_span.is_none() {
if let Some(info) = cfg.node_weight(o.node) {
out.source_span = Some(info.classification_span());
}
}
out.node = placeholder_node;
out
};
let push_internal = |target: &mut SmallVec<[TaintOrigin; 2]>, orig: &TaintOrigin| {
let new_orig = prep_internal(orig);
if target
.iter()
.any(|o| o.source_span == new_orig.source_span && o.source_kind == new_orig.source_kind)
{
return;
}
if target.len() < effective_max_origins() {
target.push(new_orig);
} else {
ORIGINS_TRUNCATION_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
record_engine_note(crate::engine_notes::EngineNote::OriginsTruncated { dropped: 1 });
}
};
let mut derived_caps = Cap::empty();
let mut derived_internal: SmallVec<[TaintOrigin; 2]> = SmallVec::new();
let mut derived_params: u64 = 0;
let mut derived_receiver: bool = false;
let mut param_caps = Cap::empty();
let mut param_internal: SmallVec<[TaintOrigin; 2]> = SmallVec::new();
let mut param_params: u64 = 0;
let mut param_receiver: bool = false;
let mut return_path_fact_acc: Option<crate::abstract_interp::PathFact> = None;
let mut per_return_path_entries: SmallVec<
[crate::summary::ssa_summary::PathFactReturnEntry; 2],
> = SmallVec::new();
let classify_and_push = |orig: &TaintOrigin,
internal: &mut SmallVec<[TaintOrigin; 2]>,
provenance: &mut u64,
receiver_prov: &mut bool| {
match param_node_map.get(&orig.node) {
Some(bits) => {
*provenance |= bits.params;
if bits.receiver {
*receiver_prov = true;
}
}
None => {
push_internal(internal, orig);
}
}
};
for (bid, block) in ssa.blocks.iter().enumerate() {
let ret_val = match &block.terminator {
Terminator::Return(rv) => rv.as_ref().copied(),
_ => continue,
};
if let Some(entry_state) = &block_states[bid] {
let exit = transfer_block(
block,
cfg,
ssa,
transfer,
entry_state.clone(),
induction_vars,
None,
);
if let Some(rv) = ret_val {
if let Some(taint) = exit.get(rv) {
if param_values.contains(&rv) {
param_caps |= taint.caps;
for orig in &taint.origins {
classify_and_push(
orig,
&mut param_internal,
&mut param_params,
&mut param_receiver,
);
}
} else {
derived_caps |= taint.caps;
for orig in &taint.origins {
classify_and_push(
orig,
&mut derived_internal,
&mut derived_params,
&mut derived_receiver,
);
}
}
}
let single_pred = block.preds.len() <= 1;
let mut block_outer_fact: Option<crate::abstract_interp::PathFact> = None;
let mut block_variant_inner: Option<crate::abstract_interp::PathFact> = None;
if single_pred {
if let Some(ref abs) = exit.abstract_state {
let fact = abs.get(rv).path;
block_outer_fact = Some(fact);
block_variant_inner = detect_variant_inner_fact(rv, ssa, &exit);
}
} else {
for pred in &block.preds {
let pred_idx = pred.0 as usize;
if let Some(pred_exit) =
block_exit_states.get(pred_idx).and_then(|o| o.as_ref())
{
let per_pred_exit = transfer_block(
block,
cfg,
ssa,
transfer,
pred_exit.clone(),
induction_vars,
None,
);
if let Some(ref abs) = per_pred_exit.abstract_state {
let fact = abs.get(rv).path;
block_outer_fact = Some(match block_outer_fact {
None => fact,
Some(prev) => prev.join(&fact),
});
let inner_this = detect_variant_inner_fact(rv, ssa, &per_pred_exit);
block_variant_inner = match (block_variant_inner, inner_this) {
(Some(a), Some(b)) => Some(a.join(&b)),
(Some(a), None) => Some(a),
(None, Some(b)) => Some(b),
(None, None) => None,
};
}
}
}
}
let rv_carries_no_data = is_non_data_return(rv, ssa);
let block_contribution = if rv_carries_no_data {
None
} else {
block_variant_inner
.clone()
.or_else(|| block_outer_fact.clone())
};
if let Some(fact) = block_contribution {
return_path_fact_acc = Some(match return_path_fact_acc.clone() {
None => fact,
Some(prev) => prev.join(&fact),
});
}
if let Some(outer) = block_outer_fact {
let (predicate_hash, known_true, known_false) =
summary_extract::summarise_return_predicates(&exit);
let entry = crate::summary::ssa_summary::PathFactReturnEntry {
predicate_hash,
known_true,
known_false,
path_fact: outer,
variant_inner_fact: block_variant_inner,
};
crate::summary::ssa_summary::merge_path_fact_return_paths(
&mut per_return_path_entries,
&[entry],
);
}
} else {
for (val, taint) in &exit.values {
if param_values.contains(val) {
param_caps |= taint.caps;
for orig in &taint.origins {
classify_and_push(
orig,
&mut param_internal,
&mut param_params,
&mut param_receiver,
);
}
} else {
derived_caps |= taint.caps;
for orig in &taint.origins {
classify_and_push(
orig,
&mut derived_internal,
&mut derived_params,
&mut derived_receiver,
);
}
}
}
}
}
}
let (final_caps, final_internal, final_params, final_receiver) = if !derived_caps.is_empty() {
(
derived_caps,
derived_internal,
derived_params,
derived_receiver,
)
} else {
(param_caps, param_internal, param_params, param_receiver)
};
let return_path_fact =
return_path_fact_acc.unwrap_or_else(crate::abstract_interp::PathFact::top);
let return_path_facts = if per_return_path_entries.len() >= 2
&& per_return_path_entries
.iter()
.any(|e| !e.path_fact.is_top() || e.variant_inner_fact.is_some())
{
per_return_path_entries
} else {
SmallVec::new()
};
if final_caps.is_empty()
&& final_params == 0
&& !final_receiver
&& final_internal.is_empty()
&& return_path_fact.is_top()
&& return_path_facts.is_empty()
{
return CachedInlineShape(None);
}
CachedInlineShape(Some(ReturnShape {
caps: final_caps,
internal_origins: final_internal,
param_provenance: final_params,
receiver_provenance: final_receiver,
uses_summary: true, return_path_fact,
return_path_facts,
}))
}
fn is_non_data_return(rv: SsaValue, ssa: &SsaBody) -> bool {
for block in &ssa.blocks {
for inst in block.phis.iter().chain(block.body.iter()) {
if inst.value != rv {
continue;
}
match &inst.op {
SsaOp::Const(Some(text)) => {
let trimmed = text.trim();
return matches!(
trimmed,
"None"
| "NONE"
| "null"
| "NULL"
| "nil"
| "undefined"
| "Nothing"
| "()"
| ""
);
}
SsaOp::Call {
callee,
args,
receiver,
..
} => {
if receiver.is_none()
&& args.is_empty()
&& crate::abstract_interp::path_domain::is_structural_variant_ctor(callee)
{
return true;
}
return false;
}
_ => return false,
}
}
}
false
}
pub(super) fn detect_variant_inner_fact(
rv: SsaValue,
ssa: &SsaBody,
exit: &state::SsaTaintState,
) -> Option<crate::abstract_interp::PathFact> {
for block in &ssa.blocks {
for inst in block.phis.iter().chain(block.body.iter()) {
if inst.value != rv {
continue;
}
let SsaOp::Call {
callee,
args,
receiver,
..
} = &inst.op
else {
return None;
};
if receiver.is_some() {
return None;
}
if !crate::abstract_interp::path_domain::is_structural_variant_ctor(callee) {
return None;
}
let group = args.first()?;
if group.is_empty() {
return None;
}
let abs = exit.abstract_state.as_ref()?;
let mut joined: Option<crate::abstract_interp::PathFact> = None;
for &v in group {
let fact = abs.get(v).path;
if fact.is_top() {
continue;
}
joined = Some(match joined {
None => fact,
Some(prev) => prev.join(&fact),
});
}
return joined;
}
}
None
}
fn apply_cached_shape(
shape: &CachedInlineShape,
args: &[SmallVec<[SsaValue; 2]>],
receiver: &Option<SsaValue>,
state: &SsaTaintState,
call_site_node: NodeIndex,
) -> InlineResult {
let Some(ret) = shape.0.as_ref() else {
return InlineResult {
return_taint: None,
return_path_fact: crate::abstract_interp::PathFact::top(),
return_path_facts: SmallVec::new(),
};
};
let cap = effective_max_origins();
let mut origins: SmallVec<[TaintOrigin; 2]> = SmallVec::new();
let mut dropped: u32 = 0;
let push =
|origins: &mut SmallVec<[TaintOrigin; 2]>, dropped: &mut u32, new_orig: TaintOrigin| {
if origins.iter().any(|o| {
o.node == new_orig.node
&& o.source_span == new_orig.source_span
&& o.source_kind == new_orig.source_kind
}) {
return;
}
if origins.len() < cap {
origins.push(new_orig);
} else {
*dropped += 1;
}
};
for orig in &ret.internal_origins {
let mut o = *orig;
o.node = call_site_node;
push(&mut origins, &mut dropped, o);
}
let mut bits = ret.param_provenance;
while bits != 0 {
let idx = bits.trailing_zeros() as usize;
bits &= bits - 1;
if let Some(arg_vals) = args.get(idx) {
for v in arg_vals {
if let Some(taint) = state.get(*v) {
for orig in &taint.origins {
push(&mut origins, &mut dropped, *orig);
}
}
}
}
}
if ret.receiver_provenance {
if let Some(rv) = receiver {
if let Some(taint) = state.get(*rv) {
for orig in &taint.origins {
push(&mut origins, &mut dropped, *orig);
}
}
}
}
if dropped > 0 {
ORIGINS_TRUNCATION_COUNT.fetch_add(dropped as usize, std::sync::atomic::Ordering::Relaxed);
record_engine_note(crate::engine_notes::EngineNote::OriginsTruncated { dropped });
}
let return_taint = if ret.caps.is_empty() && origins.is_empty() {
None
} else {
Some(VarTaint {
caps: ret.caps,
origins,
uses_summary: ret.uses_summary,
})
};
InlineResult {
return_taint,
return_path_fact: ret.return_path_fact.clone(),
return_path_facts: ret.return_path_facts.clone(),
}
}
fn apply_field_points_to_writes(
summary: &crate::summary::points_to::FieldPointsToSummary,
args: &[SmallVec<[SsaValue; 2]>],
receiver: &Option<SsaValue>,
state: &mut SsaTaintState,
ssa: &SsaBody,
pf: &crate::pointer::PointsToFacts,
interner: &crate::state::symbol::SymbolInterner,
) {
if summary.is_empty() || summary.overflow {
return;
}
for (param_idx, field_names) in &summary.param_field_writes {
let caller_vals: SmallVec<[SsaValue; 2]> = if *param_idx == u32::MAX {
match receiver {
Some(rv) => smallvec::smallvec![*rv],
None => continue,
}
} else {
let idx = *param_idx as usize;
match args.get(idx) {
Some(group) if !group.is_empty() => group.clone(),
_ => continue,
}
};
let mut combined_caps = crate::labels::Cap::empty();
let mut combined_origins: SmallVec<[TaintOrigin; 2]> = SmallVec::new();
let mut combined_summary = false;
let mut combined_must = true;
let mut combined_may = false;
for &v in &caller_vals {
if let Some(t) = state.get(v) {
combined_caps |= t.caps;
combined_summary |= t.uses_summary;
for o in &t.origins {
push_origin_bounded(&mut combined_origins, *o);
}
}
let (am, av) = ssa_value_validated_bits(v, ssa, interner, state);
combined_must &= am;
combined_may |= av;
}
if combined_caps.is_empty() {
continue;
}
let cell_taint = VarTaint {
caps: combined_caps,
origins: combined_origins,
uses_summary: combined_summary,
};
for name in field_names {
let fid = if name == "<elem>" {
crate::ssa::ir::FieldId::ELEM
} else {
match ssa.field_interner.lookup(name) {
Some(id) => id,
None => continue,
}
};
for &v in &caller_vals {
let pt = pf.pt(v);
if pt.is_empty() || pt.is_top() {
continue;
}
for loc in pt.iter() {
let key = crate::taint::ssa_transfer::state::FieldTaintKey { loc, field: fid };
state.add_field(key, cell_taint.clone(), combined_must, combined_may);
}
}
}
}
}
fn apply_container_elem_read_w4(
inst: &SsaInst,
ssa: &SsaBody,
transfer: &SsaTaintTransfer,
state: &mut SsaTaintState,
) {
let SsaOp::Call {
callee, receiver, ..
} = &inst.op
else {
return;
};
let (Some(pf), Some(rcv)) = (transfer.pointer_facts, *receiver) else {
return;
};
if !crate::pointer::is_container_read_callee_pub(callee) {
return;
}
let pt = pf.pt(rcv);
if pt.is_empty() || pt.is_top() {
return;
}
let mut elem_caps = Cap::empty();
let mut elem_origins: SmallVec<[TaintOrigin; 2]> = SmallVec::new();
let mut elem_summary = false;
let mut cell_must_all: Option<bool> = None;
let mut cell_may_any = false;
for loc in pt.iter() {
let key = crate::taint::ssa_transfer::state::FieldTaintKey {
loc,
field: crate::ssa::ir::FieldId::ELEM,
};
if let Some(cell) = state.get_field(key) {
elem_caps |= cell.taint.caps;
elem_summary |= cell.taint.uses_summary;
for o in &cell.taint.origins {
push_origin_bounded(&mut elem_origins, *o);
}
cell_must_all = Some(match cell_must_all {
Some(prev) => prev && cell.validated_must,
None => cell.validated_must,
});
cell_may_any |= cell.validated_may;
}
}
if cell_must_all.is_none() {
return;
}
if !elem_caps.is_empty() {
let cur = state.get(inst.value).cloned();
let merged = match cur {
Some(mut acc) => {
acc.caps |= elem_caps;
acc.uses_summary |= elem_summary;
for o in &elem_origins {
push_origin_bounded(&mut acc.origins, *o);
}
acc
}
None => VarTaint {
caps: elem_caps,
origins: elem_origins,
uses_summary: elem_summary,
},
};
state.set(inst.value, merged);
}
if let Some(name) = ssa
.value_defs
.get(inst.value.0 as usize)
.and_then(|vd| vd.var_name.as_deref())
{
if let Some(sym) = transfer.interner.get(name) {
if cell_must_all == Some(true) {
state.validated_must.insert(sym);
}
if cell_may_any {
state.validated_may.insert(sym);
}
}
}
}
fn ssa_value_validated_bits(
v: SsaValue,
ssa: &SsaBody,
interner: &crate::state::symbol::SymbolInterner,
state: &SsaTaintState,
) -> (bool, bool) {
let name = match ssa
.value_defs
.get(v.0 as usize)
.and_then(|vd| vd.var_name.as_deref())
{
Some(n) => n,
None => return (false, false),
};
match interner.get(name) {
Some(sym) => (
state.validated_must.contains(sym),
state.validated_may.contains(sym),
),
None => (false, false),
}
}
pub(super) fn transfer_inst(
inst: &SsaInst,
cfg: &Cfg,
ssa: &SsaBody,
transfer: &SsaTaintTransfer,
state: &mut SsaTaintState,
) {
let info = &cfg[inst.cfg_node];
let mut callee_return_abstract: Option<crate::abstract_interp::AbstractValue> = None;
match &inst.op {
SsaOp::Source => {
let mut source_caps = Cap::empty();
for lbl in &info.taint.labels {
if let DataLabel::Source(bits) = lbl {
source_caps |= *bits;
}
}
if !source_caps.is_empty() {
let callee = info.call.callee.as_deref().unwrap_or("");
let source_kind = crate::labels::infer_source_kind(source_caps, callee);
let origin = TaintOrigin {
node: inst.cfg_node,
source_kind,
source_span: None,
};
state.set(
inst.value,
VarTaint {
caps: source_caps,
origins: SmallVec::from_elem(origin, 1),
uses_summary: false,
},
);
}
}
SsaOp::CatchParam => {
let origin = TaintOrigin {
node: inst.cfg_node,
source_kind: SourceKind::CaughtException,
source_span: None,
};
state.set(
inst.value,
VarTaint {
caps: Cap::all(),
origins: SmallVec::from_elem(origin, 1),
uses_summary: false,
},
);
}
SsaOp::Call {
callee,
args,
receiver,
..
} => {
if crate::labels::is_excluded(transfer.lang.as_str(), callee.as_bytes()) {
return;
}
if let (Some(pf), Some(rcv)) = (transfer.pointer_facts, *receiver) {
if crate::pointer::is_container_write_callee(callee) {
let pt = pf.pt(rcv);
if !pt.is_empty() && !pt.is_top() {
let mut elem_caps = Cap::empty();
let mut elem_origins: SmallVec<[TaintOrigin; 2]> = SmallVec::new();
let mut elem_summary = false;
let mut elem_must_all = true; let mut elem_may_any = false; let mut saw_any_arg = false;
for arg_group in args {
for &arg_v in arg_group {
saw_any_arg = true;
if let Some(t) = state.get(arg_v) {
elem_caps |= t.caps;
elem_summary |= t.uses_summary;
for o in &t.origins {
push_origin_bounded(&mut elem_origins, *o);
}
}
let (am, av) =
ssa_value_validated_bits(arg_v, ssa, transfer.interner, state);
elem_must_all &= am;
elem_may_any |= av;
}
}
if !saw_any_arg {
elem_must_all = false;
}
if !elem_caps.is_empty() {
let cell = VarTaint {
caps: elem_caps,
origins: elem_origins,
uses_summary: elem_summary,
};
for loc in pt.iter() {
let key = crate::taint::ssa_transfer::state::FieldTaintKey {
loc,
field: crate::ssa::ir::FieldId::ELEM,
};
state.add_field(key, cell.clone(), elem_must_all, elem_may_any);
}
}
}
}
}
let mut return_bits = Cap::empty();
let mut return_origins: SmallVec<[TaintOrigin; 2]> = SmallVec::new();
let is_network_fetch_source = info
.taint
.labels
.iter()
.any(|l| matches!(l, DataLabel::Source(_)))
&& info
.taint
.labels
.iter()
.any(|l| matches!(l, DataLabel::Sink(c) if c.contains(Cap::SSRF)));
let url_prefix_safe_via_node = info
.string_prefix
.as_deref()
.map(|p| {
let synthetic = crate::abstract_interp::StringFact::from_prefix(p);
is_string_safe_for_ssrf(&synthetic)
})
.unwrap_or(false);
let url_prefix_safe_via_abs = state.abstract_state.as_ref().is_some_and(|abs| {
args.first().is_some_and(|first_arg| {
!first_arg.is_empty()
&& first_arg
.iter()
.all(|v| is_string_safe_for_ssrf(&abs.get(*v).string))
})
});
let url_prefix_safe_via_first_arg_text = is_network_fetch_source
&& info
.call
.arg_string_literals
.first()
.and_then(|v| v.as_deref())
.map(|s| {
let synthetic = crate::abstract_interp::StringFact::from_prefix(s);
is_string_safe_for_ssrf(&synthetic)
})
.unwrap_or(false);
let url_is_hardcoded_safe = is_network_fetch_source
&& (url_prefix_safe_via_node
|| url_prefix_safe_via_abs
|| url_prefix_safe_via_first_arg_text);
for lbl in &info.taint.labels {
if let DataLabel::Source(bits) = lbl {
if url_is_hardcoded_safe {
continue;
}
return_bits |= *bits;
let callee_str = info.call.callee.as_deref().unwrap_or("");
let source_kind = crate::labels::infer_source_kind(*bits, callee_str);
let origin = TaintOrigin {
node: inst.cfg_node,
source_kind,
source_span: None,
};
if !return_origins.iter().any(|o| o.node == inst.cfg_node) {
return_origins.push(origin);
}
}
}
if !return_bits.is_empty() {
if let Some(positions) =
crate::labels::output_param_source_positions(transfer.lang.as_str(), callee)
{
for &pos in positions {
if let Some(arg_group) = args.get(pos) {
for &arg_v in arg_group {
state.set(
arg_v,
VarTaint {
caps: return_bits,
origins: return_origins.clone(),
uses_summary: false,
},
);
}
}
}
}
}
let mut sanitizer_bits = Cap::empty();
for lbl in &info.taint.labels {
if let DataLabel::Sanitizer(bits) = lbl {
sanitizer_bits |= *bits;
}
}
let caller_func = info.ast.enclosing_func.as_deref().unwrap_or("");
let has_source_label = info
.taint
.labels
.iter()
.any(|l| matches!(l, DataLabel::Source(_)));
let mut resolved_callee = false;
if transfer.inline_cache.is_some() && transfer.context_depth < 1 {
if let Some(result) =
inline_analyse_callee(callee, args, receiver, state, transfer, cfg, ssa, inst)
{
if let Some(ref ret) = result.return_taint {
resolved_callee = true;
return_bits |= ret.caps;
for orig in &ret.origins {
push_origin_bounded(&mut return_origins, *orig);
}
}
if !result.return_path_fact.is_top() {
if let Some(ref mut abs) = state.abstract_state {
let mut av = abs.get(inst.value);
av.path = <crate::abstract_interp::PathFact as crate::state::lattice::AbstractDomain>::meet(
&av.path,
&result.return_path_fact,
);
if !av.is_top() {
abs.set(inst.value, av);
}
}
}
}
}
let mut resolved_container_to_return: Vec<usize> = Vec::new();
let mut resolved_container_store: Vec<(usize, usize)> = Vec::new();
let mut resolved_points_to: crate::summary::points_to::PointsToSummary =
crate::summary::points_to::PointsToSummary::empty();
let arity_hint = info.call.arg_uses.len();
let callee_summary = resolve_callee_typed(
transfer,
callee,
caller_func,
info.call.call_ordinal,
Some(arity_hint),
*receiver,
);
if let Some(ref resolved) = callee_summary {
resolved_container_to_return = resolved.param_container_to_return.clone();
resolved_container_store = resolved.param_to_container_store.clone();
resolved_points_to = resolved.points_to.clone();
if let Some(pf) = transfer.pointer_facts {
apply_field_points_to_writes(
&resolved.field_points_to,
args,
receiver,
state,
ssa,
pf,
transfer.interner,
);
}
callee_return_abstract = resolved.return_abstract.clone();
if !resolved.abstract_transfer.is_empty() {
let mut synthesised: Option<crate::abstract_interp::AbstractValue> = None;
for (idx, transfer) in &resolved.abstract_transfer {
if transfer.is_top() {
continue;
}
let arg_abs = if let Some(group) = args.get(*idx) {
let mut joined: Option<crate::abstract_interp::AbstractValue> = None;
for &v in group {
let av = state
.abstract_state
.as_ref()
.map(|a| a.get(v))
.unwrap_or_else(crate::abstract_interp::AbstractValue::top);
joined = Some(match joined {
None => av,
Some(prev) => prev.join(&av),
});
}
joined.unwrap_or_else(crate::abstract_interp::AbstractValue::top)
} else {
crate::abstract_interp::AbstractValue::top()
};
let applied = transfer.apply(&arg_abs);
if applied.is_top() {
continue;
}
synthesised = Some(match synthesised {
None => applied,
Some(prev) => prev.join(&applied),
});
}
if let Some(synth) = synthesised {
callee_return_abstract = match callee_return_abstract.take() {
Some(base) => {
let m = base.meet(&synth);
if m.is_bottom() {
Some(synth.join(&base))
} else {
Some(m)
}
}
None => Some(synth),
};
}
}
if let Some(ref rtype) = resolved.return_type {
if let Some(ref mut env) = state.path_env {
use crate::constraint::domain::{TypeSet, ValueFact};
let mut fact = ValueFact::top();
fact.types = TypeSet::singleton(rtype);
env.refine(inst.value, &fact);
}
}
}
if resolved_container_store.is_empty() {
if let Some(ref oc) = info.call.outer_callee {
if let Some(ref resolved) = resolve_callee_hinted(
transfer,
oc,
caller_func,
info.call.call_ordinal,
Some(arity_hint),
) {
if resolved_container_to_return.is_empty() {
resolved_container_to_return =
resolved.param_container_to_return.clone();
}
resolved_container_store = resolved.param_to_container_store.clone();
}
}
}
if !resolved_callee && let Some(resolved) = callee_summary {
resolved_callee = true;
if !has_source_label && !resolved.source_caps.is_empty() {
return_bits |= resolved.source_caps;
let source_kind =
crate::labels::infer_source_kind(resolved.source_caps, callee);
let origin = TaintOrigin {
node: inst.cfg_node,
source_kind,
source_span: None,
};
if !return_origins.iter().any(|o| o.node == inst.cfg_node) {
return_origins.push(origin);
}
}
let mut aggregate_sanitizer_applied = false;
if resolved.propagates_taint {
let effective_params = if info.call.arg_uses.is_empty() {
&[] as &[usize]
} else {
&resolved.propagating_params
};
if !resolved.param_return_paths.is_empty() && !effective_params.is_empty() {
let mut any_origin_added = false;
for ¶m_idx in effective_params {
let arg_caps_origins =
collect_args_taint(args, receiver, state, &[param_idx]);
let arg_caps = arg_caps_origins.0;
let arg_origins = arg_caps_origins.1;
let param_sanitizer =
effective_param_sanitizer(&resolved, param_idx, state);
return_bits |= arg_caps & !param_sanitizer;
for orig in &arg_origins {
if push_origin_bounded(&mut return_origins, *orig) {
any_origin_added = true;
}
}
}
aggregate_sanitizer_applied = true;
let _ = any_origin_added;
} else {
let (prop_caps, prop_origins) =
collect_args_taint(args, receiver, state, effective_params);
return_bits |= prop_caps;
for orig in &prop_origins {
push_origin_bounded(&mut return_origins, *orig);
}
}
}
if !aggregate_sanitizer_applied {
return_bits &= !resolved.sanitizer_caps;
}
}
if !resolved_callee && info.taint.labels.is_empty() {
if let Some(rv) = receiver {
if transfer.type_facts.is_some() || state.path_env.is_some() {
let tq_labels = resolve_type_qualified_labels(
callee,
*rv,
transfer.type_facts,
state.path_env.as_ref(),
transfer.lang,
transfer.extra_labels,
Some(ssa),
);
for lbl in &tq_labels {
match lbl {
DataLabel::Source(bits) if !has_source_label => {
return_bits |= *bits;
let source_kind =
crate::labels::infer_source_kind(*bits, callee);
let origin = TaintOrigin {
node: inst.cfg_node,
source_kind,
source_span: None,
};
if !return_origins.iter().any(|o| o.node == inst.cfg_node) {
return_origins.push(origin);
}
}
DataLabel::Sanitizer(bits) => {
sanitizer_bits |= *bits;
}
DataLabel::Sink(_) => {
}
_ => {}
}
}
}
}
}
if !sanitizer_bits.is_empty() {
if !resolved_callee {
let (use_caps, use_origins) = collect_args_taint(args, receiver, state, &[]);
return_bits |= use_caps;
for orig in &use_origins {
push_origin_bounded(&mut return_origins, *orig);
}
}
return_bits &= !sanitizer_bits;
if sanitizer_bits.contains(Cap::UNAUTHORIZED_ID) {
strip_cap_from_call_args(args, receiver, state, Cap::UNAUTHORIZED_ID);
}
} else if !resolved_callee {
let mut container_handled = try_container_propagation(
inst, info, args, receiver, state, transfer, callee, ssa,
);
if !container_handled {
if let Some(ref oc) = info.call.outer_callee {
container_handled = try_container_propagation(
inst, info, args, receiver, state, transfer, oc, ssa,
);
}
}
if container_handled {
if !return_bits.is_empty() {
let recv_callee = info.call.outer_callee.as_deref().unwrap_or(callee);
if let Some(container_val) =
find_container_receiver(recv_callee, receiver, args, ssa, transfer.lang)
{
if let Some(pts) = lookup_pts(transfer, container_val) {
state.heap.store_set(
&pts,
HeapSlot::Elements,
return_bits,
&return_origins,
);
}
merge_taint_into(state, container_val, return_bits, &return_origins);
}
}
if return_bits.is_empty() {
apply_container_elem_read_w4(inst, ssa, transfer, state);
return;
}
} else {
if try_curl_url_propagation(inst, info, args, state) {
return;
}
if let Some(prop) =
crate::labels::arg_propagation(transfer.lang.as_str(), callee)
{
let mut input_caps = Cap::empty();
let mut input_origins: SmallVec<[TaintOrigin; 2]> = SmallVec::new();
for &from_pos in prop.from_args {
if let Some(arg_group) = args.get(from_pos) {
for &v in arg_group {
if let Some(taint) = state.get(v) {
input_caps |= taint.caps;
for orig in &taint.origins {
push_origin_bounded(&mut input_origins, *orig);
}
}
}
}
}
if !input_caps.is_empty() {
for &to_pos in prop.to_args {
if let Some(arg_group) = args.get(to_pos) {
for &arg_v in arg_group {
state.set(
arg_v,
VarTaint {
caps: input_caps,
origins: input_origins.clone(),
uses_summary: false,
},
);
}
}
}
}
}
let (use_caps, use_origins) = collect_args_taint(args, receiver, state, &[]);
if return_bits.is_empty() {
return_bits = use_caps;
return_origins = use_origins;
}
}
}
if !sanitizer_bits.is_empty() {
if let Some(aliases) = transfer.base_aliases {
if !aliases.is_empty() {
propagate_sanitization_to_aliases(
inst,
state,
sanitizer_bits,
aliases,
ssa,
);
}
}
}
if !resolved_container_to_return.is_empty() {
if let Some(dyn_ref) = transfer.dynamic_pts {
let mut container_pts_list: SmallVec<[PointsToSet; 2]> = SmallVec::new();
for ¶m_idx in &resolved_container_to_return {
if let Some(arg_group) = args.get(param_idx) {
for &arg_v in arg_group {
if let Some(pts) = lookup_pts(transfer, arg_v) {
container_pts_list.push(pts);
}
}
}
}
if !container_pts_list.is_empty() {
let mut dyn_pts = dyn_ref.borrow_mut();
for pts in &container_pts_list {
match dyn_pts.get(&inst.value) {
Some(existing) => {
let merged = existing.union(pts);
dyn_pts.insert(inst.value, merged);
}
None => {
dyn_pts.insert(inst.value, pts.clone());
}
}
}
}
}
}
if !resolved_container_store.is_empty() {
for &(src_param, container_param) in &resolved_container_store {
let mut container_pts: SmallVec<[PointsToSet; 2]> = SmallVec::new();
if let Some(arg_group) = args.get(container_param) {
for &v in arg_group {
if let Some(pts) = lookup_pts(transfer, v) {
container_pts.push(pts);
}
}
}
if container_pts.is_empty() {
continue;
}
let mut src_caps = Cap::empty();
let mut src_origins: SmallVec<[TaintOrigin; 2]> = SmallVec::new();
if let Some(arg_group) = args.get(src_param) {
for &v in arg_group {
if let Some(taint) = state.get(v) {
src_caps |= taint.caps;
for orig in &taint.origins {
push_origin_bounded(&mut src_origins, *orig);
}
}
}
}
if src_caps.is_empty() && !return_bits.is_empty() {
src_caps = return_bits;
src_origins = return_origins.clone();
}
if !src_caps.is_empty() {
for pts in &container_pts {
state
.heap
.store_set(pts, HeapSlot::Elements, src_caps, &src_origins);
}
}
}
}
if resolved_points_to.returns_fresh_alloc
&& let Some(dyn_ref) = transfer.dynamic_pts
{
let fresh = PointsToSet::singleton(HeapObjectId(inst.value));
let mut dyn_pts = dyn_ref.borrow_mut();
match dyn_pts.get(&inst.value) {
Some(existing) => {
let merged = existing.union(&fresh);
dyn_pts.insert(inst.value, merged);
}
None => {
dyn_pts.insert(inst.value, fresh);
}
}
}
if resolved_points_to.overflow || !resolved_points_to.edges.is_empty() {
use crate::summary::points_to::AliasPosition;
type ParamToParamEdges = SmallVec<[(usize, usize); 8]>;
type ParamToReturnEdges = SmallVec<[usize; 4]>;
let (param_to_param_edges, param_to_return_edges): (
ParamToParamEdges,
ParamToReturnEdges,
) = if resolved_points_to.overflow {
let n = args.len();
let mut p2p: SmallVec<[(usize, usize); 8]> = SmallVec::new();
let mut p2r: SmallVec<[usize; 4]> = SmallVec::new();
for i in 0..n {
p2r.push(i);
for j in 0..n {
if i != j {
p2p.push((i, j));
}
}
}
(p2p, p2r)
} else {
let mut p2p: SmallVec<[(usize, usize); 8]> = SmallVec::new();
let mut p2r: SmallVec<[usize; 4]> = SmallVec::new();
for edge in &resolved_points_to.edges {
match (edge.source, edge.target) {
(AliasPosition::Param(s), AliasPosition::Param(t)) => {
p2p.push((s as usize, t as usize));
}
(AliasPosition::Param(s), AliasPosition::Return) => {
p2r.push(s as usize);
}
_ => {}
}
}
(p2p, p2r)
};
for (src, dst) in ¶m_to_param_edges {
let mut src_caps = Cap::empty();
let mut src_origins: SmallVec<[TaintOrigin; 2]> = SmallVec::new();
if let Some(arg_vals) = args.get(*src) {
for &v in arg_vals {
if let Some(taint) = state.get(v) {
src_caps |= taint.caps;
for orig in &taint.origins {
push_origin_bounded(&mut src_origins, *orig);
}
}
}
}
if src_caps.is_empty() {
continue;
}
let mut dst_pts: SmallVec<[PointsToSet; 2]> = SmallVec::new();
let mut dst_ssa_vals: SmallVec<[SsaValue; 2]> = SmallVec::new();
if let Some(arg_vals) = args.get(*dst) {
for &v in arg_vals {
dst_ssa_vals.push(v);
if let Some(pts) = lookup_pts(transfer, v) {
dst_pts.push(pts);
}
}
}
for pts in &dst_pts {
state
.heap
.store_set(pts, HeapSlot::Elements, src_caps, &src_origins);
}
for dv in &dst_ssa_vals {
merge_taint_into(state, *dv, src_caps, &src_origins);
}
}
if !param_to_return_edges.is_empty()
&& let Some(dyn_ref) = transfer.dynamic_pts
{
for src in ¶m_to_return_edges {
let mut src_pts: SmallVec<[PointsToSet; 2]> = SmallVec::new();
if let Some(arg_vals) = args.get(*src) {
for &v in arg_vals {
if let Some(pts) = lookup_pts(transfer, v) {
src_pts.push(pts);
}
}
}
if src_pts.is_empty() {
continue;
}
let mut dyn_pts = dyn_ref.borrow_mut();
for pts in &src_pts {
match dyn_pts.get(&inst.value) {
Some(existing) => {
let merged = existing.union(pts);
dyn_pts.insert(inst.value, merged);
}
None => {
dyn_pts.insert(inst.value, pts.clone());
}
}
}
}
}
}
if !return_bits.is_empty() {
if let Some(aliases) = transfer.base_aliases {
if !aliases.is_empty() {
propagate_taint_to_aliases(
inst,
state,
return_bits,
&return_origins,
aliases,
ssa,
);
}
}
}
if !return_bits.is_empty() && has_source_label {
if let Some(ref oc) = info.call.outer_callee {
if let Some(ref oc_sum) = resolve_callee_hinted(
transfer,
oc,
caller_func,
info.call.call_ordinal,
Some(arity_hint),
) {
if !oc_sum.propagates_taint && oc_sum.source_caps.is_empty() {
return_bits = Cap::empty();
return_origins.clear();
}
}
}
}
if return_bits.is_empty() {
state.remove(inst.value);
} else {
state.set(
inst.value,
VarTaint {
caps: return_bits,
origins: return_origins,
uses_summary: resolved_callee,
},
);
}
}
SsaOp::Assign(uses) => {
let mut sanitizer_bits = Cap::empty();
for lbl in &info.taint.labels {
if let DataLabel::Sanitizer(bits) = lbl {
sanitizer_bits |= *bits;
}
}
let mut combined_caps = Cap::empty();
let mut combined_origins: SmallVec<[TaintOrigin; 2]> = SmallVec::new();
let mut inherited_summary = false;
if !info.is_eq_with_const {
for &use_val in uses {
if let Some(taint) = state.get(use_val) {
combined_caps |= taint.caps;
inherited_summary |= taint.uses_summary;
for orig in &taint.origins {
push_origin_bounded(&mut combined_origins, *orig);
}
}
}
}
if let Some((receiver, _fid)) = ssa.field_writes.get(&inst.value).copied() {
if let Some(taint) = state.get(receiver) {
combined_caps |= taint.caps;
inherited_summary |= taint.uses_summary;
for orig in &taint.origins {
push_origin_bounded(&mut combined_origins, *orig);
}
}
}
combined_caps &= !sanitizer_bits;
if !sanitizer_bits.is_empty() {
if let Some(aliases) = transfer.base_aliases {
if !aliases.is_empty() {
propagate_sanitization_to_aliases(
inst,
state,
sanitizer_bits,
aliases,
ssa,
);
}
}
}
for lbl in &info.taint.labels {
if let DataLabel::Source(bits) = lbl {
combined_caps |= *bits;
let callee_str = info.call.callee.as_deref().unwrap_or("");
let source_kind = crate::labels::infer_source_kind(*bits, callee_str);
let origin = TaintOrigin {
node: inst.cfg_node,
source_kind,
source_span: None,
};
push_origin_bounded(&mut combined_origins, origin);
}
}
if !combined_caps.is_empty() {
if let Some(aliases) = transfer.base_aliases {
if !aliases.is_empty() {
propagate_taint_to_aliases(
inst,
state,
combined_caps,
&combined_origins,
aliases,
ssa,
);
}
}
}
if combined_caps.is_empty() {
state.remove(inst.value);
} else {
state.set(
inst.value,
VarTaint {
caps: combined_caps,
origins: combined_origins.clone(),
uses_summary: inherited_summary,
},
);
}
if let Some(pf) = transfer.pointer_facts {
if let Some((receiver, fid)) = ssa.field_writes.get(&inst.value).copied() {
let pt = pf.pt(receiver);
if !pt.is_empty() && !pt.is_top() && !combined_caps.is_empty() {
let rhs_taint = VarTaint {
caps: combined_caps,
origins: combined_origins.clone(),
uses_summary: inherited_summary,
};
let mut must_all = true;
let mut may_any = false;
let mut saw_use = false;
if let SsaOp::Assign(uses) = &inst.op {
for &u in uses {
saw_use = true;
let (am, av) =
ssa_value_validated_bits(u, ssa, transfer.interner, state);
must_all &= am;
may_any |= av;
}
}
if !saw_use {
must_all = false;
}
for loc in pt.iter() {
let key = crate::taint::ssa_transfer::state::FieldTaintKey {
loc,
field: fid,
};
state.add_field(key, rhs_taint.clone(), must_all, may_any);
}
}
}
}
}
SsaOp::Const(_) | SsaOp::Nop => {
}
SsaOp::Param { .. } | SsaOp::SelfParam => {
let mut seeded_from_scope = false;
let per_call_taint: Option<&VarTaint> = match &inst.op {
SsaOp::Param { index } => transfer
.param_seed
.and_then(|ps| ps.get(*index))
.and_then(|slot| slot.as_ref()),
SsaOp::SelfParam => transfer.receiver_seed,
_ => None,
};
if let Some(taint) = per_call_taint {
let remapped_origins: SmallVec<[TaintOrigin; 2]> = taint
.origins
.iter()
.map(|o| TaintOrigin {
node: inst.cfg_node,
source_kind: o.source_kind,
source_span: o.source_span,
})
.collect();
state.set(
inst.value,
VarTaint {
caps: taint.caps,
origins: remapped_origins,
uses_summary: true,
},
);
seeded_from_scope = true;
}
if !seeded_from_scope {
if let Some(seed) = &transfer.global_seed {
if let Some(var_name) = ssa
.value_defs
.get(inst.value.0 as usize)
.and_then(|vd| vd.var_name.as_deref())
{
let mut ancestors: SmallVec<[BodyId; 2]> = SmallVec::new();
if let Some(pid) = transfer.parent_body_id {
ancestors.push(pid);
}
if !ancestors.contains(&BodyId(0)) {
ancestors.push(BodyId(0));
}
for body_id in ancestors {
let key = BindingKey::new(var_name, body_id);
if let Some(taint) = seed_lookup(seed, &key) {
let remapped_origins: SmallVec<[TaintOrigin; 2]> = taint
.origins
.iter()
.map(|o| TaintOrigin {
node: inst.cfg_node,
source_kind: o.source_kind,
source_span: o.source_span,
})
.collect();
state.set(
inst.value,
VarTaint {
caps: taint.caps,
origins: remapped_origins,
uses_summary: true,
},
);
seeded_from_scope = true;
break;
}
}
}
}
}
if transfer.auto_seed_handler_params
&& !seeded_from_scope
&& matches!(&inst.op, SsaOp::Param { .. })
{
if let Some(var_name) = ssa
.value_defs
.get(inst.value.0 as usize)
.and_then(|vd| vd.var_name.as_deref())
{
if crate::labels::is_js_ts_handler_param_name(var_name) {
let origin = TaintOrigin {
node: inst.cfg_node,
source_kind: SourceKind::UserInput,
source_span: None,
};
state.set(
inst.value,
VarTaint {
caps: Cap::all(),
origins: SmallVec::from_elem(origin, 1),
uses_summary: false,
},
);
}
}
}
}
SsaOp::Phi(_) => {
}
SsaOp::Undef => {
}
SsaOp::FieldProj {
receiver, field, ..
} => {
let mut combined: Option<VarTaint> = state.get(*receiver).cloned();
let mut cell_must_all: Option<bool> = None;
let mut cell_may_any = false;
if let Some(pf) = transfer.pointer_facts {
let pt = pf.pt(*receiver);
if !pt.is_empty() && !pt.is_top() {
for loc in pt.iter() {
let mut hit_specific = false;
for field_id in [*field, crate::ssa::ir::FieldId::ANY_FIELD].iter().copied()
{
if field_id == crate::ssa::ir::FieldId::ANY_FIELD && hit_specific {
break;
}
if field_id == crate::ssa::ir::FieldId::ANY_FIELD
&& *field == crate::ssa::ir::FieldId::ANY_FIELD
{
continue;
}
let key = crate::taint::ssa_transfer::state::FieldTaintKey {
loc,
field: field_id,
};
if let Some(cell) = state.get_field(key) {
if field_id == *field {
hit_specific = true;
}
let t = cell.taint.clone();
cell_must_all = Some(match cell_must_all {
Some(prev) => prev && cell.validated_must,
None => cell.validated_must,
});
cell_may_any |= cell.validated_may;
combined = Some(match combined {
Some(mut acc) => {
acc.caps |= t.caps;
acc.uses_summary |= t.uses_summary;
for o in &t.origins {
push_origin_bounded(&mut acc.origins, *o);
}
acc
}
None => {
let mut bounded: SmallVec<[TaintOrigin; 2]> =
SmallVec::new();
for o in &t.origins {
push_origin_bounded(&mut bounded, *o);
}
VarTaint {
caps: t.caps,
origins: bounded,
uses_summary: t.uses_summary,
}
}
});
}
}
}
}
}
if let Some(t) = combined {
state.set(inst.value, t);
}
if let Some(must_all) = cell_must_all {
if let Some(name) = ssa
.value_defs
.get(inst.value.0 as usize)
.and_then(|vd| vd.var_name.as_deref())
{
if let Some(sym) = transfer.interner.get(name) {
if must_all {
state.validated_must.insert(sym);
}
if cell_may_any {
state.validated_may.insert(sym);
}
}
}
}
}
}
if matches!(&inst.op, SsaOp::Call { .. }) {
apply_container_elem_read_w4(inst, ssa, transfer, state);
}
if let Some(ref mut env) = state.path_env {
match &inst.op {
SsaOp::Assign(uses) if uses.len() == 1 => {
let src_fact = env.get(uses[0]);
if !src_fact.is_top() {
env.refine(inst.value, &src_fact);
env.assert_equal(inst.value, uses[0]);
}
let node_info = &cfg[inst.cfg_node];
if let Some(ref cast_type) = node_info.cast_target_type {
if let Some(kind) = crate::constraint::solver::parse_type_name(cast_type) {
let mut fact = constraint::ValueFact::top();
fact.types = constraint::TypeSet::singleton(&kind);
fact.null = constraint::Nullability::NonNull;
env.refine(inst.value, &fact);
}
}
}
SsaOp::Const(Some(text)) => {
if let Some(cv) = constraint::ConstValue::parse_literal(text) {
let mut fact = constraint::ValueFact::top();
fact.exact = Some(cv.clone());
match &cv {
constraint::ConstValue::Int(i) => {
fact.lo = Some(*i);
fact.hi = Some(*i);
fact.types = constraint::TypeSet::singleton(
&crate::ssa::type_facts::TypeKind::Int,
);
fact.null = constraint::Nullability::NonNull;
}
constraint::ConstValue::Bool(b) => {
fact.bool_state = if *b {
constraint::BoolState::True
} else {
constraint::BoolState::False
};
fact.types = constraint::TypeSet::singleton(
&crate::ssa::type_facts::TypeKind::Bool,
);
fact.null = constraint::Nullability::NonNull;
}
constraint::ConstValue::Null => {
fact.null = constraint::Nullability::Null;
fact.types = constraint::TypeSet::singleton(
&crate::ssa::type_facts::TypeKind::Null,
);
}
constraint::ConstValue::Str(_) => {
fact.types = constraint::TypeSet::singleton(
&crate::ssa::type_facts::TypeKind::String,
);
fact.null = constraint::Nullability::NonNull;
}
}
env.refine(inst.value, &fact);
}
}
_ => {
}
}
}
if let Some(ref mut abs) = state.abstract_state {
transfer_abstract(inst, cfg, abs, Some(transfer.lang));
}
if let Some(ref abs_val) = callee_return_abstract {
if let Some(ref mut abs) = state.abstract_state {
abs.set(inst.value, abs_val.clone());
}
}
}
fn transfer_abstract(inst: &SsaInst, cfg: &Cfg, abs: &mut AbstractState, lang: Option<Lang>) {
use crate::abstract_interp::{AbstractValue, BitFact, IntervalFact, PathFact, StringFact};
use crate::cfg::BinOp;
let info = &cfg[inst.cfg_node];
match &inst.op {
SsaOp::Const(Some(text)) => {
let trimmed = text.trim();
if let Ok(n) = trimmed.parse::<i64>() {
abs.set(
inst.value,
AbstractValue {
interval: IntervalFact::exact(n),
string: StringFact::top(),
bits: BitFact::from_const(n),
path: PathFact::top(),
},
);
} else if is_string_const(trimmed) {
let s = strip_string_quotes(trimmed);
let mut pf = PathFact::top();
if !s.contains("..") {
pf = pf.with_dotdot_cleared();
}
if !(s.starts_with('/') || s.starts_with('\\')) {
pf = pf.with_absolute_cleared();
}
abs.set(
inst.value,
AbstractValue {
interval: IntervalFact::top(),
string: StringFact::exact(&s),
bits: BitFact::top(),
path: pf,
},
);
}
}
SsaOp::Assign(_) if info.string_prefix.is_some() => {
let prefix = info.string_prefix.as_deref().unwrap();
abs.set(
inst.value,
AbstractValue {
interval: IntervalFact::top(),
string: StringFact::from_prefix(prefix),
bits: BitFact::top(),
path: PathFact::top(),
},
);
}
SsaOp::Call { .. } if info.string_prefix.is_some() => {
let prefix = info.string_prefix.as_deref().unwrap();
abs.set(
inst.value,
AbstractValue {
interval: IntervalFact::top(),
string: StringFact::from_prefix(prefix),
bits: BitFact::top(),
path: PathFact::top(),
},
);
}
SsaOp::Assign(uses) if uses.len() == 1 => {
if let (Some(bin_op), Some(const_val)) = (info.bin_op, info.bin_op_const) {
let var_abs = abs.get(uses[0]);
let const_abs = AbstractValue {
interval: IntervalFact::exact(const_val),
string: StringFact::top(),
bits: BitFact::from_const(const_val),
path: PathFact::top(),
};
let result_interval = match bin_op {
BinOp::Add => var_abs.interval.add(&const_abs.interval),
BinOp::Sub => var_abs.interval.sub(&const_abs.interval),
BinOp::Mul => var_abs.interval.mul(&const_abs.interval),
BinOp::Div => var_abs.interval.div(&const_abs.interval),
BinOp::Mod => var_abs.interval.modulo(&const_abs.interval),
BinOp::BitAnd => var_abs.interval.bit_and(&const_abs.interval),
BinOp::BitOr => var_abs.interval.bit_or(&const_abs.interval),
BinOp::BitXor => var_abs.interval.bit_xor(&const_abs.interval),
BinOp::LeftShift => var_abs.interval.left_shift(&const_abs.interval),
BinOp::RightShift => var_abs.interval.right_shift(&const_abs.interval),
BinOp::Eq
| BinOp::NotEq
| BinOp::Lt
| BinOp::LtEq
| BinOp::Gt
| BinOp::GtEq => IntervalFact {
lo: Some(0),
hi: Some(1),
},
};
let result_bits = match bin_op {
BinOp::BitAnd => var_abs.bits.bit_and(&const_abs.bits),
BinOp::BitOr => var_abs.bits.bit_or(&const_abs.bits),
BinOp::BitXor => var_abs.bits.bit_xor(&const_abs.bits),
BinOp::LeftShift => var_abs.bits.left_shift(&const_abs.interval),
BinOp::RightShift => var_abs.bits.right_shift(&const_abs.interval),
_ => BitFact::top(),
};
let val = AbstractValue {
interval: result_interval,
string: StringFact::top(),
bits: result_bits,
path: PathFact::top(),
};
if !val.is_top() {
abs.set(inst.value, val);
}
} else {
let src = abs.get(uses[0]);
if !src.is_top() {
abs.set(inst.value, src);
}
}
}
SsaOp::Assign(uses) if uses.len() == 2 => {
let lhs_abs = abs.get(uses[0]);
let rhs_abs = abs.get(uses[1]);
if let Some(bin_op) = info.bin_op {
let result_interval = match bin_op {
BinOp::Add => lhs_abs.interval.add(&rhs_abs.interval),
BinOp::Sub => lhs_abs.interval.sub(&rhs_abs.interval),
BinOp::Mul => lhs_abs.interval.mul(&rhs_abs.interval),
BinOp::Div => lhs_abs.interval.div(&rhs_abs.interval),
BinOp::Mod => lhs_abs.interval.modulo(&rhs_abs.interval),
BinOp::BitAnd => lhs_abs.interval.bit_and(&rhs_abs.interval),
BinOp::BitOr => lhs_abs.interval.bit_or(&rhs_abs.interval),
BinOp::BitXor => lhs_abs.interval.bit_xor(&rhs_abs.interval),
BinOp::LeftShift => lhs_abs.interval.left_shift(&rhs_abs.interval),
BinOp::RightShift => lhs_abs.interval.right_shift(&rhs_abs.interval),
BinOp::Eq
| BinOp::NotEq
| BinOp::Lt
| BinOp::LtEq
| BinOp::Gt
| BinOp::GtEq => IntervalFact {
lo: Some(0),
hi: Some(1),
},
};
let result_string = if bin_op == BinOp::Add {
lhs_abs.string.concat(&rhs_abs.string)
} else {
StringFact::top()
};
let result_bits = match bin_op {
BinOp::BitAnd => lhs_abs.bits.bit_and(&rhs_abs.bits),
BinOp::BitOr => lhs_abs.bits.bit_or(&rhs_abs.bits),
BinOp::BitXor => lhs_abs.bits.bit_xor(&rhs_abs.bits),
BinOp::LeftShift => lhs_abs.bits.left_shift(&rhs_abs.interval),
BinOp::RightShift => lhs_abs.bits.right_shift(&rhs_abs.interval),
_ => BitFact::top(),
};
let val = AbstractValue {
interval: result_interval,
string: result_string,
bits: result_bits,
path: PathFact::top(),
};
if !val.is_top() {
abs.set(inst.value, val);
}
} else {
let string_result = lhs_abs.string.concat(&rhs_abs.string);
if !string_result.is_top() {
abs.set(
inst.value,
AbstractValue {
interval: IntervalFact::top(),
string: string_result,
bits: BitFact::top(),
path: PathFact::top(),
},
);
}
}
}
SsaOp::Call { callee, .. } if is_int_producing_callee(callee) => {
abs.set(
inst.value,
AbstractValue {
interval: IntervalFact {
lo: Some(i32::MIN as i64),
hi: Some(i32::MAX as i64),
},
string: StringFact::top(),
bits: BitFact::top(),
path: PathFact::top(),
},
);
}
SsaOp::Call {
callee,
args,
receiver,
..
} if lang.is_some() => {
let input_val = receiver
.as_ref()
.copied()
.or_else(|| args.first().and_then(|g| g.first().copied()));
let input_fact = input_val
.map(|v| abs.get(v).path)
.unwrap_or_else(PathFact::top);
let lang_unwrapped = lang.expect("guard ensures lang.is_some()");
if let Some(pf) = crate::abstract_interp::path_domain::classify_path_primitive_for_lang(
lang_unwrapped,
callee,
&input_fact,
) {
abs.set(inst.value, AbstractValue::with_path_fact(pf));
} else if matches!(lang, Some(Lang::Rust)) {
let leaf = crate::callgraph::callee_leaf_name(callee);
let mut handled = false;
if leaf == "replace" {
if let Some(first_arg) = args.first().and_then(|g| g.first()) {
let arg_string = abs.get(*first_arg).string;
let needle = arg_string
.domain
.as_ref()
.and_then(|d| (d.len() == 1).then(|| d[0].clone()));
if let Some(needle) = needle {
let mut new_fact = input_fact.clone();
let mut narrowed = false;
if needle == ".." {
new_fact = new_fact.with_dotdot_cleared();
narrowed = true;
} else if needle == "/" || needle == "\\" {
new_fact = new_fact.with_absolute_cleared();
narrowed = true;
}
if narrowed {
abs.set(inst.value, AbstractValue::with_path_fact(new_fact));
handled = true;
}
}
}
}
if !handled
&& receiver.is_none()
&& info.call.arg_uses.len() == 1
&& crate::abstract_interp::path_domain::is_structural_variant_ctor(callee)
{
if let Some(group) = args.first() {
let mut joined_inner: Option<PathFact> = None;
for &v in group {
let f = abs.get(v).path;
if f.is_top() {
continue;
}
joined_inner = Some(match joined_inner {
None => f,
Some(prev) => prev.join(&f),
});
}
if let Some(inner_fact) = joined_inner {
abs.set(inst.value, AbstractValue::with_path_fact(inner_fact));
handled = true;
}
}
}
if !handled
&& receiver.is_none()
&& args.is_empty()
&& has_typeprefix_upper_scoped(callee)
&& !has_source_label_on_node(info)
{
let fact = PathFact::top()
.with_dotdot_cleared()
.with_absolute_cleared();
abs.set(inst.value, AbstractValue::with_path_fact(fact));
}
}
}
SsaOp::Source | SsaOp::CatchParam | SsaOp::Param { .. } => {
}
_ => {}
}
}
fn is_int_producing_callee(callee: &str) -> bool {
crate::ssa::type_facts::is_int_producing_callee(callee)
}
fn has_typeprefix_upper_scoped(callee: &str) -> bool {
let normalised = crate::ssa::type_facts::peel_identity_suffix(callee);
let normalised = if normalised.is_empty() {
callee
} else {
normalised.as_str()
};
let mut segments: smallvec::SmallVec<[&str; 4]> =
normalised.split("::").filter(|s| !s.is_empty()).collect();
if segments.len() < 2 {
return false;
}
if let Some(leaf) = segments.last_mut() {
if let Some(dot_idx) = leaf.find('.') {
*leaf = &leaf[..dot_idx];
}
}
let parent = segments[segments.len() - 2];
let Some(first) = parent.chars().next() else {
return false;
};
if !first.is_ascii_uppercase() {
return false;
}
parent
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
fn has_source_label_on_node(info: &NodeInfo) -> bool {
info.taint
.labels
.iter()
.any(|l| matches!(l, DataLabel::Source(_)))
}
fn is_string_const(text: &str) -> bool {
(text.starts_with('"') && text.ends_with('"') && text.len() >= 2)
|| (text.starts_with('\'') && text.ends_with('\'') && text.len() >= 2)
}
fn strip_string_quotes(text: &str) -> String {
if text.len() >= 2
&& ((text.starts_with('"') && text.ends_with('"'))
|| (text.starts_with('\'') && text.ends_with('\'')))
{
text[1..text.len() - 1].to_string()
} else {
text.to_string()
}
}
fn collect_block_events(
block: &SsaBlock,
cfg: &Cfg,
ssa: &SsaBody,
transfer: &SsaTaintTransfer,
mut state: SsaTaintState,
events: &mut Vec<SsaTaintEvent>,
induction_vars: &HashSet<SsaValue>,
pred_states: Option<&PredStates>,
) {
let block_idx = block.id.0 as usize;
for phi in &block.phis {
if let SsaOp::Phi(ref operands) = phi.op {
let is_induction = induction_vars.contains(&phi.value);
let mut combined_caps = Cap::empty();
let mut combined_origins: SmallVec<[TaintOrigin; 2]> = SmallVec::new();
let mut all_tainted_validated = true;
let mut any_tainted = false;
for &(pred_blk, operand_val) in operands {
if is_induction && pred_blk.0 >= block.id.0 {
continue;
}
let operand_taint = if let Some(ps) = pred_states {
ps.get(&(block_idx, pred_blk.0 as usize))
.and_then(|pred_st| pred_st.get(operand_val))
} else {
None
};
let operand_taint = operand_taint.or_else(|| state.get(operand_val));
if let Some(taint) = operand_taint {
any_tainted = true;
combined_caps |= taint.caps;
for orig in &taint.origins {
push_origin_bounded(&mut combined_origins, *orig);
}
if let Some(ps) = pred_states {
if let Some(pred_st) = ps.get(&(block_idx, pred_blk.0 as usize)) {
let var_name = ssa
.value_defs
.get(operand_val.0 as usize)
.and_then(|vd| vd.var_name.as_deref());
if let Some(name) = var_name {
if let Some(sym) = transfer.interner.get(name) {
if !pred_st.validated_must.contains(sym) {
all_tainted_validated = false;
}
} else {
all_tainted_validated = false;
}
} else {
all_tainted_validated = false;
}
} else {
all_tainted_validated = false;
}
} else {
all_tainted_validated = false;
}
}
}
if combined_caps.is_empty() {
state.remove(phi.value);
} else {
state.set(
phi.value,
VarTaint {
caps: combined_caps,
origins: combined_origins,
uses_summary: false,
},
);
if any_tainted && all_tainted_validated {
if let Some(name) = ssa
.value_defs
.get(phi.value.0 as usize)
.and_then(|vd| vd.var_name.as_deref())
{
if let Some(sym) = transfer.interner.get(name) {
state.validated_may.insert(sym);
state.validated_must.insert(sym);
}
}
}
}
}
}
if state.abstract_state.is_some() {
for phi in &block.phis {
if let SsaOp::Phi(ref operands) = phi.op {
use crate::abstract_interp::AbstractValue;
let is_induction = induction_vars.contains(&phi.value);
let mut joined = AbstractValue::bottom();
let mut any_operand = false;
for &(pred_blk, operand_val) in operands {
if is_induction && pred_blk.0 >= block.id.0 {
continue;
}
if let Some(ps) = pred_states {
if let Some(pred_st) = ps.get(&(block_idx, pred_blk.0 as usize)) {
if pred_st.path_env.as_ref().is_some_and(|e| e.is_unsat()) {
continue;
}
}
}
let pred_abs = pred_states
.and_then(|ps| ps.get(&(block_idx, pred_blk.0 as usize)))
.and_then(|s| s.abstract_state.as_ref())
.map(|a| a.get(operand_val))
.unwrap_or_else(AbstractValue::top);
joined = joined.join(&pred_abs);
any_operand = true;
}
if any_operand {
if let Some(ref mut abs) = state.abstract_state {
abs.set(phi.value, joined);
}
}
}
}
}
for inst in &block.body {
transfer_inst(inst, cfg, ssa, transfer, &mut state);
let info = &cfg[inst.cfg_node];
if info.all_args_literal {
continue;
}
if info.parameterized_query {
continue;
}
let sink_info = resolve_sink_info(info, transfer);
let mut sink_caps = sink_info.caps;
if sink_caps.is_empty() {
if let SsaOp::Call {
callee,
receiver: Some(rv),
..
} = &inst.op
{
if transfer.type_facts.is_some() || state.path_env.is_some() {
let tq_labels = resolve_type_qualified_labels(
callee,
*rv,
transfer.type_facts,
state.path_env.as_ref(),
transfer.lang,
transfer.extra_labels,
Some(ssa),
);
for lbl in &tq_labels {
if let DataLabel::Sink(bits) = lbl {
sink_caps |= *bits;
}
}
}
}
}
if sink_caps.is_empty() {
if let SsaOp::Call {
callee,
receiver: Some(rv),
..
} = &inst.op
{
if let Some(aliases) = transfer.module_aliases {
if let Some(module_names) = aliases.get(rv) {
if let Some(dot_pos) = callee.find('.') {
let method = &callee[dot_pos + 1..];
let lang_str = transfer.lang.as_str();
for module_name in module_names {
let qualified = format!("{}.{}", module_name, method);
let labels = crate::labels::classify_all(
lang_str,
&qualified,
transfer.extra_labels,
);
for lbl in &labels {
if let DataLabel::Sink(bits) = lbl {
sink_caps |= *bits;
}
}
}
}
}
}
}
}
if sink_caps.is_empty() {
if let SsaOp::Call { callee, .. } = &inst.op {
let caller_func = info.ast.enclosing_func.as_deref().unwrap_or("");
if let Some(resolved) = resolve_callee_hinted(
transfer,
callee,
caller_func,
info.call.call_ordinal,
Some(info.call.arg_uses.len()),
) {
for &(cb_idx, src_caps) in &resolved.source_to_callback {
let cb_name = info.arg_callees.get(cb_idx).and_then(|ac| ac.as_ref());
if let Some(cb_callee) = cb_name {
if let Some(cb_resolved) =
resolve_callee(transfer, cb_callee, caller_func, 0)
{
let matching_sink_caps = cb_resolved
.param_to_sink
.iter()
.filter(|(_, caps)| !(src_caps & *caps).is_empty())
.fold(Cap::empty(), |acc, (_, c)| acc | *c);
if !matching_sink_caps.is_empty() {
let source_kind =
crate::labels::infer_source_kind(src_caps, callee);
let origin = TaintOrigin {
node: inst.cfg_node,
source_kind,
source_span: None,
};
let cb_tainted: Vec<(
SsaValue,
Cap,
SmallVec<[TaintOrigin; 2]>,
)> = vec![(
inst.value,
src_caps & matching_sink_caps,
SmallVec::from_elem(origin, 1),
)];
let cb_sites = pick_primary_sink_sites_from_resolved(
matching_sink_caps,
&cb_resolved.param_to_sink_sites,
);
emit_ssa_taint_events(
events,
inst.cfg_node,
cb_tainted,
matching_sink_caps,
false,
None,
true,
cb_sites,
);
}
}
}
}
}
}
continue;
}
if let Some(ref env) = state.path_env {
if let SsaOp::Call {
receiver: Some(rv), ..
} = &inst.op
{
if let Some(kind) = env.get(*rv).types.as_singleton() {
sink_caps &= !receiver_incompatible_sink_caps(&kind, sink_caps);
}
}
}
if sink_caps.is_empty() {
continue;
}
if transfer.lang == Lang::Go {
if let Some(ref env) = state.path_env {
if let SsaOp::Call { args, .. } = &inst.op {
if let Some(first_arg_vals) = args.first() {
if let Some(&first_val) = first_arg_vals.first() {
if let Some(kind) = env.get(first_val).types.as_singleton() {
if crate::ssa::type_facts::GoInterfaceTable::definitely_not(
&kind,
"http.ResponseWriter",
) && sink_caps.intersects(Cap::HTML_ESCAPE)
{
sink_caps &= !Cap::HTML_ESCAPE;
}
}
}
}
}
}
}
if sink_caps.is_empty() {
continue;
}
let same_node_sanitizer_caps = info.taint.labels.iter().fold(Cap::empty(), |acc, lbl| {
if let DataLabel::Sanitizer(caps) = lbl {
acc | *caps
} else {
acc
}
});
if !same_node_sanitizer_caps.is_empty() {
sink_caps &= !same_node_sanitizer_caps;
if sink_caps.is_empty() {
continue;
}
}
if let SsaOp::Call { callee, .. } = &inst.op {
sink_caps = suppress_known_safe_callees(sink_caps, callee, transfer.lang);
if sink_caps.is_empty() {
continue;
}
}
for maybe_callee in &info.arg_callees {
if let Some(inner_callee) = maybe_callee {
let caller_func = info.ast.enclosing_func.as_deref().unwrap_or("");
if let Some(resolved) = resolve_callee(transfer, inner_callee, caller_func, 0) {
sink_caps &= !resolved.sanitizer_caps;
} else {
let lang_str = transfer.lang.as_str();
let labels =
crate::labels::classify_all(lang_str, inner_callee, transfer.extra_labels);
for lbl in &labels {
if let DataLabel::Sanitizer(bits) = lbl {
sink_caps &= !*bits;
}
}
}
}
}
if sink_caps.is_empty() {
continue;
}
if !matches!(inst.op, SsaOp::Call { .. }) {
if let Some(const_values) = transfer.const_values {
if all_args_const(inst, const_values) {
continue;
}
}
}
if !matches!(inst.op, SsaOp::Call { .. }) {
if let Some(type_facts) = transfer.type_facts {
if is_type_safe_for_sink(inst, sink_caps, type_facts) {
continue;
}
}
}
if !matches!(inst.op, SsaOp::Call { .. }) {
if let Some(ref env) = state.path_env {
if is_path_type_safe_for_sink(inst, sink_caps, env) {
continue;
}
}
}
if let Some(ref abs) = state.abstract_state {
if is_abstract_safe_for_sink(
inst,
sink_caps,
abs,
transfer.type_facts,
transfer.static_map,
&state,
ssa,
cfg,
) {
continue;
}
}
if let SsaOp::Call { ref args, .. } = inst.op {
if let Some(ref abs) = state.abstract_state {
if is_call_abstract_safe(
inst,
args,
sink_caps,
abs,
transfer.type_facts,
transfer.static_map,
&state,
ssa,
cfg,
) {
continue;
}
}
}
let tainted = collect_tainted_sink_values(
inst,
info,
&state,
sink_caps,
ssa,
transfer,
&sink_info.param_to_sink,
);
if !tainted.is_empty() {
let all_validated = tainted.iter().all(|(val, _, _)| {
let var_name = ssa
.value_defs
.get(val.0 as usize)
.and_then(|vd| vd.var_name.as_deref());
if let Some(name) = var_name {
if let Some(sym) = transfer.interner.get(name) {
return state.validated_may.contains(sym);
}
}
false
});
let guard_kind = if all_validated {
Some(PredicateKind::ValidationCall)
} else {
None
};
let any_uses_summary = tainted
.iter()
.any(|(val, _, _)| state.get(*val).is_some_and(|t| t.uses_summary));
let primary_sites =
pick_primary_sink_sites(inst, &tainted, sink_caps, &sink_info.param_to_sink_sites);
emit_ssa_taint_events(
events,
inst.cfg_node,
tainted,
sink_caps,
all_validated,
guard_kind,
any_uses_summary,
primary_sites,
);
}
}
}
fn pick_primary_sink_sites(
inst: &SsaInst,
tainted: &[(SsaValue, Cap, SmallVec<[TaintOrigin; 2]>)],
sink_caps: Cap,
param_to_sink_sites: &[(usize, SmallVec<[SinkSite; 1]>)],
) -> Vec<SinkSite> {
if param_to_sink_sites.is_empty() || tainted.is_empty() {
return Vec::new();
}
let SsaOp::Call { ref args, .. } = inst.op else {
return Vec::new();
};
let mut out: Vec<SinkSite> = Vec::new();
let mut seen: HashSet<(String, u32, u32, u16)> = HashSet::new();
for (param_idx, sites) in param_to_sink_sites {
let Some(arg_vals) = args.get(*param_idx) else {
continue;
};
let carries_tainted = arg_vals
.iter()
.any(|v| tainted.iter().any(|(tv, _, _)| tv == v));
if !carries_tainted {
continue;
}
for site in sites {
if site.line == 0 {
continue;
}
if (site.cap & sink_caps).is_empty() {
continue;
}
let key = (site.file_rel.clone(), site.line, site.col, site.cap.bits());
if seen.insert(key) {
out.push(site.clone());
}
}
}
out
}
fn pick_primary_sink_sites_from_resolved(
sink_caps: Cap,
param_to_sink_sites: &[(usize, SmallVec<[SinkSite; 1]>)],
) -> Vec<SinkSite> {
if param_to_sink_sites.is_empty() {
return Vec::new();
}
let mut out: Vec<SinkSite> = Vec::new();
let mut seen: HashSet<(String, u32, u32, u16)> = HashSet::new();
for (_, sites) in param_to_sink_sites {
for site in sites {
if site.line == 0 {
continue;
}
if (site.cap & sink_caps).is_empty() {
continue;
}
let key = (site.file_rel.clone(), site.line, site.col, site.cap.bits());
if seen.insert(key) {
out.push(site.clone());
}
}
}
out
}
fn emit_ssa_taint_events(
events: &mut Vec<SsaTaintEvent>,
sink_node: NodeIndex,
tainted_values: Vec<(SsaValue, Cap, SmallVec<[TaintOrigin; 2]>)>,
sink_caps: Cap,
all_validated: bool,
guard_kind: Option<PredicateKind>,
uses_summary: bool,
primary_sites: Vec<SinkSite>,
) {
debug_assert!(
primary_sites
.iter()
.all(|s| s.line != 0 && !(s.cap & sink_caps).is_empty()),
"primary_sites must all carry resolved coordinates and cap ∩ sink_caps ≠ ∅",
);
if primary_sites.is_empty() {
events.push(SsaTaintEvent {
sink_node,
tainted_values,
sink_caps,
all_validated,
guard_kind,
uses_summary,
primary_sink_site: None,
});
return;
}
for site in primary_sites {
events.push(SsaTaintEvent {
sink_node,
tainted_values: tainted_values.clone(),
sink_caps,
all_validated,
guard_kind,
uses_summary,
primary_sink_site: Some(site),
});
}
}
fn collect_args_taint(
args: &[SmallVec<[SsaValue; 2]>],
receiver: &Option<SsaValue>,
state: &SsaTaintState,
propagating_params: &[usize],
) -> (Cap, SmallVec<[TaintOrigin; 2]>) {
let mut combined_caps = Cap::empty();
let mut combined_origins: SmallVec<[TaintOrigin; 2]> = SmallVec::new();
if propagating_params.is_empty() {
if let Some(rv) = receiver {
if let Some(taint) = state.get(*rv) {
combined_caps |= taint.caps;
for orig in &taint.origins {
push_origin_bounded(&mut combined_origins, *orig);
}
}
}
for arg_vals in args {
for &v in arg_vals {
if let Some(taint) = state.get(v) {
combined_caps |= taint.caps;
for orig in &taint.origins {
push_origin_bounded(&mut combined_origins, *orig);
}
}
}
}
} else {
for ¶m_idx in propagating_params {
if let Some(arg_vals) = args.get(param_idx) {
for &v in arg_vals {
if let Some(taint) = state.get(v) {
combined_caps |= taint.caps;
for orig in &taint.origins {
push_origin_bounded(&mut combined_origins, *orig);
}
}
}
}
}
}
(combined_caps, combined_origins)
}
fn strip_cap_from_call_args(
args: &[SmallVec<[SsaValue; 2]>],
receiver: &Option<SsaValue>,
state: &mut SsaTaintState,
cap: Cap,
) {
let mut targets: SmallVec<[SsaValue; 8]> = SmallVec::new();
if let Some(rv) = receiver {
targets.push(*rv);
}
for arg_vals in args {
for &v in arg_vals {
targets.push(v);
}
}
for v in targets {
if let Some(current) = state.get(v) {
if !current.caps.contains(cap) {
continue;
}
let mut updated = current.clone();
updated.caps &= !cap;
state.set(v, updated);
}
}
}
fn try_curl_url_propagation(
inst: &SsaInst,
info: &NodeInfo,
args: &[SmallVec<[SsaValue; 2]>],
state: &mut SsaTaintState,
) -> bool {
if info.taint.defines.is_some() {
return false;
}
let callee = match info.call.callee.as_deref() {
Some(c) if c.ends_with("curl_easy_setopt") => c,
_ => return false,
};
if !info.taint.uses.iter().any(|u| u == "CURLOPT_URL") {
return false;
}
let handle_val = args.first().and_then(|a| a.first().copied());
let handle_val = match handle_val {
Some(v) => v,
None => return false,
};
let mut url_caps = Cap::empty();
let mut url_origins: SmallVec<[TaintOrigin; 2]> = SmallVec::new();
for arg_vals in args.iter().skip(1) {
for &v in arg_vals {
if let Some(taint) = state.get(v) {
url_caps |= taint.caps;
for orig in &taint.origins {
push_origin_bounded(&mut url_origins, *orig);
}
}
}
}
if url_caps.is_empty() {
let used = inst_use_values(inst);
for v in used {
if v == handle_val {
continue;
}
if let Some(taint) = state.get(v) {
url_caps |= taint.caps;
for orig in &taint.origins {
push_origin_bounded(&mut url_origins, *orig);
}
}
}
}
if url_caps.is_empty() {
return false;
}
match state.get(handle_val) {
Some(existing) => {
let mut merged = existing.clone();
merged.caps |= url_caps;
for orig in &url_origins {
push_origin_bounded(&mut merged.origins, *orig);
}
state.set(handle_val, merged);
}
None => {
state.set(
handle_val,
VarTaint {
caps: url_caps,
origins: url_origins,
uses_summary: false,
},
);
}
}
let _ = callee;
true
}
fn resolve_container_index(index_val: SsaValue, transfer: &SsaTaintTransfer) -> HeapSlot {
use crate::ssa::heap::MAX_TRACKED_INDICES;
if let Some(cv) = transfer.const_values {
if let Some(crate::ssa::const_prop::ConstLattice::Int(n)) = cv.get(&index_val) {
if *n >= 0 && (*n as u64) < MAX_TRACKED_INDICES as u64 {
return HeapSlot::Index(*n as u64);
}
}
}
HeapSlot::Elements
}
fn resolve_op_slot(
index_arg: Option<usize>,
arg_offset: usize,
args: &[SmallVec<[SsaValue; 2]>],
transfer: &SsaTaintTransfer,
) -> HeapSlot {
if let Some(idx_pos) = index_arg {
let effective = idx_pos + arg_offset;
if let Some(arg_vals) = args.get(effective) {
if let Some(&v) = arg_vals.first() {
return resolve_container_index(v, transfer);
}
}
}
HeapSlot::Elements
}
fn try_container_propagation(
inst: &SsaInst,
_info: &NodeInfo,
args: &[SmallVec<[SsaValue; 2]>],
receiver: &Option<SsaValue>,
state: &mut SsaTaintState,
transfer: &SsaTaintTransfer,
callee: &str,
ssa: &SsaBody,
) -> bool {
let lang = transfer.lang;
use crate::ssa::pointsto::{ContainerOp, classify_container_op};
let op = match classify_container_op(callee, lang) {
Some(op) => op,
None => return false,
};
let resolve_container = |recv: &Option<SsaValue>| -> Option<SsaValue> {
if let Some(v) = *recv {
return Some(v);
}
if lang == Lang::Go {
return args.first().and_then(|a| a.first().copied());
}
let dot_pos = callee.rfind('.')?;
let receiver_name = &callee[..dot_pos];
for arg_group in args {
for &v in arg_group {
if let Some(def) = ssa.value_defs.get(v.0 as usize) {
if def.var_name.as_deref() == Some(receiver_name) {
return Some(v);
}
}
}
}
None
};
match op {
ContainerOp::Store {
value_args,
index_arg,
} => {
let container_val = match resolve_container(receiver) {
Some(v) => v,
None => return false,
};
let arg_offset = if lang == Lang::Go && receiver.is_none() {
1usize
} else {
0
};
let slot = resolve_op_slot(index_arg, arg_offset, args, transfer);
let mut val_caps = Cap::empty();
let mut val_origins: SmallVec<[TaintOrigin; 2]> = SmallVec::new();
for &arg_idx in &value_args {
let effective_idx = arg_idx + arg_offset;
if let Some(arg_vals) = args.get(effective_idx) {
for &v in arg_vals {
if let Some(taint) = state.get(v) {
val_caps |= taint.caps;
for orig in &taint.origins {
push_origin_bounded(&mut val_origins, *orig);
}
}
}
}
}
if val_caps.is_empty() {
return true; }
if let Some(pts) = lookup_pts(transfer, container_val) {
state.heap.store_set(&pts, slot, val_caps, &val_origins);
if lang == Lang::Go && receiver.is_none() {
if let Some(ht) = state.heap.load_set(&pts, HeapSlot::Elements) {
state.set(
inst.value,
VarTaint {
caps: ht.caps,
origins: ht.origins,
uses_summary: false,
},
);
}
}
return true;
}
merge_taint_into(state, container_val, val_caps, &val_origins);
if lang == Lang::Go && receiver.is_none() {
if let Some(merged) = state.get(container_val) {
state.set(inst.value, merged.clone());
}
}
true
}
ContainerOp::Load { index_arg } => {
let container_val = match resolve_container(receiver) {
Some(v) => v,
None => return false,
};
let arg_offset = if lang == Lang::Go && receiver.is_none() {
1usize
} else {
0
};
let slot = resolve_op_slot(index_arg, arg_offset, args, transfer);
if let Some(pts) = lookup_pts(transfer, container_val) {
if let Some(ht) = state.heap.load_set(&pts, slot) {
state.set(
inst.value,
VarTaint {
caps: ht.caps,
origins: ht.origins,
uses_summary: false,
},
);
}
return true;
}
if let Some(taint) = state.get(container_val) {
state.set(inst.value, taint.clone());
}
true
}
ContainerOp::Writeback { dest_arg } => {
let chain_shape = {
let dot_pos = callee.rfind('.');
match dot_pos {
Some(p) => callee[..p].contains('('),
None => false,
}
};
let recv_val = if let Some(v) = *receiver {
Some(v)
} else if !chain_shape && let Some(dot_pos) = callee.rfind('.') {
let recv_name = &callee[..dot_pos];
let mut found = None;
'outer: for arg_group in args {
for &v in arg_group {
if let Some(def) = ssa.value_defs.get(v.0 as usize) {
if def.var_name.as_deref() == Some(recv_name) {
found = Some(v);
break 'outer;
}
}
}
}
if found.is_none() {
for (idx, def) in ssa.value_defs.iter().enumerate() {
if def.var_name.as_deref() == Some(recv_name) {
found = Some(SsaValue(idx as u32));
break;
}
}
}
found
} else {
None
};
let recv_taint = if let Some(v) = recv_val {
if let Some(t) = state.get(v) {
t.clone()
} else if chain_shape {
let mut caps = Cap::empty();
let mut origins: SmallVec<[TaintOrigin; 2]> = SmallVec::new();
for (idx, arg_group) in args.iter().enumerate() {
if idx == dest_arg {
continue;
}
for &v in arg_group {
if let Some(t) = state.get(v) {
caps |= t.caps;
for orig in &t.origins {
push_origin_bounded(&mut origins, *orig);
}
}
}
}
if caps.is_empty() {
return true;
}
VarTaint {
caps,
origins,
uses_summary: false,
}
} else {
return true; }
} else if chain_shape {
let mut caps = Cap::empty();
let mut origins: SmallVec<[TaintOrigin; 2]> = SmallVec::new();
for (idx, arg_group) in args.iter().enumerate() {
if idx == dest_arg {
continue;
}
for &v in arg_group {
if let Some(t) = state.get(v) {
caps |= t.caps;
for orig in &t.origins {
push_origin_bounded(&mut origins, *orig);
}
}
}
}
if caps.is_empty() {
return true;
}
VarTaint {
caps,
origins,
uses_summary: false,
}
} else {
if std::env::var("NYX_DEBUG_WRITEBACK").is_ok() {
eprintln!(" writeback: no receiver SSA value for callee {callee:?}");
}
return false;
};
if let Some(arg_vals) = args.get(dest_arg) {
for &v in arg_vals {
merge_taint_into(state, v, recv_taint.caps, &recv_taint.origins);
if let Some(pts) = lookup_pts(transfer, v) {
state.heap.store_set(
&pts,
crate::ssa::heap::HeapSlot::Elements,
recv_taint.caps,
&recv_taint.origins,
);
}
if let Some(pf) = transfer.pointer_facts {
let pt_arg = pf.pt(v);
if !pt_arg.is_empty() && !pt_arg.is_top() {
let cell_taint = recv_taint.clone();
for loc in pt_arg.iter() {
let key = crate::taint::ssa_transfer::state::FieldTaintKey {
loc,
field: crate::ssa::ir::FieldId::ANY_FIELD,
};
state.add_field(key, cell_taint.clone(), false, false);
}
}
}
}
}
true
}
}
}
fn find_container_receiver(
callee: &str,
receiver: &Option<SsaValue>,
args: &[SmallVec<[SsaValue; 2]>],
ssa: &SsaBody,
lang: Lang,
) -> Option<SsaValue> {
if let Some(v) = *receiver {
return Some(v);
}
if lang == Lang::Go {
return args.first().and_then(|a| a.first().copied());
}
let dot_pos = callee.rfind('.')?;
let receiver_name = &callee[..dot_pos];
for arg_group in args {
for &v in arg_group {
if let Some(def) = ssa.value_defs.get(v.0 as usize) {
if def.var_name.as_deref() == Some(receiver_name) {
return Some(v);
}
}
}
}
None
}
fn lookup_pts(transfer: &SsaTaintTransfer, v: SsaValue) -> Option<PointsToSet> {
if let Some(pts_result) = transfer.points_to {
if let Some(pts) = pts_result.get(v) {
return Some(pts.clone());
}
}
if let Some(dyn_ref) = transfer.dynamic_pts {
if let Some(pts) = dyn_ref.borrow().get(&v) {
return Some(pts.clone());
}
}
None
}
fn merge_taint_into(
state: &mut SsaTaintState,
target: SsaValue,
caps: Cap,
origins: &SmallVec<[TaintOrigin; 2]>,
) {
match state.get(target) {
Some(existing) => {
let mut merged = existing.clone();
merged.caps |= caps;
for orig in origins {
push_origin_bounded(&mut merged.origins, *orig);
}
state.set(target, merged);
}
None => {
state.set(
target,
VarTaint {
caps,
origins: origins.clone(),
uses_summary: false,
},
);
}
}
}
struct SinkInfo {
caps: Cap,
param_to_sink: Vec<(usize, Cap)>,
param_to_sink_sites: Vec<(usize, SmallVec<[SinkSite; 1]>)>,
}
fn resolve_sink_info(info: &NodeInfo, transfer: &SsaTaintTransfer) -> SinkInfo {
let label_sink_caps = info.taint.labels.iter().fold(Cap::empty(), |acc, lbl| {
if let DataLabel::Sink(caps) = lbl {
acc | *caps
} else {
acc
}
});
if !label_sink_caps.is_empty() {
return SinkInfo {
caps: label_sink_caps,
param_to_sink: vec![],
param_to_sink_sites: vec![],
};
}
let caller_func = info.ast.enclosing_func.as_deref().unwrap_or("");
let arity_hint = if info.call.arg_uses.is_empty() {
None
} else {
Some(info.call.arg_uses.len())
};
let primary = info.call.callee.as_ref().and_then(|c| {
resolve_callee_hinted(transfer, c, caller_func, info.call.call_ordinal, arity_hint)
});
if let Some(r) = primary.filter(|r| !r.sink_caps.is_empty()) {
return SinkInfo {
caps: r.sink_caps,
param_to_sink: r.param_to_sink,
param_to_sink_sites: r.param_to_sink_sites,
};
}
if let Some(oc) = info.call.outer_callee.as_ref() {
if let Some(r) = resolve_callee_hinted(
transfer,
oc,
caller_func,
info.call.call_ordinal,
arity_hint,
)
.filter(|r| !r.sink_caps.is_empty())
{
return SinkInfo {
caps: r.sink_caps,
param_to_sink: r.param_to_sink,
param_to_sink_sites: r.param_to_sink_sites,
};
}
}
SinkInfo {
caps: Cap::empty(),
param_to_sink: vec![],
param_to_sink_sites: vec![],
}
}
fn collect_tainted_sink_values(
inst: &SsaInst,
info: &NodeInfo,
state: &SsaTaintState,
sink_caps: Cap,
ssa: &SsaBody,
transfer: &SsaTaintTransfer,
param_to_sink: &[(usize, Cap)],
) -> Vec<(SsaValue, Cap, SmallVec<[TaintOrigin; 2]>)> {
let mut result = Vec::new();
let check_heap_taint =
|v: SsaValue, result: &mut Vec<(SsaValue, Cap, SmallVec<[TaintOrigin; 2]>)>| {
if let Some(pts) = lookup_pts(transfer, v) {
if let Some(ht) = state.heap.load_set(&pts, HeapSlot::Elements) {
let effective = ht.caps & sink_caps;
if !effective.is_empty() && !result.iter().any(|&(rv, _, _)| rv == v) {
result.push((v, ht.caps, ht.origins));
}
}
}
};
let used_values = inst_use_values(inst);
if let Some(ref positions) = info.call.sink_payload_args {
if let SsaOp::Call { args, .. } = &inst.op {
let destination_filter = info.call.destination_uses.as_deref();
for &pos in positions {
if let Some(arg_vals) = args.get(pos) {
for &v in arg_vals {
if let Some(names) = destination_filter {
let var_name = ssa.def_of(v).var_name.as_deref();
let matches = var_name.is_some_and(|vn| names.iter().any(|n| n == vn));
if !matches {
continue;
}
}
if let Some(taint) = state.get(v) {
if (taint.caps & sink_caps) != Cap::empty() {
result.push((v, taint.caps, taint.origins.clone()));
}
}
check_heap_taint(v, &mut result);
}
}
}
apply_field_aware_suppression(&mut result, inst, info, state, sink_caps, ssa);
return result;
}
}
if !param_to_sink.is_empty() {
if let SsaOp::Call { args, .. } = &inst.op {
for &(param_idx, per_param_caps) in param_to_sink {
let effective_caps = per_param_caps & sink_caps;
if effective_caps.is_empty() {
continue;
}
if let Some(arg_vals) = args.get(param_idx) {
for &v in arg_vals {
if let Some(taint) = state.get(v) {
if (taint.caps & effective_caps) != Cap::empty()
&& !result.iter().any(|&(rv, _, _)| rv == v)
{
result.push((v, taint.caps, taint.origins.clone()));
}
}
check_heap_taint(v, &mut result);
}
}
}
apply_field_aware_suppression(&mut result, inst, info, state, sink_caps, ssa);
return result;
}
}
for v in used_values {
if let Some(taint) = state.get(v) {
if (taint.caps & sink_caps) != Cap::empty() {
result.push((v, taint.caps, taint.origins.clone()));
}
}
check_heap_taint(v, &mut result);
}
apply_field_aware_suppression(&mut result, inst, info, state, sink_caps, ssa);
result
}
fn apply_field_aware_suppression(
result: &mut Vec<(SsaValue, Cap, SmallVec<[TaintOrigin; 2]>)>,
inst: &SsaInst,
info: &NodeInfo,
state: &SsaTaintState,
sink_caps: Cap,
ssa: &SsaBody,
) {
if result.is_empty() {
return;
}
let all_used = inst_use_values(inst);
result.retain(|(v, _, _)| {
let Some(base) = ssa.def_of(*v).var_name.as_deref() else {
return true;
};
if base.contains('.') {
return true;
}
let prefix = format!("{}.", base);
let callee_name = match &inst.op {
SsaOp::Call { callee, .. } => Some(callee.as_str()),
_ => None,
};
let field_values: SmallVec<[SsaValue; 4]> = all_used
.iter()
.copied()
.filter(|&u| {
if u == *v {
return false;
}
let uname = match ssa.def_of(u).var_name.as_deref() {
Some(n) => n,
None => return false,
};
if !uname.starts_with(&prefix) {
return false;
}
if callee_name.is_some_and(|cn| uname == cn) {
return false;
}
if is_likely_method_expression(uname) {
return false;
}
if is_phantom_param_value(u, ssa)
&& info.arg_callees.iter().any(|c| c.as_deref() == Some(uname))
{
return false;
}
true
})
.collect();
let all_fields_clean = !field_values.is_empty()
&& field_values.iter().all(|&u| match state.get(u) {
None => true,
Some(t) => (t.caps & sink_caps).is_empty(),
});
!all_fields_clean
});
}
fn is_phantom_param_value(v: SsaValue, ssa: &SsaBody) -> bool {
let def = ssa.def_of(v);
let block = &ssa.blocks[def.block.0 as usize];
block
.phis
.iter()
.chain(block.body.iter())
.find(|inst| inst.value == v)
.is_some_and(|inst| matches!(inst.op, SsaOp::Param { .. } | SsaOp::SelfParam))
}
fn is_likely_method_expression(name: &str) -> bool {
let suffix = name.rsplit('.').next().unwrap_or(name);
matches!(
suffix,
"push"
| "pop"
| "shift"
| "unshift"
| "join"
| "split"
| "concat"
| "slice"
| "splice"
| "map"
| "filter"
| "reduce"
| "forEach"
| "find"
| "some"
| "every"
| "get"
| "set"
| "has"
| "delete"
| "add"
| "remove"
| "clear"
| "keys"
| "values"
| "entries"
| "toString"
| "valueOf"
| "send"
| "write"
| "end"
| "render"
| "redirect"
| "append"
| "extend"
| "insert"
| "update"
| "items"
| "call"
| "apply"
| "bind"
| "then"
| "catch"
| "trim"
| "replace"
| "match"
| "search"
| "test"
| "log"
| "warn"
| "error"
| "info"
| "debug"
| "execute"
| "query"
| "fetch"
| "request"
)
}
fn inst_use_values(inst: &SsaInst) -> Vec<SsaValue> {
match &inst.op {
SsaOp::Phi(operands) => operands.iter().map(|(_, v)| *v).collect(),
SsaOp::Assign(uses) => uses.to_vec(),
SsaOp::Call { args, receiver, .. } => {
let mut vals = Vec::new();
if let Some(rv) = receiver {
vals.push(*rv);
}
for arg in args {
vals.extend(arg.iter());
}
vals
}
SsaOp::FieldProj { receiver, .. } => vec![*receiver],
SsaOp::Source
| SsaOp::Const(_)
| SsaOp::Param { .. }
| SsaOp::SelfParam
| SsaOp::CatchParam
| SsaOp::Nop
| SsaOp::Undef => Vec::new(),
}
}
fn propagate_sanitization_to_aliases(
inst: &SsaInst,
state: &mut SsaTaintState,
sanitizer_bits: Cap,
aliases: &crate::ssa::alias::BaseAliasResult,
ssa: &SsaBody,
) {
let var_name = match inst.var_name.as_deref() {
Some(n) => n,
None => return,
};
let (base, suffix) = match var_name.find('.') {
Some(pos) => (&var_name[..pos], &var_name[pos..]),
None => (var_name, ""),
};
let alias_bases = match aliases.aliases_of(base) {
Some(bases) => bases,
None => return,
};
let to_sanitize: SmallVec<[SsaValue; 8]> = state
.values
.iter()
.filter_map(|&(v, ref t)| {
if t.caps.is_empty() {
return None;
}
let vdef_name = ssa.value_defs.get(v.0 as usize)?.var_name.as_deref()?;
for alias_base in alias_bases {
if alias_base == base {
continue; }
let target = if suffix.is_empty() {
alias_base.as_str()
} else {
""
};
if suffix.is_empty() {
if vdef_name == target {
return Some(v);
}
} else {
if vdef_name.len() == alias_base.len() + suffix.len()
&& vdef_name.starts_with(alias_base.as_str())
&& vdef_name.ends_with(suffix)
{
return Some(v);
}
}
}
None
})
.collect();
for v in to_sanitize {
if let Some(taint) = state.get(v) {
let new_caps = taint.caps & !sanitizer_bits;
if new_caps.is_empty() {
state.remove(v);
} else {
state.set(
v,
VarTaint {
caps: new_caps,
origins: taint.origins.clone(),
uses_summary: taint.uses_summary,
},
);
}
}
}
}
fn propagate_taint_to_aliases(
inst: &SsaInst,
state: &mut SsaTaintState,
taint_caps: Cap,
taint_origins: &SmallVec<[TaintOrigin; 2]>,
aliases: &crate::ssa::alias::BaseAliasResult,
ssa: &SsaBody,
) {
let var_name = match inst.var_name.as_deref() {
Some(n) => n,
None => return,
};
let (base, suffix) = match var_name.find('.') {
Some(pos) => (&var_name[..pos], &var_name[pos..]),
None => (var_name, ""),
};
let alias_bases = match aliases.aliases_of(base) {
Some(bases) => bases,
None => return,
};
let to_taint: SmallVec<[SsaValue; 8]> = ssa
.value_defs
.iter()
.enumerate()
.filter_map(|(idx, vdef)| {
let vdef_name = vdef.var_name.as_deref()?;
for alias_base in alias_bases {
if alias_base == base {
continue; }
if suffix.is_empty() {
if vdef_name == alias_base.as_str() {
return Some(SsaValue(idx as u32));
}
} else {
if vdef_name.len() == alias_base.len() + suffix.len()
&& vdef_name.starts_with(alias_base.as_str())
&& vdef_name.ends_with(suffix)
{
return Some(SsaValue(idx as u32));
}
}
}
None
})
.collect();
for v in to_taint {
if let Some(existing) = state.get(v) {
let merged_caps = existing.caps | taint_caps;
let mut merged_origins = existing.origins.clone();
for orig in taint_origins {
push_origin_bounded(&mut merged_origins, *orig);
}
state.set(
v,
VarTaint {
caps: merged_caps,
origins: merged_origins,
uses_summary: existing.uses_summary,
},
);
} else {
state.set(
v,
VarTaint {
caps: taint_caps,
origins: taint_origins.clone(),
uses_summary: false,
},
);
}
}
}
fn all_args_const(
inst: &SsaInst,
const_values: &HashMap<SsaValue, crate::ssa::const_prop::ConstLattice>,
) -> bool {
let used = inst_use_values(inst);
if used.is_empty() {
return false; }
used.iter().all(|v| {
matches!(
const_values.get(v),
Some(
crate::ssa::const_prop::ConstLattice::Str(_)
| crate::ssa::const_prop::ConstLattice::Int(_)
| crate::ssa::const_prop::ConstLattice::Bool(_)
| crate::ssa::const_prop::ConstLattice::Null
)
)
})
}
fn resolve_type_qualified_labels(
callee: &str,
receiver: SsaValue,
type_facts: Option<&crate::ssa::type_facts::TypeFactResult>,
path_env: Option<&constraint::PathEnv>,
lang: Lang,
extra_labels: Option<&[crate::labels::RuntimeLabelRule]>,
ssa: Option<&SsaBody>,
) -> SmallVec<[DataLabel; 2]> {
let method_candidates = method_candidates_from_chain(callee, lang);
let receiver_candidates = receiver_candidates_for_type_lookup(receiver, ssa, lang);
if let Some(tf) = type_facts {
for rv in &receiver_candidates {
if let Some(receiver_type) = tf.get_type(*rv) {
if let Some(prefix) = receiver_type.label_prefix() {
for method in &method_candidates {
let qualified = format!("{}.{}", prefix, method);
let labels =
crate::labels::classify_all(lang.as_str(), &qualified, extra_labels);
if !labels.is_empty() {
return labels;
}
}
}
}
}
}
if let Some(env) = path_env {
for rv in &receiver_candidates {
let types = env.get(*rv).types;
if let Some(kind) = types.as_singleton() {
if let Some(prefix) = kind.label_prefix() {
for method in &method_candidates {
let qualified = format!("{}.{}", prefix, method);
let labels =
crate::labels::classify_all(lang.as_str(), &qualified, extra_labels);
if !labels.is_empty() {
return labels;
}
}
}
}
}
}
SmallVec::new()
}
fn receiver_candidates_for_type_lookup(
start: SsaValue,
ssa: Option<&SsaBody>,
lang: Lang,
) -> SmallVec<[SsaValue; 4]> {
let mut out: SmallVec<[SsaValue; 4]> = SmallVec::new();
out.push(start);
let Some(body) = ssa else {
return out;
};
let mut current = start;
for _ in 0..8 {
let mut next_receiver: Option<SsaValue> = None;
'scan: for block in &body.blocks {
for inst in block.phis.iter().chain(block.body.iter()) {
if inst.value == current {
match &inst.op {
SsaOp::FieldProj { receiver, .. } => {
next_receiver = Some(*receiver);
}
SsaOp::Call {
receiver: Some(rv), ..
} if matches!(lang, Lang::Rust) => {
next_receiver = Some(*rv);
}
_ => {}
}
break 'scan;
}
}
}
match next_receiver {
Some(rv) if !out.contains(&rv) => {
out.push(rv);
current = rv;
}
_ => break,
}
}
out
}
fn method_candidates_from_chain(callee: &str, lang: Lang) -> SmallVec<[String; 4]> {
let mut out: SmallVec<[String; 4]> = SmallVec::new();
let normalized = crate::labels::normalize_chained_call_for_classify(callee);
let segments: Vec<&str> = normalized.split('.').collect();
if segments.is_empty() {
return out;
}
let mut i = segments.len();
while i > 0 {
let seg = segments[i - 1];
if !seg.is_empty() {
out.push(seg.to_string());
}
if matches!(lang, Lang::Rust) && crate::ssa::type_facts::is_identity_method(seg) {
i -= 1;
continue;
}
break;
}
out
}
fn suppress_known_safe_callees(sink_caps: Cap, callee: &str, lang: Lang) -> Cap {
match lang {
Lang::Java => {
if callee.starts_with("System.out.") || callee.starts_with("System.err.") {
sink_caps & !Cap::HTML_ESCAPE
} else {
sink_caps
}
}
_ => sink_caps,
}
}
fn is_type_safe_for_sink(
inst: &SsaInst,
sink_caps: Cap,
type_facts: &crate::ssa::type_facts::TypeFactResult,
) -> bool {
let used = inst_use_values(inst);
crate::ssa::type_facts::is_type_safe_for_sink(&used, sink_caps, type_facts)
}
fn type_safe_for_taint_sink(kind: &crate::ssa::type_facts::TypeKind, cap: Cap) -> bool {
use crate::ssa::type_facts::TypeKind;
match kind {
TypeKind::Int | TypeKind::Bool => {
cap.intersects(Cap::SQL_QUERY | Cap::FILE_IO | Cap::CODE_EXEC | Cap::SHELL_ESCAPE)
}
_ => false,
}
}
fn receiver_incompatible_sink_caps(kind: &crate::ssa::type_facts::TypeKind, sink_caps: Cap) -> Cap {
use crate::ssa::type_facts::TypeKind;
let mut remove = Cap::empty();
if sink_caps.intersects(Cap::HTML_ESCAPE) {
match kind {
TypeKind::HttpResponse => {} TypeKind::Unknown | TypeKind::Object => {} _ => {
remove |= Cap::HTML_ESCAPE;
}
}
}
if type_safe_for_taint_sink(kind, sink_caps) {
remove |= sink_caps & (Cap::SQL_QUERY | Cap::FILE_IO | Cap::CODE_EXEC);
}
remove
}
fn is_path_type_safe_for_sink(inst: &SsaInst, sink_caps: Cap, env: &constraint::PathEnv) -> bool {
let type_suppressible = Cap::SQL_QUERY | Cap::FILE_IO | Cap::CODE_EXEC;
if !sink_caps.intersects(type_suppressible) {
return false;
}
let used = inst_use_values(inst);
if used.is_empty() {
return false;
}
used.iter().all(|v| match env.get(*v).types.as_singleton() {
Some(ref kind) => type_safe_for_taint_sink(kind, sink_caps),
None => false, })
}
fn is_abstract_safe_for_sink(
inst: &SsaInst,
sink_caps: Cap,
abs: &AbstractState,
type_facts: Option<&crate::ssa::type_facts::TypeFactResult>,
static_map: Option<&crate::ssa::static_map::StaticMapResult>,
state: &SsaTaintState,
ssa: &SsaBody,
cfg: &Cfg,
) -> bool {
let used = inst_use_values(inst);
if used.is_empty() {
return false;
}
if sink_caps.intersects(Cap::SSRF) {
let node_info = &cfg[inst.cfg_node];
if let Some(prefix) = node_info.string_prefix.as_deref() {
let synthetic = crate::abstract_interp::StringFact::from_prefix(prefix);
if is_string_safe_for_ssrf(&synthetic) {
return true;
}
}
if used
.iter()
.all(|v| is_string_safe_for_ssrf(&abs.get(*v).string))
{
return true;
}
}
if sink_caps.intersects(Cap::SHELL_ESCAPE) && is_static_map_shell_safe(&used, static_map) {
return true;
}
if sink_caps.intersects(Cap::HTML_ESCAPE) {
if let Some(tf) = type_facts {
let leaves = trace_tainted_leaf_values(inst, state, ssa, cfg);
if !leaves.is_empty() && leaves.iter().all(|v| tf.is_int(*v)) {
return true;
}
}
}
if sink_caps.intersects(Cap::SQL_QUERY | Cap::FILE_IO | Cap::SHELL_ESCAPE) {
if let Some(tf) = type_facts {
let leaves = trace_tainted_leaf_values(inst, state, ssa, cfg);
if !leaves.is_empty()
&& leaves
.iter()
.all(|v| tf.is_int(*v) && abs.get(*v).interval.is_proven_bounded())
{
return true;
}
}
}
if sink_caps.intersects(Cap::FILE_IO) && is_path_safe_for_sink(inst, state, ssa, cfg, abs) {
return true;
}
false
}
fn is_path_safe_for_sink(
inst: &SsaInst,
state: &SsaTaintState,
ssa: &SsaBody,
cfg: &Cfg,
abs: &AbstractState,
) -> bool {
let leaves = trace_tainted_leaf_values(inst, state, ssa, cfg);
if leaves.is_empty() {
return false;
}
let safe = leaves.iter().all(|v| abs.get(*v).path.is_path_safe());
if safe {
let span = cfg[inst.cfg_node].ast.span;
crate::taint::ssa_transfer::state::record_path_safe_suppressed_span(span);
}
safe
}
fn is_call_abstract_safe(
inst: &SsaInst,
args: &[SmallVec<[SsaValue; 2]>],
sink_caps: Cap,
abs: &AbstractState,
type_facts: Option<&crate::ssa::type_facts::TypeFactResult>,
static_map: Option<&crate::ssa::static_map::StaticMapResult>,
state: &SsaTaintState,
ssa: &SsaBody,
cfg: &Cfg,
) -> bool {
if sink_caps.intersects(Cap::SSRF) {
let node_info = &cfg[inst.cfg_node];
if let Some(prefix) = node_info.string_prefix.as_deref() {
let synthetic = crate::abstract_interp::StringFact::from_prefix(prefix);
if is_string_safe_for_ssrf(&synthetic) {
return true;
}
}
if let Some(first_arg) = args.first() {
if !first_arg.is_empty()
&& first_arg
.iter()
.all(|v| is_string_safe_for_ssrf(&abs.get(*v).string))
{
return true;
}
}
}
if sink_caps.intersects(Cap::SHELL_ESCAPE) && !args.is_empty() {
let all_values: Vec<SsaValue> = args.iter().flat_map(|g| g.iter().copied()).collect();
if !all_values.is_empty() && is_static_map_shell_safe(&all_values, static_map) {
return true;
}
}
if sink_caps.intersects(Cap::HTML_ESCAPE) {
if let Some(tf) = type_facts {
let leaves = trace_tainted_leaf_values(inst, state, ssa, cfg);
if !leaves.is_empty() && leaves.iter().all(|v| tf.is_int(*v)) {
return true;
}
}
}
if sink_caps.intersects(Cap::SQL_QUERY | Cap::FILE_IO | Cap::SHELL_ESCAPE) {
if let Some(tf) = type_facts {
let leaves = trace_tainted_leaf_values(inst, state, ssa, cfg);
if !leaves.is_empty()
&& leaves
.iter()
.all(|v| tf.is_int(*v) && abs.get(*v).interval.is_proven_bounded())
{
return true;
}
}
}
if sink_caps.intersects(Cap::FILE_IO) && is_path_safe_for_sink(inst, state, ssa, cfg, abs) {
return true;
}
false
}
const MAX_TRACE_DEPTH: usize = 8;
fn trace_tainted_leaf_values(
inst: &SsaInst,
state: &SsaTaintState,
ssa: &SsaBody,
cfg: &Cfg,
) -> SmallVec<[SsaValue; 4]> {
let mut leaves = SmallVec::new();
let used = inst_use_values(inst);
for &v in &used {
if state.get(v).is_some() {
trace_single_leaf(v, state, ssa, cfg, &mut leaves, 0);
}
}
leaves
}
fn trace_single_leaf(
v: SsaValue,
state: &SsaTaintState,
ssa: &SsaBody,
cfg: &Cfg,
leaves: &mut SmallVec<[SsaValue; 4]>,
depth: usize,
) {
if depth >= MAX_TRACE_DEPTH || leaves.len() >= 16 {
leaves.push(v);
return;
}
let vd = &ssa.value_defs[v.0 as usize];
let block = &ssa.blocks[vd.block.0 as usize];
let inst = match block.body.iter().find(|i| i.value == v) {
Some(i) => i,
None => {
leaves.push(v);
return;
}
};
if cfg
.node_weight(inst.cfg_node)
.is_some_and(|ni| ni.is_numeric_length_access)
{
leaves.push(v);
return;
}
match &inst.op {
SsaOp::Assign(uses) if uses.len() >= 2 => {
let bin_op = cfg.node_weight(inst.cfg_node).and_then(|ni| ni.bin_op);
let is_numeric_op = matches!(
bin_op,
Some(
crate::cfg::BinOp::Sub
| crate::cfg::BinOp::Mul
| crate::cfg::BinOp::Div
| crate::cfg::BinOp::Mod
| crate::cfg::BinOp::BitAnd
| crate::cfg::BinOp::BitOr
| crate::cfg::BinOp::BitXor
| crate::cfg::BinOp::LeftShift
| crate::cfg::BinOp::RightShift
| crate::cfg::BinOp::Eq
| crate::cfg::BinOp::NotEq
| crate::cfg::BinOp::Lt
| crate::cfg::BinOp::LtEq
| crate::cfg::BinOp::Gt
| crate::cfg::BinOp::GtEq
)
);
if is_numeric_op {
leaves.push(v);
return;
}
let mut found = false;
for &u in uses {
if state.get(u).is_some() {
trace_single_leaf(u, state, ssa, cfg, leaves, depth + 1);
found = true;
}
}
if !found {
leaves.push(v);
}
}
SsaOp::Call { callee, args, .. } if is_stringify_callee(callee) => {
let mut found = false;
for arg in args {
for &u in arg {
if state.get(u).is_some() {
trace_single_leaf(u, state, ssa, cfg, leaves, depth + 1);
found = true;
}
}
}
if !found {
leaves.push(v);
}
}
SsaOp::Call { args, .. } => {
let is_source = cfg
.node_weight(inst.cfg_node)
.map(|ni| {
ni.taint
.labels
.iter()
.any(|l| matches!(l, crate::labels::DataLabel::Source(_)))
})
.unwrap_or(false);
let proves_path_safe = state.abstract_state.as_ref().is_some_and(|abs_state| {
let f = abs_state.get(v).path;
!f.is_top() && f.is_path_safe()
});
if is_source || proves_path_safe {
leaves.push(v);
} else {
let mut found = false;
for arg in args {
for &u in arg {
if state.get(u).is_some() {
trace_single_leaf(u, state, ssa, cfg, leaves, depth + 1);
found = true;
}
}
}
if !found {
leaves.push(v);
}
}
}
SsaOp::Assign(uses) if uses.len() == 1 => {
let u = uses[0];
if state.get(u).is_some() {
trace_single_leaf(u, state, ssa, cfg, leaves, depth + 1);
} else {
leaves.push(v);
}
}
_ => {
leaves.push(v);
}
}
}
fn is_stringify_callee(callee: &str) -> bool {
let base = crate::ssa::type_facts::peel_identity_suffix(callee);
let suffix = base.rsplit(['.', ':']).next().unwrap_or(&base);
matches!(
suffix,
"to_string" | "to_owned" | "format" | "String" | "str"
)
}
fn is_static_map_shell_safe(
values: &[SsaValue],
static_map: Option<&crate::ssa::static_map::StaticMapResult>,
) -> bool {
let Some(sm) = static_map else {
return false;
};
if values.is_empty() {
return false;
}
values.iter().all(|v| match sm.finite_string_values.get(v) {
Some(set) if !set.is_empty() => set
.iter()
.all(|s| crate::abstract_interp::string_domain::is_shell_safe_literal(s)),
_ => false,
})
}
fn is_string_safe_for_ssrf(sf: &crate::abstract_interp::StringFact) -> bool {
let prefix = match &sf.prefix {
Some(p) => p.as_str(),
None => return false,
};
if prefix.starts_with('/') {
return true;
}
if let Some(after_scheme) = prefix.find("://") {
let host_and_rest = &prefix[after_scheme + 3..];
if let Some(slash_pos) = host_and_rest.find('/') {
return slash_pos > 0; }
}
false
}
fn split_qualifier(raw: &str) -> (Option<&str>, Option<&str>) {
if let Some(pos) = raw.rfind("::") {
let prefix = &raw[..pos];
let last = prefix.rsplit("::").next().unwrap_or(prefix);
return (if last.is_empty() { None } else { Some(last) }, None);
}
if let Some(pos) = raw.rfind('.') {
let prefix = &raw[..pos];
let last = prefix.rsplit('.').next().unwrap_or(prefix);
return (None, if last.is_empty() { None } else { Some(last) });
}
(None, None)
}
fn caller_container_for(transfer: &SsaTaintTransfer, caller_func: &str) -> Option<String> {
if caller_func.is_empty() {
return None;
}
let mut containers: Vec<&str> = transfer
.local_summaries
.keys()
.filter(|k| k.lang == transfer.lang && k.name == caller_func)
.map(|k| k.container.as_str())
.filter(|c| !c.is_empty())
.collect();
containers.sort();
containers.dedup();
if containers.len() == 1 {
Some(containers[0].to_string())
} else {
None
}
}
pub(crate) fn resolve_local_func_key_query(
local_summaries: &FuncSummaries,
q: &CalleeQuery<'_>,
) -> Option<FuncKey> {
let all: Vec<&FuncKey> = local_summaries
.keys()
.filter(|k| k.name == q.name && k.lang == q.caller_lang)
.collect();
if all.is_empty() {
return None;
}
let arity_matches = |k: &FuncKey| match q.arity {
Some(a) => k.arity == Some(a),
None => true,
};
let pick_with_container = |container: &str| -> Option<FuncKey> {
if container.is_empty() {
return None;
}
let narrowed: Vec<&FuncKey> = all
.iter()
.copied()
.filter(|k| k.container == container)
.filter(|k| arity_matches(k))
.collect();
if narrowed.len() == 1 {
Some(narrowed[0].clone())
} else {
None
}
};
if let Some(rt) = q.receiver_type {
if let Some(k) = pick_with_container(rt) {
return Some(k);
}
return None;
}
if let Some(nq) = q.namespace_qualifier {
if let Some(k) = pick_with_container(nq) {
return Some(k);
}
}
if let Some(cc) = q.caller_container {
if let Some(k) = pick_with_container(cc) {
return Some(k);
}
}
let arity_filtered: Vec<&FuncKey> = all.iter().copied().filter(|k| arity_matches(k)).collect();
if arity_filtered.len() == 1 {
return Some(arity_filtered[0].clone());
}
if let Some(rv) = q.receiver_var {
if let Some(k) = pick_with_container(rv) {
return Some(k);
}
}
if q.receiver_type.is_none() && q.namespace_qualifier.is_none() && q.receiver_var.is_none() {
let empty: Vec<&FuncKey> = arity_filtered
.iter()
.copied()
.filter(|k| k.container.is_empty())
.collect();
if empty.len() == 1 {
return Some(empty[0].clone());
}
}
None
}
pub(crate) fn resolve_local_func_key(
local_summaries: &FuncSummaries,
lang: Lang,
_namespace: &str,
leaf_name: &str,
container_hint: Option<&str>,
) -> Option<FuncKey> {
let mut candidates: Vec<&FuncKey> = local_summaries
.keys()
.filter(|k| k.name == leaf_name && k.lang == lang)
.collect();
if candidates.is_empty() {
return None;
}
if candidates.len() > 1 {
if let Some(container) = container_hint {
let narrowed: Vec<&FuncKey> = candidates
.iter()
.copied()
.filter(|k| k.container == container)
.collect();
if narrowed.len() == 1 {
return Some(narrowed[0].clone());
}
candidates = narrowed;
}
}
if candidates.len() == 1 {
Some(candidates[0].clone())
} else {
None
}
}
struct ResolvedSummary {
source_caps: Cap,
sanitizer_caps: Cap,
sink_caps: Cap,
param_to_sink: Vec<(usize, Cap)>,
param_to_sink_sites: Vec<(usize, SmallVec<[SinkSite; 1]>)>,
propagates_taint: bool,
propagating_params: Vec<usize>,
param_container_to_return: Vec<usize>,
param_to_container_store: Vec<(usize, usize)>,
return_type: Option<crate::ssa::type_facts::TypeKind>,
return_abstract: Option<crate::abstract_interp::AbstractValue>,
source_to_callback: Vec<(usize, Cap)>,
#[allow(dead_code)]
receiver_to_return: Option<crate::summary::ssa_summary::TaintTransform>,
#[allow(dead_code)]
receiver_to_sink: Cap,
abstract_transfer: Vec<(usize, crate::abstract_interp::AbstractTransfer)>,
param_return_paths: Vec<(
usize,
smallvec::SmallVec<[crate::summary::ssa_summary::ReturnPathTransform; 2]>,
)>,
points_to: crate::summary::points_to::PointsToSummary,
field_points_to: crate::summary::points_to::FieldPointsToSummary,
}
fn resolve_callee(
transfer: &SsaTaintTransfer,
callee: &str,
caller_func: &str,
call_ordinal: u32,
) -> Option<ResolvedSummary> {
resolve_callee_hinted(transfer, callee, caller_func, call_ordinal, None)
}
fn resolve_callee_hinted(
transfer: &SsaTaintTransfer,
callee: &str,
caller_func: &str,
call_ordinal: u32,
arity_hint: Option<usize>,
) -> Option<ResolvedSummary> {
resolve_callee_full(
transfer,
callee,
caller_func,
call_ordinal,
arity_hint,
None,
)
}
fn resolve_callee_typed(
transfer: &SsaTaintTransfer,
callee: &str,
caller_func: &str,
call_ordinal: u32,
arity_hint: Option<usize>,
receiver: Option<SsaValue>,
) -> Option<ResolvedSummary> {
let receiver_type = receiver_type_prefix(transfer, receiver);
resolve_callee_full(
transfer,
callee,
caller_func,
call_ordinal,
arity_hint,
receiver_type,
)
}
fn receiver_type_prefix(
transfer: &SsaTaintTransfer,
receiver: Option<SsaValue>,
) -> Option<&'static str> {
let v = receiver?;
let tf = transfer.type_facts?;
let kind = tf.get_type(v)?;
kind.label_prefix()
}
fn resolve_callee_full(
transfer: &SsaTaintTransfer,
callee: &str,
caller_func: &str,
call_ordinal: u32,
arity_hint: Option<usize>,
receiver_type: Option<&str>,
) -> Option<ResolvedSummary> {
let normalized = callee_leaf_name(callee);
let (namespace_qualifier, receiver_var) = split_qualifier(callee);
if let Some(bindings) = transfer.import_bindings {
if let Some(binding) = bindings.get(normalized) {
return resolve_callee_hinted(
transfer,
&binding.original,
caller_func,
call_ordinal,
arity_hint,
);
}
}
if let Some(cb) = transfer.callback_bindings {
if let Some(real_key) = cb.get(normalized) {
if let Some(ssa_sums) = transfer.ssa_summaries {
if let Some(ssa_sum) = ssa_sums.get(real_key) {
return Some(convert_ssa_to_resolved_for_caller(
ssa_sum,
Some(transfer.namespace),
));
}
}
if let Some(ls) = transfer.local_summaries.get(real_key) {
return Some(ResolvedSummary {
source_caps: ls.source_caps,
sanitizer_caps: ls.sanitizer_caps,
sink_caps: ls.sink_caps,
param_to_sink: ls
.tainted_sink_params
.iter()
.map(|&i| (i, ls.sink_caps))
.collect(),
param_to_sink_sites: vec![],
propagates_taint: !ls.propagating_params.is_empty(),
propagating_params: ls.propagating_params.clone(),
param_container_to_return: vec![],
param_to_container_store: vec![],
return_type: None,
return_abstract: None,
source_to_callback: vec![],
receiver_to_return: None,
receiver_to_sink: Cap::empty(),
abstract_transfer: vec![],
param_return_paths: vec![],
points_to: Default::default(),
field_points_to: Default::default(),
});
}
let labels = crate::labels::classify_all(
transfer.lang.as_str(),
&real_key.name,
transfer.extra_labels,
);
if !labels.is_empty() {
let mut source_caps = Cap::empty();
let mut sanitizer_caps = Cap::empty();
let mut sink_caps = Cap::empty();
for lbl in &labels {
match lbl {
DataLabel::Source(bits) => source_caps |= *bits,
DataLabel::Sanitizer(bits) => sanitizer_caps |= *bits,
DataLabel::Sink(bits) => sink_caps |= *bits,
}
}
return Some(ResolvedSummary {
source_caps,
sanitizer_caps,
sink_caps,
param_to_sink: vec![],
param_to_sink_sites: vec![],
propagates_taint: false,
propagating_params: vec![],
param_container_to_return: vec![],
param_to_container_store: vec![],
return_type: None,
return_abstract: None,
source_to_callback: vec![],
receiver_to_return: None,
receiver_to_sink: Cap::empty(),
abstract_transfer: vec![],
param_return_paths: vec![],
points_to: Default::default(),
field_points_to: Default::default(),
});
}
}
}
let caller_container_opt = caller_container_for(transfer, caller_func);
let caller_container: Option<&str> = caller_container_opt.as_deref();
let build_query = || CalleeQuery {
name: normalized,
caller_lang: transfer.lang,
caller_namespace: transfer.namespace,
caller_container,
receiver_type,
namespace_qualifier,
receiver_var,
arity: arity_hint,
};
if let Some(ssa_sums) = transfer.ssa_summaries {
if let Some(key) = resolve_local_func_key_query(transfer.local_summaries, &build_query()) {
if let Some(ssa_sum) = ssa_sums.get(&key) {
return Some(convert_ssa_to_resolved(ssa_sum));
}
if let Some((_, ssa_sum)) = ssa_sums.iter().find(|(k, _)| {
k.lang == key.lang
&& k.container == key.container
&& k.name == key.name
&& k.arity == key.arity
&& k.disambig == key.disambig
&& k.kind == key.kind
}) {
return Some(convert_ssa_to_resolved(ssa_sum));
}
}
}
if let Some(gs) = transfer.global_summaries {
let widened = gs.resolve_callee_widened(&build_query());
match widened.len() {
0 => {}
1 => {
if let Some(ssa_sum) = gs.get_ssa(&widened[0]) {
return Some(convert_ssa_to_resolved_for_caller(
ssa_sum,
Some(transfer.namespace),
));
}
}
_ => {
let mut accum: Option<ResolvedSummary> = None;
let mut covered: usize = 0;
for key in &widened {
if let Some(ssa_sum) = gs.get_ssa(key) {
let r =
convert_ssa_to_resolved_for_caller(ssa_sum, Some(transfer.namespace));
accum = Some(match accum {
None => r,
Some(a) => merge_resolved_summaries_fanout(a, r),
});
covered += 1;
}
}
if covered > 0 {
tracing::debug!(
callee = %callee,
impls = covered,
widened_total = widened.len(),
"hierarchy fan-out: SSA summaries unioned at call site"
);
return accum;
}
}
}
}
if let Some(key) = resolve_local_func_key_query(transfer.local_summaries, &build_query()) {
if let Some(ls) = transfer.local_summaries.get(&key) {
return Some(ResolvedSummary {
source_caps: ls.source_caps,
sanitizer_caps: ls.sanitizer_caps,
sink_caps: ls.sink_caps,
param_to_sink: ls
.tainted_sink_params
.iter()
.map(|&i| (i, ls.sink_caps))
.collect(),
param_to_sink_sites: vec![],
propagates_taint: !ls.propagating_params.is_empty(),
propagating_params: ls.propagating_params.clone(),
param_container_to_return: vec![],
param_to_container_store: vec![],
return_type: None,
return_abstract: None,
source_to_callback: vec![],
receiver_to_return: None,
receiver_to_sink: Cap::empty(),
abstract_transfer: vec![],
param_return_paths: vec![],
points_to: Default::default(),
field_points_to: Default::default(),
});
}
} else {
let ambiguous_local = transfer
.local_summaries
.keys()
.filter(|k| k.name == normalized && k.lang == transfer.lang)
.count()
> 1;
if ambiguous_local {
return None;
}
}
if let Some(gs) = transfer.global_summaries {
let widened = gs.resolve_callee_widened(&build_query());
let convert = |fs: &crate::summary::FuncSummary| ResolvedSummary {
source_caps: fs.source_caps(),
sanitizer_caps: fs.sanitizer_caps(),
sink_caps: fs.sink_caps(),
param_to_sink: fs
.tainted_sink_params
.iter()
.map(|&i| (i, fs.sink_caps()))
.collect(),
param_to_sink_sites: fs.param_to_sink.clone(),
propagates_taint: fs.propagates_any(),
propagating_params: fs.propagating_params.clone(),
param_container_to_return: vec![],
param_to_container_store: vec![],
return_type: None,
return_abstract: None,
source_to_callback: vec![],
receiver_to_return: None,
receiver_to_sink: Cap::empty(),
abstract_transfer: vec![],
param_return_paths: vec![],
points_to: Default::default(),
field_points_to: Default::default(),
};
match widened.len() {
0 => {}
1 => {
if let Some(fs) = gs.get(&widened[0]) {
return Some(convert(fs));
}
}
_ => {
let mut accum: Option<ResolvedSummary> = None;
let mut covered: usize = 0;
for key in &widened {
if let Some(fs) = gs.get(key) {
let r = convert(fs);
accum = Some(match accum {
None => r,
Some(a) => merge_resolved_summaries_fanout(a, r),
});
covered += 1;
}
}
if covered > 0 {
tracing::debug!(
callee = %callee,
impls = covered,
widened_total = widened.len(),
"hierarchy fan-out: FuncSummaries unioned at call site"
);
return accum;
}
}
}
}
for edge in transfer.interop_edges {
if edge.from.caller_lang == transfer.lang
&& edge.from.caller_namespace == transfer.namespace
&& edge.from.callee_symbol == callee
&& (edge.from.caller_func.is_empty() || edge.from.caller_func == caller_func)
&& (edge.from.ordinal == 0 || edge.from.ordinal == call_ordinal)
&& let Some(gs) = transfer.global_summaries
&& let Some(fs) = gs.get_for_interop(&edge.to)
{
return Some(ResolvedSummary {
source_caps: fs.source_caps(),
sanitizer_caps: fs.sanitizer_caps(),
sink_caps: fs.sink_caps(),
param_to_sink: fs
.tainted_sink_params
.iter()
.map(|&i| (i, fs.sink_caps()))
.collect(),
param_to_sink_sites: fs.param_to_sink.clone(),
propagates_taint: fs.propagates_any(),
propagating_params: fs.propagating_params.clone(),
param_container_to_return: vec![],
param_to_container_store: vec![],
return_type: None,
return_abstract: None,
source_to_callback: vec![],
receiver_to_return: None,
receiver_to_sink: Cap::empty(),
abstract_transfer: vec![],
param_return_paths: vec![],
points_to: Default::default(),
field_points_to: Default::default(),
});
}
}
None
}
fn effective_param_sanitizer(
resolved: &ResolvedSummary,
param_idx: usize,
state: &SsaTaintState,
) -> Cap {
use crate::summary::ssa_summary::TaintTransform;
let paths = match resolved
.param_return_paths
.iter()
.find(|(i, _)| *i == param_idx)
{
Some((_, p)) => p,
None => return resolved.sanitizer_caps,
};
let mut caller_kt: u8 = 0;
let mut caller_kf: u8 = 0;
for (_, pred) in &state.predicates {
caller_kt |= pred.known_true;
caller_kf |= pred.known_false;
}
let mut compatible: smallvec::SmallVec<[&_; 2]> = smallvec::SmallVec::new();
for path in paths {
if path.known_true & caller_kf != 0 {
continue;
}
if path.known_false & caller_kt != 0 {
continue;
}
compatible.push(path);
}
if compatible.is_empty() {
return resolved.sanitizer_caps;
}
let mut common = Cap::all();
let mut saw_any = false;
for path in &compatible {
match &path.transform {
TaintTransform::StripBits(bits) => {
common &= *bits;
saw_any = true;
}
TaintTransform::Identity => {
common = Cap::empty();
saw_any = true;
}
TaintTransform::AddBits(_) => {
common = Cap::empty();
saw_any = true;
}
}
}
if !saw_any {
resolved.sanitizer_caps
} else {
common
}
}
fn convert_ssa_to_resolved(
ssa_sum: &crate::summary::ssa_summary::SsaFuncSummary,
) -> ResolvedSummary {
convert_ssa_to_resolved_for_caller(ssa_sum, None)
}
fn convert_ssa_to_resolved_for_caller(
ssa_sum: &crate::summary::ssa_summary::SsaFuncSummary,
caller_namespace: Option<&str>,
) -> ResolvedSummary {
use crate::summary::ssa_summary::TaintTransform;
let propagating_params: Vec<usize> = ssa_sum
.param_to_return
.iter()
.map(|(idx, _)| *idx)
.collect();
let mut sanitizer_caps = Cap::empty();
for (_, transform) in &ssa_sum.param_to_return {
if let TaintTransform::StripBits(bits) = transform {
sanitizer_caps |= *bits;
}
}
let sink_caps = ssa_sum.total_param_sink_caps();
let param_to_sink = ssa_sum.param_to_sink_caps();
let param_to_sink_sites = if let Some(caller_ns) = caller_namespace {
ssa_sum
.param_to_sink
.iter()
.map(|(idx, sites)| {
let filtered: SmallVec<[crate::summary::SinkSite; 1]> = sites
.iter()
.filter(|s| s.file_rel.is_empty() || s.file_rel != caller_ns)
.cloned()
.collect();
(*idx, filtered)
})
.filter(|(_, sites)| !sites.is_empty())
.collect()
} else {
ssa_sum.param_to_sink.clone()
};
ResolvedSummary {
source_caps: ssa_sum.source_caps,
sanitizer_caps,
sink_caps,
param_to_sink,
param_to_sink_sites,
propagates_taint: !propagating_params.is_empty(),
propagating_params,
param_container_to_return: ssa_sum.param_container_to_return.clone(),
param_to_container_store: ssa_sum.param_to_container_store.clone(),
return_type: ssa_sum.return_type.clone(),
return_abstract: ssa_sum.return_abstract.clone(),
source_to_callback: ssa_sum.source_to_callback.clone(),
receiver_to_return: ssa_sum.receiver_to_return.clone(),
receiver_to_sink: ssa_sum.receiver_to_sink,
abstract_transfer: ssa_sum.abstract_transfer.clone(),
param_return_paths: ssa_sum.param_return_paths.clone(),
points_to: ssa_sum.points_to.clone(),
field_points_to: ssa_sum.field_points_to.clone(),
}
}
fn merge_resolved_summaries_fanout(
mut acc: ResolvedSummary,
r: ResolvedSummary,
) -> ResolvedSummary {
acc.source_caps |= r.source_caps;
acc.sanitizer_caps &= r.sanitizer_caps;
acc.sink_caps |= r.sink_caps;
acc.propagates_taint |= r.propagates_taint;
acc.receiver_to_sink |= r.receiver_to_sink;
for (idx, caps) in r.param_to_sink {
if let Some(slot) = acc.param_to_sink.iter_mut().find(|(i, _)| *i == idx) {
slot.1 |= caps;
} else {
acc.param_to_sink.push((idx, caps));
}
}
for (idx, sites) in r.param_to_sink_sites {
if let Some(slot) = acc.param_to_sink_sites.iter_mut().find(|(i, _)| *i == idx) {
for site in sites {
if !slot.1.iter().any(|s| s == &site) {
slot.1.push(site);
}
}
} else {
acc.param_to_sink_sites.push((idx, sites));
}
}
for p in r.propagating_params {
if !acc.propagating_params.contains(&p) {
acc.propagating_params.push(p);
}
}
for p in r.param_container_to_return {
if !acc.param_container_to_return.contains(&p) {
acc.param_container_to_return.push(p);
}
}
for pair in r.param_to_container_store {
if !acc.param_to_container_store.contains(&pair) {
acc.param_to_container_store.push(pair);
}
}
for (idx, caps) in r.source_to_callback {
if let Some(slot) = acc.source_to_callback.iter_mut().find(|(i, _)| *i == idx) {
slot.1 |= caps;
} else {
acc.source_to_callback.push((idx, caps));
}
}
if acc.return_type != r.return_type {
acc.return_type = None;
}
if acc.return_abstract != r.return_abstract {
acc.return_abstract = None;
}
if acc.receiver_to_return != r.receiver_to_return {
acc.receiver_to_return = None;
}
if acc.abstract_transfer != r.abstract_transfer {
acc.abstract_transfer = Vec::new();
}
if acc.param_return_paths != r.param_return_paths {
acc.param_return_paths = Vec::new();
}
if acc.points_to != r.points_to {
acc.points_to = Default::default();
}
acc
}