ANYKS
RU
in developmentpart of AWH · 21,355 lines

awh::alloc

A memory allocator that replaces the system one entirely, for the whole process. Not a wrapper around malloc and not a pool for special cases: once it takes over, every allocation goes through it — ours, the standard library's and those of any third-party library linked into the program.

01 Why

Four needs the system allocator does not cover

Accounting

How much application code holds right now, what the peak was, how much sits in caches, how much went back to the system. The system allocator is either silent or reports on the whole process.

Returning memory

A server that releases pages in the middle of a traffic spike pays in latency. The policy should be set by whoever writes the application, not by another library's defaults.

Fault analysis

When a program crashes you need to know: null, past the end of a block, use after free, or foreign memory. The system allocator can't answer at all.

Protecting secrets

Keys and passwords need memory that stays out of crash dumps, never swaps and is wiped on release. That is a property of allocation and cannot be added from outside.

All of it through calls, not environment variables: the application sets the mode itself and can change it at runtime.

02 Capture

Three capture techniques, one per family of systems

TechniqueSystemsHow it works
Symbol interpositionLinux, all BSDs, Solaris, illumosour object defines malloc and the linker puts it in place of the system one — captured before the first line of main
Symbols plus zonemacOSinterposition redirects the image's own calls, and a malloc_zone_t catches allocations made inside libsystem
Entry patchingWindowsthe entry points of ucrtbase functions are rewritten to jump to ours; aligned allocation is caught separately

Proven by a decisive observation: memory allocated inside the standard library (for example by std::string) is found in our region. A pool plugged into your own code cannot do that.

03 Design

Five layers, with features alongside

LayerWhat it does
Capturemalloc, free, calloc, realloc, aligned allocation
Cacheper-thread cache, takes no lock at all
Centralcentral free lists, one lock per size class — exchange in batches
Pagespage heap — exchange in spans
Sourcepages from the system, replaceable by the consumer
  • 62 size classes from 16 bytes to 32,768, rounding loss no more than an eighth of a block.
  • 8 KB heap pages, 4 MB chunks aligned to their size: a chunk's start is found with one mask.
  • Free-list links live inside the blocks. Pointers are mangled with a per-process seed: overwriting a free block does not give an attacker a controlled pointer.
  • Fast path — a dozen instructions and no lock: thread cache, size class by table lookup without division, block from the list head, per-thread accounting.

Beside the ladder: Huge for large allocations, Guard for guard pages, Profile for allocation sites, Trace for fault addresses.

04 Features

What the system allocator simply doesn't have

  • Fault address analysis resolve() — null page, foreign memory, live block, freed block, past the end, before the start.
  • Guard pages guardRate — sampled blocks behind closed pages: an overrun faults on the spot. Your own ASan, working in release builds.
  • Use-after-free detection — quarantine keeps freed memory filled with a pattern; spoiled() reports the number of corrupted blocks, the address and the offset.
  • Allocation-site profiling profileRate with stack capture — find leaks in a running program.
  • Secrets store secure() — memory kept out of dumps, locked against swap and wiped on release. shelter_t tells you what actually took effect.
  • Release policy — never, on request, by threshold on free, or by a service thread after a delay.
  • Usage queries — six figures: in use, peak, taken from the system, free in caches, free in the heap, returned.
  • Ceiling heapLimit guards everything taken from the system, not just the heap.
alloc::options_t options;
options.purge     = alloc::purge_t::ONFREE;    // never · on request · by threshold · by thread
options.heapLimit = 2ull * 1024 * 1024 * 1024; // ceiling on everything taken from the system
options.hugeCache = 16 * 1024 * 1024;          // keep large regions for reuse
options.guardRate = 1000;                      // one block in a thousand behind guard pages
if(!alloc::Allocator::capture(options)) /* capture did not happen */;
05 Benchmarks

Mixed sizes — the way real applications work

Nanoseconds per operation, lower is better; sizes spread across all size classes.

SystemAWHsystemjemalloctcmalloc
macOS ARM642.8111.2710.73—
Debian x86-6410.0422.2442.568.45
DragonFly BSD11.0926.90650.109.16
Alpine (musl)11.191,116.0750.988.61
NetBSD11.3134.9635.06—
FreeBSD11.8440.31(is the system one)9.02
OpenBSD21.922,563.45——

jemalloc beaten everywhere

By 3.1–3.9× on the mixed workload, 58× on DragonFly.

tcmalloc ahead by 7–15 %

It stores the size class in the block header; we identify a block by its address — which is what makes fault analysis possible.

Evenness — 22 %

Spread across thirteen systems is 10.1–12.4 ns; competitors vary by orders of magnitude.

06 Two changes

In-place growth and retention of large regions

realloc grows in place

A region grows into neighbouring free pages and keeps its address. A doubling chain from 16 bytes to 256 KB: was 14,750 µs, now 3,259 against 2,634 for the system allocator.

Keeping large regions

Above 4 MB the cost lies in first-touching pages. Freed regions are kept under the hugeCache ceiling: “allocate and fill” 5 MB went from 15,761 to 108,799 MB/s against 110,000 for the system.

Turning a feature off returns its memory at once: zero in quarantine releases everything the quarantine held, without waiting for the next free.

07 Tested

Eight systems, a fuzzer and sanitizers

SystemSuiteTSan
macOS 2663/64clean
Debian 12 · Alpine · FreeBSD 1549/49clean
NetBSD 10.149/49—
OpenBSD 7.9 · Solaris 11.448/49—
Windows 11 ARM64passed—
Elbrus-8C2 (lcc)port in progress—

Line coverage of the module is 79.65 %, and the uncovered part is explained: allocator teardown can't be tested inside the process, fork paths are exercised only on ELF systems, defensive branches are unreachable by design.

Skips are recorded system properties: OpenBSD has no syscall(2), and Sun systems don't support calling it directly.

Under sanitizers the capture does not happen, and the suite asserts that; the layers are tested under them directly.