validation: handle null mempool on delete
What changed, and why it matters
This commit fixes a crash bug in Bitcoin Core's chainstate manager. When deleting a temporary chainstate used for AssumeUTXO loading, the code assumed every chainstate had a memory pool (mempool) attached. In the 'kernel' library mode, a ChainstateManager can be created without a mempool, so deleting a chainstate dereferenced a null pointer and crashed. The fix simply checks whether the mempool exists before checking its size, and adds a test for this lifecycle.
No immediate user action required; this is a crash fix for an internal API path. Developers using the Kernel API or AssumeUTXO workflows should update to include this commit. Consider backporting to relevant maintenance branches if the Kernel API is exposed.
Security signals we found
Null-pointer dereference in validation code path
Crash/assertion failure during chainstate deletion
AssumeUTXO / kernel-specific code path affected
Defensive null check added to existing assertion
Evidence from the diff
In ChainstateManager::DeleteChainstate(), the assertion assert(prev_chainstate->m_mempool->size() == 0) unconditionally dereferenced prev_chainstate->m_mempool. When the ChainstateManager was constructed without a mempool (valid for the Kernel API), prev_chainstate->m_mempool is nullptr, causing a null-pointer dereference/crash during chainstate deletion. The patch changes the assertion to assert(!prev_chainstate->m_mempool || prev_chainstate->m_mempool->size() == 0) and adds a unit test covering the reindex/wipe lifecycle with a null mempool.
Changed components
src/validation.cpp ChainstateManager::DeleteChainstate()AssumeUTXO chainstate loading/deletion lifecycleKernel API ChainstateManager without mempoolInspect captured patch +10 / −1
diff --git a/src/test/validation_chainstatemanager_tests.cpp b/src/test/validation_chainstatemanager_tests.cpp
index eae3fcdc..9b14344c 100644
--- a/src/test/validation_chainstatemanager_tests.cpp
+++ b/src/test/validation_chainstatemanager_tests.cpp
@@ -117,6 +117,15 @@ BOOST_FIXTURE_TEST_CASE(chainstatemanager, TestChain100Setup)
m_node.validation_signals->SyncWithValidationInterfaceQueue();
}
+BOOST_FIXTURE_TEST_CASE(chainstatemanager_delete_chainstate_no_mempool, ChainTestingSetup)
+{
+ auto& manager{*Assert(m_node.chainman)};
+ auto& validated{WITH_LOCK(::cs_main, return manager.InitializeChainstate(/*mempool=*/nullptr))};
+ auto& snapshot{WITH_LOCK(::cs_main, return manager.AddChainstate(std::make_unique<Chainstate>(nullptr, manager.m_blockman, manager, uint256::ONE)))};
+ WITH_LOCK(::cs_main, validated.SetTargetBlock(nullptr));
+ BOOST_CHECK(WITH_LOCK(::cs_main, return manager.DeleteChainstate(snapshot))); // Accept Kernel's null mempool
+}
+
//! Test rebalancing the caches associated with each chainstate.
BOOST_FIXTURE_TEST_CASE(chainstatemanager_rebalance_caches, TestChain100Setup)
{
diff --git a/src/validation.cpp b/src/validation.cpp
index 924b7f60..a21e8baf 100644
--- a/src/validation.cpp
+++ b/src/validation.cpp
@@ -6268,7 +6268,7 @@ bool ChainstateManager::DeleteChainstate(Chainstate& chainstate)
}
std::unique_ptr<Chainstate> prev_chainstate{Assert(RemoveChainstate(chainstate))};
Chainstate& curr_chainstate{CurrentChainstate()};
- assert(prev_chainstate->m_mempool->size() == 0);
+ assert(!prev_chainstate->m_mempool || prev_chainstate->m_mempool->size() == 0);
assert(!curr_chainstate.m_mempool);
std::swap(curr_chainstate.m_mempool, prev_chainstate->m_mempool);
return true;
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.