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>,
	 Alexander Viro <viro@zeniv.linux.org.uk>,
	Jan Kara <jack@suse.cz>,
	 "Christian Brauner (Amutable)" <brauner@kernel.org>
Subject: [PATCH 6/8] selftests/filesystems: check that a busy propagated copy blocks a synchronous umount
Date: Wed, 23 Sep 2026 14:27:58 +0200	[thread overview]
Message-ID: <20260923-work-mount-fixes-v1-6-f424cf8d3242@kernel.org> (raw)
In-Reply-To: <20260923-work-mount-fixes-v1-0-f424cf8d3242@kernel.org>

A slave namespace moves an open_tree() copy of the shared tree beneath
the propagated copy of the victim and keeps the descriptor. Check that:

- umount(2) of the victim fails with EBUSY while the descriptor is open

- the moved tree is still attached in the slave namespace afterwards

- the umount succeeds once the descriptor is closed

The test needs CAP_SYS_ADMIN and skips otherwise.

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 tools/testing/selftests/Makefile                   |   1 +
 .../filesystems/umount_propagation/Makefile        |   6 +
 .../umount_propagation/umount_propagation_test.c   | 226 +++++++++++++++++++++
 3 files changed, 233 insertions(+)

diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
index a3df9a15ebb7..43d4a33afe71 100644
--- a/tools/testing/selftests/Makefile
+++ b/tools/testing/selftests/Makefile
@@ -51,6 +51,7 @@ TARGETS += filesystems/fsmount_ns
 TARGETS += filesystems/fscontext_ns
 TARGETS += filesystems/xattr
 TARGETS += filesystems/mntns_unbindable
+TARGETS += filesystems/umount_propagation
 TARGETS += firmware
 TARGETS += fpu
 TARGETS += ftrace
diff --git a/tools/testing/selftests/filesystems/umount_propagation/Makefile b/tools/testing/selftests/filesystems/umount_propagation/Makefile
new file mode 100644
index 000000000000..fc0a0783018b
--- /dev/null
+++ b/tools/testing/selftests/filesystems/umount_propagation/Makefile
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: GPL-2.0
+TEST_GEN_PROGS := umount_propagation_test
+
+CFLAGS += -Wall -O2 -g $(KHDR_INCLUDES)
+
+include ../../lib.mk
diff --git a/tools/testing/selftests/filesystems/umount_propagation/umount_propagation_test.c b/tools/testing/selftests/filesystems/umount_propagation/umount_propagation_test.c
new file mode 100644
index 000000000000..9e18d54dfb32
--- /dev/null
+++ b/tools/testing/selftests/filesystems/umount_propagation/umount_propagation_test.c
@@ -0,0 +1,226 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * A synchronous umount fails with EBUSY when a mount it would pull out by
+ * propagation is still in use.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/syscall.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#include <linux/mount.h>
+#include <linux/stat.h>
+
+#include "../../kselftest_harness.h"
+
+#ifndef OPEN_TREE_CLONE
+#define OPEN_TREE_CLONE		1
+#endif
+#ifndef OPEN_TREE_CLOEXEC
+#define OPEN_TREE_CLOEXEC	O_CLOEXEC
+#endif
+#ifndef AT_RECURSIVE
+#define AT_RECURSIVE		0x8000
+#endif
+#ifndef MOVE_MOUNT_F_EMPTY_PATH
+#define MOVE_MOUNT_F_EMPTY_PATH	0x00000004
+#endif
+#ifndef MOVE_MOUNT_BENEATH
+#define MOVE_MOUNT_BENEATH	0x00000200
+#endif
+#ifndef STATX_MNT_ID
+#define STATX_MNT_ID		0x00001000U
+#endif
+
+static int sys_open_tree(int dfd, const char *filename, unsigned int flags)
+{
+	return syscall(__NR_open_tree, dfd, filename, flags);
+}
+
+static int sys_move_mount(int from_dfd, const char *from_pathname,
+			  int to_dfd, const char *to_pathname,
+			  unsigned int flags)
+{
+	return syscall(__NR_move_mount, from_dfd, from_pathname, to_dfd,
+		       to_pathname, flags);
+}
+
+/* Child exit codes. */
+enum {
+	CHILD_OK,
+	CHILD_UNSHARE,		/* could not set up the slave namespace */
+	CHILD_OPEN_TREE,	/* open_tree() failed */
+	CHILD_MOVE_MOUNT,	/* move_mount() failed */
+	CHILD_STATX,		/* statx() failed */
+	CHILD_PIPE,		/* the parent went away */
+};
+
+/* Messages between parent and child. */
+enum {
+	MSG_READY = 'r',	/* child: the copy is mounted and referenced */
+	MSG_CHECK = 'c',	/* parent: check that the copy is still attached */
+	MSG_ATTACHED = 'a',	/* child: it is */
+	MSG_DETACHED = 'd',	/* child: it is not */
+	MSG_CLOSE = 'x',	/* parent: drop the reference */
+	MSG_CLOSED = 'y',	/* child: dropped */
+	MSG_EXIT = 'e',		/* parent: done */
+};
+
+FIXTURE(umount_propagation)
+{
+	char base[64];
+	char victim[80];
+	bool mounted;
+};
+
+FIXTURE_SETUP(umount_propagation)
+{
+	self->mounted = false;
+
+	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/umount_propagation.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_SHARED, NULL), 0);
+
+	snprintf(self->victim, sizeof(self->victim), "%s/victim", self->base);
+	ASSERT_EQ(mkdir(self->victim, 0755), 0);
+	ASSERT_EQ(mount("tmpfs", self->victim, "tmpfs", 0, NULL), 0);
+}
+
+FIXTURE_TEARDOWN(umount_propagation)
+{
+	if (self->mounted)
+		umount2(self->base, MNT_DETACH);
+	rmdir(self->base);
+}
+
+static int send_msg(int fd, char msg)
+{
+	return write(fd, &msg, 1) == 1 ? 0 : -1;
+}
+
+static char recv_msg(int fd)
+{
+	char msg;
+
+	if (read(fd, &msg, 1) != 1)
+		return 0;
+	return msg;
+}
+
+/* Is the mount with id @mnt_id attached in this mount namespace? */
+static bool mount_attached(__u64 mnt_id)
+{
+	char line[4096];
+	bool found = false;
+	FILE *f;
+
+	f = fopen("/proc/self/mountinfo", "re");
+	if (!f)
+		return false;
+
+	while (fgets(line, sizeof(line), f)) {
+		if (strtoull(line, NULL, 10) == mnt_id) {
+			found = true;
+			break;
+		}
+	}
+	fclose(f);
+	return found;
+}
+
+/*
+ * The slave namespace: take a detached copy of the shared tree and move it
+ * beneath the propagated copy of the victim, keeping the open_tree()
+ * descriptor as a reference on it.
+ */
+static int slave_child(const char *base, const char *victim, int to_parent,
+		       int from_parent)
+{
+	struct statx stx;
+	int fd;
+
+	if (unshare(CLONE_NEWNS))
+		return CHILD_UNSHARE;
+	if (mount("", "/", NULL, MS_REC | MS_SLAVE, NULL))
+		return CHILD_UNSHARE;
+
+	fd = sys_open_tree(AT_FDCWD, base,
+			   OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC | AT_RECURSIVE);
+	if (fd < 0)
+		return CHILD_OPEN_TREE;
+	if (sys_move_mount(fd, "", AT_FDCWD, victim,
+			   MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_BENEATH))
+		return CHILD_MOVE_MOUNT;
+	if (statx(fd, "", AT_EMPTY_PATH, STATX_MNT_ID, &stx))
+		return CHILD_STATX;
+
+	if (send_msg(to_parent, MSG_READY) || recv_msg(from_parent) != MSG_CHECK)
+		return CHILD_PIPE;
+	if (send_msg(to_parent, mount_attached(stx.stx_mnt_id) ?
+				MSG_ATTACHED : MSG_DETACHED))
+		return CHILD_PIPE;
+
+	if (recv_msg(from_parent) != MSG_CLOSE)
+		return CHILD_PIPE;
+	close(fd);
+	if (send_msg(to_parent, MSG_CLOSED) || recv_msg(from_parent) != MSG_EXIT)
+		return CHILD_PIPE;
+	return CHILD_OK;
+}
+
+TEST_F(umount_propagation, busy_copy_pulled_out)
+{
+	int to_child[2], to_parent[2];
+	int status;
+	pid_t pid;
+
+	ASSERT_EQ(pipe(to_child), 0);
+	ASSERT_EQ(pipe(to_parent), 0);
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0) {
+		close(to_child[1]);
+		close(to_parent[0]);
+		_exit(slave_child(self->base, self->victim, to_parent[1],
+				  to_child[0]));
+	}
+	close(to_child[0]);
+	close(to_parent[1]);
+
+	ASSERT_EQ(recv_msg(to_parent[0]), MSG_READY);
+
+	/* the copy in the slave namespace is in use */
+	ASSERT_EQ(umount2(self->victim, 0), -1);
+	ASSERT_EQ(errno, EBUSY);
+
+	ASSERT_EQ(send_msg(to_child[1], MSG_CHECK), 0);
+	ASSERT_EQ(recv_msg(to_parent[0]), MSG_ATTACHED);
+
+	/* and once it is not, the umount goes through */
+	ASSERT_EQ(send_msg(to_child[1], MSG_CLOSE), 0);
+	ASSERT_EQ(recv_msg(to_parent[0]), MSG_CLOSED);
+	ASSERT_EQ(umount2(self->victim, 0), 0);
+
+	ASSERT_EQ(send_msg(to_child[1], MSG_EXIT), 0);
+	ASSERT_EQ(waitpid(pid, &status, 0), pid);
+	ASSERT_TRUE(WIFEXITED(status));
+	ASSERT_EQ(WEXITSTATUS(status), CHILD_OK);
+}
+
+TEST_HARNESS_MAIN

-- 
2.53.0


  parent reply	other threads:[~2026-09-23 12:28 UTC|newest]

Thread overview: 9+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-23 12:27 [PATCH 0/8] mount: a few gnarly fixes Christian Brauner
2026-09-23 12:27 ` [PATCH 1/8] mount: keep a copied mount unbindable Christian Brauner
2026-09-23 12:27 ` [PATCH 2/8] selftests/filesystems: check that a copied mount namespace keeps unbindable Christian Brauner
2026-09-23 12:27 ` [PATCH 3/8] mount: refuse MOVE_MOUNT_SET_GROUP on an unbindable mount Christian Brauner
2026-09-23 12:27 ` [PATCH 4/8] selftests/move_mount_set_group: check that an unbindable target is refused Christian Brauner
2026-09-23 12:27 ` [PATCH 5/8] fs: don't silently unmount busy mounts Christian Brauner
2026-09-23 12:27 ` Christian Brauner [this message]
2026-09-23 12:27 ` [PATCH 7/8] fs: don't let a migrating task hide its reference from do_umount() Christian Brauner
2026-09-23 12:28 ` [PATCH 8/8] docs: update the unmount propagation rule 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=20260923-work-mount-fixes-v1-6-f424cf8d3242@kernel.org \
    --to=brauner@kernel.org \
    --cc=jack@suse.cz \
    --cc=linux-fsdevel@vger.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