carrick

Run unmodified Linux binaries on macOS. No VM, no guest kernel, no daemon.

Carrick loads a Linux ELF binary, runs its ARM64 instructions directly on the CPU via Apple's Hypervisor.framework, and traps each svc #0 (Linux syscall) at the hardware boundary. A Rust runtime translates the trapped call to a Darwin equivalent and resumes the guest. The Linux process becomes a native macOS process — visible to ps, lsof, kill, and dtrace on your host.

Install

$ brew tap carrick-sh/carrick
$ brew install --HEAD carrick

Apple Silicon macOS only. Built from source and codesigned with the Hypervisor.framework entitlement on install.

See it run

These are real recordings of the carrick binary — captured straight from the terminal (a pty typing the actual carrick command into a shell, with carrick's real output and timing), not hand-written mock-ups.

A Linux container is just macOS processes

Run a container detached, then look at the macOS process table. The guest's processes are right there — real macOS PIDs, real %CPU and RSS, labelled carrick:web: python3 and signalable with kill(1). Inside, the container has its own PID namespace (its ps shows PID 1); on the host those same processes are ordinary native processes — no VM hiding them. Networking is host-level, so a curl from macOS reaches the server directly.

carrick run -d, then ps on the macOS host — the container's processes are ordinary native macOS processes (carrick:web: python3) with real PID, %CPU and RSS. No VM; host networking means a curl from macOS hits the server directly.
On your MaccarrickDocker Desktop
ps / lsof shows every guest process, as carrick:<id>: <cmd>, with a real PID, %CPU and RSS only Docker's VM helpers (e.g. com.docker.virtualization) — never the container's own processes
The process really is a native macOS process running guest ARM64 at EL0 a Linux process inside the LinuxKit VM, invisible to the host kernel
Signal / trace it kill, dtrace, sample and lsof work directly from macOS docker kill only; host tools can't reach inside the VM
Activity Monitor lists each container process by name shows a single Docker VM process

More recordings

x86_64 Ubuntu on Apple Silicon via Rosetta 2 — uname -m really reports x86_64
Interactive bash with real job control — background jobs, Ctrl-Z suspend, fg/bg, ps
apt-get update && apt-get install hello — real Debian packaging, end to end
Python http.server in a Linux container, curl'd from macOS over host networking
Bind-mount a macOS directory into the container and read it

Usage

A quick command reference — see the recordings above for real output.

# pull an OCI image and run a command
$ carrick run ubuntu:24.04 /bin/bash -c 'apt-get update && apt-get install -y hello && hello'

# interactive shell with a real PTY (job control, Ctrl-Z/fg, ps)
$ carrick run -it ubuntu:24.04 bash

# run an x86_64 image via Apple Rosetta 2
$ carrick run --platform linux/amd64 ubuntu:24.04 uname -m

# run a prebuilt Linux ELF directly — no image pull
$ carrick run-elf ./my-linux-binary --flag value

# bind-mount a host directory into the guest
$ carrick run -v /Users/me/data:/mnt:ro ubuntu:24.04 ls /mnt

# build an image from a Dockerfile (runs kaniko as a guest) — like docker build
$ carrick build -t myapp:latest .

# live syscall trace via DTrace USDT probes
$ sudo carrick trace run alpine:latest /bin/echo hi

What happens under the hood

Take the Python http.server recording above: the guest binds port 8765 and a curl from macOS gets a real HTTP 200. That works because the guest socket binds directly to a host interface — no port forwarding.

No VM booted. No container runtime started. The Python binary's ARM64 instructions executed on your CPU at EL0 (unprivileged). When Python called bind(), carrick trapped the svc #0, decoded it as Linux sys_bind, and called macOS bind() on a real host socket. The server is listening on your actual network interface.

How it works

Carrick uses Apple's Hypervisor.framework to create a lightweight execution context — not a virtual machine. There is no guest kernel, no virtual disk, no BIOS. One host pthread and one HVF vCPU per guest thread.

  Linux Binary (ARM64 EL0)          Carrick Runtime (Rust)         macOS Kernel
 ┌──────────────────────┐        ┌──────────────────────────┐    ┌──────────────┐
 │                      │  trap  │                          │    │              │
 │  guest executes      │───────>│  VBAR_EL1 vector catches │    │              │
 │  svc #0 (syscall)    │        │  hvc #2 exits to host    │    │              │
 │                      │        │                          │    │              │
 │                      │        │  decode x8 (syscall nr)  │    │              │
 │                      │        │  decode x0-x5 (args)     │───>│  Darwin API  │
 │                      │<───────│  write result to x0      │<───│  (native)    │
 │  resumes execution   │        │  resume vCPU             │    │              │
 └──────────────────────┘        └──────────────────────────┘    └──────────────┘

What works today

Carrick runs real workloads end-to-end today — apt-get, Python servers, Node.js, the Go runtime suite, and the full libuv async I/O surface. ~127,000 lines of Rust across 12 crates. We publish LTP and ecosystem conformance baselines so you can assess fit.

WorkloadStatusDetail
apt-get install verified Runs end-to-end including dpkg post-install scripts.
Python http.server verified ThreadingHTTPServer serves concurrent requests from the host. Single-digit ms response times.
Go runtime suite verified ~876/880 standard-library test binaries pass (sync, atomic, context, time, runtime, net, cgo). At parity with Docker.
Node.js & V8 verified node-core full plan: 5301/5304 (99.9%). The 3 fails are cosmetic stderr snapshots the Docker oracle also fails.
libuv test suite verified 498/507 tests pass (98.2%). Full async I/O, pipe, IPC, and event-loop surface.
LTP syscall conformance verified 568/896 valid tests match (63%). Strong: sched 76%, timers 74%, signals 73%, fs 68%. Weaker: mm 34%, ipc 38%.
CPython module parity verified 425/492 regrtest modules match (86.4%). test_subprocess/test_multiprocessing now run (nested-fork bug fixed).
Interactive shell (-t) verified Real PTY via pty_relay.rs. Ctrl-C, Ctrl-Z, job control work.
Docker-style CLI verified OCI pull, layer composition, -e/-v/-w/--entrypoint flags.
x86_64 via Rosetta 2 (--platform linux/amd64) verified amd64 images JIT-translate through Apple Rosetta 2 and run — glibc (Debian/Ubuntu, incl. multi-call coreutils) and static-musl (Alpine: busybox, apk) alike. Translated, so slower than native ARM64.

Full baseline data: compatibility page.

Performance

Measured via DTrace USDT probes on carrick run … /bin/true:

PhaseCostNote
First boot ~90 ms OCI load + hv_vm_create + page tables + ELF load
Fork ~5.7 ms HVF context rebuild — ~16× cheaper than boot, no global lock
Fork + exec ~7.8 ms vs ~1 ms native Linux fork+exec
Child teardown ~7 µs Effectively free
Process teardown ~175 ms Kernel reclaiming the large VM mapping

Known limitations

Carrick is syscall emulation, and that cuts both ways. Not every binary will work. We'd rather be upfront about this than have you find out the hard way.

Compared to VMs

Carrick complements containers — and increasingly stands in for them: build images with carrick build, and drive carrick through a Docker-compatible API with carrick serve, all without a VM. The Docker API is early (SDK-level container lifecycle, not the interactive docker CLI yet) — see using carrick with Docker.

Docker Desktop Lima / Colima Full VM Carrick
Model Linux VM → containers Linux VM Full guest OS Binary as macOS process
Startup 30–60 s 10–30 s Minutes ~90 ms / ~5.7 ms fork
Filesystem FUSE sync Mounted share Virtual disk Direct host paths
Networking Port forwarding Shared IP NAT/bridge Direct host sockets
Host integration Opaque Inside VM Inside VM ps/lsof/kill/dtrace

Links