🧱

Heap

How the allocator sees memory: chunks

notion image
Most general-purpose allocators manage the heap as a sequence of chunks. Each chunk is a header (metadata the allocator trusts) plus the user area you get back from malloc. Free chunks also carry links for size‑segregated free lists.
Address grows ---> +---------------------------+-------------------------------------+ | chunk header | user area | | size | flags | (fd,bk)* | | +---------------------------+-------------------------------------+ ^ ^ | └─ pointer returned by malloc() (p) └─ allocator metadata (not corrupt) * fd/bk only present/used when the chunk is on a free list.
Adjacent chunks let the allocator coalesce on free:
... [chunk A][chunk B][chunk C] ... ^ free(B) merges with neighbors if A/B/C are free-adjacent
Because metadata sits next to user data, overflows from one user area can hit the next chunk’s header.

Initialization errors

Reading fields of a freshly malloc’d object before writing them. Heap memory is uninitialized by malloc and contains stale bytes.
typedef struct { int len; char *buf; } Msg; Msg *m = malloc(sizeof *m); // not zeroed if (!m) return; printf("%d\\n", m->len); // X UB: reading garbage
  • Control flow based on garbage leads to OOB reads/writes.
  • Nondeterministic: different runs, different junk.
alloc --> init fields --> use //good alloc --(skip init)--> use //bad

Not checking malloc return codes

Assuming allocations always succeed and dereferencing NULL on pressure or limits.
char *p = malloc(n); memcpy(p, src, n); // (x) crash if p == NULL

Dereferencing null or invalid pointers

Touching NULL, freed pointers, out‑of‑bounds addresses, or made‑up constants.
char *p = NULL; *p = 'x'; // (x) null deref char *q = (char*)0xdeadbeef; *q = 0; // (x) nonsense address

Use‑after‑free (UAF)

notion image
Keeping a pointer after free (or after a function returned a pointer to a stack local) and using it later.
char *a = malloc(16); char *b = a + 5; free(a); b[2] = 'c'; // UAF write
t0: [alloc A] ───────► p = &A[0] t1: [use A] t2: [free A] (A may be put on a free list) t3: [realloc B uses the same region] t4: b points into old A; write via b clobbers B or metadata (x)
Stack variant (use‑after‑return):
char *retptr(void){ char p; return &p; // (x) address of a local }

Double free (and the old frontlink trick)

notion image
Calling free twice on the same pointer, often via error paths or partial cleanup.
void f(char **pp) { free(*pp); if (error) free(*pp); // (x) double free }
The allocator reuses fd/bk links of a free chunk. A second free may treat stale user bytes as list pointers.
Free list (simplified) A <-> B <-> C Double free / corruption can forge: B.fd = &TARGET-0x10 B.bk = &WRITEVAL Relinking performs writes via: B.fd->bk = B.bk B.bk->fd = B.fd → *(TARGET) = WRITEVAL (arbitrary write primitive)
Modern allocators add integrity checks, but logical safety still matters.

Memory leaks

Allocated memory is never freed; long‑running processes grow in RSS and fragment the heap.
for (int i = 0; i < N; ++i) { char *buf = malloc(1<<20); // 1 MB do_work(buf); } // (x) no free

Zero‑length allocations

Ambiguous behavior for malloc(0) / realloc(p,0):
  • May return NULL or a unique pointer you must free.
  • realloc(p,0) may behave like free(p) and return NULL.
size_t n = maybe_zero(); char *p = malloc(n); // semantics vary when n == 0

Heap overflow

Code 1:
int main(int argc, char **argv) { char *buf; buf = (char*)malloc(1024); printf("buf=%p\n", buf); strcpy(buf, argv[1]); // may write past 1024 (bad!) free(buf); // but this free is unlikely exploitable }
It is overflow that’s not (practically) exploitable.
notion image
There’s no guarantee another user chunk sits immediately after buf. Often the next region is the allocator’s top/wilderness chunk or padding that isn’t linked in a free list yet. Code 2:
int main(int argc, char **argv) { char *buf; char *buf2; buf = (char*)malloc(1024); buf2 = (char*)malloc(1024); // likely adjacent to buf printf("buf=%p buf2=%p\n", buf, buf2); strcpy(buf, argv[1]); // OOB writes into buf2’s header/user free(buf2); // triggers allocator logic on corrupted chunk }
Two same-sized allocations in sequence often sit back-to-back. Overflowing out of buf can land directly on buf2’s header
notion image
  • tweaks size/flags so coalescing goes down a wrong path, or
  • (in older allocators) plants fake fd/bk pointers, then free(buf2) will consult that corrupted header and can be steered into dangerous writes (the classic ā€œunlink/frontlinkā€ ideas).

Unlink exploit technique

turn a heap overflow into an arbitrary write by corrupting the next chunk’s header and then freeing it.
  1. Overflow from chunk X into chunk Y’s header.
  1. Plant fake pointers so unlink(Y) performs write‑what‑where.
Forge Y header (glibc-style pseudostructure) Y.size = ... (valid-ish size bits) Y.fd = &WRITEVAL Y.bk = &TARGET-0x10 // align so bk->fd lands on TARGET
Free Y (or cause a path that unlinks Y from a bin). Old allocators did:
unlink(Y): Y.bk->fd = Y.fd --> *(TARGET) = WRITEVAL (arbitrary write) Y.fd->bk = Y.bk