mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Cong Wang <xiyou.wangcong@gmail.com>
To: Richard Weinberger <richard@nod.at>,
	Anton Ivanov <anton.ivanov@cambridgegreys.com>,
	Johannes Berg <johannes@sipsolutions.net>
Cc: Benjamin Berg <benjamin@sipsolutions.net>,
	linux-um@lists.infradead.org, linux-kernel@vger.kernel.org,
	Cong Wang <cwang@multikernel.io>
Subject: [RFC PATCH 6/6] selftests/pidfd: add pidfd_mmap()/pidfd_munmap() tests
Date: Fri, 10 Jul 2026 13:53:24 -0700	[thread overview]
Message-ID: <20260710205324.1343217-7-xiyou.wangcong@gmail.com> (raw)
In-Reply-To: <20260710205324.1343217-1-xiyou.wangcong@gmail.com>

From: Cong Wang <cwang@multikernel.io>

Exercise the new syscalls against a forked target reached by pidfd:

 - install_and_read: map a parent-owned memfd into the target, verify the
   target's new mapping shows the memfd contents (read via
   process_vm_readv), that MAP_SHARED writes propagate, and that
   pidfd_munmap() removes it. This also covers the key semantic that the
   backing fd is resolved in the *caller's* fd table (the target never
   sees the memfd).
 - anonymous: MAP_ANONYMOUS install + write-back round-trip.
 - bad_args: __spare != 0, undersized @size, non-zero reserved flags, and
   a bad backing fd are all rejected.
 - no_ptrace_access: a non-dumpable target (no ptrace access) yields
   -EPERM, matching the ptrace_may_access() gate.

The suite skips cleanly (TAP "SKIP") when the kernel lacks the syscall.
The pidfd_mmap ABI definitions are kept in pidfd.h alongside the existing
local uapi copies (struct pidfd_info, etc.).

Assisted-by: Claude:claude-opus-4.8
Signed-off-by: Cong Wang <cwang@multikernel.io>
---
 tools/testing/selftests/pidfd/Makefile        |   3 +-
 tools/testing/selftests/pidfd/pidfd.h         |  34 +++
 .../testing/selftests/pidfd/pidfd_mmap_test.c | 234 ++++++++++++++++++
 3 files changed, 270 insertions(+), 1 deletion(-)
 create mode 100644 tools/testing/selftests/pidfd/pidfd_mmap_test.c

diff --git a/tools/testing/selftests/pidfd/Makefile b/tools/testing/selftests/pidfd/Makefile
index 4211f91e9af8..f1c421bf725a 100644
--- a/tools/testing/selftests/pidfd/Makefile
+++ b/tools/testing/selftests/pidfd/Makefile
@@ -4,7 +4,8 @@ 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_autoreap_test
+	pidfd_xattr_test pidfd_setattr_test pidfd_autoreap_test \
+	pidfd_mmap_test
 
 TEST_GEN_PROGS_EXTENDED := pidfd_exec_helper
 
diff --git a/tools/testing/selftests/pidfd/pidfd.h b/tools/testing/selftests/pidfd/pidfd.h
index 5a4e78c10f43..91e65283d5fe 100644
--- a/tools/testing/selftests/pidfd/pidfd.h
+++ b/tools/testing/selftests/pidfd/pidfd.h
@@ -203,6 +203,28 @@ struct pidfd_info {
 	__u64 supported_mask;
 };
 
