5.1.18 · D4C Programming

Exercises — Common memory errors — null dereference, buffer overflow, use-after-free, double free, memory leak

2,007 words9 min readBack to topic

Before we start, one picture of the vocabulary every problem uses. A pointer is a sticky note carrying a locker number; the heap is the row of lockers malloc hands out; free returns a locker to the desk without erasing your sticky note.

Figure — Common memory errors — null dereference, buffer overflow, use-after-free, double free, memory leak

Prerequisites you may want open: Pointers in C, malloc and free, Stack vs Heap Memory, Undefined Behaviour, Strings in C and the null terminator, Valgrind and AddressSanitizer.


L1 — Recognition

Exercise 1.1

Name the memory error in each snippet (choose from: null dereference, buffer overflow, use-after-free, double free, memory leak).

// (a)
int *p = malloc(sizeof(int));
free(p);
free(p);
 
// (b)
char *q = malloc(50);
q = malloc(80);
free(q);
 
// (c)
int *r = NULL;
*r = 5;
 
// (d)
char buf[4];
strcpy(buf, "data");   // "data" = 4 chars + '\0'
Recall Solution 1.1

(a) Double freefree(p) is called twice on the same address; the second call corrupts the allocator's free-list. (b) Memory leakq is overwritten by the second malloc, so the first 50-byte block loses its only pointer. free(q) only frees the 80-byte block. Leaked bytes: . (c) Null dereferencer holds NULL (address 0), writing *r traps. (d) Buffer overflow"data" needs bytes but buf holds only . It writes byte past the end (the \0).


L2 — Application

Exercise 2.1

Fix this so it has no memory error. State each fix and why.

int *p = malloc(sizeof(int) * 3);
p[3] = 9;
printf("%d\n", *p);
free(p);
free(p);
Recall Solution 2.1

Three bugs. The corrected version:

int *p = malloc(sizeof(int) * 3);
if (p == NULL) return 1;   // fix 1: check malloc → no null deref
p[2] = 9;                  // fix 2: valid indices 0..2, not p[3]
printf("%d\n", *p);
free(p);
p = NULL;                  // fix 3: null-after-free
free(p);                   // now free(NULL) → guaranteed no-op
  • Fix 1 guards against malloc returning NULL.
  • Fix 2: an array of has valid indices . p[3] is one past the end (overflow).
  • Fix 3: setting p = NULL makes the second free(p) a safe no-op, killing the double free.

Exercise 2.2

Rewrite safely using a size-bounded copy. src may be any length.

char dst[16];
char *src = get_user_input();   // could be 100 chars
strcpy(dst, src);
Recall Solution 2.2
char dst[16];
char *src = get_user_input();
snprintf(dst, sizeof dst, "%s", src);   // copies at most 15 chars + '\0'

snprintf writes at most sizeof dst - 1 = 15 characters and always adds the \0, so it can never exceed . The invariant is now enforced by the function itself.


L3 — Analysis

Exercise 3.1

Trace this program. For each numbered line say what happens to the memory and whether it is legal.

int *a = malloc(sizeof(int));   // (1)
*a = 100;                       // (2)
int *b = a;                     // (3)
free(a);                        // (4)
printf("%d\n", *b);             // (5)
Recall Solution 3.1
  • (1) Allocates a 4-byte block on the heap; a holds its address. Legal.
  • (2) Writes 100 into that block. Legal.
  • (3) b is a copy of the same address — two sticky notes, one locker. Legal (aliasing).
  • (4) Returns the block to the allocator. a is now dangling; but so is b, because it names the same freed address.
  • (5) Use-after-free through b. Setting a = NULL after (4) would not save you here — b still holds the old address. This is why "one owner per block" matters.

Key insight: nulling the pointer you freed does not null its aliases.

Exercise 3.2

Given this program, compute the number of allocations , the number of frees of distinct live blocks , and the leaked bytes.

char *x = malloc(30);
char *y = malloc(40);
x = malloc(50);      // reassigns x
free(y);
free(x);
Recall Solution 3.2

