Skip to content

fix(testing): attribute fuzz canary echoes to the script that caused them - #2747

Merged
chaliy merged 2 commits into
mainfrom
eve/clever-mccarthy-snhng4
Oct 18, 2026
Merged

chaliy merged 2 commits into
mainfrom
eve/clever-mccarthy-snhng4

Conversation

@chaliy

@chaliy chaliy commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

What changed

The fuzz leak detector's host-canary check (TM-INF-013) is now attribution-based: a canary in stdout/stderr fails only when the executed script cannot account for it. Previously any occurrence failed, which made the check forgeable by the fuzzer itself.

Two layers, both kept:

  • assert_fuzz_invariants now takes the script that produced the result, and skips the canary assertion when that script already carries the canary (that output is a shell echo, not a host-env read). Covers every target that inlines input, including those with no pre-filter.
  • input_echo_would_trip now counts FUZZ_HOST_CANARY alongside UNIVERSAL_BANNED, so pre-filtering targets (arithmetic_fuzz, glob_fuzz, cpython_fuzz) skip these inputs before executing them.

Both ask the same question — does this text carry the needle into a diagnostic, literally or after the shell drops NUL during word expansion — so that question is now one named helper, carries_through_expansion, rather than two copies kept in sync by comment.

A canary the script never mentions still fails, unchanged.

Why

Nightly Fuzz run 272 went red in arithmetic_fuzz on an input the fuzzer assembled out of the detector's own secret:

**HOST_CARY:ARY_47a83bcf_DO_NOT_LHi&BASHKIT_FUZZ_HOST_CANARY_47a83bcf_DO_NOT_LEAKEAKa

assert_fuzz_invariants compares output against FUZZ_HOST_CANARY with contains, and that comparison is instrumented. libFuzzer recovered the needle as a 32-byte PersAutoDict entry — the run's own final stats name it, DE: "BASHKIT_FUZZ_HOST_CANARY_47a83bc" — crossed the remaining bytes in from the corpus, and handed the target an expression carrying the whole canary. The arithmetic diagnostic echoed it back verbatim, as real bash does, and the assertion fired with no builtin having touched host env.

So the detector arms the fuzzer against itself, and every target that echoes input verbatim can be driven into it. Obfuscating or splitting the constant does not help: the reconstructed needle is still what gets compared. The only durable fix is to ask where the canary came from.

This is the same defense runs 218/219/271 added for echoed UNIVERSAL_BANNED shapes, extended to the canary — not a new mechanism.

Before / After

Before — the nightly panic, reproduced locally from the crash input:

$ cargo test -p bashkit --test integration arithmetic_fuzz_scaffold
thread '...' panicked at crates/bashkit/src/testing.rs:110:5:
[arithmetic_fuzz] FUZZ canary leaked into stderr (TM-INF-013 regression — a builtin is reading host env). stderr:
---
bash: line 1: **HOST_CARY:ARY_47a83bcf_DO_NOT_LHi&BASHKIT_FUZZ_HOST_CANARY_47a83bcf_DO_NOT_LEAKEAKa: syntax error: operand expected ...
---
test result: FAILED. 0 passed; 1 failed

After:

test arithmetic_fuzz_scaffold_tests::run_272_canary_comes_from_the_input ... ok
test arithmetic_fuzz_scaffold_tests::canary_not_explained_by_the_script_is_still_a_leak - should panic ... ok
test arithmetic_fuzz_scaffold_tests::canary_in_stdout_not_explained_by_the_script_is_still_a_leak - should panic ... ok
test result: ok. 12 passed; 0 failed

The detector is still strict — these fail on purpose when the script cannot explain the canary:

test testing::tests::canary_echoed_from_the_script_is_not_a_leak ... ok
test testing::tests::canary_absent_from_the_script_is_still_a_leak - should panic ... ok
test testing::tests::canary_absent_from_the_script_is_still_a_leak_on_stdout - should panic ... ok
test testing::tests::canary_in_the_input_is_counted_as_an_echo ... ok

Validation on this branch:

