Skip to content

Commit 51f63a9

Browse files
authored
feat(fuzz): add sed_fuzz target (#1145)
## Summary - Add fuzz target for the `sed` builtin (`builtins/sed.rs`) - Tests sed command parsing, BRE-to-ERE regex conversion, and address ranges - Exercises basic, extended regex (`-E`), and suppress output (`-n`) modes Closes #1096 ## Test plan - [x] `cargo check` in fuzz crate passes - [x] `cargo fmt --check` clean - [x] `cargo clippy --all-targets --all-features -- -D warnings` clean - [x] All tests pass (pre-existing `ssh_supabase` and `stack_overflow` failures unrelated)
1 parent 527d6fd commit 51f63a9

File tree

2 files changed

+101
-0
lines changed

2 files changed

+101
-0
lines changed

crates/bashkit/fuzz/Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,10 @@ path = "fuzz_targets/envsubst_fuzz.rs"
8787
test = false
8888
doc = false
8989
bench = false
90+
91+
[[bin]]
92+
name = "sed_fuzz"
93+
path = "fuzz_targets/sed_fuzz.rs"
94+
test = false
95+
doc = false
96+
bench = false
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
//! Fuzz target for the sed builtin
2+
//!
3+
//! Tests sed command parsing and BRE-to-ERE regex conversion to find:
4+
//! - Panics in sed command parsing or address ranges
5+
//! - Stack overflow from deeply nested regex groups
6+
//! - ReDoS from pathological BRE/ERE patterns
7+
//! - Edge cases in BRE-to-ERE conversion logic
8+
//!
9+
//! Run with: cargo +nightly fuzz run sed_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 sed expression (first line) and input data (rest)
24+
let (expr, input_data) = 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 expressions
30+
if expr.trim().is_empty() {
31+
return;
32+
}
33+
34+
// Reject deeply nested regex groups
35+
let depth: i32 = expr
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 sed expression
70+
let script = format!(
71+
"echo '{}' | sed '{}' 2>/dev/null; true",
72+
input_data.replace('\'', "'\\''"),
73+
expr.replace('\'', "'\\''"),
74+
);
75+
let _ = bash.exec(&script).await;
76+
77+
// Test 2: sed with -E (extended regex) flag
78+
let script2 = format!(
79+
"echo '{}' | sed -E '{}' 2>/dev/null; true",
80+
input_data.replace('\'', "'\\''"),
81+
expr.replace('\'', "'\\''"),
82+
);
83+
let _ = bash.exec(&script2).await;
84+
85+
// Test 3: sed with -n (suppress output) flag
86+
let script3 = format!(
87+
"echo '{}' | sed -n '{}' 2>/dev/null; true",
88+
input_data.replace('\'', "'\\''"),
89+
expr.replace('\'', "'\\''"),
90+
);
91+
let _ = bash.exec(&script3).await;
92+
});
93+
}
94+
});

0 commit comments

Comments
 (0)