src: fix use-after-free in CleanupHookThunkRun - #65630
Merged
nodejs-github-bot merged 2 commits intoSep 4, 2026
Merged
nodejs-github-bot merged 2 commits into
nodejs-github-bot merged 2 commits into
Conversation
everett1992
marked this pull request as ready for review
August 29, 2026 16:32
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #65630 +/- ##
=======================================
Coverage 90.07% 90.07%
=======================================
Files 754 754
Lines 256395 256398 +3
Branches 48494 48495 +1
=======================================
+ Hits 230947 230954 +7
+ Misses 16563 16561 -2
+ Partials 8885 8883 -2
🚀 New features to boost your workflow:
|
trivikr
approved these changes
Sep 1, 2026
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
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 nodejs#65196, which has been inactive; the original change is unmodified apart from the added comment. This also unblocks nodejs#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 (nodejs#65446), as do 26.x runtimes before 26.4.0 when used with newer headers (nodejs#65262). Fixes: nodejs#65195 Refs: nodejs#65196 Refs: nodejs#65042 Refs: nodejs#65446 Refs: nodejs#65262 Assisted-by: a closed-source coding agent Co-authored-by: Sreehari Annam <sreehari.annam@gmail.com> Signed-off-by: Caleb Everett <everett.caleb@gmail.com>
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 nodejs#63642, because its destructor removes its own hook, and nodejs#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 (nodejs#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: nodejs#65195 Refs: nodejs#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>
everett1992
force-pushed
the
test-cleanup-hook-uaf-regression-65196
branch
from
September 1, 2026 16:55
bb42c6d to
fd325a7
Compare
trivikr
approved these changes
Sep 1, 2026
This comment was marked as outdated.
This comment was marked as outdated.
Collaborator
This was referenced Sep 2, 2026
codebytere
approved these changes
Sep 4, 2026
Collaborator
|
Landed in 03e2b9b |
codebytere
added a commit
to codebytere/node
that referenced
this pull request
Sep 4, 2026
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 nodejs#65630 fixed.
Refs: nodejs#63985
Refs: nodejs#65630
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
aduh95
pushed a commit
that referenced
this pull request
Sep 7, 2026
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 Assisted-by: a closed-source coding agent Co-authored-by: Sreehari Annam <sreehari.annam@gmail.com> Signed-off-by: Caleb Everett <everett.caleb@gmail.com> PR-URL: #65630 Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: Shelley Vohr <shelley.vohr@gmail.com>
aduh95
pushed a commit
that referenced
this pull request
Sep 7, 2026
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 Assisted-by: a closed-source coding agent Co-authored-by: Sreehari Annam <sreehari.annam@gmail.com> Signed-off-by: Caleb Everett <everett.caleb@gmail.com> PR-URL: #65630 Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: Shelley Vohr <shelley.vohr@gmail.com>
codebytere
added a commit
to codebytere/node
that referenced
this pull request
Sep 7, 2026
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 nodejs#65630 fixed.
Refs: nodejs#63985
Refs: nodejs#65630
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
everett1992
added a commit
to everett1992/node
that referenced
this pull request
Sep 10, 2026
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 nodejs#65196, which has been inactive; the original change is unmodified apart from the added comment. This also unblocks nodejs#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 (nodejs#65446), as do 26.x runtimes before 26.4.0 when used with newer headers (nodejs#65262). Fixes: nodejs#65195 Refs: nodejs#65196 Refs: nodejs#65042 Refs: nodejs#65446 Refs: nodejs#65262 Assisted-by: a closed-source coding agent Co-authored-by: Sreehari Annam <sreehari.annam@gmail.com> Signed-off-by: Caleb Everett <everett.caleb@gmail.com> PR-URL: nodejs#65630 Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: Shelley Vohr <shelley.vohr@gmail.com>
codebytere
added a commit
to codebytere/node
that referenced
this pull request
Sep 11, 2026
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 nodejs#65630 fixed.
Refs: nodejs#63985
Refs: nodejs#65630
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
codebytere
added a commit
to codebytere/node
that referenced
this pull request
Sep 11, 2026
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 nodejs#65630 fixed.
Refs: nodejs#63985
Refs: nodejs#65630
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
codebytere
added a commit
to codebytere/node
that referenced
this pull request
Sep 13, 2026
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 nodejs#65630 fixed.
Refs: nodejs#63985
Refs: nodejs#65630
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
aduh95
pushed a commit
that referenced
this pull request
Sep 15, 2026
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 Assisted-by: a closed-source coding agent Co-authored-by: Sreehari Annam <sreehari.annam@gmail.com> Signed-off-by: Caleb Everett <everett.caleb@gmail.com> PR-URL: #65630 Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: Shelley Vohr <shelley.vohr@gmail.com>
2 tasks
MarshallOfSound
pushed a commit
to electron/electron
that referenced
this pull request
Sep 16, 2026
* chore: squash the perfetto backports into one Node.js patch The perfetto trace agent came in as six separate cherry-picks of nodejs/node#64565 and nodejs/node#64721. Both are marked dont-land-on-v24.x upstream, so they stay for as long as Electron ships Node.js 24; carry them as one patch. Likewise fold the CleanupHookThunkRun regression test into the patch for its fix, matching nodejs/node#65630. No change to the patched tree. * chore: keep the test commit's trailers on the folded cleanup-hook patch
mydickassbusiness
pushed a commit
to mydickassbusiness/electron
that referenced
this pull request
Sep 17, 2026
…#53990) * chore: squash the perfetto backports into one Node.js patch The perfetto trace agent came in as six separate cherry-picks of nodejs/node#64565 and nodejs/node#64721. Both are marked dont-land-on-v24.x upstream, so they stay for as long as Electron ships Node.js 24; carry them as one patch. Likewise fold the CleanupHookThunkRun regression test into the patch for its fix, matching nodejs/node#65630. No change to the patched tree. * chore: keep the test commit's trailers on the folded cleanup-hook patch
mydickassbusiness
pushed a commit
to mydickassbusiness/electron
that referenced
this pull request
Sep 17, 2026
…#53990) * chore: squash the perfetto backports into one Node.js patch The perfetto trace agent came in as six separate cherry-picks of nodejs/node#64565 and nodejs/node#64721. Both are marked dont-land-on-v24.x upstream, so they stay for as long as Electron ships Node.js 24; carry them as one patch. Likewise fold the CleanupHookThunkRun regression test into the patch for its fix, matching nodejs/node#65630. No change to the patched tree. * chore: keep the test commit's trailers on the folded cleanup-hook patch
codebytere
added a commit
to codebytere/node
that referenced
this pull request
Sep 19, 2026
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 nodejs#65630 fixed.
Refs: nodejs#63985
Refs: nodejs#65630
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
panva
pushed a commit
to panva/node
that referenced
this pull request
Sep 20, 2026
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 nodejs#65196, which has been inactive; the original change is unmodified apart from the added comment. This also unblocks nodejs#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 (nodejs#65446), as do 26.x runtimes before 26.4.0 when used with newer headers (nodejs#65262). Fixes: nodejs#65195 Refs: nodejs#65196 Refs: nodejs#65042 Refs: nodejs#65446 Refs: nodejs#65262 Co-authored-by: Sreehari Annam <sreehari.annam@gmail.com> Signed-off-by: Caleb Everett <everett.caleb@gmail.com> PR-URL: nodejs#65630 Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: Shelley Vohr <shelley.vohr@gmail.com> Signed-off-by: Filip Skokan <panva.ip@gmail.com> Assisted-by: Codex
nodejs-github-bot
pushed a commit
that referenced
this pull request
Sep 20, 2026
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>
aduh95
pushed a commit
that referenced
this pull request
Sep 21, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR is a continuation of @sreehariannam's work in
#65196
I've added a regression test and fixed the lint issue. I'm only opening this because there's no activity on that PR, and this bug is blocking the backport of #65042 which fixes a crash with better-sqlite3 on node v24.19.0
Fixes: #65195