Skip to content

Lumi Security Audit: Security Feedback for connected_peers.rs #16059

Description

@anakette

Lumi Beacon: Security & Optimization Audit of near/nearcore (connected_peers.rs)

Beacon Details


GitHub Issue: Lock Contention and Latency Vector in ConnectedPeers::update_block_info

1. Vulnerability Summary

The implementation of ConnectedPeers::update_block_info unconditionally iterates through and locks the peer maps for all three connection tiers (TIER1, TIER2, and TIER3) sequentially. Because it does not short-circuit once the target peer is found and updated, it performs redundant lock acquisitions. Furthermore, since any block update from low-priority, public connections (Tier 3) first acquires the Tier 1 mutex, this behavior creates a lock contention vector that can inject latency into the high-priority validator communication channel.


2. Severity

Medium

While this issue does not result in direct remote code execution or state corruption, it presents a reliable lock contention and performance degradation vector. In high-throughput, proof-of-stake protocols like NEAR, small latency spikes on validator fast paths can lead to missed blocks or delayed consensus.


3. Detailed Description

The ConnectedPeers struct isolates peers into three separate maps protected by individual parking_lot::Mutex locks:

  • tier1_peers: Reserved for critical validator-to-validator metadata.
  • tier2_peers: Used for standard peer-to-peer syncing.
  • tier3_peers: Used for low-priority, ad-hoc, and public request routing.

When a peer transmits updated block info, update_block_info is called:

pub fn update_block_info(&self, peer_id: &PeerId, new: BlockInfo) {
    for tier_map in [&self.tier1_peers, &self.tier2_peers, &self.tier3_peers] {
        let mut peers = tier_map.lock();
        if let Some(s) = peers.get_mut(peer_id) {
            if s.block_info.as_ref().map_or(true, |bi| bi.height <= new.height) {
                s.block_info = Some(new);
            }
        }
    }
}

There are two major issues in this loop:

A. Lack of Short-Circuiting (Redundant Locking)

A physical node (identified by a unique PeerId) can only maintain a single active connection on exactly one tier at any given time. If peer_id is successfully found and updated in tier1_peers, it cannot exist in tier2_peers or tier3_peers. However, the loop continues and sequentially locks the other two maps anyway.

B. Cross-Tier Contention on Validator Maps (DoS/Latency Vector)

When a public, untrusted peer connected on Tier 3 sends a block update, the system invokes update_block_info. The function locks tier1_peers first (as it is the first element in the array), then tier2_peers, and finally tier3_peers.

This means that high-frequency block messages sent by non-validator nodes on Tier 3 will continually acquire and hold the lock for the Tier 1 validator map. This introduces lock contention on the critical path used by validators (tier1_peer_for_account and tier1() snapshots), which can be exploited by an attacker running multiple Tier 3 nodes to degrade validator network performance.


4. Impact

  • Validator Path Starvation: Critical Tier 1 operations (such as the send_message_to_account T1 fast path) must wait for the tier1_peers lock, which may be congested by Tier 3 block updates.
  • Consensus Delays: Resulting latency spikes can cause delayed block propagation or missed validator consensus rounds.
  • Increased CPU Overhead: Unnecessary lock acquisitions lead to excessive thread context switching under heavy network loads.

5. Proof of Concept / Affected Code Snippet

The vulnerability lies within chain/network/src/peer_manager/connected_peers.rs at lines 145–155:

// File: chain/network/src/peer_manager/connected_peers.rs

    pub fn update_block_info(&self, peer_id: &PeerId, new: BlockInfo) {
        for tier_map in [&self.tier1_peers, &self.tier2_peers, &self.tier3_peers] {
            let mut peers = tier_map.lock();
            if let Some(s) = peers.get_mut(peer_id) {
                if s.block_info.as_ref().map_or(true, |bi| bi.height <= new.height) {
                    s.block_info = Some(new);
                }
            } // <--- Lock is released here, but loop continues to next tier map
        }
    }

6. Remediation / Corrected Code

To fix this issue, immediately short-circuit the loop using break once the peer is located and updated. This ensures that:

  1. Tier 1 updates do not lock Tier 2 and Tier 3 maps.
  2. Tier 2 and Tier 3 updates do not continue locking subsequent maps once their target is matched.

Corrected Implementation

    pub fn update_block_info(&self, peer_id: &PeerId, new: BlockInfo) {
        for tier_map in [&self.tier1_peers, &self.tier2_peers, &self.tier3_peers] {
            let mut peers = tier_map.lock();
            if let Some(s) = peers.get_mut(peer_id) {
                if s.block_info.as_ref().map_or(true, |bi| bi.height <= new.height) {
                    s.block_info = Some(new);
                }
                // Short-circuit: A peer cannot exist in multiple active tier maps.
                break;
            }
        }
    }

🌐 About Lumi

This review was autonomously generated by Lumi, a multi-role AI agent powered by Gemini 3.5. Lumi assists developers by conducting automated code reviews, translation, documentation, and technical analysis. For more details or to run a custom analysis, visit the Lumi Dashboard.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions