xref: /xnu-12377.41.6/tests/disk_mount_conditioner.m (revision bbb1b6f9e71b8cdde6e5cd6f4841f207dee3d828)
1#ifdef T_NAMESPACE
2#undef T_NAMESPACE
3#endif
4#include <darwintest.h>
5#include <darwintest_utils.h>
6
7#include <stdlib.h>
8#include <unistd.h>
9#include <fcntl.h>
10#include <System/sys/fsctl.h>
11#include <paths.h>
12#import <Foundation/Foundation.h>
13
14static char *mktempdir(void);
15static char *mktempmount(void);
16
17#ifndef TEST_UNENTITLED
18static int system_legal(const char *command);
19static char *mkramdisk(void);
20static uint64_t time_for_read(int fd, const char *expected);
21static void perf_setup(char **path, int *fd);
22
23#define READSIZE 1024L
24#endif /* !TEST_UNENTITLED */
25
26T_GLOBAL_META(
27	T_META_NAMESPACE("xnu.vfs.dmc"),
28	T_META_ASROOT(true),
29	T_META_RUN_CONCURRENTLY(true)
30	);
31
32#pragma mark Entitled Tests
33
34#ifndef TEST_UNENTITLED
35T_DECL(fsctl_get_uninitialized,
36    "Initial fsctl.get should return zeros",
37    T_META_ASROOT(false), T_META_TAG_VM_PREFERRED)
38{
39	int err;
40	char *mount_path;
41	disk_conditioner_info info = {0};
42	disk_conditioner_info expected_info = {0};
43
44	T_SETUPBEGIN;
45	mount_path = mktempmount();
46	T_SETUPEND;
47
48	info.enabled = true;
49	info.is_ssd = true;
50	err = fsctl(mount_path, DISK_CONDITIONER_IOC_GET, &info, 0);
51	T_WITH_ERRNO;
52	T_ASSERT_EQ_INT(0, err, "fsctl(DISK_CONDITIONER_IOC_GET)");
53
54	err = memcmp(&info, &expected_info, sizeof(info));
55	T_ASSERT_EQ_INT(0, err, "initial DMC info is zeroed");
56}
57
58T_DECL(fsctl_set,
59    "fsctl.set should succeed and fsctl.get should verify", T_META_TAG_VM_PREFERRED)
60{
61	int err;
62	char *mount_path;
63	disk_conditioner_info info = {0};
64	disk_conditioner_info expected_info = {0};
65
66	T_SETUPBEGIN;
67	mount_path = mktempmount();
68	T_SETUPEND;
69
70	info.enabled = 1;
71	info.access_time_usec = 10;
72	info.read_throughput_mbps = 40;
73	info.write_throughput_mbps = 40;
74	info.is_ssd = 0;
75	info.ioqueue_depth = 8;
76	info.maxreadcnt = 8;
77	info.maxwritecnt = 8;
78	info.segreadcnt = 8;
79	info.segwritecnt = 8;
80	expected_info = info;
81
82	err = fsctl(mount_path, DISK_CONDITIONER_IOC_SET, &info, 0);
83	T_WITH_ERRNO;
84	T_ASSERT_EQ_INT(0, err, "fsctl(DISK_CONDITIONER_IOC_SET)");
85
86	err = fsctl(mount_path, DISK_CONDITIONER_IOC_GET, &info, 0);
87	T_WITH_ERRNO;
88	T_ASSERT_EQ_INT(0, err, "fsctl(DISK_CONDITIONER_IOC_GET) after SET");
89
90	err = memcmp(&info, &expected_info, sizeof(info));
91	T_ASSERT_EQ_INT(0, err, "fsctl.get is the info configured by fsctl.set");
92}
93
94static void
95verify_mount_fallback_values(const char *mount_path, disk_conditioner_info *info)
96{
97	int err;
98	disk_conditioner_info newinfo = {0};
99
100	err = fsctl(mount_path, DISK_CONDITIONER_IOC_SET, info, 0);
101	T_WITH_ERRNO;
102	T_ASSERT_EQ_INT(0, err, "fsctl(DISK_CONDITIONER_IOC_SET)");
103
104	err = fsctl(mount_path, DISK_CONDITIONER_IOC_GET, &newinfo, 0);
105	T_WITH_ERRNO;
106	T_ASSERT_EQ_INT(0, err, "fsctl(DISK_CONDITIONER_IOC_GET) after SET");
107
108	// without querying the drive for the expected values, the best we can do is
109	// assert that they are not zero (impossible) or less than UINT32_MAX (unlikely)
110	T_ASSERT_GT(newinfo.ioqueue_depth, 0u, "ioqueue_depth is the value from the mount");
111	T_ASSERT_GT(newinfo.maxreadcnt, 0u, "maxreadcnt is value from the mount");
112	T_ASSERT_GT(newinfo.maxwritecnt, 0u, "maxwritecnt is value from the mount");
113	T_ASSERT_GT(newinfo.segreadcnt, 0u, "segreadcnt is value from the mount");
114	T_ASSERT_GT(newinfo.segwritecnt, 0u, "segwritecnt is value from the mount");
115	T_ASSERT_LT(newinfo.ioqueue_depth, UINT32_MAX, "ioqueue_depth is the value from the mount");
116	T_ASSERT_LT(newinfo.maxreadcnt, UINT32_MAX, "maxreadcnt is value from the mount");
117	T_ASSERT_LT(newinfo.maxwritecnt, UINT32_MAX, "maxwritecnt is value from the mount");
118	T_ASSERT_LT(newinfo.segreadcnt, UINT32_MAX, "segreadcnt is value from the mount");
119	T_ASSERT_LT(newinfo.segwritecnt, UINT32_MAX, "segwritecnt is value from the mount");
120}
121
122T_DECL(fsctl_set_zero,
123    "fsctl.set zero values should fall back to original mount settings", T_META_TAG_VM_PREFERRED)
124{
125	char *mount_path;
126	disk_conditioner_info info = {0};
127
128	T_SETUPBEGIN;
129	mount_path = mktempmount();
130
131	info.enabled = 1;
132	/* everything else is 0 */
133
134	T_SETUPEND;
135
136	verify_mount_fallback_values(mount_path, &info);
137}
138
139T_DECL(fsctl_set_out_of_bounds,
140    "fsctl.set out-of-bounds values should fall back to original mount settings", T_META_TAG_VM_PREFERRED)
141{
142	char *mount_path;
143	disk_conditioner_info info;
144
145	T_SETUPBEGIN;
146	mount_path = mktempmount();
147
148	memset(&info, UINT32_MAX, sizeof(info));
149	info.enabled = 1;
150	info.access_time_usec = 0;
151	info.read_throughput_mbps = 0;
152	info.write_throughput_mbps = 0;
153	/* everything else is UINT32_MAX */
154
155	T_SETUPEND;
156
157	verify_mount_fallback_values(mount_path, &info);
158}
159
160T_DECL(fsctl_restore_mount_fields,
161    "fsctl.set should restore fields on mount_t that it temporarily overrides", T_META_TAG_VM_PREFERRED)
162{
163	int err;
164	char *mount_path;
165	disk_conditioner_info info;
166	disk_conditioner_info mount_fields;
167
168	T_SETUPBEGIN;
169	mount_path = mktempmount();
170	T_SETUPEND;
171
172	/* first set out-of-bounds values to retrieve the original mount_t fields */
173	memset(&info, UINT32_MAX, sizeof(info));
174	info.enabled = 1;
175	info.access_time_usec = 0;
176	info.read_throughput_mbps = 0;
177	info.write_throughput_mbps = 0;
178	/* everything else is UINT32_MAX */
179	err = fsctl(mount_path, DISK_CONDITIONER_IOC_SET, &info, 0);
180	T_WITH_ERRNO;
181	T_ASSERT_EQ_INT(0, err, "fsctl(DISK_CONDITIONER_IOC_SET)");
182
183	err = fsctl(mount_path, DISK_CONDITIONER_IOC_GET, &mount_fields, 0);
184	T_WITH_ERRNO;
185	T_ASSERT_EQ_INT(0, err, "fsctl(DISK_CONDITIONER_IOC_GET)");
186
187	/* now turn off the disk conditioner which should restore fields on the mount_t */
188	memset(&info, 1, sizeof(info));
189	info.enabled = 0;
190	err = fsctl(mount_path, DISK_CONDITIONER_IOC_SET, &info, 0);
191	T_WITH_ERRNO;
192	T_ASSERT_EQ_INT(0, err, "fsctl(DISK_CONDITIONER_IOC_SET)");
193
194	/* and finally set out-of-bounds values again to retrieve the new mount_t fields which should not have changed */
195	memset(&info, UINT32_MAX, sizeof(info));
196	info.enabled = 0;
197	info.access_time_usec = 0;
198	info.read_throughput_mbps = 0;
199	info.write_throughput_mbps = 0;
200	/* everything else is UINT32_MAX */
201	err = fsctl(mount_path, DISK_CONDITIONER_IOC_SET, &info, 0);
202	T_WITH_ERRNO;
203	T_ASSERT_EQ_INT(0, err, "fsctl(DISK_CONDITIONER_IOC_SET)");
204
205	err = fsctl(mount_path, DISK_CONDITIONER_IOC_GET, &info, 0);
206	T_WITH_ERRNO;
207	T_ASSERT_EQ_INT(0, err, "fsctl(DISK_CONDITIONER_IOC_GET)");
208
209	T_ASSERT_EQ(info.maxreadcnt, mount_fields.maxreadcnt, "mount_t maxreadcnt restored");
210	T_ASSERT_EQ(info.maxwritecnt, mount_fields.maxwritecnt, "mount_t maxwritecnt restored");
211	T_ASSERT_EQ(info.segreadcnt, mount_fields.segreadcnt, "mount_t segreadcnt restored");
212	T_ASSERT_EQ(info.segwritecnt, mount_fields.segwritecnt, "mount_t segwritecnt restored");
213	T_ASSERT_EQ(info.ioqueue_depth, mount_fields.ioqueue_depth, "mount_t ioqueue_depth restored");
214}
215
216T_DECL(fsctl_get_nonroot,
217    "fsctl.get should not require root",
218    T_META_ASROOT(false), T_META_TAG_VM_PREFERRED)
219{
220	int err;
221	char *mount_path;
222	disk_conditioner_info info;
223
224	T_SETUPBEGIN;
225	// make sure we're not root
226	if (0 == geteuid()) {
227		seteuid(5000);
228	}
229
230	mount_path = mktempmount();
231	T_SETUPEND;
232
233	err = fsctl(mount_path, DISK_CONDITIONER_IOC_GET, &info, 0);
234	T_WITH_ERRNO;
235	T_ASSERT_EQ_INT(0, err, "fsctl.get without root");
236}
237
238T_DECL(fsctl_set_nonroot,
239    "fsctl.set should require root",
240    T_META_ASROOT(false), T_META_TAG_VM_PREFERRED)
241{
242	int err;
243	char *mount_path;
244	disk_conditioner_info info = {0};
245	disk_conditioner_info expected_info = {0};
246
247	T_SETUPBEGIN;
248	// make sure we're not root
249	if (0 == geteuid()) {
250		seteuid(5000);
251	}
252
253	mount_path = mktempmount();
254	T_SETUPEND;
255
256	// save original info
257	err = fsctl(mount_path, DISK_CONDITIONER_IOC_GET, &expected_info, 0);
258	T_WITH_ERRNO;
259	T_ASSERT_EQ_INT(0, err, "Get original DMC info");
260
261	info.enabled = 1;
262	info.access_time_usec = 10;
263	err = fsctl(mount_path, DISK_CONDITIONER_IOC_SET, &info, 0);
264	T_WITH_ERRNO;
265	T_ASSERT_NE_INT(0, err, "fsctl.set returns error without root");
266
267	err = fsctl(mount_path, DISK_CONDITIONER_IOC_GET, &info, 0);
268	T_WITH_ERRNO;
269	T_ASSERT_EQ_INT(0, err, "fsctl.get after nonroot fsctl.set");
270
271	err = memcmp(&info, &expected_info, sizeof(info));
272	T_ASSERT_EQ_INT(0, err, "fsctl.set should not change info without root");
273}
274
275T_DECL(fsctl_delays,
276    "Validate I/O delays when DMC is enabled",
277    T_META_ENABLED(!TARGET_OS_BRIDGE), // diskutil is unavailable on bridgeOS
278    T_META_RUN_CONCURRENTLY(false), T_META_TAG_VM_PREFERRED)
279{
280	char *path;
281	int fd;
282	int err;
283	uint64_t elapsed_nsec, expected_nsec;
284	disk_conditioner_info info = {0};
285	char buf[READSIZE];
286
287	T_SETUPBEGIN;
288	perf_setup(&path, &fd);
289	memset(buf, 0xFF, sizeof(buf));
290	T_ASSERT_EQ_LONG((long)sizeof(buf), write(fd, buf, sizeof(buf)), "write random data to temp file");
291	fcntl(fd, F_FULLFSYNC);
292	T_SETUPEND;
293
294	expected_nsec = NSEC_PER_SEC / 2;
295
296	// measure delay before setting parameters (should be none)
297	elapsed_nsec = time_for_read(fd, buf);
298	T_ASSERT_LT_ULLONG(elapsed_nsec, expected_nsec, "DMC disabled read(%ld) is reasonably fast", READSIZE);
299
300	// measure delay after setting parameters
301	info.enabled = 1;
302	info.access_time_usec = expected_nsec / NSEC_PER_USEC;
303	info.read_throughput_mbps = 40;
304	info.write_throughput_mbps = 40;
305	info.is_ssd = 1; // is_ssd will ensure we get constant access_time delays rather than scaled
306	err = fsctl(path, DISK_CONDITIONER_IOC_SET, &info, 0);
307	T_WITH_ERRNO;
308	T_ASSERT_EQ_INT(0, err, "fsctl(DISK_CONDITIONER_IOC_SET) delay");
309
310	elapsed_nsec = time_for_read(fd, buf);
311	T_ASSERT_GT_ULLONG(elapsed_nsec, expected_nsec, "DMC enabled read(%ld) is at least the expected delay", READSIZE);
312	T_ASSERT_LT_ULLONG(elapsed_nsec, 2 * expected_nsec, "DMC enabled read(%ld) is no more than twice the expected delay", READSIZE);
313
314	// measure delay after resetting parameters (should be none)
315	info.enabled = 0;
316	err = fsctl(path, DISK_CONDITIONER_IOC_SET, &info, 0);
317	T_WITH_ERRNO;
318	T_ASSERT_EQ_INT(0, err, "fsctl(DISK_CONDITIONER_IOC_SET) reset delay");
319
320	usleep(USEC_PER_SEC / 2); // might still be other I/O inflight
321	elapsed_nsec = time_for_read(fd, buf);
322	T_ASSERT_LT_ULLONG(elapsed_nsec, expected_nsec, "After disabling DMC read(%ld) is reasonably fast", READSIZE);
323}
324
325#else /* TEST_UNENTITLED */
326
327#pragma mark Unentitled Tests
328
329T_DECL(fsctl_get_unentitled,
330    "fsctl.get should not require entitlement", T_META_TAG_VM_PREFERRED)
331{
332	int err;
333	char *mount_path;
334	disk_conditioner_info info;
335
336	T_SETUPBEGIN;
337	mount_path = mktempmount();
338	T_SETUPEND;
339
340	err = fsctl(mount_path, DISK_CONDITIONER_IOC_GET, &info, 0);
341	T_WITH_ERRNO;
342	T_ASSERT_EQ_INT(0, err, "fsctl.get without entitlement");
343}
344
345T_DECL(fsctl_set_unentitled,
346    "fsctl.set should require entitlement", T_META_TAG_VM_PREFERRED)
347{
348	int err;
349	char *mount_path;
350	disk_conditioner_info info = {0};
351	disk_conditioner_info expected_info = {0};
352
353	T_SETUPBEGIN;
354	mount_path = mktempmount();
355	T_SETUPEND;
356
357	// save original info
358	err = fsctl(mount_path, DISK_CONDITIONER_IOC_GET, &expected_info, 0);
359	T_WITH_ERRNO;
360	T_ASSERT_EQ_INT(0, err, "Get original DMC info");
361
362	info.enabled = 1;
363	info.access_time_usec = 10;
364	err = fsctl(mount_path, DISK_CONDITIONER_IOC_SET, &info, 0);
365	T_WITH_ERRNO;
366	T_ASSERT_NE_INT(0, err, "fsctl.set returns error without entitlement");
367
368	err = fsctl(mount_path, DISK_CONDITIONER_IOC_GET, &info, 0);
369	T_WITH_ERRNO;
370	T_ASSERT_EQ_INT(0, err, "fsctl.get after unentitled fsctl.set");
371
372	err = memcmp(&info, &expected_info, sizeof(info));
373	T_ASSERT_EQ_INT(0, err, "fsctl.set should not change info without entitlement");
374}
375
376#endif /* TEST_UNENTITLED */
377
378#pragma mark Helpers
379
380static char *
381mktempdir(void)
382{
383	char *path = malloc(PATH_MAX);
384	strcpy(path, "/tmp/dmc.XXXXXXXX");
385	atexit_b(^{ free(path); });
386
387	// create a temporary mount to run the fsctl on
388	T_WITH_ERRNO;
389	T_ASSERT_NOTNULL(mkdtemp(path), "Create temporary directory");
390	atexit_b(^{ remove(path); });
391
392	return path;
393}
394
395/*
396 * Return the path to a temporary mount
397 * with no usable filesystem but still
398 * can be configured by the disk conditioner
399 *
400 * Faster than creating a ram disk to test with
401 * when access to the filesystem is not necessary
402 */
403static char *
404mktempmount(void)
405{
406	char *mount_path = mktempdir();
407
408	T_WITH_ERRNO;
409	T_ASSERT_EQ_INT(0, mount("devfs", mount_path, MNT_RDONLY, NULL), "Create temporary devfs mount");
410	atexit_b(^{ unmount(mount_path, MNT_FORCE); });
411
412	return mount_path;
413}
414
415#ifndef TEST_UNENTITLED
416
417/*
418 * Wrapper around dt_launch_tool/dt_waitpid
419 * that works like libc:system()
420 */
421static int
422system_legal(const char *command)
423{
424	pid_t pid = -1;
425	int exit_status = 0;
426	const char *argv[] = {
427		_PATH_BSHELL,
428		"-c",
429		command,
430		NULL
431	};
432
433	int rc = dt_launch_tool(&pid, (char **)(void *)argv, false, NULL, NULL);
434	if (rc != 0) {
435		return -1;
436	}
437	if (!dt_waitpid(pid, &exit_status, NULL, 30)) {
438		if (exit_status != 0) {
439			return exit_status;
440		}
441		return -1;
442	}
443
444	return exit_status;
445}
446
447/*
448 * Return the path to a temporary mount
449 * that contains a usable APFS filesystem
450 * mounted via a ram disk
451 */
452static char *
453mkramdisk(void)
454{
455	char cmd[1024];
456	char *dev_disk_file = malloc(256);
457	atexit_b(^{ free(dev_disk_file); });
458	strcpy(dev_disk_file, "/tmp/dmc.ramdisk.XXXXXXXX");
459
460	T_QUIET; T_WITH_ERRNO; T_ASSERT_NOTNULL(mktemp(dev_disk_file), "Create temporary file to store dev disk for ramdisk");
461	atexit_b(^{ remove(dev_disk_file); });
462
463	// create the RAM disk device
464	// dev_disk_file will store the /dev/diskX path
465	snprintf(cmd, sizeof(cmd), "diskimagetool attach --nomount ram://16m > %s", dev_disk_file);
466	T_ASSERT_EQ_INT(0, system_legal(cmd), "Create ramdisk");
467
468	atexit_b(^{
469		char eject_cmd[1024];
470		snprintf(eject_cmd, sizeof(eject_cmd), "diskutil eject force `cat %s`", dev_disk_file);
471		system_legal(eject_cmd);
472		remove(dev_disk_file);
473	});
474
475	// initialize and mount as an APFS volume
476	snprintf(cmd, sizeof(cmd), "diskutil eraseVolume APFS dmc.ramdisk `cat %s`", dev_disk_file);
477	T_ASSERT_EQ_INT(0, system_legal(cmd), "Initialize ramdisk as APFS");
478
479	// on iOS the previous eraseVolume doesn't automatically mount
480	// on macOS this mount will be redundant, but successful
481	snprintf(cmd, sizeof(cmd), "diskutil mountDisk `cat %s`", dev_disk_file);
482	T_ASSERT_EQ_INT(0, system_legal(cmd), "Mount ramdisk");
483
484	// on iOS the previous mountDisk doesn't support -mountPoint, so we have to find where it was mounted
485	char *mount_info_path = malloc(256);
486	strcpy(mount_info_path, "/tmp/dmc.mount_info.XXXXXXXX");
487	T_QUIET; T_WITH_ERRNO; T_ASSERT_NOTNULL(mktemp(mount_info_path), "Create temporary file to store mount info for ramdisk");
488	atexit_b(^{ remove(mount_info_path); });
489
490	snprintf(cmd, sizeof(cmd), "diskimagetool list -plist `cat %s` > %s", dev_disk_file, mount_info_path);
491	T_QUIET; T_ASSERT_EQ_INT(0, system_legal(cmd), "Fetch ramdisk mount info");
492
493	NSURL *mountInfoURL = [NSURL fileURLWithPath:@(mount_info_path) isDirectory:NO];
494	free(mount_info_path);
495
496	NSError *error;
497	NSDictionary *mountInfo = [NSDictionary dictionaryWithContentsOfURL:mountInfoURL error:&error];
498	if (!mountInfo) {
499		T_LOG("Error: %s", error.localizedDescription.UTF8String);
500	}
501	T_QUIET; T_ASSERT_NOTNULL(mountInfo, "Read mount info plist");
502
503	NSString *mountPoint = nil;
504	for (NSDictionary *entity in (NSArray *)mountInfo[@"System Entities"]) {
505		mountPoint = entity[@"Mount Point"];
506		if (mountPoint) {
507			break;
508		}
509	}
510	T_QUIET; T_ASSERT_NOTNULL(mountPoint, "Find mount point in mount info plist");
511
512	char *mount_path = malloc(PATH_MAX);
513	atexit_b(^{ free(mount_path); });
514	strlcpy(mount_path, mountPoint.UTF8String, PATH_MAX);
515	return mount_path;
516}
517
518static uint64_t
519time_for_read(int fd, const char *expected)
520{
521	int err;
522	ssize_t ret;
523	char buf[READSIZE];
524	uint64_t start, stop;
525
526	bzero(buf, sizeof(buf));
527	lseek(fd, 0, SEEK_SET);
528
529	start = dt_nanoseconds();
530	ret = read(fd, buf, READSIZE);
531	stop = dt_nanoseconds();
532
533	T_QUIET; T_ASSERT_GE_LONG(ret, 0L, "read from temporary file");
534	T_QUIET; T_ASSERT_EQ_LONG(ret, READSIZE, "read %ld bytes from temporary file", READSIZE);
535	err = memcmp(buf, expected, sizeof(buf));
536	T_QUIET; T_ASSERT_EQ_INT(0, err, "read expected contents from temporary file");
537
538	return stop - start;
539}
540
541static void
542perf_setup(char **path, int *fd)
543{
544	int temp_fd;
545	char *temp_path;
546
547	char *mount_path = mkramdisk();
548	T_LOG("Using ramdisk mounted at %s", mount_path);
549
550	temp_path = *path = malloc(PATH_MAX);
551	snprintf(temp_path, PATH_MAX, "%s/dmc.XXXXXXXX", mount_path);
552	atexit_b(^{ free(temp_path); });
553
554	T_ASSERT_NOTNULL(mktemp(temp_path), "Create temporary file");
555	atexit_b(^{ remove(temp_path); });
556	T_LOG("Using temporary file at %s", temp_path);
557
558	temp_fd = *fd = open(temp_path, O_RDWR | O_CREAT);
559	T_WITH_ERRNO;
560	T_ASSERT_GE_INT(temp_fd, 0, "Open temporary file for read/write");
561	atexit_b(^{ close(temp_fd); });
562	fcntl(temp_fd, F_NOCACHE, 1);
563}
564#endif /* !TEST_UNENTITLED */
565