← Back to list

How do I use valgrind to find memory leaks?

Valgrind is a tool that runs your program in a special sandbox and tracks every heap allocation and memory access. It can then tell you:

Ajay Kumar · 2025-11-09 10:30 · 0 claps · 7.8 min read paywalled
#valgrind
Open on Medium ↗

How do I use valgrind to find memory leaks?

Valgrind is a tool that runs your program in a special sandbox and tracks every heap allocation and memory access. It can then tell you:

  • Which allocations were never freed (memory leaks)
  • When you read or write memory out of bounds
  • When you use uninitialized memory
  • When you use memory after free()

In other words: Valgrind is the X-ray machine for your program’s memory behavior.

2. Installing Valgrind

On most Unix-like systems, Valgrind is just a package away.

# Ubuntu, Debian, etc.
sudo apt install valgrind
# RHEL, CentOS, Fedora, etc.
sudo yum install valgrind
# Arch, Manjaro, Garuda, etc.
sudo pacman -Syu valgrind
# FreeBSD
sudo pkg ins valgrind
# illumos
sudo pkg install valgrind

Valgrind works great with C, C++, Rust, and Ada, and can be used with other languages too (often via wrappers).

3. Your First Valgrind Run

Let’s say you’ve compiled a program:

gcc -o executable main.c

To run it under Valgrind:

valgrind --leak-check=full \
         --show-leak-kinds=all \
         --track-origins=yes \
         --verbose \
         --log-file=valgrind-out.txt \
         ./executable exampleParam1

Let’s decode those options:

  • --leak-check=full Show detailed information about each leak (not just a summary).
  • --show-leak-kinds=all Report all leak types:
  • definite (real leaks)
  • indirect (pointed to only by leaked blocks)
  • possible (might be leaks, might be still reachable)
  • reachable (still reachable from global variables / pointers at exit)
  • --track-origins=yes Track where uninitialized values came from. This can massively improve error messages, at the cost of slowing down execution further.
  • --verbose Extra information about what Valgrind is doing. Add more --verbose if you like noise.
  • --log-file=valgrind-out.txt Send all output to a file instead of flooding your terminal. Very handy for big programs.

When everything is fine, you’re hoping to see something like:

HEAP SUMMARY:
    in use at exit: 0 bytes in 0 blocks
  total heap usage: 636 allocs, 636 frees, 25,393 bytes allocated
All heap blocks were freed -- no leaks are possible
ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

That’s the golden ticket: no leaks, no errors.

4. When Valgrind Says You Have a Leak

Now, let’s look at the more realistic case: you have a leak.

Consider this tiny program:

#include <stdlib.h>
int main(void) {
    char *string = malloc(5 * sizeof(char)); // LEAK: never freed
    return 0;
}

Run that under Valgrind, and you might see:

5 bytes in 1 blocks are definitely lost in loss record 1 of 1
   at 0x4C29BE3: malloc (vg_replace_malloc.c:299)
   by 0x40053E: main (in /home/user/executable)

So you know there’s a leak, but the backtrace isn’t very helpful: it only mentions main and malloc with no line number.

Why? Because we compiled without debug symbols.

5. Recompiling with Debug Symbols (-g, -ggdb3, -Og)

To let Valgrind point to exact lines in your source, recompile with debug information:

# Original
gcc -o executable -std=c11 -Wall main.c
# With rich debug info
gcc -o executable -std=c11 -Wall -ggdb3 main.c

Now run Valgrind again. You’ll get something like:

5 bytes in 1 blocks are definitely lost in loss record 1 of 1
   at 0x4C29BE3: malloc (vg_replace_malloc.c:299)
   by 0x40053E: main (main.c:4)

Boom. Now you know the leak is from line 4 of main.c.

What about optimizations?

By default, GCC uses optimizations (like -O2), which can reorder or inline code. Sometimes this makes debugging confusing.

If you want your compiled binary to stay closer to what you wrote, try:

gcc -o executable -std=c11 -Wall -Og -ggdb3 main.c
  • -Og enables optimizations that are friendly to debugging.
  • -ggdb3 embeds rich debug info for tools like Valgrind and gdb.

6. Debugging Memory Leaks: Practical Techniques

Valgrind will tell you where memory was allocated and whether it was freed. It doesn’t know your program’s intent, but it gives you a trail to follow.

Here are some useful strategies:

6.1 General leak-hunting habits

  • If you allocate memory dynamically (malloc, calloc, realloc, new), make sure it’s freed.
  • Don’t “forget” to assign the pointer returned by malloc or realloc to something you still track.
  • Don’t overwrite a pointer without freeing what it previously pointed to.
  • In C++, prefer RAII (Resource Acquisition Is Initialization): wrap resources in classes whose destructors free them automatically.

