Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

Intermittent fatal CHECK in compiler/heap-refs on v24.x — request to backport V8 14.6's JSHeapBroker thread-safety fixes

オープン
#66,126 コメント 1 件 リアクション 0 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

評価

難易度
5/5
見積もり時間
1週間以上
初心者へのやさしさ
25/100
issue の種類
バグ
明瞭さ
おおむね明確
活発さ
活発
技術スタック
cpp, javascript, linux, node.js

調査の方向性

src/compiler/heap-refs.h と src/compiler/heap-refs.cc から始め、引用されている V8 の変更 c3553509f、b1c2cda9b、0bb186fd を v24.x の V8 バージョンと比較します。可能であれば、説明されている Vitest 5 fork ワークロードを使って failure を観察します。完了条件は、fixes を v24.x に適用できるかどうかを判断し、その結果の検証を記録すること、または backport が実行不可能な理由を文書化することです。

索引モデルが issue の本文から書いたものです。

説明

Version

v24.11.1 (V8 13.6.233.10-node.28)

Platform
Linux x86_64, kernel 6.1.134-152.225.amzn2023.x86_64
libc: musl (Alpine-based container image)
cgroup v2, CPU quota = 12 cores, memory limit 24576 MiB, swap off
Subsystem

deps/v8 (compiler)

What steps will reproduce the bug?

I don't have a minimal reproduction, and I want to be upfront about that.

The workload is a Vitest 5 run in forks pool with isolate: true: one short-lived node child process per test file, 1579 children per run, 32 concurrent against a 12-core quota, ~740 s wall clock. Each child spends most of its life loading and evaluating modules (transform 61%, import 27%, tests 5%), so it is optimizing-compiler-heavy startup, repeated 1579 times. The children are plain JS workloads — component/unit tests on jsdom. No node:http2, no network server, no HTTP/2 client anywhere in them.

Occasionally one child dies on a signal. The rest of the run is green.

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

7 runs with at least one crash out of 87 runs (~8%), i.e. roughly 1 in 20000 child processes. One run produced three separate events.

CPU oversubscription appears to be required: I have never reproduced it on an idle machine, and a fork-storm stress test (tens of thousands of bare node -e '' spawns) never hit it — the child has to actually be compiling JavaScript. Anyone trying to reproduce should think in terms of total contention (host baseline + concurrent children ÷ available cores); the loadavg in my logs is sampled at run start and excludes the load the run itself creates.

What is the expected behavior?

The child process should not die.

What do you see instead?

Three signatures so far, all in the same subsystem:

  • A. Check failed: (data_) != nullptr. with SIGTRAP
  • B. Check failed: IsJSFunction(). with SIGTRAP
  • C. plain SIGSEGV with no V8 output at all

Signature A verbatim; B is identical in shape:

#
# Fatal error in , line 0
# Check failed: (data_) != nullptr.
#
#
#
#FailureMessage Object: 0x7fa7fa0b27e0
----- Native stack trace -----

The parent reports Worker exited unexpectedly with signal <SIG>. SIGTRAP rather than SIGABRT is consistent with --hard-abort (default true).

At 13.6.233.10 both CHECKs sit in src/compiler/heap-refs.h: A is CHECK_NOT_NULL(data_), which appears at 360 and 386 and is reached through the latter in a release build, and B is the CHECK(Is##Name()) in DEFINE_REF_CONSTRUCTOR. The null check runs first, so B means a non-null ObjectData of the wrong type.

Additional information

What I'm asking for. Please consider backporting these to v24.x:

CL title bug
c3553509f [compiler] Thread-safe IsArrayOrObjectPrototype 467311868
b1c2cda9b [compiler] Thread-safe MapRef::GetConstructor,GetBackpointer 467311868
0bb186fd9 [objects][compiler] Release-store the initial map into the prototype tuple 542923494

All three are already-accepted upstream fixes for unsynchronized reads in JSHeapBroker, which is exactly where we crash; the CLs carry the mechanism. Signature B — non-null, wrong type — matches both shapes they fix: a slot holding either a JSFunction or a Map read mid-flip (b1c2cda9b), and a map observed before Factory::InitializeMap's stores are visible (0bb186fd9).

Version containment, checked with gh api repos/v8/v8/compare/<tag>...<sha>:

Node branch V8 has c3553509f / b1c2cda9b has 0bb186fd9
v24.x 13.6.233.17 no no
v25.x 14.1.146.11 no no
v26.x / main 14.6.202.34 yes no

Staying on 24.x doesn't help: 13.6.233.10 → 13.6.233.17 adds 5 substantive cherry-picks, none touching heap-refs, the broker, or handles.

I'm not asking anyone to debug my workload — the report is that a supported LTS line ships a V8 predating this cluster of fixes. Cost disclosed up front: b1c2cda9b widens a public return type to OptionalObjectRef and updates four caller files that have moved a lot since 13.6, so it isn't a mechanical cherry-pick. "Too invasive for an LTS V8" is a legitimate answer; I'd then suggest the class is still worth recording against 24.x, since the failure mode is a hard process death with no diagnosable output.

What distinguishes this from #64841. Same shape — intermittent Linux crash, JSHeapBroker involved, contention required, --no-maglev making it go away — but that one resolved to invalid nghttp2 accesses and is now scoped to HTTP/2, which this workload never touches, so I'm deliberately not commenting there.

The distinguishing evidence is that for some of our crashes there was no third-party native code in the process at all. I hooked process.dlopen from a --require shim inherited by the forked children and enumerated a full run: the children load at most bufferutil (884 of 1579) and bigint-buffer (18), while the bundler's native binding stays in the parent because children receive transformed modules over IPC. Of the six distinct test files that have crashed, two ran in processes with no addon at all, and the most recent crash came from a run where bufferutil and utf-8-validate were blocked at require() time. Node's own built-in native code remains in scope — I can't turn that off to test it.

Ruled out, two observability notes, and the mitigation in flight

Ruled out. Container OOM: cgroup memory.peak 10844/24576 MiB, memory.events oom_kill=0, swap off, pids 385 of 37827 — and it would be SIGKILL, with a V8 heap OOM being SIGABRT. musl's 128 KiB default thread stack: concurrent compilation runs on Node's platform workers, which node_platform.cc creates via uv_thread_create → libuv's uv__thread_stack_size() (RLIMIT_STACK, else 2 MB), so the musl default never applies; verified in node:24.11.1-alpine, where V8's own regress-crbug-388320179.js passes a 2000-link prototype chain that a 128 KiB stack would have killed at ~340. Stale V8 code cache: the runner deletes NODE_COMPILE_CACHE for the children and nothing sets it.

Observability, independent of the root cause. The release V8_Fatal path passes an empty file and line 0, so the assertion text is the only usable signal — unique for both of my signatures, but it would not be for a commoner CHECK. And ----- Native stack trace ----- prints its header and then nothing on musl, because Node's backtrace depends on glibc's execinfo.h; a reader cannot distinguish "no frames available on this libc" from "the process died before it could walk the stack". A one-line explanation instead of an empty section would save people real time, which also bears on the Alpine tier-2 discussion in #62764.

Mitigation in flight. --no-concurrent-recompilation for the children, both as mitigation and as the only discriminating experiment I have left: a recurrence under that flag would falsify the attribution above in a single run. I'll report back either way, though a clean result is much weaker evidence — at an 8% per-run base rate it takes ~28 consecutive clean runs to reach p≈0.1.

Body condensed at maintainer request; the original long-form version remains in this issue's edit history.

主要言語
JavaScript
スター
122k
フォーク
37.4k
平均マージ
4日 3時間
マージ済み PR(30日)
279

コントリビューションガイド

コントリビューションガイドを開く

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

nodejs/node のほかの issue

nodejs/node の issue をすべて見る

似ている issue

JavaScript の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。