containerization-framework 0.2.0

Rust bindings for Apple's Containerization framework: Linux containers in lightweight VMs, in-process and without a daemon.
docs.rs failed to build containerization-framework-0.2.0
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.

containerization-framework

Rust bindings for Apple's Containerization framework: Linux containers.

use containerization_framework as cfw;
use cfw::model::{Dns, LinuxProcessConfiguration, NatInterface, VmResources};

let cache = std::path::Path::new("/Users/me/.cache/containers");
let store = cfw::Store::at(
    cache,
    cache.join(format!("vmlinux-{}", cfw::KERNEL_VERSION)),
    cfw::INITFS_REFERENCE,
    cache.join(format!("vminit-{}.ext4", cfw::INITFS_VERSION)),
);
let session = cfw::Session::new(store);

let mut spec = cfw::BootSpec::new("example", "docker.io/library/debian:stable-slim");
spec.vm = VmResources { cpus: 4, memory_in_bytes: (4 << 30) + VmResources::GUEST_MEMORY_OVERHEAD };
spec.configuration.cpus = 4;
spec.configuration.memory_in_bytes = 4 << 30;
spec.configuration.process = LinuxProcessConfiguration::new(&["/bin/sleep", "infinity"]);
spec.configuration.interfaces = vec![NatInterface::new("192.168.64.7/24", "192.168.64.1")];
spec.configuration.dns = Some(Dns { nameservers: vec!["192.168.64.1".into()], ..Dns::default() });

session.boot(&spec)?;

let code = session.exec(
    "example",
    "hello",
    &LinuxProcessConfiguration::new(&["/bin/echo", "hello"]),
    cfw::Stdio::inherit(false),
)?;

A container belongs to the process that booted it and dies with it. Nothing lists containers, though other processes may join running ones.

Requirements

  • macOS 26 on Apple silicon, and Xcode 26 to build.
  • Network on a first build: the build script compiles the bundled Swift package, which resolves Containerization and its dependencies through SwiftPM. Versions are pinned by the Package.resolved that ships with this crate.

On non-macOS platforms, this crate compiles but returns errors on every call.

Codesigning

A binary using this crate must carry the com.apple.security.virtualization entitlement. Without it Virtualization.framework refuses to start a VM, and Session::boot fails saying so.

A containerization.entitlements file ships with this crate; binaries compiled against containerization-framework should pass it, or a copy of it, to codesign after compilation.

cargo build --release
codesign --force --sign - --entitlements containerization.entitlements \
  target/release/your-binary

Signing ad hoc (--sign -) satisfies the entitlement but gives the binary a new code identity on every rebuild, so anything keyed to that identity — Keychain access, for one — prompts again. Sign with a development identity to keep it stable.

A rebuild drops the signature, so this runs after every build.

Linking

The Swift runtime this links against is dynamic and referenced as @rpath/libswift_Concurrency.dylib, which dyld resolves against /usr/lib/swift in macOS. Anything that links this crate — a binary of yours, and the test binaries of any crate of yours that links it — needs that rpath, or it links and then dies in dyld at launch.

A build script's link arguments reach only its package's targets, so the rpath belongs in .cargo/config.toml, where a rustflag covers every kind of target:

[target.'cfg(target_os = "macos")']
rustflags = ["-C", "link-arg=-Wl,-rpath,/usr/lib/swift"]

Shape

  • [Store] is an image store on disk, in Containerization's layout.
  • [Builder] provisions it (a kernel and the vminitd init image) and turns a [BuildPlan] into an image: pull the base, unpack it to a writable ext4 block, boot it, run each step, and store the result as a single-layer image. No daemon, no builder image, no Dockerfile.
  • [Session] boots a [BootSpec]'s container from an image and runs processes in it.

Configuration types in model mirror Containerization's (LinuxContainerConfiguration, LinuxProcessConfiguration, Mount, ...), with the same names and defaults, so its documentation applies. One difference: fields the image seeds (arguments, working directory, user) are Options, and None keeps the image's.

Build caching is by rootfs snapshot rather than by layer: a rebuild resumes from the deepest step whose cache_key still matches. This crate only stores and compares them -- caching, cache invalidation, etc. are the responsibility of callers.

Unimplemented

The framework is larger than these bindings. Not exposed: signals to a guest process, LinuxPod (several containers in one VM), container statistics, filesystem freeze/thaw/trim, host↔guest file copy, registry authentication and push, OCI layout import/export, per-process rlimits and capabilities, and Rosetta (so no linux/amd64 — arm64 only).

An OCI runtime (and so seccomp) is configurable, but requires an init image with runc, which Apple does not publish. Pass one's reference to Store::at.

Versioning

The Containerization release and the kernel are pinned separately, and a mismatch fails at runtime rather than at build time. Session::version() names both.

The pins are public (KERNEL_VERSION, KERNEL_URL, INITFS_VERSION, INITFS_REFERENCE). As in Containerization, the caller chooses where the kernel and unpacked init image live (Store::at). Provisioning fills empty paths and leaves existing files alone, so name paths for the pinned versions, as above, to pick up upgrades. Another init image must carry the pinned release's vminitd.

Testing

cargo nextest run runs the unit tests.

The suite in tests/ boots real containers, so it sits behind the integration feature and runs through bin/dev/test-integration, which signs each test binary with containerization.entitlements first — the entitlement is checked against the calling process.

Those tests share an image store at ~/.cache/containerization-framework-tests, kept between runs. The first run fills it — a kernel download, the init image, and a small image built from alpine:3 — so it needs the network; later runs reuse it. This directory can be deleted.

License

MIT. Containerization itself is Apache-2.0 and is fetched at build time, not vendored here.