Skip to content

Commit f6ea32d

Browse files
committed
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.
1 parent 97463b2 commit f6ea32d

3 files changed

Lines changed: 59 additions & 1 deletion

File tree

fs/vfs/vfs_syscalls.cc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,16 @@ sys_read(struct file *fp, const struct iovec *iov, size_t niov,
250250
if ((fp->f_flags & FREAD) == 0)
251251
return EBADF;
252252

253+
// Reject an out-of-range iovec count with EINVAL *before* touching iov: a
254+
// caller-controlled niov > UIO_MAXIOV would otherwise (a) walk iov[] for
255+
// up to niov entries in the summation loop below (OOB read) and (b) hit
256+
// the assert() further down, which in a _KERNEL build is compiled in and
257+
// calls abort() -> whole-VM panic. It also caps the VLA below to a sane
258+
// size. (niov is size_t; guard both the >MAXIOV and, defensively, the
259+
// count itself.)
260+
if (niov > UIO_MAXIOV)
261+
return EINVAL;
262+
253263
size_t bytes = 0;
254264
auto iovp = iov;
255265
for (unsigned i = 0; i < niov; i++) {
@@ -289,6 +299,10 @@ sys_write(struct file *fp, const struct iovec *iov, size_t niov,
289299
if ((fp->f_flags & FWRITE) == 0)
290300
return EBADF;
291301

302+
// See sys_read: reject niov > UIO_MAXIOV up front (OOB-read + abort() DoS).
303+
if (niov > UIO_MAXIOV)
304+
return EINVAL;
305+
292306
size_t bytes = 0;
293307
auto iovp = iov;
294308
for (unsigned i = 0; i < niov; i++) {

modules/tests/Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ ext-only-tests := tst-readdir.so tst-concurrent-read.so tst-fs-link.so
115115

116116
specific-fs-tests := $($(fs_type)-only-tests)
117117

118-
tests := tst-pthread.so misc-ramdisk.so tst-vblk.so tst-mq-smoke.so tst-bsd-evh.so \
118+
tests := tst-iovcnt-guard.so tst-pthread.so misc-ramdisk.so tst-vblk.so tst-mq-smoke.so tst-bsd-evh.so \
119119
misc-bsd-callout.so tst-bsd-kthread.so tst-bsd-taskqueue.so \
120120
tst-fpu.so tst-preempt.so tst-tracepoint.so tst-hub.so \
121121
misc-console.so misc-leak.so misc-readbench.so misc-mmap-anon-perf.so \

tests/tst-iovcnt-guard.cc

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright (C) 2026 Greg Burd
3+
*
4+
* This work is open source software, licensed under the terms of the
5+
* BSD license as described in the LICENSE file in the top-level directory.
6+
*/
7+
8+
// Regression: a huge iovcnt to readv/writev must return -1/EINVAL, not panic
9+
// the kernel (previously an assert(niov <= UIO_MAXIOV) -> abort()).
10+
#include <sys/uio.h>
11+
#include <fcntl.h>
12+
#include <unistd.h>
13+
#include <errno.h>
14+
#include <stdio.h>
15+
#include <string.h>
16+
17+
int main()
18+
{
19+
int fd = open("/tmp/iovcnt-test", O_RDWR | O_CREAT | O_TRUNC, 0644);
20+
if (fd < 0) { perror("open"); return 1; }
21+
// A normal small write to give the file content.
22+
struct iovec one = { (void*)"hi", 2 };
23+
if (writev(fd, &one, 1) != 2) { perror("writev(1)"); return 1; }
24+
lseek(fd, 0, SEEK_SET);
25+
26+
// Oversized iovcnt: must be rejected with EINVAL, not abort the VM.
27+
struct iovec v = { 0, 0 };
28+
errno = 0;
29+
ssize_t r = readv(fd, &v, 0x40000000);
30+
if (!(r == -1 && errno == EINVAL)) {
31+
fprintf(stderr, "FAIL: readv(huge iovcnt) returned %zd errno=%d (want -1/EINVAL)\n", r, errno);
32+
return 1;
33+
}
34+
errno = 0;
35+
r = writev(fd, &v, 0x40000000);
36+
if (!(r == -1 && errno == EINVAL)) {
37+
fprintf(stderr, "FAIL: writev(huge iovcnt) returned %zd errno=%d (want -1/EINVAL)\n", r, errno);
38+
return 1;
39+
}
40+
close(fd);
41+
unlink("/tmp/iovcnt-test");
42+
fprintf(stderr, "iovcnt-guard test PASSED\n");
43+
return 0;
44+
}

0 commit comments

Comments
 (0)