mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Christian Brauner <brauner@kernel.org>
To: linux-fsdevel@vger.kernel.org, Jann Horn <jannh@google.com>
Cc: Kees Cook <kees@kernel.org>,
	Andy Lutomirski <luto@amacapital.net>,
	 Alexander Viro <viro@zeniv.linux.org.uk>,
	Jan Kara <jack@suse.cz>,
	 linux-kernel@vger.kernel.org,
	Christian Brauner <brauner@kernel.org>
Subject: [PATCH RFC v3 2/2] selftests/pidfd: add inode ownership and permission tests
Date: Mon, 23 Feb 2026 14:20:09 +0100	[thread overview]
Message-ID: <20260223-work-pidfs-inode-owner-v3-2-490855c59999@kernel.org> (raw)
In-Reply-To: <20260223-work-pidfs-inode-owner-v3-0-490855c59999@kernel.org>

Test the pidfs inode ownership reporting (via fstat) and the permission
model (via user.* xattr operations that trigger pidfs_permission()):

Ownership tests:
- owner_self: own pidfd reports caller's euid/egid
- owner_child: child pidfd reports correct ownership
- owner_child_changed_euid: ownership tracks live credential changes
- owner_exited_child: ownership persists after exit and reap
- owner_exited_child_changed_euid: exit_cred preserves changed credentials

Permission tests:
- permission_same_user: same-user xattr access succeeds (EOPNOTSUPP)
- permission_different_user_denied: cross-user access denied (EPERM)
- permission_kthread: kernel thread access always denied (EPERM)

The user.* xattr namespace is used to exercise pidfs_permission() from
userspace: xattr_permission() calls inode_permission() for user.* on
S_IFREG inodes, so fgetxattr() returns EOPNOTSUPP when permission is
granted (no handler) and EPERM when denied.

Tests requiring root skip gracefully via SKIP().

Signed-off-by: Christian Brauner <brauner@kernel.org>
---
 tools/testing/selftests/pidfd/.gitignore           |   1 +
 tools/testing/selftests/pidfd/Makefile             |   2 +-
 .../selftests/pidfd/pidfd_inode_owner_test.c       | 289 +++++++++++++++++++++
 3 files changed, 291 insertions(+), 1 deletion(-)

diff --git a/tools/testing/selftests/pidfd/.gitignore b/tools/testing/selftests/pidfd/.gitignore
index 144e7ff65d6a..1981d39fe3dc 100644
--- a/tools/testing/selftests/pidfd/.gitignore
+++ b/tools/testing/selftests/pidfd/.gitignore
@@ -12,3 +12,4 @@ pidfd_info_test
 pidfd_exec_helper
 pidfd_xattr_test
 pidfd_setattr_test
+pidfd_inode_owner_test
diff --git a/tools/testing/selftests/pidfd/Makefile b/tools/testing/selftests/pidfd/Makefile
index 764a8f9ecefa..904c9fd595c1 100644
--- a/tools/testing/selftests/pidfd/Makefile
+++ b/tools/testing/selftests/pidfd/Makefile
@@ -4,7 +4,7 @@ CFLAGS += -g $(KHDR_INCLUDES) $(TOOLS_INCLUDES) -pthread -Wall
 TEST_GEN_PROGS := pidfd_test pidfd_fdinfo_test pidfd_open_test \
 	pidfd_poll_test pidfd_wait pidfd_getfd_test pidfd_setns_test \
 	pidfd_file_handle_test pidfd_bind_mount pidfd_info_test \
-	pidfd_xattr_test pidfd_setattr_test
+	pidfd_xattr_test pidfd_setattr_test pidfd_inode_owner_test
 
 TEST_GEN_PROGS_EXTENDED := pidfd_exec_helper
 
diff --git a/tools/testing/selftests/pidfd/pidfd_inode_owner_test.c b/tools/testing/selftests/pidfd/pidfd_inode_owner_test.c
new file mode 100644
index 000000000000..58666b87638b
--- /dev/null
+++ b/tools/testing/selftests/pidfd/pidfd_inode_owner_test.c
@@ -0,0 +1,289 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <linux/types.h>
+#include <sched.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <syscall.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <sys/xattr.h>
+#include <unistd.h>
+
+#include "pidfd.h"
+#include "kselftest_harness.h"
+
+FIXTURE(pidfs_inode_owner)
+{
+	pid_t child_pid;
+	int child_pidfd;
+};
+
+FIXTURE_SETUP(pidfs_inode_owner)
+{
+	int pipe_fds[2];
+	char buf;
+
+	self->child_pid = -1;
+	self->child_pidfd = -1;
+
+	ASSERT_EQ(pipe(pipe_fds), 0);
+
+	self->child_pid = create_child(&self->child_pidfd, 0);
+	ASSERT_GE(self->child_pid, 0);
+
+	if (self->child_pid == 0) {
+		close(pipe_fds[0]);
+		write_nointr(pipe_fds[1], "c", 1);
+		close(pipe_fds[1]);
+		pause();
+		_exit(EXIT_SUCCESS);
+	}
+
+	close(pipe_fds[1]);
+	ASSERT_EQ(read_nointr(pipe_fds[0], &buf, 1), 1);
+	close(pipe_fds[0]);
+}
+
+FIXTURE_TEARDOWN(pidfs_inode_owner)
+{
+	if (self->child_pid > 0) {
+		kill(self->child_pid, SIGKILL);
+		sys_waitid(P_PID, self->child_pid, NULL, WEXITED);
+	}
+	if (self->child_pidfd >= 0)
+		close(self->child_pidfd);
+}
+
+/* Own pidfd reports correct ownership. */
+TEST_F(pidfs_inode_owner, owner_self)
+{
+	int pidfd;
+	struct stat st;
+
+	pidfd = sys_pidfd_open(getpid(), 0);
+	ASSERT_GE(pidfd, 0);
+
+	ASSERT_EQ(fstat(pidfd, &st), 0);
+	EXPECT_EQ(st.st_uid, geteuid());
+	EXPECT_EQ(st.st_gid, getegid());
+
+	close(pidfd);
+}
+
+/* Child pidfd reports correct ownership. */
+TEST_F(pidfs_inode_owner, owner_child)
+{
+	struct stat st;
+
+	ASSERT_EQ(fstat(self->child_pidfd, &st), 0);
+	EXPECT_EQ(st.st_uid, geteuid());
+	EXPECT_EQ(st.st_gid, getegid());
+}
+
+/* Ownership tracks credential changes in a live task. */
+TEST_F(pidfs_inode_owner, owner_child_changed_euid)
+{
+	pid_t pid;
+	int pidfd, pipe_fds[2];
+	struct stat st;
+	char buf;
+
+	if (getuid() != 0)
+		SKIP(return, "Test requires root");
+
+	ASSERT_EQ(pipe(pipe_fds), 0);
+
+	pid = create_child(&pidfd, 0);
+	ASSERT_GE(pid, 0);
+
+	if (pid == 0) {
+		close(pipe_fds[0]);
+		if (setresgid(65534, 65534, 65534))
+			_exit(PIDFD_ERROR);
+		if (setresuid(65534, 65534, 65534))
+			_exit(PIDFD_ERROR);
+		write_nointr(pipe_fds[1], "c", 1);
+		close(pipe_fds[1]);
+		pause();
+		_exit(EXIT_SUCCESS);
+	}
+
+	close(pipe_fds[1]);
+	ASSERT_EQ(read_nointr(pipe_fds[0], &buf, 1), 1);
+	close(pipe_fds[0]);
+
+	ASSERT_EQ(fstat(pidfd, &st), 0);
+	EXPECT_EQ(st.st_uid, (uid_t)65534);
+	EXPECT_EQ(st.st_gid, (gid_t)65534);
+
+	kill(pid, SIGKILL);
+	sys_waitid(P_PID, pid, NULL, WEXITED);
+	close(pidfd);
+}
+
+/* Ownership persists after the child exits and is reaped. */
+TEST_F(pidfs_inode_owner, owner_exited_child)
+{
+	pid_t pid;
+	int pidfd;
+	struct stat st;
+
+	pid = create_child(&pidfd, 0);
+	ASSERT_GE(pid, 0);
+
+	if (pid == 0)
+		_exit(EXIT_SUCCESS);
+
+	ASSERT_EQ(sys_waitid(P_PID, pid, NULL, WEXITED), 0);
+
+	ASSERT_EQ(fstat(pidfd, &st), 0);
+	EXPECT_EQ(st.st_uid, geteuid());
+	EXPECT_EQ(st.st_gid, getegid());
+
+	close(pidfd);
+}
+
+/* Exit credentials preserve changed credentials. */
+TEST_F(pidfs_inode_owner, owner_exited_child_changed_euid)
+{
+	pid_t pid;
+	int pidfd;
+	struct stat st;
+
+	if (getuid() != 0)
+		SKIP(return, "Test requires root");
+
+	pid = create_child(&pidfd, 0);
+	ASSERT_GE(pid, 0);
+
+	if (pid == 0) {
+		if (setresgid(65534, 65534, 65534))
+			_exit(PIDFD_ERROR);
+		if (setresuid(65534, 65534, 65534))
+			_exit(PIDFD_ERROR);
+		_exit(EXIT_SUCCESS);
+	}
+
+	ASSERT_EQ(sys_waitid(P_PID, pid, NULL, WEXITED), 0);
+
+	ASSERT_EQ(fstat(pidfd, &st), 0);
+	EXPECT_EQ(st.st_uid, (uid_t)65534);
+	EXPECT_EQ(st.st_gid, (gid_t)65534);
+
+	close(pidfd);
+}
+
+/* Same-user cross-process permission check succeeds. */
+TEST_F(pidfs_inode_owner, permission_same_user)
+{
+	pid_t pid;
+	int pidfd;
+	pid_t parent_pid = getpid();
+
+	pid = create_child(&pidfd, 0);
+	ASSERT_GE(pid, 0);
+
+	if (pid == 0) {
+		int fd;
+		char buf;
+
+		fd = sys_pidfd_open(parent_pid, 0);
+		if (fd < 0)
+			_exit(PIDFD_ERROR);
+
+		/*
+		 * user.* xattr access triggers pidfs_permission().
+		 * Same user passes may_signal_creds() and
+		 * generic_permission(), so we get EOPNOTSUPP
+		 * (no user.* xattr handler) instead of EPERM.
+		 */
+		if (fgetxattr(fd, "user.test", &buf, sizeof(buf)) < 0 &&
+		    errno == EOPNOTSUPP) {
+			close(fd);
+			_exit(PIDFD_PASS);
+		}
+
+		close(fd);
+		_exit(PIDFD_FAIL);
+	}
+
+	ASSERT_EQ(wait_for_pid(pid), PIDFD_PASS);
+	close(pidfd);
+}
+
+/* Cross-user access is denied without signal permission. */
+TEST_F(pidfs_inode_owner, permission_different_user_denied)
+{
+	pid_t pid;
+	int pidfd;
+
+	if (getuid() != 0)
+		SKIP(return, "Test requires root");
+
+	pid = create_child(&pidfd, 0);
+	ASSERT_GE(pid, 0);
+
+	if (pid == 0) {
+		int fd;
+		char buf;
+
+		/* Drop to uid/gid 65534 and lose all capabilities. */
+		if (setresgid(65534, 65534, 65534))
+			_exit(PIDFD_ERROR);
+		if (setresuid(65534, 65534, 65534))
+			_exit(PIDFD_ERROR);
+
+		/* Open pidfd for init (uid 0). */
+		fd = sys_pidfd_open(1, 0);
+		if (fd < 0)
+			_exit(PIDFD_ERROR);
+
+		/*
+		 * uid 65534 cannot signal uid 0 (no CAP_KILL),
+		 * so pidfs_permission() denies access.
+		 */
+		if (fgetxattr(fd, "user.test", &buf, sizeof(buf)) < 0 &&
+		    errno == EPERM) {
+			close(fd);
+			_exit(PIDFD_PASS);
+		}
+
+		close(fd);
+		_exit(PIDFD_FAIL);
+	}
+
+	ASSERT_EQ(wait_for_pid(pid), PIDFD_PASS);
+	close(pidfd);
+}
+
+/* Kernel thread access is always denied. */
+TEST_F(pidfs_inode_owner, permission_kthread)
+{
+	int pidfd;
+	struct stat st;
+	char buf;
+
+	/* pid 2 is kthreadd. */
+	pidfd = sys_pidfd_open(2, 0);
+	ASSERT_GE(pidfd, 0);
+
+	/* pidfs_permission() returns EPERM for kernel threads. */
+	ASSERT_LT(fgetxattr(pidfd, "user.test", &buf, sizeof(buf)), 0);
+	EXPECT_EQ(errno, EPERM);
+
+	/* fstat bypasses permission and reports root ownership. */
+	ASSERT_EQ(fstat(pidfd, &st), 0);
+	EXPECT_EQ(st.st_uid, (uid_t)0);
+	EXPECT_EQ(st.st_gid, (gid_t)0);
+
+	close(pidfd);
+}
+
+TEST_HARNESS_MAIN

-- 
2.47.3


      parent reply	other threads:[~2026-02-23 13:20 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-02-23 13:20 [PATCH RFC v3 0/2] pidfs: make the effective {g,u}id the owner of the inode Christian Brauner
2026-02-23 13:20 ` [PATCH RFC v3 1/2] pidfs: add inode ownership and permission checks Christian Brauner
2026-02-24 21:22   ` Jann Horn
2026-02-25 15:32     ` Christian Brauner
2026-02-23 13:20 ` Christian Brauner [this message]

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=20260223-work-pidfs-inode-owner-v3-2-490855c59999@kernel.org \
    --to=brauner@kernel.org \
    --cc=jack@suse.cz \
    --cc=jannh@google.com \
    --cc=kees@kernel.org \
    --cc=linux-fsdevel@vger.kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=luto@amacapital.net \
    --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

all inboxes | Powered by JetHome®