Loading...
--- /dev/null
+++ libmalloc/libmalloc-792.41.1/src/xzone_malloc/xzone_introspect.c
@@ -0,0 +1,1908 @@
+/* ----------------------------------------------------------------------------
+Copyright (c) 2018-2022, Microsoft Research, Daan Leijen
+Copyright © 2025 Apple Inc.
+This is free software; you can redistribute it and/or modify it under the
+terms of the MIT license. A copy of the license can be found in the file
+"LICENSE" in the same directory as this file.
+-----------------------------------------------------------------------------*/
+
+#include "../internal.h"
+
+#if CONFIG_XZONE_MALLOC
+
+#define XZM_DEBUG_ENUMERATOR 0
+
+#pragma mark libmalloc segment introspection
+
+kern_return_t
+xzm_segment_group_segment_foreach_span(xzm_segment_t segment,
+ xzm_span_enumerator_t enumerator)
+{
+ xzm_slice_t end = _xzm_segment_slices_end(segment);
+ xzm_slice_t slice = _xzm_segment_slices_begin(segment);
+
+ if (segment->xzs_kind == XZM_SEGMENT_KIND_HUGE) {
+ return enumerator(slice, slice->xzcs_slice_count);
+ }
+
+ // Enumeration protocol: the kind bits of a chunk are updated last, after
+ // the rest of the chunk metadata is initialized, so if we see a chunk slice
+ // it should be valid. Anything else may be in an intermediate state and is
+ // not to be trusted, so rather than iterating span-by-span as we would
+ // under the lock we need to scan linearly from chunk to chunk.
+ kern_return_t kr;
+ while (slice < end) {
+ xzm_slice_kind_t kind = slice->xzc_bits.xzcb_kind;
+ if (_xzm_slice_kind_is_chunk_safe(kind) ||
+ // Guard pages aren't chunks, but should be enumerated like them
+ kind == XZM_SLICE_KIND_GUARD) {
+ xzm_slice_count_t slice_count;
+ if (kind == XZM_SLICE_KIND_TINY_CHUNK) {
+ slice_count = 1;
+ } else {
+ slice_count = slice->xzcs_slice_count;
+ }
+
+ kr = enumerator(slice, slice_count);
+ if (kr) {
+ return kr;
+ }
+
+ slice += slice_count;
+ } else {
+ // Scan forward to the next chunk.
+ xzm_slice_t first_slice = slice;
+ do {
+ slice++;
+ } while (!_xzm_slice_kind_is_chunk_safe(slice->xzc_bits.xzcb_kind) &&
+ slice->xzc_bits.xzcb_kind != XZM_SLICE_KIND_GUARD &&
+ slice < end);
+
+ // Report the free span.
+ kr = enumerator(first_slice,
+ (xzm_slice_count_t)(slice - first_slice));
+ if (kr) {
+ return kr;
+ }
+ }
+ }
+
+ return KERN_SUCCESS;
+}
+
+kern_return_t
+xzm_segment_table_foreach(xzm_segment_table_entry_s *segment_table,
+ size_t num_entries, xzm_segment_table_enumerator_t enumerator,
+ xzm_segment_t *last_segment_enumerated)
+{
+ xzm_segment_t last_segment = NULL;
+ if (last_segment_enumerated) {
+ last_segment = *last_segment_enumerated;
+ }
+ for (size_t i = 0; i < num_entries; i++) {
+ xzm_segment_t segment =
+ _xzm_segment_table_entry_to_segment(segment_table[i]);
+ if (!segment) {
+ continue;
+ }
+ // Huge segments can be in multiple adjacent entries in the segment map
+ // if the segment spans multiple segment granules. Only enumerate the
+ // first entry
+ if (segment == last_segment) {
+ continue;
+ } else {
+ last_segment = segment;
+ }
+
+ kern_return_t kr = enumerator((vm_address_t)segment);
+ if (kr) {
+ return kr;
+ }
+ }
+ if (last_segment_enumerated) {
+ *last_segment_enumerated = last_segment;
+ }
+ return KERN_SUCCESS;
+}
+
+#pragma mark libmalloc zone introspection
+
+#if CONFIG_XZM_THREAD_CACHE
+
+static kern_return_t
+_xzm_introspect_enumerate_thread_caches(task_t task, memory_reader_t reader,
+ xzm_main_malloc_zone_t main,
+ MALLOC_NOESCAPE xzm_thread_cache_enumerator_t thread_cache_enumerator)
+{
+ xzm_debug_assert(main->xzmz_base.xzz_thread_cache_enabled);
+
+ vm_address_t thread_cache_addr = (vm_address_t)LIST_FIRST(
+ &main->xzmz_thread_cache_list);
+ size_t thread_cache_size = sizeof(struct xzm_thread_cache_s) +
+ (main->xzmz_base.xzz_thread_cache_xzone_count *
+ sizeof(xzm_xzone_thread_cache_u));
+ while (thread_cache_addr != 0) {
+ xzm_thread_cache_t tc;
+ kern_return_t kr = reader(task, thread_cache_addr, thread_cache_size,
+ (void **)&tc);
+ if (kr) {
+ xzm_debug_abort("Failed to map thread cache");
+ return kr;
+ }
+
+ kr = thread_cache_enumerator(thread_cache_addr, tc);
+ if (kr) {
+ // Allow KERN_RETURN_MAX as a way to request early exit
+ if (kr != KERN_RETURN_MAX) {
+ xzm_debug_abort("Failed to enumerate thread cache");
+ }
+ return kr;
+ }
+
+ thread_cache_addr = (vm_address_t)LIST_NEXT(tc, xtc_linkage);
+ }
+
+ return KERN_SUCCESS;
+}
+
+#endif // CONFIG_XZM_THREAD_CACHE
+
+static kern_return_t
+_xzm_introspect_small_chunk_blocks(xzm_malloc_zone_t zone,
+ vm_address_t segment_addr, xzm_segment_t segment, xzm_chunk_t chunk,
+ xzm_slice_count_t slice_count, uintptr_t start, vm_address_t start_addr,
+ xzm_xzone_t xz, MALLOC_NOESCAPE xzm_chunk_enumerator_t chunk_enumerator)
+{
+ uint32_t block_size = (uint32_t)xz->xz_block_size;
+ size_t capacity = xz->xz_chunk_capacity;
+
+ union {
+ vm_range_t range;
+ bool free;
+ } blocks[XZM_CHUNK_MAX_BLOCK_COUNT] = { 0 };
+
+ size_t range_idx = 0;
+
+ for (xzm_block_index_t block_index = 0; block_index < capacity;
+ block_index++) {
+ if (!_xzm_small_chunk_block_index_is_free(chunk, block_index)) {
+ blocks[range_idx].range = (vm_range_t){
+ .address = start_addr + block_index * block_size,
+ .size = block_size,
+ };
+ range_idx++;
+ }
+ }
+
+ return chunk_enumerator(segment_addr, segment, chunk, slice_count,
+ start_addr, xz, (vm_range_t *)blocks, (unsigned)range_idx);
+}
+
+static kern_return_t
+_xzm_introspect_freelist_chunk_blocks(task_t task, memory_reader_t reader,
+ xzm_malloc_zone_t zone, vm_address_t segment_addr,
+ xzm_segment_t segment, xzm_chunk_t chunk, xzm_slice_count_t slice_count,
+ uintptr_t start, vm_address_t start_addr, xzm_xzone_t xz,
+ MALLOC_NOESCAPE xzm_chunk_enumerator_t chunk_enumerator)
+{
+ kern_return_t kr = KERN_FAILURE;
+
+ uint32_t block_size = (uint32_t)xz->xz_block_size;
+ size_t granule = block_size > XZM_TINY_BLOCK_SIZE_MAX ?
+ XZM_SMALL_GRANULE : XZM_GRANULE;
+ size_t capacity = xz->xz_chunk_capacity;
+
+ xzm_chunk_atomic_meta_u meta = chunk->xzc_atomic_meta;
+
+ if (meta.xca_alloc_head == XZM_FREE_MADVISING ||
+ meta.xca_alloc_head == XZM_FREE_MADVISED) {
+ xzm_debug_assert(meta.xca_free_count == 0);
+ // This chunk is madvised, so there can be no blocks in use
+ return chunk_enumerator(segment_addr, segment, chunk, slice_count,
+ start_addr, xz, NULL, 0);
+ }
+
+ xzm_debug_assert(meta.xca_free_count <= capacity);
+
+#if CONFIG_MTE
+ // This code can be invoked by memory tools that are not running
+ // under MTE, to introspect a zone mapped in from a process that
+ // is actually running under MTE. Therefore, we only ldg when we are running
+ // in a process spawned with has_sec_transition=1.
+ //
+ // This should only matter for the case of memory tools on non-MTE hardware
+ // introspecting processes running under MTE emulation, as we need to ensure
+ // we don't try to execute the unsupported MTE instructions. On real
+ // hardware, we expect memory tools to run with Allocation Tag Access
+ // disabled (SCTLR.ATA=0), so there should be no need to do anything to
+ // safely access the mapped memory of a remote process even if it is running
+ // under MTE.
+#endif
+
+ // To produce an array of all of the live blocks in a tiny chunk:
+ // - We walk the chunk freelist, marking everything on it as free
+ // - If the chunk is marked as installed to a thread cache:
+ // - We search the thread caches to find the specific one that the chunk
+ // is installed to
+ // - Once we find the cache containing the chunk, we walk the cache
+ // freelist, marking all of those blocks as free as well
+ // - We determine the bump offset as the difference between the free count
+ // and the number of blocks
+ // - Then we prepare the range array by initializing the range for each
+ // block below the bump offset that wasn't marked free on the first pass
+
+ // N.B. Nano handles inconsistent freelist state by assuming that whatever
+ // it saw before the inconsistency is what's on it. It might be better to
+ // return an error and let the caller know that the state is inconsistent,
+ // but for now we'll do as nano does for compatibility.
+
+ union {
+ vm_range_t range;
+ bool free;
+ } blocks[XZM_CHUNK_MAX_BLOCK_COUNT] = { 0 };
+
+ // First, walk the chunk freelist. We should only walk it to the length on
+ // the chunk. In the thread caching case, a detaching thread may be in the
+ // process of linking a local freelist to the end, but we'll pick those
+ // blocks up later and don't want to include them in this walk.
+ bool cached = (meta.xca_alloc_idx == XZM_SLOT_INDEX_THREAD_INSTALLED);
+
+ size_t block_granule_size = block_size / granule;
+ uint64_t block_offset = meta.xca_alloc_head;
+
+ size_t total_chunk_freelist_count = meta.xca_free_count;
+ size_t current_chunk_freelist_count = 0;
+ while (current_chunk_freelist_count < total_chunk_freelist_count &&
+ block_offset < XZM_FREE_LIMIT &&
+ block_offset % block_granule_size == 0) {
+ size_t block_index = block_offset / block_granule_size;
+ if (blocks[block_index].free) {
+ xzm_debug_abort("loop in freelist");
+ break;
+ }
+ current_chunk_freelist_count++;
+ blocks[block_index].free = true;
+
+ xzm_block_t block = (xzm_block_t)(
+ start + (block_offset * granule));
+#if CONFIG_MTE && !MALLOC_TARGET_EXCLAVES_INTROSPECTOR
+ if (malloc_has_sec_transition) {
+ block = (xzm_block_t)memtag_fixup_ptr((void *)block);
+ }
+#endif
+ block_offset = block->xzb_linkage.xzbl_next_offset;
+ }
+
+ size_t allocated_limit = capacity;
+ if (cached) {
+#if CONFIG_XZM_THREAD_CACHE
+ if (zone->xzz_main_ref) {
+ xzm_debug_abort("cached chunk on non-main zone");
+ goto fail;
+ }
+
+ // We should have walked the full reported length of the remote freelist
+ if (current_chunk_freelist_count != total_chunk_freelist_count) {
+ xzm_debug_abort("Cached chunk freelist walk incomplete");
+ // XXX By failing the enumeration here, we're being stricter than
+ // nano was about weirdness in the freelist. That seems worth the
+ // increased visibility into possible bugs this enumerator might
+ // have, so for this case fail hard rather than allowing it.
+ goto fail;
+ }
+
+ xzm_main_malloc_zone_t main = (xzm_main_malloc_zone_t)zone;
+
+ xzm_xzone_index_t xz_idx = xz->xz_idx;
+ if (xz_idx >= zone->xzz_thread_cache_xzone_count) {
+ xzm_debug_abort("out-of-bounds cached xzone index");
+ goto fail;
+ }
+
+ __block xzm_thread_cache_t tc = NULL;
+ __block xzm_xzone_thread_cache_t cache = NULL;
+
+ // This chunk is installed to a thread cache, so we need to go find the
+ // right one. The priority order is:
+ // - If a thread cache for a non-detaching thread cache holds the chunk,
+ // it owns it. There should be at most one such cache.
+ // - Otherwise, if one or more detaching caches holds it, the cache with
+ // the highest teardown generation owns it.
+ kern_return_t kr2 = _xzm_introspect_enumerate_thread_caches(task,
+ reader, main, ^(vm_address_t thread_cache_addr,
+ xzm_thread_cache_t curr_tc){
+ xzm_xzone_thread_cache_t curr_cache =
+ &curr_tc->xtc_xz_caches[xz_idx];
+ if ((curr_cache->xztc_state < XZM_FREE_LIMIT ||
+ curr_cache->xztc_state == XZM_FREE_NULL) &&
+ (vm_address_t)curr_cache->xztc_chunk_start == start_addr) {
+ // This is a match. Is is a better match?
+
+ // If the cache we're looking at isn't tearing down, it's a
+ // perfect match, and we can stop searching.
+ if (!curr_tc->xtc_teardown_gen) {
+ xzm_debug_assert(!tc || tc->xtc_teardown_gen);
+ tc = curr_tc;
+ cache = curr_cache;
+ return KERN_RETURN_MAX;
+ }
+
+ // Otherwise, if it is tearing down, if it's more recent than
+ // the last one we saw then it's the best match we've seen so
+ // far.
+ if (!tc || curr_tc->xtc_teardown_gen > tc->xtc_teardown_gen) {
+ tc = curr_tc;
+ cache = curr_cache;
+ }
+ }
+ return KERN_SUCCESS;
+ });
+
+ if (kr2 && kr2 != KERN_RETURN_MAX) {
+ xzm_debug_abort("Failure enumerating thread caches");
+ kr = kr2;
+ goto fail;
+ }
+
+ if (!tc) {
+ xzm_debug_abort("Failed to find cache for cached chunk");
+ goto fail;
+ }
+
+ xzm_debug_assert(tc && cache);
+
+ // Walk the local freelist to add its free blocks to the set of known
+ // free blocks.
+ size_t total_local_freelist_count = cache->xztc_free_count;
+ uint64_t block_offset = cache->xztc_head;
+ size_t current_local_freelist_count = 0;
+ while (current_local_freelist_count < total_local_freelist_count &&
+ block_offset < XZM_FREE_LIMIT &&
+ block_offset % block_granule_size == 0) {
+ size_t block_index = block_offset / block_granule_size;
+ if (blocks[block_index].free) {
+ xzm_debug_abort("loop in local freelist");
+ break;
+ }
+ current_local_freelist_count++;
+ blocks[block_index].free = true;
+
+ xzm_block_t block = (xzm_block_t)(
+ start + (block_offset * granule));
+#if CONFIG_MTE && !MALLOC_TARGET_EXCLAVES_INTROSPECTOR
+ if (malloc_has_sec_transition) {
+ block = (xzm_block_t)memtag_fixup_ptr((void *)block);
+ }
+#endif
+ block_offset = block->xzb_linkage.xzbl_next_offset;
+ }
+
+ xzm_debug_assert(block_offset == XZM_FREE_NULL);
+
+ // Account for the bump on the local freelist
+ if (current_local_freelist_count < total_local_freelist_count &&
+ total_local_freelist_count <= capacity) {
+ allocated_limit = capacity -
+ (total_local_freelist_count - current_local_freelist_count);
+ }
+#else // CONFIG_XZM_THREAD_CACHE
+ xzm_debug_abort("Unexpected cached chunk");
+ goto fail;
+#endif // CONFIG_XZM_THREAD_CACHE
+ } else {
+ xzm_debug_assert(block_offset == XZM_FREE_NULL);
+
+ // Account for the bump on the remote freelist
+ if (current_chunk_freelist_count < total_chunk_freelist_count &&
+ total_chunk_freelist_count <= capacity) {
+ allocated_limit = capacity -
+ (total_chunk_freelist_count - current_chunk_freelist_count);
+ }
+ }
+
+ size_t range_idx = 0;
+ for (size_t i = 0; i < allocated_limit; i++) {
+ if (!blocks[i].free) {
+ blocks[range_idx].range = (vm_range_t){
+ .address = start_addr + i * block_size,
+ .size = block_size,
+ };
+ range_idx++;
+ }
+ }
+
+ return chunk_enumerator(segment_addr, segment, chunk, slice_count,
+ start_addr, xz, (vm_range_t *)blocks, (unsigned)range_idx);
+
+fail:
+ xzm_debug_assert(kr);
+ return kr;
+}
+
+static kern_return_t
+_xzm_introspect_chunk_blocks(task_t task, memory_reader_t reader,
+ xzm_malloc_zone_t zone, vm_address_t segment_addr,
+ xzm_segment_t segment, xzm_chunk_t chunk, xzm_slice_count_t slice_count,
+ uintptr_t start, vm_address_t start_addr, xzm_xzone_t xz,
+ MALLOC_NOESCAPE xzm_chunk_enumerator_t chunk_enumerator)
+{
+ xzm_slice_kind_t kind = chunk->xzc_bits.xzcb_kind;
+ if (!_xzm_slice_kind_uses_xzones(kind)) {
+ // This is a large or huge chunk, which has exactly one block
+ vm_range_t range = {
+ .address = start_addr,
+ .size = slice_count * XZM_SEGMENT_SLICE_SIZE,
+ };
+
+ return chunk_enumerator(segment_addr, segment, chunk, slice_count,
+ start_addr, NULL, &range, 1);
+ }
+
+ uint32_t block_size = (uint32_t)xz->xz_block_size;
+ size_t capacity = xz->xz_chunk_capacity;
+ // Sanity check
+ if ((slice_count * XZM_SEGMENT_SLICE_SIZE) / block_size != capacity ||
+ capacity > XZM_CHUNK_MAX_BLOCK_COUNT) {
+ xzm_debug_abort("inconsistent xzone info");
+ return KERN_FAILURE;
+ }
+
+ if (kind == XZM_SLICE_KIND_SMALL_CHUNK) {
+ return _xzm_introspect_small_chunk_blocks(zone, segment_addr, segment,
+ chunk, slice_count, start, start_addr, xz, chunk_enumerator);
+ }
+
+ xzm_debug_assert(kind == XZM_SLICE_KIND_TINY_CHUNK ||
+ kind == XZM_SLICE_KIND_SMALL_FREELIST_CHUNK);
+ return _xzm_introspect_freelist_chunk_blocks(task, reader, zone,
+ segment_addr, segment, chunk, slice_count, start, start_addr, xz,
+ chunk_enumerator);
+
+}
+
+static kern_return_t
+_xzm_introspect_enumerate(task_t task, memory_reader_t reader,
+ vm_address_t zone_address, xzm_malloc_zone_t zone,
+ vm_address_t main_address, xzm_main_malloc_zone_t main,
+ bool include_blocks,
+ MALLOC_NOESCAPE xzm_metapool_enumerator_t metapool_slab_enumerator,
+ MALLOC_NOESCAPE xzm_segment_enumerator_t segment_enumerator,
+ MALLOC_NOESCAPE xzm_chunk_enumerator_t chunk_enumerator,
+ MALLOC_NOESCAPE xzm_free_span_enumerator_t span_enumerator)
+{
+ bool zone_is_main = (zone_address == main_address);
+ xzm_debug_assert(!span_enumerator || zone_is_main);
+
+ size_t zone_size = zone_is_main ? main->xzmz_total_size :
+ zone->xzz_total_size;
+
+ if (zone_is_main) {
+ size_t metapools_size;
+ if (os_mul_overflow(main->xzmz_metapool_count,
+ sizeof(struct xzm_metapool_s), &metapools_size)) {
+ xzm_debug_abort("Failed to compute metapools size");
+ return KERN_FAILURE;
+ }
+ xzm_metapool_t metapools = (xzm_metapool_t)_xzm_introspect_rebase(
+ main_address, main, main->xzmz_total_size, main->xzmz_metapools,
+ metapools_size);
+ if (!metapools) {
+ xzm_debug_abort("Failed to rebase metapools");
+ return KERN_FAILURE;
+ }
+ for (int i = 0; i < main->xzmz_metapool_count; i++) {
+ xzm_metapool_t mp = &metapools[i];
+ vm_address_t slab_addr = (vm_address_t)SLIST_FIRST(&mp->xzmp_slabs);
+ while (slab_addr != 0) {
+ xzm_metapool_slab_t slab = NULL;
+
+ kern_return_t kr = reader(task, slab_addr,
+ sizeof(struct xzm_metapool_slab_s), (void **)&slab);
+ if (kr) {
+ xzm_debug_abort("Failed to map metapool slab");
+ return kr;
+ }
+
+ kr = metapool_slab_enumerator((vm_address_t)slab->xzmps_base,
+ mp->xzmp_slab_size, mp->xzmp_id);
+ if (kr) {
+ return kr;
+ }
+
+ slab_addr = (vm_address_t)SLIST_NEXT(slab, xzmps_entry);
+ }
+ }
+ }
+
+ size_t table_size;
+ if (os_mul_overflow(XZM_SEGMENT_TABLE_ENTRIES,
+ sizeof(xzm_segment_table_entry_s), &table_size)) {
+ xzm_debug_abort("failed to compute segment table size");
+ return KERN_FAILURE;
+ }
+ xzm_segment_table_entry_s *segment_table =
+ (xzm_segment_table_entry_s *)_xzm_introspect_rebase(main_address,
+ main, main->xzmz_total_size, main->xzmz_segment_table, table_size);
+ if (!segment_table) {
+ xzm_debug_abort("failed to rebase segment table");
+ return KERN_FAILURE;
+ }
+
+ xzm_segment_table_enumerator_t enumerator = ^(vm_address_t segment_addr){
+ xzm_segment_t segment;
+ // Even for huge segments, we don't need to map more than normal segment
+ // size because we don't need to see anything in the body of huge
+ // segments.
+ //
+ // XXX Note: the mapped segment is _not_ guaranteed to have the same
+ // alignment as the original segment, so many of the manipulation
+ // helpers can't be used with it.
+
+ // Map in the segment metadata to see how big it is.
+ kern_return_t kr = reader(task, segment_addr,
+ sizeof(struct xzm_segment_s), (void **)&segment);
+ if (kr) {
+ xzm_debug_abort("failed to map segment header");
+ return kr;
+ }
+
+ void *segment_body;
+ kr = reader(task, (vm_address_t)_xzm_segment_start(segment),
+ segment->xzs_slice_count * XZM_SEGMENT_SLICE_SIZE,
+ &segment_body);
+ if (kr) {
+ xzm_debug_abort("failed to map segment");
+ return kr;
+ }
+
+ kr = segment_enumerator(segment_addr, segment, " ");
+ if (kr) {
+ return kr;
+ }
+
+ return xzm_segment_group_segment_foreach_span(segment,
+ ^(xzm_slice_t span, xzm_slice_count_t slice_count){
+ ptrdiff_t idx = span - segment->xzs_slices;
+ size_t start_offset = idx * XZM_SEGMENT_SLICE_SIZE;
+ uintptr_t start = (uintptr_t)segment_body + start_offset;
+ uintptr_t orig_start = (uintptr_t)_xzm_segment_slice_index_start(
+ segment, (xzm_slice_count_t)idx);
+ vm_address_t start_addr = (vm_address_t)orig_start;
+
+ xzm_slice_kind_t kind = span->xzc_bits.xzcb_kind;
+ if (_xzm_slice_kind_is_chunk_safe(kind) &&
+ span->xzc_mzone_idx == zone->xzz_mzone_idx) {
+ // This is a chunk that belongs to this zone.
+ xzm_chunk_t chunk = span;
+
+ xzm_xzone_t xz = NULL;
+ if (_xzm_slice_kind_uses_xzones(kind)) {
+ xz = (xzm_xzone_t)_xzm_introspect_rebase(zone_address, zone,
+ zone_size, &zone->xzz_xzones[chunk->xzc_xzone_idx],
+ sizeof(struct xzm_xzone_s));
+ if (!xz) {
+ xzm_debug_abort("failed to rebase xzone");
+ return KERN_FAILURE;
+ }
+ }
+
+ if (include_blocks) {
+ return _xzm_introspect_chunk_blocks(task, reader, zone,
+ segment_addr, segment, chunk, slice_count, start,
+ start_addr, xz, chunk_enumerator);
+ } else {
+ return chunk_enumerator(segment_addr, segment, chunk,
+ slice_count, start_addr, xz, NULL, 0);
+ }
+ } else if (zone_is_main &&
+ span->xzc_mzone_idx == XZM_MZONE_INDEX_INVALID) {
+ // Include free spans and sequestered chunks when enumerating
+ // the main zone
+ //
+ // TODO: could include xzone for sequestered chunks that belong
+ // to one
+ return span_enumerator(segment_addr, segment, span,
+ slice_count, start_addr);
+ }
+
+ // Either a free span we don't care about or a Valid chunk that
+ // belongs to a different zone: skip, continue iteration
+ return KERN_SUCCESS;
+ });
+ };
+
+ xzm_segment_t last_segment_enumerated = NULL;
+ kern_return_t kr = xzm_segment_table_foreach(segment_table,
+ XZM_SEGMENT_TABLE_ENTRIES, enumerator, &last_segment_enumerated);
+ if (kr) {
+ return kr;
+ }
+
+ size_t ext_seg_table_size;
+ if (os_mul_overflow(main->xzmz_extended_segment_table_entries,
+ sizeof(xzm_extended_segment_table_entry_s), &ext_seg_table_size)) {
+ xzm_debug_abort("failed to compute extended segment table size");
+ return KERN_FAILURE;
+ }
+ xzm_extended_segment_table_entry_s *ext_segment_table =
+ (xzm_extended_segment_table_entry_s *) _xzm_introspect_rebase(
+ main_address, main, main->xzmz_total_size,
+ main->xzmz_extended_segment_table, ext_seg_table_size);
+ if (ext_segment_table) {
+ for (size_t i = 0; i < main->xzmz_extended_segment_table_entries; i++) {
+ // If leaf table pointer is non-null, map it in and enumerate over
+ // it
+ if (ext_segment_table[i].xeste_val != 0) {
+ // There is (or was) at least one segment in the 64GB span
+ // represented by this segment map entry
+ xzm_segment_table_entry_s *table;
+
+ vm_address_t table_addr = ((vm_address_t)ext_segment_table[i].xeste_val *
+ XZM_SEGMENT_TABLE_ALIGN);
+
+ kern_return_t kr = reader(task, table_addr,
+ XZM_SEGMENT_TABLE_SIZE, (void **)&table);
+ if (kr) {
+ xzm_debug_abort("Failed to map segment table");
+ return kr;
+ }
+
+ kr = xzm_segment_table_foreach(table, XZM_SEGMENT_TABLE_ENTRIES,
+ enumerator, &last_segment_enumerated);
+ if (kr) {
+ return kr;
+ }
+ }
+ }
+ }
+
+ return KERN_SUCCESS;
+}
+
+#if CONFIG_XZM_DEFERRED_RECLAIM
+
+static kern_return_t
+_xzm_introspect_map_reclaim_buffer(task_t task, memory_reader_t reader,
+ vm_address_t metadata_addr, xzm_reclaim_buffer_t *xzm_metadata_out,
+ mach_vm_reclaim_ring_t *buffer_out)
+{
+ xzm_reclaim_buffer_t xzm_buffer = NULL;
+ mach_vm_reclaim_ring_t buffer = NULL;
+ kern_return_t kr;
+
+ kr = reader(task, metadata_addr,
+ sizeof(struct xzm_reclaim_buffer_s), (void **)&xzm_buffer);
+ if (kr) {
+ xzm_debug_abort_with_reason("failed to map reclaim buffer metadata", kr);
+ goto out;
+ }
+
+ vm_address_t buffer_addr = (vm_address_t)xzm_buffer->xrb_ringbuffer;
+ size_t buffer_size = (xzm_buffer->xrb_len *
+ sizeof(struct mach_vm_reclaim_entry_s)) +
+ offsetof(struct mach_vm_reclaim_ring_s, entries);
+ if (buffer_addr != 0) {
+ xzm_debug_assert(buffer_size % vm_page_quanta_size == 0);
+ kr = reader(task, buffer_addr, buffer_size, (void **)&buffer);
+ if (kr) {
+ xzm_debug_abort_with_reason("failed to map reclaim buffer", kr);
+ goto out;
+ }
+ }
+
+out:
+ *xzm_metadata_out = xzm_buffer;
+ *buffer_out = buffer;
+ return kr;
+}
+
+#endif // CONFIG_XZM_DEFERRED_RECLAIM
+
+static kern_return_t
+_xzm_introspect_map_zone_and_main(task_t task, vm_address_t zone_address,
+ memory_reader_t reader, xzm_malloc_zone_t *zone_p_out,
+ xzm_main_malloc_zone_t *main_p_out, vm_address_t *main_address_out)
+{
+ xzm_malloc_zone_t zone = NULL;
+
+ // Map the base structure first to find its size, then map that full size
+ kern_return_t kr = reader(task, zone_address, sizeof(*zone),
+ (void **)&zone);
+ if (kr) {
+ xzm_debug_abort("failed to map zone");
+ return kr;
+ }
+
+ uint64_t zone_size = zone->xzz_total_size;
+ if (zone_size < sizeof(*zone)) {
+ xzm_debug_abort("inconsistent zone region info");
+ return KERN_FAILURE;
+ }
+
+ kr = reader(task, zone_address, zone_size, (void **)&zone);
+ if (kr) {
+ xzm_debug_abort("failed to map full zone");
+ return kr;
+ }
+
+ xzm_main_malloc_zone_t main = NULL;
+ uint64_t main_zone_size;
+ vm_address_t main_address = 0;
+ if (zone->xzz_main_ref) {
+ main_address = (vm_address_t)zone->xzz_main_ref;
+
+ kr = reader(task, main_address, sizeof(*main), (void **)&main);
+ if (kr) {
+ xzm_debug_abort("failed to map main zone");
+ return kr;
+ }
+
+ main_zone_size = main->xzmz_total_size;
+ if (main_zone_size < sizeof(*main)) {
+ xzm_debug_abort("inconsistent main zone info");
+ return KERN_FAILURE;
+ }
+
+ kr = reader(task, main_address, main_zone_size, (void **)&main);
+ if (kr) {
+ xzm_debug_abort("failed to map full main zone");
+ return kr;
+ }
+ } else {
+ main = (xzm_main_malloc_zone_t)zone;
+ if (main->xzmz_total_size != zone_size) {
+ xzm_debug_abort("inconsistent main zone size");
+ return KERN_FAILURE;
+ }
+
+ main_address = zone_address;
+ main_zone_size = zone_size;
+ }
+
+ if (main_zone_size < main->xzmz_total_size) {
+ xzm_debug_abort("inconsistent main region size");
+ return KERN_FAILURE;
+ }
+
+ xzm_assert(zone);
+ xzm_assert(main);
+ xzm_assert(main_address);
+ *zone_p_out = zone;
+ *main_p_out = main;
+ *main_address_out = main_address;
+
+ return KERN_SUCCESS;
+}
+
+static kern_return_t
+xzm_ptr_in_use_enumerator(task_t task, void *context, unsigned type_mask,
+ vm_address_t zone_address, memory_reader_t reader,
+ vm_range_recorder_t recorder)
+{
+ xzm_malloc_zone_t zone;
+ xzm_main_malloc_zone_t main;
+ vm_address_t main_address;
+ bool zone_is_main = false;
+
+ reader = reader_or_in_memory_fallback(reader, task);
+
+ bool record_admin = (type_mask & MALLOC_ADMIN_REGION_RANGE_TYPE);
+ bool record_ptr_region = (type_mask & MALLOC_PTR_REGION_RANGE_TYPE);
+ bool record_ptr_in_use = (type_mask & MALLOC_PTR_IN_USE_RANGE_TYPE);
+
+ kern_return_t kr = _xzm_introspect_map_zone_and_main(task, zone_address,
+ reader, &zone, &main, &main_address);
+ if (kr) {
+ return kr;
+ }
+
+ zone_is_main = (zone_address == main_address);
+
+ if (zone_is_main) {
+ vm_address_t mfm_addr = (vm_address_t)main->xzmz_mfm_address;
+ if (mfm_addr) {
+ kr = mfm_introspect.enumerator(task, context, type_mask, mfm_addr,
+ reader, recorder);
+ if (kr) {
+ return kr;
+ }
+ }
+ }
+
+ return _xzm_introspect_enumerate(task, reader, zone_address,
+ zone, main_address, main,
+ /* include_blocks */record_ptr_in_use,
+ ^(vm_address_t slab_addr, vm_size_t slab_size, xzm_metapool_id_t mp_id){
+ // Metapool slab enumerator
+ if (record_admin && zone_is_main) {
+ vm_range_t segment_meta_range = {
+ .address = slab_addr,
+ .size = slab_size,
+ };
+ recorder(task, context, MALLOC_ADMIN_REGION_RANGE_TYPE,
+ &segment_meta_range, 1);
+ }
+ return KERN_SUCCESS;
+ }, ^(vm_address_t segment_addr, xzm_segment_t segment, const char *indent){
+ // Segment enumerator
+ // Nothing to do, since segment metadata is recorded by the slab
+ // enumerator
+ return KERN_SUCCESS;
+ }, ^(vm_address_t segment_addr, xzm_segment_t segment, xzm_chunk_t chunk,
+ xzm_slice_count_t slice_count, vm_address_t start_addr,
+ xzm_xzone_t xz, vm_range_t *ranges, size_t count){
+ // Chunk enumerator
+ xzm_slice_kind_t kind = chunk->xzc_bits.xzcb_kind;
+ if (record_admin && kind == XZM_SLICE_KIND_HUGE_CHUNK) {
+ // Record the info slices of huge segments against the mzone they
+ // belong to
+ vm_range_t header_range = {
+ .address = segment_addr,
+ .size = XZM_METAPOOL_SEGMENT_BLOCK_SIZE,
+ };
+ recorder(task, context, MALLOC_ADMIN_REGION_RANGE_TYPE,
+ &header_range, 1);
+ }
+
+ if (!record_ptr_region && !record_ptr_in_use) {
+ return KERN_SUCCESS;
+ }
+
+ vm_range_t region_range = {
+ .address = start_addr,
+ .size = slice_count * XZM_SEGMENT_SLICE_SIZE,
+ };
+ if (_xzm_slice_kind_uses_xzones(kind)) {
+ if (record_ptr_region) {
+ recorder(task, context, MALLOC_PTR_REGION_RANGE_TYPE,
+ ®ion_range, 1);
+ }
+
+ if (record_ptr_in_use) {
+ recorder(task, context, MALLOC_PTR_IN_USE_RANGE_TYPE, ranges,
+ (unsigned)count);
+ }
+ } else {
+ recorder(task, context, MALLOC_PTR_IN_USE_RANGE_TYPE |
+ MALLOC_PTR_REGION_RANGE_TYPE, ®ion_range, 1);
+ }
+
+ return KERN_SUCCESS;
+ }, !zone_is_main ? NULL : ^(vm_address_t segment_addr,
+ xzm_segment_t segment, xzm_chunk_t span,
+ xzm_slice_count_t slice_count, vm_address_t start_addr){
+ // Main zone span enumerator
+
+ // Record all free spans and chunks with no mzone against the main zone,
+ // with the exception of huge chunks that could be in the reclaim buffer
+ if (record_ptr_region) {
+ bool should_record = true;
+
+#if CONFIG_XZM_DEFERRED_RECLAIM
+ // Unfortunately, there's no way for us to reliably tell whether a
+ // given huge chunk is in the reclaim buffer, because when marking
+ // them free there's a window where we haven't yet stored the
+ // reclaim index in xzs_reclaim_id. So, we err on the side of
+ // caution and just never record them.
+ if (segment->xzs_kind == XZM_SEGMENT_KIND_HUGE &&
+ span->xzc_bits.xzcb_kind == XZM_SLICE_KIND_HUGE_CHUNK) {
+ should_record = false;
+ }
+#endif // CONFIG_XZM_DEFERRED_RECLAIM
+
+ if (should_record) {
+ vm_range_t region_range = {
+ .address = start_addr,
+ .size = slice_count * XZM_SEGMENT_SLICE_SIZE,
+ };
+ recorder(task, context, MALLOC_PTR_REGION_RANGE_TYPE,
+ ®ion_range, 1);
+ }
+ }
+
+ return KERN_SUCCESS;
+ });
+}
+
+#if XZM_DEBUG_ENUMERATOR
+
+struct xzm_debug_recorder_context_s {
+ vm_range_recorder_t *orig_recorder;
+ void *orig_context;
+};
+
+static void
+_xzm_debug_range_recorder(task_t task, void *context, unsigned type,
+ vm_range_t *ranges, unsigned count)
+{
+ const char *type_str = "(invalid?)";
+ switch (type) {
+ case MALLOC_PTR_IN_USE_RANGE_TYPE | MALLOC_PTR_REGION_RANGE_TYPE:
+ type_str = "PTR_IN_USE | PTR_REGION";
+ break;
+ case MALLOC_PTR_IN_USE_RANGE_TYPE:
+ type_str = "PTR_IN_USE";
+ break;
+ case MALLOC_PTR_REGION_RANGE_TYPE:
+ type_str = "PTR_REGION";
+ break;
+ case MALLOC_ADMIN_REGION_RANGE_TYPE:
+ type_str = "ADMIN_REGION";
+ break;
+ default:
+ break;
+ }
+
+ printf("XZM ENUMERATOR: %s (%x) - %p %u ranges\n", type_str, type, ranges,
+ count);
+ for (unsigned i = 0; i < count; i++) {
+ printf("XZM ENUMERATOR: %p[%u]: %p %llu\n", ranges, i,
+ (void *)ranges[i].address, (unsigned long long)ranges[i].size);
+ }
+
+ struct xzm_debug_recorder_context_s *ctx = context;
+ ctx->orig_recorder(task, ctx->orig_context, type, ranges, count);
+}
+
+static kern_return_t
+xzm_debug_ptr_in_use_enumerator(task_t task, void *context, unsigned type_mask,
+ vm_address_t zone_address, memory_reader_t reader,
+ vm_range_recorder_t recorder)
+{
+ struct xzm_debug_recorder_context_s ctx = {
+ .orig_recorder = recorder,
+ .orig_context = context,
+ };
+
+ return xzm_ptr_in_use_enumerator(task, &ctx, type_mask, zone_address,
+ reader, _xzm_debug_range_recorder);
+
+}
+
+#endif // XZM_DEBUG_ENUMERATOR
+
+static void
+xzm_print(task_t task, unsigned level, vm_address_t zone_address,
+ memory_reader_t reader, print_task_printer_t printer)
+{
+ xzm_malloc_zone_t zone;
+ xzm_main_malloc_zone_t main;
+ vm_address_t main_address;
+ bool zone_is_main = false;
+
+ kern_return_t kr = _xzm_introspect_map_zone_and_main(task, zone_address,
+ reader, &zone, &main, &main_address);
+ if (kr) {
+ return;
+ }
+
+ zone_is_main = (zone_address == main_address);
+
+ printer("Begin xzone malloc JSON:\n");
+ printer("{\n");
+ printer("\"desc\": \"xzone malloc\", \n");
+ printer("\"addr\": \"%p\", \n", zone_address);
+ printer("\"segment_size\": %zu, \n", XZM_SEGMENT_SIZE);
+ printer("\"slice_size\": %zu, \n", XZM_SEGMENT_SLICE_SIZE);
+ printer("\"mzone\": %d, \n", (int)zone->xzz_mzone_idx);
+ printer("\"is_main\": %d, \n", zone_is_main);
+ printer("\"max_list_config\": %d, \n", (int)zone->xzz_max_list_config);
+ printer("\"initial_slot_config\": %d, \n", (int)zone->xzz_initial_slot_config);
+ printer("\"slot_initial_threshold\": %u, \n", zone->xzz_slot_initial_threshold);
+ printer("\"max_slot_config\": %d, \n", (int)zone->xzz_max_slot_config);
+
+ // TODO: early allocator info
+
+ __block size_t dispositions_count = 0;
+ __block int *dispositions = NULL;
+ __block vm_address_t dispositions_start_addr = 0;
+ kern_return_t (^print_dispositions)(vm_address_t, vm_size_t, const char *);
+ print_dispositions = ^kern_return_t(vm_address_t addr, vm_size_t size, const char *indent) {
+ kern_return_t kr = KERN_SUCCESS;
+
+ // When operating on a core dump, no pages can be queried
+ if (task == TASK_NULL) {
+ return kr;
+ }
+
+ // If dispositions doesn't cover the full range of this request,
+ // (possibly) reallocate and re-query the VM
+ vm_address_t request_end = addr + size;
+ vm_address_t dispositions_end = dispositions_start_addr +
+ (dispositions_count * vm_page_size);
+ if ((dispositions_start_addr > addr) ||
+ (dispositions_end < request_end)) {
+ dispositions_start_addr = addr;
+
+ // This interface is usually used to query the disposition of a
+ // full segment, so to reduce the number of calls into the vm, request at least a segment
+ size_t request_pages = howmany(size, vm_page_size);
+ if (request_pages < (XZM_SEGMENT_SIZE / vm_page_size)) {
+ request_pages = XZM_SEGMENT_SIZE / vm_page_size;
+ }
+
+ // TODO: mixed page size difficulties
+ if (request_pages > dispositions_count) {
+ if (dispositions) {
+ mach_vm_deallocate(mach_task_self(),
+ (mach_vm_address_t)dispositions,
+ dispositions_count * sizeof(dispositions[0]));
+ dispositions = NULL;
+ }
+
+ dispositions_count = request_pages;
+ kr = mach_vm_allocate(mach_task_self(),
+ (mach_vm_address_t *)&dispositions,
+ dispositions_count * sizeof(dispositions[0]),
+ VM_FLAGS_ANYWHERE);
+ if (kr) {
+ xzm_debug_abort("failed to allocate memory for vm stats");
+ return kr;
+ }
+ }
+
+ mach_vm_size_t mvs_page_span = (mach_vm_size_t)request_pages;
+ kr = mach_vm_page_range_query(task,
+ (mach_vm_address_t)addr, MAX(size, XZM_SEGMENT_SIZE),
+ (mach_vm_address_t)dispositions, &mvs_page_span);
+ if (kr) {
+ xzm_debug_abort("Failed to query vm stats");
+ return kr;
+ }
+ }
+
+ printer("%s \"dispositions\": \"", indent);
+
+ size_t dirty_count = 0;
+ size_t swapped_count = 0;
+ size_t disposition_idx =
+ (addr - dispositions_start_addr) / vm_page_size;
+ for (size_t i = 0; i < (size / vm_page_size); i++) {
+ if (disposition_idx >= dispositions_count) {
+ xzm_debug_abort("inconsistent slice counts");
+ return KERN_FAILURE;
+ }
+
+ int disposition = dispositions[disposition_idx];
+ if (disposition & VM_PAGE_QUERY_PAGE_DIRTY) {
+ dirty_count++;
+ printer("d");
+ } else if (disposition & VM_PAGE_QUERY_PAGE_PAGED_OUT) {
+ swapped_count++;
+ printer("s");
+ } else {
+ printer("c");
+ }
+
+ disposition_idx++;
+ }
+
+ printer("\", \n"); // dispositions
+ printer("%s \"dirty_count\": %zu, \n", indent, dirty_count);
+ printer("%s \"swapped_count\": %zu, \n", indent, swapped_count);
+
+ return KERN_SUCCESS;
+ };
+
+ __block bool first_span = true;
+ __block bool print_segment_dispositions = true;
+ const xzm_segment_enumerator_t segment_enumerator =
+ ^(vm_address_t segment_addr, xzm_segment_t segment,
+ const char *indent) {
+ // Segment enumerator
+
+ if (!first_span) {
+ printer(", ");
+ }
+
+ printer("%s\"%p\": {\n", indent, (void *)segment_addr);
+ printer("%s \"addr\": \"%p\", \n", indent, (void *)segment_addr);
+ xzm_segment_group_id_t sg_id = segment->xzs_segment_group -
+ main->xzmz_segment_groups;
+ printer("%s \"segment_group\": \"%s\", \n", indent,
+ _xzm_segment_group_id_to_string(sg_id));
+ printer("%s \"body_addr\": \"%p\", \n", indent,
+ segment->xzs_segment_body);
+ printer("%s \"used\": %u, \n", indent, segment->xzs_used);
+ printer("%s \"kind\": \"%s\", \n", indent,
+ _xzm_segment_kind_to_string(segment->xzs_kind));
+#if CONFIG_XZM_DEFERRED_RECLAIM
+ if (segment->xzs_reclaim_id == VM_RECLAIM_ID_NULL) {
+ printer("%s \"reclaim_id\": -1, \n", indent);
+ } else {
+ printer("%s \"reclaim_id\": %llu, \n", indent,
+ segment->xzs_reclaim_id);
+ }
+#endif // CONFIG_XZM_DEFERRED_RECLAIM
+ if (print_segment_dispositions) {
+ print_dispositions((vm_address_t)segment->xzs_segment_body,
+ segment->xzs_slice_count * XZM_SEGMENT_SLICE_SIZE, indent);
+ }
+
+ printer("%s \"slice_count\": %u, \n", indent,
+ segment->xzs_slice_count);
+ printer("%s \"slice_entry_count\": %u \n", indent,
+ segment->xzs_slice_entry_count);
+
+ printer("%s}\n", indent); // segment
+ first_span = false;
+
+ return KERN_SUCCESS;
+ };
+
+ if (zone_is_main) {
+ printer("\"bucketing_key\": \"%016llx%016llx\", \n",
+ main->xzmz_bucketing_keys.xbk_key_data[0],
+ main->xzmz_bucketing_keys.xbk_key_data[1]);
+ printer("\"guard_config\": {\n");
+ printer(" \"guards_enabled\": %d, \n",
+ main->xzmz_guard_config.xgpc_enabled);
+ printer(" \"data_guards_enabled\": %d, \n",
+ main->xzmz_guard_config.xgpc_enabled_for_data);
+ printer(" \"tiny_run_size\": %d, \n",
+ main->xzmz_guard_config.xgpc_max_run_tiny);
+ printer(" \"tiny_guard_density\": %d, \n",
+ main->xzmz_guard_config.xgpc_tiny_guard_density);
+ printer(" \"small_run_size\": %d, \n",
+ main->xzmz_guard_config.xgpc_max_run_small);
+ printer(" \"small_guard_density\": %d \n",
+ main->xzmz_guard_config.xgpc_small_guard_density);
+ printer("}, \n");
+ printer("\"chunk_threshold\": %u, \n", main->xzmz_xzone_chunk_threshold);
+ printer("\"ptr_bucket_count\": %d, \n", main->xzmz_ptr_bucket_count);
+ // guard_config
+
+#if CONFIG_MTE
+ printer("\"mte_config\": {\n");
+ printer(" \"enabled\": %d, \n",
+ (int)main->xzmz_base.xzz_memtag_config.enabled);
+ printer(" \"tag_data\": %d, \n",
+ (int)main->xzmz_base.xzz_memtag_config.tag_data);
+ printer(" \"max_block_size\": %d \n",
+ (int)main->xzmz_base.xzz_memtag_config.max_block_size);
+ printer("}, \n"); // mte_config
+#endif // CONFIG_MTE
+
+ printer("\"defer_tiny\": %s, \n", main->xzmz_defer_tiny ?
+ "true" : "false");
+ printer("\"defer_small\": %s, \n", main->xzmz_defer_small ?
+ "true" : "false");
+ printer("\"defer_large\": %s, \n", main->xzmz_defer_large ?
+ "true" : "false");
+ printer("\"deallocate_segment\": %s, \n", main->xzmz_deallocate_segment ?
+ "true" : "false");
+
+ printer("\"use_early_alloc\": %s, \n", main->xzmz_mfm_address ?
+ "true" : "false");
+
+ printer("\"batch_size\": %u, \n", main->xzmz_batch_size);
+
+#if CONFIG_XZM_DEFERRED_RECLAIM
+ if (main->xzmz_reclaim_buffer != NULL) {
+ vm_address_t xzm_buffer_addr = (vm_address_t)main->xzmz_reclaim_buffer;
+ xzm_reclaim_buffer_t xzm_reclaim_buffer;
+ mach_vm_reclaim_ring_t ringbuffer;
+ kr = _xzm_introspect_map_reclaim_buffer(task, reader,
+ xzm_buffer_addr, &xzm_reclaim_buffer, &ringbuffer);
+ if (kr) {
+ xzm_debug_abort("failed to map reclaim buffer");
+ return;
+ }
+ if (ringbuffer != NULL) {
+ printer("\"reclaim_buffer\": { \n");
+ printer(" \"buffer_len\": %llu, \n",
+ ringbuffer->len);
+ printer(" \"max_len\": %llu, \n",
+ ringbuffer->max_len);
+ printer(" \"sampling_period_abs\": %llu, \n", ringbuffer->sampling_period_abs);
+ printer(" \"last_sample_abs\": %llu, \n",
+ ringbuffer->last_sample_abs);
+ printer(" \"reclaimable_bytes\": %llu, \n",
+ os_atomic_load(&ringbuffer->reclaimable_bytes,
+ relaxed));
+ printer(" \"reclaimable_bytes_min\": %llu, \n",
+ os_atomic_load(&ringbuffer->reclaimable_bytes_min,
+ relaxed));
+
+ printer(" \"head\": %llu, \n",
+ os_atomic_load(&ringbuffer->head, relaxed));
+ printer(" \"busy\": %llu, \n",
+ os_atomic_load(&ringbuffer->busy, relaxed));
+ printer(" \"tail\": %llu, \n",
+ os_atomic_load(&ringbuffer->tail, relaxed));
+
+ printer(" \"entries\": [ \n");
+
+ for (mach_vm_reclaim_count_t i = 0; i < ringbuffer->len; i++) {
+ mach_vm_reclaim_entry_t entry = &ringbuffer->entries[i];
+ printer(" { \n");
+ printer(" \"id\": %u, \n", i);
+ printer(" \"address\": \"%p\", \n", entry->address);
+ printer(" \"size\": %u, \n", entry->size);
+ // TODO: add string decoder to libsyscall
+ printer(" \"behavior\": %u \n", entry->behavior);
+ printer(" }");
+ if (i < ringbuffer->len - 1) {
+ printer(",");
+ }
+ printer(" \n");
+ }
+ printer(" ] \n"); // entries
+ }
+ printer("}, \n"); // reclaim buffer
+ }
+
+#endif // CONFIG_XZM_DEFERRED_RECLAIM
+
+ printer("\"allocation_front_count\": %u, \n",
+ main->xzmz_allocation_front_count);
+ printer("\"range_group_count\": %u, \n", main->xzmz_range_group_count);
+ printer("\"range_groups\": {\n");
+
+ size_t range_group_size;
+ if (os_mul_overflow(main->xzmz_range_group_count,
+ sizeof(struct xzm_range_group_s), &range_group_size)) {
+ xzm_debug_abort("failed to compute range group size");
+ return;
+ }
+ struct xzm_range_group_s *mapped_range_groups =
+ (struct xzm_range_group_s *)_xzm_introspect_rebase(main_address,
+ main, main->xzmz_total_size, main->xzmz_range_groups,
+ range_group_size);
+ if (!mapped_range_groups) {
+ xzm_debug_abort("failed to map range_groups");
+ return;
+ }
+
+ for (uint8_t i = 0; i < main->xzmz_range_group_count; i++) {
+ printer(" ");
+ if (i) {
+ printer(", ");
+ }
+ xzm_range_group_t rg = &mapped_range_groups[i];
+ printer("\"%d\": {\n", (int)i);
+ printer(" \"id\": \"%s\", \n",
+ _xzm_range_group_id_to_string(rg->xzrg_id));
+ printer(" \"front\": %d, \n", (int)rg->xzrg_front);
+ printer(" \"lock\": %u, \n", *(uint32_t *)&rg->xzrg_lock);
+ printer(" \"base\": \"%p\", \n", (void *)rg->xzrg_base);
+ printer(" \"size\": %zu, \n", rg->xzrg_size);
+ printer(" \"skip_addr\": \"%p\", \n",
+ (void *)rg->xzrg_skip_addr);
+ printer(" \"skip_size\": %zu, \n", rg->xzrg_skip_size);
+ printer(" \"next\": \"%p\", \n", (void *)rg->xzrg_next);
+ printer(" \"remaining\": %zu, \n", rg->xzrg_remaining);
+ printer(" \"direction\": \"%s\"\n",
+ rg->xzrg_direction == XZM_FRONT_INCREASING ? "up" : "down");
+ printer(" }\n"); // range group
+ }
+
+ printer("}, \n"); // range_groups
+
+ printer("\"segment_group_ids_count\": %u, \n",
+ main->xzmz_segment_group_ids_count);
+ printer("\"segment_group_front_count\": %u, \n",
+ main->xzmz_segment_group_front_count);
+ printer("\"segment_group_count\": %u, \n",
+ main->xzmz_segment_group_count);
+ printer("\"segment_groups\": {\n");
+
+ size_t segment_group_size;
+ if (os_mul_overflow(main->xzmz_segment_group_count,
+ sizeof(struct xzm_segment_group_s), &segment_group_size)) {
+ xzm_debug_abort("failed to compute segment group size");
+ return;
+ }
+ struct xzm_segment_group_s *mapped_segment_groups =
+ (struct xzm_segment_group_s *)_xzm_introspect_rebase(main_address,
+ main, main->xzmz_total_size, main->xzmz_segment_groups,
+ segment_group_size);
+ if (!mapped_segment_groups) {
+ xzm_debug_abort("failed to map segment_groups");
+ return;
+ }
+
+ for (uint8_t i = 0; i < main->xzmz_segment_group_count; i++) {
+ printer(" ");
+ if (i) {
+ printer(", ");
+ }
+ xzm_segment_group_t sg = &mapped_segment_groups[i];
+ printer("\"%d\": {\n", (int)i);
+ printer(" \"id\": \"%s\", \n",
+ _xzm_segment_group_id_to_string(sg->xzsg_id));
+ printer(" \"front\": %d, \n", (int)sg->xzsg_front);
+ printer(" \"range_group\": \"%p\", \n",
+ sg->xzsg_range_group);
+ printer(" \"segment_cache\": { \n");
+ printer(" \"max_count\": %u, \n",
+ (unsigned)sg->xzsg_cache.xzsc_max_count);
+ printer(" \"count\": %u, \n",
+ (unsigned)sg->xzsg_cache.xzsc_count);
+ printer(" \"max_entry_slices\": %u, \n",
+ (unsigned)sg->xzsg_cache.xzsc_max_entry_slices);
+ printer(" \"segments\": { \n");
+ if (sg->xzsg_cache.xzsc_count) {
+ // Segments in the segment cache will not be present in the segment
+ // table, so they must be enumerated here
+ vm_address_t segment_addr = (vm_address_t)TAILQ_FIRST(
+ &sg->xzsg_cache.xzsc_head);
+ while (segment_addr != 0) {
+ xzm_segment_t segment;
+ kern_return_t kr = reader(task, segment_addr,
+ sizeof(struct xzm_segment_s), (void **)&segment);
+ if (kr) {
+ xzm_debug_abort("Failed to map cached segment");
+ return;
+ }
+
+ kr = segment_enumerator(segment_addr, segment,
+ " ");
+ if (kr) {
+ xzm_debug_abort("Failed to enumerate segment");
+ return;
+ }
+ segment_addr = (vm_address_t)TAILQ_NEXT(segment,
+ xzs_cache_entry);
+ }
+ }
+ printer(" } \n"); // segments
+ printer(" } \n"); // segment cache
+ printer(" }\n"); // segment group
+ }
+
+ printer("}, \n"); // segment_groups
+
+ printer("\"xzones\": {\n");
+
+ size_t xzone_size;
+ if (os_mul_overflow(main->xzmz_base.xzz_xzone_count,
+ sizeof(struct xzm_xzone_s), &xzone_size)) {
+ xzm_debug_abort("failed to compute xzone array size");
+ return;
+ }
+ uintptr_t rebased_xzones = _xzm_introspect_rebase(main_address, main,
+ main->xzmz_total_size, main->xzmz_base.xzz_xzones, xzone_size);
+ struct xzm_xzone_s *mapped_xzones =
+ (struct xzm_xzone_s *)rebased_xzones;
+ if (!mapped_xzones) {
+ xzm_debug_abort("failed to map main xzones");
+ return;
+ }
+
+ size_t slots_size;
+ if (os_mul3_overflow(main->xzmz_base.xzz_xzone_count,
+ main->xzmz_base.xzz_slot_count,
+ sizeof(struct xzm_xzone_allocation_slot_s), &slots_size)) {
+ xzm_debug_abort("failed to compute allocation slots size");
+ return;
+ }
+ uintptr_t rebased_slots = _xzm_introspect_rebase(main_address, main,
+ main->xzmz_total_size,
+ main->xzmz_base.xzz_xzone_allocation_slots, slots_size);
+ struct xzm_xzone_allocation_slot_s *mapped_slots =
+ (struct xzm_xzone_allocation_slot_s *)rebased_slots;
+ if (!mapped_slots) {
+ xzm_debug_abort("failed to map main allocation slots");
+ return;
+ }
+
+ for (uint8_t xzidx = XZM_XZONE_INDEX_FIRST;
+ xzidx < zone->xzz_xzone_count; xzidx++) {
+ xzm_xzone_t xz = &mapped_xzones[xzidx];
+ printer(" \"%d\": {\n", (int)xzidx);
+ printer(" \"early_budget\": %u, \n", xz->xz_early_budget);
+ printer(" \"id\": %d, \n", (int)xz->xz_idx);
+ printer(" \"bucket\": %d, \n", (int)xz->xz_bucket);
+ printer(" \"segment_group_id\": %d, \n",
+ xz->xz_segment_group_id);
+ printer(" \"front\": %d, \n", xz->xz_front);
+ printer(" \"batch_count\": %u, \n",
+ xz->xz_block_size <= XZM_TINY_BLOCK_SIZE_MAX ?
+ xz->xz_batch_list.xzch_batch_count :
+ xz->xz_chunkq_batch_count);
+ printer(" \"block_size\": %llu, \n", xz->xz_block_size);
+ printer(" \"chunk_count\": %llu, \n", xz->xz_chunk_count);
+ printer(" \"chunk_capacity\": %u, \n", xz->xz_chunk_capacity);
+ printer(" \"sequestered\": %d,\n", (int)xz->xz_sequestered);
+ printer(" \"list_config\": \"%s\",\n",
+ _xzm_slot_config_to_string(xz->xz_list_config));
+ printer(" \"slot_config\": \"%s\",\n",
+ _xzm_slot_config_to_string(xz->xz_slot_config));
+ printer(" \"allocation_slots\": [\n");
+
+ for (xzm_allocation_index_t slot_idx = 0;
+ slot_idx < zone->xzz_slot_count;
+ slot_idx++) {
+ xzm_xzone_allocation_slot_t xas = &mapped_slots[
+ (slot_idx * zone->xzz_xzone_count) + xzidx];
+ printer(" {\n");
+
+ if (xz->xz_block_size <= XZM_TINY_BLOCK_SIZE_MAX ||
+ zone->xzz_small_freelist_enabled) {
+ printer(" \"atomic_value\": \"0x%llx\",\n",
+ xas->xas_atomic.xasa_value);
+ printer(" \"xsg_locked\": \"0x%llx\",\n",
+ xas->xas_atomic.xasa_gate.xsg_locked);
+ printer(" \"xsg_waiters\": \"0x%llx\",\n",
+ xas->xas_atomic.xasa_gate.xsg_waiters);
+ printer(" \"xsc_ptr\": \"0x%llx\",\n",
+ xas->xas_atomic.xasa_chunk.xsc_ptr);
+
+ printer(" \"operations\": %lu,\n",
+ xas->xas_counters.xsc_ops);
+ printer(" \"contentions\": %lu,\n",
+ xas->xas_counters.xsc_contentions);
+
+ printer(" \"slot_config\": \"%s\",\n",
+ _xzm_slot_config_to_string(
+ xas->xas_counters.xsc_slot_config));
+ } else {
+ printer(" \"chunk\": \"%p\",\n",
+ (void *)xas->xas_chunk);
+ printer(" \"allocations\": %lu,\n",
+ xas->xas_allocs);
+ printer(" \"contentions\": %lu,\n",
+ xas->xas_contentions);
+ }
+
+ printer(" \"last_chunk_empty_ts\": %llu\n",
+ xas->xas_last_chunk_empty_ts);
+ printer(" }");
+ if (slot_idx < zone->xzz_slot_count - 1) {
+ printer(",");
+ }
+ printer("\n");
+ }
+
+ printer(" ]\n"); // allocation slots
+ printer(" }"); // xzone
+ if (xzidx < zone->xzz_xzone_count - 1) {
+ printer(",");
+ }
+ printer("\n");
+ }
+
+ printer("}, \n"); // xzones
+
+#if CONFIG_XZM_THREAD_CACHE
+ printer("\"thread_cache_enabled\": %s, \n",
+ zone->xzz_thread_cache_enabled ? "true" : "false");
+ printer("\"thread_cache_activation_period\": %lu, \n",
+ zone->xzz_thread_cache_xzone_activation_period);
+ printer("\"thread_cache_activation_contentions\": %lu, \n",
+ zone->xzz_thread_cache_xzone_activation_contentions);
+ printer("\"thread_cache_activation_time\": %llu, \n",
+ zone->xzz_thread_cache_xzone_activation_time);
+
+ if (zone->xzz_thread_cache_enabled) {
+ printer("\"thread_caches\": [ \n");
+ __block bool first_thread_cache = true;
+ kr = _xzm_introspect_enumerate_thread_caches(task, reader, main,
+ ^(vm_address_t thread_cache_addr, xzm_thread_cache_t tc){
+ printer(" ");
+ if (!first_thread_cache) {
+ printer(", ");
+ } else {
+ first_thread_cache = false;
+ }
+ printer("{\n");
+ printer(" \"thread\": \"%p\",\n", (void *)tc->xtc_thread);
+ printer(" \"xz_caches\": {\n", (void *)tc->xtc_thread);
+ for (uint8_t xzidx = XZM_XZONE_INDEX_FIRST;
+ xzidx < zone->xzz_thread_cache_xzone_count; xzidx++) {
+ xzm_xzone_thread_cache_t cache = &tc->xtc_xz_caches[xzidx];
+
+ printer(" \"%d\": {\n", (int)xzidx);
+ printer(" \"xz_idx\": %d, \n", (int)xzidx);
+
+ uint16_t head = cache->xztc_head;
+ if (head == XZM_XZONE_NOT_CACHED) {
+ printer(" \"head\": \"NOT_CACHED\", \n");
+ printer(" \"timestamp\": \"%llu\", \n",
+ cache->xztc_timestamp);
+ printer(" \"contentions\": \"%llu\", \n",
+ (uint64_t)cache->xztc_contentions);
+ printer(" \"allocs\": \"%llu\" \n",
+ (uint64_t)cache->xztc_allocs);
+ } else if (head == XZM_XZONE_CACHE_EMPTY) {
+ printer(" \"head\": \"EMPTY\" \n");
+ } else {
+ printer(" \"head\": \"0x%llx\", \n",
+ (uint64_t)head);
+ printer(" \"chunk\": \"%p\", \n",
+ cache->xztc_chunk);
+ printer(" \"chunk_start\": \"%p\", \n",
+ cache->xztc_chunk_start);
+ printer(" \"head_seqno\": \"0x%llx\", \n",
+ (uint64_t)cache->xztc_head_seqno);
+ printer(" \"free_count\": \"0x%llx\", \n",
+ (uint64_t)cache->xztc_free_count);
+ printer(" \"seqno\": \"0x%llx\" \n",
+ (uint64_t)cache->xztc_seqno);
+ }
+
+
+ printer(" }"); // xzone thread cache
+ if (xzidx < zone->xzz_thread_cache_xzone_count - 1) {
+ printer(",");
+ }
+ printer("\n");
+ }
+ printer(" } \n"); // xzone thread caches
+ printer(" } \n"); // thread cache
+
+ return KERN_SUCCESS;
+ });
+
+ printer("], \n"); // thread_caches
+ }
+#endif // CONFIG_XZM_THREAD_CACHE
+ }
+
+ printer("\"spans\": {\n");
+
+ first_span = true;
+ // un-cached segments will have their dispositions enumerated via the
+ // spans/chunks they contain
+ print_segment_dispositions = false;
+
+ kr = _xzm_introspect_enumerate(task, reader, zone_address,
+ zone, main_address, main,
+ /* include_blocks */false,
+ ^(vm_address_t slab_addr, vm_size_t slab_size, xzm_metapool_id_t
+ metapool_id) {
+ // Metapool slab enumerator
+
+ printer(" ");
+ if (!first_span) {
+ printer(", ");
+ }
+
+ printer("\"%p\": {\n", (void *)slab_addr);
+ printer(" \"addr\": \"%p\", \n", (void *)slab_addr);
+ printer(" \"kind\": \"%s\", \n",
+ _xzm_metapool_id_to_string(metapool_id));
+ print_dispositions(slab_addr, slab_size, " ");
+ printer(" \"size\": %u \n",
+ slab_size);
+
+ printer(" }\n");
+ first_span = false;
+
+ return KERN_SUCCESS;
+ },
+ segment_enumerator,
+ ^(vm_address_t segment_addr, xzm_segment_t segment, xzm_chunk_t chunk,
+ xzm_slice_count_t slice_count, vm_address_t start_addr,
+ xzm_xzone_t xz, vm_range_t *ranges, size_t count){
+ // Chunk enumerator
+
+ printer(" ");
+ if (!first_span) {
+ printer(", ");
+ }
+
+ printer("\"%p\": {\n", (void *)start_addr);
+ printer(" \"addr\": \"%p\", \n", (void *)start_addr);
+ printer(" \"metadata_addr\": \"%p\", \n", (void *)(segment_addr +
+ ((vm_address_t)chunk - (vm_address_t)segment)));
+ printer(" \"mzone\": %d, \n", (int)chunk->xzc_mzone_idx);
+ printer(" \"xzone\": %d, \n", (int)chunk->xzc_xzone_idx);
+ printer(" \"segment\": \"%p\", \n", (void *)segment_addr);
+ printer(" \"segment_group\": %zu, \n",
+ segment->xzs_segment_group - main->xzmz_segment_groups);
+
+ xzm_slice_kind_t kind = chunk->xzc_bits.xzcb_kind;
+ const char *kind_str = _xzm_slice_kind_to_string(kind);
+ printer(" \"kind\": \"%s\", \n", kind_str);
+ printer(" \"slice_count\": %u, \n", slice_count);
+ printer(" \"block_size\": %u, \n",
+ xz ? (unsigned)xz->xz_block_size : 0);
+ printer(" \"in_use\": 1, \n");
+
+ xzm_slice_count_t slice_index = (xzm_slice_count_t)
+ (chunk - segment->xzs_slices);
+ xzm_xzone_slice_metadata_u *metadata =
+ &segment->xzs_slice_metadata[slice_index];
+ printer(" \"slice_metadata\": \"%p\", \n",
+ metadata->xzsm_batch_next);
+
+ kern_return_t kr = print_dispositions(start_addr, slice_count *
+ XZM_SEGMENT_SLICE_SIZE, " ");
+ if (kr) {
+ return kr;
+ }
+
+ if (_xzm_slice_kind_uses_xzones(kind)) {
+ printer(" \"bucket\": %u,\n", (unsigned)xz->xz_bucket);
+ }
+
+ switch (kind) {
+ case XZM_SLICE_KIND_TINY_CHUNK:
+ case XZM_SLICE_KIND_SMALL_FREELIST_CHUNK:
+ printer(" \"meta\": \"0x%llx\",\n",
+ chunk->xzc_atomic_meta.xca_value);
+ printer(" \"xca_alloc_head\": \"0x%llx\",\n",
+ chunk->xzc_atomic_meta.xca_alloc_head);
+ printer(" \"xca_free_count\": \"0x%llx\",\n",
+ chunk->xzc_atomic_meta.xca_free_count);
+ printer(" \"xca_alloc_idx\": \"0x%llx\",\n",
+ chunk->xzc_atomic_meta.xca_alloc_idx);
+ printer(" \"xca_on_partial_list\": \"0x%llx\",\n",
+ chunk->xzc_atomic_meta.xca_on_partial_list);
+ printer(" \"xca_on_empty_list\": \"0x%llx\",\n",
+ chunk->xzc_atomic_meta.xca_on_empty_list);
+ printer(" \"xca_walk_locked\": \"0x%llx\",\n",
+ chunk->xzc_atomic_meta.xca_walk_locked);
+ printer(" \"xca_head_seqno\": \"0x%llx\",\n",
+ chunk->xzc_atomic_meta.xca_head_seqno);
+ printer(" \"xca_seqno\": \"0x%llx\",\n",
+ chunk->xzc_atomic_meta.xca_seqno);
+ break;
+ case XZM_SLICE_KIND_SMALL_CHUNK:
+ printer(" \"free\": \"0x%x\",\n", chunk->xzc_free);
+ printer(" \"used\": %u,\n", (unsigned)chunk->xzc_used);
+ printer(" \"alloc_idx\": %u,\n",
+ (unsigned)chunk->xzc_alloc_idx);
+ break;
+ default:
+ break;
+ }
+
+ printer(" \"is_preallocated\": %d,\n",
+ (int)chunk->xzc_bits.xzcb_preallocated);
+ printer(" \"is_pristine\": %d\n",
+ (int)chunk->xzc_bits.xzcb_is_pristine);
+ printer(" }\n");
+ first_span = false;
+
+ return KERN_SUCCESS;
+ }, !zone_is_main ? NULL : ^(vm_address_t segment_addr,
+ xzm_segment_t segment, xzm_chunk_t span,
+ xzm_slice_count_t slice_count, vm_address_t start_addr){
+ // Main zone span enumerator
+
+ // TODO: better sharing with the chunk enumerator
+ printer(" ");
+ if (!first_span) {
+ printer(", ");
+ }
+
+ printer("\"%p\": {\n", (void *)start_addr);
+ printer(" \"addr\": \"%p\", \n", (void *)start_addr);
+ printer(" \"metadata_addr\": \"%p\", \n", (void *)(segment_addr +
+ ((vm_address_t)span - (vm_address_t)segment)));
+ printer(" \"mzone\": %d, \n", (int)span->xzc_mzone_idx);
+ printer(" \"xzone\": %d, \n", (int)span->xzc_xzone_idx);
+ printer(" \"segment\": \"%p\", \n", (void *)segment_addr);
+ printer(" \"segment_group\": %zu, \n",
+ segment->xzs_segment_group - main->xzmz_segment_groups);
+
+ xzm_slice_kind_t kind = span->xzc_bits.xzcb_kind;
+ const char *kind_str = _xzm_slice_kind_to_string(kind);
+ printer(" \"kind\": \"%s\", \n", kind_str);
+ printer(" \"slice_count\": %u, \n", slice_count);
+
+ xzm_slice_count_t slice_index = (xzm_slice_count_t)
+ (span - segment->xzs_slices);
+ xzm_xzone_slice_metadata_u *metadata =
+ &segment->xzs_slice_metadata[slice_index];
+ printer(" \"slice_metadata\": \"%p\", \n",
+ metadata->xzsm_batch_next);
+
+ kern_return_t kr = print_dispositions(start_addr, slice_count *
+ XZM_SEGMENT_SLICE_SIZE, " ");
+ if (kr) {
+ return kr;
+ }
+
+ printer(" \"is_preallocated\": %d,\n",
+ (int)span->xzc_bits.xzcb_preallocated);
+ printer(" \"in_use\": 0 \n");
+ printer(" }\n"); // span
+ first_span = false;
+
+ return KERN_SUCCESS;
+ });
+
+ if (dispositions) {
+ mach_vm_deallocate(mach_task_self(), (mach_vm_address_t)dispositions,
+ dispositions_count * sizeof(dispositions[0]));
+ }
+
+ if (kr) {
+ return;
+ }
+
+ printer("}\n"); // spans
+ printer("}\n"); // overall
+ printer("End xzone malloc JSON\n");
+}
+
+static void
+xzm_print_task(task_t task, unsigned level, vm_address_t zone_address,
+ memory_reader_t reader, print_task_printer_t printer)
+{
+ xzm_print(task, level, zone_address, reader, printer);
+}
+
+static void
+xzm_print_self(xzm_malloc_zone_t zone, boolean_t verbose)
+{
+ xzm_print(mach_task_self(), verbose ? MALLOC_VERBOSE_PRINT_LEVEL : 0,
+ (vm_address_t)zone, _malloc_default_reader, malloc_report_simple);
+}
+
+static kern_return_t
+xzm_statistics(task_t task, vm_address_t zone_address,
+ memory_reader_t reader, print_task_printer_t printer,
+ malloc_statistics_t *stats)
+{
+ // It's straightforward to compute blocks_in_use and size_in_use from an
+ // enumeration pass.
+ //
+ // size_allocated, which is the sum of the virtual sizes of all live
+ // non-metadata VM reservations, is also fairly easy.
+ //
+ // max_size_in_use, which is supposed to track the "high water mark of
+ // touched memory", is harder:
+ // - nano doesn't even try
+ // - szone malloc sort of does, but its accounting is wrong in several
+ // ways:
+ // - it doesn't account for madvise, so its calculation for "in-use"
+ // memory in regions is too pessimistic (not everything outside the
+ // pristine parts of a region was necessarily all in use at the same
+ // time)
+ // - regions that only became partially full before being deallocated
+ // are incorrectly assumed to have been fully used
+ // - it doesn't keep track of the high water mark in large, so the
+ // current size of large is taken as the max
+ //
+ // Keeping a running max of the sizes of live chunks in each segment group
+ // seems like it could be reasonable. We'd also have to somehow deal with
+ // sequestered empty chunks, though, since they look "live" to the range
+ // group as-is.
+ //
+ // Nano's policy of not even trying seems best, if we can get away with it,
+ // and the lack of complaints from default enablement of nano on macOS seems
+ // like strong evidence that we can, so that's what we should go with unless
+ // a compelling need to do otherwise arises.
+ (void)printer;
+ *stats = (malloc_statistics_t){ 0 };
+
+ reader = reader_or_in_memory_fallback(reader, task);
+
+ xzm_malloc_zone_t zone;
+ xzm_main_malloc_zone_t main;
+ vm_address_t main_address;
+ bool zone_is_main = false;
+
+ kern_return_t kr = _xzm_introspect_map_zone_and_main(task, zone_address,
+ reader, &zone, &main, &main_address);
+ if (kr) {
+ return kr;
+ }
+
+ zone_is_main = (zone_address == main_address);
+
+ if (zone_is_main) {
+ vm_address_t mfm_addr = (vm_address_t)main->xzmz_mfm_address;
+ if (mfm_addr) {
+ mfm_introspect.task_statistics(task, mfm_addr, reader, stats);
+
+ // We don't know how to report max_size_in_use, so don't confuse things
+ // by only including the max_size_in_use from mfm
+ stats->max_size_in_use = 0;
+ }
+ }
+
+ return _xzm_introspect_enumerate(task, reader, zone_address,
+ zone, main_address, main, /* include_blocks */ false,
+ ^(vm_address_t slab_addr, vm_size_t slab_size, xzm_metapool_id_t mp_id){
+ // Metapool slab enumerator
+ return KERN_SUCCESS;
+ }, ^(vm_address_t segment_addr, xzm_segment_t segment, const char *indent){
+ // Segment enumerator
+
+ // Nothing interesting for stats at the segment level
+ return KERN_SUCCESS;
+ }, ^(vm_address_t segment_addr, xzm_segment_t segment, xzm_chunk_t chunk,
+ xzm_slice_count_t slice_count, vm_address_t start_addr,
+ xzm_xzone_t xz, vm_range_t *ranges, size_t count){
+ // Chunk enumerator
+ size_t chunk_size = slice_count * XZM_SEGMENT_SLICE_SIZE;
+ size_t used = 0;
+
+ xzm_slice_kind_t kind = chunk->xzc_bits.xzcb_kind;
+ switch (kind) {
+ case XZM_SLICE_KIND_TINY_CHUNK:
+ case XZM_SLICE_KIND_SMALL_FREELIST_CHUNK:;
+ xzm_chunk_atomic_meta_u meta = chunk->xzc_atomic_meta;
+ uint32_t capacity = xz->xz_chunk_capacity;
+ if (meta.xca_alloc_head != XZM_FREE_MADVISING &&
+ meta.xca_alloc_head != XZM_FREE_MADVISED) {
+ used = (size_t)(capacity - meta.xca_free_count);
+ stats->blocks_in_use += used;
+ stats->size_in_use += used * xz->xz_block_size;
+ }
+ break;
+ case XZM_SLICE_KIND_SMALL_CHUNK:
+ used = chunk->xzc_used;
+ stats->blocks_in_use += used;
+ stats->size_in_use += used * xz->xz_block_size;
+ break;
+ default:
+ stats->blocks_in_use++;
+ stats->size_in_use += chunk_size;
+ break;
+ }
+ stats->size_allocated += chunk_size;
+
+ return KERN_SUCCESS;
+ }, !zone_is_main ? NULL : ^(vm_address_t segment_addr,
+ xzm_segment_t segment, xzm_chunk_t span,
+ xzm_slice_count_t slice_count, vm_address_t start_addr){
+ // Main zone span enumerator
+
+ // Record all free spans and chunks with no mzone against the main zone
+ stats->size_allocated += slice_count * XZM_SEGMENT_SLICE_SIZE;
+
+ return KERN_SUCCESS;
+ });
+}
+
+static void
+xzm_statistics_self(xzm_malloc_zone_t zone, malloc_statistics_t *stats)
+{
+ if (_xzm_malloc_zone_is_main(zone)) {
+ mfm_lock();
+ }
+
+ xzm_force_lock(zone);
+ xzm_statistics(mach_task_self(), (vm_address_t)zone, _malloc_default_reader,
+ malloc_report_simple, stats);
+ xzm_force_unlock(zone);
+ if (_xzm_malloc_zone_is_main(zone)) {
+ mfm_unlock();
+ }
+}
+
+static void
+xzm_statistics_task(task_t task, vm_address_t zone_address,
+ memory_reader_t reader, malloc_statistics_t *stats)
+{
+ xzm_statistics(task, zone_address, reader, NULL, stats);
+}
+
+
+const struct malloc_introspection_t xzm_malloc_zone_introspect = {
+#if XZM_DEBUG_ENUMERATOR
+ .enumerator = (void *)xzm_debug_ptr_in_use_enumerator,
+#else // XZM_DEBUG_ENUMERATOR
+ .enumerator = (void *)xzm_ptr_in_use_enumerator,
+#endif // XZM_DEBUG_ENUMERATOR
+ .print_task = (void *)xzm_print_task,
+ .good_size = (void *)xzm_good_size,
+ .check = (void *)xzm_check,
+ .print = (void *)xzm_print_self,
+ .statistics = (void *)xzm_statistics_self,
+ .task_statistics = (void*)xzm_statistics_task,
+ .log = (void *)xzm_log,
+ .zone_locked = (void *)xzm_locked,
+ .force_lock = (void *)xzm_force_lock,
+ .force_unlock = (void *)xzm_force_unlock,
+ .reinit_lock = (void *)xzm_reinit_lock,
+ // discharge checking is a vestigial interface relating to the historical
+ // ObjC gc - not to be implemented
+ .enable_discharge_checking = NULL,
+ .disable_discharge_checking = NULL,
+#ifdef __BLOCKS__
+ .enumerate_discharged_pointers = NULL,
+#else // __BLOCKS__
+ .enumerate_unavailable_without_blocks = NULL,
+#endif // __BLOCKS__
+ .zone_type = MALLOC_ZONE_TYPE_XZONE,
+};
+
+#endif // CONFIG_XZONE_MALLOC