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