C fundamentals · lesson 10

Undefined behavior

Undefined behavior doesn't mean "crash." It means the C standard places no constraint on what happens. The compiler can do anything — and modern compilers do.

in progress
12 min

What "undefined" actually means

The C standard describes a legal C program. For certain operations it says: "if a program does this, the behavior is undefined." Not "it crashes." Not "it returns garbage." Undefined. The standard has nothing to say. The compiler is free to assume the operation never happens — and to optimize your code under that assumption.

This sounds abstract until you see what happens in practice. A program that triggers UB might crash. It might produce wrong output. It might work perfectly in debug builds and fail in release builds. It might behave differently on different CPUs, compilers, or optimization levels. There is no predictable outcome.

🚫
UB is not a runtime error — it's a contract violation. The moment your program triggers UB, the entire execution is undefined — including code that ran before the UB occurred. The compiler may rearrange, eliminate, or transform code in ways that assume UB never happens.

Signed integer overflow

Signed integer overflow is undefined behavior. The compiler assumes it cannot happen and optimizes accordingly. This produces some of the most surprising bugs in C:

c
// This looks like a reasonable overflow check: int is_near_max(int x) { return x + 100 > x; } // With -O2, gcc compiles this to: return 1; // Because: signed overflow is UB, so the compiler assumes // (x + 100) never wraps, so it's always > x. Always true. // Your "check" disappears entirely.

Null pointer dereference

c
void process(int *p) { int val = *p; // UB if p == NULL if (p == NULL) return; // this check is AFTER the dereference use(val); } // The compiler sees: *p was dereferenced, so p cannot be NULL // (null dereference is UB, so the compiler assumes it didn't happen) // Therefore the null check is dead code and may be eliminated.

Out-of-bounds array access

c
int arr[4] = {1, 2, 3, 4}; int x = arr[4]; // UB — reads 4 bytes past the array arr[-1] = 99; // UB — writes 4 bytes before the array // In practice: usually reads/writes adjacent stack variables. // This is how stack-smashing attacks work.
Use sanitizers to find UB: compile with -fsanitize=undefined,address during development. UBSan catches signed overflow, null dereferences, out-of-bounds, and more at runtime. AddressSanitizer catches heap/stack buffer overflows and use-after-free. These are indispensable tools.

The common sources of UB

  • Signed integer overflow
  • Null or dangling pointer dereference
  • Out-of-bounds array access
  • Reading uninitialized memory
  • Modifying a string literal
  • Calling free() twice on the same pointer
  • Data races (multiple threads accessing shared data without synchronization)
  • Shift by negative amount or by more than the type's width
one-line takeaway

Undefined behavior means the compiler assumes it never happens — and rewrites your code around that assumption.