diff --git a/.github/workflows/compilation_on_macos.yml b/.github/workflows/compilation_on_macos.yml index 9fcc34b435..e2538117dd 100644 --- a/.github/workflows/compilation_on_macos.yml +++ b/.github/workflows/compilation_on_macos.yml @@ -440,3 +440,13 @@ jobs: cmake .. cmake --build . --config Release --parallel 4 ./import-func-callback + + - name: Test x18 register reservation (macOS ARM64 only) + if: matrix.os == 'macos-15' + run: | + cd product-mini/platforms/darwin + mkdir -p build && cd build + cmake .. -DWAMR_BUILD_AOT=1 + cmake --build . --config Release --parallel 4 + cd ../../../../tests/standalone/test-aot-x18-reserve + ./run.sh --aot diff --git a/core/iwasm/compilation/aot_llvm.c b/core/iwasm/compilation/aot_llvm.c index ab351e6d44..4e8fd2cf59 100644 --- a/core/iwasm/compilation/aot_llvm.c +++ b/core/iwasm/compilation/aot_llvm.c @@ -3005,6 +3005,10 @@ aot_create_comp_context(const AOTCompData *comp_data, aot_comp_option_t option) else vendor_sys = "-pc-windows-"; } + else if (!strcmp(abi, "darwin") || !strcmp(abi, "macho")) { + /* macOS/Darwin: x18 is reserved by Apple */ + vendor_sys = "-apple-"; + } else { if (is_baremetal_target(arch, cpu, abi)) vendor_sys = "-unknown-none-"; @@ -3050,6 +3054,14 @@ aot_create_comp_context(const AOTCompData *comp_data, aot_comp_option_t option) if (!abi) abi = "gnu"; } + else if (strstr(default_triple, "darwin") + || strstr(default_triple, "apple")) { + /* macOS/Darwin: x18 is reserved by Apple, must use correct + * triple to prevent LLVM from using it */ + vendor_sys = "-apple-darwin"; + if (!abi) + abi = ""; + } else { vendor_sys = "-pc-linux-"; if (!abi) @@ -3139,6 +3151,30 @@ aot_create_comp_context(const AOTCompData *comp_data, aot_comp_option_t option) if (!features) features = ""; +#if (defined(__APPLE__) || defined(__MACH__)) && defined(BUILD_TARGET_AARCH64) + /* On macOS ARM64, x18 is reserved by Apple for TLS. Even though we're + * generating ELF (Linux-style) AOT files, we must tell LLVM to not + * use x18, otherwise the AOT code will crash when running on macOS. */ + { + bool is_aarch64 = false; + if (arch && !strncmp(arch, "aarch64", 7)) + is_aarch64 = true; + else if (triple_norm && strstr(triple_norm, "aarch64")) + is_aarch64 = true; + + if (is_aarch64) { + if (features[0] != '\0') { + snprintf(features_buf, sizeof(features_buf), + "%s,+reserve-x18", features); + features = features_buf; + } + else { + features = "+reserve-x18"; + } + } + } +#endif + /* Get target with triple, note that LLVMGetTargetFromTriple() return 0 when success, but not true. */ if (LLVMGetTargetFromTriple(triple_norm, &target, &err) != 0) { diff --git a/tests/standalone/test-aot-x18-reserve/run.sh b/tests/standalone/test-aot-x18-reserve/run.sh new file mode 100755 index 0000000000..fe31e2d17c --- /dev/null +++ b/tests/standalone/test-aot-x18-reserve/run.sh @@ -0,0 +1,120 @@ +#!/bin/bash +# +# Copyright (C) 2019 Intel Corporation. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# Test for x18 register reservation on macOS ARM64 (aarch64). +# +# On macOS ARM64, x18 is reserved by Apple for TLS (Thread Local Storage). +# Without the +reserve-x18 LLVM flag, the AOT compiler may generate code +# that uses x18, causing random SIGSEGV crashes when run on macOS. +# +# This test compiles a WASM module that stresses register allocation +# (forcing x18 usage without the fix) and runs it 1000 times to verify +# no crashes occur. +# + +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +WAMR_DIR="${SCRIPT_DIR}/../../.." + +# Detect platform +UNAME_S=$(uname -s) +UNAME_M=$(uname -m) + +# Only run this test on macOS ARM64 +if [[ "${UNAME_S}" != "Darwin" ]] || [[ "${UNAME_M}" != "arm64" ]]; then + echo "Skipping x18 reserve test: only applicable on macOS ARM64" + echo "Current platform: ${UNAME_S} ${UNAME_M}" + exit 0 +fi + +# Determine iwasm path based on platform +if [[ "${UNAME_S}" == "Darwin" ]]; then + IWASM_CMD="${WAMR_DIR}/product-mini/platforms/darwin/build/iwasm" +else + IWASM_CMD="${WAMR_DIR}/product-mini/platforms/linux/build/iwasm" +fi + +WAMRC_CMD="${WAMR_DIR}/wamr-compiler/build/wamrc" + +# Check if required binaries exist +if [[ ! -x "${IWASM_CMD}" ]]; then + echo "Error: iwasm not found at ${IWASM_CMD}" + echo "Please build iwasm first" + exit 1 +fi + +if [[ ! -x "${WAMRC_CMD}" ]]; then + echo "Error: wamrc not found at ${WAMRC_CMD}" + echo "Please build wamrc first" + exit 1 +fi + +cd "${SCRIPT_DIR}" + +# Find wat2wasm (check CI path first, then system PATH) +if [[ -x "/opt/wabt/bin/wat2wasm" ]]; then + WAT2WASM="/opt/wabt/bin/wat2wasm" +elif command -v wat2wasm &> /dev/null; then + WAT2WASM="wat2wasm" +else + echo "Error: wat2wasm not found" + echo "Please install wabt tools" + exit 1 +fi + +# Compile WAT to WASM if needed +if [[ ! -f stress_registers.wasm ]] || [[ stress_registers.wat -nt stress_registers.wasm ]]; then + echo "Compiling stress_registers.wat to WASM..." + if ! ${WAT2WASM} stress_registers.wat -o stress_registers.wasm; then + echo "Error: Failed to compile WAT to WASM" + exit 1 + fi +fi + +if [[ $1 != "--aot" ]]; then + echo "============> run stress_registers.wasm (interpreter mode)" + echo "Running 1000 iterations in interpreter mode..." + for i in $(seq 1 1000); do + if ! ${IWASM_CMD} stress_registers.wasm 2>&1; then + echo "FAILED: Crash at iteration $i" + exit 1 + fi + done + echo "PASSED: 1000 iterations completed without crash" +else + echo "============> compile stress_registers.wasm to AOT" + + # Compile to AOT - the fix should add +reserve-x18 automatically on macOS ARM64 + if ! ${WAMRC_CMD} --opt-level=3 -o stress_registers.aot stress_registers.wasm; then + echo "Error: Failed to compile WASM to AOT" + exit 1 + fi + + echo "============> run stress_registers.aot" + echo "Running 1000 iterations to verify x18 is properly reserved..." + echo "(Without the fix, this would crash ~80% of the time)" + + failed=0 + for i in $(seq 1 1000); do + if ! ${IWASM_CMD} stress_registers.aot 2>&1 > /dev/null; then + echo "FAILED: Crash at iteration $i" + failed=1 + break + fi + # Progress indicator every 100 iterations + if [[ $((i % 100)) -eq 0 ]]; then + echo " Progress: $i/1000 iterations completed" + fi + done + + if [[ ${failed} -eq 0 ]]; then + echo "PASSED: 1000 iterations completed without crash" + echo "The +reserve-x18 fix is working correctly" + exit 0 + else + echo "FAILED: x18 register corruption detected" + echo "The +reserve-x18 fix may not be applied correctly" + exit 1 + fi +fi diff --git a/tests/standalone/test-aot-x18-reserve/stress_registers.wat b/tests/standalone/test-aot-x18-reserve/stress_registers.wat new file mode 100644 index 0000000000..71b627fd5d --- /dev/null +++ b/tests/standalone/test-aot-x18-reserve/stress_registers.wat @@ -0,0 +1,97 @@ +;; Copyright (C) 2019 Intel Corporation. All rights reserved. +;; SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +;; Test module that uses many local variables to stress register allocation. +;; On ARM64, this will force LLVM to use x18 register if +reserve-x18 is not set. +;; x18 is reserved by Apple on macOS for TLS, so using it causes crashes. + +(module + (memory (export "memory") 1) + + (func $stress_registers (export "stress_registers") (param $input i64) (result i64) + (local $a i64) (local $b i64) (local $c i64) (local $d i64) + (local $e i64) (local $f i64) (local $g i64) (local $h i64) + (local $i i64) (local $j i64) (local $k i64) (local $l i64) + (local $m i64) (local $n i64) (local $o i64) (local $p i64) + (local $q i64) (local $r i64) (local $s i64) (local $t i64) + (local $u i64) (local $v i64) (local $w i64) (local $x i64) + + ;; Initialize all locals with different values based on input + (local.set $a (i64.add (local.get $input) (i64.const 1))) + (local.set $b (i64.mul (local.get $a) (i64.const 2))) + (local.set $c (i64.add (local.get $b) (i64.const 3))) + (local.set $d (i64.mul (local.get $c) (i64.const 4))) + (local.set $e (i64.add (local.get $d) (i64.const 5))) + (local.set $f (i64.mul (local.get $e) (i64.const 6))) + (local.set $g (i64.add (local.get $f) (i64.const 7))) + (local.set $h (i64.mul (local.get $g) (i64.const 8))) + (local.set $i (i64.add (local.get $h) (i64.const 9))) + (local.set $j (i64.mul (local.get $i) (i64.const 10))) + (local.set $k (i64.add (local.get $j) (i64.const 11))) + (local.set $l (i64.mul (local.get $k) (i64.const 12))) + (local.set $m (i64.add (local.get $l) (i64.const 13))) + (local.set $n (i64.mul (local.get $m) (i64.const 14))) + (local.set $o (i64.add (local.get $n) (i64.const 15))) + (local.set $p (i64.mul (local.get $o) (i64.const 16))) + (local.set $q (i64.add (local.get $p) (i64.const 17))) + (local.set $r (i64.mul (local.get $q) (i64.const 18))) + (local.set $s (i64.add (local.get $r) (i64.const 19))) + (local.set $t (i64.mul (local.get $s) (i64.const 20))) + (local.set $u (i64.add (local.get $t) (i64.const 21))) + (local.set $v (i64.mul (local.get $u) (i64.const 22))) + (local.set $w (i64.add (local.get $v) (i64.const 23))) + (local.set $x (i64.mul (local.get $w) (i64.const 24))) + + ;; Now use all of them together to prevent optimization + (i64.add + (i64.add + (i64.add + (i64.add + (i64.add + (i64.add + (i64.add + (i64.add + (i64.add + (i64.add + (i64.add + (i64.add + (local.get $a) + (local.get $b)) + (local.get $c)) + (local.get $d)) + (local.get $e)) + (local.get $f)) + (local.get $g)) + (local.get $h)) + (local.get $i)) + (local.get $j)) + (local.get $k)) + (local.get $l)) + (i64.add + (i64.add + (i64.add + (i64.add + (i64.add + (i64.add + (i64.add + (i64.add + (i64.add + (i64.add + (i64.add + (local.get $m) + (local.get $n)) + (local.get $o)) + (local.get $p)) + (local.get $q)) + (local.get $r)) + (local.get $s)) + (local.get $t)) + (local.get $u)) + (local.get $v)) + (local.get $w)) + (local.get $x)))) + + (func $_start (export "_start") + (drop (call $stress_registers (i64.const 42))) + ) +)