Memory Bugs and How to Find Them

Manual memory management brings a family of bugs that other languages prevent: writing past the end of a buffer, using memory after freeing it, freeing twice, and leaking. They can corrupt data silently and are behind many security vulnerabilities. Learn to recognise them — and let tools find them for you.

The usual suspects

BugWhat happens
Buffer overflowReading or writing outside an array or allocated block
Use after freeUsing memory through a pointer after it was freed
Double freeFreeing the same block twice
Memory leakNever freeing memory you no longer need
Uninitialised readReading a variable or malloced memory before writing it
Dangling stack pointerUsing the address of a local variable after its function returned
Null dereferenceUsing a pointer that is NULL

All of them are undefined behaviour. The frustrating part is that the program often seems to work — until different input, a different compiler or an optimisation level makes it crash or produce wrong results.

AddressSanitizer

GCC and Clang include AddressSanitizer (ASan), which adds checks around every memory access while you test. Compile with:

$
gcc -g -fsanitize=address -fno-omit-frame-pointer main.c -o main
./main

When something goes wrong, ASan stops the program and prints a detailed report: the kind of error, the exact line, and where the memory was allocated and freed. The full report includes memory addresses that differ every run; the examples below show the program's output and the report's final SUMMARY line.

An off-by-one overflow

C
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *marks = malloc(3 * sizeof *marks);
    if (marks == NULL) return 1;
    for (int i = 0; i <= 3; i++) {     // <= should be <
        marks[i] = 50 + i;
    }
    printf("%d\n", marks[0]);
    free(marks);
    return 0;
}
With AddressSanitizer
SUMMARY: AddressSanitizer: heap-buffer-overflow main.c:8 in main

Without the sanitizer this program usually runs and prints 50 — the bug would go unnoticed. ASan catches the write to marks[3] on line 8.

Use after free

C
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void) {
    char *name = malloc(16);
    if (name == NULL) return 1;
    strcpy(name, "temporary");
    free(name);
    printf("%s\n", name);       // the memory no longer belongs to us
    return 0;
}
With AddressSanitizer
SUMMARY: AddressSanitizer: heap-use-after-free main.c:10 in main

Setting a pointer to NULL right after freeing it turns this kind of bug into an immediate, obvious crash instead of silent corruption.

Double free

C
#include <stdlib.h>

int main(void) {
    int *data = malloc(10 * sizeof *data);
    free(data);
    free(data);
    return 0;
}
With AddressSanitizer
SUMMARY: AddressSanitizer: double-free main.c:6 in main

Stack buffer overflow

C
#include <stdio.h>
#include <string.h>

int main(void) {
    char code[8];
    const char *input = "ABCD-1234-XYZ";
    strcpy(code, input);        // 13 characters + '\0' into 8 bytes
    printf("%s\n", code);
    return 0;
}
With AddressSanitizer
SUMMARY: AddressSanitizer: stack-buffer-overflow main.c:7 in main

This is the classic security hole: input longer than the buffer overwrites neighbouring memory. The fix is to check lengths or use a size-limited function such as snprintf.

Finding leaks

On Linux, AddressSanitizer also reports leaks when the program exits (LeakSanitizer). On macOS, use the leaks command (leaks --atExit -- ./main). Valgrind (valgrind --leak-check=full ./main) is another popular Linux tool that finds leaks and uninitialised reads without recompiling.

Other sanitizers

OptionFinds
-fsanitize=addressOverflows, use after free, double free (and leaks on Linux)
-fsanitize=undefinedSigned overflow, bad shifts, misaligned access and other undefined behaviour (lesson 54)
-fsanitize=memory (Clang, Linux)Reads of uninitialised memory
-fsanitize=threadData races in multithreaded code

Sanitized programs run slower and use more memory, so use them for development and testing, not release builds. Running your test suite under ASan and UBSan regularly catches most memory bugs long before users do.

Habits that prevent memory bugs

  • Compile with -Wall -Wextra and fix every warning.
  • Initialise every variable and pointer; set pointers to NULL after free.
  • Keep each array's length next to it, and loop with i < length.
  • Use size-limited functions (snprintf, fgets) and check lengths before copying.
  • Give every allocation one clear owner who frees it, and free in the reverse order of allocation.
  • Test with sanitizers on.

Try it yourself

This function has a subtle bug: it allocates one byte too few for the copy. Spot it, then compile the program with -fsanitize=address to confirm, and fix it.

Try it yourself · main.c
Edit the code, copy it into main.c, then compile and run it with cc -std=c17 -Wall main.c -o main && ./main. How to set up a C compiler
Output
MEMORY MATTERS