// 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