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.
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.txtdeliverable 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 (thep = 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::stringinternals or some JITs; irrelevant here.) - still reachable. Pointers to this block exist at
exit, but you never calledfree. 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 ormain’spstill holds the address atexit). Lab 6 rubric requires zero here;free(p)before returning. - C: the
headblock isdefinitely lost. Thenextblock isindirectly 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 -Wformatat 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 ap = realloc(p, n)bug. Rewrite with the tmp-idiom. - still reachable: the program exited (via
exit()orreturnfrommain) before the cleanup path ran. Make surecleanUp(array)executes on every normal exit. - uninitialised value: a
mallocwherecallocwas needed, or a loop that skipped initialization of some slot. Add--track-origins=yesif you did not already; the origin stack trace points at the allocation site. - Invalid read/write: pointer arithmetic off-by-one (check
i < lengthvsi <= length), or a read afterfree(check that everyfreeis followed byp = NULL;and that nothing dereferences the pointer afterward).
What comes next
- 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.