SporeVM is a small aarch64 virtual machine monitor for forkable Linux microVM checkpoints.
A spore is a sealed VM checkpoint with normalized machine state, device state, verified memory chunks, optional rootfs state, and a platform contract that fails closed when a host cannot restore it honestly.
The useful shape is:
- Start a runtime once.
- Warm it up until the expensive boring work is done.
- Capture it at a clean point.
- Fork cheap child spores.
- Resume the children on compatible aarch64 hosts without copying all RAM for every child.
SporeVM 1.0 expects spores to resume on the same backend and compatible host class they were captured for: KVM/aarch64 to KVM/aarch64, or Apple Silicon HVF to Apple Silicon HVF. The repo still keeps KVM/HVF restore checks because they catch backend-specific state leaking into the spore format, but users should not plan distribution around moving one running machine between those hypervisors.
- docs/spore-format.md: manifest, bundle, and invariant contract.
- docs/state-portability.md: KVM/HVF state mapping and fail-closed restore matrix.
- docs/memory.md: memory chunks, local backing, and dirty tracking.
- docs/filesystem.md: rootfs CAS and writable root disk layers.
- docs/rootfs.md: OCI image and rootfs CLI workflows.
- docs/fanout.md: fork identity and fan-out behavior.
- docs/networking.md: SporeVM-managed networking.
- docs/lifecycle.md: named VM lifecycle.
- docs/libspore.md: Zig, C, and Go embedding surface.
If you use mise, install it globally:
mise use -g github:sporevm/sporevm@latest
spore versionOr download the Linux ARM64 or macOS ARM64 archive from GitHub releases:
asset=spore_Darwin_arm64 # or spore_Linux_arm64
tar -xzf "$asset.tar.gz"
"$asset/bin/spore" versionUse spore_Linux_arm64 on Linux. Add $asset/bin to PATH, or move the
extracted directory wherever you keep standalone tools.
spore is the CLI. libspore is the embedding surface for Zig, C, and Go
callers. See docs/libspore.md for import, ownership, C ABI,
and Go binding details.
Tooling is pinned with mise:
mise install
mise run check
mise run installmise run check runs unit tests, the product build, and diff hygiene.
mise run install builds an optimized spore and installs it into ~/bin,
with runtime assets under ~/share/sporevm.
mise run smoke builds once, then runs the default product run, run-capture,
and resume smokes. Focused smoke commands are listed under
Validation.
For local iteration:
mise run build
zig-out/bin/spore versionRun one command in a throwaway VM:
spore run -- /bin/writeoutspore run uses the managed SporeVM run kernel and the embedded minimal exec
initrd.
Forward host stdin explicitly with -i when the guest process should read
input. Without -i, runs keep the script-friendly default and do not attach
host stdin:
printf 'hello\n' | spore run -i -- /bin/catAllocate a guest terminal explicitly with -t. Use -it for an interactive
shell; TTY output is a single terminal byte stream, so stdout and stderr are not
separated in this mode:
spore run -it --image docker.io/library/alpine:3.20 -- /bin/shOverride boot assets when needed:
spore run --kernel Image --initrd root.cpio -- /bin/writeoutUse spore --debug run ... for verbose VMM setup and restore logs.
Build or reuse a cached ext4 rootfs from an OCI reference, then run a shell command inside it:
spore run --image docker.io/library/alpine:3.20 'echo hi'--image applies OCI Env and WorkingDir when present. It does not apply
OCI Entrypoint, Cmd, or User. Shell commands run as /bin/sh -lc in the
guest. Use -- <argv...> when you need exact argv.
Build a reusable rootfs artifact explicitly:
spore rootfs build docker.io/library/alpine:3.20 \
--platform linux/arm64 \
--output alpine.ext4
spore run --rootfs alpine.ext4 'echo hi'See docs/rootfs.md for cache behavior, local OCI layout imports, and rootfs pruning.
SporeVM-managed networking is explicit:
spore run --net --allow-host example.com \
--image docker.io/library/alpine:3.20 \
-- /bin/wget -qO- https://example.comUse --allow-host or --allow-cidr to open egress beyond the built-in deny
floor. Captured network policy is replayed by spore run --from; omit --net
and allow flags on resumed runs.
See docs/networking.md for policy, bound-service, and resume limits.
Start a named VM with a running process:
spore create counter --image docker.io/library/alpine:3.20 \
'i=0; while true; do echo "$i" > /tick; i=$((i + 1)); sleep 1; done'Fork it while that process is still running:
spore fork --vm counter --count 2 --name child-%dBoth children keep running from the fork point:
spore exec child-0 'cat /tick; sleep 1; cat /tick'
spore exec child-1 'cat /tick; sleep 1; cat /tick'Named exec can also be interactive when you opt in to input or a terminal:
printf 'hello\n' | spore exec -i child-0 -- /bin/cat
spore exec -it child-0 -- /bin/shspore create, spore run, and spore exec run shell commands as
/bin/sh -lc. Use -- <argv...> when you need exact argv.
Capture a run when the command exits:
spore run --image docker.io/library/alpine:3.20 \
--capture base.spore \
'echo warmed > /var/tmp/example'Run another command from that completed base spore, or attach to the captured default session:
spore run --from base.spore 'cat /var/tmp/example'
spore run --from base.sporeIf the captured session was still running with a guest terminal, reattach with the same explicit terminal flags:
spore run -it --from live-shell.sporeInput attach fails closed when the captured session was not started with interactive stdin or a terminal. The spore contains guest process and PTY state, not the original host terminal connection.
--from resumes the spore and either attaches to the captured default session
or runs a fresh command through the restored exec agent. See
docs/filesystem.md for rootfs-backed writable state and
docs/memory.md for memory restore behavior.
Fork an existing spore:
spore fork base.spore --count 100 --out forksChildren are named 000000, 000001, and so on. They share verified content
and get distinct generation metadata.
Resume forked children locally with prefixed output:
spore fanout forks --parallel --for 20sSee docs/fanout.md for the child identity contract and docs/memory.md for the memory chunk/backing contract.
Pack a spore, optionally with forked children:
spore pack base.spore --children forks --out base.bundleUnpack or pull one selected child before resume:
spore unpack base.bundle --child 000042 --out child.spore
spore resume child.sporeRemote pulls are digest-pinned:
spore pull s3://bucket/path/base.bundle@sha256:<bundle-digest> \
--child 000042 \
--out child.sporespore pack, spore unpack, spore push, and spore pull carry the
manifest-selected memory, rootfs, and writable disk bytes. See
docs/spore-format.md and
docs/filesystem.md for the artifact contract.
Named VM lifecycle is stable on supported HVF/KVM backends:
export SPOREVM_RUNTIME_DIR=/tmp/sporevm-demo
spore create bench-1 --image docker.io/library/alpine:3.20
spore exec bench-1 'echo hi'
spore suspend bench-1 --out bench-1.spore
spore resume bench-1.spore --name bench-2
spore ps
spore rm bench-2Machine callers can use global --json for structured lifecycle state. See
docs/lifecycle.md for runtime layout, monitor jailing,
named live fork, and limits.
SporeVM supports one-shot runs, capture/resume, local fork/fan-out, rootfs-backed runs, local and remote bundle materialization, explicit guest networking, and named lifecycle on supported aarch64 HVF/KVM hosts.
Known limits: compatible host-class restore only, rootfs-bound writable disk state only, diskless named live fork, and no hardened public-cloud multi-tenant isolation claim. The detailed contracts are in the docs linked above.
Most local changes should start here:
mise run check
mise run smokeUseful focused checks:
mise run smoke:run
mise run smoke:run-stdin
mise run smoke:run-tty
mise run smoke:run-attach
mise run smoke:run-capture
mise run smoke:lifecycle-tty
mise run smoke:rootfs-fanout
mise run smoke:writable-rootfs
mise run smoke:run-net-dns
mise run smoke:monitor-jail
mise run smoke:monitor-failure-modesRepeatable benchmark runs live in docs/benchmarks.md. Release notes and release-gate summaries live on GitHub releases.
Releases are tag driven:
SPOREVM_RELEASE_VERSION=vX.Y.Z mise run releasemise run release runs local checks, verifies src/root.zig matches the target
version, and pushes the tag. The Buildkite tag build creates Linux ARM64 and
macOS ARM64 CLI archives plus matching libspore archives, writes
checksums.txt, and publishes the GitHub release. Use
mise run release:snapshot to build release archives locally without publishing.
Read SECURITY.md before changing virtqueue parsing, manifest or bundle decoding, guest memory access, rootfs materialization, or monitor control paths.
