History

Merge patch series "selftests: openat2: migrate to kselftest harness"

Aleksa Sarai <aleksa@amutable.com> says: These tests were written in the early days of selftests' TAP support, the more modern kselftest harness is much easier to follow and maintain. The actual contents of the tests are unchanged by this change. * patches from https://patch.msgid.link/20260401-openat2-selftests-kunit-v2-0-ad153a07da0c@amutable.com: selftests: openat2: migrate to kselftest harness selftests: openat2: switch from custom ARRAY_LEN to ARRAY_SIZE selftests: openat2: move helpers to header selftests: move openat2 tests to selftests/filesystems/

Christian Brauner committed 7 months ago

Commit 3e0be8c2 parents 254f496 215be769 files · +631 −669Browse files

Unified · Split

9 files
  1. tools/testing/selftests/filesystems/openat2/.gitignore +0 −0
  2. tools/testing/selftests/filesystems/openat2/Makefile +2 −3
  3. tools/testing/selftests/filesystems/openat2/helpers.h +168 −0
  4. tools/testing/selftests/filesystems/openat2/openat2_test.c +99 −124
  5. tools/testing/selftests/filesystems/openat2/rename_attack_test.c +159 −0
  6. tools/testing/selftests/filesystems/openat2/resolve_test.c +203 −165
  7. tools/testing/selftests/openat2/helpers.c +0 −109
  8. tools/testing/selftests/openat2/helpers.h +0 −108
  9. tools/testing/selftests/openat2/rename_attack_test.c +0 −160
tools/testing/selftests/filesystems/openat2/.gitignorerenamed from tools/testing/selftests/openat2/.gitignore+0−0

File renamed without changes.

