From f6ea32d7eb22f5e9dca7a6d4e7d507bdbc3a2ff7 Mon Sep 17 00:00:00 2001 From: Greg Burd Date: Tue, 14 Jul 2026 09:13:48 -0400 Subject: [PATCH] vfs: reject oversized iovcnt in sys_read/sys_write instead of panicking sys_read()/sys_write() summed iov_len over niov entries and only then asserted niov <= UIO_MAXIOV before a VLA copy. niov is caller-controlled and reaches here unvalidated from readv/writev/preadv/pwritev/preadv2/pwritev2, io_uring READV/WRITEV(_FIXED), and libaio PREADV/PWRITEV. Two problems: - OSv keeps assert() live in _KERNEL builds (include/api/assert.h), so niov > UIO_MAXIOV -> __assert_fail -> abort() = whole-VM panic. Any unprivileged app crashes the kernel with one readv(fd, iov, 0x40000000). (In a unikernel, an app-triggerable panic is a full-system DoS.) - The summation loop dereferences iov[i] for up to niov entries *before* the assert -> out-of-bounds read walk. - struct iovec copy_iov[niov] is a VLA sized by the unvalidated count. Reject niov > UIO_MAXIOV with EINVAL at function entry (before touching iov), which fixes all three and matches Linux's EINVAL for iovcnt out of range. Added tst-iovcnt-guard: readv/writev with a huge iovcnt now return -1/EINVAL instead of aborting. --- fs/vfs/vfs_syscalls.cc | 14 +++++++++++++ modules/tests/Makefile | 2 +- tests/tst-iovcnt-guard.cc | 44 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 tests/tst-iovcnt-guard.cc diff --git a/fs/vfs/vfs_syscalls.cc b/fs/vfs/vfs_syscalls.cc index 1a43aabfc..d51ab9584 100644 --- a/fs/vfs/vfs_syscalls.cc +++ b/fs/vfs/vfs_syscalls.cc @@ -250,6 +250,16 @@ sys_read(struct file *fp, const struct iovec *iov, size_t niov, if ((fp->f_flags & FREAD) == 0) return EBADF; + // Reject an out-of-range iovec count with EINVAL *before* touching iov: a + // caller-controlled niov > UIO_MAXIOV would otherwise (a) walk iov[] for + // up to niov entries in the summation loop below (OOB read) and (b) hit + // the assert() further down, which in a _KERNEL build is compiled in and + // calls abort() -> whole-VM panic. It also caps the VLA below to a sane + // size. (niov is size_t; guard both the >MAXIOV and, defensively, the + // count itself.) + if (niov > UIO_MAXIOV) + return EINVAL; + size_t bytes = 0; auto iovp = iov; for (unsigned i = 0; i < niov; i++) { @@ -289,6 +299,10 @@ sys_write(struct file *fp, const struct iovec *iov, size_t niov, if ((fp->f_flags & FWRITE) == 0) return EBADF; + // See sys_read: reject niov > UIO_MAXIOV up front (OOB-read + abort() DoS). + if (niov > UIO_MAXIOV) + return EINVAL; + size_t bytes = 0; auto iovp = iov; for (unsigned i = 0; i < niov; i++) { diff --git a/modules/tests/Makefile b/modules/tests/Makefile index 1eb3df46e..804652e54 100644 --- a/modules/tests/Makefile +++ b/modules/tests/Makefile @@ -115,7 +115,7 @@ ext-only-tests := tst-readdir.so tst-concurrent-read.so tst-fs-link.so specific-fs-tests := $($(fs_type)-only-tests) -tests := tst-pthread.so misc-ramdisk.so tst-vblk.so tst-mq-smoke.so tst-bsd-evh.so \ +tests := tst-iovcnt-guard.so tst-pthread.so misc-ramdisk.so tst-vblk.so tst-mq-smoke.so tst-bsd-evh.so \ misc-bsd-callout.so tst-bsd-kthread.so tst-bsd-taskqueue.so \ tst-fpu.so tst-preempt.so tst-tracepoint.so tst-hub.so \ misc-console.so misc-leak.so misc-readbench.so misc-mmap-anon-perf.so \ diff --git a/tests/tst-iovcnt-guard.cc b/tests/tst-iovcnt-guard.cc new file mode 100644 index 000000000..1c40084c6 --- /dev/null +++ b/tests/tst-iovcnt-guard.cc @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2026 Greg Burd + * + * This work is open source software, licensed under the terms of the + * BSD license as described in the LICENSE file in the top-level directory. + */ + +// Regression: a huge iovcnt to readv/writev must return -1/EINVAL, not panic +// the kernel (previously an assert(niov <= UIO_MAXIOV) -> abort()). +#include +#include +#include +#include +#include +#include + +int main() +{ + int fd = open("/tmp/iovcnt-test", O_RDWR | O_CREAT | O_TRUNC, 0644); + if (fd < 0) { perror("open"); return 1; } + // A normal small write to give the file content. + struct iovec one = { (void*)"hi", 2 }; + if (writev(fd, &one, 1) != 2) { perror("writev(1)"); return 1; } + lseek(fd, 0, SEEK_SET); + + // Oversized iovcnt: must be rejected with EINVAL, not abort the VM. + struct iovec v = { 0, 0 }; + errno = 0; + ssize_t r = readv(fd, &v, 0x40000000); + if (!(r == -1 && errno == EINVAL)) { + fprintf(stderr, "FAIL: readv(huge iovcnt) returned %zd errno=%d (want -1/EINVAL)\n", r, errno); + return 1; + } + errno = 0; + r = writev(fd, &v, 0x40000000); + if (!(r == -1 && errno == EINVAL)) { + fprintf(stderr, "FAIL: writev(huge iovcnt) returned %zd errno=%d (want -1/EINVAL)\n", r, errno); + return 1; + } + close(fd); + unlink("/tmp/iovcnt-test"); + fprintf(stderr, "iovcnt-guard test PASSED\n"); + return 0; +}