Linux filesystem development
 help / color / mirror / Atom feed
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


  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