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 },
233*7714d469SColin 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