Repository navigation
fix(testing): attribute fuzz canary echoes to the script that caused them - #2747
Merged
Merged
Conversation
…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.
Deploying with
|
| 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_invariantsnow 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_tripnow countsFUZZ_HOST_CANARYalongsideUNIVERSAL_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_fuzzon an input the fuzzer assembled out of the detector's own secret:assert_fuzz_invariantscompares output againstFUZZ_HOST_CANARYwithcontains, and that comparison is instrumented. libFuzzer recovered the needle as a 32-bytePersAutoDictentry — 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_BANNEDshapes, extended to the canary — not a new mechanism.Before / After
Before — the nightly panic, reproduced locally from the crash input:
After:
The detector is still strict — these fail on purpose when the script cannot explain the canary:
Validation on this branch:
cargo test --workspace --lib --bins --tests --features http_client,ssh,sqlitecargo test --test proptest_security -- --test-threads=1cargo fmt --all --checkcargo clippy -p bashkit --all-targets --features http_client,ssh,sqlite -- -D warningsjust check-okfFuzz Testingdispatched on this branchThe one local failure is
ssh_supabase_tests::ssh_supabase_connects, which dials the live publicsupabase.shendpoint on port 22 — unreachable from this HTTPS-only sandbox, unrelated to this change.Risk
src/testing.rsis#[doc(hidden)]and used solely by fuzz targets, proptest cases and integration tests. No production code path changes.input_echo_would_trippre-filter already makes forUNIVERSAL_BANNED, and it is pinned by the two negative tests above. No fuzz target puts the canary in a script on purpose.cpython_fuzz), the pre-filter layer is what covers them — and it now includes the canary.Checklist
assert_fuzz_invariantsgains ascriptparameter; all 6 call sites inproptest_security.rsupdated. Internal test helper, not public API.