24.19.0: `node::ObjectWrap` cleanup hooks backported without the cleanup hook registry, aborts on every 24.x runtime
まだ誰も着手していません。
評価
- 難易度
- 4/5
- 見積もり時間
- 3〜5日
- 初心者へのやさしさ
- 48/100
調査の方向性
src/node_object_wrap.h と src/api/hooks.cc から始め、提供されている addon.cc と index.js のアロケーションループを使って header/runtime マトリックスを再現します。24.x の状態を commit 1723773d および関連する #63642 の変更と比較します。24.19.0 でビルドされた ObjectWrap addon が、24.19.0 runtime 上でアロケーションによって駆動されるコレクションを cleanup-hook assertion なしで生き残れば完了です。
索引モデルが issue の本文から書いたものです。
説明
Version
v24.19.0 (also affects v24.x-staging)
Platform
Reproduced on: Darwin 25.5.0 arm64 (official darwin-arm64 tarballs)
Originally hit on: Linux x86_64, Debian 13 container
Subsystem
src, addons
What steps will reproduce the bug?
This is the 24.x counterpart of #65262, but with an important difference: on the 24.x line
the crash does not need a header/runtime version mismatch. Headers and runtime both at
24.19.0 abort deterministically.
addon.cc (same minimal reproducer as #65262):
#include <node.h>
#include <node_object_wrap.h>
using namespace v8;
class Thing : public node::ObjectWrap {
public:
static void New(const FunctionCallbackInfo<Value>& args) {
(new Thing())->Wrap(args.This());
}
};
void Init(Local<Object> exports) {
Isolate* isolate = Isolate::GetCurrent();
Local<Context> context = isolate->GetCurrentContext();
Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, Thing::New);
tpl->InstanceTemplate()->SetInternalFieldCount(1);
exports->Set(context,
String::NewFromUtf8(isolate, "Thing").ToLocalChecked(),
tpl->GetFunction(context).ToLocalChecked()).Check();
}
NODE_MODULE(NODE_GYP_MODULE_NAME, Init)
index.js:
const { Thing } = require(process.argv[2]);
let junk = [];
for (let i = 0; i < 300000; i++) {
new Thing(); // wrapped object, immediately unreachable
junk.push({ a: i }); // keep allocation rate high so V8 collects on its own
if (junk.length > 1000) junk = [];
}
console.log('survived');
Build against each header set and run each binary on each runtime:
for V in 24.18.1 24.19.0; do
curl -sfLO "https://nodejs.org/dist/v$V/node-v$V-darwin-arm64.tar.gz"
tar -xzf "node-v$V-darwin-arm64.tar.gz"
clang++ -std=c++20 -fPIC -shared -Wl,-undefined,dynamic_lookup \
-I"node-v$V-darwin-arm64/include/node" \
-DNODE_GYP_MODULE_NAME=addon -o "addon-hdr-$V.node" addon.cc
done
for HV in 24.18.1 24.19.0; do
for RV in 24.18.1 24.19.0; do
./node-v$RV-darwin-arm64/bin/node index.js "./addon-hdr-$HV.node"
echo "hdr $HV / rt $RV -> rc=$?"
done
done
How often does it reproduce? Is there a required condition?
Deterministic, 5/5 runs for both crashing combinations.
| hdr \ rt | 24.18.1 | 24.19.0 |
|---|---|---|
| 24.18.1 | ok | ok |
| 24.19.0 | crash | crash |
Compare with the 26.x matrix in #65262, where headers 26.4.0 on runtime 26.4.0 is fine.
As in that issue, an explicit global.gc() does not trigger it; the collection has to be
allocation driven.
What is the expected behavior? Why is that the expected behavior?
An addon that uses node::ObjectWrap and is built against 24.19.0 headers should keep
working on a 24.19.0 runtime. Today there is no 24.x runtime it works on.
What do you see instead?
# node[52504]: void node::RemoveEnvironmentCleanupHook(Isolate *, CleanupHook, void *) at ../src/api/hooks.cc:142
# Assertion failed: (env) != nullptr
1: node::Assert(node::AssertionInfo const&) [node]
2: node::RemoveEnvironmentCleanupHook(v8::Isolate*, void (*)(void*), void*) (.cold.1) [node]
4: node::ObjectWrap::RemoveCleanupHook() [addon-hdr-24.19.0.node]
5: node::ObjectWrap::~ObjectWrap() [addon-hdr-24.19.0.node]
Additional information
The cause looks like a partial backport. 24.19.0 picked up src: add cleanup hooks to node::ObjectWrap (#63642), which adds AddCleanupHook() to the constructor and
RemoveCleanupHook() to the destructor of the header only node::ObjectWrap class. It did
not pick up 1723773d4133fc71215a9cbb7e2b9a2a11fc3688 (src: keep global list of addon-provided cleanup hooks), which is what makes hook removal survive without a live
Environment and which is why the 26.x matrix in #65262 has a working diagonal.
$ curl -s https://raw.githubusercontent.com/nodejs/node/v24.19.0/src/node_object_wrap.h | grep -c 'RemoveCleanupHook()'
2
$ curl -s https://raw.githubusercontent.com/nodejs/node/v24.19.0/src/api/hooks.cc | grep -c CleanupHookThunk
0
$ curl -s https://raw.githubusercontent.com/nodejs/node/v26.4.0/src/api/hooks.cc | grep -c CleanupHookThunk
6
v24.x-staging is currently in the same state, and v24.19.0 is the newest v24 tag, so
every published 24.x runtime is affected once an addon is compiled with 24.19.0 headers.
Because node_object_wrap.h is header only, this reaches users who never changed a
dependency: rebuilding a container image after 2026-08-03 is enough, since the new
destructor code gets compiled into the addon. It hits NAN style addons in particular,
because they commonly do using namespace node; and inherit from node::ObjectWrap.
Real world example that led me here: a long running service in a Docker image, no
dependency version changed, only the Node base image moved 24.18.0 -> 24.19.0. The process
started aborting during periodic SFTP transfers, when the garbage collector reclaimed one
of ssh2's native cipher objects:
# node[7]: void node::RemoveEnvironmentCleanupHook(v8::Isolate*, CleanupHook, void*) at ../../src/api/hooks.cc:142
# Assertion failed: (env) != nullptr
1: node::Assert(node::AssertionInfo const&) [node]
2: node::RemoveEnvironmentCleanupHook(v8::Isolate*, void (*)(void*), void*) [node]
3: AESGCMDecipher::~AESGCMDecipher() [<app>/node_modules/ssh2/lib/protocol/crypto/build/Release/sshcrypto.node]
Downstream reports of the same assertion with other NAN style addons (better-sqlite3,
node-pty) after the same base image move: nexu-io/open-design#6462.
Possible ways out, in order of preference from a user point of view:
- Backport
1723773dtov24.x-stagingsoRemoveEnvironmentCleanupHookno longer needs
a liveEnvironment. - Or revert #63642 on the 24.x line until 1 is done.
Refs: #63642 #65262 #65195
- 主要言語
- JavaScript
- スター
- 122k
- フォーク
- 37.4k
- 平均マージ
- 4日 4時間
- マージ済み PR(30日)
- 276
コントリビューションガイド
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
nodejs/node のほかの issue
-
doc
難易度 2/5 1〜3時間 初心者へのやさしさ 65/100
-
build
難易度 1/5 1時間未満 初心者へのやさしさ 88/100
-
難易度 2/5 1〜3時間 初心者へのやさしさ 84/100
-
難易度 1/5 1時間未満 初心者へのやさしさ 90/100
-
feature request
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
似ている issue
-
bug customer-eng Durable Agents Inngest status: needs triage
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
-
optimization optimization:agents-md-curator
難易度 2/5 1〜3時間 初心者へのやさしさ 75/100
githubnext/gh-aw-cao#13475 ·
-
[BUG]: "Clear All" in Settings doesn't clear the saved analysis, old data comes back after reload オープンbug
難易度 2/5 1〜3時間 初心者へのやさしさ 88/100
AOSSIE-Org/OrgExplorer#253 · コメント 1 件 ·
-
難易度 2/5 1〜3時間 初心者へのやさしさ 88/100
oxc-project/oxc#26944 ·
-
ai-observability bug team/ai-observability
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100