Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
6 changes: 5 additions & 1 deletion src/runtime/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -913,7 +913,11 @@ pub mod command {
};

if is_bun_x(argv0) {
if let Some(next) = argv.get(1) {
// BUN_OPTIONS tokens are spliced in right after argv[0], so the
// "add"/"exec" the parent bunx spawned its install child with sits
// at `1 + bun_options_argc()` — checking argv[1] made the escape
// hatch below miss it and bunx re-spawn itself forever (#39377).
if let Some(next) = argv.get(1 + bun::bun_options_argc()) {
let next_bytes = next.as_bytes();
if next_bytes == b"add"
&& bun_core::env_var::feature_flag::BUN_INTERNAL_BUNX_INSTALL.get()
Expand Down
39 changes: 39 additions & 0 deletions test/cli/install/bunx.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,45 @@ it.concurrent("should handle postinstall scripts correctly with symlinked bunx",
expect(exited).toBe(0);
});

// Regression test for #39377: BUN_OPTIONS tokens spliced after argv[0] used to
// defeat the BUN_INTERNAL_BUNX_INSTALL escape hatch's positional check, so the
// install child re-entered bunx mode and re-spawned itself forever. Before the
// fix this test hangs instead of completing.
it.concurrent(
"bunx does not fork-bomb when BUN_OPTIONS is set",
async () => {
const { x_dir, env } = setup();
// The bug only triggers when argv[0] is "bunx": the `bun x` spelling
// dispatches through the generic flag-skipping path and is immune.
copyFileSync(bunExe(), join(x_dir, isWindows ? "bun.exe" : "bun"));
copyFileSync(bunExe(), join(x_dir, isWindows ? "bunx.exe" : "bunx"));

const subprocess = spawn({
cmd: ["bunx", "esbuild@latest", "--version"],
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
cwd: x_dir,
stdout: "pipe",
stdin: "inherit",
stderr: "pipe",
env: {
...env,
BUN_OPTIONS: "--smol",
PATH: `${x_dir}${isWindows ? ";" : ":"}${env.PATH || ""}`,
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
});

const [err, out, exited] = await Promise.all([
subprocess.stderr.text(),
subprocess.stdout.text(),
subprocess.exited,
]);

expect(err).not.toContain("error:");
expect(out.trim()).not.toContain(Bun.version);
expect(exited).toBe(0);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
},
1000 * 60 * 2,
);

// Pinned to 20: its engines are "^20.19.0 || ^22.12.0 || >=24.0.0", so the node-24
// requirement this test exercises holds no matter what Node.js version Bun reports.
// @latest tracks Angular's engines upward and breaks whenever they outrun us.
Expand Down