Skip to content

Commit d43a3b0

Browse files
authored
test: implement missing glob_fuzz target (#926)
Closes #915
1 parent 1dd3e2b commit d43a3b0

File tree

3 files changed

+86
-1
lines changed

3 files changed

+86
-1
lines changed

.github/workflows/fuzz.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
strategy:
2727
fail-fast: false
2828
matrix:
29-
target: [parser_fuzz, lexer_fuzz, arithmetic_fuzz]
29+
target: [parser_fuzz, lexer_fuzz, arithmetic_fuzz, glob_fuzz]
3030

3131
steps:
3232
- uses: actions/checkout@v6

crates/bashkit/fuzz/Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,10 @@ path = "fuzz_targets/arithmetic_fuzz.rs"
4545
test = false
4646
doc = false
4747
bench = false
48+
49+
[[bin]]
50+
name = "glob_fuzz"
51+
path = "fuzz_targets/glob_fuzz.rs"
52+
test = false
53+
doc = false
54+
bench = false
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
//! Fuzz target for glob/pathname expansion
2+
//!
3+
//! Tests glob pattern matching and expansion to find:
4+
//! - Exponential blowup with pathological patterns (TM-DOS-031)
5+
//! - Stack overflow with deeply nested extglob operators
6+
//! - Panics on malformed bracket expressions
7+
//! - Resource exhaustion from recursive globstar patterns
8+
//!
9+
//! Run with: cargo +nightly fuzz run glob_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 on huge patterns
19+
if input.len() > 512 {
20+
return;
21+
}
22+
23+
// Reject deeply nested extglob operators that could blow up
24+
let nesting: i32 = input
25+
.bytes()
26+
.map(|b| match b {
27+
b'(' => 1,
28+
b')' => -1,
29+
_ => 0,
30+
})
31+
.scan(0i32, |acc, d| {
32+
*acc += d;
33+
Some(*acc)
34+
})
35+
.max()
36+
.unwrap_or(0);
37+
if nesting > 15 {
38+
return;
39+
}
40+
41+
let rt = tokio::runtime::Builder::new_current_thread()
42+
.enable_all()
43+
.build()
44+
.unwrap();
45+
46+
rt.block_on(async {
47+
// Build a small VFS with some files to match against
48+
let mut bash = bashkit::Bash::builder()
49+
.limits(
50+
bashkit::ExecutionLimits::new()
51+
.max_commands(50)
52+
.timeout(std::time::Duration::from_millis(200)),
53+
)
54+
.mount_text("/tmp/a.txt", "")
55+
.mount_text("/tmp/b.sh", "")
56+
.mount_text("/tmp/c.md", "")
57+
.mount_text("/tmp/sub/d.txt", "")
58+
.mount_text("/tmp/sub/e.rs", "")
59+
.mount_text("/tmp/.hidden", "")
60+
.build();
61+
62+
// Test 1: glob expansion via ls (triggers expand_glob)
63+
let script = format!("ls /tmp/{} 2>/dev/null; true", input);
64+
let _ = bash.exec(&script).await;
65+
66+
// Test 2: pattern matching via case statement
67+
let script2 = format!(
68+
"case \"test.txt\" in {}) echo match;; *) echo no;; esac",
69+
input
70+
);
71+
let _ = bash.exec(&script2).await;
72+
73+
// Test 3: [[ conditional pattern matching
74+
let script3 = format!("if [[ \"hello.world\" == {} ]]; then echo y; fi", input);
75+
let _ = bash.exec(&script3).await;
76+
});
77+
}
78+
});

0 commit comments

Comments
 (0)