Skip to content

Instantly share code, notes, and snippets.

@jetpks
Created July 24, 2026 19:01
Show Gist options
  • Select an option

  • Save jetpks/418dea782fbe06f4cd7f50b84e7aa83e to your computer and use it in GitHub Desktop.

Select an option

Save jetpks/418dea782fbe06f4cd7f50b84e7aa83e to your computer and use it in GitHub Desktop.
macOS 26 xzm crash root cause

xzm SIGTRAP root cause — macOS 26 deferred reclaim uint32 truncation

2026-07-24, studio.slush.systems (macOS 26.5.2 / 25F84, M4 Max 16c/128GB). Upstream gigatoken main @ 0d9765f, stock wheel (system allocator, shrink_to_fit present) unless noted. Companion to the matrix in this directory.

The crash, exactly

Both 11.9 GB crash reports (python3.13-2026-07-24-1103{11,13}.ips) and the threshold-probe crashes (12:53) carry the identical signature:

EXC_BREAKPOINT (SIGTRAP)
BUG IN LIBMALLOC: malloc assertion "err == VM_RECLAIM_SUCCESS" failed
  (libmalloc/src/xzone_malloc/xzone_segment.c:214)

libsystem_malloc  _xzm_reclaim_mark_used_locked.cold.1
libsystem_malloc  _xzm_reclaim_mark_used_locked
libsystem_malloc  _xzm_reclaim_mark_used
libsystem_malloc  _xzm_segment_group_cache_mark_used
libsystem_malloc  xzm_segment_group_free_chunk
gigatoken_rs.abi3.so (defer_drop worker thread)

Not memory corruption: a mach_vm_reclaim call returns non-success during an ordinary free, and libmalloc's xzm zone treats that as fatal.

Mechanism (from Apple source, libmalloc-812.100.31 + xnu-12377.121.6)

xzm's deferred reclamation puts freed spans in a mach_vm_reclaim ring buffer so the kernel can reclaim pages lazily. The entry and exit paths disagree about size width:

  • Entryxzm_reclaim_mark_free_locked (xzone_segment.c): uint32_t vm_size = (uint32_t)size; — a silent truncating cast. The guard xzm_debug_assert(size <= UINT32_MAX) is compiled out of production. A span > 4 GiB enters the ring with size mod 2^32.
  • Exitmach_vm_reclaim_try_cancel (xnu libsyscall/mach/vm_reclaim.c): takes the full 64-bit size and rejects it: if (os_convert_overflow(region_size, &size32)) return VM_RECLAIM_INVALID_REGION_SIZE;
  • libmalloc then hits xzm_assert(err == VM_RECLAIM_SUCCESS) → EXC_BREAKPOINT.

So any span larger than 4 GiB handed to the deferred-reclaim ring is a time bomb: entering it succeeds (truncated), and whatever later free/allocation causes the segment cache to mark it used (reuse or evict) detonates the process.

How gigatoken arms it

Committer::try_new reserves total_bytes u32 elements = 4× input bytes. The gather writes 4 × tokens bytes (~0.907 bytes per input byte for GPT-2 OWT). flat.shrink_to_fit() then trims the tail:

tail_bytes = 4·input − 4·tokens ≈ 4·input·(1 − 0.2269) = 3.0926·input

On xzm the trim is in place — the tail becomes a freed span entering the deferred-reclaim ring. When tail > UINT32_MAX, i.e. input > ~1.389 GB, the bomb is armed; the very next huge-chunk frees (the rayon defer_drop worker buffers) touch the segment cache and trap. That's why the crash lands on a worker thread inside the same encode call.

Empirical confirmation

Predicted cliff: input* = UINT32_MAX / (4·(1−r)) with r = 0.22685 (measured tokens/byte at this prefix) → 1.3888 GB.

Measured (stock wheel, fresh process each, 2 attempts per size):

input bytes result
1,350,000,000 ok (recorded 2026-07-23)
1,360,000,000 ok 2/2
1,385,000,000 ok 2/2
1,395,000,000 SIGTRAP 133, 2/2
1,400,000,000 SIGTRAP 133
1,410,000,000 SIGTRAP 133
1,420,000,000 SIGTRAP 133, 2/2
11,920,511,061 SIGTRAP 133, 2/2

The cliff sits in (1.385, 1.395] GB — the prediction lands inside the bracket. The "~1.4 GB single-doc ceiling" recorded on 2026-07-23 is this uint32 boundary, nothing else.

With shrink_to_fit removed (same box, same allocator): 7/7 clean at 11.9 GB (~10.9 GB/s warm). The full-buffer free at result-drop deallocates the huge segment directly and never defers a >4 GiB span, so nothing arms.

Negative results worth keeping

Single-threaded synthetic repros do NOT trigger it: plain C (malloc 6 GiB → memset 1 GiB → realloc down → churn), Python bytearray resize, and ctypes malloc/realloc with a pristine tail + 700 MB-class churn all survive. The detonation appears to need the multi-threaded rayon allocation pattern (16 workers' huge chunk buffers interleaved with the trim). A minimal standalone repro for an Apple radar would need to mimic that; otherwise the gigatoken encode itself is the repro.

Consequences for issue #38

  • The author's "lots of RAM shelters me" theory is wrong — it's the OS, not RAM. macOS 27 beta evidently changed vm_reclaim/xzm (source not yet published, so unverified); every macOS 26 user crashes on any single doc over ~1.39 GB with the stock wheel.
  • The one line causes both observed pathologies, via two different allocators: under mimalloc it's a hidden full-buffer memcpy (~5 GB/s tax at 11.9 GB); under macOS 26 xzm it's a fatal SIGTRAP once input exceeds ~1.39 GB.
  • Radar-worthy against libmalloc: entry-side uint32 truncation guarded only by debug asserts vs exit-side fatal 64-bit validation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment