Run Arm64/x86-64 Linux ELF binaries on macOS Apple Silicon
C
269
798 commits
updated Sep 21, 2026
Run Linux ELF binaries directly from the macOS shell -- no Docker, no
full VM image, no daemon. elfuse is a process-scoped Linux user-space
runtime: each guest runs inside a lightweight Hypervisor.framework VM
owned by the elfuse process itself, and Linux syscalls are translated
to macOS behavior in host-side handlers rather than served by a real
Linux kernel.
Native aarch64-linux executes directly on the CPU. x86_64-linux
executes through Apple's embedded Rosetta translator hosted inside the
same VM; the architecture is auto-detected from the ELF header. Both
static and dynamically linked guests are supported, with the dynamic
linker resolved against an external sysroot via --sysroot.
aarch64-linux ELF binariesx86_64-linux ELF binaries via Apple
Rosetta (auto-detected from the ELF header, opt out with
--no-rosetta)--sysroot, including
case-colliding names on the default case-folding APFS (see
docs/filenames.md)/proc and selected /dev emulation for user-space probes/dev/bus/usb and /sys/bus/usb/devices are
built from the IOKit registry, and opening a device node yields a
usbdevfs fd whose ioctls -- interface claim, control and bulk transfers,
and asynchronous URBs -- drive the attached device through IOKit. A
udev-backed lsusb also needs name_to_handle_at, and macOS publishes
no root hubs, so there are no usbN entries and lsusb -t lists devices
without their bus rows/dev/fuse and mount("fuse") work without
macFUSE / FUSE-T / FSKitgdb or lldbelfuse-oci is a separate Go binary that pulls OCI images into a local
OCI image layout. It does not add container isolation. See
docs/usage.md and
docs/oci-images.md.
elfuse is intentionally narrow. It runs single Linux binaries (and
their fork/exec children) with minimal overhead; it does not host a
Linux kernel, namespaces, cgroups, or kernel modules. For workloads
that need full kernel features, container orchestration, or systemd,
prefer a full VM tool (Lima, UTM, OrbStack) or Docker Desktop. For
single-binary tooling, language runtimes, test harnesses, and
debugger-driven workflows, elfuse removes the disk-image and
boot-time overhead those tools impose.
elfuse is C11 plus a small amount of aarch64 assembly. In short:
mmap-ed slab the guest rewrites from another vCPU thread, so
host access already runs through bounds-checked accessors (guest_ptr,
guest_read, guest_write). Rust would enforce that discipline rather
than leave it to convention, which is a real gain, but the checks inside
those accessors are the same ones to get right.p_vaddr + p_memsz overflows has to be
rejected where src/core/elf.c parses it: saturating load_max to
UINT64_MAX instead relocates the wrap into every consumer that adds a
load base, where the elf_end > guest_size check it was meant to trip
passes. Rust's + wraps in release builds too; catching it takes an
explicit checked add either way.cppcheck, clang-tidy, scan-build, Infer, and a runtime
matrix under ASAN, UBSAN, and TSAN. That catches such defects after the
fact rather than excluding them by construction, which is the honest cost
of the choice. Frama-C proves each selected function body free of arithmetic
runtime errors, assuming its stated preconditions: the WP targets in
mk/verify.mk (make verify) discharge the bounds math in src/proved/ and
the ELF loader's arithmetic helpers under -wp-rte, and
make verify-mutants asserts each proof rejects a known-broken source.SCM_RIGHTS, and hosting Apple Rosetta. The EL1 shim is
aarch64 assembly under any host language.Longer version in docs/internals.md.
clang, codesign, and GNU makeobjcopy or llvm-objcopycom.apple.security.hypervisorTo build only (make elfuse) without running tests, just the
Xcode Command Line Tools and objcopy (brew install binutils) suffice.
For guest test binaries, the project also expects an AArch64 Linux cross
toolchain. The default paths in mk/toolchain.mk target the toolchain layout
used by the repository test harness, but CROSS_COMPILE and
BAREMETAL_CROSS are overridable.
See docs/testing.md for toolchain setup guide.
git clone https://github.com/sysprog21/elfuse
cd elfuse
make elfuse
make test-busybox
build/elfuse build/busybox
Replace build/busybox with an aarch64-linux or x86_64-linux executable.
The guest architecture is auto-detected from the ELF header.
For dynamically linked guests:
build/elfuse --sysroot /path/to/sysroot ./path/to/program
For x86_64-linux guests, Rosetta is on by default. To disable:
build/elfuse --no-rosetta ./path/to/aarch64-only-binary
For early debugging:
build/elfuse --gdb 1234 --gdb-stop-on-entry ./path/to/program
--gdb is rejected for x86_64 guests because the stub serves the
aarch64 view Rosetta produces, not the original x86_64 architectural
state.
The build signs build/elfuse before use. Override the signing identity with
SIGN_IDENTITY="Developer ID ..." when needed.
--sysroot, and attaching gdb /
lldb to the built-in stub.make check flow, the QEMU and Rosetta cross-check matrices, and
fixture handling.elfuse-oci store,
pull behavior, and validation.Most common targets:
make elfuse # build and codesign build/elfuse
make check # quick unit suite + BusyBox applet smoke
make test-gdbstub # debugger integration
make test-matrix # cross-check elfuse against QEMU on the same corpus
make verify # Frama-C WP proofs
make lint # clang-tidy
make check is the recommended pre-commit gate. make test-matrix is the
recommended gate for changes touching procfs, dynamic linking, networking,
or process semantics. make test-rosetta-all covers the x86_64 acceptance
suites in isolation. make verify and make verify-mutants gate changes to
src/proved/, to the ACSL contracts, or to any function a proof target
names. See docs/testing.md for the full target list,
fixture flow, and validation-by-change-type guidance.
The first make in a fresh clone installs Git hooks that run the same checks
CI does, at commit and push time instead of after: staged formatting, comment
reflow, banned APIs, whitespace and conflict markers, and the commit-message
rules in CONTRIBUTING.md. A missing local formatter warns
rather than blocks, and a hook you already wrote is never replaced.
make uninstall-hooks removes them, make install-hooks puts them back. They
do not replace make check.
elfuse runs single Linux user-space processes (and their fork /
exec children). It is not a Linux kernel.
That framing shapes both what it does and what it explicitly will not
do.
io_uring, KVM, perf
events.elfuse host process. HVF allows one VM
per host process; Linux-style fork is implemented by
posix_spawn-ing a fresh elfuse host process and transferring
state (see docs/internals.md).MAX_THREADS = 64).src/syscall/dispatch.tbl; anything
outside it returns -ENOSYS rather than silently succeeding.FUTEX_LOCK_PI and friends behave as plain mutex acquire / release;
true priority-inheritance scheduling is not modeled.sched_setaffinity is honored as a no-op (returns the all-CPUs
mask); the host scheduler picks the actual CPU./proc, /dev, and mount data are synthetic compatibility views,
not host pass-throughs.CLAIMINTERFACE reports EBUSY, and root-mode
device capture is not implemented. CDC serial devices are reachable by
opening the host's /dev/cu.* node instead.EINVAL.uname and /proc/version report Linux 6.18 LTS, a floor for
version-gated userspace; src/syscall/dispatch.tbl states what is
implemented.Read CONTRIBUTING.md first. It is the tracked style guide: the C conventions this tree actually follows, which formatter owns which part of a file, what CI gates and what it does not, and the seven commit-message rules the log is written to.
Two things settle most review comments before they are written:
make indent # clang-format and comment reflow (FORMAT_SHELL/FORMAT_PY add the rest)
make check-format # verify without rewriting
File an issue before a substantial change, so the design discussion happens before the effort does. Typo fixes, small refactors, and comment or documentation edits need no issue.
Apache License 2.0. See LICENSE.
Copyright 2026 elfuse contributors
Copyright 2025 Moritz Angermann, zw3rk pte. ltd.
C
85.6%
Shell
5.9%
Python
4.3%
Makefile
2.7%
Run Arm64/x86-64 Linux ELF binaries on macOS Apple Silicon
C
269
798 commits
updated Sep 21, 2026
Run Linux ELF binaries directly from the macOS shell -- no Docker, no
full VM image, no daemon. elfuse is a process-scoped Linux user-space
runtime: each guest runs inside a lightweight Hypervisor.framework VM
owned by the elfuse process itself, and Linux syscalls are translated
to macOS behavior in host-side handlers rather than served by a real
Linux kernel.
Native aarch64-linux executes directly on the CPU. x86_64-linux
executes through Apple's embedded Rosetta translator hosted inside the
same VM; the architecture is auto-detected from the ELF header. Both
static and dynamically linked guests are supported, with the dynamic
linker resolved against an external sysroot via --sysroot.
aarch64-linux ELF binariesx86_64-linux ELF binaries via Apple
Rosetta (auto-detected from the ELF header, opt out with
--no-rosetta)--sysroot, including
case-colliding names on the default case-folding APFS (see
docs/filenames.md)/proc and selected /dev emulation for user-space probes/dev/bus/usb and /sys/bus/usb/devices are
built from the IOKit registry, and opening a device node yields a
usbdevfs fd whose ioctls -- interface claim, control and bulk transfers,
and asynchronous URBs -- drive the attached device through IOKit. A
udev-backed lsusb also needs name_to_handle_at, and macOS publishes
no root hubs, so there are no usbN entries and lsusb -t lists devices
without their bus rows/dev/fuse and mount("fuse") work without
macFUSE / FUSE-T / FSKitgdb or lldbelfuse-oci is a separate Go binary that pulls OCI images into a local
OCI image layout. It does not add container isolation. See
docs/usage.md and
docs/oci-images.md.
elfuse is intentionally narrow. It runs single Linux binaries (and
their fork/exec children) with minimal overhead; it does not host a
Linux kernel, namespaces, cgroups, or kernel modules. For workloads
that need full kernel features, container orchestration, or systemd,
prefer a full VM tool (Lima, UTM, OrbStack) or Docker Desktop. For
single-binary tooling, language runtimes, test harnesses, and
debugger-driven workflows, elfuse removes the disk-image and
boot-time overhead those tools impose.
elfuse is C11 plus a small amount of aarch64 assembly. In short:
mmap-ed slab the guest rewrites from another vCPU thread, so
host access already runs through bounds-checked accessors (guest_ptr,
guest_read, guest_write). Rust would enforce that discipline rather
than leave it to convention, which is a real gain, but the checks inside
those accessors are the same ones to get right.p_vaddr + p_memsz overflows has to be
rejected where src/core/elf.c parses it: saturating load_max to
UINT64_MAX instead relocates the wrap into every consumer that adds a
load base, where the elf_end > guest_size check it was meant to trip
passes. Rust's + wraps in release builds too; catching it takes an
explicit checked add either way.cppcheck, clang-tidy, scan-build, Infer, and a runtime
matrix under ASAN, UBSAN, and TSAN. That catches such defects after the
fact rather than excluding them by construction, which is the honest cost
of the choice. Frama-C proves each selected function body free of arithmetic
runtime errors, assuming its stated preconditions: the WP targets in
mk/verify.mk (make verify) discharge the bounds math in src/proved/ and
the ELF loader's arithmetic helpers under -wp-rte, and
make verify-mutants asserts each proof rejects a known-broken source.SCM_RIGHTS, and hosting Apple Rosetta. The EL1 shim is
aarch64 assembly under any host language.Longer version in docs/internals.md.
clang, codesign, and GNU makeobjcopy or llvm-objcopycom.apple.security.hypervisorTo build only (make elfuse) without running tests, just the
Xcode Command Line Tools and objcopy (brew install binutils) suffice.
For guest test binaries, the project also expects an AArch64 Linux cross
toolchain. The default paths in mk/toolchain.mk target the toolchain layout
used by the repository test harness, but CROSS_COMPILE and
BAREMETAL_CROSS are overridable.
See docs/testing.md for toolchain setup guide.
git clone https://github.com/sysprog21/elfuse
cd elfuse
make elfuse
make test-busybox
build/elfuse build/busybox
Replace build/busybox with an aarch64-linux or x86_64-linux executable.
The guest architecture is auto-detected from the ELF header.
For dynamically linked guests:
build/elfuse --sysroot /path/to/sysroot ./path/to/program
For x86_64-linux guests, Rosetta is on by default. To disable:
build/elfuse --no-rosetta ./path/to/aarch64-only-binary
For early debugging:
build/elfuse --gdb 1234 --gdb-stop-on-entry ./path/to/program
--gdb is rejected for x86_64 guests because the stub serves the
aarch64 view Rosetta produces, not the original x86_64 architectural
state.
The build signs build/elfuse before use. Override the signing identity with
SIGN_IDENTITY="Developer ID ..." when needed.
--sysroot, and attaching gdb /
lldb to the built-in stub.make check flow, the QEMU and Rosetta cross-check matrices, and
fixture handling.elfuse-oci store,
pull behavior, and validation.Most common targets:
make elfuse # build and codesign build/elfuse
make check # quick unit suite + BusyBox applet smoke
make test-gdbstub # debugger integration
make test-matrix # cross-check elfuse against QEMU on the same corpus
make verify # Frama-C WP proofs
make lint # clang-tidy
make check is the recommended pre-commit gate. make test-matrix is the
recommended gate for changes touching procfs, dynamic linking, networking,
or process semantics. make test-rosetta-all covers the x86_64 acceptance
suites in isolation. make verify and make verify-mutants gate changes to
src/proved/, to the ACSL contracts, or to any function a proof target
names. See docs/testing.md for the full target list,
fixture flow, and validation-by-change-type guidance.
The first make in a fresh clone installs Git hooks that run the same checks
CI does, at commit and push time instead of after: staged formatting, comment
reflow, banned APIs, whitespace and conflict markers, and the commit-message
rules in CONTRIBUTING.md. A missing local formatter warns
rather than blocks, and a hook you already wrote is never replaced.
make uninstall-hooks removes them, make install-hooks puts them back. They
do not replace make check.
elfuse runs single Linux user-space processes (and their fork /
exec children). It is not a Linux kernel.
That framing shapes both what it does and what it explicitly will not
do.
io_uring, KVM, perf
events.elfuse host process. HVF allows one VM
per host process; Linux-style fork is implemented by
posix_spawn-ing a fresh elfuse host process and transferring
state (see docs/internals.md).MAX_THREADS = 64).src/syscall/dispatch.tbl; anything
outside it returns -ENOSYS rather than silently succeeding.FUTEX_LOCK_PI and friends behave as plain mutex acquire / release;
true priority-inheritance scheduling is not modeled.sched_setaffinity is honored as a no-op (returns the all-CPUs
mask); the host scheduler picks the actual CPU./proc, /dev, and mount data are synthetic compatibility views,
not host pass-throughs.CLAIMINTERFACE reports EBUSY, and root-mode
device capture is not implemented. CDC serial devices are reachable by
opening the host's /dev/cu.* node instead.EINVAL.uname and /proc/version report Linux 6.18 LTS, a floor for
version-gated userspace; src/syscall/dispatch.tbl states what is
implemented.Read CONTRIBUTING.md first. It is the tracked style guide: the C conventions this tree actually follows, which formatter owns which part of a file, what CI gates and what it does not, and the seven commit-message rules the log is written to.
Two things settle most review comments before they are written:
make indent # clang-format and comment reflow (FORMAT_SHELL/FORMAT_PY add the rest)
make check-format # verify without rewriting
File an issue before a substantial change, so the design discussion happens before the effort does. Typo fixes, small refactors, and comment or documentation edits need no issue.
Apache License 2.0. See LICENSE.
Copyright 2026 elfuse contributors
Copyright 2025 Moritz Angermann, zw3rk pte. ltd.
C
85.6%
Shell
5.9%
Python
4.3%
Makefile
2.7%