Programming Languages · Memory and Ownership

malloc, free, and the Conventions That Replace a Type System

What the allocator stores around every block, why the ownership rule must be maintained by hand, and which C idioms encode it well enough to survive review.

malloc returns a pointer and free takes one back. Between those two calls sits an obligation that the language does not express: the block must be freed exactly once, by the code that owns it, after the last use and before the last pointer to it is lost. Every other ownership discipline in this cluster is a way of making some part of that obligation checkable. In C it stays a convention, and the conventions that work are worth naming precisely because they are the reference point everything else improves on.

The allocator’s own bookkeeping

A block returned by malloc is not the whole allocation. The allocator keeps metadata adjacent to the payload, and the exact amount is implementation-specific. In the dlmalloc design that glibc’s allocator derives from, each chunk begins with a size_t holding its size in bytes; the size is always a multiple of the alignment, so the low three bits are always zero and glibc reuses them as flags, which is why the header costs one size_t (8 bytes) on a 64-bit build. That is the minimum: alignment padding and the per-arena bookkeeping that glibc adds sit on top of it. Freed chunks whose neighbours are also free are consolidated into larger ones, and the payload area of a free chunk holds the free-list links, which is why the minimum chunk is large enough to hold them alongside its header. glibc’s own documentation warns that a call to free does not necessarily return memory to the operating system; small blocks are kept on the heap for reuse, and only the top of the heap can be trimmed with malloc_trim or by the allocator’s own consolidation heuristics.

That explains three facts that surprise people. malloc(0) may return either a null pointer or a unique pointer that must be passed to free; the standard permits both, so portable code must not assume either. realloc may move the block, so any pointer into the old block becomes dangling even when the call succeeds. And the returned pointer is aligned suitably for any fundamental type, which in glibc on 64-bit x86-64 means 16 bytes, not merely the 8 that a double or a pointer requires.

The allocator also has to defend itself. Writing one byte past the end of a block corrupts the neighbouring chunk’s header, and the corruption typically surfaces at the next free or malloc rather than at the write. glibc’s consistency checks detect some of these at free time and abort with a message about a corrupted size or a double free; they cannot detect all of them, and they cannot detect an overflow that lands in another live block’s payload at all.

Why the rule cannot be local

The obligation is not a property of a single function. Ownership of a heap block passes between functions, and each function that receives a pointer must know whether it may free it. A function like void consume(char *p) that frees its argument and one like void inspect(const char *p) that only reads look identical at the call site unless the convention is written down.

Three conventions cover most real C code, and they are distinguishable by what the signature promises.

The first is caller-owns. The function borrows the pointer for the duration of the call and never retains it. A const parameter is the usual marker, and the documentation says the pointer must remain valid until the call returns. This is the default for anything that reads a string.

The second is callee-takes. The function assumes ownership and frees the block, or stores the pointer somewhere with a longer lifetime. Naming carries the signal in most codebases: a _take, _adopt, or _into suffix, or a verb like consume. Without such a signal, a callee-takes function is indistinguishable from a leak, and it is a reliable source of double frees when the caller also frees.

The third is transfer-on-success. The function takes ownership only if it returns success; on failure the caller retains it and must free it. This is the convention of asprintf, which on success transfers a buffer the caller must free and on failure returns -1 with no buffer to release, and of container insertion functions that keep a copy only when the insertion succeeds. It is the most error-prone of the three because the caller has to branch on the result before deciding whether to free: a caller that frees the buffer unconditionally double-frees on success, and a caller that never frees leaks on failure if the implementation populated the pointer before reporting the error.

Returning ownership is simpler to state: a function that returns a newly allocated pointer transfers ownership to the caller, and the documentation must say so. Functions that return a pointer into an existing object (strchr, strstr, a pointer to a struct field) transfer nothing. Mixing the two in one API is where leaks begin.

The idioms that make the rule visible

C has no destructors, so cleanup is written as a single exit path with labels. The pattern is old and it works because it makes the free order the reverse of the acquire order:

#include <stdio.h>
#include <stdlib.h>

