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