Skip to Content
Course content

200: Common Undefined Behavior Pitfalls

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I once spent an entire weekend chasing a bug that only existed in the production build. In the debug environment, the code worked perfectly. But the moment I turned on -O3 optimization, a critical if statement—one designed to catch null pointers—simply disappeared. The compiler hadn't missed it; it had deliberately deleted it. Why? Because earlier in the function, I had dereferenced that same pointer. To the compiler, dereferencing a null pointer is Undefined Behavior (UB). The C standard essentially says, "This will never happen in a valid program." Therefore, the compiler reasoned that since the pointer was dereferenced, it couldn't be null, making the subsequent null check redundant and safe to remove. It's a jarring realization: UB isn't just a crash; it's a permission slip for the compiler to rewrite your logic.

The Optimizer's License to Lie

When you see "Undefined Behavior" in the manual, don't read it as "the program might crash." Read it as "the compiler is no longer obligated to make sense." Modern compilers like GCC and Clang don't just translate your code; they analyze it for mathematical impossibilities. If you write code that triggers UB, you are effectively telling the compiler, "I promise I will never do this." If you then actually do it, the compiler is free to assume that path of execution is unreachable.

One of the most insidious examples is signed integer overflow. In many languages, if a signed 32-bit integer hits 2,147,483,647 and you add 1, it wraps around to a negative number. In C, signed overflow is UB. I've seen cases where a loop like for (int i = 0; i <= limit + 1; i++) was optimized into an infinite loop because the compiler assumed i would never overflow and therefore the termination condition would always eventually be met, regardless of the actual hardware behavior. If you need wrap-around behavior, you must use unsigned int.

The Memory Minefield

You've already learned about pointers, but UB is where those pointers become dangerous. The most common trap is the "dangling pointer"—using memory after you've called free(). Now, if you're lucky, the program crashes immediately with a Segmentation Fault. If you're unlucky, it continues to work. This is the "Heisenbug" scenario: the memory hasn't been reclaimed by the OS yet, so your pointer still points to the old value. You think your code is correct, but the moment you deploy it to a system with a different memory allocator or more load, that memory is overwritten by another thread, and your data vanishes.

Then there is the classic off-by-one error. Accessing array[10] when the array only has 10 elements (indices 0-9) is UB. You might just be reading a piece of a neighboring variable, or you might be overwriting the return address on the stack. I've seen a bug where an off-by-one error in a string buffer didn't crash the program, but it subtly changed the value of a boolean flag located immediately after the buffer in memory, flipping the application's "admin mode" to true. That's not just a bug; that's a security vulnerability.

The Fragility of String Literals

A final point of caution: never try to modify a string literal. Writing char *s = "Hello"; s[0] = 'h'; looks innocent, but in most modern environments, string literals are stored in a read-only section of the binary. Attempting to modify them is UB. On some systems, it will trigger a hardware exception (a crash). On others, if the linker happened to place the string in a writable section, it might actually work. This inconsistency is exactly why UB is so dangerous; it creates a false sense of security that disappears the moment you change compilers or target a new architecture.




📋 Practical Task

Fixing the Corrupted Loop Counter

Below is a piece of code that exhibits a subtle form of Undefined Behavior. The developer is trying to process a buffer, but they've made a critical mistake regarding memory boundaries. When run on some compilers, this code might actually "work" or print strange values, but it is fundamentally broken.


#include <stdio.h>
#include <stdlib.h>

int main() {
    int size = 5;
    int *array = malloc(size * sizeof(int));
    
    for (int i = 0; i < size; i++) {
        array[i] = i * 10;
    }

    // BUG: The developer intends to print the array, 
    // but the loop condition is slightly off.
    for (int i = 0; i <= size; i++) {
        printf("Value at index %d: %d\n", i, array[i]);
    }

    free(array);
    return 0;
}


Your Task:

  • Identify the exact line causing Undefined Behavior.
  • Correct the code so it only accesses valid memory.
  • Add a comment explaining why accessing array[size] is UB and what the potential real-world consequences could be beyond a simple crash.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.