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