6.2 Follow the stack trace

When Valgrind shows a leak, it prints a stack trace. Work from the bottom up:

  • The last frame in your code is usually where the allocation is made.
  • Trace the pointer’s lifetime forward: where is it stored, passed, overwritten, or forgotten?

6.3 Use gdb alongside Valgrind

You can also:

  • Run under Valgrind to find the problematic line(s)
  • Then run under gdb and set breakpoints around that code
  • Step through and watch how the pointer’s lifetime evolves

7. Classic Mistake #1: Losing Track of realloc

Here’s a very common bug pattern involving realloc:

#include <stdlib.h>
#include <stdint.h>
typedef struct {
    int32_t *data;
    int32_t length;
} List;
List *resizeArray(List *array) {
    int32_t *dPtr = array->data;
    dPtr = realloc(dPtr, 15 * sizeof(int32_t)); // doesn't update array->data
    return array;
}
int main(void) {
    List *array = calloc(1, sizeof(List));
    array->data = calloc(10, sizeof(int32_t));
    array->length = 10;
    array = resizeArray(array);
    free(array->data);
    free(array);
    return 0;
}

Valgrind might say:

60 bytes in 1 blocks are definitely lost in loss record 1 of 1
   at 0x4C2BB78: realloc (vg_replace_malloc.c:785)
   by 0x4005E4: resizeArray (main.c:12)
   by 0x40062E: main (main.c:19)

What happened?

  • realloc can move the memory block to a new address.
  • We stored the result in dPtr, but never wrote it back to array->data.
  • After resizeArray returns, array->data still points to the old memory, which is now lost.

Fix:

List *resizeArray(List *array) {
    int32_t *newData = realloc(array->data, 15 * sizeof(int32_t));
    if (newData == NULL) {
        // handle allocation failure
        return array;
    }
    array->data = newData;
    array->length = 15;
    return array;
}

Golden rule: Whenever you call realloc, make sure the owning pointer gets updated.

8. Classic Mistake #2: Invalid Writes (Off-by-One)

Valgrind doesn’t just find leaks; it also catches illegal memory accesses.

Example:

#include <stdlib.h>
#include <stdint.h>
int main(void) {
    char *alphabet = calloc(26, sizeof(char));
    for (uint8_t i = 0; i < 26; i++) {
        alphabet[i] = 'A' + i;
    }
    alphabet[26] = '\0'; // attempt to null-terminate
    free(alphabet);
    return 0;
}

Valgrind output:

Invalid write of size 1
   at 0x4005CA: main (main.c:10)
 Address 0x51f905a is 0 bytes after a block of size 26 alloc'd
   at 0x4C2B975: calloc (vg_replace_malloc.c:711)
   by 0x400593: main (main.c:5)

The array has space for 26 characters, indexed 025. Writing alphabet[26] goes one past the end. That’s undefined behavior. Valgrind calls it an invalid write.

Fix options:

  • Allocate one extra byte for the terminator:
  • char *alphabet = calloc(27, sizeof(char));
  • Or avoid manually extending beyond the allocated size.

9. Classic Mistake #3: Invalid Reads (Also Off-by-One)

Another example, this time reading out of bounds:

#include <stdlib.h>
#include <stdint.h>
int main(void) {
    char *destination = calloc(27, sizeof(char));
    char *source = malloc(26 * sizeof(char));
    // (Pretend source is filled with something valid.)
    for (uint8_t i = 0; i < 27; i++) {
        destination[i] = source[i]; // last iteration reads out of bounds
    }
    free(destination);
    free(source);
    return 0;
}

Valgrind:

Invalid read of size 1
   at 0x400602: main (main.c:9)
 Address 0x51f90ba is 0 bytes after a block of size 26 alloc'd
   at 0x4C29BE3: malloc (vg_replace_malloc.c:299)
   by 0x4005E1: main (main.c:6)

On the last loop iteration, i = 26, we read source[26]. But source has valid indices 025. Again, classic off-by-one.

Takeaway:

  • Invalid write? Look at the left side of the assignment.
  • Invalid read? Look at the right side.

10. When Libraries Are Involved: “Is This Leak Even Mine?”

Life gets more interesting when you use third-party libraries. Two important concepts:

  1. Ownership / reference counting (who frees what?)
  2. Known leaks in libraries

Let’s look at two real-world style examples.

10.1 Jansson (JSON library): borrowed vs owned references

Consider:

#include <jansson.h>
#include <stdio.h>
int main(void) {
    const char *string = "{ \"key\": \"value\" }";
    json_error_t error;
    json_t *root  = json_loads(string, 0, &error);       // allocate JSON root
    json_t *value = json_object_get(root, "key");        // get value
    printf("\"%s\" is the value field.\n", json_string_value(value));
    json_decref(value); // <- is this correct?
    json_decref(root);
    return 0;
}

Depending on the version and actual API semantics, calling json_decref(value) here can cause invalid reads/writes, because the Jansson API often treats such pointers as borrowed references.

In other words:

  • You must decref structures you own.
  • You must not decref references that are merely borrowed from another object.

Lesson: always read the library’s documentation for its memory ownership model. Don’t blindly free everything you get back from a function.

10.2 SDL: Leaks you can’t fix (and maybe shouldn’t care about)

Another example using SDL:

#include "SDL2/SDL.h"
int main(int argc, char *argv[]) {
    if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) != 0) {
        SDL_Log("Unable to initialize SDL: %s", SDL_GetError());
        return 1;
    }
    SDL_Quit();
    return 0;
}

Sometimes running this under Valgrind shows a small leak (e.g., ~200 KB) that comes from SDL internals, not your code.

You did the right thing: initialize, then quit. Still, Valgrind complains.

This happens. Graphics, drivers, and system libraries can leave behind allocations that aren’t cleaned up in a way Valgrind expects. Often they’re:

  • One-time allocations that live until process exit
  • Known issues that don’t impact your actual program logic

In such cases, you can:

  • Check whether the leak is reported as coming from your code or deep inside the library.
  • Search online or in the library’s bug tracker to see if it’s a known issue.
  • Use Valgrind suppression files to silence those specific, external leaks so you can focus on your own.

11. “Is This Leak Mine?” and Other Frequently Asked Questions

Q1: How do I know if the leak is mine?

Blunt but accurate answer: almost always, it is. 😄

Unless the stack trace clearly shows only library internals and no frames from your code, assume you did something wrong:

  • Forgot to free something
  • Lost a pointer via reassignment/realloc
  • Misunderstood a library’s ownership rules

Q2: What if I’m using a bunch of third-party code?

Strategy:

Look at the stack trace:

  • If it includes your functions near the top → start there.
  • If it’s all library frames, google the error or search the library’s issue tracker.

Temporarily comment out certain blocks of your code (if possible) and run Valgrind again:

  • If the leak disappears, you’ve isolated the region.

Simplify: build a minimal reproducible example that allocates, uses, and frees the library object. Run that under Valgrind.

Q3: I found a leak in a library. What should I do?

  • Check if it’s already reported.
  • If not, open an issue with:
  • Valgrind output
  • Your minimal code sample
  • Library version and platform

Congratulations, you’ve just given back to open source. 🎉

12. A Simple Leak-Hunting Checklist

When Valgrind reports problems:

Rebuild with debug info Use -g or -ggdb3 and preferably -Og.

Run with full checks enabled

  • valgrind --leak-check=full --show-leak-kinds=all \ --track-origins=yes --verbose \ ./executable ...
  1. Look at the first error Fixing one leak or invalid access can make several others vanish.
  2. Follow the pointer’s lifetime Where is it allocated, passed, stored, overwritten, and freed?
  3. Check for off-by-one errors If you see “Invalid read/write … 0 bytes after a block of size …”, think array bounds.
  4. Distinguish your code from library code If leaks only feature library frames, research or suppress them.

13. Where to Go Next

If reading this made you curious, here are some topics to explore next (search them up):

  • How malloc/free and the heap allocator actually work
  • What a segmentation fault really is
  • The formal definition of a memory leak vs. a memory access error
  • How Valgrind implements its magic (shadow memory, instrumentation, etc.)

Final Thoughts

Valgrind can seem intimidating at first — its output is noisy, and the stack traces can be long. But once you understand:

  • How to compile with debug info
  • How to run with the right flags
  • How to interpret the core parts of its report

…it becomes an incredibly powerful ally.

You don’t have to eliminate every leak that Valgrind shows (especially those inside third-party libraries), but you do want:

  • Your own code to be clean and leak-free
  • No invalid reads/writes
  • No use-after-free or uninitialized reads

Next time your program mysteriously grows in memory or crashes after an hour, fire up Valgrind and start hunting. 🕵️‍♀️


메타데이터
post_id
67af89744a95
slug
how-do-i-use-valgrind-to-find-memory-leaks-67af89744a95
url
https://medium.com/@trivajay259/how-do-i-use-valgrind-to-find-memory-leaks-67af89744a95
canonical_url
https://medium.com/@trivajay259/how-do-i-use-valgrind-to-find-memory-leaks-67af89744a95
author_url
https://medium.com/@trivajay259
status
ok
fetched_at
2026-08-12 23:53:48