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