process: boot from a deleted cwd, throw node's ENOENT from process.cwd() (+4 tests) - #34843
Closed
cirospaciari wants to merge 3 commits into
Closed
process: boot from a deleted cwd, throw node's ENOENT from process.cwd() (+4 tests)#34843cirospaciari wants to merge 3 commits into
cirospaciari wants to merge 3 commits into
Conversation
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).
Collaborator
|
Updated 1:42 PM PT - Jul 20th, 2026
❌ @autofix-ci[bot], your commit 12c23e7 has 1 failures in 🧪 To try this PR locally: bunx bun-pr 34843That installs a local version of the PR into your bun-34843 --bun |
Contributor
|
Found 1 issue this PR may fix:
🤖 Generated with Claude Code |
Member
Author
|
Folded into #34660 (branch merged) as part of PR consolidation — same commits and tests, fewer PRs. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
Root cause:
Arguments.parseand every synthetic entry-path builder (-e, stdin, cron, node-emulation) didgetcwd(...)?→ hard abort. Node'sEnvironment::GetCwdfalls back to dirname(exec_path). Bun now does the same via a bounds-checkedgetcwd_or_exe_dir— deliberately restricted to runtime commands (Auto|Run|RunAsNode):bun install/test/buildfrom a deleted cwd keep the hard error, because an early iteration of this fix silently raninstallagainst 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 vanishesByte-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_cachedCwdwas repopulated on chdir where node clears it, and the Rust side read the resolver'stop_level_dirinstead 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:
writableFromFileSinkbuilt aWriteStream("")whose ctorpath.resolve("")d — sochild_processspawn 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.stderrReported, 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 withexpectWarning-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/testfrom deleted cwd still exit 1; relative--cwdfrom 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-all10/10 targets.Known limitations (flag errored before, so these are strict improvements):
--interactive -eevaluates and exits where node stays in the REPL;-iis not aliased (bun's-i=--install=fallback).