* [PATCH 7.2.y 2/6] NFSD: Prevent client use-after-free during admin state revocation
2026-09-07 20:18 ` [PATCH 7.2.y 1/6] NFSD: Prevent client use-after-free during delegation revoke Sasha Levin
@ 2026-09-07 20:18 ` Sasha Levin
2026-09-07 20:18 ` [PATCH 7.2.y 3/6] NFSD: Prevent client use-after-free during export " Sasha Levin
` (3 subsequent siblings)
4 siblings, 0 replies; 7+ messages in thread
From: Sasha Levin @ 2026-09-07 20:18 UTC (permalink / raw)
To: stable; +Cc: Chuck Lever, NeilBrown, Jeff Layton, Sasha Levin
From: Chuck Lever <cel@kernel.org>
[ Upstream commit e270e5a0778e5bff852c8862ce9576ce70359393 ]
A stateid holds only a bare pointer to its nfs4_client; a stateid
reference does not pin it. The client survives only because
__destroy_client() drains its stateids before free_client() runs.
nfsd4_revoke_states() drops nn->client_lock across revoke_one_stid(),
which dereferences the client to revoke a stateid and read
clp->cl_minorversion. A teardown racing the dropped lock can free
the client first.
Pinning cl_rpc_users under client_lock blocks the DESTROY_CLIENTID and
EXCHANGE_ID teardown, which refuses while cl_rpc_users is non-zero.
force_expire_client() ignores it: once its wait for cl_rpc_users to
reach zero has passed, a later pin goes unnoticed.
Under client_lock, skip a client whose cl_time is already zero --
force_expire_client() clears it there before waiting -- otherwise pin
cl_rpc_users before dropping the lock. The walk then either sees the
expiry and skips, or pins in time for that wait to cover the revoke.
Fixes: 1c13bf9f2e3c ("nfsd: allow lock state ids to be revoked and then freed")
Cc: stable@vger.kernel.org
Reviewed-by: NeilBrown <neil@brown.name>
Reviewed-by: Jeff Layton <jlayton@kernel.org>
Link: https://patch.msgid.link/20260709-cel-v4-3-1d519d9be0cb@kernel.org
Signed-off-by: Chuck Lever <cel@kernel.org>
Stable-dep-of: 9026932ac8be ("NFSD: Prevent client use-after-free during blocked-lock reaping")
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
fs/nfsd/nfs4state.c | 22 ++++++++++++++++++++--
1 file changed, 20 insertions(+), 2 deletions(-)
diff --git a/fs/nfsd/nfs4state.c b/fs/nfsd/nfs4state.c
index e1a8d300636b9..8b0feceee5ac6 100644
--- a/fs/nfsd/nfs4state.c
+++ b/fs/nfsd/nfs4state.c
@@ -1889,9 +1889,19 @@ void nfsd4_revoke_states(struct nfsd_net *nn, struct super_block *sb)
struct nfs4_client *clp;
retry:
list_for_each_entry(clp, head, cl_idhash) {
- struct nfs4_stid *stid = find_one_sb_stid(clp, sb,
- sc_types);
+ struct nfs4_stid *stid;
+
+ /*
+ * force_expire_client() ignores cl_rpc_users once
+ * its wait_event() has passed, so pinning cannot
+ * keep an already-expiring client alive; the
+ * expiry path revokes its states instead.
+ */
+ if (is_client_expired(clp))
+ continue;
+ stid = find_one_sb_stid(clp, sb, 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);
@@ -1904,6 +1914,9 @@ void nfsd4_revoke_states(struct nfsd_net *nn, struct super_block *sb)
*/
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;
}
}
@@ -3259,6 +3272,11 @@ static void force_expire_client(struct nfs4_client *clp)
trace_nfsd_clid_admin_expired(&clp->cl_clientid);
+ /*
+ * cl_time is cleared under client_lock before the wait so a
+ * revocation walk pinning cl_rpc_users under it either skips
+ * this client or is seen by this wait_event().
+ */
spin_lock(&nn->client_lock);
clp->cl_time = 0;
spin_unlock(&nn->client_lock);
--
2.53.0
^ permalink raw reply related [flat|nested] 7+ messages in thread* [PATCH 7.2.y 3/6] NFSD: Prevent client use-after-free during export state revocation
2026-09-07 20:18 ` [PATCH 7.2.y 1/6] NFSD: Prevent client use-after-free during delegation revoke Sasha Levin
2026-09-07 20:18 ` [PATCH 7.2.y 2/6] NFSD: Prevent client use-after-free during admin state revocation Sasha Levin
@ 2026-09-07 20:18 ` Sasha Levin
2026-09-07 20:18 ` [PATCH 7.2.y 4/6] NFSD: Prevent client use-after-free during NFSv4.0 revoked-state cleanup Sasha Levin
` (2 subsequent siblings)
4 siblings, 0 replies; 7+ messages in thread
From: Sasha Levin @ 2026-09-07 20:18 UTC (permalink / raw)
To: stable; +Cc: Chuck Lever, NeilBrown, Jeff Layton, Sasha Levin
From: Chuck Lever <cel@kernel.org>
[ Upstream commit 2108de53568a64936a0da3e04d85c35df98d3fb6 ]
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: 2eac189bb059 ("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>
Stable-dep-of: 9026932ac8be ("NFSD: Prevent client use-after-free during blocked-lock reaping")
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
fs/nfsd/nfs4state.c | 13 ++++++++++---
1 file changed, 10 insertions(+), 3 deletions(-)
diff --git a/fs/nfsd/nfs4state.c b/fs/nfsd/nfs4state.c
index 8b0feceee5ac6..fd97042abbb2e 100644
--- a/fs/nfsd/nfs4state.c
+++ b/fs/nfsd/nfs4state.c
@@ -1973,10 +1973,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);
@@ -1984,6 +1988,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;
}
}
--
2.53.0
^ permalink raw reply related [flat|nested] 7+ messages in thread* [PATCH 7.2.y 4/6] NFSD: Prevent client use-after-free during NFSv4.0 revoked-state cleanup
2026-09-07 20:18 ` [PATCH 7.2.y 1/6] NFSD: Prevent client use-after-free during delegation revoke Sasha Levin
2026-09-07 20:18 ` [PATCH 7.2.y 2/6] NFSD: Prevent client use-after-free during admin state revocation Sasha Levin
2026-09-07 20:18 ` [PATCH 7.2.y 3/6] NFSD: Prevent client use-after-free during export " Sasha Levin
@ 2026-09-07 20:18 ` Sasha Levin
2026-09-07 20:18 ` [PATCH 7.2.y 5/6] NFSD: Consolidate the revocation-path client unpin Sasha Levin
2026-09-07 20:18 ` [PATCH 7.2.y 6/6] NFSD: Prevent client use-after-free during blocked-lock reaping Sasha Levin
4 siblings, 0 replies; 7+ messages in thread
From: Sasha Levin @ 2026-09-07 20:18 UTC (permalink / raw)
To: stable; +Cc: Chuck Lever, NeilBrown, Jeff Layton, Sasha Levin
From: Chuck Lever <cel@kernel.org>
[ Upstream commit 7b4f8a1586c42d3afc3c0ac779af2db7ab1a5c55 ]
nfs40_clean_admin_revoked() takes a stateid reference under
clp->cl_lock, drops nn->client_lock, and calls
nfsd4_drop_revoked_stid(), which dereferences the stateid's client
through s->sc_client->cl_lock. The stateid reference does not pin the
client, so a teardown racing the dropped lock can free the client
while nfsd4_drop_revoked_stid() is still using it.
This cleanup runs from the laundromat, so a periodic sweep can race
force_expire_client() driven by a write to the clients/<id>/ctl file.
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: d688d8585e6b ("nfsd: allow admin-revoked NFSv4.0 state to be freed.")
Cc: stable@vger.kernel.org
Reviewed-by: NeilBrown <neil@brown.name>
Reviewed-by: Jeff Layton <jlayton@kernel.org>
Link: https://patch.msgid.link/20260709-cel-v4-5-1d519d9be0cb@kernel.org
Signed-off-by: Chuck Lever <cel@kernel.org>
Stable-dep-of: 9026932ac8be ("NFSD: Prevent client use-after-free during blocked-lock reaping")
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
fs/nfsd/nfs4state.c | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/fs/nfsd/nfs4state.c b/fs/nfsd/nfs4state.c
index fd97042abbb2e..e26ea5b6295c2 100644
--- a/fs/nfsd/nfs4state.c
+++ b/fs/nfsd/nfs4state.c
@@ -7057,16 +7057,22 @@ static void nfs40_clean_admin_revoked(struct nfsd_net *nn,
if (atomic_read(&clp->cl_admin_revoked) == 0)
continue;
+ if (is_client_expired(clp))
+ continue;
spin_lock(&clp->cl_lock);
idr_for_each_entry_ul(&clp->cl_stateids, stid, tmp, id)
if (stid->sc_status & SC_STATUS_ADMIN_REVOKED) {
refcount_inc(&stid->sc_count);
+ atomic_inc(&clp->cl_rpc_users);
spin_unlock(&nn->client_lock);
/* this function drops ->cl_lock */
nfsd4_drop_revoked_stid(stid);
nfs4_put_stid(stid);
spin_lock(&nn->client_lock);
+ if (atomic_dec_and_test(&clp->cl_rpc_users) &&
+ is_client_expired(clp))
+ wake_up_all(&expiry_wq);
goto retry;
}
spin_unlock(&clp->cl_lock);
--
2.53.0
^ permalink raw reply related [flat|nested] 7+ messages in thread* [PATCH 7.2.y 5/6] NFSD: Consolidate the revocation-path client unpin
2026-09-07 20:18 ` [PATCH 7.2.y 1/6] NFSD: Prevent client use-after-free during delegation revoke Sasha Levin
` (2 preceding siblings ...)
2026-09-07 20:18 ` [PATCH 7.2.y 4/6] NFSD: Prevent client use-after-free during NFSv4.0 revoked-state cleanup Sasha Levin
@ 2026-09-07 20:18 ` Sasha Levin
2026-09-07 20:18 ` [PATCH 7.2.y 6/6] NFSD: Prevent client use-after-free during blocked-lock reaping Sasha Levin
4 siblings, 0 replies; 7+ messages in thread
From: Sasha Levin @ 2026-09-07 20:18 UTC (permalink / raw)
To: stable; +Cc: Chuck Lever, NeilBrown, Jeff Layton, Sasha Levin
From: Chuck Lever <cel@kernel.org>
[ Upstream commit 3308cf3f11ed23c79f9f3f90b34bbbad3e3a6ea9 ]
The client use-after-free fixes in the state-revocation paths left
four open-coded copies of one idiom: drop a cl_rpc_users pin without
renewing the client's lease, waking force_expire_client() when the
last pin drops on a client it is tearing down. The accompanying "do
not renew" rationale was documented at only one of the four sites.
put_client_renew_locked() and put_client_renew() already carry the
same pin-drop logic, but they renew a non-expired client's lease and
so would resurrect the client whose state is being revoked. Factor
the common pin-drop into __put_client_locked(), parameterized by
whether to renew. The renew helpers pass true; the new
put_client_no_renew_locked() and put_client_no_renew() pass false and
carry the revocation paths, which must not revive the client they are
tearing down. No change in behavior.
Reviewed-by: NeilBrown <neil@brown.name>
Reviewed-by: Jeff Layton <jlayton@kernel.org>
Link: https://patch.msgid.link/20260709-cel-v4-6-1d519d9be0cb@kernel.org
Signed-off-by: Chuck Lever <cel@kernel.org>
Stable-dep-of: 9026932ac8be ("NFSD: Prevent client use-after-free during blocked-lock reaping")
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
fs/nfsd/nfs4state.c | 69 +++++++++++++++++++++++++++------------------
1 file changed, 41 insertions(+), 28 deletions(-)
diff --git a/fs/nfsd/nfs4state.c b/fs/nfsd/nfs4state.c
index e26ea5b6295c2..9fde3505fac52 100644
--- a/fs/nfsd/nfs4state.c
+++ b/fs/nfsd/nfs4state.c
@@ -202,18 +202,28 @@ renew_client_locked(struct nfs4_client *clp)
clp->cl_state = NFSD4_ACTIVE;
}
+/*
+ * Finish a cl_rpc_users unpin with the client_lock held. A
+ * revocation walk clears @renew so the client whose state it is
+ * revoking is not revived; every other caller renews the lease of
+ * a still-active client.
+ */
+static void __put_client_locked(struct nfs4_client *clp, bool renew)
+{
+ if (is_client_expired(clp))
+ wake_up_all(&expiry_wq);
+ else if (renew)
+ renew_client_locked(clp);
+}
+
static void put_client_renew_locked(struct nfs4_client *clp)
{
struct nfsd_net *nn = net_generic(clp->net, nfsd_net_id);
lockdep_assert_held(&nn->client_lock);
- if (!atomic_dec_and_test(&clp->cl_rpc_users))
- return;
- if (!is_client_expired(clp))
- renew_client_locked(clp);
- else
- wake_up_all(&expiry_wq);
+ if (atomic_dec_and_test(&clp->cl_rpc_users))
+ __put_client_locked(clp, true);
}
static void put_client_renew(struct nfs4_client *clp)
@@ -222,10 +232,27 @@ static void put_client_renew(struct nfs4_client *clp)
if (!atomic_dec_and_lock(&clp->cl_rpc_users, &nn->client_lock))
return;
- if (!is_client_expired(clp))
- renew_client_locked(clp);
- else
- wake_up_all(&expiry_wq);
+ __put_client_locked(clp, true);
+ spin_unlock(&nn->client_lock);
+}
+
+static void put_client_no_renew_locked(struct nfs4_client *clp)
+{
+ struct nfsd_net *nn = net_generic(clp->net, nfsd_net_id);
+
+ lockdep_assert_held(&nn->client_lock);
+
+ if (atomic_dec_and_test(&clp->cl_rpc_users))
+ __put_client_locked(clp, false);
+}
+
+static void put_client_no_renew(struct nfs4_client *clp)
+{
+ struct nfsd_net *nn = net_generic(clp->net, nfsd_net_id);
+
+ if (!atomic_dec_and_lock(&clp->cl_rpc_users, &nn->client_lock))
+ return;
+ __put_client_locked(clp, false);
spin_unlock(&nn->client_lock);
}
@@ -1914,9 +1941,7 @@ void nfsd4_revoke_states(struct nfsd_net *nn, struct super_block *sb)
*/
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);
+ put_client_no_renew_locked(clp);
goto retry;
}
}
@@ -1988,9 +2013,7 @@ 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);
+ put_client_no_renew_locked(clp);
goto retry;
}
}
@@ -7070,9 +7093,7 @@ static void nfs40_clean_admin_revoked(struct nfsd_net *nn,
nfsd4_drop_revoked_stid(stid);
nfs4_put_stid(stid);
spin_lock(&nn->client_lock);
- if (atomic_dec_and_test(&clp->cl_rpc_users) &&
- is_client_expired(clp))
- wake_up_all(&expiry_wq);
+ put_client_no_renew_locked(clp);
goto retry;
}
spin_unlock(&clp->cl_lock);
@@ -7145,15 +7166,7 @@ nfs4_laundromat(struct nfsd_net *nn)
clp = dp->dl_stid.sc_client;
list_del_init(&dp->dl_recall_lru);
revoke_delegation(dp);
- /*
- * Unpin without renewing: put_client_renew() would
- * renew the reaped client's lease.
- */
- if (atomic_dec_and_lock(&clp->cl_rpc_users, &nn->client_lock)) {
- if (is_client_expired(clp))
- wake_up_all(&expiry_wq);
- spin_unlock(&nn->client_lock);
- }
+ put_client_no_renew(clp);
}
spin_lock(&nn->client_lock);
--
2.53.0
^ permalink raw reply related [flat|nested] 7+ messages in thread* [PATCH 7.2.y 6/6] NFSD: Prevent client use-after-free during blocked-lock reaping
2026-09-07 20:18 ` [PATCH 7.2.y 1/6] NFSD: Prevent client use-after-free during delegation revoke Sasha Levin
` (3 preceding siblings ...)
2026-09-07 20:18 ` [PATCH 7.2.y 5/6] NFSD: Consolidate the revocation-path client unpin Sasha Levin
@ 2026-09-07 20:18 ` Sasha Levin
4 siblings, 0 replies; 7+ messages in thread
From: Sasha Levin @ 2026-09-07 20:18 UTC (permalink / raw)
To: stable; +Cc: Chuck Lever, Jeff Layton, Sasha Levin
From: Chuck Lever <cel@kernel.org>
[ Upstream commit 9026932ac8be4d0ae01db47f23619a98cc57b671 ]
A bare lock owner -- its only remaining reference a blocked lock on
nn->blocked_locks_lru -- holds a raw pointer to its nfs4_client but
no reference keeping the client alive. When the per-net laundromat
reaps such a lock, freeing the nbl drops the owner reference
held through flc_owner, and the final nfs4_put_stateowner()
takes the client's cl_lock. Because the laundromat detaches the
nbl first, __destroy_client() no longer finds it, so a concurrent
force_expire_client() can free the client before nfs4_put_stateowner()
runs, dereferencing cl_lock in freed memory.
Pin the client with cl_rpc_users before dropping
nn->blocked_locks_lock, and skip clients already expiring, whose
blocked locks __destroy_client() frees while holding an owner
reference. Take nn->client_lock outside nn->blocked_locks_lock.
Every other site holds nn->blocked_locks_lock as a leaf, acquiring
no further lock, so placing nn->client_lock outside it cannot form
a lock-order cycle.
Fixes: 7919d0a27f1e ("nfsd: add a LRU list for blocked locks")
Cc: stable@vger.kernel.org
Reviewed-by: Jeff Layton <jlayton@kernel.org>
Link: https://patch.msgid.link/20260709-cel-v4-7-1d519d9be0cb@kernel.org
Signed-off-by: Chuck Lever <cel@kernel.org>
Signed-off-by: Sasha Levin <sashal@kernel.org>
---
fs/nfsd/nfs4state.c | 23 ++++++++++++++++++++---
1 file changed, 20 insertions(+), 3 deletions(-)
diff --git a/fs/nfsd/nfs4state.c b/fs/nfsd/nfs4state.c
index 9fde3505fac52..d682d65d74128 100644
--- a/fs/nfsd/nfs4state.c
+++ b/fs/nfsd/nfs4state.c
@@ -353,6 +353,16 @@ free_blocked_lock(struct nfsd4_blocked_lock *nbl)
kref_put(&nbl->nbl_kref, free_nbl);
}
+/* A blocked lock's flc_owner is its nfs4_lockowner. */
+static struct nfs4_client *
+nbl_client(struct nfsd4_blocked_lock *nbl)
+{
+ struct nfs4_lockowner *lo;
+
+ lo = (struct nfs4_lockowner *)nbl->nbl_lock.c.flc_owner;
+ return lo->lo_owner.so_client;
+}
+
static void
remove_blocked_locks(struct nfs4_lockowner *lo)
{
@@ -7196,22 +7206,29 @@ nfs4_laundromat(struct nfsd_net *nn)
* indefinitely once the lock does become free.
*/
BUG_ON(!list_empty(&reaplist));
+ spin_lock(&nn->client_lock);
spin_lock(&nn->blocked_locks_lock);
- while (!list_empty(&nn->blocked_locks_lru)) {
- nbl = list_first_entry(&nn->blocked_locks_lru,
- struct nfsd4_blocked_lock, nbl_lru);
+ list_for_each_safe(pos, next, &nn->blocked_locks_lru) {
+ nbl = list_entry(pos, struct nfsd4_blocked_lock, nbl_lru);
if (!state_expired(<, nbl->nbl_time))
break;
+ clp = nbl_client(nbl);
+ if (is_client_expired(clp))
+ continue;
+ atomic_inc(&clp->cl_rpc_users);
list_move(&nbl->nbl_lru, &reaplist);
list_del_init(&nbl->nbl_list);
}
spin_unlock(&nn->blocked_locks_lock);
+ spin_unlock(&nn->client_lock);
while (!list_empty(&reaplist)) {
nbl = list_first_entry(&reaplist,
struct nfsd4_blocked_lock, nbl_lru);
+ clp = nbl_client(nbl);
list_del_init(&nbl->nbl_lru);
free_blocked_lock(nbl);
+ put_client_no_renew(clp);
}
#ifdef CONFIG_NFSD_V4_2_INTER_SSC
/* service the server-to-server copy delayed unmount list */
--
2.53.0
^ permalink raw reply related [flat|nested] 7+ messages in thread