cachepc-linux

Fork of AMDESE/linux with modifications for CachePC side-channel attack
git clone https://git.sinitax.com/sinitax/cachepc-linux
Log | Files | Refs | README | LICENSE | sfeed.txt

resolve_test.c (21258B)


      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}