From: netdev-bot+sashiko@kernel.org
To: norbert@doyensec.com
Cc: netdev@vger.kernel.org, jhs@mojatatu.com, jiri@resnulli.us,
davem@davemloft.net, edumazet@google.com, kuba@kernel.org,
pabeni@redhat.com, horms@kernel.org, xiyou.wangcong@gmail.com,
linux-kernel@vger.kernel.org
Subject: Re: [PATCH net] net/sched: cls_u32: take a reference on the hash table in u32_change()
Date: Wed, 09 Sep 2026 12:39:14 +0000 [thread overview]
Message-ID: <178895755454.219967.8093907569129807456@kernel.org> (raw)
In-Reply-To: <79DB27AC-0FC0-42B2-A795-A8B585B73CFB@doyensec.com>
Thank you for your contribution! Sashiko AI review found 2 potential
issue(s) to consider:
Critical: 0 · High: 1 · Medium: 1 · Low: 0
- [High] The new pin in u32_change() (refcount_inc(&ht->refcnt) /
refcount_dec(&ht->refcnt) around u32_set_parms()) assumes every drop…
- [Medium] Commit message understates the reachability of the bug being
fixed.
--
net/sched: cls_u32: take a reference on the hash table in u32_change()
u32_change() holds the target tc_u_hnode in a local variable while
u32_set_parms() may drop rtnl to load an action module, so a concurrent
RTM_DELTFILTER can free it. The patch pins the hnode with
refcount_inc()/refcount_dec() around the u32_set_parms() call.
Two questions below, one on the changelog and one on the new pin itself.
> u32_change() keeps the target hash table in a local variable without
> taking a reference. u32_set_parms() may then drop rtnl to load an action
> module, and on a failed load returns without asking the caller to
> replay (-ENOENT). A concurrent RTM_DELTFILTER can free the table in
> that window, and u32_change() then removes the node id it reserved from
> the freed table.
[Severity: Medium]
Is the race really limited to the "failed load" case? tc_action_load_ops()
in net/sched/act_api.c drops rtnl in both outcomes:
net/sched/act_api.c:tc_action_load_ops() {
...
rtnl_unlock();
request_module("act_%s", act_name);
rtnl_lock();
...
if (a_o != NULL) {
module_put(a_o->owner);
return ERR_PTR(-EAGAIN);
}
#endif
NL_SET_ERR_MSG(extack, "Failed to load TC action module");
return ERR_PTR(-ENOENT);
}
In u32_change() any non-zero err from u32_set_parms() skips the
if (err == 0) block and falls through errunbind -> errout -> erridr, where
idr_remove(&ht->handle_idr, handle) dereferences ht. So the -EAGAIN path
(module present and loaded successfully, request replayed afterwards) also
touches ht after rtnl was dropped, and that is the common case on a normal
system.
Would it be worth describing the window as "u32_set_parms() may drop rtnl"
without tying it to -ENOENT, so backport triage sees the wider trigger?
> diff --git a/net/sched/cls_u32.c b/net/sched/cls_u32.c
> index a3e65c8cf29ef..0e9128f2f33e8 100644
> --- a/net/sched/cls_u32.c
> +++ b/net/sched/cls_u32.c
> @@ -1180,8 +1180,11 @@ static int u32_change(struct net *net, struct sk_buff *in_skb,
> }
> #endif
>
> + /* u32_set_parms() may drop rtnl; keep ht alive across it */
> + refcount_inc(&ht->refcnt);
> err = u32_set_parms(net, tp, n, tb, tca[TCA_RATE],
> flags, n->flags, extack);
> + refcount_dec(&ht->refcnt);
[Severity: High]
Does this pin hold when the hnode being pinned belongs to a different
tcf_proto?
The changelog states that a racing deletion "fails its
refcount_dec_if_one() and returns -EBUSY", which is true for u32_delete()
of a non-root hnode. But u32_destroy() drops the root hnode reference
unconditionally:
net/sched/cls_u32.c:u32_destroy() {
if (root_ht && refcount_dec_and_test(&root_ht->refcnt))
u32_destroy_hnode(tp, root_ht, extack);
...
}
tc_u_common is shared by all cls_u32 tcf_protos on the same qdisc/block,
and u32_change() resolves TCA_U32_HASH through the shared list without any
is_root or owner check:
net/sched/cls_u32.c:u32_change() {
...
ht = u32_lookup_ht(tp->data, TC_U32_HTID(htid));
if (!ht) {
NL_SET_ERR_MSG_MOD(extack, "Specified hash table not found");
return -EINVAL;
}
...
}
net/sched/cls_u32.c:u32_lookup_ht() {
for (ht = rtnl_dereference(tp_c->hlist);
ht;
ht = rtnl_dereference(ht->next))
if (ht->handle == handle)
break;
...
}
So a filter added at prio 2 with "ht 800:" can pin prio 1's root hnode.
If prio 1 is deleted in the rtnl-dropped window (RTM_DELTFILTER with
tcm_handle == 0 -> tcf_chain_tp_remove() + tcf_proto_put() ->
tcf_proto_destroy() -> u32_destroy()), the sequence looks like:
u32_change() refcount_inc() refcnt 1 -> 2
u32_destroy() refcount_dec_and_test() false refcnt 2 -> 1
u32_change() refcount_dec() refcnt 1 -> 0
Two things follow. First, u32_destroy_hnode() is skipped, so the hnode
stays linked in tp_c->hlist with no owner and u32_clear_hnode(),
u32_clear_hw_hnode(), idr_remove(&tp_c->handle_idr, ...), idr_destroy()
and kfree_rcu() never run.
Second, the trailing refcount_dec() takes the count 1 -> 0:
include/linux/refcount.h:__refcount_dec() {
int old = atomic_fetch_sub_release(1, &r->refs);
...
if (unlikely(old <= 1))
refcount_warn_saturate(r, REFCOUNT_DEC_LEAK);
}
That warns and sets REFCOUNT_SATURATED without freeing anything, and once
saturated no later refcount_dec_and_test() in u32_destroy() can reach zero:
net/sched/cls_u32.c:u32_destroy() {
...
if (refcount_dec_and_test(&ht->refcnt))
kfree_rcu(ht, rcu);
...
}
Can this leak the hnode (plus its idr and any knodes later inserted into
it) for the lifetime of the block, and splat under panic_on_warn? Would
handling the "we dropped the last reference" case, or re-looking up ht
after u32_set_parms() instead of carrying a borrowed pointer, avoid this?
As a smaller note on the same two lines: the reference is released before
the remaining ht dereferences it is meant to cover, namely
idr_remove(&ht->handle_idr, handle) at erridr and the
&ht->ht[TC_U32_HASH(handle)] insertion, so it relies on u32_set_parms()
always having re-acquired rtnl by the time it returns.
--
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/79DB27AC-0FC0-42B2-A795-A8B585B73CFB%40doyensec.com
prev parent reply other threads:[~2026-09-09 12:39 UTC|newest]
Thread overview: 2+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-05 16:38 Norbert Szetei
2026-09-09 12:39 ` netdev-bot+sashiko [this message]
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=178895755454.219967.8093907569129807456@kernel.org \
--to=netdev-bot+sashiko@kernel.org \
--cc=davem@davemloft.net \
--cc=edumazet@google.com \
--cc=horms@kernel.org \
--cc=jhs@mojatatu.com \
--cc=jiri@resnulli.us \
--cc=kuba@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=netdev@vger.kernel.org \
--cc=norbert@doyensec.com \
--cc=pabeni@redhat.com \
--cc=xiyou.wangcong@gmail.com \
/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®