5 min read
On this page

Memory Bugs & Tools

Why Memory Bugs Matter

Memory bugs are the number one source of security vulnerabilities in C and C++ programs. Buffer overflows, use-after-free, and double free bugs account for the majority of CVEs in browsers, operating systems, and network services. These bugs are difficult to find through code review alone because they often depend on runtime conditions.

Categories of Memory Bugs

Use-After-Free

Accessing memory that has already been freed:

int *p = malloc(sizeof(int));
*p = 42;
free(p);
printf("%d\n", *p);  /* undefined behavior: use-after-free */

The memory may have been reallocated for something else. Reading it returns garbage; writing it corrupts unrelated data.

Double Free

Freeing the same pointer twice:

char *buf = malloc(100);
free(buf);
free(buf);  /* undefined behavior: double free */

This corrupts the heap allocator's internal data structures and can be exploited by attackers.

Memory Leak

Allocating memory and never freeing it:

void leaky(void) {
    char *buf = malloc(1024);
    /* function returns without freeing buf */
}

In long-running programs (servers, daemons), leaks accumulate until the process runs out of memory.

Buffer Overflow

Writing past the end of an allocated buffer:

char *buf = malloc(10);
strcpy(buf, "this string is way too long for the buffer");
/* writes past the end of buf, corrupting adjacent heap data */

Uninitialized Read

Reading memory that was allocated but never written:

int *p = malloc(sizeof(int));
printf("%d\n", *p);  /* undefined behavior: uninitialized read */

The value is whatever was previously in that memory.

Valgrind

Valgrind is the most widely used memory debugging tool. It runs your program in a virtual machine that tracks every memory access.

Basic Usage

$ gcc -g -O0 -o myapp myapp.c
$ valgrind --leak-check=full ./myapp

The -g flag includes debug information so Valgrind can report source file and line numbers. -O0 disables optimization so the reports match the source code.

Example: Detecting a Leak

/* leak_example.c */
#include <stdlib.h>

int main(void) {
    int *a = malloc(40);
    int *b = malloc(80);
    free(a);
    /* b is never freed */
    return 0;
}
$ valgrind --leak-check=full ./leak_example
==12345== HEAP SUMMARY:
==12345==     in use at exit: 80 bytes in 1 blocks
==12345==   total heap usage: 2 allocs, 1 frees, 120 bytes allocated
==12345==
==12345== 80 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345==    at 0x4C2FB0F: malloc (in /usr/lib/valgrind/...)
==12345==    by 0x40054E: main (leak_example.c:5)
==12345==
==12345== LEAK SUMMARY:
==12345==    definitely lost: 80 bytes in 1 blocks

Valgrind reports the exact line where the leaked memory was allocated.

Example: Detecting Use-After-Free

/* uaf_example.c */
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *p = malloc(sizeof(int));
    *p = 42;
    free(p);
    printf("%d\n", *p);
    return 0;
}
$ valgrind ./uaf_example
==12346== Invalid read of size 4
==12346==    at 0x400567: main (uaf_example.c:8)
==12346==  Address 0x5204040 is 0 bytes inside a block of size 4 free'd
==12346==    at 0x4C30D3B: free (in /usr/lib/valgrind/...)
==12346==    by 0x40055E: main (uaf_example.c:7)

Valgrind Limitations

  • Slow: programs run 10-50x slower under Valgrind.
  • Only detects bugs in code paths that actually execute. Untested paths are invisible.
  • Does not detect stack buffer overflows (only heap).
  • Not available on all platforms (primarily Linux).

AddressSanitizer (ASan)

ASan is a compiler-based tool that instruments your code at compile time. It is much faster than Valgrind (only 2x slowdown) and detects more bug types.

Usage

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

Or with Clang:

$ clang -g -fsanitize=address -fno-omit-frame-pointer -o myapp myapp.c
$ ./myapp

Example: Detecting a Heap Buffer Overflow

/* overflow.c */
#include <stdlib.h>
#include <string.h>

int main(void) {
    char *buf = malloc(10);
    memset(buf, 'A', 20);  /* writes 10 bytes past the end */
    free(buf);
    return 0;
}
$ gcc -g -fsanitize=address -o overflow overflow.c && ./overflow
=================================================================
==12347==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200000001a
WRITE of size 20 at 0x602000000010 thread T0
    #0 0x7f... in __asan_memset
    #1 0x400a5e in main overflow.c:6

ASan stops the program immediately at the point of the overflow and prints a detailed report with a stack trace.

What ASan Detects

  • Heap buffer overflow and underflow
  • Stack buffer overflow
  • Global buffer overflow
  • Use-after-free
  • Use-after-return (with additional flags)
  • Double free and invalid free
  • Memory leaks (when combined with LeakSanitizer)

ASan vs Valgrind

Feature Valgrind ASan
Slowdown 10-50x 2x
Stack overflows No Yes
Requires recompile No Yes
Platform support Mainly Linux GCC, Clang, MSVC
Memory overhead Moderate 2-3x

MemorySanitizer (MSan)

MSan detects reads of uninitialized memory. It is available only with Clang.

$ clang -g -fsanitize=memory -fno-omit-frame-pointer -o myapp myapp.c
$ ./myapp
/* uninit.c */
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *p = malloc(sizeof(int));
    if (*p > 0) {  /* reading uninitialized memory */
        printf("positive\n");
    }
    free(p);
    return 0;
}
$ clang -g -fsanitize=memory -o uninit uninit.c && ./uninit
==12348==WARNING: MemorySanitizer: use-of-uninitialized-value
    #0 0x400b12 in main uninit.c:6

MSan cannot be combined with ASan in the same build. Run them separately.

LeakSanitizer

LeakSanitizer detects memory leaks at program exit. It is built into ASan on Linux (enabled by default) and can also run standalone:

$ gcc -g -fsanitize=leak -o myapp myapp.c
$ ./myapp
==12349==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 80 byte(s) in 1 object(s) allocated from:
    #0 0x7f... in malloc
    #1 0x400a3e in main leak_example.c:5

The Debugging Workflow

When you suspect a memory bug, follow this sequence:

Step 1: Reproduce

Write a test case or input that triggers the bug. Memory bugs are often non-deterministic, so try running multiple times or with different inputs.

Step 2: Build with Sanitizers

$ gcc -g -O0 -fsanitize=address -fno-omit-frame-pointer -o myapp myapp.c

Step 3: Run and Analyze

$ ./myapp

Read the sanitizer output. It tells you:

  • The type of bug (overflow, use-after-free, leak).
  • The exact source line where the bad access occurred.
  • Where the memory was allocated and (for use-after-free) where it was freed.

Step 4: Fix

Common fixes:

  • Leak: Add the missing free call, or restructure ownership.
  • Use-after-free: Ensure the pointer is not used after its memory is freed. Set it to NULL.
  • Buffer overflow: Check bounds before writing. Use snprintf instead of sprintf.
  • Double free: Track ownership. Set pointers to NULL after freeing.

Step 5: Verify

Rebuild with sanitizers and run the test case again. The sanitizer should report no errors.

$ gcc -g -fsanitize=address -o myapp myapp.c && ./myapp
$ echo $?
0

Real-World Example: Finding a Bug

/* buggy_program.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char *duplicate_and_uppercase(const char *input) {
    char *result = malloc(strlen(input));  /* BUG: off by one, no room for '\0' */
    if (!result) return NULL;

    for (size_t i = 0; i <= strlen(input); i++) {  /* copies the '\0' past the buffer */
        char c = input[i];
        if (c >= 'a' && c <= 'z') c -= 32;
        result[i] = c;
    }
    return result;
}

int main(void) {
    char *upper = duplicate_and_uppercase("hello");
    printf("%s\n", upper);
    free(upper);
    return 0;
}

The bug: malloc(strlen(input)) should be malloc(strlen(input) + 1). The loop writes the null terminator one byte past the end of the allocation.

$ gcc -g -fsanitize=address -o buggy buggy_program.c && ./buggy
=================================================================
==12350==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000015
WRITE of size 1 at 0x602000000015 thread T0
    #0 0x400b8e in duplicate_and_uppercase buggy_program.c:10
    #1 0x400c12 in main buggy_program.c:16

The fix:

char *result = malloc(strlen(input) + 1);  /* +1 for the null terminator */

Common Pitfalls

  • Not building with debug info. Without -g, sanitizer reports show only addresses, not source lines.
  • Testing only with sanitizers off. Memory bugs may not crash in normal builds. They silently corrupt data. Always run tests under ASan.
  • Assuming clean Valgrind output means no bugs. Valgrind only checks executed paths. Branches not taken during testing are unchecked.
  • Ignoring sanitizer warnings. Every sanitizer report is a real bug, even if the program appears to work. Undefined behavior can do anything, including appearing to work correctly.
  • Using optimization with sanitizers. High optimization levels can eliminate the buggy code or reorder operations, making bugs harder to reproduce. Use -O0 or -O1 during debugging.

Key Takeaways

  • Memory bugs (overflows, use-after-free, leaks) are the primary source of security vulnerabilities in C programs.
  • Valgrind detects heap memory errors at runtime without recompilation but runs 10-50x slower.
  • AddressSanitizer (ASan) detects heap, stack, and global buffer overflows with only 2x slowdown. Build with -fsanitize=address.
  • MemorySanitizer (MSan) detects uninitialized reads. Build with -fsanitize=memory (Clang only).
  • LeakSanitizer detects memory leaks at exit and is included with ASan on Linux.
  • The debugging workflow: reproduce, build with sanitizers, run, fix, verify.
  • Run your test suite under ASan as part of CI. Every sanitizer finding is a real bug.