5.3.13 · D4Build Systems & Toolchain

Exercises — Undefined Behavior Sanitizer (UBSan)

2,388 words11 min readBack to topic

Before we start, one shared fact we will use again and again. Everything here assumes a 32-bit int. From Two's Complement Representation, the representable range is We write for the top (most significant) bit of the machine word — it is when is negative, when is non-negative. Look at the figure below to see why one bit tells you the sign at all.

Figure — Undefined Behavior Sanitizer (UBSan)

The bar shows a 32-bit signed integer. The leftmost cell (orange) is the sign bit with weight ; the other 31 cells (blue) all add positive weights. So if you flip only the top bit you swing by exactly — that jump is what "wraparound" means (see Integer Overflow & Wraparound).


Level 1 — Recognition

Recall Solution L1·Q1
  • (a) UB. Signed overflow: true sum . Not representable ⇒ UB.
  • (b) Defined. Unsigned overflow is not UB — it wraps modulo . Result is . (See the parent note's recall: signed = UB, unsigned = defined.)
  • (c) UB. 1 << 31 shifts a into the sign-bit position of a signed type. Since C++20 this is defined, but in C and pre-C++20 shifting into the sign bit is UB. UBSan flags it under the shift check.
  • (d) Defined (implementation-defined). -1 >> 1 is a right shift of a negative value — implementation-defined, not UB. UBSan does not flag it.

Level 2 — Application

Recall Solution L2·Q2

True sum . . Since , the sum is not representable ⇒ overflow (UB). Same-sign check: both operands are non-negative (), and the wrapped result is negative (). Sign flipped despite equal input signs ⇒ overflow flag set ⇒ UBSan fires.

Recall Solution L2·Q3

The standard requires the shift amount to satisfy , i.e. . UBSan inserts, straight from that constraint:

if (k < 0 || k >= 32) __ubsan_handle_shift_out_of_bounds(...);

So or both report. (On x86 the bare hardware would compute k & 31, silently giving x << 0 for k = 32 — a wrong answer with no crash, exactly why UBSan is needed.)


Level 3 — Analysis

Recall Solution L3·Q4

With and , the sum lies between the two operands: Since and , we get . The result is squeezed inside the valid range, so it always fits — no overflow possible. Geometrically (figure below), adding a negative moves you left from but never past ; adding a positive to moves right but never past . You can never escape the interval from inside it by a step that stays inside it.

Figure — Undefined Behavior Sanitizer (UBSan)
Recall Solution L3·Q5
  • INT_MIN / -1 . But , so is one past the top ⇒ not representable ⇒ UB (and on many CPUs a hardware divide trap).
  • INT_MIN * -1 — same value, same problem ⇒ UB.
  • INT_MAX / -1 , which is ⇒ representable ⇒ defined. The asymmetry is the whole story: two's complement has one more negative number than positive, so negating (or the equivalent divide/multiply) the single extreme negative has no positive twin.

Level 4 — Synthesis

Recall Solution L4·Q6

Line (1) dereferences p. Dereferencing NULL is UB, so the compiler is entitled to assume p is not NULL — because if it were, the program already had UB and the standard imposes no requirements. Under that assumption, the test p == NULL on line (2) is always false, so the optimizer may delete the entire if branch. Your safety check silently vanishes.

  • UBSan (-fsanitize=undefined) catches the null / invalid pointer dereference at line (1) with a null-pointer-use diagnostic.
  • ASan is the stronger tool for the general case (wild pointers, use-after-free, out-of-bounds), because it validates the actual memory address. For a plain NULL deref both catch it; for a garbage-but-non-null pointer, ASan is the one that flags the bad load. Real fix: reorder — check p == NULL before the dereference. Then the assumption never kicks in.
Recall Solution L4·Q7
clang -fsanitize=undefined,address -fno-sanitize-recover=all -g -O1 file.c -o prog
  • -fsanitize=undefined → UBSan (overflow, shifts). ,address → ASan for the heap-buffer-overflow (a memory error UBSan doesn't cover).
  • -fno-sanitize-recover=allabort on the first violation, so the earliest (root-cause) error isn't buried by cascading later reports.
  • -g → embed debug info so reports show file:line:col instead of raw addresses.
  • -O1 (not -O0) → catches optimization-sensitive UB while keeping stack traces readable; both GCC and Clang support this combination.

Level 5 — Mastery

Recall Solution L8

No UB. In C, int8_t operands are promoted to int before +. So the addition is computed in int — no overflow (well within int range). The narrowing back to int8_t (range ): doesn't fit, so it is implementation-defined conversion (typically wraps to ), not UB. Default UBSan does not flag this; only the optional -fsanitize=implicit-conversion (implicit-integer-truncation) sublevel would warn. So the base -fsanitize=undefined build stays silent.

Recall Solution L5·Q9
  • (a) UB. ⇒ underflow. Wrapped value: .
  • (b) UB. , one past INT_MAX. Wrapped value: (negating INT_MIN gives INT_MIN back — the famous fixed point).
  • (c) Defined. Unsigned: .
  • (d) UB. . Wrapped: .
  • (e) Defined (implementation-defined). Right-shifting a negative is implementation-defined, not UB. Arithmetic shift typically gives .
Recall Solution L5·Q10

Let (true math). We show: () (same sign in, flipped sign out).

  • Opposite signs ( or vice versa): by L3·Q4 the sum stays in range, so never leaves range and the sign-bit can't flip past a limit (result lies between operands). Both rules say "no overflow." ✓
  • Both non-negative (): . Overflow (range rule) the true sum needs bit , i.e. the wrapped machine result has (flipped). Same condition. ✓
  • Both negative (): . Overflow the wrapped result's top bit becomes (flipped to non-negative). Same condition. ✓ Every case matches, so the two rules are equivalent — and the sign-bit version is what the hardware OF flag computes in one step.

Wrap-up recall

Recall Which of these are UB? (cover the answers)
  • INT_MAX + 1 ::: UB (signed overflow)
  • UINT_MAX + 1u ::: defined (wraps to 0)
  • INT_MIN / -1 ::: UB (result not representable)
  • 1 << 32 ::: UB (shift ≥ width)
  • -1 >> 1 ::: defined (implementation-defined, not UB)
  • deref p then check p == NULL ::: the check may be deleted by the optimizer; deref of possibly-null is UB

Parent: Undefined Behavior Sanitizer (UBSan) · Hinglish: 5.3.13 Undefined Behavior Sanitizer (UBSan) (Hinglish)