Why this path is awkward on a stock desktop
The vulnerable path lives in net/sched, where action lookup reads shared state under rcu_read_lock() and deletion can free objects without waiting for a grace period. That leaves a short window where a freed tc_action can be reclaimed before refcount_inc_not_zero() checks tcfa_refcnt. If the reclaim lands badly, the lookup falls over; if it lands cleanly, the object survives long enough to be used again.
A stock desktop gets in the way in boring but effective ways. Direct action creation and deletion go through netlink paths that expect CAP_NET_ADMIN in the init namespace, so the obvious RTM_NEWACTION and RTM_DELACTION route is not the route you get. The practical path uses filter management instead, which is why the bug matters to local privilege escalation rather than to casual testing.
The race only works if you can make net/sched behave
The exploit path relies on RTM_NEWTFILTER and RTM_DELTFILTER, not the direct action ioctls. That means the setup has to go through a clsact qdisc and a flower filter, using unlocked paths where possible so the lock holding does not kill the race before it starts. If the filter install path serialises too hard, the window closes and the bug stays a bug.
Move through clsact and flower, not the direct action ioctls
clsact and flower matter because they expose paths that avoid some of the heavier locking around action management. The direct action operations are the obvious choice only until CAP_NET_ADMIN blocks them. After that, the only workable angle is the filter side, where the action index can still be looked up while another thread tears it down.
Separate chains help because each chain carries its own mutex during tcf_chain_tp_find() in tc_new_tfilter(). That reduces shared lock contention and makes the race less noisy. It does not remove the race, but it stops the kernel from serialising the whole thing into something harmless.
Keep the reclaim window open long enough for tcfa_refcnt to matter
The vulnerable lookup only becomes useful if the reclaimed object is in place before refcount_inc_not_zero() runs. That means reclaim timing is the whole game. A freed action that comes back with a non-zero reference count gives the lookup a believable object instead of a dead one, and that is what turns a lookup bug into kernel memory corruption.
Timer pressure helps widen the window. timerfd paired with epoll can keep a core busy and stretch the race enough for a controlled reclaim to land. The reported setup uses a very short timer interval, struct itimerspec its = { .it_value = { .tv_nsec = 30000 } };, which is the sort of detail that looks silly until it stops the race from missing by a few microseconds.
What CentOS 9 changes about reliability and recovery
CentOS 9 Desktop is not hostile in a dramatic way. It is just less forgiving. Kernel build choices, desktop load, and user namespace settings all affect whether the race becomes repeatable or just becomes a crash report.
Separate chains, timerfd pressure, and fewer shared locks
The chain split matters more on a desktop than in a quiet lab VM because the machine already has enough background activity to blur timing. Using separate chains reduces shared mutex pressure, which leaves less interference from unrelated filter work. The race still needs a precise overlap between free and reclaim, but it stops competing with itself quite as much.
timerfd and epoll are useful here because they keep the process active without needing busy loops everywhere. That leaves room for the reclaim primitive while still giving enough scheduling churn to keep the window open. On a loaded desktop, this can be the difference between a repeatable crash and a usable local privilege escalation path.
Check the kernel build before you trust the result
The exploit path depends on CONFIG_NET_ACT_GACT and CONFIG_NET_CLS_FLOWER being set, either built in or as modules. It also relies on unprivileged user namespaces being available, which changes the attack surface sharply. If those pieces are missing, the path collapses before the race even starts.
Kernel build details matter because the behaviour is not identical across distro kernels. A desktop kernel with extra hardening or different netlink handling can move the timing enough to break the exploit even when the vulnerable code exists. That is why a working proof on one CentOS 9 system tells you very little about the next one.
Practical limits for defenders on a user session
A user session is enough to matter if the preconditions line up. That is the part defenders need to treat seriously. A local user who can reach the unlocked filter path, trigger the race, and reclaim the freed object gets a path from a scheduling bug to full kernel memory corruption.
Spot the preconditions that turn a bug into kernel memory corruption
Look for three things at once: user namespace support, the relevant qdisc and classifier modules, and netlink access that reaches the filter path. If all three are present, the bug is not just a crash candidate. It becomes a plausible local privilege escalation route, with the race itself hiding behind otherwise normal traffic-control operations.
clsact and flower should also be treated as real attack surface, not as harmless desktop plumbing. They are common enough to be ignored and flexible enough to be abused. If a user session can create and delete filters without needing init-namespace admin rights, the preconditions are already close enough for comfort.
Block the local privilege escalation path with the right desktop hardening
Disable unprivileged user namespaces where that is acceptable for the workstation fleet. That removes one of the easier routes into the filter setup. Keep the relevant traffic-control modules out of general desktop builds unless they are actually needed, because optional kernel features have a habit of becoming attack surface long after they stop being noticed.
Restrict network administration capabilities in the user session, and do not assume a desktop user cannot reach kernel-adjacent netlink paths through helper processes or misconfigured policy. If traffic control is required, keep the allowed surface narrow and predictable. A desktop that can create and tear down clsact and flower state from an untrusted session is already carrying more kernel risk than it looks.

