From 7d5082cc0e6e9b8e5e6c3d7daf18e7b7331569d6 Mon Sep 17 00:00:00 2001 From: yhirose Date: Sat, 9 May 2026 21:13:40 -0400 Subject: [PATCH] Make ThreadPool ctor exception-safe on partial thread creation (#2445) * Make ThreadPool ctor exception-safe on partial thread creation If std::thread construction throws partway through the ThreadPool constructor (e.g., pthread_create returns EAGAIN under thread-resource pressure), the partially-built threads_ vector would destruct joinable std::thread objects, calling std::terminate(). Wrap the spawn loop and, on failure, signal shutdown to the workers already created, join them, and rethrow. Adds a reproducer test in test_thread_pool.cc that interposes pthread_create at link time to deterministically fail the second call, gated to POSIX + exceptions-enabled builds. Fix #2444 * Strip ASAN from test_thread_pool to coexist with pthread_create override Linux libasan installs its own pthread_create interceptor; our in-binary symbol override sits on top of it and corrupts ASAN's thread bookkeeping, which surfaces as "Joining already joined thread" on the very first test. Disable ASAN for this small unit-test binary -- ThreadPool memory behavior is still exercised under ASAN by the main `test` binary. --- httplib.h | 24 +++++++++++++++-- test/Makefile | 19 +++++++++++++- test/test_thread_pool.cc | 57 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 3 deletions(-) diff --git a/httplib.h b/httplib.h index 7740b9d..521b4f8 100644 --- a/httplib.h +++ b/httplib.h @@ -10047,9 +10047,29 @@ inline ThreadPool::ThreadPool(size_t n, size_t max_n, size_t mqr) #endif max_thread_count_ = max_n == 0 ? n : max_n; threads_.reserve(base_thread_count_); - for (size_t i = 0; i < base_thread_count_; i++) { - threads_.emplace_back(std::thread([this]() { worker(false); })); +#ifndef CPPHTTPLIB_NO_EXCEPTIONS + try { +#endif + for (size_t i = 0; i < base_thread_count_; i++) { + threads_.emplace_back(std::thread([this]() { worker(false); })); + } +#ifndef CPPHTTPLIB_NO_EXCEPTIONS + } catch (...) { + // If thread creation fails partway (e.g., pthread_create returns EAGAIN), + // signal the workers we already spawned to exit and join them so the + // vector destructor does not see joinable threads (which would call + // std::terminate). Then rethrow so the caller learns of the failure. + { + std::unique_lock lock(mutex_); + shutdown_ = true; + } + cond_.notify_all(); + for (auto &t : threads_) { + if (t.joinable()) { t.join(); } + } + throw; } +#endif } inline bool ThreadPool::enqueue(std::function fn) { diff --git a/test/Makefile b/test/Makefile index f449f20..9441e36 100644 --- a/test/Makefile +++ b/test/Makefile @@ -202,8 +202,25 @@ test_split_no_tls : test.cc ../httplib.h httplib.cc Makefile $(CXX) -o $@ $(CXXFLAGS) test.cc httplib.cc $(TEST_ARGS_NO_TLS) # ThreadPool unit tests (no TLS, no compression needed) +# +# The constructor-exception-safety reproducer test interposes pthread_create +# at link time. The link flags below enable that interposition. ASAN is also +# stripped from this target because libasan installs its own pthread_create +# interceptor; layering our override on top corrupts ASAN's thread bookkeeping +# and trips "Joining already joined thread" on Linux. ThreadPool memory +# behavior is still covered by the ASAN-instrumented `test` binary. +ifneq ($(OS), Windows_NT) +ifeq ($(shell uname -s), Darwin) +THREAD_POOL_INTERPOSE_LDFLAGS := -Wl,-flat_namespace +else +THREAD_POOL_INTERPOSE_LDFLAGS := -Wl,--export-dynamic +endif +endif + +THREAD_POOL_CXXFLAGS := $(filter-out -fsanitize=address,$(CXXFLAGS)) + test_thread_pool : test_thread_pool.cc ../httplib.h Makefile - $(CXX) -o $@ -I.. $(CXXFLAGS) test_thread_pool.cc gtest/src/gtest-all.cc gtest/src/gtest_main.cc -Igtest -Igtest/include -lpthread + $(CXX) -o $@ -I.. $(THREAD_POOL_CXXFLAGS) test_thread_pool.cc gtest/src/gtest-all.cc gtest/src/gtest_main.cc -Igtest -Igtest/include -lpthread $(THREAD_POOL_INTERPOSE_LDFLAGS) check_abi: @./check-shared-library-abi-compatibility.sh diff --git a/test/test_thread_pool.cc b/test/test_thread_pool.cc index 1f08c7f..8ef178d 100644 --- a/test/test_thread_pool.cc +++ b/test/test_thread_pool.cc @@ -198,6 +198,63 @@ TEST(ThreadPoolTest, InvalidMaxThreadsThrows) { } #endif +// Issue #2444: ThreadPool constructor must be exception-safe when std::thread +// construction fails partway (e.g., pthread_create returns EAGAIN under thread +// resource pressure). Without proper handling, the partially-built threads_ +// vector destroys joinable std::thread objects, calling std::terminate(). +// +// We reproduce the failure portably by interposing pthread_create at link +// time: while the counter is armed, the first N calls succeed, the rest +// return EAGAIN. This is gated to POSIX + exceptions-enabled builds. +#ifndef CPPHTTPLIB_NO_EXCEPTIONS +#if defined(__unix__) || defined(__APPLE__) + +#include +#include +#include + +namespace { +// -1 = pass-through (default). >= 0 = number of remaining successful calls +// before EAGAIN is returned. Reset to -1 after each test that arms it. +std::atomic g_pthread_create_remaining{-1}; +} // namespace + +extern "C" int pthread_create(pthread_t *thread, const pthread_attr_t *attr, + void *(*start_routine)(void *), void *arg) { + using fn_t = + int (*)(pthread_t *, const pthread_attr_t *, void *(*)(void *), void *); + static fn_t real = reinterpret_cast(dlsym(RTLD_NEXT, "pthread_create")); + + int n = g_pthread_create_remaining.load(std::memory_order_relaxed); + if (n == 0) { return EAGAIN; } + if (n > 0) { + g_pthread_create_remaining.fetch_sub(1, std::memory_order_relaxed); + } + return real(thread, attr, start_routine, arg); +} + +TEST(ThreadPoolTest, ConstructorRecoversWhenThreadCreationFails) { + // Allow only the first thread to spawn; subsequent pthread_create calls + // return EAGAIN, causing std::thread() to throw std::system_error mid-loop. + g_pthread_create_remaining.store(1); + + bool caught = false; + try { + ThreadPool pool(/*n=*/4); + (void)pool; + } catch (const std::system_error &) { caught = true; } catch (...) { + caught = true; + } + + // Disarm before any further test runs. + g_pthread_create_remaining.store(-1); + + EXPECT_TRUE(caught); +} + +#endif // POSIX +#endif // CPPHTTPLIB_NO_EXCEPTIONS + TEST(ThreadPoolTest, EnqueueAfterShutdownReturnsFalse) { ThreadPool pool(2); pool.shutdown();