From b41b24c416aea43f0d0456f615dba0a735f91050 Mon Sep 17 00:00:00 2001 From: pitboss Date: Sun, 17 May 2026 10:54:04 -0500 Subject: [PATCH] [pitboss/grind] deferred session-0030 (20260517T044708Z-e058) --- src/dynamic/repro.rs | 77 ++++++++++++++++++- src/dynamic/verify.rs | 47 ++++++++++- tests/repro_fixture_bundles.rs | 5 +- .../repro/harness/Dockerfile.harness | 2 +- .../python-3.11/repro/toolchain.lock | 2 +- 5 files changed, 124 insertions(+), 9 deletions(-) diff --git a/src/dynamic/repro.rs b/src/dynamic/repro.rs index 84a13d20..a9532580 100644 --- a/src/dynamic/repro.rs +++ b/src/dynamic/repro.rs @@ -307,14 +307,43 @@ fn source_ext_for_lang(lang: &crate::symbol::Lang) -> &'static str { } } -fn dockerfile_for_spec(spec: &HarnessSpec) -> String { +/// Resolve the `FROM` reference for `toolchain_id`. +/// +/// Prefers the pinned digest from +/// [`crate::dynamic::toolchain::pinned_image_ref`] so the emitted +/// Dockerfile is hermetic across hosts. Falls back to a tag-only +/// reference derived from `toolchain_id` when the catalogue has no +/// digest for the toolchain. +fn resolve_dockerfile_from(spec: &HarnessSpec) -> String { use crate::symbol::Lang; + + if let Some(pinned) = crate::dynamic::toolchain::pinned_image_ref(&spec.toolchain_id) { + return pinned.to_owned(); + } + match spec.lang { Lang::Rust => { let toolchain = spec.toolchain_id.strip_prefix("rust-").unwrap_or("stable"); + format!("rust:{toolchain}-slim") + } + Lang::Python => { + format!("python:{}", spec.toolchain_id.strip_prefix("python-").unwrap_or("3")) + } + _ => "ubuntu:latest".to_owned(), + } +} + +fn dockerfile_for_spec(spec: &HarnessSpec) -> String { + use crate::symbol::Lang; + let image = resolve_dockerfile_from(spec); + match spec.lang { + Lang::Rust => { // Multi-stage: build with Rust, run the binary directly. + // The builder stage uses the resolved (pinned-or-tag) image; + // the runtime stage stays on debian:bookworm-slim because the + // resulting nyx_harness binary is self-contained. format!( - "FROM rust:{toolchain}-slim AS builder\n\ + "FROM {image} AS builder\n\ WORKDIR /harness\n\ COPY Cargo.toml Cargo.lock* ./\n\ COPY src/ src/\n\ @@ -326,13 +355,12 @@ fn dockerfile_for_spec(spec: &HarnessSpec) -> String { ) } Lang::Python => { - let image = format!("python:{}", spec.toolchain_id.strip_prefix("python-").unwrap_or("3")); format!( "FROM {image}\nWORKDIR /harness\nCOPY harness.py .\nCMD [\"python3\", \"harness.py\"]\n" ) } _ => { - format!("# Unsupported language: {:?}\nFROM ubuntu:latest\n", spec.lang) + format!("# Unsupported language: {:?}\nFROM {image}\n", spec.lang) } } } @@ -759,6 +787,47 @@ mod tests { unsafe { std::env::remove_var("NYX_REPRO_BASE") }; } + #[test] + fn dockerfile_for_pinned_toolchain_uses_pinned_digest() { + // python-3.11 is in the image catalogue with a pinned digest, so the + // emitted Dockerfile must `FROM @sha256:…` for hermeticity. + let spec = make_spec(); + let pinned = crate::dynamic::toolchain::pinned_image_ref(&spec.toolchain_id) + .expect("python-3.11 should resolve to a pinned digest in images.toml"); + assert!( + pinned.contains("@sha256:"), + "pinned_image_ref returned a non-pinned value: {pinned}", + ); + let dockerfile = dockerfile_for_spec(&spec); + let expected_from = format!("FROM {pinned}"); + assert!( + dockerfile.contains(&expected_from), + "dockerfile did not embed pinned digest;\n expected substring: {expected_from}\n got:\n{dockerfile}", + ); + } + + #[test] + fn dockerfile_falls_back_to_tag_when_toolchain_absent_from_catalogue() { + // Unpinned toolchain id: no entry in IMAGE_DIGESTS, so the emitter + // must fall back to a tag-only `FROM` so an operator can still build + // the bundle (with a docker_pull.sh that is not emitted in this case). + let mut spec = make_spec(); + spec.toolchain_id = "python-2.7".into(); + assert!( + crate::dynamic::toolchain::pinned_image_ref(&spec.toolchain_id).is_none(), + "test precondition: python-2.7 must NOT be in the catalogue", + ); + let dockerfile = dockerfile_for_spec(&spec); + assert!( + dockerfile.contains("FROM python:2.7"), + "fallback dockerfile missing tag-only FROM line:\n{dockerfile}", + ); + assert!( + !dockerfile.contains("@sha256:"), + "fallback dockerfile must not invent a digest:\n{dockerfile}", + ); + } + #[test] fn reproduce_sh_contains_toolchain_check_and_exit_codes() { let dir = TempDir::new().unwrap(); diff --git a/src/dynamic/verify.rs b/src/dynamic/verify.rs index e8c5e874..a353bdf0 100644 --- a/src/dynamic/verify.rs +++ b/src/dynamic/verify.rs @@ -85,6 +85,19 @@ pub struct VerifyOptions { /// Default `false`. [`Self::from_config`] honours the /// `NYX_VERIFY_REPLAY_STABLE` environment variable (`1` / `true`). pub replay_stable_check: bool, + /// Phase 31 follow-up: when `true` and `replay_stable_check` is also + /// `true`, the verifier passes `--docker` to `reproduce.sh` instead of + /// running it through the host's process backend. Lets the eval-corpus + /// driver mark `replay_stable` based on the bare-image replay path so + /// the M7 ship-gate's Gate 5 reflects the docker bundle's green/red + /// signal — required when the corpus walks a host that has stripped + /// the language toolchains (the bare-image CI matrix at + /// `.github/workflows/repro-bare.yml`). + /// + /// Default `false`. [`Self::from_config`] honours the + /// `NYX_VERIFY_REPLAY_DOCKER` environment variable (`1` / `true`). + /// The flag is inert when `replay_stable_check == false`. + pub replay_use_docker: bool, } impl VerifyOptions { @@ -141,6 +154,9 @@ impl VerifyOptions { let replay_stable_check = std::env::var("NYX_VERIFY_REPLAY_STABLE") .map(|v| matches!(v.as_str(), "1" | "true" | "TRUE")) .unwrap_or(false); + let replay_use_docker = std::env::var("NYX_VERIFY_REPLAY_DOCKER") + .map(|v| matches!(v.as_str(), "1" | "true" | "TRUE")) + .unwrap_or(false); Self { sandbox: SandboxOptions { @@ -158,6 +174,7 @@ impl VerifyOptions { telemetry_policy: SamplingPolicy::from_config(&config.telemetry), trace_verbose: false, replay_stable_check, + replay_use_docker, } } } @@ -760,7 +777,8 @@ pub fn verify_finding(diag: &Diag, opts: &VerifyOptions) -> VerifyResult { && let Some(bundle) = crate::dynamic::repro::bundle_root_for(&spec.spec_hash) && bundle.join("reproduce.sh").exists() { - let replay = crate::dynamic::repro::replay_bundle(&bundle, &[]); + let replay_args: &[&str] = if opts.replay_use_docker { &["--docker"] } else { &[] }; + let replay = crate::dynamic::repro::replay_bundle(&bundle, replay_args); verdict.replay_stable = crate::dynamic::repro::replay_stability(&replay); } @@ -1273,6 +1291,33 @@ mod tests { unsafe { std::env::remove_var("NYX_VERIFY_REPLAY_STABLE") }; } + #[test] + fn from_config_defaults_replay_use_docker_off() { + // Same hermeticity concern as `replay_stable_check`: clear any + // stale process-wide setting so the default is observable. + unsafe { std::env::remove_var("NYX_VERIFY_REPLAY_DOCKER") }; + let opts = VerifyOptions::from_config(&Config::default()); + assert!( + !opts.replay_use_docker, + "NYX_VERIFY_REPLAY_DOCKER absent must leave the opt-in off so \ + interactive `nyx scan` does not require docker for the replay step" + ); + } + + #[test] + fn from_config_picks_up_replay_docker_env_flag() { + unsafe { std::env::set_var("NYX_VERIFY_REPLAY_DOCKER", "1") }; + let opts = VerifyOptions::from_config(&Config::default()); + assert!(opts.replay_use_docker); + unsafe { std::env::set_var("NYX_VERIFY_REPLAY_DOCKER", "true") }; + let opts = VerifyOptions::from_config(&Config::default()); + assert!(opts.replay_use_docker); + unsafe { std::env::set_var("NYX_VERIFY_REPLAY_DOCKER", "0") }; + let opts = VerifyOptions::from_config(&Config::default()); + assert!(!opts.replay_use_docker); + unsafe { std::env::remove_var("NYX_VERIFY_REPLAY_DOCKER") }; + } + #[test] fn from_config_defaults_process_hardening_to_standard() { use crate::dynamic::sandbox::ProcessHardeningProfile; diff --git a/tests/repro_fixture_bundles.rs b/tests/repro_fixture_bundles.rs index fca62f1a..e3707bed 100644 --- a/tests/repro_fixture_bundles.rs +++ b/tests/repro_fixture_bundles.rs @@ -258,8 +258,9 @@ fn python_3_11_flask_eval_bundle_structural_invariants() { let dockerfile = std::fs::read_to_string(root.join("harness/Dockerfile.harness")).unwrap(); assert!( - dockerfile.contains("FROM python:3.11"), - "dockerfile missing pinned FROM line", + dockerfile.contains("FROM python:3.11-slim@sha256:"), + "dockerfile missing pinned FROM line (expected `FROM python:3.11-slim@sha256:…` so the \ + bundle is hermetic across hosts); got:\n{dockerfile}", ); let payload = std::fs::read(root.join("payload/payload.bin")).unwrap(); diff --git a/tests/repro_fixtures/python-3.11/repro/harness/Dockerfile.harness b/tests/repro_fixtures/python-3.11/repro/harness/Dockerfile.harness index bf0804fb..70602cd3 100644 --- a/tests/repro_fixtures/python-3.11/repro/harness/Dockerfile.harness +++ b/tests/repro_fixtures/python-3.11/repro/harness/Dockerfile.harness @@ -1,4 +1,4 @@ -FROM python:3.11 +FROM python:3.11-slim@sha256:9a7765b36773a37061455b332f18e265e7f58f6fea9c419a550d2a8b0e9db834 WORKDIR /harness COPY harness.py . CMD ["python3", "harness.py"] diff --git a/tests/repro_fixtures/python-3.11/repro/toolchain.lock b/tests/repro_fixtures/python-3.11/repro/toolchain.lock index 99b1df1e..78d712b3 100644 --- a/tests/repro_fixtures/python-3.11/repro/toolchain.lock +++ b/tests/repro_fixtures/python-3.11/repro/toolchain.lock @@ -1,7 +1,7 @@ { "files": { "entry/extracted_source.py": "d18631435ec059c8cabafe7854f18d45e06a5c62da6274710712cf862cf9afa8", - "harness/Dockerfile.harness": "88bfe406a6305222207469e68777e09e68c558e66b4b15ca7f31670cb74f91b5", + "harness/Dockerfile.harness": "9ae78bdafc9cf11e9530f8c88deebc62b4c754c7ffa4759a40c80049c5a84586", "harness/harness.py": "15cc817251cf0c8915be782996b4af9b5b456f0b8fd75c360dcda153e071961c", "payload/payload.bin": "f3dc1d1a3d5a282cb6f171544ad5c8a5e78a6065a6decf6955c20763302bd574" },