CDetecting Leaks with Valgrind

Detecting Leaks with Valgrind

Compiler warnings and sanitizers catch a lot of mistakes at compile time or while a specific code path is running, but some bugs only show up when you watch a program's actual memory behavior over its full lifetime. Valgrind is a dynamic analysis tool suite built exactly for that job. Instead of reading your source code, it runs your compiled program inside a synthetic CPU it controls, watching every memory access as it happens.

Valgrind ships several tools, but the one C programmers reach for constantly is Memcheck — it is the default tool Valgrind runs if you don't ask for another one. Memcheck tracks every byte your program allocates, reads, and writes, and flags the moment something looks wrong.
Running a Program Under Valgrind

Compile normally, ideally with debug symbols so Valgrind can show you source line numbers, then prefix the run with valgrind.

Bash
gcc -g -Wall -Wextra -o myprogram myprogram.c
valgrind ./myprogram

The -g flag matters: without debug symbols, Valgrind can still detect problems, but its report will show raw addresses instead of the file and line number where the bad access happened.

What Memcheck Catches
  • Memory leaks — memory allocated with malloc/calloc/realloc that is never freed before the program exits.

  • Use-after-free — reading or writing memory through a pointer after it has already been passed to free.

  • Reading uninitialized memory — using a value before it has ever been written, which can silently produce different results on different runs.

  • Invalid reads/writes — buffer overflows and underflows, such as writing one element past the end of an array.

  • Double frees — calling free twice on the same pointer.

  • Mismatched allocation/deallocation — e.g. allocating with new and freeing with free in mixed C/C++ code.

A Worked Example

Here is a small program with a deliberate, easy-to-miss leak: it allocates a buffer inside a function and returns without freeing it.

C
#include <stdlib.h>

void build_report(void) {
    int *scores = malloc(10 * sizeof(int)); /* never freed */
    scores[0] = 100;
}

int main(void) {
    build_report();
    return 0;
}

Running it under Valgrind produces a report like this:

Bash
==12345== HEAP SUMMARY:
==12345==     in use at exit: 40 bytes in 1 blocks
==12345==   total heap usage: 1 allocs, 0 frees, 40 bytes allocated
==12345==
==12345== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345==    at 0x4C2FB0F: malloc (vg_replace_malloc.c:299)
==12345==    by 0x1091A9: build_report (leak.c:4)
==12345==    by 0x1091C7: main (leak.c:9)
==12345==
==12345== LEAK SUMMARY:
==12345==    definitely lost: 40 bytes in 1 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

The report gives you the exact allocation site (leak.c:4) and the call stack that reached it, so tracking down the fix — adding a free(scores); before returning — is usually fast. "Definitely lost" means Valgrind found no remaining pointer anywhere in the program that could still reach that block; it is a real leak.

Common Valgrind Flags

Flag

Purpose

--leak-check=full

Shows details for every individual leaked block instead of just a summary count.

--track-origins=yes

Traces uninitialized values back to where they were created (slower, but very helpful).

--show-leak-kinds=all

Reports all leak categories (definite, indirect, possible, still reachable).

--error-exitcode=1

Makes Valgrind return a non-zero exit code on errors, useful for CI pipelines.

Tip
Run Valgrind early and often, not just when you suspect a bug. Many teams run it as part of their test suite so leaks are caught the moment they are introduced, rather than months later.
Warning
Valgrind is primarily a Linux and macOS tool. It is not natively available on Windows. Windows users typically run it inside WSL (Windows Subsystem for Linux), which gives a real Linux environment to compile and run under Valgrind normally.
Note
Valgrind instruments every memory access, which commonly slows a program down 10-50x or more. This makes it a development and testing tool, not something you run in production. For production monitoring of memory issues, teams typically rely on lighter-weight sanitizers (like AddressSanitizer, built into the compiler) that add much less overhead.