Skip to content

Commit 9511071

Browse files
codebytereaduh95
authored andcommitted
inspector: fix abort when two Environments own the inspector
Two Environments with default flags alive at the same time (for example the embedding.md example run on two threads, or two `CommonEnvironmentSetup`s) aborted the process: `Agent::Start()` bound one file-level static `uv_async_t` to the current Environment's loop for every Environment with `kOwnsInspector`, which `kDefaultFlags` implies, and CHECKed that nobody else had. Environments created one after another did not abort, but each ran `StartDebugSignalHandler()` again, which re-initialized the semaphore the watchdog waits on and spawned another detached watchdog thread, leaking one thread per Environment. Give every Agent that asks for the debug signal handler its own async handle, keep those Agents in a mutex-protected list that the watchdog (or the Windows remote thread) walks, and set the watchdog up once per process while still unblocking SIGUSR1 on each Environment's thread. The handle is heap-allocated, closed by the cleanup hook or `~Agent()`, whichever runs first, and freed by its close callback. A SIGUSR1 now reaches every Environment that asked for the handler, and no longer starts the inspector of one that passed `kNoStartDebugSignalHandler`. Refs: #25777 Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com> PR-URL: #65877 Refs: #44121 Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent cce2795 commit 9511071

3 files changed

Lines changed: 81 additions & 61 deletions

File tree

src/inspector_agent.cc

Lines changed: 71 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -72,19 +72,18 @@ using v8_inspector::V8InspectorClient;
7272
#ifdef __POSIX__
7373
static uv_sem_t start_io_thread_semaphore;
7474
#endif // __POSIX__
75-
static uv_async_t start_io_thread_async;
76-
// This is just an additional check to make sure start_io_thread_async
77-
// is not accidentally re-used or used when uninitialized.
78-
static std::atomic_bool start_io_thread_async_initialized { false };
79-
// Protects the Agent* stored in start_io_thread_async.data.
80-
static Mutex start_io_thread_async_mutex;
81-
82-
// Called on the main thread.
83-
void StartIoThreadAsyncCallback(uv_async_t* handle) {
84-
static_cast<Agent*>(handle->data)->StartIoThread();
75+
// Agents that asked for the debug signal handler; SIGUSR1 (or the Windows
76+
// remote thread) starts the io thread of each. The mutex also guards the
77+
// once-per-process watchdog setup.
78+
static Mutex start_io_thread_agents_mutex;
79+
static std::vector<Agent*> start_io_thread_agents;
80+
static bool debug_signal_handler_started = false;
81+
82+
static void RequestIoThreadStartOnAgents() {
83+
Mutex::ScopedLock lock(start_io_thread_agents_mutex);
84+
for (Agent* agent : start_io_thread_agents) agent->RequestIoThreadStart();
8585
}
8686

87-
8887
#ifdef __POSIX__
8988
static void StartIoThreadWakeup(int signo, siginfo_t* info, void* ucontext) {
9089
uv_sem_post(&start_io_thread_semaphore);
@@ -94,16 +93,11 @@ inline void* StartIoThreadMain(void* unused) {
9493
uv_thread_setname("SignalInspector");
9594
for (;;) {
9695
uv_sem_wait(&start_io_thread_semaphore);
97-
Mutex::ScopedLock lock(start_io_thread_async_mutex);
98-
99-
CHECK(start_io_thread_async_initialized);
100-
Agent* agent = static_cast<Agent*>(start_io_thread_async.data);
101-
if (agent != nullptr)
102-
agent->RequestIoThreadStart();
96+
RequestIoThreadStartOnAgents();
10397
}
10498
}
10599

106-
static int StartDebugSignalHandler() {
100+
static int StartWatchdogThread() {
107101
// Start a watchdog thread for calling v8::Debug::DebugBreak() because
108102
// it's not safe to call directly from the signal handler, it can
109103
// deadlock with the thread it interrupts.
@@ -138,14 +132,28 @@ static int StartDebugSignalHandler() {
138132
fprintf(stderr, "node[%u]: pthread_create: %s\n",
139133
uv_os_getpid(), strerror(err));
140134
fflush(stderr);
141-
// Leave SIGUSR1 blocked. We don't install a signal handler,
142-
// receiving the signal would terminate the process.
135+
uv_sem_destroy(&start_io_thread_semaphore);
143136
return -err;
144137
}
145138
RegisterSignalHandler(SIGUSR1, StartIoThreadWakeup);
146139
// Restore original mask
147140
CHECK_EQ(0, pthread_sigmask(SIG_SETMASK, &sigmask, nullptr));
148-
// Unblock SIGUSR1. A pending SIGUSR1 signal will now be delivered.
141+
return 0;
142+
}
143+
144+
static int StartDebugSignalHandler() {
145+
{
146+
Mutex::ScopedLock lock(start_io_thread_agents_mutex);
147+
if (!debug_signal_handler_started) {
148+
// Leave SIGUSR1 blocked on failure. We don't install a signal handler,
149+
// receiving the signal would terminate the process.
150+
if (int err = StartWatchdogThread()) return err;
151+
debug_signal_handler_started = true;
152+
}
153+
}
154+
// Unblock SIGUSR1 on this thread; PlatformInit() left it blocked. A pending
155+
// SIGUSR1 signal will now be delivered.
156+
sigset_t sigmask;
149157
sigemptyset(&sigmask);
150158
sigaddset(&sigmask, SIGUSR1);
151159
CHECK_EQ(0, pthread_sigmask(SIG_UNBLOCK, &sigmask, nullptr));
@@ -156,11 +164,7 @@ static int StartDebugSignalHandler() {
156164

157165
#ifdef _WIN32
158166
DWORD WINAPI StartIoThreadProc(void* arg) {
159-
Mutex::ScopedLock lock(start_io_thread_async_mutex);
160-
CHECK(start_io_thread_async_initialized);
161-
Agent* agent = static_cast<Agent*>(start_io_thread_async.data);
162-
if (agent != nullptr)
163-
agent->RequestIoThreadStart();
167+
RequestIoThreadStartOnAgents();
164168
return 0;
165169
}
166170

@@ -170,6 +174,9 @@ static int GetDebugSignalHandlerMappingName(DWORD pid, wchar_t* buf,
170174
}
171175

172176
static int StartDebugSignalHandler() {
177+
Mutex::ScopedLock lock(start_io_thread_agents_mutex);
178+
if (debug_signal_handler_started) return 0;
179+
debug_signal_handler_started = true;
173180
wchar_t mapping_name[32];
174181
HANDLE mapping_handle;
175182
DWORD pid;
@@ -845,7 +852,21 @@ Agent::Agent(Environment* env)
845852
debug_options_(env->options()->debug_options()),
846853
host_port_(env->inspector_host_port()) {}
847854

848-
Agent::~Agent() = default;
855+
Agent::~Agent() {
856+
StopAcceptingIoThreadStarts();
857+
}
858+
859+
void Agent::StopAcceptingIoThreadStarts() {
860+
if (start_io_thread_async_ == nullptr) return;
861+
{
862+
Mutex::ScopedLock lock(start_io_thread_agents_mutex);
863+
std::erase(start_io_thread_agents, this);
864+
}
865+
parent_env_->RemoveCleanupHook(StopAcceptingIoThreadStartsHook, this);
866+
parent_env_->CloseHandle(start_io_thread_async_,
867+
[](uv_async_t* handle) { delete handle; });
868+
start_io_thread_async_ = nullptr;
869+
}
849870

850871
bool Agent::Start(const std::string& path,
851872
const DebugOptions& options,
@@ -857,33 +878,25 @@ bool Agent::Start(const std::string& path,
857878
host_port_ = host_port;
858879

859880
client_ = std::make_shared<NodeInspectorClient>(parent_env_, is_main);
860-
if (parent_env_->owns_inspector()) {
861-
Mutex::ScopedLock lock(start_io_thread_async_mutex);
862-
CHECK_EQ(start_io_thread_async_initialized.exchange(true), false);
863-
CHECK_EQ(0, uv_async_init(parent_env_->event_loop(),
864-
&start_io_thread_async,
865-
StartIoThreadAsyncCallback));
866-
uv_unref(reinterpret_cast<uv_handle_t*>(&start_io_thread_async));
867-
start_io_thread_async.data = this;
868-
if (parent_env_->should_start_debug_signal_handler()) {
869-
// Ignore failure, SIGUSR1 won't work, but that should not block node
870-
// start.
871-
StartDebugSignalHandler();
881+
if (parent_env_->owns_inspector() &&
882+
parent_env_->should_start_debug_signal_handler()) {
883+
start_io_thread_async_ = new uv_async_t;
884+
start_io_thread_async_->data = this;
885+
CHECK_EQ(0,
886+
uv_async_init(parent_env_->event_loop(),
887+
start_io_thread_async_,
888+
[](uv_async_t* handle) {
889+
static_cast<Agent*>(handle->data)->StartIoThread();
890+
}));
891+
uv_unref(reinterpret_cast<uv_handle_t*>(start_io_thread_async_));
892+
{
893+
Mutex::ScopedLock lock(start_io_thread_agents_mutex);
894+
start_io_thread_agents.push_back(this);
872895
}
873-
874-
parent_env_->AddCleanupHook([](void* data) {
875-
Environment* env = static_cast<Environment*>(data);
876-
877-
{
878-
Mutex::ScopedLock lock(start_io_thread_async_mutex);
879-
start_io_thread_async.data = nullptr;
880-
}
881-
882-
// This is global, will never get freed
883-
env->CloseHandle(&start_io_thread_async, [](uv_async_t*) {
884-
CHECK(start_io_thread_async_initialized.exchange(false));
885-
});
886-
}, parent_env_);
896+
parent_env_->AddCleanupHook(StopAcceptingIoThreadStartsHook, this);
897+
// Ignore failure, SIGUSR1 won't work, but that should not block node
898+
// start.
899+
StartDebugSignalHandler();
887900
}
888901

889902
AtExit(parent_env_, [](void* env) {
@@ -1162,21 +1175,21 @@ void Agent::AllAsyncTasksCanceled() {
11621175
client_->AllAsyncTasksCanceled();
11631176
}
11641177

1178+
void Agent::StopAcceptingIoThreadStartsHook(void* agent) {
1179+
static_cast<Agent*>(agent)->StopAcceptingIoThreadStarts();
1180+
}
1181+
11651182
void Agent::RequestIoThreadStart() {
11661183
// We need to attempt to interrupt V8 flow (in case Node is running
11671184
// continuous JS code) and to wake up libuv thread (in case Node is waiting
11681185
// for IO events)
11691186
if (!options().allow_attaching_debugger) {
11701187
return;
11711188
}
1172-
CHECK(start_io_thread_async_initialized);
1173-
uv_async_send(&start_io_thread_async);
11741189
parent_env_->RequestInterrupt([this](Environment*) {
11751190
StartIoThread();
11761191
});
1177-
1178-
CHECK(start_io_thread_async_initialized);
1179-
uv_async_send(&start_io_thread_async);
1192+
uv_async_send(start_io_thread_async_);
11801193
}
11811194

11821195
void Agent::ContextCreated(Local<Context> context, const ContextInfo& info) {

src/inspector_agent.h

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#endif
99

1010
#include "node_options.h"
11+
#include "uv.h"
1112
#include "v8.h"
1213

1314
#include <cstddef>
@@ -117,7 +118,8 @@ class Agent {
117118
// Can only be called from the main thread.
118119
bool StartIoThread();
119120

120-
// Calls StartIoThread() from off the main thread.
121+
// Calls StartIoThread() from off the main thread. Only valid while the
122+
// Environment owns the inspector and has not started cleanup.
121123
void RequestIoThreadStart();
122124

123125
const DebugOptions& options() { return debug_options_; }
@@ -156,6 +158,12 @@ class Agent {
156158
bool async_hook_enabled_ = false;
157159
bool syncing_async_hook_state_ = false;
158160

161+
// Woken by the SIGUSR1 watchdog; closed by the cleanup hook or ~Agent(),
162+
// whichever runs first, and freed by its close callback.
163+
uv_async_t* start_io_thread_async_ = nullptr;
164+
void StopAcceptingIoThreadStarts();
165+
static void StopAcceptingIoThreadStartsHook(void* agent);
166+
159167
bool network_tracking_enabled_ = false;
160168
bool pending_enable_network_tracking = false;
161169
bool pending_disable_network_tracking = false;

test/cctest/test_environment.cc

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -343,9 +343,8 @@ TEST_F(EnvironmentTest, RemoveEnvironmentCleanupHookDuringCleanup) {
343343
TEST_F(EnvironmentTest, MultipleEnvironmentsPerIsolate) {
344344
const v8::HandleScope handle_scope(isolate_);
345345
const Argv argv;
346-
// Only one of the Environments can have default flags and own the inspector.
347346
Env env1 {handle_scope, argv};
348-
Env env2 {handle_scope, argv, node::EnvironmentFlags::kNoFlags};
347+
Env env2{handle_scope, argv};
349348

350349
AtExit(*env1, at_exit_callback1, nullptr);
351350
AtExit(*env2, at_exit_callback2, nullptr);

0 commit comments

Comments
 (0)