Part VIII — Writing safe, leak-free C
Parts I–VII taught you the mechanisms. This Part is the discipline layer on top: the leak shapes that actually show up in real bug trackers (rarely "forgot one
free"), a checklist for writing new C defensively from the first line, and a bad-code/good-code catalog of the mistakes that keep recurring across beginner and intermediate C — each with the exact compiler flag or tool that would have caught it.
44. Memory leaks, in depth: every shape they actually take in production
Section 10.2 showed the textbook leak: allocate, never free. Real leaks in real codebases almost never look that clean — they hide in error paths, loops, and partial failures. Every example below is a pattern you will meet again.
44.1 The error-path leak — the single most common real-world leak
A function acquires several resources in sequence; an early one succeeds, a later one fails, and the function returns without releasing what it already acquired.
// BAD
typedef struct { FILE *log; char *buf1; char *buf2; } Ctx;
int setup_bad(Ctx *ctx) {
ctx->log = fopen("/tmp/app.log", "w");
if (!ctx->log) return -1;
ctx->buf1 = malloc(1024);
if (!ctx->buf1) return -1; // LEAK: ctx->log is still open, never fclose'd
ctx->buf2 = malloc(2048);
if (!ctx->buf2) return -1; // LEAK: ctx->log AND ctx->buf1 both leak here
return 0;
}
// GOOD — Section 4.2's single forward goto to one cleanup label
int setup_good(Ctx *ctx) {
int rc = -1;
ctx->log = NULL; ctx->buf1 = NULL; ctx->buf2 = NULL;
ctx->log = fopen("/tmp/app.log", "w");
if (!ctx->log) goto cleanup;
ctx->buf1 = malloc(1024);
if (!ctx->buf1) goto cleanup;
ctx->buf2 = malloc(2048);
if (!ctx->buf2) goto cleanup;
rc = 0;
cleanup:
if (rc != 0) {
free(ctx->buf1); // free(NULL) is always safe (Section 10.3) —
free(ctx->buf2); // no need to check which ones actually got allocated
if (ctx->log) fclose(ctx->log);
}
return rc;
}
Every resource this function might hold gets exactly one release path,
reached from every failure point — this is why Section 4.2 introduced
goto cleanup as idiomatic C rather than an anti-pattern: the
alternative (repeating the right subset of cleanup calls before every
return) is what actually produces error-path leaks, because it's easy
to add a fourth resource later and forget to update three of the four
early-return blocks.
44.2 The lost-reference leak
// BAD
void lost_reference_bad(void) {
char *buf = malloc(100);
buf = malloc(200); // the 100-byte block's address is gone — nothing points to it anymore,
free(buf); // so it can never be freed for the rest of the program's life
}
// GOOD
void lost_reference_good(void) {
char *buf = malloc(100);
free(buf); // release it BEFORE the variable that names it is reassigned
buf = malloc(200);
free(buf);
}
This is the leak hiding behind innocuous-looking code like
result = process(result) where process allocates a new buffer and
the caller forgot the old result needed freeing first — very common
in string-building loops (s = concat(s, next_word), repeated).
44.3 The partial-cleanup leak in an array of allocations
// BAD
char **alloc_rows_bad(int n, int width) {
char **rows = malloc((size_t)n * sizeof(char *));
for (int i = 0; i < n; i++) {
rows[i] = malloc((size_t)width);
if (!rows[i]) {
free(rows); // LEAK: rows[0..i-1] were allocated and are now unreachable —
return NULL; // freeing the array of POINTERS did not free what they pointed to
}
}
return rows;
}
// GOOD
char **alloc_rows_good(int n, int width) {
char **rows = malloc((size_t)n * sizeof(char *));
if (!rows) return NULL;
for (int i = 0; i < n; i++) {
rows[i] = malloc((size_t)width);
if (!rows[i]) {
for (int j = 0; j < i; j++) free(rows[j]); // unwind exactly what succeeded so far
free(rows);
return NULL;
}
}
return rows;
}
This is the array version of 44.1's error-path leak, and it generalizes to any structure built incrementally: a linked list built node-by-node, a hash table resized bucket-by-bucket — the rule is always "the cleanup path must free every element that succeeded before the one that failed, not just the outer container."
44.4 The realloc-failure leak (revisiting Section 10.1)
// BAD
void grow_bad(char **buf, size_t new_size) {
*buf = realloc(*buf, new_size); // if realloc fails, it returns NULL — and the ORIGINAL
} // block's address, which *buf held, is now overwritten
// and gone: a leak, not just "buf is now NULL"
// GOOD
int grow_good(char **buf, size_t new_size) {
char *tmp = realloc(*buf, new_size);
if (!tmp) return -1; // *buf is untouched and still valid — free it if the caller gives up,
*buf = tmp; // or keep using the old, smaller buffer and try again later
return 0;
}
44.5 Leaks aren't just malloc — every acquire needs a matching release
// BAD — leaks a file descriptor and a mutex lock, not "memory," but the
// same resource-lifetime discipline applies
int read_config_bad(const char *path, pthread_mutex_t *lock) {
pthread_mutex_lock(lock);
FILE *f = fopen(path, "r");
if (!f) return -1; // LEAK: lock is never unlocked — every future
// caller of this function deadlocks forever
// ... read ...
fclose(f);
pthread_mutex_unlock(lock);
return 0;
}
A held mutex that's never unlocked, a socket that's never closed, a
mmaped region that's never munmapped — all are "leaks" in the sense
that matters: a finite resource acquired and never released. The
ownership discipline from Section 10.3 (one _new for one _free, one
lock for one unlock, one open for one close) is the same fix for
all of them.
44.6 Finding leaks you already wrote
# Linux: the standard leak-triage tool, no recompilation needed
valgrind --leak-check=full --show-leak-kinds=all ./app
# Clang/GCC: LeakSanitizer, built into ASan on Linux (bundle with -fsanitize=address)
clang -std=gnu17 -g -fsanitize=address -o app app.c
ASAN_OPTIONS=detect_leaks=1 ./app # prints each leak's allocation stack trace on exit
Valgrind's leak-kind vocabulary is worth knowing: definitely lost
(nothing in the program points to this block anymore — a real leak,
exactly Sections 44.1–44.3), still reachable (a global/static pointer
still points to it at exit — usually fine, e.g. a cache freed by process
exit instead of explicit code), and possibly lost (a pointer to the
interior of the block still exists, but not to its start — often a
custom allocator or intrusive-pointer pattern, worth a second look).