Skip to content

fix(legacy/netsync): bounds-check parent output index in ExtendTransaction#768

Merged
ordishs merged 3 commits into
bsv-blockchain:mainfrom
ordishs:security/4564-extend-tx-oob
May 14, 2026
Merged

fix(legacy/netsync): bounds-check parent output index in ExtendTransaction#768
ordishs merged 3 commits into
bsv-blockchain:mainfrom
ordishs:security/4564-extend-tx-oob

Conversation

@ordishs

@ordishs ordishs commented Apr 28, 2026

Copy link
Copy Markdown
Collaborator

Closes #4564.

Summary

ExtendTransaction dereferences prevTxWrapper.Tx.Outputs[input.PreviousTxOutIndex] without a bounds check. A peer can send a block where a child tx references its in-block parent with PreviousTxOutIndex >= len(parent.Outputs), panicking the node before full block validation runs. The phase-1 path (extendFromTxMap, around line 1011) already had this guard; the parallel path in ExtendTransaction (lines ~1272-1273) was missed.

Fix

Insert the same shape of bounds check that exists in extendFromTxMap, immediately before the dereference. On out-of-range, return errors.NewTxInvalidError(...) with the full context (offending tx, input index, parent, output count). The block came from a peer; the transaction is invalid, so TxInvalidError is more semantically correct than ProcessingError.

Test plan

  • Regression test TestSyncManager_ExtendTransaction_OOB constructs a parent with 2 outputs and a child whose input has PreviousTxOutIndex == 5; pre-fix panics with index out of range [5] with length 2; post-fix returns TxInvalidError with the expected message.
  • go test -race ./services/legacy/netsync/... passes.

@github-actions

github-actions Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

🤖 Claude Code Review

Status: Complete

