Skip to content

vfs: fix vn_io_fault1() resume point past 2 GiB - #2407

Open
ThomasWaldmann wants to merge 1 commit into
freebsd:mainfrom
ThomasWaldmann:vn_io_fault1-int-truncation
Open

vfs: fix vn_io_fault1() resume point past 2 GiB#2407
ThomasWaldmann wants to merge 1 commit into
freebsd:mainfrom
ThomasWaldmann:vn_io_fault1-int-truncation

Conversation

@ThomasWaldmann

@ThomasWaldmann ThomasWaldmann commented Sep 3, 2026

Copy link
Copy Markdown

vn_io_fault1() advances its clone to the EFAULT resume point with uiomove(NULL, resid - uio->uio_resid, uio_clone). The count is a size_t; uiomove(9) takes an int. When the first (faults-disabled) attempt transferred 2 GiB or more before hitting a non-resident user page, the truncated count is negative, the clone is not advanced, and the retry loop re-does the whole request while still subtracting from uio->uio_resid. read(2)/write(2) then return more bytes than requested; the data is correct. Present since the original vn_io_fault() commit (41014d9, 2012), shipped in 9.2 through 15.1.

Real-world impact. borgbackup 1.x reads its repository index with a single 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 (borgbackup/borg#6140, python/cpython#93287). Every reported N/M pair is N = M + P - 18 with P a 128 KiB ZFS record boundary in (2^31, 2^32), i.e. exactly this truncation.

Reproduction (15.1-RELEASE amd64, stock GENERIC kernel; sources: https://gist.github.com/ThomasWaldmann/8b5c0e6f0c376b7a3da2de9fd29e3639): gen writes a 3 GiB file of counters, faultread2 maps a fresh anonymous buffer, touches its first N bytes and issues one read() of the rest of the file from offset 18.

touched prefix fs read() returned expected
1 GiB zfs 3221225454 3221225454
2 GiB − 128 KiB zfs 3221225454 3221225454
2 GiB + 128 KiB zfs 5368840156 3221225454
2.25 GiB zfs 5637144540 3221225454
2.25 GiB ufs 5637144540 3221225454

The excess is always prefix − 18 and record aligned; the file offset advances by the same excess; the buffer content is correct. write() of 3221225472 bytes with a 2.25 GiB touched prefix returns 5637144576. With debug.vn_io_fault_enable=0 the counts are exact.

Validation of the fix. Kernel built from the stock GENERIC config on 15.1-RELEASE, installed and booted: every cell above returns the exact count, write() returns 3221225472, and debug.vn_io_faults still increments, so the retry path is exercised rather than bypassed.

Checklist. tools/build/checkstyle9.pl on the commit: 0 errors, 0 warnings; no trailing whitespace. No new Kyua test is included because a test needs a ≥ 3 GiB file and ≥ 2.25 GiB of RAM for the user buffer; I can add one under tests/sys/kern with require.memory/require.diskspace metadata if that is wanted. Bugzilla: https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=298159 (reproducers attached there; the patch is submitted only via this pull request).

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown

Thank you for taking the time to contribute to FreeBSD!

All issues resolved.

@ThomasWaldmann ThomasWaldmann changed the title vfs: fix vn_io_fault1() resume point for transfers of 2 GiB and more vfs: fix vn_io_fault1() resume point past 2 GiB Sep 3, 2026
@ThomasWaldmann
ThomasWaldmann force-pushed the vn_io_fault1-int-truncation branch from 57fa568 to 1ab76bd Compare September 3, 2026 21:41
vn_io_fault1() first runs the VOP with page faults disabled.  When that
attempt returns EFAULT after transferring part of the request, the
number of bytes already done (resid - uio->uio_resid, a size_t) is used
to advance uio_clone to the resume point with uiomove(9), whose count
argument is an int.  For 2 GiB and more the count is truncated to a
negative int, uiomove() does nothing, the clone stays at the start of
the request, and the retry loop re-does the whole transfer while still
subtracting every chunk from uio->uio_resid.  The syscall ends with a
negative uio_resid and read(2)/write(2) return more bytes than were
requested; the data itself is correct.

This needs a single request of 2 GiB or more into a user buffer that is
resident for its first 2 GiB and then has a non-resident page, on a
filesystem that sets MNTK_NO_IOPF and does not rewind the uio on EFAULT
(ZFS, UFS reads, NFS client, msdosfs, nullfs over those).  borgbackup
1.x reads its repository index with one read() of the whole file and
has been hitting this on FreeBSD/ZFS hosts since 2022:
borgbackup/borg#6140

Split the advance into INT_MAX-sized uiomove() calls.  Verified with a
reproducer on 15.1-RELEASE (ZFS and UFS, read and write): the retry
path is still taken and the returned count is exact.

PR:		298159
Fixes:		41014d9 ("vn_io_fault() is a facility to prevent page faults while filesystems")
MFC after:	1 week
Signed-off-by: Thomas Waldmann <tw@waldmann-edv.de>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@ThomasWaldmann
ThomasWaldmann force-pushed the vn_io_fault1-int-truncation branch from 1ab76bd to b88623b Compare September 4, 2026 07:48
@VexedUXR
VexedUXR requested review from kostikbel and removed request for VexedUXR September 5, 2026 04:59

@kostikbel kostikbel left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants