Back to Blog
medium SEVERITY7 min read

Integer Overflow in Rust: How Unchecked Addition Can Bypass File Size Limits

A medium-severity integer overflow vulnerability was discovered and patched in a Rust file transfer receiver, where unchecked byte accumulation could allow attackers to bypass file size limits by exploiting arithmetic wraparound in release builds. The fix replaces a simple `+=` operation with Rust's `checked_add` method, which returns an error instead of silently wrapping around. This is a great reminder that even memory-safe languages like Rust can harbor subtle numeric vulnerabilities in relea

O
By orbisai0security
May 18, 2026
#rust#integer-overflow#file-transfer#secure-coding#resource-exhaustion#cwe-190#vulnerability-fix

Integer Overflow in Rust: How Unchecked Addition Can Bypass File Size Limits

Introduction

Rust is celebrated for its memory safety guarantees — no null pointer dereferences, no buffer overflows, no use-after-free bugs. But "memory safe" doesn't mean "numerically safe." A subtle and dangerous class of bugs can still sneak through: integer overflow.

This post dives into a real-world vulnerability found in a Rust file transfer receiver where a single unchecked += operation could allow an attacker to feed unlimited data into a system that believed it was enforcing size limits. We'll break down exactly how it works, why Rust's release mode makes it worse, and how a one-line fix closes the door.

Whether you're new to Rust or a seasoned systems programmer, this vulnerability is a valuable reminder: safe memory management and safe arithmetic are two different things.


The Vulnerability Explained

What Went Wrong

In the file transfer receiver (src/transfer/receiver.rs), the code tracked how many bytes had been received from a sender using a running total:

// The vulnerable code
self.bytes_received += n;

Simple, right? The problem is what happens when bytes_received gets very, very large.

Rust's Debug vs. Release Mode Behavior

Here's the critical detail that makes this dangerous:

  • In debug builds, Rust panics on integer overflow. Your program crashes with a clear error message.
  • In release builds (cargo build --release), integer overflow silently wraps around — just like C and C++.

This is a deliberate performance trade-off documented by the Rust team, but it means production code is vulnerable in ways that testing (usually done in debug mode) will never catch.

When a u64 value exceeds its maximum (18,446,744,073,709,551,615), it wraps back to 0. When a usize on a 64-bit system does the same, the accumulated byte counter suddenly looks tiny — even though gigabytes or terabytes of data may have already been processed.

How an Attacker Exploits This

Here's the attack scenario, step by step:

  1. Attacker initiates a file transfer, advertising a file size just under the system's maximum allowed limit (e.g., MAX_SIZE - 1 bytes).
  2. The receiver accepts the transfer because the advertised size passes validation.
  3. The attacker sends data in chunks, far exceeding the advertised size.
  4. The bytes_received counter accumulates until it approaches u64::MAX.
  5. Overflow occurs — the counter wraps around to a small value (e.g., near 0).
  6. Size limit checks now pass again, because the wrapped value appears to be well within bounds.
  7. The attacker continues sending data indefinitely, exhausting memory, disk space, or CPU — a classic resource exhaustion / denial-of-service attack.
bytes_received progression (u64):
  0 → 1,000,000 → ... → 18,446,744,073,709,551,615 → 0 (OVERFLOW!)
                                                         ^
                                                   Size check passes again!

Real-World Impact

  • Denial of Service (DoS): Unlimited data floods the receiver, consuming memory and disk.
  • Security Bypass: Any downstream logic gated on bytes_received (logging, billing, rate limiting) can be fooled.
  • Data Integrity Issues: Systems that trust the byte counter for integrity checks may produce incorrect results.
  • Amplified Risk in File Import Flows: Combined with the lack of JSON depth limits mentioned in the broader vulnerability description, a crafted import file could simultaneously trigger overflow and deeply nested parsing — compounding the resource exhaustion.

The Fix

What Changed

The fix is elegant and idiomatic Rust — replace the unchecked addition with checked_add, which returns None on overflow instead of wrapping:

Before (vulnerable):

self.bytes_received += n;

After (secure):

self.bytes_received = self.bytes_received
    .checked_add(n)
    .ok_or_else(|| FenvoyError::InvalidMessage("bytes_received overflow".into()))?;

How It Works

Rust's standard library provides checked arithmetic methods on all integer types:

Method Behavior on Overflow
checked_add(n) Returns None
saturating_add(n) Returns MAX value
wrapping_add(n) Wraps around (explicit)
overflowing_add(n) Returns (result, did_overflow)

checked_add returns an Option<T>:
- Some(result) if the addition succeeded without overflow
- None if overflow would have occurred

The .ok_or_else(...) call converts None into a meaningful error, and the ? operator propagates that error up the call stack — terminating the transfer cleanly with an informative error message instead of silently continuing with a corrupted counter.

Why This Fix Is the Right Approach

There are several ways to handle overflow, but checked_add with an error is the best choice here because:

  1. It fails fast — the transfer is immediately terminated when something impossible happens.
  2. It's explicit — future maintainers can see that overflow is a considered case, not an accident.
  3. It's informative — the error message "bytes_received overflow" makes debugging and log analysis straightforward.
  4. It doesn't mask bugs — unlike saturating_add (which would silently cap the value), an error forces the issue to be handled.

Prevention & Best Practices

1. Use Checked Arithmetic for Security-Sensitive Counters

Any time you're tracking sizes, lengths, offsets, or counts that feed into security decisions, use checked arithmetic:

// ✅ Safe: explicit overflow handling
let new_total = current
    .checked_add(chunk_size)
    .ok_or(MyError::Overflow)?;

// ✅ Also safe when you want to cap: saturating
let display_count = count.saturating_add(1);

// ❌ Dangerous in release builds: silent wraparound
let total = current + chunk_size;

2. Enable Overflow Checks in Release Builds (For Critical Code)

You can opt into overflow panics in release mode via Cargo.toml:

[profile.release]
overflow-checks = true

This adds a small runtime cost but provides the same overflow protection as debug builds. Consider enabling this for security-critical services.

3. Validate Early, Validate Often

Don't just check the advertised file size — continuously validate the actual bytes received against your limits:

const MAX_TRANSFER_BYTES: u64 = 100 * 1024 * 1024; // 100 MB

fn add_bytes(&mut self, n: u64) -> Result<(), TransferError> {
    self.bytes_received = self.bytes_received
        .checked_add(n)
        .ok_or(TransferError::Overflow)?;

    if self.bytes_received > MAX_TRANSFER_BYTES {
        return Err(TransferError::SizeLimitExceeded);
    }

    Ok(())
}

4. Pair Numeric Safety with Resource Limits

This vulnerability exists in a broader context of missing resource limits. Secure file import code should enforce:

  • Maximum file size (before loading into memory)
  • Maximum JSON nesting depth (use serde_json's depth limits or a streaming parser)
  • Maximum number of entries in arrays/objects
  • Timeouts on long-running transfers
// Example: limiting JSON nesting depth with serde_json
use serde_json::de::Deserializer;

let mut deserializer = Deserializer::from_str(&json_string);
deserializer.disable_recursion_limit(); // ← DON'T do this!

// ✅ Instead, use a depth-limited approach or a streaming parser

5. Test in Release Mode

Many Rust developers only run tests in debug mode. Add release-mode tests for numeric edge cases:

cargo test --release

Or write explicit tests for overflow scenarios:

#[test]
fn test_bytes_received_overflow_is_rejected() {
    let mut receiver = Receiver::new();
    receiver.bytes_received = u64::MAX - 10;

    // Adding 100 bytes should fail, not wrap around
    assert!(receiver.add_bytes(100).is_err());
}

6. Use Linters and Static Analysis

  • cargo clippy — Catches many common Rust mistakes; some overflow patterns are flagged.
  • cargo audit — Checks for known vulnerabilities in dependencies.
  • rust-clippy lint checked_conversions — Suggests safer numeric conversions.
  • Fuzzing with cargo-fuzz — Extremely effective at finding numeric edge cases in parsers and receivers.

Relevant Security Standards


Conclusion

This vulnerability is a perfect case study in the gap between "memory safe" and "fully safe." Rust's ownership model prevents an entire class of bugs that plague C and C++ — but integer arithmetic in release mode is still a sharp edge that can cut you if you're not careful.

The key takeaways:

  • 🦀 Rust release builds do NOT panic on integer overflow — silent wraparound is the default.
  • 🔢 Use checked_add (and friends) for any security-sensitive arithmetic — especially byte counters, size tracking, and index calculations.
  • 🚦 Validate continuously, not just at the start — an attacker controls the data stream, not just the initial handshake.
  • 🧪 Test in release mode — your debug tests won't catch overflow bugs in production.
  • 📏 Pair numeric safety with resource limits — overflow protection and size caps are complementary, not alternatives.

A single += replaced with checked_add closed this vulnerability. It's a small change with a big impact — and a great reminder that secure code is built from careful, deliberate choices at every level, even the arithmetic.

Write safe code. Check your math. Ship with confidence.


Found a vulnerability in your own codebase? Consider responsible disclosure and always patch promptly. Security is a team sport.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #4

Related Articles

medium

Command Injection in Python Subprocess: A Security Fix Case Study

A medium-severity command injection vulnerability was discovered and fixed in a Python testing utility where unsanitized input could be passed to subprocess calls. This fix demonstrates the critical importance of input validation and safe subprocess handling to prevent attackers from executing arbitrary system commands.

medium

Buffer Overflow in miniz.h: How a Missing Length Check Could Lead to Privilege Escalation

A medium-severity buffer overflow vulnerability was discovered and patched in the miniz.h file embedded within the KittyMemoryEx library, a memory manipulation tool used on Android and iOS platforms. The missing buffer-length check could have allowed attackers to exploit ZIP processing code to achieve arbitrary code execution with elevated privileges. This post breaks down how the vulnerability works, why it's dangerous in privileged contexts, and what developers can do to prevent similar issues

medium

Resource Exhaustion via Unchecked File Imports: How Missing Limits Create DoS Vulnerabilities

A medium-severity vulnerability in a file transfer receiver allowed attackers to exhaust server resources by sending maliciously crafted import files with no size limits, no JSON depth restrictions, and millions of entries loaded directly into memory. The fix introduces explicit input validation guards that reject unauthenticated or malformed requests before any disk or network operations begin. Understanding this class of vulnerability is essential for any developer building file ingestion pipe