1// SPDX-License-Identifier: GPL-2.0-or-later
2/*
3 * Author: Aleksa Sarai <cyphar@cyphar.com>
4 * Copyright (C) 2018-2019 SUSE LLC.
5 */
6
7#define _GNU_SOURCE
8#include <fcntl.h>
9#include <sched.h>
10#include <sys/stat.h>
11#include <sys/types.h>
12#include <sys/mount.h>
13#include <stdlib.h>
14#include <stdbool.h>
15#include <string.h>
16
17#include "../kselftest.h"
18#include "helpers.h"
19
20/*
21 * Construct a test directory with the following structure:
22 *
23 * root/
24 * |-- procexe -> /proc/self/exe
25 * |-- procroot -> /proc/self/root
26 * |-- root/
27 * |-- mnt/ [mountpoint]
28 * |   |-- self -> ../mnt/
29 * |   `-- absself -> /mnt/
30 * |-- etc/
31 * |   `-- passwd
32 * |-- creatlink -> /newfile3
33 * |-- reletc -> etc/
34 * |-- relsym -> etc/passwd
35 * |-- absetc -> /etc/
36 * |-- abssym -> /etc/passwd
37 * |-- abscheeky -> /cheeky
38 * `-- cheeky/
39 *     |-- absself -> /
40 *     |-- self -> ../../root/
41 *     |-- garbageself -> /../../root/
42 *     |-- passwd -> ../cheeky/../cheeky/../etc/../etc/passwd
43 *     |-- abspasswd -> /../cheeky/../cheeky/../etc/../etc/passwd
44 *     |-- dotdotlink -> ../../../../../../../../../../../../../../etc/passwd
45 *     `-- garbagelink -> /../../../../../../../../../../../../../../etc/passwd
46 */
47int setup_testdir(void)
48{
49	int dfd, tmpfd;
50	char dirname[] = "/tmp/ksft-openat2-testdir.XXXXXX";
51
52	/* Unshare and make /tmp a new directory. */
53	E_unshare(CLONE_NEWNS);
54	E_mount("", "/tmp", "", MS_PRIVATE, "");
55
56	/* Make the top-level directory. */
57	if (!mkdtemp(dirname))
58		ksft_exit_fail_msg("setup_testdir: failed to create tmpdir\n");
59	dfd = open(dirname, O_PATH | O_DIRECTORY);
60	if (dfd < 0)
61		ksft_exit_fail_msg("setup_testdir: failed to open tmpdir\n");
62
63	/* A sub-directory which is actually used for tests. */
64	E_mkdirat(dfd, "root", 0755);
65	tmpfd = openat(dfd, "root", O_PATH | O_DIRECTORY);
66	if (tmpfd < 0)
67		ksft_exit_fail_msg("setup_testdir: failed to open tmpdir\n");
68	close(dfd);
69	dfd = tmpfd;
70
71	E_symlinkat("/proc/self/exe", dfd, "procexe");
72	E_symlinkat("/proc/self/root", dfd, "procroot");
73	E_mkdirat(dfd, "root", 0755);
74
75	/* There is no mountat(2), so use chdir. */
76	E_mkdirat(dfd, "mnt", 0755);
77	E_fchdir(dfd);
78	E_mount("tmpfs", "./mnt", "tmpfs", MS_NOSUID | MS_NODEV, "");
79	E_symlinkat("../mnt/", dfd, "mnt/self");
80	E_symlinkat("/mnt/", dfd, "mnt/absself");
81
82	E_mkdirat(dfd, "etc", 0755);
83	E_touchat(dfd, "etc/passwd");
84
85	E_symlinkat("/newfile3", dfd, "creatlink");
86	E_symlinkat("etc/", dfd, "reletc");
87	E_symlinkat("etc/passwd", dfd, "relsym");
88	E_symlinkat("/etc/", dfd, "absetc");
89	E_symlinkat("/etc/passwd", dfd, "abssym");
90	E_symlinkat("/cheeky", dfd, "abscheeky");
91
92	E_mkdirat(dfd, "cheeky", 0755);
93
94	E_symlinkat("/", dfd, "cheeky/absself");
95	E_symlinkat("../../root/", dfd, "cheeky/self");
96	E_symlinkat("/../../root/", dfd, "cheeky/garbageself");
97
98	E_symlinkat("../cheeky/../etc/../etc/passwd", dfd, "cheeky/passwd");
99	E_symlinkat("/../cheeky/../etc/../etc/passwd", dfd, "cheeky/abspasswd");
100
101	E_symlinkat("../../../../../../../../../../../../../../etc/passwd",
102		    dfd, "cheeky/dotdotlink");
103	E_symlinkat("/../../../../../../../../../../../../../../etc/passwd",
104		    dfd, "cheeky/garbagelink");
105
106	return dfd;
107}
108
109struct basic_test {
110	const char *name;
111	const char *dir;
112	const char *path;
113	struct open_how how;
114	bool pass;
115	union {
116		int err;
117		const char *path;
118	} out;
119};
120
121#define NUM_OPENAT2_OPATH_TESTS 88
122
123void test_openat2_opath_tests(void)
124{
125	int rootfd, hardcoded_fd;
126	char *procselfexe, *hardcoded_fdpath;
127
128	E_asprintf(&procselfexe, "/proc/%d/exe", getpid());
129	rootfd = setup_testdir();
130
131	hardcoded_fd = open("/dev/null", O_RDONLY);
132	E_assert(hardcoded_fd >= 0, "open fd to hardcode");
133	E_asprintf(&hardcoded_fdpath, "self/fd/%d", hardcoded_fd);
134
135	struct basic_test tests[] = {
136		/** RESOLVE_BENEATH **/
137		/* Attempts to cross dirfd should be blocked. */
138		{ .name = "[beneath] jump to /",
139		  .path = "/",			.how.resolve = RESOLVE_BENEATH,
140		  .out.err = -EXDEV,		.pass = false },
141		{ .name = "[beneath] absolute link to $root",
142		  .path = "cheeky/absself",	.how.resolve = RESOLVE_BENEATH,
143		  .out.err = -EXDEV,		.pass = false },
144		{ .name = "[beneath] chained absolute links to $root",
145		  .path = "abscheeky/absself",	.how.resolve = RESOLVE_BENEATH,
146		  .out.err = -EXDEV,		.pass = false },
147		{ .name = "[beneath] jump outside $root",
148		  .path = "..",			.how.resolve = RESOLVE_BENEATH,
149		  .out.err = -EXDEV,		.pass = false },
150		{ .name = "[beneath] temporary jump outside $root",
151		  .path = "../root/",		.how.resolve = RESOLVE_BENEATH,
152		  .out.err = -EXDEV,		.pass = false },
153		{ .name = "[beneath] symlink temporary jump outside $root",
154		  .path = "cheeky/self",	.how.resolve = RESOLVE_BENEATH,
155		  .out.err = -EXDEV,		.pass = false },
156		{ .name = "[beneath] chained symlink temporary jump outside $root",
157		  .path = "abscheeky/self",	.how.resolve = RESOLVE_BENEATH,
158		  .out.err = -EXDEV,		.pass = false },
159		{ .name = "[beneath] garbage links to $root",
160		  .path = "cheeky/garbageself",	.how.resolve = RESOLVE_BENEATH,
161		  .out.err = -EXDEV,		.pass = false },
162		{ .name = "[beneath] chained garbage links to $root",
163		  .path = "abscheeky/garbageself", .how.resolve = RESOLVE_BENEATH,
164		  .out.err = -EXDEV,		.pass = false },
165		/* Only relative paths that stay inside dirfd should work. */
166		{ .name = "[beneath] ordinary path to 'root'",
167		  .path = "root",		.how.resolve = RESOLVE_BENEATH,
168		  .out.path = "root",		.pass = true },
169		{ .name = "[beneath] ordinary path to 'etc'",
170		  .path = "etc",		.how.resolve = RESOLVE_BENEATH,
171		  .out.path = "etc",		.pass = true },
172		{ .name = "[beneath] ordinary path to 'etc/passwd'",
173		  .path = "etc/passwd",		.how.resolve = RESOLVE_BENEATH,
174		  .out.path = "etc/passwd",	.pass = true },
175		{ .name = "[beneath] relative symlink inside $root",
176		  .path = "relsym",		.how.resolve = RESOLVE_BENEATH,
177		  .out.path = "etc/passwd",	.pass = true },
178		{ .name = "[beneath] chained-'..' relative symlink inside $root",
179		  .path = "cheeky/passwd",	.how.resolve = RESOLVE_BENEATH,
180		  .out.path = "etc/passwd",	.pass = true },
181		{ .name = "[beneath] absolute symlink component outside $root",
182		  .path = "abscheeky/passwd",	.how.resolve = RESOLVE_BENEATH,
183		  .out.err = -EXDEV,		.pass = false },
184		{ .name = "[beneath] absolute symlink target outside $root",
185		  .path = "abssym",		.how.resolve = RESOLVE_BENEATH,
186		  .out.err = -EXDEV,		.pass = false },
187		{ .name = "[beneath] absolute path outside $root",
188		  .path = "/etc/passwd",	.how.resolve = RESOLVE_BENEATH,
189		  .out.err = -EXDEV,		.pass = false },
190		{ .name = "[beneath] cheeky absolute path outside $root",
191		  .path = "cheeky/abspasswd",	.how.resolve = RESOLVE_BENEATH,
192		  .out.err = -EXDEV,		.pass = false },
193		{ .name = "[beneath] chained cheeky absolute path outside $root",
194		  .path = "abscheeky/abspasswd", .how.resolve = RESOLVE_BENEATH,
195		  .out.err = -EXDEV,		.pass = false },
196		/* Tricky paths should fail. */
197		{ .name = "[beneath] tricky '..'-chained symlink outside $root",
198		  .path = "cheeky/dotdotlink",	.how.resolve = RESOLVE_BENEATH,
199		  .out.err = -EXDEV,		.pass = false },
200		{ .name = "[beneath] tricky absolute + '..'-chained symlink outside $root",
201		  .path = "abscheeky/dotdotlink", .how.resolve = RESOLVE_BENEATH,
202		  .out.err = -EXDEV,		.pass = false },
203		{ .name = "[beneath] tricky garbage link outside $root",
204		  .path = "cheeky/garbagelink",	.how.resolve = RESOLVE_BENEATH,
205		  .out.err = -EXDEV,		.pass = false },
206		{ .name = "[beneath] tricky absolute + garbage link outside $root",
207		  .path = "abscheeky/garbagelink", .how.resolve = RESOLVE_BENEATH,
208		  .out.err = -EXDEV,		.pass = false },
209
210		/** RESOLVE_IN_ROOT **/
211		/* All attempts to cross the dirfd will be scoped-to-root. */
212		{ .name = "[in_root] jump to /",
213		  .path = "/",			.how.resolve = RESOLVE_IN_ROOT,
214		  .out.path = NULL,		.pass = true },
215		{ .name = "[in_root] absolute symlink to /root",
216		  .path = "cheeky/absself",	.how.resolve = RESOLVE_IN_ROOT,
217		  .out.path = NULL,		.pass = true },
218		{ .name = "[in_root] chained absolute symlinks to /root",
219		  .path = "abscheeky/absself",	.how.resolve = RESOLVE_IN_ROOT,
220		  .out.path = NULL,		.pass = true },
221		{ .name = "[in_root] '..' at root",
222		  .path = "..",			.how.resolve = RESOLVE_IN_ROOT,
223		  .out.path = NULL,		.pass = true },
224		{ .name = "[in_root] '../root' at root",
225		  .path = "../root/",		.how.resolve = RESOLVE_IN_ROOT,
226		  .out.path = "root",		.pass = true },
227		{ .name = "[in_root] relative symlink containing '..' above root",
228		  .path = "cheeky/self",	.how.resolve = RESOLVE_IN_ROOT,
229		  .out.path = "root",		.pass = true },
230		{ .name = "[in_root] garbage link to /root",
231		  .path = "cheeky/garbageself",	.how.resolve = RESOLVE_IN_ROOT,
232		  .out.path = "root",		.pass = true },
233		{ .name = "[in_root] chained garbage links to /root",
234		  .path = "abscheeky/garbageself", .how.resolve = RESOLVE_IN_ROOT,
235		  .out.path = "root",		.pass = true },
236		{ .name = "[in_root] relative path to 'root'",
237		  .path = "root",		.how.resolve = RESOLVE_IN_ROOT,
238		  .out.path = "root",		.pass = true },
239		{ .name = "[in_root] relative path to 'etc'",
240		  .path = "etc",		.how.resolve = RESOLVE_IN_ROOT,
241		  .out.path = "etc",		.pass = true },
242		{ .name = "[in_root] relative path to 'etc/passwd'",
243		  .path = "etc/passwd",		.how.resolve = RESOLVE_IN_ROOT,
244		  .out.path = "etc/passwd",	.pass = true },
245		{ .name = "[in_root] relative symlink to 'etc/passwd'",
246		  .path = "relsym",		.how.resolve = RESOLVE_IN_ROOT,
247		  .out.path = "etc/passwd",	.pass = true },
248		{ .name = "[in_root] chained-'..' relative symlink to 'etc/passwd'",
249		  .path = "cheeky/passwd",	.how.resolve = RESOLVE_IN_ROOT,
250		  .out.path = "etc/passwd",	.pass = true },
251		{ .name = "[in_root] chained-'..' absolute + relative symlink to 'etc/passwd'",
252		  .path = "abscheeky/passwd",	.how.resolve = RESOLVE_IN_ROOT,
253		  .out.path = "etc/passwd",	.pass = true },
254		{ .name = "[in_root] absolute symlink to 'etc/passwd'",
255		  .path = "abssym",		.how.resolve = RESOLVE_IN_ROOT,
256		  .out.path = "etc/passwd",	.pass = true },
257		{ .name = "[in_root] absolute path 'etc/passwd'",
258		  .path = "/etc/passwd",	.how.resolve = RESOLVE_IN_ROOT,
259		  .out.path = "etc/passwd",	.pass = true },
260		{ .name = "[in_root] cheeky absolute path 'etc/passwd'",
261		  .path = "cheeky/abspasswd",	.how.resolve = RESOLVE_IN_ROOT,
262		  .out.path = "etc/passwd",	.pass = true },
263		{ .name = "[in_root] chained cheeky absolute path 'etc/passwd'",
264		  .path = "abscheeky/abspasswd", .how.resolve = RESOLVE_IN_ROOT,
265		  .out.path = "etc/passwd",	.pass = true },
266		{ .name = "[in_root] tricky '..'-chained symlink outside $root",
267		  .path = "cheeky/dotdotlink",	.how.resolve = RESOLVE_IN_ROOT,
268		  .out.path = "etc/passwd",	.pass = true },
269		{ .name = "[in_root] tricky absolute + '..'-chained symlink outside $root",
270		  .path = "abscheeky/dotdotlink", .how.resolve = RESOLVE_IN_ROOT,
271		  .out.path = "etc/passwd",	.pass = true },
272		{ .name = "[in_root] tricky absolute path + absolute + '..'-chained symlink outside $root",
273		  .path = "/../../../../abscheeky/dotdotlink", .how.resolve = RESOLVE_IN_ROOT,
274		  .out.path = "etc/passwd",	.pass = true },
275		{ .name = "[in_root] tricky garbage link outside $root",
276		  .path = "cheeky/garbagelink",	.how.resolve = RESOLVE_IN_ROOT,
277		  .out.path = "etc/passwd",	.pass = true },
278		{ .name = "[in_root] tricky absolute + garbage link outside $root",
279		  .path = "abscheeky/garbagelink", .how.resolve = RESOLVE_IN_ROOT,
280		  .out.path = "etc/passwd",	.pass = true },
281		{ .name = "[in_root] tricky absolute path + absolute + garbage link outside $root",
282		  .path = "/../../../../abscheeky/garbagelink", .how.resolve = RESOLVE_IN_ROOT,
283		  .out.path = "etc/passwd",	.pass = true },
284		/* O_CREAT should handle trailing symlinks correctly. */
285		{ .name = "[in_root] O_CREAT of relative path inside $root",
286		  .path = "newfile1",		.how.flags = O_CREAT,
287						.how.mode = 0700,
288						.how.resolve = RESOLVE_IN_ROOT,
289		  .out.path = "newfile1",	.pass = true },
290		{ .name = "[in_root] O_CREAT of absolute path",
291		  .path = "/newfile2",		.how.flags = O_CREAT,
292						.how.mode = 0700,
293						.how.resolve = RESOLVE_IN_ROOT,
294		  .out.path = "newfile2",	.pass = true },
295		{ .name = "[in_root] O_CREAT of tricky symlink outside root",
296		  .path = "/creatlink",		.how.flags = O_CREAT,
297						.how.mode = 0700,
298						.how.resolve = RESOLVE_IN_ROOT,
299		  .out.path = "newfile3",	.pass = true },
300
301		/** RESOLVE_NO_XDEV **/
302		/* Crossing *down* into a mountpoint is disallowed. */
303		{ .name = "[no_xdev] cross into $mnt",
304		  .path = "mnt",		.how.resolve = RESOLVE_NO_XDEV,
305		  .out.err = -EXDEV,		.pass = false },
306		{ .name = "[no_xdev] cross into $mnt/",
307		  .path = "mnt/",		.how.resolve = RESOLVE_NO_XDEV,
308		  .out.err = -EXDEV,		.pass = false },
309		{ .name = "[no_xdev] cross into $mnt/.",
310		  .path = "mnt/.",		.how.resolve = RESOLVE_NO_XDEV,
311		  .out.err = -EXDEV,		.pass = false },
312		/* Crossing *up* out of a mountpoint is disallowed. */
313		{ .name = "[no_xdev] goto mountpoint root",
314		  .dir = "mnt", .path = ".",	.how.resolve = RESOLVE_NO_XDEV,
315		  .out.path = "mnt",		.pass = true },
316		{ .name = "[no_xdev] cross up through '..'",
317		  .dir = "mnt", .path = "..",	.how.resolve = RESOLVE_NO_XDEV,
318		  .out.err = -EXDEV,		.pass = false },
319		{ .name = "[no_xdev] temporary cross up through '..'",
320		  .dir = "mnt", .path = "../mnt", .how.resolve = RESOLVE_NO_XDEV,
321		  .out.err = -EXDEV,		.pass = false },
322		{ .name = "[no_xdev] temporary relative symlink cross up",
323		  .dir = "mnt", .path = "self",	.how.resolve = RESOLVE_NO_XDEV,
324		  .out.err = -EXDEV,		.pass = false },
325		{ .name = "[no_xdev] temporary absolute symlink cross up",
326		  .dir = "mnt", .path = "absself", .how.resolve = RESOLVE_NO_XDEV,
327		  .out.err = -EXDEV,		.pass = false },
328		/* Jumping to "/" is ok, but later components cannot cross. */
329		{ .name = "[no_xdev] jump to / directly",
330		  .dir = "mnt", .path = "/",	.how.resolve = RESOLVE_NO_XDEV,
331		  .out.path = "/",		.pass = true },
332		{ .name = "[no_xdev] jump to / (from /) directly",
333		  .dir = "/", .path = "/",	.how.resolve = RESOLVE_NO_XDEV,
334		  .out.path = "/",		.pass = true },
335		{ .name = "[no_xdev] jump to / then proc",
336		  .path = "/proc/1",		.how.resolve = RESOLVE_NO_XDEV,
337		  .out.err = -EXDEV,		.pass = false },
338		{ .name = "[no_xdev] jump to / then tmp",
339		  .path = "/tmp",		.how.resolve = RESOLVE_NO_XDEV,
340		  .out.err = -EXDEV,		.pass = false },
341		/* Magic-links are blocked since they can switch vfsmounts. */
342		{ .name = "[no_xdev] cross through magic-link to self/root",
343		  .dir = "/proc", .path = "self/root", 	.how.resolve = RESOLVE_NO_XDEV,
344		  .out.err = -EXDEV,			.pass = false },
345		{ .name = "[no_xdev] cross through magic-link to self/cwd",
346		  .dir = "/proc", .path = "self/cwd",	.how.resolve = RESOLVE_NO_XDEV,
347		  .out.err = -EXDEV,			.pass = false },
348		/* Except magic-link jumps inside the same vfsmount. */
349		{ .name = "[no_xdev] jump through magic-link to same procfs",
350		  .dir = "/proc", .path = hardcoded_fdpath, .how.resolve = RESOLVE_NO_XDEV,
351		  .out.path = "/proc",			    .pass = true, },
352
353		/** RESOLVE_NO_MAGICLINKS **/
354		/* Regular symlinks should work. */
355		{ .name = "[no_magiclinks] ordinary relative symlink",
356		  .path = "relsym",		.how.resolve = RESOLVE_NO_MAGICLINKS,
357		  .out.path = "etc/passwd",	.pass = true },
358		/* Magic-links should not work. */
359		{ .name = "[no_magiclinks] symlink to magic-link",
360		  .path = "procexe",		.how.resolve = RESOLVE_NO_MAGICLINKS,
361		  .out.err = -ELOOP,		.pass = false },
362		{ .name = "[no_magiclinks] normal path to magic-link",
363		  .path = "/proc/self/exe",	.how.resolve = RESOLVE_NO_MAGICLINKS,
364		  .out.err = -ELOOP,		.pass = false },
365		{ .name = "[no_magiclinks] normal path to magic-link with O_NOFOLLOW",
366		  .path = "/proc/self/exe",	.how.flags = O_NOFOLLOW,
367						.how.resolve = RESOLVE_NO_MAGICLINKS,
368		  .out.path = procselfexe,	.pass = true },
369		{ .name = "[no_magiclinks] symlink to magic-link path component",
370		  .path = "procroot/etc",	.how.resolve = RESOLVE_NO_MAGICLINKS,
371		  .out.err = -ELOOP,		.pass = false },
372		{ .name = "[no_magiclinks] magic-link path component",
373		  .path = "/proc/self/root/etc", .how.resolve = RESOLVE_NO_MAGICLINKS,
374		  .out.err = -ELOOP,		.pass = false },
375		{ .name = "[no_magiclinks] magic-link path component with O_NOFOLLOW",
376		  .path = "/proc/self/root/etc", .how.flags = O_NOFOLLOW,
377						 .how.resolve = RESOLVE_NO_MAGICLINKS,
378		  .out.err = -ELOOP,		.pass = false },
379
380		/** RESOLVE_NO_SYMLINKS **/
381		/* Normal paths should work. */
382		{ .name = "[no_symlinks] ordinary path to '.'",
383		  .path = ".",			.how.resolve = RESOLVE_NO_SYMLINKS,
384		  .out.path = NULL,		.pass = true },
385		{ .name = "[no_symlinks] ordinary path to 'root'",
386		  .path = "root",		.how.resolve = RESOLVE_NO_SYMLINKS,
387		  .out.path = "root",		.pass = true },
388		{ .name = "[no_symlinks] ordinary path to 'etc'",
389		  .path = "etc",		.how.resolve = RESOLVE_NO_SYMLINKS,
390		  .out.path = "etc",		.pass = true },
391		{ .name = "[no_symlinks] ordinary path to 'etc/passwd'",
392		  .path = "etc/passwd",		.how.resolve = RESOLVE_NO_SYMLINKS,
393		  .out.path = "etc/passwd",	.pass = true },
394		/* Regular symlinks are blocked. */
395		{ .name = "[no_symlinks] relative symlink target",
396		  .path = "relsym",		.how.resolve = RESOLVE_NO_SYMLINKS,
397		  .out.err = -ELOOP,		.pass = false },
398		{ .name = "[no_symlinks] relative symlink component",
399		  .path = "reletc/passwd",	.how.resolve = RESOLVE_NO_SYMLINKS,
400		  .out.err = -ELOOP,		.pass = false },
401		{ .name = "[no_symlinks] absolute symlink target",
402		  .path = "abssym",		.how.resolve = RESOLVE_NO_SYMLINKS,
403		  .out.err = -ELOOP,		.pass = false },
404		{ .name = "[no_symlinks] absolute symlink component",
405		  .path = "absetc/passwd",	.how.resolve = RESOLVE_NO_SYMLINKS,
406		  .out.err = -ELOOP,		.pass = false },
407		{ .name = "[no_symlinks] cheeky garbage link",
408		  .path = "cheeky/garbagelink",	.how.resolve = RESOLVE_NO_SYMLINKS,
409		  .out.err = -ELOOP,		.pass = false },
410		{ .name = "[no_symlinks] cheeky absolute + garbage link",
411		  .path = "abscheeky/garbagelink", .how.resolve = RESOLVE_NO_SYMLINKS,
412		  .out.err = -ELOOP,		.pass = false },
413		{ .name = "[no_symlinks] cheeky absolute + absolute symlink",
414		  .path = "abscheeky/absself",	.how.resolve = RESOLVE_NO_SYMLINKS,
415		  .out.err = -ELOOP,		.pass = false },
416		/* Trailing symlinks with NO_FOLLOW. */
417		{ .name = "[no_symlinks] relative symlink with O_NOFOLLOW",
418		  .path = "relsym",		.how.flags = O_NOFOLLOW,
419						.how.resolve = RESOLVE_NO_SYMLINKS,
420		  .out.path = "relsym",		.pass = true },
421		{ .name = "[no_symlinks] absolute symlink with O_NOFOLLOW",
422		  .path = "abssym",		.how.flags = O_NOFOLLOW,
423						.how.resolve = RESOLVE_NO_SYMLINKS,
424		  .out.path = "abssym",		.pass = true },
425		{ .name = "[no_symlinks] trailing symlink with O_NOFOLLOW",
426		  .path = "cheeky/garbagelink",	.how.flags = O_NOFOLLOW,
427						.how.resolve = RESOLVE_NO_SYMLINKS,
428		  .out.path = "cheeky/garbagelink", .pass = true },
429		{ .name = "[no_symlinks] multiple symlink components with O_NOFOLLOW",
430		  .path = "abscheeky/absself",	.how.flags = O_NOFOLLOW,
431						.how.resolve = RESOLVE_NO_SYMLINKS,
432		  .out.err = -ELOOP,		.pass = false },
433		{ .name = "[no_symlinks] multiple symlink (and garbage link) components with O_NOFOLLOW",
434		  .path = "abscheeky/garbagelink", .how.flags = O_NOFOLLOW,
435						   .how.resolve = RESOLVE_NO_SYMLINKS,
436		  .out.err = -ELOOP,		.pass = false },
437	};
438
439	BUILD_BUG_ON(ARRAY_LEN(tests) != NUM_OPENAT2_OPATH_TESTS);
440
441	for (int i = 0; i < ARRAY_LEN(tests); i++) {
442		int dfd, fd;
443		char *fdpath = NULL;
444		bool failed;
445		void (*resultfn)(const char *msg, ...) = ksft_test_result_pass;
446		struct basic_test *test = &tests[i];
447
448		if (!openat2_supported) {
449			ksft_print_msg("openat2(2) unsupported\n");
450			resultfn = ksft_test_result_skip;
451			goto skip;
452		}
453
454		/* Auto-set O_PATH. */
455		if (!(test->how.flags & O_CREAT))
456			test->how.flags |= O_PATH;
457
458		if (test->dir)
459			dfd = openat(rootfd, test->dir, O_PATH | O_DIRECTORY);
460		else
461			dfd = dup(rootfd);
462		E_assert(dfd, "failed to openat root '%s': %m", test->dir);
463
464		E_dup2(dfd, hardcoded_fd);
465
466		fd = sys_openat2(dfd, test->path, &test->how);
467		if (test->pass)
468			failed = (fd < 0 || !fdequal(fd, rootfd, test->out.path));
469		else
470			failed = (fd != test->out.err);
471		if (fd >= 0) {
472			fdpath = fdreadlink(fd);
473			close(fd);
474		}
475		close(dfd);
476
477		if (failed) {
478			resultfn = ksft_test_result_fail;
479
480			ksft_print_msg("openat2 unexpectedly returned ");
481			if (fdpath)
482				ksft_print_msg("%d['%s']\n", fd, fdpath);
483			else
484				ksft_print_msg("%d (%s)\n", fd, strerror(-fd));
485		}
486
487skip:
488		if (test->pass)
489			resultfn("%s gives path '%s'\n", test->name,
490				 test->out.path ?: ".");
491		else
492			resultfn("%s fails with %d (%s)\n", test->name,
493				 test->out.err, strerror(-test->out.err));
494
495		fflush(stdout);
496		free(fdpath);
497	}
498
499	free(procselfexe);
500	close(rootfd);
501
502	free(hardcoded_fdpath);
503	close(hardcoded_fd);
504}
505
506#define NUM_TESTS NUM_OPENAT2_OPATH_TESTS
507
508int main(int argc, char **argv)
509{
510	ksft_print_header();
511	ksft_set_plan(NUM_TESTS);
512
513	/* NOTE: We should be checking for CAP_SYS_ADMIN here... */
514	if (geteuid() != 0)
515		ksft_exit_skip("all tests require euid == 0\n");
516
517	test_openat2_opath_tests();
518
519	if (ksft_get_fail_cnt() + ksft_get_error_cnt() > 0)
520		ksft_exit_fail();
521	else
522		ksft_exit_pass();
523}
524