int process(const char *path) {
    FILE *f = NULL;
    char *buf = NULL;
    int rc = -1;

    f = fopen(path, "r");
    if (!f) goto out;
    buf = malloc(4096);
    if (!buf) goto out;

    if (fread(buf, 1, 4096, f) == 0) goto out;
    rc = 0;

out:
    free(buf);          /* free(NULL) is a no-op, so order is safe */
    if (f) fclose(f);
    return rc;
}

The pattern’s virtue is that every allocation has exactly one matching free, at a single textual location, regardless of how many failure paths exist. Its cost is that goto forward past initializations is a discipline the compiler will not check, and a later edit that adds a resource without adding it to the cleanup block reintroduces the leak.

Two refinements carry most of the remaining weight. free(NULL) is defined by the C standard to do nothing, so a null-initialized pointer can be freed without a branch, and every pointer that might be conditionally allocated should start as null. And a pointer should be set to null immediately after it is freed when a copy of it might survive in the same scope, since free cannot reach into the caller’s variables.

GCC and Clang provide __attribute__((cleanup(fn))), which attaches a function to a local variable and calls it when the variable goes out of scope, including on early return. It is a compiler extension rather than standard C, but it makes the exit-label pattern unnecessary:

static void free_ptr(char **p) { free(*p); }

void use(void) {
    char *buf __attribute__((cleanup(free_ptr))) = malloc(256);
    if (!buf) return;      /* cleanup still runs, freeing NULL */
}

The attribute is the closest C gets to a destructor. It runs on normal scope exit and on early return, but not when the function leaves through longjmp, which is the same hole that C++ destructors have with setjmp. The Linux kernel’s cleanup-based __free helpers build on the same compiler support, for code that cannot use C++.

The failure modes and their signatures

Double free is the loudest. glibc detects the common case (freeing a chunk that is already on a free list) and aborts. The subtle case is a block that was freed and then reallocated to a different caller before the stale pointer was used again, which produces corruption in an unrelated object with no diagnostic pointing at the original error.

Leak is the quietest. A process that leaks steadily grows its resident set until the allocator requests more memory from the kernel; on a 64-bit system the address space is large enough that a leak of a few bytes per iteration can run for days. Leak detectors work by interposing on malloc and free and reporting blocks still live at exit, which finds leaks of unreachable blocks but not of blocks that remain referenced: a buffer still pointed at by a global is not reported.

Use after free is the most dangerous because it is exploitable. A freed block that still holds a length field and a data pointer, reallocated to an attacker-controlled caller, becomes a write primitive. This is why hardened allocators place freed chunks on quarantine lists so that a reuse of the block does not immediately hand the stale data back, and why glibc’s own consistency checks and the sanitizers built on compiler instrumentation exist.

Invalid free covers three distinct mistakes: freeing a pointer not returned by malloc (a pointer into the middle of a block, or a stack address), freeing a pointer twice, and passing a mismatched deallocator. The last is specific to C++: memory from new[] must go to delete[], memory from malloc must go to free, and mixing them is undefined even when the layout happens to match.

Where convention gives way

Manual reclamation is not a mistake to be corrected. It is the right choice when allocation must be predictable, when no runtime may be linked, or when the allocator itself is the thing being built. What it lacks is a checkable rule, and the languages that follow this cluster in the timeline of design all attack that gap from a different angle: move semantics makes transfer explicit, destructors and unwinding attaches reclamation to scope exit, reference counting replaces the single owner with a counted one, and arena allocation removes per-object ownership altogether by making the region the unit of reclamation.

The honest comparison is in constant factors. A malloc and free pair costs a free-list traversal, a header write, and possibly a brk or mmap call on the slow path, with the fast path in the tens of cycles for a cached size class. An arena bump allocation is an add and a compare, a few cycles, and its free is a single pointer reset for the entire region. The price of the arena is that nothing is returned until the region dies. Choosing between them is choosing which of those two cost profiles the program’s allocation pattern actually resembles, and the answer is usually mixed: arenas for request-scoped work, individual allocation for long-lived objects.

References

  1. POSIX.1-2017, System Interfaces: malloc — The Open Group
  2. glibc manual, Memory Allocation — GNU Project
  3. Doug Lea, A Memory Allocator — Doug Lea