This commit is contained in:
Eli Peter 2026-06-05 10:16:30 -05:00 committed by GitHub
parent 55247b7fcd
commit 991c84a1eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
1464 changed files with 225448 additions and 1985 deletions

113
src/dynamic/build_pool/c.rs Normal file
View file

@ -0,0 +1,113 @@
//! C build pool (Phase 23 / Track O.1).
//!
//! Wraps the C compiler in `ccache` (when present) backed by a shared object
//! cache under the pool cache root, so a finding that recompiles a harness
//! whose `main.c` matches a previously-built one gets a cache hit instead of a
//! cold `cc` invocation.
//!
//! `ccache` degrades gracefully: when it is not on `PATH` the pool runs the
//! bare compiler, byte-for-byte the same `cc` invocation the legacy
//! [`crate::dynamic::build_sandbox::prepare_c`] path uses, so success / failure
//! parity holds. The static-link fallback (drop `-static` and retry) mirrors
//! the legacy `run_cc` behaviour for chroot-bound Strict-profile harnesses.
use super::{BuildPool, PoolCompileResult, base_command, binary_runnable, pool_cache_dir};
use std::path::Path;
use std::time::Instant;
pub struct CPool {
cc_bin: String,
ccache_bin: Option<String>,
}
impl CPool {
pub fn try_new() -> Result<Self, String> {
let cc_bin = std::env::var("NYX_CC_BIN").unwrap_or_else(|_| "cc".to_owned());
if !binary_runnable(&cc_bin, "--version") {
return Err(format!("c-pool: {cc_bin} not runnable"));
}
Ok(CPool {
cc_bin,
ccache_bin: super::detect_ccache(),
})
}
}
impl BuildPool for CPool {
fn name(&self) -> &'static str {
"c"
}
/// `args[0]` = binary destination, `args[1]` = `"static"` or `"dynamic"`.
fn compile_batch(&self, workdir: &Path, args: &[String]) -> PoolCompileResult {
let start = Instant::now();
let dest = match args.first() {
Some(d) => d.clone(),
None => {
return PoolCompileResult {
success: false,
stderr: "c-pool: missing binary destination arg".to_owned(),
duration: start.elapsed(),
};
}
};
let static_link = args.get(1).map(|s| s == "static").unwrap_or(false);
if static_link {
match self.run(workdir, &dest, &["-static", "-O0", "-g"]) {
Ok(()) => {
return PoolCompileResult {
success: true,
stderr: String::new(),
duration: start.elapsed(),
};
}
Err(stderr) => {
unsafe { std::env::set_var("NYX_BUILD_STATIC_FALLBACK", "1") };
eprintln!("nyx: c-pool cc -static failed, retrying without -static: {stderr}");
let _ = std::fs::remove_file(&dest);
}
}
}
match self.run(workdir, &dest, &["-O0", "-g"]) {
Ok(()) => PoolCompileResult {
success: true,
stderr: String::new(),
duration: start.elapsed(),
},
Err(stderr) => PoolCompileResult {
success: false,
stderr,
duration: start.elapsed(),
},
}
}
fn is_healthy(&self) -> bool {
binary_runnable(&self.cc_bin, "--version")
}
}
impl CPool {
/// Run one compile of `main.c`, optionally fronted by `ccache`.
fn run(&self, workdir: &Path, dest: &str, leading_flags: &[&str]) -> Result<(), String> {
let mut cmd = match (&self.ccache_bin, pool_cache_dir("c", "ccache")) {
(Some(ccache), Some(cache_dir)) => {
let mut c = base_command(ccache);
c.arg(&self.cc_bin).env("CCACHE_DIR", cache_dir);
c
}
_ => base_command(&self.cc_bin),
};
cmd.args(leading_flags)
.args(["-o", dest, "main.c"])
.current_dir(workdir);
let output = cmd.output().map_err(|e| format!("c-pool: cc: {e}"))?;
if !output.status.success() {
return Err(String::from_utf8_lossy(&output.stderr).into_owned());
}
Ok(())
}
}

View file

@ -0,0 +1,83 @@
//! C++ build pool (Phase 23 / Track O.1).
//!
//! Same shape as the C pool: front the C++ driver with `ccache` backed by a
//! shared object cache under the pool cache root. Falls back to a bare
//! `c++ -std=c++17` compile — byte-for-byte the legacy
//! [`crate::dynamic::build_sandbox::prepare_cpp`] invocation — when `ccache` is
//! absent.
use super::{BuildPool, PoolCompileResult, base_command, binary_runnable, pool_cache_dir};
use std::path::Path;
use std::time::Instant;
pub struct CppPool {
cxx_bin: String,
ccache_bin: Option<String>,
}
impl CppPool {
pub fn try_new() -> Result<Self, String> {
let cxx_bin = std::env::var("NYX_CXX_BIN").unwrap_or_else(|_| "c++".to_owned());
if !binary_runnable(&cxx_bin, "--version") {
return Err(format!("cpp-pool: {cxx_bin} not runnable"));
}
Ok(CppPool {
cxx_bin,
ccache_bin: super::detect_ccache(),
})
}
}
impl BuildPool for CppPool {
fn name(&self) -> &'static str {
"cpp"
}
/// `args[0]` = absolute path the compiled `nyx_harness` binary lands at.
fn compile_batch(&self, workdir: &Path, args: &[String]) -> PoolCompileResult {
let start = Instant::now();
let dest = match args.first() {
Some(d) => d.clone(),
None => {
return PoolCompileResult {
success: false,
stderr: "cpp-pool: missing binary destination arg".to_owned(),
duration: start.elapsed(),
};
}
};
let mut cmd = match (&self.ccache_bin, pool_cache_dir("cpp", "ccache")) {
(Some(ccache), Some(cache_dir)) => {
let mut c = base_command(ccache);
c.arg(&self.cxx_bin).env("CCACHE_DIR", cache_dir);
c
}
_ => base_command(&self.cxx_bin),
};
cmd.args(["-O0", "-g", "-std=c++17", "-o", &dest, "main.cpp"])
.current_dir(workdir);
match cmd.output() {
Ok(o) if o.status.success() => PoolCompileResult {
success: true,
stderr: String::new(),
duration: start.elapsed(),
},
Ok(o) => PoolCompileResult {
success: false,
stderr: String::from_utf8_lossy(&o.stderr).into_owned(),
duration: start.elapsed(),
},
Err(e) => PoolCompileResult {
success: false,
stderr: format!("cpp-pool: c++: {e}"),
duration: start.elapsed(),
},
}
}
fn is_healthy(&self) -> bool {
binary_runnable(&self.cxx_bin, "--version")
}
}

View file

@ -0,0 +1,140 @@
//! Go build pool (Phase 23 / Track O.1).
//!
//! The legacy [`crate::dynamic::build_sandbox::prepare_go`] gives each finding
//! its own `GOCACHE`/`GOMODCACHE` (default: a per-workdir `.gocache`), so the
//! Go toolchain recompiles the standard library and every module from cold on
//! every harness.
//!
//! [`GoPool`] mounts one shared `GOCACHE` + `GOMODCACHE` under the pool cache
//! root so compiled std-lib + module artefacts are reused across findings, and
//! builds with `-trimpath -buildvcs=false` so the output is reproducible (no
//! absolute workdir paths or VCS stamping baked in, which otherwise defeats the
//! build cache's keying).
use super::{BuildPool, PoolCompileResult, base_command, binary_runnable, pool_cache_dir};
use std::path::Path;
use std::time::Instant;
pub struct GoPool {
go_bin: String,
}
impl GoPool {
pub fn try_new() -> Result<Self, String> {
let go_bin = std::env::var("NYX_GO_BIN").unwrap_or_else(|_| "go".to_owned());
if !binary_runnable(&go_bin, "version") {
return Err(format!("go-pool: {go_bin} not runnable"));
}
Ok(GoPool { go_bin })
}
}
impl BuildPool for GoPool {
fn name(&self) -> &'static str {
"go"
}
/// `args[0]` = absolute path the compiled `nyx_harness` binary must land
/// at.
fn compile_batch(&self, workdir: &Path, args: &[String]) -> PoolCompileResult {
let start = Instant::now();
let dest = match args.first() {
Some(d) => d.clone(),
None => {
return PoolCompileResult {
success: false,
stderr: "go-pool: missing binary destination arg".to_owned(),
duration: start.elapsed(),
};
}
};
let go_cache = match pool_cache_dir("go", "cache") {
Some(d) => d,
None => {
return PoolCompileResult {
success: false,
stderr: "go-pool: no shared GOCACHE".to_owned(),
duration: start.elapsed(),
};
}
};
let go_mod_cache = match pool_cache_dir("go", "modcache") {
Some(d) => d,
None => {
return PoolCompileResult {
success: false,
stderr: "go-pool: no shared GOMODCACHE".to_owned(),
duration: start.elapsed(),
};
}
};
let go_path = std::env::var("GOPATH").unwrap_or_else(|_| {
std::env::var("HOME")
.map(|h| format!("{h}/go"))
.unwrap_or_else(|_| "/tmp/go".to_owned())
});
// `go mod tidy` resolves imports into the shared module cache.
if workdir.join("go.mod").exists() {
let tidy = base_command(&self.go_bin)
.args(["mod", "tidy"])
.current_dir(workdir)
.env("GOCACHE", &go_cache)
.env("GOPATH", &go_path)
.env("GOMODCACHE", &go_mod_cache)
.output();
match tidy {
Ok(o) if o.status.success() => {}
Ok(o) => {
let mut msg = String::from_utf8_lossy(&o.stderr).into_owned();
if msg.is_empty() {
msg = String::from_utf8_lossy(&o.stdout).into_owned();
}
return PoolCompileResult {
success: false,
stderr: format!("go mod tidy failed: {msg}"),
duration: start.elapsed(),
};
}
Err(e) => {
return PoolCompileResult {
success: false,
stderr: format!("go-pool: go mod tidy: {e}"),
duration: start.elapsed(),
};
}
}
}
let output = base_command(&self.go_bin)
.args(["build", "-trimpath", "-buildvcs=false", "-o", &dest, "."])
.current_dir(workdir)
.env("GOCACHE", &go_cache)
.env("GOPATH", &go_path)
.env("GOMODCACHE", &go_mod_cache)
.output();
match output {
Ok(o) if o.status.success() => PoolCompileResult {
success: true,
stderr: String::new(),
duration: start.elapsed(),
},
Ok(o) => PoolCompileResult {
success: false,
stderr: String::from_utf8_lossy(&o.stderr).into_owned(),
duration: start.elapsed(),
},
Err(e) => PoolCompileResult {
success: false,
stderr: format!("go-pool: go build: {e}"),
duration: start.elapsed(),
},
}
}
fn is_healthy(&self) -> bool {
binary_runnable(&self.go_bin, "version")
}
}

View file

@ -0,0 +1,952 @@
//! Long-lived `javac` daemon (Phase 22 / Track O.0).
//!
//! The legacy `try_compile_java_with_toolchain` in `build_sandbox` shell-execs a
//! fresh `javac` per harness — every invocation pays the JVM cold-start tax
//! (~700ms on the macOS reference machine, ~300ms on Linux CI). At 50
//! findings per OWASP-scale run that single line burns > 30s before any
//! real work happens.
//!
//! [`JavacPool`] replaces the shell-exec with a long-running worker JVM:
//!
//! ```text
//! nyx ─┐
//! │ framed JSON ┌─────────────┐
//! ├──stdin──────► │ NyxJavac │
//! │ │ Worker │
//! │ ◄──stdout──── │ (live JVM) │
//! │ framed JSON └─────────────┘
//! ```
//!
//! Bootstrap (paid once per toolchain id):
//! 1. Drop `NyxJavacWorker.java` into a cache dir.
//! 2. Compile it with `javac` (~1s).
//! 3. Spawn `java -cp <dir> NyxJavacWorker` (~700ms cold start).
//! 4. Read the worker's `{"ready":true}` banner.
//!
//! After bootstrap, each [`JavacPool::compile_batch`] is a single JSON
//! round-trip — typical wall-clock < 50ms even on small harnesses.
//!
//! # Robustness
//!
//! A crashed / hung worker is non-fatal:
//! - On any IO error, the pool marks itself unhealthy and the caller
//! falls back to the direct-spawn legacy path.
//! - The next pool lookup spawns a fresh worker.
//!
//! # Test hook
//!
//! `NYX_JAVAC_BIN` + `NYX_JAVA_BIN` override the binaries the pool
//! invokes so integration tests can swap in a wrapper.
use super::{BuildPool, PoolCompileResult};
use serde::Deserialize;
use std::fs::{File, OpenOptions};
use std::io::{BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
use std::sync::{Mutex, mpsc};
use std::thread;
use std::time::{Duration, Instant};
/// Java source compiled at first use to drive the worker.
const WORKER_SOURCE: &str = include_str!("java_worker/NyxJavacWorker.java");
const WORKER_CLASS: &str = "NyxJavacWorker";
const WORKER_FILENAME: &str = "NyxJavacWorker.java";
/// Manifest written last (atomically) by `publish_class_set` after every
/// class lands, so its presence is the "publish finished" signal a
/// lock-free reader keys on. Its *contents* are NOT trusted as the
/// completeness oracle -- see `WORKER_CLASS_FILES`.
const WORKER_MANIFEST: &str = ".worker-classes";
/// The exact set of `.class` files the worker JVM must load at runtime:
/// the top-level class plus its nested `$Request` / `$Parser` types.
///
/// Readiness keys on *this fixed set*, not on whatever the on-disk
/// manifest happens to name. A bootstrap cache left by an older binary
/// can carry a manifest that lists only `NyxJavacWorker.class`; trusting
/// that list let the gate pass with the nested classes absent, so the
/// worker spawned, announced readiness, then died on the first request
/// with `NoClassDefFoundError` surfaced as
/// `nyx-javac-worker: parse error: NyxJavacWorker$Parser`. Pinning the
/// required set here makes any such partial cache fail the gate and
/// trigger a clean recompile. Kept in lock-step with the worker's real
/// nested-class layout by `worker_class_files_match_javac_output`.
const WORKER_CLASS_FILES: &[&str] = &[
"NyxJavacWorker.class",
"NyxJavacWorker$Request.class",
"NyxJavacWorker$Parser.class",
];
const WORKER_READY_TIMEOUT: Duration = Duration::from_secs(10);
const COMPILE_RESPONSE_TIMEOUT: Duration = Duration::from_secs(60);
/// Live worker handle. Held inside a `Mutex` so concurrent
/// `compile_batch` callers serialise on the single JVM.
struct Worker {
child: Child,
stdin: ChildStdin,
stdout: BufReader<ChildStdout>,
next_id: u64,
}
pub struct JavacPool {
/// `None` when the worker has crashed and a future call should
/// surface the unhealthy state to the dispatcher.
inner: Mutex<Option<Worker>>,
/// Cache dir holding `NyxJavacWorker.class`. Persisted between
/// runs so subsequent process invocations skip the compile step.
bootstrap_dir: PathBuf,
}
impl JavacPool {
/// Create a fresh pool for `toolchain_id`.
///
/// Returns `Err` when the worker cannot be bootstrapped (missing
/// `javac`, missing `java`, compile failure, spawn failure). The
/// caller is expected to fall back to the legacy direct-spawn path
/// on any error.
pub fn try_new(toolchain_id: &str) -> Result<Self, String> {
let bootstrap_dir = bootstrap_dir_for(toolchain_id)?;
std::fs::create_dir_all(&bootstrap_dir)
.map_err(|e| format!("javac-pool: mkdir {}: {e}", bootstrap_dir.display()))?;
ensure_worker_compiled(&bootstrap_dir)?;
let worker = spawn_worker(&bootstrap_dir)?;
Ok(JavacPool {
inner: Mutex::new(Some(worker)),
bootstrap_dir,
})
}
fn compile_with_worker(&self, workdir: &Path, args: &[String]) -> PoolCompileResult {
let start = Instant::now();
let mut guard = match self.inner.lock() {
Ok(g) => g,
Err(p) => p.into_inner(),
};
// If a prior call torched the worker, try one re-spawn here so
// the caller doesn't see consecutive failures from a transient
// JVM crash.
if guard.is_none()
&& let Ok(w) = spawn_worker(&self.bootstrap_dir)
{
*guard = Some(w);
}
let worker = match guard.as_mut() {
Some(w) => w,
None => {
return PoolCompileResult {
success: false,
stderr: "javac-pool: worker unavailable".to_owned(),
duration: start.elapsed(),
};
}
};
let id = worker.next_id;
worker.next_id = worker.next_id.wrapping_add(1);
let req = build_request(id, workdir, args);
if let Err(e) = worker.stdin.write_all(req.as_bytes()) {
*guard = None;
return PoolCompileResult {
success: false,
stderr: format!("javac-pool: write failed: {e}"),
duration: start.elapsed(),
};
}
if let Err(e) = worker.stdin.flush() {
*guard = None;
return PoolCompileResult {
success: false,
stderr: format!("javac-pool: flush failed: {e}"),
duration: start.elapsed(),
};
}
match read_line_with_timeout(
&mut worker.child,
&mut worker.stdout,
COMPILE_RESPONSE_TIMEOUT,
"read response",
) {
Ok(None) => {
*guard = None;
PoolCompileResult {
success: false,
stderr: "javac-pool: worker closed stdout".to_owned(),
duration: start.elapsed(),
}
}
Err(e) => {
*guard = None;
PoolCompileResult {
success: false,
stderr: e,
duration: start.elapsed(),
}
}
Ok(Some(line)) => match parse_response(&line) {
Some((success, stderr)) => PoolCompileResult {
success,
stderr,
duration: start.elapsed(),
},
None => {
*guard = None;
PoolCompileResult {
success: false,
stderr: format!("javac-pool: malformed response: {line}"),
duration: start.elapsed(),
}
}
},
}
}
}
impl Drop for JavacPool {
fn drop(&mut self) {
// Best-effort: close stdin so the worker exits cleanly, then
// wait briefly. We don't propagate errors -- pool teardown
// happens at process exit, by which point everyone is already
// leaving anyway.
if let Ok(mut guard) = self.inner.lock()
&& let Some(mut worker) = guard.take()
{
// Dropping stdin sends EOF to the worker's `readLine` loop.
drop(worker.stdin);
let _ = worker.child.wait();
}
}
}
impl BuildPool for JavacPool {
fn name(&self) -> &'static str {
"javac"
}
fn compile_batch(&self, workdir: &Path, args: &[String]) -> PoolCompileResult {
self.compile_with_worker(workdir, args)
}
fn is_healthy(&self) -> bool {
match self.inner.lock() {
Ok(g) => g.is_some(),
Err(_) => false,
}
}
}
fn bootstrap_dir_for(toolchain_id: &str) -> Result<PathBuf, String> {
if let Ok(custom) = std::env::var("NYX_BUILD_POOL_DIR") {
return Ok(PathBuf::from(custom).join("javac").join(toolchain_id));
}
let base = directories::ProjectDirs::from("dev", "nyx", "nyx")
.ok_or_else(|| "javac-pool: no cache dir on this platform".to_owned())?;
Ok(base
.cache_dir()
.join("dynamic")
.join("build-pool")
.join("javac")
.join(toolchain_id))
}
/// Drop `NyxJavacWorker.java` + compile `NyxJavacWorker.class` into
/// `dir` if they are not already present. Always re-writes the source
/// when the on-disk copy differs from the embedded one so a binary
/// upgrade picks up worker fixes without manual cache eviction.
///
/// The bootstrap dir is shared across every concurrent `nyx` process on
/// the host, so the compile-and-publish step is hardened against the
/// cross-process race that otherwise hands a half-written
/// `NyxJavacWorker.class` to a peer process spawning its worker (which
/// then fails to start, manifesting downstream as a flaky build):
///
/// - The publish is **atomic**: `javac` writes into a private,
/// pid-scoped staging dir and the finished class is `rename`d into
/// place. A concurrent reader sees either the previous complete
/// class or the new one, never a partial file. The old class is
/// never `remove`d first.
/// - Compiles are **serialised** on a `flock(2)` over `.bootstrap.lock`
/// so two processes never run `javac` into the same staging at once
/// and a waiter re-checks the now-published class instead of
/// recompiling.
fn ensure_worker_compiled(dir: &Path) -> Result<(), String> {
let src_path = dir.join(WORKER_FILENAME);
// Fast path: a complete class set already matches the current worker
// source. Checked before taking the cross-process lock so steady
// state stays lock-free.
if worker_class_ready(dir) {
return Ok(());
}
// Serialise the compile-and-publish across processes sharing `dir`.
let _lock = BootstrapLock::acquire(dir)?;
// Re-check under the lock: another process may have published a good
// class set while we were waiting on the lock.
if worker_class_ready(dir) {
return Ok(());
}
// Publish the source (idempotent) so cache inspectors can see what
// the class was built from.
std::fs::write(&src_path, WORKER_SOURCE)
.map_err(|e| format!("javac-pool: write worker source: {e}"))?;
// Compile into a private staging dir, then atomically publish the
// class files into place.
let staging = dir.join(format!(".compile-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&staging);
std::fs::create_dir_all(&staging).map_err(|e| format!("javac-pool: mkdir staging: {e}"))?;
let javac = std::env::var("NYX_JAVAC_BIN").unwrap_or_else(|_| "javac".to_owned());
let compiled = Command::new(&javac)
// Pin the source charset so the bootstrap compile is independent of
// the host locale (a `C`/`POSIX` CI runner defaults `javac` to
// `US-ASCII` and would reject any non-ASCII byte in the worker
// source). Mirrors the harness-compile pin in `build_sandbox`.
.arg("-encoding")
.arg("UTF-8")
.arg("-d")
.arg(&staging)
.arg(&src_path)
.env_clear()
.env("PATH", std::env::var("PATH").unwrap_or_default())
.env("HOME", std::env::var("HOME").unwrap_or_default())
.output();
let output = match compiled {
Ok(o) => o,
Err(e) => {
let _ = std::fs::remove_dir_all(&staging);
return Err(format!("javac-pool: spawn javac: {e}"));
}
};
if !output.status.success() {
let _ = std::fs::remove_dir_all(&staging);
return Err(format!(
"javac-pool: bootstrap compile failed: {}",
String::from_utf8_lossy(&output.stderr),
));
}
let publish = publish_class_set(&staging, dir);
let _ = std::fs::remove_dir_all(&staging);
publish
}
/// Move every `.class` file `javac` emitted from the private `staging`
/// dir into the shared `dir`, then write the manifest last.
///
/// The worker source compiles to the top-level `NyxJavacWorker.class`
/// plus the nested `NyxJavacWorker$Request` / `NyxJavacWorker$Parser`
/// classes. Every one of them must land in `dir` (the worker JVM's
/// classpath), or the worker hits `NoClassDefFoundError` the first time
/// it touches a nested class -- which surfaced downstream as a bogus
/// `nyx-javac-worker: parse error: NyxJavacWorker$Parser`.
///
/// Renames are same-filesystem (staging is a child of `dir`) so each is
/// atomic. The manifest is written last via a temp-then-rename, so a
/// concurrent peer on the lock-free fast path sees either no manifest
/// (and serialises on the lock) or a complete one whose every named
/// class is already in place.
fn publish_class_set(staging: &Path, dir: &Path) -> Result<(), String> {
let entries =
std::fs::read_dir(staging).map_err(|e| format!("javac-pool: read staging dir: {e}"))?;
let mut names: Vec<String> = Vec::new();
for entry in entries {
let path = entry
.map_err(|e| format!("javac-pool: read staging entry: {e}"))?
.path();
if path.extension().is_none_or(|x| x != "class") {
continue;
}
let name = match path.file_name().and_then(|n| n.to_str()) {
Some(n) => n.to_owned(),
None => continue,
};
std::fs::rename(&path, dir.join(&name))
.map_err(|e| format!("javac-pool: publish {name}: {e}"))?;
names.push(name);
}
if names.is_empty() {
return Err("javac-pool: bootstrap compile produced no .class files".to_owned());
}
// Refuse to publish (and to write the readiness-signalling manifest) a
// set missing any class the worker loads at runtime. Fail loud here
// rather than leave a half-set the worker would die on later.
for required in WORKER_CLASS_FILES {
if !names.iter().any(|n| n == required) {
return Err(format!(
"javac-pool: bootstrap compile missing required class {required}; got {names:?}",
));
}
}
// Write the manifest atomically (temp + rename) so it appears in one
// step after every class is already published.
let manifest = dir.join(WORKER_MANIFEST);
let tmp = dir.join(format!("{WORKER_MANIFEST}.{}", std::process::id()));
std::fs::write(&tmp, names.join("\n"))
.map_err(|e| format!("javac-pool: write manifest: {e}"))?;
std::fs::rename(&tmp, &manifest).map_err(|e| {
let _ = std::fs::remove_file(&tmp);
format!("javac-pool: publish manifest: {e}")
})?;
Ok(())
}
/// True when `dir` holds a complete, non-empty class set built from the
/// current embedded `WORKER_SOURCE`: the source matches, the manifest is
/// present, and every class the manifest names exists and is non-empty.
fn worker_class_ready(dir: &Path) -> bool {
if std::fs::read_to_string(dir.join(WORKER_FILENAME))
.ok()
.as_deref()
!= Some(WORKER_SOURCE)
{
return false;
}
// The manifest is written last by `publish_class_set`, so its presence
// is the "publish finished" barrier: a reader that sees it knows no
// peer is mid-rename. Absence forces the cross-process lock path.
if std::fs::metadata(dir.join(WORKER_MANIFEST)).is_err() {
return false;
}
// Completeness is judged against the fixed required set, never against
// the manifest's lines -- a stale or partial manifest must not be able
// to vouch for classes it simply fails to name.
for name in WORKER_CLASS_FILES {
let present = std::fs::metadata(dir.join(name))
.map(|m| m.is_file() && m.len() > 0)
.unwrap_or(false);
if !present {
return false;
}
}
true
}
/// Cross-process advisory lock guarding the shared bootstrap dir's
/// compile-and-publish step. The held lock file lives at
/// `<dir>/.bootstrap.lock`; the `flock(2)` releases when the guard (and
/// thus the file) drops.
struct BootstrapLock {
_file: File,
}
impl BootstrapLock {
fn acquire(dir: &Path) -> Result<Self, String> {
let lock_path = dir.join(".bootstrap.lock");
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&lock_path)
.map_err(|e| format!("javac-pool: open bootstrap lock: {e}"))?;
lock_file_exclusive(&file).map_err(|e| format!("javac-pool: bootstrap lock: {e}"))?;
Ok(BootstrapLock { _file: file })
}
}
#[cfg(unix)]
fn lock_file_exclusive(file: &File) -> std::io::Result<()> {
use std::os::fd::AsRawFd;
unsafe extern "C" {
fn flock(fd: i32, operation: i32) -> i32;
}
const LOCK_EX: i32 = 2;
loop {
// SAFETY: `file.as_raw_fd()` is a live fd owned by `file`; `flock`
// only reads the scalar args and we check the return value.
let ret = unsafe { flock(file.as_raw_fd(), LOCK_EX) };
if ret == 0 {
return Ok(());
}
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::Interrupted {
continue;
}
return Err(err);
}
}
#[cfg(not(unix))]
fn lock_file_exclusive(_file: &File) -> std::io::Result<()> {
Ok(())
}
fn spawn_worker(dir: &Path) -> Result<Worker, String> {
let java = std::env::var("NYX_JAVA_BIN").unwrap_or_else(|_| "java".to_owned());
let mut child = Command::new(&java)
// The worker is tiny -- keep the JVM frugal so the pool
// overhead stays well below the per-finding cost it
// replaces.
.arg("-Xss256k")
.arg("-XX:+UseSerialGC")
.arg("-cp")
.arg(dir)
.arg(WORKER_CLASS)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.env_clear()
.env("PATH", std::env::var("PATH").unwrap_or_default())
.env("HOME", std::env::var("HOME").unwrap_or_default())
.spawn()
.map_err(|e| format!("javac-pool: spawn java: {e}"))?;
let stdin = child
.stdin
.take()
.ok_or_else(|| "javac-pool: missing stdin".to_owned())?;
let stdout = child
.stdout
.take()
.ok_or_else(|| "javac-pool: missing stdout".to_owned())?;
let mut stdout = BufReader::new(stdout);
let banner =
match read_line_with_timeout(&mut child, &mut stdout, WORKER_READY_TIMEOUT, "read banner")?
{
Some(line) => line,
None => {
let _ = child.kill();
let stderr_tail = drain_stderr(&mut child);
return Err(format!(
"javac-pool: worker closed stdout before readiness; stderr: {stderr_tail}",
));
}
};
if !banner.contains("\"ready\":true") {
// Drain stderr for diagnostic context, then bail.
let _ = child.kill();
let stderr_tail = drain_stderr(&mut child);
return Err(format!(
"javac-pool: worker did not announce readiness; got {banner:?}; stderr: {stderr_tail}",
));
}
Ok(Worker {
child,
stdin,
stdout,
next_id: 0,
})
}
fn drain_stderr(child: &mut Child) -> String {
use std::io::Read;
let mut buf = String::new();
if let Some(mut e) = child.stderr.take() {
// Best-effort, non-blocking-ish.
let _ = e.read_to_string(&mut buf);
}
buf
}
fn read_line_with_timeout(
child: &mut Child,
stdout: &mut BufReader<ChildStdout>,
timeout: Duration,
context: &str,
) -> Result<Option<String>, String> {
let (tx, rx) = mpsc::channel();
thread::scope(|scope| {
scope.spawn(move || {
let mut line = String::new();
let result = stdout.read_line(&mut line).map(|n| (n, line));
let _ = tx.send(result);
});
match rx.recv_timeout(timeout) {
Ok(Ok((0, _))) => Ok(None),
Ok(Ok((_n, line))) => Ok(Some(line)),
Ok(Err(e)) => Err(format!("javac-pool: {context} failed: {e}")),
Err(mpsc::RecvTimeoutError::Timeout) => {
let _ = child.kill();
Err(format!("javac-pool: {context} timed out after {timeout:?}"))
}
Err(mpsc::RecvTimeoutError::Disconnected) => {
Err(format!("javac-pool: {context} reader disconnected"))
}
}
})
}
fn build_request(id: u64, workdir: &Path, args: &[String]) -> String {
let mut s = String::with_capacity(128 + args.iter().map(|a| a.len() + 4).sum::<usize>());
s.push_str("{\"id\":\"");
s.push_str(&id.to_string());
s.push_str("\",\"cwd\":");
append_json_string(&mut s, &workdir.to_string_lossy());
s.push_str(",\"args\":[");
for (i, a) in args.iter().enumerate() {
if i > 0 {
s.push(',');
}
append_json_string(&mut s, a);
}
s.push_str("]}\n");
s
}
fn append_json_string(out: &mut String, s: &str) {
out.push('"');
for c in s.chars() {
match c {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out.push('"');
}
/// Extract `(success, stderr)` from a worker JSON response line.
fn parse_response(line: &str) -> Option<(bool, String)> {
let response: JavacWorkerResponse = serde_json::from_str(line).ok()?;
let stderr =
decode_b64(&response.stderr_b64).unwrap_or_else(|| "<unable to decode stderr>".to_owned());
Some((response.success, stderr))
}
#[derive(Debug, Deserialize)]
struct JavacWorkerResponse {
success: bool,
#[serde(default)]
stderr_b64: String,
}
/// Tiny RFC 4648 base64 decoder. Used only for the worker's
/// `stderr_b64` field so we can carry raw bytes through the JSON
/// envelope without dragging in a base64 crate.
fn decode_b64(s: &str) -> Option<String> {
static ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut lookup = [0xffu8; 256];
for (i, &b) in ALPHABET.iter().enumerate() {
lookup[b as usize] = i as u8;
}
let bytes: Vec<u8> = s.bytes().filter(|b| !b.is_ascii_whitespace()).collect();
let mut out = Vec::with_capacity(bytes.len() / 4 * 3);
for chunk in bytes.chunks(4) {
if chunk.len() < 2 {
return None;
}
let mut vals = [0u8; 4];
let mut pads = 0;
for (i, &b) in chunk.iter().enumerate() {
if b == b'=' {
pads += 1;
vals[i] = 0;
} else {
let v = lookup[b as usize];
if v == 0xff {
return None;
}
vals[i] = v;
}
}
let triple = ((vals[0] as u32) << 18)
| ((vals[1] as u32) << 12)
| ((vals[2] as u32) << 6)
| (vals[3] as u32);
out.push(((triple >> 16) & 0xff) as u8);
if pads < 2 {
out.push(((triple >> 8) & 0xff) as u8);
}
if pads < 1 {
out.push((triple & 0xff) as u8);
}
}
String::from_utf8(out).ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_envelope_escapes_specials() {
let s = build_request(
7,
Path::new("/tmp/x"),
&["a\"b".to_owned(), "c\\d".to_owned()],
);
assert!(s.contains("\"id\":\"7\""));
assert!(s.contains("\"cwd\":\"/tmp/x\""));
assert!(s.contains("\"a\\\"b\""));
assert!(s.contains("\"c\\\\d\""));
assert!(s.ends_with("]}\n"));
}
#[test]
fn parse_response_success() {
let (ok, err) =
parse_response("{\"id\":\"0\",\"success\":true,\"stderr_b64\":\"\"}\n").unwrap();
assert!(ok);
assert!(err.is_empty());
}
#[test]
fn parse_response_failure_decodes_stderr() {
// "boom" -> base64 "Ym9vbQ=="
let (ok, err) =
parse_response("{\"id\":\"1\",\"success\":false,\"stderr_b64\":\"Ym9vbQ==\"}\n")
.unwrap();
assert!(!ok);
assert_eq!(err, "boom");
}
#[test]
fn parse_response_rejects_off_shape() {
assert!(parse_response("not json").is_none());
// Missing success field.
assert!(parse_response("{\"id\":\"0\",\"stderr_b64\":\"\"}").is_none());
}
#[test]
fn parse_response_accepts_reordered_fields() {
let (ok, err) =
parse_response("{\"stderr_b64\":\"YQ==\",\"success\":true,\"id\":\"7\"}\n").unwrap();
assert!(ok);
assert_eq!(err, "a");
}
#[test]
fn b64_decode_roundtrip() {
for (raw, encoded) in &[
("", ""),
("a", "YQ=="),
("ab", "YWI="),
("abc", "YWJj"),
("hello world", "aGVsbG8gd29ybGQ="),
] {
assert_eq!(decode_b64(encoded).as_deref(), Some(*raw));
}
}
#[test]
fn worker_class_ready_rejects_truncated_or_mismatched() {
let tmp = tempfile::TempDir::new().unwrap();
let dir = tmp.path();
let src = dir.join(WORKER_FILENAME);
let main_class = dir.join(format!("{WORKER_CLASS}.class"));
let parser = dir.join(format!("{WORKER_CLASS}$Parser.class"));
let request = dir.join(format!("{WORKER_CLASS}$Request.class"));
let manifest = dir.join(WORKER_MANIFEST);
let manifest_body = format!(
"{WORKER_CLASS}.class\n{WORKER_CLASS}$Parser.class\n{WORKER_CLASS}$Request.class"
);
// Nothing on disk yet.
assert!(!worker_class_ready(dir));
// Matching source but no class / manifest.
std::fs::write(&src, WORKER_SOURCE).unwrap();
assert!(!worker_class_ready(dir));
// Top-level class + manifest present but the nested classes are
// missing -- the stale-cache shape an older binary left behind.
std::fs::write(&main_class, b"\xca\xfe\xba\xbe").unwrap();
std::fs::write(&manifest, &manifest_body).unwrap();
assert!(!worker_class_ready(dir));
// A zero-byte nested class (the corruption shape a racing peer can
// leave behind) must not count as ready.
std::fs::write(&parser, b"").unwrap();
std::fs::write(&request, b"\xca\xfe\xba\xbe").unwrap();
assert!(!worker_class_ready(dir));
// Every required class non-empty with matching source is ready.
std::fs::write(&parser, b"\xca\xfe\xba\xbe").unwrap();
assert!(worker_class_ready(dir));
// A missing manifest invalidates an otherwise-complete class set.
std::fs::remove_file(&manifest).unwrap();
assert!(!worker_class_ready(dir));
std::fs::write(&manifest, &manifest_body).unwrap();
assert!(worker_class_ready(dir));
// Stale source invalidates an otherwise-present class set.
std::fs::write(&src, "// not the worker source").unwrap();
assert!(!worker_class_ready(dir));
}
#[test]
fn worker_class_ready_rejects_manifest_that_omits_nested_classes() {
// The exact stale-cache shape that produced
// `nyx-javac-worker: parse error: NyxJavacWorker$Parser` on Linux:
// a self-consistent manifest that simply does not name the nested
// classes, with only the top-level class on disk. The old guard
// iterated the manifest's lines and so trusted this; readiness must
// now reject it because the fixed required set is incomplete.
let tmp = tempfile::TempDir::new().unwrap();
let dir = tmp.path();
std::fs::write(dir.join(WORKER_FILENAME), WORKER_SOURCE).unwrap();
std::fs::write(
dir.join(format!("{WORKER_CLASS}.class")),
b"\xca\xfe\xba\xbe",
)
.unwrap();
// Manifest names only the top-level class -- exactly what poisoned
// the persisted bootstrap cache.
std::fs::write(dir.join(WORKER_MANIFEST), format!("{WORKER_CLASS}.class")).unwrap();
assert!(
!worker_class_ready(dir),
"a manifest omitting the nested classes must not satisfy readiness",
);
// Drop in the nested classes the worker actually loads -> ready.
std::fs::write(
dir.join(format!("{WORKER_CLASS}$Parser.class")),
b"\xca\xfe\xba\xbe",
)
.unwrap();
std::fs::write(
dir.join(format!("{WORKER_CLASS}$Request.class")),
b"\xca\xfe\xba\xbe",
)
.unwrap();
assert!(worker_class_ready(dir));
}
#[test]
fn ensure_worker_compiled_heals_partial_cache() {
// End-to-end heal: seed the exact poisoned-cache shape that broke
// Linux (top-level class + a one-line manifest, nested classes
// absent) and confirm `ensure_worker_compiled` recompiles a full,
// loadable class set instead of trusting the stale manifest.
let javac = std::env::var("NYX_JAVAC_BIN").unwrap_or_else(|_| "javac".to_owned());
let have_javac = std::process::Command::new(&javac)
.arg("-version")
.output()
.map(|o| o.status.success())
.unwrap_or(false);
if !have_javac {
return; // No JDK on this host: nothing to recompile with.
}
let tmp = tempfile::TempDir::new().unwrap();
let dir = tmp.path();
std::fs::write(dir.join(WORKER_FILENAME), WORKER_SOURCE).unwrap();
std::fs::write(
dir.join(format!("{WORKER_CLASS}.class")),
b"\xca\xfe\xba\xbe",
)
.unwrap();
std::fs::write(dir.join(WORKER_MANIFEST), format!("{WORKER_CLASS}.class")).unwrap();
assert!(
!worker_class_ready(dir),
"poisoned cache must read not-ready"
);
ensure_worker_compiled(dir).expect("recompile heals the cache");
assert!(worker_class_ready(dir), "healed cache must read ready");
for cls in WORKER_CLASS_FILES {
let meta = std::fs::metadata(dir.join(cls)).expect("class published");
assert!(meta.len() > 0, "{cls} must be a real (non-empty) class");
}
}
#[test]
fn worker_class_files_match_javac_output() {
// Guards `WORKER_CLASS_FILES` against drift: compile the embedded
// worker source and assert the emitted `.class` set is exactly the
// pinned required set, so a future nested type added to the worker
// can't silently fall outside the readiness gate.
let javac = std::env::var("NYX_JAVAC_BIN").unwrap_or_else(|_| "javac".to_owned());
let have_javac = std::process::Command::new(&javac)
.arg("-version")
.output()
.map(|o| o.status.success())
.unwrap_or(false);
if !have_javac {
return; // JRE-only / no JDK: nothing to compile against.
}
let tmp = tempfile::TempDir::new().unwrap();
let src = tmp.path().join(WORKER_FILENAME);
std::fs::write(&src, WORKER_SOURCE).unwrap();
let out = tmp.path().join("out");
std::fs::create_dir_all(&out).unwrap();
let status = std::process::Command::new(&javac)
.arg("-encoding")
.arg("UTF-8")
.arg("-d")
.arg(&out)
.arg(&src)
.status()
.expect("spawn javac");
assert!(status.success(), "worker source must compile");
let mut emitted: Vec<String> = std::fs::read_dir(&out)
.unwrap()
.filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().into_owned())
.filter(|n| n.ends_with(".class"))
.collect();
emitted.sort();
let mut expected: Vec<String> =
WORKER_CLASS_FILES.iter().map(|s| (*s).to_owned()).collect();
expected.sort();
assert_eq!(
emitted, expected,
"WORKER_CLASS_FILES must mirror the worker's javac output",
);
}
#[test]
fn publish_class_set_moves_every_class_and_writes_manifest() {
let tmp = tempfile::TempDir::new().unwrap();
let dir = tmp.path();
let staging = dir.join(".compile-test");
std::fs::create_dir_all(&staging).unwrap();
// Simulate javac output: top-level + nested classes plus a
// non-class artifact that must be ignored.
std::fs::write(staging.join("NyxJavacWorker.class"), b"\xca\xfe\xba\xbe").unwrap();
std::fs::write(
staging.join("NyxJavacWorker$Parser.class"),
b"\xca\xfe\xba\xbe",
)
.unwrap();
std::fs::write(
staging.join("NyxJavacWorker$Request.class"),
b"\xca\xfe\xba\xbe",
)
.unwrap();
std::fs::write(staging.join("notes.txt"), b"ignore me").unwrap();
publish_class_set(&staging, dir).expect("publish");
for cls in [
"NyxJavacWorker.class",
"NyxJavacWorker$Parser.class",
"NyxJavacWorker$Request.class",
] {
assert!(dir.join(cls).is_file(), "{cls} must be published");
}
// The non-class file stays in staging (not published).
assert!(!dir.join("notes.txt").exists());
let manifest = std::fs::read_to_string(dir.join(WORKER_MANIFEST)).unwrap();
let listed: Vec<&str> = manifest.lines().collect();
assert_eq!(listed.len(), 3, "manifest lists all 3 classes: {listed:?}");
assert!(listed.contains(&"NyxJavacWorker$Parser.class"));
}
#[test]
fn bootstrap_lock_is_reentrant_across_sequential_acquires() {
// The flock is released when the guard drops, so back-to-back
// acquires from the same process succeed without deadlock.
let dir = tempfile::TempDir::new().unwrap();
{
let _g = BootstrapLock::acquire(dir.path()).expect("first acquire");
}
let _g = BootstrapLock::acquire(dir.path()).expect("second acquire");
assert!(dir.path().join(".bootstrap.lock").exists());
}
}

View file

@ -0,0 +1,256 @@
// SPDX-License-Identifier: GPL-3.0-or-later
//
// Long-lived javac worker bundled with nyx-scanner. The Rust pool side
// compiles + spawns this once per toolchain id; subsequent harness
// compiles run in-process via ToolProvider#getSystemJavaCompiler so the
// JVM cold-start cost is amortised across every harness in a verify run.
//
// Wire format: newline-terminated UTF-8 JSON, one request per line:
// {"id":"0","cwd":"/path/to/workdir","args":["-d","/tmp/x","Foo.java"]}\n
//
// Response: newline-terminated UTF-8 JSON, one per request:
// {"id":"0","success":true,"stderr_b64":"<base64 of javac stderr>"}\n
//
// stderr is base64-encoded so it never embeds raw newlines or quotes
// inside the JSON envelope -- keeps the parser on both sides tiny.
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;
public class NyxJavacWorker {
public static void main(String[] argv) throws Exception {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
if (compiler == null) {
// JRE without javac (rare on dev boxes, possible on slim CI
// images). Signal the Rust side so it falls back to the
// direct-spawn legacy path.
System.err.println("nyx-javac-worker: no system Java compiler (JRE-only install?)");
System.exit(2);
}
BufferedReader in = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8));
PrintStream out = new PrintStream(System.out, true, StandardCharsets.UTF_8);
// Banner line. The Rust side reads this first so it knows the
// worker is live before it queues any compile requests.
out.println("{\"ready\":true}");
out.flush();
String line;
while ((line = in.readLine()) != null) {
line = line.trim();
if (line.isEmpty()) continue;
Request req;
try {
req = parse(line);
} catch (Throwable t) {
// Malformed request -- emit an error response keyed on
// an empty id so the Rust side can at least surface it.
writeResponse(out, "", false, ("nyx-javac-worker: parse error: " + t.getMessage()).getBytes(StandardCharsets.UTF_8));
continue;
}
ByteArrayOutputStream errBuf = new ByteArrayOutputStream();
PrintStream errStream = new PrintStream(errBuf, true, StandardCharsets.UTF_8);
int rc;
try {
String[] args = req.args.toArray(new String[0]);
if (req.cwd != null && !req.cwd.isEmpty()) {
// The JDK compiler API has no per-task cwd switch,
// so we rewrite relative args. The harness build
// already supplies absolute paths via the Rust side,
// but we still set user.dir defensively so any
// relative -d / -cp / source-path entries resolve
// against the requested workdir rather than the
// worker JVM's launch directory.
System.setProperty("user.dir", req.cwd);
}
rc = compiler.run(null, null, errStream, args);
} catch (Throwable t) {
t.printStackTrace(errStream);
rc = 1;
}
boolean success = (rc == 0);
writeResponse(out, req.id, success, errBuf.toByteArray());
}
}
private static void writeResponse(PrintStream out, String id, boolean success, byte[] stderr) {
String b64 = Base64.getEncoder().encodeToString(stderr);
StringBuilder sb = new StringBuilder(64 + b64.length());
sb.append("{\"id\":");
appendJsonString(sb, id);
sb.append(",\"success\":").append(success);
sb.append(",\"stderr_b64\":\"").append(b64).append("\"}");
out.println(sb);
out.flush();
}
private static void appendJsonString(StringBuilder sb, String s) {
sb.append('"');
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
switch (c) {
case '\\': sb.append("\\\\"); break;
case '"': sb.append("\\\""); break;
case '\n': sb.append("\\n"); break;
case '\r': sb.append("\\r"); break;
case '\t': sb.append("\\t"); break;
default:
if (c < 0x20) {
sb.append(String.format("\\u%04x", (int) c));
} else {
sb.append(c);
}
}
}
sb.append('"');
}
private static final class Request {
String id = "";
String cwd = "";
List<String> args = new ArrayList<>();
}
private static Request parse(String s) {
Parser p = new Parser(s);
Request r = new Request();
p.skipWs();
p.expect('{');
p.skipWs();
if (p.peek() == '}') {
p.next();
return r;
}
while (true) {
p.skipWs();
String key = p.parseString();
p.skipWs();
p.expect(':');
p.skipWs();
if (key.equals("id")) {
r.id = p.parseString();
} else if (key.equals("cwd")) {
r.cwd = p.parseString();
} else if (key.equals("args")) {
p.expect('[');
p.skipWs();
if (p.peek() != ']') {
while (true) {
p.skipWs();
r.args.add(p.parseString());
p.skipWs();
if (p.peek() == ',') { p.next(); continue; }
break;
}
}
p.skipWs();
p.expect(']');
} else {
skipValue(p);
}
p.skipWs();
if (p.peek() == ',') { p.next(); continue; }
break;
}
p.skipWs();
p.expect('}');
return r;
}
private static void skipValue(Parser p) {
p.skipWs();
char c = p.peek();
if (c == '"') { p.parseString(); }
else if (c == '[') {
p.next();
p.skipWs();
if (p.peek() != ']') {
while (true) {
skipValue(p); p.skipWs();
if (p.peek() == ',') { p.next(); continue; }
break;
}
}
p.skipWs();
p.expect(']');
} else if (c == '{') {
p.next();
p.skipWs();
if (p.peek() != '}') {
while (true) {
p.skipWs();
p.parseString();
p.skipWs();
p.expect(':');
skipValue(p);
p.skipWs();
if (p.peek() == ',') { p.next(); continue; }
break;
}
}
p.skipWs();
p.expect('}');
} else {
int start = p.pos;
while (p.pos < p.s.length() && "0123456789.-+eEtrufalsn".indexOf(p.s.charAt(p.pos)) >= 0) {
p.pos++;
}
if (p.pos == start) {
throw new RuntimeException("bad value at " + p.pos);
}
}
}
private static final class Parser {
final String s; int pos = 0;
Parser(String s) { this.s = s; }
char peek() { return s.charAt(pos); }
char next() { return s.charAt(pos++); }
void skipWs() { while (pos < s.length() && Character.isWhitespace(s.charAt(pos))) pos++; }
void expect(char c) {
if (pos >= s.length() || s.charAt(pos) != c) {
throw new RuntimeException("expected '" + c + "' at " + pos + " of " + s);
}
pos++;
}
String parseString() {
expect('"');
StringBuilder sb = new StringBuilder();
while (pos < s.length()) {
char c = s.charAt(pos++);
if (c == '"') return sb.toString();
if (c == '\\') {
char e = s.charAt(pos++);
switch (e) {
case '"': sb.append('"'); break;
case '\\': sb.append('\\'); break;
case '/': sb.append('/'); break;
case 'b': sb.append('\b'); break;
case 'f': sb.append('\f'); break;
case 'n': sb.append('\n'); break;
case 'r': sb.append('\r'); break;
case 't': sb.append('\t'); break;
case 'u': {
String hex = s.substring(pos, pos + 4);
pos += 4;
sb.append((char) Integer.parseInt(hex, 16));
break;
}
default: throw new RuntimeException("bad escape \\" + e);
}
} else {
sb.append(c);
}
}
throw new RuntimeException("unterminated string");
}
}
}

View file

