Skip to content

fix(subtreevalidation): nil-check subtree before deleting orphans#771

Open
ordishs wants to merge 2 commits intobsv-blockchain:mainfrom
ordishs:security/4570-subtreehandler-nil
Open

fix(subtreevalidation): nil-check subtree before deleting orphans#771
ordishs wants to merge 2 commits intobsv-blockchain:mainfrom
ordishs:security/4570-subtreehandler-nil

Conversation

@ordishs
Copy link
Copy Markdown
Collaborator

@ordishs ordishs commented Apr 28, 2026

Closes #4570.

Summary

subtreesHandler calls ValidateSubtreeInternal, which legitimately returns (nil, nil) when the subtree already exists in the store (a race between TryLockIfFileNotExists and another goroutine completing the write). The handler then panicked on subtree.Nodes. Severity HIGH because any concurrent subtree-write race can crash the goroutine.

Fix

Insert a if subtree == nil { return nil } between the error check and the orphan-deletion loop. Matches the audit's prescribed shape and mirrors the existing guard in Server.CheckSubtree (line 802).

This PR also implements the previously stubbed GetSubtreeExists so it actually consults subtreeStore.Exists for the subtree file. Without this, the (nil, nil) race path documented in the audit was unreachable - so the regression test could not exercise the bug. Now the existence check works as the comments described and the new defensive guard is properly covered.

Test plan

  • Regression test (TestSubtreesHandler_NilSubtree) drives the handler down the (nil, nil) return path; asserts no panic and no error.
  • go test -race ./services/subtreevalidation/... passes (241s).

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 28, 2026

🤖 Claude Code Review

Status: Complete


Summary: The fix correctly addresses issue #4570 by adding nil-checks before dereferencing subtree.Nodes at three call sites. The implementation matches the existing pattern at line 802 in Server.go. Test coverage is comprehensive with well-designed race simulation.

Findings:

[Minor] Missing documentation for GetSubtreeExists
The PR implements the previously stubbed GetSubtreeExists function (SubtreeValidation.go:102-108) by consulting subtreeStore.Exists, but removes the godoc comments without replacement. Since this function is now critical to the race-condition fix—the (nil, nil) return path from ValidateSubtreeInternal depends on it returning true when another goroutine writes the subtree—it should be documented. The old TODO-marked documentation should be replaced with an explanation of the actual behavior: returns true if subtree exists in store, false if store is nil or subtree absent.

Positive observations:

  • Nil-checks use identical, clear comments at all three sites explaining the race condition
  • Test design is excellent: raceSubtreeStore wrapper simulates the exact race window by toggling Exists() result
  • Fix follows CLAUDE.md conventions (no "Get" prefix on getter is pre-existing, not introduced here)
  • The PR description accurately explains both the bug and the fix

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 28, 2026

Benchmark Comparison Report

Baseline: main (unknown)

Current: PR-771 (1903fca)

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.700µ 1.768µ ~ 0.400
SplitSyncedParentMap_SetIfNotExists/256_buckets-4 61.57n 61.72n ~ 1.000
SplitSyncedParentMap_SetIfNotExists/16_buckets-4 61.66n 61.61n ~ 1.000
SplitSyncedParentMap_SetIfNotExists/1_bucket-4 61.59n 61.54n ~ 1.000
SplitSyncedParentMap_ConcurrentSetIfNotExists/256_buckets... 31.12n 31.15n ~ 1.000
SplitSyncedParentMap_ConcurrentSetIfNotExists/16_buckets_... 54.63n 52.70n ~ 0.200
SplitSyncedParentMap_ConcurrentSetIfNotExists/1_bucket_pa... 112.5n 121.9n ~ 0.100
MiningCandidate_Stringify_Short-4 264.6n 268.3n ~ 0.100
MiningCandidate_Stringify_Long-4 1.914µ 1.962µ ~ 0.100
MiningSolution_Stringify-4 999.1n 995.9n ~ 1.000
BlockInfo_MarshalJSON-4 1.837µ 1.810µ ~ 0.100
NewFromBytes-4 126.7n 127.7n ~ 0.200
Mine_EasyDifficulty-4 58.48µ 58.63µ ~ 1.000
Mine_WithAddress-4 4.695µ 4.863µ ~ 0.100
DirectSubtreeAdd/4_per_subtree-4 62.14n 60.27n ~ 1.000
DirectSubtreeAdd/64_per_subtree-4 28.90n 29.31n ~ 0.300
DirectSubtreeAdd/256_per_subtree-4 28.39n 27.87n ~ 0.700
DirectSubtreeAdd/1024_per_subtree-4 26.75n 26.77n ~ 0.700
DirectSubtreeAdd/2048_per_subtree-4 26.40n 26.45n ~ 0.700
SubtreeProcessorAdd/4_per_subtree-4 315.4n 314.3n ~ 0.100
SubtreeProcessorAdd/64_per_subtree-4 313.4n 312.1n ~ 0.700
SubtreeProcessorAdd/256_per_subtree-4 312.6n 320.2n ~ 0.700
SubtreeProcessorAdd/1024_per_subtree-4 316.9n 321.1n ~ 0.400
SubtreeProcessorAdd/2048_per_subtree-4 314.6n 319.9n ~ 0.200
SubtreeProcessorRotate/4_per_subtree-4 318.9n 327.2n ~ 0.400
SubtreeProcessorRotate/64_per_subtree-4 317.0n 319.2n ~ 0.700
SubtreeProcessorRotate/256_per_subtree-4 315.2n 316.9n ~ 0.700
SubtreeProcessorRotate/1024_per_subtree-4 308.2n 311.0n ~ 0.400
SubtreeNodeAddOnly/4_per_subtree-4 67.94n 69.16n ~ 0.100
SubtreeNodeAddOnly/64_per_subtree-4 39.95n 40.00n ~ 0.500
SubtreeNodeAddOnly/256_per_subtree-4 38.72n 38.32n ~ 0.400
SubtreeNodeAddOnly/1024_per_subtree-4 37.89n 37.94n ~ 0.700
SubtreeCreationOnly/4_per_subtree-4 164.7n 165.6n ~ 1.000
SubtreeCreationOnly/64_per_subtree-4 829.1n 841.4n ~ 1.000
SubtreeCreationOnly/256_per_subtree-4 2.149µ 2.232µ ~ 0.400
SubtreeCreationOnly/1024_per_subtree-4 5.458µ 5.587µ ~ 1.000
SubtreeCreationOnly/2048_per_subtree-4 8.596µ 9.430µ ~ 1.000
SubtreeProcessorOverheadBreakdown/64_per_subtree-4 310.2n 311.6n ~ 0.400
SubtreeProcessorOverheadBreakdown/1024_per_subtree-4 314.5n 316.3n ~ 0.700
ParallelGetAndSetIfNotExists/1k_nodes-4 980.8µ 1005.5µ ~ 0.100
ParallelGetAndSetIfNotExists/10k_nodes-4 1.940m 2.012m ~ 0.100
ParallelGetAndSetIfNotExists/50k_nodes-4 8.748m 8.813m ~ 0.400
ParallelGetAndSetIfNotExists/100k_nodes-4 17.67m 17.72m ~ 1.000
SequentialGetAndSetIfNotExists/1k_nodes-4 791.2µ 776.4µ ~ 0.100
SequentialGetAndSetIfNotExists/10k_nodes-4 3.025m 3.043m ~ 0.100
SequentialGetAndSetIfNotExists/50k_nodes-4 10.84m 10.90m ~ 1.000
SequentialGetAndSetIfNotExists/100k_nodes-4 20.50m 20.62m ~ 0.200
ProcessOwnBlockSubtreeNodesParallel/1k_nodes-4 1.015m 1.037m ~ 0.400
ProcessOwnBlockSubtreeNodesParallel/10k_nodes-4 4.819m 4.852m ~ 0.400
ProcessOwnBlockSubtreeNodesParallel/100k_nodes-4 19.59m 19.58m ~ 0.700
ProcessOwnBlockSubtreeNodesSequential/1k_nodes-4 850.4µ 847.8µ ~ 0.700
ProcessOwnBlockSubtreeNodesSequential/10k_nodes-4 6.127m 6.351m ~ 0.700
ProcessOwnBlockSubtreeNodesSequential/100k_nodes-4 40.18m 40.45m ~ 0.700
BlockAssembler_AddTx-4 0.02414n 0.02127n ~ 0.700
AddNode-4 9.416 9.052 ~ 0.400
AddNodeWithMap-4 9.051 8.640 ~ 0.100
DiskTxMap_SetIfNotExists-4 3.841µ 3.826µ ~ 1.000
DiskTxMap_SetIfNotExists_Parallel-4 3.682µ 3.886µ ~ 0.700
DiskTxMap_ExistenceOnly-4 307.7n 298.9n ~ 0.100
Queue-4 198.0n 192.0n ~ 0.100
AtomicPointer-4 11.34n 11.29n ~ 0.600
ReorgOptimizations/DedupFilterPipeline/Old/10K-4 798.5µ 780.6µ ~ 0.200
ReorgOptimizations/DedupFilterPipeline/New/10K-4 720.1µ 725.3µ ~ 0.400
ReorgOptimizations/AllMarkFalse/Old/10K-4 117.2µ 123.7µ ~ 0.700
ReorgOptimizations/AllMarkFalse/New/10K-4 58.07µ 58.27µ ~ 0.400
ReorgOptimizations/HashSlicePool/Old/10K-4 68.44µ 64.05µ ~ 0.100
ReorgOptimizations/HashSlicePool/New/10K-4 11.79µ 11.78µ ~ 0.100
ReorgOptimizations/NodeFlags/Old/10K-4 5.228µ 5.311µ ~ 0.100
ReorgOptimizations/NodeFlags/New/10K-4 1.793µ 1.837µ ~ 0.100
ReorgOptimizations/DedupFilterPipeline/Old/100K-4 10.180m 9.454m ~ 0.100
ReorgOptimizations/DedupFilterPipeline/New/100K-4 10.037m 9.081m ~ 0.200
ReorgOptimizations/AllMarkFalse/Old/100K-4 1.143m 1.127m ~ 0.200
ReorgOptimizations/AllMarkFalse/New/100K-4 722.7µ 721.0µ ~ 0.700
ReorgOptimizations/HashSlicePool/Old/100K-4 588.6µ 605.2µ ~ 0.100
ReorgOptimizations/HashSlicePool/New/100K-4 297.2µ 310.0µ ~ 0.100
ReorgOptimizations/NodeFlags/Old/100K-4 55.42µ 49.77µ ~ 0.100
ReorgOptimizations/NodeFlags/New/100K-4 19.18µ 18.34µ ~ 0.100
TxMapSetIfNotExists-4 50.05n 49.66n ~ 0.700
TxMapSetIfNotExistsDuplicate-4 43.54n 43.44n ~ 0.600
ChannelSendReceive-4 686.8n 670.3n ~ 0.100
CalcBlockWork-4 545.7n 553.8n ~ 0.700
CalculateWork-4 750.5n 774.4n ~ 0.400
BuildBlockLocatorString_Helpers/Size_10-4 1.322µ 1.306µ ~ 0.200
BuildBlockLocatorString_Helpers/Size_100-4 12.59µ 12.71µ ~ 0.200
BuildBlockLocatorString_Helpers/Size_1000-4 139.7µ 156.4µ ~ 0.700
CatchupWithHeaderCache-4 104.3m 104.2m ~ 0.400
_BufferPoolAllocation/16KB-4 3.397µ 4.688µ ~ 0.200
_BufferPoolAllocation/32KB-4 8.954µ 7.092µ ~ 0.100
_BufferPoolAllocation/64KB-4 14.99µ 14.47µ ~ 0.700
_BufferPoolAllocation/128KB-4 28.66µ 29.86µ ~ 0.700
_BufferPoolAllocation/512KB-4 116.7µ 115.0µ ~ 0.700
_BufferPoolConcurrent/32KB-4 19.00µ 19.16µ ~ 0.100
_BufferPoolConcurrent/64KB-4 30.63µ 30.62µ ~ 1.000
_BufferPoolConcurrent/512KB-4 145.0µ 143.9µ ~ 0.400
_SubtreeDeserializationWithBufferSizes/16KB-4 661.1µ 615.2µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/32KB-4 650.6µ 628.2µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/64KB-4 641.8µ 620.8µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/128KB-4 643.7µ 621.0µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/512KB-4 648.7µ 611.1µ ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/16KB-4 36.25m 36.30m ~ 1.000
_SubtreeDataDeserializationWithBufferSizes/32KB-4 35.92m 35.44m ~ 0.400
_SubtreeDataDeserializationWithBufferSizes/64KB-4 36.22m 35.87m ~ 1.000
_SubtreeDataDeserializationWithBufferSizes/128KB-4 35.30m 35.26m ~ 1.000
_SubtreeDataDeserializationWithBufferSizes/512KB-4 35.33m 35.45m ~ 0.400
_PooledVsNonPooled/Pooled-4 830.8n 670.3n ~ 0.100
_PooledVsNonPooled/NonPooled-4 6.644µ 6.845µ ~ 0.400
_MemoryFootprint/Current_512KB_32concurrent-4 7.162µ 6.827µ ~ 0.100
_MemoryFootprint/Proposed_32KB_32concurrent-4 9.706µ 9.390µ ~ 0.100
_MemoryFootprint/Alternative_64KB_32concurrent-4 9.308µ 9.044µ ~ 0.100
_prepareTxsPerLevel-4 403.6m 402.7m ~ 0.700
_prepareTxsPerLevelOrdered-4 3.713m 3.494m ~ 0.200
_prepareTxsPerLevel_Comparison/Original-4 407.7m 398.4m ~ 0.100
_prepareTxsPerLevel_Comparison/Optimized-4 3.535m 3.419m ~ 0.100
SubtreeSizes/10k_tx_4_per_subtree-4 1.438m 1.445m ~ 0.700
SubtreeSizes/10k_tx_16_per_subtree-4 336.8µ 335.2µ ~ 0.700
SubtreeSizes/10k_tx_64_per_subtree-4 81.91µ 82.70µ ~ 0.400
SubtreeSizes/10k_tx_256_per_subtree-4 20.78µ 20.79µ ~ 1.000
SubtreeSizes/10k_tx_512_per_subtree-4 10.14µ 10.42µ ~ 0.200
SubtreeSizes/10k_tx_1024_per_subtree-4 5.086µ 5.076µ ~ 0.700
SubtreeSizes/10k_tx_2k_per_subtree-4 2.530µ 2.533µ ~ 1.000
BlockSizeScaling/10k_tx_64_per_subtree-4 80.47µ 81.04µ ~ 1.000
BlockSizeScaling/10k_tx_256_per_subtree-4 20.41µ 20.64µ ~ 0.200
BlockSizeScaling/10k_tx_1024_per_subtree-4 5.139µ 5.113µ ~ 0.700
BlockSizeScaling/50k_tx_64_per_subtree-4 407.3µ 408.9µ ~ 1.000
BlockSizeScaling/50k_tx_256_per_subtree-4 100.1µ 101.0µ ~ 0.400
BlockSizeScaling/50k_tx_1024_per_subtree-4 25.18µ 25.53µ ~ 0.400
SubtreeAllocations/small_subtrees_exists_check-4 165.6µ 169.5µ ~ 0.200
SubtreeAllocations/small_subtrees_data_fetch-4 175.4µ 172.4µ ~ 0.200
SubtreeAllocations/small_subtrees_full_validation-4 334.7µ 331.5µ ~ 1.000
SubtreeAllocations/medium_subtrees_exists_check-4 10.19µ 10.26µ ~ 1.000
SubtreeAllocations/medium_subtrees_data_fetch-4 10.89µ 11.02µ ~ 0.700
SubtreeAllocations/medium_subtrees_full_validation-4 20.81µ 20.80µ ~ 1.000
SubtreeAllocations/large_subtrees_exists_check-4 2.487µ 2.527µ ~ 0.100
SubtreeAllocations/large_subtrees_data_fetch-4 2.678µ 2.676µ ~ 1.000
SubtreeAllocations/large_subtrees_full_validation-4 5.252µ 5.236µ ~ 0.400
StoreBlock_Sequential/BelowCSVHeight-4 241.3µ 248.6µ ~ 0.100
StoreBlock_Sequential/AboveCSVHeight-4 255.8µ 251.8µ ~ 0.700
GetUtxoHashes-4 259.2n 258.5n ~ 0.400
GetUtxoHashes_ManyOutputs-4 44.22µ 42.86µ ~ 0.100
_NewMetaDataFromBytes-4 236.2n 238.0n ~ 0.700
_Bytes-4 611.7n 619.9n ~ 0.100
_MetaBytes-4 562.8n 556.1n ~ 0.200

Threshold: >10% with p < 0.05 | Generated: 2026-04-28 19:07 UTC

@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
70.6% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

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.

1 participant