Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions crates/bashkit/fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,10 @@ path = "fuzz_targets/glob_fuzz.rs"
test = false
doc = false
bench = false

[[bin]]
name = "jq_fuzz"
path = "fuzz_targets/jq_fuzz.rs"
test = false
doc = false
bench = false
86 changes: 86 additions & 0 deletions crates/bashkit/fuzz/fuzz_targets/jq_fuzz.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
//! Fuzz target for the jq builtin
//!
//! Tests jq filter expression parsing and JSON processing to find:
//! - Panics in the jaq filter compiler
//! - Stack overflow from deeply nested filters
//! - ReDoS or CPU exhaustion from pathological patterns
//! - Memory exhaustion from recursive JSON generation
//!
//! Run with: cargo +nightly fuzz run jq_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 filter (first line) and JSON data (rest)
let (filter, json_data) = match input.find('\n') {
Some(pos) => (&input[..pos], &input[pos + 1..]),
None => (input, "{}" as &str),
};

// Skip empty filters
if filter.trim().is_empty() {
return;
}

// Reject deeply nested expressions
let depth: i32 = filter
.bytes()
.map(|b| match b {
b'(' | b'[' | b'{' => 1,
b')' | b']' | b'}' => -1,
_ => 0,
})
.scan(0i32, |acc, d| {
*acc += d;
Some(*acc)
})
.max()
.unwrap_or(0);
if depth > 20 {
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();

// Test 1: pipe JSON through jq filter
let script = format!(
"echo '{}' | jq '{}' 2>/dev/null; true",
json_data.replace('\'', "'\\''"),
filter.replace('\'', "'\\''"),
);
let _ = bash.exec(&script).await;

// Test 2: jq with -r (raw output) flag
let script2 = format!(
"echo '{}' | jq -r '{}' 2>/dev/null; true",
json_data.replace('\'', "'\\''"),
filter.replace('\'', "'\\''"),
);
let _ = bash.exec(&script2).await;
});
}
});
57 changes: 57 additions & 0 deletions crates/bashkit/tests/jq_fuzz_scaffold_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Scaffold tests for the jq_fuzz target.
// Validates that the jq builtin handles arbitrary filter expressions and
// malformed JSON without panicking.

use bashkit::{Bash, ExecutionLimits};

fn fuzz_bash() -> Bash {
Bash::builder()
.limits(
ExecutionLimits::new()
.max_commands(50)
.max_subst_depth(3)
.max_stdout_bytes(4096)
.max_stderr_bytes(4096)
.timeout(std::time::Duration::from_secs(2)),
)
.build()
}

#[tokio::test]
async fn jq_valid_filter() {
let mut bash = fuzz_bash();
let result = bash.exec("echo '{\"a\":1}' | jq '.a'").await.unwrap();
assert_eq!(result.stdout.trim(), "1");
}

#[tokio::test]
async fn jq_malformed_json() {
let mut bash = fuzz_bash();
let _ = bash.exec("echo 'not json' | jq '.' 2>/dev/null").await;
// Must not panic
}

#[tokio::test]
async fn jq_invalid_filter() {
let mut bash = fuzz_bash();
let _ = bash.exec("echo '{}' | jq '.[[[[[' 2>/dev/null; true").await;
// Must not panic
}

#[tokio::test]
async fn jq_deeply_nested_filter() {
let mut bash = fuzz_bash();
let filter = ".a".repeat(50);
let script = format!("echo '{{}}' | jq '{}' 2>/dev/null; true", filter);
let _ = bash.exec(&script).await;
// Must not panic or hang
}

#[tokio::test]
async fn jq_null_bytes_in_input() {
let mut bash = fuzz_bash();
let _ = bash
.exec("printf '{\"a\":\\x00}' | jq '.' 2>/dev/null; true")
.await;
// Must not panic
}
Loading