CWE-190: Integer Overflow or Wraparound¶
CWE-190 | CVSS Base Score range: 5.9-9.8 (when leading to heap corruption) | Rank 23 in CWE Top 25
Functional Semantics¶
Integer overflow occurs when an arithmetic operation produces a mathematical result that cannot be represented in the target integer type. The stored value wraps (unsigned), is undefined (signed C/C++), or saturates (hardware-specific). The corrupted value subsequently propagates into security-sensitive operations: memory allocation sizes, array index calculations, loop bounds, and security threshold comparisons. The direct consequence is typically not exploitable; the exploitable condition arises when the wrapped value is used as a size (CWE-131/CWE-787), an index (CWE-129), or a security gate (bypassed check).
Root Cause¶
Integer types have fixed bit widths. No language-level overflow detection exists in C, C++, Go, or Java by default. The programmer assumes arithmetic results remain in-range; the attacker provides inputs that violate this assumption. The vulnerability is structural: the operation and its use are logically coupled, but the type system does not enforce their combined safety.
Overflow Taxonomy¶
| Type | Behavior | Language | Exploitability |
|---|---|---|---|
| Signed overflow | Undefined behavior (C/C++) — optimizer may eliminate check | C, C++ | High — UB enables compiler optimization that removes bounds checks |
| Unsigned wraparound | Well-defined: (UINT_MAX + 1) → 0 | C, C++, Go | High — predictable, commonly exploited |
| Integer truncation | High bits lost on narrowing cast: (uint8_t)256 → 0 | All | High — often occurs on function parameter cast |
| Multiplication overflow | size_t count * size_t element_size wraps to small value | C, C++ | High — classic heap allocation undersize |
| Signed/unsigned comparison | Signed negative compared as large unsigned positive | C, C++ | Medium — bypasses < MAX_SIZE checks |
Trigger Conditions¶
- User controls a value used in:
malloc(n),new T[n], array subscriptarr[n],memcpy(dst, src, n), loop boundfor(i=0; i<n; i++) - Arithmetic is performed on user input before the security-sensitive use:
malloc(user_count * element_size) - Narrowing cast from wider to narrower type:
int n = (int) user_supplied_size_t - Comparison mixes signed and unsigned:
if (user_int < MAX_UNSIGNED)where user_int is signed
Language-Specific Behavior¶
| Language | Signed Overflow | Unsigned Overflow | Narrowing Cast | Built-in Check |
|---|---|---|---|---|
| C | Undefined behavior (UB) | Wraparound (defined) | Implementation-defined | -ftrapv flag (performance cost) |
| C++ | Undefined behavior | Wraparound (defined) | Implementation-defined | -fsanitize=integer (AddressSanitizer) |
| Go | Wraparound (defined) | Wraparound (defined) | Truncates, no panic | None; math/bits.Add for overflow-safe ops |
| Java | Wraparound (defined) | N/A (no unsigned) | (int) longValue silently truncates | Math.addExact() throws ArithmeticException |
| Rust | Panic in debug, wraparound in release | Same | Compile error (explicit cast required) | checked_add(), saturating_add(), wrapping_add() |
| Python | Arbitrary precision integers — no overflow | No overflow | N/A | N/A — immune |
Vulnerable Patterns¶
C — multiplication overflow in allocation¶
// VULNERABLE: count=65537, size=65536 → 65537*65536 = 4,295,032,832
// On 32-bit: wraps to 65536 — allocates 64KB instead of 4GB
// memcpy then writes 4GB into 64KB buffer → heap overflow
void process_records(size_t count, size_t record_size, const char *src) {
char *buf = malloc(count * record_size); // OVERFLOW: no check
if (!buf) return;
memcpy(buf, src, count * record_size); // writes to undersized buffer
free(buf);
}
C — signed overflow in loop (UB → optimizer removes check)¶
// VULNERABLE: signed integer overflow is UB in C
// Compiler may optimize: since i+1 > i is always true (no UB assumed),
// the loop continuation check may be removed entirely
int i;
for (i = 0; i <= INT_MAX; i++) { // compiler may prove i <= INT_MAX always, infinite loop or skip
process(buf[i]);
}
Java — silent int truncation on long multiplication¶
// VULNERABLE: count * PAGE_SIZE overflows int if count > 2^21 (page size = 1024)
public byte[] allocateBuffer(int count, int pageSize) {
int totalSize = count * pageSize; // wraps negative if > Integer.MAX_VALUE
return new byte[totalSize]; // NegativeArraySizeException or undersized
}
// Example: count=2097153, pageSize=1024 → 2097153*1024 = 2,147,484,672 > INT_MAX
// Actual stored value: -2146482624 → NegativeArraySizeException → DoS
Go — silent wraparound, no panic¶
// VULNERABLE: Go integers wrap silently in all builds
func allocate(count, size int) []byte {
total := count * size // wraps if count*size > MaxInt64
return make([]byte, total) // panic: runtime: out of memory (if large) or undersized alloc
}
// More insidious: security bypass
func checkQuota(used, delta int64) bool {
newUsed := used + delta // if delta is negative: used + large_negative = bypasses quota
return newUsed <= QUOTA_MAX // attacker passes negative delta
}
Fixed Patterns¶
C — safe multiplication with overflow check¶
#include <stdint.h>
#include <stddef.h>
// Method 1: __builtin_mul_overflow (GCC/Clang)
void process_records_safe(size_t count, size_t record_size, const char *src) {
size_t total;
if (__builtin_mul_overflow(count, record_size, &total)) {
return; // overflow detected, reject
}
char *buf = malloc(total);
if (!buf) return;
memcpy(buf, src, total);
free(buf);
}
// Method 2: pre-division check (portable C99)
void process_records_safe_v2(size_t count, size_t record_size, const char *src) {
if (count > 0 && record_size > SIZE_MAX / count) {
return; // would overflow
}
char *buf = malloc(count * record_size);
if (!buf) return;
memcpy(buf, src, count * record_size);
free(buf);
}
Java — Math.addExact / multiplyExact¶
// FIXED: Math.multiplyExact throws ArithmeticException on overflow
public byte[] allocateBuffer(int count, int pageSize) {
try {
int totalSize = Math.multiplyExact(count, pageSize);
if (totalSize > MAX_ALLOWED_SIZE) throw new IllegalArgumentException("Too large");
return new byte[totalSize];
} catch (ArithmeticException e) {
throw new IllegalArgumentException("Buffer size overflow", e);
}
}
// Alternative: promote to long before multiply, check, then cast
public byte[] allocateBuffer2(long count, long pageSize) {
long totalSize = count * pageSize; // long multiplication — overflow at 2^63
if (totalSize < 0 || totalSize > Integer.MAX_VALUE || totalSize > MAX_ALLOWED_SIZE) {
throw new IllegalArgumentException("Invalid size");
}
return new byte[(int) totalSize];
}
Rust — checked arithmetic (explicit overflow handling)¶
// VULNERABLE in release builds (wraps silently):
fn allocate(count: usize, size: usize) -> Vec<u8> {
vec![0u8; count * size] // wraps in release, panics in debug
}
// FIXED: checked_mul returns None on overflow
fn allocate_safe(count: usize, size: usize) -> Result<Vec<u8>, &'static str> {
let total = count.checked_mul(size).ok_or("size overflow")?;
if total > MAX_ALLOCATION {
return Err("allocation too large");
}
Ok(vec![0u8; total])
}
// For financial: saturating_add prevents wraparound, caps at MAX
fn add_to_balance(balance: u64, amount: u64) -> u64 {
balance.saturating_add(amount) // returns u64::MAX instead of wrapping
}
Go — explicit overflow pre-check¶
import "math"
// FIXED: check before multiply
func allocateSafe(count, size int) ([]byte, error) {
if count > 0 && size > math.MaxInt/count {
return nil, fmt.Errorf("allocation overflow: count=%d size=%d", count, size)
}
total := count * size
if total > MaxAllocationSize {
return nil, fmt.Errorf("allocation too large: %d", total)
}
return make([]byte, total), nil
}
Detection Heuristics¶
Static analysis triggers: - malloc(a * b) or calloc(a, b) where a or b derives from user input without prior overflow check - new T[user_value] or make([]T, user_value) without bounds check - Arithmetic on user-controlled values preceding an array subscript: arr[user_a + user_b] - Narrowing cast of user-supplied value: (int)userLong, (uint8_t)userInt, (short)userInt - Mixed signed/unsigned comparison: if (signed_var < unsigned_constant) — implicit promotion may invert logic - Java: int result = (int)(userLong * constant) without Math.multiplyExact
Pattern triggers by consequence: - Allocation undersize: arithmetic result used as allocation size - Index out of bounds: arithmetic result used as array subscript - Security check bypass: arithmetic result used in if (value < threshold) or if (value == expected) - Financial calculation: arithmetic on monetary values where negative results are possible
False positive indicators: - Multiplication where both operands are compile-time constants or enum values with known ranges - User-supplied value already validated against maximum before arithmetic: if (n > 1000) return error; buf = malloc(n * 8); — safe if 1000 * 8 < SIZE_MAX - Rust code using wrapping_add/wrapping_mul intentionally (e.g., hash functions, checksums) — semantic wraparound is the intended behavior - Go code operating on values bounded by previous validation
Compiler Mitigations¶
| Mitigation | Effect | Cost |
|---|---|---|
-ftrapv (GCC/Clang) | Traps on signed overflow at runtime | 5-30% performance |
-fsanitize=integer (UBSan) | Detects UB integer operations | For testing only |
FORTIFY_SOURCE=2 | Hardens stdlib string/memory functions | Minimal |
Java -ea (assertions) | Enables assertion checks; use assert Math.multiplyExact(a,b) > 0 | For testing only |
| Rust debug builds | Panic on overflow | Debug only by default |
Rust overflow-checks = true in Cargo.toml | Panic on overflow in release | ~5-10% performance |
Gotchas¶
- Signed overflow is UB in C/C++, not wraparound. Compiler optimization passes (especially loop strength reduction) exploit the assumption that signed overflow never occurs. Code like
if (i + 1 < i) { handle_overflow(); }may be optimized away entirely because the compiler assumesi + 1 < iis always false (signed overflow → UB → condition always false per compiler). Use-fwrapvto treat signed overflow as wraparound, or use unsigned arithmetic with explicit checks. - calloc vs malloc:
calloc(count, size)performs the overflow check internally on most platforms.malloc(count * size)does not. Prefercallocfor array allocations in C. - Go int size is platform-dependent:
intis 32-bit on 32-bit platforms, 64-bit on 64-bit platforms. Code that passes overflow checks on 64-bit dev machines may overflow on 32-bit targets or older ARM devices. - Rust release mode wraps silently by default. Many Rust developers assume overflow panics everywhere because debug mode does. Security-critical Rust code must use
checked_*operations or enableoverflow-checks = trueinCargo.toml. - Financial arithmetic demands arbitrary precision or decimal types.
float64has 15-17 significant decimal digits but accumulates rounding errors. Usebig.Int/big.Floatin Go,decimal.Decimalin Python,BigDecimalin Java for monetary calculations. Integer overflow in financial code can cause incorrect balances, not just crashes.
See Also¶
- CWE-787: Out Of Bounds Write — most common consequence of allocation undersize from overflow
- CWE-125: Out Of Bounds Read — index overflow leading to read past buffer end
- CWE-369: Divide By Zero — related arithmetic error; attacker-controlled divisor = 0
- CWE-020: Input Validation — upstream control: validate numeric ranges before arithmetic