Expired Pointer Dereference
Description
Expired Pointer Dereference is a memory safety vulnerability where software dereferences a pointer that points to a memory location that was previously valid but is no longer valid. This typically occurs when memory is freed but a pointer to that location (a "dangling pointer") is retained and later used. Once memory is released, it may be reallocated for a different purpose. Accessing through the original pointer can cause the program to read or modify data belonging to another function or process, leading to unpredictable behavior, denial of service, information exposure, or code execution.
Risk
This vulnerability is extremely dangerous because the freed memory often gets reused. Attackers can craft inputs that cause specific memory allocation patterns, allowing them to control what data occupies the previously freed location. When the dangling pointer is later dereferenced, it accesses attacker-controlled data. If the pointer was used for function calls, attackers can redirect execution to arbitrary code. Use-after-free vulnerabilities are among the most commonly exploited memory corruption issues in modern software. The timing and reallocation behavior make exploitation challenging to prevent through simple fixes.
Solution
Set pointers to NULL immediately after freeing the memory they reference, then always check for NULL before dereferencing. However, this is not a complete solution when multiple pointers reference the same memory. Use smart pointers in C++ (unique_ptr, shared_ptr) that automatically manage lifetimes and prevent dangling references. Prefer languages with automatic garbage collection where feasible. Use static analysis tools to detect potential use-after-free patterns. Enable AddressSanitizer during development and testing to catch expired pointer dereferences at runtime. Design data structures to clearly track ownership and lifetime of allocated memory. Avoid storing pointers in multiple locations when possible.
Common Consequences
| Impact | Details |
|---|---|
| Confidentiality | Scope: Confidentiality Read Memory - If the expired pointer is used for reading, attackers may observe data that was placed in the reallocated memory by other code. |
| Availability | Scope: Availability DoS: Crash, Exit, or Restart - Accessing freed memory can cause crashes or corruption leading to system instability. |
| Integrity, Confidentiality, Availability | Scope: Integrity, Confidentiality, Availability Execute Unauthorized Code or Commands - If the pointer is used in function calls or writes, attackers may achieve arbitrary code execution. |
Example Code
Vulnerable Code
// Vulnerable: Use after free
#define SIZE 1024
void vulnerable_use_after_free(int err) {
char* ptr = (char*)malloc(SIZE);
int abrt = 0;
if (err) {
abrt = 1;
free(ptr); // Memory freed
}
// ... more code ...
if (abrt) {
// Vulnerable: ptr was freed but still used
logError("operation aborted before commit", ptr);
}
}
// Vulnerable: Double free
void vulnerable_double_free(int condition) {
char* ptr = (char*)malloc(SIZE);
// ... use ptr ...
if (condition) {
free(ptr); // First free
}
// ... more code ...
free(ptr); // Vulnerable: Second free of same pointer
}
// Vulnerable: Dangling pointer in data structure
struct list_node {
int data;
struct list_node *next;
};
void vulnerable_remove_node(struct list_node **head, int value) {
struct list_node *prev = NULL;
struct list_node *curr = *head;
while (curr != NULL) {
if (curr->data == value) {
if (prev) {
prev->next = curr->next;
} else {
*head = curr->next;
}
free(curr); // Memory freed
// Vulnerable: curr still points to freed memory
break;
}
prev = curr;
curr = curr->next;
}
// Later use of curr would be dangerous
printf("Removed: %d\n", curr->data); // Use after free!
}
// Vulnerable: Freed pointer escapes function
char* vulnerable_return_freed(void) {
char *buffer = malloc(256);
strcpy(buffer, "temporary data");
process_data(buffer);
free(buffer); // Memory freed
// Vulnerable: Returns pointer to freed memory
return buffer;
}
// Vulnerable: Iterator invalidation
#include <vector>
void vulnerable_iterator(std::vector<int>& vec) {
auto it = vec.begin();
// Store iterator
auto saved_it = it;
// This may reallocate, invalidating all iterators
vec.push_back(100);
// Vulnerable: saved_it may now be invalid
*saved_it = 42; // Undefined behavior
}
// Vulnerable: Pointer aliasing after free
void vulnerable_alias(void) {
char *original = malloc(100);
char *alias = original; // Both point to same memory
strcpy(original, "sensitive data");
free(original);
original = NULL; // Only original is NULLed
// Vulnerable: alias still points to freed memory
printf("Data: %s\n", alias); // Use after free via alias
}
Fixed Code
// Fixed: Set pointer to NULL after free and check before use
#define SIZE 1024
void fixed_use_after_free(int err) {
char* ptr = (char*)malloc(SIZE);
int abrt = 0;
if (err) {
abrt = 1;
free(ptr);
ptr = NULL; // Fixed: Set to NULL after free
}
if (abrt) {
// Fixed: Check before use
if (ptr != NULL) {
logError("operation aborted before commit", ptr);
} else {
logError("operation aborted before commit", "(no data)");
}
}
// Clean up if not already freed
if (ptr != NULL) {
free(ptr);
}
}
// Fixed: Track allocation state
void fixed_double_free(int condition) {
char* ptr = (char*)malloc(SIZE);
int ptr_freed = 0;
if (condition) {
free(ptr);
ptr = NULL;
ptr_freed = 1;
}
// Fixed: Only free if not already freed
if (!ptr_freed && ptr != NULL) {
free(ptr);
}
}
// Fixed: Don't use pointer after free
struct list_node {
int data;
struct list_node *next;
};
int fixed_remove_node(struct list_node **head, int value) {
struct list_node *prev = NULL;
struct list_node *curr = *head;
int removed_value = 0;
int found = 0;
while (curr != NULL) {
if (curr->data == value) {
removed_value = curr->data; // Save before free
found = 1;
if (prev) {
prev->next = curr->next;
} else {
*head = curr->next;
}
free(curr);
curr = NULL; // Fixed: Set to NULL immediately
break;
}
prev = curr;
curr = curr->next;
}
if (found) {
printf("Removed: %d\n", removed_value);
}
return found;
}
// Fixed: Don't return freed pointers
char* fixed_no_return_freed(void) {
char *buffer = malloc(256);
if (!buffer) return NULL;
strcpy(buffer, "temporary data");
process_data(buffer);
// Fixed: Either return the buffer (caller must free)
// or don't return it at all
return buffer; // Caller responsible for freeing
}
// Or copy the result before freeing
int fixed_copy_before_free(char *output, size_t output_size) {
char *buffer = malloc(256);
if (!buffer) return -1;
strcpy(buffer, "temporary data");
process_data(buffer);
// Fixed: Copy result to caller's buffer before freeing
strncpy(output, buffer, output_size - 1);
output[output_size - 1] = '\0';
free(buffer);
return 0;
}
// Fixed: Use smart pointers
#include <vector>
#include <memory>
void fixed_smart_pointer() {
// unique_ptr automatically manages lifetime
auto ptr = std::make_unique<std::string>("data");
// No manual free needed - automatically freed when out of scope
// Cannot accidentally use after destruction
}
// Fixed: Be aware of iterator invalidation
void fixed_iterator(std::vector<int>& vec) {
// Reserve space to avoid reallocation
vec.reserve(vec.size() + 10);
auto it = vec.begin();
vec.push_back(100); // Safe if reserved
// Or: don't save iterators across potential invalidation
vec.push_back(100);
auto it2 = vec.begin(); // Get fresh iterator
*it2 = 42; // Safe
}
// Fixed: Handle all aliases
void fixed_alias(void) {
char *original = malloc(100);
char *alias = original;
strcpy(original, "sensitive data");
// Fixed: Clear all aliases before freeing
alias = NULL;
free(original);
original = NULL;
// Now neither pointer can be misused
}
// Better: Use a wrapper that tracks references
typedef struct {
char *data;
int refcount;
} RefCountedBuffer;
RefCountedBuffer* create_buffer(size_t size) {
RefCountedBuffer *buf = malloc(sizeof(RefCountedBuffer));
buf->data = malloc(size);
buf->refcount = 1;
return buf;
}
void release_buffer(RefCountedBuffer **buf) {
if (buf && *buf) {
(*buf)->refcount--;
if ((*buf)->refcount == 0) {
free((*buf)->data);
free(*buf);
}
*buf = NULL;
}
}
Related CWEs
- CWE-672: Operation on a Resource after Expiration or Release (parent)
- CWE-416: Use After Free (child - specific variant)
- CWE-415: Double Free (child - specific variant)
- CWE-119: Improper Restriction of Operations within Bounds of Memory Buffer (related)
- CWE-125: Out-of-bounds Read (can follow)
- CWE-787: Out-of-bounds Write (can follow)
References
- MITRE Corporation. "CWE-825: Expired Pointer Dereference." https://cwe.mitre.org/data/definitions/825.html
- CERT C Secure Coding Standard. "MEM30-C. Do not access freed memory."
- OWASP. "Using Freed Memory." https://owasp.org/www-community/vulnerabilities/Using_freed_memory