From d4d4f45b5006d9a9c59417d782b2593b093944fb Mon Sep 17 00:00:00 2001 From: chen3feng Date: Fri, 3 Jul 2026 08:12:23 +0800 Subject: [PATCH 1/2] base: never-destroy tls_queues to fix an exit-time mutex crash A static `MonitoredTimer` (and similar globals) calls DeleteThreadOutOfDutyCallback in its destructor during static destruction, which locks the `tls_queues` ThreadLocal's internal mutex. `tls_queues` was a plain namespace static, so on macOS it could already be destroyed by then, and std::mutex::lock() throws std::system_error EINVAL ("mutex lock failed") -> std::terminate at process exit. monitoring_test reproduced this reliably (all cases pass, then SIGABRT on shutdown). Wrap it in NeverDestroyed, exactly as GetGlobalQueue() already does in the same file, so the lock stays valid for the whole process lifetime. Leaking it is harmless (the OS reclaims it at exit). Co-Authored-By: Claude Opus 4.8 --- flare/base/thread/out_of_duty_callback.cc | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/flare/base/thread/out_of_duty_callback.cc b/flare/base/thread/out_of_duty_callback.cc index d2c6b64..f78f52d 100644 --- a/flare/base/thread/out_of_duty_callback.cc +++ b/flare/base/thread/out_of_duty_callback.cc @@ -106,7 +106,13 @@ struct GlobalQueue { std::atomic next_callback_id = 1; -ThreadLocal tls_queues; +// Leaked on purpose (like `GetGlobalQueue` below): a static `MonitoredTimer` +// (and other globals) can call `DeleteThreadOutOfDutyCallback` during static +// destruction, which locks this ThreadLocal's internal mutex. If `tls_queues` +// were a plain static it might already be destroyed by then -> `std::mutex:: +// lock()` throws EINVAL ("mutex lock failed") at exit. Never destroying it keeps +// that lock valid for the whole process lifetime. +NeverDestroyed> tls_queues; GlobalQueue* GetGlobalQueue() { static NeverDestroyed queue; @@ -157,7 +163,7 @@ void DeleteThreadOutOfDutyCallback(std::uint64_t handle) { } // And then sweep thread-locally cached queues. - tls_queues.ForEach([&](ThreadLocalQueue* queue) { + tls_queues->ForEach([&](ThreadLocalQueue* queue) { std::scoped_lock _(*queue->lock.really_slow_side()); queue->callbacks.EraseIf([&](auto&& e) { return e.id == handle; }); }); @@ -169,7 +175,7 @@ void DeleteThreadOutOfDutyCallback(std::uint64_t handle) { void NotifyThreadOutOfDutyCallbacks() { auto now = ReadCoarseSteadyClock(); - auto&& tls_queue = tls_queues.Get(); + auto&& tls_queue = tls_queues->Get(); auto&& global_queue = GetGlobalQueue(); std::scoped_lock _(*tls_queue->lock.blessed_side()); From 3fc70f0ab87683cb89ebbdcd36dfa2478ac5935c Mon Sep 17 00:00:00 2001 From: chen3feng Date: Fri, 3 Jul 2026 08:18:13 +0800 Subject: [PATCH 2/2] base: clang-format the tls_queues comment Co-Authored-By: Claude Opus 4.8 --- flare/base/thread/out_of_duty_callback.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flare/base/thread/out_of_duty_callback.cc b/flare/base/thread/out_of_duty_callback.cc index f78f52d..d97bfcf 100644 --- a/flare/base/thread/out_of_duty_callback.cc +++ b/flare/base/thread/out_of_duty_callback.cc @@ -110,8 +110,8 @@ std::atomic next_callback_id = 1; // (and other globals) can call `DeleteThreadOutOfDutyCallback` during static // destruction, which locks this ThreadLocal's internal mutex. If `tls_queues` // were a plain static it might already be destroyed by then -> `std::mutex:: -// lock()` throws EINVAL ("mutex lock failed") at exit. Never destroying it keeps -// that lock valid for the whole process lifetime. +// lock()` throws EINVAL ("mutex lock failed") at exit. Never destroying it +// keeps that lock valid for the whole process lifetime. NeverDestroyed> tls_queues; GlobalQueue* GetGlobalQueue() {