From fdb42c0b75245921c76a796018fc274010b760b4 Mon Sep 17 00:00:00 2001 From: pitboss Date: Sun, 17 May 2026 15:36:24 -0500 Subject: [PATCH] [pitboss] sweep after phase 02: 2 deferred items resolved --- benches/dynamic_bench.rs | 6 ++++ src/dynamic/verify.rs | 17 +++++++++- tests/dynamic_sandbox_escape.rs | 32 +++++++++++++++++-- tests/dynamic_verify_e2e.rs | 55 +++++++++++++++++++++++++++++++++ 4 files changed, 107 insertions(+), 3 deletions(-) diff --git a/benches/dynamic_bench.rs b/benches/dynamic_bench.rs index 1fa89e6b..93584e32 100644 --- a/benches/dynamic_bench.rs +++ b/benches/dynamic_bench.rs @@ -67,6 +67,7 @@ fn make_rust_sqli_spec() -> HarnessSpec { spec_hash: "benchrustsqli0001".into(), derivation: SpecDerivationStrategy::FromFlowSteps, stubs_required: vec![], + framework: None, } } @@ -87,6 +88,7 @@ fn make_sqli_spec() -> HarnessSpec { spec_hash: "benchsqli000001".into(), derivation: SpecDerivationStrategy::FromFlowSteps, stubs_required: vec![], + framework: None, } } @@ -285,6 +287,7 @@ fn make_js_sqli_spec() -> HarnessSpec { spec_hash: "benchjssqli000001".into(), derivation: SpecDerivationStrategy::FromFlowSteps, stubs_required: vec![], + framework: None, } } @@ -305,6 +308,7 @@ fn make_go_sqli_spec() -> HarnessSpec { spec_hash: "benchgosqli000001".into(), derivation: SpecDerivationStrategy::FromFlowSteps, stubs_required: vec![], + framework: None, } } @@ -325,6 +329,7 @@ fn make_java_sqli_spec() -> HarnessSpec { spec_hash: "benchjavasqli00001".into(), derivation: SpecDerivationStrategy::FromFlowSteps, stubs_required: vec![], + framework: None, } } @@ -345,6 +350,7 @@ fn make_php_sqli_spec() -> HarnessSpec { spec_hash: "benchphpsqli000001".into(), derivation: SpecDerivationStrategy::FromFlowSteps, stubs_required: vec![], + framework: None, } } diff --git a/src/dynamic/verify.rs b/src/dynamic/verify.rs index b962efec..5c4bf934 100644 --- a/src/dynamic/verify.rs +++ b/src/dynamic/verify.rs @@ -98,6 +98,13 @@ pub struct VerifyOptions { /// `NYX_VERIFY_REPLAY_DOCKER` environment variable (`1` / `true`). /// The flag is inert when `replay_stable_check == false`. pub replay_use_docker: bool, + /// Test/observability hook: when `Some`, [`verify_finding`] records + /// every [`crate::dynamic::trace::TraceEvent`] into this trace handle + /// instead of constructing a fresh internal one. Lets integration + /// tests inspect the verifier's stage timeline (e.g. the Track L.0 + /// `framework_adapter_*` events) without scraping stderr or writing + /// a repro bundle. `None` in production paths. + pub trace_sink: Option>, } impl VerifyOptions { @@ -175,6 +182,7 @@ impl VerifyOptions { trace_verbose: false, replay_stable_check, replay_use_docker, + trace_sink: None, } } } @@ -483,7 +491,14 @@ pub fn verify_finding(diag: &Diag, opts: &VerifyOptions) -> VerifyResult { // Phase 30 (Track C observability): one trace per finding, threaded // into [`SandboxOptions`] so the runner can append `build_*` / // `sandbox_started` / `oracle_*` stages from inside `run_spec`. - let trace = Arc::new(crate::dynamic::trace::VerifyTrace::new()); + // + // Tests may pre-seed `opts.trace_sink` with their own `Arc` + // handle; when present we reuse it instead of allocating a fresh one + // so assertions can inspect the recorded stages after the call returns. + let trace = opts + .trace_sink + .clone() + .unwrap_or_else(|| Arc::new(crate::dynamic::trace::VerifyTrace::new())); trace.record( crate::dynamic::trace::TraceStage::SpecStarted, Some(format!("rule={} path={}", diag.id, diag.path)), diff --git a/tests/dynamic_sandbox_escape.rs b/tests/dynamic_sandbox_escape.rs index 746412ff..f7acd9f1 100644 --- a/tests/dynamic_sandbox_escape.rs +++ b/tests/dynamic_sandbox_escape.rs @@ -149,6 +149,20 @@ mod escape_tests { assert_no_escape(result, $fixture, None); } }; + ($name:ident, $fixture:literal, linux_only) => { + // macOS Docker Desktop does not enforce host /tmp isolation or + // pid-cgroup limits the way the Linux backend does, so these + // fixtures escape on macOS. The `linux-with-docker` CI row is + // the authoritative gate (see module docstring). + #[cfg(target_os = "linux")] + #[test] + fn $name() { + if !docker_available() { return; } + let (_tmpdir, harness) = harness_for_fixture($fixture); + let result = sandbox::run(&harness, &noop_payload(), &escape_opts()); + assert_no_escape(result, $fixture, None); + } + }; ($name:ident, $fixture:literal, marker = $marker:expr) => { #[test] fn $name() { @@ -163,14 +177,28 @@ mod escape_tests { let _ = fs::remove_file(&marker); } }; + ($name:ident, $fixture:literal, marker = $marker:expr, linux_only) => { + #[cfg(target_os = "linux")] + #[test] + fn $name() { + if !docker_available() { return; } + let marker: PathBuf = PathBuf::from($marker); + let _ = fs::remove_file(&marker); + let (_tmpdir, harness) = harness_for_fixture($fixture); + let result = sandbox::run(&harness, &noop_payload(), &escape_opts()); + assert_no_escape(result, $fixture, Some(&marker)); + let _ = fs::remove_file(&marker); + } + }; } escape_test!( escape_file_write_outside_workdir, "file_write_outside_workdir.py", - marker = "/tmp/nyx_escape_file_write_marker" + marker = "/tmp/nyx_escape_file_write_marker", + linux_only ); - escape_test!(escape_fork_bomb, "fork_bomb.py"); + escape_test!(escape_fork_bomb, "fork_bomb.py", linux_only); escape_test!(escape_raw_socket, "raw_socket.py"); escape_test!(escape_proc_mem_write, "proc_mem_write.py"); escape_test!(escape_ptrace_attach, "ptrace_attach.py"); diff --git a/tests/dynamic_verify_e2e.rs b/tests/dynamic_verify_e2e.rs index 5f150215..b0712650 100644 --- a/tests/dynamic_verify_e2e.rs +++ b/tests/dynamic_verify_e2e.rs @@ -158,6 +158,61 @@ mod verify_e2e { assert_eq!(result.reason, Some(UnsupportedReason::ConfidenceTooLow)); } + /// Phase 01 / Track L.0 acceptance: every spec the verifier + /// finalises must emit either `framework_adapter_detected` or + /// `framework_adapter_none` into the [`VerifyTrace`]. The Phase 01 + /// adapter registry is empty, so the baseline contract is that + /// every successfully-derived spec records a `framework_adapter_none` + /// event whose `detail` carries `lang= entry=`. + /// + /// We drive `verify_finding` through the `NoPayloadsForCap` short-circuit + /// (CRYPTO has no curated payload corpus) so the trace is recorded + /// without needing a working toolchain or sandbox backend. + #[test] + fn verify_finding_emits_framework_adapter_none_for_empty_registry() { + use nyx_scanner::dynamic::trace::{TraceStage, VerifyTrace}; + use std::sync::Arc; + + let diag = taint_diag_with_cap(Cap::CRYPTO); + let trace = Arc::new(VerifyTrace::new()); + let mut opts = VerifyOptions::default(); + opts.trace_sink = Some(Arc::clone(&trace)); + + let _result = verify_finding(&diag, &opts); + + let events = trace.events(); + let adapter_event = events + .iter() + .find(|e| e.stage == TraceStage::FrameworkAdapterNone) + .expect( + "Phase 01 / Track L.0 contract: every finalised spec must emit \ + a `framework_adapter_none` event when the adapter registry is empty", + ); + let detail = adapter_event + .detail + .as_deref() + .expect("framework_adapter_none must carry a detail string"); + assert!( + detail.contains("lang="), + "framework_adapter_none detail must include `lang=…`, got: {detail:?}" + ); + assert!( + detail.contains("entry="), + "framework_adapter_none detail must include `entry=…`, got: {detail:?}" + ); + assert!( + detail.contains("entry=handle_request"), + "framework_adapter_none detail must name the spec's entry function, got: {detail:?}" + ); + assert!( + !events + .iter() + .any(|e| e.stage == TraceStage::FrameworkAdapterDetected), + "Phase 01 ships zero adapters, so no `framework_adapter_detected` event \ + can fire on the baseline path" + ); + } + /// The JSON shape of `VerifyResult` for an evidence-less finding /// matches the documented contract: `status` present; transient /// fields like `triggered_payload`, `detail`, `attempts` absent