From 41f2a2d7f828b2279f66708f6e0d60f6786be0a9 Mon Sep 17 00:00:00 2001 From: pitboss Date: Fri, 22 May 2026 05:42:58 -0500 Subject: [PATCH] [pitboss/grind] deferred session-0018 (20260522T043516Z-29b8) --- src/dynamic/lang/rust.rs | 318 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 318 insertions(+) diff --git a/src/dynamic/lang/rust.rs b/src/dynamic/lang/rust.rs index 0dea5218..f28bf572 100644 --- a/src/dynamic/lang/rust.rs +++ b/src/dynamic/lang/rust.rs @@ -1062,6 +1062,140 @@ fn read_entry_source(entry_file: &str) -> String { String::new() } +/// Phase 11 — Track J.9 CRYPTO weak-RNG harness for Rust. +/// +/// Stages the fixture at `src/entry.rs`, builds against `rand = "0.8"` +/// (added to `Cargo.toml` automatically when `Cap::CRYPTO` is set — +/// see [`generate_cargo_toml_with_extras`]), invokes +/// `entry::(&payload)`, reduces the produced key into a +/// `u64` via the `NyxKeyToInt` trait, and writes a +/// `ProbeKind::WeakKey { key_int }` probe. +/// +/// The `NyxKeyToInt` trait has impls for `u8` / `u16` / `u32` / `u64` / +/// `usize` / signed counterparts (masked to `i64::MAX` so the sign bit +/// does not flip a 16-bit predicate), `bool` (1/0), `[u8; N]`, +/// `Vec`, `String`, and `&str`. Byte / string returns are left- +/// zero-padded to 8 bytes then read as big-endian `u64`, mirroring the +/// Python / Go / Java / PHP sibling reduction: a `rand::thread_rng() +/// .gen_range(0..=0xFFFF) as u16` vuln return lands in `[0, 65535]` and +/// trips the `WeakKeyEntropy { max_bits: 16 }` predicate; an +/// `OsRng.fill_bytes([u8; 32])` benign return's leading 8 bytes are +/// uniformly distributed across `u64::MAX` and overshoot the budget +/// with probability `1 - 2^-48` — effectively always. +pub fn emit_crypto_harness(spec: &HarnessSpec) -> HarnessSource { + let shim = probe_shim(); + let entry_fn = if spec.entry_name.is_empty() { + "run".to_owned() + } else { + spec.entry_name.clone() + }; + let cargo_toml = generate_cargo_toml(Cap::CRYPTO); + + let main_rs = format!( + r##"//! Nyx dynamic harness — CRYPTO weak-RNG key entropy (Phase 11 / Track J.9). +mod entry; +use std::env; +use std::fs::OpenOptions; +use std::io::Write; +use std::time::{{SystemTime, UNIX_EPOCH}}; + +{shim} + +/// Reduce the fixture's produced key to a `u64` the `WeakKey` probe +/// shape can carry verbatim. Impls below cover the return types the +/// curated CRYPTO fixtures hand back; future fixtures returning other +/// shapes should grow an impl here rather than panicking at compile +/// time. +trait NyxKeyToInt {{ + fn to_key_int(self) -> u64; +}} + +fn nyx_bytes_to_key_int(bytes: &[u8]) -> u64 {{ + // Left-zero-pad short slices then read the leading 8 bytes as + // big-endian, mirroring PHP's `unpack('J', str_pad($head, 8, + // "\\0", STR_PAD_LEFT))` and Go's `binary.BigEndian.Uint64` with + // left zero-pad. + let mut buf = [0u8; 8]; + let n = bytes.len().min(8); + let start = 8 - n; + buf[start..start + n].copy_from_slice(&bytes[..n]); + u64::from_be_bytes(buf) +}} + +impl NyxKeyToInt for u8 {{ fn to_key_int(self) -> u64 {{ u64::from(self) }} }} +impl NyxKeyToInt for u16 {{ fn to_key_int(self) -> u64 {{ u64::from(self) }} }} +impl NyxKeyToInt for u32 {{ fn to_key_int(self) -> u64 {{ u64::from(self) }} }} +impl NyxKeyToInt for u64 {{ fn to_key_int(self) -> u64 {{ self }} }} +impl NyxKeyToInt for usize {{ fn to_key_int(self) -> u64 {{ self as u64 }} }} +impl NyxKeyToInt for i8 {{ fn to_key_int(self) -> u64 {{ (self as u64) & (i64::MAX as u64) }} }} +impl NyxKeyToInt for i16 {{ fn to_key_int(self) -> u64 {{ (self as u64) & (i64::MAX as u64) }} }} +impl NyxKeyToInt for i32 {{ fn to_key_int(self) -> u64 {{ (self as u64) & (i64::MAX as u64) }} }} +impl NyxKeyToInt for i64 {{ fn to_key_int(self) -> u64 {{ (self as u64) & (i64::MAX as u64) }} }} +impl NyxKeyToInt for isize {{ fn to_key_int(self) -> u64 {{ (self as u64) & (i64::MAX as u64) }} }} +impl NyxKeyToInt for bool {{ fn to_key_int(self) -> u64 {{ if self {{ 1 }} else {{ 0 }} }} }} +impl NyxKeyToInt for [u8; N] {{ + fn to_key_int(self) -> u64 {{ nyx_bytes_to_key_int(&self) }} +}} +impl NyxKeyToInt for Vec {{ + fn to_key_int(self) -> u64 {{ nyx_bytes_to_key_int(&self) }} +}} +impl NyxKeyToInt for String {{ + fn to_key_int(self) -> u64 {{ nyx_bytes_to_key_int(self.as_bytes()) }} +}} +impl<'a> NyxKeyToInt for &'a str {{ + fn to_key_int(self) -> u64 {{ nyx_bytes_to_key_int(self.as_bytes()) }} +}} + +fn nyx_weak_key_probe(key_int: u64) {{ + let p = match env::var("NYX_PROBE_PATH") {{ Ok(s) => s, Err(_) => return }}; + if p.is_empty() {{ return; }} + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_nanos() as u64) + .unwrap_or(0); + let payload_id = env::var("NYX_PAYLOAD_ID").unwrap_or_default(); + let key_str = key_int.to_string(); + let mut line = String::with_capacity(256); + line.push_str("{{\"sink_callee\":\"__nyx_weak_key\",\"args\":["); + line.push_str("{{\"kind\":\"Int\",\"value\":"); + line.push_str(&key_str); + line.push_str("}}],"); + line.push_str("\"captured_at_ns\":"); + line.push_str(&now.to_string()); + line.push_str(",\"payload_id\":\""); + let mut esc_pid = String::new(); + __nyx_esc(&payload_id, &mut esc_pid); + line.push_str(&esc_pid); + line.push_str("\",\"kind\":{{\"kind\":\"WeakKey\",\"key_int\":"); + line.push_str(&key_str); + line.push_str("}},\"witness\":"); + line.push_str(&__nyx_witness_json("__nyx_weak_key", &[&key_str])); + line.push_str("}}\n"); + if let Ok(mut f) = OpenOptions::new().create(true).append(true).open(&p) {{ + let _ = f.write_all(line.as_bytes()); + }} +}} + +fn main() {{ + let payload = env::var("NYX_PAYLOAD").unwrap_or_default(); + __nyx_install_crash_guard("__nyx_weak_key"); + let produced = entry::{entry_fn}(&payload); + let key_int = produced.to_key_int(); + nyx_weak_key_probe(key_int); + println!("__NYX_SINK_HIT__"); + println!("{{{{\"key_int\":{{key_int}}}}}}", key_int = key_int); +}} +"## + ); + HarnessSource { + source: main_rs, + filename: "src/main.rs".into(), + command: vec!["target/release/nyx_harness".into()], + extra_files: vec![("Cargo.toml".into(), cargo_toml)], + entry_subpath: Some("src/entry.rs".into()), + } +} + /// Emit a Rust harness for `spec`. pub fn emit(spec: &HarnessSpec) -> Result { // Phase 08 (Track J.6): HEADER_INJECTION-sink short-circuit. The @@ -1080,6 +1214,20 @@ pub fn emit(spec: &HarnessSpec) -> Result { return Ok(emit_open_redirect_harness(spec)); } + // Phase 11 (Track J.9): CRYPTO weak-RNG short-circuit. Stages the + // fixture at `src/entry.rs`, builds against `rand = "0.8"` (the + // benign fixture uses `rand::rngs::OsRng`, the vuln fixture uses + // `rand::thread_rng().gen_range(...)`), invokes `entry::run(&payload)`, + // reduces the produced key to a `u64` via the `NyxKeyToInt` trait + // (`u16`/`u32`/`u64` flow through verbatim, `[u8; N]`/`Vec`/ + // `String`/`&str` are left-zero-padded to 8 bytes then read as BE + // u64 so a 32-byte CSPRNG benign result trivially overshoots any + // 16-bit budget), and writes a `ProbeKind::WeakKey { key_int }` + // record. + if spec.expected_cap == crate::labels::Cap::CRYPTO { + return Ok(emit_crypto_harness(spec)); + } + // Phase 19 (Track M.1): ClassMethod short-circuit. Rust has no // class system — the dispatcher maps `class` to a struct exported // from `entry::`, and `method` to a `&self` method on that @@ -1443,6 +1591,9 @@ pub fn generate_cargo_toml_with_extras(cap: Cap, needs_percent_encoding: bool) - if needs_percent_encoding { deps.push_str("percent-encoding = \"2\"\n"); } + if cap.contains(Cap::CRYPTO) { + deps.push_str("rand = \"0.8\"\n"); + } format!( "[package]\n\ @@ -2384,4 +2535,171 @@ mod tests { "Cargo.toml must declare edition 2021, got: {body}", ); } + + // ── Phase 11 (Track J.9) Rust CRYPTO emitter tests ───────────────────────── + + fn make_crypto_spec(entry_file: &str, entry_name: &str) -> HarnessSpec { + let mut spec = make_spec(PayloadSlot::Param(0)); + spec.expected_cap = Cap::CRYPTO; + spec.entry_file = entry_file.to_owned(); + spec.entry_name = entry_name.to_owned(); + spec + } + + #[test] + fn emit_dispatches_to_crypto_harness_when_cap_is_crypto() { + let h = emit(&make_crypto_spec( + "tests/dynamic_fixtures/crypto/rust/vuln.rs", + "run", + )) + .unwrap(); + assert!( + h.source.contains("fn nyx_weak_key_probe"), + "dispatcher must short-circuit Cap::CRYPTO into emit_crypto_harness so the weak-key probe shim is present: {}", + h.source + ); + // The harness source quotes the JSON field names with escaped + // backslashes (the generated Rust code splices the JSON via + // `push_str("\"kind\":\"WeakKey\"")`). Assert against the + // escaped form so the test pins the runtime probe shape, not + // an accidental colocation. + assert!( + h.source.contains(r#"\"kind\":\"WeakKey\""#), + "Rust CRYPTO harness must record probes with kind WeakKey so the WeakKeyEntropy predicate fires: {}", + h.source + ); + } + + #[test] + fn emit_crypto_harness_invokes_entry_via_mod_entry() { + let h = emit_crypto_harness(&make_crypto_spec( + "tests/dynamic_fixtures/crypto/rust/vuln.rs", + "run", + )); + assert!( + h.source.contains("mod entry;"), + "Rust CRYPTO harness must declare `mod entry;` so the staged fixture is in scope: {}", + h.source + ); + assert!( + h.source.contains("let produced = entry::run(&payload);"), + "Rust CRYPTO harness must invoke the entry function with the payload: {}", + h.source + ); + assert_eq!( + h.entry_subpath, + Some("src/entry.rs".to_string()), + "Rust CRYPTO harness must stage the fixture at src/entry.rs so `mod entry;` picks it up", + ); + assert_eq!( + h.filename, "src/main.rs", + "Rust CRYPTO harness main file must be src/main.rs", + ); + } + + #[test] + fn emit_crypto_harness_emits_weak_key_probe_kind() { + let h = emit_crypto_harness(&make_crypto_spec( + "tests/dynamic_fixtures/crypto/rust/vuln.rs", + "run", + )); + assert!( + h.source + .contains(r#"\"kind\":{\"kind\":\"WeakKey\",\"key_int\":"#), + "Rust CRYPTO harness must emit ProbeKind::WeakKey records carrying a key_int field so the WeakKeyEntropy predicate fires: {}", + h.source + ); + assert!( + h.source.contains("__NYX_SINK_HIT__"), + "Rust CRYPTO harness must print the universal sink-hit sentinel: {}", + h.source + ); + } + + #[test] + fn emit_crypto_harness_cargo_toml_pulls_in_rand_crate() { + let h = emit_crypto_harness(&make_crypto_spec( + "tests/dynamic_fixtures/crypto/rust/vuln.rs", + "run", + )); + let cargo = h + .extra_files + .iter() + .find(|(n, _)| n == "Cargo.toml") + .expect("Cargo.toml must be in extra_files"); + assert!( + cargo.1.contains("rand = \"0.8\""), + "Rust CRYPTO harness Cargo.toml must depend on rand = \"0.8\" so the fixture's `rand::thread_rng()` / `rand::rngs::OsRng` imports resolve: {}", + cargo.1 + ); + assert!( + cargo.1.contains("libc = \"0.2\""), + "Rust CRYPTO harness Cargo.toml must keep libc dep for the probe shim's sigaction path", + ); + } + + #[test] + fn emit_crypto_harness_reduces_byte_array_via_be_u64() { + let h = emit_crypto_harness(&make_crypto_spec( + "tests/dynamic_fixtures/crypto/rust/benign.rs", + "run", + )); + assert!( + h.source.contains("fn nyx_bytes_to_key_int"), + "Rust CRYPTO harness must define the byte-slice reduction helper: {}", + h.source + ); + assert!( + h.source.contains("u64::from_be_bytes"), + "Rust CRYPTO harness must use big-endian u64 reduction so a 32-byte CSPRNG benign result overshoots any 16-bit budget: {}", + h.source + ); + assert!( + h.source.contains("impl NyxKeyToInt for [u8; N]"), + "Rust CRYPTO harness must provide a generic [u8; N] impl so both [u8; 32] (benign) and other-sized array returns reduce uniformly: {}", + h.source + ); + } + + #[test] + fn emit_crypto_harness_provides_impls_for_primitive_int_returns() { + let h = emit_crypto_harness(&make_crypto_spec( + "tests/dynamic_fixtures/crypto/rust/vuln.rs", + "run", + )); + for ty in &["u8", "u16", "u32", "u64", "i64", "bool"] { + let needle = format!("impl NyxKeyToInt for {ty}"); + assert!( + h.source.contains(&needle), + "Rust CRYPTO harness must provide a NyxKeyToInt impl for {ty} so fixture return-type variation does not break compilation: {}", + h.source + ); + } + } + + #[test] + fn emit_crypto_harness_signed_impls_mask_sign_bit() { + let h = emit_crypto_harness(&make_crypto_spec( + "tests/dynamic_fixtures/crypto/rust/vuln.rs", + "run", + )); + assert!( + h.source.contains("(self as u64) & (i64::MAX as u64)"), + "signed-int impls must mask the sign bit so a negative key value does not flip a small-bit-budget predicate: {}", + h.source + ); + } + + #[test] + fn emit_crypto_harness_honours_entry_name_when_set() { + let h = emit_crypto_harness(&make_crypto_spec( + "tests/dynamic_fixtures/crypto/rust/vuln.rs", + "weak_key_derivation", + )); + assert!( + h.source.contains("entry::weak_key_derivation(&payload)"), + "Rust CRYPTO harness must use spec.entry_name (not a hard-coded literal) when invoking the entry: {}", + h.source + ); + } }