cli: stop aborting on absolute script paths longer than 1024 bytes on Windows

Closed
RO
20d ago
+86-14

Reproduction

Windows only. Run a script by an absolute path longer than 1024 bytes (the file does not need to exist):

bun ("C:\" + ("a" * 1100) + ".js")
panic: range end index 1106 out of range for slice of length 1024
oh no: Bun has crashed. This indicates a bug in Bun, not your code.

Exit code 3. Same with bun run <path>, with forward slashes, and with a drive-less \dir\file.js. A script that does exist at such a path cannot be run at all; a 1000 byte path works fine. Windows allows paths up to 32767 characters, and MAX_PATH_BYTES on Windows is 98302.

Cause

RunCommand::maybe_open_with_bun_js (src/runtime/cli/run_command.rs) handles absolute targets with

let resolved = normalize_string::<false, Windows>(resolved);   // Windows only
if resolved.len() >= MAX_PATH_BYTES { return false; }

normalize_string writes its output into PARSER_BUFFER, a thread-local [u8; 1024] in src/paths/resolve_path.rs, with plain slice indexing, so the length check on the next line never runs for a target of 1024 bytes or more. The target is argv as typed by the user, so nothing bounds it before this point. The Zig version had the same buffer and the same order of operations; this is not a regression.

Fix

Check the length first, then normalize straight into script_name_buf, the PathBuffer the function already owns and opens from. The bound is resolved.len() + 1 >= MAX_PATH_BYTES: normalizing grows a path by at most one byte (a bare \\server\share gets its trailing separator appended; everything else is copied at most once, and ./.. segments and repeated separators only shrink it), and the NUL terminator needs one more byte. A target rejected by that check could not be opened anyway, so it takes the same return false fall-through as before, which ends in the regular "Module not found" error. POSIX keeps its existing copy and >= MAX_PATH_BYTES check unchanged; the normalize call was Windows-only before as well.

PosixToWinNormalizer::resolve_cwd failing (it returns ENAMETOOLONG when a drive-less path plus the cwd root does not fit a PathBuffer, or when getcwd fails) used to panic!("Could not resolve path") in the same spot. It now returns false like every other failure in this probe, so the caller falls through to the normal lookup and error message.

normalize_string is itself normalize_string_buf pointed at the thread-local buffer, so the normalization applied to paths that used to fit is exactly the same; the only change for them is which buffer receives the output.

Verification

Two Windows-only cases added to test/cli/run/run_command.test.ts: an existing script at a ~1270 byte path must run when given with backslashes, forward slashes, without a drive letter, and via bun run; a missing 1106 byte C:\aaa...a.js must print Module not found and exit 1 for both bun <path> and bun run <path>.

On Windows x64 (Server 2019, LongPathsEnabled = 0):

  • USE_SYSTEM_BUN=1 bun test test/cli/run/run_command.test.ts (canary at da3851e57): both new tests fail, every child aborts with panic: range end index N out of range for slice of length 1024.
  • bun bd test test/cli/run/run_command.test.ts: 5 pass.
  • Manually with the debug build: existing scripts at 422 and 1227 byte paths run in all four spellings; C:\, \\localhost\c$ and missing files in each spelling report Module not found and exit 1.

On Linux the new tests skip; bun bd test test/cli/run/run_command.test.ts passes and bun <existing abs>, bun <missing abs>, bun /tmp behave as before.

The fall-through for over-long paths on POSIX (where MAX_PATH_BYTES is 1024 or 4096) still aborts inside the resolver's load_as_file; that is a separate buffer and is fixed by #35857.