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