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 -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 without pmull).
- 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
- 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.
- 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
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-24aarch64 package executes an instruction unsupported by ARMv8.0-A baseline CPUs (specifically Cortex-A72 in Raspberry Pi 4) on the globalfetch()code path, producingSIGILL(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 officialnodejs.orgaarch64 tarball at the same version (24.15.0) runs the samefetch()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
nodejs-24(also reproduces withnodejs-24-minimal)node 24.15.0,openssl 3.6.2,libcrypto.so.3mtimeApr 30 13:31UTCcgr.dev/chainguard/wolfi-basepulled 2026-05-05Affected hardware
ARMv8.0-A AArch64 CPUs without the optional crypto / atomics / dot-product extensions, e.g. Raspberry Pi 4 (Cortex-A72).
/proc/cpuinfoFeatures:fp asimd evtstrm crc32 cpuid(only).AT_HWCAPfrom/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):Equivalent test with the official upstream binary on the same Pi succeeds:
Triage already performed
The crash is not in any of:
https.get("https://openrouter.ai/api/v1/models")TLS_AES_256_GCM_SHA384crypto.createCipherivover 1 MB plaintextOPENSSL_armcap=0node:http2request to OpenRouterhttpshttps.get+ body readfetch()globalhttps://example.com/fetch()headers onlyThe fault therefore sits in the V8 / undici / streams code path that
fetch()enters andhttps.get/http2do not. OpenSSL was eliminated by direct reproduction in the same container; the suspicion that theOpenSSL 3.5 → 3.6bump was the cause is disproven.Side-by-side
process.versions(Wolfi vs upstream)Identical for everything that's compiled into the binary:
Differences (dynamically linked or trivial):
Process of elimination → the divergence is in V8 compilation flags in the Wolfi build. The most plausible cause is a
-march/ V8enable_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
gdbon the Pi:The faulting instruction is
PMULL2(polynomial multiply long, second variant), which is part of AArch64's optionalFEAT_PMULLextension and gated onHWCAP_PMULL(bit 4 ofAT_HWCAP). Cortex-A72's HWCAP is0x887— 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 innode, not in any loaded shared object (gdb'sinfo symbolfound no match). This places it in V8 JIT pages or a V8-compiled WASM code region. Combined with the fact thathttps.get/node:http2/ directcryptocalls all run cleanly on this binary, the regression is in V8's aarch64 code generator: it is emittingPMULL2unconditionally instead of going throughCpuFeatures::Probe()runtime detection. The most plausible mechanism is a build-time GN arg that assertsenable_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 olderhttps/http2Node APIs never reach.Proposed solution
Suggested next steps for Wolfi
qemu-aarch64 -cpu cortex-a72, which models a CPU withoutpmull).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_enable_arm_pmull,arm_use_neon,arm_optionally_use_neon,arm_cpu,arm_tune)pmullfrom an optional/runtime-detected feature to a build-time-required one-marchdefaults that imply+crypto(which includes PMULL on aarch64)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.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