Integer Overflow: The Hardware Wraps, the Language Decides
Add one to the largest 8-bit signed value and the bits roll around to the most negative one. The hardware behaviour is simple and identical everywhere; what your language claims about it ranges from "wraps" to "this can never happen, and I will optimise on that basis".
Software view, hardware view
The gap between what you wrote and what the machine does is where this whole domain lives.
What the bits actually do
In eight-bit two's complement the representable signed range is −128 to 127. The pattern for 127 is 0111 1111; adding one carries through the low seven bits and sets the top bit, producing 1000 0000, which that encoding defines as −128. Nothing exceptional happened — the adder did exactly what Two's Complement: One Circuit for Addition and Subtraction describes, and the ninth bit fell off the end as it always does.
The CPU does notice. Architectures maintain condition flags, and a signed overflow sets a distinct one from an unsigned carry, precisely because the same bit pattern means different things under the two interpretations. Adding 1111 1111 and 0000 0001 sets the carry flag (unsigned 255 + 1 exceeded the range) while the signed reading (−1 + 1 = 0) is perfectly fine. One addition, two verdicts, and the instruction cannot know which you meant.
What almost no language does by default is *check* those flags. Doing so would cost a conditional branch after every arithmetic operation, so the flags sit there and the program continues with the wrapped value. That is the whole mechanism: the hardware is not silent, but nobody is listening.
Signed overflow: 127 + 1
0111 1111 ( 127)
+ 0000 0001 ( 1)
------------
1000 0000 (-128) <- signed overflow flag SET
carry flag CLEAR
Unsigned wrap: 255 + 1
1111 1111 ( 255 unsigned / -1 signed)
+ 0000 0001 ( 1)
------------
1 0000 0000 ( 0) <- carry flag SET
signed overflow flag CLEAR
Same adder, same discarded ninth bit. Which flag matters
depends on how YOU meant to interpret the operands; the
instruction has no idea.Four languages, four different promises
This is where the domain boundary matters. The hardware behaviour above is essentially universal. The *language* behaviour is not, and the differences are not stylistic — they change what the optimiser is permitted to do to your code.
The critical case is C and C++, where signed overflow is undefined behaviour while unsigned overflow is defined to wrap. Undefined behaviour does not mean "wraps unpredictably"; it means the compiler may assume it never occurs. A check written as if (x + 1 < x) to detect signed overflow can be deleted entirely, because for a compiler that assumes no overflow, that condition is always false. The check disappears and the overflow it was guarding against happens anyway.
This is why the recommended forms are the ones that never overflow in the first place — rearranging a + b > MAX into a > MAX - b, or using explicit checked-arithmetic intrinsics that report overflow without invoking it.
| Language | Signed overflow | What it means for you |
|---|---|---|
| C / C++ | Undefined behaviour | The optimiser may assume it cannot happen and delete checks that depend on it. Unsigned overflow, by contrast, is defined to wrap. |
| Java / C# | Wraps, defined | Predictable and portable, but silent — no signal that anything went wrong. |
| Rust | Panics in debug, wraps in release | Caught during development; explicit checked_, wrapping_ and saturating_ methods express the intent you actually want. |
| Python | Cannot occur | Integers are arbitrary precision and grow as needed. The cost is that they are heap objects, not machine words. |
| JavaScript | Not applicable to number | Numbers are doubles; integer precision is lost beyond 2^53. BigInt provides arbitrary precision separately. |
Why this is a security topic, not just a correctness one
The canonical exploit shape is an allocation size that wraps. Code computes count * size to decide how large a buffer to allocate, the product overflows to a small number, a small buffer is allocated, and then the original large count is used to write into it. The bounds check passed because it was checking the wrapped value.
The same shape appears with lengths, offsets and indices. What makes it dangerous is that the check *looks* correct — the arithmetic is right for every input the author considered, and the failure only appears at values near the type's limit, which are exactly the values an attacker will supply.
The defence is structural rather than vigilant: compute in a wider type where the product cannot overflow, or use checked arithmetic that reports the condition rather than producing a wrapped value. Both are shown below, and both are cheap enough that "it is on a hot path" is rarely a real objection.
1function allocate(count: number, size: number) {2 const bytes = count * size // may overflow to something small3 if (bytes > MAX_ALLOC) throw new Error('too large')4 const buf = alloc(bytes) // allocates the SMALL wrapped size5 for (let i = 0; i < count; i++) { // but writes 'count' elements6 write(buf, i * size) // -> heap overflow7 }8}1function allocate(count: number, size: number) {2 // Rearranged so no multiplication can overflow: divide instead.3 if (size !== 0 && count > MAX_ALLOC / size) {4 throw new Error('too large')5 }6 const bytes = count * size // now provably within range7 const buf = alloc(bytes)8 for (let i = 0; i < count; i++) write(buf, i * size)9}10// Equivalently, use checked arithmetic where the language has it:11// Rust: count.checked_mul(size).ok_or(TooLarge)?12// C++20: if (__builtin_mul_overflow(count, size, &bytes)) fail();The first version performs the overflowing operation and then inspects the result, which is too late — the information was destroyed by the discarded carry. The second rearranges the test so the dangerous operation never executes on out-of-range inputs. In C or C++ the first is worse still: if the values are signed, the compiler may assume the overflow cannot happen and remove the check.
Key points
- Overflow discards the carry out of the top bit; for signed values the largest positive wraps to the most negative.
- The CPU sets distinct flags for signed overflow and unsigned carry, but almost no language checks them by default.
- Hardware behaviour is essentially universal; language behaviour ranges from wrapping to undefined to impossible.
- In C and C++, signed overflow is undefined, so the compiler may delete checks that rely on it having occurred.
- Check before the operation, not after — once the carry is discarded the information is gone.
Follow the mechanism
The path through the machine, hop by hop — and the conclusions it invites that are wrong.
- 1Operands → adder: two N-bit values enter an adder that produces N+1 bits internally.
- 2Adder → result register: only the low N bits are retained; the top bit is discarded.
- 3Adder → flag register: signed-overflow and unsigned-carry flags are set independently from the same addition.
- 4Flags → nothing, usually: no trap is raised, so execution continues with the wrapped value unless the program explicitly branches on a flag.
- 5Wrapped value → bounds check: a subsequent comparison now tests a value that bears no relation to the intended arithmetic.
- • Believing undefined behaviour means "wraps in some unpredictable way"; it means the compiler may assume it does not occur and optimise accordingly.
- • Writing
if (x + 1 < x)to detect overflow, which is exactly the check an optimiser is entitled to delete. - • Assuming that because a value is "obviously" small, an attacker cannot supply one near the type limit.
- • Testing only typical values; overflow bugs live entirely at the boundaries and are invisible in the middle of the range.
Consequences, controls and cost
- • A size or index computation can pass a bounds check it should have failed, which is the standard shape of overflow vulnerabilities.
- • Loop counters and accumulators can silently reverse sign, producing infinite loops or negative totals.
- • In C and C++ the optimiser can remove overflow checks entirely, so the mitigation vanishes without warning.
- • Behaviour differs between debug and release builds in languages like Rust, so testing configuration matters.
- • Rearrange the arithmetic so overflow cannot occur — compare against `MAX / size` instead of computing the product first.
- • Use checked or saturating arithmetic where the language provides it, and make the failure path explicit.
- • Compute in a wider type when the inputs are bounded and the width is available.
- • Enable sanitizers in test builds to catch signed overflow at the point it happens rather than at the corruption it causes.
- • Build with an undefined-behaviour or integer sanitizer and run the test suite; it reports the exact operation and line.
- • Fuzz any parsing or sizing code with values near the type's limits, which is where these bugs exclusively live.
- • Add explicit tests at MAX, MAX−1, MIN and MIN+1 for every fixed-width type in a calculation.
- • Inspect the disassembly to confirm an overflow check survived optimisation rather than assuming it did.
- • Checked arithmetic adds a branch per operation, which is measurable in tight numeric loops though usually negligible elsewhere.
- • Wider types cost memory and cache footprint, and merely move the boundary rather than removing it.
- • Arbitrary-precision integers remove the problem entirely but make every integer a heap object with a large constant-factor cost.
Scope
§224 — what these claims are specific to.
- GENERALDiscarding the carry and wrapping is what essentially all contemporary hardware does. Flag naming differs — x86-64 has separate OF and CF; AArch64 uses V and C — but the behaviour matches.
- PLATFORM-SPECIFICWhether overflow is defined, undefined, trapping or impossible is decided by the language specification, not the CPU, and the five rows in the matrix above genuinely differ.
Misconceptions
Where the rest of this lives
Why C and C++ leave signed overflow undefined — and what the optimiser is thereby permitted to assume about loop bounds and pointer arithmetic — belongs to compiler semantics rather than to hardware. That domain does not exist yet.