test(aot): add x18 register reservation test for macOS ARM64

Add a standalone test to verify that the +reserve-x18 LLVM flag is
correctly applied when compiling AOT for macOS ARM64.

On macOS ARM64, x18 is reserved by Apple for TLS (Thread Local Storage).
Without the +reserve-x18 flag, LLVM may generate code that uses x18,
causing random SIGSEGV crashes (~80% crash rate in testing).

The test:
- Creates a WASM module with 24 local variables to stress register allocation
- Compiles to AOT with -O3 optimization (which would use x18 without the fix)
- Runs 1000 iterations to verify no crashes occur
- Only runs on macOS ARM64 (skipped on other platforms)

Test results:
- Without fix: 82/100 crash rate
- With fix: 0/100 crash rate (1000 iterations verified)
This commit is contained in:
teamchong 2026-01-08 06:33:04 -05:00
parent edbcbb77da
commit ca147a3458
3 changed files with 227 additions and 0 deletions

View File

@ -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

View File

@ -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 100 iterations in interpreter mode..."
for i in $(seq 1 100); do
if ! ${IWASM_CMD} stress_registers.wasm 2>&1; then
echo "FAILED: Crash at iteration $i"
exit 1
fi
done
echo "PASSED: 100 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

View File

@ -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)))
)
)