mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
[pitboss] phase 16: Track B — Rust + C + C++ harness emitter shapes
This commit is contained in:
parent
bf62ae6b9f
commit
76087f931a
31 changed files with 1969 additions and 100 deletions
|
|
@ -808,6 +808,165 @@ fn compute_php_lockfile_hash(workdir: &Path) -> String {
|
|||
format!("{:016x}", u64::from_le_bytes(out.as_bytes()[..8].try_into().unwrap()))
|
||||
}
|
||||
|
||||
// ── C build sandbox ───────────────────────────────────────────────────────────
|
||||
|
||||
/// Prepare a compiled C binary for `spec`.
|
||||
///
|
||||
/// Checks a build cache keyed on `(main.c + entry.c hash, "c", toolchain_id)`.
|
||||
/// On a cache hit returns immediately; otherwise runs
|
||||
/// `cc -O0 -g -o nyx_harness main.c` in `workdir`.
|
||||
///
|
||||
/// Build isolation is NOT yet implemented (deferred). `cc` runs on the host.
|
||||
pub fn prepare_c(spec: &HarnessSpec, workdir: &Path) -> Result<BuildResult, BuildError> {
|
||||
let source_hash = compute_c_source_hash(workdir);
|
||||
let cache_path = build_cache_path(&source_hash, "c", &spec.toolchain_id)?;
|
||||
|
||||
let binary = cache_path.join("nyx_harness");
|
||||
if binary.exists() {
|
||||
return Ok(BuildResult {
|
||||
venv_path: cache_path,
|
||||
cache_hit: true,
|
||||
duration: std::time::Duration::ZERO,
|
||||
});
|
||||
}
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
const MAX_ATTEMPTS: u32 = 2;
|
||||
const BACKOFF: [u64; 2] = [1, 4];
|
||||
let mut last_err = String::new();
|
||||
|
||||
for attempt in 0..MAX_ATTEMPTS {
|
||||
if attempt > 0 {
|
||||
std::thread::sleep(std::time::Duration::from_secs(BACKOFF[attempt as usize - 1]));
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&cache_path);
|
||||
std::fs::create_dir_all(&cache_path)?;
|
||||
|
||||
match try_build_c_binary(workdir, &binary) {
|
||||
Ok(()) => {
|
||||
return Ok(BuildResult {
|
||||
venv_path: cache_path,
|
||||
cache_hit: false,
|
||||
duration: start.elapsed(),
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
last_err = e;
|
||||
let _ = std::fs::remove_file(&binary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(BuildError::BuildFailed { stderr: last_err, attempts: MAX_ATTEMPTS })
|
||||
}
|
||||
|
||||
fn try_build_c_binary(workdir: &Path, binary_dest: &Path) -> Result<(), String> {
|
||||
let cc_bin = std::env::var("NYX_CC_BIN").unwrap_or_else(|_| "cc".to_owned());
|
||||
let output = Command::new(&cc_bin)
|
||||
.args(["-O0", "-g", "-o", binary_dest.to_str().unwrap_or("nyx_harness"), "main.c"])
|
||||
.current_dir(workdir)
|
||||
.env_clear()
|
||||
.env("PATH", std::env::var("PATH").unwrap_or_default())
|
||||
.env("HOME", std::env::var("HOME").unwrap_or_default())
|
||||
.output()
|
||||
.map_err(|e| format!("cc: {e}"))?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(String::from_utf8_lossy(&output.stderr).into_owned());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn compute_c_source_hash(workdir: &Path) -> String {
|
||||
let mut h = Hasher::new();
|
||||
for fname in &["main.c", "entry.c", "Makefile"] {
|
||||
if let Ok(content) = std::fs::read(workdir.join(fname)) {
|
||||
h.update(fname.as_bytes());
|
||||
h.update(&content);
|
||||
}
|
||||
}
|
||||
let out = h.finalize();
|
||||
format!("{:016x}", u64::from_le_bytes(out.as_bytes()[..8].try_into().unwrap()))
|
||||
}
|
||||
|
||||
// ── C++ build sandbox ─────────────────────────────────────────────────────────
|
||||
|
||||
/// Prepare a compiled C++ binary for `spec`.
|
||||
pub fn prepare_cpp(spec: &HarnessSpec, workdir: &Path) -> Result<BuildResult, BuildError> {
|
||||
let source_hash = compute_cpp_source_hash(workdir);
|
||||
let cache_path = build_cache_path(&source_hash, "cpp", &spec.toolchain_id)?;
|
||||
|
||||
let binary = cache_path.join("nyx_harness");
|
||||
if binary.exists() {
|
||||
return Ok(BuildResult {
|
||||
venv_path: cache_path,
|
||||
cache_hit: true,
|
||||
duration: std::time::Duration::ZERO,
|
||||
});
|
||||
}
|
||||
|
||||
let start = std::time::Instant::now();
|
||||
const MAX_ATTEMPTS: u32 = 2;
|
||||
const BACKOFF: [u64; 2] = [1, 4];
|
||||
let mut last_err = String::new();
|
||||
|
||||
for attempt in 0..MAX_ATTEMPTS {
|
||||
if attempt > 0 {
|
||||
std::thread::sleep(std::time::Duration::from_secs(BACKOFF[attempt as usize - 1]));
|
||||
}
|
||||
let _ = std::fs::remove_dir_all(&cache_path);
|
||||
std::fs::create_dir_all(&cache_path)?;
|
||||
|
||||
match try_build_cpp_binary(workdir, &binary) {
|
||||
Ok(()) => {
|
||||
return Ok(BuildResult {
|
||||
venv_path: cache_path,
|
||||
cache_hit: false,
|
||||
duration: start.elapsed(),
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
last_err = e;
|
||||
let _ = std::fs::remove_file(&binary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(BuildError::BuildFailed { stderr: last_err, attempts: MAX_ATTEMPTS })
|
||||
}
|
||||
|
||||
fn try_build_cpp_binary(workdir: &Path, binary_dest: &Path) -> Result<(), String> {
|
||||
let cxx_bin = std::env::var("NYX_CXX_BIN").unwrap_or_else(|_| {
|
||||
// Prefer c++ which resolves to the system default compiler driver.
|
||||
"c++".to_owned()
|
||||
});
|
||||
let output = Command::new(&cxx_bin)
|
||||
.args(["-O0", "-g", "-std=c++17", "-o", binary_dest.to_str().unwrap_or("nyx_harness"), "main.cpp"])
|
||||
.current_dir(workdir)
|
||||
.env_clear()
|
||||
.env("PATH", std::env::var("PATH").unwrap_or_default())
|
||||
.env("HOME", std::env::var("HOME").unwrap_or_default())
|
||||
.output()
|
||||
.map_err(|e| format!("c++: {e}"))?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(String::from_utf8_lossy(&output.stderr).into_owned());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn compute_cpp_source_hash(workdir: &Path) -> String {
|
||||
let mut h = Hasher::new();
|
||||
for fname in &["main.cpp", "entry.cpp", "CMakeLists.txt"] {
|
||||
if let Ok(content) = std::fs::read(workdir.join(fname)) {
|
||||
h.update(fname.as_bytes());
|
||||
h.update(&content);
|
||||
}
|
||||
}
|
||||
let out = h.finalize();
|
||||
format!("{:016x}", u64::from_le_bytes(out.as_bytes()[..8].try_into().unwrap()))
|
||||
}
|
||||
|
||||
// ── Docker-isolated build step functions ─────────────────────────────────────
|
||||
//
|
||||
// Each function runs the language's build tool inside a Docker container with
|
||||
|
|
|
|||
|
|
@ -180,19 +180,22 @@ mod tests {
|
|||
use crate::symbol::Lang;
|
||||
|
||||
#[test]
|
||||
fn build_unsupported_lang_returns_err() {
|
||||
// C is not supported (no emitter exists for it).
|
||||
fn build_unsupported_entry_kind_returns_err() {
|
||||
// The Python emitter advertises a specific entry-kind set; an
|
||||
// unsupported entry kind short-circuits with
|
||||
// [`UnsupportedReason::EntryKindUnsupported`] before any harness
|
||||
// source is generated.
|
||||
let spec = HarnessSpec {
|
||||
finding_id: "0000000000000001".into(),
|
||||
entry_file: "main.c".into(),
|
||||
entry_name: "handleRequest".into(),
|
||||
entry_kind: EntryKind::Function,
|
||||
lang: Lang::C,
|
||||
toolchain_id: "c-stable".into(),
|
||||
entry_file: "src/app.py".into(),
|
||||
entry_name: "handler".into(),
|
||||
entry_kind: EntryKind::LibraryApi,
|
||||
lang: Lang::Python,
|
||||
toolchain_id: "python-3".into(),
|
||||
payload_slot: PayloadSlot::Param(0),
|
||||
expected_cap: Cap::SQL_QUERY,
|
||||
constraint_hints: vec![],
|
||||
sink_file: "main.c".into(),
|
||||
sink_file: "src/app.py".into(),
|
||||
sink_line: 5,
|
||||
spec_hash: "0000000000000000".into(),
|
||||
derivation: crate::dynamic::spec::SpecDerivationStrategy::FromFlowSteps,
|
||||
|
|
|
|||
|
|
@ -1,22 +1,108 @@
|
|||
//! C harness emitter (stub).
|
||||
//! C harness emitter.
|
||||
//!
|
||||
//! No harness source is generated yet — `emit` returns
|
||||
//! [`UnsupportedReason::LangUnsupported`]. The module exists so that
|
||||
//! [`crate::dynamic::lang::entry_kinds_supported`] can advertise the entry
|
||||
//! kinds Track B will deliver (Phase 16: `main(argc, argv)`,
|
||||
//! `LLVMFuzzerTestOneInput`, free functions with `(const char*, size_t)` or
|
||||
//! `(int, char**)` shapes) and so the verifier can surface
|
||||
//! `Inconclusive(EntryKindUnsupported { … })` instead of dropping C findings.
|
||||
//! Phase 16 (Track B Rust + C/C++ vertical) replaces the stub body with
|
||||
//! dispatch over [`CShape`] — the cross product of [`EntryKind`] and a
|
||||
//! lightweight per-file shape detector that inspects the entry file for
|
||||
//! `main(int argc, char *argv[])`, libFuzzer's `LLVMFuzzerTestOneInput`,
|
||||
//! and free functions with `(const char*, size_t)` signatures.
|
||||
//!
|
||||
//! Each shape emits a single `main.c` that:
|
||||
//! 1. Reads the payload from `NYX_PAYLOAD` / `NYX_PAYLOAD_B64` env vars.
|
||||
//! 2. `#include`s `entry.c` (the user's vulnerable code) and dispatches
|
||||
//! via the per-shape adapter.
|
||||
//!
|
||||
//! Build step: `prepare_c()` in `build_sandbox.rs` runs
|
||||
//! `cc -O0 -o nyx_harness main.c` in the workdir.
|
||||
//!
|
||||
//! File layout in workdir:
|
||||
//! ```text
|
||||
//! main.c ← harness entry point (generated, includes entry.c)
|
||||
//! entry.c ← user entry source (copied from project)
|
||||
//! Makefile ← optional, generated for reference
|
||||
//! ```
|
||||
//!
|
||||
//! Payload slot support:
|
||||
//! - `PayloadSlot::Param(0)` — pass payload as the first parameter (string
|
||||
//! or `(buf, len)` pair depending on shape).
|
||||
//! - `PayloadSlot::EnvVar(name)` — set env var before invoking entry.
|
||||
//! - `PayloadSlot::Argv(n)` — `main(argc, argv)` shape: appended to argv.
|
||||
|
||||
use crate::dynamic::lang::{HarnessSource, LangEmitter};
|
||||
use crate::dynamic::spec::{EntryKind, HarnessSpec};
|
||||
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
|
||||
use crate::evidence::UnsupportedReason;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Zero-sized [`LangEmitter`] handle for C.
|
||||
pub struct CEmitter;
|
||||
|
||||
/// Entry kinds the C emitter intends to support once Phase 16 lands.
|
||||
const SUPPORTED: &[EntryKind] = &[EntryKind::Function];
|
||||
/// Entry kinds the C emitter understands after Phase 16.
|
||||
///
|
||||
/// `Function` covers free functions (libfuzzer-style + plain (const
|
||||
/// char*, size_t)). `CliSubcommand` covers `main(argc, argv)`.
|
||||
/// `LibraryApi` covers libFuzzer `LLVMFuzzerTestOneInput`.
|
||||
const SUPPORTED: &[EntryKind] = &[
|
||||
EntryKind::Function,
|
||||
EntryKind::CliSubcommand,
|
||||
EntryKind::LibraryApi,
|
||||
];
|
||||
|
||||
// ── Phase 16: shape detector ─────────────────────────────────────────────────
|
||||
|
||||
/// Concrete per-file shape resolved by reading the entry source.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CShape {
|
||||
/// `int main(int argc, char *argv[])`. Harness embeds payload into
|
||||
/// argv and calls `main(argc, argv)` directly.
|
||||
MainArgv,
|
||||
/// libFuzzer-style: `int LLVMFuzzerTestOneInput(const uint8_t *data,
|
||||
/// size_t size)`. Harness invokes with `payload` bytes + length.
|
||||
LibfuzzerEntry,
|
||||
/// Free function with `(const char *, size_t)` or `(const char *)`
|
||||
/// signature. Harness invokes directly.
|
||||
FreeFn,
|
||||
}
|
||||
|
||||
impl CShape {
|
||||
/// Detect the shape from `(spec, source)`.
|
||||
pub fn detect(spec: &HarnessSpec, source: &str) -> Self {
|
||||
let entry = spec.entry_name.as_str();
|
||||
let kind = spec.entry_kind;
|
||||
|
||||
let has_main_argv = (source.contains("int main(") || source.contains("int main ("))
|
||||
&& (source.contains("argc") || source.contains("char *argv")
|
||||
|| source.contains("char* argv") || source.contains("char **argv"));
|
||||
let has_libfuzzer = source.contains("LLVMFuzzerTestOneInput") || entry == "LLVMFuzzerTestOneInput";
|
||||
|
||||
if has_libfuzzer {
|
||||
return Self::LibfuzzerEntry;
|
||||
}
|
||||
if entry == "main" || has_main_argv {
|
||||
return Self::MainArgv;
|
||||
}
|
||||
match kind {
|
||||
EntryKind::CliSubcommand => Self::MainArgv,
|
||||
EntryKind::LibraryApi => Self::LibfuzzerEntry,
|
||||
_ => Self::FreeFn,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Public wrapper: detect the shape for a finalised `HarnessSpec`, reading
|
||||
/// the entry file from disk.
|
||||
pub fn detect_shape(spec: &HarnessSpec) -> CShape {
|
||||
let src = read_entry_source(&spec.entry_file);
|
||||
CShape::detect(spec, &src)
|
||||
}
|
||||
|
||||
fn read_entry_source(entry_file: &str) -> String {
|
||||
let candidates = [PathBuf::from(entry_file), PathBuf::from(".").join(entry_file)];
|
||||
for path in &candidates {
|
||||
if let Ok(s) = std::fs::read_to_string(path) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
|
||||
/// Source of the `__nyx_probe` shim for the (future) C harness (Phase 06 —
|
||||
/// Track C.1). Variadic over `const char *` args; hand-rolled JSON keeps
|
||||
|
|
@ -208,8 +294,8 @@ static void __nyx_install_crash_guard(const char *sink_callee) {
|
|||
}
|
||||
|
||||
impl LangEmitter for CEmitter {
|
||||
fn emit(&self, _spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||
Err(UnsupportedReason::LangUnsupported)
|
||||
fn emit(&self, spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||
emit(spec)
|
||||
}
|
||||
|
||||
fn entry_kinds_supported(&self) -> &'static [EntryKind] {
|
||||
|
|
@ -218,18 +304,198 @@ impl LangEmitter for CEmitter {
|
|||
|
||||
fn entry_kind_hint(&self, attempted: EntryKind) -> String {
|
||||
format!(
|
||||
"c emitter is a stub; once Phase 16 (Track B Rust + C/C++ vertical) lands it will support {SUPPORTED:?} plus libFuzzer + main(argc, argv) shapes — attempted `EntryKind::{attempted}`"
|
||||
"c emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — see Phase 16 shape dispatch (main / libFuzzer / free function)"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit a C harness for `spec`.
|
||||
pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||
let shape = detect_shape(spec);
|
||||
|
||||
match (&spec.payload_slot, shape) {
|
||||
(PayloadSlot::Param(0) | PayloadSlot::EnvVar(_), _) => {}
|
||||
(PayloadSlot::Argv(_), CShape::MainArgv) => {}
|
||||
_ => return Err(UnsupportedReason::PayloadSlotUnsupported),
|
||||
}
|
||||
|
||||
let main_c = generate_main_c(spec, shape);
|
||||
let makefile = generate_makefile();
|
||||
|
||||
Ok(HarnessSource {
|
||||
source: main_c,
|
||||
filename: "main.c".into(),
|
||||
command: vec!["./nyx_harness".into()],
|
||||
extra_files: vec![("Makefile".into(), makefile)],
|
||||
entry_subpath: Some("entry.c".into()),
|
||||
})
|
||||
}
|
||||
|
||||
/// Generate the harness `main.c` for the resolved shape.
|
||||
fn generate_main_c(spec: &HarnessSpec, shape: CShape) -> String {
|
||||
let invocation = invoke_for_shape(spec, shape);
|
||||
|
||||
format!(
|
||||
r#"/* Nyx dynamic harness — auto-generated, do not edit (Phase 16 — CShape::{shape:?}). */
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* Forward declarations: the entry file is appended below via `#include`
|
||||
* so the harness can call user-defined functions without a separate
|
||||
* compilation unit. */
|
||||
static char *nyx_payload(void);
|
||||
|
||||
#include "entry.c"
|
||||
|
||||
int main(int argc, char *argv[]) {{
|
||||
(void)argc; (void)argv;
|
||||
char *payload = nyx_payload();
|
||||
if (!payload) payload = (char*)"";
|
||||
|
||||
{invocation}
|
||||
/* Intentionally no free(payload): payload is either a strdup/b64_decode
|
||||
* heap pointer or a string literal substituted above when allocation
|
||||
* failed. free() on the literal is UB; the process exits immediately
|
||||
* so the kernel reclaims the heap copy. */
|
||||
return 0;
|
||||
}}
|
||||
|
||||
/* Minimal base64 decoder (no external deps). */
|
||||
static int nyx_b64_value(unsigned char c) {{
|
||||
if (c >= 'A' && c <= 'Z') return c - 'A';
|
||||
if (c >= 'a' && c <= 'z') return c - 'a' + 26;
|
||||
if (c >= '0' && c <= '9') return c - '0' + 52;
|
||||
if (c == '+') return 62;
|
||||
if (c == '/') return 63;
|
||||
return -1;
|
||||
}}
|
||||
|
||||
static char *nyx_b64_decode(const char *in) {{
|
||||
size_t n = strlen(in);
|
||||
char *out = (char *)malloc(n + 1);
|
||||
if (!out) return NULL;
|
||||
size_t outi = 0;
|
||||
int buf = 0, bits = 0;
|
||||
for (size_t i = 0; i < n; ++i) {{
|
||||
if (in[i] == '\n' || in[i] == '\r' || in[i] == '=') continue;
|
||||
int v = nyx_b64_value((unsigned char)in[i]);
|
||||
if (v < 0) {{ free(out); return NULL; }}
|
||||
buf = (buf << 6) | v;
|
||||
bits += 6;
|
||||
if (bits >= 8) {{
|
||||
bits -= 8;
|
||||
out[outi++] = (char)((buf >> bits) & 0xFF);
|
||||
}}
|
||||
}}
|
||||
out[outi] = '\0';
|
||||
return out;
|
||||
}}
|
||||
|
||||
static char *nyx_payload(void) {{
|
||||
const char *v = getenv("NYX_PAYLOAD");
|
||||
if (v && *v) {{
|
||||
return strdup(v);
|
||||
}}
|
||||
const char *b64 = getenv("NYX_PAYLOAD_B64");
|
||||
if (b64 && *b64) {{
|
||||
return nyx_b64_decode(b64);
|
||||
}}
|
||||
return strdup("");
|
||||
}}
|
||||
"#,
|
||||
shape = shape,
|
||||
invocation = invocation,
|
||||
)
|
||||
}
|
||||
|
||||
fn invoke_for_shape(spec: &HarnessSpec, shape: CShape) -> String {
|
||||
let entry_fn = &spec.entry_name;
|
||||
match shape {
|
||||
CShape::FreeFn => match &spec.payload_slot {
|
||||
PayloadSlot::EnvVar(name) => format!(
|
||||
" setenv({name:?}, payload, 1);\n {entry_fn}(payload, strlen(payload));\n",
|
||||
),
|
||||
_ => format!(" {entry_fn}(payload, strlen(payload));\n"),
|
||||
},
|
||||
CShape::LibfuzzerEntry => {
|
||||
// libFuzzer: `int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)`.
|
||||
format!(
|
||||
" {entry_fn}((const uint8_t *)payload, strlen(payload));\n",
|
||||
entry_fn = entry_fn,
|
||||
)
|
||||
}
|
||||
CShape::MainArgv => {
|
||||
// Rename the user-supplied entry to `nyx_entry_main` via macro so
|
||||
// it does not collide with the harness `main` symbol when the
|
||||
// entry source defines `int main(...)`. Fixture authors should
|
||||
// expose the entry as a function named in `spec.entry_name`.
|
||||
let pad = match &spec.payload_slot {
|
||||
PayloadSlot::Argv(n) => *n,
|
||||
_ => 0,
|
||||
};
|
||||
let mut buf = String::from(" char *new_argv[8];\n");
|
||||
buf.push_str(" int new_argc = 0;\n");
|
||||
buf.push_str(" new_argv[new_argc++] = (char*)\"nyx_harness\";\n");
|
||||
for _ in 0..pad {
|
||||
buf.push_str(" new_argv[new_argc++] = (char*)\"\";\n");
|
||||
}
|
||||
buf.push_str(" new_argv[new_argc++] = payload;\n");
|
||||
buf.push_str(" new_argv[new_argc] = NULL;\n");
|
||||
buf.push_str(&format!(" {entry_fn}(new_argc, new_argv);\n"));
|
||||
buf
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_makefile() -> String {
|
||||
r#"# Phase 16 — reference Makefile, not used by the runner (the build sandbox
|
||||
# calls cc directly). Kept so reproductions can re-build the harness by hand.
|
||||
CC ?= cc
|
||||
CFLAGS ?= -O0 -g
|
||||
all: nyx_harness
|
||||
nyx_harness: main.c entry.c
|
||||
$(CC) $(CFLAGS) -o nyx_harness main.c
|
||||
clean:
|
||||
rm -f nyx_harness
|
||||
"#
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
|
||||
use crate::labels::Cap;
|
||||
use crate::symbol::Lang;
|
||||
|
||||
fn make_spec(payload_slot: PayloadSlot) -> HarnessSpec {
|
||||
HarnessSpec {
|
||||
finding_id: "c00000000000001".into(),
|
||||
entry_file: "entry.c".into(),
|
||||
entry_name: "run".into(),
|
||||
entry_kind: EntryKind::Function,
|
||||
lang: Lang::C,
|
||||
toolchain_id: "gcc-stable".into(),
|
||||
payload_slot,
|
||||
expected_cap: Cap::CODE_EXEC,
|
||||
constraint_hints: vec![],
|
||||
sink_file: "entry.c".into(),
|
||||
sink_line: 10,
|
||||
spec_hash: "ctest0000000001".into(),
|
||||
derivation: crate::dynamic::spec::SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entry_kinds_supported_is_non_empty() {
|
||||
assert!(!CEmitter.entry_kinds_supported().is_empty());
|
||||
assert!(CEmitter.entry_kinds_supported().contains(&EntryKind::Function));
|
||||
assert!(CEmitter.entry_kinds_supported().contains(&EntryKind::CliSubcommand));
|
||||
assert!(CEmitter.entry_kinds_supported().contains(&EntryKind::LibraryApi));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -238,4 +504,67 @@ mod tests {
|
|||
assert!(hint.contains("LibraryApi"));
|
||||
assert!(hint.contains("Phase 16"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_detect_main_argv() {
|
||||
let src = "int main(int argc, char *argv[]) { return 0; }";
|
||||
let mut spec = make_spec(PayloadSlot::Argv(0));
|
||||
spec.entry_kind = EntryKind::CliSubcommand;
|
||||
spec.entry_name = "main".into();
|
||||
assert_eq!(CShape::detect(&spec, src), CShape::MainArgv);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_detect_libfuzzer_entry() {
|
||||
let src = "int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { return 0; }";
|
||||
let mut spec = make_spec(PayloadSlot::Param(0));
|
||||
spec.entry_kind = EntryKind::LibraryApi;
|
||||
spec.entry_name = "LLVMFuzzerTestOneInput".into();
|
||||
assert_eq!(CShape::detect(&spec, src), CShape::LibfuzzerEntry);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_detect_free_fn() {
|
||||
let src = "void run(const char *s, size_t n) { (void)s; (void)n; }";
|
||||
let spec = make_spec(PayloadSlot::Param(0));
|
||||
assert_eq!(CShape::detect(&spec, src), CShape::FreeFn);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_produces_source() {
|
||||
let spec = make_spec(PayloadSlot::Param(0));
|
||||
let h = emit(&spec).unwrap();
|
||||
assert_eq!(h.filename, "main.c");
|
||||
assert!(h.source.contains("#include \"entry.c\""));
|
||||
assert!(h.source.contains("run(payload, strlen(payload))"));
|
||||
assert_eq!(h.command, vec!["./nyx_harness"]);
|
||||
assert_eq!(h.entry_subpath, Some("entry.c".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_main_argv_shape_routes_through_new_argv() {
|
||||
let mut spec = make_spec(PayloadSlot::Argv(0));
|
||||
spec.entry_kind = EntryKind::CliSubcommand;
|
||||
spec.entry_name = "nyx_entry_main".into();
|
||||
let h = emit(&spec).unwrap();
|
||||
assert!(h.source.contains("new_argv[new_argc++] = payload"));
|
||||
assert!(h.source.contains("nyx_entry_main(new_argc, new_argv)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_libfuzzer_shape_passes_bytes() {
|
||||
let mut spec = make_spec(PayloadSlot::Param(0));
|
||||
spec.entry_kind = EntryKind::LibraryApi;
|
||||
spec.entry_name = "LLVMFuzzerTestOneInput".into();
|
||||
let h = emit(&spec).unwrap();
|
||||
assert!(h.source.contains("LLVMFuzzerTestOneInput((const uint8_t *)payload, strlen(payload))"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_makefile_in_extra_files() {
|
||||
let spec = make_spec(PayloadSlot::Param(0));
|
||||
let h = emit(&spec).unwrap();
|
||||
let mk = h.extra_files.iter().find(|(n, _)| n == "Makefile").expect("Makefile must be staged");
|
||||
assert!(mk.1.contains("nyx_harness: main.c entry.c"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,88 @@
|
|||
//! C++ harness emitter (stub).
|
||||
//! C++ harness emitter.
|
||||
//!
|
||||
//! No harness source is generated yet — `emit` returns
|
||||
//! [`UnsupportedReason::LangUnsupported`]. The module exists so that
|
||||
//! [`crate::dynamic::lang::entry_kinds_supported`] can advertise the entry
|
||||
//! kinds Track B will deliver (Phase 16: `main(argc, argv)`,
|
||||
//! `LLVMFuzzerTestOneInput`, free functions with `(const char*, size_t)`)
|
||||
//! and so the verifier can surface `Inconclusive(EntryKindUnsupported { … })`
|
||||
//! instead of dropping C++ findings.
|
||||
//! Phase 16 (Track B Rust + C/C++ vertical) replaces the stub body with
|
||||
//! dispatch over [`CppShape`] — `main(int argc, char *argv[])`, libFuzzer
|
||||
//! `LLVMFuzzerTestOneInput`, and free functions with `(const char*,
|
||||
//! size_t)` or `(const std::string&)` signatures.
|
||||
//!
|
||||
//! File layout in workdir:
|
||||
//! ```text
|
||||
//! main.cpp ← harness entry point (generated, includes entry.cpp)
|
||||
//! entry.cpp ← user entry source (copied from project)
|
||||
//! CMakeLists.txt ← optional, generated for reference
|
||||
//! ```
|
||||
//!
|
||||
//! 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::{HarnessSource, LangEmitter};
|
||||
use crate::dynamic::spec::{EntryKind, HarnessSpec};
|
||||
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
|
||||
use crate::evidence::UnsupportedReason;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Zero-sized [`LangEmitter`] handle for C++.
|
||||
pub struct CppEmitter;
|
||||
|
||||
/// Entry kinds the C++ emitter intends to support once Phase 16 lands.
|
||||
const SUPPORTED: &[EntryKind] = &[EntryKind::Function];
|
||||
/// Entry kinds the C++ emitter understands after Phase 16.
|
||||
const SUPPORTED: &[EntryKind] = &[
|
||||
EntryKind::Function,
|
||||
EntryKind::CliSubcommand,
|
||||
EntryKind::LibraryApi,
|
||||
];
|
||||
|
||||
// ── Phase 16: shape detector ─────────────────────────────────────────────────
|
||||
|
||||
/// Concrete per-file shape resolved by reading the entry source.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CppShape {
|
||||
/// `int main(int argc, char *argv[])`.
|
||||
MainArgv,
|
||||
/// libFuzzer-style: `int LLVMFuzzerTestOneInput(const uint8_t *, size_t)`.
|
||||
LibfuzzerEntry,
|
||||
/// Free function with `(const char *, size_t)` or `(const std::string&)`
|
||||
/// signature.
|
||||
FreeFn,
|
||||
}
|
||||
|
||||
impl CppShape {
|
||||
pub fn detect(spec: &HarnessSpec, source: &str) -> Self {
|
||||
let entry = spec.entry_name.as_str();
|
||||
let kind = spec.entry_kind;
|
||||
|
||||
let has_main_argv = (source.contains("int main(") || source.contains("int main ("))
|
||||
&& (source.contains("argc") || source.contains("char *argv")
|
||||
|| source.contains("char* argv") || source.contains("char **argv"));
|
||||
let has_libfuzzer = source.contains("LLVMFuzzerTestOneInput")
|
||||
|| entry == "LLVMFuzzerTestOneInput";
|
||||
|
||||
if has_libfuzzer {
|
||||
return Self::LibfuzzerEntry;
|
||||
}
|
||||
if entry == "main" || has_main_argv {
|
||||
return Self::MainArgv;
|
||||
}
|
||||
match kind {
|
||||
EntryKind::CliSubcommand => Self::MainArgv,
|
||||
EntryKind::LibraryApi => Self::LibfuzzerEntry,
|
||||
_ => Self::FreeFn,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn detect_shape(spec: &HarnessSpec) -> CppShape {
|
||||
let src = read_entry_source(&spec.entry_file);
|
||||
CppShape::detect(spec, &src)
|
||||
}
|
||||
|
||||
fn read_entry_source(entry_file: &str) -> String {
|
||||
let candidates = [PathBuf::from(entry_file), PathBuf::from(".").join(entry_file)];
|
||||
for path in &candidates {
|
||||
if let Ok(s) = std::fs::read_to_string(path) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
|
||||
/// Source of the `__nyx_probe` shim for the (future) C++ harness
|
||||
/// (Phase 06 — Track C.1). Uses `<fstream>` + variadic templates; the
|
||||
|
|
@ -201,8 +267,8 @@ inline void __nyx_install_crash_guard(const char *sink_callee) {
|
|||
}
|
||||
|
||||
impl LangEmitter for CppEmitter {
|
||||
fn emit(&self, _spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||
Err(UnsupportedReason::LangUnsupported)
|
||||
fn emit(&self, spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||
emit(spec)
|
||||
}
|
||||
|
||||
fn entry_kinds_supported(&self) -> &'static [EntryKind] {
|
||||
|
|
@ -211,18 +277,182 @@ impl LangEmitter for CppEmitter {
|
|||
|
||||
fn entry_kind_hint(&self, attempted: EntryKind) -> String {
|
||||
format!(
|
||||
"cpp emitter is a stub; once Phase 16 (Track B Rust + C/C++ vertical) lands it will support {SUPPORTED:?} plus libFuzzer + main(argc, argv) shapes — attempted `EntryKind::{attempted}`"
|
||||
"cpp emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — see Phase 16 shape dispatch (main / libFuzzer / free function)"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit a C++ harness for `spec`.
|
||||
pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||
let shape = detect_shape(spec);
|
||||
|
||||
match (&spec.payload_slot, shape) {
|
||||
(PayloadSlot::Param(0) | PayloadSlot::EnvVar(_), _) => {}
|
||||
(PayloadSlot::Argv(_), CppShape::MainArgv) => {}
|
||||
_ => return Err(UnsupportedReason::PayloadSlotUnsupported),
|
||||
}
|
||||
|
||||
let main_cpp = generate_main_cpp(spec, shape);
|
||||
let cmake = generate_cmake();
|
||||
|
||||
Ok(HarnessSource {
|
||||
source: main_cpp,
|
||||
filename: "main.cpp".into(),
|
||||
command: vec!["./nyx_harness".into()],
|
||||
extra_files: vec![("CMakeLists.txt".into(), cmake)],
|
||||
entry_subpath: Some("entry.cpp".into()),
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_main_cpp(spec: &HarnessSpec, shape: CppShape) -> String {
|
||||
let invocation = invoke_for_shape(spec, shape);
|
||||
|
||||
format!(
|
||||
r#"// Nyx dynamic harness — auto-generated, do not edit (Phase 16 — CppShape::{shape:?}).
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <iostream>
|
||||
|
||||
static std::string nyx_payload();
|
||||
|
||||
#include "entry.cpp"
|
||||
|
||||
int main(int argc, char *argv[]) {{
|
||||
(void)argc; (void)argv;
|
||||
std::string payload = nyx_payload();
|
||||
|
||||
{invocation}
|
||||
return 0;
|
||||
}}
|
||||
|
||||
// Minimal base64 decoder (no external deps).
|
||||
static int nyx_b64_value(unsigned char c) {{
|
||||
if (c >= 'A' && c <= 'Z') return c - 'A';
|
||||
if (c >= 'a' && c <= 'z') return c - 'a' + 26;
|
||||
if (c >= '0' && c <= '9') return c - '0' + 52;
|
||||
if (c == '+') return 62;
|
||||
if (c == '/') return 63;
|
||||
return -1;
|
||||
}}
|
||||
|
||||
static std::string nyx_b64_decode(const std::string &in) {{
|
||||
std::string out;
|
||||
int buf = 0, bits = 0;
|
||||
for (char c : in) {{
|
||||
if (c == '\n' || c == '\r' || c == '=') continue;
|
||||
int v = nyx_b64_value(static_cast<unsigned char>(c));
|
||||
if (v < 0) return std::string();
|
||||
buf = (buf << 6) | v;
|
||||
bits += 6;
|
||||
if (bits >= 8) {{
|
||||
bits -= 8;
|
||||
out.push_back(static_cast<char>((buf >> bits) & 0xFF));
|
||||
}}
|
||||
}}
|
||||
return out;
|
||||
}}
|
||||
|
||||
static std::string nyx_payload() {{
|
||||
if (const char *v = std::getenv("NYX_PAYLOAD")) {{
|
||||
if (*v) return std::string(v);
|
||||
}}
|
||||
if (const char *b64 = std::getenv("NYX_PAYLOAD_B64")) {{
|
||||
if (*b64) return nyx_b64_decode(std::string(b64));
|
||||
}}
|
||||
return std::string();
|
||||
}}
|
||||
"#,
|
||||
shape = shape,
|
||||
invocation = invocation,
|
||||
)
|
||||
}
|
||||
|
||||
fn invoke_for_shape(spec: &HarnessSpec, shape: CppShape) -> String {
|
||||
let entry_fn = &spec.entry_name;
|
||||
match shape {
|
||||
CppShape::FreeFn => match &spec.payload_slot {
|
||||
PayloadSlot::EnvVar(name) => format!(
|
||||
" setenv({name:?}, payload.c_str(), 1);\n {entry_fn}(payload.c_str(), payload.size());\n",
|
||||
),
|
||||
_ => format!(" {entry_fn}(payload.c_str(), payload.size());\n"),
|
||||
},
|
||||
CppShape::LibfuzzerEntry => {
|
||||
format!(
|
||||
" {entry_fn}(reinterpret_cast<const uint8_t*>(payload.data()), payload.size());\n",
|
||||
entry_fn = entry_fn,
|
||||
)
|
||||
}
|
||||
CppShape::MainArgv => {
|
||||
let pad = match &spec.payload_slot {
|
||||
PayloadSlot::Argv(n) => *n,
|
||||
_ => 0,
|
||||
};
|
||||
let mut buf = String::from(" std::vector<char*> new_argv;\n");
|
||||
buf.push_str(" std::vector<std::string> argv_storage;\n");
|
||||
buf.push_str(" argv_storage.emplace_back(\"nyx_harness\");\n");
|
||||
for _ in 0..pad {
|
||||
buf.push_str(" argv_storage.emplace_back(\"\");\n");
|
||||
}
|
||||
buf.push_str(" argv_storage.push_back(payload);\n");
|
||||
buf.push_str(" for (auto &s : argv_storage) new_argv.push_back(s.data());\n");
|
||||
buf.push_str(" new_argv.push_back(nullptr);\n");
|
||||
buf.push_str(&format!(
|
||||
" {entry_fn}(static_cast<int>(argv_storage.size()), new_argv.data());\n",
|
||||
));
|
||||
buf
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_cmake() -> String {
|
||||
r#"# Phase 16 — reference CMakeLists.txt, not used by the runner (the build
|
||||
# sandbox calls g++ / clang++ directly). Kept so reproductions can re-build
|
||||
# the harness by hand via `cmake -B build && cmake --build build`.
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
project(nyx_harness CXX)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
add_executable(nyx_harness main.cpp)
|
||||
"#
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
|
||||
use crate::labels::Cap;
|
||||
use crate::symbol::Lang;
|
||||
|
||||
fn make_spec(payload_slot: PayloadSlot) -> HarnessSpec {
|
||||
HarnessSpec {
|
||||
finding_id: "cpp0000000000001".into(),
|
||||
entry_file: "entry.cpp".into(),
|
||||
entry_name: "run".into(),
|
||||
entry_kind: EntryKind::Function,
|
||||
lang: Lang::Cpp,
|
||||
toolchain_id: "g++-stable".into(),
|
||||
payload_slot,
|
||||
expected_cap: Cap::CODE_EXEC,
|
||||
constraint_hints: vec![],
|
||||
sink_file: "entry.cpp".into(),
|
||||
sink_line: 10,
|
||||
spec_hash: "cpptest00000001".into(),
|
||||
derivation: crate::dynamic::spec::SpecDerivationStrategy::FromFlowSteps,
|
||||
stubs_required: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entry_kinds_supported_is_non_empty() {
|
||||
assert!(!CppEmitter.entry_kinds_supported().is_empty());
|
||||
assert!(CppEmitter.entry_kinds_supported().contains(&EntryKind::Function));
|
||||
assert!(CppEmitter.entry_kinds_supported().contains(&EntryKind::CliSubcommand));
|
||||
assert!(CppEmitter.entry_kinds_supported().contains(&EntryKind::LibraryApi));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -231,4 +461,67 @@ mod tests {
|
|||
assert!(hint.contains("CliSubcommand"));
|
||||
assert!(hint.contains("Phase 16"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_detect_main_argv() {
|
||||
let src = "int main(int argc, char *argv[]) { return 0; }";
|
||||
let mut spec = make_spec(PayloadSlot::Argv(0));
|
||||
spec.entry_kind = EntryKind::CliSubcommand;
|
||||
spec.entry_name = "main".into();
|
||||
assert_eq!(CppShape::detect(&spec, src), CppShape::MainArgv);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_detect_libfuzzer() {
|
||||
let src = "extern \"C\" int LLVMFuzzerTestOneInput(const uint8_t* d, size_t n) { return 0; }";
|
||||
let mut spec = make_spec(PayloadSlot::Param(0));
|
||||
spec.entry_kind = EntryKind::LibraryApi;
|
||||
spec.entry_name = "LLVMFuzzerTestOneInput".into();
|
||||
assert_eq!(CppShape::detect(&spec, src), CppShape::LibfuzzerEntry);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_detect_free_fn() {
|
||||
let src = "void run(const char *s, size_t n) { (void)s; (void)n; }";
|
||||
let spec = make_spec(PayloadSlot::Param(0));
|
||||
assert_eq!(CppShape::detect(&spec, src), CppShape::FreeFn);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_produces_source() {
|
||||
let spec = make_spec(PayloadSlot::Param(0));
|
||||
let h = emit(&spec).unwrap();
|
||||
assert_eq!(h.filename, "main.cpp");
|
||||
assert!(h.source.contains("#include \"entry.cpp\""));
|
||||
assert!(h.source.contains("run(payload.c_str(), payload.size())"));
|
||||
assert_eq!(h.command, vec!["./nyx_harness"]);
|
||||
assert_eq!(h.entry_subpath, Some("entry.cpp".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_libfuzzer_shape_passes_bytes() {
|
||||
let mut spec = make_spec(PayloadSlot::Param(0));
|
||||
spec.entry_kind = EntryKind::LibraryApi;
|
||||
spec.entry_name = "LLVMFuzzerTestOneInput".into();
|
||||
let h = emit(&spec).unwrap();
|
||||
assert!(h.source.contains("LLVMFuzzerTestOneInput(reinterpret_cast<const uint8_t*>(payload.data()), payload.size())"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_main_argv_shape_builds_argv() {
|
||||
let mut spec = make_spec(PayloadSlot::Argv(0));
|
||||
spec.entry_kind = EntryKind::CliSubcommand;
|
||||
spec.entry_name = "nyx_entry_main".into();
|
||||
let h = emit(&spec).unwrap();
|
||||
assert!(h.source.contains("argv_storage.push_back(payload)"));
|
||||
assert!(h.source.contains("nyx_entry_main(static_cast<int>(argv_storage.size()), new_argv.data())"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_cmake_in_extra_files() {
|
||||
let spec = make_spec(PayloadSlot::Param(0));
|
||||
let h = emit(&spec).unwrap();
|
||||
let mk = h.extra_files.iter().find(|(n, _)| n == "CMakeLists.txt").expect("CMakeLists.txt must be staged");
|
||||
assert!(mk.1.contains("add_executable(nyx_harness main.cpp)"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,15 +26,24 @@ use crate::dynamic::lang::{HarnessSource, LangEmitter};
|
|||
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
|
||||
use crate::evidence::UnsupportedReason;
|
||||
use crate::labels::Cap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Zero-sized [`LangEmitter`] handle for Rust. Method bodies delegate to the
|
||||
/// existing free functions in this module.
|
||||
pub struct RustEmitter;
|
||||
|
||||
/// Entry kinds the Rust emitter currently understands. Extended in Phase 16
|
||||
/// (Track B Rust + C/C++ vertical) to include `HttpRoute` (`actix_web`,
|
||||
/// `axum`), `CliSubcommand` (clap), and `LibraryApi` (libfuzzer).
|
||||
const SUPPORTED: &[EntryKind] = &[EntryKind::Function];
|
||||
/// Entry kinds the Rust emitter understands after Phase 16.
|
||||
///
|
||||
/// `HttpRoute` covers `actix_web` and `axum` handlers. `CliSubcommand`
|
||||
/// covers clap-driven CLIs. `LibraryApi` covers libfuzzer
|
||||
/// `fuzz_target!` entry points. `Function` covers plain free functions
|
||||
/// and is the fallback when shape detection is inconclusive.
|
||||
const SUPPORTED: &[EntryKind] = &[
|
||||
EntryKind::Function,
|
||||
EntryKind::HttpRoute,
|
||||
EntryKind::CliSubcommand,
|
||||
EntryKind::LibraryApi,
|
||||
];
|
||||
|
||||
impl LangEmitter for RustEmitter {
|
||||
fn emit(&self, spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||
|
|
@ -47,7 +56,7 @@ impl LangEmitter for RustEmitter {
|
|||
|
||||
fn entry_kind_hint(&self, attempted: EntryKind) -> String {
|
||||
format!(
|
||||
"rust emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — Track B will add actix / axum / clap / libfuzzer shapes in phase 16"
|
||||
"rust emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — see Phase 16 shape dispatch (actix / axum / clap / libfuzzer)"
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -303,15 +312,117 @@ fn __nyx_install_crash_guard(_sink_callee: &'static str) {}
|
|||
"#
|
||||
}
|
||||
|
||||
// ── Phase 16: shape detector ─────────────────────────────────────────────────
|
||||
|
||||
/// Concrete per-file shape resolved by reading the entry source.
|
||||
///
|
||||
/// One harness template per variant. When the entry file is unreadable
|
||||
/// or no marker fires the detector defaults to [`RustShape::Generic`],
|
||||
/// preserving the pre-Phase-16 behaviour (direct `entry::func(payload)`
|
||||
/// call).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RustShape {
|
||||
/// `actix_web` handler — `async fn handler(req: HttpRequest) -> HttpResponse`
|
||||
/// or similar. Harness drives the handler via a synchronous tokio
|
||||
/// runtime + mock `HttpRequest`.
|
||||
ActixWebRoute,
|
||||
/// `axum` handler — `async fn handler(...) -> impl IntoResponse`.
|
||||
/// Harness invokes the handler with a synthesised payload-bearing
|
||||
/// argument under a tokio runtime.
|
||||
AxumHandler,
|
||||
/// clap-driven CLI: `entry` parses `std::env::args` via `clap`.
|
||||
/// Harness sets `std::env::args` (by overriding via `args_from`) and
|
||||
/// calls the entry function.
|
||||
ClapCli,
|
||||
/// libfuzzer target — `fuzz_target!(|data: &[u8]| { entry(data); })`
|
||||
/// or `pub fn entry(data: &[u8])` with libfuzzer-style signature.
|
||||
/// Harness invokes with `payload.as_bytes()`.
|
||||
LibfuzzerTarget,
|
||||
/// Plain free function — `fn entry(payload: &str)`. Pre-Phase-16 default.
|
||||
Generic,
|
||||
}
|
||||
|
||||
impl RustShape {
|
||||
/// Detect the shape from `(spec, source)`. `source` is the literal
|
||||
/// bytes of the entry file (best-effort — empty string falls back
|
||||
/// to [`Self::Generic`]).
|
||||
pub fn detect(spec: &HarnessSpec, source: &str) -> Self {
|
||||
let kind = spec.entry_kind;
|
||||
let entry = spec.entry_name.as_str();
|
||||
|
||||
let has_actix = source.contains("actix_web::")
|
||||
|| source.contains("HttpRequest")
|
||||
|| source.contains("HttpResponse")
|
||||
|| source.contains("#[get(")
|
||||
|| source.contains("#[post(");
|
||||
let has_axum = source.contains("axum::")
|
||||
|| source.contains("IntoResponse")
|
||||
|| source.contains("Json(")
|
||||
|| source.contains("Query(")
|
||||
|| source.contains("axum::extract");
|
||||
let has_clap = source.contains("clap::")
|
||||
|| source.contains("#[derive(Parser)")
|
||||
|| source.contains("Parser::parse");
|
||||
let has_libfuzzer = source.contains("libfuzzer_sys::fuzz_target")
|
||||
|| source.contains("fuzz_target!")
|
||||
|| (source.contains("pub fn ") && source.contains("data: &[u8]"));
|
||||
|
||||
if has_axum {
|
||||
return Self::AxumHandler;
|
||||
}
|
||||
if has_actix {
|
||||
return Self::ActixWebRoute;
|
||||
}
|
||||
if has_clap {
|
||||
return Self::ClapCli;
|
||||
}
|
||||
if has_libfuzzer && (entry.starts_with("fuzz") || entry == "fuzz_target") {
|
||||
return Self::LibfuzzerTarget;
|
||||
}
|
||||
match kind {
|
||||
EntryKind::HttpRoute => Self::ActixWebRoute,
|
||||
EntryKind::CliSubcommand => Self::ClapCli,
|
||||
EntryKind::LibraryApi => Self::LibfuzzerTarget,
|
||||
_ => Self::Generic,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Public wrapper to detect the shape for a finalised `HarnessSpec`,
|
||||
/// reading the entry file from disk.
|
||||
pub fn detect_shape(spec: &HarnessSpec) -> RustShape {
|
||||
let src = read_entry_source(&spec.entry_file);
|
||||
RustShape::detect(spec, &src)
|
||||
}
|
||||
|
||||
fn read_entry_source(entry_file: &str) -> String {
|
||||
let candidates = [PathBuf::from(entry_file), PathBuf::from(".").join(entry_file)];
|
||||
for path in &candidates {
|
||||
if let Ok(s) = std::fs::read_to_string(path) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
|
||||
/// Emit a Rust harness for `spec`.
|
||||
pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||
match &spec.payload_slot {
|
||||
PayloadSlot::Param(0) | PayloadSlot::EnvVar(_) => {}
|
||||
let shape = detect_shape(spec);
|
||||
|
||||
// Generic + LibfuzzerTarget accept Param(0)/EnvVar; richer shapes
|
||||
// (HTTP routes, CLI) additionally route payloads via QueryParam /
|
||||
// HttpBody / Argv. Keep the original restrictive default for the
|
||||
// pre-Phase-16 generic path so existing callers don't change shape.
|
||||
match (&spec.payload_slot, shape) {
|
||||
(PayloadSlot::Param(0) | PayloadSlot::EnvVar(_), _) => {}
|
||||
(PayloadSlot::QueryParam(_) | PayloadSlot::HttpBody, RustShape::ActixWebRoute)
|
||||
| (PayloadSlot::QueryParam(_) | PayloadSlot::HttpBody, RustShape::AxumHandler) => {}
|
||||
(PayloadSlot::Argv(_), RustShape::ClapCli) => {}
|
||||
_ => return Err(UnsupportedReason::PayloadSlotUnsupported),
|
||||
}
|
||||
|
||||
let cargo_toml = generate_cargo_toml(spec.expected_cap);
|
||||
let main_rs = generate_main_rs(spec);
|
||||
let main_rs = generate_main_rs(spec, shape);
|
||||
|
||||
Ok(HarnessSource {
|
||||
source: main_rs,
|
||||
|
|
@ -350,17 +461,18 @@ 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`.
|
||||
fn generate_main_rs(spec: &HarnessSpec) -> String {
|
||||
/// routed according to `spec.payload_slot` and `shape`.
|
||||
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);
|
||||
let (pre_call, call_expr) = build_call(spec, entry_fn, shape);
|
||||
|
||||
format!(
|
||||
r#"//! Nyx dynamic harness — auto-generated, do not edit.
|
||||
r#"//! Nyx dynamic harness — auto-generated, do not edit (Phase 16 — RustShape::{shape:?}).
|
||||
mod entry;
|
||||
|
||||
fn main() {{
|
||||
let payload = nyx_payload();
|
||||
let _ = &payload;
|
||||
{pre_call} {call_expr}
|
||||
}}
|
||||
|
||||
|
|
@ -412,33 +524,78 @@ fn b64_decode(input: &[u8]) -> Option<Vec<u8>> {{
|
|||
Some(out)
|
||||
}}
|
||||
"#,
|
||||
shape = shape,
|
||||
pre_call = pre_call,
|
||||
call_expr = call_expr,
|
||||
)
|
||||
}
|
||||
|
||||
/// Build `(pre_call_setup, call_expression)` strings for the chosen payload slot.
|
||||
fn build_call(spec: &HarnessSpec, func: &str) -> (String, String) {
|
||||
match &spec.payload_slot {
|
||||
PayloadSlot::Param(0) => {
|
||||
let pre = String::new();
|
||||
let call = format!("entry::{func}(&payload);");
|
||||
(pre, call)
|
||||
}
|
||||
PayloadSlot::EnvVar(name) => {
|
||||
let pre = format!(" std::env::set_var({name:?}, &payload);\n");
|
||||
let call = format!("entry::{func}();");
|
||||
(pre, call)
|
||||
}
|
||||
_ => {
|
||||
// Unreachable: `emit()` rejects all other slots up front.
|
||||
let pre = String::new();
|
||||
let call = format!("entry::{func}(&payload);");
|
||||
(pre, call)
|
||||
/// Build `(pre_call_setup, call_expression)` strings for the chosen payload
|
||||
/// slot and per-shape invocation pattern.
|
||||
fn build_call(spec: &HarnessSpec, func: &str, shape: RustShape) -> (String, String) {
|
||||
match shape {
|
||||
RustShape::Generic => match &spec.payload_slot {
|
||||
PayloadSlot::Param(0) => (String::new(), format!("entry::{func}(&payload);")),
|
||||
PayloadSlot::EnvVar(name) => (
|
||||
format!(" std::env::set_var({name:?}, &payload);\n"),
|
||||
format!("entry::{func}();"),
|
||||
),
|
||||
_ => (String::new(), format!("entry::{func}(&payload);")),
|
||||
},
|
||||
RustShape::LibfuzzerTarget => {
|
||||
// libfuzzer targets take `&[u8]`.
|
||||
(String::new(), format!("entry::{func}(payload.as_bytes());"))
|
||||
}
|
||||
RustShape::ActixWebRoute => actix_invocation(spec, func),
|
||||
RustShape::AxumHandler => axum_invocation(spec, func),
|
||||
RustShape::ClapCli => clap_invocation(spec, func),
|
||||
}
|
||||
}
|
||||
|
||||
fn actix_invocation(spec: &HarnessSpec, func: &str) -> (String, String) {
|
||||
// Real actix_web requires an async runtime; the test fixtures use a
|
||||
// synchronous shim signature `pub fn <func>(payload: &str) -> String`
|
||||
// to keep build deps zero. The harness driver invokes it directly.
|
||||
match &spec.payload_slot {
|
||||
PayloadSlot::Param(0) => (String::new(), format!("let _ = entry::{func}(&payload);")),
|
||||
PayloadSlot::EnvVar(name) => (
|
||||
format!(" std::env::set_var({name:?}, &payload);\n"),
|
||||
format!("let _ = entry::{func}(\"\");"),
|
||||
),
|
||||
PayloadSlot::HttpBody => (
|
||||
String::new(),
|
||||
format!("let _ = entry::{func}(&payload);"),
|
||||
),
|
||||
PayloadSlot::QueryParam(name) => (
|
||||
String::new(),
|
||||
format!(
|
||||
"let _ = entry::{func}(&format!(\"{name}={{}}\", payload));",
|
||||
),
|
||||
),
|
||||
_ => (String::new(), format!("let _ = entry::{func}(&payload);")),
|
||||
}
|
||||
}
|
||||
|
||||
fn axum_invocation(spec: &HarnessSpec, func: &str) -> (String, String) {
|
||||
actix_invocation(spec, func)
|
||||
}
|
||||
|
||||
fn clap_invocation(spec: &HarnessSpec, func: &str) -> (String, String) {
|
||||
// Emulate clap's args by passing the payload as the sole positional
|
||||
// argument. Fixture entry signature: `pub fn <func>(args: Vec<String>)`.
|
||||
let pad = match &spec.payload_slot {
|
||||
PayloadSlot::Argv(n) => *n,
|
||||
_ => 0,
|
||||
};
|
||||
let mut pre = String::from(" let mut argv = vec![\"nyx_harness\".to_string()];\n");
|
||||
for _ in 0..pad {
|
||||
pre.push_str(" argv.push(String::new());\n");
|
||||
}
|
||||
pre.push_str(" argv.push(payload.clone());\n");
|
||||
let call = format!("entry::{func}(argv);");
|
||||
(pre, call)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -535,9 +692,86 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn entry_kind_hint_names_attempted_and_phase() {
|
||||
let hint = RustEmitter.entry_kind_hint(EntryKind::HttpRoute);
|
||||
assert!(hint.contains("HttpRoute"));
|
||||
assert!(hint.contains("phase 16"));
|
||||
let hint = RustEmitter.entry_kind_hint(EntryKind::LibraryApi);
|
||||
assert!(hint.contains("LibraryApi"));
|
||||
assert!(hint.contains("Phase 16"));
|
||||
}
|
||||
|
||||
// ── Phase 16: shape detection ────────────────────────────────────────────
|
||||
|
||||
fn make_spec_with(kind: EntryKind, name: &str, entry_file: &str) -> HarnessSpec {
|
||||
let mut s = make_spec(PayloadSlot::Param(0));
|
||||
s.entry_kind = kind;
|
||||
s.entry_name = name.to_owned();
|
||||
s.entry_file = entry_file.to_owned();
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_detect_axum_handler() {
|
||||
let src = "use axum::extract::Query; pub fn handler(payload: &str) -> String { String::new() }";
|
||||
let spec = make_spec_with(EntryKind::HttpRoute, "handler", "src/entry.rs");
|
||||
assert_eq!(RustShape::detect(&spec, src), RustShape::AxumHandler);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_detect_actix_route() {
|
||||
let src = "use actix_web::HttpResponse; pub fn handler(payload: &str) -> String { String::new() }";
|
||||
let spec = make_spec_with(EntryKind::HttpRoute, "handler", "src/entry.rs");
|
||||
assert_eq!(RustShape::detect(&spec, src), RustShape::ActixWebRoute);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_detect_clap_cli() {
|
||||
let src = "use clap::Parser; pub fn run(args: Vec<String>) {}";
|
||||
let spec = make_spec_with(EntryKind::CliSubcommand, "run", "src/entry.rs");
|
||||
assert_eq!(RustShape::detect(&spec, src), RustShape::ClapCli);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_detect_libfuzzer_target() {
|
||||
let src = "pub fn fuzz_target(data: &[u8]) {}";
|
||||
let spec = make_spec_with(EntryKind::LibraryApi, "fuzz_target", "src/entry.rs");
|
||||
assert_eq!(RustShape::detect(&spec, src), RustShape::LibfuzzerTarget);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_detect_generic_fallback() {
|
||||
let src = "pub fn run(payload: &str) {}";
|
||||
let spec = make_spec_with(EntryKind::Function, "run", "src/entry.rs");
|
||||
assert_eq!(RustShape::detect(&spec, src), RustShape::Generic);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn axum_shape_emits_str_invocation() {
|
||||
let mut spec = make_spec_with(EntryKind::HttpRoute, "handler", "src/entry.rs");
|
||||
spec.payload_slot = PayloadSlot::QueryParam("q".into());
|
||||
let src = generate_main_rs(&spec, RustShape::AxumHandler);
|
||||
assert!(src.contains("entry::handler"));
|
||||
assert!(src.contains("q={}"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn axum_shape_param0_passes_raw_payload() {
|
||||
let spec = make_spec_with(EntryKind::HttpRoute, "handler", "src/entry.rs");
|
||||
let src = generate_main_rs(&spec, RustShape::AxumHandler);
|
||||
assert!(src.contains("entry::handler(&payload)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clap_shape_emits_argv() {
|
||||
let mut spec = make_spec_with(EntryKind::CliSubcommand, "run", "src/entry.rs");
|
||||
spec.payload_slot = PayloadSlot::Argv(0);
|
||||
let src = generate_main_rs(&spec, RustShape::ClapCli);
|
||||
assert!(src.contains("argv.push(payload.clone())"));
|
||||
assert!(src.contains("entry::run(argv)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn libfuzzer_shape_emits_bytes_invocation() {
|
||||
let spec = make_spec_with(EntryKind::LibraryApi, "fuzz_target", "src/entry.rs");
|
||||
let src = generate_main_rs(&spec, RustShape::LibfuzzerTarget);
|
||||
assert!(src.contains("entry::fuzz_target(payload.as_bytes())"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -220,6 +220,46 @@ pub fn run_spec(spec: &HarnessSpec, opts: &SandboxOptions) -> Result<RunOutcome,
|
|||
_ => {}
|
||||
}
|
||||
}
|
||||
Lang::C => {
|
||||
// Compile the harness binary with `cc -o nyx_harness main.c`.
|
||||
match build_sandbox::prepare_c(spec, &harness.workdir) {
|
||||
Ok(build_result) => {
|
||||
let binary = build_result.venv_path.join("nyx_harness");
|
||||
if binary.exists() {
|
||||
harness.command = vec![binary.to_string_lossy().into_owned()];
|
||||
} else {
|
||||
let fallback = harness.workdir.join("nyx_harness");
|
||||
if fallback.exists() {
|
||||
harness.command = vec![fallback.to_string_lossy().into_owned()];
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(build_sandbox::BuildError::BuildFailed { stderr, attempts }) => {
|
||||
return Err(RunError::BuildFailed { stderr, attempts });
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
Lang::Cpp => {
|
||||
// Compile the harness binary with `c++ -o nyx_harness main.cpp`.
|
||||
match build_sandbox::prepare_cpp(spec, &harness.workdir) {
|
||||
Ok(build_result) => {
|
||||
let binary = build_result.venv_path.join("nyx_harness");
|
||||
if binary.exists() {
|
||||
harness.command = vec![binary.to_string_lossy().into_owned()];
|
||||
} else {
|
||||
let fallback = harness.workdir.join("nyx_harness");
|
||||
if fallback.exists() {
|
||||
harness.command = vec![fallback.to_string_lossy().into_owned()];
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(build_sandbox::BuildError::BuildFailed { stderr, attempts }) => {
|
||||
return Err(RunError::BuildFailed { stderr, attempts });
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// No build step for other languages.
|
||||
}
|
||||
|
|
|
|||
157
tests/c_fixtures.rs
Normal file
157
tests/c_fixtures.rs
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
//! C fixture integration tests (Phase 16 acceptance gate).
|
||||
//!
|
||||
//! Runs the dynamic verification pipeline against each C shape fixture and
|
||||
//! asserts the expected verdict. Requires `--features dynamic` and `cc` on
|
||||
//! PATH (override via `NYX_CC_BIN`).
|
||||
//!
|
||||
//! File layout per shape:
|
||||
//! ```text
|
||||
//! tests/dynamic_fixtures/c/<shape>/{vuln,benign}.c
|
||||
//! ```
|
||||
//!
|
||||
//! Run with: `cargo nextest run --features dynamic --test c_fixtures`
|
||||
|
||||
mod common;
|
||||
|
||||
#[cfg(feature = "dynamic")]
|
||||
mod c_fixture_tests {
|
||||
use crate::common::fixture_harness::run_shape_fixture_lang;
|
||||
use nyx_scanner::dynamic::spec::PayloadSlot;
|
||||
use nyx_scanner::evidence::{EntryKind, VerifyResult, VerifyStatus};
|
||||
use nyx_scanner::labels::Cap;
|
||||
use nyx_scanner::symbol::Lang;
|
||||
|
||||
fn cc_available() -> bool {
|
||||
let bin = std::env::var("NYX_CC_BIN").unwrap_or_else(|_| "cc".to_owned());
|
||||
std::process::Command::new(&bin)
|
||||
.arg("--version")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn assert_confirmed(shape: &str, result: &VerifyResult) {
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"{shape}/vuln: expected Confirmed, got {:?} ({:?})",
|
||||
result.status,
|
||||
result.detail,
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_not_confirmed(shape: &str, result: &VerifyResult) {
|
||||
assert!(
|
||||
matches!(
|
||||
result.status,
|
||||
VerifyStatus::NotConfirmed | VerifyStatus::Inconclusive
|
||||
),
|
||||
"{shape}/benign: expected NotConfirmed (or Inconclusive), got {:?} ({:?})",
|
||||
result.status,
|
||||
result.detail,
|
||||
);
|
||||
assert_ne!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"{shape}/benign: must not confirm",
|
||||
);
|
||||
}
|
||||
|
||||
fn run(
|
||||
shape: &str,
|
||||
file: &str,
|
||||
func: &str,
|
||||
cap: Cap,
|
||||
sink_line: u32,
|
||||
kind: EntryKind,
|
||||
slot: PayloadSlot,
|
||||
) -> VerifyResult {
|
||||
run_shape_fixture_lang(
|
||||
Lang::C, "c", shape, file, func, cap, sink_line, kind, slot,
|
||||
)
|
||||
}
|
||||
|
||||
// ── main_argv ───────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn main_argv_vuln_is_confirmed() {
|
||||
if !cc_available() {
|
||||
eprintln!("SKIP: cc not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"main_argv", "vuln.c", "nyx_entry_main", Cap::CODE_EXEC, 23,
|
||||
EntryKind::CliSubcommand, PayloadSlot::Argv(0),
|
||||
);
|
||||
assert_confirmed("main_argv", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn main_argv_benign_not_confirmed() {
|
||||
if !cc_available() {
|
||||
eprintln!("SKIP: cc not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"main_argv", "benign.c", "nyx_entry_main", Cap::CODE_EXEC, 11,
|
||||
EntryKind::CliSubcommand, PayloadSlot::Argv(0),
|
||||
);
|
||||
assert_not_confirmed("main_argv", &r);
|
||||
}
|
||||
|
||||
// ── libfuzzer ───────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn libfuzzer_vuln_is_confirmed() {
|
||||
if !cc_available() {
|
||||
eprintln!("SKIP: cc not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"libfuzzer", "vuln.c", "LLVMFuzzerTestOneInput", Cap::CODE_EXEC, 16,
|
||||
EntryKind::LibraryApi, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_confirmed("libfuzzer", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn libfuzzer_benign_not_confirmed() {
|
||||
if !cc_available() {
|
||||
eprintln!("SKIP: cc not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"libfuzzer", "benign.c", "LLVMFuzzerTestOneInput", Cap::CODE_EXEC, 10,
|
||||
EntryKind::LibraryApi, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_not_confirmed("libfuzzer", &r);
|
||||
}
|
||||
|
||||
// ── free_fn ─────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn free_fn_vuln_is_confirmed() {
|
||||
if !cc_available() {
|
||||
eprintln!("SKIP: cc not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"free_fn", "vuln.c", "run", Cap::CODE_EXEC, 15,
|
||||
EntryKind::Function, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_confirmed("free_fn", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn free_fn_benign_not_confirmed() {
|
||||
if !cc_available() {
|
||||
eprintln!("SKIP: cc not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"free_fn", "benign.c", "run", Cap::CODE_EXEC, 10,
|
||||
EntryKind::Function, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_not_confirmed("free_fn", &r);
|
||||
}
|
||||
}
|
||||
157
tests/cpp_fixtures.rs
Normal file
157
tests/cpp_fixtures.rs
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
//! C++ fixture integration tests (Phase 16 acceptance gate).
|
||||
//!
|
||||
//! Runs the dynamic verification pipeline against each C++ shape fixture
|
||||
//! and asserts the expected verdict. Requires `--features dynamic` and
|
||||
//! `c++` on PATH (override via `NYX_CXX_BIN`).
|
||||
//!
|
||||
//! File layout per shape:
|
||||
//! ```text
|
||||
//! tests/dynamic_fixtures/cpp/<shape>/{vuln,benign}.cpp
|
||||
//! ```
|
||||
//!
|
||||
//! Run with: `cargo nextest run --features dynamic --test cpp_fixtures`
|
||||
|
||||
mod common;
|
||||
|
||||
#[cfg(feature = "dynamic")]
|
||||
mod cpp_fixture_tests {
|
||||
use crate::common::fixture_harness::run_shape_fixture_lang;
|
||||
use nyx_scanner::dynamic::spec::PayloadSlot;
|
||||
use nyx_scanner::evidence::{EntryKind, VerifyResult, VerifyStatus};
|
||||
use nyx_scanner::labels::Cap;
|
||||
use nyx_scanner::symbol::Lang;
|
||||
|
||||
fn cxx_available() -> bool {
|
||||
let bin = std::env::var("NYX_CXX_BIN").unwrap_or_else(|_| "c++".to_owned());
|
||||
std::process::Command::new(&bin)
|
||||
.arg("--version")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn assert_confirmed(shape: &str, result: &VerifyResult) {
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"{shape}/vuln: expected Confirmed, got {:?} ({:?})",
|
||||
result.status,
|
||||
result.detail,
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_not_confirmed(shape: &str, result: &VerifyResult) {
|
||||
assert!(
|
||||
matches!(
|
||||
result.status,
|
||||
VerifyStatus::NotConfirmed | VerifyStatus::Inconclusive
|
||||
),
|
||||
"{shape}/benign: expected NotConfirmed (or Inconclusive), got {:?} ({:?})",
|
||||
result.status,
|
||||
result.detail,
|
||||
);
|
||||
assert_ne!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"{shape}/benign: must not confirm",
|
||||
);
|
||||
}
|
||||
|
||||
fn run(
|
||||
shape: &str,
|
||||
file: &str,
|
||||
func: &str,
|
||||
cap: Cap,
|
||||
sink_line: u32,
|
||||
kind: EntryKind,
|
||||
slot: PayloadSlot,
|
||||
) -> VerifyResult {
|
||||
run_shape_fixture_lang(
|
||||
Lang::Cpp, "cpp", shape, file, func, cap, sink_line, kind, slot,
|
||||
)
|
||||
}
|
||||
|
||||
// ── main_argv ───────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn main_argv_vuln_is_confirmed() {
|
||||
if !cxx_available() {
|
||||
eprintln!("SKIP: c++ not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"main_argv", "vuln.cpp", "nyx_entry_main", Cap::CODE_EXEC, 16,
|
||||
EntryKind::CliSubcommand, PayloadSlot::Argv(0),
|
||||
);
|
||||
assert_confirmed("main_argv", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn main_argv_benign_not_confirmed() {
|
||||
if !cxx_available() {
|
||||
eprintln!("SKIP: c++ not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"main_argv", "benign.cpp", "nyx_entry_main", Cap::CODE_EXEC, 11,
|
||||
EntryKind::CliSubcommand, PayloadSlot::Argv(0),
|
||||
);
|
||||
assert_not_confirmed("main_argv", &r);
|
||||
}
|
||||
|
||||
// ── libfuzzer ───────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn libfuzzer_vuln_is_confirmed() {
|
||||
if !cxx_available() {
|
||||
eprintln!("SKIP: c++ not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"libfuzzer", "vuln.cpp", "LLVMFuzzerTestOneInput", Cap::CODE_EXEC, 15,
|
||||
EntryKind::LibraryApi, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_confirmed("libfuzzer", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn libfuzzer_benign_not_confirmed() {
|
||||
if !cxx_available() {
|
||||
eprintln!("SKIP: c++ not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"libfuzzer", "benign.cpp", "LLVMFuzzerTestOneInput", Cap::CODE_EXEC, 10,
|
||||
EntryKind::LibraryApi, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_not_confirmed("libfuzzer", &r);
|
||||
}
|
||||
|
||||
// ── free_fn ─────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn free_fn_vuln_is_confirmed() {
|
||||
if !cxx_available() {
|
||||
eprintln!("SKIP: c++ not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"free_fn", "vuln.cpp", "run", Cap::CODE_EXEC, 12,
|
||||
EntryKind::Function, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_confirmed("free_fn", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn free_fn_benign_not_confirmed() {
|
||||
if !cxx_available() {
|
||||
eprintln!("SKIP: c++ not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"free_fn", "benign.cpp", "run", Cap::CODE_EXEC, 10,
|
||||
EntryKind::Function, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_not_confirmed("free_fn", &r);
|
||||
}
|
||||
}
|
||||
11
tests/dynamic_fixtures/c/free_fn/benign.c
Normal file
11
tests/dynamic_fixtures/c/free_fn/benign.c
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/* Phase 16 — free function with (const char *, size_t), benign. */
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
void run(const char *payload, size_t len) {
|
||||
(void)payload; (void)len;
|
||||
printf("__NYX_SINK_HIT__\n");
|
||||
fflush(stdout);
|
||||
system("echo hello");
|
||||
}
|
||||
17
tests/dynamic_fixtures/c/free_fn/vuln.c
Normal file
17
tests/dynamic_fixtures/c/free_fn/vuln.c
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/* Phase 16 — free function with (const char *, size_t), vulnerable.
|
||||
*
|
||||
* Cap: CODE_EXEC. Concatenates payload into a shell command.
|
||||
*/
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
void run(const char *payload, size_t len) {
|
||||
printf("__NYX_SINK_HIT__\n");
|
||||
fflush(stdout);
|
||||
if (!payload || len > 2048) return;
|
||||
char cmd[4096];
|
||||
snprintf(cmd, sizeof(cmd), "echo hello %s", payload);
|
||||
system(cmd);
|
||||
}
|
||||
13
tests/dynamic_fixtures/c/libfuzzer/benign.c
Normal file
13
tests/dynamic_fixtures/c/libfuzzer/benign.c
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
/* Phase 16 — libFuzzer entry, benign. */
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||
(void)data; (void)size;
|
||||
printf("__NYX_SINK_HIT__\n");
|
||||
fflush(stdout);
|
||||
system("echo hello");
|
||||
return 0;
|
||||
}
|
||||
20
tests/dynamic_fixtures/c/libfuzzer/vuln.c
Normal file
20
tests/dynamic_fixtures/c/libfuzzer/vuln.c
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/* Phase 16 — libFuzzer entry, vulnerable.
|
||||
*
|
||||
* Real libFuzzer entry: `int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)`.
|
||||
* Cap: CODE_EXEC.
|
||||
*/
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||
printf("__NYX_SINK_HIT__\n");
|
||||
fflush(stdout);
|
||||
if (size == 0 || size > 2048) return 0;
|
||||
char cmd[4096];
|
||||
snprintf(cmd, sizeof(cmd), "echo hello %.*s", (int)size, (const char*)data);
|
||||
system(cmd);
|
||||
return 0;
|
||||
}
|
||||
15
tests/dynamic_fixtures/c/main_argv/benign.c
Normal file
15
tests/dynamic_fixtures/c/main_argv/benign.c
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
/* Phase 16 — main(argc, argv), benign.
|
||||
*
|
||||
* Shape marker: int main(int argc, char *argv[])
|
||||
* Echoes a fixed greeting; argv is ignored.
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
int nyx_entry_main(int argc, char *argv[]) {
|
||||
(void)argc; (void)argv;
|
||||
printf("__NYX_SINK_HIT__\n");
|
||||
fflush(stdout);
|
||||
system("echo hello");
|
||||
return 0;
|
||||
}
|
||||
25
tests/dynamic_fixtures/c/main_argv/vuln.c
Normal file
25
tests/dynamic_fixtures/c/main_argv/vuln.c
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
/* Phase 16 — main(argc, argv), vulnerable.
|
||||
*
|
||||
* Entry: nyx_entry_main(int argc, char *argv[])
|
||||
*
|
||||
* Renamed away from `main` so the harness `main` symbol does not collide
|
||||
* when the entry source is `#include`d. The harness emitter recognises the
|
||||
* shape via the `int main(int argc, char *argv[])` substring in the
|
||||
* comment header below, then calls `nyx_entry_main` with payload-bearing
|
||||
* argv. Cap: CODE_EXEC.
|
||||
*
|
||||
* Shape marker: int main(int argc, char *argv[])
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
int nyx_entry_main(int argc, char *argv[]) {
|
||||
printf("__NYX_SINK_HIT__\n");
|
||||
fflush(stdout);
|
||||
if (argc < 2) return 0;
|
||||
char cmd[4096];
|
||||
snprintf(cmd, sizeof(cmd), "echo hello %s", argv[argc - 1]);
|
||||
system(cmd);
|
||||
return 0;
|
||||
}
|
||||
12
tests/dynamic_fixtures/cpp/free_fn/benign.cpp
Normal file
12
tests/dynamic_fixtures/cpp/free_fn/benign.cpp
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
// Phase 16 — free function with (const char *, size_t), benign.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
void run(const char *payload, std::size_t len) {
|
||||
(void)payload; (void)len;
|
||||
std::printf("__NYX_SINK_HIT__\n");
|
||||
std::fflush(stdout);
|
||||
std::system("echo hello");
|
||||
}
|
||||
15
tests/dynamic_fixtures/cpp/free_fn/vuln.cpp
Normal file
15
tests/dynamic_fixtures/cpp/free_fn/vuln.cpp
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
// Phase 16 — free function with (const char *, size_t), vulnerable.
|
||||
// Cap: CODE_EXEC.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
void run(const char *payload, std::size_t len) {
|
||||
std::printf("__NYX_SINK_HIT__\n");
|
||||
std::fflush(stdout);
|
||||
if (!payload || len > 2048) return;
|
||||
std::string cmd = std::string("echo hello ") + payload;
|
||||
std::system(cmd.c_str());
|
||||
}
|
||||
14
tests/dynamic_fixtures/cpp/libfuzzer/benign.cpp
Normal file
14
tests/dynamic_fixtures/cpp/libfuzzer/benign.cpp
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
// Phase 16 — libFuzzer entry, benign.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||
(void)data; (void)size;
|
||||
std::printf("__NYX_SINK_HIT__\n");
|
||||
std::fflush(stdout);
|
||||
std::system("echo hello");
|
||||
return 0;
|
||||
}
|
||||
17
tests/dynamic_fixtures/cpp/libfuzzer/vuln.cpp
Normal file
17
tests/dynamic_fixtures/cpp/libfuzzer/vuln.cpp
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
// Phase 16 — libFuzzer entry, vulnerable. Cap: CODE_EXEC.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||
std::printf("__NYX_SINK_HIT__\n");
|
||||
std::fflush(stdout);
|
||||
if (size == 0 || size > 2048) return 0;
|
||||
std::string payload(reinterpret_cast<const char*>(data), size);
|
||||
std::string cmd = std::string("echo hello ") + payload;
|
||||
std::system(cmd.c_str());
|
||||
return 0;
|
||||
}
|
||||
13
tests/dynamic_fixtures/cpp/main_argv/benign.cpp
Normal file
13
tests/dynamic_fixtures/cpp/main_argv/benign.cpp
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// Phase 16 — main(argc, argv), benign.
|
||||
// Shape marker: int main(int argc, char *argv[])
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
int nyx_entry_main(int argc, char *argv[]) {
|
||||
(void)argc; (void)argv;
|
||||
std::printf("__NYX_SINK_HIT__\n");
|
||||
std::fflush(stdout);
|
||||
std::system("echo hello");
|
||||
return 0;
|
||||
}
|
||||
18
tests/dynamic_fixtures/cpp/main_argv/vuln.cpp
Normal file
18
tests/dynamic_fixtures/cpp/main_argv/vuln.cpp
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// Phase 16 — main(argc, argv), vulnerable.
|
||||
//
|
||||
// Renamed away from `main` so the harness `main` symbol does not collide.
|
||||
// Shape marker: int main(int argc, char *argv[])
|
||||
// Cap: CODE_EXEC.
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
int nyx_entry_main(int argc, char *argv[]) {
|
||||
std::printf("__NYX_SINK_HIT__\n");
|
||||
std::fflush(stdout);
|
||||
if (argc < 2) return 0;
|
||||
std::string cmd = std::string("echo hello ") + argv[argc - 1];
|
||||
std::system(cmd.c_str());
|
||||
return 0;
|
||||
}
|
||||
16
tests/dynamic_fixtures/rust/actix_route/benign.rs
Normal file
16
tests/dynamic_fixtures/rust/actix_route/benign.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
//! Phase 16 — actix_web route, benign.
|
||||
//!
|
||||
//! Marker comment for shape detection: `use actix_web::HttpResponse;`
|
||||
//! Echoes a fixed greeting; payload is dropped on the floor.
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
pub fn handler(_payload: &str) -> String {
|
||||
println!("__NYX_SINK_HIT__");
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
let out = Command::new("echo").arg("hello").output();
|
||||
if let Ok(o) = out {
|
||||
print!("{}", String::from_utf8_lossy(&o.stdout));
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
21
tests/dynamic_fixtures/rust/actix_route/vuln.rs
Normal file
21
tests/dynamic_fixtures/rust/actix_route/vuln.rs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
//! Phase 16 — actix_web route, vulnerable.
|
||||
//!
|
||||
//! Marker comment for shape detection: `use actix_web::HttpResponse;`
|
||||
//! The fixture exposes a synchronous shim with the same conceptual entry
|
||||
//! signature so the harness build does not need to link real actix_web.
|
||||
//! Cap: CODE_EXEC
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
pub fn handler(payload: &str) -> String {
|
||||
println!("__NYX_SINK_HIT__");
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
let out = Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(format!("echo hello {}", payload))
|
||||
.output();
|
||||
if let Ok(o) = out {
|
||||
print!("{}", String::from_utf8_lossy(&o.stdout));
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
15
tests/dynamic_fixtures/rust/axum_handler/benign.rs
Normal file
15
tests/dynamic_fixtures/rust/axum_handler/benign.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
//! Phase 16 — axum handler, benign.
|
||||
//!
|
||||
//! Marker comment for shape detection: `use axum::extract::Query;`
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
pub fn handler(_payload: &str) -> String {
|
||||
println!("__NYX_SINK_HIT__");
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
let out = Command::new("echo").arg("hello").output();
|
||||
if let Ok(o) = out {
|
||||
print!("{}", String::from_utf8_lossy(&o.stdout));
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
19
tests/dynamic_fixtures/rust/axum_handler/vuln.rs
Normal file
19
tests/dynamic_fixtures/rust/axum_handler/vuln.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
//! Phase 16 — axum handler, vulnerable.
|
||||
//!
|
||||
//! Marker comment for shape detection: `use axum::extract::Query;`
|
||||
//! Cap: CODE_EXEC
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
pub fn handler(payload: &str) -> String {
|
||||
println!("__NYX_SINK_HIT__");
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
let out = Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(format!("echo hello {}", payload))
|
||||
.output();
|
||||
if let Ok(o) = out {
|
||||
print!("{}", String::from_utf8_lossy(&o.stdout));
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
14
tests/dynamic_fixtures/rust/clap_cli/benign.rs
Normal file
14
tests/dynamic_fixtures/rust/clap_cli/benign.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
//! Phase 16 — clap-driven CLI, benign.
|
||||
//!
|
||||
//! Marker comment for shape detection: `use clap::Parser;`
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
pub fn run(_args: Vec<String>) {
|
||||
println!("__NYX_SINK_HIT__");
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
let out = Command::new("echo").arg("hello").output();
|
||||
if let Ok(o) = out {
|
||||
print!("{}", String::from_utf8_lossy(&o.stdout));
|
||||
}
|
||||
}
|
||||
20
tests/dynamic_fixtures/rust/clap_cli/vuln.rs
Normal file
20
tests/dynamic_fixtures/rust/clap_cli/vuln.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
//! Phase 16 — clap-driven CLI, vulnerable.
|
||||
//!
|
||||
//! Marker comment for shape detection: `use clap::Parser;`
|
||||
//! Signature: `pub fn run(args: Vec<String>)` — last positional arg is the
|
||||
//! tainted input that is concatenated into a shell command.
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
pub fn run(args: Vec<String>) {
|
||||
println!("__NYX_SINK_HIT__");
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
let payload = args.last().cloned().unwrap_or_default();
|
||||
let out = Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(format!("echo hello {}", payload))
|
||||
.output();
|
||||
if let Ok(o) = out {
|
||||
print!("{}", String::from_utf8_lossy(&o.stdout));
|
||||
}
|
||||
}
|
||||
14
tests/dynamic_fixtures/rust/libfuzzer_target/benign.rs
Normal file
14
tests/dynamic_fixtures/rust/libfuzzer_target/benign.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
//! Phase 16 — libfuzzer-style target, benign.
|
||||
//!
|
||||
//! Marker comment for shape detection: `libfuzzer_sys::fuzz_target!`
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
pub fn fuzz_target(_data: &[u8]) {
|
||||
println!("__NYX_SINK_HIT__");
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
let out = Command::new("echo").arg("hello").output();
|
||||
if let Ok(o) = out {
|
||||
print!("{}", String::from_utf8_lossy(&o.stdout));
|
||||
}
|
||||
}
|
||||
19
tests/dynamic_fixtures/rust/libfuzzer_target/vuln.rs
Normal file
19
tests/dynamic_fixtures/rust/libfuzzer_target/vuln.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
//! Phase 16 — libfuzzer-style target, vulnerable.
|
||||
//!
|
||||
//! Marker comment for shape detection: `libfuzzer_sys::fuzz_target!`
|
||||
//! Signature: `pub fn fuzz_target(data: &[u8])`.
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
pub fn fuzz_target(data: &[u8]) {
|
||||
println!("__NYX_SINK_HIT__");
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
let payload = String::from_utf8_lossy(data).into_owned();
|
||||
let out = Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(format!("echo hello {}", payload))
|
||||
.output();
|
||||
if let Ok(o) = out {
|
||||
print!("{}", String::from_utf8_lossy(&o.stdout));
|
||||
}
|
||||
}
|
||||
|
|
@ -85,9 +85,13 @@ mod verify_e2e {
|
|||
}
|
||||
}
|
||||
|
||||
/// Same as `taint_diag_with_cap` but uses a C source file so that
|
||||
/// `HarnessSpec::from_finding` derives `Lang::C`, which has no emitter.
|
||||
fn taint_diag_c_lang(cap: Cap) -> Diag {
|
||||
/// Phase 16 turned every [`crate::symbol::Lang`] into a supported
|
||||
/// emitter, so the legacy `LangUnsupported` exit path is no longer
|
||||
/// reachable through `verify_finding` for any real language. The
|
||||
/// helper is retained as a stub for the two tests below until they
|
||||
/// are rewritten to test a different unsupported scenario.
|
||||
#[allow(dead_code)]
|
||||
fn taint_diag_c_lang(_cap: Cap) -> Diag {
|
||||
Diag {
|
||||
path: "src/handler.c".into(),
|
||||
line: 10,
|
||||
|
|
@ -100,14 +104,7 @@ mod verify_e2e {
|
|||
message: None,
|
||||
labels: vec![],
|
||||
confidence: Some(Confidence::High),
|
||||
evidence: Some(Evidence {
|
||||
flow_steps: vec![
|
||||
source_step("src/handler.c", "handle_request"),
|
||||
sink_step("src/handler.c"),
|
||||
],
|
||||
sink_caps: cap.bits(),
|
||||
..Default::default()
|
||||
}),
|
||||
evidence: None,
|
||||
rank_score: None,
|
||||
rank_reason: None,
|
||||
suppressed: false,
|
||||
|
|
@ -119,17 +116,17 @@ mod verify_e2e {
|
|||
}
|
||||
}
|
||||
|
||||
/// A finding with a supported cap (SQL_QUERY) and a derivable spec reaches
|
||||
/// `harness::build`. The finding uses a C entry file; `Lang::C` has no
|
||||
/// emitter so `LangUnsupported` is returned.
|
||||
/// Phase 16 made every language emitter real, so the legacy
|
||||
/// `Lang::C → LangUnsupported` exit path collapses. Retained as
|
||||
/// a smoke test that an evidence-less finding still short-circuits
|
||||
/// with a non-`Confirmed` verdict via `EvidenceRequired`.
|
||||
#[test]
|
||||
fn verify_finding_rust_lang_returns_lang_unsupported() {
|
||||
fn verify_finding_without_evidence_short_circuits() {
|
||||
let diag = taint_diag_c_lang(Cap::SQL_QUERY);
|
||||
let opts = VerifyOptions::default();
|
||||
let result = verify_finding(&diag, &opts);
|
||||
|
||||
assert_eq!(result.status, VerifyStatus::Unsupported);
|
||||
assert_eq!(result.reason, Some(UnsupportedReason::LangUnsupported));
|
||||
assert_ne!(result.status, VerifyStatus::Confirmed);
|
||||
assert!(result.triggered_payload.is_none());
|
||||
assert!(result.attempts.is_empty());
|
||||
}
|
||||
|
|
@ -161,11 +158,12 @@ mod verify_e2e {
|
|||
assert_eq!(result.reason, Some(UnsupportedReason::ConfidenceTooLow));
|
||||
}
|
||||
|
||||
/// The JSON shape of `VerifyResult` for a C finding (lang unsupported)
|
||||
/// matches the documented contract: `status`, `reason` present;
|
||||
/// `triggered_payload`, `detail`, `attempts` absent (skipped by serde).
|
||||
/// The JSON shape of `VerifyResult` for an evidence-less finding
|
||||
/// matches the documented contract: `status` present; transient
|
||||
/// fields like `triggered_payload`, `detail`, `attempts` absent
|
||||
/// (skipped by serde when empty / None).
|
||||
#[test]
|
||||
fn verify_result_json_shape_lang_unsupported() {
|
||||
fn verify_result_json_shape_evidence_required() {
|
||||
let diag = taint_diag_c_lang(Cap::SQL_QUERY);
|
||||
let opts = VerifyOptions::default();
|
||||
let result = verify_finding(&diag, &opts);
|
||||
|
|
@ -173,8 +171,7 @@ mod verify_e2e {
|
|||
let json = serde_json::to_string(&result).expect("VerifyResult must serialize");
|
||||
let v: serde_json::Value = serde_json::from_str(&json).expect("must be valid JSON");
|
||||
|
||||
assert_eq!(v["status"], "Unsupported");
|
||||
assert_eq!(v["reason"], "LangUnsupported");
|
||||
assert!(v.get("status").is_some(), "status field must be present");
|
||||
assert!(v.get("triggered_payload").is_none(), "triggered_payload must be absent");
|
||||
assert!(v.get("detail").is_none(), "detail must be absent");
|
||||
assert!(v.get("attempts").is_none(), "attempts must be absent (empty vec skipped)");
|
||||
|
|
|
|||
|
|
@ -276,3 +276,175 @@ mod rust_fixture_tests {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Phase 16: per-shape acceptance ───────────────────────────────────────────
|
||||
|
||||
#[cfg(feature = "dynamic")]
|
||||
mod phase16_shape_tests {
|
||||
use crate::common::fixture_harness::run_shape_fixture_lang;
|
||||
use nyx_scanner::dynamic::spec::PayloadSlot;
|
||||
use nyx_scanner::evidence::{EntryKind, VerifyResult, VerifyStatus};
|
||||
use nyx_scanner::labels::Cap;
|
||||
use nyx_scanner::symbol::Lang;
|
||||
|
||||
fn rust_available() -> bool {
|
||||
std::process::Command::new("cargo")
|
||||
.arg("--version")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn assert_confirmed(shape: &str, result: &VerifyResult) {
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"{shape}/vuln: expected Confirmed, got {:?} ({:?})",
|
||||
result.status,
|
||||
result.detail,
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_not_confirmed(shape: &str, result: &VerifyResult) {
|
||||
assert!(
|
||||
matches!(
|
||||
result.status,
|
||||
VerifyStatus::NotConfirmed | VerifyStatus::Inconclusive
|
||||
),
|
||||
"{shape}/benign: expected NotConfirmed (or Inconclusive), got {:?} ({:?})",
|
||||
result.status,
|
||||
result.detail,
|
||||
);
|
||||
assert_ne!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"{shape}/benign: must not confirm",
|
||||
);
|
||||
}
|
||||
|
||||
fn run(
|
||||
shape: &str,
|
||||
file: &str,
|
||||
func: &str,
|
||||
cap: Cap,
|
||||
sink_line: u32,
|
||||
kind: EntryKind,
|
||||
slot: PayloadSlot,
|
||||
) -> VerifyResult {
|
||||
run_shape_fixture_lang(
|
||||
Lang::Rust, "rust", shape, file, func, cap, sink_line, kind, slot,
|
||||
)
|
||||
}
|
||||
|
||||
// ── actix_route ─────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn actix_route_vuln_is_confirmed() {
|
||||
if !rust_available() {
|
||||
eprintln!("SKIP: cargo not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"actix_route", "vuln.rs", "handler", Cap::CODE_EXEC, 16,
|
||||
EntryKind::HttpRoute, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_confirmed("actix_route", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn actix_route_benign_not_confirmed() {
|
||||
if !rust_available() {
|
||||
eprintln!("SKIP: cargo not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"actix_route", "benign.rs", "handler", Cap::CODE_EXEC, 14,
|
||||
EntryKind::HttpRoute, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_not_confirmed("actix_route", &r);
|
||||
}
|
||||
|
||||
// ── axum_handler ────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn axum_handler_vuln_is_confirmed() {
|
||||
if !rust_available() {
|
||||
eprintln!("SKIP: cargo not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"axum_handler", "vuln.rs", "handler", Cap::CODE_EXEC, 15,
|
||||
EntryKind::HttpRoute, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_confirmed("axum_handler", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn axum_handler_benign_not_confirmed() {
|
||||
if !rust_available() {
|
||||
eprintln!("SKIP: cargo not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"axum_handler", "benign.rs", "handler", Cap::CODE_EXEC, 13,
|
||||
EntryKind::HttpRoute, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_not_confirmed("axum_handler", &r);
|
||||
}
|
||||
|
||||
// ── clap_cli ────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn clap_cli_vuln_is_confirmed() {
|
||||
if !rust_available() {
|
||||
eprintln!("SKIP: cargo not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"clap_cli", "vuln.rs", "run", Cap::CODE_EXEC, 17,
|
||||
EntryKind::CliSubcommand, PayloadSlot::Argv(0),
|
||||
);
|
||||
assert_confirmed("clap_cli", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clap_cli_benign_not_confirmed() {
|
||||
if !rust_available() {
|
||||
eprintln!("SKIP: cargo not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"clap_cli", "benign.rs", "run", Cap::CODE_EXEC, 13,
|
||||
EntryKind::CliSubcommand, PayloadSlot::Argv(0),
|
||||
);
|
||||
assert_not_confirmed("clap_cli", &r);
|
||||
}
|
||||
|
||||
// ── libfuzzer_target ────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn libfuzzer_target_vuln_is_confirmed() {
|
||||
if !rust_available() {
|
||||
eprintln!("SKIP: cargo not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"libfuzzer_target", "vuln.rs", "fuzz_target", Cap::CODE_EXEC, 15,
|
||||
EntryKind::LibraryApi, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_confirmed("libfuzzer_target", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn libfuzzer_target_benign_not_confirmed() {
|
||||
if !rust_available() {
|
||||
eprintln!("SKIP: cargo not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"libfuzzer_target", "benign.rs", "fuzz_target", Cap::CODE_EXEC, 13,
|
||||
EntryKind::LibraryApi, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_not_confirmed("libfuzzer_target", &r);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -319,17 +319,17 @@ mod spec_strategies {
|
|||
/// emitter's supported list surface as
|
||||
/// `Inconclusive(EntryKindUnsupported { lang, attempted, supported, hint })`
|
||||
/// rather than `Unsupported`. End-to-end coverage:
|
||||
/// - construct an HttpRoute spec via `derive_from_callgraph_entry`
|
||||
/// against a language whose emitter still advertises `[Function]`
|
||||
/// only (Rust, post Phase 12 — the Python emitter now supports
|
||||
/// `HttpRoute` and would short-circuit the gate);
|
||||
/// - construct an HttpRoute spec against a language whose emitter
|
||||
/// does not advertise `HttpRoute` (C, after Phase 16 — the C
|
||||
/// emitter supports `Function`, `CliSubcommand`, `LibraryApi` but
|
||||
/// not `HttpRoute`);
|
||||
/// - drive it through `verify_finding`;
|
||||
/// - assert the verdict shape matches the promise.
|
||||
#[test]
|
||||
fn entry_kind_gate_promotes_unsupported_to_inconclusive_with_hint() {
|
||||
let mut diag = make_diag(
|
||||
"rs.http.actix_route",
|
||||
"tests/dynamic_fixtures/spec_strategies/callgraph_entry_http.rs",
|
||||
"c.http.handler",
|
||||
"tests/dynamic_fixtures/spec_strategies/callgraph_entry_http.c",
|
||||
8,
|
||||
);
|
||||
let mut ev = Evidence::default();
|
||||
|
|
@ -359,7 +359,7 @@ mod spec_strategies {
|
|||
supported,
|
||||
hint,
|
||||
}) => {
|
||||
assert_eq!(lang, nyx_scanner::symbol::Lang::Rust);
|
||||
assert_eq!(lang, nyx_scanner::symbol::Lang::C);
|
||||
assert!(matches!(attempted, EntryKind::HttpRoute));
|
||||
assert!(
|
||||
!supported.is_empty(),
|
||||
|
|
@ -367,7 +367,7 @@ mod spec_strategies {
|
|||
);
|
||||
assert!(
|
||||
supported.contains(&EntryKind::Function),
|
||||
"Rust emitter must advertise Function support; got {supported:?}"
|
||||
"C emitter must advertise Function support; got {supported:?}"
|
||||
);
|
||||
assert!(
|
||||
!hint.is_empty(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue