The Undefined Behavior Catalog¶
Undefined behavior (UB) is the single most misunderstood thing about C. It is not “unspecified” (compiler picks one of several documented options) and it is not “implementation-defined” (compiler documents its choice). It is the standard makes no promises whatsoever. The compiler is allowed to assume you never write UB, and modern optimizers ruthlessly exploit that assumption: they’ll delete branches, hoist loads out of loops, and produce output that looks nothing like your source. If your code hits UB, whatever the program does — including “seems to work” — is not a guarantee.
This file catalogs the 12 UB patterns an ML engineer coming back to C will actually hit. Not the exotic ones (trap representations on obsolete hardware, sequence points across function calls in ways no one writes). The ones that ship bugs to production in 2026.
Why UB Matters More in 2026 Than in 2010¶
Compiler optimizations got smarter every year. Chris Lattner’s famous 2011 series “What Every C Programmer Should Know About Undefined Behavior” is even more true today than then. Real production examples:
Kernel bug from a null check after dereference:
p->x = ...; if (!p) return -1;— the compiler sawp->xand concludedp != NULL, then deleted the null check. Linux CVE-2009-1897 is the exact story.Loop that assumed signed wraparound:
for (int i = 0; i * i < 100; i++)— compiler assumedi * idoesn’t overflow (signed overflow is UB), replaced withi < 10, but a hand-crafted input hit overflow and the “safe” loop ran forever.Strict-aliasing violation in a bit-cast:
uint32_t bits = *(uint32_t *)&some_float;— the compiler is allowed to assumefloat*anduint32_t*never point to the same object; it can reorder loads/stores across them.
The 12 UBs You Will Actually Hit¶
1. Signed Integer Overflow¶
int x = INT_MAX;
x + 1; // UB. Not INT_MIN. Not any specific value.
Why: the standard lets the compiler assume signed math doesn’t wrap so it can optimize x + 1 > x to true and hoist induction variables into registers.
Detector: -fsanitize=signed-integer-overflow (part of UBSan).
Fix: use unsigned when you want modular arithmetic; use explicit overflow checks (__builtin_add_overflow in GCC/Clang) when you want to detect.
2. Unsigned Overflow Is Not UB — Wraparound Is Defined¶
Not undefined behavior. Included here for symmetry: UINT_MAX + 1 == 0 is guaranteed. Rely on it in hash functions and CRC.
3. Strict Aliasing Violation¶
float f = 3.14f;
uint32_t bits = *(uint32_t *)&f; // UB. The compiler assumes float* and uint32_t*
// never point to the same object.
Fix: use memcpy. It has an aliasing-exempt definition and modern compilers optimize it to a single move for known sizes:
uint32_t bits;
memcpy(&bits, &f, sizeof(bits)); // portable and fast.
Detector: GCC’s -fstrict-aliasing -Wstrict-aliasing. Clang doesn’t emit an equally sharp warning; UBSan catches some cases but not all.
The allowed aliases (per the “effective type” rule):
char *,signed char *,unsigned char *— can alias anything (this is howmemcpyworks).Same type on both sides.
Signed vs unsigned version of same type.
Compatible pointer types (a
struct { int x; }and its members, etc).
4. Uninitialized Read¶
int x;
printf("%d\n", x); // UB. x has indeterminate value.
// Modern compilers may return 0, or the last thing on the stack,
// or delete the whole function if they can prove UB.
Detector: -fsanitize=memory (Clang only, Linux only), or -Wuninitialized (compile-time, incomplete).
Fix: always initialize: int x = 0; or int x = compute();. C99+ mixed declarations mean you can declare-at-first-use.
5. Out-of-Bounds Access¶
int a[10];
a[10] = 0; // UB. Off-by-one on the top.
a[-1] = 0; // UB.
int *p = a + 15; // Even computing this address is UB
// (only address of one past the last element is legal).
Detector: -fsanitize=address. Catches heap, stack, and global OOB.
6. Null Pointer Dereference¶
int *p = NULL;
*p = 42; // UB. Usually SIGSEGV, but not required to be.
p->field; // Also UB. Even *reading* through NULL is UB.
Detector: ASan reports SEGV on unknown address 0x0.
7. Use-After-Free¶
int *p = malloc(sizeof(int));
*p = 42;
free(p);
*p = 43; // UB. May "work" until the allocator reuses that memory.
Detector: ASan; specifically heap-use-after-free.
Discipline: set the pointer to NULL after free. Doesn’t prevent UAF via other aliases, but catches the local case:
free(p); p = NULL;
8. Double Free¶
free(p);
free(p); // UB. Corrupts the heap allocator's bookkeeping.
Detector: ASan reports attempting double-free. glibc’s default allocator also has some runtime detection.
9. Misaligned Access¶
char buf[16];
uint64_t *p = (uint64_t *)(buf + 1); // p is 1-byte-aligned but points at a 8-align type
*p = 0; // UB. On x86 you may not notice. On ARM (Apple Silicon!) you can trap.
Detector: -fsanitize=alignment (part of UBSan).
Fix: use memcpy for possibly-misaligned reads/writes.
10. VLA Size Overflow / Non-Positive Size¶
int n = get_from_user();
int arr[n]; // If n <= 0 or n * sizeof(int) overflows: UB.
// If n is huge: stack overflow (which is also, effectively, UB).
Fix: don’t use VLAs. Use malloc. Linux kernel policy since 2018 is “no VLAs anywhere.”
11. Sequence Point / Sequenced-Before Violations¶
int i = 0;
i = i++; // UB (before C11). Unsequenced modifications.
a[i] = i++; // UB in most standards; even in C11 the ordering is unspecified.
printf("%d %d\n", i++, i++); // Unspecified evaluation order → different results per compiler.
C11 tightened the rules with the “sequenced before” model but there are still traps. Rule: do not modify the same object twice in an expression without an intervening sequence point (a full-expression boundary like ;, &&, ||, ?:, function call comma).
Detector: -Wsequence-point (compile-time, catches obvious cases).
12. INT_MIN / -1 and INT_MIN % -1¶
int x = INT_MIN;
int y = -1;
int q = x / y; // UB. Mathematically INT_MIN / -1 == -INT_MIN == INT_MAX + 1,
// which overflows a signed int.
int r = x % y; // Also UB on many implementations.
Detector: -fsanitize=integer-divide-by-overflow.
Fix: check !(x == INT_MIN && y == -1) before dividing signed integers when both are attacker-controlled.
The Meta-Lesson: How the Compiler “Uses” UB¶
Consider:
int f(int x) {
if (x + 1 < x) return -1; // overflow check
return x + 1;
}
The if looks like a signed-overflow guard. But signed overflow is UB. So the compiler assumes x + 1 < x is false (because if x + 1 overflows, the whole program is undefined, so we don’t care what it computes). It deletes the check. Your “safe” function is no longer safe.
Correct form:
if (x > INT_MAX - 1) return -1; // no overflow, defined arithmetic
Or use the compiler builtin:
int r;
if (__builtin_add_overflow(x, 1, &r)) return -1;
return r;
Or in C23:
#include <stdckdint.h>
int r;
if (ckd_add(&r, x, 1)) return -1; // checked add, C23
return r;
The Standard Defense Stack¶
For any code you ship:
Compile with
-Wall -Wextra -Wpedantic -Werror. Catches ~10% at compile time.Run under
-fsanitize=address,undefinedin dev + CI. Catches ~70% of UB you’d actually hit at runtime.Run under
-fsanitize=threadin a separate CI job (Phase 4+). Catches races.For high-assurance code, use
-fsanitize=memoryon Clang/Linux. Catches uninit reads.Static analysis:
clang --analyze,scan-build,-fanalyzeron GCC 14+.Fuzzing (Phase 6+): AFL++, libFuzzer. Explores paths your tests didn’t.
If you do only #1 and #2, you’ll catch the majority of UB. That’s your Phase 0 default and it stays your default forever.
What Most People Get Wrong About UB¶
They think “UB is theoretical — my code works, so it’s fine.” UB is not theoretical; it’s a license the compiler has to break your assumptions in the next version. Code that “worked” under GCC 9 breaks under GCC 14 all the time because a new optimization pass exploited a UB that was previously benign. The only guarantee against this is not writing UB in the first place.
The second thing they get wrong: they see “UB” and think “crash.” UB does not have to crash. Sometimes it silently returns the wrong answer, forever, in production, for one customer in six. That’s worse than a crash. Sanitizers exist to convert silent UB into loud crashes at dev time — that’s why they’re not optional.
Exercises¶
Write a program that triggers each of the 12 UBs above (except #2, which isn’t UB). For each, run it without sanitizers and see what happens. Then run it with
-fsanitize=address,undefinedand read the output.Write the “safe” signed-overflow-checking function above, compile with
-O2, run under UBSan with an input that triggers overflow. Watch UBSan fire and see how the compiler removed your check.Take a piece of your own code from a past project. Recompile with the strict flag set + sanitizers. Fix everything that fires. Report the count to yourself.
Read John Regehr’s blog series on undefined behavior at blog.regehr.org. Two evenings well spent.
Return to README.md · Next: 06_c_standards_landscape.md