diff --git a/benches/dynamic_bench.rs b/benches/dynamic_bench.rs index 4dae488b..631b6f03 100644 --- a/benches/dynamic_bench.rs +++ b/benches/dynamic_bench.rs @@ -1,6 +1,6 @@ /// Dynamic verification benchmarks (§8.4). /// -/// Tracks six cost anchors: +/// Tracks the per-scan cost anchors: /// /// 1. `harness_build_cold` — fresh workdir, spec → BuiltHarness (source gen + disk write). /// 2. `harness_build_warm` — same spec, workdir already staged (file write skipped). @@ -9,6 +9,25 @@ /// 4. `docker_image_build` — cold image pull/build for the python:3-slim base. /// 5. `docker_exec_warm` — `docker exec` into a running container (no cold start). /// 6. `docker_payload_cost` — per-payload sandbox cost via docker backend end-to-end. +/// 7. `composite_chain_reverify_dispatch` — `reverify_top_chains` on a +/// synthetic 3-member chain with no member diags. Measures the no-derive +/// dispatch path (chain_step_specs miss, early-exit build/run loops, +/// Inconclusive verdict allocation, severity downgrade). +/// 8. `composite_chain_reverify_stub_confirmed` — same chain shape, stubbed +/// reverifier returning `Confirmed`. Measures the apply-verdict happy path +/// (no severity bucket change). +/// 9. `composite_chain_reverify_top_n_slice` — 5-chain slice with `top_n=3`. +/// Measures the slice traversal cost so a regression that walks the full +/// slice instead of the prefix is visible. +/// +/// Wall-clock budget anchors for the composite reverify path (per the +/// Phase 26 acceptance literal): the live process backend stays under +/// 400ms per 3-member chain, the docker backend under 1500ms. Those +/// live-run numbers are covered by the +/// `flask_eval_chain_reverify_populates_dynamic_verdict` integration +/// test in `tests/chain_emission_e2e.rs`; the microbenches here anchor +/// the dispatch + verdict-application overhead so regressions on the +/// API-shape half land in the criterion baseline. /// /// Baselines committed to `benches/dynamic_bench_baseline.json`. /// Run: `cargo bench --features dynamic -- dynamic` @@ -386,6 +405,164 @@ fn bench_php_harness_build_cold(c: &mut Criterion) { }); } +#[cfg(feature = "dynamic")] +fn mk_chain_member(hash: u64, idx: usize) -> nyx_scanner::chain::FindingRef { + use nyx_scanner::surface::SourceLocation; + nyx_scanner::chain::FindingRef { + finding_id: format!("bench-chain-member-{idx}"), + stable_hash: hash, + location: SourceLocation::new("bench/synthetic.py", (idx as u32) + 1, 1), + rule_id: "taint-unsanitised-flow".into(), + cap_bits: 0, + } +} + +#[cfg(feature = "dynamic")] +fn mk_synthetic_chain(hash: u64, members: usize) -> nyx_scanner::chain::ChainFinding { + use nyx_scanner::chain::{ChainFinding, ChainSeverity, ChainSink, ImpactCategory}; + ChainFinding { + stable_hash: hash, + members: (0..members) + .map(|i| mk_chain_member(hash.wrapping_add(i as u64 + 1), i)) + .collect(), + sink: ChainSink { + file: "bench/synthetic.py".into(), + line: 99, + col: 1, + function_name: "sink".into(), + cap_bits: 0, + }, + implied_impact: ImpactCategory::Rce, + severity: ChainSeverity::Critical, + score: 100.0, + dynamic_verdict: None, + reverify_reason: None, + } +} + +#[cfg(feature = "dynamic")] +struct BenchConfirmedReverifier; + +#[cfg(feature = "dynamic")] +impl nyx_scanner::chain::CompositeReverifier for BenchConfirmedReverifier { + fn reverify( + &self, + _chain: &nyx_scanner::chain::ChainFinding, + _member_diags: &[nyx_scanner::commands::scan::Diag], + _surface: &nyx_scanner::surface::SurfaceMap, + _opts: &nyx_scanner::dynamic::verify::VerifyOptions, + ) -> nyx_scanner::evidence::VerifyResult { + nyx_scanner::evidence::VerifyResult { + finding_id: "bench".into(), + status: nyx_scanner::evidence::VerifyStatus::Confirmed, + triggered_payload: None, + reason: None, + inconclusive_reason: None, + detail: None, + attempts: vec![], + toolchain_match: None, + differential: None, + replay_stable: None, + wrong: None, + hardening_outcome: None, + } + } +} + +/// Phase 26 dispatch-cost anchor: synthetic 3-member chain with no +/// matching member diags. The reverifier walks chain_step_specs (3 +/// HashMap misses → 3 NoFlowSteps errors), the build loop sees zero +/// derived specs and exits early, the run loop sees zero built steps +/// and exits early. The composed VerifyResult is allocated and applied +/// via `apply_dynamic_verdict` (Inconclusive → severity downgrade). +/// +/// This is the no-toolchain-dep dispatch overhead — a regression here +/// signals a hot-path allocation introduced into the reverify pipeline. +#[cfg(feature = "dynamic")] +fn bench_composite_chain_reverify_dispatch(c: &mut Criterion) { + use nyx_scanner::chain::reverify; + use nyx_scanner::dynamic::verify::VerifyOptions; + use nyx_scanner::surface::SurfaceMap; + + let surface = SurfaceMap::new(); + let opts = VerifyOptions::default(); + + c.bench_function("composite_chain_reverify_dispatch", |b| { + b.iter(|| { + let mut chains = [mk_synthetic_chain(0xC1A1, 3)]; + let _ = reverify::reverify_top_chains(&mut chains, &[], &surface, &opts, 1); + }); + }); +} + +/// Phase 26 stub-reverifier happy-path anchor: synthetic 3-member +/// chain driven through `reverify_top_chains_with` + a stubbed +/// reverifier returning `Confirmed`. Measures the apply-verdict path +/// when the verdict does NOT trigger a severity downgrade, so the +/// `ChainReverifyResult` allocation + `chain.apply_dynamic_verdict` +/// transition cost is exercised independent of the verdict-side +/// allocation in the dispatch bench. +#[cfg(feature = "dynamic")] +fn bench_composite_chain_reverify_stub_confirmed(c: &mut Criterion) { + use nyx_scanner::chain::reverify; + use nyx_scanner::dynamic::verify::VerifyOptions; + use nyx_scanner::surface::SurfaceMap; + + let surface = SurfaceMap::new(); + let opts = VerifyOptions::default(); + let reverifier = BenchConfirmedReverifier; + + c.bench_function("composite_chain_reverify_stub_confirmed", |b| { + b.iter(|| { + let mut chains = [mk_synthetic_chain(0xC2A2, 3)]; + let _ = reverify::reverify_top_chains_with( + &mut chains, + &[], + &surface, + &opts, + 1, + &reverifier, + ); + }); + }); +} + +/// Phase 26 top-N slice anchor: 5-chain slice with `top_n=3`. Asserts +/// (by way of regression) that the reverify pass never walks past the +/// top-N prefix. The fan-in is the per-chain dispatch cost times three; +/// a regression that drops the `bound = top_n.min(chains.len())` cap +/// would show up as a ~5/3 increase in this bench. +#[cfg(feature = "dynamic")] +fn bench_composite_chain_reverify_top_n_slice(c: &mut Criterion) { + use nyx_scanner::chain::reverify; + use nyx_scanner::dynamic::verify::VerifyOptions; + use nyx_scanner::surface::SurfaceMap; + + let surface = SurfaceMap::new(); + let opts = VerifyOptions::default(); + let reverifier = BenchConfirmedReverifier; + + c.bench_function("composite_chain_reverify_top_n_slice", |b| { + b.iter(|| { + let mut chains: [nyx_scanner::chain::ChainFinding; 5] = [ + mk_synthetic_chain(0xC301, 3), + mk_synthetic_chain(0xC302, 3), + mk_synthetic_chain(0xC303, 3), + mk_synthetic_chain(0xC304, 3), + mk_synthetic_chain(0xC305, 3), + ]; + let _ = reverify::reverify_top_chains_with( + &mut chains, + &[], + &surface, + &opts, + 3, + &reverifier, + ); + }); + }); +} + #[cfg(feature = "dynamic")] fn bench_noop(_c: &mut Criterion) {} @@ -409,6 +586,9 @@ criterion_group!( bench_go_harness_build_cold, bench_java_harness_build_cold, bench_php_harness_build_cold, + bench_composite_chain_reverify_dispatch, + bench_composite_chain_reverify_stub_confirmed, + bench_composite_chain_reverify_top_n_slice, ); #[cfg(not(feature = "dynamic"))]