Carrick: Linux ABI Emulation on macOS in 14 Days

Running Linux binaries on macOS as native processes, via Hypervisor.framework and Rust.

Bryan Cantrill—my former colleague at Joyent, now CTO of Oxide Computer Company—loves to tell a story from Tracy Kidder’s The Soul of a New Machine. At Data General in the late 1970s, the senior engineers on the Eagle project handed a new recruit what they considered an impossible task: build a cycle-accurate simulator. It was a snipe hunt. Something to keep the kid busy. But nobody actually told him it was impossible, so he just went and did it. He came back finished and asked what was next.

I think about that story a lot.

Back in my time at Joyent, I spent my days working on Node.js and debugging core dumps from Linux versions of Node.js on SmartOS using mdb. If you’ve ever had to trace memory leaks four bytes at a time inside a core file, you know the exact mix of patience and stubbornness it requires. That was the landscape when we launched LX Branded Zones—a thin veneer, a system call translation layer, that let unmodified Linux binaries run inside native, lightweight OS-level containers with direct, host-native observability. I was surrounded by people who could build things like that: Bryan, Dave Pacheco, Robert Mustacchi, Joshua Clulow, Jerry Jelinek (who actually resurrected the lx brand), Keith Wesolowski, and so many others—most of whom are now building rack-scale computers from scratch at Oxide. I don’t have their depth of systems engineering experience—I can only imagine what Keith would have to say about this project—but I’ve spent a long time watching them work, and you pick things up.

Fast forward to a Sunday evening in May 2026. I’m sitting at my desk with my MacBook Air—a machine that is, frankly, a monster for what it is—and I’m staring at Activity Monitor watching Docker’s Linux VM hold onto gigabytes of memory and disk. This is a fanless laptop, so it’s not even complaining audibly. It’s just quietly allocating resources to a full operating system I don’t actually need, because I want to run a test suite.

And I thought: what if I just… didn’t need the VM?

The idea isn’t new. The temptation to bypass the virtual machine tax is evergreen. In 2004, Sun Microsystems introduced Solaris Zones, proving that operating system containers could offer near-zero overhead. Branded Zones (BrandZ) followed in 2007 with the original lx brand for emulating the Linux ABI. Joyent was running these containers in production in 2006, years before Docker popularized the container image format and developer workflow in 2013. When Oracle eventually dropped support for the original lx brand, Joyent resurrected it on SmartOS in 2014-2015 so developers could run Docker containers directly on bare metal without any guest Linux kernel or virtual machine overhead.

But that was then, and this is macOS on Apple Silicon.

We’ve seen others try the same idea on different platforms since. WSLv1 translated Linux syscalls to the Windows NT kernel. The archived noah project first brought userspace Hypervisor.framework translation to Intel Macs. FreeBSD continues to maintain its own Linux compatibility layer.

And yet, here we are in 2026, and everyone on a Mac still runs Docker inside a full Linux VM—Docker Desktop, Lima, Colima, UTM. To be clear: Docker is a fantastic tool. I know a lot of people there—there’s even some overlap of folks from the Joyent days who are now at Docker—and they dramatically improved the developer workflow around container images. That contribution is exactly why Carrick itself runs OCI images and uses a Docker-compatible set of CLI arguments. But a real Linux kernel in a VM will probably always be more accurate, because the long tail of syscalls, obscure edge cases, and undocumented behaviors is incredibly long. It’s just that accuracy comes with overhead—a full guest OS in memory, a virtual disk on your filesystem, balloon drivers trying to claw back what they can—and no amount of memory management tuning fully changes that tradeoff. Carrick is an attempt to find a different compromise.

So, armed with my trusty intern—by which I mean AI coding agents—I set off to build a modern-day noah. I both “didn’t know any better” and, frankly, why not?

The first git commit was Sun May 17 20:44:58 2026 -0700. Today is Sat May 30 16:25:00 PDT 2026. In exactly thirteen days, Carrick has gone from a blank repository to passing 341/341 Go runtime tests, matching 74% of CPython modules, and achieving 63% syscall conformance on the Linux Test Project baseline. It turns out that with a bit of modern hypervisor hardware support and some Rust, you can implement a lot of syscalls very quickly.

