Skip to content

Fix OOB panic in redacting JS highlighter when ${} ends the input#31700

Merged
Jarred-Sumner merged 3 commits into
mainfrom
farm/cfe7760f/fix-highlighter-redact-oob
Jun 2, 2026
Merged

Fix OOB panic in redacting JS highlighter when ${} ends the input#31700
Jarred-Sumner merged 3 commits into
mainfrom
farm/cfe7760f/fix-highlighter-redact-oob

Conversation

@robobun

@robobun robobun commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

Crash

Sentry groups BUN-3C97 / BUN-3AA5 report a panic inside
QuickAndDirtyJavaScriptSyntaxHighlighter::fmt (src/bun_core/fmt.rs) while printing an
error message:

panic: range end index 5 out of range for slice of length 4

Investigating that signature turned up three bugs in the highlighter's scan loop — one
already fixed, two still live on main and fixed here:

  1. The Sentry signature itself (end 5 / len 4) matches Bun REPL dumps characters from memory (out-of-bounds read) and segfaults when inputting a string interpolation with a backslash in it #31434 exactly: a trailing \
    inside an unterminated `${ (the 4 bytes `${\) pushed the cursor to 5. That was
    fixed by Fix out-of-bounds read in JS highlighter on trailing backslash in ${} #31435 (May 26); the reported events predate the fix reaching canary users.

  2. ${...} ending the input (still crashing on current canary): when a template
    interpolation ends exactly at the end of the input, the string-scan loop exits with
    i == 0 and text fully consumed. The redact_sensitive_information branch then slices
    text[1..i]&[][1..0]:

    panic: range start index 1 out of range for slice of length 0
    
  3. Redacted keyword followed by only whitespace (found in review): after a
    RedactedKeyword (token, email, _auth, …), the whitespace-skip loop can drain
    text to empty, and control falls through to match text[0]:

    panic: index out of bounds: the len is 0 but the index is 0
    

Repro (current canary, 1.4.0-canary.1)

printf 'logLevel = 3 # `${}\n' > bunfig.toml
echo 'console.log("hi")' > index.js
FORCE_COLOR=1 bun run index.js   # crashes, exit 132

The bunfig type error is printed with redact_sensitive_information: true and the offending
source line highlighted; the line ends with `${}, so the highlighter panics while
printing the error message
— the user sees a crash report instead of their config error.
Same shape is reachable from .npmrc error printing (also redacting). Bug 3 reproduces with
highlightJavaScriptRedacted("token ") via the testing binding.

Fix

  • Guard the i == 0 case before forming the inner slice in the redact branch; i == 0
    can only happen via the interpolation sub-path with an empty remainder, so there is no
    quoted content to inspect.
  • Hoist the emptiness check to right after the whitespace-skip loop; the existing check
    inside the =/: branch only fired when a separator was present.
  • Both mirrored into the .zig porting reference.

Also exposes the redacting highlighter options through the bun:internal-for-testing fmt
binding (highlight-javascript-redacted) so tests can drive the redact code path directly,
matching how #31435 tests the non-redacting options.

Verification

  • USE_SYSTEM_BUN=1 bun test test/js/bun/util/highlighter.test.ts → the end-to-end test
    crashes with panic: range start index 1 out of range for slice of length 0 (exit 132),
    matching the live bug; the redacted-binding tests fail (binding doesn't exist yet).

  • bun bd test test/js/bun/util/highlighter.test.ts → 16 pass, 0 fail.

  • Fuzzed the patched highlighter through the real binary in both modes (plain + redacting):

    • exhaustive over all inputs of length ≤ 5 from a 24-char alphabet covering every token
      class the scanner dispatches on (backtick/quotes, ${/}, \, digits, identifier
      chars, /*, //, newlines, =/:, and 2-, 3- and 4-byte UTF-8 chars) — ~8.3M inputs;
    • keyword-gated paths: every RedactedKeyword and the prev_keyword-setting syntax
      keywords as prefixes (optionally inside `${), × exhaustive tails of length ≤ 3
      (alphabet + \t) × keyword-not-at-start variants, plus ~1M keyword-prefixed random
      inputs up to 33 chars;
    • ~2.1M additional seeded-random inputs of length 5–30.

    No panics remain.

