* Re: [PATCH net v7] net: stop defer_count outliving the deferred-free list
2026-10-29 21:11 [PATCH net v7] net: stop defer_count outliving the deferred-free list Joseph Kain
@ 2026-10-29 21:13 ` netdev-bot+sinfo
2026-10-29 21:21 ` Joseph Kain
2026-11-02 21:21 ` netdev-bot+sashiko
1 sibling, 1 reply; 5+ messages in thread
From: netdev-bot+sinfo @ 2026-10-29 21:13 UTC (permalink / raw)
To: Joseph Kain
Cc: Eric Dumazet, Jakub Kicinski, Paolo Abeni, David S. Miller,
netdev, Simon Horman, Kuniyuki Iwashima, Stanislav Fomichev,
Jason Xing, Willem de Bruijn, Mina Almasry, Martin Karsten,
Jiayuan Chen, linux-kernel
Hi!
This is an automated message. This series looks like a fix, but its
commit messages seem to be missing some information:
- How the issue was discovered, e.g. hit in production, hit during
development, syzbot report, manual code inspection, LLM or static
analysis tool scan.
- Whether the issue was actually triggered, or is only theoretical
(e.g. found by code inspection). If it was triggered please include
the symptoms, like the stack trace or error messages.
Please do not repost the series just to address the above. Instead,
reply to this email with the missing information, so that reviewers
can take it into account. If the series needs another revision for
other reasons, please include the information in the commit messages
then.
The evaluation is done by an LLM so it may be wrong, if you think
that is the case please reply and explain.
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH net v7] net: stop defer_count outliving the deferred-free list
2026-10-29 21:11 [PATCH net v7] net: stop defer_count outliving the deferred-free list Joseph Kain
2026-10-29 21:13 ` netdev-bot+sinfo
@ 2026-11-02 21:21 ` netdev-bot+sashiko
2026-11-03 17:33 ` Joseph Kain
1 sibling, 1 reply; 5+ messages in thread
From: netdev-bot+sashiko @ 2026-11-02 21:21 UTC (permalink / raw)
To: jkain
Cc: edumazet, kuba, pabeni, davem, netdev, horms, kuniyu, sdf.kernel,
kerneljasonxing, willemb, almasrymina, mkarsten, jiayuan.chen,
linux-kernel
Thank you for your contribution! Sashiko AI review found 1 potential
issue(s) to consider.
Critical: 0 · High: 0 · Medium: 1 (1) · Low: 0 (1)
If you believe none of the issues are legitimate, please reply to each
issue of Medium severity or higher and explain why the AI is wrong.
If any of them are legitimate and you plan to respin, please let patchwork
know by including "pw-bot: cr" as a separate line at the end of your reply
(one such reply per series is enough).
- [Medium] __skb_defer_free_flush() (net/core/dev.c) now holds the whole
drained batch's reservation on sdn->defer_count until every detached…
Pre-existing issues:
- [Medium] A runtime change to net.core.skb_defer_max can leave a
per-(cpu, node) defer queue at or above the new limit with no purge…
- [Low] Documentation/admin-guide/sysctl/net.rst (lines 362-368)
describes skb_defer_max as the "Max size (in skbs) of the per-cpu list…
--
net: stop defer_count outliving the deferred-free list
This commit changes the deferred skb free accounting. A rejected free now
undoes its own defer_count increment, and __skb_defer_free_flush()
subtracts the number of drained skbs instead of resetting the counter to
zero. It also allows admission up to defer_max and adds a remote kick when
the reserved count reaches defer_max.
> diff --git a/net/core/dev.c b/net/core/dev.c
> index 3ac700d37e596..f5ed0ecf534ac 100644
> --- a/net/core/dev.c
> +++ b/net/core/dev.c
> @@ -7073,16 +7073,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);
[Severity: Medium]
With the subtraction moved after the napi_consume_skb() loop, does
deferral stay disabled for the whole drain?
The list is empty after llist_del_all(), but sdn->defer_count still
counts every detached skb until atomic_long_sub() runs. Suppose a full
queue (defer_max skbs, default 128) is flushed through
net_rx_action()->skb_defer_free_flush()->__skb_defer_free_flush(). The
counter then stays at about defer_max for the entire free loop.
A producer that calls skb_attempt_defer_free() for the same (alloc cpu,
node) pair during that window would hit:
defer_count = atomic_long_inc_return(&sdn->defer_count);
if (defer_count > defer_max) {
atomic_long_dec(&sdn->defer_count);
goto nodefer;
}
It would then fall back to kfree_skb_napi_cache(), which is the
cross-CPU free that deferral is meant to avoid.
The earlier versions both released the budget before freeing. The
spinlock version did atomic_set(&sd->defer_count, 0) under defer_lock
before the free loop. Commit 844c9db7f7f5 did atomic_long_set() before
llist_del_all().
The detached skbs are no longer on the list. Could n be computed first
and subtracted before the free loop? A counting pass over free_list would
do it, and would also warm the cache lines that napi_consume_skb()
touches. Neither the commit message nor the new comment mentions this
change to the admission window.
> }
>
> void skb_defer_node_flush(struct skb_defer_node *sdn)
> diff --git a/net/core/skbuff.c b/net/core/skbuff.c
> index 48f6764117243..68192e477ff1f 100644
> --- a/net/core/skbuff.c
> +++ b/net/core/skbuff.c
> @@ -7499,8 +7499,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;
> + }
[Severity: Low]
This isn't a bug introduced by this patch, but
Documentation/admin-guide/sysctl/net.rst still describes skb_defer_max
as:
Max size (in skbs) of the per-cpu list of skbs being freed
by the cpu which allocated them.
The limit is actually applied separately to each (allocating cpu,
freeing node) list:
sdn = per_cpu_ptr(net_hotdata.skb_defer_nodes, cpu) + cpu_to_node(my_cpu);
net_dev_init() allocates nr_node_ids of these lists per CPU. One
allocating CPU can therefore have up to defer_max * nr_node_ids skbs
pending.
The documentation also doesn't say that writing 0 disables deferral
entirely, which proc_do_skb_defer_max() does through
skb_defer_disable_key.
The switch from >= to > makes the per-list bound match "Max size". The
per-node scope and the meaning of 0 still differ from the text. Should
net.rst be updated to describe them?
>
> llist_add(&skb->ll_node, &sdn->defer_list);
>
> @@ -7509,8 +7515,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;
[Severity: Medium]
This is a pre-existing issue, but can a runtime change to
net.core.skb_defer_max still leave a per-(cpu, node) queue at or above
the new limit with no purge pending?
Both kick conditions are equality tests against the defer_max that was
read earlier with READ_ONCE(). The rejection branch above returns
without kicking.
proc_do_skb_defer_max() only toggles skb_defer_disable_key when the value
changes to or from 0. It doesn't drain or kick the existing lists:
if (nval != oval) {
if (nval)
static_branch_enable(&skb_defer_disable_key);
else
static_branch_disable(&skb_defer_disable_key);
}
Take a limit of 128 with 31 skbs queued, which is below the
half-capacity kick, so nothing is pending. If the limit is lowered to 16,
every later producer gets 32 > 16, undoes its reservation and returns.
The count never passes through 9 or 16, so neither kick fires.
In another case, a producer reads 128, the limit is lowered to 32, and
that producer then reserves 32. Both of its checks (31 == 64 and
32 == 128) fail. Later producers get 33 > 32 and are rejected.
In both cases deferral for that pair stays off. The queued skbs stay
pinned until the allocating CPU runs net_rx_action() or
napi_threaded_poll_loop() for some unrelated reason. On a CPU that only
allocates TX skbs, that may never happen.
The new full-capacity kick only guarantees a pending purge for a full
queue while the limit stays constant. Should the rejection path also
kick, or should the sysctl handler flush the existing lists?
[ ... ]
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/20261029211101.2089535-1-jkain%40nuro.ai
^ permalink raw reply [flat|nested] 5+ messages in thread