Code of the Day
IntermediateMemory management

Memory leaks and how to find them

Understand what memory leaks are, how they accumulate in long-running programs, and how to detect and fix them with valgrind and AddressSanitizer.

CIntermediate10 min read
By the end of this lesson you will be able to:
  • Explain what a memory leak is and how it degrades program performance
  • Identify common patterns that cause leaks
  • Use valgrind to detect leaks in a program
  • Apply the "single owner" discipline to prevent leaks

A memory leak occurs when heap memory is allocated but never freed, and no pointer to it survives. The memory cannot be reclaimed by the allocator — the program's memory usage grows steadily until the OS kills it or it exhausts available memory. In long-running servers and daemons, even a small leak per request can accumulate to gigabytes over days.

What causes a leak

Losing the only pointer:

void leaky(void) {
    int *p = malloc(100 * sizeof(int));
    /* do some work... */
    return; /* p goes out of scope; memory is leaked */
}

When leaky returns, p is destroyed. No other variable holds the address of that memory. It can never be freed.

Early return without freeing:

int process(void) {
    char *buf = malloc(1024);
    if (!buf) { return -1; }

    if (some_error_condition()) {
        return -2; /* LEAK: buf was not freed */
    }

    /* ... use buf ... */
    free(buf);
    return 0;
}

Every exit path from a function must free all allocations. The common fix: use a goto cleanup pattern (legitimate C idiom for error handling) or restructure to have a single return at the end.

int process(void) {
    int result = 0;
    char *buf = malloc(1024);
    if (!buf) { return -1; }

    if (some_error_condition()) {
        result = -2;
        goto cleanup;
    }

    /* ... use buf ... */

cleanup:
    free(buf);
    return result;
}

Overwriting a pointer before freeing:

int *p = malloc(sizeof(int));
p = malloc(sizeof(int)); /* LEAK: first allocation is lost */
free(p);                 /* frees only the second */

Detecting leaks with valgrind

Valgrind's memcheck tool tracks every allocation and reports any that are not freed when the program exits.

Install on Linux: sudo apt install valgrind

Compile with debug info: gcc -g -O0 leaky.c -o leaky

Run: valgrind --leak-check=full ./leaky

Output from a program that leaks 400 bytes:

LEAK SUMMARY:
   definitely lost: 400 bytes in 1 blocks
   indirectly lost: 0 bytes in 0 blocks
     possibly lost: 0 bytes in 0 blocks
   still reachable: 0 bytes in 0 blocks
        suppressed: 0 bytes in 0 blocks

"Definitely lost" means the allocator cannot reach the memory at all. "Still reachable" means there is a valid pointer to it at exit — not a leak per se, but a sign that cleanup code is missing.

AddressSanitizer — faster leak detection

Compile with: gcc -g -fsanitize=address,leak leaky.c -o leaky

Run normally: ./leaky

ASan prints a detailed report at program exit, including a stack trace of where the leaked memory was allocated. It is faster than valgrind (roughly 2× slower than normal execution vs valgrind's 10–50× overhead) and suitable for CI pipelines.

The "single owner" discipline

Each piece of heap memory should have exactly one owner — one variable or data structure that is responsible for freeing it. When ownership transfers (e.g., you return a malloc'd pointer from a function), document it:

/* Returns a heap-allocated string; caller is responsible for calling free(). */
char *build_greeting(const char *name) {
    char *buf = malloc(strlen(name) + 8);
    if (!buf) { return NULL; }
    sprintf(buf, "Hello, %s!", name);
    return buf;
}

int main(void) {
    char *g = build_greeting("Alice");
    if (g) {
        printf("%s\n", g);
        free(g); /* caller frees -- as documented */
    }
    return 0;
}

Comment the ownership contract. If you are unsure who owns memory, that is a sign to redesign the interface.

Modern C++ and Rust make this automatic. C++'s unique_ptr and shared_ptr free memory when the pointer goes out of scope. Rust's ownership system enforces the single-owner rule at compile time. Understanding the manual version in C makes those mechanisms much easier to appreciate.

Where to go next

Next: buffer overflows — the other major memory corruption bug, where you write past the end of a buffer and corrupt adjacent memory.

Finished reading? Mark it complete to track your progress.

On this page