Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit ade01e9

Browse files
committedOct 3, 2020
src: add check against non-weak BaseObjects at process exit
When a process exits cleanly, i.e. because the event loop ends up without things to wait for, the Node.js objects that are left on the heap should be: 1. weak, i.e. ready for garbage collection once no longer referenced, or 2. detached, i.e. scheduled for destruction once no longer referenced, or 3. an unrefed libuv handle, i.e. does not keep the event loop alive, or 4. an inactive libuv handle (essentially the same here) There are a few exceptions to this rule, but generally, if there are C++-backed Node.js objects on the heap that do not fall into the above categories, we may be looking at a potential memory leak. Most likely, the cause is a missing `MakeWeak()` call on the corresponding object. In order to avoid this kind of problem, we check the list of BaseObjects for these criteria. In this commit, we only do so when explicitly instructed to or when in debug mode (where --verify-base-objects is always-on). In particular, this avoids the kinds of memory leak issues that were fixed in the PRs referenced below. Refs: nodejs#35488 Refs: nodejs#35487 Refs: nodejs#35481
1 parent f83f683 commit ade01e9

18 files changed

+120
-16
lines changed
 

‎src/async_wrap.cc

+6
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@ struct AsyncWrapObject : public AsyncWrap {
9999
return tmpl;
100100
}
101101

102+
bool IsAllowedStrongObjectAtExit() const override {
103+
// We can't really know what the underlying operation does. One of the
104+
// signs that it's time to remove this class. :)
105+
return true;
106+
}
107+
102108
SET_NO_MEMORY_INFO()
103109
SET_MEMORY_INFO_NAME(AsyncWrapObject)
104110
SET_SELF_SIZE(AsyncWrapObject)

‎src/base_object-inl.h

+7
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,13 @@ void BaseObject::ClearWeak() {
146146
persistent_handle_.ClearWeak();
147147
}
148148

149+
bool BaseObject::IsWeakOrDetached() const {
150+
if (persistent_handle_.IsWeak()) return true;
151+
152+
if (!has_pointer_data()) return false;
153+
const PointerData* pd = const_cast<BaseObject*>(this)->pointer_data();
154+
return pd->wants_weak_jsobj || pd->is_detached;
155+
}
149156

