diff --git a/src/dynamic/lang/go.rs b/src/dynamic/lang/go.rs index 919c5ad0..a3023177 100644 --- a/src/dynamic/lang/go.rs +++ b/src/dynamic/lang/go.rs @@ -83,14 +83,22 @@ impl LangEmitter for GoEmitter { /// Phase 26 — Go chain-step harness. /// -/// Emits a `main.go` driver that reads `NYX_PREV_OUTPUT` and forwards it -/// on stdout. The Go probe shim (`__nyx_probe`) is top-level Go code -/// requiring extra stdlib imports; chain steps keep the harness minimal -/// and rely on the sandbox runner's outer probe channel to observe the -/// final sink fire. Wiring the probe shim into chain steps is tracked -/// alongside the Phase 15 emitter follow-up about probe shim splicing. +/// 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. +/// +/// 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 { - let source = "package main\n\nimport (\n \"fmt\"\n \"os\"\n)\n\nfunc main() {\n prev := os.Getenv(\"NYX_PREV_OUTPUT\")\n fmt.Print(prev)\n}\n".to_owned(); + 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 source = format!("package main\n\nimport (\n{imports})\n{shim}\n{driver}"); ChainStepHarness { source, filename: "step.go".to_owned(), @@ -106,6 +114,27 @@ fn chain_step(prev_output: Option<&[u8]>) -> ChainStepHarness { } } +/// Sorted, deduped tab-prefixed import lines covering the driver's +/// `fmt` + `os` plus everything in [`SHIM_IMPORTS`]. +fn chain_step_imports() -> String { + let driver_imports: &[&str] = &["fmt", "os"]; + let mut all: Vec<&str> = driver_imports + .iter() + .copied() + .chain(SHIM_IMPORTS.iter().copied()) + .collect(); + all.sort_unstable(); + all.dedup(); + let mut out = String::new(); + for path in &all { + out.push('\t'); + out.push('"'); + out.push_str(path); + out.push_str("\"\n"); + } + out +} + // ── Phase 15: shape detector ───────────────────────────────────────────────── /// Concrete per-file shape resolved by reading the entry source. @@ -846,4 +875,42 @@ mod tests { ); } } + + #[test] + fn chain_step_splices_probe_shim_for_composite_reverify() { + let step = chain_step(Some(b"")); + assert!( + step.source.contains("__nyx_probe"), + "Go chain step must splice the probe shim" + ); + assert!( + step.source.starts_with("package main"), + "Go chain step must open with package main" + ); + assert!( + step.source.contains("os.Getenv(\"NYX_PREV_OUTPUT\")"), + "Go chain step must keep its NYX_PREV_OUTPUT forwarder" + ); + let import_close = step.source.find(")\n").expect("import block must close"); + let shim_pos = step.source.find("__nyx_probe").unwrap(); + let main_pos = step.source.find("func main()").unwrap(); + assert!( + import_close < shim_pos, + "probe shim must come after the import block", + ); + assert!( + shim_pos < main_pos, + "probe shim must come before func main() so its helpers are in scope when a sink rewrite splices in", + ); + for path in SHIM_IMPORTS { + let quoted = format!("\"{path}\""); + assert!( + step.source.contains("ed), + "Go chain step must merge shim-required import {quoted} into its import block", + ); + } + // Driver imports preserved alongside the shim imports. + assert!(step.source.contains("\"fmt\"")); + assert!(step.source.contains("\"os\"")); + } } diff --git a/src/dynamic/lang/php.rs b/src/dynamic/lang/php.rs index 9c908210..ed2ac2b2 100644 --- a/src/dynamic/lang/php.rs +++ b/src/dynamic/lang/php.rs @@ -332,6 +332,26 @@ function __nyx_install_crash_guard(string $sinkCallee): void { } } } + +// Phase 10 (Track D.3) stub helpers. When the verifier spawned a SqlStub it +// publishes the queries-log path through NYX_SQL_LOG; a sink call site that +// wants the host-side stub to see its query appends one record-per-call. The +// helper is a no-op when NYX_SQL_LOG is unset so the same fixture source still +// runs under harness modes that didn't spawn a stub. Mirrors the Python and +// Node shims so the host-side SqlStub log-line format (hash-space-prefixed +// detail lines, then the query line) is identical across language emitters. +function __nyx_stub_sql_record($query, array $detail = []): void { + $p = getenv('NYX_SQL_LOG'); + if ($p === false || $p === '') return; + $buf = ''; + foreach ($detail as $k => $v) { + $buf .= '# ' . (string)$k . ': ' . (string)$v . "\n"; + } + $q = (string)$query; + $buf .= $q; + if (substr($q, -1) !== "\n") $buf .= "\n"; + @file_put_contents($p, $buf, FILE_APPEND); +} "# } @@ -718,6 +738,19 @@ mod tests { ); } + #[test] + fn probe_shim_publishes_stub_sql_recorder() { + let shim = probe_shim(); + assert!( + shim.contains("function __nyx_stub_sql_record"), + "PHP probe shim must define __nyx_stub_sql_record" + ); + assert!( + shim.contains("NYX_SQL_LOG"), + "stub recorder must read NYX_SQL_LOG" + ); + } + #[test] fn chain_step_splices_probe_shim_for_composite_reverify() { let step = chain_step(Some(b"")); diff --git a/tests/dynamic_fixtures/stubs_e2e/php/sql/vuln/main.php b/tests/dynamic_fixtures/stubs_e2e/php/sql/vuln/main.php new file mode 100644 index 00000000..40b6f989 --- /dev/null +++ b/tests/dynamic_fixtures/stubs_e2e/php/sql/vuln/main.php @@ -0,0 +1,41 @@ +query($query); + if ($rows !== false) { + while ($r = $rows->fetchArray(SQLITE3_NUM)) { + echo $r[0] . "\n"; + } + } + $db->close(); + } + // Record the executed query through the probe shim so the host + // SqlStub captures it on the next drain_events() call. + __nyx_stub_sql_record($query, ['driver' => $driver]); +} + +main(); diff --git a/tests/stubs_e2e_per_lang.rs b/tests/stubs_e2e_per_lang.rs index 72180011..1749cfad 100644 --- a/tests/stubs_e2e_per_lang.rs +++ b/tests/stubs_e2e_per_lang.rs @@ -21,6 +21,7 @@ #![cfg(feature = "dynamic")] use nyx_scanner::dynamic::lang::javascript::probe_shim as node_probe_shim; +use nyx_scanner::dynamic::lang::php::probe_shim as php_probe_shim; use nyx_scanner::dynamic::lang::python::probe_shim as python_probe_shim; use nyx_scanner::dynamic::stubs::{SqlStub, StubProvider}; use std::path::PathBuf; @@ -43,6 +44,14 @@ fn node_available() -> bool { .unwrap_or(false) } +fn php_available() -> bool { + Command::new("php") + .arg("--version") + .output() + .map(|o| o.status.success()) + .unwrap_or(false) +} + fn fixture_path(rel: &str) -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests") @@ -212,6 +221,119 @@ fn node_sql_stub_captures_tautology_query_via_shim_recorder() { ); } +fn strip_php_open_tag(src: &str) -> &str { + src.strip_prefix("