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] chainged garbage links to /root",
234 		  .path = "abscheeky/garbageself", .how.resolve = RESOLVE_IN_ROOT,
235 		  .out.path = "root",		.pass = true },
236 		{ .name = "[in_root] relative path to 'root'",
237 		  .path = "root",		.how.resolve = RESOLVE_IN_ROOT,
238 		  .out.path = "root",		.pass = true },
239 		{ .name = "[in_root] relative path to 'etc'",
240 		  .path = "etc",		.how.resolve = RESOLVE_IN_ROOT,
241 		  .out.path = "etc",		.pass = true },
242 		{ .name = "[in_root] relative path to 'etc/passwd'",
243 		  .path = "etc/passwd",		.how.resolve = RESOLVE_IN_ROOT,
244 		  .out.path = "etc/passwd",	.pass = true },
245 		{ .name = "[in_root] relative symlink to 'etc/passwd'",
246 		  .path = "relsym",		.how.resolve = RESOLVE_IN_ROOT,
247 		  .out.path = "etc/passwd",	.pass = true },
248 		{ .name = "[in_root] chained-'..' relative symlink to 'etc/passwd'",
249 		  .path = "cheeky/passwd",	.how.resolve = RESOLVE_IN_ROOT,
250 		  .out.path = "etc/passwd",	.pass = true },
251 		{ .name = "[in_root] chained-'..' absolute + relative symlink to 'etc/passwd'",
252 		  .path = "abscheeky/passwd",	.how.resolve = RESOLVE_IN_ROOT,
253 		  .out.path = "etc/passwd",	.pass = true },
254 		{ .name = "[in_root] absolute symlink to 'etc/passwd'",
255 		  .path = "abssym",		.how.resolve = RESOLVE_IN_ROOT,
256 		  .out.path = "etc/passwd",	.pass = true },
257 		{ .name = "[in_root] absolute path 'etc/passwd'",
258 		  .path = "/etc/passwd",	.how.resolve = RESOLVE_IN_ROOT,
259 		  .out.path = "etc/passwd",	.pass = true },
260 		{ .name = "[in_root] cheeky absolute path 'etc/passwd'",
261 		  .path = "cheeky/abspasswd",	.how.resolve = RESOLVE_IN_ROOT,
262 		  .out.path = "etc/passwd",	.pass = true },
263 		{ .name = "[in_root] chained cheeky absolute path 'etc/passwd'",
264 		  .path = "abscheeky/abspasswd", .how.resolve = RESOLVE_IN_ROOT,
265 		  .out.path = "etc/passwd",	.pass = true },
266 		{ .name = "[in_root] tricky '..'-chained symlink outside $root",
267 		  .path = "cheeky/dotdotlink",	.how.resolve = RESOLVE_IN_ROOT,
268 		  .out.path = "etc/passwd",	.pass = true },
269 		{ .name = "[in_root] tricky absolute + '..'-chained symlink outside $root",
270 		  .path = "abscheeky/dotdotlink", .how.resolve = RESOLVE_IN_ROOT,
271 		  .out.path = "etc/passwd",	.pass = true },
272 		{ .name = "[in_root] tricky absolute path + absolute + '..'-chained symlink outside $root",
273 		  .path = "/../../../../abscheeky/dotdotlink", .how.resolve = RESOLVE_IN_ROOT,
274 		  .out.path = "etc/passwd",	.pass = true },
275 		{ .name = "[in_root] tricky garbage link outside $root",
276 		  .path = "cheeky/garbagelink",	.how.resolve = RESOLVE_IN_ROOT,
277 		  .out.path = "etc/passwd",	.pass = true },
278 		{ .name = "[in_root] tricky absolute + garbage link outside $root",
279 		  .path = "abscheeky/garbagelink", .how.resolve = RESOLVE_IN_ROOT,
280 		  .out.path = "etc/passwd",	.pass = true },
281 		{ .name = "[in_root] tricky absolute path + absolute + garbage link outside $root",
282 		  .path = "/../../../../abscheeky/garbagelink", .how.resolve = RESOLVE_IN_ROOT,
283 		  .out.path = "etc/passwd",	.pass = true },
284 		/* O_CREAT should handle trailing symlinks correctly. */
285 		{ .name = "[in_root] O_CREAT of relative path inside $root",
286 		  .path = "newfile1",		.how.flags = O_CREAT,
287 						.how.mode = 0700,
288 						.how.resolve = RESOLVE_IN_ROOT,
289 		  .out.path = "newfile1",	.pass = true },
290 		{ .name = "[in_root] O_CREAT of absolute path",
291 		  .path = "/newfile2",		.how.flags = O_CREAT,
292 						.how.mode = 0700,
293 						.how.resolve = RESOLVE_IN_ROOT,
294 		  .out.path = "newfile2",	.pass = true },
295 		{ .name = "[in_root] O_CREAT of tricky symlink outside root",
296 		  .path = "/creatlink",		.how.flags = O_CREAT,
297 						.how.mode = 0700,
298 						.how.resolve = RESOLVE_IN_ROOT,
299 		  .out.path = "newfile3",	.pass = true },
300 
301 		/** RESOLVE_NO_XDEV **/
302 		/* Crossing *down* into a mountpoint is disallowed. */
303 		{ .name = "[no_xdev] cross into $mnt",
304 		  .path = "mnt",		.how.resolve = RESOLVE_NO_XDEV,
305 		  .out.err = -EXDEV,		.pass = false },
306 		{ .name = "[no_xdev] cross into $mnt/",
307 		  .path = "mnt/",		.how.resolve = RESOLVE_NO_XDEV,
308 		  .out.err = -EXDEV,		.pass = false },
309 		{ .name = "[no_xdev] cross into $mnt/.",
310 		  .path = "mnt/.",		.how.resolve = RESOLVE_NO_XDEV,
311 		  .out.err = -EXDEV,		.pass = false },
312 		/* Crossing *up* out of a mountpoint is disallowed. */
313 		{ .name = "[no_xdev] goto mountpoint root",
314 		  .dir = "mnt", .path = ".",	.how.resolve = RESOLVE_NO_XDEV,
315 		  .out.path = "mnt",		.pass = true },
316 		{ .name = "[no_xdev] cross up through '..'",
317 		  .dir = "mnt", .path = "..",	.how.resolve = RESOLVE_NO_XDEV,
318 		  .out.err = -EXDEV,		.pass = false },
319 		{ .name = "[no_xdev] temporary cross up through '..'",
320 		  .dir = "mnt", .path = "../mnt", .how.resolve = RESOLVE_NO_XDEV,
321 		  .out.err = -EXDEV,		.pass = false },
322 		{ .name = "[no_xdev] temporary relative symlink cross up",
323 		  .dir = "mnt", .path = "self",	.how.resolve = RESOLVE_NO_XDEV,
324 		  .out.err = -EXDEV,		.pass = false },
325 		{ .name = "[no_xdev] temporary absolute symlink cross up",
326 		  .dir = "mnt", .path = "absself", .how.resolve = RESOLVE_NO_XDEV,
327 		  .out.err = -EXDEV,		.pass = false },
328 		/* Jumping to "/" is ok, but later components cannot cross. */
329 		{ .name = "[no_xdev] jump to / directly",
330 		  .dir = "mnt", .path = "/",	.how.resolve = RESOLVE_NO_XDEV,
331 		  .out.path = "/",		.pass = true },
332 		{ .name = "[no_xdev] jump to / (from /) directly",
333 		  .dir = "/", .path = "/",	.how.resolve = RESOLVE_NO_XDEV,
334 		  .out.path = "/",		.pass = true },
335 		{ .name = "[no_xdev] jump to / then proc",
336 		  .path = "/proc/1",		.how.resolve = RESOLVE_NO_XDEV,
337 		  .out.err = -EXDEV,		.pass = false },
338 		{ .name = "[no_xdev] jump to / then tmp",
339 		  .path = "/tmp",		.how.resolve = RESOLVE_NO_XDEV,
340 		  .out.err = -EXDEV,		.pass = false },
341 		/* Magic-links are blocked since they can switch vfsmounts. */
342 		{ .name = "[no_xdev] cross through magic-link to self/root",
343 		  .dir = "/proc", .path = "self/root", 	.how.resolve = RESOLVE_NO_XDEV,
344 		  .out.err = -EXDEV,			.pass = false },
345 		{ .name = "[no_xdev] cross through magic-link to self/cwd",
346 		  .dir = "/proc", .path = "self/cwd",	.how.resolve = RESOLVE_NO_XDEV,
347 		  .out.err = -EXDEV,			.pass = false },
348 		/* Except magic-link jumps inside the same vfsmount. */
349 		{ .name = "[no_xdev] jump through magic-link to same procfs",
350 		  .dir = "/proc", .path = hardcoded_fdpath, .how.resolve = RESOLVE_NO_XDEV,
351 		  .out.path = "/proc",			    .pass = true, },
352 
353 		/** RESOLVE_NO_MAGICLINKS **/
354 		/* Regular symlinks should work. */
355 		{ .name = "[no_magiclinks] ordinary relative symlink",
356 		  .path = "relsym",		.how.resolve = RESOLVE_NO_MAGICLINKS,
357 		  .out.path = "etc/passwd",	.pass = true },
358 		/* Magic-links should not work. */
359 		{ .name = "[no_magiclinks] symlink to magic-link",
360 		  .path = "procexe",		.how.resolve = RESOLVE_NO_MAGICLINKS,
361 		  .out.err = -ELOOP,		.pass = false },
362 		{ .name = "[no_magiclinks] normal path to magic-link",
363 		  .path = "/proc/self/exe",	.how.resolve = RESOLVE_NO_MAGICLINKS,
364 		  .out.err = -ELOOP,		.pass = false },
365 		{ .name = "[no_magiclinks] normal path to magic-link with O_NOFOLLOW",
366 		  .path = "/proc/self/exe",	.how.flags = O_NOFOLLOW,
367 						.how.resolve = RESOLVE_NO_MAGICLINKS,
368 		  .out.path = procselfexe,	.pass = true },
369 		{ .name = "[no_magiclinks] symlink to magic-link path component",
370 		  .path = "procroot/etc",	.how.resolve = RESOLVE_NO_MAGICLINKS,
371 		  .out.err = -ELOOP,		.pass = false },
372 		{ .name = "[no_magiclinks] magic-link path component",
373 		  .path = "/proc/self/root/etc", .how.resolve = RESOLVE_NO_MAGICLINKS,
374 		  .out.err = -ELOOP,		.pass = false },
375 		{ .name = "[no_magiclinks] magic-link path component with O_NOFOLLOW",
376 		  .path = "/proc/self/root/etc", .how.flags = O_NOFOLLOW,
377 						 .how.resolve = RESOLVE_NO_MAGICLINKS,
378 		  .out.err = -ELOOP,		.pass = false },
379 
380 		/** RESOLVE_NO_SYMLINKS **/
381 		/* Normal paths should work. */
382 		{ .name = "[no_symlinks] ordinary path to '.'",
383 		  .path = ".",			.how.resolve = RESOLVE_NO_SYMLINKS,
384 		  .out.path = NULL,		.pass = true },
385 		{ .name = "[no_symlinks] ordinary path to 'root'",
386 		  .path = "root",		.how.resolve = RESOLVE_NO_SYMLINKS,
387 		  .out.path = "root",		.pass = true },
388 		{ .name = "[no_symlinks] ordinary path to 'etc'",
389 		  .path = "etc",		.how.resolve = RESOLVE_NO_SYMLINKS,
390 		  .out.path = "etc",		.pass = true },
391 		{ .name = "[no_symlinks] ordinary path to 'etc/passwd'",
392 		  .path = "etc/passwd",		.how.resolve = RESOLVE_NO_SYMLINKS,
393 		  .out.path = "etc/passwd",	.pass = true },
394 		/* Regular symlinks are blocked. */
395 		{ .name = "[no_symlinks] relative symlink target",
396 		  .path = "relsym",		.how.resolve = RESOLVE_NO_SYMLINKS,
397 		  .out.err = -ELOOP,		.pass = false },
398 		{ .name = "[no_symlinks] relative symlink component",
399 		  .path = "reletc/passwd",	.how.resolve = RESOLVE_NO_SYMLINKS,
400 		  .out.err = -ELOOP,		.pass = false },
401 		{ .name = "[no_symlinks] absolute symlink target",
402 		  .path = "abssym",		.how.resolve = RESOLVE_NO_SYMLINKS,
403 		  .out.err = -ELOOP,		.pass = false },
404 		{ .name = "[no_symlinks] absolute symlink component",
405 		  .path = "absetc/passwd",	.how.resolve = RESOLVE_NO_SYMLINKS,
406 		  .out.err = -ELOOP,		.pass = false },
407 		{ .name = "[no_symlinks] cheeky garbage link",
408 		  .path = "cheeky/garbagelink",	.how.resolve = RESOLVE_NO_SYMLINKS,
409 		  .out.err = -ELOOP,		.pass = false },
410 		{ .name = "[no_symlinks] cheeky absolute + garbage link",
411 		  .path = "abscheeky/garbagelink", .how.resolve = RESOLVE_NO_SYMLINKS,
412 		  .out.err = -ELOOP,		.pass = false },
413 		{ .name = "[no_symlinks] cheeky absolute + absolute symlink",
414 		  .path = "abscheeky/absself",	.how.resolve = RESOLVE_NO_SYMLINKS,
415 		  .out.err = -ELOOP,		.pass = false },
416 		/* Trailing symlinks with NO_FOLLOW. */
417 		{ .name = "[no_symlinks] relative symlink with O_NOFOLLOW",
418 		  .path = "relsym",		.how.flags = O_NOFOLLOW,
419 						.how.resolve = RESOLVE_NO_SYMLINKS,
420 		  .out.path = "relsym",		.pass = true },
421 		{ .name = "[no_symlinks] absolute symlink with O_NOFOLLOW",
422 		  .path = "abssym",		.how.flags = O_NOFOLLOW,
423 						.how.resolve = RESOLVE_NO_SYMLINKS,
424 		  .out.path = "abssym",		.pass = true },
425 		{ .name = "[no_symlinks] trailing symlink with O_NOFOLLOW",
426 		  .path = "cheeky/garbagelink",	.how.flags = O_NOFOLLOW,
427 						.how.resolve = RESOLVE_NO_SYMLINKS,
428 		  .out.path = "cheeky/garbagelink", .pass = true },
429 		{ .name = "[no_symlinks] multiple symlink components with O_NOFOLLOW",
430 		  .path = "abscheeky/absself",	.how.flags = O_NOFOLLOW,
431 						.how.resolve = RESOLVE_NO_SYMLINKS,
432 		  .out.err = -ELOOP,		.pass = false },
433 		{ .name = "[no_symlinks] multiple symlink (and garbage link) components with O_NOFOLLOW",
434 		  .path = "abscheeky/garbagelink", .how.flags = O_NOFOLLOW,
435 						   .how.resolve = RESOLVE_NO_SYMLINKS,
436 		  .out.err = -ELOOP,		.pass = false },
437 	};
438 
439 	BUILD_BUG_ON(ARRAY_LEN(tests) != NUM_OPENAT2_OPATH_TESTS);
440 
441 	for (int i = 0; i < ARRAY_LEN(tests); i++) {
442 		int dfd, fd;
443 		char *fdpath = NULL;
444 		bool failed;
445 		void (*resultfn)(const char *msg, ...) = ksft_test_result_pass;
446 		struct basic_test *test = &tests[i];
447 
448 		if (!openat2_supported) {
449 			ksft_print_msg("openat2(2) unsupported\n");
450 			resultfn = ksft_test_result_skip;
451 			goto skip;
452 		}
453 
454 		/* Auto-set O_PATH. */
455 		if (!(test->how.flags & O_CREAT))
456 			test->how.flags |= O_PATH;
457 
458 		if (test->dir)
459 			dfd = openat(rootfd, test->dir, O_PATH | O_DIRECTORY);
460 		else
461 			dfd = dup(rootfd);
462 		E_assert(dfd, "failed to openat root '%s': %m", test->dir);
463 
464 		E_dup2(dfd, hardcoded_fd);
465 
466 		fd = sys_openat2(dfd, test->path, &test->how);
467 		if (test->pass)
468 			failed = (fd < 0 || !fdequal(fd, rootfd, test->out.path));
469 		else
470 			failed = (fd != test->out.err);
471 		if (fd >= 0) {
472 			fdpath = fdreadlink(fd);
473 			close(fd);
474 		}
475 		close(dfd);
476 
477 		if (failed) {
478 			resultfn = ksft_test_result_fail;
479 
480 			ksft_print_msg("openat2 unexpectedly returned ");
481 			if (fdpath)
482 				ksft_print_msg("%d['%s']\n", fd, fdpath);
483 			else
484 				ksft_print_msg("%d (%s)\n", fd, strerror(-fd));
485 		}
486 
487 skip:
488 		if (test->pass)
489 			resultfn("%s gives path '%s'\n", test->name,
490 				 test->out.path ?: ".");
491 		else
492 			resultfn("%s fails with %d (%s)\n", test->name,
493 				 test->out.err, strerror(-test->out.err));
494 
495 		fflush(stdout);
496 		free(fdpath);
497 	}
498 
499 	free(procselfexe);
500 	close(rootfd);
501 
502 	free(hardcoded_fdpath);
503 	close(hardcoded_fd);
504 }
505 
506 #define NUM_TESTS NUM_OPENAT2_OPATH_TESTS
507 
508 int main(int argc, char **argv)
509 {
510 	ksft_print_header();
511 	ksft_set_plan(NUM_TESTS);
512 
513 	/* NOTE: We should be checking for CAP_SYS_ADMIN here... */
514 	if (geteuid() != 0)
515 		ksft_exit_skip("all tests require euid == 0\n");
516 
517 	test_openat2_opath_tests();
518 
519 	if (ksft_get_fail_cnt() + ksft_get_error_cnt() > 0)
520 		ksft_exit_fail();
521 	else
522 		ksft_exit_pass();
523 }
524