FAILED: patch "[PATCH] NFSD: Prevent client use-after-free during close_lru reaping" failed to apply to 7.2-stable tree
Subsystems (2)
[email protected]
maintainerThe patch below does not apply to the 7.2-stable tree.
If someone wants it applied there, or to any other stable or longterm tree, then please email the backport, including the original git commit id to <[email protected]>.
To reproduce the conflict and resubmit, you may use the following commands:
git fetch https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/ linux-7.2.y
git checkout FETCH_HEAD
git cherry-pick -x 2330b788d732f43668b965b3105b37ceb276dfea
# <resolve conflicts, build, test, etc.>
git commit -s
git send-email --to '<[email protected]>' --in-reply-to '2026090304-open-erasable-9a07@gregkh' --subject-prefix 'PATCH 7.2.y' 'HEAD^..'
Possible dependencies:
thanks,
greg k-h
------------------ original commit in Linus's tree ------------------
From 2330b788d732f43668b965b3105b37ceb276dfea Mon Sep 17 00:00:00 2001
From: Chuck Lever <[email protected]>
Date: Thu, 9 Jul 2026 13:40:31 -0400
Subject: [PATCH] NFSD: Prevent client use-after-free during close_lru reaping
An nfs4_openowner left on nn->close_lru after its final CLOSE keeps its last closed stateid in oo_last_closed_stid, holding only a raw pointer to its nfs4_client. The laundromat reaps timed-out entries, drops nn->client_lock, and calls nfs4_put_stid(), which dereferences the client through cl_lock. Nothing pins the client across that window, so a concurrent force_expire_client() can free it and nfs4_put_stid() reads freed memory. __destroy_client() hits the same race, walking clp->cl_openowners without cl_lock.
Pin the client with cl_rpc_users before dropping client_lock, and skip clients already expiring. __destroy_client() then cleans up its own close_lru entries through release_last_closed_stateid(), so teardown no longer races the laundromat.
fs/nfsd/nfs4state.c +5 -0
@@ -7581,11 +7581,16 @@ nfs4_laundromat(struct nfsd_net *nn)if (!state_expired (<, oo->oo_time))break ;list_del_init (&oo->oo_close_lru); + clp = oo->oo_owner.so_client; +if (is_client_expired (clp)) +continue ; stp = oo->oo_last_closed_stid; oo->oo_last_closed_stid =NULL ; +atomic_inc (&clp->cl_rpc_users);spin_unlock (&nn->client_lock);nfs4_put_stid (&stp->st_stid);spin_lock (&nn->client_lock); +put_client_no_renew_locked (clp); }spin_unlock (&nn->client_lock);
Sasha Levin
maintainerPATCH 1/6 NFSD: Prevent client use-after-free during delegation revoke
From: Chuck Lever <[email protected]>
[ Upstream commit 4683ca76b3b7e5808338491c6eb3c20e6b4894d5 ]
A delegation stateid holds only a bare pointer to its owning nfs4_client and does not keep it alive. The client survives its stateids only because __destroy_client() drains cl_delegations and cl_revoked before free_client() runs.
nfs4_laundromat() breaks that invariant: it unhashes an expired delegation from cl_delegations, drops deleg_lock, then revoke_delegation() relinks it onto cl_revoked under cl_lock. In that window the delegation is on neither list, so client_has_state() can report no remaining state.
Every teardown path first requires cl_rpc_users to be zero, but the laundromat holds no such reference. A client whose recalled delegation has just timed out can therefore reach free_client() while revoke_delegation() is still about to dereference cl_lock, a use-after-free.
Pin the client with cl_rpc_users across the revoke so teardown blocks until it completes, then reap the delegation from cl_revoked. A client already expiring reaps its own, so skip it and leave the delegation on del_recall_lru.
Stable-dep-of: 2330b788d732 ("NFSD: Prevent client use-after-free during close_lru reaping")
--- fs/nfsd/netns.h | 6 ++++-- fs/nfsd/nfs4state.c | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-)
fs/nfsd/netns.h +4 -2
@@ -101,7 +101,8 @@ struct nfsd_net {struct list_head client_lru;struct list_head close_lru; -/* protects del_recall_lru and delegation hash/unhash */ +/* protects del_recall_lru and delegation hash/unhash; +* nests outside client_lock */ spinlock_t deleg_lock____cacheline_aligned ;struct list_head del_recall_lru;
@@ -110,7 +111,8 @@ struct nfsd_net {struct delayed_work laundromat_work; -/* client_lock protects the client lru list and session hash table */ +/* client_lock protects the client lru list and session hash +* table; nests inside deleg_lock */ spinlock_t client_lock;/* protects blocked_locks_lru */
fs/nfsd/nfs4state.c +23 -0
@@ -7062,6 +7062,7 @@ nfs4_laundromat(struct nfsd_net *nn) .new_timeo = nn->nfsd4_lease };struct nfs4_cpntf_state *cps; +struct nfs4_client *clp;copy_stateid_t *cps_t;int i;
@@ -7090,6 +7091,18 @@ nfs4_laundromat(struct nfsd_net *nn) dp = list_entry (pos, struct nfs4_delegation, dl_recall_lru);if (!state_expired (<, dp->dl_time))break ; + clp = dp->dl_stid.sc_client; +spin_lock (&nn->client_lock); +if (is_client_expired (clp)) { +spin_unlock (&nn->client_lock); +continue ; + } +/* +* Pin without reviving: get_client_locked() would +* flip a courtesy client back to NFSD4_ACTIVE. +*/ +atomic_inc (&clp->cl_rpc_users); +spin_unlock (&nn->client_lock);refcount_inc (&dp->dl_stid.sc_count);unhash_delegation_locked (dp,SC_STATUS_REVOKED );list_add (&dp->dl_recall_lru, &reaplist);
@@ -7098,8 +7111,18 @@ nfs4_laundromat(struct nfsd_net *nn)while (!list_empty (&reaplist)) { dp = list_first_entry(&reaplist, struct nfs4_delegation, dl_recall_lru); + 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); + } }spin_lock (&nn->client_lock);
Signature · 1 line
2.53.0
Sasha Levin
maintainerPATCH 2/6 NFSD: Prevent client use-after-free during admin state revocation
From: Chuck Lever <[email protected]>
[ 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.
Stable-dep-of: 2330b788d732 ("NFSD: Prevent client use-after-free during close_lru reaping")
--- fs/nfsd/nfs4state.c | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-)
fs/nfsd/nfs4state.c +20 -2
@@ -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);
Signature · 1 line
2.53.0
Sasha Levin
maintainerPATCH 3/6 NFSD: Prevent client use-after-free during export state revocation
In reply to Sasha LevinFrom: Chuck Lever <[email protected]>
[ 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().
Stable-dep-of: 2330b788d732 ("NFSD: Prevent client use-after-free during close_lru reaping")
--- fs/nfsd/nfs4state.c | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-)
fs/nfsd/nfs4state.c +10 -3
@@ -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; } }
Signature · 1 line
2.53.0
Sasha Levin
maintainerPATCH 4/6 NFSD: Prevent client use-after-free during NFSv4.0 revoked-state cleanup
In reply to Sasha LevinFrom: Chuck Lever <[email protected]>
[ 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().
Stable-dep-of: 2330b788d732 ("NFSD: Prevent client use-after-free during close_lru reaping")
--- fs/nfsd/nfs4state.c | 6 ++++++ 1 file changed, 6 insertions(+)
fs/nfsd/nfs4state.c +6 -0
@@ -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);
Signature · 1 line
2.53.0
Sasha Levin
maintainerPATCH 5/6 NFSD: Consolidate the revocation-path client unpin
In reply to Sasha LevinFrom: Chuck Lever <[email protected]>
[ 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.
Stable-dep-of: 2330b788d732 ("NFSD: Prevent client use-after-free during close_lru reaping")
--- fs/nfsd/nfs4state.c | 69 +++++++++++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 28 deletions(-)
fs/nfsd/nfs4state.c +41 -28
@@ -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);
Signature · 1 line
2.53.0
Sasha Levin
maintainerPATCH 6/6 NFSD: Prevent client use-after-free during close_lru reaping
In reply to Sasha LevinFrom: Chuck Lever <[email protected]>
[ Upstream commit 2330b788d732f43668b965b3105b37ceb276dfea ]
An nfs4_openowner left on nn->close_lru after its final CLOSE keeps its last closed stateid in oo_last_closed_stid, holding only a raw pointer to its nfs4_client. The laundromat reaps timed-out entries, drops nn->client_lock, and calls nfs4_put_stid(), which dereferences the client through cl_lock. Nothing pins the client across that window, so a concurrent force_expire_client() can free it and nfs4_put_stid() reads freed memory. __destroy_client() hits the same race, walking clp->cl_openowners without cl_lock.
Pin the client with cl_rpc_users before dropping client_lock, and skip clients already expiring. __destroy_client() then cleans up its own close_lru entries through release_last_closed_stateid(), so teardown no longer races the laundromat.
--- fs/nfsd/nfs4state.c | 5 +++++ 1 file changed, 5 insertions(+)
fs/nfsd/nfs4state.c +5 -0
@@ -7176,11 +7176,16 @@ nfs4_laundromat(struct nfsd_net *nn)if (!state_expired (<, oo->oo_time))break ;list_del_init (&oo->oo_close_lru); + clp = oo->oo_owner.so_client; +if (is_client_expired (clp)) +continue ; stp = oo->oo_last_closed_stid; oo->oo_last_closed_stid =NULL ; +atomic_inc (&clp->cl_rpc_users);spin_unlock (&nn->client_lock);nfs4_put_stid (&stp->st_stid);spin_lock (&nn->client_lock); +put_client_no_renew_locked (clp); }spin_unlock (&nn->client_lock);
Signature · 1 line
2.53.0