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
15 changes: 15 additions & 0 deletions crates/bashkit/fuzz/fuzz_targets/arithmetic_fuzz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,21 @@ fuzz_target!(|data: &[u8]| {
return;
}

// Reject inputs that themselves contain banned substrings. An
// arithmetic error names the expression and the unparsed rest
// verbatim, so the script's own text comes back in stderr -- real bash
// does the same. An echoed user-controlled string that happens to
// contain e.g. `Tok::` is not a TM-INF-022 leak: the tokenizer's enum
// is never formatted. Run 271 found `:A>>=::TTA:::Tok::::`, whose
// diagnostic reads `... (error token is ":A>>=::TTA:::Tok::::")`.
// The arithmetic diagnostic is not one of the real-shell templates
// `strip_real_shell_error_lines` recognizes, so filter at the input
// layer, as `glob_fuzz` and `cpython_fuzz` do, and keep the leak
// detector strict for genuine internals.
if bashkit::testing::input_echo_would_trip(input) {
return;
}

// Wrap input in arithmetic expansion context
let script = format!("echo $(({}))", input);

Expand Down
38 changes: 38 additions & 0 deletions crates/bashkit/tests/integration/arithmetic_fuzz_scaffold_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,41 @@ async fn valid_expression_still_evaluates() {
let r = bash.exec("echo $((2 + 3 * 4))").await.unwrap();
assert_eq!(r.stdout, "14\n");
}

/// Run 271: the diagnostic echoes the script's own text, which happened to
/// contain the banned parser-token shape `Tok::`.
///
/// The tokenizer's `Tok` enum is never formatted -- this is the expression
/// coming back verbatim, exactly as real bash echoes it. The target now skips
/// such inputs at the input layer. Assert that, then assert the diagnostic is
/// still a faithful echo and still bounded, so the skip is hiding a false
/// positive rather than a real leak.
#[tokio::test]
async fn run_271_banned_shape_comes_from_the_input() {
let input = ":A>>=::TTA:::Tok::::";
assert!(bashkit::testing::input_echo_would_trip(input));

let mut bash = fuzz_bash();
let r = bash
.exec(&format!("echo $(({input}))"))
.await
.expect("exec");
let err = r.stderr.to_string();
// The echo is the user's own text, not a Debug shape: it appears inside
// the quoted error token, and only because the input put it there.
assert!(
err.contains(&format!("(error token is \"{input}\")")),
"{err}"
);
assert!(err.len() <= 1024, "stderr is {} bytes:\n{err}", err.len());
}

/// A banned shape bashkit itself emitted would still be caught: the same
/// expression without the `Tok::` text produces a diagnostic with no banned
/// substring at all, so the filter above is not blanket-suppressing this path.
#[tokio::test]
async fn same_shape_without_the_banned_text_is_still_checked() {
let input = ":A>>=::TTA:::X::::";
assert!(!bashkit::testing::input_echo_would_trip(input));
fuzz_arith(input, "no_banned_text").await;
}
12 changes: 12 additions & 0 deletions knowledge/security/threat-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,18 @@ fragment is now bounded by `MAX_ARITHMETIC_DIAG_ECHO` via
that follows it (L-ARITH-002). Any new diagnostic that quotes script text back
has the same obligation: bound the quoted run, not the finished line.

The flip side is a false positive. A diagnostic that quotes script text back
can quote a banned shape the *input* supplied, which is not a leak: real bash
echoes the same text, and the named internal (the arithmetic tokenizer's `Tok`
enum, say) is never formatted. Two independent defenses handle this, and a
fuzz target that inlines raw bytes needs one of them: either its diagnostic
matches a template `strip_real_shell_error_lines` recognizes, or the target
pre-filters with `input_echo_would_trip` as `glob_fuzz`, `cpython_fuzz` and
`arithmetic_fuzz` do. Arithmetic diagnostics are not one of those templates,
so `arithmetic_fuzz` went red in run 271 on `:A>>=::TTA:::Tok::::` until it
pre-filtered. Adding a builtin whose errors quote user text means choosing one
of the two, never relaxing `UNIVERSAL_BANNED`.

Display of a **Bashkit** error is not a safe formatter either: `Error`'s own
variants stringify as Rust enum shapes (`io error: `, `internal error: `) that
no shell prints, and `internal error:` is in `UNIVERSAL_BANNED`. Builtins
Expand Down
Loading