Assignment
=Storing a wide value into a narrower variable silently keeps only the low N bits.
An interactive walkthrough of integer overflow — what happens when an arithmetic result no longer fits in the bits a program reserved for it. Seven chapters: how integers are stored, how the same bits can mean two different numbers, when addition and subtraction wrap, which operators are safe and which are not, what real software has lost to overflow, what overflow looks like in working C, and how to defend against each kind.
A computer cannot literally "store the number 42." It stores a fixed pattern of on and off switches — bits — and reads that pattern as 42 by convention. N bits accommodate exactly 2N patterns: an 8‑bit byte holds 256, no more. Integer overflow is what happens when a value a program tries to store does not fit in the bits reserved for it — and from this single mismatch every behaviour in this lab descends.
Each bit represents a power of two. To form a value the machine sums the powers whose bits are on. The cage is exactly its size — N bits, never more — and that ceiling is the same on every machine.
Take an 8‑bit unsigned byte. Eight bits; 256 unique patterns, from 00000000 to 11111111. Ask the byte to hold a value larger than 255 and the high bits silently fall off — the byte keeps only the rightmost 8 bits and forgets the rest. That silent loss is integer overflow, and every example in this lab is a different costume on the same idea.
Below: the four canonical widths, their unsigned spans, and their signed spans (using two's complement, the encoding every modern CPU uses, in which the leftmost bit carries a negative weight). Click a row to designate that width as the working width for this section's gauge.
| Width | Patterns (2N) | Unsigned span | Signed span | Common name |
|---|---|---|---|---|
| 8 | 256 | 0 — 255 | −128 — 127 | byte |
| 16 | 65 536 | 0 — 65 535 | −32 768 — 32 767 | short |
| 32 | ≈ 4.29 × 10⁹ | 0 — 4 294 967 295 | −2 147 483 648 — 2 147 483 647 | int |
| 64 | ≈ 1.84 × 10¹⁹ | 0 — 18 446 744 073 709 551 615 | ±9.22 × 10¹⁸ | long |
The same eight bits mean 255 to one reader and −1 to another, depending on whether the leftmost bit is read as a sign. Signed and unsigned overflow look different from each other for exactly this reason — the same bytes, two contracts.
In two's complement, the leftmost bit carries a negative weight. To negate a number, invert every bit and add one. This single rule produces every quirk of signed arithmetic — and explains why signed overflow lands on a negative number while unsigned overflow lands on a small positive one.
The instrument below holds 8, 16, 32, or 64 bits — your choice. Edit the bits directly (click them), sweep the slider, type a decimal value, or randomise. The same bit pattern produces two distinct decimal readings: one if the leftmost bit is treated as a sign, one if it is not. The reading depends entirely on which type the program declared, never on the bytes themselves.
Add or subtract two numbers in any of four cages. The arithmetic is computed on the host machine in arbitrary precision first; then the cage truncates the result to its width. When the truncated value disagrees with the true result, that disagreement is overflow.
Edit either operand; the calculation runs immediately. Switch widths to see how the same arithmetic produces overflow at one width and not at another.
The instrument below shows the same operation in two columns. The left column is the arithmetic in decimal: A, B, and the result a real CPU would store after truncating to N bits. The right column is the same values shown as bits, so the truncation is visible directly. When the arithmetic on the left disagrees with the true mathematical answer, the bits on the right show what was lost — and that disagreement is overflow, observed.
Of the integer operations a C program performs, only some can produce overflow or truncation. The rest always fit. Here is the complete catalog — four categories, two unsafe, two safe — with one interactive demo per category.
Edit any operand, switch any width or sign; the result updates immediately. The red-striped cards are the operations that can silently corrupt a value; the green-striped cards are the ones that always produce a defined result within range.
The unsafe pair — assignment and arithmetic — covers every example you have seen so far in this lab. The safe pair — bitwise/shift and compare — is what well-written C falls back to when an arithmetic operation cannot be guaranteed to fit. Knowing which is which is the first step in writing code that doesn't lose information by accident.
Storing a wide value into a narrower variable silently keeps only the low N bits.
Sums, differences, products, and quotients can exceed the cage and silently wrap.
Operate on bits directly; the result is always defined and stays within N bits.
Returns true or false. Cannot overflow — but the same bytes can compare differently in signed vs unsigned.
Six historical incidents in which integer overflow caused real damage. In every case the program followed its rules precisely; the bug was that nobody had written a rule for what should happen when a counter exceeded its width.
A horizontal‑velocity reading from the inertial reference system was converted from a 64‑bit float to a 16‑bit signed integer. Forty seconds into flight the value exceeded 32 767; the conversion threw an exception; the rocket self‑destructed.
POSIX time counts seconds since 1 Jan 1970 in a 32‑bit signed integer. At 03:14:07 UTC on 19 January 2038 the counter will reach 2 147 483 647 — and the next tick will roll it to −2 147 483 648, suddenly placing the world in December 1901.
The video accumulated views toward 2³¹ − 1 = 2 147 483 647, the maximum of YouTube's signed 32‑bit counter. To prevent a wraparound to negative views, the platform quietly upgraded the counter to 64‑bit before the seam was reached.
Pac‑Man stores the level number in a single unsigned byte. Reaching level 256 wraps the byte to 0, and a routine that draws fruit reads invalid memory — corrupting the right half of the maze into garbled letters. Unwinnable by design accident.
An attacker requests a buffer of n × m bytes, choosing values whose product overflows to a small number. The system allocates a tiny buffer, then attempts to write the originally requested bytes — overrunning the allocation and yielding arbitrary code execution.
The exchange's index truncated each calculation to three decimals rather than rounding. Three thousand updates per day, twenty‑two months later, the index had silently bled 574 points — about half its value — through repeated downward drift.
A small ANSI‑C runtime, built into this page, executes real programs in the browser. Edit the highlighted operands, press compile & run, and watch the bug emerge in the terminal — exactly as it would on a host machine.
Six C programs, each demonstrating a different kind of integer overflow. Every program is real C: declarations, types, control flow, printf. The runtime honours fixed‑width integer semantics down to the byte.
The integer types you have studied — uint8_t, int32_t, size_t — appear here as they would in working production code. The operands shown in amber are editable; everything else is the program itself, untouched. After a run, if overflow occurred, the offending line is marked in red.
The terminal beside the code is not decoration: it shows the program's stdout, character by character, with the same exit semantics a real ./a.out would produce.
Each of the six C programs in Live Specimens above has a corresponding patch below — and only those programs do. Every fix is a small, idiomatic change: no new dependencies, just different choices about types, validation, and where the check runs.
Each card is a side‑by‑side diff between the original program and the secured version. Tokens that changed are framed in red on the vulnerable side and green on the patched side. New lines are striped in green with a leading +.
Almost every defense against integer overflow falls into one of two complementary strategies, marked on each remedy's tag. PRE‑CHECK — a precondition test — asks "would this operation overflow?" before running it, and refuses if so (Remedies 02, 03, 05). POST‑CHECK — a postcondition test — runs the operation and then asks "did it overflow?", undoing the result if it did (Remedies 04, 06). The patched code in every remedy carries an explicit /* PRE-CONDITION */ or /* POST-CHECK */ comment naming the strategy in use. The third option, TYPE‑level prevention, picks a wider type so neither check is needed at all — that is what Remedy 01 does, and is the cheapest defense when storage permits.
The remedies cluster further around four habits: widen before you narrow, validate the original input (not what survives a cast), use checked arithmetic when wrap‑around is possible, and let the standard library compute sizes for you when it can. The closing card distils these into a single defensive checklist.
if (a > UINT_MAX − b) return ‑1; result = a + b; Cheap, portable, doesn't need any builtin or library — but the rearranged inequality must be expressible in the operand's type.
if (__builtin_add_overflow(a, b, &r)) return ‑1; The compiler folds this into a single CPU instruction (CF/OF flag) and is the only safe option for signed integers, where naïve if (a + b < a) is undefined behaviour.
uint16_t bytes = a × b can wrap; size_t bytes = (size_t)a × b cannot, for any 16‑bit a, b. The cheapest defense — no runtime cost — but only available when a wider type fits in your storage budget.
-Wall -Wextra enable the broad set of compiler warnings (uninitialised variables, suspicious comparisons, dead branches);
-Wconversion warns on every implicit narrowing — exactly the silent int → int8_t cast that the live programs exploit;
-Wsign-conversion catches the signed‑↔‑unsigned mismatches that produce CWE‑195 (the bug in Remedy 05);
-fsanitize=undefined (UBSan) instruments the binary so that any signed overflow, division by zero, or out‑of‑bounds index aborts immediately with a precise diagnostic — use it in CI and during fuzzing;
-ftrapv (or its modern equivalent -fsanitize=signed-integer-overflow,trap) emits CPU trap instructions on signed overflow, faulting the program rather than continuing with corrupted state — recommended for long‑running services where any silent wrap is unacceptable.
wrapping_add / checked_add / saturating_add for the wrap, trap, or clamp behaviour you actually want; the type system carries the choice. Zig distinguishes + (trap) from +% (wrap). Java, C#, Python, Swift all check signed arithmetic by default. C and C++ make safety a per‑expression decision — make it, but if the budget allows, write the new code in something that makes that decision for you.
A byte cannot hold a sum that exceeds 255. By widening the destination type — and casting at least one operand so the addition itself happens in the wider type — the result has nowhere to overflow into. For two‑byte sums, sixteen bits suffice; for two 32‑bit values, use int64_t.
A loop counter must accommodate the maximum value its condition compares against — and one more, since the increment runs first. The idiomatic choice for "an index into a thing in memory" is size_t; it is large enough on every conforming platform. With a correctly‑sized counter, the safety cap becomes unnecessary.
Narrowing destroys information; a bounds check on the destination type sees only the residue. Always validate the untouched input — both lower and upper bound — and cast to the narrower index type only after the check has passed. Using size_t for the index also forecloses the negative‑index path entirely.
The standard library's calloc already detects size overflow and returns NULL — using it is the simplest and most portable defence. Where you must compute a size yourself, GCC and Clang provide __builtin_mul_overflow (and _add, _sub) that report wrap‑around through their boolean return value.
A signed‑to‑unsigned conversion happens silently every time a signed int meets a function that takes a size_t (memcpy, memset, read, fread). The fix is to validate both bounds — lower and upper — on the original signed value, before the implicit cast erases the sign. if (len < 0 || (size_t)len > sizeof(buf)) stops the negative path that the original "len < BUFFER_SIZE" check missed. This is CWE‑195 in the wild.
The original CVE‑2002‑0639 patch widened the type of nresp and used a checked allocator. Both choices matter: a wide size_t for count means the multiplication has room; calloc(count, 8) additionally short‑circuits the allocation when the product would overflow size_t, returning NULL rather than handing back a tiny chunk for a huge write. The lesson generalises: any size computed as n × m on attacker‑influenced inputs should run through calloc or __builtin_mul_overflow.
Fraunces, by Phaedra Charles & Flavia Zimbardi. Newsreader, by Production Type. JetBrains Mono, by JetBrains.
All arithmetic herein is performed in the host machine's BigInt engine and then truncated, in software, to the chosen cage. The viewport is rendered with three.js. Findings are illustrative; the laws are universal.
Bureau of Numerical Artifacts, Annex IV. — end of file —