5.1.19 · D5C Programming

Question bank — Valgrind — detecting memory errors

1,932 words9 min readBack to topic

Before you start, keep three words in mind (from the parent's mnemonic): Addressability ("am I allowed to touch this byte?"), Validity ("is the value here real or garbage from malloc?"), and Leak ("did I lose the only pointer to a block?"). Almost every trap here is really asking which of these three is being violated — or whether any is.

The whole page is really one picture. Before you touch a single trap, burn this map into your head — a heap block, its red zones, and the two shadow bits that live under every byte:

Figure — Valgrind — detecting memory errors

Look at the pale-yellow guard bytes flanking the real block: they always have their A-bit = 0. That single fact powers half the "Spot the error" answers below — stepping one byte past the block lands on a red zone, and the guard slams the door.


The A/V/L map — read this before answering

Every Valgrind message you will ever meet sorts into exactly three bins. This is the decision tree the parent's mnemonic compresses into three letters — follow an arrow from any symptom to its category:

yes

yes

yes

Valgrind complained. Which bin?

Did you touch a byte you are not allowed to touch?

Did a garbage value change what the program did?

Did a block survive to exit with no live pointer?

A - Addressability

V - Validity

L - Leak

Invalid read or write, use-after-free, double free, past red zone

Conditional jump depends on uninitialised value, syscall on undefined bytes

definitely lost, indirectly lost, possibly lost, still reachable

Now watch the bits actually change. This strip shows one tiny program — malloc, read-before-write, write, free, use-after-free — and how the A-bit row and V-bit row flip at each step. Every trap in the "Spot the error" and "Edge cases" sections is just one frame of this film:

Figure — Valgrind — detecting memory errors

Trace it left to right: after malloc the block is touchable (A=1) but garbage (V=0); the write turns V=1; after free the whole block goes A=0 and drops into quarantine — which is exactly why the final read is caught.

And here is what the guard's report actually looks like on your terminal, colour-coded to the same three bins so you can eyeball which bit failed at a glance:

Figure — Valgrind — detecting memory errors

True or false — justify

A clean Valgrind run guarantees your program is bug-free.
False. Memcheck only catches memory errors on the paths it actually executed; a bug on an untaken branch or unseen input is invisible, and logic bugs aren't memory bugs at all.
Reading one byte before the start of a heap block is fine because it's still "your" allocation.
False. Bytes just below the block are not part of it — they sit in the red zone (figure 1), A-bit 0, so it's an "Invalid read ... N bytes before a block". Only bytes [a, a+size) are yours.
Memcheck flags every use of an uninitialised value the instant it is read.
False. It only complains when the undefined value affects observable behaviour — a conditional jump or a syscall argument. Merely copying or arithmetic-combining undefined bits stays silent; the V-bits just travel along.
still reachable bytes in the leak summary are always real leaks you must fix.
False. still reachable means a pointer to the block still exists at exit (e.g. a global cache the OS reclaims anyway). It's often harmless; prioritise definitely lost.
Compiling with -O2 gives Valgrind a more accurate picture because it's the release build.
False. Optimisation can reorder, inline, or eliminate the very access that's buggy and strips line info. Debug with -O0 -g; check the release build separately if needed.
A program that returns exit code 0 and prints correct output has no memory errors.
False. Heap overflows and uninitialised reads are undefined behaviour — "correct today" is a coincidence of heap layout and compiler version, not a proof of safety.
Valgrind needs your source code to be recompiled with special Valgrind instrumentation.
False. It works on the ordinary binary by JIT-translating it onto a synthetic CPU at runtime. You only add -g so the report can name source lines — that's optional, not required.
A double free() of the same pointer is caught by the V-bit machinery.
False. It's an addressability/allocator-state issue, not a value one. The second free targets a block whose A-bits are already 0 (quarantined), so Memcheck reports "Invalid free() / delete / delete[]".
If malloc returns NULL and you write through it, Valgrind will always report an invalid write.
False-ish. Writing to address 0 usually segfaults immediately, and the guaranteed diagnosis is a crash, not necessarily a tidy A-bit message; the real fix is checking malloc's return, which Valgrind can't do for you.

Spot the error

char *p = malloc(10); strcpy(p, "hello"); free(p); printf("%c", p[0]); — what does Valgrind say and which bit is at fault?
Invalid read of size 1 (use-after-free): after free, the block sits in quarantine with A-bits = 0 (last frame of figure 2), so touching p[0] is an addressability error even though the bytes still look like "hello".
int *a = malloc(4*sizeof(int)); for(int i=0;i<=4;i++) a[i]=i; — where is the off-by-one and what's the message?
The <= writes a[4], one element past the 4-element block — straight into the red zone of figure 1: "Invalid write of size 4, 0 bytes after a block of size 16". The A-bit at byte 16 is 0.
int x; if (x > 0) foo(); — is this a Valgrind error, and what would it report?
Yes: x is an uninitialised stack variable, so branching on it gives "Conditional jump or move depends on uninitialised value(s)" — a V-bit error. (Note: Memcheck tracks stack values too, not only heap.)
char *buf = malloc(100); buf = malloc(200); free(buf); — which block leaks and how is it classified?
The first 100-byte block: after reassigning buf no pointer holds its address, so the exit scan finds it unreachable ⇒ "definitely lost". The 200-byte block was freed and is fine.
free(p); free(p); where p isn't nulled between — safe or not?
Not safe: double free. The second call is an "Invalid free()" because the block is already freed/quarantined. Setting p = NULL after the first free makes the second free(NULL) a no-op.
int *p = malloc(sizeof(int)); int y = *p + 1; if (y == 5) ... — where does Valgrind finally complain?
At the if, not at *p + 1. The undefined V-bits ride through the addition into y, and only the conditional jump — where garbage changes control flow — triggers the report. --track-origins=yes would point back to the malloc.

Why questions

Why does Valgrind quarantine freed blocks on a freelist instead of returning them to the allocator immediately?
So the A-bits stay 0 long enough that a later use-after-free hits inaccessible memory. If the block were reused instantly, the stale access would land on valid memory and go undetected.
Why does Memcheck keep two separate bit-tracks (A and V) instead of one "is this OK" flag?
"May I touch it?" and "is what's there meaningful?" are independent questions. Freed memory is untouchable but its bytes are defined; fresh malloc'd memory is touchable but undefined. One flag can't distinguish them.
Why does --track-origins=yes cost noticeable speed?
It records the birth-place (allocation or stack frame) of every undefined value and propagates that provenance through operations — extra shadow bookkeeping on top of the normal V-bit tracking.
Why does Valgrind slow a program down by roughly 10–50×?
It doesn't run your instructions on the real CPU; it JIT-translates them onto a synthetic one and wraps every load and store with A/V-bit checks. That per-access instrumentation is the price of seeing everything.
Why is -g recommended even though Valgrind runs without it?
-g embeds debug info so the report maps hex addresses back to source file, line, and variable names — turning "Invalid write at 0x4005f2" into "at main (prog.c:4)". It changes readability, not detection.
Why can Memcheck catch heap overflows but not many stack-array overflows?
It surrounds heap blocks with unaddressable "red-zone" guard bytes (figure 1), so stepping past a heap block hits a 0 A-bit. Adjacent stack locals have no red zones between them, so a small overflow just lands on another valid local. AddressSanitizer (ASan) instruments the stack more aggressively for that case.

Edge cases

free(NULL); — error or silently fine?
Silently fine. The C standard defines free(NULL) as a no-op, and Valgrind treats it as such — no report.
malloc(0) returns a pointer — is writing one byte to it OK?
No. A zero-size allocation gives a block of 0 accessible bytes (implementation may return non-NULL), so any write is "Invalid write, 0 bytes after a block of size 0".
At program exit you free everything except one still-in-scope global pointer's block — leak or not?
Reported as still reachable, not definitely lost: the global pointer still points into it, so the mark-and-sweep scan finds it reachable. Not a "lost" leak, though it is technically unfreed.
A block is only pointed to by a pointer to its middle, not its start — how is it classified?
possibly lost: only an interior pointer reaches it. Valgrind can't be sure that address is meant as a live reference, so it hedges rather than calling it definitely lost.
You leak block B, but B was the only pointer to block C — how is C classified?
indirectly lost: C is unreachable only because the lost block B held its pointer. The root leak is B; fixing B's leak would resolve C too.
An uninitialised value is written to a file via a syscall — silent or reported?
Reported. Passing undefined bytes to a syscall (like write) is an observable use of garbage, so Memcheck flags "syscall param ... points to uninitialised byte(s)" — same principle as the conditional-jump case.
Reading uninitialised memory that you never branch on and never output — bug or not, per Memcheck?
Memcheck stays silent because the value never influences observable behaviour, but it is still undefined behaviour in principle. Silence here means "harmless so far", not "correct".

Recall One-line self-check

If you can state, for any Valgrind message, whether it's an A, a V, or an L problem (walk the flowchart above) — and name the boundary case that produced it — you've internalised this bank. Anything that fits none of those three isn't a Memcheck bug; reach for GDB instead.