From: Jimmy Zuber <jamz@amazon.com>
To: <miklos@szeredi.hu>, <shuah@kernel.org>
Cc: <fuse-devel@lists.linux.dev>, <linux-kernel@vger.kernel.org>,
<linux-kselftest@vger.kernel.org>, <jack@suse.com>,
<joannelkoong@gmail.com>, <bfoster@redhat.com>
Subject: [PATCH v3 1/2] fuse: zero the partial EOF page when extending a file
Date: Thu, 20 Aug 2026 12:35:32 +0000 [thread overview]
Message-ID: <20260820123533.190470-2-jamz@amazon.com> (raw)
In-Reply-To: <20260820123533.190470-1-jamz@amazon.com>
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.
pagecache_isize_extended() cannot be reused: it is a no-op when
i_blocksize() >= PAGE_SIZE, and would increase the work in that function
for use cases that don't need it, if changed to support this situation.
Add fuse_zero_partial_eof_folio(), which zeroes the tail of the old EOF
folio, and call it up front from the three paths that extend a file,
mirroring xfs_file_write_zero_eof() and ext4_block_zero_eof():
- a buffered write whose position is past the old EOF (fuse_perform_write(),
before the page cache is filled);
- a size-extending setattr/truncate (fuse_do_setattr());
- a size-extending fallocate (fuse_file_fallocate()).
Zeroing [old EOF, write start) before the write, rather than after it, keeps
the zeroed range disjoint from the written data, so a write that lands inside
the old EOF folio is preserved without special-casing.
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 <jamz@amazon.com>
---
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..57630ad0af66 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,6 +1202,43 @@ static ssize_t fuse_send_write(struct fuse_io_args *ia, loff_t pos,
return err ?: ia->write.out.size;
}
+/*
+ * A size-extending operation is about to turn [@from, @to) -- the range past a
+ * non-folio-aligned old EOF at @from -- 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 instead of
+ * zeros (xfstests generic/363). Only the folio straddling @from can hold such
+ * bytes, so a single folio is handled, as in pagecache_isize_extended().
+ *
+ * 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);
@@ -1368,9 +1407,19 @@ 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 the write starts past a non-aligned EOF, zero the old EOF folio's
+ * tail before filling the page cache, so [old_size, pos) reads as the
+ * hole it is. The write below fills from @pos, disjoint from this
+ * range.
+ */
+ if (pos > old_size)
+ fuse_zero_partial_eof_folio(inode, old_size, pos);
+
if (inode->i_size < pos + iov_iter_count(ii))
set_bit(FUSE_I_SIZE_UNSTABLE, &fi->state);
@@ -2913,6 +2962,13 @@ 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)) {
+ /*
+ * fallocate writes no data, so the whole extension past the old
+ * EOF is a hole; zero the old EOF folio's tail before publishing
+ * the new size.
+ */
+ fuse_zero_partial_eof_folio(inode, i_size_read(inode),
+ offset + length);
if (fuse_write_update_attr(inode, offset + length, length))
file_update_time(file);
}
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
next prev parent reply other threads:[~2026-08-20 12:36 UTC|newest]
Thread overview: 3+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-08-20 12:35 [PATCH v3 0/2] fuse: zero the partial EOF page when extending a file Jimmy Zuber
2026-08-20 12:35 ` Jimmy Zuber [this message]
2026-08-20 12:35 ` [PATCH v3 2/2] selftests/fuse: test post-EOF page zeroing when a file is extended Jimmy Zuber
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260820123533.190470-2-jamz@amazon.com \
--to=jamz@amazon.com \
--cc=bfoster@redhat.com \
--cc=fuse-devel@lists.linux.dev \
--cc=jack@suse.com \
--cc=joannelkoong@gmail.com \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-kselftest@vger.kernel.org \
--cc=miklos@szeredi.hu \
--cc=shuah@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.