C course menu
Common Memory Errors in C: Leaks, Dangling Pointers, Double Free and Buffer Overflows
Recognise and prevent the most common C memory bugs: memory leaks, use after free, double free, buffer overflows, uninitialised memory, and freeing memory you did not allocate, plus tools such as AddressSanitizer and Valgrind that find them.
On this page
Manual memory management gives C its speed and control, and it is also the source of its most serious bugs. Many security vulnerabilities in C programs come from the mistakes in this lesson. Most of them are undefined behavior: the program may seem to work, crash, or quietly corrupt data, and it can behave differently on another machine.
The broken examples below are not run by our checker, because their behavior is not predictable. Each one is followed by a fix.
1. Memory leaks#
A leak happens when you lose the last pointer to a block without freeing it:
void process(void) {
char *buffer = malloc(1024);
if (buffer == NULL) return;
if (some_error()) {
return; /* leak: buffer is never freed */
}
free(buffer);
}A short program's leaks are reclaimed when it exits, but a long-running server that leaks a little on every request eventually runs out of memory.
Fix: free on every path. A single exit point makes this easier:
int process(void) {
int result = -1;
char *buffer = malloc(1024);
if (buffer == NULL) return -1;
if (some_error()) goto cleanup;
/* ... work ... */
result = 0;
cleanup:
free(buffer);
return result;
}Using goto for cleanup is a widely accepted C idiom, including in the Linux kernel.
2. Use after free (dangling pointers)#
After free(p), the memory may be reused for something else, but p still holds the old address:
int *p = malloc(sizeof *p);
*p = 42;
free(p);
printf("%d\n", *p); /* use after free: undefined behavior */Fix: set the pointer to NULL straight after freeing it, and do not keep other copies of the pointer that outlive the block.
3. Double free#
Freeing the same block twice corrupts the allocator's bookkeeping and often crashes the program:
char *s = malloc(16);
free(s);
free(s); /* double free */Fix: decide clearly which part of the program owns each block and is responsible for freeing it. Setting pointers to NULL after free also helps, because free(NULL) is harmless.
4. Buffer overflows#
Writing past the end of an array overwrites whatever is next in memory:
char name[8];
strcpy(name, "Christopher"); /* 12 bytes into an 8-byte array */Fix: use functions that take the buffer size, and check lengths:
#include <stdio.h>
int main(void) {
char name[8];
int needed = snprintf(name, sizeof name, "%s", "Christopher");
printf("[%s]", name);
if (needed >= (int)sizeof name) printf(" truncated, needed %d chars", needed);
printf("\n");
return 0;
}[Christo] truncated, needed 11 chars5. Uninitialised memory#
Local variables and malloced memory start with whatever bytes happened to be there:
int total; /* garbage */
for (int i = 0; i < 3; i++) total += i;Fix: initialise every variable (int total = 0;) and use calloc when you need zeroed memory. -Wall -Wextra catches many of these cases.
6. Freeing what was not allocated#
Only pass free a pointer that came from malloc, calloc or realloc, and only the start of the block:
int arr[10];
free(arr); /* not from malloc */
int *p = malloc(10 * sizeof *p);
free(p + 1); /* not the start of the block */Tools that find these bugs#
You do not have to find memory bugs by reading code alone:
- AddressSanitizer: compile with
gcc -g -fsanitize=address(supported by GCC and Clang on Linux and macOS). The program stops at the first invalid access and reports the line. - Valgrind: run
valgrind --leak-check=full ./programon Linux to report invalid reads and writes and every leaked block. - Compiler warnings:
-Wall -Wextrawarns about many uninitialised variables and some overflows at compile time.
Practice#
Practice exercise
The function below is supposed to return a copy of s in uppercase. Find the two memory bugs, then write a correct version.
char *upper_copy(const char *s) {
char *out = malloc(strlen(s));
for (size_t i = 0; i <= strlen(s); i++) out[i] = toupper(s[i]);
return out;
}Show solution
- It allocates
strlen(s)bytes, with no room for'\0', but the loop writesstrlen(s) + 1bytes: a buffer overflow. - It never checks whether
mallocreturnedNULL.
(It also passes a char to toupper without the unsigned char cast.)
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char *upper_copy(const char *s) {
size_t len = strlen(s);
char *out = malloc(len + 1);
if (out == NULL) return NULL;
for (size_t i = 0; i < len; i++) out[i] = (char)toupper((unsigned char)s[i]);
out[len] = '\0';
return out;
}
int main(void) {
char *u = upper_copy("memory safe");
if (u == NULL) return 1;
printf("%s\n", u);
free(u);
return 0;
}MEMORY SAFEKey takeaway#
Finished this lesson?