[PATCH v4 1/2] fuse: zero the partial EOF page when extending a file
Jimmy Zuber <[email protected]>
| Newsgroups | dev.linux.lists.fuse-devel,org.kernel.vger.linux-kernel,org.kernel.vger.linux-kselftest |
|---|---|
| Message-ID | <[email protected]> |
Extending a fuse file past a non-page-aligned EOF does not zero the tail of the old last page. When that page is cached and has been mmap-dirtied beyond the old EOF, the now in-bounds tail is served to later reads as stale data rather than zeros, which violates POSIX file-extension semantics. Some file systems get this zeroing automatically at writeback time (block_write_full_folio() / iomap_writeback_handle_eof() zero the tail of the folio straddling i_size). A non-writeback caching fuse file system uses neither path, so it has to zero the tail itself from the size-extending paths, like XFS (xfs_file_write_zero_eof()) and ext4 (ext4_block_zero_eof()) do. Call truncate_pagecache_range() over the newly-exposed range up front from the three paths that extend a file, before the new size is published: - a buffered write whose position is past the old EOF (fuse_perform_write()); - a size-extending setattr/truncate (fuse_do_setattr()); - a size-extending fallocate (fuse_file_fallocate()). This unmaps the stale mappings and zeroes the partial tail of the old EOF folio, so a later read returns zeros. Truncating [old EOF, write start) before a buffered write keeps the dropped range disjoint from the written data, so a write that lands inside the old EOF folio is preserved. writeback_cache connections are unaffected, as their writes go through iomap_file_buffered_write(), which zeroes post-EOF folios. The bug is observable on a non-writeback_cache server that returns FOPEN_KEEP_CACHE on writable files (without FOPEN_DIRECT_IO), and is caught by the new write_extend_eof fuse selftest. Signed-off-by: Jimmy Zuber <[email protected]> --- fs/fuse/dir.c | 3 +++ fs/fuse/file.c | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c index 795e92037ce7..a6f0f509f840 100644 --- a/fs/fuse/dir.c +++ b/fs/fuse/dir.c @@ -2282,6 +2282,9 @@ int fuse_do_setattr(struct mnt_idmap *idmap, struct dentry *dentry, */ if ((is_truncate || !is_wb) && S_ISREG(inode->i_mode) && oldsize != outarg.attr.size) { + if (outarg.attr.size > oldsize) + truncate_pagecache_range(inode, oldsize, + outarg.attr.size - 1); truncate_pagecache(inode, outarg.attr.size); invalidate_inode_pages2(mapping); } diff --git a/fs/fuse/file.c b/fs/fuse/file.c index cb8da4c06d17..6ec13c6aafe6 100644 --- a/fs/fuse/file.c +++ b/fs/fuse/file.c @@ -1368,9 +1368,13 @@ static ssize_t fuse_perform_write(struct kiocb *iocb, struct iov_iter *ii) struct fuse_conn *fc = get_fuse_conn(inode); struct fuse_inode *fi = get_fuse_inode(inode); loff_t pos = iocb->ki_pos; + loff_t old_size = i_size_read(inode); int err = 0; ssize_t res = 0; + if (pos > old_size) + truncate_pagecache_range(inode, old_size, pos - 1); + if (inode->i_size < pos + iov_iter_count(ii)) set_bit(FUSE_I_SIZE_UNSTABLE, &fi->state); @@ -2913,6 +2917,11 @@ static long fuse_file_fallocate(struct file *file, int mode, loff_t offset, /* we could have extended the file */ if (!(mode & FALLOC_FL_KEEP_SIZE)) { + loff_t oldsize = i_size_read(inode); + + if (offset + length > oldsize) + truncate_pagecache_range(inode, oldsize, + offset + length - 1); if (fuse_write_update_attr(inode, offset + length, length)) file_update_time(file); } -- 2.50.1