Skip to content

Follow up on #388 review comments - #391

Open
szegedi wants to merge 1 commit into
mainfrom
szegedi/ctxwrap-review-followups
Open

Follow up on #388 review comments#391
szegedi wants to merge 1 commit into
mainfrom
szegedi/ctxwrap-review-followups

Conversation

@szegedi

@szegedi szegedi commented Aug 10, 2026

Copy link
Copy Markdown

Three review comments landed on #388 after it merged (@nsavoire). All three are valid; this addresses them.

1. Duplicated internal-field accessors

These functions are duplicated both here and in wall.cc.

Correct, and it was deliberate but temporary: #388 introduced internal-field.hh and left wall.cc's copies alone to avoid conflicting with #387, which was in flight at the time, with a note to fold them in afterwards. #387 has landed, so wall.cc now includes the header and its copies are gone. Same namespace and same names, so every call site is untouched.

2. Register the hook in Init(), drop g_drain_hook_registered

nit: register the environment cleanup hook in Init() and remove g_drain_hook_registered

Better, and it removes state rather than adding it. Module initialisation always runs with a context entered, so AddEnvironmentCleanupHook's own CHECK is satisfied there just as it was in Wrap(), and Init() runs exactly once per isolate — which is the lifetime the hook should match.

The flag existed only to make the lazy registration idempotent and to re-arm after an isolate was torn down and recreated on the same thread. Init() running again on the new isolate covers the second case by construction, so both reasons evaporate.

3. Clear the internal field before deleting the wrap

nit: clear JS object internal field before deleting the wrap object

Applied, and I'd argue it is more than a nit. That slot is exactly what the out-of-process OTEP-4947 reader walks to reach record_, so leaving it pointing at freed memory is a loaded gun aimed at a consumer we do not control. #388's comment claimed the dangling pointer "stays inert because nothing reads it" — true of our own code, but not of the reader.

The drain hook now nulls slot 0 on its way through the list. Being on the live list means V8 has not collected the holder, so reading the handle there is safe. The WeakCallback path cannot do this and does not need to: there the holder is the object being collected.

Verification

result
Node 26 npm test exit 0, 165 passing, 0 aborts
Node 24 test:js-asan exit 0, 165 passing, 0 leaks, 0 aborts
Node 20 test:js-asan exit 0, 101 passing, 0 leaks, 0 aborts
teardown regression test passing on 24 and 26 (the OTEP block skips on 20, no ACF)
original repro, N=3000 / 10000 exit 0
published native_wrap_fields_offset still 0

Local suite 115 passing, gts check 0 errors, clang-format -Werror clean.

The zero leak counts remain the load-bearing check: they confirm the drain hook still does what node::ObjectWrap's per-instance hook used to, now that it is registered from a different place.

@github-actions

Copy link
Copy Markdown

Overall package size

Self size: 2.48 MB
Deduped: 3.18 MB
No deduping: 3.18 MB

Dependency sizes | name | version | self size | total size | |------|---------|-----------|------------| | pprof-format | 2.3.1 | 504.33 kB | 504.33 kB | | source-map | 0.8.0 | 185.66 kB | 185.66 kB | | node-gyp-build | 4.8.4 | 13.86 kB | 13.86 kB |

🤖 This report was automatically generated by heaviest-objects-in-the-universe

@datadog-datadog-us1-prod

datadog-datadog-us1-prod Bot commented Aug 10, 2026

Copy link
Copy Markdown

Pipelines

⚠️ Warnings

🚦 4 Pipeline jobs failed

Build | build / darwin-arm64-test-22   View in Datadog   GitHub Actions

Build | build-successful   View in Datadog   GitHub Actions

DataDog/apm-reliability/pprof-nodejs | benchmarks-pr-comment   View in Datadog   GitLab

View all 4 failed jobs.

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 8dfa9bb | Docs | Datadog PR Page | Give us feedback!

szegedi added a commit to szegedi/custom-labels that referenced this pull request Aug 10, 2026
Ports two review nits raised on DataDog/pprof-nodejs#388, which vendors this
file, and addressed there in DataDog/pprof-nodejs#391. The third comment on
that PR was about deduplicating helpers against a file that has no
counterpart here, so it does not apply.

Register the drain hook in Init() rather than lazily on first Wrap(), which
removes g_drain_hook_registered entirely. Module initialisation always runs
with a context entered, so AddEnvironmentCleanupHook's CHECK is satisfied
there too, and Init() runs exactly once per isolate — the lifetime the hook
should match. The flag existed only to make the lazy registration idempotent
and to re-arm after an isolate was torn down and recreated on the same thread;
Init() running again on the new isolate covers that by construction.

Clear the holder's internal field before freeing the CtxWrap it points at.
That slot is exactly what the out-of-process OTEP-4947 reader walks to reach
record_, so leaving it pointing at freed memory aims a dangling pointer at a
consumer we do not control. Being on the live list means V8 has not collected
the holder, so reading the handle there is safe; the WeakCallback path cannot
do this and does not need to, since there the holder is the object being
collected.

Verified on Node 22, 24 and 26: 49/49 tests pass on each, and the teardown
repro stays clean at N=3000.
Three review nits from #388, all valid.

Deduplicate the internal-field accessors. wall.cc had its own copies of
GetAlignedPointerFromInternalField / SetAlignedPointerInInternalField; #388
added the same pair in internal-field.hh and deliberately left wall.cc alone
to avoid conflicting with #387, which was in flight. #387 has landed, so
wall.cc now includes the header and its copies are gone. Same namespace and
names, so every call site is unchanged.

Register the drain hook in Init() rather than lazily on first Wrap(), which
removes g_drain_hook_registered entirely. Module initialisation always runs
with a context entered, so AddEnvironmentCleanupHook's CHECK is satisfied
there too, and Init() runs exactly once per isolate — which is the lifetime
the hook should match. The flag existed only to make the lazy registration
idempotent and to re-arm after an isolate was torn down and recreated on the
same thread; Init() running again on the new isolate covers that by
construction.

Clear the holder's internal field before freeing the CtxWrap it points at.
The drain hook now nulls slot 0 on its way through the list. This matters
more than a tidiness nit: that slot is exactly what the out-of-process
OTEP-4947 reader walks, so leaving it pointing at freed memory is a loaded
gun aimed at a consumer we do not control. Being on the live list means V8
has not collected the holder, so reading the handle there is safe; the
WeakCallback path cannot do this and does not need to, since there the holder
is the object being collected.

Verified on Node 20, 24 and 26: ASAN exit 0 with zero leaks and zero aborts
on 20 and 24, 165 passing on 24 and 26, the teardown regression test passing
where the OTEP block runs, the original repro clean at N=3000 and N=10000,
and the published native_wrap_fields_offset still 0.
@szegedi
szegedi force-pushed the szegedi/ctxwrap-review-followups branch from c9f9fee to 8dfa9bb Compare August 10, 2026 15:18
@szegedi szegedi added the semver-patch Bug or security fixes, mainly label Aug 10, 2026
// means V8 has not collected the holder, so the handle is safe to read
// here; the WeakCallback path cannot do this and does not need to,
// since there the holder is the thing being collected.
if (!p->handle_.IsEmpty()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: maybe do the same for PCP ?

memcpy(&new_rec->attrs_data[current_used], appended.data(), appended.size());
new_rec->attrs_data_size = static_cast<uint16_t>(new_used);
// The copy should've preserved valid=1 from the source record.
assert(new_rec->valid == 1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI flagged that this assert will fire in debug builds when doing invalidate() followed by large enough Append(...)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

semver-patch Bug or security fixes, mainly

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants