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.
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
Vecfriction has a kernel-level fix. - Try today: run the
mremapsnippet and watch a buffer grow 16x at one address.