diff --git a/src/bun.js/bindings/c-bindings.cpp b/src/bun.js/bindings/c-bindings.cpp
index 71c3e46c79f6..bcb8dbcb8b44 100644
--- a/src/bun.js/bindings/c-bindings.cpp
+++ b/src/bun.js/bindings/c-bindings.cpp
@@ -473,19 +473,25 @@ extern "C" void bun_initialize_process()
} while (devNullFd_ < 0 and errno == EINTR);
};
+ if (devNullFd_ < 0) {
+ // open("/dev/null") failed (e.g., in macOS App Sandbox).
+ // Continue without redirecting; this is best-effort.
+ return;
+ }
+
if (devNullFd_ == target_fd) {
devNullFd_ = -1;
return;
}
- ASSERT(devNullFd_ != -1);
int err;
do {
err = dup2(devNullFd_, target_fd);
} while (err < 0 && errno == EINTR);
- if (err != 0) [[unlikely]] {
- abort();
+ // dup2 returns the new fd on success (not 0), or -1 on error.
+ if (err < 0) [[unlikely]] {
+ bun_is_stdio_null[target_fd] = 0;
}
};
@@ -497,7 +503,6 @@ extern "C" void bun_initialize_process()
setDevNullFd(fd);
}
} else {
- bun_stdio_tty[fd] = 1;
int err = 0;
do {
@@ -505,6 +510,11 @@ extern "C" void bun_initialize_process()
} while (err == -1 && errno == EINTR);
if (err == 0) [[likely]] {
+ // Only mark as TTY if we successfully captured termios state.
+ // In macOS App Sandbox, tcgetattr fails with EPERM even though
+ // isatty() returns true. We must not try to restore state we
+ // never captured.
+ bun_stdio_tty[fd] = 1;
anyTTYs = true;
}
}
diff --git a/test/js/bun/test-macos-app-sandbox.test.ts b/test/js/bun/test-macos-app-sandbox.test.ts
new file mode 100644
index 000000000000..05bb880b862e
--- /dev/null
+++ b/test/js/bun/test-macos-app-sandbox.test.ts
@@ -0,0 +1,122 @@
+import { describe, expect, test } from "bun:test";
+import { copyFileSync } from "fs";
+import { bunEnv, bunExe, isMacOS, tempDir } from "harness";
+import { join } from "path";
+
+// Match Bun's own entitlements from entitlements.plist, plus app-sandbox.
+const entitlementsPlist = `
+
+
+
+ com.apple.security.app-sandbox
+
+ com.apple.security.cs.allow-jit
+
+ com.apple.security.cs.allow-unsigned-executable-memory
+
+ com.apple.security.cs.disable-executable-page-protection
+
+ com.apple.security.cs.allow-dyld-environment-variables
+
+ com.apple.security.cs.disable-library-validation
+
+ com.apple.security.network.client
+
+
+`;
+
+function makeInfoPlist(bundleId: string) {
+ return `
+
+
+
+ CFBundleExecutable
+ bun
+ CFBundleIdentifier
+ ${bundleId}
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ bun_sandboxed
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 1.0
+ CFBundleSupportedPlatforms
+
+ MacOSX
+
+ CFBundleVersion
+ 1
+
+`;
+}
+
+function createSandboxedApp(prefix: string, bundleId: string) {
+ const dir = tempDir(prefix, {
+ "entitlements.plist": entitlementsPlist,
+ "bun_sandboxed.app": {
+ "Contents": {
+ "Info.plist": makeInfoPlist(bundleId),
+ "MacOS": {},
+ },
+ },
+ });
+
+ const bunPath = join(String(dir), "bun_sandboxed.app", "Contents", "MacOS", "bun");
+ const appBundlePath = join(String(dir), "bun_sandboxed.app");
+ const entitlementsPath = join(String(dir), "entitlements.plist");
+
+ copyFileSync(bunExe(), bunPath);
+
+ const codesignResult = Bun.spawnSync({
+ cmd: ["/usr/bin/codesign", "--entitlements", entitlementsPath, "--force", "-s", "-", appBundlePath],
+ env: bunEnv,
+ stderr: "inherit",
+ });
+ expect(codesignResult.exitCode).toBe(0);
+
+ return { dir, bunPath, bundleId };
+}
+
+// Modeled after Node.js's test/parallel/test-macos-app-sandbox.js
+describe.skipIf(!isMacOS)("macOS App Sandbox", () => {
+ test("bun can execute JavaScript inside the app sandbox", async () => {
+ const { dir, bunPath } = createSandboxedApp("macos-sandbox-test", "dev.bun.test.sandbox_exec");
+ using _dir = dir;
+
+ await using proc = Bun.spawn({
+ cmd: [bunPath, "-e", "console.log('hello sandbox')"],
+ env: bunEnv,
+ stdout: "pipe",
+ stderr: "inherit",
+ });
+
+ const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
+
+ expect(stdout.trim()).toBe("hello sandbox");
+ expect(exitCode).toBe(0);
+ });
+
+ test("sandboxed bun runs inside the sandbox container", async () => {
+ const { dir, bunPath, bundleId } = createSandboxedApp(
+ "macos-sandbox-test-container",
+ "dev.bun.test.sandbox_container",
+ );
+ using _dir = dir;
+
+ // When running inside a macOS App Sandbox, os.homedir() should return
+ // the sandbox container path, not the real home directory.
+ await using proc = Bun.spawn({
+ cmd: [bunPath, "-e", "console.log(require('os').homedir())"],
+ env: bunEnv,
+ stdout: "pipe",
+ stderr: "inherit",
+ });
+
+ const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
+
+ expect(stdout.trim()).toContain(`Library/Containers/${bundleId}`);
+ expect(exitCode).toBe(0);
+ });
+});