From: Christian Brauner <brauner@kernel.org>
To: linux-fsdevel@vger.kernel.org
Cc: Alexander Viro <viro@zeniv.linux.org.uk>, Jan Kara <jack@suse.cz>,
"Christian Brauner (Amutable)" <brauner@kernel.org>
Subject: [PATCH 2/2] selftests/filesystems: test O_TMPFILE creation on idmapped mounts
Date: Mon, 15 Jun 2026 14:52:19 +0200 [thread overview]
Message-ID: <20260615-work-idmapped-tmpfile-v1-2-754a94d81f83@kernel.org> (raw)
In-Reply-To: <20260615-work-idmapped-tmpfile-v1-0-754a94d81f83@kernel.org>
Add a regression test for the fsuidgid_has_mapping() check in
vfs_tmpfile(). It idmaps a detached tmpfs mount so that the
caller-visible id range [0, 10000) maps onto the on-disk range
[10000, 20000) and checks that:
- a caller whose fsuid/fsgid fall outside that range cannot create an
O_TMPFILE through the mount and gets -EOVERFLOW instead of an inode
owned by (uid_t)-1;
- a mapped caller can create an O_TMPFILE, link it into the namespace,
and the ownership round-trips through the mount idmap: it is reported
as 0 through the mount and stored as 10000 on the underlying tmpfs.
The test runs entirely as root and uses setfsuid()/setfsgid() to become
the unmapped caller, so it needs no helper user. The layer directory is
world-writable so that an unmapped caller still clears the directory
permission check and reaches the fsuidgid_has_mapping() test.
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
tools/testing/selftests/filesystems/.gitignore | 1 +
tools/testing/selftests/filesystems/Makefile | 4 +
.../selftests/filesystems/idmapped_tmpfile.c | 168 +++++++++++++++++++++
3 files changed, 173 insertions(+)
diff --git a/tools/testing/selftests/filesystems/.gitignore b/tools/testing/selftests/filesystems/.gitignore
index 64ac0dfa46b7..a78f894157de 100644
--- a/tools/testing/selftests/filesystems/.gitignore
+++ b/tools/testing/selftests/filesystems/.gitignore
@@ -5,3 +5,4 @@ fclog
file_stressor
anon_inode_test
kernfs_test
+idmapped_tmpfile
diff --git a/tools/testing/selftests/filesystems/Makefile b/tools/testing/selftests/filesystems/Makefile
index 85427d7f19b9..a7ec2ba2dd83 100644
--- a/tools/testing/selftests/filesystems/Makefile
+++ b/tools/testing/selftests/filesystems/Makefile
@@ -2,6 +2,10 @@
CFLAGS += $(KHDR_INCLUDES)
TEST_GEN_PROGS := devpts_pts file_stressor anon_inode_test kernfs_test fclog
+TEST_GEN_PROGS += idmapped_tmpfile
TEST_GEN_PROGS_EXTENDED := dnotify_test
include ../lib.mk
+
+$(OUTPUT)/idmapped_tmpfile: LDLIBS += -lcap
+$(OUTPUT)/idmapped_tmpfile: utils.c
diff --git a/tools/testing/selftests/filesystems/idmapped_tmpfile.c b/tools/testing/selftests/filesystems/idmapped_tmpfile.c
new file mode 100644
index 000000000000..bc411ab8281e
--- /dev/null
+++ b/tools/testing/selftests/filesystems/idmapped_tmpfile.c
@@ -0,0 +1,168 @@
+// SPDX-License-Identifier: GPL-2.0
+#define _GNU_SOURCE
+
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <sched.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <sys/fsuid.h>
+#include <sys/stat.h>
+#include <sys/syscall.h>
+
+#include <linux/mount.h>
+#include <linux/types.h>
+
+#include "kselftest_harness.h"
+#include "wrappers.h"
+#include "utils.h"
+
+/*
+ * The test mount maps caller-visible ids [0, MAP_RANGE) onto the on-disk range
+ * [MAP_HOST, MAP_HOST + MAP_RANGE). An id outside [0, MAP_RANGE) therefore has
+ * no mapping in the mount and is not representable in the filesystem.
+ */
+#define MAP_HOST 10000
+#define MAP_RANGE 10000
+#define UNMAPPED 50000
+
+#ifndef MOUNT_ATTR_IDMAP
+#define MOUNT_ATTR_IDMAP 0x00100000
+#endif
+
+#ifndef __NR_mount_setattr
+#define __NR_mount_setattr 442
+#endif
+
+static inline int sys_mount_setattr(int dfd, const char *path,
+ unsigned int flags,
+ struct mount_attr *attr, size_t size)
+{
+ return syscall(__NR_mount_setattr, dfd, path, flags, attr, size);
+}
+
+/*
+ * Clone @path into a detached mount idmapped so that caller-visible ids
+ * [0, MAP_RANGE) map onto the on-disk ids [MAP_HOST, MAP_HOST + MAP_RANGE).
+ * Returns the mount fd, or -1 if idmapped mounts are not available.
+ */
+static int idmapped_clone(const char *path)
+{
+ struct mount_attr attr = {
+ .attr_set = MOUNT_ATTR_IDMAP,
+ };
+ int fd_tree, userns_fd, ret;
+
+ fd_tree = sys_open_tree(AT_FDCWD, path,
+ OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC);
+ if (fd_tree < 0)
+ return -1;
+
+ userns_fd = get_userns_fd(MAP_HOST, 0, MAP_RANGE);
+ if (userns_fd < 0) {
+ close(fd_tree);
+ return -1;
+ }
+
+ attr.userns_fd = userns_fd;
+ ret = sys_mount_setattr(fd_tree, "", AT_EMPTY_PATH, &attr, sizeof(attr));
+ close(userns_fd);
+ if (ret) {
+ close(fd_tree);
+ return -1;
+ }
+
+ return fd_tree;
+}
+
+FIXTURE(idmapped_tmpfile) {
+ char dir[64]; /* non-idmapped path to the layer directory */
+};
+
+FIXTURE_SETUP(idmapped_tmpfile)
+{
+ /* Private mount namespace so test mounts need no cleanup. */
+ ASSERT_EQ(unshare(CLONE_NEWNS), 0);
+ ASSERT_EQ(sys_mount(NULL, "/", NULL, MS_SLAVE | MS_REC, NULL), 0);
+ ASSERT_EQ(sys_mount("tmpfs", "/tmp", "tmpfs", 0, NULL), 0);
+
+ snprintf(self->dir, sizeof(self->dir), "/tmp/d");
+ ASSERT_EQ(mkdir(self->dir, 0777), 0);
+ /* World-writable so an unmapped caller still passes permission(). */
+ ASSERT_EQ(chmod(self->dir, 0777), 0);
+}
+
+FIXTURE_TEARDOWN(idmapped_tmpfile)
+{
+}
+
+/*
+ * A caller whose fsuid/fsgid have no mapping in the idmapped mount must not be
+ * able to create an O_TMPFILE. Without the check in vfs_tmpfile() the inode
+ * would be created owned by (uid_t)-1 and could then be linked into the
+ * namespace.
+ */
+TEST_F(idmapped_tmpfile, unmapped_caller_is_refused)
+{
+ int mfd, fd;
+
+ mfd = idmapped_clone(self->dir);
+ if (mfd < 0)
+ SKIP(return, "idmapped mounts not supported");
+
+ /* Become a caller outside the mount's [0, MAP_RANGE) range. */
+ setfsgid(UNMAPPED);
+ setfsuid(UNMAPPED);
+ ASSERT_EQ(setfsuid(-1), UNMAPPED);
+
+ fd = openat(mfd, ".", O_TMPFILE | O_WRONLY, 0644);
+ ASSERT_LT(fd, 0);
+ EXPECT_EQ(errno, EOVERFLOW);
+ if (fd >= 0)
+ close(fd);
+
+ EXPECT_EQ(close(mfd), 0);
+}
+
+/*
+ * A mapped caller can create an O_TMPFILE and link it into the namespace; the
+ * ownership round-trips through the mount idmap. This is what makes refusing
+ * the unmapped case above necessary in the first place.
+ */
+TEST_F(idmapped_tmpfile, mapped_caller_creates_and_links)
+{
+ char path[PATH_MAX];
+ struct stat st;
+ int mfd, fd;
+
+ mfd = idmapped_clone(self->dir);
+ if (mfd < 0)
+ SKIP(return, "idmapped mounts not supported");
+
+ /* Caller is uid/gid 0, which maps to MAP_HOST through the mount. */
+ fd = openat(mfd, ".", O_TMPFILE | O_RDWR, 0600);
+ ASSERT_GE(fd, 0);
+
+ ASSERT_EQ(fstat(fd, &st), 0);
+ EXPECT_EQ(st.st_uid, 0);
+ EXPECT_EQ(st.st_gid, 0);
+
+ /* The tmpfile is linkable: splice it into the directory. */
+ ASSERT_EQ(linkat(fd, "", mfd, "linked", AT_EMPTY_PATH), 0);
+ EXPECT_EQ(close(fd), 0);
+
+ ASSERT_EQ(fstatat(mfd, "linked", &st, 0), 0);
+ EXPECT_EQ(st.st_uid, 0);
+ EXPECT_EQ(st.st_gid, 0);
+
+ /* On the underlying, non-idmapped tmpfs it is stored as MAP_HOST. */
+ snprintf(path, sizeof(path), "%s/linked", self->dir);
+ ASSERT_EQ(stat(path, &st), 0);
+ EXPECT_EQ(st.st_uid, MAP_HOST);
+ EXPECT_EQ(st.st_gid, MAP_HOST);
+
+ EXPECT_EQ(close(mfd), 0);
+}
+
+TEST_HARNESS_MAIN
--
2.47.3
next prev parent reply other threads:[~2026-06-15 12:52 UTC|newest]
Thread overview: 11+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-15 12:52 [PATCH 0/2] fs: refuse O_TMPFILE creation with an unmapped fsuid or fsgid Christian Brauner
2026-06-15 12:52 ` [PATCH 1/2] " Christian Brauner
2026-06-15 13:29 ` Jan Kara
2026-07-04 11:31 ` Christoph Hellwig
2026-07-07 2:18 ` Darrick J. Wong
2026-07-07 7:20 ` Christian Brauner
2026-07-07 11:20 ` Christian Brauner
2026-07-07 16:47 ` Darrick J. Wong
2026-07-10 5:31 ` Christoph Hellwig
2026-06-15 12:52 ` Christian Brauner [this message]
2026-06-15 13:33 ` [PATCH 2/2] selftests/filesystems: test O_TMPFILE creation on idmapped mounts Jan Kara
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=20260615-work-idmapped-tmpfile-v1-2-754a94d81f83@kernel.org \
--to=brauner@kernel.org \
--cc=jack@suse.cz \
--cc=linux-fsdevel@vger.kernel.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.