Bug 298159 - read(2)/write(2) of >= 2 GiB return more bytes than requested: vn_io_fault1() truncates the EFAULT-redo advance to int
Summary: read(2)/write(2) of >= 2 GiB return more bytes than requested: vn_io_fault1()...
Status: In Progress
Alias: None
Product: Base System
Classification: Unclassified
Component: kern (show other bugs)
Version: 16.0-CURRENT
Hardware: Any Any
: --- Affects Some People
Assignee: Dag-Erling Smørgrav
URL:
Keywords:
Depends on:
Blocks:
 
Reported: 2026-09-04 07:43 UTC by Thomas Waldmann
Modified: 2026-09-21 06:52 UTC (History)
2 users (show)

See Also:


Attachments
reproducer: single 2 GiB read() into a partially resident buffer (3.62 KB, text/plain)
2026-09-04 07:43 UTC, Thomas Waldmann
no flags Details
reproducer for write() (1.90 KB, text/plain)
2026-09-04 07:45 UTC, Thomas Waldmann
no flags Details
writes the uint64 counter test file (917 bytes, text/plain)
2026-09-04 07:46 UTC, Thomas Waldmann
no flags Details

Note You need to log in before you can comment on or make changes to this bug.
Description Thomas Waldmann 2026-09-04 07:43:31 UTC
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.
Comment 1 Thomas Waldmann 2026-09-04 07:45:19 UTC
Created attachment 274397 [details]
reproducer for write()
Comment 2 Thomas Waldmann 2026-09-04 07:46:26 UTC
Created attachment 274398 [details]
writes the uint64 counter test file
Comment 3 Dag-Erling Smørgrav freebsd_committer freebsd_triage 2026-09-04 19:05:36 UTC
Quick fix for 14 and 15:

https://reviews.freebsd.org/D59412

Proper fix for 16:

https://reviews.freebsd.org/D59413
Comment 4 Thomas Waldmann 2026-09-05 15:51:15 UTC
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.
Comment 5 Dag-Erling Smørgrav freebsd_committer freebsd_triage 2026-09-05 16:00:38 UTC
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?
Comment 6 Thomas Waldmann 2026-09-05 16:04:29 UTC
Thomas Waldmann <tw@waldmann-edv.de>