Skip to content

fix(interpreter): bound script text echoed into arithmetic diagnostics - #2665

Merged
chaliy merged 2 commits into
mainfrom
eve/clever-mccarthy-fasrpn
Oct 10, 2026
Merged

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

Conversation

@chaliy

@chaliy chaliy commented Oct 10, 2026

Copy link
Copy Markdown
Contributor

What changed

An arithmetic error no longer echoes unbounded script text back into stderr.
$(( )), (( )), let, [[ a -eq b ]] and ${v:off:len} name at most 256
bytes each of the expression and of the "error token" (the unparsed rest), with
a ... marker where either was cut. Expressions at or under the cap are
unchanged, byte for byte.

A new arithmetic_fuzz scaffold suite carries the crash input into cargo test,
so this class is caught on every PR instead of only by the nightly fuzz job.

Why

An arithmetic diagnostic names the whole expression and then the whole
remaining source. Both come from the script, so an expression whose first
rejected character sits near the front was echoed back twice over. One input
well under MAX_ARITHMETIC_EXPANSION_BYTES (64 KiB) therefore rendered a
diagnostic about twice its size, past the 1 KiB stderr budget every builtin is
held to under TM-INF-022.

Nightly fuzz run 270 (arithmetic_fuzz) tripped assert_no_leak on exactly
that: 507 bytes of input, 1,076 bytes of stderr.

Real bash emits 1,065 bytes for the same input, so bashkit was bash-faithful
here — the ceiling is bashkit's own contract, and the arithmetic path had simply
never been held to it. The divergence is recorded as L-ARITH-002.

Before / After

The fuzz crash input, 507 bytes of ~ with a rejected # near the front:

Before: 1076 bytes of stderr  -> assert_no_leak panics, fuzz target exits 77
After:   596 bytes of stderr  -> under the 1024-byte cap

(Measured through the CLI as 691 bytes with a 99-character script path as $0;
596 is the same line with $0 = bash, as the fuzz target and tests run it.)

The reason for the failure survives truncation. Both fragments stop at 256
bytes with a ... marker, and the text between and after them is intact
(real output, elided in the middle here only for width):

crash.sh: line 1: +---+~~~~~~~~~~~#~~~~~~~~ech~~~~ [...226B...] ~~~...: syntax
error: invalid arithmetic operator (error token is "#~~~~~~~~ech~~~~~~~~~~~~
[...218B...] ~~~...")

Short diagnostics are untouched and still match bash 5.2 exactly:

$ bashkit -c 'echo $((1/0))'
bash: line 1: 1/0: division by 0 (error token is "0")
$ bashkit -c 'echo $((1/(0+0)))'
bash: line 1: 1/(0+0): division by 0 (error token is "(0+0)")

Negative control: with MAX_ARITHMETIC_DIAG_ECHO neutralised, all six new
bounded-diagnostic tests fail, at 1,081 / 1,085 / 1,085 / 1,086 / 1,265 / 1,658
bytes of stderr; with the cap in place all six pass.

Risk

  • Low.
  • Arithmetic diagnostics for expressions over 256 bytes are now truncated, so a
    caller that string-matched on the tail of a very long echoed expression would
    see the ... marker instead. Expressions at or under the cap are unchanged.
  • No change to exit status, control flow, or evaluation results — only the text
    of the diagnostic.

Checklist

  • Tests added or updated
  • Backward compatibility considered

https://claude.ai/code/session_01BegucbcUgCEsPZiAzt6GRx


Generated by Claude Code

An arithmetic error names the whole expression and then the whole unparsed
rest as the "error token". Both come from the script, so an expression whose
first rejected character sits near the front was echoed back twice over: one
input well under MAX_ARITHMETIC_EXPANSION_BYTES rendered a diagnostic about
twice its size, past the 1 KiB stderr budget TM-INF-022 holds every builtin to.

Nightly fuzz run 270 (arithmetic_fuzz) tripped assert_no_leak on exactly that:
507 bytes of input, 1,076 bytes of stderr. Real bash emits 1,065 bytes for the
same input, so the ceiling is bashkit's own contract rather than a parity bug,
and the arithmetic path had never been held to it.

diag_echo bounds each echoed fragment at MAX_ARITHMETIC_DIAG_ECHO (256 bytes)
on a UTF-8 boundary, applied to the expression echo, the tokenizer and parser
error tokens, bad-number and division-by-zero tokens, the [[ ]] operand, and
the substring-expression messages. The fixed text that says what went wrong
always survives. Expressions at or under the cap are unchanged, byte for byte.

A new arithmetic_fuzz scaffold suite carries the crash input into cargo test,
so the class is caught per-PR rather than only by the nightly fuzz job.

Recorded as L-ARITH-002; TM-INF-022 now states that interpreter-written
diagnostics quoting script text must bound the quoted run, not the finished line.

Claude-Session: https://claude.ai/code/session_01BegucbcUgCEsPZiAzt6GRx
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 10, 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 987ff45 Commit Preview URL

Branch Preview URL
Oct 10 2026, 09:32 AM

The rustdoc TM-INF-022 row described the ceiling as a builtin concern
enforced by the Debug-format scan. Arithmetic diagnostics quote the script
back and had their own way past it, so name that path and the cap that now
bounds it.

Claude-Session: https://claude.ai/code/session_01BegucbcUgCEsPZiAzt6GRx
@chaliy
chaliy merged commit da890ec into main Oct 10, 2026
49 checks passed
@chaliy
chaliy deleted the eve/clever-mccarthy-fasrpn branch October 10, 2026 09:59
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