Skip to content

Commit f76e1ae

Browse files
committed
Merge bitcoin/bitcoin#32313: coins: fix cachedCoinsUsage accounting in CCoinsViewCache
24d861d coins: only adjust `cachedCoinsUsage` on `EmplaceCoinInternalDANGER` insert (Lőrinc) d7c9d6c coins: fix `cachedCoinsUsage` accounting to prevent underflow (Lőrinc) 39cf8bb refactor: remove redundant usage tracking from `CoinsViewCacheCursor` (Lőrinc) 67cff8b refactor: assert newly-created parent cache entry has zero memory usage (Lőrinc) Pull request description: ### Summary This PR fixes `cachedCoinsUsage` accounting bugs in `CCoinsViewCache` that caused UBSan `unsigned-integer-overflow` violations during testing. The issues stemmed from incorrect decrement timing in `AddCoin()`, unconditional reset in `Flush()` on failure, and incorrect increment in `EmplaceCoinInternalDANGER()` when insertion fails. ### Problems Fixed **1. `AddCoin()` underflow on exception** - Previously decremented `cachedCoinsUsage` *before* the `possible_overwrite` validation - If validation threw, the map entry remained unchanged but counter was decremented - This corrupted accounting and later caused underflow - **Impact**: Test-only in current codebase, but unsound accounting that could affect future changes **2. `Flush()` accounting drift on failure** - Unconditionally reset `cachedCoinsUsage` to 0, even when `BatchWrite()` failed - Left the map populated while the counter read zero - **Impact**: Test-only (production `BatchWrite()` returns `true`), but broke accounting consistency **3. Cursor redundant usage tracking** - `CoinsViewCacheCursor::NextAndMaybeErase()` subtracted usage when erasing spent entries - However, `SpendCoin()` already decremented and cleared the `scriptPubKey`, leaving `DynamicMemoryUsage()` at 0 - **Impact**: Redundant code that obscured actual accounting behavior **4. `EmplaceCoinInternalDANGER()` double-counting** - Incremented `cachedCoinsUsage` even when `try_emplace` did not insert (duplicate key) - Inflated the counter on duplicate attempts - **Impact**: Mostly test-reachable (AssumeUTXO doesn't overwrite in production), but incorrect accounting ### Testing To reproduce the historical UBSan failures on the referenced baseline and to verify the fix, run: ``` MAKEJOBS="-j$(nproc)" FILE_ENV="./ci/test/00_setup_env_native_fuzz.sh" ./ci/test_run_all.sh ``` The change was tested with the related unit and fuzz test, and asserted before/after each `cachedCoinsUsage` change (in production code and fuzz) that the calculations are still correct by recalculating them from scratch. <details> <summary>Details</summary> ```C++ bool CCoinsViewCache::CacheUsageValid() const { size_t actual{0}; for (auto& entry : cacheCoins | std::views::values) actual += entry.coin.DynamicMemoryUsage(); return actual == cachedCoinsUsage; } ``` or ```patch diff --git a/src/coins.cpp b/src/coins.cpp --- a/src/coins.cpp(revision fd3b1a7f4bb2ac527f23d4eb4cfa40a3215906e5) +++ b/src/coins.cpp(revision 872a05633bfdbd06ad82190d7fe34b42d13ebfe9) @@ -96,6 +96,7 @@ fresh = !it->second.IsDirty(); } if (!inserted) { + Assert(cachedCoinsUsage >= it->second.coin.DynamicMemoryUsage()); cachedCoinsUsage -= it->second.coin.DynamicMemoryUsage(); } it->second.coin = std::move(coin); @@ -133,6 +134,7 @@ bool CCoinsViewCache::SpendCoin(const COutPoint &outpoint, Coin* moveout) { CCoinsMap::iterator it = FetchCoin(outpoint); if (it == cacheCoins.end()) return false; + Assert(cachedCoinsUsage >= it->second.coin.DynamicMemoryUsage()); cachedCoinsUsage -= it->second.coin.DynamicMemoryUsage(); TRACEPOINT(utxocache, spent, outpoint.hash.data(), @@ -226,10 +228,12 @@ if (itUs->second.IsFresh() && it->second.coin.IsSpent()) { // The grandparent cache does not have an entry, and the coin // has been spent. We can just delete it from the parent cache. + Assert(cachedCoinsUsage >= itUs->second.coin.DynamicMemoryUsage()); cachedCoinsUsage -= itUs->second.coin.DynamicMemoryUsage(); cacheCoins.erase(itUs); } else { // A normal modification. + Assert(cachedCoinsUsage >= itUs->second.coin.DynamicMemoryUsage()); cachedCoinsUsage -= itUs->second.coin.DynamicMemoryUsage(); if (cursor.WillErase(*it)) { // Since this entry will be erased, @@ -279,6 +283,7 @@ { CCoinsMap::iterator it = cacheCoins.find(hash); if (it != cacheCoins.end() && !it->second.IsDirty() && !it->second.IsFresh()) { + Assert(cachedCoinsUsage >= it->second.coin.DynamicMemoryUsage()); cachedCoinsUsage -= it->second.coin.DynamicMemoryUsage(); TRACEPOINT(utxocache, uncache, hash.hash.data(), ``` </details> ACKs for top commit: optout21: reACK 24d861d andrewtoth: ACK 24d861d sipa: ACK 24d861d w0xlt: ACK bitcoin/bitcoin@24d861d Tree-SHA512: ff1b756b46220f278ab6c850626a0f376bed64389ef7f66a95c994e1c7cceec1d1843d2b24e8deabe10e2bdade2a274d9654ac60eb2b9bf471a71db8a2ff496c
2 parents 40e7d4c + 24d861d commit f76e1ae

File tree

5 files changed

+66
-41
lines changed

5 files changed

+66
-41
lines changed

src/coins.cpp

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,6 @@ void CCoinsViewCache::AddCoin(const COutPoint &outpoint, Coin&& coin, bool possi
7676
bool inserted;
7777
std::tie(it, inserted) = cacheCoins.emplace(std::piecewise_construct, std::forward_as_tuple(outpoint), std::tuple<>());
7878
bool fresh = false;
79-
if (!inserted) {
80-
cachedCoinsUsage -= it->second.coin.DynamicMemoryUsage();
81-
}
8279
if (!possible_overwrite) {
8380
if (!it->second.coin.IsSpent()) {
8481
throw std::logic_error("Attempted to overwrite an unspent coin (when possible_overwrite is false)");
@@ -98,6 +95,9 @@ void CCoinsViewCache::AddCoin(const COutPoint &outpoint, Coin&& coin, bool possi
9895
// DIRTY, then it can be marked FRESH.
9996
fresh = !it->second.IsDirty();
10097
}
98+
if (!inserted) {
99+
cachedCoinsUsage -= it->second.coin.DynamicMemoryUsage();
100+
}
101101
it->second.coin = std::move(coin);
102102
CCoinsCacheEntry::SetDirty(*it, m_sentinel);
103103
if (fresh) CCoinsCacheEntry::SetFresh(*it, m_sentinel);
@@ -111,9 +111,12 @@ void CCoinsViewCache::AddCoin(const COutPoint &outpoint, Coin&& coin, bool possi
111111
}
112112

113113
void CCoinsViewCache::EmplaceCoinInternalDANGER(COutPoint&& outpoint, Coin&& coin) {
114-
cachedCoinsUsage += coin.DynamicMemoryUsage();
114+
const auto mem_usage{coin.DynamicMemoryUsage()};
115115
auto [it, inserted] = cacheCoins.try_emplace(std::move(outpoint), std::move(coin));
116-
if (inserted) CCoinsCacheEntry::SetDirty(*it, m_sentinel);
116+
if (inserted) {
117+
CCoinsCacheEntry::SetDirty(*it, m_sentinel);
118+
cachedCoinsUsage += mem_usage;
119+
}
117120
}
118121

119122
void AddCoins(CCoinsViewCache& cache, const CTransaction &tx, int nHeight, bool check_for_overwrite) {
@@ -195,6 +198,7 @@ bool CCoinsViewCache::BatchWrite(CoinsViewCacheCursor& cursor, const uint256 &ha
195198
// and mark it as dirty.
196199
itUs = cacheCoins.try_emplace(it->first).first;
197200
CCoinsCacheEntry& entry{itUs->second};
201+
assert(entry.coin.DynamicMemoryUsage() == 0);
198202
if (cursor.WillErase(*it)) {
199203
// Since this entry will be erased,
200204
// we can move the coin into us instead of copying it
@@ -248,19 +252,19 @@ bool CCoinsViewCache::BatchWrite(CoinsViewCacheCursor& cursor, const uint256 &ha
248252
}
249253

250254
bool CCoinsViewCache::Flush() {
251-
auto cursor{CoinsViewCacheCursor(cachedCoinsUsage, m_sentinel, cacheCoins, /*will_erase=*/true)};
255+
auto cursor{CoinsViewCacheCursor(m_sentinel, cacheCoins, /*will_erase=*/true)};
252256
bool fOk = base->BatchWrite(cursor, hashBlock);
253257
if (fOk) {
254258
cacheCoins.clear();
255259
ReallocateCache();
260+
cachedCoinsUsage = 0;
256261
}
257-
cachedCoinsUsage = 0;
258262
return fOk;
259263
}
260264

261265
bool CCoinsViewCache::Sync()
262266
{
263-
auto cursor{CoinsViewCacheCursor(cachedCoinsUsage, m_sentinel, cacheCoins, /*will_erase=*/false)};
267+
auto cursor{CoinsViewCacheCursor(m_sentinel, cacheCoins, /*will_erase=*/false)};
264268
bool fOk = base->BatchWrite(cursor, hashBlock);
265269
if (fOk) {
266270
if (m_sentinel.second.Next() != &m_sentinel) {

src/coins.h

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -271,11 +271,10 @@ struct CoinsViewCacheCursor
271271
//! This is an optimization compared to erasing all entries as the cursor iterates them when will_erase is set.
272272
//! Calling CCoinsMap::clear() afterwards is faster because a CoinsCachePair cannot be coerced back into a
273273
//! CCoinsMap::iterator to be erased, and must therefore be looked up again by key in the CCoinsMap before being erased.
274-
CoinsViewCacheCursor(size_t& usage LIFETIMEBOUND,
275-
CoinsCachePair& sentinel LIFETIMEBOUND,
276-
CCoinsMap& map LIFETIMEBOUND,
277-
bool will_erase) noexcept
278-
: m_usage(usage), m_sentinel(sentinel), m_map(map), m_will_erase(will_erase) {}
274+
CoinsViewCacheCursor(CoinsCachePair& sentinel LIFETIMEBOUND,
275+
CCoinsMap& map LIFETIMEBOUND,
276+
bool will_erase) noexcept
277+
: m_sentinel(sentinel), m_map(map), m_will_erase(will_erase) {}
279278

280279
inline CoinsCachePair* Begin() const noexcept { return m_sentinel.second.Next(); }
281280
inline CoinsCachePair* End() const noexcept { return &m_sentinel; }
@@ -288,7 +287,7 @@ struct CoinsViewCacheCursor
288287
// Otherwise, clear the state of the entry.
289288
if (!m_will_erase) {
290289
if (current.second.coin.IsSpent()) {
291-
m_usage -= current.second.coin.DynamicMemoryUsage();
290+
assert(current.second.coin.DynamicMemoryUsage() == 0); // scriptPubKey was already cleared in SpendCoin
292291
m_map.erase(current.first);
293292
} else {
294293
current.second.SetClean();
@@ -299,7 +298,6 @@ struct CoinsViewCacheCursor
299298

300299
inline bool WillErase(CoinsCachePair& current) const noexcept { return m_will_erase || current.second.coin.IsSpent(); }
301300
private:
302-
size_t& m_usage;
303301
CoinsCachePair& m_sentinel;
304302
CCoinsMap& m_map;
305303
bool m_will_erase;

src/test/coins_tests.cpp

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -662,8 +662,8 @@ static void WriteCoinsViewEntry(CCoinsView& view, const MaybeCoin& cache_coin)
662662
sentinel.second.SelfRef(sentinel);
663663
CCoinsMapMemoryResource resource;
664664
CCoinsMap map{0, CCoinsMap::hasher{}, CCoinsMap::key_equal{}, &resource};
665-
auto usage{cache_coin ? InsertCoinsMapEntry(map, sentinel, *cache_coin) : 0};
666-
auto cursor{CoinsViewCacheCursor(usage, sentinel, map, /*will_erase=*/true)};
665+
if (cache_coin) InsertCoinsMapEntry(map, sentinel, *cache_coin);
666+
auto cursor{CoinsViewCacheCursor(sentinel, map, /*will_erase=*/true)};
667667
BOOST_CHECK(view.BatchWrite(cursor, {}));
668668
}
669669

@@ -1085,4 +1085,40 @@ BOOST_AUTO_TEST_CASE(coins_resource_is_used)
10851085
PoolResourceTester::CheckAllDataAccountedFor(resource);
10861086
}
10871087

1088+
BOOST_AUTO_TEST_CASE(ccoins_addcoin_exception_keeps_usage_balanced)
1089+
{
1090+
CCoinsView root;
1091+
CCoinsViewCacheTest cache{&root};
1092+
1093+
const COutPoint outpoint{Txid::FromUint256(m_rng.rand256()), m_rng.rand32()};
1094+
1095+
const Coin coin1{CTxOut{m_rng.randrange(10), CScript{} << m_rng.randbytes(CScriptBase::STATIC_SIZE + 1)}, 1, false};
1096+
cache.AddCoin(outpoint, Coin{coin1}, /*possible_overwrite=*/false);
1097+
cache.SelfTest();
1098+
1099+
const Coin coin2{CTxOut{m_rng.randrange(20), CScript{} << m_rng.randbytes(CScriptBase::STATIC_SIZE + 2)}, 2, false};
1100+
BOOST_CHECK_THROW(cache.AddCoin(outpoint, Coin{coin2}, /*possible_overwrite=*/false), std::logic_error);
1101+
cache.SelfTest();
1102+
1103+
BOOST_CHECK(cache.AccessCoin(outpoint) == coin1);
1104+
}
1105+
1106+
BOOST_AUTO_TEST_CASE(ccoins_emplace_duplicate_keeps_usage_balanced)
1107+
{
1108+
CCoinsView root;
1109+
CCoinsViewCacheTest cache{&root};
1110+
1111+
const COutPoint outpoint{Txid::FromUint256(m_rng.rand256()), m_rng.rand32()};
1112+
1113+
const Coin coin1{CTxOut{m_rng.randrange(10), CScript{} << m_rng.randbytes(CScriptBase::STATIC_SIZE + 1)}, 1, false};
1114+
cache.EmplaceCoinInternalDANGER(COutPoint{outpoint}, Coin{coin1});
1115+
cache.SelfTest();
1116+
1117+
const Coin coin2{CTxOut{m_rng.randrange(20), CScript{} << m_rng.randbytes(CScriptBase::STATIC_SIZE + 2)}, 2, false};
1118+
cache.EmplaceCoinInternalDANGER(COutPoint{outpoint}, Coin{coin2});
1119+
cache.SelfTest();
1120+
1121+
BOOST_CHECK(cache.AccessCoin(outpoint) == coin1);
1122+
}
1123+
10881124
BOOST_AUTO_TEST_SUITE_END()

src/test/fuzz/coins_view.cpp

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -59,25 +59,19 @@ void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsView& backend
5959
if (random_coin.IsSpent()) {
6060
return;
6161
}
62-
Coin coin = random_coin;
63-
bool expected_code_path = false;
64-
const bool possible_overwrite = fuzzed_data_provider.ConsumeBool();
65-
try {
66-
coins_view_cache.AddCoin(random_out_point, std::move(coin), possible_overwrite);
67-
expected_code_path = true;
68-
} catch (const std::logic_error& e) {
69-
if (e.what() == std::string{"Attempted to overwrite an unspent coin (when possible_overwrite is false)"}) {
62+
COutPoint outpoint{random_out_point};
63+
Coin coin{random_coin};
64+
if (fuzzed_data_provider.ConsumeBool()) {
65+
const bool possible_overwrite{fuzzed_data_provider.ConsumeBool()};
66+
try {
67+
coins_view_cache.AddCoin(outpoint, std::move(coin), possible_overwrite);
68+
} catch (const std::logic_error& e) {
69+
assert(e.what() == std::string{"Attempted to overwrite an unspent coin (when possible_overwrite is false)"});
7070
assert(!possible_overwrite);
71-
expected_code_path = true;
72-
// AddCoin() decreases cachedCoinsUsage by the memory usage of the old coin at the beginning and
73-
// increases it by the value of the new coin at the end. If it throws in the process, the value
74-
// of cachedCoinsUsage would have been incorrectly decreased, leading to an underflow later on.
75-
// To avoid this, use Flush() to reset the value of cachedCoinsUsage in sync with the cacheCoins
76-
// mapping.
77-
(void)coins_view_cache.Flush();
7871
}
72+
} else {
73+
coins_view_cache.EmplaceCoinInternalDANGER(std::move(outpoint), std::move(coin));
7974
}
80-
assert(expected_code_path);
8175
},
8276
[&] {
8377
(void)coins_view_cache.Flush();
@@ -131,7 +125,6 @@ void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsView& backend
131125
[&] {
132126
CoinsCachePair sentinel{};
133127
sentinel.second.SelfRef(sentinel);
134-
size_t usage{0};
135128
CCoinsMapMemoryResource resource;
136129
CCoinsMap coins_map{0, SaltedOutpointHasher{/*deterministic=*/true}, CCoinsMap::key_equal{}, &resource};
137130
LIMITED_WHILE(good_data && fuzzed_data_provider.ConsumeBool(), 10'000)
@@ -152,11 +145,10 @@ void TestCoinsView(FuzzedDataProvider& fuzzed_data_provider, CCoinsView& backend
152145
auto it{coins_map.emplace(random_out_point, std::move(coins_cache_entry)).first};
153146
if (dirty) CCoinsCacheEntry::SetDirty(*it, sentinel);
154147
if (fresh) CCoinsCacheEntry::SetFresh(*it, sentinel);
155-
usage += it->second.coin.DynamicMemoryUsage();
156148
}
157149
bool expected_code_path = false;
158150
try {
159-
auto cursor{CoinsViewCacheCursor(usage, sentinel, coins_map, /*will_erase=*/true)};
151+
auto cursor{CoinsViewCacheCursor(sentinel, coins_map, /*will_erase=*/true)};
160152
uint256 best_block{coins_view_cache.GetBestBlock()};
161153
if (fuzzed_data_provider.ConsumeBool()) best_block = ConsumeUInt256(fuzzed_data_provider);
162154
// Set best block hash to non-null to satisfy the assertion in CCoinsViewDB::BatchWrite().

test/sanitizer_suppressions/ubsan

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,6 @@ unsigned-integer-overflow:arith_uint256.h
4747
unsigned-integer-overflow:CBloomFilter::Hash
4848
unsigned-integer-overflow:CRollingBloomFilter::insert
4949
unsigned-integer-overflow:RollingBloomHash
50-
unsigned-integer-overflow:CCoinsViewCache::AddCoin
51-
unsigned-integer-overflow:CCoinsViewCache::BatchWrite
52-
unsigned-integer-overflow:CCoinsViewCache::DynamicMemoryUsage
53-
unsigned-integer-overflow:CCoinsViewCache::SpendCoin
54-
unsigned-integer-overflow:CCoinsViewCache::Uncache
5550
unsigned-integer-overflow:CompressAmount
5651
unsigned-integer-overflow:DecompressAmount
5752
unsigned-integer-overflow:crypto/

0 commit comments

Comments
 (0)