# Isc Mem Allocator

> 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.

- Skill: `isc-projects/isc-mem-allocator` (Agent Skill)
- Install (CLI): `npx skillmds@latest add isc-projects/isc-mem-allocator`
- Raw SKILL.md: https://api.skillmd.com/api/skills/isc-projects/isc-mem-allocator/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: isc-projects (https://skillmd.com/u/isc-projects)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/isc-projects/isc-mem-allocator

---


# 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:

```c
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.):

```c
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:

1. 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.
2. 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.
3. No NULL checks / error paths after allocation — remove dead OOM handling.
4. Put-macro arguments: lvalue, no side effects, and nothing reads the
   pointer after the macro (it's NULL now).
5. Destructor idiom: last-ref objects use `isc_mem_putanddetach`.
6. Mempools: single-thread confinement is actually guaranteed; destroy path
   returns every item first.
7. New long-lived subsystems get their own named mctx (visible in stats
   channel), not `isc_g_mctx`.

