[PATCH net] net/sched: sch_hfsc: bound the classify inner-filter walk
From: Jamal Hadi Salim <jhs@mojatatu.com>
Date: 2026-09-12 18:10:05
Subsystem:
networking [general], tc subsystem, the rest · Maintainers:
"David S. Miller", Eric Dumazet, Jakub Kicinski, Paolo Abeni, Jamal Hadi Salim, Jiri Pirko, Linus Torvalds
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 <redacted>
Tested-by: hybris <redacted>
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