bump a number past the edge and it wraps

++ / -- & Integer Overflow

The ++ and -- operators fold a read, a write, and a value into one token — and the value they hand back is not always the one you expect. Push a bounded integer one step too far and it doesn't overflow upward, it wraps: MAX + 1 becomes MIN, and a wrapped length or index can chain straight into a buffer overflow.

Pre- versus post-increment hides off-by-one and double-counting; bump a fixed-width integer past its max and it silently wraps to the bottom.

01in the wild

In the wild

Pre- vs Post-Increment

i++ yields the old value and ++i the new one, so using the result inline changes what gets stored.

example.java
int i = 0;

// SMELL: post-increment returns the OLD value
int a = i++;        // a = 0, i = 1

// the classic self-assignment trap: i never advances
i = i++;            // i is STILL 0 (old value written back)

// RIGHT: ++i returns the new value; or just be explicit
int b = ++i;        // b = 2, i = 2
i += 1;             // unambiguous
i = i++ evaluates i++ to the old value, bumps i, then overwrites it with the saved old value. The increment is lost.
// observed
i++  -> a = 0, i = 1
i = i++ -> i = 0  (no change!)
++i  -> b = 2
example.js
const arr = [];
let i = 0;

// SMELL: post-increment indexes with the OLD value
arr[i++] = "a";     // writes index 0, then i = 1
arr[i++] = "b";     // writes index 1, then i = 2

// reading inline is where it bites
let n = 0;
console.log(n++ + " then " + n);   // "0 then 1"

// RIGHT: separate the bump from the use
arr.push("a", "b");
Post-increment is fine when the bump stands alone, but combining it with the value it returns hides an off-by-one.
// observed
arr = ['a', 'b']
log: '0 then 1'

Undefined Evaluation Order

Mutating the same variable twice in one expression has no defined order in C — it is undefined behavior.

example.c
int i = 0;
int a[4];

/* UNDEFINED BEHAVIOR: i read & written with no sequence point */
a[i] = i++;          /* which index? the standard does not say */
int x = i++ + ++i;   /* result is not portable -- anything goes */

/* RIGHT: one mutation per statement, ordered explicitly */
a[i] = 0;
i++;
Between sequence points C lets the compiler order sub-expressions freely. Two updates to i in one expression is UB, not just unclear.
// observed
UB: result varies by compiler / flags
right: deterministic, one step at a time
example.py
# Python has no ++ on purpose -- the antidote by design
i = 0
# i++            # SyntaxError

i += 1            # one explicit, atomic step

# +1 in a comprehension stays a value, never a hidden write
nums = [n + 1 for n in range(4)]   # [1, 2, 3, 4]
By omitting ++/--, Python forces the read-modify-write to be a visible statement, sidestepping the whole class of bugs.
// observed
i += 1 -> i = 1
nums   -> [1, 2, 3, 4]

Wraparound at the Boundary

A fixed-width integer has a ceiling; one bump past it wraps to the floor — and a wrapped size can become a buffer overflow.

example.c
/* SMELL: unsigned wraps; signed overflow is undefined behavior */
uint8_t x = 255;
x++;                       /* x == 0, not 256 */

/* CWE-680: an overflowed length feeds an allocation, then a copy */
uint16_t len = a + b;      /* a + b > 65535 wraps to something tiny */
char *buf = malloc(len);   /* too small */
memcpy(buf, src, a + b);   /* writes past the buffer */

/* RIGHT: check before you compute the size */
if (a > UINT16_MAX - b) return -1;   /* would overflow */
size_t n = (size_t)a + b;            /* widen, then allocate */
Wraparound turns a big number into a small one, so the allocation is sized from the wrapped value while the copy uses the real one. Widen the type and range-check before allocating.
// observed
wrap:  len wraps small -> heap overflow on memcpy
guard: oversize input rejected before malloc
example.java
// SMELL: int silently overflows -- no exception
int max = Integer.MAX_VALUE;     // 2147483647
int oops = max + 1;              // -2147483648  (wrapped to MIN)

// the classic: a midpoint that goes negative on big arrays
int mid = (low + high) / 2;      // low+high can overflow

// RIGHT: fail loudly, or compute without overflowing
int safe = Math.addExact(max, 1);   // throws ArithmeticException
int m    = low + (high - low) / 2;  // never overflows
Java's int wraps on overflow instead of throwing, so a bad sum sails on as a negative. Math.addExact makes the overflow an exception; the (high - low) form avoids it entirely.
// observed
wrap:    MAX + 1 -> MIN (no error)
addExact: throws ArithmeticException
example.rs
// Rust makes the choice explicit instead of guessing.
let x: u8 = 255;
// let y = x + 1;        // debug: panics; release: wraps to 0

// Spell out which behavior you actually want:
let wrapped = x.wrapping_add(1);     // 0, on purpose
let checked = x.checked_add(1);      // None -> you must handle it
let (v, of) = x.overflowing_add(1);  // (0, true) -- carry flag

match checked {
    Some(n) => println!("{n}"),
    None    => eprintln!("would overflow"),
}
A bare + can panic in debug and wrap in release, so Rust gives named operations: wrapping_, checked_, and overflowing_ each make the intent visible and reviewable.
// observed
checked_add: None -> handled by match
wrapping_add: 0 (wrap chosen deliberately)
02cross-pollination

Where this compounds

Runtime Errors
03weakness catalog

Mapped weaknesses (CWE)

On its own, this defect is catalogued by MITRE as one or more of these weaknesses. The exploitable vulnerability usually appears only when it chains or combines with another.