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.

Bash
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);
    }
}

Bash
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 cycle

How Leaks Happen

Bash
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?

Bash
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 stability

Leaks Aren’t Just malloc — “Hidden Leaks” in Production

Bash
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 forever

Detecting Memory Leaks

Bash
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 manually

Prevention

Bash
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
Last modified: 2024年10月6日

Author

Comments

Write a Reply or Comment

Your email address will not be published.