When a template-literal interpolation ends exactly at the end of the
highlighted text, the string-scan loop in
QuickAndDirtyJavaScriptSyntaxHighlighter exits with i == 0 and an empty
remainder, and the redact_sensitive_information branch then slices
text[1..0]:

  panic: range start index 1 out of range for slice of length 0

Reachable from any error printed with source context and redaction
enabled (bunfig.toml / .npmrc errors), e.g. a bunfig.toml containing:

  logLevel = 3 # `${}

Guard the i == 0 case before forming the inner slice; mirror the fix
into the .zig porting reference. Expose the redacting highlighter
options through the internal-for-testing fmt binding so the regression
test can exercise them directly.
@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 5 minutes and 42 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0b3028ca-9e2d-4d35-8ecb-e16531761e1d

📥 Commits

Reviewing files that changed from the base of the PR and between 64ae83c and ed241e8.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • src/bun_core/fmt.rs
  • src/bun_core/fmt.zig
  • src/js/internal-for-testing.ts
  • src/jsc/fmt_jsc.bind.ts
  • src/jsc/fmt_jsc.rs
  • test/js/bun/util/highlighter.test.ts

Comment @coderabbitai help to get the list of available commands and usage tips.

@robobun

robobun commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator Author

Status: ready for review/merge — diff is green; the red build is four unrelated flaky/infra failures.

  • Reproduced the live crash on 1.4.0-canary.1: bunfig.tomllogLevel = 3 # \${}→ panic while printing the error (exit 132). Root cause: redact branch slicestext[1..0]when a${...}` interpolation ends exactly at end-of-input.
  • Review found a third OOB in the same loop (redacted keyword + trailing whitespace → text[0] on empty slice) — also fixed; thread resolved; two LGTMs from claude[bot].
  • Regression tests cover all shapes (16 pass on the fix; on unpatched bun the e2e test reproduces the panic). Fuzzed ~12M inputs incl. keyword-prefixed sweeps through both highlighter modes — no remaining panics.
  • The original Sentry signature (end 5 / len 4) is the already-merged Fix out-of-bounds read in JS highlighter on trailing backslash in ${} #31435; the events predate that fix. This PR fixes the two remaining OOBs in the same scan loop.

CI disposition (build 59873, final — 282 passed, 4 failed):

lane failure relation to diff
debian-13-x64 streams-leak.test.ts expected >20 chunks, got 8–11 (load-dependent coalescing); failed 4 auto-retries none — same tree passed all 20 shards of this lane in build 59851
darwin-14-aarch64 test-retry-repeats-basic.test.ts none — test-runner retry semantics; diff doesn't touch the runner
darwin-14-x64 test-retry-repeats-basic.test.ts same
darwin-14-x64 terminal.test.ts (PTY) + npm test-registry No version matching "1.0.1" for "bytes" none — PTY flake + registry infra

test/js/bun/util/highlighter.test.ts passed in every lane that ran it (incl. ASAN). Build 59851 on the identical tree was fully green outside 3 Expired (agent-starved) macOS lanes; one retrigger already used. Needs a maintainer to merge past the flaky lanes (or retry the failed jobs).

@github-actions github-actions Bot added the claude label Jun 2, 2026
@robobun

robobun commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator Author

Comment thread src/bun_core/fmt.rs
When the previous token is a redacted keyword (token, email, _auth, ...)
and the rest of the input is whitespace, the whitespace-skip loop drains
text to empty and control falls through to `match text[0]`:

  panic: index out of bounds: the len is 0 but the index is 0

