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 */
verify_resolve_test(struct __test_metadata * _metadata,int rootfd,int hardcoded_fd,const struct resolve_test * test)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 */
FIXTURE(openat2_resolve)120 FIXTURE(openat2_resolve) {
121 int rootfd;
122 int hardcoded_fd;
123 char *hardcoded_fdpath;
124 char *procselfexe;
125 };
126
FIXTURE_SETUP(openat2_resolve)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 the mount tree private. */
144 ASSERT_EQ(unshare(CLONE_NEWNS), 0);
145 ASSERT_EQ(mount("", "/", "", MS_PRIVATE | MS_REC, ""), 0);
146
147 /* Ensure /tmp is a mountpoint for RESOLVE_NO_XDEV test crossing into /tmp. */
148 ASSERT_EQ(mount("/tmp", "/tmp", NULL, MS_BIND, NULL), 0);
149
150 /* Make the top-level directory. */
151 ASSERT_NE(mkdtemp(dirname), NULL);
152 dfd = open(dirname, O_PATH | O_DIRECTORY);
153 ASSERT_GE(dfd, 0);
154
155 /* A sub-directory which is actually used for tests. */
156 ASSERT_EQ(mkdirat(dfd, "root", 0755), 0);
157 tmpfd = openat(dfd, "root", O_PATH | O_DIRECTORY);
158 ASSERT_GE(tmpfd, 0);
159 close(dfd);
160 dfd = tmpfd;
161
162 ASSERT_EQ(symlinkat("/proc/self/exe", dfd, "procexe"), 0);
163 ASSERT_EQ(symlinkat("/proc/self/root", dfd, "procroot"), 0);
164 ASSERT_EQ(mkdirat(dfd, "root", 0755), 0);
165
166 /* There is no mountat(2), so use chdir. */
167 ASSERT_EQ(mkdirat(dfd, "mnt", 0755), 0);
168 ASSERT_EQ(fchdir(dfd), 0);
169 ASSERT_EQ(mount("tmpfs", "./mnt", "tmpfs", MS_NOSUID | MS_NODEV, ""), 0);
170 ASSERT_EQ(symlinkat("../mnt/", dfd, "mnt/self"), 0);
171 ASSERT_EQ(symlinkat("/mnt/", dfd, "mnt/absself"), 0);
172
173 ASSERT_EQ(mkdirat(dfd, "etc", 0755), 0);
174 ASSERT_GE(touchat(dfd, "etc/passwd"), 0);
175
176 ASSERT_EQ(symlinkat("/newfile3", dfd, "creatlink"), 0);
177 ASSERT_EQ(symlinkat("etc/", dfd, "reletc"), 0);
178 ASSERT_EQ(symlinkat("etc/passwd", dfd, "relsym"), 0);
179 ASSERT_EQ(symlinkat("/etc/", dfd, "absetc"), 0);
180 ASSERT_EQ(symlinkat("/etc/passwd", dfd, "abssym"), 0);
181 ASSERT_EQ(symlinkat("/cheeky", dfd, "abscheeky"), 0);
182
183 ASSERT_EQ(mkdirat(dfd, "cheeky", 0755), 0);
184
185 ASSERT_EQ(symlinkat("/", dfd, "cheeky/absself"), 0);
186 ASSERT_EQ(symlinkat("../../root/", dfd, "cheeky/self"), 0);
187 ASSERT_EQ(symlinkat("/../../root/", dfd, "cheeky/garbageself"), 0);
188
189 ASSERT_EQ(symlinkat("../cheeky/../etc/../etc/passwd",
190 dfd, "cheeky/passwd"), 0);
191 ASSERT_EQ(symlinkat("/../cheeky/../etc/../etc/passwd",
192 dfd, "cheeky/abspasswd"), 0);
193
194 ASSERT_EQ(symlinkat("../../../../../../../../../../../../../../etc/passwd",
195 dfd, "cheeky/dotdotlink"), 0);
196 ASSERT_EQ(symlinkat("/../../../../../../../../../../../../../../etc/passwd",
197 dfd, "cheeky/garbagelink"), 0);
198
199 self->rootfd = dfd;
200
201 self->hardcoded_fd = open("/dev/null", O_RDONLY);
202 ASSERT_GE(self->hardcoded_fd, 0);
203 ASSERT_GE(asprintf(&self->hardcoded_fdpath, "self/fd/%d",
204 self->hardcoded_fd), 0);
205 ASSERT_GE(asprintf(&self->procselfexe, "/proc/%d/exe", getpid()), 0);
206 }
207
FIXTURE_TEARDOWN(openat2_resolve)208 FIXTURE_TEARDOWN(openat2_resolve)
209 {
210 free(self->procselfexe);
211 free(self->hardcoded_fdpath);
212 if (self->hardcoded_fd >= 0)
213 close(self->hardcoded_fd);
214 if (self->rootfd >= 0)
215 close(self->rootfd);
216 }
217
218 /* Attempts to cross the dirfd should be blocked with -EXDEV. */
TEST_F(openat2_resolve,resolve_beneath)219 TEST_F(openat2_resolve, resolve_beneath)
220 {
221 struct resolve_test tests[] = {
222 /* Attempts to cross dirfd should be blocked. */
223 { .name = "[beneath] jump to /",
224 .path = "/", .how.resolve = RESOLVE_BENEATH,
225 .out.err = -EXDEV, .pass = false },
226 { .name = "[beneath] absolute link to $root",
227 .path = "cheeky/absself", .how.resolve = RESOLVE_BENEATH,
228 .out.err = -EXDEV, .pass = false },
229 { .name = "[beneath] chained absolute links to $root",
230 .path = "abscheeky/absself", .how.resolve = RESOLVE_BENEATH,
231 .out.err = -EXDEV, .pass = false },
232 { .name = "[beneath] jump outside $root",
233 .path = "..", .how.resolve = RESOLVE_BENEATH,
234 .out.err = -EXDEV, .pass = false },
235 { .name = "[beneath] temporary jump outside $root",
236 .path = "../root/", .how.resolve = RESOLVE_BENEATH,
237 .out.err = -EXDEV, .pass = false },
238 { .name = "[beneath] symlink temporary jump outside $root",
239 .path = "cheeky/self", .how.resolve = RESOLVE_BENEATH,
240 .out.err = -EXDEV, .pass = false },
241 { .name = "[beneath] chained symlink temporary jump outside $root",
242 .path = "abscheeky/self", .how.resolve = RESOLVE_BENEATH,
243 .out.err = -EXDEV, .pass = false },
244 { .name = "[beneath] garbage links to $root",
245 .path = "cheeky/garbageself", .how.resolve = RESOLVE_BENEATH,
246 .out.err = -EXDEV, .pass = false },
247 { .name = "[beneath] chained garbage links to $root",
248 .path = "abscheeky/garbageself", .how.resolve = RESOLVE_BENEATH,
249 .out.err = -EXDEV, .pass = false },
250 /* Only relative paths that stay inside dirfd should work. */
251 { .name = "[beneath] ordinary path to 'root'",
252 .path = "root", .how.resolve = RESOLVE_BENEATH,
253 .out.path = "root", .pass = true },
254 { .name = "[beneath] ordinary path to 'etc'",
255 .path = "etc", .how.resolve = RESOLVE_BENEATH,
256 .out.path = "etc", .pass = true },
257 { .name = "[beneath] ordinary path to 'etc/passwd'",
258 .path = "etc/passwd", .how.resolve = RESOLVE_BENEATH,
259 .out.path = "etc/passwd", .pass = true },
260 { .name = "[beneath] relative symlink inside $root",
261 .path = "relsym", .how.resolve = RESOLVE_BENEATH,
262 .out.path = "etc/passwd", .pass = true },
263 { .name = "[beneath] chained-'..' relative symlink inside $root",
264 .path = "cheeky/passwd", .how.resolve = RESOLVE_BENEATH,
265 .out.path = "etc/passwd", .pass = true },
266 { .name = "[beneath] absolute symlink component outside $root",
267 .path = "abscheeky/passwd", .how.resolve = RESOLVE_BENEATH,
268 .out.err = -EXDEV, .pass = false },
269 { .name = "[beneath] absolute symlink target outside $root",
270 .path = "abssym", .how.resolve = RESOLVE_BENEATH,
271 .out.err = -EXDEV, .pass = false },
272 { .name = "[beneath] absolute path outside $root",
273 .path = "/etc/passwd", .how.resolve = RESOLVE_BENEATH,
274 .out.err = -EXDEV, .pass = false },
275 { .name = "[beneath] cheeky absolute path outside $root",
276 .path = "cheeky/abspasswd", .how.resolve = RESOLVE_BENEATH,
277 .out.err = -EXDEV, .pass = false },
278 { .name = "[beneath] chained cheeky absolute path outside $root",
279 .path = "abscheeky/abspasswd", .how.resolve = RESOLVE_BENEATH,
280 .out.err = -EXDEV, .pass = false },
281 /* Tricky paths should fail. */
282 { .name = "[beneath] tricky '..'-chained symlink outside $root",
283 .path = "cheeky/dotdotlink", .how.resolve = RESOLVE_BENEATH,
284 .out.err = -EXDEV, .pass = false },
285 { .name = "[beneath] tricky absolute + '..'-chained symlink outside $root",
286 .path = "abscheeky/dotdotlink", .how.resolve = RESOLVE_BENEATH,
287 .out.err = -EXDEV, .pass = false },
288 { .name = "[beneath] tricky garbage link outside $root",
289 .path = "cheeky/garbagelink", .how.resolve = RESOLVE_BENEATH,
290 .out.err = -EXDEV, .pass = false },
291 { .name = "[beneath] tricky absolute + garbage link outside $root",
292 .path = "abscheeky/garbagelink", .how.resolve = RESOLVE_BENEATH,
293 .out.err = -EXDEV, .pass = false },
294 };
295
296 for (int i = 0; i < ARRAY_SIZE(tests); i++)
297 verify_resolve_test(_metadata, self->rootfd,
298 self->hardcoded_fd, &tests[i]);
299 }
300
301 /* All attempts to cross the dirfd will be scoped-to-root. */
TEST_F(openat2_resolve,resolve_in_root)302 TEST_F(openat2_resolve, resolve_in_root)
303 {
304 struct resolve_test tests[] = {
305 { .name = "[in_root] jump to /",
306 .path = "/", .how.resolve = RESOLVE_IN_ROOT,
307 .out.path = NULL, .pass = true },
308 { .name = "[in_root] absolute symlink to /root",
309 .path = "cheeky/absself", .how.resolve = RESOLVE_IN_ROOT,
310 .out.path = NULL, .pass = true },
311 { .name = "[in_root] chained absolute symlinks to /root",
312 .path = "abscheeky/absself", .how.resolve = RESOLVE_IN_ROOT,
313 .out.path = NULL, .pass = true },
314 { .name = "[in_root] '..' at root",
315 .path = "..", .how.resolve = RESOLVE_IN_ROOT,
316 .out.path = NULL, .pass = true },
317 { .name = "[in_root] '../root' at root",
318 .path = "../root/", .how.resolve = RESOLVE_IN_ROOT,
319 .out.path = "root", .pass = true },
320 { .name = "[in_root] relative symlink containing '..' above root",
321 .path = "cheeky/self", .how.resolve = RESOLVE_IN_ROOT,
322 .out.path = "root", .pass = true },
323 { .name = "[in_root] garbage link to /root",
324 .path = "cheeky/garbageself", .how.resolve = RESOLVE_IN_ROOT,
325 .out.path = "root", .pass = true },
326 { .name = "[in_root] chained garbage links to /root",
327 .path = "abscheeky/garbageself", .how.resolve = RESOLVE_IN_ROOT,
328 .out.path = "root", .pass = true },
329 { .name = "[in_root] relative path to 'root'",
330 .path = "root", .how.resolve = RESOLVE_IN_ROOT,
331 .out.path = "root", .pass = true },
332 { .name = "[in_root] relative path to 'etc'",
333 .path = "etc", .how.resolve = RESOLVE_IN_ROOT,
334 .out.path = "etc", .pass = true },
335 { .name = "[in_root] relative path to 'etc/passwd'",
336 .path = "etc/passwd", .how.resolve = RESOLVE_IN_ROOT,
337 .out.path = "etc/passwd", .pass = true },
338 { .name = "[in_root] relative symlink to 'etc/passwd'",
339 .path = "relsym", .how.resolve = RESOLVE_IN_ROOT,
340 .out.path = "etc/passwd", .pass = true },
341 { .name = "[in_root] chained-'..' relative symlink to 'etc/passwd'",
342 .path = "cheeky/passwd", .how.resolve = RESOLVE_IN_ROOT,
343 .out.path = "etc/passwd", .pass = true },
344 { .name = "[in_root] chained-'..' absolute + relative symlink to 'etc/passwd'",
345 .path = "abscheeky/passwd", .how.resolve = RESOLVE_IN_ROOT,
346 .out.path = "etc/passwd", .pass = true },
347 { .name = "[in_root] absolute symlink to 'etc/passwd'",
348 .path = "abssym", .how.resolve = RESOLVE_IN_ROOT,
349 .out.path = "etc/passwd", .pass = true },
350 { .name = "[in_root] absolute path 'etc/passwd'",
351 .path = "/etc/passwd", .how.resolve = RESOLVE_IN_ROOT,
352 .out.path = "etc/passwd", .pass = true },
353 { .name = "[in_root] cheeky absolute path 'etc/passwd'",
354 .path = "cheeky/abspasswd", .how.resolve = RESOLVE_IN_ROOT,
355 .out.path = "etc/passwd", .pass = true },
356 { .name = "[in_root] chained cheeky absolute path 'etc/passwd'",
357 .path = "abscheeky/abspasswd", .how.resolve = RESOLVE_IN_ROOT,
358 .out.path = "etc/passwd", .pass = true },
359 { .name = "[in_root] tricky '..'-chained symlink outside $root",
360 .path = "cheeky/dotdotlink", .how.resolve = RESOLVE_IN_ROOT,
361 .out.path = "etc/passwd", .pass = true },
362 { .name = "[in_root] tricky 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 absolute path + absolute + '..'-chained symlink outside $root",
366 .path = "/../../../../abscheeky/dotdotlink", .how.resolve = RESOLVE_IN_ROOT,
367 .out.path = "etc/passwd", .pass = true },
368 { .name = "[in_root] tricky garbage link outside $root",
369 .path = "cheeky/garbagelink", .how.resolve = RESOLVE_IN_ROOT,
370 .out.path = "etc/passwd", .pass = true },
371 { .name = "[in_root] tricky absolute + garbage link outside $root",
372 .path = "abscheeky/garbagelink", .how.resolve = RESOLVE_IN_ROOT,
373 .out.path = "etc/passwd", .pass = true },
374 { .name = "[in_root] tricky absolute path + absolute + garbage link outside $root",
375 .path = "/../../../../abscheeky/garbagelink", .how.resolve = RESOLVE_IN_ROOT,
376 .out.path = "etc/passwd", .pass = true },
377 /* O_CREAT should handle trailing symlinks correctly. */
378 { .name = "[in_root] O_CREAT of relative path inside $root",
379 .path = "newfile1", .how.flags = O_CREAT,
380 .how.mode = 0700,
381 .how.resolve = RESOLVE_IN_ROOT,
382 .out.path = "newfile1", .pass = true },
383 { .name = "[in_root] O_CREAT of absolute path",
384 .path = "/newfile2", .how.flags = O_CREAT,
385 .how.mode = 0700,
386 .how.resolve = RESOLVE_IN_ROOT,
387 .out.path = "newfile2", .pass = true },
388 { .name = "[in_root] O_CREAT of tricky symlink outside root",
389 .path = "/creatlink", .how.flags = O_CREAT,
390 .how.mode = 0700,
391 .how.resolve = RESOLVE_IN_ROOT,
392 .out.path = "newfile3", .pass = true },
393 };
394
395 for (int i = 0; i < ARRAY_SIZE(tests); i++)
396 verify_resolve_test(_metadata, self->rootfd,
397 self->hardcoded_fd, &tests[i]);
398 }
399
400 /* Crossing mount boundaries should be blocked. */
TEST_F(openat2_resolve,resolve_no_xdev)401 TEST_F(openat2_resolve, resolve_no_xdev)
402 {
403 struct resolve_test tests[] = {
404 /* Crossing *down* into a mountpoint is disallowed. */
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 { .name = "[no_xdev] cross into $mnt/.",
412 .path = "mnt/.", .how.resolve = RESOLVE_NO_XDEV,
413 .out.err = -EXDEV, .pass = false },
414 /* Crossing *up* out of a mountpoint is disallowed. */
415 { .name = "[no_xdev] goto mountpoint root",
416 .dir = "mnt", .path = ".", .how.resolve = RESOLVE_NO_XDEV,
417 .out.path = "mnt", .pass = true },
418 { .name = "[no_xdev] cross up through '..'",
419 .dir = "mnt", .path = "..", .how.resolve = RESOLVE_NO_XDEV,
420 .out.err = -EXDEV, .pass = false },
421 { .name = "[no_xdev] temporary cross up through '..'",
422 .dir = "mnt", .path = "../mnt", .how.resolve = RESOLVE_NO_XDEV,
423 .out.err = -EXDEV, .pass = false },
424 { .name = "[no_xdev] temporary relative symlink cross up",
425 .dir = "mnt", .path = "self", .how.resolve = RESOLVE_NO_XDEV,
426 .out.err = -EXDEV, .pass = false },
427 { .name = "[no_xdev] temporary absolute symlink cross up",
428 .dir = "mnt", .path = "absself", .how.resolve = RESOLVE_NO_XDEV,
429 .out.err = -EXDEV, .pass = false },
430 /* Jumping to "/" is ok, but later components cannot cross. */
431 { .name = "[no_xdev] jump to / directly",
432 .dir = "mnt", .path = "/", .how.resolve = RESOLVE_NO_XDEV,
433 .out.path = "/", .pass = true },
434 { .name = "[no_xdev] jump to / (from /) directly",
435 .dir = "/", .path = "/", .how.resolve = RESOLVE_NO_XDEV,
436 .out.path = "/", .pass = true },
437 { .name = "[no_xdev] jump to / then proc",
438 .path = "/proc/1", .how.resolve = RESOLVE_NO_XDEV,
439 .out.err = -EXDEV, .pass = false },
440 { .name = "[no_xdev] jump to / then tmp",
441 .path = "/tmp", .how.resolve = RESOLVE_NO_XDEV,
442 .out.err = -EXDEV, .pass = false },
443 /* Magic-links are blocked since they can switch vfsmounts. */
444 { .name = "[no_xdev] cross through magic-link to self/root",
445 .dir = "/proc", .path = "self/root", .how.resolve = RESOLVE_NO_XDEV,
446 .out.err = -EXDEV, .pass = false },
447 { .name = "[no_xdev] cross through magic-link to self/cwd",
448 .dir = "/proc", .path = "self/cwd", .how.resolve = RESOLVE_NO_XDEV,
449 .out.err = -EXDEV, .pass = false },
450 /* Except magic-link jumps inside the same vfsmount. */
451 { .name = "[no_xdev] jump through magic-link to same procfs",
452 .dir = "/proc", .path = self->hardcoded_fdpath, .how.resolve = RESOLVE_NO_XDEV,
453 .out.path = "/proc", .pass = true, },
454 };
455
456 for (int i = 0; i < ARRAY_SIZE(tests); i++)
457 verify_resolve_test(_metadata, self->rootfd,
458 self->hardcoded_fd, &tests[i]);
459 }
460
461 /* Procfs-style magic-link resolution should be blocked. */
TEST_F(openat2_resolve,resolve_no_magiclinks)462 TEST_F(openat2_resolve, resolve_no_magiclinks)
463 {
464 struct resolve_test tests[] = {
465 /* Regular symlinks should work. */
466 { .name = "[no_magiclinks] ordinary relative symlink",
467 .path = "relsym", .how.resolve = RESOLVE_NO_MAGICLINKS,
468 .out.path = "etc/passwd", .pass = true },
469 /* Magic-links should not work. */
470 { .name = "[no_magiclinks] symlink to magic-link",
471 .path = "procexe", .how.resolve = RESOLVE_NO_MAGICLINKS,
472 .out.err = -ELOOP, .pass = false },
473 { .name = "[no_magiclinks] normal path to magic-link",
474 .path = "/proc/self/exe", .how.resolve = RESOLVE_NO_MAGICLINKS,
475 .out.err = -ELOOP, .pass = false },
476 { .name = "[no_magiclinks] normal path to magic-link with O_NOFOLLOW",
477 .path = "/proc/self/exe", .how.flags = O_NOFOLLOW,
478 .how.resolve = RESOLVE_NO_MAGICLINKS,
479 .out.path = self->procselfexe, .pass = true },
480 { .name = "[no_magiclinks] symlink to magic-link path component",
481 .path = "procroot/etc", .how.resolve = RESOLVE_NO_MAGICLINKS,
482 .out.err = -ELOOP, .pass = false },
483 { .name = "[no_magiclinks] magic-link path component",
484 .path = "/proc/self/root/etc", .how.resolve = RESOLVE_NO_MAGICLINKS,
485 .out.err = -ELOOP, .pass = false },
486 { .name = "[no_magiclinks] magic-link path component with O_NOFOLLOW",
487 .path = "/proc/self/root/etc", .how.flags = O_NOFOLLOW,
488 .how.resolve = RESOLVE_NO_MAGICLINKS,
489 .out.err = -ELOOP, .pass = false },
490 };
491
492 for (int i = 0; i < ARRAY_SIZE(tests); i++)
493 verify_resolve_test(_metadata, self->rootfd,
494 self->hardcoded_fd, &tests[i]);
495 }
496
497 /* All symlink resolution should be blocked. */
TEST_F(openat2_resolve,resolve_no_symlinks)498 TEST_F(openat2_resolve, resolve_no_symlinks)
499 {
500 struct resolve_test tests[] = {
501 /* Normal paths should work. */
502 { .name = "[no_symlinks] ordinary path to '.'",
503 .path = ".", .how.resolve = RESOLVE_NO_SYMLINKS,
504 .out.path = NULL, .pass = true },
505 { .name = "[no_symlinks] ordinary path to 'root'",
506 .path = "root", .how.resolve = RESOLVE_NO_SYMLINKS,
507 .out.path = "root", .pass = true },
508 { .name = "[no_symlinks] ordinary path to 'etc'",
509 .path = "etc", .how.resolve = RESOLVE_NO_SYMLINKS,
510 .out.path = "etc", .pass = true },
511 { .name = "[no_symlinks] ordinary path to 'etc/passwd'",
512 .path = "etc/passwd", .how.resolve = RESOLVE_NO_SYMLINKS,
513 .out.path = "etc/passwd", .pass = true },
514 /* Regular symlinks are blocked. */
515 { .name = "[no_symlinks] relative symlink target",
516 .path = "relsym", .how.resolve = RESOLVE_NO_SYMLINKS,
517 .out.err = -ELOOP, .pass = false },
518 { .name = "[no_symlinks] relative symlink component",
519 .path = "reletc/passwd", .how.resolve = RESOLVE_NO_SYMLINKS,
520 .out.err = -ELOOP, .pass = false },
521 { .name = "[no_symlinks] absolute symlink target",
522 .path = "abssym", .how.resolve = RESOLVE_NO_SYMLINKS,
523 .out.err = -ELOOP, .pass = false },
524 { .name = "[no_symlinks] absolute symlink component",
525 .path = "absetc/passwd", .how.resolve = RESOLVE_NO_SYMLINKS,
526 .out.err = -ELOOP, .pass = false },
527 { .name = "[no_symlinks] cheeky garbage link",
528 .path = "cheeky/garbagelink", .how.resolve = RESOLVE_NO_SYMLINKS,
529 .out.err = -ELOOP, .pass = false },
530 { .name = "[no_symlinks] cheeky absolute + garbage link",
531 .path = "abscheeky/garbagelink", .how.resolve = RESOLVE_NO_SYMLINKS,
532 .out.err = -ELOOP, .pass = false },
533 { .name = "[no_symlinks] cheeky absolute + absolute symlink",
534 .path = "abscheeky/absself", .how.resolve = RESOLVE_NO_SYMLINKS,
535 .out.err = -ELOOP, .pass = false },
536 /* Trailing symlinks with NO_FOLLOW. */
537 { .name = "[no_symlinks] relative symlink with O_NOFOLLOW",
538 .path = "relsym", .how.flags = O_NOFOLLOW,
539 .how.resolve = RESOLVE_NO_SYMLINKS,
540 .out.path = "relsym", .pass = true },
541 { .name = "[no_symlinks] absolute symlink with O_NOFOLLOW",
542 .path = "abssym", .how.flags = O_NOFOLLOW,
543 .how.resolve = RESOLVE_NO_SYMLINKS,
544 .out.path = "abssym", .pass = true },
545 { .name = "[no_symlinks] trailing symlink with O_NOFOLLOW",
546 .path = "cheeky/garbagelink", .how.flags = O_NOFOLLOW,
547 .how.resolve = RESOLVE_NO_SYMLINKS,
548 .out.path = "cheeky/garbagelink", .pass = true },
549 { .name = "[no_symlinks] multiple symlink components with O_NOFOLLOW",
550 .path = "abscheeky/absself", .how.flags = O_NOFOLLOW,
551 .how.resolve = RESOLVE_NO_SYMLINKS,
552 .out.err = -ELOOP, .pass = false },
553 { .name = "[no_symlinks] multiple symlink (and garbage link) components with O_NOFOLLOW",
554 .path = "abscheeky/garbagelink", .how.flags = O_NOFOLLOW,
555 .how.resolve = RESOLVE_NO_SYMLINKS,
556 .out.err = -ELOOP, .pass = false },
557 };
558
559 for (int i = 0; i < ARRAY_SIZE(tests); i++)
560 verify_resolve_test(_metadata, self->rootfd,
561 self->hardcoded_fd, &tests[i]);
562 }
563
564 TEST_HARNESS_MAIN
565