The process_vm_readv/writev Syscalls: Reading Another Process's Memory Without ptrace

2026-07-16

Before Linux 3.2, if you wanted to read memory from another process, you had exactly two options: attach with ptrace (which stops the target and requires a word-at-a-time PEEKDATA loop) or read /proc/PID/mem (which requires a seek + read per region and traps into the VFS layer). Both are miserable for anything larger than a few kilobytes.

process_vm_readv(2) and process_vm_writev(2) fix this. They take a target PID, an array of local iovecs, and an array of remote iovecs, and perform a bulk cross-address-space copy in a single syscall — without stopping the target and without touching the page cache.

ssize_t process_vm_readv(pid_t pid,
    const struct iovec *local_iov,  unsigned long liovcnt,
    const struct iovec *remote_iov, unsigned long riovcnt,
    unsigned long flags);

Internally, the kernel calls get_user_pages_remote() on the target's mm, pins the pages, memcpys directly, then releases them. No signal, no stop, no scheduling perturbation on the target. The target keeps running throughout — reads see whatever value happens to be there when the copy hits that page.

Permissions: The caller must have PTRACE_MODE_ATTACH_REALCREDS access to the target — same rules as ptrace. Same UID, or CAP_SYS_PTRACE, and Yama's ptrace_scope still applies. This is not a security bypass; it's an ergonomics upgrade.

Real-world example: Google's gperftools heap profiler and criu (Checkpoint/Restore in Userspace) both use process_vm_readv to snapshot a target's heap. CRIU's dump phase reads gigabytes of anonymous memory this way at roughly memcpy speed — a ptrace(PTRACE_PEEKDATA) loop would take hours per GB (one syscall per 8 bytes = ~125M syscalls/GB). GDB uses it as of 7.7 for the same reason: attaching to a 32GB process and doing info proc mappings + x/ reads is now bounded by memory bandwidth, not syscall overhead.

Rule of thumb: One process_vm_readv call caps out around UIO_MAXIOV (1024) iovecs per side, and the kernel limits each transfer to SSIZE_MAX. For bulk copies, batch into ~64KB chunks per iovec — that amortizes the get_user_pages walk and stays under the pinned-memory limit. Expect ~5-10 GB/s on modern hardware, vs. ~50 MB/s for a PEEKDATA loop. That's a ~100× improvement, and the target never blocks.

Caveat: The read is not atomic across pages. If the target is writing while you're reading a struct that spans a page boundary, you can see torn state. For consistency, either freeze the target (SIGSTOP), use CRIU's seize protocol, or read only immutable regions.

See it in action: Check out Linux Process Injection with Ptrace Part 1 - Emmanuel Adewale by RITSEC to see this theory applied.
Key Takeaway: process_vm_readv/writev gives you memcpy-speed cross-process memory access with ptrace-equivalent permissions and zero target perturbation — the right tool whenever you'd otherwise loop over PTRACE_PEEKDATA or seek through /proc/PID/mem.

All newsletters