fix(process): support running Bun inside macOS App Sandbox#27041
fix(process): support running Bun inside macOS App Sandbox#27041
Conversation
|
Updated 11:15 PM PT - Feb 15th, 2026
❌ @Jarred-Sumner, your commit 20e26a7 has 3 failures in
🧪 To try this PR locally: bunx bun-pr 27041That installs a local version of the PR into your bun-27041 --bun |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 WalkthroughC bindings modified to avoid aborts on /dev/null redirection failures and to set TTY state only when captured; sandbox-related comments added. A new macOS-only App Sandbox test was added that builds and codesigns a faux Changes
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/bun.js/bindings/c-bindings.cpp (1)
468-495:⚠️ Potential issue | 🟡 MinorClear
bun_is_stdio_nullwhen/dev/nullcan’t be openedOn the open failure path,
bun_is_stdio_null[target_fd]remains 1 even though no redirection happened. That can mislead downstream logic that assumes stdio is valid. Consider clearing it before returning.Proposed fix
- if (devNullFd_ < 0) { + if (devNullFd_ < 0) { + bun_is_stdio_null[target_fd] = 0; // open("/dev/null") failed (e.g., in macOS App Sandbox). // Continue without redirecting; this is best-effort. return; }
🤖 Fix all issues with AI agents
In `@test/js/bun/test-macos-app-sandbox.test.ts`:
- Around line 173-182: Modify the spawned script so it emits a unique marker
before calling require('fs').readdirSync (e.g., console.log("SANDBOX_START"))
and then assert that the marker appears in result.stdout (or result.stderr) to
confirm the process started successfully; keep the existing check that
result.exitCode is non-zero to ensure failure came from the sandboxed filesystem
access. Update the Bun.spawnSync cmd string (where homedir, bunPath, bunEnv are
used) to print the marker before readdirSync and add an assertion on
result.stdout.includes("SANDBOX_START") (or stderr) prior to
expect(result.exitCode).not.toBe(0).
Code ReviewNewest first ✅ 3d0fa — Looks good! Reviewed 2 files across Powered by Claude Code Review |
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>
3d0fae4 to
1af1095
Compare
Code ReviewNewest first 🟡 1af10 — 1 issue found
Powered by Claude Code Review |
| if (devNullFd_ < 0) { | ||
| // open("/dev/null") failed (e.g., in macOS App Sandbox). | ||
| // Continue without redirecting; this is best-effort. | ||
| return; | ||
| } |
There was a problem hiding this comment.
🟡 Minor: When open("/dev/null") fails, setDevNullFd returns early without resetting bun_is_stdio_null[target_fd] back to 0, leaving stale state. The dup2 failure path at line 494 correctly resets the flag, but this early-return path does not. While both code paths currently lead to errors downstream, the inconsistency could mislead future code that checks isStdoutNull/isStderrNull/isStdinNull.
Why this is a problem
Bug Description
The setDevNullFd lambda in c-bindings.cpp (line 468) unconditionally sets bun_is_stdio_null[target_fd] = 1 at line 469 before attempting to open /dev/null. If the open("/dev/null") call fails (line 476 check, devNullFd_ < 0), the function returns early at line 479 without resetting the flag back to 0. This leaves bun_is_stdio_null[target_fd] in a semantically incorrect state: it claims the fd was redirected to /dev/null when in fact the fd is still invalid (EBADF).
Code Path Analysis
The specific trigger is the macOS App Sandbox scenario that this PR was designed to address. When isatty(fd) returns 0 and errno is EBADF for any of the stdio fds (0, 1, 2), setDevNullFd(fd) is called. Inside the lambda, bun_is_stdio_null[target_fd] is set to 1 optimistically. The code then attempts open("/dev/null", O_RDWR | O_CLOEXEC, 0). In an App Sandbox, this open can fail, causing the early return at line 479.
Notably, the dup2 failure path at line 493-495 does properly reset bun_is_stdio_null[target_fd] = 0, creating an asymmetry between the two error paths within the same function. This asymmetry strongly suggests the missing reset on the open failure path was an oversight rather than an intentional design choice.
Step-by-Step Proof
- Bun starts inside a macOS App Sandbox.
- The initialization loop at line 498 iterates over fds 0, 1, 2.
- For some fd (say fd=1, stdout),
isatty(1)returns 0 anderrno == EBADF. setDevNullFd(1)is called.- Line 469:
bun_is_stdio_null[1] = 1is set unconditionally. - Line 470-474: Since
devNullFd_is -1, the code attemptsopen("/dev/null", O_RDWR | O_CLOEXEC, 0). - The
openfails because the App Sandbox restricts filesystem access.devNullFd_remains negative. - Line 476-480: The
devNullFd_ < 0check is true, so the function returns early. bun_is_stdio_null[1]remains 1, falsely indicating stdout was redirected to/dev/null.
Practical Impact
In the current codebase, both code paths (flag=1 and flag=0) lead to errors in the downstream shell interpreter consumers. When the flag is 1, bun.sys.openNullDevice() is called, which itself calls open("/dev/null") and also fails in the sandbox. When the flag is 0, ShellSyscall.dup(fd) is called on an EBADF fd, which also fails. Both errors are handled by the same .err branch.
However, the flag's name (bun_is_stdio_null) and its consumers (isStdoutNull, isStderrNull, isStdinNull) describe the current state of the fd, not the intended state. Setting it to 1 when the redirect did not actually occur misrepresents reality and could mislead future code.
Recommended Fix
Add bun_is_stdio_null[target_fd] = 0; before the return; on line 479, mirroring the existing reset on the dup2 failure path at line 494:
| if (devNullFd_ < 0) { | |
| // open("/dev/null") failed (e.g., in macOS App Sandbox). | |
| // Continue without redirecting; this is best-effort. | |
| return; | |
| } | |
| if (devNullFd_ < 0) { | |
| // open("/dev/null") failed (e.g., in macOS App Sandbox). | |
| // Continue without redirecting; this is best-effort. | |
| bun_is_stdio_null[target_fd] = 0; | |
| return; | |
| } |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@test/js/bun/test-macos-app-sandbox.test.ts`:
- Around line 87-90: The test captures stderr but never uses it; either assert
it's empty or stop capturing it—update the Promise.all calls that create
[stdout, stderr, exitCode] (and the second test's equivalent) to either 1) keep
stderr and add expect(stderr.trim()).toBe("") after the existing stdout/exitCode
assertions, or 2) remove stderr from the array and Promise.all call so you only
await proc.stdout.text() and proc.exited; apply the same change to both tests
referencing proc, stdout, stderr, and exitCode.
- Around line 65-71: When running the codesign process (the Bun.spawn call
stored in codesign) capture and log its stderr if the exit code is non-zero so
failures show diagnostic output; after awaiting codesign.exited check the exit
value (the awaited result used in expect(await codesign.exited).toBe(0)), and on
a non-zero result read and include the contents of codesign.stderr (e.g., by
awaiting a read or converting to string) in the test failure message or console
output, including context like entitlementsPath and appBundlePath and using
bunEnv for reproducibility.
| await using codesign = Bun.spawn({ | ||
| cmd: ["/usr/bin/codesign", "--entitlements", entitlementsPath, "--force", "-s", "-", appBundlePath], | ||
| env: bunEnv, | ||
| stderr: "pipe", | ||
| }); | ||
| expect(await codesign.exited).toBe(0); | ||
|
|
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Consider logging codesign stderr on failure for better diagnostics.
If codesign fails, the test will report only that the exit code wasn't 0 without showing why. Capturing stderr would help diagnose signing issues (e.g., keychain access, entitlement problems).
♻️ Proposed improvement
await using codesign = Bun.spawn({
cmd: ["/usr/bin/codesign", "--entitlements", entitlementsPath, "--force", "-s", "-", appBundlePath],
env: bunEnv,
stderr: "pipe",
});
- expect(await codesign.exited).toBe(0);
+ const [codesignStderr, codesignExitCode] = await Promise.all([codesign.stderr.text(), codesign.exited]);
+ expect(codesignStderr).toBe("");
+ expect(codesignExitCode).toBe(0);🤖 Prompt for AI Agents
In `@test/js/bun/test-macos-app-sandbox.test.ts` around lines 65 - 71, When
running the codesign process (the Bun.spawn call stored in codesign) capture and
log its stderr if the exit code is non-zero so failures show diagnostic output;
after awaiting codesign.exited check the exit value (the awaited result used in
expect(await codesign.exited).toBe(0)), and on a non-zero result read and
include the contents of codesign.stderr (e.g., by awaiting a read or converting
to string) in the test failure message or console output, including context like
entitlementsPath and appBundlePath and using bunEnv for reproducibility.
Code ReviewNewest first 🟡 8a61d — 1 issue(s) found
Powered by Claude Code Review |
| @@ -0,0 +1,110 @@ | |||
| import { describe, expect, test } from "bun:test"; | |||
There was a problem hiding this comment.
🟡 Minor: Nit: This PR closes #15661, so per CLAUDE.md convention this test should be placed at test/regression/issue/15661.test.ts rather than test/js/bun/. That said, this is arguably a feature test (modeled after Node.js's test/parallel/test-macos-app-sandbox.js), so the current placement in test/js/bun/ is also defensible if intentional.
Why this is a problem
Convention Violation: Test File Placement
The test file test/js/bun/test-macos-app-sandbox.test.ts is added as part of a PR that explicitly closes GitHub issue #15661. According to the CLAUDE.md convention under "Test Organization": "If a test is for a specific numbered GitHub Issue, it should be placed in test/regression/issue/${issueNumber}.test.ts. Ensure the issue number is REAL and not a placeholder!" The same rule is echoed in test/CLAUDE.md: "Regression tests for specific issues go in /test/regression/issue/${issueNumber}.test.ts."
Based on these guidelines, because the PR is tied to issue #15661, the test file should be located at test/regression/issue/15661.test.ts. Other tests for nearby issue numbers (e.g., test/regression/issue/15276.test.ts, test/regression/issue/15314.test.ts) follow this convention consistently.
Counterargument
One reviewer disagreed, arguing this is not purely a regression test for a single bug but rather a feature test for macOS App Sandbox support. The test file itself notes on line 75 that it is "Modeled after Node.js's test/parallel/test-macos-app-sandbox.js", and the test covers general sandbox functionality (executing JavaScript inside a sandbox, verifying the sandbox container path) rather than reproducing a narrow regression scenario. The test/CLAUDE.md also states: "Unit tests for specific features are organized by module (e.g., /test/js/bun/, /test/js/node/)", which supports the current placement.
This is a reasonable interpretation. The test does read more like a feature test than a minimal reproduction of a single bug. However, the CLAUDE.md rule about issue-linked tests is fairly explicit, and the commit message directly ties this work to issue #15661.
Recommendation
Since this is a nit-level convention issue, the simplest fix would be to move the file to test/regression/issue/15661.test.ts to align with the documented convention. Alternatively, if the author considers this a feature test that should live alongside other Bun-specific API tests, they could keep the current placement but should be aware it diverges from the stated guideline for issue-linked tests.
Code ReviewNewest first 🔴 e74a6 — 2 issue(s) found
Powered by Claude Code Review |
|
Test comment |
| @@ -0,0 +1,110 @@ | |||
| import { describe, expect, test } from "bun:test"; | |||
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@test/js/bun/test-macos-app-sandbox.test.ts`:
- Around line 68-74: The spawned codesign process is created with stdout: "pipe"
and its output is never consumed, which can block; change the Bun.spawn call
that defines codesign (the variable from Bun.spawn with cmd using
entitlementsPath, appBundlePath and env bunEnv) to either use stdout: "inherit"
or read/consume the pipe before awaiting codesign.exited (for example await
codesign.stdout.readAll() or stream it) and then assert await codesign.exited
=== 0; ensure the fix is applied where codesign is declared and where
codesign.exited is awaited.
Code ReviewNewest first 🔴 17f5b — 1 issue(s) found
Powered by Claude Code Review |
Code ReviewNewest first ✅ 20e26 — Looks good! Reviewed 2 files across ✅ 17f5b — Looks good! Reviewed 2 files across Powered by Claude Code Review |
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>
|
What's required to land this? I'm dying to have this. If I can help out in any way I would love to. |
|
when is this landing top G @Jarred-Sumner |
Bun crashes inside macOS App Sandbox (oven-sh/bun#27041). Remove the entitlement so the app launches. Keep the full verify section on the website but ghost it with a coming-soon stamp linking to the upstream PR. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove app-sandbox entitlement (Bun crashes in sandbox, oven-sh/bun#27041) - Ghost the verify section with a coming-soon stamp linking to the upstream PR - Add clickable screenshot lightbox with keyboard dismiss Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Remove in-app updater and enable macOS App Sandbox The app's core promise is zero network access enforced by the macOS sandbox. The in-app updater is incompatible with this — it requires network entitlements. Users download new versions from the website. - Remove all updater code (bun, RPC, types, frontend banner) - Add com.apple.security.app-sandbox entitlement - Add com.apple.security.files.user-selected.read-write for vault/repo access - No network entitlements: grep -c "network" returns 0 - Update website verify section to show sandbox + network check Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Format upload script and OG render imports Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Allow manual release workflow dispatch Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Remove sandbox entitlement, add coming-soon overlay on website Bun crashes inside macOS App Sandbox (oven-sh/bun#27041). Remove the entitlement so the app launches. Keep the full verify section on the website but ghost it with a coming-soon stamp linking to the upstream PR. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Remove sandbox entitlement, add coming-soon overlay, screenshot lightbox - Remove app-sandbox entitlement (Bun crashes in sandbox, oven-sh/bun#27041) - Ghost the verify section with a coming-soon stamp linking to the upstream PR - Add clickable screenshot lightbox with keyboard dismiss Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Fixes two issues in
bun_initialize_process()that prevent Bun from running inside a macOS App Sandbox (com.apple.security.app-sandbox):TTY state capture fix: Move
bun_stdio_tty[fd] = 1inside thetcgetattrsuccess check. In the macOS App Sandbox,tcgetattrfails withEPERMeven thoughisatty()returns true. Previously the TTY flag was set unconditionally, causingbun_restore_stdio()to calltcsetattrwith uninitialized termios state at exit. This is the same class of bug Node.js fixed in nodejs/node#33944.dup2return value bug:dup2(oldfd, newfd)returnsnewfdon success (not 0), so the checkerr != 0incorrectly triggeredabort()for stdout/stderr (fd 1 and 2). Changed toerr < 0and replacedabort()with a graceful fallback. Also handlesopen("/dev/null")failure gracefully for restricted environments.Closes #15661
Test plan
bun bd)tty.test.ts,nodettywrap.test.ts)test-macos-app-sandbox.test.tscorrectly skips on non-macOSbun -einside sandbox, verifies exit code 0🤖 Generated with Claude Code