Skip to content

Commit 8ffaf59

Browse files
addaleaxtargos
authored andcommitted
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: #35488 Refs: #35487 Refs: #35481 PR-URL: #35490 Backport-PR-URL: #38786 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Rich Trott <[email protected]> Reviewed-By: Joyee Cheung <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]>
1 parent 55d0cd8 commit 8ffaf59

18 files changed

+116
-1
lines changed

src/async_wrap.cc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ struct AsyncWrapObject : public AsyncWrap {
9898
return tmpl;
9999
}
100100

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

src/base_object-inl.h

Lines changed: 7 additions & 0 deletions
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

Lines changed: 9 additions & 0 deletions
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 IsNotIndicativeOfMemoryLeakAtExit() const;
158+
150159
virtual inline void OnGCCollect();
151160

152161
private:

src/env.cc

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,6 +1043,36 @@ void Environment::RemoveUnmanagedFd(int fd) {
10431043
}
10441044
}
10451045

1046+
void Environment::VerifyNoStrongBaseObjects() {
1047+
// When a process exits cleanly, i.e. because the event loop ends up without
1048+
// things to wait for, the Node.js objects that are left on the heap should
1049+
// be:
1050+
//
1051+
// 1. weak, i.e. ready for garbage collection once no longer referenced, or
1052+
// 2. detached, i.e. scheduled for destruction once no longer referenced, or
1053+
// 3. an unrefed libuv handle, i.e. does not keep the event loop alive, or
1054+
// 4. an inactive libuv handle (essentially the same here)
1055+
//
1056+
// There are a few exceptions to this rule, but generally, if there are
1057+
// C++-backed Node.js objects on the heap that do not fall into the above
1058+
// categories, we may be looking at a potential memory leak. Most likely,
1059+
// the cause is a missing MakeWeak() call on the corresponding object.
1060+
//
1061+
// In order to avoid this kind of problem, we check the list of BaseObjects
1062+
// for these criteria. Currently, we only do so when explicitly instructed to
1063+
// or when in debug mode (where --verify-base-objects is always-on).
1064+
1065+
if (!options()->verify_base_objects) return;
1066+
1067+
ForEachBaseObject([this](BaseObject* obj) {
1068+
if (obj->IsNotIndicativeOfMemoryLeakAtExit()) return;
1069+
fprintf(stderr, "Found bad BaseObject during clean exit: %s\n",
1070+
obj->MemoryInfoName().c_str());
1071+
fflush(stderr);
1072+
ABORT();
1073+
});
1074+
}
1075+
10461076
void Environment::BuildEmbedderGraph(Isolate* isolate,
10471077
EmbedderGraph* graph,
10481078
void* data) {
@@ -1135,4 +1165,8 @@ Local<FunctionTemplate> BaseObject::GetConstructorTemplate(Environment* env) {
11351165
return tmpl;
11361166
}
11371167

1168+
bool BaseObject::IsNotIndicativeOfMemoryLeakAtExit() const {
1169+
return IsWeakOrDetached();
1170+
}
1171+
11381172
} // namespace node

src/env.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -833,6 +833,7 @@ class Environment : public MemoryRetainer {
833833
void MemoryInfo(MemoryTracker* tracker) const override;
834834

835835
void CreateProperties();
836+
void VerifyNoStrongBaseObjects();
836837
// Should be called before InitializeInspector()
837838
void InitializeDiagnostics();
838839
#if HAVE_INSPECTOR

src/handle_wrap.cc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,13 @@ void HandleWrap::OnGCCollect() {
8888
}
8989

9090

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

src/handle_wrap.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ class HandleWrap : public AsyncWrap {
8585
AsyncWrap::ProviderType provider);
8686
virtual void OnClose() {}
8787
void OnGCCollect() final;
88+
bool IsNotIndicativeOfMemoryLeakAtExit() const override;
8889

8990
void MarkAsInitialized();
9091
void MarkAsUninitialized();

src/inspector_js_api.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,10 @@ class JSBindingsConnection : public AsyncWrap {
155155
SET_MEMORY_INFO_NAME(JSBindingsConnection)
156156
SET_SELF_SIZE(JSBindingsConnection)
157157

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

src/module_wrap.h

Lines changed: 6 additions & 0 deletions
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 IsNotIndicativeOfMemoryLeakAtExit() 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

Lines changed: 2 additions & 0 deletions
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 IsNotIndicativeOfMemoryLeakAtExit() const override { return true; }
178+
177179
private:
178180
uint32_t id_;
179181
v8::Global<v8::ScriptOrModule> script_;

src/node_http_parser.cc

Lines changed: 9 additions & 0 deletions
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 IsNotIndicativeOfMemoryLeakAtExit() 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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ int NodeMainInstance::Run() {
147147
}
148148

149149
env->set_trace_sync_io(false);
150+
if (!env->is_stopping()) env->VerifyNoStrongBaseObjects();
150151
exit_code = EmitExit(env.get());
151152
}
152153

src/node_options.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
483483
"exit code 1 unless 'unhandledRejection' hook is set).",
484484
&EnvironmentOptions::unhandled_rejections,
485485
kAllowedInEnvironment);
486+
AddOption("--verify-base-objects",
487+
"", /* undocumented, only for debugging */
488+
&EnvironmentOptions::verify_base_objects,
489+
kAllowedInEnvironment);
486490

487491
AddOption("--check",
488492
"syntax check script without executing",

src/node_options.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,12 @@ class EnvironmentOptions : public Options {
151151
bool trace_warnings = false;
152152
std::string unhandled_rejections;
153153
std::string userland_loader;
154+
bool verify_base_objects =
155+
#ifdef DEBUG
156+
true;
157+
#else
158+
false;
159+
#endif // DEBUG
154160

155161
bool syntax_check_only = false;
156162
bool has_eval_string = false;

src/node_worker.cc

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,8 +366,10 @@ void Worker::Run() {
366366
{
367367
int exit_code;
368368
bool stopped = is_stopped();
369-
if (!stopped)
369+
if (!stopped) {
370+
env_->VerifyNoStrongBaseObjects();
370371
exit_code = EmitExit(env_.get());
372+
}
371373
Mutex::ScopedLock lock(mutex_);
372374
if (exit_code_ == 0 && !stopped)
373375
exit_code_ = exit_code;
@@ -718,6 +720,12 @@ void Worker::MemoryInfo(MemoryTracker* tracker) const {
718720
tracker->TrackField("parent_port", parent_port_);
719721
}
720722

723+
bool Worker::IsNotIndicativeOfMemoryLeakAtExit() const {
724+
// Worker objects always stay alive as long as the child thread, regardless
725+
// of whether they are being referenced in the parent thread.
726+
return true;
727+
}
728+
721729
class WorkerHeapSnapshotTaker : public AsyncWrap {
722730
public:
723731
WorkerHeapSnapshotTaker(Environment* env, Local<Object> obj)

src/node_worker.h

Lines changed: 1 addition & 0 deletions
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 IsNotIndicativeOfMemoryLeakAtExit() const override;
5354

5455
bool is_stopped() const;
5556

src/stream_base.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,10 @@ class SimpleShutdownWrap : public ShutdownWrap, public OtherBase {
423423
SET_NO_MEMORY_INFO()
424424
SET_MEMORY_INFO_NAME(SimpleShutdownWrap)
425425
SET_SELF_SIZE(SimpleShutdownWrap)
426+
427+
bool IsNotIndicativeOfMemoryLeakAtExit() const override {
428+
return OtherBase::IsNotIndicativeOfMemoryLeakAtExit();
429+
}
426430
};
427431

428432
template <typename OtherBase>
@@ -436,6 +440,10 @@ class SimpleWriteWrap : public WriteWrap, public OtherBase {
436440
SET_NO_MEMORY_INFO()
437441
SET_MEMORY_INFO_NAME(SimpleWriteWrap)
438442
SET_SELF_SIZE(SimpleWriteWrap)
443+
444+
bool IsNotIndicativeOfMemoryLeakAtExit() const override {
445+
return OtherBase::IsNotIndicativeOfMemoryLeakAtExit();
446+
}
439447
};
440448

441449
} // namespace node

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

Lines changed: 1 addition & 0 deletions
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)