From d1c723c516dc2d43b3a5d668839da21c466953f4 Mon Sep 17 00:00:00 2001 From: Joanne Koong Date: Tue, 15 Sep 2026 18:05:25 -0700 Subject: [PATCH] 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 Signed-off-by: Miklos Szeredi --- fs/fuse/file.c | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/fs/fuse/file.c b/fs/fuse/file.c index 2aa575a02e05..8083404f4cbc 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -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);