Netdev List
 help / color / mirror / Atom feed
* [PATCH net] net/sched: sch_hfsc: bound the classify inner-filter walk
@ 2026-09-12 18:10 Jamal Hadi Salim
  2026-09-12 20:36 ` netdev-bot+sashiko
  0 siblings, 1 reply; 3+ messages in thread
From: Jamal Hadi Salim @ 2026-09-12 18:10 UTC (permalink / raw)
  To: netdev
  Cc: Jamal Hadi Salim, Jiri Pirko, David S. Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, Simon Horman, Patrick McHardy,
	Sashiko, Victor Nogueira, hybris

hfsc_classify() applies the "filter may only point downwards" level
check only when the filter result carries no bound class. A filter
created with a flowid gets res.class set once at bind time, so the
check never runs for it during classification. hfsc_adjust_levels()
can later raise a class's level without revalidating existing
bindings, so two binds that were each legal at bind time can point at
each other; the classify walk then bounces between the two classes
forever with the qdisc lock held and BH disabled — a soft lockup from
a single packet. The stuck walk trips the watchdog on both KASAN and
KASAN-off builds:

  watchdog: BUG: soft lockup - CPU#3 stuck for 13s! [ping:444]
  RIP: 0010:u32_classify+0x542/0x17f0
  ...
  tcf_classify+0x66/0xa0
  hfsc_enqueue+0x166/0xdf0

  watchdog: BUG: soft lockup - CPU#0 stuck for 13s! [ping:340]
  tcf_action_exec+0x37/0x3e0
  u32_classify+0x12a/0x550
  hfsc_enqueue+0x7a/0x380
  Kernel panic - not syncing: softlockup: hung tasks

Bound the traversal the same way the HTB side was fixed: a sane walk
strictly descends the class tree, so it consumes fewer hops than the
level the walk starts at; anything beyond that is a cycle. Drop the
packet with a rate-limited warning when the bound is exhausted.

Conditions to recreate the bug:
- CONFIG_NET_SCHED, CONFIG_NET_SCH_HFSC, CONFIG_NET_CLS_U32,
  CONFIG_LOCKUP_DETECTOR.
- Build a cycle with two legal-at-bind-time flowid binds and a level
  drift: class X 1:1 (child of root) with leaf child 1:10; class Y 1:2
  (sibling of X) with children 1:20 and 1:200; root u32 filter flowid
  1:1; filter on X flowid 1:2 (legal when Y is a leaf); after Y's level
  rises to 2, filter on Y flowid 1:1 (legal then). Send one packet
  (ping on the device). Unfixed kernel: classify spins with the qdisc
  lock held; with softlockup_panic=1 it panics.
- Reachable from unprivileged user via unshare -Urn (CAP_NET_ADMIN).

Fixes: a2f79227138c ("net_sched: sch_hfsc: fix classification loops")
Reported-by: Sashiko (gemini + nipa) <sashiko-bot@kernel.org>
Link: https://sashiko.dev/#/patchset/20260824161809.4147223-1-victor@mojatatu.com
Reviewed-by: Victor Nogueira <victor@mojatatu.com>
Tested-by: hybris <hybris@mojatatu.ai>
Signed-off-by: Jamal Hadi Salim <jhs@mojatatu.com>
---
 net/sched/sch_hfsc.c | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/net/sched/sch_hfsc.c b/net/sched/sch_hfsc.c
index e87f5021a199..71744a6c2f38 100644
--- a/net/sched/sch_hfsc.c
+++ b/net/sched/sch_hfsc.c
@@ -1133,6 +1133,7 @@ hfsc_classify(struct sk_buff *skb, struct Qdisc *sch, int *qerr)
 	struct hfsc_class *head, *cl;
 	struct tcf_result res;
 	struct tcf_proto *tcf;
+	unsigned int hops;
 	int result;
 
 	if (TC_H_MAJ(skb->priority ^ sch->handle) == 0 &&
@@ -1142,6 +1143,7 @@ hfsc_classify(struct sk_buff *skb, struct Qdisc *sch, int *qerr)
 
 	*qerr = NET_XMIT_SUCCESS | __NET_XMIT_BYPASS;
 	head = &q->root;
+	hops = head->level;
 	tcf = rcu_dereference_bh(q->root.filter_list);
 	while (tcf && (result = tcf_classify_qdisc(skb, tcf, &res, false)) >= 0) {
 #ifdef CONFIG_NET_CLS_ACT
@@ -1167,6 +1169,15 @@ hfsc_classify(struct sk_buff *skb, struct Qdisc *sch, int *qerr)
 		if (cl->level == 0)
 			return cl; /* hit leaf class */
 
+		/*
+		 * flowid binds skip the level check above, and levels
+		 * drift after bind time, so this walk can cycle.
+		 */
+		if (hops-- == 0) {
+			pr_warn_ratelimited("hfsc: classify loop detected, dropping packet\n");
+			return NULL;
+		}
+
 		/* apply inner filter chain */
 		tcf = rcu_dereference_bh(cl->filter_list);
 		head = cl;
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 3+ messages in thread

* Re: [PATCH net] net/sched: sch_hfsc: bound the classify inner-filter walk
  2026-09-12 18:10 [PATCH net] net/sched: sch_hfsc: bound the classify inner-filter walk Jamal Hadi Salim
@ 2026-09-12 20:36 ` netdev-bot+sashiko
  2026-09-13 10:39   ` Jamal Hadi Salim
  0 siblings, 1 reply; 3+ messages in thread
From: netdev-bot+sashiko @ 2026-09-12 20:36 UTC (permalink / raw)
  To: jhs
  Cc: netdev, jiri, davem, edumazet, kuba, pabeni, horms, kaber,
	sashiko-bot, victor, hybris

Thank you for your contribution! Sashiko AI review found 1 potential
issue(s) to consider:

Critical: 0 · High: 1 · Medium: 0 · Low: 0

- [High] hfsc_classify() (net/sched/sch_hfsc.c:1146) seeds the new cycle
  guard with `hops = head->level` where `head = &q->root`.

--

net/sched: sch_hfsc: bound the classify inner-filter walk

This adds a hop budget to the inner-filter walk in hfsc_classify(), seeded
from the root class level, and drops the packet with a rate-limited warning
when the budget is exhausted. The intent is to stop the classify walk from
spinning forever when two flowid binds end up pointing at each other after
hfsc_adjust_levels() raises a class level.

Link: https://sashiko.dev/#/patchset/20260824161809.4147223-1-victor@mojatatu.com

> diff --git a/net/sched/sch_hfsc.c b/net/sched/sch_hfsc.c
> index e87f5021a1995..71744a6c2f38e 100644
> --- a/net/sched/sch_hfsc.c
> +++ b/net/sched/sch_hfsc.c
> @@ -1133,6 +1133,7 @@ hfsc_classify(struct sk_buff *skb, struct Qdisc *sch, int *qerr)
>  	struct hfsc_class *head, *cl;
>  	struct tcf_result res;
>  	struct tcf_proto *tcf;
> +	unsigned int hops;
>  	int result;
>  
>  	if (TC_H_MAJ(skb->priority ^ sch->handle) == 0 &&
> @@ -1142,6 +1143,7 @@ hfsc_classify(struct sk_buff *skb, struct Qdisc *sch, int *qerr)
>  
>  	*qerr = NET_XMIT_SUCCESS | __NET_XMIT_BYPASS;
>  	head = &q->root;
> +	hops = head->level;
        ^^^^^^^^^^^^^^^^^^

[Severity: High]

Can a legal, acyclic chain of flowid binds consume more hops than
q->root.level and get dropped here?

The budget root->level is an upper bound on the number of internal classes
visited only if every filter step strictly descends the tree. But as the
commit message itself points out, the descent check is only reached on the
classid-lookup path:

	cl = (struct hfsc_class *)res.class;
	if (!cl) {
		cl = hfsc_find_class(res.classid, sch);
		if (!cl)
			break; /* filter selected invalid classid */
		if (cl->level >= head->level)
			break; /* filter may only point downwards */
	}

For a flowid bind, res.class is already set and no level relation is
enforced at classify time.  hfsc_bind_tcf() checks the relation once, at
bind time:

	if (cl != NULL) {
		if (p != NULL && p->level <= cl->level)
			return 0;

and hfsc_adjust_levels() later raises levels without revalidating the
stored res.class:

		WRITE_ONCE(cl->level, level);

So the walk can step between classes of equal level without cycling.
A configuration that the current code accepts at every step, with no cycle
at all:

- A1 child of root with one leaf child -> A1->level == 1, root->level == 2,
  so hops == 2.
- A2 as a leaf child of root (level 0); filter on A1 with flowid A2 is
  accepted because p(A1)->level 1 > cl(A2)->level 0.
- Add a leaf child to A2 -> A2->level becomes 1 (root->level stays 2).
- A3 as a leaf child of root; filter on A2 with flowid A3 accepted
  (1 > 0); then add a leaf child to A3 -> A3->level == 1.
- Filter on A3 with flowid A3's leaf child (strictly descending), root
  filter selects A1.

Walking a matching packet: A1 (hops 2 -> 1), A2 (hops 1 -> 0), A3 hits
hops-- == 0 and returns NULL, so hfsc_enqueue() drops the packet even
though the next step was a leaf.  Before this patch the same packet was
enqueued into A3's leaf.

Would a configuration-independent constant avoid this, the way the HTB
side does it in htb_classify()?

	if (++hops > TC_HTB_MAXDEPTH) {
		pr_warn_ratelimited("htb: classify loop detected, dropping packet\n");
		return NULL;
	}

The commit message says "Bound the traversal the same way the HTB side was
fixed: a sane walk strictly descends the class tree, so it consumes fewer
hops than the level the walk starts at".  Is that premise consistent with
the rest of the message, which states the strict-descent check "never runs"
for flowid binds and that levels drift after bind time?

>  	tcf = rcu_dereference_bh(q->root.filter_list);
>  	while (tcf && (result = tcf_classify_qdisc(skb, tcf, &res, false)) >= 0) {
>  #ifdef CONFIG_NET_CLS_ACT
> @@ -1167,6 +1169,15 @@ hfsc_classify(struct sk_buff *skb, struct Qdisc *sch, int *qerr)
>  		if (cl->level == 0)
>  			return cl; /* hit leaf class */
>  
> +		/*
> +		 * flowid binds skip the level check above, and levels
> +		 * drift after bind time, so this walk can cycle.
> +		 */
> +		if (hops-- == 0) {
> +			pr_warn_ratelimited("hfsc: classify loop detected, dropping packet\n");
> +			return NULL;
> +		}
> +
>  		/* apply inner filter chain */
>  		tcf = rcu_dereference_bh(cl->filter_list);
>  		head = cl;

Also, since budget exhaustion is reported unconditionally as "classify loop
detected" without any evidence that a class was revisited, would the
warning be misleading for the acyclic case above?

-- 
Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/QDISC-CTUU.v1.20260908094501%40mojatatu.com

^ permalink raw reply	[flat|nested] 3+ messages in thread

* Re: [PATCH net] net/sched: sch_hfsc: bound the classify inner-filter walk
  2026-09-12 20:36 ` netdev-bot+sashiko
@ 2026-09-13 10:39   ` Jamal Hadi Salim
  0 siblings, 0 replies; 3+ messages in thread
From: Jamal Hadi Salim @ 2026-09-13 10:39 UTC (permalink / raw)
  To: netdev-bot+sashiko
  Cc: netdev, jiri, davem, edumazet, kuba, pabeni, horms, kaber,
	sashiko-bot, victor, hybris

On Sat, Sep 12, 2026 at 4:36 PM <netdev-bot+sashiko@kernel.org> wrote:
>
> Thank you for your contribution! Sashiko AI review found 1 potential
> issue(s) to consider:
>
> Critical: 0 · High: 1 · Medium: 0 · Low: 0
>
> - [High] hfsc_classify() (net/sched/sch_hfsc.c:1146) seeds the new cycle
>   guard with `hops = head->level` where `head = &q->root`.
>

Yes, this was an oversight on my part. I will send a v2.

pw-bot: cr

cheers,
jamal
> --
>
> net/sched: sch_hfsc: bound the classify inner-filter walk
>
> This adds a hop budget to the inner-filter walk in hfsc_classify(), seeded
> from the root class level, and drops the packet with a rate-limited warning
> when the budget is exhausted. The intent is to stop the classify walk from
> spinning forever when two flowid binds end up pointing at each other after
> hfsc_adjust_levels() raises a class level.
>
> Link: https://sashiko.dev/#/patchset/20260824161809.4147223-1-victor@mojatatu.com
>
> > diff --git a/net/sched/sch_hfsc.c b/net/sched/sch_hfsc.c
> > index e87f5021a1995..71744a6c2f38e 100644
> > --- a/net/sched/sch_hfsc.c
> > +++ b/net/sched/sch_hfsc.c
> > @@ -1133,6 +1133,7 @@ hfsc_classify(struct sk_buff *skb, struct Qdisc *sch, int *qerr)
> >       struct hfsc_class *head, *cl;
> >       struct tcf_result res;
> >       struct tcf_proto *tcf;
> > +     unsigned int hops;
> >       int result;
> >
> >       if (TC_H_MAJ(skb->priority ^ sch->handle) == 0 &&
> > @@ -1142,6 +1143,7 @@ hfsc_classify(struct sk_buff *skb, struct Qdisc *sch, int *qerr)
> >
> >       *qerr = NET_XMIT_SUCCESS | __NET_XMIT_BYPASS;
> >       head = &q->root;
> > +     hops = head->level;
>         ^^^^^^^^^^^^^^^^^^
>
> [Severity: High]
>
> Can a legal, acyclic chain of flowid binds consume more hops than
> q->root.level and get dropped here?
>
> The budget root->level is an upper bound on the number of internal classes
> visited only if every filter step strictly descends the tree. But as the
> commit message itself points out, the descent check is only reached on the
> classid-lookup path:
>
>         cl = (struct hfsc_class *)res.class;
>         if (!cl) {
>                 cl = hfsc_find_class(res.classid, sch);
>                 if (!cl)
>                         break; /* filter selected invalid classid */
>                 if (cl->level >= head->level)
>                         break; /* filter may only point downwards */
>         }
>
> For a flowid bind, res.class is already set and no level relation is
> enforced at classify time.  hfsc_bind_tcf() checks the relation once, at
> bind time:
>
>         if (cl != NULL) {
>                 if (p != NULL && p->level <= cl->level)
>                         return 0;
>
> and hfsc_adjust_levels() later raises levels without revalidating the
> stored res.class:
>
>                 WRITE_ONCE(cl->level, level);
>
> So the walk can step between classes of equal level without cycling.
> A configuration that the current code accepts at every step, with no cycle
> at all:
>
> - A1 child of root with one leaf child -> A1->level == 1, root->level == 2,
>   so hops == 2.
> - A2 as a leaf child of root (level 0); filter on A1 with flowid A2 is
>   accepted because p(A1)->level 1 > cl(A2)->level 0.
> - Add a leaf child to A2 -> A2->level becomes 1 (root->level stays 2).
> - A3 as a leaf child of root; filter on A2 with flowid A3 accepted
>   (1 > 0); then add a leaf child to A3 -> A3->level == 1.
> - Filter on A3 with flowid A3's leaf child (strictly descending), root
>   filter selects A1.
>
> Walking a matching packet: A1 (hops 2 -> 1), A2 (hops 1 -> 0), A3 hits
> hops-- == 0 and returns NULL, so hfsc_enqueue() drops the packet even
> though the next step was a leaf.  Before this patch the same packet was
> enqueued into A3's leaf.
>
> Would a configuration-independent constant avoid this, the way the HTB
> side does it in htb_classify()?
>
>         if (++hops > TC_HTB_MAXDEPTH) {
>                 pr_warn_ratelimited("htb: classify loop detected, dropping packet\n");
>                 return NULL;
>         }
>
> The commit message says "Bound the traversal the same way the HTB side was
> fixed: a sane walk strictly descends the class tree, so it consumes fewer
> hops than the level the walk starts at".  Is that premise consistent with
> the rest of the message, which states the strict-descent check "never runs"
> for flowid binds and that levels drift after bind time?
>
> >       tcf = rcu_dereference_bh(q->root.filter_list);
> >       while (tcf && (result = tcf_classify_qdisc(skb, tcf, &res, false)) >= 0) {
> >  #ifdef CONFIG_NET_CLS_ACT
> > @@ -1167,6 +1169,15 @@ hfsc_classify(struct sk_buff *skb, struct Qdisc *sch, int *qerr)
> >               if (cl->level == 0)
> >                       return cl; /* hit leaf class */
> >
> > +             /*
> > +              * flowid binds skip the level check above, and levels
> > +              * drift after bind time, so this walk can cycle.
> > +              */
> > +             if (hops-- == 0) {
> > +                     pr_warn_ratelimited("hfsc: classify loop detected, dropping packet\n");
> > +                     return NULL;
> > +             }
> > +
> >               /* apply inner filter chain */
> >               tcf = rcu_dereference_bh(cl->filter_list);
> >               head = cl;
>
> Also, since budget exhaustion is reported unconditionally as "classify loop
> detected" without any evidence that a class was revisited, would the
> warning be misleading for the acyclic case above?
>
> --
> Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/QDISC-CTUU.v1.20260908094501%40mojatatu.com

^ permalink raw reply	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2026-09-13 10:40 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-12 18:10 [PATCH net] net/sched: sch_hfsc: bound the classify inner-filter walk Jamal Hadi Salim
2026-09-12 20:36 ` netdev-bot+sashiko
2026-09-13 10:39   ` Jamal Hadi Salim

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox