Behavior change for malloc zones in macOS Ventura

In our codebase we have an option in our Debug builds to replace the malloc implementation with a wrapper function so we can write unit tests to detect allocations in code we expect to be allocation free. We were doing this using malloc_default_zone() to get the malloc zone and then vm_protect to make the zone read/write to change malloc and free to wrapper functions (then restore the permissions on the zone with vm_protect.

We noticed this was no longer working on macOS Ventura.

When trying to research this I couldn't find anything related in the change logs but did find this StackOverflow post suggesting to use malloc_get_all_zones() instead of malloc_default_zone(). We tried this and still were not able to override the malloc behavior.

In the example program below I would expect to see some MY MALLOC! message printed when malloc is called:

#include <array>
#include <cstdint>
#include <iostream>
#include <mach/mach.h>
#include <malloc/malloc.h>


using malloc_ptr = void *(*)(malloc_zone_t *zone, size_t size);

std::array<malloc_ptr, 3> original_mallocs;

template <size_t N> void *my_malloc(malloc_zone_t *zone, size_t size) {
  std::cout << "MY MALLOC! " << N << '\n';
  return original_mallocs[N](zone, size);
}

template <size_t N> void hook_malloc(auto zone) {
  original_mallocs[N] = zone->malloc;
  vm_protect(mach_task_self(), (uintptr_t)zone, sizeof(malloc_zone_t), 0,
             VM_PROT_READ | VM_PROT_WRITE);
  zone->malloc = &my_malloc<N>;
}

void setup_malloc_hooks() {
  malloc_zone_t **zones = nullptr;
  unsigned int num_zones = 0;
  if (KERN_SUCCESS !=
      malloc_get_all_zones(0, NULL, (vm_address_t **)&zones, &num_zones)) {
    // Reset the value in case the failure happened after it was
    num_zones = 0;
  }
  if (num_zones) {
    for (auto i = 0; i < num_zones; ++i) {
      switch (i) {
      case 0:
        std::cout << "hooking zone 0\n";
        hook_malloc<0>(zones[0]);
        break;
      case 1:
        std::cout << "hooking zone 1\n";
        hook_malloc<1>(zones[1]);
        break;
      case 2:
        std::cout << "hooking zone 2\n";
        hook_malloc<2>(zones[2]);
        break;
      default:
        std::cout << "NOT hooking zone " << i << '\n';
        break;
      }
    }
  } else {
    std::cout << "using default zone\n";
    hook_malloc<0u>(malloc_default_zone());
  }
  std::cout << '\n';
}

void print_mallocs() {
  malloc_zone_t **zones = nullptr;
  unsigned int num_zones = 0;
  if (KERN_SUCCESS !=
      malloc_get_all_zones(0, NULL, (vm_address_t **)&zones, &num_zones)) {
    // Reset the value in case the failure happened after it was
    num_zones = 0;
  }
  if (num_zones) {
    for (auto i = 0; i < num_zones; ++i) {
      std::cout << "zone: " << reinterpret_cast<uintptr_t>(zones[i]->malloc)
                << '\n';
    }
  } else {
    auto zone = malloc_default_zone();
    std::cout << "default zone: " << reinterpret_cast<uintptr_t>(zone->malloc)
              << '\n';
  }
  std::cout << '\n';
}

int main(int argc, const char *argv[]) {

  print_mallocs();

  setup_malloc_hooks();

  print_mallocs();

  void *v = malloc(123);
  uintptr_t my_v = reinterpret_cast<uintptr_t>(v);
  std::cout << "my_v is " << my_v << "\n";

  return 0;
}

Does anyone have any insight into what might have changed or how we can work around it?

This sounds like the result of a security hardening.

Taking a step back, modifying malloc zones that you don’t ‘own’ isn’t something that I’d expect to work in the long term. It’s exactly the sort of thing that malware does O-: Fortunately, you only need to do this during development, so there’s a better approach, namely dynamic linker interposing. For more on that, see An Apple Library Primer.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Behavior change for malloc zones in macOS Ventura
 
 
Q