# pullrun

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.

```bash
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](https://github.com/rkyv/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](https://github.com/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-docker-sandbox-microvm|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 [[toolbox/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](https://doi.org/10.5281/zenodo.20679669).
