- What: A use-after-free vulnerability exists in the Linux Kernel's CAKE Qdisc implementation on CentOS 9.
- Impact: A local user can exploit this flaw to escalate privileges to root.
Linux Kernel net/sched CAKE Qdisc Use-After-Free LPE February 5, 2026 Noamr Vulnerability publication Summary A local user under the CentOS 9 operating system can trigger an use-after-free, which in turn can be used to elevate to root privileges. Vendor Response The vendor has been notified more than 90 days ago and has offered the only feedback that: The work is in progress. No release yet. Credit The vulnerability won first place in the Linux category during TyphoonPWN 2025 event. Vulnerability Details The specific flaw exists within the handling of sch_cake . The vulnerability occurs because the enqueue function in CAKE Qdisc returns success even though it drops the packet. An attacker can leverage this vulnerability to escalate privileges and execute arbitrary code in the context of root. Root Cause Analysis The vulnerability occurs because the enqueue function in CAKE Qdisc returns success even though it drops the packet. static s32 cake_enqueue(struct sk_buff *skb, struct Qdisc *sch, struct sk_buff **to_free) { ... if (q->buffer_used > q->buffer_max_used) q->buffer_max_used = q->buffer_used; if (q->buffer_used > q->buffer_limit) { // [1] u32 dropped = 0; while (q->buffer_used > q->buffer_limit) { dropped++; cake_drop(sch, to_free); // [2] } b->drop_overlimit += dropped; } return NET_XMIT_SUCCESS; } If buffer_used is larger than the buffer_limit value set in Qdisc [1], the cake_enqueue function calls cake_drop [2]. In cake_drop , one packet is selected and dropped from Qdisc. If buffer_limit is given a small value, such as 1 , it will drop even if one packet is sent. static int hfsc_enqueue(struct sk_buff *skb, struct Qdisc *sch, struct sk_buff **to_free) { unsigned int len = qdisc_pkt_len(skb); struct hfsc_class *cl; int err; bool first; cl = hfsc_classify(skb, sch, &err); if (cl == NULL) { if (err & __NET_XMIT_BYPASS) qdisc_qstats_drop(sch); __qdisc_drop(skb, to_free); return err; } first = !cl->qdisc->q.qlen; err = qdisc_enqueue(skb, cl->qdisc, to_free); if (unlikely(err != NET_XMIT_SUCCESS)) { // [3] if (net_xmit_drop_count(err)) { cl->qstats.drops++; qdisc_qstats_drop(sch); } return err; } if (first) { if (cl->cl_flags & HFSC_RSC) init_ed(cl, len); // [4] if (cl->cl_flags & HFSC_FSC) init_vf(cl, len); /* * If this is the first packet, isolate the head so an eventual * head drop before the first dequeue operation has no chance * to invalidate the deadline. */ if (cl->cl_flags & HFSC_RSC) cl->qdisc->ops->peek(cl->qdisc); } sch->qstats.backlog += len; sch->q.qlen++; return NET_XMIT_SUCCESS; } A UAF can occur when a classful Qdisc, such as an HFSC Qdisc, exists on top of a CAKE Qdisc. The HFSC Qdisc performs the enqueue process normally without exception handling because the CAKE Qdisc’s enqueue() returned NET_XMIT_SUCCESS [3]. If this is the first time a packet has been enqueued to an HFSC Qdisc class, the init_ed function is called to add the class to the HFSC Qdisc’s active list [4]. static int hfsc_delete_class(struct Qdisc *sch, unsigned long arg, struct netlink_ext_ack *extack) { struct hfsc_sched *q = qdisc_priv(sch); struct hfsc_class *cl = (struct hfsc_class *)arg; if (cl->level > 0 || qdisc_class_in_use(&cl->cl_common) || cl == &q->root) { NL_SET_ERR_MSG(extack, "HFSC class in use"); return -EBUSY; } sch_tree_lock(sch); list_del(&cl->siblings); hfsc_adjust_levels(cl->cl_parent); qdisc_purge_queue(cl->qdisc); // [5] qdisc_class_hash_remove(&q->clhash, &cl->cl_common); sch_tree_unlock(sch); hfsc_destroy_class(sch, cl); return 0; } Next, removing this HFSC Qdisc class frees the class but does not remove it from the HFSC’s active list, leaving it as a dangling pointer. The hfsc_delete_class calls qdisc_purge_queue to purge the child Qdisc, the CAKE Qdisc [5]. static inline void qdisc_purge_queue(struct Qdisc *sch) { __u32 qlen, backlog; qdisc_qstats_qlen_backlog(sch, &qlen, &backlog); qdisc_reset(sch); qdisc_tree_reduce_backlog(sch, qlen, backlog); // [6] } qdisc_purge_queue calls qdisc_tree_reduce_backlog [6]. Since no packets exist in CAKE Qdisc at this time, the arguments of qdisc_tree_reduce_backlog , qlen and backlog , are called with 0 . void qdisc_tree_reduce_backlog(struct Qdisc *sch, int n, int len) { bool qdisc_is_offloaded = sch->flags & TCQ_F_OFFLOADED; const struct Qdisc_class_ops *cops; unsigned long cl; u32 parentid; bool notify; int drops; if (n == 0 && len == 0) // [7] return; drops = max_t(int, n, 0); rcu_read_lock(); while ((parentid = sch->parent)) { if (parentid == TC_H_ROOT) break; if (sch->flags & TCQ_F_NOPARENT) break; /* Notify parent qdisc only if child qdisc becomes empty. * * If child was empty even before update then backlog * counter is screwed and we skip notification because * parent class is already passive. * * If the original child was offloaded then it is allowed * to be seem as empty, so the parent is notified anyway. */ notify = !sch->q.qlen && !WARN_ON_ONCE(!n && !qdisc_is_offloaded); /* TODO: perform the search on a per txq basis */ sch = qdisc_lookup_rcu(...