tools/testing/selftests/filesystems/openat2/Makefilerenamed from tools/testing/selftests/openat2/Makefile+2−3
@@ -1,5 +1,6 @@
1# SPDX-License-Identifier: GPL-2.0-or-later1# SPDX-License-Identifier: GPL-2.0-or-later
2 2
3CFLAGS += $(KHDR_INCLUDES)
3CFLAGS += -Wall -O2 -g -fsanitize=address -fsanitize=undefined4CFLAGS += -Wall -O2 -g -fsanitize=address -fsanitize=undefined
4TEST_GEN_PROGS := openat2_test resolve_test rename_attack_test5TEST_GEN_PROGS := openat2_test resolve_test rename_attack_test
5 6
@@ -13,6 +14,4 @@ endif · 7 unmodified lines
13 14
14LOCAL_HDRS += helpers.h15LOCAL_HDRS += helpers.h
15 16
16include ../lib.mk17include ../../lib.mk
17
18$(TEST_GEN_PROGS): helpers.c
tools/testing/selftests/filesystems/openat2/helpers.h+168−0
@@ -0,0 +1,168 @@
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 * Copyright (C) 2026 Amutable GmbH
6 */
7
8#ifndef __RESOLVEAT_H__
9#define __RESOLVEAT_H__
10
11#define _GNU_SOURCE
12#include <stdint.h>
13#include <stdbool.h>
14#include <errno.h>
15#include <limits.h>
16#include <linux/types.h>
17#include <linux/unistd.h>
18#include "kselftest_harness.h"
19
20#define BUILD_BUG_ON(e) ((void)(sizeof(struct { int:(-!!(e)); })))
21
22/*
23 * Arguments for how openat2(2) should open the target path. If @resolve is
24 * zero, then openat2(2) operates very similarly to openat(2).
25 *
26 * However, unlike openat(2), unknown bits in @flags result in -EINVAL rather
27 * than being silently ignored. @mode must be zero unless one of {O_CREAT,
28 * O_TMPFILE} are set.
29 *
30 * @flags: O_* flags.
31 * @mode: O_CREAT/O_TMPFILE file mode.
32 * @resolve: RESOLVE_* flags.
33 */
34struct open_how {
35 __u64 flags;
36 __u64 mode;
37 __u64 resolve;
38};
39
40#define OPEN_HOW_SIZE_VER0 24 /* sizeof first published struct */
41#define OPEN_HOW_SIZE_LATEST OPEN_HOW_SIZE_VER0
42
43#ifndef RESOLVE_IN_ROOT
44/* how->resolve flags for openat2(2). */
45#define RESOLVE_NO_XDEV 0x01 /* Block mount-point crossings
46 (includes bind-mounts). */
47#define RESOLVE_NO_MAGICLINKS 0x02 /* Block traversal through procfs-style
48 "magic-links". */
49#define RESOLVE_NO_SYMLINKS 0x04 /* Block traversal through all symlinks
50 (implies OEXT_NO_MAGICLINKS) */
51#define RESOLVE_BENEATH 0x08 /* Block "lexical" trickery like
52 "..", symlinks, and absolute
53 paths which escape the dirfd. */
54#define RESOLVE_IN_ROOT 0x10 /* Make all jumps to "/" and ".."
55 be scoped inside the dirfd
56 (similar to chroot(2)). */
57#endif /* RESOLVE_IN_ROOT */
58
59__maybe_unused
60static bool needs_openat2(const struct open_how *how)
61{
62 return how->resolve != 0;
63}
64
65__maybe_unused
66static int raw_openat2(int dfd, const char *path, void *how, size_t size)
67{
68 int ret = syscall(__NR_openat2, dfd, path, how, size);
69
70 return ret >= 0 ? ret : -errno;
71}
72
73__maybe_unused
74static int sys_openat2(int dfd, const char *path, struct open_how *how)
75{
76 return raw_openat2(dfd, path, how, sizeof(*how));
77}
78
79__maybe_unused
80static int sys_openat(int dfd, const char *path, struct open_how *how)
81{
82 int ret = openat(dfd, path, how->flags, how->mode);
83
84 return ret >= 0 ? ret : -errno;
85}
86
87__maybe_unused
88static int sys_renameat2(int olddirfd, const char *oldpath,
89 int newdirfd, const char *newpath, unsigned int flags)
90{
91 int ret = syscall(__NR_renameat2, olddirfd, oldpath,
92 newdirfd, newpath, flags);
93
94 return ret >= 0 ? ret : -errno;
95}
96
97__maybe_unused
98static int touchat(int dfd, const char *path)
99{
100 int fd = openat(dfd, path, O_CREAT, 0700);
101
102 if (fd >= 0)
103 close(fd);
104 return fd;
105}
106
107__maybe_unused
108static char *fdreadlink(struct __test_metadata *_metadata, int fd)
109{
110 char *target, *tmp;
111
112 ASSERT_GT(asprintf(&tmp, "/proc/self/fd/%d", fd), 0);
113
114 target = malloc(PATH_MAX);
115 ASSERT_NE(target, NULL);
116 memset(target, 0, PATH_MAX);
117
118 ASSERT_GT(readlink(tmp, target, PATH_MAX), 0);
119
120 free(tmp);
121 return target;
122}
123
124__maybe_unused
125static bool fdequal(struct __test_metadata *_metadata, int fd,
126 int dfd, const char *path)
127{
128 char *fdpath, *dfdpath, *other;
129 bool cmp;
130
131 fdpath = fdreadlink(_metadata, fd);
132 dfdpath = fdreadlink(_metadata, dfd);
133
134 if (!path) {
135 ASSERT_GT(asprintf(&other, "%s", dfdpath), 0);
136 } else if (*path == '/') {
137 ASSERT_GT(asprintf(&other, "%s", path), 0);
138 } else {
139 ASSERT_GT(asprintf(&other, "%s/%s", dfdpath, path), 0);
140 }
141
142 cmp = !strcmp(fdpath, other);
143
144 free(fdpath);
145 free(dfdpath);
146 free(other);
147 return cmp;
148}
149
150static bool openat2_supported = false;
151
152__attribute__((constructor))
153static void __detect_openat2_supported(void)
154{
155 struct open_how how = {};
156 int fd;
157
158 BUILD_BUG_ON(sizeof(struct open_how) != OPEN_HOW_SIZE_VER0);
159
160 /* Check openat2(2) support. */
161 fd = sys_openat2(AT_FDCWD, ".", &how);
162 openat2_supported = (fd >= 0);
163
164 if (fd >= 0)
165 close(fd);
166}
167
168#endif /* __RESOLVEAT_H__ */
tools/testing/selftests/filesystems/openat2/openat2_test.crenamed from tools/testing/selftests/openat2/openat2_test.c+99−124
@@ -15,8 +15,8 @@ · 14 unmodified lines
15#include <stdbool.h>15#include <stdbool.h>
16#include <string.h>16#include <string.h>
17 17
18#include "kselftest.h"
19#include "helpers.h"18#include "helpers.h"
19#include "kselftest_harness.h"
20 20
21/*21/*
22 * O_LARGEFILE is set to 0 by glibc.22 * O_LARGEFILE is set to 0 by glibc.
@@ -45,13 +45,29 @@ struct struct_test { · 22 unmodified lines
45 int err;45 int err;
46};46};
47 47
48#define NUM_OPENAT2_STRUCT_TESTS 748struct flag_test {
49#define NUM_OPENAT2_STRUCT_VARIATIONS 1349 const char *name;
50 struct open_how how;
51 int err;
52};
53
54FIXTURE(openat2) {};
50 55
51void test_openat2_struct(void)56FIXTURE_SETUP(openat2)
52{57{
53 int misalignments[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 17, 87 };58 if (!openat2_supported)
59 SKIP(return, "openat2(2) not supported");
60}
61
62FIXTURE_TEARDOWN(openat2) {}
54 63
64/*
65 * Verify that the struct size and misalignment handling for openat2(2) is
66 * correct, including that is_zeroed_user() works.
67 */
68TEST_F(openat2, struct_argument_sizes)
69{
70 int misalignments[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 17, 87 };
55 struct struct_test tests[] = {71 struct struct_test tests[] = {
56 /* Normal struct. */72 /* Normal struct. */
57 { .name = "normal struct",73 { .name = "normal struct",
@@ -83,26 +99,14 @@ void test_openat2_struct(void) · 25 unmodified lines
83 .size = sizeof(struct open_how_ext), .err = -E2BIG },99 .size = sizeof(struct open_how_ext), .err = -E2BIG },
84 };100 };
85 101
86 BUILD_BUG_ON(ARRAY_LEN(misalignments) != NUM_OPENAT2_STRUCT_VARIATIONS);102 for (int i = 0; i < ARRAY_SIZE(tests); i++) {
87 BUILD_BUG_ON(ARRAY_LEN(tests) != NUM_OPENAT2_STRUCT_TESTS);
88
89 for (int i = 0; i < ARRAY_LEN(tests); i++) {
90 struct struct_test *test = &tests[i];103 struct struct_test *test = &tests[i];
91 struct open_how_ext how_ext = test->arg;104 struct open_how_ext how_ext = test->arg;
92 105
93 for (int j = 0; j < ARRAY_LEN(misalignments); j++) {106 for (int j = 0; j < ARRAY_SIZE(misalignments); j++) {
94 int fd, misalign = misalignments[j];107 int fd, misalign = misalignments[j];
95 char *fdpath = NULL;
96 bool failed;
97 void (*resultfn)(const char *msg, ...) = ksft_test_result_pass;
98
99 void *copy = NULL, *how_copy = &how_ext;108 void *copy = NULL, *how_copy = &how_ext;
100 109 char *fdpath = NULL;
101 if (!openat2_supported) {
102 ksft_print_msg("openat2(2) unsupported\n");
103 resultfn = ksft_test_result_skip;
104 goto skip;
105 }
106 110
107 if (misalign) {111 if (misalign) {
108 /*112 /*
@@ -119,50 +123,42 @@ void test_openat2_struct(void) · 10 unmodified lines
119 }123 }
120 124
121 fd = raw_openat2(AT_FDCWD, ".", how_copy, test->size);125 fd = raw_openat2(AT_FDCWD, ".", how_copy, test->size);
122 if (test->err >= 0)
123 failed = (fd < 0);
124 else
125 failed = (fd != test->err);
126 if (fd >= 0) {126 if (fd >= 0) {
127 fdpath = fdreadlink(fd);127 fdpath = fdreadlink(_metadata, fd);
128 close(fd);128 close(fd);
129 }129 }
130 130
131 if (failed) {131 if (test->err >= 0) {
132 resultfn = ksft_test_result_fail;132 EXPECT_GE(fd, 0) {
133 133 TH_LOG("openat2 with %s [misalign=%d] should succeed, got %d (%s)",
134 ksft_print_msg("openat2 unexpectedly returned ");134 test->name, misalign,
135 if (fdpath)135 fd, strerror(-fd));
136 ksft_print_msg("%d['%s']\n", fd, fdpath);136 }
137 else137 } else {
138 ksft_print_msg("%d (%s)\n", fd, strerror(-fd));138 EXPECT_EQ(test->err, fd) {
139 if (fdpath)
140 TH_LOG("openat2 with %s [misalign=%d] should fail with %d (%s), got %d['%s']",
141 test->name, misalign,
142 test->err,
143 strerror(-test->err),
144 fd, fdpath);
145 else
146 TH_LOG("openat2 with %s [misalign=%d] should fail with %d (%s), got %d (%s)",
147 test->name, misalign,
148 test->err,
149 strerror(-test->err),
150 fd, strerror(-fd));
151 }
139 }152 }
140 153
141skip:
142 if (test->err >= 0)
143 resultfn("openat2 with %s argument [misalign=%d] succeeds\n",
144 test->name, misalign);
145 else
146 resultfn("openat2 with %s argument [misalign=%d] fails with %d (%s)\n",
147 test->name, misalign, test->err,
148 strerror(-test->err));
149
150 free(copy);154 free(copy);
151 free(fdpath);155 free(fdpath);
152 fflush(stdout);
153 }156 }
154 }157 }
155}158}
156 159
157struct flag_test {160/* Verify openat2(2) flag and mode validation. */
158 const char *name;161TEST_F(openat2, flag_validation)
159 struct open_how how;
160 int err;
161};
162
163#define NUM_OPENAT2_FLAG_TESTS 25
164
165void test_openat2_flags(void)
166{162{
167 struct flag_test tests[] = {163 struct flag_test tests[] = {
168 /* O_TMPFILE is incompatible with O_PATH and O_CREAT. */164 /* O_TMPFILE is incompatible with O_PATH and O_CREAT. */
@@ -241,20 +237,10 @@ void test_openat2_flags(void) · 72 unmodified lines
241 .how.resolve = 0, .err = -EINVAL },237 .how.resolve = 0, .err = -EINVAL },
242 };238 };
243 239
244 BUILD_BUG_ON(ARRAY_LEN(tests) != NUM_OPENAT2_FLAG_TESTS);240 for (int i = 0; i < ARRAY_SIZE(tests); i++) {
245
246 for (int i = 0; i < ARRAY_LEN(tests); i++) {
247 int fd, fdflags = -1;241 int fd, fdflags = -1;
248 char *path, *fdpath = NULL;242 char *path, *fdpath = NULL;
249 bool failed = false;
250 struct flag_test *test = &tests[i];243 struct flag_test *test = &tests[i];
251 void (*resultfn)(const char *msg, ...) = ksft_test_result_pass;
252
253 if (!openat2_supported) {
254 ksft_print_msg("openat2(2) unsupported\n");
255 resultfn = ksft_test_result_skip;
256 goto skip;
257 }
258 244
259 path = (test->how.flags & O_CREAT) ? "/tmp/ksft.openat2_tmpfile" : ".";245 path = (test->how.flags & O_CREAT) ? "/tmp/ksft.openat2_tmpfile" : ".";
260 unlink(path);246 unlink(path);
@@ -265,74 +251,63 @@ void test_openat2_flags(void) · 4 unmodified lines
265 * Skip the testcase if it failed because not supported251 * Skip the testcase if it failed because not supported
266 * by FS. (e.g. a valid O_TMPFILE combination on NFS)252 * by FS. (e.g. a valid O_TMPFILE combination on NFS)
267 */253 */
268 ksft_test_result_skip("openat2 with %s fails with %d (%s)\n",254 TH_LOG("openat2 with %s not supported by FS -- skipping",
269 test->name, fd, strerror(-fd));255 test->name);
270 goto next;256 continue;
271 }257 }
272 258
273 if (test->err >= 0)259 if (test->err >= 0) {
274 failed = (fd < 0);260 EXPECT_GE(fd, 0) {
275 else261 TH_LOG("openat2 with %s should succeed, got %d (%s)",
276 failed = (fd != test->err);262 test->name, fd, strerror(-fd));
277 if (fd >= 0) {263 }
278 int otherflags;264 if (fd >= 0) {
279 265 int otherflags;
280 fdpath = fdreadlink(fd);
281 fdflags = fcntl(fd, F_GETFL);
282 otherflags = fcntl(fd, F_GETFD);
283 close(fd);
284
285 E_assert(fdflags >= 0, "fcntl F_GETFL of new fd");
286 E_assert(otherflags >= 0, "fcntl F_GETFD of new fd");
287
288 /* O_CLOEXEC isn't shown in F_GETFL. */
289 if (otherflags & FD_CLOEXEC)
290 fdflags |= O_CLOEXEC;
291 /* O_CREAT is hidden from F_GETFL. */
292 if (test->how.flags & O_CREAT)
293 fdflags |= O_CREAT;
294 if (!(test->how.flags & O_LARGEFILE))
295 fdflags &= ~O_LARGEFILE;
296 failed |= (fdflags != test->how.flags);
297 }
298 266
299 if (failed) {267 fdpath = fdreadlink(_metadata, fd);
300 resultfn = ksft_test_result_fail;268 fdflags = fcntl(fd, F_GETFL);
269 otherflags = fcntl(fd, F_GETFD);
270 close(fd);
301 271
302 ksft_print_msg("openat2 unexpectedly returned ");272 ASSERT_GE(fdflags, 0);
303 if (fdpath)273 ASSERT_GE(otherflags, 0);
304 ksft_print_msg("%d['%s'] with %X (!= %llX)\n",274
305 fd, fdpath, fdflags,275 /* O_CLOEXEC isn't shown in F_GETFL. */
306 test->how.flags);276 if (otherflags & FD_CLOEXEC)
307 else277 fdflags |= O_CLOEXEC;
308 ksft_print_msg("%d (%s)\n", fd, strerror(-fd));278 /* O_CREAT is hidden from F_GETFL. */
279 if (test->how.flags & O_CREAT)
280 fdflags |= O_CREAT;
281 if (!(test->how.flags & O_LARGEFILE))
282 fdflags &= ~O_LARGEFILE;
283
284 EXPECT_EQ(fdflags, (int)test->how.flags) {
285 TH_LOG("openat2 with %s: flags mismatch %X != %llX",
286 test->name, fdflags,
287 (unsigned long long)test->how.flags);
288 }
289 }
290 } else {
291 EXPECT_EQ(test->err, fd) {
292 if (fd >= 0) {
293 fdpath = fdreadlink(_metadata, fd);
294 TH_LOG("openat2 with %s should fail with %d (%s), got %d['%s']",
295 test->name, test->err,
296 strerror(-test->err),
297 fd, fdpath);
298 } else {
299 TH_LOG("openat2 with %s should fail with %d (%s), got %d (%s)",
300 test->name, test->err,
301 strerror(-test->err),
302 fd, strerror(-fd));
303 }
304 }
305 if (fd >= 0)
306 close(fd);
309 }307 }
310 308
311skip:
312 if (test->err >= 0)
313 resultfn("openat2 with %s succeeds\n", test->name);
314 else
315 resultfn("openat2 with %s fails with %d (%s)\n",
316 test->name, test->err, strerror(-test->err));
317next:
318 free(fdpath);309 free(fdpath);
319 fflush(stdout);
320 }310 }
321}311}
322 312
323#define NUM_TESTS (NUM_OPENAT2_STRUCT_VARIATIONS * NUM_OPENAT2_STRUCT_TESTS + \313TEST_HARNESS_MAIN
324 NUM_OPENAT2_FLAG_TESTS)
325
326int main(int argc, char **argv)
327{
328 ksft_print_header();
329 ksft_set_plan(NUM_TESTS);
330
331 test_openat2_struct();
332 test_openat2_flags();
333
334 if (ksft_get_fail_cnt() + ksft_get_error_cnt() > 0)
335 ksft_exit_fail();
336 else
337 ksft_exit_pass();
338}
tools/testing/selftests/filesystems/openat2/rename_attack_test.c+159−0
@@ -0,0 +1,159 @@
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 "helpers.h"
26#include "kselftest_harness.h"
27
28#define ROUNDS 400000
29
30/* Swap @dirfd/@a and @dirfd/@b constantly. Parent must kill this process. */
31pid_t spawn_attack(struct __test_metadata *_metadata,
32 int dirfd, char *a, char *b)
33{
34 pid_t child = fork();
35 if (child != 0)
36 return child;
37
38 /* If the parent (the test process) dies, kill ourselves too. */
39 ASSERT_EQ(prctl(PR_SET_PDEATHSIG, SIGKILL), 0);
40
41 /* Swap @a and @b. */
42 for (;;)
43 renameat2(dirfd, a, dirfd, b, RENAME_EXCHANGE);
44 exit(1);
45}
46
47/*
48 * Construct a test directory with the following structure:
49 *
50 * root/
51 * |-- a/
52 * | `-- c/
53 * `-- b/
54 */
55FIXTURE(rename_attack) {
56 int dfd;
57 int afd;
58 pid_t child;
59};
60
61FIXTURE_SETUP(rename_attack)
62{
63 char dirname[] = "/tmp/ksft-openat2-rename-attack.XXXXXX";
64
65 self->dfd = -1;
66 self->afd = -1;
67 self->child = 0;
68
69 /* Make the top-level directory. */
70 ASSERT_NE(mkdtemp(dirname), NULL);
71 self->dfd = open(dirname, O_PATH | O_DIRECTORY);
72 ASSERT_GE(self->dfd, 0);
73
74 ASSERT_EQ(mkdirat(self->dfd, "a", 0755), 0);
75 ASSERT_EQ(mkdirat(self->dfd, "b", 0755), 0);
76 ASSERT_EQ(mkdirat(self->dfd, "a/c", 0755), 0);
77
78 self->afd = openat(self->dfd, "a", O_PATH);
79 ASSERT_GE(self->afd, 0);
80
81 self->child = spawn_attack(_metadata, self->dfd, "a/c", "b");
82 ASSERT_GT(self->child, 0);
83}
84
85FIXTURE_TEARDOWN(rename_attack)
86{
87 if (self->child > 0)
88 kill(self->child, SIGKILL);
89 if (self->afd >= 0)
90 close(self->afd);
91 if (self->dfd >= 0)
92 close(self->dfd);
93}
94
95FIXTURE_VARIANT(rename_attack) {
96 int resolve;
97 const char *name;
98};
99
100FIXTURE_VARIANT_ADD(rename_attack, resolve_beneath) {
101 .resolve = RESOLVE_BENEATH,
102 .name = "RESOLVE_BENEATH",
103};
104
105FIXTURE_VARIANT_ADD(rename_attack, resolve_in_root) {
106 .resolve = RESOLVE_IN_ROOT,
107 .name = "RESOLVE_IN_ROOT",
108};
109
110TEST_F_TIMEOUT(rename_attack, test, 120)
111{
112 int escapes = 0, successes = 0, other_errs = 0, exdevs = 0, eagains = 0;
113 char *victim_path = "c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../..";
114 struct open_how how = {
115 .flags = O_PATH,
116 .resolve = variant->resolve,
117 };
118
119 if (!openat2_supported) {
120 how.resolve = 0;
121 TH_LOG("openat2(2) unsupported -- using openat(2) instead");
122 }
123
124 for (int i = 0; i < ROUNDS; i++) {
125 int fd;
126
127 if (openat2_supported)
128 fd = sys_openat2(self->afd, victim_path, &how);
129 else
130 fd = sys_openat(self->afd, victim_path, &how);
131
132 if (fd < 0) {
133 if (fd == -EAGAIN)
134 eagains++;
135 else if (fd == -EXDEV)
136 exdevs++;
137 else if (fd == -ENOENT)
138 escapes++; /* escaped outside and got ENOENT... */
139 else
140 other_errs++; /* unexpected error */
141 } else {
142 if (fdequal(_metadata, fd, self->afd, NULL))
143 successes++;
144 else
145 escapes++; /* we got an unexpected fd */
146 }
147 if (fd >= 0)
148 close(fd);
149 }
150
151 TH_LOG("non-escapes: EAGAIN=%d EXDEV=%d E<other>=%d success=%d",
152 eagains, exdevs, other_errs, successes);
153 ASSERT_EQ(escapes, 0) {
154 TH_LOG("rename attack with %s (%d runs, got %d escapes)",
155 variant->name, ROUNDS, escapes);
156 }
157}
158
159TEST_HARNESS_MAIN
tools/testing/selftests/filesystems/openat2/resolve_test.crenamed from tools/testing/selftests/openat2/resolve_test.c+203−165
@@ -14,8 +14,81 @@ · 13 unmodified lines
14#include <stdbool.h>14#include <stdbool.h>
15#include <string.h>15#include <string.h>
16 16
17#include "kselftest.h"
18#include "helpers.h"17#include "helpers.h"
18#include "kselftest_harness.h"
19
20struct 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 */
36static 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}
19 92
20/*93/*
21 * Construct a test directory with the following structure:94 * Construct a test directory with the following structure:
@@ -39,101 +112,110 @@ · 17 unmodified lines
39 * |-- absself -> /112 * |-- absself -> /
40 * |-- self -> ../../root/113 * |-- self -> ../../root/
41 * |-- garbageself -> /../../root/114 * |-- garbageself -> /../../root/
42 * |-- passwd -> ../cheeky/../cheeky/../etc/../etc/passwd115 * |-- passwd -> ../cheeky/../etc/../etc/passwd
43 * |-- abspasswd -> /../cheeky/../cheeky/../etc/../etc/passwd116 * |-- abspasswd -> /../cheeky/../etc/../etc/passwd
44 * |-- dotdotlink -> ../../../../../../../../../../../../../../etc/passwd117 * |-- dotdotlink -> ../../../../../../../../../../../../../../etc/passwd
45 * `-- garbagelink -> /../../../../../../../../../../../../../../etc/passwd118 * `-- garbagelink -> /../../../../../../../../../../../../../../etc/passwd
46 */119 */
47int setup_testdir(void)120FIXTURE(openat2_resolve) {
121 int rootfd;
122 int hardcoded_fd;
123 char *hardcoded_fdpath;
124 char *procselfexe;
125};
126
127FIXTURE_SETUP(openat2_resolve)
48{128{
49 int dfd, tmpfd;
50 char dirname[] = "/tmp/ksft-openat2-testdir.XXXXXX";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");
51 142
52 /* Unshare and make /tmp a new directory. */143 /* Unshare and make /tmp a new directory. */
53 E_unshare(CLONE_NEWNS);144 ASSERT_EQ(unshare(CLONE_NEWNS), 0);
54 E_mount("", "/tmp", "", MS_PRIVATE, "");145 ASSERT_EQ(mount("", "/tmp", "", MS_PRIVATE, ""), 0);
55 146
56 /* Make the top-level directory. */147 /* Make the top-level directory. */
57 if (!mkdtemp(dirname))148 ASSERT_NE(mkdtemp(dirname), NULL);
58 ksft_exit_fail_msg("setup_testdir: failed to create tmpdir\n");
59 dfd = open(dirname, O_PATH | O_DIRECTORY);149 dfd = open(dirname, O_PATH | O_DIRECTORY);
60 if (dfd < 0)150 ASSERT_GE(dfd, 0);
61 ksft_exit_fail_msg("setup_testdir: failed to open tmpdir\n");
62 151
63 /* A sub-directory which is actually used for tests. */152 /* A sub-directory which is actually used for tests. */
64 E_mkdirat(dfd, "root", 0755);153 ASSERT_EQ(mkdirat(dfd, "root", 0755), 0);
65 tmpfd = openat(dfd, "root", O_PATH | O_DIRECTORY);154 tmpfd = openat(dfd, "root", O_PATH | O_DIRECTORY);
66 if (tmpfd < 0)155 ASSERT_GE(tmpfd, 0);
67 ksft_exit_fail_msg("setup_testdir: failed to open tmpdir\n");
68 close(dfd);156 close(dfd);
69 dfd = tmpfd;157 dfd = tmpfd;
70 158
71 E_symlinkat("/proc/self/exe", dfd, "procexe");159 ASSERT_EQ(symlinkat("/proc/self/exe", dfd, "procexe"), 0);
72 E_symlinkat("/proc/self/root", dfd, "procroot");160 ASSERT_EQ(symlinkat("/proc/self/root", dfd, "procroot"), 0);
73 E_mkdirat(dfd, "root", 0755);161 ASSERT_EQ(mkdirat(dfd, "root", 0755), 0);
74 162
75 /* There is no mountat(2), so use chdir. */163 /* There is no mountat(2), so use chdir. */
76 E_mkdirat(dfd, "mnt", 0755);164 ASSERT_EQ(mkdirat(dfd, "mnt", 0755), 0);
77 E_fchdir(dfd);165 ASSERT_EQ(fchdir(dfd), 0);
78 E_mount("tmpfs", "./mnt", "tmpfs", MS_NOSUID | MS_NODEV, "");166 ASSERT_EQ(mount("tmpfs", "./mnt", "tmpfs", MS_NOSUID | MS_NODEV, ""), 0);
79 E_symlinkat("../mnt/", dfd, "mnt/self");167 ASSERT_EQ(symlinkat("../mnt/", dfd, "mnt/self"), 0);
80 E_symlinkat("/mnt/", dfd, "mnt/absself");168 ASSERT_EQ(symlinkat("/mnt/", dfd, "mnt/absself"), 0);
81 169
82 E_mkdirat(dfd, "etc", 0755);170 ASSERT_EQ(mkdirat(dfd, "etc", 0755), 0);
83 E_touchat(dfd, "etc/passwd");171 ASSERT_GE(touchat(dfd, "etc/passwd"), 0);
84 172
85 E_symlinkat("/newfile3", dfd, "creatlink");173 ASSERT_EQ(symlinkat("/newfile3", dfd, "creatlink"), 0);
86 E_symlinkat("etc/", dfd, "reletc");174 ASSERT_EQ(symlinkat("etc/", dfd, "reletc"), 0);
87 E_symlinkat("etc/passwd", dfd, "relsym");175 ASSERT_EQ(symlinkat("etc/passwd", dfd, "relsym"), 0);
88 E_symlinkat("/etc/", dfd, "absetc");176 ASSERT_EQ(symlinkat("/etc/", dfd, "absetc"), 0);
89 E_symlinkat("/etc/passwd", dfd, "abssym");177 ASSERT_EQ(symlinkat("/etc/passwd", dfd, "abssym"), 0);
90 E_symlinkat("/cheeky", dfd, "abscheeky");178 ASSERT_EQ(symlinkat("/cheeky", dfd, "abscheeky"), 0);
91 179
92 E_mkdirat(dfd, "cheeky", 0755);180 ASSERT_EQ(mkdirat(dfd, "cheeky", 0755), 0);
93 181
94 E_symlinkat("/", dfd, "cheeky/absself");182 ASSERT_EQ(symlinkat("/", dfd, "cheeky/absself"), 0);
95 E_symlinkat("../../root/", dfd, "cheeky/self");183 ASSERT_EQ(symlinkat("../../root/", dfd, "cheeky/self"), 0);
96 E_symlinkat("/../../root/", dfd, "cheeky/garbageself");184 ASSERT_EQ(symlinkat("/../../root/", dfd, "cheeky/garbageself"), 0);
97 185
98 E_symlinkat("../cheeky/../etc/../etc/passwd", dfd, "cheeky/passwd");186 ASSERT_EQ(symlinkat("../cheeky/../etc/../etc/passwd",
99 E_symlinkat("/../cheeky/../etc/../etc/passwd", dfd, "cheeky/abspasswd");187 dfd, "cheeky/passwd"), 0);
100 188 ASSERT_EQ(symlinkat("/../cheeky/../etc/../etc/passwd",
101 E_symlinkat("../../../../../../../../../../../../../../etc/passwd",189 dfd, "cheeky/abspasswd"), 0);
102 dfd, "cheeky/dotdotlink");190
103 E_symlinkat("/../../../../../../../../../../../../../../etc/passwd",191 ASSERT_EQ(symlinkat("../../../../../../../../../../../../../../etc/passwd",
104 dfd, "cheeky/garbagelink");192 dfd, "cheeky/dotdotlink"), 0);
105 193 ASSERT_EQ(symlinkat("/../../../../../../../../../../../../../../etc/passwd",
106 return dfd;194 dfd, "cheeky/garbagelink"), 0);
195
196 self->rootfd = dfd;
197
198 self->hardcoded_fd = open("/dev/null", O_RDONLY);
199 ASSERT_GE(self->hardcoded_fd, 0);
200 ASSERT_GE(asprintf(&self->hardcoded_fdpath, "self/fd/%d",
201 self->hardcoded_fd), 0);
202 ASSERT_GE(asprintf(&self->procselfexe, "/proc/%d/exe", getpid()), 0);
107}203}
108 204
109struct basic_test {205FIXTURE_TEARDOWN(openat2_resolve)
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
123void test_openat2_opath_tests(void)
124{206{
125 int rootfd, hardcoded_fd;207 free(self->procselfexe);
126 char *procselfexe, *hardcoded_fdpath;208 free(self->hardcoded_fdpath);
127 209 if (self->hardcoded_fd >= 0)
128 E_asprintf(&procselfexe, "/proc/%d/exe", getpid());210 close(self->hardcoded_fd);
129 rootfd = setup_testdir();211 if (self->rootfd >= 0)
130 212 close(self->rootfd);
131 hardcoded_fd = open("/dev/null", O_RDONLY);213}
132 E_assert(hardcoded_fd >= 0, "open fd to hardcode");
133 E_asprintf(&hardcoded_fdpath, "self/fd/%d", hardcoded_fd);
134 214
135 struct basic_test tests[] = {215/* Attempts to cross the dirfd should be blocked with -EXDEV. */
136 /** RESOLVE_BENEATH **/216TEST_F(openat2_resolve, resolve_beneath)
217{
218 struct resolve_test tests[] = {
137 /* Attempts to cross dirfd should be blocked. */219 /* Attempts to cross dirfd should be blocked. */
138 { .name = "[beneath] jump to /",220 { .name = "[beneath] jump to /",
139 .path = "/", .how.resolve = RESOLVE_BENEATH,221 .path = "/", .how.resolve = RESOLVE_BENEATH,
@@ -206,9 +288,17 @@ void test_openat2_opath_tests(void) · 66 unmodified lines
206 { .name = "[beneath] tricky absolute + garbage link outside $root",288 { .name = "[beneath] tricky absolute + garbage link outside $root",
207 .path = "abscheeky/garbagelink", .how.resolve = RESOLVE_BENEATH,289 .path = "abscheeky/garbagelink", .how.resolve = RESOLVE_BENEATH,
208 .out.err = -EXDEV, .pass = false },290 .out.err = -EXDEV, .pass = false },
291 };
292
293 for (int i = 0; i < ARRAY_SIZE(tests); i++)
294 verify_resolve_test(_metadata, self->rootfd,
295 self->hardcoded_fd, &tests[i]);
296}
209 297
210 /** RESOLVE_IN_ROOT **/298/* All attempts to cross the dirfd will be scoped-to-root. */
211 /* All attempts to cross the dirfd will be scoped-to-root. */299TEST_F(openat2_resolve, resolve_in_root)
300{
301 struct resolve_test tests[] = {
212 { .name = "[in_root] jump to /",302 { .name = "[in_root] jump to /",
213 .path = "/", .how.resolve = RESOLVE_IN_ROOT,303 .path = "/", .how.resolve = RESOLVE_IN_ROOT,
214 .out.path = NULL, .pass = true },304 .out.path = NULL, .pass = true },
@@ -297,8 +387,17 @@ void test_openat2_opath_tests(void) · 82 unmodified lines
297 .how.mode = 0700,387 .how.mode = 0700,
298 .how.resolve = RESOLVE_IN_ROOT,388 .how.resolve = RESOLVE_IN_ROOT,
299 .out.path = "newfile3", .pass = true },389 .out.path = "newfile3", .pass = true },
390 };
300 391
301 /** RESOLVE_NO_XDEV **/392 for (int i = 0; i < ARRAY_SIZE(tests); i++)
393 verify_resolve_test(_metadata, self->rootfd,
394 self->hardcoded_fd, &tests[i]);
395}
396
397/* Crossing mount boundaries should be blocked. */
398TEST_F(openat2_resolve, resolve_no_xdev)
399{
400 struct resolve_test tests[] = {
302 /* Crossing *down* into a mountpoint is disallowed. */401 /* Crossing *down* into a mountpoint is disallowed. */
303 { .name = "[no_xdev] cross into $mnt",402 { .name = "[no_xdev] cross into $mnt",
304 .path = "mnt", .how.resolve = RESOLVE_NO_XDEV,403 .path = "mnt", .how.resolve = RESOLVE_NO_XDEV,
@@ -347,10 +446,19 @@ void test_openat2_opath_tests(void) · 42 unmodified lines
347 .out.err = -EXDEV, .pass = false },446 .out.err = -EXDEV, .pass = false },
348 /* Except magic-link jumps inside the same vfsmount. */447 /* Except magic-link jumps inside the same vfsmount. */
349 { .name = "[no_xdev] jump through magic-link to same procfs",448 { .name = "[no_xdev] jump through magic-link to same procfs",
350 .dir = "/proc", .path = hardcoded_fdpath, .how.resolve = RESOLVE_NO_XDEV,449 .dir = "/proc", .path = self->hardcoded_fdpath, .how.resolve = RESOLVE_NO_XDEV,
351 .out.path = "/proc", .pass = true, },450 .out.path = "/proc", .pass = true, },
451 };
452
453 for (int i = 0; i < ARRAY_SIZE(tests); i++)
454 verify_resolve_test(_metadata, self->rootfd,
455 self->hardcoded_fd, &tests[i]);
456}
352 457
353 /** RESOLVE_NO_MAGICLINKS **/458/* Procfs-style magic-link resolution should be blocked. */
459TEST_F(openat2_resolve, resolve_no_magiclinks)
460{
461 struct resolve_test tests[] = {
354 /* Regular symlinks should work. */462 /* Regular symlinks should work. */
355 { .name = "[no_magiclinks] ordinary relative symlink",463 { .name = "[no_magiclinks] ordinary relative symlink",
356 .path = "relsym", .how.resolve = RESOLVE_NO_MAGICLINKS,464 .path = "relsym", .how.resolve = RESOLVE_NO_MAGICLINKS,
@@ -365,7 +473,7 @@ void test_openat2_opath_tests(void) · 8 unmodified lines
365 { .name = "[no_magiclinks] normal path to magic-link with O_NOFOLLOW",473 { .name = "[no_magiclinks] normal path to magic-link with O_NOFOLLOW",
366 .path = "/proc/self/exe", .how.flags = O_NOFOLLOW,474 .path = "/proc/self/exe", .how.flags = O_NOFOLLOW,
367 .how.resolve = RESOLVE_NO_MAGICLINKS,475 .how.resolve = RESOLVE_NO_MAGICLINKS,
368 .out.path = procselfexe, .pass = true },476 .out.path = self->procselfexe, .pass = true },
369 { .name = "[no_magiclinks] symlink to magic-link path component",477 { .name = "[no_magiclinks] symlink to magic-link path component",
370 .path = "procroot/etc", .how.resolve = RESOLVE_NO_MAGICLINKS,478 .path = "procroot/etc", .how.resolve = RESOLVE_NO_MAGICLINKS,
371 .out.err = -ELOOP, .pass = false },479 .out.err = -ELOOP, .pass = false },
@@ -376,8 +484,17 @@ void test_openat2_opath_tests(void) · 4 unmodified lines
376 .path = "/proc/self/root/etc", .how.flags = O_NOFOLLOW,484 .path = "/proc/self/root/etc", .how.flags = O_NOFOLLOW,
377 .how.resolve = RESOLVE_NO_MAGICLINKS,485 .how.resolve = RESOLVE_NO_MAGICLINKS,
378 .out.err = -ELOOP, .pass = false },486 .out.err = -ELOOP, .pass = false },
487 };
488
489 for (int i = 0; i < ARRAY_SIZE(tests); i++)
490 verify_resolve_test(_metadata, self->rootfd,
491 self->hardcoded_fd, &tests[i]);
492}
379 493
380 /** RESOLVE_NO_SYMLINKS **/494/* All symlink resolution should be blocked. */
495TEST_F(openat2_resolve, resolve_no_symlinks)
496{
497 struct resolve_test tests[] = {
381 /* Normal paths should work. */498 /* Normal paths should work. */
382 { .name = "[no_symlinks] ordinary path to '.'",499 { .name = "[no_symlinks] ordinary path to '.'",
383 .path = ".", .how.resolve = RESOLVE_NO_SYMLINKS,500 .path = ".", .how.resolve = RESOLVE_NO_SYMLINKS,
@@ -436,88 +553,9 @@ void test_openat2_opath_tests(void) · 52 unmodified lines
436 .out.err = -ELOOP, .pass = false },553 .out.err = -ELOOP, .pass = false },
437 };554 };
438 555
439 BUILD_BUG_ON(ARRAY_LEN(tests) != NUM_OPENAT2_OPATH_TESTS);556 for (int i = 0; i < ARRAY_SIZE(tests); i++)
440 557 verify_resolve_test(_metadata, self->rootfd,
441 for (int i = 0; i < ARRAY_LEN(tests); i++) {558 self->hardcoded_fd, &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
487skip:
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}559}
505 560
506#define NUM_TESTS NUM_OPENAT2_OPATH_TESTS561TEST_HARNESS_MAIN
507
508int 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}
tools/testing/selftests/openat2/helpers.c+0−109
@@ -1,109 +0,0 @@
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
17bool needs_openat2(const struct open_how *how)
18{
19 return how->resolve != 0;
20}
21
22int 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
28int sys_openat2(int dfd, const char *path, struct open_how *how)
29{
30 return raw_openat2(dfd, path, how, sizeof(*how));
31}
32
33int 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
39int 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
47int touchat(int dfd, const char *path)
48{
49 int fd = openat(dfd, path, O_CREAT, 0700);
50 if (fd >= 0)
51 close(fd);
52 return fd;
53}
54
55char *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
71bool 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
94bool openat2_supported = false;
95
96void __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}
tools/testing/selftests/openat2/helpers.h+0−108
@@ -1,108 +0,0 @@
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 <stdbool.h>
13#include <errno.h>
14#include <linux/types.h>
15#include "kselftest.h"
16
17#define ARRAY_LEN(X) (sizeof (X) / sizeof (*(X)))
18#define BUILD_BUG_ON(e) ((void)(sizeof(struct { int:(-!!(e)); })))
19
20#ifndef SYS_openat2
21#ifndef __NR_openat2
22#define __NR_openat2 437
23#endif /* __NR_openat2 */
24#define SYS_openat2 __NR_openat2
25#endif /* SYS_openat2 */
26
27/*
28 * Arguments for how openat2(2) should open the target path. If @resolve is
29 * zero, then openat2(2) operates very similarly to openat(2).
30 *
31 * However, unlike openat(2), unknown bits in @flags result in -EINVAL rather
32 * than being silently ignored. @mode must be zero unless one of {O_CREAT,
33 * O_TMPFILE} are set.
34 *
35 * @flags: O_* flags.
36 * @mode: O_CREAT/O_TMPFILE file mode.
37 * @resolve: RESOLVE_* flags.
38 */
39struct open_how {
40 __u64 flags;
41 __u64 mode;
42 __u64 resolve;
43};
44
45#define OPEN_HOW_SIZE_VER0 24 /* sizeof first published struct */
46#define OPEN_HOW_SIZE_LATEST OPEN_HOW_SIZE_VER0
47
48bool needs_openat2(const struct open_how *how);
49
50#ifndef RESOLVE_IN_ROOT
51/* how->resolve flags for openat2(2). */
52#define RESOLVE_NO_XDEV 0x01 /* Block mount-point crossings
53 (includes bind-mounts). */
54#define RESOLVE_NO_MAGICLINKS 0x02 /* Block traversal through procfs-style
55 "magic-links". */
56#define RESOLVE_NO_SYMLINKS 0x04 /* Block traversal through all symlinks
57 (implies OEXT_NO_MAGICLINKS) */
58#define RESOLVE_BENEATH 0x08 /* Block "lexical" trickery like
59 "..", symlinks, and absolute
60 paths which escape the dirfd. */
61#define RESOLVE_IN_ROOT 0x10 /* Make all jumps to "/" and ".."
62 be scoped inside the dirfd
63 (similar to chroot(2)). */
64#endif /* RESOLVE_IN_ROOT */
65
66#define E_func(func, ...) \
67 do { \
68 errno = 0; \
69 if (func(__VA_ARGS__) < 0) \
70 ksft_exit_fail_msg("%s:%d %s failed - errno:%d\n", \
71 __FILE__, __LINE__, #func, errno); \
72 } while (0)
73
74#define E_asprintf(...) E_func(asprintf, __VA_ARGS__)
75#define E_chmod(...) E_func(chmod, __VA_ARGS__)
76#define E_dup2(...) E_func(dup2, __VA_ARGS__)
77#define E_fchdir(...) E_func(fchdir, __VA_ARGS__)
78#define E_fstatat(...) E_func(fstatat, __VA_ARGS__)
79#define E_kill(...) E_func(kill, __VA_ARGS__)
80#define E_mkdirat(...) E_func(mkdirat, __VA_ARGS__)
81#define E_mount(...) E_func(mount, __VA_ARGS__)
82#define E_prctl(...) E_func(prctl, __VA_ARGS__)
83#define E_readlink(...) E_func(readlink, __VA_ARGS__)
84#define E_setresuid(...) E_func(setresuid, __VA_ARGS__)
85#define E_symlinkat(...) E_func(symlinkat, __VA_ARGS__)
86#define E_touchat(...) E_func(touchat, __VA_ARGS__)
87#define E_unshare(...) E_func(unshare, __VA_ARGS__)
88
89#define E_assert(expr, msg, ...) \
90 do { \
91 if (!(expr)) \
92 ksft_exit_fail_msg("ASSERT(%s:%d) failed (%s): " msg "\n", \
93 __FILE__, __LINE__, #expr, ##__VA_ARGS__); \
94 } while (0)
95
96int raw_openat2(int dfd, const char *path, void *how, size_t size);
97int sys_openat2(int dfd, const char *path, struct open_how *how);
98int sys_openat(int dfd, const char *path, struct open_how *how);
99int sys_renameat2(int olddirfd, const char *oldpath,
100 int newdirfd, const char *newpath, unsigned int flags);
101
102int touchat(int dfd, const char *path);
103char *fdreadlink(int fd);
104bool fdequal(int fd, int dfd, const char *path);
105
106extern bool openat2_supported;
107
108#endif /* __RESOLVEAT_H__ */
tools/testing/selftests/openat2/rename_attack_test.c+0−160
@@ -1,160 +0,0 @@
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 */
35int 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. */
55pid_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
73const 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
84void 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
148int 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}