From b32dc7ac0b11566fc60c4ae80ceeb50c2e40dcd0 Mon Sep 17 00:00:00 2001 From: elipeter Date: Wed, 3 Jun 2026 16:48:12 -0500 Subject: [PATCH] fix failing tests --- src/dynamic/build_pool/mod.rs | 42 +++++++- src/dynamic/build_sandbox.rs | 191 +++++++++++++++++++++------------- src/dynamic/lang/rust.rs | 119 ++++++++++++--------- src/dynamic/runner.rs | 95 +++++++++-------- tests/go_fixtures.rs | 4 +- 5 files changed, 284 insertions(+), 167 deletions(-) diff --git a/src/dynamic/build_pool/mod.rs b/src/dynamic/build_pool/mod.rs index 403b8775..6614aeb5 100644 --- a/src/dynamic/build_pool/mod.rs +++ b/src/dynamic/build_pool/mod.rs @@ -121,8 +121,9 @@ pub fn is_pool_enabled(lang: &str) -> bool { /// dir is available — callers treat that as "pool unavailable" and fall /// back to the legacy direct-spawn build path. pub(crate) fn pool_cache_dir(lang: &str, sub: &str) -> Option { - let base = if let Ok(custom) = std::env::var("NYX_BUILD_POOL_DIR") { - PathBuf::from(custom) + let custom = std::env::var("NYX_BUILD_POOL_DIR").ok().map(PathBuf::from); + let base = if let Some(custom) = custom.clone() { + custom } else { directories::ProjectDirs::from("dev", "nyx", "nyx")? .cache_dir() @@ -130,8 +131,27 @@ pub(crate) fn pool_cache_dir(lang: &str, sub: &str) -> Option { .join("build-pool") }; let dir = base.join(lang).join(sub); - std::fs::create_dir_all(&dir).ok()?; - Some(dir) + if ensure_writable_dir(&dir).is_some() { + return Some(dir); + } + if custom.is_some() { + return None; + } + let fallback = std::env::temp_dir() + .join("nyx") + .join("dynamic") + .join("build-pool") + .join(lang) + .join(sub); + ensure_writable_dir(&fallback) +} + +fn ensure_writable_dir(dir: &Path) -> Option { + std::fs::create_dir_all(dir).ok()?; + let probe = dir.join(format!(".nyx-write-probe-{}", std::process::id())); + std::fs::write(&probe, b"ok").ok()?; + let _ = std::fs::remove_file(probe); + Some(dir.to_path_buf()) } /// Construct a `Command` for `bin` with a scrubbed environment, matching @@ -140,12 +160,24 @@ pub(crate) fn pool_cache_dir(lang: &str, sub: &str) -> Option { /// (`CARGO_TARGET_DIR`, `CCACHE_DIR`, `GOCACHE`, …) on top of this. pub(crate) fn base_command(bin: &str) -> Command { let mut cmd = Command::new(bin); + let tmp = build_temp_dir(); cmd.env_clear() .env("PATH", std::env::var("PATH").unwrap_or_default()) - .env("HOME", std::env::var("HOME").unwrap_or_default()); + .env("HOME", std::env::var("HOME").unwrap_or_default()) + .env("TMPDIR", &tmp) + .env("TMP", &tmp) + .env("TEMP", &tmp); cmd } +fn build_temp_dir() -> PathBuf { + let dir = std::env::temp_dir().join("nyx-build-tmp"); + if std::fs::create_dir_all(&dir).is_ok() { + return dir; + } + std::env::temp_dir() +} + /// Hermetic Bundler / RubyGems environment pinned to a writable per-workdir /// vendor directory. /// diff --git a/src/dynamic/build_sandbox.rs b/src/dynamic/build_sandbox.rs index 5d80bebd..eacf1595 100644 --- a/src/dynamic/build_sandbox.rs +++ b/src/dynamic/build_sandbox.rs @@ -122,12 +122,11 @@ fn try_build_rust_binary(workdir: &Path, binary_dest: &Path) -> Result<(), Strin let cargo = cargo_binary(); // Run `cargo build --release` in the workdir. - let output = Command::new(&cargo) + let mut cmd = Command::new(&cargo); + apply_basic_build_env(&mut cmd); + let output = cmd .args(["build", "--release"]) .current_dir(workdir) - .env_clear() - .env("PATH", std::env::var("PATH").unwrap_or_default()) - .env("HOME", std::env::var("HOME").unwrap_or_default()) // Inherit CARGO_HOME so the local registry cache is reused. .env( "CARGO_HOME", @@ -326,12 +325,11 @@ fn try_build_venv(venv_path: &Path, workdir: &Path, spec: &HarnessSpec) -> Resul let python = python_binary(spec); // Create the venv. - let status = Command::new(&python) + let mut cmd = Command::new(&python); + apply_basic_build_env(&mut cmd); + let status = cmd .args(["-m", "venv", "--clear"]) .arg(venv_path) - .env_clear() - .env("PATH", std::env::var("PATH").unwrap_or_default()) - .env("HOME", std::env::var("HOME").unwrap_or_default()) .status() .map_err(|e| format!("venv create: {e}"))?; @@ -343,12 +341,11 @@ fn try_build_venv(venv_path: &Path, workdir: &Path, spec: &HarnessSpec) -> Resul let req_path = workdir.join("requirements.txt"); if req_path.exists() { let pip = venv_path.join("bin").join("pip"); - let output = Command::new(&pip) + let mut cmd = Command::new(&pip); + apply_basic_build_env(&mut cmd); + let output = cmd .args(["install", "--no-cache-dir", "-r"]) .arg(&req_path) - .env_clear() - .env("PATH", std::env::var("PATH").unwrap_or_default()) - .env("HOME", std::env::var("HOME").unwrap_or_default()) .output() .map_err(|e| format!("pip install: {e}"))?; @@ -414,21 +411,30 @@ fn build_cache_path( let name = format!("{lockfile_hash}-{language}-{toolchain_id}"); let path = base.join(&name); - match create_build_cache_dir(&path) { + match prepare_build_cache_dir(&path) { Ok(()) => Ok(path), - Err(e) if override_base.is_none() && e.kind() == std::io::ErrorKind::PermissionDenied => { + Err(e) if override_base.is_none() => { let fallback = std::env::temp_dir() .join("nyx") .join("dynamic") .join("build-cache") .join(&name); - create_build_cache_dir(&fallback)?; + prepare_build_cache_dir(&fallback)?; Ok(fallback) } Err(e) => Err(BuildError::Io(e)), } } +fn prepare_build_cache_dir(path: &Path) -> std::io::Result<()> { + create_build_cache_dir(path)?; + write_probe(path)?; + if let Some(parent) = path.parent() { + write_probe(parent)?; + } + Ok(()) +} + fn create_build_cache_dir(path: &Path) -> std::io::Result<()> { std::fs::create_dir_all(path)?; #[cfg(unix)] @@ -439,6 +445,32 @@ fn create_build_cache_dir(path: &Path) -> std::io::Result<()> { Ok(()) } +fn write_probe(dir: &Path) -> std::io::Result<()> { + std::fs::create_dir_all(dir)?; + let probe = dir.join(format!(".nyx-write-probe-{}", std::process::id())); + std::fs::write(&probe, b"ok")?; + let _ = std::fs::remove_file(probe); + Ok(()) +} + +fn build_temp_dir() -> PathBuf { + let dir = std::env::temp_dir().join("nyx-build-tmp"); + if std::fs::create_dir_all(&dir).is_ok() { + return dir; + } + std::env::temp_dir() +} + +fn apply_basic_build_env(cmd: &mut Command) { + let tmp = build_temp_dir(); + cmd.env_clear() + .env("PATH", std::env::var("PATH").unwrap_or_default()) + .env("HOME", std::env::var("HOME").unwrap_or_default()) + .env("TMPDIR", &tmp) + .env("TMP", &tmp) + .env("TEMP", &tmp); +} + const PYTHON_CACHE_DONE: &str = ".python_cache_done"; fn python_cache_done_path(cache_path: &Path) -> PathBuf { @@ -636,12 +668,11 @@ fn bundle_check(bundle: &str, workdir: &Path) -> Result { // 1.x's view of already-installed system gems and produces spurious // BuildFailed for a Gemfile the host can already satisfy. See the parallel // comment in `RubyPool::compile_batch`. - let output = Command::new(bundle) + let mut cmd = Command::new(bundle); + apply_basic_build_env(&mut cmd); + let output = cmd .arg("check") .current_dir(workdir) - .env_clear() - .env("PATH", std::env::var("PATH").unwrap_or_default()) - .env("HOME", std::env::var("HOME").unwrap_or_default()) .output() .map_err(|e| format!("bundle check: {e}"))?; Ok(output.status.success()) @@ -654,10 +685,8 @@ fn bundle_check(bundle: &str, workdir: &Path) -> Result { /// Ruby harness build never invokes `sudo` and never touches the network. fn ruby_build_command(bundle: &str, workdir: &Path) -> Command { let mut cmd = Command::new(bundle); - cmd.current_dir(workdir) - .env_clear() - .env("PATH", std::env::var("PATH").unwrap_or_default()) - .env("HOME", std::env::var("HOME").unwrap_or_default()); + apply_basic_build_env(&mut cmd); + cmd.current_dir(workdir); for (k, v) in ruby_hermetic_env(workdir) { cmd.env(k, v); } @@ -713,22 +742,36 @@ pub fn prepare_node(spec: &HarnessSpec, workdir: &Path) -> Result Result<(), String> { fn try_npm_install(workdir: &Path) -> Result<(), String> { let npm = std::env::var("NYX_NPM_BIN").unwrap_or_else(|_| "npm".to_owned()); - let output = Command::new(&npm) + let mut cmd = Command::new(&npm); + apply_basic_build_env(&mut cmd); + let output = cmd .args(["install", "--no-save", "--no-audit", "--no-fund"]) .current_dir(workdir) - .env_clear() - .env("PATH", std::env::var("PATH").unwrap_or_default()) - .env("HOME", std::env::var("HOME").unwrap_or_default()) .output() .map_err(|e| format!("npm install: {e}"))?; @@ -939,12 +981,11 @@ fn try_build_go_binary(workdir: &Path, binary_dest: &Path) -> Result<(), String> let go_mod_cache = std::env::var("GOMODCACHE").unwrap_or_else(|_| format!("{go_path}/pkg/mod")); if workdir.join("go.mod").exists() { - let output = Command::new(&go_bin) + let mut cmd = Command::new(&go_bin); + apply_basic_build_env(&mut cmd); + let output = cmd .args(["mod", "tidy"]) .current_dir(workdir) - .env_clear() - .env("PATH", std::env::var("PATH").unwrap_or_default()) - .env("HOME", std::env::var("HOME").unwrap_or_default()) .env("GOCACHE", &go_cache) .env("GOPATH", &go_path) .env("GOMODCACHE", &go_mod_cache) @@ -960,7 +1001,9 @@ fn try_build_go_binary(workdir: &Path, binary_dest: &Path) -> Result<(), String> } } - let output = Command::new(&go_bin) + let mut cmd = Command::new(&go_bin); + apply_basic_build_env(&mut cmd); + let output = cmd .args([ "build", "-o", @@ -968,9 +1011,6 @@ fn try_build_go_binary(workdir: &Path, binary_dest: &Path) -> Result<(), String> ".", ]) .current_dir(workdir) - .env_clear() - .env("PATH", std::env::var("PATH").unwrap_or_default()) - .env("HOME", std::env::var("HOME").unwrap_or_default()) .env("GOCACHE", go_cache) .env("GOPATH", go_path) .env("GOMODCACHE", go_mod_cache) @@ -1274,12 +1314,11 @@ fn try_compile_java_with_toolchain( } let javac = std::env::var("NYX_JAVAC_BIN").unwrap_or_else(|_| "javac".to_owned()); - let output = Command::new(&javac) + let mut cmd = Command::new(&javac); + apply_basic_build_env(&mut cmd); + let output = cmd .args(&args) .current_dir(workdir) - .env_clear() - .env("PATH", std::env::var("PATH").unwrap_or_default()) - .env("HOME", std::env::var("HOME").unwrap_or_default()) .output() .map_err(|e| format!("javac: {e}"))?; @@ -1332,12 +1371,11 @@ fn finalize_java_compile(workdir: &Path, cache_path: &Path, lib_on_cp: bool) -> /// build path can surface it as `BuildFailed` upstream. fn fetch_maven_deps(workdir: &Path) -> Result<(), String> { let mvn = std::env::var("NYX_MAVEN_BIN").unwrap_or_else(|_| "mvn".to_owned()); - let output = Command::new(&mvn) + let mut cmd = Command::new(&mvn); + apply_basic_build_env(&mut cmd); + let output = cmd .args(maven_copy_dependency_args()) .current_dir(workdir) - .env_clear() - .env("PATH", std::env::var("PATH").unwrap_or_default()) - .env("HOME", std::env::var("HOME").unwrap_or_default()) .output() .map_err(|e| format!("mvn dependency:copy-dependencies: {e}"))?; @@ -1451,19 +1489,33 @@ pub fn prepare_php(spec: &HarnessSpec, workdir: &Path) -> Result Result<(), String> { fn try_composer_install(workdir: &Path) -> Result<(), String> { let composer = std::env::var("NYX_COMPOSER_BIN").unwrap_or_else(|_| "composer".to_owned()); - let output = Command::new(&composer) + let mut cmd = Command::new(&composer); + apply_basic_build_env(&mut cmd); + let output = cmd .args(["install", "--no-interaction", "--no-dev", "--prefer-dist"]) .current_dir(workdir) - .env_clear() - .env("PATH", std::env::var("PATH").unwrap_or_default()) - .env("HOME", std::env::var("HOME").unwrap_or_default()) .env("COMPOSER_ALLOW_SUPERUSER", "1") .output() .map_err(|e| format!("composer install: {e}"))?; @@ -1706,12 +1757,11 @@ fn run_cc( let mut args: Vec<&str> = leading_flags.to_vec(); args.extend(["-o", binary_str, "main.c"]); - let output = Command::new(cc_bin) + let mut cmd = Command::new(cc_bin); + apply_basic_build_env(&mut cmd); + let output = cmd .args(&args) .current_dir(workdir) - .env_clear() - .env("PATH", std::env::var("PATH").unwrap_or_default()) - .env("HOME", std::env::var("HOME").unwrap_or_default()) .output() .map_err(|e| format!("cc: {e}"))?; @@ -1817,7 +1867,9 @@ fn try_build_cpp_binary(workdir: &Path, binary_dest: &Path) -> Result<(), String // Prefer c++ which resolves to the system default compiler driver. "c++".to_owned() }); - let output = Command::new(&cxx_bin) + let mut cmd = Command::new(&cxx_bin); + apply_basic_build_env(&mut cmd); + let output = cmd .args([ "-O0", "-g", @@ -1827,9 +1879,6 @@ fn try_build_cpp_binary(workdir: &Path, binary_dest: &Path) -> Result<(), String "main.cpp", ]) .current_dir(workdir) - .env_clear() - .env("PATH", std::env::var("PATH").unwrap_or_default()) - .env("HOME", std::env::var("HOME").unwrap_or_default()) .output() .map_err(|e| format!("c++: {e}"))?; diff --git a/src/dynamic/lang/rust.rs b/src/dynamic/lang/rust.rs index d6dc2864..f88114a6 100644 --- a/src/dynamic/lang/rust.rs +++ b/src/dynamic/lang/rust.rs @@ -79,11 +79,11 @@ impl LangEmitter for RustEmitter { /// /// Splices the Rust probe shim ([`probe_shim`]) in front of a minimal /// driver that reads `NYX_PREV_OUTPUT` and writes it on stdout. The -/// shim references `libc::*` from its `__nyx_install_crash_guard` -/// definition, so a single-file `rustc step.rs` build cannot resolve -/// the symbols. Instead the step ships a companion `Cargo.toml` -/// pinning `libc = "0.2"` via [`ChainStepHarness::extra_files`] and -/// drives the build through `cargo run --quiet`. +/// shim installs its crash guard through a tiny generated POSIX FFI +/// wrapper, so std-only steps do not need to fetch crates before they +/// can run. The companion `Cargo.toml` is still emitted because the +/// chain driver uses `cargo run --quiet` for parity with normal Rust +/// harnesses. /// /// When `terminal` is set, the driver also calls /// `__nyx_probe(callee, &[&prev])` and prints @@ -113,8 +113,7 @@ fn chain_step( [[bin]]\n\ name = \"step\"\n\ path = \"step.rs\"\n\n\ - [dependencies]\n\ - libc = \"0.2\"\n" + [dependencies]\n" .to_owned(); ChainStepHarness { source, @@ -330,11 +329,39 @@ fn __nyx_probe(sink_callee: &str, args: &[&str]) { __nyx_emit(&line); } -// Phase 08: install a sink-site signal handler via `libc::sigaction` so a -// SIGSEGV / SIGABRT / etc. inside the sink call is captured as a Crash -// probe before the kernel re-delivers it via SIG_DFL. The shim is -// no-op on non-Unix targets (the dynamic-verification supported set is -// Unix-only) so consumers can splice it unconditionally. +// Phase 08: install a sink-site signal handler via a tiny generated POSIX +// `signal(3)` / `raise(3)` FFI wrapper so SIGSEGV / SIGABRT / etc. inside the +// sink call is captured as a Crash probe before the kernel re-delivers it via +// SIG_DFL. The shim is no-op on non-Unix targets (the dynamic-verification +// supported set is Unix-only) so consumers can splice it unconditionally. +#[cfg(unix)] +#[allow(dead_code)] +mod __nyx_unix_signal { + pub const SIG_DFL: usize = 0; + pub const SIGSEGV: i32 = 11; + pub const SIGABRT: i32 = 6; + #[cfg(target_os = "macos")] + pub const SIGBUS: i32 = 10; + #[cfg(not(target_os = "macos"))] + pub const SIGBUS: i32 = 7; + pub const SIGFPE: i32 = 8; + pub const SIGILL: i32 = 4; + + unsafe extern "C" { + fn signal(sig: i32, handler: usize) -> usize; + fn raise(sig: i32) -> i32; + } + + pub unsafe fn install(sig: i32, handler: extern "C" fn(i32)) { + let _ = unsafe { signal(sig, handler as usize) }; + } + + pub unsafe fn reset_and_raise(sig: i32) { + let _ = unsafe { signal(sig, SIG_DFL) }; + let _ = unsafe { raise(sig) }; + } +} + #[cfg(unix)] #[allow(dead_code)] fn __nyx_install_crash_guard(sink_callee: &'static str) { @@ -349,11 +376,11 @@ fn __nyx_install_crash_guard(sink_callee: &'static str) { // accept the risk because the process is already dying and we // need the forensic record. let name = match sig { - libc::SIGSEGV => "SIGSEGV", - libc::SIGABRT => "SIGABRT", - libc::SIGBUS => "SIGBUS", - libc::SIGFPE => "SIGFPE", - libc::SIGILL => "SIGILL", + __nyx_unix_signal::SIGSEGV => "SIGSEGV", + __nyx_unix_signal::SIGABRT => "SIGABRT", + __nyx_unix_signal::SIGBUS => "SIGBUS", + __nyx_unix_signal::SIGFPE => "SIGFPE", + __nyx_unix_signal::SIGILL => "SIGILL", _ => "SIGABRT", }; let p = SINK_CALLEE.load(Ordering::SeqCst); @@ -385,18 +412,18 @@ fn __nyx_install_crash_guard(sink_callee: &'static str) { __nyx_emit(&line); // Restore default handler and re-raise so process actually dies. unsafe { - let mut sa: libc::sigaction = std::mem::zeroed(); - sa.sa_sigaction = libc::SIG_DFL; - libc::sigaction(sig, &sa, std::ptr::null_mut()); - libc::raise(sig); + __nyx_unix_signal::reset_and_raise(sig); } } unsafe { - let mut sa: libc::sigaction = std::mem::zeroed(); - sa.sa_sigaction = handler as usize; - libc::sigemptyset(&mut sa.sa_mask); - for sig in [libc::SIGSEGV, libc::SIGABRT, libc::SIGBUS, libc::SIGFPE, libc::SIGILL] { - libc::sigaction(sig, &sa, std::ptr::null_mut()); + for sig in [ + __nyx_unix_signal::SIGSEGV, + __nyx_unix_signal::SIGABRT, + __nyx_unix_signal::SIGBUS, + __nyx_unix_signal::SIGFPE, + __nyx_unix_signal::SIGILL, + ] { + __nyx_unix_signal::install(sig, handler); } } } @@ -2670,10 +2697,9 @@ fn is_ident_char(ch: char) -> bool { /// - `SQL_QUERY` → `rusqlite` with the `bundled` feature (embeds SQLite). /// - Other caps use only std (no extra deps). /// -/// `libc` is always pinned because the Phase 16 probe shim (spliced into -/// `src/main.rs` by `generate_main_rs`) calls `libc::sigaction` from -/// `__nyx_install_crash_guard`. The shim is unconditionally compiled so -/// the dep must be unconditional too. +/// The Phase 16 probe shim is std-only: its Unix crash guard declares the +/// handful of POSIX symbols it needs directly, so ordinary Rust fixtures do +/// not need network access just to fetch `libc`. pub fn generate_cargo_toml(cap: Cap) -> String { generate_cargo_toml_with_extras(cap, false) } @@ -2736,7 +2762,6 @@ fn generate_cargo_toml_for_spec(cap: Cap, shape: RustShape, spec: &HarnessSpec) pub fn generate_cargo_toml_with_extras(cap: Cap, needs_percent_encoding: bool) -> String { let mut deps = String::new(); - deps.push_str("libc = \"0.2\"\n"); if cap.contains(Cap::SQL_QUERY) { deps.push_str("rusqlite = { version = \"0.39\", features = [\"bundled\"] }\n"); } @@ -3449,15 +3474,14 @@ mod tests { } #[test] - fn cargo_toml_always_pins_libc_for_probe_shim() { - // Phase 16 follow-up: the probe shim calls `libc::sigaction` so - // `libc` must be unconditionally pinned (independent of the - // expected_cap dep matrix). + fn cargo_toml_does_not_pin_libc_for_std_only_probe_shim() { + // The probe shim declares its tiny POSIX FFI inline, so std-only + // fixtures must not need crates.io just to build the harness. for cap in [Cap::SQL_QUERY, Cap::CODE_EXEC, Cap::FILE_IO, Cap::SSRF] { let cargo = generate_cargo_toml(cap); assert!( - cargo.contains("libc = \"0.2\""), - "libc dep missing for cap={cap:?}", + !cargo.contains("libc = \"0.2\""), + "unexpected libc dep for cap={cap:?}" ); } } @@ -3476,9 +3500,9 @@ mod tests { // Phase 26 follow-up: Rust chain_step now splices the probe // shim ahead of the driver so a chain step that terminates at // a sink can drive the `__nyx_probe` channel directly. The - // shim references `libc::*` so the step also ships a companion + // shim stays std-only, but the step still ships a companion // `Cargo.toml` via `extra_files` and drives the build through - // `cargo run --quiet` rather than single-file `rustc`. + // `cargo run --quiet` to match normal Rust harness execution. let step = chain_step(Some(b"prev-output"), None); assert!( step.source.contains("__nyx_probe shim (Phase 06"), @@ -3980,15 +4004,14 @@ mod tests { #[test] fn cargo_toml_extras_pins_percent_encoding_when_requested() { let cargo = generate_cargo_toml_with_extras(Cap::HEADER_INJECTION, true); - assert!(cargo.contains("libc = \"0.2\"")); assert!(cargo.contains("percent-encoding = \"2\"")); let cargo_no_extras = generate_cargo_toml_with_extras(Cap::HEADER_INJECTION, false); - assert!(cargo_no_extras.contains("libc = \"0.2\"")); assert!(!cargo_no_extras.contains("percent-encoding")); + assert!(!cargo_no_extras.contains("libc = \"0.2\"")); } #[test] - fn chain_step_emits_cargo_toml_with_libc_dep() { + fn chain_step_emits_std_only_cargo_toml() { let step = chain_step(None, None); let cargo = step .extra_files @@ -3997,8 +4020,8 @@ mod tests { .expect("Cargo.toml must be in extra_files for cargo run"); let body = &cargo.1; assert!( - body.contains("libc = \"0.2\""), - "Cargo.toml must pin libc for the probe shim's sigaction path, got: {body}", + !body.contains("libc = \"0.2\""), + "chain-step Cargo.toml should stay std-only for the inline signal FFI, got: {body}", ); assert!( body.contains("path = \"step.rs\""), @@ -4107,8 +4130,8 @@ mod tests { cargo.1 ); assert!( - cargo.1.contains("libc = \"0.2\""), - "Rust CRYPTO harness Cargo.toml must keep libc dep for the probe shim's sigaction path", + !cargo.1.contains("libc = \"0.2\""), + "Rust CRYPTO harness should not need libc after the inline signal FFI change", ); } @@ -4242,8 +4265,8 @@ mod tests { cargo.1 ); assert!( - cargo.1.contains("libc = \"0.2\""), - "Rust JSON_PARSE harness Cargo.toml must keep libc dep for the probe shim's sigaction path", + !cargo.1.contains("libc = \"0.2\""), + "Rust JSON_PARSE harness should not need libc after the inline signal FFI change", ); } diff --git a/src/dynamic/runner.rs b/src/dynamic/runner.rs index 957dad07..5782dff0 100644 --- a/src/dynamic/runner.rs +++ b/src/dynamic/runner.rs @@ -23,6 +23,7 @@ use crate::evidence::{DifferentialOutcome, DifferentialVerdict}; use crate::labels::Cap; use crate::symbol::Lang; use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; /// Record a trace event on the caller's [`VerifyTrace`] handle if one @@ -55,6 +56,46 @@ fn oracle_short_name(oracle: &Oracle) -> &'static str { /// Max harness-build attempts before giving up. const MAX_BUILD_ATTEMPTS: u32 = 2; +fn stage_native_harness_command( + harness: &mut harness::BuiltHarness, + build_root: &Path, + fallback: PathBuf, +) { + let cached = build_root.join("nyx_harness"); + let source = if cached.exists() { + cached + } else if fallback.exists() { + fallback + } else { + return; + }; + let run_path = harness.workdir.join("nyx_harness"); + if source != run_path { + if let Some(parent) = run_path.parent() { + let _ = std::fs::create_dir_all(parent); + } + if std::fs::copy(&source, &run_path).is_ok() { + make_executable(&run_path); + harness.command = vec![run_path.to_string_lossy().into_owned()]; + return; + } + } + harness.command = vec![source.to_string_lossy().into_owned()]; +} + +#[cfg(unix)] +fn make_executable(path: &Path) { + use std::os::unix::fs::PermissionsExt; + if let Ok(meta) = std::fs::metadata(path) { + let mut perms = meta.permissions(); + perms.set_mode(perms.mode() | 0o700); + let _ = std::fs::set_permissions(path, perms); + } +} + +#[cfg(not(unix))] +fn make_executable(_path: &Path) {} + #[derive(Debug)] pub struct RunOutcome { pub spec: HarnessSpec, @@ -260,21 +301,12 @@ pub fn run_spec(spec: &HarnessSpec, opts: &SandboxOptions) -> Result { - // Update command to the compiled binary path. - let binary = build_result.venv_path.join("nyx_harness"); - if binary.exists() { - harness.command = vec![binary.to_string_lossy().into_owned()]; - } else { - // Fall back to binary inside the workdir. - let fallback = harness - .workdir - .join("target") - .join("release") - .join("nyx_harness"); - if fallback.exists() { - harness.command = vec![fallback.to_string_lossy().into_owned()]; - } - } + let fallback = harness + .workdir + .join("target") + .join("release") + .join("nyx_harness"); + stage_native_harness_command(&mut harness, &build_result.venv_path, fallback); } Err(build_sandbox::BuildError::BuildFailed { stderr, attempts }) => { return Err(RunError::BuildFailed { stderr, attempts }); @@ -305,15 +337,8 @@ pub fn run_spec(spec: &HarnessSpec, opts: &SandboxOptions) -> Result { - let binary = build_result.venv_path.join("nyx_harness"); - if binary.exists() { - harness.command = vec![binary.to_string_lossy().into_owned()]; - } else { - let fallback = harness.workdir.join("nyx_harness"); - if fallback.exists() { - harness.command = vec![fallback.to_string_lossy().into_owned()]; - } - } + let fallback = harness.workdir.join("nyx_harness"); + stage_native_harness_command(&mut harness, &build_result.venv_path, fallback); } Err(build_sandbox::BuildError::BuildFailed { stderr, attempts }) => { return Err(RunError::BuildFailed { stderr, attempts }); @@ -403,15 +428,8 @@ pub fn run_spec(spec: &HarnessSpec, opts: &SandboxOptions) -> Result { - let binary = build_result.venv_path.join("nyx_harness"); - if binary.exists() { - harness.command = vec![binary.to_string_lossy().into_owned()]; - } else { - let fallback = harness.workdir.join("nyx_harness"); - if fallback.exists() { - harness.command = vec![fallback.to_string_lossy().into_owned()]; - } - } + let fallback = harness.workdir.join("nyx_harness"); + stage_native_harness_command(&mut harness, &build_result.venv_path, fallback); } Err(build_sandbox::BuildError::BuildFailed { stderr, attempts }) => { return Err(RunError::BuildFailed { stderr, attempts }); @@ -423,15 +441,8 @@ pub fn run_spec(spec: &HarnessSpec, opts: &SandboxOptions) -> Result { - let binary = build_result.venv_path.join("nyx_harness"); - if binary.exists() { - harness.command = vec![binary.to_string_lossy().into_owned()]; - } else { - let fallback = harness.workdir.join("nyx_harness"); - if fallback.exists() { - harness.command = vec![fallback.to_string_lossy().into_owned()]; - } - } + let fallback = harness.workdir.join("nyx_harness"); + stage_native_harness_command(&mut harness, &build_result.venv_path, fallback); } Err(build_sandbox::BuildError::BuildFailed { stderr, attempts }) => { return Err(RunError::BuildFailed { stderr, attempts }); diff --git a/tests/go_fixtures.rs b/tests/go_fixtures.rs index 02d6d008..c9fed4e0 100644 --- a/tests/go_fixtures.rs +++ b/tests/go_fixtures.rs @@ -15,6 +15,7 @@ mod common; mod go_fixture_tests { use crate::common::fixture_harness::FIXTURE_LOCK; use nyx_scanner::commands::scan::Diag; + use nyx_scanner::dynamic::sandbox::SandboxBackend; use nyx_scanner::dynamic::verify::{VerifyOptions, verify_finding}; use nyx_scanner::evidence::{ Confidence, Evidence, FlowStep, FlowStepKind, InconclusiveReason, UnsupportedReason, @@ -80,7 +81,8 @@ mod go_fixture_tests { } let diag = make_diag(&path, func, cap, sink_line); - let opts = VerifyOptions::default(); + let mut opts = VerifyOptions::default(); + opts.sandbox.backend = SandboxBackend::Process; let result = verify_finding(&diag, &opts); unsafe {