Skip to content

Commit 5570a84

Browse files
everett1992sreehariannam
authored andcommitted
src: fix use-after-free in CleanupHookThunkRun
CleanupHookThunkRun() read thunk->isolate/fun/arg from the CleanupHookThunk after invoking thunk->fun(). For every node::ObjectWrap alive at teardown, thunk->fun is ObjectWrap::CleanupHook, which deletes the wrap; ~ObjectWrap() calls RemoveEnvironmentCleanupHook() itself, erasing the CleanupHookThunk from the registry and freeing the node it lives in. The subsequent read of thunk->isolate/fun/arg to make the (now redundant) second RemoveEnvironmentCleanupHook() call was therefore a use-after-free. Cache the fields before running the hook so nothing is read from `thunk` once it may have been freed. Taken over from #65196, which has been inactive; the original change is unmodified apart from the added comment. This also unblocks #65042, the backport of the cleanup hook registry to v24.x. Without that registry ~ObjectWrap() asserts during garbage collection, so every 24.x runtime aborts for ObjectWrap addons (#65446), as do 26.x runtimes before 26.4.0 when used with newer headers (#65262). Fixes: #65195 Refs: #65196 Refs: #65042 Refs: #65446 Refs: #65262 Co-authored-by: Sreehari Annam <sreehari.annam@gmail.com> Signed-off-by: Caleb Everett <everett.caleb@gmail.com> PR-URL: #65630 Backport-PR-URL: #66128 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent abe1b67 commit 5570a84

2 files changed

Lines changed: 49 additions & 2 deletions

File tree

‎src/api/hooks.cc‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,15 @@ static ExclusiveAccess<CleanupHookRegistry> cleanup_hook_registry;
157157

158158
static void CleanupHookThunkRun(void* arg) {
159159
const CleanupHookThunk* thunk = static_cast<CleanupHookThunk*>(arg);
160-
thunk->fun(thunk->arg);
161-
RemoveEnvironmentCleanupHook(thunk->isolate, thunk->fun, thunk->arg);
160+
// `thunk->fun` may itself remove and free this CleanupHookThunk (e.g. via
161+
// ~ObjectWrap(), which calls RemoveEnvironmentCleanupHook()), so cache the
162+
// fields we still need before invoking it rather than reading them from
163+
// `thunk` afterwards.
164+
Isolate* isolate = thunk->isolate;
165+
CleanupHook fun = thunk->fun;
166+
void* fun_arg = thunk->arg;
167+
fun(fun_arg);
168+
RemoveEnvironmentCleanupHook(isolate, fun, fun_arg);
162169
}
163170

164171
void AddEnvironmentCleanupHook(Isolate* isolate,

‎test/cctest/test_environment.cc‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ static void at_exit_callback_ordered2(void* arg);
2727
static void at_exit_js(void* arg);
2828
static std::string cb_1_arg; // NOLINT(runtime/string)
2929

30+
struct SelfRemovingCleanupHookState {
31+
v8::Isolate* isolate;
32+
bool ran = false;
33+
};
34+
static void self_removing_cleanup_hook(void* arg);
35+
3036
class EnvironmentTest : public EnvironmentTestFixture {
3137
private:
3238
void TearDown() override {
@@ -309,6 +315,27 @@ TEST_F(EnvironmentTest, AtExitRunsJS) {
309315
EXPECT_TRUE(called_at_exit_js);
310316
}
311317

318+
// A cleanup hook that removes itself while the environment cleanup queue is
319+
// being drained must not cause a use-after-free. This registers such a hook
320+
// directly rather than through node::ObjectWrap, whose destructor removes
321+
// its own hook and is what makes this reachable for addons since #63642.
322+
// The use-after-free is silent in ordinary builds; it is caught by the
323+
// ASan/Valgrind CI, which is also how the original assertion (#63923)
324+
// surfaced. Regression test for https://github.com/nodejs/node/issues/65195.
325+
TEST_F(EnvironmentTest, RemoveEnvironmentCleanupHookDuringCleanup) {
326+
const v8::HandleScope handle_scope(isolate_);
327+
const Argv argv;
328+
SelfRemovingCleanupHookState state{isolate_};
329+
{
330+
Env env{handle_scope, argv};
331+
node::AddEnvironmentCleanupHook(
332+
isolate_, self_removing_cleanup_hook, &state);
333+
// Destroying `env` runs FreeEnvironment() -> RunCleanup(), which drains
334+
// the cleanup queue and invokes CleanupHookThunkRun() for the hook above.
335+
}
336+
EXPECT_TRUE(state.ran);
337+
}
338+
312339
TEST_F(EnvironmentTest, MultipleEnvironmentsPerIsolate) {
313340
const v8::HandleScope handle_scope(isolate_);
314341
const Argv argv;
@@ -392,6 +419,19 @@ static void at_exit_js(void* arg) {
392419
called_at_exit_js = true;
393420
}
394421

422+
// Reproduces the sequence node::ObjectWrap performs since
423+
// https://github.com/nodejs/node/pull/63642, without using ObjectWrap
424+
// itself: the hook removes its own environment cleanup hook. When that runs
425+
// while the cleanup queue is being drained, CleanupHookThunkRun() must not
426+
// read the CleanupHookThunk after invoking the hook -- the hook has already
427+
// erased and freed it. See https://github.com/nodejs/node/issues/65195.
428+
static void self_removing_cleanup_hook(void* arg) {
429+
auto* state = static_cast<SelfRemovingCleanupHookState*>(arg);
430+
state->ran = true;
431+
node::RemoveEnvironmentCleanupHook(
432+
state->isolate, self_removing_cleanup_hook, state);
433+
}
434+
395435
TEST_F(EnvironmentTest, SetImmediateCleanup) {
396436
int called = 0;
397437
int called_unref = 0;

0 commit comments

Comments
 (0)