5.8.3 macOS Sandboxing Challenges
recommendedOn 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:
| Name | What it means on macOS |
|---|---|
sandboxed | Ask Bazel to choose an available local sandbox backend, preferring the OS-specific one. |
darwin-sandbox | Build a per-action symlink-tree execroot, then run the action through /usr/bin/sandbox-exec and Bazel's process wrapper. |
processwrapper-sandbox | Build 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-sandboxregresses, 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.
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
darwin-sandbox and processwrapper-sandbox stage declared inputs in a per-action tree.darwin-sandbox adds Bazel's generated sandbox-exec policy.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
darwin-sandbox regressesFootnotes
-
Sandboxing — strategy selection,
processwrapper-sandbox,darwin-sandbox, fallback, and--sandbox_debug↩1 ↩2 ↩3 ↩4 ↩5 -
Bazel — core implementation, documentation, and regression corpus — Bazel 9.0
SandboxModule,DarwinSandboxedSpawnRunner, andProcessWrapperSandboxedSpawnRunner↩1 ↩2 ↩3 ↩4 -
Bazel and action (non-) determinism — platform-specific sandbox limits and the absence of a macOS PID namespace ↩
-
sandboxfs: A retrospective — workload-specific macOS measurements and the compiler-state finding behind them ↩