+#ifndef __NR_pidfd_mmap
+#define __NR_pidfd_mmap 472
+#endif
+
+#ifndef __NR_pidfd_munmap
+#define __NR_pidfd_munmap 473
+#endif
+
+#ifndef PIDFD_MMAP_ARGS_SIZE_VER0
+struct pidfd_mmap_args {
+	__u64 size;
+	__u64 addr;
+	__u64 len;
+	__u64 prot;
+	__u64 flags;
+	__u64 pgoff;
+	__s32 fd;
+	__u32 __spare;
+};
+#define PIDFD_MMAP_ARGS_SIZE_VER0	56
+#endif
+
 /*
  * The kernel reserves 300 pids via RESERVED_PIDS in kernel/pid.c
  * That means, when it wraps around any pid < 300 will be skipped.
@@ -267,6 +289,18 @@ static inline int sys_pidfd_getfd(int pidfd, int fd, int flags)
 	return syscall(__NR_pidfd_getfd, pidfd, fd, flags);
 }
 
+static inline long sys_pidfd_mmap(int pidfd, struct pidfd_mmap_args *args,
+				  unsigned int flags)
+{
+	return syscall(__NR_pidfd_mmap, pidfd, args, flags);
+}
+
+static inline long sys_pidfd_munmap(int pidfd, unsigned long addr,
+				    unsigned long len)
+{
+	return syscall(__NR_pidfd_munmap, pidfd, addr, len);
+}
+
 static inline int sys_memfd_create(const char *name, unsigned int flags)
 {
 	return syscall(__NR_memfd_create, name, flags);
diff --git a/tools/testing/selftests/pidfd/pidfd_mmap_test.c b/tools/testing/selftests/pidfd/pidfd_mmap_test.c
new file mode 100644
index 000000000000..39d623ce7b9d
--- /dev/null
+++ b/tools/testing/selftests/pidfd/pidfd_mmap_test.c
@@ -0,0 +1,234 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <sched.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <syscall.h>
+#include <unistd.h>
+#include <sys/mman.h>
+#include <sys/prctl.h>
+#include <sys/socket.h>
+#include <sys/uio.h>
+#include <sys/wait.h>
+
+#include "pidfd.h"
+#include "kselftest_harness.h"
+
+#define UID_NOBODY 65535
+#define PATTERN 0xab
+#define MAP_LEN (4 * 1024)
+
+static struct pidfd_mmap_args mmap_args(int fd)
+{
+	struct pidfd_mmap_args args = {
+		.size	= sizeof(args),
+		.addr	= 0,		/* let the kernel choose */
+		.len	= MAP_LEN,
+		.prot	= PROT_READ | PROT_WRITE,
+		.flags	= MAP_SHARED,
+		.pgoff	= 0,
+		.fd	= fd,
+	};
+
+	return args;
+}
+
+static int read_remote(pid_t pid, unsigned long addr, void *buf, size_t len)
+{
+	struct iovec local = { .iov_base = buf, .iov_len = len };
+	struct iovec remote = { .iov_base = (void *)addr, .iov_len = len };
+
+	return process_vm_readv(pid, &local, 1, &remote, 1, 0) == (ssize_t)len ?
+	       0 : -1;
+}
+
+static int target(int sk)
+{
+	char buf;
+	int ret;
+
+	prctl(PR_SET_PDEATHSIG, SIGKILL);
+
+	if (send(sk, "R", 1, 0) != 1)		/* ready */
+		return -1;
+
+	while ((ret = recv(sk, &buf, sizeof(buf), 0)) > 0) {
+		if (buf == 'P' && prctl(PR_SET_DUMPABLE, 0) < 0)
+			return -1;
+		if (send(sk, &buf, 1, 0) != 1)
+			return -1;
+	}
+
+	return ret < 0 ? -1 : 0;
+}
+
+FIXTURE(pidfd_mmap)
+{
+	pid_t pid;
+	int pidfd;
+	int sk;		/* parent side of the socketpair */
+	int memfd;	/* lives in the PARENT fd table */
+	void *local;	/* the parent's own mapping of memfd */
+};
+
+FIXTURE_SETUP(pidfd_mmap)
+{
+	struct pidfd_mmap_args probe;
+	int sk_pair[2];
+	char c;
+
+	/*
+	 * The backing fd is resolved in the *caller's* (parent's) fd table,
+	 * so the target never needs to see it.
+	 */
+	self->memfd = sys_memfd_create("pidfd_mmap", 0);
+	ASSERT_GE(self->memfd, 0);
+	ASSERT_EQ(0, ftruncate(self->memfd, MAP_LEN));
+
+	self->local = mmap(NULL, MAP_LEN, PROT_READ | PROT_WRITE, MAP_SHARED,
+			   self->memfd, 0);
+	ASSERT_NE(MAP_FAILED, self->local);
+	memset(self->local, PATTERN, MAP_LEN);
+
+	ASSERT_EQ(0, socketpair(PF_LOCAL, SOCK_SEQPACKET, 0, sk_pair));
+	self->sk = sk_pair[0];
+
+	self->pid = fork();
+	ASSERT_GE(self->pid, 0);
+	if (self->pid == 0) {
+		close(sk_pair[0]);
+		_exit(target(sk_pair[1]) ? EXIT_FAILURE : EXIT_SUCCESS);
+	}
+	close(sk_pair[1]);
+
+	self->pidfd = sys_pidfd_open(self->pid, 0);
+	ASSERT_GE(self->pidfd, 0);
+
+	ASSERT_EQ(1, recv(self->sk, &c, 1, 0));		/* wait for "R" */
+
+	/* Skip the whole suite if the kernel does not have pidfd_mmap(). */
+	probe = mmap_args(self->memfd);
+	sys_pidfd_mmap(self->pidfd, &probe, 1);		/* reserved flags -> EINVAL */
+	if (errno == ENOSYS)
+		SKIP(return, "pidfd_mmap() is not supported");
+}
+
+FIXTURE_TEARDOWN(pidfd_mmap)
+{
+	EXPECT_EQ(0, close(self->pidfd));
+	EXPECT_EQ(0, close(self->sk));		/* tells the target to exit */
+	munmap(self->local, MAP_LEN);
+	close(self->memfd);
+	EXPECT_EQ(0, wait_for_pid(self->pid));
+}
+
+TEST_F(pidfd_mmap, install_and_read)
+{
+	struct pidfd_mmap_args args = mmap_args(self->memfd);
+	char buf[MAP_LEN];
+	long addr;
+	int i;
+
+	addr = sys_pidfd_mmap(self->pidfd, &args, 0);
+	ASSERT_GE(addr, 0);
+	ASSERT_EQ(0, addr & (sysconf(_SC_PAGESIZE) - 1));	/* page aligned */
+
+	/* The target's new mapping must show the memfd contents. */
+	ASSERT_EQ(0, read_remote(self->pid, addr, buf, MAP_LEN));
+	for (i = 0; i < MAP_LEN; i++)
+		ASSERT_EQ((unsigned char)PATTERN, (unsigned char)buf[i]);
+
+	/* MAP_SHARED: a write the parent makes to memfd is visible remotely. */
+	memset(self->local, 0xcd, MAP_LEN);
+	ASSERT_EQ(0, read_remote(self->pid, addr, buf, sizeof(buf)));
+	ASSERT_EQ((unsigned char)0xcd, (unsigned char)buf[0]);
+
+	/* pidfd_munmap() removes it. */
+	ASSERT_EQ(0, sys_pidfd_munmap(self->pidfd, addr, MAP_LEN));
+	EXPECT_EQ(-1, read_remote(self->pid, addr, buf, MAP_LEN));
+}
+
+TEST_F(pidfd_mmap, anonymous)
+{
+	struct pidfd_mmap_args args = mmap_args(-1);
+	struct iovec local, remote;
+	char in[64], out[64];
+	long addr;
+
+	args.flags = MAP_PRIVATE | MAP_ANONYMOUS;
+
+	addr = sys_pidfd_mmap(self->pidfd, &args, 0);
+	ASSERT_GE(addr, 0);
+
+	memset(out, 0x5a, sizeof(out));
+	local.iov_base = out;
+	local.iov_len = sizeof(out);
+	remote.iov_base = (void *)addr;
+	remote.iov_len = sizeof(out);
+	ASSERT_EQ((ssize_t)sizeof(out),
+		  process_vm_writev(self->pid, &local, 1, &remote, 1, 0));
+
+	ASSERT_EQ(0, read_remote(self->pid, addr, in, sizeof(in)));
+	ASSERT_EQ((unsigned char)0x5a, (unsigned char)in[0]);
+
+	ASSERT_EQ(0, sys_pidfd_munmap(self->pidfd, addr, MAP_LEN));
+}
+
+TEST_F(pidfd_mmap, bad_args)
+{
+	struct pidfd_mmap_args args;
+
+	/* The reserved padding must be zero. */
+	args = mmap_args(self->memfd);
+	args.__spare = 1;
+	EXPECT_EQ(-1, sys_pidfd_mmap(self->pidfd, &args, 0));
+	EXPECT_EQ(EINVAL, errno);
+
+	/* @size below the first published version is rejected. */
+	args = mmap_args(self->memfd);
+	args.size = 8;
+	EXPECT_EQ(-1, sys_pidfd_mmap(self->pidfd, &args, 0));
+	EXPECT_EQ(EINVAL, errno);
+
+	/* The reserved flags argument must be zero. */
+	args = mmap_args(self->memfd);
+	EXPECT_EQ(-1, sys_pidfd_mmap(self->pidfd, &args, 1));
+	EXPECT_EQ(EINVAL, errno);
+
+	/* A bad backing fd (resolved in the caller's table) is -EBADF. */
+	args = mmap_args(-1);
+	EXPECT_EQ(-1, sys_pidfd_mmap(self->pidfd, &args, 0));
+	EXPECT_EQ(EBADF, errno);
+
+	/* A bad pidfd fails. */
+	args = mmap_args(self->memfd);
+	EXPECT_EQ(-1, sys_pidfd_mmap(-1, &args, 0));
+}
+
+TEST_F(pidfd_mmap, no_ptrace_access)
+{
+	struct pidfd_mmap_args args = mmap_args(self->memfd);
+	int uid = getuid();
+	char c;
+
+	/* Drop privilege so CAP_SYS_PTRACE can't bypass the dumpable check. */
+	if (uid == 0)
+		ASSERT_EQ(0, seteuid(UID_NOBODY));
+
+	ASSERT_EQ(1, send(self->sk, "P", 1, 0));	/* target: PR_SET_DUMPABLE 0 */
+	ASSERT_EQ(1, recv(self->sk, &c, 1, 0));
+
+	EXPECT_EQ(-1, sys_pidfd_mmap(self->pidfd, &args, 0));
+	EXPECT_EQ(EPERM, errno);
+
+	if (uid == 0)
+		ASSERT_EQ(0, seteuid(0));
+}
+
+TEST_HARNESS_MAIN
-- 
2.43.0


      parent reply	other threads:[~2026-07-10 20:54 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-07-10 20:53 [RFC PATCH 0/6] um: introduce pidfd_mmap()/pidfd_munmap() syscalls Cong Wang
2026-07-10 20:53 ` [RFC PATCH 1/6] pidfd: add " Cong Wang
2026-07-10 20:53 ` [RFC PATCH 2/6] um: acquire a stub pidfd via CLONE_PIDFD in seccomp mode Cong Wang
2026-07-10 20:53 ` [RFC PATCH 3/6] um: install guest mappings via pidfd_mmap() " Cong Wang
2026-07-10 20:53 ` [RFC PATCH 4/6] um: forbid mmap/munmap in the stub seccomp filter Cong Wang
2026-07-10 20:53 ` [RFC PATCH 5/6] um: install guest mappings via pidfd_mmap() in both modes Cong Wang
2026-07-10 20:53 ` Cong Wang [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=20260710205324.1343217-7-xiyou.wangcong@gmail.com \
    --to=xiyou.wangcong@gmail.com \
    --cc=anton.ivanov@cambridgegreys.com \
    --cc=benjamin@sipsolutions.net \
    --cc=cwang@multikernel.io \
    --cc=johannes@sipsolutions.net \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-um@lists.infradead.org \
    --cc=richard@nod.at \
    /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