Skip to content

feat(security): #505 MVP — PERRY_SANDBOX_BUILDRS=1 wraps cargo in sandbox-exec on macOS#981

Merged
proggeramlug merged 1 commit into
mainfrom
feat/505-sandbox-cargo-buildrs
May 18, 2026
Merged

feat(security): #505 MVP — PERRY_SANDBOX_BUILDRS=1 wraps cargo in sandbox-exec on macOS#981
proggeramlug merged 1 commit into
mainfrom
feat/505-sandbox-cargo-buildrs

Conversation

@proggeramlug
Copy link
Copy Markdown
Contributor

Closes #505 (MVP — Linux landlock follow-up tracked).

Summary

perry.nativeLibrary resolution triggers cargo build on developer machines for any source-distributed crate. Crate build.rs scripts run with full developer privileges. Most TypeScript 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. 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* to target/ + ~/.cargo + ~/.rustup + /tmp + TempDir
  • allow 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:

PERRY_SANDBOX_BUILDRS=1 perry compile main.ts -o myapp

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:

cargo fetch --manifest-path node_modules/@foo/native-bar/Cargo.toml
PERRY_SANDBOX_BUILDRS=1 perry compile main.ts -o myapp

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.json and shows up in code review.

Cross-platform scope

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

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 — pins PERRY_SANDBOX_BUILDRS=1, allowUnsandboxedBuild, security: sandbox cargo build.rs execution 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

  • CompilationContext gains pub allow_unsandboxed_build: Vec<String>.
  • Parsed from perry.allowUnsandboxedBuild in host package.json.
  • New module commands::compile::sandbox_buildrs with wrap_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 hostile name: "../etc/passwd" can't escape the cache dir.
  • link.rs swaps Command::new("cargo") for the wrapper.

Docs

docs/src/cli/sandbox-buildrs.md documents 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

  • Linux landlock + bubblewrap profile, optionally unshare -n for network denial.
  • Auto pre-fetch (cargo fetch outside the sandbox before the sandboxed build).
  • HIR-driven profile refinement.

Notes

No Cargo.toml version bump, no CLAUDE.md touch, no CHANGELOG.md entry — maintainer folds those in at merge time.

…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).
@proggeramlug proggeramlug force-pushed the feat/505-sandbox-cargo-buildrs branch from cbf61f4 to 61d292a Compare May 18, 2026 10:28
@proggeramlug proggeramlug merged commit 5eac782 into main May 18, 2026
@proggeramlug proggeramlug deleted the feat/505-sandbox-cargo-buildrs branch May 18, 2026 10:28
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.
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.

security: sandbox cargo build.rs execution for native-archive crates

1 participant