Summary
On macOS 26.5.2 (Darwin 25.5.0, arm64), overriding via
DYLD_INSERT_LIBRARIES is asymmetric: mimalloc's free is
reached, but its malloc is not. mimalloc therefore frees blocks it
never allocated. Its own statistics report this directly, as
negative current counters.
This looks like a correctness issue rather than only a performance
one: mi_free is being handed pointers owned by the system allocator.
Environment
|
|
| OS |
macOS 26.5.2 (build 25F84), Darwin 25.5.0 |
| Arch |
arm64 (Apple M1 Max) |
| Compiler |
Apple clang 21.0.0 (clang-2100.1.1.101) |
| mimalloc |
v2.3.2 (MI_MALLOC_VERSION 20302) |
| Build |
default CMake Release; MI_OVERRIDE=ON, MI_OSX_INTERPOSE=ON, MI_OSX_ZONE=ON |
Reproduction
/* mi_repro.c */
#include <stdlib.h>
#include <string.h>
int main(void) {
for (int i = 0; i < 800; i++) { void *p = malloc(1 << 20); memset(p, i, 4096); free(p); }
return 0;
}
cc -O0 mi_repro.c -o mi_repro # -O0 so the alloc/free pair cannot be elided
DYLD_INSERT_LIBRARIES=/path/to/libmimalloc.dylib MIMALLOC_SHOW_STATS=1 ./mi_repro
Actual output
heap stats: peak total current block total#
reserved: 32.0 MiB 32.0 MiB 32.0 MiB
committed: 32.0 MiB 32.0 MiB 32.0 MiB
touched: 64.2 KiB 64.2 KiB -799.9 MiB
segments: 1 1 1
pages: 0 0 -800
touched current = -799.9 MiB and pages current = -800: 800
blocks were freed through mimalloc that were never allocated
through it.
touched peak/total = 64.2 KiB for a workload that allocates 800
MiB: the allocation side never reached mimalloc at all.
Expected
Either both sides are intercepted (non-negative counters, touched ≈
800 MiB), or neither is.
Additional observations
-
Interposition is not simply inert — it is one-sided. A dylib
that interposes both malloc and free sees only the free side
for calls made by the main executable. Calls made by system
dylibs do appear to be intercepted on the allocation side (see 3).
-
The zone override does not take effect either. Registering a
zone and then unregistering/re-registering the default zone — the
sequence in
src/prim/osx/alloc-override-zone.c::_mi_macos_override_malloc —
leaves malloc_default_zone() still returning DefaultMallocZone:
3 zones registered; first=DefaultMallocZone
installed=<ours> default-now=DefaultMallocZone
The in-tree comment already anticipates this ("thomcc: Unsure if
the next test is always false or just false in the cases I've
tried"). On this OS the "last registered becomes the default"
promotion appears not to hold, so the zone path cannot compensate
for the interpose asymmetry.
-
The mirror-image failure aborts processes. Interposing the
malloc_type_* family does capture allocations made by system
dylibs during startup. libobjc's map_images (a dyld notifier,
before main) then releases through a free that is not
intercepted, so the allocator sees a foreign pointer and aborts
before main:
malloc: *** error for object 0x…: pointer being freed was not allocated
libobjc NXCreateHashTable / NXCreateMapTable / map_images_nolock / map_images
dyld setObjCNotifiers
Same root asymmetry, opposite direction: captured on allocation,
released elsewhere.
Notes for triage
- The reproduction uses stock mimalloc sources. It was found in a
downstream fork, but the override path (src/alloc-override.c,
src/prim/osx/alloc-override-zone.c, src/static.c) is
byte-identical to upstream there — those files have zero modifying
commits since import — and the fork's changes are confined to
unrelated files. A maintainer should reproduce with a clean
checkout; nothing in the recipe depends on the fork.
-O0 matters: at -O1+ clang may legally elide an unescaped
malloc/free pair, which produces a superficially similar
"nothing was captured" result for an entirely different reason.
- Not yet established: whether this is new in macOS 26 / Darwin
25.x. A datum from an older macOS (same test, non-negative counters)
would bracket it.
Summary
On macOS 26.5.2 (Darwin 25.5.0, arm64), overriding via
DYLD_INSERT_LIBRARIESis asymmetric: mimalloc'sfreeisreached, but its
mallocis not. mimalloc therefore frees blocks itnever allocated. Its own statistics report this directly, as
negative
currentcounters.This looks like a correctness issue rather than only a performance
one:
mi_freeis being handed pointers owned by the system allocator.Environment
MI_MALLOC_VERSION 20302)MI_OVERRIDE=ON,MI_OSX_INTERPOSE=ON,MI_OSX_ZONE=ONReproduction
cc -O0 mi_repro.c -o mi_repro # -O0 so the alloc/free pair cannot be elided DYLD_INSERT_LIBRARIES=/path/to/libmimalloc.dylib MIMALLOC_SHOW_STATS=1 ./mi_reproActual output
touched current = -799.9 MiBandpages current = -800: 800blocks were freed through mimalloc that were never allocated
through it.
touched peak/total = 64.2 KiBfor a workload that allocates 800MiB: the allocation side never reached mimalloc at all.
Expected
Either both sides are intercepted (non-negative counters,
touched≈800 MiB), or neither is.
Additional observations
Interposition is not simply inert — it is one-sided. A dylib
that interposes both
mallocandfreesees only thefreesidefor calls made by the main executable. Calls made by system
dylibs do appear to be intercepted on the allocation side (see 3).
The zone override does not take effect either. Registering a
zone and then unregistering/re-registering the default zone — the
sequence in
src/prim/osx/alloc-override-zone.c::_mi_macos_override_malloc—leaves
malloc_default_zone()still returningDefaultMallocZone:The in-tree comment already anticipates this ("thomcc: Unsure if
the next test is always false or just false in the cases I've
tried"). On this OS the "last registered becomes the default"
promotion appears not to hold, so the zone path cannot compensate
for the interpose asymmetry.
The mirror-image failure aborts processes. Interposing the
malloc_type_*family does capture allocations made by systemdylibs during startup. libobjc's
map_images(a dyld notifier,before
main) then releases through afreethat is notintercepted, so the allocator sees a foreign pointer and aborts
before
main:Same root asymmetry, opposite direction: captured on allocation,
released elsewhere.
Notes for triage
downstream fork, but the override path (
src/alloc-override.c,src/prim/osx/alloc-override-zone.c,src/static.c) isbyte-identical to upstream there — those files have zero modifying
commits since import — and the fork's changes are confined to
unrelated files. A maintainer should reproduce with a clean
checkout; nothing in the recipe depends on the fork.
-O0matters: at-O1+ clang may legally elide an unescapedmalloc/freepair, which produces a superficially similar"nothing was captured" result for an entirely different reason.
25.x. A datum from an older macOS (same test, non-negative counters)
would bracket it.