From 8e9431edd9f3d3a6fbf353c99f18bc9fde7ce0d5 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Wed, 16 Sep 2026 12:06:37 +0000 Subject: [PATCH] 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 --- src/node_platform.cc | 14 +++++++++----- src/node_platform.h | 4 +++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/node_platform.cc b/src/node_platform.cc index 2deccdcdf686..5f320bd7924e 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 : scheduler->tasks_.Lock().PopAll()) { + std::vector> tasks = + scheduler->tasks_.Lock().PopAll(); + for (std::unique_ptr& task : tasks) { task->Run(); } } @@ -605,8 +607,9 @@ void NodePlatform::DrainTasks(Isolate* isolate) { bool PerIsolatePlatformData::FlushForegroundTasksInternal() { bool did_work = false; - for (std::unique_ptr& delayed : - foreground_delayed_tasks_.Lock().PopAll()) { + std::vector> delayed_tasks = + foreground_delayed_tasks_.Lock().PopAll(); + for (std::unique_ptr& 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& entry : - foreground_tasks_.Lock().PopAll()) { + std::vector> tasks = + foreground_tasks_.Lock().PopAll(); + for (std::unique_ptr& entry : tasks) { did_work = true; RunForegroundTask(std::move(entry->task)); } diff --git a/src/node_platform.h b/src/node_platform.h index bd6fe024d5e8..52e3e629bc00 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> PopAll(); private: