Prerelease cleanup (#46)

* feat: Add const_bound_vars tracking to prevent false positives in ownership checks

* feat: Introduce field interner and typed bounded vars for enhanced type tracking

* feat: Add typed_call_receivers and typed_bounded_dto_fields for enhanced type tracking

* feat: Centralize method name extraction with bare_method_name helper

* feat: Implement Phase-6 hierarchy fan-out for runtime virtual dispatch

* feat: Enhance C++ taint tracking with additional container operations and inline method resolution

* feat: Introduce field-sensitive points-to analysis for enhanced resource tracking

* feat: Implement Pointer-Phase 6 subscript handling for enhanced container analysis

* test: Add comprehensive tests for JavaScript control flow constructs and lattice operations

* docs: Update advanced analysis documentation with field-sensitive points-to and hierarchy fan-out details

* test: Add comprehensive tests for lattice algebra laws and SSA edge cases

* feat: Add destructured session user handling and safe user ID access patterns

* feat: Implement row-population reverse-walk for enhanced authorization checks

* feat: Enhance authorization checks with local alias chain for self-actor types

* feat: Introduce ActiveRecord query safety checks and enhance snippet extraction

* feat: Implement chained method call inner-gate rebinding for SSRF prevention

* feat: Add observability and error modules, enhance debug functionality, and implement theme context

* feat: Remove Auth Analysis page and update navigation to redirect to Explorer

* feat: Optimize SSA lowering by sharing results between taint engine and artifact extractor

* feat: Optimize SSA lowering by sharing results between taint engine and artifact extractor

* feat: Reset path-safe-suppressed spans before lowering to maintain analysis integrity

* fix(ssa): ungate debug_assert_bfs_ordering for release-tests build

The helper at src/ssa/lower.rs was gated `#[cfg(debug_assertions)]` while
the unit test at the bottom of the file was gated only `#[cfg(test)]`.
Since `cfg(test)` is set in release builds with `--tests` but
`cfg(debug_assertions)` is not, `cargo build --release --tests` failed
with E0425. Removing the gate fixes the build; the body is `debug_assert!`
only, so the helper is free in release. Also drop the gate at the call
site to avoid a `dead_code` warning when the lib is built without
`--tests`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(closure-capture): flip JS/TS fixtures to required-finding

The JS and TS closure-capture fixtures pinned the old broken behaviour
via `forbidden_findings: [{ "id_prefix": "taint-" }]`. The engine now
correctly traces taint through the closure boundary (env source captured
by an arrow function, sunk via `child_process.exec` inside the body), so
the formerly-forbidden finding is a true positive.

Match the Python sibling's shape — `required_findings` with
`id_prefix` + `min_count` plus a small `noise_budget` — and rewrite the
companion READMEs and the phase8_fragility_tests doc-comments from
"known gap" to "regression guard".

Verified:
- cargo test --release --test phase8_fragility_tests → 8/8 pass
- cargo test --release --lib bfs_assertion → pass
- corpus benchmark F1 = 0.9976 (TP=205, FP=1, FN=0) — unchanged

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat: Add OWASP mapping and baseline mutation hooks for enhanced security analysis

* feat: Introduce health module and enhance health score computation with calibration tests

* feat: Add expectations configuration and cleanup .gitignore for log files

* feat: Implement theme selection and enhance settings panel for triage sync

* feat: Suppress false positives for strcpy calls with literal sources in AST

* feat: Update analyse_function_ssa to return body CFG for accurate analysis

* feat: Add bug report and feature request templates for improved issue tracking

* feat: removed dev scripts

* feat: update README.md for clarity and consistency in fixture descriptions

* feat: removed dev docs

* feat: clean up error handling and UI elements for improved user experience

* feat: adjust button sizes in HeaderBar for better UI consistency

* feat: enhance taint analysis with additional context for sanitizer and taint findings

* cargo fmt

* prettier

* refactor: simplify conditional checks and improve code readability in AST and screenshot capture scripts

* feat: add script to frame PNG screenshots with brand gradient

* feat: add fuzzing support with new targets and CI workflows

* refactor: streamline match expressions and improve formatting in CLI and output handling

* feat: enhance configuration display with detailed output options

* feat: stage demo configuration for improved CLI screenshot output

* feat: expose merge_configs function for user-configurable settings

* refactor: simplify code structure and improve readability in config handling

* refactor: improve descriptions for vulnerability patterns in various languages

* feat: update MIT License section with additional usage details and copyright information

* feat: update screenshots

* refactor: update build process and paths for frontend assets

* feat: add cross-file taint fuzzing target and supporting dictionary

* refactor: clean up formatting and comments in fuzz configuration and example files

* refactor: remove outdated comments and clean up CI configuration files

* chore: update changelog dates and improve formatting in documentation

* refactor: update Cargo.toml and CI configuration for improved packaging and build process

* refactor: enhance quote-stripping logic to prevent panics and add regression tests

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Eli Peter 2026-04-29 00:58:38 -04:00 committed by GitHub
parent 79c29b394d
commit 82f18184b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
348 changed files with 48731 additions and 2925 deletions

View file

@ -1379,6 +1379,8 @@ mod tests {
value_defs: vec![],
cfg_node_map: HashMap::new(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
};
let empty_succs = HashMap::new();
@ -1436,6 +1438,8 @@ mod tests {
value_defs: vec![],
cfg_node_map: HashMap::new(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
};
let empty_succs = HashMap::new();
@ -1566,6 +1570,8 @@ mod tests {
value_defs: vec![make_value_def(b0, n0), make_value_def(b1, n1)],
cfg_node_map: [(n0, SsaValue(0)), (n1, SsaValue(1))].into_iter().collect(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
};
let finding = make_finding(n0, n1);
@ -1671,6 +1677,8 @@ mod tests {
.into_iter()
.collect(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
};
// Finding path goes through B0 → B1 → B3
@ -1814,6 +1822,8 @@ mod tests {
.into_iter()
.collect(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
};
let finding = Finding {
@ -1923,6 +1933,8 @@ mod tests {
value_defs: vec![],
cfg_node_map: HashMap::new(),
exception_edges: vec![(b0, b2)],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
};
let mut exc_succs: HashMap<BlockId, SmallVec<[BlockId; 2]>> = HashMap::new();
@ -1987,6 +1999,8 @@ mod tests {
value_defs: vec![],
cfg_node_map: HashMap::new(),
exception_edges: vec![(b0, b2)],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
};
let mut exc_succs: HashMap<BlockId, SmallVec<[BlockId; 2]>> = HashMap::new();
@ -2041,6 +2055,7 @@ mod tests {
value: SsaValue(1),
op: SsaOp::Call {
callee: "JSON.parse".into(),
callee_text: None,
args: vec![smallvec![SsaValue(0)]],
receiver: None,
},
@ -2091,6 +2106,8 @@ mod tests {
.into_iter()
.collect(),
exception_edges: vec![(b1, b2)],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
};
let finding = Finding {

View file

@ -1094,6 +1094,7 @@ fn handle_nested_calls(
callee,
args,
receiver,
..
} = &inst.op
{
// Only attempt if the current result is opaque

View file

@ -387,6 +387,8 @@ mod tests {
value_defs: vec![],
cfg_node_map: HashMap::new(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
};
let info = analyse_loops(&ssa);
@ -430,6 +432,8 @@ mod tests {
value_defs: vec![],
cfg_node_map: HashMap::new(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
};
let info = analyse_loops(&ssa);
@ -509,6 +513,8 @@ mod tests {
value_defs: vec![],
cfg_node_map: HashMap::new(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
};
let info = analyse_loops(&ssa);
@ -569,6 +575,8 @@ mod tests {
value_defs: vec![],
cfg_node_map: HashMap::new(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
};
let info = analyse_loops(&ssa);
@ -647,6 +655,8 @@ mod tests {
value_defs: vec![],
cfg_node_map: HashMap::new(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
};
let info = analyse_loops(&ssa);
@ -716,6 +726,8 @@ mod tests {
value_defs: vec![],
cfg_node_map: HashMap::new(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
};
let info = analyse_loops(&ssa);
@ -748,6 +760,8 @@ mod tests {
value_defs: vec![],
cfg_node_map: HashMap::new(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
};
let info = analyse_loops(&ssa);
@ -802,6 +816,8 @@ mod tests {
value_defs: vec![],
cfg_node_map: HashMap::new(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
};
let info = analyse_loops(&ssa);
@ -880,6 +896,8 @@ mod tests {
],
cfg_node_map: HashMap::new(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
};
let info = analyse_loops(&ssa);
@ -929,6 +947,7 @@ mod tests {
2,
SsaOp::Call {
callee: "f".into(),
callee_text: None,
args: vec![smallvec![SsaValue(1)]],
receiver: None,
},
@ -955,6 +974,8 @@ mod tests {
],
cfg_node_map: HashMap::new(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
};
let info = analyse_loops(&ssa);
@ -988,6 +1009,8 @@ mod tests {
value_defs: vec![],
cfg_node_map: HashMap::new(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
};
let info = analyse_loops(&ssa);

View file

@ -377,6 +377,8 @@ mod tests {
value_defs: vec![make_value_def(b0, n0), make_value_def(b1, n1)],
cfg_node_map: [(n0, SsaValue(0)), (n1, SsaValue(1))].into_iter().collect(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
};
let finding = Finding {
@ -447,6 +449,8 @@ mod tests {
value_defs: vec![make_value_def(b0, n0), make_value_def(b1, n1)],
cfg_node_map: [(n0, SsaValue(0)), (n1, SsaValue(1))].into_iter().collect(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
};
let finding = Finding {
@ -545,6 +549,8 @@ mod tests {
value_defs: vec![],
cfg_node_map: HashMap::new(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
};
let ctx = SymexContext {
@ -602,6 +608,8 @@ mod tests {
value_defs: vec![],
cfg_node_map: HashMap::new(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
};
let ctx = SymexContext {

View file

@ -350,6 +350,8 @@ mod tests {
value_defs: vec![],
cfg_node_map: [(node, SsaValue(5))].into_iter().collect(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
};
let witness = state.get_sink_witness(&finding, &ssa);
@ -387,6 +389,8 @@ mod tests {
value_defs: vec![],
cfg_node_map: [(node, SsaValue(5))].into_iter().collect(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
};
assert_eq!(state.get_sink_witness(&finding, &ssa), None);
@ -421,6 +425,8 @@ mod tests {
value_defs: vec![],
cfg_node_map: HashMap::new(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
};
assert_eq!(state.get_sink_witness(&finding, &ssa), None);
@ -459,6 +465,8 @@ mod tests {
value_defs: vec![],
cfg_node_map: HashMap::new(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
};
state.widen_at_loop_head(BlockId(0), &ssa);
@ -500,6 +508,8 @@ mod tests {
value_defs: vec![],
cfg_node_map: HashMap::new(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
};
state.widen_at_loop_head(BlockId(0), &ssa);
@ -541,6 +551,8 @@ mod tests {
value_defs: vec![],
cfg_node_map: HashMap::new(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
};
state.widen_at_loop_head(BlockId(0), &ssa);

View file

@ -8,7 +8,7 @@
//! etc.) for witness enrichment and heuristic mismatch diagnostics. They do
//! NOT affect taint semantics.
use crate::labels::Cap;
use crate::labels::{Cap, bare_method_name};
use crate::symbol::Lang;
use super::value::SymbolicValue;
@ -155,7 +155,7 @@ pub fn classify_string_method(
args: &[SymbolicValue],
lang: Lang,
) -> Option<StringMethodInfo> {
let method = callee.rsplit('.').next().unwrap_or(callee);
let method = bare_method_name(callee);
match lang {
Lang::JavaScript | Lang::TypeScript => classify_js(method, args),
@ -506,7 +506,7 @@ fn classify_transform_js(callee: &str) -> Option<TransformMethodInfo> {
use StringOperandSource::*;
use TransformKind::*;
let method = callee.rsplit('.').next().unwrap_or(callee);
let method = bare_method_name(callee);
match method {
// URL encoding/decoding
"encodeURIComponent" | "encodeURI" => Some(TransformMethodInfo {
@ -622,7 +622,7 @@ fn classify_transform_java(callee: &str) -> Option<TransformMethodInfo> {
// `URLEncoder.encode`, `Base64.getEncoder.encodeToString`). Match on
// the suffix after the last `.` for the leaf method name, but also
// examine the dotted callee for receiver-qualified disambiguation.
let method = callee.rsplit('.').next().unwrap_or(callee);
let method = bare_method_name(callee);
// URL encoding/decoding — `java.net.URLEncoder.encode` / `URLDecoder.decode`.
if callee.ends_with("URLEncoder.encode") {
@ -1039,7 +1039,7 @@ pub fn detect_replace_sanitizer(
/// Determine whether a replace call is global (replaces all occurrences).
fn is_global_replace(callee: &str, lang: Lang) -> bool {
let method = callee.rsplit('.').next().unwrap_or(callee);
let method = bare_method_name(callee);
match lang {
// JS: replace() is NOT global; replaceAll() IS global
Lang::JavaScript | Lang::TypeScript => method == "replaceAll",

View file

@ -130,6 +130,25 @@ pub fn transfer_inst(
state.set(inst.value, SymbolicValue::Unknown);
}
SsaOp::FieldProj { receiver, .. } => {
// Symbolic field read: model `obj.field` as an opaque value
// tied to the projection's SsaValue, and propagate the
// receiver's taint to the result so flat root-set tracking
// continues to flow taint through chained accesses.
//
// Phase 4 deliberately keeps the opaque-Symbol model: without
// a field-sensitive heap, a dedicated `Field { receiver, name }`
// SymbolicValue variant cannot soundly carry concrete reads
// across method boundaries — the witness pipeline already
// reconstructs `obj.field` text from `ValueDef.var_name`
// (populated by lower.rs to `"base.f1.f2"` for chain projections).
// The structured variant is deferred to the field-sensitive
// pointer analysis prompt, where heap loads consume `FieldProj`
// directly.
state.set(inst.value, SymbolicValue::Symbol(inst.value));
state.propagate_taint(inst.value, std::slice::from_ref(receiver));
}
SsaOp::Assign(uses) => {
let uses_slice: &[_] = uses;
match uses_slice.len() {
@ -202,6 +221,7 @@ pub fn transfer_inst(
callee,
args,
receiver,
..
} => {
// Collect symbolic values for arguments
let mut arg_syms: Vec<SymbolicValue> = Vec::new();
@ -285,6 +305,11 @@ pub fn transfer_inst(
}
// Fall through to normal Call
}
ContainerOp::Writeback { .. } => {
// Symex doesn't model writeback yet — taint
// engine handles the destination-arg taint
// directly. Fall through to normal Call.
}
}
}
}
@ -985,6 +1010,8 @@ mod tests {
value_defs: vec![],
cfg_node_map: std::collections::HashMap::new(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
}
}
@ -1133,6 +1160,7 @@ mod tests {
1,
SsaOp::Call {
callee: "parseInt".into(),
callee_text: None,
args: vec![smallvec![SsaValue(0)]],
receiver: None,
},
@ -1159,6 +1187,7 @@ mod tests {
2,
SsaOp::Call {
callee: "send".into(),
callee_text: None,
args: vec![smallvec![SsaValue(1)]],
receiver: Some(SsaValue(0)),
},
@ -1255,6 +1284,7 @@ mod tests {
4,
SsaOp::Call {
callee: "toString".into(),
callee_text: None,
args: vec![smallvec![SsaValue(3)]],
receiver: None,
},
@ -1558,7 +1588,9 @@ mod tests {
abstract_transfer: vec![],
param_return_paths: vec![],
points_to: Default::default(),
field_points_to: Default::default(),
return_path_facts: smallvec::SmallVec::new(),
typed_call_receivers: vec![],
},
);
let ctx = make_summary_ctx(&gs);
@ -1567,6 +1599,7 @@ mod tests {
1,
SsaOp::Call {
callee: "passthrough".into(),
callee_text: None,
args: vec![smallvec![SsaValue(0)]],
receiver: None,
},
@ -1623,7 +1656,9 @@ mod tests {
abstract_transfer: vec![],
param_return_paths: vec![],
points_to: Default::default(),
field_points_to: Default::default(),
return_path_facts: smallvec::SmallVec::new(),
typed_call_receivers: vec![],
},
);
let ctx = make_summary_ctx(&gs);
@ -1632,6 +1667,7 @@ mod tests {
2,
SsaOp::Call {
callee: "ambig".into(),
callee_text: None,
args: vec![smallvec![SsaValue(0)], smallvec![SsaValue(1)]],
receiver: None,
},
@ -1688,7 +1724,9 @@ mod tests {
abstract_transfer: vec![],
param_return_paths: vec![],
points_to: Default::default(),
field_points_to: Default::default(),
return_path_facts: smallvec::SmallVec::new(),
typed_call_receivers: vec![],
},
);
let ctx = make_summary_ctx(&gs);
@ -1697,6 +1735,7 @@ mod tests {
1,
SsaOp::Call {
callee: "sanitize".into(),
callee_text: None,
args: vec![smallvec![SsaValue(0)]],
receiver: None,
},
@ -1748,7 +1787,9 @@ mod tests {
abstract_transfer: vec![],
param_return_paths: vec![],
points_to: Default::default(),
field_points_to: Default::default(),
return_path_facts: smallvec::SmallVec::new(),
typed_call_receivers: vec![],
},
);
let ctx = make_summary_ctx(&gs);
@ -1757,6 +1798,7 @@ mod tests {
1,
SsaOp::Call {
callee: "enrich".into(),
callee_text: None,
args: vec![smallvec![SsaValue(0)]],
receiver: None,
},
@ -1808,7 +1850,9 @@ mod tests {
abstract_transfer: vec![],
param_return_paths: vec![],
points_to: Default::default(),
field_points_to: Default::default(),
return_path_facts: smallvec::SmallVec::new(),
typed_call_receivers: vec![],
},
);
let ctx = make_summary_ctx(&gs);
@ -1817,6 +1861,7 @@ mod tests {
0,
SsaOp::Call {
callee: "readEnv".into(),
callee_text: None,
args: vec![],
receiver: None,
},
@ -1855,6 +1900,7 @@ mod tests {
1,
SsaOp::Call {
callee: "unknown_func".into(),
callee_text: None,
args: vec![smallvec![SsaValue(0)]],
receiver: None,
},
@ -1892,6 +1938,7 @@ mod tests {
1,
SsaOp::Call {
callee: "foo".into(),
callee_text: None,
args: vec![smallvec![SsaValue(0)]],
receiver: None,
},
@ -2000,7 +2047,9 @@ mod tests {
abstract_transfer: vec![],
param_return_paths: vec![],
points_to: Default::default(),
field_points_to: Default::default(),
return_path_facts: smallvec::SmallVec::new(),
typed_call_receivers: vec![],
},
);
@ -2017,6 +2066,7 @@ mod tests {
2,
SsaOp::Call {
callee: "send".into(),
callee_text: None,
args: vec![smallvec![SsaValue(0)]],
receiver: Some(SsaValue(1)),
},
@ -2075,7 +2125,9 @@ mod tests {
abstract_transfer: vec![],
param_return_paths: vec![],
points_to: Default::default(),
field_points_to: Default::default(),
return_path_facts: smallvec::SmallVec::new(),
typed_call_receivers: vec![],
},
);
@ -2092,6 +2144,7 @@ mod tests {
1,
SsaOp::Call {
callee: "passthrough".into(),
callee_text: None,
args: vec![smallvec![SsaValue(0)]],
receiver: None,
},
@ -2151,7 +2204,9 @@ mod tests {
abstract_transfer: vec![],
param_return_paths: vec![],
points_to: Default::default(),
field_points_to: Default::default(),
return_path_facts: smallvec::SmallVec::new(),
typed_call_receivers: vec![],
},
);
// Second "send" — in ns B, also with same arity → ambiguous bare-name
@ -2178,7 +2233,9 @@ mod tests {
abstract_transfer: vec![],
param_return_paths: vec![],
points_to: Default::default(),
field_points_to: Default::default(),
return_path_facts: smallvec::SmallVec::new(),
typed_call_receivers: vec![],
},
);
// Also register the type-qualified name so Attempt 1 can find it
@ -2205,7 +2262,9 @@ mod tests {
abstract_transfer: vec![],
param_return_paths: vec![],
points_to: Default::default(),
field_points_to: Default::default(),
return_path_facts: smallvec::SmallVec::new(),
typed_call_receivers: vec![],
},
);
@ -2222,6 +2281,7 @@ mod tests {
2,
SsaOp::Call {
callee: "send".into(),
callee_text: None,
args: vec![smallvec![SsaValue(0)]],
receiver: Some(SsaValue(1)),
},
@ -2280,7 +2340,9 @@ mod tests {
abstract_transfer: vec![],
param_return_paths: vec![],
points_to: Default::default(),
field_points_to: Default::default(),
return_path_facts: smallvec::SmallVec::new(),
typed_call_receivers: vec![],
},
);
@ -2297,6 +2359,7 @@ mod tests {
2,
SsaOp::Call {
callee: "send".into(),
callee_text: None,
args: vec![smallvec![SsaValue(0)]],
receiver: Some(SsaValue(1)),
},
@ -2357,7 +2420,9 @@ mod tests {
abstract_transfer: vec![],
param_return_paths: vec![],
points_to: Default::default(),
field_points_to: Default::default(),
return_path_facts: smallvec::SmallVec::new(),
typed_call_receivers: vec![],
},
);
insert_java_summary(
@ -2383,7 +2448,9 @@ mod tests {
abstract_transfer: vec![],
param_return_paths: vec![],
points_to: Default::default(),
field_points_to: Default::default(),
return_path_facts: smallvec::SmallVec::new(),
typed_call_receivers: vec![],
},
);
// No "HttpClient.send" summary registered — disambiguation has 0 exact matches
@ -2400,6 +2467,7 @@ mod tests {
2,
SsaOp::Call {
callee: "send".into(),
callee_text: None,
args: vec![smallvec![SsaValue(0)]],
receiver: Some(SsaValue(1)),
},

View file

@ -1265,4 +1265,39 @@ mod tests {
let v = mk_encode(TransformKind::UrlEncode, inner);
assert_eq!(v.depth(), 1);
}
/// `mk_binop(Add, ConcreteStr, Concrete(int))` must not silently
/// coerce types. The fold path only triggers when *both* operands
/// are `Concrete(i64)`; mixed-type operands must build a symbolic
/// `BinOp` so downstream witness rendering / type analysis can
/// reject the bogus arithmetic.
#[test]
fn binop_mixed_str_int_does_not_coerce() {
let v = mk_binop(
Op::Add,
SymbolicValue::ConcreteStr("price=".into()),
SymbolicValue::Concrete(42),
);
assert!(
matches!(v, SymbolicValue::BinOp(Op::Add, _, _)),
"mixed-type Add must produce a symbolic BinOp, not silently fold"
);
}
/// `mk_phi` must not fold when operands have differing types
/// (e.g. one branch returns a Concrete int, another returns
/// ConcreteStr). The result is genuinely uncertain — a Phi node
/// must be preserved to expose the type-conflict to downstream
/// witness logic, not collapse to one operand.
#[test]
fn phi_mixed_types_keeps_phi() {
let v = mk_phi(vec![
(BlockId(0), SymbolicValue::Concrete(7)),
(BlockId(1), SymbolicValue::ConcreteStr("x".into())),
]);
assert!(
matches!(v, SymbolicValue::Phi(_)),
"phi over mixed types must NOT fold to a single operand"
);
}
}

View file

@ -772,6 +772,8 @@ mod tests {
value_defs: vec![],
cfg_node_map: [(sink_node, sink_val)].into_iter().collect(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
};
let finding = Finding {
@ -824,6 +826,8 @@ mod tests {
value_defs: vec![],
cfg_node_map: [(sink_node, SsaValue(5))].into_iter().collect(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
};
let cfg = Cfg::new();
let finding = Finding {
@ -882,6 +886,8 @@ mod tests {
value_defs: vec![],
cfg_node_map: [(sink_node, sink_val)].into_iter().collect(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
};
let finding = Finding {
@ -941,6 +947,8 @@ mod tests {
value_defs: vec![],
cfg_node_map: [(sink_node, sink_val)].into_iter().collect(),
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
};
let finding = Finding {