All of lore.kernel.org
 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 07/17] selftests/filesystems: check that OPEN_TREE_NAMESPACE keeps mounts covered
Date: Wed, 30 Sep 2026 15:31:59 +0200	[thread overview]
Message-ID: <20260930-work-mount-fixes-3-v1-7-be34c83956ae@kernel.org> (raw)
In-Reply-To: <20260930-work-mount-fixes-3-v1-0-be34c83956ae@kernel.org>

Add a test for open_tree(OPEN_TREE_NAMESPACE) from a user namespace that
doesn't own the mount namespace it copies from:

- a file covered by a private or an unbindable tmpfs mount
- the non-recursive copy of the parent mount fails with EINVAL
- the recursive copy keeps the file covered in the new mount namespace
- the owner of the mount namespace keeps the bind mount semantics

Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
 .../selftests/filesystems/open_tree_ns/.gitignore  |   1 +
 .../selftests/filesystems/open_tree_ns/Makefile    |   2 +-
 .../open_tree_ns/open_tree_ns_covered_test.c       | 183 +++++++++++++++++++++
 3 files changed, 185 insertions(+), 1 deletion(-)

diff --git a/tools/testing/selftests/filesystems/open_tree_ns/.gitignore b/tools/testing/selftests/filesystems/open_tree_ns/.gitignore
index fb12b93fbcaa..76f95c0ae5ef 100644
--- a/tools/testing/selftests/filesystems/open_tree_ns/.gitignore
+++ b/tools/testing/selftests/filesystems/open_tree_ns/.gitignore
@@ -1 +1,2 @@
 open_tree_ns_test
+open_tree_ns_covered_test
diff --git a/tools/testing/selftests/filesystems/open_tree_ns/Makefile b/tools/testing/selftests/filesystems/open_tree_ns/Makefile
index 4976ed1d7d4a..fb2aa77b6edb 100644
--- a/tools/testing/selftests/filesystems/open_tree_ns/Makefile
+++ b/tools/testing/selftests/filesystems/open_tree_ns/Makefile
@@ -1,5 +1,5 @@
 # SPDX-License-Identifier: GPL-2.0
-TEST_GEN_PROGS := open_tree_ns_test
+TEST_GEN_PROGS := open_tree_ns_test open_tree_ns_covered_test
 
 CFLAGS += -Wall -O0 -g $(KHDR_INCLUDES) $(TOOLS_INCLUDES)
 LDLIBS := -lcap
diff --git a/tools/testing/selftests/filesystems/open_tree_ns/open_tree_ns_covered_test.c b/tools/testing/selftests/filesystems/open_tree_ns/open_tree_ns_covered_test.c
new file mode 100644
index 000000000000..1b2f1385326a
--- /dev/null
+++ b/tools/testing/selftests/filesystems/open_tree_ns/open_tree_ns_covered_test.c
@@ -0,0 +1,183 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * open_tree(OPEN_TREE_NAMESPACE) by a caller that isn't privileged over the
+ * mount namespace it copies from must not reveal what the mounts below the
+ * copied mount cover. Without AT_RECURSIVE the copy is refused when there's
+ * anything mounted below the requested directory. With AT_RECURSIVE
+ * unbindable mounts are copied as well.
+ */
+#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/mount.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+
+#include "../wrappers.h"
+#include "../../kselftest_harness.h"
+
+#ifndef OPEN_TREE_NAMESPACE
+#define OPEN_TREE_NAMESPACE	(1 << 1)
+#endif
+
+#define DIR_LEN 64
+#define PATH_LEN 128
+
+/* Child exit codes. */
+enum {
+	CHILD_OK,
+	CHILD_USERNS,		/* could not create the child user namespace */
+	CHILD_NONREC_ALLOWED,	/* the non-recursive copy was not refused */
+	CHILD_NONREC_ERRNO,	/* it was refused with the wrong error */
+	CHILD_REC_REFUSED,	/* the recursive copy failed */
+	CHILD_SETNS,		/* could not enter the new mount namespace */
+	CHILD_NO_COVER,		/* the covering mount is missing in the copy */
+	CHILD_REVEALED,		/* the covered file is visible in the copy */
+};
+
+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;
+}
+
+/* Become root in a new user namespace, the uid @uid is mapped to 0. */
+static int enter_userns(uid_t uid, gid_t gid)
+{
+	char map[32];
+
+	if (unshare(CLONE_NEWUSER))
+		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;
+	return setgid(0) || setuid(0) ? -1 : 0;
+}
+
+FIXTURE(open_tree_ns_covered) {
+	char dir[DIR_LEN];
+	char cover[PATH_LEN];
+};
+
+FIXTURE_VARIANT(open_tree_ns_covered) {
+	int propagation;
+};
+
+FIXTURE_VARIANT_ADD(open_tree_ns_covered, private_cover) {
+	.propagation = MS_PRIVATE,
+};
+
+FIXTURE_VARIANT_ADD(open_tree_ns_covered, unbindable_cover) {
+	.propagation = MS_UNBINDABLE,
+};
+
+/*
+ * Root in a user namespace owns a private mount namespace with a tmpfs
+ * on @dir and a second tmpfs covering @dir/covered/under.txt.
+ */
+FIXTURE_SETUP(open_tree_ns_covered)
+{
+	char p[PATH_LEN];
+
+	snprintf(self->dir, sizeof(self->dir), "/tmp/open_tree_ns_covered.XXXXXX");
+	ASSERT_NE(mkdtemp(self->dir), NULL);
+	if (enter_userns(getuid(), getgid()) || unshare(CLONE_NEWNS)) {
+		rmdir(self->dir);
+		SKIP(return, "test requires user namespaces");
+	}
+	ASSERT_EQ(mount("", "/", NULL, MS_REC | MS_PRIVATE, NULL), 0);
+	ASSERT_EQ(mount("tmpfs", self->dir, "tmpfs", 0, NULL), 0);
+
+	snprintf(self->cover, sizeof(self->cover), "%s/covered", self->dir);
+	ASSERT_EQ(mkdir(self->cover, 0755), 0);
+	snprintf(p, sizeof(p), "%s/covered/under.txt", self->dir);
+	ASSERT_EQ(create_file(p, "hidden"), 0);
+	ASSERT_EQ(mount("tmpfs", self->cover, "tmpfs", 0, NULL), 0);
+	ASSERT_EQ(mount(NULL, self->cover, NULL, variant->propagation, NULL), 0);
+}
+
+FIXTURE_TEARDOWN(open_tree_ns_covered)
+{
+	umount2(self->dir, MNT_DETACH);
+	rmdir(self->dir);
+}
+
+/* A caller in a new user namespace that doesn't own the mount namespace. */
+static int foreign_child(const char *dir)
+{
+	struct stat st;
+	int fd;
+
+	if (enter_userns(0, 0))
+		return CHILD_USERNS;
+
+	fd = sys_open_tree(AT_FDCWD, dir, OPEN_TREE_NAMESPACE | OPEN_TREE_CLOEXEC);
+	if (fd >= 0)
+		return CHILD_NONREC_ALLOWED;
+	if (errno != EINVAL)
+		return CHILD_NONREC_ERRNO;
+
+	fd = sys_open_tree(AT_FDCWD, dir,
+			   OPEN_TREE_NAMESPACE | OPEN_TREE_CLOEXEC | AT_RECURSIVE);
+	if (fd < 0)
+		return CHILD_REC_REFUSED;
+	if (setns(fd, CLONE_NEWNS))
+		return CHILD_SETNS;
+	if (stat("/covered", &st))
+		return CHILD_NO_COVER;
+	if (!access("/covered/under.txt", F_OK) || errno != ENOENT)
+		return CHILD_REVEALED;
+	return CHILD_OK;
+}
+
+TEST_F(open_tree_ns_covered, foreign_user_namespace)
+{
+	int status, fd;
+	pid_t pid;
+
+	pid = fork();
+	ASSERT_GE(pid, 0);
+	if (pid == 0)
+		_exit(foreign_child(self->dir));
+	ASSERT_EQ(waitpid(pid, &status, 0), pid);
+	ASSERT_TRUE(WIFEXITED(status));
+	ASSERT_EQ(WEXITSTATUS(status), CHILD_OK);
+
+	/* the owner of the mount namespace keeps bind mount semantics */
+	fd = sys_open_tree(AT_FDCWD, self->dir,
+			   OPEN_TREE_NAMESPACE | OPEN_TREE_CLOEXEC);
+	ASSERT_GE(fd, 0);
+	close(fd);
+}
+
+TEST_HARNESS_MAIN

-- 
2.53.0


  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 ` Christian Brauner [this message]
2026-09-30 13:43   ` [PATCH 07/17] selftests/filesystems: check that OPEN_TREE_NAMESPACE keeps mounts covered 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 ` [PATCH 09/17] selftests/filesystems: check that a mount namespace file on top doesn't bury a mount Christian Brauner
2026-09-30 13:40   ` 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-7-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.