[PATCH net v6] net: schedule a purge when the defer queue rejects a free

From: Joseph Kain

Date: Mon Oct 19 2026 - 15:04:00 EST


__skb_defer_free_flush() can leave defer_count at or above skb_defer_max
with an empty defer_list. That state is absorbing: every producer then
takes the nodefer path without queueing, so the list never refills.
Deferred freeing stays off for that (cpu, node) until reboot, and
cross-CPU frees fall back to the page allocator, which is the contention
deferral exists to avoid.

skb_attempt_defer_free() increments before testing the limit and keeps
the increment when it rejects, so a rejected free leaves the counter
raised with nothing queued.

Nothing brings it back down. The reject path returns before
kick_defer_list_purge(), so once the counter is stuck no producer
schedules a flush on the owner CPU. A CPU that never raises NET_RX itself
-- an application CPU doing TX while NIC IRQs are pinned elsewhere and
RPS is off -- then never runs the flush at all, so clearing the counter
inside the flush cannot help it either. proc_do_skb_defer_max() neither
resets the counters nor schedules a flush when the limit changes.

It is reachable with no race at all:

sysctl -w net.core.skb_defer_max=1 # frees increment, then reject
sysctl -w net.core.skb_defer_max=128 # does not recover

skb_defer_max has .extra1 = SYSCTL_ZERO and no .extra2, so 1 is settable.
>From v7.1 on, 0 is safe because proc_do_skb_defer_max() flips
skb_defer_disable_key and the producer returns before incrementing. On
6.18 through 7.0, which carry the llist conversion but not commit
08dc30de1a40 ("net: add skb_defer_disable_key static key"), setting 0 and
restoring the previous value strands the counter the same way -- and 0 is
the documented way to turn the feature off.

Kick the purge from the reject path instead. The reject is the one place
that observes a full queue, so scheduling the flush there is what stops
the counter outliving the list, and it does not depend on the owner CPU
ever raising NET_RX on its own. kick_defer_list_purge() already coalesces
through sd->defer_ipi_scheduled, so a burst of rejects costs one IPI.

This also covers the case where skb_defer_max is lowered below the
current defer_count. The kick condition tests equality with half the
limit, so a reduction can move that point behind the counter and no
later producer ever reaches it; rejecting without a kick then strands a
non-empty list the same way. The reject-path kick recovers both.

While here, test the limit with > rather than >=, so the effective limit
is skb_defer_max rather than skb_defer_max - 1, matching what
Documentation/admin-guide/sysctl/net.rst describes.

That also makes the half-capacity kick reachable at small limits. At
skb_defer_max = 1 nothing was ever queued, so no kick was ever sent, and
at 2 the equality was never hit; both now kick. That is what commit
c09b0cd2cc6c ("net: avoid strange behavior with skb_defer_max == 1") set
out to allow.

Also drain before clearing in the flush. A producer that queues between
the clear and the drain currently has its skb drained but its increment
kept, which over-counts and costs deferral until the next flush.
Draining first matches the ordering __dev_xmit_skb() already uses for the
qdisc defer_list.

Found by manual code inspection, LLM-assisted, while backporting the
llist conversion to 6.1 after hitting the sd->defer_lock contention it
removes. The latch itself was not hit in production; it was reproduced by
lowering net.core.skb_defer_max.

Fixes: 844c9db7f7f5 ("net: use llist for sd->defer_list")
Suggested-by: Kuniyuki Iwashima <kuniyu@xxxxxxxxxx>
Assisted-by: Claude:claude-opus-5
Signed-off-by: Joseph Kain <jkain@xxxxxxx>
---
v6:
- Keep the increment and kick the purge from the reject path, rather
than testing before incrementing as v4 did. One atomic instead of a
read followed by an RMW, the increment still reserves the slot so the
limit stays exact, and it also recovers the case where skb_defer_max
is lowered below the current defer_count, which v4 did not
(Kuniyuki Iwashima; also reported against v3 and v4 by
netdev-bot sashiko, whose suggested fix this is).
- Describe the two resulting behaviour changes -- the effective limit
and the kick at small limits -- in the commit message rather than
only here, since git am drops this part (netdev-bot sashiko).

Measured on v7.3-rc6 in a VM, 8 cpus, cross-CPU TCP load, sampling
defer_count via bpftrace. Dropping net.core.skb_defer_max from 128 to 1
and then restoring 128:

7b63ef2d55f2 (unpatched) defer_count climbed 701404 -> 3244783 and
stayed at 3244783 after restoring 128
this patch defer_count stayed within 0..1 throughout
and returned to 0

With skb_defer_max=1 and a cross-CPU TCP load, kick_defer_list_purge()
is called 0 times before this patch and ~756k times after, over ~750k
skb_attempt_defer_free() calls; at skb_defer_max=2, 0 before and ~163k
after.

Built for x86_64 and arm64, with and without CONFIG_PREEMPT_RT.

v4: https://lore.kernel.org/all/20261015162209.485940-1-jkain@xxxxxxx/
v3: https://lore.kernel.org/all/20261013205416.3417296-1-jkain@xxxxxxx/
v2: https://lore.kernel.org/all/20261008220256.1844072-1-jkain@xxxxxxx/
v1: https://lore.kernel.org/all/20261007180056.1394868-1-jkain@xxxxxxx/
net/core/dev.c | 5 ++++-
net/core/skbuff.c | 8 +++++++-
2 files changed, 11 insertions(+), 2 deletions(-)

diff --git a/net/core/dev.c b/net/core/dev.c
index 18dc88990510..60d86bc21632 100644
--- a/net/core/dev.c
+++ b/net/core/dev.c
@@ -6940,8 +6940,11 @@ static void __skb_defer_free_flush(struct skb_defer_node *sdn, int budget)

if (llist_empty(&sdn->defer_list))
return;
- atomic_long_set(&sdn->defer_count, 0);
free_list = llist_del_all(&sdn->defer_list);
+ /* Racy on purpose, as in __dev_xmit_skb(): a producer landing between
+ * the del_all and this store stays queued but uncounted.
+ */
+ atomic_long_set(&sdn->defer_count, 0);

llist_for_each_entry_safe(skb, next, free_list, ll_node) {
prefetch(next);
diff --git a/net/core/skbuff.c b/net/core/skbuff.c
index 4aea06d5167d..643666d9f4c9 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 (unlikely(defer_count > defer_max)) {
+ /* The kept increment only comes back down when a flush zeroes
+ * it, and this is the one path that sees a full queue.
+ */
+ if (!llist_empty(&sdn->defer_list))
+ kick_defer_list_purge(cpu);
goto nodefer;
+ }

llist_add(&skb->ll_node, &sdn->defer_list);

--
2.55.0