summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLinus Torvalds <torvalds@linux-foundation.org>2026-10-19 20:06:27 -1000
committerLinus Torvalds <torvalds@linux-foundation.org>2026-10-19 20:06:27 -1000
commit0cbcaf75a1eb0dd9e800f66324fe4a295a251ccc (patch)
treefd43ba6eba6eff7e5cb6ec27e343ce8070060b15
parent7ed37e9d5c05bf3dc74146572d89e7391091e1a4 (diff)
parent062b32e4b55082f11b63dce22c78cd00fc6c7ed5 (diff)
Merge tag 'vfs-7.4-rc1.mount' of git://git.kernel.org/pub/scm/linux/kernel/git/vfs/vfs
Pull mount updates from Christian Brauner: "This adds OPEN_TREE_DROP_MNTNS_MOUNTS. crun started using OPEN_TREE_NAMESPACE to set up the container's mount namespace and open_tree(OPEN_TREE_CLONE) for the sources of its bind mounts. That fails on any host with a mount namespace pinned below such a source. The recursive clone picks the pinned namespace up and move_mount() refuses to attach the tree with ELOOP. Only a younger mount namespace may ever be bound into an older one or mount namespace references could form a loop. A namespace created with OPEN_TREE_NAMESPACE is younger than everything else on the system so from inside it every pinned namespace is older. By the time this fails setns() has run and the host tree is out of reach, so the runtime has to predict the situation beforehand and lose the optimization. snapd pins mount namespaces under /run/snapd/ns so on Ubuntu that is any container bind mounting the host root. Every other path crossing a mount namespace boundary already leaves these mounts behind, copy_mnt_ns() and create_new_namespace() both do. Only get_detached_copy() asks for them. The new flag lets a caller get a clone without them. It stays opt-in because open_tree(OPEN_TREE_CLONE) plus move_mount() is mount --rbind through a file descriptor and dropping them by default would silently lose /run/snapd/ns/*.mnt on a live system. It requires AT_RECURSIVE, is a no-op with OPEN_TREE_NAMESPACE and skips locked nsfs mounts the way copy_mnt_ns() does, so nothing new is exposed. Selftests are included" * tag 'vfs-7.4-rc1.mount' of git://git.kernel.org/pub/scm/linux/kernel/git/vfs/vfs: selftests/filesystems: test OPEN_TREE_DROP_MNTNS_MOUNTS mount: add OPEN_TREE_DROP_MNTNS_MOUNTS
-rw-r--r--fs/namespace.c16
-rw-r--r--include/uapi/linux/mount.h1
-rw-r--r--tools/testing/selftests/filesystems/open_tree_ns/open_tree_ns_test.c193
3 files changed, 208 insertions, 2 deletions
diff --git a/fs/namespace.c b/fs/namespace.c
index 3ce5d995691c..280ad18f1949 100644
--- a/fs/namespace.c
+++ b/fs/namespace.c
@@ -3168,7 +3168,8 @@ static struct mnt_namespace *get_detached_copy(const struct path *path, unsigned
ns->seq_origin = src_mnt_ns->ns.ns_id;
}
- mnt = __do_loopback(path, (flags & AT_RECURSIVE), CL_COPY_MNT_NS_FILE);
+ mnt = __do_loopback(path, (flags & AT_RECURSIVE),
+ (flags & OPEN_TREE_DROP_MNTNS_MOUNTS) ? 0 : CL_COPY_MNT_NS_FILE);
if (IS_ERR(mnt)) {
emptied_ns = ns;
return ERR_CAST(mnt);
@@ -3320,7 +3321,18 @@ static struct file *vfs_open_tree(int dfd, const char __user *filename, unsigned
if (flags & ~(AT_EMPTY_PATH | AT_NO_AUTOMOUNT | AT_RECURSIVE |
AT_SYMLINK_NOFOLLOW | OPEN_TREE_CLONE |
- OPEN_TREE_CLOEXEC | OPEN_TREE_NAMESPACE))
+ OPEN_TREE_CLOEXEC | OPEN_TREE_NAMESPACE |
+ OPEN_TREE_DROP_MNTNS_MOUNTS))
+ return ERR_PTR(-EINVAL);
+
+ /*
+ * Only meaningful when a tree is copied recursively: a non-recursive
+ * clone has no submounts to drop. OPEN_TREE_NAMESPACE never copies
+ * pinned mount namespaces, so there the flag is a no-op.
+ */
+ if ((flags & OPEN_TREE_DROP_MNTNS_MOUNTS) &&
+ (!(flags & AT_RECURSIVE) ||
+ !(flags & (OPEN_TREE_CLONE | OPEN_TREE_NAMESPACE))))
return ERR_PTR(-EINVAL);
if ((flags & (AT_RECURSIVE | OPEN_TREE_CLONE | OPEN_TREE_NAMESPACE)) ==
diff --git a/include/uapi/linux/mount.h b/include/uapi/linux/mount.h
index 2204708dbf7a..ac865edac517 100644
--- a/include/uapi/linux/mount.h
+++ b/include/uapi/linux/mount.h
@@ -63,6 +63,7 @@
*/
#define OPEN_TREE_CLONE (1 << 0) /* Clone the target tree and attach the clone */
#define OPEN_TREE_NAMESPACE (1 << 1) /* Clone the target tree into a new mount namespace */
+#define OPEN_TREE_DROP_MNTNS_MOUNTS (1 << 2) /* Drop mntns mounts from the clone */
#define OPEN_TREE_CLOEXEC O_CLOEXEC /* Close the file on execve() */
/*
diff --git a/tools/testing/selftests/filesystems/open_tree_ns/open_tree_ns_test.c b/tools/testing/selftests/filesystems/open_tree_ns/open_tree_ns_test.c
index 82f3c8c02c9a..438054ab6a6a 100644
--- a/tools/testing/selftests/filesystems/open_tree_ns/open_tree_ns_test.c
+++ b/tools/testing/selftests/filesystems/open_tree_ns/open_tree_ns_test.c
@@ -14,6 +14,7 @@
#include <limits.h>
#include <linux/nsfs.h>
#include <sched.h>
+#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -32,6 +33,10 @@
#define OPEN_TREE_NAMESPACE (1 << 1)
#endif
+#ifndef OPEN_TREE_DROP_MNTNS_MOUNTS
+#define OPEN_TREE_DROP_MNTNS_MOUNTS (1 << 2)
+#endif
+
static int get_mnt_ns_id(int fd, uint64_t *mnt_ns_id)
{
if (ioctl(fd, NS_GET_MNTNS_ID, mnt_ns_id) < 0)
@@ -1004,4 +1009,192 @@ TEST_F(open_tree_ns_unbindable, recursive_skips_on_unbindable)
close(fd);
}
+/*
+ * Pin a mount namespace at @where, the way snapd does under /run/snapd/ns.
+ * The namespace has to be younger than ours, so a child unshares and the
+ * parent binds the child's namespace file: binding one's own namespace is
+ * refused by the very check this test is about.
+ */
+static int pin_mount_namespace(const char *where)
+{
+ int pipefd[2], status, ret = -1;
+ char path[PATH_MAX];
+ pid_t pid;
+ char c;
+
+ if (pipe(pipefd))
+ return -1;
+
+ pid = fork();
+ if (pid < 0)
+ goto out;
+ if (pid == 0) {
+ close(pipefd[0]);
+ if (unshare(CLONE_NEWNS))
+ _exit(1);
+ /* Tell the parent the namespace exists, then hold it open. */
+ if (write(pipefd[1], "x", 1) != 1)
+ _exit(1);
+ pause();
+ _exit(0);
+ }
+
+ close(pipefd[1]);
+ pipefd[1] = -1;
+ if (read(pipefd[0], &c, 1) != 1)
+ goto out_kill;
+
+ snprintf(path, sizeof(path), "/proc/%d/ns/mnt", pid);
+ if (mount(path, where, NULL, MS_BIND, NULL))
+ goto out_kill;
+
+ ret = 0;
+
+out_kill:
+ kill(pid, SIGKILL);
+ waitpid(pid, &status, 0);
+out:
+ close(pipefd[0]);
+ if (pipefd[1] >= 0)
+ close(pipefd[1]);
+ return ret;
+}
+
+FIXTURE(open_tree_ns_drop_mntns)
+{
+ char dir[64];
+ char pin[PATH_MAX];
+ bool mounted;
+};
+
+FIXTURE_SETUP(open_tree_ns_drop_mntns)
+{
+ int fd, ret;
+
+ self->mounted = false;
+ snprintf(self->dir, sizeof(self->dir), "/tmp/open_tree_ns_drop_mntns.XXXXXX");
+
+ ret = sys_open_tree(-1, NULL, 0);
+ if (ret == -1 && errno == ENOSYS)
+ SKIP(return, "open_tree() syscall not supported");
+
+ /*
+ * Work in a private mount namespace, so whatever is mounted here,
+ * the pinned namespace included, goes away with the test process.
+ */
+ if (unshare(CLONE_NEWNS))
+ SKIP(return, "unshare(CLONE_NEWNS) failed: %s", strerror(errno));
+ ASSERT_EQ(mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL), 0);
+
+ ASSERT_NE(mkdtemp(self->dir), NULL);
+ if (mount("tmpfs", self->dir, "tmpfs", 0, NULL))
+ SKIP(return, "Failed to mount tmpfs");
+ self->mounted = true;
+
+ fd = sys_open_tree(AT_FDCWD, self->dir,
+ OPEN_TREE_CLONE | OPEN_TREE_DROP_MNTNS_MOUNTS |
+ AT_RECURSIVE | OPEN_TREE_CLOEXEC);
+ if (fd < 0 && errno == EINVAL)
+ SKIP(return, "OPEN_TREE_DROP_MNTNS_MOUNTS not supported");
+ ASSERT_GE(fd, 0);
+ close(fd);
+
+ snprintf(self->pin, sizeof(self->pin), "%s/ns", self->dir);
+ fd = open(self->pin, O_CREAT | O_RDONLY | O_CLOEXEC, 0600);
+ ASSERT_GE(fd, 0);
+ close(fd);
+ ASSERT_EQ(pin_mount_namespace(self->pin), 0);
+}
+
+FIXTURE_TEARDOWN(open_tree_ns_drop_mntns)
+{
+ if (self->mounted)
+ umount2(self->dir, MNT_DETACH);
+ rmdir(self->dir);
+}
+
+/* Attach @clone_fd at "/" inside the namespace @ns_fd; returns errno. */
+static int move_into_namespace(int clone_fd, int ns_fd)
+{
+ pid_t pid;
+ int status;
+
+ pid = fork();
+ if (pid < 0)
+ return -1;
+ if (pid == 0) {
+ int target;
+
+ if (setns(ns_fd, CLONE_NEWNS))
+ _exit(255);
+ target = open("/", O_PATH | O_DIRECTORY | O_CLOEXEC);
+ if (target < 0)
+ _exit(255);
+ if (sys_move_mount(clone_fd, "", target, "",
+ MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_T_EMPTY_PATH))
+ _exit(errno);
+ _exit(0);
+ }
+ if (waitpid(pid, &status, 0) != pid || !WIFEXITED(status))
+ return -1;
+ return WEXITSTATUS(status);
+}
+
+TEST_F(open_tree_ns_drop_mntns, move_into_younger_namespace)
+{
+ int clone_fd, ns_fd;
+
+ ns_fd = sys_open_tree(AT_FDCWD, self->dir,
+ OPEN_TREE_NAMESPACE | OPEN_TREE_CLOEXEC);
+ ASSERT_GE(ns_fd, 0);
+
+ /*
+ * Without the flag the clone carries the pinned namespace along,
+ * and move_mount() refuses it from inside a younger namespace.
+ */
+ clone_fd = sys_open_tree(AT_FDCWD, self->dir,
+ OPEN_TREE_CLONE | AT_RECURSIVE | OPEN_TREE_CLOEXEC);
+ ASSERT_GE(clone_fd, 0);
+ EXPECT_EQ(move_into_namespace(clone_fd, ns_fd), ELOOP);
+ close(clone_fd);
+
+ /* With the flag the pinned namespace is left out. */
+ clone_fd = sys_open_tree(AT_FDCWD, self->dir,
+ OPEN_TREE_CLONE | OPEN_TREE_DROP_MNTNS_MOUNTS |
+ AT_RECURSIVE | OPEN_TREE_CLOEXEC);
+ ASSERT_GE(clone_fd, 0);
+ EXPECT_EQ(move_into_namespace(clone_fd, ns_fd), 0);
+ close(clone_fd);
+
+ close(ns_fd);
+}
+
+TEST_F(open_tree_ns_drop_mntns, flag_combinations)
+{
+ int fd;
+
+ /* Nothing is copied, so there is nothing to drop. */
+ EXPECT_LT(sys_open_tree(AT_FDCWD, self->dir,
+ OPEN_TREE_DROP_MNTNS_MOUNTS | OPEN_TREE_CLOEXEC), 0);
+ EXPECT_EQ(errno, EINVAL);
+
+ /* A non-recursive clone has no submounts, so there is nothing to drop. */
+ EXPECT_LT(sys_open_tree(AT_FDCWD, self->dir,
+ OPEN_TREE_CLONE | OPEN_TREE_DROP_MNTNS_MOUNTS |
+ OPEN_TREE_CLOEXEC), 0);
+ EXPECT_EQ(errno, EINVAL);
+ EXPECT_LT(sys_open_tree(AT_FDCWD, self->dir,
+ OPEN_TREE_NAMESPACE | OPEN_TREE_DROP_MNTNS_MOUNTS |
+ OPEN_TREE_CLOEXEC), 0);
+ EXPECT_EQ(errno, EINVAL);
+
+ /* OPEN_TREE_NAMESPACE never copies them; the flag is a no-op there. */
+ fd = sys_open_tree(AT_FDCWD, self->dir,
+ OPEN_TREE_NAMESPACE | OPEN_TREE_DROP_MNTNS_MOUNTS |
+ AT_RECURSIVE | OPEN_TREE_CLOEXEC);
+ EXPECT_GE(fd, 0);
+ if (fd >= 0)
+ close(fd);
+}
+
TEST_HARNESS_MAIN