rpc: fix race condition in gettxoutsetinfo
What changed, and why it matters
This patch fixes a crash in a Bitcoin Core RPC command called gettxoutsetinfo. The command reports statistics about the set of unspent transaction outputs (UTXOs). The bug was a timing issue: the code briefly noted the current best block while holding one lock, then released that lock and later compared the saved block to a newer best block. If the blockchain advanced in between, an internal consistency check could fail and crash the node. The fix removes the stale saved block reference and instead uses the block that the statistics were actually computed against.
Apply the patch. It is a minimal, targeted fix. RPC operators should upgrade to avoid node crashes when gettxoutsetinfo is called while the chain tip is advancing. No immediate incident response is indicated beyond normal patching.
Security signals we found
Assertion failure / crash in RPC path (denial of service vector for RPC users)
Race condition between cs_main release and best-block advancement
Use of stale pindex pointer after lock release
Fix references upstream issue #34263
Evidence from the diff
gettxoutsetinfo captured pindex via blockman->LookupBlockIndex(coins_view->GetBestBlock()) while holding cs_main, then released cs_main before calling ComputeUTXOStats()/GetUTXOStats(). If the tip advanced during that window, the later assertion CHECK_NONFATAL(pindex) in GetUTXOStats() could fire because the newer best block’s hash was not found via the stale pindex, or related height/prev checks could mismatch. The patch removes the early pindex capture and instead derives the block index from stats.hashBlock under cs_main when needed, using stats.nHeight for the previous-stats height check. This ensures the block index used for the previous-block lookup matches the actual UTXO snapshot.
Changed components
src/rpc/blockchain.cppgettxoutsetinfo RPCComputeUTXOStats / GetUTXOStatsCCoinsStatsInspect captured patch +4 / −4
diff --git a/src/rpc/blockchain.cpp b/src/rpc/blockchain.cpp
index d97b7c6c..c631a936 100644
--- a/src/rpc/blockchain.cpp
+++ b/src/rpc/blockchain.cpp
@@ -1080,7 +1080,6 @@ static RPCHelpMan gettxoutsetinfo()
LOCK(::cs_main);
coins_view = &active_chainstate.CoinsDB();
blockman = &active_chainstate.m_blockman;
- pindex = blockman->LookupBlockIndex(coins_view->GetBestBlock());
}
if (!request.params[1].isNull()) {
@@ -1104,7 +1103,7 @@ static RPCHelpMan gettxoutsetinfo()
// If a specific block was requested and the index has already synced past that height, we can return the
// data already even though the index is not fully synced yet.
- if (pindex->nHeight > summary.best_block_height) {
+ if (pindex && pindex->nHeight > summary.best_block_height) {
throw JSONRPCError(RPC_INTERNAL_ERROR, strprintf("Unable to get data because coinstatsindex is still syncing. Current height: %d", summary.best_block_height));
}
}
@@ -1130,8 +1129,9 @@ static RPCHelpMan gettxoutsetinfo()
ret.pushKV("disk_size", stats.nDiskSize);
} else {
CCoinsStats prev_stats{};
- if (pindex->nHeight > 0) {
- const std::optional<CCoinsStats> maybe_prev_stats = GetUTXOStats(coins_view, *blockman, hash_type, node.rpc_interruption_point, pindex->pprev, index_requested);
+ if (stats.nHeight > 0) {
+ const CBlockIndex& block_index = *CHECK_NONFATAL(WITH_LOCK(::cs_main, return blockman->LookupBlockIndex(stats.hashBlock)));
+ const std::optional<CCoinsStats> maybe_prev_stats = GetUTXOStats(coins_view, *blockman, hash_type, node.rpc_interruption_point, block_index.pprev, index_requested);
if (!maybe_prev_stats) {
throw JSONRPCError(RPC_INTERNAL_ERROR, "Unable to read UTXO set");
}
Why this scored 32/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.