aboutsummaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
Diffstat (limited to 'tools')
-rw-r--r--tools/testing/selftests/Makefile3
-rw-r--r--tools/testing/selftests/filesystems/.gitignore1
-rw-r--r--tools/testing/selftests/filesystems/Makefile2
-rw-r--r--tools/testing/selftests/filesystems/empty_mntns/.gitignore3
-rw-r--r--tools/testing/selftests/filesystems/empty_mntns/Makefile5
-rw-r--r--tools/testing/selftests/filesystems/empty_mntns/internal_sb_reconfigure_test.c108
-rw-r--r--tools/testing/selftests/filesystems/empty_mntns/nullfs_atime_test.c129
-rw-r--r--tools/testing/selftests/filesystems/empty_mntns/root_readdir_test.c45
-rw-r--r--tools/testing/selftests/filesystems/mntns_unbindable/Makefile6
-rw-r--r--tools/testing/selftests/filesystems/mntns_unbindable/mntns_unbindable_test.c227
-rw-r--r--tools/testing/selftests/filesystems/mount_cycle/.gitignore8
-rw-r--r--tools/testing/selftests/filesystems/mount_cycle/Makefile12
-rw-r--r--tools/testing/selftests/filesystems/mount_cycle/config39
-rw-r--r--tools/testing/selftests/filesystems/mount_cycle/locked_handle_test.c377
-rw-r--r--tools/testing/selftests/filesystems/mount_cycle/loop_cycle_test.c1542
-rw-r--r--tools/testing/selftests/filesystems/mount_cycle/mount_cover_test.c579
-rw-r--r--tools/testing/selftests/filesystems/mount_cycle/nsfs_rbind_loop_test.c193
-rw-r--r--tools/testing/selftests/filesystems/mount_cycle/overmount_ns_file_test.c190
-rw-r--r--tools/testing/selftests/filesystems/mount_cycle/overmount_reparent_test.c181
-rw-r--r--tools/testing/selftests/filesystems/mount_cycle/settings1
-rw-r--r--tools/testing/selftests/filesystems/mount_cycle/unmounted_tree_test.c484
-rw-r--r--tools/testing/selftests/filesystems/open_tree_ns/.gitignore1
-rw-r--r--tools/testing/selftests/filesystems/open_tree_ns/Makefile2
-rw-r--r--tools/testing/selftests/filesystems/open_tree_ns/open_tree_ns_covered_test.c195
-rw-r--r--tools/testing/selftests/filesystems/overlayfs/.gitignore1
-rw-r--r--tools/testing/selftests/filesystems/overlayfs/Makefile1
-rw-r--r--tools/testing/selftests/filesystems/overlayfs/automount_in_layer.c174
-rw-r--r--tools/testing/selftests/filesystems/readdir_hold.h224
-rw-r--r--tools/testing/selftests/filesystems/rw_hint_test.c129
-rw-r--r--tools/testing/selftests/filesystems/umount_propagation/Makefile6
-rw-r--r--tools/testing/selftests/filesystems/umount_propagation/locked_mount_test.c432
-rw-r--r--tools/testing/selftests/filesystems/umount_propagation/shrink_submounts_test.c211
-rw-r--r--tools/testing/selftests/filesystems/umount_propagation/umount_propagation_test.c226
-rw-r--r--tools/testing/selftests/move_mount_set_group/move_mount_set_group_test.c74
34 files changed, 5803 insertions, 8 deletions
diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
index 273853937c25..bc4fdb6f5337 100644
--- a/tools/testing/selftests/Makefile
+++ b/tools/testing/selftests/Makefile
@@ -43,6 +43,7 @@ TARGETS += filesystems/open_tree_ns
TARGETS += filesystems/overlayfs
TARGETS += filesystems/statmount
TARGETS += filesystems/mount-notify
+TARGETS += filesystems/mount_cycle
TARGETS += filesystems/nsfs
TARGETS += filesystems/fuse
TARGETS += filesystems/move_mount
@@ -50,6 +51,8 @@ TARGETS += filesystems/empty_mntns
TARGETS += filesystems/fsmount_ns
TARGETS += filesystems/fscontext_ns
TARGETS += filesystems/xattr
+TARGETS += filesystems/mntns_unbindable
+TARGETS += filesystems/umount_propagation
TARGETS += firmware
TARGETS += fpu
TARGETS += ftrace
diff --git a/tools/testing/selftests/filesystems/.gitignore b/tools/testing/selftests/filesystems/.gitignore
index 9eb185fb2f9d..62f7b1c46649 100644
--- a/tools/testing/selftests/filesystems/.gitignore
+++ b/tools/testing/selftests/filesystems/.gitignore
@@ -7,3 +7,4 @@ anon_inode_test
kernfs_test
idmapped_tmpfile
ustat_test
+rw_hint_test
diff --git a/tools/testing/selftests/filesystems/Makefile b/tools/testing/selftests/filesystems/Makefile
index 03be337c1f35..93e2cc9123b0 100644
--- a/tools/testing/selftests/filesystems/Makefile
+++ b/tools/testing/selftests/filesystems/Makefile
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: GPL-2.0
CFLAGS += $(KHDR_INCLUDES)
-TEST_GEN_PROGS := devpts_pts file_stressor anon_inode_test kernfs_test fclog ustat_test
+TEST_GEN_PROGS := devpts_pts file_stressor anon_inode_test kernfs_test fclog ustat_test rw_hint_test
TEST_GEN_PROGS += idmapped_tmpfile
TEST_GEN_PROGS_EXTENDED := dnotify_test
diff --git a/tools/testing/selftests/filesystems/empty_mntns/.gitignore b/tools/testing/selftests/filesystems/empty_mntns/.gitignore
index 99f89d329db2..27bcbcaeb1d1 100644
--- a/tools/testing/selftests/filesystems/empty_mntns/.gitignore
+++ b/tools/testing/selftests/filesystems/empty_mntns/.gitignore
@@ -2,3 +2,6 @@
clone3_empty_mntns_test
empty_mntns_test
overmount_chroot_test
+internal_sb_reconfigure_test
+nullfs_atime_test
+root_readdir_test
diff --git a/tools/testing/selftests/filesystems/empty_mntns/Makefile b/tools/testing/selftests/filesystems/empty_mntns/Makefile
index 22e3fb915e81..22af2164c54e 100644
--- a/tools/testing/selftests/filesystems/empty_mntns/Makefile
+++ b/tools/testing/selftests/filesystems/empty_mntns/Makefile
@@ -4,9 +4,14 @@ CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES) $(TOOLS_INCLUDES)
LDLIBS += -lcap
TEST_GEN_PROGS := empty_mntns_test overmount_chroot_test clone3_empty_mntns_test
+TEST_GEN_PROGS += internal_sb_reconfigure_test nullfs_atime_test root_readdir_test
+
+LOCAL_HDRS += ../readdir_hold.h
include ../../lib.mk
$(OUTPUT)/empty_mntns_test: ../utils.c
$(OUTPUT)/overmount_chroot_test: ../utils.c
$(OUTPUT)/clone3_empty_mntns_test: ../utils.c
+$(OUTPUT)/internal_sb_reconfigure_test: ../utils.c
+$(OUTPUT)/root_readdir_test: LDLIBS += -pthread
diff --git a/tools/testing/selftests/filesystems/empty_mntns/internal_sb_reconfigure_test.c b/tools/testing/selftests/filesystems/empty_mntns/internal_sb_reconfigure_test.c
new file mode 100644
index 000000000000..cb645d1e5a9a
--- /dev/null
+++ b/tools/testing/selftests/filesystems/empty_mntns/internal_sb_reconfigure_test.c
@@ -0,0 +1,108 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * The root of an empty mount namespace is a nullfs mount. Its superblock is
+ * kernel-internal and shared by every mount namespace. It can't be
+ * reconfigured, neither through fspick() nor through mount(MS_REMOUNT) nor
+ * through umount() of the root which remounts it read-only.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/mount.h>
+#include <sys/statfs.h>
+#include <sys/statvfs.h>
+#include <sys/syscall.h>
+#include <sys/vfs.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "../utils.h"
+#include "../wrappers.h"
+#include "empty_mntns.h"
+#include "kselftest_harness.h"
+
+#ifndef __NR_fspick
+#define __NR_fspick 433
+#endif
+
+static int sys_fspick(int dfd, const char *path, unsigned int flags)
+{
+ return syscall(__NR_fspick, dfd, path, flags);
+}
+
+/* Child exit codes. */
+enum {
+ CHILD_OK,
+ CHILD_USERNS, /* could not create the user namespace */
+ CHILD_UNSHARE, /* could not create the empty mount namespace */
+ CHILD_FSPICK, /* fspick() of the root was not refused with EINVAL */
+ CHILD_REMOUNT, /* mount(MS_REMOUNT) was not refused with EINVAL */
+ CHILD_UMOUNT, /* umount() of the root succeeded */
+ CHILD_STATFS, /* statfs() of the root failed */
+ CHILD_RDONLY, /* the root ended up read-only */
+};
+
+static int empty_mntns_child(void)
+{
+ struct statfs st;
+
+ if (enter_userns())
+ return CHILD_USERNS;
+ if (unshare(UNSHARE_EMPTY_MNTNS))
+ return CHILD_UNSHARE;
+
+ if (sys_fspick(AT_FDCWD, "/", 0) >= 0 || errno != EINVAL)
+ return CHILD_FSPICK;
+ if (!mount(NULL, "/", NULL, MS_REMOUNT | MS_RDONLY, NULL) ||
+ errno != EINVAL)
+ return CHILD_REMOUNT;
+ if (!umount2("/", 0))
+ return CHILD_UMOUNT;
+ if (statfs("/", &st))
+ return CHILD_STATFS;
+ if (st.f_flags & ST_RDONLY)
+ return CHILD_RDONLY;
+ return CHILD_OK;
+}
+
+FIXTURE(internal_sb_reconfigure) {};
+
+FIXTURE_SETUP(internal_sb_reconfigure)
+{
+ pid_t pid;
+ int status;
+
+ pid = fork();
+ ASSERT_GE(pid, 0);
+ if (pid == 0) {
+ if (enter_userns())
+ _exit(1);
+ if (unshare(UNSHARE_EMPTY_MNTNS))
+ _exit(1);
+ _exit(0);
+ }
+ ASSERT_EQ(waitpid(pid, &status, 0), pid);
+ if (!WIFEXITED(status) || WEXITSTATUS(status))
+ SKIP(return, "UNSHARE_EMPTY_MNTNS not supported");
+}
+
+FIXTURE_TEARDOWN(internal_sb_reconfigure) {}
+
+TEST_F(internal_sb_reconfigure, nullfs_root)
+{
+ pid_t pid;
+ int status;
+
+ pid = fork();
+ ASSERT_GE(pid, 0);
+ if (pid == 0)
+ _exit(empty_mntns_child());
+ ASSERT_EQ(waitpid(pid, &status, 0), pid);
+ ASSERT_TRUE(WIFEXITED(status));
+ ASSERT_EQ(WEXITSTATUS(status), CHILD_OK);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/filesystems/empty_mntns/nullfs_atime_test.c b/tools/testing/selftests/filesystems/empty_mntns/nullfs_atime_test.c
new file mode 100644
index 000000000000..51e34f3c4f54
--- /dev/null
+++ b/tools/testing/selftests/filesystems/empty_mntns/nullfs_atime_test.c
@@ -0,0 +1,129 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * The root of every empty mount namespace is the same nullfs inode. A read
+ * of it by one user must not change the access time another user sees.
+ */
+#define _GNU_SOURCE
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+
+#include "../../kselftest_harness.h"
+
+#ifndef UNSHARE_EMPTY_MNTNS
+#define UNSHARE_EMPTY_MNTNS 0x00100000
+#endif
+
+enum {
+ CHILD_OK,
+ CHILD_UNSUPPORTED,
+ CHILD_SETUP,
+ CHILD_CHANGED,
+};
+
+static int wait_byte(int fd)
+{
+ char c;
+
+ return read(fd, &c, 1) == 1 ? 0 : -1;
+}
+
+static int send_byte(int fd)
+{
+ return write(fd, "x", 1) == 1 ? 0 : -1;
+}
+
+static int empty_mntns(void)
+{
+ if (!unshare(UNSHARE_EMPTY_MNTNS))
+ return 0;
+ return errno == EINVAL ? CHILD_UNSUPPORTED : CHILD_SETUP;
+}
+
+/* the watcher: stats its root before and after the reader read its own */
+static int watcher(int to_reader, int from_reader)
+{
+ struct stat before, after;
+ int ret;
+
+ ret = empty_mntns();
+ if (ret)
+ return ret;
+ if (stat("/", &before))
+ return CHILD_SETUP;
+ if (send_byte(to_reader) || wait_byte(from_reader))
+ return CHILD_SETUP;
+ if (stat("/", &after))
+ return CHILD_SETUP;
+ if (before.st_atim.tv_sec != after.st_atim.tv_sec ||
+ before.st_atim.tv_nsec != after.st_atim.tv_nsec)
+ return CHILD_CHANGED;
+ return CHILD_OK;
+}
+
+/* the reader: lists its own root, which is the same inode */
+static int reader(int to_watcher, int from_watcher)
+{
+ struct dirent *de;
+ DIR *d;
+ int ret;
+
+ ret = empty_mntns();
+ if (ret)
+ return ret;
+ if (wait_byte(from_watcher))
+ return CHILD_SETUP;
+ d = opendir("/");
+ if (!d)
+ return CHILD_SETUP;
+ while ((de = readdir(d)))
+ ;
+ closedir(d);
+ return send_byte(to_watcher) ? CHILD_SETUP : CHILD_OK;
+}
+
+static int wait_child(pid_t pid)
+{
+ int status;
+
+ if (waitpid(pid, &status, 0) != pid || !WIFEXITED(status))
+ return -1;
+ return WEXITSTATUS(status);
+}
+
+TEST(empty_mntns_root_atime)
+{
+ int to_reader[2], to_watcher[2], w, r;
+ pid_t watcher_pid, reader_pid;
+
+ if (geteuid())
+ SKIP(return, "test requires root");
+ ASSERT_EQ(pipe(to_reader), 0);
+ ASSERT_EQ(pipe(to_watcher), 0);
+
+ watcher_pid = fork();
+ ASSERT_GE(watcher_pid, 0);
+ if (watcher_pid == 0)
+ _exit(watcher(to_reader[1], to_watcher[0]));
+ reader_pid = fork();
+ ASSERT_GE(reader_pid, 0);
+ if (reader_pid == 0)
+ _exit(reader(to_watcher[1], to_reader[0]));
+
+ r = wait_child(reader_pid);
+ w = wait_child(watcher_pid);
+ if (r == CHILD_UNSUPPORTED || w == CHILD_UNSUPPORTED)
+ SKIP(return, "UNSHARE_EMPTY_MNTNS not supported");
+ EXPECT_EQ(r, CHILD_OK);
+ EXPECT_EQ(w, CHILD_OK)
+ TH_LOG("the access time of the root changed while this namespace did nothing");
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/filesystems/empty_mntns/root_readdir_test.c b/tools/testing/selftests/filesystems/empty_mntns/root_readdir_test.c
new file mode 100644
index 000000000000..0ff544247027
--- /dev/null
+++ b/tools/testing/selftests/filesystems/empty_mntns/root_readdir_test.c
@@ -0,0 +1,45 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Reading the root of an empty mount namespace holds nothing that anybody
+ * else waits for: the directory never has an entry, so a readdir stuck in
+ * the page fault of its buffer stalls neither a create nor a lookup in it.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "../../kselftest_harness.h"
+#include "../readdir_hold.h"
+#include "empty_mntns.h"
+
+TEST(readdir_blocks_nobody)
+{
+ struct readdir_hold hold;
+ bool stalled;
+ int dfd;
+
+ if (geteuid())
+ SKIP(return, "test requires root");
+ if (readdir_hold_init(&hold))
+ SKIP(return, "test requires userfaultfd");
+ if (unshare(UNSHARE_EMPTY_MNTNS)) {
+ readdir_hold_destroy(&hold);
+ if (errno == EINVAL)
+ SKIP(return, "UNSHARE_EMPTY_MNTNS not supported");
+ ASSERT_TRUE(false)
+ TH_LOG("unshare(UNSHARE_EMPTY_MNTNS): %m");
+ }
+ dfd = open("/", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
+ ASSERT_GE(dfd, 0);
+ ASSERT_EQ(readdir_hold_check(&hold, dfd, &stalled), 0);
+ /* the lookup came back while the readdir was stuck in its fault */
+ EXPECT_FALSE(stalled);
+ close(dfd);
+ readdir_hold_destroy(&hold);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/filesystems/mntns_unbindable/Makefile b/tools/testing/selftests/filesystems/mntns_unbindable/Makefile
new file mode 100644
index 000000000000..33a311c5bd72
--- /dev/null
+++ b/tools/testing/selftests/filesystems/mntns_unbindable/Makefile
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: GPL-2.0
+TEST_GEN_PROGS := mntns_unbindable_test
+
+CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES)
+
+include ../../lib.mk
diff --git a/tools/testing/selftests/filesystems/mntns_unbindable/mntns_unbindable_test.c b/tools/testing/selftests/filesystems/mntns_unbindable/mntns_unbindable_test.c
new file mode 100644
index 000000000000..9aebc37cf74d
--- /dev/null
+++ b/tools/testing/selftests/filesystems/mntns_unbindable/mntns_unbindable_test.c
@@ -0,0 +1,227 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * An unbindable mount stays unbindable in a cloned mount namespace.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/syscall.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "../../kselftest_harness.h"
+
+#ifndef OPEN_TREE_CLONE
+#define OPEN_TREE_CLONE 1
+#endif
+#ifndef OPEN_TREE_CLOEXEC
+#define OPEN_TREE_CLOEXEC O_CLOEXEC
+#endif
+
+static int sys_open_tree(int dfd, const char *filename, unsigned int flags)
+{
+ return syscall(__NR_open_tree, dfd, filename, flags);
+}
+
+/* Child exit codes. */
+enum {
+ CHILD_OK, /* the operation failed with EINVAL as it must */
+ CHILD_ALLOWED, /* the operation succeeded: the flag was lost */
+ CHILD_UNSHARE, /* unshare(CLONE_NEWNS) failed */
+ CHILD_ERRNO, /* the operation failed with some other errno */
+ CHILD_MOUNTINFO, /* the mount was not found in mountinfo */
+};
+
+FIXTURE(mntns_unbindable)
+{
+ char base[64];
+ char src[80];
+ char dst[80];
+ bool mounted;
+};
+
+FIXTURE_SETUP(mntns_unbindable)
+{
+ self->mounted = false;
+
+ if (geteuid() != 0)
+ SKIP(return, "test requires CAP_SYS_ADMIN");
+
+ ASSERT_EQ(unshare(CLONE_NEWNS), 0);
+ ASSERT_EQ(mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL), 0);
+
+ snprintf(self->base, sizeof(self->base), "/tmp/mntns_unbindable.XXXXXX");
+ ASSERT_NE(mkdtemp(self->base), NULL);
+ ASSERT_EQ(mount("tmpfs", self->base, "tmpfs", 0, NULL), 0);
+ self->mounted = true;
+
+ snprintf(self->src, sizeof(self->src), "%s/src", self->base);
+ snprintf(self->dst, sizeof(self->dst), "%s/dst", self->base);
+ ASSERT_EQ(mkdir(self->src, 0755), 0);
+ ASSERT_EQ(mkdir(self->dst, 0755), 0);
+
+ ASSERT_EQ(mount("tmpfs", self->src, "tmpfs", 0, NULL), 0);
+ ASSERT_EQ(mount(NULL, self->src, NULL, MS_UNBINDABLE, NULL), 0);
+}
+
+FIXTURE_TEARDOWN(mntns_unbindable)
+{
+ if (self->mounted)
+ umount2(self->base, MNT_DETACH);
+ rmdir(self->base);
+}
+
+static int classify(int ret, int err)
+{
+ if (ret >= 0)
+ return CHILD_ALLOWED;
+ return err == EINVAL ? CHILD_OK : CHILD_ERRNO;
+}
+
+/* Is the mount on @mountpoint marked unbindable in /proc/self/mountinfo? */
+static int mountinfo_unbindable(const char *mountpoint)
+{
+ char line[4096];
+ FILE *f;
+ int ret = CHILD_MOUNTINFO;
+
+ f = fopen("/proc/self/mountinfo", "re");
+ if (!f)
+ return CHILD_ERRNO;
+
+ while (fgets(line, sizeof(line), f)) {
+ char *fields[6], *p = line, *opt;
+ int i;
+
+ for (i = 0; i < 6; i++) {
+ fields[i] = strsep(&p, " ");
+ if (!fields[i])
+ break;
+ }
+ if (i < 6 || strcmp(fields[4], mountpoint))
+ continue;
+
+ /* the optional fields, up to the "-" separator */
+ ret = CHILD_ALLOWED;
+ while ((opt = strsep(&p, " ")) && strcmp(opt, "-")) {
+ if (!strcmp(opt, "unbindable"))
+ ret = CHILD_OK;
+ }
+ break;
+ }
+ fclose(f);
+ return ret;
+}
+
+static int run_in_child(int (*fn)(const char *src, const char *dst),
+ const char *src, const char *dst)
+{
+ int status;
+ pid_t pid;
+
+ pid = fork();
+ if (pid < 0)
+ return -1;
+ if (pid == 0)
+ _exit(fn(src, dst));
+ if (waitpid(pid, &status, 0) != pid || !WIFEXITED(status))
+ return -1;
+ return WEXITSTATUS(status);
+}
+
+static int bind_after_clone(const char *src, const char *dst)
+{
+ int ret;
+
+ if (unshare(CLONE_NEWNS))
+ return CHILD_UNSHARE;
+ ret = mount(src, dst, NULL, MS_BIND, NULL);
+ return classify(ret, errno);
+}
+
+static int rbind_after_clone(const char *src, const char *dst)
+{
+ int ret;
+
+ if (unshare(CLONE_NEWNS))
+ return CHILD_UNSHARE;
+ ret = mount(src, dst, NULL, MS_BIND | MS_REC, NULL);
+ return classify(ret, errno);
+}
+
+static int open_tree_after_clone(const char *src, const char *dst)
+{
+ int ret;
+
+ if (unshare(CLONE_NEWNS))
+ return CHILD_UNSHARE;
+ ret = sys_open_tree(AT_FDCWD, src, OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
+ return classify(ret, errno);
+}
+
+static int mountinfo_after_clone(const char *src, const char *dst)
+{
+ if (unshare(CLONE_NEWNS))
+ return CHILD_UNSHARE;
+ return mountinfo_unbindable(src);
+}
+
+static int bind_after_two_clones(const char *src, const char *dst)
+{
+ int ret;
+
+ if (unshare(CLONE_NEWNS))
+ return CHILD_UNSHARE;
+ if (unshare(CLONE_NEWNS))
+ return CHILD_UNSHARE;
+ ret = mount(src, dst, NULL, MS_BIND, NULL);
+ return classify(ret, errno);
+}
+
+/* The namespace the mount was made unbindable in. */
+TEST_F(mntns_unbindable, refuses_bind)
+{
+ int ret = mount(self->src, self->dst, NULL, MS_BIND, NULL);
+
+ ASSERT_EQ(classify(ret, errno), CHILD_OK);
+ ASSERT_EQ(mountinfo_unbindable(self->src), CHILD_OK);
+}
+
+/* A copy of the namespace must not turn the mount bindable. */
+TEST_F(mntns_unbindable, refuses_bind_after_clone)
+{
+ ASSERT_EQ(run_in_child(bind_after_clone, self->src, self->dst), CHILD_OK)
+ TH_LOG("bind of an unbindable mount allowed in a copied mount namespace");
+}
+
+TEST_F(mntns_unbindable, refuses_rbind_after_clone)
+{
+ ASSERT_EQ(run_in_child(rbind_after_clone, self->src, self->dst), CHILD_OK)
+ TH_LOG("rbind of an unbindable mount allowed in a copied mount namespace");
+}
+
+TEST_F(mntns_unbindable, refuses_open_tree_after_clone)
+{
+ ASSERT_EQ(run_in_child(open_tree_after_clone, self->src, self->dst), CHILD_OK)
+ TH_LOG("OPEN_TREE_CLONE of an unbindable mount allowed in a copied mount namespace");
+}
+
+TEST_F(mntns_unbindable, mountinfo_after_clone)
+{
+ ASSERT_EQ(run_in_child(mountinfo_after_clone, self->src, self->dst), CHILD_OK)
+ TH_LOG("mountinfo does not show the mount as unbindable in a copied mount namespace");
+}
+
+TEST_F(mntns_unbindable, refuses_bind_after_two_clones)
+{
+ ASSERT_EQ(run_in_child(bind_after_two_clones, self->src, self->dst), CHILD_OK)
+ TH_LOG("bind of an unbindable mount allowed two mount namespace copies down");
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/filesystems/mount_cycle/.gitignore b/tools/testing/selftests/filesystems/mount_cycle/.gitignore
new file mode 100644
index 000000000000..30f7dd071bcb
--- /dev/null
+++ b/tools/testing/selftests/filesystems/mount_cycle/.gitignore
@@ -0,0 +1,8 @@
+# SPDX-License-Identifier: GPL-2.0-only
+loop_cycle_test
+unmounted_tree_test
+overmount_reparent_test
+mount_cover_test
+locked_handle_test
+nsfs_rbind_loop_test
+overmount_ns_file_test
diff --git a/tools/testing/selftests/filesystems/mount_cycle/Makefile b/tools/testing/selftests/filesystems/mount_cycle/Makefile
new file mode 100644
index 000000000000..fbe8c5e19d42
--- /dev/null
+++ b/tools/testing/selftests/filesystems/mount_cycle/Makefile
@@ -0,0 +1,12 @@
+# SPDX-License-Identifier: GPL-2.0
+TEST_GEN_PROGS := loop_cycle_test unmounted_tree_test overmount_reparent_test mount_cover_test locked_handle_test
+TEST_GEN_PROGS += nsfs_rbind_loop_test overmount_ns_file_test
+
+CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES)
+
+LOCAL_HDRS += ../readdir_hold.h
+
+include ../../lib.mk
+
+$(OUTPUT)/locked_handle_test: LDLIBS += -pthread
+$(OUTPUT)/mount_cover_test: LDLIBS += -pthread
diff --git a/tools/testing/selftests/filesystems/mount_cycle/config b/tools/testing/selftests/filesystems/mount_cycle/config
new file mode 100644
index 000000000000..3bd5ce46af74
--- /dev/null
+++ b/tools/testing/selftests/filesystems/mount_cycle/config
@@ -0,0 +1,39 @@
+CONFIG_USER_NS=y
+CONFIG_TMPFS=y
+CONFIG_BLK_DEV_LOOP=y
+CONFIG_VFAT_FS=y
+CONFIG_MSDOS_FS=y
+CONFIG_NLS_CODEPAGE_437=y
+CONFIG_NLS_ISO8859_1=y
+CONFIG_MINIX_FS=y
+CONFIG_AUTOFS_FS=y
+CONFIG_ZRAM=y
+CONFIG_ZRAM_WRITEBACK=y
+CONFIG_KEYS=y
+CONFIG_ECRYPT_FS=y
+CONFIG_BINFMT_MISC=y
+CONFIG_FUSE_FS=y
+CONFIG_FUSE_PASSTHROUGH=y
+CONFIG_BLK_DEV_ZONED=y
+CONFIG_BLK_DEV_ZONED_LOOP=y
+CONFIG_CONFIGFS_FS=y
+CONFIG_USB_SUPPORT=y
+CONFIG_USB=y
+CONFIG_USB_STORAGE=y
+CONFIG_SCSI=y
+CONFIG_BLK_DEV_SD=y
+CONFIG_USB_GADGET=y
+CONFIG_USB_DUMMY_HCD=y
+CONFIG_USB_CONFIGFS=y
+CONFIG_USB_CONFIGFS_MASS_STORAGE=y
+CONFIG_MD=y
+CONFIG_BLK_DEV_MD=y
+CONFIG_MD_RAID1=y
+CONFIG_MD_BITMAP=y
+CONFIG_MD_BITMAP_FILE=y
+CONFIG_INOTIFY_USER=y
+CONFIG_DNOTIFY=y
+CONFIG_FANOTIFY=y
+CONFIG_FILE_LOCKING=y
+CONFIG_CRYPTO_AES=y
+CONFIG_USERFAULTFD=y
diff --git a/tools/testing/selftests/filesystems/mount_cycle/locked_handle_test.c b/tools/testing/selftests/filesystems/mount_cycle/locked_handle_test.c
new file mode 100644
index 000000000000..b21f43471e49
--- /dev/null
+++ b/tools/testing/selftests/filesystems/mount_cycle/locked_handle_test.c
@@ -0,0 +1,377 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * may_decode_fh() refuses a file handle below a mount with locked children
+ * to root in a user namespace. That has to hold while the mount is lazily
+ * unmounted and its children, the locked ones too, are taken off it.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <pthread.h>
+#include <sched.h>
+#include <stdatomic.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/mount.h>
+#include <sys/prctl.h>
+#include <sys/stat.h>
+#include <sys/syscall.h>
+#include <sys/wait.h>
+
+#include "../../kselftest_harness.h"
+
+#ifndef OPEN_TREE_CLONE
+#define OPEN_TREE_CLONE 1
+#endif
+#ifndef OPEN_TREE_CLOEXEC
+#define OPEN_TREE_CLOEXEC O_CLOEXEC
+#endif
+#ifndef AT_RECURSIVE
+#define AT_RECURSIVE 0x8000
+#endif
+
+#define DIR_LEN 64
+#define PATH_LEN 128
+
+#define ROUNDS 100 /* lazy umounts raced per test */
+#define RACERS 3 /* threads in open_by_handle_at() */
+#define EXTRA_MOUNTS 64 /* make umount_tree() hold mount_lock longer */
+#define MAX_DELAY_US 3000 /* before the umount */
+#define TAIL_US 2000 /* after it */
+
+struct handle {
+ struct file_handle fh;
+ unsigned char buf[MAX_HANDLE_SZ];
+};
+
+/* exit codes of the child */
+enum {
+ CHILD_OK,
+ CHILD_SETUP,
+ CHILD_DECODED, /* decoded past a locked child */
+ CHILD_MOUNTED, /* not refused while mounted */
+ CHILD_UNMOUNTED, /* not refused once unmounted */
+ CHILD_ALLOWED, /* refused where nothing is locked */
+ CHILD_NOUSERNS, /* no user namespace to be had */
+};
+
+static int write_file(const char *path, const char *s)
+{
+ ssize_t n = -1;
+ int fd;
+
+ fd = open(path, O_WRONLY | O_CLOEXEC);
+ if (fd >= 0) {
+ n = write(fd, s, strlen(s));
+ close(fd);
+ }
+ return n == (ssize_t)strlen(s) ? 0 : -1;
+}
+
+/* Become root in a new user namespace with a private mount namespace. */
+static int enter_userns(void)
+{
+ uid_t uid = getuid();
+ gid_t gid = getgid();
+ char map[32];
+
+ prctl(PR_SET_DUMPABLE, 1);
+ /* EINVAL: no USER_NS, ENOSPC: user.max_user_namespaces is 0, EPERM: an LSM */
+ if (unshare(CLONE_NEWUSER | CLONE_NEWNS))
+ return errno == EINVAL || errno == ENOSPC || errno == EPERM ?
+ CHILD_NOUSERNS : CHILD_SETUP;
+ if (write_file("/proc/self/setgroups", "deny") && errno != ENOENT)
+ return CHILD_SETUP;
+ snprintf(map, sizeof(map), "0 %d 1", uid);
+ if (write_file("/proc/self/uid_map", map))
+ return CHILD_SETUP;
+ snprintf(map, sizeof(map), "0 %d 1", gid);
+ if (write_file("/proc/self/gid_map", map))
+ return CHILD_SETUP;
+ if (setgid(0) || setuid(0))
+ return CHILD_SETUP;
+ if (mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL))
+ return CHILD_SETUP;
+ return CHILD_OK;
+}
+
+static int get_handle(const char *path, struct handle *h)
+{
+ int mntid;
+
+ h->fh.handle_bytes = MAX_HANDLE_SZ;
+ return name_to_handle_at(AT_FDCWD, path, &h->fh, &mntid, 0);
+}
+
+static int decode(int dfd, struct handle *h)
+{
+ return open_by_handle_at(dfd, &h->fh, O_RDONLY | O_DIRECT