Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

selftests: add openat2(2) selftests

Test all of the various openat2(2) flags. A small stress-test of a
symlink-rename attack is included to show that the protections against
".."-based attacks are sufficient.

The main things these self-tests are enforcing are:

* The struct+usize ABI for openat2(2) and copy_struct_from_user() to
ensure that upgrades will be handled gracefully (in addition,
ensuring that misaligned structures are also handled correctly).

* The -EINVAL checks for openat2(2) are all correctly handled to avoid
userspace passing unknown or conflicting flag sets (most
importantly, ensuring that invalid flag combinations are checked).

* All of the RESOLVE_* semantics (including errno values) are
correctly handled with various combinations of paths and flags.

* RESOLVE_IN_ROOT correctly protects against the symlink rename(2)
attack that has been responsible for several CVEs (and likely will
be responsible for several more).

Cc: Shuah Khan <shuah@kernel.org>
Signed-off-by: Aleksa Sarai <cyphar@cyphar.com>
Signed-off-by: Al Viro <viro@zeniv.linux.org.uk>

authored by

Aleksa Sarai and committed by
Al Viro
b28a10ae fddb5d43

+1220
+1
tools/testing/selftests/Makefile
··· 40 40 TARGETS += proc 41 41 TARGETS += pstore 42 42 TARGETS += ptrace 43 + TARGETS += openat2 43 44 TARGETS += rseq 44 45 TARGETS += rtc 45 46 TARGETS += seccomp
+1
tools/testing/selftests/openat2/.gitignore
··· 1 + /*_test
+8
tools/testing/selftests/openat2/Makefile
··· 1 + # SPDX-License-Identifier: GPL-2.0-or-later 2 + 3 + CFLAGS += -Wall -O2 -g -fsanitize=address -fsanitize=undefined 4 + TEST_GEN_PROGS := openat2_test resolve_test rename_attack_test 5 + 6 + include ../lib.mk 7 + 8 + $(TEST_GEN_PROGS): helpers.c
+109
tools/testing/selftests/openat2/helpers.c
··· 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 <errno.h> 9 + #include <fcntl.h> 10 + #include <stdbool.h> 11 + #include <string.h> 12 + #include <syscall.h> 13 + #include <limits.h> 14 + 15 + #include "helpers.h" 16 + 17 + bool needs_openat2(const struct open_how *how) 18 + { 19 + return how->resolve != 0; 20 + } 21 + 22 + int raw_openat2(int dfd, const char *path, void *how, size_t size) 23 + { 24 + int ret = syscall(__NR_openat2, dfd, path, how, size); 25 + return ret >= 0 ? ret : -errno; 26 + } 27 + 28 + int sys_openat2(int dfd, const char *path, struct open_how *how) 29 + { 30 + return raw_openat2(dfd, path, how, sizeof(*how)); 31 + } 32 + 33 + int sys_openat(int dfd, const char *path, struct open_how *how) 34 + { 35 + int ret = openat(dfd, path, how->flags, how->mode); 36 + return ret >= 0 ? ret : -errno; 37 + } 38 + 39 + int sys_renameat2(int olddirfd, const char *oldpath, 40 + int newdirfd, const char *newpath, unsigned int flags) 41 + { 42 + int ret = syscall(__NR_renameat2, olddirfd, oldpath, 43 + newdirfd, newpath, flags); 44 + return ret >= 0 ? ret : -errno; 45 + } 46 + 47 + int touchat(int dfd, const char *path) 48 + { 49 + int fd = openat(dfd, path, O_CREAT); 50 + if (fd >= 0) 51 + close(fd); 52 + return fd; 53 + } 54 + 55 + char *fdreadlink(int fd) 56 + { 57 + char *target, *tmp; 58 + 59 + E_asprintf(&tmp, "/proc/self/fd/%d", fd); 60 + 61 + target = malloc(PATH_MAX); 62 + if (!target) 63 + ksft_exit_fail_msg("fdreadlink: malloc failed\n"); 64 + memset(target, 0, PATH_MAX); 65 + 66 + E_readlink(tmp, target, PATH_MAX); 67 + free(tmp); 68 + return target; 69 + } 70 + 71 + bool fdequal(int fd, int dfd, const char *path) 72 + { 73 + char *fdpath, *dfdpath, *other; 74 + bool cmp; 75 + 76 + fdpath = fdreadlink(fd); 77 + dfdpath = fdreadlink(dfd); 78 + 79 + if (!path) 80 + E_asprintf(&other, "%s", dfdpath); 81 + else if (*path == '/') 82 + E_asprintf(&other, "%s", path); 83 + else 84 + E_asprintf(&other, "%s/%s", dfdpath, path); 85 + 86 + cmp = !strcmp(fdpath, other); 87 + 88 + free(fdpath); 89 + free(dfdpath); 90 + free(other); 91 + return cmp; 92 + } 93 + 94 + bool openat2_supported = false; 95 + 96 + void __attribute__((constructor)) init(void) 97 + { 98 + struct open_how how = {}; 99 + int fd; 100 + 101 + BUILD_BUG_ON(sizeof(struct open_how) != OPEN_HOW_SIZE_VER0); 102 + 103 + /* Check openat2(2) support. */ 104 + fd = sys_openat2(AT_FDCWD, ".", &how); 105 + openat2_supported = (fd >= 0); 106 + 107 + if (fd >= 0) 108 + close(fd); 109 + }
+106
tools/testing/selftests/openat2/helpers.h
··· 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 + #ifndef __RESOLVEAT_H__ 8 + #define __RESOLVEAT_H__ 9 + 10 + #define _GNU_SOURCE 11 + #include <stdint.h> 12 + #include <errno.h> 13 + #include <linux/types.h> 14 + #include "../kselftest.h" 15 + 16 + #define ARRAY_LEN(X) (sizeof (X) / sizeof (*(X))) 17 + #define BUILD_BUG_ON(e) ((void)(sizeof(struct { int:(-!!(e)); }))) 18 + 19 + #ifndef SYS_openat2 20 + #ifndef __NR_openat2 21 + #define __NR_openat2 437 22 + #endif /* __NR_openat2 */ 23 + #define SYS_openat2 __NR_openat2 24 + #endif /* SYS_openat2 */ 25 + 26 + /* 27 + * Arguments for how openat2(2) should open the target path. If @resolve is 28 + * zero, then openat2(2) operates very similarly to openat(2). 29 + * 30 + * However, unlike openat(2), unknown bits in @flags result in -EINVAL rather 31 + * than being silently ignored. @mode must be zero unless one of {O_CREAT, 32 + * O_TMPFILE} are set. 33 + * 34 + * @flags: O_* flags. 35 + * @mode: O_CREAT/O_TMPFILE file mode. 36 + * @resolve: RESOLVE_* flags. 37 + */ 38 + struct open_how { 39 + __u64 flags; 40 + __u64 mode; 41 + __u64 resolve; 42 + }; 43 + 44 + #define OPEN_HOW_SIZE_VER0 24 /* sizeof first published struct */ 45 + #define OPEN_HOW_SIZE_LATEST OPEN_HOW_SIZE_VER0 46 + 47 + bool needs_openat2(const struct open_how *how); 48 + 49 + #ifndef RESOLVE_IN_ROOT 50 + /* how->resolve flags for openat2(2). */ 51 + #define RESOLVE_NO_XDEV 0x01 /* Block mount-point crossings 52 + (includes bind-mounts). */ 53 + #define RESOLVE_NO_MAGICLINKS 0x02 /* Block traversal through procfs-style 54 + "magic-links". */ 55 + #define RESOLVE_NO_SYMLINKS 0x04 /* Block traversal through all symlinks 56 + (implies OEXT_NO_MAGICLINKS) */ 57 + #define RESOLVE_BENEATH 0x08 /* Block "lexical" trickery like 58 + "..", symlinks, and absolute 59 + paths which escape the dirfd. */ 60 + #define RESOLVE_IN_ROOT 0x10 /* Make all jumps to "/" and ".." 61 + be scoped inside the dirfd 62 + (similar to chroot(2)). */ 63 + #endif /* RESOLVE_IN_ROOT */ 64 + 65 + #define E_func(func, ...) \ 66 + do { \ 67 + if (func(__VA_ARGS__) < 0) \ 68 + ksft_exit_fail_msg("%s:%d %s failed\n", \ 69 + __FILE__, __LINE__, #func);\ 70 + } while (0) 71 + 72 + #define E_asprintf(...) E_func(asprintf, __VA_ARGS__) 73 + #define E_chmod(...) E_func(chmod, __VA_ARGS__) 74 + #define E_dup2(...) E_func(dup2, __VA_ARGS__) 75 + #define E_fchdir(...) E_func(fchdir, __VA_ARGS__) 76 + #define E_fstatat(...) E_func(fstatat, __VA_ARGS__) 77 + #define E_kill(...) E_func(kill, __VA_ARGS__) 78 + #define E_mkdirat(...) E_func(mkdirat, __VA_ARGS__) 79 + #define E_mount(...) E_func(mount, __VA_ARGS__) 80 + #define E_prctl(...) E_func(prctl, __VA_ARGS__) 81 + #define E_readlink(...) E_func(readlink, __VA_ARGS__) 82 + #define E_setresuid(...) E_func(setresuid, __VA_ARGS__) 83 + #define E_symlinkat(...) E_func(symlinkat, __VA_ARGS__) 84 + #define E_touchat(...) E_func(touchat, __VA_ARGS__) 85 + #define E_unshare(...) E_func(unshare, __VA_ARGS__) 86 + 87 + #define E_assert(expr, msg, ...) \ 88 + do { \ 89 + if (!(expr)) \ 90 + ksft_exit_fail_msg("ASSERT(%s:%d) failed (%s): " msg "\n", \ 91 + __FILE__, __LINE__, #expr, ##__VA_ARGS__); \ 92 + } while (0) 93 + 94 + int raw_openat2(int dfd, const char *path, void *how, size_t size); 95 + int sys_openat2(int dfd, const char *path, struct open_how *how); 96 + int sys_openat(int dfd, const char *path, struct open_how *how); 97 + int sys_renameat2(int olddirfd, const char *oldpath, 98 + int newdirfd, const char *newpath, unsigned int flags); 99 + 100 + int touchat(int dfd, const char *path); 101 + char *fdreadlink(int fd); 102 + bool fdequal(int fd, int dfd, const char *path); 103 + 104 + extern bool openat2_supported; 105 + 106 + #endif /* __RESOLVEAT_H__ */
+312
tools/testing/selftests/openat2/openat2_test.c
··· 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 + * O_LARGEFILE is set to 0 by glibc. 22 + * XXX: This is wrong on {mips, parisc, powerpc, sparc}. 23 + */ 24 + #undef O_LARGEFILE 25 + #define O_LARGEFILE 0x8000 26 + 27 + struct open_how_ext { 28 + struct open_how inner; 29 + uint32_t extra1; 30 + char pad1[128]; 31 + uint32_t extra2; 32 + char pad2[128]; 33 + uint32_t extra3; 34 + }; 35 + 36 + struct struct_test { 37 + const char *name; 38 + struct open_how_ext arg; 39 + size_t size; 40 + int err; 41 + }; 42 + 43 + #define NUM_OPENAT2_STRUCT_TESTS 7 44 + #define NUM_OPENAT2_STRUCT_VARIATIONS 13 45 + 46 + void test_openat2_struct(void) 47 + { 48 + int misalignments[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 17, 87 }; 49 + 50 + struct struct_test tests[] = { 51 + /* Normal struct. */ 52 + { .name = "normal struct", 53 + .arg.inner.flags = O_RDONLY, 54 + .size = sizeof(struct open_how) }, 55 + /* Bigger struct, with zeroed out end. */ 56 + { .name = "bigger struct (zeroed out)", 57 + .arg.inner.flags = O_RDONLY, 58 + .size = sizeof(struct open_how_ext) }, 59 + 60 + /* TODO: Once expanded, check zero-padding. */ 61 + 62 + /* Smaller than version-0 struct. */ 63 + { .name = "zero-sized 'struct'", 64 + .arg.inner.flags = O_RDONLY, .size = 0, .err = -EINVAL }, 65 + { .name = "smaller-than-v0 struct", 66 + .arg.inner.flags = O_RDONLY, 67 + .size = OPEN_HOW_SIZE_VER0 - 1, .err = -EINVAL }, 68 + 69 + /* Bigger struct, with non-zero trailing bytes. */ 70 + { .name = "bigger struct (non-zero data in first 'future field')", 71 + .arg.inner.flags = O_RDONLY, .arg.extra1 = 0xdeadbeef, 72 + .size = sizeof(struct open_how_ext), .err = -E2BIG }, 73 + { .name = "bigger struct (non-zero data in middle of 'future fields')", 74 + .arg.inner.flags = O_RDONLY, .arg.extra2 = 0xfeedcafe, 75 + .size = sizeof(struct open_how_ext), .err = -E2BIG }, 76 + { .name = "bigger struct (non-zero data at end of 'future fields')", 77 + .arg.inner.flags = O_RDONLY, .arg.extra3 = 0xabad1dea, 78 + .size = sizeof(struct open_how_ext), .err = -E2BIG }, 79 + }; 80 + 81 + BUILD_BUG_ON(ARRAY_LEN(misalignments) != NUM_OPENAT2_STRUCT_VARIATIONS); 82 + BUILD_BUG_ON(ARRAY_LEN(tests) != NUM_OPENAT2_STRUCT_TESTS); 83 + 84 + for (int i = 0; i < ARRAY_LEN(tests); i++) { 85 + struct struct_test *test = &tests[i]; 86 + struct open_how_ext how_ext = test->arg; 87 + 88 + for (int j = 0; j < ARRAY_LEN(misalignments); j++) { 89 + int fd, misalign = misalignments[j]; 90 + char *fdpath = NULL; 91 + bool failed; 92 + void (*resultfn)(const char *msg, ...) = ksft_test_result_pass; 93 + 94 + void *copy = NULL, *how_copy = &how_ext; 95 + 96 + if (!openat2_supported) { 97 + ksft_print_msg("openat2(2) unsupported\n"); 98 + resultfn = ksft_test_result_skip; 99 + goto skip; 100 + } 101 + 102 + if (misalign) { 103 + /* 104 + * Explicitly misalign the structure copying it with the given 105 + * (mis)alignment offset. The other data is set to be non-zero to 106 + * make sure that non-zero bytes outside the struct aren't checked 107 + * 108 + * This is effectively to check that is_zeroed_user() works. 109 + */ 110 + copy = malloc(misalign + sizeof(how_ext)); 111 + how_copy = copy + misalign; 112 + memset(copy, 0xff, misalign); 113 + memcpy(how_copy, &how_ext, sizeof(how_ext)); 114 + } 115 + 116 + fd = raw_openat2(AT_FDCWD, ".", how_copy, test->size); 117 + if (test->err >= 0) 118 + failed = (fd < 0); 119 + else 120 + failed = (fd != test->err); 121 + if (fd >= 0) { 122 + fdpath = fdreadlink(fd); 123 + close(fd); 124 + } 125 + 126 + if (failed) { 127 + resultfn = ksft_test_result_fail; 128 + 129 + ksft_print_msg("openat2 unexpectedly returned "); 130 + if (fdpath) 131 + ksft_print_msg("%d['%s']\n", fd, fdpath); 132 + else 133 + ksft_print_msg("%d (%s)\n", fd, strerror(-fd)); 134 + } 135 + 136 + skip: 137 + if (test->err >= 0) 138 + resultfn("openat2 with %s argument [misalign=%d] succeeds\n", 139 + test->name, misalign); 140 + else 141 + resultfn("openat2 with %s argument [misalign=%d] fails with %d (%s)\n", 142 + test->name, misalign, test->err, 143 + strerror(-test->err)); 144 + 145 + free(copy); 146 + free(fdpath); 147 + fflush(stdout); 148 + } 149 + } 150 + } 151 + 152 + struct flag_test { 153 + const char *name; 154 + struct open_how how; 155 + int err; 156 + }; 157 + 158 + #define NUM_OPENAT2_FLAG_TESTS 23 159 + 160 + void test_openat2_flags(void) 161 + { 162 + struct flag_test tests[] = { 163 + /* O_TMPFILE is incompatible with O_PATH and O_CREAT. */ 164 + { .name = "incompatible flags (O_TMPFILE | O_PATH)", 165 + .how.flags = O_TMPFILE | O_PATH | O_RDWR, .err = -EINVAL }, 166 + { .name = "incompatible flags (O_TMPFILE | O_CREAT)", 167 + .how.flags = O_TMPFILE | O_CREAT | O_RDWR, .err = -EINVAL }, 168 + 169 + /* O_PATH only permits certain other flags to be set ... */ 170 + { .name = "compatible flags (O_PATH | O_CLOEXEC)", 171 + .how.flags = O_PATH | O_CLOEXEC }, 172 + { .name = "compatible flags (O_PATH | O_DIRECTORY)", 173 + .how.flags = O_PATH | O_DIRECTORY }, 174 + { .name = "compatible flags (O_PATH | O_NOFOLLOW)", 175 + .how.flags = O_PATH | O_NOFOLLOW }, 176 + /* ... and others are absolutely not permitted. */ 177 + { .name = "incompatible flags (O_PATH | O_RDWR)", 178 + .how.flags = O_PATH | O_RDWR, .err = -EINVAL }, 179 + { .name = "incompatible flags (O_PATH | O_CREAT)", 180 + .how.flags = O_PATH | O_CREAT, .err = -EINVAL }, 181 + { .name = "incompatible flags (O_PATH | O_EXCL)", 182 + .how.flags = O_PATH | O_EXCL, .err = -EINVAL }, 183 + { .name = "incompatible flags (O_PATH | O_NOCTTY)", 184 + .how.flags = O_PATH | O_NOCTTY, .err = -EINVAL }, 185 + { .name = "incompatible flags (O_PATH | O_DIRECT)", 186 + .how.flags = O_PATH | O_DIRECT, .err = -EINVAL }, 187 + { .name = "incompatible flags (O_PATH | O_LARGEFILE)", 188 + .how.flags = O_PATH | O_LARGEFILE, .err = -EINVAL }, 189 + 190 + /* ->mode must only be set with O_{CREAT,TMPFILE}. */ 191 + { .name = "non-zero how.mode and O_RDONLY", 192 + .how.flags = O_RDONLY, .how.mode = 0600, .err = -EINVAL }, 193 + { .name = "non-zero how.mode and O_PATH", 194 + .how.flags = O_PATH, .how.mode = 0600, .err = -EINVAL }, 195 + { .name = "valid how.mode and O_CREAT", 196 + .how.flags = O_CREAT, .how.mode = 0600 }, 197 + { .name = "valid how.mode and O_TMPFILE", 198 + .how.flags = O_TMPFILE | O_RDWR, .how.mode = 0600 }, 199 + /* ->mode must only contain 0777 bits. */ 200 + { .name = "invalid how.mode and O_CREAT", 201 + .how.flags = O_CREAT, 202 + .how.mode = 0xFFFF, .err = -EINVAL }, 203 + { .name = "invalid (very large) how.mode and O_CREAT", 204 + .how.flags = O_CREAT, 205 + .how.mode = 0xC000000000000000ULL, .err = -EINVAL }, 206 + { .name = "invalid how.mode and O_TMPFILE", 207 + .how.flags = O_TMPFILE | O_RDWR, 208 + .how.mode = 0x1337, .err = -EINVAL }, 209 + { .name = "invalid (very large) how.mode and O_TMPFILE", 210 + .how.flags = O_TMPFILE | O_RDWR, 211 + .how.mode = 0x0000A00000000000ULL, .err = -EINVAL }, 212 + 213 + /* ->resolve must only contain RESOLVE_* flags. */ 214 + { .name = "invalid how.resolve and O_RDONLY", 215 + .how.flags = O_RDONLY, 216 + .how.resolve = 0x1337, .err = -EINVAL }, 217 + { .name = "invalid how.resolve and O_CREAT", 218 + .how.flags = O_CREAT, 219 + .how.resolve = 0x1337, .err = -EINVAL }, 220 + { .name = "invalid how.resolve and O_TMPFILE", 221 + .how.flags = O_TMPFILE | O_RDWR, 222 + .how.resolve = 0x1337, .err = -EINVAL }, 223 + { .name = "invalid how.resolve and O_PATH", 224 + .how.flags = O_PATH, 225 + .how.resolve = 0x1337, .err = -EINVAL }, 226 + }; 227 + 228 + BUILD_BUG_ON(ARRAY_LEN(tests) != NUM_OPENAT2_FLAG_TESTS); 229 + 230 + for (int i = 0; i < ARRAY_LEN(tests); i++) { 231 + int fd, fdflags = -1; 232 + char *path, *fdpath = NULL; 233 + bool failed = false; 234 + struct flag_test *test = &tests[i]; 235 + void (*resultfn)(const char *msg, ...) = ksft_test_result_pass; 236 + 237 + if (!openat2_supported) { 238 + ksft_print_msg("openat2(2) unsupported\n"); 239 + resultfn = ksft_test_result_skip; 240 + goto skip; 241 + } 242 + 243 + path = (test->how.flags & O_CREAT) ? "/tmp/ksft.openat2_tmpfile" : "."; 244 + unlink(path); 245 + 246 + fd = sys_openat2(AT_FDCWD, path, &test->how); 247 + if (test->err >= 0) 248 + failed = (fd < 0); 249 + else 250 + failed = (fd != test->err); 251 + if (fd >= 0) { 252 + int otherflags; 253 + 254 + fdpath = fdreadlink(fd); 255 + fdflags = fcntl(fd, F_GETFL); 256 + otherflags = fcntl(fd, F_GETFD); 257 + close(fd); 258 + 259 + E_assert(fdflags >= 0, "fcntl F_GETFL of new fd"); 260 + E_assert(otherflags >= 0, "fcntl F_GETFD of new fd"); 261 + 262 + /* O_CLOEXEC isn't shown in F_GETFL. */ 263 + if (otherflags & FD_CLOEXEC) 264 + fdflags |= O_CLOEXEC; 265 + /* O_CREAT is hidden from F_GETFL. */ 266 + if (test->how.flags & O_CREAT) 267 + fdflags |= O_CREAT; 268 + if (!(test->how.flags & O_LARGEFILE)) 269 + fdflags &= ~O_LARGEFILE; 270 + failed |= (fdflags != test->how.flags); 271 + } 272 + 273 + if (failed) { 274 + resultfn = ksft_test_result_fail; 275 + 276 + ksft_print_msg("openat2 unexpectedly returned "); 277 + if (fdpath) 278 + ksft_print_msg("%d['%s'] with %X (!= %X)\n", 279 + fd, fdpath, fdflags, 280 + test->how.flags); 281 + else 282 + ksft_print_msg("%d (%s)\n", fd, strerror(-fd)); 283 + } 284 + 285 + skip: 286 + if (test->err >= 0) 287 + resultfn("openat2 with %s succeeds\n", test->name); 288 + else 289 + resultfn("openat2 with %s fails with %d (%s)\n", 290 + test->name, test->err, strerror(-test->err)); 291 + 292 + free(fdpath); 293 + fflush(stdout); 294 + } 295 + } 296 + 297 + #define NUM_TESTS (NUM_OPENAT2_STRUCT_VARIATIONS * NUM_OPENAT2_STRUCT_TESTS + \ 298 + NUM_OPENAT2_FLAG_TESTS) 299 + 300 + int main(int argc, char **argv) 301 + { 302 + ksft_print_header(); 303 + ksft_set_plan(NUM_TESTS); 304 + 305 + test_openat2_struct(); 306 + test_openat2_flags(); 307 + 308 + if (ksft_get_fail_cnt() + ksft_get_error_cnt() > 0) 309 + ksft_exit_fail(); 310 + else 311 + ksft_exit_pass(); 312 + }
+160
tools/testing/selftests/openat2/rename_attack_test.c
··· 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 <errno.h> 9 + #include <fcntl.h> 10 + #include <sched.h> 11 + #include <sys/stat.h> 12 + #include <sys/types.h> 13 + #include <sys/mount.h> 14 + #include <sys/mman.h> 15 + #include <sys/prctl.h> 16 + #include <signal.h> 17 + #include <stdio.h> 18 + #include <stdlib.h> 19 + #include <stdbool.h> 20 + #include <string.h> 21 + #include <syscall.h> 22 + #include <limits.h> 23 + #include <unistd.h> 24 + 25 + #include "../kselftest.h" 26 + #include "helpers.h" 27 + 28 + /* Construct a test directory with the following structure: 29 + * 30 + * root/ 31 + * |-- a/ 32 + * | `-- c/ 33 + * `-- b/ 34 + */ 35 + int setup_testdir(void) 36 + { 37 + int dfd; 38 + char dirname[] = "/tmp/ksft-openat2-rename-attack.XXXXXX"; 39 + 40 + /* Make the top-level directory. */ 41 + if (!mkdtemp(dirname)) 42 + ksft_exit_fail_msg("setup_testdir: failed to create tmpdir\n"); 43 + dfd = open(dirname, O_PATH | O_DIRECTORY); 44 + if (dfd < 0) 45 + ksft_exit_fail_msg("setup_testdir: failed to open tmpdir\n"); 46 + 47 + E_mkdirat(dfd, "a", 0755); 48 + E_mkdirat(dfd, "b", 0755); 49 + E_mkdirat(dfd, "a/c", 0755); 50 + 51 + return dfd; 52 + } 53 + 54 + /* Swap @dirfd/@a and @dirfd/@b constantly. Parent must kill this process. */ 55 + pid_t spawn_attack(int dirfd, char *a, char *b) 56 + { 57 + pid_t child = fork(); 58 + if (child != 0) 59 + return child; 60 + 61 + /* If the parent (the test process) dies, kill ourselves too. */ 62 + E_prctl(PR_SET_PDEATHSIG, SIGKILL); 63 + 64 + /* Swap @a and @b. */ 65 + for (;;) 66 + renameat2(dirfd, a, dirfd, b, RENAME_EXCHANGE); 67 + exit(1); 68 + } 69 + 70 + #define NUM_RENAME_TESTS 2 71 + #define ROUNDS 400000 72 + 73 + const char *flagname(int resolve) 74 + { 75 + switch (resolve) { 76 + case RESOLVE_IN_ROOT: 77 + return "RESOLVE_IN_ROOT"; 78 + case RESOLVE_BENEATH: 79 + return "RESOLVE_BENEATH"; 80 + } 81 + return "(unknown)"; 82 + } 83 + 84 + void test_rename_attack(int resolve) 85 + { 86 + int dfd, afd; 87 + pid_t child; 88 + void (*resultfn)(const char *msg, ...) = ksft_test_result_pass; 89 + int escapes = 0, other_errs = 0, exdevs = 0, eagains = 0, successes = 0; 90 + 91 + struct open_how how = { 92 + .flags = O_PATH, 93 + .resolve = resolve, 94 + }; 95 + 96 + if (!openat2_supported) { 97 + how.resolve = 0; 98 + ksft_print_msg("openat2(2) unsupported -- using openat(2) instead\n"); 99 + } 100 + 101 + dfd = setup_testdir(); 102 + afd = openat(dfd, "a", O_PATH); 103 + if (afd < 0) 104 + ksft_exit_fail_msg("test_rename_attack: failed to open 'a'\n"); 105 + 106 + child = spawn_attack(dfd, "a/c", "b"); 107 + 108 + for (int i = 0; i < ROUNDS; i++) { 109 + int fd; 110 + char *victim_path = "c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../.."; 111 + 112 + if (openat2_supported) 113 + fd = sys_openat2(afd, victim_path, &how); 114 + else 115 + fd = sys_openat(afd, victim_path, &how); 116 + 117 + if (fd < 0) { 118 + if (fd == -EAGAIN) 119 + eagains++; 120 + else if (fd == -EXDEV) 121 + exdevs++; 122 + else if (fd == -ENOENT) 123 + escapes++; /* escaped outside and got ENOENT... */ 124 + else 125 + other_errs++; /* unexpected error */ 126 + } else { 127 + if (fdequal(fd, afd, NULL)) 128 + successes++; 129 + else 130 + escapes++; /* we got an unexpected fd */ 131 + } 132 + close(fd); 133 + } 134 + 135 + if (escapes > 0) 136 + resultfn = ksft_test_result_fail; 137 + ksft_print_msg("non-escapes: EAGAIN=%d EXDEV=%d E<other>=%d success=%d\n", 138 + eagains, exdevs, other_errs, successes); 139 + resultfn("rename attack with %s (%d runs, got %d escapes)\n", 140 + flagname(resolve), ROUNDS, escapes); 141 + 142 + /* Should be killed anyway, but might as well make sure. */ 143 + E_kill(child, SIGKILL); 144 + } 145 + 146 + #define NUM_TESTS NUM_RENAME_TESTS 147 + 148 + int main(int argc, char **argv) 149 + { 150 + ksft_print_header(); 151 + ksft_set_plan(NUM_TESTS); 152 + 153 + test_rename_attack(RESOLVE_BENEATH); 154 + test_rename_attack(RESOLVE_IN_ROOT); 155 + 156 + if (ksft_get_fail_cnt() + ksft_get_error_cnt() > 0) 157 + ksft_exit_fail(); 158 + else 159 + ksft_exit_pass(); 160 + }
+523
tools/testing/selftests/openat2/resolve_test.c
··· 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("setup_testdir: failed to create tmpdir\n"); 59 + dfd = open(dirname, O_PATH | O_DIRECTORY); 60 + if (dfd < 0) 61 + ksft_exit_fail_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("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] chainged 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("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 + }