RCU and refcount faults in Linux kernel net/sched

RCU and refcount faults in Linux kernel net/sched

The bug starts in action lookup. A filter path can fetch a shared tc_action under RCU, then later try to bump its reference count with refcount_inc_not_zero(). If deletion has already dropped the object and slab reuse has moved fast enough, the lookup is using stale memory before the refcount check ever helps.

That is the awkward part of this class of bug: the code does check a refcount, but it checks too late. Once the object has been freed, the old contents can be overwritten by whatever the allocator hands out next. At that point, the refcount check is not protecting anything real.

RCU lookup can hand back a dying tc_action

idr_find() under RCU is fast, which is the point. It also means the lookup can return an object that is no longer stable if deletion has already progressed far enough. The action exists long enough to be found, then disappears before the caller finishes with it.

Deletion is also not waiting for an RCU grace period before the raw free. That leaves a narrow overlap where readers still see the pointer, while writers have already made the object eligible for reuse. If the slab is recycled quickly, the lookup path reads the wrong memory with kernel privileges.

refcountincnot_zero arrives after the object can already be gone

refcount_inc_not_zero() helps only when the object is still the same object. If the freed chunk has already been reclaimed, the refcount field is just one more byte pattern in reused memory. A harmless zero refcount drops the object, but a reclaimed object can make the window look valid long enough for the race to matter.

That is why exploit reliability is poor by default. The attacker is racing three things at once: deletion, free, and reclaim. Miss any one of them and the bug collapses back into a failed lookup or a plain crash.

Turning a kernel memory corruption bug into a local privilege escalation

The escalation path is local, not remote. It needs a user account, a live kernel race, and a configuration that allows the relevant netlink operations without init-namespace admin rights. On CentOS 9 desktop systems with unprivileged user namespaces enabled, that is the sort of setup that can be awkward rather than impossible.

The practical value of the bug sits in the filter path, not in the action management ioctls. The latter are tied up with capability checks that block the easy route.

Why RTMNEWTFILTER and RTMDELTFILTER matter more than the action ioctls

RTM_NEWACTION and RTM_DELACTION require CAP_NET_ADMIN in the init namespace, which makes them a poor choice for an unprivileged attacker. The filter path is more interesting. RTM_NEWTFILTER and RTM_DELTFILTER can be used in a setup that stays inside user namespaces and still reaches the vulnerable action lookup.

That detail matters because it changes the attack from impossible to messy. The kernel still has to accept the netlink messages, create and delete the filter state, and walk the shared action pointers. Once that is in play, the race is in reach.

clsact, flower, and separate chains make the window less miserable to hit

Most of the kernel path wants rtnl_lock(), which is bad news for a race. clsact and flower help because they use the unlocked DOIT_UNLOCKED path, which avoids taking the big lock in the hot path. That does not make the race easy. It just removes one of the larger bricks in the way.

Separate chains also help by reducing mutex contention in tcf_chain_tp_find(). If different threads are creating filters on separate chains, they spend less time blocking each other. That widens the time gap around lookup and delete, which is exactly where the bug lives.

Widening the race and checking the damage on CentOS 9

A working exploit needs the free to happen inside the lookup window, then needs slab reclaim to land on the same memory before refcount_inc_not_zero() runs. The timing is brutal. The more the system stays busy in the wrong places, the less repeatable the hit becomes.

One common trick is to stall the CPU with timer and wait states while the race is open. A timerfd configured with a very short expiry, paired with epoll, can create enough churn to stretch the vulnerable interval. In practice, that means the thread doing the racing is not the only thing running, which is the point.

CentOS 9 desktop is a useful target because it is a common modern Linux build with the right user namespace behaviour exposed in many default setups. The exploit still depends on kernel configuration, and it needs CONFIG_NET_ACT_GACT and CONFIG_NET_CLS_FLOWER enabled as module or built-in support. Without those, the path closes before the interesting part starts.

The real check is not whether the bug exists in theory. It is whether the allocator, the scheduler, and the filter path line up often enough for a local user to reclaim freed action memory before the refcount check fires. Most of the time, they do not. When they do, the result is kernel memory corruption with a privilege boundary attached to it.

Related posts

Caddy | v2.11.8

Caddy v2 11 8: security hardening, path traversal fixes, proxy and timeout reliability, Caddyfile and file server fixes, TLS,ECH and automation updates

n8n | n8n@2.46.6

n8n v2 46 6: fixes npm install cross platform failures, restores npm installation, upgrade with npm install n8n 2 46 6

Security Advisory – 05 Nov 2026

Daily security advisory summary for 05 Nov 2026