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