Fix TOCTOU race between _try_remove_self_if_above and destructor

The _stop check in _try_remove_self_if_above was outside the
_threads_mutex lock, creating a window where a worker thread could
read _stop==false, then the destructor could set _stop and snapshot
_threads (including that worker), then the worker could acquire the
lock, detach itself, and exit.  The destructor would then call
pthread_join on a detached thread — undefined behavior.

Move the _stop check inside the _threads_mutex lock so it is
serialized with the destructor's snapshot.  Now either the worker
holds the lock first (detaches and erases itself before the
snapshot) or the destructor sets _stop first (worker sees it and
bails out, remaining joinable in the snapshot).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Antonio S. Musumeci 2026-04-10 11:45:44 -04:00
parent 0466d7208f
commit b9552758fd

View file

@ -643,11 +643,21 @@ bool
ThreadPool::_try_remove_self_if_above(pthread_t t_,
std::size_t min_count_)
{
mutex_lockguard(_threads_mutex);
// Check _stop under the lock to close the TOCTOU race with the
// destructor. The destructor sets _stop, then acquires
// _threads_mutex to snapshot _threads. By checking _stop here
// under the same lock, either:
// (a) we hold the lock first, see _stop==false, detach and
// erase ourselves — the destructor's snapshot won't
// include us, so it won't join a detached thread; or
// (b) the destructor sets _stop first — we see _stop==true
// and bail out, remaining in _threads for the destructor
// to join.
if(_stop.load(std::memory_order_acquire))
return false;
mutex_lockguard(_threads_mutex);
if(_threads.size() <= min_count_)
return false;