Finding the Invisible: A Complete Guide to Memory Leak Detection in C

Memory leaks are among the most insidious bugs in C programming. They don't crash your program immediately—instead, they silently consume resources until your application grinds to a halt or the system runs out of memory. Detecting and fixing memory leaks requires discipline, tools, and a deep understanding of how dynamic memory works in C. This guide covers everything from manual techniques to advanced automated tools for memory leak detection.

What Is a Memory Leak?

A memory leak occurs when a program allocates memory but never frees it, losing all references to that memory. The memory remains allocated until the program terminates, wasting system resources.

void leak_example() {
int *ptr = malloc(sizeof(int) * 100);
// Do something with ptr...
// Forgot to free(ptr) - memory leak!
} // ptr goes out of scope, memory lost forever

Basic Causes of Memory Leaks

1. Forgetting to Free

#include <stdlib.h>
void simple_leak() {
int *data = malloc(1000 * sizeof(int));
// Use data...
// Missing free(data)
} // 4000 bytes leaked (on 32-bit int)

2. Losing the Pointer

void lost_pointer_leak() {
int *ptr = malloc(sizeof(int));
ptr = malloc(sizeof(int));  // First allocation lost!
free(ptr);  // Only frees the second allocation
}

3. Not Freeing All Members of Structures

typedef struct {
int *data;
int size;
} Array;
Array* create_array(int size) {
Array *arr = malloc(sizeof(Array));
arr->data = malloc(size * sizeof(int));
arr->size = size;
return arr;
}
void bad_free(Array *arr) {
free(arr);  // Frees the structure but not arr->data!
}
void good_free(Array *arr) {
free(arr->data);  // Free internal data first
free(arr);        // Then free the structure
}

4. Leaks in Error Paths

int risky_function() {
int *data = malloc(1000 * sizeof(int));
FILE *file = fopen("data.txt", "r");
if (!file) {
// Error: forgot to free data before returning
return -1;  // Leak!
}
// ... use file and data ...
fclose(file);
free(data);
return 0;
}

Manual Detection Techniques

1. Code Review Checklist

// Look for these patterns:
malloc() / calloc() - needs matching free()
strdup() - needs free()
fopen() - needs fclose()
*_alloc functions - need corresponding *_free functions
// Every allocation should have a clear ownership and cleanup path

2. Tracking Allocations Manually

#include <stdio.h>
#include <stdlib.h>
#define MAX_ALLOCS 10000
typedef struct {
void *ptr;
size_t size;
const char *file;
int line;
int freed;
} AllocationRecord;
AllocationRecord records[MAX_ALLOCS];
int alloc_count = 0;
void* tracked_malloc(size_t size, const char *file, int line) {
void *ptr = malloc(size);
if (ptr && alloc_count < MAX_ALLOCS) {
records[alloc_count].ptr = ptr;
records[alloc_count].size = size;
records[alloc_count].file = file;
records[alloc_count].line = line;
records[alloc_count].freed = 0;
alloc_count++;
}
return ptr;
}
void tracked_free(void *ptr, const char *file, int line) {
for (int i = 0; i < alloc_count; i++) {
if (records[i].ptr == ptr && !records[i].freed) {
records[i].freed = 1;
free(ptr);
return;
}
}
fprintf(stderr, "Warning: Attempt to free unknown pointer at %s:%d\n", 
file, line);
}
void print_leaks() {
int leaks = 0;
size_t total_leaked = 0;
printf("\n=== Memory Leak Report ===\n");
for (int i = 0; i < alloc_count; i++) {
if (!records[i].freed) {
printf("Leak: %zu bytes at %p (allocated at %s:%d)\n",
records[i].size, records[i].ptr,
records[i].file, records[i].line);
leaks++;
total_leaked += records[i].size;
}
}
if (leaks == 0) {
printf("No memory leaks detected!\n");
} else {
printf("Total: %d leaks, %zu bytes\n", leaks, total_leaked);
}
}
#define MALLOC(size) tracked_malloc(size, __FILE__, __LINE__)
#define FREE(ptr) tracked_free(ptr, __FILE__, __LINE__)
int main() {
int *p1 = MALLOC(100 * sizeof(int));
int *p2 = MALLOC(200 * sizeof(int));
int *p3 = MALLOC(300 * sizeof(int));
FREE(p1);
FREE(p3);
// Forgot to free p2 - will show as leak
print_leaks();
return 0;
}

3. Reference Counting

#include <stdio.h>
#include <stdlib.h>
typedef struct {
int ref_count;
int data;
} RefCountedObject;
RefCountedObject* create_object(int value) {
RefCountedObject *obj = malloc(sizeof(RefCountedObject));
obj->ref_count = 1;
obj->data = value;
return obj;
}
void retain_object(RefCountedObject *obj) {
if (obj) {
obj->ref_count++;
}
}
void release_object(RefCountedObject *obj) {
if (obj) {
obj->ref_count--;
if (obj->ref_count == 0) {
printf("Freeing object with data %d\n", obj->data);
free(obj);
}
}
}
typedef struct {
RefCountedObject *obj;
// other fields
} Container;
int main() {
RefCountedObject *obj = create_object(42);
Container *c1 = malloc(sizeof(Container));
c1->obj = obj;
retain_object(obj);  // c1 holds reference
Container *c2 = malloc(sizeof(Container));
c2->obj = obj;
retain_object(obj);  // c2 holds reference
release_object(obj);  // obj still has 2 references
release_object(c1->obj);  // 1 reference left
release_object(c2->obj);  // 0 references - freed
free(c1);
free(c2);
return 0;
}

Using Valgrind

Valgrind is the most powerful tool for memory leak detection on Linux.

Basic Usage:

# Compile with debug symbols
gcc -g -o program program.c
# Run under Valgrind
valgrind --leak-check=full ./program
# More detailed output
valgrind --leak-check=full --show-leak-kinds=all --verbose ./program
# Generate suppression file
valgrind --gen-suppressions=all --leak-check=full ./program 2> suppressions.txt

Example Program with Leaks:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
char* create_string(const char *s) {
char *str = malloc(strlen(s) + 1);
strcpy(str, s);
return str;
}
void leak_function() {
int *data = malloc(1000 * sizeof(int));
// data not freed
}
int main() {
char *str = create_string("Hello");
printf("%s\n", str);
// str not freed
leak_function();
return 0;
}

Valgrind Output:

==12345== Memcheck, a memory error detector
==12345== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==12345== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==12345== Command: ./program
==12345== 
Hello
==12345== 
==12345== HEAP SUMMARY:
==12345==     in use at exit: 1,016 bytes in 2 blocks
==12345==   total heap usage: 3 allocs, 1 frees, 2,048 bytes allocated
==12345== 
==12345== 16 bytes in 1 blocks are definitely lost in loss record 1 of 2
==12345==    at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==12345==    by 0x109182: create_string (program.c:5)
==12345==    by 0x1091F2: main (program.c:14)
==12345== 
==12345== 1,000 bytes in 1 blocks are definitely lost in loss record 2 of 2
==12345==    at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==12345==    by 0x1091C0: leak_function (program.c:9)
==12345==    by 0x109207: main (program.c:17)
==12345== 
==12345== LEAK SUMMARY:
==12345==    definitely lost: 1,016 bytes in 2 blocks
==12345==    indirectly lost: 0 bytes in 0 blocks
==12345==      possibly lost: 0 bytes in 0 blocks
==12345==    still reachable: 0 bytes in 0 blocks
==12345==         suppressed: 0 bytes in 0 blocks
==12345== 
==12345== For lists of detected and suppressed errors, rerun with: -s
==12345== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)

AddressSanitizer (ASan)

A modern alternative to Valgrind, integrated with GCC and Clang:

#include <stdlib.h>
int main() {
int *leak = malloc(100 * sizeof(int));
// leak not freed
int *out_of_bounds = malloc(10 * sizeof(int));
out_of_bounds[10] = 42;  // Buffer overflow
return 0;
}

Compilation and Execution:

# Compile with ASan
gcc -fsanitize=address -g -o program program.c
# Run
./program

ASan Output:

=================================================================
==12345==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 400 byte(s) in 1 object(s) allocated from:
#0 0x7f8b3c0b3bc8 in malloc (/usr/lib/x86_64-linux-gnu/libasan.so.5+0x10dbc8)
#1 0x558a1f4b0160 in main /home/user/program.c:4
#2 0x7f8b3be56082 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x24082)
SUMMARY: AddressSanitizer: 400 byte(s) leaked in 1 allocation(s).

Electric Fence

A library that helps catch memory leaks and overruns:

#include <stdlib.h>
#include <stdio.h>
#include <efence.h>
int main() {
// Compile with -lefence
int *data = malloc(100 * sizeof(int));
// Use data...
// Forgot to free - Electric Fence will report
return 0;
}
gcc -g -o program program.c -lefence
./program

Custom Memory Pool with Leak Detection

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#define POOL_SIZE 1024 * 1024  // 1MB
#define ALIGNMENT 8
#define ALIGN_SIZE(size) (((size) + (ALIGNMENT - 1)) & ~(ALIGNMENT - 1))
typedef struct BlockHeader {
struct BlockHeader *next;
size_t size;
const char *file;
int line;
uint32_t magic;  // For corruption detection
} BlockHeader;
typedef struct {
char *memory;
size_t total_size;
size_t used;
BlockHeader *free_list;
int allocation_count;
size_t peak_used;
} MemoryPool;
MemoryPool* create_pool(size_t size) {
MemoryPool *pool = malloc(sizeof(MemoryPool));
pool->memory = malloc(size);
pool->total_size = size;
pool->used = 0;
pool->free_list = NULL;
pool->allocation_count = 0;
pool->peak_used = 0;
return pool;
}
void* pool_alloc_debug(MemoryPool *pool, size_t size, 
const char *file, int line) {
size = ALIGN_SIZE(size);
size_t total_needed = size + sizeof(BlockHeader);
if (pool->used + total_needed > pool->total_size) {
fprintf(stderr, "Pool out of memory at %s:%d\n", file, line);
return NULL;
}
BlockHeader *header = (BlockHeader*)(pool->memory + pool->used);
header->next = NULL;
header->size = size;
header->file = file;
header->line = line;
header->magic = 0xDEADBEEF;
pool->used += total_needed;
pool->allocation_count++;
if (pool->used > pool->peak_used) {
pool->peak_used = pool->used;
}
return (char*)header + sizeof(BlockHeader);
}
void pool_free(MemoryPool *pool, void *ptr) {
if (!ptr) return;
BlockHeader *header = (BlockHeader*)((char*)ptr - sizeof(BlockHeader));
if (header->magic != 0xDEADBEEF) {
fprintf(stderr, "Memory corruption detected: invalid magic\n");
return;
}
header->magic = 0;
pool->allocation_count--;
}
void pool_check_leaks(MemoryPool *pool) {
printf("\n=== Memory Pool Leak Report ===\n");
printf("Total pool size: %zu bytes\n", pool->total_size);
printf("Currently used: %zu bytes\n", pool->used);
printf("Peak usage: %zu bytes\n", pool->peak_used);
if (pool->allocation_count == 0 && pool->used == 0) {
printf("No memory leaks in pool!\n");
return;
}
printf("WARNING: %d active allocations\n", pool->allocation_count);
// Scan for unfreed allocations
char *ptr = pool->memory;
while (ptr < pool->memory + pool->used) {
BlockHeader *header = (BlockHeader*)ptr;
if (header->magic == 0xDEADBEEF) {
printf("Leak: %zu bytes at offset %ld (allocated at %s:%d)\n",
header->size, ptr - pool->memory,
header->file ? header->file : "unknown",
header->line);
}
ptr += sizeof(BlockHeader) + header->size;
}
}
void destroy_pool(MemoryPool *pool) {
pool_check_leaks(pool);
free(pool->memory);
free(pool);
}
#define POOL_ALLOC(pool, type) \
(type*)pool_alloc_debug(pool, sizeof(type), __FILE__, __LINE__)
#define POOL_ALLOC_ARRAY(pool, type, count) \
(type*)pool_alloc_debug(pool, sizeof(type) * (count), __FILE__, __LINE__)
int main() {
MemoryPool *pool = create_pool(10000);
// Allocate some memory
int *p1 = POOL_ALLOC(pool, int);
*p1 = 42;
double *p2 = POOL_ALLOC_ARRAY(pool, double, 100);
p2[0] = 3.14;
char *p3 = POOL_ALLOC_ARRAY(pool, char, 50);
strcpy(p3, "Hello, pool!");
printf("p1: %d\n", *p1);
printf("p2[0]: %f\n", p2[0]);
printf("p3: %s\n", p3);
// Free only some allocations
pool_free(pool, p1);
// p2 and p3 not freed - will show as leaks
destroy_pool(pool);
return 0;
}

Leak Detection in Long-Running Programs

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
// Global allocation tracking
typedef struct AllocNode {
void *ptr;
size_t size;
const char *file;
int line;
struct AllocNode *next;
} AllocNode;
AllocNode *alloc_list = NULL;
int total_allocs = 0;
size_t total_bytes = 0;
void* tracked_malloc_detailed(size_t size, const char *file, int line) {
void *ptr = malloc(size);
if (ptr) {
AllocNode *node = malloc(sizeof(AllocNode));
node->ptr = ptr;
node->size = size;
node->file = file;
node->line = line;
node->next = alloc_list;
alloc_list = node;
total_allocs++;
total_bytes += size;
}
return ptr;
}
void tracked_free_detailed(void *ptr) {
if (!ptr) return;
AllocNode **curr = &alloc_list;
while (*curr) {
if ((*curr)->ptr == ptr) {
AllocNode *to_free = *curr;
*curr = (*curr)->next;
total_bytes -= to_free->size;
total_allocs--;
free(to_free);
free(ptr);
return;
}
curr = &(*curr)->next;
}
fprintf(stderr, "Warning: freeing untracked pointer %p\n", ptr);
free(ptr);
}
void print_stats(int signum) {
printf("\n=== Memory Stats at signal %d ===\n", signum);
printf("Active allocations: %d\n", total_allocs);
printf("Total bytes: %zu\n", total_bytes);
if (total_allocs > 0) {
printf("\nActive allocations:\n");
AllocNode *curr = alloc_list;
while (curr) {
printf("  %zu bytes at %p (allocated at %s:%d)\n",
curr->size, curr->ptr,
curr->file ? curr->file : "unknown",
curr->line);
curr = curr->next;
}
}
}
void signal_handler(int signum) {
print_stats(signum);
exit(0);
}
#define MALLOC(size) tracked_malloc_detailed(size, __FILE__, __LINE__)
#define FREE(ptr) tracked_free_detailed(ptr)
int main() {
signal(SIGINT, signal_handler);  // Ctrl+C
signal(SIGTERM, signal_handler);
// Simulate a long-running program with leaks
for (int i = 0; i < 10; i++) {
int *p1 = MALLOC(100 * sizeof(int));
double *p2 = MALLOC(50 * sizeof(double));
// Use memory...
// Free only one
FREE(p1);
// p2 leaks
printf("Iteration %d done\n", i);
sleep(1);
}
print_stats(0);
return 0;
}

Static Analysis Tools

1. Using Clang Static Analyzer

// compile with: clang --analyze program.c
#include <stdlib.h>
void leak_example() {
int *x = malloc(sizeof(int) * 10);
// no free - analyzer will warn
}

2. Using CPPCheck

cppcheck --enable=all program.c

Example warnings:

[program.c:5]: (error) Memory leak: x
[program.c:10]: (error) Resource leak: fp

3. Using Splint

splint +checks program.c

Real-World Leak Detection Example

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Complex program with multiple potential leaks
typedef struct {
char *name;
int *scores;
int num_scores;
} Student;
typedef struct {
Student **students;
int count;
int capacity;
} Class;
Class* create_class(int initial_capacity) {
Class *c = malloc(sizeof(Class));
if (!c) return NULL;
c->students = malloc(initial_capacity * sizeof(Student*));
if (!c->students) {
free(c);
return NULL;
}
c->count = 0;
c->capacity = initial_capacity;
return c;
}
Student* create_student(const char *name, int num_scores) {
Student *s = malloc(sizeof(Student));
if (!s) return NULL;
s->name = malloc(strlen(name) + 1);
if (!s->name) {
free(s);
return NULL;
}
strcpy(s->name, name);
s->scores = malloc(num_scores * sizeof(int));
if (!s->scores) {
free(s->name);
free(s);
return NULL;
}
s->num_scores = num_scores;
return s;
}
void add_student(Class *c, Student *s) {
if (c->count >= c->capacity) {
c->capacity *= 2;
c->students = realloc(c->students, 
c->capacity * sizeof(Student*));
}
c->students[c->count++] = s;
}
// BAD: Missing cleanup functions
// void free_student(Student *s) { ... }
// void free_class(Class *c) { ... }
int main() {
Class *my_class = create_class(5);
Student *s1 = create_student("Alice", 5);
Student *s2 = create_student("Bob", 4);
Student *s3 = create_student("Charlie", 6);
add_student(my_class, s1);
add_student(my_class, s2);
add_student(my_class, s3);
printf("Class has %d students\n", my_class->count);
// OOPS! Forgot to free everything
// free_class(my_class);
return 0;  // Multiple leaks!
}

Proper cleanup version:

void free_student(Student *s) {
if (s) {
free(s->name);
free(s->scores);
free(s);
}
}
void free_class(Class *c) {
if (c) {
for (int i = 0; i < c->count; i++) {
free_student(c->students[i]);
}
free(c->students);
free(c);
}
}
// In main, add:
// free_class(my_class);

Leak Detection Checklist

  • [ ] Every malloc/calloc has a matching free
  • [ ] Error paths free allocated resources
  • [ ] Structures with nested allocations have proper destructors
  • [ ] Functions that allocate memory document ownership
  • [ ] Strings created with strdup are freed
  • [ ] Files opened with fopen are closed
  • [ ] Thread-local storage is cleaned up
  • [ ] Container structures free all elements
  • [ ] Reference counting is implemented correctly
  • [ ] Pool allocators track all allocations

Best Practices to Prevent Leaks

  1. Establish clear ownership rules - Document who frees what
  2. Use consistent allocation/deallocation pairs - If you write create_x, write destroy_x
  3. Initialize pointers to NULL - Makes freeing safer
  4. Free in reverse order of allocation - Nested structures: free innermost first
  5. Use static analysis tools - Integrate into CI pipeline
  6. Test with Valgrind/ASan - Regularly run under leak detectors
  7. Implement reference counting for shared objects
  8. Use memory pools for many small allocations
  9. Set freed pointers to NULL - Prevents double frees
  10. Write unit tests for error paths

Common Leak Patterns to Watch For

// Pattern 1: Reassigning pointer without freeing
void *ptr = malloc(100);
ptr = malloc(200);  // First allocation leaked!
// Pattern 2: Leaks in loops
for (int i = 0; i < 100; i++) {
char *str = malloc(100);
// Only free the last one!
if (i == 99) free(str);
}
// Pattern 3: Hidden allocations
char *str = strdup("hello");  // Need to free!
char *path = realpath("file", NULL);  // Need to free!
// Pattern 4: Early returns
void *ptr = malloc(100);
if (some_error) {
return;  // Forgot to free ptr!
}
free(ptr);
// Pattern 5: Leaks in data structures
struct Node *node = malloc(sizeof(struct Node));
node->data = malloc(100);
// Need to free node->data before freeing node

Conclusion

Memory leak detection is a critical skill for C programmers. Key takeaways:

  • Leaks are cumulative - small leaks in loops become big problems
  • Tools are essential - Valgrind, ASan, and static analyzers catch what humans miss
  • Prevention is better than detection - Clear ownership rules and consistent patterns
  • Test error paths - leaks often hide in error handling code
  • Document ownership - every allocation should have a clear owner
  • Regular monitoring - check long-running programs for growth

Mastering memory leak detection and prevention will make your C programs more reliable, efficient, and maintainable. It's not just about finding bugs—it's about developing habits that prevent them from occurring in the first place.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper