* [PATCH net] net/sched: Avoid quadratic handle scan in qdisc_alloc_handle
@ 2026-09-11 13:31 Victor Nogueira
0 siblings, 0 replies; only message in thread
From: Victor Nogueira @ 2026-09-11 13:31 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. 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
^ permalink raw reply related [flat|nested] only message in thread
only message in thread, other threads:[~2026-09-11 13:31 UTC | newest]
Thread overview: (only message) (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-11 13:31 [PATCH net] net/sched: Avoid quadratic handle scan in qdisc_alloc_handle Victor Nogueira
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox