The parent note taught you what UBSan is and why the overflow check is even possible. This page does the opposite of hand-waving: we march through every kind of forbidden move a C program can make, one worked example per case, and we predict what the umpire (UBSan ) will shout before we run it. If you can forecast the verdict for every cell below, you understand UBSan.
Everything here rests on one number system: Two's Complement Representation for a 32-bit int, whose range is [ − 2 31 , 2 31 − 1 ] , i.e. [ − 2147483648 , 2147483647 ] . Keep those two edge numbers in your head — most of the traps live right at them.
Each cell is a distinct way a program can (or cannot) commit Undefined Behavior. We will hit every one.
Cell
Case class
The trap
UBSan verdict?
A
Signed add, same sign , past the edge
INT_MAX + 1
✅ overflow
B
Signed add, opposite sign
INT_MAX + (-1)
❌ always safe
C
Signed negative edge overflow
INT_MIN + (-1)
✅ overflow
D
Unsigned overflow (the twist)
UINT_MAX + 1u
❌ well-defined wrap
E
Division degenerate
INT_MIN / -1
✅ overflow
F
Division by zero
x / 0
✅ (separate check)
G
Shift out of range
n << 32, n << -1
✅ bad shift
H
Pointer misalignment / null
((S*)1)->a
✅ alignment
I
Word problem (real bug)
averaging two big sensor ints
✅ overflow
J
Exam twist (optimizer + UB)
null-check deleted at -O2
✅ / silent
The single most important column is the last: for each cell you must know whether it is UB at all . Cells B and D look dangerous but are perfectly legal — knowing why is half the skill.
Before the examples, look at the picture that decides cells A, B, and C. The parent gave you the rule: only same-sign additions can overflow. Here is why, on a number line.
Intuition Read the figure
Adding a positive and a negative number (blue arrows) always moves toward zero — the result sits between the two inputs, so it can never leave a range that already contained them. Adding two positives (pink) pushes right; if you start near INT_MAX you fall off the right cliff. Two negatives push left off the left cliff. The cliffs are the only places UBSan can fire.
INT_MAX + 1
int a = 2147483647 ; // INT_MAX
int b = 1 ;
int s = a + b; // UB?
Forecast: does UBSan fire? What number does the "true" sum want to be?
Identify the signs. a ≥ 0 , b ≥ 0 — same sign .
Why this step? From the figure, only same-sign adds can overflow, so this cell is a candidate.
Compute the true mathematical sum in unbounded integers: s true = 2147483647 + 1 = 2147483648 = 2 31 .
Why this step? UB is defined by "is the true value representable?", not by whatever the CPU wraps to.
Compare to the range. Is 2 31 ≤ 2 31 − 1 ? No — it exceeds INT_MAX by exactly 1 .
Why this step? Out of range ⇒ the hardware overflow flag OF is set ⇒ handler fires.
Verify: 2 31 − ( 2 31 − 1 ) = 1 > 0 , so the value overshoots. Verdict: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'. ✅ (See Integer Overflow & Wraparound .)
INT_MAX + (-1)
int a = 2147483647 ;
int b = - 1 ;
int s = a + b; // UB?
Forecast: clean, or does the umpire whistle?
Signs differ (a ≥ 0 , b < 0 ).
Why this step? Opposite-sign adds move toward zero — the pink/blue figure says they cannot overflow.
True sum = 2147483647 − 1 = 2147483646 .
Why this step? Show it lands strictly inside the range.
Range check: − 2 31 ≤ 2147483646 ≤ 2 31 − 1 ? Yes.
Verify: result magnitude 2147483646 ≤ ∣ a ∣ = 2147483647 , confirming "toward zero." Verdict: no UB, no report. ✅ silent and correct.
INT_MIN + (-1)
int a = - 2147483648 ; // INT_MIN
int b = - 1 ;
int s = a + b; // UB?
Forecast: symmetric to cell A?
Both negative — same sign , candidate for overflow.
Why this step? The left cliff of the number line.
True sum = − 2147483648 − 1 = − 2147483649 = − ( 2 31 + 1 ) .
Range check: is − 2 31 − 1 ≥ − 2 31 ? No — it is 1 below INT_MIN.
Why this step? Below the floor ⇒ overflow toward the negative side.
Verify: − 2 31 − ( − 2 31 − 1 ) = 1 > 0 , so we fell off the left edge by one. Verdict: signed integer overflow. ✅
UINT_MAX + 1u
unsigned a = 4294967295 u ; // UINT_MAX = 2^32 - 1
unsigned b = 1 u ;
unsigned s = a + b; // UB?
Forecast: it "overflowed" — surely UBSan fires? (Trap!)
Note the type: unsigned, not int.
Why this step? The standard treats them oppositely. Unsigned arithmetic is defined to wrap modulo 2 32 .
Compute the defined result: s = ( 4294967295 + 1 ) mod 2 32 = 4294967296 mod 4294967296 = 0 .
Why this step? This is a specified value, so there is nothing undefined to catch.
Therefore plain -fsanitize=undefined stays silent.
Why this step? You can opt in to catch this with -fsanitize=unsigned-integer-overflow, but it is not UB and off by default.
Verify: 4294967296 mod 4294967296 = 0 . Verdict under undefined: no report , s == 0. This is the single most-tested gotcha: signed overflow = UB, unsigned overflow = defined wrap.
INT_MIN / -1
int a = - 2147483648 ; // INT_MIN
int r = a / - 1 ; // UB?
Forecast: division making things bigger ? How?
Mathematically, − ( − 2 31 ) = 2 31 .
Why this step? Negating the most-negative number is the one negation that escapes the range — there is no +2^{31} in a signed 32-bit type.
Range check: 2 31 ≤ 2 31 − 1 ? No, off by one.
Why this step? Same overflow test as addition — "is the true result representable?"
UBSan reports it (some CPUs also raise #DE hardware trap).
Verify: 2 31 − ( 2 31 − 1 ) = 1 > 0 . Verdict: signed integer overflow: division of -2147483648 by -1 cannot be represented. ✅
x / 0
int x = 7 ;
int q = x / 0 ; // UB?
Forecast: overflow, or something else?
This is not an overflow — it's the float-divide-by-zero/integer-divide-by-zero check.
Why this step? Division by zero has no mathematical value at all, a separate class of UB.
The inserted guard is if (divisor == 0) report(); — straight from the standard's constraint.
Why this step? Shows UBSan checks constraints , not just ranges.
Verify: divisor = 0 ⇒ guard true. Verdict: division by zero. ✅ (Distinct message from cell E — don't confuse them.)
n << 32 and n << -1
int n = 1 ;
int r1 = n << 32 ; // UB?
int r2 = n << - 1 ; // UB?
Forecast: what's the legal shift range, and what does x86 actually do to 32?
The standard requires shift amount ∈ [ 0 , width ) = [ 0 , 32 ) .
Why this step? Both 32 and -1 fall outside, so both are UB.
The inserted guard: if (shift < 0 || shift >= 32) report();.
Why this step? Directly encodes the constraint from step 1.
Note the silent danger: x86 masks the count as & 31, so 32 & 31 = 0, and 1 << 0 = 1 — a wrong-but-non-crashing answer without UBSan.
Why this step? Explains why "it ran fine" is worthless (see the parent's [!mistake]).
Verify: 32 ≥ 32 is true (out of range), and 32 mod 32 = 0 so the masked result would be 1 << 0 = 1 . Verdict: shift exponent 32 is too large for 32-bit type 'int'. ✅
Worked example Reading through a bad pointer
struct S { int a; };
struct S * p = ( struct S * ) 1 ; // address 1
int v = p -> a; // UB?
Forecast: what does int demand of an address?
alignof(int) == 4 on typical platforms.
Why this step? A 4-byte load must sit on a 4-byte boundary.
UBSan's alignment guard: if (addr % 4 != 0) report();. Here 1 mod 4 = 1 = 0 .
Why this step? The address is not a multiple of the alignment.
Verify: 1 mod 4 = 1 = 0 . Verdict: member access within misaligned address 0x1 ... requires 4 byte alignment. ✅ For the memory-validity side of bad pointers, pair with AddressSanitizer (ASan) .
Worked example Averaging two large sensor readings
Two temperature-scaled sensors return lo = 2000000000 and hi = 2000000000 (both int). A junior computes the midpoint as int mid = (lo + hi) / 2;. Does it work?
Forecast: the true average is 2000000000 , safely below INT_MAX. So the answer's fine, right? (Trap: the intermediate isn't.)
Evaluate the parenthesised sum first , before the divide: l o + hi = 4000000000 .
Why this step? Operator order means the add happens in int, at full magnitude, before any halving.
Range check the intermediate: is 4000000000 ≤ 2147483647 ? No — overflow inside the expression.
Why this step? Both operands positive, sum past INT_MAX ⇒ cell A fires here.
The fix is lo + (hi - lo) / 2 (opposite-sign-safe by cell B) or use a wider/unsigned type.
Why this step? hi - lo = 0 stays tiny; no intermediate ever exceeds the range.
Verify: naive intermediate 4000000000 − 2147483647 = 1852516353 > 0 (overflows); safe formula gives 2000000000 + 0/2 = 2000000000 , in range. UBSan flags the naive line, is silent on the fix. ✅
-O0 "passes" and -O2 "breaks"
int deref ( int * p ) {
int x = * p; // dereference FIRST
if (p == NULL ) // ...then check
return - 1 ;
return x;
}
Forecast: at -O2, what happens to the if? Call deref(NULL).
Dereferencing a null pointer is UB, so the compiler is allowed to assume p is non-null after line *p.
Why this step? This is the whole optimizer license — see Compiler Optimization Levels and the parent's [!mistake].
Under that assumption p == NULL is provably false, so the optimizer deletes the if — the -1 branch vanishes.
Why this step? Shows UB doesn't just corrupt data; it can erase your safety code.
With -fsanitize=undefined -g, UBSan reports the null dereference at the exact line before the optimizer's reasoning can bite.
Why this step? Dynamic checking (see Static Analysis vs Dynamic Analysis ) catches it on the executed path.
Verify: the check-deletion is a boolean fact — p == NULL is assumed false ⇒ branch removed. Verdict: at -O2 the guard is gone (silent corruption); under UBSan: runtime error: load of null pointer of type 'int'. Build with -g so the line number prints. ✅
Intuition The decision you now own
Every arithmetic operation lands in one of three buckets: defined (opposite-sign add, unsigned wrap), UB caught by UBSan (same-sign overflow, INT_MIN/-1, /0, bad shift, bad pointer), or UB hidden by the optimizer (cell J) unless you instrument. UBSan collapses the last two into one loud, located message.
defined wrap mod 2 to the 32
UBSan reports alignment or null
Recall Predict the verdict (cover the answers!)
INT_MAX + (-1) — UB? ::: No. Opposite signs move toward zero; result 2147483646 is in range.
UINT_MAX + 1u — UB? ::: No. Unsigned wraps to 0 by definition; not caught by default.
INT_MIN / -1 — UB? ::: Yes. True value 2 31 exceeds INT_MAX by 1.
1 << 32 — UB and what does x86 do? ::: UB (shift ≥ width); x86 masks to 1<<0 = 1, silently wrong.
(lo+hi)/2 with lo=hi=2000000000 — UB? ::: Yes, the intermediate 4000000000 overflows int before the divide.
Why does the -O2 null-check disappear? ::: Deref before the check is UB, so p is assumed non-null; the if is deleted.
Same Sign Sinks, Opposite is OK; Unsigned Wraps, Signed Snaps. Three words tell you the add cases; the rest are constraint checks (/0, shift, pointer).
Back to Undefined Behavior Sanitizer (UBSan) · Hinglish: 5.3.13 Undefined Behavior Sanitizer (UBSan) (Hinglish)