linux-fsdevel.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
From: Matt Bobrowski <matt@bobrowski.net>
To: daan@amutable.com
Cc: Christian Brauner <brauner@kernel.org>, Jan Kara <jack@suse.cz>,
	Alexander Viro <viro@zeniv.linux.org.uk>,
	Paul Moore <paul@paul-moore.com>,
	linux-fsdevel@vger.kernel.org,
	linux-security-module@vger.kernel.org
Subject: Re: [PATCH v3 2/2] selftests/bpf: verify mount idmaps reach inode hooks
Date: Fri, 11 Sep 2026 13:25:28 +1000	[thread overview]
Message-ID: <aqN0EGbbHk8XMqI6@lima-development> (raw)
In-Reply-To: <20260904-lsm-mount-idmaps-v3-2-920a1963675d@amutable.com>

On Fri, Sep 04, 2026 at 04:48:56PM +0200, Daan De Meyer via B4 Relay wrote:
> From: Daan De Meyer <daan@amutable.com>
> 
> Extend the BPF LSM selftest to exercise create, link, symlink, mkdir,
> mknod, and permission through both the VFS identity idmap and a real
> idmapped tmpfs mount.
> 
> Record the idmap observed by each hook and verify that every updated hook
> receives the mapping used by the VFS operation. This provides regression
> coverage for passing mount idmaps through inode security hooks.
> 
> Signed-off-by: Daan De Meyer <daan@amutable.com>

This all looks fine to me. Feel free to add:

Reviewed-by: Matt Bobrowski <matt@bobrowski.net>

