Skip to content

Commit dda2d4e

Browse files
committed
Merge bitcoin/bitcoin#32113: fuzz: enable running fuzz test cases in Debug mode
3669ecd doc: Document fuzz build options (Anthony Towns) c1d01f5 fuzz: enable running fuzz test cases in Debug mode (Anthony Towns) Pull request description: When building with BUILD_FOR_FUZZING=OFF BUILD_FUZZ_BINARY=ON CMAKE_BUILD_TYPE=Debug allow the fuzz binary to execute given test cases (without actual fuzzing) to make it easier to reproduce fuzz test failures in a more normal debug build. In Debug builds, deterministic fuzz behaviour is controlled via a runtime variable, which is normally false, but set to true automatically in the fuzz binary, unless the FUZZ_NONDETERMINISM environment variable is set. ACKs for top commit: maflcko: re-ACK 3669ecd 🏉 marcofleon: re ACK 3669ecd ryanofsky: Code review ACK 3669ecd with just variable renamed and documentation added since last review Tree-SHA512: 5da5736462f98437d0aa1bd01aeacb9d46a9cc446a748080291067f7a27854c89f560f3a6481b760b9a0ea15a8d3ad90cd329ee2a008e5e347a101ed2516449e
2 parents e5a00b2 + 3669ecd commit dda2d4e

File tree

7 files changed

+79
-16
lines changed

7 files changed

+79
-16
lines changed

doc/fuzzing.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,37 @@ If you find coverage increasing inputs when fuzzing you are highly encouraged to
150150
151151
Every single pull request submitted against the Bitcoin Core repo is automatically tested against all inputs in the [`bitcoin-core/qa-assets`](https://github.com/bitcoin-core/qa-assets) repo. Contributing new coverage increasing inputs is an easy way to help make Bitcoin Core more robust.
152152
153+
## Building and debugging fuzz tests
154+
155+
There are 3 ways fuzz tests can be built:
156+
157+
1. With `-DBUILD_FOR_FUZZING=ON` which forces on fuzz determinism (skipping
158+
proof of work checks, disabling random number seeding, disabling clock time)
159+
and causes `Assume()` checks to abort on failure.
160+
161+
This is the normal way to run fuzz tests and generate new inputs. Because
162+
determinism is hardcoded on in this build, only the fuzz binary can be built
163+
and all other binaries are disabled.
164+
165+
2. With `-DBUILD_FUZZ_BINARY=ON -DCMAKE_BUILD_TYPE=Debug` which causes
166+
`Assume()` checks to abort on failure, and enables fuzz determinism, but
167+
makes it optional.
168+
169+
Determinism is turned on in the fuzz binary by default, but can be turned off
170+
by setting the `FUZZ_NONDETERMINISM` environment variable to any value, which
171+
may be useful for running fuzz tests with code that deterministic execution
172+
would otherwise skip.
173+
174+
Since `BUILD_FUZZ_BINARY`, unlike `BUILD_FOR_FUZZING`, does not hardcode on
175+
determinism, this allows non-fuzz binaries to coexist in the same build,
176+
making it possible to reproduce fuzz test failures in a normal build.
177+
178+
3. With `-DBUILD_FUZZ_BINARY=ON -DCMAKE_BUILD_TYPE=Release`. In this build, the
179+
fuzz binary will build but refuse to run, because in release builds
180+
determinism is forced off and `Assume()` checks do not abort, so running the
181+
tests would not be useful. This build is only useful for ensuring fuzz tests
182+
compile and link.
183+
153184
## macOS hints for libFuzzer
154185
155186
The default Clang/LLVM version supplied by Apple on macOS does not include

src/pow.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ bool PermittedDifficultyTransition(const Consensus::Params& params, int64_t heig
139139
// the most significant bit of the last byte of the hash is set.
140140
bool CheckProofOfWork(uint256 hash, unsigned int nBits, const Consensus::Params& params)
141141
{
142-
if constexpr (G_FUZZING) return (hash.data()[31] & 0x80) == 0;
142+
if (EnableFuzzDeterminism()) return (hash.data()[31] & 0x80) == 0;
143143
return CheckProofOfWorkImpl(hash, nBits, params);
144144
}
145145

src/test/fuzz/fuzz.cpp

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,10 +149,18 @@ static void initialize()
149149
std::cerr << "No fuzz target compiled for " << g_fuzz_target << "." << std::endl;
150150
std::exit(EXIT_FAILURE);
151151
}
152-
if constexpr (!G_FUZZING) {
153-
std::cerr << "Must compile with -DBUILD_FOR_FUZZING=ON to execute a fuzz target." << std::endl;
152+
if constexpr (!G_FUZZING_BUILD && !G_ABORT_ON_FAILED_ASSUME) {
153+
std::cerr << "Must compile with -DBUILD_FOR_FUZZING=ON or in Debug mode to execute a fuzz target." << std::endl;
154154
std::exit(EXIT_FAILURE);
155155
}
156+
if (!EnableFuzzDeterminism()) {
157+
if (std::getenv("FUZZ_NONDETERMINISM")) {
158+
std::cerr << "Warning: FUZZ_NONDETERMINISM env var set, results may be inconsistent with fuzz build" << std::endl;
159+
} else {
160+
g_enable_dynamic_fuzz_determinism = true;
161+
assert(EnableFuzzDeterminism());
162+
}
163+
}
156164
Assert(!g_test_one_input);
157165
g_test_one_input = &it->second.test_one_input;
158166
it->second.opts.init();

src/test/util/random.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ void SeedRandomStateForTest(SeedRand seedtype)
2525
// no longer truly random. It should be enough to get the seed once for the
2626
// process.
2727
static const auto g_ctx_seed = []() -> std::optional<uint256> {
28-
if constexpr (G_FUZZING) return {};
28+
if (EnableFuzzDeterminism()) return {};
2929
// If RANDOM_CTX_SEED is set, use that as seed.
3030
if (const char* num{std::getenv(RANDOM_CTX_SEED)}) {
3131
if (auto num_parsed{uint256::FromUserHex(num)}) {
@@ -40,7 +40,7 @@ void SeedRandomStateForTest(SeedRand seedtype)
4040
}();
4141

4242
g_seeded_g_prng_zero = seedtype == SeedRand::ZEROS;
43-
if constexpr (G_FUZZING) {
43+
if (EnableFuzzDeterminism()) {
4444
Assert(g_seeded_g_prng_zero); // Only SeedRandomStateForTest(SeedRand::ZEROS) is allowed in fuzz tests
4545
Assert(!g_used_g_prng); // The global PRNG must not have been used before SeedRandomStateForTest(SeedRand::ZEROS)
4646
}

src/test/util/setup_common.cpp

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ static void ExitFailure(std::string_view str_err)
112112
BasicTestingSetup::BasicTestingSetup(const ChainType chainType, TestOpts opts)
113113
: m_args{}
114114
{
115-
if constexpr (!G_FUZZING) {
115+
if (!EnableFuzzDeterminism()) {
116116
SeedRandomForTest(SeedRand::FIXED_SEED);
117117
}
118118
m_node.shutdown_signal = &m_interrupt;
@@ -203,7 +203,7 @@ BasicTestingSetup::~BasicTestingSetup()
203203
{
204204
m_node.ecc_context.reset();
205205
m_node.kernel.reset();
206-
if constexpr (!G_FUZZING) {
206+
if (!EnableFuzzDeterminism()) {
207207
SetMockTime(0s); // Reset mocktime for following tests
208208
}
209209
LogInstance().DisconnectTestLogger();
@@ -229,8 +229,9 @@ ChainTestingSetup::ChainTestingSetup(const ChainType chainType, TestOpts opts)
229229
m_node.scheduler->m_service_thread = std::thread(util::TraceThread, "scheduler", [&] { m_node.scheduler->serviceQueue(); });
230230
m_node.validation_signals =
231231
// Use synchronous task runner while fuzzing to avoid non-determinism
232-
G_FUZZING ? std::make_unique<ValidationSignals>(std::make_unique<util::ImmediateTaskRunner>()) :
233-
std::make_unique<ValidationSignals>(std::make_unique<SerialTaskRunner>(*m_node.scheduler));
232+
EnableFuzzDeterminism() ?
233+
std::make_unique<ValidationSignals>(std::make_unique<util::ImmediateTaskRunner>()) :
234+
std::make_unique<ValidationSignals>(std::make_unique<SerialTaskRunner>(*m_node.scheduler));
234235
{
235236
// Ensure deterministic coverage by waiting for m_service_thread to be running
236237
std::promise<void> promise;
@@ -255,7 +256,7 @@ ChainTestingSetup::ChainTestingSetup(const ChainType chainType, TestOpts opts)
255256
.notifications = *m_node.notifications,
256257
.signals = m_node.validation_signals.get(),
257258
// Use no worker threads while fuzzing to avoid non-determinism
258-
.worker_threads_num = G_FUZZING ? 0 : 2,
259+
.worker_threads_num = EnableFuzzDeterminism() ? 0 : 2,
259260
};
260261
if (opts.min_validation_cache) {
261262
chainman_opts.script_execution_cache_bytes = 0;

src/util/check.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,5 @@ void assertion_fail(std::string_view file, int line, std::string_view func, std:
3333
fwrite(str.data(), 1, str.size(), stderr);
3434
std::abort();
3535
}
36+
37+
std::atomic<bool> g_enable_dynamic_fuzz_determinism{false};

src/util/check.h

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,44 @@
77

88
#include <attributes.h>
99

10+
#include <atomic>
1011
#include <cassert> // IWYU pragma: export
1112
#include <stdexcept>
1213
#include <string>
1314
#include <string_view>
1415
#include <utility>
1516

16-
constexpr bool G_FUZZING{
17+
constexpr bool G_FUZZING_BUILD{
1718
#ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
1819
true
1920
#else
2021
false
2122
#endif
2223
};
24+
constexpr bool G_ABORT_ON_FAILED_ASSUME{
25+
#ifdef ABORT_ON_FAILED_ASSUME
26+
true
27+
#else
28+
false
29+
#endif
30+
};
31+
32+
extern std::atomic<bool> g_enable_dynamic_fuzz_determinism;
33+
34+
inline bool EnableFuzzDeterminism()
35+
{
36+
if constexpr (G_FUZZING_BUILD) {
37+
return true;
38+
} else if constexpr (!G_ABORT_ON_FAILED_ASSUME) {
39+
// Running fuzz tests is always disabled if Assume() doesn't abort
40+
// (ie, non-fuzz non-debug builds), as otherwise tests which
41+
// should fail due to a failing Assume may still pass. As such,
42+
// we also statically disable fuzz determinism in that case.
43+
return false;
44+
} else {
45+
return g_enable_dynamic_fuzz_determinism;
46+
}
47+
}
2348

2449
std::string StrFormatInternalBug(std::string_view msg, std::string_view file, int line, std::string_view func);
2550

@@ -50,11 +75,7 @@ void assertion_fail(std::string_view file, int line, std::string_view func, std:
5075
template <bool IS_ASSERT, typename T>
5176
constexpr T&& inline_assertion_check(LIFETIMEBOUND T&& val, [[maybe_unused]] const char* file, [[maybe_unused]] int line, [[maybe_unused]] const char* func, [[maybe_unused]] const char* assertion)
5277
{
53-
if (IS_ASSERT || std::is_constant_evaluated() || G_FUZZING
54-
#ifdef ABORT_ON_FAILED_ASSUME
55-
|| true
56-
#endif
57-
) {
78+
if (IS_ASSERT || std::is_constant_evaluated() || G_FUZZING_BUILD || G_ABORT_ON_FAILED_ASSUME) {
5879
if (!val) {
5980
assertion_fail(file, line, func, assertion);
6081
}

0 commit comments

Comments
 (0)