5.8.3 macOS Sandboxing Challenges

recommended

On macOS, sandboxed is a selector rather than the name of one fixed implementation. Bazel prefers the darwin-sandbox backend when Apple's sandbox-exec mechanism is available and otherwise can fall back to processwrapper-sandbox.1 That fallback still stages declared inputs, but it does not enforce the same host-side policy. A consultant therefore needs to identify the backend that actually ran before drawing conclusions about either correctness or performance.

Separate the selector from the backend

The names describe different layers of the execution choice:

NameWhat it means on macOS
sandboxedAsk Bazel to choose an available local sandbox backend, preferring the OS-specific one.
darwin-sandboxBuild a per-action symlink-tree execroot, then run the action through /usr/bin/sandbox-exec and Bazel's process wrapper.
processwrapper-sandboxBuild the same kind of per-action symlink-tree execroot and extract declared outputs, without an OS sandbox policy.
local (standalone is also a current alias)Run directly in the normal execroot without per-action sandbox staging.

The portable backend is named processwrapper-sandbox, not processwrapper. Likewise, --spawn_strategy=processwrapper is not the current configuration form. 2.5.2 Strategies & Mnemonics introduces the distinction between a strategy selector, a concrete backend, and an action mnemonic.

Both sandbox backends make undeclared relative inputs disappear from the action's prepared working directory. Both also move only declared outputs back to the normal execroot.1 This directory staging is the baseline mechanism from 2.3.2 Sandboxing. darwin-sandbox adds a macOS policy around it. processwrapper-sandbox does not.

What the Darwin policy actually restricts

In Bazel 9.0, darwin-sandbox generates a Seatbelt profile and invokes /usr/bin/sandbox-exec -f <profile> .... The profile starts from allow default, denies filesystem writes globally, then grants writes to the sandbox and a compatibility set that includes temporary directories and selected user cache, log, and developer directories. It can deny non-local network access, but Bazel 9 defaults --sandbox_default_allow_network to true. Use --nosandbox_default_allow_network when the default should be denial. An action's block-network or requires-network execution requirement can then override that default for the action. Paths passed through Bazel's block-path option receive explicit read denials.2

The generated policy has no blanket deny file-read*. It is not a private filesystem namespace, and it does not hide the host filesystem or other host processes. An action can still read many absolute host paths if the normal macOS permissions permit it. macOS also has no Linux-style PID namespace in this backend.2,3

That gives darwin-sandbox three useful but bounded checks:

  • the prepared execroot catches the usual undeclared relative-path dependency.
  • the Seatbelt policy prevents most writes outside Bazel's allowed locations.
  • network policy catches many undeclared network accesses when it is enabled.

It does not prove that an action depends only on declared files, make output bytes deterministic, or provide a security container for untrusted code. An absolute read from /usr, a user cache, or another permitted host path can remain a hidden dependency. Treat a green sandboxed build as evidence, not a proof of complete hermeticity.

processwrapper-sandbox has a still narrower boundary. Its symlink tree catches accidental relative reads and keeps stray outputs out of the normal execroot, but there is no Seatbelt policy to constrain absolute host reads, host writes, network access, or process visibility.1,2 It is therefore inaccurate to describe it as “no filesystem isolation” and equally inaccurate to treat it as equivalent to darwin-sandbox: it provides directory staging, but not OS-level enforcement.

Detect fallback instead of assuming the backend

Bazel probes whether sandbox-exec can run. When the probe fails, or when Bazel itself is already inside a macOS sandbox that cannot nest sandbox-exec, the generic selector can fall back to processwrapper-sandbox.1,2 The execution summary reports concrete names such as darwin-sandbox. Use that evidence rather than inferring the backend from the host OS.

For a local diagnostic run, --sandbox_debug retains the per-action directory and prints extra sandbox information. Disable it after the investigation because retained sandboxes accumulate on disk.1 If CI must fail instead of silently accepting the weaker fallback, route an affected mnemonic with --strategy=<Mnemonic>=darwin-sandbox. Use --spawn_strategy=darwin-sandbox only when that boundary is intended for all ordinary spawns, and verify the concrete strategy in the invocation evidence. Also audit mnemonic-specific local, worker, and no-sandbox exceptions: a strict default does not erase explicit exceptions.

Measure the trade-off on the workload that matters

The sandboxing cost is not one macOS constant. Both backends create and remove input trees. Action input count, filesystem behavior, cleanup, persistent workers, and tool-owned caches can dominate the result. Historical iOS data showed large sandbox penalties for one workload, but the investigation also found that compiler state which could not persist mattered more than symlink creation alone.4 That is evidence against a universal rule, not evidence for a fixed percentage or a guaranteed faster backend.

Compare matched invocations of the affected mnemonic under darwin-sandbox, processwrapper-sandbox, and local. Keep targets, revision, flags, machine load, server state, and cache state comparable, then inspect the trace profile and repeat the measurement. Interpret the deltas narrowly:

  • if only darwin-sandbox regresses, investigate Seatbelt policy interaction.
  • if both sandbox backends regress similarly, investigate staging, cleanup, or per-action tool state.
  • if repeated actions improve under a worker, investigate a sandboxed-worker configuration before removing sandboxing broadly.

For local development, keep sandboxed as the baseline and introduce the smallest mnemonic-specific exception only after a repeatable measurement. A blanket --spawn_strategy=local trades away the diagnostic signal for every action. For macOS CI that executes actions locally, prefer explicit darwin-sandbox when fallback would violate policy. Remote execution is a separate isolation boundary and should be evaluated from the executor's actual contract, not inferred from the client platform.

key takeaway

On macOS, sandboxed normally chooses darwin-sandbox, while processwrapper-sandbox is the weaker staged-directory fallback. Both expose declared inputs through a per-action tree. Only the Darwin backend adds a sandbox-exec policy, and even that policy allows broad host reads and selected writes. Verify the concrete backend, treat it as a correctness check rather than a security container, and change local strategy only after a controlled measurement while keeping CI's required boundary explicit.

Check your understanding · 3 questions

1.A macOS CI policy requires failure rather than fallback to the weaker sandbox backend for C++ compiles. What should the team do?

Select one answer

2.Assess these boundaries of Bazel 9 sandboxing on macOS:

Choose True or False for each sentence

Both darwin-sandbox and processwrapper-sandbox stage declared inputs in a per-action tree.
Only darwin-sandbox adds Bazel's generated sandbox-exec policy.
The Darwin policy makes arbitrary absolute host files unreadable.
The Darwin backend denies non-local network access by default in Bazel 9.

3.Match each controlled measurement to the first hypothesis it supports:

Drag each answer onto the matching prompt, or click an answer and then click a prompt

Answers
Only darwin-sandbox regresses
Both sandbox backends regress similarly
Repeated actions improve under a worker
0 of 3 answered

Footnotes

  1. Sandboxing — strategy selection, processwrapper-sandbox, darwin-sandbox, fallback, and --sandbox_debug 1 2 3 4 5

  2. Bazel — core implementation, documentation, and regression corpus — Bazel 9.0 SandboxModule, DarwinSandboxedSpawnRunner, and ProcessWrapperSandboxedSpawnRunner 1 2 3 4

  3. Bazel and action (non-) determinism — platform-specific sandbox limits and the absence of a macOS PID namespace

  4. sandboxfs: A retrospective — workload-specific macOS measurements and the compiler-state finding behind them