diff --git a/src/syscall/abi.h b/src/syscall/abi.h index 2c27320..71b02e6 100644 --- a/src/syscall/abi.h +++ b/src/syscall/abi.h @@ -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 */ diff --git a/src/syscall/dispatch.tbl b/src/syscall/dispatch.tbl index 7962dfa..d5dd842 100644 --- a/src/syscall/dispatch.tbl +++ b/src/syscall/dispatch.tbl @@ -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 diff --git a/src/syscall/io.c b/src/syscall/io.c index cfdcc05..b5edf28 100644 --- a/src/syscall/io.c +++ b/src/syscall/io.c @@ -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()) + 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) diff --git a/src/syscall/io.h b/src/syscall/io.h index 20198b8..12a279d 100644 --- a/src/syscall/io.h +++ b/src/syscall/io.h @@ -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); diff --git a/src/syscall/syscall.c b/src/syscall/syscall.c index bac5168..8567f2a 100644 --- a/src/syscall/syscall.c +++ b/src/syscall/syscall.c @@ -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)) diff --git a/tests/test-matrix.sh b/tests/test-matrix.sh index e4a937d..65f35bc 100755 --- a/tests/test-matrix.sh +++ b/tests/test-matrix.sh @@ -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" diff --git a/tests/test-process-vm.c b/tests/test-process-vm.c new file mode 100644 index 0000000..d6e9ea6 --- /dev/null +++ b/tests/test-process-vm.c @@ -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 +#include +#include +#include +#include + +#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; +}