From 9d22d3bf4f80a49fdd0ff87cfcfb14f1a2ba0a6e 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 d032636b3a9dad..e1a53914fd92a9 100644 --- a/fs/fuse/dir.c +++ b/fs/fuse/dir.c @@ -1672,7 +1672,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 98dab92959d2a6..3b3897f7ddfc80 100644 --- a/fs/fuse/fuse_i.h +++ b/fs/fuse/fuse_i.h @@ -124,6 +124,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 23ffc488147e69..88c377e95bf569 100644 --- a/fs/fuse/inode.c +++ b/fs/fuse/inode.c @@ -268,6 +268,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 88c04f0d6d8e0fdd8943e213b288428874f4e41e 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 e1a53914fd92a9..3ec99cee3ef62d 100644 --- a/fs/fuse/dir.c +++ b/fs/fuse/dir.c @@ -330,8 +330,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) {