Skip to content

fix(sandbox): allow realpath(cwd) so bun run/bunx work in enforcing workspaces#36

Merged
simion merged 1 commit into
simion:mainfrom
pds:fix/sandbox-realpath-ancestor-dirs
Jun 14, 2026
Merged

fix(sandbox): allow realpath(cwd) so bun run/bunx work in enforcing workspaces#36
simion merged 1 commit into
simion:mainfrom
pds:fix/sandbox-realpath-ancestor-dirs

Conversation

@pds

@pds pds commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Fixes #35.

Problem

In an enforcing workspace, bun run <script> and bun x <pkg> fail at startup with:

error loading current directory
error: An internal error occurred (CouldntReadCurrentDirectory)

More generally, anything that canonicalizes the cwd with realpath(3) fails — which includes the Bun-compiled agent CLIs' bun run/bunx subprocesses and any Husky/git hook that uses bunx.

Root cause

render_profile allows file-read-metadata + file-test-existence globally, but file-read* (which includes file-read-data, i.e. opening a directory) only on the workspace, runtime, system roots, and (literal "/"). The workspace's intermediate ancestor directories (/Users, /Users/<me>, … down to the workspace's parent) get only metadata/existence.

realpath(3) on the cwd has to open() each ancestor directory to resolve the path — a file-read-data op on each — which the allow-list denies → EPERM. It's the exact (literal "/") situation one level down: the components between / and the workspace are traversable (metadata is global) but not openable, and realpath(3) needs the open.

Empirically (inside the cage): realpath(".") → EPERM, while fcntl(F_GETPATH), getattrlist(ATTR_CMN_FULLPATH), getcwd, and a userspace lstat-walk all succeed (they don't open ancestors). That's why bun --version/bun pm ls/Bun.spawnSync work but bun run/bun x don't, and why bun x from /tmp works (/private, / are broadly readable).

Fix

Grant (allow file-read* (literal "<dir>")) on each strict ancestor of the workspace root (and each composition member), up to but not including /. Using literal (the directory node only, not subpath) lets realpath/traversal open the path components without exposing sibling subtrees' contents — the same trade-off as the existing (literal "/") grant, one level down. Sibling names are already enumerable via the global metadata allow, so this leaks no new secrets (sibling CONTENTS stay denied by (deny default)).

A shared helper, workspace_ancestor_dirs, is used by both render_profile (enforcement) and compute_monitor_policy (the would-block classifier) so the two stay in sync — MonitorPolicy gains a read_literals set checked with exact-match (not the subtree under() check) so monitoring mode doesn't mis-classify these ancestor reads.

Testing

  • cargo test --lib sandbox::55 pass (52 existing + 3 new):
    • ancestor_dirs_walks_up_to_but_not_root
    • workspace_ancestor_dirs_strict_ancestors_only
    • would_block_allows_ancestor_node_read_not_subtree
  • cargo check clean; cargo clippy --lib introduces no new warnings (the pre-existing ones are untouched).
  • The existing would_block_classifies_pure_allowlist test was updated for the new read_literals field (set to vec![], behavior unchanged).

I verified the root cause end-to-end on a live enforcing workspace (the realpath EPERM repro in #35) and confirmed the fix follows the existing (literal "/") pattern, but I could not run a full signed app build in my environment — CI / a maintainer build is the final confirmation that a real sandboxed bun run now succeeds.


By opening this PR I agree to the CLA per CONTRIBUTING.md.

…stor dirs

Enforcing mode denied `bun run`/`bunx` — and anything that realpath(3)s the
cwd — with EPERM ("error loading current directory" /
"CouldntReadCurrentDirectory"). The profile allows file-read-metadata +
file-test-existence globally but file-read* (which includes opening a
directory) only on the workspace, runtime, system roots, and `(literal "/")`
— not the workspace's intermediate ancestor dirs. realpath(3) must open()
each ancestor to canonicalize the cwd, so it hit `(deny default)`.

Grant `(allow file-read* (literal <dir>))` on each strict ancestor of the
workspace root + composition members, up to but not including "/". `literal`
(the node only, not `subpath`) permits traversal/enumeration of the path
components without exposing sibling subtrees' contents — the same trade-off
as the existing `(literal "/")` grant, one level down. A shared helper
(`workspace_ancestor_dirs`) keeps render_profile and compute_monitor_policy's
would-block classifier in sync; unit tests cover the helper + classifier.

Fixes simion#35
simion added a commit that referenced this pull request Jun 14, 2026
Completion sound (#34): play the sound directly via afplay instead of
relying on the desktop notification's sound field. mac-notification-sys
rides the deprecated NSUserNotification API, which drops the banner sound
on modern macOS, so Preview and real completions were silent. New
play_completion_sound Rust command resolves the macOS sound name via the
Library/Sounds search path and plays it on a detached thread; the banner
still fires but silent.

Sandbox bun/realpath (#35, #36): grant each strict workspace-ancestor
directory read as (literal ...) so realpath(3) on the cwd can open() the
path components. Without it, bun run / bunx (and any tool that
canonicalizes the cwd) failed at startup with CouldntReadCurrentDirectory
in enforcing workspaces. Shared workspace_ancestor_dirs helper keeps
render_profile and compute_monitor_policy in sync.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@simion simion merged commit 792e9f7 into simion:main Jun 14, 2026
2 checks passed
@simion

simion commented Jun 14, 2026

Copy link
Copy Markdown
Owner

Thanks for contributing!

tested locally and it works accordingly. 🍻

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Enforcing sandbox breaks bun run/bunx (and any realpath-of-cwd): EPERM opening workspace ancestor dirs

2 participants