Merge tag 'zynqmp-soc-for-v5.7' of https://github.com/Xilinx/linux-xlnx into arm/soc
[linux-2.6-microblaze.git] / tools / testing / selftests / openat2 / resolve_test.c
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 }