NFSD: Prevent client use-after-free during export state revocation

nfsd4_revoke_export_states() has the same use-after-free as
nfsd4_revoke_states(): it drops nn->client_lock across
revoke_one_stid() and the following read of clp->cl_minorversion, but
the stateid reference it holds does not pin the client.  A teardown
racing the dropped lock can free the client while revoke_one_stid()
still dereferences it.

exportfs -u drives this path through NFSD_CMD_UNLOCK_EXPORT, so an
administrator removing an export can race a client expiry.

Skip a client that is already expiring and otherwise pin it with
cl_rpc_users under client_lock before dropping the lock, matching
nfsd4_revoke_states().

Fixes: 2eac189bb0 ("NFSD: Add NFSD_CMD_UNLOCK_EXPORT netlink command")
Reviewed-by: NeilBrown <neil@brown.name>
Reviewed-by: Jeff Layton <jlayton@kernel.org>
Link: https://patch.msgid.link/20260709-cel-v4-4-1d519d9be0cb@kernel.org
Signed-off-by: Chuck Lever <cel@kernel.org>
This commit is contained in:
Chuck Lever
2026-07-09 13:40:27 -04:00
parent e270e5a077
commit 2108de5356

View File

@@ -2055,10 +2055,14 @@ void nfsd4_revoke_export_states(struct nfsd_net *nn, const struct path *path)
struct nfs4_client *clp;
retry:
list_for_each_entry(clp, head, cl_idhash) {
struct nfs4_stid *stid = find_one_export_stid(
clp, path,
sc_types);
struct nfs4_stid *stid;
/* Skip or pin clp as in nfsd4_revoke_states(). */
if (is_client_expired(clp))
continue;
stid = find_one_export_stid(clp, path, sc_types);
if (stid) {
atomic_inc(&clp->cl_rpc_users);
spin_unlock(&nn->client_lock);
revoke_one_stid(nn, clp, stid);
nfs4_put_stid(stid);
@@ -2066,6 +2070,9 @@ void nfsd4_revoke_export_states(struct nfsd_net *nn, const struct path *path)
if (clp->cl_minorversion == 0)
nn->nfs40_last_revoke =
ktime_get_boottime_seconds();
if (atomic_dec_and_test(&clp->cl_rpc_users) &&
is_client_expired(clp))
wake_up_all(&expiry_wq);
goto retry;
}
}