lkml.org 
[lkml]   [2019]   [Aug]   [19]   [last100]   RSS Feed
Views: [wrap][no wrap]   [headers]  [forward] 
 
Messages in this thread
    Patch in this message
    /
    From
    Subject[PATCH RESEND v11 8/8] selftests: add openat2(2) selftests
    Date
    Test all of the various openat2(2) flags, as well as how file
    descriptor re-opening works. A small stress-test of a symlink-rename
    attack is included to show that the protections against ".."-based
    attacks are sufficient.

    In addition, the memfd selftest is fixed to no longer depend on the
    now-disallowed functionality of upgrading an O_RDONLY descriptor to
    O_RDWR.

    Signed-off-by: Aleksa Sarai <cyphar@cyphar.com>
    ---
    tools/testing/selftests/Makefile | 1 +
    tools/testing/selftests/memfd/memfd_test.c | 7 +-
    tools/testing/selftests/openat2/.gitignore | 1 +
    tools/testing/selftests/openat2/Makefile | 8 +
    tools/testing/selftests/openat2/helpers.c | 162 +++++++
    tools/testing/selftests/openat2/helpers.h | 116 +++++
    .../testing/selftests/openat2/linkmode_test.c | 333 +++++++++++++++
    .../selftests/openat2/rename_attack_test.c | 127 ++++++
    .../testing/selftests/openat2/resolve_test.c | 402 ++++++++++++++++++
    9 files changed, 1155 insertions(+), 2 deletions(-)
    create mode 100644 tools/testing/selftests/openat2/.gitignore
    create mode 100644 tools/testing/selftests/openat2/Makefile
    create mode 100644 tools/testing/selftests/openat2/helpers.c
    create mode 100644 tools/testing/selftests/openat2/helpers.h
    create mode 100644 tools/testing/selftests/openat2/linkmode_test.c
    create mode 100644 tools/testing/selftests/openat2/rename_attack_test.c
    create mode 100644 tools/testing/selftests/openat2/resolve_test.c

    diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
    index 25b43a8c2b15..13c02e0d0efc 100644
    --- a/tools/testing/selftests/Makefile
    +++ b/tools/testing/selftests/Makefile
    @@ -37,6 +37,7 @@ TARGETS += powerpc
    TARGETS += proc
    TARGETS += pstore
    TARGETS += ptrace
    +TARGETS += openat2
    TARGETS += rseq
    TARGETS += rtc
    TARGETS += seccomp
    diff --git a/tools/testing/selftests/memfd/memfd_test.c b/tools/testing/selftests/memfd/memfd_test.c
    index c67d32eeb668..e71df3d3e55d 100644
    --- a/tools/testing/selftests/memfd/memfd_test.c
    +++ b/tools/testing/selftests/memfd/memfd_test.c
    @@ -925,7 +925,7 @@ static void test_share_mmap(char *banner, char *b_suffix)
    */
    static void test_share_open(char *banner, char *b_suffix)
    {
    - int fd, fd2;
    + int procfd, fd, fd2;

    printf("%s %s %s\n", memfd_str, banner, b_suffix);

    @@ -950,13 +950,16 @@ static void test_share_open(char *banner, char *b_suffix)
    mfd_assert_has_seals(fd, F_SEAL_WRITE | F_SEAL_SHRINK);
    mfd_assert_has_seals(fd2, F_SEAL_WRITE | F_SEAL_SHRINK);

    + /* We cannot do a MAY_WRITE re-open of an O_RDONLY fd. */
    + procfd = mfd_assert_open(fd2, O_PATH, 0);
    close(fd2);
    - fd2 = mfd_assert_open(fd, O_RDWR, 0);
    + fd2 = mfd_assert_open(procfd, O_WRONLY, 0);

    mfd_assert_add_seals(fd2, F_SEAL_SEAL);
    mfd_assert_has_seals(fd, F_SEAL_WRITE | F_SEAL_SHRINK | F_SEAL_SEAL);
    mfd_assert_has_seals(fd2, F_SEAL_WRITE | F_SEAL_SHRINK | F_SEAL_SEAL);

    + close(procfd);
    close(fd2);
    close(fd);
    }
    diff --git a/tools/testing/selftests/openat2/.gitignore b/tools/testing/selftests/openat2/.gitignore
    new file mode 100644
    index 000000000000..bd68f6c3fd07
    --- /dev/null
    +++ b/tools/testing/selftests/openat2/.gitignore
    @@ -0,0 +1 @@
    +/*_test
    diff --git a/tools/testing/selftests/openat2/Makefile b/tools/testing/selftests/openat2/Makefile
    new file mode 100644
    index 000000000000..a0c1b53fd268
    --- /dev/null
    +++ b/tools/testing/selftests/openat2/Makefile
    @@ -0,0 +1,8 @@
    +# SPDX-License-Identifier: GPL-2.0
    +
    +CFLAGS += -Wall -O2 -g
    +TEST_GEN_PROGS := linkmode_test resolve_test rename_attack_test
    +
    +include ../lib.mk
    +
    +$(TEST_GEN_PROGS): helpers.c
    diff --git a/tools/testing/selftests/openat2/helpers.c b/tools/testing/selftests/openat2/helpers.c
    new file mode 100644
    index 000000000000..b9b7c7fc7a99
    --- /dev/null
    +++ b/tools/testing/selftests/openat2/helpers.c
    @@ -0,0 +1,162 @@
    +// SPDX-License-Identifier: GPL-2.0-or-later
    +/*
    + * Author: Aleksa Sarai <cyphar@cyphar.com>
    + * Copyright (C) 2018-2019 SUSE LLC.
    + */
    +
    +#define _GNU_SOURCE
    +#include <errno.h>
    +#include <fcntl.h>
    +#include <stdbool.h>
    +#include <string.h>
    +#include <syscall.h>
    +#include <limits.h>
    +
    +#include "helpers.h"
    +
    +int sys_openat2(int dfd, const char *path, const struct open_how *how)
    +{
    + int ret = syscall(__NR_openat2, dfd, path, how);
    + return ret >= 0 ? ret : -errno;
    +}
    +
    +int sys_openat(int dfd, const char *path, const struct open_how *how)
    +{
    + int ret = openat(dfd, path, how->flags, how->mode);
    + return ret >= 0 ? ret : -errno;
    +}
    +
    +int sys_renameat2(int olddirfd, const char *oldpath,
    + int newdirfd, const char *newpath, unsigned int flags)
    +{
    + int ret = syscall(__NR_renameat2, olddirfd, oldpath,
    + newdirfd, newpath, flags);
    + return ret >= 0 ? ret : -errno;
    +}
    +
    +char *openat_flags(unsigned int flags)
    +{
    + char *flagset, *accmode = "(none)";
    +
    + switch (flags & 0x03) {
    + case O_RDWR:
    + accmode = "O_RDWR";
    + break;
    + case O_RDONLY:
    + accmode = "O_RDONLY";
    + break;
    + case O_WRONLY:
    + accmode = "O_WRONLY";
    + break;
    + }
    +
    + E_asprintf(&flagset, "%s%s%s",
    + (flags & O_PATH) ? "O_PATH|" : "",
    + (flags & O_CREAT) ? "O_CREAT|" : "",
    + accmode);
    +
    + return flagset;
    +}
    +
    +char *openat2_flags(const struct open_how *how)
    +{
    + char *p;
    + char *flags_set, *resolve_set, *acc_set, *set;
    +
    + flags_set = openat_flags(how->flags);
    +
    + E_asprintf(&resolve_set, "%s%s%s%s%s0",
    + (how->resolve & RESOLVE_NO_XDEV) ? "RESOLVE_NO_XDEV|" : "",
    + (how->resolve & RESOLVE_NO_MAGICLINKS) ? "RESOLVE_NO_MAGICLINKS|" : "",
    + (how->resolve & RESOLVE_NO_SYMLINKS) ? "RESOLVE_NO_SYMLINKS|" : "",
    + (how->resolve & RESOLVE_BENEATH) ? "RESOLVE_BENEATH|" : "",
    + (how->resolve & RESOLVE_IN_ROOT) ? "RESOLVE_IN_ROOT|" : "");
    +
    + /* Remove trailing "|0". */
    + p = strstr(resolve_set, "|0");
    + if (p)
    + *p = '\0';
    +
    + if (how->flags & O_PATH)
    + E_asprintf(&acc_set, ", upgrade_mask=%s%s0",
    + (how->upgrade_mask & UPGRADE_NOREAD) ? "UPGRADE_NOREAD|" : "",
    + (how->upgrade_mask & UPGRADE_NOWRITE) ? "UPGRADE_NOWRITE|" : "");
    + else if (how->flags & O_CREAT)
    + E_asprintf(&acc_set, ", mode=0%o", how->mode);
    + else
    + acc_set = strdup("");
    +
    + /* Remove trailing "|0". */
    + p = strstr(acc_set, "|0");
    + if (p)
    + *p = '\0';
    +
    + /* And now generate our flagset. */
    + E_asprintf(&set, "[flags=%s, resolve=%s%s]",
    + flags_set, resolve_set, acc_set);
    +
    + free(flags_set);
    + free(resolve_set);
    + free(acc_set);
    + return set;
    +}
    +
    +int touchat(int dfd, const char *path)
    +{
    + int fd = openat(dfd, path, O_CREAT);
    + if (fd >= 0)
    + close(fd);
    + return fd;
    +}
    +
    +char *fdreadlink(int fd)
    +{
    + char *target, *tmp;
    +
    + E_asprintf(&tmp, "/proc/self/fd/%d", fd);
    +
    + target = malloc(PATH_MAX);
    + if (!target)
    + ksft_exit_fail_msg("fdreadlink: malloc failed\n");
    + memset(target, 0, PATH_MAX);
    +
    + E_readlink(tmp, target, PATH_MAX);
    + free(tmp);
    + return target;
    +}
    +
    +bool fdequal(int fd, int dfd, const char *path)
    +{
    + char *fdpath, *dfdpath, *other;
    + bool cmp;
    +
    + fdpath = fdreadlink(fd);
    + dfdpath = fdreadlink(dfd);
    +
    + if (!path)
    + E_asprintf(&other, "%s", dfdpath);
    + else if (*path == '/')
    + E_asprintf(&other, "%s", path);
    + else
    + E_asprintf(&other, "%s/%s", dfdpath, path);
    +
    + cmp = !strcmp(fdpath, other);
    + if (!cmp)
    + ksft_print_msg("fdequal: expected '%s' but got '%s'\n", other, fdpath);
    +
    + free(fdpath);
    + free(dfdpath);
    + free(other);
    + return cmp;
    +}
    +
    +void test_openat2_supported(void)
    +{
    + struct open_how how = {};
    + int fd = sys_openat2(AT_FDCWD, ".", &how);
    + if (fd == -ENOSYS)
    + ksft_exit_skip("openat2(2) unsupported on this kernel\n");
    + if (fd < 0)
    + ksft_exit_fail_msg("openat2(2) supported check failed: %s\n", strerror(-fd));
    + close(fd);
    +}
    diff --git a/tools/testing/selftests/openat2/helpers.h b/tools/testing/selftests/openat2/helpers.h
    new file mode 100644
    index 000000000000..43fa7835950f
    --- /dev/null
    +++ b/tools/testing/selftests/openat2/helpers.h
    @@ -0,0 +1,116 @@
    +// SPDX-License-Identifier: GPL-2.0-or-later
    +/*
    + * Author: Aleksa Sarai <cyphar@cyphar.com>
    + * Copyright (C) 2018-2019 SUSE LLC.
    + */
    +
    +#ifndef __RESOLVEAT_H__
    +#define __RESOLVEAT_H__
    +
    +#define _GNU_SOURCE
    +#include <stdint.h>
    +#include <errno.h>
    +#include "../kselftest.h"
    +
    +#define ARRAY_LEN(X) (sizeof (X) / sizeof (*(X)))
    +#define BUILD_BUG_ON(e) ((void)(sizeof(struct { int:(-!!(e)); })))
    +
    +#ifndef SYS_openat2
    +#ifndef __NR_openat2
    +#define __NR_openat2 437
    +#endif /* __NR_openat2 */
    +#define SYS_openat2 __NR_openat2
    +#endif /* SYS_openat2 */
    +
    +/**
    + * Arguments for how openat2(2) should open the target path. If @extra is zero,
    + * then openat2 is identical to openat(2). Only one of @mode or @upgrade_mask
    + * may be set at any given time.
    + *
    + * @flags: O_* flags (unknown flags ignored).
    + * @mode: O_CREAT file mode (ignored otherwise).
    + * @upgrade_mask: restrict how the O_PATH may be re-opened (ignored otherwise).
    + * @resolve: RESOLVE_* flags (-EINVAL on unknown flags).
    + * @reserved: reserved for future extensions, must be zeroed.
    + */
    +struct open_how {
    + uint32_t flags;
    + union {
    + uint16_t mode;
    + uint16_t upgrade_mask;
    + };
    + uint16_t resolve;
    + uint64_t reserved[7]; /* must be zeroed */
    +};
    +
    +#ifndef RESOLVE_INROOT
    +/* how->resolve flags for openat2(2). */
    +#define RESOLVE_NO_XDEV 0x01 /* Block mount-point crossings
    + (includes bind-mounts). */
    +#define RESOLVE_NO_MAGICLINKS 0x02 /* Block traversal through procfs-style
    + "magic-links". */
    +#define RESOLVE_NO_SYMLINKS 0x04 /* Block traversal through all symlinks
    + (implies OEXT_NO_MAGICLINKS) */
    +#define RESOLVE_BENEATH 0x08 /* Block "lexical" trickery like
    + "..", symlinks, and absolute
    + paths which escape the dirfd. */
    +#define RESOLVE_IN_ROOT 0x10 /* Make all jumps to "/" and ".."
    + be scoped inside the dirfd
    + (similar to chroot(2)). */
    +#endif /* RESOLVE_IN_ROOT */
    +
    +#ifndef UPGRADE_NOREAD
    +/* how->upgrade flags for openat2(2). */
    +/* First bit is reserved for a future UPGRADE_NOEXEC flag. */
    +#define UPGRADE_NOREAD 0x02 /* Block re-opening with MAY_READ. */
    +#define UPGRADE_NOWRITE 0x04 /* Block re-opening with MAY_WRITE. */
    +#endif /* UPGRADE_NOREAD */
    +
    +#ifndef O_EMPTYPATH
    +#define O_EMPTYPATH 040000000
    +#endif /* O_EMPTYPATH */
    +
    +#define E_func(func, ...) \
    + do { \
    + if (func(__VA_ARGS__) < 0) \
    + ksft_exit_fail_msg("%s:%d %s failed\n", \
    + __FILE__, __LINE__, #func);\
    + } while (0)
    +
    +#define E_mkdirat(...) E_func(mkdirat, __VA_ARGS__)
    +#define E_symlinkat(...) E_func(symlinkat, __VA_ARGS__)
    +#define E_touchat(...) E_func(touchat, __VA_ARGS__)
    +#define E_readlink(...) E_func(readlink, __VA_ARGS__)
    +#define E_fstatat(...) E_func(fstatat, __VA_ARGS__)
    +#define E_asprintf(...) E_func(asprintf, __VA_ARGS__)
    +#define E_fchdir(...) E_func(fchdir, __VA_ARGS__)
    +#define E_mount(...) E_func(mount, __VA_ARGS__)
    +#define E_unshare(...) E_func(unshare, __VA_ARGS__)
    +#define E_setresuid(...) E_func(setresuid, __VA_ARGS__)
    +#define E_chmod(...) E_func(chmod, __VA_ARGS__)
    +
    +#define E_assert(expr, msg, ...) \
    + do { \
    + if (!(expr)) \
    + ksft_exit_fail_msg("ASSERT(%s:%d) failed (%s): " msg "\n", \
    + __FILE__, __LINE__, #expr, ##__VA_ARGS__); \
    + } while (0)
    +
    +typedef int (*openfunc_t)(int dfd, const char *path, const struct open_how *how);
    +
    +int sys_openat2(int dfd, const char *path, const struct open_how *how);
    +char *openat2_flags(const struct open_how *how);
    +
    +int sys_openat(int dfd, const char *path, const struct open_how *how);
    +char *openat_flags(unsigned int flags);
    +
    +int sys_renameat2(int olddirfd, const char *oldpath,
    + int newdirfd, const char *newpath, unsigned int flags);
    +
    +int touchat(int dfd, const char *path);
    +char *fdreadlink(int fd);
    +bool fdequal(int fd, int dfd, const char *path);
    +
    +void test_openat2_supported(void);
    +
    +#endif /* __RESOLVEAT_H__ */
    diff --git a/tools/testing/selftests/openat2/linkmode_test.c b/tools/testing/selftests/openat2/linkmode_test.c
    new file mode 100644
    index 000000000000..44fcba738686
    --- /dev/null
    +++ b/tools/testing/selftests/openat2/linkmode_test.c
    @@ -0,0 +1,333 @@
    +// SPDX-License-Identifier: GPL-2.0-or-later
    +/*
    + * Author: Aleksa Sarai <cyphar@cyphar.com>
    + * Copyright (C) 2018-2019 SUSE LLC.
    + */
    +
    +#define _GNU_SOURCE
    +#include <fcntl.h>
    +#include <sys/stat.h>
    +#include <sys/types.h>
    +#include <stdbool.h>
    +#include <string.h>
    +
    +#include "../kselftest.h"
    +#include "helpers.h"
    +
    +static mode_t fdmode(int fd)
    +{
    + char *fdpath;
    + struct stat statbuf;
    + mode_t mode;
    +
    + E_asprintf(&fdpath, "/proc/self/fd/%d", fd);
    + E_fstatat(AT_FDCWD, fdpath, &statbuf, AT_SYMLINK_NOFOLLOW);
    + mode = (statbuf.st_mode & ~S_IFMT);
    + free(fdpath);
    +
    + return mode;
    +}
    +
    +static int reopen_proc(int fd, unsigned int flags)
    +{
    + int ret, saved_errno;
    + char *fdpath;
    +
    + E_asprintf(&fdpath, "/proc/self/fd/%d", fd);
    + ret = open(fdpath, flags);
    + saved_errno = errno;
    + free(fdpath);
    +
    + return ret >= 0 ? ret : -saved_errno;
    +}
    +
    +static int reopen_oemptypath(int fd, unsigned int flags)
    +{
    + int ret = openat(fd, "", O_EMPTYPATH | flags);
    + return ret >= 0 ? ret : -errno;
    +}
    +
    +struct reopen_test {
    + openfunc_t open;
    + mode_t chmod_mode;
    + struct {
    + struct open_how how;
    + mode_t mode;
    + int err;
    + } orig, new;
    +};
    +
    +static bool reopen(int fd, struct reopen_test *test)
    +{
    + int newfd;
    + mode_t proc_mode;
    + bool failed = false;
    +
    + /* Check that the proc mode is correct. */
    + proc_mode = fdmode(fd);
    + if (proc_mode != test->orig.mode) {
    + ksft_print_msg("incorrect fdmode (got[%o] != want[%o])\n",
    + proc_mode, test->orig.mode);
    + failed = true;
    + }
    +
    + /* Re-open through /proc. */
    + newfd = reopen_proc(fd, test->new.how.flags);
    + if (newfd != test->new.err && (newfd < 0 || test->new.err < 0)) {
    + ksft_print_msg("/proc failure (%d != %d [%s])\n",
    + newfd, test->new.err, strerror(-test->new.err));
    + failed = true;
    + }
    + if (newfd >= 0) {
    + proc_mode = fdmode(newfd);
    + if (proc_mode != test->new.mode) {
    + ksft_print_msg("/proc wrong fdmode (got[%o] != want[%o])\n",
    + proc_mode, test->new.mode);
    + failed = true;
    + }
    + close(newfd);
    + }
    +
    + /* Re-open with O_EMPTYPATH. */
    + newfd = reopen_oemptypath(fd, test->new.how.flags);
    + if (newfd != test->new.err && (newfd < 0 || test->new.err < 0)) {
    + ksft_print_msg("O_EMPTYPATH failure (%d != %d [%s])\n",
    + newfd, test->new.err, strerror(-test->new.err));
    + failed = true;
    + }
    + if (newfd >= 0) {
    + proc_mode = fdmode(newfd);
    + if (proc_mode != test->new.mode) {
    + ksft_print_msg("O_EMPTYPATH wrong fdmode (got[%o] != want[%o])\n",
    + proc_mode, test->new.mode);
    + failed = true;
    + }
    + close(newfd);
    + }
    +
    + return failed;
    +}
    +
    +#define NUM_REOPEN_TESTS 28
    +
    +void test_reopen_ordinary(bool privileged)
    +{
    + int fd;
    + int err_access = privileged ? 0 : -EACCES;
    + char tmpfile[] = "/tmp/ksft-openat2-reopen-testfile.XXXXXX";
    +
    + fd = mkstemp(tmpfile);
    + E_assert(fd >= 0, "mkstemp failed: %m\n");
    + close(fd);
    +
    + struct reopen_test tests[] = {
    + /* Re-opening with the same mode should succeed. */
    + { .open = sys_openat, .chmod_mode = 0400,
    + .orig.how.flags = O_RDONLY, .orig.mode = 0500,
    + .new.how.flags = O_RDONLY, .new.mode = 0500 },
    + { .open = sys_openat, .chmod_mode = 0200,
    + .orig.how.flags = O_WRONLY, .orig.mode = 0300,
    + .new.how.flags = O_WRONLY, .new.mode = 0300 },
    + { .open = sys_openat, .chmod_mode = 0600,
    + .orig.how.flags = O_RDWR, .orig.mode = 0700,
    + .new.how.flags = O_RDWR, .new.mode = 0700 },
    + { .open = sys_openat, .chmod_mode = 0600,
    + .orig.how.flags = O_RDWR, .orig.mode = 0700,
    + .new.how.flags = O_RDONLY, .new.mode = 0500 },
    + { .open = sys_openat, .chmod_mode = 0600,
    + .orig.how.flags = O_RDWR, .orig.mode = 0700,
    + .new.how.flags = O_WRONLY, .new.mode = 0300 },
    +
    + /*
    + * Re-opening with a different mode will always fail (with an obvious
    + * carve-out for privileged users).
    + */
    + { .open = sys_openat, .chmod_mode = 0600,
    + .orig.how.flags = O_RDONLY, .orig.mode = 0500,
    + .new.how.flags = O_WRONLY, .new.mode = 0300, .new.err = err_access },
    + { .open = sys_openat, .chmod_mode = 0600,
    + .orig.how.flags = O_WRONLY, .orig.mode = 0300,
    + .new.how.flags = O_RDONLY, .new.mode = 0500, .new.err = err_access },
    + { .open = sys_openat, .chmod_mode = 0600,
    + .orig.how.flags = O_RDONLY, .orig.mode = 0500,
    + .new.how.flags = O_RDWR, .new.mode = 0700, .new.err = err_access },
    + { .open = sys_openat, .chmod_mode = 0600,
    + .orig.how.flags = O_WRONLY, .orig.mode = 0300,
    + .new.how.flags = O_RDWR, .new.mode = 0700, .new.err = err_access },
    +
    + /* Doubly so if they didn't even have permissions at open-time. */
    + { .open = sys_openat, .chmod_mode = 0400,
    + .orig.how.flags = O_RDONLY, .orig.mode = 0500,
    + .new.how.flags = O_WRONLY, .new.mode = 0300, .new.err = err_access },
    + { .open = sys_openat, .chmod_mode = 0200,
    + .orig.how.flags = O_WRONLY, .orig.mode = 0300,
    + .new.how.flags = O_RDONLY, .new.mode = 0500, .new.err = err_access },
    + { .open = sys_openat, .chmod_mode = 0400,
    + .orig.how.flags = O_RDONLY, .orig.mode = 0500,
    + .new.how.flags = O_RDWR, .new.mode = 0700, .new.err = err_access },
    + { .open = sys_openat, .chmod_mode = 0200,
    + .orig.how.flags = O_WRONLY, .orig.mode = 0300,
    + .new.how.flags = O_RDWR, .new.mode = 0700, .new.err = err_access },
    +
    + /* O_PATH re-opens (of ordinary files) will always work. */
    + { .open = sys_openat, .chmod_mode = 0000,
    + .orig.how.flags = O_PATH, .orig.mode = 0070,
    + .new.how.flags = O_WRONLY, .new.mode = 0300 },
    + { .open = sys_openat2, .chmod_mode = 0000,
    + .orig.how.flags = O_PATH, .orig.mode = 0070,
    + .new.how.flags = O_WRONLY, .new.mode = 0300 },
    +
    + { .open = sys_openat, .chmod_mode = 0000,
    + .orig.how.flags = O_PATH, .orig.mode = 0070,
    + .new.how.flags = O_RDONLY, .new.mode = 0500 },
    + { .open = sys_openat2, .chmod_mode = 0000,
    + .orig.how.flags = O_PATH, .orig.mode = 0070,
    + .new.how.flags = O_RDONLY, .new.mode = 0500 },
    +
    + { .open = sys_openat, .chmod_mode = 0000,
    + .orig.how.flags = O_PATH, .orig.mode = 0070,
    + .new.how.flags = O_RDWR, .new.mode = 0700 },
    + { .open = sys_openat2, .chmod_mode = 0000,
    + .orig.how.flags = O_PATH, .orig.mode = 0070,
    + .new.how.flags = O_RDWR, .new.mode = 0700 },
    +
    + /*
    + * openat2(2) UPGRADE_NO* flags. In the privileged case, the re-open
    + * will work but the mode will still be scoped to the mode (or'd with
    + * the open acc_mode).
    + */
    + { .open = sys_openat2, .chmod_mode = 0000,
    + .orig.how.flags = O_PATH, .orig.mode = 0010,
    + .orig.how.upgrade_mask = UPGRADE_NOREAD | UPGRADE_NOWRITE,
    + .new.how.flags = O_RDONLY, .new.mode = 0500, .new.err = err_access },
    + { .open = sys_openat2, .chmod_mode = 0000,
    + .orig.how.flags = O_PATH, .orig.mode = 0010,
    + .orig.how.upgrade_mask = UPGRADE_NOREAD | UPGRADE_NOWRITE,
    + .new.how.flags = O_WRONLY, .new.mode = 0300, .new.err = err_access },
    + { .open = sys_openat2, .chmod_mode = 0000,
    + .orig.how.flags = O_PATH, .orig.mode = 0010,
    + .orig.how.upgrade_mask = UPGRADE_NOREAD | UPGRADE_NOWRITE,
    + .new.how.flags = O_RDWR, .new.mode = 0700, .new.err = err_access },
    +
    + { .open = sys_openat2, .chmod_mode = 0000,
    + .orig.how.flags = O_PATH, .orig.mode = 0050,
    + .orig.how.upgrade_mask = UPGRADE_NOWRITE,
    + .new.how.flags = O_RDONLY, .new.mode = 0500 },
    +
    + { .open = sys_openat2, .chmod_mode = 0000,
    + .orig.how.flags = O_PATH, .orig.mode = 0030,
    + .orig.how.upgrade_mask = UPGRADE_NOREAD,
    + .new.how.flags = O_WRONLY, .new.mode = 0300 },
    +
    + { .open = sys_openat2, .chmod_mode = 0000,
    + .orig.how.flags = O_PATH, .orig.mode = 0030,
    + .orig.how.upgrade_mask = UPGRADE_NOREAD,
    + .new.how.flags = O_RDONLY, .new.mode = 0500, .new.err = err_access },
    + { .open = sys_openat2, .chmod_mode = 0000,
    + .orig.how.flags = O_PATH, .orig.mode = 0050,
    + .orig.how.upgrade_mask = UPGRADE_NOWRITE,
    + .new.how.flags = O_WRONLY, .new.mode = 0300, .new.err = err_access },
    + { .open = sys_openat2, .chmod_mode = 0000,
    + .orig.how.flags = O_PATH, .orig.mode = 0030,
    + .orig.how.upgrade_mask = UPGRADE_NOREAD,
    + .new.how.flags = O_RDWR, .new.mode = 0700, .new.err = err_access },
    + { .open = sys_openat2, .chmod_mode = 0000,
    + .orig.how.flags = O_PATH, .orig.mode = 0050,
    + .orig.how.upgrade_mask = UPGRADE_NOWRITE,
    + .new.how.flags = O_RDWR, .new.mode = 0700, .new.err = err_access },
    + };
    +
    + BUILD_BUG_ON(ARRAY_LEN(tests) != NUM_REOPEN_TESTS);
    +
    + for (int i = 0; i < ARRAY_LEN(tests); i++) {
    + int fd;
    + char *orig_flagset, *new_flagset;
    + struct reopen_test *test = &tests[i];
    + void (*resultfn)(const char *msg, ...) = ksft_test_result_pass;
    +
    + E_chmod(tmpfile, test->chmod_mode);
    +
    + fd = test->open(AT_FDCWD, tmpfile, &test->orig.how);
    + E_assert(fd >= 0, "open '%s' failed: %m\n", tmpfile);
    +
    + /* Make sure that any EACCES we see is not from inode permissions. */
    + E_chmod(tmpfile, 0777);
    +
    + if (reopen(fd, test))
    + resultfn = ksft_test_result_fail;
    +
    + close(fd);
    +
    + new_flagset = openat_flags(test->new.how.flags);
    + if (test->open == sys_openat)
    + orig_flagset = openat_flags(test->orig.how.flags);
    + else if (test->open == sys_openat2)
    + orig_flagset = openat2_flags(&test->orig.how);
    + else
    + ksft_exit_fail_msg("unknown test->open\n");
    +
    + resultfn("%sordinary reopen of (orig[%s]=%s, new=%s) chmod=%.3o %s\n",
    + privileged ? "privileged " : "",
    + test->open == sys_openat ? "openat" : "openat2",
    + orig_flagset, new_flagset, test->chmod_mode,
    + test->new.err < 0 ? strerror(-test->new.err) : "works");
    + fflush(stdout);
    +
    + free(new_flagset);
    + free(orig_flagset);
    + }
    +
    + unlink(tmpfile);
    +}
    +
    +#define NUM_CLOEXEC_TESTS 1
    +
    +void test_openat2_cloexec_test(void)
    +{
    + void (*resultfn)(const char *msg, ...) = ksft_test_result_pass;
    + struct open_how how = {
    + .flags = O_CLOEXEC | O_PATH | O_DIRECTORY,
    + };
    +
    + int fd = sys_openat2(AT_FDCWD, ".", &how);
    + E_assert(fd >= 0, "open '.' failed: %m\n");
    +
    + int flags = fcntl(fd, F_GETFD);
    + E_assert(flags >= 0, "F_GETFD failed: %m\n");
    +
    + if (!(flags & FD_CLOEXEC))
    + resultfn = ksft_test_result_fail;
    +
    + resultfn("openat2(O_CLOEXEC) works as expected\n");
    +}
    +
    +int main(int argc, char **argv)
    +{
    + bool privileged;
    +
    + ksft_print_header();
    + ksft_set_plan(2 * NUM_REOPEN_TESTS + NUM_CLOEXEC_TESTS);
    + test_openat2_supported();
    +
    + /*
    + * Technically we should be checking CAP_DAC_OVERRIDE, but it's easier to
    + * just assume that euid=0 has the full capability set.
    + */
    + privileged = (geteuid() == 0);
    + if (!privileged)
    + ksft_test_result_skip("privileged tests require euid == 0\n");
    + else {
    + test_reopen_ordinary(privileged);
    +
    + E_setresuid(65534, 65534, 65534);
    + privileged = (geteuid() == 0);
    + }
    +
    + test_reopen_ordinary(privileged);
    + test_openat2_cloexec_test();
    +
    + if (ksft_get_fail_cnt() + ksft_get_error_cnt() > 0)
    + ksft_exit_fail();
    + else
    + ksft_exit_pass();
    +}
    diff --git a/tools/testing/selftests/openat2/rename_attack_test.c b/tools/testing/selftests/openat2/rename_attack_test.c
    new file mode 100644
    index 000000000000..39b20ea185d5
    --- /dev/null
    +++ b/tools/testing/selftests/openat2/rename_attack_test.c
    @@ -0,0 +1,127 @@
    +// SPDX-License-Identifier: GPL-2.0-or-later
    +/*
    + * Author: Aleksa Sarai <cyphar@cyphar.com>
    + * Copyright (C) 2018-2019 SUSE LLC.
    + */
    +
    +#define _GNU_SOURCE
    +#include <errno.h>
    +#include <fcntl.h>
    +#include <sched.h>
    +#include <sys/stat.h>
    +#include <sys/types.h>
    +#include <sys/mount.h>
    +#include <sys/mman.h>
    +#include <sys/prctl.h>
    +#include <signal.h>
    +#include <stdio.h>
    +#include <stdlib.h>
    +#include <stdbool.h>
    +#include <string.h>
    +#include <syscall.h>
    +#include <limits.h>
    +#include <unistd.h>
    +
    +#include "../kselftest.h"
    +#include "helpers.h"
    +
    +/* Construct a test directory with the following structure:
    + *
    + * root/
    + * |-- a/
    + * | `-- c/
    + * `-- b/
    + */
    +int setup_testdir(void)
    +{
    + int dfd;
    + char dirname[] = "/tmp/ksft-openat2-rename-attack.XXXXXX";
    +
    + /* Make the top-level directory. */
    + if (!mkdtemp(dirname))
    + ksft_exit_fail_msg("setup_testdir: failed to create tmpdir\n");
    + dfd = open(dirname, O_PATH | O_DIRECTORY);
    + if (dfd < 0)
    + ksft_exit_fail_msg("setup_testdir: failed to open tmpdir\n");
    +
    + E_mkdirat(dfd, "a", 0755);
    + E_mkdirat(dfd, "b", 0755);
    + E_mkdirat(dfd, "a/c", 0755);
    +
    + return dfd;
    +}
    +
    +/* Swap @dirfd/@a and @dirfd/@b constantly. Parent must kill this process. */
    +pid_t spawn_attack(int dirfd, char *a, char *b)
    +{
    + pid_t child = fork();
    + if (child != 0)
    + return child;
    +
    + /* If the parent (the test process) dies, kill ourselves too. */
    + prctl(PR_SET_PDEATHSIG, SIGKILL);
    +
    + /* Swap @a and @b. */
    + for (;;)
    + renameat2(dirfd, a, dirfd, b, RENAME_EXCHANGE);
    + exit(1);
    +}
    +
    +#define NUM_RENAME_TESTS 1
    +#define ROUNDS 400000
    +
    +void test_rename_attack(void)
    +{
    + int dfd, afd, escaped_count = 0;
    + void (*resultfn)(const char *msg, ...) = ksft_test_result_pass;
    + pid_t child;
    +
    + dfd = setup_testdir();
    + afd = openat(dfd, "a", O_PATH);
    + if (afd < 0)
    + ksft_exit_fail_msg("test_rename_attack: failed to open 'a'\n");
    +
    + child = spawn_attack(dfd, "a/c", "b");
    +
    + for (int i = 0; i < ROUNDS; i++) {
    + int fd;
    + bool failed;
    + struct open_how how = {
    + .flags = O_PATH,
    + .resolve = RESOLVE_IN_ROOT,
    + };
    + char *victim_path = "c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../../c/../..";
    +
    + fd = sys_openat2(afd, victim_path, &how);
    + if (fd < 0)
    + failed = (fd != -EXDEV);
    + else
    + failed = !fdequal(fd, afd, NULL);
    +
    + escaped_count += failed;
    + close(fd);
    + }
    +
    + if (escaped_count > 0)
    + resultfn = ksft_test_result_fail;
    +
    + resultfn("rename attack fails (expected 0 breakouts in %d runs, got %d)\n",
    + ROUNDS, escaped_count);
    +
    + /* Should be killed anyway, but might as well make sure. */
    + kill(child, SIGKILL);
    +}
    +
    +int main(int argc, char **argv)
    +{
    + ksft_print_header();
    + ksft_set_plan(NUM_RENAME_TESTS);
    + test_openat2_supported();
    +
    + test_rename_attack();
    +
    + if (ksft_get_fail_cnt() + ksft_get_error_cnt() > 0)
    + ksft_exit_fail();
    + else
    + ksft_exit_pass();
    +}
    diff --git a/tools/testing/selftests/openat2/resolve_test.c b/tools/testing/selftests/openat2/resolve_test.c
    new file mode 100644
    index 000000000000..8ef3dbb7edbe
    --- /dev/null
    +++ b/tools/testing/selftests/openat2/resolve_test.c
    @@ -0,0 +1,402 @@
    +// SPDX-License-Identifier: GPL-2.0-or-later
    +/*
    + * Author: Aleksa Sarai <cyphar@cyphar.com>
    + * Copyright (C) 2018-2019 SUSE LLC.
    + */
    +
    +#define _GNU_SOURCE
    +#include <fcntl.h>
    +#include <sched.h>
    +#include <sys/stat.h>
    +#include <sys/types.h>
    +#include <sys/mount.h>
    +#include <stdlib.h>
    +#include <stdbool.h>
    +#include <string.h>
    +
    +#include "../kselftest.h"
    +#include "helpers.h"
    +
    +/*
    + * Construct a test directory with the following structure:
    + *
    + * root/
    + * |-- procexe -> /proc/self/exe
    + * |-- procroot -> /proc/self/root
    + * |-- root/
    + * |-- mnt/ [mountpoint]
    + * | |-- self -> ../mnt/
    + * | `-- absself -> /mnt/
    + * |-- etc/
    + * | `-- passwd
    + * |-- creatlink -> /newfile3
    + * |-- relsym -> etc/passwd
    + * |-- abssym -> /etc/passwd
    + * |-- abscheeky -> /cheeky
    + * |-- abscheeky -> /cheeky
    + * `-- cheeky/
    + * |-- absself -> /
    + * |-- self -> ../../root/
    + * |-- garbageself -> /../../root/
    + * |-- passwd -> ../cheeky/../cheeky/../etc/../etc/passwd
    + * |-- abspasswd -> /../cheeky/../cheeky/../etc/../etc/passwd
    + * |-- dotdotlink -> ../../../../../../../../../../../../../../etc/passwd
    + * `-- garbagelink -> /../../../../../../../../../../../../../../etc/passwd
    + */
    +int setup_testdir(void)
    +{
    + int dfd, tmpfd;
    + char dirname[] = "/tmp/ksft-openat2-testdir.XXXXXX";
    +
    + /* Unshare and make /tmp a new directory. */
    + E_unshare(CLONE_NEWNS);
    + E_mount("", "/tmp", "", MS_PRIVATE, "");
    +
    + /* Make the top-level directory. */
    + if (!mkdtemp(dirname))
    + ksft_exit_fail_msg("setup_testdir: failed to create tmpdir\n");
    + dfd = open(dirname, O_PATH | O_DIRECTORY);
    + if (dfd < 0)
    + ksft_exit_fail_msg("setup_testdir: failed to open tmpdir\n");
    +
    + /* A sub-directory which is actually used for tests. */
    + E_mkdirat(dfd, "root", 0755);
    + tmpfd = openat(dfd, "root", O_PATH | O_DIRECTORY);
    + if (tmpfd < 0)
    + ksft_exit_fail_msg("setup_testdir: failed to open tmpdir\n");
    + close(dfd);
    + dfd = tmpfd;
    +
    + E_symlinkat("/proc/self/exe", dfd, "procexe");
    + E_symlinkat("/proc/self/root", dfd, "procroot");
    + E_mkdirat(dfd, "root", 0755);
    +
    + /* There is no mountat(2), so use chdir. */
    + E_mkdirat(dfd, "mnt", 0755);
    + E_fchdir(dfd);
    + E_mount("tmpfs", "./mnt", "tmpfs", MS_NOSUID | MS_NODEV, "");
    + E_symlinkat("../mnt/", dfd, "mnt/self");
    + E_symlinkat("/mnt/", dfd, "mnt/absself");
    +
    + E_mkdirat(dfd, "etc", 0755);
    + E_touchat(dfd, "etc/passwd");
    +
    + E_symlinkat("/newfile3", dfd, "creatlink");
    + E_symlinkat("etc/passwd", dfd, "relsym");
    + E_symlinkat("/etc/passwd", dfd, "abssym");
    + E_symlinkat("/cheeky", dfd, "abscheeky");
    +
    + E_mkdirat(dfd, "cheeky", 0755);
    +
    + E_symlinkat("/", dfd, "cheeky/absself");
    + E_symlinkat("../../root/", dfd, "cheeky/self");
    + E_symlinkat("/../../root/", dfd, "cheeky/garbageself");
    +
    + E_symlinkat("../cheeky/../etc/../etc/passwd", dfd, "cheeky/passwd");
    + E_symlinkat("/../cheeky/../etc/../etc/passwd", dfd, "cheeky/abspasswd");
    +
    + E_symlinkat("../../../../../../../../../../../../../../etc/passwd",
    + dfd, "cheeky/dotdotlink");
    + E_symlinkat("/../../../../../../../../../../../../../../etc/passwd",
    + dfd, "cheeky/garbagelink");
    +
    + return dfd;
    +}
    +
    +struct basic_test {
    + const char *dir;
    + const char *path;
    + struct open_how how;
    + bool pass;
    + union {
    + int err;
    + const char *path;
    + } out;
    +};
    +
    +#define NUM_OPENAT2_OPATH_TESTS 84
    +
    +void test_openat2_opath_tests(void)
    +{
    + int rootfd;
    + char *procselfexe;
    +
    + E_asprintf(&procselfexe, "/proc/%d/exe", getpid());
    + rootfd = setup_testdir();
    +
    + struct basic_test tests[] = {
    + /** RESOLVE_BENEATH **/
    + /* Attempts to cross dirfd should be blocked. */
    + { .path = "/", .how.resolve = RESOLVE_BENEATH,
    + .out.err = -EXDEV, .pass = false },
    + { .path = "cheeky/absself", .how.resolve = RESOLVE_BENEATH,
    + .out.err = -EXDEV, .pass = false },
    + { .path = "abscheeky/absself", .how.resolve = RESOLVE_BENEATH,
    + .out.err = -EXDEV, .pass = false },
    + { .path = "..", .how.resolve = RESOLVE_BENEATH,
    + .out.err = -EXDEV, .pass = false },
    + { .path = "../root/", .how.resolve = RESOLVE_BENEATH,
    + .out.err = -EXDEV, .pass = false },
    + { .path = "cheeky/self", .how.resolve = RESOLVE_BENEATH,
    + .out.err = -EXDEV, .pass = false },
    + { .path = "abscheeky/self", .how.resolve = RESOLVE_BENEATH,
    + .out.err = -EXDEV, .pass = false },
    + { .path = "cheeky/garbageself", .how.resolve = RESOLVE_BENEATH,
    + .out.err = -EXDEV, .pass = false },
    + { .path = "abscheeky/garbageself", .how.resolve = RESOLVE_BENEATH,
    + .out.err = -EXDEV, .pass = false },
    + /* Only relative paths that stay inside dirfd should work. */
    + { .path = "root", .how.resolve = RESOLVE_BENEATH,
    + .out.path = "root", .pass = true },
    + { .path = "etc", .how.resolve = RESOLVE_BENEATH,
    + .out.path = "etc", .pass = true },
    + { .path = "etc/passwd", .how.resolve = RESOLVE_BENEATH,
    + .out.path = "etc/passwd", .pass = true },
    + { .path = "relsym", .how.resolve = RESOLVE_BENEATH,
    + .out.path = "etc/passwd", .pass = true },
    + { .path = "cheeky/passwd", .how.resolve = RESOLVE_BENEATH,
    + .out.path = "etc/passwd", .pass = true },
    + { .path = "abscheeky/passwd", .how.resolve = RESOLVE_BENEATH,
    + .out.err = -EXDEV, .pass = false },
    + { .path = "abssym", .how.resolve = RESOLVE_BENEATH,
    + .out.err = -EXDEV, .pass = false },
    + { .path = "/etc/passwd", .how.resolve = RESOLVE_BENEATH,
    + .out.err = -EXDEV, .pass = false },
    + { .path = "cheeky/abspasswd", .how.resolve = RESOLVE_BENEATH,
    + .out.err = -EXDEV, .pass = false },
    + { .path = "abscheeky/abspasswd", .how.resolve = RESOLVE_BENEATH,
    + .out.err = -EXDEV, .pass = false },
    + /* Tricky paths should fail. */
    + { .path = "cheeky/dotdotlink", .how.resolve = RESOLVE_BENEATH,
    + .out.err = -EXDEV, .pass = false },
    + { .path = "abscheeky/dotdotlink", .how.resolve = RESOLVE_BENEATH,
    + .out.err = -EXDEV, .pass = false },
    + { .path = "cheeky/garbagelink", .how.resolve = RESOLVE_BENEATH,
    + .out.err = -EXDEV, .pass = false },
    + { .path = "abscheeky/garbagelink", .how.resolve = RESOLVE_BENEATH,
    + .out.err = -EXDEV, .pass = false },
    +
    + /** RESOLVE_IN_ROOT **/
    + /* All attempts to cross the dirfd will be scoped-to-root. */
    + { .path = "/", .how.resolve = RESOLVE_IN_ROOT,
    + .out.path = NULL, .pass = true },
    + { .path = "cheeky/absself", .how.resolve = RESOLVE_IN_ROOT,
    + .out.path = NULL, .pass = true },
    + { .path = "abscheeky/absself", .how.resolve = RESOLVE_IN_ROOT,
    + .out.path = NULL, .pass = true },
    + { .path = "..", .how.resolve = RESOLVE_IN_ROOT,
    + .out.path = NULL, .pass = true },
    + { .path = "../root/", .how.resolve = RESOLVE_IN_ROOT,
    + .out.path = "root", .pass = true },
    + { .path = "../root/", .how.resolve = RESOLVE_IN_ROOT,
    + .out.path = "root", .pass = true },
    + { .path = "cheeky/self", .how.resolve = RESOLVE_IN_ROOT,
    + .out.path = "root", .pass = true },
    + { .path = "cheeky/garbageself", .how.resolve = RESOLVE_IN_ROOT,
    + .out.path = "root", .pass = true },
    + { .path = "abscheeky/garbageself", .how.resolve = RESOLVE_IN_ROOT,
    + .out.path = "root", .pass = true },
    + { .path = "root", .how.resolve = RESOLVE_IN_ROOT,
    + .out.path = "root", .pass = true },
    + { .path = "etc", .how.resolve = RESOLVE_IN_ROOT,
    + .out.path = "etc", .pass = true },
    + { .path = "etc/passwd", .how.resolve = RESOLVE_IN_ROOT,
    + .out.path = "etc/passwd", .pass = true },
    + { .path = "relsym", .how.resolve = RESOLVE_IN_ROOT,
    + .out.path = "etc/passwd", .pass = true },
    + { .path = "cheeky/passwd", .how.resolve = RESOLVE_IN_ROOT,
    + .out.path = "etc/passwd", .pass = true },
    + { .path = "abscheeky/passwd", .how.resolve = RESOLVE_IN_ROOT,
    + .out.path = "etc/passwd", .pass = true },
    + { .path = "abssym", .how.resolve = RESOLVE_IN_ROOT,
    + .out.path = "etc/passwd", .pass = true },
    + { .path = "/etc/passwd", .how.resolve = RESOLVE_IN_ROOT,
    + .out.path = "etc/passwd", .pass = true },
    + { .path = "cheeky/abspasswd", .how.resolve = RESOLVE_IN_ROOT,
    + .out.path = "etc/passwd", .pass = true },
    + { .path = "abscheeky/abspasswd", .how.resolve = RESOLVE_IN_ROOT,
    + .out.path = "etc/passwd", .pass = true },
    + { .path = "cheeky/dotdotlink", .how.resolve = RESOLVE_IN_ROOT,
    + .out.path = "etc/passwd", .pass = true },
    + { .path = "abscheeky/dotdotlink", .how.resolve = RESOLVE_IN_ROOT,
    + .out.path = "etc/passwd", .pass = true },
    + { .path = "/../../../../abscheeky/dotdotlink", .how.resolve = RESOLVE_IN_ROOT,
    + .out.path = "etc/passwd", .pass = true },
    + { .path = "cheeky/garbagelink", .how.resolve = RESOLVE_IN_ROOT,
    + .out.path = "etc/passwd", .pass = true },
    + { .path = "abscheeky/garbagelink", .how.resolve = RESOLVE_IN_ROOT,
    + .out.path = "etc/passwd", .pass = true },
    + { .path = "/../../../../abscheeky/garbagelink", .how.resolve = RESOLVE_IN_ROOT,
    + .out.path = "etc/passwd", .pass = true },
    + /* O_CREAT should handle trailing symlinks correctly. */
    + { .path = "newfile1", .how.flags = O_CREAT,
    + .how.mode = 0700,
    + .how.resolve = RESOLVE_IN_ROOT,
    + .out.path = "newfile1", .pass = true },
    + { .path = "/newfile2", .how.flags = O_CREAT,
    + .how.mode = 0700,
    + .how.resolve = RESOLVE_IN_ROOT,
    + .out.path = "newfile2", .pass = true },
    + { .path = "/creatlink", .how.flags = O_CREAT,
    + .how.mode = 0700,
    + .how.resolve = RESOLVE_IN_ROOT,
    + .out.path = "newfile3", .pass = true },
    +
    + /** RESOLVE_NO_XDEV **/
    + /* Crossing *down* into a mountpoint is disallowed. */
    + { .path = "mnt", .how.resolve = RESOLVE_NO_XDEV,
    + .out.err = -EXDEV, .pass = false },
    + { .path = "mnt/", .how.resolve = RESOLVE_NO_XDEV,
    + .out.err = -EXDEV, .pass = false },
    + { .path = "mnt/.", .how.resolve = RESOLVE_NO_XDEV,
    + .out.err = -EXDEV, .pass = false },
    + /* Crossing *up* out of a mountpoint is disallowed. */
    + { .dir = "mnt", .path = ".", .how.resolve = RESOLVE_NO_XDEV,
    + .out.path = "mnt", .pass = true },
    + { .dir = "mnt", .path = "..", .how.resolve = RESOLVE_NO_XDEV,
    + .out.err = -EXDEV, .pass = false },
    + { .dir = "mnt", .path = "../mnt", .how.resolve = RESOLVE_NO_XDEV,
    + .out.err = -EXDEV, .pass = false },
    + { .dir = "mnt", .path = "self", .how.resolve = RESOLVE_NO_XDEV,
    + .out.err = -EXDEV, .pass = false },
    + { .dir = "mnt", .path = "absself", .how.resolve = RESOLVE_NO_XDEV,
    + .out.err = -EXDEV, .pass = false },
    + /* Jumping to "/" is ok, but later components cannot cross. */
    + { .dir = "mnt", .path = "/", .how.resolve = RESOLVE_NO_XDEV,
    + .out.path = "/", .pass = true },
    + { .dir = "/", .path = "/", .how.resolve = RESOLVE_NO_XDEV,
    + .out.path = "/", .pass = true },
    + { .path = "/proc/1", .how.resolve = RESOLVE_NO_XDEV,
    + .out.err = -EXDEV, .pass = false },
    + { .path = "/tmp", .how.resolve = RESOLVE_NO_XDEV,
    + .out.err = -EXDEV, .pass = false },
    +
    + /** RESOLVE_NO_MAGICLINKS **/
    + /* Regular symlinks should work. */
    + { .path = "relsym", .how.resolve = RESOLVE_NO_MAGICLINKS,
    + .out.path = "etc/passwd", .pass = true },
    + /* Magic-links should not work. */
    + { .path = "procexe", .how.resolve = RESOLVE_NO_MAGICLINKS,
    + .out.err = -ELOOP, .pass = false },
    + { .path = "/proc/self/exe", .how.resolve = RESOLVE_NO_MAGICLINKS,
    + .out.err = -ELOOP, .pass = false },
    + { .path = "procroot/etc", .how.resolve = RESOLVE_NO_MAGICLINKS,
    + .out.err = -ELOOP, .pass = false },
    + { .path = "/proc/self/root/etc", .how.resolve = RESOLVE_NO_MAGICLINKS,
    + .out.err = -ELOOP, .pass = false },
    + { .path = "/proc/self/root/etc", .how.flags = O_NOFOLLOW,
    + .how.resolve = RESOLVE_NO_MAGICLINKS,
    + .out.err = -ELOOP, .pass = false },
    + { .path = "/proc/self/exe", .how.flags = O_NOFOLLOW,
    + .how.resolve = RESOLVE_NO_MAGICLINKS,
    + .out.path = procselfexe, .pass = true },
    +
    + /** RESOLVE_NO_SYMLINKS **/
    + /* Normal paths should work. */
    + { .path = ".", .how.resolve = RESOLVE_NO_SYMLINKS,
    + .out.path = NULL, .pass = true },
    + { .path = "root", .how.resolve = RESOLVE_NO_SYMLINKS,
    + .out.path = "root", .pass = true },
    + { .path = "etc", .how.resolve = RESOLVE_NO_SYMLINKS,
    + .out.path = "etc", .pass = true },
    + { .path = "etc/passwd", .how.resolve = RESOLVE_NO_SYMLINKS,
    + .out.path = "etc/passwd", .pass = true },
    + /* Regular symlinks are blocked. */
    + { .path = "relsym", .how.resolve = RESOLVE_NO_SYMLINKS,
    + .out.err = -ELOOP, .pass = false },
    + { .path = "abssym", .how.resolve = RESOLVE_NO_SYMLINKS,
    + .out.err = -ELOOP, .pass = false },
    + { .path = "cheeky/garbagelink", .how.resolve = RESOLVE_NO_SYMLINKS,
    + .out.err = -ELOOP, .pass = false },
    + { .path = "abscheeky/garbagelink", .how.resolve = RESOLVE_NO_SYMLINKS,
    + .out.err = -ELOOP, .pass = false },
    + { .path = "abscheeky/absself", .how.resolve = RESOLVE_NO_SYMLINKS,
    + .out.err = -ELOOP, .pass = false },
    + /* Trailing symlinks with NO_FOLLOW. */
    + { .path = "relsym", .how.flags = O_NOFOLLOW,
    + .how.resolve = RESOLVE_NO_SYMLINKS,
    + .out.path = "relsym", .pass = true },
    + { .path = "abssym", .how.flags = O_NOFOLLOW,
    + .how.resolve = RESOLVE_NO_SYMLINKS,
    + .out.path = "abssym", .pass = true },
    + { .path = "cheeky/garbagelink", .how.flags = O_NOFOLLOW,
    + .how.resolve = RESOLVE_NO_SYMLINKS,
    + .out.path = "cheeky/garbagelink", .pass = true },
    + { .path = "abscheeky/garbagelink", .how.flags = O_NOFOLLOW,
    + .how.resolve = RESOLVE_NO_SYMLINKS,
    + .out.err = -ELOOP, .pass = false },
    + { .path = "abscheeky/absself", .how.flags = O_NOFOLLOW,
    + .how.resolve = RESOLVE_NO_SYMLINKS,
    + .out.err = -ELOOP, .pass = false },
    + };
    +
    + BUILD_BUG_ON(ARRAY_LEN(tests) != NUM_OPENAT2_OPATH_TESTS);
    +
    + for (int i = 0; i < ARRAY_LEN(tests); i++) {
    + int dfd, fd;
    + bool failed;
    + void (*resultfn)(const char *msg, ...) = ksft_test_result_pass;
    + struct basic_test *test = &tests[i];
    + char *flagstr;
    +
    + /* Auto-set O_PATH. */
    + if (!(test->how.flags & O_CREAT))
    + test->how.flags |= O_PATH;
    + flagstr = openat2_flags(&test->how);
    +
    + if (test->dir)
    + dfd = openat(rootfd, test->dir, O_PATH | O_DIRECTORY);
    + else
    + dfd = dup(rootfd);
    + if (dfd < 0) {
    + resultfn = ksft_test_result_error;
    + goto next;
    + }
    +
    + fd = sys_openat2(dfd, test->path, &test->how);
    + if (test->pass)
    + failed = (fd < 0 || !fdequal(fd, rootfd, test->out.path));
    + else
    + failed = (fd != test->out.err);
    + if (fd >= 0)
    + close(fd);
    + close(dfd);
    +
    + if (failed)
    + resultfn = ksft_test_result_fail;
    +
    +next:
    + if (test->pass)
    + resultfn("openat2(root[%s], %s, %s) ==> %s\n",
    + test->dir ?: ".", test->path, flagstr,
    + test->out.path ?: ".");
    + else
    + resultfn("openat2(root[%s], %s, %s) ==> %d (%s)\n",
    + test->dir ?: ".", test->path, flagstr,
    + test->out.err, strerror(-test->out.err));
    + fflush(stdout);
    +
    + free(flagstr);
    + }
    +
    + free(procselfexe);
    + close(rootfd);
    +}
    +
    +int main(int argc, char **argv)
    +{
    + ksft_print_header();
    + ksft_set_plan(NUM_OPENAT2_OPATH_TESTS);
    + test_openat2_supported();
    +
    + /* NOTE: We should be checking for CAP_SYS_ADMIN here... */
    + if (geteuid() != 0)
    + ksft_exit_skip("openat2(2) tests require euid == 0\n");
    +
    + test_openat2_opath_tests();
    +
    + if (ksft_get_fail_cnt() + ksft_get_error_cnt() > 0)
    + ksft_exit_fail();
    + else
    + ksft_exit_pass();
    +}
    --
    2.22.0

    \
     
     \ /
      Last update: 2019-08-20 05:40    [W:4.927 / U:0.068 seconds]
    ©2003-2020 Jasper Spaans|hosted at Digital Ocean and TransIP|Read the blog|Advertise on this site