| From 4e7a3469506bb05e00920968de00c409cd90c119 Mon Sep 17 00:00:00 2001 |
| From: Sasha Levin <sashal@kernel.org> |
| Date: Mon, 17 Nov 2025 15:28:17 -0500 |
| Subject: NFS: Avoid changing nlink when file removes and attribute updates |
| race |
| |
| From: Trond Myklebust <trond.myklebust@hammerspace.com> |
| |
| [ Upstream commit bd4928ec799b31c492eb63f9f4a0c1e0bb4bb3f7 ] |
| |
| If a file removal races with another operation that updates its |
| attributes, then skip the change to nlink, and just mark the attributes |
| as being stale. |
| |
| Reported-by: Aiden Lambert <alambert48@gatech.edu> |
| Fixes: 59a707b0d42e ("NFS: Ensure we revalidate the inode correctly after remove or rename") |
| Signed-off-by: Trond Myklebust <trond.myklebust@hammerspace.com> |
| Signed-off-by: Sasha Levin <sashal@kernel.org> |
| --- |
| fs/nfs/dir.c | 19 +++++++++++++------ |
| 1 file changed, 13 insertions(+), 6 deletions(-) |
| |
| diff --git a/fs/nfs/dir.c b/fs/nfs/dir.c |
| index 6dc3dcf23550d..847627a69a417 100644 |
| --- a/fs/nfs/dir.c |
| +++ b/fs/nfs/dir.c |
| @@ -1499,13 +1499,15 @@ static int nfs_dentry_delete(const struct dentry *dentry) |
| } |
| |
| /* Ensure that we revalidate inode->i_nlink */ |
| -static void nfs_drop_nlink(struct inode *inode) |
| +static void nfs_drop_nlink(struct inode *inode, unsigned long gencount) |
| { |
| + struct nfs_inode *nfsi = NFS_I(inode); |
| + |
| spin_lock(&inode->i_lock); |
| /* drop the inode if we're reasonably sure this is the last link */ |
| - if (inode->i_nlink > 0) |
| + if (inode->i_nlink > 0 && gencount == nfsi->attr_gencount) |
| drop_nlink(inode); |
| - NFS_I(inode)->attr_gencount = nfs_inc_attr_generation_counter(); |
| + nfsi->attr_gencount = nfs_inc_attr_generation_counter(); |
| nfs_set_cache_invalid( |
| inode, NFS_INO_INVALID_CHANGE | NFS_INO_INVALID_CTIME | |
| NFS_INO_INVALID_OTHER | NFS_INO_REVAL_FORCED); |
| @@ -1523,8 +1525,9 @@ static void nfs_dentry_iput(struct dentry *dentry, struct inode *inode) |
| nfs_set_cache_invalid(inode, NFS_INO_INVALID_DATA); |
| |
| if (dentry->d_flags & DCACHE_NFSFS_RENAMED) { |
| + unsigned long gencount = READ_ONCE(NFS_I(inode)->attr_gencount); |
| nfs_complete_unlink(dentry, inode); |
| - nfs_drop_nlink(inode); |
| + nfs_drop_nlink(inode, gencount); |
| } |
| iput(inode); |
| } |
| @@ -2064,9 +2067,11 @@ static int nfs_safe_remove(struct dentry *dentry) |
| |
| trace_nfs_remove_enter(dir, dentry); |
| if (inode != NULL) { |
| + unsigned long gencount = READ_ONCE(NFS_I(inode)->attr_gencount); |
| + |
| error = NFS_PROTO(dir)->remove(dir, dentry); |
| if (error == 0) |
| - nfs_drop_nlink(inode); |
| + nfs_drop_nlink(inode, gencount); |
| } else |
| error = NFS_PROTO(dir)->remove(dir, dentry); |
| if (error == -ENOENT) |
| @@ -2257,6 +2262,7 @@ int nfs_rename(struct inode *old_dir, struct dentry *old_dentry, |
| { |
| struct inode *old_inode = d_inode(old_dentry); |
| struct inode *new_inode = d_inode(new_dentry); |
| + unsigned long new_gencount = 0; |
| struct dentry *dentry = NULL; |
| struct rpc_task *task; |
| bool must_unblock = false; |
| @@ -2314,6 +2320,7 @@ int nfs_rename(struct inode *old_dir, struct dentry *old_dentry, |
| } else { |
| new_dentry->d_fsdata = NFS_FSDATA_BLOCKED; |
| must_unblock = true; |
| + new_gencount = NFS_I(new_inode)->attr_gencount; |
| spin_unlock(&new_dentry->d_lock); |
| } |
| |
| @@ -2350,7 +2357,7 @@ int nfs_rename(struct inode *old_dir, struct dentry *old_dentry, |
| new_dir, new_dentry, error); |
| if (!error) { |
| if (new_inode != NULL) |
| - nfs_drop_nlink(new_inode); |
| + nfs_drop_nlink(new_inode, new_gencount); |
| /* |
| * The d_move() should be here instead of in an async RPC completion |
| * handler because we need the proper locks to move the dentry. If |
| -- |
| 2.51.0 |
| |