When AI Makes 0-Days Feel Like N-Days | STAR Labs
MENU
Table of Contents
Introduction
After my n-day analysis on net/tls bugs and exploit writing for a patched net/rxrpc bug, I moved on to 0-day bug hunting. With the help of AI, I found a UAF bug in net/sched, and went about creating an LPE exploit based on it. This blog goes into the technical details of that exploit, and how I optimized it to target CentOS 9 desktop in TyphoonPwn 2026.
Additionally, I will show a glimpse of two other exploitable bugs I found in kernel/events/core.c (with an LPE exploit for one).
AI usage
Compared to my previous exploit for an n-day in net/rxrpc, I had a greater focus on completing and improving this exploit quickly rather than fully understanding every aspect from the ground up. As such, I used AI to speed up various aspects of the process - discovery of the bug, KASAN poc, and improving the race condition. It was certainly helpful for iterating quickly, but still lacking in reasoning ability and having clear blind spots. It was still crucial to exercise my own judgement especially when fine-tuning.
Brief conceptual overview of net/sched
net/sched is the packet scheduling subsystem in linux. It sits in a layer above the device drivers, and decides when, in what order, and whether packets are transmitted. It also provides an API through netlink to configure packet handling rules. Each network device is attached to a Qdisc (queueing discipline) that holds all this configuration data.
To decide what to do with a given packet, net/sched introduces chains, filters and actions. Chains are an ordered list of filters, filters check for certain attributes in the packet, and based on that, decide what action to perform on it.
net/sched is designed in a way to maximally allow reuse of components - multiple network devices can share the same Qdisc. Importantly for this bug, actions can be shared within the same net namespace, and are uniquely identified by an “index”. A per-net radix tree action_idr records all action objects and enables lookup by their indexes. This is done by the tcf_idr_check_alloc function:
int tcf_idr_check_alloc(struct tc_action_net *tn, u32 *index,<br>struct tc_action **a, int bind)<br>struct tcf_idrinfo *idrinfo = tn->idrinfo;<br>struct tc_action *p;<br>int ret;<br>u32 max;
if (*index) {<br>rcu_read_lock();<br>p = idr_find(&idrinfo->action_idr, *index); // [0]: ACTION LOOKUP
// [1]: WINDOW OPENS
if (IS_ERR(p)) {<br>/* This means that another process allocated<br>* index but did not assign the pointer yet.<br>*/<br>rcu_read_unlock();<br>return -EAGAIN;
if (!p) {<br>/* Empty slot, try to allocate it */<br>max = *index;<br>rcu_read_unlock();<br>goto new;
// [2]: WINDOW CLOSES
if (!refcount_inc_not_zero(&p->tcfa_refcnt)) {<br>/* Action was deleted in parallel */<br>rcu_read_unlock();<br>return -EAGAIN;
if (bind)<br>atomic_inc(&p->tcfa_bindcnt);<br>*a = p;
rcu_read_unlock();
return 1;<br>} else {<br>/* Find a slot */<br>*index = 1;<br>max = UINT_MAX;
new:<br>*a = NULL;
mutex_lock(&idrinfo->lock);<br>ret = idr_alloc_u32(&idrinfo->action_idr, ERR_PTR(-EBUSY), index, max,<br>GFP_KERNEL);<br>mutex_unlock(&idrinfo->lock);
/* N binds raced for action allocation,<br>* retry for all the ones that failed.<br>*/<br>if (ret == -ENOSPC && *index == max)<br>ret = -EAGAIN;
return ret;
When creating a filter with a list of actions, we can simply specify the action index and it will be fetched from the idr without having to create a new object.
For this exploit, we need only focus on the netlink operations used to configure packet-handling rules, specifically those for creating and deleting filters.
The bug
The vulnerability is a lock-mismatch: tcf_idr_check_alloc() accesses the action idr with only rcu_read_lock(), while actions are freed with idrinfo->lock and rtnl_lock() held, but without waiting for the RCU grace period (i.e. raw kfree).
Thus, this leads to a race condition where the action can be freed during lookup leading to a UAF scenario.
Take another look at the tcf_idr_check_alloc function above. If the retrieved tc_action has a tcfa_refcnt of 0, it gets dropped harmlessly. Thus, for a successful UAF we have to both free and reclaim the action (to overwrite tcfa_refcnt) within the same window [1] to [2].
The basic structure of the race looks like this:
CPU 0: lookup action CPU 1: delete action CPU 2: reclaim<br>p = idr_find(<br>&idrinfo->action_idr,<br>*index<br>);<br>kfree(p);<br>// reclaim<br>// set p->tcfa_refcnt != 0<br>refcount_inc_not_zero(<br>&p->tcfa_refcnt
How are these functions reached?
I initially proved the bug using RTM_NEWACTION and RTM_DELACTION. However, these operations require CAP_NET_ADMIN in the init namespace, making this path infeasible:
static int tc_ctl_action(struct sk_buff *skb, struct nlmsghdr *n,<br>struct netlink_ext_ack *extack)<br>struct net *net = sock_net(skb->sk);<br>struct nlattr *tca[TCA_ROOT_MAX + 1];<br>u32 portid = NETLINK_CB(skb).portid;<br>u32 flags = 0;<br>int ret = 0;
if ((n->nlmsg_type != RTM_GETACTION) &&<br>!netlink_capable(skb, CAP_NET_ADMIN))<br>return...