Is this project a dead end? Highly probable. But it’s an interesting, highly educational dead end that might still offer real value for high-performance, low-overhead workloads with deep host-level observability.

What It Looks Like

Here’s the short version. You type this:

$ carrick run ubuntu:24.04 python3 -c "import platform; print(platform.machine())"
aarch64

That’s an unmodified Linux CPython binary, pulled from an OCI image, running on your Mac. There’s no VM to boot, no kernel to wait for. The process shows up in your host ps. You can kill it, lsof it, or point dtrace at it. The filesystem and networking are direct—sockets are standard Darwin sockets talking to the host TCP/IP stack, and guest files map straight to host paths. No FUSE daemon, no VirtIO mount syncing.

A cold start takes about ~90 ms. Subsequent forks clock in around ~5.7 ms.

How It Works

The key difference between Carrick and everything that came before is where the translation layer lives. LX Branded Zones required custom illumos kernel modules. WSLv1 was an in-kernel translation subsystem within Windows NT. FreeBSD’s compatibility layer is compiled directly into the FreeBSD kernel.

Carrick doesn’t touch the macOS kernel at all. Instead, it uses Apple’s Hypervisor.framework to trap guest execution entirely in host userspace:

+--------------------------------------+                 +--------------------------------+
|        Linux Guest (ARM64 EL0)       |                 |     Carrick Runtime (Rust)     |
|                                      |  Exception Trap |                                |
|  [ ELF Binary Code ]                 | --------------> |  [ Syscall Dispatcher ]        |
|  [ svc #0 (e.g., epoll_wait) ]       |  (VBAR_EL1 /    |  [ Translates to Darwin kqueue]|
|                                      |   hvc #0 exit)  |                                |
+--------------------------------------+                 +--------------------------------+
                                                                         |
                                                                         | Dispatch
                                                                         v
                                                         +--------------------------------+
                                                         |      macOS Host Kernel         |
                                                         +--------------------------------+

Because we’re running on Apple Silicon, we can configure the CPU to run the guest binary at ARM64 EL0 (unprivileged userspace) while controlling the Exception Vector Table (VBAR_EL1) from our host harness. When the guest executes an svc #0—the ARM64 equivalent of a Linux syscall—it traps instantly into a host exception vector, which issues an hvc #0 to drop control back into the Carrick userspace runtime. The runtime decodes x8 (syscall number) and x0x5 (arguments), dispatches to a clean-room Rust handler that maps them to native Darwin calls, writes the result back into x0, and resumes the vCPU. The whole cycle happens in userspace. No kernel module, no guest kernel, no VM lifecycle.

And because there’s no virtualized Linux kernel in the loop, there’s no “Big Kernel Lock” or VM scheduler overhead either. Per-subsystem Mutex and RwLock structures let independent syscalls on different subsystems—file system, process table, signals, virtual memory—execute concurrently across thread pools. Fork is just a page table copy and an HVF context rebuild (~5.7 ms), which is about 16× cheaper than a cold boot.

So What Actually Breaks?

Emulating the Linux ABI is a treadmill that doesn’t stop. There will always be another ioctl to implement, another obscure socket option, or a nested multithreaded fork race condition—we have a known Heisenbug in that last one right now. We currently pass 63% of the Linux Test Project baseline, and while the Go runtime hits 341/341, the CPython test suite still blocks on some subprocess and multiprocessing tests due to that same fork bug.

This isn’t going to replace your production Kubernetes cluster. But for running local CLI tools, instant test suites, or inspecting guest execution with host-native observability, it fills a gap that nothing else on macOS does today.

Try It, Break It, Tell Us

If you want to see what works and what doesn’t, check out the compatibility matrix and the docs. Run your own binaries. File issues. The syscall coverage is transparent—we publish the baselines because hiding them would be worse.

Nobody told the kid at Data General that building the simulator was impossible, so he just did it. Nobody told my AI agents that implementing epoll on top of kqueue was a bad idea, so they just did it. And thirteen days later, here we are—still debugging, still stubborn, still tracing things four bytes at a time. Keith would probably call this amateur hour. He might not even be wrong. But some habits don’t change.