Back to home page

OSCL-LXR

 
 

    


0001 // SPDX-License-Identifier: GPL-2.0-or-later
0002 /*
0003  * Author: Aleksa Sarai <cyphar@cyphar.com>
0004  * Copyright (C) 2018-2019 SUSE LLC.
0005  */
0006 
0007 #define _GNU_SOURCE
0008 #include <fcntl.h>
0009 #include <sched.h>
0010 #include <sys/stat.h>
0011 #include <sys/types.h>
0012 #include <sys/mount.h>
0013 #include <stdlib.h>
0014 #include <stdbool.h>
0015 #include <string.h>
0016 
0017 #include "../kselftest.h"
0018 #include "helpers.h"
0019 
0020 /*
0021  * Construct a test directory with the following structure:
0022  *
0023  * root/
0024  * |-- procexe -> /proc/self/exe
0025  * |-- procroot -> /proc/self/root
0026  * |-- root/
0027  * |-- mnt/ [mountpoint]
0028  * |   |-- self -> ../mnt/
0029  * |   `-- absself -> /mnt/
0030  * |-- etc/
0031  * |   `-- passwd
0032  * |-- creatlink -> /newfile3
0033  * |-- reletc -> etc/
0034  * |-- relsym -> etc/passwd
0035  * |-- absetc -> /etc/
0036  * |-- abssym -> /etc/passwd
0037  * |-- abscheeky -> /cheeky
0038  * `-- cheeky/
0039  *     |-- absself -> /
0040  *     |-- self -> ../../root/
0041  *     |-- garbageself -> /../../root/
0042  *     |-- passwd -> ../cheeky/../cheeky/../etc/../etc/passwd
0043  *     |-- abspasswd -> /../cheeky/../cheeky/../etc/../etc/passwd
0044  *     |-- dotdotlink -> ../../../../../../../../../../../../../../etc/passwd
0045  *     `-- garbagelink -> /../../../../../../../../../../../../../../etc/passwd
0046  */
0047 int setup_testdir(void)
0048 {
0049     int dfd, tmpfd;
0050     char dirname[] = "/tmp/ksft-openat2-testdir.XXXXXX";
0051 
0052     /* Unshare and make /tmp a new directory. */
0053     E_unshare(CLONE_NEWNS);
0054     E_mount("", "/tmp", "", MS_PRIVATE, "");
0055 
0056     /* Make the top-level directory. */
0057     if (!mkdtemp(dirname))
0058         ksft_exit_fail_msg("setup_testdir: failed to create tmpdir\n");
0059     dfd = open(dirname, O_PATH | O_DIRECTORY);
0060     if (dfd < 0)
0061         ksft_exit_fail_msg("setup_testdir: failed to open tmpdir\n");
0062 
0063     /* A sub-directory which is actually used for tests. */
0064     E_mkdirat(dfd, "root", 0755);
0065     tmpfd = openat(dfd, "root", O_PATH | O_DIRECTORY);
0066     if (tmpfd < 0)
0067         ksft_exit_fail_msg("setup_testdir: failed to open tmpdir\n");
0068     close(dfd);
0069     dfd = tmpfd;
0070 
0071     E_symlinkat("/proc/self/exe", dfd, "procexe");
0072     E_symlinkat("/proc/self/root", dfd, "procroot");
0073     E_mkdirat(dfd, "root", 0755);
0074 
0075     /* There is no mountat(2), so use chdir. */
0076     E_mkdirat(dfd, "mnt", 0755);
0077     E_fchdir(dfd);
0078     E_mount("tmpfs", "./mnt", "tmpfs", MS_NOSUID | MS_NODEV, "");
0079     E_symlinkat("../mnt/", dfd, "mnt/self");
0080     E_symlinkat("/mnt/", dfd, "mnt/absself");
0081 
0082     E_mkdirat(dfd, "etc", 0755);
0083     E_touchat(dfd, "etc/passwd");
0084 
0085     E_symlinkat("/newfile3", dfd, "creatlink");
0086     E_symlinkat("etc/", dfd, "reletc");
0087     E_symlinkat("etc/passwd", dfd, "relsym");
0088     E_symlinkat("/etc/", dfd, "absetc");
0089     E_symlinkat("/etc/passwd", dfd, "abssym");
0090     E_symlinkat("/cheeky", dfd, "abscheeky");
0091 
0092     E_mkdirat(dfd, "cheeky", 0755);
0093 
0094     E_symlinkat("/", dfd, "cheeky/absself");
0095     E_symlinkat("../../root/", dfd, "cheeky/self");
0096     E_symlinkat("/../../root/", dfd, "cheeky/garbageself");
0097 
0098     E_symlinkat("../cheeky/../etc/../etc/passwd", dfd, "cheeky/passwd");
0099     E_symlinkat("/../cheeky/../etc/../etc/passwd", dfd, "cheeky/abspasswd");
0100 
0101     E_symlinkat("../../../../../../../../../../../../../../etc/passwd",
0102             dfd, "cheeky/dotdotlink");
0103     E_symlinkat("/../../../../../../../../../../../../../../etc/passwd",
0104             dfd, "cheeky/garbagelink");
0105 
0106     return dfd;
0107 }
0108 
0109 struct basic_test {
0110     const char *name;
0111     const char *dir;
0112     const char *path;
0113     struct open_how how;
0114     bool pass;
0115     union {
0116         int err;
0117         const char *path;
0118     } out;
0119 };
0120 
0121 #define NUM_OPENAT2_OPATH_TESTS 88
0122 
0123 void test_openat2_opath_tests(void)
0124 {
0125     int rootfd, hardcoded_fd;
0126     char *procselfexe, *hardcoded_fdpath;
0127 
0128     E_asprintf(&procselfexe, "/proc/%d/exe", getpid());
0129     rootfd = setup_testdir();
0130 
0131     hardcoded_fd = open("/dev/null", O_RDONLY);
0132     E_assert(hardcoded_fd >= 0, "open fd to hardcode");
0133     E_asprintf(&hardcoded_fdpath, "self/fd/%d", hardcoded_fd);
0134 
0135     struct basic_test tests[] = {
0136         /** RESOLVE_BENEATH **/
0137         /* Attempts to cross dirfd should be blocked. */
0138         { .name = "[beneath] jump to /",
0139           .path = "/",          .how.resolve = RESOLVE_BENEATH,
0140           .out.err = -EXDEV,        .pass = false },
0141         { .name = "[beneath] absolute link to $root",
0142           .path = "cheeky/absself", .how.resolve = RESOLVE_BENEATH,
0143           .out.err = -EXDEV,        .pass = false },
0144         { .name = "[beneath] chained absolute links to $root",
0145           .path = "abscheeky/absself",  .how.resolve = RESOLVE_BENEATH,
0146           .out.err = -EXDEV,        .pass = false },
0147         { .name = "[beneath] jump outside $root",
0148           .path = "..",         .how.resolve = RESOLVE_BENEATH,
0149           .out.err = -EXDEV,        .pass = false },
0150         { .name = "[beneath] temporary jump outside $root",
0151           .path = "../root/",       .how.resolve = RESOLVE_BENEATH,
0152           .out.err = -EXDEV,        .pass = false },
0153         { .name = "[beneath] symlink temporary jump outside $root",
0154           .path = "cheeky/self",    .how.resolve = RESOLVE_BENEATH,
0155           .out.err = -EXDEV,        .pass = false },
0156         { .name = "[beneath] chained symlink temporary jump outside $root",
0157           .path = "abscheeky/self", .how.resolve = RESOLVE_BENEATH,
0158           .out.err = -EXDEV,        .pass = false },
0159         { .name = "[beneath] garbage links to $root",
0160           .path = "cheeky/garbageself", .how.resolve = RESOLVE_BENEATH,
0161           .out.err = -EXDEV,        .pass = false },
0162         { .name = "[beneath] chained garbage links to $root",
0163           .path = "abscheeky/garbageself", .how.resolve = RESOLVE_BENEATH,
0164           .out.err = -EXDEV,        .pass = false },
0165         /* Only relative paths that stay inside dirfd should work. */
0166         { .name = "[beneath] ordinary path to 'root'",
0167           .path = "root",       .how.resolve = RESOLVE_BENEATH,
0168           .out.path = "root",       .pass = true },
0169         { .name = "[beneath] ordinary path to 'etc'",
0170           .path = "etc",        .how.resolve = RESOLVE_BENEATH,
0171           .out.path = "etc",        .pass = true },
0172         { .name = "[beneath] ordinary path to 'etc/passwd'",
0173           .path = "etc/passwd",     .how.resolve = RESOLVE_BENEATH,
0174           .out.path = "etc/passwd", .pass = true },
0175         { .name = "[beneath] relative symlink inside $root",
0176           .path = "relsym",     .how.resolve = RESOLVE_BENEATH,
0177           .out.path = "etc/passwd", .pass = true },
0178         { .name = "[beneath] chained-'..' relative symlink inside $root",
0179           .path = "cheeky/passwd",  .how.resolve = RESOLVE_BENEATH,
0180           .out.path = "etc/passwd", .pass = true },
0181         { .name = "[beneath] absolute symlink component outside $root",
0182           .path = "abscheeky/passwd",   .how.resolve = RESOLVE_BENEATH,
0183           .out.err = -EXDEV,        .pass = false },
0184         { .name = "[beneath] absolute symlink target outside $root",
0185           .path = "abssym",     .how.resolve = RESOLVE_BENEATH,
0186           .out.err = -EXDEV,        .pass = false },
0187         { .name = "[beneath] absolute path outside $root",
0188           .path = "/etc/passwd",    .how.resolve = RESOLVE_BENEATH,
0189           .out.err = -EXDEV,        .pass = false },
0190         { .name = "[beneath] cheeky absolute path outside $root",
0191           .path = "cheeky/abspasswd",   .how.resolve = RESOLVE_BENEATH,
0192           .out.err = -EXDEV,        .pass = false },
0193         { .name = "[beneath] chained cheeky absolute path outside $root",
0194           .path = "abscheeky/abspasswd", .how.resolve = RESOLVE_BENEATH,
0195           .out.err = -EXDEV,        .pass = false },
0196         /* Tricky paths should fail. */
0197         { .name = "[beneath] tricky '..'-chained symlink outside $root",
0198           .path = "cheeky/dotdotlink",  .how.resolve = RESOLVE_BENEATH,
0199           .out.err = -EXDEV,        .pass = false },
0200         { .name = "[beneath] tricky absolute + '..'-chained symlink outside $root",
0201           .path = "abscheeky/dotdotlink", .how.resolve = RESOLVE_BENEATH,
0202           .out.err = -EXDEV,        .pass = false },
0203         { .name = "[beneath] tricky garbage link outside $root",
0204           .path = "cheeky/garbagelink", .how.resolve = RESOLVE_BENEATH,
0205           .out.err = -EXDEV,        .pass = false },
0206         { .name = "[beneath] tricky absolute + garbage link outside $root",
0207           .path = "abscheeky/garbagelink", .how.resolve = RESOLVE_BENEATH,
0208           .out.err = -EXDEV,        .pass = false },
0209 
0210         /** RESOLVE_IN_ROOT **/
0211         /* All attempts to cross the dirfd will be scoped-to-root. */
0212         { .name = "[in_root] jump to /",
0213           .path = "/",          .how.resolve = RESOLVE_IN_ROOT,
0214           .out.path = NULL,     .pass = true },
0215         { .name = "[in_root] absolute symlink to /root",
0216           .path = "cheeky/absself", .how.resolve = RESOLVE_IN_ROOT,
0217           .out.path = NULL,     .pass = true },
0218         { .name = "[in_root] chained absolute symlinks to /root",
0219           .path = "abscheeky/absself",  .how.resolve = RESOLVE_IN_ROOT,
0220           .out.path = NULL,     .pass = true },
0221         { .name = "[in_root] '..' at root",
0222           .path = "..",         .how.resolve = RESOLVE_IN_ROOT,
0223           .out.path = NULL,     .pass = true },
0224         { .name = "[in_root] '../root' at root",
0225           .path = "../root/",       .how.resolve = RESOLVE_IN_ROOT,
0226           .out.path = "root",       .pass = true },
0227         { .name = "[in_root] relative symlink containing '..' above root",
0228           .path = "cheeky/self",    .how.resolve = RESOLVE_IN_ROOT,
0229           .out.path = "root",       .pass = true },
0230         { .name = "[in_root] garbage link to /root",
0231           .path = "cheeky/garbageself", .how.resolve = RESOLVE_IN_ROOT,
0232           .out.path = "root",       .pass = true },
0233         { .name = "[in_root] chained garbage links to /root",
0234           .path = "abscheeky/garbageself", .how.resolve = RESOLVE_IN_ROOT,
0235           .out.path = "root",       .pass = true },
0236         { .name = "[in_root] relative path to 'root'",
0237           .path = "root",       .how.resolve = RESOLVE_IN_ROOT,
0238           .out.path = "root",       .pass = true },
0239         { .name = "[in_root] relative path to 'etc'",
0240           .path = "etc",        .how.resolve = RESOLVE_IN_ROOT,
0241           .out.path = "etc",        .pass = true },
0242         { .name = "[in_root] relative path to 'etc/passwd'",
0243           .path = "etc/passwd",     .how.resolve = RESOLVE_IN_ROOT,
0244           .out.path = "etc/passwd", .pass = true },
0245         { .name = "[in_root] relative symlink to 'etc/passwd'",
0246           .path = "relsym",     .how.resolve = RESOLVE_IN_ROOT,
0247           .out.path = "etc/passwd", .pass = true },
0248         { .name = "[in_root] chained-'..' relative symlink to 'etc/passwd'",
0249           .path = "cheeky/passwd",  .how.resolve = RESOLVE_IN_ROOT,
0250           .out.path = "etc/passwd", .pass = true },
0251         { .name = "[in_root] chained-'..' absolute + relative symlink to 'etc/passwd'",
0252           .path = "abscheeky/passwd",   .how.resolve = RESOLVE_IN_ROOT,
0253           .out.path = "etc/passwd", .pass = true },
0254         { .name = "[in_root] absolute symlink to 'etc/passwd'",
0255           .path = "abssym",     .how.resolve = RESOLVE_IN_ROOT,
0256           .out.path = "etc/passwd", .pass = true },
0257         { .name = "[in_root] absolute path 'etc/passwd'",
0258           .path = "/etc/passwd",    .how.resolve = RESOLVE_IN_ROOT,
0259           .out.path = "etc/passwd", .pass = true },
0260         { .name = "[in_root] cheeky absolute path 'etc/passwd'",
0261           .path = "cheeky/abspasswd",   .how.resolve = RESOLVE_IN_ROOT,
0262           .out.path = "etc/passwd", .pass = true },
0263         { .name = "[in_root] chained cheeky absolute path 'etc/passwd'",
0264           .path = "abscheeky/abspasswd", .how.resolve = RESOLVE_IN_ROOT,
0265           .out.path = "etc/passwd", .pass = true },
0266         { .name = "[in_root] tricky '..'-chained symlink outside $root",
0267           .path = "cheeky/dotdotlink",  .how.resolve = RESOLVE_IN_ROOT,
0268           .out.path = "etc/passwd", .pass = true },
0269         { .name = "[in_root] tricky absolute + '..'-chained symlink outside $root",
0270           .path = "abscheeky/dotdotlink", .how.resolve = RESOLVE_IN_ROOT,
0271           .out.path = "etc/passwd", .pass = true },
0272         { .name = "[in_root] tricky absolute path + absolute + '..'-chained symlink outside $root",
0273           .path = "/../../../../abscheeky/dotdotlink", .how.resolve = RESOLVE_IN_ROOT,
0274           .out.path = "etc/passwd", .pass = true },
0275         { .name = "[in_root] tricky garbage link outside $root",
0276           .path = "cheeky/garbagelink", .how.resolve = RESOLVE_IN_ROOT,
0277           .out.path = "etc/passwd", .pass = true },
0278         { .name = "[in_root] tricky absolute + garbage link outside $root",
0279           .path = "abscheeky/garbagelink", .how.resolve = RESOLVE_IN_ROOT,
0280           .out.path = "etc/passwd", .pass = true },
0281         { .name = "[in_root] tricky absolute path + absolute + garbage link outside $root",
0282           .path = "/../../../../abscheeky/garbagelink", .how.resolve = RESOLVE_IN_ROOT,
0283           .out.path = "etc/passwd", .pass = true },
0284         /* O_CREAT should handle trailing symlinks correctly. */
0285         { .name = "[in_root] O_CREAT of relative path inside $root",
0286           .path = "newfile1",       .how.flags = O_CREAT,
0287                         .how.mode = 0700,
0288                         .how.resolve = RESOLVE_IN_ROOT,
0289           .out.path = "newfile1",   .pass = true },
0290         { .name = "[in_root] O_CREAT of absolute path",
0291           .path = "/newfile2",      .how.flags = O_CREAT,
0292                         .how.mode = 0700,
0293                         .how.resolve = RESOLVE_IN_ROOT,
0294           .out.path = "newfile2",   .pass = true },
0295         { .name = "[in_root] O_CREAT of tricky symlink outside root",
0296           .path = "/creatlink",     .how.flags = O_CREAT,
0297                         .how.mode = 0700,
0298                         .how.resolve = RESOLVE_IN_ROOT,
0299           .out.path = "newfile3",   .pass = true },
0300 
0301         /** RESOLVE_NO_XDEV **/
0302         /* Crossing *down* into a mountpoint is disallowed. */
0303         { .name = "[no_xdev] cross into $mnt",
0304           .path = "mnt",        .how.resolve = RESOLVE_NO_XDEV,
0305           .out.err = -EXDEV,        .pass = false },
0306         { .name = "[no_xdev] cross into $mnt/",
0307           .path = "mnt/",       .how.resolve = RESOLVE_NO_XDEV,
0308           .out.err = -EXDEV,        .pass = false },
0309         { .name = "[no_xdev] cross into $mnt/.",
0310           .path = "mnt/.",      .how.resolve = RESOLVE_NO_XDEV,
0311           .out.err = -EXDEV,        .pass = false },
0312         /* Crossing *up* out of a mountpoint is disallowed. */
0313         { .name = "[no_xdev] goto mountpoint root",
0314           .dir = "mnt", .path = ".",    .how.resolve = RESOLVE_NO_XDEV,
0315           .out.path = "mnt",        .pass = true },
0316         { .name = "[no_xdev] cross up through '..'",
0317           .dir = "mnt", .path = "..",   .how.resolve = RESOLVE_NO_XDEV,
0318           .out.err = -EXDEV,        .pass = false },
0319         { .name = "[no_xdev] temporary cross up through '..'",
0320           .dir = "mnt", .path = "../mnt", .how.resolve = RESOLVE_NO_XDEV,
0321           .out.err = -EXDEV,        .pass = false },
0322         { .name = "[no_xdev] temporary relative symlink cross up",
0323           .dir = "mnt", .path = "self", .how.resolve = RESOLVE_NO_XDEV,
0324           .out.err = -EXDEV,        .pass = false },
0325         { .name = "[no_xdev] temporary absolute symlink cross up",
0326           .dir = "mnt", .path = "absself", .how.resolve = RESOLVE_NO_XDEV,
0327           .out.err = -EXDEV,        .pass = false },
0328         /* Jumping to "/" is ok, but later components cannot cross. */
0329         { .name = "[no_xdev] jump to / directly",
0330           .dir = "mnt", .path = "/",    .how.resolve = RESOLVE_NO_XDEV,
0331           .out.path = "/",      .pass = true },
0332         { .name = "[no_xdev] jump to / (from /) directly",
0333           .dir = "/", .path = "/",  .how.resolve = RESOLVE_NO_XDEV,
0334           .out.path = "/",      .pass = true },
0335         { .name = "[no_xdev] jump to / then proc",
0336           .path = "/proc/1",        .how.resolve = RESOLVE_NO_XDEV,
0337           .out.err = -EXDEV,        .pass = false },
0338         { .name = "[no_xdev] jump to / then tmp",
0339           .path = "/tmp",       .how.resolve = RESOLVE_NO_XDEV,
0340           .out.err = -EXDEV,        .pass = false },
0341         /* Magic-links are blocked since they can switch vfsmounts. */
0342         { .name = "[no_xdev] cross through magic-link to self/root",
0343           .dir = "/proc", .path = "self/root",  .how.resolve = RESOLVE_NO_XDEV,
0344           .out.err = -EXDEV,            .pass = false },
0345         { .name = "[no_xdev] cross through magic-link to self/cwd",
0346           .dir = "/proc", .path = "self/cwd",   .how.resolve = RESOLVE_NO_XDEV,
0347           .out.err = -EXDEV,            .pass = false },
0348         /* Except magic-link jumps inside the same vfsmount. */
0349         { .name = "[no_xdev] jump through magic-link to same procfs",
0350           .dir = "/proc", .path = hardcoded_fdpath, .how.resolve = RESOLVE_NO_XDEV,
0351           .out.path = "/proc",              .pass = true, },
0352 
0353         /** RESOLVE_NO_MAGICLINKS **/
0354         /* Regular symlinks should work. */
0355         { .name = "[no_magiclinks] ordinary relative symlink",
0356           .path = "relsym",     .how.resolve = RESOLVE_NO_MAGICLINKS,
0357           .out.path = "etc/passwd", .pass = true },
0358         /* Magic-links should not work. */
0359         { .name = "[no_magiclinks] symlink to magic-link",
0360           .path = "procexe",        .how.resolve = RESOLVE_NO_MAGICLINKS,
0361           .out.err = -ELOOP,        .pass = false },
0362         { .name = "[no_magiclinks] normal path to magic-link",
0363           .path = "/proc/self/exe", .how.resolve = RESOLVE_NO_MAGICLINKS,
0364           .out.err = -ELOOP,        .pass = false },
0365         { .name = "[no_magiclinks] normal path to magic-link with O_NOFOLLOW",
0366           .path = "/proc/self/exe", .how.flags = O_NOFOLLOW,
0367                         .how.resolve = RESOLVE_NO_MAGICLINKS,
0368           .out.path = procselfexe,  .pass = true },
0369         { .name = "[no_magiclinks] symlink to magic-link path component",
0370           .path = "procroot/etc",   .how.resolve = RESOLVE_NO_MAGICLINKS,
0371           .out.err = -ELOOP,        .pass = false },
0372         { .name = "[no_magiclinks] magic-link path component",
0373           .path = "/proc/self/root/etc", .how.resolve = RESOLVE_NO_MAGICLINKS,
0374           .out.err = -ELOOP,        .pass = false },
0375         { .name = "[no_magiclinks] magic-link path component with O_NOFOLLOW",
0376           .path = "/proc/self/root/etc", .how.flags = O_NOFOLLOW,
0377                          .how.resolve = RESOLVE_NO_MAGICLINKS,
0378           .out.err = -ELOOP,        .pass = false },
0379 
0380         /** RESOLVE_NO_SYMLINKS **/
0381         /* Normal paths should work. */
0382         { .name = "[no_symlinks] ordinary path to '.'",
0383           .path = ".",          .how.resolve = RESOLVE_NO_SYMLINKS,
0384           .out.path = NULL,     .pass = true },
0385         { .name = "[no_symlinks] ordinary path to 'root'",
0386           .path = "root",       .how.resolve = RESOLVE_NO_SYMLINKS,
0387           .out.path = "root",       .pass = true },
0388         { .name = "[no_symlinks] ordinary path to 'etc'",
0389           .path = "etc",        .how.resolve = RESOLVE_NO_SYMLINKS,
0390           .out.path = "etc",        .pass = true },
0391         { .name = "[no_symlinks] ordinary path to 'etc/passwd'",
0392           .path = "etc/passwd",     .how.resolve = RESOLVE_NO_SYMLINKS,
0393           .out.path = "etc/passwd", .pass = true },
0394         /* Regular symlinks are blocked. */
0395         { .name = "[no_symlinks] relative symlink target",
0396           .path = "relsym",     .how.resolve = RESOLVE_NO_SYMLINKS,
0397           .out.err = -ELOOP,        .pass = false },
0398         { .name = "[no_symlinks] relative symlink component",
0399           .path = "reletc/passwd",  .how.resolve = RESOLVE_NO_SYMLINKS,
0400           .out.err = -ELOOP,        .pass = false },
0401         { .name = "[no_symlinks] absolute symlink target",
0402           .path = "abssym",     .how.resolve = RESOLVE_NO_SYMLINKS,
0403           .out.err = -ELOOP,        .pass = false },
0404         { .name = "[no_symlinks] absolute symlink component",
0405           .path = "absetc/passwd",  .how.resolve = RESOLVE_NO_SYMLINKS,
0406           .out.err = -ELOOP,        .pass = false },
0407         { .name = "[no_symlinks] cheeky garbage link",
0408           .path = "cheeky/garbagelink", .how.resolve = RESOLVE_NO_SYMLINKS,
0409           .out.err = -ELOOP,        .pass = false },
0410         { .name = "[no_symlinks] cheeky absolute + garbage link",
0411           .path = "abscheeky/garbagelink", .how.resolve = RESOLVE_NO_SYMLINKS,
0412           .out.err = -ELOOP,        .pass = false },
0413         { .name = "[no_symlinks] cheeky absolute + absolute symlink",
0414           .path = "abscheeky/absself",  .how.resolve = RESOLVE_NO_SYMLINKS,
0415           .out.err = -ELOOP,        .pass = false },
0416         /* Trailing symlinks with NO_FOLLOW. */
0417         { .name = "[no_symlinks] relative symlink with O_NOFOLLOW",
0418           .path = "relsym",     .how.flags = O_NOFOLLOW,
0419                         .how.resolve = RESOLVE_NO_SYMLINKS,
0420           .out.path = "relsym",     .pass = true },
0421         { .name = "[no_symlinks] absolute symlink with O_NOFOLLOW",
0422           .path = "abssym",     .how.flags = O_NOFOLLOW,
0423                         .how.resolve = RESOLVE_NO_SYMLINKS,
0424           .out.path = "abssym",     .pass = true },
0425         { .name = "[no_symlinks] trailing symlink with O_NOFOLLOW",
0426           .path = "cheeky/garbagelink", .how.flags = O_NOFOLLOW,
0427                         .how.resolve = RESOLVE_NO_SYMLINKS,
0428           .out.path = "cheeky/garbagelink", .pass = true },
0429         { .name = "[no_symlinks] multiple symlink components with O_NOFOLLOW",
0430           .path = "abscheeky/absself",  .how.flags = O_NOFOLLOW,
0431                         .how.resolve = RESOLVE_NO_SYMLINKS,
0432           .out.err = -ELOOP,        .pass = false },
0433         { .name = "[no_symlinks] multiple symlink (and garbage link) components with O_NOFOLLOW",
0434           .path = "abscheeky/garbagelink", .how.flags = O_NOFOLLOW,
0435                            .how.resolve = RESOLVE_NO_SYMLINKS,
0436           .out.err = -ELOOP,        .pass = false },
0437     };
0438 
0439     BUILD_BUG_ON(ARRAY_LEN(tests) != NUM_OPENAT2_OPATH_TESTS);
0440 
0441     for (int i = 0; i < ARRAY_LEN(tests); i++) {
0442         int dfd, fd;
0443         char *fdpath = NULL;
0444         bool failed;
0445         void (*resultfn)(const char *msg, ...) = ksft_test_result_pass;
0446         struct basic_test *test = &tests[i];
0447 
0448         if (!openat2_supported) {
0449             ksft_print_msg("openat2(2) unsupported\n");
0450             resultfn = ksft_test_result_skip;
0451             goto skip;
0452         }
0453 
0454         /* Auto-set O_PATH. */
0455         if (!(test->how.flags & O_CREAT))
0456             test->how.flags |= O_PATH;
0457 
0458         if (test->dir)
0459             dfd = openat(rootfd, test->dir, O_PATH | O_DIRECTORY);
0460         else
0461             dfd = dup(rootfd);
0462         E_assert(dfd, "failed to openat root '%s': %m", test->dir);
0463 
0464         E_dup2(dfd, hardcoded_fd);
0465 
0466         fd = sys_openat2(dfd, test->path, &test->how);
0467         if (test->pass)
0468             failed = (fd < 0 || !fdequal(fd, rootfd, test->out.path));
0469         else
0470             failed = (fd != test->out.err);
0471         if (fd >= 0) {
0472             fdpath = fdreadlink(fd);
0473             close(fd);
0474         }
0475         close(dfd);
0476 
0477         if (failed) {
0478             resultfn = ksft_test_result_fail;
0479 
0480             ksft_print_msg("openat2 unexpectedly returned ");
0481             if (fdpath)
0482                 ksft_print_msg("%d['%s']\n", fd, fdpath);
0483             else
0484                 ksft_print_msg("%d (%s)\n", fd, strerror(-fd));
0485         }
0486 
0487 skip:
0488         if (test->pass)
0489             resultfn("%s gives path '%s'\n", test->name,
0490                  test->out.path ?: ".");
0491         else
0492             resultfn("%s fails with %d (%s)\n", test->name,
0493                  test->out.err, strerror(-test->out.err));
0494 
0495         fflush(stdout);
0496         free(fdpath);
0497     }
0498 
0499     free(procselfexe);
0500     close(rootfd);
0501 
0502     free(hardcoded_fdpath);
0503     close(hardcoded_fd);
0504 }
0505 
0506 #define NUM_TESTS NUM_OPENAT2_OPATH_TESTS
0507 
0508 int main(int argc, char **argv)
0509 {
0510     ksft_print_header();
0511     ksft_set_plan(NUM_TESTS);
0512 
0513     /* NOTE: We should be checking for CAP_SYS_ADMIN here... */
0514     if (geteuid() != 0)
0515         ksft_exit_skip("all tests require euid == 0\n");
0516 
0517     test_openat2_opath_tests();
0518 
0519     if (ksft_get_fail_cnt() + ksft_get_error_cnt() > 0)
0520         ksft_exit_fail();
0521     else
0522         ksft_exit_pass();
0523 }