5.8.1 Symlink Forests

recommended

A sandboxed action does not run in a copy of the repository. Bazel gives it a small filesystem view shaped like the normal execution root, then materializes the action's declared inputs at their exec paths. For the ordinary symlinked sandbox implementation, most of those entries are symbolic links back to files in Bazel's real execroot. Understanding that indirection makes both sandbox failures and sandbox setup costs much easier to diagnose.1,2

Keep the two execroots separate

Bazel maintains a shared execroot under the output base. Unsandboxed local actions work there, and its paths are the reference points for action inputs and outputs. A sandboxed spawn instead receives a temporary directory that mimics that layout. The action still opens paths such as pkg/source.cc or bazel-out/.../generated.h, but those names now resolve inside its private working tree.2,3

This per-action tree is not the repository-level symlink forest described by the output-directory layout, nor is it a binary's runfiles tree or one of the bazel-bin convenience links. All of those structures may use symlinks, but they have different lifetimes and consumers. Here, “symlink forest” means the input view prepared for one sandboxed spawn.

For each ordinary file input, Bazel knows two paths:

  • the exec path at which the tool expects to see the input, and
  • the backing path in the shared execroot where Bazel already has that source, generated artifact, tool, or runfile.

The symlinked sandbox creates the first path and points it at the second. Declared symlink inputs are recreated with their declared link target instead, and special empty inputs are created directly in the sandbox. Bazel also creates the necessary parent directories and writable locations for declared outputs. After the process exits, Bazel moves known outputs back to the shared output tree and discards unknown files with the sandbox.1,4

That mapping is the filesystem realization of the action contract designed in 4.4.3 Action Execution Contract. The rule supplies the inputs and tools. sandbox preparation turns that set into the process's working view. Changing execution strategy does not repair an incomplete contract—it only changes how strictly the missing input is exposed.

Symlinks stage inputs. The sandbox backend enforces policy

The symlink forest and OS isolation solve related but distinct problems. The forest gives the process a sparse, execroot-shaped view, so an undeclared cwd-relative path is normally absent. processwrapper-sandbox uses this view, runs the command there, extracts declared outputs, and removes the directory. linux-sandbox and darwin-sandbox add platform facilities such as Linux namespaces or macOS sandbox-exec to constrain access outside that view more strongly.1

This distinction prevents an overclaim: a symbolic link is not an access-control boundary by itself. A tool can resolve a link to its backing path, and the generic process wrapper does not hide the whole host filesystem. OS-specific backends enforce stronger restrictions—for example, linux-sandbox makes the host filesystem read-only outside writable sandbox locations and can isolate network and process visibility—but ordinary sandboxing does not imply that arbitrary host files are unreadable. The input tree is what presents the declared working set. 2.3.2 Sandboxing develops the correctness model. At this level, inspect the two mechanisms separately when behavior differs between processwrapper-sandbox, linux-sandbox, and darwin-sandbox.

The strategy names and mnemonics from 2.5.2 Strategies & Mnemonics remain the outer routing layer. “This was a CppCompile action using sandboxed” identifies the action class and requested strategy. The concrete backend and its input-tree implementation explain the filesystem behavior that followed.

The cost follows filesystem work, not target count

A fresh symlinked sandbox performs work proportional to the filesystem entries it must materialize. Ordinary file input paths require link creation, while their directory prefixes require directory creation. The special input forms described above still require their own filesystem entries. Teardown must remove those entries again. The number of BUILD targets is therefore the wrong denominator: one action with a very large toolchain or runfiles closure can cost more to stage than many small actions.4

The useful cost model is:

  1. compute the action's input mapping.
  2. create or reconcile directories and links.
  3. execute the tool while it reads through those links.
  4. extract outputs and clean or retain the sandbox directory.

Steps 2 and 4 are metadata-heavy. Their wall time depends on input-tree shape, filesystem metadata latency, platform behavior, and whether a reusable sandbox already contains compatible entries. Do not turn the linear operation count into a universal latency claim. The sandboxfs history is a useful warning: on real macOS builds, symlink creation was not always the dominant cost. Lost compiler-local state and I/O through the sandbox could matter more.4

Historical lesson: sandboxfs moved the cost

sandboxfs was an experimental alternative that exposed each action's input mapping through a long-lived FUSE filesystem. Bazel still had to enumerate the mapping, build a manifest, and send it to the daemon, but it avoided creating and deleting one host filesystem entry for every mapped input. File and metadata access then crossed the userspace filesystem while the action ran. The design therefore moved work from view materialization into the control path and per-access I/O; it did not make sandboxing independent of input-tree shape.4

That trade can favor a large declared tree that the tool touches sparsely and lose for metadata-heavy or full-tree access. In practice, operation and maintenance complexity combined with workload-dependent performance, and Bazel removed the integration. There is no current sandboxfs strategy or replacement flag to enable. Current Bazel uses its maintained sandbox backends with physical input trees, while directory reuse reduces avoidable setup for sandboxed non-worker actions.1,5,6 Treat sandboxfs as a design experiment: measure where a filesystem-view implementation pays, rather than assuming that fewer symlinks means a faster action.

Reuse reconciles a previous tree

In Bazel 9.0, --reuse_sandbox_directories defaults to true for sandboxed non-worker execution. Reuse does not mean handing the next action an unchanged directory. Bazel may stash an old sandbox, remove entries the new action does not need, keep links that already point at the correct backing files, and create the missing remainder. The optimization reduces avoidable setup work while the new action still receives its own declared input mapping.5

Reuse is a host optimization, not a declaration fix, and its benefit depends on the workload. Workers have a separate lifecycle and sandboxing model, so the flag's non-worker scope matters.5 When setup cost becomes the incident, continue with 5.8.2 Diagnosing Sandbox Issues for the profile-first comparison, the exact reuse experiment, and sandbox-base tuning. Use the mechanism above as the hypothesis those measurements can confirm or reject.

Know when this model stops applying

This article describes the ordinary symlinked per-action sandbox. It does not claim that every Bazel filesystem view uses this representation. The experimental hermetic Linux sandbox can stage ordinary input files with hard links, copying across filesystems when necessary. Persistent and multiplex workers have distinct directory lifetimes. Remote executors construct input roots using their own storage and isolation mechanisms.5

key takeaway

A per-action symlink forest is an execroot-shaped view in which ordinary file inputs map from declared exec paths to backing files in Bazel's shared execroot. It stages the action's view. The concrete sandbox backend supplies the stronger host restrictions.

Diagnose its performance by filesystem entries and lifecycle work, not targets. On Bazel 9, directory reuse is already the default for sandboxed non-worker execution and reconciles old contents with the new input mapping. Its benefit is workload-specific. The historical sandboxfs experiment reinforces the same rule: filesystem virtualization moves setup work into mapping and access paths rather than eliminating it. The later diagnostic workflow tests whether setup is material before changing execution policy.

Check your understanding · 3 questions

1.Match each sandbox layer to the role it plays for one action:

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

Answers
Shared execroot
Per-action symlink forest
OS-specific sandbox backend

2.Which observation is the best reason to investigate symlink-forest setup cost?

Select one answer

3.Assess these claims about sandbox-directory reuse in Bazel 9:

Choose True or False for each sentence

Reuse defaults on for sandboxed non-worker execution.
Reuse gives the next action the previous action's tree unchanged.
Reuse can keep correct links while removing or replacing incompatible entries.
Enabling reuse repairs missing input declarations in an action.
0 of 3 answered

Footnotes

  1. Sandboxingprocesswrapper-sandbox symlink setup, OS-specific enforcement, output extraction, cleanup, and reuse guidance 1 2 3 4

  2. Output Directory Layout — the shared execroot and the contract that sandboxed actions run in a directory that mimics it 1 2

  3. What are Bazel's strategies? — sandbox lifecycle and the separation between strategy choice and action semantics

  4. Whatever happened to sandboxfs? — per-action symlink-forest construction, syscall cost, and evidence that setup was not always the dominant macOS penalty 1 2 3 4

  5. Command-Line Reference — Bazel 9 defaults and scope for --reuse_sandbox_directories, plus the hermetic Linux sandbox's hard-link/copy exception 1 2 3 4

  6. Bazel — core implementation, documentation, and regression corpus — upstream removal of the sandboxfs integration and the maintained strategy surface