From bdd2d389b90e9196355091c39a65d83fa620782c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerhard=20St=C3=B6bich?= <18708370+Flarna@users.noreply.github.com> Date: Wed, 25 Feb 2026 23:11:23 +0100 Subject: [PATCH 1/3] src: release context frame in AsyncWrap::EmitDestroy Release the async context frame in AsyncWrap::EmitDestroy to allow gc to collect it. This is in special relevant for reused resources like HTTPParser otherwise they might keep ALS stores alive. --- src/async_wrap.cc | 9 ++++-- ...st-async-local-storage-http-parser-leak.js | 29 +++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 test/parallel/test-async-local-storage-http-parser-leak.js diff --git a/src/async_wrap.cc b/src/async_wrap.cc index 4a9e5fc1ab673c..3ebaf30949ea97 100644 --- a/src/async_wrap.cc +++ b/src/async_wrap.cc @@ -333,9 +333,12 @@ void AsyncWrap::EmitDestroy(bool from_gc) { // Ensure no double destroy is emitted via AsyncReset(). async_id_ = kInvalidAsyncId; - if (!persistent().IsEmpty() && !from_gc) { - HandleScope handle_scope(env()->isolate()); - USE(object()->Set(env()->context(), env()->resource_symbol(), object())); + if (!from_gc) { + if (!persistent().IsEmpty()) { + HandleScope handle_scope(env()->isolate()); + USE(object()->Set(env()->context(), env()->resource_symbol(), object())); + } + context_frame_.Reset(); } } diff --git a/test/parallel/test-async-local-storage-http-parser-leak.js b/test/parallel/test-async-local-storage-http-parser-leak.js new file mode 100644 index 00000000000000..2992db7c73f575 --- /dev/null +++ b/test/parallel/test-async-local-storage-http-parser-leak.js @@ -0,0 +1,29 @@ +// Flags: --expose-gc +'use strict'; + +const common = require('../common'); +const { onGC } = require('../common/gc'); +const assert = require('node:assert'); +const { AsyncLocalStorage } = require('node:async_hooks'); +const { freeParser, parsers, HTTPParser } = require('_http_common'); + +let storeGCed = false; + +const als = new AsyncLocalStorage(); + +function test() { + const store = {}; + onGC(store, { ongc: common.mustCall(() => { storeGCed = true; }) }); + let parser; + als.run(store, common.mustCall(() => { + parser = parsers.alloc(); + parser.initialize(HTTPParser.RESPONSE, {}); + })); + freeParser(parser); +} + +test(); +global.gc(); +setImmediate(common.mustCall(() => { + assert.ok(storeGCed); +})); From 2500801783c1f39e5a3127f0a56a012d06152ba7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerhard=20St=C3=B6bich?= Date: Thu, 26 Feb 2026 12:17:34 +0100 Subject: [PATCH 2/3] Add DCHECK to ensure context_frame is not emtpy in MakeCallback Co-authored-by: Anna Henningsen --- src/async_wrap.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/async_wrap.cc b/src/async_wrap.cc index 3ebaf30949ea97..e10cdb4dbdcd62 100644 --- a/src/async_wrap.cc +++ b/src/async_wrap.cc @@ -662,6 +662,7 @@ MaybeLocal AsyncWrap::MakeCallback(const Local cb, Local* argv) { EmitTraceEventBefore(); + DCHECK(!context_frame_.IsEmpty()); ProviderType provider = provider_type(); async_context context { get_async_id(), get_trigger_async_id() }; MaybeLocal ret = From c4583995275a6af247413b14aa350fdb3b373c1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerhard=20St=C3=B6bich?= <18708370+Flarna@users.noreply.github.com> Date: Thu, 26 Feb 2026 20:15:33 +0100 Subject: [PATCH 3/3] use undefined to avoid empty global --- src/async_wrap.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/async_wrap.cc b/src/async_wrap.cc index e10cdb4dbdcd62..66d96847e478c7 100644 --- a/src/async_wrap.cc +++ b/src/async_wrap.cc @@ -338,7 +338,8 @@ void AsyncWrap::EmitDestroy(bool from_gc) { HandleScope handle_scope(env()->isolate()); USE(object()->Set(env()->context(), env()->resource_symbol(), object())); } - context_frame_.Reset(); + Isolate* isolate = env()->isolate(); + context_frame_.Reset(isolate, Undefined(isolate)); } }