From b63ef20d545f739f9def814155c0eb95c85c0b85 Mon Sep 17 00:00:00 2001 From: Andrew Toth Date: Sat, 21 Mar 2026 11:21:27 -0400 Subject: [PATCH] test: add fuzz harness for CDBWrapper Introduces a libFuzzer harness that exercises CDBWrapper operations against a std::map oracle, with a DeterministicEnv that captures LevelDB background compaction for single-threaded determinism. A sibling dbwrapper_threaded target uses a bare memenv so LevelDB's real background thread runs, exercising force_compact and threaded compaction paths that the deterministic variant cannot reach. Adds an implicit-integer-sign-change suppression for BytewiseComparatorImpl::FindShortSuccessor (leveldb/util/comparator.cc:58) to the test ubsan suppressions list. LevelDB's bytewise comparator implicitly converts a signed `char` byte to `uint8_t` there. The path is only reached when compaction picks an SST boundary key, so it requires a small enough max_file_size for compaction to fire during the fuzz run. Co-authored-by: l0rinc --- src/test/fuzz/CMakeLists.txt | 1 + src/test/fuzz/dbwrapper.cpp | 325 ++++++++++++++++++++++++++++++ test/sanitizer_suppressions/ubsan | 1 + 3 files changed, 327 insertions(+) create mode 100644 src/test/fuzz/dbwrapper.cpp diff --git a/src/test/fuzz/CMakeLists.txt b/src/test/fuzz/CMakeLists.txt index fc82fdc03a2..805a092b3ec 100644 --- a/src/test/fuzz/CMakeLists.txt +++ b/src/test/fuzz/CMakeLists.txt @@ -39,6 +39,7 @@ add_executable(fuzz crypto_hkdf_hmac_sha256_l32.cpp crypto_poly1305.cpp cuckoocache.cpp + dbwrapper.cpp decode_tx.cpp descriptor_parse.cpp deserialize.cpp diff --git a/src/test/fuzz/dbwrapper.cpp b/src/test/fuzz/dbwrapper.cpp new file mode 100644 index 00000000000..13fff136aee --- /dev/null +++ b/src/test/fuzz/dbwrapper.cpp @@ -0,0 +1,325 @@ +// 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 +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +/** + * A leveldb::Env that wraps a memenv and captures scheduled background + * work (compaction) instead of dispatching to a real thread. The fuzz + * harness calls RunOne() or DrainWork() at fuzzer-chosen points to + * execute it, giving deterministic control over when compaction + * interleaves with foreground operations. + * + * Deadlock prevention: LevelDB's MakeRoomForWrite blocks on a condition + * variable when the previous immutable memtable is still awaiting compaction, + * or when the L0 file count hits kL0_StopWritesTrigger. Since both conditions + * can only be resolved by the (deferred) background work, the harness drains + * all pending work before every write to avoid a single-threaded deadlock. + * Callers must also DrainWork() before destroying the CDBWrapper, since the + * leveldb destructor waits for any pending background work to complete. + * + * The same reasoning rules out exercising DBOptions::force_compact under + * this env, because CompactRange(nullptr, nullptr) blocks waiting for + * background work that is queued on the (blocked) foreground thread. The + * sibling dbwrapper_threaded target covers that path. + */ +class DeterministicEnv final : public leveldb::EnvWrapper +{ + using WorkFunction = void (*)(void*); + + struct Work { + WorkFunction function; + void* arg; + }; + + std::deque m_queue; + +public: + explicit DeterministicEnv(leveldb::Env* base) : EnvWrapper(base) {} + + void Schedule(WorkFunction function, void* arg) override + { + m_queue.push_back({function, arg}); + } + + /** Execute one pending background task. The task may schedule a + * successor which is left pending for a later call. */ + bool RunOne() + { + if (m_queue.empty()) return false; + const Work work{m_queue.front()}; + m_queue.pop_front(); + work.function(work.arg); + return true; + } + + /** Execute pending background tasks until none remain. */ + void DrainWork() { while (RunOne()) {} } +}; + +constexpr size_t MAX_VALUE_LEN{4096}; +constexpr uint8_t MAX_VALUE_MULTIPLIER{8}; +constexpr size_t WRITE_BATCH_HEADER{12}; // See kHeader in db/write_batch.cc + +/** Mirror of CDBWrapper::OBFUSCATION_KEY, the fixed key under which leveldb + * stores the obfuscation metadata entry when obfuscation is enabled. */ +const std::string OBFUSCATION_KEY{"\000obfuscate_key", 14}; + +/** Generate a deterministic value from key and size. The fuzz input picks + * a 16-bit length (up to MAX_VALUE_LEN) and an 8-bit multiplier so that a + * small amount of fuzz input can produce a wide range of value sizes. */ +std::vector MakeValue(uint16_t key, uint32_t size) +{ + std::vector v(size); + std::iota(v.begin(), v.end(), static_cast(key ^ (key >> 8))); + return v; +} + +/** Equivalent to leveldb::BytewiseComparator() on 2-byte little-endian + * serialized uint16_t keys, while keeping the oracle keyed by uint16_t. */ +struct LevelDBBytewiseU16Cmp { + bool operator()(uint16_t a, uint16_t b) const { return internal_bswap_16(a) < internal_bswap_16(b); } +}; + +/** key → value-size map ordered by LevelDB's bytewise comparator. */ +using Oracle = std::map; + +struct FailUnserialize { + template + void Unserialize(Stream&) { throw std::ios_base::failure{"always fail"}; } +}; + +uint16_t ConsumeKey(FuzzedDataProvider& provider) { return provider.ConsumeIntegral(); } +uint32_t ConsumeValueSize(FuzzedDataProvider& provider) +{ + const uint16_t len{provider.ConsumeIntegralInRange(0, MAX_VALUE_LEN)}; + const uint8_t multiplier{provider.ConsumeIntegralInRange(1, MAX_VALUE_MULTIPLIER)}; + return static_cast(len) * multiplier; +} + +/** Verify that the DB iterator matches the oracle, handling the obfuscation + * metadata entry (stored under a non-uint16_t key) when obfuscation is on. */ +void VerifyIterator(CDBWrapper& dbw, const Oracle& oracle, + bool obfuscate, std::optional seek_key = std::nullopt) +{ + const std::unique_ptr it{dbw.NewIterator()}; + auto oracle_it{seek_key ? oracle.lower_bound(*seek_key) : oracle.begin()}; + if (seek_key) { + it->Seek(*seek_key); + } else { + it->SeekToFirst(); + } + for (; it->Valid(); it->Next()) { + uint16_t db_key; + assert(it->GetKey(db_key)); + if (oracle_it != oracle.end() && db_key == oracle_it->first) { + std::vector db_value; + assert(it->GetValue(db_value)); + assert(db_value == MakeValue(db_key, oracle_it->second)); + ++oracle_it; + } else { + assert(obfuscate); + std::string key_str; + assert(it->GetKey(key_str)); + assert(key_str == OBFUSCATION_KEY); + } + } + assert(oracle_it == oracle.end()); +} + +template +void TestDbWrapper(FuzzedDataProvider& provider, + leveldb::Env* testing_env, + DrainWorkFn drain_work, + RunOneFn run_one, + bool allow_force_compact) +{ + SeedRandomStateForTest(SeedRand::ZEROS); + + const bool obfuscate{provider.ConsumeBool()}; + + const auto make_db{[&](DBOptions options = {}) { + return std::make_unique(DBParams{ + .path = "dbwrapper_fuzz", + .cache_bytes = provider.ConsumeIntegralInRange(64 << 10, 1_MiB), + .obfuscate = obfuscate, + .options = options, + .testing_env = testing_env, + .max_file_size = provider.ConsumeBool() + ? DBWRAPPER_MAX_FILE_SIZE + : provider.ConsumeIntegralInRange(1_MiB, 4_MiB), + }); + }}; + std::unique_ptr dbw{make_db()}; + + // Oracle: key → value size. Content is reconstructed via MakeValue(). + Oracle oracle; + + LIMITED_WHILE(provider.ConsumeBool(), 1'000) + { + CallOneOf( + provider, + // --- Mutations --- + [&] { + const auto key{ConsumeKey(provider)}; + const auto size{ConsumeValueSize(provider)}; + drain_work(); + dbw->Write(key, MakeValue(key, size), /*fSync=*/provider.ConsumeBool()); + oracle[key] = size; + }, + [&] { + const auto key{ConsumeKey(provider)}; + drain_work(); + dbw->Erase(key, /*fSync=*/provider.ConsumeBool()); + oracle.erase(key); + }, + [&] { + CDBBatch batch{*dbw}; + std::map batch_writes; + std::set batch_erases; + const auto fill{[&] { + LIMITED_WHILE(provider.ConsumeBool(), 20) + { + const auto key{ConsumeKey(provider)}; + if (provider.ConsumeBool()) { + const auto size{ConsumeValueSize(provider)}; + batch.Write(key, MakeValue(key, size)); + batch_writes[key] = size; + batch_erases.erase(key); + } else { + batch.Erase(key); + batch_erases.insert(key); + batch_writes.erase(key); + } + } + }}; + fill(); + if (provider.ConsumeBool()) { + assert(batch.ApproximateSize() >= WRITE_BATCH_HEADER); + batch.Clear(); + assert(batch.ApproximateSize() == WRITE_BATCH_HEADER); + batch_writes.clear(); + batch_erases.clear(); + fill(); + } + drain_work(); + dbw->WriteBatch(batch, /*fSync=*/provider.ConsumeBool()); + for (const auto& [k, v] : batch_writes) oracle[k] = v; + for (const auto& k : batch_erases) oracle.erase(k); + }, + [&] { + drain_work(); + dbw.reset(); + DBOptions options{}; + if (allow_force_compact && provider.ConsumeBool()) { + options.force_compact = true; + } + dbw = make_db(options); + VerifyIterator(*dbw, oracle, obfuscate); + }, + // --- Reads --- + [&] { + const auto key{ConsumeKey(provider)}; + std::vector value; + const bool found{dbw->Read(key, value)}; + if (const auto it{oracle.find(key)}; it != oracle.end()) { + assert(found && value == MakeValue(key, it->second)); + } else { + assert(!found); + } + }, + [&] { + const auto key{ConsumeKey(provider)}; + assert(dbw->Exists(key) == oracle.contains(key)); + }, + [&] { + uint16_t key{}; + if (!oracle.empty() && provider.ConsumeBool()) { + auto it{oracle.begin()}; + std::advance(it, provider.ConsumeIntegralInRange(0, oracle.size() - 1)); + key = it->first; + } else { + key = ConsumeKey(provider); + } + FailUnserialize wrong_type; + assert(!dbw->Read(key, wrong_type)); + }, + [&] { + const auto seek_key{provider.ConsumeBool() + ? std::optional{ConsumeKey(provider)} + : std::nullopt}; + VerifyIterator(*dbw, oracle, obfuscate, seek_key); + }, + // --- Stats --- + [&] { + assert(dbw->IsEmpty() == (oracle.empty() && !obfuscate)); + }, + [&] { + const auto [k1, k2]{std::minmax({ConsumeKey(provider), ConsumeKey(provider)}, LevelDBBytewiseU16Cmp{})}; + const size_t estimate_size{dbw->EstimateSize(k1, k2)}; + if (k1 == k2) assert(estimate_size == 0); + }, + [&] { + (void)dbw->DynamicMemoryUsage(); + }, + // --- Compaction control (no-op when run_one is no-op) --- + [&] { + run_one(); + }); + } + + VerifyIterator(*dbw, oracle, obfuscate); + drain_work(); +} + +} // namespace + +FUZZ_TARGET(dbwrapper, .init = [] { static auto setup{MakeNoLogFileContext<>()}; }) +{ + FuzzedDataProvider provider{buffer.data(), buffer.size()}; + + const auto memenv{std::unique_ptr{leveldb::NewMemEnv(leveldb::Env::Default())}}; + DeterministicEnv det_env{memenv.get()}; + TestDbWrapper( + provider, &det_env, + [&] { det_env.DrainWork(); }, + [&] { return det_env.RunOne(); }, + /*allow_force_compact=*/false); +} + +FUZZ_TARGET(dbwrapper_threaded, .init = [] { static auto setup{MakeNoLogFileContext<>()}; }) +{ + FuzzedDataProvider provider{buffer.data(), buffer.size()}; + + const auto memenv{std::unique_ptr{leveldb::NewMemEnv(leveldb::Env::Default())}}; + TestDbWrapper( + provider, memenv.get(), + /*drain_work=*/[] {}, + /*run_one=*/[] { return false; }, + /*allow_force_compact=*/true); +} diff --git a/test/sanitizer_suppressions/ubsan b/test/sanitizer_suppressions/ubsan index ad604e6357b..063a8e31231 100644 --- a/test/sanitizer_suppressions/ubsan +++ b/test/sanitizer_suppressions/ubsan @@ -19,6 +19,7 @@ implicit-integer-sign-change:*/include/c++/ implicit-integer-sign-change:*/new_allocator.h implicit-integer-sign-change:*/qarraydata.h implicit-integer-sign-change:crc32c/ +implicit-integer-sign-change:BytewiseComparatorImpl::FindShortSuccessor implicit-integer-sign-change:minisketch/ implicit-integer-sign-change:secp256k1/ implicit-signed-integer-truncation:*/include/c++/