Skip to content

Commit 3f9c35f

Browse files
authored
feat(fuzz): add printf_fuzz target (#1140)
## Summary - Add fuzz target for the `printf` builtin covering arbitrary format strings, width/precision specifiers, numeric conversion, escape sequences, and `-v` flag assignment - Follows existing fuzz target patterns with execution limits and timeout guards ## Test plan - [x] `cargo check` passes in fuzz directory - [ ] CI green Closes #1103
1 parent 35a9f8f commit 3f9c35f

File tree

2 files changed

+94
-0
lines changed

2 files changed

+94
-0
lines changed

crates/bashkit/fuzz/Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,10 @@ path = "fuzz_targets/awk_fuzz.rs"
6666
test = false
6767
doc = false
6868
bench = false
69+
70+
[[bin]]
71+
name = "printf_fuzz"
72+
path = "fuzz_targets/printf_fuzz.rs"
73+
test = false
74+
doc = false
75+
bench = false
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
//! Fuzz target for the printf builtin
2+
//!
3+
//! Tests printf format string parsing and argument formatting to find:
4+
//! - Panics on malformed format specifiers (width, precision, type)
5+
//! - Stack overflow from pathological format strings
6+
//! - Numeric conversion edge cases (%d, %o, %x on non-numeric input)
7+
//! - Escape sequence handling (\n, \x, \0, etc.)
8+
//!
9+
//! Run with: cargo +nightly fuzz run printf_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 format string (first line) and arguments (remaining lines)
24+
let (format, args_str) = match input.find('\n') {
25+
Some(pos) => (&input[..pos], &input[pos + 1..]),
26+
None => (input, "" as &str),
27+
};
28+
29+
// Skip empty format strings
30+
if format.is_empty() {
31+
return;
32+
}
33+
34+
let rt = tokio::runtime::Builder::new_current_thread()
35+
.enable_all()
36+
.build()
37+
.unwrap();
38+
39+
rt.block_on(async {
40+
let mut bash = bashkit::Bash::builder()
41+
.limits(
42+
bashkit::ExecutionLimits::new()
43+
.max_commands(50)
44+
.max_subst_depth(3)
45+
.max_stdout_bytes(4096)
46+
.max_stderr_bytes(4096)
47+
.timeout(std::time::Duration::from_millis(200)),
48+
)
49+
.build();
50+
51+
// Build argument list from remaining lines
52+
let args: Vec<&str> = if args_str.is_empty() {
53+
vec![]
54+
} else {
55+
args_str.lines().collect()
56+
};
57+
let args_escaped: Vec<String> = args
58+
.iter()
59+
.map(|a| format!("'{}'", a.replace('\'', "'\\''")))
60+
.collect();
61+
let args_joined = args_escaped.join(" ");
62+
63+
// Test 1: printf with fuzzed format string and arguments
64+
let script = format!(
65+
"printf '{}' {} 2>/dev/null; true",
66+
format.replace('\'', "'\\''"),
67+
args_joined,
68+
);
69+
let _ = bash.exec(&script).await;
70+
71+
// Test 2: printf with -v flag (assign to variable)
72+
let script2 = format!(
73+
"printf -v result '{}' {} 2>/dev/null; true",
74+
format.replace('\'', "'\\''"),
75+
args_joined,
76+
);
77+
let _ = bash.exec(&script2).await;
78+
79+
// Test 3: printf with numeric format specifiers and fuzzed args
80+
let script3 = format!(
81+
"printf '%d %o %x' {} 2>/dev/null; true",
82+
args_joined,
83+
);
84+
let _ = bash.exec(&script3).await;
85+
});
86+
}
87+
});

0 commit comments

Comments
 (0)