Netdev List
 help / color / mirror / Atom feed
* [PATCH net v2 1/2] net/sched: sch_hfsc: bound the classify inner-filter walk with a drift budget
@ 2026-09-14  8:06 Jamal Hadi Salim
  2026-09-14  8:06 ` [PATCH net v2 2/2] selftests: tc-testing: add a lateral-drift hfsc classify-walk test Jamal Hadi Salim
  2026-09-15  8:17 ` [PATCH net v2 1/2] net/sched: sch_hfsc: bound the classify inner-filter walk with a drift budget netdev-bot+sashiko
  0 siblings, 2 replies; 6+ messages in thread
From: Jamal Hadi Salim @ 2026-09-14  8:06 UTC (permalink / raw)
  To: netdev
  Cc: Jamal Hadi Salim, Jiri Pirko, David S . Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, Simon Horman, Victor Nogueira,
	Sashiko, hybris, stable

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,
leaving two binds that were each legal at bind time pointing at each
other; the classify walk then bounces between two interior classes
forever with the qdisc lock held and BH disabled — a soft lockup from a
single packet.

Bound the traversal, mirroring HTB: count only the non-descending hops,
which a legitimate walked tree can only take by level drift after bind
time; anything beyond a small budget is a cycle.
Drop the packet with a rate-limited warning when the budget is
exhausted. Descending hops never consume budget, so legitimately deep
trees are unaffected, and a terminating lateral chain (the level-drift
false-positive family) still classifies normally.

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/QDISC-CTUU.v1.20260908094501@mojatatu.com
Cc: stable@vger.kernel.org
Reviewed-by: Victor Nogueira <victor@mojatatu.com>
Tested-by: hybris <hybris@mojatatu.ai>
Signed-off-by: Jamal Hadi Salim <jhs@mojatatu.com>
---
v2 changes:
   -  Replace the absolute hop bound (root->level) with a budget that
      counts only non-descending hops, so a legal lateral level
      chain still classifies. (Sashiko)
   -  Reword the "strict descent" changelog claim (Sashiko).

 net/sched/sch_hfsc.c | 18 ++++++++++++++++++
 1 file changed, 18 insertions(+)

diff --git a/net/sched/sch_hfsc.c b/net/sched/sch_hfsc.c
index e87f5021a199..b073efaf2bf5 100644
--- a/net/sched/sch_hfsc.c
+++ b/net/sched/sch_hfsc.c
@@ -386,6 +386,12 @@ cftree_update(struct hfsc_class *cl)
 #define	SM_MASK		((1ULL << SM_SHIFT) - 1)
 #define	ISM_MASK	((1ULL << ISM_SHIFT) - 1)
 
+/*
+ * bound on consecutive non-descending (lateral / upward) hops in a
+ * classify walk, mirroring htb's TC_HTB_MAXDEPTH headroom
+ */
+#define	HFSC_CLASSIFY_MAX_DRIFT	8
+
 static inline u64
 seg_x2y(u64 x, u64 sm)
 {
@@ -1133,6 +1139,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 drift;
 	int result;
 
 	if (TC_H_MAJ(skb->priority ^ sch->handle) == 0 &&
@@ -1142,6 +1149,7 @@ hfsc_classify(struct sk_buff *skb, struct Qdisc *sch, int *qerr)
 
 	*qerr = NET_XMIT_SUCCESS | __NET_XMIT_BYPASS;
 	head = &q->root;
+	drift = HFSC_CLASSIFY_MAX_DRIFT;
 	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 +1175,16 @@ 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 (res.class is set
+		 * at bind time and levels drift after), so a walk can follow
+		 * lateral hops without descending; bound them here.
+		 */
+		if (cl->level >= head->level && drift-- == 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] 6+ messages in thread

* [PATCH net v2 2/2] selftests: tc-testing: add a lateral-drift hfsc classify-walk test
  2026-09-14  8:06 [PATCH net v2 1/2] net/sched: sch_hfsc: bound the classify inner-filter walk with a drift budget Jamal Hadi Salim
@ 2026-09-14  8:06 ` Jamal Hadi Salim
  2026-09-15  8:17   ` netdev-bot+sashiko
  2026-09-15  8:17 ` [PATCH net v2 1/2] net/sched: sch_hfsc: bound the classify inner-filter walk with a drift budget netdev-bot+sashiko
  1 sibling, 1 reply; 6+ messages in thread
From: Jamal Hadi Salim @ 2026-09-14  8:06 UTC (permalink / raw)
  To: netdev
  Cc: Jamal Hadi Salim, Jiri Pirko, David S . Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, Simon Horman, Victor Nogueira,
	Sashiko, hybris

The classify-loop fix bounds the number of consecutive lateral
hops, so the guard must not misfire on a legal
walk that reaches its leaf through a level-drift lateral chain. Add a
case that builds exactly that chain (root -> 1:1 -> 1:2 -> 1:3 ->
leaf 1:31, with 1:2/1:3 deepened to interior level 1 after the binds)
and asserts traffic still reaches the leaf. Passes on the patched
kernel; on a too-tight bound it would drop the packet instead.

Reviewed-by: Victor Nogueira <victor@mojatatu.com>
Tested-by: hybris <hybris@mojatatu.ai>
Signed-off-by: Jamal Hadi Salim <jhs@mojatatu.com>
---
 .../tc-testing/tc-tests/qdiscs/hfsc.json      | 34 +++++++++++++++++++
 1 file changed, 34 insertions(+)

diff --git a/tools/testing/selftests/tc-testing/tc-tests/qdiscs/hfsc.json b/tools/testing/selftests/tc-testing/tc-tests/qdiscs/hfsc.json
index c98c339424d4..82d2f8af1460 100644
--- a/tools/testing/selftests/tc-testing/tc-tests/qdiscs/hfsc.json
+++ b/tools/testing/selftests/tc-testing/tc-tests/qdiscs/hfsc.json
@@ -169,5 +169,39 @@
         "teardown": [
             "$TC qdisc del dev $DUMMY handle 1: root"
         ]
+    },
+    {
+        "id": "8c39",
+        "name": "HFSC classify walk still reaches leaf after lateral drift",
+        "category": [
+            "qdisc",
+            "hfsc"
+        ],
+        "plugins": {
+            "requires": "nsPlugin"
+        },
+        "setup": [
+            "ip link set lo up",
+            "$TC qdisc add dev lo handle 1: root hfsc default 31",
+            "$TC class add dev lo parent 1: classid 1:1 hfsc rt m2 100kbit",
+            "$TC class add dev lo parent 1: classid 1:2 hfsc rt m2 100kbit",
+            "$TC class add dev lo parent 1: classid 1:3 hfsc rt m2 100kbit",
+            "$TC class add dev lo parent 1:1 classid 1:10 hfsc rt m2 50kbit",
+            "$TC class add dev lo parent 1:2 classid 1:20 hfsc rt m2 10kbit",
+            "$TC class add dev lo parent 1:3 classid 1:30 hfsc rt m2 10kbit",
+            "$TC class add dev lo parent 1:3 classid 1:31 hfsc rt m2 100kbit",
+            "$TC filter add dev lo parent 1: protocol ip prio 1 u32 match u8 0 0 at 0 flowid 1:1",
+            "$TC filter add dev lo parent 1:1 protocol ip prio 1 u32 match u8 0 0 at 0 flowid 1:2",
+            "$TC filter add dev lo parent 1:2 protocol ip prio 1 u32 match u8 0 0 at 0 flowid 1:3",
+            "$TC filter add dev lo parent 1:3 protocol ip prio 1 u32 match u8 0 0 at 0 flowid 1:31"
+        ],
+        "cmdUnderTest": "ping -n -c 10 -W 1 127.0.0.1; sleep 1",
+        "expExitCode": "0",
+        "verifyCmd": "$TC -s class show dev lo",
+        "matchPattern": "class hfsc 1:31 parent 1:3 rt.*\\n.*Sent [0-9]+ bytes [1-9][0-9]* pkt",
+        "matchCount": "1",
+        "teardown": [
+            "$TC qdisc del dev lo handle 1: root"
+        ]
     }
 ]
-- 
2.43.0


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

* Re: [PATCH net v2 1/2] net/sched: sch_hfsc: bound the classify inner-filter walk with a drift budget
  2026-09-14  8:06 [PATCH net v2 1/2] net/sched: sch_hfsc: bound the classify inner-filter walk with a drift budget Jamal Hadi Salim
  2026-09-14  8:06 ` [PATCH net v2 2/2] selftests: tc-testing: add a lateral-drift hfsc classify-walk test Jamal Hadi Salim
@ 2026-09-15  8:17 ` netdev-bot+sashiko
  2026-09-16 11:22   ` Jamal Hadi Salim
  1 sibling, 1 reply; 6+ messages in thread
From: netdev-bot+sashiko @ 2026-09-15  8:17 UTC (permalink / raw)
  To: jhs
  Cc: netdev, jiri, davem, edumazet, kuba, pabeni, horms, victor,
	sashiko-bot, hybris

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

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

- [Medium] net/sched/sch_hfsc.c:hfsc_classify(): the new `drift` budget
  is a *cumulative* per-packet allowance of 8 non-descending hops, not…

--

net/sched: sch_hfsc: bound the classify inner-filter walk with a drift budget

hfsc_classify() can loop forever between two interior classes when two
flowid binds that were each legal at bind time become mutually pointing
after hfsc_adjust_levels() raises a level. The patch adds a budget of
non-descending hops to the classify walk and drops the packet with a
rate-limited warning when the budget runs out.

Link: https://sashiko.dev/#/patchset/QDISC-CTUU.v1.20260908094501@mojatatu.com

> diff --git a/net/sched/sch_hfsc.c b/net/sched/sch_hfsc.c
> index e87f5021a1995..b073efaf2bf5f 100644
> --- a/net/sched/sch_hfsc.c
> +++ b/net/sched/sch_hfsc.c
> @@ -386,6 +386,12 @@ cftree_update(struct hfsc_class *cl)
>  #define	SM_MASK		((1ULL << SM_SHIFT) - 1)
>  #define	ISM_MASK	((1ULL << ISM_SHIFT) - 1)
>  
> +/*
> + * bound on consecutive non-descending (lateral / upward) hops in a
> + * classify walk, mirroring htb's TC_HTB_MAXDEPTH headroom
> + */
> +#define	HFSC_CLASSIFY_MAX_DRIFT	8
> +
>  static inline u64
>  seg_x2y(u64 x, u64 sm)
>  {

[ ... ]

> @@ -1142,6 +1149,7 @@ hfsc_classify(struct sk_buff *skb, struct Qdisc *sch, int *qerr)
>  
>  	*qerr = NET_XMIT_SUCCESS | __NET_XMIT_BYPASS;
>  	head = &q->root;
> +	drift = HFSC_CLASSIFY_MAX_DRIFT;
>  	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 +1175,16 @@ 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 (res.class is set
> +		 * at bind time and levels drift after), so a walk can follow
> +		 * lateral hops without descending; bound them here.
> +		 */
> +		if (cl->level >= head->level && drift-- == 0) {
> +			pr_warn_ratelimited("hfsc: classify loop detected, dropping packet\n");
> +			return NULL;
> +		}

[Severity: Medium]
Can this drop packets for an acyclic configuration?  drift is seeded once
before the while loop in hfsc_classify() and the only mutation is the
post-decrement above, so a descending hop never returns any budget.  That
makes the budget cumulative over the whole walk rather than the bound on
"consecutive non-descending (lateral / upward) hops" the macro comment
describes, and rather than what the changelog says:

  "Bound the traversal, mirroring HTB: count only the non-descending hops"
  "a terminating lateral chain (the level-drift false-positive family)
   still classifies normally"

A terminating lateral chain longer than eight hops, or several shorter
lateral segments separated by legal descents, would exhaust the budget.
Such a chain is constructible with plain tc commands because bindings are
only checked at bind time:

net/sched/sch_hfsc.c:hfsc_bind_tcf() {
	if (cl != NULL) {
		if (p != NULL && p->level <= cl->level)
			return 0;

and levels are raised afterwards with no revalidation:

net/sched/sch_hfsc.c:hfsc_adjust_levels() {
	do {
		level = 0;
		list_for_each_entry(p, &cl->children, siblings) {
			if (p->level >= level)
				level = p->level + 1;
		}
		WRITE_ONCE(cl->level, level);
	} while ((cl = cl->cl_parent) != NULL);

So for sibling classes A1..A10, each with one leaf child, binding a filter
on A_i with flowid A_{i+1} while A_{i+1} is still a leaf passes
hfsc_bind_tcf(), and adding a child to A_{i+1} afterwards turns that hop
lateral.  Would resetting drift back to HFSC_CLASSIFY_MAX_DRIFT on every
descending hop match the documented intent?

Should this path use break instead of return NULL?  The two existing
failure exits in the same loop break out to the default 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 */
		}

	/* classification failed, try default class */
	cl = hfsc_find_class(TC_H_MAKE(TC_H_MAJ(sch->handle),
				       READ_ONCE(q->defcls)), sch);

With return NULL, a configured defcls leaf is skipped and hfsc_enqueue()
does qdisc_qstats_drop(sch) plus __qdisc_drop(skb, to_free).

On the macro comment itself, is "mirroring htb's TC_HTB_MAXDEPTH headroom"
accurate?  htb_classify() increments hops on every inner hop and HTB
enforces a maximum depth of TC_HTB_MAXDEPTH structurally, so its bound
cannot reject a legal walk.  HFSC caps neither class level nor hierarchy
depth, so the value 8 has no equivalent backing here.  Could the comment
and the changelog sentence "anything beyond a small budget is a cycle" be
reworded to describe the heuristic that is actually implemented?

Also, the warning text asserts "classify loop detected" on a walk where no
class has been revisited.  Would a message that reports the budget being
exhausted be more accurate?

>  		/* apply inner filter chain */
>  		tcf = rcu_dereference_bh(cl->filter_list);
>  		head = cl;

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

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

* Re: [PATCH net v2 2/2] selftests: tc-testing: add a lateral-drift hfsc classify-walk test
  2026-09-14  8:06 ` [PATCH net v2 2/2] selftests: tc-testing: add a lateral-drift hfsc classify-walk test Jamal Hadi Salim
@ 2026-09-15  8:17   ` netdev-bot+sashiko
  2026-09-16 11:23     ` Jamal Hadi Salim
  0 siblings, 1 reply; 6+ messages in thread
From: netdev-bot+sashiko @ 2026-09-15  8:17 UTC (permalink / raw)
  To: jhs
  Cc: netdev, jiri, davem, edumazet, kuba, pabeni, horms, victor,
	sashiko-bot, hybris

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

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

- [High] Test 8c39 ("HFSC classify walk still reaches leaf after lateral
  drift") never exercises the new lateral-hop drift budget in…
- [Medium] Test 8c39 makes the qdisc's default class the same leaf it
  asserts on: the root qdisc is created with `hfsc default 31`…
- [Medium] Test 8c39's cmdUnderTest is the compound shell command `ping
  -n -c 10 -W 1 127.0.0.1; sleep 1` with expExitCode "0"…

--

selftests: tc-testing: add a lateral-drift hfsc classify-walk test

This adds tc-testing case 8c39, which builds an HFSC hierarchy with
lateral filter binds (root -> 1:1 -> 1:2 -> 1:3 -> leaf 1:31), pings
through it, and checks that packets are counted on class 1:31.  The
intent is to show that the new classify-walk drift budget does not
misfire on a legal lateral chain.

> diff --git a/tools/testing/selftests/tc-testing/tc-tests/qdiscs/hfsc.json b/tools/testing/selftests/tc-testing/tc-tests/qdiscs/hfsc.json
> index c98c339424d4e..82d2f8af1460d 100644
> --- a/tools/testing/selftests/tc-testing/tc-tests/qdiscs/hfsc.json
> +++ b/tools/testing/selftests/tc-testing/tc-tests/qdiscs/hfsc.json
> @@ -169,5 +169,39 @@
>          "teardown": [
>              "$TC qdisc del dev $DUMMY handle 1: root"
>          ]
> +    },
> +    {
> +        "id": "8c39",
> +        "name": "HFSC classify walk still reaches leaf after lateral drift",
> +        "category": [
> +            "qdisc",
> +            "hfsc"
> +        ],
> +        "plugins": {
> +            "requires": "nsPlugin"
> +        },
> +        "setup": [
> +            "ip link set lo up",
> +            "$TC qdisc add dev lo handle 1: root hfsc default 31",
> +            "$TC class add dev lo parent 1: classid 1:1 hfsc rt m2 100kbit",
> +            "$TC class add dev lo parent 1: classid 1:2 hfsc rt m2 100kbit",
> +            "$TC class add dev lo parent 1: classid 1:3 hfsc rt m2 100kbit",
> +            "$TC class add dev lo parent 1:1 classid 1:10 hfsc rt m2 50kbit",
> +            "$TC class add dev lo parent 1:2 classid 1:20 hfsc rt m2 10kbit",
> +            "$TC class add dev lo parent 1:3 classid 1:30 hfsc rt m2 10kbit",
> +            "$TC class add dev lo parent 1:3 classid 1:31 hfsc rt m2 100kbit",
> +            "$TC filter add dev lo parent 1: protocol ip prio 1 u32 match u8 0 0 at 0 flowid 1:1",
> +            "$TC filter add dev lo parent 1:1 protocol ip prio 1 u32 match u8 0 0 at 0 flowid 1:2",
> +            "$TC filter add dev lo parent 1:2 protocol ip prio 1 u32 match u8 0 0 at 0 flowid 1:3",
> +            "$TC filter add dev lo parent 1:3 protocol ip prio 1 u32 match u8 0 0 at 0 flowid 1:31"
> +        ],

[Severity: High]
Does this setup order actually build the lateral chain the test needs?

All of the child class adds (1:10, 1:20, 1:30, 1:31) come before the
filter adds, so by the time the lateral binds are issued 1:2 and 1:3 are
already interior classes at level 1.

For "$TC filter add dev lo parent 1:1 ... flowid 1:2", the class 1:1 is
passed as the bind base, and hfsc_bind_tcf() rejects the bind:

net/sched/sch_hfsc.c:hfsc_bind_tcf() {
	...
	if (cl != NULL) {
		if (p != NULL && p->level <= cl->level)
			return 0;
		qdisc_class_get(&cl->cl_common);
	}
	...
}

With p = 1:1 (level 1) and cl = 1:2 (level 1), p->level <= cl->level
holds, so 0 is returned.  __tcf_bind_filter() then stores that as
res.class:

include/net/pkt_cls.h:__tcf_bind_filter() {
	cl = q->ops->cl_ops->bind_tcf(q, base, r->classid);
	cl = __cls_set_class(&r->class, cl);
	...
}

Nothing is propagated back to userspace, so the tc command still exits 0
and the setup step looks like it succeeded.  The same applies to the
1:2 -> 1:3 filter.

At classify time the second hop therefore has res.class == NULL and the
walk stops at the pre-existing downward-only check rather than at the new
drift check:

net/sched/sch_hfsc.c:hfsc_classify() {
	...
	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 */
	}
	...
}

1 >= 1 breaks the loop, so the drift budget is never consumed and the
test would pass identically with a bound of 0.

Related to that, the commit message says the chain is built "with 1:2/1:3
deepened to interior level 1 after the binds", but in the setup list the
deepening class adds precede the binds.  Should each deepening add follow
its corresponding lateral bind instead, i.e. bind 1:1 -> 1:2 while 1:2 is
still level 0, then add 1:20; bind 1:2 -> 1:3 while 1:3 is still level 0,
then add 1:30 and 1:31?

The commit message also states "on a too-tight bound it would drop the
packet instead" - with the current ordering, is that still true?

> +        "cmdUnderTest": "ping -n -c 10 -W 1 127.0.0.1; sleep 1",
> +        "expExitCode": "0",

[Severity: Medium]
Which command's exit status does expExitCode compare against here?

tdc runs cmdUnderTest through a shell:

tools/testing/selftests/tc-testing/tdc.py:exec_cmd() {
	proc = subprocess.Popen(command,
	    shell=True,
	    stdout=subprocess.PIPE,
	    stderr=subprocess.PIPE,
	    env=ENVIR)
	...
}

Because the ';' is a shell separator, proc.returncode is the status of
sleep 1, which is always 0.  Does that make the expExitCode "0" check
unable to fail even if ping is missing or loses every echo request?

There is a second effect from the compound command.  nsPlugin prepends
the netns wrapper only to the head of the split command:

tools/testing/selftests/tc-testing/plugin-lib/nsPlugin.py:adjust_command() {
	cmdlist = command.split()
	...
	cmdlist.insert(0, self.args.NAMES['NS'])
	cmdlist.insert(0, 'exec')
	cmdlist.insert(0, 'netns')
	cmdlist.insert(0, self.args.NAMES['IP'])
	...
	command = ' '.join(cmdlist)
}

So does "sleep 1" run in the initial namespace rather than in the test
namespace?  Would putting the delay in the verify step, or dropping it,
keep the whole command inside the namespace and let the ping status be
the one that is checked?

> +        "verifyCmd": "$TC -s class show dev lo",
> +        "matchPattern": "class hfsc 1:31 parent 1:3 rt.*\\n.*Sent [0-9]+ bytes [1-9][0-9]* pkt",
> +        "matchCount": "1",

[Severity: Medium]
Can this assertion distinguish a successful lateral walk from a failed
classification?

The root qdisc is created with "hfsc default 31" and the pattern counts
packets on class 1:31, which is the same class the default fallback
selects:

net/sched/sch_hfsc.c:hfsc_classify() {
	...
	/* classification failed, try default class */
	cl = hfsc_find_class(TC_H_MAKE(TC_H_MAJ(sch->handle),
				       READ_ONCE(q->defcls)), sch);
	if (cl == NULL || cl->level > 0)
		return NULL;

	return cl;
}

Any classification failure - a rejected bind, an early downward-only
break, or a future misfire of the drift guard - still credits the packets
to 1:31, so the tc output looks the same either way.

Would a default class distinct from the walk's terminal leaf (for example
default 30), or a default that is not a valid leaf so failures drop, make
this test able to fail when the guard misbehaves?

> +        "teardown": [
> +            "$TC qdisc del dev lo handle 1: root"
> +        ]
>      }
>  ]

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

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

* Re: [PATCH net v2 1/2] net/sched: sch_hfsc: bound the classify inner-filter walk with a drift budget
  2026-09-15  8:17 ` [PATCH net v2 1/2] net/sched: sch_hfsc: bound the classify inner-filter walk with a drift budget netdev-bot+sashiko
@ 2026-09-16 11:22   ` Jamal Hadi Salim
  0 siblings, 0 replies; 6+ messages in thread
From: Jamal Hadi Salim @ 2026-09-16 11:22 UTC (permalink / raw)
  To: netdev-bot+sashiko
  Cc: netdev, jiri, davem, edumazet, kuba, pabeni, horms, victor,
	sashiko-bot, hybris

On Tue, Sep 15, 2026 at 4:17 AM <netdev-bot+sashiko@kernel.org> wrote:
>
> Thank you for your contribution! Sashiko AI review found 1 potential
> issue(s) to consider:
>
> Critical: 0 · High: 0 · Medium: 1 · Low: 0
>
> - [Medium] net/sched/sch_hfsc.c:hfsc_classify(): the new `drift` budget
>   is a *cumulative* per-packet allowance of 8 non-descending hops, not…
>


I am going to send a v3 - i think the tdc test needs some improvement.

pw-bot:cr

cheers,
jamal
> --
>
> net/sched: sch_hfsc: bound the classify inner-filter walk with a drift budget
>
> hfsc_classify() can loop forever between two interior classes when two
> flowid binds that were each legal at bind time become mutually pointing
> after hfsc_adjust_levels() raises a level. The patch adds a budget of
> non-descending hops to the classify walk and drops the packet with a
> rate-limited warning when the budget runs out.
>
> Link: https://sashiko.dev/#/patchset/QDISC-CTUU.v1.20260908094501@mojatatu.com
>
> > diff --git a/net/sched/sch_hfsc.c b/net/sched/sch_hfsc.c
> > index e87f5021a1995..b073efaf2bf5f 100644
> > --- a/net/sched/sch_hfsc.c
> > +++ b/net/sched/sch_hfsc.c
> > @@ -386,6 +386,12 @@ cftree_update(struct hfsc_class *cl)
> >  #define      SM_MASK         ((1ULL << SM_SHIFT) - 1)
> >  #define      ISM_MASK        ((1ULL << ISM_SHIFT) - 1)
> >
> > +/*
> > + * bound on consecutive non-descending (lateral / upward) hops in a
> > + * classify walk, mirroring htb's TC_HTB_MAXDEPTH headroom
> > + */
> > +#define      HFSC_CLASSIFY_MAX_DRIFT 8
> > +
> >  static inline u64
> >  seg_x2y(u64 x, u64 sm)
> >  {
>
> [ ... ]
>
> > @@ -1142,6 +1149,7 @@ hfsc_classify(struct sk_buff *skb, struct Qdisc *sch, int *qerr)
> >
> >       *qerr = NET_XMIT_SUCCESS | __NET_XMIT_BYPASS;
> >       head = &q->root;
> > +     drift = HFSC_CLASSIFY_MAX_DRIFT;
> >       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 +1175,16 @@ 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 (res.class is set
> > +              * at bind time and levels drift after), so a walk can follow
> > +              * lateral hops without descending; bound them here.
> > +              */
> > +             if (cl->level >= head->level && drift-- == 0) {
> > +                     pr_warn_ratelimited("hfsc: classify loop detected, dropping packet\n");
> > +                     return NULL;
> > +             }
>
> [Severity: Medium]
> Can this drop packets for an acyclic configuration?  drift is seeded once
> before the while loop in hfsc_classify() and the only mutation is the
> post-decrement above, so a descending hop never returns any budget.  That
> makes the budget cumulative over the whole walk rather than the bound on
> "consecutive non-descending (lateral / upward) hops" the macro comment
> describes, and rather than what the changelog says:
>
>   "Bound the traversal, mirroring HTB: count only the non-descending hops"
>   "a terminating lateral chain (the level-drift false-positive family)
>    still classifies normally"
>
> A terminating lateral chain longer than eight hops, or several shorter
> lateral segments separated by legal descents, would exhaust the budget.
> Such a chain is constructible with plain tc commands because bindings are
> only checked at bind time:
>
> net/sched/sch_hfsc.c:hfsc_bind_tcf() {
>         if (cl != NULL) {
>                 if (p != NULL && p->level <= cl->level)
>                         return 0;
>
> and levels are raised afterwards with no revalidation:
>
> net/sched/sch_hfsc.c:hfsc_adjust_levels() {
>         do {
>                 level = 0;
>                 list_for_each_entry(p, &cl->children, siblings) {
>                         if (p->level >= level)
>                                 level = p->level + 1;
>                 }
>                 WRITE_ONCE(cl->level, level);
>         } while ((cl = cl->cl_parent) != NULL);
>
> So for sibling classes A1..A10, each with one leaf child, binding a filter
> on A_i with flowid A_{i+1} while A_{i+1} is still a leaf passes
> hfsc_bind_tcf(), and adding a child to A_{i+1} afterwards turns that hop
> lateral.  Would resetting drift back to HFSC_CLASSIFY_MAX_DRIFT on every
> descending hop match the documented intent?
>
> Should this path use break instead of return NULL?  The two existing
> failure exits in the same loop break out to the default 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 */
>                 }
>
>         /* classification failed, try default class */
>         cl = hfsc_find_class(TC_H_MAKE(TC_H_MAJ(sch->handle),
>                                        READ_ONCE(q->defcls)), sch);
>
> With return NULL, a configured defcls leaf is skipped and hfsc_enqueue()
> does qdisc_qstats_drop(sch) plus __qdisc_drop(skb, to_free).
>
> On the macro comment itself, is "mirroring htb's TC_HTB_MAXDEPTH headroom"
> accurate?  htb_classify() increments hops on every inner hop and HTB
> enforces a maximum depth of TC_HTB_MAXDEPTH structurally, so its bound
> cannot reject a legal walk.  HFSC caps neither class level nor hierarchy
> depth, so the value 8 has no equivalent backing here.  Could the comment
> and the changelog sentence "anything beyond a small budget is a cycle" be
> reworded to describe the heuristic that is actually implemented?
>
> Also, the warning text asserts "classify loop detected" on a walk where no
> class has been revisited.  Would a message that reports the budget being
> exhausted be more accurate?
>
> >               /* apply inner filter chain */
> >               tcf = rcu_dereference_bh(cl->filter_list);
> >               head = cl;
>
> --
> Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/QDISC-CTUU.v2.20260913192614%40mojatatu.com

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

* Re: [PATCH net v2 2/2] selftests: tc-testing: add a lateral-drift hfsc classify-walk test
  2026-09-15  8:17   ` netdev-bot+sashiko
@ 2026-09-16 11:23     ` Jamal Hadi Salim
  0 siblings, 0 replies; 6+ messages in thread
From: Jamal Hadi Salim @ 2026-09-16 11:23 UTC (permalink / raw)
  To: netdev-bot+sashiko
  Cc: netdev, jiri, davem, edumazet, kuba, pabeni, horms, victor,
	sashiko-bot, hybris

On Tue, Sep 15, 2026 at 4:17 AM <netdev-bot+sashiko@kernel.org> wrote:
>
> Thank you for your contribution! Sashiko AI review found 3 potential
> issue(s) to consider:
>
> Critical: 0 · High: 1 · Medium: 2 · Low: 0
>

pw-bot:cr

cheers,
jamal

> - [High] Test 8c39 ("HFSC classify walk still reaches leaf after lateral
>   drift") never exercises the new lateral-hop drift budget in…
> - [Medium] Test 8c39 makes the qdisc's default class the same leaf it
>   asserts on: the root qdisc is created with `hfsc default 31`…
> - [Medium] Test 8c39's cmdUnderTest is the compound shell command `ping
>   -n -c 10 -W 1 127.0.0.1; sleep 1` with expExitCode "0"…
>
> --
>
> selftests: tc-testing: add a lateral-drift hfsc classify-walk test
>
> This adds tc-testing case 8c39, which builds an HFSC hierarchy with
> lateral filter binds (root -> 1:1 -> 1:2 -> 1:3 -> leaf 1:31), pings
> through it, and checks that packets are counted on class 1:31.  The
> intent is to show that the new classify-walk drift budget does not
> misfire on a legal lateral chain.
>
> > diff --git a/tools/testing/selftests/tc-testing/tc-tests/qdiscs/hfsc.json b/tools/testing/selftests/tc-testing/tc-tests/qdiscs/hfsc.json
> > index c98c339424d4e..82d2f8af1460d 100644
> > --- a/tools/testing/selftests/tc-testing/tc-tests/qdiscs/hfsc.json
> > +++ b/tools/testing/selftests/tc-testing/tc-tests/qdiscs/hfsc.json
> > @@ -169,5 +169,39 @@
> >          "teardown": [
> >              "$TC qdisc del dev $DUMMY handle 1: root"
> >          ]
> > +    },
> > +    {
> > +        "id": "8c39",
> > +        "name": "HFSC classify walk still reaches leaf after lateral drift",
> > +        "category": [
> > +            "qdisc",
> > +            "hfsc"
> > +        ],
> > +        "plugins": {
> > +            "requires": "nsPlugin"
> > +        },
> > +        "setup": [
> > +            "ip link set lo up",
> > +            "$TC qdisc add dev lo handle 1: root hfsc default 31",
> > +            "$TC class add dev lo parent 1: classid 1:1 hfsc rt m2 100kbit",
> > +            "$TC class add dev lo parent 1: classid 1:2 hfsc rt m2 100kbit",
> > +            "$TC class add dev lo parent 1: classid 1:3 hfsc rt m2 100kbit",
> > +            "$TC class add dev lo parent 1:1 classid 1:10 hfsc rt m2 50kbit",
> > +            "$TC class add dev lo parent 1:2 classid 1:20 hfsc rt m2 10kbit",
> > +            "$TC class add dev lo parent 1:3 classid 1:30 hfsc rt m2 10kbit",
> > +            "$TC class add dev lo parent 1:3 classid 1:31 hfsc rt m2 100kbit",
> > +            "$TC filter add dev lo parent 1: protocol ip prio 1 u32 match u8 0 0 at 0 flowid 1:1",
> > +            "$TC filter add dev lo parent 1:1 protocol ip prio 1 u32 match u8 0 0 at 0 flowid 1:2",
> > +            "$TC filter add dev lo parent 1:2 protocol ip prio 1 u32 match u8 0 0 at 0 flowid 1:3",
> > +            "$TC filter add dev lo parent 1:3 protocol ip prio 1 u32 match u8 0 0 at 0 flowid 1:31"
> > +        ],
>
> [Severity: High]
> Does this setup order actually build the lateral chain the test needs?
>
> All of the child class adds (1:10, 1:20, 1:30, 1:31) come before the
> filter adds, so by the time the lateral binds are issued 1:2 and 1:3 are
> already interior classes at level 1.
>
> For "$TC filter add dev lo parent 1:1 ... flowid 1:2", the class 1:1 is
> passed as the bind base, and hfsc_bind_tcf() rejects the bind:
>
> net/sched/sch_hfsc.c:hfsc_bind_tcf() {
>         ...
>         if (cl != NULL) {
>                 if (p != NULL && p->level <= cl->level)
>                         return 0;
>                 qdisc_class_get(&cl->cl_common);
>         }
>         ...
> }
>
> With p = 1:1 (level 1) and cl = 1:2 (level 1), p->level <= cl->level
> holds, so 0 is returned.  __tcf_bind_filter() then stores that as
> res.class:
>
> include/net/pkt_cls.h:__tcf_bind_filter() {
>         cl = q->ops->cl_ops->bind_tcf(q, base, r->classid);
>         cl = __cls_set_class(&r->class, cl);
>         ...
> }
>
> Nothing is propagated back to userspace, so the tc command still exits 0
> and the setup step looks like it succeeded.  The same applies to the
> 1:2 -> 1:3 filter.
>
> At classify time the second hop therefore has res.class == NULL and the
> walk stops at the pre-existing downward-only check rather than at the new
> drift check:
>
> net/sched/sch_hfsc.c:hfsc_classify() {
>         ...
>         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 */
>         }
>         ...
> }
>
> 1 >= 1 breaks the loop, so the drift budget is never consumed and the
> test would pass identically with a bound of 0.
>
> Related to that, the commit message says the chain is built "with 1:2/1:3
> deepened to interior level 1 after the binds", but in the setup list the
> deepening class adds precede the binds.  Should each deepening add follow
> its corresponding lateral bind instead, i.e. bind 1:1 -> 1:2 while 1:2 is
> still level 0, then add 1:20; bind 1:2 -> 1:3 while 1:3 is still level 0,
> then add 1:30 and 1:31?
>
> The commit message also states "on a too-tight bound it would drop the
> packet instead" - with the current ordering, is that still true?
>
> > +        "cmdUnderTest": "ping -n -c 10 -W 1 127.0.0.1; sleep 1",
> > +        "expExitCode": "0",
>
> [Severity: Medium]
> Which command's exit status does expExitCode compare against here?
>
> tdc runs cmdUnderTest through a shell:
>
> tools/testing/selftests/tc-testing/tdc.py:exec_cmd() {
>         proc = subprocess.Popen(command,
>             shell=True,
>             stdout=subprocess.PIPE,
>             stderr=subprocess.PIPE,
>             env=ENVIR)
>         ...
> }
>
> Because the ';' is a shell separator, proc.returncode is the status of
> sleep 1, which is always 0.  Does that make the expExitCode "0" check
> unable to fail even if ping is missing or loses every echo request?
>
> There is a second effect from the compound command.  nsPlugin prepends
> the netns wrapper only to the head of the split command:
>
> tools/testing/selftests/tc-testing/plugin-lib/nsPlugin.py:adjust_command() {
>         cmdlist = command.split()
>         ...
>         cmdlist.insert(0, self.args.NAMES['NS'])
>         cmdlist.insert(0, 'exec')
>         cmdlist.insert(0, 'netns')
>         cmdlist.insert(0, self.args.NAMES['IP'])
>         ...
>         command = ' '.join(cmdlist)
> }
>
> So does "sleep 1" run in the initial namespace rather than in the test
> namespace?  Would putting the delay in the verify step, or dropping it,
> keep the whole command inside the namespace and let the ping status be
> the one that is checked?
>
> > +        "verifyCmd": "$TC -s class show dev lo",
> > +        "matchPattern": "class hfsc 1:31 parent 1:3 rt.*\\n.*Sent [0-9]+ bytes [1-9][0-9]* pkt",
> > +        "matchCount": "1",
>
> [Severity: Medium]
> Can this assertion distinguish a successful lateral walk from a failed
> classification?
>
> The root qdisc is created with "hfsc default 31" and the pattern counts
> packets on class 1:31, which is the same class the default fallback
> selects:
>
> net/sched/sch_hfsc.c:hfsc_classify() {
>         ...
>         /* classification failed, try default class */
>         cl = hfsc_find_class(TC_H_MAKE(TC_H_MAJ(sch->handle),
>                                        READ_ONCE(q->defcls)), sch);
>         if (cl == NULL || cl->level > 0)
>                 return NULL;
>
>         return cl;
> }
>
> Any classification failure - a rejected bind, an early downward-only
> break, or a future misfire of the drift guard - still credits the packets
> to 1:31, so the tc output looks the same either way.
>
> Would a default class distinct from the walk's terminal leaf (for example
> default 30), or a default that is not a valid leaf so failures drop, make
> this test able to fail when the guard misbehaves?
>
> > +        "teardown": [
> > +            "$TC qdisc del dev lo handle 1: root"
> > +        ]
> >      }
> >  ]
>
> --
> Sashiko AI review · https://netdev-ai.bots.linux.dev/sashiko/#/patchset/QDISC-CTUU.v2.20260913192614%40mojatatu.com

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

end of thread, other threads:[~2026-09-16 11:23 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-14  8:06 [PATCH net v2 1/2] net/sched: sch_hfsc: bound the classify inner-filter walk with a drift budget Jamal Hadi Salim
2026-09-14  8:06 ` [PATCH net v2 2/2] selftests: tc-testing: add a lateral-drift hfsc classify-walk test Jamal Hadi Salim
2026-09-15  8:17   ` netdev-bot+sashiko
2026-09-16 11:23     ` Jamal Hadi Salim
2026-09-15  8:17 ` [PATCH net v2 1/2] net/sched: sch_hfsc: bound the classify inner-filter walk with a drift budget netdev-bot+sashiko
2026-09-16 11:22   ` 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