Common Pointer Bugs
Pointers give you direct access to memory. When you use that access correctly, you get performance and flexibility that no other language can match. When you use it incorrectly, you get crashes, data corruption, and security vulnerabilities. These are the bugs that C makes possible and that other languages prevent by design. Learning to recognize and avoid them is a core skill for any C programmer.
Null Pointer Dereference
Dereferencing a pointer with the value NULL is undefined behavior. On most systems, it causes a segmentation fault — the operating system kills your process.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *p = NULL;
// This crashes with a segfault
// printf("%d\n", *p);
// Common cause: not checking malloc
int *arr = malloc(sizeof(int) * 1000000000000UL); // likely fails
if (arr == NULL) {
fprintf(stderr, "allocation failed\n");
return 1;
}
arr[0] = 42; // safe only after the NULL check
free(arr);
return 0;
}
The fix is always the same: check for NULL before dereferencing.
// Defensive pattern
int *find_item(int *arr, size_t len, int target) {
if (arr == NULL || len == 0) {
return NULL;
}
for (size_t i = 0; i < len; i++) {
if (arr[i] == target) {
return &arr[i];
}
}
return NULL;
}
// Caller must check
int *result = find_item(data, len, 42);
if (result != NULL) {
printf("found: %d\n", *result);
}
Dangling Pointer
A dangling pointer points to memory that has been freed or is no longer valid. Using it reads garbage or corrupts unrelated data.
Returning a Pointer to a Local Variable
#include <stdio.h>
// BUG: returns pointer to stack memory that will be reclaimed
int *make_number(void) {
int x = 42;
return &x; // WARNING: x is destroyed when this function returns
}
int main(void) {
int *p = make_number();
// p now points to deallocated stack memory
printf("%d\n", *p); // undefined behavior — might print 42, might crash
return 0;
}
The fix: allocate on the heap, or have the caller provide the buffer.
#include <stdlib.h>
// Option 1: heap allocation
int *make_number_heap(void) {
int *p = malloc(sizeof(int));
if (p != NULL) {
*p = 42;
}
return p; // caller must free
}
// Option 2: caller provides storage
void make_number_out(int *out) {
*out = 42;
}
Using Memory After Free
#include <stdlib.h>
#include <stdio.h>
int main(void) {
int *p = malloc(sizeof(int) * 5);
if (p == NULL) return 1;
p[0] = 100;
free(p);
// BUG: p still holds the old address, but the memory is freed
printf("%d\n", p[0]); // undefined behavior
return 0;
}
The fix: set the pointer to NULL after freeing.
free(p);
p = NULL; // now any dereference will segfault immediately
// which is better than silently reading garbage
Buffer Overflow
Writing past the end of an array overwrites adjacent memory. This is the single most exploited class of vulnerabilities in the history of computing.
#include <stdio.h>
#include <string.h>
int main(void) {
char buffer[8];
int important_value = 42;
printf("before: important_value = %d\n", important_value);
// BUG: "Hello, World!" is 13 characters + null = 14 bytes
// buffer is only 8 bytes
strcpy(buffer, "Hello, World!");
printf("after: important_value = %d\n", important_value);
// important_value may now be corrupted
return 0;
}
before: important_value = 42
after: important_value = 1869769828
The strcpy wrote past the end of buffer and overwrote important_value (and possibly other memory). In a real program, this could overwrite return addresses on the stack, enabling remote code execution.
The fix: always bounds-check.
// Safe alternative: strncpy or snprintf
char buffer[8];
snprintf(buffer, sizeof(buffer), "%s", "Hello, World!");
// buffer now contains "Hello, " (truncated to fit, null-terminated)
Memory Leak
A memory leak occurs when you allocate memory and lose all pointers to it, making it impossible to free.
#include <stdlib.h>
void process_data(void) {
int *data = malloc(sizeof(int) * 1000);
if (data == NULL) return;
// ... use data ...
// BUG: if we return early here, data is never freed
if (/* some error condition */ 0) {
return; // leak: data is not freed
}
// ... more processing ...
free(data);
}
In a short-lived program, leaks are annoying but not fatal — the OS reclaims all memory when the process exits. In a long-running server, leaks accumulate and eventually exhaust available memory.
The fix: ensure every malloc has a corresponding free on every code path. The goto cleanup pattern is idiomatic — declare all resources at the top, use goto cleanup on every error, and release everything in one block at the end. free(NULL) is defined to do nothing, so you do not need to check before freeing.
Uninitialized Pointer
An uninitialized pointer holds whatever garbage value was in memory. Dereferencing it reads or writes to a random address.
#include <stdio.h>
int main(void) {
int *p; // uninitialized — holds garbage
// printf("%d\n", *p); // undefined behavior — might crash, might not
// Fix: always initialize
int *q = NULL; // explicitly "points to nothing"
int x = 42;
int *r = &x; // explicitly points to x
return 0;
}
The rule: every pointer should be initialized at declaration — to NULL, to a valid address, or to the result of malloc. An uninitialized pointer is a loaded gun pointed in a random direction.
Double Free
Freeing the same memory twice corrupts the memory allocator's internal data structures. It can cause crashes, silent data corruption, or exploitable security vulnerabilities.
#include <stdlib.h>
int main(void) {
int *p = malloc(sizeof(int));
if (p == NULL) return 1;
*p = 42;
free(p);
// BUG: p is already freed
free(p); // undefined behavior — corrupts allocator
return 0;
}
The fix: set pointers to NULL after freeing. free(NULL) is safe.
free(p);
p = NULL;
// free(p); // now this is free(NULL), which is a no-op
Use-After-Free
Use-after-free is the hardest pointer bug to find. The memory has been freed, but the pointer still holds the old address. The allocator may have given that memory to another allocation. Now two parts of your program think they own the same memory.
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(void) {
char *name = malloc(32);
if (name == NULL) return 1;
strcpy(name, "Alice");
char *alias = name; // alias points to the same memory
free(name);
name = NULL;
// BUG: alias still points to the freed memory
// The allocator may have reused it for something else
char *other = malloc(32);
if (other == NULL) return 1;
strcpy(other, "XXXXXXXXXX");
// alias might now point to "XXXXXXXXXX" instead of "Alice"
printf("alias: %s\n", alias); // undefined behavior
free(other);
return 0;
}
This bug is subtle because it may not crash. The program may appear to work correctly for months before the allocator reuses the memory in a way that causes observable corruption. This is why use-after-free is the source of many real-world security exploits.
Tools That Catch These Bugs
Valgrind
Valgrind runs your program in a virtual environment that tracks every memory access.
$ gcc -g -o program program.c
$ valgrind --leak-check=full ./program
==12345== Invalid read of size 4
==12345== at 0x400567: main (program.c:15)
==12345== Address 0x5204040 is 0 bytes inside a block of size 20 free'd
==12345== at 0x4C2BDEC: free (vg_replace_malloc.c:530)
==12345== by 0x40055E: main (program.c:12)
Valgrind tells you exactly what happened: a read of freed memory, at which line, and where the memory was freed. It also reports memory leaks at program exit.
AddressSanitizer (ASan)
ASan is a compiler feature that inserts runtime checks into your code. It is faster than Valgrind and catches most of the same bugs.
$ gcc -g -fsanitize=address -o program program.c
$ ./program
=================================================================
==12345==ERROR: AddressSanitizer: heap-use-after-free on address 0x602000000010
READ of size 4 at 0x602000000010 thread T0
#0 0x400567 in main program.c:15
#1 0x7f123456 in __libc_start_main
For development, compile with both sanitizers:
$ gcc -Wall -Wextra -Werror -std=c17 -g -fsanitize=address,undefined -o program program.c
This catches the majority of memory bugs at runtime with clear, actionable error messages. The performance cost is irrelevant in development. The bugs these tools catch are real.
Common Pitfalls
- Assuming crashes are the worst outcome — many pointer bugs do not crash. They silently corrupt data, produce wrong results, or create exploitable security holes. A crash is actually the best-case failure mode.
- Testing only the happy path — pointer bugs often manifest under unusual conditions: large inputs, memory pressure, concurrent access. Test edge cases and error paths.
- Not using sanitizers during development — compile with
-fsanitize=address,undefinedduring development. The performance cost is irrelevant in development. The bugs it catches are real. - Manual code review as the only defense — humans miss pointer bugs. Tools catch them mechanically. Use both.
- Ignoring Valgrind's output — Valgrind reports are detailed and accurate. "0 errors" should be the goal. Every reported error is a real bug.
Key Takeaways
- Null pointer dereference is the most common pointer bug. Always check for
NULLbefore dereferencing, especially aftermallocand any function that may returnNULL. - Dangling pointers occur when memory is freed or a local variable goes out of scope. Never return pointers to local variables. Set pointers to
NULLafterfree. - Buffer overflows are the most dangerous pointer bug — they enable remote code execution. Always bounds-check array access and use
snprintfinstead ofstrcpy. - Memory leaks exhaust memory in long-running programs. Ensure every
mallochas afreeon every code path. Thegoto cleanuppattern helps. - Use-after-free is the hardest bug to find because it may not crash and may not manifest consistently.
- Valgrind and AddressSanitizer catch these bugs mechanically. Use them in every development cycle. They are not optional tools — they are essential.