1 // SPDX-License-Identifier: GPL-2.0-or-later 2 /* 3 * Author: Aleksa Sarai <cyphar@cyphar.com> 4 * Copyright (C) 2018-2019 SUSE LLC. 5 */ 6 7 #define _GNU_SOURCE 8 #include <fcntl.h> 9 #include <sched.h> 10 #include <sys/stat.h> 11 #include <sys/types.h> 12 #include <sys/mount.h> 13 #include <stdlib.h> 14 #include <stdbool.h> 15 #include <string.h> 16 17 #include "helpers.h" 18 #include "kselftest_harness.h" 19 20 struct resolve_test { 21 const char *name; 22 const char *dir; 23 const char *path; 24 struct open_how how; 25 bool pass; 26 union { 27 int err; 28 const char *path; 29 } out; 30 }; 31 32 /* 33 * Verify a single resolve test case. This must be called from within a TEST_F 34 * function with _metadata in scope. 35 */ 36 static void verify_resolve_test(struct __test_metadata *_metadata, 37 int rootfd, int hardcoded_fd, 38 const struct resolve_test *test) 39 { 40 struct open_how how = test->how; 41 int dfd, fd; 42 char *fdpath = NULL; 43 44 /* Auto-set O_PATH. */ 45 if (!(how.flags & O_CREAT)) 46 how.flags |= O_PATH; 47 48 if (test->dir) 49 dfd = openat(rootfd, test->dir, O_PATH | O_DIRECTORY); 50 else 51 dfd = dup(rootfd); 52 ASSERT_GE(dfd, 0) TH_LOG("failed to open dir '%s': %m", test->dir ?: "."); 53 ASSERT_EQ(dup2(dfd, hardcoded_fd), hardcoded_fd); 54 55 fd = sys_openat2(dfd, test->path, &how); 56 57 if (test->pass) { 58 EXPECT_GE(fd, 0) { 59 TH_LOG("%s: expected success, got %d (%s)", 60 test->name, fd, strerror(-fd)); 61 } 62 if (fd >= 0) { 63 EXPECT_TRUE(fdequal(_metadata, fd, rootfd, test->out.path)) { 64 fdpath = fdreadlink(_metadata, fd); 65 TH_LOG("%s: wrong path '%s', expected '%s'", 66 test->name, fdpath, 67 test->out.path ?: "."); 68 free(fdpath); 69 } 70 } 71 } else { 72 EXPECT_EQ(test->out.err, fd) { 73 if (fd >= 0) { 74 fdpath = fdreadlink(_metadata, fd); 75 TH_LOG("%s: expected %d (%s), got %d['%s']", 76 test->name, test->out.err, 77 strerror(-test->out.err), fd, fdpath); 78 free(fdpath); 79 } else { 80 TH_LOG("%s: expected %d (%s), got %d (%s)", 81 test->name, test->out.err, 82 strerror(-test->out.err), 83 fd, strerror(-fd)); 84 } 85 } 86 } 87 88 if (fd >= 0) 89 close(fd); 90 close(dfd); 91 } 92 93 /* 94 * Construct a test directory with the following structure: 95 * 96 * root/ 97 * |-- procexe -> /proc/self/exe 98 * |-- procroot -> /proc/self/root 99 * |-- root/ 100 * |-- mnt/ [mountpoint] 101 * | |-- self -> ../mnt/ 102 * | `-- absself -> /mnt/ 103 * |-- etc/ 104 * | `-- passwd 105 * |-- creatlink -> /newfile3 106 * |-- reletc -> etc/ 107 * |-- relsym -> etc/passwd 108 * |-- absetc -> /etc/ 109 * |-- abssym -> /etc/passwd 110 * |-- abscheeky -> /cheeky 111 * `-- cheeky/ 112 * |-- absself -> / 113 * |-- self -> ../../root/ 114 * |-- garbageself -> /../../root/ 115 * |-- passwd -> ../cheeky/../etc/../etc/passwd 116 * |-- abspasswd -> /../cheeky/../etc/../etc/passwd 117 * |-- dotdotlink -> ../../../../../../../../../../../../../../etc/passwd 118 * `-- garbagelink -> /../../../../../../../../../../../../../../etc/passwd 119 */ 120 FIXTURE(openat2_resolve) { 121 int rootfd; 122 int hardcoded_fd; 123 char *hardcoded_fdpath; 124 char *procselfexe; 125 }; 126 127 FIXTURE_SETUP(openat2_resolve) 128 { 129 char dirname[] = "/tmp/ksft-openat2-testdir.XXXXXX"; 130 int dfd, tmpfd; 131 132 self->rootfd = -1; 133 self->hardcoded_fd = -1; 134 self->hardcoded_fdpath = NULL; 135 self->procselfexe = NULL; 136 137 /* NOTE: We should be checking for CAP_SYS_ADMIN here... */ 138 if (geteuid() != 0) 139 SKIP(return, "all tests require euid == 0"); 140 if (!openat2_supported) 141 SKIP(return, "openat2(2) not supported"); 142 143 /* Unshare and make /tmp a new directory. */ 144 ASSERT_EQ(unshare(CLONE_NEWNS), 0); 145 ASSERT_EQ(mount("", "/tmp", "", MS_PRIVATE, ""), 0); 146 147 /* Make the top-level directory. */ 148 ASSERT_NE(mkdtemp(dirname), NULL); 149 dfd = open(dirname, O_PATH | O_DIRECTORY); 150 ASSERT_GE(dfd, 0); 151 152 /* A sub-directory which is actually used for tests. */ 153 ASSERT_EQ(mkdirat(dfd, "root", 0755), 0); 154 tmpfd = openat(dfd, "root", O_PATH | O_DIRECTORY); 155 ASSERT_GE(tmpfd, 0); 156 close(dfd); 157 dfd = tmpfd; 158 159 ASSERT_EQ(symlinkat("/proc/self/exe", dfd, "procexe"), 0); 160 ASSERT_EQ(symlinkat("/proc/self/root", dfd, "procroot"), 0); 161 ASSERT_EQ(mkdirat(dfd, "root", 0755), 0); 162 163 /* There is no mountat(2), so use chdir. */ 164 ASSERT_EQ(mkdirat(dfd, "mnt", 0755), 0); 165 ASSERT_EQ(fchdir(dfd), 0); 166 ASSERT_EQ(mount("tmpfs", "./mnt", "tmpfs", MS_NOSUID | MS_NODEV, ""), 0); 167 ASSERT_EQ(symlinkat("../mnt/", dfd, "mnt/self"), 0); 168 ASSERT_EQ(symlinkat("/mnt/", dfd, "mnt/absself"), 0); 169 170 ASSERT_EQ(mkdirat(dfd, "etc", 0755), 0); 171 ASSERT_GE(touchat(dfd, "etc/passwd"), 0); 172 173 ASSERT_EQ(symlinkat("/newfile3", dfd, "creatlink"), 0); 174 ASSERT_EQ(symlinkat("etc/", dfd, "reletc"), 0); 175 ASSERT_EQ(symlinkat("etc/passwd", dfd, "relsym"), 0); 176 ASSERT_EQ(symlinkat("/etc/", dfd, "absetc"), 0); 177 ASSERT_EQ(symlinkat("/etc/passwd", dfd, "abssym"), 0); 178 ASSERT_EQ(symlinkat("/cheeky", dfd, "abscheeky"), 0); 179 180 ASSERT_EQ(mkdirat(dfd, "cheeky", 0755), 0); 181 182 ASSERT_EQ(symlinkat("/", dfd, "cheeky/absself"), 0); 183 ASSERT_EQ(symlinkat("../../root/", dfd, "cheeky/self"), 0); 184 ASSERT_EQ(symlinkat("/../../root/", dfd, "cheeky/garbageself"), 0); 185 186 ASSERT_EQ(symlinkat("../cheeky/../etc/../etc/passwd", 187 dfd, "cheeky/passwd"), 0); 188 ASSERT_EQ(symlinkat("/../cheeky/../etc/../etc/passwd", 189 dfd, "cheeky/abspasswd"), 0); 190 191 ASSERT_EQ(symlinkat("../../../../../../../../../../../../../../etc/passwd", 192 dfd, "cheeky/dotdotlink"), 0); 193 ASSERT_EQ(symlinkat("/../../../../../../../../../../../../../../etc/passwd", 194 dfd, "cheeky/garbagelink"), 0); 195 196 self->rootfd = dfd; 197 198 self->hardcoded_fd = open("/dev/null", O_RDONLY); 199 ASSERT_GE(self->hardcoded_fd, 0); 200 ASSERT_GE(asprintf(&self->hardcoded_fdpath, "self/fd/%d", 201 self->hardcoded_fd), 0); 202 ASSERT_GE(asprintf(&self->procselfexe, "/proc/%d/exe", getpid()), 0); 203 } 204 205 FIXTURE_TEARDOWN(openat2_resolve) 206 { 207 free(self->procselfexe); 208 free(self->hardcoded_fdpath); 209 if (self->hardcoded_fd >= 0) 210 close(self->hardcoded_fd); 211 if (self->rootfd >= 0) 212 close(self->rootfd); 213 } 214 215 /* Attempts to cross the dirfd should be blocked with -EXDEV. */ 216 TEST_F(openat2_resolve, resolve_beneath) 217 { 218 struct resolve_test tests[] = { 219 /* Attempts to cross dirfd should be blocked. */ 220 { .name = "[beneath] jump to /", 221 .path = "/", .how.resolve = RESOLVE_BENEATH, 222 .out.err = -EXDEV, .pass = false }, 223 { .name = "[beneath] absolute link to $root", 224 .path = "cheeky/absself", .how.resolve = RESOLVE_BENEATH, 225 .out.err = -EXDEV, .pass = false }, 226 { .name = "[beneath] chained absolute links to $root", 227 .path = "abscheeky/absself", .how.resolve = RESOLVE_BENEATH, 228 .out.err = -EXDEV, .pass = false }, 229 { .name = "[beneath] jump outside $root", 230 .path = "..", .how.resolve = RESOLVE_BENEATH, 231 .out.err = -EXDEV, .pass = false }, 232 { .name = "[beneath] temporary jump outside $root", 233 .path = "../root/", .how.resolve = RESOLVE_BENEATH, 234 .out.err = -EXDEV, .pass = false }, 235 { .name = "[beneath] symlink temporary jump outside $root", 236 .path = "cheeky/self", .how.resolve = RESOLVE_BENEATH, 237 .out.err = -EXDEV, .pass = false }, 238 { .name = "[beneath] chained symlink temporary jump outside $root", 239 .path = "abscheeky/self", .how.resolve = RESOLVE_BENEATH, 240 .out.err = -EXDEV, .pass = false }, 241 { .name = "[beneath] garbage links to $root", 242 .path = "cheeky/garbageself", .how.resolve = RESOLVE_BENEATH, 243 .out.err = -EXDEV, .pass = false }, 244 { .name = "[beneath] chained garbage links to $root", 245 .path = "abscheeky/garbageself", .how.resolve = RESOLVE_BENEATH, 246 .out.err = -EXDEV, .pass = false }, 247 /* Only relative paths that stay inside dirfd should work. */ 248 { .name = "[beneath] ordinary path to 'root'", 249 .path = "root", .how.resolve = RESOLVE_BENEATH, 250 .out.path = "root", .pass = true }, 251 { .name = "[beneath] ordinary path to 'etc'", 252 .path = "etc", .how.resolve = RESOLVE_BENEATH, 253 .out.path = "etc", .pass = true }, 254 { .name = "[beneath] ordinary path to 'etc/passwd'", 255 .path = "etc/passwd", .how.resolve = RESOLVE_BENEATH, 256 .out.path = "etc/passwd", .pass = true }, 257 { .name = "[beneath] relative symlink inside $root", 258 .path = "relsym", .how.resolve = RESOLVE_BENEATH, 259 .out.path = "etc/passwd", .pass = true }, 260 { .name = "[beneath] chained-'..' relative symlink inside $root", 261 .path = "cheeky/passwd", .how.resolve = RESOLVE_BENEATH, 262 .out.path = "etc/passwd", .pass = true }, 263 { .name = "[beneath] absolute symlink component outside $root", 264 .path = "abscheeky/passwd", .how.resolve = RESOLVE_BENEATH, 265 .out.err = -EXDEV, .pass = false }, 266 { .name = "[beneath] absolute symlink target outside $root", 267 .path = "abssym", .how.resolve = RESOLVE_BENEATH, 268 .out.err = -EXDEV, .pass = false }, 269 { .name = "[beneath] absolute path outside $root", 270 .path = "/etc/passwd", .how.resolve = RESOLVE_BENEATH, 271 .out.err = -EXDEV, .pass = false }, 272 { .name = "[beneath] cheeky absolute path outside $root", 273 .path = "cheeky/abspasswd", .how.resolve = RESOLVE_BENEATH, 274 .out.err = -EXDEV, .pass = false }, 275 { .name = "[beneath] chained cheeky absolute path outside $root", 276 .path = "abscheeky/abspasswd", .how.resolve = RESOLVE_BENEATH, 277 .out.err = -EXDEV, .pass = false }, 278 /* Tricky paths should fail. */ 279 { .name = "[beneath] tricky '..'-chained symlink outside $root", 280 .path = "cheeky/dotdotlink", .how.resolve = RESOLVE_BENEATH, 281 .out.err = -EXDEV, .pass = false }, 282 { .name = "[beneath] tricky absolute + '..'-chained symlink outside $root", 283 .path = "abscheeky/dotdotlink", .how.resolve = RESOLVE_BENEATH, 284 .out.err = -EXDEV, .pass = false }, 285 { .name = "[beneath] tricky garbage link outside $root", 286 .path = "cheeky/garbagelink", .how.resolve = RESOLVE_BENEATH, 287 .out.err = -EXDEV, .pass = false }, 288 { .name = "[beneath] tricky absolute + garbage link outside $root", 289 .path = "abscheeky/garbagelink", .how.resolve = RESOLVE_BENEATH, 290 .out.err = -EXDEV, .pass = false }, 291 }; 292 293 for (int i = 0; i < ARRAY_SIZE(tests); i++) 294 verify_resolve_test(_metadata, self->rootfd, 295 self->hardcoded_fd, &tests[i]); 296 } 297 298 /* All attempts to cross the dirfd will be scoped-to-root. */ 299 TEST_F(openat2_resolve, resolve_in_root) 300 { 301 struct resolve_test tests[] = { 302 { .name = "[in_root] jump to /", 303 .path = "/", .how.resolve = RESOLVE_IN_ROOT, 304 .out.path = NULL, .pass = true }, 305 { .name = "[in_root] absolute symlink to /root", 306 .path = "cheeky/absself", .how.resolve = RESOLVE_IN_ROOT, 307 .out.path = NULL, .pass = true }, 308 { .name = "[in_root] chained absolute symlinks to /root", 309 .path = "abscheeky/absself", .how.resolve = RESOLVE_IN_ROOT, 310 .out.path = NULL, .pass = true }, 311 { .name = "[in_root] '..' at root", 312 .path = "..", .how.resolve = RESOLVE_IN_ROOT, 313 .out.path = NULL, .pass = true }, 314 { .name = "[in_root] '../root' at root", 315 .path = "../root/", .how.resolve = RESOLVE_IN_ROOT, 316 .out.path = "root", .pass = true }, 317 { .name = "[in_root] relative symlink containing '..' above root", 318 .path = "cheeky/self", .how.resolve = RESOLVE_IN_ROOT, 319 .out.path = "root", .pass = true }, 320 { .name = "[in_root] garbage link to /root", 321 .path = "cheeky/garbageself", .how.resolve = RESOLVE_IN_ROOT, 322 .out.path = "root", .pass = true }, 323 { .name = "[in_root] chained garbage links to /root", 324 .path = "abscheeky/garbageself", .how.resolve = RESOLVE_IN_ROOT, 325 .out.path = "root", .pass = true }, 326 { .name = "[in_root] relative path to 'root'", 327 .path = "root", .how.resolve = RESOLVE_IN_ROOT, 328 .out.path = "root", .pass = true }, 329 { .name = "[in_root] relative path to 'etc'", 330 .path = "etc", .how.resolve = RESOLVE_IN_ROOT, 331 .out.path = "etc", .pass = true }, 332 { .name = "[in_root] relative path to 'etc/passwd'", 333 .path = "etc/passwd", .how.resolve = RESOLVE_IN_ROOT, 334 .out.path = "etc/passwd", .pass = true }, 335 { .name = "[in_root] relative symlink to 'etc/passwd'", 336 .path = "relsym", .how.resolve = RESOLVE_IN_ROOT, 337 .out.path = "etc/passwd", .pass = true }, 338 { .name = "[in_root] chained-'..' relative symlink to 'etc/passwd'", 339 .path = "cheeky/passwd", .how.resolve = RESOLVE_IN_ROOT, 340 .out.path = "etc/passwd", .pass = true }, 341 { .name = "[in_root] chained-'..' absolute + relative symlink to 'etc/passwd'", 342 .path = "abscheeky/passwd", .how.resolve = RESOLVE_IN_ROOT, 343 .out.path = "etc/passwd", .pass = true }, 344 { .name = "[in_root] absolute symlink to 'etc/passwd'", 345 .path = "abssym", .how.resolve = RESOLVE_IN_ROOT, 346 .out.path = "etc/passwd", .pass = true }, 347 { .name = "[in_root] absolute path 'etc/passwd'", 348 .path = "/etc/passwd", .how.resolve = RESOLVE_IN_ROOT, 349 .out.path = "etc/passwd", .pass = true }, 350 { .name = "[in_root] cheeky absolute path 'etc/passwd'", 351 .path = "cheeky/abspasswd", .how.resolve = RESOLVE_IN_ROOT, 352 .out.path = "etc/passwd", .pass = true }, 353 { .name = "[in_root] chained cheeky absolute path 'etc/passwd'", 354 .path = "abscheeky/abspasswd", .how.resolve = RESOLVE_IN_ROOT, 355 .out.path = "etc/passwd", .pass = true }, 356 { .name = "[in_root] tricky '..'-chained symlink outside $root", 357 .path = "cheeky/dotdotlink", .how.resolve = RESOLVE_IN_ROOT, 358 .out.path = "etc/passwd", .pass = true }, 359 { .name = "[in_root] tricky absolute + '..'-chained symlink outside $root", 360 .path = "abscheeky/dotdotlink", .how.resolve = RESOLVE_IN_ROOT, 361 .out.path = "etc/passwd", .pass = true }, 362 { .name = "[in_root] tricky absolute path + absolute + '..'-chained symlink outside $root", 363 .path = "/../../../../abscheeky/dotdotlink", .how.resolve = RESOLVE_IN_ROOT, 364 .out.path = "etc/passwd", .pass = true }, 365 { .name = "[in_root] tricky garbage link outside $root", 366 .path = "cheeky/garbagelink", .how.resolve = RESOLVE_IN_ROOT, 367 .out.path = "etc/passwd", .pass = true }, 368 { .name = "[in_root] tricky absolute + garbage link outside $root", 369 .path = "abscheeky/garbagelink", .how.resolve = RESOLVE_IN_ROOT, 370 .out.path = "etc/passwd", .pass = true }, 371 { .name = "[in_root] tricky absolute path + absolute + garbage link outside $root", 372 .path = "/../../../../abscheeky/garbagelink", .how.resolve = RESOLVE_IN_ROOT, 373 .out.path = "etc/passwd", .pass = true }, 374 /* O_CREAT should handle trailing symlinks correctly. */ 375 { .name = "[in_root] O_CREAT of relative path inside $root", 376 .path = "newfile1", .how.flags = O_CREAT, 377 .how.mode = 0700, 378 .how.resolve = RESOLVE_IN_ROOT, 379 .out.path = "newfile1", .pass = true }, 380 { .name = "[in_root] O_CREAT of absolute path", 381 .path = "/newfile2", .how.flags = O_CREAT, 382 .how.mode = 0700, 383 .how.resolve = RESOLVE_IN_ROOT, 384 .out.path = "newfile2", .pass = true }, 385 { .name = "[in_root] O_CREAT of tricky symlink outside root", 386 .path = "/creatlink", .how.flags = O_CREAT, 387 .how.mode = 0700, 388 .how.resolve = RESOLVE_IN_ROOT, 389 .out.path = "newfile3", .pass = true }, 390 }; 391 392 for (int i = 0; i < ARRAY_SIZE(tests); i++) 393 verify_resolve_test(_metadata, self->rootfd, 394 self->hardcoded_fd, &tests[i]); 395 } 396 397 /* Crossing mount boundaries should be blocked. */ 398 TEST_F(openat2_resolve, resolve_no_xdev) 399 { 400 struct resolve_test tests[] = { 401 /* Crossing *down* into a mountpoint is disallowed. */ 402 { .name = "[no_xdev] cross into $mnt", 403 .path = "mnt", .how.resolve = RESOLVE_NO_XDEV, 404 .out.err = -EXDEV, .pass = false }, 405 { .name = "[no_xdev] cross into $mnt/", 406 .path = "mnt/", .how.resolve = RESOLVE_NO_XDEV, 407 .out.err = -EXDEV, .pass = false }, 408 { .name = "[no_xdev] cross into $mnt/.", 409 .path = "mnt/.", .how.resolve = RESOLVE_NO_XDEV, 410 .out.err = -EXDEV, .pass = false }, 411 /* Crossing *up* out of a mountpoint is disallowed. */ 412 { .name = "[no_xdev] goto mountpoint root", 413 .dir = "mnt", .path = ".", .how.resolve = RESOLVE_NO_XDEV, 414 .out.path = "mnt", .pass = true }, 415 { .name = "[no_xdev] cross up through '..'", 416 .dir = "mnt", .path = "..", .how.resolve = RESOLVE_NO_XDEV, 417 .out.err = -EXDEV, .pass = false }, 418 { .name = "[no_xdev] temporary cross up through '..'", 419 .dir = "mnt", .path = "../mnt", .how.resolve = RESOLVE_NO_XDEV, 420 .out.err = -EXDEV, .pass = false }, 421 { .name = "[no_xdev] temporary relative symlink cross up", 422 .dir = "mnt", .path = "self", .how.resolve = RESOLVE_NO_XDEV, 423 .out.err = -EXDEV, .pass = false }, 424 { .name = "[no_xdev] temporary absolute symlink cross up", 425 .dir = "mnt", .path = "absself", .how.resolve = RESOLVE_NO_XDEV, 426 .out.err = -EXDEV, .pass = false }, 427 /* Jumping to "/" is ok, but later components cannot cross. */ 428 { .name = "[no_xdev] jump to / directly", 429 .dir = "mnt", .path = "/", .how.resolve = RESOLVE_NO_XDEV, 430 .out.path = "/", .pass = true }, 431 { .name = "[no_xdev] jump to / (from /) directly", 432 .dir = "/", .path = "/", .how.resolve = RESOLVE_NO_XDEV, 433 .out.path = "/", .pass = true }, 434 { .name = "[no_xdev] jump to / then proc", 435 .path = "/proc/1", .how.resolve = RESOLVE_NO_XDEV, 436 .out.err = -EXDEV, .pass = false }, 437 { .name = "[no_xdev] jump to / then tmp", 438 .path = "/tmp", .how.resolve = RESOLVE_NO_XDEV, 439 .out.err = -EXDEV, .pass = false }, 440 /* Magic-links are blocked since they can switch vfsmounts. */ 441 { .name = "[no_xdev] cross through magic-link to self/root", 442 .dir = "/proc", .path = "self/root", .how.resolve = RESOLVE_NO_XDEV, 443 .out.err = -EXDEV, .pass = false }, 444 { .name = "[no_xdev] cross through magic-link to self/cwd", 445 .dir = "/proc", .path = "self/cwd", .how.resolve = RESOLVE_NO_XDEV, 446 .out.err = -EXDEV, .pass = false }, 447 /* Except magic-link jumps inside the same vfsmount. */ 448 { .name = "[no_xdev] jump through magic-link to same procfs", 449 .dir = "/proc", .path = self->hardcoded_fdpath, .how.resolve = RESOLVE_NO_XDEV, 450 .out.path = "/proc", .pass = true, }, 451 }; 452 453 for (int i = 0; i < ARRAY_SIZE(tests); i++) 454 verify_resolve_test(_metadata, self->rootfd, 455 self->hardcoded_fd, &tests[i]); 456 } 457 458 /* Procfs-style magic-link resolution should be blocked. */ 459 TEST_F(openat2_resolve, resolve_no_magiclinks) 460 { 461 struct resolve_test tests[] = { 462 /* Regular symlinks should work. */ 463 { .name = "[no_magiclinks] ordinary relative symlink", 464 .path = "relsym", .how.resolve = RESOLVE_NO_MAGICLINKS, 465 .out.path = "etc/passwd", .pass = true }, 466 /* Magic-links should not work. */ 467 { .name = "[no_magiclinks] symlink to magic-link", 468 .path = "procexe", .how.resolve = RESOLVE_NO_MAGICLINKS, 469 .out.err = -ELOOP, .pass = false }, 470 { .name = "[no_magiclinks] normal path to magic-link", 471 .path = "/proc/self/exe", .how.resolve = RESOLVE_NO_MAGICLINKS, 472 .out.err = -ELOOP, .pass = false }, 473 { .name = "[no_magiclinks] normal path to magic-link with O_NOFOLLOW", 474 .path = "/proc/self/exe", .how.flags = O_NOFOLLOW, 475 .how.resolve = RESOLVE_NO_MAGICLINKS, 476 .out.path = self->procselfexe, .pass = true }, 477 { .name = "[no_magiclinks] symlink to magic-link path component", 478 .path = "procroot/etc", .how.resolve = RESOLVE_NO_MAGICLINKS, 479 .out.err = -ELOOP, .pass = false }, 480 { .name = "[no_magiclinks] magic-link path component", 481 .path = "/proc/self/root/etc", .how.resolve = RESOLVE_NO_MAGICLINKS, 482 .out.err = -ELOOP, .pass = false }, 483 { .name = "[no_magiclinks] magic-link path component with O_NOFOLLOW", 484 .path = "/proc/self/root/etc", .how.flags = O_NOFOLLOW, 485 .how.resolve = RESOLVE_NO_MAGICLINKS, 486 .out.err = -ELOOP, .pass = false }, 487 }; 488 489 for (int i = 0; i < ARRAY_SIZE(tests); i++) 490 verify_resolve_test(_metadata, self->rootfd, 491 self->hardcoded_fd, &tests[i]); 492 } 493 494 /* All symlink resolution should be blocked. */ 495 TEST_F(openat2_resolve, resolve_no_symlinks) 496 { 497 struct resolve_test tests[] = { 498 /* Normal paths should work. */ 499 { .name = "[no_symlinks] ordinary path to '.'", 500 .path = ".", .how.resolve = RESOLVE_NO_SYMLINKS, 501 .out.path = NULL, .pass = true }, 502 { .name = "[no_symlinks] ordinary path to 'root'", 503 .path = "root", .how.resolve = RESOLVE_NO_SYMLINKS, 504 .out.path = "root", .pass = true }, 505 { .name = "[no_symlinks] ordinary path to 'etc'", 506 .path = "etc", .how.resolve = RESOLVE_NO_SYMLINKS, 507 .out.path = "etc", .pass = true }, 508 { .name = "[no_symlinks] ordinary path to 'etc/passwd'", 509 .path = "etc/passwd", .how.resolve = RESOLVE_NO_SYMLINKS, 510 .out.path = "etc/passwd", .pass = true }, 511 /* Regular symlinks are blocked. */ 512 { .name = "[no_symlinks] relative symlink target", 513 .path = "relsym", .how.resolve = RESOLVE_NO_SYMLINKS, 514 .out.err = -ELOOP, .pass = false }, 515 { .name = "[no_symlinks] relative symlink component", 516 .path = "reletc/passwd", .how.resolve = RESOLVE_NO_SYMLINKS, 517 .out.err = -ELOOP, .pass = false }, 518 { .name = "[no_symlinks] absolute symlink target", 519 .path = "abssym", .how.resolve = RESOLVE_NO_SYMLINKS, 520 .out.err = -ELOOP, .pass = false }, 521 { .name = "[no_symlinks] absolute symlink component", 522 .path = "absetc/passwd", .how.resolve = RESOLVE_NO_SYMLINKS, 523 .out.err = -ELOOP, .pass = false }, 524 { .name = "[no_symlinks] cheeky garbage link", 525 .path = "cheeky/garbagelink", .how.resolve = RESOLVE_NO_SYMLINKS, 526 .out.err = -ELOOP, .pass = false }, 527 { .name = "[no_symlinks] cheeky absolute + garbage link", 528 .path = "abscheeky/garbagelink", .how.resolve = RESOLVE_NO_SYMLINKS, 529 .out.err = -ELOOP, .pass = false }, 530 { .name = "[no_symlinks] cheeky absolute + absolute symlink", 531 .path = "abscheeky/absself", .how.resolve = RESOLVE_NO_SYMLINKS, 532 .out.err = -ELOOP, .pass = false }, 533 /* Trailing symlinks with NO_FOLLOW. */ 534 { .name = "[no_symlinks] relative symlink with O_NOFOLLOW", 535 .path = "relsym", .how.flags = O_NOFOLLOW, 536 .how.resolve = RESOLVE_NO_SYMLINKS, 537 .out.path = "relsym", .pass = true }, 538 { .name = "[no_symlinks] absolute symlink with O_NOFOLLOW", 539 .path = "abssym", .how.flags = O_NOFOLLOW, 540 .how.resolve = RESOLVE_NO_SYMLINKS, 541 .out.path = "abssym", .pass = true }, 542 { .name = "[no_symlinks] trailing symlink with O_NOFOLLOW", 543 .path = "cheeky/garbagelink", .how.flags = O_NOFOLLOW, 544 .how.resolve = RESOLVE_NO_SYMLINKS, 545 .out.path = "cheeky/garbagelink", .pass = true }, 546 { .name = "[no_symlinks] multiple symlink components with O_NOFOLLOW", 547 .path = "abscheeky/absself", .how.flags = O_NOFOLLOW, 548 .how.resolve = RESOLVE_NO_SYMLINKS, 549 .out.err = -ELOOP, .pass = false }, 550 { .name = "[no_symlinks] multiple symlink (and garbage link) components with O_NOFOLLOW", 551 .path = "abscheeky/garbagelink", .how.flags = O_NOFOLLOW, 552 .how.resolve = RESOLVE_NO_SYMLINKS, 553 .out.err = -ELOOP, .pass = false }, 554 }; 555 556 for (int i = 0; i < ARRAY_SIZE(tests); i++) 557 verify_resolve_test(_metadata, self->rootfd, 558 self->hardcoded_fd, &tests[i]); 559 } 560 561 TEST_HARNESS_MAIN 562