Skip to content

ARM64 build uses host target-cpu=native for native dependencies and can produce SIGILL on Lambda #934

Description

@libbkmz

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:

  1. With the inherited target-cpu=native
  2. With native dependencies limited to baseline ARMv8-A

Run:

./reproduce.sh

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions