Memory Leak Detection
Master memory leak detection and prevention. Learn common leak patterns, debugging tools (Valgrind, AddressSanitizer), manual tracking techniques, and best practices for leak-free C programs.
Understanding Memory Leaks
A memory leak occurs when allocated memory is never freed. Over time, leaks accumulate, wasting memory and eventually causing crashes or slowdowns. Unlike garbage-collected languages, C requires manual memory management - every malloc() needs a corresponding free(). Understanding leak patterns helps you write leak-free code.
#include <stdio.h>
#include <stdlib.h>
/* Simple memory leak */
void memory_leak_example(void) {
int *ptr = malloc(100 * sizeof(int));
if (ptr == NULL) {
return;
}
/* Use ptr... */
/* Function returns without free() */
/* LEAK: 400 bytes lost */
}
/* Repeated leaks */
void repeated_leaks(void) {
for (int i = 0; i < 1000; i++) {
int *ptr = malloc(1000 * sizeof(int));
/* Never freed */
/* After loop: 4,000,000 bytes leaked! */
}
}
/* Leak from lost pointer */
void lost_pointer_leak(void) {
int *ptr = malloc(100 * sizeof(int));
if (ptr == NULL) {
return;
}
/* Reassign without freeing */
ptr = malloc(200 * sizeof(int)); /* LEAK: First allocation lost */
if (ptr != NULL) {
free(ptr); /* Only frees second allocation */
}
}
/* Leak from early return */
int early_return_leak(int condition) {
int *buffer = malloc(1000 * sizeof(int));
if (buffer == NULL) {
return -1;
}
if (condition) {
return 0; /* LEAK: Forgot to free */
}
/* Use buffer... */
free(buffer);
return 0;
}
/* Fix: Free before all returns */
int fixed_early_return(int condition) {
int *buffer = malloc(1000 * sizeof(int));
int result = 0;
if (buffer == NULL) {
return -1;
}
if (condition) {
result = 0;
goto cleanup;
}
/* Use buffer... */
cleanup:
free(buffer);
return result;
}
/* Leak from exception path */
typedef struct {
int *data;
size_t size;
} Container;
Container* create_container(size_t size) {
Container *cont = malloc(sizeof(Container));
if (cont == NULL) {
return NULL;
}
cont->data = malloc(size * sizeof(int));
if (cont->data == NULL) {
free(cont); /* Important: Free container */
return NULL;
}
cont->size = size;
return cont;
}
void free_container(Container *cont) {
if (cont != NULL) {
free(cont->data); /* Free nested allocation first */
free(cont); /* Then free container */
}
}Common Leak Patterns
Recognizing common leak patterns helps you spot and prevent them. Loops, data structures, error paths, and complex control flow are leak-prone. Understanding these patterns improves code review and debugging.
/* Pattern 1: Loop allocation without free */
void loop_leak(void) {
for (int i = 0; i < 100; i++) {
int *temp = malloc(100 * sizeof(int));
/* Use temp */
/* LEAK: Never freed */
}
}
/* Fix */
void loop_no_leak(void) {
for (int i = 0; i < 100; i++) {
int *temp = malloc(100 * sizeof(int));
if (temp == NULL) {
continue;
}
/* Use temp */
free(temp); /* Free inside loop */
}
}
/* Pattern 2: Linked list creation leak */
typedef struct Node {
int data;
struct Node *next;
} Node;
Node* create_list_leaky(int count) {
Node *head = NULL;
for (int i = 0; i < count; i++) {
Node *node = malloc(sizeof(Node));
if (node == NULL) {
return NULL; /* LEAK: Previous nodes not freed */
}
node->data = i;
node->next = head;
head = node;
}
return head;
}
/* Fix: Clean up on failure */
Node* create_list_safe(int count) {
Node *head = NULL;
for (int i = 0; i < count; i++) {
Node *node = malloc(sizeof(Node));
if (node == NULL) {
/* Free all nodes created so far */
while (head != NULL) {
Node *temp = head;
head = head->next;
free(temp);
}
return NULL;
}
node->data = i;
node->next = head;
head = node;
}
return head;
}
/* Pattern 3: Reallocation leak */
void realloc_leak(void) {
int *arr = malloc(10 * sizeof(int));
if (arr == NULL) {
return;
}
/* WRONG: Loses original on failure */
arr = realloc(arr, 100 * sizeof(int));
if (arr == NULL) {
return; /* LEAK: Original lost */
}
free(arr);
}
/* Fix */
void realloc_no_leak(void) {
int *arr = malloc(10 * sizeof(int));
if (arr == NULL) {
return;
}
/* RIGHT: Use temporary */
int *temp = realloc(arr, 100 * sizeof(int));
if (temp == NULL) {
free(arr); /* Free original */
return;
}
arr = temp;
free(arr);
}
/* Pattern 4: String duplication leak */
char* duplicate_string_leaky(const char *str) {
char *copy = malloc(strlen(str) + 1);
if (copy == NULL) {
return NULL;
}
strcpy(copy, str);
return copy; /* Caller must free */
}
void use_string_leaky(void) {
char *str = duplicate_string_leaky("Hello");
printf("%s\n", str);
/* LEAK: Never freed */
}
/* Fix */
void use_string_no_leak(void) {
char *str = duplicate_string_leaky("Hello");
if (str != NULL) {
printf("%s\n", str);
free(str); /* Free after use */
}
}
/* Pattern 5: Complex data structure leak */
typedef struct {
char *name;
int *values;
size_t count;
} Record;
Record* create_record_leaky(const char *name, size_t count) {
Record *rec = malloc(sizeof(Record));
if (rec == NULL) {
return NULL;
}
rec->name = malloc(strlen(name) + 1);
if (rec->name == NULL) {
free(rec); /* Good: Freed rec */
return NULL;
}
strcpy(rec->name, name);
rec->values = malloc(count * sizeof(int));
if (rec->values == NULL) {
/* LEAK: Forgot to free rec->name */
free(rec);
return NULL;
}
rec->count = count;
return rec;
}
/* Fix */
Record* create_record_safe(const char *name, size_t count) {
Record *rec = malloc(sizeof(Record));
if (rec == NULL) {
return NULL;
}
rec->name = malloc(strlen(name) + 1);
if (rec->name == NULL) {
free(rec);
return NULL;
}
strcpy(rec->name, name);
rec->values = malloc(count * sizeof(int));
if (rec->values == NULL) {
free(rec->name); /* Free name */
free(rec); /* Free rec */
return NULL;
}
rec->count = count;
return rec;
}
void free_record(Record *rec) {
if (rec != NULL) {
free(rec->name); /* Free nested allocations */
free(rec->values);
free(rec); /* Free structure */
}
}Manual Leak Tracking
Before using sophisticated tools, understand manual tracking techniques. These help during development and in environments where tools aren't available. Track allocations, balance malloc/free calls, and use sentinel values to detect use-after-free.
/* Simple allocation counter */
static size_t allocation_count = 0;
static size_t deallocation_count = 0;
void* tracked_malloc(size_t size) {
void *ptr = malloc(size);
if (ptr != NULL) {
allocation_count++;
printf("Alloc %p (%zu total)\n", ptr, allocation_count);
}
return ptr;
}
void tracked_free(void *ptr) {
if (ptr != NULL) {
deallocation_count++;
printf("Free %p (%zu total)\n", ptr, deallocation_count);
}
free(ptr);
}
void check_leaks(void) {
if (allocation_count != deallocation_count) {
printf("LEAK: %zu allocations, %zu frees\n",
allocation_count, deallocation_count);
} else {
printf("No leaks detected\n");
}
}
/* Enhanced tracking with metadata */
typedef struct AllocationInfo {
void *ptr;
size_t size;
const char *file;
int line;
struct AllocationInfo *next;
} AllocationInfo;
static AllocationInfo *allocation_list = NULL;
#define TRACKED_MALLOC(size) \
tracked_malloc_debug(size, __FILE__, __LINE__)
void* tracked_malloc_debug(size_t size, const char *file, int line) {
void *ptr = malloc(size);
if (ptr == NULL) {
return NULL;
}
/* Record allocation */
AllocationInfo *info = malloc(sizeof(AllocationInfo));
if (info != NULL) {
info->ptr = ptr;
info->size = size;
info->file = file;
info->line = line;
info->next = allocation_list;
allocation_list = info;
}
return ptr;
}
#define TRACKED_FREE(ptr) \
tracked_free_debug(ptr, __FILE__, __LINE__)
void tracked_free_debug(void *ptr, const char *file, int line) {
if (ptr == NULL) {
return;
}
/* Find and remove from list */
AllocationInfo **current = &allocation_list;
while (*current != NULL) {
if ((*current)->ptr == ptr) {
AllocationInfo *to_free = *current;
*current = (*current)->next;
free(to_free);
free(ptr);
return;
}
current = &(*current)->next;
}
/* Not found */
printf("WARNING: Free of untracked pointer %p at %s:%d\n",
ptr, file, line);
free(ptr);
}
void report_leaks(void) {
if (allocation_list == NULL) {
printf("No leaks detected\n");
return;
}
printf("Memory leaks detected:\n");
AllocationInfo *current = allocation_list;
size_t total_leaked = 0;
while (current != NULL) {
printf(" %zu bytes at %s:%d\n",
current->size, current->file, current->line);
total_leaked += current->size;
current = current->next;
}
printf("Total leaked: %zu bytes\n", total_leaked);
}
/* Usage example */
void test_tracking(void) {
int *a = TRACKED_MALLOC(sizeof(int));
int *b = TRACKED_MALLOC(sizeof(int) * 10);
TRACKED_FREE(a);
/* Forgot to free b */
report_leaks(); /* Reports b as leaked */
}
/* Sentinel value for freed memory */
#define FREED_MEMORY_PATTERN 0xDEADBEEF
void safe_free(void **ptr) {
if (ptr != NULL && *ptr != NULL) {
/* Overwrite with pattern (helps catch use-after-free) */
memset(*ptr, 0xDD, 16); /* First 16 bytes */
free(*ptr);
*ptr = NULL;
}
}
/* Usage */
void use_safe_free(void) {
int *ptr = malloc(sizeof(int));
if (ptr == NULL) {
return;
}
*ptr = 42;
safe_free((void**)&ptr);
/* ptr now NULL, can't accidentally use */
if (ptr != NULL) {
*ptr = 100; /* Won't execute */
}
}Using Valgrind
Valgrind is the industry-standard tool for finding memory leaks and errors on Linux. It detects leaks, invalid accesses, use-after-free, double frees, and more. Understanding Valgrind output is essential for professional C development.
/* Example program with leaks */
/* leak_example.c */
#include <stdlib.h>
#include <string.h>
void leak1(void) {
int *ptr = malloc(100 * sizeof(int));
/* Never freed */
}
void leak2(void) {
char *str = malloc(50);
if (str != NULL) {
strcpy(str, "test");
}
/* Never freed */
}
void use_after_free(void) {
int *ptr = malloc(sizeof(int));
if (ptr != NULL) {
*ptr = 42;
free(ptr);
*ptr = 100; /* Use after free */
}
}
void double_free_bug(void) {
int *ptr = malloc(sizeof(int));
if (ptr != NULL) {
free(ptr);
free(ptr); /* Double free */
}
}
int main(void) {
leak1();
leak2();
/* use_after_free(); */
/* double_free_bug(); */
return 0;
}
/* Compile with debug info */
/* gcc -g -o leak_example leak_example.c */
/* Run with Valgrind */
/* valgrind --leak-check=full --show-leak-kinds=all ./leak_example */
/* Valgrind output interpretation:
==12345== HEAP SUMMARY:
==12345== in use at exit: 450 bytes in 2 blocks
==12345== total heap usage: 2 allocs, 0 frees, 450 bytes allocated
This shows 2 leaks: 400 bytes (leak1) + 50 bytes (leak2)
==12345== 400 bytes in 1 blocks are definitely lost in loss record 1 of 2
==12345== at 0x4C2AB80: malloc (in /usr/lib/valgrind/...)
==12345== by 0x40052E: leak1 (leak_example.c:5)
==12345== by 0x4005B4: main (leak_example.c:28)
This shows leak1() allocates 400 bytes that are never freed
Leak categories:
- definitely lost: Memory leaked (can't be reached)
- indirectly lost: Leaked via lost pointers
- possibly lost: Pointers to middle of blocks
- still reachable: Not freed but still accessible
- suppressed: Ignored by suppressions
*/
/* Fix leaks */
void no_leak1(void) {
int *ptr = malloc(100 * sizeof(int));
if (ptr != NULL) {
/* Use ptr */
free(ptr); /* Fixed */
}
}
void no_leak2(void) {
char *str = malloc(50);
if (str != NULL) {
strcpy(str, "test");
free(str); /* Fixed */
}
}
/* Valgrind will now report:
==12345== HEAP SUMMARY:
==12345== in use at exit: 0 bytes in 0 blocks
==12345== total heap usage: 2 allocs, 2 frees, 450 bytes allocated
==12345==
==12345== All heap blocks were freed -- no leaks are possible
*/
/* Common Valgrind flags:
--leak-check=full - Detailed leak info
--show-leak-kinds=all - Show all leak types
--track-origins=yes - Track uninitialized values
--verbose - More detailed output
--log-file=valgrind.log - Save to file
--suppressions=file.supp - Suppress known issues
*/
/* Suppression file example (valgrind.supp) */
/*
{
<insert_name_here>
Memcheck:Leak
fun:malloc
fun:known_leak_in_library
}
*/
/* Run with suppressions:
valgrind --suppressions=valgrind.supp --leak-check=full ./program
*/AddressSanitizer and Other Tools
AddressSanitizer (ASan) is a fast memory error detector built into GCC and Clang. It catches buffer overflows, use-after-free, double free, and leaks. Faster than Valgrind but requires recompilation. Other tools include Dr. Memory, Electric Fence, and custom solutions.
/* AddressSanitizer (ASan) usage */
/* Compile with ASan */
/* gcc -fsanitize=address -g -o program program.c */
/* Or with Clang */
/* clang -fsanitize=address -g -o program program.c */
/* Example program */
/* asan_example.c */
#include <stdlib.h>
#include <string.h>
void heap_overflow(void) {
int *arr = malloc(10 * sizeof(int));
arr[10] = 42; /* Out of bounds */
free(arr);
}
void heap_use_after_free(void) {
int *ptr = malloc(sizeof(int));
free(ptr);
*ptr = 42; /* Use after free */
}
void stack_overflow(void) {
int arr[10];
arr[10] = 42; /* Out of bounds */
}
int main(void) {
/* heap_overflow(); */
/* heap_use_after_free(); */
/* stack_overflow(); */
return 0;
}
/* ASan output for heap overflow:
=================================================================
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000028
WRITE of size 4 at 0x602000000028 thread T0
#0 0x400752 in heap_overflow asan_example.c:7
#1 0x4007a4 in main asan_example.c:24
Clearly identifies:
- Type of error (heap-buffer-overflow)
- Location (line 7 in heap_overflow)
- Call stack (how we got there)
*/
/* ASan advantages:
- Faster than Valgrind (2x slowdown vs 20x)
- Catches more error types
- Better error messages
- Detects leaks (with ASAN_OPTIONS)
*/
/* Run with leak detection */
/* ASAN_OPTIONS=detect_leaks=1 ./program */
/* ASan environment variables:
ASAN_OPTIONS=detect_leaks=1 - Enable leak detection
ASAN_OPTIONS=halt_on_error=0 - Continue after error
ASAN_OPTIONS=log_path=asan.log - Log to file
ASAN_OPTIONS=symbolize=1 - Symbolize stack traces
*/
/* Dr. Memory (Windows and Linux) */
/* Similar to Valgrind but works on Windows */
/* drmemory -- ./program */
/* Electric Fence (simple tool) */
/* Catches buffer overruns using VM protection */
/* gcc -o program program.c -lefence */
/* LeakSanitizer (part of ASan) */
/* gcc -fsanitize=leak -o program program.c */
/* Detects leaks only, faster than full ASan */
/* MemorySanitizer (MSan) */
/* gcc -fsanitize=memory -o program program.c */
/* Detects uninitialized memory reads */
/* UndefinedBehaviorSanitizer (UBSan) */
/* gcc -fsanitize=undefined -o program program.c */
/* Detects undefined behavior */
/* Combining sanitizers */
/* gcc -fsanitize=address,undefined -o program program.c */
/* Static analysis tools */
/* - Clang Static Analyzer */
/* clang --analyze program.c */
/* - Cppcheck */
/* cppcheck --enable=all program.c */
/* - Coverity (commercial) */
/* - PVS-Studio (commercial) */
/* Best practice: Use multiple tools */
/*
1. Development: ASan (fast, catches most issues)
2. Testing: Valgrind (thorough, slower)
3. CI/CD: Static analyzers (no runtime cost)
4. Production: Minimal instrumentation
*/
/* Tool comparison */
/*
Tool Speed Coverage Platform Cost
---------------------------------------------------
ASan Fast Good Linux/Mac Free
Valgrind Slow Excellent Linux Free
Dr. Memory Slow Good Win/Linux Free
LeakSan Fast Leaks only Linux/Mac Free
MSan Medium Uninit Linux Free
UBSan Fast UB only Linux/Mac Free
Coverity N/A Excellent All $$$
PVS-Studio N/A Excellent All $$$
*/Leak Prevention Best Practices
Preventing leaks is better than detecting them. Follow ownership rules, use RAII-like patterns, document who frees what, prefer stack when possible, and establish team conventions. Good practices eliminate most leaks during development.
/* Practice 1: Clear ownership */
/* Caller owns and must free */
char* create_string(const char *input) {
char *result = malloc(strlen(input) + 1);
if (result != NULL) {
strcpy(result, input);
}
return result; /* Documented: Caller must free */
}
/* Callee owns and frees */
void process_data(const int *data, size_t count) {
/* Function doesn't own data, won't free */
for (size_t i = 0; i < count; i++) {
/* Process data[i] */
}
}
/* Practice 2: Constructor/Destructor pattern */
typedef struct {
int *data;
size_t size;
} Array;
Array* array_create(size_t size) {
Array *arr = malloc(sizeof(Array));
if (arr == NULL) {
return NULL;
}
arr->data = malloc(size * sizeof(int));
if (arr->data == NULL) {
free(arr);
return NULL;
}
arr->size = size;
return arr;
}
void array_destroy(Array *arr) {
if (arr != NULL) {
free(arr->data);
free(arr);
}
}
/* Practice 3: RAII-like cleanup */
#define CLEANUP_FUNC(func) __attribute__((cleanup(func)))
void free_wrapper(void *ptr) {
free(*(void**)ptr);
}
void auto_cleanup_example(void) {
int *ptr CLEANUP_FUNC(free_wrapper) = malloc(sizeof(int));
if (ptr == NULL) {
return;
}
*ptr = 42;
/* ptr freed automatically when going out of scope */
}
/* Practice 4: Cleanup functions */
int process_file(const char *filename) {
FILE *file = NULL;
char *buffer = NULL;
int result = -1;
file = fopen(filename, "r");
if (file == NULL) {
goto cleanup;
}
buffer = malloc(1024);
if (buffer == NULL) {
goto cleanup;
}
/* Process file... */
result = 0;
cleanup:
free(buffer);
if (file != NULL) {
fclose(file);
}
return result;
}
/* Practice 5: Reference counting */
typedef struct {
int *data;
size_t size;
int ref_count;
} RefCounted;
RefCounted* ref_create(size_t size) {
RefCounted *obj = malloc(sizeof(RefCounted));
if (obj == NULL) {
return NULL;
}
obj->data = malloc(size * sizeof(int));
if (obj->data == NULL) {
free(obj);
return NULL;
}
obj->size = size;
obj->ref_count = 1;
return obj;
}
void ref_retain(RefCounted *obj) {
if (obj != NULL) {
obj->ref_count++;
}
}
void ref_release(RefCounted *obj) {
if (obj != NULL) {
obj->ref_count--;
if (obj->ref_count == 0) {
free(obj->data);
free(obj);
}
}
}
/* Practice 6: Memory pools */
typedef struct {
void *memory;
size_t size;
size_t used;
} MemoryPool;
MemoryPool* pool_create(size_t size) {
MemoryPool *pool = malloc(sizeof(MemoryPool));
if (pool == NULL) {
return NULL;
}
pool->memory = malloc(size);
if (pool->memory == NULL) {
free(pool);
return NULL;
}
pool->size = size;
pool->used = 0;
return pool;
}
void* pool_alloc(MemoryPool *pool, size_t size) {
if (pool->used + size > pool->size) {
return NULL;
}
void *ptr = (char*)pool->memory + pool->used;
pool->used += size;
return ptr;
}
void pool_destroy(MemoryPool *pool) {
if (pool != NULL) {
free(pool->memory); /* Frees everything at once */
free(pool);
}
}
/* Practice 7: Document ownership */
/**
* Creates a copy of the string.
* @param input Source string
* @return Allocated copy (caller must free) or NULL on error
*/
char* string_copy(const char *input) {
char *copy = malloc(strlen(input) + 1);
if (copy != NULL) {
strcpy(copy, input);
}
return copy;
}
/**
* Processes data array.
* @param data Array to process (not modified, not freed)
* @param count Number of elements
*/
void process_array(const int *data, size_t count) {
/* Function doesn't own data */
}Summary & What's Next
Key Takeaways:
- ✅ Memory leaks occur when malloc() lacks matching free()
- ✅ Leaks accumulate over time causing slowdowns/crashes
- ✅ Common patterns: loops, early returns, lost pointers
- ✅ Manual tracking helps but tools are more reliable
- ✅ Valgrind finds leaks and memory errors (Linux)
- ✅ AddressSanitizer is faster alternative (needs recompile)
- ✅ Clear ownership prevents most leaks
- ✅ Use create/destroy pairs for consistency