How the allocator sees memory: chunks
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)
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)
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
NULLor a unique pointer you mustfree.
realloc(p,0)may behave likefree(p)and returnNULL.
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.
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- tweaks
size/flags so coalescing goes down a wrong path, or
- (in older allocators) plants fake
fd/bkpointers, thenfree(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.
- Overflow from chunk X into chunk Yās header.
- 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