1 // SPDX-License-Identifier: GPL-2.0-or-later
3 * Author: Aleksa Sarai <cyphar@cyphar.com>
4 * Copyright (C) 2018-2019 SUSE LLC.
11 #include <sys/types.h>
12 #include <sys/mount.h>
17 #include "../kselftest.h"
21 * Construct a test directory with the following structure:
24 * |-- procexe -> /proc/self/exe
25 * |-- procroot -> /proc/self/root
27 * |-- mnt/ [mountpoint]
28 * | |-- self -> ../mnt/
29 * | `-- absself -> /mnt/
32 * |-- creatlink -> /newfile3
34 * |-- relsym -> etc/passwd
36 * |-- abssym -> /etc/passwd
37 * |-- abscheeky -> /cheeky
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
47 int setup_testdir(void)
50 char dirname
[] = "/tmp/ksft-openat2-testdir.XXXXXX";
52 /* Unshare and make /tmp a new directory. */
53 E_unshare(CLONE_NEWNS
);
54 E_mount("", "/tmp", "", MS_PRIVATE
, "");
56 /* Make the top-level directory. */
57 if (!mkdtemp(dirname
))
58 ksft_exit_fail_msg("setup_testdir: failed to create tmpdir\n");
59 dfd
= open(dirname
, O_PATH
| O_DIRECTORY
);
61 ksft_exit_fail_msg("setup_testdir: failed to open tmpdir\n");
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
);
67 ksft_exit_fail_msg("setup_testdir: failed to open tmpdir\n");
71 E_symlinkat("/proc/self/exe", dfd
, "procexe");
72 E_symlinkat("/proc/self/root", dfd
, "procroot");
73 E_mkdirat(dfd
, "root", 0755);
75 /* There is no mountat(2), so use chdir. */
76 E_mkdirat(dfd
, "mnt", 0755);
78 E_mount("tmpfs", "./mnt", "tmpfs", MS_NOSUID
| MS_NODEV
, "");
79 E_symlinkat("../mnt/", dfd
, "mnt/self");
80 E_symlinkat("/mnt/", dfd
, "mnt/absself");
82 E_mkdirat(dfd
, "etc", 0755);
83 E_touchat(dfd
, "etc/passwd");
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");
92 E_mkdirat(dfd
, "cheeky", 0755);
94 E_symlinkat("/", dfd
, "cheeky/absself");
95 E_symlinkat("../../root/", dfd
, "cheeky/self");
96 E_symlinkat("/../../root/", dfd
, "cheeky/garbageself");
98 E_symlinkat("../cheeky/../etc/../etc/passwd", dfd
, "cheeky/passwd");
99 E_symlinkat("/../cheeky/../etc/../etc/passwd", dfd
, "cheeky/abspasswd");
101 E_symlinkat("../../../../../../../../../../../../../../etc/passwd",
102 dfd
, "cheeky/dotdotlink");
103 E_symlinkat("/../../../../../../../../../../../../../../etc/passwd",
104 dfd
, "cheeky/garbagelink");
121 #define NUM_OPENAT2_OPATH_TESTS 88
123 void test_openat2_opath_tests(void)
125 int rootfd
, hardcoded_fd
;
126 char *procselfexe
, *hardcoded_fdpath
;
128 E_asprintf(&procselfexe
, "/proc/%d/exe", getpid());
129 rootfd
= setup_testdir();
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
);
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 },
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
,
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
,
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
,
298 .how
.resolve
= RESOLVE_IN_ROOT
,
299 .out
.path
= "newfile3", .pass
= true },
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, },
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 },
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 },
439 BUILD_BUG_ON(ARRAY_LEN(tests
) != NUM_OPENAT2_OPATH_TESTS
);
441 for (int i
= 0; i
< ARRAY_LEN(tests
); i
++) {
445 void (*resultfn
)(const char *msg
, ...) = ksft_test_result_pass
;
446 struct basic_test
*test
= &tests
[i
];
448 if (!openat2_supported
) {
449 ksft_print_msg("openat2(2) unsupported\n");
450 resultfn
= ksft_test_result_skip
;
454 /* Auto-set O_PATH. */
455 if (!(test
->how
.flags
& O_CREAT
))
456 test
->how
.flags
|= O_PATH
;
459 dfd
= openat(rootfd
, test
->dir
, O_PATH
| O_DIRECTORY
);
462 E_assert(dfd
, "failed to openat root '%s': %m", test
->dir
);
464 E_dup2(dfd
, hardcoded_fd
);
466 fd
= sys_openat2(dfd
, test
->path
, &test
->how
);
468 failed
= (fd
< 0 || !fdequal(fd
, rootfd
, test
->out
.path
));
470 failed
= (fd
!= test
->out
.err
);
472 fdpath
= fdreadlink(fd
);
478 resultfn
= ksft_test_result_fail
;
480 ksft_print_msg("openat2 unexpectedly returned ");
482 ksft_print_msg("%d['%s']\n", fd
, fdpath
);
484 ksft_print_msg("%d (%s)\n", fd
, strerror(-fd
));
489 resultfn("%s gives path '%s'\n", test
->name
,
490 test
->out
.path
?: ".");
492 resultfn("%s fails with %d (%s)\n", test
->name
,
493 test
->out
.err
, strerror(-test
->out
.err
));
502 free(hardcoded_fdpath
);
506 #define NUM_TESTS NUM_OPENAT2_OPATH_TESTS
508 int main(int argc
, char **argv
)
511 ksft_set_plan(NUM_TESTS
);
513 /* NOTE: We should be checking for CAP_SYS_ADMIN here... */
515 ksft_exit_skip("all tests require euid == 0\n");
517 test_openat2_opath_tests();
519 if (ksft_get_fail_cnt() + ksft_get_error_cnt() > 0)