Skip to content

fix(process): support running Bun inside macOS App Sandbox#27041

Open
robobun wants to merge 6 commits intomainfrom
claude/fix-macos-app-sandbox
Open

fix(process): support running Bun inside macOS App Sandbox#27041
robobun wants to merge 6 commits intomainfrom
claude/fix-macos-app-sandbox

Conversation

@robobun
Copy link
Copy Markdown
Collaborator

@robobun robobun commented Feb 14, 2026

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] = 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.

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

Closes #15661

Test plan

  • Build succeeds (bun bd)
  • Existing TTY tests pass (tty.test.ts, nodettywrap.test.ts)
  • New test-macos-app-sandbox.test.ts correctly skips on non-macOS
  • On macOS: new test creates app bundle, signs with sandbox entitlements, runs bun -e inside sandbox, verifies exit code 0

🤖 Generated with Claude Code

@robobun
Copy link
Copy Markdown
Collaborator Author

robobun commented Feb 14, 2026

Updated 11:15 PM PT - Feb 15th, 2026

@Jarred-Sumner, your commit 20e26a7 has 3 failures in Build #37376 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 27041

That installs a local version of the PR into your bun-27041 executable, so you can run:

bun-27041 --bun

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 14, 2026

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉


Walkthrough

C 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 .app, runs Bun inside the sandbox, and verifies execution and homedir resolution.

Changes

Cohort / File(s) Summary
File Descriptor Redirection Logic
src/bun.js/bindings/c-bindings.cpp
Return early when devNullFd_ < 0; on dup2 failure set bun_is_stdio_null[target_fd] = 0 instead of aborting; remove assertion that devNullFd_ must be valid; set bun_stdio_tty[fd] only if tcgetattr succeeds; add comments about sandbox-related capture behavior.
macOS App Sandbox Testing
test/js/bun/test-macos-app-sandbox.test.ts
Add macOS-only test that constructs a faux .app bundle (Contents/MacOS, Info.plist, entitlements), codesigns and places the Bun binary into the bundle, runs Bun in the App Sandbox, asserts JS execution and that os.homedir() resolves to the sandbox container, and cleans up artifacts.
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (14 files):

⚔️ src/bun.js/api/Timer/TimerObjectInternals.zig (content)
⚔️ src/bun.js/bindings/c-bindings.cpp (content)
⚔️ src/bundler/Chunk.zig (content)
⚔️ src/bundler/linker_context/computeChunks.zig (content)
⚔️ src/napi/napi.zig (content)
⚔️ src/runtime.js (content)
⚔️ test/bundler/__snapshots__/bun-build-api.test.ts.snap (content)
⚔️ test/bundler/bundler_edgecase.test.ts (content)
⚔️ test/bundler/bundler_html.test.ts (content)
⚔️ test/bundler/bundler_npm.test.ts (content)
⚔️ test/bundler/bundler_promiseall_deadcode.test.ts (content)
⚔️ test/bundler/esbuild/css.test.ts (content)
⚔️ test/bundler/html-import-manifest.test.ts (content)
⚔️ test/regression/issue/cyclic-imports-async-bundler.test.js (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: fixing support for running Bun inside the macOS App Sandbox, which directly matches the PR objectives and code changes.
Description check ✅ Passed The PR description covers both required template sections with substantial detail: 'What does this PR do?' section explains the two fixes with technical depth and references, and 'How did you verify your code works?' section provides a comprehensive test plan with checkmarks.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟡 Minor

Clear bun_is_stdio_null when /dev/null can’t be opened

On 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).

Comment thread test/js/bun/test-macos-app-sandbox.test.ts Outdated
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Feb 14, 2026

Code Review

Newest first

3d0fa — Looks good!

Reviewed 2 files across src/bun.js/bindings/ and test/js/bun/: Fixes two initialization bugs in bun_initialize_process() that prevented Bun from running inside a macOS App Sandbox — correcting the dup2 return value check and moving the TTY state flag inside the tcgetattr success check.


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>
@robobun robobun force-pushed the claude/fix-macos-app-sandbox branch from 3d0fae4 to 1af1095 Compare February 14, 2026 23:54
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Feb 15, 2026

