Memory Systems · Allocators
Hardened Allocators and Metadata Integrity
How allocator metadata becomes an attack surface, what canaries, quarantines, and out-of-band tables actually detect, and the performance each check costs.
A heap overflow is a bug. A heap overflow that yields control of execution is a design failure in the
allocator, because the memory it corrupts is the allocator’s own bookkeeping. Three fields carry
that trust: the size that free reads, the forward and backward pointers that link a free block
into a list, and the flag that says a neighbour is available for merging. Each is a word of ordinary
memory, writable by anything that can write past a buffer, and each is believed implicitly by code
that runs on the next allocation. Hardening is the practice of making that belief conditional.
Why the metadata is the target
The classic exploit primitive follows from the structure described in the boundary-tag design. Free blocks are linked through pointers stored inside the free payload, because a free block has no other use for those bytes. Unlinking a block writes the neighbour’s pointer into a location computed from the block’s own pointers. If an attacker can overwrite a free block’s forward and backward links, the unlink operation performs two writes to addresses of the attacker’s choosing.
The size field is the second primitive. Overwriting a block’s size to a larger value makes the allocator believe the block extends past its real end; the next allocation returns overlapping memory, and the attacker gains a write primitive into a live object. Overwriting it to a smaller value makes the following block appear to start inside the first, which produces a similar overlap through a different route.
Both primitives require only that the attacker can write a controlled amount past the end of a
buffer that happens to sit before allocator metadata. Metadata is interleaved with payload by
design, because that interleaving is what makes free cheap, so the bar is low. Every hardening
technique is therefore a way of either detecting that the metadata changed, or of removing it from
the writable region altogether.
Integrity checks on the metadata itself
The cheapest defence is to store a value derived from the metadata alongside it and verify the derivation on every use. The derivations fall into two classes.
A canary is a constant written into a known location, checked before the location is trusted. The size field can be stored with a fixed pattern in its unused low bits, since sizes are multiples of the alignment and three or four bits are always zero. Any value that does not have that pattern is rejected. The check is a mask and a compare, costing one or two instructions on the free path. The weakness is that the pattern is public: an attacker who knows the allocator writes the correct low bits when overwriting the size, and the check passes.
A checksum derived from the metadata and a per-process random value resists that, because the attacker cannot compute a valid checksum without reading the secret. The cost is that the checksum must be recomputed and stored whenever the metadata changes: on every split, every merge, every list insertion. That multiplies the number of places that must be correct. A single missed update makes the allocator reject a valid block, and the resulting failure is a crash in a program that has no bug. This is why checksums appear on the most security-sensitive paths rather than universally.
Pointer integrity is the third variant. Free-list pointers stored inside blocks are mangled by exclusive-or with a per-process random value and, in some designs, with the address of the slot holding the pointer. Unlinking then requires the attacker to know the secret, and a forged pointer produces an unmangled value that does not correspond to a mapped address. The technique is directly analogous to pointer authentication on architectures that provide it, and the trade is the same: a few instructions on the fast path, plus a rotation or update of the secret at process start.
Taking the metadata out of band
The stronger structural response is to stop storing metadata next to the payload. If block sizes and allocation state live in a table indexed by span and object index, then a linear overflow into the heap cannot reach them. The attacker must instead find a bug that writes to the table, a different and usually harder target.
The cost is a lookup on both allocation and free. With a two-level radix over the address space, the lookup is a few dependent loads; with a hash it is a hash computation plus a probe. Either way it is on the hot path, and it touches memory that a payload overflow cannot reach, which is precisely the point. This is the design that makes size-class heaps naturally harder to exploit than header-per-block heaps: the class and the object extent are properties of the span, not of the object.
Out-of-band metadata has a second advantage: it can be placed in read-only memory after initialisation, or protected by a guard page on either side, so that an overflow from a payload cannot reach it without crossing an unmapped page. The implementation effort is significant, since every metadata access becomes an indirection, and the designs that adopt it fully are usually those that also want the space savings of headerless small objects.
Detection by delay
Several techniques do not prevent corruption but make it visible before it can be exploited.
Quarantine. A freed block is not returned to the free list immediately; it is held in a queue of recently freed blocks and released only after a delay or a size threshold. A use-after-free then reads or writes memory that is still mapped and still poisoned, and the allocator can detect the second free of the same address because the block is in the quarantine rather than in a free list. The cost is footprint: a quarantine holding megabytes of dead objects inflates the resident set for the duration of the delay. Quarantine sizes are a direct trade between detection window and memory.
Poisoning. Freed payloads are filled with a known pattern. A use-after-free that reads the pattern produces obviously wrong data rather than plausible stale values, and a double free can be detected by checking the pattern before pushing the block. The check is a comparison over the block’s first word, or over the whole block in debug builds; whole-block poisoning costs time proportional to the freed size, which is why some allocators restrict release-mode poisoning to the header region.
Guard pages and guard zones. A large allocation can be followed by an unmapped page, so an overflow faults immediately at the site of the write instead of corrupting a neighbour. Page-granular guards are cheap for large allocations and impossible for small ones, where the guard would consume a whole page per object. For small objects the equivalent is a redzone: a few bytes of known pattern after the payload, checked at free, which detects overflow only when the block is freed, and only if the overflow did not also rewrite the redzone’s expected value.
Delayed coalescing. Merging on free means a corrupted size field is acted upon immediately. An allocator that verifies the neighbour’s header before merging, and refuses to merge if the sizes are inconsistent, converts a silent corruption into a detected inconsistency. The check requires reading the neighbour’s footer and confirming that it points back. That is a cheap consistency test that catches the common size-overwrite primitive.
What the hardening does not cover
Each technique has a blind spot, and knowing them is the difference between a mitigation and a false sense of one.
Canaries in low bits of a size field do not detect a size overwrite that preserves those bits. A checksum protects the metadata it covers and nothing else. The payload is still the payload, and a corrupted function pointer in a live object is outside the allocator’s view entirely. Quarantine delays reuse but does not prevent it; an attacker who can wait, or who can allocate enough to flush the quarantine, gets the reuse. Guard pages protect the end of an allocation, not the beginning, so an underflow into the previous block’s metadata passes through.
The largest blind spot is the one that no allocator can address: corruption of live data. If the attacker overwrites a struct field rather than heap metadata, the allocator sees nothing unusual because nothing it owns has changed. Hardening the allocator raises the cost of a specific exploit path; it does not make memory-unsafe code safe. That is why hardened allocators are deployed alongside, not instead of, bounds checking, and why the mitigation is best described as lowering the reliability of an exploit rather than preventing it.
Performance and the shape of the cost
The costs are not uniform across the API. Checks on the free path (canary verification, footer consistency, poison comparison) add work proportional to the number of frees. Checks on allocation add work proportional to the number of allocations. A program that allocates rarely and frees often, or the reverse, sees a different profile depending on which side carries the check.
The dominant cost in practice is not the arithmetic but the loss of layout freedom. Out-of-band metadata forces an indirection; quarantines break the locality that makes LIFO free lists fast, because a reused block is no longer the most recently freed one; poisoning dirties cache lines that would otherwise be reused without a write. A hardened allocator can therefore be markedly slower than a lean one on allocation-heavy code while being nearly indistinguishable on programs dominated by other work. Reporting a single percentage for the overhead of hardening is misleading for that reason: the number depends entirely on the allocation rate and the size distribution.
The practical configuration is layered: cheap structural checks always on, quarantine and full poisoning enabled by an environment variable or a tunable, and the whole set enabled in test and fuzzing builds. The tunables exposed by general-purpose allocators, among them the trim threshold, the mmap threshold and the number of arenas, are the knobs that let the same library serve both roles, and their interaction with page release and thread caches means that hardening a deployed configuration is a matter of adjusting several of them together rather than flipping one switch.
References
- Secure Coding in C and C++ — Robert C. Seacord, Addison-Wesley
- Linux man-pages — mallopt(3) — Linux man-pages project
- GNU C Library manual — Malloc Tunable Parameters — GNU Project
- The Art of Software Security Assessment — Mark Dowd, John McDonald, Justin Schuh, Addison-Wesley