From 95ae0a5ba0f87cb8f008be0ad38952378b9c16a4 Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Thu, 15 Jan 2026 16:28:37 +0100 Subject: [PATCH] fuse: fix race between inode creation and invalidation There is a race condition between fuse_iget() and fuse_reverse_inval_inode() where an invalidation request can arrive while an inode is still being initialized. This can lead to the invalidation being lost, causing stale inode attributes to persist. The race occurs when: 1. fuse_iget() is called to create/initialize an inode 2. Before fuse_change_attributes_i() completes, fuse_reverse_inval_inode() is called for the same inode 3. The invalidation updates attr_version, but the subsequent fuse_change_attributes_i() overwrites it, losing the invalidation Fix this by introducing a delayed invalidation mechanism using attr_version as a state indicator: - attr_version == 0: inode initialization in progress (fuse_iget not done) - attr_version == 1: delayed invalidation marker - attr_version > 1: normal operation When fuse_reverse_inval_inode() encounters an inode with attr_version == 0, it marks it with attr_version = 1 instead of performing immediate invalidation. After fuse_iget() completes fuse_change_attributes_i(), it checks for the delayed invalidation marker (attr_version == 1) and performs the invalidation at that point. This ensures invalidation requests are not lost during inode initialization, maintaining cache coherency. Signed-off-by: Guang Yuan Wu Applied-by: Horst Birthelmer --- fs/fuse/inode.c | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c index 61962fd615857a..9b7e06777b8c5b 100644 --- a/fs/fuse/inode.c +++ b/fs/fuse/inode.c @@ -231,13 +231,14 @@ void fuse_change_attributes_common(struct inode *inode, struct fuse_attr *attr, * if the attr_version would've been preserved. * * !evict_ctr -> this is create - * fi->attr_version != 0 -> this is not a new inode + * fi->attr_version > 1 -> this is not a new inode * evict_ctr == fuse_get_evict_ctr() -> no evicts while during request */ - if (!evict_ctr || fi->attr_version || evict_ctr == fuse_get_evict_ctr(fc)) + if (!evict_ctr || fi->attr_version > 1 || evict_ctr == fuse_get_evict_ctr(fc)) set_mask_bits(&fi->inval_mask, STATX_BASIC_STATS, 0); - fi->attr_version = atomic64_inc_return(&fc->attr_version); + if (fi->attr_version != 1) /* fuse_iget() handle if attr_version == 1 */ + fi->attr_version = atomic64_inc_return(&fc->attr_version); fi->i_time = attr_valid; inode->i_ino = fuse_squash_ino(attr->ino); @@ -520,6 +521,18 @@ struct inode *fuse_iget(struct super_block *sb, u64 nodeid, done: fuse_change_attributes_i(inode, attr, NULL, attr_valid, attr_version, evict_ctr); + + spin_lock(&fi->lock); + if (fi->attr_version == 1) /* delayed inode invalidation */ + { + spin_unlock(&fi->lock); + down_read(&fc->killsb); + fuse_reverse_inval_inode(fc, nodeid, 0, 0); + up_read(&fc->killsb); + } + else + spin_unlock(&fi->lock); + return inode; } @@ -598,6 +611,21 @@ int fuse_reverse_inval_inode(struct fuse_conn *fc, u64 nodeid, fi = get_fuse_inode(inode); spin_lock(&fi->lock); + + if (fi->attr_version == 0) + { + /* attr_version == 0 indicate fuse_iget() is not completed yet. + Skip the inode invalidation operation here, and delay it in the + function fuse_iget(), after call to fuse_change_attributes_i(). + Initialized value of fc->attr_version is 1, so fi->attr_version + will be 0 or >= 2, we use value 1 to indicate the "delayed" + inode invalidation operation been recorded */ + fi->attr_version = 1; + spin_unlock(&fi->lock); + iput(inode); + return 0; + } + fi->attr_version = atomic64_inc_return(&fc->attr_version); spin_unlock(&fi->lock);