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.
Usage
# 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'
Hello, world!
# interactive shell with a real PTY
$ carrick run -t alpine:latest /bin/sh
# 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
When you run carrick run ubuntu:24.04 python3 -m http.server,
here's what actually happens:
$ carrick run ubuntu:24.04 python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 ...
# In another terminal — this works because guest sockets
# bind directly to host interfaces. No port forwarding.
$ curl -s http://localhost:8000/ | head -5
<!DOCTYPE HTML>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Directory listing for /</title>
# The guest Python process is a real macOS process:
$ ps aux | grep python
me 41892 0.4 0.1 ... python3 -m http.server
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. |
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. - ARM64 only. x86_64 Linux binary support via Rosetta 2 is under active research but not functional. Do not expect it to work.
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