DevSecOps

Buck2 (Meta) Build Security Considerations

A security engineer's look at Buck2, Meta's open-source build system, including Starlark sandbox properties, remote execution, and actual supply chain guarantees.

Shadab Khan
Senior Security Engineer
7 min read

Meta open-sourced Buck2 in April 2023, and I spent the following year evaluating it as a potential Bazel alternative for a large client's mobile and backend monorepo. The evaluation surfaced a set of security trade-offs that are worth documenting, because Buck2's security story is both better than commonly claimed in some dimensions and weaker in others.

This is a frank look at what Buck2 gives you on the supply chain front, what it doesn't, and how that compares to the alternatives. I am writing from the perspective of someone who shipped a production migration, not a benchmark paper.

Buck2's Architecture and Why It Matters

Buck2 is a Rust rewrite of Buck (the original Java build system Meta has used since roughly 2013). The Rust rewrite matters because it changes the trust boundaries. Buck's .buckconfig parser, expression evaluator, and action scheduler were all Java, which meant a process with access to large parts of Meta's internal JVM stack. Buck2 replaces this with a single Rust binary and delegates rule logic to Starlark -- the same restricted Python-like language Bazel uses.

From a supply chain perspective, the Starlark choice is significant. Starlark is deterministic by design: no file I/O from rules, no network access, no reading environment variables. The rule authors cannot accidentally create non-hermetic builds through random file reads, because the language simply doesn't let them. This is stronger than Bazel's effective guarantee, because Bazel rules can shell out to Python via ctx.actions.run in ways that escape the sandbox.

The Sandbox Model

Buck2 runs every action in a sandbox by default. On Linux, this uses unshare(2) to create a new mount, PID, and network namespace. On macOS, it uses sandbox-exec (Apple's deprecated-but-still-functional sandbox framework). On Windows, sandboxing is more limited -- Buck2 relies on file path isolation rather than OS-enforced namespaces.

The relevant config knobs live in .buckconfig:

[build]
execution_platforms = prelude//platforms:default

[buck2]
allow_eden_io = false
file_watcher = notify

[sandbox]
default = true
default_allow_network = false

allow_eden_io is a Meta-internal filesystem integration that external users should leave disabled. default_allow_network = false is the critical one for supply chain: without it, an action can reach out to an internal package mirror or a remote CI cache, which is how exfiltration attacks against build systems work.

Buck2's remote execution protocol speaks REAPI (the same spec Bazel uses), so the same supply chain concerns apply: a shared remote cache is a write-through attack surface. Segregate cache buckets by trust tier and disable cache writes from untrusted branches.

Bxl, and Why It Worries Me

Buck2 ships with a feature called Bxl ("Buck extension language") that lets you write Starlark scripts that introspect the build graph, query targets, and produce arbitrary outputs. It is a genuinely powerful tool -- we used Bxl to generate ownership reports, compliance attestations, and dependency graphs.

The security concern with Bxl is that it runs with broader privileges than a normal build rule. Bxl scripts can invoke subprocesses, read files outside the sandbox, and call into the REAPI. If you commit a malicious Bxl script and a developer runs buck2 bxl //some:script, you have a local code execution primitive against that developer's machine.

Mitigations:

  1. Treat Bxl scripts as security-sensitive. They belong in a dedicated directory (bxl/ at the repo root) with stricter code owners than BUCK files.
  2. Use buck2 bxl --fail-fast with allowlists rather than open invocation from CI.
  3. Static-scan Bxl scripts for subprocess invocations before merge. We use a pre-commit hook that greps for ctx.bxl.actions.run in new Bxl files.

None of this is documented in the Buck2 README as a security best practice, which is part of why I am writing this post.

Third-Party Dependencies and External Cells

Buck2's equivalent of Bazel's MODULE.bazel is the concept of "external cells." A cell is a directory with its own .buckconfig that can be referenced from another cell via a cell_path. External cells let you pull in third-party code -- typically a vendored copy of rules_* or a third-party dependency set.

Unlike Bzlmod, Buck2 does not ship a central registry or enforce integrity hashes on external cells. If you reference an external cell via a git submodule, the trust boundary is the submodule's commit hash. If you reference a released tarball, there is no built-in verification that the tarball matches an expected SHA256.

This is a regression from Bazel's current model. We worked around it by vendoring all third-party dependencies into the monorepo under a third-party/ cell, with each dependency's source tree committed directly. Every upstream update goes through a dedicated PR with a security review. This is heavier than Bazel's registry-based model but gives us a single point of audit.

For the Rust dependencies, we use buck2-rust-third-party, which wraps cargo vendor output into Buck2 targets. The wrapper produces a Cargo.lock equivalent with all crates pinned by version and hash; we feed this into Safeguard for vulnerability analysis.

Reindeer and Rust Supply Chain

Meta's reindeer tool (open-sourced 2022) converts Cargo manifests to Buck2 targets. It honors Cargo.lock hashes, so crates pulled in through reindeer are integrity-checked. This is the only place in the Buck2 ecosystem where I have seen hash-checked third-party inputs as a first-class feature, and it is limited to Rust.

For Python, Go, and Node dependencies, you are on your own. The prelude rules for pip_parse and go_module do not enforce integrity by default. In .buckconfig:

[python]
pip_hash_check = required

[go]
module_verification = strict

These options exist but are off by default. Turn them on, and accept the setup overhead.

Reproducibility in Practice

Buck2 builds are reproducible in the sense that running the same action with the same inputs produces the same output. They are not reproducible in the sense that the Debian Reproducible Builds project uses -- bit-for-bit identical output across arbitrary machines and times is possible but requires discipline.

Common sources of non-reproducibility we encountered:

  • Timestamps embedded in JAR files. Fixed by setting SOURCE_DATE_EPOCH in the action environment and using --jvm-arg=-Duser.timezone=UTC.
  • Non-deterministic protoc plugin ordering. Fixed by pinning plugin versions in BUCK files rather than relying on PATH lookup.
  • Go build IDs that include build timestamps. Fixed with -buildid="" and -trimpath linker flags.

Once these were stamped out, we reached roughly 99.2% reproducibility across the Linux subset of our monorepo. The remaining non-reproducible builds were all in third-party code we did not control.

Remote Execution Trust

Buck2's REAPI implementation talks to whatever backend you point it at -- BuildBuddy, BuildBarn, Bazel Remote, or Meta's internal Sandcastle. Every backend has its own security posture. We run BuildBarn in a private VPC with mTLS between clients and the scheduler. The worker pool runs on dedicated nodes that do not host any other workloads, with read-only rootfs and ephemeral scratch space.

The single most important remote execution setting is execution_platforms. A compromised execution platform can return arbitrary outputs that look legitimate to clients. Treat your REAPI cluster as a production system: rotate credentials, audit access, and monitor for unexpected action digests.

How Safeguard Helps

Safeguard ingests Buck2 build outputs and the buck2 audit providers dependency graph to produce CycloneDX 1.5 SBOMs that capture the full set of external cells, reindeer-managed crates, and vendored third-party code. Policy gates can block Buck2 builds that introduce dependencies missing hash verification or pull from untrusted REAPI backends. For teams using Bxl for compliance tooling, Safeguard verifies Bxl outputs against expected schema and flags Bxl scripts that invoke unapproved subprocesses. The combination extends Buck2's deterministic execution model with the external governance and vulnerability visibility that the tool itself does not provide.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.