Undefined Behaviour

The C standard’s escape hatch: when your code triggers UB, the compiler is allowed to do anything — not just produce wrong output, but eliminate entire code paths based on the assumption that UB never occurs.

The Compiler’s Contract

The standard defines certain constructs as UB to give compilers room to optimise. The optimiser is entitled to assume:

  • Signed integers never overflow
  • Pointers are never dereferenced out-of-bounds
  • Variables are not accessed before initialisation
  • The same memory location is not aliased by pointers of incompatible types

When UB does occur, the compiler hasn’t made a mistake — your code violated the contract. The result is often not a crash (which would be detectable) but silently wrong behaviour that manifests only under specific optimisation levels.

Common UB Sources

Arithmetic UB

Signed integer overflow — the most common numeric UB:

int x = INT_MAX;
x + 1;              // UB: result not representable in int

Under -O2, gcc may prove x + 1 > x is always true (no overflow possible) and eliminate the else branch of a subsequent if. This is legal by the standard.

Unsigned overflow is NOT UB — it wraps modulo 2ⁿ. Use unsigned when you need defined wrap-around.

Shift UB:

  • Shifting by a negative amount → UB
  • Shifting by ≥ width of the type → UB (e.g. 1 << 32 on 32-bit int)
  • Left-shifting a negative signed value → UB
  • Right-shifting a negative signed value → implementation-defined (usually arithmetic)

Pointer UB

  • Dereferencing NULL → UB (usually SIGSEGV, but not guaranteed)
  • Accessing memory out of bounds → UB
  • Using a pointer after the object it points to has been freed (UAF) → UB
  • Pointer arithmetic outside the bounds of the same array → UB
  • Dereferencing a pointer to an object after it has gone out of scope (dangling pointer) → UB

Other Common Sources

  • Reading an uninitialised variable
  • Modifying a string literal ("hello"[0] = 'H' → UB; string literals live in read-only memory)
  • Strict aliasing violations: accessing an object through a pointer of an incompatible type
  • Data races: two threads accessing the same variable without synchronisation where at least one access is a write

Why It’s Insidious

UB bugs are hard to find because:

  1. They may not crash — they produce wrong output silently
  2. They may only manifest under -O2 but look correct under -O0 (the optimiser relies on the UB assumption)
  3. They may work on one compiler version and break on another
  4. They may be architecture-dependent (x86 happens to have defined wrap-around for signed overflow at the hardware level; the compiler is not required to use it)

Detection

ToolWhat it catches
-fsanitize=undefined (UBSan)Signed overflow, shift UB, null deref, misaligned access, invalid enum
-fsanitize=address (ASan)Buffer overflow, use-after-free, stack/heap overflow
-fsanitize=memory (MSan)Uninitialised reads
-Wall -WextraSign-compare, implicit conversions (compile-time)
valgrind --tool=memcheckHeap errors at runtime (slower but no recompile needed)

Use UBSan in development builds. The overhead is modest (~1.5–2× slowdown). Turn it off for benchmarks.

Safe Alternatives for Common UB

Overflow-safe addition:

int safe_add(int a, int b, int *result) {
    if (b > 0 && a > INT_MAX - b) return -1;  // would overflow
    if (b < 0 && a < INT_MIN - b) return -1;  // would underflow
    *result = a + b;
    return 0;
}

Or use compiler builtins (gcc/clang): __builtin_add_overflow(a, b, &result).

Portable shift:

// Safe left shift — always use unsigned, check count
uint32_t safe_shl(uint32_t v, unsigned n) {
    return (n < 32) ? (v << n) : 0;
}

Pedagogical Note

UB is not a bug in C — it’s a deliberate design choice that enables the optimisations that make C fast. The mistake is writing code that invokes it. Treat -fsanitize=undefined as a required part of your test suite, not an optional extra.

Appears In

  • m01-c-is-not-java — lesson 1-5: printf, format strings & UB
  • m02-pointers-memory-model — dangling pointers, dereferencing NULL
  • PDF ch01 — C standard and what the compiler actually does
  • PDF ch02 — signed overflow, shift UB, implicit conversion UB (full treatment)
  • PDF ch04 — pointer UB (strict aliasing, pointer provenance)