Skip to content

Commit 8929166

Browse files
authored
src: let Environments on one isolate share a cleanup hook
The registry behind `AddEnvironmentCleanupHook()` is keyed on {isolate, fun, arg} and asserts that every insertion is unique. Two Environments on one isolate that register the same hook, which the Node-API documentation allows per environment, abort the process on the second `napi_add_env_cleanup_hook()`. Key the registry on `arg` only and tell entries apart by Environment: adding the same hook to one Environment twice still aborts as documented, and removal prefers the current Environment's registration, falling back to a matching one from another Environment when there is no current context. Because the entry to remove after a hook has run can no longer be found by {isolate, fun, arg} alone, `CleanupHookThunkRun()` marks its entry as running and erases exactly that entry afterwards; a removal of a running entry (a hook removing itself, as `~ObjectWrap()` does) is a no-op, which keeps the use-after-free fixed by #65630 fixed. Refs: #63985 Refs: #65630 Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com> PR-URL: #65777 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
1 parent 32d23ec commit 8929166

2 files changed

Lines changed: 110 additions & 32 deletions

File tree

src/api/hooks.cc

Lines changed: 50 additions & 32 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,63 +131,78 @@ 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);
148-
// `thunk->fun` may itself remove and free this CleanupHookThunk (e.g. via
149-
// ~ObjectWrap(), which calls RemoveEnvironmentCleanupHook()), so cache the
150-
// fields we still need before invoking it rather than reading them from
151-
// `thunk` afterwards.
152-
Isolate* isolate = thunk->isolate;
153-
CleanupHook fun = thunk->fun;
154-
void* fun_arg = thunk->arg;
155-
fun(fun_arg);
156-
RemoveEnvironmentCleanupHook(isolate, fun, fun_arg);
142+
CleanupHookThunk* thunk = static_cast<CleanupHookThunk*>(arg);
143+
{
144+
ExclusiveAccess<CleanupHookRegistry>::Scoped registry(
145+
&cleanup_hook_registry);
146+
thunk->running = true;
147+
}
148+
thunk->fun(thunk->arg);
149+
ExclusiveAccess<CleanupHookRegistry>::Scoped registry(&cleanup_hook_registry);
150+
auto [begin, end] = registry->equal_range(thunk->arg);
151+
auto self = std::find_if(
152+
begin, end, [&](const auto& entry) { return &entry.second == thunk; });
153+
CHECK(self != end);
154+
registry->erase(self);
157155
}
158156

159157
void AddEnvironmentCleanupHook(Isolate* isolate,
160158
CleanupHook fun,
161159
void* arg) {
162160
Environment* env = Environment::GetCurrent(isolate);
163161
CHECK_NOT_NULL(env);
164-
void* wrapped_arg;
162+
CleanupHookThunk* thunk;
165163
{
166164
ExclusiveAccess<CleanupHookRegistry>::Scoped registry(
167165
&cleanup_hook_registry);
168-
auto result = registry->insert({isolate, env, fun, arg});
169-
CHECK(result.second);
170-
wrapped_arg = const_cast<CleanupHookThunk*>(&*result.first);
166+
auto [begin, end] = registry->equal_range(arg);
167+
// Adding the same hook twice to one Environment is documented to abort;
168+
// a running hook may register itself again.
169+
CHECK(std::none_of(begin, end, [&](const auto& entry) {
170+
return entry.second.env == env && entry.second.fun == fun &&
171+
!entry.second.running;
172+
}));
173+
thunk = &registry->emplace(arg, CleanupHookThunk{isolate, env, fun, arg})
174+
->second;
171175
}
172-
env->AddCleanupHook(CleanupHookThunkRun, wrapped_arg);
176+
env->AddCleanupHook(CleanupHookThunkRun, thunk);
173177
}
174178

175179
void RemoveEnvironmentCleanupHook(Isolate* isolate,
176180
CleanupHook fun,
177181
void* arg) {
182+
// Prefer the current Environment's registration and otherwise take any
183+
// match: there may be no current context (GC, addon threads) or it may
184+
// belong to another Environment on the same isolate.
185+
Environment* current =
186+
isolate != nullptr && isolate == Isolate::TryGetCurrent()
187+
? Environment::GetCurrent(isolate)
188+
: nullptr;
178189
CleanupHookThunk thunk;
179190
void* wrapped_arg;
180191
{
181192
ExclusiveAccess<CleanupHookRegistry>::Scoped registry(
182193
&cleanup_hook_registry);
183-
auto result = registry->find({isolate, nullptr, fun, arg});
184-
if (result == registry->end()) return;
185-
wrapped_arg = const_cast<CleanupHookThunk*>(&*result);
186-
thunk = *result;
187-
registry->erase(result);
194+
auto [begin, end] = registry->equal_range(arg);
195+
auto found = end;
196+
for (auto it = begin; it != end; ++it) {
197+
if (it->second.isolate != isolate || it->second.fun != fun) continue;
198+
if (found == end || it->second.env == current) found = it;
199+
if (it->second.env == current) break;
200+
}
201+
// A running hook is removing itself; CleanupHookThunkRun() cleans up.
202+
if (found == end || found->second.running) return;
203+
wrapped_arg = &found->second;
204+
thunk = found->second;
205+
registry->erase(found);
188206
}
189207
thunk.env->RemoveCleanupHook(CleanupHookThunkRun, wrapped_arg);
190208
}

test/cctest/test_environment.cc

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,66 @@ TEST_F(EnvironmentTest, WorkerConnectToMainThreadWithoutInspector) {
455455
}
456456
#endif // HAVE_INSPECTOR
457457

458+
static int cleanup_hook_runs = 0;
459+
static void CountingCleanupHook(void* arg) {
460+
cleanup_hook_runs++;
461+
}
462+
463+
TEST_F(EnvironmentTest, SameCleanupHookInTwoEnvironmentsOnOneIsolate) {
464+
const v8::HandleScope handle_scope(isolate_);
465+
const Argv argv;
466+
cleanup_hook_runs = 0;
467+
{
468+
Env env1{handle_scope, argv};
469+
{
470+
Env env2{handle_scope, argv, node::EnvironmentFlags::kNoCreateInspector};
471+
{
472+
v8::Context::Scope context_scope(env1.context());
473+
node::AddEnvironmentCleanupHook(isolate_, CountingCleanupHook, nullptr);
474+
}
475+
node::AddEnvironmentCleanupHook(isolate_, CountingCleanupHook, nullptr);
476+
}
477+
EXPECT_EQ(cleanup_hook_runs, 1);
478+
}
479+
EXPECT_EQ(cleanup_hook_runs, 2);
480+
}
481+
482+
TEST_F(EnvironmentTest, RemoveCleanupHookOfOtherEnvironmentOnSameIsolate) {
483+
const v8::HandleScope handle_scope(isolate_);
484+
const Argv argv;
485+
cleanup_hook_runs = 0;
486+
int arg;
487+
{
488+
Env env1{handle_scope, argv};
489+
node::AddEnvironmentCleanupHook(isolate_, CountingCleanupHook, &arg);
490+
Env env2{handle_scope, argv, node::EnvironmentFlags::kNoCreateInspector};
491+
// env2's context is current; the hook belongs to env1.
492+
node::RemoveEnvironmentCleanupHook(isolate_, CountingCleanupHook, &arg);
493+
}
494+
EXPECT_EQ(cleanup_hook_runs, 0);
495+
}
496+
497+
struct SelfRemovingHook {
498+
v8::Isolate* isolate;
499+
bool ran = false;
500+
static void Run(void* arg) {
501+
SelfRemovingHook* self = static_cast<SelfRemovingHook*>(arg);
502+
self->ran = true;
503+
node::RemoveEnvironmentCleanupHook(self->isolate, Run, arg);
504+
}
505+
};
506+
507+
TEST_F(EnvironmentTest, CleanupHookRemovesItselfWhileRunning) {
508+
const v8::HandleScope handle_scope(isolate_);
509+
const Argv argv;
510+
SelfRemovingHook hook{isolate_};
511+
{
512+
Env env{handle_scope, argv};
513+
node::AddEnvironmentCleanupHook(isolate_, SelfRemovingHook::Run, &hook);
514+
}
515+
EXPECT_TRUE(hook.ran);
516+
}
517+
458518
TEST_F(EnvironmentTest, NoEnvironmentSanity) {
459519
const v8::HandleScope handle_scope(isolate_);
460520
v8::Local<v8::Context> context = v8::Context::New(isolate_);

0 commit comments

Comments
 (0)