Skip to content

Commit 8000b3e

Browse files
szegediclaude
authored andcommitted
fix(wall): don't remove the env cleanup hook from inside itself (#360)
WallProfiler registers an environment cleanup hook (CleanupHook) via node::AddEnvironmentCleanupHook so it can stop profiling when a worker isolate is terminated without a beforeExit notification. On termination, CleanupHook -> Cleanup -> Dispose called node::RemoveEnvironmentCleanupHook for that same hook. Node's cleanup-hook trampoline (CleanupHookThunkRun) invokes the hook and then dereferences its internal hook record again to remove the hook itself. Removing the hook from within the hook frees that record early, so when our callback returns Node reads freed memory — a use-after-free that segfaults on worker termination (observed reliably on Node 26, exercised by the "should not crash when worker is terminated" test). Add a removeCleanupHook flag to Dispose (default true). Cleanup, which only runs from inside CleanupHook, passes false and lets Node remove the hook itself. The still-running call sites (StopCore, StartImpl failure) keep removing it, since there the hook is live and must not fire later against a destroyed profiler. Reproduced and verified fixed in an Alpine/musl Node 26 container under gdb: crashed within 1-2 runs before, 30/30 clean after. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c82c2e7 commit 8000b3e

3 files changed

Lines changed: 19 additions & 6 deletions

File tree

bindings/profilers/wall.cc

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,10 @@ void WallProfiler::Cleanup(Isolate* isolate) {
467467
if (interceptSignal()) {
468468
SignalHandler::DecreaseUseCount();
469469
}
470-
Dispose(isolate);
470+
// We're running inside the environment cleanup hook itself; Node removes
471+
// the hook (and frees its internal record) after we return, so we must not
472+
// remove it here — doing so would be a use-after-free. See Dispose().
473+
Dispose(isolate, /*removeCleanupHook=*/false);
471474
}
472475
}
473476

@@ -689,7 +692,7 @@ WallProfiler::~WallProfiler() {
689692
liveContextPtrHead_ = nullptr;
690693
}
691694

692-
void WallProfiler::Dispose(Isolate* isolate) {
695+
void WallProfiler::Dispose(Isolate* isolate, bool removeCleanupHook) {
693696
if (cpuProfiler_ != nullptr) {
694697
cpuProfiler_->Dispose();
695698
cpuProfiler_ = nullptr;
@@ -704,8 +707,10 @@ void WallProfiler::Dispose(Isolate* isolate) {
704707
isolate->RemoveGCEpilogueCallback(&GCEpilogueCallback, this);
705708
}
706709

707-
node::RemoveEnvironmentCleanupHook(
708-
isolate, &WallProfiler::CleanupHook, isolate);
710+
if (removeCleanupHook) {
711+
node::RemoveEnvironmentCleanupHook(
712+
isolate, &WallProfiler::CleanupHook, isolate);
713+
}
709714
}
710715
}
711716

bindings/profilers/wall.hh

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,14 @@ class WallProfiler : public Nan::ObjectWrap {
113113
ContextBuffer contexts_;
114114

115115
~WallProfiler();
116-
void Dispose(v8::Isolate* isolate);
116+
// removeCleanupHook must be false when Dispose is called from within the
117+
// environment cleanup hook (CleanupHook). Node removes the hook itself after
118+
// invoking it and dereferences its internal hook record again afterwards, so
119+
// calling node::RemoveEnvironmentCleanupHook from inside the hook frees that
120+
// record early and leaves Node reading freed memory (use-after-free crash on
121+
// worker termination). In all other (still-running) call sites the hook is
122+
// live and must be removed so it doesn't fire later against a dead profiler.
123+
void Dispose(v8::Isolate* isolate, bool removeCleanupHook = true);
117124

118125
// A new CPU profiler object will be created each time profiling is started
119126
// to work around https://bugs.chromium.org/p/v8/issues/detail?id=11051.

suppressions/lsan_suppr.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
leak:CRYPTO_zalloc
1+
leak:CRYPTO_zalloc
2+
leak:ossl_load_builtin_compressions

0 commit comments

Comments
 (0)