From: Victor Nogueira <victor@mojatatu.com>
To: davem@davemloft.net, edumazet@google.com, kuba@kernel.org,
pabeni@redhat.com, jhs@mojatatu.com, jiri@resnulli.us
Cc: horms@kernel.org, vega@nebusec.ai, netdev@vger.kernel.org
Subject: [PATCH net] net/sched: Avoid quadratic handle scan in qdisc_alloc_handle
Date: Fri, 11 Sep 2026 10:31:46 -0300 [thread overview]
Message-ID: <20260911133146.3440982-1-victor@mojatatu.com> (raw)
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. Each failing add holds RTNL for 0.6-0.9 s on a
production kernel and about 1.7 s with KASAN and lock debugging, and it
can be repeated back to back, stalling network administration in every
namespace.
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".
This is a less intrusive fix meant for backporting; a cleaner approach that
uses a per-device IDA over all qdisc handles is planned for net-next.
Conditions to recreate the bug:
- unshare -Urn (Level 2, namespace-local CAP_NET_ADMIN)
- classful qdisc (e.g. HTB) with many classes
- attach ~32767 auto-handle child qdiscs to fill the handle space
- the final tc qdisc add with no explicit handle scans the full space
Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
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>
---
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
next reply other threads:[~2026-09-11 13:31 UTC|newest]
Thread overview: 6+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-11 13:31 Victor Nogueira [this message]
2026-09-14 8:22 ` [PATCH net] net/sched: Avoid quadratic handle scan in qdisc_alloc_handle Simon Horman
2026-09-15 2:11 ` Jakub Kicinski
2026-09-15 11:50 ` Jamal Hadi Salim
2026-09-15 12:28 ` Eric Dumazet
2026-09-15 14:01 ` Jamal Hadi Salim
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260911133146.3440982-1-victor@mojatatu.com \
--to=victor@mojatatu.com \
--cc=davem@davemloft.net \
--cc=edumazet@google.com \
--cc=horms@kernel.org \
--cc=jhs@mojatatu.com \
--cc=jiri@resnulli.us \
--cc=kuba@kernel.org \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
--cc=vega@nebusec.ai \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).