mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH v5 0/2] mm/process_vm_access: pidfd and nowait support for process_vm_readv/writev
@ 2026-06-02 10:09 Alban Crequy
  2026-06-02 10:09 ` [PATCH v5 1/2] " Alban Crequy
  2026-06-02 10:09 ` [PATCH v5 2/2] selftests/mm: add tests for process_vm_readv flags Alban Crequy
  0 siblings, 2 replies; 9+ messages in thread
From: Alban Crequy @ 2026-06-02 10:09 UTC (permalink / raw)
  To: Andrew Morton, David Hildenbrand, Christian Brauner
  Cc: Lorenzo Stoakes, Liam R . Howlett, Vlastimil Babka,
	Mike Rapoport, Suren Baghdasaryan, Michal Hocko, linux-kernel,
	linux-mm, Alban Crequy, Alban Crequy, Peter Xu, Willy Tarreau,
	linux-kselftest, shuah, Usama Arif, David Laight

This adds two flags to process_vm_readv/writev:

- PROCESS_VM_PIDFD: refer to the remote process via PID file descriptor
  instead of PID.
- PROCESS_VM_NOWAIT: do not block on IO if the memory access causes a
  page fault.

v4: https://lore.kernel.org/lkml/20260515143335.739511-1-alban.crequy@gmail.com/
v3: https://lore.kernel.org/lkml/20260428122826.339550-1-alban.crequy@gmail.com/
v2: https://lore.kernel.org/lkml/20260408145436.843538-1-alban.crequy@gmail.com/
v1: https://lore.kernel.org/lkml/20251118132348.2415603-1-alban.crequy@gmail.com/
Sashiko review of v4: https://sashiko.dev/#/patchset/20260515143335.739511-1-alban.crequy@gmail.com
Sashiko review of v3: https://sashiko.dev/#/patchset/20260428122826.339550-1-alban.crequy@gmail.com
Sashiko review of v2: https://sashiko.dev/#/patchset/20260408145436.843538-1-alban.crequy@gmail.com

Related man-pages fix (corrects pre-existing partial transfer documentation,
not the new flags proposed here):
https://git.kernel.org/pub/scm/docs/man-pages/man-pages.git/commit/?id=a0b1df86b0c951beb896d468f200be7fd44631e3

Changes since v4:
- Add process_vm_readv to .gitignore (Sashiko)
- Use ~0UL instead of 255 for invalid flags tests (Sashiko)
- Use volatile uint8_t instead of volatile uint64_t for page fault-in
  to avoid alignment issues on strict-alignment architectures (Sashiko)
- Use UFFDIO_UNREGISTER in uffd handler error path to safely unblock
  the main thread without double-close (Sashiko)
- Rebase onto v7.1-rc6

Changes since v3:
- Rename include/uapi/linux/process_vm.h to process_vm_access.h to
  match mm/process_vm_access.c (David Hildenbrand)
- Fix MAINTAINERS alphabetical sort order (David Hildenbrand)
- Document NOWAIT return value in commit message (David Hildenbrand)
- Document PIDFD EBADF error in commit message (David Hildenbrand)
- Add suggested man page update text in commit message
  (David Hildenbrand, Christian Brauner, Mike Rapoport)
- Keep (1UL << N) in UAPI header: BIT() is defined in vdso/bits.h
  which is not exported to userspace (David Hildenbrand)
- Add selftests for NOWAIT partial reads across resident and
  non-resident pages (single iovec and two iovecs)
- SKIP tests gracefully on kernels without flag support (Sashiko)
- Verify content of partial reads in selftests
- Rebase onto v7.1-rc3

Changes since v2:
- Fix ERR_PTR handling for pidfd_get_task(): use IS_ERR()/PTR_ERR()
  for the pidfd path, matching process_madvise() (Usama Arif, Sashiko)
- Add selftest for invalid pidfd (David Hildenbrand)
- Add selftest for invalid pid
- Remove hardcoded __NR_pidfd_open fallback, use <sys/syscall.h> (Sashiko)
- SKIP pidfd tests on kernels without pidfd_open (ENOSYS) (Sashiko)
- SKIP userfaultfd tests when unprivileged userfaultfd is disabled (EPERM) (Sashiko)
- Fault in test_data before NOWAIT tests to ensure page is resident (Sashiko)
- Add ksft_process_vm_readv.sh wrapper and run_vmtests.sh entry
  so the test runs in CI
- Rebase onto v7.1-rc1

Not addressed:
- 64-bit process reading 32-bit process high addresses: pre-existing
  concern in the existing process_vm_readv code, not introduced by this
  patch (David Laight)
- EAGAIN instead of short read/EFAULT for non-resident pages: short-read
  semantics are more informative and consistent with O_NONBLOCK
  conventions; the profiler use case does not intend to retry (Sashiko)
- mmap_read_trylock with NOWAIT: would expand scope significantly and is
  not how other FOLL_NOWAIT users in the kernel work (Sashiko)
- GFP_NOWAIT for page array allocation: the allocation is small and
  rarely blocks; not on the data path (Sashiko)

Alban Crequy (2):
  mm/process_vm_access: pidfd and nowait support for
    process_vm_readv/writev
  selftests/mm: add tests for process_vm_readv flags

 MAINTAINERS                                   |   1 +
 include/uapi/linux/process_vm_access.h        |   9 +
 mm/process_vm_access.c                        |  34 +-
 tools/testing/selftests/mm/.gitignore         |   1 +
 tools/testing/selftests/mm/Makefile           |   2 +
 .../selftests/mm/ksft_process_vm_readv.sh     |   4 +
 tools/testing/selftests/mm/process_vm_readv.c | 591 ++++++++++++++++++
 tools/testing/selftests/mm/run_vmtests.sh     |   4 +
 8 files changed, 637 insertions(+), 9 deletions(-)
 create mode 100644 include/uapi/linux/process_vm_access.h
 create mode 100755 tools/testing/selftests/mm/ksft_process_vm_readv.sh
 create mode 100644 tools/testing/selftests/mm/process_vm_readv.c

-- 
2.45.0


^ permalink raw reply	[flat|nested] 9+ messages in thread

* [PATCH v5 1/2] mm/process_vm_access: pidfd and nowait support for process_vm_readv/writev
  2026-06-02 10:09 [PATCH v5 0/2] mm/process_vm_access: pidfd and nowait support for process_vm_readv/writev Alban Crequy
@ 2026-06-02 10:09 ` Alban Crequy
  2026-06-02 12:16   ` David Hildenbrand (Arm)
  2026-06-03  8:27   ` Alban Crequy
  2026-06-02 10:09 ` [PATCH v5 2/2] selftests/mm: add tests for process_vm_readv flags Alban Crequy
  1 sibling, 2 replies; 9+ messages in thread
From: Alban Crequy @ 2026-06-02 10:09 UTC (permalink / raw)
  To: Andrew Morton, David Hildenbrand, Christian Brauner
  Cc: Lorenzo Stoakes, Liam R . Howlett, Vlastimil Babka,
	Mike Rapoport, Suren Baghdasaryan, Michal Hocko, linux-kernel,
	linux-mm, Alban Crequy, Alban Crequy, Peter Xu, Willy Tarreau,
	linux-kselftest, shuah, Usama Arif, David Laight

From: Alban Crequy <albancrequy@microsoft.com>

There are two categories of users for process_vm_readv:

1. Debuggers like GDB or strace.

   When a debugger attempts to read the target memory and triggers a
   page fault, the page fault needs to be resolved so that the debugger
   can accurately interpret the memory. A debugger is typically attached
   to a single process.

2. Profilers like OpenTelemetry eBPF Profiler.

   The profiler uses a perf event to get stack traces from all
   processes at 20Hz (20 stack traces to resolve per second). For
   interpreted languages (Ruby, Python, etc.), the profiler uses
   process_vm_readv to get the correct symbols. In this case,
   performance is the most important. It is fine if some stack traces
   cannot be resolved as long as it is not statistically significant.

The current behaviour of process_vm_readv is to resolve page faults in
the target VM. This is as desired for debuggers, but unwelcome for
profilers because the page fault resolution could take a lot of time
depending on the backing filesystem. Additionally, since profilers
monitor all processes, we don't want a slow page fault resolution for
one target process slowing down the monitoring for all other target
processes.

