mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-06-05 18:52:29 +02:00
Merge bitcoin/bitcoin#34655: fuzz: keep coins_view fuzzers within caller contracts
3281824ecffuzz: prevent invalid `FRESH` entries and surface `BatchWrite` errors (Lőrinc)780f460635fuzz: avoid invalid `AddCoin` overwrites (Lőrinc)d7e0d510f2fuzz: make `AddCoins` query view for overwrites (Lőrinc)b8fa6f0f70util: introduce `TrySub` to prevent unsigned underflow (Lőrinc) Pull request description: ### Problem This is an alternative approach to #34647, fixes #34645. ### Fix First, add `CheckedSub` and use it for decrements of `m_dirty_count` and `cachedCoinsUsage`, so unsigned underflows turn into immediate failures instead of silently wrapping and only failing later. <details><summary>Assertion `j <= i' failed.</summary> ```bash util/overflow.h:44 T CheckedSub(const T, const U) [T = unsigned long, U = bool]: Assertion `j <= i' failed. ==72817== ERROR: libFuzzer: deadly signal #0 0x556e9225eab5 in __sanitizer_print_stack_trace (/mnt/my_storage/bitcoin/build_fuzz/bin/fuzz+0x191dab5) (BuildId: d77c4d5f9dfd38ea06fab463f49341735205e109) #1 0x556e921acafc in fuzzer::PrintStackTrace() (/mnt/my_storage/bitcoin/build_fuzz/bin/fuzz+0x186bafc) (BuildId: d77c4d5f9dfd38ea06fab463f49341735205e109) #2 0x556e92191bb7 in fuzzer::Fuzzer::CrashCallback() (/mnt/my_storage/bitcoin/build_fuzz/bin/fuzz+0x1850bb7) (BuildId: d77c4d5f9dfd38ea06fab463f49341735205e109) #3 0x7164cfc458cf (/lib/x86_64-linux-gnu/libc.so.6+0x458cf) (BuildId: ae7440bbdce614e0e79280c3b2e45b1df44e639c) #4 0x7164cfca49bb in __pthread_kill_implementation nptl/pthread_kill.c:43:17 #5 0x7164cfca49bb in __pthread_kill_internal nptl/pthread_kill.c:89:10 #6 0x7164cfca49bb in pthread_kill nptl/pthread_kill.c:100:10 #7 0x7164cfc4579d in raise signal/../sysdeps/posix/raise.c:26:13 #8 0x7164cfc288cc in abort stdlib/abort.c:73:3 #9 0x556e92f9d591 in assertion_fail(std::source_location const&, std::basic_string_view<char, std::char_traits<char>>) /mnt/my_storage/bitcoin/src/util/check.cpp:41:5 #10 0x556e9250daf0 in bool&& inline_assertion_check<false, bool>(bool&&, std::source_location const&, std::basic_string_view<char, std::char_traits<char>>) /mnt/my_storage/bitcoin/src/util/check.h:90:13 #11 0x556e9250daf0 in unsigned long CheckedSub<unsigned long, bool>(unsigned long, bool) /mnt/my_storage/bitcoin/src/util/overflow.h:44:5 #12 0x556e9250daf0 in CoinsViewCacheCursor::NextAndMaybeErase(std::pair<COutPoint const, CCoinsCacheEntry>&) /mnt/my_storage/bitcoin/src/coins.h:282:25 #13 0x556e92507eb2 in (anonymous namespace)::MutationGuardCoinsViewCache::BatchWrite(CoinsViewCacheCursor&, uint256 const&) /mnt/my_storage/bitcoin/src/test/fuzz/coins_view.cpp:90:75 #14 0x556e92c17a2b in CCoinsViewCache::Flush(bool) /mnt/my_storage/bitcoin/src/coins.cpp:282:11 #15 0x556e924fb732 in TestCoinsView(FuzzedDataProvider&, CCoinsViewCache&, CCoinsView&, bool)::$_1::operator()() const /mnt/my_storage/bitcoin/src/test/fuzz/coins_view.cpp:135:34 #16 0x556e924fb732 in unsigned long CallOneOf<TestCoinsView(FuzzedDataProvider&, CCoinsViewCache&, CCoinsView&, bool)::$_0, TestCoinsView(FuzzedDataProvider&, CCoinsViewCache&, CCoinsView&, bool)::$_1, TestCoinsView(FuzzedDataProvider&, CCoinsViewCache&, CCoinsView&, bool)::$_2, TestCoinsView(FuzzedDataProvider&, CCoinsViewCache&, CCoinsView&, bool)::$_3, TestCoinsView(FuzzedDataProvider&, CCoinsViewCache&, CCoinsView&, bool)::$_4, TestCoinsView(FuzzedDataProvider&, CCoinsViewCache&, CCoinsView&, bool)::$_5, TestCoinsView(FuzzedDataProvider&, CCoinsViewCache&, CCoinsView&, bool)::$_6, TestCoinsView(FuzzedDataProvider&, CCoinsViewCache&, CCoinsView&, bool)::$_7, TestCoinsView(FuzzedDataProvider&, CCoinsViewCache&, CCoinsView&, bool)::$_8, TestCoinsView(FuzzedDataProvider&, CCoinsViewCache&, CCoinsView&, bool)::$_9, TestCoinsView(FuzzedDataProvider&, CCoinsViewCache&, CCoinsView&, bool)::$_10, TestCoinsView(FuzzedDataProvider&, CCoinsViewCache&, CCoinsView&, bool)::$_11>(FuzzedDataProvider&, TestCoinsView(FuzzedDataProvider&, CCoinsViewCache&, CCoinsView&, bool)::$_0, TestCoinsView(FuzzedDataProvider&, CCoinsViewCache&, CCoinsView&, bool)::$_1, TestCoinsView(FuzzedDataProvider&, CCoinsViewCache&, CCoinsView&, bool)::$_2, TestCoinsView(FuzzedDataProvider&, CCoinsViewCache&, CCoinsView&, bool)::$_3, TestCoinsView(FuzzedDataProvider&, CCoinsViewCache&, CCoinsView&, bool)::$_4, TestCoinsView(FuzzedDataProvider&, CCoinsViewCache&, CCoinsView&, bool)::$_5, TestCoinsView(FuzzedDataProvider&, CCoinsViewCache&, CCoinsView&, bool)::$_6, TestCoinsView(FuzzedDataProvider&, CCoinsViewCache&, CCoinsView&, bool)::$_7, TestCoinsView(FuzzedDataProvider&, CCoinsViewCache&, CCoinsView&, bool)::$_8, TestCoinsView(FuzzedDataProvider&, CCoinsViewCache&, CCoinsView&, bool)::$_9, TestCoinsView(FuzzedDataProvider&, CCoinsViewCache&, CCoinsView&, bool)::$_10, TestCoinsView(FuzzedDataProvider&, CCoinsViewCache&, CCoinsView&, bool)::$_11) /mnt/my_storage/bitcoin/src/test/fuzz/util.h:42:27 #17 0x556e924fb732 in TestCoinsView(FuzzedDataProvider&, CCoinsViewCache&, CCoinsView&, bool) /mnt/my_storage/bitcoin/src/test/fuzz/coins_view.cpp:114:9 #18 0x556e92503b0c in coins_view_overlay_fuzz_target(std::span<unsigned char const, 18446744073709551615ul>) /mnt/my_storage/bitcoin/src/test/fuzz/coins_view.cpp:404:5 #19 0x556e92bcb7a5 in std::function<void (std::span<unsigned char const, 18446744073709551615ul>)>::operator()(std::span<unsigned char const, 18446744073709551615ul>) const /usr/lib/gcc/x86_64-linux-gnu/15/../../../../include/c++/15/bits/std_function.h:593:9 #20 0x556e92bcb7a5 in test_one_input(std::span<unsigned char const, 18446744073709551615ul>) /mnt/my_storage/bitcoin/src/test/fuzz/fuzz.cpp:88:5 #21 0x556e92bcb7a5 in LLVMFuzzerTestOneInput /mnt/my_storage/bitcoin/src/test/fuzz/fuzz.cpp:216:5 #22 0x556e9219318f in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) (/mnt/my_storage/bitcoin/build_fuzz/bin/fuzz+0x185218f) (BuildId: d77c4d5f9dfd38ea06fab463f49341735205e109) #23 0x556e92192799 in fuzzer::Fuzzer::RunOne(unsigned char const*, unsigned long, bool, fuzzer::InputInfo*, bool, bool*) (/mnt/my_storage/bitcoin/build_fuzz/bin/fuzz+0x1851799) (BuildId: d77c4d5f9dfd38ea06fab463f49341735205e109) #24 0x556e92194139 in fuzzer::Fuzzer::MutateAndTestOne() (/mnt/my_storage/bitcoin/build_fuzz/bin/fuzz+0x1853139) (BuildId: d77c4d5f9dfd38ea06fab463f49341735205e109) #25 0x556e92194c95 in fuzzer::Fuzzer::Loop(std::vector<fuzzer::SizedFile, std::allocator<fuzzer::SizedFile>>&) (/mnt/my_storage/bitcoin/build_fuzz/bin/fuzz+0x1853c95) (BuildId: d77c4d5f9dfd38ea06fab463f49341735205e109) #26 0x556e92181255 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) (/mnt/my_storage/bitcoin/build_fuzz/bin/fuzz+0x1840255) (BuildId: d77c4d5f9dfd38ea06fab463f49341735205e109) #27 0x556e921ad696 in main (/mnt/my_storage/bitcoin/build_fuzz/bin/fuzz+0x186c696) (BuildId: d77c4d5f9dfd38ea06fab463f49341735205e109) #28 0x7164cfc2a577 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16 #29 0x7164cfc2a63a in __libc_start_main csu/../csu/libc-start.c:360:3 #30 0x556e921757e4 in _start (/mnt/my_storage/bitcoin/build_fuzz/bin/fuzz+0x18347e4) (BuildId: d77c4d5f9dfd38ea06fab463f49341735205e109) NOTE: libFuzzer has rudimentary signal handlers. Combine libFuzzer with AddressSanitizer or similar for better crash reports. SUMMARY: libFuzzer: deadly signal MS: 2 PersAutoDict-CopyPart- DE: "\005\000"-; base unit: ecb626aff8724f0fdde38a0a6965718f2096d474 artifact_prefix='/tmp/fuzz_artifacts/'; Test unit written to /tmp/fuzz_artifacts/crash-1d19026c1a23f08bfe693fd684a56ce51187c6e5 ./build_fuzz/bin/fuzz /tmp/fuzz_corpus/coins_view_overlay -max_total_time=3600 -rss_limit_mb=2560 -artifact_prefix=/tmp/fuzz_artifacts/ >fuzz-16.log 2>&1 ``` </details> The coins view fuzz targets can call `AddCoin`/`AddCoins` and construct `BatchWrite` cursors in ways that violate `CCoinsViewCache` caller contracts. These invalid states can trigger `BatchWrite` `std::logic_error` and can desync dirty-entry accounting (caught by `Assume(m_dirty_count == 0)` currently). Make the fuzzer avoid generating invalid states instead of catching and resetting: * Derive `AddCoin`’s `possible_overwrite` from `PeekCoin`, so `possible_overwrite=false` is only used when the outpoint is absent - similarly to67c0d1798e/src/test/fuzz/coinscache_sim.cpp (L312-L317)- Only use `AddCoins(check=false)` when we have confirmed the txid has no unspent outputs; otherwise fall back to `check=true` so `AddCoins` determines overwrites via the view. - When constructing a `CoinsViewCacheCursor`, avoid setting `FRESH` when the parent already has an unspent coin, and ensure `FRESH` implies `DIRTY`. ### Fuzzing The original error could be reproduced in ~10 minutes using `coins_view_overlay`. I ran the `coins_view`, `coins_view_db`, `coins_view_overlay`, and `coinscache_sim` fuzzers for this PR overnight and they didn't fail anymore. ACKs for top commit: achow101: ACK3281824ecfsipa: ACK3281824ecf. Ran the 4 relevant fuzz tests for ~1 CPU day each. Will run more overnight. andrewtoth: ACK3281824ecfTree-SHA512: b8155e8d21740eb7800e373c27a8a1457eb84468c24af879bac5a1ed251ade2aec99c34a350a31f2ebb74e41bb7380bf20214d38d14fe23310a43282d2434fb7
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -120,13 +109,9 @@ void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsViewCache& co
|
||||
COutPoint outpoint{random_out_point};
|
||||
Coin coin{random_coin};
|
||||
if (fuzzed_data_provider.ConsumeBool()) {
|
||||
const bool possible_overwrite{fuzzed_data_provider.ConsumeBool()};
|
||||
try {
|
||||
coins_view_cache.AddCoin(outpoint, std::move(coin), possible_overwrite);
|
||||
} catch (const std::logic_error& e) {
|
||||
assert(e.what() == std::string{"Attempted to overwrite an unspent coin (when possible_overwrite is false)"});
|
||||
assert(!possible_overwrite);
|
||||
}
|
||||
// We can only skip the check if no unspent coin exists for this outpoint.
|
||||
const bool possible_overwrite{coins_view_cache.PeekCoin(outpoint) || fuzzed_data_provider.ConsumeBool()};
|
||||
coins_view_cache.AddCoin(outpoint, std::move(coin), possible_overwrite);
|
||||
} else {
|
||||
coins_view_cache.EmplaceCoinInternalDANGER(std::move(outpoint), std::move(coin));
|
||||
}
|
||||
@@ -203,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 {
|
||||
@@ -215,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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -280,19 +257,14 @@ void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsViewCache& co
|
||||
// coins.cpp:69: void CCoinsViewCache::AddCoin(const COutPoint &, Coin &&, bool): Assertion `!coin.IsSpent()' failed.
|
||||
return;
|
||||
}
|
||||
bool expected_code_path = false;
|
||||
const int height{int(fuzzed_data_provider.ConsumeIntegral<uint32_t>() >> 1)};
|
||||
const bool possible_overwrite = fuzzed_data_provider.ConsumeBool();
|
||||
try {
|
||||
AddCoins(coins_view_cache, transaction, height, possible_overwrite);
|
||||
expected_code_path = true;
|
||||
} catch (const std::logic_error& e) {
|
||||
if (e.what() == std::string{"Attempted to overwrite an unspent coin (when possible_overwrite is false)"}) {
|
||||
assert(!possible_overwrite);
|
||||
expected_code_path = true;
|
||||
const bool check_for_overwrite{transaction.IsCoinBase() || [&] {
|
||||
for (uint32_t i{0}; i < transaction.vout.size(); ++i) {
|
||||
if (coins_view_cache.PeekCoin(COutPoint{transaction.GetHash(), i})) return true;
|
||||
}
|
||||
}
|
||||
assert(expected_code_path);
|
||||
return fuzzed_data_provider.ConsumeBool();
|
||||
}()}; // We can only skip the check if the current txid has no unspent outputs
|
||||
AddCoins(coins_view_cache, transaction, height, check_for_overwrite);
|
||||
},
|
||||
[&] {
|
||||
(void)AreInputsStandard(CTransaction{random_mutable_transaction}, coins_view_cache);
|
||||
|
||||
Reference in New Issue
Block a user