Problem
I ran into an ARM64 Lambda that would randomly die with:
FATAL SIGNAL: SIGILL
Error Type: Runtime.ExitError
The same binary worked on some Lambda hosts and consistently failed on others. It turned out the failing hosts didn't support ARM SHA3, while the binary contained unconditional eor3 instructions.
I eventually traced those instructions back to native code inside Rust dependencies. I was building on Apple Silicon, and the Zig C compiler wrapper generated by cargo-zigbuild contained -mcpu=native from the parent ../.cargo/config.toml but it also works(fails) within the same folder.
Environment
- Cargo Lambda:
1.9.1
- cargo-zigbuild:
0.20.1
- Build host: Apple M4 Max / macOS
- Target:
aarch64-unknown-linux-gnu
- Lambda runtime:
provided.al2023
- Lambda architecture:
arm64
Configuration that triggers it
My Cargo configuration contains:
[build]
rustflags = ["-C", "target-cpu=native"]
Cargo Lambda overrides this with target-cpu=neoverse-n1 for Rust compilation. However, it looks like cargo-zigbuild reads the original Cargo configuration again when creating the compiler wrapper for native dependencies.
I found this generated wrapper at:
~/Library/Caches/cargo-zigbuild/0.20.1/zigcc-aarch64-unknown-linux-gnu-3635.sh
It contained:
exec "/opt/homebrew/bin/cargo-lambda" zig cc -- -g -fno-sanitize=all -mcpu=native -target aarch64-linux-gnu "$@"
So Rust code gets the Lambda-specific CPU target, while C code inside dependencies gets -mcpu=native.
I know that using target-cpu=native directly for cross-compilation isn't portable. The confusing part is that Cargo Lambda replaces it for Rust but the original value still leaks into native dependency compilation.
Reproducer
https://github.com/libbkmz/cargo-lambda-arm64-sigill-repro
The repository contains a small Rust Lambda using Ring's X25519 API. The script builds it twice:
- With the inherited
target-cpu=native
- With native dependencies limited to baseline ARMv8-A
Run:
On my Apple Silicon machine, this produces:
=== broken: compiler-generated SHA3 instructions ===
eor3 count: 42
113248: ce036cbd eor3 v29.16b, v5.16b, v3.16b, v27.16b
=== working: compiler-generated SHA3 instructions ===
eor3 count: 0
none
No AWS deployment is needed to reproduce the build difference. The script disassembles Ring's table_select function and checks for compiler-generated eor3 instructions.
Expected
Once Cargo Lambda selects neoverse-n1 for an ARM64 Lambda build, native dependencies should use the same target or another Lambda-compatible ARM64 baseline.
The build shouldn't silently use the Apple host's CPU features for native dependencies.
Actual
Rust compilation uses the Lambda CPU target, but the generated Zig C wrapper uses -mcpu=native.
This can produce instructions unsupported by some Lambda ARM64 hosts. The resulting failure looks intermittent because it depends on which host runs the function.
Workaround
Forcing native dependencies to use baseline ARMv8-A removes the instructions:
[env]
CFLAGS_aarch64_unknown_linux_gnu = { value = "-march=armv8-a", force = true }
After rebuilding with this setting, the affected function contains no eor3 instructions, and I no longer see the SIGILL failures.
Problem
I ran into an ARM64 Lambda that would randomly die with:
The same binary worked on some Lambda hosts and consistently failed on others. It turned out the failing hosts didn't support ARM SHA3, while the binary contained unconditional
eor3instructions.I eventually traced those instructions back to native code inside Rust dependencies. I was building on Apple Silicon, and the Zig C compiler wrapper generated by cargo-zigbuild contained
-mcpu=nativefrom the parent../.cargo/config.tomlbut it also works(fails) within the same folder.Environment
1.9.10.20.1aarch64-unknown-linux-gnuprovided.al2023arm64Configuration that triggers it
My Cargo configuration contains:
Cargo Lambda overrides this with
target-cpu=neoverse-n1for Rust compilation. However, it looks like cargo-zigbuild reads the original Cargo configuration again when creating the compiler wrapper for native dependencies.I found this generated wrapper at:
It contained:
So Rust code gets the Lambda-specific CPU target, while C code inside dependencies gets
-mcpu=native.I know that using
target-cpu=nativedirectly for cross-compilation isn't portable. The confusing part is that Cargo Lambda replaces it for Rust but the original value still leaks into native dependency compilation.Reproducer
https://github.com/libbkmz/cargo-lambda-arm64-sigill-repro
The repository contains a small Rust Lambda using Ring's X25519 API. The script builds it twice:
target-cpu=nativeRun:
On my Apple Silicon machine, this produces:
No AWS deployment is needed to reproduce the build difference. The script disassembles Ring's
table_selectfunction and checks for compiler-generatedeor3instructions.Expected
Once Cargo Lambda selects
neoverse-n1for an ARM64 Lambda build, native dependencies should use the same target or another Lambda-compatible ARM64 baseline.The build shouldn't silently use the Apple host's CPU features for native dependencies.
Actual
Rust compilation uses the Lambda CPU target, but the generated Zig C wrapper uses
-mcpu=native.This can produce instructions unsupported by some Lambda ARM64 hosts. The resulting failure looks intermittent because it depends on which host runs the function.
Workaround
Forcing native dependencies to use baseline ARMv8-A removes the instructions:
After rebuilding with this setting, the affected function contains no
eor3instructions, and I no longer see theSIGILLfailures.