[pitboss/grind] deferred session-0017 (20260517T044708Z-e058)

This commit is contained in:
pitboss 2026-05-17 06:20:10 -05:00
parent 5b90a67f5c
commit 5b4181e4dd
14 changed files with 489 additions and 131 deletions

View file

@ -53,7 +53,7 @@ use crate::chain::finding::{ChainFinding, ChainSeverity};
use crate::commands::scan::Diag;
use crate::dynamic::build_sandbox::dispatch_prepare;
use crate::dynamic::harness::{self, BuiltHarness};
use crate::dynamic::lang;
use crate::dynamic::lang::{self, ChainStepTerminal};
use crate::dynamic::sandbox;
use crate::dynamic::spec::HarnessSpec;
use crate::dynamic::verify::VerifyOptions;
@ -278,12 +278,18 @@ impl CompositeReverifier for DefaultCompositeReverifier {
// Sub-task (c) of the Phase 26 live-execution split:
// sequentially run each built chain-step harness through
// `sandbox::run`, threading the previous step's stdout into
// the next step via `NYX_PREV_OUTPUT`. The final step's
// `sink_hit` is captured for the detail field; today it stays
// false because `compose_chain_step` does not yet rewrite the
// chain's terminal sink.
// the next step via `NYX_PREV_OUTPUT`. The final step is
// composed with a `ChainStepTerminal` carrying the chain's
// sink callee, so the per-language emitter splices in a
// `__nyx_probe(callee, prev)` call plus the
// `SINK_HIT_SENTINEL` banner that `sandbox::run` detects via
// `SandboxOutcome::sink_hit`.
let terminal = ChainStepTerminal {
sink_callee: chain.sink.function_name.clone(),
sink_cap_bits: chain.sink.cap_bits,
};
let (steps_run, sandbox_errors, steps_timeout, nonzero_exits, final_sink_hit) =
run_chain_steps(&built_steps, &opts.sandbox);
run_chain_steps(&built_steps, &opts.sandbox, &terminal);
let detail = format!(
"composite chain re-verification: live runs collect step coverage; \
@ -291,22 +297,49 @@ impl CompositeReverifier for DefaultCompositeReverifier {
built {built}/{derived} (cache_hit={cache_hits}, build_ms={total_build_ms}, build_errors={build_errors}); \
ran {steps_run}/{built} (sandbox_errors={sandbox_errors}, timeouts={steps_timeout}, nonzero_exits={nonzero_exits}, final_sink_hit={final_sink_hit})"
);
VerifyResult {
finding_id,
status: VerifyStatus::Inconclusive,
triggered_payload: None,
reason: None,
inconclusive_reason: Some(InconclusiveReason::BackendInsufficient {
backend: "composite-chain".to_owned(),
oracle_kind: "chain-step-harness".to_owned(),
}),
detail: Some(detail),
attempts: vec![],
toolchain_match: None,
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
// Verdict resolution: a composite chain is `Confirmed` when
// (a) every derived step built, (b) every built step ran
// without a sandbox error, (c) the final step's terminal
// compose fired the sink sentinel (`final_sink_hit=true`).
// Anything short of all three keeps the verdict
// `Inconclusive(BackendInsufficient)` so the chain's severity
// takes the existing downgrade rule.
let all_built = derived > 0 && built == derived;
let all_ran = built > 0 && steps_run == built && sandbox_errors == 0;
if all_built && all_ran && final_sink_hit {
VerifyResult {
finding_id,
status: VerifyStatus::Confirmed,
triggered_payload: None,
reason: None,
inconclusive_reason: None,
detail: Some(detail),
attempts: vec![],
toolchain_match: None,
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
}
} else {
VerifyResult {
finding_id,
status: VerifyStatus::Inconclusive,
triggered_payload: None,
reason: None,
inconclusive_reason: Some(InconclusiveReason::BackendInsufficient {
backend: "composite-chain".to_owned(),
oracle_kind: "chain-step-harness".to_owned(),
}),
detail: Some(detail),
attempts: vec![],
toolchain_match: None,
differential: None,
replay_stable: None,
wrong: None,
hardening_outcome: None,
}
}
}
}
@ -337,6 +370,7 @@ impl CompositeReverifier for DefaultCompositeReverifier {
fn run_chain_steps(
built_steps: &[(PathBuf, &HarnessSpec)],
base_opts: &sandbox::SandboxOptions,
terminal: &ChainStepTerminal,
) -> (usize, usize, usize, usize, bool) {
let mut steps_run = 0usize;
let mut sandbox_errors = 0usize;
@ -346,7 +380,8 @@ fn run_chain_steps(
let mut prev_output: Option<Vec<u8>> = None;
let last_idx = built_steps.len().saturating_sub(1);
for (idx, (workdir, spec)) in built_steps.iter().enumerate() {
let step = lang::compose_chain_step(spec.lang, prev_output.as_deref());
let step_terminal = if idx == last_idx { Some(terminal) } else { None };
let step = lang::compose_chain_step(spec.lang, prev_output.as_deref(), step_terminal);
let step_path = workdir.join(&step.filename);
if let Some(parent) = step_path.parent() {
@ -762,7 +797,11 @@ mod tests {
// function of the (steps_run, sandbox_errors, timeouts,
// nonzero_exits, final_sink_hit) tuple this helper returns.
let opts = sandbox::SandboxOptions::default();
let result = run_chain_steps(&[], &opts);
let terminal = ChainStepTerminal {
sink_callee: "noop".into(),
sink_cap_bits: 0,
};
let result = run_chain_steps(&[], &opts, &terminal);
assert_eq!(result, (0, 0, 0, 0, false));
}

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::{ChainStepHarness, HarnessSource, LangEmitter};
use crate::dynamic::lang::{ChainStepHarness, ChainStepTerminal, HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::evidence::UnsupportedReason;
use std::path::PathBuf;
@ -372,28 +372,43 @@ impl LangEmitter for CEmitter {
)
}
fn compose_chain_step(&self, prev_output: Option<&[u8]>) -> ChainStepHarness {
chain_step(prev_output)
fn compose_chain_step(
&self,
prev_output: Option<&[u8]>,
terminal: Option<&ChainStepTerminal>,
) -> ChainStepHarness {
chain_step(prev_output, terminal)
}
}
/// Phase 26 — C chain-step harness.
///
/// Splices the C probe shim ([`probe_shim`]) ahead of a minimal driver
/// that reads `NYX_PREV_OUTPUT` and forwards it on stdout. The shim's
/// static functions (`__nyx_probe`, `__nyx_install_crash_guard`,
/// `__nyx_stub_sql_record`, `__nyx_stub_http_record`) become callable
/// from a future sink-rewrite pass without bringing in another
/// translation unit. Unreferenced shim helpers stay quiet under
/// default `cc` flags — `-Wunused-function` is not on the warning
/// baseline so dead helpers do not fail the build.
/// that reads `NYX_PREV_OUTPUT` and forwards it on stdout. When the
/// step is the chain's terminal step (`terminal == Some(_)`) the driver
/// also calls `__nyx_probe(callee, 1, prev)` and emits the
/// [`ChainStepHarness::SINK_HIT_SENTINEL`] on stdout so the runner
/// flips `sink_hit` for the chain.
///
/// Shell-wraps `cc` + run so the compiled binary actually executes after
/// the build completes — `ChainStepHarness.command` models a single
/// process, so the build-then-run sequence must collapse to one `sh -c`.
fn chain_step(prev_output: Option<&[u8]>) -> ChainStepHarness {
fn chain_step(
prev_output: Option<&[u8]>,
terminal: Option<&ChainStepTerminal>,
) -> ChainStepHarness {
let shim = probe_shim();
let driver = "\nint main(void) {\n const char *prev = getenv(\"NYX_PREV_OUTPUT\");\n if (prev) fputs(prev, stdout);\n return 0;\n}\n";
let mut driver = String::from(
"\nint main(void) {\n const char *prev = getenv(\"NYX_PREV_OUTPUT\");\n if (prev) fputs(prev, stdout);\n",
);
if let Some(t) = terminal {
let callee = c_string_literal(&t.sink_callee);
let sentinel = c_string_literal(ChainStepHarness::SINK_HIT_SENTINEL);
driver.push_str(&format!(
" __nyx_probe({callee}, 1, prev ? prev : \"\");\n puts({sentinel});\n fflush(stdout);\n",
));
}
driver.push_str(" return 0;\n}\n");
let source = format!("{shim}{driver}");
ChainStepHarness {
source,
@ -415,6 +430,12 @@ fn chain_step(prev_output: Option<&[u8]>) -> ChainStepHarness {
}
}
/// Escape a string for safe C double-quoted literal embedding.
fn c_string_literal(s: &str) -> String {
let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
format!("\"{escaped}\"")
}
/// Emit a C harness for `spec`.
pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
let shape = detect_shape(spec);
@ -875,7 +896,7 @@ mod tests {
// source, that `prev_output` rides through `extra_env`, and
// that the build-then-run command stays in one `sh -c` so the
// sandbox sees a single process.
let step = chain_step(Some(b"prev-output"));
let step = chain_step(Some(b"prev-output"), None);
assert!(
step.source.contains("__nyx_probe shim (Phase 06"),
"probe_shim banner missing from chain step source",

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::{ChainStepHarness, HarnessSource, LangEmitter};
use crate::dynamic::lang::{ChainStepHarness, ChainStepTerminal, HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::evidence::UnsupportedReason;
use std::path::PathBuf;
@ -325,24 +325,42 @@ impl LangEmitter for CppEmitter {
)
}
fn compose_chain_step(&self, prev_output: Option<&[u8]>) -> ChainStepHarness {
chain_step(prev_output)
fn compose_chain_step(
&self,
prev_output: Option<&[u8]>,
terminal: Option<&ChainStepTerminal>,
) -> ChainStepHarness {
chain_step(prev_output, terminal)
}
}
/// Phase 26 — C++ chain-step harness.
///
/// Splices the C++ probe shim ([`probe_shim`]) ahead of a minimal driver
/// that reads `NYX_PREV_OUTPUT` and forwards it on stdout. Same
/// rationale as the C sibling: the inline shim helpers become callable
/// from a future sink-rewrite pass without a separate translation unit;
/// unreferenced inline functions stay quiet under default `c++` flags.
/// that reads `NYX_PREV_OUTPUT` and forwards it on stdout. When the
/// step is the chain's terminal step (`terminal == Some(_)`) the driver
/// also calls `__nyx_probe(callee, std::string(prev))` and emits the
/// [`ChainStepHarness::SINK_HIT_SENTINEL`] so the runner flips
/// `sink_hit` for the chain.
///
/// Shell-wraps `c++` + run so the compiled binary actually executes
/// after the build completes (see C-side commentary for the rationale).
fn chain_step(prev_output: Option<&[u8]>) -> ChainStepHarness {
fn chain_step(
prev_output: Option<&[u8]>,
terminal: Option<&ChainStepTerminal>,
) -> ChainStepHarness {
let shim = probe_shim();
let driver = "\nint main() {\n const char *prev = std::getenv(\"NYX_PREV_OUTPUT\");\n if (prev) std::fputs(prev, stdout);\n return 0;\n}\n";
let mut driver = String::from(
"\nint main() {\n const char *prev = std::getenv(\"NYX_PREV_OUTPUT\");\n if (prev) std::fputs(prev, stdout);\n",
);
if let Some(t) = terminal {
let callee = cpp_string_literal(&t.sink_callee);
let sentinel = cpp_string_literal(ChainStepHarness::SINK_HIT_SENTINEL);
driver.push_str(&format!(
" __nyx_probe({callee}, std::string(prev ? prev : \"\"));\n std::puts({sentinel});\n std::fflush(stdout);\n",
));
}
driver.push_str(" return 0;\n}\n");
let source = format!("{shim}{driver}");
ChainStepHarness {
source,
@ -364,6 +382,12 @@ fn chain_step(prev_output: Option<&[u8]>) -> ChainStepHarness {
}
}
/// Escape a string for safe C++ double-quoted literal embedding.
fn cpp_string_literal(s: &str) -> String {
let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
format!("\"{escaped}\"")
}
/// Emit a C++ harness for `spec`.
pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
let shape = detect_shape(spec);
@ -742,7 +766,7 @@ mod tests {
// shim banner is present and lands before `int main`, that
// `__nyx_install_crash_guard` is reachable, prev_output rides
// through `extra_env`, and build-then-run stays one `sh -c`.
let step = chain_step(Some(b"prev-output"));
let step = chain_step(Some(b"prev-output"), None);
assert!(
step.source.contains("__nyx_probe shim (Phase 06"),
"probe_shim banner missing from chain step source",

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::{ChainStepHarness, HarnessSource, LangEmitter};
use crate::dynamic::lang::{ChainStepHarness, ChainStepTerminal, HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::evidence::UnsupportedReason;
use std::path::PathBuf;
@ -76,28 +76,44 @@ impl LangEmitter for GoEmitter {
materialize_go(env)
}
fn compose_chain_step(&self, prev_output: Option<&[u8]>) -> ChainStepHarness {
chain_step(prev_output)
fn compose_chain_step(
&self,
prev_output: Option<&[u8]>,
terminal: Option<&ChainStepTerminal>,
) -> ChainStepHarness {
chain_step(prev_output, terminal)
}
}
/// Phase 26 — Go chain-step harness.
///
/// Splices the Go probe shim ([`probe_shim`]) ahead 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; the shim
/// has to be in the same compilation unit so a chain step that terminates
/// at a sink can drive the `__nyx_probe` channel directly.
/// that reads `NYX_PREV_OUTPUT` and forwards it on stdout. When the
/// step is the chain's terminal step the driver also calls
/// `__nyx_probe(callee, prev)` and prints the
/// [`ChainStepHarness::SINK_HIT_SENTINEL`] so the runner flips
/// `sink_hit` for the chain.
///
/// Imports are the union of the driver imports (`fmt`, `os`) and the
/// shim's [`SHIM_IMPORTS`], deduped + sorted so `go run step.go`
/// compiles in a single command.
fn chain_step(prev_output: Option<&[u8]>) -> ChainStepHarness {
fn chain_step(
prev_output: Option<&[u8]>,
terminal: Option<&ChainStepTerminal>,
) -> ChainStepHarness {
let imports = chain_step_imports();
let shim = probe_shim();
let driver =
"func main() {\n prev := os.Getenv(\"NYX_PREV_OUTPUT\")\n fmt.Print(prev)\n}\n";
let mut driver = String::from(
"func main() {\n prev := os.Getenv(\"NYX_PREV_OUTPUT\")\n fmt.Print(prev)\n",
);
if let Some(t) = terminal {
let callee = go_string_literal(&t.sink_callee);
let sentinel = go_string_literal(ChainStepHarness::SINK_HIT_SENTINEL);
driver.push_str(&format!(
" __nyx_probe({callee}, prev)\n fmt.Println({sentinel})\n",
));
}
driver.push_str("}\n");
let source = format!("package main\n\nimport (\n{imports})\n{shim}\n{driver}");
ChainStepHarness {
source,
@ -115,6 +131,12 @@ fn chain_step(prev_output: Option<&[u8]>) -> ChainStepHarness {
}
}
/// Escape a string for safe Go double-quoted literal embedding.
fn go_string_literal(s: &str) -> String {
let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
format!("\"{escaped}\"")
}
/// Sorted, deduped tab-prefixed import lines covering the driver's
/// `fmt` + `os` plus everything in [`SHIM_IMPORTS`].
fn chain_step_imports() -> String {
@ -968,7 +990,7 @@ mod tests {
#[test]
fn chain_step_splices_probe_shim_for_composite_reverify() {
let step = chain_step(Some(b"<prev>"));
let step = chain_step(Some(b"<prev>"), None);
assert!(
step.source.contains("__nyx_probe"),
"Go chain step must splice the probe shim"

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::{ChainStepHarness, HarnessSource, LangEmitter};
use crate::dynamic::lang::{ChainStepHarness, ChainStepTerminal, HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::evidence::UnsupportedReason;
use std::path::PathBuf;
@ -75,16 +75,23 @@ impl LangEmitter for JavaEmitter {
materialize_java(env)
}
fn compose_chain_step(&self, prev_output: Option<&[u8]>) -> ChainStepHarness {
chain_step(prev_output)
fn compose_chain_step(
&self,
prev_output: Option<&[u8]>,
terminal: Option<&ChainStepTerminal>,
) -> ChainStepHarness {
chain_step(prev_output, terminal)
}
}
/// Phase 26 — Java chain-step harness.
///
/// Emits a `Step.java` class whose `main` reads `NYX_PREV_OUTPUT` and
/// forwards it on stdout. The command shell-wraps `javac` + `java` so
/// the step actually runs after the build step completes (the
/// forwards it on stdout. When the step is the chain's terminal step
/// the `main` body also calls `__nyx_probe(callee, prev)` and prints
/// [`ChainStepHarness::SINK_HIT_SENTINEL`] so the runner flips
/// `sink_hit` for the chain. The command shell-wraps `javac` + `java`
/// so the step actually runs after the build step completes (the
/// `ChainStepHarness.command` slot models a single process).
///
/// The Java probe shim (`__nyx_probe`, `__nyx_install_crash_guard`,
@ -95,10 +102,23 @@ impl LangEmitter for JavaEmitter {
/// fully-qualified `java.util.TreeMap` / `java.io.FileWriter` /
/// `java.nio.charset.StandardCharsets`, so no extra `import` lines
/// are needed beyond what stock Java implicitly imports.
fn chain_step(prev_output: Option<&[u8]>) -> ChainStepHarness {
fn chain_step(
prev_output: Option<&[u8]>,
terminal: Option<&ChainStepTerminal>,
) -> ChainStepHarness {
let shim = probe_shim();
let mut body = String::from(
" String prev = System.getenv(\"NYX_PREV_OUTPUT\");\n if (prev == null) prev = \"\";\n System.out.print(prev);\n",
);
if let Some(t) = terminal {
let callee = java_string_literal(&t.sink_callee);
let sentinel = java_string_literal(ChainStepHarness::SINK_HIT_SENTINEL);
body.push_str(&format!(
" __nyx_probe({callee}, prev);\n System.out.println({sentinel});\n System.out.flush();\n",
));
}
let source = format!(
"public class Step {{\n{shim}\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"
"public class Step {{\n{shim}\n public static void main(String[] args) {{\n{body} }}\n}}\n"
);
ChainStepHarness {
source,
@ -120,6 +140,12 @@ fn chain_step(prev_output: Option<&[u8]>) -> ChainStepHarness {
}
}
/// Escape a string for safe Java double-quoted literal embedding.
fn java_string_literal(s: &str) -> String {
let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
format!("\"{escaped}\"")
}
// ── Phase 14: shape detector ─────────────────────────────────────────────────
/// Concrete per-file shape resolved by reading the entry source.
@ -1142,7 +1168,7 @@ mod tests {
#[test]
fn chain_step_splices_probe_shim_for_composite_reverify() {
let step = chain_step(Some(b"<prev>"));
let step = chain_step(Some(b"<prev>"), None);
assert!(
step.source.contains("__nyx_probe"),
"Java chain step must splice the probe shim"

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, ChainStepHarness, HarnessSource, LangEmitter};
use crate::dynamic::lang::{js_shared, ChainStepHarness, ChainStepTerminal, HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec};
use crate::evidence::UnsupportedReason;
@ -44,8 +44,12 @@ impl LangEmitter for JavaScriptEmitter {
materialize_node(env)
}
fn compose_chain_step(&self, prev_output: Option<&[u8]>) -> ChainStepHarness {
js_shared::chain_step(prev_output, /* typescript = */ false)
fn compose_chain_step(
&self,
prev_output: Option<&[u8]>,
terminal: Option<&ChainStepTerminal>,
) -> ChainStepHarness {
js_shared::chain_step(prev_output, /* typescript = */ false, terminal)
}
}

View file

@ -24,7 +24,7 @@
//! which preserves the pre-Phase-13 behaviour.
use crate::dynamic::environment::{Environment, RuntimeArtifacts};
use crate::dynamic::lang::{ChainStepHarness, HarnessSource};
use crate::dynamic::lang::{ChainStepHarness, ChainStepTerminal, HarnessSource};
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::evidence::UnsupportedReason;
use crate::utils::project::DetectedFramework;
@ -454,12 +454,27 @@ 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 {
/// driver that reads `NYX_PREV_OUTPUT` and forwards it on stdout. When
/// the step is the chain's terminal step the driver also calls
/// `__nyx_probe(callee, prev)` and prints the
/// [`ChainStepHarness::SINK_HIT_SENTINEL`] so the runner flips
/// `sink_hit` for the chain.
pub fn chain_step(
prev_output: Option<&[u8]>,
is_typescript: bool,
terminal: Option<&ChainStepTerminal>,
) -> ChainStepHarness {
let probe = probe_shim();
let driver = "\nprocess.stdout.write(process.env.NYX_PREV_OUTPUT || '');\n";
let mut driver = String::from(
"\nconst __nyx_prev = process.env.NYX_PREV_OUTPUT || '';\nprocess.stdout.write(__nyx_prev);\n",
);
if let Some(t) = terminal {
let callee = js_string_literal(&t.sink_callee);
let sentinel = js_string_literal(ChainStepHarness::SINK_HIT_SENTINEL);
driver.push_str(&format!(
"__nyx_probe({callee}, __nyx_prev);\nconsole.log({sentinel});\n",
));
}
// The chain-step source is pure JS even under the TypeScript emitter
// — the probe shim uses no TS-specific syntax — so we keep the `.ts`
// filename intent (so the workdir reflects which emitter produced
@ -498,6 +513,12 @@ pub fn chain_step(prev_output: Option<&[u8]>, is_typescript: bool) -> ChainStepH
}
}
/// Escape a string for safe JS double-quoted literal embedding.
fn js_string_literal(s: &str) -> String {
let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
format!("\"{escaped}\"")
}
/// 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

@ -81,6 +81,41 @@ impl ChainStepHarness {
/// 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";
/// Sentinel printed to stdout by the terminal chain step so the
/// runner's [`crate::dynamic::sandbox::SandboxOutcome::sink_hit`]
/// fold can flip to `true` on a successful end-to-end compose.
/// Mirrors the per-language tracer sentinel used by the regular
/// harness emitters; the runner detects the byte sequence in
/// stdout/stderr.
pub const SINK_HIT_SENTINEL: &'static str = "__NYX_SINK_HIT__";
}
/// Phase 26 — terminal-step descriptor for [`LangEmitter::compose_chain_step`].
///
/// Carries the chain's terminal sink callee so the emitter can rewrite
/// the final step's source to invoke the probe shim with the threaded
/// payload and emit the [`ChainStepHarness::SINK_HIT_SENTINEL`]; the
/// composite reverifier then promotes its verdict from `Inconclusive`
/// to `Confirmed` when the runner observes the sentinel on the chain's
/// last step.
///
/// Non-terminal steps pass `None` so they retain the prev-output echo
/// behaviour.
#[derive(Debug, Clone)]
pub struct ChainStepTerminal {
/// Callee name for the chain's terminal sink (e.g. `"eval"`,
/// `"os.system"`, `"setattr"`). Used as the first argument to
/// `__nyx_probe(callee, prev)` so the per-language probe shim
/// records the witness. Kept as `String` rather than `&str` so the
/// reverifier can hand-roll a `ChainStepTerminal` from a
/// [`crate::chain::finding::ChainSink`] without lifetime gymnastics.
pub sink_callee: String,
/// Capability bits associated with the sink. Today the emitters do
/// not read this — recorded so a future per-cap sink-fire shape
/// dispatcher can pick the right invocation idiom without re-walking
/// the chain.
pub sink_cap_bits: u32,
}
/// Per-language harness emitter contract.
@ -135,25 +170,39 @@ pub trait LangEmitter {
/// 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.
/// the chain's entry step). `terminal` is `Some` only on the
/// chain's last step and carries the sink callee so the emitter
/// can splice in a `__nyx_probe(callee, prev)` call plus the
/// [`ChainStepHarness::SINK_HIT_SENTINEL`] stdout banner that the
/// runner detects via [`crate::dynamic::sandbox::SandboxOutcome::sink_hit`].
///
/// 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)
/// the previous step's output verbatim, and (when `terminal` is
/// set) appends a `printf '__NYX_SINK_HIT__\n'` line. Concrete
/// emitters override to splice in the language-native probe shim.
fn compose_chain_step(
&self,
prev_output: Option<&[u8]>,
terminal: Option<&ChainStepTerminal>,
) -> ChainStepHarness {
default_chain_step(prev_output, terminal)
}
}
/// 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 {
pub fn default_chain_step(
prev_output: Option<&[u8]>,
terminal: Option<&ChainStepTerminal>,
) -> ChainStepHarness {
let mut script = String::from("#!/bin/sh\nprintf '%s' \"${NYX_PREV_OUTPUT:-}\"\n");
if terminal.is_some() {
script.push_str("printf '\\n");
script.push_str(ChainStepHarness::SINK_HIT_SENTINEL);
script.push_str("\\n'\n");
}
ChainStepHarness {
source: "#!/bin/sh\nprintf '%s' \"${NYX_PREV_OUTPUT:-}\"\n".to_owned(),
source: script,
filename: "step.sh".to_owned(),
command: vec!["sh".to_owned(), "step.sh".to_owned()],
extra_env: prev_output
@ -172,9 +221,13 @@ pub fn default_chain_step(prev_output: Option<&[u8]>) -> ChainStepHarness {
///
/// 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))
pub fn compose_chain_step(
lang: Lang,
prev_output: Option<&[u8]>,
terminal: Option<&ChainStepTerminal>,
) -> ChainStepHarness {
dispatch(lang, |e| e.compose_chain_step(prev_output, terminal))
.unwrap_or_else(|| default_chain_step(prev_output, terminal))
}
/// 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::{ChainStepHarness, HarnessSource, LangEmitter};
use crate::dynamic::lang::{ChainStepHarness, ChainStepTerminal, HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::evidence::UnsupportedReason;
use std::path::PathBuf;
@ -68,8 +68,12 @@ impl LangEmitter for PhpEmitter {
materialize_php(env)
}
fn compose_chain_step(&self, prev_output: Option<&[u8]>) -> ChainStepHarness {
chain_step(prev_output)
fn compose_chain_step(
&self,
prev_output: Option<&[u8]>,
terminal: Option<&ChainStepTerminal>,
) -> ChainStepHarness {
chain_step(prev_output, terminal)
}
}
@ -77,14 +81,25 @@ impl LangEmitter for PhpEmitter {
///
/// Splices the PHP probe shim ([`probe_shim`]) in front of a minimal
/// driver that reads `NYX_PREV_OUTPUT` via `getenv()` 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; the shim has to be in the same file so a chain
/// step that terminates at a sink can also drive the `__nyx_probe`
/// channel.
fn chain_step(prev_output: Option<&[u8]>) -> ChainStepHarness {
/// on stdout. When the step is the chain's terminal step the driver
/// also calls `__nyx_probe(callee, [prev])` and emits the
/// [`ChainStepHarness::SINK_HIT_SENTINEL`] so the runner flips
/// `sink_hit` for the chain.
fn chain_step(
prev_output: Option<&[u8]>,
terminal: Option<&ChainStepTerminal>,
) -> ChainStepHarness {
let shim = probe_shim();
let driver = "$prev = getenv(\"NYX_PREV_OUTPUT\");\nif ($prev === false) { $prev = \"\"; }\necho $prev;\n";
let mut driver = String::from(
"$prev = getenv(\"NYX_PREV_OUTPUT\");\nif ($prev === false) { $prev = \"\"; }\necho $prev;\n",
);
if let Some(t) = terminal {
let callee = php_string_literal(&t.sink_callee);
let sentinel = php_string_literal(ChainStepHarness::SINK_HIT_SENTINEL);
driver.push_str(&format!(
"__nyx_probe({callee}, [$prev]);\necho \"\\n\" . {sentinel} . \"\\n\";\n",
));
}
let source = format!("<?php\n{shim}\n{driver}");
ChainStepHarness {
source,
@ -102,6 +117,14 @@ fn chain_step(prev_output: Option<&[u8]>) -> ChainStepHarness {
}
}
/// Escape a string for safe PHP double-quoted literal embedding.
/// Backslash and double-quote escape only; bytes outside printable
/// ASCII are left to PHP's source decoder.
fn php_string_literal(s: &str) -> String {
let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
format!("\"{escaped}\"")
}
// ── Phase 15: shape detector ─────────────────────────────────────────────────
/// Concrete per-file shape resolved by reading the entry source.
@ -789,7 +812,7 @@ mod tests {
#[test]
fn chain_step_splices_probe_shim_for_composite_reverify() {
let step = chain_step(Some(b"<prev>"));
let step = chain_step(Some(b"<prev>"), None);
assert!(
step.source.contains("__nyx_probe"),
"PHP chain step must splice the probe shim"

View file

@ -23,7 +23,7 @@
//! - Other slots produce [`UnsupportedReason::PayloadSlotUnsupported`].
use crate::dynamic::environment::{Environment, RuntimeArtifacts};
use crate::dynamic::lang::{ChainStepHarness, HarnessSource, LangEmitter};
use crate::dynamic::lang::{ChainStepHarness, ChainStepTerminal, HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::evidence::UnsupportedReason;
use crate::utils::project::DetectedFramework;
@ -66,20 +66,38 @@ impl LangEmitter for PythonEmitter {
materialize_python(env)
}
fn compose_chain_step(&self, prev_output: Option<&[u8]>) -> ChainStepHarness {
chain_step(prev_output)
fn compose_chain_step(
&self,
prev_output: Option<&[u8]>,
terminal: Option<&ChainStepTerminal>,
) -> ChainStepHarness {
chain_step(prev_output, terminal)
}
}
/// 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 {
/// driver that reads `NYX_PREV_OUTPUT` and forwards it on stdout. When
/// `terminal` is `Some`, the driver also calls `__nyx_probe(callee,
/// prev)` so the spliced shim records a witness, then prints the
/// [`ChainStepHarness::SINK_HIT_SENTINEL`] so the runner flips
/// `sink_hit` on the terminal step.
fn chain_step(
prev_output: Option<&[u8]>,
terminal: Option<&ChainStepTerminal>,
) -> 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";
let mut driver = String::from(
"\nimport os, sys\nprev = os.environ.get('NYX_PREV_OUTPUT', '')\nsys.stdout.write(prev)\nsys.stdout.flush()\n",
);
if let Some(t) = terminal {
let callee = python_string_literal(&t.sink_callee);
driver.push_str(&format!(
"__nyx_probe({callee}, prev)\nprint({sentinel}, flush=True)\n",
sentinel = python_string_literal(ChainStepHarness::SINK_HIT_SENTINEL),
));
}
ChainStepHarness {
source: format!("{probe}{driver}"),
filename: "step.py".to_owned(),
@ -96,6 +114,14 @@ fn chain_step(prev_output: Option<&[u8]>) -> ChainStepHarness {
}
}
/// Escape a string for safe Python single-quoted literal embedding.
/// Conservative: backslash + single-quote escape only; bytes outside
/// printable ASCII are left to Python's UTF-8 source decoder.
fn python_string_literal(s: &str) -> String {
let escaped = s.replace('\\', "\\\\").replace('\'', "\\'");
format!("'{escaped}'")
}
// ── Phase 12: shape detector ─────────────────────────────────────────────────
/// Concrete per-file shape resolved by reading the entry source.

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::{ChainStepHarness, HarnessSource, LangEmitter};
use crate::dynamic::lang::{ChainStepHarness, ChainStepTerminal, HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::evidence::UnsupportedReason;
use std::path::PathBuf;
@ -65,8 +65,12 @@ impl LangEmitter for RubyEmitter {
materialize_ruby(env)
}
fn compose_chain_step(&self, prev_output: Option<&[u8]>) -> ChainStepHarness {
chain_step(prev_output)
fn compose_chain_step(
&self,
prev_output: Option<&[u8]>,
terminal: Option<&ChainStepTerminal>,
) -> ChainStepHarness {
chain_step(prev_output, terminal)
}
}
@ -74,12 +78,25 @@ impl LangEmitter for RubyEmitter {
///
/// Splices the Ruby probe shim ([`probe_shim`]) in front of a minimal
/// driver that reads `NYX_PREV_OUTPUT` from `ENV` and forwards it on
/// stdout. Mirrors the Python / Node steps: a step that terminates at
/// a sink needs the shim in the same file so it can drive the
/// `__nyx_probe` channel.
fn chain_step(prev_output: Option<&[u8]>) -> ChainStepHarness {
/// stdout. When the step is the chain's terminal step the driver also
/// calls `__nyx_probe(callee, prev)` and emits the
/// [`ChainStepHarness::SINK_HIT_SENTINEL`] so the runner flips
/// `sink_hit` for the chain.
fn chain_step(
prev_output: Option<&[u8]>,
terminal: Option<&ChainStepTerminal>,
) -> ChainStepHarness {
let shim = probe_shim();
let driver = "prev = ENV[\"NYX_PREV_OUTPUT\"] || \"\"\n$stdout.write(prev)\n";
let mut driver = String::from(
"prev = ENV[\"NYX_PREV_OUTPUT\"] || \"\"\n$stdout.write(prev)\n",
);
if let Some(t) = terminal {
let callee = ruby_string_literal(&t.sink_callee);
let sentinel = ruby_string_literal(ChainStepHarness::SINK_HIT_SENTINEL);
driver.push_str(&format!(
"__nyx_probe({callee}, prev)\nputs {sentinel}\n$stdout.flush\n",
));
}
let source = format!("{shim}\n{driver}");
ChainStepHarness {
source,
@ -97,6 +114,12 @@ fn chain_step(prev_output: Option<&[u8]>) -> ChainStepHarness {
}
}
/// Escape a string for safe Ruby double-quoted literal embedding.
fn ruby_string_literal(s: &str) -> String {
let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
format!("\"{escaped}\"")
}
// ── Phase 15: shape detector ─────────────────────────────────────────────────
/// Concrete per-file shape resolved by reading the entry source.
@ -867,7 +890,7 @@ mod tests {
#[test]
fn chain_step_splices_probe_shim_for_composite_reverify() {
let step = chain_step(Some(b"<prev>"));
let step = chain_step(Some(b"<prev>"), None);
assert!(
step.source.contains("__nyx_probe"),
"Ruby chain step must splice the probe shim"

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::{ChainStepHarness, HarnessSource, LangEmitter};
use crate::dynamic::lang::{ChainStepHarness, ChainStepTerminal, HarnessSource, LangEmitter};
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
use crate::evidence::UnsupportedReason;
use crate::labels::Cap;
@ -64,8 +64,12 @@ impl LangEmitter for RustEmitter {
materialize_rust(env)
}
fn compose_chain_step(&self, prev_output: Option<&[u8]>) -> ChainStepHarness {
chain_step(prev_output)
fn compose_chain_step(
&self,
prev_output: Option<&[u8]>,
terminal: Option<&ChainStepTerminal>,
) -> ChainStepHarness {
chain_step(prev_output, terminal)
}
}
@ -78,9 +82,27 @@ impl LangEmitter for RustEmitter {
/// 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`.
fn chain_step(prev_output: Option<&[u8]>) -> ChainStepHarness {
///
/// When `terminal` is set, the driver also calls
/// `__nyx_probe(callee, &[&prev])` and prints
/// [`ChainStepHarness::SINK_HIT_SENTINEL`] so the runner flips
/// `sink_hit` on the chain's last step.
fn chain_step(
prev_output: Option<&[u8]>,
terminal: Option<&ChainStepTerminal>,
) -> ChainStepHarness {
let shim = probe_shim();
let driver = "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";
let mut driver = String::from(
"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",
);
if let Some(t) = terminal {
let callee = rust_string_literal(&t.sink_callee);
let sentinel = rust_string_literal(ChainStepHarness::SINK_HIT_SENTINEL);
driver.push_str(&format!(
" __nyx_probe({callee}, &[prev.as_str()]);\n println!({sentinel});\n",
));
}
driver.push_str("}\n");
let source = format!("{shim}\n{driver}");
let cargo_toml = "[package]\n\
name = \"nyx-chain-step\"\n\
@ -108,6 +130,12 @@ fn chain_step(prev_output: Option<&[u8]>) -> ChainStepHarness {
}
}
/// Escape a string for safe Rust double-quoted literal embedding.
fn rust_string_literal(s: &str) -> String {
let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
format!("\"{escaped}\"")
}
/// Phase 09 — Track D.2: synthesise a `Cargo.toml` that pins every
/// captured crate dep. The base cap-driven dep set lives in
/// [`generate_cargo_toml`]; this function layers the user's direct
@ -986,7 +1014,7 @@ mod tests {
// shim references `libc::*` so the step also ships a companion
// `Cargo.toml` via `extra_files` and drives the build through
// `cargo run --quiet` rather than single-file `rustc`.
let step = chain_step(Some(b"prev-output"));
let step = chain_step(Some(b"prev-output"), None);
assert!(
step.source.contains("__nyx_probe shim (Phase 06"),
"probe_shim banner missing from chain step source",
@ -1048,7 +1076,7 @@ mod tests {
#[test]
fn chain_step_emits_cargo_toml_with_libc_dep() {
let step = chain_step(None);
let step = chain_step(None, None);
let cargo = step
.extra_files
.iter()

View file

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

View file

@ -24,7 +24,7 @@ use nyx_scanner::chain::reverify::{
CompositeReverifier, chain_step_specs, reverify_chain_with, reverify_top_chains_with,
};
use nyx_scanner::commands::scan::Diag;
use nyx_scanner::dynamic::lang::{ChainStepHarness, compose_chain_step};
use nyx_scanner::dynamic::lang::{ChainStepHarness, ChainStepTerminal, compose_chain_step};
use nyx_scanner::dynamic::verify::VerifyOptions;
use nyx_scanner::evidence::{InconclusiveReason, UnsupportedReason, VerifyResult, VerifyStatus};
use nyx_scanner::surface::{SourceLocation, SurfaceMap};
@ -185,7 +185,7 @@ fn compose_chain_step_threads_prev_output_for_every_emitter() {
Lang::C,
Lang::Cpp,
] {
let step = compose_chain_step(lang, Some(prev));
let step = compose_chain_step(lang, Some(prev), None);
assert!(
step.extra_env
.iter()
@ -195,15 +195,59 @@ fn compose_chain_step_threads_prev_output_for_every_emitter() {
);
assert!(!step.source.is_empty(), "{lang:?} step source must be non-empty");
assert!(!step.command.is_empty(), "{lang:?} step command must be non-empty");
assert!(
!step.source.contains(ChainStepHarness::SINK_HIT_SENTINEL),
"{lang:?} non-terminal step must NOT carry the sink-hit sentinel; got source:\n{}",
step.source,
);
}
}
#[test]
fn compose_chain_step_with_no_prev_output_has_empty_extra_env() {
let step = compose_chain_step(Lang::Python, None);
let step = compose_chain_step(Lang::Python, None, None);
assert!(step.extra_env.is_empty());
}
#[test]
fn compose_chain_step_terminal_splices_sink_hit_sentinel_for_every_emitter() {
// Phase 26 deliverable: when `terminal` is `Some`, every emitter
// must splice the `SINK_HIT_SENTINEL` into the step's source so a
// successful end-to-end compose flips
// `SandboxOutcome::sink_hit` and the composite reverifier can
// promote its verdict from `Inconclusive` to `Confirmed`.
let prev = b"terminal-witness".as_slice();
let terminal = ChainStepTerminal {
sink_callee: "eval".into(),
sink_cap_bits: 0x400,
};
for lang in [
Lang::Python,
Lang::Rust,
Lang::JavaScript,
Lang::TypeScript,
Lang::Go,
Lang::Java,
Lang::Php,
Lang::Ruby,
Lang::C,
Lang::Cpp,
] {
let step = compose_chain_step(lang, Some(prev), Some(&terminal));
assert!(
step.source.contains(ChainStepHarness::SINK_HIT_SENTINEL),
"{lang:?} terminal step must splice {} into source; got source:\n{}",
ChainStepHarness::SINK_HIT_SENTINEL,
step.source,
);
assert!(
step.source.contains("eval"),
"{lang:?} terminal step must reference the sink callee `eval`; got source:\n{}",
step.source,
);
}
}
#[test]
fn chain_step_specs_aligns_results_to_member_order_and_reports_missing_diags() {
let chain = ChainFinding {