Skip to content

About

Fix for Java/JVM crashing on Apple Silicon Macs: SIGBUS (0xa) in CodeHeap::allocate at startup (macOS 27, SIP disabled). Root cause: dyld hardware TPRO resets the JIT W^X state. Workaround, risks, tools. 修复 Mac 上 Java 启动即崩溃

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

English 简体中文

Native arm64 JVMs crash with SIGBUS in CodeHeap::allocate on macOS 27 (Apple Silicon)

A root-cause analysis of the crash, a system-wide workaround, and a user-space fallback.

Warning

The workaround changes an undocumented kernel boot argument and disables a hardware security feature for the entire system. Read Risks and limitations before applying it.

Last verified: 2026-09-29 · macOS 27.0 (26A428) · Apple M2 Max


Contents

TL;DR

  • On affected machines, every native arm64 HotSpot JVM tested (OpenJDK 17, 21 and 26) aborts during start-up with SIGBUS in CodeHeap::allocate, before any Java code runs.
  • Cause. When the kernel marks a process with dyld_hw_tpro=1, dyld protects its internal state with hardware TPRO (Trusted Path Read-Only). Each time dyld toggles TPRO, it writes a complete value into a per-thread permission register that also holds the thread's MAP_JIT write/execute state. This silently switches the thread back to execute mode. HotSpot caches its own view of that state, so after the first dlopen() during VM start-up it writes to its code cache while the thread is actually in execute mode.
  • Workaround. Add sprr_tpro=0 sprr_tpro_pagers=0 to the boot arguments and reboot. The kernel then no longer passes dyld_hw_tpro=1, dyld falls back to mprotect()-based protection, and the JIT state is no longer overwritten. Nothing in the JVM is modified, so this applies to every JVM version and vendor.
  • Fallback. A small interposing library, libjitfix.dylib, restores the JIT write state after dyld calls. It needs no reboot but has more limitations.

Symptoms

#  SIGBUS (0xa) at pc=0x..., pid=..., tid=...
# Problematic frame:
# V  [libjvm.dylib+0x...]  CodeHeap::allocate(unsigned long)+0x...

siginfo: si_signo: 10 (SIGBUS), si_code: 1 (BUS_ADRALN), si_addr: <page-aligned address>
  • The macOS crash report shows EXC_BAD_ACCESS (SIGABRT) with KERN_PROTECTION_FAILURE at the same address.
  • java -version is enough to reproduce it, and it reproduces every time.
  • The native stack is always CodeHeap::allocate ← CodeCache::allocate ← BufferBlob::create ← stub generation ← init_globals ← Threads::create_vm ← JNI_CreateJavaVM.
  • The faulting address is the first byte of a freshly committed code-heap region whose protection is rwx/rwx according to vmmap, so the memory is mapped correctly.
  • No JVM option helps. -Xint, -Xshare:off, -XX:-SegmentedCodeCache, a smaller -XX:ReservedCodeCacheSize and different collectors were all tried.
  • IDEs, Gradle, Maven and applications that bundle a JVM crash the same way.

Test environment

Item Value
Hardware Apple M2 Max (Mac14,6, T6020)
OS macOS 27.0 (26A428), xnu-13432.1.9, dyld-27062
Security settings SIP disabled; AMFI enabled (no amfi_get_out_of_my_way); other boot-args: -v ipc_control_port_options=0
JVMs Homebrew OpenJDK 17.0.18, 21.0.11, 26.0.1 (all arm64)

Root cause

1. HotSpot and per-thread W^X on Apple Silicon

On macOS/arm64, HotSpot allocates its code cache with MAP_JIT. For a given thread, a MAP_JIT page is either writable or executable, never both. The thread switches between the two with pthread_jit_write_protect_np(), which writes an Apple-specific system register. HotSpot tracks each thread's current mode (WXWrite / WXExec) itself and calls pthread_jit_write_protect_np() only when it believes the mode needs to change.

On the test machine the state can be read from user space (mrs x0, S3_6_C15_C1_5), and it is bit 21:

Thread state S3_6_C15_C1_5
JIT execute 0x2010002030100000
JIT write 0x2010002030300000

2. dyld's TPRO toggle overwrites the JIT state

