Skip to content

Commit bb42c6d

Browse files
everett1992sreehariannamnsavoire
committed
test: add regression test for cleanup hook UAF
Add a cctest that registers an environment cleanup hook which removes itself while the cleanup queue is drained. It exercises CleanupHookThunkRun(), which must not read the CleanupHookThunk after invoking the hook, because the hook has already erased and freed it. The hook is registered directly rather than through node::ObjectWrap. ObjectWrap is what makes this reachable for addons since #63642, because its destructor removes its own hook, and #65195 reproduces the fault that way with test/addons/worker-addon-exit. That reproducer needs an addon build and depends on when the wrapper is collected, whereas this test drives the self-removal directly. The use-after-free is silent in ordinary builds and is caught by the ASan/Valgrind CI, which is how the original assertion (#63923) surfaced. Verified locally with an ASan build: without the preceding commit both this test and test/addons/worker-addon-exit report heap-use-after-free in CleanupHookThunkRun(); both are clean with it. Refs: #65195 Refs: #65196 Assisted-by: a closed-source coding agent Co-authored-by: Sreehari Annam <sreehari.annam@gmail.com> Co-authored-by: nsavoire <19255994+nsavoire@users.noreply.github.com> Signed-off-by: Caleb Everett <everett.caleb@gmail.com>
1 parent 6dd9a72 commit bb42c6d

1 file changed

Lines changed: 40 additions & 0 deletions

File tree

test/cctest/test_environment.cc

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

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

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

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

0 commit comments

Comments
 (0)