FAILED: patch "[PATCH] nfsd: close shrinker/GC/fsnotify vs per-net shutdown race in" failed to apply to 6.12-stable tree
Subsystems (2)
[email protected]
maintainerThe patch below does not apply to the 6.12-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-6.12.y
git checkout FETCH_HEAD
git cherry-pick -x 40162cfea79b9510380decfdd1795b754dc9f972
# <resolve conflicts, build, test, etc.>
git commit -s
git send-email --to '<[email protected]>' --in-reply-to '2026090354-amends-antarctic-06e4@gregkh' --subject-prefix 'PATCH 6.12.y' 'HEAD^..'
Possible dependencies:
thanks,
greg k-h
------------------ original commit in Linus's tree ------------------
From 40162cfea79b9510380decfdd1795b754dc9f972 Mon Sep 17 00:00:00 2001
From: Jeff Layton <[email protected]>
Date: Thu, 4 Jun 2026 10:31:09 -0400
Subject: [PATCH] nfsd: close shrinker/GC/fsnotify vs per-net shutdown race in
filecache
The shrinker, GC worker, and fsnotify/lease callbacks can unhash an nfsd_file from the rhashtable and then call nfsd_file_dispose_list_delayed() to move it to the per-net dispose list. If nfsd_file_cache_shutdown_net() runs concurrently, its rhashtable walk misses the already-unhashed file, and its drain of the per-net dispose list can run before the file has been queued. The file then sits on the per-net list with no thread to drain it, leaking both the file and its associated state.
The GC worker and shrinker already hold nfsd_gc_lock while walking the LRU, but in the original code they release it before calling nfsd_file_dispose_list_delayed(). The fsnotify/lease path (nfsd_file_close_inode) has no synchronization at all.
Fix this by:
1. Widening nfsd_gc_lock in both nfsd_file_gc() and nfsd_file_lru_scan() to cover the nfsd_file_dispose_list_delayed() call.
2. Wrapping nfsd_file_close_inode() in nfsd_gc_lock so that all three callers of nfsd_file_dispose_list_delayed() hold the lock.
3. Adding a spin_lock/unlock(nfsd_gc_lock) barrier in nfsd_file_cache_shutdown_net() after the purge, so that any in-progress disposal has fully completed before the per-net list is drained.
All operations inside the lock are non-sleeping (rhashtable lookups, atomic bit/refcount ops, list moves, svc_wake_up), so the spinlock is appropriate.
fs/nfsd/filecache.c +29 -9
@@ -55,6 +55,17 @@/* We only care about NFSD_MAY_READ/WRITE for this cache */ #define NFSD_FILE_MAY_MASK (NFSD_MAY_READ|NFSD_MAY_WRITE|NFSD_MAY_LOCALIO) +/* If the shrinker runs between calls to list_lru_walk_node() in +* nfsd_file_gc(), the "remaining" count will be wrong. This could +* result in premature freeing of some files. This may not matter much +* but is easy to fix with this spinlock which temporarily disables +* the shrinker. +* +* It also serializes callers of nfsd_file_dispose_list_delayed() +* against per-net shutdown. +*/ +static DEFINE_SPINLOCK (nfsd_gc_lock); + static DEFINE_PER_CPU(unsigned long, nfsd_file_cache_hits); static DEFINE_PER_CPU(unsigned long, nfsd_file_acquisitions); static DEFINE_PER_CPU(unsigned long, nfsd_file_allocations);
@@ -423,10 +434,16 @@ nfsd_file_dispose_list(struct list_head *dispose)* Transfers each file to the dispose list in its nfsd_net and wakes an nfsd * thread to do the actual close. This keeps the cost of fput() in the nfsd * threads rather than in the shrinker or GC worker. +* +* All callers must hold nfsd_gc_lock, so that nfsd_file_cache_shutdown_net() +* can synchronize against them before draining the per-net dispose list. +* This guarantees nf_net is still live when we call net_generic(). */ static void nfsd_file_dispose_list_delayed (struct list_head *dispose) { +lockdep_assert_held (&nfsd_gc_lock); +while (!list_empty (dispose)) {struct nfsd_file *nf =list_first_entry (dispose, struct nfsd_file, nf_gc);
@@ -557,13 +574,6 @@ nfsd_file_gc_cb(struct list_head *item, struct list_lru_one *lru,return nfsd_file_lru_cb (item, lru, arg); } -/* If the shrinker runs between calls to list_lru_walk_node() in -* nfsd_file_gc(), the "remaining" count will be wrong. This could -* result in premature freeing of some files. This may not matter much -* but is easy to fix with this spinlock which temporarily disables -* the shrinker. -*/ -static DEFINE_SPINLOCK (nfsd_gc_lock);static void nfsd_file_gc (void ) {
@@ -586,9 +596,9 @@ nfsd_file_gc(void) remaining =0 ; } } +nfsd_file_dispose_list_delayed (&dispose);spin_unlock (&nfsd_gc_lock);trace_nfsd_file_gc_removed (ret,list_lru_count (&nfsd_file_lru)); -nfsd_file_dispose_list_delayed (&dispose); } static void
@@ -616,9 +626,9 @@ nfsd_file_lru_scan(struct shrinker *s, struct shrink_control *sc) ret =list_lru_shrink_walk (&nfsd_file_lru, sc, nfsd_file_lru_cb, &dispose); +nfsd_file_dispose_list_delayed (&dispose);spin_unlock (&nfsd_gc_lock);trace_nfsd_file_shrinker_removed (ret,list_lru_count (&nfsd_file_lru)); -nfsd_file_dispose_list_delayed (&dispose);return ret; }
@@ -704,8 +714,10 @@ nfsd_file_close_inode(struct inode *inode) {LIST_HEAD (dispose); +spin_lock (&nfsd_gc_lock);nfsd_file_queue_for_close (inode, &dispose);nfsd_file_dispose_list_delayed (&dispose); +spin_unlock (&nfsd_gc_lock); }/**
@@ -974,6 +986,14 @@ nfsd_file_cache_shutdown_net(struct net *net)struct nfsd_net *nn =net_generic (net, nfsd_net_id);nfsd_file_cache_purge (net); +/* +* Ensure any in-progress shrinker, GC, or fsnotify/lease callback +* (all of which hold nfsd_gc_lock while calling +* nfsd_file_dispose_list_delayed()) has fully completed before +* draining the per-net dispose list. +*/ +spin_lock (&nfsd_gc_lock); +spin_unlock (&nfsd_gc_lock);nfsd_file_dispose_list (&nn->fcache_dispose_list); }
Sasha Levin
maintainerPATCH 1/2 nfsd: guard nfsd_serv deref in nfsd_file_net_dispose
From: Chris Mason <[email protected]>
[ Upstream commit 9f1ddfc8cb9076592401a611eb3a44d36186d014 ]
nfsd_file_net_dispose() is the consumer side of l->freeme: the nfsd service thread loop calls it to drain entries that the filecache garbage collector and shrinker append via nfsd_file_dispose_list_delayed(). During per-net teardown, nn->nfsd_serv is cleared before the filecache laundrette is shut down, so the service thread can still run a dispose pass that finds more than eight entries on l->freeme and dereferences a NULL svc_serv:
nfsd service thread loop
nfsd_file_net_dispose(nn)
if (!list_empty(&l->freeme)) {
...
svc_wake_up(nn->nfsd_serv); /* nn->nfsd_serv == NULL */
}The sibling helper nfsd_file_dispose_list_delayed() already documents this ordering and caches nn->nfsd_serv into a local before testing it for NULL. nfsd_file_net_dispose() was introduced with the same raw svc_wake_up(nn->nfsd_serv) call and never picked up the guard.
Fix by loading nn->nfsd_serv into a local svc_serv pointer and only calling svc_wake_up() when it is non-NULL, matching the pattern in nfsd_file_dispose_list_delayed().
Stable-dep-of: 40162cfea79b ("nfsd: close shrinker/GC/fsnotify vs per-net shutdown race in filecache")
--- fs/nfsd/filecache.c | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-)
fs/nfsd/filecache.c +12 -3
@@ -483,11 +483,20 @@ void nfsd_file_net_dispose(struct nfsd_net *nn)for (i =0 ; i <8 && !list_empty (&l->freeme); i++)list_move (l->freeme.next, &dispose);spin_unlock (&l->lock); -if (!list_empty (&l->freeme)) -/* Wake up another thread to share the work +if (!list_empty (&l->freeme)) { +/* +* Wake up another thread to share the work * *before* doing any actual disposing. +* +* The filecache laundrette is shut down after +* the nn->nfsd_serv pointer is cleared, but +* before the svc_serv is freed. */ -svc_wake_up (nn->nfsd_serv); +struct svc_serv *serv = nn->nfsd_serv; + +if (serv) +svc_wake_up (serv); + }nfsd_file_dispose_list (&dispose); } }
Signature · 1 line
2.53.0
Sasha Levin
maintainerPATCH 2/2 nfsd: close shrinker/GC/fsnotify vs per-net shutdown race in filecache
From: Jeff Layton <[email protected]>
[ Upstream commit 40162cfea79b9510380decfdd1795b754dc9f972 ]
The shrinker, GC worker, and fsnotify/lease callbacks can unhash an nfsd_file from the rhashtable and then call nfsd_file_dispose_list_delayed() to move it to the per-net dispose list. If nfsd_file_cache_shutdown_net() runs concurrently, its rhashtable walk misses the already-unhashed file, and its drain of the per-net dispose list can run before the file has been queued. The file then sits on the per-net list with no thread to drain it, leaking both the file and its associated state.
The GC worker and shrinker already hold nfsd_gc_lock while walking the LRU, but in the original code they release it before calling nfsd_file_dispose_list_delayed(). The fsnotify/lease path (nfsd_file_close_inode) has no synchronization at all.
Fix this by:
1. Widening nfsd_gc_lock in both nfsd_file_gc() and nfsd_file_lru_scan() to cover the nfsd_file_dispose_list_delayed() call.
2. Wrapping nfsd_file_close_inode() in nfsd_gc_lock so that all three callers of nfsd_file_dispose_list_delayed() hold the lock.
3. Adding a spin_lock/unlock(nfsd_gc_lock) barrier in nfsd_file_cache_shutdown_net() after the purge, so that any in-progress disposal has fully completed before the per-net list is drained.
All operations inside the lock are non-sleeping (rhashtable lookups, atomic bit/refcount ops, list moves, svc_wake_up), so the spinlock is appropriate.
--- fs/nfsd/filecache.c | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-)
fs/nfsd/filecache.c +31 -3
@@ -54,6 +54,12 @@/* We only care about NFSD_MAY_READ/WRITE for this cache */ #define NFSD_FILE_MAY_MASK (NFSD_MAY_READ|NFSD_MAY_WRITE|NFSD_MAY_LOCALIO) +/* +* Serialize callers of nfsd_file_dispose_list_delayed() against per-net +* shutdown. +*/ +static DEFINE_SPINLOCK (nfsd_gc_lock); + static DEFINE_PER_CPU(unsigned long, nfsd_file_cache_hits); static DEFINE_PER_CPU(unsigned long, nfsd_file_acquisitions); static DEFINE_PER_CPU(unsigned long, nfsd_file_allocations);
@@ -436,11 +442,17 @@ nfsd_file_dispose_list(struct list_head *dispose)* * Transfers each file to the "freeme" list for its nfsd_net, to eventually * be disposed of by the per-net garbage collector. +* +* All callers must hold nfsd_gc_lock, so that nfsd_file_cache_shutdown_net() +* can synchronize against them before draining the per-net dispose list. +* This guarantees nf_net is still live when we call net_generic(). */ static void nfsd_file_dispose_list_delayed (struct list_head *dispose) { -while (!list_empty (dispose)) { +lockdep_assert_held (&nfsd_gc_lock); + +while (!list_empty (dispose)) {struct nfsd_file *nf =list_first_entry (dispose, struct nfsd_file, nf_gc);struct nfsd_net *nn =net_generic (nf->nf_net, nfsd_net_id);
@@ -565,10 +577,12 @@ nfsd_file_gc(void)LIST_HEAD (dispose);unsigned long ret; +spin_lock (&nfsd_gc_lock); ret =list_lru_walk (&nfsd_file_lru, nfsd_file_lru_cb, &dispose,list_lru_count (&nfsd_file_lru)); -trace_nfsd_file_gc_removed (ret,list_lru_count (&nfsd_file_lru));nfsd_file_dispose_list_delayed (&dispose); +spin_unlock (&nfsd_gc_lock); +trace_nfsd_file_gc_removed (ret,list_lru_count (&nfsd_file_lru)); } static void
@@ -591,10 +605,14 @@ nfsd_file_lru_scan(struct shrinker *s, struct shrink_control *sc)LIST_HEAD (dispose);unsigned long ret; +if (!spin_trylock (&nfsd_gc_lock)) +return SHRINK_STOP ; + ret =list_lru_shrink_walk (&nfsd_file_lru, sc, nfsd_file_lru_cb, &dispose); -trace_nfsd_file_shrinker_removed (ret,list_lru_count (&nfsd_file_lru));nfsd_file_dispose_list_delayed (&dispose); +spin_unlock (&nfsd_gc_lock); +trace_nfsd_file_shrinker_removed (ret,list_lru_count (&nfsd_file_lru));return ret; }
@@ -680,8 +698,10 @@ nfsd_file_close_inode(struct inode *inode) {LIST_HEAD (dispose); +spin_lock (&nfsd_gc_lock);nfsd_file_queue_for_close (inode, &dispose);nfsd_file_dispose_list_delayed (&dispose); +spin_unlock (&nfsd_gc_lock); }/**
@@ -927,6 +947,14 @@ void nfsd_file_cache_shutdown_net(struct net *net) {nfsd_file_cache_purge (net); +/* +* Ensure any in-progress shrinker, GC, or fsnotify/lease callback +* (all of which hold nfsd_gc_lock while calling +* nfsd_file_dispose_list_delayed()) has fully completed before +* draining the per-net dispose list. +*/ +spin_lock (&nfsd_gc_lock); +spin_unlock (&nfsd_gc_lock);nfsd_free_fcache_disposal_net (net); }
Signature · 1 line
2.53.0