Code of the Day
IntermediatePointers

Common pointer bugs

Identify and understand the most dangerous pointer bugs in C — null dereference, dangling pointers, double free, wild pointers, and off-by-one out-of-bounds accesses.

CIntermediate11 min read
Recommended first
By the end of this lesson you will be able to:
  • Identify null pointer dereference and explain the crash it causes
  • Explain what a dangling pointer is and how it arises
  • Describe the double free bug and why it corrupts the heap
  • Recognise wild (uninitialised) pointers and why they are dangerous

Pointer bugs are the most common source of crashes and security vulnerabilities in C programs. Unlike many bugs that cause wrong output, pointer bugs often cause undefined behaviour — the program might appear to work until a specific memory layout change causes it to corrupt data silently or crash unpredictably. This lesson catalogs the main categories so you can recognise them.

1. Null pointer dereference

Dereferencing a NULL pointer is always a crash (segmentation fault):

int *p = NULL;
*p = 42; /* CRASH: cannot write to address 0 */

The most common cause: a function returns NULL on failure and the caller does not check:

int *arr = malloc(100 * sizeof(int));
arr[0] = 1; /* CRASH if malloc returned NULL (out of memory) */

Fix: always check the return value of functions that can return NULL:

int *arr = malloc(100 * sizeof(int));
if (arr == NULL) {
    fprintf(stderr, "malloc failed\n");
    exit(1);
}
arr[0] = 1; /* safe */

2. Dangling pointer

A points to memory that has been freed or gone out of scope:

/* Dangling pointer after free */
int *p = malloc(sizeof(int));
*p = 42;
free(p);
*p = 100; /* UNDEFINED BEHAVIOUR: memory has been returned to allocator */
/* Dangling pointer to a local variable */
int *bad_pointer(void) {
    int local = 42;
    return &local; /* WRONG: local is gone when function returns */
}

After free(p), the allocator may reuse that memory for something else. Writing to it can corrupt an unrelated data structure. Accessing freed memory is undefined behaviour and a security vulnerability.

Fix: set pointers to NULL after freeing:

free(p);
p = NULL;
/* Now *p = 100 would crash immediately (null dereference) rather than silently corrupt */

3. Double free

Calling free on the same pointer twice corrupts the allocator's internal bookkeeping:

int *p = malloc(sizeof(int));
free(p);
free(p); /* UNDEFINED BEHAVIOUR: heap corruption */

The allocator uses the freed memory block for its own metadata. Freeing it again overwrites that metadata with garbage, often causing the next malloc or free call to crash in a completely unrelated part of the program.

The bug is particularly nasty to debug because the crash happens far from the original mistake. Tools like valgrind catch it immediately.

Fix: set to NULL after free (freeing NULL is a safe no-op):

free(p);
p = NULL;
free(p); /* safe -- free(NULL) is defined to do nothing */

4. Wild (uninitialised) pointer

A pointer that has never been assigned holds whatever bytes were in that stack location:

int *p; /* not initialised -- could hold any value */
*p = 42; /* UNDEFINED BEHAVIOUR: writes to a random address */

The address could be anything: another variable's memory, the stack's return addresses, or an unmapped region. Writes can silently corrupt data; the crash (if any) comes later in an unrelated location.

Fix: always initialise pointers. Either to a valid address or to NULL:

int *p = NULL; /* explicitly "points nowhere" */
/* or */
int x = 0;
int *p = &x;

5. Out-of-bounds access

Writing past the end of an array can overwrite the return address on the stack:

char buf[8];
strcpy(buf, "This string is too long for the buffer"); /* buffer overflow */

This is the basis of classic stack-smashing exploits. The overflow overwrites the saved return address with an attacker-supplied value, redirecting execution when the function returns.

Buffer overflows are security vulnerabilities. Even on modern systems with stack canaries and ASLR, buffer overflows are among the most exploited bug classes. Use strncpy, snprintf, and size-checked functions everywhere user-supplied data interacts with fixed-size buffers.

6. Pointer aliasing bugs

When two pointers point to overlapping memory, modifying through one can produce unexpected results through the other:

void bad_copy(int *dst, int *src, int n) {
    for (int i = 0; i < n; i++) {
        dst[i] = src[i]; /* if dst and src overlap, src[i] may have been modified */
    }
}

Use memmove instead of memcpy when source and destination may overlap — memmove handles this correctly.

Debugging pointer bugs

  • Valgrind: catches null/dangling pointer reads, double free, and out-of-bounds heap accesses. Run with valgrind --leak-check=full ./program.
  • AddressSanitizer: compile with -fsanitize=address (gcc or clang). Catches stack and heap out-of-bounds, use-after-free, and double free at runtime, with precise error messages.
  • NULL after free: the simplest discipline that prevents half of these bugs.

Where to go next

Next: the strlen and strcpy lab — you will implement your own versions of strlen and strcpy from scratch using raw pointer manipulation, which solidifies everything in this module.

Finished reading? Mark it complete to track your progress.

On this page