cli: refuse to start when the cwd is longer than PATH_MAX instead of using the executable's directory

RO
2mo ago
+159-43

Problem

  • bun x.cjs started from a working directory whose absolute path is longer than PATH_MAX (4096 bytes on Linux, 1024 on macOS) runs <directory containing the bun binary>/x.cjs and exits 0; the x.cjs in the real cwd is ignored. With -e, require("./mod.cjs"), path.resolve() and Bun.main all point at the binary's directory while fs.readFileSync("mod.cjs") reads the real cwd. No warning is printed.
  • Cause: bun_core::getcwd_or_exe_dir (src/bun_core/util.rs:4140) substituted the executable's directory on every getcwd failure, and Arguments::parse (src/runtime/cli/Arguments.rs:867) seeded top_level_dir for run/auto commands from it. The fallback exists for a cwd that was deleted (ENOENT), but getcwd also fails with ERANGE (glibc, macOS) or ENAMETOOLONG (musl) when the cwd's path does not fit in the 4096-byte buffer, and that directory still exists.
  • Node refuses to start in that situation (ERANGE: process.cwd failed with error result too large, uv_cwd, exit 1). Bun's non-runtime commands already refused too, but with error: An internal error occurred (ERANGE).

Fix

  • getcwd_or_exe_dir falls back to the executable's directory only for ENOENT and returns every other failure; getcwd_len now maps errno instead of collapsing everything to Unexpected. The [eval]/[stdin]/cron/feedback callers in run_command.rs propagate the error with ?.
  • Arguments::parse does the same for the startup cwd: ENOENT on a run/auto/node command keeps the exe-dir fallback (bun_core::self_exe_dir, the old fallback body split out); anything else, and a relative --cwd without a usable base, prints ERANGE: Numerical result out of range: Could not get the current working directory (getcwd) and exits 1. This is the same error path --cwd already uses for a failed chdir, so bun test/bun build get the readable message as well.
  • Correct because the substituted directory is never what the user's relative paths mean: a deleted cwd has no files for them to refer to (and Node boots there too, which test/js/node/test/parallel/test-cwd-enoent*.js pin), while an over-long cwd does, so the only safe answer is to refuse.
  • Verified with test/cli/run/run_command.test.ts: five bun ... invocations (-e, x.cjs, run x.cjs, --cwd . x.cjs, test) from a cwd longer than PATH_MAX must print that error and exit 1; they fail on the current release build (-e exits 0 printing the exe dir, x.cjs and run x.cjs print Module not found "x.cjs", --cwd . and test print the internal-error line) and pass with this change. A sixth test pins that bun -e still boots from a deleted cwd and process.cwd() throws ENOENT there; it passes before and after.
  • Also ran the four test-cwd-enoent*.js node tests, test/cli/run/run-eval.test.ts and test/cli/run/as-node.test.ts against the debug build; all pass. The original repro (bun hardlinked next to a planted x.cjs, cwd of 4300 bytes) now prints the error instead of running the planted file, and a 4000-byte cwd still runs the real file.

Background

  • PATH_MAX bounds the length of a path passed to a single syscall, not how deep a process can be: chdir one component at a time works indefinitely, but getcwd has to return the whole path and fails once it does not fit. Bun's PathBuffer is exactly PATH_MAX bytes, so such a cwd cannot be represented anywhere in the resolver, and startup is the only place to reject it.
  • top_level_dir is the directory the runtime resolves the entry point, relative require/import and path.resolve() against. It is captured once at startup; process.cwd() does a fresh getcwd on every call, which is why it reported ERANGE while everything else used the exe dir.
  • The exe-dir fallback mirrors Node's Environment::GetCwd, which substitutes the executable's directory when uv_cwd fails so that node -e, -r and the REPL still work after the shell's cwd is deleted; Node's own relative-script path fails regardless because path.resolve calls process.cwd().
Not changed here
  • bun install / bun pm / bunx do not go through this block; they already refuse an over-long cwd via FileSystem::init(None) with the generic An internal error occurred (ERANGE) line, unchanged.
  • A deleted cwd plus a relative script (cd dir; rmdir dir; bun x.cjs) still resolves against the executable's directory, as before: that is the ENOENT fallback working as designed. Node errors there instead; tightening that is a separate decision.
  • A cwd within a few bytes of PATH_MAX (4084..4095 on Linux) panics earlier in bunfig loading, and process.chdir() into a directory whose path is exactly PATH_MAX-1 bytes panics in set_process_cwd; both are tracked separately.
  • #32357 (stale) maps the same ENOENT to a dedicated error variant for the generic root-error message; if it is revived, its mapping and the FileNotFound match here need to agree.

no test proof · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/cli/run/run_command.test.ts