Skip to content

Commit fe61478

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 7ae0855 commit fe61478

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
@@ -70,19 +70,18 @@ using v8_inspector::V8InspectorClient;
7070
#ifdef __POSIX__
7171
static uv_sem_t start_io_thread_semaphore;
7272
#endif // __POSIX__
73-
static uv_async_t start_io_thread_async;
74-
// This is just an additional check to make sure start_io_thread_async
75-
// is not accidentally re-used or used when uninitialized.
76-
static std::atomic_bool start_io_thread_async_initialized { false };
77-
// Protects the Agent* stored in start_io_thread_async.data.
78-
static Mutex start_io_thread_async_mutex;
79-
80-
// Called on the main thread.
81-
void StartIoThreadAsyncCallback(uv_async_t* handle) {
82-
static_cast<Agent*>(handle->data)->StartIoThread();
73+
// Agents that asked for the debug signal handler; SIGUSR1 (or the Windows
74+
// remote thread) starts the io thread of each. The mutex also guards the
75+
// once-per-process watchdog setup.
76+
static Mutex start_io_thread_agents_mutex;
77+
static std::vector<Agent*> start_io_thread_agents;
78+
static bool debug_signal_handler_started = false;
79+
80+
static void RequestIoThreadStartOnAgents() {
81+
Mutex::ScopedLock lock(start_io_thread_agents_mutex);
82+
for (Agent* agent : start_io_thread_agents) agent->RequestIoThreadStart();
8383
}
8484

85-
8685
#ifdef __POSIX__
8786
static void StartIoThreadWakeup(int signo, siginfo_t* info, void* ucontext) {
8887
uv_sem_post(&start_io_thread_semaphore);
@@ -92,16 +91,11 @@ inline void* StartIoThreadMain(void* unused) {
9291
uv_thread_setname("SignalInspector");
9392
for (;;) {
9493
uv_sem_wait(&start_io_thread_semaphore);
95-
Mutex::ScopedLock lock(start_io_thread_async_mutex);
96-
97-
CHECK(start_io_thread_async_initialized);
98-
Agent* agent = static_cast<Agent*>(start_io_thread_async.data);
99-
if (agent != nullptr)
100-
agent->RequestIoThreadStart();
94+
RequestIoThreadStartOnAgents();
10195
}
10296
}
10397

104-
static int StartDebugSignalHandler() {
98+
static int StartWatchdogThread() {
10599
// Start a watchdog thread for calling v8::Debug::DebugBreak() because
106100
// it's not safe to call directly from the signal handler, it can
107101
// deadlock with the thread it interrupts.
@@ -136,14 +130,28 @@ static int StartDebugSignalHandler() {
136130
fprintf(stderr, "node[%u]: pthread_create: %s\n",
137131
uv_os_getpid(), strerror(err));
138132
fflush(stderr);
139-
// Leave SIGUSR1 blocked. We don't install a signal handler,
140-
// receiving the signal would terminate the process.
133+
uv_sem_destroy(&start_io_thread_semaphore);
141134
return -err;
142135
}
143136
RegisterSignalHandler(SIGUSR1, StartIoThreadWakeup);
144137
// Restore original mask
145138
CHECK_EQ(0, pthread_sigmask(SIG_SETMASK, &sigmask, nullptr));
146-
// Unblock SIGUSR1. A pending SIGUSR1 signal will now be delivered.
139+
return 0;
140+
}
141+
142+
static int StartDebugSignalHandler() {
143+
{
144+
Mutex::ScopedLock lock(start_io_thread_agents_mutex);
145+
if (!debug_signal_handler_started) {
146+
// Leave SIGUSR1 blocked on failure. We don't install a signal handler,
147+
// receiving the signal would terminate the process.
148+
if (int err = StartWatchdogThread()) return err;
149+
debug_signal_handler_started = true;
150+
}
151+
}
152+
// Unblock SIGUSR1 on this thread; PlatformInit() left it blocked. A pending
153+
// SIGUSR1 signal will now be delivered.
154+
sigset_t sigmask;
147155
sigemptyset(&sigmask);
148156
sigaddset(&sigmask, SIGUSR1);
149157
CHECK_EQ(0, pthread_sigmask(SIG_UNBLOCK, &sigmask, nullptr));
@@ -154,11 +162,7 @@ static int StartDebugSignalHandler() {
154162

155163
#ifdef _WIN32
156164
DWORD WINAPI StartIoThreadProc(void* arg) {
157-
Mutex::ScopedLock lock(start_io_thread_async_mutex);
158-
CHECK(start_io_thread_async_initialized);
159-
Agent* agent = static_cast<Agent*>(start_io_thread_async.data);
160-
if (agent != nullptr)
161-
agent->RequestIoThreadStart();
165+
RequestIoThreadStartOnAgents();
162166
return 0;
163167
}
164168

@@ -168,6 +172,9 @@ static int GetDebugSignalHandlerMappingName(DWORD pid, wchar_t* buf,
168172
}
169173

170174
static int StartDebugSignalHandler() {
175+
Mutex::ScopedLock lock(start_io_thread_agents_mutex);
176+
if (debug_signal_handler_started) return 0;
177+
debug_signal_handler_started = true;
171178
wchar_t mapping_name[32];
172179
HANDLE mapping_handle;
173180
DWORD pid;
@@ -837,7 +844,21 @@ Agent::Agent(Environment* env)
837844
debug_options_(env->options()->debug_options()),
838845
host_port_(env->inspector_host_port()) {}
839846

840-
Agent::~Agent() = default;
847+
Agent::~Agent() {
848+
StopAcceptingIoThreadStarts();
849+
}
850+
851+
void Agent::StopAcceptingIoThreadStarts() {
852+
if (start_io_thread_async_ == nullptr) return;
853+
{
854+
Mutex::ScopedLock lock(start_io_thread_agents_mutex);
855+
std::erase(start_io_thread_agents, this);
856+
}
857+
parent_env_->RemoveCleanupHook(StopAcceptingIoThreadStartsHook, this);
858+
parent_env_->CloseHandle(start_io_thread_async_,
859+
[](uv_async_t* handle) { delete handle; });
860+
start_io_thread_async_ = nullptr;
861+
}
841862

842863
bool Agent::Start(const std::string& path,
843864
const DebugOptions& options,
@@ -849,33 +870,25 @@ bool Agent::Start(const std::string& path,
849870
host_port_ = host_port;
850871

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

881894
AtExit(parent_env_, [](void* env) {
@@ -1154,21 +1167,21 @@ void Agent::AllAsyncTasksCanceled() {
11541167
client_->AllAsyncTasksCanceled();
11551168
}
11561169

1170+
void Agent::StopAcceptingIoThreadStartsHook(void* agent) {
1171+
static_cast<Agent*>(agent)->StopAcceptingIoThreadStarts();
1172+
}
1173+
11571174
void Agent::RequestIoThreadStart() {
11581175
// We need to attempt to interrupt V8 flow (in case Node is running
11591176
// continuous JS code) and to wake up libuv thread (in case Node is waiting
11601177
// for IO events)
11611178
if (!options().allow_attaching_debugger) {
11621179
return;
11631180
}
1164-
CHECK(start_io_thread_async_initialized);
1165-
uv_async_send(&start_io_thread_async);
11661181
parent_env_->RequestInterrupt([this](Environment*) {
11671182
StartIoThread();
11681183
});
1169-
1170-
CHECK(start_io_thread_async_initialized);
1171-
uv_async_send(&start_io_thread_async);
1184+
uv_async_send(start_io_thread_async_);
11721185
}
11731186

11741187
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
@@ -339,9 +339,8 @@ TEST_F(EnvironmentTest, RemoveEnvironmentCleanupHookDuringCleanup) {
339339
TEST_F(EnvironmentTest, MultipleEnvironmentsPerIsolate) {
340340
const v8::HandleScope handle_scope(isolate_);
341341
const Argv argv;
342-
// Only one of the Environments can have default flags and own the inspector.
343342
Env env1 {handle_scope, argv};
344-
Env env2 {handle_scope, argv, node::EnvironmentFlags::kNoFlags};
343+
Env env2{handle_scope, argv};
345344

346345
AtExit(*env1, at_exit_callback1, nullptr);
347346
AtExit(*env2, at_exit_callback2, nullptr);

0 commit comments

Comments
 (0)