1 // SPDX-License-Identifier: GPL-2.0 2 #define _GNU_SOURCE 3 #include <errno.h> 4 #include <fcntl.h> 5 #include <limits.h> 6 #include <link.h> 7 #include <sched.h> 8 #include <signal.h> 9 #include <stdio.h> 10 #include <stdlib.h> 11 #include <string.h> 12 #include <sys/mount.h> 13 #include <sys/prctl.h> 14 #include <sys/stat.h> 15 #include <sys/syscall.h> 16 #include <sys/types.h> 17 #include <sys/vfs.h> 18 #include <sys/wait.h> 19 #include <unistd.h> 20 21 #include "../../kselftest_harness.h" 22 23 #ifndef __NR_fchroot 24 #define __NR_fchroot 472 25 #endif 26 27 #ifndef FD_PIDFS_ROOT 28 #define FD_PIDFS_ROOT -10002 29 #endif 30 31 #ifndef FD_NSFS_ROOT 32 #define FD_NSFS_ROOT -10003 33 #endif 34 35 #ifndef FD_FAILFS_ROOT 36 #define FD_FAILFS_ROOT -10004 37 #endif 38 39 #define NOBODY_UID 65534 40 41 /* Child sentinel exit code: the exec was blocked as expected. */ 42 #define FAILFS_EXEC_BLOCKED 99 43 44 /* Stack for the CLONE_FS helper in fchroot_sentinel_shared_fs_struct. */ 45 #define FAILFS_CLONE_STACK (64 * 1024) 46 47 static int sys_fchroot(int fd, unsigned int flags) 48 { 49 return syscall(__NR_fchroot, fd, flags); 50 } 51 52 /* 53 * Raw syscall: glibc's getcwd() rejects the kernel's "(unreachable)" 54 * result and falls back to a generic implementation. 55 */ 56 static long sys_getcwd(char *buf, size_t size) 57 { 58 return syscall(__NR_getcwd, buf, size); 59 } 60 61 static int drop_to_nobody(void) 62 { 63 return setresuid(NOBODY_UID, NOBODY_UID, NOBODY_UID); 64 } 65 66 /* Parked CLONE_FS child; dies with its parent so it never leaks. */ 67 static int failfs_park(void *arg) 68 { 69 pid_t parent = (pid_t)(long)arg; 70 71 prctl(PR_SET_PDEATHSIG, SIGKILL); 72 /* The parent may have died before the death signal was armed. */ 73 if (getppid() != parent) 74 _exit(0); 75 pause(); 76 return 0; 77 } 78 79 /* Is fd a dynamically linked ELF with an absolute PT_INTERP interpreter? */ 80 static int elf_has_absolute_interp(int fd) 81 { 82 ElfW(Ehdr) ehdr; 83 ElfW(Phdr) phdr; 84 char interp; 85 int i; 86 87 if (pread(fd, &ehdr, sizeof(ehdr), 0) != sizeof(ehdr)) 88 return 0; 89 if (memcmp(ehdr.e_ident, ELFMAG, SELFMAG) != 0) 90 return 0; 91 92 for (i = 0; i < ehdr.e_phnum; i++) { 93 if (pread(fd, &phdr, sizeof(phdr), 94 ehdr.e_phoff + i * sizeof(phdr)) != sizeof(phdr)) 95 return 0; 96 if (phdr.p_type != PT_INTERP) 97 continue; 98 if (pread(fd, &interp, 1, phdr.p_offset) != 1) 99 return 0; 100 return interp == '/'; 101 } 102 103 return 0; 104 } 105 106 TEST(fchdir_sentinel) 107 { 108 char buf[PATH_MAX]; 109 int fd; 110 111 ASSERT_EQ(fchdir(FD_FAILFS_ROOT), 0); 112 113 /* The working directory is unreachable from the process root. */ 114 ASSERT_GT(sys_getcwd(buf, sizeof(buf)), 0); 115 ASSERT_EQ(strncmp(buf, "(unreachable)", 13), 0); 116 117 /* Every AT_FDCWD-relative lookup fails. */ 118 ASSERT_EQ(openat(AT_FDCWD, "foo", O_RDONLY), -1); 119 ASSERT_EQ(errno, EOPNOTSUPP); 120 ASSERT_EQ(openat(AT_FDCWD, ".", O_RDONLY), -1); 121 ASSERT_EQ(errno, EOPNOTSUPP); 122 ASSERT_EQ(openat(AT_FDCWD, "..", O_RDONLY), -1); 123 ASSERT_EQ(errno, EOPNOTSUPP); 124 ASSERT_EQ(openat(AT_FDCWD, "foo", O_WRONLY | O_CREAT, 0600), -1); 125 ASSERT_EQ(errno, EOPNOTSUPP); 126 127 /* The cwd cannot be pinned by following /proc/self/cwd into it. */ 128 ASSERT_EQ(open("/proc/self/cwd", O_PATH), -1); 129 ASSERT_EQ(errno, EOPNOTSUPP); 130 131 /* The root is untouched so absolute lookups keep working... */ 132 fd = open("/", O_RDONLY | O_DIRECTORY); 133 ASSERT_GE(fd, 0); 134 ASSERT_EQ(close(fd), 0); 135 136 /* ... and the working directory can be recovered. */ 137 ASSERT_EQ(chdir("/"), 0); 138 ASSERT_GT(sys_getcwd(buf, sizeof(buf)), 0); 139 ASSERT_EQ(strcmp(buf, "/"), 0); 140 } 141 142 TEST(fchdir_rejects_other_sentinels) 143 { 144 ASSERT_EQ(fchdir(FD_PIDFS_ROOT), -1); 145 ASSERT_EQ(errno, EBADF); 146 ASSERT_EQ(fchdir(FD_NSFS_ROOT), -1); 147 ASSERT_EQ(errno, EBADF); 148 ASSERT_EQ(fchdir(-10009), -1); 149 ASSERT_EQ(errno, EBADF); 150 } 151 152 TEST(fchroot_flags) 153 { 154 int fd; 155 156 ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 1), -1); 157 ASSERT_EQ(errno, EINVAL); 158 159 fd = open("/", O_PATH | O_DIRECTORY); 160 ASSERT_GE(fd, 0); 161 ASSERT_EQ(sys_fchroot(fd, 1), -1); 162 ASSERT_EQ(errno, EINVAL); 163 ASSERT_EQ(close(fd), 0); 164 } 165 166 TEST(fchroot_bad_fd) 167 { 168 ASSERT_EQ(sys_fchroot(-1, 0), -1); 169 ASSERT_EQ(errno, EBADF); 170 171 /* Only FD_FAILFS_ROOT is a valid sentinel. */ 172 ASSERT_EQ(sys_fchroot(FD_PIDFS_ROOT, 0), -1); 173 ASSERT_EQ(errno, EBADF); 174 ASSERT_EQ(sys_fchroot(FD_NSFS_ROOT, 0), -1); 175 ASSERT_EQ(errno, EBADF); 176 } 177 178 TEST(fchroot_notdir) 179 { 180 int fd; 181 182 fd = open("/proc/self/status", O_RDONLY); 183 ASSERT_GE(fd, 0); 184 ASSERT_EQ(sys_fchroot(fd, 0), -1); 185 ASSERT_EQ(errno, ENOTDIR); 186 ASSERT_EQ(close(fd), 0); 187 } 188 189 TEST(fchroot_realfd_requires_cap) 190 { 191 int fd; 192 193 if (geteuid() == 0) 194 ASSERT_EQ(drop_to_nobody(), 0); 195 196 fd = open("/", O_PATH | O_DIRECTORY); 197 ASSERT_GE(fd, 0); 198 ASSERT_EQ(sys_fchroot(fd, 0), -1); 199 ASSERT_EQ(errno, EPERM); 200 ASSERT_EQ(close(fd), 0); 201 } 202 203 TEST(fchroot_realfd) 204 { 205 char template[] = "/tmp/failfs_test.XXXXXX"; 206 char path[PATH_MAX]; 207 struct stat st; 208 int tmpfd, dfd, fd; 209 210 if (geteuid() != 0) 211 SKIP(return, "fchroot() with a regular fd requires CAP_SYS_CHROOT"); 212 213 tmpfd = open("/tmp", O_PATH | O_DIRECTORY); 214 ASSERT_GE(tmpfd, 0); 215 216 ASSERT_NE(mkdtemp(template), NULL); 217 snprintf(path, sizeof(path), "%s/canary", template); 218 fd = open(path, O_WRONLY | O_CREAT, 0600); 219 ASSERT_GE(fd, 0); 220 ASSERT_EQ(close(fd), 0); 221 222 dfd = open(template, O_PATH | O_DIRECTORY); 223 ASSERT_GE(dfd, 0); 224 ASSERT_EQ(sys_fchroot(dfd, 0), 0); 225 ASSERT_EQ(close(dfd), 0); 226 227 ASSERT_EQ(stat("/canary", &st), 0); 228 229 /* Best-effort cleanup: dirfd-anchored I/O works with the new root. */ 230 snprintf(path, sizeof(path), "%s/canary", template + strlen("/tmp/")); 231 unlinkat(tmpfd, path, 0); 232 unlinkat(tmpfd, template + strlen("/tmp/"), AT_REMOVEDIR); 233 } 234 235 TEST(fchroot_sentinel) 236 { 237 char template[] = "/tmp/failfs_test.XXXXXX"; 238 struct stat realroot, st; 239 struct statfs sfs; 240 char buf[PATH_MAX]; 241 int procfd, tmpfd, dfd, fd; 242 struct { 243 struct file_handle handle; 244 unsigned char f_handle[MAX_HANDLE_SZ]; 245 } fh; 246 int mntid; 247 ssize_t ret; 248 249 if (geteuid() != 0) 250 SKIP(return, "privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT"); 251 252 ASSERT_EQ(stat("/", &realroot), 0); 253 procfd = open("/proc", O_PATH | O_DIRECTORY); 254 ASSERT_GE(procfd, 0); 255 tmpfd = open("/tmp", O_PATH | O_DIRECTORY); 256 ASSERT_GE(tmpfd, 0); 257 ASSERT_NE(mkdtemp(template), NULL); 258 dfd = open(template, O_RDONLY | O_DIRECTORY); 259 ASSERT_GE(dfd, 0); 260 261 ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0); 262 263 /* Absolute lookups fail. */ 264 ASSERT_EQ(open("/etc/passwd", O_RDONLY), -1); 265 ASSERT_EQ(errno, EOPNOTSUPP); 266 ASSERT_EQ(mkdir("/foo", 0700), -1); 267 ASSERT_EQ(errno, EOPNOTSUPP); 268 269 /* 270 * The root cannot be referenced at all - not even an O_PATH open, 271 * which skips ->permission(), because it lands on the root as a 272 * jumped walk terminal that ->d_weak_revalidate() refuses. 273 */ 274 ASSERT_EQ(open("/", O_RDONLY | O_DIRECTORY), -1); 275 ASSERT_EQ(errno, EOPNOTSUPP); 276 ASSERT_EQ(open("/", O_PATH), -1); 277 ASSERT_EQ(errno, EOPNOTSUPP); 278 ASSERT_EQ(statfs("/", &sfs), -1); 279 ASSERT_EQ(errno, EOPNOTSUPP); 280 281 /* 282 * It cannot be pinned by following /proc/self/root into it either 283 * (only the root is in failfs here, so self/cwd is still real). 284 */ 285 ASSERT_EQ(openat(procfd, "self/root", O_PATH), -1); 286 ASSERT_EQ(errno, EOPNOTSUPP); 287 288 /* Nor encoded into a file handle. */ 289 fh.handle.handle_bytes = MAX_HANDLE_SZ; 290 ASSERT_EQ(name_to_handle_at(AT_FDCWD, "/", &fh.handle, &mntid, 0), -1); 291 ASSERT_EQ(errno, EOPNOTSUPP); 292 293 /* The working directory is now unreachable from the root. */ 294 ASSERT_GT(sys_getcwd(buf, sizeof(buf)), 0); 295 ASSERT_EQ(strncmp(buf, "(unreachable)", 13), 0); 296 297 /* Lookups anchored at real directories keep working. */ 298 fd = openat(AT_FDCWD, ".", O_RDONLY | O_DIRECTORY); 299 ASSERT_GE(fd, 0); 300 ASSERT_EQ(close(fd), 0); 301 fd = openat(dfd, "canary", O_WRONLY | O_CREAT, 0600); 302 ASSERT_GE(fd, 0); 303 ASSERT_EQ(write(fd, "x", 1), 1); 304 ASSERT_EQ(close(fd), 0); 305 fd = openat(dfd, "canary", O_RDONLY); 306 ASSERT_GE(fd, 0); 307 ASSERT_EQ(close(fd), 0); 308 309 /* ".." walks clamp at the top of the mount tree, not at failfs. */ 310 fd = openat(AT_FDCWD, "../../../../../../../../../..", O_PATH); 311 ASSERT_GE(fd, 0); 312 ASSERT_EQ(fstat(fd, &st), 0); 313 ASSERT_EQ(st.st_dev, realroot.st_dev); 314 ASSERT_EQ(st.st_ino, realroot.st_ino); 315 ASSERT_EQ(close(fd), 0); 316 317 /* readlink of the magic link still works: it does not follow. */ 318 ret = readlinkat(procfd, "self/root", buf, sizeof(buf) - 1); 319 ASSERT_GT(ret, 0); 320 buf[ret] = '\0'; 321 TH_LOG("/proc/self/root points to '%s'", buf); 322 /* d_path() names the failfs root synthetically, never as a real path. */ 323 ASSERT_EQ(strcmp(buf, "failfs:/"), 0); 324 325 /* But following it into failfs is refused. */ 326 ASSERT_EQ(fstatat(procfd, "self/root", &st, 0), -1); 327 ASSERT_EQ(errno, EOPNOTSUPP); 328 329 /* Best-effort cleanup via the pre-opened dirfds. */ 330 unlinkat(dfd, "canary", 0); 331 unlinkat(tmpfd, template + strlen("/tmp/"), AT_REMOVEDIR); 332 } 333 334 TEST(fchroot_sentinel_absolute_symlink) 335 { 336 char template[] = "/tmp/failfs_test.XXXXXX"; 337 int tmpfd, dfd, fd; 338 339 if (geteuid() != 0) 340 SKIP(return, "privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT"); 341 342 tmpfd = open("/tmp", O_PATH | O_DIRECTORY); 343 ASSERT_GE(tmpfd, 0); 344 ASSERT_NE(mkdtemp(template), NULL); 345 dfd = open(template, O_RDONLY | O_DIRECTORY); 346 ASSERT_GE(dfd, 0); 347 348 fd = openat(dfd, "target", O_WRONLY | O_CREAT, 0600); 349 ASSERT_GE(fd, 0); 350 ASSERT_EQ(close(fd), 0); 351 ASSERT_EQ(symlinkat("target", dfd, "rel"), 0); 352 ASSERT_EQ(symlinkat("/etc", dfd, "abs"), 0); 353 354 ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0); 355 356 /* Relative symlinks keep resolving within the dirfd-anchored walk... */ 357 fd = openat(dfd, "rel", O_RDONLY); 358 ASSERT_GE(fd, 0); 359 ASSERT_EQ(close(fd), 0); 360 361 /* ... absolute symlinks restart the walk at the failfs root. */ 362 ASSERT_EQ(openat(dfd, "abs", O_RDONLY), -1); 363 ASSERT_EQ(errno, EOPNOTSUPP); 364 365 /* Best-effort cleanup via the pre-opened dirfds. */ 366 unlinkat(dfd, "abs", 0); 367 unlinkat(dfd, "rel", 0); 368 unlinkat(dfd, "target", 0); 369 unlinkat(tmpfd, template + strlen("/tmp/"), AT_REMOVEDIR); 370 } 371 372 TEST(fchroot_sentinel_unprivileged) 373 { 374 char buf[PATH_MAX]; 375 376 if (geteuid() == 0) 377 ASSERT_EQ(drop_to_nobody(), 0); 378 379 /* Without no_new_privs entering failfs is not allowed... */ 380 ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), -1); 381 ASSERT_EQ(errno, EPERM); 382 383 /* ... with no_new_privs set it is allowed. */ 384 ASSERT_EQ(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0), 0); 385 ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0); 386 387 ASSERT_EQ(open("/etc/passwd", O_RDONLY), -1); 388 ASSERT_EQ(errno, EOPNOTSUPP); 389 390 /* The task counts as chrooted: no user namespaces anymore. */ 391 ASSERT_EQ(unshare(CLONE_NEWUSER), -1); 392 ASSERT_EQ(errno, EPERM); 393 394 /* With both root and cwd in failfs getcwd() reports "/". */ 395 ASSERT_EQ(fchdir(FD_FAILFS_ROOT), 0); 396 ASSERT_GT(sys_getcwd(buf, sizeof(buf)), 0); 397 ASSERT_EQ(strcmp(buf, "/"), 0); 398 } 399 400 TEST(fchroot_sentinel_rejected_when_chrooted) 401 { 402 char template[] = "/tmp/failfs_test.XXXXXX"; 403 int tmpfd; 404 405 if (geteuid() != 0) 406 SKIP(return, "chroot() requires CAP_SYS_CHROOT"); 407 408 tmpfd = open("/tmp", O_PATH | O_DIRECTORY); 409 ASSERT_GE(tmpfd, 0); 410 ASSERT_NE(mkdtemp(template), NULL); 411 ASSERT_EQ(chroot(template), 0); 412 ASSERT_EQ(chdir("/"), 0); 413 414 /* Remove the jail while still privileged; sticky /tmp blocks nobody. */ 415 unlinkat(tmpfd, template + strlen("/tmp/"), AT_REMOVEDIR); 416 417 ASSERT_EQ(drop_to_nobody(), 0); 418 ASSERT_EQ(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0), 0); 419 420 /* An unprivileged chrooted task must not lift its ".." barrier. */ 421 ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), -1); 422 ASSERT_EQ(errno, EPERM); 423 } 424 425 TEST(fchroot_sentinel_shared_fs_struct) 426 { 427 char stack[FAILFS_CLONE_STACK]; 428 pid_t pid; 429 430 if (geteuid() == 0) 431 ASSERT_EQ(drop_to_nobody(), 0); 432 433 /* A CLONE_FS sibling shares the fs_struct: bump fs->users to 2. */ 434 pid = clone(failfs_park, stack + sizeof(stack), CLONE_FS | SIGCHLD, 435 (void *)(long)getpid()); 436 ASSERT_GE(pid, 0); 437 438 ASSERT_EQ(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0), 0); 439 440 /* 441 * A sibling without no_new_privs could exec a setuid binary with 442 * the failfs root, so a shared fs_struct is refused even with 443 * no_new_privs set. 444 */ 445 ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), -1); 446 ASSERT_EQ(errno, EINVAL); 447 448 ASSERT_EQ(kill(pid, SIGKILL), 0); 449 ASSERT_EQ(waitpid(pid, NULL, 0), pid); 450 } 451 452 TEST(fchroot_sentinel_no_overmount) 453 { 454 if (geteuid() != 0) 455 SKIP(return, "mounting requires privileges"); 456 457 /* 458 * Contain the blast radius: if failfs ever regressed and "/" 459 * resolved to the real root, the tmpfs mount below must not touch 460 * the host. A private mount namespace keeps it local to this child. 461 */ 462 ASSERT_EQ(unshare(CLONE_NEWNS), 0); 463 ASSERT_EQ(mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL), 0); 464 465 ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0); 466 467 /* 468 * Nothing can be mounted on top of the failfs root. It cannot even 469 * be named as a mount target: resolving "/" is refused before the 470 * mount machinery (which, failfs being in no mount namespace, would 471 * reject it anyway) is ever reached. open_tree(OPEN_TREE_CLONE) is 472 * likewise moot since no fd to the root can be obtained. 473 */ 474 ASSERT_EQ(mount("none", "/", "tmpfs", 0, NULL), -1); 475 ASSERT_EQ(errno, EOPNOTSUPP); 476 } 477 478 TEST(fchroot_sentinel_setns_escape) 479 { 480 struct stat realroot, st; 481 int nsfd; 482 483 if (geteuid() != 0) 484 SKIP(return, "setns() to a mount namespace requires privileges"); 485 486 ASSERT_EQ(stat("/", &realroot), 0); 487 nsfd = open("/proc/self/ns/mnt", O_RDONLY); 488 ASSERT_GE(nsfd, 0); 489 490 ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0); 491 ASSERT_EQ(open("/etc", O_PATH), -1); 492 ASSERT_EQ(errno, EOPNOTSUPP); 493 494 /* A mount namespace fd is the key out: it resets root and cwd. */ 495 ASSERT_EQ(setns(nsfd, CLONE_NEWNS), 0); 496 ASSERT_EQ(close(nsfd), 0); 497 498 ASSERT_EQ(stat("/", &st), 0); 499 ASSERT_EQ(st.st_dev, realroot.st_dev); 500 ASSERT_EQ(st.st_ino, realroot.st_ino); 501 } 502 503 TEST(fchroot_sentinel_exec) 504 { 505 pid_t pid; 506 int status; 507 508 if (geteuid() != 0) 509 SKIP(return, "privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT"); 510 511 ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0); 512 513 /* 514 * Exec in a child: a wrongly successful exec would replace the test 515 * image and its exit code would not match the sentinel below. 516 */ 517 pid = fork(); 518 ASSERT_GE(pid, 0); 519 if (pid == 0) { 520 execl("/bin/true", "true", NULL); 521 _exit(errno == EOPNOTSUPP ? FAILFS_EXEC_BLOCKED : 1); 522 } 523 ASSERT_EQ(waitpid(pid, &status, 0), pid); 524 ASSERT_TRUE(WIFEXITED(status)); 525 ASSERT_EQ(WEXITSTATUS(status), FAILFS_EXEC_BLOCKED); 526 } 527 528 TEST(fchroot_sentinel_exec_interpreter) 529 { 530 static const char * const argv[] = { "failfs_test", NULL }; 531 static const char * const envp[] = { NULL }; 532 pid_t pid; 533 int status, exefd; 534 535 if (geteuid() != 0) 536 SKIP(return, "privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT"); 537 538 /* Exec ourselves: the one binary guaranteed to be around. */ 539 exefd = open("/proc/self/exe", O_RDONLY); 540 ASSERT_GE(exefd, 0); 541 if (!elf_has_absolute_interp(exefd)) 542 SKIP(return, "test binary has no absolute PT_INTERP interpreter"); 543 544 ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0); 545 546 /* 547 * The binary itself needs no path lookup - it is executed by fd - 548 * but loading it fails on opening the absolute PT_INTERP 549 * interpreter. Run it in a child so a wrongly successful exec does 550 * not replace the test image and masquerade as a pass. 551 */ 552 pid = fork(); 553 ASSERT_GE(pid, 0); 554 if (pid == 0) { 555 syscall(__NR_execveat, exefd, "", argv, envp, AT_EMPTY_PATH); 556 _exit(errno == EOPNOTSUPP ? FAILFS_EXEC_BLOCKED : 1); 557 } 558 ASSERT_EQ(waitpid(pid, &status, 0), pid); 559 ASSERT_TRUE(WIFEXITED(status)); 560 ASSERT_EQ(WEXITSTATUS(status), FAILFS_EXEC_BLOCKED); 561 } 562 563 TEST(fchroot_sentinel_inherited) 564 { 565 pid_t pid; 566 int status; 567 568 if (geteuid() != 0) 569 SKIP(return, "privileged fchroot(FD_FAILFS_ROOT) requires CAP_SYS_CHROOT"); 570 571 ASSERT_EQ(sys_fchroot(FD_FAILFS_ROOT, 0), 0); 572 573 pid = fork(); 574 ASSERT_GE(pid, 0); 575 if (pid == 0) { 576 if (open("/etc", O_PATH) != -1 || errno != EOPNOTSUPP) 577 _exit(1); 578 _exit(0); 579 } 580 ASSERT_EQ(waitpid(pid, &status, 0), pid); 581 ASSERT_TRUE(WIFEXITED(status)); 582 ASSERT_EQ(WEXITSTATUS(status), 0); 583 } 584 585 TEST_HARNESS_MAIN 586