Hoist the emptiness check to right after the whitespace-skip loop; the
existing check inside the '='/':' branch only fired when a separator was
present. Mirror in the .zig porting reference.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — the follow-up commit addresses the third OOB (token → empty-slice index) exactly as suggested, with matching tests and the fmt.zig mirror.

Extended reasoning...

Overview

Two narrowly-scoped bounds guards in QuickAndDirtyJavaScriptSyntaxHighlighter::fmt (src/bun_core/fmt.rs, mirrored in the src/bun_core/fmt.zig porting reference): an i == 0 early-break before slicing text[1..i] in the redact branch, and a hoisted text.is_empty() early-return after the redact-preamble whitespace-skip loop. A new highlight-javascript-redacted variant is plumbed through the test-only fmt_jsc binding so the redacting code path can be driven directly from highlighter.test.ts, which gains three new test.each blocks plus an end-to-end bunfig repro. Cargo.lock adds bstr to bun_bin (already a transitive dep — lockfile-only noise).

Security risks

None. The highlighter is a best-effort cosmetic colorizer for diagnostic output; the redaction logic it touches only masks values for display and is unchanged in behavior except for not panicking on degenerate tail inputs. The new binding is gated behind bun:internal-for-testing. No new inputs, parsing surfaces, or trust boundaries are introduced.

Level of scrutiny

Low–medium. This is defensive hardening of an error-printing path, traced back to a real Sentry crash, with a reproducible repro and regression tests. The two guards are pure additions (early-return / early-break) — they cannot change output for previously-non-crashing inputs. The follow-up commit (36317b2) implements exactly the fix I requested in the prior review pass, and the inline thread is resolved.

Other factors

The bug-hunting pass on the latest revision found nothing further. Targeted re-fuzzing now covers the keyword-gated paths that the original sweep missed. The one CI failure on d8aaf7d is the unrelated, known-flaky duckdb.test.ts third-party segfault. No CODEOWNERS rules apply to the touched files.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — the third OOB (redacted keyword + trailing whitespace) is fixed exactly as suggested, mirrored in the Zig reference, and covered by the new tests.

Extended reasoning...

Overview

Two narrowly-scoped bounds-check guards in QuickAndDirtyJavaScriptSyntaxHighlighter::fmt (both src/bun_core/fmt.rs and the .zig porting reference): an i == 0 early-break before slicing text[1..i] in the redact branch, and a hoisted text.is_empty() early-return after the whitespace-skip loop. A new highlight-javascript-redacted variant is plumbed through the bun:internal-for-testing fmt binding so the redact path can be driven directly from tests, and highlighter.test.ts gains regression cases for both panics plus an end-to-end bunfig repro and a positive assertion that redaction still works. Cargo.lock picks up bstr for bun_bin.

Security risks

None. The change only adds early-exit guards to a cosmetic error-printing path; it does not weaken redaction (the new _authToken = "npm_123456" test confirms values are still masked). The new binding is gated behind bun:internal-for-testing.

Level of scrutiny

Low–medium. This is a crash fix in a best-effort syntax highlighter used for error messages, not core runtime logic. The guards are minimal, well-commented, and the failure mode they prevent (panic while printing a diagnostic) is strictly worse than any possible regression in highlight quality.

Other factors

My previous review flagged the third OOB; commit 36317b2 applied the suggested fix verbatim in both files and added the requested test cases, and the thread is resolved. The author re-fuzzed with keyword-prefixed inputs and reports clean. The current bug-hunting pass found nothing. The only CI failure noted was macOS lanes expiring (infra), with a retrigger commit on top — no code change since the fix landed.

@Jarred-Sumner Jarred-Sumner merged commit 4b42439 into main Jun 2, 2026
75 of 79 checks passed
@Jarred-Sumner Jarred-Sumner deleted the farm/cfe7760f/fix-highlighter-redact-oob branch June 2, 2026 21:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants