xref: /xnu-8796.101.5/tests/vm/vm_reclaim.c (revision aca3beaa3dfbd42498b42c5e5ce20a938e6554e5)
1 #include <sys/types.h>
2 #include <sys/sysctl.h>
3 #include <mach/mach.h>
4 #include <mach/mach_vm.h>
5 #include <mach/vm_reclaim.h>
6 #include <mach-o/dyld.h>
7 #include <os/atomic_private.h>
8 #include <signal.h>
9 
10 #include <darwintest.h>
11 #include <darwintest_utils.h>
12 
13 #include <Kernel/kern/ledger.h>
14 extern int ledger(int cmd, caddr_t arg1, caddr_t arg2, caddr_t arg3);
15 
16 #include "memorystatus_assertion_helpers.h"
17 
18 // Some of the unit tests test deferred deallocations.
19 // For these we need to set a sufficiently large reclaim threshold
20 // to ensure their buffers aren't freed prematurely.
21 #define VM_RECLAIM_THRESHOLD_BOOT_ARG "vm_reclaim_max_threshold=268435456"
22 
23 T_GLOBAL_META(
24 	T_META_NAMESPACE("xnu.vm"),
25 	T_META_RADAR_COMPONENT_NAME("xnu"),
26 	T_META_RADAR_COMPONENT_VERSION("VM"),
27 	T_META_ENABLED(TARGET_OS_IOS && !TARGET_OS_MACCATALYST),
28 	T_META_ENVVAR("MallocLargeCache=0") // Ensure we don't conflict with libmalloc's reclaim buffer
29 	);
30 
31 T_DECL(vm_reclaim_init, "Set up and tear down a reclaim buffer")
32 {
33 	struct mach_vm_reclaim_ringbuffer_v1_s ringbuffer;
34 
35 	kern_return_t kr = mach_vm_reclaim_ringbuffer_init(&ringbuffer);
36 
37 	T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "mach_vm_reclaim_ringbuffer_init");
38 }
39 
40 /*
41  * Allocate a buffer of the given size, write val to each byte, and free it via a deferred free call.
42  */
43 static uint64_t
allocate_and_defer_free(size_t size,mach_vm_reclaim_ringbuffer_v1_t ringbuffer,unsigned char val,mach_vm_address_t * addr)44 allocate_and_defer_free(size_t size, mach_vm_reclaim_ringbuffer_v1_t ringbuffer, unsigned char val, mach_vm_address_t *addr /* OUT */)
45 {
46 	kern_return_t kr = mach_vm_map(mach_task_self(), addr, size, 0, VM_FLAGS_ANYWHERE, MEMORY_OBJECT_NULL, 0, FALSE, VM_PROT_DEFAULT, VM_PROT_ALL, VM_INHERIT_DEFAULT);
47 	bool should_update_kernel_accounting = false;
48 	uint64_t idx;
49 	T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "mach_vm_map");
50 
51 	memset((void *) *addr, val, size);
52 
53 	idx = mach_vm_reclaim_mark_free(ringbuffer, *addr, (uint32_t) size, &should_update_kernel_accounting);
54 	if (should_update_kernel_accounting) {
55 		mach_vm_reclaim_update_kernel_accounting(ringbuffer);
56 	}
57 	return idx;
58 }
59 
60 T_DECL(vm_reclaim_single_entry, "Place a single entry in the buffer and call sync")
61 {
62 	struct mach_vm_reclaim_ringbuffer_v1_s ringbuffer;
63 	static const size_t kAllocationSize = (1UL << 20); // 1MB
64 	mach_vm_address_t addr;
65 
66 	kern_return_t kr = mach_vm_reclaim_ringbuffer_init(&ringbuffer);
67 	T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "mach_vm_reclaim_ringbuffer_init");
68 
69 	uint64_t idx = allocate_and_defer_free(kAllocationSize, &ringbuffer, 1, &addr);
70 	T_QUIET; T_ASSERT_EQ(idx, 0ULL, "Entry placed at start of buffer");
71 	mach_vm_reclaim_synchronize(&ringbuffer, 1);
72 }
73 
74 static int
spawn_helper_and_wait_for_exit(char * helper)75 spawn_helper_and_wait_for_exit(char *helper)
76 {
77 	char **launch_tool_args;
78 	char testpath[PATH_MAX];
79 	uint32_t testpath_buf_size;
80 	pid_t child_pid;
81 
82 	testpath_buf_size = sizeof(testpath);
83 	int ret = _NSGetExecutablePath(testpath, &testpath_buf_size);
84 	T_QUIET; T_ASSERT_POSIX_ZERO(ret, "_NSGetExecutablePath");
85 	T_LOG("Executable path: %s", testpath);
86 	launch_tool_args = (char *[]){
87 		testpath,
88 		"-n",
89 		helper,
90 		NULL
91 	};
92 
93 	/* Spawn the child process. */
94 	ret = dt_launch_tool(&child_pid, launch_tool_args, false, NULL, NULL);
95 	if (ret != 0) {
96 		T_LOG("dt_launch tool returned %d with error code %d", ret, errno);
97 	}
98 	T_QUIET; T_ASSERT_POSIX_SUCCESS(child_pid, "dt_launch_tool");
99 
100 	int status;
101 	pid_t rc = waitpid(child_pid, &status, 0);
102 	T_QUIET; T_ASSERT_EQ(rc, child_pid, "waitpid");
103 	return status;
104 }
105 
106 /*
107  * Returns true iff every entry in buffer is expected.
108  */
109 static bool
check_buffer(mach_vm_address_t addr,size_t size,unsigned char expected)110 check_buffer(mach_vm_address_t addr, size_t size, unsigned char expected)
111 {
112 	unsigned char *buffer = (unsigned char *) addr;
113 	for (size_t i = 0; i < size; i++) {
114 		if (buffer[i] != expected) {
115 			return false;
116 		}
117 	}
118 	return true;
119 }
120 
121 /*
122  * Check that the given (freed) buffer has changed.
123  * This will likely crash, but if we make it through the entire buffer then segfault on purpose.
124  */
125 static void
assert_buffer_has_changed_and_crash(mach_vm_address_t addr,size_t size,unsigned char expected)126 assert_buffer_has_changed_and_crash(mach_vm_address_t addr, size_t size, unsigned char expected)
127 {
128 	/*
129 	 * mach_vm_reclaim_synchronize should have ensured the buffer was freed.
130 	 * Two cases:
131 	 * 1. The buffer is still still free (touching it causes a crash)
132 	 * 2. The address range was re-allocated by some other library in process.
133 	 * #1 is far more likely. But if #2 happened, the buffer shouldn't be filled
134 	 * with the value we wrote to it. So scan the buffer. If we segfault it's case #1
135 	 * and if we see another value it's case #2.
136 	 */
137 	bool changed = !check_buffer(addr, size, expected);
138 	T_QUIET; T_ASSERT_TRUE(changed, "buffer was re-allocated");
139 	/* Case #2. Force a segfault so the parent sees that we crashed. */
140 	*(volatile int *) 0 = 1;
141 
142 	T_FAIL("Test did not crash when dereferencing NULL");
143 }
144 
145 T_HELPER_DECL(reuse_freed_entry,
146     "defer free, sync, and try to use entry")
147 {
148 	struct mach_vm_reclaim_ringbuffer_v1_s ringbuffer;
149 	static const size_t kAllocationSize = (1UL << 20); // 1MB
150 	mach_vm_address_t addr;
151 	static const unsigned char kValue = 220;
152 
153 	kern_return_t kr = mach_vm_reclaim_ringbuffer_init(&ringbuffer);
154 	T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "mach_vm_reclaim_ringbuffer_init");
155 
156 	uint64_t idx = allocate_and_defer_free(kAllocationSize, &ringbuffer, kValue, &addr);
157 	T_QUIET; T_ASSERT_EQ(idx, 0ULL, "Entry placed at start of buffer");
158 	kr = mach_vm_reclaim_synchronize(&ringbuffer, 10);
159 	T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "mach_vm_reclaim_synchronize");
160 	assert_buffer_has_changed_and_crash(addr, kAllocationSize, kValue);
161 }
162 
163 T_DECL(vm_reclaim_single_entry_verify_free, "Place a single entry in the buffer and call sync",
164     T_META_IGNORECRASHES("vm_reclaim_single_entry_verify_free*"))
165 {
166 	int status = spawn_helper_and_wait_for_exit("reuse_freed_entry");
167 	T_QUIET; T_ASSERT_TRUE(WIFSIGNALED(status), "Test process crashed.");
168 	T_QUIET; T_ASSERT_EQ(WTERMSIG(status), SIGSEGV, "Test process crashed with segmentation fault.");
169 }
170 
171 static void
allocate_and_suspend(char * const * argv,bool free_buffer,bool double_free)172 allocate_and_suspend(char *const *argv, bool free_buffer, bool double_free)
173 {
174 	struct mach_vm_reclaim_ringbuffer_v1_s ringbuffer;
175 	static const size_t kAllocationSize = (1UL << 20); // 1MB
176 	mach_vm_address_t addr = 0;
177 	bool should_update_kernel_accounting = false;
178 
179 	const mach_vm_size_t kNumEntries = (size_t) atoi(argv[0]);
180 
181 	kern_return_t kr = mach_vm_reclaim_ringbuffer_init(&ringbuffer);
182 	T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "mach_vm_reclaim_ringbuffer_init");
183 	T_QUIET; T_ASSERT_LT(kNumEntries, ringbuffer.buffer_len, "Test does not fill up ringubffer");
184 
185 	for (size_t i = 0; i < kNumEntries; i++) {
186 		uint64_t idx = allocate_and_defer_free(kAllocationSize, &ringbuffer, (unsigned char) i, &addr);
187 		T_QUIET; T_ASSERT_EQ(idx, (uint64_t) i, "idx is correct");
188 	}
189 
190 	if (double_free) {
191 		// Double free the last entry
192 		mach_vm_reclaim_mark_free(&ringbuffer, addr, (uint32_t) kAllocationSize, &should_update_kernel_accounting);
193 		T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "mach_vm_reclaim_mark_free");
194 	}
195 
196 	if (free_buffer) {
197 		kr = mach_vm_deallocate(mach_task_self(), (mach_vm_address_t) ringbuffer.buffer, ringbuffer.buffer_len * sizeof(mach_vm_reclaim_entry_v1_t));
198 		T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "mach_vm_deallocate");
199 	}
200 
201 	// Signal to our parent to suspend us
202 	if (kill(getppid(), SIGUSR1) != 0) {
203 		T_LOG("Unable to signal to parent process!");
204 		exit(1);
205 	}
206 
207 	while (1) {
208 		;
209 	}
210 }
211 
212 T_HELPER_DECL(allocate_and_suspend,
213     "defer free, and signal parent to suspend")
214 {
215 	allocate_and_suspend(argv, false, false);
216 }
217 
218 static void
resume_and_kill_proc(pid_t pid)219 resume_and_kill_proc(pid_t pid)
220 {
221 	int ret = pid_resume(pid);
222 	T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "proc resumed after freeze");
223 	T_QUIET; T_ASSERT_POSIX_SUCCESS(kill(pid, SIGKILL), "Killed process");
224 }
225 
226 static void
drain_async_queue(pid_t child_pid)227 drain_async_queue(pid_t child_pid)
228 {
229 	int val = child_pid;
230 	int ret;
231 	size_t len = sizeof(val);
232 	ret = sysctlbyname("vm.reclaim_drain_async_queue", NULL, NULL, &val, len);
233 	T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "vm.reclaim_drain_async_queue");
234 }
235 
236 static size_t
ledger_phys_footprint_index(size_t * num_entries)237 ledger_phys_footprint_index(size_t *num_entries)
238 {
239 	struct ledger_info li;
240 	struct ledger_template_info *templateInfo = NULL;
241 	int ret;
242 	size_t i, footprint_index;
243 	bool found = false;
244 
245 	ret = ledger(LEDGER_INFO, (caddr_t)(uintptr_t)getpid(), (caddr_t)&li, NULL);
246 	T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "ledger(LEDGER_INFO)");
247 
248 	T_QUIET; T_ASSERT_GT(li.li_entries, (int64_t) 0, "num ledger entries is valid");
249 	*num_entries = (size_t) li.li_entries;
250 	templateInfo = malloc((size_t)li.li_entries * sizeof(struct ledger_template_info));
251 	T_QUIET; T_ASSERT_NOTNULL(templateInfo, "malloc entries");
252 
253 	footprint_index = 0;
254 	ret = ledger(LEDGER_TEMPLATE_INFO, (caddr_t) templateInfo, (caddr_t) num_entries, NULL);
255 	T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "ledger(LEDGER_TEMPLATE_INFO)");
256 	for (i = 0; i < *num_entries; i++) {
257 		if (strcmp(templateInfo[i].lti_name, "phys_footprint") == 0) {
258 			footprint_index = i;
259 			found = true;
260 		}
261 	}
262 	free(templateInfo);
263 	T_QUIET; T_ASSERT_TRUE(found, "found phys_footprint in ledger");
264 	return footprint_index;
265 }
266 
267 static int64_t
get_ledger_entry_for_pid(pid_t pid,size_t index,size_t num_entries)268 get_ledger_entry_for_pid(pid_t pid, size_t index, size_t num_entries)
269 {
270 	int ret;
271 	int64_t value;
272 	struct ledger_entry_info *lei = NULL;
273 
274 	lei = malloc(num_entries * sizeof(*lei));
275 	ret = ledger(LEDGER_ENTRY_INFO, (caddr_t) (uintptr_t) pid, (caddr_t) lei, (caddr_t) &num_entries);
276 	T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "ledger(LEDGER_ENTRY_INFO)");
277 	value = lei[index].lei_balance;
278 	free(lei);
279 	return value;
280 }
281 
282 static pid_t child_pid;
283 
284 static void
test_after_background_helper_launches(char * variant,char * arg1,dispatch_block_t test_block,dispatch_block_t exit_block)285 test_after_background_helper_launches(char* variant, char * arg1, dispatch_block_t test_block, dispatch_block_t exit_block)
286 {
287 	char **launch_tool_args;
288 	char testpath[PATH_MAX];
289 	uint32_t testpath_buf_size;
290 
291 	dispatch_source_t ds_signal, ds_exit;
292 
293 	/* Wait for the child process to tell us that it's ready, and then freeze it */
294 	signal(SIGUSR1, SIG_IGN);
295 	ds_signal = dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, SIGUSR1, 0, dispatch_get_main_queue());
296 	T_QUIET; T_ASSERT_NOTNULL(ds_signal, "dispatch_source_create");
297 	dispatch_source_set_event_handler(ds_signal, test_block);
298 
299 	dispatch_activate(ds_signal);
300 
301 	testpath_buf_size = sizeof(testpath);
302 	int ret = _NSGetExecutablePath(testpath, &testpath_buf_size);
303 	T_QUIET; T_ASSERT_POSIX_ZERO(ret, "_NSGetExecutablePath");
304 	T_LOG("Executable path: %s", testpath);
305 	launch_tool_args = (char *[]){
306 		testpath,
307 		"-n",
308 		variant,
309 		arg1,
310 		NULL
311 	};
312 
313 	/* Spawn the child process. */
314 	ret = dt_launch_tool(&child_pid, launch_tool_args, false, NULL, NULL);
315 	if (ret != 0) {
316 		T_LOG("dt_launch tool returned %d with error code %d", ret, errno);
317 	}
318 	T_QUIET; T_ASSERT_POSIX_SUCCESS(child_pid, "dt_launch_tool");
319 
320 	/* Listen for exit. */
321 	ds_exit = dispatch_source_create(DISPATCH_SOURCE_TYPE_PROC, (uintptr_t)child_pid, DISPATCH_PROC_EXIT, dispatch_get_main_queue());
322 	dispatch_source_set_event_handler(ds_exit, exit_block);
323 
324 	dispatch_activate(ds_exit);
325 	dispatch_main();
326 }
327 
328 T_DECL(vm_reclaim_full_reclaim_on_suspend, "Defer free memory and then suspend.",
329     T_META_ASROOT(true),
330     T_META_BOOTARGS_SET(VM_RECLAIM_THRESHOLD_BOOT_ARG))
331 {
332 	test_after_background_helper_launches("allocate_and_suspend", "20", ^{
333 		int ret = 0;
334 		size_t num_ledger_entries = 0;
335 		size_t phys_footprint_index = ledger_phys_footprint_index(&num_ledger_entries);
336 		int64_t before_footprint, after_footprint, reclaimable_bytes = 20 * (1ULL << 20);
337 		before_footprint = get_ledger_entry_for_pid(child_pid, phys_footprint_index, num_ledger_entries);
338 		T_QUIET; T_EXPECT_GE(before_footprint, reclaimable_bytes, "memory was allocated");
339 		ret = pid_suspend(child_pid);
340 		T_ASSERT_POSIX_SUCCESS(ret, "child suspended");
341 		/*
342 		 * The reclaim work is kicked off asynchronously by the suspend.
343 		 * So we need to call into the kernel to synchronize with the reclaim worker
344 		 * thread.
345 		 */
346 		drain_async_queue(child_pid);
347 
348 		after_footprint = get_ledger_entry_for_pid(child_pid, phys_footprint_index, num_ledger_entries);
349 		T_QUIET; T_EXPECT_LE(after_footprint, before_footprint - reclaimable_bytes, "memory was reclaimed");
350 
351 		resume_and_kill_proc(child_pid);
352 	},
353 	    ^{
354 		int status = 0, code = 0;
355 		pid_t rc = waitpid(child_pid, &status, 0);
356 		T_QUIET; T_ASSERT_EQ(rc, child_pid, "waitpid");
357 		code = WEXITSTATUS(status);
358 		T_QUIET; T_ASSERT_EQ(code, 0, "Child exited cleanly");
359 		T_END;
360 	});
361 }
362 
363 T_DECL(vm_reclaim_limit_kills, "Deferred reclaims are processed before a limit kill")
364 {
365 	int err;
366 	struct mach_vm_reclaim_ringbuffer_v1_s ringbuffer;
367 	const size_t kNumEntries = 50;
368 	static const size_t kAllocationSize = (1UL << 20); // 1MB
369 	static const size_t kMemoryLimit = kNumEntries / 10 * kAllocationSize;
370 
371 	kern_return_t kr = mach_vm_reclaim_ringbuffer_init(&ringbuffer);
372 	T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "mach_vm_reclaim_ringbuffer_init");
373 
374 	err = set_memlimits(getpid(), kMemoryLimit >> 20, kMemoryLimit >> 20, TRUE, TRUE);
375 	T_QUIET; T_ASSERT_POSIX_SUCCESS(err, "set_memlimits");
376 
377 	for (size_t i = 0; i < kNumEntries; i++) {
378 		mach_vm_address_t addr = 0;
379 		uint64_t idx = allocate_and_defer_free(kAllocationSize, &ringbuffer, (unsigned char) i, &addr);
380 		T_QUIET; T_ASSERT_EQ(idx, (uint64_t) i, "idx is correct");
381 	}
382 
383 	T_PASS("Was able to allocate and defer free %zu chunks of size %zu bytes while staying under limit of %zu bytes", kNumEntries, kAllocationSize, kMemoryLimit);
384 }
385 
386 T_DECL(vm_reclaim_update_reclaimable_bytes_threshold, "Kernel reclaims when num_bytes_reclaimable crosses threshold")
387 {
388 	mach_vm_size_t kNumEntries = 0;
389 	struct mach_vm_reclaim_ringbuffer_v1_s ringbuffer;
390 	const size_t kAllocationSize = vm_kernel_page_size;
391 	uint64_t vm_reclaim_reclaimable_max_threshold;
392 	int ret;
393 	size_t len = sizeof(vm_reclaim_reclaimable_max_threshold);
394 	size_t num_ledger_entries = 0;
395 	size_t phys_footprint_index = ledger_phys_footprint_index(&num_ledger_entries);
396 
397 	kern_return_t kr = mach_vm_reclaim_ringbuffer_init(&ringbuffer);
398 	T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "mach_vm_reclaim_ringbuffer_init");
399 
400 	// Allocate 1000 times the reclaim threshold
401 	ret = sysctlbyname("vm.reclaim_max_threshold", &vm_reclaim_reclaimable_max_threshold, &len, NULL, 0);
402 	T_QUIET; T_ASSERT_POSIX_SUCCESS(ret, "vm.reclaim_max_threshold");
403 	kNumEntries = vm_reclaim_reclaimable_max_threshold / kAllocationSize * 1000;
404 	T_QUIET; T_ASSERT_LT(kNumEntries, ringbuffer.buffer_len, "Entries will not fill up ringbuffer.");
405 
406 	mach_vm_address_t addr = 0;
407 	for (uint64_t i = 0; i < kNumEntries; i++) {
408 		uint64_t idx = allocate_and_defer_free(kAllocationSize, &ringbuffer, (unsigned char) i, &addr);
409 		T_QUIET; T_ASSERT_EQ(idx, i, "idx is correct");
410 	}
411 
412 	T_QUIET; T_ASSERT_LT(get_ledger_entry_for_pid(getpid(), phys_footprint_index, num_ledger_entries),
413 	    (int64_t) ((kNumEntries) * kAllocationSize), "Entries were reclaimed as we crossed threshold");
414 }
415 
416 T_HELPER_DECL(deallocate_indices,
417     "deallocate the indices from underneath the kernel")
418 {
419 	mach_vm_reclaim_ringbuffer_v1_t ringbuffer = NULL;
420 	static const size_t kAllocationSize = (1UL << 20); // 1MB
421 	mach_vm_address_t addr;
422 
423 	kern_return_t kr;
424 	kr = mach_vm_map(mach_task_self(), (mach_vm_address_t *) &ringbuffer, vm_page_size, 0, VM_FLAGS_ANYWHERE, MEMORY_OBJECT_NULL, 0, FALSE, VM_PROT_DEFAULT, VM_PROT_ALL, VM_INHERIT_DEFAULT);
425 	T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "mach_vm_map");
426 	kr = mach_vm_reclaim_ringbuffer_init(ringbuffer);
427 	T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "mach_vm_reclaim_ringbuffer_init");
428 
429 	uint64_t idx = allocate_and_defer_free(kAllocationSize, ringbuffer, 1, &addr);
430 	T_QUIET; T_ASSERT_EQ(idx, 0ULL, "Entry placed at start of buffer");
431 	kr = mach_vm_deallocate(mach_task_self(), (mach_vm_address_t) ringbuffer, vm_page_size);
432 	T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "mach_vm_deallocate");
433 
434 	mach_vm_reclaim_synchronize(ringbuffer, 10);
435 
436 	T_FAIL("Test did not crash when synchronizing with deallocated indices");
437 }
438 
439 T_DECL(vm_reclaim_copyio_indices_error, "Force a copyio error on the indices",
440     T_META_IGNORECRASHES("vm_reclaim_copyio_indices_error*"))
441 {
442 	int status = spawn_helper_and_wait_for_exit("deallocate_indices");
443 	T_QUIET; T_ASSERT_TRUE(WIFSIGNALED(status), "Test process crashed.");
444 	T_QUIET; T_ASSERT_EQ(WTERMSIG(status), SIGKILL, "Test process crashed with SIGKILL.");
445 }
446 
447 T_HELPER_DECL(deallocate_buffer,
448     "deallocate the buffer from underneath the kernel")
449 {
450 	struct mach_vm_reclaim_ringbuffer_v1_s ringbuffer;
451 	static const size_t kAllocationSize = (1UL << 20); // 1MB
452 	mach_vm_address_t addr;
453 
454 	kern_return_t kr = mach_vm_reclaim_ringbuffer_init(&ringbuffer);
455 	T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "mach_vm_reclaim_ringbuffer_init");
456 
457 	uint64_t idx = allocate_and_defer_free(kAllocationSize, &ringbuffer, 1, &addr);
458 	T_QUIET; T_ASSERT_EQ(idx, 0ULL, "Entry placed at start of buffer");
459 	kr = mach_vm_deallocate(mach_task_self(), (mach_vm_address_t) ringbuffer.buffer, ringbuffer.buffer_len * sizeof(mach_vm_reclaim_entry_v1_t));
460 	T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "mach_vm_deallocate");
461 
462 	mach_vm_reclaim_synchronize(&ringbuffer, 10);
463 
464 	T_FAIL("Test did not crash when synchronizing on a deallocated buffer!");
465 }
466 
467 T_DECL(vm_reclaim_copyio_buffer_error, "Force a copyio error on the buffer",
468     T_META_IGNORECRASHES("vm_reclaim_copyio_buffer_error*"),
469     T_META_BOOTARGS_SET(VM_RECLAIM_THRESHOLD_BOOT_ARG))
470 {
471 	int status = spawn_helper_and_wait_for_exit("deallocate_buffer");
472 	T_QUIET; T_ASSERT_TRUE(WIFSIGNALED(status), "Test process crashed.");
473 	T_QUIET; T_ASSERT_EQ(WTERMSIG(status), SIGKILL, "Test process crashed with SIGKILL.");
474 }
475 
476 T_HELPER_DECL(dealloc_gap, "Put a bad entry in the buffer")
477 {
478 	struct mach_vm_reclaim_ringbuffer_v1_s ringbuffer;
479 	static const size_t kAllocationSize = (1UL << 20); // 1MB
480 	mach_vm_address_t addr;
481 	bool should_update_kernel_accounting = false;
482 
483 	kern_return_t kr = mach_vm_reclaim_ringbuffer_init(&ringbuffer);
484 	T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "mach_vm_reclaim_ringbuffer_init");
485 
486 	uint64_t idx = allocate_and_defer_free(kAllocationSize, &ringbuffer, 1, &addr);
487 	T_QUIET; T_ASSERT_EQ(idx, 0ULL, "Entry placed at start of buffer");
488 	idx = mach_vm_reclaim_mark_free(&ringbuffer, addr, (uint32_t) kAllocationSize, &should_update_kernel_accounting);
489 	T_QUIET; T_ASSERT_EQ(idx, 1ULL, "Entry placed at correct index");
490 
491 	mach_vm_reclaim_synchronize(&ringbuffer, 2);
492 
493 	T_FAIL("Test did not crash when doing a double free!");
494 }
495 
496 T_DECL(vm_reclaim_dealloc_gap, "Ensure a dealloc gap delivers a fatal exception",
497     T_META_IGNORECRASHES("vm_reclaim_dealloc_gap*"))
498 {
499 	int status = spawn_helper_and_wait_for_exit("dealloc_gap");
500 	T_QUIET; T_ASSERT_TRUE(WIFSIGNALED(status), "Test process crashed.");
501 	T_QUIET; T_ASSERT_EQ(WTERMSIG(status), SIGKILL, "Test process crashed with SIGKILL.");
502 }
503 
504 T_HELPER_DECL(allocate_and_suspend_with_dealloc_gap,
505     "defer double free, and signal parent to suspend")
506 {
507 	allocate_and_suspend(argv, false, true);
508 }
509 
510 static void
vm_reclaim_async_exception(char * variant,char * arg1)511 vm_reclaim_async_exception(char *variant, char *arg1)
512 {
513 	test_after_background_helper_launches(variant, arg1, ^{
514 		int ret = 0;
515 		ret = pid_suspend(child_pid);
516 		T_ASSERT_POSIX_SUCCESS(ret, "child suspended");
517 		/*
518 		 * The reclaim work is kicked off asynchronously by the suspend.
519 		 * So we need to call into the kernel to synchronize with the reclaim worker
520 		 * thread.
521 		 */
522 		drain_async_queue(child_pid);
523 	}, ^{
524 		int status;
525 		pid_t rc = waitpid(child_pid, &status, 0);
526 		T_QUIET; T_ASSERT_EQ(rc, child_pid, "waitpid");
527 		T_QUIET; T_ASSERT_TRUE(WIFSIGNALED(status), "Test process crashed.");
528 		T_QUIET; T_ASSERT_EQ(WTERMSIG(status), SIGKILL, "Test process crashed with SIGKILL.");
529 		T_END;
530 	});
531 }
532 
533 T_DECL(vm_reclaim_dealloc_gap_async, "Ensure a dealloc gap delivers an async fatal exception",
534     T_META_IGNORECRASHES("vm_reclaim_dealloc_gap_async*"))
535 {
536 	vm_reclaim_async_exception("allocate_and_suspend_with_dealloc_gap", "15");
537 }
538 
539 T_HELPER_DECL(allocate_and_suspend_with_buffer_error,
540     "defer free, free buffer, and signal parent to suspend")
541 {
542 	allocate_and_suspend(argv, true, false);
543 }
544 
545 T_DECL(vm_reclaim_copyio_buffer_error_async, "Ensure a buffer copyio failure delivers an async fatal exception",
546     T_META_IGNORECRASHES("vm_reclaim_dealloc_gap_async*"),
547     T_META_BOOTARGS_SET(VM_RECLAIM_THRESHOLD_BOOT_ARG))
548 {
549 	vm_reclaim_async_exception("allocate_and_suspend_with_buffer_error", "15");
550 }
551 
552 T_HELPER_DECL(reuse_freed_entry_fork,
553     "defer free, sync, and try to use entry")
554 {
555 	struct mach_vm_reclaim_ringbuffer_v1_s ringbuffer;
556 	static const size_t kAllocationSize = (1UL << 20); // 1MB
557 	mach_vm_address_t addr;
558 	static const unsigned char kValue = 119;
559 
560 	kern_return_t kr = mach_vm_reclaim_ringbuffer_init(&ringbuffer);
561 	T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "mach_vm_reclaim_ringbuffer_init");
562 
563 	uint64_t idx = allocate_and_defer_free(kAllocationSize, &ringbuffer, kValue, &addr);
564 	T_QUIET; T_ASSERT_EQ(idx, 0ULL, "Entry placed at start of buffer");
565 
566 	pid_t forked_pid = fork();
567 	T_QUIET; T_WITH_ERRNO; T_ASSERT_NE(forked_pid, -1, "fork()");
568 	if (forked_pid == 0) {
569 		kr = mach_vm_reclaim_synchronize(&ringbuffer, 10);
570 		T_QUIET; T_ASSERT_MACH_SUCCESS(kr, "mach_vm_reclaim_synchronize");
571 		assert_buffer_has_changed_and_crash(addr, kAllocationSize, kValue);
572 	} else {
573 		int status;
574 		pid_t rc = waitpid(forked_pid, &status, 0);
575 		T_QUIET; T_ASSERT_EQ(rc, forked_pid, "waitpid");
576 		T_QUIET; T_ASSERT_TRUE(WIFSIGNALED(status), "Forked process crashed.");
577 		T_QUIET; T_ASSERT_EQ(WTERMSIG(status), SIGSEGV, "Forked process crashed with segmentation fault.");
578 	}
579 }
580 
581 T_DECL(vm_reclaim_fork, "Ensure reclaim buffer is inherited across a fork",
582     T_META_IGNORECRASHES("vm_reclaim_fork*"),
583     T_META_BOOTARGS_SET(VM_RECLAIM_THRESHOLD_BOOT_ARG))
584 {
585 	int status = spawn_helper_and_wait_for_exit("reuse_freed_entry_fork");
586 	T_QUIET; T_ASSERT_TRUE(WIFEXITED(status), "Test process exited.");
587 	T_QUIET; T_ASSERT_EQ(WEXITSTATUS(status), 0, "Test process exited cleanly.");
588 }
589