Dataslope logoDataslope

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.

Code Block
C 17 (201710L)

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-deref

2. 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.

Code Block
C 17 (201710L)

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.

Code Block
C 17 (201710L)

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.

Code Block
C 17 (201710L)

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 terminates

5. 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 <).

Code Block
C 17 (201710L)

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.

Code Block
C 17 (201710L)

Cure:

  • Initialize every variable at declaration: int local = 0;.
  • Use calloc instead of malloc when 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.

Code Block
C 17 (201710L)

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.

Code Block
C 17 (201710L)

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:

DefenseWhat it catches
-Wall -Wextra -WpedanticMany 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)
ValgrindUAF, leaks, uninit, double-free (no recompile needed)
Static analyzers (clang-tidy, cppcheck, Coverity)Patterns the compiler misses
Always initialize at declarationEliminates a whole class of UB
Pair every malloc with a matching free, document the ownerEliminates leaks and double-frees
free(p); p = NULL;Turns UAF/double-free into NULL deref
Prefer snprintf / fgets over strcpy / getsEliminates trivial overflows

gets is so dangerous it was removed from C11. Never use it.

Practice: write a leak-free echo

Challenge
C 17 (201710L)
Allocate, use, and free correctly

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

Challenge
C 17 (201710L)
Build a string by repeated append

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

QuestionSelect one

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.

QuestionSelect one

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.

QuestionSelect one

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.

On this page