Skip to content

Commit 2f14395

Browse files
committed
src: make realm binding data store weak
The binding data must be weak so that it won't keep the realm reachable from strong GC roots indefinitely. The wrapper object of binding data should be referenced from JavaScript, thus the binding data should be reachable throughout the lifetime of the realm.
1 parent 9398ff1 commit 2f14395

14 files changed

+65
-42
lines changed

src/aliased_buffer-inl.h

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,8 @@ AliasedBufferBase<NativeT, V8T>::AliasedBufferBase(
7070
count_(that.count_),
7171
byte_offset_(that.byte_offset_),
7272
buffer_(that.buffer_) {
73-
DCHECK(is_valid());
7473
js_array_ = v8::Global<V8T>(that.isolate_, that.GetJSArray());
74+
DCHECK(is_valid());
7575
}
7676

7777
template <typename NativeT, typename V8T>
@@ -126,19 +126,10 @@ void AliasedBufferBase<NativeT, V8T>::Release() {
126126
js_array_.Reset();
127127
}
128128

129-
template <typename NativeT, typename V8T>
130-
inline void AliasedBufferBase<NativeT, V8T>::WeakCallback(
131-
const v8::WeakCallbackInfo<AliasedBufferBase<NativeT, V8T>>& data) {
132-
AliasedBufferBase<NativeT, V8T>* buffer = data.GetParameter();
133-
DCHECK(buffer->is_valid());
134-
buffer->cleared_ = true;
135-
buffer->js_array_.Reset();
136-
}
137-
138129
template <typename NativeT, typename V8T>
139130
inline void AliasedBufferBase<NativeT, V8T>::MakeWeak() {
140131
DCHECK(is_valid());
141-
js_array_.SetWeak(this, WeakCallback, v8::WeakCallbackType::kParameter);
132+
js_array_.SetWeak();
142133
}
143134

144135
template <typename NativeT, typename V8T>
@@ -223,7 +214,7 @@ void AliasedBufferBase<NativeT, V8T>::reserve(size_t new_capacity) {
223214

224215
template <typename NativeT, typename V8T>
225216
inline bool AliasedBufferBase<NativeT, V8T>::is_valid() const {
226-
return index_ == nullptr && !cleared_;
217+
return index_ == nullptr && !js_array_.IsEmpty();
227218
}
228219

229220
template <typename NativeT, typename V8T>

src/aliased_buffer.h

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,14 +173,11 @@ class AliasedBufferBase : public MemoryRetainer {
173173

174174
private:
175175
inline bool is_valid() const;
176-
static inline void WeakCallback(
177-
const v8::WeakCallbackInfo<AliasedBufferBase<NativeT, V8T>>& data);
178176
v8::Isolate* isolate_ = nullptr;
179177
size_t count_ = 0;
180178
size_t byte_offset_ = 0;
181179
NativeT* buffer_ = nullptr;
182180
v8::Global<V8T> js_array_;
183-
bool cleared_ = false;
184181

185182
// Deserialize data
186183
const AliasedBufferIndex* index_ = nullptr;

src/base_object-inl.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,12 @@ template <typename T, typename... Args>
289289
BaseObjectPtr<T> MakeBaseObject(Args&&... args) {
290290
return BaseObjectPtr<T>(new T(std::forward<Args>(args)...));
291291
}
292+
template <typename T, typename... Args>
293+
BaseObjectWeakPtr<T> MakeWeakBaseObject(Args&&... args) {
294+
T* target = new T(std::forward<Args>(args)...);
295+
target->MakeWeak();
296+
return BaseObjectWeakPtr<T>(target);
297+
}
292298

293299
template <typename T, typename... Args>
294300
BaseObjectPtr<T> MakeDetachedBaseObject(Args&&... args) {

src/base_object.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,10 @@ using BaseObjectWeakPtr = BaseObjectPtrImpl<T, true>;
302302
template <typename T, typename... Args>
303303
inline BaseObjectPtr<T> MakeBaseObject(Args&&... args);
304304
// Create a BaseObject instance and return a pointer to it.
305+
// This variant makes the object a weak GC root by default.
306+
template <typename T, typename... Args>
307+
inline BaseObjectWeakPtr<T> MakeWeakBaseObject(Args&&... args);
308+
// Create a BaseObject instance and return a pointer to it.
305309
// This variant detaches the object by default, meaning that the caller fully
306310
// owns it, and once the last BaseObjectPtr to it is destroyed, the object
307311
// itself is also destroyed.

src/env.cc

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1714,6 +1714,9 @@ void Environment::DeserializeProperties(const EnvSerializeInfo* info) {
17141714
std::cerr << *info << "\n";
17151715
}
17161716

1717+
// Deserialize the realm's properties before running the deserialize
1718+
// requests as the requests may need to access the realm's properties.
1719+
principal_realm_->DeserializeProperties(&info->principal_realm);
17171720
RunDeserializeRequests();
17181721

17191722
async_hooks_.Deserialize(ctx);
@@ -1724,8 +1727,6 @@ void Environment::DeserializeProperties(const EnvSerializeInfo* info) {
17241727
exit_info_.Deserialize(ctx);
17251728
stream_base_state_.Deserialize(ctx);
17261729
should_abort_on_uncaught_toggle_.Deserialize(ctx);
1727-
1728-
principal_realm_->DeserializeProperties(&info->principal_realm);
17291730
}
17301731

17311732
uint64_t GuessMemoryAvailableToTheProcess() {

src/inspector_js_api.cc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,12 +172,12 @@ static bool InspectorEnabled(Environment* env) {
172172
}
173173

174174
void SetConsoleExtensionInstaller(const FunctionCallbackInfo<Value>& info) {
175-
auto env = Environment::GetCurrent(info);
175+
Realm* realm = Realm::GetCurrent(info);
176176

177177
CHECK_EQ(info.Length(), 1);
178178
CHECK(info[0]->IsFunction());
179179

180-
env->set_inspector_console_extension_installer(info[0].As<Function>());
180+
realm->set_inspector_console_extension_installer(info[0].As<Function>());
181181
}
182182

183183
void CallAndPauseOnStart(const FunctionCallbackInfo<v8::Value>& args) {

src/node_realm-inl.h

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,13 @@ inline T* Realm::AddBindingData(v8::Local<v8::Context> context,
8686
Args&&... args) {
8787
DCHECK_EQ(GetCurrent(context), this);
8888
// This won't compile if T is not a BaseObject subclass.
89-
BaseObjectPtr<T> item =
90-
MakeDetachedBaseObject<T>(this, target, std::forward<Args>(args)...);
89+
static_assert(std::is_base_of_v<BaseObject, T>);
90+
// The binding data must be weak so that it won't keep the realm reachable
91+
// from strong GC roots indefinitely. The wrapper object of binding data
92+
// should be referenced from JavaScript, thus the binding data should be
93+
// reachable throughout the lifetime of the realm.
94+
BaseObjectWeakPtr<T> item =
95+
MakeWeakBaseObject<T>(this, target, std::forward<Args>(args)...);
9196
DCHECK_EQ(context->GetAlignedPointerFromEmbedderData(
9297
ContextEmbedderIndex::kBindingDataStoreIndex),
9398
&binding_data_store_);

src/node_realm.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,11 +278,15 @@ v8::Local<v8::Context> Realm::context() const {
278278
return PersistentToLocal::Strong(context_);
279279
}
280280

281+
// Per-realm strong value accessors. The per-realm values should avoid being
282+
// accessed across realms.
281283
#define V(PropertyName, TypeName) \
282284
v8::Local<TypeName> PrincipalRealm::PropertyName() const { \
283285
return PersistentToLocal::Strong(PropertyName##_); \
284286
} \
285287
void PrincipalRealm::set_##PropertyName(v8::Local<TypeName> value) { \
288+
DCHECK_IMPLIES(!value.IsEmpty(), \
289+
isolate()->GetCurrentContext() == context()); \
286290
PropertyName##_.Reset(isolate(), value); \
287291
}
288292
PER_REALM_STRONG_PERSISTENT_VALUES(V)

src/node_realm.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ struct RealmSerializeInfo {
2121
friend std::ostream& operator<<(std::ostream& o, const RealmSerializeInfo& i);
2222
};
2323

24-
using BindingDataStore = std::array<BaseObjectPtr<BaseObject>,
25-
static_cast<size_t>(
26-
BindingDataType::kBindingDataTypeCount)>;
24+
using BindingDataStore =
25+
std::array<BaseObjectWeakPtr<BaseObject>,
26+
static_cast<size_t>(BindingDataType::kBindingDataTypeCount)>;
2727

2828
/**
2929
* node::Realm is a container for a set of JavaScript objects and functions

src/node_shadow_realm.cc

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace node {
66
namespace shadow_realm {
77
using v8::Context;
8+
using v8::EscapableHandleScope;
89
using v8::HandleScope;
910
using v8::Local;
1011
using v8::MaybeLocal;
@@ -31,17 +32,24 @@ ShadowRealm* ShadowRealm::New(Environment* env) {
3132
MaybeLocal<Context> HostCreateShadowRealmContextCallback(
3233
Local<Context> initiator_context) {
3334
Environment* env = Environment::GetCurrent(initiator_context);
35+
EscapableHandleScope scope(env->isolate());
3436
ShadowRealm* realm = ShadowRealm::New(env);
3537
if (realm != nullptr) {
36-
return realm->context();
38+
return scope.Escape(realm->context());
3739
}
3840
return MaybeLocal<Context>();
3941
}
4042

4143
// static
4244
void ShadowRealm::WeakCallback(const v8::WeakCallbackInfo<ShadowRealm>& data) {
4345
ShadowRealm* realm = data.GetParameter();
44-
delete realm;
46+
realm->context_.Reset();
47+
48+
// Yield to pending weak callbacks before deleting the realm.
49+
// This is necessary to avoid cleaning up base objects before their scheduled
50+
// weak callbacks are invoked, which can lead to accessing to v8 apis during
51+
// the first pass of the weak callback.
52+
realm->env()->SetImmediate([realm](Environment* env) { delete realm; });
4553
}
4654

4755
ShadowRealm::ShadowRealm(Environment* env)

src/node_url.cc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ BindingData::BindingData(Realm* realm, v8::Local<v8::Object> object)
4141
FIXED_ONE_BYTE_STRING(realm->isolate(), "urlComponents"),
4242
url_components_buffer_.GetJSArray())
4343
.Check();
44+
url_components_buffer_.MakeWeak();
4445
}
4546

4647
bool BindingData::PrepareForSerialization(v8::Local<v8::Context> context,

test/known_issues/known_issues.status

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,6 @@ prefix known_issues
1111
# foreseeable future. The test itself is flaky and skipped. It
1212
# serves as a demonstration of the issue only.
1313
test-vm-timeout-escape-queuemicrotask: SKIP
14-
# Skipping it because it crashes out of OOM instead of exiting.
15-
# https://github.com/nodejs/node/issues/47353
16-
test-shadow-realm-gc: SKIP
1714

1815
[$system==win32]
1916

test/known_issues/test-shadow-realm-gc.js

Lines changed: 0 additions & 13 deletions
This file was deleted.

test/parallel/test-shadow-realm-gc.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Flags: --experimental-shadow-realm --max-old-space-size=20
2+
'use strict';
3+
4+
/**
5+
* Verifying ShadowRealm instances can be correctly garbage collected.
6+
*/
7+
8+
const common = require('../common');
9+
const assert = require('assert');
10+
const { isMainThread, Worker } = require('worker_threads');
11+
12+
for (let i = 0; i < 100; i++) {
13+
const realm = new ShadowRealm();
14+
realm.evaluate('new TextEncoder(); 1;');
15+
}
16+
17+
if (isMainThread) {
18+
const worker = new Worker(__filename);
19+
worker.on('exit', common.mustCall((code) => {
20+
assert.strictEqual(code, 0);
21+
}));
22+
}

0 commit comments

Comments
 (0)