Blocks created: -byte, -byte, -byte .

  • x = malloc(50) overwrote the pointer to the -byte block → that block is now unreachable.
  • free(y) frees the -byte block. free(x) frees the -byte block. So distinct live blocks freed.
  • Since (), there is a leak of the -byte block. Leaked bytes .

L4 — Synthesis

Exercise 4.1

This function is meant to duplicate a string onto the heap. It has two distinct bugs. Find both and write the fully correct version.

char *mydup(const char *s) {
    char *out = malloc(strlen(s));   // (A)
    strcpy(out, s);                  // (B)
    return out;
}
Recall Solution 4.1

Bug 1 (line A): allocates strlen(s) bytes but a copy needs strlen(s) + 1 for the \0. Line B then overflows by 1 byte. Bug 2 (lines A–B): malloc is never checked; if it returns NULL, strcpy is a null dereference. Corrected:

char *mydup(const char *s) {
    char *out = malloc(strlen(s) + 1);   // +1 for '\0'
    if (out == NULL) return NULL;        // check malloc
    strcpy(out, s);                      // now fits exactly
    return out;
}

For s = "hello": strlen = 5, correct allocation bytes.

Exercise 4.2

Apply the parent's mnemonic CONF-B (Check, Owner, Null-after-free, Free-once, Bounds). For each of the five errors below, state which single CONF-B rule would have prevented it.

  1. *p where malloc returned NULL.
  2. Writing a[5] in int a[5].
  3. Reading *p after free(p).
  4. free(p); free(p);.
  5. p = malloc(...) overwriting a live pointer.
Recall Solution 4.2
  1. Null deref → Check malloc.
  2. Overflow → Bounds (indices ).
  3. Use-after-free → Null-after-free (a later *p becomes a detectable null deref).
  4. Double free → Free once (plus Null-after-free makes the 2nd free(NULL) a no-op).
  5. Leak → One owner per block (don't overwrite the only handle).

Rule count: 4 of the 5 bugs are killed by Null-after-free + Owner + Check + Bounds — exactly the parent's "one discipline kills four of five" claim.


L5 — Mastery

Exercise 5.1

A long-running server calls handle() once per request. Each call leaks a fixed number of bytes. Given the leak below, how many bytes leak per 1000 requests, and after how many requests does the total leak reach bytes (round up)?

void handle(void) {
    char *log = malloc(64);
    char *buf = malloc(256);
    // ... uses both ...
    free(buf);
    // forgot free(log);
}
Recall Solution 5.1

Each call leaks the -byte log block ( buf is correctly freed). So leak per call bytes.

  • Per requests: bytes.
  • To reach bytes: requests.

Because it never crashes on request 1, a leak like this is invisible until the OOM-killer strikes — exactly why tools matter.

Exercise 5.2

You run this program under Valgrind and AddressSanitizer. For each line, name the exact diagnostic a tool would report (choose: "Invalid write", "Invalid read", "Invalid free / double free", "definitely lost").

char *p = malloc(8);      // (1)
p[8] = 'x';               // (2)
free(p);                  // (3)
char c = p[0];            // (4)
free(p);                  // (5)
char *q = malloc(16);     // (6) — never freed
Recall Solution 5.2
  • (2) p[8] is index 8 in an 8-byte block (valid ) → Invalid write (heap-buffer-overflow).
  • (4) reading p[0] after free(p)Invalid read (heap-use-after-free).
  • (5) freeing the already-freed block → Invalid free / double free.
  • (6) q's block is never freed and its pointer is lost at exit → definitely lost (leak) of bytes.
  • Line (3) alone is legal.

Total distinct errors flagged: . This is why AddressSanitizer/Valgrind is the mastery-level safety net — it makes every silent UB from this page loud.


Recall Quick self-check tally

Bugs you should now be able to name, fix, trace, prevent, and detect: null deref, buffer overflow, use-after-free, double free, memory leak. If any level felt shaky, reread its [!mistake] box — each is the single trap the grader expects at that difficulty.

Reveal-line drills:

Leaked bytes in Exercise 3.2
30 bytes (the unreachable 30-byte block).
Correct allocation size to copy "hello"
6 bytes ().
Requests to leak 1,000,000 bytes at 64 B/call
15,625.
Which CONF-B rule stops a leak by reassignment
Owner (one owner per block).