> ---
>  tools/testing/selftests/bpf/prog_tests/test_lsm.c | 231 ++++++++++++++++++++++
>  tools/testing/selftests/bpf/progs/lsm.c           |  79 ++++++++
>  2 files changed, 310 insertions(+)
> 
> diff --git a/tools/testing/selftests/bpf/prog_tests/test_lsm.c b/tools/testing/selftests/bpf/prog_tests/test_lsm.c
> index d7495efd4a56..c0ae813698ae 100644
> --- a/tools/testing/selftests/bpf/prog_tests/test_lsm.c
> +++ b/tools/testing/selftests/bpf/prog_tests/test_lsm.c
> @@ -1,18 +1,30 @@
>  // SPDX-License-Identifier: GPL-2.0
> +#define _GNU_SOURCE
>  
>  /*
>   * Copyright (C) 2020 Google LLC.
>   */
>  
>  #include <test_progs.h>
> +#include <sched.h>
> +#include <signal.h>
> +#include <string.h>
> +#include <sys/stat.h>
> +#include <sys/syscall.h>
>  #include <sys/wait.h>
>  #include <unistd.h>
>  
> +#include <linux/mount.h>
> +
>  #include "lsm.skel.h"
>  #include "lsm_tailcall.skel.h"
>  
>  char *CMD_ARGS[] = {"true", NULL};
>  
> +enum {
> +	INODE_IDMAP_ALL = (1U << 6) - 1,
> +};
> +
>  int exec_cmd(int *monitored_pid)
>  {
>  	int child_pid, child_status;
> @@ -30,6 +42,219 @@ int exec_cmd(int *monitored_pid)
>  	return -EINVAL;
>  }
>  
> +static ssize_t write_nointr(int fd, const void *buf, size_t count)
> +{
> +	ssize_t ret;
> +
> +	do {
> +		ret = write(fd, buf, count);
> +	} while (ret < 0 && errno == EINTR);
> +
> +	return ret;
> +}
> +
> +static int write_file(const char *path, const char *value)
> +{
> +	size_t len = strlen(value);
> +	int fd, saved_errno = 0;
> +	ssize_t ret;
> +
> +	fd = open(path, O_WRONLY | O_CLOEXEC | O_NOCTTY | O_NOFOLLOW);
> +	if (fd < 0)
> +		return -1;
> +
> +	ret = write_nointr(fd, value, len);
> +	if (ret < 0)
> +		saved_errno = errno;
> +	else if ((size_t)ret != len)
> +		saved_errno = EIO;
> +	close(fd);
> +	if (saved_errno) {
> +		errno = saved_errno;
> +		return -1;
> +	}
> +	return 0;
> +}
> +
> +static int write_userns_file(pid_t pid, const char *name, const char *value)
> +{
> +	char path[64];
> +	int len;
> +
> +	len = snprintf(path, sizeof(path), "/proc/%d/%s", pid, name);
> +	if (len < 0 || (size_t)len >= sizeof(path)) {
> +		errno = EOVERFLOW;
> +		return -1;
> +	}
> +
> +	return write_file(path, value);
> +}
> +
> +static int create_userns_fd(void)
> +{
> +	char path[64];
> +	pid_t pid, waited;
> +	int fd = -1, len, saved_errno, status;
> +
> +	pid = fork();
> +	if (pid < 0)
> +		return -1;
> +	if (pid == 0) {
> +		if (unshare(CLONE_NEWUSER))
> +			_exit(1);
> +		raise(SIGSTOP);
> +		_exit(0);
> +	}
> +
> +	do {
> +		waited = waitpid(pid, &status, WUNTRACED);
> +	} while (waited < 0 && errno == EINTR);
> +	if (waited != pid)
> +		goto out;
> +	if (!WIFSTOPPED(status)) {
> +		pid = -1;
> +		goto out;
> +	}
> +
> +	/* A one-entry map is identity for root but remains distinct from nop_mnt_idmap. */
> +	if (write_userns_file(pid, "setgroups", "deny") && errno != ENOENT)
> +		goto out;
> +	if (write_userns_file(pid, "uid_map", "0 0 1") ||
> +	    write_userns_file(pid, "gid_map", "0 0 1"))
> +		goto out;
> +
> +	len = snprintf(path, sizeof(path), "/proc/%d/ns/user", pid);
> +	if (len < 0 || (size_t)len >= sizeof(path)) {
> +		errno = EOVERFLOW;
> +		goto out;
> +	}
> +	fd = open(path, O_RDONLY | O_CLOEXEC);
> +
> +out:
> +	saved_errno = errno;
> +	if (pid > 0) {
> +		kill(pid, SIGKILL);
> +		do {
> +			waited = waitpid(pid, NULL, 0);
> +		} while (waited < 0 && errno == EINTR);
> +	}
> +	errno = saved_errno;
> +	return fd;
> +}
> +
> +static int create_idmapped_tmpfs(void)
> +{
> +	struct mount_attr attr = {
> +		.attr_set = MOUNT_ATTR_IDMAP,
> +	};
> +	int fsfd = -1, mntfd = -1, saved_errno, userns_fd = -1;
> +
> +	userns_fd = create_userns_fd();
> +	if (userns_fd < 0)
> +		goto out;
> +
> +	/* A detached tmpfs avoids relying on the host test directory supporting idmaps. */
> +	fsfd = syscall(__NR_fsopen, "tmpfs", FSOPEN_CLOEXEC);
> +	if (fsfd < 0)
> +		goto out;
> +	if (syscall(__NR_fsconfig, fsfd, FSCONFIG_CMD_CREATE, NULL, NULL, 0))
> +		goto out;
> +
> +	mntfd = syscall(__NR_fsmount, fsfd, FSMOUNT_CLOEXEC, 0);
> +	if (mntfd < 0)
> +		goto out;
> +
> +	attr.userns_fd = userns_fd;
> +	if (syscall(__NR_mount_setattr, mntfd, "", AT_EMPTY_PATH, &attr,
> +		    sizeof(attr))) {
> +		close(mntfd);
> +		mntfd = -1;
> +	}
> +
> +out:
> +	saved_errno = errno;
> +	if (fsfd >= 0)
> +		close(fsfd);
> +	if (userns_fd >= 0)
> +		close(userns_fd);
> +	errno = saved_errno;
> +	return mntfd;
> +}
> +
> +static int exercise_inode_idmap_hooks(int dirfd)
> +{
> +	int fd = -1, ret = -1;
> +
> +	fd = openat(dirfd, "file", O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC,
> +		    0600);
> +	if (!ASSERT_GE(fd, 0, "create"))
> +		goto out;
> +	close(fd);
> +	fd = -1;
> +
> +	if (!ASSERT_OK(mkdirat(dirfd, "dir", 0700), "mkdir"))
> +		goto out;
> +	if (!ASSERT_OK(symlinkat("target", dirfd, "symlink"), "symlink"))
> +		goto out;
> +	if (!ASSERT_OK(linkat(dirfd, "file", dirfd, "link", 0), "link"))
> +		goto out;
> +	if (!ASSERT_OK(mkfifoat(dirfd, "fifo", 0600), "mknod"))
> +		goto out;
> +
> +	ret = 0;
> +out:
> +	if (fd >= 0)
> +		close(fd);
> +	unlinkat(dirfd, "link", 0);
> +	unlinkat(dirfd, "fifo", 0);
> +	unlinkat(dirfd, "symlink", 0);
> +	unlinkat(dirfd, "file", 0);
> +	unlinkat(dirfd, "dir", AT_REMOVEDIR);
> +	return ret;
> +}
> +
> +static int test_lsm_inode_idmap(struct lsm *skel)
> +{
> +	char tmpdir[] = "/var/tmp/test_lsm_idmap.XXXXXX";
> +	__u32 expected = INODE_IDMAP_ALL;
> +	int dirfd = -1, idmapped_dirfd = -1;
> +	int ret = -1;
> +
> +	if (!ASSERT_OK_PTR(mkdtemp(tmpdir), "mkdtemp"))
> +		return -1;
> +
> +	dirfd = open(tmpdir, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
> +	if (!ASSERT_GE(dirfd, 0, "open_tmpdir"))
> +		goto out;
> +
> +	idmapped_dirfd = create_idmapped_tmpfs();
> +	if (!ASSERT_GE(idmapped_dirfd, 0, "create_idmapped_tmpfs"))
> +		goto out;
> +
> +	skel->bss->inode_identity_idmap_seen = 0;
> +	skel->bss->inode_idmapped_mount_seen = 0;
> +
> +	if (!ASSERT_OK(exercise_inode_idmap_hooks(dirfd), "identity_idmap"))
> +		goto out;
> +	if (!ASSERT_OK(exercise_inode_idmap_hooks(idmapped_dirfd),
> +		       "idmapped_mount"))
> +		goto out;
> +
> +	if (!ASSERT_EQ(skel->bss->inode_identity_idmap_seen, expected,
> +		       "inode_identity_idmap_seen"))
> +		goto out;
> +	ret = ASSERT_EQ(skel->bss->inode_idmapped_mount_seen, expected,
> +			"inode_idmapped_mount_seen") ? 0 : -1;
> +
> +out:
> +	if (idmapped_dirfd >= 0)
> +		close(idmapped_dirfd);
> +	if (dirfd >= 0)
> +		close(dirfd);
> +	rmdir(tmpdir);
> +	return ret;
> +}
> +
>  static int test_lsm(struct lsm *skel)
>  {
>  	struct bpf_link *link;
> @@ -53,6 +278,10 @@ static int test_lsm(struct lsm *skel)
>  
>  	skel->bss->monitored_pid = getpid();
>  
> +	err = test_lsm_inode_idmap(skel);
> +	if (!ASSERT_OK(err, "test_lsm_inode_idmap"))
> +		return err;
> +
>  	err = stack_mprotect();
>  	if (!ASSERT_EQ(err, -1, "stack_mprotect") ||
>  	    !ASSERT_EQ(errno, EPERM, "stack_mprotect"))
> @@ -71,6 +300,8 @@ static int test_lsm(struct lsm *skel)
>  	skel->bss->copy_test = 0;
>  	skel->bss->bprm_count = 0;
>  	skel->bss->mprotect_count = 0;
> +	skel->bss->inode_identity_idmap_seen = 0;
> +	skel->bss->inode_idmapped_mount_seen = 0;
>  	return 0;
>  }
>  
> diff --git a/tools/testing/selftests/bpf/progs/lsm.c b/tools/testing/selftests/bpf/progs/lsm.c
> index 7de173daf27b..7e32fbddbfad 100644
> --- a/tools/testing/selftests/bpf/progs/lsm.c
> +++ b/tools/testing/selftests/bpf/progs/lsm.c
> @@ -84,6 +84,85 @@ char _license[] SEC("license") = "GPL";
>  int monitored_pid = 0;
>  int mprotect_count = 0;
>  int bprm_count = 0;
> +__u32 inode_identity_idmap_seen = 0;
> +__u32 inode_idmapped_mount_seen = 0;
> +
> +enum {
> +	INODE_IDMAP_CREATE	= 1U << 0,
> +	INODE_IDMAP_LINK	= 1U << 1,
> +	INODE_IDMAP_SYMLINK	= 1U << 2,
> +	INODE_IDMAP_MKDIR	= 1U << 3,
> +	INODE_IDMAP_MKNOD	= 1U << 4,
> +	INODE_IDMAP_PERMISSION	= 1U << 5,
> +};
> +
> +static __always_inline bool is_monitored_idmap(struct mnt_idmap *idmap)
> +{
> +	__u32 pid = bpf_get_current_pid_tgid() >> 32;
> +
> +	return monitored_pid == pid && idmap;
> +}
> +
> +static __always_inline bool is_identity_idmap(struct mnt_idmap *idmap)
> +{
> +	return idmap->uid_map.nr_extents == 0 &&
> +	       idmap->gid_map.nr_extents == 0;
> +}
> +
> +static __always_inline int record_inode_idmap(struct mnt_idmap *idmap,
> +					      __u32 hook, int ret)
> +{
> +	if (ret || !is_monitored_idmap(idmap))
> +		return ret;
> +	if (is_identity_idmap(idmap))
> +		inode_identity_idmap_seen |= hook;
> +	else
> +		inode_idmapped_mount_seen |= hook;
> +	return 0;
> +}
> +
> +SEC("lsm/inode_create")
> +int BPF_PROG(test_inode_create, struct mnt_idmap *idmap, struct inode *dir,
> +	     struct dentry *dentry, umode_t mode, int ret)
> +{
> +	return record_inode_idmap(idmap, INODE_IDMAP_CREATE, ret);
> +}
> +
> +SEC("lsm/inode_link")
> +int BPF_PROG(test_inode_link, struct mnt_idmap *idmap,
> +	     struct dentry *old_dentry, struct inode *dir,
> +	     struct dentry *new_dentry, int ret)
> +{
> +	return record_inode_idmap(idmap, INODE_IDMAP_LINK, ret);
> +}
> +
> +SEC("lsm/inode_symlink")
> +int BPF_PROG(test_inode_symlink, struct mnt_idmap *idmap, struct inode *dir,
> +	     struct dentry *dentry, const char *old_name, int ret)
> +{
> +	return record_inode_idmap(idmap, INODE_IDMAP_SYMLINK, ret);
> +}
> +
> +SEC("lsm/inode_mkdir")
> +int BPF_PROG(test_inode_mkdir, struct mnt_idmap *idmap, struct inode *dir,
> +	     struct dentry *dentry, umode_t mode, int ret)
> +{
> +	return record_inode_idmap(idmap, INODE_IDMAP_MKDIR, ret);
> +}
> +
> +SEC("lsm/inode_mknod")
> +int BPF_PROG(test_inode_mknod, struct mnt_idmap *idmap, struct inode *dir,
> +	     struct dentry *dentry, umode_t mode, dev_t dev, int ret)
> +{
> +	return record_inode_idmap(idmap, INODE_IDMAP_MKNOD, ret);
> +}
> +
> +SEC("lsm/inode_permission")
> +int BPF_PROG(test_inode_permission, struct mnt_idmap *idmap,
> +	     struct inode *inode, int mask, int ret)
> +{
> +	return record_inode_idmap(idmap, INODE_IDMAP_PERMISSION, ret);
> +}
>  
>  SEC("lsm/file_mprotect")
>  int BPF_PROG(test_int_hook, struct vm_area_struct *vma,
> 
> -- 
> 2.54.0
> 
> 

  parent reply	other threads:[~2026-09-11  3:25 UTC|newest]

Thread overview: 15+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-04 14:48 [PATCH v3 0/2] lsm: expose mount idmaps to inode hooks Daan De Meyer via B4 Relay
2026-09-04 14:48 ` [PATCH v3 1/2] " Daan De Meyer via B4 Relay
2026-09-08 22:05   ` Paul Moore
2026-09-08 22:27     ` Casey Schaufler
2026-09-11 17:32       ` Paul Moore
2026-09-09 11:31     ` Daan De Meyer
2026-09-11 17:48       ` Paul Moore
2026-09-04 14:48 ` [PATCH v3 2/2] selftests/bpf: verify mount idmaps reach " Daan De Meyer via B4 Relay
2026-09-08 22:22   ` Paul Moore
2026-09-08 23:20     ` Matt Bobrowski
2026-09-11  3:25   ` Matt Bobrowski [this message]
2026-09-11 18:11     ` Paul Moore
2026-09-11 18:14   ` Paul Moore
2026-09-08 22:04 ` [PATCH v3 0/2] lsm: expose mount idmaps to " Paul Moore
2026-09-09 11:28 ` 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=aqN0EGbbHk8XMqI6@lima-development \
    --to=matt@bobrowski.net \
    --cc=brauner@kernel.org \
    --cc=daan@amutable.com \
    --cc=jack@suse.cz \
    --cc=linux-fsdevel@vger.kernel.org \
    --cc=linux-security-module@vger.kernel.org \
    --cc=paul@paul-moore.com \
    --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;
as well as URLs for NNTP newsgroup(s).