linux/tools/testing/selftests/openat2/resolve_test.c
<<
>>
Prefs
   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 */
  47int 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
 109struct 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
 123void 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
 487skip:
 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
 508int 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