Skip to content

Concurrent PGlite WebAssembly initialization intermittently SIGSEGVs Node.js #64500

Description

@gadicc

Version

v26.5.0 (also reproduced on Node 22, 24, 25)

Platform

The primary Docker reproduction ran on this host:


Linux dragon-pro18 6.18.35-1-cachyos-lts #1 SMP PREEMPT_DYNAMIC Sat, 13 Jun 2026 12:39:40 +0000 x86_64 GNU/Linux
Intel Core Ultra 9 285HX, 24 cores online


Docker replaces the userspace and Node installation but shares the host kernel and CPU. The same failure occurs with the host Node binary.

Subsystem

V8 / WebAssembly (observed through child_process)

What steps will reproduce the bug?

The minimal repository is:

https://github.com/gadicc/node-pglite-wasm-sigsegv-repro

It has one runtime dependency, @electric-sql/pglite@0.5.4. PGlite ships its PostgreSQL engine as WebAssembly; there is no native addon.

On a machine with at least 20 GiB available:

git clone https://github.com/gadicc/node-pglite-wasm-sigsegv-repro.git
cd node-pglite-wasm-sigsegv-repro
docker build -t pglite-node-sigsegv-repro .
docker run --rm pglite-node-sigsegv-repro

Equivalently, with a local Node installation:

npm ci
npm run repro

Each of 16 child processes independently creates an in-memory PGlite client, runs SELECT 1, awaits client.close() in finally, and exits. The parent repeats that wave up to 50 times and stops on the first failure. There is no Vitest, Vite, test runner, application code, native addon, IPC dependency, or database persistence.

The memory warning matters: the observed cgroup peak was approximately 19 GiB. The recorded failing run had oom=0 and oom_kill=0.

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

It is intermittent and requires concurrent processes in the measured setup.

Two clean Node 26 official-image runs failed at waves 13 and 8. Other results
without V8 CLI overrides were:

Node Result
25 1/20 waves failed
24 4/45 waves failed
22 1/10 waves failed
20 0/40 waves failed

The Node 20 result is a non-reproduction in an intermittent test, not evidence that it is unaffected. Official Node 20, 22, 24, and 26 images expose the same relevant defaults: Liftoff, lazy compilation, dynamic tiering, and WASM tier-up are enabled, with up to 128 compilation tasks. Node 20 did reproduce with --no-liftoff, which forces the optimizing compiler.

The Node 27 V8 canary failed at wave 17 with child_process.fork() and failed 2/50 waves with ordinary child_process.spawn(). A sequential one-child control passed 50/50 waves.

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

Every child should complete the query, close the PGlite client, and exit with code 0.

What do you see instead?

An intermittent child is terminated by SIGSEGV:

node=v26.5.0 v8=14.6.202.34-node.24 platform=linux arch=x64 children=16 waves=50
wave=1 passed=16/16
...
wave=8 passed=15/16
child=10 code=null signal=SIGSEGV elapsedMs=4315
failedWaves=1 completedWaves=8 requestedWaves=50

Additional information

Native diagnostics from a sanitized-environment reproduction:

  1. strace recorded an initial SIGSEGV with SEGV_MAPERR at a large guarded WASM address.
  2. Node's node::TrapWebAssemblyOrContinue called raise(SIGSEGV) after V8 declined to handle the address as a valid WASM trap.
  3. The retained core records the resulting SI_TKILL signal.
  4. GDB placed another crash in the lazy WASM compilation path through LiftoffAssembler::PrepareCall, ExecuteLiftoffCompilation, CompileLazy, and Runtime_WasmCompileLazy, with background Turboshaft compilation active. A separate retained crash was in v8::internal::MarkingBarrier::MarkValueLocal.

Relevant flag controls on Node 25:

  • default: reproduced;
  • --wasm-enforce-bounds-checks: reproduced;
  • --wasm-num-compilation-tasks=1: reproduced;
  • --no-wasm-tier-up: passed 100/100 waves in that version;
  • --no-liftoff: reproduced.

The Node 27 canary nevertheless reproduced with both --no-wasm-tier-up and --liftoff-only, so no single WASM tier is universally required.

I also built the exact PGlite 0.5.4 tag using its official pnpm build:all:debug workflow. That PostgreSQL WASM used -g,
-gsource-map, and --no-wasm-opt; LLVM verified 1,322 DWARF compilation units without errors. With Node 25.2.1, a sequential control passed 5/5 waves and eight concurrent children passed 5/5 waves, while 16 concurrent children
reproduced SIGSEGV in wave 2. The cgroup peak was 67,306,729,472 bytes (62.7 GiB), with oom=0 and oom_kill=0.

Runtime controls using the same PGlite version and concurrency:

  • Bun 1.3.11 / JavaScriptCore: 50/50 waves passed;
  • Deno 2.8.0 / V8 14.9.207.2, default: 50/50 passed;
  • Deno 2.8.0 / V8 14.9.207.2, --no-liftoff: 50/50 passed.

The evidence therefore establishes a Node-runtime-specific interaction in the tested configurations, but does not yet distinguish Node's WASM trap/embedding layer or build configuration from a V8 defect exposed by Node. d8 is V8's standalone JavaScript shell; reproducing there without Node or its APIs would prove that the bug exists independently in V8. I would appreciate guidance on whether this should be routed or cross-filed to V8.

On 2026-07-14 I searched open Node and V8 reports for PGlite, TrapWebAssemblyOrContinue, WasmCompileLazy, LiftoffAssembler, and concurrent WASM SIGSEGVs, and found no exact duplicate. The reproduction README records the related but materially different reports.

Lastly,

  • Co-authored with Codex gpt-5.6-sol, reasoning: high, with significant human input.
  • This issue was opened by me personally by hand and without any automation.

Edit 1: Added PGLite debug build info in "Additional Information" above.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions