--- /dev/null
+---
+name: isc-mem-allocator
+description: 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.
+---
+
+# 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`.