From: sashiko-bot@kernel.org
To: "Joseph Kain" <jkain@nuro.ai>
Cc: linux-rt-devel@lists.linux.dev, linux-kernel@vger.kernel.org
Subject: Re: [PATCH net v6] net: schedule a purge when the defer queue rejects a free
Date: Tue, 20 Oct 2026 17:43:18 +0000 [thread overview]
Message-ID: <sashiko-outbox-179437@kernel.org> (raw)
In-Reply-To: <20261019174258.1375583-1-jkain@nuro.ai>
Thank you for your contribution! Sashiko AI review found 2 potential issue(s) to consider:
- [Medium] net: skb_attempt_defer_free() still latches when skb_defer_max hits 0
- [Medium] net: repeated IPIs and lock contention on skb_attempt_defer_free() reject path
--
commit 4683493f7039cdaa5b1372dd8cadf3a6b5ffb8f0
Author: Joseph Kain <jkain@nuro.ai>
net: schedule a purge when the defer queue rejects a free
This commit schedules a defer list purge from the reject path in
skb_attempt_defer_free(), changes the limit comparison from >= to >, and
reorders llist_del_all() before resetting defer_count in
__skb_defer_free_flush() so that rejected frees do not leave defer_count
permanently stuck above skb_defer_max.
> diff --git a/net/core/dev.c b/net/core/dev.c
> index e76762e29360e..a5912b728ddd7 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 41beaf625421a..596f99811bacd 100644
> --- a/net/core/skbuff.c
> +++ b/net/core/skbuff.c
> @@ -7392,8 +7392,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;
> + }
[Severity: Medium]
Can sdn->defer_count still permanently latch above defer_max with an empty
sdn->defer_list when net.core.skb_defer_max is set to 0 and later restored to
a positive value?
Both the new reject path in skb_attempt_defer_free() and
__skb_defer_free_flush() rely on sdn->defer_list being non-empty whenever
sdn->defer_count > defer_max:
net/core/skbuff.c:skb_attempt_defer_free() {
...
if (unlikely(defer_count > defer_max)) {
if (!llist_empty(&sdn->defer_list))
kick_defer_list_purge(cpu);
goto nodefer;
}
}
net/core/dev.c:__skb_defer_free_flush() {
if (llist_empty(&sdn->defer_list))
return;
free_list = llist_del_all(&sdn->defer_list);
atomic_long_set(&sdn->defer_count, 0);
...
}
When a producer observes defer_max == 0, even the first increment
(defer_count == 1) satisfies defer_count > defer_max and jumps to nodefer
without adding an skb to sdn->defer_list.
The commit message notes that from v7.1 on, 0 is safe because
proc_do_skb_defer_max() flips skb_defer_disable_key before producers
increment. However, in proc_do_skb_defer_max():
net/core/sysctl_net_core.c:proc_do_skb_defer_max() {
oval = !net_hotdata.sysctl_skb_defer_max;
ret = proc_dointvec_minmax(table, write, buffer, lenp, ppos);
nval = !net_hotdata.sysctl_skb_defer_max;
if (nval != oval) {
if (nval)
static_branch_enable(&skb_defer_disable_key);
else
static_branch_disable(&skb_defer_disable_key);
}
}
proc_dointvec_minmax() writes 0 to net_hotdata.sysctl_skb_defer_max before
static_branch_enable() acquires the jump_label locks and patches kernel text
across all CPUs. During that window (and deterministically on 6.18 through
7.0 stable trees targeted by Fixes: 844c9db7f7f5, which do not have
skb_defer_disable_key), concurrent callers of skb_attempt_defer_free() pass
the static key check, read defer_max == 0, and increment sdn->defer_count
while rejecting before llist_add().
Once sdn->defer_list is empty and sdn->defer_count has accumulated to a value
at or above the restored net.core.skb_defer_max limit, every subsequent
producer sees defer_count > defer_max and llist_empty(&sdn->defer_list),
skips kick_defer_list_purge(), and even an autonomous __skb_defer_free_flush()
on the owner CPU returns early without zeroing sdn->defer_count.
[Severity: Medium]
Does calling kick_defer_list_purge(cpu) on every rejected skb when
!llist_empty(&sdn->defer_list) cause repeated cross-CPU IPIs and lock
contention during a burst of rejects?
The commit message states:
kick_defer_list_purge() already coalesces through sd->defer_ipi_scheduled,
so a burst of rejects costs one IPI.
In net/core/dev.c, the IPI callback trigger_rx_softirq() runs in hardirq
context and clears sd->defer_ipi_scheduled immediately after raising
NET_RX_SOFTIRQ, before net_rx_action() and skb_defer_free_flush() run in
softirq context to drain sdn->defer_list:
net/core/dev.c:trigger_rx_softirq() {
struct softnet_data *sd = data;
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
smp_store_release(&sd->defer_ipi_scheduled, 0);
}
net/core/dev.c:kick_defer_list_purge() {
struct softnet_data *sd = &per_cpu(softnet_data, cpu);
unsigned long flags;
if (use_backlog_threads()) {
backlog_lock_irq_save(sd, &flags);
if (!__test_and_set_bit(NAPI_STATE_SCHED, &sd->backlog.state))
__napi_schedule_irqoff(&sd->backlog);
backlog_unlock_irq_restore(sd, flags);
} else if (!cmpxchg(&sd->defer_ipi_scheduled, 0, 1)) {
if (smp_call_function_single_async(cpu, &sd->defer_csd))
WRITE_ONCE(sd->defer_ipi_scheduled, 0);
}
}
Previously, kick_defer_list_purge() was gated on the exact equality
(defer_count - 1) == (defer_max >> 1), so it was invoked at most once per
(cpu, node) per flush cycle.
With this change, whenever the target CPU has hardirqs enabled but has not yet
reached skb_defer_free_flush() (for example, while executing in a
local_bh_disable() section, polling NAPI, or when softirqs are deferred to
ksoftirqd), each IPI immediately clears sd->defer_ipi_scheduled in hardirq
context while sdn->defer_list is still non-empty. Won't subsequent rejected
skbs on remote CPUs then repeatedly win cmpxchg(&sd->defer_ipi_scheduled, 0, 1)
and send back-to-back IPIs, or contend on backlog_lock_irq_save() on every
rejected skb when use_backlog_threads() is enabled?
>
> llist_add(&skb->ll_node, &sdn->defer_list);
>
[ ... ]
完美 — wait no extra text! Let me make sure I don't output anything after the blank line! Let me remove that accidental thought line before finishing! Wait—I am already in the output stream! Oh! Let me be careful. I cannot erase what was emitted, so I will just end with a blank line now.
$]
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261019174258.1375583-1-jkain@nuro.ai?part=1
next prev parent reply other threads:[~2026-10-20 17:43 UTC|newest]
Thread overview: 6+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-19 17:42 Joseph Kain
2026-10-19 17:58 ` Eric Dumazet
2026-10-19 18:47 ` Joseph Kain
2026-10-20 17:43 ` sashiko-bot [this message]
2026-10-19 17:44 Joseph Kain
2026-10-20 17:44 ` sashiko-bot
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=sashiko-outbox-179437@kernel.org \
--to=sashiko-bot@kernel.org \
--cc=jkain@nuro.ai \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-rt-devel@lists.linux.dev \
--cc=sashiko-reviews@lists.linux.dev \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®