Skip to content

Lumi Security Audit: Security Feedback for simulator.rs #16063

Description

@anakette

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

Beacon Details


GitHub Issue: Potential Infinite Loop and Denial of Service (DoS) in Bandwidth Simulator via Zero-Sized Receipts

1. Vulnerability Summary

An infinite loop vulnerability exists in the ChainSimulator::apply_chunk method. If a configured receipt size generator returns a size of 0 bytes, the loop condition for generating new simulated receipts will never be satisfied, and the loop will execute indefinitely. This leads to 100% CPU utilization and eventual memory exhaustion (Out of Memory) as elements are endlessly appended to an internal vector.


2. Severity

  • Severity: Medium
  • Context: Test Suite / Simulator. While this does not affect the live consensus network directly, it can be triggered during local execution or CI/CD testing pipelines, leading to hanging jobs, runner timeouts, and resource starvation.

3. Detailed Description

In simulator.rs, the simulator attempts to fill the outgoing buffer of a shard up to a threshold of 20MB (20_000_000 bytes). The generation logic is governed by the following while loop:

let mut outgoing_buffer_size: u64 = outgoing_buffer.iter().map(|r| r.size).sum();
// Produce new receipts until the buffer size exceeds 20MB
while outgoing_buffer_size < 20_000_000 {
    let new_receipt_size = link_generator.generate_receipt_size(&mut self.rng);
    let new_receipt = SimReceipt { size: new_receipt_size.as_u64() };

    if *outgoing_limit >= new_receipt.size {
        *outgoing_limit -= new_receipt.size;
        outgoing_receipts_to_receiver.push(new_receipt);
    } else {
        outgoing_buffer_size += new_receipt.size;
        outgoing_buffer.push_back(new_receipt);
    }
}

If the link_generator returns a receipt size of 0:

  1. new_receipt.size is initialized to 0.
  2. The condition *outgoing_limit >= new_receipt.size evaluates to *outgoing_limit >= 0, which is always true because outgoing_limit is an unsigned integer (u64).
  3. The simulator enters the if block, subtracts 0 from outgoing_limit (no-op), and appends the zero-sized receipt to outgoing_receipts_to_receiver.
  4. Crucially, outgoing_buffer_size is never incremented because the else branch is bypassed.
  5. The loop condition outgoing_buffer_size < 20_000_000 remains true, causing the loop to repeat indefinitely.

This issue can be triggered if any custom or randomized receipt generator (e.g., during fuzzing or parameter testing) returns 0 as a valid receipt size.


4. Impact

  • Denial of Service (DoS): Test runner threads executing the simulation will hang indefinitely.
  • Resource Exhaustion: The outgoing_receipts_to_receiver vector grows without bound, consuming all available system memory until the operating system terminates the process via the Out-Of-Memory (OOM) killer.

5. Proof of Concept / Affected Code Snippet

The vulnerability is located in runtime/runtime/src/bandwidth_scheduler/simulator.rs within the apply_chunk function:

// File: runtime/runtime/src/bandwidth_scheduler/simulator.rs
// Line reference: inside apply_chunk method

                // Produce new receipts until the buffer size exceeds 20MB
                while outgoing_buffer_size < 20_000_000 {
                    let new_receipt_size = link_generator.generate_receipt_size(&mut self.rng);
                    let new_receipt = SimReceipt { size: new_receipt_size.as_u64() };

                    if *outgoing_limit >= new_receipt.size {
                        *outgoing_limit -= new_receipt.size;
                        outgoing_receipts_to_receiver.push(new_receipt);
                    } else {
                        outgoing_buffer_size += new_receipt.size;
                        outgoing_buffer.push_back(new_receipt);
                    }
                }

6. Remediation / Corrected Code

To resolve this issue, enforce a minimum non-zero progress increment inside the loop. Either prevent the generation of zero-sized receipts or explicitly handle them to ensure the loop terminates:

                // Produce new receipts until the buffer size exceeds 20MB
                while outgoing_buffer_size < 20_000_000 {
                    let new_receipt_size = link_generator.generate_receipt_size(&mut self.rng);
                    let size_val = new_receipt_size.as_u64();
                    
                    // Prevent infinite loops from 0-sized receipts
                    if size_val == 0 {
                        // Either skip, default to a minimum of 1 byte, or break.
                        // Assuming 1 byte minimum for simulation sanity:
                        panic!("Receipt size generator returned 0, which would cause an infinite loop");
                    }

                    let new_receipt = SimReceipt { size: size_val };

                    if *outgoing_limit >= new_receipt.size {
                        *outgoing_limit -= new_receipt.size;
                        outgoing_receipts_to_receiver.push(new_receipt);
                    } else {
                        outgoing_buffer_size += new_receipt.size;
                        outgoing_buffer.push_back(new_receipt);
                    }
                }

🌐 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