pullrun
- title
- pullrun
- type
- toolbox
- summary
- Runs one OCI image as a runc container, a Firecracker microVM, or an Apple Silicon VM off a content-addressed DAG store
- tags
- rust, golang, containers, microvm, oci, p2p, sandboxing, watchlist
- language
- Rust (runtime), Golang (CLI, CRI shim, compose)
- license
- Apache-2.0
- created
- 2026-07-29
- updated
- 2026-07-29
Pullrun takes an ordinary OCI image and runs it three ways: as a runc container, as a Firecracker microVM, or as an Apple Virtualization VM on Apple Silicon. Same image, same command, same local store β the only thing that changes is --backend. There is no separate VM image build step and no VM-specific format, because the OCI manifest is treated as the VM rootfs directly.
pullrun run alpine:3.18 --cmd "echo" --cmd "hello" # container (Linux)
pullrun run alpine:3.18 --backend vm --cmd "echo" --cmd "hello" # Firecracker microVM
pullrun run alpine:3.18 --cmd "echo" --cmd "hello" --attach -t # Apple VM (macOS default)
That is the whole pitch, and it lands on the same argument microvm-2026 makes: a container is a resource-control mechanism, not a security boundary, so the choice between shared-kernel and per-VM-kernel isolation should be a runtime flag rather than a different toolchain. Where matryoshka-isolation describes the containers-inside-VMs sandwich most platforms settle on, pullrun makes the two siblings off one store.
The DAG store
The storage design is the part worth reading. Instead of unpacking layers through overlayfs, pullrun keeps each OCI layer as-is in a content-addressed directed acyclic graph, keyed by its sha256: digest, and mmap()s blobs on read. The Rust implementation uses rkyv for zero-copy deserialization, memmap2 for the mapping, and a DashMap<Digest, Arc<Mmap>> cache so the first reader pays for the mmap() and page faults while every later reader does a single atomic load.
Two things follow from content addressing. Every node that pulled alpine:3.18 holds byte-identical files, which is what makes the peer-to-peer sync layer cheap β blocks are verified by hash on receipt, so a peer doesn't have to be trusted, only reachable. And deletion becomes a graph problem rather than a filesystem one: pullrun rmi cascade-deletes the unreachable subtree, per-node refcount sidecar files keep layers shared with another image alive until the last referrer goes, and a recompute_all_refcounts pass on daemon startup rebuilds the counts after a crash. pullrun gc sweeps whatever is no longer reachable from a tagged image or a live workload, dry-run by default, with a 90% safety guard you have to --force past.
Dropping overlayfs is also pitched as a security argument: the README names CVE-2023-0386 and CVE-2023-32629 as overlayfs escapes that a store which never builds an overlay simply cannot hit. It goes on to cite a kernel page-cache bug, CVE-2026-31431 ("Copy Fail"), as an example of a shared-kernel problem no storage driver can fix and per-VM kernels can β which is the argument for the VM backends existing at all. Both claims come from the project's own README; the second CVE has no other source given.
Backends and the numbers
| Backend | Isolation | Platform | Claimed boot |
|---|---|---|---|
| runc | namespaces | Linux, macOS, Windows (WSL2) | ~400 ms |
| Firecracker | per-VM kernel (KVM) | Linux x86_64, WSL2 x86_64 | ~500 ms cold, ~200 ms warm pool |
| Apple Virtualization | per-VM kernel (Hypervisor.framework) | macOS Apple Silicon | ~160 ms |
Alongside those, the README claims a 968 ms first pull of alpine:3.18 against Docker's ~2 s, 24.6 MiB idle daemon RSS against ~90 MiB, and ~20 MB of stripped binaries against ~75 MB. These are the project's own benchmarks, run with hyperfine on an M3 against Docker Desktop 4.27, with the script published at hack/bench.sh β reproducible in principle, unreproduced by anyone else so far. The warm-pool figure is the one to watch, since it is the same trick Firecracker snapshot-restore uses to get under the cold-boot floor described in microvm.
Behaviour after stop differs by backend in a way that matters more than boot time: an Apple Virt VM keeps its rootfs through VirtioFS and a Firecracker VM keeps its ext4 image, so both restart like hibernated machines, while a runc workload's rootfs is ephemeral and only --volume mounts survive.
Everything else in the binary
The surface is wide for a project this young. There is a native Dockerfile builder that shells out to runc for RUN and caches layers by instruction hash, so builds need no Docker daemon. A separate pullrun-compose binary parses standard docker-compose.yml through compose-spec/compose-go and can boot every service in the file as a microVM with --backend vm. Kernels are themselves OCI content: pullrun kernel install pulls a vmlinux from a registry into the DAG store, and --kernel-image points a workload at one by reference.
A policy engine gates workloads before they start β Cosign signature verification, SBOM evaluation with CVSS thresholds and license denylists, seccomp profiles, read-only rootfs, no_new_privileges β declared as required_signature: true, max_cvss_score: 7.0, deny_licenses: ["GPL-3.0"]. The P2P layer discovers peers over mDNS, gossips state, and uses Bloom filters to avoid re-sending blocks a peer already has, so a cluster pulls from the registry once and delta-syncs the rest at LAN speed. There is a Kubernetes CRI shim (beta) exposing pullrun-container and pullrun-vm RuntimeClasses, and an MCP server exposing 15 runtime operations plus four resource types, which puts it in the same tool-for-agents position as Rivet's Sandbox Agent SDK β except pullrun's API is documented rather than reverse-engineered.
What to be careful about
The repo was created on 2026-06-11 and had 114 stars, one fork and zero open issues at the end of July 2026. Every number, CVE citation and "only runtime that does X" claim in this page comes from a README written by the project itself; nothing here is independently corroborated. Contributions require signing a CLA, which is a signal about intended licensing control on an Apache-2.0 project. The control plane is an explicit v1 work-in-progress (etcd, DNS and admission control are all "planned"), the CRI shim is labelled beta, and the Rust/Golang split across eleven crates plus four Golang components is a lot of surface for what appears to be a very small team.
Tagged watchlist for that reason β see watchlist for what a re-check looks at. The storage idea is good enough to steal regardless of whether this implementation survives.
Apache-2.0, ~114β as of 2026-07-29. A technical report is archived at doi.org/10.5281/zenodo.20679669.