Skip to content

Commit 15e04d2

Browse files
TheCharlatanfurszy
andcommitted
fuzz: add test case for threadpool
Co-authored-by: furszy <[email protected]>
1 parent c219b93 commit 15e04d2

File tree

2 files changed

+112
-0
lines changed

2 files changed

+112
-0
lines changed

src/test/fuzz/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ add_executable(fuzz
118118
string.cpp
119119
strprintf.cpp
120120
system.cpp
121+
threadpool.cpp
121122
timeoffsets.cpp
122123
torcontrol.cpp
123124
transaction.cpp

src/test/fuzz/threadpool.cpp

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Copyright (c) 2025-present The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#include <common/system.h>
6+
#include <logging.h>
7+
#include <util/threadpool.h>
8+
9+
#include <test/fuzz/FuzzedDataProvider.h>
10+
#include <test/fuzz/fuzz.h>
11+
12+
#include <atomic>
13+
#include <future>
14+
#include <queue>
15+
#include <iostream>
16+
17+
struct ExpectedException : std::runtime_error {
18+
explicit ExpectedException(const std::string& msg) : std::runtime_error(msg) {}
19+
};
20+
21+
struct ThrowTask {
22+
void operator()() const { throw ExpectedException("fail"); }
23+
};
24+
25+
struct CounterTask {
26+
std::atomic_uint32_t& m_counter;
27+
explicit CounterTask(std::atomic_uint32_t& counter) : m_counter{counter} {}
28+
void operator()() const { m_counter.fetch_add(1); }
29+
};
30+
31+
// Waits for a future to complete. Increments 'fail_counter' if the expected exception is thrown.
32+
static void GetFuture(std::future<void>& future, uint32_t& fail_counter)
33+
{
34+
try {
35+
future.get();
36+
} catch (const ExpectedException&) {
37+
fail_counter++;
38+
} catch (...) {
39+
assert(false && "Unexpected exception type");
40+
}
41+
}
42+
43+
// Global thread pool for fuzzing. Persisting it across iterations prevents
44+
// the excessive thread creation/destruction overhead that can lead to
45+
// instability in the fuzzing environment.
46+
// This is also how we use it in the app's lifecycle.
47+
ThreadPool g_pool{"fuzz"};
48+
// Global to verify we always have the same number of threads.
49+
size_t g_num_workers = 3;
50+
51+
static void setup_threadpool_test()
52+
{
53+
// Disable logging entirely. It seems to cause memory leaks.
54+
LogInstance().DisableLogging();
55+
// Ensure the pool is started only once.
56+
static std::once_flag started;
57+
std::call_once(started, []() {
58+
g_pool.Start(g_num_workers);
59+
});
60+
}
61+
62+
FUZZ_TARGET(threadpool, .init = setup_threadpool_test)
63+
{
64+
FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size());
65+
66+
const uint32_t num_tasks = fuzzed_data_provider.ConsumeIntegralInRange<uint32_t>(0, 1024);
67+
assert(g_pool.WorkersCount() == g_num_workers);
68+
assert(g_pool.WorkQueueSize() == 0);
69+
70+
// Counters
71+
std::atomic_uint32_t task_counter{0};
72+
uint32_t fail_counter{0};
73+
uint32_t expected_task_counter{0};
74+
uint32_t expected_fail_tasks{0};
75+
76+
std::queue<std::future<void>> futures;
77+
for (uint32_t i = 0; i < num_tasks; ++i) {
78+
const bool will_throw = fuzzed_data_provider.ConsumeBool();
79+
const bool wait_immediately = fuzzed_data_provider.ConsumeBool();
80+
81+
std::future<void> fut;
82+
if (will_throw) {
83+
expected_fail_tasks++;
84+
fut = g_pool.Submit(ThrowTask{});
85+
} else {
86+
expected_task_counter++;
87+
fut = g_pool.Submit(CounterTask{task_counter});
88+
}
89+
90+
// If caller wants to wait immediately, consume the future here (safe).
91+
if (wait_immediately) {
92+
// Waits for this task to complete immediately; prior queued tasks may also complete
93+
// as they were queued earlier.
94+
GetFuture(fut, fail_counter);
95+
} else {
96+
// Store task for a posterior check
97+
futures.emplace(std::move(fut));
98+
}
99+
}
100+
101+
// Drain remaining futures
102+
while (!futures.empty()) {
103+
auto fut = std::move(futures.front());
104+
futures.pop();
105+
GetFuture(fut, fail_counter);
106+
}
107+
108+
assert(g_pool.WorkQueueSize() == 0);
109+
assert(task_counter.load() == expected_task_counter);
110+
assert(fail_counter == expected_fail_tasks);
111+
}

0 commit comments

Comments
 (0)