mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
[pitboss/grind] deferred session-0017 (20260517T044708Z-e058)
This commit is contained in:
parent
5b90a67f5c
commit
5b4181e4dd
14 changed files with 489 additions and 131 deletions
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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`].
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue