Skip to content

Commit bb29cbb

Browse files
committed
src: fix task queue deadlock when built as C++23
Since #65353, FlushForegroundTasksInternal() and the DelayedTaskScheduler drain their queues with `for (auto& task : queue.Lock().PopAll())`. In C++20 the Locked temporary is destroyed at the end of the range initializer, before the loop body runs. C++23 (P2718R0) extends the lifetime of every temporary in a range-for initializer to the end of the loop, so the queue's mutex stays held while the tasks run, and the first task that posts to the same queue - any V8 foreground task that schedules another one - deadlocks on the non-recursive mutex. Node.js itself builds with -std=gnu++20, but an embedder that compiles it as C++23 hangs in the first foreground task flush. Store the drained tasks in a local before iterating so the lock is released independent of the language version. Refs: #65353 Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
1 parent 67e66b8 commit bb29cbb

2 files changed

Lines changed: 12 additions & 6 deletions

File tree

src/node_platform.cc

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,9 @@ class WorkerThreadsTaskRunner::DelayedTaskScheduler {
159159

160160
// ScheduleTasks (start a timer that pops the task into the worker queue)
161161
// in posting order, then, once Stop() was called, the StopTask.
162-
for (std::unique_ptr<Task>& task : scheduler->tasks_.Lock().PopAll()) {
162+
std::vector<std::unique_ptr<Task>> tasks =
163+
scheduler->tasks_.Lock().PopAll();
164+
for (std::unique_ptr<Task>& task : tasks) {
163165
task->Run();
164166
}
165167
}
@@ -605,8 +607,9 @@ void NodePlatform::DrainTasks(Isolate* isolate) {
605607
bool PerIsolatePlatformData::FlushForegroundTasksInternal() {
606608
bool did_work = false;
607609

608-
for (std::unique_ptr<DelayedTask>& delayed :
609-
foreground_delayed_tasks_.Lock().PopAll()) {
610+
std::vector<std::unique_ptr<DelayedTask>> delayed_tasks =
611+
foreground_delayed_tasks_.Lock().PopAll();
612+
for (std::unique_ptr<DelayedTask>& delayed : delayed_tasks) {
610613
did_work = true;
611614
uint64_t delay_millis = llround(delayed->timeout * 1000);
612615

@@ -629,8 +632,9 @@ bool PerIsolatePlatformData::FlushForegroundTasksInternal() {
629632
});
630633
}
631634

632-
for (std::unique_ptr<TaskQueueEntry>& entry :
633-
foreground_tasks_.Lock().PopAll()) {
635+
std::vector<std::unique_ptr<TaskQueueEntry>> tasks =
636+
foreground_tasks_.Lock().PopAll();
637+
for (std::unique_ptr<TaskQueueEntry>& entry : tasks) {
634638
did_work = true;
635639
RunForegroundTask(std::move(entry->task));
636640
}

src/node_platform.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ class TaskQueue {
3535
void NotifyOfOutstandingCompletion();
3636
void BlockingDrain();
3737
void Stop();
38-
// All queued tasks, in the order Pop() would have returned them.
38+
// All queued tasks, in the order Pop() would have returned them. Store the
39+
// result before iterating it: used directly as a range-for initializer,
40+
// `Lock().PopAll()` keeps the lock held for the whole loop from C++23 on.
3941
std::vector<std::unique_ptr<T>> PopAll();
4042

4143
private:

0 commit comments

Comments
 (0)