Skip to content

fix(redirect): report redirection failures with bash's strerror wording - #2319

Merged
chaliy merged 1 commit into
mainfrom
claude/pensive-hypatia-92oe7p
Aug 21, 2026
Merged

chaliy merged 1 commit into
mainfrom
claude/pensive-hypatia-92oe7p

Conversation

@chaliy

@chaliy chaliy commented Aug 21, 2026 •

Copy link
Copy Markdown
Contributor

What changed

Redirection failures no longer print the Rust error-enum Display. ls < /nope now says bash: /nope: No such file or directory — real bash's wording — instead of bash: /nope: io error: file not found.

The substitution is deliberately narrow: only messages that restate their io::ErrorKind and nothing more are replaced. A backend's own reason survives verbatim, because it tells an agent why in a way the bare errno cannot.

This also unbreaks the nightly fuzz gate (glob_fuzz), currently red on main.

Why

Two problems, one root cause.

Parity. Every redirection diagnostic rendered crate::Error's Display, which prefixes io error: and emits the VFS's internal message. Real bash prints the C strerror string.

Red CI. fuzz.yml run 218 failed on input </r\0ustc/:

thread '<unnamed>' panicked at crates/bashkit/src/testing.rs:104:5:
[glob_fuzz] stderr leaks banned shape `/rustc/` (after stripping shell echoes):
---raw stderr---
bash: /rustc/: io error: file not found

glob_fuzz pre-filters inputs that literally contain a UNIVERSAL_BANNED shape, but the shell drops the NUL byte during word expansion — so the redirect target became /rustc/ after that filter had already run. The leak detector suppresses such echoes only for lines matching a recognized real-shell template, and io error: file not found is not one. A genuine echo of user input was reported as a TM-INF-016 host-path leak.

Fixing the wording fixes both: the diagnostic becomes bash-accurate and recognizable as an echo.

Before / After

$ bashkit -c 'ls /tmp/ < /nope/missing'
- bash: /nope/missing: io error: file not found
+ bash: /nope/missing: No such file or directory

$ bashkit -c 'echo hi > /nope/missing/out.txt'
- bash: /nope/missing/out.txt: io error: parent directory not found
+ bash: /nope/missing/out.txt: No such file or directory

# read-only mount — reason kept, only the enum prefix goes
$ bashkit -c 'printf nope > /tmp/nope.txt'
- bash: /tmp/nope.txt: io error: filesystem is read-only
+ bash: /tmp/nope.txt: filesystem is read-only

Real bash, for reference:

$ bash -c 'ls /tmp/ < /nope/missing'
bash: line 1: /nope/missing: No such file or directory
$ bash -c 'echo hi > /nope/missing/out.txt'
bash: line 1: /nope/missing/out.txt: No such file or directory

The crash input replayed through the actual fuzz target, rebuilt against this branch:

$ cargo +nightly fuzz run glob_fuzz fuzz/artifacts/glob_fuzz/crash-fae53719665e9c77d2e1a1b7d4da7e43902525d0
Running: fuzz/artifacts/glob_fuzz/crash-fae53719665e9c77d2e1a1b7d4da7e43902525d0
Executed crash-fae53719665e9c77d2e1a1b7d4da7e43902525d0 in 13 ms

(before this branch, the same command aborted with libFuzzer: deadly signal)

New scaffold, which replays all three scripts glob_fuzz builds from one input:

$ cargo test -p bashkit --test integration glob_fuzz
test glob_fuzz_scaffold_tests::glob_fuzz_crash_nul_stripped_redirect_target ... ok
test glob_fuzz_scaffold_tests::missing_input_redirect_matches_bash_wording ... ok
test glob_fuzz_scaffold_tests::missing_output_redirect_dir_matches_bash_wording ... ok
test glob_fuzz_scaffold_tests::read_only_mount_keeps_its_specific_reason ... ok
test glob_fuzz_scaffold_tests::nul_byte_in_word_is_dropped ... ok
test result: ok. 5 passed; 0 failed

Risk

  • Low

Redirection error text changes. The first push of this branch collapsed every failure to bare errno text and turned 8 checks red — two Python binding tests asserting on the read-only reason, one of them backed by a custom FileSystem impl. That is exactly the information loss the narrow rule now avoids, and both tests pass unmodified. No test assertions were changed to accommodate this PR.

Verified locally: workspace lib/bins/tests, doc tests, realfs, failpoints, proptest_security, pytest (788 passed), clippy -D warnings, cargo fmt, check_okf.py, check_doc_links.py, plus a 10-minute glob_fuzz session against the rebuilt target.

Expanding the harness echo filter narrows leak detection slightly. The added entries are fixed templates that quote the redirect target verbatim, same rationale as the existing entries; the byte-length cap and host-canary check still run on unfiltered stderr.

Two known-environmental local failures, both green on main in CI and unrelated to this diff: ssh_supabase_connects (sandbox blocks outbound port 22) and test_tm_dos_021_fork_bomb_blocked (segfaults in a debug build; CI builds release).

Checklist

  • Tests added or updated
  • Backward compatibility considered

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 21, 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 04d1e2d Commit Preview URL

Branch Preview URL
Aug 21 2026, 10:11 AM

Redirection diagnostics rendered the Rust error-enum `Display`, so a
missing file reported `bash: /nope: io error: file not found` where real
bash reports `bash: /nope: No such file or directory`.

Besides the parity gap this broke the nightly fuzz gate. `glob_fuzz` run
218 failed on input `</r\0ustc/`: the shell drops the NUL during
expansion, so the redirect target became `/rustc/` *after* the target's
own input pre-filter ran, and the unrecognized `io error:` template was
reported as a TM-INF-016 host-path leak rather than a shell echo.

`redirect_error_reason` drops the enum prefix and substitutes bash's
strerror text — but only for messages that restate their `io::ErrorKind`
and nothing more: the VFS placeholders in `ERRNO_RESTATING_MESSAGES`, and
errors carrying a `raw_os_error` (whose Display appends a non-bash
`(os error N)` suffix). Every other message survives verbatim, because a
backend-specific reason tells an agent why in a way the bare errno
cannot: `filesystem is read-only` must not collapse into `Permission
denied`, and a custom `FileSystem` impl's wording is the only diagnostic
its embedder gets.

Those specific reasons are covered instead by adding their fixed
templates to the harness echo filter, alongside the remaining errno
templates.
@chaliy
chaliy force-pushed the claude/pensive-hypatia-92oe7p branch from cad9ae6 to 04d1e2d Compare August 21, 2026 10:10
@chaliy
chaliy merged commit 6f0e6ba into main Aug 21, 2026
43 checks passed
@chaliy
chaliy deleted the claude/pensive-hypatia-92oe7p branch August 21, 2026 10:25
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