Skip to content

ARM64 (aarch64) Linux host support - #1818

Merged
LubosD merged 32 commits into
masterfrom
arm64-support
Nov 6, 2026
Merged

LubosD merged 32 commits into
masterfrom
arm64-support

Conversation

@LubosD

@LubosD LubosD commented Nov 6, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds support for building and running Darling on ARM64 (aarch64) Linux hosts, with native arm64 Mach-O binaries. This supersedes #1753: it contains that PR's base port plus the runtime fixes needed to actually boot the container and run programs.

On an aarch64 host, darling shell now starts and runs native arm64 Mach-O binaries: bash, coreutils, pipes and job control, launchctl, Perl 5.18, Ruby 2.6 and Python 2.7.

What's in here

Base port (from #1753, by @kkHAIKE)

  • Build and SDK: aarch64 host detection and the TARGET_ARM64 option, arm/arm64 SDK header links, and framework and tool build adjustments.
  • libm: an ARM64 math runtime, ported from openlibm plus hardware builtins.
  • mldr: ARM64 startup support.
  • clang 16+: relaxed C strictness so the older vendored code still builds.

Runtime fixes (from a follow-up patchset by fervi)

  • shellspawn: fixed out-of-bounds fd cleanup and child exit-status handling, and replaced the EVFILT_PROC kqueue loop with a poll() + waitpid() loop.
  • darling.c: accepts sid == 0 for darlingserver, and the shellspawn wait timeout is now 60 s.
  • mldr: install path for binfmt_misc, a fixed dyld_path, and paths already inside the container are no longer prefixed with SYSTEM_ROOT.

Fixes made while bringing this up on a 48-bit-VA aarch64 kernel

  • FSEvents: linux/fanotify.h no longer includes the host's <linux/types.h>. That include only worked on x86_64 because gcc-multilib installs /usr/include/asm.
  • Submodule fixes (see the linked PRs), cherry-picked from the VibeDarling forks with attribution:
    • darlingserver's priority_queue sign-extended heap pointers (0xaaaa… became 0xffffaaaa…), and the timer queue crashed.
    • dyld's _tlv_get_addr read TPIDRRO_EL0, which Linux always leaves at 0.
    • The xnu low-VA mmap arena used MAP_FIXED and silently replaced live mappings. It now uses MAP_FIXED_NOREPLACE.
    • libffi's CFI directives didn't assemble for Mach-O.

Deviations from the original patches

  • objc4: the "part2" patch that hardcoded OBJC_IS_DEBUG_BUILD was dropped. Upstream already derives it from NDEBUG (Update objc4 to derive OBJC_IS_DEBUG_BUILD from NDEBUG #1798).
  • Gitlinks: the patchset's submodule bumps pointed at commits that exist only in the author's forks. Each submodule's series was applied onto an arm64-support branch in our repos instead (PRs below).
  • Top-level CMakeLists.txt: a context conflict with the ObjC-literals block, resolved by hand.

Submodule PRs (merge these first)

⚠️ The gitlinks here point at the head commits of those arm64-support branches. If the submodule PRs are squash- or rebase-merged, the commits get new SHAs and this PR needs a bump before it merges. A plain merge commit keeps the SHAs.

Testing

  • aarch64: a full build (≈26k ninja steps) on Debian 13 with clang 19.1.7 in a privileged Docker container, on a 48-bit-VA linuxkit kernel with 4 KB pages. darling shell runs: uname, sw_vers, ls, pipes, background jobs, file (it reports Mach-O 64-bit executable arm64), sysctl, launchctl list, and Perl, Ruby and Python one-liners.
  • x86_64: not re-tested yet. Most changes are behind __aarch64__/__arm64__/TARGET_ARM64 guards, but this still needs an x86_64 build and smoke test before merge.

Known issues (follow-ups)

  • defaults CLI: the tool is GNUstep's, so it doesn't accept macOS type flags (-string, -int, -bool, -array, -dict-add, …), and defaults read domain key prints domain key value instead of just the value. Persistence itself is fixed here; see the foundation PR.
  • Untested: anything graphical.

Fixed since this PR was opened: the cross-thread TSD mix-ups and main-thread TSD registration (xnu + dyld), hw.machine/uname -m now report arm64, NSUserDefaults persistent domains (defaults write now persists), libpthread's ____chkstk_darwin no longer reads TPIDRRO_EL0, and tools/shutdown-user.sh (run by ninja install) no longer hangs on, and now actually stops, containers in a non-default DPREFIX. An interactive shell in a pty now works: tty/ttyname() find /dev/pts/N, and tcsetattr() no longer resets the line speed to B0 (generic bugs, not arm64-specific).

Provenance

  • Base port: taken from ARM64 (aarch64) runtime, ABI, and build-system support #1753 by @kkHAIKE, as included in a patchset posted publicly by fervi. The follow-up runtime fixes come from that same patchset, and commit authorship is preserved.
  • Cherry-picks: from the VibeDarling forks of these repositories, which use the same licenses as ours. Each one is recorded with git cherry-pick -x and credited in the commit messages.
  • New fixes: written from the public Linux/AArch64 ABI (the TPIDRRO_EL0 semantics, MAP_FIXED_NOREPLACE) and from the XNU priority_queue source that is already in our tree.
  • Debugging method: gdb and strace on Darling's own builds only. No Apple binaries were disassembled, and no restricted material (corecrypto or similar) was used.
  • AI assistance: prepared with Claude Code. Opened as a draft until the author has reviewed it per AGENTS.md and x86_64 is re-tested.

🤖 Generated with Claude Code

kkHAIKE and others added 20 commits November 6, 2026 08:01
Top-level CMakeLists.txt:
  * detect host arch via `uname -m` so subsequent project() and the
    SDK-platform error message recognise aarch64 hosts.
  * add TARGET_ARM64 option, defaulted to ON when host is aarch64
    (and i386/x86_64 default to OFF in that case).

cmake helpers:
  * architecture.cmake: pick APPLE_ARCH_64BIT="arm64" and the matching
    triplets when TARGET_ARM64 is on.
  * compiler_include.cmake: arm64 triplet handling for the per-arch
    compile flags.

basic-headers/arm/arch.h: thin stub matching the existing x86 layout
so anything that does `#include <arm/arch.h>` resolves on arm64 builds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Apple's MacOSX.sdk ships per-arch include trees:
  architecture/arm/...
  arm/...
  libkern/arm/...
  mach/arm/...
  mach/arm64/...

All but a handful of these files are thin symlinks (the typical SDK
layout where, e.g., arm/_types.h aliases an existing public header).
Add them so any source that does `#include <arm/...>` or
`#include <mach/arm/...>` resolves on arm64 builds. A couple of
`mach/arm/_structs.h` / `mach/arm/thread_status.h` get small
adjustments to match the canonical Darwin layout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add the per-arch libm sources for arm64:
  * Source/ARM/arm64_builtins.c — synthesises the builtins Darwin's
    libm exposes that LLVM compiler-rt doesn't provide on Linux/arm64.
  * Source/ARM/fenv.{c,h}, fpclassify.c, required_arithmetic.h, math.h
    — arm-specific implementations of fenv/fpclassify/required_arithmetic
    and the public ARM math.h header.
  * include/architecture/arm/math.h — symlink to Source/ARM/math.h.
  * top-level CMakeLists.txt: wire the ARM/ sources in when
    TARGET_ARM64 is on.

Also: Source/complex.h grows an aarch64 branch (was `__arm__` only,
which is 32-bit) so libm/Source/ARM/complex.c (and any consumer of
this header) builds on Linux/aarch64.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add the bits the Mach-O loader needs on arm64:
  * include/{arm/cpu_capabilities,libkern/arm/...,mach/arm/...}.h —
    the arm-specific helpers that mldr's translation layer needs at
    build time on aarch64 hosts.
  * elfcalls/threads.c — wire up arm64 thread bootstrap helpers.
  * commpage.c — arm64 commpage initialisation.
  * mldr.c — branch out for arm64 in entry/setup code.
  * loader.c — when loading the Mach-O image on arm64, force the
    initial mmap into the low-VA window (<2^47). Linux ARM64 kernels
    happily return 48-bit VAs; anything above 2^47 collides with
    ObjC's FAST_DATA_MASK (47 bits) and class_initialize SIGSEGVs.
    Retry with MAP_FIXED if the kernel hands out a high VA, and bump
    a sequential slot so successive loads don't collide.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
  * frameworks/CoreServices: add reexport_arm64.exp and wire it in
    via CMakeLists.txt; Components.h / ComponentManager.cpp small
    branch-out for arm64.
  * launchd/src/core.c: arm64-conditional adjustment for the spawn
    path (kept minimal — see arm64 commit log).
  * libelfloader/wrapgen/wrapgen.cpp: arm64-aware codegen tweaks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e files

Rework the ARM64 libm, choosing each symbol's source deliberately:

- clang __builtin_* for ops that lower to single/few ARM64 instructions
  (sqrt/fabs/ceil/floor/trunc/rint/round/fmin/fmax/fma/lround/llround/
  lrint/llrint/copysign). Verified at -O2 to be pure hardware lowering with
  no self-recursion; drop the old wrappers that fell back to libcalls.
- openlibm (FreeBSD msun) C for transcendentals/complex/bessel and for the
  Apple files that were soft-float-only stubs on arm64 (fmod, modf, exp2).
- Apple's own arch-portable implementations kept where valid and without an
  arm64 perf regression (cbrt, fmodf, log10/log1p via log.c, nextafter[f]).

fenv: split the ARM64 declarations out of the Source/fenv.h dispatcher into
Source/architecture/arm/fenv.h (+ include/ and SDK symlinks), matching the
i386 layout; values are taken verbatim from Apple's shipping arm64 fenv.h.
Source/ARM/fenv.h stays a self-contained implementation-private header (like
Source/Intel/fenv.h) carrying the mrs/msr FPCR/FPSR accessors.

Also: classify backings (__isnan/__isinf/__isfinite/__isnormal/__signbit/
__fpclassify*), legacy + Apple-private wrappers (finite/matherr/rinttol/
roundtol/significand/__inf/__inff/__nan), GNU fenv extensions (fegetexcept/
fesetexcept/__fegetfltrounds), ___Libm_version, and exp2f. Exports/alias are
brought to the AMD64 ABI (294/295 symbols; the only gap is the Intel-only
__FE_DFL_DISABLE_SSE_DENORMS_ENV). Newly ported openlibm files use fdlibm
names (e_/s_/k_) like Source/Intel; pre-existing _freeBSD.c names are kept.

deps: fix the migexe -> chmod -> libsyscall -> migexe cyclic dependency in
the bootstrap_cmds submodule by invoking /bin/chmod through cmake -E env
instead of a bare `chmod` (which CMake bound to the Darling Mach-O chmod
target); /bin/chmod also works on cmake 3.18 where `cmake -E chmod` does not.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
clang 16 promoted several long-standing warnings to errors by default:
  -Werror=implicit-function-declaration
  -Werror=implicit-int
  -Werror=int-conversion
  -Werror=incompatible-function-pointer-types

Many of Darling's vendored C sources predate C99 strictness and trip
these. Demote them back to warnings inside an `if(clang>=16)` guard so
older clangs are unaffected. This unblocks building the rest of the
upgrade on amd64 with a modern toolchain (clang-19 in our case);
the libcxx-15 bump that follows requires clang-14+ anyway.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The submodule bumps in the posted ARM64 patchset reference commits that
only exist in the patch author's forks. Instead, the per-submodule patch
series were applied locally onto an arm64-support branch in each
submodule, and this commit records those commits.

objc4: the "part2" patch (hardcoding OBJC_IS_DEBUG_BUILD) was dropped,
since upstream already derives it from NDEBUG.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fseventsd is built for the Darwin target, so <linux/types.h> resolves to
the host header, which needs the arch-specific <asm/types.h>. On x86_64
Debian that only works because gcc-multilib installs /usr/include/asm;
on aarch64 the build fails. Define the few kernel types used directly.

Content matches VibeDarling/darling@9c5adac29 ("compat: Android Bionic
libc, signal range, and shared memory adaptations"), which fixed the
same problem.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Runtime fixes needed on aarch64 hosts with a 48-bit VA, cherry-picked
from the VibeDarling forks (which carry a newer version of this ARM64
port):

- darlingserver: don't pack priority_queue child pointers under
  __DARLING__ (sign-extending 0xaaaa... heap pointers crashed the timer
  queue) [VibeDarling/darlingserver@3281745c0]
- dyld: don't read TPIDRRO_EL0 in _tlv_get_addr; Linux keeps it 0
  [VibeDarling/darling-dyld@640b559b8, @fe73598dd]
- libffi: cfi_startproc after the label so Mach-O CFI assembles
  [VibeDarling/darling-libffi@e67f81120]
- xnu: low-VA arena uses MAP_FIXED_NOREPLACE instead of clobbering
  existing mappings, then moved into BSD mmap
  [VibeDarling/darling-xnu@2c98884, @6bc004a, @499e41d, @e0ee023]

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@LubosD

LubosD commented Nov 6, 2026

Copy link
Copy Markdown
Member Author

A lot of this was probably already done in VibeDarling, I exclusively worked on arm64.

Yes, I used Claude to get it running + I told it to check VibeDarling for hints.

@LubosD
LubosD merged commit 58d2b2a into master Nov 6, 2026
6 checks passed
This was referenced Nov 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants