diff --git a/src/bun_core/error.rs b/src/bun_core/error.rs index 34e2a6572d12..b4cd3f1e4ccd 100644 --- a/src/bun_core/error.rs +++ b/src/bun_core/error.rs @@ -25,6 +25,8 @@ pub enum Error { AccessDenied, #[error("WriteFailed")] WriteFailed, + #[error("CurrentWorkingDirectoryUnlinked")] + CurrentWorkingDirectoryUnlinked, #[error(transparent)] Alloc(#[from] bun_alloc::AllocError), } @@ -45,6 +47,7 @@ impl Error { Self::FileNotFound => "FileNotFound", Self::AccessDenied => "AccessDenied", Self::WriteFailed => "WriteFailed", + Self::CurrentWorkingDirectoryUnlinked => "CurrentWorkingDirectoryUnlinked", Self::Alloc(_) => "OutOfMemory", } } diff --git a/src/bun_core/util.rs b/src/bun_core/util.rs index 1e90288ed229..3908c9d40229 100644 --- a/src/bun_core/util.rs +++ b/src/bun_core/util.rs @@ -4165,6 +4165,9 @@ fn getcwd_len(buf: &mut PathBuffer) -> crate::CrateResult { unsafe { let p = libc::getcwd(buf.0.as_mut_ptr().cast(), buf.0.len()); if p.is_null() { + if crate::ffi::errno() == libc::ENOENT { + return Err(crate::CrateError::CurrentWorkingDirectoryUnlinked); + } return Err(crate::CrateError::Unexpected); } Ok(libc::strlen(p)) diff --git a/src/resolver/lib.rs b/src/resolver/lib.rs index 23bed02537d5..3d3c7f6ceecf 100644 --- a/src/resolver/lib.rs +++ b/src/resolver/lib.rs @@ -282,8 +282,7 @@ pub mod fs { Some(d) => DirnameStore::instance().append_slice(d)?, None => { let mut buf = bun_paths::PathBuffer::default(); - let n = bun_sys::getcwd(&mut buf[..])?; - DirnameStore::instance().append_slice(&buf[..n])? + DirnameStore::instance().append_slice(bun_core::getcwd(&mut buf)?.as_bytes())? } }; // Seed the lower-tier `bun_paths::fs::FileSystem` singleton with the diff --git a/src/runtime/cli/Arguments.rs b/src/runtime/cli/Arguments.rs index 486f92a04d5b..ab0fc39e9b37 100644 --- a/src/runtime/cli/Arguments.rs +++ b/src/runtime/cli/Arguments.rs @@ -834,8 +834,7 @@ pub(crate) fn parse(cmd: CommandTag, ctx: Context<'_>) -> crate::Result(base, cwd_arg); // `chdir` wants a NUL-terminated path; `join_abs` returns a borrowed @@ -869,8 +868,7 @@ pub(crate) fn parse(cmd: CommandTag, ctx: Context<'_>) -> crate::Result::from(&temp[..len]) + Box::<[u8]>::from(bun_core::getcwd(&mut temp)?.as_bytes()) }; // Not gated on .BunxCommand: bunx skips Arguments.parse entirely diff --git a/test/bundler/bun-build-compile.test.ts b/test/bundler/bun-build-compile.test.ts index 3020baa43de3..25f16b630c86 100644 --- a/test/bundler/bun-build-compile.test.ts +++ b/test/bundler/bun-build-compile.test.ts @@ -758,10 +758,8 @@ describe("compiled binary in a deleted cwd", () => { }); const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - // The entry never runs (VM init aborts first), the ENOENT surfaces, and the - // process exits 1 — a crash would terminate via a signal, never exit 1. expect(stdout).toBe(""); - expect(stderr).toContain("ENOENT"); + expect(stderr).toContain("The current working directory was deleted"); expect(exitCode).toBe(1); }, 60_000, diff --git a/test/cli/run/run-crash-handler.test.ts b/test/cli/run/run-crash-handler.test.ts index af71635faddf..4d436bbedc7b 100644 --- a/test/cli/run/run-crash-handler.test.ts +++ b/test/cli/run/run-crash-handler.test.ts @@ -129,6 +129,46 @@ describe.if(isPosix)("terminal signal reflects the crash cause", () => { }); }); +// POSIX-only: Windows refuses to remove a directory that is any process's cwd. +describe.if(isPosix)("cwd deleted before startup", () => { + test.concurrent.each(["install", "test"])("bun %s prints the cwd-deleted hint", async cmd => { + using dir = tempDir("cwd-unlinked", {}); + const gone = String(dir); + + await using proc = Bun.spawn({ + cmd: ["/bin/sh", "-c", `cd "${gone}" && rmdir "${gone}" && exec "${bunExe()}" '${cmd}'`], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect({ stdout, stderr, exitCode }).toEqual({ + stdout: "", + stderr: expect.stringContaining("The current working directory was deleted"), + exitCode: 1, + }); + expect(stderr).not.toContain("Bun could not find a file"); + }); + + test.concurrent("bun -e boots via the exe-dir fallback instead", async () => { + using dir = tempDir("cwd-unlinked-run", {}); + const gone = String(dir); + + await using proc = Bun.spawn({ + cmd: ["/bin/sh", "-c", `cd "${gone}" && rmdir "${gone}" && exec "${bunExe()}" -e 'console.log(1)'`], + env: bunEnv, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stdout).toBe("1\n"); + expect(stderr).toBe(""); + expect(exitCode).toBe(0); + }); +}); + // Windows: the VEH handler must walk the stack from the fault CONTEXT record // (RtlVirtualUnwind), not from inside the handler. When the fault is in an // external DLL the old RtlCaptureStackBackTrace path could stop at