diff --git a/crates/bashkit/fuzz/Cargo.toml b/crates/bashkit/fuzz/Cargo.toml index 9b7c2889..9a53dbc7 100644 --- a/crates/bashkit/fuzz/Cargo.toml +++ b/crates/bashkit/fuzz/Cargo.toml @@ -66,3 +66,10 @@ path = "fuzz_targets/awk_fuzz.rs" test = false doc = false bench = false + +[[bin]] +name = "printf_fuzz" +path = "fuzz_targets/printf_fuzz.rs" +test = false +doc = false +bench = false diff --git a/crates/bashkit/fuzz/fuzz_targets/printf_fuzz.rs b/crates/bashkit/fuzz/fuzz_targets/printf_fuzz.rs new file mode 100644 index 00000000..2646016d --- /dev/null +++ b/crates/bashkit/fuzz/fuzz_targets/printf_fuzz.rs @@ -0,0 +1,87 @@ +//! Fuzz target for the printf builtin +//! +//! Tests printf format string parsing and argument formatting to find: +//! - Panics on malformed format specifiers (width, precision, type) +//! - Stack overflow from pathological format strings +//! - Numeric conversion edge cases (%d, %o, %x on non-numeric input) +//! - Escape sequence handling (\n, \x, \0, etc.) +//! +//! Run with: cargo +nightly fuzz run printf_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 format string (first line) and arguments (remaining lines) + let (format, args_str) = match input.find('\n') { + Some(pos) => (&input[..pos], &input[pos + 1..]), + None => (input, "" as &str), + }; + + // Skip empty format strings + if format.is_empty() { + 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(); + + // Build argument list from remaining lines + let args: Vec<&str> = if args_str.is_empty() { + vec![] + } else { + args_str.lines().collect() + }; + let args_escaped: Vec = args + .iter() + .map(|a| format!("'{}'", a.replace('\'', "'\\''"))) + .collect(); + let args_joined = args_escaped.join(" "); + + // Test 1: printf with fuzzed format string and arguments + let script = format!( + "printf '{}' {} 2>/dev/null; true", + format.replace('\'', "'\\''"), + args_joined, + ); + let _ = bash.exec(&script).await; + + // Test 2: printf with -v flag (assign to variable) + let script2 = format!( + "printf -v result '{}' {} 2>/dev/null; true", + format.replace('\'', "'\\''"), + args_joined, + ); + let _ = bash.exec(&script2).await; + + // Test 3: printf with numeric format specifiers and fuzzed args + let script3 = format!( + "printf '%d %o %x' {} 2>/dev/null; true", + args_joined, + ); + let _ = bash.exec(&script3).await; + }); + } +});