Skip to content

process: boot from a deleted cwd, throw node's ENOENT from process.cwd() (+4 tests) - #34843

Closed
cirospaciari wants to merge 3 commits into
mainfrom
claude/process-cwd-warn
Closed

process: boot from a deleted cwd, throw node's ENOENT from process.cwd() (+4 tests)#34843
cirospaciari wants to merge 3 commits into
mainfrom
claude/process-cwd-warn

Conversation

@cirospaciari

Copy link
Copy Markdown
Member

Bun could not start from a deleted working directory, and process.cwd() served a cached value forever after the directory vanished. Node boots fine and throws its uv_cwd ENOENT on the call. Two signed commits.

Startup from a deleted cwd

mkdir d && cd d && rmdir ../d
node -e '0'   # exit 0
bun  -e '0'   # was: "ENOENT: Bun could not find a file...", exit 1 — now exit 0

Root cause: Arguments.parse and every synthetic entry-path builder (-e, stdin, cron, node-emulation) did getcwd(...)? → hard abort. Node's Environment::GetCwd falls back to dirname(exec_path). Bun now does the same via a bounds-checked getcwd_or_exe_dirdeliberately restricted to runtime commands (Auto|Run|RunAsNode): bun install/test/build from a deleted cwd keep the hard error, because an early iteration of this fix silently ran install against the repo found above the executable — caught by re-driving the CLI, and exactly the failure a fallback must not create.

--interactive (bare) now routes to the REPL as node does; with a script, the script wins (node's behavior, checked on the binary).

process.cwd() after the cwd vanishes

Byte-identical to node v26.3.0: message, code: 'ENOENT', errno: -2, syscall: 'uv_cwd', including the "was likely removed without changing the working directory" hint. Root cause was two caches: the C++ m_cachedCwd was repopulated on chdir where node clears it, and the Rust side read the resolver's top_level_dir instead of the OS.

Perf, measured rather than assumed: hot path unchanged — 2M cached calls: 8.1-8.7ms control vs 8.8-8.9ms fixed (~4.4ns/call both). Only the first call after startup/chdir pays one syscall, node's exact cost model.

Ride-along fix this exposed: writableFromFileSink built a WriteStream("") whose ctor path.resolve("")d — so child_process spawn from a deleted cwd threw. The internal fast path now skips resolution of a path it discards two lines later.

Deliberately skipped: warning printer through process.stderr

Reported, not attempted: (a) bun's default warning printer is the native console inspector and there is no in-tree "format to string" hook, so formatting cannot be preserved through process.stderr.write; (b) node's default handler is a bootstrap-registered 'warning' listener that prints even when user listeners exist — flipping bun to that silently changes stderr for every program with expectWarning-style listeners. Needs its own PR with node's format adopted deliberately.

Verification

4 tests byte-identical to upstream, 3× green, failing on unfixed bun, 12 tampers all non-zero. Adversarial probes: bun install/add/test from deleted cwd still exit 1; relative --cwd from deleted cwd still errors; --cwd <symlink> now yields consistent physical paths; repeated chdir→rmdir→cwd() throws every iteration; cache identity preserved.

Regressions: fs 606/0, path 122/0, fs+path+child_process+run-eval 869 pass / 1 pre-existing network fail; process 209 pass with the control binary failing the same 3 plus 2 more; bun run rust:check-all 10/10 targets.

Known limitations (flag errored before, so these are strict improvements): --interactive -e evaluates and exits where node stays in the REPL; -i is not aliased (bun's -i = --install=fallback).

A getcwd failure at startup aborted every entry point with a generic
ENOENT. Node falls back to the executable's directory
(Environment::GetCwd in env.cc) and lets process.cwd() surface the
error later.

- add bun_core::getcwd_or_exe_dir and use it for the synthetic entry
  paths (-e/-p, stdin, cron, node-emulation eval and relative scripts,
  feedback) and for the parsed startup cwd of runtime commands only;
  install/test/build keep the hard error so they never act on a tree
  found above the executable
- an absolute --cwd no longer needs a live cwd; a relative one still
  errors; the stored cwd is the post-chdir physical path so
  process.cwd(), path.resolve, and the resolver agree
- route a bare --interactive to the REPL like node's --interactive

Enables test-cwd-enoent{,-preload,-repl}.js (verbatim from Node
v26.3.0).
process.cwd() returned the resolver's cached top_level_dir, so it kept
returning a stale path after the directory was rmdir'ed. Node re-runs
uv_cwd on every cache miss and clears its cache on chdir
(lib/internal/bootstrap/switches/does_own_process_state.js).

- Bun__Process__getCwd now does a real getcwd and on failure throws
  Node's uv_cwd UVException (message, code, errno, syscall verified
  byte-identical against node v26.3.0)
- Process_functionChdir clears the cached cwd instead of repopulating
  it, so the next process.cwd() re-queries the OS
- WriteStream's internal $fastPath no longer path.resolve()s its
  discarded path, so child_process spawn keeps working from a deleted
  cwd (its stdin wiring constructs WriteStream(""))
- drop the now-unused node::path get_cwd alias

The hot path is unchanged: the cached JSString still serves repeated
process.cwd() calls; only the first call after startup or chdir pays
the syscall.

Enables test-cwd-enoent-improved-message.js (verbatim from Node
v26.3.0).
@robobun

robobun commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator
Updated 1:42 PM PT - Jul 20th, 2026

@autofix-ci[bot], your commit 12c23e7 has 1 failures in Build #76353 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 34843

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

bun-34843 --bun

@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. process.chdir throws ENOENT getcwd after current working directory is deleted #32409 - process.chdir throws ENOENT from internal getcwd after the cwd is deleted; this PR's chdir cache fix (clearing m_cachedCwd instead of repopulating it) avoids the failing getcwd syscall

If this is helpful, copy the block below into the PR description to auto-close this issue on merge.

Fixes #32409

🤖 Generated with Claude Code

@cirospaciari

Copy link
Copy Markdown
Member Author

Folded into #34660 (branch merged) as part of PR consolidation — same commits and tests, fewer PRs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants