fuzz: prevent invalid `FRESH` entries and surface `BatchWrite` errors
What changed, and why it matters
This change only touches a fuzz test file, not the main Bitcoin Core wallet or consensus code. It removes a workaround that previously swallowed a specific logic error during fuzz testing and instead makes the fuzzer avoid creating the invalid condition in the first place. There is no indication this fixes a real-world vulnerability in production software.
No production action needed. Treat as a test-quality improvement. Reviewers may want to confirm the invariant logic in the fuzzer matches production expectations, but the commit itself does not require deployment or incident response.
Security signals we found
Change is confined to a fuzz test harness (src/test/fuzz/coins_view.cpp)
Previously swallowed std::logic_error is no longer caught, so real bugs can fail tests
Fuzzer now enforces realistic CCoinsCacheEntry invariants (FRESH implies DIRTY, no FRESH for existing parent coins)
No changes to production CCoinsViewCache, CCoinsViewDB, or consensus logic
Evidence from the diff
The commit modifies src/test/fuzz/coins_view.cpp. It stops catching and hiding std::logic_error(“FRESH flag misapplied to coin that exists in parent cache”) in a custom BatchWrite wrapper and in the fuzz test body. Instead, the fuzzer now constructs CCoinsCacheEntry values so that FRESH is never set when the parent view already has an unspent coin for that outpoint, and FRESH always implies DIRTY. This makes the fuzz test exercise realistic cache invariants and lets genuine BatchWrite failures surface as test failures.
Changed components
src/test/fuzz/coins_view.cppInspect captured patch +10 / −29
diff --git a/src/test/fuzz/coins_view.cpp b/src/test/fuzz/coins_view.cpp
index 011b660e..a47dc370 100644
--- a/src/test/fuzz/coins_view.cpp
+++ b/src/test/fuzz/coins_view.cpp
@@ -77,18 +77,7 @@ public:
{
// Nothing must modify cacheCoins other than BatchWrite.
assert(ComputeCacheCoinsSnapshot() == m_expected_snapshot);
- try {
- CCoinsViewCache::BatchWrite(cursor, block_hash);
- } catch (const std::logic_error& e) {
- // This error is thrown if the cursor contains a fresh entry for an outpoint that we already have a fresh
- // entry for. This can happen if the fuzzer calls AddCoin -> Flush -> AddCoin -> Flush on the child cache.
- // There's not an easy way to prevent the fuzzer from reaching this, so we handle it here.
- // Since it is thrown in the middle of the write, we reset our own state and iterate through
- // the cursor so the caller's state is also reset.
- assert(e.what() == std::string{"FRESH flag misapplied to coin that exists in parent cache"});
- Reset();
- for (auto it{cursor.Begin()}; it != cursor.End(); it = cursor.NextAndMaybeErase(*it)) {}
- }
+ CCoinsViewCache::BatchWrite(cursor, block_hash);
m_expected_snapshot = ComputeCacheCoinsSnapshot();
}
@@ -199,8 +188,6 @@ void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsViewCache& co
LIMITED_WHILE(good_data && fuzzed_data_provider.ConsumeBool(), 10'000)
{
CCoinsCacheEntry coins_cache_entry;
- const auto dirty{fuzzed_data_provider.ConsumeBool()};
- const auto fresh{fuzzed_data_provider.ConsumeBool()};
if (fuzzed_data_provider.ConsumeBool()) {
coins_cache_entry.coin = random_coin;
} else {
@@ -211,26 +198,20 @@ void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsViewCache& co
}
coins_cache_entry.coin = *opt_coin;
}
+ // Avoid setting FRESH for an outpoint that already exists unspent in the parent view.
+ bool fresh{!coins_view_cache.PeekCoin(random_out_point) && fuzzed_data_provider.ConsumeBool()};
+ bool dirty{fresh || fuzzed_data_provider.ConsumeBool()};
auto it{coins_map.emplace(random_out_point, std::move(coins_cache_entry)).first};
if (dirty) CCoinsCacheEntry::SetDirty(*it, sentinel);
if (fresh) CCoinsCacheEntry::SetFresh(*it, sentinel);
dirty_count += dirty;
}
- bool expected_code_path = false;
- try {
- auto cursor{CoinsViewCacheCursor(dirty_count, sentinel, coins_map, /*will_erase=*/true)};
- uint256 best_block{coins_view_cache.GetBestBlock()};
- if (fuzzed_data_provider.ConsumeBool()) best_block = ConsumeUInt256(fuzzed_data_provider);
- // Set best block hash to non-null to satisfy the assertion in CCoinsViewDB::BatchWrite().
- if (is_db && best_block.IsNull()) best_block = uint256::ONE;
- coins_view_cache.BatchWrite(cursor, best_block);
- expected_code_path = true;
- } catch (const std::logic_error& e) {
- if (e.what() == std::string{"FRESH flag misapplied to coin that exists in parent cache"}) {
- expected_code_path = true;
- }
- }
- assert(expected_code_path);
+ auto cursor{CoinsViewCacheCursor(dirty_count, sentinel, coins_map, /*will_erase=*/true)};
+ uint256 best_block{coins_view_cache.GetBestBlock()};
+ if (fuzzed_data_provider.ConsumeBool()) best_block = ConsumeUInt256(fuzzed_data_provider);
+ // Set best block hash to non-null to satisfy the assertion in CCoinsViewDB::BatchWrite().
+ if (is_db && best_block.IsNull()) best_block = uint256::ONE;
+ coins_view_cache.BatchWrite(cursor, best_block);
});
}
Why this scored 17/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.