150157
v8::Local<v8::FunctionTemplate>
151158
BaseObject::MakeLazilyInitializedJSTemplate(Environment* env) {

‎src/base_object.h

+9
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ class BaseObject : public MemoryRetainer {
7878
// root and will not be touched by the garbage collector.
7979
inline void ClearWeak();
8080

81+
// Reports whether this BaseObject is using a weak reference or detached,
82+
// i.e. whether is can be deleted by GC once no strong BaseObjectPtrs refer
83+
// to it anymore.
84+
inline bool IsWeakOrDetached() const;
85+
8186
// Utility to create a FunctionTemplate with one internal field (used for
8287
// the `BaseObject*` pointer) and a constructor that initializes that field
8388
// to `nullptr`.
@@ -147,6 +152,10 @@ class BaseObject : public MemoryRetainer {
147152
virtual v8::Maybe<bool> FinalizeTransferRead(
148153
v8::Local<v8::Context> context, v8::ValueDeserializer* deserializer);
149154

155+
// Indicates whether this object is expected to use a strong reference during
156+
// a clean process exit (due to an empty event loop).
157+
virtual bool IsAllowedStrongObjectAtExit() const;
158+
150159
virtual inline void OnGCCollect();
151160

152161
private:

‎src/env.cc

+38-13
Original file line numberDiff line numberDiff line change
@@ -1194,22 +1194,43 @@ void Environment::RemoveUnmanagedFd(int fd) {
11941194
}
11951195
}
11961196

1197-
void Environment::ForEachBaseObject(BaseObjectIterator iterator) {
1197+
void Environment::PrintAllBaseObjects() {
11981198
size_t i = 0;
1199-
for (const auto& hook : cleanup_hooks_) {
1200-
BaseObject* obj = hook.GetBaseObject();
1201-
if (obj != nullptr) iterator(i, obj);
1202-
i++;
1203-
}
1204-
}
1205-
1206-
void PrintBaseObject(size_t i, BaseObject* obj) {
1207-
std::cout << "#" << i << " " << obj << ": " << obj->MemoryInfoName() << "\n";
1199+
std::cout << "BaseObjects\n";
1200+
ForEachBaseObject([&](BaseObject* obj) {
1201+
std::cout << "#" << i++ << " " << obj << ": " <<
1202+
obj->MemoryInfoName() << "\n";
1203+
});
12081204
}
12091205

1210-
void Environment::PrintAllBaseObjects() {
1211-
std::cout << "BaseObjects\n";
1212-
ForEachBaseObject(PrintBaseObject);
1206+
void Environment::VerifyNoStrongBaseObjects() {
1207+
// When a process exits cleanly, i.e. because the event loop ends up without
1208+
// things to wait for, the Node.js objects that are left on the heap should
1209+
// be:
1210+
//
1211+
// 1. weak, i.e. ready for garbage collection once no longer referenced, or
1212+
// 2. detached, i.e. scheduled for destruction once no longer referenced, or
1213+
// 3. an unrefed libuv handle, i.e. does not keep the event loop alive, or
1214+
// 4. an inactive libuv handle (essentially the same here)
1215+
//
1216+
// There are a few exceptions to this rule, but generally, if there are
1217+
// C++-backed Node.js objects on the heap that do not fall into the above
1218+
// categories, we may be looking at a potential memory leak. Most likely,
1219+
// the cause is a missing MakeWeak() call on the corresponding object.
1220+
//
1221+
// In order to avoid this kind of problem, we check the list of BaseObjects
1222+
// for these criteria. Currently, we only do so when explicitly instructed to
1223+
// or when in debug mode (where --verify-base-objects is always-on).
1224+
1225+
if (!options()->verify_base_objects) return;
1226+
1227+
ForEachBaseObject([this](BaseObject* obj) {
1228+
if (obj->IsAllowedStrongObjectAtExit()) return;
1229+
fprintf(stderr, "Found bad BaseObject during clean exit: %s\n",
1230+
obj->MemoryInfoName().c_str());
1231+
fflush(stderr);
1232+
ABORT();
1233+
});
12131234
}
12141235

12151236
EnvSerializeInfo Environment::Serialize(SnapshotCreator* creator) {
@@ -1473,4 +1494,8 @@ Local<FunctionTemplate> BaseObject::GetConstructorTemplate(Environment* env) {
14731494
return tmpl;
14741495
}
14751496

1497+
bool BaseObject::IsAllowedStrongObjectAtExit() const {
1498+
return IsWeakOrDetached();
1499+
}
1500+
14761501
} // namespace node

‎src/env.h

+1-2
Original file line numberDiff line numberDiff line change
@@ -934,9 +934,8 @@ class Environment : public MemoryRetainer {
934934
void CreateProperties();
935935
void DeserializeProperties(const EnvSerializeInfo* info);
936936

937-
typedef void (*BaseObjectIterator)(size_t, BaseObject*);
938-
void ForEachBaseObject(BaseObjectIterator iterator);
939937
void PrintAllBaseObjects();
938+
void VerifyNoStrongBaseObjects();
940939
// Should be called before InitializeInspector()
941940
void InitializeDiagnostics();
942941
#if HAVE_INSPECTOR

‎src/handle_wrap.cc

+7
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,13 @@ void HandleWrap::OnGCCollect() {
8989
}
9090

9191

92+
bool HandleWrap::IsAllowedStrongObjectAtExit() const {
93+
return IsWeakOrDetached() ||
94+
!HandleWrap::HasRef(this) ||
95+
!uv_is_active(GetHandle());
96+
}
97+
98+
9299
void HandleWrap::MarkAsInitialized() {
93100
env()->handle_wrap_queue()->PushBack(this);
94101
state_ = kInitialized;

‎src/handle_wrap.h

+1
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ class HandleWrap : public AsyncWrap {
8787
AsyncWrap::ProviderType provider);
8888
virtual void OnClose() {}
8989
void OnGCCollect() final;
90+
bool IsAllowedStrongObjectAtExit() const override;
9091

9192
void MarkAsInitialized();
9293
void MarkAsUninitialized();

‎src/inspector_js_api.cc

+4
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,10 @@ class JSBindingsConnection : public AsyncWrap {
156156
SET_MEMORY_INFO_NAME(JSBindingsConnection)
157157
SET_SELF_SIZE(JSBindingsConnection)
158158

159+
bool IsAllowedStrongObjectAtExit() const override {
160+
return true; // Binding connections emit events on their own.
161+
}
162+
159163
private:
160164
std::unique_ptr<InspectorSession> session_;
161165
Global<Function> callback_;

‎src/module_wrap.h

+6
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ class ModuleWrap : public BaseObject {
6060
SET_MEMORY_INFO_NAME(ModuleWrap)
6161
SET_SELF_SIZE(ModuleWrap)
6262

63+
bool IsAllowedStrongObjectAtExit() const override {
64+
// XXX: The garbage collection rules for ModuleWrap are *super* unclear.
65+
// Do these objects ever get GC'd? Are we just okay with leaking them?
66+
return true;
67+
}
68+
6369
private:
6470
ModuleWrap(Environment* env,
6571
v8::Local<v8::Object> object,

‎src/node_contextify.h

+2
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,8 @@ class CompiledFnEntry final : public BaseObject {
174174
v8::Local<v8::ScriptOrModule> script);
175175
~CompiledFnEntry();
176176

177+
bool IsAllowedStrongObjectAtExit() const override { return true; }
178+
177179
private:
178180
uint32_t id_;
179181
v8::Global<v8::ScriptOrModule> script_;

‎src/node_http_parser.cc

+9
Original file line numberDiff line numberDiff line change
@@ -880,6 +880,15 @@ class Parser : public AsyncWrap, public StreamListener {
880880
return HPE_PAUSED;
881881
}
882882

883+
884+
bool IsAllowedStrongObjectAtExit() const override {
885+
// HTTP parsers are able to emit events without any GC root referring
886+
// to them, because they receive events directly from the underlying
887+
// libuv resource.
888+
return true;
889+
}
890+
891+
883892
llhttp_t parser_;
884893
StringPtr fields_[kMaxHeaderFieldsCount]; // header fields
885894
StringPtr values_[kMaxHeaderFieldsCount]; // header values

‎src/node_main_instance.cc

+1
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ int NodeMainInstance::Run(const EnvSerializeInfo* env_info) {
168168
}
169169

170170
env->set_trace_sync_io(false);
171+
if (!env->is_stopping()) env->VerifyNoStrongBaseObjects();
171172
exit_code = EmitExit(env.get());
172173
}
173174

‎src/node_options.cc

+4
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
475475
"an error), 'warn' (enforce warnings) or 'none' (silence warnings)",
476476
&EnvironmentOptions::unhandled_rejections,
477477
kAllowedInEnvironment);
478+
AddOption("--verify-base-objects",
479+
"", /* undocumented, only for debugging */
480+
&EnvironmentOptions::verify_base_objects,
481+
kAllowedInEnvironment);
478482

479483
AddOption("--check",
480484
"syntax check script without executing",

‎src/node_options.h

+6
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,12 @@ class EnvironmentOptions : public Options {
150150
bool trace_warnings = false;
151151
std::string unhandled_rejections;
152152
std::string userland_loader;
153+
bool verify_base_objects =
154+
#ifdef DEBUG
155+
true;
156+
#else
157+
false;
158+
#endif // DEBUG
153159

154160
bool syntax_check_only = false;
155161
bool has_eval_string = false;

‎src/node_worker.cc

+9-1
Original file line numberDiff line numberDiff line change
@@ -362,8 +362,10 @@ void Worker::Run() {
362362
{
363363
int exit_code;
364364
bool stopped = is_stopped();
365-
if (!stopped)
365+
if (!stopped) {
366+
env_->VerifyNoStrongBaseObjects();
366367
exit_code = EmitExit(env_.get());
368+
}
367369
Mutex::ScopedLock lock(mutex_);
368370
if (exit_code_ == 0 && !stopped)
369371
exit_code_ = exit_code;
@@ -714,6 +716,12 @@ void Worker::MemoryInfo(MemoryTracker* tracker) const {
714716
tracker->TrackField("parent_port", parent_port_);
715717
}
716718

719+
bool Worker::IsAllowedStrongObjectAtExit() const {
720+
// Worker objects always stay alive as long as the child thread, regardless
721+
// of whether they are being referenced in the parent thread.
722+
return true;
723+
}
724+
717725
class WorkerHeapSnapshotTaker : public AsyncWrap {
718726
public:
719727
WorkerHeapSnapshotTaker(Environment* env, Local<Object> obj)

‎src/node_worker.h

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ class Worker : public AsyncWrap {
5050
void MemoryInfo(MemoryTracker* tracker) const override;
5151
SET_MEMORY_INFO_NAME(Worker)
5252
SET_SELF_SIZE(Worker)
53+
bool IsAllowedStrongObjectAtExit() const override;
5354

5455
bool is_stopped() const;
5556

‎src/stream_base.h

+8
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,10 @@ class SimpleShutdownWrap : public ShutdownWrap, public OtherBase {
412412
SET_NO_MEMORY_INFO()
413413
SET_MEMORY_INFO_NAME(SimpleShutdownWrap)
414414
SET_SELF_SIZE(SimpleShutdownWrap)
415+
416+
bool IsAllowedStrongObjectAtExit() const override {
417+
return OtherBase::IsAllowedStrongObjectAtExit();
418+
}
415419
};
416420

417421
template <typename OtherBase>
@@ -425,6 +429,10 @@ class SimpleWriteWrap : public WriteWrap, public OtherBase {
425429
SET_NO_MEMORY_INFO()
426430
SET_MEMORY_INFO_NAME(SimpleWriteWrap)
427431
SET_SELF_SIZE(SimpleWriteWrap)
432+
433+
bool IsAllowedStrongObjectAtExit() const override {
434+
return OtherBase::IsAllowedStrongObjectAtExit();
435+
}
428436
};
429437

430438
} // namespace node

‎test/parallel/test-process-env-allowed-flags-are-documented.js

+1
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ assert(undocumented.delete('--experimental-report'));
8888
assert(undocumented.delete('--experimental-worker'));
8989
assert(undocumented.delete('--no-node-snapshot'));
9090
assert(undocumented.delete('--loader'));
91+
assert(undocumented.delete('--verify-base-objects'));
9192

9293
assert.strictEqual(undocumented.size, 0,
9394
'The following options are not documented as allowed in ' +

0 commit comments

Comments
 (0)
Please sign in to comment.