Skip to content

Lumi Security Audit: Security Feedback for backfill_receipt_to_tx.rs #16056

Description

@anakette

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

Beacon Details


GitHub Issue: Potential Integer Overflow and Resource Exhaustion in Database Backfill Utility

1. Vulnerability Summary

The database backfill utility backfill_receipt_to_tx.rs contains potential arithmetic overflow vulnerabilities during block height calculations and lacks proper validation/bounds on user-supplied parameters (batch_size and num_threads). This can lead to runtime crashes (panics in debug builds or unexpected wrapping behavior in release builds) and Out-of-Memory (OOM) conditions.


2. Severity

Low
Reasoning: This is an offline administrative database tool run locally by node operators rather than a remotely exploitable service on the live P2P network. However, failure of database tools during migration can interrupt node maintenance operations.


3. Detailed Description

Issue A: Unsafe Arithmetic Operations

There are two places where basic arithmetic is used on BlockHeight (which is an alias for u64) without overflow protection:

  1. Progress Bar Initialization: The calculation to_height - from_height + 1 assumes that to_height - from_height will not equal u64::MAX. If from_height is 0 and to_height is u64::MAX, adding 1 will cause an arithmetic overflow.
  2. Chunk Calculation: The calculation height + batch_size - 1 can overflow if height is close to the upper bound of u64.

In Rust, arithmetic overflows trigger a panic in debug profiles and wrap around (using two's complement) in release profiles. Wrapping around can lead to logical errors, such as creating an incorrect chunk range or triggering infinite loops if chunk_end wraps to a value lower than height.

Issue B: Memory Exhaustion via Unbounded Batch Size

The batch_size option is read directly from command-line arguments and converted to u64 with only a lower bound check (max(1)). Within the main processing loop, the heights are collected into a vector:

let heights: Vec<BlockHeight> = (height..=chunk_end).collect();

If an operator accidentally provides an extremely large value for --batch-size (e.g., 1_000_000_000), the program will attempt to allocate a massive vector of u64 values in memory, causing an immediate Out-Of-Memory (OOM) abort.


4. Impact

  • Denial of Service (Local): Node operators executing database migrations could experience unexpected crashes or resource exhaustion on host servers.
  • Data Inconsistency / Infinite Loops: If arithmetic wraps silently in production builds, the loop termination condition height <= to_height might behave unexpectedly, potentially causing infinite loops or incorrect database checkpoints.

5. Proof of Concept / Affected Code Snippet

Unsafe Range and Progress Calculation:

tools/database/src/backfill_receipt_to_tx.rs

// Potential overflow if to_height is u64::MAX and from_height is 0
let progress = ProgressBar::new(to_height - from_height + 1);

Unsafe Chunk End Calculation & Allocation:

let batch_size = options.batch_size.max(1) as u64;
let mut height = from_height;

while height <= to_height {
    // Potential overflow if height + batch_size wraps around
    let chunk_end = (height + batch_size - 1).min(to_height);
    let blocks_before = stats.blocks_processed;

    // Unbounded memory allocation based on batch_size
    let heights: Vec<BlockHeight> = (height..=chunk_end).collect();
    ...
    height = chunk_end + 1; // Potential overflow if chunk_end is u64::MAX
}

6. Remediation / Corrected Code

To mitigate these issues:

  1. Use safe saturating or checked arithmetic for block height adjustments.
  2. Set a sensible upper bound on the CLI parameters for batch_size and num_threads.

Suggested Patch:

// 1. In BackfillReceiptToTxCommand parsing / validation
impl BackfillReceiptToTxCommand {
    pub(crate) fn run(
        &self,
        home: &PathBuf,
        genesis_validation: GenesisValidationMode,
    ) -> anyhow::Result<()> {
        // Enforce safe limits on batch size and threads to prevent OOM/exhaustion
        let validated_batch_size = self.batch_size.clamp(1, 100_000); 
        let validated_num_threads = self.num_threads.clamp(1, 512);

        // ... Config and storage loading ...

        // Safe subtraction and addition for progress calculation
        let range_len = to_height
            .checked_sub(from_height)
            .and_then(|diff| diff.checked_add(1))
            .context("Invalid block range calculation overflow")?;

        let progress = ProgressBar::new(range_len);
        
        let options = BackfillOptions {
            batch_size: validated_batch_size,
            num_threads: validated_num_threads,
            use_checkpoint: !self.no_checkpoint,
        };
        
        // ... Call to backfill_receipt_to_tx ...
    }
}

// 2. In backfill_receipt_to_tx core loop logic
pub fn backfill_receipt_to_tx(
    chain_store: &ChainStore,
    storage: &BackfillStorage,
    from_height: BlockHeight,
    to_height: BlockHeight,
    options: &BackfillOptions,
    progress: Option<&ProgressBar>,
) -> anyhow::Result<BackfillStats> {
    let mut stats = BackfillStats::default();

    let pool = rayon::ThreadPoolBuilder::new()
        .num_threads(options.num_threads)
        .build()
        .context("failed to build thread pool")?;

    let batch_size = options.batch_size.max(1) as u64;
    let mut height = from_height;

    while height <= to_height {
        // Use saturating_add and checked_sub to prevent overflow
        let offset = batch_size.saturating_sub(1);
        let chunk_end = height.saturating_add(offset).min(to_height);
        
        let blocks_before = stats.blocks_processed;
        let heights: Vec<BlockHeight> = (height..=chunk_end).collect();
        
        let checkpoint = options.use_checkpoint.then_some((BACKFILL_CHECKPOINT_KEY, chunk_end));
        let batch_stats = process_one_batch(chain_store, storage, &pool, &heights, checkpoint)?;
        stats.add(&batch_stats);

        if let Some(p) = progress {
            p.inc(heights.len() as u64);
        }

        // Safe increment to avoid overflow when chunk_end is u64::MAX
        if chunk_end == u64::MAX {
            break;
        }
        height = chunk_end + 1;
    }

    Ok(stats)
}

🌐 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