From 7fbcc793f76e0f3bf78f7e41bde634107233714f Mon Sep 17 00:00:00 2001 From: Christian Brauner Date: Sat, 26 Sep 2026 15:44:33 +0200 Subject: [PATCH] selftests/filesystems: check that a dead mount's overmount isn't followed Add overmount_reparent_test: - a mount that a file descriptor keeps alive after propagate_umount() reparented its overmount and unmounted it must not be walked into the freed overmount by move_mount(MOVE_MOUNT_BENEATH) Link: https://patch.msgid.link/20260926-work-mount-fixes-2-v1-10-f357abf3d17b@kernel.org Signed-off-by: Christian Brauner (Amutable) --- .../filesystems/mount_cycle/.gitignore | 1 + .../filesystems/mount_cycle/Makefile | 2 +- .../mount_cycle/overmount_reparent_test.c | 181 ++++++++++++++++++ 3 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 tools/testing/selftests/filesystems/mount_cycle/overmount_reparent_test.c diff --git a/tools/testing/selftests/filesystems/mount_cycle/.gitignore b/tools/testing/selftests/filesystems/mount_cycle/.gitignore index 7bdfe4268fb4..03ca95de7765 100644 --- a/tools/testing/selftests/filesystems/mount_cycle/.gitignore +++ b/tools/testing/selftests/filesystems/mount_cycle/.gitignore @@ -1,2 +1,3 @@ # SPDX-License-Identifier: GPL-2.0-only unmounted_tree_test +overmount_reparent_test diff --git a/tools/testing/selftests/filesystems/mount_cycle/Makefile b/tools/testing/selftests/filesystems/mount_cycle/Makefile index 46a68ca234ba..88271c82d1c1 100644 --- a/tools/testing/selftests/filesystems/mount_cycle/Makefile +++ b/tools/testing/selftests/filesystems/mount_cycle/Makefile @@ -1,5 +1,5 @@ # SPDX-License-Identifier: GPL-2.0 -TEST_GEN_PROGS := unmounted_tree_test +TEST_GEN_PROGS := unmounted_tree_test overmount_reparent_test CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES) diff --git a/tools/testing/selftests/filesystems/mount_cycle/overmount_reparent_test.c b/tools/testing/selftests/filesystems/mount_cycle/overmount_reparent_test.c new file mode 100644 index 000000000000..46a56b722f3e --- /dev/null +++ b/tools/testing/selftests/filesystems/mount_cycle/overmount_reparent_test.c @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * propagate_umount() slides a surviving overmount off a mount that is being + * unmounted and unmounts that mount right after. If a file descriptor keeps + * the unmounted mount alive its ->overmount is left pointing at the moved + * mount, which is freed on its own schedule. move_mount(MOVE_MOUNT_BENEATH) + * with such a file descriptor as the target walks ->overmount in + * topmost_overmount() before it checks that the target is still mounted and + * must not step into the freed mount. + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../wrappers.h" +#include "../../kselftest_harness.h" + +#ifndef MOVE_MOUNT_BENEATH +#define MOVE_MOUNT_BENEATH 0x00000200 +#endif + +#ifndef FSCONFIG_CMD_CREATE +#define FSCONFIG_CMD_CREATE 6 +#endif + +#define DIR_LEN 64 +#define PATH_LEN 128 + +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]; + + if (unshare(CLONE_NEWUSER | CLONE_NEWNS)) + return -1; + if (write_file("/proc/self/setgroups", "deny") && errno != ENOENT) + return -1; + snprintf(map, sizeof(map), "0 %d 1", uid); + if (write_file("/proc/self/uid_map", map)) + return -1; + snprintf(map, sizeof(map), "0 %d 1", gid); + if (write_file("/proc/self/gid_map", map)) + return -1; + if (setgid(0) || setuid(0)) + return -1; + return mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL); +} + +/* A detached tmpfs mount to serve as the source of a move. */ +static int detached_tmpfs(void) +{ + int sfd, mfd; + + sfd = sys_fsopen("tmpfs", 0); + if (sfd < 0) + return -1; + if (sys_fsconfig(sfd, FSCONFIG_CMD_CREATE, NULL, NULL, 0)) { + close(sfd); + return -1; + } + mfd = sys_fsmount(sfd, 0, 0); + close(sfd); + return mfd; +} + +FIXTURE(overmount_reparent) { + char dir[DIR_LEN]; +}; + +FIXTURE_SETUP(overmount_reparent) +{ + snprintf(self->dir, sizeof(self->dir), "/tmp/overmount_reparent.XXXXXX"); + ASSERT_NE(mkdtemp(self->dir), NULL); + if (enter_userns()) { + rmdir(self->dir); + SKIP(return, "test requires user namespaces"); + } +} + +FIXTURE_TEARDOWN(overmount_reparent) +{ + char p[PATH_LEN]; + + snprintf(p, sizeof(p), "%s/b", self->dir); + umount2(p, MNT_DETACH); + snprintf(p, sizeof(p), "%s/a", self->dir); + umount2(p, MNT_DETACH); +} + +/* + * A shared mount base_a is bind-mounted to base_b as its peer. A mount X on + * base_a/mp propagates a copy X_b onto base_b/mp. A file descriptor pins X_b, + * then X_b is made private and an overmount is stacked on its root. + * + * A lazy unmount of X propagates to X_b: X_b is committed to the unmount while + * its overmount survives, so propagate_umount() reparents the overmount onto + * base_b and unmounts X_b, which the file descriptor keeps alive. Unmounting + * the reparented overmount frees it. X_b->overmount now dangles unless + * mnt_change_mountpoint() reset it. + * + * move_mount(MOVE_MOUNT_BENEATH) through the file descriptor makes + * do_lock_mount() follow X_b->overmount in topmost_overmount() before it + * notices that X_b is no longer mounted. With the pointer reset the move fails + * cleanly with ENOENT; otherwise it reads the freed overmount. + */ +TEST_F(overmount_reparent, dead_overmount_holder_not_followed) +{ + char a[PATH_LEN], b[PATH_LEN], amp[PATH_LEN], bmp[PATH_LEN]; + int fd, mfd, ret; + + snprintf(a, sizeof(a), "%s/a", self->dir); + snprintf(b, sizeof(b), "%s/b", self->dir); + ASSERT_EQ(mkdir(a, 0755), 0); + ASSERT_EQ(mkdir(b, 0755), 0); + + ASSERT_EQ(mount("tmpfs", a, "tmpfs", 0, NULL), 0); + ASSERT_EQ(mount(NULL, a, NULL, MS_SHARED, NULL), 0); + ASSERT_EQ(mount(a, b, NULL, MS_BIND, NULL), 0); + + snprintf(amp, sizeof(amp), "%s/a/mp", self->dir); + snprintf(bmp, sizeof(bmp), "%s/b/mp", self->dir); + /* base_a and base_b share one superblock, so this dir is in both. */ + ASSERT_EQ(mkdir(amp, 0755), 0); + + /* X on base_a/mp propagates a copy X_b onto base_b/mp. */ + ASSERT_EQ(mount("tmpfs", amp, "tmpfs", 0, NULL), 0); + + /* Pin X_b before anything is stacked on top of it. */ + fd = open(bmp, O_PATH | O_CLOEXEC); + ASSERT_GE(fd, 0); + + /* Keep the overmount local to X_b. */ + ASSERT_EQ(mount(NULL, bmp, NULL, MS_PRIVATE, NULL), 0); + + /* The overmount on X_b's root: X_b->overmount points at it. */ + ASSERT_EQ(mount("tmpfs", bmp, "tmpfs", 0, NULL), 0); + + /* Reparents the overmount onto base_b and unmounts X_b. */ + ASSERT_EQ(umount2(amp, MNT_DETACH), 0); + + /* Free the reparented overmount. */ + ASSERT_EQ(umount2(bmp, MNT_DETACH), 0); + usleep(100000); + + mfd = detached_tmpfs(); + ASSERT_GE(mfd, 0); + + ret = sys_move_mount(mfd, "", fd, "", + MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_T_EMPTY_PATH | + MOVE_MOUNT_BENEATH); + EXPECT_EQ(ret, -1); + EXPECT_EQ(errno, ENOENT); + + close(mfd); + close(fd); +} + +TEST_HARNESS_MAIN