Linux process_vm_readv(): Remote Bytes, Partial Transfers, and Permission Boundaries
Read a cooperating child with Linux process_vm_readv(), handle page-level short transfers, understand ptrace checks, and avoid treating reads as snapshots.
process_vm_readv() copies bytes from a process identified by PID into the caller’s address space. It is useful for cooperating diagnostics, debuggers, and test harnesses, but its contract is narrower than “read a remote object.” It moves ranges; it does not establish an application-level snapshot, discover safe addresses, freeze the target, or bypass Linux’s process-access checks.
The safest example is a child that the caller created specifically for the test. The child publishes one address over a pipe, keeps the buffer alive, and waits while its parent copies a bounded string. This avoids reading an arbitrary PID and gives the target a clear lifetime. It still tests a Linux permission path and must be run on Linux; macOS does not provide this Linux system call.
Two iovec arrays describe two ordered byte streams
The interface is declared in <sys/uio.h>; with glibc, define _GNU_SOURCE before including headers. local_iov and liovcnt describe writable buffers in the caller. remote_iov and riovcnt describe source ranges in the target process. The kernel walks each array in order and transfers bytes from the remote stream into the local stream. The array boundaries need not correspond one-for-one, but the caller must verify exactly how many bytes were returned.
ssize_t n = process_vm_readv(pid, local_iov, local_count,
remote_iov, remote_count, 0);
if (n == -1) {
/* The call failed; errno explains why. */
} else if ((size_t)n != expected_bytes) {
/* The call succeeded with only a prefix of the requested data. */
}
flags must currently be zero. A return of -1 is an error and sets errno; a nonnegative return is a byte count and can be short. Do not use errno to interpret a short positive transfer. The current Linux man-page describes partial transfers at remote-page granularity: if a remote iovec spans several pages and a later page is inaccessible or not resident, earlier pages can already have been copied, even splitting that one iovec element.
For data whose length or mapping is uncertain, make remote segments no larger than the page range you have independently validated. Splitting at page boundaries makes a fault easier to isolate; it does not make addresses stable or make a short read impossible. Check vector counts against IOV_MAX, keep the total requested size within the syscall’s documented range, and treat every short result as partial progress, not as a complete structure.
Permission is a ptrace access decision
For another process, the documented check is PTRACE_MODE_ATTACH_REALCREDS. That is an access-control mode, not a statement that the caller has attached with ptrace(). The check considers the caller’s real credentials, target credentials and dumpability, capabilities in the target’s user namespace, and active Linux Security Module policy. On systems with Yama, ptrace_scope can add a relationship restriction. A direct parent reading its child is a useful test arrangement, but matching user IDs or a parent relationship should not be generalized into “same user can read every process.”
CAP_SYS_PTRACE can satisfy relevant capability checks in the target’s user namespace; it does not turn the interface into a safe way to inspect arbitrary production processes. Container boundaries, user namespaces, dumpability, SELinux/AppArmor or another LSM, and Yama policy can still affect an attempted access. A denied call is a normal outcome to diagnose, not a reason to add broad privilege to a service.
The kernel implementation makes this boundary visible: upstream process_vm_access.c looks up the target task, calls mm_access() with PTRACE_MODE_ATTACH_REALCREDS, then walks the remote iovec entries while pinning and copying pages. The man-page contract remains the portable guide to return values and errors; implementation details can change and should be checked against the kernel you deploy.
A cooperating-child fixture
This Linux-only program forks one disposable child. The child reports the address of a static test string, then blocks on a pipe so the parent can read while the mapping remains alive. The PID comes directly from fork(); the program never accepts an external PID. It copies only the fixed string and releases and reaps the child even if the remote read fails.
Save as process-vm-child-test.c and run only on a disposable Linux test system:
cc -Wall -Wextra -Wpedantic -O2 -o process-vm-child-test process-vm-child-test.c
./process-vm-child-test
#define _GNU_SOURCE
#include <errno.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <sys/wait.h>
#include <unistd.h>
static const char payload[] = "cooperating child only";
static int
write_all(int fd, const void *buffer, size_t length)
{
const char *p = buffer;
while (length > 0) {
ssize_t n = write(fd, p, length);
if (n < 0 && errno == EINTR)
continue;
if (n <= 0)
return -1;
p += (size_t)n;
length -= (size_t)n;
}
return 0;
}
static int
read_all(int fd, void *buffer, size_t length)
{
char *p = buffer;
while (length > 0) {
ssize_t n = read(fd, p, length);
if (n < 0 && errno == EINTR)
continue;
if (n <= 0)
return -1;
p += (size_t)n;
length -= (size_t)n;
}
return 0;
}
int
main(void)
{
int ready[2], release[2];
pid_t child;
int result = 1;
uintptr_t remote_address = 0;
char received[sizeof(payload)] = {0};
if (pipe(ready) == -1 || pipe(release) == -1) {
perror("pipe");
return 1;
}
child = fork();
if (child == -1) {
perror("fork");
close(ready[0]); close(ready[1]);
close(release[0]); close(release[1]);
return 1;
}
if (child == 0) {
char token;
ssize_t n;
int ok;
close(ready[0]);
close(release[1]);
remote_address = (uintptr_t)payload;
ok = write_all(ready[1], &remote_address, sizeof(remote_address));
close(ready[1]);
do {
n = read(release[0], &token, sizeof(token));
} while (n < 0 && errno == EINTR);
close(release[0]);
_exit(ok == 0 && n == 1 ? 0 : 1);
}
close(ready[1]);
close(release[0]);
if (read_all(ready[0], &remote_address, sizeof(remote_address)) == -1) {
perror("read child address");
} else {
struct iovec local = { .iov_base = received,
.iov_len = sizeof(received) };
struct iovec remote = { .iov_base = (void *)remote_address,
.iov_len = sizeof(payload) };
ssize_t n = process_vm_readv(child, &local, 1, &remote, 1, 0);
if (n == -1) {
perror("process_vm_readv");
} else if ((size_t)n != sizeof(payload)) {
fprintf(stderr, "short read: %zd of %zu bytes\n",
n, sizeof(payload));
} else if (memcmp(received, payload, sizeof(payload)) != 0) {
fprintf(stderr, "copied bytes differ from the expected fixture\n");
} else {
printf("copied %zd bytes: %s\n", n, received);
result = 0;
}
}
close(ready[0]);
{
char token = 1;
if (write_all(release[1], &token, sizeof(token)) == -1) {
perror("release child");
result = 1;
}
}
close(release[1]);
{
int status;
if (waitpid(child, &status, 0) == -1) {
perror("waitpid");
result = 1;
} else if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
fprintf(stderr, "test child did not exit cleanly\n");
result = 1;
}
}
return result;
}
The parent-child pipe is part of the test protocol, not a feature supplied by process_vm_readv(). The child waits after publishing the address, so this fixture avoids a lifetime race. In a real diagnostic tool, a remote pointer can become stale between discovering it and copying it; mappings may be unmapped or changed, and the target may update bytes while the copy proceeds. process_vm_readv() does not promise a consistent application-level snapshot. If a coherent view matters, arrange target cooperation, such as a request/acknowledgment protocol that holds the data stable, rather than assuming one syscall freezes memory.
Interpret results and errors narrowly
The syscall reports bytes transferred, not whether the entire logical record is valid. On a positive short read, preserve the copied prefix and decide whether the application protocol permits retrying the remainder. Retrying blindly can read a different object if the target changed the mapping or contents after the first call. For record-oriented diagnostics, version or checksum the payload and coordinate a stable publication protocol with the child.
On -1, inspect errno: EPERM means the caller lacks permission to access the target address space, and ESRCH means no process with that PID exists. The documented EINVAL cases are liovcnt or riovcnt exceeding IOV_MAX, the sum of either vector array’s iov_len values overflowing ssize_t, or flags being nonzero. EFAULT covers inaccessible memory described by local_iov or a remote range outside the target’s accessible address space. If a remote element crosses pages and a later page fails, the documented page-granular behavior can instead return the bytes copied before that page. Retain the requested length, returned count, PID provenance, and error separately in diagnostics.
Use this interface only where the target and caller have an explicit trust relationship. It is not a remote object protocol, not a memory snapshot API, and not a permission bypass. Use an IPC channel designed for data exchange when the other process can cooperate; reserve remote-memory reads for tightly bounded diagnostic cases with verified identity, address lifetime, and access policy.
Related:
- Linux pidfds: Race-Free Process Handles Beyond Numeric PIDs
- Linux process_madvise(): Advising the Kernel About Another Process’s Memory
Sources: