[BUG]: nodejs-24 aarch64 binary SIGILLs on ARMv8.0-A CPUs
Nobody has claimed this yet.
- Dominant language
- Shell
- Stars
- 1.3k
- Forks
- 443
- PR merge metrics
- No merged PRs in 30d
Description
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 withnodejs-24-minimal) - Wolfi build observed:
node 24.15.0,openssl 3.6.2,libcrypto.so.3mtimeApr 30 13:31UTC - Wolfi base:
cgr.dev/chainguard/wolfi-basepulled 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 -m → aarch64, 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
- Confirm the regression on a Cortex-A72 host (or under
qemu-aarch64 -cpu cortex-a72, which models a CPU withoutpmull). - Diff the
nodejs-24build environment / wolfi-base / shared toolchain between the wolfi-base build dated2026-04-17(last known good) and the rebuild dated2026-05-01(first broken). The faulting instruction isPMULL2, 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
pmullfrom an optional/runtime-detected feature to a build-time-required one - global wolfi-base / melange CFLAGS or
-marchdefaults that imply+crypto(which includes PMULL on aarch64) - toolchain / GCC / Clang upgrade in the same window
- V8 GN args related to PMULL / crypto extensions (
- The fix is to ensure V8 emits
PMULL2only behindCpuFeatures::IsSupported(PMULL)runtime checks. Cortex-A53/A72/A73 (an enormous chunk of the ARM SBC and embedded install base) do not haveFEAT_PMULLand rely on this gating. - After fixing, pin the offending
nodejs-24andwolfi-basedigests so downstream image consumers can reproduce exactly, and consider adding a Cortex-A72 (or no-pmullqemu) 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
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reproducing nodejs-24 on a Cortex-A72 host or with qemu-aarch64 -cpu cortex-a72, then compare the wolfi-base builds from 2026-04-17 and 2026-05-01. Inspect the V8 GN arguments and Wolfi toolchain flags named in the report, especially PMULL-related settings and -march defaults. Done means fetch() no longer SIGILLs on CPUs without PMULL and a suitable smoke test is added.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- node.js
- Domain
- build-system, operating-systems
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100