dyld's internal memory manager (lsl::MemoryManager) can protect dyld's own state with hardware TPRO. Its constructor enables this only if the kernel passed dyld_hw_tpro in the process's apple[] strings (_simple_getenv(apple, "dyld_hw_tpro")).

With TPRO enabled, dyld makes its state writable or read-only (for example in dlopen_from, around image-load notifications and around initializers) by loading a precomputed value from the commpage and writing it to S3_6_C15_C1_5 with msr. One such site is dyld4::APIs::dlopen_from + 764 in dyld-27062. The value is written as a whole, so the thread's current JIT bit is not preserved. Every value observed (0x2010003030100000, 0x2010002030100000) has bit 21 cleared, which means JIT execute mode.

The transition was captured by setting lldb breakpoints on every instruction in dyld and libsystem that writes this register, then running java -version:

thread state before : W (JIT writable)
msr S3_6_C15_C1_5, x0   x0 = 0x2010003030100000     dyld4::APIs::dlopen_from
  <- os::Bsd::dlopen_helper
  <- ClassLoader::load_jimage_library
  <- ClassLoader::lookup_vm_options
  <- Arguments::parse
  <- Threads::create_vm
thread state after  : X (JIT execute)

HotSpot switches the thread to write mode early in Threads::create_vm, then dlopens libjimage and libjava. Still believing the thread is writable, it generates stubs into the code cache, and the first store raises SIGBUS.

Two observations support this chain:

  • In a process that does not receive dyld_hw_tpro, the same dlopen executes no write to this register, and the thread stays in write mode.
  • Re-asserting write mode after each dyld call (libjitfix.dylib, below) was enough to make the JVMs tested with it (17 and 26) start and pass the full workload in all 7 configurations.

3. Which processes receive dyld_hw_tpro

The kernel decides this in the exec path. xnu computes a per-process security configuration, which appears in apple[] as security_config=0x…. Bit 0x2 enables hardware TPRO for dyld; when it is set, apple[] also contains dyld_hw_tpro=1. Setting bit 0x2 requires, among other conditions, two global switches to be non-zero, and one of them is the tunable boot argument sprr_tpro (default 1). A second tunable, sprr_tpro_pagers (default 1), gates dyld_hw_tpro_pagers=1.

This comes from static analysis of kernel.release.t6020 from the build above. The kernel is stripped, so the kernel functions have no names. The meaning of the bits is inferred from the code that sets and reads them.

On the test machine the affected JVMs receive security_config=0x1a dyld_hw_tpro=1. The remaining bits (0x18) come from a separate condition in the same function.

Which executables get the flag is only partly understood:

  • The flag follows the file (inode). A hard link to the JDK's java still receives it; a byte-identical copy (a new inode) does not, and that copy runs normally.
  • It does not depend on path, file name, file contents, the SDK version the binary was built with (15.0 through 26.5 were all affected), creation time, or how often the binary has been run.
  • Of 168 Homebrew executables that could be checked, 92 had the flag. All 92 had been ad-hoc re-signed with codesign (not linker-signed). Of the 76 unflagged ones, 75 were linker-signed and 1 had been re-signed, so re-signing alone does not explain the flag.

Copying a JDK can therefore make it work, but since the selection criterion is unknown, this is not a reliable fix.

4. Relation to "SIP disabled"

The same crash has been reported since macOS 14, always on machines with SIP disabled (JBR-6473, JDK-8326663, corretto-11#385). An analysis in the Corretto issue attributes it to dyld taking this path when it considers the process platform-signed. On earlier macOS versions that happened to every process once AMFI was also disabled. OpenJDK closed JDK-8326663 as Won't Fix in February 2026, noting that enabling write mode just before CodeHeap::allocate would be undone by any later dlopen.

On the test machine, AMFI is enabled and the flag comes from the macOS 27 kernel logic described above. JetBrainsRuntime#641 reports the same crash on macOS 27 on an M1 Pro, without stating the SIP status. Whether re-enabling SIP avoids the problem on macOS 27 was not tested.

The same dyld routine that parses dyld_hw_tpro also checks whether the process name starts with MATLAB, a product that ships its own JVM. What that special case changes was not analyzed.

Workaround: disable hardware TPRO via boot-args

Prerequisites

  • An Apple Silicon Mac on which custom boot arguments take effect. This requires SIP to be disabled (Permissive Security). sysctl kern.bootargs shows the arguments the running kernel was actually booted with.
  • An administrator account.

Steps

  1. Build the tools and confirm that you are affected. The expected output is CRASH(…) security_config=0x1a dyld_hw_tpro=1.

    make && ./verify.sh
  2. Note your current boot arguments. You will keep them; the next step only appends to them.

    nvram boot-args
  3. Append the two arguments.

    sudo nvram boot-args="$(nvram boot-args 2>/dev/null | cut -f2-) sprr_tpro=0 sprr_tpro_pagers=0"
  4. Reboot.

  5. Verify. verify.sh should report OK security_config=0x18 dyld_hw_tpro=none for every JVM, and test/run.sh should report PASS for every configuration.

    ./verify.sh && ./test/run.sh

Why both arguments? sprr_tpro=0 alone removes dyld_hw_tpro. Some processes may still receive dyld_hw_tpro_pagers=1, which asks dyld to use TPRO pagers for the shared cache. sprr_tpro_pagers=0 removes that as well, so the kernel and dyld stay consistent.

Reverting. Remove the two arguments and reboot:

sudo nvram boot-args="$(nvram boot-args | cut -f2- | sed -E 's/ *sprr_tpro(_pagers)?=0//g')"

If this leaves boot-args empty, delete the variable instead:

sudo nvram -d boot-args

Fallback without rebooting: libjitfix.dylib

make
DYLD_INSERT_LIBRARIES="$PWD/libjitfix.dylib" java -version

The library interposes dlopen, dlclose, dlsym, dladdr and dlopen_preflight. If the calling thread was in JIT write mode before the call and is in execute mode afterwards, it calls pthread_jit_write_protect_np(0). Set JITFIX_VERBOSE=1 to log each restore. In testing with JDK 26 and a JIT-heavy workload, dyld reset the state 9 times and the library restored it each time.

Use this only as a stop-gap; see its limitations.

Verification results

The test machine was measured before and after applying the boot arguments. test/run.sh runs a workload with C1/C2 compilation, concurrent compilation on 8 threads, invokedynamic, and native libraries (libzip, libnet, libnio, libawt) under 7 configurations: default, JDWP agent, JFR, -Xcomp, C1 only, -Xint, and -Xshare:off with Serial GC.

Before After sprr_tpro=0 sprr_tpro_pagers=0
apple[] of the JVMs security_config=0x1a dyld_hw_tpro=1 security_config=0x18, no dyld_hw_tpro
java -version (17 / 21 / 26) SIGBUS OK
test/run.sh (3 JDKs × 7 configurations) all crash 21 / 21 PASS
JIT-state resets by dyld during the workload (JDK 26, counted by libjitfix.dylib with JITFIX_VERBOSE=1) 9 0
Executables flagged by the kernel (Homebrew, 168 checked) 92 × 0x1a 92 × 0x18
TPRO-related kernel log messages after reboot — none

Risks and limitations

The boot-arg workaround

  1. It weakens a security mitigation for the whole system. With sprr_tpro=0, no process receives hardware TPRO, including Apple applications that opt into Enhanced Security (for example browsers and messaging apps). dyld and the system libraries fall back to software (mprotect()-based) protection, but an attacker who already has a memory-write primitive has an easier job tampering with that state. With SIP also disabled, consider this carefully before using the machine for untrusted content.
  2. It relies on an undocumented, unsupported kernel tunable. sprr_tpro is an internal switch. Apple can rename or remove it, or change its meaning, in any update. That could silently bring the crash back or cause different problems. Run ./verify.sh after every macOS update, and use sysctl kern.bootargs to check that the arguments are still active.
  3. Side effects have not been fully analyzed. The same kernel variable is also read by virtual-memory mapping code. The paths reviewed simply skip TPRO-specific handling when it is disabled, which is the same software path that most third-party processes already use, but the review was not exhaustive. After the reboot on the test machine, there were no TPRO-related kernel log messages. One third-party application that loads an injected third-party library crashed once at login; no causal link was established. Software that patches or hooks memory while it is being loaded is the most likely to be sensitive to this change.
  4. Editing boot arguments is a low-level operation. A typo can prevent a normal boot. Always keep the existing arguments. To recover, start up in macOS Recovery, open Terminal, and restore the previous value with nvram boot-args="…" or delete it with nvram -d boot-args, then restart.
  5. It only works while custom boot arguments are honored. If you re-enable SIP (or otherwise raise the security policy), the arguments may be ignored and the crash may return.
  6. It was tested on exactly one configuration, listed under Test environment. The sprr_tpro tunable exists in all 13 Apple Silicon kernels shipped with this build (T6000 through T8152). Behavior on other chips and macOS versions has not been verified.

libjitfix.dylib

  1. It only works in processes that honor DYLD_INSERT_LIBRARIES, meaning binaries without the hardened runtime or with the allow-dyld-environment-variables entitlement. The variable must also actually reach the JVM, which is not guaranteed for IDEs, Gradle daemons and other launchers. Setting it globally, for example with launchctl setenv, injects the library into every process and is not recommended.
  2. It only covers the interposed dyld entry points. Other paths into dyld that may toggle TPRO, such as other _dyld_* APIs or thread-local-variable setup, are not covered. The library relies on HotSpot's own W^X transitions to correct the state in those cases.
  3. It deliberately undoes a state change that dyld made for security reasons. Saving a permission state and restoring it later is the kind of pattern that upstream developers considered unsafe to adopt in general, because the saved state exists in memory for a short time.
  4. It is a workaround, not a fix for HotSpot or for macOS.

General

This project is not affiliated with Apple, Oracle or the OpenJDK project. The findings come from observing and reverse-engineering one specific build and may be wrong in details. Everything here is provided as is, without warranty; use it at your own risk.

Open questions

  • The exact criterion the kernel uses to decide which executables receive the flag.
  • Whether macOS 27 with SIP enabled is affected.
  • What the MATLAB special case in dyld does.
  • Behavior on Apple Silicon generations other than M2.

Reports from other machines are welcome. The output of ./verify.sh and sysctl -n machdep.cpu.brand_string kern.bootargs together with sw_vers is enough to start.

How the analysis was done

  1. Reproduction and isolation. A minimal C program that follows HotSpot's exact sequence (reserve a MAP_JIT region, mprotect parts of it to RWX, switch to write mode, write) works. So the problem depends on what else happens inside the JVM process.
  2. Interposition. DYLD_INSERT_LIBRARIES hooks logged every mmap, mprotect and pthread_jit_write_protect_np call. They showed that the thread's write mode was lost between HotSpot's own calls.
  3. State probe. The JIT state was read directly from S3_6_C15_C1_5 at HotSpot function boundaries with lldb, which narrowed the change down to a single dlopen().
  4. Catching the writer. lldb breakpoints were set on every instruction in dyld and libsystem that writes the register, which pinpointed dyld's TPRO toggle.
  5. Finding the gate. dyld was disassembled (dyld_hw_tpro → lsl::MemoryManager), and so was the kernel (security_config, dyld_hw_tpro=1, and the tunable table entries for sprr_tpro and sprr_tpro_pagers).
  6. Confirming the fix. The boot arguments were applied, the machine rebooted, and the measurements above were repeated.

tprocheck.c (which prints a process's security_config and dyld_hw_tpro) and jitfix.c (which reads the JIT state) show how to observe the relevant state yourself.

Repository layout

Path Purpose
tprocheck.c Injected library that prints the security_config / dyld_hw_tpro a process received, then exits before main
jitfix.c User-space fallback that restores JIT write mode after dyld calls
verify.sh Shows active boot-args, and for each installed JVM its TPRO status and whether it starts
test/Workload.java, test/run.sh JIT-heavy workload run under 7 HotSpot configurations
Makefile make, make verify, make test, make clean

Requirements: Xcode Command Line Tools (clang), zsh, and at least one JDK.

References

About

Fix for Java/JVM crashing on Apple Silicon Macs: SIGBUS (0xa) in CodeHeap::allocate at startup (macOS 27, SIP disabled). Root cause: dyld hardware TPRO resets the JIT W^X state. Workaround, risks, tools. 修复 Mac 上 Java 启动即崩溃

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages