mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-06 19:35:13 +02:00
* refactor: Update comments for clarity and add expectations.json files for performance metrics * feat: Implement FP guard for JS/TS local-collection receivers to suppress missing ownership checks * feat: Enhance Rust parameter handling to classify local collections and prevent false ownership checks * refactor: Simplify code formatting for better readability in multiple files * refactor: Improve UTF-8 sequence length handling and enhance clarity in loop iteration * feat: Update Java and Python patterns to include new security rules * refactor: Improve comment clarity and consistency across multiple Rust files * refactor: Simplify code formatting for improved readability in integration tests and module files * refactor: Improve comment formatting and enhance clarity in assertions across multiple files
180 lines
7.5 KiB
Rust
180 lines
7.5 KiB
Rust
//! Regression tests for JS/TS in-file pass-2 convergence.
|
|
//!
|
|
//! Pass 2 is the Jacobi-style iteration that combines each non-toplevel
|
|
//! body's exit state (filtered to top-level keys) back into the shared
|
|
//! seed and re-runs non-toplevel bodies with the enlarged seed. The
|
|
//! hardcoded cap of `3` that used to live in `analyse_file` silently
|
|
//! truncated any file whose convergence required 4+ rounds, this
|
|
//! phase lifts the cap to [`JS_TS_PASS2_SAFETY_CAP`] (64), adds an
|
|
//! observability counter, and tags cap-hit findings with
|
|
//! [`EngineNote::InFileFixpointCapped`].
|
|
//!
|
|
//! Mirrors `tests/scc_convergence_tests.rs` in structure and intent.
|
|
//!
|
|
//! If you raise or lower the cap in `taint::JS_TS_PASS2_SAFETY_CAP`,
|
|
//! update the iteration-count assertions accordingly.
|
|
|
|
mod common;
|
|
|
|
use common::{scan_fixture_dir, validate_expectations};
|
|
use nyx_scanner::taint::{last_js_ts_pass2_iterations, set_js_ts_pass2_cap_override};
|
|
use nyx_scanner::utils::config::AnalysisMode;
|
|
use std::path::Path;
|
|
use std::sync::Mutex;
|
|
|
|
fn fixture_path(name: &str) -> std::path::PathBuf {
|
|
Path::new(env!("CARGO_MANIFEST_DIR"))
|
|
.join("tests/fixtures")
|
|
.join(name)
|
|
}
|
|
|
|
/// Serialize any test that mutates the pass-2 cap override or reads
|
|
/// `last_js_ts_pass2_iterations()`. The override is a process-wide
|
|
/// `AtomicUsize` and `cargo test` runs tests in parallel by default ,
|
|
/// without this guard, one test's override leaks into another's scan.
|
|
static PASS2_TEST_GUARD: Mutex<()> = Mutex::new(());
|
|
|
|
/// Five top-level `const` bindings threaded through four helper
|
|
/// functions. With the default cap of 64 this converges and the
|
|
/// `child_process.exec(stage4)` sink sees the transitive taint flow.
|
|
///
|
|
/// The test asserts both that the finding is reported and that the
|
|
/// observability counter surfaces a sensible value so future
|
|
/// regressions in the pass-2 plumbing (e.g. the counter being reset
|
|
/// or the cap being bypassed) are caught.
|
|
#[test]
|
|
fn js_ts_pass2_deep_chain_emits_transitive_finding() {
|
|
let _guard = PASS2_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
|
|
set_js_ts_pass2_cap_override(0); // ensure no stale override from a prior test
|
|
let dir = fixture_path("js_ts_pass2_deep_chain");
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
|
|
// Hard assertion: the transitive taint must be detected.
|
|
validate_expectations(&diags, &dir);
|
|
|
|
// Observability + load-bearing assertion. The fixture's
|
|
// cross-body global publishing means pass-2 has to do real work:
|
|
// at least two rounds are needed for `seed_handler`'s
|
|
// `globalG1` to reach `finalize_handler`'s sink, then one more
|
|
// round to confirm convergence. A drop to `1` means the pass-2
|
|
// loop is short-circuiting and this fixture is no longer
|
|
// load-bearing. An upper bound of `8` catches summary-monotonicity
|
|
// regressions that would churn near the safety cap of 64.
|
|
let iters = last_js_ts_pass2_iterations();
|
|
assert!(
|
|
(2..=8).contains(&iters),
|
|
"expected 2..=8 pass-2 iterations for the deep-chain fixture; \
|
|
got {iters}. Lower bound guards against pass-2 becoming a \
|
|
no-op on this fixture; upper bound guards against \
|
|
summary-monotonicity regressions.",
|
|
);
|
|
}
|
|
|
|
/// Override plumbing: verify that `set_js_ts_pass2_cap_override` binds
|
|
/// the effective cap and that restoring the default clears cleanly.
|
|
///
|
|
/// We use a cap of 1 (meaning `rounds == 0`, the pass-2 loop does not
|
|
/// enter). This is the sharpest possible override and exercises the
|
|
/// "cap bound to minimum" code path. The counter must then fall back
|
|
/// to the pass-1-only value of `1`.
|
|
#[test]
|
|
fn js_ts_pass2_cap_override_binds_effective_cap() {
|
|
let _guard = PASS2_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
|
|
let dir = fixture_path("js_ts_pass2_deep_chain");
|
|
|
|
// First scan with the cap forced to 1, the pass-2 loop does not
|
|
// enter at all (`max_iterations.saturating_sub(1) == 0`). The
|
|
// counter must report exactly `1` (the sentinel for "pass-1
|
|
// containment ran, no pass-2 iterations").
|
|
set_js_ts_pass2_cap_override(1);
|
|
let _ = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
let iters_capped = last_js_ts_pass2_iterations();
|
|
assert_eq!(
|
|
iters_capped, 1,
|
|
"cap=1 must short-circuit pass-2 to zero rounds; got {iters_capped} \
|
|
iterations. Check js_ts_pass2_cap() is wired into max_iterations.",
|
|
);
|
|
|
|
// Restore default and scan again. On this fixture pass-2 needs
|
|
// several rounds to converge, so the counter must now report a
|
|
// value strictly greater than the cap=1 short-circuit reading.
|
|
// This guards against the override "sticking" (e.g. if the
|
|
// override were stored into the cap const instead of a distinct
|
|
// atomic).
|
|
set_js_ts_pass2_cap_override(0);
|
|
let _ = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
let iters_default = last_js_ts_pass2_iterations();
|
|
assert!(
|
|
iters_default > iters_capped && iters_default <= 64,
|
|
"after clearing the override the counter must report a value \
|
|
strictly greater than the cap=1 reading ({iters_capped}); \
|
|
got {iters_default}",
|
|
);
|
|
}
|
|
|
|
/// Cap-hit engine-note emission.
|
|
///
|
|
/// When pass-2 exhausts its budget without detecting convergence,
|
|
/// every finding from the file must carry
|
|
/// [`EngineNote::InFileFixpointCapped`] so downstream reviewers can
|
|
/// identify potentially-imprecise results. The deep-chain fixture's
|
|
/// pass-2 seed actually grows between rounds (`seed_handler` publishes
|
|
/// `globalG1` to other bodies), so forcing the cap to `2` binds the
|
|
/// loop at a single round, the seed grew, no convergence was
|
|
/// detected, and the note path fires.
|
|
#[test]
|
|
fn js_ts_pass2_cap_hit_emits_engine_note() {
|
|
use nyx_scanner::engine_notes::EngineNote;
|
|
let _guard = PASS2_TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
|
|
let dir = fixture_path("js_ts_pass2_deep_chain");
|
|
|
|
// cap=2 → max_iterations=2, rounds=1. Round 0 combines
|
|
// `seed_handler`'s exit (which includes `globalG1`) into the
|
|
// seed, the seed grows from empty to 1 entry, so the
|
|
// convergence-equality branch does not fire. Loop exits with
|
|
// `converged_early = false`, note emission triggers.
|
|
set_js_ts_pass2_cap_override(2);
|
|
let diags = scan_fixture_dir(&dir, AnalysisMode::Full);
|
|
set_js_ts_pass2_cap_override(0);
|
|
|
|
let iters = last_js_ts_pass2_iterations();
|
|
assert_eq!(
|
|
iters, 1,
|
|
"expected cap-override (2) to bind the pass-2 loop at 1 round; \
|
|
got {iters} iterations",
|
|
);
|
|
|
|
// Every finding produced from the capped file must carry the note.
|
|
// Strict assertion: if *any* finding lacks the note, pass-2's
|
|
// per-finding note merging regressed.
|
|
assert!(
|
|
!diags.is_empty(),
|
|
"cap-hit scan must still emit diagnostics (truncation is not silent drop); \
|
|
got none",
|
|
);
|
|
for d in &diags {
|
|
let has_note = d
|
|
.evidence
|
|
.as_ref()
|
|
.map(|e| {
|
|
e.engine_notes
|
|
.iter()
|
|
.any(|n| matches!(n, EngineNote::InFileFixpointCapped { .. }))
|
|
})
|
|
.unwrap_or(false);
|
|
assert!(
|
|
has_note,
|
|
"every diag from a cap-hit pass-2 scan must carry \
|
|
EngineNote::InFileFixpointCapped; missing on {}:{}:{} id={} \
|
|
notes={:?}",
|
|
d.path,
|
|
d.line,
|
|
d.col,
|
|
d.id,
|
|
d.evidence
|
|
.as_ref()
|
|
.map(|e| e.engine_notes.clone())
|
|
.unwrap_or_default(),
|
|
);
|
|
}
|
|
}
|