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 03/17] selftests/filesystems: check that a busy submount survives a synchronous umount
Date: Wed, 30 Sep 2026 15:31:55 +0200 [thread overview]
Message-ID: <20260930-work-mount-fixes-3-v1-3-be34c83956ae@kernel.org> (raw)
In-Reply-To: <20260930-work-mount-fixes-3-v1-0-be34c83956ae@kernel.org>
Add a test for the shrinkable submounts of a synchronous umount:
- P shared, P1 a slave that is shared in turn, P2 its peer
- B on P/options with copies on P1 and P2, R on top of the copy in P2
- P with B moved below P1, R is the working directory
- umount(P1) slides R to where the copy of B is looked up
The umount fails with EBUSY and R stays mounted. Needs the tracefs
automount below debugfs for the shrinkable mounts.
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
.../filesystems/umount_propagation/Makefile | 2 +-
.../umount_propagation/shrink_submounts_test.c | 205 +++++++++++++++++++++
2 files changed, 206 insertions(+), 1 deletion(-)
diff --git a/tools/testing/selftests/filesystems/umount_propagation/Makefile b/tools/testing/selftests/filesystems/umount_propagation/Makefile
index fc0a0783018b..eb85612abf8d 100644
--- a/tools/testing/selftests/filesystems/umount_propagation/Makefile
+++ b/tools/testing/selftests/filesystems/umount_propagation/Makefile
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: GPL-2.0
-TEST_GEN_PROGS := umount_propagation_test
+TEST_GEN_PROGS := umount_propagation_test shrink_submounts_test
CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES)
diff --git a/tools/testing/selftests/filesystems/umount_propagation/shrink_submounts_test.c b/tools/testing/selftests/filesystems/umount_propagation/shrink_submounts_test.c
new file mode 100644
index 000000000000..43efb7faf95c
--- /dev/null
+++ b/tools/testing/selftests/filesystems/umount_propagation/shrink_submounts_test.c
@@ -0,0 +1,205 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * A synchronous umount first unmounts the shrinkable submounts of the
+ * victim that aren't busy. Every one of them has to be checked right
+ * before it is unmounted: unmounting one can slide a busy mount to where
+ * the propagated copy of the next one is looked up.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/fanotify.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/statfs.h>
+#include <sys/vfs.h>
+#include <linux/magic.h>
+
+#include "../../kselftest_harness.h"
+
+#ifndef FAN_REPORT_MNT
+#define FAN_REPORT_MNT 0x00004000
+#endif
+#ifndef FAN_MARK_MNTNS
+#define FAN_MARK_MNTNS 0x00000110
+#endif
+#ifndef FAN_MNT_ATTACH
+#define FAN_MNT_ATTACH 0x01000000
+#endif
+#ifndef FAN_MNT_DETACH
+#define FAN_MNT_DETACH 0x02000000
+#endif
+
+#define DIR_LEN 64
+#define PATH_LEN 128
+
+FIXTURE(shrink_submounts) {
+ char base[DIR_LEN];
+ char automount[PATH_LEN];
+ bool mounted;
+ int fan;
+};
+
+/*
+ * Shrinkable mounts come from an automount. The tracefs mount below debugfs
+ * is one and bind mounts inherit the flag.
+ */
+FIXTURE_SETUP(shrink_submounts)
+{
+ struct stat st;
+ char p[PATH_LEN];
+
+ self->mounted = false;
+ self->fan = -1;
+
+ if (geteuid() != 0)
+ SKIP(return, "test requires CAP_SYS_ADMIN");
+
+ ASSERT_EQ(unshare(CLONE_NEWNS), 0);
+ ASSERT_EQ(mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL), 0);
+
+ snprintf(self->base, sizeof(self->base), "/tmp/shrink_submounts.XXXXXX");
+ ASSERT_NE(mkdtemp(self->base), NULL);
+ ASSERT_EQ(mount("tmpfs", self->base, "tmpfs", 0, NULL), 0);
+ self->mounted = true;
+ ASSERT_EQ(mount(NULL, self->base, NULL, MS_PRIVATE, NULL), 0);
+
+ snprintf(p, sizeof(p), "%s/dbg", self->base);
+ ASSERT_EQ(mkdir(p, 0755), 0);
+ if (mount("debugfs", p, "debugfs", 0, NULL))
+ SKIP(return, "test requires debugfs");
+ snprintf(self->automount, sizeof(self->automount), "%s/dbg/tracing",
+ self->base);
+ snprintf(p, sizeof(p), "%s/dbg/tracing/.", self->base);
+ if (stat(p, &st))
+ SKIP(return, "test requires the tracefs automount");
+}
+
+FIXTURE_TEARDOWN(shrink_submounts)
+{
+ if (self->fan >= 0)
+ close(self->fan);
+ chdir("/");
+ if (self->mounted)
+ umount2(self->base, MNT_DETACH);
+ rmdir(self->base);
+}
+
+static bool mounted_tmpfs(const char *path)
+{
+ struct statfs st;
+
+ return !statfs(path, &st) && st.f_type == TMPFS_MAGIC;
+}
+
+/*
+ * P is a shared bind mount of the automount, P1 a slave of P that is shared
+ * in turn and P2 its peer. B, another bind mount of the automount, goes on
+ * P/options and propagates copies Bc1 and Bc2 onto P1/options and
+ * P2/options. R, a tmpfs and our working directory, sits on top of Bc2 which
+ * is made private first. P, with B on it, is moved to P1/instances.
+ *
+ * A synchronous umount of P1 unmounts the shrinkable submounts Bc1 and B
+ * first. Unmounting Bc1 takes Bc2 along and slides R to P2/options where
+ * the propagated copy of B is looked up next. R is busy, so B has to stay
+ * and the umount fails with EBUSY.
+ */
+TEST_F(shrink_submounts, busy_mount_moved_into_reach)
+{
+ char p[PATH_LEN], p1[PATH_LEN], p2[PATH_LEN], r[PATH_LEN], cwd[PATH_LEN];
+ int nsfd;
+
+ snprintf(p, sizeof(p), "%s/p", self->base);
+ snprintf(p1, sizeof(p1), "%s/p1", self->base);
+ snprintf(p2, sizeof(p2), "%s/p2", self->base);
+ ASSERT_EQ(mkdir(p, 0755), 0);
+ ASSERT_EQ(mkdir(p1, 0755), 0);
+ ASSERT_EQ(mkdir(p2, 0755), 0);
+
+ /* watch the mount namespace so that the detached mounts get queued */
+ self->fan = fanotify_init(FAN_REPORT_MNT, O_RDONLY);
+ if (self->fan >= 0) {
+ nsfd = open("/proc/self/ns/mnt", O_RDONLY | O_CLOEXEC);
+ ASSERT_GE(nsfd, 0);
+ EXPECT_EQ(fanotify_mark(self->fan, FAN_MARK_ADD | FAN_MARK_MNTNS,
+ FAN_MNT_ATTACH | FAN_MNT_DETACH, nsfd, NULL), 0);
+ close(nsfd);
+ }
+
+ ASSERT_EQ(mount(self->automount, p, NULL, MS_BIND, NULL), 0);
+ ASSERT_EQ(mount(NULL, p, NULL, MS_SHARED, NULL), 0);
+ ASSERT_EQ(mount(p, p1, NULL, MS_BIND, NULL), 0);
+ ASSERT_EQ(mount(NULL, p1, NULL, MS_SLAVE, NULL), 0);
+ ASSERT_EQ(mount(NULL, p1, NULL, MS_SHARED, NULL), 0);
+ ASSERT_EQ(mount(p1, p2, NULL, MS_BIND, NULL), 0);
+
+ /* B on P/options, copies on P1/options and P2/options */
+ snprintf(r, sizeof(r), "%s/p/options", self->base);
+ ASSERT_EQ(mount(self->automount, r, NULL, MS_BIND, NULL), 0);
+
+ /* R on top of Bc2 */
+ snprintf(r, sizeof(r), "%s/p2/options", self->base);
+ ASSERT_EQ(mount(NULL, r, NULL, MS_PRIVATE, NULL), 0);
+ ASSERT_EQ(mount("R", r, "tmpfs", 0, NULL), 0);
+ ASSERT_EQ(chdir(r), 0);
+
+ /* P, with B on it, below the victim */
+ snprintf(cwd, sizeof(cwd), "%s/p1/instances", self->base);
+ ASSERT_EQ(mount(p, cwd, NULL, MS_MOVE, NULL), 0);
+
+ ASSERT_TRUE(mounted_tmpfs(r));
+ ASSERT_EQ(umount2(p1, 0), -1);
+ EXPECT_EQ(errno, EBUSY);
+
+ /* R is still mounted and still our working directory */
+ EXPECT_TRUE(mounted_tmpfs(r));
+ ASSERT_NE(getcwd(cwd, sizeof(cwd)), NULL);
+ EXPECT_STREQ(cwd, r);
+}
+
+/*
+ * T is shared and Q, a slave of T, has T moved into it, so Q receives
+ * propagation from its own child. M, another bind mount of the automount, is
+ * on T/options and that is the dentry T sits on in Q. Unmounting M makes T
+ * the propagated victim at that dentry in Q and takes T along. The shrink
+ * walk of V has just unmounted M and continues in the children of T.
+ */
+TEST_F(shrink_submounts, parent_goes_with_child)
+{
+ char v[PATH_LEN], t[PATH_LEN], q[PATH_LEN], p[PATH_LEN];
+ struct stat before, after;
+
+ snprintf(v, sizeof(v), "%s/v", self->base);
+ snprintf(t, sizeof(t), "%s/v/t", self->base);
+ snprintf(q, sizeof(q), "%s/v/q", self->base);
+ ASSERT_EQ(mkdir(v, 0755), 0);
+ ASSERT_EQ(mount("V", v, "tmpfs", 0, NULL), 0);
+ ASSERT_EQ(mount(NULL, v, NULL, MS_PRIVATE, NULL), 0);
+ ASSERT_EQ(stat(v, &before), 0);
+ ASSERT_EQ(mkdir(t, 0755), 0);
+ ASSERT_EQ(mkdir(q, 0755), 0);
+
+ /* T shared, M on T/options before anything receives from T */
+ ASSERT_EQ(mount(self->automount, t, NULL, MS_BIND, NULL), 0);
+ ASSERT_EQ(mount(NULL, t, NULL, MS_SHARED, NULL), 0);
+ snprintf(p, sizeof(p), "%s/v/t/options", self->base);
+ ASSERT_EQ(mount(self->automount, p, NULL, MS_BIND, NULL), 0);
+
+ /* Q, a slave of T, and T moved into Q at the dentry M sits on */
+ ASSERT_EQ(mount(t, q, NULL, MS_BIND, NULL), 0);
+ ASSERT_EQ(mount(NULL, q, NULL, MS_SLAVE, NULL), 0);
+ snprintf(p, sizeof(p), "%s/v/q/options", self->base);
+ ASSERT_EQ(mount(t, p, NULL, MS_MOVE, NULL), 0);
+
+ /* M, T and then Q go, V is empty and can be unmounted */
+ ASSERT_EQ(umount2(v, 0), 0);
+ ASSERT_EQ(stat(v, &after), 0);
+ EXPECT_NE(before.st_dev, after.st_dev);
+}
+
+TEST_HARNESS_MAIN
--
2.53.0
next prev parent reply other threads:[~2026-09-30 13:32 UTC|newest]
Thread overview: 22+ 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 ` Christian Brauner [this message]
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:32 ` [PATCH 08/17] namespace: look at the topmost mount for a mount namespace file Christian Brauner
2026-09-30 13:32 ` [PATCH 09/17] selftests/filesystems: check that a mount namespace file on top doesn't bury a mount Christian Brauner
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-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-3-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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox