Skip to content

Undefined behaviors in C/C++

C and C++ are incredibly powerful programming languages, known for their efficiency and fine-grained control over system resources (e.g., memory). However, this power comes with a caveat: they make it easy to “shoot yourself in the foot” (a quote by Bjarne Stroustrup, the creator of C++). This is because of undefined behaviors and subtle bugs that can arise from improper memory management, pointer arithmetic and other low-level operations permitted by C/C++.

What do we mean by undefined behavior?

Undefined behavior (UB) refers to code whose behavior is unpredictable according to the language specification. When a program executes an operation that has an undefined behavior, the language specification does not specify what should happen. It could lead the program to crash, produce incorrect results, lose/corrupt data, or behave differently depending on the compiler and environment being used. They can also lead to security vulnerabilities, which could potentially be exploited to execute malicious code and gain privileges.

Undefined behaviors can be triggered in many ways, but here are some of the most common ones in C/C++.

Dereferencing a null pointer

A pointer in C and C++ is a variable that stores a memory address, allowing direct manipulation of data and efficient memory management. A pointer to null, also known as a null pointer, is created by assigning 0, NULL, or in the case of C++ nullptr to a pointer variable. A null pointer doesn't point to a valid object, nor to a valid memory location. For this reason, attempting to access the memory pointed by such a pointer (i.e., derefencing) is undefined behavior.

int* ptr = nullptr;
int value = *ptr; // UB

This typically results in the program to crash, or even the whole system in the case of kernel drivers.

Uninitialized variables

This may come as a surprise for people coming from other languages, but C/C++ doesn't initialize built-in type variables (e.g., bool, char, int, float) to a default value. This is because the act of initializing a variable comes with a slight computational overhead. Instead, the value will be whatever is stored in the address at the time of initialization, so usually garbage.

int x;
printf("%d", x); // UB: The memory of x is uninitialized, so it contains a random value.

Similarly, structures that simply aggregate variables of built-in types, such as arrays or struct/class types without a constructor, will not initialize their members when declared without an initializer.

struct Point {
  int x;
  int y;
};

Point origin; // Members origin.x and origin.y are not initialized

Also, with the exception of the main method, returning nothing in a non-void function is undefined behaviour.

int foo() {
    return;
}

Out-of-bounds reads and write

Accessing out-of-bounds memory can cause segmentation faults or corrupt memory. I remember being surprised to see that the compiler would allow this the first time I tried, but it did, and it still does.

int array[5] = { 1, 2, 3, 4, 5 };
array[10] = 32; // UB

Fortunately, compilers today are smarter than they were 25 years ago and clang will emit a warning in this case:

warning: array index 10 is past the end of the array (which contains 5 elements) [-Warray-bounds]

But it won't in the case where the index in not a constant literal, a more thorough analysis of the program would be required.

Integer overflow

Adding or substracting an integer beyond its limit is considered undefined behaviour and compilers may handle it differently. For example, in the case below, some compilers might assign INT_MAX to y, while others might wraparound and return -INT_MAX.

int x = INT_MAX;
int y = x + 1; // UB

Andrew Bedford