Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/syscall/abi.h
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,9 @@
/* network batch I/O */
#define SYS_recvmmsg 243
#define SYS_sendmmsg 269
/* cross-process memory */
#define SYS_process_vm_readv 270
#define SYS_process_vm_writev 271
/* file advisory */
#define SYS_fadvise64 223
/* vectored positioned I/O */
Expand Down
2 changes: 2 additions & 0 deletions src/syscall/dispatch.tbl
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ SYS_ptrace sc_ptrace 1
SYS_pidfd_open sc_pidfd_open 1
SYS_pidfd_send_signal sc_pidfd_send_signal 1
SYS_pidfd_getfd sc_pidfd_getfd 1
SYS_process_vm_readv sc_process_vm_readv 1
SYS_process_vm_writev sc_process_vm_writev 1

# Identity and scheduling
SYS_getpid sc_getpid 0
Expand Down
159 changes: 159 additions & 0 deletions src/syscall/io.c
Original file line number Diff line number Diff line change
Expand Up @@ -1730,6 +1730,165 @@ int64_t sys_pwritev2(guest_t *g,
return r;
}

static int64_t process_vm_import_iov(guest_t *g,
uint64_t iov_gva,
uint64_t iovcnt,
linux_iovec_t **iov_out)
{
*iov_out = NULL;

if (iovcnt > SYSCALL_IOV_MAX)
return -LINUX_EINVAL;
if (iovcnt == 0)
return 0;
if (iovcnt > SIZE_MAX / sizeof(linux_iovec_t))
return -LINUX_EINVAL;

size_t bytes = (size_t) iovcnt * sizeof(linux_iovec_t);
linux_iovec_t *iov = malloc(bytes);
if (!iov)
return -LINUX_ENOMEM;
if (guest_read(g, iov_gva, iov, bytes) < 0) {
free(iov);
return -LINUX_EFAULT;
}

uint64_t total = 0;
for (uint64_t i = 0; i < iovcnt; i++) {
if (iov[i].iov_len > (uint64_t) SSIZE_MAX ||
total > (uint64_t) SSIZE_MAX - iov[i].iov_len) {
free(iov);
return -LINUX_EINVAL;
}
total += iov[i].iov_len;
}

*iov_out = iov;
return 0;
}

static void process_vm_advance_iov(linux_iovec_t *iov,
uint64_t iovcnt,
uint64_t *idx,
uint64_t *off)
{
while (*idx < iovcnt && *off >= iov[*idx].iov_len) {
*off = 0;
(*idx)++;
}
}

static int64_t process_vm_copy(guest_t *g,
linux_iovec_t *local_iov,
uint64_t local_iovcnt,
linux_iovec_t *remote_iov,
uint64_t remote_iovcnt,
bool write_remote)
{
uint64_t li = 0, ri = 0, lo = 0, ro = 0;
uint64_t copied = 0;

for (;;) {
process_vm_advance_iov(local_iov, local_iovcnt, &li, &lo);
process_vm_advance_iov(remote_iov, remote_iovcnt, &ri, &ro);
if (li >= local_iovcnt || ri >= remote_iovcnt)
return (int64_t) copied;

uint64_t local_left = local_iov[li].iov_len - lo;
uint64_t remote_left = remote_iov[ri].iov_len - ro;
uint64_t len = local_left < remote_left ? local_left : remote_left;
if (len == 0)
continue;

if (local_iov[li].iov_base > UINT64_MAX - lo ||
remote_iov[ri].iov_base > UINT64_MAX - ro)
return copied > 0 ? (int64_t) copied : -LINUX_EFAULT;

uint64_t src_gva = write_remote ? local_iov[li].iov_base + lo
: remote_iov[ri].iov_base + ro;
uint64_t dst_gva = write_remote ? remote_iov[ri].iov_base + ro
: local_iov[li].iov_base + lo;

uint64_t src_avail = 0, dst_avail = 0;
void *src = guest_ptr_bound(g, src_gva, &src_avail, MEM_PERM_R, len);
void *dst = guest_ptr_bound(g, dst_gva, &dst_avail, MEM_PERM_W, len);
if (!src || !dst)
return copied > 0 ? (int64_t) copied : -LINUX_EFAULT;

uint64_t chunk = len;
if (chunk > src_avail)
chunk = src_avail;
if (chunk > dst_avail)
chunk = dst_avail;
if (chunk == 0)
return copied > 0 ? (int64_t) copied : -LINUX_EFAULT;

memmove(dst, src, (size_t) chunk);
copied += chunk;
lo += chunk;
ro += chunk;
}
}

