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.
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 #0 exits to host │ │ │
│ │ │ │ │ │
│ │ │ decode x8 (syscall nr) │ │ │
│ │ │ decode x0-x5 (args) │───>│ Darwin API │
│ │<───────│ write result to x0 │<───│ (native) │
│ resumes execution │ │ resume vCPU │ │ │
└──────────────────────┘ └──────────────────────────┘ └──────────────┘ - Trap boundary. Guest executes
svc #0→ synchronous exception toVBAR_EL1→hvc #0exits to the runtime → Rust handler translates and dispatches → result written tox0→ vCPU resumed. - Clean-room. Syscall handlers are written from specs and observed behavior, not from Linux kernel source.
- Memory. Stage-1 identity-mapped page tables with MMU enabled
(
SCTLR_EL1.M=1), so ARM exclusive loads/stores (ldaxr/stlxr) work and mutexes behave correctly. - Concurrency. Per-subsystem locks (fs, creds, proc, signal, mem) — no big kernel lock. Unrelated syscalls run in parallel across threads.
- Fork.
fork(2)→ real macOSfork()+ COW guest pages + rebuilt HVF context.execve(2)→ tear down, reload ELF, resume. - Translation examples.
epoll→kqueue, Linux sockets → Darwin sockets,AF_NETLINKsynthesized, synthetic/procand/sys.
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.
~124,000 lines of Rust across 12 crates. We publish LTP and ecosystem
conformance baselines so you can assess fit.
| Workload | Status | Detail |
|---|---|---|
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 | Glibc amd64 images (Debian/Ubuntu) JIT-translate through Apple Rosetta 2 and run, incl. multi-call coreutils. Static-musl (Alpine) is an Apple Rosetta limitation. |
Full baseline data: compatibility page.
Performance
Measured via DTrace USDT probes on carrick run … /bin/true:
| Phase | Cost | Note |
|---|---|---|
| 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.
- ~75% syscall coverage. The LTP baseline shows where the gaps are. Memory management (34%) and IPC (38%) are the weakest subsystems.
- Rare Go runtime coherence races. Under heavy concurrent memory operations, the Go runtime occasionally hits HVF coherence exceptions.
- No
ptracesupport beyond Phase-1.gdbanddelvedo not work on guest binaries yet. - PTY edge cases.
ttyname//dev/ttydon't fully resolve. Intermittent newline race withlson wide terminals. - x86_64 via Rosetta 2: glibc works, static-musl doesn't. Dynamic glibc amd64 images (Debian, Ubuntu) run under Apple Rosetta 2 — see the recording above. Static-PIE musl binaries (Alpine/busybox) SIGSEGV inside Rosetta itself — the same Apple limitation Docker Desktop hits (docker/for-mac#6773); use a glibc base. Native ARM64 remains the primary, fully-supported target.
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
- Documentation — install, CLI reference, tracing
- Compatibility — LTP, Go, CPython conformance baselines
- Blog — engineering notes and deep dives
- GitHub