@ -0,0 +1,340 @@
//! Build pools: long-lived compiler / toolchain daemons shared across many
//! per-finding harness builds.
//!
//! The naive `prepare_*` path in [`crate::dynamic::build_sandbox`] spawns a
//! fresh `javac` / `tsc` / `cargo build` subprocess for every finding the
//! verifier touches. Cold-start dominates the cost: `javac` alone burns
//! ~700ms before it has read a single source. A 50-harness OWASP run pays
//! that 50× — > 30s of pure JVM startup.
//!
//! A `BuildPool` is a long-running worker process (or in-process service)
//! that compiles batches of harness sources in a single toolchain instance.
//! The per-harness wall-clock collapses to milliseconds once the pool is
//! warm.
//!
//! # Lifecycle
//!
//! `OnceLock<Arc<P>>` per toolchain id, lazily spawned on first request.
//! Pools live for the rest of the process; the OS reaps them on exit.
//! Crashes are non-fatal: callers fall back to the legacy direct-spawn path
//! via [`BuildPool::is_healthy`] and a re-spawn on the next call.
//!
//! # Future-language plug-in
//!
//! Per-language sub-modules (`java.rs`, eventually `node.rs`, `python.rs`,
//! …) implement the [`BuildPool`] trait. The harness build dispatcher in
//! [`crate::dynamic::build_sandbox`] reads `NYX_DYNAMIC_BUILD_POOL` and
//! routes each request to the matching pool when enabled.
use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Duration;
pub mod c;
pub mod cpp;
pub mod go;
pub mod java;
pub mod node;
pub mod php;
pub mod python;
pub mod ruby;
pub mod rust;
/// Outcome of a single batched compile request.
#[derive(Debug)]
pub struct PoolCompileResult {
/// `true` when the toolchain reported a clean compile.
pub success: bool,
/// Toolchain stderr — surfaced as `BuildError::BuildFailed` upstream
/// when `success == false`.
pub stderr: String,
/// Wall-clock for the in-pool compile step (excludes any IPC / queue
/// wait time). Useful for telemetry; callers may ignore.
pub duration: Duration,
}
/// Common contract for every per-language build pool.
///
/// Implementations are expected to be `Send + Sync` so an `Arc<dyn BuildPool>`
/// can be cached in a static `OnceLock` and shared across rayon worker
/// threads.
pub trait BuildPool: Send + Sync {
/// Stable identifier — used in log lines + telemetry so an operator
/// can correlate a pool warmup with the harness that triggered it.
fn name(&self) -> &'static str;
/// Compile every source file under `workdir` matching the pool's
/// language convention. On success the toolchain has written
/// artefacts back into `workdir` (or wherever the pool's contract
/// dictates).
fn compile_batch(&self, workdir: &Path, args: &[String]) -> PoolCompileResult;
/// Cheap health check — when this returns `false`, the harness build
/// dispatcher falls back to the direct-spawn legacy path and tears
/// down the cached handle so the next request triggers a re-spawn.
fn is_healthy(&self) -> bool;
}
/// Languages that ship a [`BuildPool`] implementation and are therefore
/// enabled by default. Phase 22 shipped `java`; Phase 23 (Track O.1) adds
/// the remaining eight, so every supported language now has a warm fast path
/// unless an operator opts out via `NYX_DYNAMIC_BUILD_POOL=<lang>=0`.
const POOL_ENABLED_LANGS: &[&str] = &[
"java", "node", "python", "php", "ruby", "go", "rust", "c", "cpp",
];
/// Parse the `NYX_DYNAMIC_BUILD_POOL` env var.
///
/// Format is a comma-separated list of `lang=bit` entries: `java=1,node=0`.
/// A missing language returns the default: `true` for every language that
/// ships a pool (see `POOL_ENABLED_LANGS`), `false` otherwise.
pub fn is_pool_enabled(lang: &str) -> bool {
let default = POOL_ENABLED_LANGS.contains(&lang);
let raw = match std::env::var("NYX_DYNAMIC_BUILD_POOL") {
Ok(v) => v,
Err(_) => return default,
};
for entry in raw.split(',') {
let entry = entry.trim();
if entry.is_empty() {
continue;
}
let (k, v) = match entry.split_once('=') {
Some(kv) => kv,
None => continue,
};
if k.trim().eq_ignore_ascii_case(lang) {
return matches!(v.trim(), "1" | "true" | "TRUE" | "yes" | "on");
}
}
default
}
/// Shared root for a pool's persistent caches (sccache dir, shared
/// `GOCACHE`, opcache file-cache, Bootsnap cache, shared venvs, …).
///
/// Honours `NYX_BUILD_POOL_DIR` so tests can redirect the cache into a
/// `TempDir`; otherwise falls back to the platform cache dir, mirroring
/// the javac pool's layout under `dynamic/build-pool/`.
///
/// Returns `None` only when neither the env override nor a platform cache
/// dir is available — callers treat that as "pool unavailable" and fall
/// back to the legacy direct-spawn build path.
pub(crate) fn pool_cache_dir(lang: &str, sub: &str) -> Option<PathBuf> {
let custom = std::env::var("NYX_BUILD_POOL_DIR").ok().map(PathBuf::from);
let base = if let Some(custom) = custom.clone() {
custom
} else {
directories::ProjectDirs::from("dev", "nyx", "nyx")?
.cache_dir()
.join("dynamic")
.join("build-pool")
};
let dir = base.join(lang).join(sub);
if ensure_writable_dir(&dir).is_some() {
return Some(dir);
}
if custom.is_some() {
return None;
}
let fallback = std::env::temp_dir()
.join("nyx")
.join("dynamic")
.join("build-pool")
.join(lang)
.join(sub);
ensure_writable_dir(&fallback)
}
fn ensure_writable_dir(dir: &Path) -> Option<PathBuf> {
std::fs::create_dir_all(dir).ok()?;
let probe = dir.join(format!(".nyx-write-probe-{}", std::process::id()));
std::fs::write(&probe, b"ok").ok()?;
let _ = std::fs::remove_file(probe);
Some(dir.to_path_buf())
}
/// Construct a `Command` for `bin` with a scrubbed environment, matching
/// the isolation envelope every legacy `prepare_*` build uses: `env_clear`
/// plus an inherited `PATH` + `HOME` only. Pools layer their cache env
/// (`CARGO_TARGET_DIR`, `CCACHE_DIR`, `GOCACHE`, …) on top of this.
pub(crate) fn base_command(bin: &str) -> Command {
let mut cmd = Command::new(bin);
let tmp = build_temp_dir();
cmd.env_clear()
.env("PATH", std::env::var("PATH").unwrap_or_default())
.env("HOME", std::env::var("HOME").unwrap_or_default())
.env("TMPDIR", &tmp)
.env("TMP", &tmp)
.env("TEMP", &tmp);
cmd
}
fn build_temp_dir() -> PathBuf {
let dir = std::env::temp_dir().join("nyx-build-tmp");
if std::fs::create_dir_all(&dir).is_ok() {
return dir;
}
std::env::temp_dir()
}
/// Hermetic Bundler / RubyGems environment pinned to a writable per-workdir
/// vendor directory.
///
/// Points `GEM_HOME` and `BUNDLE_PATH` at `<workdir>/vendor/bundle` so every
/// gem *install* lands in a directory the current user owns. This is the
/// load-bearing fix for the harness build invoking `sudo`: legacy Bundler
/// (1.x) shells out to `sudo` when the install target — the root-owned system
/// gem dir (`/Library/Ruby/Gems/...`) — is not writable, which then blocks on
/// a terminal password prompt (`sudo: a terminal is required to read the
/// password`). With a writable target there is no privilege escalation and
/// no prompt, ever.
///
/// `GEM_PATH` is deliberately left unset so RubyGems still includes the system
/// gem path when *resolving* (paired with `BUNDLE_DISABLE_SHARED_GEMS=false`),
/// letting an already-installed gem satisfy the Gemfile without a network
/// fetch — while installs of missing gems still land in the writable vendor
/// dir. `BUNDLE_APP_CONFIG` keeps Bundler's per-project config writable and
/// inside the workdir.
///
/// Returned as env pairs (not applied to a `Command` here) so both the pooled
/// path ([`ruby::RubyPool`]) and the legacy direct-spawn path
/// ([`crate::dynamic::build_sandbox`]) layer them on identically. Setting
/// these env vars is Bundler-version-agnostic: 1.x and 2.x both honour
/// `BUNDLE_*` / `GEM_*`, unlike the 2.x-only `bundle config set` CLI the old
/// path relied on (which is a silent no-op on 1.x, leaving the install target
/// pointed at the system dir — the original root cause).
pub(crate) fn ruby_hermetic_env(workdir: &Path) -> Vec<(&'static str, std::ffi::OsString)> {
let gem_dir = workdir.join("vendor").join("bundle");
let _ = std::fs::create_dir_all(&gem_dir);
vec![
("GEM_HOME", gem_dir.clone().into_os_string()),
("BUNDLE_PATH", gem_dir.into_os_string()),
("BUNDLE_DISABLE_SHARED_GEMS", "false".into()),
("BUNDLE_FROZEN", "false".into()),
(
"BUNDLE_APP_CONFIG",
workdir.join(".bundle").into_os_string(),
),
]
}
/// Merge a process's stdout and stderr into one diagnostic blob.
///
/// Some build tools split their failure diagnostics across streams — Bundler
/// in particular prints "Could not find gem …" to stdout while only an
/// unrelated RubyGems extension warning lands on stderr. Capturing both keeps
/// the downstream host-limitation classifier from missing the real reason.
pub(crate) fn combine_output(stdout: &[u8], stderr: &[u8]) -> String {
let out = String::from_utf8_lossy(stdout);
let err = String::from_utf8_lossy(stderr);
match (out.trim().is_empty(), err.trim().is_empty()) {
(true, _) => err.into_owned(),
(false, true) => out.into_owned(),
(false, false) => format!("{out}\n{err}"),
}
}
/// Detect a runnable `ccache` binary (honouring `NYX_CCACHE_BIN`). Shared
/// by the C and C++ pools to front their compiler with the shared object
/// cache; `None` means "compile bare", preserving legacy parity.
pub(crate) fn detect_ccache() -> Option<String> {
let bin = std::env::var("NYX_CCACHE_BIN").unwrap_or_else(|_| "ccache".to_owned());
binary_runnable(&bin, "--version").then_some(bin)
}
/// Cheap "is this binary runnable" probe used by every pool's
/// [`BuildPool::is_healthy`] / `try_new`. Runs `bin <probe_arg>` with a
/// scrubbed env and reports whether it exited 0.
pub(crate) fn binary_runnable(bin: &str, probe_arg: &str) -> bool {
base_command(bin)
.arg(probe_arg)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
struct EnvGuard {
prior: Option<String>,
}
impl EnvGuard {
fn set(value: Option<&str>) -> Self {
let prior = std::env::var("NYX_DYNAMIC_BUILD_POOL").ok();
match value {
Some(v) => unsafe { std::env::set_var("NYX_DYNAMIC_BUILD_POOL", v) },
None => unsafe { std::env::remove_var("NYX_DYNAMIC_BUILD_POOL") },
}
Self { prior }
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
match self.prior.take() {
Some(v) => unsafe { std::env::set_var("NYX_DYNAMIC_BUILD_POOL", v) },
None => unsafe { std::env::remove_var("NYX_DYNAMIC_BUILD_POOL") },
}
}
}
#[test]
fn default_enables_every_shipped_pool() {
let _l = ENV_LOCK.lock().unwrap();
let _g = EnvGuard::set(None);
for lang in POOL_ENABLED_LANGS {
assert!(is_pool_enabled(lang), "{lang} pool must default on");
}
// A language with no pool stays off.
assert!(!is_pool_enabled("cobol"));
}
#[test]
fn explicit_override_disables_node() {
let _l = ENV_LOCK.lock().unwrap();
let _g = EnvGuard::set(Some("node=0"));
assert!(!is_pool_enabled("node"));
// Other languages keep their default-on state.
assert!(is_pool_enabled("python"));
}
#[test]
fn explicit_override_disables_java() {
let _l = ENV_LOCK.lock().unwrap();
let _g = EnvGuard::set(Some("java=0"));
assert!(!is_pool_enabled("java"));
}
#[test]
fn multi_entry_parses_per_lang() {
let _l = ENV_LOCK.lock().unwrap();
let _g = EnvGuard::set(Some("java=1,node=1,python=0"));
assert!(is_pool_enabled("java"));
assert!(is_pool_enabled("node"));
assert!(!is_pool_enabled("python"));
}
#[test]
fn case_insensitive_keys() {
let _l = ENV_LOCK.lock().unwrap();
let _g = EnvGuard::set(Some("JAVA=0"));
assert!(!is_pool_enabled("java"));
}
#[test]
fn unknown_value_treated_as_disabled() {
let _l = ENV_LOCK.lock().unwrap();
let _g = EnvGuard::set(Some("java=maybe"));
assert!(!is_pool_enabled("java"));
}
}

View file

@ -0,0 +1,87 @@
//! Node.js build pool (Phase 23 / Track O.1).
//!
//! `prepare_node` already snapshots `node_modules` per `package.json` hash.
//! What it lacks is a shared npm download cache: a fresh lock hash re-downloads
//! every tarball from cold.
//!
//! [`NodePool`] points `npm_config_cache` at the shared pool root so package
//! tarballs are reused across lock hashes, collapsing a cold `npm install` to
//! an unpack of already-fetched tarballs. TypeScript harnesses that do not
//! need full type checking are run with `--experimental-strip-types` at
//! execution time (the runner reads [`strip_types_flag`]); the pool itself only
//! owns the install step.
use super::{BuildPool, PoolCompileResult, base_command, binary_runnable, pool_cache_dir};
use std::path::Path;
use std::time::Instant;
pub struct NodePool {
npm_bin: String,
}
impl NodePool {
pub fn try_new() -> Result<Self, String> {
let npm_bin = std::env::var("NYX_NPM_BIN").unwrap_or_else(|_| "npm".to_owned());
if !binary_runnable(&npm_bin, "--version") {
return Err(format!("node-pool: {npm_bin} not runnable"));
}
Ok(NodePool { npm_bin })
}
}
/// The Node flag that lets a TS harness skip a full `tsc` compile when the
/// spec does not need type checking. Surfaced as a free function so the
/// runner can splice it into the harness exec without holding a pool handle.
pub fn strip_types_flag() -> &'static str {
"--experimental-strip-types"
}
impl BuildPool for NodePool {
fn name(&self) -> &'static str {
"node"
}
/// Install dependencies declared by `workdir/package.json` into
/// `workdir/node_modules`. Args are unused.
fn compile_batch(&self, workdir: &Path, _args: &[String]) -> PoolCompileResult {
let start = Instant::now();
let mut cmd = base_command(&self.npm_bin);
cmd.args(["install", "--no-save", "--no-audit", "--no-fund"])
.current_dir(workdir);
if let Some(cache) = pool_cache_dir("node", "npm-cache") {
cmd.env("npm_config_cache", cache);
}
match cmd.output() {
Ok(o) if o.status.success() => PoolCompileResult {
success: true,
stderr: String::new(),
duration: start.elapsed(),
},
Ok(o) => PoolCompileResult {
success: false,
stderr: String::from_utf8_lossy(&o.stderr).into_owned(),
duration: start.elapsed(),
},
Err(e) => PoolCompileResult {
success: false,
stderr: format!("node-pool: npm install: {e}"),
duration: start.elapsed(),
},
}
}
fn is_healthy(&self) -> bool {
binary_runnable(&self.npm_bin, "--version")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strip_types_flag_is_the_node_native_ts_flag() {
assert_eq!(strip_types_flag(), "--experimental-strip-types");
}
}

View file

@ -0,0 +1,110 @@
//! PHP build pool (Phase 23 / Track O.1).
//!
//! Two warm caches keyed off the Composer lockfile:
//! - `COMPOSER_CACHE_DIR` points at the shared pool root so package downloads
//! are reused across lock hashes, and
//! - an opcache file-cache directory is pre-warmed so the harness `php`
//! process skips re-parsing the vendored sources on first run.
//!
//! Both degrade gracefully: a missing `composer` makes `try_new` fail and the
//! caller falls back to the legacy
//! [`crate::dynamic::build_sandbox::prepare_php`] path; a missing `php` simply
//! skips the opcache warm (the install still succeeds).
use super::{BuildPool, PoolCompileResult, base_command, binary_runnable, pool_cache_dir};
use std::path::Path;
use std::time::Instant;
pub struct PhpPool {
composer_bin: String,
}
impl PhpPool {
pub fn try_new() -> Result<Self, String> {
let composer_bin =
std::env::var("NYX_COMPOSER_BIN").unwrap_or_else(|_| "composer".to_owned());
if !binary_runnable(&composer_bin, "--version") {
return Err(format!("php-pool: {composer_bin} not runnable"));
}
Ok(PhpPool { composer_bin })
}
}
impl BuildPool for PhpPool {
fn name(&self) -> &'static str {
"php"
}
/// Install `composer.json` deps into `workdir/vendor` then warm the
/// shared opcache file-cache. Args are unused.
fn compile_batch(&self, workdir: &Path, _args: &[String]) -> PoolCompileResult {
let start = Instant::now();
let mut cmd = base_command(&self.composer_bin);
cmd.args(["install", "--no-interaction", "--no-dev", "--prefer-dist"])
.current_dir(workdir)
.env("COMPOSER_ALLOW_SUPERUSER", "1");
if let Some(cache) = pool_cache_dir("php", "composer-cache") {
cmd.env("COMPOSER_CACHE_DIR", cache);
}
match cmd.output() {
Ok(o) if o.status.success() => {}
Ok(o) => {
return PoolCompileResult {
success: false,
stderr: String::from_utf8_lossy(&o.stderr).into_owned(),
duration: start.elapsed(),
};
}
Err(e) => {
return PoolCompileResult {
success: false,
stderr: format!("php-pool: composer install: {e}"),
duration: start.elapsed(),
};
}
}
warm_opcache(workdir);
PoolCompileResult {
success: true,
stderr: String::new(),
duration: start.elapsed(),
}
}
fn is_healthy(&self) -> bool {
binary_runnable(&self.composer_bin, "--version")
}
}
/// Best-effort opcache file-cache pre-warm: compile every vendored `.php`
/// into the shared opcache file-cache so the harness `php` process boots with
/// the bytecode already on disk. A missing `php` or partial failure is
/// swallowed — the install already succeeded and opcache is a pure speed win.
fn warm_opcache(workdir: &Path) {
let vendor = workdir.join("vendor");
if !vendor.exists() {
return;
}
let php = std::env::var("NYX_PHP_BIN").unwrap_or_else(|_| "php".to_owned());
let file_cache = match pool_cache_dir("php", "opcache") {
Some(d) => d,
None => return,
};
let _ = base_command(&php)
.arg("-d")
.arg("opcache.enable_cli=1")
.arg("-d")
.arg(format!("opcache.file_cache={}", file_cache.display()))
.arg("-d")
.arg("opcache.file_cache_only=1")
.arg("-r")
.arg(
"foreach(new RecursiveIteratorIterator(new RecursiveDirectoryIterator('vendor')) \
as $f){ if(substr($f,-4)==='.php'){ @opcache_compile_file($f); } }",
)
.current_dir(workdir)
.output();
}

View file

@ -0,0 +1,122 @@
//! Python build pool (Phase 23 / Track O.1).
//!
//! `prepare_python` already keys its venv on the requirements hash, so the
//! venv itself is the "shared venv per `requirements_hash`". What the legacy
//! path lacks is a warm bytecode cache: the first harness to import a package
//! pays the `.py` -> `.pyc` compile.
//!
//! [`PythonPool`] runs `python -m compileall` over the venv's `site-packages`
//! once at venv-creation time so every later harness import is a `__pycache__`
//! hit. The pip download cache is pointed at the shared pool root so repeated
//! installs across requirements hashes reuse wheels.
use super::{BuildPool, PoolCompileResult, base_command, binary_runnable, pool_cache_dir};
use std::path::Path;
use std::time::Instant;
pub struct PythonPool;
impl PythonPool {
pub fn try_new(python_bin: &str) -> Result<Self, String> {
if !binary_runnable(python_bin, "--version") {
return Err(format!("python-pool: {python_bin} not runnable"));
}
Ok(PythonPool)
}
}
impl BuildPool for PythonPool {
fn name(&self) -> &'static str {
"python"
}
/// `args[0]` = venv path to create, `args[1]` = python interpreter binary.
fn compile_batch(&self, workdir: &Path, args: &[String]) -> PoolCompileResult {
let start = Instant::now();
let venv_path = match args.first() {
Some(v) => Path::new(v),
None => {
return PoolCompileResult {
success: false,
stderr: "python-pool: missing venv path arg".to_owned(),
duration: start.elapsed(),
};
}
};
let python = args.get(1).map(String::as_str).unwrap_or("python3");
// 1. Create the venv.
let create = base_command(python)
.args(["-m", "venv", "--clear", "--system-site-packages"])
.arg(venv_path)
.status();
match create {
Ok(s) if s.success() => {}
Ok(s) => {
return PoolCompileResult {
success: false,
stderr: format!("venv create failed: exit {s}"),
duration: start.elapsed(),
};
}
Err(e) => {
return PoolCompileResult {
success: false,
stderr: format!("python-pool: venv create: {e}"),
duration: start.elapsed(),
};
}
}
// 2. Install requirements with the shared wheel cache.
let req_path = workdir.join("requirements.txt");
if req_path.exists() {
let pip = venv_path.join("bin").join("pip");
let mut cmd = base_command(&pip.to_string_lossy());
cmd.args(["install", "-r"]).arg(&req_path);
if let Some(cache) = pool_cache_dir("python", "pip-cache") {
cmd.env("PIP_CACHE_DIR", cache);
} else {
cmd.arg("--no-cache-dir");
}
match cmd.output() {
Ok(o) if o.status.success() => {}
Ok(o) => {
return PoolCompileResult {
success: false,
stderr: String::from_utf8_lossy(&o.stderr).into_owned(),
duration: start.elapsed(),
};
}
Err(e) => {
return PoolCompileResult {
success: false,
stderr: format!("python-pool: pip install: {e}"),
duration: start.elapsed(),
};
}
}
}
// 3. Warm __pycache__ for the whole venv (best-effort: a partial
// failure to byte-compile one module must not fail the build).
let venv_python = venv_path.join("bin").join("python");
let _ = base_command(&venv_python.to_string_lossy())
.args(["-m", "compileall", "-q"])
.arg(venv_path)
.output();
PoolCompileResult {
success: true,
stderr: String::new(),
duration: start.elapsed(),
}
}
fn is_healthy(&self) -> bool {
// The interpreter is resolved per-request via args; treat the pool as
// always healthy and let an unrunnable interpreter surface as a build
// error, which the dispatcher already falls back from.
true
}
}

View file

@ -0,0 +1,120 @@
//! Ruby build pool (Phase 23 / Track O.1).
//!
//! `prepare_ruby` already vendors gems per `Gemfile.lock` hash. What it lacks
//! is a warm Bootsnap cache: the first harness to `require` a gem pays the
//! load-path scan + compile.
//!
//! [`RubyPool`] points `BOOTSNAP_CACHE_DIR` at the shared pool root and runs
//! `bundle install` with the shared gem cache. Bootsnap then persists its
//! compiled require-cache across findings. Falls back to the legacy path when
//! `bundle` is not runnable.
use super::{
BuildPool, PoolCompileResult, base_command, binary_runnable, combine_output, pool_cache_dir,
ruby_hermetic_env,
};
use std::path::Path;
use std::time::Instant;
pub struct RubyPool {
bundle_bin: String,
}
impl RubyPool {
pub fn try_new() -> Result<Self, String> {
let bundle_bin = std::env::var("NYX_BUNDLE_BIN").unwrap_or_else(|_| "bundle".to_owned());
if !binary_runnable(&bundle_bin, "--version") {
return Err(format!("ruby-pool: {bundle_bin} not runnable"));
}
Ok(RubyPool { bundle_bin })
}
fn bundle(&self, workdir: &Path) -> std::process::Command {
let mut cmd = base_command(&self.bundle_bin);
cmd.current_dir(workdir);
// Writable gem target → no privilege escalation → never `sudo`.
for (k, v) in ruby_hermetic_env(workdir) {
cmd.env(k, v);
}
if let Some(cache) = pool_cache_dir("ruby", "bootsnap") {
cmd.env("BOOTSNAP_CACHE_DIR", cache);
}
cmd
}
}
impl BuildPool for RubyPool {
fn name(&self) -> &'static str {
"ruby"
}
/// Resolve `Gemfile` deps into `workdir/vendor/bundle`. Args are unused.
fn compile_batch(&self, workdir: &Path, _args: &[String]) -> PoolCompileResult {
let start = Instant::now();
// `bundle check` short-circuits when the host already has every gem.
//
// Run the check with the *runtime* environment — plain system gems, no
// `GEM_HOME`/`BUNDLE_PATH` override. The harness is executed as
// `ruby harness.rb`, whose `require 'bundler/setup'` resolves against
// the system gem path, so the build-time check must consult that same
// path to predict whether the run will succeed. The hermetic
// `GEM_HOME` override (below) exists only to give `bundle install` a
// writable, sudo-free target for *missing* gems; applying it to the
// check breaks Bundler 1.x's ability to see an already-installed system
// gem (e.g. `rack`), turning a satisfiable Gemfile into a spurious
// BuildFailed.
let mut check = base_command(&self.bundle_bin);
check.current_dir(workdir);
if let Some(cache) = pool_cache_dir("ruby", "bootsnap") {
check.env("BOOTSNAP_CACHE_DIR", cache);
}
if let Ok(o) = check.arg("check").output()
&& o.status.success()
{
return PoolCompileResult {
success: true,
stderr: String::new(),
duration: start.elapsed(),
};
}
// The install target is pinned to a writable vendor dir via
// `ruby_hermetic_env` (GEM_HOME / BUNDLE_PATH), so the legacy
// `bundle config set --local path …` step is gone: it is 2.x-only
// syntax that no-ops on Bundler 1.x (leaving the target pointed at
// the root-owned system dir — the `sudo` root cause). `--local`
// keeps the build offline: missing gems fail fast with a
// host-limitation error instead of reaching for the network.
let install = self
.bundle(workdir)
.args(["install", "--local", "--jobs", "4", "--retry", "0"])
.output();
match install {
Ok(o) if o.status.success() => PoolCompileResult {
success: true,
stderr: String::new(),
duration: start.elapsed(),
},
Ok(o) => PoolCompileResult {
success: false,
// Bundler prints its dependency-resolution diagnostics
// ("Could not find gem '…' in any of the gem sources …") to
// STDOUT, leaving only the RubyGems extension warning on
// stderr. Combine both so the host-limitation classifier at
// the verify boundary can see the real reason.
stderr: combine_output(&o.stdout, &o.stderr),
duration: start.elapsed(),
},
Err(e) => PoolCompileResult {
success: false,
stderr: format!("ruby-pool: bundle install: {e}"),
duration: start.elapsed(),
},
}
}
fn is_healthy(&self) -> bool {
binary_runnable(&self.bundle_bin, "--version")
}
}

View file

@ -0,0 +1,369 @@
//! Rust build pool (Phase 23 / Track O.1).
//!
//! The legacy [`crate::dynamic::build_sandbox::prepare_rust`] runs a fresh
//! `cargo build --release` per finding with a per-workdir `target/`. Every
//! harness therefore recompiles the (identical) harness scaffold and all of
//! its dependencies from cold.
//!
//! [`RustPool`] keeps two warm caches keyed on the `Cargo.lock` hash:
//! - a shared `CARGO_TARGET_DIR` so incremental artefacts survive across
//! per-finding workdirs, and
//! - `sccache` as `RUSTC_WRAPPER` when it is on `PATH`, which caches the
//! per-crate `rustc` invocations across *different* lock hashes too.
//!
//! Both degrade gracefully: a missing `sccache` simply drops the wrapper and
//! a fresh lock hash gets a fresh (empty) shared target dir. The compile
//! itself is byte-for-byte the same `cargo build --release` the legacy path
//! runs, so success / failure parity holds.
use super::{BuildPool, PoolCompileResult, base_command, binary_runnable, pool_cache_dir};
use blake3::Hasher;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
pub struct RustPool {
cargo_bin: String,
/// `Some(path)` when an `sccache` binary is runnable. Wired in as
/// `RUSTC_WRAPPER`; `None` falls back to plain `rustc`.
sccache_bin: Option<String>,
}
impl RustPool {
pub fn try_new() -> Result<Self, String> {
let cargo_bin = std::env::var("NYX_CARGO_BIN").unwrap_or_else(|_| "cargo".to_owned());
if !binary_runnable(&cargo_bin, "--version") {
return Err(format!("rust-pool: {cargo_bin} not runnable"));
}
let sccache_bin = detect_sccache();
Ok(RustPool {
cargo_bin,
sccache_bin,
})
}
}
fn detect_sccache() -> Option<String> {
let bin = std::env::var("NYX_SCCACHE_BIN").unwrap_or_else(|_| "sccache".to_owned());
binary_runnable(&bin, "--version").then_some(bin)
}
impl BuildPool for RustPool {
fn name(&self) -> &'static str {
"rust"
}
/// `args[0]` = absolute path the compiled `nyx_harness` binary must land
/// at (the caller's cache slot).
fn compile_batch(&self, workdir: &Path, args: &[String]) -> PoolCompileResult {
let start = Instant::now();
let dest = match args.first() {
Some(d) => Path::new(d),
None => {
return PoolCompileResult {
success: false,
stderr: "rust-pool: missing binary destination arg".to_owned(),
duration: start.elapsed(),
};
}
};
// Key the shared target dir on the manifest *and* every `src/` file,
// not the manifest alone. Two fixtures built for the same cap share a
// `Cargo.toml` (identical lock hash) but differ only in their source;
// a manifest-only key routed both into the same `release/nyx_harness`
// slot, letting cargo skip the second fixture's relink so the copy
// below shipped the *first* fixture's binary — cross-fixture verdict
// corruption (a vuln / benign pair confirming identically). Folding
// the source hash in gives each distinct harness its own target dir.
let build_hash = hash_build_inputs(workdir);
let target_dir = match pool_cache_dir("rust", &build_hash) {
Some(d) => d,
None => {
return PoolCompileResult {
success: false,
stderr: "rust-pool: no shared target dir".to_owned(),
duration: start.elapsed(),
};
}
};
// Serialise build + copy across processes for this shared target dir.
//
// The target dir is keyed only on the Cargo manifest hash, so every
// fixture that shares a `Cargo.toml` compiles the same bin name
// (`nyx_harness`) into the same `release/nyx_harness` path here.
// `cargo` already serialises the *build* across processes via its own
// target lock, but releases that lock the moment it exits — before the
// copy below moves `release/nyx_harness` to the caller's per-fixture
// cache slot. A second process's `cargo build` landing in that window
// overwrites `release/nyx_harness`, so we copy a *different* fixture's
// binary into our slot and poison its build cache (observed as
// cross-fixture verdict corruption under a parallel `cargo test`).
// Holding this lock across build+copy folds the copy into the existing
// serialised section, so it adds the copy's few milliseconds, not a
// new build barrier.
let _build_lock = TargetDirLock::acquire(&target_dir);
let mut cmd = base_command(&self.cargo_bin);
cmd.args(["build", "--release"])
.current_dir(workdir)
.env(
"CARGO_HOME",
std::env::var("CARGO_HOME").unwrap_or_else(|_| default_cargo_home()),
)
.env(
"RUSTUP_HOME",
std::env::var("RUSTUP_HOME").unwrap_or_default(),
)
.env("CARGO_TARGET_DIR", &target_dir);
if let Some(sccache) = &self.sccache_bin {
cmd.env("RUSTC_WRAPPER", sccache);
}
let output = match cmd.output() {
Ok(o) => o,
Err(e) => {
return PoolCompileResult {
success: false,
stderr: format!("rust-pool: cargo build: {e}"),
duration: start.elapsed(),
};
}
};
if !output.status.success() {
return PoolCompileResult {
success: false,
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
duration: start.elapsed(),
};
}
let compiled = target_dir.join("release").join("nyx_harness");
if let Err(e) = std::fs::copy(&compiled, dest) {
return PoolCompileResult {
success: false,
stderr: format!(
"rust-pool: cargo build ok but copy {} -> {} failed: {e}",
compiled.display(),
dest.display(),
),
duration: start.elapsed(),
};
}
PoolCompileResult {
success: true,
stderr: String::new(),
duration: start.elapsed(),
}
}
fn is_healthy(&self) -> bool {
binary_runnable(&self.cargo_bin, "--version")
}
}
fn default_cargo_home() -> String {
std::env::var("HOME")
.map(|h| format!("{h}/.cargo"))
.unwrap_or_else(|_| ".cargo".to_owned())
}
/// Cross-process advisory lock guarding build+copy for a shared
/// `CARGO_TARGET_DIR` (see the call site in [`RustPool::compile_batch`]).
///
/// Implemented as an atomic `create_new` (O_EXCL) lockfile so it works across
/// the separate processes a parallel `cargo test` spawns — an in-process
/// `Mutex` would not. A lock older than `STALE_AFTER` is stolen so a crashed
/// holder cannot wedge the pool, and acquisition gives up after `MAX_WAIT`
/// (proceeding unlocked) so a pathological case degrades to the pre-fix
/// behaviour rather than deadlocking.
struct TargetDirLock {
path: PathBuf,
/// Only the process that created the lockfile removes it on drop, so a
/// give-up / steal path never deletes another holder's lock.
owned: bool,
}
impl TargetDirLock {
fn acquire(target_dir: &Path) -> Self {
const MAX_WAIT: Duration = Duration::from_secs(300);
const STALE_AFTER: Duration = Duration::from_secs(180);
let path = target_dir.join(".nyx-pool-build.lock");
let start = Instant::now();
let mut spins: u64 = 0;
loop {
match std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&path)
{
Ok(mut f) => {
use std::io::Write;
let _ = writeln!(f, "{}", std::process::id());
return Self { path, owned: true };
}
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
// Steal a stale lock left behind by a crashed holder.
if let Ok(meta) = std::fs::metadata(&path)
&& let Ok(mtime) = meta.modified()
&& mtime.elapsed().map(|d| d > STALE_AFTER).unwrap_or(false)
{
let _ = std::fs::remove_file(&path);
continue;
}
if start.elapsed() > MAX_WAIT {
// Best-effort: a slow build beats a deadlock.
return Self { path, owned: false };
}
let nap = 10u64.saturating_add(spins.min(40).saturating_mul(2));
std::thread::sleep(Duration::from_millis(nap));
spins = spins.saturating_add(1);
}
Err(_) => {
// Cannot create the lockfile (perms / race on dir) — proceed
// unlocked rather than fail the build outright.
return Self { path, owned: false };
}
}
}
}
}
impl Drop for TargetDirLock {
fn drop(&mut self) {
if self.owned {
let _ = std::fs::remove_file(&self.path);
}
}
}
/// Stable short hash of the named manifest files under `workdir`.
fn hash_files(workdir: &Path, files: &[&str]) -> String {
let mut h = Hasher::new();
for fname in files {
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())
)
}
/// Hash of every input that determines the compiled `nyx_harness` binary: the
/// Cargo manifest/lock *plus* every `.rs` file under `src/`. Used to key the
/// shared `CARGO_TARGET_DIR` so source-distinct harnesses never share a
/// `release/nyx_harness` slot (see the call site in [`RustPool::compile_batch`]
/// for why manifest-only keying corrupted cross-fixture verdicts). Mirrors
/// [`crate::dynamic::build_sandbox::compute_rust_lockfile_hash`].
fn hash_build_inputs(workdir: &Path) -> String {
let manifest = hash_files(workdir, &["Cargo.lock", "Cargo.toml"]);
let src_dir = workdir.join("src");
let mut rs_files: Vec<PathBuf> = Vec::new();
collect_rs_files(&src_dir, &src_dir, &mut rs_files);
rs_files.sort();
let mut h = Hasher::new();
for rel in &rs_files {
if let Ok(content) = std::fs::read(src_dir.join(rel)) {
h.update(rel.to_string_lossy().as_bytes());
h.update(b"\0");
h.update(&content);
}
}
let out = h.finalize();
format!(
"{manifest}-{:016x}",
u64::from_le_bytes(out.as_bytes()[..8].try_into().unwrap())
)
}
/// Recursively collect `.rs` file paths (relative to `root`) under `dir`.
fn collect_rs_files(root: &Path, dir: &Path, out: &mut Vec<PathBuf>) {
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return,
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect_rs_files(root, &path, out);
} else if path.extension().and_then(|e| e.to_str()) == Some("rs")
&& let Ok(rel) = path.strip_prefix(root)
{
out.push(rel.to_path_buf());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hash_is_deterministic_and_content_sensitive() {
let dir = tempfile::TempDir::new().unwrap();
let h1 = hash_files(dir.path(), &["Cargo.lock"]);
let h2 = hash_files(dir.path(), &["Cargo.lock"]);
assert_eq!(h1, h2);
std::fs::write(dir.path().join("Cargo.lock"), b"[[package]]\n").unwrap();
let h3 = hash_files(dir.path(), &["Cargo.lock"]);
assert_ne!(h1, h3);
}
#[test]
fn build_hash_differs_for_same_manifest_distinct_source() {
// A vuln / benign pair built for the same cap ships an identical
// Cargo.toml but a different `src/entry.rs`. The shared target-dir key
// must differ between them, else cargo skips the second relink and the
// pool copies out the first fixture's binary (cross-fixture verdict
// corruption — the cmdi / data-exfil Rust regression).
let manifest = b"[package]\nname=\"nyx_harness\"\nversion=\"0.0.0\"\n";
let vuln = tempfile::TempDir::new().unwrap();
std::fs::create_dir_all(vuln.path().join("src")).unwrap();
std::fs::write(vuln.path().join("Cargo.toml"), manifest).unwrap();
std::fs::write(vuln.path().join("src/main.rs"), b"fn main(){}\n").unwrap();
std::fs::write(
vuln.path().join("src/entry.rs"),
b"pub fn run(){ /*vuln*/ }\n",
)
.unwrap();
let benign = tempfile::TempDir::new().unwrap();
std::fs::create_dir_all(benign.path().join("src")).unwrap();
std::fs::write(benign.path().join("Cargo.toml"), manifest).unwrap();
std::fs::write(benign.path().join("src/main.rs"), b"fn main(){}\n").unwrap();
std::fs::write(
benign.path().join("src/entry.rs"),
b"pub fn run(){ /*benign*/ }\n",
)
.unwrap();
// Identical manifests collide under the old manifest-only key …
assert_eq!(
hash_files(vuln.path(), &["Cargo.lock", "Cargo.toml"]),
hash_files(benign.path(), &["Cargo.lock", "Cargo.toml"]),
);
// … but the source-aware key separates them.
assert_ne!(
hash_build_inputs(vuln.path()),
hash_build_inputs(benign.path())
);
}
#[test]
fn missing_dest_arg_is_an_error_not_a_panic() {
let dir = tempfile::TempDir::new().unwrap();
// Construct without a toolchain probe so the test runs JDK/cargo-free.
let pool = RustPool {
cargo_bin: "cargo".to_owned(),
sccache_bin: None,
};
let r = pool.compile_batch(dir.path(), &[]);
assert!(!r.success);
assert!(r.stderr.contains("missing binary destination"));
}
}

2879
src/dynamic/build_sandbox.rs Normal file

File diff suppressed because it is too large Load diff

214
src/dynamic/corpus.rs Normal file
View file

@ -0,0 +1,214 @@
// Legacy [`Oracle::OutputContains`] is intentionally retained for
// pre-Phase-06 corpus entries until they migrate to
// [`Oracle::SinkProbe`]. The deprecation warning is informational, not a
// signal to migrate inside this module.
#![allow(deprecated)]
//! Per-capability payload corpus, keyed by `(Cap, Lang)`.
//!
//! Each `(Cap, Lang)` pair maps to a small set of canonical payloads plus a
//! matching detection oracle. Payloads are static data — adding a new one
//! is a code review, not a runtime config knob, so they cannot drift
//! between versions.
//!
//! Differential confirmation (§4.1): every non-benign payload either
//! references a paired benign control (resolved inside the same
//! `(cap, lang)` slice) or carries a written
//! [`CuratedPayload::no_benign_control_rationale`] explaining why no
//! control is meaningful. The [`audit`] module enforces this both at
//! compile time and via the runtime `corpus_registry::audit` test.
//!
//! # Module layout
//!
//! ```text
//! corpus.rs — types, public re-exports, module root
//! corpus/registry.rs — CapCorpus, CORPUS, payloads_for{,_lang}
//! corpus/audit.rs — compile-time + runtime audits
//! corpus/<cap>/<lang>.rs — per-(cap, lang) `pub const PAYLOADS`
//! ```
//!
//! Adding a new language for a cap means: drop a new file under
//! `corpus/<cap>/<lang>.rs`, register `pub mod <lang>;` in the cap's
//! `mod.rs`, and wire `(Cap::<CAP>, Lang::<Lang>, <cap>::<lang>::PAYLOADS)`
//! into `registry::ENTRIES`. No other file needs to change.
//!
//! # Corpus governance (§16.1)
//!
//! Every payload carries [`PayloadProvenance`], a [`CuratedPayload::since_corpus_version`],
//! and at least one [`CuratedPayload::fixture_paths`] entry. The [`CORPUS_VERSION`] const
//! tracks the history of incompatible corpus changes; bumping it
//! invalidates all `dynamic_verdict_cache` entries whose spec touched the
//! changed cap.
use crate::dynamic::oracle::ProbePredicate;
use crate::labels::Cap;
use crate::symbol::Lang;
pub mod audit;
pub mod registry;
mod cmdi;
mod crypto;
mod data_exfil;
mod deserialize;
mod fmt_string;
mod header_injection;
mod json_parse;
mod ldap;
mod open_redirect;
// `pub(crate)` so the Java emitter can read the FILE_IO canary filename /
// marker consts it must stage into the servlet harness workdir.
pub(crate) mod path_trav;
mod prototype_pollution;
mod sqli;
mod ssrf;
mod ssti;
mod unauthorized_id;
mod xpath;
mod xss;
mod xxe;
pub use registry::{
CORPUS, CORPUS_UNSUPPORTED_LANG_NEUTRAL, audit_marker_collisions, benign_payload_for,
benign_payload_for_lang, materialise_bytes, payloads_for, payloads_for_lang,
resolve_benign_control, resolve_benign_control_lang,
};
/// Re-exported canonical [`Oracle`] type.
///
/// The actual enum lives in [`crate::dynamic::oracle`] alongside
/// [`crate::dynamic::oracle::ProbePredicate`] and
/// [`crate::dynamic::oracle::oracle_fired`]. Re-exported here so the
/// `CuratedPayload.oracle: Oracle` field reads naturally and existing
/// `crate::dynamic::corpus::Oracle` callers keep working.
pub use crate::dynamic::oracle::Oracle;
/// Bump when the corpus content changes in a way that invalidates previously-
/// computed [`crate::dynamic::spec::HarnessSpec::spec_hash`] values.
///
/// # Bump history
///
/// | Version | Date | Change |
/// |---------|------------|-----------------------------------------------|
/// | 1 | 2025-11-01 | Initial corpus (SQLi, CMDI, PATH_TRAV, SSRF, XSS) |
/// | 2 | 2025-12-15 | SSRF OOB-variant added; oracle semantics tightened |
/// | 3 | 2026-05-12 | Migrated to `CuratedPayload`; provenance + fixture_paths enforced; SSRF OOB-nonce slot added |
/// | 4 | 2026-05-14 | Phase 07: `benign_control` paired refs + benign payloads added to SQLI / CMDI / SSRF (file-scheme) |
/// | 5 | 2026-05-16 | FMT_STRING SinkCrash payload + benign control (Phase 08 unrelated-crash acceptance fixture) |
/// | 6 | 2026-05-17 | Phase 02 / Track J.0: `(Cap, Lang)` registry refactor; `no_benign_control_rationale` field; compile-time provenance audit |
/// | 7 | 2026-05-17 | Phase 03 / Track J.1: `DESERIALIZE` cap lit for Java / Python / PHP / Ruby; `ProbeKind::Deserialize` + `ProbePredicate::DeserializeGadgetInvoked` |
/// | 8 | 2026-05-17 | Phase 04 / Track J.2: `SSTI` cap lit for Jinja2 / ERB / Twig / Thymeleaf / Handlebars; `ProbePredicate::TemplateEvalEqual` |
/// | 9 | 2026-05-17 | Phase 05 / Track J.3: `XXE` cap lit for Java / Python / PHP / Ruby / Go; `ProbeKind::Xxe` + `ProbePredicate::XxeEntityExpanded` |
/// | 10 | 2026-05-17 | Phase 06 / Track J.4: `LDAP_INJECTION` cap lit for Java / Python / PHP; `ProbeKind::Ldap` + `ProbePredicate::LdapResultCountGreaterThan`; `StubKind::Ldap` + in-sandbox LDAP server stub |
/// | 11 | 2026-05-17 | Phase 07 / Track J.5: `XPATH_INJECTION` cap lit for Java / Python / PHP / JS; `ProbeKind::Xpath`; `LdapResultCountGreaterThan` renamed to `QueryResultCountGreaterThan` (shared by LDAP + XPath); `xpath_corpus.xml` staged in workdir |
/// | 12 | 2026-05-18 | Phase 08 / Track J.6: `HEADER_INJECTION` cap lit for Java / Python / PHP / Ruby / JS / Go / Rust; `ProbeKind::HeaderEmit` + `ProbePredicate::HeaderInjected`; per-lang `setHeader` shims |
/// | 13 | 2026-05-18 | Phase 09 / Track J.7: `OPEN_REDIRECT` cap lit for Java / Python / PHP / Ruby / JS / Go / Rust; `ProbeKind::Redirect` + `ProbePredicate::RedirectHostNotIn`; per-lang `sendRedirect` / `redirect()` shims |
/// | 14 | 2026-05-18 | Phase 10 / Track J.8: `PROTOTYPE_POLLUTION` cap lit for JS / TS; `ProbeKind::PrototypePollution` + `ProbePredicate::PrototypeCanaryTouched`; Node harness installs `Proxy`-style canary trap on `Object.prototype.__nyx_canary` |
/// | 15 | 2026-05-18 | Phase 11 / Track J.9: `CRYPTO` (Java/Python/PHP/Go/Rust) + `JSON_PARSE` (JS/Python/Ruby) + `UNAUTHORIZED_ID` (7 langs) + `DATA_EXFIL` (7 langs); `ProbeKind::{WeakKey,IdorAccess,OutboundNetwork}` + `ProbePredicate::{WeakKeyEntropy,IdorBoundaryCrossed,OutboundHostNotIn}`; `UnsupportedReason::SoundOracleUnavailable` for caps with no sound oracle |
/// | 16 | 2026-06-01 | Collision-resistant `cmdi` (`CODE_EXEC`) marker: payload `; echo NYX_PWN_$((113*7))_CMDI`, oracle `OutputContains("NYX_PWN_791_CMDI")`. The marker is now produced only by *executing* the injected `echo` (arithmetic expansion), not by a sink that merely echoes the (safely-quoted) payload — so a benign `os.system("echo " + shlex.quote(x))` control no longer false-confirms. Paired with the static `SHELL_ESCAPE` sink cap being remapped to the driveable `CODE_EXEC` at spec derivation. |
/// | 17 | 2026-06-01 | Collision-resistant `path_traversal` (`FILE_IO`) Java payload for the entry-driven servlet harness: vuln `../nyx_pt_canary` reads a workdir-root canary the emitter plants; oracle `OutputContains(CANARY_MARKER)` where the marker is the canary's CONTENT (not a substring of the path payload), so a fixture that echoes the requested filename back cannot reproduce it — only an unsanitised read of the canary does. |
pub const CORPUS_VERSION: u32 = 17;
/// Where a payload originated.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PayloadProvenance {
/// Manually written and reviewed by the Nyx team.
Curated,
/// Produced by the internal mutation fuzzer (`fuzz/dynamic_corpus/`).
/// Still requires human promotion review (§16.4) before landing here.
InternalFuzzer,
/// Derived from a public CVE or external security report.
ExternalReport,
}
/// Reference from a vulnerable payload to its paired benign control.
///
/// Resolved at call time by scanning the same cap's payload slice for an
/// `is_benign == true` entry whose `label` matches. Stored as `&'static
/// str` (rather than a back-pointer to [`CuratedPayload`]) so the corpus
/// tables stay `const`-declarable.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PayloadRef {
/// Label of the benign-control entry inside the same cap's payload set.
pub label: &'static str,
}
/// A single payload entry in the curated corpus.
///
/// Governs both static payload bytes (or an OOB-nonce template) and the
/// oracle used to confirm the vulnerability fired. All fields are
/// `'static` so the corpus can live in read-only memory.
#[derive(Debug, Clone)]
pub struct CuratedPayload {
/// Bytes injected into the [`crate::dynamic::spec::PayloadSlot`].
///
/// When [`Self::oob_nonce_slot`] is `true` this field is ignored; the
/// runner materialises the actual bytes from the OOB listener URL at
/// call time.
pub bytes: &'static [u8],
/// Human label for logs and reports.
pub label: &'static str,
/// How we decide the sink fired. See [`Oracle`].
pub oracle: Oracle,
/// If `true`, this is a benign control payload.
/// `Confirmed` requires the vuln payload to trigger AND the benign payload
/// NOT to trigger (differential confirmation, §4.1).
pub is_benign: bool,
/// Where this payload came from.
pub provenance: PayloadProvenance,
/// `CORPUS_VERSION` when this payload was added.
pub since_corpus_version: u32,
/// `CORPUS_VERSION` at which this payload was deprecated, if any.
pub deprecated_at_corpus_version: Option<u32>,
/// Source files that exercise this payload in the dynamic harness.
/// At least one entry required per §16.1.
pub fixture_paths: &'static [&'static str],
/// When `true`, the runner generates the actual bytes from the OOB
/// listener URL + per-finding nonce at execution time (SSRF OOB variant).
/// The `bytes` field is unused for such payloads.
pub oob_nonce_slot: bool,
/// Structured-oracle predicates evaluated against
/// [`crate::dynamic::probe::SinkProbe`] records drained from the run's
/// probe channel (Phase 06 — Track C.1). Always populated; empty when
/// the payload still relies on the legacy
/// [`Oracle::OutputContains`]
/// path and has not been migrated to
/// [`Oracle::SinkProbe`] yet.
pub probe_predicates: &'static [ProbePredicate],
/// Paired benign-control payload inside the same cap's slice.
///
/// `Some(PayloadRef)` on a vulnerable entry means the differential rule
/// (Phase 07, §4.1) compares this entry's oracle firing against the
/// referenced benign. `None` marks the entry as having no paired
/// control — the runner downgrades any would-be `Confirmed` to
/// [`crate::evidence::InconclusiveReason::NoBenignControl`].
/// Always `None` on benign entries themselves.
pub benign_control: Option<PayloadRef>,
/// Written rationale required when a non-benign payload has
/// `benign_control = None`. Compile-time audit
/// ([`audit::audit_benign_controls_runtime`]) rejects any entry that
/// elides the paired control without a non-empty explanation here.
/// Always `None` on entries that DO carry a `benign_control` and on
/// benign entries themselves.
pub no_benign_control_rationale: Option<&'static str>,
}
/// Backward-compatible type alias.
pub type Payload = CuratedPayload;
/// Read-only registry of `(Cap, Lang)` payload slices.
///
/// Constructed once as the [`registry::CORPUS`] const. Layered as
/// `&'static` slices so the entire registry can live in read-only memory
/// and so [`audit`] can walk it in const eval.
#[derive(Debug, Clone, Copy)]
pub struct CapCorpus {
/// `(Cap, Lang, payloads)` triples. A single cap may appear once per
/// supported language. See [`registry::payloads_for_lang`] for the
/// per-language lookup and [`registry::payloads_for`] for the
/// back-compatible union shim.
pub entries: &'static [(Cap, Lang, &'static [CuratedPayload])],
/// Per-cap probe predicates lifted off individual payloads. Reserved
/// for later Track J phases; empty in Phase 02.
pub oracles: &'static [(Cap, &'static [ProbePredicate])],
}

212
src/dynamic/corpus/audit.rs Normal file
View file

@ -0,0 +1,212 @@
//! Compile-time + runtime audits over the corpus registry.
//!
//! Two invariants enforced here fail the build (via `const _: () = assert!(...)`)
//! if they regress:
//!
//! 1. **`benign_control` resolves locally.** Every non-benign payload either
//! references a benign control whose `label` appears inside the same
//! `(cap, lang)` slice, *or* carries an explicit
//! [`CuratedPayload::no_benign_control_rationale`] with a non-empty
//! written rationale. Without this guard the differential rule
//! (§4.1) silently downgrades to `Inconclusive(NoBenignControl)`
//! whenever a maintainer forgets to wire a paired benign entry.
//!
//! 2. **Cap coverage is exhaustive.** The set of caps appearing in
//! [`CORPUS`]'s [`entries`](super::CapCorpus::entries) OR [`CORPUS_UNSUPPORTED_LANG_NEUTRAL`] must
//! equal [`Cap::all`]. Adding a new `Cap` bit without classifying it
//! fails the build.
//!
//! The runtime `corpus_registry::audit` test mirrors both checks so
//! failure surfaces in `cargo test` output, not just `cargo build`.
use super::CuratedPayload;
use super::registry::{CORPUS, CORPUS_UNSUPPORTED_LANG_NEUTRAL};
use crate::labels::Cap;
/// Byte-level equality for `&'static str` usable in const eval.
#[allow(dead_code)] // Called from const-eval audit helpers on MSRV/CI compilers.
const fn str_eq(a: &str, b: &str) -> bool {
let ab = a.as_bytes();
let bb = b.as_bytes();
if ab.len() != bb.len() {
return false;
}
let mut i = 0;
while i < ab.len() {
if ab[i] != bb[i] {
return false;
}
i += 1;
}
true
}
/// Walk every `(cap, lang)` slice; for each non-benign payload check that
/// either its `benign_control.label` resolves inside the same slice or it
/// carries a non-empty `no_benign_control_rationale`.
#[allow(dead_code)] // Called from a const assertion; MSRV lints may miss const-eval uses.
const fn audit_benign_controls() -> bool {
let entries = CORPUS.entries;
let mut e = 0;
while e < entries.len() {
let slice: &[CuratedPayload] = entries[e].2;
let mut i = 0;
while i < slice.len() {
let p = &slice[i];
if !p.is_benign {
match p.benign_control {
Some(r) => {
let mut j = 0;
let mut found = false;
while j < slice.len() {
if slice[j].is_benign && str_eq(slice[j].label, r.label) {
found = true;
break;
}
j += 1;
}
if !found {
return false;
}
}
None => match p.no_benign_control_rationale {
Some(rationale) => {
if rationale.is_empty() {
return false;
}
}
None => return false,
},
}
}
i += 1;
}
e += 1;
}
true
}
/// OR of cap bits appearing in `CORPUS.entries`.
const fn registered_cap_bits() -> u32 {
let entries = CORPUS.entries;
let mut bits = 0u32;
let mut i = 0;
while i < entries.len() {
bits |= entries[i].0.bits();
i += 1;
}
bits
}
/// Compile-time guards. Bumping or breaking these fails `cargo build`.
const _: () = assert!(
audit_benign_controls(),
"corpus audit: a non-benign payload references a `benign_control` whose \
label does not resolve inside its own (cap, lang) slice AND carries no \
`no_benign_control_rationale` see src/dynamic/corpus/audit.rs.",
);
const _: () = assert!(
registered_cap_bits() | CORPUS_UNSUPPORTED_LANG_NEUTRAL == Cap::all().bits(),
"corpus audit: union of (cap, lang) entries and \
`CORPUS_UNSUPPORTED_LANG_NEUTRAL` does not cover every `Cap` bit. \
Add the missing cap to either a `(cap, lang)` slice or the \
lang-neutral unsupported list.",
);
/// Runtime mirror of the compile-time benign-control audit.
pub fn audit_benign_controls_runtime() -> Result<(), String> {
for &(cap, lang, slice) in CORPUS.entries {
for p in slice {
if p.is_benign {
continue;
}
match p.benign_control {
Some(r) => {
let found = slice.iter().any(|q| q.is_benign && q.label == r.label);
if !found {
return Err(format!(
"({:?}, {:?}) vuln payload {:?} references missing \
benign_control label {:?}",
cap, lang, p.label, r.label,
));
}
}
None => match p.no_benign_control_rationale {
Some(rationale) if !rationale.is_empty() => {}
_ => {
return Err(format!(
"({:?}, {:?}) vuln payload {:?} has neither a \
benign_control nor a written \
no_benign_control_rationale",
cap, lang, p.label,
));
}
},
}
}
}
Ok(())
}
/// Runtime mirror of the compile-time cap-coverage audit.
pub fn audit_cap_coverage_runtime() -> Result<(), String> {
let covered = registered_cap_bits() | CORPUS_UNSUPPORTED_LANG_NEUTRAL;
if covered != Cap::all().bits() {
let missing = Cap::all().bits() & !covered;
return Err(format!(
"Cap bits {missing:#x} are neither registered in CORPUS.entries \
nor listed in CORPUS_UNSUPPORTED_LANG_NEUTRAL",
));
}
Ok(())
}
/// Track J.0 deferred audit: a non-benign payload's `benign_control.label`
/// must be unique *within its own `(cap, lang)` slice* — and a benign
/// payload's label may not collide with any other benign label inside the
/// same cap across lang slices, otherwise the lang-agnostic union shim
/// could resolve a vuln payload in language A against a benign payload
/// declared in language B (the latent §4.1 bug captured in the deferred
/// queue).
pub fn audit_benign_label_uniqueness_runtime() -> Result<(), String> {
use std::collections::HashMap;
let mut by_cap: HashMap<u32, HashMap<&'static str, crate::symbol::Lang>> = HashMap::new();
for &(cap, lang, slice) in CORPUS.entries {
let bucket = by_cap.entry(cap.bits()).or_default();
for p in slice {
if !p.is_benign {
continue;
}
if let Some(prev_lang) = bucket.insert(p.label, lang)
&& prev_lang != lang
{
return Err(format!(
"benign label {:?} for cap {:#x} is registered in both \
{:?} and {:?} lang-agnostic resolve_benign_control \
could match the wrong language",
p.label,
cap.bits(),
prev_lang,
lang,
));
}
}
}
Ok(())
}
#[cfg(test)]
mod corpus_registry {
use super::*;
/// Plan §02 acceptance: `cargo test corpus_registry::audit` must pass.
/// The test name and module name jointly form the required path.
#[test]
fn audit() {
audit_benign_controls_runtime().expect("benign_control audit failed");
audit_cap_coverage_runtime().expect("cap coverage audit failed");
audit_benign_label_uniqueness_runtime().expect("benign label uniqueness audit failed");
}
}

View file

@ -0,0 +1,46 @@
//! C `Cap::CODE_EXEC` payloads.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b":; echo NYX_PWN_$((113*7))_CMDI",
label: "cmdi-echo-marker-c",
oracle: Oracle::OutputContains("NYX_PWN_791_CMDI"),
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &[
"tests/benchmark/corpus/c/cmdi/cmdi_exec.c",
"tests/benchmark/corpus/c/cmdi/cmdi_fgets.c",
"tests/benchmark/corpus/c/cmdi/cmdi_popen.c",
"tests/benchmark/corpus/c/cmdi/cmdi_system.c",
],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: Some(PayloadRef {
label: "cmdi-benign-c",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"benign_safe_cmdi_NYX_BENIGN",
label: "cmdi-benign-c",
oracle: Oracle::OutputContains("NYX_PWN_791_CMDI"),
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &[
"tests/benchmark/corpus/c/cmdi/cmdi_exec.c",
"tests/benchmark/corpus/c/cmdi/cmdi_fgets.c",
"tests/benchmark/corpus/c/cmdi/cmdi_popen.c",
"tests/benchmark/corpus/c/cmdi/cmdi_system.c",
],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,52 @@
//! C++ `Cap::CODE_EXEC` payloads.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b":; echo NYX_PWN_$((113*7))_CMDI",
label: "cmdi-echo-marker-cpp",
oracle: Oracle::OutputContains("NYX_PWN_791_CMDI"),
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &[
"tests/benchmark/corpus/cpp/cmdi/cmdi_class_inline_method.cpp",
"tests/benchmark/corpus/cpp/cmdi/cmdi_exec.cpp",
"tests/benchmark/corpus/cpp/cmdi/cmdi_getline.cpp",
"tests/benchmark/corpus/cpp/cmdi/cmdi_lambda_passthrough.cpp",
"tests/benchmark/corpus/cpp/cmdi/cmdi_popen.cpp",
"tests/benchmark/corpus/cpp/cmdi/cmdi_stl_vector_string.cpp",
"tests/benchmark/corpus/cpp/cmdi/cmdi_system.cpp",
],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: Some(PayloadRef {
label: "cmdi-benign-cpp",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"benign_safe_cmdi_NYX_BENIGN",
label: "cmdi-benign-cpp",
oracle: Oracle::OutputContains("NYX_PWN_791_CMDI"),
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &[
"tests/benchmark/corpus/cpp/cmdi/cmdi_class_inline_method.cpp",
"tests/benchmark/corpus/cpp/cmdi/cmdi_exec.cpp",
"tests/benchmark/corpus/cpp/cmdi/cmdi_getline.cpp",
"tests/benchmark/corpus/cpp/cmdi/cmdi_lambda_passthrough.cpp",
"tests/benchmark/corpus/cpp/cmdi/cmdi_popen.cpp",
"tests/benchmark/corpus/cpp/cmdi/cmdi_stl_vector_string.cpp",
"tests/benchmark/corpus/cpp/cmdi/cmdi_system.cpp",
],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,46 @@
//! Go `Cap::CODE_EXEC` payloads.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b":; echo NYX_PWN_$((113*7))_CMDI",
label: "cmdi-echo-marker-go",
oracle: Oracle::OutputContains("NYX_PWN_791_CMDI"),
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &[
"tests/benchmark/corpus/go/cmdi/cmdi_direct.go",
"tests/benchmark/corpus/go/cmdi/cmdi_indirect.go",
"tests/benchmark/corpus/go/cmdi/cmdi_unvalidated_queue_element.go",
"tests/benchmark/corpus/go/cmdi/vuln_error_log_then_sink.go",
],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: Some(PayloadRef {
label: "cmdi-benign-go",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"benign_safe_cmdi_NYX_BENIGN",
label: "cmdi-benign-go",
oracle: Oracle::OutputContains("NYX_PWN_791_CMDI"),
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &[
"tests/benchmark/corpus/go/cmdi/cmdi_direct.go",
"tests/benchmark/corpus/go/cmdi/cmdi_indirect.go",
"tests/benchmark/corpus/go/cmdi/cmdi_unvalidated_queue_element.go",
"tests/benchmark/corpus/go/cmdi/vuln_error_log_then_sink.go",
],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,42 @@
//! Java `Cap::CODE_EXEC` payloads.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b":; echo NYX_PWN_$((113*7))_CMDI",
label: "cmdi-echo-marker-java",
oracle: Oracle::OutputContains("NYX_PWN_791_CMDI"),
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &[
"tests/benchmark/corpus/java/cmdi/CmdiDirect.java",
"tests/benchmark/corpus/java/cmdi/CmdiIndirect.java",
],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: Some(PayloadRef {
label: "cmdi-benign-java",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"benign_safe_cmdi_NYX_BENIGN",
label: "cmdi-benign-java",
oracle: Oracle::OutputContains("NYX_PWN_791_CMDI"),
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &[
"tests/benchmark/corpus/java/cmdi/CmdiDirect.java",
"tests/benchmark/corpus/java/cmdi/CmdiIndirect.java",
],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,42 @@
//! JavaScript `Cap::CODE_EXEC` payloads.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b":; echo NYX_PWN_$((113*7))_CMDI",
label: "cmdi-echo-marker-javascript",
oracle: Oracle::OutputContains("NYX_PWN_791_CMDI"),
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &[
"tests/benchmark/corpus/javascript/cmdi/cmdi_direct.js",
"tests/benchmark/corpus/javascript/cmdi/cmdi_indirect.js",
],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: Some(PayloadRef {
label: "cmdi-benign-javascript",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"benign_safe_cmdi_NYX_BENIGN",
label: "cmdi-benign-javascript",
oracle: Oracle::OutputContains("NYX_PWN_791_CMDI"),
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &[
"tests/benchmark/corpus/javascript/cmdi/cmdi_direct.js",
"tests/benchmark/corpus/javascript/cmdi/cmdi_indirect.js",
],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,12 @@
//! Command-injection (`Cap::CODE_EXEC`) per-language payload slices.
pub mod c;
pub mod cpp;
pub mod go;
pub mod java;
pub mod javascript;
pub mod php;
pub mod python;
pub mod ruby;
pub mod rust;
pub mod typescript;

View file

@ -0,0 +1,42 @@
//! PHP `Cap::CODE_EXEC` payloads.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b":; echo NYX_PWN_$((113*7))_CMDI",
label: "cmdi-echo-marker-php",
oracle: Oracle::OutputContains("NYX_PWN_791_CMDI"),
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &[
"tests/benchmark/corpus/php/cmdi/cmdi_direct.php",
"tests/benchmark/corpus/php/cmdi/cmdi_indirect.php",
],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: Some(PayloadRef {
label: "cmdi-benign-php",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"benign_safe_cmdi_NYX_BENIGN",
label: "cmdi-benign-php",
oracle: Oracle::OutputContains("NYX_PWN_791_CMDI"),
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &[
"tests/benchmark/corpus/php/cmdi/cmdi_direct.php",
"tests/benchmark/corpus/php/cmdi/cmdi_indirect.php",
],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,48 @@
//! Python `Cap::CODE_EXEC` payloads.
//!
//! Same shell-syntax bytes as [`super::rust::PAYLOADS`]; the per-language
//! slice exists so the lookup is a per-language assertion rather than a
//! cross-language fallback through [`super::super::registry::payloads_for`].
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b":; echo NYX_PWN_$((113*7))_CMDI",
label: "cmdi-echo-marker-python",
oracle: Oracle::OutputContains("NYX_PWN_791_CMDI"),
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &[
"tests/benchmark/corpus/python/cmdi/cmdi_direct.py",
"tests/benchmark/corpus/python/cmdi/cmdi_indirect.py",
"tests/benchmark/corpus/python/cmdi/cmdi_popen_shell.py",
],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: Some(PayloadRef {
label: "cmdi-benign-python",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"benign_safe_cmdi_NYX_BENIGN",
label: "cmdi-benign-python",
oracle: Oracle::OutputContains("NYX_PWN_791_CMDI"),
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &[
"tests/benchmark/corpus/python/cmdi/cmdi_direct.py",
"tests/benchmark/corpus/python/cmdi/cmdi_indirect.py",
"tests/benchmark/corpus/python/cmdi/cmdi_popen_shell.py",
],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,44 @@
//! Ruby `Cap::CODE_EXEC` payloads.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b":; echo NYX_PWN_$((113*7))_CMDI",
label: "cmdi-echo-marker-ruby",
oracle: Oracle::OutputContains("NYX_PWN_791_CMDI"),
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &[
"tests/benchmark/corpus/ruby/cmdi/cmdi_backtick.rb",
"tests/benchmark/corpus/ruby/cmdi/cmdi_kernel_open.rb",
"tests/benchmark/corpus/ruby/cmdi/cmdi_system.rb",
],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: Some(PayloadRef {
label: "cmdi-benign-ruby",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"benign_safe_cmdi_NYX_BENIGN",
label: "cmdi-benign-ruby",
oracle: Oracle::OutputContains("NYX_PWN_791_CMDI"),
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &[
"tests/benchmark/corpus/ruby/cmdi/cmdi_backtick.rb",
"tests/benchmark/corpus/ruby/cmdi/cmdi_kernel_open.rb",
"tests/benchmark/corpus/ruby/cmdi/cmdi_system.rb",
],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,48 @@
//! Command-injection payloads exercised by Rust fixtures
//! (`tests/benchmark/corpus/rust/cmdi/`).
//!
//! Bytes are shell-syntax, not Rust-specific; Track J phases 0311 add
//! per-language slices (Python `os.system`, PHP `exec`, …) as new fixtures
//! land.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b":; echo NYX_PWN_$((113*7))_CMDI",
label: "cmdi-echo-marker",
oracle: Oracle::OutputContains("NYX_PWN_791_CMDI"),
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 1,
deprecated_at_corpus_version: None,
fixture_paths: &[
"tests/benchmark/corpus/rust/cmdi/cmdi_command.rs",
"tests/benchmark/corpus/rust/cmdi/cmdi_args.rs",
],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: Some(PayloadRef {
label: "cmdi-benign",
}),
no_benign_control_rationale: None,
},
// Benign control: plain text that should never produce the cmdi marker.
CuratedPayload {
bytes: b"benign_safe_cmdi_NYX_BENIGN",
label: "cmdi-benign",
oracle: Oracle::OutputContains("NYX_PWN_791_CMDI"),
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 4,
deprecated_at_corpus_version: None,
fixture_paths: &[
"tests/benchmark/corpus/rust/cmdi/cmdi_command.rs",
"tests/benchmark/corpus/rust/cmdi/cmdi_args.rs",
],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,42 @@
//! TypeScript `Cap::CODE_EXEC` payloads.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b":; echo NYX_PWN_$((113*7))_CMDI",
label: "cmdi-echo-marker-typescript",
oracle: Oracle::OutputContains("NYX_PWN_791_CMDI"),
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &[
"tests/benchmark/corpus/typescript/cmdi/cmdi_async_wrapper.ts",
"tests/benchmark/corpus/typescript/cmdi/cmdi_exec_template.ts",
],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: Some(PayloadRef {
label: "cmdi-benign-typescript",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"benign_safe_cmdi_NYX_BENIGN",
label: "cmdi-benign-typescript",
oracle: Oracle::OutputContains("NYX_PWN_791_CMDI"),
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &[
"tests/benchmark/corpus/typescript/cmdi/cmdi_async_wrapper.ts",
"tests/benchmark/corpus/typescript/cmdi/cmdi_exec_template.ts",
],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,50 @@
//! Go `Cap::CRYPTO` payloads — `math/rand.Intn` weak-key
//! generation.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
const WEAK_BITS: u32 = 16;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"NYX_CRYPTO_WEAK",
label: "crypto-go-weak-random",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::WeakKeyEntropy {
max_bits: WEAK_BITS,
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/crypto/go/vuln.go"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::WeakKeyEntropy {
max_bits: WEAK_BITS,
}],
benign_control: Some(PayloadRef {
label: "crypto-go-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"NYX_CRYPTO_STRONG",
label: "crypto-go-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::WeakKeyEntropy {
max_bits: WEAK_BITS,
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/crypto/go/benign.go"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,61 @@
//! Java `Cap::CRYPTO` payloads — `java.util.Random.nextBytes`
//! weak-key generation.
//!
//! Vuln payload: marker bytes that signal the harness to drive its
//! `java.util.Random` key-generation path. The harness emits a key
//! bounded inside a 16-bit search space and writes a
//! [`crate::dynamic::probe::ProbeKind::WeakKey`] probe — the
//! [`crate::dynamic::oracle::ProbePredicate::WeakKeyEntropy`]
//! predicate fires for `key_int < 2^16`.
//!
//! Benign control: marker bytes that route the harness through
//! `java.security.SecureRandom`, producing a 256-bit key whose
//! integer view trivially exceeds the budget.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
const WEAK_BITS: u32 = 16;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"NYX_CRYPTO_WEAK",
label: "crypto-java-weak-random",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::WeakKeyEntropy {
max_bits: WEAK_BITS,
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/crypto/java/vuln.java"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::WeakKeyEntropy {
max_bits: WEAK_BITS,
}],
benign_control: Some(PayloadRef {
label: "crypto-java-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"NYX_CRYPTO_STRONG",
label: "crypto-java-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::WeakKeyEntropy {
max_bits: WEAK_BITS,
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/crypto/java/benign.java"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,26 @@
//! Weak-crypto (`Cap::CRYPTO`) per-language payload slices.
//!
//! Phase 11 (Track J.9) carves a weak-key entropy oracle across the
//! five backend languages where homegrown key generation is common
//! enough to matter: Java (`java.util.Random.nextBytes` → key bytes),
//! Python (`random.randint(0, 0xFFFF)`), PHP (`mt_rand(0, 0xFFFF)`),
//! Go (`math/rand.Intn(0x10000)`), Rust (`rand::thread_rng` truncated
//! to 16 bits). Every vuln payload triggers the harness's
//! instrumented key-generation path with a seed that produces an
//! attacker-derivable key bounded inside the 16-bit search space.
//! The harness shim writes a
//! [`crate::dynamic::probe::ProbeKind::WeakKey { key_int }`] probe
//! with the produced integer view of the key bytes; the
//! [`crate::dynamic::oracle::ProbePredicate::WeakKeyEntropy`]
//! predicate fires when `key_int < 2^max_bits` (`max_bits = 16` by
//! default). The paired benign control routes the same harness
//! through a CSPRNG (`SecureRandom`, `secrets.token_bytes`,
//! `random_bytes(32)`, `crypto/rand.Read`, `rand::rngs::OsRng`) so
//! the produced `key_int` trivially exceeds the budget and the
//! predicate stays clear.
pub mod go;
pub mod java;
pub mod php;
pub mod python;
pub mod rust;

View file

@ -0,0 +1,49 @@
//! PHP `Cap::CRYPTO` payloads — `mt_rand` weak-key generation.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
const WEAK_BITS: u32 = 16;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"NYX_CRYPTO_WEAK",
label: "crypto-php-weak-random",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::WeakKeyEntropy {
max_bits: WEAK_BITS,
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/crypto/php/vuln.php"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::WeakKeyEntropy {
max_bits: WEAK_BITS,
}],
benign_control: Some(PayloadRef {
label: "crypto-php-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"NYX_CRYPTO_STRONG",
label: "crypto-php-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::WeakKeyEntropy {
max_bits: WEAK_BITS,
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/crypto/php/benign.php"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,59 @@
//! Python `Cap::CRYPTO` payloads — `random.randint` weak-key
//! generation.
//!
//! Vuln payload: marker bytes that route the harness through
//! `random.randint(0, 0xFFFF)`; the harness emits a
//! [`crate::dynamic::probe::ProbeKind::WeakKey`] probe and the
//! [`crate::dynamic::oracle::ProbePredicate::WeakKeyEntropy`]
//! predicate fires.
//!
//! Benign control: marker bytes that route the harness through
//! `secrets.token_bytes(32)`.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
const WEAK_BITS: u32 = 16;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"NYX_CRYPTO_WEAK",
label: "crypto-python-weak-random",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::WeakKeyEntropy {
max_bits: WEAK_BITS,
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/crypto/python/vuln.py"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::WeakKeyEntropy {
max_bits: WEAK_BITS,
}],
benign_control: Some(PayloadRef {
label: "crypto-python-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"NYX_CRYPTO_STRONG",
label: "crypto-python-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::WeakKeyEntropy {
max_bits: WEAK_BITS,
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/crypto/python/benign.py"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,50 @@
//! Rust `Cap::CRYPTO` payloads — `rand::thread_rng` weak-key
//! generation truncated to 16 bits.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
const WEAK_BITS: u32 = 16;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"NYX_CRYPTO_WEAK",
label: "crypto-rust-weak-random",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::WeakKeyEntropy {
max_bits: WEAK_BITS,
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/crypto/rust/vuln.rs"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::WeakKeyEntropy {
max_bits: WEAK_BITS,
}],
benign_control: Some(PayloadRef {
label: "crypto-rust-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"NYX_CRYPTO_STRONG",
label: "crypto-rust-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::WeakKeyEntropy {
max_bits: WEAK_BITS,
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/crypto/rust/benign.rs"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,49 @@
//! go `Cap::DATA_EXFIL` payloads.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
const ALLOWLIST: &[&str] = &["127.0.0.1", "localhost"];
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"attacker.test",
label: "data-exfil-go-external",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::OutboundHostNotIn {
allowlist: ALLOWLIST,
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/data_exfil/go/vuln.go"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::OutboundHostNotIn {
allowlist: ALLOWLIST,
}],
benign_control: Some(PayloadRef {
label: "data-exfil-go-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"127.0.0.1",
label: "data-exfil-go-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::OutboundHostNotIn {
allowlist: ALLOWLIST,
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/data_exfil/go/benign.go"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,49 @@
//! java `Cap::DATA_EXFIL` payloads.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
const ALLOWLIST: &[&str] = &["127.0.0.1", "localhost"];
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"attacker.test",
label: "data-exfil-java-external",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::OutboundHostNotIn {
allowlist: ALLOWLIST,
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/data_exfil/java/Vuln.java"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::OutboundHostNotIn {
allowlist: ALLOWLIST,
}],
benign_control: Some(PayloadRef {
label: "data-exfil-java-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"127.0.0.1",
label: "data-exfil-java-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::OutboundHostNotIn {
allowlist: ALLOWLIST,
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/data_exfil/java/Benign.java"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,49 @@
//! js `Cap::DATA_EXFIL` payloads.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
const ALLOWLIST: &[&str] = &["127.0.0.1", "localhost"];
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"attacker.test",
label: "data-exfil-js-external",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::OutboundHostNotIn {
allowlist: ALLOWLIST,
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/data_exfil/js/vuln.js"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::OutboundHostNotIn {
allowlist: ALLOWLIST,
}],
benign_control: Some(PayloadRef {
label: "data-exfil-js-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"127.0.0.1",
label: "data-exfil-js-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::OutboundHostNotIn {
allowlist: ALLOWLIST,
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/data_exfil/js/benign.js"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,22 @@
//! Data-exfiltration (`Cap::DATA_EXFIL`) per-language payload
//! slices.
//!
//! Phase 11 (Track J.9) carves an outbound-network oracle across
//! all seven backend-capable languages. Each harness stands up a
//! mock HTTP client that records the destination host of every
//! outbound request via a
//! [`crate::dynamic::probe::ProbeKind::OutboundNetwork { host }`]
//! probe. The
//! [`crate::dynamic::oracle::ProbePredicate::OutboundHostNotIn`]
//! predicate fires when the captured `host` falls outside the
//! configured loopback allowlist (`&["127.0.0.1", "localhost"]`).
//! The vuln payload supplies `attacker.test`; the paired benign
//! control supplies `127.0.0.1` so the predicate stays clear.
pub mod go;
pub mod java;
pub mod js;
pub mod php;
pub mod python;
pub mod ruby;
pub mod rust;

View file

@ -0,0 +1,49 @@
//! php `Cap::DATA_EXFIL` payloads.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
const ALLOWLIST: &[&str] = &["127.0.0.1", "localhost"];
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"attacker.test",
label: "data-exfil-php-external",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::OutboundHostNotIn {
allowlist: ALLOWLIST,
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/data_exfil/php/vuln.php"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::OutboundHostNotIn {
allowlist: ALLOWLIST,
}],
benign_control: Some(PayloadRef {
label: "data-exfil-php-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"127.0.0.1",
label: "data-exfil-php-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::OutboundHostNotIn {
allowlist: ALLOWLIST,
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/data_exfil/php/benign.php"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,49 @@
//! python `Cap::DATA_EXFIL` payloads.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
const ALLOWLIST: &[&str] = &["127.0.0.1", "localhost"];
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"attacker.test",
label: "data-exfil-python-external",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::OutboundHostNotIn {
allowlist: ALLOWLIST,
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/data_exfil/python/vuln.py"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::OutboundHostNotIn {
allowlist: ALLOWLIST,
}],
benign_control: Some(PayloadRef {
label: "data-exfil-python-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"127.0.0.1",
label: "data-exfil-python-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::OutboundHostNotIn {
allowlist: ALLOWLIST,
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/data_exfil/python/benign.py"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,49 @@
//! ruby `Cap::DATA_EXFIL` payloads.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
const ALLOWLIST: &[&str] = &["127.0.0.1", "localhost"];
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"attacker.test",
label: "data-exfil-ruby-external",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::OutboundHostNotIn {
allowlist: ALLOWLIST,
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/data_exfil/ruby/vuln.rb"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::OutboundHostNotIn {
allowlist: ALLOWLIST,
}],
benign_control: Some(PayloadRef {
label: "data-exfil-ruby-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"127.0.0.1",
label: "data-exfil-ruby-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::OutboundHostNotIn {
allowlist: ALLOWLIST,
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/data_exfil/ruby/benign.rb"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,49 @@
//! rust `Cap::DATA_EXFIL` payloads.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
const ALLOWLIST: &[&str] = &["127.0.0.1", "localhost"];
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"attacker.test",
label: "data-exfil-rust-external",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::OutboundHostNotIn {
allowlist: ALLOWLIST,
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/data_exfil/rust/vuln.rs"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::OutboundHostNotIn {
allowlist: ALLOWLIST,
}],
benign_control: Some(PayloadRef {
label: "data-exfil-rust-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"127.0.0.1",
label: "data-exfil-rust-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::OutboundHostNotIn {
allowlist: ALLOWLIST,
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/data_exfil/rust/benign.rs"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,62 @@
//! Java `Cap::DESERIALIZE` payloads.
//!
//! Vuln payload: a base64-encoded `java.io.ObjectInputStream` byte stream
//! that materialises a gadget class outside the harness's allowlist.
//! The harness's `RestrictedObjectInputStream.resolveClass` intercepts
//! the lookup and emits a `ProbeKind::Deserialize { gadget_chain_invoked
//! = true }` probe before aborting the chain.
//!
//! Benign control: a base64-encoded `ObjectInputStream` byte stream of a
//! single allow-listed `java.lang.Integer`. The class lives inside the
//! resolveClass allowlist so no Deserialize probe is emitted.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
// Marker class name embedded in the serialized stream — the
// harness allowlist contains `java.lang.Integer` and `java.lang.String`
// only. The byte form is a small literal so const-eval can keep it.
bytes: b"NYX_GADGET_CLASS:org.nyx.deserialize.Gadget",
label: "java-deserialize-gadget",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::DeserializeGadgetInvoked {
require_invoked: true,
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 7,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/deserialize/java/Vuln.java"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::DeserializeGadgetInvoked {
require_invoked: true,
}],
benign_control: Some(PayloadRef {
label: "java-deserialize-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
// Allow-listed payload — the marker carries `java.lang.Integer`,
// which the harness resolveClass accepts without writing a probe.
bytes: b"NYX_GADGET_CLASS:java.lang.Integer",
label: "java-deserialize-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::DeserializeGadgetInvoked {
require_invoked: true,
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 7,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/deserialize/java/Benign.java"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,17 @@
//! Deserialization (`Cap::DESERIALIZE`) per-language payload slices.
//!
//! Phase 03 (Track J.1) lands the first cap end-to-end: Java
//! (`ObjectInputStream.readObject` / `XMLDecoder`), Python (`pickle.loads`
//! / `yaml.unsafe_load`), PHP (`unserialize`), and Ruby (`Marshal.load`
//! / `YAML.load`). Every vuln payload is paired with a benign control
//! whose oracle should *not* fire — the per-language harness shims
//! emit a [`crate::dynamic::probe::ProbeKind::Deserialize`] record with
//! `gadget_chain_invoked: true` when a non-allowlisted gadget class is
//! materialised by the instrumented deserialiser; benign well-formed
//! serialized data does not reach the allowlist boundary and so leaves
//! no Deserialize probe.
pub mod java;
pub mod php;
pub mod python;
pub mod ruby;

View file

@ -0,0 +1,60 @@
//! PHP `Cap::DESERIALIZE` payloads.
//!
//! Vuln payload: marker string handed to `unserialize($input)` where the
//! harness wraps the call with `['allowed_classes' => false]` and an
//! observer on `__wakeup`. When `unserialize` materialises a
//! `__PHP_Incomplete_Class` from a non-allowlisted class name, the
//! observer emits a `ProbeKind::Deserialize { gadget_chain_invoked:
//! true }` probe.
//!
//! Benign control: serialised primitive (an `int`) that
//! `unserialize` materialises without engaging the allowlist boundary.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"NYX_GADGET_CLASS:PHP_Object_Injection_RCE",
label: "php-unserialize-gadget",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::DeserializeGadgetInvoked {
require_invoked: true,
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 7,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/deserialize/php/vuln.php"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::DeserializeGadgetInvoked {
require_invoked: true,
}],
benign_control: Some(PayloadRef {
label: "php-unserialize-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
// Allow-listed marker — the harness allowlist accepts
// `__primitive_int` as a no-op type representing a serialised
// integer literal.
bytes: b"NYX_GADGET_CLASS:__primitive_int",
label: "php-unserialize-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::DeserializeGadgetInvoked {
require_invoked: true,
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 7,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/deserialize/php/benign.php"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,56 @@
//! Python `Cap::DESERIALIZE` payloads.
//!
//! Vuln payload: marker string consumed by the harness shim which calls
//! `pickle.Unpickler(...).load()` with `find_class` overridden to record
//! a `ProbeKind::Deserialize { gadget_chain_invoked: true }` whenever a
//! non-allowlisted class is requested. The harness allowlists
//! `builtins.list` / `builtins.dict` / `builtins.int`; the marker class
//! `nyx.gadget.RCE` is outside that set.
//!
//! Benign control: payload requests only allow-listed builtins.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"NYX_GADGET_CLASS:nyx.gadget.RCE",
label: "python-pickle-gadget",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::DeserializeGadgetInvoked {
require_invoked: true,
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 7,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/deserialize/python/vuln.py"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::DeserializeGadgetInvoked {
require_invoked: true,
}],
benign_control: Some(PayloadRef {
label: "python-pickle-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"NYX_GADGET_CLASS:builtins.list",
label: "python-pickle-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::DeserializeGadgetInvoked {
require_invoked: true,
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 7,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/deserialize/python/benign.py"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,57 @@
//! Ruby `Cap::DESERIALIZE` payloads.
//!
//! Vuln payload: marker string consumed by the harness shim which calls
//! `Marshal.load(input)` with `Marshal.const_defined?`-style
//! instrumentation that records a `ProbeKind::Deserialize {
//! gadget_chain_invoked: true }` probe whenever a non-allowlisted
//! constant is materialised. The harness allowlist contains `Integer`
//! / `String` / `Array`.
//!
//! Benign control: marker requests only the allow-listed `Integer`
//! constant.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"NYX_GADGET_CLASS:Nyx::Gadget::RCE",
label: "ruby-marshal-gadget",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::DeserializeGadgetInvoked {
require_invoked: true,
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 7,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/deserialize/ruby/vuln.rb"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::DeserializeGadgetInvoked {
require_invoked: true,
}],
benign_control: Some(PayloadRef {
label: "ruby-marshal-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"NYX_GADGET_CLASS:Integer",
label: "ruby-marshal-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::DeserializeGadgetInvoked {
require_invoked: true,
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 7,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/deserialize/ruby/benign.rb"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,56 @@
//! Format-string (`Cap::FMT_STRING`) payloads exercised by C fixtures
//! (`tests/dynamic_fixtures/c/free_fn/`).
//!
//! The vuln payload confirms via a sink-site Crash probe rather than an
//! output marker. The bytes themselves are not load-bearing — the
//! detection contract is "process aborts inside the entry call AFTER the
//! crash-guard installs". Fixtures choose how to crash on the payload;
//! the canonical example is a `printf`-family sink that interprets `%n`
//! against a controlled destination pointer on a guard-compiled binary.
//!
//! The benign control must reach the same entry without crashing; its
//! bytes carry `NYX_BENIGN` so fixture code can short-circuit before
//! the fault path.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::SignalSet;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"%n%n%n%n%n%n%n%n%n%n",
label: "fmt-string-percent-n-crash",
oracle: Oracle::SinkCrash {
signals: SignalSet::all(),
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 5,
deprecated_at_corpus_version: None,
fixture_paths: &[
"tests/dynamic_fixtures/c/free_fn/sink_fault.c",
"tests/dynamic_fixtures/c/free_fn/setup_fault.c",
],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: Some(PayloadRef {
label: "fmt-string-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"benign_safe_fmt_NYX_BENIGN",
label: "fmt-string-benign",
oracle: Oracle::SinkCrash {
signals: SignalSet::all(),
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 5,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/c/free_fn/sink_fault.c"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,3 @@
//! Format-string (`Cap::FMT_STRING`) per-language payload slices.
pub mod c;

View file

@ -0,0 +1,56 @@
//! Go `Cap::HEADER_INJECTION` payloads —
//! `http.ResponseWriter.Header().Set` CRLF injection.
//!
//! Vuln payload: a cookie value followed by `\r\nSet-Cookie:
//! nyx-injected=pwn`. Spliced into the host's `w.Header().Set("Set-
//! Cookie", value)` call without CRLF stripping.
//!
//! Benign control: same logical cookie value pre-encoded with
//! `net/url.QueryEscape`. Captured value carries `%0D%0A` so the
//! predicate stays clear.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"nyx-session\r\nSet-Cookie: nyx-injected=pwn",
label: "header-injection-go-crlf",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::HeaderInjected {
header_name: "Set-Cookie",
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 12,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/header_injection/go/vuln.go"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::HeaderInjected {
header_name: "Set-Cookie",
}],
benign_control: Some(PayloadRef {
label: "header-injection-go-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"nyx-session%0D%0ASet-Cookie%3A%20nyx-injected%3Dpwn",
label: "header-injection-go-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::HeaderInjected {
header_name: "Set-Cookie",
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 12,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/header_injection/go/benign.go"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,122 @@
//! Java `Cap::HEADER_INJECTION` payloads —
//! `HttpServletResponse.setHeader` CRLF injection.
//!
//! Vuln payload: a cookie value followed by `\r\nSet-Cookie:
//! nyx-injected=pwn`. Concatenated into the host's
//! `response.setHeader("Set-Cookie", value)` call without CRLF
//! stripping, the wire response carries the attacker's second
//! header. The harness's instrumented `setHeader` records a
//! `ProbeKind::HeaderEmit { name: "Set-Cookie", value: <raw bytes> }`
//! probe with the unescaped CRLF intact.
//!
//! Benign control: same logical session-id, but the harness's
//! benign code path runs the value through `URLEncoder.encode(...,
//! "UTF-8")` so the carried bytes become
//! `nyx-session%0D%0ASet-Cookie%3A%20nyx-injected%3Dpwn`. The
//! captured value has no literal `\r\n`; the
//! [`ProbePredicate::HeaderInjected`] predicate stays clear.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"nyx-session\r\nSet-Cookie: nyx-injected=pwn",
label: "header-injection-java-crlf",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::HeaderInjected {
header_name: "Set-Cookie",
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 12,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/header_injection/java/Vuln.java"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::HeaderInjected {
header_name: "Set-Cookie",
}],
benign_control: Some(PayloadRef {
label: "header-injection-java-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"nyx-session%0D%0ASet-Cookie%3A%20nyx-injected%3Dpwn",
label: "header-injection-java-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::HeaderInjected {
header_name: "Set-Cookie",
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 12,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/header_injection/java/Benign.java"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
// Phase 08 tier-(b): raw-socket wire-frame smuggling payload.
// Same CRLF-bearing bytes as the servlet payload above, but pinned
// to the `java_raw` fixture (a `java.net.ServerSocket` driven by
// `createServer` + `runOnce` that writes raw bytes via
// `OutputStream.write(byte[])`). The wire frame captured off the
// response socket carries two distinct `Set-Cookie:` lines, so
// `HeaderSmuggledInWire { primary: "Set-Cookie", smuggled:
// "Set-Cookie" }` fires — proving the smuggled header survived
// to the actual wire instead of being CRLF-stripped en route by
// Tomcat / Jetty / Undertow.
//
// Distinct payload (not just an extra predicate on the servlet
// row) because every modern Java servlet container response
// serializer strips CRLF at the wire-write boundary, so the
// wire-frame predicate would never fire against the canonical
// servlet fixture.
CuratedPayload {
bytes: b"nyx-session\r\nSet-Cookie: nyx-injected=pwn",
label: "header-injection-java-raw-wire-smuggle",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::HeaderSmuggledInWire {
primary: "Set-Cookie",
smuggled: "Set-Cookie",
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 12,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/header_injection/java_raw/Vuln.java"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::HeaderSmuggledInWire {
primary: "Set-Cookie",
smuggled: "Set-Cookie",
}],
benign_control: Some(PayloadRef {
label: "header-injection-java-raw-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"nyx-session%0D%0ASet-Cookie%3A%20nyx-injected%3Dpwn",
label: "header-injection-java-raw-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::HeaderSmuggledInWire {
primary: "Set-Cookie",
smuggled: "Set-Cookie",
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 12,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/header_injection/java_raw/Vuln.java"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,114 @@
//! JavaScript `Cap::HEADER_INJECTION` payloads —
//! `http.ServerResponse#setHeader` CRLF injection.
//!
//! Vuln payload: a cookie value followed by `\r\nSet-Cookie:
//! nyx-injected=pwn`. Spliced into the host's
//! `res.setHeader('Set-Cookie', value)` call without CRLF stripping.
//!
//! Benign control: same logical cookie value pre-encoded with
//! `encodeURIComponent`. Captured value carries `%0D%0A` so the
//! predicate stays clear.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"nyx-session\r\nSet-Cookie: nyx-injected=pwn",
label: "header-injection-js-crlf",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::HeaderInjected {
header_name: "Set-Cookie",
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 12,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/header_injection/js/vuln.js"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::HeaderInjected {
header_name: "Set-Cookie",
}],
benign_control: Some(PayloadRef {
label: "header-injection-js-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"nyx-session%0D%0ASet-Cookie%3A%20nyx-injected%3Dpwn",
label: "header-injection-js-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::HeaderInjected {
header_name: "Set-Cookie",
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 12,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/header_injection/js/benign.js"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
// Phase 08 tier-(b): raw-socket wire-frame smuggling payload.
// Same CRLF-bearing bytes as the Node payload above, but pinned to
// the `js_raw` fixture (a `net.createServer` callback writing raw
// bytes via `socket.write`). The wire frame captured off the
// response socket carries two distinct `Set-Cookie:` lines, so
// `HeaderSmuggledInWire { primary: "Set-Cookie", smuggled:
// "Set-Cookie" }` fires — proving the smuggled header survived to
// the actual wire instead of being CRLF-stripped en route.
//
// Distinct payload (not just an extra predicate on the Node row)
// because Node's `http.ServerResponse#setHeader` validator strips
// CRLF at the wire-write boundary, so the wire-frame predicate
// would never fire against the canonical Node fixture. See
// `.pitboss/play/deferred.md` (Phase 08 wire-frame option A) for
// the framework-level CRLF-strip empirical from session-0018.
CuratedPayload {
bytes: b"nyx-session\r\nSet-Cookie: nyx-injected=pwn",
label: "header-injection-js-raw-wire-smuggle",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::HeaderSmuggledInWire {
primary: "Set-Cookie",
smuggled: "Set-Cookie",
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 12,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/header_injection/js_raw/vuln.js"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::HeaderSmuggledInWire {
primary: "Set-Cookie",
smuggled: "Set-Cookie",
}],
benign_control: Some(PayloadRef {
label: "header-injection-js-raw-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"nyx-session%0D%0ASet-Cookie%3A%20nyx-injected%3Dpwn",
label: "header-injection-js-raw-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::HeaderSmuggledInWire {
primary: "Set-Cookie",
smuggled: "Set-Cookie",
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 12,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/header_injection/js_raw/vuln.js"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,31 @@
//! HTTP response-header CRLF injection (`Cap::HEADER_INJECTION`)
//! per-language payload slices.
//!
//! Phase 08 (Track J.6) carves header injection across the seven HTTP
//! framework ecosystems Nyx supports: Java (`HttpServletResponse.
//! setHeader`), Python (`flask.Response.headers.__setitem__`), PHP
//! (`header()`), Ruby (`Rack::Response#set_header`), JavaScript
//! (`http.ServerResponse#setHeader`), Go (`http.ResponseWriter.
//! Header().Set`), Rust (`axum`-style `HeaderMap::insert`). Every
//! vuln payload appends a `\r\n` followed by an injected header line
//! (`Set-Cookie: nyx-injected=pwn`) — once the host code splices the
//! attacker bytes into the response writer's value argument the wire
//! actually carries two headers instead of one. The paired benign
//! control passes the same logical value through the per-language URL
//! encoder so the captured value carries `%0d%0a` (not the raw
//! bytes), the encoded text is preserved verbatim inside a single
//! header value, and the differential rule stays clear.
//!
//! The oracle's
//! [`crate::dynamic::oracle::ProbePredicate::HeaderInjected`] reads
//! the per-payload `ProbeKind::HeaderEmit { name, value }` records
//! and fires when the value contains a literal CRLF byte pair —
//! vuln passes, benign clears, fulfilling the §4.1 differential rule.
pub mod go;
pub mod java;
pub mod js;
pub mod php;
pub mod python;
pub mod ruby;
pub mod rust;

View file

@ -0,0 +1,117 @@
//! PHP `Cap::HEADER_INJECTION` payloads — `header()` CRLF injection.
//!
//! Vuln payload: a cookie value followed by `\r\nSet-Cookie:
//! nyx-injected=pwn`. Concatenated into the host's `header("Set-
//! Cookie: " . $value)` call without CRLF stripping, the wire response
//! carries the attacker's second header. The harness's instrumented
//! `header()` records a `ProbeKind::HeaderEmit` probe with the
//! unescaped CRLF intact.
//!
//! Benign control: same logical cookie value pre-encoded with PHP's
//! `urlencode`. Captured value carries `%0D%0A` so the predicate
//! stays clear.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"nyx-session\r\nSet-Cookie: nyx-injected=pwn",
label: "header-injection-php-crlf",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::HeaderInjected {
header_name: "Set-Cookie",
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 12,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/header_injection/php/vuln.php"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::HeaderInjected {
header_name: "Set-Cookie",
}],
benign_control: Some(PayloadRef {
label: "header-injection-php-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"nyx-session%0D%0ASet-Cookie%3A%20nyx-injected%3Dpwn",
label: "header-injection-php-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::HeaderInjected {
header_name: "Set-Cookie",
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 12,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/header_injection/php/benign.php"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
// Phase 08 tier-(b): raw-socket wire-frame smuggling payload.
// Same CRLF-bearing bytes as the `header()` payload above, but
// pinned to the `php_raw` fixture (a `stream_socket_server` driven
// by `create_server` + `run_once` that writes raw bytes via
// `fwrite($conn, $raw)`). The wire frame captured off the
// response socket carries two distinct `Set-Cookie:` lines, so
// `HeaderSmuggledInWire { primary: "Set-Cookie", smuggled:
// "Set-Cookie" }` fires — proving the smuggled header survived to
// the actual wire instead of being CRLF-stripped en route.
//
// Distinct payload (not just an extra predicate on the `header()`
// row) because PHP's built-in `header()` rejects raw CRLF since
// 5.1.2 and modern Slim / Laravel / Symfony response serializers
// strip CRLF at the wire-write boundary, so the wire-frame
// predicate would never fire against the canonical `header()`
// fixture.
CuratedPayload {
bytes: b"nyx-session\r\nSet-Cookie: nyx-injected=pwn",
label: "header-injection-php-raw-wire-smuggle",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::HeaderSmuggledInWire {
primary: "Set-Cookie",
smuggled: "Set-Cookie",
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 12,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/header_injection/php_raw/vuln.php"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::HeaderSmuggledInWire {
primary: "Set-Cookie",
smuggled: "Set-Cookie",
}],
benign_control: Some(PayloadRef {
label: "header-injection-php-raw-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"nyx-session%0D%0ASet-Cookie%3A%20nyx-injected%3Dpwn",
label: "header-injection-php-raw-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::HeaderSmuggledInWire {
primary: "Set-Cookie",
smuggled: "Set-Cookie",
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 12,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/header_injection/php_raw/vuln.php"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,120 @@
//! Python `Cap::HEADER_INJECTION` payloads —
//! `flask.Response.headers.__setitem__` CRLF injection.
//!
//! Vuln payload: a session cookie value followed by `\r\nSet-Cookie:
//! nyx-injected=pwn`. Spliced into the host's
//! `response.headers["Set-Cookie"] = value` assignment without CRLF
//! stripping, the WSGI layer carries the attacker's second header on
//! the wire. The harness's instrumented response writer records a
//! `ProbeKind::HeaderEmit { name: "Set-Cookie", value: <raw bytes> }`
//! probe with the unescaped CRLF intact.
//!
//! Benign control: same logical cookie value pre-encoded with
//! `urllib.parse.quote`. The carried bytes become
//! `nyx-session%0D%0ASet-Cookie%3A%20nyx-injected%3Dpwn` — no literal
//! CRLF — and the [`ProbePredicate::HeaderInjected`] predicate stays
//! clear.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"nyx-session\r\nSet-Cookie: nyx-injected=pwn",
label: "header-injection-python-crlf",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::HeaderInjected {
header_name: "Set-Cookie",
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 12,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/header_injection/python/vuln.py"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::HeaderInjected {
header_name: "Set-Cookie",
}],
benign_control: Some(PayloadRef {
label: "header-injection-python-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"nyx-session%0D%0ASet-Cookie%3A%20nyx-injected%3Dpwn",
label: "header-injection-python-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::HeaderInjected {
header_name: "Set-Cookie",
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 12,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/header_injection/python/benign.py"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
// Phase 08 tier-(b): raw-socket wire-frame smuggling payload.
// Same CRLF-bearing bytes as the Flask payload above, but pinned
// to the `python_raw` fixture (a `BaseHTTPRequestHandler` writing
// raw bytes via `self.wfile.write`). The wire frame captured off
// the response socket carries two distinct `Set-Cookie:` lines, so
// `HeaderSmuggledInWire { primary: "Set-Cookie", smuggled:
// "Set-Cookie" }` fires — proving the smuggled header survived to
// the actual wire instead of being CRLF-stripped en route.
//
// Distinct payload (not just an extra predicate on the Flask row)
// because Flask's werkzeug response serializer strips CRLF at the
// wire-write boundary, so the wire-frame predicate would never
// fire against the canonical Flask fixture. See
// `.pitboss/play/deferred.md` (Phase 08 wire-frame option A) for
// the framework-level CRLF-strip empirical from session-0018.
CuratedPayload {
bytes: b"nyx-session\r\nSet-Cookie: nyx-injected=pwn",
label: "header-injection-python-raw-wire-smuggle",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::HeaderSmuggledInWire {
primary: "Set-Cookie",
smuggled: "Set-Cookie",
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 12,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/header_injection/python_raw/vuln.py"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::HeaderSmuggledInWire {
primary: "Set-Cookie",
smuggled: "Set-Cookie",
}],
benign_control: Some(PayloadRef {
label: "header-injection-python-raw-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"nyx-session%0D%0ASet-Cookie%3A%20nyx-injected%3Dpwn",
label: "header-injection-python-raw-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::HeaderSmuggledInWire {
primary: "Set-Cookie",
smuggled: "Set-Cookie",
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 12,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/header_injection/python_raw/vuln.py"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,114 @@
//! Ruby `Cap::HEADER_INJECTION` payloads —
//! `Rack::Response#set_header` CRLF injection.
//!
//! Vuln payload: a cookie value followed by `\r\nSet-Cookie:
//! nyx-injected=pwn`. Spliced into the host's
//! `response.set_header("Set-Cookie", value)` call without CRLF
//! stripping, the wire response carries the attacker's second header.
//!
//! Benign control: same logical cookie value pre-encoded with
//! `URI.encode_www_form_component`. Captured value carries `%0D%0A`
//! so the predicate stays clear.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"nyx-session\r\nSet-Cookie: nyx-injected=pwn",
label: "header-injection-ruby-crlf",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::HeaderInjected {
header_name: "Set-Cookie",
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 12,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/header_injection/ruby/vuln.rb"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::HeaderInjected {
header_name: "Set-Cookie",
}],
benign_control: Some(PayloadRef {
label: "header-injection-ruby-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"nyx-session%0D%0ASet-Cookie%3A%20nyx-injected%3Dpwn",
label: "header-injection-ruby-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::HeaderInjected {
header_name: "Set-Cookie",
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 12,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/header_injection/ruby/benign.rb"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
// Phase 08 tier-(b): raw-socket wire-frame smuggling payload.
// Same CRLF-bearing bytes as the Rack payload above, but pinned to
// the `ruby_raw` fixture (a `TCPServer` driven by `create_server`
// + `run_once` that writes raw bytes via `TCPSocket#write`). The
// wire frame captured off the response socket carries two
// distinct `Set-Cookie:` lines, so `HeaderSmuggledInWire { primary:
// "Set-Cookie", smuggled: "Set-Cookie" }` fires — proving the
// smuggled header survived to the actual wire instead of being
// CRLF-stripped en route.
//
// Distinct payload (not just an extra predicate on the Rack row)
// because Rack / Sinatra / Rails response serializers strip CRLF
// at the wire-write boundary, so the wire-frame predicate would
// never fire against the canonical Rack fixture.
CuratedPayload {
bytes: b"nyx-session\r\nSet-Cookie: nyx-injected=pwn",
label: "header-injection-ruby-raw-wire-smuggle",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::HeaderSmuggledInWire {
primary: "Set-Cookie",
smuggled: "Set-Cookie",
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 12,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/header_injection/ruby_raw/vuln.rb"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::HeaderSmuggledInWire {
primary: "Set-Cookie",
smuggled: "Set-Cookie",
}],
benign_control: Some(PayloadRef {
label: "header-injection-ruby-raw-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"nyx-session%0D%0ASet-Cookie%3A%20nyx-injected%3Dpwn",
label: "header-injection-ruby-raw-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::HeaderSmuggledInWire {
primary: "Set-Cookie",
smuggled: "Set-Cookie",
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 12,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/header_injection/ruby_raw/vuln.rb"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,116 @@
//! Rust `Cap::HEADER_INJECTION` payloads — `axum`-style
//! `HeaderMap::insert` CRLF injection.
//!
//! Vuln payload: a cookie value followed by `\r\nSet-Cookie:
//! nyx-injected=pwn`. Spliced into a hand-rolled `HeaderMap` insert
//! that bypasses the `HeaderValue::from_str` validity check (e.g.
//! `HeaderValue::from_bytes(...).unwrap()` over a tainted slice).
//!
//! Benign control: same logical cookie value pre-encoded with the
//! `percent-encoding` crate. Captured value carries `%0D%0A` so the
//! predicate stays clear.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"nyx-session\r\nSet-Cookie: nyx-injected=pwn",
label: "header-injection-rust-crlf",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::HeaderInjected {
header_name: "Set-Cookie",
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 12,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/header_injection/rust/vuln.rs"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::HeaderInjected {
header_name: "Set-Cookie",
}],
benign_control: Some(PayloadRef {
label: "header-injection-rust-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"nyx-session%0D%0ASet-Cookie%3A%20nyx-injected%3Dpwn",
label: "header-injection-rust-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::HeaderInjected {
header_name: "Set-Cookie",
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 12,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/header_injection/rust/benign.rs"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
// Phase 08 tier-(b): raw-socket wire-frame smuggling payload.
// Same CRLF-bearing bytes as the axum payload above, but pinned to
// the `rust_raw` fixture (a `std::net::TcpListener` driven by
// `create_server` + `run_once` that writes raw bytes via
// `TcpStream::write_all`). The wire frame captured off the
// response socket carries two distinct `Set-Cookie:` lines, so
// `HeaderSmuggledInWire { primary: "Set-Cookie", smuggled:
// "Set-Cookie" }` fires — proving the smuggled header survived to
// the actual wire instead of being CRLF-stripped en route.
//
// Distinct payload (not just an extra predicate on the axum row)
// because every framework's response serializer strips CRLF at
// the wire-write boundary, so the wire-frame predicate would
// never fire against the canonical axum fixture. See
// `.pitboss/play/deferred.md` (Phase 08 wire-frame option A) for
// the framework-level CRLF-strip empirical from session-0018.
CuratedPayload {
bytes: b"nyx-session\r\nSet-Cookie: nyx-injected=pwn",
label: "header-injection-rust-raw-wire-smuggle",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::HeaderSmuggledInWire {
primary: "Set-Cookie",
smuggled: "Set-Cookie",
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 12,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/header_injection/rust_raw/vuln.rs"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::HeaderSmuggledInWire {
primary: "Set-Cookie",
smuggled: "Set-Cookie",
}],
benign_control: Some(PayloadRef {
label: "header-injection-rust-raw-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"nyx-session%0D%0ASet-Cookie%3A%20nyx-injected%3Dpwn",
label: "header-injection-rust-raw-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::HeaderSmuggledInWire {
primary: "Set-Cookie",
smuggled: "Set-Cookie",
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 12,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/header_injection/rust_raw/vuln.rs"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,54 @@
//! Go `Cap::JSON_PARSE` payloads.
//!
//! The depth pair shares a single fixture; the payload tag
//! (`NYX_JSON_DEEP` vs `NYX_JSON_SHALLOW`) picks the branch. Go has
//! no prototype-pollution surface so the canary half of the slice is
//! intentionally omitted.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
const MAX_DEPTH: u32 = 64;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"NYX_JSON_DEEP",
label: "json-parse-go-depth-bomb",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::JsonParseExcessiveDepth {
max_depth: MAX_DEPTH,
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/json_parse_depth/go/vuln.go"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::JsonParseExcessiveDepth {
max_depth: MAX_DEPTH,
}],
benign_control: Some(PayloadRef {
label: "json-parse-go-depth-shallow",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"NYX_JSON_SHALLOW",
label: "json-parse-go-depth-shallow",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::JsonParseExcessiveDepth {
max_depth: MAX_DEPTH,
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/json_parse_depth/go/vuln.go"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,59 @@
//! Java `Cap::JSON_PARSE` payloads.
//!
//! The depth pair shares a single fixture; the payload tag
//! (`NYX_JSON_DEEP` vs `NYX_JSON_SHALLOW`) picks the branch. Java has
//! no prototype-pollution surface so the canary half of the slice is
//! intentionally omitted, matching the PHP / Go / Rust shape.
//!
//! Java has no stdlib JSON parser, so the harness ships a hand-rolled
//! iterative JSON walker as a sibling class (`NyxJsonProbe.java`); the
//! fixture calls `NyxJsonProbe.parse(text)` in place of any Jackson /
//! Gson dependency so the build path never reaches for an external jar.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
const MAX_DEPTH: u32 = 64;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"NYX_JSON_DEEP",
label: "json-parse-java-depth-bomb",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::JsonParseExcessiveDepth {
max_depth: MAX_DEPTH,
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/json_parse_depth/java/Vuln.java"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::JsonParseExcessiveDepth {
max_depth: MAX_DEPTH,
}],
benign_control: Some(PayloadRef {
label: "json-parse-java-depth-shallow",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"NYX_JSON_SHALLOW",
label: "json-parse-java-depth-shallow",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::JsonParseExcessiveDepth {
max_depth: MAX_DEPTH,
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/json_parse_depth/java/Vuln.java"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,93 @@
//! JavaScript `Cap::JSON_PARSE` payloads.
//!
//! Covers two oracle shapes: the prototype-canary pair reuses the
//! Phase 10 PROTOTYPE_POLLUTION canary
//! ([`crate::dynamic::oracle::ProbePredicate::PrototypeCanaryTouched`])
//! against a `JSON.parse`-then-deep-merge fixture, and the depth-bomb
//! pair drives `JSON.parse` past the 64-level depth budget for the
//! [`crate::dynamic::oracle::ProbePredicate::JsonParseExcessiveDepth`]
//! oracle. The depth pair shares a single fixture; the payload tag
//! (`NYX_JSON_DEEP` vs `NYX_JSON_SHALLOW`) picks the branch.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::{Canary, ProbePredicate};
const CANARY: &str = Canary::PLACEHOLDER;
const MAX_DEPTH: u32 = 64;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: br#"{"__proto__":{"__nyx_canary":"pwned"}}"#,
label: "json-parse-js-proto-key",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::PrototypeCanaryTouched { canary: CANARY }],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/json_parse/javascript/vuln.js"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::PrototypeCanaryTouched { canary: CANARY }],
benign_control: Some(PayloadRef {
label: "json-parse-js-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: br#"{"data":{"__nyx_canary":"pwned"}}"#,
label: "json-parse-js-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::PrototypeCanaryTouched { canary: CANARY }],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/json_parse/javascript/benign.js"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"NYX_JSON_DEEP",
label: "json-parse-js-depth-bomb",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::JsonParseExcessiveDepth {
max_depth: MAX_DEPTH,
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/json_parse_depth/javascript/vuln.js"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::JsonParseExcessiveDepth {
max_depth: MAX_DEPTH,
}],
benign_control: Some(PayloadRef {
label: "json-parse-js-depth-shallow",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"NYX_JSON_SHALLOW",
label: "json-parse-js-depth-shallow",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::JsonParseExcessiveDepth {
max_depth: MAX_DEPTH,
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/json_parse_depth/javascript/vuln.js"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,25 @@
//! JSON-parse pollution (`Cap::JSON_PARSE`) per-language payload
//! slices.
//!
//! Phase 11 (Track J.9) reuses the prototype-canary oracle from
//! Phase 10 across the three languages whose JSON parsers have a
//! published pollution surface: JavaScript (`JSON.parse` then deep
//! assign), Python (`json.loads` then `dict.update` /
//! `setattr`-driven attribute pollution), Ruby (`JSON.parse` then
//! recursive merge). Every vuln payload binds a JSON literal whose
//! top-level key is `__proto__`; the per-language harness's
//! instrumented canary trap (`Object.prototype.__nyx_canary` in JS,
//! a `dict`/class-scoped sentinel in Python, an `Object.prepend`
//! flag in Ruby) records a
//! [`crate::dynamic::probe::ProbeKind::PrototypePollution`] probe
//! once the malicious key reaches the shared chain. The paired
//! benign control sends a JSON literal whose top-level key is the
//! regular property `data`, leaving the chain untouched.
pub mod go;
pub mod java;
pub mod javascript;
pub mod php;
pub mod python;
pub mod ruby;
pub mod rust;

View file

@ -0,0 +1,54 @@
//! PHP `Cap::JSON_PARSE` payloads.
//!
//! The depth pair shares a single fixture; the payload tag
//! (`NYX_JSON_DEEP` vs `NYX_JSON_SHALLOW`) picks the branch. PHP has
//! no prototype-pollution surface so the canary half of the slice is
//! intentionally omitted.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
const MAX_DEPTH: u32 = 64;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"NYX_JSON_DEEP",
label: "json-parse-php-depth-bomb",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::JsonParseExcessiveDepth {
max_depth: MAX_DEPTH,
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/json_parse_depth/php/vuln.php"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::JsonParseExcessiveDepth {
max_depth: MAX_DEPTH,
}],
benign_control: Some(PayloadRef {
label: "json-parse-php-depth-shallow",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"NYX_JSON_SHALLOW",
label: "json-parse-php-depth-shallow",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::JsonParseExcessiveDepth {
max_depth: MAX_DEPTH,
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/json_parse_depth/php/vuln.php"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,88 @@
//! Python `Cap::JSON_PARSE` payloads.
//!
//! The canary cases cover pollution-style parses. The depth cases drive
//! `json.loads` past the depth oracle while sharing one fixture for the
//! vulnerable and benign attempts.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::{Canary, ProbePredicate};
const CANARY: &str = Canary::PLACEHOLDER;
const MAX_DEPTH: u32 = 64;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: br#"{"__proto__":{"__nyx_canary":"pwned"}}"#,
label: "json-parse-python-proto-key",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::PrototypeCanaryTouched { canary: CANARY }],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/json_parse/python/vuln.py"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::PrototypeCanaryTouched { canary: CANARY }],
benign_control: Some(PayloadRef {
label: "json-parse-python-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: br#"{"data":{"__nyx_canary":"pwned"}}"#,
label: "json-parse-python-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::PrototypeCanaryTouched { canary: CANARY }],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/json_parse/python/benign.py"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"NYX_JSON_DEEP",
label: "json-parse-python-depth-bomb",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::JsonParseExcessiveDepth {
max_depth: MAX_DEPTH,
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/json_parse_depth/python/vuln.py"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::JsonParseExcessiveDepth {
max_depth: MAX_DEPTH,
}],
benign_control: Some(PayloadRef {
label: "json-parse-python-depth-shallow",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"NYX_JSON_SHALLOW",
label: "json-parse-python-depth-shallow",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::JsonParseExcessiveDepth {
max_depth: MAX_DEPTH,
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/json_parse_depth/python/vuln.py"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,92 @@
//! Ruby `Cap::JSON_PARSE` payloads.
//!
//! Covers two oracle shapes: the prototype-canary pair reuses the
//! Phase 10 PROTOTYPE_POLLUTION canary against a `JSON.parse` then
//! recursive `Hash#deep_merge!` fixture, and the depth-bomb pair
//! drives `JSON.parse` past the 64-level depth budget for the
//! [`crate::dynamic::oracle::ProbePredicate::JsonParseExcessiveDepth`]
//! oracle. The depth pair shares a single fixture; the payload tag
//! (`NYX_JSON_DEEP` vs `NYX_JSON_SHALLOW`) picks the branch.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::{Canary, ProbePredicate};
const CANARY: &str = Canary::PLACEHOLDER;
const MAX_DEPTH: u32 = 64;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: br#"{"__proto__":{"__nyx_canary":"pwned"}}"#,
label: "json-parse-ruby-proto-key",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::PrototypeCanaryTouched { canary: CANARY }],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/json_parse/ruby/vuln.rb"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::PrototypeCanaryTouched { canary: CANARY }],
benign_control: Some(PayloadRef {
label: "json-parse-ruby-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: br#"{"data":{"__nyx_canary":"pwned"}}"#,
label: "json-parse-ruby-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::PrototypeCanaryTouched { canary: CANARY }],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/json_parse/ruby/benign.rb"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"NYX_JSON_DEEP",
label: "json-parse-ruby-depth-bomb",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::JsonParseExcessiveDepth {
max_depth: MAX_DEPTH,
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/json_parse_depth/ruby/vuln.rb"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::JsonParseExcessiveDepth {
max_depth: MAX_DEPTH,
}],
benign_control: Some(PayloadRef {
label: "json-parse-ruby-depth-shallow",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"NYX_JSON_SHALLOW",
label: "json-parse-ruby-depth-shallow",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::JsonParseExcessiveDepth {
max_depth: MAX_DEPTH,
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/json_parse_depth/ruby/vuln.rb"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,54 @@
//! Rust `Cap::JSON_PARSE` payloads.
//!
//! The depth pair shares a single fixture; the payload tag
//! (`NYX_JSON_DEEP` vs `NYX_JSON_SHALLOW`) picks the branch. Rust has
//! no prototype-pollution surface so the canary half of the slice is
//! intentionally omitted.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
const MAX_DEPTH: u32 = 64;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"NYX_JSON_DEEP",
label: "json-parse-rust-depth-bomb",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::JsonParseExcessiveDepth {
max_depth: MAX_DEPTH,
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/json_parse_depth/rust/vuln.rs"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::JsonParseExcessiveDepth {
max_depth: MAX_DEPTH,
}],
benign_control: Some(PayloadRef {
label: "json-parse-rust-depth-shallow",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"NYX_JSON_SHALLOW",
label: "json-parse-rust-depth-shallow",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::JsonParseExcessiveDepth {
max_depth: MAX_DEPTH,
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/json_parse_depth/rust/vuln.rs"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,53 @@
//! Java `Cap::LDAP_INJECTION` payloads — `LdapTemplate.search` /
//! `DirContext.search` filter injection.
//!
//! Vuln payload: a filter fragment whose `*)(uid=*` tail breaks out of
//! the host template's `(uid=…)` clause and rewraps the search as
//! `(|(uid=…)(uid=*))`, matching every user the directory carries.
//! The harness's instrumented LDAP client (talking to
//! [`crate::dynamic::stubs::ldap_server`]) records
//! `ProbeKind::Ldap { entries_returned: 3 }`.
//!
//! Benign control: the same intended username quoted through
//! `EscapeDN` so the LDAP filter stays pinned to a single entry; the
//! shim records `entries_returned: 1` and the oracle does not fire.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"alice*)(uid=*",
label: "ldap-java-filter-wildcard",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::QueryResultCountGreaterThan { n: 1 }],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 10,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/ldap_injection/java/Vuln.java"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::QueryResultCountGreaterThan { n: 1 }],
benign_control: Some(PayloadRef {
label: "ldap-java-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"alice",
label: "ldap-java-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::QueryResultCountGreaterThan { n: 1 }],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 10,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/ldap_injection/java/Benign.java"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,30 @@
//! LDAP filter injection (`Cap::LDAP_INJECTION`) per-language payload
//! slices.
//!
//! Phase 06 (Track J.4) carves LDAP filter injection across the three
//! most-common directory clients: Java (`LdapTemplate.search` /
//! `DirContext.search`), Python (`ldap.search_s`), and PHP
//! (`ldap_search`). Every vuln payload appends the canonical
//! `*)(uid=*` quote-escape break — once the host code substitutes the
//! attacker bytes into its filter template the synthesized LDAP
//! filter matches every entry the directory carries (the
//! [`crate::dynamic::stubs::ldap_server`] stub returns its three
//! provisioned users). The paired benign control quotes the same
//! bytes through `EscapeDN` / `ldap.dn.escape_filter_chars` /
//! `ldap_escape`, leaving the filter pinned to the originally
//! intended single user.
//!
//! The oracle's
//! [`crate::dynamic::oracle::ProbePredicate::QueryResultCountGreaterThan`]
//! checks the per-payload `ProbeKind::Ldap.entries_returned` against
//! `n = 1` — vuln passes (3 entries), benign clears (1 entry),
//! fulfilling the §4.1 differential rule.
//!
//! C# is intentionally omitted: the [`crate::symbol::Lang`] enum has
//! no `CSharp` variant, so the corpus has nowhere to register it.
//! Tracked in `.pitboss/play/deferred.md` alongside the Phase 05
//! Lang::CSharp gap.
pub mod java;
pub mod php;
pub mod python;

View file

@ -0,0 +1,51 @@
//! PHP `Cap::LDAP_INJECTION` payloads — `ldap_search` filter injection.
//!
//! Vuln payload: a filter fragment whose `*)(uid=*` tail breaks out of
//! the host template's `(uid=…)` clause; the synthesized filter
//! becomes `(|(uid=…)(uid=*))` and matches every directory entry.
//! The harness's instrumented `ldap_search` records
//! `ProbeKind::Ldap { entries_returned: 3 }`.
//!
//! Benign control: the same intended username quoted via
//! `ldap_escape($value, "", LDAP_ESCAPE_FILTER)` — `entries_returned:
//! 1`, oracle clear.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"alice*)(uid=*",
label: "ldap-php-filter-wildcard",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::QueryResultCountGreaterThan { n: 1 }],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 10,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/ldap_injection/php/vuln.php"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::QueryResultCountGreaterThan { n: 1 }],
benign_control: Some(PayloadRef {
label: "ldap-php-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"alice",
label: "ldap-php-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::QueryResultCountGreaterThan { n: 1 }],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 10,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/ldap_injection/php/benign.php"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,52 @@
//! Python `Cap::LDAP_INJECTION` payloads — `ldap.search_s` filter
//! injection.
//!
//! Vuln payload: a filter fragment whose `*)(uid=*` tail breaks out of
//! the host template's `(uid=…)` clause; the synthesized filter
//! becomes `(|(uid=…)(uid=*))` and matches every directory entry.
//! The harness's instrumented `ldap.search_s` records
//! `ProbeKind::Ldap { entries_returned: 3 }`.
//!
//! Benign control: the same intended username quoted via
//! `ldap.dn.escape_filter_chars`, leaving the filter pinned to a
//! single entry — `entries_returned: 1`, oracle clear.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"alice*)(uid=*",
label: "ldap-python-filter-wildcard",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::QueryResultCountGreaterThan { n: 1 }],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 10,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/ldap_injection/python/vuln.py"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::QueryResultCountGreaterThan { n: 1 }],
benign_control: Some(PayloadRef {
label: "ldap-python-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"alice",
label: "ldap-python-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::QueryResultCountGreaterThan { n: 1 }],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 10,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/ldap_injection/python/benign.py"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,89 @@
//! Go `Cap::OPEN_REDIRECT` payloads — `gin.Context.Redirect` /
//! `http.Redirect` off-origin redirect.
//!
//! Vuln payload: an absolute attacker URL spliced into
//! `c.Redirect(http.StatusFound, value)` (or
//! `http.Redirect(w, r, value, ...)`) without host validation; the
//! harness records a `ProbeKind::Redirect` probe whose `location`
//! points off-origin.
//!
//! Benign control: same shape but redirects to the same-origin path
//! `/dashboard`, so the captured `location` has no authority
//! component and the predicate stays clear.
//!
//! OOB-nonce variant (added 2026-05-22): when the runner attaches an
//! [`crate::dynamic::oob::OobListener`] the harness follows the
//! captured `Location:` URL via a real `net/http.Get` against the
//! loopback nonce URL so the listener records the per-finding
//! callback. Ordered first so the runner exercises the OOB observation
//! path before the absolute-URL vuln below triggers and short-circuits
//! iteration; runs without a listener skip cleanly (runner
//! `oob_nonce_slot` branch).
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
const ALLOWLIST: &[&str] = &["example.com"];
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
// `bytes` is unused when `oob_nonce_slot = true`; the runner
// substitutes a per-finding loopback URL (see runner.rs:405-413).
bytes: b"",
label: "open-redirect-go-oob-nonce",
oracle: Oracle::OobCallback { host: "127.0.0.1" },
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/go/vuln.go"],
oob_nonce_slot: true,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: Some(
"OOB-nonce open-redirect payload self-confirms via the per-finding listener \
callback when the harness follows the captured Location URL with net/http.Get; \
no benign URL can hit the nonce path.",
),
},
CuratedPayload {
bytes: b"https://attacker.test/",
label: "open-redirect-go-absolute",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::RedirectHostNotIn {
allowlist: ALLOWLIST,
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 13,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/go/vuln.go"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::RedirectHostNotIn {
allowlist: ALLOWLIST,
}],
benign_control: Some(PayloadRef {
label: "open-redirect-go-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"/dashboard",
label: "open-redirect-go-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::RedirectHostNotIn {
allowlist: ALLOWLIST,
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 13,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/go/benign.go"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,94 @@
//! Java `Cap::OPEN_REDIRECT` payloads —
//! `HttpServletResponse.sendRedirect` off-origin redirect.
//!
//! Vuln payload: a fully-qualified attacker URL
//! (`https://attacker.test/`). Spliced into the host's
//! `response.sendRedirect(value)` call without host validation, the
//! servlet response's `Location:` header points off-origin. The
//! harness's instrumented `sendRedirect` shim records a
//! `ProbeKind::Redirect { location: <raw>, request_host:
//! "example.com" }` probe; the predicate
//! [`crate::dynamic::oracle::ProbePredicate::RedirectHostNotIn`] sees
//! the off-allowlist host and fires.
//!
//! Benign control: same logical entry point, but the harness's benign
//! code path redirects to the relative path `/dashboard` (no host
//! component). The captured `location` has no off-origin authority
//! and the predicate stays clear.
//!
//! OOB-nonce variant (added 2026-05-22): when the runner attaches an
//! [`crate::dynamic::oob::OobListener`] the harness follows the
//! captured `Location:` URL via a real `HttpURLConnection.getInputStream()`
//! against the loopback nonce URL so the listener records the per-finding
//! callback. Ordered first so the runner exercises the OOB observation
//! path before the absolute-URL vuln below triggers and short-circuits
//! iteration; runs without a listener skip cleanly (runner
//! `oob_nonce_slot` branch).
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
const ALLOWLIST: &[&str] = &["example.com"];
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
// `bytes` is unused when `oob_nonce_slot = true`; the runner
// substitutes a per-finding loopback URL (see runner.rs:405-413).
bytes: b"",
label: "open-redirect-java-oob-nonce",
oracle: Oracle::OobCallback { host: "127.0.0.1" },
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/java/Vuln.java"],
oob_nonce_slot: true,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: Some(
"OOB-nonce open-redirect payload self-confirms via the per-finding listener \
callback when the harness follows the captured Location URL with \
HttpURLConnection.getInputStream; no benign URL can hit the nonce path.",
),
},
CuratedPayload {
bytes: b"https://attacker.test/",
label: "open-redirect-java-absolute",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::RedirectHostNotIn {
allowlist: ALLOWLIST,
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 13,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/java/Vuln.java"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::RedirectHostNotIn {
allowlist: ALLOWLIST,
}],
benign_control: Some(PayloadRef {
label: "open-redirect-java-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"/dashboard",
label: "open-redirect-java-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::RedirectHostNotIn {
allowlist: ALLOWLIST,
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 13,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/java/Benign.java"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,87 @@
//! JavaScript `Cap::OPEN_REDIRECT` payloads —
//! Express `res.redirect` off-origin redirect.
//!
//! Vuln payload: an absolute attacker URL spliced into
//! `res.redirect(value)` without host validation; the harness
//! records a `ProbeKind::Redirect` probe whose `location` points
//! off-origin.
//!
//! Benign control: same shape but redirects to the same-origin path
//! `/dashboard`, so the captured `location` has no authority
//! component and the predicate stays clear.
//!
//! OOB-nonce variant (added 2026-05-22): when the runner attaches an
//! [`crate::dynamic::oob::OobListener`] the harness follows the
//! captured `Location:` URL via a real `http.get` against the loopback
//! nonce URL so the listener records the per-finding callback. Ordered
//! first so the runner exercises the OOB observation path before the
//! absolute-URL vuln below triggers and short-circuits iteration; runs
//! without a listener skip cleanly (runner `oob_nonce_slot` branch).
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
const ALLOWLIST: &[&str] = &["example.com"];
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
// `bytes` is unused when `oob_nonce_slot = true`; the runner
// substitutes a per-finding loopback URL (see runner.rs:405-413).
bytes: b"",
label: "open-redirect-js-oob-nonce",
oracle: Oracle::OobCallback { host: "127.0.0.1" },
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/js/vuln.js"],
oob_nonce_slot: true,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: Some(
"OOB-nonce open-redirect payload self-confirms via the per-finding listener \
callback when the harness follows the captured Location URL with http.get; \
no benign URL can hit the nonce path.",
),
},
CuratedPayload {
bytes: b"https://attacker.test/",
label: "open-redirect-js-absolute",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::RedirectHostNotIn {
allowlist: ALLOWLIST,
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 13,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/js/vuln.js"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::RedirectHostNotIn {
allowlist: ALLOWLIST,
}],
benign_control: Some(PayloadRef {
label: "open-redirect-js-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"/dashboard",
label: "open-redirect-js-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::RedirectHostNotIn {
allowlist: ALLOWLIST,
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 13,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/js/benign.js"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,26 @@
//! Open-redirect (`Cap::OPEN_REDIRECT`) per-language payload slices.
//!
//! Phase 09 (Track J.7) carves open redirects across the seven HTTP
//! framework ecosystems Nyx supports: Java
//! (`HttpServletResponse.sendRedirect`), Python (`flask.redirect`),
//! PHP (Symfony `Response::redirect` / Slim `Response::withHeader`),
//! Ruby (`Rack::Response#redirect`), JavaScript (Express
//! `res.redirect`), Go (`gin.Context.Redirect`), Rust (`axum::response::
//! Redirect::to`). Every vuln payload binds an absolute attacker URL
//! (`https://attacker.test/`) into the response writer's redirect
//! entry point; the paired benign control redirects to a same-origin
//! path (`/dashboard`). The harness's instrumented redirect shim
//! records a [`crate::dynamic::probe::ProbeKind::Redirect { location,
//! request_host }`] probe with the unmodified location and the
//! request's origin host, and the
//! [`crate::dynamic::oracle::ProbePredicate::RedirectHostNotIn`]
//! predicate fires when the captured `location` resolves off-origin
//! relative to `allowlist {request_host}`.
pub mod go;
pub mod java;
pub mod js;
pub mod php;
pub mod python;
pub mod ruby;
pub mod rust;

View file

@ -0,0 +1,92 @@
//! PHP `Cap::OPEN_REDIRECT` payloads — `Response::redirect` /
//! Symfony `RedirectResponse(...)` off-origin redirect.
//!
//! Vuln payload: an absolute attacker URL passed to
//! `header("Location: $value")` or
//! `new \Symfony\Component\HttpFoundation\RedirectResponse($value)`
//! without host validation. The harness records a
//! `ProbeKind::Redirect { location, request_host }` probe and the
//! predicate fires on the off-allowlist host.
//!
//! Benign control: same shape but redirects to the same-origin path
//! `/dashboard`, so the captured `location` has no authority
//! component and the predicate stays clear.
//!
//! OOB-nonce variant (added 2026-05-22): when the runner attaches an
//! [`crate::dynamic::oob::OobListener`] the harness follows the
//! captured `Location:` URL via a real
//! `file_get_contents($location, stream_context_create(...))` against
//! the loopback nonce URL so the listener records the per-finding
//! callback. Ordered first so the runner exercises the OOB observation
//! path before the absolute-URL vuln below triggers and short-circuits
//! iteration; runs without a listener skip cleanly (runner
//! `oob_nonce_slot` branch).
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
const ALLOWLIST: &[&str] = &["example.com"];
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
// `bytes` is unused when `oob_nonce_slot = true`; the runner
// substitutes a per-finding loopback URL (see runner.rs:405-413).
bytes: b"",
label: "open-redirect-php-oob-nonce",
oracle: Oracle::OobCallback { host: "127.0.0.1" },
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/php/vuln.php"],
oob_nonce_slot: true,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: Some(
"OOB-nonce open-redirect payload self-confirms via the per-finding listener \
callback when the harness follows the captured Location URL with \
file_get_contents under a stream context timeout; no benign URL can hit \
the nonce path.",
),
},
CuratedPayload {
bytes: b"https://attacker.test/",
label: "open-redirect-php-absolute",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::RedirectHostNotIn {
allowlist: ALLOWLIST,
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 13,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/php/vuln.php"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::RedirectHostNotIn {
allowlist: ALLOWLIST,
}],
benign_control: Some(PayloadRef {
label: "open-redirect-php-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"/dashboard",
label: "open-redirect-php-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::RedirectHostNotIn {
allowlist: ALLOWLIST,
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 13,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/php/benign.php"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,89 @@
//! Python `Cap::OPEN_REDIRECT` payloads — `flask.redirect`
//! off-origin redirect.
//!
//! Vuln payload: an attacker-controlled absolute URL spliced into
//! `flask.redirect(value)` without host validation; the captured
//! `Location:` header points off-origin and the
//! [`crate::dynamic::oracle::ProbePredicate::RedirectHostNotIn`]
//! predicate fires.
//!
//! Benign control: same shape but redirects to the relative path
//! `/dashboard`, so the captured location has no authority component
//! and the predicate stays clear.
//!
//! OOB-nonce variant (added 2026-05-22): when the runner attaches an
//! [`crate::dynamic::oob::OobListener`] the harness follows the
//! captured `Location:` URL via a real `urllib.request.urlopen`
//! against the loopback nonce URL so the listener records the per-finding
//! callback. Ordered first so the runner exercises the OOB observation
//! path before the absolute-URL vuln below triggers and short-circuits
//! iteration; runs without a listener skip cleanly (runner
//! `oob_nonce_slot` branch).
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
const ALLOWLIST: &[&str] = &["example.com"];
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
// `bytes` is unused when `oob_nonce_slot = true`; the runner
// substitutes a per-finding loopback URL (see runner.rs:405-413).
bytes: b"",
label: "open-redirect-python-oob-nonce",
oracle: Oracle::OobCallback { host: "127.0.0.1" },
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/python/vuln.py"],
oob_nonce_slot: true,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: Some(
"OOB-nonce open-redirect payload self-confirms via the per-finding listener \
callback when the harness follows the captured Location URL with \
urllib.request.urlopen; no benign URL can hit the nonce path.",
),
},
CuratedPayload {
bytes: b"https://attacker.test/",
label: "open-redirect-python-absolute",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::RedirectHostNotIn {
allowlist: ALLOWLIST,
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 13,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/python/vuln.py"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::RedirectHostNotIn {
allowlist: ALLOWLIST,
}],
benign_control: Some(PayloadRef {
label: "open-redirect-python-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"/dashboard",
label: "open-redirect-python-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::RedirectHostNotIn {
allowlist: ALLOWLIST,
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 13,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/python/benign.py"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,88 @@
//! Ruby `Cap::OPEN_REDIRECT` payloads —
//! `Rack::Response#redirect` off-origin redirect.
//!
//! Vuln payload: an absolute attacker URL spliced into
//! `response.redirect(value)` without host validation; the harness
//! records a `ProbeKind::Redirect` probe whose `location` points
//! off-origin.
//!
//! Benign control: same shape but redirects to the same-origin path
//! `/dashboard`, so the captured `location` has no authority
//! component and the predicate stays clear.
//!
//! OOB-nonce variant (added 2026-05-22): when the runner attaches an
//! [`crate::dynamic::oob::OobListener`] the harness follows the
//! captured `Location:` URL via a real `Net::HTTP.get_response` against
//! the loopback nonce URL so the listener records the per-finding
//! callback. Ordered first so the runner exercises the OOB observation
//! path before the absolute-URL vuln below triggers and short-circuits
//! iteration; runs without a listener skip cleanly (runner
//! `oob_nonce_slot` branch).
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
const ALLOWLIST: &[&str] = &["example.com"];
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
// `bytes` is unused when `oob_nonce_slot = true`; the runner
// substitutes a per-finding loopback URL (see runner.rs:405-413).
bytes: b"",
label: "open-redirect-ruby-oob-nonce",
oracle: Oracle::OobCallback { host: "127.0.0.1" },
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/ruby/vuln.rb"],
oob_nonce_slot: true,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: Some(
"OOB-nonce open-redirect payload self-confirms via the per-finding listener \
callback when the harness follows the captured Location URL with \
Net::HTTP.get_response; no benign URL can hit the nonce path.",
),
},
CuratedPayload {
bytes: b"https://attacker.test/",
label: "open-redirect-ruby-absolute",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::RedirectHostNotIn {
allowlist: ALLOWLIST,
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 13,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/ruby/vuln.rb"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::RedirectHostNotIn {
allowlist: ALLOWLIST,
}],
benign_control: Some(PayloadRef {
label: "open-redirect-ruby-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"/dashboard",
label: "open-redirect-ruby-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::RedirectHostNotIn {
allowlist: ALLOWLIST,
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 13,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/ruby/benign.rb"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,88 @@
//! Rust `Cap::OPEN_REDIRECT` payloads — `axum::response::Redirect::to`
//! off-origin redirect.
//!
//! Vuln payload: an absolute attacker URL spliced into
//! `Redirect::to(value)` without host validation; the harness
//! records a `ProbeKind::Redirect` probe whose `location` points
//! off-origin.
//!
//! Benign control: same shape but redirects to the same-origin path
//! `/dashboard`, so the captured `location` has no authority
//! component and the predicate stays clear.
//!
//! OOB-nonce variant (added 2026-05-22): when the runner attaches an
//! [`crate::dynamic::oob::OobListener`] the harness follows the
//! captured `Location:` URL via a zero-dep `std::net::TcpStream`
//! `GET / HTTP/1.0` against the loopback nonce URL so the listener
//! records the per-finding callback. Ordered first so the runner
//! exercises the OOB observation path before the absolute-URL vuln
//! below triggers and short-circuits iteration; runs without a
//! listener skip cleanly (runner `oob_nonce_slot` branch).
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
const ALLOWLIST: &[&str] = &["example.com"];
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
// `bytes` is unused when `oob_nonce_slot = true`; the runner
// substitutes a per-finding loopback URL (see runner.rs:405-413).
bytes: b"",
label: "open-redirect-rust-oob-nonce",
oracle: Oracle::OobCallback { host: "127.0.0.1" },
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/rust/vuln.rs"],
oob_nonce_slot: true,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: Some(
"OOB-nonce open-redirect payload self-confirms via the per-finding listener \
callback when the harness follows the captured Location URL with a zero-dep \
TcpStream-based GET; no benign URL can hit the nonce path.",
),
},
CuratedPayload {
bytes: b"https://attacker.test/",
label: "open-redirect-rust-absolute",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::RedirectHostNotIn {
allowlist: ALLOWLIST,
}],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 13,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/rust/vuln.rs"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::RedirectHostNotIn {
allowlist: ALLOWLIST,
}],
benign_control: Some(PayloadRef {
label: "open-redirect-rust-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"/dashboard",
label: "open-redirect-rust-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::RedirectHostNotIn {
allowlist: ALLOWLIST,
}],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 13,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/open_redirect/rust/benign.rs"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,71 @@
//! Java `Cap::FILE_IO` path-traversal payloads (entry-driven servlet harness).
//!
//! The vulnerable payload escapes the fixture's `testfileDir`
//! (`<workdir>/testfiles/`) one level up to a canary file the harness plants at
//! the workdir root. The oracle marker is the canary file's CONTENT
//! ([`CANARY_MARKER`]), which is deliberately NOT a substring of the path
//! payload: an OWASP fixture that merely echoes the requested filename back to
//! the response (e.g. `getWriter().write("...'" + fileName + "'...")`) or logs
//! it (`System.out.println(... fileName ...)`) cannot reproduce the marker —
//! only an unsanitised `new FileInputStream(testfileDir + param)` that actually
//! opens and reads the canary does. This is the FILE_IO analogue of the
//! collision-resistant CODE_EXEC computed marker (`NYX_PWN_791`).
//!
//! The host sandbox (`path_traversal.sb`) denies `/etc/passwd` &c. but allows
//! reads inside the workdir, so the legacy `/etc/passwd` → `root:` rust payload
//! cannot confirm under isolation; the planted canary lives in the readable
//! workdir instead.
//!
//! The benign control names a file that does not exist under `testfileDir`, so
//! the same content-marker oracle cannot fire on it.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
/// Canary filename planted at the harness workdir root (the parent of
/// `testfiles/`). The Java emitter stages `<workdir>/nyx_pt_canary` with
/// [`CANARY_MARKER`] as its content plus an empty `testfiles/` directory so the
/// `../nyx_pt_canary` traversal resolves.
pub const CANARY_FILENAME: &str = "nyx_pt_canary";
/// Canary file content for the collision-resistant FILE_IO marker. It uses
/// alphanumeric characters plus underscore, so a faithful HTML/URL escaper
/// leaves it intact when the fixture writes the read bytes to the response.
/// NOT a substring of any payload path.
pub const CANARY_MARKER: &str = "NYX_PATHTRAVERSAL_R34D_a7f3c1d8";
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
// `testfileDir + "../nyx_pt_canary"` == `<workdir>/testfiles/../nyx_pt_canary`
// == `<workdir>/nyx_pt_canary` (the planted canary).
bytes: b"../nyx_pt_canary",
label: "path-traversal-canary-java",
oracle: Oracle::OutputContains(CANARY_MARKER),
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 17,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/benchmark/corpus/java/path_traversal/PathTraversalServlet.java"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: Some(PayloadRef {
label: "path-traversal-benign-java",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
// No traversal and no such file under `testfileDir`, so the canary is
// never read and the content marker cannot appear.
bytes: b"nyx_pt_benign_absent_NYX_BENIGN",
label: "path-traversal-benign-java",
oracle: Oracle::OutputContains(CANARY_MARKER),
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 17,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/benchmark/corpus/java/path_traversal/PathTraversalServlet.java"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,4 @@
//! Path-traversal (`Cap::FILE_IO`) per-language payload slices.
pub mod java;
pub mod rust;

View file

@ -0,0 +1,43 @@
//! Path-traversal payloads exercised by Rust fixtures
//! (`tests/benchmark/corpus/rust/path_traversal/`).
//!
//! Vuln payload reads `/etc/passwd`; benign payload names a file that does
//! not exist so the same oracle marker cannot fire.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"../../../../etc/passwd",
label: "path-traversal-passwd",
oracle: Oracle::OutputContains("root:"),
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 1,
deprecated_at_corpus_version: None,
fixture_paths: &[
"tests/benchmark/corpus/rust/path_traversal/path_file_open.rs",
"tests/benchmark/corpus/rust/path_traversal/path_read.rs",
],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: Some(PayloadRef {
label: "path-traversal-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"benign_safe_file_that_does_not_exist_NYX_BENIGN",
label: "path-traversal-benign",
oracle: Oracle::OutputContains("root:"),
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 1,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/benchmark/corpus/rust/path_traversal/path_file_open.rs"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,64 @@
//! JavaScript `Cap::PROTOTYPE_POLLUTION` payloads.
//!
//! Vuln payload: a JSON literal whose top-level key is `__proto__`.
//! When the host's deep-merge / `Object.assign` / JSON-parse-then-
//! deep-assign sink walks the key into the merge target, the
//! `__proto__` reference reaches `Object.prototype` and writes
//! `__nyx_canary`. The harness's instrumented setter trap records a
//! `ProbeKind::PrototypePollution { property: "__nyx_canary", … }`
//! probe; the predicate
//! [`crate::dynamic::oracle::ProbePredicate::PrototypeCanaryTouched`]
//! fires on the captured canary write.
//!
//! Benign control: a JSON literal whose top-level key is the regular
//! property `data`. Naive deep-merge walks `target.data` rather than
//! `target.__proto__`, so the prototype chain stays untouched and the
//! predicate stays clear. The matching fixture
//! (`tests/dynamic_fixtures/prototype_pollution/javascript/benign.js`)
//! additionally demonstrates the canonical mitigation pattern of
//! using `Object.create(null)` as the merge target — static analysis
//! of the fixture surfaces the prototype-less target while the
//! runtime differential is driven by the absence of a `__proto__` key
//! in the payload bytes.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::{Canary, ProbePredicate};
const CANARY: &str = Canary::PLACEHOLDER;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: br#"{"__proto__":{"__nyx_canary":"pwned"}}"#,
label: "prototype-pollution-js-proto-key",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::PrototypeCanaryTouched { canary: CANARY }],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 14,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/prototype_pollution/javascript/vuln.js"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::PrototypeCanaryTouched { canary: CANARY }],
benign_control: Some(PayloadRef {
label: "prototype-pollution-js-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: br#"{"data":{"__nyx_canary":"pwned"}}"#,
label: "prototype-pollution-js-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::PrototypeCanaryTouched { canary: CANARY }],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 14,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/prototype_pollution/javascript/benign.js"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,20 @@
//! Prototype-pollution (`Cap::PROTOTYPE_POLLUTION`) per-language
//! payload slices.
//!
//! Phase 10 (Track J.8) carves the JavaScript / TypeScript prototype-
//! pollution gadget against three sink families: `lodash.merge`,
//! `Object.assign` with tainted RHS, and `JSON.parse`-then-deep-assign.
//! Every vuln payload binds a JSON literal whose top-level key is
//! `__proto__`; the harness's instrumented deep-merge walks the key
//! into `Object.prototype` and a `Proxy`-style setter trap on
//! `Object.prototype.__nyx_canary` records a
//! [`crate::dynamic::probe::ProbeKind::PrototypePollution`] probe. The
//! paired benign control sends a JSON literal whose top-level key is
//! the regular property `data`, leaving the prototype chain
//! untouched. The
//! [`crate::dynamic::oracle::ProbePredicate::PrototypeCanaryTouched`]
//! predicate fires only on probes whose `property` equals the canary
//! name (`__nyx_canary`).
pub mod javascript;
pub mod typescript;

View file

@ -0,0 +1,50 @@
//! TypeScript `Cap::PROTOTYPE_POLLUTION` payloads.
//!
//! Mirrors [`super::javascript`] — the runtime is Node.js in both
//! cases, so the payload shape and oracle predicate are identical.
//! The per-language slice exists so the lang-aware corpus resolver
//! pairs TS vuln payloads against TS benign controls without crossing
//! the JS slice (and so the fixture paths point at the TS-specific
//! fixtures the static-analysis side consumes).
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::{Canary, ProbePredicate};
const CANARY: &str = Canary::PLACEHOLDER;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: br#"{"__proto__":{"__nyx_canary":"pwned"}}"#,
label: "prototype-pollution-ts-proto-key",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::PrototypeCanaryTouched { canary: CANARY }],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 14,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/prototype_pollution/typescript/vuln.ts"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::PrototypeCanaryTouched { canary: CANARY }],
benign_control: Some(PayloadRef {
label: "prototype-pollution-ts-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: br#"{"data":{"__nyx_canary":"pwned"}}"#,
label: "prototype-pollution-ts-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::PrototypeCanaryTouched { canary: CANARY }],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 14,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/prototype_pollution/typescript/benign.ts"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,7 @@
//! SQLi (`Cap::SQL_QUERY`) per-language payload slices.
//!
//! Each submodule exposes a `pub const PAYLOADS: &[CuratedPayload]` slice
//! registered against `(Cap::SQL_QUERY, Lang::<lang>)` in
//! [`super::registry::CORPUS`].
pub mod rust;

View file

@ -0,0 +1,57 @@
//! SQLi payloads exercised by Rust fixtures (`tests/benchmark/corpus/rust/sqli/`).
//!
//! Payload bytes are SQL-syntax, not Rust-specific; the `Lang::Rust` slot
//! reflects the fixture that currently drives them. Track J phases 0311
//! add per-language slices as new fixtures land.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"' OR '1'='1",
label: "sqli-tautology",
oracle: Oracle::OutputContains("NYX_SQL_CONFIRMED"),
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 1,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/benchmark/corpus/rust/sqli/sqli_rusqlite_format.rs"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: Some(PayloadRef {
label: "sqli-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"' UNION SELECT 'NYX_SQL_CONFIRMED'--",
label: "sqli-union-nyx",
oracle: Oracle::OutputContains("NYX_SQL_CONFIRMED"),
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 1,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/benchmark/corpus/rust/sqli/sqli_rusqlite_format.rs"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: Some(PayloadRef {
label: "sqli-benign",
}),
no_benign_control_rationale: None,
},
// Benign control: ordinary value that should never produce the SQL marker.
CuratedPayload {
bytes: b"benign_safe_sqli_NYX_BENIGN",
label: "sqli-benign",
oracle: Oracle::OutputContains("NYX_SQL_CONFIRMED"),
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 4,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/benchmark/corpus/rust/sqli/sqli_rusqlite_format.rs"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,3 @@
//! SSRF (`Cap::SSRF`) per-language payload slices.
pub mod rust;

View file

@ -0,0 +1,73 @@
//! SSRF payloads exercised by Rust fixtures
//! (`tests/benchmark/corpus/rust/ssrf/`).
//!
//! Two variants:
//! 1. `file://` scheme — static payload, `OutputContains` oracle. Works in
//! the process backend without OOB infrastructure.
//! 2. OOB nonce slot — URL generated at runtime from the OOB listener.
//! Confirms SSRF by recording the callback nonce.
//!
//! Oracle notes:
//! `OutputContains("daemon:")` matches both Linux (`daemon:x:1:1:`) and
//! macOS (`daemon:*:1:1:`) `/etc/passwd` formats and must NOT collide with
//! FILE_IO's `"root:"` marker (see marker_uniqueness test).
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"file:///etc/passwd",
label: "ssrf-file-scheme",
oracle: Oracle::OutputContains("daemon:"),
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 1,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/benchmark/corpus/rust/ssrf/ssrf_reqwest.rs"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: Some(PayloadRef {
label: "ssrf-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
// `bytes` is unused when `oob_nonce_slot = true`; the runner
// materialises the URL from the OOB listener at call time.
bytes: b"",
label: "ssrf-oob-nonce",
oracle: Oracle::OobCallback { host: "127.0.0.1" },
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 2,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/benchmark/corpus/rust/ssrf/ssrf_reqwest.rs"],
oob_nonce_slot: true,
probe_predicates: &[],
// OOB-nonce payloads are self-confirming via the listener; no benign
// counterpart is meaningful (a benign URL can never hit the nonce
// listener), so this entry sits at `NoControl`.
benign_control: None,
no_benign_control_rationale: Some(
"OOB-nonce payload self-confirms via the per-finding listener callback; \
no benign URL can hit the nonce path, so no paired control is meaningful.",
),
},
// Benign control for the file-scheme SSRF variant. Fetched the same
// way as the vuln payload but cannot resolve to a body containing the
// `daemon:` marker.
CuratedPayload {
bytes: b"benign_safe_ssrf_NYX_BENIGN",
label: "ssrf-benign",
oracle: Oracle::OutputContains("daemon:"),
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 4,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/benchmark/corpus/rust/ssrf/ssrf_reqwest.rs"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,46 @@
//! Java Thymeleaf `Cap::SSTI` payloads.
//!
//! Vuln payload: `[[${7*7}]]` — Thymeleaf evaluates the SpEL-style
//! expression inside the inlined-output marker and renders `49`.
//! Benign control sends the literal `7*7` text; without the `[[${...}]]`
//! markers Thymeleaf passes the payload through unchanged.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"[[${7*7}]]",
label: "ssti-thymeleaf-eval",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::TemplateEvalEqual { expected: 49 }],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 8,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/ssti/java_thymeleaf/vuln.java"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::TemplateEvalEqual { expected: 49 }],
benign_control: Some(PayloadRef {
label: "ssti-thymeleaf-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"7*7",
label: "ssti-thymeleaf-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::TemplateEvalEqual { expected: 49 }],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 8,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/ssti/java_thymeleaf/benign.java"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,52 @@
//! JavaScript Handlebars `Cap::SSTI` payloads.
//!
//! Handlebars does not evaluate arbitrary arithmetic in `{{ ... }}`
//! expressions out of the box, so the vuln payload reaches the engine
//! through the built-in `lookup` helper combined with a constructor
//! gadget chain: `{{#with (lookup this 'constructor')}}{{lookup
//! this 'constructor'}}{{/with}}` is the canonical pattern, but the
//! evaluation marker we need ("rendered constant only via eval")
//! reduces to a much simpler `{{multiply 7 7}}` against the in-harness
//! `multiply` helper. The harness registers that helper before
//! compiling so the rendered body is `49`; benign control sends `7*7`
//! plain text which Handlebars echoes verbatim.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"{{multiply 7 7}}",
label: "ssti-handlebars-eval",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::TemplateEvalEqual { expected: 49 }],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 8,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/ssti/js_handlebars/vuln.js"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::TemplateEvalEqual { expected: 49 }],
benign_control: Some(PayloadRef {
label: "ssti-handlebars-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"7*7",
label: "ssti-handlebars-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::TemplateEvalEqual { expected: 49 }],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 8,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/ssti/js_handlebars/benign.js"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,19 @@
//! Server-Side Template Injection (`Cap::SSTI`) per-engine payload slices.
//!
//! Phase 04 (Track J.2) carves SSTI across the five most-common template
//! engines: Jinja2 (Python), ERB (Ruby), Twig (PHP), Thymeleaf (Java), and
//! Handlebars (JavaScript). Every vuln payload sends a template
//! expression that resolves to a known constant *only* when the engine
//! actually evaluates the expression (e.g. `{{7*7}}` → `49` in Jinja2,
//! `<%= 7*7 %>` → `49` in ERB). The paired benign control sends the
//! literal arithmetic text without engine markers so the per-engine
//! harness echoes the payload verbatim rather than evaluating it; the
//! oracle's [`crate::dynamic::oracle::ProbePredicate::TemplateEvalEqual`]
//! check fires on the vuln render (`49`) and does not fire on the
//! benign render (`7*7`), satisfying the §4.1 differential rule.
pub mod java_thymeleaf;
pub mod js_handlebars;
pub mod php_twig;
pub mod python_jinja2;
pub mod ruby_erb;

View file

@ -0,0 +1,46 @@
//! PHP Twig `Cap::SSTI` payloads.
//!
//! Vuln payload: `{{7*7}}` — Twig evaluates the expression and the
//! rendered template body is `49`. Benign control sends the literal
//! `7*7` text; Twig has no `{{ ... }}` markers around it and echoes
//! the payload verbatim.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"{{7*7}}",
label: "ssti-twig-eval",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::TemplateEvalEqual { expected: 49 }],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 8,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/ssti/php_twig/vuln.php"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::TemplateEvalEqual { expected: 49 }],
benign_control: Some(PayloadRef {
label: "ssti-twig-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"7*7",
label: "ssti-twig-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::TemplateEvalEqual { expected: 49 }],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 8,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/ssti/php_twig/benign.php"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,53 @@
//! Python Jinja2 `Cap::SSTI` payloads.
//!
//! Vuln payload: `{{7*7}}` — Jinja2 evaluates the expression and the
//! rendered template body is `49`. The harness's
//! [`crate::dynamic::oracle::ProbePredicate::TemplateEvalEqual`] check
//! compares the captured `{"render": "49"}` JSON body against
//! `expected = 49` and the oracle fires.
//!
//! Benign control: literal `7*7` — Jinja2 has no `{{ ... }}` markers to
//! evaluate so the engine echoes the payload verbatim. The rendered
//! body is `7*7`, the oracle's integer parse fails, and the oracle
//! does not fire. Together with the vuln payload this satisfies the
//! §4.1 differential confirmation rule.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"{{7*7}}",
label: "ssti-jinja2-eval",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::TemplateEvalEqual { expected: 49 }],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 8,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/ssti/python_jinja2/vuln.py"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::TemplateEvalEqual { expected: 49 }],
benign_control: Some(PayloadRef {
label: "ssti-jinja2-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"7*7",
label: "ssti-jinja2-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::TemplateEvalEqual { expected: 49 }],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 8,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/ssti/python_jinja2/benign.py"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,46 @@
//! Ruby ERB `Cap::SSTI` payloads.
//!
//! Vuln payload: `<%= 7*7 %>` — ERB evaluates the embedded Ruby
//! expression and the rendered template body is `49`. Benign control
//! ships the literal `7*7` text which ERB has no `<%= ... %>` marker
//! around and so passes through verbatim.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"<%= 7*7 %>",
label: "ssti-erb-eval",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::TemplateEvalEqual { expected: 49 }],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 8,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/ssti/ruby_erb/vuln.rb"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::TemplateEvalEqual { expected: 49 }],
benign_control: Some(PayloadRef {
label: "ssti-erb-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"7*7",
label: "ssti-erb-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::TemplateEvalEqual { expected: 49 }],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 8,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/ssti/ruby_erb/benign.rb"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,41 @@
//! go `Cap::UNAUTHORIZED_ID` payloads.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"bob",
label: "idor-go-cross-tenant",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::IdorBoundaryCrossed],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/unauthorized_id/go/vuln.go"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::IdorBoundaryCrossed],
benign_control: Some(PayloadRef {
label: "idor-go-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"alice",
label: "idor-go-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::IdorBoundaryCrossed],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/unauthorized_id/go/benign.go"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,41 @@
//! java `Cap::UNAUTHORIZED_ID` payloads.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"bob",
label: "idor-java-cross-tenant",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::IdorBoundaryCrossed],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/unauthorized_id/java/Vuln.java"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::IdorBoundaryCrossed],
benign_control: Some(PayloadRef {
label: "idor-java-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"alice",
label: "idor-java-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::IdorBoundaryCrossed],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/unauthorized_id/java/Benign.java"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,41 @@
//! js `Cap::UNAUTHORIZED_ID` payloads.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"bob",
label: "idor-js-cross-tenant",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::IdorBoundaryCrossed],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/unauthorized_id/js/vuln.js"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::IdorBoundaryCrossed],
benign_control: Some(PayloadRef {
label: "idor-js-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"alice",
label: "idor-js-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::IdorBoundaryCrossed],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/unauthorized_id/js/benign.js"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,23 @@
//! IDOR / unauthorized-id-access (`Cap::UNAUTHORIZED_ID`)
//! per-language payload slices.
//!
//! Phase 11 (Track J.9) carves an IDOR oracle across all seven
//! backend-capable languages. Each harness stands up a mock data
//! store keyed by `owner_id` and a hard-coded `caller_id`
//! (`"alice"`). The vuln payload supplies an `owner_id` that
//! belongs to another user (`"bob"`); the harness's instrumented
//! lookup returns the record without an authorization check and
//! writes a [`crate::dynamic::probe::ProbeKind::IdorAccess { caller_id,
//! owner_id }`] probe. The
//! [`crate::dynamic::oracle::ProbePredicate::IdorBoundaryCrossed`]
//! predicate fires whenever `caller_id != owner_id`. The paired
//! benign control asks for the caller's own record (`"alice"`), so
//! the probe records matching ids and the predicate stays clear.
pub mod go;
pub mod java;
pub mod js;
pub mod php;
pub mod python;
pub mod ruby;
pub mod rust;

View file

@ -0,0 +1,41 @@
//! php `Cap::UNAUTHORIZED_ID` payloads.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"bob",
label: "idor-php-cross-tenant",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::IdorBoundaryCrossed],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/unauthorized_id/php/vuln.php"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::IdorBoundaryCrossed],
benign_control: Some(PayloadRef {
label: "idor-php-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"alice",
label: "idor-php-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::IdorBoundaryCrossed],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/unauthorized_id/php/benign.php"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,41 @@
//! Python `Cap::UNAUTHORIZED_ID` payloads.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"bob",
label: "idor-python-cross-tenant",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::IdorBoundaryCrossed],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/unauthorized_id/python/vuln.py"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::IdorBoundaryCrossed],
benign_control: Some(PayloadRef {
label: "idor-python-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"alice",
label: "idor-python-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::IdorBoundaryCrossed],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/unauthorized_id/python/benign.py"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,41 @@
//! ruby `Cap::UNAUTHORIZED_ID` payloads.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"bob",
label: "idor-ruby-cross-tenant",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::IdorBoundaryCrossed],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/unauthorized_id/ruby/vuln.rb"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::IdorBoundaryCrossed],
benign_control: Some(PayloadRef {
label: "idor-ruby-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"alice",
label: "idor-ruby-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::IdorBoundaryCrossed],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/unauthorized_id/ruby/benign.rb"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,41 @@
//! rust `Cap::UNAUTHORIZED_ID` payloads.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"bob",
label: "idor-rust-cross-tenant",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::IdorBoundaryCrossed],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/unauthorized_id/rust/vuln.rs"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::IdorBoundaryCrossed],
benign_control: Some(PayloadRef {
label: "idor-rust-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"alice",
label: "idor-rust-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::IdorBoundaryCrossed],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 15,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/unauthorized_id/rust/benign.rs"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

View file

@ -0,0 +1,53 @@
//! Java `Cap::XPATH_INJECTION` payloads — `javax.xml.xpath.XPath.evaluate`
//! expression injection.
//!
//! Vuln payload: an XPath fragment whose `' or '1'='1` tail breaks
//! out of the host template's `[@name='…']` predicate and rewraps
//! the selector as `//user[@name='' or '1'='1']`, matching every
//! node the staged document carries. The harness's instrumented
//! `XPath.evaluate` records
//! `ProbeKind::Xpath { nodes_returned: 3 }`.
//!
//! Benign control: the same intended username quoted via the
//! harness's XPath-escape helper, leaving the expression pinned to a
//! single node — `nodes_returned: 1`, oracle clear.
use super::super::{CuratedPayload, Oracle, PayloadProvenance, PayloadRef};
use crate::dynamic::oracle::ProbePredicate;
pub const PAYLOADS: &[CuratedPayload] = &[
CuratedPayload {
bytes: b"alice' or '1'='1",
label: "xpath-java-expression-wildcard",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::QueryResultCountGreaterThan { n: 1 }],
},
is_benign: false,
provenance: PayloadProvenance::Curated,
since_corpus_version: 11,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/xpath_injection/java/Vuln.java"],
oob_nonce_slot: false,
probe_predicates: &[ProbePredicate::QueryResultCountGreaterThan { n: 1 }],
benign_control: Some(PayloadRef {
label: "xpath-java-benign",
}),
no_benign_control_rationale: None,
},
CuratedPayload {
bytes: b"alice",
label: "xpath-java-benign",
oracle: Oracle::SinkProbe {
predicates: &[ProbePredicate::QueryResultCountGreaterThan { n: 1 }],
},
is_benign: true,
provenance: PayloadProvenance::Curated,
since_corpus_version: 11,
deprecated_at_corpus_version: None,
fixture_paths: &["tests/dynamic_fixtures/xpath_injection/java/Benign.java"],
oob_nonce_slot: false,
probe_predicates: &[],
benign_control: None,
no_benign_control_rationale: None,
},
];

Some files were not shown because too many files have changed in this diff Show more