Skip to content

WASM engine: uncatchable V8 fatal abort on repeated buildGraph() calls in one process (follow-up to #965) #2131

Description

@carlos-alm

Summary

While benchmarking v3.15.0 vs the v3.16.0 release candidate (2e0e94f) for #2127/#2081, I hit the same "uncatchable" V8 fatal abort documented in #965's Mode 1, on both the wasm and native engines, on both source versions. Non-deterministic: sometimes the run completes cleanly, sometimes it crashes on the 2nd buildGraph() call in-process, sometimes on the 1st.

#965 was closed by #972's catchable-error hardening (wasmExtractSymbols try/catch), but its own closing comment explicitly carved out this class as unresolved:

The uncatchable V8 aborts (Mode 1 unreachable trap, Mode 2 Check failed: maybe_code.has_value(), Mode 4 segfault) cannot be intercepted by JS try/catch. Root-causing them needs bisection + grammar/Node/Windows investigation and is tracked as follow-up.
Proposal: ... close and open a new issue specifically for worker-thread isolation / grammar bisection.

That follow-up issue was never filed — this is it.

Environment

  • Windows 11 x64, Node v22.18.0
  • Reproduced against two separate source checkouts: v3.15.0 (cb420491) and the v3.16.0 release-candidate commit (2e0e94f) — same crash signature on both, so this predates and postdates that release
  • Both the wasm engine directly, and the native engine indirectly (native's WASM-backfill path for files the Rust extractor drops into WASM hits the same crash)

Reproduction

node --experimental-strip-types --import ./scripts/ts-resolve-loader.js scripts/incremental-benchmark.ts --version "x" --dist

This script calls buildGraph() multiple times sequentially within one process (full build, 2 warmup + 5 timed no-op rebuilds, 2 warmup + 5 timed 1-file rebuilds — ~15 calls per engine). Crash rate looked roughly 1-in-3 to 1-in-2 runs in my testing; when it occurs, it's always on an early call (1st or 2nd), never mid-sequence.

Symptom

#
# Fatal error in , line 0
# unreachable code
#
#FailureMessage Object: ...
----- Native stack trace -----
 1: node::SetCppgcReference+16599
 2: node::TriggerNodeReport+74095
 3: V8_Fatal+197
 4: v8::MemorySpan<...>::end+...
 ...

Exit codes observed: 2147483651 (0x80000003, STATUS_BREAKPOINT) and 3221225477 (0xC0000005, STATUS_ACCESS_VIOLATION) — different each time, consistent with memory corruption rather than a deterministic logic bug.

Why worker-thread isolation (#975) doesn't cover this

#975 isolates tree-sitter parsing in a worker thread so a catchable per-file parse failure doesn't take down the process. But worker_threads share the same V8 isolate/process — a true V8-level fatal abort (V8_Fatal, not a JS exception) terminates the entire process regardless of which thread triggered it. That's exactly what #965's closing comment flagged as out of reach for the #972/#975 hardening, and exactly what I'm still hitting.

Impact

  • Any long-lived process that calls buildGraph() more than once (watch mode, MCP server issuing repeated rebuilds, this benchmark harness) is exposed.
  • Non-deterministic, so it's a reliability/flakiness problem more than a hard blocker — but it did make getting a clean A/B benchmark comparison for build-perf regression blocking v3.16.0: full build +42%, incremental rebuild +53-83% #2127 harder than expected (had to retry several times to get matched successful runs on both sides).

Suggested next steps (not investigated here — out of scope for what I was doing)

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