Labs

CVE-2026-53264: Linux Kernel net/sched Use-After-Free

VM lab Privilege Escalation

Updated July 28, 2026 · Written by PWNMI — see About.

Most of the labs on this site involve an application. This one involves the kernel underneath it. CVE-2026-53264 is a use-after-free in Linux's net/sched traffic-control subsystem, discovered by Lee Jia Jie at STAR Labs with AI-assisted tooling, and prepared for TyphoonPwn 2026's Linux local-privilege-escalation category. It's CVSS 7.8, CWE-416, and it's a local bug — it needs an existing unprivileged shell to start from — but that's exactly what makes it worth practicing. Almost every real engagement's initial-access foothold lands you as some low-privilege user first; this is the step that turns "some access" into root.

Affects kernels 4.14 through 7.1-rc6. Fixed upstream (commit 5057e1a, landing in 7.1-rc7 and backported to 5.10.259, 5.15.210, 6.1.176, 6.6.143, 6.12.94, and 6.18.36). Original research and public PoC: star-sg/CVE.

What you'll practice: reading an RCU-read-lock-vs-explicit-lock mismatch as a real, timeable race window instead of an abstract code smell, why this specific lab needs a full VM instead of a container (a kernel exploit roots the actual kernel underneath — Docker's namespace isolation doesn't sit between you and that), and — the same discipline every lab on this site pushes — treating "did I actually get a root shell" as the only proof that matters, not a clean exit code from a PoC binary.

Before you start: this one is different

Every other lab here runs in Docker, fully isolated, torn down with docker compose down -v. This one can't — containers share the host's kernel, so a working exploit doesn't compromise a sandboxed process, it compromises the real kernel your hypervisor (or your host, if you're reckless enough to skip a VM) is running. Treat this lab with different rules:

  • Run it only inside a disposable VM, on a hypervisor that supports snapshots (libvirt/KVM via virt-manager, VirtualBox, or a cloud VM you can destroy afterward).
  • A cloud VM has a real advantage here over a local hypervisor: destroying it when you're done is a button, not a "did I actually revert the snapshot" question you have to trust yourself on later.
  • Take a snapshot immediately after the OS install, before you do anything else. A failed race can still crash the box; a successful one leaves kernel state (a hijacked core_pattern, manipulated heap objects) you don't want to keep using for anything else.
  • Never run this on a host you don't fully control, and never on anything shared or persistent. When you're done, revert to the snapshot or destroy the VM outright — don't keep using this instance afterward.

Set up the lab

New to git or unsure what a snapshot-first workflow looks like in practice? See Git for Security Work — the Docker guide doesn't apply to this one.

The fastest path to a clean CentOS Stream 9 box is a disposable cloud VM rather than installing from an ISO yourself: DigitalOcean offers CentOS Stream 9 as a standard Droplet image, up in a couple minutes, billed hourly, gone the moment you destroy it. Disclosure: that's an affiliate link. A local hypervisor (KVM/virt-manager, VirtualBox) with the official ISO works the same way if you'd rather keep this entirely offline — the verification steps below don't care which one you used.

Either way, snapshot right after the box is up, before you touch anything else. The original research targeted a Stream 9 desktop build; Stream is a rolling release, so confirm your own install is actually vulnerable before going further — don't assume the image you get today matches the one the original research used:

uname -r
grep -E 'CONFIG_NET_ACT_GACT|CONFIG_NET_CLS_FLOWER' /boot/config-$(uname -r)
sysctl user.max_user_namespaces

You need the kernel version to predate the fix commit above, both configs set to y or m, and user.max_user_namespaces greater than 0 (unprivileged user namespaces enabled — this is CentOS Stream's default, but confirm it). If your installed kernel already carries the fix, install and boot an earlier kernel package explicitly via dnf and grub2-set-default rather than assuming the current one works — this is the one step in this lab you have to verify against your own build, not take from a walkthrough.

Get the public PoC and build it as your normal unprivileged user — not root:

git clone https://github.com/star-sg/CVE
cd CVE/CVE-2026-53264
make
whoami && id

That id is worth remembering — it's what you're about to escalate from.

Understanding the vulnerability

The bug lives in tcf_idr_check_alloc() in net/sched/act_api.c, and it's a mismatch, not a missing check. When the kernel looks up a traffic-control action object by ID, it does so under rcu_read_lock() alone. But when an action object is freed, that happens under idrinfo->lock and rtnl_lock() — without waiting for the RCU grace period to elapse first. Those are two different synchronization disciplines that were never meant to coexist safely: RCU readers are supposed to be able to trust that an object stays valid until the grace period ends, and the free path here doesn't honor that guarantee.

That opens a real, timeable window between idr_find() locating the object and the subsequent refcount_inc_not_zero() confirming it's still alive. If another thread frees the object and something else reclaims that memory inside that window, the refcount check operates on memory that's no longer what it thinks it is.

Reaching that path from an unprivileged shell takes two more pieces. First, a user namespace gets you CAP_NET_ADMIN scoped to that namespace — no real root required, just the netlink capability the traffic-control API checks for. Second, a clsact qdisc paired with a flower filter routes through handlers flagged DOIT_UNLOCKED, meaning they skip the normal rtnl_lock() serialization that would otherwise prevent concurrent RTM_NEWTFILTER and RTM_DELTFILTER requests from racing each other at all.

The actual fix is exactly what the mismatch implies it should be: defer the free until the RCU grace period has genuinely elapsed, so a concurrent reader can never observe memory that's already been handed back.

Exploitation

The PoC binary runs the full chain end to end — reading through it (poc.c, pwn_utils.cpp) alongside make is worth doing before you run it, since every stage maps back to a real, separately-interesting technique:

  1. KASLR bypass. An EntryBleed-style side channel leaks the kernel's base address before anything else happens — none of the later offsets mean anything without it.
  2. Widening the race. Rather than hoping to win the idr_find()-to-refcount_inc_not_zero() window on luck, the PoC spawns filter-creation ("binder") threads and a deleter thread across separate CPUs and separate filter chains — separate chains avoid mutex contention that would otherwise serialize the threads back into safety — and uses timerfd plus a long epoll waiter list to stall execution right inside the critical section. That combination is what took the original research from a sub-1%, 15-minute-plus race to a consistent few-second win.
  3. Heap reclamation. The freed object is a tc_action, allocated from the kmalloc-256 cache. The PoC reclaims that exact slab slot with a user_key_payload object via KEYCTL_UPDATE — a completely unrelated kernel subsystem, chosen because it lets an unprivileged process control the reclaimed memory's contents directly, landing a fake vtable pointer at the offset the freed object's own vtable pointer used to occupy.
  4. The ROP chain. With a hijacked function pointer, execution redirects into a short chain built from gadgets in the leaked kernel image, pivoting the stack twice (once to gain a usable stack, once more into an unused .ktext region so the chain survives a msleep() call without corrupting itself) and ending by overwriting /proc/sys/kernel/core_pattern with |/proc/%P/fd/666 %P.
  5. Cashing it in. core_pattern with a leading | tells the kernel to pipe any crashing process's core dump into the named program instead of writing a file — run as root, regardless of the crashing process's own privilege level. The PoC deliberately crashes a process it controls, the kernel's coredump handler fires, and the attacker-supplied program runs with root's own credentials.

Run it:

./poc

The original research logged run times ranging from roughly 5 seconds to just under two minutes across ten consecutive runs on similar hardware — thermal throttling on a laptop CPU was enough to move that number around, so don't read a slow run as a failure. When it lands, you'll be dropped into a shell. Confirm it properly, the same way every lab on this site insists on:

id
hostname
cat /etc/shadow | head -1

id showing uid=0(root) is the only thing that actually proves this worked — not the PoC exiting cleanly, not any status message it prints along the way. Being able to read /etc/shadow, which your original unprivileged user couldn't, is the independent confirmation.

Cleanup

There isn't one, and that's the point of the VM-first setup above. This exploit doesn't just create an artifact you can delete — it executes a hand-built ROP chain inside kernel context and overwrites live kernel state (core_pattern, the reclaimed heap object) on the way to root. Nothing about that is something you can cleanly reverse from inside the machine. Revert to the snapshot you took before you started, or destroy the VM outright. Don't keep using this instance for anything else, and don't skip the snapshot step next time thinking you'll clean up manually after — you won't be able to.

Lessons worth keeping

  • A locking scheme has to agree with itself everywhere, not just look correct at each call site. tcf_idr_check_alloc()'s RCU read wasn't wrong in isolation, and the free path's rtnl_lock() + idrinfo->lock wasn't wrong in isolation either — the bug only exists in the gap between two locally-reasonable choices that don't compose into one consistent contract.
  • Kernel LPE chains converge on a small set of well-worn primitives regardless of how novel the initial bug is. The UAF here is genuinely new research; the endgame — hijack a function pointer, build a ROP chain, overwrite core_pattern, trigger a coredump as root — is a pattern that shows up across a lot of unrelated kernel LPEs. Recognizing the endgame primitive is often more transferable than any single bug's root cause.
  • AI-assisted tooling changed the economics here, not just the outcome. The optimization work in the original research took the reliable exploitation window from over 15 minutes down to about 5 seconds — that's not a difference in whether the bug is exploitable, it's a difference in how fast a working exploit gets built once the bug is found, which is exactly the kind of timeline compression defenders now have to plan around.

Next step

For the broader mental model this lab is one instance of, see Privilege Escalation Fundamentals. For the environment-setup habits (snapshotting, disposable infrastructure) this lab leans on harder than most, see Linux Fundamentals Every Beginner Hacker Needs. For another real CVE reproduced with the same "verify the actual effect, not the exit code" discipline, see CVE-2026-34197 (Apache ActiveMQ).