FAILED: patch "[PATCH] nfsd: close shrinker/GC/fsnotify vs per-net shutdown race in" failed to apply to 6.18-stable tree
Subsystems (2)
[email protected]
maintainerThe patch below does not apply to the 6.18-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.18.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 '2026090353-raft-script-514e@gregkh' --subject-prefix 'PATCH 6.18.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/3 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
@@ -471,11 +471,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/3 nfsd: fix fcache_disposal UAF by inlining dispose state into nfsd_net
From: Jeff Layton <[email protected]>
[ Upstream commit bbf13732f74351d21c5e0e8dd9bd8e1c48dc35d4 ]
nfsd_file_dispose_list_delayed() defers fput() to nfsd service threads via a per-net freeme queue, preventing the shrinker and GC worker from bearing the cost of closing files (see ffb402596147). However, the queue lives in a separately-allocated struct nfsd_fcache_disposal that is freed by nfsd_free_fcache_disposal_net() during per-net teardown. The global shrinker, laundrette, and fsnotify callbacks can still be inside nfsd_file_dispose_list_delayed() dereferencing that pointer, causing a use-after-free.
Inline the spinlock and freeme list directly into struct nfsd_net (as fcache_dispose_lock and fcache_dispose_list), eliminating the separately allocated struct nfsd_fcache_disposal entirely. These fields now have the same lifetime as the net namespace itself, so there is no dangling pointer to chase.
nfsd_file_cache_start_net() now just initializes the inline fields and cannot fail due to allocation. nfsd_file_cache_shutdown_net() drains the inline list directly instead of freeing a separate struct. The alloc/free helpers are removed.
Stable-dep-of: 40162cfea79b ("nfsd: close shrinker/GC/fsnotify vs per-net shutdown race in filecache")
--- fs/nfsd/filecache.c | 77 +++++++++++++-------------------------------- fs/nfsd/netns.h | 3 +- 2 files changed, 24 insertions(+), 56 deletions(-)
fs/nfsd/filecache.c +22 -55
@@ -62,11 +62,6 @@ static DEFINE_PER_CPU(unsigned long, nfsd_file_releases); static DEFINE_PER_CPU(unsigned long, nfsd_file_total_age); static DEFINE_PER_CPU(unsigned long, nfsd_file_evictions); -struct nfsd_fcache_disposal { -spinlock_t lock; -struct list_head freeme; -}; -static struct kmem_cache *nfsd_file_slab;static struct kmem_cache *nfsd_file_mark_slab;static struct list_lru nfsd_file_lru;
@@ -419,25 +414,25 @@ nfsd_file_dispose_list(struct list_head *dispose) }/** -* nfsd_file_dispose_list_delayed - move list of dead files to net's freeme list +* nfsd_file_dispose_list_delayed - queue dead files for nfsd thread disposal * @dispose: list of nfsd_files to be disposed * -* Transfers each file to the "freeme" list for its nfsd_net, to eventually -* be disposed of by the per-net garbage collector. +* 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. */ static void nfsd_file_dispose_list_delayed (struct list_head *dispose) { -while (!list_empty (dispose)) { +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); -struct nfsd_fcache_disposal *l = nn->fcache_disposal;struct svc_serv *serv; -spin_lock (&l->lock); -list_move_tail (&nf->nf_gc, &l->freeme); -spin_unlock (&l->lock); +spin_lock (&nn->fcache_dispose_lock); +list_move_tail (&nf->nf_gc, &nn->fcache_dispose_list); +spin_unlock (&nn->fcache_dispose_lock);/* * The filecache laundrette is shut down after the
@@ -461,17 +456,15 @@ nfsd_file_dispose_list_delayed(struct list_head *dispose)*/ void nfsd_file_net_dispose (struct nfsd_net *nn) { -struct nfsd_fcache_disposal *l = nn->fcache_disposal; - -if (!list_empty (&l->freeme)) { +if (!list_empty (&nn->fcache_dispose_list)) {LIST_HEAD (dispose);int i; -spin_lock (&l->lock); -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)) { +spin_lock (&nn->fcache_dispose_lock); +for (i =0 ; i <8 && !list_empty (&nn->fcache_dispose_list); i++) +list_move (nn->fcache_dispose_list.next, &dispose); +spin_unlock (&nn->fcache_dispose_lock); +if (!list_empty (&nn->fcache_dispose_list)) {/* * Wake up another thread to share the work * *before* doing any actual disposing.
@@ -695,11 +688,11 @@ nfsd_file_queue_for_close(struct inode *inode, struct list_head *dispose) }/** -* nfsd_file_close_inode - attempt a delayed close of a nfsd_file +* nfsd_file_close_inode - attempt a deferred close of a nfsd_file * @inode: inode of the file to attempt to remove * * Close out any open nfsd_files that can be reaped for @inode. The -* actual freeing is deferred to the dispose_list_delayed infrastructure. +* actual freeing is deferred to the nfsd service threads. * * This is used by the fsnotify callbacks and setlease notifier. */
@@ -903,42 +896,14 @@ __nfsd_file_cache_purge(struct net *net)nfsd_file_dispose_list (&dispose); } -static struct nfsd_fcache_disposal * -nfsd_alloc_fcache_disposal (void ) -{ -struct nfsd_fcache_disposal *l; - - l =kmalloc (sizeof (*l),GFP_KERNEL ); -if (!l) -return NULL ; -spin_lock_init (&l->lock); -INIT_LIST_HEAD (&l->freeme); -return l; -} - -static void -nfsd_free_fcache_disposal (struct nfsd_fcache_disposal *l) -{ -nfsd_file_dispose_list (&l->freeme); -kfree (l); -} - -static void -nfsd_free_fcache_disposal_net (struct net *net) -{ -struct nfsd_net *nn =net_generic (net, nfsd_net_id); -struct nfsd_fcache_disposal *l = nn->fcache_disposal; - -nfsd_free_fcache_disposal (l); -} -int nfsd_file_cache_start_net (struct net *net) {struct nfsd_net *nn =net_generic (net, nfsd_net_id); - nn->fcache_disposal =nfsd_alloc_fcache_disposal (); -return nn->fcache_disposal ?0 : -ENOMEM ; +spin_lock_init (&nn->fcache_dispose_lock); +INIT_LIST_HEAD (&nn->fcache_dispose_list); +return 0 ; }/**
@@ -957,8 +922,10 @@ nfsd_file_cache_purge(struct net *net)void nfsd_file_cache_shutdown_net (struct net *net) { +struct nfsd_net *nn =net_generic (net, nfsd_net_id); +nfsd_file_cache_purge (net); -nfsd_free_fcache_disposal_net (net); +nfsd_file_dispose_list (&nn->fcache_dispose_list); }void
fs/nfsd/netns.h +2 -1
@@ -200,7 +200,8 @@ struct nfsd_net {/* utsname taken from the process that starts the server */ char nfsd_name[UNX_MAXNODENAME +1 ]; -struct nfsd_fcache_disposal *fcache_disposal; +spinlock_t fcache_dispose_lock; +struct list_head fcache_dispose_list;siphash_key_t siphash_key;
Signature · 1 line
2.53.0
Sasha Levin
maintainerPATCH 3/3 nfsd: close shrinker/GC/fsnotify vs per-net shutdown race in filecache
In reply to Sasha LevinFrom: 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 | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-)
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);
@@ -420,10 +431,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);
@@ -554,13 +571,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 ) {
@@ -583,9 +593,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
@@ -613,9 +623,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; }
@@ -701,8 +711,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); }/**
@@ -925,6 +937,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); }
Signature · 1 line
2.53.0