C Part I · Module 9 · Memory and Object Lifetime
Memory Leaks
This is the gentlest of the memory bugs and in some ways the most instructive. Nothing is corrupted. No pointer goes bad. No read returns garbage. The program produces exactly the right answers, passes every test, exits with status zero, and is nonetheless broken — because somewhere it allocated a block and then lost the ability to release it. On a program that runs for a tenth of a second nobody will ever notice. On a program that is supposed to run for a month, the same two lines of code will eventually stop the machine. That difference is the real subject here: not the mechanism, which takes a paragraph, but how to reason about whether a particular leak is a cosmetic flaw or a shutdown waiting to happen.
Intermediate20 min readCPart IFundamentalsMemory
Part I · Module 9 · Chapter 9.9 · Memory Leaks
1. What a Leak Is
A memory leak is an allocated block that the program can no longer reach. Its lifetime has not ended — Chapter 9.1's allocated duration runs until
free, andfreeis never going to be called — but no pointer anywhere in the program holds its address, so callingfreehas become impossible.
Three things follow immediately, and all three are the reason leaks are hard to find.
Nothing is wrong with the block. Its bytes are intact. It is not corrupted, not reused, not overwritten. It is simply unreachable, like a house whose only key has been thrown into the sea.
Nothing in the program misbehaves. A leak produces no wrong answers. Every computation that used the block got the right result before the pointer was lost.
Nothing reports it. There is no signal, no diagnostic, no return value. The bug is the absence of a function call, and absences do not raise errors.
The smallest possible example is two lines:
int *p = malloc(sizeof *p);
p = NULL; /* the block is now unreachable */The allocation succeeded. The block exists. p no longer points to it. There is no other pointer. That block will remain allocated until the process ends.
2. Why It Is Not a Crash
Here is the fact that makes leaks easy to dismiss: when a process exits, the operating system reclaims every page it held, leaked or not. Nothing is lost permanently. Nothing persists after the program. A leak is not a corruption of the system.
So a program that leaks one block, runs for forty milliseconds, and exits is — in any practical sense — fine. If you have been told that every leak is a serious bug, that claim is too strong, and holding it makes it harder to prioritise the ones that matter.
But the dismissal is also too strong, and for a reason worth stating precisely:
Leaked memory is not memory your program is using. It is memory your program can never use again. Memory in use is doing work and will be returned. Leaked memory is doing nothing and will never be returned. For every purpose except the final exit, the leaked block is strictly worse than an allocated one.
The question is therefore never "does this program leak?" It is "how does the leak scale, and how long does the program live?"
3. The Only Question That Matters: Does It Scale With Work?
/* Leak A — once, at startup, bounded */
static char *banner;
void init(void) { banner = malloc(64); /* never freed */ }
/* Leak B — once per request, unbounded */
void handle(int fd) { char *buf = malloc(4096); /* never freed */ }Leak A costs 64 bytes for the life of the process. It is a smell: it makes the leak report noisy, so it hides leak B. It is not a danger.
Leak B costs 4096 bytes per request. Serve a million requests and the program has mislaid four gigabytes. Nothing about the code gets worse; only the arithmetic does.
| Leak shape | Cost | Matters in a short tool? | Matters in a daemon or server? |
|---|---|---|---|
| Bounded — a fixed number of blocks, once | constant | no | no, but it hides the others |
| Proportional to input — one block per item read | grows with input size | only on large inputs | yes |
| Proportional to work — one block per request, per loop, per event | grows without bound | rarely | fatal |
A leak proportional to work in a program with no natural end is not a leak, it is a countdown. The program will exhaust memory; the only unknown is when.
And when it does, one of two things happens. Either malloc begins returning NULL — at which point every allocation check you wrote in Chapter 9.6 §3 earns its keep, and a well-written program degrades or exits cleanly — or the operating system's memory manager kills the process outright, with no chance to report anything. Which of the two you get is not under your control. The checks are.
4. Measuring One
Two programs, identical but for a single line. Both call getrusage, which is not part of C — it is a platform service, used here only as a measuring instrument, exactly as a stopwatch is not part of the race.
#include <stdio.h>
#include <stdlib.h>
#include <sys/resource.h>
static long peak_rss(void)
{
struct rusage ru;
getrusage(RUSAGE_SELF, &ru);
return ru.ru_maxrss; /* units are platform-dependent */
}
static void leaky(size_t bytes)
{
char *p = malloc(bytes);
if (p == NULL) { return; }
p[0] = 1; /* touch both ends so pages are real */
p[bytes - 1] = 1;
} /* no free — p is gone, block is not */
int main(void)
{
printf("peak before %ld\n", peak_rss());
for (int i = 0; i < 4000; i++) {
leaky(64 * 1024);
}
printf("peak after leaks %ld\n", peak_rss());
return 0;
}peak before 1343488
peak after leaks 89391104The same program with free(p); added before leaky returns:
peak before 1343488
peak after tidy 1392640The leaky version's peak resident memory grew by a factor of about 66. The correct version grew by about 1.04 — 4000 allocations of 64 KiB each, and the process never needed more than its starting footprint, because each block was returned before the next was requested. That reuse is the entire economic argument for free.
5. The Four Shapes
Almost every leak in real code is one of these four. All four are in the program below, which runs cleanly and reports nothing.
#include <stdio.h>
#include <stdlib.h>
/* SHAPE 1 — the early return. The validation was added later, below the
allocation, and the free at the bottom stopped being reached. */
static int shape_one(int n)
{
int *v = malloc(8 * sizeof *v);
if (v == NULL) { return -1; }
if (n < 0) {
return -1; /* LEAK: v is never released */
}
v[0] = n;
int r = v[0];
free(v);
return r;
}
/* SHAPE 2 — the overwritten pointer. The second allocation replaced the
only pointer to the first block. */
static void shape_two(void)
{
char *buf = malloc(100);
if (buf == NULL) { return; }
buf[0] = 'a';
buf = malloc(200); /* LEAK: the 100-byte block is unreachable */
if (buf == NULL) { return; }
buf[0] = 'b';
free(buf);
}
/* SHAPE 3 — the lost realloc, exactly as Chapter 9.7 §6 described it. */
static void shape_three(void)
{
int *p = malloc(4 * sizeof *p);
if (p == NULL) { return; }
p[0] = 1;
p = realloc(p, (size_t)-1); /* refused; the old block is now lost */
if (p == NULL) { return; } /* LEAK: 16 bytes nothing can reach */
free(p);
}
/* SHAPE 4 — the loop that frees on only one branch. */
static void shape_four(void)
{
for (int i = 0; i < 3; i++) {
int *t = malloc(40);
if (t == NULL) { return; }
t[0] = i;
if (i % 2 == 0) {
continue; /* LEAK on i = 0 and i = 2 */
}
free(t);
}
}
int main(void)
{
printf("shape_one(-1) = %d\n", shape_one(-1));
printf("shape_one( 5) = %d\n", shape_one(5));
shape_two();
shape_three();
shape_four();
printf("all four shapes run; the program exits cleanly\n");
return 0;
}Output:
shape_one(-1) = -1
shape_one( 5) = 5
all four shapes run; the program exits cleanlyEvery answer is right. The exit status is zero. Read the four together and a pattern emerges that is more useful than the individual cases:
| Shape | The underlying cause |
|---|---|
| Early return | a new exit path was added after the free was written |
| Overwritten pointer | the only handle was reassigned |
Lost realloc | the only handle was reassigned by a function that can fail |
| Conditional free | the free sits on one branch of several |
All four are the same mistake stated four ways: the free is attached to one path, and the allocation is attached to all of them. That is the sentence to carry away, because it is also the review technique in §7.
6. What the Compiler Tells You: Nothing
$ cc -std=c17 -Wall -Wextra -Wpedantic -O2 -c leaks.c
$No output. Not one warning, at the curriculum's standard warning level with optimisation on, for any of the four shapes.
This is not an oversight in the compiler. Deciding whether a free will eventually be called requires reasoning about every path through the whole program, including paths through other translation units — which is outside what a translation-unit-at-a-time compiler does. Chapter 6.4 §5 made the same point about array bounds and Chapter 7.4 §4 about null checks: the warning flags are excellent at local mistakes and structurally unable to see lifetime ones.
A dedicated static analyser, which does trace paths, does considerably better:
$ clang --analyze -std=c17 leaks.c
leaks.c:12:17: warning: Potential leak of memory pointed to by 'v' [unix.Malloc]
leaks.c:31:14: warning: Potential leak of memory pointed to by 'buf' [unix.Malloc]
leaks.c:42:22: warning: Potential leak of memory pointed to by 'p' [unix.Malloc]
leaks.c:49:28: warning: Potential leak of memory pointed to by 't' [unix.Malloc]
4 warnings generated.All four, by line, at compile time, with no test run and no input. If you are writing C that allocates, run a static analyser. It is the cheapest tool in this chapter and it found every shape.
7. Finding Leaks Without a Tool
The review technique follows directly from §5's one-sentence pattern. For each allocation in a function:
- Count the exits. Every
return, everybreakout of the owning scope, everygoto, and the fall-off-the-end. - Check each one against the ownership rule from Chapter 9.8: on this path, does the block still belong to this function? If it does, it must be released before the exit; if ownership was transferred out, it must not be.
- Find every assignment to the pointer variable. Each one is a potential shape 2 or shape 3. The question at each is: did anything else hold this address first?
- On the error paths, check Chapter 9.8 §8's rule — everything already acquired must be released, in reverse order.
Two structural habits make all four checks shorter:
Allocate as late as possible. An allocation above a validation check is shape 1 waiting to happen; move the validation above the allocation and the early return becomes harmless.
Have one exit for the cleanup, which is Chapter 4.8's cleanup label and Chapter 9.8 §8's pattern. If there is exactly one place the function releases what it owns, there is exactly one place to get right.
8. The Tools Under-Report
A run-time leak detector works by stopping the program, scanning everything it can reach for values that look like addresses of live blocks, and reporting the blocks nothing points at. On the four-shape program:
Process 98972: 4 leaks for 240 total leaked bytes.
1 (112 bytes) ROOT LEAK: <malloc in shape_two>
1 (48 bytes) ROOT LEAK: <malloc in shape_four>
1 (48 bytes) ROOT LEAK: <malloc in shape_one>
1 (32 bytes) ROOT LEAK: <malloc in shape_three>Four blocks, attributed to the right functions, with sizes. That is extremely useful — and it is also wrong, which is the most important thing in this chapter.
shape_four leaks two blocks, not one. Re-read it: the continue is taken when i is 0 and again when i is 2. Two allocations, neither freed.
Here is the same leak isolated, with a switch that calls a harmless function afterwards:
#include <stdio.h>
#include <stdlib.h>
static void leak_two(void)
{
for (int i = 0; i < 3; i++) {
int *t = malloc(40);
if (t == NULL) { return; }
t[0] = i;
if (i % 2 == 0) { continue; } /* leaks on i = 0 and i = 2 */
free(t);
}
}
/* writes over the stack frame leak_two just vacated.
volatile appears here for one narrow reason — it stops the compiler
from deleting writes whose results nothing reads. Chapter 17.2 is its
real subject; treat it as instrumentation for now. */
static void scrub(void)
{
volatile long pad[64];
for (int i = 0; i < 64; i++) { pad[i] = 0; }
}
int main(int argc, char **argv)
{
(void)argv;
leak_two();
if (argc > 1) { scrub(); }
return 0;
}Run without the argument, so scrub is not called:
Process 99760: 1 leak for 48 total leaked bytes.Run with any argument, so scrub overwrites the vacated frame:
Process 99602: 2 leaks for 96 total leaked bytes.Both results were identical across three runs each. The program leaks exactly two blocks in both cases. The only difference is whether a dead stack slot still happened to hold the second block's address when the scan ran — and because the detector cannot tell a live pointer from an abandoned copy of one, it treated that block as reachable and said nothing.
9. A Leak Is the Mild Case
It is worth being explicit about where this chapter sits, because the rest of the module is harsher.
A leak is the failure mode where you lose the pointer and keep the block. Nothing is corrupted; the cost is resources, and it is bounded by how long the program lives.
The next chapter is the mirror image: you release the block and keep the pointer. Now the bytes are no longer yours, your pointer still addresses them, and the program may read or write storage that the allocator has handed to something else. The cost is not resources. The cost is correctness, and it is not bounded by anything.
10. What You Should Be Able to Say Now
- A leak is an allocated block the program can no longer reach. Its lifetime cannot be ended because no pointer to it survives.
- It corrupts nothing and crashes nothing, which is why it passes every test.
- The OS reclaims everything at exit, so a leak in a short-lived program is a smell rather than a danger.
- Leaked memory is memory the program can never use again — strictly worse than memory in use.
- The question is whether the leak scales with work. One proportional to work in a long-running program is a countdown.
- Verified: the leaky loop's peak memory grew about 66×; the identical loop with
freegrew about 1.04×. - Leaked bytes and resident bytes are different numbers — 327,680,000 leaked against 89,391,104 resident, because untouched pages never become real.
- The four shapes — early return, overwritten pointer, lost
realloc, conditional free — are one mistake: thefreeis on one path and the allocation is on all of them. -Wall -Wextra -Wpedantic -O2reported nothing;clang --analyzereported all four. Run a static analyser.- A run-time detector under-reports: measured, it found 1 of 2 real leaks because a dead stack slot still held the address. A clean report is not a proof.
11. Exercises
Exercise 1 — classify four leaks. For each, say whether it is bounded, proportional to input, or proportional to work, and whether it matters in a command-line tool and in a server.
- one 2 KB block allocated in
mainbefore the main loop and never freed - one 80-byte block per line of a log file being parsed
- one 4 KB block per accepted connection
- one 16-byte block per retry of a failed operation that retries forever
Exercise 2 — name the shape. Which of §5's four shapes is each?
char *s = malloc(10); if (!valid) return; free(s);
int *a = malloc(40); a = malloc(80); free(a);
while (next()) { char *t = malloc(8); if (skip()) continue; free(t); }
p = realloc(p, n);Exercise 3 — count the exits. Apply §7's technique. How many exit paths does this function have, which ones leak, and what is the smallest change that fixes all of them at once?
int run(int a, int b)
{
int *buf = malloc(100);
if (buf == NULL) { return -1; }
if (a < 0) { return -2; }
if (b < 0) { free(buf); return -3; }
for (int i = 0; i < a; i++) {
if (i == b) { return -4; }
}
free(buf);
return 0;
}Exercise 4 — move the validation. Rewrite shape_one from §5 so the early return cannot leak, without adding a second free and without using goto. What general rule did you apply?
Exercise 5 — predict the detector's report. This program leaks three blocks. Say how many a conservative scanning detector is likely to report, and why your answer is not a certainty.
#include <stdlib.h>
void f(void) { for (int i = 0; i < 3; i++) { char *p = malloc(32); p[0] = (char)i; } }
int main(void) { f(); return 0; }Exercise 6 — the hidden leak. A program's resident memory is flat over an hour, and the leak detector reports nothing. Give two distinct reasons it could still be leaking badly, drawing on §4 and §8.
Exercise 7 — rank the tools. You can run exactly one of: -Wall -Wextra, a static analyser, a run-time leak detector. Using this chapter's measurements, rank them for finding the §5 shapes, and say what each would miss.
Exercise 8 — argue the priority. Your team has a leak proportional to work in a daemon, and a bounded 64-byte leak at startup. Argue for fixing the bounded one too, in two sentences, using something from §8.
12. Summary
- A leak is an allocated block nothing can reach — the
freehas become impossible, not merely forgotten. - It corrupts nothing, crashes nothing, and reports nothing.
- The OS reclaims at exit, so the real question is does the leak scale with work, and how long does the program live.
- Leaked memory can never be reused — worse than memory in use, for every purpose but the last.
- Measured: 66× peak memory leaking, 1.04× with the
free. - Leaked bytes ≠ resident bytes: 327,680,000 against 89,391,104.
- The four shapes reduce to one sentence: the
freeis on one path, the allocation on all of them. - Warning flags found none; a static analyser found all four.
- A run-time detector misses leaks — 1 of 2, measured. A clean report is weak evidence.
- Prevention is Chapter 9.8's one documented owner; detection is a supplement, not a substitute.
A leak keeps the block and loses the pointer. The next chapter inverts that, and the inversion is far more dangerous than it sounds — because the first thing you will notice about a program that reads through a freed pointer is that it usually works. The old values are often still sitting there, untouched, and the program sails through its tests printing exactly the right answers. What has actually happened is that the block was handed to the allocator and may be handed to someone else at any moment, and the day it is, the bug surfaces as corrupted data in a completely unrelated part of the program. That gap between "appears to work" and "is catastrophically wrong" is the subject of the next chapter.
Standards & specifications
- Governing standard
- ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)
Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C Programming curriculum.
