Firecracker on macOS

Jerboa supports the native ARM64 Hypervisor.framework backend from the sibling ../firecracker-macos checkout. Use its signed macOS executable, not an upstream Linux Firecracker binary. The fork requires macOS 26 and Apple Silicon; this integration was exercised on the local M5 host. Source configurations retain QEMU as the default; the native package installer selects Firecracker.

Install a published native release

On macOS 26+ in a native Apple Silicon terminal:

curl -fsSL https://jerboa.dev/install.sh | bash
# Or select a released version:
curl -fsSL https://jerboa.dev/install.sh | bash -s -- --version VERSION
jerboa status

Run without sudo; the script requests elevation only to install the package and CLI wrappers. Homebrew supplies jq and minisign if they are missing. The installer detects macOS/ARM64, verifies the signed release manifest and package SHA-256, checks the Developer ID signature and Gatekeeper assessment, then installs the complete package at /usr/local/libexec/jerboa. Wrappers in /usr/local/bin preserve relative library and tool lookup. It selects Firecracker in the user’s config and starts the user’s launchd daemon. Stop running VMs before upgrading; the installer stops the managed daemon.

This route requires a published release whose signed manifest contains the macos component. Releases without it are rejected before installation. CI does not publish that package yet because releases are not Developer ID signed; it publishes the unsigned, self-contained Jerboa Desktop DMG instead. Until a signed package is published, use the DMG or the source build below. See the distribution pipeline.

Build and run

Build Jerboa and the ARM64 kernel with ./scripts/build-macos.sh. If the native tools already exist, rebuilding only the CLI and daemon is sufficient:

go build -o dist/macos-arm64/ ./cmd/jerboa ./cmd/jerboad
export JERBOA_FIRECRACKER_BIN="$PWD/../firecracker-macos/build/macos-arm64/firecracker"
./dist/macos-arm64/jerboa config set hypervisor firecracker
./dist/macos-arm64/jerboa daemon start
./dist/macos-arm64/jerboa build examples/hello --platform linux/arm64 --name hello
./dist/macos-arm64/jerboa run hello:latest --attach

Stop existing VMs and the managed daemon before changing backends. To select Firecracker for a single managed launch, use daemon start --hypervisor firecracker. The launcher resolves JERBOA_FIRECRACKER_BIN, then a firecracker executable beside jerboad, then PATH or /opt/homebrew/bin. It records the absolute path in the launchd service. Keep the fork and its native dependencies in place.

Foreground operation permits explicit kernel and policy selection:

./dist/macos-arm64/jerboad \
  --hypervisor firecracker \
  --fc-bin "$PWD/../firecracker-macos/build/macos-arm64/firecracker" \
  --tools-dir "$PWD/dist/macos-arm64/tools"

On macOS the default kernel is tools/kernel.img, an ARM64 ELF. --fc-kernel overrides it. Jerboa does not download the Linux/x86 Firecracker kernel for this backend. The executable retains the fork’s own code-signing and entitlement requirements; Jerboa does not rebuild or re-sign it.

Supported behavior

ARM64 ELF guests can use 1-4 vCPUs, 64-2048 MiB of RAM, an ephemeral root disk, and up to three persistent volumes. Environment, static network configuration and mount labels are delivered through opt/uni/* firmware files. Root disk writes never modify the image-store copy.

Without --network, existing private slirp configurations keep guest IP 10.0.2.15 and VMM-owned TCP/UDP publishing. Their health checks still require a published TCP port.

With --network, Firecracker connects its generic Unix Ethernet transport to Jerboa’s shared gVisor stack and Ethernet switch. QEMU is not required. Jerboa allocates guest IPs from the network subnet, accepts --ip, and provides direct VM-to-VM communication and DNS names/aliases within that network. Overlapping subnets and duplicate allocated IPs are rejected. TCP/HTTP health checks dial the guest through the stack, including ports that are not published.

TCP/UDP publishing supports explicit IPv4 bind addresses. An omitted address binds 0.0.0.0 on all host IPv4 interfaces, for both slirp and shared networks; use 127.0.0.1:HOST:GUEST for local access. Slirp grants exactly that address/protocol/nonzero port in security.listeners. A wildcard listener grants no egress or DNS permission and does not match a concrete bind. Darwin TCP SO_REUSEADDR can allow a concrete bind to coexist with a wildcard bind on the same port (the concrete bind takes precedence); identical listener endpoints collide. Binding collisions fail startup and roll back the new VM’s resources. Ports, accepted connections, outbound sockets and the private link are closed when that VM stops, even if other VMs keep the shared network alive. The daemon recreates links, forwarding, DNS and health checks after replacement, and the VMM reconnects automatically. Established connections may break and applications must retry.

Console output and VMM diagnostics are available through jerboa logs. The manager observes guest exit separately from the persistent API supervisor, preserving on-failure restart behavior. Existing VMs can be adopted after a daemon crash; automatic restart policies are not applied to adopted VMs, matching Jerboa’s existing adoption behavior. Console history is not recovered.

Network policy and limits

Outbound traffic and external DNS are denied by default. Joining a named network explicitly enables communication and service DNS inside that network. Supply a JSON security object with jerboad --fc-security /absolute/path/policy.json, or set JERBOA_FIRECRACKER_SECURITY when starting the managed service. For example:

{
  "version": 1,
  "egress": [{"protocol": "tcp", "address": "192.0.2.10", "port": 443}],
  "dns": {"address": "1.1.1.1", "port": 53}
}

Replace the example destination with the actual endpoint needed by the guest. Policy is shared by this daemon’s Firecracker VMs. Slirp receives these rules in the VMM and adds published listener permissions. For shared networks, Jerboa enforces exact IPv4/protocol/port egress rules and uses only dns as its external resolver. JERBOA_DNS_UPSTREAM does not override Firecracker’s policy. Service DNS is answered locally. Access through the network gateway to the host is translated to 127.0.0.1, so authorize that address and the required port explicitly. Granting DNS does not authorize TCP/UDP to resolved addresses.

Shared networking requires version 1 hardened mode. It rejects development mode, unknown policy fields and manually supplied listeners; publish host listeners using port mappings. Jerboa gives the VMM only the exact private Unix socket capability. Seatbelt remains enabled in the VMM and broker; Internet sockets are owned by the policy-enforcing daemon.

The shared stack binds each Ethernet link to its assigned source MAC/IP, rejects source spoofing, IPv6 and VLAN frames, validates framing before allocating, and bounds each link’s pending output to 64 frames. The transport limit is 65536 bytes per frame; the stack MTU is 1500. Shared host connections (published and outbound, including pending dials) are capped at 64 per VM and 256 per network. UDP flows expire after 30 seconds without response activity; TCP dials time out after five seconds. DNS has bounded query workers. Removing a VM cancels its pending dials and closes its live host connections.

Verification

go test ./...
python3 scripts/test-firecracker-macos.py
JERBOA_TEST_BIN="$PWD/dist/macos-arm64" python3 scripts/test-firecracker-network.py

The standalone smoke uses a temporary home, builds its own ARM64 guest through Jerboa, and exercises one/four CPU boots, environment, persistent volumes, TCP/UDP, occupied ports, guest exit, daemon recovery, failure restart and stop. It uses JERBOA_FIRECRACKER_BIN or the sibling fork’s build path. It does not replace the user’s managed daemon.

An additional manager-level test can use the fork’s optional Jerboa demo disk (HTTP 8080, UDP echo 8081, MARKER=hvf-smoke, /shutdown):

JERBOA_TEST_FC_BIN="$PWD/../firecracker-macos/build/macos-arm64/firecracker" \
JERBOA_TEST_FC_KERNEL="$PWD/dist/macos-arm64/tools/kernel.img" \
JERBOA_TEST_FC_DISK="$PWD/../firecracker-macos/build/macos-arm64/demo/root.img" \
go test ./internal/vm -run TestNativeFC -v

Shared networks and Compose

jerboa network create app --subnet 172.25.0.0/24
jerboa run service:latest --name postgres --network app --ip 172.25.0.10 \
  --health-check tcp:5432
jerboa run backend:latest --network app -e DATABASE_HOST=postgres \
  -p 127.0.0.1:8080:8080

Both images must contain ARM64 executables compatible with the Jerboa kernel. A working network does not make an x86 PostgreSQL package executable on HVF. The OPS PostgreSQL packages consulted on 2026-09-13 are x86_64. A separate ARM64 PostgreSQL fixture now builds a pinned experimental pthread fork and tests real SQL from another guest, including volume persistence after server replacement. It is not stock PostgreSQL or a production/durability qualification. The general shared-network regression still uses an ARM64 HTTP/UDP guest and reports that distinction explicitly.

Compose accepts the existing network list or an attachment mapping:

services:
  postgres:
    image: postgres-arm64:latest
    health_check: tcp:5432
    networks:
      app:
        ipv4_address: 172.25.0.10
        aliases: [database]
  backend:
    image: backend-arm64:latest
    depends_on: [postgres]
    networks: [app]
    environment: [DATABASE_HOST=database, DATABASE_PORT=5432]
    ports: ['127.0.0.1:8080:8080']
networks:
  app:
    ipam:
      config:
        - subnet: 172.25.0.0/24

Services without attachments join an implicit network whose generated name is scoped to the Compose file. Explicitly declared network names keep Jerboa’s existing global naming semantics. This remains Jerboa’s Compose subset, not a promise to accept every Docker Compose option. Only one attachment/subnet is supported. Names and aliases are network-scoped and persisted in VM configuration (network_aliases in the Run API). A literal name/alias in the caller’s network takes priority over stripping a network suffix: database.app assigned to postgres wins over service database in network app. If no literal match exists, host.network is tried within that same scope. Explicit caller scope never switches networks. Unscoped dotted queries infer their scope from the final suffix.

For an isolated build, put the rebuilt CLI and daemon in a separate directory and link its tools directory to the existing ARM64 tools. Set JERBOA_TEST_BIN to that directory and JERBOA_FIRECRACKER_BIN to the signed fork executable. test-firecracker-network.py covers IPAM, IP/name/alias traffic, isolation, unpublished health, TCP/UDP bindings and collisions, daemon SIGKILL/restart, Compose and default/explicit outbound and DNS policies. The tests use a temporary HOME and a separate daemon socket.