Skip to content

Extremes, Overflow, and Invalid Input

The previous page, Empty, Null, and Boundaries, walked inward to the seams of a range: L-1, L, L+1, the exact fence a decision draws. This page walks outward, all the way to the edge of what a value can even be, and then one step past it. What is the largest number this field could ever hold? The smallest? The strangest sequence of bytes that could arrive dressed up as valid input? Boundary analysis asks “is the value inside the limit?” This page asks a harder question: “what happens when the type itself runs out of room?”

There are two hostile forces to court here. The first is the machine’s own limits — integers overflow, floats lose precision, and a conversion from a wide type to a narrow one throws data on the floor. The second is the input you never asked for — the wrong type, the malformed encoding, the injection payload, the adversarial Unicode string engineered to break the parser downstream. A tester learns to reach for both.

Every numeric type is a box with a fixed size, and every box has a lid. A tester’s job is to walk each number to the lid and lean on it.

Type Approx range What breaks at the edge
──────────────────────────────────────────────────────────────────────────────
i32 (signed) -2,147,483,648 .. 2,147,483,647 wraps or panics past max
u32 (unsigned) 0 .. 4,294,967,295 0 - 1 wraps to ~4 billion
i64 (signed) ±9.2 quintillion "surely big enough" — until not
f64 (float) huge range, ~15-17 sig. digits precision, not range, is the trap

For every number the code accepts, test the four corners:

  • Maximum — the largest value the type holds. Then add one to it somewhere downstream and watch it overflow: wrap to a negative in C or Java, panic in debug Rust, silently wrap in release Rust, or grow without limit in Python (which has its own cost).
  • Minimum — the most negative value. abs(i32::MIN) has no positive counterpart and is itself a classic overflow. Zero minus one on an unsigned type wraps to the maximum.
  • Just below / just above a business limit — a price of 2,000,000,000 cents, an age of -1, a quantity of 100,000,000. The type may accept it long before your logic should.
  • The “impossible” large input — a 10-gigabyte upload, a search for 5 million results, a loop counter fed by an attacker. Size is an input too.

Under the hood — how overflow actually behaves

Section titled “Under the hood — how overflow actually behaves”

Overflow is not one behaviour; it is a family, and the family member you get depends on the language, the build, and the type. That variability is why it slips through: the test on your laptop and the crash in production can be the same code compiled differently.

n = INT_MAX; n = n + 1;
C / C++ (signed) → undefined behaviour (may wrap, may be optimized away)
Java (int) → wraps silently to INT_MIN, no error
Rust (debug) → panics: "attempt to add with overflow"
Rust (release) → wraps silently (unless checked/overflowing arithmetic used)
Python (int) → promotes to arbitrary precision — no overflow, but slow/large
JavaScript → all numbers are f64; beyond 2^53 integers lose precision silently

The dangerous cases are the silent ones. A balance that wraps from a huge positive to a huge negative does not throw — it just becomes wrong, and the next line of code trusts it. So the test is not only “does it overflow?” but “how, and does anything downstream notice?”

The extremes of floats — precision, not just range

Section titled “The extremes of floats — precision, not just range”

Floating-point numbers rarely run out of rangef64 reaches past 10^300. They run out of precision, and they do it quietly. A float stores a fixed number of significant digits, so very large and very small values, and any value that is not a tidy sum of powers of two, are approximations.

0.1 + 0.2 → 0.30000000000000004 (not 0.3)
1e16 + 1.0 → 1e16 (the +1 vanishes: too small to represent)
0.1 summed 10 times → 0.9999999999999999 (not 1.0)
money as float → cents drift; totals fail to reconcile

Test the tiny alongside the huge: the smallest positive value that is not zero (a subnormal), a value so small that adding it to a large one changes nothing, and the special values NaN and ±Infinity — which compare unequal to everything, including themselves, and poison any sort or == check they touch. The tester’s habit: for any float computation, ask “at what magnitude does the least-significant digit I care about fall off the end?”

Now turn from the machine’s limits to the input itself. Two categories look alike and are worlds apart, and confusing them is its own bug.

Invalid input is input the spec says is wrong, and the correct response is to reject it cleanly — a clear error, no crash, no corruption. A letter where a number belongs. A date of February 30th. A JSON body that is not valid JSON. A negative age. The spec has an opinion, and the opinion is “no.”

Unexpected-but-valid input is input the spec never mentioned, yet nothing forbids it. It is not wrong — it is unforeseen. A name containing an apostrophe (O'Brien). A perfectly legal but enormous ZIP file. An address in a script the form author never imagined. A user in a time zone the scheduler’s author forgot exists. Rejecting this input is itself a bug, because the input is legitimate; the code’s imagination was too small.

Input Category Correct behaviour
──────────────────────────────────────────────────────────────────────────
age = "twelve" invalid (wrong type) reject with clear error
age = -5 invalid (out of domain) reject with clear error
{ "age": } invalid (malformed) reject, do not crash
name = "O'Brien" unexpected but VALID accept; don't break SQL/quotes
name = "李明" unexpected but VALID accept; store correctly
filename = "résumé .pdf" unexpected but VALID accept; don't mangle bytes

The discipline: for every field, ask two separate questions. “What invalid values must I reject, and do I reject them cleanly?” and “What valid values did the author probably not picture, and do I handle them anyway?” The first protects against garbage; the second protects against a narrow imagination — and the second is where the subtler, more embarrassing bugs live.

Hostile input — the values engineered to break you

Section titled “Hostile input — the values engineered to break you”

Some input is not merely unexpected; it is adversarial. It was crafted specifically to slip past a parser, break out of a string, or exhaust a resource. A tester borrows the attacker’s playbook and fires these at every boundary where data crosses a trust line.

  • Injection payloads — a name like Robert'); DROP TABLE students;-- (the “little Bobby Tables” classic), or ${jndi:...}, or a shell metacharacter in a filename. These probe whether input is ever concatenated into a query, a command, or a template instead of being safely parameterized.
  • Malformed encoding — bytes that are not valid UTF-8, an overlong encoding, a lone surrogate, a mismatched charset. Does the decoder reject cleanly, or does it produce a mangled string that passes validation and corrupts data downstream?
  • Adversarial Unicode — a right-to-left override character that visually reverses text, a string of thousands of combining marks (“Zalgo” text) that breaks layout, homoglyphs (a Cyrillic а that looks identical to Latin a) used to forge a lookalike username, or a normalization mismatch where café composed and café decomposed are byte-different but visually identical.
  • Resource-exhaustion input — a deeply nested JSON document, a “billion laughs” XML entity bomb, a regular expression crafted to trigger catastrophic backtracking, a decompression bomb. The value is small; its effect is enormous.

The unifying question is the one this whole page is built around, applied to the meanest possible source: what is the largest, smallest, or weirdest value that could arrive here — and what does an adversary gain by sending it?

The habit: interrogate every input’s extremes

Section titled “The habit: interrogate every input’s extremes”

None of this requires cleverness in the moment. It requires a fixed reflex you run at every place data enters the code — a form field, an API parameter, a file, a message off a queue, a value read from another system. At each such point, ask the same short list:

For every value that arrives here:
─────────────────────────────────────────────
largest? push to the type's max, then past it → overflow / truncation?
smallest? most-negative, zero, subnormal, empty → underflow / wrap?
precision? does the least significant digit I care about survive?
wrong type? a string where a number is expected → reject cleanly?
malformed? bad encoding, broken JSON → reject, not crash?
hostile? injection, adversarial Unicode, bomb → contained?
unforeseen? valid but never imagined → accepted, not broken?

Run that list and the exotic bugs stop being surprises. They become entries you already checked — the overflow you drove on purpose, the emoji you stored on purpose, the SQL payload you fired on purpose and watched bounce off a parameterized query.

Testing at the extremes and against hostile input is a core technique, so it earns the five questions.

  • Why does it exist? Because every type is a finite box and every parser trusts its input, so the two richest veins of catastrophic bugs are values that overflow the box and values crafted to abuse the trust. The technique exists to find both before an attacker or a rare production input does.
  • What problem does it solve? It converts “we never thought that value could arrive” from a post-mortem sentence into a test case you wrote on purpose — covering overflow, precision loss, malformed and adversarial input, and legitimate-but-unforeseen values in one reflex.
  • What are the trade-offs? The space of hostile inputs is effectively infinite, so you can never test all of it; you spend effort on inputs that may never occur, and extreme-value tests can be noisier and slower than happy-path ones. The payoff is asymmetric — one caught overflow can be worth more than a thousand green happy-path assertions.
  • When should I avoid it? Never skip it at a trust boundary, but do not let it crowd out correctness of ordinary values — a system that survives every injection payload can still compute the normal answer wrong. Weight the effort by blast radius: a public API or a narrowing type conversion deserves far more than an internal, fully-controlled value.
  • What breaks if I remove it? The silent integer overflow, the drifting float total, the SQL injection, the truncated Unicode, and the reused conversion that fit yesterday’s inputs but not today’s — every one of them ships, because these are exactly the cases a “typical, well-formed value” test never touches.
  1. Distinguish an overflow from a precision loss, and give a numeric example of each and the type most associated with it.
  2. Java, debug-Rust, release-Rust, and Python all handle INT_MAX + 1 differently. Which behaviours are the most dangerous to a tester, and why?
  3. Recall the Ariane 5 failure in one sentence of cause, and state the general testing lesson about reused code and narrowing conversions it teaches.
  4. Classify each as invalid or unexpected-but-valid, and give the correct behaviour: age = "twelve"; a name O'Brien; a JSON body { "age": }; a user whose name is 李明.
  5. Name three kinds of hostile input from this page and, for one of them, describe the single input you would send and what a buggy system would do with it.
Show answers
  1. Overflow is a value exceeding the range of its type, so it wraps, panics, or is truncated — e.g. i32 at 2,147,483,647 + 1 wrapping to a negative. Precision loss is a value that fits the range but cannot be represented exactly, so digits are dropped — e.g. f64 computing 0.1 + 0.2 = 0.30000000000000004, or 1e16 + 1 = 1e16. Overflow is an integer problem of range; precision loss is a floating-point problem of significant digits.
  2. The silent ones: Java wraps int to INT_MIN with no error, and release-mode Rust wraps silently too. These are dangerous because nothing throws — the value simply becomes wrong and the next line trusts it. Debug-Rust panicking is safe (it announces the bug), and Python’s promotion to arbitrary precision avoids overflow entirely (at a memory/speed cost). A test that passes in debug can still ship a silent wrap in release.
  3. Cause: a horizontal-velocity value was converted from a 64-bit float to a 16-bit signed integer, overflowed because it no longer fit, and the unhandled error destroyed the rocket. Lesson: reused code inherits the old limits of its narrowing conversions, and new operating conditions can push a once-safe value past them — so always test conversions with the new, extreme inputs, not just the inputs the code was originally written for.
  4. age = "twelve"invalid (wrong type); reject with a clear error. O'Brienunexpected-but-valid; accept it and don’t let the apostrophe break a query or quoting. { "age": }invalid (malformed JSON); reject cleanly without crashing. 李明unexpected-but-valid; accept and store it correctly (rejecting it would itself be a bug).
  5. Any three of: injection payloads (SQL/command/template), malformed encoding, adversarial Unicode (RTL override, Zalgo, homoglyphs, normalization mismatch), resource-exhaustion input (nested JSON, entity bomb, catastrophic-backtracking regex, decompression bomb). Example: send a name Robert'); DROP TABLE students;--; a buggy system concatenates it straight into an SQL statement and executes the injected DROP TABLE, whereas a correct one uses a parameterized query and stores the literal string harmlessly.