diff options
| author | Linus Torvalds <torvalds@linux-foundation.org> | 2026-10-19 20:06:27 -1000 |
|---|---|---|
| committer | Linus Torvalds <torvalds@linux-foundation.org> | 2026-10-19 20:06:27 -1000 |
| commit | 0cbcaf75a1eb0dd9e800f66324fe4a295a251ccc (patch) | |
| tree | fd43ba6eba6eff7e5cb6ec27e343ce8070060b15 | |
| parent | 7ed37e9d5c05bf3dc74146572d89e7391091e1a4 (diff) | |
| parent | 062b32e4b55082f11b63dce22c78cd00fc6c7ed5 (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.c | 16 | ||||
| -rw-r--r-- | include/uapi/linux/mount.h | 1 | ||||
| -rw-r--r-- | tools/testing/selftests/filesystems/open_tree_ns/open_tree_ns_test.c | 193 |
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 |
