From d9ff84aff6be86817deca5e223e45fc2efedf742 Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Tue, 2 Jun 2026 15:08:11 +0200 Subject: [PATCH 1/2] fuse: track permission-cache validity separately from i_time fuse_perm_getattr() requests only STATX_MODE | STATX_UID | STATX_GID. fi->i_time is only advanced when the response covers STATX_BASIC_STATS in full, so a perm-only refresh clears the perm bits from inval_mask but leaves i_time at its old (expired) value. Add fi->i_perm_time, updated whenever a refresh covers MODE|UID|GID (always for the non-statx getattr path; conditionally on returned_attrs for the statx path), and gate fuse_permission()'s sync decision on it instead of i_time. fuse_update_get_attr() continues to use i_time so a full stat() still re-queries the server when size/mtime/etc. expire. Fixes: 09ed47b12cf2 ("fuse: Optimize statx for permission checks by requesting only needed attributes") Signed-off-by: Horst Birthelmer (cherry picked from commit a2d397168b8784bb0ecbf2f593a658cf53701736) --- fs/fuse/dir.c | 2 +- fs/fuse/fuse_i.h | 9 +++++++++ fs/fuse/inode.c | 12 ++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c index 1b0761e6a65c5d..e0b2c29cfcccc4 100644 --- a/fs/fuse/dir.c +++ b/fs/fuse/dir.c @@ -1640,7 +1640,7 @@ static int fuse_permission(struct mnt_idmap *idmap, struct fuse_inode *fi = get_fuse_inode(inode); if (perm_mask & READ_ONCE(fi->inval_mask) || - time_before64(fi->i_time, get_jiffies_64())) { + time_before64(fi->i_perm_time, get_jiffies_64())) { refreshed = true; err = fuse_perm_getattr(inode, mask, perm_mask); diff --git a/fs/fuse/fuse_i.h b/fs/fuse/fuse_i.h index d78d752055f196..233ee25f43ab23 100644 --- a/fs/fuse/fuse_i.h +++ b/fs/fuse/fuse_i.h @@ -114,6 +114,15 @@ struct fuse_inode { /** Time in jiffies until the file attributes are valid */ u64 i_time; + /* + * Time in jiffies until mode/uid/gid (the permission-check subset of + * STATX_BASIC_STATS) are valid. Tracked separately from i_time so that + * a partial statx refresh covering only the perm bits can extend the + * permission-check cache without falsely advancing i_time for the + * other (un-refreshed) attributes. + */ + u64 i_perm_time; + /* Which attributes are invalid */ u32 inval_mask; diff --git a/fs/fuse/inode.c b/fs/fuse/inode.c index f95f280f8f6f09..4904a9131f2c25 100644 --- a/fs/fuse/inode.c +++ b/fs/fuse/inode.c @@ -265,6 +265,17 @@ static void fuse_change_attributes_common_sx(struct inode *inode, fi->i_time = attr_valid; } + /* + * Permission-check cache: independent of i_time so that a partial + * refresh which covers only mode/uid/gid (e.g. fuse_perm_getattr()) + * still extends the window during which fuse_permission() can hit + * the cache. Requires all three perm bits because generic_permission() + * needs the full triple. + */ + if ((returned_attrs & (STATX_MODE | STATX_UID | STATX_GID)) == + (STATX_MODE | STATX_UID | STATX_GID)) + fi->i_perm_time = attr_valid; + /* * Only update inode fields for attributes that were actually returned. * TYPE is part of i_mode but already set during inode creation. @@ -379,6 +390,7 @@ void fuse_change_attributes_common(struct inode *inode, struct fuse_attr *attr, fi->attr_version = atomic64_inc_return(&fc->attr_version); wake_up_all(&fc->attr_version_waitq); fi->i_time = attr_valid; + fi->i_perm_time = attr_valid; inode->i_ino = fuse_squash_ino(attr->ino); inode->i_mode = (inode->i_mode & S_IFMT) | (attr->mode & 07777); From 634dd3850a12291f96e1739cc6c75157be4371ca Mon Sep 17 00:00:00 2001 From: Horst Birthelmer Date: Wed, 3 Jun 2026 08:45:26 +0200 Subject: [PATCH 2/2] fuse: call fuse_change_entry_timeout() unconditionally when reavlidating dentry Signed-off-by: Horst Birthelmer (cherry picked from commit ec5c66bf68d1bdcf9b7ee70a197914f73ec44d87) --- fs/fuse/dir.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/fs/fuse/dir.c b/fs/fuse/dir.c index e0b2c29cfcccc4..04ed40902ae526 100644 --- a/fs/fuse/dir.c +++ b/fs/fuse/dir.c @@ -329,8 +329,7 @@ static int fuse_dentry_revalidate(struct dentry *entry, unsigned int flags) fuse_change_attributes(inode, &ext_out.entry.attr, &sx, ATTR_TIMEOUT(&ext_out.entry), attr_version); - if ((ext_out.mask & STATX_BASIC_STATS) == STATX_BASIC_STATS) - fuse_change_entry_timeout(entry, &ext_out.entry); + fuse_change_entry_timeout(entry, &ext_out.entry); } else if (inode) { fi = get_fuse_inode(inode); if (flags & LOOKUP_RCU) {