From c1d01f59acc2067ecbf8a8b42ba0d8e596694439 Mon Sep 17 00:00:00 2001 From: Anthony Towns Date: Sat, 22 Mar 2025 01:02:13 +1000 Subject: [PATCH 1/2] fuzz: enable running fuzz test cases in Debug mode 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. --- src/pow.cpp | 2 +- src/test/fuzz/fuzz.cpp | 12 ++++++++++-- src/test/util/random.cpp | 4 ++-- src/test/util/setup_common.cpp | 11 ++++++----- src/util/check.cpp | 2 ++ src/util/check.h | 33 +++++++++++++++++++++++++++------ 6 files changed, 48 insertions(+), 16 deletions(-) diff --git a/src/pow.cpp b/src/pow.cpp index 0f54aaec0f7..907d378741d 100644 --- a/src/pow.cpp +++ b/src/pow.cpp @@ -139,7 +139,7 @@ bool PermittedDifficultyTransition(const Consensus::Params& params, int64_t heig // the most significant bit of the last byte of the hash is set. bool CheckProofOfWork(uint256 hash, unsigned int nBits, const Consensus::Params& params) { - if constexpr (G_FUZZING) return (hash.data()[31] & 0x80) == 0; + if (EnableFuzzDeterminism()) return (hash.data()[31] & 0x80) == 0; return CheckProofOfWorkImpl(hash, nBits, params); } diff --git a/src/test/fuzz/fuzz.cpp b/src/test/fuzz/fuzz.cpp index 30766700b9d..77341898536 100644 --- a/src/test/fuzz/fuzz.cpp +++ b/src/test/fuzz/fuzz.cpp @@ -147,10 +147,18 @@ static void initialize() std::cerr << "No fuzz target compiled for " << g_fuzz_target << "." << std::endl; std::exit(EXIT_FAILURE); } - if constexpr (!G_FUZZING) { - std::cerr << "Must compile with -DBUILD_FOR_FUZZING=ON to execute a fuzz target." << std::endl; + if constexpr (!G_FUZZING_BUILD && !G_ABORT_ON_FAILED_ASSUME) { + std::cerr << "Must compile with -DBUILD_FOR_FUZZING=ON or in Debug mode to execute a fuzz target." << std::endl; std::exit(EXIT_FAILURE); } + if (!EnableFuzzDeterminism()) { + if (std::getenv("FUZZ_NONDETERMINISM")) { + std::cerr << "Warning: FUZZ_NONDETERMINISM env var set, results may be inconsistent with fuzz build" << std::endl; + } else { + g_enable_dynamic_fuzz_determinism = true; + assert(EnableFuzzDeterminism()); + } + } Assert(!g_test_one_input); g_test_one_input = &it->second.test_one_input; it->second.opts.init(); diff --git a/src/test/util/random.cpp b/src/test/util/random.cpp index d75f1ef8a6b..90ddf66ca41 100644 --- a/src/test/util/random.cpp +++ b/src/test/util/random.cpp @@ -25,7 +25,7 @@ void SeedRandomStateForTest(SeedRand seedtype) // no longer truly random. It should be enough to get the seed once for the // process. static const auto g_ctx_seed = []() -> std::optional { - if constexpr (G_FUZZING) return {}; + if (EnableFuzzDeterminism()) return {}; // If RANDOM_CTX_SEED is set, use that as seed. if (const char* num{std::getenv(RANDOM_CTX_SEED)}) { if (auto num_parsed{uint256::FromUserHex(num)}) { @@ -40,7 +40,7 @@ void SeedRandomStateForTest(SeedRand seedtype) }(); g_seeded_g_prng_zero = seedtype == SeedRand::ZEROS; - if constexpr (G_FUZZING) { + if (EnableFuzzDeterminism()) { Assert(g_seeded_g_prng_zero); // Only SeedRandomStateForTest(SeedRand::ZEROS) is allowed in fuzz tests Assert(!g_used_g_prng); // The global PRNG must not have been used before SeedRandomStateForTest(SeedRand::ZEROS) } diff --git a/src/test/util/setup_common.cpp b/src/test/util/setup_common.cpp index 87f0b31cad8..82bbc4adcba 100644 --- a/src/test/util/setup_common.cpp +++ b/src/test/util/setup_common.cpp @@ -112,7 +112,7 @@ static void ExitFailure(std::string_view str_err) BasicTestingSetup::BasicTestingSetup(const ChainType chainType, TestOpts opts) : m_args{} { - if constexpr (!G_FUZZING) { + if (!EnableFuzzDeterminism()) { SeedRandomForTest(SeedRand::FIXED_SEED); } m_node.shutdown_signal = &m_interrupt; @@ -203,7 +203,7 @@ BasicTestingSetup::~BasicTestingSetup() { m_node.ecc_context.reset(); m_node.kernel.reset(); - if constexpr (!G_FUZZING) { + if (!EnableFuzzDeterminism()) { SetMockTime(0s); // Reset mocktime for following tests } LogInstance().DisconnectTestLogger(); @@ -229,8 +229,9 @@ ChainTestingSetup::ChainTestingSetup(const ChainType chainType, TestOpts opts) m_node.scheduler->m_service_thread = std::thread(util::TraceThread, "scheduler", [&] { m_node.scheduler->serviceQueue(); }); m_node.validation_signals = // Use synchronous task runner while fuzzing to avoid non-determinism - G_FUZZING ? std::make_unique(std::make_unique()) : - std::make_unique(std::make_unique(*m_node.scheduler)); + EnableFuzzDeterminism() ? + std::make_unique(std::make_unique()) : + std::make_unique(std::make_unique(*m_node.scheduler)); { // Ensure deterministic coverage by waiting for m_service_thread to be running std::promise promise; @@ -255,7 +256,7 @@ ChainTestingSetup::ChainTestingSetup(const ChainType chainType, TestOpts opts) .notifications = *m_node.notifications, .signals = m_node.validation_signals.get(), // Use no worker threads while fuzzing to avoid non-determinism - .worker_threads_num = G_FUZZING ? 0 : 2, + .worker_threads_num = EnableFuzzDeterminism() ? 0 : 2, }; if (opts.min_validation_cache) { chainman_opts.script_execution_cache_bytes = 0; diff --git a/src/util/check.cpp b/src/util/check.cpp index 1430c0e8e29..9f07fbe4780 100644 --- a/src/util/check.cpp +++ b/src/util/check.cpp @@ -33,3 +33,5 @@ void assertion_fail(std::string_view file, int line, std::string_view func, std: fwrite(str.data(), 1, str.size(), stderr); std::abort(); } + +std::atomic g_enable_dynamic_fuzz_determinism{false}; diff --git a/src/util/check.h b/src/util/check.h index efc78915a9e..f7dea5a3c2e 100644 --- a/src/util/check.h +++ b/src/util/check.h @@ -7,19 +7,44 @@ #include +#include #include // IWYU pragma: export #include #include #include #include -constexpr bool G_FUZZING{ +constexpr bool G_FUZZING_BUILD{ #ifdef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION true #else false #endif }; +constexpr bool G_ABORT_ON_FAILED_ASSUME{ +#ifdef ABORT_ON_FAILED_ASSUME + true +#else + false +#endif +}; + +extern std::atomic g_enable_dynamic_fuzz_determinism; + +inline bool EnableFuzzDeterminism() +{ + if constexpr (G_FUZZING_BUILD) { + return true; + } else if constexpr (!G_ABORT_ON_FAILED_ASSUME) { + // Running fuzz tests is always disabled if Assume() doesn't abort + // (ie, non-fuzz non-debug builds), as otherwise tests which + // should fail due to a failing Assume may still pass. As such, + // we also statically disable fuzz determinism in that case. + return false; + } else { + return g_enable_dynamic_fuzz_determinism; + } +} std::string StrFormatInternalBug(std::string_view msg, std::string_view file, int line, std::string_view func); @@ -50,11 +75,7 @@ void assertion_fail(std::string_view file, int line, std::string_view func, std: template 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) { - if (IS_ASSERT || std::is_constant_evaluated() || G_FUZZING -#ifdef ABORT_ON_FAILED_ASSUME - || true -#endif - ) { + if (IS_ASSERT || std::is_constant_evaluated() || G_FUZZING_BUILD || G_ABORT_ON_FAILED_ASSUME) { if (!val) { assertion_fail(file, line, func, assertion); } From 3669ecd4ccd8e7a1e2b1a9dcbe708c51c78e4d6c Mon Sep 17 00:00:00 2001 From: Anthony Towns Date: Tue, 22 Apr 2025 17:11:03 +1000 Subject: [PATCH 2/2] doc: Document fuzz build options Co-Authored-By: Ryan Ofsky --- doc/fuzzing.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/doc/fuzzing.md b/doc/fuzzing.md index 816eee53f90..fd097708e27 100644 --- a/doc/fuzzing.md +++ b/doc/fuzzing.md @@ -143,6 +143,37 @@ If you find coverage increasing inputs when fuzzing you are highly encouraged to 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. +## Building and debugging fuzz tests + +There are 3 ways fuzz tests can be built: + +1. With `-DBUILD_FOR_FUZZING=ON` which forces on fuzz determinism (skipping + proof of work checks, disabling random number seeding, disabling clock time) + and causes `Assume()` checks to abort on failure. + + This is the normal way to run fuzz tests and generate new inputs. Because + determinism is hardcoded on in this build, only the fuzz binary can be built + and all other binaries are disabled. + +2. With `-DBUILD_FUZZ_BINARY=ON -DCMAKE_BUILD_TYPE=Debug` which causes + `Assume()` checks to abort on failure, and enables fuzz determinism, but + makes it optional. + + Determinism is turned on in the fuzz binary by default, but can be turned off + by setting the `FUZZ_NONDETERMINISM` environment variable to any value, which + may be useful for running fuzz tests with code that deterministic execution + would otherwise skip. + + Since `BUILD_FUZZ_BINARY`, unlike `BUILD_FOR_FUZZING`, does not hardcode on + determinism, this allows non-fuzz binaries to coexist in the same build, + making it possible to reproduce fuzz test failures in a normal build. + +3. With `-DBUILD_FUZZ_BINARY=ON -DCMAKE_BUILD_TYPE=Release`. In this build, the + fuzz binary will build but refuse to run, because in release builds + determinism is forced off and `Assume()` checks do not abort, so running the + tests would not be useful. This build is only useful for ensuring fuzz tests + compile and link. + ## macOS hints for libFuzzer The default Clang/LLVM version supplied by Apple on macOS does not include