Code Review

Newest first

🟡 1af10 — 1 issue found

Issue Severity File
bun_is_stdio_null flag not reset on open() failure 🟡 src/bun.js/bindings/c-bindings.cpp

Powered by Claude Code Review

Comment on lines +476 to +480
if (devNullFd_ < 0) {
// open("/dev/null") failed (e.g., in macOS App Sandbox).
// Continue without redirecting; this is best-effort.
return;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

  1. Bun starts inside a macOS App Sandbox.
  2. The initialization loop at line 498 iterates over fds 0, 1, 2.
  3. For some fd (say fd=1, stdout), isatty(1) returns 0 and errno == EBADF.
  4. setDevNullFd(1) is called.
  5. Line 469: bun_is_stdio_null[1] = 1 is set unconditionally.
  6. Line 470-474: Since devNullFd_ is -1, the code attempts open("/dev/null", O_RDWR | O_CLOEXEC, 0).
  7. The open fails because the App Sandbox restricts filesystem access. devNullFd_ remains negative.
  8. Line 476-480: The devNullFd_ < 0 check is true, so the function returns early.
  9. 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:

Suggested change
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;
}

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +65 to +71
await using codesign = Bun.spawn({
cmd: ["/usr/bin/codesign", "--entitlements", entitlementsPath, "--force", "-s", "-", appBundlePath],
env: bunEnv,
stderr: "pipe",
});
expect(await codesign.exited).toBe(0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 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.

Comment thread test/js/bun/test-macos-app-sandbox.test.ts Outdated
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Feb 15, 2026

Code Review

Newest first

🟡 8a61d — 1 issue(s) found

Issue Severity File
Test should be in regression directory per CLAUDE.md 🟡 test/js/bun/test-macos-app-sandbox.test.ts

Powered by Claude Code Review

@@ -0,0 +1,110 @@
import { describe, expect, test } from "bun:test";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Feb 15, 2026

Code Review

Newest first

🔴 e74a6 — 2 issue(s) found

Issue Severity File
bun_is_stdio_null flag not reset on open failure 🔴 src/bun.js/bindings/c-bindings.cpp
Test should use test.concurrent 🟡 test/js/bun/http/bun-serve-html-entry.test.ts

Powered by Claude Code Review

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Feb 15, 2026

Test comment

@@ -0,0 +1,110 @@
import { describe, expect, test } from "bun:test";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test comment

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread test/js/bun/test-macos-app-sandbox.test.ts Outdated
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Feb 15, 2026

Code Review

Newest first

🔴 17f5b — 1 issue(s) found

Issue Severity File
bun_is_stdio_null flag left stale when open("/dev/null") fails 🔴 src/bun.js/bindings/c-bindings.cpp

Powered by Claude Code Review

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Feb 15, 2026

Code Review

Newest first

20e26 — Looks good!

Reviewed 2 files across src/bun.js/bindings/ and test/js/bun/: Fixes two bugs in process initialization that prevented Bun from running inside a macOS App Sandbox.


17f5b — Looks good!

Reviewed 2 files across src/bun.js/bindings/ and test/js/bun/: Fixes two bugs in bun_initialize_process() to support running Bun inside a macOS App Sandbox: corrects the dup2 return value check and gates TTY state capture on tcgetattr success.


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>
@merlinaudio
Copy link
Copy Markdown

What's required to land this? I'm dying to have this. If I can help out in any way I would love to.

@tsconfigdotjson
Copy link
Copy Markdown

tsconfigdotjson commented Mar 26, 2026

when is this landing top G @Jarred-Sumner

tsconfigdotjson added a commit to tsconfigdotjson/dotlock that referenced this pull request Mar 26, 2026
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>
tsconfigdotjson added a commit to tsconfigdotjson/dotlock that referenced this pull request Mar 26, 2026
- 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>
tsconfigdotjson added a commit to tsconfigdotjson/dotlock that referenced this pull request Mar 26, 2026
* 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cannot run Bun within macOS sandbox

4 participants