feat(security): #505 MVP — PERRY_SANDBOX_BUILDRS=1 wraps cargo in sandbox-exec on macOS#981
Merged
Merged
Conversation
…dbox-exec on macOS
perry.nativeLibrary resolution triggers cargo build on developer
machines for any source-distributed crate. Crate build.rs scripts
run with full developer privileges. Most TS developers don't think
of `bun add` as triggering arbitrary Rust code, so the implicit
trust escalation is invisible.
This adds an opt-in sandbox: when PERRY_SANDBOX_BUILDRS=1 is set,
the compile driver wraps cargo invocations for nativeLibrary crate
builds in `sandbox-exec` on macOS with a profile that:
- denies network (build.rs can't phone home)
- restricts FS writes to target/ + ~/.cargo + ~/.rustup + /tmp
+ TempDir
- allows file-read everywhere (cargo/rustc need it)
- allows process-fork/process-exec (rustc, cc, ld)
- allows sysctl-read / mach-lookup / iokit-open (system queries)
Build-time only — zero runtime cost. The compiled binary is the
same whether the cargo invocation was sandboxed or not.
Off by default for backwards compatibility (existing builds don't
need to pre-fetch deps to keep working). CI enables via env var.
The pre-fetch workflow is documented for fresh-checkout flows:
cargo fetch --manifest-path ... # outside the sandbox
PERRY_SANDBOX_BUILDRS=1 perry compile ... # inside
`perry.allowUnsandboxedBuild: ["pkg-name", ...]` in the host
package.json exempts named packages. Host-controlled — transitive
deps can't opt themselves out. The exemption sits in the host
repo's package.json so it shows up in code review.
| Platform | Status |
|------------|-----------------------------------------------------------|
| **macOS** | Real impl: sandbox-exec wrapper + per-package profile |
| Linux | One-line note: landlock/bubblewrap follow-up under #505 |
| Windows | Same — Win32 sandbox profile follow-up |
| Others | N/A (no cargo cross-toolchain on those hosts via Perry) |
- CompilationContext gains `pub allow_unsandboxed_build: Vec<String>`.
- Parsed from `perry.allowUnsandboxedBuild` in host package.json.
- New module `commands::compile::sandbox_buildrs`:
- `wrap_cargo_command(ctx, pkg_name) -> Command` returns either
Command::new("cargo") (legacy) or Command::new("sandbox-exec")
with the right preamble args. Falls through cleanly when sandbox
disabled / package exempt / non-macOS host.
- `build_macos_buildrs_profile()` returns the static profile body
(testable in isolation).
- `write_macos_buildrs_profile()` writes to
`<project>/.perry-cache/buildrs-<safe-pkg>.sandbox` and
returns the absolute path. Sanitises the package name into a
single path component so a hostile `name: "../etc/passwd"`
can't escape the cache dir.
- link.rs swaps `Command::new("cargo")` for the wrapper.
- profile_denies_network_allows_build_dirs — pins the deny-network,
allow-process-spawn, allow-file-read, target/ write semantics.
- profile_header_documents_usage_and_escape_hatch — pins
PERRY_SANDBOX_BUILDRS=1 + allowUnsandboxedBuild + #505 in the
emitted header.
- wrap_cargo_falls_through_when_sandbox_disabled — opt-in invariant.
- wrap_cargo_falls_through_when_package_exempt — escape hatch.
`docs/src/cli/sandbox-buildrs.md` documents the workflow + the
pre-fetch pattern + the escape hatch + the deferred Linux follow-up.
- Linux landlock / bubblewrap profile + `unshare -n` for network
denial.
- Auto pre-fetch (`cargo fetch` outside the sandbox before the
sandboxed build) so fresh-checkout flows don't fail.
- HIR-driven profile refinement (tighter writes when the build is
known not to need certain paths).
cbf61f4 to
61d292a
Compare
3 tasks
proggeramlug
added a commit
that referenced
this pull request
May 18, 2026
…Rs (#1019) #981 (PERRY_SANDBOX_BUILDRS), #988 (--emit-attest), and #969 (perry.permissions) each landed via admin-bypass with their SUMMARY.md entries intact but without the actual .md content files (or, for #969, without any docs entry at all). docs/src/cli/lockdown.md and docs/src/cli/emit-sandbox.md did make it into main and are fine; the others left dead links. Separately, #976 / #972 / #974 added perry/system runtime methods (getOSVersion, shareText, shareUrl, appGroupSet/Get/Delete) but never updated the hand-maintained types/perry/system/index.d.ts. TypeScript users importing those APIs from `perry/system` get a type error today. This PR: - Creates docs/src/cli/sandbox-buildrs.md (#505) - Creates docs/src/cli/emit-attest.md (#504) - Creates docs/src/cli/capabilities.md (#501) and adds the SUMMARY.md entry - Adds the six new perry/system signatures to types/perry/system/index.d.ts The auto-generated docs/api/perry.d.ts + docs/src/api/reference.md were regenerated during the original PRs and are already current. Pure docs-only diff. No code changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #505 (MVP — Linux landlock follow-up tracked).
Summary
perry.nativeLibraryresolution triggerscargo buildon developer machines for any source-distributed crate. Cratebuild.rsscripts run with full developer privileges. Most TypeScript developers don't think ofbun addas triggering arbitrary Rust code, so the implicit trust escalation is invisible.This adds an opt-in sandbox: when
PERRY_SANDBOX_BUILDRS=1is set, the compile driver wraps cargo invocations for nativeLibrary crate builds insandbox-execon macOS. Build-time only — zero runtime cost in the produced binary.Profile contents
deny default+deny network*(build.rs can't phone home)allow file-read*everywhere (cargo / rustc read system libs, source, dep crates)allow file-write*totarget/+~/.cargo+~/.rustup+/tmp+TempDirallow process-fork+process-exec(rustc, cc, ld, build.rs binaries)allow sysctl-read/mach-lookup/iokit-open(system queries)Opt-in
Off by default for backwards compat. Enable per build:
CI sets the env var on every job. Local dev keeps the legacy flow until ready.
Pre-fetch workflow
The sandbox denies network, so cargo can't reach crates.io from inside it. Pre-fetch outside the sandbox once before the sandboxed build:
CI typically caches
~/.cargo, so this pre-fetch is free on subsequent runs.Per-package escape hatch
{ "perry": { "allowUnsandboxedBuild": ["@some-vendor/builds-with-network"] } }Host-controlled — transitive deps can't opt themselves out. The exemption sits in the host repo's
package.jsonand shows up in code review.Cross-platform scope
Test coverage
4 unit tests in
commands::compile::sandbox_buildrs::tests:profile_denies_network_allows_build_dirs— pins the deny-network + allow-process-spawn + allow-file-read +target/write semantics.profile_header_documents_usage_and_escape_hatch— pinsPERRY_SANDBOX_BUILDRS=1,allowUnsandboxedBuild, security: sandboxcargo build.rsexecution for native-archive crates #505 cite in the emitted header.wrap_cargo_falls_through_when_sandbox_disabled— opt-in invariant.wrap_cargo_falls_through_when_package_exempt— escape-hatch invariant.Plumbing
CompilationContextgainspub allow_unsandboxed_build: Vec<String>.perry.allowUnsandboxedBuildin hostpackage.json.commands::compile::sandbox_buildrswithwrap_cargo_command()/build_macos_buildrs_profile()/write_macos_buildrs_profile(). Sanitises the package name into a single path component when writing the profile so a hostilename: "../etc/passwd"can't escape the cache dir.link.rsswapsCommand::new("cargo")for the wrapper.Docs
docs/src/cli/sandbox-buildrs.mddocuments the workflow + the pre-fetch pattern + the escape hatch + the deferred Linux follow-up. Linked from SUMMARY.md under CLI Reference.What's deferred for #505 follow-ups
unshare -nfor network denial.cargo fetchoutside the sandbox before the sandboxed build).Notes
No
Cargo.tomlversion bump, noCLAUDE.mdtouch, noCHANGELOG.mdentry — maintainer folds those in at merge time.