Created attachment 274396 [details] 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.
Created attachment 274397 [details] reproducer for write()
Created attachment 274398 [details] writes the uint64 counter test file
Quick fix for 14 and 15: https://reviews.freebsd.org/D59412 Proper fix for 16: https://reviews.freebsd.org/D59413
Thanks for addressing this that quickly, much appreciated! When you close this, can you add a comment here about which releases will contain the fix, please? I would like to forward this to rsync.net and hetzner, so they can resolve the production issues seen due to this.
Nothing has been decided yet, but we will probably put out an errata notice for this bug. Can you please let me know how you would like to be credited?
Thomas Waldmann <tw@waldmann-edv.de>