validation: prevent FindMostWorkChain from causing UB - #317
Conversation
In a pruned node undergoing a deep reorg, FindMostWorkChain can insert duplicate entries into m_blocks_unlinked. This can happen when: - Traversing from one candidate tip to the fork point adds blocks whose parents have been pruned. - Traversing from another candidate tip over the same fork inserts the same pairs again, since the blocks are shared across both branches. When we finally download the missing parent from our peer and call ReceivedBlockTransactions to process m_blocks_unlinked, the same entry may be processed multiple times. This can lead to re-insertion into setBlockIndexCandidates with a modified nSequenceId, violating its ordering invariants and causing undefined behavior. So avoid duplicate insertions into m_blocks_unlinked here. Co-authored-by: Martin Zumsande <mzumsande@gmail.com> (cherry picked from commit c787b3b)
when pruned node2 finally receives the blocks from node0, node2 will process duplicate entires in ReceivedBlockTransactions twice but it will only insert into setBlockIndexCandidates if the block has more work that the current chain tip. the duplicate entries in m_blocks_unlinked in this test are from height 1171 to 1294. before this commit - we invalidated height 1320 and chain tip became 1319. so we won't add duplicate entries (all have <1319) into setBlockIndexCandidates and won't have coverage for this UB scenario. with this commit - we invalidate height 1295 and chain tip became 1294. so we will process the duplicate entry 1294 in m_blocks_unlinked and have coverage for this UB. Co-authored-by: Martin Zumsande <mzumsande@gmail.com> (cherry picked from commit ca4a380)
| assert(!foundInUnlinked); // No duplicates in m_blocks_unlinked | ||
| foundInUnlinked = true; | ||
| break; | ||
| } |
There was a problem hiding this comment.
This assert now enforces a global no-duplicates invariant, but the dedup only lives in FindMostWorkChain. The two other insert sites (ReceivedBlockTransactions and LoadBlockIndex) stay raw and are thus load-bearing for this assert without defending it themselves. They are safe today (disjoint block sets: 4316 only runs when pprev lacks a chain tx count, which FindMostWorkChain candidates always have), but a future change to those preconditions would surface only as an assert-crash under -checkblockindex. Upstream avoided the coupling with a shared AddUnlinkedBlock() helper. A one-line comment at the raw insert noting the invariant would suffice if not adopting the helper.
|
Any plans to pick this back up? This is a strong candidate for backporting. |
There was a problem hiding this comment.
Pull request overview
This PR addresses a potential undefined-behavior scenario in chain selection by preventing duplicate (pprev -> child) entries in m_blocks_unlinked, which could otherwise lead to mutating CBlockIndex ordering fields while the index is stored in setBlockIndexCandidates.
Changes:
- Add de-duplication when (re)inserting entries into
m_blocks_unlinkedfromChainstate::FindMostWorkChain()to prevent duplicate processing inReceivedBlockTransactions(). - Strengthen
CheckBlockIndex()to assert thatm_blocks_unlinkedcontains no duplicate entries for the same(pprev, pindex)pair. - Adjust the pruning functional test’s invalidation target to avoid the problematic scenario.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
test/functional/feature_pruning.py |
Updates invalidation behavior used to trigger reorg/redownload during pruning test. |
src/validation.cpp |
Prevents duplicate m_blocks_unlinked entries that can trigger UB via candidate ordering mutations; adds an invariant assert in CheckBlockIndex(). |
Comments suppressed due to low confidence (1)
test/functional/feature_pruning.py:258
- Keep the same computed invalidate hash for node 0, rather than relying on a height-specific variable name.
self.nodes[0].invalidateblock(block_hash_1295)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
See bitcoin#35070 for details