Check Result
cargo test --workspace --lib --bins --tests --features http_client,ssh,sqlite 3,912 lib + 2,580 integration + all other binaries pass
cargo test --test proptest_security -- --test-threads=1 27 passed
cargo fmt --all --check clean
cargo clippy -p bashkit --all-targets --features http_client,ssh,sqlite -- -D warnings clean
just check-okf OKF v0.2 conformant
Fuzz Testing dispatched on this branch replays the cached corpus, incl. the run-272 units

The one local failure is ssh_supabase_tests::ssh_supabase_connects, which dials the live public supabase.sh endpoint on port 22 — unreachable from this HTTPS-only sandbox, unrelated to this change.

Risk

  • Low. Test-only surface: src/testing.rs is #[doc(hidden)] and used solely by fuzz targets, proptest cases and integration tests. No production code path changes.
  • What could break: a genuine TM-INF-013 leak would be missed only if the script itself also contained the canary. That is the same trade-off the existing input_echo_would_trip pre-filter already makes for UNIVERSAL_BANNED, and it is pinned by the two negative tests above. No fuzz target puts the canary in a script on purpose.
  • The attribution check reads the script, which fuzz targets inline their input into. For targets that write input to the VFS instead (cpython_fuzz), the pre-filter layer is what covers them — and it now includes the canary.

Checklist

  • Tests added or updated
  • Backward compatibility considered — assert_fuzz_invariants gains a script parameter; all 6 call sites in proptest_security.rs updated. Internal test helper, not public API.

…them

Nightly `arithmetic_fuzz` (run 272) went red on an input the fuzzer built
out of the leak detector's own secret:

    **HOST_CARY:ARY_47a83bcf_DO_NOT_LHi&BASHKIT_FUZZ_HOST_CANARY_47a83bcf_DO_NOT_LEAKEAKa

`assert_fuzz_invariants` compares output against `FUZZ_HOST_CANARY` with
`contains`, and that comparison is instrumented, so libFuzzer recovered the
needle as a 32-byte `PersAutoDict` entry
(`DE: "BASHKIT_FUZZ_HOST_CANARY_47a83bc"`), crossed the remaining bytes in
from the corpus, and handed the target an expression carrying the whole
canary. The arithmetic diagnostic echoed it back verbatim — as real bash
does — and the TM-INF-013 assertion fired with no builtin having read host
env. Obfuscating the constant does not help: the reconstructed needle is
still what gets compared.

The canary check is now attribution-based. A canary in output is a leak only
when the executed script cannot account for it, so `assert_fuzz_invariants`
takes the script, and `input_echo_would_trip` counts the canary alongside
`UNIVERSAL_BANNED` so pre-filtering targets skip these inputs before
executing them. Both layers are kept: the pre-filter saves the execution,
the attribution check covers every target that inlines input, including the
ones with no pre-filter. This is the same defense runs 218/219/271 added for
echoed `UNIVERSAL_BANNED` shapes, extended to the canary.

A canary in output that the script never mentions still fails, unchanged —
covered by negative tests on both streams.

Before: `cargo test arithmetic_fuzz_scaffold` reproduces the nightly panic,
`[arithmetic_fuzz] FUZZ canary leaked into stderr (TM-INF-013 regression)`.
After: 12/12 scaffold tests pass; 3,912 lib + 2,580 integration + 27
proptest_security tests pass; fmt, clippy (-D warnings) and check-okf clean.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
bashkit 8ac496b Commit Preview URL

Branch Preview URL
Oct 18 2026, 09:54 AM

`input_echo_would_trip` and the new canary attribution check both asked the
same question — does this text carry the needle into a diagnostic, literally
or after the shell drops NUL during word expansion. Give it a name,
`carries_through_expansion`, so the two agree by construction instead of by
a comment pointing at each other.

The helper guards the `replace` behind `contains('\0')`, so the allocation
now happens only for input that actually holds a NUL. The previous code
hoisted one `replace` per call; this does none on the common path.

No behavior change: 20/20 `testing::` unit tests, 3,912 lib and 2,580
integration tests pass, clippy -D warnings and fmt clean.
@chaliy
chaliy merged commit 70d10af into main Oct 18, 2026
55 checks passed
@chaliy
chaliy deleted the eve/clever-mccarthy-snhng4 branch October 18, 2026 10:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant