mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
[pitboss/grind] deferred session-0021 (20260516T052512Z-20f8)
This commit is contained in:
parent
aa209148b0
commit
c051f58647
8 changed files with 778 additions and 9 deletions
|
|
@ -108,7 +108,11 @@ fn read_entry_source(entry_file: &str) -> String {
|
|||
/// Track C.1). Variadic over `const char *` args; hand-rolled JSON keeps
|
||||
/// the only dep on libc / stdio.
|
||||
pub fn probe_shim() -> &'static str {
|
||||
r#"
|
||||
// The body holds literal `"# key: value\n"` log-line formats for the
|
||||
// Phase 10 stub recorders, so the surrounding raw string uses
|
||||
// `r##"..."##` to keep `"#` substrings from terminating it early
|
||||
// (same trick the Rust / Java / Go / Ruby siblings use).
|
||||
r##"
|
||||
/* ── __nyx_probe shim (Phase 06 — Track C.1, Phase 08 — Track C.4 + C.5) ── */
|
||||
#include <signal.h>
|
||||
#include <stdarg.h>
|
||||
|
|
@ -290,7 +294,67 @@ static void __nyx_install_crash_guard(const char *sink_callee) {
|
|||
sigaction(sigs[i], &sa, NULL);
|
||||
}
|
||||
}
|
||||
"#
|
||||
|
||||
/* Phase 10 (Track D.3) stub recorder helpers. When the verifier spawns a
|
||||
* SqlStub it publishes the queries-log path through NYX_SQL_LOG; a sink
|
||||
* call site that wants the host-side stub to see its query appends one
|
||||
* record-per-call. Detail kv pairs use parallel arrays so the helper is
|
||||
* variadic in arity without depending on stdarg-with-typed args. The
|
||||
* helper is a no-op when the env var is unset so the same source still
|
||||
* runs under harness modes that did not spawn a stub. */
|
||||
static void __nyx_stub_sql_record(const char *query,
|
||||
const char **detail_keys,
|
||||
const char **detail_vals,
|
||||
int detail_count) {
|
||||
const char *p = getenv("NYX_SQL_LOG");
|
||||
if (!p || *p == '\0') return;
|
||||
FILE *f = fopen(p, "a");
|
||||
if (!f) return;
|
||||
for (int i = 0; i < detail_count; ++i) {
|
||||
if (detail_keys && detail_vals && detail_keys[i] && detail_vals[i]) {
|
||||
fprintf(f, "# %s: %s\n", detail_keys[i], detail_vals[i]);
|
||||
}
|
||||
}
|
||||
if (query) {
|
||||
size_t qlen = strlen(query);
|
||||
fputs(query, f);
|
||||
if (qlen == 0 || query[qlen - 1] != '\n') {
|
||||
fputc('\n', f);
|
||||
}
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
/* Phase 10 (Track D.3) HTTP recording helper. When the verifier spawns an
|
||||
* HttpStub it publishes the side-channel log path through NYX_HTTP_LOG; a
|
||||
* sink call site whose outbound request never reaches the on-the-wire
|
||||
* listener (DNS-mocked, network-isolated sandbox, pre-flight check) can
|
||||
* call this helper to surface the attempted call. Format matches the SQL
|
||||
* helper so the host-side merger parses both streams identically. */
|
||||
static void __nyx_stub_http_record(const char *method,
|
||||
const char *url,
|
||||
const char *body,
|
||||
const char **detail_keys,
|
||||
const char **detail_vals,
|
||||
int detail_count) {
|
||||
const char *p = getenv("NYX_HTTP_LOG");
|
||||
if (!p || *p == '\0') return;
|
||||
FILE *f = fopen(p, "a");
|
||||
if (!f) return;
|
||||
if (method) fprintf(f, "# method: %s\n", method);
|
||||
if (url) fprintf(f, "# url: %s\n", url);
|
||||
if (body) fprintf(f, "# body: %s\n", body);
|
||||
for (int i = 0; i < detail_count; ++i) {
|
||||
if (detail_keys && detail_vals && detail_keys[i] && detail_vals[i]) {
|
||||
fprintf(f, "# %s: %s\n", detail_keys[i], detail_vals[i]);
|
||||
}
|
||||
}
|
||||
if (method && url) {
|
||||
fprintf(f, "%s %s\n", method, url);
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
"##
|
||||
}
|
||||
|
||||
impl LangEmitter for CEmitter {
|
||||
|
|
@ -730,6 +794,33 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn probe_shim_publishes_stub_sql_and_http_recorders() {
|
||||
// Phase 10 (Track D.3): the C probe shim ships the manual-record
|
||||
// stub helpers so a C harness can surface attempted DB / outbound
|
||||
// calls to the host-side SqlStub / HttpStub through their
|
||||
// NYX_SQL_LOG / NYX_HTTP_LOG side channels. Helpers must be
|
||||
// declared before `__nyx_install_crash_guard` so a sink-rewrite
|
||||
// pass can reference them from anywhere in the entry source.
|
||||
let shim = probe_shim();
|
||||
assert!(
|
||||
shim.contains("static void __nyx_stub_sql_record("),
|
||||
"C probe shim must define __nyx_stub_sql_record",
|
||||
);
|
||||
assert!(
|
||||
shim.contains("static void __nyx_stub_http_record("),
|
||||
"C probe shim must define __nyx_stub_http_record",
|
||||
);
|
||||
assert!(
|
||||
shim.contains("getenv(\"NYX_SQL_LOG\")"),
|
||||
"SQL recorder must read NYX_SQL_LOG so the SqlStub side channel picks it up",
|
||||
);
|
||||
assert!(
|
||||
shim.contains("getenv(\"NYX_HTTP_LOG\")"),
|
||||
"HTTP recorder must read NYX_HTTP_LOG so the HttpStub side channel picks it up",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_install_crash_guard_targets_renamed_main_entry() {
|
||||
// Real-world Track B CLI vuln: spec.entry_name == "main" → the entry
|
||||
|
|
|
|||
|
|
@ -88,7 +88,11 @@ fn read_entry_source(entry_file: &str) -> String {
|
|||
/// (Phase 06 — Track C.1). Uses `<fstream>` + variadic templates; the
|
||||
/// JSON-emit format matches [`crate::dynamic::probe::SinkProbe`].
|
||||
pub fn probe_shim() -> &'static str {
|
||||
r#"
|
||||
// The body holds literal `"# key: value\n"` log-line formats for the
|
||||
// Phase 10 stub recorders, so the surrounding raw string uses
|
||||
// `r##"..."##` to keep `"#` substrings from terminating it early
|
||||
// (same trick the Rust / Java / Go / Ruby siblings use).
|
||||
r##"
|
||||
/* ── __nyx_probe shim (Phase 06 — Track C.1, Phase 08 — Track C.4 + C.5) ── */
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
|
|
@ -263,7 +267,47 @@ inline void __nyx_install_crash_guard(const char *sink_callee) {
|
|||
sigaction(sig, &sa, nullptr);
|
||||
}
|
||||
}
|
||||
"#
|
||||
|
||||
/* Phase 10 (Track D.3) stub recorder helpers. See the C-side commentary
|
||||
* for the contract — these are the same helpers expressed in C++ idiom
|
||||
* (std::ofstream + std::initializer_list of {key, value} pairs). Both
|
||||
* are no-ops when the relevant NYX_*_LOG env var is unset. */
|
||||
inline void __nyx_stub_sql_record(
|
||||
const std::string &query,
|
||||
std::initializer_list<std::pair<std::string, std::string>> detail = {}) {
|
||||
const char *p = std::getenv("NYX_SQL_LOG");
|
||||
if (!p || *p == '\0') return;
|
||||
std::ofstream f(p, std::ios::app);
|
||||
if (!f.is_open()) return;
|
||||
for (const auto &kv : detail) {
|
||||
f << "# " << kv.first << ": " << kv.second << "\n";
|
||||
}
|
||||
f << query;
|
||||
if (query.empty() || query.back() != '\n') {
|
||||
f << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
inline void __nyx_stub_http_record(
|
||||
const std::string &method,
|
||||
const std::string &url,
|
||||
const std::string &body = std::string(),
|
||||
std::initializer_list<std::pair<std::string, std::string>> detail = {}) {
|
||||
const char *p = std::getenv("NYX_HTTP_LOG");
|
||||
if (!p || *p == '\0') return;
|
||||
std::ofstream f(p, std::ios::app);
|
||||
if (!f.is_open()) return;
|
||||
f << "# method: " << method << "\n";
|
||||
f << "# url: " << url << "\n";
|
||||
if (!body.empty()) {
|
||||
f << "# body: " << body << "\n";
|
||||
}
|
||||
for (const auto &kv : detail) {
|
||||
f << "# " << kv.first << ": " << kv.second << "\n";
|
||||
}
|
||||
f << method << " " << url << "\n";
|
||||
}
|
||||
"##
|
||||
}
|
||||
|
||||
impl LangEmitter for CppEmitter {
|
||||
|
|
@ -649,6 +693,31 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn probe_shim_publishes_stub_sql_and_http_recorders() {
|
||||
// Phase 10 (Track D.3): the C++ probe shim ships the manual-record
|
||||
// stub helpers so a C++ harness can surface attempted DB / outbound
|
||||
// calls to the host-side SqlStub / HttpStub through their
|
||||
// NYX_SQL_LOG / NYX_HTTP_LOG side channels.
|
||||
let shim = probe_shim();
|
||||
assert!(
|
||||
shim.contains("inline void __nyx_stub_sql_record("),
|
||||
"C++ probe shim must define __nyx_stub_sql_record",
|
||||
);
|
||||
assert!(
|
||||
shim.contains("inline void __nyx_stub_http_record("),
|
||||
"C++ probe shim must define __nyx_stub_http_record",
|
||||
);
|
||||
assert!(
|
||||
shim.contains("std::getenv(\"NYX_SQL_LOG\")"),
|
||||
"SQL recorder must read NYX_SQL_LOG so the SqlStub side channel picks it up",
|
||||
);
|
||||
assert!(
|
||||
shim.contains("std::getenv(\"NYX_HTTP_LOG\")"),
|
||||
"HTTP recorder must read NYX_HTTP_LOG so the HttpStub side channel picks it up",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_cmake_in_extra_files() {
|
||||
let spec = make_spec(PayloadSlot::Param(0));
|
||||
|
|
|
|||
|
|
@ -128,19 +128,35 @@ const PROFILE_SOURCES: &[(&str, &str)] = &[
|
|||
];
|
||||
|
||||
/// Cap → profile-name dispatch. The most restrictive matching profile
|
||||
/// wins: `FILE_IO` outranks `SSRF` outranks `CODE_EXEC` outranks
|
||||
/// `DESERIALIZE`. A cap bit with no matching profile falls back to the
|
||||
/// `base` profile.
|
||||
/// wins: filesystem caps outrank network caps outrank CODE_EXEC outranks
|
||||
/// DESERIALIZE. Filesystem-shaped caps (`FILE_IO`, `SQL_QUERY` — DBs are
|
||||
/// files in WORKDIR) map to `path_traversal`; outbound-network-shaped caps
|
||||
/// (`SSRF`, `HEADER_INJECTION`, `OPEN_REDIRECT`, `UNVALIDATED_REDIRECT`,
|
||||
/// `LDAP_INJECTION`, `XPATH_INJECTION`) map to `ssrf` since they share the
|
||||
/// "outbound allowed; host secrets denied" shape. Caps with no shared
|
||||
/// shape (CRYPTO, AUTH, RACE, MEMORY_SAFETY, XSS, XXE) fall back to `base`
|
||||
/// — XXE in particular would want a network-deny profile for entity
|
||||
/// resolution, which the bundled `.sb` set does not yet ship.
|
||||
pub fn profile_for_caps(caps: u32) -> &'static str {
|
||||
// Mirror the bit positions declared in `src/labels/mod.rs`.
|
||||
const FILE_IO: u32 = 1 << 5;
|
||||
const SQL_QUERY: u32 = 1 << 7;
|
||||
const DESERIALIZE: u32 = 1 << 8;
|
||||
const SSRF: u32 = 1 << 9;
|
||||
const CODE_EXEC: u32 = 1 << 10;
|
||||
const LDAP_INJECTION: u32 = 1 << 14;
|
||||
const XPATH_INJECTION: u32 = 1 << 15;
|
||||
const HEADER_INJECTION: u32 = 1 << 16;
|
||||
const OPEN_REDIRECT: u32 = 1 << 17;
|
||||
const UNVALIDATED_REDIRECT: u32 = 1 << 18;
|
||||
|
||||
if caps & FILE_IO != 0 {
|
||||
const FS_SHAPED: u32 = FILE_IO | SQL_QUERY;
|
||||
const NET_SHAPED: u32 =
|
||||
SSRF | LDAP_INJECTION | XPATH_INJECTION | HEADER_INJECTION | OPEN_REDIRECT | UNVALIDATED_REDIRECT;
|
||||
|
||||
if caps & FS_SHAPED != 0 {
|
||||
"path_traversal"
|
||||
} else if caps & SSRF != 0 {
|
||||
} else if caps & NET_SHAPED != 0 {
|
||||
"ssrf"
|
||||
} else if caps & CODE_EXEC != 0 {
|
||||
"cmdi"
|
||||
|
|
@ -323,6 +339,48 @@ mod tests {
|
|||
assert_eq!(profile_for_caps(0), "base");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_for_caps_routes_filesystem_shaped_caps_to_path_traversal() {
|
||||
// SQL_QUERY shares the `file-write into WORKDIR / file-read of
|
||||
// host secrets denied` shape with FILE_IO (SQLite DBs live as
|
||||
// files in the workdir), so it routes to the same profile.
|
||||
const SQL_QUERY: u32 = 1 << 7;
|
||||
const CODE_EXEC: u32 = 1 << 10;
|
||||
assert_eq!(profile_for_caps(SQL_QUERY), "path_traversal");
|
||||
// Filesystem shape outranks the lesser-restrictive cmdi profile.
|
||||
assert_eq!(profile_for_caps(SQL_QUERY | CODE_EXEC), "path_traversal");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_for_caps_routes_outbound_network_caps_to_ssrf() {
|
||||
// Outbound HTTP request sinks (HEADER_INJECTION / OPEN_REDIRECT /
|
||||
// UNVALIDATED_REDIRECT) and other network-traffic injection caps
|
||||
// (LDAP_INJECTION / XPATH_INJECTION) all share the SSRF shape:
|
||||
// outbound allowed, host-secret reads denied.
|
||||
const LDAP_INJECTION: u32 = 1 << 14;
|
||||
const XPATH_INJECTION: u32 = 1 << 15;
|
||||
const HEADER_INJECTION: u32 = 1 << 16;
|
||||
const OPEN_REDIRECT: u32 = 1 << 17;
|
||||
const UNVALIDATED_REDIRECT: u32 = 1 << 18;
|
||||
assert_eq!(profile_for_caps(LDAP_INJECTION), "ssrf");
|
||||
assert_eq!(profile_for_caps(XPATH_INJECTION), "ssrf");
|
||||
assert_eq!(profile_for_caps(HEADER_INJECTION), "ssrf");
|
||||
assert_eq!(profile_for_caps(OPEN_REDIRECT), "ssrf");
|
||||
assert_eq!(profile_for_caps(UNVALIDATED_REDIRECT), "ssrf");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_for_caps_falls_back_to_base_for_unmapped_caps() {
|
||||
// CRYPTO / AUTH / RACE / MEMORY_SAFETY / XSS / XXE do not yet
|
||||
// have a cap-specific .sb profile. XXE in particular would want
|
||||
// a network-deny profile (entity resolution), but the bundled .sb
|
||||
// set does not ship one — track in deferred.md.
|
||||
const CRYPTO: u32 = 1 << 11;
|
||||
const XXE: u32 = 1 << 19;
|
||||
assert_eq!(profile_for_caps(CRYPTO), "base");
|
||||
assert_eq!(profile_for_caps(XXE), "base");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_path_materialises_baked_source() {
|
||||
let path = profile_path("base").expect("base profile");
|
||||
|
|
|
|||
14
tests/dynamic_fixtures/stubs_e2e/c/http/vuln/main.c.fragment
Normal file
14
tests/dynamic_fixtures/stubs_e2e/c/http/vuln/main.c.fragment
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
/* Phase 10 (Track D.3) — C HTTP recorder body-only fragment.
|
||||
*
|
||||
* Wrapped at test time by `wrap_c_fragment(body, shim)`. The
|
||||
* fixture surfaces an SSRF attempt at the IMDS metadata endpoint
|
||||
* through the shim recorder, so the host-side HttpStub captures
|
||||
* the attempted outbound call without the harness opening a real
|
||||
* socket. Mirrors the per-lang HTTP recording siblings.
|
||||
*/
|
||||
const char *method = "GET";
|
||||
const char *url = "http://169.254.169.254/latest/meta-data/";
|
||||
const char *body = NULL;
|
||||
const char *detail_keys[] = { "driver" };
|
||||
const char *detail_vals[] = { "manual" };
|
||||
__nyx_stub_http_record(method, url, body, detail_keys, detail_vals, 1);
|
||||
16
tests/dynamic_fixtures/stubs_e2e/c/sql/vuln/main.c.fragment
Normal file
16
tests/dynamic_fixtures/stubs_e2e/c/sql/vuln/main.c.fragment
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/* Phase 10 (Track D.3) — C SQL recorder body-only fragment.
|
||||
*
|
||||
* Wrapped at test time by `wrap_c_fragment(body, shim)` in
|
||||
* `tests/stubs_e2e_per_lang.rs`: the wrapper prepends the C probe
|
||||
* shim (which carries `__nyx_stub_sql_record`) and a `main()` shell
|
||||
* so `cc <source>.c -o <bin> && ./<bin>` builds the program in place.
|
||||
*
|
||||
* The fixture surfaces the attempted tautology query through the
|
||||
* shim recorder so the host-side SqlStub captures it as
|
||||
* `driver = "manual"` — no libsqlite3-dev / sqlite3.h dependency on
|
||||
* the dynamic CI matrix.
|
||||
*/
|
||||
const char *query = "SELECT 1 WHERE 'a' = 'a' OR 1=1 --";
|
||||
const char *detail_keys[] = { "driver" };
|
||||
const char *detail_vals[] = { "manual" };
|
||||
__nyx_stub_sql_record(query, detail_keys, detail_vals, 1);
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
// Phase 10 (Track D.3) — C++ HTTP recorder body-only fragment.
|
||||
//
|
||||
// Wrapped at test time by `wrap_cpp_fragment(body, shim)`. Records
|
||||
// an SSRF attempt at the IMDS metadata endpoint through the shim
|
||||
// recorder; the host-side HttpStub captures the attempted outbound
|
||||
// call without the harness opening a real socket.
|
||||
std::string method = "GET";
|
||||
std::string url = "http://169.254.169.254/latest/meta-data/";
|
||||
__nyx_stub_http_record(method, url, std::string(), { {"driver", "manual"} });
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
// Phase 10 (Track D.3) — C++ SQL recorder body-only fragment.
|
||||
//
|
||||
// Wrapped at test time by `wrap_cpp_fragment(body, shim)` in
|
||||
// `tests/stubs_e2e_per_lang.rs`: the wrapper prepends the C++
|
||||
// probe shim (which carries `__nyx_stub_sql_record`) and a
|
||||
// `int main()` shell so `c++ <source>.cpp -o <bin> && ./<bin>`
|
||||
// builds the program in place.
|
||||
//
|
||||
// Records the attempted tautology query through the shim recorder
|
||||
// so the host-side SqlStub captures it as `driver = "manual"` —
|
||||
// no libsqlite3 / sqlite3pp dependency on the dynamic CI matrix.
|
||||
std::string query = "SELECT 1 WHERE 'a' = 'a' OR 1=1 --";
|
||||
__nyx_stub_sql_record(query, { {"driver", "manual"} });
|
||||
|
|
@ -20,6 +20,8 @@
|
|||
|
||||
#![cfg(feature = "dynamic")]
|
||||
|
||||
use nyx_scanner::dynamic::lang::c::probe_shim as c_probe_shim;
|
||||
use nyx_scanner::dynamic::lang::cpp::probe_shim as cpp_probe_shim;
|
||||
use nyx_scanner::dynamic::lang::go::probe_shim as go_probe_shim;
|
||||
use nyx_scanner::dynamic::lang::java::probe_shim as java_probe_shim;
|
||||
use nyx_scanner::dynamic::lang::javascript::probe_shim as node_probe_shim;
|
||||
|
|
@ -80,6 +82,34 @@ fn cargo_available() -> bool {
|
|||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn cc_available() -> bool {
|
||||
// Honours the same NYX_CC_BIN override used by the Phase 29
|
||||
// CommandAvailableEnvOverride prereq variant in the C fixture suite.
|
||||
let bin = std::env::var("NYX_CC_BIN").unwrap_or_else(|_| "cc".to_owned());
|
||||
Command::new(bin)
|
||||
.arg("--version")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn cxx_available() -> bool {
|
||||
let bin = std::env::var("NYX_CXX_BIN").unwrap_or_else(|_| "c++".to_owned());
|
||||
Command::new(bin)
|
||||
.arg("--version")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn cc_bin() -> String {
|
||||
std::env::var("NYX_CC_BIN").unwrap_or_else(|_| "cc".to_owned())
|
||||
}
|
||||
|
||||
fn cxx_bin() -> String {
|
||||
std::env::var("NYX_CXX_BIN").unwrap_or_else(|_| "c++".to_owned())
|
||||
}
|
||||
|
||||
fn java_available() -> bool {
|
||||
// The Java shim helpers use `java MainSource.java` single-file
|
||||
// source-mode (JEP 330, JDK 11+) so only the `java` runtime is
|
||||
|
|
@ -163,6 +193,38 @@ fn wrap_rust_fragment(body: &str, shim: &str) -> String {
|
|||
/// `CARGO_TARGET_DIR` when nextest runs the Rust stub tests in
|
||||
/// parallel (every test still benefits from the cached `libc` build,
|
||||
/// only the final `nyx-stub-driver-<slug>` link is per-test).
|
||||
/// Wrap a body-only C fragment in a complete translation unit: prepend
|
||||
/// the C probe shim (which carries `__nyx_stub_sql_record` /
|
||||
/// `__nyx_stub_http_record`) at file scope, then wrap the fragment as
|
||||
/// the body of `int main(void)`. The shim's own `#include` directives
|
||||
/// pull in stdio / string / signal headers, so the fragment can use
|
||||
/// `NULL`, string literals, and the recorder helpers without any
|
||||
/// additional preamble.
|
||||
fn wrap_c_fragment(body: &str, shim: &str) -> String {
|
||||
format!(
|
||||
"{shim}\n\
|
||||
int main(void) {{\n\
|
||||
{body}\n\
|
||||
return 0;\n\
|
||||
}}\n"
|
||||
)
|
||||
}
|
||||
|
||||
/// Wrap a body-only C++ fragment in a complete translation unit: prepend
|
||||
/// the C++ probe shim and wrap the fragment as the body of `int main()`.
|
||||
/// The shim's own `#include` block covers `<string>` / `<fstream>` /
|
||||
/// `<utility>` so initializer-list `{key, value}` literals + `std::string`
|
||||
/// in the fragment compile cleanly.
|
||||
fn wrap_cpp_fragment(body: &str, shim: &str) -> String {
|
||||
format!(
|
||||
"{shim}\n\
|
||||
int main() {{\n\
|
||||
{body}\n\
|
||||
return 0;\n\
|
||||
}}\n"
|
||||
)
|
||||
}
|
||||
|
||||
fn rust_stub_cargo_toml(slug: &str) -> String {
|
||||
format!(
|
||||
"[package]\n\
|
||||
|
|
@ -1668,3 +1730,440 @@ fn rust_sql_shim_recorder_is_noop_without_log_env() {
|
|||
events.len()
|
||||
);
|
||||
}
|
||||
|
||||
// ── C ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Build + run a wrapped C source: writes the source to
|
||||
/// `<workdir>/<slug>.c`, drives `cc` to compile to `<workdir>/<slug>`,
|
||||
/// runs the binary with the supplied env block. Returns the binary's
|
||||
/// own `Output` so tests assert on exit code + stdout/stderr. Build
|
||||
/// failures surface as a panic with the compiler's stderr.
|
||||
fn build_and_run_c(
|
||||
workdir: &std::path::Path,
|
||||
slug: &str,
|
||||
source: &str,
|
||||
extra_env: &[(&str, &str)],
|
||||
suppress_env: &[&str],
|
||||
) -> std::process::Output {
|
||||
let src_path = workdir.join(format!("{slug}.c"));
|
||||
let bin_path = workdir.join(slug);
|
||||
std::fs::write(&src_path, source).expect("write C source");
|
||||
|
||||
let build = Command::new(cc_bin())
|
||||
.arg(&src_path)
|
||||
.arg("-o")
|
||||
.arg(&bin_path)
|
||||
.output()
|
||||
.expect("invoke cc");
|
||||
assert!(
|
||||
build.status.success(),
|
||||
"cc must build the wrapped C source; stderr = {}",
|
||||
String::from_utf8_lossy(&build.stderr)
|
||||
);
|
||||
|
||||
let mut cmd = Command::new(&bin_path);
|
||||
for (k, v) in extra_env {
|
||||
cmd.env(k, v);
|
||||
}
|
||||
for k in suppress_env {
|
||||
cmd.env_remove(*k);
|
||||
}
|
||||
cmd.output().expect("run C driver")
|
||||
}
|
||||
|
||||
fn build_and_run_cpp(
|
||||
workdir: &std::path::Path,
|
||||
slug: &str,
|
||||
source: &str,
|
||||
extra_env: &[(&str, &str)],
|
||||
suppress_env: &[&str],
|
||||
) -> std::process::Output {
|
||||
let src_path = workdir.join(format!("{slug}.cpp"));
|
||||
let bin_path = workdir.join(slug);
|
||||
std::fs::write(&src_path, source).expect("write C++ source");
|
||||
|
||||
let build = Command::new(cxx_bin())
|
||||
.arg(&src_path)
|
||||
.arg("-o")
|
||||
.arg(&bin_path)
|
||||
.output()
|
||||
.expect("invoke c++");
|
||||
assert!(
|
||||
build.status.success(),
|
||||
"c++ must build the wrapped C++ source; stderr = {}",
|
||||
String::from_utf8_lossy(&build.stderr)
|
||||
);
|
||||
|
||||
let mut cmd = Command::new(&bin_path);
|
||||
for (k, v) in extra_env {
|
||||
cmd.env(k, v);
|
||||
}
|
||||
for k in suppress_env {
|
||||
cmd.env_remove(*k);
|
||||
}
|
||||
cmd.output().expect("run C++ driver")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c_sql_stub_captures_tautology_query_via_shim_recorder() {
|
||||
// Phase 10 (Track D.3) SQL recording: C leg of the side-channel
|
||||
// `__nyx_stub_sql_record` helper. Mirrors the Rust SQL test —
|
||||
// the C fragment never opens a live SQLite handle (no sqlite3.h
|
||||
// dependency on the dynamic CI matrix) so it surfaces the
|
||||
// attempted tautology query through the shim recorder as
|
||||
// `driver = "manual"`.
|
||||
if !cc_available() {
|
||||
eprintln!("SKIP: cc not available");
|
||||
return;
|
||||
}
|
||||
|
||||
let workdir = TempDir::new().expect("tempdir");
|
||||
let stub = SqlStub::start(workdir.path()).expect("SqlStub::start");
|
||||
|
||||
let endpoint = stub.endpoint();
|
||||
let recording = stub
|
||||
.recording_endpoint()
|
||||
.expect("SqlStub must publish a recording endpoint");
|
||||
|
||||
let fragment = std::fs::read_to_string(fixture_path("c/sql/vuln/main.c.fragment"))
|
||||
.expect("read c sql fragment");
|
||||
let source = wrap_c_fragment(&fragment, c_probe_shim());
|
||||
|
||||
let output = build_and_run_c(
|
||||
workdir.path(),
|
||||
"driver_c_sql",
|
||||
&source,
|
||||
&[
|
||||
("NYX_SQL_ENDPOINT", endpoint.as_str()),
|
||||
(recording.0, recording.1.as_str()),
|
||||
],
|
||||
&[],
|
||||
);
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"driver must exit 0; stdout = {}\nstderr = {}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let events = stub.drain_events();
|
||||
assert!(
|
||||
!events.is_empty(),
|
||||
"SqlStub must capture at least one event after the C shim recorder fires"
|
||||
);
|
||||
let tautology = events
|
||||
.iter()
|
||||
.find(|e| e.summary.contains("OR 1=1"))
|
||||
.expect("recorded query must contain the tautology marker");
|
||||
assert_eq!(
|
||||
tautology.detail.get("driver").map(String::as_str),
|
||||
Some("manual"),
|
||||
"parallel-array detail passed to __nyx_stub_sql_record must surface as event detail"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c_sql_shim_recorder_is_noop_without_log_env() {
|
||||
if !cc_available() {
|
||||
eprintln!("SKIP: cc not available");
|
||||
return;
|
||||
}
|
||||
|
||||
let workdir = TempDir::new().expect("tempdir");
|
||||
let stub = SqlStub::start(workdir.path()).expect("SqlStub::start");
|
||||
|
||||
let endpoint = stub.endpoint();
|
||||
let fragment = std::fs::read_to_string(fixture_path("c/sql/vuln/main.c.fragment"))
|
||||
.expect("read c sql fragment");
|
||||
let source = wrap_c_fragment(&fragment, c_probe_shim());
|
||||
|
||||
let output = build_and_run_c(
|
||||
workdir.path(),
|
||||
"driver_c_sql_no_log",
|
||||
&source,
|
||||
&[("NYX_SQL_ENDPOINT", endpoint.as_str())],
|
||||
&["NYX_SQL_LOG"],
|
||||
);
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"driver must exit 0 even without NYX_SQL_LOG; stdout = {}\nstderr = {}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let events = stub.drain_events();
|
||||
assert!(
|
||||
events.is_empty(),
|
||||
"no events expected when the recording env var is unset, got {} entries",
|
||||
events.len()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c_http_stub_captures_attempted_outbound_via_shim_recorder() {
|
||||
if !cc_available() {
|
||||
eprintln!("SKIP: cc not available");
|
||||
return;
|
||||
}
|
||||
|
||||
let workdir = TempDir::new().expect("tempdir");
|
||||
let stub = HttpStub::start(workdir.path()).expect("HttpStub::start");
|
||||
|
||||
let endpoint = stub.endpoint();
|
||||
let recording = stub
|
||||
.recording_endpoint()
|
||||
.expect("HttpStub must publish a recording endpoint");
|
||||
|
||||
let fragment = std::fs::read_to_string(fixture_path("c/http/vuln/main.c.fragment"))
|
||||
.expect("read c http fragment");
|
||||
let source = wrap_c_fragment(&fragment, c_probe_shim());
|
||||
|
||||
let output = build_and_run_c(
|
||||
workdir.path(),
|
||||
"driver_c_http",
|
||||
&source,
|
||||
&[
|
||||
("NYX_HTTP_ENDPOINT", endpoint.as_str()),
|
||||
(recording.0, recording.1.as_str()),
|
||||
],
|
||||
&[],
|
||||
);
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"driver must exit 0; stdout = {}\nstderr = {}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let events = stub.drain_events();
|
||||
assert!(
|
||||
!events.is_empty(),
|
||||
"HttpStub must capture at least one event after the C shim recorder fires"
|
||||
);
|
||||
let imds = events
|
||||
.iter()
|
||||
.find(|e| e.summary.contains("169.254.169.254"))
|
||||
.expect("recorded URL must contain the IMDS metadata host");
|
||||
assert_eq!(
|
||||
imds.detail.get("method").map(String::as_str),
|
||||
Some("GET"),
|
||||
"method line must surface in the recorded event detail"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn c_http_shim_recorder_is_noop_without_log_env() {
|
||||
if !cc_available() {
|
||||
eprintln!("SKIP: cc not available");
|
||||
return;
|
||||
}
|
||||
|
||||
let workdir = TempDir::new().expect("tempdir");
|
||||
let stub = HttpStub::start(workdir.path()).expect("HttpStub::start");
|
||||
|
||||
let endpoint = stub.endpoint();
|
||||
let fragment = std::fs::read_to_string(fixture_path("c/http/vuln/main.c.fragment"))
|
||||
.expect("read c http fragment");
|
||||
let source = wrap_c_fragment(&fragment, c_probe_shim());
|
||||
|
||||
let output = build_and_run_c(
|
||||
workdir.path(),
|
||||
"driver_c_http_no_log",
|
||||
&source,
|
||||
&[("NYX_HTTP_ENDPOINT", endpoint.as_str())],
|
||||
&["NYX_HTTP_LOG"],
|
||||
);
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"driver must exit 0 even without NYX_HTTP_LOG; stdout = {}\nstderr = {}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let events = stub.drain_events();
|
||||
assert!(
|
||||
events.is_empty(),
|
||||
"no events expected when the recording env var is unset, got {} entries",
|
||||
events.len()
|
||||
);
|
||||
}
|
||||
|
||||
// ── C++ ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn cpp_sql_stub_captures_tautology_query_via_shim_recorder() {
|
||||
if !cxx_available() {
|
||||
eprintln!("SKIP: c++ not available");
|
||||
return;
|
||||
}
|
||||
|
||||
let workdir = TempDir::new().expect("tempdir");
|
||||
let stub = SqlStub::start(workdir.path()).expect("SqlStub::start");
|
||||
|
||||
let endpoint = stub.endpoint();
|
||||
let recording = stub
|
||||
.recording_endpoint()
|
||||
.expect("SqlStub must publish a recording endpoint");
|
||||
|
||||
let fragment = std::fs::read_to_string(fixture_path("cpp/sql/vuln/main.cpp.fragment"))
|
||||
.expect("read cpp sql fragment");
|
||||
let source = wrap_cpp_fragment(&fragment, cpp_probe_shim());
|
||||
|
||||
let output = build_and_run_cpp(
|
||||
workdir.path(),
|
||||
"driver_cpp_sql",
|
||||
&source,
|
||||
&[
|
||||
("NYX_SQL_ENDPOINT", endpoint.as_str()),
|
||||
(recording.0, recording.1.as_str()),
|
||||
],
|
||||
&[],
|
||||
);
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"driver must exit 0; stdout = {}\nstderr = {}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let events = stub.drain_events();
|
||||
assert!(
|
||||
!events.is_empty(),
|
||||
"SqlStub must capture at least one event after the C++ shim recorder fires"
|
||||
);
|
||||
let tautology = events
|
||||
.iter()
|
||||
.find(|e| e.summary.contains("OR 1=1"))
|
||||
.expect("recorded query must contain the tautology marker");
|
||||
assert_eq!(
|
||||
tautology.detail.get("driver").map(String::as_str),
|
||||
Some("manual"),
|
||||
"initializer-list detail passed to __nyx_stub_sql_record must surface as event detail"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cpp_sql_shim_recorder_is_noop_without_log_env() {
|
||||
if !cxx_available() {
|
||||
eprintln!("SKIP: c++ not available");
|
||||
return;
|
||||
}
|
||||
|
||||
let workdir = TempDir::new().expect("tempdir");
|
||||
let stub = SqlStub::start(workdir.path()).expect("SqlStub::start");
|
||||
|
||||
let endpoint = stub.endpoint();
|
||||
let fragment = std::fs::read_to_string(fixture_path("cpp/sql/vuln/main.cpp.fragment"))
|
||||
.expect("read cpp sql fragment");
|
||||
let source = wrap_cpp_fragment(&fragment, cpp_probe_shim());
|
||||
|
||||
let output = build_and_run_cpp(
|
||||
workdir.path(),
|
||||
"driver_cpp_sql_no_log",
|
||||
&source,
|
||||
&[("NYX_SQL_ENDPOINT", endpoint.as_str())],
|
||||
&["NYX_SQL_LOG"],
|
||||
);
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"driver must exit 0 even without NYX_SQL_LOG; stdout = {}\nstderr = {}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let events = stub.drain_events();
|
||||
assert!(
|
||||
events.is_empty(),
|
||||
"no events expected when the recording env var is unset, got {} entries",
|
||||
events.len()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cpp_http_stub_captures_attempted_outbound_via_shim_recorder() {
|
||||
if !cxx_available() {
|
||||
eprintln!("SKIP: c++ not available");
|
||||
return;
|
||||
}
|
||||
|
||||
let workdir = TempDir::new().expect("tempdir");
|
||||
let stub = HttpStub::start(workdir.path()).expect("HttpStub::start");
|
||||
|
||||
let endpoint = stub.endpoint();
|
||||
let recording = stub
|
||||
.recording_endpoint()
|
||||
.expect("HttpStub must publish a recording endpoint");
|
||||
|
||||
let fragment = std::fs::read_to_string(fixture_path("cpp/http/vuln/main.cpp.fragment"))
|
||||
.expect("read cpp http fragment");
|
||||
let source = wrap_cpp_fragment(&fragment, cpp_probe_shim());
|
||||
|
||||
let output = build_and_run_cpp(
|
||||
workdir.path(),
|
||||
"driver_cpp_http",
|
||||
&source,
|
||||
&[
|
||||
("NYX_HTTP_ENDPOINT", endpoint.as_str()),
|
||||
(recording.0, recording.1.as_str()),
|
||||
],
|
||||
&[],
|
||||
);
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"driver must exit 0; stdout = {}\nstderr = {}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let events = stub.drain_events();
|
||||
assert!(
|
||||
!events.is_empty(),
|
||||
"HttpStub must capture at least one event after the C++ shim recorder fires"
|
||||
);
|
||||
let imds = events
|
||||
.iter()
|
||||
.find(|e| e.summary.contains("169.254.169.254"))
|
||||
.expect("recorded URL must contain the IMDS metadata host");
|
||||
assert_eq!(
|
||||
imds.detail.get("method").map(String::as_str),
|
||||
Some("GET"),
|
||||
"method line must surface in the recorded event detail"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cpp_http_shim_recorder_is_noop_without_log_env() {
|
||||
if !cxx_available() {
|
||||
eprintln!("SKIP: c++ not available");
|
||||
return;
|
||||
}
|
||||
|
||||
let workdir = TempDir::new().expect("tempdir");
|
||||
let stub = HttpStub::start(workdir.path()).expect("HttpStub::start");
|
||||
|
||||
let endpoint = stub.endpoint();
|
||||
let fragment = std::fs::read_to_string(fixture_path("cpp/http/vuln/main.cpp.fragment"))
|
||||
.expect("read cpp http fragment");
|
||||
let source = wrap_cpp_fragment(&fragment, cpp_probe_shim());
|
||||
|
||||
let output = build_and_run_cpp(
|
||||
workdir.path(),
|
||||
"driver_cpp_http_no_log",
|
||||
&source,
|
||||
&[("NYX_HTTP_ENDPOINT", endpoint.as_str())],
|
||||
&["NYX_HTTP_LOG"],
|
||||
);
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"driver must exit 0 even without NYX_HTTP_LOG; stdout = {}\nstderr = {}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
|
||||
let events = stub.drain_events();
|
||||
assert!(
|
||||
events.is_empty(),
|
||||
"no events expected when the recording env var is unset, got {} entries",
|
||||
events.len()
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue