[pitboss] phase 26: Track G.3 — End-to-end chain re-verification

This commit is contained in:
pitboss 2026-05-15 17:22:46 -05:00
parent 4228be2db6
commit 8a801953e2
21 changed files with 991 additions and 15 deletions

View file

@ -27,7 +27,7 @@
//! - `PayloadSlot::EnvVar(name)` — set env var before invoking entry.
//! - `PayloadSlot::Argv(n)` — `main(argc, argv)` shape: appended to argv.
use crate::dynamic::lang::{HarnessSource, LangEmitter};
use crate::dynamic::lang::{ChainStepHarness, HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::evidence::UnsupportedReason;
use std::path::PathBuf;
@ -307,6 +307,28 @@ impl LangEmitter for CEmitter {
"c emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — see Phase 16 shape dispatch (main / libFuzzer / free function)"
)
}
fn compose_chain_step(&self, prev_output: Option<&[u8]>) -> ChainStepHarness {
chain_step(prev_output)
}
}
/// Phase 26 — C chain-step harness.
fn chain_step(prev_output: Option<&[u8]>) -> ChainStepHarness {
let source = "#include <stdio.h>\n#include <stdlib.h>\n\nint main(void) {\n const char *prev = getenv(\"NYX_PREV_OUTPUT\");\n if (prev) fputs(prev, stdout);\n return 0;\n}\n".to_owned();
ChainStepHarness {
source,
filename: "step.c".to_owned(),
command: vec!["cc".to_owned(), "step.c".to_owned(), "-o".to_owned(), "step".to_owned()],
extra_env: prev_output
.map(|bytes| {
vec![(
ChainStepHarness::PREV_OUTPUT_ENV.to_owned(),
String::from_utf8_lossy(bytes).into_owned(),
)]
})
.unwrap_or_default(),
}
}
/// Emit a C harness for `spec`.

View file

@ -15,7 +15,7 @@
//! Build step: `prepare_cpp()` in `build_sandbox.rs` runs
//! `g++ -O0 -std=c++17 -o nyx_harness main.cpp` in the workdir.
use crate::dynamic::lang::{HarnessSource, LangEmitter};
use crate::dynamic::lang::{ChainStepHarness, HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::evidence::UnsupportedReason;
use std::path::PathBuf;
@ -280,6 +280,28 @@ impl LangEmitter for CppEmitter {
"cpp emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — see Phase 16 shape dispatch (main / libFuzzer / free function)"
)
}
fn compose_chain_step(&self, prev_output: Option<&[u8]>) -> ChainStepHarness {
chain_step(prev_output)
}
}
/// Phase 26 — C++ chain-step harness.
fn chain_step(prev_output: Option<&[u8]>) -> ChainStepHarness {
let source = "#include <cstdio>\n#include <cstdlib>\n\nint main() {\n const char *prev = std::getenv(\"NYX_PREV_OUTPUT\");\n if (prev) std::fputs(prev, stdout);\n return 0;\n}\n".to_owned();
ChainStepHarness {
source,
filename: "step.cpp".to_owned(),
command: vec!["c++".to_owned(), "step.cpp".to_owned(), "-o".to_owned(), "step".to_owned()],
extra_env: prev_output
.map(|bytes| {
vec![(
ChainStepHarness::PREV_OUTPUT_ENV.to_owned(),
String::from_utf8_lossy(bytes).into_owned(),
)]
})
.unwrap_or_default(),
}
}
/// Emit a C++ harness for `spec`.

View file

@ -37,7 +37,7 @@
//! Build container: `nyx-build-go:{toolchain_id}` (deferred; §19.1).
use crate::dynamic::environment::{Environment, RuntimeArtifacts};
use crate::dynamic::lang::{HarnessSource, LangEmitter};
use crate::dynamic::lang::{ChainStepHarness, HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::evidence::UnsupportedReason;
use std::path::PathBuf;
@ -75,6 +75,35 @@ impl LangEmitter for GoEmitter {
fn materialize_runtime(&self, env: &Environment) -> RuntimeArtifacts {
materialize_go(env)
}
fn compose_chain_step(&self, prev_output: Option<&[u8]>) -> ChainStepHarness {
chain_step(prev_output)
}
}
/// Phase 26 — Go chain-step harness.
///
/// Emits a `main.go` driver that reads `NYX_PREV_OUTPUT` and forwards it
/// on stdout. The Go probe shim (`__nyx_probe`) is top-level Go code
/// requiring extra stdlib imports; chain steps keep the harness minimal
/// and rely on the sandbox runner's outer probe channel to observe the
/// final sink fire. Wiring the probe shim into chain steps is tracked
/// alongside the Phase 15 emitter follow-up about probe shim splicing.
fn chain_step(prev_output: Option<&[u8]>) -> ChainStepHarness {
let source = "package main\n\nimport (\n \"fmt\"\n \"os\"\n)\n\nfunc main() {\n prev := os.Getenv(\"NYX_PREV_OUTPUT\")\n fmt.Print(prev)\n}\n".to_owned();
ChainStepHarness {
source,
filename: "step.go".to_owned(),
command: vec!["go".to_owned(), "run".to_owned(), "step.go".to_owned()],
extra_env: prev_output
.map(|bytes| {
vec![(
ChainStepHarness::PREV_OUTPUT_ENV.to_owned(),
String::from_utf8_lossy(bytes).into_owned(),
)]
})
.unwrap_or_default(),
}
}
// ── Phase 15: shape detector ─────────────────────────────────────────────────

View file

@ -36,7 +36,7 @@
//! Build container: `nyx-build-java:{toolchain_id}` (deferred; §19.1).
use crate::dynamic::environment::{Environment, RuntimeArtifacts};
use crate::dynamic::lang::{HarnessSource, LangEmitter};
use crate::dynamic::lang::{ChainStepHarness, HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::evidence::UnsupportedReason;
use std::path::PathBuf;
@ -74,6 +74,34 @@ impl LangEmitter for JavaEmitter {
fn materialize_runtime(&self, env: &Environment) -> RuntimeArtifacts {
materialize_java(env)
}
fn compose_chain_step(&self, prev_output: Option<&[u8]>) -> ChainStepHarness {
chain_step(prev_output)
}
}
/// Phase 26 — Java chain-step harness.
///
/// Emits a `Step.java` class whose `main` reads `NYX_PREV_OUTPUT` and
/// forwards it on stdout. The Java probe shim is class-level and
/// requires `System`/`java.io.*` imports the chain step already pulls in
/// implicitly; wiring the full shim is tracked alongside the Phase 14
/// emitter follow-up about probe shim splicing.
fn chain_step(prev_output: Option<&[u8]>) -> ChainStepHarness {
let source = "public class Step {\n public static void main(String[] args) {\n String prev = System.getenv(\"NYX_PREV_OUTPUT\");\n if (prev == null) prev = \"\";\n System.out.print(prev);\n }\n}\n".to_owned();
ChainStepHarness {
source,
filename: "Step.java".to_owned(),
command: vec!["java".to_owned(), "Step".to_owned()],
extra_env: prev_output
.map(|bytes| {
vec![(
ChainStepHarness::PREV_OUTPUT_ENV.to_owned(),
String::from_utf8_lossy(bytes).into_owned(),
)]
})
.unwrap_or_default(),
}
}
// ── Phase 14: shape detector ─────────────────────────────────────────────────

View file

@ -15,7 +15,7 @@
//! - [`PayloadSlot::Argv`] — coerced to positional `Param(0)` by build_call.
use crate::dynamic::environment::{Environment, RuntimeArtifacts};
use crate::dynamic::lang::{js_shared, HarnessSource, LangEmitter};
use crate::dynamic::lang::{js_shared, ChainStepHarness, HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec};
use crate::evidence::UnsupportedReason;
@ -43,6 +43,10 @@ impl LangEmitter for JavaScriptEmitter {
fn materialize_runtime(&self, env: &Environment) -> RuntimeArtifacts {
materialize_node(env)
}
fn compose_chain_step(&self, prev_output: Option<&[u8]>) -> ChainStepHarness {
js_shared::chain_step(prev_output, /* typescript = */ false)
}
}
/// Emit a JS harness for `spec`.

View file

@ -24,7 +24,7 @@
//! which preserves the pre-Phase-13 behaviour.
use crate::dynamic::environment::{Environment, RuntimeArtifacts};
use crate::dynamic::lang::HarnessSource;
use crate::dynamic::lang::{ChainStepHarness, HarnessSource};
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::evidence::UnsupportedReason;
use crate::utils::project::DetectedFramework;
@ -394,6 +394,41 @@ pub fn emit(spec: &HarnessSpec, is_typescript: bool) -> Result<HarnessSource, Un
})
}
/// Phase 26 — Node chain-step harness (shared between JS + TS emitters).
///
/// Splices the Node probe shim ([`probe_shim`]) in front of a minimal
/// driver that reads `NYX_PREV_OUTPUT` and forwards it on stdout. The
/// composite re-verifier swaps the trailing forward for the next member's
/// payload-injection prologue when running a multi-step chain.
pub fn chain_step(prev_output: Option<&[u8]>, is_typescript: bool) -> ChainStepHarness {
let probe = probe_shim();
let driver = "\nprocess.stdout.write(process.env.NYX_PREV_OUTPUT || '');\n";
let (filename, command) = if is_typescript {
(
"step.ts".to_owned(),
vec!["node".to_owned(), "step.ts".to_owned()],
)
} else {
(
"step.js".to_owned(),
vec!["node".to_owned(), "step.js".to_owned()],
)
};
ChainStepHarness {
source: format!("{probe}{driver}"),
filename,
command,
extra_env: prev_output
.map(|bytes| {
vec![(
ChainStepHarness::PREV_OUTPUT_ENV.to_owned(),
String::from_utf8_lossy(bytes).into_owned(),
)]
})
.unwrap_or_default(),
}
}
/// Public wrapper to detect the shape for a finalised [`HarnessSpec`].
pub fn detect_shape(spec: &HarnessSpec) -> JsShape {
let entry_source = read_entry_source(&spec.entry_file);

View file

@ -48,6 +48,33 @@ pub struct HarnessSource {
pub entry_subpath: Option<String>,
}
/// Phase 26 — one step in a chain-composite harness.
///
/// The composite re-verifier walks every member of a chain and assembles
/// a sequence of per-step harnesses. Each step is invoked with the
/// previous step's stdout threaded into the
/// [`ChainStepHarness::PREV_OUTPUT_ENV`] env var so the harness can fold
/// the chained input into its payload (e.g. browser-fetch → websocket
/// message → shell tool).
///
/// `extra_env` is additive on top of the sandbox's own
/// [`crate::dynamic::sandbox::SandboxOptions::extra_env`]; the runner is
/// responsible for splicing both in.
#[derive(Debug, Clone)]
pub struct ChainStepHarness {
pub source: String,
pub filename: String,
pub command: Vec<String>,
pub extra_env: Vec<(String, String)>,
}
impl ChainStepHarness {
/// Env-var name the previous step's stdout is bound to in the next
/// step's environment. Stable surface — kept distinct from
/// `NYX_PAYLOAD` so a chain step can read both at once.
pub const PREV_OUTPUT_ENV: &'static str = "NYX_PREV_OUTPUT";
}
/// Per-language harness emitter contract.
///
/// Implementations are zero-sized unit structs (one per `src/dynamic/lang/*.rs`
@ -96,6 +123,49 @@ pub trait LangEmitter {
fn materialize_runtime(&self, _env: &Environment) -> RuntimeArtifacts {
RuntimeArtifacts::default()
}
/// Phase 26 — Track G.3: build one step of a chain-composite harness.
///
/// `prev_output` carries the previous step's stdout (or `None` for
/// the chain's entry step). The returned [`ChainStepHarness`]
/// reads `NYX_PREV_OUTPUT` from its env to fold the chained input
/// into the step's behaviour and (when the step terminates at a
/// sink) invokes the Phase 06 `__nyx_probe` shim so the runner's
/// probe channel observes the sink fire.
///
/// Default impl produces a portable POSIX-shell stub that echoes
/// the previous step's output verbatim. Concrete emitters override
/// to splice in the language-native probe shim.
fn compose_chain_step(&self, prev_output: Option<&[u8]>) -> ChainStepHarness {
default_chain_step(prev_output)
}
}
/// Default chain-step harness. Emitted by [`LangEmitter::compose_chain_step`]
/// when an emitter does not override the trait method.
pub fn default_chain_step(prev_output: Option<&[u8]>) -> ChainStepHarness {
ChainStepHarness {
source: "#!/bin/sh\nprintf '%s' \"${NYX_PREV_OUTPUT:-}\"\n".to_owned(),
filename: "step.sh".to_owned(),
command: vec!["sh".to_owned(), "step.sh".to_owned()],
extra_env: prev_output
.map(|bytes| {
vec![(
ChainStepHarness::PREV_OUTPUT_ENV.to_owned(),
String::from_utf8_lossy(bytes).into_owned(),
)]
})
.unwrap_or_default(),
}
}
/// Public free-fn dispatcher for [`LangEmitter::compose_chain_step`].
///
/// Returns the lang-agnostic shell stub when `lang` has no registered
/// emitter so callers do not need to special-case that path.
pub fn compose_chain_step(lang: Lang, prev_output: Option<&[u8]>) -> ChainStepHarness {
dispatch(lang, |e| e.compose_chain_step(prev_output))
.unwrap_or_else(|| default_chain_step(prev_output))
}
/// Public free-fn dispatcher for [`LangEmitter::materialize_runtime`].

View file

@ -29,7 +29,7 @@
//! Build container: `nyx-build-php:{toolchain_id}` (deferred; §19.1).
use crate::dynamic::environment::{Environment, RuntimeArtifacts};
use crate::dynamic::lang::{HarnessSource, LangEmitter};
use crate::dynamic::lang::{ChainStepHarness, HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::evidence::UnsupportedReason;
use std::path::PathBuf;
@ -67,6 +67,33 @@ impl LangEmitter for PhpEmitter {
fn materialize_runtime(&self, env: &Environment) -> RuntimeArtifacts {
materialize_php(env)
}
fn compose_chain_step(&self, prev_output: Option<&[u8]>) -> ChainStepHarness {
chain_step(prev_output)
}
}
/// Phase 26 — PHP chain-step harness.
///
/// Emits a `step.php` script that reads `NYX_PREV_OUTPUT` via
/// `getenv()` and forwards it on stdout. The PHP probe shim is kept
/// outside the chain step for now and wired in alongside the Phase 15
/// emitter follow-up about probe shim splicing.
fn chain_step(prev_output: Option<&[u8]>) -> ChainStepHarness {
let source = "<?php\n$prev = getenv(\"NYX_PREV_OUTPUT\");\nif ($prev === false) { $prev = \"\"; }\necho $prev;\n".to_owned();
ChainStepHarness {
source,
filename: "step.php".to_owned(),
command: vec!["php".to_owned(), "step.php".to_owned()],
extra_env: prev_output
.map(|bytes| {
vec![(
ChainStepHarness::PREV_OUTPUT_ENV.to_owned(),
String::from_utf8_lossy(bytes).into_owned(),
)]
})
.unwrap_or_default(),
}
}
// ── Phase 15: shape detector ─────────────────────────────────────────────────

View file

@ -23,7 +23,7 @@
//! - Other slots produce [`UnsupportedReason::PayloadSlotUnsupported`].
use crate::dynamic::environment::{Environment, RuntimeArtifacts};
use crate::dynamic::lang::{HarnessSource, LangEmitter};
use crate::dynamic::lang::{ChainStepHarness, HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::evidence::UnsupportedReason;
use crate::utils::project::DetectedFramework;
@ -65,6 +65,34 @@ impl LangEmitter for PythonEmitter {
fn materialize_runtime(&self, env: &Environment) -> RuntimeArtifacts {
materialize_python(env)
}
fn compose_chain_step(&self, prev_output: Option<&[u8]>) -> ChainStepHarness {
chain_step(prev_output)
}
}
/// Phase 26 — Python chain-step harness.
///
/// Splices the Python probe shim ([`probe_shim`]) in front of a minimal
/// driver that reads `NYX_PREV_OUTPUT` and forwards it on stdout. The
/// composite re-verifier swaps the trailing forward for the next member's
/// payload-injection prologue when running a multi-step chain.
fn chain_step(prev_output: Option<&[u8]>) -> ChainStepHarness {
let probe = probe_shim();
let driver = "\nimport os, sys\nprev = os.environ.get('NYX_PREV_OUTPUT', '')\nsys.stdout.write(prev)\nsys.stdout.flush()\n";
ChainStepHarness {
source: format!("{probe}{driver}"),
filename: "step.py".to_owned(),
command: vec!["python3".to_owned(), "step.py".to_owned()],
extra_env: prev_output
.map(|bytes| {
vec![(
ChainStepHarness::PREV_OUTPUT_ENV.to_owned(),
String::from_utf8_lossy(bytes).into_owned(),
)]
})
.unwrap_or_default(),
}
}
// ── Phase 12: shape detector ─────────────────────────────────────────────────

View file

@ -27,7 +27,7 @@
//! Build: no compilation step. Command is `ruby harness.rb`.
use crate::dynamic::environment::{Environment, RuntimeArtifacts};
use crate::dynamic::lang::{HarnessSource, LangEmitter};
use crate::dynamic::lang::{ChainStepHarness, HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::evidence::UnsupportedReason;
use std::path::PathBuf;
@ -64,6 +64,28 @@ impl LangEmitter for RubyEmitter {
fn materialize_runtime(&self, env: &Environment) -> RuntimeArtifacts {
materialize_ruby(env)
}
fn compose_chain_step(&self, prev_output: Option<&[u8]>) -> ChainStepHarness {
chain_step(prev_output)
}
}
/// Phase 26 — Ruby chain-step harness.
fn chain_step(prev_output: Option<&[u8]>) -> ChainStepHarness {
let source = "prev = ENV[\"NYX_PREV_OUTPUT\"] || \"\"\n$stdout.write(prev)\n".to_owned();
ChainStepHarness {
source,
filename: "step.rb".to_owned(),
command: vec!["ruby".to_owned(), "step.rb".to_owned()],
extra_env: prev_output
.map(|bytes| {
vec![(
ChainStepHarness::PREV_OUTPUT_ENV.to_owned(),
String::from_utf8_lossy(bytes).into_owned(),
)]
})
.unwrap_or_default(),
}
}
// ── Phase 15: shape detector ─────────────────────────────────────────────────

View file

@ -22,7 +22,7 @@
//! HTML_ESCAPE is n/a for Rust (§15.4).
use crate::dynamic::environment::{Environment, RuntimeArtifacts};
use crate::dynamic::lang::{HarnessSource, LangEmitter};
use crate::dynamic::lang::{ChainStepHarness, HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::evidence::UnsupportedReason;
use crate::labels::Cap;
@ -63,6 +63,34 @@ impl LangEmitter for RustEmitter {
fn materialize_runtime(&self, env: &Environment) -> RuntimeArtifacts {
materialize_rust(env)
}
fn compose_chain_step(&self, prev_output: Option<&[u8]>) -> ChainStepHarness {
chain_step(prev_output)
}
}
/// Phase 26 — Rust chain-step harness.
///
/// Emits a minimal `step.rs` file that reads `NYX_PREV_OUTPUT` and writes
/// it on stdout. The chain composer drives the step with `rustc step.rs`
/// (single-file build) — full Cargo crate scaffolding is reserved for
/// chain members whose underlying finding already produced a HarnessSpec
/// via the standard emit path.
fn chain_step(prev_output: Option<&[u8]>) -> ChainStepHarness {
let source = "use std::env;\nuse std::io::{self, Write};\n\nfn main() {\n let prev = env::var(\"NYX_PREV_OUTPUT\").unwrap_or_default();\n let _ = io::stdout().write_all(prev.as_bytes());\n}\n".to_owned();
ChainStepHarness {
source,
filename: "step.rs".to_owned(),
command: vec!["rustc".to_owned(), "step.rs".to_owned(), "-o".to_owned(), "step".to_owned()],
extra_env: prev_output
.map(|bytes| {
vec![(
ChainStepHarness::PREV_OUTPUT_ENV.to_owned(),
String::from_utf8_lossy(bytes).into_owned(),
)]
})
.unwrap_or_default(),
}
}
/// Phase 09 — Track D.2: synthesise a `Cargo.toml` that pins every

View file

@ -15,7 +15,7 @@
//! runtime ignores.
use crate::dynamic::environment::{Environment, RuntimeArtifacts};
use crate::dynamic::lang::{js_shared, HarnessSource, LangEmitter};
use crate::dynamic::lang::{js_shared, ChainStepHarness, HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec};
use crate::evidence::UnsupportedReason;
@ -46,6 +46,10 @@ impl LangEmitter for TypeScriptEmitter {
fn materialize_runtime(&self, env: &Environment) -> RuntimeArtifacts {
js_shared::materialize_node(env)
}
fn compose_chain_step(&self, prev_output: Option<&[u8]>) -> ChainStepHarness {
js_shared::chain_step(prev_output, /* typescript = */ true)
}
}
#[cfg(test)]