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);
+}
+
 bool fuse_write_update_attr(struct inode *inode, loff_t pos, ssize_t written)
 {
        struct fuse_conn *fc = get_fuse_conn(inode);
        struct fuse_inode *fi = get_fuse_inode(inode);
        bool ret = false;
+       loff_t old_size = 0;
 
        spin_lock(&fi->lock);
        fi->attr_version = atomic64_inc_return(&fc->attr_version);
        if (written > 0 && pos > inode->i_size) {
+               old_size = inode->i_size;
                i_size_write(inode, pos);
                ret = true;
        }
        spin_unlock(&fi->lock);
 
+       /* [old_size, pos - written) is the hole this write opened past EOF. */
+       if (ret)
+               fuse_zero_partial_eof_folio(inode, old_size, pos - written);
+
        fuse_invalidate_attr_mask(inode, FUSE_STATX_MODSIZE);
 
        return ret;
@@ -2913,8 +2959,18 @@ 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 (fuse_write_update_attr(inode, offset + length, length))
                        file_update_time(file);
+               /*
+                * fuse_write_update_attr() already zeroes up to @offset when
+                * the write started past the old EOF; this additionally covers
+                * a fallocate whose range starts at or before it.  fallocate
+                * writes no data, so the whole extension must read as zero; the
+                * overlap is a no-op.
+                */
+               fuse_zero_partial_eof_folio(inode, oldsize, offset + length);
        }
 
        if (mode & (FALLOC_FL_PUNCH_HOLE | FALLOC_FL_ZERO_RANGE))
diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h
index 85f738c53122..ee3b91b56fef 100644
--- a/fs/fuse/fuse_i.h
+++ b/fs/fuse/fuse_i.h
@@ -1183,6 +1183,7 @@ long fuse_ioctl_common(struct file *file, unsigned int 
cmd,
 __poll_t fuse_file_poll(struct file *file, poll_table *wait);
 
 bool fuse_write_update_attr(struct inode *inode, loff_t pos, ssize_t written);
+void fuse_zero_partial_eof_folio(struct inode *inode, loff_t from, loff_t to);
 
 int fuse_flush_times(struct inode *inode, struct fuse_file *ff);
 int fuse_write_inode(struct inode *inode, struct writeback_control *wbc);
-- 
2.50.1


Reply via email to