Firecracker snapshots on macOS

jerboad can capture a running Firecracker/HVF VM into a private store and restore it in place: the same stopped VM resumes from the captured instant with its ID, MAC, IP, name, aliases and published ports. This is a deliberately narrow v1 for local development on the Mac that took the snapshot. It is not a clone, migration, backup or crash-consistency mechanism, and it is not approved for production use.

Requirements and scope

CLI and API

jerboa snapshot create <vm> <name>    # pause, capture, resume
jerboa snapshot restore <vm> <name>   # stopped VM -> running, in place
jerboa snapshot ls [--output json]
jerboa snapshot inspect <name>
jerboa snapshot rm <name>

JSON-RPC methods: VM.SnapshotCreate {id, name} returns SnapshotInfo, VM.SnapshotRestore {id, name} returns VMInfo, and Snapshot.List {}, Snapshot.Inspect {name}, Snapshot.Remove {name}. Create and restore block until they finish. Daemon shutdown cancels them; a client disconnect does not.

Names are 1-63 characters of lowercase letters, digits, ., _ and -, starting with a letter or digit. The client never chooses a path: the daemon resolves names only inside its store.

What is preserved

Preserved from the snapshotRecreated from the VM and current daemon
Guest RAM, vCPU/GIC and VirtIO stateSwitch link and a fresh private socket
Ephemeral root disk contentsService DNS, name and aliases
Guest-visible MAC (checked, not changed)Published ports and health checks
CPU, memory and VMM limitsNetwork policy (egress/DNS)

The guest does not reboot; application state in memory resumes at the capture instant. Established TCP connections and switch flow tables are not preserved: clients must reconnect. After the restored incarnation stops, a normal VM.Start boots the base image as usual. VM.Start on a stopped VM creates a replacement with a new ID, so snapshots of the old ID cannot be restored into it.

Store

Snapshots live in ~/.jerboa/snapshots (jerboad --snapshot-dir). Each entry is a 0700 directory containing the fork artifact (vm.snapshot) and an atomically written sidecar, jerboa-snapshot.json. The sidecar records the VM ID and name, backend, image, CPU/memory, a configuration fingerprint (environment values are hashed, not stored), network name, subnet, gateway, IP, MAC and aliases, published ports, fork compatibility data, per-component sizes and SHA-256 digests, and the total size. It never stores socket or temporary paths.

Lifecycle coordination

Create pauses the guest through the supervisor API, captures it, publishes the entry and always resumes it. If resume fails, the VM is force-stopped and the error says so. The daemon never reports a paused guest as running.

Restore runs under the same daemon lock as VM.Run and VM.Start. It:

  1. validates the snapshot, the VM’s identity and configuration fingerprint, the exact MAC, and that the registered network keeps its subnet and gateway, the IP belongs to this VM, and no other live VM on that network holds its name or aliases;
  2. marks the VM restoring (persisted) and prepares the switch link with the same MAC, a new socket, DNS and the original published ports;
  3. starts a supervisor without a guest, sets the unix-stream interface and socket capability, loads the snapshot, waits for Paused and resumes;
  4. persists the VM as running before confirming success, then starts health checks, statistics and the normal exit monitor. A persistence error triggers rollback instead of returning success.

Any failure (port or alias collision, integrity, load or resume error) stops only the supervisor this restore launched, closes the link and publications, and returns the VM to stopped.

A per-VM lifecycle claim is acquired by start, stop, kill, removal, snapshot operations and automatic restart. Signals use the stop/kill lifecycle too. Conflicting operations fail with “vm lifecycle operation in progress”; the registry is rechecked after acquisition to reject an already removed VM. An automatic restart waiting in backoff blocks restore, and a restart skips a VM that is no longer stopped.

Crash recovery

Create and restore write a durable journal record in .operations before each externally visible effect (pause, capture, publication, supervisor launch, load, resume). At start-up, before adopting running VMs, the daemon:

If resuming the guest or persisting recovery fails, daemon startup fails and keeps the operation journal for the next attempt. A failed restore also keeps its journal when rollback cannot be persisted. Do not delete these records to bypass an error; resolve the reported storage or VMM error and restart the daemon.

This is ordered recovery, not atomicity. A restore interrupted after resume but before the state was persisted is designed to come back running; one interrupted earlier comes back stopped. In local acceptance, daemon kills at supervisor-started, loading and resuming all rolled back to stopped. The adopt-while-restoring branch is implemented but was not observed on real HVF. Adoption of a completed restore after a daemon kill was observed.

Network policy

The snapshot never widens network permissions. Egress, DNS forwarding and listeners come from the daemon’s current --fc-security policy. The fork rejects snapshots whose unix-stream interface ID or MAC differ, and IP rules inside the VMM configuration. A restored VM can reach only what a freshly started VM with the same configuration could reach.

Limits

Acceptance harnesses

The daemon/CLI acceptance runs an isolated jerboad (its own HOME, TMPDIR, socket and dynamic ports) against real HVF guests:

export GO="$PWD/../firecracker-macos/experiments/hvf/build/guest-tools/go/bin/go"
B=path/to/jerboa-bundle/bin
python3 scripts/test-jerboa-snapshots.py --bin-dir path/to/jerboa-under-test \
  --firecracker-bin "$B/firecracker" --tools-dir "$B/tools" \
  --go-bin "$GO" --output-dir /absolute/output

It covers create and JSON-RPC restore, negative cases, races and daemon SIGKILL recovery, and signals only processes carrying its private path.

The low-level fork harness drives the supervisor API directly with Jerboa’s switch, without jerboad:

python3 scripts/test-firecracker-snapshot-network.py \
  --firecracker-bin dist/sol-closure/snapshots/bin/firecracker \
  --seed-snapshot dist/sol-closure/snapshots/seed.snapshot \
  --output-dir dist/sol-closure/snapshots/versioned-run \
  --go-bin "$GO"

Low-level VMM workflow

For reference, the fork API sequence the daemon automates:

PUT /actions         {"action_type":"Pause"}
PUT /snapshot/create {"snapshot_path":"/absolute/private/vm.snapshot"}
GET /operations/<id>

# restore, from a supervisor started with only --api-sock
PUT /network-interfaces [{"iface_id":"eth0","guest_mac":"02:…","backend":"unix-stream","socket_path":"/absolute/private/new.sock"}]
PUT /security           {"version":1,"unix_stream":"/absolute/private/new.sock"}
PUT /snapshot/load      {"snapshot_path":"/absolute/private/vm.snapshot"}
GET /operations/<id>
PUT /actions            {"action_type":"Resume"}

Do not send ForceStop or snapshot requests directly to a Jerboa-managed VM’s supervisor. Use the CLI or API so the daemon’s state, claims and journal stay consistent.