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();