qfq_change_class() snapshots the current aggregate settings while holding the qdisc tree lock, but drops the lock before selecting the destination aggregate. During that gap, qfq_enqueue() can move the class to an aggregate matching the requested settings. When qfq_change_class() resumes, qfq_find_agg() then returns cl->agg. If it is a singleton, qfq_deact_rm_from_agg() frees the aggregate before qfq_add_to_agg() immediately accesses the same pointer, causing a use-after-free. While holding the tree lock, skip the replacement when the destination is already the current aggregate. Any estimator replacement has already completed, so the class change can finish normally. Fixes: 462dbc9101ac ("pkt_sched: QFQ Plus: fair-queueing service at DRR cost") Bug found and triaged by OpenAI Security Research and validated by Trail of Bits. Assisted-by: Codex:gpt-5.6-sol gpt-5.5-cyber Signed-off-by: Kyle Zeng --- Trail of Bits has a reproducer for this bug that triggers a KASAN use-after-free and can share if needed. net/sched/sch_qfq.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/net/sched/sch_qfq.c b/net/sched/sch_qfq.c index 6f3b7273c..e900890e9 100644 --- a/net/sched/sch_qfq.c +++ b/net/sched/sch_qfq.c @@ -517,11 +517,14 @@ static int qfq_change_class(struct Qdisc *sch, u32 classid, u32 parentid, sch_tree_lock(sch); qfq_init_agg(q, new_agg, lmax, weight); } + if (existing && new_agg == cl->agg) + goto unlock; if (existing) qfq_deact_rm_from_agg(q, cl); else qdisc_class_hash_insert(&q->clhash, &cl->common); qfq_add_to_agg(q, new_agg, cl); +unlock: sch_tree_unlock(sch); qdisc_class_hash_grow(sch, &q->clhash); -- 2.53.0