Skip to content

validation: prevent FindMostWorkChain from causing UB - #317

Open
1440000bytes wants to merge 2 commits into
bitcoinknots:29.x-knotsfrom
1440000bytes:fix/blocks-unlinked-ub-35070
Open

validation: prevent FindMostWorkChain from causing UB#317
1440000bytes wants to merge 2 commits into
bitcoinknots:29.x-knotsfrom
1440000bytes:fix/blocks-unlinked-ub-35070

Conversation

@1440000bytes

Copy link
Copy Markdown

See bitcoin#35070 for details

stratospher and others added 2 commits April 12, 2026 20:22
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)
Comment thread src/validation.cpp
assert(!foundInUnlinked); // No duplicates in m_blocks_unlinked
foundInUnlinked = true;
break;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@kwsantiago

kwsantiago commented Jul 17, 2026

Copy link
Copy Markdown

Any plans to pick this back up? This is a strong candidate for backporting.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_unlinked from Chainstate::FindMostWorkChain() to prevent duplicate processing in ReceivedBlockTransactions().
  • Strengthen CheckBlockIndex() to assert that m_blocks_unlinked contains 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.

Comment thread test/functional/feature_pruning.py
@1440000bytes

Copy link
Copy Markdown
Author

@luke-jr

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants