fuse: drop rather than zero the copied range in copy_file_range()

After a FUSE_COPY_FILE_RANGE the destination page cache holds stale data,
so __fuse_copy_file_range() clears it with truncate_inode_pages_range()
over a page aligned range.

truncate_inode_pages_range() is built for ranges that are meant to read
back as zeros. A folio that the range only partially covers is handed to
truncate_inode_partial_folio(), which zeroes the covered bytes and then
tries to split the folio so that the covered part can be dropped. Today
every folio is a single page, and a page aligned range is therefore also
folio aligned, so every folio in the range is covered in full and nothing
is ever zeroed.

Once folios can be larger than a page, the range may not line up with
folios (eg the folio holding the start of the range can begin before it
and the folio holding the end can go beyond it). In this case, only the
copied ranges inside those folios will get zeroed. If blocks within
those folios are dirty, then splitting returns -EBUSY and
truncate_inode_partial_folio() returns false. The folio stays in the
page cache with the copied range zeroed, and those zeros are written
back to the server, which overwrites the previously copied file range.
__fuse_copy_file_range() issues a synchronous writeback call before
calling truncate_inode_pages_range(), so in most cases the folio is
not dirty, but there is still the possibility that a memory map raced
with the copy (which is the case the comment in the code already calls
out).

Instead of calling truncate_inode_pages_range(), use
invalidate_inode_pages2_range(). It works in whole folios and never
zeroes, and fuse_writeback_range() has already run on the destination, so
the folios are normally clean and are simply dropped. It is also what the
rest of fuse already uses when the cache has to be re-read from the
server.

Outside the mmap race, the two are equivalent over the copied range for
order-0 folios. Regarding the mmap race however, the old call
(truncate_inode_pages_range()) loses the mmap changes but preserves
the copied ones, whereas the new code (invalidate_inode_pages2_range())
launders the folio before dropping it, which means it loses the copied
changes but preserves the mmap ones. Neither is entirely correct, and
the (nontrivial) fix for both is the mapping->invalidate_lock described
in the comment.

Signed-off-by: Joanne Koong <joannelkoong@gmail.com>
Signed-off-by: Miklos Szeredi <mszeredi@redhat.com>
This commit is contained in:
Joanne Koong 2026-09-15 18:05:25 -07:00 committed by Miklos Szeredi
parent ad866d756d
commit d1c723c516

View File

@ -3002,14 +3002,16 @@ static ssize_t __fuse_copy_file_range(struct file *file_in, loff_t pos_in,
/*
* Write out dirty pages in the destination file before sending the COPY
* request to userspace. After the request is completed, truncate off
* pages (including partial ones) from the cache that have been copied,
* since these contain stale data at that point.
* request to userspace. After the request is completed, drop the
* folios covering the copied range from the cache, since these contain
* stale data at that point.
*
* This should be mostly correct, but if the COPY writes to partial
* pages (at the start or end) and the parts not covered by the COPY are
* This should be mostly correct, but if the COPY writes to a partial
* folio (at the start or end) and the parts not covered by the COPY are
* written through a memory map after calling fuse_writeback_range(),
* then these partial page modifications will be lost on truncation.
* then that folio is laundered before it is dropped, so the memory map
* modifications are written back over the range the server just copied
* into and the copied data is lost.
*
* It is unlikely that someone would rely on such mixed style
* modifications. Yet this does give less guarantees than if the
@ -3063,10 +3065,9 @@ static ssize_t __fuse_copy_file_range(struct file *file_in, loff_t pos_in,
}
if (bytes_copied)
truncate_inode_pages_range(inode_out->i_mapping,
ALIGN_DOWN(pos_out, PAGE_SIZE),
ALIGN(pos_out + bytes_copied,
PAGE_SIZE) - 1);
invalidate_inode_pages2_range(inode_out->i_mapping,
pos_out >> PAGE_SHIFT,
(pos_out + bytes_copied - 1) >> PAGE_SHIFT);
file_update_time(file_out);
fuse_write_update_attr(inode_out, pos_out + bytes_copied, bytes_copied);