* [PATCH net-next v3] net/sched: Avoid quadratic handle scan in qdisc_alloc_handle
@ 2026-09-18 13:51 Victor Nogueira
2026-09-22 0:40 ` patchwork-bot+netdevbpf
0 siblings, 1 reply; 2+ messages in thread
From: Victor Nogueira @ 2026-09-18 13:51 UTC (permalink / raw)
To: davem, edumazet, kuba, pabeni, jhs, jiri; +Cc: horms, vega, netdev
qdisc_alloc_handle scans for a free auto-handle by probing the
per-device qdisc hash one handle at a time. With the 16-bucket hash
populated by tens of thousands of qdiscs, the failing scan is O(N^2)
under rtnl_lock. A single failing add then holds rtnl_lock for most of a
second, and rtnl_lock is global, so all other netlink configuration is
blocked for that time.
First try the handle after the rotating cursor with one qdisc_lookup,
as the first iteration of the current loop does; that handle is
normally free. Only when it is taken, build a bitmap of the occupied
auto-handle majors in one pass over the hash and pick the first free
major from the cursor. The scan is cyclic over all 0x7FFF majors
starting from the same autohandle + 1 position as the current loop, so
it returns the same handle for the same device state and fails only
when all 0x7FFF majors are taken.
The root qdisc is seeded separately because qdisc_hash_add skips
parent == TC_H_ROOT.
When the next handle is free, only the single qdisc_lookup of the
current loop's first iteration runs; when it is taken, one O(N) walk
follows.
Measured as tc wall time on a defconfig kernel in a 2-vCPU guest, with
an HTB root holding 32768 classes and all 32767 auto-handles in use (a
no-op tc command takes 5-6.5 ms):
unpatched patched
failing add, space exhausted 613-749 ms 8-10 ms
add after a mid-range delete 572-573 ms 9 ms
The failure path returns -ENOMEM (bitmap allocation) or -ENOSPC (space
exhausted) through a handle out-parameter instead of the overloaded 0
return. Since either error is now possible, the extack message changes
from "Maximum number of qdisc handles was exceeded" to "Failed to
allocate a qdisc handle".
Reported-by: Vega <vega@nebusec.ai>
Co-developed-by: Jamal Hadi Salim <jhs@mojatatu.com>
Signed-off-by: Jamal Hadi Salim <jhs@mojatatu.com>
Signed-off-by: Victor Nogueira <victor@mojatatu.com>
Reviewed-by: Simon Horman <horms@kernel.org>
---
v2 -> v3:
- Removed fixes tag
v2: https://lore.kernel.org/netdev/20260917124725.4127273-1-victor@mojatatu.com
v1 -> v2:
- Retarget from net to net-next - Jakub
- No code changes
v1: https://lore.kernel.org/netdev/20260911133146.3440982-1-victor@mojatatu.com
---
---
net/sched/sch_api.c | 86 ++++++++++++++++++++++++++++++++++++---------
1 file changed, 70 insertions(+), 16 deletions(-)
diff --git a/net/sched/sch_api.c b/net/sched/sch_api.c
index 463ededcdcfe..f36c41224bcc 100644
--- a/net/sched/sch_api.c
+++ b/net/sched/sch_api.c
@@ -21,6 +21,7 @@
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/kmod.h>
+#include <linux/bitmap.h>
#include <linux/list.h>
#include <linux/hrtimer.h>
#include <linux/slab.h>
@@ -770,22 +771,75 @@ void qdisc_class_hash_remove(struct Qdisc_class_hash *clhash,
}
EXPORT_SYMBOL(qdisc_class_hash_remove);
-/* Allocate an unique handle from space managed by kernel
- * Possible range is [8000-FFFF]:0000 (0x8000 values)
+#define QDISC_AUTO_HANDLE_BASE 0x8000 /* first auto-handle major */
+#define QDISC_AUTO_HANDLE_END 0xFFFE /* last auto-handle major */
+#define QDISC_AUTO_HANDLE_COUNT (QDISC_AUTO_HANDLE_END - \
+ QDISC_AUTO_HANDLE_BASE + 1)
+
+/* Allocate a unique handle from space managed by kernel
+ * Possible range is [8000-FFFE]:0000 (0x7FFF values); 0xFFFF is the
+ * TC_H_ROOT/ingress major.
*/
-static u32 qdisc_alloc_handle(struct net_device *dev)
+static int qdisc_alloc_handle(struct net_device *dev, u32 *handlep)
{
- int i = 0x8000;
static u32 autohandle = TC_H_MAKE(0x80000000U, 0);
+ unsigned long start, bit;
+ unsigned long *bitmap;
+ struct Qdisc *q;
+ u32 handle, maj;
+ int b;
+
+ start = ((autohandle >> 16) - QDISC_AUTO_HANDLE_BASE + 1) %
+ QDISC_AUTO_HANDLE_COUNT;
+
+ /* The cursor moves past every handle it hands out, so the next
+ * handle is normally free: try it with one hashed lookup before
+ * walking the whole hash.
+ */
+ handle = (start + QDISC_AUTO_HANDLE_BASE) << 16;
+ if (!qdisc_lookup(dev, handle)) {
+ autohandle = handle;
+ *handlep = handle;
+ return 0;
+ }
- do {
- autohandle += TC_H_MAKE(0x10000U, 0);
- if (autohandle == TC_H_MAKE(TC_H_ROOT, 0))
- autohandle = TC_H_MAKE(0x80000000U, 0);
- if (!qdisc_lookup(dev, autohandle))
- return autohandle;
- cond_resched();
- } while (--i > 0);
+ bitmap = bitmap_zalloc(QDISC_AUTO_HANDLE_COUNT, GFP_KERNEL);
+ if (!bitmap)
+ return -ENOMEM;
+
+ /* Mark the occupied auto-handle majors: the hash holds all
+ * qdiscs except root and ingress ones, and the root can carry
+ * an auto-range handle.
+ */
+ q = rtnl_dereference(dev->qdisc);
+ maj = TC_H_MAJ(q->handle) >> 16;
+ if (!(q->flags & TCQ_F_BUILTIN) && !TC_H_MIN(q->handle) &&
+ maj >= QDISC_AUTO_HANDLE_BASE && maj <= QDISC_AUTO_HANDLE_END)
+ set_bit(maj - QDISC_AUTO_HANDLE_BASE, bitmap);
+
+ hash_for_each(dev->qdisc_hash, b, q, hash) {
+ maj = TC_H_MAJ(q->handle) >> 16;
+ if (maj >= QDISC_AUTO_HANDLE_BASE &&
+ maj <= QDISC_AUTO_HANDLE_END && !TC_H_MIN(q->handle))
+ set_bit(maj - QDISC_AUTO_HANDLE_BASE, bitmap);
+ }
+
+ bit = find_next_zero_bit(bitmap, QDISC_AUTO_HANDLE_COUNT, start);
+ if (bit >= QDISC_AUTO_HANDLE_COUNT) {
+ bit = find_first_zero_bit(bitmap, start);
+ if (bit >= start)
+ bit = QDISC_AUTO_HANDLE_COUNT;
+ }
+
+ if (bit >= QDISC_AUTO_HANDLE_COUNT) {
+ bitmap_free(bitmap);
+ return -ENOSPC;
+ }
+
+ handle = (bit + QDISC_AUTO_HANDLE_BASE) << 16;
+ autohandle = handle;
+ bitmap_free(bitmap);
+ *handlep = handle;
return 0;
}
@@ -1308,10 +1362,10 @@ static struct Qdisc *qdisc_create(struct net_device *dev,
handle = TC_H_MAKE(TC_H_INGRESS, 0);
} else {
if (handle == 0) {
- handle = qdisc_alloc_handle(dev);
- if (handle == 0) {
- NL_SET_ERR_MSG(extack, "Maximum number of qdisc handles was exceeded");
- err = -ENOSPC;
+ err = qdisc_alloc_handle(dev, &handle);
+ if (err) {
+ NL_SET_ERR_MSG(extack,
+ "Failed to allocate a qdisc handle");
goto err_out3;
}
}
--
2.55.0
^ permalink raw reply related [flat|nested] 2+ messages in thread
* Re: [PATCH net-next v3] net/sched: Avoid quadratic handle scan in qdisc_alloc_handle
2026-09-18 13:51 [PATCH net-next v3] net/sched: Avoid quadratic handle scan in qdisc_alloc_handle Victor Nogueira
@ 2026-09-22 0:40 ` patchwork-bot+netdevbpf
0 siblings, 0 replies; 2+ messages in thread
From: patchwork-bot+netdevbpf @ 2026-09-22 0:40 UTC (permalink / raw)
To: Victor Nogueira
Cc: davem, edumazet, kuba, pabeni, jhs, jiri, horms, vega, netdev
Hello:
This patch was applied to netdev/net-next.git (main)
by Jakub Kicinski <kuba@kernel.org>:
On Fri, 18 Sep 2026 10:51:57 -0300 you wrote:
> qdisc_alloc_handle scans for a free auto-handle by probing the
> per-device qdisc hash one handle at a time. With the 16-bucket hash
> populated by tens of thousands of qdiscs, the failing scan is O(N^2)
> under rtnl_lock. A single failing add then holds rtnl_lock for most of a
> second, and rtnl_lock is global, so all other netlink configuration is
> blocked for that time.
>
> [...]
Here is the summary with links:
- [net-next,v3] net/sched: Avoid quadratic handle scan in qdisc_alloc_handle
https://git.kernel.org/netdev/net-next/c/1b4efa0399f5
You are awesome, thank you!
--
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/patchwork/pwbot.html
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-09-22 0:41 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-18 13:51 [PATCH net-next v3] net/sched: Avoid quadratic handle scan in qdisc_alloc_handle Victor Nogueira
2026-09-22 0:40 ` patchwork-bot+netdevbpf
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox