1
    2
    3
    4
    5
    6
    7
    8
    9
   10
   11
   12
   13
   14
   15
   16
   17
   18
   19
   20
   21
   22
   23
   24
   25
   26
   27
   28
   29
   30
   31
   32
   33
   34
   35
   36
   37
   38
   39
   40
   41
   42
   43
   44
   45
   46
   47
   48
   49
   50
   51
   52
   53
   54
   55
   56
   57
   58
   59
   60
   61
   62
   63
   64
   65
   66
   67
   68
   69
   70
   71
   72
   73
   74
   75
   76
   77
   78
   79
   80
   81
   82
   83
   84
   85
   86
   87
   88
   89
   90
   91
   92
   93
   94
   95
   96
   97
   98
   99
  100
  101
  102
  103
  104
  105
  106
  107
  108
  109
  110
  111
  112
  113
  114
  115
  116
  117
  118
  119
  120
  121
  122
  123
  124
  125
  126
  127
  128
  129
  130
  131
  132
  133
  134
  135
  136
  137
  138
  139
  140
  141
  142
  143
  144
  145
  146
  147
  148
  149
  150
  151
  152
  153
  154
  155
  156
  157
  158
  159
  160
  161
  162
  163
  164
  165
  166
  167
  168
  169
  170
  171
  172
  173
  174
  175
  176
  177
  178
  179
  180
  181
  182
  183
  184
  185
  186
  187
  188
  189
  190
  191
  192
  193
  194
  195
  196
  197
  198
  199
  200
  201
  202
  203
  204
  205
  206
  207
  208
  209
  210
  211
  212
  213
  214
  215
  216
  217
  218
  219
  220
  221
  222
  223
  224
  225
  226
  227
  228
  229
  230
  231
  232
  233
  234
  235
  236
  237
  238
  239
  240
  241
  242
  243
  244
  245
  246
  247
  248
  249
  250
  251
  252
  253
  254
  255
  256
  257
  258
  259
  260
  261
  262
  263
  264
  265
  266

base / allocator / early_zone_registration_apple.cc [blame]

// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "base/allocator/early_zone_registration_apple.h"

#include <mach/mach.h>
#include <malloc/malloc.h>

#include "partition_alloc/buildflags.h"
#include "partition_alloc/shim/early_zone_registration_constants.h"

// BASE_EXPORT tends to be defined as soon as anything from //base is included.
#if defined(BASE_EXPORT)
#error "This file cannot depend on //base"
#endif

namespace partition_alloc {

#if !PA_BUILDFLAG(USE_PARTITION_ALLOC_AS_MALLOC)

void EarlyMallocZoneRegistration() {}
void AllowDoublePartitionAllocZoneRegistration() {}

#else

extern "C" {
// abort_report_np() records the message in a special section that both the
// system CrashReporter and Crashpad collect in crash reports. See also in
// chrome_exe_main_mac.cc.
void abort_report_np(const char* fmt, ...);
}

namespace {

malloc_zone_t* GetDefaultMallocZone() {
  // malloc_default_zone() does not return... the default zone, but the
  // initial one. The default one is the first element of the default zone
  // array.
  unsigned int zone_count = 0;
  vm_address_t* zones = nullptr;
  kern_return_t result = malloc_get_all_zones(
      mach_task_self(), /*reader=*/nullptr, &zones, &zone_count);
  if (result != KERN_SUCCESS) {
    abort_report_np("Cannot enumerate malloc() zones");
  }
  return reinterpret_cast<malloc_zone_t*>(zones[0]);
}

}  // namespace

void EarlyMallocZoneRegistration() {
  // Must have static storage duration, as raw pointers are passed to
  // libsystem_malloc.
  static malloc_zone_t g_delegating_zone;
  static malloc_introspection_t g_delegating_zone_introspect;
  static malloc_zone_t* g_default_zone;

  // Make sure that the default zone is instantiated.
  malloc_zone_t* purgeable_zone = malloc_default_purgeable_zone();

  g_default_zone = GetDefaultMallocZone();

  // The delegating zone:
  // - Forwards all allocations to the existing default zone
  // - Does *not* claim to own any memory, meaning that it will always be
  //   skipped in free() in libsystem_malloc.dylib.
  //
  // This is a temporary zone, until it gets replaced by PartitionAlloc, inside
  // the main library. Since the main library depends on many external
  // libraries, we cannot install PartitionAlloc as the default zone without
  // concurrency issues.
  //
  // Instead, what we do is here, while the process is single-threaded:
  // - Register the delegating zone as the default one.
  // - Set the original (libsystem_malloc's) one as the second zone
  //
  // Later, when PartitionAlloc initializes, we replace the default (delegating)
  // zone with ours. The end state is:
  // 1. PartitionAlloc zone
  // 2. libsystem_malloc zone

  // Set up of the delegating zone. Note that it doesn't just forward calls to
  // the default zone. This is because the system zone's malloc_zone_t pointer
  // actually points to a larger struct, containing allocator metadata. So if we
  // pass as the first parameter the "simple" delegating zone pointer, then we
  // immediately crash inside the system zone functions. So we need to replace
  // the zone pointer as well.
  //
  // Calls fall into 4 categories:
  // - Allocation calls: forwarded to the real system zone
  // - "Is this pointer yours" calls: always answer no
  // - free(): Should never be called, but is in practice, see comments below.
  // - Diagnostics and debugging: these are typically called for every
  //   zone. They are no-ops for us, as we don't want to double-count, or lock
  //   the data structures of the real zone twice.

  // Allocation: Forward to the real zone.
  g_delegating_zone.malloc = [](malloc_zone_t* zone, size_t size) {
    return g_default_zone->malloc(g_default_zone, size);
  };
  g_delegating_zone.calloc = [](malloc_zone_t* zone, size_t num_items,
                                size_t size) {
    return g_default_zone->calloc(g_default_zone, num_items, size);
  };
  g_delegating_zone.valloc = [](malloc_zone_t* zone, size_t size) {
    return g_default_zone->valloc(g_default_zone, size);
  };
  g_delegating_zone.realloc = [](malloc_zone_t* zone, void* ptr, size_t size) {
    return g_default_zone->realloc(g_default_zone, ptr, size);
  };
  g_delegating_zone.batch_malloc = [](malloc_zone_t* zone, size_t size,
                                      void** results, unsigned num_requested) {
    return g_default_zone->batch_malloc(g_default_zone, size, results,
                                        num_requested);
  };
  g_delegating_zone.memalign = [](malloc_zone_t* zone, size_t alignment,
                                  size_t size) {
    return g_default_zone->memalign(g_default_zone, alignment, size);
  };

  // Does ptr belong to this zone? Return value is != 0 if so.
  g_delegating_zone.size = [](malloc_zone_t* zone, const void* ptr) -> size_t {
    return 0;
  };

  // Free functions.
  // The normal path for freeing memory is:
  // 1. Try all zones in order, call zone->size(ptr)
  // 2. If zone->size(ptr) != 0, call zone->free(ptr) (or free_definite_size)
  // 3. If no zone matches, crash.
  //
  // Since this zone always returns 0 in size() (see above), then zone->free()
  // should never be called. Unfortunately, this is not the case, as some places
  // in CoreFoundation call malloc_zone_free(zone, ptr) directly. So rather than
  // crashing, forward the call. It's the caller's responsibility to use the
  // same zone for free() as for the allocation (this is in the contract of
  // malloc_zone_free()).
  //
  // However, note that the sequence of calls size() -> free() is not possible
  // for this zone, as size() always returns 0.
  g_delegating_zone.free = [](malloc_zone_t* zone, void* ptr) {
    return g_default_zone->free(g_default_zone, ptr);
  };
  g_delegating_zone.free_definite_size = [](malloc_zone_t* zone, void* ptr,
                                            size_t size) {
    return g_default_zone->free_definite_size(g_default_zone, ptr, size);
  };
  g_delegating_zone.batch_free = [](malloc_zone_t* zone, void** to_be_freed,
                                    unsigned num_to_be_freed) {
    return g_default_zone->batch_free(g_default_zone, to_be_freed,
                                      num_to_be_freed);
  };
#if PA_TRY_FREE_DEFAULT_IS_AVAILABLE
  g_delegating_zone.try_free_default = [](malloc_zone_t* zone, void* ptr) {
    return g_default_zone->try_free_default(g_default_zone, ptr);
  };
#endif

  // Diagnostics and debugging.
  //
  // Do nothing to reduce memory footprint, the real
  // zone will do it.
  g_delegating_zone.pressure_relief = [](malloc_zone_t* zone,
                                         size_t goal) -> size_t { return 0; };

  // Introspection calls are not all optional, for instance locking and
  // unlocking before/after fork() is not optional.
  //
  // Nothing to enumerate.
  g_delegating_zone_introspect.enumerator =
      [](task_t task, void*, unsigned type_mask, vm_address_t zone_address,
         memory_reader_t reader,
         vm_range_recorder_t recorder) -> kern_return_t {
    return KERN_SUCCESS;
  };
  // Need to provide a real implementation, it is used for e.g. array sizing.
  g_delegating_zone_introspect.good_size = [](malloc_zone_t* zone,
                                              size_t size) {
    return g_default_zone->introspect->good_size(g_default_zone, size);
  };
  // Nothing to do.
  g_delegating_zone_introspect.check = [](malloc_zone_t* zone) -> boolean_t {
    return true;
  };
  g_delegating_zone_introspect.print = [](malloc_zone_t* zone,
                                          boolean_t verbose) {};
  g_delegating_zone_introspect.log = [](malloc_zone_t*, void*) {};
  // Do not forward the lock / unlock calls. Since the default zone is still
  // there, we should not lock here, as it would lock the zone twice (all
  // zones are locked before fork().). Rather, do nothing, since this fake
  // zone does not need any locking.
  g_delegating_zone_introspect.force_lock = [](malloc_zone_t* zone) {};
  g_delegating_zone_introspect.force_unlock = [](malloc_zone_t* zone) {};
  g_delegating_zone_introspect.reinit_lock = [](malloc_zone_t* zone) {};
  // No stats.
  g_delegating_zone_introspect.statistics = [](malloc_zone_t* zone,
                                               malloc_statistics_t* stats) {};
  // We are not locked.
  g_delegating_zone_introspect.zone_locked =
      [](malloc_zone_t* zone) -> boolean_t { return false; };
  // Don't support discharge checking.
  g_delegating_zone_introspect.enable_discharge_checking =
      [](malloc_zone_t* zone) -> boolean_t { return false; };
  g_delegating_zone_introspect.disable_discharge_checking =
      [](malloc_zone_t* zone) {};
  g_delegating_zone_introspect.discharge = [](malloc_zone_t* zone,
                                              void* memory) {};

  // Could use something lower to support fewer functions, but this is
  // consistent with the real zone installed by PartitionAlloc.
  g_delegating_zone.version = allocator_shim::kZoneVersion;
  g_delegating_zone.introspect = &g_delegating_zone_introspect;
  // This name is used in PartitionAlloc's initialization to determine whether
  // it should replace the delegating zone.
  g_delegating_zone.zone_name = allocator_shim::kDelegatingZoneName;

  // Register puts the new zone at the end, unregister swaps the new zone with
  // the last one.
  // The zone array is, after these lines, in order:
  // 1. |g_default_zone|...|g_delegating_zone|
  // 2. |g_delegating_zone|...|  (no more default)
  // 3. |g_delegating_zone|...|g_default_zone|
  malloc_zone_register(&g_delegating_zone);
  malloc_zone_unregister(g_default_zone);
  malloc_zone_register(g_default_zone);

  // Make sure that the purgeable zone is after the default one.
  // Will make g_default_zone take the purgeable zone spot
  malloc_zone_unregister(purgeable_zone);
  // Add back the purgeable zone as the last one.
  malloc_zone_register(purgeable_zone);

  // Final configuration:
  // |g_delegating_zone|...|g_default_zone|purgeable_zone|

  // Sanity check.
  if (GetDefaultMallocZone() != &g_delegating_zone) {
    abort_report_np("Failed to install the delegating zone as default.");
  }
}

void AllowDoublePartitionAllocZoneRegistration() {
  unsigned int zone_count = 0;
  vm_address_t* zones = nullptr;
  kern_return_t result = malloc_get_all_zones(
      mach_task_self(), /*reader=*/nullptr, &zones, &zone_count);
  if (result != KERN_SUCCESS) {
    abort_report_np("Cannot enumerate malloc() zones");
  }

  // If PartitionAlloc is one of the zones, *change* its name so that
  // registration can happen multiple times. This works because zone
  // registration only keeps a pointer to the struct, it does not copy the data.
  for (unsigned int i = 0; i < zone_count; i++) {
    malloc_zone_t* zone = reinterpret_cast<malloc_zone_t*>(zones[i]);
    if (zone->zone_name &&
        strcmp(zone->zone_name, allocator_shim::kPartitionAllocZoneName) == 0) {
      zone->zone_name = "RenamedPartitionAlloc";
      break;
    }
  }
}

#endif  // PA_BUILDFLAG(USE_PARTITION_ALLOC_AS_MALLOC)
}  // namespace partition_alloc