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:
elipeter 2026-05-28 11:08:59 -05:00
parent 71fade1d83
commit c3a1550315
20 changed files with 2025 additions and 213 deletions

View file

@ -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

View 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());
}

View file

@ -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 —

View file

@ -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") };
}
}

View file

@ -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") };
}
}

View file

@ -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