Missing Release of Memory after Effective Lifetime (Memory Leak)
Description
Missing Release of Memory after Effective Lifetime occurs when a program allocates memory but fails to release it after it is no longer needed. This leads to memory leaks where the program's memory usage grows continuously. In long-running applications, memory leaks can eventually exhaust available memory, causing crashes, system instability, or denial of service. Memory leaks are particularly dangerous in servers, daemons, and embedded systems where applications run continuously.
Risk
Memory leaks are a significant reliability and availability concern. In web servers, each request leaking memory can eventually crash the server. In embedded systems with limited memory, leaks quickly become critical. Memory exhaustion can lead to denial of service. Leaks may also mask other vulnerabilities—memory corruption might not crash immediately but only when memory becomes fragmented. Modern systems with large amounts of RAM may hide leaks until they suddenly become critical.
Solution
Use memory management tools (Valgrind, AddressSanitizer) during development and testing. Implement consistent allocation/deallocation patterns. Use RAII (Resource Acquisition Is Initialization) in C++ with smart pointers. Consider garbage-collected languages for complex applications. Implement reference counting for shared resources. Use static analysis tools to detect leaks. Monitor production systems for memory growth. Implement proper error handling that cleans up allocations on failure paths.
Common Consequences
| Impact | Details |
|---|---|
| Availability | Scope: Denial of Service Continuous memory leaks eventually exhaust available memory, crashing the application. |
| Performance | Scope: Degradation Memory pressure causes swapping, garbage collection pressure, and overall slowdown. |
| Reliability | Scope: System Instability Memory exhaustion can affect other processes on the same system. |
Example Code + Solution Code
Vulnerable Code
// VULNERABLE: Memory allocated but never freed
void process_request(const char *data) {
char *buffer = malloc(1024);
strcpy(buffer, data);
process(buffer);
// Missing: free(buffer);
// Memory leaks on every call!
}
// VULNERABLE: Early return without freeing
int read_config(const char *filename) {
FILE *fp = fopen(filename, "r");
if (!fp) return -1;
char *buffer = malloc(4096);
if (!buffer) {
fclose(fp);
return -1;
}
if (fread(buffer, 1, 4096, fp) == 0) {
fclose(fp);
return -1; // Leaks buffer!
}
// Process config...
free(buffer);
fclose(fp);
return 0;
}
// VULNERABLE: Lost pointer
void create_list(void) {
Node *head = malloc(sizeof(Node));
head->next = malloc(sizeof(Node));
head->next->next = malloc(sizeof(Node));
// Reassign head - lose reference to allocated memory!
head = malloc(sizeof(Node)); // Original chain leaked!
}
// VULNERABLE: Exception-like path leaks
int complex_operation(void) {
void *resource1 = malloc(100);
void *resource2 = malloc(200);
void *resource3 = malloc(300);
if (step1(resource1) != 0) {
free(resource1);
return -1; // Leaks resource2, resource3!
}
if (step2(resource2) != 0) {
free(resource1);
free(resource2);
return -1; // Leaks resource3!
}
// ... continue ...
free(resource1);
free(resource2);
free(resource3);
return 0;
}
// VULNERABLE: C++ raw pointers in exception path
class DataProcessor {
int* data;
public:
DataProcessor() {
data = new int[1000];
}
void process() {
// If exception thrown, destructor not called for local objects
DataProcessor* temp = new DataProcessor();
throw std::runtime_error("Error"); // temp leaked!
delete temp;
}
~DataProcessor() {
delete[] data;
}
};
// VULNERABLE: Container of raw pointers
void process_items() {
std::vector<Item*> items;
for (int i = 0; i < 100; i++) {
items.push_back(new Item());
}
// Vector destroyed but Items not deleted - all leaked!
}
# VULNERABLE: Python circular reference (GC usually handles, but not always)
class Node:
def __init__(self):
self.ref = None
self.data = bytearray(1024 * 1024) # 1MB
def create_cycle():
a = Node()
b = Node()
a.ref = b
b.ref = a # Circular reference
# With custom __del__, garbage collector may not collect
# VULNERABLE: File handles not closed (resource leak)
def read_files(filenames):
contents = []
for fname in filenames:
f = open(fname, 'r') # Never closed!
contents.append(f.read())
return contents
Fixed Code
// SAFE: Always free allocated memory
void process_request_safe(const char *data) {
char *buffer = malloc(1024);
if (!buffer) return;
strcpy(buffer, data);
process(buffer);
free(buffer); // Always free
}
// SAFE: Single cleanup path
int read_config_safe(const char *filename) {
FILE *fp = NULL;
char *buffer = NULL;
int result = -1;
fp = fopen(filename, "r");
if (!fp) goto cleanup;
buffer = malloc(4096);
if (!buffer) goto cleanup;
if (fread(buffer, 1, 4096, fp) == 0) {
goto cleanup; // Single cleanup path
}
// Process config...
result = 0;
cleanup:
free(buffer); // free(NULL) is safe
if (fp) fclose(fp);
return result;
}
// SAFE: Track all allocations
void create_list_safe(void) {
Node *head = malloc(sizeof(Node));
if (!head) return;
head->next = malloc(sizeof(Node));
if (!head->next) {
free(head);
return;
}
head->next->next = malloc(sizeof(Node));
if (!head->next->next) {
free(head->next);
free(head);
return;
}
// Use the list...
// Free entire list
free(head->next->next);
free(head->next);
free(head);
}
// SAFE: Consistent cleanup pattern
typedef struct {
void *resource1;
void *resource2;
void *resource3;
} Resources;
void cleanup_resources(Resources *r) {
free(r->resource1);
free(r->resource2);
free(r->resource3);
memset(r, 0, sizeof(*r));
}
int complex_operation_safe(void) {
Resources r = {0};
r.resource1 = malloc(100);
r.resource2 = malloc(200);
r.resource3 = malloc(300);
if (!r.resource1 || !r.resource2 || !r.resource3) {
cleanup_resources(&r);
return -1;
}
if (step1(r.resource1) != 0) {
cleanup_resources(&r);
return -1;
}
if (step2(r.resource2) != 0) {
cleanup_resources(&r);
return -1;
}
// Success path also cleans up
cleanup_resources(&r);
return 0;
}
// SAFE: Use smart pointers
#include <memory>
#include <vector>
class DataProcessorSafe {
std::unique_ptr<int[]> data;
public:
DataProcessorSafe() : data(std::make_unique<int[]>(1000)) {}
void process() {
// Smart pointer handles cleanup even with exceptions
auto temp = std::make_unique<DataProcessorSafe>();
throw std::runtime_error("Error"); // temp automatically cleaned up!
}
// Destructor not needed - unique_ptr handles it
};
// SAFE: Container of smart pointers
void process_items_safe() {
std::vector<std::unique_ptr<Item>> items;
for (int i = 0; i < 100; i++) {
items.push_back(std::make_unique<Item>());
}
// Vector destroyed - all Items automatically deleted!
}
// SAFE: RAII wrapper for C resources
class FileHandle {
FILE* fp;
public:
explicit FileHandle(const char* name, const char* mode)
: fp(fopen(name, mode)) {
if (!fp) throw std::runtime_error("Cannot open file");
}
~FileHandle() {
if (fp) fclose(fp);
}
// Delete copy
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
// Allow move
FileHandle(FileHandle&& other) noexcept : fp(other.fp) {
other.fp = nullptr;
}
FILE* get() { return fp; }
};
// Usage - automatic cleanup
void process_file(const char* filename) {
FileHandle fh(filename, "r");
// Use fh.get()...
// Automatically closed when fh goes out of scope
}
# SAFE: Use context managers
def read_files_safe(filenames):
contents = []
for fname in filenames:
with open(fname, 'r') as f: # Auto-closes
contents.append(f.read())
return contents
# SAFE: Weak references to break cycles
import weakref
class Node:
def __init__(self):
self._ref = None
self.data = bytearray(1024 * 1024)
@property
def ref(self):
return self._ref() if self._ref else None
@ref.setter
def ref(self, value):
self._ref = weakref.ref(value) if value else None
def create_no_cycle():
a = Node()
b = Node()
a.ref = b # Normal reference
b.ref = a # Weak reference - won't prevent collection
# SAFE: Explicit cleanup
class ManagedResource:
def __init__(self):
self.resource = allocate_large_resource()
def __enter__(self):
return self
def __exit__(self, *args):
self.cleanup()
def cleanup(self):
if self.resource:
release_resource(self.resource)
self.resource = None
# Usage
with ManagedResource() as r:
use_resource(r)
# Automatically cleaned up
Exploited in the Wild
Microsoft Windows LSASS Memory Leak (2003)
Memory leaks in LSASS (Local Security Authority Subsystem Service) could be triggered remotely, eventually causing system crashes and denial of service.
OpenSSL Memory Leak DoS (2016)
CVE-2016-6304 was a memory leak in OpenSSL's OCSP response handling that could be exploited for denial of service by sending repeated status requests.
Firefox Memory Leaks
Multiple memory leaks in Firefox have been reported over the years, causing browser memory usage to grow significantly during long browsing sessions.
Tools to test/exploit
-
Valgrind — comprehensive memory leak detection.
-
AddressSanitizer — fast memory error detector with leak checking.
-
LeakSanitizer — standalone leak detector.
-
Visual Studio Memory Diagnostics — Windows memory profiling.
CVE Examples
-
CVE-2016-6304 — OpenSSL OCSP memory leak DoS.
-
CVE-2019-1559 — OpenSSL memory leak in padding oracle.
-
CVE-2017-7668 — Apache HTTP Server memory leak.
References
-
MITRE. "CWE-401: Missing Release of Memory after Effective Lifetime." https://cwe.mitre.org/data/definitions/401.html
-
CERT. "MEM31-C: Free dynamically allocated memory when no longer needed." https://wiki.sei.cmu.edu/confluence/display/c/MEM31-C.+Free+dynamically+allocated+memory+when+no+longer+needed