Summary
Several validation, rate-limiting, and deduplication issues exist in sync message processing:
ChainInventoryMessage.remainNum is not validated against integer overflow, allowing the future block-height upper-bound check to be bypassed.
SYNC_BLOCK_CHAIN rate limiting can be bypassed under a specific combination of fields.
- An off-by-one mismatch exists between the block-fetch deduplication window and the deduplication cache size, allowing a block ID at the window boundary to be requested repeatedly.
These areas should be hardened to ensure stricter validation and more reliable rate limiting and deduplication during synchronization.
Root Cause
1. Future block-height validation bypass through remainNum overflow
ChainInventoryMsgHandler.check() uses the following comparison to limit the future block height claimed by a peer:
lastNum + remainNum > maxFutureNum
However, remainNum is a peer-controlled signed 64-bit value and is not range-checked. When it is set close to Long.MAX_VALUE, the addition may overflow and wrap to a negative value. The comparison then evaluates to false, bypassing the validation intended to reject the message.
2. SYNC_BLOCK_CHAIN rate-limit bypass
The rate-limit check in SyncBlockChainMsgHandler depends on peer.getRemainNum() > 0.
A crafted request combining remainNum == 0 with a forged trailing block ID can bypass the rate-limit check, causing the limiter not to be applied to the request.
Rate limiting should apply to the incoming request itself rather than depend on peer-controlled or previously derived synchronization state.
3. Off-by-one mismatch between the block-fetch window and cache size
FetchInvDataMsgHandler permits block requests within the inclusive range:
[lastSyncBlockId - 2 * SYNC_FETCH_BATCH_NUM, lastSyncBlockId]
With SYNC_FETCH_BATCH_NUM = 2000, this is:
The inclusive window contains 4,001 distinct block numbers. However, syncBlockIdCache, which prevents duplicate requests, has a maximum size of:
2 * SYNC_FETCH_BATCH_NUM = 4000
The valid request window is therefore one entry larger than the deduplication cache.
After 4,000 entries have filled the cache, inserting the 4,001st valid entry may evict an older entry due to the cache's maximum-size eviction policy. Because the evicted block ID remains inside the valid request window, it can be requested again and pass the getIfPresent() deduplication check.
Reproduction
1. remainNum overflow
Construct a ChainInventoryMessage that satisfies the preceding continuity and linkage checks, but set remainNum close to Long.MAX_VALUE.
The addition in the upper-bound check overflows to a negative value, allowing the message to pass validation.
2. Rate-limit bypass
Construct a SYNC_BLOCK_CHAIN request that combines remainNum == 0 with a specially crafted trailing block ID.
Observe that the rate limiter is not invoked as expected.
3. Deduplication off-by-one
- Fill
syncBlockIdCache with 4,000 block IDs from the valid request window.
- Request the 4,001st block ID in the window, causing an older cache entry to be evicted.
- Request the evicted block ID again.
- Because the ID is still within the valid range but no longer exists in the cache, it passes the
getIfPresent() deduplication check and is fetched again.
Impact
- Validation and rate limiting: A peer may cause the node to accept synchronization messages that should have been rejected or process certain requests without applying the intended rate limit. Other continuity and linkage checks still constrain these messages, so the issues do not affect consensus correctness or asset security.
- Deduplication off-by-one: A block ID within the valid request window can fall outside the deduplication cache and become requestable again, causing additional redundant block-fetch work. The impact is bounded: because the handler checks every hash and rejects the entire message if any entry is already cached, at most one entry within the valid window can fall outside the cache at a given time. This results in a rolling single-entry bypass rather than bypassing the entire window.
- Overall, the impact is limited to resource consumption and processing efficiency in the synchronization flow. It does not affect consensus correctness or asset security.
Suggested Fix
-
remainNum: Reject negative or otherwise out-of-range remainNum values. Use Math.addExact() with explicit overflow handling, or use a subtraction-based comparison after validating the relevant ranges, to prevent the future block-height check from being bypassed by integer overflow.
-
SYNC_BLOCK_CHAIN rate limiting: Remove the peer.getRemainNum() > 0 precondition and apply rate limiting to every received SYNC_BLOCK_CHAIN request.
-
Deduplication off-by-one: Increase the maximum size of syncBlockIdCache so that it covers the entire valid request window:
2 * SYNC_FETCH_BATCH_NUM + 1 = 4001
Alternatively, narrow the permitted block-fetch window by one entry so that it matches the existing cache capacity.
Summary
Several validation, rate-limiting, and deduplication issues exist in sync message processing:
ChainInventoryMessage.remainNumis not validated against integer overflow, allowing the future block-height upper-bound check to be bypassed.SYNC_BLOCK_CHAINrate limiting can be bypassed under a specific combination of fields.These areas should be hardened to ensure stricter validation and more reliable rate limiting and deduplication during synchronization.
Root Cause
1. Future block-height validation bypass through
remainNumoverflowChainInventoryMsgHandler.check()uses the following comparison to limit the future block height claimed by a peer:However,
remainNumis a peer-controlled signed 64-bit value and is not range-checked. When it is set close toLong.MAX_VALUE, the addition may overflow and wrap to a negative value. The comparison then evaluates to false, bypassing the validation intended to reject the message.2.
SYNC_BLOCK_CHAINrate-limit bypassThe rate-limit check in
SyncBlockChainMsgHandlerdepends onpeer.getRemainNum() > 0.A crafted request combining
remainNum == 0with a forged trailing block ID can bypass the rate-limit check, causing the limiter not to be applied to the request.Rate limiting should apply to the incoming request itself rather than depend on peer-controlled or previously derived synchronization state.
3. Off-by-one mismatch between the block-fetch window and cache size
FetchInvDataMsgHandlerpermits block requests within the inclusive range:With
SYNC_FETCH_BATCH_NUM = 2000, this is:The inclusive window contains 4,001 distinct block numbers. However,
syncBlockIdCache, which prevents duplicate requests, has a maximum size of:The valid request window is therefore one entry larger than the deduplication cache.
After 4,000 entries have filled the cache, inserting the 4,001st valid entry may evict an older entry due to the cache's maximum-size eviction policy. Because the evicted block ID remains inside the valid request window, it can be requested again and pass the
getIfPresent()deduplication check.Reproduction
1.
remainNumoverflowConstruct a
ChainInventoryMessagethat satisfies the preceding continuity and linkage checks, but setremainNumclose toLong.MAX_VALUE.The addition in the upper-bound check overflows to a negative value, allowing the message to pass validation.
2. Rate-limit bypass
Construct a
SYNC_BLOCK_CHAINrequest that combinesremainNum == 0with a specially crafted trailing block ID.Observe that the rate limiter is not invoked as expected.
3. Deduplication off-by-one
syncBlockIdCachewith 4,000 block IDs from the valid request window.getIfPresent()deduplication check and is fetched again.Impact
Suggested Fix
remainNum: Reject negative or otherwise out-of-rangeremainNumvalues. UseMath.addExact()with explicit overflow handling, or use a subtraction-based comparison after validating the relevant ranges, to prevent the future block-height check from being bypassed by integer overflow.SYNC_BLOCK_CHAINrate limiting: Remove thepeer.getRemainNum() > 0precondition and apply rate limiting to every receivedSYNC_BLOCK_CHAINrequest.Deduplication off-by-one: Increase the maximum size of
syncBlockIdCacheso that it covers the entire valid request window:Alternatively, narrow the permitted block-fetch window by one entry so that it matches the existing cache capacity.