Memory Bugs

Course 2 · Ch 3
Memory Bugs
Catching at runtime, with tools, what Rust's compiler catches for free

Every bug this chapter names has already appeared somewhere in this course — c1-6's buffer overflow, c1-7's dangling pointer, c2-2's leaks and double-frees. This chapter gathers them into one catalog, and introduces the tools real C developers actually use to catch them.

The Bug Catalog, Named Precisely

  • Use-after-free — dereferencing a pointer after its memory was freed (c1-7, c2-2)
  • Double-free — calling free twice on the same allocation (c2-2)
  • Memory leak — allocated memory that's never freed, with no remaining pointer to it at all (c2-2)
  • Buffer overflow — reading or writing past an array or allocation's actual bounds, stack or heap (c1-6, c1-8)
  • Uninitialized read — reading memory (from malloc, or a declared-but-unset variable) before anything was written to it

A Genuine Leak Example

void process() { int *buf = malloc(100 * sizeof(int)); // ... uses buf ... // no free(buf) — the pointer goes out of scope, the memory doesn't } for (int i = 0; i < 1000000; i++) { process(); // leaks 100 ints, one million times over }

Each call leaks a small, fixed amount — individually harmless-looking, but accumulating without bound. Rust makes this specific pattern impossible: a value's Drop runs automatically the moment its owner goes out of scope, with no equivalent "forgot to call it" failure mode in safe code.

valgrind

A dynamic analysis tool — run an already-compiled program under it, no recompilation needed. It reports leaks, invalid reads/writes, and use-after-free, each with a stack trace showing exactly where the bad allocation, access, or free occurred.

$ valgrind --leak-check=full ./program ==12345== HEAP SUMMARY: ==12345== definitely lost: 400 bytes in 1 blocks ==12345== ==12345== 400 bytes in 1 blocks are definitely lost ==12345== at malloc (in /usr/lib/valgrind/...) ==12345== by 0x4006B7: process (program.c:3)

AddressSanitizer (ASan)

A compile-time instrumentation approach — add -fsanitize=address and recompile. It catches much of the same bug class, but faster than valgrind, and crashes immediately with a detailed report at the exact moment the bad access happens, rather than only summarizing at program exit.

$ gcc -fsanitize=address -g program.c -o program $ ./program ==12345==ERROR: AddressSanitizer: heap-use-after-free READ of size 4 at 0x602000000010 thread T0 #0 0x... in main program.c:8
ToolRequires recompilingSpeed
valgrindnoslower — full emulation
AddressSanitizeryes — a compiler flagmuch faster, precise timing

Why This Chapter Exists

This is the honest cost of the tradeoff Chapter 1 named: C's speed and control come with these exact bug classes as a genuine, permanent possibility — not a temporary tooling gap that will eventually be fixed. Rust's compiler catches most of this entire category at compile time, before the program ever runs. C requires catching it at runtime, with tools like these, ideally before shipping — a real and lasting asymmetry between the two languages, not a difference in maturity or polish.

Bug classRustC
Use-after-freecompile-time — borrow checkerruntime — valgrind/ASan, if you run them
Double-freecompile-time — single ownershipruntime — valgrind/ASan, if you run them
Memory leaknot possible in safe, non-cyclic coderuntime — valgrind's leak checker, if you run it
Buffer overflowruntime panic — always checkedruntime — ASan, if you run it; otherwise silent UB
Run these tools routinely, not just when something looks wrong
Many memory bugs produce no visible symptom most of the time. Building the habit of running valgrind or an ASan-instrumented build regularly during development — not only when chasing a specific crash — catches problems long before they become a mystery in production.
Undefined behavior is not guaranteed to crash
A program with a genuine memory bug can appear to work correctly for a long time — sometimes indefinitely, on a given machine and compiler. Undefined behavior means the standard makes no promise at all, not "it will crash reliably." This unpredictability is exactly what makes this bug class so dangerous in practice: the absence of a crash is not evidence of correctness.

Coding Challenges

Challenge 1

Write a small program with a deliberate memory leak (malloc without a matching free), compile it, and run it under valgrind --leak-check=full. Report what the leak summary shows.

📄 View solution
Challenge 2

Write a small program with a deliberate use-after-free (free a pointer, then dereference it), compile it with -fsanitize=address, and run it. Report what AddressSanitizer's error output shows.

📄 View solution
Challenge 3

Explain why "the program didn't crash" is not proof a C program is free of memory bugs, and why the same reasoning doesn't apply to a Rust program that compiles successfully.

📄 View solution

Chapter 3 Quick Reference

  • Bug catalog: use-after-free, double-free, memory leak, buffer overflow, uninitialized read
  • valgrind --leak-check=full — no recompile needed, slower, reports leaks/invalid access with stack traces
  • -fsanitize=address — requires recompiling, much faster, crashes precisely at the moment of the bad access
  • Rust catches most of this category at compile time; C requires catching it at runtime, with tools
  • Undefined behavior is not guaranteed to crash — a program can "work" for a long time despite a real bug
  • Next chapter: the preprocessor — #define, macros, and conditional compilation