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?