Exercises — Valgrind — detecting memory errors
Prerequisites you may want open: malloc and free, Pointers in C, Undefined Behaviour in C, Stack vs Heap.
Level 1 — Recognition
Can you read a Valgrind report and name the bug class?
L1.1 — Name the error class
Valgrind prints:
Invalid read of size 4
at main (prog.c:7)
Address 0x... is 0 bytes inside a block of size 20 free'd
Which of A / V / L is this, and what is the everyday name of the bug?
Recall Solution
The block was free'd, so its A-bits are 0 — you are not allowed to touch it. This is an A-bit error. Everyday name: use-after-free. It is not a V-bit error because the issue is not "the value is garbage", it is "this byte is off-limits".
L1.2 — Read vs write, inside vs after
Two reports:
(a) Invalid write of size 1
Address 0x... is 0 bytes after a block of size 10 alloc'd
(b) Conditional jump or move depends on uninitialised value(s)
Classify each as A or V, and say in one line what code likely caused it.
Recall Solution
(a) "0 bytes after a block of size 10" → you wrote one byte past a 10-byte buffer, e.g. buf[10] = 'x';. The byte past the end is not addressable → A-bit (heap buffer overflow).
(b) "depends on uninitialised value(s)" → you branched (if/while) on a variable whose value was never set → V-bit.
Level 2 — Application
Given code, predict the exact Valgrind verdict and the numbers.
L2.1 — Off-by-one index
int *a = malloc(4 * sizeof(int)); // 4 ints
a[4] = 99;
free(a);On a system where sizeof(int) == 4, what block size does Valgrind report, and how many bytes after the block is the faulting address?
Recall Solution
Block size bytes. Index 4 addresses byte , i.e. exactly 0 bytes after the 16-byte block. Report:
Invalid write of size 4
Address 0x... is 0 bytes after a block of size 16 alloc'd
L2.2 — Counting leaked bytes
char *p = malloc(50);
char *q = malloc(80);
p = q; // the 50-byte block is now orphaned
free(q); // frees the 80-byte blockHow many bytes are definitely lost?
Recall Solution
After p = q, both pointers hold the 80-block's address; no pointer holds the 50-block's address, so at exit the scan finds it unreachable. free(q) cleans up the 80-block. Leaked = 50 bytes in 1 block. (See Stack vs Heap: the heap block outlives the lost pointer — the block is what leaks, not the pointer.)
L2.3 — How many invalid bytes in a strcpy?
char *dst = malloc(3); // room for "ab" + '\0' -> but we copy more
strcpy(dst, "hello"); // "hello" needs 6 bytes
free(dst);How many bytes get written past the block?
Recall Solution
"hello" is h e l l o \0 = 6 bytes. The buffer holds 3. Bytes 0,1,2 are legal; bytes 3,4,5 are past the block → 3 invalid bytes written. Valgrind reports Invalid write of size 1 at the first out-of-bounds byte (offset 3).
Level 3 — Analysis
Explain the mechanism, not just the label.
L3.1 — Why quarantine?
A student frees a block, then reads from it. They complain: "I freed it and then re-malloced the same size and Valgrind sometimes stops reporting the use-after-free." Explain what's happening in terms of the freelist quarantine.
Recall Solution
On free, Memcheck sets the block's A-bits to 0 and parks it in a quarantine freelist so it is not handed back immediately. That is why a stray read is caught: the A-bits are still 0. But the quarantine has a finite size (default ~16 MB of freed blocks). If you free enough memory afterwards, the old block is evicted and eventually recycled by a later malloc — its A-bits flip back to 1 and the use-after-free becomes invisible. Fix for reliable detection: enlarge it with --freelist-vol= and delay recycling with --freelist-big-blocks.
L3.2 — Why does copying garbage NOT error, but branching does?
int *p = malloc(sizeof(int)); // V-bits undefined
int x = *p; // line A: copy
int y = x + 1; // line B: arithmetic
if (y > 0) // line C: branch
puts("pos");
free(p);At which line does Valgrind actually complain, and why not earlier?
Recall Solution
Valgrind complains at line C. Reason: V-bits travel with the value. Copying (line A) and arithmetic (line B) just propagate the "undefined" mark forward — no decision has been made yet, so no observable behaviour depends on garbage. Only at the conditional jump (line C) does the undefined value change which path the program takes — that is the first point where garbage produces different real behaviour, so that is where Memcheck fires. (This is Undefined Behaviour in C made observable exactly at the branch.)
L3.3 — Classify all four leak kinds
struct Node { struct Node *next; int v; };
struct Node *head = malloc(sizeof(struct Node)); // block H
head->next = malloc(sizeof(struct Node)); // block T
head = NULL; // lose the only pointer to H
// program exits here (no free)Classify block H and block T as definitely / indirectly / still reachable / possibly lost.
Recall Solution
- H — after
head = NULLno pointer anywhere points to H → definitely lost. - T — the only pointer to T lives inside H (
head->next). T is reachable only through a lost block → indirectly lost. - Nothing is still reachable (no live pointer survives) and nothing is possibly lost (no interior-only pointer case here).
Valgrind reports:
definitely lost: <sizeof Node> in 1 blockplusindirectly lost: <sizeof Node> in 1 block.
Level 4 — Synthesis
Combine tools, flags, and reasoning to design a diagnosis.
L4.1 — Design the invocation
You have a leak and an uninitialised-value bug, and you need to know where the garbage value was born and see every leak kind. Write the single Valgrind command line and justify each flag.
Recall Solution
valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes ./prog
--leak-check=full→ full per-block leak listing (not just the summary).--show-leak-kinds=all→ shows indirectly / possibly / still reachable too, not only definitely lost.--track-origins=yes→ for each V-bit error, reports the allocation site where the undefined value originated (pays a speed cost — that is why it is off by default). And compile withgcc -g -O0so lines/names appear and the optimiser doesn't elide the bug.
L4.2 — Valgrind vs GDB vs ASan
Match each tool to the situation it wins in: (a) intermittent heap corruption, no source changes wanted, willing to accept 10–30× slowdown; (b) a reproducible crash where you want to step line-by-line and inspect variables live; (c) fast CI runs where a small (~2×) slowdown and a recompile are both acceptable.
Recall Solution
- (a) → Valgrind/Memcheck — no recompile of your logic needed (just
-g), catches heap errors via shadow memory, slowness is expected. See parent note. - (b) → Debugging with GDB — interactive stepping, breakpoints, live variable inspection; it does not track every byte, but it lets you investigate.
- (c) → AddressSanitizer (ASan) — compile-time instrumentation, roughly 2× slowdown, ideal for continuous integration where you can afford the rebuild.
Level 5 — Mastery
Reason about the model's edges and failure modes.
L5.1 — A leak Valgrind calls "possibly lost"
int *base = malloc(10 * sizeof(int));
base += 3; // 'base' now points into the MIDDLE of the block
// no free; exitWhy is this possibly lost, not definitely lost? What single edit makes it definitely lost instead?
Recall Solution
At exit the scan finds a pointer (base) that points into the block but not to its start — an interior pointer. Memcheck cannot be sure this is a genuine "I still own this" pointer or a coincidental value, so it hedges: possibly lost.
Make it definitely lost by destroying even the interior pointer: base = NULL; before exit → now no pointer references the block at all → definitely lost.
L5.2 — Why a real-memory tool would miss the overflow
int *a = malloc(4 * sizeof(int)); // 16 bytes
a[4] = 7; // one int past the endThe heap allocator often rounds malloc(16) up to a larger real chunk (say 32 bytes). Explain why writing a[4] might not crash on real hardware yet Valgrind still reports it, referencing shadow memory.
Recall Solution
The hardware sees only the rounded allocation (32 real bytes), so byte 16 is inside a page the OS granted → no segfault; the write silently corrupts allocator bookkeeping — classic silent Undefined Behaviour in C. Memcheck does not rely on hardware pages. It keeps a shadow A-bit for every byte and marks A-bits = 1 only for the 16 bytes you requested, not the padding. Writing byte 16 hits an A-bit of 0 → Invalid write reported. This is the whole reason the tool exists: it tracks your intended boundaries, which the CPU cannot see.
L5.3 — Where Memcheck is blind
Name one memory bug class Memcheck cannot catch, and say why the A/V/L model misses it.
Recall Solution
Stack buffer overflows that stay within the process's own stack, and overflows between two adjacent live heap fields inside one allocation (e.g. writing past field 1 into field 2 of the same struct).
Why: the whole allocation's A-bits are 1 (it is one legal block) and the overwritten bytes are addressable and defined — so neither the A-model nor the V-model is violated. Memcheck's boundaries are allocation boundaries, not sub-object boundaries. For sub-object/stack overflows, AddressSanitizer (ASan) (which adds redzones around stack objects) is the better tool.
Recall Self-test recap (hidden)
Every bug above reduces to one letter. Say the letter before you say the fix. Use-after-free ::: A Heap buffer overflow ::: A Branch on uninitialised value ::: V Forgetting a block's only pointer ::: L Interior-only pointer at exit ::: L (possibly lost)