static int64_t sys_process_vm(guest_t *g,
int64_t pid,
uint64_t local_iov_gva,
uint64_t local_iovcnt,
uint64_t remote_iov_gva,
uint64_t remote_iovcnt,
uint64_t flags,
bool write_remote)
{
if (flags != 0)
return -LINUX_EINVAL;
if (pid <= 0 || pid != proc_get_pid())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: PID values with nonzero upper register bits incorrectly return ESRCH instead of addressing the low-32-bit pid_t. Narrow pid before validation, matching the syscall ABI and other PID handlers.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/syscall/io.c, line 1844:

<comment>PID values with nonzero upper register bits incorrectly return ESRCH instead of addressing the low-32-bit `pid_t`. Narrow `pid` before validation, matching the syscall ABI and other PID handlers.</comment>

<file context>
@@ -1730,6 +1730,165 @@ int64_t sys_pwritev2(guest_t *g,
+{
+    if (flags != 0)
+        return -LINUX_EINVAL;
+    if (pid <= 0 || pid != proc_get_pid())
+        return -LINUX_ESRCH;
+    if (local_iovcnt == 0 || remote_iovcnt == 0)
</file context>
Suggested change
if (pid <= 0 || pid != proc_get_pid())
if ((int) pid <= 0 || (int) pid != proc_get_pid())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The PID gate is stricter than the rest of the runtime’s process identity handling and rejects same-process aliases outright. That can turn a same-process memory copy into ESRCH even though the new feature is intended to work within the current guest process.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/syscall/io.c, line 1844:

<comment>The PID gate is stricter than the rest of the runtime’s process identity handling and rejects same-process aliases outright. That can turn a same-process memory copy into `ESRCH` even though the new feature is intended to work within the current guest process.</comment>

<file context>
@@ -1730,6 +1730,165 @@ int64_t sys_pwritev2(guest_t *g,
+{
+    if (flags != 0)
+        return -LINUX_EINVAL;
+    if (pid <= 0 || pid != proc_get_pid())
+        return -LINUX_ESRCH;
+    if (local_iovcnt == 0 || remote_iovcnt == 0)
</file context>

return -LINUX_ESRCH;
if (local_iovcnt == 0 || remote_iovcnt == 0)
return 0;

linux_iovec_t *local_iov = NULL;
linux_iovec_t *remote_iov = NULL;
int64_t err =
process_vm_import_iov(g, local_iov_gva, local_iovcnt, &local_iov);
if (err < 0)
return err;
err = process_vm_import_iov(g, remote_iov_gva, remote_iovcnt, &remote_iov);
if (err < 0) {
free(local_iov);
return err;
}

int64_t ret = process_vm_copy(g, local_iov, local_iovcnt, remote_iov,
remote_iovcnt, write_remote);
free(remote_iov);
free(local_iov);
return ret;
}

int64_t sys_process_vm_readv(guest_t *g,
int64_t pid,
uint64_t local_iov_gva,
uint64_t local_iovcnt,
uint64_t remote_iov_gva,
uint64_t remote_iovcnt,
uint64_t flags)
{
return sys_process_vm(g, pid, local_iov_gva, local_iovcnt, remote_iov_gva,
remote_iovcnt, flags, false);
}

int64_t sys_process_vm_writev(guest_t *g,
int64_t pid,
uint64_t local_iov_gva,
uint64_t local_iovcnt,
uint64_t remote_iov_gva,
uint64_t remote_iovcnt,
uint64_t flags)
{
return sys_process_vm(g, pid, local_iov_gva, local_iovcnt, remote_iov_gva,
remote_iovcnt, flags, true);
}

/* terminal I/O. */

int64_t sys_ioctl(guest_t *g, int fd, uint64_t request, uint64_t arg)
Expand Down
14 changes: 14 additions & 0 deletions src/syscall/io.h
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,20 @@ int64_t sys_pwritev2(guest_t *g,
int iovcnt,
int64_t offset,
int flags);
int64_t sys_process_vm_readv(guest_t *g,
int64_t pid,
uint64_t local_iov_gva,
uint64_t local_iovcnt,
uint64_t remote_iov_gva,
uint64_t remote_iovcnt,
uint64_t flags);
int64_t sys_process_vm_writev(guest_t *g,
int64_t pid,
uint64_t local_iov_gva,
uint64_t local_iovcnt,
uint64_t remote_iov_gva,
uint64_t remote_iovcnt,
uint64_t flags);

/* terminal I/O */
int64_t sys_ioctl(guest_t *g, int fd, uint64_t request, uint64_t arg);
Expand Down
2 changes: 2 additions & 0 deletions src/syscall/syscall.c
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,8 @@ SC_FORWARD(sc_pwritev, sys_pwritev(g, (int) x0, x1, (int) x2, (int64_t) x3))
*/
SC_FORWARD(sc_preadv2, sys_preadv2(g, (int) x0, x1, (int) x2, (int64_t) x3, (int) x5))
SC_FORWARD(sc_pwritev2, sys_pwritev2(g, (int) x0, x1, (int) x2, (int64_t) x3, (int) x5))
SC_FORWARD(sc_process_vm_readv, sys_process_vm_readv(g, (int64_t) x0, x1, x2, x3, x4, x5))
SC_FORWARD(sc_process_vm_writev, sys_process_vm_writev(g, (int64_t) x0, x1, x2, x3, x4, x5))
SC_FORWARD(sc_sendfile, sys_sendfile(g, (int) x0, (int) x1, x2, x3))
SC_FORWARD(sc_splice, sys_splice(g, (int) x0, x1, (int) x2, x3, (size_t) x4, (unsigned int) x5))
SC_FORWARD(sc_vmsplice, sys_vmsplice(g, (int) x0, x1, (unsigned long) x2, (unsigned int) x3))
Expand Down
1 change: 1 addition & 0 deletions tests/test-matrix.sh
Original file line number Diff line number Diff line change
Expand Up @@ -629,6 +629,7 @@ run_unit_tests()
test_rc "$runner" "test-ofd-lock" 0 "$bindir/test-ofd-lock"
test_rc "$runner" "test-times" 0 "$bindir/test-times"
test_rc "$runner" "test-syscall-smoke" 0 "$bindir/test-syscall-smoke"
test_rc "$runner" "test-process-vm" 0 "$bindir/test-process-vm"
test_rc "$runner" "test-vdso" 0 "$bindir/test-vdso"

printf "\nI/O subsystem\n"
Expand Down
170 changes: 170 additions & 0 deletions tests/test-process-vm.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/*
* process_vm_readv/process_vm_writev tests
*
* Copyright 2026 elfuse contributors
* SPDX-License-Identifier: Apache-2.0
*
* Syscalls exercised: process_vm_readv(270), process_vm_writev(271)
*/

#include <errno.h>
#include <stdint.h>
#include <string.h>
#include <sys/uio.h>
#include <unistd.h>

#include "test-harness.h"
#include "raw-syscall.h"

int passes = 0, fails = 0;

#ifndef SYS_process_vm_readv
#define SYS_process_vm_readv 270
#endif

#ifndef SYS_process_vm_writev
#define SYS_process_vm_writev 271
#endif

static long raw_process_vm_readv(pid_t pid,
const struct iovec *local_iov,
unsigned long local_iovcnt,
const struct iovec *remote_iov,
unsigned long remote_iovcnt,
unsigned long flags)
{
return raw_syscall6(SYS_process_vm_readv, (long) pid, (long) local_iov,
(long) local_iovcnt, (long) remote_iov,
(long) remote_iovcnt, (long) flags);
}

static long raw_process_vm_writev(pid_t pid,
const struct iovec *local_iov,
unsigned long local_iovcnt,
const struct iovec *remote_iov,
unsigned long remote_iovcnt,
unsigned long flags)
{
return raw_syscall6(SYS_process_vm_writev, (long) pid, (long) local_iov,
(long) local_iovcnt, (long) remote_iov,
(long) remote_iovcnt, (long) flags);
}

static void test_readv_scatter(void)
{
TEST("process_vm_readv scatter");

char remote[] = "hello-world";
char a[6] = {0};
char b[6] = {0};
struct iovec local[2] = {
{.iov_base = a, .iov_len = 5},
{.iov_base = b, .iov_len = 5},
};
struct iovec remote_iov[2] = {
{.iov_base = remote, .iov_len = 5},
{.iov_base = remote + 6, .iov_len = 5},
};

long n = raw_process_vm_readv(getpid(), local, 2, remote_iov, 2, 0);
EXPECT_TRUE(n == 10 && !strcmp(a, "hello") && !strcmp(b, "world"),
"scatter read failed");
}

static void test_writev_scatter(void)
{
TEST("process_vm_writev scatter");

char src0[] = "ab";
char src1[] = "cd";
char remote[] = "....";
struct iovec local[2] = {
{.iov_base = src0, .iov_len = 2},
{.iov_base = src1, .iov_len = 2},
};
struct iovec remote_iov[2] = {
{.iov_base = remote, .iov_len = 2},
{.iov_base = remote + 2, .iov_len = 2},
};

long n = raw_process_vm_writev(getpid(), local, 2, remote_iov, 2, 0);
EXPECT_TRUE(n == 4 && !memcmp(remote, "abcd", 4), "scatter write failed");
}

static void test_short_copy_on_remote_fault(void)
{
TEST("process_vm_readv short remote fault");

char remote[] = "abcd";
char dst[8] = {0};
struct iovec local = {.iov_base = dst, .iov_len = sizeof(dst)};
struct iovec remote_iov[2] = {
{.iov_base = remote, .iov_len = 4},
{.iov_base = (void *) 1, .iov_len = 4},
};

long n = raw_process_vm_readv(getpid(), &local, 1, remote_iov, 2, 0);
EXPECT_TRUE(n == 4 && !memcmp(dst, "abcd", 4), "short read fault");
}

static void test_short_copy_on_local_fault(void)
{
TEST("process_vm_writev short local fault");

char src[] = "wxyz";
char remote[8] = {0};
struct iovec local[2] = {
{.iov_base = src, .iov_len = 4},
{.iov_base = (void *) 1, .iov_len = 4},
};
struct iovec remote_iov = {.iov_base = remote, .iov_len = sizeof(remote)};

long n = raw_process_vm_writev(getpid(), local, 2, &remote_iov, 1, 0);
EXPECT_TRUE(n == 4 && !memcmp(remote, "wxyz", 4), "short write fault");
}

static void test_error_paths(void)
{
char src[] = "x";
char dst[] = ".";
struct iovec src_iov = {.iov_base = src, .iov_len = 1};
struct iovec dst_iov = {.iov_base = dst, .iov_len = 1};

TEST("process_vm flags EINVAL");
EXPECT_RAW_ERRNO(
raw_process_vm_readv(getpid(), &dst_iov, 1, &src_iov, 1, 1), -EINVAL,
"flags accepted");

TEST("process_vm foreign pid ESRCH");
EXPECT_RAW_ERRNO(
raw_process_vm_writev(getpid() + 99999, &src_iov, 1, &dst_iov, 1, 0),
-ESRCH, "foreign pid accepted");

TEST("process_vm bad iov pointer EFAULT");
EXPECT_RAW_ERRNO(raw_process_vm_readv(getpid(), (const struct iovec *) 1, 1,
&src_iov, 1, 0),
-EFAULT, "bad local iov accepted");

TEST("process_vm iovcnt EINVAL");
EXPECT_RAW_ERRNO(
raw_process_vm_readv(getpid(), &dst_iov, 1025, &src_iov, 1, 0), -EINVAL,
"oversized iovcnt accepted");

TEST("process_vm zero count");
EXPECT_EQ(raw_process_vm_readv(getpid(), NULL, 0, NULL, 0, 0), 0,
"zero count failed");
}

int main(void)
{
printf("test-process-vm: process_vm_readv/writev tests\n\n");

test_readv_scatter();
test_writev_scatter();
test_short_copy_on_remote_fault();
test_short_copy_on_local_fault();
test_error_paths();

SUMMARY("test-process-vm");
return fails > 0 ? 1 : 0;
}
Loading