Skip to content

fix(legacy/netsync): nil-deref guards in input decoration paths#862

Merged
freemans13 merged 5 commits into
bsv-blockchain:mainfrom
freemans13:stu/legacy-netsync-extend-nil-checks
May 21, 2026
Merged

fix(legacy/netsync): nil-deref guards in input decoration paths#862
freemans13 merged 5 commits into
bsv-blockchain:mainfrom
freemans13:stu/legacy-netsync-extend-nil-checks

Conversation

@freemans13

Copy link
Copy Markdown
Collaborator

Summary

Both extendFromTxMap (phase-1 same-block parents) and ExtendTransaction (parallel-decoration path) dereference prevTxWrapper.Tx, the parent's Output, and the output's LockingScript without first checking they're non-nil. A malformed or hostile peer block could leave us in any of these states:

  • TxMapWrapper present but Tx == nil (entry created before parse)
  • parent.Outputs[i] == nil
  • parent.Outputs[i].LockingScript == nil

In production those would panic on the deref into bscript.NewFromBytes or on the Outputs index. The bounds-check added in #768 covers out-of-range indices but not nil entries within the slice.

This PR adds the three guards on both paths, returning TxInvalidError so the caller's normal error path runs.

Tests

  • TestSyncManager_extendFromTxMap_NilParentTx
  • TestSyncManager_extendFromTxMap_NilOutput
  • TestSyncManager_extendFromTxMap_NilLockingScript
  • TestSyncManager_ExtendTransaction_NilParentTx
  • new buildInRangeFixture helper (parallel to buildOOBFixture) so the bounds-check passes and the nil-check is the one under test

go test -race -short ./services/legacy/netsync/ — 56 passed. go vet clean.

Context

Originally bundled in #723; pulled out as a focused PR after #723's core change (broadening quickValidationMode to CATCHINGBLOCKS) was superseded by #847.

Both extendFromTxMap (phase-1 same-block parents) and ExtendTransaction
(parallel-decoration path) dereference prevTxWrapper.Tx and the parent's
Output / LockingScript without first checking they're non-nil. A
malformed or hostile peer block could land us in any of these states:

- TxMapWrapper present but Tx == nil (entry created before parse)
- parent.Outputs[i] == nil
- parent.Outputs[i].LockingScript == nil

In production those would panic on the deref into bscript.NewFromBytes
or on the Outputs index. The existing bounds-check from bsv-blockchain#768 covers
out-of-range indices but not nil entries within the slice.

Add the three guards on both paths, returning TxInvalidError so the
caller's normal error path runs.

Tests:
- TestSyncManager_extendFromTxMap_NilParentTx
- TestSyncManager_extendFromTxMap_NilOutput
- TestSyncManager_extendFromTxMap_NilLockingScript
- TestSyncManager_ExtendTransaction_NilParentTx
- new buildInRangeFixture helper (parallel to buildOOBFixture) so the
  bounds-check passes and the nil-check is the one under test

Originally bundled in bsv-blockchain#723; pulled out as a focused PR after bsv-blockchain#723's
core change (CATCHINGBLOCKS quick-validation gating) was superseded
by bsv-blockchain#847.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented May 14, 2026

Copy link
Copy Markdown
Contributor

🤖 Claude Code Review

Status: Complete

Current Review:

Summary:
This PR adds defensive nil-checks for malformed/hostile block data in input decoration paths. The implementation is correct and follows best practices:

  • Security: Properly prevents panics from malicious peer blocks by checking nil parent Tx, nil outputs, and nil locking scripts
  • Error handling: Returns TxInvalidError consistently, allowing callers to handle errors through normal paths
  • Code quality: Guards are applied symmetrically in both extendFromTxMap and ExtendTransaction paths
  • Tests: Comprehensive coverage for extendFromTxMap with dedicated tests for each nil scenario

The only minor suggestion is to add two additional tests (ExtendTransaction nil output/locking script) to achieve complete symmetry with the extendFromTxMap test suite.

@github-actions

github-actions Bot commented May 14, 2026

Copy link
Copy Markdown
Contributor

Benchmark Comparison Report

Baseline: main (unknown)

Current: PR-862 (60be53d)

Summary

  • Regressions: 0
  • Improvements: 0
  • Unchanged: 144
  • Significance level: p < 0.05
All benchmark results (sec/op)
Benchmark Baseline Current Change p-value
_NewBlockFromBytes-4 1.677µ 1.946µ ~ 0.100
SplitSyncedParentMap_SetIfNotExists/256_buckets-4 61.52n 61.46n ~ 0.700
SplitSyncedParentMap_SetIfNotExists/16_buckets-4 61.42n 61.42n ~ 1.000
SplitSyncedParentMap_SetIfNotExists/1_bucket-4 61.65n 61.40n ~ 0.100
SplitSyncedParentMap_ConcurrentSetIfNotExists/256_buckets... 29.87n 29.70n ~ 1.000
SplitSyncedParentMap_ConcurrentSetIfNotExists/16_buckets_... 50.48n 50.35n ~ 1.000
SplitSyncedParentMap_ConcurrentSetIfNotExists/1_bucket_pa... 117.0n 109.2n ~ 0.100
MiningCandidate_Stringify_Short-4 259.8n 259.9n ~ 1.000
MiningCandidate_Stringify_Long-4 1.917µ 1.896µ ~ 0.100
MiningSolution_Stringify-4 963.3n 958.2n ~ 0.100
BlockInfo_MarshalJSON-4 1.772µ 1.791µ ~ 0.100
NewFromBytes-4 126.3n 127.9n ~ 0.700
AddTxBatchColumnar_Validation-4 2.487µ 2.461µ ~ 0.200
OffsetValidationLoop-4 638.0n 638.9n ~ 0.700
Mine_EasyDifficulty-4 51.87µ 52.35µ ~ 0.400
Mine_WithAddress-4 5.375µ 5.452µ ~ 0.100
BlockAssembler_AddTx-4 0.02642n 0.02747n ~ 1.000
AddNode-4 11.04 10.68 ~ 0.200
AddNodeWithMap-4 11.89 11.51 ~ 0.400
DiskTxMap_SetIfNotExists-4 3.499µ 3.409µ ~ 0.100
DiskTxMap_SetIfNotExists_Parallel-4 3.354µ 3.366µ ~ 1.000
DiskTxMap_ExistenceOnly-4 314.9n 300.4n ~ 0.400
Queue-4 188.1n 186.9n ~ 0.700
AtomicPointer-4 4.898n 4.962n ~ 1.000
ReorgOptimizations/DedupFilterPipeline/Old/10K-4 839.6µ 829.0µ ~ 0.400
ReorgOptimizations/DedupFilterPipeline/New/10K-4 788.7µ 783.1µ ~ 0.100
ReorgOptimizations/AllMarkFalse/Old/10K-4 103.8µ 103.3µ ~ 0.500
ReorgOptimizations/AllMarkFalse/New/10K-4 62.43µ 61.82µ ~ 0.100
ReorgOptimizations/HashSlicePool/Old/10K-4 54.16µ 51.94µ ~ 0.100
ReorgOptimizations/HashSlicePool/New/10K-4 11.53µ 10.45µ ~ 0.100
ReorgOptimizations/NodeFlags/Old/10K-4 4.697µ 4.544µ ~ 0.100
ReorgOptimizations/NodeFlags/New/10K-4 1.591µ 1.559µ ~ 0.400
ReorgOptimizations/DedupFilterPipeline/Old/100K-4 10.079m 9.273m ~ 0.200
ReorgOptimizations/DedupFilterPipeline/New/100K-4 9.576m 9.299m ~ 0.100
ReorgOptimizations/AllMarkFalse/Old/100K-4 1.085m 1.044m ~ 0.100
ReorgOptimizations/AllMarkFalse/New/100K-4 682.4µ 682.4µ ~ 0.700
ReorgOptimizations/HashSlicePool/Old/100K-4 537.1µ 531.7µ ~ 0.100
ReorgOptimizations/HashSlicePool/New/100K-4 306.2µ 336.1µ ~ 0.100
ReorgOptimizations/NodeFlags/Old/100K-4 45.98µ 48.98µ ~ 0.100
ReorgOptimizations/NodeFlags/New/100K-4 15.27µ 17.28µ ~ 0.100
TxMapSetIfNotExists-4 51.83n 52.41n ~ 0.100
TxMapSetIfNotExistsDuplicate-4 40.85n 40.56n ~ 0.300
ChannelSendReceive-4 610.7n 595.1n ~ 0.100
DirectSubtreeAdd/4_per_subtree-4 58.23n 57.29n ~ 0.700
DirectSubtreeAdd/64_per_subtree-4 28.87n 29.11n ~ 1.000
DirectSubtreeAdd/256_per_subtree-4 27.74n 28.17n ~ 0.700
DirectSubtreeAdd/1024_per_subtree-4 26.47n 26.46n ~ 0.800
DirectSubtreeAdd/2048_per_subtree-4 25.98n 26.04n ~ 0.100
SubtreeProcessorAdd/4_per_subtree-4 290.4n 290.8n ~ 0.700
SubtreeProcessorAdd/64_per_subtree-4 283.4n 290.2n ~ 0.100
SubtreeProcessorAdd/256_per_subtree-4 283.2n 294.1n ~ 0.400
SubtreeProcessorAdd/1024_per_subtree-4 276.2n 283.1n ~ 0.100
SubtreeProcessorAdd/2048_per_subtree-4 276.5n 280.3n ~ 0.100
SubtreeProcessorRotate/4_per_subtree-4 279.6n 281.0n ~ 0.700
SubtreeProcessorRotate/64_per_subtree-4 277.2n 279.4n ~ 0.200
SubtreeProcessorRotate/256_per_subtree-4 275.5n 281.2n ~ 0.100
SubtreeProcessorRotate/1024_per_subtree-4 279.1n 277.6n ~ 0.700
SubtreeNodeAddOnly/4_per_subtree-4 55.14n 55.54n ~ 0.100
SubtreeNodeAddOnly/64_per_subtree-4 36.58n 36.19n ~ 1.000
SubtreeNodeAddOnly/256_per_subtree-4 35.05n 35.13n ~ 0.200
SubtreeNodeAddOnly/1024_per_subtree-4 34.50n 34.54n ~ 0.800
SubtreeCreationOnly/4_per_subtree-4 110.2n 109.8n ~ 0.800
SubtreeCreationOnly/64_per_subtree-4 349.8n 349.7n ~ 1.000
SubtreeCreationOnly/256_per_subtree-4 1.218µ 1.231µ ~ 0.100
SubtreeCreationOnly/1024_per_subtree-4 3.831µ 3.818µ ~ 0.700
SubtreeCreationOnly/2048_per_subtree-4 6.770µ 7.003µ ~ 0.100
SubtreeProcessorOverheadBreakdown/64_per_subtree-4 278.9n 283.1n ~ 0.400
SubtreeProcessorOverheadBreakdown/1024_per_subtree-4 279.4n 280.1n ~ 1.000
ParallelGetAndSetIfNotExists/1k_nodes-4 1.982m 1.990m ~ 0.700
ParallelGetAndSetIfNotExists/10k_nodes-4 5.096m 5.047m ~ 0.100
ParallelGetAndSetIfNotExists/50k_nodes-4 7.315m 6.951m ~ 0.100
ParallelGetAndSetIfNotExists/100k_nodes-4 10.006m 9.722m ~ 0.100
SequentialGetAndSetIfNotExists/1k_nodes-4 1.792m 1.778m ~ 1.000
SequentialGetAndSetIfNotExists/10k_nodes-4 4.535m 4.517m ~ 1.000
SequentialGetAndSetIfNotExists/50k_nodes-4 14.53m 13.70m ~ 0.200
SequentialGetAndSetIfNotExists/100k_nodes-4 25.85m 25.32m ~ 0.400
ProcessOwnBlockSubtreeNodesParallel/1k_nodes-4 2.046m 2.029m ~ 0.400
ProcessOwnBlockSubtreeNodesParallel/10k_nodes-4 8.390m 8.264m ~ 0.100
ProcessOwnBlockSubtreeNodesParallel/100k_nodes-4 13.28m 13.01m ~ 0.100
ProcessOwnBlockSubtreeNodesSequential/1k_nodes-4 1.816m 1.797m ~ 0.700
ProcessOwnBlockSubtreeNodesSequential/10k_nodes-4 7.983m 7.993m ~ 0.700
ProcessOwnBlockSubtreeNodesSequential/100k_nodes-4 42.90m 43.25m ~ 0.400
CalcBlockWork-4 461.0n 463.7n ~ 0.400
CalculateWork-4 658.0n 626.2n ~ 0.700
BuildBlockLocatorString_Helpers/Size_10-4 1.331µ 1.373µ ~ 0.300
BuildBlockLocatorString_Helpers/Size_100-4 14.07µ 12.66µ ~ 0.700
BuildBlockLocatorString_Helpers/Size_1000-4 126.1µ 127.3µ ~ 0.100
CatchupWithHeaderCache-4 104.0m 103.9m ~ 0.700
_BufferPoolAllocation/16KB-4 3.868µ 3.888µ ~ 0.600
_BufferPoolAllocation/32KB-4 8.260µ 7.681µ ~ 0.100
_BufferPoolAllocation/64KB-4 15.95µ 21.12µ ~ 0.700
_BufferPoolAllocation/128KB-4 28.18µ 36.39µ ~ 0.100
_BufferPoolAllocation/512KB-4 114.1µ 117.9µ ~ 0.200
_BufferPoolConcurrent/32KB-4 18.53µ 19.47µ ~ 0.400
_BufferPoolConcurrent/64KB-4 30.35µ 30.70µ ~ 0.700
_BufferPoolConcurrent/512KB-4 145.1µ 145.9µ ~ 0.400
_SubtreeDeserializationWithBufferSizes/16KB-4 613.3µ 625.8µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/32KB-4 611.3µ 619.4µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/64KB-4 597.9µ 592.3µ ~ 0.400
_SubtreeDeserializationWithBufferSizes/128KB-4 591.5µ 603.4µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/512KB-4 617.9µ 613.7µ ~ 1.000
_SubtreeDataDeserializationWithBufferSizes/16KB-4 37.19m 36.85m ~ 0.400
_SubtreeDataDeserializationWithBufferSizes/32KB-4 37.37m 37.11m ~ 0.700
_SubtreeDataDeserializationWithBufferSizes/64KB-4 36.97m 36.64m ~ 1.000
_SubtreeDataDeserializationWithBufferSizes/128KB-4 36.81m 36.69m ~ 1.000
_SubtreeDataDeserializationWithBufferSizes/512KB-4 36.41m 36.47m ~ 0.400
_PooledVsNonPooled/Pooled-4 743.1n 741.2n ~ 0.300
_PooledVsNonPooled/NonPooled-4 8.308µ 7.604µ ~ 0.100
_MemoryFootprint/Current_512KB_32concurrent-4 6.529µ 6.560µ ~ 0.400
_MemoryFootprint/Proposed_32KB_32concurrent-4 9.681µ 9.892µ ~ 0.200
_MemoryFootprint/Alternative_64KB_32concurrent-4 9.394µ 9.762µ ~ 0.700
SubtreeSizes/10k_tx_4_per_subtree-4 1.299m 1.331m ~ 0.400
SubtreeSizes/10k_tx_16_per_subtree-4 313.8µ 311.0µ ~ 1.000
SubtreeSizes/10k_tx_64_per_subtree-4 74.63µ 74.92µ ~ 0.700
SubtreeSizes/10k_tx_256_per_subtree-4 18.70µ 18.65µ ~ 0.400
SubtreeSizes/10k_tx_512_per_subtree-4 9.254µ 9.316µ ~ 0.200
SubtreeSizes/10k_tx_1024_per_subtree-4 4.596µ 4.569µ ~ 0.700
SubtreeSizes/10k_tx_2k_per_subtree-4 2.313µ 2.307µ ~ 1.000
BlockSizeScaling/10k_tx_64_per_subtree-4 73.50µ 75.15µ ~ 0.200
BlockSizeScaling/10k_tx_256_per_subtree-4 18.47µ 18.60µ ~ 0.700
BlockSizeScaling/10k_tx_1024_per_subtree-4 4.575µ 4.630µ ~ 0.100
BlockSizeScaling/50k_tx_64_per_subtree-4 387.7µ 384.9µ ~ 1.000
BlockSizeScaling/50k_tx_256_per_subtree-4 92.44µ 92.86µ ~ 0.700
BlockSizeScaling/50k_tx_1024_per_subtree-4 22.84µ 22.72µ ~ 0.700
SubtreeAllocations/small_subtrees_exists_check-4 155.4µ 154.3µ ~ 1.000
SubtreeAllocations/small_subtrees_data_fetch-4 160.7µ 158.8µ ~ 1.000
SubtreeAllocations/small_subtrees_full_validation-4 318.4µ 321.4µ ~ 0.100
SubtreeAllocations/medium_subtrees_exists_check-4 9.240µ 9.168µ ~ 0.400
SubtreeAllocations/medium_subtrees_data_fetch-4 9.458µ 9.627µ ~ 0.400
SubtreeAllocations/medium_subtrees_full_validation-4 18.66µ 18.48µ ~ 0.300
SubtreeAllocations/large_subtrees_exists_check-4 2.191µ 2.194µ ~ 1.000
SubtreeAllocations/large_subtrees_data_fetch-4 2.293µ 2.302µ ~ 0.700
SubtreeAllocations/large_subtrees_full_validation-4 4.653µ 4.672µ ~ 0.100
_prepareTxsPerLevel-4 409.1m 405.2m ~ 1.000
_prepareTxsPerLevelOrdered-4 3.711m 3.619m ~ 0.400
_prepareTxsPerLevel_Comparison/Original-4 398.3m 405.3m ~ 0.400
_prepareTxsPerLevel_Comparison/Optimized-4 3.666m 3.650m ~ 1.000
StoreBlock_Sequential/BelowCSVHeight-4 332.3µ 331.7µ ~ 1.000
StoreBlock_Sequential/AboveCSVHeight-4 333.6µ 332.1µ ~ 0.400
GetUtxoHashes-4 268.9n 273.4n ~ 0.400
GetUtxoHashes_ManyOutputs-4 44.86µ 44.90µ ~ 0.700
_NewMetaDataFromBytes-4 213.5n 213.9n ~ 0.600
_Bytes-4 396.7n 393.0n ~ 0.100
_MetaBytes-4 336.3n 337.6n ~ 0.200

Threshold: >10% with p < 0.05 | Generated: 2026-05-21 09:52 UTC

@freemans13 freemans13 self-assigned this May 14, 2026

@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.

Guards are correct, minimal, at the right layer. Approve with comments.

Worth fixing before merge: extendFromTxMap has three nil-path tests (NilParentTx, NilOutput, NilLockingScript); ExtendTransaction only has NilParentTx. The prevOutput == nil || prevOutput.LockingScript == nil guard at handle_block.go:1382-1386 is unverified. Add ExtendTransaction_NilOutput and ExtendTransaction_NilLockingScript to match — the two paths are independent goroutine-based code, regression in the parallel path would slip through.

Worth a comment or follow-up: ExtendTransaction wraps the goroutine error in NewProcessingError at g.Wait() (line 1417). The TxInvalid sentinel ends up two levels deep after extendTransactions wraps it again (line 1016-1017). errors.Is(err, ErrTxInvalid) still reaches it via the custom Is, but the test asserts this on ExtendTransaction's direct return, not on the orchestrating caller's return. No production caller currently checks for ErrTxInvalid specifically (all errors → block reject), so this is fine for safety. Classification just silently lost — worth clarifying intended semantics.

Questions:

  • Is TxMapWrapper ever populated with nil Tx outside tests? PR description says "entry created before parse" — is that a real two-phase population state, or purely defensive against malicious peer messages?
  • ExtendTransaction is exported. Called from outside netsync other than tests? Callers may depend on prior panic / error-type behaviour.

require.Error(t, err)
require.True(t, errors.Is(err, errors.ErrTxInvalid), "expected TxInvalid error, got %v", err)
require.Contains(t, err.Error(), parentHash.String())
}

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.

[Minor] Test coverage gap: ExtendTransaction has the same nil output and nil locking script guards (lines 1418-1422 in handle_block.go) as extendFromTxMap, but only the nil parent test exists for ExtendTransaction. Consider adding TestSyncManager_ExtendTransaction_NilOutput and TestSyncManager_ExtendTransaction_NilLockingScript to mirror the extendFromTxMap test suite for consistency and completeness.

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
16.8% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@freemans13 freemans13 merged commit 64f0f0e into bsv-blockchain:main May 21, 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