BIND 9 allocator wrapper (isc_mem / isc_mempool)
Source of truth: lib/isc/include/isc/mem.h (public API), lib/isc/mem.c
(implementation), lib/isc/mem_p.h (private init/shutdown),
lib/isc/jemalloc_shim.h (non-jemalloc fallback).
Big picture
isc_mem_t (an "mctx") is a thin, reference-counted accounting wrapper over
jemalloc's non-standard API: mallocx / sdallocx / rallocx / sallocx.
On systems without jemalloc, jemalloc_shim.h emulates those on top of
malloc, recovering sizes from malloc_usable_size() / malloc_size(), or
as a last resort by prepending a size_info header to every allocation.
Key consequences:
- Allocation never fails. OOM calls
oom(), which writes a
signal-safe report + backtrace to stderr and abort()s. Never write
if (ptr == NULL) after an isc_mem_* call; there is no NULL return.
- Deallocation is size-hinted (
sdallocx). Freeing with the wrong size
is undefined behavior inside jemalloc, not just a stats bug.
- All contexts share jemalloc arenas (
jemalloc_flags is currently always 0),
so an mctx is an accounting domain, not a heap: it tracks who owes what,
it does not partition memory.
- A global default context
isc_g_mctx always exists (created by the library
constructor isc__lib_initialize() → isc__mem_initialize() in
lib/isc/lib.c). Use it only when nothing more specific fits.
The two allocation families — never mix them
|
sized family |
usable-size family |
| alloc |
isc_mem_get(mctx, size) |
isc_mem_allocate(mctx, size) |
| zeroed |
isc_mem_cget(mctx, n, size) |
isc_mem_callocate(mctx, n, size) |
| realloc |
isc_mem_reget(mctx, p, oldsize, newsize) / isc_mem_creget(mctx, p, oldn, newn, size) |
isc_mem_reallocate(mctx, p, newsize) |
| free |
isc_mem_put(mctx, p, size) / isc_mem_cput(mctx, p, n, size) |
isc_mem_free(mctx, p) |
| strdup |
— |
isc_mem_strdup(mctx, s) |
| stats charge |
the size you passed |
real usable size via sallocx (≥ requested) |
Rules:
- Memory from
isc_mem_get must be returned with isc_mem_put using the
same mctx and the same size. Memory from isc_mem_allocate must go back
via isc_mem_free. Crossing the families corrupts the per-context inuse
counter (put charges the requested size, free charges the sallocx size),
which detonates later as INSIST(isc_mem_inuse(ctx) == 0) when the context
is destroyed. The header's ISC_ATTR_MALLOC_DEALLOCATOR_IDX attributes make
compilers warn about some mismatches — treat those warnings as errors.
- Prefer the sized family whenever the caller knows the size (the common case:
isc_mem_get(mctx, sizeof(*obj))). Use allocate/free only for
variable-length data whose size is inconvenient to carry around.
- The
c-prefixed variants are calloc-alikes: ISC_CHECKED_MUL(n, size)
(overflow-fatal multiply) plus ISC__MEM_ZERO. ISC__MEM_ZERO is verbatim
jemalloc's MALLOCX_ZERO (0x40), RUNTIME_CHECKed at startup. Free a cget'd
array with isc_mem_cput(mctx, p, n, size) so the sizes match.
- Size 0 is legal and symmetric: both get and put bump 0 to
sizeof(void *) (ADJUST_ZERO_ALLOCATION_SIZE).
The put/free macros NULL the pointer
isc_mem_put, isc_mem_cput, isc_mem_free, isc_mem_putanddetach, and
isc_mempool_put are statement macros that end with (p) = NULL;. So:
- The pointer argument must be an assignable lvalue and must not have side
effects (macro arguments are expanded more than once — never
isc_mem_put(mctx, arr[i++], size)).
- After the call the variable is NULL — code relying on the old pointer value
afterwards is a bug even though the memory "was just there".
isc_mem_putanddetach
For objects that hold a reference to their own mctx:
isc_mem_putanddetach(&obj->mctx, obj, sizeof(*obj));
frees obj and then drops the mctx reference in the safe order (equivalent to
attach-to-local / detach-member / put / detach-local). This is the standard
destructor idiom; use it instead of an open-coded put + detach.
Context lifecycle
isc_mem_create("name", &mctx) — name is mandatory, copied, shows up in
stats channel output and leak dumps. (The create macro also re-assigns
isc__mem_malloc = mallocx; that is a deliberate link-order hack for
jemalloc — see jemalloc issue #2566 — don't "clean it up".)
- Refcounted via
ISC_REFCOUNT_IMPL: isc_mem_attach(src, &dst),
isc_mem_detach(&mctx) (NULLs the pointer), isc_mem_ref/unref. The last
detach destroys the context.
- Destruction asserts the books balance:
INSIST(isc_mem_inuse(ctx) == 0) and
that no pools remain. isc_mem_setdestroycheck(mctx, false) disables the
leak check — almost never the right fix; a failing inuse INSIST means a leak
or a family/size mismatch.
isc_mem_checkdestroyed(stderr) (called by named/tests at shutdown) arms a
library-shutdown check that every context was destroyed; it hits
UNREACHABLE() otherwise, after dumping live contexts when debugging is on.
isc__mem_shutdown() runs rcu_barrier() before checking, so RCU-deferred
frees (call_rcu) are flushed first — deferred frees still count as live
until the grace period runs.
Statistics and accounting internals
inuse is striped per thread id: stat_s[ISC_TID_MAX + 1] cacheline-padded
slots; ctx->stat = &stat_s[1] so isc_tid() == -1 (ISC_TID_UNKNOWN,
i.e. threads outside the loopmgr) indexes stat[-1] legally.
Updates are relaxed atomics on the caller's own stripe.
- A stripe can go negative (thread A frees what thread B allocated) —
that's why stripes are signed. Only the sum (
isc_mem_inuse(), which walks
-1..isc_tid_count()) is meaningful.
isc_mem_inuse() is O(threads) and iterates all stripes — fine for
water-mark checks, don't put it in per-packet hot paths gratuitously.
Water marks / overmem
isc_mem_setwater(mctx, hiwater, lowater) (0,0 or isc_mem_clearwater()
disables). Used by the resolver/cache to bound cache memory.
isc_mem_isovermem() is probabilistic: false below lowater, true above
hiwater, and in between returns true with probability ramping linearly
0→1 (8-bit resolution, isc_random8()). This deliberately spreads cache
cleaning over many inserts instead of a thundering herd at the mark —
do not "fix" the randomness, and don't expect two consecutive calls to
agree.
Returning memory to the OS
- Each thread counts bytes it frees (
freed_bytes, thread-local); every
16 MiB it triggers mem_purge(): jemalloc arena.<all>.decay (or glibc
malloc_trim(0)), rate-limited via CAS on last_purge to once per second
globally.
- Init-time jemalloc tuning:
background_thread = true,
dirty_decay_ms = 10000 applied to existing and future arenas. Failures
are ignored on purpose (the volumetric purge covers it).
Debugging facilities
Compile-time gate: ISC_MEM_TRACKLINES (set automatically by
-Ddeveloper=enabled meson builds) compiles in per-call
__func__/__FILE__/__LINE__ plumbing. Without it, the runtime flags below are
inert no-ops. ISC_MEM_TRACE additionally turns attach/detach into traced
refcounting.
Runtime flags (a context copies the global default at creation;
isc_mem_debugon()/debugoff() adjust the default and isc_g_mctx;
isc_mem_setdebugging() sets one context but requires inuse == 0):
ISC_MEM_DEBUGTRACE — print every alloc/free (add ptr size func file line mctx / del ...) to stderr.
ISC_MEM_DEBUGRECORD — record every live allocation in a 512-bucket hash
table; freeing something never allocated hits UNREACHABLE(); leaks are
dumped (print_active) with file:line when the context is destroyed. This
is the tool for "inuse != 0 at destroy" hunts.
ISC_MEM_DEBUGUSAGE — log when usage crosses the water marks.
Each flag can be enabled by simply setting the environment variable of the
same name (existence is checked, not the value) before start; the file
name in each record is copied, not pointed to, so plugins can be unloaded
safely.
Observability: isc_mem_stats(mctx, fp) (pool table + active allocations),
isc__mem_printactive() (unit tests), and the statistics channel renders all
contexts via isc_mem_renderxml() / isc_mem_renderjson() (id, name,
references, inuse, pool count, water marks).
isc_mempool — fixed-size free-list pools
isc_mempool_t batches fixed-size items on top of an mctx to cut allocator
round-trips (used for message buffers etc.):
isc_mempool_create(mctx, sizeof(item_t), "items", &pool);
isc_mempool_setfillcount(pool, 32); /* items grabbed per refill, default 1 */
isc_mempool_setfreemax(pool, 32); /* free-list cap, default 1 */
...
item_t *it = isc_mempool_get(pool); /* never NULL */
isc_mempool_put(pool, it); /* NULLs 'it' */
...
isc_mempool_destroy(&pool);
Critical facts:
- No locking whatsoever. The struct comment says "always unlocked"; the
caller must confine a pool to one thread or provide external locking.
Getters (
getallocated, getfreecount, ...) return garbage under
concurrent mutation.
get: pops the free list; if empty, grabs fillcount items from the mctx
in one loop. put: pushes back on the free list unless freecount >= freemax, in which case the item goes straight back to the mctx.
- Item size is silently raised to
sizeof(element) (one pointer) because
free items are chained through their own storage — a pool of very small
items wastes the difference.
- Under AddressSanitizer,
fillcount is forced to 1 and freemax to 0 so
every get/put reaches the real allocator and poisoning/use-after-free
detection works. Don't assume pooling behavior in ASAN builds.
isc_mempool_destroy() requires every item returned: outstanding items log
UNEXPECTED_ERROR("mempool %s leaked memory") and fail a REQUIRE.
- The pool holds a reference on its mctx and is linked on the context's
pools list (isc_mem_stats prints them); a context cannot be destroyed
while its pools exist.
- Pool items are charged to the mctx when fetched from it (i.e. items sitting
on the pool free list still count as inuse for the mctx).
Review checklist
When touching allocation code, check:
- get↔put / allocate↔free pairing, same mctx, same size (or matched
n, size pairs for cget/cput). Grep for the struct's free sites when a
size or family changes.
- Realloc sizing:
isc_mem_reget needs the correct old size; on the
non-jemalloc path it manually zeroes the growth for creget, so a wrong
old size also breaks zeroing.
- No NULL checks / error paths after allocation — remove dead OOM handling.
- Put-macro arguments: lvalue, no side effects, and nothing reads the
pointer after the macro (it's NULL now).
- Destructor idiom: last-ref objects use
isc_mem_putanddetach.
- Mempools: single-thread confinement is actually guaranteed; destroy path
returns every item first.
- New long-lived subsystems get their own named mctx (visible in stats
channel), not
isc_g_mctx.
1---2name: isc-mem-allocator3description: BIND 9's memory allocator wrapper (isc_mem memory contexts and isc_mempool fixed-size pools). Use when writing or reviewing code that allocates/frees memory anywhere in BIND 9, when choosing between isc_mem_get/put and isc_mem_allocate/free, when debugging "isc_mem_inuse(ctx) == 0" or mempool-leak assertion failures, memory-leak reports, overmem/water-mark behavior, or ISC_MEM_DEBUG* tracing.4---56# BIND 9 allocator wrapper (isc_mem / isc_mempool)78Source of truth: `lib/isc/include/isc/mem.h` (public API), `lib/isc/mem.c`9(implementation), `lib/isc/mem_p.h` (private init/shutdown),10`lib/isc/jemalloc_shim.h` (non-jemalloc fallback).1112## Big picture1314`isc_mem_t` (an "mctx") is a thin, reference-counted accounting wrapper over15jemalloc's non-standard API: `mallocx` / `sdallocx` / `rallocx` / `sallocx`.16On systems without jemalloc, `jemalloc_shim.h` emulates those on top of17`malloc`, recovering sizes from `malloc_usable_size()` / `malloc_size()`, or18as a last resort by prepending a `size_info` header to every allocation.1920Key consequences:2122- **Allocation never fails.** OOM calls `oom()`, which writes a23 signal-safe report + backtrace to stderr and `abort()`s. Never write24 `if (ptr == NULL)` after an `isc_mem_*` call; there is no NULL return.25- **Deallocation is size-hinted** (`sdallocx`). Freeing with the wrong size26 is undefined behavior inside jemalloc, not just a stats bug.27- All contexts share jemalloc arenas (`jemalloc_flags` is currently always 0),28 so an mctx is an *accounting domain*, not a heap: it tracks who owes what,29 it does not partition memory.30- A global default context `isc_g_mctx` always exists (created by the library31 constructor `isc__lib_initialize()` → `isc__mem_initialize()` in32 `lib/isc/lib.c`). Use it only when nothing more specific fits.3334## The two allocation families — never mix them3536| | sized family | usable-size family |37|---|---|---|38| alloc | `isc_mem_get(mctx, size)` | `isc_mem_allocate(mctx, size)` |39| zeroed | `isc_mem_cget(mctx, n, size)` | `isc_mem_callocate(mctx, n, size)` |40| realloc | `isc_mem_reget(mctx, p, oldsize, newsize)` / `isc_mem_creget(mctx, p, oldn, newn, size)` | `isc_mem_reallocate(mctx, p, newsize)` |41| free | `isc_mem_put(mctx, p, size)` / `isc_mem_cput(mctx, p, n, size)` | `isc_mem_free(mctx, p)` |42| strdup | — | `isc_mem_strdup(mctx, s)` |43| stats charge | the size you passed | real usable size via `sallocx` (≥ requested) |4445Rules:4647- Memory from `isc_mem_get` **must** be returned with `isc_mem_put` using the48 **same mctx and the same size**. Memory from `isc_mem_allocate` must go back49 via `isc_mem_free`. Crossing the families corrupts the per-context `inuse`50 counter (put charges the requested size, free charges the `sallocx` size),51 which detonates later as `INSIST(isc_mem_inuse(ctx) == 0)` when the context52 is destroyed. The header's `ISC_ATTR_MALLOC_DEALLOCATOR_IDX` attributes make53 compilers warn about some mismatches — treat those warnings as errors.54- Prefer the sized family whenever the caller knows the size (the common case:55 `isc_mem_get(mctx, sizeof(*obj))`). Use allocate/free only for56 variable-length data whose size is inconvenient to carry around.57- The `c`-prefixed variants are calloc-alikes: `ISC_CHECKED_MUL(n, size)`58 (overflow-fatal multiply) plus `ISC__MEM_ZERO`. `ISC__MEM_ZERO` is verbatim59 jemalloc's `MALLOCX_ZERO` (0x40), RUNTIME_CHECKed at startup. Free a cget'd60 array with `isc_mem_cput(mctx, p, n, size)` so the sizes match.61- Size 0 is legal and symmetric: both get and put bump 0 to62 `sizeof(void *)` (`ADJUST_ZERO_ALLOCATION_SIZE`).6364### The put/free macros NULL the pointer6566`isc_mem_put`, `isc_mem_cput`, `isc_mem_free`, `isc_mem_putanddetach`, and67`isc_mempool_put` are statement macros that end with `(p) = NULL;`. So:6869- The pointer argument must be an assignable lvalue and must not have side70 effects (macro arguments are expanded more than once — never71 `isc_mem_put(mctx, arr[i++], size)`).72- After the call the variable is NULL — code relying on the old pointer value73 afterwards is a bug even though the memory "was just there".7475### isc_mem_putanddetach7677For objects that hold a reference to their own mctx:7879```c80isc_mem_putanddetach(&obj->mctx, obj, sizeof(*obj));81```8283frees `obj` and then drops the mctx reference in the safe order (equivalent to84attach-to-local / detach-member / put / detach-local). This is the standard85destructor idiom; use it instead of an open-coded put + detach.8687## Context lifecycle8889- `isc_mem_create("name", &mctx)` — name is mandatory, copied, shows up in90 stats channel output and leak dumps. (The create macro also re-assigns91 `isc__mem_malloc = mallocx`; that is a deliberate link-order hack for92 jemalloc — see jemalloc issue #2566 — don't "clean it up".)93- Refcounted via `ISC_REFCOUNT_IMPL`: `isc_mem_attach(src, &dst)`,94 `isc_mem_detach(&mctx)` (NULLs the pointer), `isc_mem_ref/unref`. The last95 detach destroys the context.96- Destruction asserts the books balance: `INSIST(isc_mem_inuse(ctx) == 0)` and97 that no pools remain. `isc_mem_setdestroycheck(mctx, false)` disables the98 leak check — almost never the right fix; a failing inuse INSIST means a leak99 or a family/size mismatch.100- `isc_mem_checkdestroyed(stderr)` (called by named/tests at shutdown) arms a101 library-shutdown check that *every* context was destroyed; it hits102 `UNREACHABLE()` otherwise, after dumping live contexts when debugging is on.103- `isc__mem_shutdown()` runs `rcu_barrier()` before checking, so RCU-deferred104 frees (call_rcu) are flushed first — deferred frees still count as live105 until the grace period runs.106107## Statistics and accounting internals108109- `inuse` is striped per thread id: `stat_s[ISC_TID_MAX + 1]` cacheline-padded110 slots; `ctx->stat = &stat_s[1]` so `isc_tid()` == -1 (ISC_TID_UNKNOWN,111 i.e. threads outside the loopmgr) indexes `stat[-1]` legally.112 Updates are relaxed atomics on the caller's own stripe.113- A stripe can go **negative** (thread A frees what thread B allocated) —114 that's why stripes are signed. Only the sum (`isc_mem_inuse()`, which walks115 -1..isc_tid_count()) is meaningful.116- `isc_mem_inuse()` is O(threads) and iterates all stripes — fine for117 water-mark checks, don't put it in per-packet hot paths gratuitously.118119### Water marks / overmem120121- `isc_mem_setwater(mctx, hiwater, lowater)` (0,0 or `isc_mem_clearwater()`122 disables). Used by the resolver/cache to bound cache memory.123- `isc_mem_isovermem()` is **probabilistic**: false below lowater, true above124 hiwater, and in between returns true with probability ramping linearly125 0→1 (8-bit resolution, `isc_random8()`). This deliberately spreads cache126 cleaning over many inserts instead of a thundering herd at the mark —127 do not "fix" the randomness, and don't expect two consecutive calls to128 agree.129130### Returning memory to the OS131132- Each thread counts bytes it frees (`freed_bytes`, thread-local); every133 16 MiB it triggers `mem_purge()`: jemalloc `arena.<all>.decay` (or glibc134 `malloc_trim(0)`), rate-limited via CAS on `last_purge` to once per second135 globally.136- Init-time jemalloc tuning: `background_thread = true`,137 `dirty_decay_ms = 10000` applied to existing and future arenas. Failures138 are ignored on purpose (the volumetric purge covers it).139140## Debugging facilities141142Compile-time gate: `ISC_MEM_TRACKLINES` (set automatically by143`-Ddeveloper=enabled` meson builds) compiles in per-call144`__func__/__FILE__/__LINE__` plumbing. Without it, the runtime flags below are145inert no-ops. `ISC_MEM_TRACE` additionally turns attach/detach into traced146refcounting.147148Runtime flags (a context copies the global default at creation;149`isc_mem_debugon()/debugoff()` adjust the default *and* `isc_g_mctx`;150`isc_mem_setdebugging()` sets one context but requires `inuse == 0`):151152- `ISC_MEM_DEBUGTRACE` — print every alloc/free (`add ptr size func file153 line mctx` / `del ...`) to stderr.154- `ISC_MEM_DEBUGRECORD` — record every live allocation in a 512-bucket hash155 table; freeing something never allocated hits `UNREACHABLE()`; leaks are156 dumped (`print_active`) with file:line when the context is destroyed. This157 is the tool for "inuse != 0 at destroy" hunts.158- `ISC_MEM_DEBUGUSAGE` — log when usage crosses the water marks.159160Each flag can be enabled by simply **setting the environment variable of the161same name** (existence is checked, not the value) before start; the file162name in each record is copied, not pointed to, so plugins can be unloaded163safely.164165Observability: `isc_mem_stats(mctx, fp)` (pool table + active allocations),166`isc__mem_printactive()` (unit tests), and the statistics channel renders all167contexts via `isc_mem_renderxml()` / `isc_mem_renderjson()` (id, name,168references, inuse, pool count, water marks).169170## isc_mempool — fixed-size free-list pools171172`isc_mempool_t` batches fixed-size items on top of an mctx to cut allocator173round-trips (used for message buffers etc.):174175```c176isc_mempool_create(mctx, sizeof(item_t), "items", &pool);177isc_mempool_setfillcount(pool, 32); /* items grabbed per refill, default 1 */178isc_mempool_setfreemax(pool, 32); /* free-list cap, default 1 */179...180item_t *it = isc_mempool_get(pool); /* never NULL */181isc_mempool_put(pool, it); /* NULLs 'it' */182...183isc_mempool_destroy(&pool);184```185186Critical facts:187188- **No locking whatsoever.** The struct comment says "always unlocked"; the189 caller must confine a pool to one thread or provide external locking.190 Getters (`getallocated`, `getfreecount`, ...) return garbage under191 concurrent mutation.192- `get`: pops the free list; if empty, grabs `fillcount` items from the mctx193 in one loop. `put`: pushes back on the free list unless `freecount >=194 freemax`, in which case the item goes straight back to the mctx.195- Item size is silently raised to `sizeof(element)` (one pointer) because196 free items are chained through their own storage — a pool of very small197 items wastes the difference.198- Under AddressSanitizer, `fillcount` is forced to 1 and `freemax` to 0 so199 every get/put reaches the real allocator and poisoning/use-after-free200 detection works. Don't assume pooling behavior in ASAN builds.201- `isc_mempool_destroy()` requires every item returned: outstanding items log202 `UNEXPECTED_ERROR("mempool %s leaked memory")` and fail a REQUIRE.203- The pool holds a reference on its mctx and is linked on the context's204 `pools` list (`isc_mem_stats` prints them); a context cannot be destroyed205 while its pools exist.206- Pool items are charged to the mctx when fetched from it (i.e. items sitting207 on the pool free list still count as inuse for the mctx).208209## Review checklist210211When touching allocation code, check:2122131. get↔put / allocate↔free pairing, same mctx, same size (or matched214 `n, size` pairs for cget/cput). Grep for the struct's free sites when a215 size or family changes.2162. Realloc sizing: `isc_mem_reget` needs the *correct old size*; on the217 non-jemalloc path it manually zeroes the growth for `creget`, so a wrong218 old size also breaks zeroing.2193. No NULL checks / error paths after allocation — remove dead OOM handling.2204. Put-macro arguments: lvalue, no side effects, and nothing reads the221 pointer after the macro (it's NULL now).2225. Destructor idiom: last-ref objects use `isc_mem_putanddetach`.2236. Mempools: single-thread confinement is actually guaranteed; destroy path224 returns every item first.2257. New long-lived subsystems get their own named mctx (visible in stats226 channel), not `isc_g_mctx`.