sbe-core 0.4.0

Core library for sbe — cross-platform sandbox executor for supply chain defense
Documentation

sbe — Sandbox Exec

Run package managers and build tools with a kernel-enforced filesystem, process, and network policy. SBE supports macOS Seatbelt/SBPL and Linux Landlock plus seccomp.

# macOS: strict domain-filtered proxy mode
sbe run -- cargo build

# Linux: the current kernel can only enforce the proxy destination port.
# This explicit compatibility option acknowledges that limitation.
sbe run --allow-insecure-linux-network -- cargo build

SBE is designed for repositories, dependencies, build scripts, compiler plugins, and install hooks that may be malicious. The kernel and the installed SBE executable remain trusted.

Security boundary

SBE 0.4 fails closed when a requested guarantee cannot be enforced.

Capability macOS Linux
Filesystem writes SBPL allowlist Landlock allowlist
Secret-path reads SBPL deny rules Descriptor-based read rules with denied descendants carved out
Executable paths SBPL allow/deny rules Landlock allowlist; broad privilege-bearing directories are rejected
Ambient environment Cleared, then rebuilt from a small positive allowlist Same
Ambient file descriptors CLOEXEC before target exec close_range(CLOEXEC) with bounded fallback
Domain-filtered HTTPS Enforced through the exact authenticated proxy port Not currently enforceable; strict mode refuses to run
Restricted TCP/UDP SBPL network policy Landlock TCP plus seccomp Internet datagram/raw-socket rules
Same-user signals and Unix sockets SBPL signal/network rules Landlock ABI v6 signal and abstract-socket scopes; ABI v9 pathname-socket mediation
Persistent W^X Validated before launch Validated before launch, including existing symlink aliases
Private temporary storage Per-run root; other shared temp roots denied Per-run root; other temp paths omitted from Landlock grants
Violation audit stream Reported unavailable unless a correlatable source exists Reported unavailable until kernel-domain correlation is verifiable

Important Linux distinction: Landlock ABI v4 can authorize a destination port, not a destination IP address. A malicious process that knows the proxy's random port can connect to a different host listening on that port. SBE therefore does not call this domain confinement. A proxy profile either:

  • refuses by default; or
  • runs only after --allow-insecure-linux-network, with a warning explaining the bypass.

Linux intentionally exposes only curated public procfs nodes such as /proc/cpuinfo; it does not grant /proc/self. A rule opened for the launcher's /proc/self inode would not follow spawned descendants to their different proc inodes and would create a false guarantee. On kernels before Landlock ABI v6, same-user signal and abstract Unix-socket isolation is unavailable; pathname Unix-socket mediation requires ABI v9.

--allow-all-network remains an explicit request to remove network isolation. --no-proxy selects direct-TCP-443 compatibility mode and is never described as domain-filtered. On Linux it also permits Internet datagram sockets so libc can resolve DNS; use proxy mode when UDP egress must remain blocked.

See the security hardening design for the threat model, findings, and adversarial verification plan.

Install

cargo install sbexec --locked

For development from this repository:

cargo install --path apps/cli --locked --force

Requirements:

  • the current Rust stable toolchain for source builds;
  • macOS with /usr/bin/sandbox-exec; or
  • Linux 5.13 or newer for Landlock filesystem enforcement, with Landlock ABI v4 or newer required for TCP destination-port enforcement.

No exact Rust patch version is pinned. CI installs stable.

GitHub Actions

Release 0.4.0 and newer publishes SHA-256 checksums, an SPDX SBOM, and GitHub build-provenance attestations. The composite action verifies both the checksum and the attestation before installing the binary. The action defaults to the audited 0.4.0 release; pass version: latest only when intentionally opting into automatic release upgrades.

permissions:
  contents: read

steps:
  - uses: actions/checkout@<full-commit-sha>
  - uses: tyrchen/sbe@sbexec-v0.4.0
    with:
      version: '0.4.0'
  - run: sbe --version
  - run: sbe run --allow-insecure-linux-network -- cargo build

For high-assurance workflows, replace the SBE release tag in uses: with the full commit SHA belonging to that tag. Supported release targets are x86_64-unknown-linux-musl, aarch64-unknown-linux-musl, and aarch64-apple-darwin.

The action accepts 0.4.0, v0.4.0, or sbexec-v0.4.0 and reports the resolved sbexec-v0.4.0 tag through its version output. Releases older than 0.4.0 are rejected because they do not provide the required checksum and provenance artifacts.

Quick start

# Detect the ecosystem from the command or project files
sbe run -- npm install
sbe run -- cargo build

# Linux proxy compatibility (required for current networked defaults)
sbe run --allow-insecure-linux-network -- npm install

# Explicit profile
sbe run --profile python -- uv build

# Add or remove domains
sbe run --allow-domain api.example.com -- npm install
sbe run --deny-domain github.com -- npm install

# Permit a build-time downloader and its destination
sbe run --allow-fetch downloads.example.com -- cargo build

# Inspect the effective policy without executing the command
sbe inspect -- cargo build

# List built-in profiles
sbe profiles

Environment grants

The child does not inherit the complete parent environment. SBE keeps only a small CLI baseline such as PATH, HOME, locale, terminal, and selected tool home variables. Credential variables and agent sockets are absent unless the user grants them explicitly:

sbe run --keep-env MY_REQUIRED_TOKEN -- cargo build
sbe run --env BUILD_MODE=release -- cargo build

Proxy, temporary-directory, and SBE-controlled build-output variables are reserved and cannot be replaced through configuration or CLI flags. inspect prints effective variable names and origins, but all values are redacted.

On current stable Cargo, SBE keeps final Rust artifacts in $PWD/target while placing intermediate artifacts and executable build scripts in the private per-run tree through CARGO_BUILD_BUILD_DIR. This preserves persistent W^X without discarding the final build output. Commands that execute target artifacts—including cargo test, cargo run, and cargo nextest—instead use the private executable CARGO_TARGET_DIR.

Node dependency installation keeps node_modules writable but non-executable. Commands that explicitly run already-installed tools, such as npm test, npm exec, npx, and corresponding Yarn/pnpm/Bun forms, switch the built-in dependency-tree grant to read/execute and remove its write grant for that invocation. Modern Yarn PnP installs pre-create .pnp.cjs (and the optional ESM loader when configured) only when .yarnrc.yml or packageManager identifies a PnP-capable Yarn; Classic Yarn and the node-modules linker remain lockfile-only. Mutating Bun commands receive only their own bun.lock output, plus yarn.lock when Bun's --yarn compatibility output is requested. SBE walks bounded, no-follow workspace metadata up to the Git boundary, including workspace roots between the current directory and repository root, and grants/pre-creates outputs at exactly one active Node workspace root. npm --no-package-lock and --package-lock=false never create an empty lockfile. Package-manager options that relocate the project (--prefix, --cwd, --dir, --directory, --project, and equivalent short forms) are rejected before policy preparation; change directory before running SBE instead.

Python installation and synchronization commands similarly keep project .venv/venv directories writable but non-executable. Run/test commands, including uv run, poetry run, activated entry points, and direct .venv/bin/... paths, switch those built-in grants to read/execute without write. This mode is for an already-installed environment; synchronize it in a separate invocation before running tools.

Persistent outputs and package caches are still attacker-controlled data after an untrusted build. W^X prevents direct execution during that invocation; it does not make generated binaries, scripts, dynamic libraries, or interpreted packages trustworthy. Review or discard them before running them outside SBE.

Stdin, stdout, and stderr are intentional capabilities. For example, sbe run -- tool < secret.txt explicitly gives that file's contents to the tool even though all other inherited descriptors are closed.

Configuration trust

Configuration sources are merged in this order:

  1. built-in platform/ecosystem policy;
  2. ~/.config/sbe/config.yaml;
  3. an auto-discovered project .sbe.yaml or .sbe.yml;
  4. an explicitly named --config file; and
  5. CLI grants.

An automatically discovered project file is untrusted. By default it may add denyRead, denyExec, or denyDomains, and may turn off a previously granted allow-all/degradation setting. It may not add read, write, execute, network, environment, fetch, or degradation authority.

Review a repository before granting expansion for one invocation:

sbe inspect --trust-project-config -- npm install
sbe run --trust-project-config -- npm install

Global, explicitly named, and CLI policy are trusted user choices. All schemas reject unknown fields, oversized input, invalid environment names, malformed IDNA domains, unsafe paths, missing bases, and cyclic extends chains.

Example trusted configuration:

profiles:
  node:
    allowWrite:
      - "$PWD/dist/"
    allowDomains:
      - "api.example.com"
    denyDomains:
      - "github.com"
    allowFetch:
      - "downloads.example.com"
    env:
      NODE_ENV: production

Every permission-bearing resolved entry retains its built-in, global, project, explicit-file, CLI, parent-environment, or runtime origin.

Filesystem and process policy

Built-in profiles grant specific outputs and lockfiles rather than the entire working tree. Source, .git, workflow definitions, manifests, and SBE policy remain non-writable unless explicitly granted. Shared system temporary roots are not writable; every invocation gets a canonical private root used for TMPDIR, TMP, TEMP, and XDG_RUNTIME_DIR.

Persistent write and execute grants may not overlap. The only default W+X exception is the private per-run root, which is deleted when the invocation finishes. Toolchains such as ~/.rustup are executable/readable but not writable. Mutable caches are readable/writable but not executable. Before launch, SBE also rejects a writable regular file unless all of its hard-link pathnames are contained in writable roots. This prevents a writable cache alias from mutating executable tools, source, workflows, or any other protected path. Hard links wholly contained in writable roots remain valid.

On Linux, paths are opened with descriptor-relative openat2 resolution using RESOLVE_BENEATH, RESOLVE_NO_SYMLINKS, and RESOLVE_NO_MAGICLINKS. Writable directories are created component by component with directory FDs. Root-owned immutable distribution symlinks are the only symlink exception. Because Landlock authorizes inodes rather than pathnames, SBE fails closed if a denied regular file—or a file below a denied directory—has multiple hard links.

On macOS, secret read denials include both the configured pathname and its canonical target. A symlinked ~/.ssh, .aws, or similar protected directory therefore cannot escape the Seatbelt deny rule through pathname resolution.

Hex 2.5.x currently extracts packages through an unpredictable tmp_* directory in the project root and exposes no temp-directory setting. Strict SBE therefore blocks mix deps.get rather than granting project-wide write access. Fetch dependencies before entering SBE, then run sbe run -- mix compile; dependency build code still executes inside the sandbox.

Network proxy

The local CONNECT proxy:

  • requires a random 256-bit per-run Basic-authentication token;
  • accepts only CONNECT over HTTP/1.0 or HTTP/1.1;
  • bounds request lines, individual and total headers, header count, concurrent connections, resolved addresses, and task lifetime;
  • enforces total header, DNS, connect, aggregate bidirectional idle, and maximum tunnel timeouts;
  • canonicalizes lowercase IDNA names and matches wildcards on label boundaries;
  • permits port 443 by default;
  • rejects IP literals and any resolution containing loopback, private, link-local, unspecified, multicast, documentation, or other selected special-use addresses; and
  • resolves once and connects only to a validated returned address.

If the proxy exits while the sandboxed command is alive, SBE terminates the command instead of silently leaving it without the requested mediator.

JVM profiles receive SBE-owned proxy system properties and a private per-run authentication agent. The agent reads the one-time token from a reserved environment variable and answers authentication challenges only for SBE's exact loopback proxy endpoint. The token is deliberately excluded from JAVA_TOOL_OPTIONS, which the JVM prints at startup. Maven and sbt therefore use the same domain allowlist as the other ecosystems. Gradle daemon IPC may require additional local-service authority and is not enabled broadly by default.

Linux launcher

Linux policy installation does not run complex Rust code in a multithreaded pre_exec closure. The parent serializes a bounded policy into an anonymous descriptor and starts an internal single-threaded launcher before constructing any runtime in that process. The launcher:

  1. parses and revalidates the policy;
  2. safely opens Landlock rule paths;
  3. applies PR_SET_NO_NEW_PRIVS, Landlock, and seccomp;
  4. marks ambient descriptors close-on-exec; and
  5. executes the target.

A dedicated status descriptor distinguishes launcher setup failure from the target itself returning exit code 126.

Limitations

  • Linux has no strict domain-egress backend yet. The explicit port-only mode is bypassable and should be combined with a network namespace, firewall, or trusted CI egress controls when the repository may be malicious.
  • On Linux kernels before Landlock ABI v4, restricted TCP modes refuse unless the same insecure compatibility option is supplied; in that case TCP confinement is unavailable and SBE says so.
  • Path-based Unix-socket mediation depends on newer Landlock ABIs. Local IPC is a separate capability from Internet egress.
  • macOS retains an allow-most/read-deny model for compatibility. The curated secret paths, Keychain service, shared temp roots, environment, and inherited descriptors are protected, but this is not a complete home-directory read allowlist.
  • Correlated kernel violation streaming is currently reported as unavailable. Ordinary denials still surface as EACCES, EPERM, or command failure.
  • SBE does not defend against kernel vulnerabilities, a compromised SBE binary, CPU/memory exhaustion by the target, timing/cache side channels, or explicit capabilities supplied through stdio.

Supported ecosystems

Ecosystem Commands Project files
Node.js node, npm, npx, yarn, pnpm, bun package.json
Rust cargo, rustc, rustup Cargo.toml
Python python, pip, uv, poetry, pdm, rye pyproject.toml, requirements.txt, and related files
Elixir mix, elixir, iex mix.exs
Java/Scala java, javac, mvn, gradle, sbt, scala Maven, Gradle, and sbt descriptors

CLI

Run sbe run --help and sbe inspect --help for the complete current reference. Security-relevant options include:

--allow-insecure-linux-network  Explicit port-only Linux compatibility
--allow-all-network             Remove network confinement
--no-proxy                      Direct-TCP-443 compatibility
--trust-project-config          Let auto-discovered project policy add grants
--keep-env NAME                 Preserve one parent variable
--env NAME=VALUE                Add one explicit variable
--allow-write PATH              Add a writable path
--deny-read PATH                Add a protected read path
--allow-exec PATH               Add an executable path
--allow-fetch DOMAIN            Add downloader execution and proxy destination
--dry-run                       Render policy without target execution

SBE passes through normal target exit codes. Its own errors use 125; sandbox setup/exec failure uses 126.

Development

cargo fmt --all -- --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace --all-features
cargo audit
cargo deny check advisories bans licenses sources
cargo machete

The release workflow uses the Rust stable channel, pinned third-party action SHAs, least-privilege job permissions, draft-first publication, SHA-256 checksums, SPDX SBOMs, and GitHub artifact attestations.

License

MIT. See LICENSE.md.

Copyright 2025–2026 Tyr Chen