Integer Overflow Bugs in Rust Release Builds and Fixes
Integer overflow panics in debug builds and wraps in release builds by default. Learn how that becomes a vulnerability, which arithmetic to use for untrusted numbers and how to enable checks in release.

Rust prevents out-of-bounds memory access, which leads many readers to think arithmetic bugs cannot hurt them. An overflowed length does not corrupt memory in safe Rust. It produces a wrong number that later logic trusts: a price that wraps to a small value, a quota check that passes, a buffer of 4 bytes where 4 GB was requested. This post shows how it happens and the arithmetic to use. It builds on release versus debug, in the security guide.
Rust 1.90, edition 2024.
Two builds, two behaviors
fn total_cents(unit: u32, qty: u32) -> u32 {
unit * qty
}
fn main() {
println!("{}", total_cents(100_000, 100_000));
}In a debug build the program panics:
thread 'main' panicked at src/main.rs:2:5:
attempt to multiply with overflowIn a release build it prints 1410065408, the product modulo 2³². The real answer is 10,000,000,000. A checkout that computes the amount to charge this way charges 14 million cents instead of 10 billion. The code is memory safe and wrong.
Make the intent explicit
fn total_cents(unit: u32, qty: u32) -> Option<u32> {
unit.checked_mul(qty)
}| Method | On overflow | Use for |
|---|---|---|
checked_add, checked_mul | returns None | untrusted numbers, with error handling |
saturating_add | clamps at MAX | counters, scores |
wrapping_add | wraps, by choice | hashes, checksums, ring indices |
overflowing_add | returns value and flag | multi-word arithmetic |
Choosing a variant documents what you meant and behaves the same in debug and release. For a chain of operations, use try_fold or the ? operator on Option.
Casts are the quiet one
let big: u64 = 5_000_000_000;
let small = big as u32; // 705032704, no warning
let safe = u32::try_from(big); // Err(TryFromIntError)as truncates and wraps without complaint. Clippy has lints for this (cast_possible_truncation, cast_sign_loss in the pedantic group). Use try_from for anything that came from input, and From when the conversion is lossless (From and Into).
Sizes and allocations
The classic bug: let bytes = count * item_size; with attacker-controlled count, then Vec::with_capacity(bytes). A wrapped product allocates less than the loop writes, or in unsafe code corrupts memory. In safe Rust the loop still bounds-checks, so you get a panic, not a breach; in unsafe or FFI you get the breach. Use checked_mul for every size computation and cap the result (limits for parsers).
Turning checks on
[profile.release]
overflow-checks = trueNow release builds panic like debug. Expect a small slowdown in arithmetic-heavy code; measure it (benchmarking pitfalls). Many security-minded projects accept the cost. Individual hot loops can opt out with wrapping_* methods.
Finding the sites
Clippy's arithmetic_side_effects lint (restriction group) flags every unchecked operation. Enable it on the modules that handle input. Fuzzing with overflow checks on finds the inputs that trigger it.
Limits
This post covers integer types. Floating-point has its own surprises with NaN and infinity, and usize is 32 bits on some targets. The language reference describes the behavior in integer overflow.


