C course menu
Undefined Behavior in C: What It Is and How to Avoid It
Understand undefined behavior in C: what the standard means by undefined, unspecified and implementation-defined behavior, the most common causes (signed overflow, out-of-bounds access, null dereference, uninitialised reads, unsequenced modifications), why the optimiser makes UB dangerous, and tools that detect it.
On this page
- Three kinds of "not fully defined"
- Common causes
- Signed integer overflow
- Out-of-bounds access
- Null and dangling pointers
- Uninitialised variables
- Modifying a variable twice without sequencing
- Other common causes
- Why the optimiser makes UB dangerous
- Writing a correct overflow check
- Tools that detect undefined behavior
- Practice
- Key takeaway
The C standard does not define what happens for every program. When a program breaks certain rules, its behavior is undefined: the standard places no requirements on it at all. The program might crash, print nonsense, appear to work, or behave differently when compiled with optimisation or on another machine. Understanding undefined behavior (UB) is what separates writing C that "works on my machine" from writing correct C.
Three kinds of "not fully defined"#
Implementation-defined and unspecified behavior are safe if your program works for every allowed choice. Undefined behavior must simply be avoided.
Common causes#
Because their results are unpredictable, the examples in this section are not run by our checker.
Signed integer overflow#
int x = INT_MAX;
x = x + 1; /* undefined: signed overflow */Unsigned arithmetic is defined to wrap around modulo 2ⁿ, so UINT_MAX + 1u is 0. Signed overflow is not. Check before the operation, as shown below.
Out-of-bounds access#
int a[4];
a[4] = 1; /* undefined: past the end */Null and dangling pointers#
int *p = NULL;
*p = 5; /* undefined: null dereference */Using memory after free, or the address of a local variable after its function returns, is also undefined (Memory Errors).
Uninitialised variables#
int total;
printf("%d\n", total); /* reading an indeterminate value */Modifying a variable twice without sequencing#
int i = 1;
i = i++ + ++i; /* undefined: i modified twice, unsequenced */
a[i] = i++; /* undefined: i read and modified, unsequenced */Other common causes#
- Modifying a string literal:
char *s = "hi"; s[0] = 'H';. - Shifting by a negative amount or by at least the type's width:
1 << 32with a 32-bitint. - Dividing by zero, or
INT_MIN / -1. - Calling a function through a pointer of the wrong type.
- Breaking the
restrictpromise, or overlappingmemcpyregions.
Why the optimiser makes UB dangerous#
Compilers assume undefined behavior never happens, and optimise on that assumption. For example, a compiler can reason that x + 1 > x is always true for a signed int x, because overflow "cannot" happen, so it may delete an overflow check written that way:
int will_overflow(int x) {
return x + 1 < x; /* intended overflow check: may be compiled to "return 0" */
}This is why UB can make code behave differently with -O0 and -O2, and why a later compiler version can "break" code that used to work.
Writing a correct overflow check#
Check the limits before doing the operation:
#include <limits.h>
#include <stdio.h>
int safe_add(int a, int b, int *result) {
if ((b > 0 && a > INT_MAX - b) || (b < 0 && a < INT_MIN - b)) return 0;
*result = a + b;
return 1;
}
int main(void) {
int r;
printf("%d\n", safe_add(2000000000, 200000000, &r));
int ok = safe_add(2000000000, -100000000, &r); /* call first, then read r */
printf("%d %d\n", ok, r);
printf("%u\n", UINT_MAX + 1u); /* unsigned wraps: well defined */
return 0;
}0
1 1900000000
0GCC and Clang also provide __builtin_add_overflow(a, b, &result), and C23 standardises this as ckd_add in <stdckdint.h>.
Tools that detect undefined behavior#
- Warnings: always compile with
-Wall -Wextra; many UB patterns produce warnings. - UndefinedBehaviorSanitizer:
gcc -g -fsanitize=undefined(Linux and macOS) reports signed overflow, bad shifts, misaligned pointers and more while the program runs. - AddressSanitizer:
-fsanitize=addressfinds out-of-bounds and use-after-free errors. - Static analysers: tools such as
clang --analyzeandcppcheckfind some bugs without running the code.
Practice#
Practice exercise
Write int safe_multiply(int a, int b, int *result) that returns 0 instead of overflowing, using __builtin_mul_overflow (which returns true on overflow). Test 50000 * 50000 and 1000 * -2000.
Show solution
#include <stdio.h>
int safe_multiply(int a, int b, int *result) {
return !__builtin_mul_overflow(a, b, result);
}
int main(void) {
int r;
printf("%d\n", safe_multiply(50000, 50000, &r));
if (safe_multiply(1000, -2000, &r)) printf("%d\n", r);
return 0;
}0
-2000000__builtin_mul_overflow is a GCC and Clang extension; with the Microsoft compiler you would compare against INT_MAX / b instead.
Key takeaway#
Finished this lesson?