Avoiding rtnl_lock() in clsact flower paths
The race opens where net/sched takes an unlocked path through clsact and flower. That path reaches action lookup without the usual netlink serialisation, which leaves a narrow window between lookup and deletion. On CentOS 9 Desktop, that window can be useful for local privilege escalation if unprivileged user namespaces are enabled.
The unlocked clsact path is where the race opens up
Filter install reaches action lookup without rtnl_lock() on the selected clsact flower path. That matters because the usual netlink serialisation is what blocks most of the useful timing. Once the code enters the unlocked path, action resolution can run while another thread is tearing the same action down.
Flower and clsact matter more than the action syscalls because the attack does not need to go through RTMNEWACTION or RTMDELACTION. Those paths are tied to CAPNETADMIN in the initial namespace, which closes off the obvious route. RTMNEWTFILTER and RTMDELTFILTER stay reachable in the weaker setup, so the race lives there instead.
The exploit surface also depends on the qdisc and classifier choice. clsact gets the code onto the unlocked path, and flower gives a filter type that reaches the action lookup logic without dragging the whole flow back under rtnl_lock(). In practice, that is the difference between a race worth testing and a race that disappears under normal locking.
The use-after-free sits in action IDR lookup and deletion
The bug lands in tcfidrcheckalloc(), where idrfind() hands back an action pointer under rcureadlock() alone. Deletion frees the action under idrinfo->lock and rtnl_lock(), but the free happens before any RCU grace period. That leaves a plain use-after-free if lookup and deletion cross at the wrong moment.
The weak point is tcfarefcnt. The pointer can already be gone by the time refcountincnotzero() gets to read it. If reclaim lands in that gap, the freed object can be reused before the refcount check finishes, which turns a lookup bug into a possible overwrite window rather than a simple crash.
That window is small and fussy. A parallel lookup can return -EAGAIN if the action vanished or if another process claimed the index first and the pointer was never assigned. The exploit only stays interesting when the stale pointer survives long enough for reclaim to put controlled data back in place.
Building a reliable local privilege escalation from the window
Reliability comes from making the race less exacting rather than trying to win it once. Separate chains reduce contention in tcfchaintp_find(), which keeps unrelated chain work from clogging the same path. timerfd wakeups and epoll stalls widen the timing window by holding one CPU in the wrong place long enough for the lookup and delete to overlap.
A small timer value was enough to help the window open. The setup used struct itimerspec its = { .it_value = { .tv_nsec = 30000 } }; and then swept timing before netlink calls with int spin = (i * 37) % 2000;. That sort of ugly timing code is exactly what exploit development tends to look like when the race is real but not friendly.
CentOS 9 Desktop adds its own boundary. The path depends on unprivileged user namespaces so a local user can obtain CAPNETADMIN in a separate user namespace, then reach the filter operations needed for the race. If user namespaces are disabled, the route closes off early. If the kernel has CONFIGNETACTGACT=y or m and CONFIGNETCLSFLOWER=y or m, the necessary pieces stay available and the path remains viable.
Tightening the boundary in net/sched without hand-waving
The fix belongs at the locking boundary, not in wishful thinking about race rarity. Action lookup and action deletion need a consistent lifetime model, with the free deferred until readers cannot still reach the pointer. A raw kfree in a path that readers touch under rcureadlock() alone leaves too much room for stale access.
The unlocked clsact flower path also deserves careful scrutiny. Any code that reaches action lookup without rtnl_lock() should be treated as a special case, not a normal fast path. If the lifetime rules differ between lookup and delete, the gap will be found again, because netlink code does not stay benign just because the locking looks tidy on paper.

