fix failing tests

This commit is contained in:
elipeter 2026-06-03 16:48:12 -05:00
parent 7fe1abda8b
commit b32dc7ac0b
5 changed files with 284 additions and 167 deletions

View file

@ -121,8 +121,9 @@ pub fn is_pool_enabled(lang: &str) -> bool {
/// dir is available — callers treat that as "pool unavailable" and fall /// dir is available — callers treat that as "pool unavailable" and fall
/// back to the legacy direct-spawn build path. /// back to the legacy direct-spawn build path.
pub(crate) fn pool_cache_dir(lang: &str, sub: &str) -> Option<PathBuf> { pub(crate) fn pool_cache_dir(lang: &str, sub: &str) -> Option<PathBuf> {
let base = if let Ok(custom) = std::env::var("NYX_BUILD_POOL_DIR") { let custom = std::env::var("NYX_BUILD_POOL_DIR").ok().map(PathBuf::from);
PathBuf::from(custom) let base = if let Some(custom) = custom.clone() {
custom
} else { } else {
directories::ProjectDirs::from("dev", "nyx", "nyx")? directories::ProjectDirs::from("dev", "nyx", "nyx")?
.cache_dir() .cache_dir()
@ -130,8 +131,27 @@ pub(crate) fn pool_cache_dir(lang: &str, sub: &str) -> Option<PathBuf> {
.join("build-pool") .join("build-pool")
}; };
let dir = base.join(lang).join(sub); let dir = base.join(lang).join(sub);
std::fs::create_dir_all(&dir).ok()?; if ensure_writable_dir(&dir).is_some() {
Some(dir) 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<PathBuf> {
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 /// 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<PathBuf> {
/// (`CARGO_TARGET_DIR`, `CCACHE_DIR`, `GOCACHE`, …) on top of this. /// (`CARGO_TARGET_DIR`, `CCACHE_DIR`, `GOCACHE`, …) on top of this.
pub(crate) fn base_command(bin: &str) -> Command { pub(crate) fn base_command(bin: &str) -> Command {
let mut cmd = Command::new(bin); let mut cmd = Command::new(bin);
let tmp = build_temp_dir();
cmd.env_clear() cmd.env_clear()
.env("PATH", std::env::var("PATH").unwrap_or_default()) .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 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 /// Hermetic Bundler / RubyGems environment pinned to a writable per-workdir
/// vendor directory. /// vendor directory.
/// ///

View file

@ -122,12 +122,11 @@ fn try_build_rust_binary(workdir: &Path, binary_dest: &Path) -> Result<(), Strin
let cargo = cargo_binary(); let cargo = cargo_binary();
// Run `cargo build --release` in the workdir. // 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"]) .args(["build", "--release"])
.current_dir(workdir) .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. // Inherit CARGO_HOME so the local registry cache is reused.
.env( .env(
"CARGO_HOME", "CARGO_HOME",
@ -326,12 +325,11 @@ fn try_build_venv(venv_path: &Path, workdir: &Path, spec: &HarnessSpec) -> Resul
let python = python_binary(spec); let python = python_binary(spec);
// Create the venv. // 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"]) .args(["-m", "venv", "--clear"])
.arg(venv_path) .arg(venv_path)
.env_clear()
.env("PATH", std::env::var("PATH").unwrap_or_default())
.env("HOME", std::env::var("HOME").unwrap_or_default())
.status() .status()
.map_err(|e| format!("venv create: {e}"))?; .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"); let req_path = workdir.join("requirements.txt");
if req_path.exists() { if req_path.exists() {
let pip = venv_path.join("bin").join("pip"); 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"]) .args(["install", "--no-cache-dir", "-r"])
.arg(&req_path) .arg(&req_path)
.env_clear()
.env("PATH", std::env::var("PATH").unwrap_or_default())
.env("HOME", std::env::var("HOME").unwrap_or_default())
.output() .output()
.map_err(|e| format!("pip install: {e}"))?; .map_err(|e| format!("pip install: {e}"))?;
@ -414,21 +411,30 @@ fn build_cache_path(
let name = format!("{lockfile_hash}-{language}-{toolchain_id}"); let name = format!("{lockfile_hash}-{language}-{toolchain_id}");
let path = base.join(&name); let path = base.join(&name);
match create_build_cache_dir(&path) { match prepare_build_cache_dir(&path) {
Ok(()) => Ok(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() let fallback = std::env::temp_dir()
.join("nyx") .join("nyx")
.join("dynamic") .join("dynamic")
.join("build-cache") .join("build-cache")
.join(&name); .join(&name);
create_build_cache_dir(&fallback)?; prepare_build_cache_dir(&fallback)?;
Ok(fallback) Ok(fallback)
} }
Err(e) => Err(BuildError::Io(e)), 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<()> { fn create_build_cache_dir(path: &Path) -> std::io::Result<()> {
std::fs::create_dir_all(path)?; std::fs::create_dir_all(path)?;
#[cfg(unix)] #[cfg(unix)]
@ -439,6 +445,32 @@ fn create_build_cache_dir(path: &Path) -> std::io::Result<()> {
Ok(()) 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"; const PYTHON_CACHE_DONE: &str = ".python_cache_done";
fn python_cache_done_path(cache_path: &Path) -> PathBuf { fn python_cache_done_path(cache_path: &Path) -> PathBuf {
@ -636,12 +668,11 @@ fn bundle_check(bundle: &str, workdir: &Path) -> Result<bool, String> {
// 1.x's view of already-installed system gems and produces spurious // 1.x's view of already-installed system gems and produces spurious
// BuildFailed for a Gemfile the host can already satisfy. See the parallel // BuildFailed for a Gemfile the host can already satisfy. See the parallel
// comment in `RubyPool::compile_batch`. // 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") .arg("check")
.current_dir(workdir) .current_dir(workdir)
.env_clear()
.env("PATH", std::env::var("PATH").unwrap_or_default())
.env("HOME", std::env::var("HOME").unwrap_or_default())
.output() .output()
.map_err(|e| format!("bundle check: {e}"))?; .map_err(|e| format!("bundle check: {e}"))?;
Ok(output.status.success()) Ok(output.status.success())
@ -654,10 +685,8 @@ fn bundle_check(bundle: &str, workdir: &Path) -> Result<bool, String> {
/// Ruby harness build never invokes `sudo` and never touches the network. /// Ruby harness build never invokes `sudo` and never touches the network.
fn ruby_build_command(bundle: &str, workdir: &Path) -> Command { fn ruby_build_command(bundle: &str, workdir: &Path) -> Command {
let mut cmd = Command::new(bundle); let mut cmd = Command::new(bundle);
cmd.current_dir(workdir) apply_basic_build_env(&mut cmd);
.env_clear() cmd.current_dir(workdir);
.env("PATH", std::env::var("PATH").unwrap_or_default())
.env("HOME", std::env::var("HOME").unwrap_or_default());
for (k, v) in ruby_hermetic_env(workdir) { for (k, v) in ruby_hermetic_env(workdir) {
cmd.env(k, v); cmd.env(k, v);
} }
@ -713,22 +742,36 @@ pub fn prepare_node(spec: &HarnessSpec, workdir: &Path) -> Result<BuildResult, B
let cache_path = build_cache_path(&lockfile_hash, "node", &spec.toolchain_id)?; let cache_path = build_cache_path(&lockfile_hash, "node", &spec.toolchain_id)?;
let _cache_guard = acquire_cache_build_lock(&cache_path)?; let _cache_guard = acquire_cache_build_lock(&cache_path)?;
let has_package_json = workdir.join("package.json").exists();
// Cache hit: node_modules already installed. Restore to fresh workdir if // Cache hit: node_modules already installed. Restore to fresh workdir if
// a different finding shares the same cache key but got a new workdir. // a different finding shares the same cache key but got a new workdir.
if cache_path.join(".node_cache_done").exists() { if cache_path.join(".node_cache_done").exists() {
let cached_nm = cache_path.join("node_modules"); let cached_nm = cache_path.join("node_modules");
if cached_nm.exists() && !workdir.join("node_modules").exists() { if !has_package_json {
let _ = copy_dir_all(&cached_nm, &workdir.join("node_modules")); return Ok(BuildResult {
venv_path: cache_path,
cache_hit: true,
duration: std::time::Duration::ZERO,
});
} }
return Ok(BuildResult { if cached_nm.exists() {
venv_path: cache_path, if !workdir.join("node_modules").exists() {
cache_hit: true, let _ = copy_dir_all(&cached_nm, &workdir.join("node_modules"));
duration: std::time::Duration::ZERO, }
}); if workdir.join("node_modules").exists() {
return Ok(BuildResult {
venv_path: cache_path,
cache_hit: true,
duration: std::time::Duration::ZERO,
});
}
}
let _ = std::fs::remove_file(cache_path.join(".node_cache_done"));
} }
// No package.json = no deps to install. // No package.json = no deps to install.
if !workdir.join("package.json").exists() { if !has_package_json {
std::fs::write(cache_path.join(".node_cache_done"), b"no-package-json")?; std::fs::write(cache_path.join(".node_cache_done"), b"no-package-json")?;
return Ok(BuildResult { return Ok(BuildResult {
venv_path: cache_path, venv_path: cache_path,
@ -794,12 +837,11 @@ fn npm_install(workdir: &Path) -> Result<(), String> {
fn try_npm_install(workdir: &Path) -> 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 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"]) .args(["install", "--no-save", "--no-audit", "--no-fund"])
.current_dir(workdir) .current_dir(workdir)
.env_clear()
.env("PATH", std::env::var("PATH").unwrap_or_default())
.env("HOME", std::env::var("HOME").unwrap_or_default())
.output() .output()
.map_err(|e| format!("npm install: {e}"))?; .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")); let go_mod_cache = std::env::var("GOMODCACHE").unwrap_or_else(|_| format!("{go_path}/pkg/mod"));
if workdir.join("go.mod").exists() { 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"]) .args(["mod", "tidy"])
.current_dir(workdir) .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("GOCACHE", &go_cache)
.env("GOPATH", &go_path) .env("GOPATH", &go_path)
.env("GOMODCACHE", &go_mod_cache) .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([ .args([
"build", "build",
"-o", "-o",
@ -968,9 +1011,6 @@ fn try_build_go_binary(workdir: &Path, binary_dest: &Path) -> Result<(), String>
".", ".",
]) ])
.current_dir(workdir) .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("GOCACHE", go_cache)
.env("GOPATH", go_path) .env("GOPATH", go_path)
.env("GOMODCACHE", go_mod_cache) .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 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) .args(&args)
.current_dir(workdir) .current_dir(workdir)
.env_clear()
.env("PATH", std::env::var("PATH").unwrap_or_default())
.env("HOME", std::env::var("HOME").unwrap_or_default())
.output() .output()
.map_err(|e| format!("javac: {e}"))?; .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. /// build path can surface it as `BuildFailed` upstream.
fn fetch_maven_deps(workdir: &Path) -> Result<(), String> { fn fetch_maven_deps(workdir: &Path) -> Result<(), String> {
let mvn = std::env::var("NYX_MAVEN_BIN").unwrap_or_else(|_| "mvn".to_owned()); 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()) .args(maven_copy_dependency_args())
.current_dir(workdir) .current_dir(workdir)
.env_clear()
.env("PATH", std::env::var("PATH").unwrap_or_default())
.env("HOME", std::env::var("HOME").unwrap_or_default())
.output() .output()
.map_err(|e| format!("mvn dependency:copy-dependencies: {e}"))?; .map_err(|e| format!("mvn dependency:copy-dependencies: {e}"))?;
@ -1451,19 +1489,33 @@ pub fn prepare_php(spec: &HarnessSpec, workdir: &Path) -> Result<BuildResult, Bu
let cache_path = build_cache_path(&lockfile_hash, "php", &spec.toolchain_id)?; let cache_path = build_cache_path(&lockfile_hash, "php", &spec.toolchain_id)?;
let _cache_guard = acquire_cache_build_lock(&cache_path)?; let _cache_guard = acquire_cache_build_lock(&cache_path)?;
let has_composer_json = workdir.join("composer.json").exists();
if cache_path.join(".php_cache_done").exists() { if cache_path.join(".php_cache_done").exists() {
let cached_vendor = cache_path.join("vendor"); let cached_vendor = cache_path.join("vendor");
if cached_vendor.exists() && !workdir.join("vendor").exists() { if !has_composer_json {
let _ = copy_dir_all(&cached_vendor, &workdir.join("vendor")); return Ok(BuildResult {
venv_path: cache_path,
cache_hit: true,
duration: std::time::Duration::ZERO,
});
} }
return Ok(BuildResult { if cached_vendor.join("autoload.php").exists() {
venv_path: cache_path, if !workdir.join("vendor").exists() {
cache_hit: true, let _ = copy_dir_all(&cached_vendor, &workdir.join("vendor"));
duration: std::time::Duration::ZERO, }
}); if workdir.join("vendor").join("autoload.php").exists() {
return Ok(BuildResult {
venv_path: cache_path,
cache_hit: true,
duration: std::time::Duration::ZERO,
});
}
}
let _ = std::fs::remove_file(cache_path.join(".php_cache_done"));
} }
if !workdir.join("composer.json").exists() { if !has_composer_json {
std::fs::write(cache_path.join(".php_cache_done"), b"no-composer-json")?; std::fs::write(cache_path.join(".php_cache_done"), b"no-composer-json")?;
return Ok(BuildResult { return Ok(BuildResult {
venv_path: cache_path, venv_path: cache_path,
@ -1529,12 +1581,11 @@ fn composer_install(workdir: &Path) -> Result<(), String> {
fn try_composer_install(workdir: &Path) -> 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 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"]) .args(["install", "--no-interaction", "--no-dev", "--prefer-dist"])
.current_dir(workdir) .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") .env("COMPOSER_ALLOW_SUPERUSER", "1")
.output() .output()
.map_err(|e| format!("composer install: {e}"))?; .map_err(|e| format!("composer install: {e}"))?;
@ -1706,12 +1757,11 @@ fn run_cc(
let mut args: Vec<&str> = leading_flags.to_vec(); let mut args: Vec<&str> = leading_flags.to_vec();
args.extend(["-o", binary_str, "main.c"]); 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) .args(&args)
.current_dir(workdir) .current_dir(workdir)
.env_clear()
.env("PATH", std::env::var("PATH").unwrap_or_default())
.env("HOME", std::env::var("HOME").unwrap_or_default())
.output() .output()
.map_err(|e| format!("cc: {e}"))?; .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. // Prefer c++ which resolves to the system default compiler driver.
"c++".to_owned() "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([ .args([
"-O0", "-O0",
"-g", "-g",
@ -1827,9 +1879,6 @@ fn try_build_cpp_binary(workdir: &Path, binary_dest: &Path) -> Result<(), String
"main.cpp", "main.cpp",
]) ])
.current_dir(workdir) .current_dir(workdir)
.env_clear()
.env("PATH", std::env::var("PATH").unwrap_or_default())
.env("HOME", std::env::var("HOME").unwrap_or_default())
.output() .output()
.map_err(|e| format!("c++: {e}"))?; .map_err(|e| format!("c++: {e}"))?;

View file

@ -79,11 +79,11 @@ impl LangEmitter for RustEmitter {
/// ///
/// Splices the Rust probe shim ([`probe_shim`]) in front of a minimal /// Splices the Rust probe shim ([`probe_shim`]) in front of a minimal
/// driver that reads `NYX_PREV_OUTPUT` and writes it on stdout. The /// driver that reads `NYX_PREV_OUTPUT` and writes it on stdout. The
/// shim references `libc::*` from its `__nyx_install_crash_guard` /// shim installs its crash guard through a tiny generated POSIX FFI
/// definition, so a single-file `rustc step.rs` build cannot resolve /// wrapper, so std-only steps do not need to fetch crates before they
/// the symbols. Instead the step ships a companion `Cargo.toml` /// can run. The companion `Cargo.toml` is still emitted because the
/// pinning `libc = "0.2"` via [`ChainStepHarness::extra_files`] and /// chain driver uses `cargo run --quiet` for parity with normal Rust
/// drives the build through `cargo run --quiet`. /// harnesses.
/// ///
/// When `terminal` is set, the driver also calls /// When `terminal` is set, the driver also calls
/// `__nyx_probe(callee, &[&prev])` and prints /// `__nyx_probe(callee, &[&prev])` and prints
@ -113,8 +113,7 @@ fn chain_step(
[[bin]]\n\ [[bin]]\n\
name = \"step\"\n\ name = \"step\"\n\
path = \"step.rs\"\n\n\ path = \"step.rs\"\n\n\
[dependencies]\n\ [dependencies]\n"
libc = \"0.2\"\n"
.to_owned(); .to_owned();
ChainStepHarness { ChainStepHarness {
source, source,
@ -330,11 +329,39 @@ fn __nyx_probe(sink_callee: &str, args: &[&str]) {
__nyx_emit(&line); __nyx_emit(&line);
} }
// Phase 08: install a sink-site signal handler via `libc::sigaction` so a // Phase 08: install a sink-site signal handler via a tiny generated POSIX
// SIGSEGV / SIGABRT / etc. inside the sink call is captured as a Crash // `signal(3)` / `raise(3)` FFI wrapper so SIGSEGV / SIGABRT / etc. inside the
// probe before the kernel re-delivers it via SIG_DFL. The shim is // sink call is captured as a Crash probe before the kernel re-delivers it via
// no-op on non-Unix targets (the dynamic-verification supported set is // SIG_DFL. The shim is no-op on non-Unix targets (the dynamic-verification
// Unix-only) so consumers can splice it unconditionally. // 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)] #[cfg(unix)]
#[allow(dead_code)] #[allow(dead_code)]
fn __nyx_install_crash_guard(sink_callee: &'static str) { 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 // accept the risk because the process is already dying and we
// need the forensic record. // need the forensic record.
let name = match sig { let name = match sig {
libc::SIGSEGV => "SIGSEGV", __nyx_unix_signal::SIGSEGV => "SIGSEGV",
libc::SIGABRT => "SIGABRT", __nyx_unix_signal::SIGABRT => "SIGABRT",
libc::SIGBUS => "SIGBUS", __nyx_unix_signal::SIGBUS => "SIGBUS",
libc::SIGFPE => "SIGFPE", __nyx_unix_signal::SIGFPE => "SIGFPE",
libc::SIGILL => "SIGILL", __nyx_unix_signal::SIGILL => "SIGILL",
_ => "SIGABRT", _ => "SIGABRT",
}; };
let p = SINK_CALLEE.load(Ordering::SeqCst); let p = SINK_CALLEE.load(Ordering::SeqCst);
@ -385,18 +412,18 @@ fn __nyx_install_crash_guard(sink_callee: &'static str) {
__nyx_emit(&line); __nyx_emit(&line);
// Restore default handler and re-raise so process actually dies. // Restore default handler and re-raise so process actually dies.
unsafe { unsafe {
let mut sa: libc::sigaction = std::mem::zeroed(); __nyx_unix_signal::reset_and_raise(sig);
sa.sa_sigaction = libc::SIG_DFL;
libc::sigaction(sig, &sa, std::ptr::null_mut());
libc::raise(sig);
} }
} }
unsafe { unsafe {
let mut sa: libc::sigaction = std::mem::zeroed(); for sig in [
sa.sa_sigaction = handler as usize; __nyx_unix_signal::SIGSEGV,
libc::sigemptyset(&mut sa.sa_mask); __nyx_unix_signal::SIGABRT,
for sig in [libc::SIGSEGV, libc::SIGABRT, libc::SIGBUS, libc::SIGFPE, libc::SIGILL] { __nyx_unix_signal::SIGBUS,
libc::sigaction(sig, &sa, std::ptr::null_mut()); __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). /// - `SQL_QUERY` → `rusqlite` with the `bundled` feature (embeds SQLite).
/// - Other caps use only std (no extra deps). /// - Other caps use only std (no extra deps).
/// ///
/// `libc` is always pinned because the Phase 16 probe shim (spliced into /// The Phase 16 probe shim is std-only: its Unix crash guard declares the
/// `src/main.rs` by `generate_main_rs`) calls `libc::sigaction` from /// handful of POSIX symbols it needs directly, so ordinary Rust fixtures do
/// `__nyx_install_crash_guard`. The shim is unconditionally compiled so /// not need network access just to fetch `libc`.
/// the dep must be unconditional too.
pub fn generate_cargo_toml(cap: Cap) -> String { pub fn generate_cargo_toml(cap: Cap) -> String {
generate_cargo_toml_with_extras(cap, false) 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 { pub fn generate_cargo_toml_with_extras(cap: Cap, needs_percent_encoding: bool) -> String {
let mut deps = String::new(); let mut deps = String::new();
deps.push_str("libc = \"0.2\"\n");
if cap.contains(Cap::SQL_QUERY) { if cap.contains(Cap::SQL_QUERY) {
deps.push_str("rusqlite = { version = \"0.39\", features = [\"bundled\"] }\n"); deps.push_str("rusqlite = { version = \"0.39\", features = [\"bundled\"] }\n");
} }
@ -3449,15 +3474,14 @@ mod tests {
} }
#[test] #[test]
fn cargo_toml_always_pins_libc_for_probe_shim() { fn cargo_toml_does_not_pin_libc_for_std_only_probe_shim() {
// Phase 16 follow-up: the probe shim calls `libc::sigaction` so // The probe shim declares its tiny POSIX FFI inline, so std-only
// `libc` must be unconditionally pinned (independent of the // fixtures must not need crates.io just to build the harness.
// expected_cap dep matrix).
for cap in [Cap::SQL_QUERY, Cap::CODE_EXEC, Cap::FILE_IO, Cap::SSRF] { for cap in [Cap::SQL_QUERY, Cap::CODE_EXEC, Cap::FILE_IO, Cap::SSRF] {
let cargo = generate_cargo_toml(cap); let cargo = generate_cargo_toml(cap);
assert!( assert!(
cargo.contains("libc = \"0.2\""), !cargo.contains("libc = \"0.2\""),
"libc dep missing for cap={cap:?}", "unexpected libc dep for cap={cap:?}"
); );
} }
} }
@ -3476,9 +3500,9 @@ mod tests {
// Phase 26 follow-up: Rust chain_step now splices the probe // Phase 26 follow-up: Rust chain_step now splices the probe
// shim ahead of the driver so a chain step that terminates at // shim ahead of the driver so a chain step that terminates at
// a sink can drive the `__nyx_probe` channel directly. The // 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.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); let step = chain_step(Some(b"prev-output"), None);
assert!( assert!(
step.source.contains("__nyx_probe shim (Phase 06"), step.source.contains("__nyx_probe shim (Phase 06"),
@ -3980,15 +4004,14 @@ mod tests {
#[test] #[test]
fn cargo_toml_extras_pins_percent_encoding_when_requested() { fn cargo_toml_extras_pins_percent_encoding_when_requested() {
let cargo = generate_cargo_toml_with_extras(Cap::HEADER_INJECTION, true); let cargo = generate_cargo_toml_with_extras(Cap::HEADER_INJECTION, true);
assert!(cargo.contains("libc = \"0.2\""));
assert!(cargo.contains("percent-encoding = \"2\"")); assert!(cargo.contains("percent-encoding = \"2\""));
let cargo_no_extras = generate_cargo_toml_with_extras(Cap::HEADER_INJECTION, false); 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("percent-encoding"));
assert!(!cargo_no_extras.contains("libc = \"0.2\""));
} }
#[test] #[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 step = chain_step(None, None);
let cargo = step let cargo = step
.extra_files .extra_files
@ -3997,8 +4020,8 @@ mod tests {
.expect("Cargo.toml must be in extra_files for cargo run"); .expect("Cargo.toml must be in extra_files for cargo run");
let body = &cargo.1; let body = &cargo.1;
assert!( assert!(
body.contains("libc = \"0.2\""), !body.contains("libc = \"0.2\""),
"Cargo.toml must pin libc for the probe shim's sigaction path, got: {body}", "chain-step Cargo.toml should stay std-only for the inline signal FFI, got: {body}",
); );
assert!( assert!(
body.contains("path = \"step.rs\""), body.contains("path = \"step.rs\""),
@ -4107,8 +4130,8 @@ mod tests {
cargo.1 cargo.1
); );
assert!( assert!(
cargo.1.contains("libc = \"0.2\""), !cargo.1.contains("libc = \"0.2\""),
"Rust CRYPTO harness Cargo.toml must keep libc dep for the probe shim's sigaction path", "Rust CRYPTO harness should not need libc after the inline signal FFI change",
); );
} }
@ -4242,8 +4265,8 @@ mod tests {
cargo.1 cargo.1
); );
assert!( assert!(
cargo.1.contains("libc = \"0.2\""), !cargo.1.contains("libc = \"0.2\""),
"Rust JSON_PARSE harness Cargo.toml must keep libc dep for the probe shim's sigaction path", "Rust JSON_PARSE harness should not need libc after the inline signal FFI change",
); );
} }

View file

@ -23,6 +23,7 @@ use crate::evidence::{DifferentialOutcome, DifferentialVerdict};
use crate::labels::Cap; use crate::labels::Cap;
use crate::symbol::Lang; use crate::symbol::Lang;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
/// Record a trace event on the caller's [`VerifyTrace`] handle if one /// 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. /// Max harness-build attempts before giving up.
const MAX_BUILD_ATTEMPTS: u32 = 2; 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)] #[derive(Debug)]
pub struct RunOutcome { pub struct RunOutcome {
pub spec: HarnessSpec, pub spec: HarnessSpec,
@ -260,21 +301,12 @@ pub fn run_spec(spec: &HarnessSpec, opts: &SandboxOptions) -> Result<RunOutcome,
// Compile the harness binary with `cargo build --release`. // Compile the harness binary with `cargo build --release`.
match build_sandbox::prepare_rust(spec, &harness.workdir) { match build_sandbox::prepare_rust(spec, &harness.workdir) {
Ok(build_result) => { Ok(build_result) => {
// Update command to the compiled binary path. let fallback = harness
let binary = build_result.venv_path.join("nyx_harness"); .workdir
if binary.exists() { .join("target")
harness.command = vec![binary.to_string_lossy().into_owned()]; .join("release")
} else { .join("nyx_harness");
// Fall back to binary inside the workdir. stage_native_harness_command(&mut harness, &build_result.venv_path, fallback);
let fallback = harness
.workdir
.join("target")
.join("release")
.join("nyx_harness");
if fallback.exists() {
harness.command = vec![fallback.to_string_lossy().into_owned()];
}
}
} }
Err(build_sandbox::BuildError::BuildFailed { stderr, attempts }) => { Err(build_sandbox::BuildError::BuildFailed { stderr, attempts }) => {
return Err(RunError::BuildFailed { stderr, attempts }); return Err(RunError::BuildFailed { stderr, attempts });
@ -305,15 +337,8 @@ pub fn run_spec(spec: &HarnessSpec, opts: &SandboxOptions) -> Result<RunOutcome,
// Compile the harness binary with `go build -o nyx_harness .`. // Compile the harness binary with `go build -o nyx_harness .`.
match build_sandbox::prepare_go(spec, &harness.workdir) { match build_sandbox::prepare_go(spec, &harness.workdir) {
Ok(build_result) => { Ok(build_result) => {
let binary = build_result.venv_path.join("nyx_harness"); let fallback = harness.workdir.join("nyx_harness");
if binary.exists() { stage_native_harness_command(&mut harness, &build_result.venv_path, fallback);
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()];
}
}
} }
Err(build_sandbox::BuildError::BuildFailed { stderr, attempts }) => { Err(build_sandbox::BuildError::BuildFailed { stderr, attempts }) => {
return Err(RunError::BuildFailed { stderr, attempts }); return Err(RunError::BuildFailed { stderr, attempts });
@ -403,15 +428,8 @@ pub fn run_spec(spec: &HarnessSpec, opts: &SandboxOptions) -> Result<RunOutcome,
// loader would otherwise miss `/lib*`. // loader would otherwise miss `/lib*`.
match build_sandbox::prepare_c(spec, &harness.workdir, opts.process_hardening) { match build_sandbox::prepare_c(spec, &harness.workdir, opts.process_hardening) {
Ok(build_result) => { Ok(build_result) => {
let binary = build_result.venv_path.join("nyx_harness"); let fallback = harness.workdir.join("nyx_harness");
if binary.exists() { stage_native_harness_command(&mut harness, &build_result.venv_path, fallback);
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()];
}
}
} }
Err(build_sandbox::BuildError::BuildFailed { stderr, attempts }) => { Err(build_sandbox::BuildError::BuildFailed { stderr, attempts }) => {
return Err(RunError::BuildFailed { stderr, attempts }); return Err(RunError::BuildFailed { stderr, attempts });
@ -423,15 +441,8 @@ pub fn run_spec(spec: &HarnessSpec, opts: &SandboxOptions) -> Result<RunOutcome,
// Compile the harness binary with `c++ -o nyx_harness main.cpp`. // Compile the harness binary with `c++ -o nyx_harness main.cpp`.
match build_sandbox::prepare_cpp(spec, &harness.workdir) { match build_sandbox::prepare_cpp(spec, &harness.workdir) {
Ok(build_result) => { Ok(build_result) => {
let binary = build_result.venv_path.join("nyx_harness"); let fallback = harness.workdir.join("nyx_harness");
if binary.exists() { stage_native_harness_command(&mut harness, &build_result.venv_path, fallback);
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()];
}
}
} }
Err(build_sandbox::BuildError::BuildFailed { stderr, attempts }) => { Err(build_sandbox::BuildError::BuildFailed { stderr, attempts }) => {
return Err(RunError::BuildFailed { stderr, attempts }); return Err(RunError::BuildFailed { stderr, attempts });

View file

@ -15,6 +15,7 @@ mod common;
mod go_fixture_tests { mod go_fixture_tests {
use crate::common::fixture_harness::FIXTURE_LOCK; use crate::common::fixture_harness::FIXTURE_LOCK;
use nyx_scanner::commands::scan::Diag; use nyx_scanner::commands::scan::Diag;
use nyx_scanner::dynamic::sandbox::SandboxBackend;
use nyx_scanner::dynamic::verify::{VerifyOptions, verify_finding}; use nyx_scanner::dynamic::verify::{VerifyOptions, verify_finding};
use nyx_scanner::evidence::{ use nyx_scanner::evidence::{
Confidence, Evidence, FlowStep, FlowStepKind, InconclusiveReason, UnsupportedReason, Confidence, Evidence, FlowStep, FlowStepKind, InconclusiveReason, UnsupportedReason,
@ -80,7 +81,8 @@ mod go_fixture_tests {
} }
let diag = make_diag(&path, func, cap, sink_line); 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); let result = verify_finding(&diag, &opts);
unsafe { unsafe {