Skip to content

bundler: do not abort on a Bun.build files specifier longer than a path buffer - #40345

Merged
Jarred-Sumner merged 5 commits into
mainfrom
farm/8c138a86/filemap-long-specifier
Sep 12, 2026
Merged

Jarred-Sumner merged 5 commits into
mainfrom
farm/8c138a86/filemap-long-specifier

Conversation

@robobun

@robobun robobun commented Aug 24, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • Bun.build({ files }) aborts when an in-memory file holds a specifier longer than a path buffer: panic: range end index 4096 out of range for slice of length 4095 (1024 on macOS, 98302 on Windows). Every loader reaches it: CSS url() and @import, JS import, import() and require(), HTML <script src> and <link href>. Any inline data: image over 4 KB in a virtual CSS file hits it. The same file from disk builds fine. Fixes Bun.build({ files }) panics on CSS data URLs at 1024 bytes #39252.
  • Cause: FileMap::resolve (src/bundler/bundle_v2.rs:1022) treats every non-absolute specifier as relative and joins it onto the importer's directory with join_abs_string_buf, which writes into a fixed PathBuffer with no bounds check. On Windows, get, contains and resolve also copy the raw specifier into a PathBuffer, unchecked.

Fix

  • FileMap::resolve joins with join_abs_string_buf_checked and returns None when the result does not fit, the rule the resolver applies in check_relative_path. The specifier then reaches the resolver, which marks a data: URL external and reports Could not resolve for a too-long path.
  • The importer path goes through abs_buf_checked and a length check before its separator normalization.
  • One get_key_value helper replaces the three Windows separator normalizations. A specifier longer than a path buffer is never a key.
  • Verified: test/bundler/bundler_files.test.ts, four new tests in a child process (CSS url(), JS import, HTML references, entry point), all abort on 1.4.0. Also bundler_plugin, bundler_defer, bundler_naming, html-import-manifest, css/doesnt_crash, cargo check for Windows.

Background

  • files: is the in-memory file map of Bun.build. Before the resolver runs, FileMap::resolve checks each import specifier against that map: by exact key, then joined onto the importer's directory.
  • PathBuffer is [u8; MAX_PATH_BYTES]: 4096 bytes on Linux, 1024 on macOS, 98302 on Windows. join_abs_string_buf normalizes into it with plain indexing. The _checked variant returns None instead.
  • The resolver parses data: URLs before any path join.
Notes

Related PRs:

Repro on 1.4.0 and on main (44411167):

const url = "data:image/svg+xml," + "A".repeat(4096 - 19);
const css = `.x { background: url("${url}") }\n`;
const r = await Bun.build({ entrypoints: ["/style.css"], files: { "/style.css": css } });
console.log(r.success);

Stack on a debug build (the crash handler only prints the top frames in release):

bun_paths::resolve_path::normalize_string_generic_tz  src/paths/resolve_path.rs:1076
bun_paths::resolve_path::join_abs_string_buf<Loose>    src/paths/resolve_path.rs:1672
bun_bundler::bundle_v2::...::JSBundler::FileMap::resolve  src/bundler/bundle_v2.rs:1022
bun_bundler::bundle_v2::BundleV2::resolve_import_records
bun_bundler::bundle_v2::BundleV2::run_resolution_for_parse_task
bun_bundler::bundle_v2::BundleV2::on_parse_task_complete

Checked on the fixed build: quoted and unquoted url(), @font-face src, https: and #fragment URLs of 64 KiB all build. A 128 KiB relative, bare or dynamic import, require(), CSS @import, and HTML <script src> / <link href> report Could not resolve, the same as from a disk file. A 128 KiB data: URL in an HTML <img src> is kept. Each of these aborts on 1.4.0 (/usr/local/bin/bun, 34cbb9a40). Relative imports between virtual files, .. segments, and a relative CSS url() to a virtual asset still resolve as before.

With the fix, a too-long specifier skips the relative join and reaches the resolver. A virtual file whose key itself is longer than a path buffer cannot be found through a relative specifier. Such keys are not supported elsewhere in the bundler either (output path computation uses path buffers).

Sentry BUN-4S5H (1.4.1-canary abe2ad4f0, Linux x64) is this crash, reached from a JS import whose specifier is ./ plus 2100 a/ segments. The same guards also cover the importer side: a virtual file whose key is longer than a path buffer, reached by an exact key match, used to abort in abs_buf or path_to_posix_buf on its first relative import.


[review] gate passed · iteration 0 · 2 files touched

fails on main (without fix)
ASAN without fix: 4 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bundler/bundler_files.test.ts
bun test v1.4.1 (4448a2e21)

test/bundler/bundler_files.test.ts:
(pass) bundler files option > basic in-memory file bundling [129.16ms]
(pass) bundler files option > in-memory file with imports [67.19ms]
(pass) bundler files option > in-memory file with relative imports (same directory) [84.60ms]
(pass) bundler files option > in-memory file with relative imports (subdirectory) [49.04ms]
(pass) bundler files option > in-memory file with relative imports (parent directory) [42.94ms]
(pass) bundler files option > in-memory file with relative imports between multiple files [42.54ms]
(pass) bundler files option > in-memory file with nested imports [43.76ms]
(pass) bundler files option > in-memory file with TypeScript [50.30ms]
(pass) bundler files option > in-memory file with JSX [242.61ms]
(pass) bundler files option > in-memory file with Blob content [45.77ms]
(pass) bundler files option > in-memory file with a file-backed Blob is rejected [30.44ms]
(pass) bundler files option > in-memory file with Uint8
... (truncated)

release without fix: all passed
bun test v1.4.1-canary.1 (939574e50)

test/bundler/bundler_files.test.ts:
(pass) bundler files option > basic in-memory file bundling [3.79ms]
(pass) bundler files option > in-memory file with imports [1.40ms]
(pass) bundler files option > in-memory file with relative imports (same directory) [1.61ms]
(pass) bundler files option > in-memory file with relative imports (subdirectory) [1.26ms]
(pass) bundler files option > in-memory file with relative imports (parent directory) [1.23ms]
(pass) bundler files option > in-memory file with relative imports between multiple files [0.96ms]
(pass) bundler files option > in-memory file with nested imports [0.90ms]
(pass) bundler files option > in-memory file with TypeScript [0.89ms]
(pass) bundler files option > in-memory file with JSX [5.06ms]
(pass) bundler files option > in-memory file with Blob content [2.13ms]
(pass) bundler files option > in-memory file with a file-backed Blob is rejected [0.91ms]
(pass) bundler files option > in-memory file with Uint8Array content [1.62ms]
(pass) bundler files option > in-memory file with ArrayBuffer content [2.36ms]
(pass) bundler files option > in-memory file with re-exports [1.46ms]

... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bundler/bundler_files.test.ts
bun test v1.4.1 (4448a2e21)

test/bundler/bundler_files.test.ts:
(pass) bundler files option > basic in-memory file bundling [138.75ms]
(pass) bundler files option > in-memory file with imports [70.73ms]
(pass) bundler files option > in-memory file with relative imports (same directory) [85.08ms]
(pass) bundler files option > in-memory file with relative imports (subdirectory) [84.82ms]
(pass) bundler files option > in-memory file with relative imports (parent directory) [45.01ms]
(pass) bundler files option > in-memory file with relative imports between multiple files [44.79ms]
(pass) bundler files option > in-memory file with nested imports [51.14ms]
(pass) bundler files option > in-memory file with TypeScript [49.94ms]
(pass) bundler files option > in-memory file with JSX [228.09ms]
(pass) bundler files option > in-memory file with Blob content [44.26ms]
(pass) bundler files option > in-memory file with a file-backed Blob is rejected [27.70ms]
(pass) bundler files option > in-memory file with Uint8
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 693ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[0/5] cargo bun_runtime → libbun_runtime.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

