Skip to content

Commit 4fab7fd

Browse files
committed
feat(fuzz): add grep_fuzz target for ReDoS prevention
Closes #1099 — Fuzz target for the grep builtin to detect catastrophic backtracking (ReDoS) in regex patterns. Tests basic, extended (-E), case-insensitive (-in), and inverted match (-vc) modes.
1 parent ae579e3 commit 4fab7fd

File tree

2 files changed

+109
-0
lines changed

2 files changed

+109
-0
lines changed

crates/bashkit/fuzz/Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,10 @@ path = "fuzz_targets/template_fuzz.rs"
108108
test = false
109109
doc = false
110110
bench = false
111+
112+
[[bin]]
113+
name = "grep_fuzz"
114+
path = "fuzz_targets/grep_fuzz.rs"
115+
test = false
116+
doc = false
117+
bench = false
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
//! Fuzz target for the grep builtin
2+
//!
3+
//! Tests regex pattern compilation and matching to find:
4+
//! - ReDoS from catastrophic backtracking on pathological patterns
5+
//! - Panics in bracket expression parsing or extended regex features
6+
//! - Edge cases in case-insensitive matching, invert, and context lines
7+
//! - Graceful rejection of invalid regex patterns
8+
//!
9+
//! Run with: cargo +nightly fuzz run grep_fuzz -- -max_total_time=300
10+
11+
#![no_main]
12+
13+
use libfuzzer_sys::fuzz_target;
14+
15+
fuzz_target!(|data: &[u8]| {
16+
// Only process valid UTF-8
17+
if let Ok(input) = std::str::from_utf8(data) {
18+
// Limit input size to prevent OOM
19+
if input.len() > 1024 {
20+
return;
21+
}
22+
23+
// Split input into regex pattern (first line) and search text (rest)
24+
let (pattern, text) = match input.find('\n') {
25+
Some(pos) => (&input[..pos], &input[pos + 1..]),
26+
None => (input, "hello world\nfoo bar\nbaz qux\n" as &str),
27+
};
28+
29+
// Skip empty patterns
30+
if pattern.trim().is_empty() {
31+
return;
32+
}
33+
34+
// Reject deeply nested regex groups (ReDoS mitigation)
35+
let depth: i32 = pattern
36+
.bytes()
37+
.map(|b| match b {
38+
b'(' | b'[' => 1,
39+
b')' | b']' => -1,
40+
_ => 0,
41+
})
42+
.scan(0i32, |acc, d| {
43+
*acc += d;
44+
Some(*acc)
45+
})
46+
.max()
47+
.unwrap_or(0);
48+
if depth > 15 {
49+
return;
50+
}
51+
52+
let rt = tokio::runtime::Builder::new_current_thread()
53+
.enable_all()
54+
.build()
55+
.unwrap();
56+
57+
rt.block_on(async {
58+
let mut bash = bashkit::Bash::builder()
59+
.limits(
60+
bashkit::ExecutionLimits::new()
61+
.max_commands(50)
62+
.max_subst_depth(3)
63+
.max_stdout_bytes(4096)
64+
.max_stderr_bytes(4096)
65+
.timeout(std::time::Duration::from_millis(200)),
66+
)
67+
.build();
68+
69+
// Test 1: basic grep pattern matching
70+
let script = format!(
71+
"echo '{}' | grep '{}' 2>/dev/null; true",
72+
text.replace('\'', "'\\''"),
73+
pattern.replace('\'', "'\\''"),
74+
);
75+
let _ = bash.exec(&script).await;
76+
77+
// Test 2: extended regex (-E)
78+
let script2 = format!(
79+
"echo '{}' | grep -E '{}' 2>/dev/null; true",
80+
text.replace('\'', "'\\''"),
81+
pattern.replace('\'', "'\\''"),
82+
);
83+
let _ = bash.exec(&script2).await;
84+
85+
// Test 3: case-insensitive with line numbers (-in)
86+
let script3 = format!(
87+
"echo '{}' | grep -in '{}' 2>/dev/null; true",
88+
text.replace('\'', "'\\''"),
89+
pattern.replace('\'', "'\\''"),
90+
);
91+
let _ = bash.exec(&script3).await;
92+
93+
// Test 4: inverted match with count (-vc)
94+
let script4 = format!(
95+
"echo '{}' | grep -vc '{}' 2>/dev/null; true",
96+
text.replace('\'', "'\\''"),
97+
pattern.replace('\'', "'\\''"),
98+
);
99+
let _ = bash.exec(&script4).await;
100+
});
101+
}
102+
});

0 commit comments

Comments
 (0)