[Cc: Jan + Joanne] On Fri, 31 Jul 2026 at 22:39, Jimmy Zuber <[email protected]> wrote: > > Extending a fuse file past a non-page-aligned EOF does not zero the tail of > the old last page. If that page is cached and was dirtied beyond the old > EOF -- e.g. an application mmap()ed the EOF page and stored into the region > past EOF, which is undefined until the file grows -- the now in-bounds tail > is exposed to subsequent reads as stale data instead of zeros, in violation > of POSIX file-extension semantics. > > Other filesystems zero this via pagecache_isize_extended(), but that helper > is a no-op for fuse: it returns early when i_blocksize() >= PAGE_SIZE, and > a non-fuseblk fuse mount has s_blocksize == PAGE_SIZE (the server-supplied > st_blksize only sets fi->cached_i_blkbits, not i_blkbits). The NFS client > hit the same problem and open-codes the zeroing in > nfs_truncate_last_folio(); add the equivalent fuse_zero_partial_eof_folio() > and call it from the three paths that extend a file: a buffered write, a > size-extending setattr/truncate, and a size-extending fallocate > (fuse_write_update_attr(), fuse_do_setattr() and fuse_file_fallocate()). > > 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 | 56 ++++++++++++++++++++++++++++++++++++++++++++++++ > fs/fuse/fuse_i.h | 1 + > 3 files changed, 60 insertions(+) > > diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c > index 795e92037ce7..f6614ccef186 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) > + fuse_zero_partial_eof_folio(inode, oldsize, > + outarg.attr.size); > truncate_pagecache(inode, outarg.attr.size); > invalidate_inode_pages2(mapping); > } > diff --git a/fs/fuse/file.c b/fs/fuse/file.c > index cb8da4c06d17..a9063b4e9217 100644 > --- a/fs/fuse/file.c > +++ b/fs/fuse/file.c > @@ -21,6 +21,8 @@ > #include <linux/splice.h> > #include <linux/task_io_accounting_ops.h> > #include <linux/iomap.h> > +#include <linux/highmem.h> > +#include <linux/rmap.h> > > static int fuse_send_open(struct fuse_mount *fm, u64 nodeid, > unsigned int open_flags, int opcode, > @@ -1200,20 +1202,64 @@ static ssize_t fuse_send_write(struct fuse_io_args > *ia, loff_t pos, > return err ?: ia->write.out.size; > } > > +/* > + * An operation extended i_size past a non-folio-aligned old EOF at @from, > + * turning [@from, @to) into a hole that must read back as zero. If the old > + * last folio is cached and was dirtied beyond the old EOF (e.g. mmap stores > + * into the post-EOF region, which are undefined until the file grows), zero > + * that tail so it is not exposed as stale data (xfstests generic/363). > + * > + * pagecache_isize_extended() cannot be used: it bails out for > + * i_blocksize() >= PAGE_SIZE, and a non-fuseblk mount has > + * s_blocksize == PAGE_SIZE, so the zeroing has to be done here. > + * Callers hold i_rwsem, serialising this against concurrent writes and > + * truncates; it must not run under fi->lock, as it locks the folio. > + */ > +void fuse_zero_partial_eof_folio(struct inode *inode, loff_t from, loff_t to) > +{ > + struct folio *folio; > + size_t offset, end; > + > + if (from >= to) > + return; > + > + folio = filemap_lock_folio(inode->i_mapping, from >> PAGE_SHIFT); > + if (IS_ERR(folio)) > + return; > + > + if (folio_mkclean(folio)) > + folio_mark_dirty(folio); > + > + if (folio_test_dirty(folio)) { > + offset = offset_in_folio(folio, from); > + end = min_t(loff_t, to - folio_pos(folio), folio_size(folio)); > + folio_zero_segment(folio, offset, end); > + } > + > + folio_unlock(folio); > + folio_put(folio); > +} > +
While the fix is probably correct, I'm not happy with it. Why does pagecache_isize_extended() not handle blocksize >= PAGE_SIZE? Where does straddling folio zeroing happen for other filesystems? Is it done by the buffer head code? Is it done by iomap? What about cases that use neither? Thanks, Miklos

