[RFC PATCH 6/6] selftests/pidfd: add pidfd_mmap()/pidfd_munmap() tests
Cong Wang <[email protected]> Fri, 10 Jul 2026 13:53:24 -0700
| Newsgroups | org.infradead.lists.linux-um,org.kernel.vger.linux-kernel |
|---|---|
| Message-ID | <[email protected]> |
From: Cong Wang <[email protected]> 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 <[email protected]> --- 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