Loading...
tests/malloc_create_zone_tests.c /dev/null libmalloc-657.80.3
--- /dev/null
+++ libmalloc/libmalloc-657.80.3/tests/malloc_create_zone_tests.c
@@ -0,0 +1,562 @@
+//
+//  malloc_create_xzone_tests.c
+//  libmalloc
+//
+//  Tests that we can create additional xzone malloc zones through
+//  malloc_create_zone, and that these zones behave properly
+//
+
+#include <darwintest.h>
+#include <stdlib.h>
+#include <malloc/malloc.h>
+#include <../src/internal.h>
+
+#if CONFIG_XZONE_MALLOC
+
+#define PTR_EQUALS(a, b) (((uintptr_t)a & (XZM_LIMIT_ADDRESS - 1)) == \
+			((uintptr_t)b & (XZM_LIMIT_ADDRESS - 1)))
+
+T_DECL(malloc_xzone_create_zone, "Test malloc_create_zone with xzones enabled",
+		T_META_ENVVAR("MallocProbGuard=0"),
+		T_META_TAG_XZONE_ONLY)
+{
+	// Check that we can create a new zone, and that it is an xzone
+	malloc_zone_t *new_zone = malloc_create_zone(0, 0);
+	T_ASSERT_NOTNULL(new_zone, "Create new zone");
+
+	T_ASSERT_GE(new_zone->version, 14, "New zone supports zone_type");
+	T_ASSERT_EQ(new_zone->introspect->zone_type, MALLOC_ZONE_TYPE_XZONE,
+			"New zone is of type XZONE");
+
+	void *ptr = malloc_zone_malloc(new_zone, 65536);
+	T_ASSERT_NOTNULL(ptr, "allocate from new zone");
+	T_ASSERT_GE(malloc_size(ptr), 65536, "pointer size works");
+
+	malloc_destroy_zone(new_zone);
+
+	// Once again, but make a small enough allocation to be served from the
+	// early allocator
+	new_zone = malloc_create_zone(0, 0);
+	T_ASSERT_NOTNULL(new_zone, "Create new zone");
+	T_ASSERT_GE(new_zone->version, 14, "New zone supports zone_type");
+	T_ASSERT_EQ(new_zone->introspect->zone_type, 1,
+			"New zone is of type XZONE");
+	ptr = malloc_zone_malloc(new_zone, 64);
+	T_ASSERT_NOTNULL(ptr, "allocate from new zone");
+	T_ASSERT_GE(malloc_size(ptr), 64, "pointer size works");
+	malloc_destroy_zone(new_zone);
+	T_ASSERT_EQ(malloc_size(ptr), 0, "pointer is freed by destroying zone");
+	T_PASS("success");
+}
+
+T_DECL(malloc_xzone_create_many_zones, "Register 2048 xzm zones, 256 at a time",
+		T_META_TAG_XZONE_ONLY)
+{
+	const int num_zones = 256;
+	malloc_zone_t *zone_array[num_zones];
+	for (int i = 0; i < (2048 / num_zones); i++) {
+		for (int j = 0; j < num_zones; j++) {
+			zone_array[j] = malloc_create_zone(0, 0);
+			T_QUIET;
+			T_ASSERT_NOTNULL(zone_array[j], "create zone %d", i * num_zones + j);
+		}
+		for (int j = 0; j < num_zones; j++) {
+			zone_array[j] = NULL;
+		}
+	}
+}
+
+// This test would exhaust the 64GB embedded address space, so only run on OSX
+#if TARGET_OS_OSX
+T_DECL(malloc_xzone_too_many_zones, "Register more zones than xzm supports",
+		T_META_ENVVAR("MallocProbGuard=0"),
+		T_META_TAG_XZONE_ONLY)
+{
+	// XZM uses a uint16_t for the mzone unique ID, so we can only register
+	// UINT16_MAX - 2 new zones (ID 1 is reserved for the main zone, and ID 0 is
+	// a magic invalid value). After running out of IDs, check that
+	// malloc_create_zone falls back to a scalable zone
+	const size_t num_xzone_zones = UINT16_MAX - 2;
+	malloc_zone_t *xzm_zones[num_xzone_zones];
+	for (int i = 0; i < num_xzone_zones; i++) {
+		xzm_zones[i] = malloc_create_zone(0, 0);
+		T_QUIET; T_ASSERT_NOTNULL(xzm_zones[i], "Create new zone %d", i);
+		T_QUIET;
+		T_ASSERT_GE(xzm_zones[i]->version, 14, "New zone supports zone_type");
+		T_QUIET;
+		T_ASSERT_EQ(xzm_zones[i]->introspect->zone_type, MALLOC_ZONE_TYPE_XZONE,
+				"New zone is of type XZONE");
+	}
+
+	malloc_zone_t *szone = malloc_create_zone(0, 0);
+	T_ASSERT_NOTNULL(szone, "Fallback to new scalable zone");
+	if (szone->version >= 14) {
+		T_ASSERT_EQ(szone->introspect->zone_type, MALLOC_ZONE_TYPE_XZONE,
+				"Fallback shouldn't be xzone malloc zone");
+	} else {
+		T_LOG("Fallback malloc zone doesn't support zone_type");
+	}
+
+	malloc_destroy_zone(szone);
+	for (int i = 0; i < num_xzone_zones; i++) {
+		malloc_destroy_zone(xzm_zones[i]);
+	}
+}
+#endif // TARGET_OS_OSX
+
+struct pointers_expected {
+	size_t num_pointers;
+	vm_range_t ranges[0];
+};
+
+// Once we find an enumerated pointer, set the expected size of it to the
+// maximum value so that re-enumerating it causes an error
+const vm_size_t already_seen = (vm_size_t)-1;
+
+static void
+pointer_recorder(task_t task, void *context, unsigned type, vm_range_t *ranges,
+		unsigned count)
+{
+	T_QUIET; T_ASSERT_NOTNULL(context, "Received expected pointer ranges");
+	struct pointers_expected *expected = context;
+
+	for (unsigned i = 0; i < count; i++) {
+		if (!(type & MALLOC_PTR_IN_USE_RANGE_TYPE)) {
+			continue;
+		}
+		// Make sure that this pointer is in the expected list
+		bool found_ptr = false;
+		for (int j = 0; j < expected->num_pointers; j++) {
+			if (PTR_EQUALS(expected->ranges[j].address, ranges[i].address)) {
+				T_QUIET; T_ASSERT_NE(already_seen, expected->ranges[j].size,
+						"enumerated %p twice", (void *)ranges[i].address);
+				T_ASSERT_LE(expected->ranges[j].size, ranges[i].size,
+						"Allocation %i has correct size", j);
+				// Set the size to the max value so that enumerating it twice
+				expected->ranges[j].size = already_seen;
+				found_ptr = true;
+			}
+		}
+		T_QUIET;
+		T_ASSERT_TRUE(found_ptr, "Enumerated unexpected_ptr  %p (size 0x%lx)",
+			(void *)ranges[i].address, ranges[i].size);
+	}
+}
+
+static kern_return_t
+memory_reader(task_t remote_task, vm_address_t remote_address, vm_size_t size,
+		void **local_memory)
+{
+	if (local_memory) {
+		*local_memory = (void*)remote_address;
+		return KERN_SUCCESS;
+	}
+	return KERN_FAILURE;
+}
+
+T_DECL(malloc_new_xzone_enumerate, "Test non-default xzone enumerator",
+		T_META_ENVVAR("MallocProbGuard=0"),
+		T_META_TAG_XZONE_ONLY)
+{
+	malloc_zone_t *new_zone = malloc_create_zone(0, 0);
+	T_ASSERT_GE(new_zone->version, 14, "New zone isn't an xzone");
+	T_ASSERT_EQ(new_zone->introspect->zone_type, MALLOC_ZONE_TYPE_XZONE,
+		"New zone isn't an xzone");
+
+	// Make 4 tiny allocations, 2 small allocations, 1 large and 1 huge
+	// allocation from the new zone, and assert that the enumerator sees them
+	const size_t num_pointers = 8;
+	struct pointers_expected *pointers = malloc(sizeof(*pointers) +
+			num_pointers * sizeof(vm_range_t));
+	T_ASSERT_NOTNULL(pointers, "Allocated space for expected ranges");
+	pointers->num_pointers = num_pointers;
+	pointers->ranges[0].size = 64;
+	pointers->ranges[1].size = 256;
+	pointers->ranges[2].size = 1024;
+	pointers->ranges[3].size = 2048; // TINY
+	pointers->ranges[4].size = 10000;
+	pointers->ranges[5].size = 30000; // SMALL
+	pointers->ranges[6].size = 65536; // LARGE
+	pointers->ranges[7].size = 4000000; // HUGE
+
+	for (int i = 0; i < num_pointers; i++) {
+		size_t size = pointers->ranges[i].size;
+		void *addr = malloc_zone_malloc(new_zone, size);
+		T_ASSERT_NOTNULL(addr, "Malloc %zu bytes from new zone", size);
+		pointers->ranges[i].address = (mach_vm_address_t)addr;
+	}
+
+	// Run the enumerator
+	kern_return_t kr = new_zone->introspect->enumerator(mach_task_self(),
+			pointers, MALLOC_PTR_IN_USE_RANGE_TYPE, (vm_address_t)new_zone,
+			memory_reader, pointer_recorder);
+	T_ASSERT_EQ(KERN_SUCCESS, kr, "Enumerator returned success");
+
+	// Check that all pointers were seen
+	for (int i = 0; i < pointers->num_pointers; i++) {
+		// The enumerator sets the size in this structure for every address
+		// it sees to already_seen
+		T_ASSERT_EQ(already_seen, pointers->ranges[i].size, "Found pointer %i",
+				i);
+	}
+
+	// Free all pointers to pass the leaks check
+	for (int i = 0; i < num_pointers; i++) {
+		free((void*)pointers->ranges[i].address);
+	}
+	free(pointers);
+}
+
+struct worker_thread_data {
+	bool done;
+	malloc_zone_t *zone;
+};
+static void *
+worker_thread(void *arg)
+{
+	struct worker_thread_data *data = (struct worker_thread_data*)arg;
+	bool *done = &data->done;
+	malloc_zone_t *zone = data->zone;
+	while (!os_atomic_load(done, relaxed)) {
+		size_t size = 1 << (rand() % 16);
+		void *ptr = malloc_zone_malloc(zone, size);
+		T_QUIET; T_ASSERT_NOTNULL(ptr, "malloc failed");
+		free(ptr);
+	}
+}
+
+T_DECL(malloc_fork_with_xzone, "Test that we can fork with a non-default xzone",
+		T_META_ENVVAR("MallocProbGuard=0"),
+		T_META_TAG_XZONE_ONLY)
+{
+	malloc_zone_t *new_zone = malloc_create_zone(0, 0);
+	T_ASSERT_GE(new_zone->version, 14, "New zone isn't an xzone");
+	T_ASSERT_EQ(new_zone->introspect->zone_type, MALLOC_ZONE_TYPE_XZONE,
+		"New zone isn't an xzone");
+
+	// Create some worker threads to do constant allocations and frees, to try
+	// to expose any multi-threading bugs
+	struct worker_thread_data data = {
+		.done = false,
+		.zone = new_zone,
+	};
+	const int num_threads = 8;
+	pthread_t worker_threads[8] = { NULL };
+	for (int i = 0; i < num_threads; i++) {
+		pthread_create(&worker_threads[i], NULL, &worker_thread, &data);
+	}
+
+	// Wait a few microseconds so that the worker threads have had a chance to
+	// run
+	usleep(100);
+
+	// Make 2 allocations, fork, and then ensure that both can see the
+	// allocations and free them
+	void *ptr1 = malloc_zone_malloc(new_zone, 64);
+	void *ptr2 = malloc_zone_malloc(new_zone, 65536);
+	T_ASSERT_NOTNULL(ptr1, "TINY allocation");
+	T_ASSERT_NOTNULL(ptr2, "LARGE allocation");
+
+	pid_t pid;
+	if ((pid = fork()) == 0) {
+		// Child - check that the pointers exist and can be freed, and exit
+		// nonzero if we run into any problems
+		if (malloc_size(ptr1) < 64) {
+			fprintf(stderr, "TINY ptr %p has unexpected size %zu after fork",
+					ptr1, malloc_size(ptr1));
+			exit(1);
+		} else if (malloc_size(ptr2) < 65536) {
+			fprintf(stderr, "LARGE ptr %p has unexpected size %zu after fork",
+					ptr2, malloc_size(ptr2));
+			exit(2);
+		}
+		free(ptr1);
+		free(ptr2);
+		// Xzone Malloc can mistakenly report tiny freed chunks as allocated if
+		// the chunk is madvised, treat a malloc_size mismatch as non-fatal
+		if (malloc_size(ptr1) != 0 && !malloc_engaged_secure_allocator()) {
+			fprintf(stderr, "TINY ptr %p has unexpected size %zu after free",
+					ptr1, malloc_size(ptr1));
+			exit(3);
+		} else if (malloc_size(ptr2) != 0) {
+			fprintf(stderr, "LARGE ptr %p has unexpected size %zu after free",
+					ptr2, malloc_size(ptr2));
+			exit(4);
+		} else {
+			// Success
+			exit(0);
+		}
+	} else if (pid == -1) {
+		T_ASSERT_FAIL("Fork failed");
+	}
+
+	// Parent
+	int child_status = -1;
+	pid_t child_pid = wait(&child_status);
+	T_ASSERT_EQ(pid, child_pid, "Expected pid from child");
+	T_ASSERT_TRUE(WIFEXITED(child_status), "Child called exit");
+	T_ASSERT_EQ(WEXITSTATUS(child_status), 0, "Child called exit(0)");
+
+	T_ASSERT_LE(malloc_size(ptr1), 64, "tiny pointer still seen post fork");
+	T_ASSERT_LE(malloc_size(ptr2), 65536, "large pointer still seen post fork");
+
+	free(ptr1);
+	free(ptr2);
+
+	// Join all the worker threads so that there are no leaks when the test ends
+	os_atomic_store(&data.done, true, relaxed);
+	for (int i = 0; i < num_threads; i++) {
+		pthread_join(worker_threads[i], NULL);
+	}
+}
+
+T_DECL(malloc_statistics, "Make sure the main and new zone support statistics",
+		T_META_ENVVAR("MallocProbGuard=0"),
+		T_META_TAG_XZONE)
+{
+	malloc_zone_t *default_zone = malloc_default_zone();
+	malloc_zone_t *new_zone = malloc_create_zone(0, 0);
+
+	malloc_statistics_t default_stats = { 0 };
+	malloc_statistics_t new_stats = { 0 };
+
+	malloc_zone_statistics(default_zone, &default_stats);
+	malloc_zone_statistics(new_zone, &new_stats);
+
+	T_ASSERT_EQ(new_stats.size_in_use, 0,
+			"Accurate stats before any allocations");
+	T_ASSERT_EQ(new_stats.blocks_in_use, 0,
+			"Accurate allocation count before any allocations");
+	void *ptr1 = malloc_zone_malloc(new_zone, 1024);
+	void *ptr2 = malloc_zone_malloc(new_zone, 8192);
+	malloc_zone_statistics(new_zone, &new_stats);
+	free(ptr1);
+	free(ptr2);
+
+	T_ASSERT_EQ(new_stats.size_in_use, 1024 + 8192,
+			"Accurate stats after allocations");
+	T_ASSERT_EQ(new_stats.blocks_in_use, 2,
+			"Accurate allocation count after allocations");
+}
+
+T_DECL(malloc_free_pointers_on_destroy,
+		"Make many allocations, make sure they're freed on zone destroy",
+		T_META_TAG_XZONE)
+{
+	malloc_zone_t *new_zone = malloc_create_zone(0, 0);
+
+	// Allocate 1 segment of tiny chunks (4k * 1024 == 4M)
+	void *tiny_ptrs[1024];
+
+	// Allocate 3 segments worth of small chunks (32k * 384 == 4M * 3)
+	void *small_ptrs[384];
+
+	// Allocate 5 segments worth of large chunks
+	void *large_ptrs[256];
+
+	// Allocate 2 huge segments
+	void *huge_ptrs[2];
+
+	for (int i = 0; i < countof(tiny_ptrs); i++) {
+		tiny_ptrs[i] = malloc_zone_malloc(new_zone, 1024);
+		T_QUIET; T_ASSERT_NOTNULL(tiny_ptrs[i], "TINY allocation succeeded");
+		T_QUIET; T_ASSERT_GE(malloc_size(tiny_ptrs[i]), 1024,
+				"TINY allocation is of requested size");
+	}
+
+	for (int i = 0; i < countof(small_ptrs); i++) {
+		small_ptrs[i] = malloc_zone_malloc(new_zone, 32768);
+		T_QUIET; T_ASSERT_NOTNULL(small_ptrs[i], "SMALL allocation succeeded");
+		T_QUIET; T_ASSERT_GE(malloc_size(small_ptrs[i]), 32768,
+				"SMALL allocation is of requested size");
+	}
+
+	for (int i = 0; i < countof(large_ptrs); i++) {
+		large_ptrs[i] = malloc_zone_malloc(new_zone, 65536);
+		T_QUIET; T_ASSERT_NOTNULL(large_ptrs[i], "LARGE allocation succeeded");
+		T_QUIET; T_ASSERT_GE(malloc_size(large_ptrs[i]), 65536,
+				"LARGE allocation is of requested size");
+	}
+
+	for (int i = 0; i < countof(huge_ptrs); i++) {
+		huge_ptrs[i] = malloc_zone_malloc(new_zone, MiB(32));
+		T_QUIET; T_ASSERT_NOTNULL(huge_ptrs[i], "HUGE allocation succeeded");
+		T_QUIET; T_ASSERT_GE(malloc_size(huge_ptrs[i]), MiB(32),
+				"HUGE allocation is of requested size");
+	}
+
+	malloc_destroy_zone(new_zone);
+
+	for (int i = 0; i < countof(tiny_ptrs); i++) {
+		T_QUIET; T_ASSERT_EQ(malloc_size(tiny_ptrs[i]), 0,
+				"TINY allocation is freed by destroying zone");
+	}
+
+	for (int i = 0; i < countof(small_ptrs); i++) {
+		T_QUIET; T_ASSERT_EQ(malloc_size(small_ptrs[i]), 0,
+				"SMALL allocation is freed by destroying zone");
+	}
+
+	for (int i = 0; i < countof(large_ptrs); i++) {
+		T_QUIET; T_ASSERT_EQ(malloc_size(large_ptrs[i]), 0,
+				"LARGE allocation is freed by destroying zone");
+	}
+
+	for (int i = 0; i < countof(huge_ptrs); i++) {
+		T_QUIET; T_ASSERT_EQ(malloc_size(huge_ptrs[i]), 0,
+				"HUGE allocation is freed by destroying zone");
+	}
+}
+
+static void zone_pressure(bool *finished, malloc_zone_t *zone, bool free)
+{
+	const int num_ptrs = 1024;
+	void *ptrs[num_ptrs] = { NULL };
+
+	malloc_type_id_t pure_data = (malloc_type_descriptor_t){
+		.summary = (malloc_type_summary_t){
+			.layout_semantics = (malloc_type_layout_semantics_t) {
+				.generic_data = true,
+			}
+		}
+	}.type_id;
+
+	while (!os_atomic_load(finished, relaxed)) {
+		bool data = rand() & 0x1;
+		int index = rand() % num_ptrs;
+		int new_sz = (rand() & 0xff) + 1;
+		new_sz <<= (rand() % 10);
+		if (ptrs[index] && ((rand() % 8) == 0)) {
+			if (data) {
+				ptrs[index] = malloc_type_zone_realloc(zone, ptrs[index],
+						new_sz, pure_data);
+			} else {
+				ptrs[index] = malloc_zone_realloc(zone, ptrs[index], new_sz);
+			}
+		} else {
+			malloc_zone_free(zone, ptrs[index]);
+			if (data) {
+				ptrs[index] = malloc_type_zone_malloc(zone, new_sz, pure_data);
+			} else {
+				ptrs[index] = malloc_zone_malloc(zone, new_sz);
+			}
+		}
+	}
+
+	if (free) {
+		for (int i = 0; i < num_ptrs; i++) {
+			malloc_zone_free(zone, ptrs[i]);
+		}
+	}
+}
+
+extern malloc_zone_t **malloc_zones;
+static void *
+main_zone_thread(void *arg)
+{
+	bool *finished = (bool *)arg;
+	malloc_zone_t *zone = malloc_zones[0];
+	zone_pressure(finished, zone, true);
+	return NULL;
+}
+
+static void *
+create_zone_thread(void *arg)
+{
+	bool *finished = (bool *)arg;
+	malloc_zone_t *zone = malloc_create_zone(0, 0);
+	zone_pressure(finished, zone, false);
+	malloc_destroy_zone(zone);
+	return NULL;
+}
+
+static void
+create_zone_stress(void)
+{
+	const int num_main_threads = 3;
+	const int concurrent_new_zones = 3;
+	const int iterations = 100;
+
+	bool main_finished = false;
+	pthread_t main_zone_threads[num_main_threads];
+	for (int i = 0; i < num_main_threads; i++) {
+		int rc = pthread_create(&main_zone_threads[i], NULL, main_zone_thread,
+				&main_finished);
+		T_ASSERT_POSIX_ZERO(rc, "Create main zone pressure thread");
+	}
+
+	sleep(1);
+
+	for (int i = 0; i < iterations; i++) {
+		bool finished = false;
+		pthread_t new_zone_threads[concurrent_new_zones];
+		for (int j = 0; j < concurrent_new_zones; j++) {
+			int rc = pthread_create(&new_zone_threads[j], NULL,
+					create_zone_thread, &finished);
+			T_ASSERT_POSIX_ZERO(rc, "Create new zone thread");
+			usleep(500);
+		}
+
+		usleep(50000);
+		os_atomic_store(&finished, true, relaxed);
+
+		for (int j = 0; j < concurrent_new_zones; j++) {
+			int rc = pthread_join(new_zone_threads[j], NULL);
+			T_ASSERT_POSIX_ZERO(rc, "Join new zone thread");
+		}
+
+		usleep(50000);
+	}
+
+	os_atomic_store(&main_finished, true, relaxed);
+	for (int i = 0; i < num_main_threads; i++) {
+		int rc = pthread_join(main_zone_threads[i], NULL);
+		T_ASSERT_POSIX_ZERO(rc, "Join main thread");
+	}
+
+	// Create new zones so that their mzone indices are enumerated by leaks
+	// after the test runs
+	for (int i = 0; i < concurrent_new_zones; i++) {
+		malloc_create_zone(0, 0);
+	}
+}
+
+T_DECL(malloc_create_zone_stress,
+		"Create and destroy zones while stressing main zone",
+		T_META_ENVVAR("MallocProbGuard=0"),
+		T_META_TAG_XZONE)
+{
+	create_zone_stress();
+}
+
+T_DECL(malloc_create_zone_stress_guarded,
+		"Create and destroy zones while stressing main zone, guard pages enabled",
+		T_META_ENVVAR("MallocXzoneGuarded=1"),
+		T_META_ENVVAR("MallocProbGuard=0"),
+		T_META_TAG_XZONE_ONLY)
+{
+	create_zone_stress();
+}
+
+T_DECL(free_default_with_scribble,
+		"Test freeing from a non-default zone while MALLOC_SCRIBBLE is enabled",
+		T_META_TAG_XZONE,
+		T_META_ENVVAR("MallocScribble=1"))
+{
+	malloc_zone_t *zone = malloc_create_zone(0, 0);
+	void *ptr = malloc_zone_malloc(zone, 16384);
+	T_ASSERT_NOTNULL(ptr, "Allocate from non-default zone");
+	free(ptr);
+	T_ASSERT_EQ(malloc_size(ptr), 0, "Pointer is freed");
+}
+
+#else // CONFIG_XZONE_MALLOC
+
+T_DECL(skip_create_zone, "Do nothing test for !CONFIG_XZONE_MALLOC",
+	T_META_ENABLED(false))
+{
+	T_SKIP("Nothing to test under !CONFIG_XZONE_MALLOC");
+
+}
+
+#endif // CONFIG_XZONE_MALLOC