Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion src/async_wrap-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,25 @@ inline double AsyncWrap::get_trigger_async_id() const {
}

inline v8::Local<v8::Value> AsyncWrap::context_frame() const {
return context_frame_.Get(env()->isolate());
auto as_data = object()->GetInternalField(kAsyncContextFrame);
DCHECK_IMPLIES(!as_data.IsEmpty(),
as_data->IsValue() || as_data->IsPrivate());
if (as_data->IsPrivate()) {
DCHECK(as_data.As<v8::Private>()->Name()->SameValue(
env()->empty_context_frame_sentinel_symbol()->Name()));
return {};
}
return as_data.As<v8::Value>();
}

inline void AsyncWrap::set_context_frame(v8::Local<v8::Value> value) {
if (value.IsEmpty()) {
// Empty values are not allowed in internal fields
object()->SetInternalField(kAsyncContextFrame,
env()->empty_context_frame_sentinel_symbol());
} else {
object()->SetInternalField(kAsyncContextFrame, value);
}
}

inline v8::MaybeLocal<v8::Value> AsyncWrap::MakeCallback(
Expand Down
23 changes: 8 additions & 15 deletions src/async_wrap.cc
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ void AsyncWrap::EmitDestroy(bool from_gc) {
HandleScope handle_scope(env()->isolate());
USE(object()->Set(env()->context(), env()->resource_symbol(), object()));
}
context_frame_.Reset();
set_context_frame({});
}
}

Expand Down Expand Up @@ -530,9 +530,9 @@ AsyncWrap::AsyncWrap(Environment* env,
}

AsyncWrap::AsyncWrap(Environment* env, Local<Object> object)
: BaseObject(env, object),
context_frame_(env->isolate(),
async_context_frame::current(env->isolate())) {}
: BaseObject(env, object) {
set_context_frame(async_context_frame::current(env->isolate()));
}

// This method is necessary to work around one specific problem:
// Before the init() hook runs, if there is one, the BaseObject() constructor
Expand Down Expand Up @@ -635,7 +635,7 @@ void AsyncWrap::AsyncReset(Local<Object> resource, double execution_async_id) {

EmitTraceAsyncStart();

context_frame_.Reset(isolate, async_context_frame::current(isolate));
set_context_frame(async_context_frame::current(isolate));

EmitAsyncInit(env(), resource,
env()->async_hooks()->provider_string(provider_type()),
Expand Down Expand Up @@ -678,7 +678,7 @@ MaybeLocal<Value> AsyncWrap::MakeCallback(const Local<Function> cb,
EmitTraceEventBefore();

#ifdef DEBUG
if (context_frame_.IsEmpty()) {
if (context_frame().IsEmpty()) {
ProcessEmitWarning(env(),
"MakeCallback() called without context_frame, "
"likely use after destroy of AsyncWrap.");
Expand All @@ -687,15 +687,8 @@ MaybeLocal<Value> AsyncWrap::MakeCallback(const Local<Function> cb,

ProviderType provider = provider_type();
async_context context { get_async_id(), get_trigger_async_id() };
MaybeLocal<Value> ret =
InternalMakeCallback(env(),
object(),
object(),
cb,
argc,
argv,
context,
context_frame_.Get(env()->isolate()));
MaybeLocal<Value> ret = InternalMakeCallback(
env(), object(), object(), cb, argc, argv, context, context_frame());

// This is a static call with cached values because the `this` object may
// no longer be alive at this point.
Expand Down
6 changes: 3 additions & 3 deletions src/async_wrap.h
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ class ExternalReferenceRegistry;
class AsyncWrap : public BaseObject {
public:
enum InternalFields {
kInternalFieldCount = BaseObject::kInternalFieldCount,
kAsyncContextFrame = BaseObject::kInternalFieldCount,
kInternalFieldCount,
};

enum ProviderType {
Expand Down Expand Up @@ -201,6 +202,7 @@ class AsyncWrap : public BaseObject {
inline double get_trigger_async_id() const;

inline v8::Local<v8::Value> context_frame() const;
inline void set_context_frame(v8::Local<v8::Value> value);

void AsyncReset(v8::Local<v8::Object> resource,
double execution_async_id = kInvalidAsyncId);
Expand Down Expand Up @@ -244,8 +246,6 @@ class AsyncWrap : public BaseObject {
// Because the values may be Reset(), cannot be made const.
double async_id_ = kInvalidAsyncId;
double trigger_async_id_ = kInvalidAsyncId;

v8::Global<v8::Value> context_frame_;
};

} // namespace node
Expand Down
1 change: 1 addition & 0 deletions src/env_properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
V(arrow_message_private_symbol, "node:arrowMessage") \
V(contextify_context_private_symbol, "node:contextify:context") \
V(decorated_private_symbol, "node:decorated") \
V(empty_context_frame_sentinel_symbol, "node:empty_context_frame_sentinel") \
V(transfer_mode_private_symbol, "node:transfer_mode") \
V(host_defined_option_symbol, "node:host_defined_option_symbol") \
V(js_transferable_wrapper_private_symbol, "node:js_transferable_wrapper") \
Expand Down
49 changes: 49 additions & 0 deletions test/parallel/test-async-local-storage-weak-asyncwrap-leak.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Flags: --expose-gc
'use strict';
const common = require('../common');
const assert = require('node:assert');
const zlib = require('node:zlib');
const v8 = require('node:v8');
const { AsyncLocalStorage } = require('node:async_hooks');

// This test verifies that referencing an AsyncLocalStorage store from
// a weak AsyncWrap does not prevent the store from being garbage collected.
// We use zlib streams as examples of weak AsyncWraps here, but the
// issue is not specific to zlib.

class Store {}

let zlibDone = false;

// Use immediates to ensure no accidental async context propagation occurs
setImmediate(common.mustCall(() => {
const asyncLocalStorage = new AsyncLocalStorage();
const store = new Store();
asyncLocalStorage.run(store, common.mustCall(() => {
(async () => {
const zlibStream = zlib.createGzip();
// (Make sure this test does not break if _handle is renamed
// to something else)
assert.strictEqual(typeof zlibStream._handle, 'object');
// Create backreference from AsyncWrap to store
store.zlibStream = zlibStream._handle;
// Let the zlib stream finish (not strictly necessary)
zlibStream.end('hello world');
await zlibStream.toArray();
zlibDone = true;
})().then(common.mustCall());
}));
}));

const finish = common.mustCall(() => {
global.gc();
// Make sure the ALS instance has been garbage-collected
assert.strictEqual(v8.queryObjects(Store), 0);
});

const interval = setInterval(() => {
if (zlibDone) {
clearInterval(interval);
finish();
}
}, 5);
Loading