resolver: bound load_as_file path before writing into its PathBuffer

Closed
RO
3mo ago
+163-5

Problem

Any absolute import specifier at or past ~4092 bytes aborts the process instead of throwing MODULE_NOT_FOUND.

const p = "/" + "a".repeat(5000) + ".ts";
try { require.resolve(p) } catch (e) { console.log("ERR", e.code) }
console.log("SURVIVED");
panic: range end index 5004 out of range for slice of length 4096

Every public entry that reaches the module resolver hits it: require(p), require.resolve(p), await import(p), Bun.resolveSync(p, cwd), new Worker(p), Bun.build({entrypoints:[p]}), and bun /aaa...ts on the CLI. A 4091-byte path throws cleanly; 4092 aborts. Byte length governs (a 1404-char / 4204-byte €-filled path also aborts).

Cause

load_as_file copies the incoming path into a fixed MAX_PATH_BYTES PathBuffer before probing extensions:

bufs!(load_as_file)[..path.len()].copy_from_slice(path);

When the specifier's dirname names a real directory (e.g. /), read_directory succeeds and the copy runs with an arbitrarily long path. load_extension then slices [0..path.len() + ext.len()] from the same buffer, so a path that fits on its own still overflows once an extension is appended.

The directory probe that runs after load_as_file gives up has the same shape. load_as_index_with_browser_remapping appends SEP and a NUL to the directory path in a PathBuffer, which writes one byte past the end for a directory at MAX_PATH_BYTES - 1 (the longest path the OS allows). Under target: "browser" it also joins the directory with "index" through the unchecked abs_buf, which panics for directories at MAX_PATH_BYTES - 2 through - 5.

Fix

src/resolver/resolver.rs:

  • load_as_file: return None when path.len() >= MAX_PATH_BYTES. >= rather than > because the .js to .tsx rewrite grows the path by one byte.
  • load_extension: return None when path.len() + ext.len() > MAX_PATH_BYTES.
  • load_as_index_with_browser_remapping: return NotFound when the path plus SEP plus NUL does not fit, and use the existing abs_buf_checked for the "index" join.

Tests

test/js/bun/resolve/resolve-error.test.ts gains a describe with eight cases:

  • a path well past MAX_PATH_BYTES through require.resolve, import(), and Bun.resolveSync (the load_as_file copy),
  • a path at MAX_PATH_BYTES - 4 so only the appended extension overflows (the load_extension slice),
  • a .js path at exactly MAX_PATH_BYTES (pins the >=: weakening it to > fails this test and nothing else),
  • a multibyte basename past MAX_PATH_BYTES,
  • a real directory created at MAX_PATH_BYTES - 1 (the SEP + NUL append),
  • a real directory at MAX_PATH_BYTES - 2 under Bun.build({ target: "browser" }) with a browser map (the "index" join).

Each spawned child aborts with a range end index or index out of bounds panic on current main and exits 0 with MODULE_NOT_FOUND / a ResolveMessage with the fix. The two directory cases skip on Windows, where MAX_PATH_BYTES is far past what the filesystem allows.

Notes

The first revision also tightened an off-by-one in dir_info_cached_maybe_log (> guarding a [..len + 1] slice). #36165 landed the same change on main independently, so that hunk dropped out in the rebase.

The two index-probe sites were found in review after the rebase. The SEP + NUL one needs a directory whose absolute path is exactly one byte under the OS limit, so it is much harder to reach than the load_as_file copy, but it goes through the same public entry points. Boundary sweep on linux after the fix: every length from 4076 to 6145 through require.resolve, and directories at MAX - 1 through MAX - 20 through both import() and browser-target Bun.build, return a normal error.

Also ran test/js/bun/resolve/resolve.test.ts and test/bundler/bundler_browser.test.ts.


no test proof · iteration 2 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/bun/resolve/resolve-error.test.ts