Skip to content

mimalloc override on macOS 26 / Darwin 25.5: free is interposed but malloc is not #1350

Description

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

  1. 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).

  2. 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.

  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions