inet: frags: flush pending skbs in fqdir_pre_exit()
Subsystems (4)
Jakub Kicinski
maintainercover inet: frags: flush pending skbs in fqdir_pre_exit()
Fix the issue reported by NIPA starting on Sep 18th [1], where pernet_ops_rwsem is constantly held by a reader, preventing writers from grabbing it (specifically driver modules from loading).
The fact that reports started around that time seems coincidental. The issue seems to be skbs queued for defrag preventing conntrack from exiting.
First patch fixes another theoretical issue, it's mostly a leftover from an attempt to get rid of the inet_frag_queue refcnt, which I gave up on (still think it's doable but a bit of a time sink). Second patch is a minor refactor.
The real fix is in the third patch. It's the simplest fix I can think of which is to flush the frag queues. Perhaps someone has a better suggestion?
Last patch adds an explicit warning for conntrack getting stuck, as this seems like something that can easily happen if bugs sneak in. The warning will hopefully save us the first 20% of the investigation effort.
Jakub Kicinski (4):
inet: frags: avoid theoretical race in ip_frag_reinit()
inet: frags: add inet_frag_queue_flush()
inet: frags: flush pending skbs in fqdir_pre_exit()
netfilter: conntrack: warn when cleanup is stuck
include/net/inet_frag.h | 18 ++-------- include/net/ipv6_frag.h | 9 +++-- net/ipv4/inet_fragment.c | 55 ++++++++++++++++++++++++++++--- net/ipv4/ip_fragment.c | 22 +++++-------- net/netfilter/nf_conntrack_core.c | 3 ++ 5 files changed, 72 insertions(+), 35 deletions(-)
Signature · 1 line
2.52.0
Jakub Kicinski
maintainerPATCH 1/4 inet: frags: avoid theoretical race in ip_frag_reinit()
In ip_frag_reinit() we want to move the frag timeout timer into the future. If the timer fires in the meantime we inadvertently scheduled it again, and since the timer assumes a ref on frag_queue we need to acquire one to balance things out.
This is technically racy, we should have acquired the reference _before_ we touch the timer, it may fire again before we take the ref. Avoid this entire dance by using mod_timer_pending() which only modifies the timer if its pending (and which exists since Linux v2.6.30)
Note that this was the only place we ever took a ref on frag_queue since Eric's conversion to RCU. So we could potentially replace the whole refcnt field with an atomic flag and a bit more RCU.
--- net/ipv4/inet_fragment.c | 4 +++- net/ipv4/ip_fragment.c | 4 +--- 2 files changed, 4 insertions(+), 4 deletions(-)
net/ipv4/inet_fragment.c +3 -1
@@ -327,7 +327,9 @@ static struct inet_frag_queue *inet_frag_alloc(struct fqdir *fqdir,timer_setup (&q->timer, f->frag_expire,0 );spin_lock_init (&q->lock); -/* One reference for the timer, one for the hash table. */ +/* One reference for the timer, one for the hash table. +* We never take any extra references, only decrement this field. +*/ refcount_set (&q->refcnt,2 );return q;
net/ipv4/ip_fragment.c +1 -3
@@ -242,10 +242,8 @@ static int ip_frag_reinit(struct ipq *qp) {unsigned int sum_truesize =0 ; -if (!mod_timer (&qp->q.timer, jiffies + qp->q.fqdir->timeout)) { -refcount_inc (&qp->q.refcnt); +if (!mod_timer_pending (&qp->q.timer, jiffies + qp->q.fqdir->timeout))return -ETIMEDOUT ; - } sum_truesize =inet_frag_rbtree_purge (&qp->q.rb_fragments,SKB_DROP_REASON_FRAG_TOO_FAR );
Signature · 1 line
2.52.0
Re: [PATCH net 1/4] inet: frags: avoid theoretical race in ip_frag_reinit()
Quoted 11 lines · Jakub Kicinski
In
ip_frag_reinit()we want to move the frag timeout timer into the future. If the timer fires in the meantime we inadvertently scheduled it again, and since the timer assumes a ref onfrag_queuewe need to acquire one to balance things out.This is technically racy, we should have acquired the reference _before_ we touch the timer, it may fire again before we take the ref. Avoid this entire dance by using
mod_timer_pending()which only modifies the timer if its pending (and which exists since Linux v2.6.30)
Quoted 7 lines
Note that this was the only place we ever took a ref on
frag_queuesince Eric's conversion to RCU. So we could potentially replace the whole refcnt field with an atomic flag and a bit more RCU.Fixes 1da177e4c3f4 ("Linux-2.6.12-rc2")Signed-off-by Jakub Kicinski <[email protected]>---
Jakub Kicinski
maintainerPATCH 2/4 inet: frags: add inet_frag_queue_flush()
In reply to Jakub KicinskiInstead of exporting inet_frag_rbtree_purge() which requires that caller takes care of memory accounting, add a new helper. We will need to call it from a few places in the next patch.
--- include/net/inet_frag.h | 5 ++--- net/ipv4/inet_fragment.c | 15 ++++++++++++--- net/ipv4/ip_fragment.c | 6 +----- 3 files changed, 15 insertions(+), 11 deletions(-)
include/net/inet_frag.h +2 -3
@@ -141,9 +141,8 @@ void inet_frag_kill(struct inet_frag_queue *q, int *refs);void inet_frag_destroy (struct inet_frag_queue *q);struct inet_frag_queue *inet_frag_find (struct fqdir *fqdir,void *key); -/* Free all skbs in the queue; return the sum of their truesizes. */ -unsigned int inet_frag_rbtree_purge (struct rb_root *root, -enum skb_drop_reason reason); +void inet_frag_queue_flush (struct inet_frag_queue *q, +enum skb_drop_reason reason);static inline void inet_frag_putn (struct inet_frag_queue *q,int refs) {
net/ipv4/inet_fragment.c +12 -3
@@ -263,8 +263,8 @@ static void inet_frag_destroy_rcu(struct rcu_head *head)kmem_cache_free (f->frags_cachep, q); } -unsigned int inet_frag_rbtree_purge (struct rb_root *root, -enum skb_drop_reason reason) +static unsigned int +inet_frag_rbtree_purge (struct rb_root *root,enum skb_drop_reason reason) {struct rb_node *p =rb_first (root);unsigned int sum =0 ;
@@ -284,7 +284,16 @@ unsigned int inet_frag_rbtree_purge(struct rb_root *root, }return sum; } -EXPORT_SYMBOL (inet_frag_rbtree_purge); + +void inet_frag_queue_flush (struct inet_frag_queue *q, +enum skb_drop_reason reason) +{ +unsigned int sum; + + sum =inet_frag_rbtree_purge (&q->rb_fragments, reason); +sub_frag_mem_limit (q->fqdir, sum); +} +EXPORT_SYMBOL (inet_frag_queue_flush);void inet_frag_destroy (struct inet_frag_queue *q) {
net/ipv4/ip_fragment.c +1 -5
@@ -240,14 +240,10 @@ static int ip_frag_too_far(struct ipq *qp)static int ip_frag_reinit (struct ipq *qp) { -unsigned int sum_truesize =0 ; -if (!mod_timer_pending (&qp->q.timer, jiffies + qp->q.fqdir->timeout))return -ETIMEDOUT ; - sum_truesize =inet_frag_rbtree_purge (&qp->q.rb_fragments, -SKB_DROP_REASON_FRAG_TOO_FAR ); -sub_frag_mem_limit (qp->q.fqdir, sum_truesize); +inet_frag_queue_flush (&qp->q,SKB_DROP_REASON_FRAG_TOO_FAR ); qp->q.flags =0 ; qp->q.len =0 ;
Signature · 1 line
2.52.0
Re: [PATCH net 2/4] inet: frags: add inet_frag_queue_flush()
Quoted 6 lines · Jakub Kicinski
Instead of exporting
inet_frag_rbtree_purge()which requires that caller takes care of memory accounting, add a new helper. We will need to call it from a few places in the next patch.Signed-off-by Jakub Kicinski <[email protected]>
Jakub Kicinski
maintainerPATCH 3/4 inet: frags: flush pending skbs in fqdir_pre_exit()
In reply to Jakub KicinskiWe have been seeing occasional deadlocks on pernet_ops_rwsem since September in NIPA. The stuck task was usually modprobe (often loading a driver like ipvlan), trying to take the lock as a Writer. lockdep does not track readers for rwsems so the read wasn't obvious from the reports.
On closer inspection the Reader holding the lock was conntrack looping forever in nf_conntrack_cleanup_net_list(). Based on past experience with occasional NIPA crashes I looked thru the tests which run before the crash and noticed that the crash follows ip_defrag.sh. An immediate red flag. Scouring thru (de)fragmentation queues reveals skbs sitting around, holding conntrack references.
The problem is that since conntrack depends on nf_defrag_ipv6, nf_defrag_ipv6 will load first. Since nf_defrag_ipv6 loads first its netns exit hooks run _after_ conntrack's netns exit hook.
Flush all fragment queue SKBs during fqdir_pre_exit() to release conntrack references before conntrack cleanup runs. Also flush the queues in timer expiry handlers when they discover fqdir->dead is set, in case packet sneaks in while we're running the pre_exit flush.
The commit under Fixes is not exactly the culprit, but I think previously the timer firing would eventually unblock the spinning conntrack.
--- include/net/inet_frag.h | 13 +------------ include/net/ipv6_frag.h | 9 ++++++--- net/ipv4/inet_fragment.c | 36 ++++++++++++++++++++++++++++++++++++ net/ipv4/ip_fragment.c | 12 +++++++----- 4 files changed, 50 insertions(+), 20 deletions(-)
include/net/inet_frag.h +1 -12
@@ -123,18 +123,7 @@ void inet_frags_fini(struct inet_frags *);int fqdir_init (struct fqdir **fqdirp,struct inet_frags *f,struct net *net); -static inline void fqdir_pre_exit (struct fqdir *fqdir) -{ -/* Prevent creation of new frags. -* Pairs with READ_ONCE() in inet_frag_find(). -*/ -WRITE_ONCE (fqdir->high_thresh,0 ); - -/* Pairs with READ_ONCE() in inet_frag_kill(), ip_expire() -* and ip6frag_expire_frag_queue(). -*/ -WRITE_ONCE (fqdir->dead, true); -} +void fqdir_pre_exit (struct fqdir *fqdir);void fqdir_exit (struct fqdir *fqdir);void inet_frag_kill (struct inet_frag_queue *q,int *refs);
include/net/ipv6_frag.h +6 -3
@@ -69,9 +69,6 @@ ip6frag_expire_frag_queue(struct net *net, struct frag_queue *fq)int refs =1 ;rcu_read_lock (); -/* Paired with the WRITE_ONCE() in fqdir_pre_exit(). */ -if (READ_ONCE (fq->q.fqdir->dead)) - goto out_rcu_unlock;spin_lock (&fq->q.lock);if (fq->q.flags &INET_FRAG_COMPLETE )
@@ -80,6 +77,12 @@ ip6frag_expire_frag_queue(struct net *net, struct frag_queue *fq) fq->q.flags |=INET_FRAG_DROP ;inet_frag_kill (&fq->q, &refs); +/* Paired with the WRITE_ONCE() in fqdir_pre_exit(). */ +if (READ_ONCE (fq->q.fqdir->dead)) { +inet_frag_queue_flush (&fq->q,0 ); + goto out; + } + dev =dev_get_by_index_rcu (net, fq->iif);if (!dev) goto out;
net/ipv4/inet_fragment.c +36 -0
@@ -218,6 +218,41 @@ static int __init inet_frag_wq_init(void)pure_initcall (inet_frag_wq_init); +void fqdir_pre_exit (struct fqdir *fqdir) +{ +struct inet_frag_queue *fq; +struct rhashtable_iter hti; + +/* Prevent creation of new frags. +* Pairs with READ_ONCE() in inet_frag_find(). +*/ +WRITE_ONCE (fqdir->high_thresh,0 ); + +/* Pairs with READ_ONCE() in inet_frag_kill(), ip_expire() +* and ip6frag_expire_frag_queue(). +*/ +WRITE_ONCE (fqdir->dead, true); + +rhashtable_walk_enter (&fqdir->rhashtable, &hti); +rhashtable_walk_start (&hti); + +while ((fq =rhashtable_walk_next (&hti))) { +if (IS_ERR (fq)) { +if (PTR_ERR (fq) != -EAGAIN ) +break ; +continue ; + } +spin_lock_bh (&fq->lock); +if (!(fq->flags &INET_FRAG_COMPLETE )) +inet_frag_queue_flush (fq,0 ); +spin_unlock_bh (&fq->lock); + } + +rhashtable_walk_stop (&hti); +rhashtable_walk_exit (&hti); +} +EXPORT_SYMBOL (fqdir_pre_exit); +void fqdir_exit (struct fqdir *fqdir) {INIT_WORK (&fqdir->destroy_work, fqdir_work_fn);
@@ -290,6 +325,7 @@ void inet_frag_queue_flush(struct inet_frag_queue *q, {unsigned int sum; + reason = reason ?:SKB_DROP_REASON_FRAG_REASM_TIMEOUT ; sum =inet_frag_rbtree_purge (&q->rb_fragments, reason);sub_frag_mem_limit (q->fqdir, sum); }
net/ipv4/ip_fragment.c +7 -5
@@ -134,11 +134,6 @@ static void ip_expire(struct timer_list *t) net = qp->q.fqdir->net;rcu_read_lock (); - -/* Paired with WRITE_ONCE() in fqdir_pre_exit(). */ -if (READ_ONCE (qp->q.fqdir->dead)) - goto out_rcu_unlock; -spin_lock (&qp->q.lock);if (qp->q.flags &INET_FRAG_COMPLETE )
@@ -146,6 +141,13 @@ static void ip_expire(struct timer_list *t) qp->q.flags |=INET_FRAG_DROP ;inet_frag_kill (&qp->q, &refs); + +/* Paired with WRITE_ONCE() in fqdir_pre_exit(). */ +if (READ_ONCE (qp->q.fqdir->dead)) { +inet_frag_queue_flush (&qp->q,0 ); + goto out; + } +__IP_INC_STATS (net,IPSTATS_MIB_REASMFAILS );__IP_INC_STATS (net,IPSTATS_MIB_REASMTIMEOUT );
Signature · 1 line
2.52.0
Quoted 24 lines · Jakub Kicinski
We have been seeing occasional deadlocks on
pernet_ops_rwsemsince September in NIPA. The stuck task was usually modprobe (often loading a driver like ipvlan), trying to take the lock as a Writer. lockdep does not track readers for rwsems so the read wasn't obvious from the reports.On closer inspection the Reader holding the lock was conntrack looping forever in
nf_conntrack_cleanup_net_list(). Based on past experience with occasional NIPA crashes I looked thru the tests which run before the crash and noticed that the crash followsip_defrag.sh. An immediate red flag. Scouring thru (de)fragmentation queues reveals skbs sitting around, holding conntrack references.The problem is that since conntrack depends on
nf_defrag_ipv6,nf_defrag_ipv6will load first. Sincenf_defrag_ipv6loads first its netns exit hooks run _after_ conntrack's netns exit hook.Flush all fragment queue SKBs during
fqdir_pre_exit()to release conntrack references before conntrack cleanup runs. Also flush the queues in timer expiry handlers when they discoverfqdir->deadis set, in case packet sneaks in while we're running thepre_exitflush.
Quoted 6 lines
The commit under Fixes is not exactly the culprit, but I think previously the timer firing would eventually unblock the spinning conntrack.
Fixes d5dd88794a13 ("inet: fix various use-after-free in defrags units")Signed-off-by Jakub Kicinski <[email protected]>
Jakub Kicinski
maintainerPATCH 4/4 netfilter: conntrack: warn when cleanup is stuck
In reply to Jakub Kicinskinf_conntrack_cleanup_net_list() calls schedule() so it does not show up as a hung task. Add an explicit check to make debugging leaked skbs/conntack references more obvious.
--- net/netfilter/nf_conntrack_core.c | 3 +++ 1 file changed, 3 insertions(+)
net/netfilter/nf_conntrack_core.c +3 -0
@@ -2487,6 +2487,7 @@ void nf_conntrack_cleanup_net(struct net *net)void nf_conntrack_cleanup_net_list (struct list_head *net_exit_list) {struct nf_ct_iter_data iter_data = {}; +unsigned long start = jiffies;struct net *net;int busy;
@@ -2507,6 +2508,8 @@ void nf_conntrack_cleanup_net_list(struct list_head *net_exit_list) busy =1 ; }if (busy) { +DEBUG_NET_WARN_ONCE (time_after (jiffies, start +60 *HZ ), +"conntrack cleanup blocked for 60s" );schedule (); goto i_see_dead_people; }
Signature · 1 line
2.52.0
Re: [PATCH net 4/4] netfilter: conntrack: warn when cleanup is stuck
Quoted 3 lines · Jakub Kicinski
nf_conntrack_cleanup_net_list()callsschedule()so it does not show up as a hung task. Add an explicit check to make debugging leaked skbs/conntack references more obvious.
Eric Dumazet
Reviewed-byRe: [PATCH net 4/4] netfilter: conntrack: warn when cleanup is stuck
Quoted 7 lines · Florian Westphal
Quoted 3 lines · Jakub Kicinski
nf_conntrack_cleanup_net_list()callsschedule()so it does not show up as a hung task. Add an explicit check to make debugging leaked skbs/conntack references more obvious.Acked-by Florian Westphal <[email protected]>
This series was applied to netdev/net.git (main) by Jakub Kicinski <[email protected]>:
Hello:
This series was applied to netdev/net.git (main)
by Jakub Kicinski <[email protected]>:
Quoted 9 lines · On Sat, 6 Dec 2025 17:09:38 -0800 you wrote:
Fix the issue reported by NIPA starting on Sep 18th [1], where
pernet_ops_rwsemis constantly held by a reader, preventing writers from grabbing it (specifically driver modules from loading).The fact that reports started around that time seems coincidental. The issue seems to be skbs queued for defrag preventing conntrack from exiting.
[...]
Here is the summary with links:
- [net,1/4] inet: frags: avoid theoretical race in ip_frag_reinit()
https://git.kernel.org/netdev/net/c/8ef522c8a59a - [net,2/4] inet: frags: add inet_frag_queue_flush() https://git.kernel.org/netdev/net/c/1231eec6994b - [net,3/4] inet: frags: flush pending skbs in fqdir_pre_exit() https://git.kernel.org/netdev/net/c/006a5035b495 - [net,4/4] netfilter: conntrack: warn when cleanup is stuck https://git.kernel.org/netdev/net/c/92df4c56cf5b
You are awesome, thank you!
Signature · 2 lines
Deet-doot-dot, I am a bot. https://korg.docs.kernel.org/patchwork/pwbot.html