Skip to content

Commit 182ce60

Browse files
authored
feat(fuzz): add jq_fuzz target for jq builtin (#1111)
## Summary - Add fuzz target for the `jq` builtin (~1572 LOC) which has a complex custom filter expression parser (jaq) processing arbitrary JSON data - Target splits fuzz input into filter expression (first line) and JSON data (remainder) - Exercises both `jq '<filter>'` and `jq -r '<filter>'` through the full interpreter with tight resource limits - Includes depth-limiting filter to reject deeply nested expressions ## Changes - New `fuzz/fuzz_targets/jq_fuzz.rs`: fuzz target - Updated `fuzz/Cargo.toml`: added `[[bin]]` entry for `jq_fuzz` - New `tests/jq_fuzz_scaffold_tests.rs`: 5 scaffold tests (valid filter, malformed JSON, invalid filter, deep nesting, null bytes) ## Test plan - [x] `cargo test --test jq_fuzz_scaffold_tests` — 5 tests pass - [x] `cargo clippy --all-targets --all-features -- -D warnings` — clean - [x] `cargo fmt --check` — clean Closes #1094
1 parent de122ec commit 182ce60

File tree

3 files changed

+150
-0
lines changed

3 files changed

+150
-0
lines changed

crates/bashkit/fuzz/Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,10 @@ path = "fuzz_targets/glob_fuzz.rs"
5252
test = false
5353
doc = false
5454
bench = false
55+
56+
[[bin]]
57+
name = "jq_fuzz"
58+
path = "fuzz_targets/jq_fuzz.rs"
59+
test = false
60+
doc = false
61+
bench = false
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
//! Fuzz target for the jq builtin
2+
//!
3+
//! Tests jq filter expression parsing and JSON processing to find:
4+
//! - Panics in the jaq filter compiler
5+
//! - Stack overflow from deeply nested filters
6+
//! - ReDoS or CPU exhaustion from pathological patterns
7+
//! - Memory exhaustion from recursive JSON generation
8+
//!
9+
//! Run with: cargo +nightly fuzz run jq_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 filter (first line) and JSON data (rest)
24+
let (filter, json_data) = match input.find('\n') {
25+
Some(pos) => (&input[..pos], &input[pos + 1..]),
26+
None => (input, "{}" as &str),
27+
};
28+
29+
// Skip empty filters
30+
if filter.trim().is_empty() {
31+
return;
32+
}
33+
34+
// Reject deeply nested expressions
35+
let depth: i32 = filter
36+
.bytes()
37+
.map(|b| match b {
38+
b'(' | b'[' | b'{' => 1,
39+
b')' | 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 > 20 {
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: pipe JSON through jq filter
70+
let script = format!(
71+
"echo '{}' | jq '{}' 2>/dev/null; true",
72+
json_data.replace('\'', "'\\''"),
73+
filter.replace('\'', "'\\''"),
74+
);
75+
let _ = bash.exec(&script).await;
76+
77+
// Test 2: jq with -r (raw output) flag
78+
let script2 = format!(
79+
"echo '{}' | jq -r '{}' 2>/dev/null; true",
80+
json_data.replace('\'', "'\\''"),
81+
filter.replace('\'', "'\\''"),
82+
);
83+
let _ = bash.exec(&script2).await;
84+
});
85+
}
86+
});
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Scaffold tests for the jq_fuzz target.
2+
// Validates that the jq builtin handles arbitrary filter expressions and
3+
// malformed JSON without panicking.
4+
5+
use bashkit::{Bash, ExecutionLimits};
6+
7+
fn fuzz_bash() -> Bash {
8+
Bash::builder()
9+
.limits(
10+
ExecutionLimits::new()
11+
.max_commands(50)
12+
.max_subst_depth(3)
13+
.max_stdout_bytes(4096)
14+
.max_stderr_bytes(4096)
15+
.timeout(std::time::Duration::from_secs(2)),
16+
)
17+
.build()
18+
}
19+
20+
#[tokio::test]
21+
async fn jq_valid_filter() {
22+
let mut bash = fuzz_bash();
23+
let result = bash.exec("echo '{\"a\":1}' | jq '.a'").await.unwrap();
24+
assert_eq!(result.stdout.trim(), "1");
25+
}
26+
27+
#[tokio::test]
28+
async fn jq_malformed_json() {
29+
let mut bash = fuzz_bash();
30+
let _ = bash.exec("echo 'not json' | jq '.' 2>/dev/null").await;
31+
// Must not panic
32+
}
33+
34+
#[tokio::test]
35+
async fn jq_invalid_filter() {
36+
let mut bash = fuzz_bash();
37+
let _ = bash.exec("echo '{}' | jq '.[[[[[' 2>/dev/null; true").await;
38+
// Must not panic
39+
}
40+
41+
#[tokio::test]
42+
async fn jq_deeply_nested_filter() {
43+
let mut bash = fuzz_bash();
44+
let filter = ".a".repeat(50);
45+
let script = format!("echo '{{}}' | jq '{}' 2>/dev/null; true", filter);
46+
let _ = bash.exec(&script).await;
47+
// Must not panic or hang
48+
}
49+
50+
#[tokio::test]
51+
async fn jq_null_bytes_in_input() {
52+
let mut bash = fuzz_bash();
53+
let _ = bash
54+
.exec("printf '{\"a\":\\x00}' | jq '.' 2>/dev/null; true")
55+
.await;
56+
// Must not panic
57+
}

0 commit comments

Comments
 (0)