Zig's ArrayList Just Stopped Breaking Your Pointers

Every growable array invalidates pointers on grow. Zig found a way out — and it's an old Linux syscall doing the heavy lifting.

Share

Every append you've ever called hides a small betrayal. Capacity runs out, the allocator moves the block, and every pointer into the array quietly dies. Zig's latest devlog entry — "Pointer Stability for ArrayLists," dated Aug 27 — is a serious attempt to make that stop happening.

If that sounds like a corner case, it isn't. It's the same mechanism behind iterator invalidation in C++, a large share of Rust's borrow-checker friction with Vec, and a graveyard of use-after-free bugs.

Why this matters

Pick your language, the story is the same. C++ std::vector invalidates all pointers on growth, and the compiler won't warn you. Rust turns it into a compile error to hold a &T while pushing, so everyone falls back to indices, arenas, or Rc. Go's append can copy the backing array under your feet — the GC saves you from corruption, not from stale copies.

Systems code pays a permanent tax for this: copy-on-write structures, epoch-based reclamation, index juggling. A growable array whose elements never move deletes an entire category of workarounds.

How it works

Classic growth: allocate a bigger block, memcpy, free the old one. Pointers die by design.

The escape hatch has been sitting in Linux for decades: mremap. Large allocations are page-backed anyway, and the kernel can grow a mapping by editing page tables — the virtual address stays identical while physical pages move underneath. No copy, and every pointer into the buffer stays valid.

Zig's allocator interface has been growing a remap path for exactly this, and routing ArrayList growth through it is what the devlog is working toward. Small arrays still copy — the trick only pays once you're page-sized.

Where this helps

  • Indexes that grow under load. Readers hold element pointers while a writer appends — today that demands RCU or copy-on-write.
  • References instead of indices. Stop re-resolving element N after every push.
  • Buffers that must not move. Audio frames and GPU staging memory handed to C libraries mid-pipeline.

Watch out

This is a devlog, not a numbered release — check what your Zig version actually does. mremap is Linux-only; other platforms fall back to copying. It's page-granular, so tiny lists see nothing. And MREMAP_MAYMOVE can still relocate if virtual address space is contended — stability is likely, not contractual.

Try it yourself

Watch an anonymous mapping grow 16x without moving — the kernel trick underneath the Zig work:

import ctypes, os

libc = ctypes.CDLL(None, use_errno=True)
libc.mmap.restype = ctypes.c_void_p
libc.mmap.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_int,
                      ctypes.c_int, ctypes.c_int, ctypes.c_long]
libc.mremap.restype = ctypes.c_void_p
libc.mremap.argtypes = [ctypes.c_void_p, ctypes.c_size_t,
                        ctypes.c_size_t, ctypes.c_uint]

PROT, FLAGS = 1 | 2, 0x02 | 0x20   # READ|WRITE, PRIVATE|ANONYMOUS
PAGE = os.sysconf("SC_PAGE_SIZE")

buf = libc.mmap(None, PAGE, PROT, FLAGS, -1, 0)
print("before:", hex(buf))

buf = libc.mremap(ctypes.c_void_p(buf), PAGE, PAGE * 16, 1)  # MAYMOVE
print("after: ", hex(buf), "— same address, 16x the space")

TL;DR

  • What changed: Zig's Aug 27 devlog details pointer-stable growth for ArrayList — appends that don't invalidate references.
  • Why it matters: the grow-and-move pattern behind C++ invalidation bugs and Rust's Vec friction has a kernel-level fix.
  • Try today: run the mremap snippet and watch a buffer grow 16x at one address.