^[[1m^[[92m   Compiling^[[0m bun_core v0.0.0 (/workspace/bun/src/bun_core)
^[[1m^[[92m   Compiling^[[0m bun_errno v0.0.0 (/workspace/bun/src/errno)
^[[1m^[[92m   Compiling^[[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr)
^[[1m^[[92m   Compiling^[[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys)
^[[1m^[[92m   Compiling^[[0m bun_safety v0.0.0 (/workspace/bun/src/safety)
^[[1m^[[92m   Compiling^[[0m bun_base64 v0.0.0 (/workspace/bun/src/base64)
^[[1m^[[92m   Compiling^[[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys)
^[[1m^[[92m   Compiling^[[0m bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys)
^[[1m^[[92m   Compiling^[[0m bun_zstd v0.0.0 (/workspace/bun/src/zstd)
^[[1m^[[92m   Compiling^[[0m bun_picohttp v0.0.0 (/workspace/bun/src/picohttp)
^[[1m^[[92m   Compiling^[[0m bun_brotli v0.0.0 (/workspace/bun/src/brotli)
^[[1m^[[92m   Compiling^[[0m bu
... (truncated)
diff hotspot
src/bundler/bundle_v2.rs           |  58 +++++++++------------
 test/bundler/bundler_files.test.ts | 100 ++++++++++++++++++++++++++++++++++++-
 2 files changed, 123 insertions(+), 35 deletions(-)

gate history · 2 passed · 0 rejected · iteration 0

evidence per changed file
file                                reads  edits  tests
src/bundler/bundle_v2.rs               10      7      0
test/bundler/bundler_files.test.ts      2      3      0

…th buffer

FileMap::resolve joined every non-absolute specifier onto the importer's
directory with join_abs_string_buf, which writes into a fixed PathBuffer
with no bounds check. A CSS url() or an import specifier longer than the
buffer (4096 bytes on Linux, 1024 on macOS) panicked with
"range end index N out of range for slice of length 4095" and aborted
the process.

Use join_abs_string_buf_checked and treat a result that does not fit as
"not a virtual file", so the specifier falls through to the resolver
(which marks data: URLs external and reports too-long paths as not
found). Guard the importer path the same way, and bound the Windows
separator normalization in get, contains and resolve, which copied the
raw specifier into a PathBuffer.

Fixes #39252
@coderabbitai

coderabbitai Bot commented Aug 24, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

  • Run on-demand review

On-demand reviews are free for the next 27 days. After that, they cost $0.25 per reviewed file.

Or wait 16 minutes for your next included review.

View limit details

Limit details: You’ve used all 5 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 43cc4af4-67c6-451a-b8d8-e080b4b087cf

📥 Commits

Reviewing files that changed from the base of the PR and between 861e9ae and de882e3.

📒 Files selected for processing (2)
  • src/bundler/bundle_v2.rs
  • test/bundler/bundler_files.test.ts

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

@robobun

robobun commented Aug 24, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 8:05 AM PT - Aug 24th, 2026

✅ @robobun, your commit de882e34722db119f136041cb208d6c81cf09d4e passed in Build #104926! 🎉


🧪   To try this PR locally:

bunx bun-pr 40345

That installs a local version of the PR into your bun-40345 executable, so you can run:

bun-40345 --bun

@robobun

robobun commented Aug 24, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: CI is green on de882e3 (Build #104926), ready for review.

Reproduced on bun 1.4.0 and on a debug build of main with the script from #39252: Bun.build({ entrypoints: ["/style.css"], files: { "/style.css": css } }) where the CSS holds a url("data:image/svg+xml,AAAA...") of 4096 bytes. The process aborts with panic: range end index 4096 out of range for slice of length 4095. The debug build's backtrace ends in FileMap::resolve (src/bundler/bundle_v2.rs:1022), the unchecked join_abs_string_buf onto the importer's directory. The same join is reached by every loader that resolves a specifier from an in-memory file (JS import, import(), require(), CSS @import, HTML <script src> and <link href>).

Verification is in the PR description (test/bundler/bundler_files.test.ts, the "specifiers longer than a path buffer" block: all four tests abort on 1.4.0, pass with the fix).

@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 — targeted crash fix replacing unchecked path-buffer writes with checked variants.

What was reviewed:

  • join_abs_string_buf_checked / abs_buf_checked return Option, so ? correctly falls through to the resolver; path_to_posix_buf writes exactly path.len() bytes, so the > buf.len() guards are the right bound.
  • Windows get_key_value: returned key/value borrow from self.map, not the local buffer, so no lifetime issue; contains/get semantics unchanged.
  • Tests use 128 KiB specifiers (exceeds all platform limits), run in child processes since the bug is a crash, and follow harness conventions (Buffer.alloc, concurrent pipe drain, describe.concurrent).
Extended reasoning...

Overview

The PR fixes a process abort in Bun.build({ files }) when an in-memory file contains an import specifier or CSS url() longer than a PathBuffer (4096 bytes on Linux, 1024 on macOS, 98302 on Windows). FileMap::resolve in src/bundler/bundle_v2.rs joined every non-absolute specifier onto the importer's directory with join_abs_string_buf, which indexes into a fixed-size buffer without bounds checking. The fix swaps in join_abs_string_buf_checked and abs_buf_checked (both already in-tree, returning Option), adds a length guard before each path_to_posix_buf call, and consolidates three copies of the Windows separator-normalization lookup into one get_key_value helper. Three new subprocess tests in test/bundler/bundler_files.test.ts cover the CSS url() case, a too-long relative import, and a too-long relative entry point.

Security risks

None. This turns a panic on adversarial-length user input into a graceful None that hands the specifier to the resolver, which already handles data: URLs and reports Could not resolve for oversized paths. No new trust boundaries, no allocation sized from untrusted input.

Level of scrutiny

Medium — bundler resolution is a hot, correctness-sensitive path, but the change is narrow and mechanical: three unchecked buffer writes become checked, plus a dedup refactor. I verified that path_to_posix_buf writes exactly path.len() bytes (so > buf.len() is the correct comparison, not >=), that abs_buf_checked and join_abs_string_buf_checked both return Option so the ? operator propagates correctly to the enclosing Option<bun_resolver::Result>, and that on Windows the new get_key_value returns references into self.map (not the local pooled buffer), so there is no dangling borrow. contains and get are now thin wrappers over get_key_value with identical semantics to before.

Other factors

The tests are well-constructed: they run in child processes because the bug aborts, use 131072-byte specifiers to exceed every platform's path-buffer size, use Buffer.alloc(n, fill).toString() per repo convention, drain stdout/stderr/exited concurrently, assert stderr and exitCode in the right order, and use describe.concurrent for the independent subprocess spawns. The PR description documents that all three tests abort on 1.4.0 and pass with the fix, and that the surrounding bundler test files still pass. The dedup of the three Windows normalization blocks into one helper is a net simplification with no behavior change on the existing paths.

Comment thread src/bundler/bundle_v2.rs Outdated
Comment thread src/bundler/bundle_v2.rs Outdated
Comment thread src/bundler/bundle_v2.rs Outdated
Comment thread src/bundler/bundle_v2.rs Outdated
Comment thread src/bundler/bundle_v2.rs Outdated

@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 — targeted crash fix that swaps unchecked path-buffer writes for their checked variants and falls through to the resolver on overflow.

What was reviewed:

  • Verified path_to_posix_buf indexes into buf[..path.len()] unchecked, so the new specifier.len() > buf.len() guard in get_key_value and the abs_source_file length check are both required and correctly bounded.
  • Confirmed join_abs_string_buf_checked and abs_buf_checked return None on overflow, so ? propagation makes resolve fall through to the real resolver — same behavior as any non-matching specifier.
  • Checked the get_key_value refactor preserves the prior get/contains/direct-match semantics on both cfg branches; only the Windows overflow guard is new.
  • Tests spawn a child (the bug is an abort), use 131072-byte specifiers to exceed every platform's path buffer, and assert exact {success, logs} shapes.
Extended reasoning...

Overview

The PR fixes a process abort in Bun.build({ files }) when an in-memory file contains an import specifier or CSS url() longer than MAX_PATH_BYTES. FileMap::resolve in src/bundler/bundle_v2.rs joined every non-absolute specifier onto the importer's directory with join_abs_string_buf, which writes into a fixed-size PathBuffer with plain indexing. On Windows, get, contains and resolve also copied the raw specifier into a path buffer via path_to_posix_buf, likewise unchecked. The fix swaps in the _checked variants and adds explicit length guards before path_to_posix_buf, and consolidates three duplicated Windows-normalization blocks into one get_key_value helper. Three subprocess tests are added to test/bundler/bundler_files.test.ts.

Security risks

None. The change turns a user-reachable panic into a graceful None return, which is exactly what the review guide asks for ("user-reachable failures are recoverable errors, never panics"). No new trust boundary is introduced; the specifier simply falls through to the normal resolver, which already handles data: URLs and reports Could not resolve for oversize relative paths.

Level of scrutiny

Medium-low. This is a mechanical unchecked→checked substitution in a well-scoped helper. I read path_to_posix_buf (resolve_path.rs:2453) to confirm it panics when path.len() > buf.len(), so both new length guards are necessary and correctly use > rather than >=. I read join_abs_string_buf_checked (resolve_path.rs:1680) and abs_buf_checked (resolver/lib.rs:348) to confirm they return Option and that ? propagation is sound here — resolve already returns Option<bun_resolver::Result>, and None is the existing not-in-map signal, so callers need no change. The get_key_value refactor is a pure deduplication: get and contains on non-Windows still hit map.get_key_value directly, and the direct-match block in resolve is behavior-identical to the removed cfg-split code.

Other factors

The tests follow harness conventions: child process for the crash case, Buffer.alloc(n, fill) instead of .repeat(), concurrent subprocess spawns, drained stdout/stderr/exited together, exact-object assertions with the specifier normalized out of the log message. The 131072-byte specifier exceeds Linux (4096), macOS (1024), and Windows (98302) buffer sizes, so the tests exercise the guard on every platform. The comment-cop bot's earlier objections to long comments were addressed in ab3fc04 and 939574e and all threads are resolved. The PR description documents that the full bundler_files suite plus adjacent bundler suites and cargo check for Windows were run.

@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 — narrow crash fix that swaps unchecked path-buffer joins for their checked variants.

What was reviewed:

  • FileMap::resolve: join_abs_string_buf_checked/abs_buf_checked return Option, and ? correctly short-circuits to None so the specifier falls through to the resolver.
  • The get_key_value refactor preserves the prior get/contains semantics on both cfg branches; the new Windows length guard is needed because path_to_posix_buf indexes into buf unchecked.
  • Tests use 128 KiB specifiers (exceeds every platform's MAX_PATH_BYTES), run in a child process, drain both pipes concurrently, and assert exact structured output.
Extended reasoning...

Overview

This PR fixes a process abort in Bun.build({ files }) when an in-memory file contains an import specifier longer than MAX_PATH_BYTES. The fix touches FileMap in src/bundler/bundle_v2.rs (~40 lines net) and adds four regression tests to test/bundler/bundler_files.test.ts. Three unchecked path-buffer writes (join_abs_string_buf, abs_buf, path_to_posix_buf) become checked, and three duplicated Windows separator-normalization blocks are folded into one get_key_value helper.

Security risks

None. This turns a user-reachable panic (a DoS on the calling process) into a graceful None that lets the resolver report Could not resolve or treat a data: URL as external. No new inputs are accepted; the change only prevents overflowing a fixed-size buffer.

Level of scrutiny

Moderate. The bundler is a hot path, but the change is mechanical: each unchecked helper is swapped for its documented _checked sibling that returns Option, and ? propagates None out of a function that already returns Option<bun_resolver::Result>. The get/contains refactor is a straight de-duplication — contains_key → get_key_value(...).is_some() is semantically identical, and the non-Windows branch is unchanged aside from returning the key alongside the value. I confirmed path_to_posix_buf uses raw slice indexing (src/paths/resolve_path.rs:2458), so the added length guard before it is required.

Other factors

The tests are well-constructed: they spawn a child process (the bug is a crash), use a 128 KiB specifier that exceeds every platform's path-buffer size including Windows's 98302, drain stdout/stderr/exited concurrently, and assert an exact JSON shape rather than substring matching. describe.concurrent keeps the four subprocess spawns from serializing. The comment-cop feedback about long comments was addressed in ab3fc04/939574e and all threads are resolved. The PR description clearly scopes out the adjacent path-buffer panics tracked in #39626 and #38696, which live at different call sites.

@Jarred-Sumner
Jarred-Sumner merged commit 02c1da3 into main Sep 12, 2026
11 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the farm/8c138a86/filemap-long-specifier branch September 12, 2026 04:34
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.

Bun.build({ files }) panics on CSS data URLs at 1024 bytes

2 participants