[PATCH net v7] net: stop defer_count outliving the deferred-free list
From: Joseph Kain
Date: Thu Oct 29 2026 - 17:16:46 EST
After the llist conversion, rejected frees retain their counter
increments. A producer racing the counter reset and list detach can
also leave a count for an skb already drained. Deferred freeing can
then remain disabled while the list is empty.
Undo rejected reservations and subtract only the number of skbs
drained. Allow admission up to the configured limit. Retain the
halfway notification and also notify when the reserved count reaches
the limit, covering reservations published after an earlier purge.
Fixes: 844c9db7f7f5 ("net: use llist for sd->defer_list")
Signed-off-by: Joseph Kain <jkain@xxxxxxx>
Assisted-by: LLM
---
For 6.18-7.1, apply the drain change inside each NUMA-node iteration
of skb_defer_free_flush(), using sdn->defer_list and sdn->defer_count.
Initialize n to zero for each node.
---
net/core/dev.c | 8 +++++++-
net/core/skbuff.c | 17 ++++++++++++++---
2 files changed, 21 insertions(+), 4 deletions(-)
diff --git a/net/core/dev.c b/net/core/dev.c
index 18dc88990510..7843ef511d7e 100644
--- a/net/core/dev.c
+++ b/net/core/dev.c
@@ -6937,16 +6937,22 @@ static void __skb_defer_free_flush(struct skb_defer_node *sdn, int budget)
{
struct llist_node *free_list;
struct sk_buff *skb, *next;
+ long n = 0;
if (llist_empty(&sdn->defer_list))
return;
- atomic_long_set(&sdn->defer_count, 0);
free_list = llist_del_all(&sdn->defer_list);
llist_for_each_entry_safe(skb, next, free_list, ll_node) {
prefetch(next);
napi_consume_skb(skb, budget);
+ n++;
}
+ /* Subtract exactly what was drained. Never store an absolute value:
+ * mixing one with the relative updates in skb_attempt_defer_free()
+ * lets the counter go negative, and it is read as unsigned there.
+ */
+ atomic_long_sub(n, &sdn->defer_count);
}
void skb_defer_node_flush(struct skb_defer_node *sdn)
diff --git a/net/core/skbuff.c b/net/core/skbuff.c
index 4aea06d5167d..b1d3fd783283 100644
--- a/net/core/skbuff.c
+++ b/net/core/skbuff.c
@@ -7388,8 +7388,14 @@ nodefer: kfree_skb_napi_cache(skb);
defer_max = READ_ONCE(net_hotdata.sysctl_skb_defer_max);
defer_count = atomic_long_inc_return(&sdn->defer_count);
- if (defer_count >= defer_max)
+ if (defer_count > defer_max) {
+ /* Undo the reservation. An llist_add() does not release one --
+ * the skb stays charged until a flush subtracts the batch that
+ * contains it -- so a rejected free must undo its own.
+ */
+ atomic_long_dec(&sdn->defer_count);
goto nodefer;
+ }
llist_add(&skb->ll_node, &sdn->defer_list);
@@ -7398,8 +7404,13 @@ nodefer: kfree_skb_napi_cache(skb);
return;
}
- /* Send an IPI every time queue reaches half capacity. */
- kick = (defer_count - 1) == (defer_max >> 1);
+ /* Half capacity, plus the skb that fills the queue. Equality alone can
+ * be consumed early -- if the producer holding the kick value publishes
+ * first, its purge drains only that skb and the reservations published
+ * after it match nothing -- which would leave a full queue with nothing
+ * pending.
+ */
+ kick = (defer_count - 1) == (defer_max >> 1) || defer_count == defer_max;
/* Make sure to trigger NET_RX_SOFTIRQ on the remote CPU
* if we are unlucky enough (this seems very unlikely).
--
2.55.0