This patch adds the flag PROCESS_VM_NOWAIT, so the caller can choose to
not block on IO if the memory access causes a page fault. When a page
is not resident and would require IO to fault in, the syscall returns
a short read (the number of bytes successfully read before the fault)
or -1 with errno set to EFAULT if no bytes were read.

Additionally, this patch adds the flag PROCESS_VM_PIDFD to refer to the
remote process via PID file descriptor instead of PID. Such a file
descriptor can be obtained with pidfd_open(2). This is useful to avoid
the pid number being reused. It is unlikely to happen for debuggers
because they can monitor the target process termination in other ways
(ptrace), but can be helpful in some profiling scenarios. When using
PROCESS_VM_PIDFD, the first argument is a pidfd instead of a pid. If
the pidfd is invalid, the syscall returns -1 with errno set to EBADF.

If a given flag is unsupported, the syscall returns the error EINVAL
without checking the buffers. This gives a way to userspace to detect
whether the current kernel supports a specific flag:

  process_vm_readv(pid, NULL, 1, NULL, 1, PROCESS_VM_PIDFD)
  -> EINVAL if the kernel does not support the flag PROCESS_VM_PIDFD
     (before this patch)
  -> EFAULT if the kernel supports the flag (after this patch)

Suggested man page update for process_vm_readv(2):

  The flags argument is the bitwise OR of zero or more of these flags:

  PROCESS_VM_PIDFD (since Linux 7.x)
      The pid argument is a PID file descriptor (see pidfd_open(2))
      instead of a PID number. When using this flag, the existing
      ESRCH error applies if the process referred to by the pidfd
      has exited.

  PROCESS_VM_NOWAIT (since Linux 7.x)
      Do not block on IO. If a page in the remote address space is not
      resident and would require disk IO to fault in, the system call
      returns a short read or fails with EFAULT if no bytes were read.

  Additional error:

  EBADF  pid is not a valid file descriptor (PROCESS_VM_PIDFD only).

Signed-off-by: Alban Crequy <albancrequy@microsoft.com>
---
v5:
- No changes in this patch.

v4:
- Rename process_vm.h to process_vm_access.h (David Hildenbrand)
- Fix MAINTAINERS alphabetical sort order (David Hildenbrand)
- Document NOWAIT return value and PIDFD EBADF error (David Hildenbrand)
- Add suggested man page update text
  (David Hildenbrand, Christian Brauner, Mike Rapoport)
- Keep (1UL << N) in UAPI header (David Hildenbrand)

v3:
- Fix ERR_PTR handling for pidfd_get_task(): use IS_ERR()/PTR_ERR()
  for the pidfd path, matching process_madvise() (Usama Arif, Sashiko)

v2:
- Expand commit message with use-case motivation (David Hildenbrand)
- Use unsigned long consistently for pvm_flags parameter (David Hildenbrand)
- Add PROCESS_VM_SUPPORTED_FLAGS kernel-internal define (David Hildenbrand)
- Keep (1UL << N) in UAPI header: BIT() is defined in vdso/bits.h
  which is not exported to userspace, so UAPI headers using BIT() would
  break when included from userspace programs (David Hildenbrand)

 MAINTAINERS                            |  1 +
 include/uapi/linux/process_vm_access.h |  9 +++++++
 mm/process_vm_access.c                 | 34 +++++++++++++++++++-------
 3 files changed, 35 insertions(+), 9 deletions(-)
 create mode 100644 include/uapi/linux/process_vm_access.h

diff --git a/MAINTAINERS b/MAINTAINERS
index 9ec290e38b44..1bfce49dd0bb 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -16800,6 +16800,7 @@ F:	include/linux/pgtable.h
 F:	include/linux/ptdump.h
 F:	include/linux/vmpressure.h
 F:	include/linux/vmstat.h
+F:	include/uapi/linux/process_vm_access.h
 F:	fs/proc/meminfo.c
 F:	kernel/fork.c
 F:	mm/Kconfig
diff --git a/include/uapi/linux/process_vm_access.h b/include/uapi/linux/process_vm_access.h
new file mode 100644
index 000000000000..2196c9c46351
--- /dev/null
+++ b/include/uapi/linux/process_vm_access.h
@@ -0,0 +1,9 @@
+/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
+#ifndef _UAPI_LINUX_PROCESS_VM_ACCESS_H
+#define _UAPI_LINUX_PROCESS_VM_ACCESS_H
+
+/* Flags for process_vm_readv/process_vm_writev */
+#define PROCESS_VM_PIDFD        (1UL << 0)
+#define PROCESS_VM_NOWAIT       (1UL << 1)
+
+#endif /* _UAPI_LINUX_PROCESS_VM_ACCESS_H */
diff --git a/mm/process_vm_access.c b/mm/process_vm_access.c
index 656d3e88755b..31004dd3c9e3 100644
--- a/mm/process_vm_access.c
+++ b/mm/process_vm_access.c
@@ -14,6 +14,9 @@
 #include <linux/ptrace.h>
 #include <linux/slab.h>
 #include <linux/syscalls.h>
