Memory Bugs
A field guide to leaks, use-after-free, double-free, buffer overflows, off-by-one, and uninitialized reads — and how to avoid them
C gives you control of every byte, and with it the freedom to break things in spectacular ways. This page is a tour of the bug classes that every C programmer eventually meets, with runnable examples that reproduce the bug in the sandbox where possible.
These are not "exotic" bugs — they are the source of an enormous share of real-world security vulnerabilities (CVEs). Recognizing the patterns is half the battle.
1. Use-after-free
After free(p) the memory is returned to the allocator and may be
handed out by the next malloc. Reading or writing through p is
undefined behavior — sometimes it "works", sometimes it returns
someone else's bytes, sometimes it crashes.
Cure: the moment you free(p), stop using p. Many codebases
adopt free(p); p = NULL; so any later use becomes an immediate
NULL-deref instead of silent corruption.
free(s);
s = NULL; // turns silent UAF into loud NULL-deref2. Double-free
Calling free twice on the same pointer corrupts the allocator's
internal bookkeeping. On modern systems it often aborts the program
immediately ("double free or corruption"); on older or simpler
allocators it can silently corrupt the heap.
Cure: same as above — free(p); p = NULL;. free(NULL) is
guaranteed by the standard to be a no-op, so the second call becomes
harmless.
3. Memory leak
A leak is the opposite of double-free: every malloc should be
matched by exactly one free, but isn't. The block lives on,
unreachable, until the process exits.
Cure: treat every successful allocation as a promise to free
later. Tools like Valgrind and AddressSanitizer can report exactly
which malloc site was never matched by a free. Modern C++ uses
RAII (smart pointers) to make this automatic; C requires discipline
and clear ownership documentation.
4. Buffer overflow
Writing past the end of an allocation (stack or heap) is the canonical C bug. Depending on the layout it may corrupt neighboring variables, allocator metadata, or — historically, before stack protections — the function's return address, leading to remote code execution.
Depending on the layout the compiler chose, age may now contain
the bytes of "ph\0" or something else entirely. On a real machine,
this same pattern with a return address instead of age is how
stack-smashing exploits work.
Cure: prefer length-aware APIs.
char name[8];
snprintf(name, sizeof name, "%s", input); // always fits, always terminates5. Off-by-one
A subtle cousin of buffer overflow: you compute the right length but
forget to account for the null terminator (or you iterate <= when
you meant <).
Cure: when sizing a string buffer, always remember the + 1 for
the terminator. Or use length-aware functions like snprintf.
6. Uninitialized read
malloc does not zero memory. Stack variables are not
zero-initialized either (unlike Python/Java). Reading them before
writing returns whatever bytes happen to be there — often the
previous user's data.
Cure:
- Initialize every variable at declaration:
int local = 0;. - Use
callocinstead ofmallocwhen you want zeros. - Compile with
-Wall -Wextra— modern compilers warn about many uninitialized reads.
7. Dangling stack pointer
A pointer to a local variable becomes dangling the moment the function returns. We saw this in the stack chapter; it deserves another mention here because the failure mode is so quiet.
The pattern "returned a pointer to a local, then called another function" is the most common way a dangling stack pointer corrupts data without crashing.
Cure: return values (which are copied), have the caller pass a
buffer, or malloc and let the caller free.
8. Type confusion via casts
C is happy to let you cast almost any pointer to any other pointer type. The compiler trusts you to know what you are doing.
Reading bytes through a char * is legal (the standard
specifically allows it). Reading an int through a float * —
without going via a union or memcpy — is generally a strict
aliasing violation, another form of undefined behavior.
Cure: when you really need to reinterpret bytes, use memcpy or
a union. Do not cast unrelated pointer types and dereference them.
Defenses in depth
C cannot give you safety, but you can build habits and tooling around the language:
| Defense | What it catches |
|---|---|
-Wall -Wextra -Wpedantic | Many UB-prone patterns at compile time |
-fsanitize=address (ASan) | UAF, double-free, OOB read/write at run time |
-fsanitize=undefined (UBSan) | Signed overflow, alignment violations |
-fsanitize=memory (MSan) | Uninitialized reads (clang only) |
| Valgrind | UAF, leaks, uninit, double-free (no recompile needed) |
| Static analyzers (clang-tidy, cppcheck, Coverity) | Patterns the compiler misses |
| Always initialize at declaration | Eliminates a whole class of UB |
Pair every malloc with a matching free, document the owner | Eliminates leaks and double-frees |
free(p); p = NULL; | Turns UAF/double-free into NULL deref |
Prefer snprintf / fgets over strcpy / gets | Eliminates trivial overflows |
gets is so dangerous it was removed from C11. Never use it.
Practice: write a leak-free echo
Implement char *make_greeting(const char *name) so that it returns a heap-allocated string of the form Hello, NAME! (without the literal word "NAME" — substitute the parameter). The provided main calls it, prints the result, and frees it. Make sure not to leak and not to overflow.
Practice: safe append with realloc
Implement char *repeat(const char *s, int times) that returns a fresh heap string containing s repeated times times. Use a single malloc (no realloc needed). Do not leak; do not overflow. repeat("ab", 3) must produce ababab. The provided main prints and frees the result.
Test your understanding
Which defensive pattern turns a use-after-free or double-free into a much louder, easier-to-debug NULL-pointer dereference?
Always cast the pointer to void * before freeing.
Wrap every free in a try / catch.
After free(p);, immediately set p = NULL;. Future free(NULL) is a no-op and *p immediately crashes instead of silently corrupting memory.
Call free twice in a row to be certain.
Why is strcpy(buf, user_input) considered unsafe?
strcpy is not part of the C standard.
It always allocates extra memory you forget to free.
It has no way to know how big buf is and will keep copying until it finds a '\0' in user_input, overflowing the buffer if the input is too long.
It corrupts the file descriptor table.
Why is reading an uninitialized stack int undefined behavior even when "it just contains some number"?
The CPU refuses to execute the read.
The C standard explicitly declares the read undefined behavior; the compiler is allowed to assume it never happens and may optimize accordingly, producing surprising results.
It always reads the value zero.
The OS will trap and terminate the program.
You can now read C with open eyes
You know the bug classes. Tools like AddressSanitizer and Valgrind will be your best friends in real codebases.