student@ubuntu:~$
pointers-memory Lesson 9 12 min read

Debugging Memory with Valgrind

The four leak categories, the six bug classes, and the command line that produces the Lab 6 deliverable

Based on content from Dr. Stu Steiner, Eastern Washington University.

Reading: Valgrind Memcheck Manual; Hanly & Koffman §13 (pp. 701–720)

In a nutshell

Compilers catch syntax errors. Test inputs catch logic errors. Neither catches leaks, use-after-free, double-free, or out-of-bounds access reliably. Valgrind does, by running your binary on a synthetic CPU that shadow-tracks every byte of memory: is it allocated? written? freed? It prints a report with exact file and line numbers for every violation. The Lab 6 deliverable is a transcript of a Valgrind-clean run. This lesson covers the invocation, the four leak categories (all four must be zero), the bug taxonomy Valgrind detects, and the specific redirection operator that captures the full report. It also names what Valgrind does not catch, so you know when to reach for AddressSanitizer or UndefinedBehaviorSanitizer instead.

Practice this topic: Valgrind drill, or browse the practice gallery.

After this lesson, you will be able to:

  • Compile with the flags Valgrind needs and run the correct memcheck invocation
  • Interpret the four leak categories and distinguish “definitely lost” from “still reachable”
  • Read a Valgrind error with its stack trace and map it to a specific source line
  • Name the six memory-safety bug classes, their CWE numbers, and which tool catches each
  • Produce the exact cscd240lab6val.txt deliverable with both stdout and stderr captured

Quick reference

Leak category Meaning Action
definitely lost no pointer anywhere reaches this block forgot to free, or overwrote your only pointer
indirectly lost reachable only through a definitely-lost block fix the parent leak; these vanish
possibly lost pointer aims into the middle of a block in Lab 6 code, treat as definitely lost
still reachable pointer exists at exit but block was never freed hygiene failure; must be zero for Lab 6

Coming from CSCD 210

Java’s runtime does this work for you. Array bounds are checked on every access (ArrayIndexOutOfBoundsException); the garbage collector reclaims unreachable objects; there is no equivalent to a free, so no use-after-free. You never ran Valgrind against a Java program because the JVM already enforced memory safety at runtime. C ships without those guardrails. Valgrind is how you get them back: a tool that stands between your program and the hardware and yells when you touch memory wrong. The cost is a 20–40× slowdown during the check; the benefit is finding bugs before your users do.


The setup: compile and run

Three flags matter for the compile, one tool invocation for the run.

gcc -g -O0 -Wall -Wextra -std=c99 -o lab6 cscd240Lab6.c lab6.c
valgrind --tool=memcheck --leak-check=full --show-leak-kinds=all \
         --track-origins=yes ./lab6

Compile flags. -g produces debug information so Valgrind can print source file names and line numbers. -O0 disables optimization so the line numbers in the report line up with your source. -Wall -Wextra -std=c99 are the course-wide defaults; warnings caught at compile time never need to be rediscovered at runtime.

Valgrind flags. --tool=memcheck selects the memory-error detector (the default, but being explicit is good form). --leak-check=full scans the heap for leaks at program exit and prints a stack trace for each. --show-leak-kinds=all reports every one of the four leak categories, not just definitely lost. --track-origins=yes tells you where an uninitialized value came from, not just where it was read.


Reading the report

The four leak categories

At program exit, Valgrind scans the heap for blocks you never freed and partitions them into four buckets. For Lab 6, every bucket must read zero bytes in zero blocks. Not “acceptable leaks.” Zero.

  • definitely lost. No pointer anywhere in your program’s data reaches this block. Classic leak. Cause: missing free, or overwriting your only pointer (the p = realloc(p, n) bug).
  • indirectly lost. Reachable only through a block that is itself definitely lost. Example: you leaked the head of a linked list, so every node is indirectly lost. Fix the head; these vanish.
  • possibly lost. Valgrind found a pointer that points into the interior of a heap block, not at its start. For Lab 6 C code, treat these as definitely lost. (Real cases involve C++ std::string internals or some JITs; irrelevant here.)
  • still reachable. Pointers to this block exist at exit, but you never called free. The OS reclaims on process termination, so a short-lived CLI tool appears fine, but in a long-running server it hides real leaks. The rubric requires zero.

A clean report

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

Five allocs, five frees, zero bytes in use at exit. This is the target.

A leaking report

==1234== 16 bytes in 1 blocks are definitely lost in loss record 1 of 1
==1234==    at 0x4848899: malloc (vg_replace_malloc.c:393)
==1234==    by 0x1091A9: createAndFill (lab6.c:14)
==1234==    by 0x10920F: main (cscd240Lab6.c:9)
==1234==
==1234== LEAK SUMMARY:
==1234==    definitely lost: 16 bytes in 1 blocks
==1234==    indirectly lost: 0 bytes in 0 blocks
==1234==      possibly lost: 0 bytes in 0 blocks
==1234==    still reachable: 0 bytes in 0 blocks

The stack trace reads bottom to top: main at line 9 called createAndFill, which at line 14 called malloc. That allocation was never freed. Go to lab6.c:14, trace the pointer, find the missing free.

Check your understanding (map the category)

Match each cause to the leak category Valgrind will report.

/* A */
int *p = malloc(100);
p = malloc(200);              /* overwrote the first pointer */

/* B */
int *p = malloc(100);
/* main returns without free(p) */

/* C */
struct node { int v; struct node *next; };
struct node *head = malloc(sizeof *head);
head->next = malloc(sizeof *head);
head = NULL;                  /* lost the whole list */
Reveal answer
  • A: definitely lost (16 bytes in the first block; the second is still reachable).
  • B: still reachable (the global or main’s p still holds the address at exit). Lab 6 rubric requires zero here; free(p) before returning.
  • C: the head block is definitely lost. The next block is indirectly lost (reachable only via the lost head).

The six bug classes Valgrind catches

Beyond leaks, Valgrind detects five runtime errors. Every one of them has a CWE number and a real-world CVE attached.

Uninitialized read

==1234== Conditional jump or move depends on uninitialised value(s)
==1234==    at 0x10916A: main (demo.c:8)
==1234==  Uninitialised value was created by a heap allocation
==1234==    at 0x4848899: malloc (vg_replace_malloc.c:393)
==1234==    by 0x10915D: main (demo.c:6)

Cause: reading from a heap byte that was never written. Fix: use calloc (zero-initializes), or write every byte before reading. CWE-457.

Invalid read or write (out of bounds)

==1234== Invalid write of size 4
==1234==    at 0x10916A: main (demo.c:10)
==1234==  Address 0x4a2b05c is 0 bytes after a block of size 40 alloc'd

Cause: pointer arithmetic past the end of a heap block. Fix: bounds-check the index. CWE-125 (read, MITRE Top-25 #7) or CWE-787 (write, MITRE Top-25 #1).

Use-after-free

==1234== Invalid read of size 4
==1234==    at 0x10916A: main (demo.c:12)
==1234==  Address 0x4a2b040 is 0 bytes inside a block of size 40 free'd
==1234==    at 0x484B25C: free (vg_replace_malloc.c:872)
==1234==    by 0x109170: main (demo.c:11)

Cause: read or write through a pointer after free. Fix: free(p); p = NULL; and stop using the pointer past the free. CWE-416 (MITRE Top-25 #4).

Double-free or bad-free

==1234== Invalid free() / delete / delete[] / realloc()
==1234==    at 0x484B25C: free (vg_replace_malloc.c:872)
==1234==    by 0x109180: main (demo.c:15)
==1234==  Address 0x4a2b040 is 0 bytes inside a block of size 40 free'd
==1234==    at 0x484B25C: free (vg_replace_malloc.c:872)
==1234==    by 0x109170: main (demo.c:13)

Cause: called free twice on the same block, or passed free a non-heap pointer. Fix: single-owner discipline for each heap block; free(p); p = NULL; so the second free becomes a no-op. CWE-415.

The taxonomy at a glance

Bug class CWE Historic CVE Caught by Valgrind?
Memory leak CWE-401 many (leak-based DoS) yes
Use-after-free CWE-416 (#4) CVE-2022-2588 (Linux kernel) yes
Double-free CWE-415 CVE-2019-11932 (WhatsApp) yes
OOB read CWE-125 (#7) CVE-2014-0160 (Heartbleed) yes
OOB write CWE-787 (#1) CVE-2021-3156 (Baron Samedit) yes
Integer overflow at alloc CWE-190 CVE-2002-0639 (OpenSSH) no

For the exploit mechanics behind Heartbleed, Baron Samedit, and the WhatsApp GIF RCE, see the memory-safety deep dive.

Check your understanding (read the trace)

==7890== Invalid read of size 4
==7890==    at 0x10918A: printIfFound (lab6.c:42)
==7890==    by 0x10924F: main (cscd240Lab6.c:14)
==7890==  Address 0x4a2b060 is 0 bytes after a block of size 16 alloc'd
==7890==    at 0x4848899: malloc (vg_replace_malloc.c:393)
==7890==    by 0x1091A9: createAndFill (lab6.c:14)

Which line of which file has the bug, and what is the bug?

Reveal answer

The bug is at lab6.c:42 inside printIfFound. That line does a 4-byte read that lands 0 bytes after a 16-byte block (which is four ints). The read is past the end of the allocation: an off-by-one in the loop, most likely i <= length where it should be i < length.

The createAndFill line in the trace is where the block was allocated, not where the bug is. Valgrind prints the allocation stack so you can confirm which block is being over-read.


What Valgrind does not catch

Memcheck is a spatial and temporal heap oracle. It does not cover:

  • Stack-array OOB. int x[4]; x[10] = 5; is not a heap access. Use AddressSanitizer: gcc -fsanitize=address -g -O0 ....
  • Global or static-array OOB. Same reason. AddressSanitizer handles these too.
  • Data races in multithreaded code. Use ThreadSanitizer (-fsanitize=thread).
  • Integer overflow, signed overflow, shift out of range. Use UndefinedBehaviorSanitizer (-fsanitize=undefined).
  • Format-string mismatches like printf("%d", "hello"). Caught by -Wall -Wformat at compile time.
  • Logic bugs, slow code, infinite loops, wrong answers. Memcheck does not know what your program is supposed to do.

A clean memcheck report is not a correctness proof. It means “my heap is clean on this one run against these inputs.” You still need your own tests. For the shadow-memory mechanics that make these checks possible, and for a side-by-side of which tool catches which bug class, see the pointer mechanics deep dive.


Producing the Lab 6 deliverable

The rubric asks for cscd240lab6val.txt containing the full Valgrind transcript of a clean run. The command:

gcc -g -O0 -Wall -Wextra -std=c99 -o lab6 cscd240Lab6.c lab6.c
valgrind --tool=memcheck --leak-check=full --show-leak-kinds=all \
         --track-origins=yes ./lab6 &> cscd240lab6val.txt
cat cscd240lab6val.txt     # always inspect before submitting

The redirection operator

The piece that trips students up every term. Valgrind writes its report to stderr, not stdout. Your program writes its interactive output to stdout. The deliverable needs both.

Operator Captures Lab 6 verdict
> file stdout only misses the Valgrind report
2> file stderr only misses your program’s output
&> file (bash/zsh) stdout and stderr correct
> file 2>&1 (POSIX) stdout and stderr correct, portable

After the run, sanity-check:

grep -E "definitely lost|indirectly lost|possibly lost|still reachable|ERROR SUMMARY" \
     cscd240lab6val.txt

Expected output:

    definitely lost: 0 bytes in 0 blocks
    indirectly lost: 0 bytes in 0 blocks
      possibly lost: 0 bytes in 0 blocks
    still reachable: 0 bytes in 0 blocks
ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

If any line shows a non-zero count, fix the bug in the code before submitting.

Common non-zero findings and their fixes

  • definitely lost: missing free(array) on some path, or a p = realloc(p, n) bug. Rewrite with the tmp-idiom.
  • still reachable: the program exited (via exit() or return from main) before the cleanup path ran. Make sure cleanUp(array) executes on every normal exit.
  • uninitialised value: a malloc where calloc was needed, or a loop that skipped initialization of some slot. Add --track-origins=yes if you did not already; the origin stack trace points at the allocation site.
  • Invalid read/write: pointer arithmetic off-by-one (check i < length vs i <= length), or a read after free (check that every free is followed by p = NULL; and that nothing dereferences the pointer afterward).

What comes next

Which statements about Valgrind are correct?
AFor Lab 6, all four leak categories must read zero bytes in zero blocks.
BValgrind catches out-of-bounds access on stack arrays.
C> alone is insufficient for capturing a Valgrind run because Valgrind writes to stderr.
D"still reachable" means a block was never freed but a pointer to it existed at exit.
EA clean Valgrind report proves your program is correct.
Correct: A, C, D.
  • B is wrong: Memcheck only tracks heap allocations. Stack-array OOB needs AddressSanitizer (-fsanitize=address).
  • E is wrong: a clean report proves the absence of specific heap bug classes on that run. It does not prove logical correctness, integer-overflow freedom, or bounds safety on stack arrays.

You now have the full memory-safety toolkit: pointer mechanics, heap discipline, and a tool that verifies you applied them. Next, Structs & typedef starts the week-6 move into aggregate data: grouping related fields into one object, laying them out in memory, and managing allocation and deallocation for structs that own heap fields. Drill this page with the Valgrind skill card, or browse the practice gallery.