mirror of
https://github.com/elicpeter/nyx.git
synced 2026-07-21 21:31:03 +02:00
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:
parent
79c29b394d
commit
82f18184b1
348 changed files with 48731 additions and 2925 deletions
|
|
@ -201,6 +201,19 @@ pub(super) fn reconstruct_flow_path(
|
|||
}
|
||||
current = pick_tainted_operand(&vals, origin, ssa);
|
||||
}
|
||||
SsaOp::FieldProj { receiver, .. } => {
|
||||
// Treat field projection as a one-step assignment for
|
||||
// flow-step reconstruction: taint reaching `obj.f` came
|
||||
// from `obj`. Phase 4 will refine the witness rendering
|
||||
// to include the field name in the step.
|
||||
steps.push(FlowStepRaw {
|
||||
cfg_node: inst.cfg_node,
|
||||
var_name: inst.var_name.clone(),
|
||||
op_kind: FlowStepKind::Assignment,
|
||||
});
|
||||
let single: SmallVec<[SsaValue; 4]> = smallvec::smallvec![*receiver];
|
||||
current = pick_tainted_operand(&single, origin, ssa);
|
||||
}
|
||||
SsaOp::Const(_) | SsaOp::Nop | SsaOp::Undef => break,
|
||||
}
|
||||
}
|
||||
|
|
@ -272,6 +285,16 @@ pub fn ssa_events_to_findings(
|
|||
// Suppress findings where all tainted variables were validated
|
||||
// (passed through an allowlist, type-check, or validation branch).
|
||||
if event.all_validated {
|
||||
let span = cfg[event.sink_node].ast.span;
|
||||
// Cap-agnostic: record the validated sink span so the
|
||||
// AST-pattern suppression gate (`TaintSuppressionCtx`) has
|
||||
// positive evidence that the SSA engine reached this sink
|
||||
// and proved safety. Without this, validation/dominator/
|
||||
// early-return-style safe code is indistinguishable from
|
||||
// silent engine failure when the function emitted no
|
||||
// findings and contains no labelled Sanitizer.
|
||||
crate::taint::ssa_transfer::state::record_all_validated_span(span);
|
||||
|
||||
// Mirror the path-safety pathway: when the SSA engine has
|
||||
// already proved every tainted input to a privileged
|
||||
// FILE_IO sink passed through validation, publish the sink
|
||||
|
|
@ -283,7 +306,6 @@ pub fn ssa_events_to_findings(
|
|||
// currently fires on; broadening would risk stretching
|
||||
// validator-name heuristics into unrelated finding classes.
|
||||
if event.sink_caps.intersects(Cap::FILE_IO) {
|
||||
let span = cfg[event.sink_node].ast.span;
|
||||
crate::taint::ssa_transfer::state::record_path_safe_suppressed_span(span);
|
||||
}
|
||||
continue;
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -13,8 +13,9 @@
|
|||
use crate::abstract_interp::{self, AbstractState};
|
||||
use crate::cfg::BodyId;
|
||||
use crate::constraint;
|
||||
use crate::pointer::LocId;
|
||||
use crate::ssa::heap::HeapState;
|
||||
use crate::ssa::ir::SsaValue;
|
||||
use crate::ssa::ir::{FieldId, SsaValue};
|
||||
use crate::state::lattice::Lattice;
|
||||
use crate::state::symbol::SymbolId;
|
||||
use crate::taint::domain::{PredicateSummary, SmallBitSet, TaintOrigin, VarTaint};
|
||||
|
|
@ -153,6 +154,29 @@ thread_local! {
|
|||
/// Reset at start of `analyse_file`, drained before state analysis.
|
||||
static PATH_SAFE_SUPPRESSED_SPANS: RefCell<std::collections::HashSet<(usize, usize)>> =
|
||||
RefCell::new(std::collections::HashSet::new());
|
||||
|
||||
/// File-level set of CFG sink spans where the SSA engine emitted an
|
||||
/// `all_validated` event — every tainted input to the sink passed
|
||||
/// through a recognised validation/sanitisation predicate before
|
||||
/// reaching it. Distinct from `PATH_SAFE_SUPPRESSED_SPANS`, which
|
||||
/// is FILE_IO-scoped and feeds state analysis: this set is
|
||||
/// cap-agnostic and feeds AST-pattern suppression, providing
|
||||
/// positive evidence that the engine reached the sink and proved
|
||||
/// safety so that downstream AST-pattern findings on the same line
|
||||
/// can be safely silenced.
|
||||
///
|
||||
/// Without this signal the suppression gate has to fall back to
|
||||
/// "function emitted at least one taint-unsanitised-flow finding"
|
||||
/// or "function contains a labelled Sanitizer node" — both of
|
||||
/// which miss validated/dominated/early-return safety where the
|
||||
/// engine cleared the flow without firing or hitting an explicit
|
||||
/// sanitiser.
|
||||
///
|
||||
/// Reset at start of `analyse_file` (mirrors the existing
|
||||
/// path-safe span lifecycle); drained inside
|
||||
/// `TaintSuppressionCtx::build`.
|
||||
static ALL_VALIDATED_SPANS: RefCell<std::collections::HashSet<(usize, usize)>> =
|
||||
RefCell::new(std::collections::HashSet::new());
|
||||
}
|
||||
|
||||
/// Record an engine note for the body currently being analysed. Safe to
|
||||
|
|
@ -201,6 +225,33 @@ pub fn take_path_safe_suppressed_spans() -> std::collections::HashSet<(usize, us
|
|||
PATH_SAFE_SUPPRESSED_SPANS.with(|c| std::mem::take(&mut *c.borrow_mut()))
|
||||
}
|
||||
|
||||
/// Record a sink CFG-node span where the SSA engine proved every
|
||||
/// tainted input was validated (`SsaTaintEvent::all_validated`).
|
||||
/// Cap-agnostic — fires for any sink the engine evaluated and cleared.
|
||||
/// Consumed by `TaintSuppressionCtx::build` as positive evidence that
|
||||
/// taint analysis reached this line and proved safety, so AST-pattern
|
||||
/// findings on the same line can be suppressed without misclassifying
|
||||
/// silent engine failures as "safe by validation".
|
||||
pub(crate) fn record_all_validated_span(span: (usize, usize)) {
|
||||
ALL_VALIDATED_SPANS.with(|c| {
|
||||
c.borrow_mut().insert(span);
|
||||
});
|
||||
}
|
||||
|
||||
/// Reset the file-level all-validated sink-span set. Called at the
|
||||
/// start of `analyse_file` alongside `reset_path_safe_suppressed_spans`
|
||||
/// so each file scan starts clean.
|
||||
pub fn reset_all_validated_spans() {
|
||||
ALL_VALIDATED_SPANS.with(|c| c.borrow_mut().clear());
|
||||
}
|
||||
|
||||
/// Take the file-level all-validated sink-span set, leaving it empty.
|
||||
/// Called inside `TaintSuppressionCtx::build` to attribute each
|
||||
/// validated sink to its enclosing function for the suppression gate.
|
||||
pub fn take_all_validated_spans() -> std::collections::HashSet<(usize, usize)> {
|
||||
ALL_VALIDATED_SPANS.with(|c| std::mem::take(&mut *c.borrow_mut()))
|
||||
}
|
||||
|
||||
/// Stable identity for a variable binding at body boundaries.
|
||||
///
|
||||
/// Translates between independent per-body `SymbolId` spaces.
|
||||
|
|
@ -246,6 +297,52 @@ pub fn seed_lookup<'a>(
|
|||
|
||||
// ── SSA Taint State ─────────────────────────────────────────────────────
|
||||
|
||||
/// Compact key for a heap-field taint cell.
|
||||
///
|
||||
/// `(loc, field)` — `loc` is the abstract location of the *parent*
|
||||
/// (interned by the body's [`crate::pointer::LocInterner`]), `field`
|
||||
/// is the [`FieldId`] of the projected field. The pair survives lattice
|
||||
/// joins / leq comparisons by `Ord`-derived sort.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct FieldTaintKey {
|
||||
pub loc: LocId,
|
||||
pub field: FieldId,
|
||||
}
|
||||
|
||||
/// Pointer-Phase 4 / W4: per-field-cell taint record.
|
||||
///
|
||||
/// Carries the union of writers' taint for the abstract field cell plus
|
||||
/// two validation channels:
|
||||
/// * `validated_must` — set when *every* writer recorded a value that was
|
||||
/// `validated_must` in its own SSA scope. Lattice join intersects
|
||||
/// (`AND`) — matching the symbol-keyed [`SsaTaintState::validated_must`]
|
||||
/// semantics for "validated on every path".
|
||||
/// * `validated_may` — set when *any* writer recorded a `validated_may`
|
||||
/// value. Lattice join unions (`OR`) — matching the symbol-keyed
|
||||
/// [`SsaTaintState::validated_may`] semantics for "validated on some
|
||||
/// path".
|
||||
///
|
||||
/// Cells with `taint.caps == empty()` never get inserted (see
|
||||
/// [`SsaTaintState::add_field`]) so the lattice bottom remains `[]`.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct FieldCell {
|
||||
pub taint: VarTaint,
|
||||
pub validated_must: bool,
|
||||
pub validated_may: bool,
|
||||
}
|
||||
|
||||
impl FieldCell {
|
||||
/// Construct a cell with no validation bits — convenience for the
|
||||
/// pre-W4 callers that don't propagate symbol-level validation.
|
||||
pub fn unvalidated(taint: VarTaint) -> Self {
|
||||
Self {
|
||||
taint,
|
||||
validated_must: false,
|
||||
validated_may: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Taint state keyed by SsaValue instead of SymbolId.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SsaTaintState {
|
||||
|
|
@ -268,6 +365,20 @@ pub struct SsaTaintState {
|
|||
/// interpretation is disabled (`analysis.engine.abstract_interpretation
|
||||
/// = false`).
|
||||
pub abstract_state: Option<AbstractState>,
|
||||
/// Pointer-Phase 3: per-heap-field taint cells, keyed by
|
||||
/// `(parent_loc, field)`. Sorted by [`FieldTaintKey`] for O(n)
|
||||
/// merge-join. Populated only when the body's
|
||||
/// [`crate::pointer::PointsToFacts`] is available
|
||||
/// (`NYX_POINTER_ANALYSIS=1`); empty otherwise so the lattice join
|
||||
/// is a strict no-op for pointer-disabled runs. Field reads
|
||||
/// (`SsaOp::FieldProj`) consult the cells; field writes record into
|
||||
/// them. Cross-call propagation lands in Phase 5 via the
|
||||
/// field-granularity `PointsToSummary`.
|
||||
///
|
||||
/// Cell shape (Phase 4 / W4): [`FieldCell`] carries `taint` plus
|
||||
/// `validated_must` / `validated_may` flags so validation flows
|
||||
/// through abstract field / element identity.
|
||||
pub field_taint: SmallVec<[(FieldTaintKey, FieldCell); 4]>,
|
||||
}
|
||||
|
||||
impl SsaTaintState {
|
||||
|
|
@ -288,6 +399,64 @@ impl SsaTaintState {
|
|||
} else {
|
||||
None
|
||||
},
|
||||
field_taint: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Pointer-Phase 3: read the field cell at `key`. Returns `None`
|
||||
/// when no cell has been recorded (caller should treat as
|
||||
/// untainted). O(log n) on the sorted [`field_taint`] list.
|
||||
pub fn get_field(&self, key: FieldTaintKey) -> Option<&FieldCell> {
|
||||
self.field_taint
|
||||
.binary_search_by_key(&key, |(k, _)| *k)
|
||||
.ok()
|
||||
.map(|idx| &self.field_taint[idx].1)
|
||||
}
|
||||
|
||||
/// Pointer-Phase 3 / W4: union `t` into the field cell at `key`,
|
||||
/// recording per-write `validated_must` / `validated_may` channels.
|
||||
///
|
||||
/// Maintains sorted invariant. No-op when `t.caps` is empty (so the
|
||||
/// lattice bottom stays `[]`). When the cell already exists, the
|
||||
/// validation channels merge with the lattice-join semantics —
|
||||
/// `must` AND-intersects, `may` OR-unions — matching the symbol-
|
||||
/// keyed [`SsaTaintState::validated_must`] / `validated_may`
|
||||
/// semantics so a write coming through a non-validated path tears
|
||||
/// down `must` while preserving `may` of any earlier validated path.
|
||||
pub fn add_field(
|
||||
&mut self,
|
||||
key: FieldTaintKey,
|
||||
t: VarTaint,
|
||||
validated_must: bool,
|
||||
validated_may: bool,
|
||||
) {
|
||||
if t.caps.is_empty() {
|
||||
return;
|
||||
}
|
||||
match self.field_taint.binary_search_by_key(&key, |(k, _)| *k) {
|
||||
Ok(idx) => {
|
||||
let cell = &mut self.field_taint[idx].1;
|
||||
cell.taint.caps |= t.caps;
|
||||
cell.taint.uses_summary |= t.uses_summary;
|
||||
let merged = merge_origins(&cell.taint.origins, &t.origins);
|
||||
cell.taint.origins = merged;
|
||||
// Lattice-join semantics on a fresh write joining an
|
||||
// existing cell: must AND-intersects (a single un-
|
||||
// validated writer breaks the invariant); may OR-unions.
|
||||
cell.validated_must &= validated_must;
|
||||
cell.validated_may |= validated_may;
|
||||
}
|
||||
Err(idx) => self.field_taint.insert(
|
||||
idx,
|
||||
(
|
||||
key,
|
||||
FieldCell {
|
||||
taint: t,
|
||||
validated_must,
|
||||
validated_may,
|
||||
},
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -337,6 +506,7 @@ impl Lattice for SsaTaintState {
|
|||
(Some(a), Some(b)) => Some(a.join(b)),
|
||||
_ => None,
|
||||
};
|
||||
let field_taint = merge_join_field_taint(&self.field_taint, &other.field_taint);
|
||||
SsaTaintState {
|
||||
values,
|
||||
validated_must,
|
||||
|
|
@ -345,6 +515,7 @@ impl Lattice for SsaTaintState {
|
|||
heap,
|
||||
path_env,
|
||||
abstract_state,
|
||||
field_taint,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -361,6 +532,9 @@ impl Lattice for SsaTaintState {
|
|||
if !self.heap.leq(&other.heap) {
|
||||
return false;
|
||||
}
|
||||
if !field_taint_leq(&self.field_taint, &other.field_taint) {
|
||||
return false;
|
||||
}
|
||||
// path_env: None (Top) ≥ everything; Some(a) ≤ None only if a is Top-equivalent
|
||||
match (&self.path_env, &other.path_env) {
|
||||
(None, Some(_)) => return false, // Top is NOT ≤ constrained
|
||||
|
|
@ -389,6 +563,126 @@ impl Lattice for SsaTaintState {
|
|||
}
|
||||
}
|
||||
|
||||
/// Pointer-Phase 3 / W4: merge-join two sorted `field_taint` lists.
|
||||
/// Same shape as [`merge_join_ssa_vars`] but keyed on [`FieldTaintKey`]:
|
||||
/// * `taint.caps` — OR-union
|
||||
/// * `taint.origins` — merged with cap-respecting de-dup
|
||||
/// * `taint.uses_summary` — OR-union
|
||||
/// * `validated_must` — AND-intersect (matches the symbol-keyed
|
||||
/// `validated_must` lattice: a path that didn't validate this cell
|
||||
/// breaks the invariant)
|
||||
/// * `validated_may` — OR-union (any path's validation contributes)
|
||||
pub(super) fn merge_join_field_taint(
|
||||
a: &[(FieldTaintKey, FieldCell)],
|
||||
b: &[(FieldTaintKey, FieldCell)],
|
||||
) -> SmallVec<[(FieldTaintKey, FieldCell); 4]> {
|
||||
let mut result = SmallVec::with_capacity(a.len().max(b.len()));
|
||||
let (mut i, mut j) = (0, 0);
|
||||
while i < a.len() && j < b.len() {
|
||||
match a[i].0.cmp(&b[j].0) {
|
||||
std::cmp::Ordering::Less => {
|
||||
// Cell present only in `a` — counterpart in `b` is the
|
||||
// lattice bottom (no validation, no taint), so:
|
||||
// must = a.must AND false = false
|
||||
// may = a.may OR false = a.may
|
||||
let mut cell = a[i].1.clone();
|
||||
cell.validated_must = false;
|
||||
result.push((a[i].0, cell));
|
||||
i += 1;
|
||||
}
|
||||
std::cmp::Ordering::Greater => {
|
||||
let mut cell = b[j].1.clone();
|
||||
cell.validated_must = false;
|
||||
result.push((b[j].0, cell));
|
||||
j += 1;
|
||||
}
|
||||
std::cmp::Ordering::Equal => {
|
||||
let caps = a[i].1.taint.caps | b[j].1.taint.caps;
|
||||
let origins = merge_origins(&a[i].1.taint.origins, &b[j].1.taint.origins);
|
||||
let uses_summary = a[i].1.taint.uses_summary || b[j].1.taint.uses_summary;
|
||||
let validated_must = a[i].1.validated_must && b[j].1.validated_must;
|
||||
let validated_may = a[i].1.validated_may || b[j].1.validated_may;
|
||||
result.push((
|
||||
a[i].0,
|
||||
FieldCell {
|
||||
taint: VarTaint {
|
||||
caps,
|
||||
origins,
|
||||
uses_summary,
|
||||
},
|
||||
validated_must,
|
||||
validated_may,
|
||||
},
|
||||
));
|
||||
i += 1;
|
||||
j += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
while i < a.len() {
|
||||
let mut cell = a[i].1.clone();
|
||||
cell.validated_must = false;
|
||||
result.push((a[i].0, cell));
|
||||
i += 1;
|
||||
}
|
||||
while j < b.len() {
|
||||
let mut cell = b[j].1.clone();
|
||||
cell.validated_must = false;
|
||||
result.push((b[j].0, cell));
|
||||
j += 1;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// `a ≤ b` for sorted `field_taint` lists. Used by the convergence
|
||||
/// check in [`Lattice::leq`]. Per-cell criteria:
|
||||
///
|
||||
/// * `taint.caps` — `a ⊆ b` (sub-state on caps; matches per-SSA-value
|
||||
/// `ssa_vars_leq`).
|
||||
/// * `validated_must` — `a.must ⊇ b.must` (super-state on must; same
|
||||
/// shape as the symbol-keyed `validated_must` leq).
|
||||
/// * `validated_may` — `a.may ⊆ b.may` (sub-state on may).
|
||||
///
|
||||
/// When `b` lacks a key present in `a`, `b`'s side is the lattice
|
||||
/// bottom: no caps, no validation. `a`'s caps must also be empty
|
||||
/// AND `a.validated_must == false` for `a ≤ b` to hold, otherwise `a`
|
||||
/// is strictly greater than `b` on that cell.
|
||||
pub(super) fn field_taint_leq(
|
||||
a: &[(FieldTaintKey, FieldCell)],
|
||||
b: &[(FieldTaintKey, FieldCell)],
|
||||
) -> bool {
|
||||
let mut j = 0;
|
||||
for (key, ca) in a {
|
||||
while j < b.len() && b[j].0 < *key {
|
||||
j += 1;
|
||||
}
|
||||
if j >= b.len() || b[j].0 != *key {
|
||||
// Key absent in b ⇒ b's value is bottom for this cell;
|
||||
// a's caps must also be empty AND a.must = false.
|
||||
if !ca.taint.caps.is_empty() || ca.validated_must {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let cb = &b[j].1;
|
||||
// Caps: a ⊆ b.
|
||||
if (ca.taint.caps - cb.taint.caps).bits() != 0 {
|
||||
return false;
|
||||
}
|
||||
// Must: a ⊇ b — every must-validated key in b is must-validated
|
||||
// in a. Equivalently: !cb.must OR ca.must.
|
||||
if cb.validated_must && !ca.validated_must {
|
||||
return false;
|
||||
}
|
||||
// May: a ⊆ b — every may-validated key in a is may-validated
|
||||
// in b. Equivalently: !ca.may OR cb.may.
|
||||
if ca.validated_may && !cb.validated_may {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Merge-join two sorted SSA var lists.
|
||||
pub(super) fn merge_join_ssa_vars(
|
||||
a: &[(SsaValue, VarTaint)],
|
||||
|
|
@ -756,3 +1050,383 @@ mod origin_cap_tests {
|
|||
set_max_origins_override(0);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod field_taint_tests {
|
||||
//! Pointer-Phase 3: tests for the heap-field taint cells on
|
||||
//! [`SsaTaintState`]. Cover get/add round-trip, lattice join
|
||||
//! (cap union + origin merge), and `leq` convergence semantics.
|
||||
use super::*;
|
||||
use crate::labels::Cap;
|
||||
use crate::pointer::LocId;
|
||||
use crate::ssa::ir::FieldId;
|
||||
use crate::taint::domain::TaintOrigin;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
fn key(loc_raw: u32, field_raw: u32) -> FieldTaintKey {
|
||||
FieldTaintKey {
|
||||
loc: LocId(loc_raw),
|
||||
field: FieldId(field_raw),
|
||||
}
|
||||
}
|
||||
|
||||
fn taint(caps: Cap) -> VarTaint {
|
||||
VarTaint {
|
||||
caps,
|
||||
origins: SmallVec::new(),
|
||||
uses_summary: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience helper: pre-W4 `add_field` calls didn't carry
|
||||
/// validation channels. The new signature accepts them explicitly;
|
||||
/// pre-W4 tests pass `(false, false)` to preserve the original
|
||||
/// semantics.
|
||||
fn add(s: &mut SsaTaintState, k: FieldTaintKey, t: VarTaint) {
|
||||
s.add_field(k, t, false, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_field_round_trips() {
|
||||
let mut s = SsaTaintState::initial();
|
||||
let k = key(1, 7);
|
||||
add(&mut s, k, taint(Cap::ENV_VAR));
|
||||
let got = s.get_field(k).expect("field cell present");
|
||||
assert!(got.taint.caps.contains(Cap::ENV_VAR));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_field_unions_caps() {
|
||||
let mut s = SsaTaintState::initial();
|
||||
let k = key(1, 7);
|
||||
add(&mut s, k, taint(Cap::ENV_VAR));
|
||||
add(&mut s, k, taint(Cap::ENV_VAR));
|
||||
let got = s.get_field(k).unwrap();
|
||||
assert!(got.taint.caps.contains(Cap::ENV_VAR));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_field_skips_empty_caps() {
|
||||
let mut s = SsaTaintState::initial();
|
||||
let k = key(2, 3);
|
||||
add(&mut s, k, taint(Cap::empty()));
|
||||
assert!(s.get_field(k).is_none(), "empty caps must not insert");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lattice_join_unions_keys_and_caps() {
|
||||
let k1 = key(1, 7);
|
||||
let k2 = key(2, 9);
|
||||
let mut a = SsaTaintState::initial();
|
||||
let mut b = SsaTaintState::initial();
|
||||
add(&mut a, k1, taint(Cap::ENV_VAR));
|
||||
add(&mut b, k1, taint(Cap::ENV_VAR));
|
||||
add(&mut b, k2, taint(Cap::FILE_IO));
|
||||
let joined = a.join(&b);
|
||||
let v1 = joined.get_field(k1).unwrap();
|
||||
assert!(v1.taint.caps.contains(Cap::ENV_VAR));
|
||||
let v2 = joined.get_field(k2).unwrap();
|
||||
assert!(v2.taint.caps.contains(Cap::FILE_IO));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lattice_leq_detects_strict_increase() {
|
||||
// a is empty; b has a cell. Empty ≤ any state, so a.leq(b)
|
||||
// holds. b ≤ a fails because b has a cell with non-empty caps
|
||||
// that a lacks.
|
||||
let mut b = SsaTaintState::initial();
|
||||
add(&mut b, key(1, 7), taint(Cap::ENV_VAR));
|
||||
let a = SsaTaintState::initial();
|
||||
assert!(a.leq(&b), "empty state ≤ state with a field cell");
|
||||
assert!(!b.leq(&a), "state with a field cell is NOT ≤ empty state");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lattice_leq_holds_when_caps_subset() {
|
||||
let k = key(3, 4);
|
||||
let mut a = SsaTaintState::initial();
|
||||
let mut b = SsaTaintState::initial();
|
||||
add(&mut a, k, taint(Cap::ENV_VAR));
|
||||
add(&mut b, k, taint(Cap::ENV_VAR | Cap::FILE_IO));
|
||||
assert!(a.leq(&b));
|
||||
assert!(!b.leq(&a));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_origins_via_join_dedups_by_node() {
|
||||
use petgraph::graph::NodeIndex;
|
||||
let k = key(1, 1);
|
||||
let o1 = TaintOrigin {
|
||||
node: NodeIndex::new(5),
|
||||
source_kind: crate::labels::SourceKind::UserInput,
|
||||
source_span: Some((0, 1)),
|
||||
};
|
||||
let o2 = TaintOrigin {
|
||||
node: NodeIndex::new(7),
|
||||
source_kind: crate::labels::SourceKind::EnvironmentConfig,
|
||||
source_span: Some((10, 11)),
|
||||
};
|
||||
let mut t1 = taint(Cap::ENV_VAR);
|
||||
t1.origins.push(o1);
|
||||
let mut t2 = taint(Cap::ENV_VAR);
|
||||
t2.origins.push(o1);
|
||||
t2.origins.push(o2);
|
||||
|
||||
let mut a = SsaTaintState::initial();
|
||||
let mut b = SsaTaintState::initial();
|
||||
add(&mut a, k, t1);
|
||||
add(&mut b, k, t2);
|
||||
let joined = a.join(&b);
|
||||
let cell = joined.get_field(k).unwrap();
|
||||
// Both origins survive; the duplicate o1 dedups.
|
||||
assert_eq!(cell.taint.origins.len(), 2);
|
||||
let nodes: Vec<_> = cell.taint.origins.iter().map(|o| o.node).collect();
|
||||
assert!(nodes.contains(&NodeIndex::new(5)));
|
||||
assert!(nodes.contains(&NodeIndex::new(7)));
|
||||
}
|
||||
|
||||
/// W4 audit: `merge_join_field_taint` AND-intersects
|
||||
/// `validated_must` when joining cells with the same key. Two
|
||||
/// states whose paths each independently must-validate the cell
|
||||
/// keep `must = true`; if either path doesn't validate, `must`
|
||||
/// drops to false on the join.
|
||||
#[test]
|
||||
fn lattice_validated_must_intersects_on_join() {
|
||||
let k = key(1, 7);
|
||||
let mut a = SsaTaintState::initial();
|
||||
let mut b = SsaTaintState::initial();
|
||||
a.add_field(k, taint(Cap::ENV_VAR), true, true);
|
||||
b.add_field(k, taint(Cap::ENV_VAR), true, true);
|
||||
let joined_aa = a.join(&b);
|
||||
let cell = joined_aa.get_field(k).unwrap();
|
||||
assert!(cell.validated_must, "a.must AND b.must = true");
|
||||
assert!(cell.validated_may);
|
||||
|
||||
// Now make `b`'s validated_must false — must should drop to
|
||||
// false on the join, may stays at OR.
|
||||
let mut c = SsaTaintState::initial();
|
||||
c.add_field(k, taint(Cap::ENV_VAR), false, true);
|
||||
let joined_ac = a.join(&c);
|
||||
let cell2 = joined_ac.get_field(k).unwrap();
|
||||
assert!(!cell2.validated_must, "a.must AND c.must = false");
|
||||
assert!(cell2.validated_may, "a.may OR c.may = true");
|
||||
}
|
||||
|
||||
/// W4 audit: `merge_join_field_taint` OR-unions `validated_may`
|
||||
/// — any path's may-validation contributes to the joined cell.
|
||||
#[test]
|
||||
fn lattice_validated_may_unions_on_join() {
|
||||
let k = key(1, 7);
|
||||
let mut a = SsaTaintState::initial();
|
||||
let mut b = SsaTaintState::initial();
|
||||
a.add_field(k, taint(Cap::ENV_VAR), false, false);
|
||||
b.add_field(k, taint(Cap::ENV_VAR), false, true);
|
||||
let joined = a.join(&b);
|
||||
let cell = joined.get_field(k).unwrap();
|
||||
assert!(!cell.validated_must);
|
||||
assert!(cell.validated_may, "a.may OR b.may = true");
|
||||
}
|
||||
|
||||
/// W4 audit: when one side of the join lacks the key, the
|
||||
/// counterpart's validated_must drops to false (intersection with
|
||||
/// the lattice bottom's `must = false`); validated_may is preserved
|
||||
/// (`OR false = self`). Caps and origins are preserved.
|
||||
#[test]
|
||||
fn lattice_validated_consistent_with_taint_join() {
|
||||
let k = key(2, 11);
|
||||
let mut a = SsaTaintState::initial();
|
||||
let b = SsaTaintState::initial();
|
||||
a.add_field(k, taint(Cap::ENV_VAR), true, true);
|
||||
let joined = a.join(&b);
|
||||
let cell = joined.get_field(k).unwrap();
|
||||
assert!(
|
||||
!cell.validated_must,
|
||||
"joined with empty side must drop validated_must"
|
||||
);
|
||||
assert!(
|
||||
cell.validated_may,
|
||||
"joined with empty side keeps validated_may"
|
||||
);
|
||||
assert!(cell.taint.caps.contains(Cap::ENV_VAR));
|
||||
|
||||
// Symmetric: empty.join(populated) yields the same cell shape.
|
||||
let joined2 = b.join(&a);
|
||||
let cell2 = joined2.get_field(k).unwrap();
|
||||
assert!(!cell2.validated_must);
|
||||
assert!(cell2.validated_may);
|
||||
}
|
||||
|
||||
/// W4 audit: `field_taint_leq` respects both validation channels.
|
||||
/// `must` is super-state (a.must ⊇ b.must); `may` is sub-state
|
||||
/// (a.may ⊆ b.may). A state strictly "smaller" on caps but
|
||||
/// strictly "larger" on may must NOT compare ≤.
|
||||
#[test]
|
||||
fn lattice_leq_respects_validated_channels() {
|
||||
let k = key(3, 5);
|
||||
|
||||
// Case 1: a has must=true, b has must=false. a.must ⊇ b.must
|
||||
// holds (true ⊇ false), so a ≤ b on this channel. But b's
|
||||
// caps must dominate a's for a ≤ b overall.
|
||||
let mut a = SsaTaintState::initial();
|
||||
let mut b = SsaTaintState::initial();
|
||||
a.add_field(k, taint(Cap::ENV_VAR), true, false);
|
||||
b.add_field(k, taint(Cap::ENV_VAR), false, false);
|
||||
assert!(
|
||||
a.leq(&b),
|
||||
"must super-state and equal caps: a ≤ b should hold"
|
||||
);
|
||||
// Reverse: b.must=false, a.must=true — for b ≤ a, we need
|
||||
// b.must ⊇ a.must which is false ⊇ true = false. So b ≤ a
|
||||
// must fail.
|
||||
assert!(!b.leq(&a), "b lacks the must invariant a holds");
|
||||
|
||||
// Case 2: a has may=true, b has may=false. a.may ⊆ b.may
|
||||
// requires true ⊆ false = false, so a ≤ b must fail.
|
||||
let mut a2 = SsaTaintState::initial();
|
||||
let mut b2 = SsaTaintState::initial();
|
||||
a2.add_field(k, taint(Cap::ENV_VAR), false, true);
|
||||
b2.add_field(k, taint(Cap::ENV_VAR), false, false);
|
||||
assert!(!a2.leq(&b2), "a.may=true is NOT ⊆ b.may=false");
|
||||
}
|
||||
|
||||
/// Pointer-Phase 3 / A8 audit: the field_taint lattice is monotone
|
||||
/// and converges under a deterministic enumeration of inputs.
|
||||
/// Caps grow (OR), `uses_summary` grows (OR), origins grow modulo
|
||||
/// the cap (merge_origins is bounded). Joins must:
|
||||
/// 1. Be commutative: join(a, b) == join(b, a).
|
||||
/// 2. Be associative: join(join(a, b), c) == join(a, join(b, c)).
|
||||
/// 3. Be idempotent: join(a, a) == a.
|
||||
/// 4. Reach a fixed point in at most |unique cells| iterations.
|
||||
#[test]
|
||||
fn lattice_converges_under_deterministic_enumeration() {
|
||||
use crate::labels::Cap;
|
||||
use petgraph::graph::NodeIndex;
|
||||
|
||||
// Build N distinct (key, taint) pairs.
|
||||
let inputs: Vec<(FieldTaintKey, VarTaint)> = (0..6)
|
||||
.map(|i| {
|
||||
let key = FieldTaintKey {
|
||||
loc: LocId(1 + (i % 3) as u32),
|
||||
field: FieldId((i % 4) as u32),
|
||||
};
|
||||
let taint = VarTaint {
|
||||
caps: if i % 2 == 0 {
|
||||
Cap::ENV_VAR
|
||||
} else {
|
||||
Cap::FILE_IO
|
||||
},
|
||||
origins: smallvec::SmallVec::from_iter([TaintOrigin {
|
||||
node: NodeIndex::new(i + 10),
|
||||
source_kind: crate::labels::SourceKind::UserInput,
|
||||
source_span: Some((i * 5, i * 5 + 2)),
|
||||
}]),
|
||||
uses_summary: false,
|
||||
};
|
||||
(key, taint)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Build a list of states, each with a single (key, taint) pair.
|
||||
let states: Vec<SsaTaintState> = inputs
|
||||
.iter()
|
||||
.map(|(k, t)| {
|
||||
let mut s = SsaTaintState::initial();
|
||||
add(&mut s, *k, t.clone());
|
||||
s
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Compute LUB by folding `join` over `states`.
|
||||
let lub = states
|
||||
.iter()
|
||||
.skip(1)
|
||||
.fold(states[0].clone(), |acc, s| acc.join(s));
|
||||
|
||||
// 1. Commutativity: join(a, b) == join(b, a).
|
||||
for i in 0..states.len() {
|
||||
for j in (i + 1)..states.len() {
|
||||
let ab = states[i].join(&states[j]);
|
||||
let ba = states[j].join(&states[i]);
|
||||
assert_eq!(
|
||||
ab, ba,
|
||||
"join must commute: states[{i}] ⊕ states[{j}] != states[{j}] ⊕ states[{i}]",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Associativity: ((a ⊕ b) ⊕ c) == (a ⊕ (b ⊕ c)).
|
||||
for i in 0..states.len() {
|
||||
for j in 0..states.len() {
|
||||
for k in 0..states.len() {
|
||||
let a = &states[i];
|
||||
let b = &states[j];
|
||||
let c = &states[k];
|
||||
let left = a.join(b).join(c);
|
||||
let right = a.join(&b.join(c));
|
||||
assert_eq!(
|
||||
left, right,
|
||||
"join must associate: states[{i},{j},{k}] left vs right",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Idempotency: lub ⊕ lub == lub, lub ⊕ s == lub for every input s.
|
||||
let lub_lub = lub.join(&lub);
|
||||
assert_eq!(lub, lub_lub, "lub must be idempotent under self-join");
|
||||
for (i, s) in states.iter().enumerate() {
|
||||
let merged = lub.join(s);
|
||||
assert_eq!(
|
||||
lub, merged,
|
||||
"lub.join(states[{i}]) must equal lub (s ≤ lub)",
|
||||
);
|
||||
}
|
||||
|
||||
// 4. Convergence within a bounded number of iterations. The
|
||||
// worklist tightens after each input is folded in; once every
|
||||
// unique key has been seen, further folds are no-ops.
|
||||
let mut acc = SsaTaintState::initial();
|
||||
let mut iter_count = 0;
|
||||
loop {
|
||||
iter_count += 1;
|
||||
if iter_count > inputs.len() + 4 {
|
||||
panic!("lattice did not converge within bounded iterations");
|
||||
}
|
||||
let mut next = acc.clone();
|
||||
for s in &states {
|
||||
next = next.join(s);
|
||||
}
|
||||
if next.field_taint == acc.field_taint {
|
||||
break;
|
||||
}
|
||||
acc = next;
|
||||
}
|
||||
assert_eq!(
|
||||
acc, lub,
|
||||
"iterative fold must converge to the lub regardless of order",
|
||||
);
|
||||
}
|
||||
|
||||
/// `field_taint_leq` is the soundness gate for worklist
|
||||
/// convergence: once `next ≤ acc`, the worklist halts. Pin that
|
||||
/// `leq` is consistent with `join` — i.e. `s.leq(s.join(t))` holds
|
||||
/// for any `s, t`. Without this, the worklist could loop
|
||||
/// indefinitely on inputs whose join produces a state not
|
||||
/// dominated by both inputs.
|
||||
#[test]
|
||||
fn lattice_leq_consistent_with_join() {
|
||||
use crate::labels::Cap;
|
||||
let mut a = SsaTaintState::initial();
|
||||
let mut b = SsaTaintState::initial();
|
||||
add(&mut a, key(1, 7), taint(Cap::ENV_VAR));
|
||||
add(&mut b, key(1, 7), taint(Cap::FILE_IO));
|
||||
add(&mut b, key(2, 9), taint(Cap::SHELL_ESCAPE));
|
||||
let j = a.join(&b);
|
||||
assert!(a.leq(&j), "a ≤ a ⊕ b");
|
||||
assert!(b.leq(&j), "b ≤ a ⊕ b");
|
||||
// Reflexive: x ≤ x.
|
||||
assert!(a.leq(&a));
|
||||
assert!(b.leq(&b));
|
||||
assert!(j.leq(&j));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,49 @@ pub fn extract_ssa_func_summary(
|
|||
module_aliases: Option<&HashMap<SsaValue, SmallVec<[String; 2]>>>,
|
||||
locator: Option<&crate::summary::SinkSiteLocator<'_>>,
|
||||
formal_param_names: Option<&[String]>,
|
||||
) -> crate::summary::ssa_summary::SsaFuncSummary {
|
||||
extract_ssa_func_summary_full(
|
||||
ssa,
|
||||
cfg,
|
||||
local_summaries,
|
||||
global_summaries,
|
||||
lang,
|
||||
namespace,
|
||||
interner,
|
||||
param_count,
|
||||
module_aliases,
|
||||
locator,
|
||||
formal_param_names,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
/// Like [`extract_ssa_func_summary`] but allows passing an in-progress
|
||||
/// `ssa_summaries` map so the per-parameter probes can resolve callee
|
||||
/// SSA summaries via step 0 of `resolve_callee_full`.
|
||||
///
|
||||
/// This enables transitive cross-function summary propagation: when a
|
||||
/// caller's body references a callee whose summary was just augmented
|
||||
/// by the closure-capture lift pass, the caller's probe sees the
|
||||
/// augmented `param_to_sink` and can propagate it onto the caller's
|
||||
/// own summary. Used by `lower_all_functions_from_bodies`'s second
|
||||
/// extraction pass after `augment_summaries_with_child_sinks`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn extract_ssa_func_summary_full(
|
||||
ssa: &SsaBody,
|
||||
cfg: &Cfg,
|
||||
local_summaries: &FuncSummaries,
|
||||
global_summaries: Option<&GlobalSummaries>,
|
||||
lang: Lang,
|
||||
namespace: &str,
|
||||
interner: &crate::state::symbol::SymbolInterner,
|
||||
param_count: usize,
|
||||
module_aliases: Option<&HashMap<SsaValue, SmallVec<[String; 2]>>>,
|
||||
locator: Option<&crate::summary::SinkSiteLocator<'_>>,
|
||||
formal_param_names: Option<&[String]>,
|
||||
ssa_summaries: Option<
|
||||
&HashMap<crate::symbol::FuncKey, crate::summary::ssa_summary::SsaFuncSummary>,
|
||||
>,
|
||||
) -> crate::summary::ssa_summary::SsaFuncSummary {
|
||||
use crate::summary::SinkSite;
|
||||
use crate::summary::ssa_summary::{SsaFuncSummary, TaintTransform};
|
||||
|
|
@ -143,7 +186,7 @@ pub fn extract_ssa_func_summary(
|
|||
receiver_seed: None,
|
||||
const_values: None,
|
||||
type_facts: None,
|
||||
ssa_summaries: None,
|
||||
ssa_summaries,
|
||||
extra_labels: None,
|
||||
base_aliases: None,
|
||||
callee_bodies: None,
|
||||
|
|
@ -158,6 +201,7 @@ pub fn extract_ssa_func_summary(
|
|||
static_map: None,
|
||||
auto_seed_handler_params: false,
|
||||
cross_file_bodies: None,
|
||||
pointer_facts: None,
|
||||
};
|
||||
|
||||
let (events, block_states) = run_ssa_taint_full(ssa, cfg, &transfer);
|
||||
|
|
@ -358,15 +402,49 @@ pub fn extract_ssa_func_summary(
|
|||
source_kind: SourceKind::UserInput,
|
||||
source_span: None,
|
||||
};
|
||||
let probe_taint = VarTaint {
|
||||
caps: Cap::all(),
|
||||
origins: SmallVec::from_elem(origin, 1),
|
||||
uses_summary: false,
|
||||
};
|
||||
seed.insert(
|
||||
BindingKey::new(var_name.as_str(), BodyId(0)),
|
||||
VarTaint {
|
||||
caps: Cap::all(),
|
||||
origins: SmallVec::from_elem(origin, 1),
|
||||
uses_summary: false,
|
||||
},
|
||||
probe_taint.clone(),
|
||||
);
|
||||
|
||||
// Phantom-Param prefix seeding. SSA lowering of arrow / nested
|
||||
// function bodies often exposes free-identifier member-access
|
||||
// expressions (e.g. `file._source.uri`) as their own
|
||||
// [`SsaOp::Param`] ops with composite `var_name`s like
|
||||
// `"file._source.uri"`. These phantom Params are the values
|
||||
// actually used as call arguments — not the formal-param SSA
|
||||
// value the seed targets. Without this, the per-param probe
|
||||
// misses cross-call sinks because the call's arg SSA value is
|
||||
// a phantom Param with no seed entry, so `transfer_inst::Param`
|
||||
// leaves it untainted and `collect_tainted_sink_values`
|
||||
// observes empty caps despite the formal param being seeded.
|
||||
//
|
||||
// Seed every phantom Param whose `var_name` begins with
|
||||
// `formal_var_name + "."` with the same caps the formal param
|
||||
// received: semantically "if `file` is tainted, then every
|
||||
// observable field path on `file` is tainted too". Bounded
|
||||
// by SSA size; cap-equivalent to direct seeding.
|
||||
let prefix = format!("{}.", var_name);
|
||||
for block in &ssa.blocks {
|
||||
for inst in block.phis.iter().chain(block.body.iter()) {
|
||||
if let SsaOp::Param { .. } = &inst.op {
|
||||
if let Some(name) = inst.var_name.as_ref() {
|
||||
if name.starts_with(&prefix) {
|
||||
seed.insert(
|
||||
BindingKey::new(name.as_str(), BodyId(0)),
|
||||
probe_taint.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let (return_caps, events, _, per_return_obs) = run_probe(seed);
|
||||
|
||||
// Subtract baseline source_caps — we only want param-contributed caps
|
||||
|
|
@ -493,7 +571,7 @@ pub fn extract_ssa_func_summary(
|
|||
receiver_seed: None,
|
||||
const_values: None,
|
||||
type_facts: None,
|
||||
ssa_summaries: None,
|
||||
ssa_summaries,
|
||||
extra_labels: None,
|
||||
base_aliases: None,
|
||||
callee_bodies: None,
|
||||
|
|
@ -508,6 +586,7 @@ pub fn extract_ssa_func_summary(
|
|||
static_map: None,
|
||||
auto_seed_handler_params: false,
|
||||
cross_file_bodies: None,
|
||||
pointer_facts: None,
|
||||
};
|
||||
detect_source_to_callback_from_states(
|
||||
ssa,
|
||||
|
|
@ -551,6 +630,16 @@ pub fn extract_ssa_func_summary(
|
|||
param_return_paths,
|
||||
return_path_facts,
|
||||
points_to,
|
||||
// Pointer-Phase 5 extension — empty until the field-granularity
|
||||
// extractor is wired (`NYX_POINTER_ANALYSIS=1` only). Default
|
||||
// path stays bit-identical to today.
|
||||
field_points_to: crate::summary::points_to::FieldPointsToSummary::empty(),
|
||||
// Populated post-extraction in
|
||||
// `taint::lower_all_functions_from_bodies` once SSA optimisation
|
||||
// has computed `opt.type_facts`. Empty here means the
|
||||
// extractor itself doesn't carry receiver-type info — the
|
||||
// caller patches it in.
|
||||
typed_call_receivers: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -909,6 +998,7 @@ pub(crate) fn extract_container_flow_summary(
|
|||
callee,
|
||||
args,
|
||||
receiver,
|
||||
..
|
||||
} = &inst.op
|
||||
{
|
||||
let op = match classify_container_op(callee, lang) {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue