Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/bun_core/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4355,7 +4355,14 @@ pub fn getcwd(buf: &mut PathBuffer) -> Result<&ZStr, crate::Error> {
unsafe {
let p = libc::getcwd(buf.0.as_mut_ptr().cast(), buf.0.len());
if p.is_null() {
return Err(std::io::Error::last_os_error().into());
let e = std::io::Error::last_os_error();
// Mirror Zig's `std.posix.getcwd`: ENOENT means the cwd was
// unlinked. `crash_handler::handle_root_error` matches on this
// name to print an actionable hint.
if e.raw_os_error() == Some(libc::ENOENT) {
return Err(crate::err!("CurrentWorkingDirectoryUnlinked"));
}
return Err(e.into());
}
let len = libc::strlen(p);
Ok(ZStr::from_buf(&buf.0, len))
Expand Down
7 changes: 7 additions & 0 deletions src/sys/Error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,13 @@ impl Error {
}

pub fn to_zig_err(&self) -> bun_core::Error {
// Zig's `std.posix.getcwd` maps ENOENT to the named error
// `CurrentWorkingDirectoryUnlinked`; preserve that name so
// `crash_handler::handle_root_error` can print the actionable hint
// instead of falling through to the generic ENOENT message.
if self.syscall == Tag::getcwd && self.get_errno() == E::ENOENT {
return bun_core::err!("CurrentWorkingDirectoryUnlinked");
}
errno_to_err(self.errno)
}

Expand Down
6 changes: 4 additions & 2 deletions src/sys/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ pub use error::ReturnCodeExt;
impl From<Error> for bun_core::Error {
#[inline]
fn from(e: Error) -> bun_core::Error {
// Encode as the errno's name (e.g., "ENOENT") in the interned table.
bun_core::Error::from_errno(e.errno as i32)
// Route through `to_zig_err` so syscall-specific errno→name mappings
// (e.g. getcwd+ENOENT → CurrentWorkingDirectoryUnlinked) apply to `?`
// conversions as well.
e.to_zig_err()
}
}
/// The JS-facing rich error
Expand Down
7 changes: 4 additions & 3 deletions test/bundler/bun-build-compile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -568,10 +568,11 @@ 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.
// The entry never runs (VM init aborts first), the getcwd ENOENT surfaces
// as the cwd-deleted hint, 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,
Expand Down
40 changes: 39 additions & 1 deletion test/cli/run/run-crash-handler.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { crash_handler } from "bun:internal-for-testing";
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, isDebug, isLinux, isPosix, mergeWindowEnvs } from "harness";
import { bunEnv, bunExe, isDebug, isLinux, isPosix, mergeWindowEnvs, tempDir } from "harness";
import path from "path";
const { getMachOImageZeroOffset } = crash_handler;

Expand Down Expand Up @@ -83,6 +83,44 @@ test.if(isPosix)(
20_000,
);

// `handle_root_error` recognizes the named error `CurrentWorkingDirectoryUnlinked`
// and prints an actionable hint. Zig's stdlib mapped `getcwd` ENOENT to that name;
// the Rust `bun_sys::Error` → `bun_core::Error` conversion dropped the syscall tag
// and emitted a bare "ENOENT", so the hint was unreachable and users saw the
// generic "Bun could not find a file" fallback instead.
//
// POSIX-only: Windows refuses to remove a directory that is any process's cwd.
describe.if(isPosix)("cwd deleted before startup", () => {
for (const cmd of [
["-e", "console.log(1)"],
["run", "foo"],
]) {
test(`bun ${cmd[0]} prints the cwd-deleted hint`, async () => {
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.map(a => `'${a}'`).join(" ")}`,
],
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.if(process.platform === "darwin")("macOS has the assumed image offset", () => {
// If this fails, then https://bun.report will be incorrect and the stack
// trace remappings will stop working.
Expand Down
Loading