Skip to content

fix(legacy/netsync): guard nil FSM state in handleNewPeerMsg#767

Merged
ordishs merged 4 commits into
bsv-blockchain:mainfrom
ordishs:security/4563-fsm-nil-pointer
May 22, 2026
Merged

fix(legacy/netsync): guard nil FSM state in handleNewPeerMsg#767
ordishs merged 4 commits into
bsv-blockchain:mainfrom
ordishs:security/4563-fsm-nil-pointer

Conversation

@ordishs

@ordishs ordishs commented Apr 28, 2026

Copy link
Copy Markdown
Collaborator

Closes #4563.

Summary

handleNewPeerMsg calls blockchainClient.GetFSMCurrentState; on error it logged a debug message but did not return, then unconditionally dereferenced *state. A (nil, err) return — common during blockchain-service transients (gRPC timeout, brief network blip, restart) — caused a panic on the very next peer connection or reconnect.

Blast radius

Severity is CRITICAL because the panic propagates through blockHandler's dispatch loop (manager.go:1985), which has no defer recover(). The result is a full-process crash on the next peer connection — not a dropped peer, not a degraded sync, the whole node goes down.

Fix

Guard the dereference: if state != nil && *state == teranodeblockchain.FSMStateLEGACYSYNCING && .... The fee-filter side-effect is skipped when state can't be read; peer registration and startSync continue unchanged. Deviation from the audit's literal suggestion (which return-ed early): early return would silently drop peer registration on every transient FSM error.

Also promote the error log from Debugf to Errorf so the failure is visible at default log levels — matches the convention used by the other three GetFSMCurrentState call sites in this file (manager.go:1157 returns a wrapped error; :1769 and :1968 use Errorf).

Test plan

  • New regression test stubs GetFSMCurrentState to return (nil, err); calls handleNewPeerMsg; asserts no panic, peer is registered, and currentFeeFilter is not set.
  • 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

No issues found.

This PR correctly fixes a critical nil-pointer dereference that caused process crashes. The implementation is solid:

Core fix (manager.go:716): Nil guard prevents panic while preserving peer registration flow. Promotes error log from Debug to Error for visibility, consistent with other FSM call sites.

Test fix (handle_block_test.go:728): Removes eviction limit from test fixture, eliminating non-deterministic map eviction that caused test flakes.

Regression test (manager_test.go:826-854): Validates nil-state handling with proper assertions for both peer registration and fee-filter side-effect.

All changes align with the minimal-diff principle in AGENTS.md and include appropriate test coverage.

@github-actions

github-actions Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Benchmark Comparison Report

Baseline: main (unknown)

