C Programming: Low-Level Mastery
HomeInsightsCoursesC ProgrammingMemory Leaks & Best Practices
Dynamic Memory

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.

C
#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-&gt;data = malloc(size * sizeof(int));
    if (cont-&gt;data == NULL) {
        free(cont);  /* Important: Free container */
        return NULL;
    }
    
    cont-&gt;size = size;
    return cont;
}

void free_container(Container *cont) {
    if (cont != NULL) {
        free(cont-&gt;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.

C
/* 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-&gt;data = i;
        node-&gt;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-&gt;next;
                free(temp);
            }
            return NULL;
        }
        
        node-&gt;data = i;
        node-&gt;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-&gt;name = malloc(strlen(name) + 1);
    if (rec-&gt;name == NULL) {
        free(rec);  /* Good: Freed rec */
        return NULL;
    }
    strcpy(rec-&gt;name, name);
    
    rec-&gt;values = malloc(count * sizeof(int));
    if (rec-&gt;values == NULL) {
        /* LEAK: Forgot to free rec-&gt;name */
        free(rec);
        return NULL;
    }
    
    rec-&gt;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-&gt;name = malloc(strlen(name) + 1);
    if (rec-&gt;name == NULL) {
        free(rec);
        return NULL;
    }
    strcpy(rec-&gt;name, name);
    
    rec-&gt;values = malloc(count * sizeof(int));
    if (rec-&gt;values == NULL) {
        free(rec-&gt;name);  /* Free name */
        free(rec);        /* Free rec */
        return NULL;
    }
    
    rec-&gt;count = count;
    return rec;
}

void free_record(Record *rec) {
    if (rec != NULL) {
        free(rec-&gt;name);    /* Free nested allocations */
        free(rec-&gt;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.

C
/* 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-&gt;ptr = ptr;
        info-&gt;size = size;
        info-&gt;file = file;
        info-&gt;line = line;
        info-&gt;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)-&gt;ptr == ptr) {
            AllocationInfo *to_free = *current;
            *current = (*current)-&gt;next;
            free(to_free);
            free(ptr);
            return;
        }
        current = &(*current)-&gt;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-&gt;size, current-&gt;file, current-&gt;line);
        total_leaked += current-&gt;size;
        current = current-&gt;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.

C
/* 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.

C
/* 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.

C
/* 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-&gt;data = malloc(size * sizeof(int));
    if (arr-&gt;data == NULL) {
        free(arr);
        return NULL;
    }
    
    arr-&gt;size = size;
    return arr;
}

void array_destroy(Array *arr) {
    if (arr != NULL) {
        free(arr-&gt;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-&gt;data = malloc(size * sizeof(int));
    if (obj-&gt;data == NULL) {
        free(obj);
        return NULL;
    }
    
    obj-&gt;size = size;
    obj-&gt;ref_count = 1;
    return obj;
}

void ref_retain(RefCounted *obj) {
    if (obj != NULL) {
        obj-&gt;ref_count++;
    }
}

void ref_release(RefCounted *obj) {
    if (obj != NULL) {
        obj-&gt;ref_count--;
        if (obj-&gt;ref_count == 0) {
            free(obj-&gt;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-&gt;memory = malloc(size);
    if (pool-&gt;memory == NULL) {
        free(pool);
        return NULL;
    }
    
    pool-&gt;size = size;
    pool-&gt;used = 0;
    return pool;
}

void* pool_alloc(MemoryPool *pool, size_t size) {
    if (pool-&gt;used + size &gt; pool-&gt;size) {
        return NULL;
    }
    
    void *ptr = (char*)pool-&gt;memory + pool-&gt;used;
    pool-&gt;used += size;
    return ptr;
}

void pool_destroy(MemoryPool *pool) {
    if (pool != NULL) {
        free(pool-&gt;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

What's Next?

Let's learn about file I/O operations for reading and writing files!