Re: [fuse-devel] Debugging a stale kernel cache during file growth

Nikolay Amiantov
2026-04-16T12:41:47Z

[-- Attachment #1.1: Type: text/plain, Size: 1336 bytes --]

On 4/16/26 19:12, Miklos Szeredi wrote:
> I wonder if we could clear PG_uptodate on the page which had its zero
> bytes exposed by the i_size increase?

I've actually tried that first. The idea was to get or create a new page
on the EOF boundary, lock it and poison it with an uptodate reset if we
need to. But this resulted in an instantaneous EIO in my test. If I
undestand correctly, this is because of another race condition:

* A fresh page gets created and read by FUSE; uptodate is true;
* The page is unlocked on return from `fuse_read_folio`;
* Simultaneously, we run `getattr`. The page gets locked, uptodate is
reset, the page is unlocked;
* Now back from `fuse_read_folio`, `filemap_read_folio` gets this page,
waits on `folio_wait_locked_killable` (waiting for the getattr to reset
uptodate), and then checks `folio_test_uptodate`;
* The page is !uptodate, so an EIO is returned.

So it effectively results in inability to have a successful `read` when
a `getattr` for a growing file happens simultaneously.

Finally, if I understand correctly, this also leaves a (much smaller)
theoretical race condition in `filemap_read` between checking uptodate
and getting the current inode size.

Attached is the patch with this attempt; please check that it does what
you meant in case I misunderstood.

Cheers,
Nikolay.

[-- Attachment #1.2: Type: text/html, Size: 1901 bytes --]

[-- Attachment #2: 0001-fuse-fix-stale-page-cache-data-race-on-file-growth.patch --]
[-- Type: text/x-patch, Size: 1830 bytes --]

From 512194b982fd0edbc1dcaa50fafad75b1be26d42 Mon Sep 17 00:00:00 2001
From: Nikolay Amiantov <ab@fmap.me>
Date: Wed, 15 Apr 2026 07:28:19 +0000
Subject: [PATCH] fuse: fix stale page cache data race on file growth

---
fs/fuse/inode.c | 36 ++++++++++++++++++++++++++++++++++--
1 file changed, 34 insertions(+), 2 deletions(-)

diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c
index 735abf426a06..20741869ac2f 100644
--- a/fs/fuse/inode.c
+++ b/fs/fuse/inode.c
@@ -334,10 +334,42 @@ void fuse_change_attributes(struct inode *inode, struct fuse_attr *attr,
* extend local i_size without keeping userspace server in sync. So,
* attr->size coming from server can be stale. We cannot trust it.
*/
- if (!(cache_mask & STATX_SIZE))
- i_size_write(inode, attr->size);
+ if (!(cache_mask & STATX_SIZE)) {
+ if (S_ISREG(inode->i_mode) && attr->size > oldsize) {
+ struct folio *folio;
+ pgoff_t index = oldsize >> PAGE_SHIFT;
+
+ spin_unlock(&fi->lock);
+ folio = __filemap_get_folio(inode->i_mapping, index,
+ FGP_LOCK | FGP_CREAT,
+ mapping_gfp_mask(inode->i_mapping));
+ if (!IS_ERR(folio)) {
+ spin_lock(&fi->lock);
+ if (!test_bit(FUSE_I_SIZE_UNSTABLE, &fi->state)) {
+ folio_clear_uptodate(folio);
+ i_size_write(inode, attr->size);
+ }
+ spin_unlock(&fi->lock);
+
+ folio_unlock(folio);
+ folio_put(folio);
+ goto size_updated;
+ }
+ spin_lock(&fi->lock);
+ /*
+ * Folio alloc failed (ENOMEM). Recheck in case a
+ * write/truncate started while we dropped the lock.
+ */
+ if (!test_bit(FUSE_I_SIZE_UNSTABLE, &fi->state))
+ i_size_write(inode, attr->size);
+ } else {
+ i_size_write(inode, attr->size);
+ }
+ }
spin_unlock(&fi->lock);

+size_updated:
+
if (!cache_mask && S_ISREG(inode->i_mode)) {
bool inval = false;

--
2.47.0