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