This page explains why macOS (Mach-O) does not behave like Linux (ELF) with respect to allocator interposition, and how dyld interposition can be used to deliberately override functions such as malloc across the entire process. This is a part of dynamic linking investigation series.

Understanding this difference is critical for making allocator behavior deterministic across platforms.

The Core Difference: Symbol Namespaces

Unlike ELF, Mach-O uses a two-level namespace model by default.

In practice, this means:

  • Each dynamic library (dylib) records both the symbol name and the library it expects that symbol to come from.
  • Symbol resolution is therefore scoped to a specific library, not globally searched.

As a result:

  • A symbol named malloc defined in the main executable does not automatically override malloc used internally by system libraries.
  • libSystem, libc++, and other dylibs continue to call their own allocator implementation unless explicitly told otherwise.

This is why the naïve strategy—statically linking allocator symbols into the executable—works on Linux but fails on macOS.

Consequence for the malloc Project

On macOS, without special handling:

  • The test executable uses the project’s allocator
  • System libraries continue using Apple’s libmalloc
  • Two independent allocators operate on the same address space

This situation is fundamentally unsafe:

  • Each allocator assumes exclusive ownership of heap metadata
  • Mixing allocators on the same memory regions leads to corruption
  • Behavior becomes non-deterministic and undefined

Therefore, macOS must be explicitly coerced into routing all allocator calls through a single implementation.

dyld: The Runtime Authority

dyld (the Dynamic Link Editor) is responsible for:

  • loading Mach-O images
  • resolving symbols
  • performing rebinding
  • invoking initializers

Importantly:

dyld has the final say over which function implementation is actually called at runtime.

Apple exposes this control via interposition.

dyld Interposition

Interposition allows one function implementation to replace another across all call sites within the process.

Conceptually:

Before : 

 ------------                ------------
|Any caller  | --- dyld --- | function F |
 ------------                ------------

After : 

 ------------                ------------
|Any caller  |              | function F |
 ----------- \                ------------
              \                   |
               \                  |
                \                 | 
                 \          ---------------
                  dyld --- | interposed F` | 
                            ---------------

The interposed function becomes the gatekeeper.

Implementing Interposition

Interposition is achieved by placing replacement mappings into a special Mach-O section:

(__DATA, __interpose)

Each entry is a tuple:

{ replacement_function, original_function }

At load time, dyld scans this section and rewrites bindings so that:

  • calls to the original function are redirected to the replacement
  • the replacement may optionally call the original

Crucially:

  • Calls from within the interposing library to the original symbol are not redirected.
  • All other callers must pass through the interposed implementation.

This property allows the interposer to fully control access to the original function.

DYLD_INSERT_LIBRARIES

Interposition logic is typically packaged as a small dylib and injected at runtime via:

DYLD_INSERT_LIBRARIES=path/to/interposed.dylib

When set:

  • dyld loads the interposed library before other images
  • its __interpose section is registered
  • bindings are rewritten accordingly

This mechanism is the macOS equivalent of LD_PRELOAD on Linux, though the semantics differ.

Why Not Use Flat Namespaces?

Mach-O technically supports a flat namespace mode where symbols are resolved globally, similar to ELF.

However:

  • it is deprecated
  • poorly supported by modern toolchains
  • discouraged by Apple documentation
  • incompatible with many system libraries

Given the allocator’s need for reliability and determinism, relying on flat namespaces is not acceptable…

Practical Notes

  • Interposition requires care to avoid recursive calls back into the original allocator
  • The interposing dylib should avoid allocating memory internally unless explicitly guarded
  • Headers must be taken from Apple’s libmalloc (malloc/malloc.h) to ensure ABI compatibility

Note: We could include Apple’s libmalloc with malloc/malloc.h because interposing compilation does not include -Iinclude thus binds to the original header.

malloc git:(interpose) ✗ ls "$(xcrun --show-sdk-path)"/usr/include/malloc
_malloc_type.h  _malloc.h       _platform.h     _ptrcheck.h     malloc.h

Summary

On macOS:

  • Mach-O uses two-level namespaces
  • executable symbols do not automatically override dylib symbols
  • allocator interposition must be explicit

Using dyld interposition allows us to:

  • force all allocator calls through a single implementation
  • regain cross-platform consistency
  • avoid undefined behavior from mixed allocators

The next page, Cross-Platform Implications, ties these mechanics back to concrete design decisions in the malloc project.