Commit f4e89e70dd3 for nodejs
commit f4e89e70dd3622570b70562a7a2132e9b32d50c4
Author: fatih <fatihguzeldev@gmail.com>
Date: Thu Nov 12 22:15:20 2026 +0300
inspector: disconnect handles before releasing interfaces
Disconnect the thread handle while NodeInspectorClient still owns the
interface. This prevents cross-thread posts from calling
shared_from_this() after the interface's last strong reference has been
released.
Add a regression test verifying handle expiration during owner teardown.
Fixes: https://github.com/nodejs/node/issues/67079
Signed-off-by: Fatih Güzel <fatihguzeldev@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/67281
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
diff --git a/node.gyp b/node.gyp
index 9c0b270ea5f..a462a9e9f2e 100644
--- a/node.gyp
+++ b/node.gyp
@@ -492,6 +492,7 @@
'test/cctest/test_quic_tokens.cc',
],
'node_cctest_inspector_sources': [
+ 'test/cctest/inspector/test_main_thread_interface.cc',
'test/cctest/inspector/test_network_requests_buffer.cc',
'test/cctest/inspector/test_node_protocol.cc',
'test/cctest/test_inspector_socket.cc',
diff --git a/src/inspector/main_thread_interface.cc b/src/inspector/main_thread_interface.cc
index 0fca7483e95..d4822598e17 100644
--- a/src/inspector/main_thread_interface.cc
+++ b/src/inspector/main_thread_interface.cc
@@ -200,6 +200,10 @@ class ThreadSafeDelegate : public InspectorSessionDelegate {
MainThreadInterface::MainThreadInterface(Agent* agent) : agent_(agent) {}
MainThreadInterface::~MainThreadInterface() {
+ Disconnect();
+}
+
+void MainThreadInterface::Disconnect() {
if (handle_)
handle_->Reset();
}
diff --git a/src/inspector/main_thread_interface.h b/src/inspector/main_thread_interface.h
index 3311f9df014..99fee99596c 100644
--- a/src/inspector/main_thread_interface.h
+++ b/src/inspector/main_thread_interface.h
@@ -80,6 +80,7 @@ class MainThreadInterface :
explicit MainThreadInterface(Agent* agent);
~MainThreadInterface();
+ void Disconnect();
void DispatchMessages();
void Post(std::unique_ptr<Request> request);
bool WaitForFrontendEvent();
diff --git a/src/inspector_agent.cc b/src/inspector_agent.cc
index cfa2f2adb9d..408c754e3e9 100644
--- a/src/inspector_agent.cc
+++ b/src/inspector_agent.cc
@@ -535,6 +535,11 @@ class NodeInspectorClient : public V8InspectorClient {
}
}
+ ~NodeInspectorClient() override {
+ // Close the handle before the interface can lose its last strong reference.
+ if (interface_) interface_->Disconnect();
+ }
+
void runMessageLoopOnPause(int context_group_id) override {
waiting_for_resume_ = true;
runMessageLoop();
diff --git a/test/cctest/inspector/test_main_thread_interface.cc b/test/cctest/inspector/test_main_thread_interface.cc
new file mode 100644
index 00000000000..edb8dd57f97
--- /dev/null
+++ b/test/cctest/inspector/test_main_thread_interface.cc
@@ -0,0 +1,54 @@
+#include "gtest/gtest.h"
+#include "inspector/main_thread_interface.h"
+#include "inspector/worker_inspector.h"
+#include "node_test_fixture.h"
+
+#include <memory>
+
+namespace node {
+namespace inspector {
+namespace {
+
+class MainThreadInterfaceTest : public EnvironmentTestFixture {};
+
+class RetainInterfaceRequest : public Request {
+ public:
+ explicit RetainInterfaceRequest(
+ std::shared_ptr<MainThreadInterface>* retained_interface)
+ : retained_interface_(retained_interface) {}
+
+ void Call(MainThreadInterface* thread) override {
+ *retained_interface_ = thread->shared_from_this();
+ }
+
+ private:
+ std::shared_ptr<MainThreadInterface>* retained_interface_;
+};
+
+TEST_F(MainThreadInterfaceTest, ClosesHandleBeforeReleasingInterface) {
+ const v8::HandleScope handle_scope(isolate_);
+ const Argv argv;
+ std::shared_ptr<MainThreadInterface> retained_interface;
+ std::shared_ptr<MainThreadHandle> handle;
+ {
+ Env env{handle_scope, argv};
+ handle = (*env)->inspector_agent()->GetWorkerManager()->MainThread();
+ ASSERT_FALSE(handle->Expired());
+ ASSERT_TRUE(handle->Post(
+ std::make_unique<RetainInterfaceRequest>(&retained_interface)));
+ (*env)->RunAndClearInterrupts();
+ ASSERT_NE(retained_interface, nullptr);
+
+ // Keep the interface alive after its inspector owner is destroyed, so
+ // closing the handle cannot depend on the interface's destructor.
+ }
+
+ ASSERT_TRUE(handle->Expired());
+ EXPECT_FALSE(handle->Post(
+ std::make_unique<RetainInterfaceRequest>(&retained_interface)));
+ retained_interface.reset();
+}
+
+} // namespace
+} // namespace inspector
+} // namespace node