K09 — Memory Leaks: Why malloc Without free Causes Unrecoverable Memory
Key Insight: After a program ends, the system reclaims its memory — but while running, leaked memory is truly leaked.
What Is a Memory Leak
A memory leak occurs when a program dynamically allocates memory but fails to release it, making that memory permanently unavailable for reuse.
Typical example of a memory leak:
void leak() {
char *buf = malloc(1024);
if (some_condition) {
return; // Early return, buf is not freed
}
free(buf); // Normal path releases
}
void real_leak() {
while (1) {
char *buf = malloc(1024);
// Forgot free(buf);
// Each iteration leaks 1024 bytes
sleep(1);
}
}Classification of memory leaks:
1. True Leak:
- Pointer is lost, memory becomes inaccessible
- Memory cannot be freed by any code
- Must restart the program to reclaim it
2. Logical Leak:
- Pointer still exists but is no longer used
- Memory is still reachable but is garbage
- Can be avoided with code discipline (free after use)
3. Indirect Leak (Circular Reference):
- A holds a reference to B, and B holds a reference to A
- Forms a circular reference with no external reference chain
- Particularly problematic in modern GC languages
- C++ can use weak_ptr to break the cycleHow Leaks Happen
Common memory leak scenarios:
1. Early return:
if (condition) return; // Not freed
...
free(buf);
2. Error handling:
if (!buf) return ERROR;
buf = malloc(...);
if (error) return ERROR; // buf not freed
...
free(buf);
3. Accumulation in loops:
while (process()) {
char *tmp = malloc(1024);
do_something(tmp);
// Forgot free(tmp)
}
4. Missing error path cleanup:
int init() {
if (!(p = malloc(...))) return -1;
if (!(q = malloc(...))) { free(p); return -1; }
return 0;
}Is Memory Reclaimed After a Process Exits?
After a process exits, memory is reclaimed by the system:
- When a process exits, all open file descriptors are closed
- All memory mappings (mmap/VMA) are released
- All physical pages are returned to the buddy system
- All memory held by the process is fully reclaimed
Therefore:
- Leaked memory IS reclaimed after process exit
- But if the process is long-running (server, daemon),
leaks accumulate until OOM
Impact of leaks:
- Short-lived programs (scripts, batch jobs): minimal impact, reclaimed on exit
- Long-lived programs (servers, daemons): leaks accumulate, eventual OOM
- Memory leaks are a major enemy of server stabilityLeaks Aren’t Just malloc — “Hidden Leaks” in Production
Beyond malloc, many leaks go unnoticed:
1. File descriptor leaks:
while (1) {
int fd = open("/dev/null", O_RDONLY);
// Forgot close(fd)
process();
}
// Each iteration leaks one fd
// After exceeding ulimit -n, cannot open more files
Signal: Too many open files → program crash
2. mmap leaks:
void leak_mmap() {
for (int i = 0; i < 1000; i++) {
mmap(NULL, 4096, PROT_READ | PROT_WRITE,
MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
}
// All mappings still exist!
// Forgot to munmap
}
// /proc/PID/maps will show thousands of entries
// Virtual memory exhausted → allocation failure
3. Thread stack leaks:
for (int i = 0; i < 1000; i++) {
pthread_t thread;
pthread_create(&thread, NULL, worker, NULL);
// Forgot pthread_detach or pthread_join
// Thread stack (~8MB) persists until join
}
// If threads are not joined, their thread stacks remain foreverDetecting Memory Leaks
Tools for detecting memory leaks:
1. Valgrind (memcheck):
$ valgrind --leak-check=full ./my_program
==12345== HEAP SUMMARY:
==12345== in use at exit: 1,024 bytes in 1 blocks
==12345== total heap usage: 10 allocs, 9 frees, 10,240 bytes
==12345==
==12345== 1,024 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345== at 0x4C2B800: malloc (vg_replace_malloc.c:309)
==12345== by 0x4005E7: main (leak.c:5)
2. AddressSanitizer (ASan):
$ gcc -fsanitize=address -g -o prog prog.c
$ ./prog
=================================================================
==12345==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 1024 byte(s) in 1 object(s)
3. GDB / Manual inspection:
$ gdb ./prog
(gdb) b main
(gdb) r
(gdb) info registers
Tracking memory manuallyPrevention
Best practices to prevent memory leaks:
1. Use RAII (C++):
- std::unique_ptr, std::shared_ptr
- Containers (std::vector automatically calls destructor)
2. Use goto for cleanup (C):
void func() {
char *buf1 = malloc(1024);
char *buf2 = malloc(2048);
if (!buf1 || !buf2) goto cleanup;
// Use buffers...
cleanup:
free(buf1);
free(buf2);
}
3. Use defer pattern (GCC extension):
void func() {
char *buf __attribute__((cleanup(free))) = malloc(1024);
// Automatically freed on any exit
}
4. Tools: Enable ASan in debug builds
5. Code review: Check malloc/free pairs
Comments