Integer Representation & Overflow — Hands-On Tasks¶
Topic: Integer Representation & Overflow Focus: Build, break, and reason about fixed-width integers and overflow with your own hands. Every task has a self-check box, a hint, and (for the harder ones) a sparse sample solution.
How to Use This Page¶
Work top to bottom; the tasks escalate from "see the wrap with your own eyes" to "exploit a size-calculation overflow and then fix it." Do them in a language with fixed-width integers and visible overflow — C is ideal because it shows the rawest behavior; Rust is excellent because its checked_*/wrapping_* APIs make intent explicit; Go and Java work well too. Avoid doing the early tasks only in Python — its arbitrary-precision integers hide the very behavior you're trying to observe (though Python is great for the masking exercises at the end).
For each task: - [ ] Read it and predict the answer before running anything. Write your prediction down. - [ ] Run it. Compare to your prediction. - [ ] If they differ, figure out why using the tier pages — that gap is where the learning is.
Tooling tip: compile C tasks with
-fsanitize=signed-integer-overflow -fsanitize=unsigned-integer-overflowto get a runtime diagnostic at the exact overflow point, and also with-O2 -fstrict-overflowto watch the optimizer change behavior. Run Rust tasks in debug mode (default) so overflow panics fire.
Section 1 — See the Representation¶
Task 1.1 — Watch the seam (signed 8-bit wrap)¶
- Print an 8-bit signed integer as you increment it from
124to130. Observe the jump at127.
Hint: Use int8_t in C, i8 in Rust, int8 in Go, byte in Java. Increment in a loop and print each value.
Sample solution (C)
The wrap at 127→−128 is the two's-complement seam.Task 1.2 — Two readings of one bit pattern¶
- Take the byte
0xFFand print it interpreted as unsigned (expect 255) and as signed (expect −1) without changing the bits.
Hint: Store 0xFF in a uint8_t, print with %u; cast the same bits to int8_t, print with %d.
Task 1.3 — Build two's complement by hand¶
- Write a function
negate8(x)that computes the 8-bit two's complement ofxusing only bitwise NOT and+1(i.e.,~x + 1), and verify it matches-xfor all values exceptINT8_MIN. Explain the exception.
Hint: Loop x over −128..127, compare negate8(x) to (int8_t)(-x). Watch what happens at −128.
Sample solution (C)
#include <stdio.h>
#include <stdint.h>
int8_t negate8(int8_t x) { return (int8_t)(~(uint8_t)x + 1); }
int main(void) {
for (int v = -128; v <= 127; v++) {
int8_t x = (int8_t)v;
int8_t n = negate8(x);
if (n != (int8_t)(-x))
printf("mismatch at %d: ~x+1=%d, -x=%d\n", x, n, (int8_t)(-x));
}
return 0;
}
// negate8(-128) == -128 (the asymmetry: -(-128) overflows; ~x+1 wraps to itself)
Task 1.4 — Sign extension vs zero extension¶
- Widen the 8-bit value
0xFFto 32 bits two ways: as a signed type (expect−1) and as an unsigned type (expect255). Print both as%dand as hex.
Hint: int32_t s = (int8_t)0xFF; vs uint32_t u = (uint8_t)0xFF;. Notice the high bits.
Section 2 — Make It Overflow¶
Task 2.1 — One overflow, five languages¶
- Compute
MAX + 1for a 32-bit signed integer in C, Java, Go, Rust (debug), and Python, and tabulate the results.
Hint: Expect: C = wraps but is UB (may print INT_MIN or anything under -O2); Java = MIN_VALUE; Go = MinInt32; Rust debug = panic; Python = a normal big number, no overflow.
Expected results table
| Language | `INT32_MAX + 1` | |----------|-----------------| | C | `−2147483648` at `-O0` (but **UB** — don't rely on it) | | Java | `−2147483648` (defined wrap) | | Go | `−2147483648` (defined wrap) | | Rust (debug) | **panic**: "attempt to add with overflow" | | Rust (release) | `−2147483648` (wraps) | | Python | `2147483648` (no overflow) |Task 2.2 — Unsigned underflow¶
- Compute
0 − 1for an unsigned 32-bit integer. Predict, then run. Why is it not−1?
Hint: Unsigned wrap is defined mod 2³². Expect 4294967295.
Task 2.3 — The infinite loop¶
- Write
for (unsigned i = 5; i >= 0; i--)and observe (briefly!) that it never terminates. Explain why, then fix it two different ways.
Hint: i >= 0 is always true for unsigned. Fixes: signed loop variable, or for (unsigned i = 5; i-- > 0; ).
Task 2.4 — Multiplication overflows before addition¶
- Using 32-bit signed ints, compute
a + banda * bfora = b = 100000. One overflows, one doesn't. Which, and why?
Hint: 100000 + 100000 = 200000 (fine). 100000 * 100000 = 10¹⁰ ≈ 10 billion (overflows 32-bit's ~2.1 billion). Multiplication grows the magnitude far faster.
Section 3 — Conversions & Promotion¶
Task 3.1 — The promotion surprise¶
- In C, print the type and value of
(unsigned char)200 + (unsigned char)100. Then store it back into anunsigned charand print again. Explain the two different results.
Hint: The expression is int with value 300 (promotion). Stored into unsigned char it truncates to 44 (300 & 0xFF).
Task 3.2 — Truncation keeps the low bits¶
- Take the 32-bit value
0x12345678and truncate it to 16 bits and to 8 bits. Predict the surviving bits before running.
Hint: 16-bit → 0x5678, 8-bit → 0x78. Truncation discards the high bits, keeps the low.
Task 3.3 — The signed/unsigned comparison trap¶
- Write
if (-1 < 1u) printf("less"); else printf("not less");in C. Predict the output, run it, then fix the comparison so it behaves mathematically.
Hint: It prints "not less" because −1 converts to 4294967295. Fix: guard the sign first — if (a < 0 || (unsigned)a < b).
Sample solution (C)
Task 3.4 — Narrowing a 64-bit id into a 32-bit field¶
- Take the 64-bit value
5_000_000_000(5 billion) and assign it to a 32-bit signedint. What value results, and what real-world bug does this model (hint: database row counts)?
Hint: 5e9 & 0xFFFFFFFF then reinterpreted as signed. It models a 32-bit primary key / API id overflowing past ~2.1 billion rows.
Section 4 — Detect Overflow¶
Task 4.1 — Pre-check addition¶
- Implement
safe_add(a, b, out)for 32-bit signed ints using only pre-checks againstINT_MAX/INT_MIN(no wider type, no builtin). Test it atINT_MAX + 1,INT_MIN - 1, and normal cases.
Hint: if (b > 0 && a > INT_MAX - b) return false; if (b < 0 && a < INT_MIN - b) return false;
Task 4.2 — Why the post-check fails for signed¶
- Write
int r = a + b; if (r < a) overflow();and test it witha = INT_MAX, b = 1. Compile at-O0and at-O2 -fstrict-overflow. Report whether the check fires in each, and explain.
Hint: It may fire at -O0 but be deleted at -O2 because signed overflow is UB and the compiler assumes a + b < a is impossible. This is the core UB lesson.
Task 4.3 — Post-check IS valid for unsigned¶
- Write the same
r = a + b; if (r < a) overflow();for unsigned and testa = UINT_MAX, b = 1. Confirm it works at all optimization levels. Explain why unsigned differs.
Hint: Unsigned wrap is defined, so a result smaller than an operand provably wrapped. No UB to exploit.
Task 4.4 — Use the hardware¶
- Rewrite
safe_addusing__builtin_add_overflow(orckd_addin C23). Disassemble (or use Compiler Explorer) and confirm it compiles to roughlyadd; jo— one instruction plus a flag-conditional branch.
Hint: return !__builtin_add_overflow(a, b, out);. The point: checked arithmetic is nearly free.
Section 5 — The INT_MIN Trap Family¶
Task 5.1 — Reproduce all three traps¶
- In C, evaluate
−INT_MIN,abs(INT_MIN), andINT_MIN / −1. Predict each, run each (compile without-ftrapvfirst), and note which one crashes and why.
Hint: INT_MIN / −1 raises SIGFPE on x86 (divide error). abs(INT_MIN) returns INT_MIN (still negative). −INT_MIN is UB.
Sample solution (C)
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int main(void) {
printf("-INT_MIN = %d (UB)\n", -INT_MIN); // UB; often prints INT_MIN
printf("abs(MIN) = %d (broken)\n", abs(INT_MIN)); // INT_MIN, still negative
volatile int b = -1; // volatile so it's not const-folded
printf("INT_MIN/-1 ...\n");
printf("= %d\n", INT_MIN / b); // SIGFPE crash on x86
return 0;
}
Task 5.2 — A complete safe_div¶
- Implement
safe_div(a, b, out)that returns false for bothb == 0and theINT_MIN / −1case. Verify it never crashes for any 32-bit inputs.
Hint: if (b == 0) return false; if (a == INT_MIN && b == -1) return false;
Task 5.3 — unsigned_abs¶
- In Rust, show that
i32::MIN.abs()panics (debug) buti32::MIN.unsigned_abs()returns2147483648. Explain why the unsigned result can hold what the signedabscannot.
Hint: 2³¹ fits in u32 (range 0..2³²−1) but not in i32 (max 2³¹−1). The asymmetry disappears when the result type is unsigned.
Section 6 — Saturating vs Wrapping¶
Task 6.1 — Choose the right one¶
- In Rust (or by hand in C), compute
200u8 + 100three ways:wrapping_add(expect 44),saturating_add(expect 255), andchecked_add(expectNone). State a real use case where each is the correct choice.
Hint: wrap → hashing/ring buffer; saturate → audio/color/meter; checked → size/money math.
Task 6.2 — Audio clipping demo¶
- Simulate mixing two audio samples near the maximum amplitude (e.g., two
i16values30000 + 10000). Show the difference between wrapping (produces a huge negative "pop") and saturating (clamps to32767). Which would you ship and why?
Hint: Wrapping: 40000 overflows i16 to −25536 — a loud click. Saturating: clamps to 32767 — graceful clipping.
Task 6.3 — Intentional wrapping hash¶
- Implement a tiny string hash
h = h * 31 + cfor each byte, using wrapping arithmetic explicitly (Wrapping<u32>in Rust, natural wrap in Go/Java). Confirm it never panics even on long strings. Explain why wrapping is correct here, not a bug.
Hint: The hash is defined over ℤ/2³²ℤ; wrap is the algorithm. A checked version would wrongly reject valid inputs.
Section 7 — Security & Production¶
Task 7.1 — Exploit a size calculation¶
- Write the vulnerable
void *buf = malloc(count * size)followed by a loop writingcountelements. Pickcountandsizeso thatcount * sizeoverflowssize_tto a small value (e.g., 16) whilecountis huge. Run it under AddressSanitizer (-fsanitize=address) and watch the heap overflow get caught.
Hint: On 64-bit, size = 16, count = (SIZE_MAX / 16) + 2 makes the product wrap small. ASan reports the heap-buffer-overflow on the first out-of-bounds write.
Sample solution (C, run with -fsanitize=address)
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
int main(void) {
size_t size = 16;
size_t count = (SIZE_MAX / size) + 2; // count*size wraps to ~16
char *buf = malloc(count * size); // tiny allocation!
if (!buf) return 1;
memset(buf, 0xAB, count * size); // writes a few bytes... but if the loop
// used `count` directly it would write count*size bytes -> overflow.
for (size_t i = 0; i < count; i++) buf[i] = (char)i; // ASan fires here
free(buf);
return 0;
}
Task 7.2 — Fix it three ways¶
- Rewrite Task 7.1's allocation safely using (a)
calloc(count, size), (b)__builtin_mul_overflow, and (c) C23ckd_mul. Confirm each refuses the overflowing allocation (returnsNULL/error) instead of allocating a tiny buffer.
Hint: calloc is the one-liner; the builtins let you keep malloc while checking.
Task 7.3 — Safe length-prefixed parser¶
- Write a function that parses a
[4-byte big-endian length][payload]message safely. It must reject: too-short input, a declared length over a sane cap, and a declared length larger than the actual data. Then craft three malicious inputs (short, oversized, lying-length) and confirm all are rejected before any allocation or copy.
Hint: Order matters — check len >= 4 first (guards the len - 4 subtraction), then cap, then consistency. See professional.md's template.
Task 7.4 — Compute a rollover horizon¶
- For a counter incremented at 100 Hz, compute the overflow horizon for 16-bit, 32-bit, and 64-bit unsigned widths (in seconds/days/years). Confirm the 32-bit figure matches the ~248-day Boeing 787 GCU number. Then state which width you'd ship for a device that must run 20 years without reboot.
Hint: horizon_seconds = 2^bits / 100. 2³² / 100 / 86400 ≈ 497 days unsigned (≈ 248 for signed, the 787 case). 64-bit is billions of years.
Expected horizons (100 Hz)
| Width | Unsigned horizon | |-------|------------------| | 16-bit | 65536 / 100 ≈ **10.9 minutes** | | 32-bit | ≈ **497 days** (signed: ≈ 248 days — the 787) | | 64-bit | ≈ **5.85 billion years** | Ship 64-bit for a 20-year device — even 32-bit (497 days) fails, and 16-bit fails in minutes.Section 8 — Cross-Language & Tooling¶
Task 8.1 — Emulate fixed-width in Python¶
- In Python (which never overflows), emulate 32-bit unsigned wraparound using masking, and 32-bit signed using masking + sign reinterpretation. Verify
wrap32u(0xFFFFFFFF + 1) == 0andwrap32s(0x7FFFFFFF + 1) == −2147483648.
Hint: wrap32u(x) = x & 0xFFFFFFFF. For signed: mask, then if result >= 0x80000000: result -= 0x100000000.
Sample solution (Python)
Task 8.2 — Make UBSan fire¶
- Compile a deliberate signed overflow with
-fsanitize=signed-integer-overflowand confirm UBSan prints the exact file, line, and the operand values. Then add-fwrapvand confirm the same code now runs silently (defined wrap). Explain the difference between detecting and defining.
Hint: UBSan reports; -fwrapv defines-to-wrap (no report). Shipping -fwrapv hides the UB but doesn't find the bug.
Task 8.3 — Watch the optimizer delete a check¶
- On Compiler Explorer (godbolt.org), paste a function with a post-overflow check (
if (a + 100 < a) return -1;). Compare the generated assembly at-O0vs-O2. Confirm the-O2build can remove the check entirely (no compare against the originala). Then add-fwrapvand watch the check reappear.
Hint: This is the most visceral way to see why post-checks fail for signed overflow in C.
Task 8.4 — Width audit across platforms¶
- Write a program that prints
sizeof(int),sizeof(long),sizeof(size_t),sizeof(ptrdiff_t)and the corresponding MAX values. Run it (or reason about it) for LP64 (Linux/macOS) vs LLP64 (Windows). Identify which type changes size between the two and what code it breaks.
Hint: long is 64-bit on LP64, 32-bit on LLP64. Code assuming long == 64 bits (e.g., storing a 64-bit value in a long) breaks on Windows.
Capstone Projects¶
Capstone A — A safe_int library¶
- Build a small library exposing
add/sub/mul/div/neg/absforint32_tandint64_t, each returning a success flag and an out-parameter, correctly handling every overflow case includingINT_MIN/-1andINT_MINnegation/abs. Write a test suite that hits MAX, MIN, 0, −1, and theINT_MINtraps. Bonus: cross-check every result against__builtin_*_overflow.
Capstone B — One overflow, six languages, one report¶
- Run an identical battery of operations (
MAX+1,MIN-1,0-1unsigned,MIN/-1,abs(MIN), narrowing300→u8) in C, Java, Go, Rust, Python, and JavaScript, and produce a single comparison matrix showing how each language responds (wrap / UB / panic / trap / no-overflow / precision-loss). This is the single best artifact for internalizing the cross-language model.
Capstone C — Harden a vulnerable parser¶
- Take (or write) a tiny binary-format parser with a
count-driven allocation and a trusted length field. Demonstrate the overflow-to-heap-overflow under ASan, then harden it: boundary validation,calloc/ckd_mul, width widening, and a fuzzing harness (libFuzzer/AFL++) under sanitizers. Show the fuzzer no longer finds a crash. Write up the before/after.
Self-Assessment Checklist¶
After completing these tasks, you should be able to check every box:
- I can write the two's-complement representation of any small signed integer by hand and explain why
~x + 1negates it. - I can state the exact range of any n-bit signed and unsigned integer and explain the
INT_MINasymmetry. - I can predict what
MAX + 1does in C, Java, Go, Rust (debug & release), and Python — and explain why each differs. - I can detect overflow correctly with a pre-check and explain why the signed post-check is UB in C.
- I can write a complete
safe_divthat handles bothb == 0andINT_MIN / −1. - I can explain why
−1 < 1uis false and fix a signed/unsigned comparison. - I can explain C integer promotion and why 8-bit arithmetic "wraps" only on the narrowing store.
- I can identify a
malloc(count * size)vulnerability, exploit it under ASan, and fix it three ways. - I can choose between wrapping, saturating, and checked arithmetic for a given domain and justify it.
- I can compute a counter's rollover horizon for a given width and rate, and pick a width for a multi-year system.
Related Topics¶
- This folder:
junior.md,middle.md,senior.md,professional.md,interview.md. - Sibling numerics: Floating-Point Representation.
- Security tooling:
../../../quality-engineering/dynamic-analysis-and-sanitizers/tasks.md— UBSan/ASan/fuzzing practice.
In this topic
- interview
- tasks