diff --git a/src/dynamic/lang/c.rs b/src/dynamic/lang/c.rs index f8d4fa7e..7b62b9d8 100644 --- a/src/dynamic/lang/c.rs +++ b/src/dynamic/lang/c.rs @@ -365,6 +365,8 @@ pub fn emit(spec: &HarnessSpec) -> Result { fn generate_main_c(spec: &HarnessSpec, shape: CShape) -> String { let invocation = invoke_for_shape(spec, shape); let (entry_open, entry_close) = entry_include_guards(spec); + let shim = probe_shim(); + let crash_callee = entry_symbol_for_spec(spec); format!( r#"/* Nyx dynamic harness — auto-generated, do not edit (Phase 16 — CShape::{shape:?}). */ @@ -373,7 +375,7 @@ fn generate_main_c(spec: &HarnessSpec, shape: CShape) -> String { #include #include #include - +{shim} /* Forward declarations: the entry file is appended below via `#include` * so the harness can call user-defined functions without a separate * compilation unit. */ @@ -386,6 +388,13 @@ int main(int argc, char *argv[]) {{ char *payload = nyx_payload(); if (!payload) payload = (char*)""; + /* Phase 08 sink-site signal handler: install AFTER payload decode so a + * crash inside `nyx_payload`/`nyx_b64_decode` (harness setup) writes no + * Crash probe, routing the verifier to `Inconclusive(UnrelatedCrash)`. + * A crash inside the entry call below DOES fire the handler and writes + * a Crash probe to `NYX_PROBE_PATH`, lifting an `Oracle::SinkCrash` + * payload to `Confirmed`. */ + __nyx_install_crash_guard("{crash_callee}"); {invocation} /* Intentionally no free(payload): payload is either a strdup/b64_decode * heap pointer or a string literal substituted above when allocation @@ -460,12 +469,21 @@ fn entry_include_guards(spec: &HarnessSpec) -> (&'static str, &'static str) { } } -fn invoke_for_shape(spec: &HarnessSpec, shape: CShape) -> String { - let entry_fn: &str = if spec.entry_name == "main" { +/// Effective C symbol used to invoke the entry from the harness `main`. +/// Mirrors the rename inserted by [`entry_include_guards`]: when the user's +/// entry function IS named `main` it is renamed to `__nyx_entry_main` via +/// the preprocessor wrap, so both the call site in [`invoke_for_shape`] and +/// the `__nyx_install_crash_guard` callee label use this helper. +fn entry_symbol_for_spec(spec: &HarnessSpec) -> &str { + if spec.entry_name == "main" { "__nyx_entry_main" } else { spec.entry_name.as_str() - }; + } +} + +fn invoke_for_shape(spec: &HarnessSpec, shape: CShape) -> String { + let entry_fn: &str = entry_symbol_for_spec(spec); match shape { CShape::FreeFn => match &spec.payload_slot { PayloadSlot::EnvVar(name) => format!( @@ -673,6 +691,60 @@ mod tests { assert!(fh.source.contains("nyx_entry_main(new_argc, new_argv)")); } + #[test] + fn emit_splices_probe_shim_and_installs_crash_guard_for_free_fn() { + // Phase 16 follow-up: the C emitter now splices probe_shim() into the + // generated harness AND installs the sink-site signal handler around + // the entry invocation. This is the joint unblock for Phase 08 + // (a) / (b) — a SIGSEGV inside the entry writes a Crash probe to + // `NYX_PROBE_PATH`; a SIGSEGV during `nyx_payload` setup (before the + // install) writes nothing, routing to `Inconclusive(UnrelatedCrash)`. + let spec = make_spec(PayloadSlot::Param(0)); + let h = emit(&spec).unwrap(); + // The shim text is identified by its banner comment. + assert!( + h.source.contains("__nyx_probe shim (Phase 06 — Track C.1"), + "probe_shim banner missing from generated main.c — splicing regressed", + ); + // The signal-handler installer is callable from the harness body. + assert!( + h.source.contains("static void __nyx_install_crash_guard("), + "install_crash_guard definition missing from generated main.c", + ); + // The install call references the entry symbol (here `run`, since + // `make_spec` sets `entry_name = "run"`). + assert!( + h.source.contains("__nyx_install_crash_guard(\"run\");"), + "install_crash_guard call site missing or wrong callee in main()", + ); + // The install must come after `nyx_payload()` returns and before the + // entry invocation — otherwise a crash inside payload decode would + // be misattributed to the sink (would defeat Phase 08(b)). + let install_pos = h.source.find("__nyx_install_crash_guard(\"run\");").unwrap(); + let payload_pos = h.source.find("char *payload = nyx_payload();").unwrap(); + let invoke_pos = h.source.find("run(payload, strlen(payload));").unwrap(); + assert!( + payload_pos < install_pos && install_pos < invoke_pos, + "install_crash_guard ordering wrong: payload_pos={payload_pos} install_pos={install_pos} invoke_pos={invoke_pos}", + ); + } + + #[test] + fn emit_install_crash_guard_targets_renamed_main_entry() { + // Real-world Track B CLI vuln: spec.entry_name == "main" → the entry + // is renamed to __nyx_entry_main by entry_include_guards, and the + // install call must reference the renamed symbol so the Crash probe + // attributes correctly. + let mut spec = make_spec(PayloadSlot::Argv(0)); + spec.entry_kind = EntryKind::CliSubcommand; + spec.entry_name = "main".into(); + let h = emit(&spec).unwrap(); + assert!( + h.source.contains("__nyx_install_crash_guard(\"__nyx_entry_main\");"), + "install_crash_guard must use the post-rename symbol when entry_name == 'main'", + ); + } + #[test] fn emit_libfuzzer_shape_passes_bytes() { let mut spec = make_spec(PayloadSlot::Param(0)); diff --git a/src/dynamic/lang/cpp.rs b/src/dynamic/lang/cpp.rs index 779242b7..60798527 100644 --- a/src/dynamic/lang/cpp.rs +++ b/src/dynamic/lang/cpp.rs @@ -336,6 +336,8 @@ pub fn emit(spec: &HarnessSpec) -> Result { fn generate_main_cpp(spec: &HarnessSpec, shape: CppShape) -> String { let invocation = invoke_for_shape(spec, shape); let (entry_open, entry_close) = entry_include_guards(spec); + let shim = probe_shim(); + let crash_callee = entry_symbol_for_spec(spec); format!( r#"// Nyx dynamic harness — auto-generated, do not edit (Phase 16 — CppShape::{shape:?}). @@ -346,7 +348,7 @@ fn generate_main_cpp(spec: &HarnessSpec, shape: CppShape) -> String { #include #include #include - +{shim} static std::string nyx_payload(); {entry_open}#include "entry.cpp" @@ -355,6 +357,11 @@ int main(int argc, char *argv[]) {{ (void)argc; (void)argv; std::string payload = nyx_payload(); + // Phase 08 sink-site signal handler: install AFTER payload decode so a + // crash in nyx_payload / nyx_b64_decode (harness setup) writes no Crash + // probe. A crash inside the entry call below fires the handler and + // writes a Crash probe to NYX_PROBE_PATH for `Oracle::SinkCrash`. + __nyx_install_crash_guard("{crash_callee}"); {invocation} return 0; }} @@ -415,12 +422,19 @@ fn entry_include_guards(spec: &HarnessSpec) -> (&'static str, &'static str) { } } -fn invoke_for_shape(spec: &HarnessSpec, shape: CppShape) -> String { - let entry_fn: &str = if spec.entry_name == "main" { +/// Effective C++ symbol used to invoke the entry from the harness `main`, +/// after [`entry_include_guards`] has rewritten an entry-side `main` to +/// `__nyx_entry_main`. +fn entry_symbol_for_spec(spec: &HarnessSpec) -> &str { + if spec.entry_name == "main" { "__nyx_entry_main" } else { spec.entry_name.as_str() - }; + } +} + +fn invoke_for_shape(spec: &HarnessSpec, shape: CppShape) -> String { + let entry_fn: &str = entry_symbol_for_spec(spec); match shape { CppShape::FreeFn => match &spec.payload_slot { PayloadSlot::EnvVar(name) => format!( @@ -594,6 +608,46 @@ mod tests { assert!(fh.source.contains("nyx_entry_main(static_cast(argv_storage.size()), new_argv.data())")); } + #[test] + fn emit_splices_probe_shim_and_installs_crash_guard_for_free_fn() { + // Phase 16 follow-up: C++ emitter now splices probe_shim() and + // installs the sink-site signal handler around the entry call. + // Mirrors the C-side splicing tests. + let spec = make_spec(PayloadSlot::Param(0)); + let h = emit(&spec).unwrap(); + assert!( + h.source.contains("__nyx_probe shim (Phase 06 — Track C.1"), + "probe_shim banner missing from generated main.cpp", + ); + assert!( + h.source.contains("inline void __nyx_install_crash_guard("), + "install_crash_guard definition missing from generated main.cpp", + ); + assert!( + h.source.contains("__nyx_install_crash_guard(\"run\");"), + "install_crash_guard call site missing or wrong callee", + ); + let install_pos = h.source.find("__nyx_install_crash_guard(\"run\");").unwrap(); + let payload_pos = h.source.find("std::string payload = nyx_payload();").unwrap(); + let invoke_pos = h.source.find("run(payload.c_str(), payload.size());").unwrap(); + assert!( + payload_pos < install_pos && install_pos < invoke_pos, + "install_crash_guard ordering wrong: payload_pos={payload_pos} install_pos={install_pos} invoke_pos={invoke_pos}", + ); + } + + #[test] + fn emit_install_crash_guard_targets_renamed_main_entry() { + let mut spec = make_spec(PayloadSlot::Argv(0)); + spec.entry_kind = EntryKind::CliSubcommand; + spec.entry_name = "main".into(); + let h = emit(&spec).unwrap(); + assert!( + h.source.contains("__nyx_install_crash_guard(\"__nyx_entry_main\");"), + "install_crash_guard must use post-rename symbol when entry_name == 'main'", + ); + } + #[test] fn emit_cmake_in_extra_files() { let spec = make_spec(PayloadSlot::Param(0)); diff --git a/src/dynamic/lang/rust.rs b/src/dynamic/lang/rust.rs index dca65071..42592bbd 100644 --- a/src/dynamic/lang/rust.rs +++ b/src/dynamic/lang/rust.rs @@ -473,9 +473,15 @@ pub fn emit(spec: &HarnessSpec) -> Result { /// Dependencies are driven by `expected_cap`: /// - `SQL_QUERY` → `rusqlite` with the `bundled` feature (embeds SQLite). /// - Other caps use only std (no extra deps). +/// +/// `libc` is always pinned because the Phase 16 probe shim (spliced into +/// `src/main.rs` by [`generate_main_rs`]) calls `libc::sigaction` from +/// `__nyx_install_crash_guard`. The shim is unconditionally compiled so +/// the dep must be unconditional too. pub fn generate_cargo_toml(cap: Cap) -> String { let mut deps = String::new(); + deps.push_str("libc = \"0.2\"\n"); if cap.contains(Cap::SQL_QUERY) { deps.push_str("rusqlite = { version = \"0.39\", features = [\"bundled\"] }\n"); } @@ -496,18 +502,28 @@ pub fn generate_cargo_toml(cap: Cap) -> String { /// Generate `src/main.rs` — the harness entry point. /// /// Reads the payload from env, calls `entry::{entry_name}` with the payload -/// routed according to `spec.payload_slot` and `shape`. +/// routed according to `spec.payload_slot` and `shape`. The probe shim +/// (Phase 06 / Phase 08) is spliced in at file scope so +/// `__nyx_install_crash_guard` is callable from `main` before the entry +/// invocation. fn generate_main_rs(spec: &HarnessSpec, shape: RustShape) -> String { let entry_fn = &spec.entry_name; let (pre_call, call_expr) = build_call(spec, entry_fn, shape); + let shim = probe_shim(); + let entry_label = spec.entry_name.replace('\\', "\\\\").replace('"', "\\\""); format!( r#"//! Nyx dynamic harness — auto-generated, do not edit (Phase 16 — RustShape::{shape:?}). mod entry; - +{shim} fn main() {{ let payload = nyx_payload(); let _ = &payload; + // Phase 08 sink-site signal handler: install AFTER payload decode so a + // crash in `nyx_payload` / `b64_decode` (harness setup) writes no Crash + // probe. A crash inside the entry call below fires the handler and + // writes a Crash probe to NYX_PROBE_PATH for `Oracle::SinkCrash`. + __nyx_install_crash_guard("{entry_label}"); {pre_call} {call_expr} }} @@ -809,6 +825,51 @@ mod tests { assert!(src.contains("entry::fuzz_target(payload.as_bytes())")); } + #[test] + fn emit_splices_probe_shim_and_installs_crash_guard() { + // Phase 16 follow-up: Rust emitter now splices probe_shim() into + // src/main.rs and installs the sink-site signal handler around the + // entry call. Mirrors the C / C++ splicing tests. + let spec = make_spec(PayloadSlot::Param(0)); + let h = emit(&spec).unwrap(); + assert!( + h.source.contains("__nyx_probe shim (Phase 06 — Track C.1"), + "probe_shim banner missing from generated src/main.rs", + ); + assert!( + h.source.contains("fn __nyx_install_crash_guard("), + "install_crash_guard definition missing from generated src/main.rs", + ); + assert!( + h.source.contains("__nyx_install_crash_guard(\"run\");"), + "install_crash_guard call site missing or wrong callee", + ); + let install_pos = h + .source + .find("__nyx_install_crash_guard(\"run\");") + .unwrap(); + let payload_pos = h.source.find("let payload = nyx_payload();").unwrap(); + let invoke_pos = h.source.find("entry::run(&payload);").unwrap(); + assert!( + payload_pos < install_pos && install_pos < invoke_pos, + "install_crash_guard ordering wrong: payload={payload_pos} install={install_pos} invoke={invoke_pos}", + ); + } + + #[test] + fn cargo_toml_always_pins_libc_for_probe_shim() { + // Phase 16 follow-up: the probe shim calls `libc::sigaction` so + // `libc` must be unconditionally pinned (independent of the + // expected_cap dep matrix). + for cap in [Cap::SQL_QUERY, Cap::CODE_EXEC, Cap::FILE_IO, Cap::SSRF] { + let cargo = generate_cargo_toml(cap); + assert!( + cargo.contains("libc = \"0.2\""), + "libc dep missing for cap={cap:?}", + ); + } + } + #[test] fn b64_decode_roundtrip() { // Test by compiling: actual b64_decode is in generated code.