Let's be direct: cargo-cage 0.1.0-alpha.3 is experimental. It adds a useful Linux
boundary around Cargo, but it is not a complete sandbox and it does not come
with a promise that hostile code can never escape. If you need a hard,
independent trust boundary, use a VM or a dedicated build service as well.
The intended boundary and the assumptions behind it live in THREAT_MODEL.md.
The reference platform is Ubuntu 24.04 x86_64 with Bubblewrap 0.12.0 or
newer, unprivileged user namespaces enabled, and a host AppArmor policy that
allows Bubblewrap to set up those namespaces. Other Linux distributions may
work, but they are experimental. macOS and Windows are not supported.
Bubblewrap versions below 0.12.0 are rejected because they are outside the
supported baseline after the upstream
GHSA-pxhw-h44j-8pfx setup vulnerability.
The Linux backend mounts a small read-only runtime, the workspace, checked toolchain paths, and checked Cargo caches. The host root is not mounted as one giant read-only tree. Each command gets a new writable target run below target/.cargo-cage/runs/; the workspace Cargo.lock is a separate persistent writable file. /tmp, /var/tmp, and /run are private.
Host mount sources are opened with fd-based, symlink-resistant resolution and passed to Bubblewrap with --ro-bind-fd or --bind-fd. After the namespace and mounts exist, a small internal Rust launcher applies a deny-by-default Landlock policy before it starts Cargo. Landlock ABI 5 is a hard requirement. When newer ABI features are available, pathname Unix-socket and scope restrictions are enabled as an additional layer.
Network access is denied twice: Bubblewrap gets a separate network namespace, and Cargo is forced into offline mode. There is no automatic fetch.
Every Bubblewrap process is also started from a private delegated cgroup-v2 child. The default budget is 512 processes, an 8 GiB memory ceiling clamped to 75% of available host memory and the parent cgroup, four CPU cores, 30 minutes of wall-clock time, a 4 GiB per-file limit, and 16,384 open descriptors. An effective memory budget below 1 GiB is rejected. Swap is disabled for the build cgroup and core dumps are disabled. If cgroup-v2 delegation or cleanup cannot be proved, the build stops before Cargo. There is no CLI switch that disables or raises this profile; smaller explicit budgets are for embedding and tests only.
The child starts with an empty environment. A fixed allowlist supplies the
Cargo/Rust, compiler, locale, and terminal values needed for normal builds.
Credentials, agent variables, CARGO_HOME, RUSTUP_HOME, and arbitrary host
variables do not cross the boundary. Policy removals win over later
environment values. Standard stdio is kept for normal Cargo behaviour; extra
inherited file descriptors are closed before the build process starts by a
small fixed /bin/bash scrubber. If Bash is missing, setup fails closed.
Cargo gets a private CARGO_HOME. Only existing registry and git caches
are considered, and only after their roots and contents pass validation.
User/global Cargo config and credentials are intentionally not mounted. A
project-local .cargo/config.toml is still part of the workspace input; it is
not a trust signal.
Before mounting, the backend checks paths, types, overlaps, and canonical
resolution. Nested mountpoints, sockets, device nodes, FIFOs, external
symlinks, and hardlink aliases leaving a validated tree stop the operation.
Hardlinks Cargo keeps entirely inside target remain usable.
The selected Rustup compiler is resolved before execution. Project path overrides outside trusted Rustup toolchains or the system runtime are rejected, and missing toolchains are not installed automatically. Child processes, procedural macros, test binaries, linkers, and compiler helpers inherit the same Bubblewrap boundary.
There is no unsandboxed fallback. If Bubblewrap is absent, too old, not executable, cannot complete its preflight, or Landlock/openat2 cannot provide the required policy, the build stops.
The crates.io release workflow is prepared for Trusted Publishing with GitHub
OIDC. Each of the four public crates must be bound to this repository, this
workflow file, and the protected crates-io environment. Once configured, the
workflow receives only a short-lived publishing token after the full CI gate
and package inspection have passed. The root crate is dry-run before
authentication; dependent crates are dry-run without credentials after their
published dependencies become visible and before their own upload. A long-lived
CARGO_REGISTRY_TOKEN secret is not used, and there is no secret fallback if
OIDC authentication fails.
Future GitHub releases must be created as drafts and published with release immutability enabled. After publication, the tag and release assets must not be moved or edited. GitHub creates a release attestation for an immutable release; that attestation describes provenance and integrity of the release, not the security of the sandbox or the correctness of its code. The current alpha intentionally ships source archives only, so it does not claim binary artifact attestations.
The Landlock launcher is deliberately context-bound: Bubblewrap creates a private marker before the launcher is reached, and the launcher rejects direct use without that marker as well as filesystem-root policy paths. The launcher is an implementation detail, not a second public sandbox interface.
Use cargo-cage build, not cargo cage build, when this boundary matters.
Cargo processes [alias] entries before it launches external subcommands. A
workspace can define an alias named cage, and then Cargo may never launch
cargo-cage at all. No code in this repository can detect a process that was
never started. This is a Cargo dispatcher limitation, not a Bubblewrap escape.
Cargo is tracking the upstream fix in
issue #10049.
Please do not put a working sandbox escape or secret-bearing proof of concept in a normal public issue.
Use GitHub's private vulnerability reporting flow. Otherwise, email security@nicdevtv.de.
A useful report includes the Linux distribution, kernel and architecture,
Bubblewrap version, cargo-cage version, a minimal reproduction, and whether
the problem involves build.rs, a procedural macro, a compiler helper, or a
child process. Please give us a reasonable chance to investigate before
publishing details.
This release deliberately has no Seccomp, disk-space quota, or syscall audit log. Resource limits reduce process, memory, CPU, file-size, descriptor, and wall-clock abuse, but they do not guarantee that a build cannot consume all available disk space. The project also does not defend against kernel, Bubblewrap, Cargo, Rustc, toolchain, or host-policy vulnerabilities. Side channels, every possible secret exposure, and all races caused by another local process changing paths while setup is in progress remain out of scope.
The default target run is retained below target/.cargo-cage/runs/ and remains untrusted. --reuse-target is an explicit trusted-workspace exception and restores the older cross-build artifact risk. Landlock also has kernel-defined limits; it is an additional access-control layer, not a replacement for Bubblewrap or a promise that every filesystem operation is observable.
The workspace and selected runtime/toolchain files are readable by design.
Data written to target is untrusted, and generated artifacts are not made
safe to execute automatically.
The validation pass is intentionally conservative and repeated for each Bubblewrap process. This can be noticeable on large workspaces and retained target trees; it is a reliability/security trade-off, not a completeness claim.
Dependency updates are checked weekly by Dependabot. The repository also runs
RustSec's cargo audit workflow on dependency changes and on a weekly
schedule. A clean audit only covers advisories known to RustSec at the time of
the run; it is not a security guarantee for Bubblewrap, the kernel, Rust, or
the dependency code itself.