Current: PR-767 (c359ed3)

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.717µ 1.714µ ~ 0.700
SplitSyncedParentMap_SetIfNotExists/256_buckets-4 61.49n 61.77n ~ 0.200
SplitSyncedParentMap_SetIfNotExists/16_buckets-4 61.76n 61.55n ~ 0.700
SplitSyncedParentMap_SetIfNotExists/1_bucket-4 61.50n 61.54n ~ 0.800
SplitSyncedParentMap_ConcurrentSetIfNotExists/256_buckets... 30.83n 33.03n ~ 0.100
SplitSyncedParentMap_ConcurrentSetIfNotExists/16_buckets_... 53.76n 55.41n ~ 0.100
SplitSyncedParentMap_ConcurrentSetIfNotExists/1_bucket_pa... 127.0n 145.0n ~ 0.100
MiningCandidate_Stringify_Short-4 261.7n 262.0n ~ 1.000
MiningCandidate_Stringify_Long-4 1.920µ 1.919µ ~ 0.700
MiningSolution_Stringify-4 973.2n 971.2n ~ 0.700
BlockInfo_MarshalJSON-4 1.825µ 1.820µ ~ 0.400
NewFromBytes-4 129.1n 141.8n ~ 0.100
AddTxBatchColumnar_Validation-4 2.576µ 2.522µ ~ 1.000
OffsetValidationLoop-4 639.0n 643.2n ~ 0.200
Mine_EasyDifficulty-4 60.89µ 60.96µ ~ 1.000
Mine_WithAddress-4 6.860µ 6.885µ ~ 1.000
BlockAssembler_AddTx-4 0.02816n 0.02674n ~ 0.700
AddNode-4 10.56 10.74 ~ 0.400
AddNodeWithMap-4 11.37 11.62 ~ 0.200
DiskTxMap_SetIfNotExists-4 3.696µ 4.047µ ~ 0.100
DiskTxMap_SetIfNotExists_Parallel-4 3.585µ 3.632µ ~ 0.700
DiskTxMap_ExistenceOnly-4 508.5n 410.3n ~ 0.400
Queue-4 197.0n 194.7n ~ 0.200
AtomicPointer-4 4.384n 4.890n ~ 0.700
ReorgOptimizations/DedupFilterPipeline/Old/10K-4 893.9µ 939.5µ ~ 0.100
ReorgOptimizations/DedupFilterPipeline/New/10K-4 833.7µ 879.8µ ~ 0.100
ReorgOptimizations/AllMarkFalse/Old/10K-4 110.0µ 111.8µ ~ 0.100
ReorgOptimizations/AllMarkFalse/New/10K-4 62.60µ 62.65µ ~ 0.700
ReorgOptimizations/HashSlicePool/Old/10K-4 63.13µ 59.50µ ~ 0.700
ReorgOptimizations/HashSlicePool/New/10K-4 11.30µ 10.96µ ~ 0.700
ReorgOptimizations/NodeFlags/Old/10K-4 5.359µ 5.193µ ~ 0.100
ReorgOptimizations/NodeFlags/New/10K-4 1.772µ 1.820µ ~ 1.000
ReorgOptimizations/DedupFilterPipeline/Old/100K-4 12.51m 11.04m ~ 0.100
ReorgOptimizations/DedupFilterPipeline/New/100K-4 12.71m 12.42m ~ 0.700
ReorgOptimizations/AllMarkFalse/Old/100K-4 1.206m 1.178m ~ 0.400
ReorgOptimizations/AllMarkFalse/New/100K-4 689.0µ 687.0µ ~ 0.400
ReorgOptimizations/HashSlicePool/Old/100K-4 572.8µ 553.0µ ~ 0.100
ReorgOptimizations/HashSlicePool/New/100K-4 338.8µ 323.4µ ~ 0.700
ReorgOptimizations/NodeFlags/Old/100K-4 54.20µ 55.93µ ~ 0.100
ReorgOptimizations/NodeFlags/New/100K-4 18.69µ 18.87µ ~ 0.700
TxMapSetIfNotExists-4 52.38n 52.41n ~ 1.000
TxMapSetIfNotExistsDuplicate-4 40.64n 40.23n ~ 1.000
ChannelSendReceive-4 631.1n 631.9n ~ 0.400
DirectSubtreeAdd/4_per_subtree-4 58.12n 56.88n ~ 0.400
DirectSubtreeAdd/64_per_subtree-4 29.12n 29.78n ~ 0.700
DirectSubtreeAdd/256_per_subtree-4 27.75n 27.73n ~ 1.000
DirectSubtreeAdd/1024_per_subtree-4 26.40n 26.49n ~ 0.400
DirectSubtreeAdd/2048_per_subtree-4 26.17n 26.15n ~ 0.400
SubtreeProcessorAdd/4_per_subtree-4 319.1n 304.1n ~ 0.200
SubtreeProcessorAdd/64_per_subtree-4 311.7n 306.1n ~ 1.000
SubtreeProcessorAdd/256_per_subtree-4 304.2n 310.8n ~ 0.100
SubtreeProcessorAdd/1024_per_subtree-4 322.7n 311.0n ~ 0.100
SubtreeProcessorAdd/2048_per_subtree-4 322.6n 312.7n ~ 0.400
SubtreeProcessorRotate/4_per_subtree-4 308.6n 327.2n ~ 1.000
SubtreeProcessorRotate/64_per_subtree-4 295.1n 297.0n ~ 1.000
SubtreeProcessorRotate/256_per_subtree-4 292.5n 288.3n ~ 0.200
SubtreeProcessorRotate/1024_per_subtree-4 310.3n 291.5n ~ 0.100
SubtreeNodeAddOnly/4_per_subtree-4 55.40n 55.19n ~ 0.200
SubtreeNodeAddOnly/64_per_subtree-4 36.17n 36.06n ~ 0.400
SubtreeNodeAddOnly/256_per_subtree-4 35.11n 35.18n ~ 1.000
SubtreeNodeAddOnly/1024_per_subtree-4 34.53n 34.68n ~ 0.700
SubtreeCreationOnly/4_per_subtree-4 111.7n 110.8n ~ 0.400
SubtreeCreationOnly/64_per_subtree-4 357.4n 358.9n ~ 1.000
SubtreeCreationOnly/256_per_subtree-4 1.248µ 1.263µ ~ 0.200
SubtreeCreationOnly/1024_per_subtree-4 3.838µ 3.873µ ~ 0.100
SubtreeCreationOnly/2048_per_subtree-4 7.084µ 7.151µ ~ 0.400
SubtreeProcessorOverheadBreakdown/64_per_subtree-4 291.0n 312.2n ~ 0.400
SubtreeProcessorOverheadBreakdown/1024_per_subtree-4 297.2n 338.5n ~ 0.100
ParallelGetAndSetIfNotExists/1k_nodes-4 2.067m 2.021m ~ 0.100
ParallelGetAndSetIfNotExists/10k_nodes-4 5.252m 5.162m ~ 0.200
ParallelGetAndSetIfNotExists/50k_nodes-4 7.700m 7.869m ~ 0.200
ParallelGetAndSetIfNotExists/100k_nodes-4 10.53m 10.56m ~ 0.700
SequentialGetAndSetIfNotExists/1k_nodes-4 1.810m 1.795m ~ 0.100
SequentialGetAndSetIfNotExists/10k_nodes-4 5.062m 6.115m ~ 0.100
SequentialGetAndSetIfNotExists/50k_nodes-4 15.68m 16.30m ~ 0.700
SequentialGetAndSetIfNotExists/100k_nodes-4 30.62m 27.85m ~ 0.200
ProcessOwnBlockSubtreeNodesParallel/1k_nodes-4 2.122m 2.108m ~ 0.400
ProcessOwnBlockSubtreeNodesParallel/10k_nodes-4 8.543m 8.624m ~ 0.400
ProcessOwnBlockSubtreeNodesParallel/100k_nodes-4 14.26m 14.64m ~ 0.200
ProcessOwnBlockSubtreeNodesSequential/1k_nodes-4 1.855m 1.829m ~ 1.000
ProcessOwnBlockSubtreeNodesSequential/10k_nodes-4 10.11m 10.63m ~ 1.000
ProcessOwnBlockSubtreeNodesSequential/100k_nodes-4 56.38m 60.96m ~ 0.400
CalcBlockWork-4 504.5n 500.8n ~ 0.400
CalculateWork-4 699.3n 746.0n ~ 1.000
BuildBlockLocatorString_Helpers/Size_10-4 1.344µ 1.329µ ~ 0.100
BuildBlockLocatorString_Helpers/Size_100-4 13.87µ 15.76µ ~ 0.700
BuildBlockLocatorString_Helpers/Size_1000-4 128.3µ 126.2µ ~ 0.100
CatchupWithHeaderCache-4 104.1m 104.2m ~ 0.400
_BufferPoolAllocation/16KB-4 3.865µ 3.884µ ~ 1.000
_BufferPoolAllocation/32KB-4 9.657µ 9.326µ ~ 0.100
_BufferPoolAllocation/64KB-4 18.94µ 20.01µ ~ 0.100
_BufferPoolAllocation/128KB-4 37.85µ 36.13µ ~ 0.700
_BufferPoolAllocation/512KB-4 120.4µ 116.6µ ~ 0.700
_BufferPoolConcurrent/32KB-4 18.52µ 18.99µ ~ 0.400
_BufferPoolConcurrent/64KB-4 29.79µ 31.56µ ~ 0.200
_BufferPoolConcurrent/512KB-4 147.6µ 150.9µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/16KB-4 626.9µ 670.6µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/32KB-4 640.0µ 664.2µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/64KB-4 622.6µ 643.6µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/128KB-4 640.1µ 639.7µ ~ 1.000
_SubtreeDeserializationWithBufferSizes/512KB-4 651.6µ 641.8µ ~ 1.000
_SubtreeDataDeserializationWithBufferSizes/16KB-4 36.97m 36.91m ~ 1.000
_SubtreeDataDeserializationWithBufferSizes/32KB-4 37.02m 36.98m ~ 0.700
_SubtreeDataDeserializationWithBufferSizes/64KB-4 37.23m 37.07m ~ 0.400
_SubtreeDataDeserializationWithBufferSizes/128KB-4 36.74m 36.75m ~ 0.700
_SubtreeDataDeserializationWithBufferSizes/512KB-4 36.76m 36.72m ~ 1.000
_PooledVsNonPooled/Pooled-4 740.0n 651.5n ~ 0.100
_PooledVsNonPooled/NonPooled-4 8.765µ 8.537µ ~ 0.700
_MemoryFootprint/Current_512KB_32concurrent-4 6.662µ 6.797µ ~ 0.100
_MemoryFootprint/Proposed_32KB_32concurrent-4 9.520µ 10.948µ ~ 0.100
_MemoryFootprint/Alternative_64KB_32concurrent-4 9.242µ 10.965µ ~ 0.100
_prepareTxsPerLevel-4 432.8m 464.9m ~ 0.100
_prepareTxsPerLevelOrdered-4 4.174m 4.261m ~ 0.700
_prepareTxsPerLevel_Comparison/Original-4 429.8m 445.0m ~ 0.100
_prepareTxsPerLevel_Comparison/Optimized-4 3.997m 4.644m ~ 0.100
SubtreeSizes/10k_tx_4_per_subtree-4 1.293m 1.363m ~ 0.400
SubtreeSizes/10k_tx_16_per_subtree-4 304.2µ 306.2µ ~ 1.000
SubtreeSizes/10k_tx_64_per_subtree-4 73.07µ 72.76µ ~ 0.300
SubtreeSizes/10k_tx_256_per_subtree-4 18.13µ 18.25µ ~ 1.000
SubtreeSizes/10k_tx_512_per_subtree-4 9.388µ 8.956µ ~ 0.100
SubtreeSizes/10k_tx_1024_per_subtree-4 4.686µ 4.512µ ~ 0.100
SubtreeSizes/10k_tx_2k_per_subtree-4 2.227µ 2.213µ ~ 0.700
BlockSizeScaling/10k_tx_64_per_subtree-4 70.16µ 70.15µ ~ 1.000
BlockSizeScaling/10k_tx_256_per_subtree-4 17.73µ 17.71µ ~ 1.000
BlockSizeScaling/10k_tx_1024_per_subtree-4 4.390µ 4.428µ ~ 0.100
BlockSizeScaling/50k_tx_64_per_subtree-4 375.3µ 376.5µ ~ 1.000
BlockSizeScaling/50k_tx_256_per_subtree-4 89.52µ 88.31µ ~ 0.700
BlockSizeScaling/50k_tx_1024_per_subtree-4 22.05µ 21.88µ ~ 1.000
SubtreeAllocations/small_subtrees_exists_check-4 152.2µ 152.3µ ~ 0.700
SubtreeAllocations/small_subtrees_data_fetch-4 161.4µ 160.8µ ~ 0.700
SubtreeAllocations/small_subtrees_full_validation-4 313.8µ 311.7µ ~ 0.700
SubtreeAllocations/medium_subtrees_exists_check-4 8.981µ 9.060µ ~ 0.700
SubtreeAllocations/medium_subtrees_data_fetch-4 9.514µ 9.516µ ~ 0.700
SubtreeAllocations/medium_subtrees_full_validation-4 17.76µ 17.99µ ~ 1.000
SubtreeAllocations/large_subtrees_exists_check-4 2.135µ 2.135µ ~ 1.000
SubtreeAllocations/large_subtrees_data_fetch-4 2.245µ 2.277µ ~ 0.200
SubtreeAllocations/large_subtrees_full_validation-4 4.369µ 4.471µ ~ 0.200
StoreBlock_Sequential/BelowCSVHeight-4 330.8µ 330.0µ ~ 0.400
StoreBlock_Sequential/AboveCSVHeight-4 329.7µ 340.6µ ~ 0.200
GetUtxoHashes-4 260.3n 260.2n ~ 1.000
GetUtxoHashes_ManyOutputs-4 42.76µ 42.68µ ~ 0.700
_NewMetaDataFromBytes-4 230.7n 234.8n ~ 0.100
_Bytes-4 405.4n 407.8n ~ 0.700
_MetaBytes-4 139.4n 139.3n ~ 1.000

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

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

Re-approval. Three small things worth tightening:

  • Debugf for a CRITICAL-tagged failure is operator-silent in prod. Other GetFSMCurrentState call sites in manager.go use Errorf (lines 1769, 1968) or return a wrapped error (line 1157). Warnf at minimum here.
  • Test asserts no-panic + peer-registered but not the fee-filter skip — add sm.currentFeeFilter.Load() == 0 to confirm the nil-state branch actually runs (not just that the function didn't crash).
  • Blast radius is full-process crash, not a dropped peer connection — handleNewPeerMsg runs in blockHandler's dispatch loop, no recover(). Worth making explicit in the PR description.

Review follow-ups for bsv-blockchain#767:

- Errorf (was Debugf) so the FSM read failure is visible at default log
  levels and matches the convention of the file's other GetFSMCurrentState
  call sites (manager.go:1157 wraps in error, :1769 and :1968 use Errorf).
- Test now asserts currentFeeFilter stays 0 to prove the nil-state guard
  short-circuits the fee-filter branch, not just that the function did
  not panic.
…limit

buildInRangeFixture constructed the txMap with limit=2. The
TestSyncManager_ExtendTransaction_NilParentTx and
TestSyncManager_extendFromTxMap_NilParentTx tests then call
txMap.Set(*parentHash, ...) a third time to swap the parent wrapper
for one with a nil Tx. SyncedMap.setUnlocked treats len >= limit as
'evict a random entry' (random Go map iteration order), so the
child entry was being dropped non-deterministically, surfacing as
PROCESSING 'tx not found in txMap' instead of the TX_INVALID
expected by the test.

The fixture has no reason to cap its size; drop the limit and the
flake disappears (verified 50x with -race).
@sonarqubecloud

Copy link
Copy Markdown

@ordishs ordishs merged commit 30c8562 into bsv-blockchain:main May 22, 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