This PR adds critical bounds checking for parent output indices in ExtendTransaction, fixing a panic vulnerability (issue #4564). The fix is semantically correct, well-tested, and consistent with the existing guard in the parallel code path.

No issues found.

The implementation:

  • Correctly uses TxInvalidError instead of ProcessingError (peer sent invalid data)
  • Moves the check before the 120s wait loop (fail-fast optimization)
  • Adds comprehensive test coverage for both code paths
  • Unifies error messages across both functions

@ordishs ordishs force-pushed the security/4564-extend-tx-oob branch from 143dda4 to 799533e Compare April 28, 2026 16:53
@ordishs ordishs changed the title fix(legacy/netsync): bounds-check parent output index in extendPerTxFallback fix(legacy/netsync): bounds-check parent output index in ExtendTransaction Apr 28, 2026
@github-actions

github-actions Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Benchmark Comparison Report

Baseline: main (unknown)

Current: PR-768 (3e69894)

Summary

  • Regressions: 0
  • Improvements: 0
  • Unchanged: 142
  • Significance level: p < 0.05
All benchmark results (sec/op)
Benchmark Baseline Current Change p-value
_NewBlockFromBytes-4 1.689µ 1.847µ ~ 0.100
SplitSyncedParentMap_SetIfNotExists/256_buckets-4 61.79n 61.71n ~ 0.400
SplitSyncedParentMap_SetIfNotExists/16_buckets-4 61.73n 61.67n ~ 0.700
SplitSyncedParentMap_SetIfNotExists/1_bucket-4 61.54n 61.50n ~ 1.000
SplitSyncedParentMap_ConcurrentSetIfNotExists/256_buckets... 30.90n 31.22n ~ 1.000
SplitSyncedParentMap_ConcurrentSetIfNotExists/16_buckets_... 53.43n 52.43n ~ 0.100
SplitSyncedParentMap_ConcurrentSetIfNotExists/1_bucket_pa... 107.2n 106.9n ~ 0.700
MiningCandidate_Stringify_Short-4 265.1n 263.9n ~ 0.300
MiningCandidate_Stringify_Long-4 1.904µ 1.916µ ~ 0.100
MiningSolution_Stringify-4 981.7n 988.2n ~ 0.700
BlockInfo_MarshalJSON-4 1.781µ 1.780µ ~ 0.800
NewFromBytes-4 130.8n 143.0n ~ 0.700
Mine_EasyDifficulty-4 60.11µ 60.83µ ~ 0.100
Mine_WithAddress-4 7.535µ 6.731µ ~ 0.700
BlockAssembler_AddTx-4 0.02640n 0.02691n ~ 1.000
AddNode-4 10.98 10.74 ~ 0.100
AddNodeWithMap-4 11.13 10.80 ~ 0.200
DirectSubtreeAdd/4_per_subtree-4 59.70n 60.41n ~ 0.700
DirectSubtreeAdd/64_per_subtree-4 29.32n 29.64n ~ 0.700
DirectSubtreeAdd/256_per_subtree-4 28.13n 27.89n ~ 0.400
DirectSubtreeAdd/1024_per_subtree-4 26.86n 26.55n ~ 0.100
DirectSubtreeAdd/2048_per_subtree-4 26.34n 26.11n ~ 0.100
SubtreeProcessorAdd/4_per_subtree-4 287.9n 288.3n ~ 1.000
SubtreeProcessorAdd/64_per_subtree-4 280.7n 278.0n ~ 0.400
SubtreeProcessorAdd/256_per_subtree-4 277.4n 284.1n ~ 0.100
SubtreeProcessorAdd/1024_per_subtree-4 270.3n 276.2n ~ 0.100
SubtreeProcessorAdd/2048_per_subtree-4 270.4n 269.5n ~ 1.000
SubtreeProcessorRotate/4_per_subtree-4 278.2n 272.5n ~ 0.200
SubtreeProcessorRotate/64_per_subtree-4 277.0n 273.8n ~ 0.400
SubtreeProcessorRotate/256_per_subtree-4 276.8n 274.6n ~ 0.400
SubtreeProcessorRotate/1024_per_subtree-4 278.6n 272.0n ~ 0.100
SubtreeNodeAddOnly/4_per_subtree-4 55.88n 55.36n ~ 0.100
SubtreeNodeAddOnly/64_per_subtree-4 36.28n 36.42n ~ 0.300
SubtreeNodeAddOnly/256_per_subtree-4 35.15n 35.27n ~ 0.300
SubtreeNodeAddOnly/1024_per_subtree-4 34.55n 34.59n ~ 0.700
SubtreeCreationOnly/4_per_subtree-4 111.7n 111.1n ~ 0.400
SubtreeCreationOnly/64_per_subtree-4 355.4n 350.5n ~ 0.700
SubtreeCreationOnly/256_per_subtree-4 1.243µ 1.228µ ~ 0.700
SubtreeCreationOnly/1024_per_subtree-4 3.865µ 3.953µ ~ 0.400
SubtreeCreationOnly/2048_per_subtree-4 6.929µ 7.317µ ~ 0.200
SubtreeProcessorOverheadBreakdown/64_per_subtree-4 278.7n 274.9n ~ 0.100
SubtreeProcessorOverheadBreakdown/1024_per_subtree-4 294.0n 274.2n ~ 0.100
ParallelGetAndSetIfNotExists/1k_nodes-4 565.2µ 560.2µ ~ 0.100
ParallelGetAndSetIfNotExists/10k_nodes-4 1.392m 1.373m ~ 0.400
ParallelGetAndSetIfNotExists/50k_nodes-4 6.799m 6.751m ~ 0.100
ParallelGetAndSetIfNotExists/100k_nodes-4 13.45m 13.54m ~ 0.100
SequentialGetAndSetIfNotExists/1k_nodes-4 648.3µ 640.2µ ~ 0.200
SequentialGetAndSetIfNotExists/10k_nodes-4 3.086m 3.005m ~ 1.000
SequentialGetAndSetIfNotExists/50k_nodes-4 12.57m 11.85m ~ 0.100
SequentialGetAndSetIfNotExists/100k_nodes-4 23.91m 22.36m ~ 0.100
ProcessOwnBlockSubtreeNodesParallel/1k_nodes-4 635.0µ 596.7µ ~ 0.100
ProcessOwnBlockSubtreeNodesParallel/10k_nodes-4 4.737m 4.572m ~ 0.100
ProcessOwnBlockSubtreeNodesParallel/100k_nodes-4 16.98m 17.05m ~ 0.700
ProcessOwnBlockSubtreeNodesSequential/1k_nodes-4 687.5µ 678.2µ ~ 0.700
ProcessOwnBlockSubtreeNodesSequential/10k_nodes-4 6.664m 6.435m ~ 0.200
ProcessOwnBlockSubtreeNodesSequential/100k_nodes-4 40.42m 39.41m ~ 0.100
DiskTxMap_SetIfNotExists-4 3.878µ 3.939µ ~ 0.700
DiskTxMap_SetIfNotExists_Parallel-4 3.746µ 3.845µ ~ 0.700
DiskTxMap_ExistenceOnly-4 348.2n 355.9n ~ 0.200
Queue-4 194.7n 194.3n ~ 1.000
AtomicPointer-4 3.728n 3.664n ~ 0.700
ReorgOptimizations/DedupFilterPipeline/Old/10K-4 816.8µ 815.8µ ~ 0.700
ReorgOptimizations/DedupFilterPipeline/New/10K-4 788.2µ 795.3µ ~ 0.100
ReorgOptimizations/AllMarkFalse/Old/10K-4 116.5µ 110.3µ ~ 0.700
ReorgOptimizations/AllMarkFalse/New/10K-4 64.21µ 66.64µ ~ 0.100
ReorgOptimizations/HashSlicePool/Old/10K-4 68.41µ 60.54µ ~ 0.200
ReorgOptimizations/HashSlicePool/New/10K-4 11.37µ 10.99µ ~ 0.100
ReorgOptimizations/NodeFlags/Old/10K-4 5.171µ 5.401µ ~ 0.100
ReorgOptimizations/NodeFlags/New/10K-4 1.899µ 1.843µ ~ 0.700
ReorgOptimizations/DedupFilterPipeline/Old/100K-4 10.313m 9.871m ~ 0.200
ReorgOptimizations/DedupFilterPipeline/New/100K-4 10.068m 9.937m ~ 0.400
ReorgOptimizations/AllMarkFalse/Old/100K-4 1.169m 1.151m ~ 0.700
ReorgOptimizations/AllMarkFalse/New/100K-4 705.2µ 711.2µ ~ 0.100
ReorgOptimizations/HashSlicePool/Old/100K-4 571.9µ 665.6µ ~ 0.100
ReorgOptimizations/HashSlicePool/New/100K-4 214.0µ 206.9µ ~ 0.400
ReorgOptimizations/NodeFlags/Old/100K-4 55.19µ 55.38µ ~ 0.700
ReorgOptimizations/NodeFlags/New/100K-4 18.67µ 19.03µ ~ 0.400
TxMapSetIfNotExists-4 46.27n 46.39n ~ 0.700
TxMapSetIfNotExistsDuplicate-4 38.82n 38.67n ~ 0.700
ChannelSendReceive-4 608.8n 579.6n ~ 0.100
CalcBlockWork-4 544.5n 500.8n ~ 1.000
CalculateWork-4 675.2n 677.8n ~ 0.100
BuildBlockLocatorString_Helpers/Size_10-4 1.292µ 1.378µ ~ 0.700
BuildBlockLocatorString_Helpers/Size_100-4 12.45µ 12.33µ ~ 0.700
BuildBlockLocatorString_Helpers/Size_1000-4 123.6µ 122.8µ ~ 0.100
CatchupWithHeaderCache-4 104.8m 104.5m ~ 0.100
_BufferPoolAllocation/16KB-4 3.394µ 3.429µ ~ 0.400
_BufferPoolAllocation/32KB-4 7.258µ 7.759µ ~ 0.100
_BufferPoolAllocation/64KB-4 15.18µ 15.13µ ~ 0.400
_BufferPoolAllocation/128KB-4 31.49µ 29.42µ ~ 0.100
_BufferPoolAllocation/512KB-4 106.3µ 112.0µ ~ 0.400
_BufferPoolConcurrent/32KB-4 19.29µ 19.38µ ~ 0.400
_BufferPoolConcurrent/64KB-4 30.02µ 31.15µ ~ 0.100
_BufferPoolConcurrent/512KB-4 147.6µ 148.1µ ~ 0.700
_SubtreeDeserializationWithBufferSizes/16KB-4 646.5µ 620.0µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/32KB-4 643.6µ 616.5µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/64KB-4 662.3µ 608.5µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/128KB-4 667.6µ 619.9µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/512KB-4 655.5µ 636.0µ ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/16KB-4 35.29m 35.36m ~ 1.000
_SubtreeDataDeserializationWithBufferSizes/32KB-4 35.64m 35.04m ~ 0.700
_SubtreeDataDeserializationWithBufferSizes/64KB-4 35.65m 35.45m ~ 0.700
_SubtreeDataDeserializationWithBufferSizes/128KB-4 35.36m 35.53m ~ 0.700
_SubtreeDataDeserializationWithBufferSizes/512KB-4 35.10m 35.20m ~ 1.000
_PooledVsNonPooled/Pooled-4 738.4n 736.5n ~ 0.400
_PooledVsNonPooled/NonPooled-4 7.388µ 7.191µ ~ 0.100
_MemoryFootprint/Current_512KB_32concurrent-4 6.842µ 6.809µ ~ 0.200
_MemoryFootprint/Proposed_32KB_32concurrent-4 10.60µ 10.61µ ~ 1.000
_MemoryFootprint/Alternative_64KB_32concurrent-4 10.06µ 10.13µ ~ 1.000
_prepareTxsPerLevel-4 414.8m 414.4m ~ 0.700
_prepareTxsPerLevelOrdered-4 3.610m 4.055m ~ 0.100
_prepareTxsPerLevel_Comparison/Original-4 421.7m 430.7m ~ 0.100
_prepareTxsPerLevel_Comparison/Optimized-4 3.607m 3.983m ~ 0.100
SubtreeSizes/10k_tx_4_per_subtree-4 1.363m 1.362m ~ 1.000
SubtreeSizes/10k_tx_16_per_subtree-4 322.4µ 326.6µ ~ 1.000
SubtreeSizes/10k_tx_64_per_subtree-4 76.87µ 77.77µ ~ 0.700
SubtreeSizes/10k_tx_256_per_subtree-4 19.06µ 19.27µ ~ 0.100
SubtreeSizes/10k_tx_512_per_subtree-4 9.472µ 9.746µ ~ 0.200
SubtreeSizes/10k_tx_1024_per_subtree-4 4.737µ 4.870µ ~ 0.100
SubtreeSizes/10k_tx_2k_per_subtree-4 2.379µ 2.398µ ~ 0.100
BlockSizeScaling/10k_tx_64_per_subtree-4 75.93µ 76.60µ ~ 0.400
BlockSizeScaling/10k_tx_256_per_subtree-4 19.12µ 19.35µ ~ 0.100
BlockSizeScaling/10k_tx_1024_per_subtree-4 4.830µ 4.788µ ~ 0.700
BlockSizeScaling/50k_tx_64_per_subtree-4 399.8µ 397.0µ ~ 1.000
BlockSizeScaling/50k_tx_256_per_subtree-4 97.54µ 95.71µ ~ 0.400
BlockSizeScaling/50k_tx_1024_per_subtree-4 23.88µ 23.90µ ~ 1.000
SubtreeAllocations/small_subtrees_exists_check-4 164.0µ 165.3µ ~ 0.700
SubtreeAllocations/small_subtrees_data_fetch-4 174.9µ 168.7µ ~ 0.100
SubtreeAllocations/small_subtrees_full_validation-4 339.4µ 336.5µ ~ 0.700
SubtreeAllocations/medium_subtrees_exists_check-4 9.942µ 9.770µ ~ 0.200
SubtreeAllocations/medium_subtrees_data_fetch-4 10.27µ 10.13µ ~ 0.400
SubtreeAllocations/medium_subtrees_full_validation-4 20.03µ 19.72µ ~ 0.100
SubtreeAllocations/large_subtrees_exists_check-4 2.345µ 2.306µ ~ 0.200
SubtreeAllocations/large_subtrees_data_fetch-4 2.499µ 2.474µ ~ 0.700
SubtreeAllocations/large_subtrees_full_validation-4 4.973µ 4.890µ ~ 0.100
StoreBlock_Sequential/BelowCSVHeight-4 328.6µ 326.6µ ~ 1.000
StoreBlock_Sequential/AboveCSVHeight-4 331.1µ 329.7µ ~ 0.700
GetUtxoHashes-4 260.8n 263.4n ~ 1.000
GetUtxoHashes_ManyOutputs-4 43.10µ 43.35µ ~ 0.700
_NewMetaDataFromBytes-4 237.5n 238.8n ~ 0.200
_Bytes-4 621.9n 640.4n ~ 0.400
_MetaBytes-4 565.0n 573.5n ~ 0.100

Threshold: >10% with p < 0.05 | Generated: 2026-05-13 14:07 UTC

Move the OOB bounds check ahead of the 120s WaitForParent loop in
ExtendTransaction. Parent Outputs are populated at wire-parse time
and never mutated afterwards, so the check is safe to run before
waiting and lets a malformed peer block be rejected without burning
up to two minutes per offending input on the polling loop.

Refactor the test fixture into a buildOOBFixture helper and add
TestSyncManager_extendFromTxMap_OOB so the sibling phase-1 path's
TxInvalidError contract is now covered by a regression test in its
own right. Loosen the string assertions in the existing test to
errors.Is plus the parent hash so benign error-message rewording
no longer breaks the test.

Refs #4564
@sonarqubecloud

Copy link
Copy Markdown

@ordishs ordishs requested review from freemans13 and oskarszoon May 14, 2026 06:49
@ordishs ordishs enabled auto-merge (squash) May 14, 2026 06:50

@oskarszoon oskarszoon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approve. Fix is correct, both regression tests trigger the original panic when the bounds check is mentally removed, error type harmonised to TxInvalidError on both paths, the "Outputs populated at wire-parse and never mutated" justification for placing the check before WaitForParent checks out (grep for mutation on the netsync path returns nothing).

Two things worth flagging:

  • Same DoS shape exists unfixed in services/blockvalidation/quick_validate.go at :1061-1062 and :1399-1400 — unguarded parentTx.Outputs[input.PreviousTxOutIndex] against peer-controlled parentTx lookups. Same panic shape as #4564, but in the block-validation hot path (so worse blast radius). Out of scope for this PR; worth filing as a separate issue / follow-up PR.
  • For the PR description / commit log: worth noting that ExtendTransaction (the parallel path getting the new check) has no production callers in the current tree — only the phase-1 path extendFromTxMap is reachable from peer blocks today, and that path already had the bounds check pre-PR. So the practical runtime impact of #768 is the error-class harmonisation at line 1011; the new check at line 1256 is forward-looking defense-in-depth for when the parallel path gets wired up. Doesn't change the fix or the urgency, but clarifies what's actually closing the live exposure vs what's pre-emptive.

@ordishs ordishs merged commit 974782b into bsv-blockchain:main May 14, 2026
25 checks passed
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.

3 participants