From 81e8f6281a4b43e4dc17ba9d837868dcdc745c9d Mon Sep 17 00:00:00 2001 From: "Antonio S. Musumeci" Date: Fri, 10 Apr 2026 22:24:14 -0400 Subject: [PATCH] fix data race on _threads between constructor and autoscale workers The constructor's spawn loop pushed to _threads without holding _threads_mutex. A just-spawned autoscale worker could time out on the empty queue (especially with a small idle_threshold_usecs) and call _try_remove_self_if_above, which acquires _threads_mutex and modifies _threads (find + erase). Concurrent unlocked push_back and locked erase on the same vector is undefined behavior per the C++ memory model, even when reserve() prevents reallocation. The same race applied to the _threads.empty() and _threads.size() reads after the spawn loop. Fix: lock _threads_mutex around push_back in the spawn loop and around the post-loop size check. Co-Authored-By: Claude Opus 4.6 (1M context) --- vendored/libfuse/include/thread_pool.hpp | 39 ++++++++++++++++++------ 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/vendored/libfuse/include/thread_pool.hpp b/vendored/libfuse/include/thread_pool.hpp index a6616336..94db7780 100644 --- a/vendored/libfuse/include/thread_pool.hpp +++ b/vendored/libfuse/include/thread_pool.hpp @@ -290,23 +290,42 @@ ThreadPool::ThreadPool(const unsigned thread_count_, } _set_thread_name(t,_name); - _threads.push_back(t); + + // Lock around push_back because a just-spawned autoscale + // worker can time out on the empty queue and call + // _try_remove_self_if_above (which acquires _threads_mutex + // and modifies _threads) before the constructor finishes. + // Concurrent unlocked push_back and locked erase is UB. + { + mutex_lockguard(_threads_mutex); + _threads.push_back(t); + } } pthread_sigmask(SIG_SETMASK,&oldset,NULL); - if(_threads.empty()) + // Check under lock: a spawned autoscale worker could have + // already modified _threads via _try_remove_self_if_above. + { + std::size_t n; { - pthread_cond_destroy(&_monitor_cond); - pthread_mutex_destroy(&_monitor_mutex); - throw std::runtime_error("threadpool: failed to spawn any threads"); + mutex_lockguard(_threads_mutex); + n = _threads.size(); } - syslog(LOG_DEBUG, - "threadpool (%s): spawned %zu threads w/ max queue depth %u", - _name.c_str(), - _threads.size(), - max_queue_depth_); + if(n == 0) + { + pthread_cond_destroy(&_monitor_cond); + pthread_mutex_destroy(&_monitor_mutex); + throw std::runtime_error("threadpool: failed to spawn any threads"); + } + + syslog(LOG_DEBUG, + "threadpool (%s): spawned %zu threads w/ max queue depth %u", + _name.c_str(), + n, + max_queue_depth_); + } // Start the monitor only when dynamic scaling can actually change // the thread count. Fixed-size pools still use _scaling_config for