From 76de47fb6b5a97fa564b354f99ddac7d090c2951 Mon Sep 17 00:00:00 2001 From: pitboss Date: Sat, 16 May 2026 05:42:07 -0500 Subject: [PATCH] [pitboss/grind] deferred session-0009 (20260516T052512Z-20f8) --- src/dynamic/sandbox/mod.rs | 183 +++++++++++++++++++++++++++++++- tests/common/fixture_harness.rs | 24 ++++- 2 files changed, 198 insertions(+), 9 deletions(-) diff --git a/src/dynamic/sandbox/mod.rs b/src/dynamic/sandbox/mod.rs index adf3ddec..0af58e90 100644 --- a/src/dynamic/sandbox/mod.rs +++ b/src/dynamic/sandbox/mod.rs @@ -25,7 +25,7 @@ use crate::dynamic::harness::BuiltHarness; use crate::dynamic::oob::OobListener; use crate::dynamic::probe::{ProbeChannel, PROBE_PATH_ENV}; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::sync::{Arc, OnceLock}; use std::time::{Duration, Instant}; @@ -735,6 +735,51 @@ fn run_firecracker( // ── Docker backend ──────────────────────────────────────────────────────────── +/// Host paths of every `StubKind::Filesystem` stub in `opts.stub_harness`. +/// +/// Ordered by spawn position in the harness so `Vec::iter().enumerate()` +/// indexes match the container-side mount layout produced by +/// [`docker::stub_mount_args`] (`/nyx/stubs/`). +fn collect_fs_stub_roots(opts: &SandboxOptions) -> Vec { + let Some(h) = opts.stub_harness.as_ref() else { + return Vec::new(); + }; + h.stubs() + .iter() + .filter(|s| s.kind() == crate::dynamic::stubs::StubKind::Filesystem) + .map(|s| PathBuf::from(s.endpoint())) + .collect() +} + +/// Rewrite `(key, value)` env pairs for delivery into a container. +/// +/// `NYX_FS_ROOT` values whose host path matches an entry in `fs_stub_roots` +/// are rewritten to `/` so the harness sees the +/// in-container mount path the docker run line set up via +/// [`docker::stub_mount_args`]. All other pairs are passed through verbatim. +fn rewrite_extra_env_for_container( + extra_env: &[(String, String)], + fs_stub_roots: &[PathBuf], +) -> Vec<(String, String)> { + extra_env + .iter() + .map(|(k, v)| { + if k == "NYX_FS_ROOT" { + if let Some(idx) = fs_stub_roots + .iter() + .position(|p| p.as_os_str() == std::ffi::OsStr::new(v)) + { + return ( + k.clone(), + format!("{}/{idx}", docker::STUB_MOUNT_ROOT), + ); + } + } + (k.clone(), v.clone()) + }) + .collect() +} + /// Docker backend: image per toolchain_id, container reuse via `docker exec`. fn run_docker( harness: &BuiltHarness, @@ -758,15 +803,23 @@ fn run_docker( false }; + let fs_stub_roots = collect_fs_stub_roots(opts); + if !reused { // Determine the Python image from the harness command (first element). // Fall back to python:3-slim when the command is not recognised. let image = detect_image_for_harness(harness); - start_container(&container_name, &harness.workdir, &image, &opts.network_policy)?; + start_container( + &container_name, + &harness.workdir, + &image, + &opts.network_policy, + &fs_stub_roots, + )?; registry.insert(container_name.clone(), container_name.clone()); } - exec_in_container(&container_name, harness, payload_bytes, opts) + exec_in_container(&container_name, harness, payload_bytes, opts, &fs_stub_roots) } /// Returns true when `docker info` succeeds using the current `NYX_DOCKER_BIN`. @@ -815,6 +868,7 @@ fn start_container( workdir: &Path, image: &str, policy: &NetworkPolicy, + fs_stub_roots: &[PathBuf], ) -> Result<(), SandboxError> { // Phase 19 (Track E.3): when `image` is a pinned reference produced by // `docker::image_reference_for_toolchain`, make sure it is present on @@ -844,6 +898,11 @@ fn start_container( // container — no follow-up `docker cp` is needed. "-v".into(), workdir_mount, ]; + // Phase 10 / Phase 19 (Track D.3 + E.3): bind-mount each + // filesystem-stub root at `STUB_MOUNT_ROOT/:rw` so the + // harness can resolve `NYX_FS_ROOT` to a container-side path the + // sandbox can reach. Empty when no `FilesystemStub` is active. + run_args.extend(docker::stub_mount_args(fs_stub_roots)); match policy { NetworkPolicy::None => { run_args.extend(["--network".into(), "none".into()]); @@ -941,6 +1000,7 @@ fn exec_in_container( harness: &BuiltHarness, payload_bytes: &[u8], opts: &SandboxOptions, + fs_stub_roots: &[PathBuf], ) -> Result { use std::io::Read; use std::process::{Command, Stdio}; @@ -964,6 +1024,16 @@ fn exec_in_container( cmd_args.push("-e".into()); cmd_args.push(format!("{k}={v}")); } + // Phase 10 (Track D.3): boundary-stub endpoints from + // `opts.extra_env` overlay AFTER `harness.env` so an emitter-supplied + // placeholder cannot accidentally shadow a verifier-set endpoint. + // `NYX_FS_ROOT` is rewritten from its host path to the + // container-side mount path produced by `start_container`'s + // `docker::stub_mount_args` extension. + for (k, v) in rewrite_extra_env_for_container(&opts.extra_env, fs_stub_roots) { + cmd_args.push("-e".into()); + cmd_args.push(format!("{k}={v}")); + } cmd_args.push(container_name.into()); // Build the exec command inside the container. @@ -1132,12 +1202,15 @@ fn run_native_binary_docker( false }; + let fs_stub_roots = collect_fs_stub_roots(opts); + if !reused { start_container( &container_name, &harness.workdir, NATIVE_BINARY_IMAGE, &opts.network_policy, + &fs_stub_roots, )?; // Copy the compiled binary into the container as @@ -1170,7 +1243,7 @@ fn run_native_binary_docker( registry.insert(container_name.clone(), container_name.clone()); } - exec_native_binary_in_container(&container_name, harness, payload_bytes, opts) + exec_native_binary_in_container(&container_name, harness, payload_bytes, opts, &fs_stub_roots) } /// Execute a native binary already in the container at `/work/nyx_harness`. @@ -1179,6 +1252,7 @@ fn exec_native_binary_in_container( harness: &BuiltHarness, payload_bytes: &[u8], opts: &SandboxOptions, + fs_stub_roots: &[PathBuf], ) -> Result { use std::io::Read; use std::process::{Command, Stdio}; @@ -1194,6 +1268,15 @@ fn exec_native_binary_in_container( cmd_args.push("-e".into()); cmd_args.push(format!("{k}={v}")); } + // Phase 10 (Track D.3): mirror the boundary-stub env overlay from + // `exec_in_container` so the native-binary docker path delivers + // `NYX_SQL_ENDPOINT` / `NYX_HTTP_ENDPOINT` / `NYX_FS_ROOT` to the + // harness. Stub endpoints from `opts.extra_env` follow `harness.env` + // so emitter-supplied placeholders cannot shadow them. + for (k, v) in rewrite_extra_env_for_container(&opts.extra_env, fs_stub_roots) { + cmd_args.push("-e".into()); + cmd_args.push(format!("{k}={v}")); + } cmd_args.push(container_name.into()); cmd_args.push(format!("{}/nyx_harness", docker::WORK_MOUNT_PATH)); @@ -1918,4 +2001,96 @@ mod tests { assert_eq!(docker_image_for_toolchain_id("rust-stable"), None); assert_eq!(docker_image_for_toolchain_id("go-1.22"), None); } + + #[test] + fn rewrite_extra_env_passes_unrelated_pairs_through() { + let extra = vec![ + ("NYX_SQL_ENDPOINT".to_owned(), "/tmp/abc.db".to_owned()), + ("NYX_HTTP_ENDPOINT".to_owned(), "http://127.0.0.1:12345".to_owned()), + ]; + let out = rewrite_extra_env_for_container(&extra, &[]); + assert_eq!(out, extra); + } + + #[test] + fn rewrite_extra_env_maps_fs_root_to_container_mount() { + let host_root = PathBuf::from("/tmp/host-fs-root-abc"); + let extra = vec![ + ("NYX_FS_ROOT".to_owned(), host_root.to_string_lossy().into_owned()), + ]; + let out = rewrite_extra_env_for_container(&extra, &[host_root]); + assert_eq!(out.len(), 1); + assert_eq!(out[0].0, "NYX_FS_ROOT"); + assert_eq!(out[0].1, format!("{}/0", docker::STUB_MOUNT_ROOT)); + } + + #[test] + fn rewrite_extra_env_leaves_fs_root_alone_when_no_root_matches() { + // Defensive: an NYX_FS_ROOT value that does not appear in the + // active fs_stub_roots list is passed through unchanged. This + // keeps the rewrite from accidentally clobbering an emitter- + // supplied placeholder. + let extra = vec![ + ("NYX_FS_ROOT".to_owned(), "/some/host/path".to_owned()), + ]; + let out = rewrite_extra_env_for_container( + &extra, + &[PathBuf::from("/different/host/path")], + ); + assert_eq!(out, extra); + } + + #[test] + fn rewrite_extra_env_indexes_multiple_fs_roots() { + let root_a = PathBuf::from("/tmp/fs-a"); + let root_b = PathBuf::from("/tmp/fs-b"); + let extra = vec![ + ("NYX_FS_ROOT".to_owned(), root_b.to_string_lossy().into_owned()), + ]; + let out = rewrite_extra_env_for_container(&extra, &[root_a, root_b]); + assert_eq!(out[0].1, format!("{}/1", docker::STUB_MOUNT_ROOT)); + } + + #[test] + fn collect_fs_stub_roots_returns_empty_without_harness() { + let opts = SandboxOptions::default(); + assert!(collect_fs_stub_roots(&opts).is_empty()); + } + + #[test] + fn collect_fs_stub_roots_returns_paths_for_filesystem_stubs() { + use crate::dynamic::stubs::StubKind; + let dir = tempfile::TempDir::new().expect("tempdir"); + let harness = crate::dynamic::stubs::StubHarness::start( + &[StubKind::Filesystem], + dir.path(), + ) + .expect("start stub harness"); + let endpoint = harness.stubs()[0].endpoint(); + let opts = SandboxOptions { + stub_harness: Some(Arc::new(harness)), + ..SandboxOptions::default() + }; + let roots = collect_fs_stub_roots(&opts); + assert_eq!(roots.len(), 1); + assert_eq!(roots[0], PathBuf::from(endpoint)); + } + + #[test] + fn collect_fs_stub_roots_skips_network_stubs() { + use crate::dynamic::stubs::StubKind; + let dir = tempfile::TempDir::new().expect("tempdir"); + let harness = crate::dynamic::stubs::StubHarness::start( + &[StubKind::Http, StubKind::Sql], + dir.path(), + ) + .expect("start stub harness"); + let opts = SandboxOptions { + stub_harness: Some(Arc::new(harness)), + ..SandboxOptions::default() + }; + // Sql endpoint is a host path but its kind is not Filesystem, + // so it must not appear in fs_stub_roots. + assert!(collect_fs_stub_roots(&opts).is_empty()); + } } diff --git a/tests/common/fixture_harness.rs b/tests/common/fixture_harness.rs index a8e48e29..a24d3198 100644 --- a/tests/common/fixture_harness.rs +++ b/tests/common/fixture_harness.rs @@ -495,12 +495,26 @@ pub fn run_shape_fixture_lang( // [`VerifyStatus`] directly without learning the runner's API. match outcome { Ok(run) => { - let status = if run.triggered_by.is_some() { - VerifyStatus::Confirmed + let (status, inconclusive_reason) = if run.triggered_by.is_some() { + (VerifyStatus::Confirmed, None) } else if run.oracle_collision { - VerifyStatus::Inconclusive + ( + VerifyStatus::Inconclusive, + Some(nyx_scanner::evidence::InconclusiveReason::OracleCollisionSuspected), + ) + } else if run.unrelated_crash { + // Mirror the runner's downgrade in + // `src/dynamic/runner.rs:425-432`: a process-level crash + // outside the sink probe routes to + // `Inconclusive(UnrelatedCrash)`. Shape suites that + // exercise SinkCrash oracles pin this branch instead of + // recreating `run_spec` plumbing inline. + ( + VerifyStatus::Inconclusive, + Some(nyx_scanner::evidence::InconclusiveReason::UnrelatedCrash), + ) } else { - VerifyStatus::NotConfirmed + (VerifyStatus::NotConfirmed, None) }; VerifyResult { finding_id: spec.finding_id.clone(), @@ -510,7 +524,7 @@ pub fn run_shape_fixture_lang( .and_then(|i| run.attempts.get(i)) .map(|a| a.payload_label.to_owned()), reason: None, - inconclusive_reason: None, + inconclusive_reason, detail: None, attempts: vec![], toolchain_match: None,