Skip to content

[BUG]: nodejs-24 aarch64 binary SIGILLs on ARMv8.0-A CPUs #78694

@robwil

Description

@robwil

Package name

nodejs-24

Current version in Wolfi

24.15.0

Requested version

No response

Upstream project URL

https://nodejs.org/dist/v24.15.0/node-v24.15.0-linux-arm64.tar.xz

Problem

Summary

The Wolfi nodejs-24 aarch64 package executes an instruction unsupported by ARMv8.0-A baseline CPUs (specifically Cortex-A72 in Raspberry Pi 4) on the global fetch() code path, producing SIGILL (exit 132, "Illegal instruction (core dumped)"). Other Node code paths — https.get, node:http2, crypto (AES-256-GCM, TLS 1.3) — run cleanly on the same binary on the same hardware. The official nodejs.org aarch64 tarball at the same version (24.15.0) runs the same fetch() reproducer on the same hardware without fault. The regression is therefore in Wolfi's compilation of Node.js, not in upstream Node sources or vendored libraries.

Affected versions

  • Package: nodejs-24 (also reproduces with nodejs-24-minimal)
  • Wolfi build observed: node 24.15.0, openssl 3.6.2, libcrypto.so.3 mtime Apr 30 13:31 UTC
  • Wolfi base: cgr.dev/chainguard/wolfi-base pulled 2026-05-05

Affected hardware

ARMv8.0-A AArch64 CPUs without the optional crypto / atomics / dot-product extensions, e.g. Raspberry Pi 4 (Cortex-A72).

/proc/cpuinfo Features: fp asimd evtstrm crc32 cpuid (only).

AT_HWCAP from /proc/self/auxv = 0x887 — bits set: FP, ASIMD, EVTSTRM, CRC32, CPUID. Notably absent: AES, PMULL, SHA1, SHA2, ATOMICS (LSE), DotProd, etc.

Steps to reproduce

Reproducer

On a Raspberry Pi 4 running 64-bit Pi OS (uname -maarch64, kernel 6.6.x):

docker run --rm cgr.dev/chainguard/wolfi-base sh -c '
  apk add --no-cache nodejs-24 ca-certificates-bundle &&
  node -e "fetch(\"https://example.com/\").then(r=>r.text()).then(t=>console.log(\"len\",t.length)).catch(e=>console.log(\"err\",e.message))"
'
# → Illegal instruction (core dumped)
# → exit code 132

Equivalent test with the official upstream binary on the same Pi succeeds:

curl -fsSL https://nodejs.org/dist/v24.15.0/node-v24.15.0-linux-arm64.tar.xz | tar xJ
./node-v24.15.0-linux-arm64/bin/node -e \
  'fetch("https://example.com/").then(r=>r.text()).then(t=>console.log("len",t.length))'
# → len 528
# → exit 0

Triage already performed

The crash is not in any of:

Subsystem Test Result
OpenSSL TLS 1.3 https.get("https://openrouter.ai/api/v1/models") OK, TLS_AES_256_GCM_SHA384
AES-256-GCM bulk crypto.createCipheriv over 1 MB plaintext OK
AES-256-GCM with OPENSSL_armcap=0 same OK
HTTP/2 node:http2 request to OpenRouter OK, 200
Plain HTTP/1 streaming via https https.get + body read OK
fetch() global any URL, even https://example.com/ SIGILL
fetch() headers only no body read SIGILL

The fault therefore sits in the V8 / undici / streams code path that fetch() enters and https.get / http2 do not. OpenSSL was eliminated by direct reproduction in the same container; the suspicion that the OpenSSL 3.5 → 3.6 bump was the cause is disproven.

Side-by-side process.versions (Wolfi vs upstream)

Identical for everything that's compiled into the binary:

node       24.15.0       (same)
v8         13.6.233.17-node.48  (same)
simdutf    6.4.0         (same)
simdjson   4.5.0         (same)
undici     7.24.4        (same)
llhttp     9.3.1         (same)
ada        3.4.4         (same)
zstd       1.5.7         (same)
brotli     1.2.0         (same)
modules    137           (same)

Differences (dynamically linked or trivial):

openssl    3.6.2 (Wolfi)  vs  3.5.5 (upstream)   — proven not the cause
libuv      1.52.1 (Wolfi) vs  1.51.0 (upstream)
nghttp2    1.68.1 (Wolfi) vs  1.68.0 (upstream)
zlib       1.3.2 (Wolfi)  vs  1.3.1-e00f703 (upstream)

Process of elimination → the divergence is in V8 compilation flags in the Wolfi build. The most plausible cause is a -march / V8 enable_armv8_* build-time feature being asserted true at compile time when it should be left to runtime detection.

Root cause (if known)

Root cause (faulting instruction confirmed via core dump)

A core dump from the SIGILL was captured and inspected with gdb on the Pi:

Program terminated with signal SIGILL, Illegal instruction.
#0  0x0000001c71ec30c8 in ?? ()
=> 0x1c71ec30c8: pmull2 v0.1q, v31.2d, v30.2d
   0x1c71ec30c0: dup    v31.2d, x16
   0x1c71ec30c4: ushr   v30.16b, v3.16b, #7
   0x1c71ec30cc: pmull  v30.1q, v31.1d, v30.1d
   0x1c71ec30d0: trn2   v30.8b, v30.8b, v0.8b

The faulting instruction is PMULL2 (polynomial multiply long, second variant), which is part of AArch64's optional FEAT_PMULL extension and gated on HWCAP_PMULL (bit 4 of AT_HWCAP). Cortex-A72's HWCAP is 0x887 — bit 4 is unset, the kernel correctly reports no PMULL. The surrounding instructions (dup + ushr + pmull2 + pmull + trn2) are a textbook PMULL-based GHASH / CRC32 16-byte fold sequence.

The faulting PC (0x1c71ec30c8) is in a high-address, dynamically-mapped executable region — not in node, not in any loaded shared object (gdb's info symbol found no match). This places it in V8 JIT pages or a V8-compiled WASM code region. Combined with the fact that https.get / node:http2 / direct crypto calls all run cleanly on this binary, the regression is in V8's aarch64 code generator: it is emitting PMULL2 unconditionally instead of going through CpuFeatures::Probe() runtime detection. The most plausible mechanism is a build-time GN arg that asserts enable_pmull (or similar) is always present at compile time, which short-circuits the runtime gate.

fetch() exclusively triggers it because undici's hot path goes through V8-compiled WASM (bundled llhttp / streaming decoders) that exercises codegen paths the older https/http2 Node APIs never reach.

Proposed solution

Suggested next steps for Wolfi

  1. Confirm the regression on a Cortex-A72 host (or under qemu-aarch64 -cpu cortex-a72, which models a CPU without pmull).
  2. Diff the nodejs-24 build environment / wolfi-base / shared toolchain between the wolfi-base build dated 2026-04-17 (last known good) and the rebuild dated 2026-05-01 (first broken). The faulting instruction is PMULL2, so look in particular at:
    • V8 GN args related to PMULL / crypto extensions (v8_enable_arm_pmull, arm_use_neon, arm_optionally_use_neon, arm_cpu, arm_tune)
    • any change that promotes pmull from an optional/runtime-detected feature to a build-time-required one
    • global wolfi-base / melange CFLAGS or -march defaults that imply +crypto (which includes PMULL on aarch64)
    • toolchain / GCC / Clang upgrade in the same window
  3. The fix is to ensure V8 emits PMULL2 only behind CpuFeatures::IsSupported(PMULL) runtime checks. Cortex-A53/A72/A73 (an enormous chunk of the ARM SBC and embedded install base) do not have FEAT_PMULL and rely on this gating.
  4. After fixing, pin the offending nodejs-24 and wolfi-base digests so downstream image consumers can reproduce exactly, and consider adding a Cortex-A72 (or no-pmull qemu) smoke test to Wolfi CI.

Why this matters

Pi-class boards (Pi 3, Pi 4, Pi 5, Cortex-A53/A72/A76 in many SBCs and edge devices) are a substantial chunk of the ARM Linux install base. A SIGILL on fetch() makes any Node 24 service that uses the global fetch (which is the recommended Node 24 idiom) unrunnable on those boards using Wolfi-derived images, with no error log surfaced to the user — the application just dies silently.

Testing performed

No response

Acceptance criteria

  • The requested version is the latest stable upstream release (no pre-releases or RCs)
  • The upstream project uses an OSI-approved license
  • The change aligns with Wolfi’s packaging and security model
  • The package can be reasonably maintained over time
  • There are no known unresolved security or supply-chain concerns

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-triageapplied to all new customer/user issues. Removed after triage occurs.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions