Integer Representation & Overflow — Middle Level¶
Topic: Integer Representation & Overflow Focus: The practical bug-finding layer — integer promotion, implicit conversions, signed/unsigned comparisons, truncation, and the concrete techniques to detect overflow before it bites you.
Table of Contents¶
- Introduction
- Prerequisites
- Glossary
- Core Concepts
- Real-World Analogies
- Mental Models
- Code Examples
- Pros & Cons
- Use Cases
- Coding Patterns
- Clean Code
- Best Practices
- Edge Cases & Pitfalls
- Common Mistakes
- Tricky Points
- Test Yourself
- Tricky Questions
- Cheat Sheet
- Summary
- What You Can Build
- Further Reading
- Related Topics
- Diagrams & Visual Aids
Introduction¶
Focus: Most integer bugs don't come from a single big number. They come from quiet, automatic conversions — a
shortpromoted toint, a signed value compared against an unsigned one, a 64-bit length truncated into a 32-bit field — and from doing arithmetic without checking whether it overflowed.
At the junior level you learned the model: integers are fixed-width, two's complement, and overflow wraps (or doesn't) depending on the language. At the middle level the work is different. You will rarely write INT_MAX + 1 on purpose. You will constantly write code where the language silently changes a value's type or width behind your back — and that is where production overflow bugs actually live.
Three families dominate:
- Implicit conversions and integer promotion — the compiler widens, narrows, or re-signs operands of an expression according to rules you didn't write and probably can't recite.
byte b = (byte)(a + c)in Java; the "usual arithmetic conversions" in C;ascasts in Rust. - Signed/unsigned comparison — when you compare a signed and an unsigned value, one of them is converted, and the conversion can turn
−1into4294967295, flipping the comparison. - Overflow detection — since most languages won't tell you overflow happened, you need techniques to find out: pre-checks against the limits, wider-type computation, the post-condition trick (carefully, because of UB), and compiler builtins.
This page is the toolkit. By the end you should be able to look at a line like if (a + b > LIMIT) or arr[i - 1] or int len = (int)bigValue; and immediately ask the right question: what got converted, what width is this evaluated at, and can it overflow?
Prerequisites¶
- Required: The junior page — two's complement, fixed widths, per-language overflow behavior, wraparound.
- Required: Comfort reading C, Java, and Go arithmetic; basic Rust helps.
- Helpful: Knowing what a cast/conversion looks like in your language(s).
- Helpful: Having been bitten by at least one "the number came out wrong/negative" bug — this page explains why.
Glossary¶
| Term | Definition |
|---|---|
| Integer promotion | The C rule that operands narrower than int (char, short, bitfields) are converted to int (or unsigned int) before arithmetic. |
| Usual arithmetic conversions | C/C++ rules that bring the two operands of a binary operator to a common type before computing. |
| Implicit conversion | A type/width/sign change the language performs without an explicit cast. |
| Narrowing / truncation | Converting to a smaller type; high bits are discarded. |
| Widening | Converting to a larger type; sign-extended (signed) or zero-extended (unsigned). |
| Signed-to-unsigned conversion | Reinterpreting the bit pattern under the unsigned reading; −1 becomes the type's max. |
| Rank | C's ordering of integer types by size/precedence; used to decide the common type. |
size_t | An unsigned type wide enough to hold any object size on the platform (32-bit on 32-bit systems, 64-bit on 64-bit). |
ptrdiff_t | The signed counterpart to size_t; the type of a pointer difference. |
| Pre-check | Testing operands against limits before the operation, to predict overflow. |
| Post-check | Testing the result after the operation to infer overflow (unsafe for signed in C — UB). |
| Checked arithmetic | An operation that reports overflow rather than silently producing a value. |
| Saturating arithmetic | Overflow clamps to the type's max/min instead of wrapping. |
| Wrapping arithmetic | Overflow is defined to wrap modulo 2ⁿ, deliberately. |
Core Concepts¶
1. Integer Promotion (C/C++): Small Types Become int¶
In C and C++, you cannot do arithmetic on a char or short directly. Before any arithmetic operator, operands smaller than int are promoted to int (the "integer promotions"). This produces results that surprise people:
unsigned char a = 200, b = 100;
unsigned char c = a + b; // a,b promoted to int: 200+100 = 300 (an int)
// then assigned back to unsigned char: 300 & 0xFF = 44
printf("%d\n", a + b); // prints 300, NOT 44 — the expression is an int!
printf("%d\n", c); // prints 44 — truncation happened on assignment
The expression a + b has type int and value 300. Only when you store it into the 8-bit c does truncation to 44 occur. A common bug: people think 8-bit arithmetic wraps at 256, but the arithmetic happens in int; the wrap only happens at the narrowing store.
2. Usual Arithmetic Conversions: Finding the Common Type¶
When the two operands of + - * / % < > == & etc. have different types, C/C++ converts them to a single "common type" by a ranked procedure (simplified):
- Apply integer promotions to both.
- If both are signed or both unsigned, convert to the higher-ranked type.
- If one is unsigned and the other signed, and the unsigned type's rank is ≥ the signed type's, the signed operand is converted to unsigned. ← the dangerous rule.
That last rule is the root of the signed/unsigned comparison bug below.
3. The Signed/Unsigned Comparison Trap¶
You expect "less" — −1 < 1. You get "not less." Why? b is unsigned int, same rank as int, so the signed a is converted to unsigned: −1 becomes 4294967295. Now the comparison is 4294967295 < 1, which is false. The mathematical relationship inverted because of an implicit conversion you never wrote.
This is one of the most common real bugs in C/C++:
int len = get_length(); // could return -1 on error
char buf[100];
if (len < sizeof(buf)) { // sizeof is size_t (unsigned)!
// if len == -1, it becomes a huge unsigned, condition FALSE,
// so the "safe" branch is skipped — or in other framings, taken when it shouldn't be
}
4. Truncation: Narrowing Keeps the Low Bits¶
Assigning a wide value into a narrow type discards the high bits — it does not clamp or round:
long big = 0x1_0000_002A; // 4294967338
int small = (int)big; // keeps low 32 bits: 0x0000002A = 42
short tiny = (short)0x12345; // keeps low 16 bits: 0x2345 = 9029
For signed targets, the kept low bits are then reinterpreted under the target's sign rule, which can flip the sign:
int x = 0xFFFF_0001; // some 32-bit value
short s = (short)x; // low 16 bits = 0x0001 = 1 (fine here)
short t = (short)0x0000_8000; // low 16 bits = 0x8000 = -32768 (sign flips!)
5. Overflow Detection Technique A: Pre-Check Against Limits¶
The correct, portable way to detect overflow is to test the operands against the type's limits before computing, so you never actually overflow:
#include <limits.h>
// Will a + b overflow a signed int?
bool add_overflows(int a, int b) {
if (b > 0 && a > INT_MAX - b) return true; // a + b would exceed INT_MAX
if (b < 0 && a < INT_MIN - b) return true; // a + b would go below INT_MIN
return false;
}
This never triggers UB because it never performs the overflowing addition. The same shape works for multiplication (a > INT_MAX / b, with care for sign and zero).
6. Overflow Detection Technique B: Compute in a Wider Type¶
If a wider type exists, compute there and check the result fits:
int32_t a, b;
int64_t wide = (int64_t)a + (int64_t)b; // 32-bit operands can't overflow 64-bit sum
if (wide > INT32_MAX || wide < INT32_MIN) { /* overflow */ }
int32_t result = (int32_t)wide;
Clean and obvious — but it fails for the widest type (there's nothing wider than int64_t to promote a 64-bit sum into), and it costs a wider operation.
7. Overflow Detection Technique C: Compiler Builtins / Checked APIs¶
The best modern answer is to let the hardware/compiler tell you, using the CPU's overflow flag:
let (r, overflowed) = a.overflowing_add(b);
match a.checked_mul(b) { Some(v) => /* ok */, None => /* overflow */ }
// Go has no builtin; use math/bits for the unsigned case:
sum, carry := bits.Add64(a, b, 0) // carry != 0 means overflow
8. Why the Post-Check Trick Is a Trap in C¶
A classic "detect overflow after the fact" idiom:
For unsigned this is valid (wraparound is defined, so a smaller result means it wrapped). For signed, the addition a + b itself is undefined behavior when it overflows — so by the time you check r, you've already invoked UB and the compiler may have assumed it never happened and deleted your check. Detect before, not after, for signed types.
Real-World Analogies¶
| Concept | Real-world thing |
|---|---|
| Integer promotion | Everyone at a meeting being asked to convert their notes to A4 before comparing — even the people who wrote on index cards. |
| Usual arithmetic conversions | Two travelers with different currencies; before settling a bill, both convert to one agreed currency — and the conversion can surprise you. |
| Signed→unsigned flip | Reading a "−1" written as "all 9s on the odometer" (999999) — same dial, wildly different number. |
| Truncation | Cutting a long receipt to fit a small frame — you keep only the bottom portion, losing the total at the top. |
| Pre-check | Weighing your luggage at home before the airport so you never get hit with the overweight fee at the gate. |
| Post-check (signed, UB) | Checking your bank balance after a bounced transaction already triggered penalties — the damage is done. |
| Wider-type computation | Doing the math on a bigger whiteboard, then copying only the result that fits back onto the small one. |
Mental Models¶
The "Expression Has a Type" Model¶
Stop reading a + b as "add two numbers." Read it as: "the language picks a common type T, converts both operands to T, computes in T, and the result is of type T." Almost every middle-level integer bug is explained by being wrong about what T is. In C, char + char has type int. In unsignedInt < negativeInt, T is unsigned and the negative becomes huge. Always ask: what is the type of this expression, and at what width does it compute?
The "Conversion Is a Reinterpretation, Not a Move" Model¶
A signed→unsigned conversion doesn't change the bits — it changes the reading rule. (unsigned)-1 is the same 0xFFFFFFFF bit pattern, now read as 4294967295. Truncation discards high bits; widening adds bits (copying the sign for signed). Picture the bits staying put while a different "lens" is placed over them.
The "Detect Before You Leap" Model¶
For overflow, the safe mental default is: never perform an operation that might overflow and then inspect the wreckage (especially in C, where the wreckage is UB). Instead, ask in advance whether the operation will overflow — compare against the limit, or compute in a wider type, or use a checked primitive that does the asking atomically with the operation.
Code Examples¶
C — The promotion + truncation surprise¶
#include <stdio.h>
int main(void) {
unsigned char a = 0xFF; // 255
unsigned char b = 0x01; // 1
printf("a + b as expr = %d\n", a + b); // 256 (computed as int!)
unsigned char c = a + b; // truncated to 8 bits
printf("a + b stored = %u\n", c); // 0
short s = -1;
int i = s; // widening: sign-extended
printf("widened -1 = %d\n", i); // -1, not 65535
return 0;
}
C — Safe addition with pre-check vs builtin¶
#include <stdio.h>
#include <limits.h>
#include <stdbool.h>
bool safe_add(int a, int b, int *out) {
if (b > 0 && a > INT_MAX - b) return false; // would overflow high
if (b < 0 && a < INT_MIN - b) return false; // would overflow low
*out = a + b; // now guaranteed safe
return true;
}
bool safe_add_builtin(int a, int b, int *out) {
return !__builtin_add_overflow(a, b, out); // hardware flag, GCC/Clang
}
int main(void) {
int r;
printf("%d\n", safe_add(INT_MAX, 1, &r)); // 0 (refused)
printf("%d\n", safe_add(2, 3, &r)); // 1, r = 5
return 0;
}
C — The signed/unsigned comparison bug, demonstrated and fixed¶
#include <stdio.h>
int main(void) {
int n = -1;
unsigned int count = 5;
if (n < count) printf("n < count\n"); // does NOT print!
else printf("n NOT < count\n"); // prints (because -1 -> 4294967295)
// Fix: compare in a common SIGNED domain, or guard the sign first:
if (n < 0 || (unsigned)n < count) printf("fixed: n < count\n");
return 0;
}
Java — Promotion and the cast-back gotcha¶
public class Promotion {
public static void main(String[] args) {
byte a = 100, b = 100;
// byte + byte is computed as int; you MUST cast to store in a byte:
// byte sum = a + b; // compile error: possible lossy conversion
byte sum = (byte)(a + b); // 200 doesn't fit in byte -> wraps to -56
System.out.println(sum); // -56
int big = 300;
byte truncated = (byte) big; // 300 & 0xFF = 44
System.out.println(truncated);// 44
// Java has no unsigned types (pre-Java 8 helpers exist):
int x = -1;
System.out.println(Integer.toUnsignedLong(x)); // 4294967295
System.out.println(Integer.compareUnsigned(-1, 1) > 0); // true
}
}
Go — Explicit conversions, no implicit mixing¶
package main
import (
"fmt"
"math/bits"
)
func main() {
var a int32 = 2_000_000_000
var b int32 = 2_000_000_000
// Go forbids mixing types implicitly; you must convert.
wide := int64(a) + int64(b) // compute in 64-bit, no overflow
fmt.Println(wide) // 4000000000
// Detecting unsigned overflow with math/bits:
sum, carry := bits.Add64(^uint64(0), 1, 0) // MAX + 1
fmt.Println(sum, carry) // 0 1 (carry==1 => overflowed)
// Go also forbids signed/unsigned mixing — a whole bug class is gone:
// var s int = -1; var u uint = 1; _ = s < u // compile error
}
Go's design choice — no implicit conversions at all — eliminates the C signed/unsigned comparison trap by construction. You must write every conversion, which is verbose but safe.
Rust — Conversions are explicit; choose your overflow semantics¶
fn main() {
let a: i32 = 2_000_000_000;
let b: i32 = 2_000_000_000;
// `as` truncates/reinterprets silently — the one footgun in Rust:
let truncated = 300_i32 as u8; // 44
println!("{}", truncated);
// Prefer fallible conversions that report loss:
let r: Result<u8, _> = u8::try_from(300_i32);
println!("{:?}", r); // Err(TryFromIntError(()))
// Choose overflow semantics explicitly:
println!("{:?}", a.checked_add(b)); // None (overflow)
println!("{}", a.wrapping_add(b)); // -294967296
println!("{}", a.saturating_add(b));// 2147483647
// Wider-type compute:
let wide = a as i64 + b as i64; // 4000000000
println!("{}", wide);
}
Pros & Cons¶
| Technique | Pros | Cons |
|---|---|---|
| Pre-check against limits | Portable, no UB, works for the widest type. | Verbose; easy to get the sign cases wrong (especially multiplication). |
| Compute in wider type | Simple and obvious; one check at the end. | Doesn't work for the widest type; costs a wider operation. |
Compiler builtins (__builtin_*_overflow) | Fast (uses CPU overflow flag), correct, concise. | Non-standard (GCC/Clang); not portable to MSVC without alternatives. |
Checked APIs (Rust checked_*, Java *Exact) | Idiomatic, safe, expresses intent. | Java throws (exception cost); must remember to use them. |
| Saturating arithmetic | Never wraps to a wrong-direction value; great for DSP/graphics/clamped meters. | Silently caps — can hide that a limit was hit. |
| Wrapping arithmetic | Correct and intended for hashing, checksums, ring buffers. | A footgun if used by accident; reader must know it's deliberate. |
Use Cases¶
- Parsing untrusted input (file/network): pre-check every size/length/count field before arithmetic. This is where conversion and overflow bugs become CVEs.
- Buffer/size calculations: compute
count * elementSizewith overflow detection (builtin or wider type) before allocating. - Cross-width serialization: when packing a 64-bit value into a 32-bit wire field, validate it fits before truncating.
- Index math:
i - 1,mid = lo + (hi - lo)/2, ring-buffer(head + 1) % cap— all need overflow/underflow awareness, especially with unsigned indices. - Interop with C from a safer language: when crossing into
size_t/intland, conversions are where data quietly changes.
Coding Patterns¶
Pattern 1: Overflow-safe multiplication via division check¶
bool mul_overflows(size_t a, size_t b) {
if (a == 0 || b == 0) return false;
return a > SIZE_MAX / b; // if a > MAX/b, then a*b > MAX
}
Pattern 2: Overflow-safe binary-search midpoint¶
// BAD: int mid = (lo + hi) / 2; // lo + hi can overflow
// GOOD:
int mid = lo + (hi - lo) / 2; // no overflow when lo <= hi
This exact bug lived in java.util.Arrays.binarySearch for years until Joshua Bloch wrote it up in 2006.
Pattern 3: Guard unsigned subtraction¶
// BAD: let last = len - 1; // panics/wraps if len == 0
// GOOD:
let last = len.checked_sub(1); // None when len == 0
if let Some(idx) = last { /* safe */ }
Pattern 4: Compare signed and unsigned safely¶
// Don't let one operand silently convert. Normalize the domain:
if (Integer.compareUnsigned(x, y) < 0) { ... } // Java
Pattern 5: Validate-then-narrow¶
Never narrow blindly; convert through a fallible path that reports the loss.
Clean Code¶
- Make every conversion visible and intentional. Prefer
try_from/compareUnsigned/explicit casts over relying on implicit promotion. A reader should see where a value changes width or sign. - Compute at the right width from the start. If a product can exceed 32 bits, declare the operands or an intermediate as 64-bit; don't compute narrow then "fix" it.
- Encapsulate overflow checks in named helpers (
safe_add,checked_size) so the call sites read as intent, not arithmetic. - Comment why a wrap is deliberate.
hash = hash.wrapping_mul(31) // intentional modular hashingsaves the next reader a panic. - Prefer the type the domain demands.
size_t/usizefor sizes, signed for deltas, fixed-width (int32_t) when wire/format compatibility matters.
Best Practices¶
- Validate ranges at trust boundaries. Any integer from a file, socket, or user gets checked against the limits before it participates in arithmetic.
- Use checked/builtin overflow operations for size and money math. The performance hit is negligible next to the bug cost.
- Never compare signed and unsigned directly in C/C++. Enable
-Wsign-compare(it's in-Wextra) and treat every warning as a bug. - Avoid narrowing where you can; when you must, do it through a fallible conversion.
- Compile tests with UBSan (
-fsanitize=signed-integer-overflow,unsigned-integer-overflow) — it pinpoints the exact line and values at runtime. - Prefer languages/APIs that remove the footgun when you have the choice: Go's no-implicit-conversion rule and Rust's
try_fromeliminate whole categories of these bugs.
Edge Cases & Pitfalls¶
char + charisintin C. People expecting 8-bit wrap during the arithmetic are wrong; the wrap only happens on the narrowing store.sizeofissize_t(unsigned). Comparing a signed length againstsizeof(buf)silently converts the signed value — the classic bug.- Post-overflow checks for signed ints are UB in C.
if (a + b < a)has already overflowed before the check; the compiler may delete it. %of a negative number varies by language. C's-7 % 3is-1; Python's is2. Ring-buffer index math(i + n) % capcan return negative in C ifiis signed and negative.- Multiplying then dividing in the wrong order overflows needlessly.
(a * b) / cmay overflow wherea * (b / c)(when exact) or wider math wouldn't. - Mixed-width shifts.
1 << 31is UB/overflow for a signed 32-bitint(the sign bit); use1u << 31or a 64-bit literal. intwidth is platform-dependent. LP64 (Linux/macOS) has 32-bitint, 64-bitlong; LLP64 (Windows) has 32-bitintand 32-bitlong, 64-bitlong long. Code that assumeslongis 64-bit breaks on Windows.
Common Mistakes¶
- Comparing a signed length to
sizeof/.size(). The signed side converts to a huge unsigned and the check inverts. - Detecting signed overflow with a post-check (
r < a) — undefined behavior; the check can be optimized away. - Truncating without validating.
(int)bigValuesilently loses the high bits; a 5 GB size becomes a small or negativeint. (lo + hi) / 2midpoints. Overflows for large indices; the canonical binary-search bug.- Assuming 8/16-bit arithmetic wraps mid-expression in C. It promotes to
intfirst. for (size_t i = n - 1; i >= 0; i--).i >= 0is always true for unsigned; infinite loop, or underflow on the last decrement.- Forgetting Java has no unsigned types. Treating
intas unsigned withoutInteger.*Unsignedhelpers gives wrong comparisons and divisions. - Relying on
longbeing 64-bit. True on Linux/macOS, false on Windows. Useint64_t/<stdint.h>.
Tricky Points¶
- The conversion happens at the operator, not at the variable.
a < bconverts for that comparison;aitself doesn't change. Two comparisons of the same variable can convert it differently depending on the other operand. - Unsigned makes the whole expression unsigned. One unsigned operand of sufficient rank "infects" the comparison/arithmetic, dragging the signed operand into the unsigned domain.
- Wider-type detection breaks at the top. You can detect 32-bit overflow with 64-bit math, but you can't detect 64-bit overflow that way — there's no 128-bit standard type in C. Use builtins or 128-bit extensions.
- Saturating and wrapping are both "correct" — for different problems. Audio sample mixing wants saturation (clipping, not a pop); a hash wants wrapping. The bug is using one where you meant the other.
- UBSan changes the program. Code that "works" because of incidental wraparound will start aborting under UBSan — that's the point, but it means UBSan can surface latent bugs in old code suddenly.
Test Yourself¶
- In C, what is the type of
('a' + 1)? What about(unsigned char)200 + (unsigned char)100— type and value of the expression vs. when stored back into anunsigned char? - Why does
if (-1 < (unsigned)1)evaluate false? Walk through the conversion. - Write
safe_mul(a, b, &out)for signed 32-bit ints using only pre-checks (no wider type, no builtin). Handle the sign and zero cases. - Show the bit-level steps that turn
(short)0x8000into−32768(truncation + sign reinterpretation). - Why is
int r = a + b; if (r < a) overflow();valid for unsigneda, bbut a bug for signed ones in C? - Rewrite
(lo + hi) / 2to be overflow-safe and explain why it can't overflow whenlo <= hi. - In Java,
byte x = (byte)(127 + 1);— what isxand why? What was the type of127 + 1before the cast? - On a Windows LLP64 system, what is
sizeof(long)? Why does code assuminglong == 64 bitsbreak there?
Tricky Questions¶
Q1: Does C's integer promotion mean 8-bit arithmetic never wraps mid-expression?
Correct. char/short operands are promoted to int first, so the arithmetic happens in (at least) 32 bits. Wrapping/truncation to the small type only occurs when you store the result back into the narrow type. So (unsigned char)255 + (unsigned char)1 is 256 as an expression, and becomes 0 only on assignment to an unsigned char.
Q2: Why does Go forbid var s int; var u uint; s < u?
By design, Go has no implicit numeric conversions. It refuses to silently convert between signed and unsigned (or between widths), which structurally eliminates the C signed/unsigned comparison bug. You must write int64(s) < int64(u) (or similar), making the conversion — and its risks — explicit.
Q3: Is __builtin_add_overflow portable?
It's a GCC/Clang extension (also accepted by recent Clang on Windows), but not standard C and not in MSVC by that name. For portability, wrap it: use the builtin where available, fall back to pre-checks or <intrin.h> _addcarry_u32/_addcarry_u64 on MSVC. C23 adds <stdckdint.h> (ckd_add, ckd_mul) as a standard solution.
Q4: When should I use saturating vs wrapping arithmetic?
Saturating when exceeding a bound should clamp and continuing makes sense: audio samples, color channels, progress meters, a rate counter you cap. Wrapping when modular behavior is the actual semantics: hash functions, checksums, ring-buffer indices, PRNGs. Using wrapping where you meant saturating produces a loud glitch (a pop, a wrong color); the reverse silently hides that a limit was hit.
Q5: Why is comparing a signed value to .size() in C++ a warning?
std::vector::size() returns size_t (unsigned). for (int i = 0; i < v.size(); i++) compares signed i to unsigned size(); the signed i converts to unsigned. It usually works but breaks if size() exceeds INT_MAX or if i ever goes negative. Compilers emit -Wsign-compare. Use size_t i or std::ssize(v) (C++20).
Q6: Can I detect 64-bit overflow without a 128-bit type?
For addition, yes — pre-check against limits, or use __builtin_add_overflow. For multiplication, the cleanest portable route is __builtin_mul_overflow, or unsigned __int128 on GCC/Clang, or a high/low product via _umul128/math/bits.Mul64. Pure pre-check multiplication (a > MAX/b) also works and needs no wide type.
Cheat Sheet¶
┌──────────────────────────────────────────────────────────────────┐
│ CONVERSIONS, COMPARISONS & DETECTION │
├──────────────────────────────────────────────────────────────────┤
│ C integer promotion: char/short → int BEFORE arithmetic │
│ so 8/16-bit "wrap" only happens on the NARROWING STORE │
├──────────────────────────────────────────────────────────────────┤
│ Usual arithmetic conversions pick a common type: │
│ unsigned (rank >= signed) WINS → signed operand goes unsigned │
│ => (-1 < 1u) is FALSE (-1 becomes 4294967295) │
├──────────────────────────────────────────────────────────────────┤
│ Truncation = keep low bits (no round, no clamp) │
│ 300 -> u8 = 300 & 0xFF = 44 │
│ Widening: signed sign-extends, unsigned zero-extends │
├──────────────────────────────────────────────────────────────────┤
│ Overflow detection: │
│ A) pre-check: b>0 && a > MAX-b (portable, no UB) │
│ B) wider type: (int64)a + (int64)b, check fits │
│ C) builtin: __builtin_add_overflow / ckd_add (C23) │
│ D) checked API: Rust checked_add / Java addExact │
│ * signed POST-check (r<a) is UB in C — never do it │
├──────────────────────────────────────────────────────────────────┤
│ Safe idioms: │
│ midpoint: lo + (hi - lo)/2 │
│ unsigned -: len.checked_sub(1) / guard len>0 │
│ sign cmp: compareUnsigned / guard sign first │
│ narrow: try_from(...) -> Result │
├──────────────────────────────────────────────────────────────────┤
│ Platform: int=32 almost everywhere; long=64 (Unix) / 32 (Win) │
│ use int32_t / int64_t when width matters │
└──────────────────────────────────────────────────────────────────┘
Summary¶
- The middle-level danger is silent conversions, not lone giant numbers.
- C integer promotion widens
char/shorttointbefore arithmetic; narrow-type wrap happens only on the store-back. - The usual arithmetic conversions can convert a signed operand to unsigned, which is why
−1 < 1uis false — the canonical signed/unsigned trap. - Truncation keeps the low bits (no rounding, no clamping) and can flip the sign of a signed target.
- Detect overflow before you compute (pre-check against limits, or compute in a wider type, or use builtins/checked APIs). Never post-check signed overflow in C — the overflow itself is UB.
- Saturating vs wrapping are both legitimate; choose by domain and make the choice explicit in code.
- Languages differ in how much footgun they leave: Go forbids implicit conversions, Rust pushes you toward
try_from/checked_*, C/C++ leave it all to you (so turn on-Wsign-compareand UBSan). - A middle engineer's habit: for every arithmetic expression, ask "what is its type, at what width does it compute, and can it overflow?" — and validate ranges at every trust boundary.
What You Can Build¶
- A
safe_intlibrary withadd/sub/mul/divthat pre-check and return success/failure, tested at the boundaries against the compiler builtins. - A signed/unsigned comparison linter (toy): scan C source for
</>where one side is signed and the othersize_t/unsigned. - A truncation playground that takes any 64-bit value and shows what survives narrowing to 32/16/8 bits, signed and unsigned.
- An overflow-detection benchmark comparing pre-check vs wider-type vs
__builtin_*_overflowperformance. - A binary-search implementation with both the buggy
(lo+hi)/2and the safelo+(hi-lo)/2midpoint, plus a test that triggers the overflow on a huge (or simulated huge) array. - A cross-platform width reporter that prints
sizeofforint/long/size_t/ptrdiff_tand explains LP64 vs LLP64.
Further Reading¶
- Computer Systems: A Programmer's Perspective (Bryant & O'Hallaron), Ch. 2.2–2.3 — conversions and the signed/unsigned interactions, with proofs.
- Secure Coding in C and C++ — Robert Seacord. The authoritative treatment of integer conversion and overflow vulnerabilities. CERT INT rules.
- CERT C Coding Standard — INT30-C, INT31-C, INT32-C (overflow, conversion, signed overflow). https://wiki.sei.cmu.edu/confluence/display/c
- "Extra, Extra - Read All About It: Nearly All Binary Searches and Mergesorts are Broken" — Joshua Bloch, 2006 (the
(lo+hi)/2bug). - The C Standard (n2310/C17) §6.3 (conversions), §6.3.1.1 (promotion) — dense but definitive.
- GCC/Clang docs for
__builtin_add_overflowand the C23<stdckdint.h>proposal. - UndefinedBehaviorSanitizer documentation (
-fsanitize=integer).
Related Topics¶
- This folder:
junior.md(the model),senior.md(internals &INT_MINtraps),professional.md(CVEs & production),interview.md,tasks.md. - Sibling numerics:
- Floating-Point Representation — conversions between int and float, another lossy boundary.
- Security framing:
../../../quality-engineering/static-analysis/sast/middle.md— tools that flag integer-conversion bugs. - Hardware:
../../cpu-architecture/01-alu/middle.md— the overflow and carry flags these techniques read.
In this topic
- junior
- middle
- senior
- professional