[pitboss/grind] deferred session-0020 (20260516T052512Z-20f8)

This commit is contained in:
pitboss 2026-05-16 11:27:45 -05:00
parent 04b3d88eb4
commit aa209148b0
8 changed files with 649 additions and 13 deletions

View file

@ -433,6 +433,34 @@ func __nyx_stub_http_record(method, url, body string, detail map[string]string)
}
f.WriteString(method + " " + url + "\n")
}
// Phase 10 (Track D.3) SQL recording helper. When the verifier spawned a
// SqlStub it publishes the side-channel log path through NYX_SQL_LOG; a
// sink callsite whose query never reaches the on-the-wire SQLite engine
// (no database/sql driver imported, query pre-flighted before sql.Open,
// network-isolated sandbox) can call this helper to surface the attempted
// query. Hash-prefixed detail lines followed by the query line so
// SqlStub::drain_events parses every language stream identically. No-op
// when NYX_SQL_LOG is unset so the same harness still runs cleanly under
// modes that did not spawn a stub.
func __nyx_stub_sql_record(query string, detail map[string]string) {
p := os.Getenv("NYX_SQL_LOG")
if p == "" {
return
}
f, err := os.OpenFile(p, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return
}
defer f.Close()
for k, v := range detail {
f.WriteString("# " + k + ": " + v + "\n")
}
f.WriteString(query)
if !strings.HasSuffix(query, "\n") {
f.WriteString("\n")
}
}
"##
}
@ -921,6 +949,23 @@ mod tests {
);
}
#[test]
fn probe_shim_publishes_stub_sql_recorder() {
let shim = probe_shim();
assert!(
shim.contains("func __nyx_stub_sql_record"),
"Go probe shim must define __nyx_stub_sql_record"
);
assert!(
shim.contains("NYX_SQL_LOG"),
"stub recorder must read NYX_SQL_LOG"
);
assert!(
shim.contains("strings.HasSuffix(query, \"\\n\")"),
"Go SQL recorder must guarantee a trailing newline on the query line so SqlStub::drain_events frames each record"
);
}
#[test]
fn chain_step_splices_probe_shim_for_composite_reverify() {
let step = chain_step(Some(b"<prev>"));

View file

@ -404,6 +404,35 @@ pub fn probe_shim() -> &'static str {
// best-effort
}
}
// Phase 10 (Track D.3) SQL recording helper. When the verifier spawned a
// SqlStub it publishes the side-channel log path through NYX_SQL_LOG; a
// sink call site whose query never reaches the on-the-wire SQLite engine
// (e.g. classpath lacks sqlite-jdbc, or the harness pre-flights the SQL
// string before opening the connection) can call this helper to surface
// the attempted query. Hash-prefixed detail lines followed by the query
// line so SqlStub::drain_events parses every language stream identically.
// Same hash-via-String.valueOf trick as __nyx_stub_http_record so this
// method body contains no literal `"#` sequence that would terminate the
// surrounding Rust raw string.
static void __nyx_stub_sql_record(String query, java.util.Map<String,String> detail) {
String p = System.getenv("NYX_SQL_LOG");
if (p == null || p.isEmpty()) return;
String hashSp = String.valueOf('#') + " ";
try (java.io.FileWriter fw = new java.io.FileWriter(p, true)) {
if (detail != null) {
for (java.util.Map.Entry<String,String> e : detail.entrySet()) {
fw.write(hashSp + e.getKey() + ": " + e.getValue() + "\n");
}
}
fw.write(query);
if (!query.endsWith("\n")) {
fw.write("\n");
}
} catch (java.io.IOException e) {
// best-effort
}
}
"##
}
@ -1094,6 +1123,23 @@ mod tests {
);
}
#[test]
fn probe_shim_publishes_stub_sql_recorder() {
let shim = probe_shim();
assert!(
shim.contains("static void __nyx_stub_sql_record"),
"Java probe shim must define __nyx_stub_sql_record"
);
assert!(
shim.contains("\"NYX_SQL_LOG\""),
"Java SQL recorder must read NYX_SQL_LOG to find the side-channel log"
);
assert!(
shim.contains("query.endsWith(\"\\n\")"),
"Java SQL recorder must guarantee a trailing newline on the query line so SqlStub::drain_events frames each record"
);
}
#[test]
fn chain_step_splices_probe_shim_for_composite_reverify() {
let step = chain_step(Some(b"<prev>"));

View file

@ -305,6 +305,29 @@ def __nyx_stub_http_record(method, url, body = nil, **detail)
rescue StandardError
end
end
# Phase 10 (Track D.3) SQL recording helper. When the verifier spawned a
# SqlStub it publishes the side-channel log path through NYX_SQL_LOG; a
# sink call site whose query never reaches the on-the-wire SQLite engine
# (no sqlite3 gem on the host, query pre-flighted before
# SQLite3::Database.open) can call this helper to surface the attempted
# query. Hash-prefixed detail lines followed by the query line so
# SqlStub::drain_events parses every language stream identically. No-op
# when NYX_SQL_LOG is unset. Single-quoted Ruby string literals keep this
# helper free of the literal hash-after-double-quote sequence.
def __nyx_stub_sql_record(query, **detail)
p = ENV['NYX_SQL_LOG']
return if p.nil? || p.empty?
begin
File.open(p, 'a') do |f|
detail.each { |k, v| f.puts('# ' + k.to_s + ': ' + v.to_s) }
line = query.to_s
line += "\n" unless line.end_with?("\n")
f.write(line)
end
rescue StandardError
end
end
"#
}
@ -825,6 +848,23 @@ mod tests {
);
}
#[test]
fn probe_shim_publishes_stub_sql_recorder() {
let shim = probe_shim();
assert!(
shim.contains("def __nyx_stub_sql_record"),
"Ruby probe shim must define __nyx_stub_sql_record"
);
assert!(
shim.contains("ENV['NYX_SQL_LOG']"),
"Ruby SQL recorder must read NYX_SQL_LOG to find the side-channel log"
);
assert!(
shim.contains("line.end_with?"),
"Ruby SQL recorder must guarantee a trailing newline on the query line so SqlStub::drain_events frames each record"
);
}
#[test]
fn chain_step_splices_probe_shim_for_composite_reverify() {
let step = chain_step(Some(b"<prev>"));

View file

@ -0,0 +1,29 @@
// Phase 10 (Track D.3) stub-end-to-end fixture: Go + SQL.
//
// Body-only fragment, not a standalone `go run`-able program. The
// companion test in `tests/stubs_e2e_per_lang.rs` wraps these lines
// in `package main` + the union of stdlib imports required by both
// the spliced probe shim and this fragment, places the Go probe
// shim ahead of `func main`, and then invokes `go run` on the
// resulting file.
//
// The verifier publishes:
//
// NYX_SQL_ENDPOINT — absolute path of a SQLite DB the SqlStub owns.
// NYX_SQL_LOG — companion log path the harness appends executed
// queries to so the host SqlStub picks them up on
// drain_events() even when the harness never opens
// an on-the-wire driver (no go-sqlite3 / pgx /
// mysql dep on the dynamic CI matrix; query
// pre-flighted before sql.Open).
//
// This fragment records the tautology query through the Go shim
// helper __nyx_stub_sql_record as `driver = "manual"` so the test
// stays stdlib-only — no `database/sql` import, no go.mod driver
// dep, no libsqlite3-dev system package. Mirrors the Phase 26
// "no live driver available" path that real Go sink callsites take
// when the build matrix lacks a driver.
query := "SELECT 1 WHERE 'a' = 'a' OR 1=1 --"
__nyx_stub_sql_record(query, map[string]string{"driver": "manual"})
// Echo so the host can confirm the driver ran end-to-end.
fmt.Print(os.Getenv("NYX_SQL_ENDPOINT"))

View file

@ -0,0 +1,26 @@
// Phase 10 (Track D.3) stub-end-to-end fixture: Java + SQL.
//
// The verifier publishes:
//
// * NYX_SQL_ENDPOINT — absolute path of a SQLite DB the SqlStub owns.
// * NYX_SQL_LOG — companion log path the harness appends executed
// queries to so the host SqlStub picks them up on drain_events()
// even when the harness never opens an on-the-wire JDBC connection
// (classpath lacks sqlite-jdbc, SQL string is pre-flighted before
// DriverManager.getConnection, sandbox blocks file-DB access).
//
// This file is a body-only fragment: the companion test in
// tests/stubs_e2e_per_lang.rs wraps it with a `public class Main { … }`
// shell that splices the Java probe shim as class members ahead of
// `public static void main`, so the shim's __nyx_stub_sql_record helper
// is in scope. The fixture stays JDK-stdlib only — no java.sql import,
// no sqlite-jdbc jar on the classpath — by recording the attempted
// tautology with `driver = "manual"`. This mirrors the Phase 26
// "no live driver available" path that real Java sink callsites take
// when the build matrix lacks a JDBC driver.
String query = "SELECT 1 WHERE 'a' = 'a' OR 1=1 --";
java.util.Map<String,String> detail = new java.util.LinkedHashMap<>();
detail.put("driver", "manual");
__nyx_stub_sql_record(query, detail);
String ep = System.getenv("NYX_SQL_ENDPOINT");
System.out.println(ep == null ? "no-endpoint" : ep);

View file

@ -0,0 +1,21 @@
# Phase 10 (Track D.3) stub-end-to-end fixture: Ruby + SQL.
#
# The verifier publishes:
#
# * NYX_SQL_ENDPOINT — absolute path of a SQLite DB the SqlStub owns.
# * NYX_SQL_LOG — companion log path the harness appends executed
# queries to so the host SqlStub picks them up on drain_events()
# even when the harness never opens an on-the-wire driver (sqlite3
# gem absent on minimal CI images, query pre-flighted before
# SQLite3::Database.open).
#
# This fixture stays gem-free by recording the tautology through
# __nyx_stub_sql_record as driver = 'manual'. No sqlite3 require, no
# Gemfile dep, no Prerequisite::GemAvailable variant required. Mirrors
# the Phase 26 "no live driver available" path that real Ruby sink
# callsites take when the build matrix lacks a driver.
query = "SELECT 1 WHERE 'a' = 'a' OR 1=1 --"
__nyx_stub_sql_record(query, driver: 'manual')
# Echo so the host can confirm the driver ran end-to-end.
$stdout.puts(ENV['NYX_SQL_ENDPOINT'] || 'no-endpoint')

View file

@ -0,0 +1,18 @@
// Phase 10 (Track D.3) — Rust SQL recorder body-only fragment.
//
// Wrapped at test time by `wrap_rust_fragment(body, shim)` in
// `tests/stubs_e2e_per_lang.rs`: the wrapper prepends the Rust probe
// shim (which carries `__nyx_stub_sql_record`) and a one-line
// `Cargo.toml` so `cargo run --quiet` builds the program in place.
//
// Rust has no stdlib SQLite client (rusqlite is a heavyweight C-link
// dep that would force a libsqlite3-dev prereq on the dynamic CI
// matrix). The fixture surfaces the attempted tautology query
// through the shim recorder so the host-side SqlStub captures it as
// `driver = "manual"`, mirroring the Phase 26 "no live driver
// available" path that real Rust sink callsites take when the build
// matrix lacks a DB driver.
let _endpoint = std::env::var("NYX_SQL_ENDPOINT").unwrap_or_default();
let query = "SELECT 1 WHERE 'a' = 'a' OR 1=1 --";
let detail: &[(&str, &str)] = &[("driver", "manual")];
__nyx_stub_sql_record(query, detail);

View file

@ -153,21 +153,29 @@ fn wrap_rust_fragment(body: &str, shim: &str) -> String {
)
}
/// One-line Cargo.toml for the Rust stub-recorder driver. Mirrors
/// Per-fixture Cargo.toml for the Rust stub-recorder driver. Mirrors
/// the Phase 26 chain_step manifest (session 0014) — `[[bin]]` points
/// at `main.rs` so `cargo run --quiet` builds the source the test
/// just wrote, and `libc = "0.2"` is unconditionally pinned because
/// the spliced probe shim's `__nyx_install_crash_guard` references
/// `libc::sigaction` on Unix.
const RUST_STUB_CARGO_TOML: &str = "[package]\n\
name = \"nyx-stub-driver\"\n\
version = \"0.0.1\"\n\
edition = \"2021\"\n\n\
[[bin]]\n\
name = \"stub_driver\"\n\
path = \"main.rs\"\n\n\
[dependencies]\n\
libc = \"0.2\"\n";
/// `libc::sigaction` on Unix. Caller supplies a unique `slug` per
/// test so the package + binary names do not collide in the shared
/// `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).
fn rust_stub_cargo_toml(slug: &str) -> String {
format!(
"[package]\n\
name = \"nyx-stub-driver-{slug}\"\n\
version = \"0.0.1\"\n\
edition = \"2021\"\n\n\
[[bin]]\n\
name = \"stub_driver_{slug}\"\n\
path = \"main.rs\"\n\n\
[dependencies]\n\
libc = \"0.2\"\n"
)
}
fn fixture_path(rel: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
@ -876,6 +884,103 @@ fn go_http_shim_recorder_is_noop_without_log_env() {
);
}
#[test]
fn go_sql_stub_captures_tautology_query_via_shim_recorder() {
// Phase 10 (Track D.3) SQL recording: Go leg of the side-channel
// `__nyx_stub_sql_record` helper. Mirrors the Python / Node / PHP /
// Rust / Java SQL tests — the Go fragment never opens a live
// `database/sql` handle (no driver imported; pulling go-sqlite3 /
// pgx / mysql would force a go.mod dep onto every dynamic CI matrix
// row) so it surfaces the attempted tautology query through the
// shim recorder as `driver = "manual"`.
if !go_available() {
eprintln!("SKIP: go 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("go/sql/vuln/main.go")).expect("read go fragment");
let combined = wrap_go_fragment(&fragment, go_probe_shim());
let script_path = workdir.path().join("driver_sql.go");
std::fs::write(&script_path, combined).expect("write go driver");
let output = Command::new("go")
.arg("run")
.arg(&script_path)
.env("NYX_SQL_ENDPOINT", &endpoint)
.env(recording.0, &recording.1)
.output()
.expect("go driver");
assert!(
output.status.success(),
"driver must exit 0; stderr = {}",
String::from_utf8_lossy(&output.stderr)
);
let events = stub.drain_events();
assert!(
!events.is_empty(),
"SqlStub must capture at least one event after the Go 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"),
"detail map entries passed to __nyx_stub_sql_record must surface as event detail entries"
);
}
#[test]
fn go_sql_shim_recorder_is_noop_without_log_env() {
if !go_available() {
eprintln!("SKIP: go 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("go/sql/vuln/main.go")).expect("read go fragment");
let combined = wrap_go_fragment(&fragment, go_probe_shim());
let script_path = workdir.path().join("driver_sql_no_log.go");
std::fs::write(&script_path, combined).expect("write go driver");
let output = Command::new("go")
.arg("run")
.arg(&script_path)
.env("NYX_SQL_ENDPOINT", &endpoint)
.env_remove("NYX_SQL_LOG")
.output()
.expect("go driver");
assert!(
output.status.success(),
"driver must exit 0 even without NYX_SQL_LOG; stderr = {}",
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 ruby_http_stub_captures_attempted_outbound_via_shim_recorder() {
// Phase 10 (Track D.3) HTTP recording: Ruby leg of the side-channel
@ -983,6 +1088,105 @@ fn ruby_http_shim_recorder_is_noop_without_log_env() {
);
}
#[test]
fn ruby_sql_stub_captures_tautology_query_via_shim_recorder() {
// Phase 10 (Track D.3) SQL recording: Ruby leg of the side-channel
// `__nyx_stub_sql_record` helper. Mirrors the Python / Node / PHP /
// Rust / Java / Go SQL tests — the Ruby fragment never opens a live
// sqlite3 handle (no require, no gem dep) so it surfaces the
// attempted tautology query through the shim recorder as
// `driver = "manual"`.
if !ruby_available() {
eprintln!("SKIP: ruby 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 fixture =
std::fs::read_to_string(fixture_path("ruby/sql/vuln/main.rb")).expect("read fixture");
let mut combined = String::with_capacity(ruby_probe_shim().len() + fixture.len() + 64);
combined.push_str(ruby_probe_shim());
combined.push_str("\n# ── fixture begins ─\n");
combined.push_str(&fixture);
let script_path = workdir.path().join("driver_sql.rb");
std::fs::write(&script_path, combined).expect("write driver");
let output = Command::new("ruby")
.arg(&script_path)
.env("NYX_SQL_ENDPOINT", &endpoint)
.env(recording.0, &recording.1)
.output()
.expect("ruby driver");
assert!(
output.status.success(),
"driver must exit 0; stderr = {}",
String::from_utf8_lossy(&output.stderr)
);
let events = stub.drain_events();
assert!(
!events.is_empty(),
"SqlStub must capture at least one event after the Ruby 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"),
"kwargs passed to __nyx_stub_sql_record must surface as event detail entries"
);
}
#[test]
fn ruby_sql_shim_recorder_is_noop_without_log_env() {
if !ruby_available() {
eprintln!("SKIP: ruby not available");
return;
}
let workdir = TempDir::new().expect("tempdir");
let stub = SqlStub::start(workdir.path()).expect("SqlStub::start");
let endpoint = stub.endpoint();
let fixture =
std::fs::read_to_string(fixture_path("ruby/sql/vuln/main.rb")).expect("read fixture");
let mut combined = String::new();
combined.push_str(ruby_probe_shim());
combined.push('\n');
combined.push_str(&fixture);
let script_path = workdir.path().join("driver_sql_no_log.rb");
std::fs::write(&script_path, combined).expect("write driver");
let output = Command::new("ruby")
.arg(&script_path)
.env("NYX_SQL_ENDPOINT", &endpoint)
.env_remove("NYX_SQL_LOG")
.output()
.expect("ruby driver");
assert!(
output.status.success(),
"driver must exit 0 even without NYX_SQL_LOG; stderr = {}",
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 java_http_stub_captures_attempted_outbound_via_shim_recorder() {
// Phase 10 (Track D.3) HTTP recording: Java leg of the side-channel
@ -1051,6 +1255,100 @@ fn java_http_stub_captures_attempted_outbound_via_shim_recorder() {
);
}
#[test]
fn java_sql_stub_captures_tautology_query_via_shim_recorder() {
// Phase 10 (Track D.3) SQL recording: Java leg of the side-channel
// `__nyx_stub_sql_record` helper. Mirrors the Python / Node / PHP /
// Rust SQL tests — the Java fragment never opens a live JDBC handle
// (sqlite-jdbc is not stdlib; pulling it would force a classpath
// prereq onto the dynamic CI matrix) so it surfaces the attempted
// tautology query through the shim recorder as `driver = "manual"`.
if !java_available() {
eprintln!("SKIP: java 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("java/sql/vuln/main.java.fragment"))
.expect("read java sql fragment");
let combined = wrap_java_fragment(&fragment, java_probe_shim());
let script_path = workdir.path().join("Main.java");
std::fs::write(&script_path, combined).expect("write java driver");
let output = Command::new("java")
.arg(&script_path)
.env("NYX_SQL_ENDPOINT", &endpoint)
.env(recording.0, &recording.1)
.output()
.expect("java driver");
assert!(
output.status.success(),
"driver must exit 0; stderr = {}",
String::from_utf8_lossy(&output.stderr)
);
let events = stub.drain_events();
assert!(
!events.is_empty(),
"SqlStub must capture at least one event after the Java 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"),
"detail map entries passed to __nyx_stub_sql_record must surface as event detail entries"
);
}
#[test]
fn java_sql_shim_recorder_is_noop_without_log_env() {
if !java_available() {
eprintln!("SKIP: java 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("java/sql/vuln/main.java.fragment"))
.expect("read java sql fragment");
let combined = wrap_java_fragment(&fragment, java_probe_shim());
let script_path = workdir.path().join("Main.java");
std::fs::write(&script_path, combined).expect("write java driver");
let output = Command::new("java")
.arg(&script_path)
.env("NYX_SQL_ENDPOINT", &endpoint)
.env_remove("NYX_SQL_LOG")
.output()
.expect("java driver");
assert!(
output.status.success(),
"driver must exit 0 even without NYX_SQL_LOG; stderr = {}",
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 java_http_shim_recorder_is_noop_without_log_env() {
if !java_available() {
@ -1166,7 +1464,7 @@ fn rust_http_stub_captures_attempted_outbound_via_shim_recorder() {
let crate_dir = workdir.path().join("driver");
std::fs::create_dir_all(&crate_dir).expect("create crate dir");
std::fs::write(crate_dir.join("Cargo.toml"), RUST_STUB_CARGO_TOML)
std::fs::write(crate_dir.join("Cargo.toml"), rust_stub_cargo_toml("http"))
.expect("write Cargo.toml");
std::fs::write(crate_dir.join("main.rs"), source).expect("write main.rs");
@ -1229,7 +1527,7 @@ fn rust_http_shim_recorder_is_noop_without_log_env() {
let crate_dir = workdir.path().join("driver_no_log");
std::fs::create_dir_all(&crate_dir).expect("create crate dir");
std::fs::write(crate_dir.join("Cargo.toml"), RUST_STUB_CARGO_TOML)
std::fs::write(crate_dir.join("Cargo.toml"), rust_stub_cargo_toml("http_no_log"))
.expect("write Cargo.toml");
std::fs::write(crate_dir.join("main.rs"), source).expect("write main.rs");
@ -1257,3 +1555,116 @@ fn rust_http_shim_recorder_is_noop_without_log_env() {
events.len()
);
}
#[test]
fn rust_sql_stub_captures_tautology_query_via_shim_recorder() {
// Phase 10 (Track D.3) SQL recording: Rust leg of the side-channel
// `__nyx_stub_sql_record` helper. Mirrors the Python / Node / PHP
// SQL tests — the Rust fragment never opens a live SQLite handle
// (no stdlib driver; rusqlite would force libsqlite3-dev onto the
// CI matrix) so it surfaces the attempted tautology query through
// the shim recorder as `driver = "manual"`. Uses the same
// `extra_files`-driven `Cargo.toml` shape as the HTTP siblings so
// `cargo run --quiet` resolves `libc` (referenced by the spliced
// probe shim's `__nyx_install_crash_guard`).
if !cargo_available() {
eprintln!("SKIP: cargo 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("rust/sql/vuln/main.rs"))
.expect("read rust sql fragment");
let source = wrap_rust_fragment(&fragment, rust_probe_shim());
let crate_dir = workdir.path().join("driver_sql");
std::fs::create_dir_all(&crate_dir).expect("create crate dir");
std::fs::write(crate_dir.join("Cargo.toml"), rust_stub_cargo_toml("sql"))
.expect("write Cargo.toml");
std::fs::write(crate_dir.join("main.rs"), source).expect("write main.rs");
let output = Command::new("cargo")
.arg("run")
.arg("--quiet")
.arg("--manifest-path")
.arg(crate_dir.join("Cargo.toml"))
.env("CARGO_TARGET_DIR", rust_stub_target_dir())
.env("NYX_SQL_ENDPOINT", &endpoint)
.env(recording.0, &recording.1)
.output()
.expect("cargo run rust sql driver");
assert!(
output.status.success(),
"driver must exit 0; stdout = {}\nstderr = {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
let events = stub.drain_events();
assert!(
!events.is_empty(),
"SqlStub must capture at least one event after the Rust 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"),
"detail slice passed to __nyx_stub_sql_record must surface as event detail entries"
);
}
#[test]
fn rust_sql_shim_recorder_is_noop_without_log_env() {
if !cargo_available() {
eprintln!("SKIP: cargo 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("rust/sql/vuln/main.rs"))
.expect("read rust sql fragment");
let source = wrap_rust_fragment(&fragment, rust_probe_shim());
let crate_dir = workdir.path().join("driver_sql_no_log");
std::fs::create_dir_all(&crate_dir).expect("create crate dir");
std::fs::write(crate_dir.join("Cargo.toml"), rust_stub_cargo_toml("sql_no_log"))
.expect("write Cargo.toml");
std::fs::write(crate_dir.join("main.rs"), source).expect("write main.rs");
let output = Command::new("cargo")
.arg("run")
.arg("--quiet")
.arg("--manifest-path")
.arg(crate_dir.join("Cargo.toml"))
.env("CARGO_TARGET_DIR", rust_stub_target_dir())
.env("NYX_SQL_ENDPOINT", &endpoint)
.env_remove("NYX_SQL_LOG")
.output()
.expect("cargo run rust sql driver");
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()
);
}