Skip to content

riscv64: SIGILL in JS->wasm wrapper — lazy compilation enters unpopulated code pages (unofficial-builds v24.20.0 & v26.0.0) #65724

Description

@Acidmoon

Version

v26.0.0 (node -v), unofficial-builds linux-riscv64.tar.xz, V8 14.6.202.33-node.19, pointer compression off
(also reproduces on v24.20.0 of the same line; both are currently-supported releases)

Platform

Linux lpi3a 6.1.15 #1.0.15.1 SMP PREEMPT Sat Sep  7 07:20:18 UTC 2024 riscv64 riscv64 riscv64 GNU/Linux

Hardware: Sipeed Lichee Pi 3A — SpacemiT K1, 8× T-Heng X60, 16 GB RAM.
OS: Bianbu 1.0.15 "Mantic Minotaur" (Ubuntu mantic base), glibc 2.38.
CPU ISA (per /proc/cpuinfo): rv64imafdcv_sscofpmf_sstc_svpbmt_zicbom_zicboz_zicbop_zihintpauseno Zba/Zbb/Zcb/Zfa; MMU sv39; RVV 1.0 works (verified with userspace vector benchmarks).
Kernel 6.1.15 has no riscv_hwprobe syscall, so V8 must fall back to AT_HWCAP for feature detection.
The node binary itself runs general workloads fine on this machine (npm install trees, 500k-call JS/wasm loops, NAPI addons incl. sharp/clipboard prebuilds — none crash).

Subsystem

webassembly (V8 wasm runtime on riscv64; JS→wasm wrapper / lazy compilation entry)

What steps will reproduce the bug?

We could not reduce the trigger below a large Rust→wasm module (see notes under “Additional information” about synthetic attempts), so the shortest reliable repro uses one publicly-published npm app that ships the wasm fallback of SWC inside next:

# on the riscv64 machine (Node >= 22.19 from https://unofficial-builds.nodejs.org/download/release/ )
npm i -g @agegr/pi-web@0.8.11
pi-web --port 30141

Output observed (no client traffic involved):

▲ Next.js 16.3.1
- Local:         http://127.0.0.1:30141
✓ Ready in 1632ms
⚠ next-swc does not have native bindings support for target triple [object Object]. ...
[pi-web] Next.js exited unexpectedly (signal SIGILL)

Control (only difference = one V8 flag):

node --no-wasm-lazy-compilation "$(npm root -g)/@agegr/pi-web/bin/pi-web.js" --port 30141
# → stays alive indefinitely; serving real pages (verified 90+ s, HTTP 200s at 15/45/75 s, zero SIGILL)

The crashing wasm module is node_modules/next/wasm/@next/swc-wasm-nodejs/wasm_bg.wasm (Next.js 16.3.1), which Next.js loads automatically on platforms without a native SWC binding.

How often does it reproduce? Is there a required condition?

Deterministic, ~100% on this machine. Required conditions:

  • riscv64 node from unofficial-builds, default V8 flags (both the standard and the pointer-compression flavors reproduce — see controls below);
  • a large wasm module whose functions are called lazily in a long-running process;
  • crash fires 5–8 s after the app becomes ready with zero requests — it is self-triggered, not traffic-triggered.

All of the following were tested and do not change the outcome (still SIGILL at the same code-page offset): --no-wasm-code-gc, --no-wasm-tier-up, --no-liftoff, --no-flush-baseline-code --no-flush-liftoff-code --no-flush-bytecode --no-flush-code-based-on-time, OPENSSL_riscvcap= (empty), NEXT_TELEMETRY_DISABLED=1, --no-riscv-b-extension (already default-off on this build), and Node v24.20.0 vs v26.0.0.

What is the expected behavior? Why is that the expected behavior?

Expected: WebAssembly functions compile lazily on first call (or eagerly if lazy is disabled) and execute correctly, as on all other supported architectures.

Why we believe this is an engine bug and not userland / not “unsupported hardware”:

  • The crash PC is inside an anonymous executable mapping owned by V8's code space, filled with 0x00000000 (RISC-V zero word = illegal instruction) — i.e. execution entered a code page that was never populated.
  • The return address at the crash is inside Node's own V8 builtin Builtins_JSToWasmWrapperAsm — the JS→wasm call trampoline itself. Nothing userland controls that jump target.
  • The crashing instruction offset is byte-identical across runs (page offset 0xa28, constant register values) — a deterministic codegen/stub-patching defect, not corruption.
  • Pure V8 flag (--no-wasm-lazy-compilation) makes it disappear; every non-wasm path on this binary is healthy.
  • The CPU fully supports the base ISA V8 uses here (rv64gc); no Zb*/Zfa instructions are involved (V8's B-extension use is default-off, and the same flag matrix shows the fault follows wasm lazy compilation, not ISA extensions).

What do you see instead?

Kernel log (repeats identically for every run):

next-server (v1[4259]: unhandled signal 4 code 0x1 at 0x00000011d183fa28
CPU: 2 PID: 4259 Comm: next-server (v1 Not tainted 6.1.15 #1.0.15.1
Hardware name: SiPEED LPi3A Board (DT)
...
status: 8000000200006020 badaddr: 0000000000000000 cause: 0000000000000002

(cause 2 = RISC-V "illegal instruction"; the low page offset is always 0xa28; t0 is always 0x6bae.)

Under gdb:

Thread 1 "next-server (v1" received signal SIGILL, Illegal instruction.
0x0000003fa86cba28 in ?? ()
(gdb) x/8i $pc
=> 0x3fa86cba28:  unimp     (eight consecutive `unimp`)
(gdb) x/8xw $pc-16
0x3fa86cba18:  0x00000000 0x00000000 0x00000000 0x00000000
0x3fa86cba28:  0x00000000 0x00000000 0x00000000 0x00000000
(gdb) p/x $ra
$1 = 0x2aabf96458
(gdb) info symbol $ra
Builtins_JSToWasmWrapperAsm + 184 in section .text of .../bin/node

Additional information

Relationship to #64538 (important). We are aware of the concurrent riscv64 wasm SIGILL thread (#64538, RVV/SIMD on SpacemiT chips) and believe our non-pc-flavor report is a distinct mechanism, not the same root cause:

  • On the exact crashing binary (non-pointer-compression v26.0.0), V8 refuses to compile SIMD-containing modules — we verified directly: a SIMD wasm (llhttp.wasm shipped with Next.js) dies with CompileError: Wasm SIMD unsupported instead of executing, so none of the VU::SetSimd128* RVV lowering paths can be reached by the crashing app;
  • the failing function itself is plain non-SIMD Rust→wasm (Next.js swc-wasm-nodejs), and the fault is a jump into zero-filled, never-written code (badaddr 0x0; in RISC-V: "Illegal instruction" crash when vector instructions detected by V8 #64538 the kernel's badaddr carries a real RVV encoding such as vmv.s.x 0x4209ec57);
  • the fixer is orthogonal to SIMD: --no-wasm-lazy-compilation (SIMD support untouched) eliminates our crash 100%.

Caveat for honesty: the pointer-compression flavor also SIGILLs on this board, but we have not yet captured its kernel badaddr signature — if it turns out to be a real RVV instruction word, that flavor's crash belongs to #64538 and only the non-pc zero-page crash is new. We will capture it when our board is next reachable and post an update here.

Build-flavor control. The linux-riscv64-pointer-compression variant of the same v26.0.0 release also crashes, at the same page offset 0xa28 with the same register signature — so the bug is riscv64-wide, not tied to the non-pointer-compression flavor.

Synthetic-reduction attempts (negative results). A 45-byte add module called 500k times, and generated modules with thousands of trivial exported functions called in sequence, all run fine — so a minimal pure-builtins snippet does not (yet) reproduce. The trigger appears tied to how large Rust→wasm modules are consumed (we suspect re-entrant import→export calls and/or wide first-call coverage across many functions). We can share our stress-generator scripts, and — more usefully — test candidate patches/flags on this exact board via ssh (8-core X60, always reachable); we can also try to continue bisecting with --trace-wasm-* on request.

Why userland is exonerated (per the guideline about userland modules): the only userland code involved is Next.js choosing its documented wasm-SWC fallback on an unmapped platform; the fault itself is in Node's V8 trampoline→code-space boundary as shown above. The identical app works on x86_64/aarch64. A prior riscv64 field report of the same fallback crashing is documented here: https://dev.to/gounthar/the-one-line-patch-that-unlocked-nextjs-on-risc-v-a-detective-story-71h

Related platform facts (context, independent of this crash):

  • QEMU control (negative — very likely the key localization clue): the same node binary and app under qemu-system-riscv64 -cpu max (all extensions enabled) + our mainline 7.2.0-rc7 kernel (with riscv_hwprobe, so V8 detects Zba/Zbb/Zfa/…) does not crash — ran 900 s with traffic every 30 s, GET / consistently OK (wasm-SWC config compile took 4.1 min under TCG and completed). The board differs in two coupled ways: no hwprobe on kernel 6.1.15 → V8 falls back to AT_HWCAP → reduced feature set → no-B fallback code path. Suspect the bug lives in V8's riscv64 code path taken when B-class extensions are not detected. (We could not test -cpu with B off using this guest image — its userspace is RVA23 and fails to boot without Zb*; would need an rv64gc userspace rootfs, e.g. Debian riscv64. Happy to run that test for you — the board is not required if a suitable rootfs is specified.)
  • Because kernel 6.1.15 lacks riscv_hwprobe, V8 additionally reports “Wasm SIMD unsupported” and cleanly refuses SIMD-containing modules (e.g. llhttp.wasm → CompileError). This SIGILL is on non-SIMD modules.
  • We have filed a downstream tracker so users can work around this immediately: pi-web issue Crashes with SIGILL on RISC-V (riscv64): next-server dies ~5s after Ready — needs --no-wasm-lazy-compilation agegr/pi-web#685 (Crashes with SIGILL on RISC-V (riscv64): next-server dies ~5s after Ready — needs --no-wasm-lazy-compilation agegr/pi-web#685) — asking the app to pass --no-wasm-lazy-compilation on riscv64.

Artifacts: node -v v26.0.0; tarball sha256 071bb81d19de45393b91412ec4f01ae85bfde2b7e78bb62c5fabfa72347b8ff8; app @agegr/pi-web@0.8.11; next@16.3.1; V8 14.6.202.33-node.19; uv 1.52.1; openssl 3.5.5.

— Acidmoon, KUBUDS Tech

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