[PATCH] NFSv4: pin the superblock for active state owners

From: Jia Zhu

Date: Tue Jul 07 2026 - 09:49:42 EST


NFSv4 open state can outlive the file and dentry that created it. This
was observed in production when NFSv4 state recovery, such as after a
server reboot or lease expiration, raced with unmount.

The race requires recovery to hold an open state reference while the last
open file is closed and the filesystem is unmounted, allowing the
superblock's active reference to drop to zero between
refcount_inc(&state->count) and nfs4_put_open_state():

state manager umount

nfs4_run_state_manager()
nfs4_do_reclaim()
nfs4_reclaim_open_state()
refcount_inc(&state->count)
...
close last file
generic_shutdown_super()
"Busy inodes after unmount"
nfs_free_server()
nfs4_put_open_state()
iput(inode)
evict()
nfs_clear_inode()
nfs_zap_acl_cache()

The "VFS: Busy inodes after unmount" warning is the visible symptom of
that lifetime mismatch: superblock teardown proceeds even though the NFS
open state still pins an inode. After umount has freed the server, the
state manager can then run nfs4_put_open_state() for the last open-state
reference. The resulting iput(inode) can evict an NFS inode with freed
server data, causing crashes at nfs_zap_acl_cache(). This can be
reproduced by delaying the reclaim path before nfs4_put_open_state(),
then closing the last file and unmounting the NFS mount.

Pin the superblock while a state owner is active, and drop the pin when
the owner becomes idle again, so the NFS server stays alive until all
open state associated with the owner has been released.

Assisted-by: Codex:GPT-5
Signed-off-by: Jia Zhu <zhujia.zj@xxxxxxxxxxxxx>
---
fs/nfs/nfs4state.c | 40 +++++++++++++++++++++++++++++++++-------
1 file changed, 33 insertions(+), 7 deletions(-)

diff --git a/fs/nfs/nfs4state.c b/fs/nfs/nfs4state.c
index 305a772e54976..a5dec0473e221 100644
--- a/fs/nfs/nfs4state.c
+++ b/fs/nfs/nfs4state.c
@@ -351,6 +351,26 @@ const struct cred *nfs4_get_clid_cred(struct nfs_client *clp)
return cred;
}

+static bool
+nfs4_get_state_owner_active_locked(struct nfs4_state_owner *sp)
+{
+ struct nfs_server *server = sp->so_server;
+
+ /*
+ * A counted state owner may dereference so_server until the final
+ * nfs4_put_state_owner(). Pin the superblock when reviving an idle
+ * owner so umount cannot free the server underneath it.
+ */
+ if (atomic_read(&sp->so_count) == 0) {
+ if (!nfs_sb_active(server->super))
+ return false;
+ if (!list_empty(&sp->so_lru))
+ list_del_init(&sp->so_lru);
+ }
+ atomic_inc(&sp->so_count);
+ return true;
+}
+
static struct nfs4_state_owner *
nfs4_find_state_owner_locked(struct nfs_server *server, const struct cred *cred)
{
@@ -369,9 +389,8 @@ nfs4_find_state_owner_locked(struct nfs_server *server, const struct cred *cred)
else if (cmp > 0)
p = &parent->rb_right;
else {
- if (!list_empty(&sp->so_lru))
- list_del_init(&sp->so_lru);
- atomic_inc(&sp->so_count);
+ if (!nfs4_get_state_owner_active_locked(sp))
+ return NULL;
return sp;
}
}
@@ -397,9 +416,8 @@ nfs4_insert_state_owner_locked(struct nfs4_state_owner *new)
else if (cmp > 0)
p = &parent->rb_right;
else {
- if (!list_empty(&sp->so_lru))
- list_del_init(&sp->so_lru);
- atomic_inc(&sp->so_count);
+ if (!nfs4_get_state_owner_active_locked(sp))
+ return NULL;
return sp;
}
}
@@ -449,6 +467,10 @@ nfs4_alloc_state_owner(struct nfs_server *server,
sp = kzalloc_obj(*sp, gfp_flags);
if (!sp)
return NULL;
+ if (!nfs_sb_active(server->super)) {
+ kfree(sp);
+ return NULL;
+ }
sp->so_seqid.owner_id = atomic64_inc_return(&server->owner_ctr);
sp->so_server = server;
sp->so_cred = get_cred(cred);
@@ -534,8 +556,10 @@ struct nfs4_state_owner *nfs4_get_state_owner(struct nfs_server *server,
spin_lock(&clp->cl_lock);
sp = nfs4_insert_state_owner_locked(new);
spin_unlock(&clp->cl_lock);
- if (sp != new)
+ if (sp != new) {
nfs4_free_state_owner(new);
+ nfs_sb_deactive(server->super);
+ }
out:
nfs4_gc_state_owners(server);
return sp;
@@ -557,6 +581,7 @@ void nfs4_put_state_owner(struct nfs4_state_owner *sp)
{
struct nfs_server *server = sp->so_server;
struct nfs_client *clp = server->nfs_client;
+ struct super_block *sb = server->super;

if (!atomic_dec_and_lock(&sp->so_count, &clp->cl_lock))
return;
@@ -564,6 +589,7 @@ void nfs4_put_state_owner(struct nfs4_state_owner *sp)
sp->so_expires = jiffies;
list_add_tail(&sp->so_lru, &server->state_owners_lru);
spin_unlock(&clp->cl_lock);
+ nfs_sb_deactive(sb);
}

/**
--
2.20.1