mirror of
https://github.com/elicpeter/nyx.git
synced 2026-07-18 21:21:03 +02:00
refactor(scan): implement IndexWriteQueue for single-writer SQLite handling, introduce ReproEnvGuard for safer environment variable management, and refactor tests to enhance isolation and determinism
This commit is contained in:
parent
71fade1d83
commit
c3a1550315
20 changed files with 2025 additions and 213 deletions
|
|
@ -20,9 +20,22 @@ use nyx_scanner::evidence::{Confidence, Evidence, VerifyStatus};
|
|||
use nyx_scanner::patterns::{FindingCategory, Severity};
|
||||
use serde_json::Value;
|
||||
use std::collections::BTreeSet;
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
|
||||
const RUN_COUNT: usize = 10;
|
||||
|
||||
// `NYX_TELEMETRY_PATH` and the telemetry log are process-wide; cargo test
|
||||
// runs the tests in this binary in parallel by default, which would race
|
||||
// the env var and interleave writes from sibling tests into the file the
|
||||
// telemetry-determinism assertion is reading. Serialise the tests in
|
||||
// this file with a module-level mutex so each owns the telemetry surface
|
||||
// exclusively for the duration of its run.
|
||||
static TEST_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
fn lock_telemetry() -> MutexGuard<'static, ()> {
|
||||
TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
fn deny_diag(stable_hash: u64) -> Diag {
|
||||
// Triggers the credentials deny rule via the AWS-key regex from
|
||||
// `crate::utils::redact::contains_secret`. The deny rule fires
|
||||
|
|
@ -75,6 +88,7 @@ fn strip_volatile_fields(line: &str) -> String {
|
|||
|
||||
#[test]
|
||||
fn ten_runs_produce_byte_identical_telemetry_minus_timestamps() {
|
||||
let _guard = lock_telemetry();
|
||||
let tmp = tempfile::TempDir::new().expect("tempdir");
|
||||
let log = tmp.path().join("events.jsonl");
|
||||
// Pin the telemetry log to the temp file and ensure the
|
||||
|
|
@ -211,6 +225,8 @@ fn confirmed_run_is_byte_identical_across_runs() {
|
|||
use nyx_scanner::utils::config::Config;
|
||||
use std::path::PathBuf;
|
||||
|
||||
let _guard = lock_telemetry();
|
||||
|
||||
const RUN_COUNT_CONFIRMED: usize = 3;
|
||||
|
||||
// Pre-flight skips: the macOS process backend needs the sandbox-exec
|
||||
|
|
@ -364,6 +380,7 @@ fn confirmed_run_is_byte_identical_across_runs() {
|
|||
|
||||
#[test]
|
||||
fn policy_deny_excerpt_is_stable_across_runs() {
|
||||
let _guard = lock_telemetry();
|
||||
// The PolicyDeniedDynamic verdict carries an excerpt scrubbed via
|
||||
// the blake3-keyed `Scrubber`. blake3 is deterministic, so the
|
||||
// excerpt should be byte-identical across runs. Independent
|
||||
|
|
|
|||
192
tests/dynamic_java_compile_pool.rs
Normal file
192
tests/dynamic_java_compile_pool.rs
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
//! Phase 22 / Track O.0 acceptance test for the warm `javac` daemon.
|
||||
//!
|
||||
//! Asserts that 50 sequential harness-shaped Java compiles run through the
|
||||
//! pool in < 5s on the dev reference machine (down from > 30s baseline with
|
||||
//! one fresh `javac` per build). The test is gated on the `dynamic`
|
||||
//! feature and skips silently when `javac` / `java` are not on PATH so a
|
||||
//! JDK-less CI image does not break the gate.
|
||||
|
||||
#![cfg(feature = "dynamic")]
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use nyx_scanner::dynamic::build_pool::BuildPool;
|
||||
use nyx_scanner::dynamic::build_pool::java::JavacPool;
|
||||
|
||||
static BUILD_POOL_ENV_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
struct BuildPoolEnvGuard {
|
||||
_lock: MutexGuard<'static, ()>,
|
||||
prior: Option<String>,
|
||||
}
|
||||
|
||||
impl BuildPoolEnvGuard {
|
||||
fn set(path: &Path) -> Self {
|
||||
let lock = BUILD_POOL_ENV_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
let prior = std::env::var("NYX_BUILD_POOL_DIR").ok();
|
||||
unsafe { std::env::set_var("NYX_BUILD_POOL_DIR", path) };
|
||||
Self { _lock: lock, prior }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for BuildPoolEnvGuard {
|
||||
fn drop(&mut self) {
|
||||
match self.prior.take() {
|
||||
Some(value) => unsafe { std::env::set_var("NYX_BUILD_POOL_DIR", value) },
|
||||
None => unsafe { std::env::remove_var("NYX_BUILD_POOL_DIR") },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn jdk_available() -> bool {
|
||||
fn ok(bin: &str) -> bool {
|
||||
Command::new(bin)
|
||||
.arg("-version")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
ok(&std::env::var("NYX_JAVAC_BIN").unwrap_or_else(|_| "javac".to_owned()))
|
||||
&& ok(&std::env::var("NYX_JAVA_BIN").unwrap_or_else(|_| "java".to_owned()))
|
||||
}
|
||||
|
||||
/// Drop a self-contained Java source into `workdir/Harness{idx}.java`
|
||||
/// and return the args list the pool expects.
|
||||
fn write_harness(workdir: &Path, idx: usize) -> Vec<String> {
|
||||
let class_name = format!("Harness{idx}");
|
||||
let src = format!(
|
||||
"public final class {class_name} {{\n \
|
||||
public static int answer() {{ return {idx}; }}\n \
|
||||
public static void main(String[] argv) {{ \
|
||||
System.out.println({class_name}.answer()); }}\n\
|
||||
}}\n",
|
||||
);
|
||||
let src_path = workdir.join(format!("{class_name}.java"));
|
||||
std::fs::write(&src_path, src).unwrap();
|
||||
vec![
|
||||
"-d".to_owned(),
|
||||
workdir.to_string_lossy().into_owned(),
|
||||
src_path.to_string_lossy().into_owned(),
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_of_fifty_harness_compiles_meets_perf_target() {
|
||||
if !jdk_available() {
|
||||
eprintln!("skipping: javac / java not available on PATH");
|
||||
return;
|
||||
}
|
||||
|
||||
// Isolate the pool bootstrap dir so this test does not race with
|
||||
// another concurrent build-pool test or pollute the user's cache.
|
||||
let bootstrap_root = tempfile::TempDir::new().unwrap();
|
||||
let _env = BuildPoolEnvGuard::set(bootstrap_root.path());
|
||||
|
||||
let pool = match JavacPool::try_new("phase22-batch-test") {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("skipping: pool bootstrap failed: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// First call warms JIT + classpath caches inside the worker JVM.
|
||||
// We deliberately measure the steady-state 50 builds with the
|
||||
// bootstrap already paid because the acceptance gate is the
|
||||
// amortised per-build cost.
|
||||
let warmup_dir = tempfile::TempDir::new().unwrap();
|
||||
let warmup_args = write_harness(warmup_dir.path(), 0);
|
||||
let warmup = pool.compile_batch(warmup_dir.path(), &warmup_args);
|
||||
assert!(
|
||||
warmup.success,
|
||||
"warmup compile must succeed: {}",
|
||||
warmup.stderr
|
||||
);
|
||||
assert!(
|
||||
warmup_dir.path().join("Harness0.class").exists(),
|
||||
"warmup compile must emit a class file",
|
||||
);
|
||||
|
||||
// 50 sequential builds, each in its own workdir so the JVM-side
|
||||
// file resolution touches a fresh path every time -- closest
|
||||
// analogue to the per-finding shape the verifier produces.
|
||||
let mut workdirs: Vec<(tempfile::TempDir, PathBuf, Vec<String>)> = Vec::with_capacity(50);
|
||||
for i in 1..=50 {
|
||||
let d = tempfile::TempDir::new().unwrap();
|
||||
let args = write_harness(d.path(), i);
|
||||
let path = d.path().to_path_buf();
|
||||
workdirs.push((d, path, args));
|
||||
}
|
||||
|
||||
let start = Instant::now();
|
||||
for (i, (_dir, path, args)) in workdirs.iter().enumerate() {
|
||||
let r = pool.compile_batch(path, args);
|
||||
assert!(r.success, "compile {} failed: {}", i + 1, r.stderr,);
|
||||
let class_file = path.join(format!("Harness{}.class", i + 1));
|
||||
assert!(
|
||||
class_file.exists(),
|
||||
"compile {} produced no class file at {}",
|
||||
i + 1,
|
||||
class_file.display(),
|
||||
);
|
||||
}
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
eprintln!(
|
||||
"phase22 javac-pool: 50 hot compiles in {:.2?} (avg {:.2}ms/build)",
|
||||
elapsed,
|
||||
elapsed.as_secs_f64() * 1000.0 / 50.0,
|
||||
);
|
||||
|
||||
let cap = Duration::from_secs(5);
|
||||
assert!(
|
||||
elapsed <= cap,
|
||||
"phase22 acceptance gate: 50 hot compiles took {elapsed:?}, expected ≤ {cap:?}",
|
||||
);
|
||||
|
||||
assert!(
|
||||
pool.is_healthy(),
|
||||
"pool must stay healthy after 50 compiles"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pool_surfaces_real_compile_errors_intact() {
|
||||
if !jdk_available() {
|
||||
eprintln!("skipping: javac / java not available on PATH");
|
||||
return;
|
||||
}
|
||||
let bootstrap_root = tempfile::TempDir::new().unwrap();
|
||||
let _env = BuildPoolEnvGuard::set(bootstrap_root.path());
|
||||
|
||||
let pool = match JavacPool::try_new("phase22-error-test") {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
eprintln!("skipping: pool bootstrap failed: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
let src = dir.path().join("Broken.java");
|
||||
std::fs::write(&src, "public class Broken { int x = ; }").unwrap();
|
||||
let args = vec![
|
||||
"-d".to_owned(),
|
||||
dir.path().to_string_lossy().into_owned(),
|
||||
src.to_string_lossy().into_owned(),
|
||||
];
|
||||
let r = pool.compile_batch(dir.path(), &args);
|
||||
assert!(!r.success, "syntactically invalid source must fail");
|
||||
assert!(
|
||||
!r.stderr.is_empty(),
|
||||
"compile failure must produce a non-empty stderr payload (got {:?})",
|
||||
r.stderr,
|
||||
);
|
||||
// Pool should still be alive for the next caller.
|
||||
assert!(pool.is_healthy());
|
||||
}
|
||||
|
|
@ -21,9 +21,37 @@ use nyx_scanner::dynamic::oracle::{Oracle, ProbePredicate, oracle_fired};
|
|||
use nyx_scanner::dynamic::probe::{
|
||||
PROBE_PATH_ENV, ProbeArg, ProbeChannel, ProbeKind, ProbeWitness, SinkProbe,
|
||||
};
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
use std::time::Duration;
|
||||
use tempfile::TempDir;
|
||||
|
||||
static PROBE_ENV_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
struct ProbeEnvGuard {
|
||||
_lock: MutexGuard<'static, ()>,
|
||||
prior: Option<String>,
|
||||
}
|
||||
|
||||
impl ProbeEnvGuard {
|
||||
fn set(channel: &ProbeChannel) -> Self {
|
||||
let lock = PROBE_ENV_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
let prior = std::env::var(PROBE_PATH_ENV).ok();
|
||||
unsafe { std::env::set_var(PROBE_PATH_ENV, channel.path()) };
|
||||
Self { _lock: lock, prior }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ProbeEnvGuard {
|
||||
fn drop(&mut self) {
|
||||
match self.prior.take() {
|
||||
Some(value) => unsafe { std::env::set_var(PROBE_PATH_ENV, value) },
|
||||
None => unsafe { std::env::remove_var(PROBE_PATH_ENV) },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimal [`SandboxOutcome`] suitable for oracle evaluation when the
|
||||
/// runner-side execution path is not exercised. All flags are off so any
|
||||
/// `true` verdict must come from the probe channel, not from
|
||||
|
|
@ -77,15 +105,7 @@ fn sink_probe_oracle_confirms_when_harness_writes_probe() {
|
|||
|
||||
// Exercise the harness env-var path so the test also locks the
|
||||
// NYX_PROBE_PATH contract the real sandbox forwards to the harness.
|
||||
// SAFETY: each test has a fresh tempdir and the env var is consumed
|
||||
// immediately by the synthetic harness body, then re-checked below.
|
||||
// Tests in this binary run on isolated channels so the env var read
|
||||
// is unambiguous.
|
||||
// SAFETY: env_var is process-global; this binary contains only the
|
||||
// oracle_sink_probe tests so the writes do not race other suites.
|
||||
unsafe {
|
||||
std::env::set_var(PROBE_PATH_ENV, channel.path());
|
||||
}
|
||||
let _env = ProbeEnvGuard::set(&channel);
|
||||
assert_eq!(
|
||||
std::env::var(PROBE_PATH_ENV).unwrap().as_str(),
|
||||
channel.path().to_str().unwrap(),
|
||||
|
|
@ -121,9 +141,7 @@ fn sink_probe_oracle_not_confirmed_when_harness_omits_probe() {
|
|||
let dir = TempDir::new().unwrap();
|
||||
let channel = ProbeChannel::for_workdir(dir.path()).unwrap();
|
||||
|
||||
unsafe {
|
||||
std::env::set_var(PROBE_PATH_ENV, channel.path());
|
||||
}
|
||||
let _env = ProbeEnvGuard::set(&channel);
|
||||
|
||||
// Control fixture: identical configuration but the harness skips its
|
||||
// probe write. Same oracle predicate set as the Confirmed test —
|
||||
|
|
|
|||
|
|
@ -16,9 +16,38 @@ mod repro_determinism_tests {
|
|||
use nyx_scanner::evidence::{AttemptSummary, VerifyResult, VerifyStatus};
|
||||
use nyx_scanner::labels::Cap;
|
||||
use nyx_scanner::symbol::Lang;
|
||||
use std::path::Path;
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
use std::time::Duration;
|
||||
use tempfile::TempDir;
|
||||
|
||||
static REPRO_ENV_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
struct ReproEnvGuard {
|
||||
_lock: MutexGuard<'static, ()>,
|
||||
prior: Option<String>,
|
||||
}
|
||||
|
||||
impl ReproEnvGuard {
|
||||
fn set(base: &Path) -> Self {
|
||||
let lock = REPRO_ENV_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
let prior = std::env::var("NYX_REPRO_BASE").ok();
|
||||
unsafe { std::env::set_var("NYX_REPRO_BASE", base) };
|
||||
Self { _lock: lock, prior }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ReproEnvGuard {
|
||||
fn drop(&mut self) {
|
||||
match self.prior.take() {
|
||||
Some(value) => unsafe { std::env::set_var("NYX_REPRO_BASE", value) },
|
||||
None => unsafe { std::env::remove_var("NYX_REPRO_BASE") },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn make_confirmed_spec(spec_hash: &str) -> HarnessSpec {
|
||||
HarnessSpec {
|
||||
finding_id: "determinism00001".into(),
|
||||
|
|
@ -80,8 +109,7 @@ mod repro_determinism_tests {
|
|||
#[test]
|
||||
fn confirmed_repro_is_deterministic() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
// Override repro base to temp dir.
|
||||
unsafe { std::env::set_var("NYX_REPRO_BASE", dir.path().to_str().unwrap()) };
|
||||
let _env = ReproEnvGuard::set(dir.path());
|
||||
|
||||
let spec = make_confirmed_spec("determ0000000001");
|
||||
let opts = SandboxOptions::default();
|
||||
|
|
@ -129,15 +157,13 @@ mod repro_determinism_tests {
|
|||
outcome_json_1, outcome_json_2,
|
||||
"outcome.json must be byte-identical across two runs with the same inputs"
|
||||
);
|
||||
|
||||
unsafe { std::env::remove_var("NYX_REPRO_BASE") };
|
||||
}
|
||||
|
||||
/// Verify that redacted outcome.json does not contain the secret.
|
||||
#[test]
|
||||
fn outcome_json_secrets_are_redacted() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
unsafe { std::env::set_var("NYX_REPRO_BASE", dir.path().to_str().unwrap()) };
|
||||
let _env = ReproEnvGuard::set(dir.path());
|
||||
|
||||
let spec = make_confirmed_spec("determ0000000002");
|
||||
let opts = SandboxOptions::default();
|
||||
|
|
@ -166,8 +192,6 @@ mod repro_determinism_tests {
|
|||
!outcome_json.contains("AKIAFAKETEST00000000"),
|
||||
"AWS key must be redacted from outcome.json; got: {outcome_json}"
|
||||
);
|
||||
|
||||
unsafe { std::env::remove_var("NYX_REPRO_BASE") };
|
||||
}
|
||||
|
||||
// ── Rust repro tests ─────────────────────────────────────────────────────
|
||||
|
|
@ -210,7 +234,7 @@ fn main() {
|
|||
#[test]
|
||||
fn rust_repro_layout_is_correct() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
unsafe { std::env::set_var("NYX_REPRO_BASE", dir.path().to_str().unwrap()) };
|
||||
let _env = ReproEnvGuard::set(dir.path());
|
||||
|
||||
let spec = make_confirmed_rust_spec("rust_determ00001");
|
||||
let opts = SandboxOptions::default();
|
||||
|
|
@ -247,15 +271,13 @@ fn main() {
|
|||
assert!(artifact.root.join("expected/outcome.json").exists());
|
||||
assert!(artifact.root.join("expected/verdict.json").exists());
|
||||
assert!(artifact.root.join("reproduce.sh").exists());
|
||||
|
||||
unsafe { std::env::remove_var("NYX_REPRO_BASE") };
|
||||
}
|
||||
|
||||
/// Rust repro outcome.json is byte-identical across two writes.
|
||||
#[test]
|
||||
fn rust_repro_outcome_is_deterministic() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
unsafe { std::env::set_var("NYX_REPRO_BASE", dir.path().to_str().unwrap()) };
|
||||
let _env = ReproEnvGuard::set(dir.path());
|
||||
|
||||
let spec = make_confirmed_rust_spec("rust_determ00002");
|
||||
let opts = SandboxOptions::default();
|
||||
|
|
@ -298,8 +320,6 @@ fn main() {
|
|||
json1, json2,
|
||||
"Rust outcome.json must be byte-identical across two writes"
|
||||
);
|
||||
|
||||
unsafe { std::env::remove_var("NYX_REPRO_BASE") };
|
||||
}
|
||||
|
||||
// ── JS repro tests ───────────────────────────────────────────────────────
|
||||
|
|
@ -328,7 +348,7 @@ fn main() {
|
|||
#[test]
|
||||
fn js_repro_outcome_is_deterministic() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
unsafe { std::env::set_var("NYX_REPRO_BASE", dir.path().to_str().unwrap()) };
|
||||
let _env = ReproEnvGuard::set(dir.path());
|
||||
|
||||
let spec = make_confirmed_js_spec("js_determ000001a");
|
||||
let opts = SandboxOptions::default();
|
||||
|
|
@ -370,8 +390,6 @@ fn main() {
|
|||
json1, json2,
|
||||
"JS outcome.json must be byte-identical across two writes"
|
||||
);
|
||||
|
||||
unsafe { std::env::remove_var("NYX_REPRO_BASE") };
|
||||
}
|
||||
|
||||
// ── Go repro tests ───────────────────────────────────────────────────────
|
||||
|
|
@ -400,7 +418,7 @@ fn main() {
|
|||
#[test]
|
||||
fn go_repro_outcome_is_deterministic() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
unsafe { std::env::set_var("NYX_REPRO_BASE", dir.path().to_str().unwrap()) };
|
||||
let _env = ReproEnvGuard::set(dir.path());
|
||||
|
||||
let spec = make_confirmed_go_spec("go_determ000001a");
|
||||
let opts = SandboxOptions::default();
|
||||
|
|
@ -442,8 +460,6 @@ fn main() {
|
|||
json1, json2,
|
||||
"Go outcome.json must be byte-identical across two writes"
|
||||
);
|
||||
|
||||
unsafe { std::env::remove_var("NYX_REPRO_BASE") };
|
||||
}
|
||||
|
||||
// ── Java repro tests ─────────────────────────────────────────────────────
|
||||
|
|
@ -472,7 +488,7 @@ fn main() {
|
|||
#[test]
|
||||
fn java_repro_outcome_is_deterministic() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
unsafe { std::env::set_var("NYX_REPRO_BASE", dir.path().to_str().unwrap()) };
|
||||
let _env = ReproEnvGuard::set(dir.path());
|
||||
|
||||
let spec = make_confirmed_java_spec("java_determ00001a");
|
||||
let opts = SandboxOptions::default();
|
||||
|
|
@ -514,8 +530,6 @@ fn main() {
|
|||
json1, json2,
|
||||
"Java outcome.json must be byte-identical across two writes"
|
||||
);
|
||||
|
||||
unsafe { std::env::remove_var("NYX_REPRO_BASE") };
|
||||
}
|
||||
|
||||
// ── PHP repro tests ──────────────────────────────────────────────────────
|
||||
|
|
@ -544,7 +558,7 @@ fn main() {
|
|||
#[test]
|
||||
fn php_repro_outcome_is_deterministic() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
unsafe { std::env::set_var("NYX_REPRO_BASE", dir.path().to_str().unwrap()) };
|
||||
let _env = ReproEnvGuard::set(dir.path());
|
||||
|
||||
let spec = make_confirmed_php_spec("php_determ000001a");
|
||||
let opts = SandboxOptions::default();
|
||||
|
|
@ -586,15 +600,13 @@ fn main() {
|
|||
json1, json2,
|
||||
"PHP outcome.json must be byte-identical across two writes"
|
||||
);
|
||||
|
||||
unsafe { std::env::remove_var("NYX_REPRO_BASE") };
|
||||
}
|
||||
|
||||
/// Verify verdict.json is correctly structured.
|
||||
#[test]
|
||||
fn verdict_json_is_valid() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
unsafe { std::env::set_var("NYX_REPRO_BASE", dir.path().to_str().unwrap()) };
|
||||
let _env = ReproEnvGuard::set(dir.path());
|
||||
|
||||
let spec = make_confirmed_spec("determ0000000003");
|
||||
let opts = SandboxOptions::default();
|
||||
|
|
@ -620,7 +632,5 @@ fn main() {
|
|||
|
||||
assert_eq!(parsed["status"], "Confirmed");
|
||||
assert_eq!(parsed["finding_id"], "determinism00003");
|
||||
|
||||
unsafe { std::env::remove_var("NYX_REPRO_BASE") };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,9 +35,38 @@ mod repro_hermetic_tests {
|
|||
use nyx_scanner::evidence::{AttemptSummary, VerifyResult, VerifyStatus};
|
||||
use nyx_scanner::labels::Cap;
|
||||
use nyx_scanner::symbol::Lang;
|
||||
use std::path::Path;
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
use std::time::Duration;
|
||||
use tempfile::TempDir;
|
||||
|
||||
static REPRO_ENV_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
struct ReproEnvGuard {
|
||||
_lock: MutexGuard<'static, ()>,
|
||||
prior: Option<String>,
|
||||
}
|
||||
|
||||
impl ReproEnvGuard {
|
||||
fn set(base: &Path) -> Self {
|
||||
let lock = REPRO_ENV_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
let prior = std::env::var("NYX_REPRO_BASE").ok();
|
||||
unsafe { std::env::set_var("NYX_REPRO_BASE", base) };
|
||||
Self { _lock: lock, prior }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ReproEnvGuard {
|
||||
fn drop(&mut self) {
|
||||
match self.prior.take() {
|
||||
Some(value) => unsafe { std::env::set_var("NYX_REPRO_BASE", value) },
|
||||
None => unsafe { std::env::remove_var("NYX_REPRO_BASE") },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn make_spec() -> HarnessSpec {
|
||||
HarnessSpec {
|
||||
finding_id: "hermetic00000001".into(),
|
||||
|
|
@ -98,7 +127,7 @@ mod repro_hermetic_tests {
|
|||
#[test]
|
||||
fn bundle_carries_toolchain_lock_with_hashes() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
unsafe { std::env::set_var("NYX_REPRO_BASE", dir.path().to_str().unwrap()) };
|
||||
let _env = ReproEnvGuard::set(dir.path());
|
||||
|
||||
let artifact = repro::write(
|
||||
&make_spec(),
|
||||
|
|
@ -146,8 +175,6 @@ mod repro_hermetic_tests {
|
|||
lock["files"], lock2["files"],
|
||||
"lock file hashes must be deterministic"
|
||||
);
|
||||
|
||||
unsafe { std::env::remove_var("NYX_REPRO_BASE") };
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -157,7 +184,7 @@ mod repro_hermetic_tests {
|
|||
// verify the script *refuses* to run rather than crashing —
|
||||
// the green path on a clean machine is via `--docker`.
|
||||
let dir = TempDir::new().unwrap();
|
||||
unsafe { std::env::set_var("NYX_REPRO_BASE", dir.path().to_str().unwrap()) };
|
||||
let _env = ReproEnvGuard::set(dir.path());
|
||||
|
||||
let artifact = repro::write(
|
||||
&make_spec(),
|
||||
|
|
@ -226,8 +253,6 @@ mod repro_hermetic_tests {
|
|||
String::from_utf8_lossy(&result.stdout),
|
||||
String::from_utf8_lossy(&result.stderr),
|
||||
);
|
||||
|
||||
unsafe { std::env::remove_var("NYX_REPRO_BASE") };
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -286,7 +311,7 @@ mod repro_hermetic_tests {
|
|||
// once digests land and gates against regressions where a
|
||||
// pinned toolchain stops emitting `docker_pull.sh`.
|
||||
let dir = TempDir::new().unwrap();
|
||||
unsafe { std::env::set_var("NYX_REPRO_BASE", dir.path().to_str().unwrap()) };
|
||||
let _env = ReproEnvGuard::set(dir.path());
|
||||
|
||||
let mut spec = make_spec();
|
||||
spec.toolchain_id = "python-3.11".into();
|
||||
|
|
@ -316,7 +341,5 @@ mod repro_hermetic_tests {
|
|||
"docker_pull.sh should not be emitted when toolchain is unpinned",
|
||||
);
|
||||
}
|
||||
|
||||
unsafe { std::env::remove_var("NYX_REPRO_BASE") };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
#[cfg(all(feature = "dynamic", target_os = "macos"))]
|
||||
mod hardening_tests {
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
use std::time::Duration;
|
||||
|
||||
use nyx_scanner::dynamic::harness::BuiltHarness;
|
||||
|
|
@ -28,6 +29,14 @@ mod hardening_tests {
|
|||
self, HardeningRecord, ProcessHardeningProfile, SandboxBackend, SandboxOptions,
|
||||
};
|
||||
|
||||
static ENV_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
fn lock_env() -> MutexGuard<'static, ()> {
|
||||
ENV_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||
}
|
||||
|
||||
fn macos_outcome(
|
||||
out: &sandbox::SandboxOutcome,
|
||||
) -> Option<&nyx_scanner::dynamic::sandbox::process_macos::HardeningOutcome> {
|
||||
|
|
@ -223,6 +232,7 @@ finally:
|
|||
/// fallback to engage — see `verify_finding_refuses_filesystem_*`.
|
||||
#[test]
|
||||
fn sandbox_exec_present_on_default_host() {
|
||||
let _env = lock_env();
|
||||
// Clear any override left by a sibling test in the same process.
|
||||
unsafe { std::env::remove_var(SANDBOX_EXEC_BIN_ENV) };
|
||||
if !sandbox_exec_available() {
|
||||
|
|
@ -241,6 +251,7 @@ finally:
|
|||
/// `NotConfirmed` (exit != 0 + no sink-hit + no oracle fire).
|
||||
#[test]
|
||||
fn path_traversal_payload_blocked_under_strict() {
|
||||
let _env = lock_env();
|
||||
unsafe { std::env::remove_var(SANDBOX_EXEC_BIN_ENV) };
|
||||
if !sandbox_exec_available() {
|
||||
eprintln!("SKIP: /usr/bin/sandbox-exec missing — cannot exercise wrap");
|
||||
|
|
@ -279,6 +290,7 @@ finally:
|
|||
/// above is actually exercising the sandbox or a probe quirk.
|
||||
#[test]
|
||||
fn standard_profile_does_not_wrap_with_sandbox_exec() {
|
||||
let _env = lock_env();
|
||||
unsafe { std::env::remove_var(SANDBOX_EXEC_BIN_ENV) };
|
||||
let tmp = workdir();
|
||||
let harness = build_harness(tmp.path());
|
||||
|
|
@ -304,6 +316,7 @@ finally:
|
|||
/// binary path via the [`SANDBOX_EXEC_BIN_ENV`] override.
|
||||
#[test]
|
||||
fn sandbox_exec_missing_records_trusted_outcome() {
|
||||
let _env = lock_env();
|
||||
const FILE_IO: u32 = 1 << 5;
|
||||
unsafe { std::env::set_var(SANDBOX_EXEC_BIN_ENV, "/nonexistent/sandbox-exec") };
|
||||
let tmp = workdir();
|
||||
|
|
@ -324,6 +337,7 @@ finally:
|
|||
/// running unconfined.
|
||||
#[test]
|
||||
fn verify_options_from_config_sets_refuse_when_sandbox_exec_missing() {
|
||||
let _env = lock_env();
|
||||
use nyx_scanner::dynamic::verify::VerifyOptions;
|
||||
use nyx_scanner::utils::config::Config;
|
||||
unsafe { std::env::set_var(SANDBOX_EXEC_BIN_ENV, "/nonexistent/sandbox-exec") };
|
||||
|
|
@ -344,6 +358,7 @@ finally:
|
|||
/// and exits 0 with the `network-attempted` marker.
|
||||
#[test]
|
||||
fn xxe_outbound_blocked_under_strict_xxe_profile() {
|
||||
let _env = lock_env();
|
||||
unsafe { std::env::remove_var(SANDBOX_EXEC_BIN_ENV) };
|
||||
if !sandbox_exec_available() {
|
||||
eprintln!("SKIP: /usr/bin/sandbox-exec missing — cannot exercise xxe profile");
|
||||
|
|
@ -381,6 +396,7 @@ finally:
|
|||
/// vacuously.
|
||||
#[test]
|
||||
fn xxe_probe_under_standard_does_not_surface_eperm() {
|
||||
let _env = lock_env();
|
||||
unsafe { std::env::remove_var(SANDBOX_EXEC_BIN_ENV) };
|
||||
let tmp = workdir();
|
||||
let harness = build_xxe_harness(tmp.path());
|
||||
|
|
@ -415,6 +431,7 @@ finally:
|
|||
/// harness free to open arbitrary outbound sockets.
|
||||
#[test]
|
||||
fn sql_profile_allows_sqlite_stub_and_blocks_non_loopback_egress() {
|
||||
let _env = lock_env();
|
||||
unsafe { std::env::remove_var(SANDBOX_EXEC_BIN_ENV) };
|
||||
if !sandbox_exec_available() {
|
||||
eprintln!("SKIP: /usr/bin/sandbox-exec missing — cannot exercise sql profile");
|
||||
|
|
@ -472,6 +489,7 @@ finally:
|
|||
/// flag stays `false` so filesystem oracles run normally.
|
||||
#[test]
|
||||
fn verify_options_from_config_does_not_refuse_when_sandbox_exec_present() {
|
||||
let _env = lock_env();
|
||||
use nyx_scanner::dynamic::verify::VerifyOptions;
|
||||
use nyx_scanner::utils::config::Config;
|
||||
unsafe { std::env::remove_var(SANDBOX_EXEC_BIN_ENV) };
|
||||
|
|
@ -497,6 +515,7 @@ finally:
|
|||
/// finding's oracle.
|
||||
#[test]
|
||||
fn summarize_hardening_lands_path_traversal_on_strict_file_io_run() {
|
||||
let _env = lock_env();
|
||||
unsafe { std::env::remove_var(SANDBOX_EXEC_BIN_ENV) };
|
||||
if !sandbox_exec_available() {
|
||||
eprintln!("SKIP: /usr/bin/sandbox-exec missing — cannot exercise wrap");
|
||||
|
|
@ -527,6 +546,7 @@ finally:
|
|||
/// `standard_profile_does_not_wrap_with_sandbox_exec`.
|
||||
#[test]
|
||||
fn summarize_hardening_returns_none_for_standard_profile_run() {
|
||||
let _env = lock_env();
|
||||
unsafe { std::env::remove_var(SANDBOX_EXEC_BIN_ENV) };
|
||||
let tmp = workdir();
|
||||
let harness = build_harness(tmp.path());
|
||||
|
|
@ -547,6 +567,7 @@ finally:
|
|||
/// reflect that.
|
||||
#[test]
|
||||
fn verify_finding_under_standard_leaves_hardening_outcome_unset() {
|
||||
let _env = lock_env();
|
||||
use std::path::PathBuf;
|
||||
let python3_available = std::process::Command::new("/usr/bin/python3")
|
||||
.arg("--version")
|
||||
|
|
@ -679,6 +700,7 @@ finally:
|
|||
/// reads of host secrets are denied via the inherited denylist).
|
||||
#[test]
|
||||
fn verify_finding_under_strict_stamps_hardening_outcome() {
|
||||
let _env = lock_env();
|
||||
use std::path::PathBuf;
|
||||
unsafe { std::env::remove_var(SANDBOX_EXEC_BIN_ENV) };
|
||||
if !sandbox_exec_available() {
|
||||
|
|
@ -838,6 +860,7 @@ finally:
|
|||
/// before this one.
|
||||
#[test]
|
||||
fn deny_default_seed_loads_under_strict() {
|
||||
let _env = lock_env();
|
||||
let seed_dir = tempfile::TempDir::new().expect("seed tempdir");
|
||||
// The seed body is intentionally over-permissive so the
|
||||
// /usr/bin/true probe at the end of the test can clear without
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue