Skip to content

Commit c16a038

Browse files
committed
src: fix cleanup hook registry for shared isolates and self-removal
The registry behind `AddEnvironmentCleanupHook()` was keyed on {isolate, fun, arg} and asserted that every insertion is unique. Two Environments on one isolate that register the same hook, which the Node-API documentation allows per environment, aborted the process on the second `napi_add_env_cleanup_hook()`. `CleanupHookThunkRun()` also read the registry entry after the hook had returned. A hook that removes itself while running, as `node::ObjectWrap`'s does through its destructor, had already erased that entry, so this was a use-after-free read on every Worker exit with a live ObjectWrap instance. Key the registry on `arg` only and tell entries apart by Environment: adding the same hook to one Environment twice still aborts as documented, removal prefers the current Environment's registration and falls back to a matching one from another Environment when there is no current context, and a hook that is running is only erased by `CleanupHookThunkRun()` itself. Refs: #63985 Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
1 parent 2befec5 commit c16a038

2 files changed

Lines changed: 110 additions & 24 deletions

File tree

‎src/api/hooks.cc‎

Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
#include "node_process-inl.h"
44
#include "async_wrap.h"
55

6+
#include <algorithm>
7+
#include <unordered_map>
8+
69
namespace node {
710

811
using v8::Context;
@@ -128,56 +131,79 @@ struct CleanupHookThunk final {
128131
Environment* env;
129132
CleanupHook fun;
130133
void* arg;
131-
132-
bool operator==(const CleanupHookThunk& other) const {
133-
// `env` is intentionally not part of this comparison
134-
return isolate == other.isolate && fun == other.fun && arg == other.arg;
135-
}
134+
bool running = false;
136135
};
137-
struct CleanupHookThunkHash {
138-
size_t operator()(const CleanupHookThunk& thunk) const {
139-
return std::hash<void*>()(thunk.arg);
140-
}
141-
};
142-
using CleanupHookRegistry =
143-
std::unordered_set<CleanupHookThunk, CleanupHookThunkHash>;
136+
// Keyed on `arg`. The same hook may be registered once per Environment, and
137+
// several Environments can share an Isolate.
138+
using CleanupHookRegistry = std::unordered_multimap<void*, CleanupHookThunk>;
144139
static ExclusiveAccess<CleanupHookRegistry> cleanup_hook_registry;
145140

146141
static void CleanupHookThunkRun(void* arg) {
147-
const CleanupHookThunk* thunk = static_cast<CleanupHookThunk*>(arg);
142+
CleanupHookThunk* thunk = static_cast<CleanupHookThunk*>(arg);
143+
{
144+
ExclusiveAccess<CleanupHookRegistry>::Scoped registry(
145+
&cleanup_hook_registry);
146+
thunk->running = true;
147+
}
148148
thunk->fun(thunk->arg);
149-
RemoveEnvironmentCleanupHook(thunk->isolate, thunk->fun, thunk->arg);
149+
ExclusiveAccess<CleanupHookRegistry>::Scoped registry(
150+
&cleanup_hook_registry);
151+
auto [begin, end] = registry->equal_range(thunk->arg);
152+
auto self = std::find_if(
153+
begin, end, [&](const auto& entry) { return &entry.second == thunk; });
154+
CHECK(self != end);
155+
registry->erase(self);
150156
}
151157

152158
void AddEnvironmentCleanupHook(Isolate* isolate,
153159
CleanupHook fun,
154160
void* arg) {
155161
Environment* env = Environment::GetCurrent(isolate);
156162
CHECK_NOT_NULL(env);
157-
void* wrapped_arg;
163+
CleanupHookThunk* thunk;
158164
{
159165
ExclusiveAccess<CleanupHookRegistry>::Scoped registry(
160166
&cleanup_hook_registry);
161-
auto result = registry->insert({isolate, env, fun, arg});
162-
CHECK(result.second);
163-
wrapped_arg = const_cast<CleanupHookThunk*>(&*result.first);
167+
auto [begin, end] = registry->equal_range(arg);
168+
// Adding the same hook twice to one Environment is documented to abort;
169+
// a running hook may register itself again.
170+
CHECK(std::none_of(begin, end, [&](const auto& entry) {
171+
return entry.second.env == env && entry.second.fun == fun &&
172+
!entry.second.running;
173+
}));
174+
thunk = &registry->emplace(arg, CleanupHookThunk{isolate, env, fun, arg})
175+
->second;
164176
}
165-
env->AddCleanupHook(CleanupHookThunkRun, wrapped_arg);
177+
env->AddCleanupHook(CleanupHookThunkRun, thunk);
166178
}
167179

168180
void RemoveEnvironmentCleanupHook(Isolate* isolate,
169181
CleanupHook fun,
170182
void* arg) {
183+
// Prefer the current Environment's registration and otherwise take any
184+
// match: there may be no current context (GC, addon threads) or it may
185+
// belong to another Environment on the same isolate.
186+
Environment* current =
187+
isolate != nullptr && isolate == Isolate::TryGetCurrent()
188+
? Environment::GetCurrent(isolate)
189+
: nullptr;
171190
CleanupHookThunk thunk;
172191
void* wrapped_arg;
173192
{
174193
ExclusiveAccess<CleanupHookRegistry>::Scoped registry(
175194
&cleanup_hook_registry);
176-
auto result = registry->find({isolate, nullptr, fun, arg});
177-
if (result == registry->end()) return;
178-
wrapped_arg = const_cast<CleanupHookThunk*>(&*result);
179-
thunk = *result;
180-
registry->erase(result);
195+
auto [begin, end] = registry->equal_range(arg);
196+
auto found = end;
197+
for (auto it = begin; it != end; ++it) {
198+
if (it->second.isolate != isolate || it->second.fun != fun) continue;
199+
if (found == end || it->second.env == current) found = it;
200+
if (it->second.env == current) break;
201+
}
202+
// A running hook is removing itself; CleanupHookThunkRun() cleans up.
203+
if (found == end || found->second.running) return;
204+
wrapped_arg = &found->second;
205+
thunk = found->second;
206+
registry->erase(found);
181207
}
182208
thunk.env->RemoveCleanupHook(CleanupHookThunkRun, wrapped_arg);
183209
}

‎test/cctest/test_environment.cc‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,66 @@ TEST_F(EnvironmentTest, MultipleEnvironmentsPerIsolate) {
328328
EXPECT_TRUE(called_cb_2);
329329
}
330330

331+
static int cleanup_hook_runs = 0;
332+
static void CountingCleanupHook(void* arg) {
333+
cleanup_hook_runs++;
334+
}
335+
336+
TEST_F(EnvironmentTest, SameCleanupHookInTwoEnvironmentsOnOneIsolate) {
337+
const v8::HandleScope handle_scope(isolate_);
338+
const Argv argv;
339+
cleanup_hook_runs = 0;
340+
{
341+
Env env1{handle_scope, argv};
342+
{
343+
Env env2{handle_scope, argv, node::EnvironmentFlags::kNoCreateInspector};
344+
{
345+
v8::Context::Scope context_scope(env1.context());
346+
node::AddEnvironmentCleanupHook(isolate_, CountingCleanupHook, nullptr);
347+
}
348+
node::AddEnvironmentCleanupHook(isolate_, CountingCleanupHook, nullptr);
349+
}
350+
EXPECT_EQ(cleanup_hook_runs, 1);
351+
}
352+
EXPECT_EQ(cleanup_hook_runs, 2);
353+
}
354+
355+
TEST_F(EnvironmentTest, RemoveCleanupHookOfOtherEnvironmentOnSameIsolate) {
356+
const v8::HandleScope handle_scope(isolate_);
357+
const Argv argv;
358+
cleanup_hook_runs = 0;
359+
int arg;
360+
{
361+
Env env1{handle_scope, argv};
362+
node::AddEnvironmentCleanupHook(isolate_, CountingCleanupHook, &arg);
363+
Env env2{handle_scope, argv, node::EnvironmentFlags::kNoCreateInspector};
364+
// env2's context is current; the hook belongs to env1.
365+
node::RemoveEnvironmentCleanupHook(isolate_, CountingCleanupHook, &arg);
366+
}
367+
EXPECT_EQ(cleanup_hook_runs, 0);
368+
}
369+
370+
struct SelfRemovingHook {
371+
v8::Isolate* isolate;
372+
bool ran = false;
373+
static void Run(void* arg) {
374+
SelfRemovingHook* self = static_cast<SelfRemovingHook*>(arg);
375+
self->ran = true;
376+
node::RemoveEnvironmentCleanupHook(self->isolate, Run, arg);
377+
}
378+
};
379+
380+
TEST_F(EnvironmentTest, CleanupHookRemovesItselfWhileRunning) {
381+
const v8::HandleScope handle_scope(isolate_);
382+
const Argv argv;
383+
SelfRemovingHook hook{isolate_};
384+
{
385+
Env env{handle_scope, argv};
386+
node::AddEnvironmentCleanupHook(isolate_, SelfRemovingHook::Run, &hook);
387+
}
388+
EXPECT_TRUE(hook.ran);
389+
}
390+
331391
TEST_F(EnvironmentTest, NoEnvironmentSanity) {
332392
const v8::HandleScope handle_scope(isolate_);
333393
v8::Local<v8::Context> context = v8::Context::New(isolate_);

0 commit comments

Comments
 (0)