Pin a mount namespace below a tmpfs mount, the way snapd does under /run/snapd/ns, and attach a recursive clone of that tmpfs inside a younger mount namespace created with OPEN_TREE_NAMESPACE. Without OPEN_TREE_DROP_MNTNS_MOUNTS move_mount(2) fails with ELOOP; with it, it succeeds.
Also check that the flag is rejected without OPEN_TREE_CLONE or OPEN_TREE_NAMESPACE, and accepted with the latter. Assisted-by: Claude:claude-opus-5 Signed-off-by: Kir Kolyshkin <[email protected]> --- .../filesystems/open_tree_ns/open_tree_ns_test.c | 183 +++++++++++++++++++++ 1 file changed, 183 insertions(+) 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..05d16c56ab35 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,182 @@ 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 | + 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); + + /* 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 -- 2.55.0

