Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
2b4d2888b7 fix(spawn): add memory barrier to fix race condition in exec failure detection
When using vfork() on Linux, the child and parent share the same address
space. The child sets a volatile int child_errno before calling rawExit(),
but without a memory barrier, the CPU store buffer may not be flushed
before the parent resumes and reads the value.

This race condition causes exec failures (like EACCES from noexec mounts)
to be silently ignored - the spawn appears to succeed (returns 0), then
the process exits with code 127, resulting in confusing error messages
or no error at all.

Add __sync_synchronize() memory barrier before rawExit() to ensure the
errno write is visible to the parent process.

Fixes #25825

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 11:27:39 +00:00
2 changed files with 103 additions and 0 deletions

View File

@@ -173,6 +173,11 @@ extern "C" ssize_t posix_spawn_bun(
// the error directly via a volatile variable. The parent will see this
// value after we call _exit().
child_errno = errno;
// Memory barrier to ensure the write to child_errno is visible to the
// parent before we exit. Without this, the CPU store buffer may not be
// flushed, causing the parent to read 0 (success) instead of the actual
// error. This fixes silent failures when exec fails (e.g., noexec mount).
__sync_synchronize();
rawExit(127);
// should never be reached

View File

@@ -0,0 +1,98 @@
import { describe, expect, test } from "bun:test";
import { chmodSync } from "fs";
import { bunEnv, isWindows, tempDir } from "harness";
import { join } from "path";
// https://github.com/oven-sh/bun/issues/25825
// Bug: When exec fails with EACCES (e.g., noexec mount or no execute permission),
// the spawn would silently fail with exit code 127 but return success (no error thrown).
// This was caused by a race condition in the vfork() path on Linux where the
// child's write to child_errno wasn't visible to the parent due to missing memory barrier.
describe("spawn exec failure should report EACCES error", () => {
test.skipIf(isWindows)("spawning a non-executable script should throw EACCES", async () => {
using dir = tempDir("issue-25825", {
// Create a script file (we'll remove execute permissions)
"script.sh": `#!/bin/sh\necho "hello"`,
});
const scriptPath = join(String(dir), "script.sh");
// Remove execute permissions to simulate EACCES scenario
chmodSync(scriptPath, 0o644);
// This should throw an error, not silently fail
let error: Error | undefined;
try {
await using proc = Bun.spawn({
cmd: [scriptPath],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
await proc.exited;
} catch (e) {
error = e as Error;
}
expect(error).toBeDefined();
expect(error!.message).toContain("EACCES");
});
test.skipIf(isWindows)("spawning a non-executable file via Bun.spawnSync should report EACCES", () => {
using dir = tempDir("issue-25825-sync", {
"script.sh": `#!/bin/sh\necho "hello"`,
});
const scriptPath = join(String(dir), "script.sh");
chmodSync(scriptPath, 0o644);
let error: Error | undefined;
try {
Bun.spawnSync({
cmd: [scriptPath],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
} catch (e) {
error = e as Error;
}
expect(error).toBeDefined();
expect(error!.message).toContain("EACCES");
});
test.skipIf(isWindows)("spawning via child_process.spawn should emit error for non-executable", async () => {
const { spawn } = await import("child_process");
using dir = tempDir("issue-25825-child-process", {
"script.sh": `#!/bin/sh\necho "hello"`,
});
const scriptPath = join(String(dir), "script.sh");
chmodSync(scriptPath, 0o644);
const { promise, resolve } = Promise.withResolvers<{ error?: Error; code?: number | null }>();
const child = spawn(scriptPath, [], {
env: bunEnv,
stdio: ["pipe", "pipe", "pipe"],
});
let errorEmitted: Error | undefined;
child.on("error", err => {
errorEmitted = err;
resolve({ error: err });
});
child.on("exit", code => {
resolve({ code });
});
const result = await promise;
// Should emit an error event with EACCES, not silently exit
expect(result.error).toBeDefined();
expect(result.error!.message).toContain("EACCES");
});
});