From 7371f3600175097fb77e1423d4611fb1c83dd0f9 Mon Sep 17 00:00:00 2001 From: Mykhailo Chalyi Date: Sat, 10 Oct 2026 10:42:48 +0000 Subject: [PATCH] test(fuzz): skip arithmetic inputs the diagnostic would echo back An arithmetic error names the expression and the unparsed rest verbatim, so the script's own text returns in stderr -- real bash does the same. When that text happens to contain a UNIVERSAL_BANNED substring, assert_no_leak trips on an echo rather than a leak: the tokenizer's Tok enum is never formatted. Fuzz run 271 found `:A>>=::TTA:::Tok::::`, whose diagnostic reads `... (error token is ":A>>=::TTA:::Tok::::")`, and the arithmetic diagnostic is not one of the real-shell templates strip_real_shell_error_lines knows. arithmetic_fuzz now pre-filters with input_echo_would_trip, the defense glob_fuzz and cpython_fuzz already use, keeping the leak detector strict for genuine internals. Of the seven targets in the nightly fuzz matrix this was the only gap: glob and cpython already filter, and parser, lexer, analyze and cpython_http do not run the leak check. Two regressions cover it: the run 271 input is recognized by the pre-filter and its diagnostic is still a bounded, faithful echo, and the same expression without the banned text still goes through the unfiltered leak check, so the filter is not blanket-suppressing this path. Claude-Session: https://claude.ai/code/session_01BegucbcUgCEsPZiAzt6GRx --- .../fuzz/fuzz_targets/arithmetic_fuzz.rs | 15 ++++++++ .../arithmetic_fuzz_scaffold_tests.rs | 38 +++++++++++++++++++ knowledge/security/threat-model.md | 12 ++++++ 3 files changed, 65 insertions(+) diff --git a/crates/bashkit/fuzz/fuzz_targets/arithmetic_fuzz.rs b/crates/bashkit/fuzz/fuzz_targets/arithmetic_fuzz.rs index 86c012453..f24fdcf97 100644 --- a/crates/bashkit/fuzz/fuzz_targets/arithmetic_fuzz.rs +++ b/crates/bashkit/fuzz/fuzz_targets/arithmetic_fuzz.rs @@ -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); diff --git a/crates/bashkit/tests/integration/arithmetic_fuzz_scaffold_tests.rs b/crates/bashkit/tests/integration/arithmetic_fuzz_scaffold_tests.rs index 3f158d2ac..79943710c 100644 --- a/crates/bashkit/tests/integration/arithmetic_fuzz_scaffold_tests.rs +++ b/crates/bashkit/tests/integration/arithmetic_fuzz_scaffold_tests.rs @@ -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; +} diff --git a/knowledge/security/threat-model.md b/knowledge/security/threat-model.md index 14fa3660c..a5896ba04 100644 --- a/knowledge/security/threat-model.md +++ b/knowledge/security/threat-model.md @@ -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