Compare commits

...

6 Commits

Author SHA1 Message Date
Jarred Sumner
20e26a775b fix(test): add missing entitlements for macOS App Sandbox test
Add `network.client` and `allow-dyld-environment-variables` entitlements
to match Bun's own entitlements. The `network.client` entitlement is
needed because Bun.spawn uses socketpair(AF_UNIX) for stdio pipes,
which the App Sandbox blocks without it.

Also fix the second test to verify os.homedir() returns the sandbox
container path, and clean up the test structure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 20:20:49 -08:00
autofix-ci[bot]
17f5be80f2 [autofix.ci] apply automated fixes 2026-02-15 08:51:04 +00:00
Jarred Sumner
f8efc63e2e Update test-macos-app-sandbox.test.ts 2026-02-15 00:49:20 -08:00
autofix-ci[bot]
e74a65ee22 [autofix.ci] apply automated fixes 2026-02-15 05:40:38 +00:00
Jarred Sumner
8a61d6dae2 Cleanup 2026-02-14 21:38:49 -08:00
Claude Bot
1af10953b7 fix(process): support running Bun inside macOS App Sandbox
Fix two issues in `bun_initialize_process()` that prevent Bun from
running inside a macOS App Sandbox (`com.apple.security.app-sandbox`):

1. Move `bun_stdio_tty[fd] = 1` inside the `tcgetattr` success check.
   In the macOS App Sandbox, `tcgetattr` fails with EPERM even though
   `isatty()` returns true. Previously, the TTY flag was set
   unconditionally, causing `bun_restore_stdio()` to call `tcsetattr`
   with uninitialized termios state at exit. This is the same class of
   bug Node.js fixed in nodejs/node#33944.

2. Fix `dup2` return value check: `dup2(oldfd, newfd)` returns `newfd`
   on success (not 0), so `err != 0` incorrectly triggered `abort()`
   for stdout/stderr. Changed to `err < 0` and replaced `abort()` with
   a graceful fallback. Also handle `open("/dev/null")` failure
   gracefully for restricted environments.

Closes #15661

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-14 23:54:27 +00:00
2 changed files with 136 additions and 4 deletions

View File

@@ -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;
}
}

View File

@@ -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 = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-executable-page-protection</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>`;
function makeInfoPlist(bundleId: string) {
return `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>bun</string>
<key>CFBundleIdentifier</key>
<string>${bundleId}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>bun_sandboxed</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>`;
}
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);
});
});