+#include <linux/process_vm_access.h>
+
+#define PROCESS_VM_SUPPORTED_FLAGS (PROCESS_VM_PIDFD | PROCESS_VM_NOWAIT)
 
 /**
  * process_vm_rw_pages - read/write pages from task specified
@@ -68,6 +71,7 @@ static int process_vm_rw_pages(struct page **pages,
  * @mm: mm for task
  * @task: task to read/write from
  * @vm_write: 0 means copy from, 1 means copy to
+ * @pvm_flags: PROCESS_VM_* flags
  * Returns 0 on success or on failure error code
  */
 static int process_vm_rw_single_vec(unsigned long addr,
@@ -76,7 +80,8 @@ static int process_vm_rw_single_vec(unsigned long addr,
 				    struct page **process_pages,
 				    struct mm_struct *mm,
 				    struct task_struct *task,
-				    int vm_write)
+				    int vm_write,
+				    unsigned long pvm_flags)
 {
 	unsigned long pa = addr & PAGE_MASK;
 	unsigned long start_offset = addr - pa;
@@ -91,6 +96,8 @@ static int process_vm_rw_single_vec(unsigned long addr,
 
 	if (vm_write)
 		flags |= FOLL_WRITE;
+	if (pvm_flags & PROCESS_VM_NOWAIT)
+		flags |= FOLL_NOWAIT;
 
 	while (!rc && nr_pages && iov_iter_count(iter)) {
 		int pinned_pages = min_t(unsigned long, nr_pages, PVM_MAX_USER_PAGES);
@@ -141,7 +148,7 @@ static int process_vm_rw_single_vec(unsigned long addr,
  * @iter: where to copy to/from locally
  * @rvec: iovec array specifying where to copy to/from in the other process
  * @riovcnt: size of rvec array
- * @flags: currently unused
+ * @flags: process_vm_readv/writev flags
  * @vm_write: 0 if reading from other process, 1 if writing to other process
  *
  * Returns the number of bytes read/written or error code. May
@@ -163,6 +170,7 @@ static ssize_t process_vm_rw_core(pid_t pid, struct iov_iter *iter,
 	unsigned long nr_pages_iov;
 	ssize_t iov_len;
 	size_t total_len = iov_iter_count(iter);
+	unsigned int f_flags;
 
 	/*
 	 * Work out how many pages of struct pages we're going to need
@@ -194,10 +202,18 @@ static ssize_t process_vm_rw_core(pid_t pid, struct iov_iter *iter,
 	}
 
 	/* Get process information */
-	task = find_get_task_by_vpid(pid);
-	if (!task) {
-		rc = -ESRCH;
-		goto free_proc_pages;
+	if (flags & PROCESS_VM_PIDFD) {
+		task = pidfd_get_task(pid, &f_flags);
+		if (IS_ERR(task)) {
+			rc = PTR_ERR(task);
+			goto free_proc_pages;
+		}
+	} else {
+		task = find_get_task_by_vpid(pid);
+		if (!task) {
+			rc = -ESRCH;
+			goto free_proc_pages;
+		}
 	}
 
 	mm = mm_access(task, PTRACE_MODE_ATTACH_REALCREDS);
@@ -215,7 +231,7 @@ static ssize_t process_vm_rw_core(pid_t pid, struct iov_iter *iter,
 	for (i = 0; i < riovcnt && iov_iter_count(iter) && !rc; i++)
 		rc = process_vm_rw_single_vec(
 			(unsigned long)rvec[i].iov_base, rvec[i].iov_len,
-			iter, process_pages, mm, task, vm_write);
+			iter, process_pages, mm, task, vm_write, flags);
 
 	/* copied = space before - space after */
 	total_len -= iov_iter_count(iter);
@@ -244,7 +260,7 @@ static ssize_t process_vm_rw_core(pid_t pid, struct iov_iter *iter,
  * @liovcnt: size of lvec array
  * @rvec: iovec array specifying where to copy to/from in the other process
  * @riovcnt: size of rvec array
- * @flags: currently unused
+ * @flags: process_vm_readv/writev flags
  * @vm_write: 0 if reading from other process, 1 if writing to other process
  *
  * Returns the number of bytes read/written or error code. May
@@ -266,7 +282,7 @@ static ssize_t process_vm_rw(pid_t pid,
 	ssize_t rc;
 	int dir = vm_write ? ITER_SOURCE : ITER_DEST;
 
-	if (flags != 0)
+	if (flags & ~PROCESS_VM_SUPPORTED_FLAGS)
 		return -EINVAL;
 
 	/* Check iovecs */
-- 
2.45.0


^ permalink raw reply	[flat|nested] 9+ messages in thread

* [PATCH v5 2/2] selftests/mm: add tests for process_vm_readv flags
  2026-06-02 10:09 [PATCH v5 0/2] mm/process_vm_access: pidfd and nowait support for process_vm_readv/writev Alban Crequy
  2026-06-02 10:09 ` [PATCH v5 1/2] " Alban Crequy
@ 2026-06-02 10:09 ` Alban Crequy
  1 sibling, 0 replies; 9+ messages in thread
From: Alban Crequy @ 2026-06-02 10:09 UTC (permalink / raw)
  To: Andrew Morton, David Hildenbrand, Christian Brauner
  Cc: Lorenzo Stoakes, Liam R . Howlett, Vlastimil Babka,
	Mike Rapoport, Suren Baghdasaryan, Michal Hocko, linux-kernel,
	linux-mm, Alban Crequy, Alban Crequy, Peter Xu, Willy Tarreau,
	linux-kselftest, shuah, Usama Arif, David Laight

From: Alban Crequy <albancrequy@microsoft.com>

Add selftests for the PROCESS_VM_PIDFD and PROCESS_VM_NOWAIT flags
introduced in process_vm_readv/writev.

Tests cover:
- basic read with no flags
- invalid flags (EINVAL)
- invalid address (EFAULT)
- flag validation precedence over address validation
- invalid pidfd (EBADF)
- invalid pid (ESRCH)
- PROCESS_VM_PIDFD: read via pidfd
- PROCESS_VM_NOWAIT: read from resident memory
- PROCESS_VM_PIDFD | PROCESS_VM_NOWAIT combined
- userfaultfd blocking read (no flags)
- PROCESS_VM_NOWAIT with userfaultfd (non-blocking, returns EFAULT)
- PROCESS_VM_NOWAIT partial read with single iovec across resident
  and non-resident pages (returns page_size bytes)
- PROCESS_VM_NOWAIT partial read with two iovecs, first resident,
  second non-resident (returns page_size bytes)

Tests gracefully SKIP on kernels without PROCESS_VM_PIDFD or
PROCESS_VM_NOWAIT support (EINVAL).

Signed-off-by: Alban Crequy <albancrequy@microsoft.com>
---
v5:
- Add process_vm_readv to .gitignore (Sashiko)
- Use ~0UL instead of 255 for invalid flags tests (Sashiko)
- Use volatile uint8_t instead of volatile uint64_t for page fault-in
  to avoid alignment issues on strict-alignment architectures (Sashiko)
- Use UFFDIO_UNREGISTER in uffd handler error path to safely unblock
  the main thread without double-close (Sashiko)

v4:
- Add selftests for NOWAIT partial reads across resident and
  non-resident pages (single iovec and two iovecs)
- SKIP tests gracefully on kernels without flag support (Sashiko)
- Verify content of partial reads

v3:
- Add selftest for invalid pidfd (David Hildenbrand)
- Add selftest for invalid pid
- SKIP on kernels without PROCESS_VM_PIDFD support
- Remove hardcoded __NR_pidfd_open fallback, use <sys/syscall.h> (Sashiko)
- SKIP pidfd tests on kernels without pidfd_open (ENOSYS) (Sashiko)
- SKIP userfaultfd tests when unprivileged userfaultfd is disabled (EPERM) (Sashiko)
- Fault in test_data before NOWAIT tests to ensure page is resident (Sashiko)
- Add ksft_process_vm_readv.sh wrapper and run_vmtests.sh entry

v2:
- New patch.

 tools/testing/selftests/mm/.gitignore         |   1 +
 tools/testing/selftests/mm/Makefile           |   2 +
 .../selftests/mm/ksft_process_vm_readv.sh     |   4 +
 tools/testing/selftests/mm/process_vm_readv.c | 591 ++++++++++++++++++
 tools/testing/selftests/mm/run_vmtests.sh     |   4 +
 5 files changed, 602 insertions(+)
 create mode 100755 tools/testing/selftests/mm/ksft_process_vm_readv.sh
 create mode 100644 tools/testing/selftests/mm/process_vm_readv.c

diff --git a/tools/testing/selftests/mm/.gitignore b/tools/testing/selftests/mm/.gitignore
index b0c30c5ee9e3..78525dfa5a95 100644
--- a/tools/testing/selftests/mm/.gitignore
+++ b/tools/testing/selftests/mm/.gitignore
@@ -23,6 +23,7 @@ transhuge-stress
 pagemap_ioctl
 pfnmap
 process_madv
+process_vm_readv
 *.tmp*
 protection_keys
 protection_keys_32
diff --git a/tools/testing/selftests/mm/Makefile b/tools/testing/selftests/mm/Makefile
index cd24596cdd27..feb3a0b9a57e 100644
--- a/tools/testing/selftests/mm/Makefile
+++ b/tools/testing/selftests/mm/Makefile
@@ -106,6 +106,7 @@ TEST_GEN_FILES += guard-regions
 TEST_GEN_FILES += merge
 TEST_GEN_FILES += rmap
 TEST_GEN_FILES += folio_split_race_test
+TEST_GEN_FILES += process_vm_readv
 
 ifneq ($(ARCH),arm64)
 TEST_GEN_FILES += soft-dirty
@@ -167,6 +168,7 @@ TEST_PROGS += ksft_pfnmap.sh
 TEST_PROGS += ksft_pkey.sh
 TEST_PROGS += ksft_process_madv.sh
 TEST_PROGS += ksft_process_mrelease.sh
+TEST_PROGS += ksft_process_vm_readv.sh
 TEST_PROGS += ksft_rmap.sh
 TEST_PROGS += ksft_soft_dirty.sh
 TEST_PROGS += ksft_thp.sh
diff --git a/tools/testing/selftests/mm/ksft_process_vm_readv.sh b/tools/testing/selftests/mm/ksft_process_vm_readv.sh
new file mode 100755
index 000000000000..09d0fcc9a35d
--- /dev/null
+++ b/tools/testing/selftests/mm/ksft_process_vm_readv.sh
@@ -0,0 +1,4 @@
+#!/bin/sh -e
+# SPDX-License-Identifier: GPL-2.0
+
+./run_vmtests.sh -t process_vm_readv
diff --git a/tools/testing/selftests/mm/process_vm_readv.c b/tools/testing/selftests/mm/process_vm_readv.c
new file mode 100644
index 000000000000..34aa44c5eb67
--- /dev/null
+++ b/tools/testing/selftests/mm/process_vm_readv.c
@@ -0,0 +1,591 @@
+// SPDX-License-Identifier: GPL-2.0-only
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <poll.h>
+#include <pthread.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/syscall.h>
+#include <sys/uio.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#include <linux/userfaultfd.h>
+
+#include "kselftest_harness.h"
+
+#ifndef PROCESS_VM_PIDFD
+#define PROCESS_VM_PIDFD	(1UL << 0)
+#endif
+
+#ifndef PROCESS_VM_NOWAIT
+#define PROCESS_VM_NOWAIT	(1UL << 1)
+#endif
+
+static int sys_pidfd_open(pid_t pid, unsigned int flags)
+{
+	return syscall(__NR_pidfd_open, pid, flags);
+}
+
+static const uint8_t test_data[] = { 0x01, 0x02, 0x03, 0x04,
+				     0x05, 0x06, 0x07, 0x08 };
+#define POISON_BYTE 0xCC
+
+/*
+ * Test: basic process_vm_readv with no flags
+ */
+TEST(read_basic)
+{
+	uint8_t buf[sizeof(test_data)];
+	struct iovec local_iov = { .iov_base = buf, .iov_len = sizeof(buf) };
+	struct iovec remote_iov = {
+		.iov_base = (void *)test_data,
+		.iov_len = sizeof(test_data)
+	};
+	ssize_t n;
+
+	memset(buf, POISON_BYTE, sizeof(buf));
+	n = process_vm_readv(getpid(), &local_iov, 1, &remote_iov, 1, 0);
+	ASSERT_EQ(sizeof(test_data), n);
+	ASSERT_EQ(0, memcmp(buf, test_data, sizeof(test_data)));
+}
+
+/*
+ * Test: invalid flags should return EINVAL
+ */
+TEST(read_invalid_flags)
+{
+	uint8_t buf[8] = { 0 };
+	struct iovec local_iov = { .iov_base = buf, .iov_len = sizeof(buf) };
+	struct iovec remote_iov = {
+		.iov_base = (void *)test_data,
+		.iov_len = sizeof(test_data)
+	};
+	ssize_t n;
+
+	n = process_vm_readv(getpid(), &local_iov, 1, &remote_iov, 1, ~0UL);
+	ASSERT_EQ(-1, n);
+	ASSERT_EQ(EINVAL, errno);
+}
+
+/*
+ * Test: invalid address should return EFAULT
+ */
+TEST(read_invalid_address)
+{
+	uint8_t buf[8] = { 0 };
+	struct iovec local_iov = { .iov_base = buf, .iov_len = sizeof(buf) };
+	struct iovec remote_iov = { .iov_base = NULL, .iov_len = 8 };
+	ssize_t n;
+
+	n = process_vm_readv(getpid(), &local_iov, 1, &remote_iov, 1, 0);
+	ASSERT_EQ(-1, n);
+	ASSERT_EQ(EFAULT, errno);
+}
+
+/*
+ * Test: invalid address with invalid flags should return EINVAL
+ * (flag check happens before address validation)
+ */
+TEST(read_invalid_address_invalid_flags)
+{
+	uint8_t buf[8] = { 0 };
+	struct iovec local_iov = { .iov_base = buf, .iov_len = sizeof(buf) };
+	struct iovec remote_iov = { .iov_base = NULL, .iov_len = 8 };
+	ssize_t n;
+
+	n = process_vm_readv(getpid(), &local_iov, 1, &remote_iov, 1, ~0UL);
+	ASSERT_EQ(-1, n);
+	ASSERT_EQ(EINVAL, errno);
+}
+
+/*
+ * Test: invalid address with all valid flags should return EFAULT
+ * (flags are valid so we get past the flag check to the address check)
+ */
+TEST(read_invalid_address_all_valid_flags)
+{
+	int pidfd;
+	struct iovec local_iov = { .iov_base = NULL, .iov_len = 8 };
+	struct iovec remote_iov = { .iov_base = NULL, .iov_len = 8 };
+	ssize_t n;
+
+	pidfd = sys_pidfd_open(getpid(), 0);
+	if (pidfd < 0 && errno == ENOSYS)
+		SKIP(return, "pidfd_open not supported");
+	ASSERT_GE(pidfd, 0);
+
+	n = process_vm_readv(pidfd, &local_iov, 1, &remote_iov, 1,
+			     PROCESS_VM_PIDFD | PROCESS_VM_NOWAIT);
+	ASSERT_EQ(-1, n);
+	if (errno == EINVAL)
+		SKIP(return,
+		     "PROCESS_VM_PIDFD or PROCESS_VM_NOWAIT not supported");
+	ASSERT_EQ(EFAULT, errno);
+
+	close(pidfd);
+}
+
+/*
+ * Test: read with an invalid pidfd should return an error, not crash
+ */
+TEST(read_invalid_pidfd)
+{
+	uint8_t buf[sizeof(test_data)] = { 0 };
+	struct iovec local_iov = { .iov_base = buf, .iov_len = sizeof(buf) };
+	struct iovec remote_iov = {
+		.iov_base = (void *)test_data,
+		.iov_len = sizeof(test_data)
+	};
+	ssize_t n;
+
+	/* fd 9999 is almost certainly not a valid pidfd */
+	n = process_vm_readv(9999, &local_iov, 1, &remote_iov, 1,
+			     PROCESS_VM_PIDFD);
+	ASSERT_EQ(-1, n);
+	if (errno == EINVAL)
+		SKIP(return, "PROCESS_VM_PIDFD not supported");
+	ASSERT_EQ(EBADF, errno);
+}
+
+/*
+ * Test: read with an invalid pid should return ESRCH
+ */
+TEST(read_invalid_pid)
+{
+	uint8_t buf[sizeof(test_data)] = { 0 };
+	struct iovec local_iov = { .iov_base = buf, .iov_len = sizeof(buf) };
+	struct iovec remote_iov = {
+		.iov_base = (void *)test_data,
+		.iov_len = sizeof(test_data)
+	};
+	ssize_t n;
+
+	/* pid 999999 is almost certainly not a valid process */
+	n = process_vm_readv(999999, &local_iov, 1, &remote_iov, 1, 0);
+	ASSERT_EQ(-1, n);
+	ASSERT_EQ(ESRCH, errno);
+}
+
+/*
+ * Test: read with PIDFD flag
+ */
+TEST(read_pidfd)
+{
+	uint8_t buf[sizeof(test_data)];
+	struct iovec local_iov = { .iov_base = buf, .iov_len = sizeof(buf) };
+	struct iovec remote_iov = {
+		.iov_base = (void *)test_data,
+		.iov_len = sizeof(test_data)
+	};
+	ssize_t n;
+	int pidfd;
+
+	memset(buf, POISON_BYTE, sizeof(buf));
+	pidfd = sys_pidfd_open(getpid(), 0);
+	if (pidfd < 0 && errno == ENOSYS)
+		SKIP(return, "pidfd_open not supported");
+	ASSERT_GE(pidfd, 0);
+
+	n = process_vm_readv(pidfd, &local_iov, 1, &remote_iov, 1,
+			     PROCESS_VM_PIDFD);
+	if (n == -1 && errno == EINVAL)
+		SKIP(return, "PROCESS_VM_PIDFD not supported");
+	ASSERT_EQ(sizeof(test_data), n);
+	ASSERT_EQ(0, memcmp(buf, test_data, sizeof(test_data)));
+
+	close(pidfd);
+}
+
+/*
+ * Test: read with NOWAIT from resident memory (should succeed)
+ */
+TEST(read_nowait_resident)
+{
+	uint8_t buf[sizeof(test_data)];
+	struct iovec local_iov = { .iov_base = buf, .iov_len = sizeof(buf) };
+	struct iovec remote_iov = {
+		.iov_base = (void *)test_data,
+		.iov_len = sizeof(test_data)
+	};
+	ssize_t n;
+
+	*(volatile uint8_t *)test_data; /* fault in page for NOWAIT */
+	memset(buf, POISON_BYTE, sizeof(buf));
+	n = process_vm_readv(getpid(), &local_iov, 1, &remote_iov, 1,
+			     PROCESS_VM_NOWAIT);
+	if (n == -1 && errno == EINVAL)
+		SKIP(return, "PROCESS_VM_NOWAIT not supported");
+	ASSERT_EQ(sizeof(test_data), n);
+	ASSERT_EQ(0, memcmp(buf, test_data, sizeof(test_data)));
+}
+
+/*
+ * Test: read with PIDFD + NOWAIT from resident memory
+ */
+TEST(read_pidfd_nowait_resident)
+{
+	uint8_t buf[sizeof(test_data)];
+	struct iovec local_iov = { .iov_base = buf, .iov_len = sizeof(buf) };
+	struct iovec remote_iov = {
+		.iov_base = (void *)test_data,
+		.iov_len = sizeof(test_data)
+	};
+	ssize_t n;
+	int pidfd;
+
+	*(volatile uint8_t *)test_data; /* fault in page for NOWAIT */
+	memset(buf, POISON_BYTE, sizeof(buf));
+	pidfd = sys_pidfd_open(getpid(), 0);
+	if (pidfd < 0 && errno == ENOSYS)
+		SKIP(return, "pidfd_open not supported");
+	ASSERT_GE(pidfd, 0);
+
+	n = process_vm_readv(pidfd, &local_iov, 1, &remote_iov, 1,
+			     PROCESS_VM_PIDFD | PROCESS_VM_NOWAIT);
+	if (n == -1 && errno == EINVAL)
+		SKIP(return, "PROCESS_VM_PIDFD or PROCESS_VM_NOWAIT not supported");
+	ASSERT_EQ(sizeof(test_data), n);
+	ASSERT_EQ(0, memcmp(buf, test_data, sizeof(test_data)));
+
+	close(pidfd);
+}
+
+/*
+ * Userfaultfd helpers for NOWAIT tests
+ */
+static int setup_userfaultfd(void)
+{
+	struct uffdio_api api = { .api = UFFD_API };
+	int uffd;
+
+	uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
+	if (uffd < 0)
+		return -errno;
+
+	if (ioctl(uffd, UFFDIO_API, &api)) {
+		close(uffd);
+		return -errno;
+	}
+
+	return uffd;
+}
+
+static void *register_uffd_region(int uffd, size_t size)
+{
+	struct uffdio_register reg;
+	void *mem;
+
+	mem = mmap(NULL, size, PROT_READ | PROT_WRITE,
+		   MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+	if (mem == MAP_FAILED)
+		return NULL;
+
+	reg.range.start = (unsigned long)mem;
+	reg.range.len = size;
+	reg.mode = UFFDIO_REGISTER_MODE_MISSING;
+	if (ioctl(uffd, UFFDIO_REGISTER, &reg)) {
+		munmap(mem, size);
+		return NULL;
+	}
+
+	return mem;
+}
+
+struct uffd_handler_args {
+	int uffd;
+	const void *content;
+	size_t content_len;
+	void *mem;
+	size_t mem_len;
+};
+
+static void *uffd_handler_thread(void *arg)
+{
+	struct uffd_handler_args *ha = arg;
+	struct uffd_msg msg;
+	struct uffdio_copy uffd_copy;
+	struct pollfd pfd = {
+		.fd = ha->uffd,
+		.events = POLLIN
+	};
+	void *page;
+	long page_size = sysconf(_SC_PAGESIZE);
+	int ret;
+
+	page = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
+		    MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+	if (page == MAP_FAILED)
+		return (void *)(long)-ENOMEM;
+
+	memcpy(page, ha->content, ha->content_len);
+
+	ret = poll(&pfd, 1, 5000);
+	if (ret <= 0)
+		goto err;
+
+	if (read(ha->uffd, &msg, sizeof(msg)) != sizeof(msg))
+		goto err;
+
+	if (msg.event != UFFD_EVENT_PAGEFAULT)
+		goto err;
+
+	uffd_copy.dst = msg.arg.pagefault.address & ~(page_size - 1);
+	uffd_copy.src = (unsigned long)page;
+	uffd_copy.len = page_size;
+	uffd_copy.mode = 0;
+	ioctl(ha->uffd, UFFDIO_COPY, &uffd_copy);
+
+	munmap(page, page_size);
+	return NULL;
+
+err:
+	/*
+	 * Unregister the uffd region to unblock the main thread if it
+	 * is waiting for page fault resolution.
+	 */
+	{
+		struct uffdio_range range = {
+			.start = (unsigned long)ha->mem,
+			.len = ha->mem_len,
+		};
+		ioctl(ha->uffd, UFFDIO_UNREGISTER, &range);
+	}
+	munmap(page, page_size);
+	return (void *)(long)-EIO;
+}
+
+/*
+ * Test: read from userfaultfd-registered memory (no flags, should block
+ * until page fault is resolved by handler thread)
+ */
+TEST(read_userfaultfd_blocking)
+{
+	int uffd;
+	void *mem;
+	long page_size = sysconf(_SC_PAGESIZE);
+	uint8_t buf[sizeof(test_data)];
+	struct iovec local_iov = { .iov_base = buf, .iov_len = sizeof(buf) };
+	struct iovec remote_iov;
+	struct uffd_handler_args ha;
+	pthread_t handler;
+	ssize_t n;
+
+	memset(buf, POISON_BYTE, sizeof(buf));
+
+	uffd = setup_userfaultfd();
+	if (uffd == -EPERM)
+		SKIP(return, "userfaultfd requires privileges (vm.unprivileged_userfaultfd=0)");
+	if (uffd == -ENOSYS)
+		SKIP(return, "userfaultfd not supported");
+	ASSERT_GE(uffd, 0);
+
+	mem = register_uffd_region(uffd, page_size);
+	ASSERT_NE(NULL, mem);
+
+	ha.uffd = uffd;
+	ha.content = test_data;
+	ha.content_len = sizeof(test_data);
+	ha.mem = mem;
+	ha.mem_len = page_size;
+	ASSERT_EQ(0, pthread_create(&handler, NULL, uffd_handler_thread, &ha));
+
+	remote_iov.iov_base = mem;
+	remote_iov.iov_len = sizeof(test_data);
+	n = process_vm_readv(getpid(), &local_iov, 1, &remote_iov, 1, 0);
+	ASSERT_EQ(sizeof(test_data), n);
+	ASSERT_EQ(0, memcmp(buf, test_data, sizeof(test_data)));
+
+	pthread_join(handler, NULL);
+	munmap(mem, page_size);
+	close(uffd);
+}
+
+/*
+ * Test: read with NOWAIT from userfaultfd-registered memory that has
+ * not been faulted in yet. Should return EFAULT (not block).
+ */
+TEST(read_nowait_userfaultfd)
+{
+	int uffd;
+	void *mem;
+	long page_size = sysconf(_SC_PAGESIZE);
+	uint8_t buf[sizeof(test_data)] = { 0 };
+	struct iovec local_iov = { .iov_base = buf, .iov_len = sizeof(buf) };
+	struct iovec remote_iov;
+	ssize_t n;
+
+	uffd = setup_userfaultfd();
+	if (uffd == -EPERM)
+		SKIP(return, "userfaultfd requires privileges (vm.unprivileged_userfaultfd=0)");
+	if (uffd == -ENOSYS)
+		SKIP(return, "userfaultfd not supported");
+	ASSERT_GE(uffd, 0);
+
+	mem = register_uffd_region(uffd, page_size);
+	ASSERT_NE(NULL, mem);
+
+	/* Ensure the page is not present */
+	madvise(mem, page_size, MADV_DONTNEED);
+
+	remote_iov.iov_base = mem;
+	remote_iov.iov_len = sizeof(test_data);
+	n = process_vm_readv(getpid(), &local_iov, 1, &remote_iov, 1,
+			     PROCESS_VM_NOWAIT);
+	if (n == -1 && errno == EINVAL)
+		SKIP(return, "PROCESS_VM_NOWAIT not supported");
+	ASSERT_EQ(-1, n);
+	ASSERT_EQ(EFAULT, errno);
+
+	munmap(mem, page_size);
+	close(uffd);
+}
+
+/*
+ * Test: NOWAIT read across two pages with a single iovec, where the
+ * first page is resident and the second is not. Tests whether a
+ * partial read within a single iovec element is possible.
+ */
+TEST(read_nowait_partial_single_iovec)
+{
+	int uffd;
+	void *mem;
+	long page_size = sysconf(_SC_PAGESIZE);
+	uint8_t *buf;
+	struct iovec local_iov;
+	struct iovec remote_iov;
+	struct uffdio_copy uffd_copy;
+	ssize_t n;
+	void *src_page;
+
+	uffd = setup_userfaultfd();
+	if (uffd == -EPERM)
+		SKIP(return, "userfaultfd requires privileges (vm.unprivileged_userfaultfd=0)");
+	if (uffd == -ENOSYS)
+		SKIP(return, "userfaultfd not supported");
+	ASSERT_GE(uffd, 0);
+
+	/* Allocate 2 pages and register with userfaultfd */
+	mem = register_uffd_region(uffd, 2 * page_size);
+	ASSERT_NE(NULL, mem);
+
+	/* Resolve page 1 via UFFDIO_COPY, leave page 2 missing */
+	src_page = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
+			MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+	ASSERT_NE(MAP_FAILED, src_page);
+	memset(src_page, 0xAA, page_size);
+
+	uffd_copy.dst = (unsigned long)mem;
+	uffd_copy.src = (unsigned long)src_page;
+	uffd_copy.len = page_size;
+	uffd_copy.mode = 0;
+	ASSERT_EQ(0, ioctl(uffd, UFFDIO_COPY, &uffd_copy));
+	munmap(src_page, page_size);
+
+	/* Read across both pages with a single iovec */
+	buf = malloc(2 * page_size);
+	ASSERT_NE(NULL, buf);
+	memset(buf, POISON_BYTE, 2 * page_size);
+
+	local_iov.iov_base = buf;
+	local_iov.iov_len = 2 * page_size;
+	remote_iov.iov_base = mem;
+	remote_iov.iov_len = 2 * page_size;
+
+	n = process_vm_readv(getpid(), &local_iov, 1, &remote_iov, 1,
+			     PROCESS_VM_NOWAIT);
+	if (n == -1 && errno == EINVAL)
+		SKIP(return, "PROCESS_VM_NOWAIT not supported");
+	ASSERT_EQ(page_size, n);
+
+	/* Verify the first page was read correctly */
+	for (int i = 0; i < page_size; i++)
+		ASSERT_EQ(0xAA, buf[i]);
+
+	free(buf);
+	munmap(mem, 2 * page_size);
+	close(uffd);
+}
+
+/*
+ * Test: NOWAIT read across two pages with two iovecs (one per page),
+ * where the first page is resident and the second is not. Tests
+ * whether the syscall returns bytes from the first iovec.
+ */
+TEST(read_nowait_partial_two_iovecs)
+{
+	int uffd;
+	void *mem;
+	long page_size = sysconf(_SC_PAGESIZE);
+	uint8_t *buf1, *buf2;
+	struct iovec local_iov[2];
+	struct iovec remote_iov[2];
+	struct uffdio_copy uffd_copy;
+	ssize_t n;
+	void *src_page;
+
+	uffd = setup_userfaultfd();
+	if (uffd == -EPERM)
+		SKIP(return, "userfaultfd requires privileges (vm.unprivileged_userfaultfd=0)");
+	if (uffd == -ENOSYS)
+		SKIP(return, "userfaultfd not supported");
+	ASSERT_GE(uffd, 0);
+
+	/* Allocate 2 pages and register with userfaultfd */
+	mem = register_uffd_region(uffd, 2 * page_size);
+	ASSERT_NE(NULL, mem);
+
+	/* Resolve page 1 via UFFDIO_COPY, leave page 2 missing */
+	src_page = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
+			MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+	ASSERT_NE(MAP_FAILED, src_page);
+	memset(src_page, 0xBB, page_size);
+
+	uffd_copy.dst = (unsigned long)mem;
+	uffd_copy.src = (unsigned long)src_page;
+	uffd_copy.len = page_size;
+	uffd_copy.mode = 0;
+	ASSERT_EQ(0, ioctl(uffd, UFFDIO_COPY, &uffd_copy));
+	munmap(src_page, page_size);
+
+	/* Two iovecs: one for each page */
+	buf1 = malloc(page_size);
+	buf2 = malloc(page_size);
+	ASSERT_NE(NULL, buf1);
+	ASSERT_NE(NULL, buf2);
+	memset(buf1, POISON_BYTE, page_size);
+	memset(buf2, POISON_BYTE, page_size);
+
+	local_iov[0].iov_base = buf1;
+	local_iov[0].iov_len = page_size;
+	local_iov[1].iov_base = buf2;
+	local_iov[1].iov_len = page_size;
+
+	remote_iov[0].iov_base = mem;
+	remote_iov[0].iov_len = page_size;
+	remote_iov[1].iov_base = (char *)mem + page_size;
+	remote_iov[1].iov_len = page_size;
+
+	n = process_vm_readv(getpid(), local_iov, 2, remote_iov, 2,
+			     PROCESS_VM_NOWAIT);
+	if (n == -1 && errno == EINVAL)
+		SKIP(return, "PROCESS_VM_NOWAIT not supported");
+	ASSERT_EQ(page_size, n);
+
+	/* Verify the first page was read correctly */
+	for (int i = 0; i < page_size; i++)
+		ASSERT_EQ(0xBB, buf1[i]);
+
+	free(buf1);
+	free(buf2);
+	munmap(mem, 2 * page_size);
+	close(uffd);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/mm/run_vmtests.sh b/tools/testing/selftests/mm/run_vmtests.sh
index c17b133a81d2..f7b55dea8d68 100755
--- a/tools/testing/selftests/mm/run_vmtests.sh
+++ b/tools/testing/selftests/mm/run_vmtests.sh
@@ -91,6 +91,8 @@ separated by spaces:
 	test VMA merge cases behave as expected
 - rmap
 	test rmap behaves as expected
+- process_vm_readv
+	test process_vm_readv flags (pidfd, nowait)
 - memory-failure
 	test memory-failure behaves as expected
 
@@ -531,6 +533,8 @@ CATEGORY="page_frag" run_test ./test_page_frag.sh nonaligned
 
 CATEGORY="rmap" run_test ./rmap
 
+CATEGORY="process_vm_readv" run_test ./process_vm_readv
+
 # Try to load hwpoison_inject if not present.
 HWPOISON_DIR=/sys/kernel/debug/hwpoison/
 if [ ! -d "$HWPOISON_DIR" ]; then
-- 
2.45.0


^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH v5 1/2] mm/process_vm_access: pidfd and nowait support for process_vm_readv/writev
  2026-06-02 10:09 ` [PATCH v5 1/2] " Alban Crequy
@ 2026-06-02 12:16   ` David Hildenbrand (Arm)
  2026-06-03  8:27   ` Alban Crequy
  1 sibling, 0 replies; 9+ messages in thread
From: David Hildenbrand (Arm) @ 2026-06-02 12:16 UTC (permalink / raw)
  To: Alban Crequy, Andrew Morton, Christian Brauner
  Cc: Lorenzo Stoakes, Liam R . Howlett, Vlastimil Babka,
	Mike Rapoport, Suren Baghdasaryan, Michal Hocko, linux-kernel,
	linux-mm, Alban Crequy, Peter Xu, Willy Tarreau, linux-kselftest,
	shuah, Usama Arif, David Laight

On 6/2/26 12:09, Alban Crequy wrote:
> From: Alban Crequy <albancrequy@microsoft.com>
> 
> There are two categories of users for process_vm_readv:
> 
> 1. Debuggers like GDB or strace.
> 
>    When a debugger attempts to read the target memory and triggers a
>    page fault, the page fault needs to be resolved so that the debugger
>    can accurately interpret the memory. A debugger is typically attached
>    to a single process.
> 
> 2. Profilers like OpenTelemetry eBPF Profiler.
> 
>    The profiler uses a perf event to get stack traces from all
>    processes at 20Hz (20 stack traces to resolve per second). For
>    interpreted languages (Ruby, Python, etc.), the profiler uses
>    process_vm_readv to get the correct symbols. In this case,
>    performance is the most important. It is fine if some stack traces
>    cannot be resolved as long as it is not statistically significant.
> 
> The current behaviour of process_vm_readv is to resolve page faults in
> the target VM. This is as desired for debuggers, but unwelcome for
> profilers because the page fault resolution could take a lot of time
> depending on the backing filesystem. Additionally, since profilers
> monitor all processes, we don't want a slow page fault resolution for
> one target process slowing down the monitoring for all other target
> processes.
> 
> This patch adds the flag PROCESS_VM_NOWAIT, so the caller can choose to
> not block on IO if the memory access causes a page fault. When a page
> is not resident and would require IO to fault in, the syscall returns
> a short read (the number of bytes successfully read before the fault)
> or -1 with errno set to EFAULT if no bytes were read.
> 
> Additionally, this patch adds the flag PROCESS_VM_PIDFD to refer to the
> remote process via PID file descriptor instead of PID. Such a file
> descriptor can be obtained with pidfd_open(2). This is useful to avoid
> the pid number being reused. It is unlikely to happen for debuggers
> because they can monitor the target process termination in other ways
> (ptrace), but can be helpful in some profiling scenarios. When using
> PROCESS_VM_PIDFD, the first argument is a pidfd instead of a pid. If
> the pidfd is invalid, the syscall returns -1 with errno set to EBADF.
> 
> If a given flag is unsupported, the syscall returns the error EINVAL
> without checking the buffers. This gives a way to userspace to detect
> whether the current kernel supports a specific flag:
> 
>   process_vm_readv(pid, NULL, 1, NULL, 1, PROCESS_VM_PIDFD)
>   -> EINVAL if the kernel does not support the flag PROCESS_VM_PIDFD
>      (before this patch)
>   -> EFAULT if the kernel supports the flag (after this patch)
> 
> Suggested man page update for process_vm_readv(2):
> 
>   The flags argument is the bitwise OR of zero or more of these flags:
> 
>   PROCESS_VM_PIDFD (since Linux 7.x)
>       The pid argument is a PID file descriptor (see pidfd_open(2))
>       instead of a PID number. When using this flag, the existing
>       ESRCH error applies if the process referred to by the pidfd
>       has exited.
> 
>   PROCESS_VM_NOWAIT (since Linux 7.x)
>       Do not block on IO. If a page in the remote address space is not
>       resident and would require disk IO to fault in, the system call
>       returns a short read or fails with EFAULT if no bytes were read.
> 
>   Additional error:
> 
>   EBADF  pid is not a valid file descriptor (PROCESS_VM_PIDFD only).
> 
> Signed-off-by: Alban Crequy <albancrequy@microsoft.com>
> ---

Nothing jumped at me, thanks!

Acked-by: David Hildenbrand (Arm) <david@kernel.org>

-- 
Cheers,

David

^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH v5 1/2] mm/process_vm_access: pidfd and nowait support for process_vm_readv/writev
  2026-06-02 10:09 ` [PATCH v5 1/2] " Alban Crequy
  2026-06-02 12:16   ` David Hildenbrand (Arm)
@ 2026-06-03  8:27   ` Alban Crequy
  2026-06-03 13:54     ` David Hildenbrand (Arm)
  2026-06-04 12:59     ` Christian Brauner
  1 sibling, 2 replies; 9+ messages in thread
From: Alban Crequy @ 2026-06-03  8:27 UTC (permalink / raw)
  To: Andrew Morton, David Hildenbrand, Christian Brauner
  Cc: Lorenzo Stoakes, Liam R . Howlett, Vlastimil Babka,
	Mike Rapoport, Suren Baghdasaryan, Michal Hocko, linux-kernel,
	linux-mm, Alban Crequy, Peter Xu, Willy Tarreau, linux-kselftest,
	shuah, Usama Arif, David Laight, Alban Crequy

Sashiko raised a question about pidfd_get_task() and PIDFD_THREAD [1],
so I ran some tests to understand the behavior.
[1] https://sashiko.dev/#/patchset/20260602100917.3641359-1-alban.crequy@gmail.com

pidfd_get_task() always resolves pidfds using PIDTYPE_TGID (kernel/pid.c
line 640), regardless of whether the pidfd was created with PIDFD_THREAD.
This means:

 - A PIDFD_THREAD pidfd for a non-leader thread fails with ESRCH.
 - A regular pidfd for a process whose leader has exited (pthread_exit
   in main, secondary thread still alive) also fails with ESRCH.

This is not specific to my patch: process_madvise() uses pidfd_get_task()
in the same way and has the same behavior. I wrote a test program
confirming this:

  https://github.com/alban/tests/tree/alban_pvm_flags/pvm_flags/pidfd_thread_test

Results summary:

  All threads alive:
    pidfd_open(pid, 0)              + process_vm_readv: OK
    pidfd_open(tid, PIDFD_THREAD)   + process_vm_readv: OK (leader tid)
    pidfd_open(tid, PIDFD_THREAD)   + process_vm_readv: ESRCH (non-leader)

  Leader thread exited (secondary still alive):
    pidfd_open(pid, 0)              + process_vm_readv: ESRCH
    pidfd_open(pid, PIDFD_THREAD)   + process_vm_readv: ESRCH
    pidfd_open(tid, PIDFD_THREAD)   + process_vm_readv: ESRCH (non-leader)
    process_vm_readv(tid, flags=0)                    : OK (plain TID path)

  process_madvise() behaves identically in all cases above.

For the non-leader thread case when all threads are alive, this is fine in
practice: all threads share the same mm_struct, so profilers just use a regular
pidfd for the thread-group leader.

However, the exited-leader case is a real limitation for profilers.
OpenTelemetry eBPF Profiler wants to profile a process where the main thread
has exited but secondary threads are still running [2].
[2] https://github.com/open-telemetry/opentelemetry-ebpf-profiler/pull/376

Using plain TIDs (flags=0) would work, but it means users cannot use
PROCESS_VM_PIDFD in this scenario.

What do you think this patch should do? I see two options:
 - Address this limitation in a separate future patch that fixes
   pidfd_get_task() to use PIDTYPE_PID when PIDFD_THREAD is detected in
   f_flags, benefiting all callers (process_vm_readv, process_madvise,
   and any future users).
 - Address it in this patch series.

^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH v5 1/2] mm/process_vm_access: pidfd and nowait support for process_vm_readv/writev
  2026-06-03  8:27   ` Alban Crequy
@ 2026-06-03 13:54     ` David Hildenbrand (Arm)
  2026-06-04 12:49       ` Christian Brauner
  2026-06-04 12:59     ` Christian Brauner
  1 sibling, 1 reply; 9+ messages in thread
From: David Hildenbrand (Arm) @ 2026-06-03 13:54 UTC (permalink / raw)
  To: Alban Crequy, Andrew Morton, Christian Brauner
  Cc: Lorenzo Stoakes, Liam R . Howlett, Vlastimil Babka,
	Mike Rapoport, Suren Baghdasaryan, Michal Hocko, linux-kernel,
	linux-mm, Alban Crequy, Peter Xu, Willy Tarreau, linux-kselftest,
	shuah, Usama Arif, David Laight

> However, the exited-leader case is a real limitation for profilers.
> OpenTelemetry eBPF Profiler wants to profile a process where the main thread
> has exited but secondary threads are still running [2].
> [2] https://github.com/open-telemetry/opentelemetry-ebpf-profiler/pull/376
> 
> Using plain TIDs (flags=0) would work, but it means users cannot use
> PROCESS_VM_PIDFD in this scenario.
> 
> What do you think this patch should do? I see two options:
>  - Address this limitation in a separate future patch that fixes
>    pidfd_get_task() to use PIDTYPE_PID when PIDFD_THREAD is detected in
>    f_flags, benefiting all callers (process_vm_readv, process_madvise,
>    and any future users).

Extending it consistently for that scenario seems like the right thing to do?
@Christian?

-- 
Cheers,

David

^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH v5 1/2] mm/process_vm_access: pidfd and nowait support for process_vm_readv/writev
  2026-06-03 13:54     ` David Hildenbrand (Arm)
@ 2026-06-04 12:49       ` Christian Brauner
  2026-06-05  9:31         ` David Hildenbrand (Arm)
  0 siblings, 1 reply; 9+ messages in thread
From: Christian Brauner @ 2026-06-04 12:49 UTC (permalink / raw)
  To: David Hildenbrand (Arm)
  Cc: Alban Crequy, Andrew Morton, Christian Brauner, Lorenzo Stoakes,
	Liam R . Howlett, Vlastimil Babka, Mike Rapoport,
	Suren Baghdasaryan, Michal Hocko, linux-kernel, linux-mm,
	Alban Crequy, Peter Xu, Willy Tarreau, linux-kselftest, shuah,
	Usama Arif, David Laight

On 2026-06-03 15:54 +0200, David Hildenbrand (Arm) wrote:
> > However, the exited-leader case is a real limitation for profilers.
> > OpenTelemetry eBPF Profiler wants to profile a process where the main thread
> > has exited but secondary threads are still running [2].
> > [2] https://github.com/open-telemetry/opentelemetry-ebpf-profiler/pull/376
> > 
> > Using plain TIDs (flags=0) would work, but it means users cannot use
> > PROCESS_VM_PIDFD in this scenario.
> > 
> > What do you think this patch should do? I see two options:
> >  - Address this limitation in a separate future patch that fixes
> >    pidfd_get_task() to use PIDTYPE_PID when PIDFD_THREAD is detected in
> >    f_flags, benefiting all callers (process_vm_readv, process_madvise,
> >    and any future users).
> 
> Extending it consistently for that scenario seems like the right thing to do?
> @Christian?

Yes, but I cannot vouch whether the mm operations deal correctively with
subthreads.


^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH v5 1/2] mm/process_vm_access: pidfd and nowait support for process_vm_readv/writev
  2026-06-03  8:27   ` Alban Crequy
  2026-06-03 13:54     ` David Hildenbrand (Arm)
@ 2026-06-04 12:59     ` Christian Brauner
  1 sibling, 0 replies; 9+ messages in thread
From: Christian Brauner @ 2026-06-04 12:59 UTC (permalink / raw)
  To: Alban Crequy
  Cc: Andrew Morton, David Hildenbrand, Christian Brauner,
	Lorenzo Stoakes, Liam R . Howlett, Vlastimil Babka,
	Mike Rapoport, Suren Baghdasaryan, Michal Hocko, linux-kernel,
	linux-mm, Alban Crequy, Peter Xu, Willy Tarreau, linux-kselftest,
	shuah, Usama Arif, David Laight

On 2026-06-03 10:27 +0200, Alban Crequy wrote:
> Sashiko raised a question about pidfd_get_task() and PIDFD_THREAD [1],
> so I ran some tests to understand the behavior.
> [1] https://sashiko.dev/#/patchset/20260602100917.3641359-1-alban.crequy@gmail.com
> 
> pidfd_get_task() always resolves pidfds using PIDTYPE_TGID (kernel/pid.c
> line 640), regardless of whether the pidfd was created with PIDFD_THREAD.
> This means:
> 
>  - A PIDFD_THREAD pidfd for a non-leader thread fails with ESRCH.
>  - A regular pidfd for a process whose leader has exited (pthread_exit
>    in main, secondary thread still alive) also fails with ESRCH.
> 
> This is not specific to my patch: process_madvise() uses pidfd_get_task()
> in the same way and has the same behavior. I wrote a test program
> confirming this:
> 
>   https://github.com/alban/tests/tree/alban_pvm_flags/pvm_flags/pidfd_thread_test
> 
> Results summary:
> 
>   All threads alive:
>     pidfd_open(pid, 0)              + process_vm_readv: OK
>     pidfd_open(tid, PIDFD_THREAD)   + process_vm_readv: OK (leader tid)
>     pidfd_open(tid, PIDFD_THREAD)   + process_vm_readv: ESRCH (non-leader)
> 
>   Leader thread exited (secondary still alive):
>     pidfd_open(pid, 0)              + process_vm_readv: ESRCH
>     pidfd_open(pid, PIDFD_THREAD)   + process_vm_readv: ESRCH
>     pidfd_open(tid, PIDFD_THREAD)   + process_vm_readv: ESRCH (non-leader)
>     process_vm_readv(tid, flags=0)                    : OK (plain TID path)
> 
>   process_madvise() behaves identically in all cases above.
> 
> For the non-leader thread case when all threads are alive, this is fine in
> practice: all threads share the same mm_struct, so profilers just use a regular
> pidfd for the thread-group leader.

This was an intentional limitation back then because pidfds only came in
thread-group flavor. I only added subthread pidfds much later.
pidfd_get_task() should drop the flags argument btw. I think that's
unused.

> However, the exited-leader case is a real limitation for profilers.
> OpenTelemetry eBPF Profiler wants to profile a process where the main thread
> has exited but secondary threads are still running [2].
> [2] https://github.com/open-telemetry/opentelemetry-ebpf-profiler/pull/376

If the thread-group leader exists before all of it's subthreads exit
then this is a broken program - even if it is a legal state. The
thread-group leader cannot be reaped while there are live subthreads and
it also means that any subthread exec "resurrects" the thread-group
leader struct pid. So that's going to make for fun profiling...

> Using plain TIDs (flags=0) would work, but it means users cannot use
> PROCESS_VM_PIDFD in this scenario.
> 
> What do you think this patch should do? I see two options:
>  - Address this limitation in a separate future patch that fixes
>    pidfd_get_task() to use PIDTYPE_PID when PIDFD_THREAD is detected in
>    f_flags, benefiting all callers (process_vm_readv, process_madvise,
>    and any future users).

As long as all users of the interface are fine with operating on
subthreads this should be perfectly fine.


^ permalink raw reply	[flat|nested] 9+ messages in thread

* Re: [PATCH v5 1/2] mm/process_vm_access: pidfd and nowait support for process_vm_readv/writev
  2026-06-04 12:49       ` Christian Brauner
@ 2026-06-05  9:31         ` David Hildenbrand (Arm)
  0 siblings, 0 replies; 9+ messages in thread
From: David Hildenbrand (Arm) @ 2026-06-05  9:31 UTC (permalink / raw)
  To: Christian Brauner
  Cc: Alban Crequy, Andrew Morton, Lorenzo Stoakes, Liam R . Howlett,
	Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
	linux-kernel, linux-mm, Alban Crequy, Peter Xu, Willy Tarreau,
	linux-kselftest, shuah, Usama Arif, David Laight

On 6/4/26 14:49, Christian Brauner wrote:
> On 2026-06-03 15:54 +0200, David Hildenbrand (Arm) wrote:
>>> However, the exited-leader case is a real limitation for profilers.
>>> OpenTelemetry eBPF Profiler wants to profile a process where the main thread
>>> has exited but secondary threads are still running [2].
>>> [2] https://github.com/open-telemetry/opentelemetry-ebpf-profiler/pull/376
>>>
>>> Using plain TIDs (flags=0) would work, but it means users cannot use
>>> PROCESS_VM_PIDFD in this scenario.
>>>
>>> What do you think this patch should do? I see two options:
>>>  - Address this limitation in a separate future patch that fixes
>>>    pidfd_get_task() to use PIDTYPE_PID when PIDFD_THREAD is detected in
>>>    f_flags, benefiting all callers (process_vm_readv, process_madvise,
>>>    and any future users).
>>
>> Extending it consistently for that scenario seems like the right thing to do?
>> @Christian?
> 
> Yes, but I cannot vouch whether the mm operations deal correctively with
> subthreads.
> 

I'd think so, but I could give it a detailed look when reviewing a patch :)

-- 
Cheers,

David

^ permalink raw reply	[flat|nested] 9+ messages in thread

end of thread, other threads:[~2026-06-05  9:31 UTC | newest]

Thread overview: 9+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-06-02 10:09 [PATCH v5 0/2] mm/process_vm_access: pidfd and nowait support for process_vm_readv/writev Alban Crequy
2026-06-02 10:09 ` [PATCH v5 1/2] " Alban Crequy
2026-06-02 12:16   ` David Hildenbrand (Arm)
2026-06-03  8:27   ` Alban Crequy
2026-06-03 13:54     ` David Hildenbrand (Arm)
2026-06-04 12:49       ` Christian Brauner
2026-06-05  9:31         ` David Hildenbrand (Arm)
2026-06-04 12:59     ` Christian Brauner
2026-06-02 10:09 ` [PATCH v5 2/2] selftests/mm: add tests for process_vm_readv flags Alban Crequy

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®