diff --git a/crates/bashkit/fuzz/Cargo.toml b/crates/bashkit/fuzz/Cargo.toml index 6528ab74..9c8120b9 100644 --- a/crates/bashkit/fuzz/Cargo.toml +++ b/crates/bashkit/fuzz/Cargo.toml @@ -108,3 +108,10 @@ path = "fuzz_targets/template_fuzz.rs" test = false doc = false bench = false + +[[bin]] +name = "grep_fuzz" +path = "fuzz_targets/grep_fuzz.rs" +test = false +doc = false +bench = false diff --git a/crates/bashkit/fuzz/fuzz_targets/grep_fuzz.rs b/crates/bashkit/fuzz/fuzz_targets/grep_fuzz.rs new file mode 100644 index 00000000..dea56011 --- /dev/null +++ b/crates/bashkit/fuzz/fuzz_targets/grep_fuzz.rs @@ -0,0 +1,102 @@ +//! Fuzz target for the grep builtin +//! +//! Tests regex pattern compilation and matching to find: +//! - ReDoS from catastrophic backtracking on pathological patterns +//! - Panics in bracket expression parsing or extended regex features +//! - Edge cases in case-insensitive matching, invert, and context lines +//! - Graceful rejection of invalid regex patterns +//! +//! Run with: cargo +nightly fuzz run grep_fuzz -- -max_total_time=300 + +#![no_main] + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + // Only process valid UTF-8 + if let Ok(input) = std::str::from_utf8(data) { + // Limit input size to prevent OOM + if input.len() > 1024 { + return; + } + + // Split input into regex pattern (first line) and search text (rest) + let (pattern, text) = match input.find('\n') { + Some(pos) => (&input[..pos], &input[pos + 1..]), + None => (input, "hello world\nfoo bar\nbaz qux\n" as &str), + }; + + // Skip empty patterns + if pattern.trim().is_empty() { + return; + } + + // Reject deeply nested regex groups (ReDoS mitigation) + let depth: i32 = pattern + .bytes() + .map(|b| match b { + b'(' | b'[' => 1, + b')' | b']' => -1, + _ => 0, + }) + .scan(0i32, |acc, d| { + *acc += d; + Some(*acc) + }) + .max() + .unwrap_or(0); + if depth > 15 { + return; + } + + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + + rt.block_on(async { + let mut bash = bashkit::Bash::builder() + .limits( + bashkit::ExecutionLimits::new() + .max_commands(50) + .max_subst_depth(3) + .max_stdout_bytes(4096) + .max_stderr_bytes(4096) + .timeout(std::time::Duration::from_millis(200)), + ) + .build(); + + // Test 1: basic grep pattern matching + let script = format!( + "echo '{}' | grep '{}' 2>/dev/null; true", + text.replace('\'', "'\\''"), + pattern.replace('\'', "'\\''"), + ); + let _ = bash.exec(&script).await; + + // Test 2: extended regex (-E) + let script2 = format!( + "echo '{}' | grep -E '{}' 2>/dev/null; true", + text.replace('\'', "'\\''"), + pattern.replace('\'', "'\\''"), + ); + let _ = bash.exec(&script2).await; + + // Test 3: case-insensitive with line numbers (-in) + let script3 = format!( + "echo '{}' | grep -in '{}' 2>/dev/null; true", + text.replace('\'', "'\\''"), + pattern.replace('\'', "'\\''"), + ); + let _ = bash.exec(&script3).await; + + // Test 4: inverted match with count (-vc) + let script4 = format!( + "echo '{}' | grep -vc '{}' 2>/dev/null; true", + text.replace('\'', "'\\''"), + pattern.replace('\'', "'\\''"), + ); + let _ = bash.exec(&script4).await; + }); + } +});