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); + }); +});