From: Christian Brauner <brauner@kernel.org>
To: linux-fsdevel@vger.kernel.org
Cc: Linus Torvalds <torvalds@linux-foundation.org>,
Chris Mason <mason@kernel.org>,
Alexander Viro <viro@zeniv.linux.org.uk>,
Jan Kara <jack@suse.cz>, Jeff Layton <jlayton@kernel.org>,
Aleksa Sarai <cyphar@cyphar.com>,
Amir Goldstein <amir73il@gmail.com>,
bpf@vger.kernel.org,
"Christian Brauner (Amutable)" <brauner@kernel.org>
Subject: [PATCH 09/17] selftests/filesystems: check that a mount namespace file on top doesn't bury a mount
Date: Wed, 30 Sep 2026 15:32:01 +0200 [thread overview]
Message-ID: <20260930-work-mount-fixes-3-v1-9-be34c83956ae@kernel.org> (raw)
In-Reply-To: <20260930-work-mount-fixes-3-v1-0-be34c83956ae@kernel.org>
Add a test for moving a mount with a mount namespace file on top of it:
- S, a bind mount of a file, with a newer mount namespace's file on top
- A shared with a slave B that has Q on B/file
- S moved onto A/file, its copy lands on B/file below Q
B/file keeps reading Q and once Q is unmounted it reads the copy.
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
.../selftests/filesystems/mount_cycle/.gitignore | 1 +
.../selftests/filesystems/mount_cycle/Makefile | 2 +-
.../mount_cycle/overmount_ns_file_test.c | 187 +++++++++++++++++++++
3 files changed, 189 insertions(+), 1 deletion(-)
diff --git a/tools/testing/selftests/filesystems/mount_cycle/.gitignore b/tools/testing/selftests/filesystems/mount_cycle/.gitignore
index 8cd722977a32..d11f5b720d5b 100644
--- a/tools/testing/selftests/filesystems/mount_cycle/.gitignore
+++ b/tools/testing/selftests/filesystems/mount_cycle/.gitignore
@@ -2,3 +2,4 @@
unmounted_tree_test
overmount_reparent_test
nsfs_rbind_loop_test
+overmount_ns_file_test
diff --git a/tools/testing/selftests/filesystems/mount_cycle/Makefile b/tools/testing/selftests/filesystems/mount_cycle/Makefile
index 32e26336b132..49a8402ca858 100644
--- a/tools/testing/selftests/filesystems/mount_cycle/Makefile
+++ b/tools/testing/selftests/filesystems/mount_cycle/Makefile
@@ -1,6 +1,6 @@
# SPDX-License-Identifier: GPL-2.0
TEST_GEN_PROGS := unmounted_tree_test overmount_reparent_test
-TEST_GEN_PROGS += nsfs_rbind_loop_test
+TEST_GEN_PROGS += nsfs_rbind_loop_test overmount_ns_file_test
CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES)
diff --git a/tools/testing/selftests/filesystems/mount_cycle/overmount_ns_file_test.c b/tools/testing/selftests/filesystems/mount_cycle/overmount_ns_file_test.c
new file mode 100644
index 000000000000..c24436a17c0f
--- /dev/null
+++ b/tools/testing/selftests/filesystems/mount_cycle/overmount_ns_file_test.c
@@ -0,0 +1,187 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * A mount namespace file bind-mounted on top of the mount that is moved
+ * onto a shared mount isn't copied to the peers and slaves. The mount that
+ * already sits at the destination in a slave has to end up on top of the
+ * propagated copy, not below the root of the mount namespace file where no
+ * path walk ever finds it.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/syscall.h>
+#include <sys/wait.h>
+
+#include "../wrappers.h"
+#include "../../kselftest_harness.h"
+
+#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;
+}
+
+static int create_file(const char *path, const char *s)
+{
+ ssize_t n = -1;
+ int fd;
+
+ fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0644);
+ if (fd >= 0) {
+ n = write(fd, s, strlen(s));
+ close(fd);
+ }
+ return n == (ssize_t)strlen(s) ? 0 : -1;
+}
+
+/* the first bytes of the file at @path, "" if it can't be read */
+static const char *read_file(const char *path, char *buf, size_t len)
+{
+ ssize_t n = -1;
+ int fd;
+
+ fd = open(path, O_RDONLY | O_CLOEXEC);
+ if (fd >= 0) {
+ n = read(fd, buf, len - 1);
+ close(fd);
+ }
+ buf[n > 0 ? n : 0] = '\0';
+ return buf;
+}
+
+/* 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);
+}
+
+FIXTURE(overmount_ns_file) {
+ char dir[DIR_LEN];
+ pid_t child;
+};
+
+FIXTURE_SETUP(overmount_ns_file)
+{
+ self->child = -1;
+ snprintf(self->dir, sizeof(self->dir), "/tmp/overmount_ns_file.XXXXXX");
+ ASSERT_NE(mkdtemp(self->dir), NULL);
+ if (enter_userns()) {
+ rmdir(self->dir);
+ SKIP(return, "test requires user namespaces");
+ }
+ ASSERT_EQ(mount("tmpfs", self->dir, "tmpfs", 0, NULL), 0);
+}
+
+FIXTURE_TEARDOWN(overmount_ns_file)
+{
+ if (self->child > 0) {
+ kill(self->child, SIGKILL);
+ waitpid(self->child, NULL, 0);
+ }
+ umount2(self->dir, MNT_DETACH);
+ rmdir(self->dir);
+}
+
+/*
+ * A is a shared tmpfs and B its slave with Q, a bind mount of a file, on
+ * B/file. S is a bind mount of a file with N, a bind mount of a newer mount
+ * namespace's file, on top of it. S is moved onto A/file. Its copy S' lands
+ * on B/file below Q, without N. B/file keeps reading Q and once Q is
+ * unmounted it reads S'.
+ */
+TEST_F(overmount_ns_file, existing_mount_stays_on_top)
+{
+ char a[PATH_LEN], b[PATH_LEN], s[PATH_LEN], p[PATH_LEN], buf[16];
+ int fd, pfd[2];
+ char c;
+
+ snprintf(a, sizeof(a), "%s/A", self->dir);
+ snprintf(b, sizeof(b), "%s/B", self->dir);
+ snprintf(s, sizeof(s), "%s/s", self->dir);
+ ASSERT_EQ(mkdir(a, 0755), 0);
+ ASSERT_EQ(mkdir(b, 0755), 0);
+ ASSERT_EQ(mkdir(s, 0755), 0);
+
+ /* A shared, B its slave, Q on B/file */
+ ASSERT_EQ(mount("tmpfs", a, "tmpfs", 0, NULL), 0);
+ ASSERT_EQ(mount(NULL, a, NULL, MS_SHARED, NULL), 0);
+ snprintf(p, sizeof(p), "%s/A/file", self->dir);
+ ASSERT_EQ(create_file(p, "A"), 0);
+ ASSERT_EQ(mount(a, b, NULL, MS_BIND, NULL), 0);
+ ASSERT_EQ(mount(NULL, b, NULL, MS_SLAVE, NULL), 0);
+ snprintf(p, sizeof(p), "%s/Q", self->dir);
+ ASSERT_EQ(create_file(p, "Q"), 0);
+ snprintf(b, sizeof(b), "%s/B/file", self->dir);
+ ASSERT_EQ(mount(p, b, NULL, MS_BIND, NULL), 0);
+
+ /* S on s/f, pinned by a file descriptor before N goes on top */
+ ASSERT_EQ(mount("tmpfs", s, "tmpfs", 0, NULL), 0);
+ snprintf(p, sizeof(p), "%s/s/f", self->dir);
+ ASSERT_EQ(create_file(p, "f"), 0);
+ snprintf(s, sizeof(s), "%s/s/S", self->dir);
+ ASSERT_EQ(create_file(s, "S"), 0);
+ ASSERT_EQ(mount(s, p, NULL, MS_BIND, NULL), 0);
+ fd = open(p, O_PATH | O_CLOEXEC);
+ ASSERT_GE(fd, 0);
+
+ /* a newer mount namespace whose file can be bound */
+ ASSERT_EQ(pipe(pfd), 0);
+ self->child = fork();
+ ASSERT_GE(self->child, 0);
+ if (self->child == 0) {
+ if (unshare(CLONE_NEWNS) || write(pfd[1], "r", 1) != 1)
+ _exit(1);
+ pause();
+ _exit(0);
+ }
+ ASSERT_EQ(read(pfd[0], &c, 1), 1);
+ snprintf(s, sizeof(s), "/proc/%d/ns/mnt", self->child);
+ ASSERT_EQ(mount(s, p, NULL, MS_BIND, NULL), 0);
+
+ ASSERT_STREQ(read_file(b, buf, sizeof(buf)), "Q");
+
+ snprintf(a, sizeof(a), "%s/A/file", self->dir);
+ ASSERT_EQ(sys_move_mount(fd, "", AT_FDCWD, a, MOVE_MOUNT_F_EMPTY_PATH), 0);
+ close(fd);
+
+ /* Q is still on top of the copy in B and can be unmounted */
+ EXPECT_STREQ(read_file(b, buf, sizeof(buf)), "Q");
+ EXPECT_EQ(umount2(b, 0), 0);
+ EXPECT_STREQ(read_file(b, buf, sizeof(buf)), "S");
+}
+
+TEST_HARNESS_MAIN
--
2.53.0
next prev parent reply other threads:[~2026-09-30 13:32 UTC|newest]
Thread overview: 26+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-30 13:31 [PATCH 00/17] mount: more bugfixes, the Oprah edition Christian Brauner
2026-09-30 13:31 ` [PATCH 01/17] namespace: queue a mount only once for mount notifications Christian Brauner
2026-09-30 13:31 ` [PATCH 02/17] namespace: check a submount for references right before unmounting it Christian Brauner
2026-09-30 13:31 ` [PATCH 03/17] selftests/filesystems: check that a busy submount survives a synchronous umount Christian Brauner
2026-09-30 13:44 ` sashiko-bot
2026-09-30 13:31 ` [PATCH 04/17] namespace: check a recursive bind mount for mount namespace loops Christian Brauner
2026-09-30 13:31 ` [PATCH 05/17] selftests/filesystems: check that a recursive bind mount can't pin the caller's mount namespace Christian Brauner
2026-09-30 13:31 ` [PATCH 06/17] namespace: keep covered mounts covered in OPEN_TREE_NAMESPACE Christian Brauner
2026-09-30 13:31 ` [PATCH 07/17] selftests/filesystems: check that OPEN_TREE_NAMESPACE keeps mounts covered Christian Brauner
2026-09-30 13:43 ` sashiko-bot
2026-09-30 13:32 ` [PATCH 08/17] namespace: look at the topmost mount for a mount namespace file Christian Brauner
2026-09-30 13:32 ` Christian Brauner [this message]
2026-09-30 13:40 ` [PATCH 09/17] selftests/filesystems: check that a mount namespace file on top doesn't bury a mount sashiko-bot
2026-09-30 13:32 ` [PATCH 10/17] namespace: check the mounts before reading their parents in pivot_root() Christian Brauner
2026-09-30 13:32 ` [PATCH 11/17] namespace: don't reconfigure internal superblocks via remount and umount Christian Brauner
2026-09-30 13:32 ` [PATCH 12/17] selftests/filesystems: check that the nullfs root can't be reconfigured Christian Brauner
2026-09-30 13:32 ` [PATCH 13/17] namespace: remove the fsnotify marks of a mount namespace in process context Christian Brauner
2026-09-30 15:07 ` Amir Goldstein
2026-09-30 13:32 ` [PATCH 14/17] fsnotify: detach the connector before destroying its marks Christian Brauner
2026-09-30 13:57 ` sashiko-bot
2026-10-01 9:31 ` Christian Brauner
2026-10-01 10:58 ` Amir Goldstein
2026-10-01 12:06 ` Christian Brauner
2026-09-30 13:32 ` [PATCH 15/17] dcache: don't put a mountpoint on a dentry that's being removed Christian Brauner
2026-09-30 13:32 ` [PATCH 16/17] unshare: don't drop active namespace references that were never taken Christian Brauner
2026-09-30 13:32 ` [PATCH 17/17] namespace: don't let a pseudo dentry become the root of a mount Christian Brauner
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260930-work-mount-fixes-3-v1-9-be34c83956ae@kernel.org \
--to=brauner@kernel.org \
--cc=amir73il@gmail.com \
--cc=bpf@vger.kernel.org \
--cc=cyphar@cyphar.com \
--cc=jack@suse.cz \
--cc=jlayton@kernel.org \
--cc=linux-fsdevel@vger.kernel.org \
--cc=mason@kernel.org \
--cc=torvalds@linux-foundation.org \
--cc=viro@zeniv.linux.org.uk \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.