What changed, and why it matters
This commit adds a new automated test (a fuzz test) for Bitcoin Core's internal thread pool. It does not change any production code, only test code. There is no security vulnerability here.
No security action needed. This is a normal test-infrastructure commit.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit introduces src/test/fuzz/threadpool.cpp and registers it in the fuzz CMake build. The fuzz target exercises ThreadPool::Submit with throwing and non-throwing tasks, verifies counters, and ensures queue draining works. It is purely a test addition with no modifications to src/util/threadpool.h or production logic.
Changed components
src/test/fuzz/threadpool.cppsrc/test/fuzz/CMakeLists.txtInspect captured patch +118 / −0
diff --git a/src/test/fuzz/CMakeLists.txt b/src/test/fuzz/CMakeLists.txt
index 3d2ff8f3..fc82fdc0 100644
--- a/src/test/fuzz/CMakeLists.txt
+++ b/src/test/fuzz/CMakeLists.txt
@@ -120,6 +120,7 @@ add_executable(fuzz
string.cpp
strprintf.cpp
system.cpp
+ threadpool.cpp
timeoffsets.cpp
torcontrol.cpp
transaction.cpp
diff --git a/src/test/fuzz/threadpool.cpp b/src/test/fuzz/threadpool.cpp
new file mode 100644
index 00000000..293aa63e
--- /dev/null
+++ b/src/test/fuzz/threadpool.cpp
@@ -0,0 +1,117 @@
+// Copyright (c) The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+#include <logging.h>
+#include <util/threadpool.h>
+
+#include <test/fuzz/FuzzedDataProvider.h>
+#include <test/fuzz/fuzz.h>
+
+#include <atomic>
+#include <future>
+#include <queue>
+
+struct ExpectedException : std::runtime_error {
+ explicit ExpectedException(const std::string& msg) : std::runtime_error(msg) {}
+};
+
+struct ThrowTask {
+ void operator()() const { throw ExpectedException("fail"); }
+};
+
+struct CounterTask {
+ std::atomic_uint32_t& m_counter;
+ explicit CounterTask(std::atomic_uint32_t& counter) : m_counter{counter} {}
+ void operator()() const { m_counter.fetch_add(1, std::memory_order_relaxed); }
+};
+
+// Waits for a future to complete. Increments 'fail_counter' if the expected exception is thrown.
+static void GetFuture(std::future<void>& future, uint32_t& fail_counter)
+{
+ try {
+ future.get();
+ } catch (const ExpectedException&) {
+ fail_counter++;
+ } catch (...) {
+ assert(false && "Unexpected exception type");
+ }
+}
+
+// Global thread pool for fuzzing. Persisting it across iterations prevents
+// the excessive thread creation/destruction overhead that can lead to
+// instability in the fuzzing environment.
+// This is also how we use it in the app's lifecycle.
+ThreadPool g_pool{"fuzz"};
+Mutex g_pool_mutex;
+// Global to verify we always have the same number of threads.
+size_t g_num_workers = 3;
+
+static void StartPoolIfNeeded() EXCLUSIVE_LOCKS_REQUIRED(!g_pool_mutex)
+{
+ LOCK(g_pool_mutex);
+ if (g_pool.WorkersCount() == g_num_workers) return;
+ g_pool.Start(g_num_workers);
+}
+
+static void setup_threadpool_test()
+{
+ // Disable logging entirely. It seems to cause memory leaks.
+ LogInstance().DisableLogging();
+}
+
+FUZZ_TARGET(threadpool, .init = setup_threadpool_test) EXCLUSIVE_LOCKS_REQUIRED(!g_pool_mutex)
+{
+ // Because LibAFL calls fork() after calling the init setup function,
+ // the child processes end up having one thread active and no workers.
+ // To work around this limitation, start thread pool inside the first runner.
+ StartPoolIfNeeded();
+
+ FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size());
+
+ const uint32_t num_tasks = fuzzed_data_provider.ConsumeIntegralInRange<uint32_t>(0, 1024);
+ assert(g_pool.WorkersCount() == g_num_workers);
+ assert(g_pool.WorkQueueSize() == 0);
+
+ // Counters
+ std::atomic_uint32_t task_counter{0};
+ uint32_t fail_counter{0};
+ uint32_t expected_task_counter{0};
+ uint32_t expected_fail_tasks{0};
+
+ std::queue<std::future<void>> futures;
+ for (uint32_t i = 0; i < num_tasks; ++i) {
+ const bool will_throw = fuzzed_data_provider.ConsumeBool();
+ const bool wait_immediately = fuzzed_data_provider.ConsumeBool();
+
+ std::future<void> fut;
+ if (will_throw) {
+ expected_fail_tasks++;
+ fut = g_pool.Submit(ThrowTask{});
+ } else {
+ expected_task_counter++;
+ fut = g_pool.Submit(CounterTask{task_counter});
+ }
+
+ // If caller wants to wait immediately, consume the future here (safe).
+ if (wait_immediately) {
+ // Waits for this task to complete immediately; prior queued tasks may also complete
+ // as they were queued earlier.
+ GetFuture(fut, fail_counter);
+ } else {
+ // Store task for a posterior check
+ futures.emplace(std::move(fut));
+ }
+ }
+
+ // Drain remaining futures
+ while (!futures.empty()) {
+ auto fut = std::move(futures.front());
+ futures.pop();
+ GetFuture(fut, fail_counter);
+ }
+
+ assert(g_pool.WorkQueueSize() == 0);
+ assert(task_counter.load() == expected_task_counter);
+ assert(fail_counter == expected_fail_tasks);
+}
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.