mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-15 20:05:13 +02:00
[pitboss/grind] deferred session-0019 (20260516T052512Z-20f8)
This commit is contained in:
parent
cf2dfb0fcf
commit
04b3d88eb4
6 changed files with 383 additions and 98 deletions
|
|
@ -160,7 +160,15 @@ fn is_rust_stdlib(name: &str) -> bool {
|
||||||
/// the shim's only dep on `std`; matches the
|
/// the shim's only dep on `std`; matches the
|
||||||
/// [`crate::dynamic::probe::SinkProbe`] wire format.
|
/// [`crate::dynamic::probe::SinkProbe`] wire format.
|
||||||
pub fn probe_shim() -> &'static str {
|
pub fn probe_shim() -> &'static str {
|
||||||
r#"
|
// Raw-string delimiter is `r##"..."##` (not `r#"..."#`) so the
|
||||||
|
// body can contain literal `"# ...` byte sequences without
|
||||||
|
// terminating the raw string early. The Phase 10 stub recorder
|
||||||
|
// helpers below emit hash-prefixed log lines (`"# method: ..."`)
|
||||||
|
// that would otherwise close `r#"..."#` at the first `"#`. Same
|
||||||
|
// workaround as Java's shim raw string (session 0018) — defensive
|
||||||
|
// so future shim extensions that introduce `"#` substrings drop
|
||||||
|
// in without further bumps.
|
||||||
|
r##"
|
||||||
// ── __nyx_probe shim (Phase 06 — Track C.1, Phase 08 — Track C.4 + C.5) ──────
|
// ── __nyx_probe shim (Phase 06 — Track C.1, Phase 08 — Track C.4 + C.5) ──────
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
const __NYX_DENY_SUBSTRINGS: &[&str] = &[
|
const __NYX_DENY_SUBSTRINGS: &[&str] = &[
|
||||||
|
|
@ -352,7 +360,90 @@ fn __nyx_install_crash_guard(sink_callee: &'static str) {
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
fn __nyx_install_crash_guard(_sink_callee: &'static str) {}
|
fn __nyx_install_crash_guard(_sink_callee: &'static str) {}
|
||||||
"#
|
|
||||||
|
// Phase 10 (Track D.3) SQL recording helper. Mirrors the
|
||||||
|
// Python/Node/PHP/Go/Ruby/Java siblings: when the verifier spawned a
|
||||||
|
// SqlStub it publishes the side-channel log path on `NYX_SQL_LOG`; a
|
||||||
|
// sink callsite whose query never reaches the on-the-wire SQLite
|
||||||
|
// engine can call this helper to surface the attempted query. Hash-
|
||||||
|
// prefixed detail lines followed by the query line so the host-side
|
||||||
|
// merger parses every language stream identically. No-op when the
|
||||||
|
// env var is unset.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn __nyx_stub_sql_record(query: &str, detail: &[(&str, &str)]) {
|
||||||
|
use std::io::Write;
|
||||||
|
let path = match std::env::var("NYX_SQL_LOG") {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
let mut buf = String::with_capacity(128);
|
||||||
|
for (k, v) in detail {
|
||||||
|
buf.push_str("# ");
|
||||||
|
buf.push_str(k);
|
||||||
|
buf.push_str(": ");
|
||||||
|
buf.push_str(v);
|
||||||
|
buf.push('\n');
|
||||||
|
}
|
||||||
|
buf.push_str(query);
|
||||||
|
if !query.ends_with('\n') {
|
||||||
|
buf.push('\n');
|
||||||
|
}
|
||||||
|
if let Ok(mut f) = std::fs::OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open(&path)
|
||||||
|
{
|
||||||
|
let _ = f.write_all(buf.as_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 10 (Track D.3) HTTP recording helper. When the verifier
|
||||||
|
// spawned an HttpStub it publishes the side-channel log path on
|
||||||
|
// `NYX_HTTP_LOG`; a sink callsite whose outbound request never
|
||||||
|
// reaches the on-the-wire listener (DNS-mocked, network-isolated
|
||||||
|
// sandbox, pre-flight check) can call this helper to surface the
|
||||||
|
// attempted call. Format matches the SQL helper so the host-side
|
||||||
|
// merger parses both streams identically. No-op when the env var
|
||||||
|
// is unset.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn __nyx_stub_http_record(method: &str, url: &str, body: Option<&str>, detail: &[(&str, &str)]) {
|
||||||
|
use std::io::Write;
|
||||||
|
let path = match std::env::var("NYX_HTTP_LOG") {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(_) => return,
|
||||||
|
};
|
||||||
|
let mut buf = String::with_capacity(128);
|
||||||
|
buf.push_str("# method: ");
|
||||||
|
buf.push_str(method);
|
||||||
|
buf.push('\n');
|
||||||
|
buf.push_str("# url: ");
|
||||||
|
buf.push_str(url);
|
||||||
|
buf.push('\n');
|
||||||
|
if let Some(b) = body {
|
||||||
|
buf.push_str("# body: ");
|
||||||
|
buf.push_str(b);
|
||||||
|
buf.push('\n');
|
||||||
|
}
|
||||||
|
for (k, v) in detail {
|
||||||
|
buf.push_str("# ");
|
||||||
|
buf.push_str(k);
|
||||||
|
buf.push_str(": ");
|
||||||
|
buf.push_str(v);
|
||||||
|
buf.push('\n');
|
||||||
|
}
|
||||||
|
buf.push_str(method);
|
||||||
|
buf.push(' ');
|
||||||
|
buf.push_str(url);
|
||||||
|
buf.push('\n');
|
||||||
|
if let Ok(mut f) = std::fs::OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open(&path)
|
||||||
|
{
|
||||||
|
let _ = f.write_all(buf.as_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"##
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Phase 16: shape detector ─────────────────────────────────────────────────
|
// ── Phase 16: shape detector ─────────────────────────────────────────────────
|
||||||
|
|
@ -927,6 +1018,34 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn probe_shim_publishes_stub_recorders() {
|
||||||
|
// Phase 10 (Track D.3): the Rust probe shim ships the SQL +
|
||||||
|
// HTTP recording helpers alongside the existing crash-guard /
|
||||||
|
// probe-emit machinery so a sink callsite can surface
|
||||||
|
// attempted boundary calls when the on-the-wire stub never
|
||||||
|
// sees them. Asserts the helper names + the `NYX_*_LOG` env
|
||||||
|
// hooks are present so future raw-string-delimiter regressions
|
||||||
|
// (`r#"..."#` → `r##"..."##`) get caught early.
|
||||||
|
let shim = probe_shim();
|
||||||
|
assert!(
|
||||||
|
shim.contains("fn __nyx_stub_sql_record("),
|
||||||
|
"Rust probe shim must define __nyx_stub_sql_record",
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
shim.contains("fn __nyx_stub_http_record("),
|
||||||
|
"Rust probe shim must define __nyx_stub_http_record",
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
shim.contains("NYX_SQL_LOG"),
|
||||||
|
"SQL recorder must read NYX_SQL_LOG",
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
shim.contains("NYX_HTTP_LOG"),
|
||||||
|
"HTTP recorder must read NYX_HTTP_LOG",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn chain_step_emits_cargo_toml_with_libc_dep() {
|
fn chain_step_emits_cargo_toml_with_libc_dep() {
|
||||||
let step = chain_step(None);
|
let step = chain_step(None);
|
||||||
|
|
|
||||||
|
|
@ -15,20 +15,16 @@ mod common;
|
||||||
|
|
||||||
#[cfg(feature = "dynamic")]
|
#[cfg(feature = "dynamic")]
|
||||||
mod c_fixture_tests {
|
mod c_fixture_tests {
|
||||||
use crate::common::fixture_harness::run_shape_fixture_lang;
|
use crate::common::fixture_harness::{run_shape_fixture_lang_or_skip, Prerequisite};
|
||||||
use nyx_scanner::dynamic::spec::PayloadSlot;
|
use nyx_scanner::dynamic::spec::PayloadSlot;
|
||||||
use nyx_scanner::evidence::{EntryKind, VerifyResult, VerifyStatus};
|
use nyx_scanner::evidence::{EntryKind, VerifyResult, VerifyStatus};
|
||||||
use nyx_scanner::labels::Cap;
|
use nyx_scanner::labels::Cap;
|
||||||
use nyx_scanner::symbol::Lang;
|
use nyx_scanner::symbol::Lang;
|
||||||
|
|
||||||
fn cc_available() -> bool {
|
const CC_REQ: &[Prerequisite] = &[Prerequisite::CommandAvailableEnvOverride {
|
||||||
let bin = std::env::var("NYX_CC_BIN").unwrap_or_else(|_| "cc".to_owned());
|
env_var: "NYX_CC_BIN",
|
||||||
std::process::Command::new(&bin)
|
default: "cc",
|
||||||
.arg("--version")
|
}];
|
||||||
.output()
|
|
||||||
.map(|o| o.status.success())
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn assert_confirmed(shape: &str, result: &VerifyResult) {
|
fn assert_confirmed(shape: &str, result: &VerifyResult) {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -57,6 +53,7 @@ mod c_fixture_tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn run(
|
fn run(
|
||||||
shape: &str,
|
shape: &str,
|
||||||
file: &str,
|
file: &str,
|
||||||
|
|
@ -65,9 +62,9 @@ mod c_fixture_tests {
|
||||||
sink_line: u32,
|
sink_line: u32,
|
||||||
kind: EntryKind,
|
kind: EntryKind,
|
||||||
slot: PayloadSlot,
|
slot: PayloadSlot,
|
||||||
) -> VerifyResult {
|
) -> Option<VerifyResult> {
|
||||||
run_shape_fixture_lang(
|
run_shape_fixture_lang_or_skip(
|
||||||
Lang::C, "c", shape, file, func, cap, sink_line, kind, slot,
|
CC_REQ, Lang::C, "c", shape, file, func, cap, sink_line, kind, slot,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,27 +72,19 @@ mod c_fixture_tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn main_argv_vuln_is_confirmed() {
|
fn main_argv_vuln_is_confirmed() {
|
||||||
if !cc_available() {
|
let Some(r) = run(
|
||||||
eprintln!("SKIP: cc not available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let r = run(
|
|
||||||
"main_argv", "vuln.c", "nyx_entry_main", Cap::CODE_EXEC, 23,
|
"main_argv", "vuln.c", "nyx_entry_main", Cap::CODE_EXEC, 23,
|
||||||
EntryKind::CliSubcommand, PayloadSlot::Argv(0),
|
EntryKind::CliSubcommand, PayloadSlot::Argv(0),
|
||||||
);
|
) else { return; };
|
||||||
assert_confirmed("main_argv", &r);
|
assert_confirmed("main_argv", &r);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn main_argv_benign_not_confirmed() {
|
fn main_argv_benign_not_confirmed() {
|
||||||
if !cc_available() {
|
let Some(r) = run(
|
||||||
eprintln!("SKIP: cc not available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let r = run(
|
|
||||||
"main_argv", "benign.c", "nyx_entry_main", Cap::CODE_EXEC, 11,
|
"main_argv", "benign.c", "nyx_entry_main", Cap::CODE_EXEC, 11,
|
||||||
EntryKind::CliSubcommand, PayloadSlot::Argv(0),
|
EntryKind::CliSubcommand, PayloadSlot::Argv(0),
|
||||||
);
|
) else { return; };
|
||||||
assert_not_confirmed("main_argv", &r);
|
assert_not_confirmed("main_argv", &r);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,27 +92,19 @@ mod c_fixture_tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn libfuzzer_vuln_is_confirmed() {
|
fn libfuzzer_vuln_is_confirmed() {
|
||||||
if !cc_available() {
|
let Some(r) = run(
|
||||||
eprintln!("SKIP: cc not available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let r = run(
|
|
||||||
"libfuzzer", "vuln.c", "LLVMFuzzerTestOneInput", Cap::CODE_EXEC, 16,
|
"libfuzzer", "vuln.c", "LLVMFuzzerTestOneInput", Cap::CODE_EXEC, 16,
|
||||||
EntryKind::LibraryApi, PayloadSlot::Param(0),
|
EntryKind::LibraryApi, PayloadSlot::Param(0),
|
||||||
);
|
) else { return; };
|
||||||
assert_confirmed("libfuzzer", &r);
|
assert_confirmed("libfuzzer", &r);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn libfuzzer_benign_not_confirmed() {
|
fn libfuzzer_benign_not_confirmed() {
|
||||||
if !cc_available() {
|
let Some(r) = run(
|
||||||
eprintln!("SKIP: cc not available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let r = run(
|
|
||||||
"libfuzzer", "benign.c", "LLVMFuzzerTestOneInput", Cap::CODE_EXEC, 10,
|
"libfuzzer", "benign.c", "LLVMFuzzerTestOneInput", Cap::CODE_EXEC, 10,
|
||||||
EntryKind::LibraryApi, PayloadSlot::Param(0),
|
EntryKind::LibraryApi, PayloadSlot::Param(0),
|
||||||
);
|
) else { return; };
|
||||||
assert_not_confirmed("libfuzzer", &r);
|
assert_not_confirmed("libfuzzer", &r);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -131,27 +112,19 @@ mod c_fixture_tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn free_fn_vuln_is_confirmed() {
|
fn free_fn_vuln_is_confirmed() {
|
||||||
if !cc_available() {
|
let Some(r) = run(
|
||||||
eprintln!("SKIP: cc not available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let r = run(
|
|
||||||
"free_fn", "vuln.c", "run", Cap::CODE_EXEC, 15,
|
"free_fn", "vuln.c", "run", Cap::CODE_EXEC, 15,
|
||||||
EntryKind::Function, PayloadSlot::Param(0),
|
EntryKind::Function, PayloadSlot::Param(0),
|
||||||
);
|
) else { return; };
|
||||||
assert_confirmed("free_fn", &r);
|
assert_confirmed("free_fn", &r);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn free_fn_benign_not_confirmed() {
|
fn free_fn_benign_not_confirmed() {
|
||||||
if !cc_available() {
|
let Some(r) = run(
|
||||||
eprintln!("SKIP: cc not available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let r = run(
|
|
||||||
"free_fn", "benign.c", "run", Cap::CODE_EXEC, 10,
|
"free_fn", "benign.c", "run", Cap::CODE_EXEC, 10,
|
||||||
EntryKind::Function, PayloadSlot::Param(0),
|
EntryKind::Function, PayloadSlot::Param(0),
|
||||||
);
|
) else { return; };
|
||||||
assert_not_confirmed("free_fn", &r);
|
assert_not_confirmed("free_fn", &r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,16 @@ pub enum Prerequisite {
|
||||||
/// the resolution path skips with a structured reason instead of
|
/// the resolution path skips with a structured reason instead of
|
||||||
/// failing the test.
|
/// failing the test.
|
||||||
NodeModuleAvailable(&'static str),
|
NodeModuleAvailable(&'static str),
|
||||||
|
/// A binary must resolve on `PATH` and respond to `--version` with
|
||||||
|
/// exit code 0, but the binary name can be overridden via an env
|
||||||
|
/// var. Used by the C / C++ fixture suites where `cc` / `c++` can
|
||||||
|
/// be swapped in for `clang` / `gcc` via `NYX_CC_BIN` / `NYX_CXX_BIN`.
|
||||||
|
/// The env var's *value* (when set) names the binary to probe;
|
||||||
|
/// otherwise `default` is used.
|
||||||
|
CommandAvailableEnvOverride {
|
||||||
|
env_var: &'static str,
|
||||||
|
default: &'static str,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Phase 29 (Track I): why the harness skipped a fixture. Carried by
|
/// Phase 29 (Track I): why the harness skipped a fixture. Carried by
|
||||||
|
|
@ -120,6 +130,27 @@ pub fn check_prerequisites(reqs: &[Prerequisite]) -> Result<(), SkipReason> {
|
||||||
return Err(SkipReason::MissingCommand(cmd));
|
return Err(SkipReason::MissingCommand(cmd));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Prerequisite::CommandAvailableEnvOverride { env_var, default } => {
|
||||||
|
// Resolve binary name from the env var when set; fall
|
||||||
|
// back to `default` so an unset override stays
|
||||||
|
// transparent to the existing acceptance contract. The
|
||||||
|
// suite under test reads the SAME env var to pick the
|
||||||
|
// binary it will execute, so the prereq probe lines up
|
||||||
|
// with the actual invocation.
|
||||||
|
let env_value = std::env::var(env_var).ok();
|
||||||
|
let bin: &str = match env_value.as_deref() {
|
||||||
|
Some(v) if !v.is_empty() => v,
|
||||||
|
_ => default,
|
||||||
|
};
|
||||||
|
let ok = std::process::Command::new(bin)
|
||||||
|
.arg("--version")
|
||||||
|
.output()
|
||||||
|
.map(|o| o.status.success())
|
||||||
|
.unwrap_or(false);
|
||||||
|
if !ok {
|
||||||
|
return Err(SkipReason::MissingCommand(default));
|
||||||
|
}
|
||||||
|
}
|
||||||
Prerequisite::EnvVar(var) => {
|
Prerequisite::EnvVar(var) => {
|
||||||
if std::env::var(var).is_err() {
|
if std::env::var(var).is_err() {
|
||||||
return Err(SkipReason::MissingEnvVar(var));
|
return Err(SkipReason::MissingEnvVar(var));
|
||||||
|
|
|
||||||
|
|
@ -15,20 +15,16 @@ mod common;
|
||||||
|
|
||||||
#[cfg(feature = "dynamic")]
|
#[cfg(feature = "dynamic")]
|
||||||
mod cpp_fixture_tests {
|
mod cpp_fixture_tests {
|
||||||
use crate::common::fixture_harness::run_shape_fixture_lang;
|
use crate::common::fixture_harness::{run_shape_fixture_lang_or_skip, Prerequisite};
|
||||||
use nyx_scanner::dynamic::spec::PayloadSlot;
|
use nyx_scanner::dynamic::spec::PayloadSlot;
|
||||||
use nyx_scanner::evidence::{EntryKind, VerifyResult, VerifyStatus};
|
use nyx_scanner::evidence::{EntryKind, VerifyResult, VerifyStatus};
|
||||||
use nyx_scanner::labels::Cap;
|
use nyx_scanner::labels::Cap;
|
||||||
use nyx_scanner::symbol::Lang;
|
use nyx_scanner::symbol::Lang;
|
||||||
|
|
||||||
fn cxx_available() -> bool {
|
const CXX_REQ: &[Prerequisite] = &[Prerequisite::CommandAvailableEnvOverride {
|
||||||
let bin = std::env::var("NYX_CXX_BIN").unwrap_or_else(|_| "c++".to_owned());
|
env_var: "NYX_CXX_BIN",
|
||||||
std::process::Command::new(&bin)
|
default: "c++",
|
||||||
.arg("--version")
|
}];
|
||||||
.output()
|
|
||||||
.map(|o| o.status.success())
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn assert_confirmed(shape: &str, result: &VerifyResult) {
|
fn assert_confirmed(shape: &str, result: &VerifyResult) {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -57,6 +53,7 @@ mod cpp_fixture_tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn run(
|
fn run(
|
||||||
shape: &str,
|
shape: &str,
|
||||||
file: &str,
|
file: &str,
|
||||||
|
|
@ -65,9 +62,9 @@ mod cpp_fixture_tests {
|
||||||
sink_line: u32,
|
sink_line: u32,
|
||||||
kind: EntryKind,
|
kind: EntryKind,
|
||||||
slot: PayloadSlot,
|
slot: PayloadSlot,
|
||||||
) -> VerifyResult {
|
) -> Option<VerifyResult> {
|
||||||
run_shape_fixture_lang(
|
run_shape_fixture_lang_or_skip(
|
||||||
Lang::Cpp, "cpp", shape, file, func, cap, sink_line, kind, slot,
|
CXX_REQ, Lang::Cpp, "cpp", shape, file, func, cap, sink_line, kind, slot,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,27 +72,19 @@ mod cpp_fixture_tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn main_argv_vuln_is_confirmed() {
|
fn main_argv_vuln_is_confirmed() {
|
||||||
if !cxx_available() {
|
let Some(r) = run(
|
||||||
eprintln!("SKIP: c++ not available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let r = run(
|
|
||||||
"main_argv", "vuln.cpp", "nyx_entry_main", Cap::CODE_EXEC, 16,
|
"main_argv", "vuln.cpp", "nyx_entry_main", Cap::CODE_EXEC, 16,
|
||||||
EntryKind::CliSubcommand, PayloadSlot::Argv(0),
|
EntryKind::CliSubcommand, PayloadSlot::Argv(0),
|
||||||
);
|
) else { return; };
|
||||||
assert_confirmed("main_argv", &r);
|
assert_confirmed("main_argv", &r);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn main_argv_benign_not_confirmed() {
|
fn main_argv_benign_not_confirmed() {
|
||||||
if !cxx_available() {
|
let Some(r) = run(
|
||||||
eprintln!("SKIP: c++ not available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let r = run(
|
|
||||||
"main_argv", "benign.cpp", "nyx_entry_main", Cap::CODE_EXEC, 11,
|
"main_argv", "benign.cpp", "nyx_entry_main", Cap::CODE_EXEC, 11,
|
||||||
EntryKind::CliSubcommand, PayloadSlot::Argv(0),
|
EntryKind::CliSubcommand, PayloadSlot::Argv(0),
|
||||||
);
|
) else { return; };
|
||||||
assert_not_confirmed("main_argv", &r);
|
assert_not_confirmed("main_argv", &r);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,27 +92,19 @@ mod cpp_fixture_tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn libfuzzer_vuln_is_confirmed() {
|
fn libfuzzer_vuln_is_confirmed() {
|
||||||
if !cxx_available() {
|
let Some(r) = run(
|
||||||
eprintln!("SKIP: c++ not available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let r = run(
|
|
||||||
"libfuzzer", "vuln.cpp", "LLVMFuzzerTestOneInput", Cap::CODE_EXEC, 15,
|
"libfuzzer", "vuln.cpp", "LLVMFuzzerTestOneInput", Cap::CODE_EXEC, 15,
|
||||||
EntryKind::LibraryApi, PayloadSlot::Param(0),
|
EntryKind::LibraryApi, PayloadSlot::Param(0),
|
||||||
);
|
) else { return; };
|
||||||
assert_confirmed("libfuzzer", &r);
|
assert_confirmed("libfuzzer", &r);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn libfuzzer_benign_not_confirmed() {
|
fn libfuzzer_benign_not_confirmed() {
|
||||||
if !cxx_available() {
|
let Some(r) = run(
|
||||||
eprintln!("SKIP: c++ not available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let r = run(
|
|
||||||
"libfuzzer", "benign.cpp", "LLVMFuzzerTestOneInput", Cap::CODE_EXEC, 10,
|
"libfuzzer", "benign.cpp", "LLVMFuzzerTestOneInput", Cap::CODE_EXEC, 10,
|
||||||
EntryKind::LibraryApi, PayloadSlot::Param(0),
|
EntryKind::LibraryApi, PayloadSlot::Param(0),
|
||||||
);
|
) else { return; };
|
||||||
assert_not_confirmed("libfuzzer", &r);
|
assert_not_confirmed("libfuzzer", &r);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -131,27 +112,19 @@ mod cpp_fixture_tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn free_fn_vuln_is_confirmed() {
|
fn free_fn_vuln_is_confirmed() {
|
||||||
if !cxx_available() {
|
let Some(r) = run(
|
||||||
eprintln!("SKIP: c++ not available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let r = run(
|
|
||||||
"free_fn", "vuln.cpp", "run", Cap::CODE_EXEC, 12,
|
"free_fn", "vuln.cpp", "run", Cap::CODE_EXEC, 12,
|
||||||
EntryKind::Function, PayloadSlot::Param(0),
|
EntryKind::Function, PayloadSlot::Param(0),
|
||||||
);
|
) else { return; };
|
||||||
assert_confirmed("free_fn", &r);
|
assert_confirmed("free_fn", &r);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn free_fn_benign_not_confirmed() {
|
fn free_fn_benign_not_confirmed() {
|
||||||
if !cxx_available() {
|
let Some(r) = run(
|
||||||
eprintln!("SKIP: c++ not available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let r = run(
|
|
||||||
"free_fn", "benign.cpp", "run", Cap::CODE_EXEC, 10,
|
"free_fn", "benign.cpp", "run", Cap::CODE_EXEC, 10,
|
||||||
EntryKind::Function, PayloadSlot::Param(0),
|
EntryKind::Function, PayloadSlot::Param(0),
|
||||||
);
|
) else { return; };
|
||||||
assert_not_confirmed("free_fn", &r);
|
assert_not_confirmed("free_fn", &r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
18
tests/dynamic_fixtures/stubs_e2e/rust/http/vuln/main.rs
Normal file
18
tests/dynamic_fixtures/stubs_e2e/rust/http/vuln/main.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
// Phase 10 (Track D.3) — Rust HTTP recorder body-only fragment.
|
||||||
|
//
|
||||||
|
// Wrapped at test time by `wrap_rust_fragment(body, shim)` in
|
||||||
|
// `tests/stubs_e2e_per_lang.rs`: the wrapper prepends the Rust probe
|
||||||
|
// shim (which carries `__nyx_stub_http_record`) and a one-line
|
||||||
|
// `Cargo.toml` so `cargo run --quiet` builds the program in place.
|
||||||
|
//
|
||||||
|
// The fragment never issues the actual network call. It records the
|
||||||
|
// SSRF attempt at 169.254.169.254/latest/meta-data/ through the shim
|
||||||
|
// recorder so the host-side HttpStub captures the boundary event.
|
||||||
|
let _endpoint = std::env::var("NYX_HTTP_ENDPOINT").unwrap_or_default();
|
||||||
|
let detail: &[(&str, &str)] = &[("driver", "manual")];
|
||||||
|
__nyx_stub_http_record(
|
||||||
|
"GET",
|
||||||
|
"http://169.254.169.254/latest/meta-data/",
|
||||||
|
None,
|
||||||
|
detail,
|
||||||
|
);
|
||||||
|
|
@ -26,6 +26,7 @@ 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::php::probe_shim as php_probe_shim;
|
||||||
use nyx_scanner::dynamic::lang::python::probe_shim as python_probe_shim;
|
use nyx_scanner::dynamic::lang::python::probe_shim as python_probe_shim;
|
||||||
use nyx_scanner::dynamic::lang::ruby::probe_shim as ruby_probe_shim;
|
use nyx_scanner::dynamic::lang::ruby::probe_shim as ruby_probe_shim;
|
||||||
|
use nyx_scanner::dynamic::lang::rust::probe_shim as rust_probe_shim;
|
||||||
use nyx_scanner::dynamic::stubs::{HttpStub, SqlStub, StubProvider};
|
use nyx_scanner::dynamic::stubs::{HttpStub, SqlStub, StubProvider};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
@ -71,6 +72,14 @@ fn ruby_available() -> bool {
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn cargo_available() -> bool {
|
||||||
|
Command::new("cargo")
|
||||||
|
.arg("--version")
|
||||||
|
.output()
|
||||||
|
.map(|o| o.status.success())
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
fn java_available() -> bool {
|
fn java_available() -> bool {
|
||||||
// The Java shim helpers use `java MainSource.java` single-file
|
// The Java shim helpers use `java MainSource.java` single-file
|
||||||
// source-mode (JEP 330, JDK 11+) so only the `java` runtime is
|
// source-mode (JEP 330, JDK 11+) so only the `java` runtime is
|
||||||
|
|
@ -127,6 +136,39 @@ fn wrap_go_fragment(body: &str, shim: &str) -> String {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wrap the body-only Rust HTTP fragment in a complete crate: prepend
|
||||||
|
/// the Rust probe shim (which carries `__nyx_stub_http_record`) at
|
||||||
|
/// file scope and wrap the fragment as the body of `fn main()`. The
|
||||||
|
/// caller writes the result alongside a one-line `Cargo.toml` that
|
||||||
|
/// pins `libc = "0.2"` (the shim's `__nyx_install_crash_guard` path
|
||||||
|
/// references `libc::sigaction`) and drives the build through
|
||||||
|
/// `cargo run --quiet`. Mirrors the production Rust emitter ordering
|
||||||
|
/// — shim at file scope, then `fn main()` calling into it.
|
||||||
|
fn wrap_rust_fragment(body: &str, shim: &str) -> String {
|
||||||
|
format!(
|
||||||
|
"{shim}\n\
|
||||||
|
fn main() {{\n\
|
||||||
|
{body}\n\
|
||||||
|
}}\n"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One-line Cargo.toml for the Rust stub-recorder driver. Mirrors
|
||||||
|
/// the Phase 26 chain_step manifest (session 0014) — `[[bin]]` points
|
||||||
|
/// at `main.rs` so `cargo run --quiet` builds the source the test
|
||||||
|
/// just wrote, and `libc = "0.2"` is unconditionally pinned because
|
||||||
|
/// the spliced probe shim's `__nyx_install_crash_guard` references
|
||||||
|
/// `libc::sigaction` on Unix.
|
||||||
|
const RUST_STUB_CARGO_TOML: &str = "[package]\n\
|
||||||
|
name = \"nyx-stub-driver\"\n\
|
||||||
|
version = \"0.0.1\"\n\
|
||||||
|
edition = \"2021\"\n\n\
|
||||||
|
[[bin]]\n\
|
||||||
|
name = \"stub_driver\"\n\
|
||||||
|
path = \"main.rs\"\n\n\
|
||||||
|
[dependencies]\n\
|
||||||
|
libc = \"0.2\"\n";
|
||||||
|
|
||||||
fn fixture_path(rel: &str) -> PathBuf {
|
fn fixture_path(rel: &str) -> PathBuf {
|
||||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
.join("tests")
|
.join("tests")
|
||||||
|
|
@ -1086,3 +1128,132 @@ fn node_sql_shim_recorder_is_noop_without_log_env() {
|
||||||
events.len()
|
events.len()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a shared CARGO_TARGET_DIR for Rust stub-recorder tests so
|
||||||
|
/// repeated runs reuse the libc build artifacts instead of paying
|
||||||
|
/// the full compile cost per test. Lives under the host crate's
|
||||||
|
/// own `target/` so `cargo clean` still wipes it.
|
||||||
|
fn rust_stub_target_dir() -> PathBuf {
|
||||||
|
PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("stubs_e2e_rust")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rust_http_stub_captures_attempted_outbound_via_shim_recorder() {
|
||||||
|
// Phase 10 (Track D.3) HTTP recording: Rust leg of the side-channel
|
||||||
|
// `__nyx_stub_http_record` helper. Mirrors the Python / Node / PHP /
|
||||||
|
// Go / Ruby / Java HTTP tests — records an SSRF attempt without
|
||||||
|
// issuing the actual network call. Uses the `extra_files`-driven
|
||||||
|
// `Cargo.toml` shape session 0014 prototyped for chain steps: write
|
||||||
|
// a one-line manifest alongside the wrapped fragment so `cargo run
|
||||||
|
// --quiet` resolves `libc` (referenced by the spliced probe shim's
|
||||||
|
// `__nyx_install_crash_guard`) without any host crate-cache assumptions.
|
||||||
|
if !cargo_available() {
|
||||||
|
eprintln!("SKIP: cargo not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let workdir = TempDir::new().expect("tempdir");
|
||||||
|
let stub = HttpStub::start(workdir.path()).expect("HttpStub::start");
|
||||||
|
|
||||||
|
let endpoint = stub.endpoint();
|
||||||
|
let recording = stub
|
||||||
|
.recording_endpoint()
|
||||||
|
.expect("HttpStub must publish a recording endpoint");
|
||||||
|
|
||||||
|
let fragment = std::fs::read_to_string(fixture_path("rust/http/vuln/main.rs"))
|
||||||
|
.expect("read rust fragment");
|
||||||
|
let source = wrap_rust_fragment(&fragment, rust_probe_shim());
|
||||||
|
|
||||||
|
let crate_dir = workdir.path().join("driver");
|
||||||
|
std::fs::create_dir_all(&crate_dir).expect("create crate dir");
|
||||||
|
std::fs::write(crate_dir.join("Cargo.toml"), RUST_STUB_CARGO_TOML)
|
||||||
|
.expect("write Cargo.toml");
|
||||||
|
std::fs::write(crate_dir.join("main.rs"), source).expect("write main.rs");
|
||||||
|
|
||||||
|
let output = Command::new("cargo")
|
||||||
|
.arg("run")
|
||||||
|
.arg("--quiet")
|
||||||
|
.arg("--manifest-path")
|
||||||
|
.arg(crate_dir.join("Cargo.toml"))
|
||||||
|
.env("CARGO_TARGET_DIR", rust_stub_target_dir())
|
||||||
|
.env("NYX_HTTP_ENDPOINT", &endpoint)
|
||||||
|
.env(recording.0, &recording.1)
|
||||||
|
.output()
|
||||||
|
.expect("cargo run rust driver");
|
||||||
|
assert!(
|
||||||
|
output.status.success(),
|
||||||
|
"driver must exit 0; stdout = {}\nstderr = {}",
|
||||||
|
String::from_utf8_lossy(&output.stdout),
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
|
||||||
|
let events = stub.drain_events();
|
||||||
|
assert!(
|
||||||
|
!events.is_empty(),
|
||||||
|
"HttpStub must capture at least one event after the Rust shim recorder fires"
|
||||||
|
);
|
||||||
|
let hit = events
|
||||||
|
.iter()
|
||||||
|
.find(|e| e.summary.contains("169.254.169.254"))
|
||||||
|
.expect("recorded URL must contain the SSRF marker");
|
||||||
|
assert_eq!(
|
||||||
|
hit.detail.get("method").map(String::as_str),
|
||||||
|
Some("GET"),
|
||||||
|
"method detail must surface on the recorded event"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
hit.detail.get("url").map(String::as_str),
|
||||||
|
Some("http://169.254.169.254/latest/meta-data/"),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
hit.detail.get("driver").map(String::as_str),
|
||||||
|
Some("manual"),
|
||||||
|
"detail slice passed to __nyx_stub_http_record must surface as event detail entries"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rust_http_shim_recorder_is_noop_without_log_env() {
|
||||||
|
if !cargo_available() {
|
||||||
|
eprintln!("SKIP: cargo not available");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let workdir = TempDir::new().expect("tempdir");
|
||||||
|
let stub = HttpStub::start(workdir.path()).expect("HttpStub::start");
|
||||||
|
|
||||||
|
let endpoint = stub.endpoint();
|
||||||
|
let fragment = std::fs::read_to_string(fixture_path("rust/http/vuln/main.rs"))
|
||||||
|
.expect("read rust fragment");
|
||||||
|
let source = wrap_rust_fragment(&fragment, rust_probe_shim());
|
||||||
|
|
||||||
|
let crate_dir = workdir.path().join("driver_no_log");
|
||||||
|
std::fs::create_dir_all(&crate_dir).expect("create crate dir");
|
||||||
|
std::fs::write(crate_dir.join("Cargo.toml"), RUST_STUB_CARGO_TOML)
|
||||||
|
.expect("write Cargo.toml");
|
||||||
|
std::fs::write(crate_dir.join("main.rs"), source).expect("write main.rs");
|
||||||
|
|
||||||
|
let output = Command::new("cargo")
|
||||||
|
.arg("run")
|
||||||
|
.arg("--quiet")
|
||||||
|
.arg("--manifest-path")
|
||||||
|
.arg(crate_dir.join("Cargo.toml"))
|
||||||
|
.env("CARGO_TARGET_DIR", rust_stub_target_dir())
|
||||||
|
.env("NYX_HTTP_ENDPOINT", &endpoint)
|
||||||
|
.env_remove("NYX_HTTP_LOG")
|
||||||
|
.output()
|
||||||
|
.expect("cargo run rust driver");
|
||||||
|
assert!(
|
||||||
|
output.status.success(),
|
||||||
|
"driver must exit 0 even without NYX_HTTP_LOG; stdout = {}\nstderr = {}",
|
||||||
|
String::from_utf8_lossy(&output.stdout),
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
|
||||||
|
let events = stub.drain_events();
|
||||||
|
assert!(
|
||||||
|
events.is_empty(),
|
||||||
|
"no events expected when the recording env var is unset, got {} entries",
|
||||||
|
events.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue