From 1b4efa0399f549d9b9bf82b3e2450e10a06d65b8 Mon Sep 17 00:00:00 2001 From: Victor Nogueira Date: Fri, 18 Sep 2026 10:51:57 -0300 Subject: [PATCH] net/sched: Avoid quadratic handle scan in qdisc_alloc_handle 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 Co-developed-by: Jamal Hadi Salim Signed-off-by: Jamal Hadi Salim Signed-off-by: Victor Nogueira Reviewed-by: Simon Horman Link: https://patch.msgid.link/20260918135157.163015-1-victor@mojatatu.com Signed-off-by: Jakub Kicinski --- net/sched/sch_api.c | 92 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 73 insertions(+), 19 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 #include #include +#include #include #include #include @@ -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) - */ -static u32 qdisc_alloc_handle(struct net_device *dev) -{ - int i = 0x8000; - static u32 autohandle = TC_H_MAKE(0x80000000U, 0); +#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) - 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); +/* 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 int qdisc_alloc_handle(struct net_device *dev, u32 *handlep) +{ + 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; + } + + 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; } }