[Bug 298159] read(2)/write(2) of >= 2 GiB return more bytes than requested: vn_io_fault1() truncates the EFAULT-redo advance to int

From: <bugzilla-noreply_at_freebsd.org>
Date: Fri, 04 Sep 2026 07:43:31 UTC
https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=298159

            Bug ID: 298159
           Summary: read(2)/write(2) of >= 2 GiB return more bytes than
                    requested: vn_io_fault1() truncates the EFAULT-redo
                    advance to int
           Product: Base System
           Version: 15.1-RELEASE
          Hardware: Any
                OS: Any
            Status: New
          Severity: Affects Some People
          Priority: ---
         Component: kern
          Assignee: bugs@FreeBSD.org
          Reporter: tw@waldmann-edv.de
 Attachment #274396 text/plain
         mime type:

Created attachment 274396
  --> https://bugs.freebsd.org/bugzilla/attachment.cgi?id=274396&action=edit
reproducer: single 2 GiB read() into a partially resident buffer

Affects: releng/10.0 through releng/15.1 and main (sys/kern/vfs_vnops.c,
vn_io_fault1()).
Filesystems: any with MNTK_NO_IOPF that does not rewind the uio on EFAULT: ZFS,
UFS reads, NFS client, msdosfs, nullfs over those.
Verified on 15.1-RELEASE amd64 with the stock GENERIC kernel
(releng/15.1-n283562-96841ea08dcf), on ZFS (OpenZFS 2.4.2) and on UFS.

Description

vn_io_fault1() first calls the VOP with page faults disabled. If the copy hits
a non-resident user page it gets EFAULT back with the uio already advanced by
the bytes transferred so far, then advances its clone to that point with

uiomove(NULL, resid - uio->uio_resid, uio_clone);
uiomove(9) takes an int count; resid - uio->uio_resid is a size_t. When the
first attempt transferred 2 GiB or more before faulting, (int) of that is
negative, uiomove() is a no-op, the clone stays at the original position, and
the retry loop re-does the whole request from the beginning while still
subtracting every chunk from uio->uio_resid. The syscall ends with a negative
uio_resid and dofileread()/dofilewrite() return nbyte + (bytes done before the
fault). Data ends up correct; the count and the file offset are wrong. Nothing
asserts uio_resid >= 0 on this path, even with INVARIANTS.

Reproduction (attached faultread2.c / faultwrite.c / gen.c)

./gen /pool/counters.bin 3221225472 ./faultread2 /pool/counters.bin 2415919104
18

allocates a fresh anonymous mapping, touches the first 2.25 GiB, then does a
single read() of 3221225454 bytes at offset 18. Result on 15.1:

read() returned 5637144540; debug.vn_io_faults delta=1; fd position
after=5637144558 (expected 3221225472) *** BUG: returned 2415919086 bytes MORE
than requested (= prefix - 18, ZFS record aligned) content check over
3221225454 bytes: 0 bad words

With a 1 GiB or 2 GiB - 128 KiB touched prefix the count is correct. write()
shows the same (3221225472 requested, 5637144576 returned, file size correct).
With debug.vn_io_fault_enable=0 the count is correct.

Real-world impact: borgbackup 1.x reads its repository index with one read() of
the whole file; users on FreeBSD/ZFS hosting (Hetzner StorageBox, rsync.net)
with indexes above 2 GiB have been getting "raw readinto() returned invalid
length N (should have been between 0 and M)" from CPython since 2022
(https://github.com/borgbackup/borg/issues/6140,
https://github.com/python/cpython/issues/93287). All reported N/M pairs satisfy
N = M + P - 18 with P a 128 KiB boundary in (2^31, 2^32), i.e. exactly this
bug.

Fix

See https://github.com/freebsd/freebsd-src/pull/2407 .

A KASSERT(uio->uio_resid >= 0) at the end of vn_io_fault1() would have caught
this.

-- 
You are receiving this mail because:
You are the assignee for the bug.