Commit 32d23ec931c for nodejs
commit 32d23ec931cb45311dae92c15f53046f3be8b2eb
Author: Shelley Vohr <shelley.vohr@gmail.com>
Date: Sun Sep 20 18:35:38 2026 +0200
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: https://github.com/nodejs/node/pull/65353
Signed-off-by: Shelley Vohr <shelley.vohr@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/66066
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
Reviewed-By: René <contact.9a5d6388@renegade334.me.uk>
Reviewed-By: Chengzhong Wu <legendecas@gmail.com>
diff --git a/src/node_platform.cc b/src/node_platform.cc
index 32905067ff1..4bb8ac52b15 100644
--- a/src/node_platform.cc
+++ b/src/node_platform.cc
@@ -159,7 +159,9 @@ class WorkerThreadsTaskRunner::DelayedTaskScheduler {
// ScheduleTasks (start a timer that pops the task into the worker queue)
// in posting order, then, once Stop() was called, the StopTask.
- for (std::unique_ptr<Task>& task : scheduler->tasks_.Lock().PopAll()) {
+ std::vector<std::unique_ptr<Task>> tasks =
+ scheduler->tasks_.Lock().PopAll();
+ for (std::unique_ptr<Task>& task : tasks) {
task->Run();
}
}
@@ -605,8 +607,9 @@ void NodePlatform::DrainTasks(Isolate* isolate) {
bool PerIsolatePlatformData::FlushForegroundTasksInternal() {
bool did_work = false;
- for (std::unique_ptr<DelayedTask>& delayed :
- foreground_delayed_tasks_.Lock().PopAll()) {
+ std::vector<std::unique_ptr<DelayedTask>> delayed_tasks =
+ foreground_delayed_tasks_.Lock().PopAll();
+ for (std::unique_ptr<DelayedTask>& delayed : delayed_tasks) {
did_work = true;
uint64_t delay_millis = llround(delayed->timeout * 1000);
@@ -629,8 +632,9 @@ bool PerIsolatePlatformData::FlushForegroundTasksInternal() {
});
}
- for (std::unique_ptr<TaskQueueEntry>& entry :
- foreground_tasks_.Lock().PopAll()) {
+ std::vector<std::unique_ptr<TaskQueueEntry>> tasks =
+ foreground_tasks_.Lock().PopAll();
+ for (std::unique_ptr<TaskQueueEntry>& entry : tasks) {
did_work = true;
RunForegroundTask(std::move(entry->task));
}
diff --git a/src/node_platform.h b/src/node_platform.h
index bd6fe024d5e..52e3e629bc0 100644
--- a/src/node_platform.h
+++ b/src/node_platform.h
@@ -35,7 +35,9 @@ class TaskQueue {
void NotifyOfOutstandingCompletion();
void BlockingDrain();
void Stop();
- // All queued tasks, in the order Pop() would have returned them.
+ // All queued tasks, in the order Pop() would have returned them. Store the
+ // result before iterating it: used directly as a range-for initializer,
+ // `Lock().PopAll()` keeps the lock held for the whole loop from C++23 on.
std::vector<std::unique_ptr<T>> PopAll();
private: