From f32f8bf5aa9c755460d925a98c997c8f08700e10 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 14 Aug 2026 05:33:24 +0000 Subject: [PATCH 1/5] install: report ENAMETOOLONG for a --cwd value that does not fit the path buffer --- .../PackageManager/CommandLineArguments.rs | 79 +++++++++++-------- 1 file changed, 48 insertions(+), 31 deletions(-) diff --git a/src/install/PackageManager/CommandLineArguments.rs b/src/install/PackageManager/CommandLineArguments.rs index 84b223a973fb..32a60be528b5 100644 --- a/src/install/PackageManager/CommandLineArguments.rs +++ b/src/install/PackageManager/CommandLineArguments.rs @@ -1295,37 +1295,8 @@ Full documentation is available at https://bun.com/docs/cli/why. cli.concurrent_scripts = strings::parse_int::(concurrency, 10).ok(); } - if let Some(cwd_) = args.option(b"--cwd") { - let mut buf = PathBuffer::uninit(); - let mut buf2 = PathBuffer::uninit(); - - let final_path: &mut bun_core::ZStr = if !cwd_.is_empty() && cwd_[0] == b'.' { - let cwd_len = bun_sys::getcwd(&mut buf[..])?; - let cwd = &buf[..cwd_len]; - let parts: [&[u8]; 1] = [cwd_]; - let len = Path::resolve_path::join_abs_string_buf::( - cwd, - &mut buf2[..], - &parts, - ) - .len(); - buf2[len] = 0; - bun_core::ZStr::from_buf_mut(&mut buf2[..], len) - } else { - buf[..cwd_.len()].copy_from_slice(cwd_); - buf[cwd_.len()] = 0; - bun_core::ZStr::from_buf_mut(&mut buf[..], cwd_.len()) - }; - if let Err(err) = bun_sys::chdir(final_path) { - Output::err_generic( - "failed to change directory to \"{}\": {}\n", - ( - bstr::BStr::new(final_path.as_bytes()), - bstr::BStr::new(err.name()), - ), - ); - Global::crash(); - } + if let Some(cwd) = args.option(b"--cwd") { + change_directory(cwd)?; } if subcommand == Subcommand::Update { @@ -1449,3 +1420,49 @@ Full documentation is available at https://bun.com/docs/cli/why. Ok(cli) } } + +/// `--cwd`: a value starting with `.` is resolved against the current directory, anything +/// else is handed to `chdir` as given. Exits the process when the directory cannot be entered. +fn change_directory(arg: &[u8]) -> Result<(), crate::Error> { + let mut buf = PathBuffer::uninit(); + let mut buf2 = PathBuffer::uninit(); + let too_long = || bun_sys::Error::from_code(bun_sys::E::ENAMETOOLONG, bun_sys::Tag::chdir); + + // Either buffer keeps its last byte for the NUL terminator `chdir` needs. + let resolved: Result<&bun_core::ZStr, bun_sys::Error> = if arg.first() == Some(&b'.') { + let cwd_len = bun_sys::getcwd(&mut buf[..])?; + let out_len = buf2.len() - 1; + match Path::resolve_path::join_abs_string_buf_checked::( + &buf[..cwd_len], + &mut buf2[..out_len], + &[arg], + ) + .map(|joined| joined.len()) + { + Some(len) => { + buf2[len] = 0; + Ok(bun_core::ZStr::from_buf(&buf2[..], len)) + } + None => Err(too_long()), + } + } else if arg.len() < buf.len() { + buf[..arg.len()].copy_from_slice(arg); + buf[arg.len()] = 0; + Ok(bun_core::ZStr::from_buf(&buf[..], arg.len())) + } else { + Err(too_long()) + }; + + let (path, err) = match resolved { + Ok(path) => match bun_sys::chdir(path) { + Ok(()) => return Ok(()), + Err(err) => (path.as_bytes(), err), + }, + Err(err) => (arg, err), + }; + Output::err_generic( + "failed to change directory to \"{}\": {}\n", + (bstr::BStr::new(path), bstr::BStr::new(err.name())), + ); + Global::crash(); +} From ecc18c9438b536143f839fe11e3d7958069852b7 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 14 Aug 2026 05:40:48 +0000 Subject: [PATCH 2/5] test: cover --cwd values that do not fit the path buffer --- test/cli/install/bun-install.test.ts | 33 ++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/test/cli/install/bun-install.test.ts b/test/cli/install/bun-install.test.ts index e35753d1be20..452e2c3fc6a1 100644 --- a/test/cli/install/bun-install.test.ts +++ b/test/cli/install/bun-install.test.ts @@ -6,6 +6,7 @@ import { bunEnv, bunExe, bunEnv as env, + isLinux, isWindows, joinP, readdirSorted, @@ -10296,3 +10297,35 @@ it.each([ expect(exitCode).not.toBe(0); }); }); + +// `--cwd` is staged in a PATH_MAX-sized buffer (4096 bytes on Linux, 1024 on the other POSIX +// targets) before chdir. Values that did not leave room for the NUL terminator used to abort the +// process; the buffer on Windows (~96 KiB) is larger than any command line, so only POSIX is affected. +describe.concurrent.skipIf(isWindows)("--cwd that does not fit the path buffer", () => { + const PATH_MAX = isLinux ? 4096 : 1024; + const name = (length: number) => Buffer.alloc(length, "a").toString(); + + it.each([ + ["install, PATH_MAX - 1 bytes (rejected by the kernel)", "install", name(PATH_MAX - 1)], + ["install, exactly PATH_MAX bytes", "install", name(PATH_MAX)], + ["install, longer than PATH_MAX", "install", name(PATH_MAX + 1000)], + ["install, ./ prefix longer than PATH_MAX", "install", "./" + name(PATH_MAX + 1000)], + ["add, longer than PATH_MAX", "add", name(PATH_MAX + 1000)], + ])("%s", async (_, subcommand, cwd) => { + using dir = tempDir("install-cwd-too-long", { + "package.json": JSON.stringify({ name: "foo", version: "0.0.1" }), + }); + + await using proc = spawn({ + cmd: [bunExe(), subcommand, "--cwd", cwd, ...(subcommand === "add" ? ["bar"] : [])], + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + env, + }); + const [err, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); + + expect(err).toContain(`failed to change directory to "${cwd}": ENAMETOOLONG`); + expect(exitCode).toBe(1); + }); +}); From 35b9e40de1618685314ddcf52b57eeac93a38590 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 14 Aug 2026 06:03:01 +0000 Subject: [PATCH 3/5] test: use the 4096 byte PATH_MAX on Android too --- test/cli/install/bun-install.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/cli/install/bun-install.test.ts b/test/cli/install/bun-install.test.ts index 452e2c3fc6a1..2167e1808eae 100644 --- a/test/cli/install/bun-install.test.ts +++ b/test/cli/install/bun-install.test.ts @@ -6,6 +6,7 @@ import { bunEnv, bunExe, bunEnv as env, + isAndroid, isLinux, isWindows, joinP, @@ -10298,11 +10299,11 @@ it.each([ }); }); -// `--cwd` is staged in a PATH_MAX-sized buffer (4096 bytes on Linux, 1024 on the other POSIX -// targets) before chdir. Values that did not leave room for the NUL terminator used to abort the +// `--cwd` is staged in a PATH_MAX-sized buffer (4096 bytes on Linux and Android, 1024 on macOS and +// the BSDs) before chdir. Values that did not leave room for the NUL terminator used to abort the // process; the buffer on Windows (~96 KiB) is larger than any command line, so only POSIX is affected. describe.concurrent.skipIf(isWindows)("--cwd that does not fit the path buffer", () => { - const PATH_MAX = isLinux ? 4096 : 1024; + const PATH_MAX = isLinux || isAndroid ? 4096 : 1024; const name = (length: number) => Buffer.alloc(length, "a").toString(); it.each([ From 8c2fecb80f5fa1e9a35d06e169e4539bb66bdf28 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 14 Aug 2026 06:05:16 +0000 Subject: [PATCH 4/5] install: shorten the change_directory doc comment --- src/install/PackageManager/CommandLineArguments.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/install/PackageManager/CommandLineArguments.rs b/src/install/PackageManager/CommandLineArguments.rs index 32a60be528b5..e2b159f25f74 100644 --- a/src/install/PackageManager/CommandLineArguments.rs +++ b/src/install/PackageManager/CommandLineArguments.rs @@ -1421,8 +1421,7 @@ Full documentation is available at https://bun.com/docs/cli/why. } } -/// `--cwd`: a value starting with `.` is resolved against the current directory, anything -/// else is handed to `chdir` as given. Exits the process when the directory cannot be entered. +/// `--cwd`. Exits the process when the directory cannot be entered. fn change_directory(arg: &[u8]) -> Result<(), crate::Error> { let mut buf = PathBuffer::uninit(); let mut buf2 = PathBuffer::uninit(); From eb150a66b8929e5ff362dc1f947d2f489679211a Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 14 Aug 2026 06:22:32 +0000 Subject: [PATCH 5/5] test: drain stdout of the --cwd child processes --- test/cli/install/bun-install.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/cli/install/bun-install.test.ts b/test/cli/install/bun-install.test.ts index 2167e1808eae..74cc0c65677f 100644 --- a/test/cli/install/bun-install.test.ts +++ b/test/cli/install/bun-install.test.ts @@ -10324,9 +10324,10 @@ describe.concurrent.skipIf(isWindows)("--cwd that does not fit the path buffer", stderr: "pipe", env, }); - const [err, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); + const [out, err, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); expect(err).toContain(`failed to change directory to "${cwd}": ENAMETOOLONG`); + expect(out).toBe(""); expect(exitCode).toBe(1); }); });