
Proof-of-concept local privilege escalation exploit for a Linux qdisc rate-table race condition, using BPF heap grooming and a pipe leak to gain root.

This repository contains a proof-of-concept local privilege-escalation exploit for CVE-2026-68138, a race in the Linux traffic-control rate-table code. In the tested QEMU environment, the PoC escalates from an ordinary process with outer UID 1000 to a shell with UID 0 in the initial user namespace.
Warning
This code intentionally corrupts kernel heap state. Use it only in an isolated, disposable VM that you own. A missed race or premature cleanup can panic the guest. Do not run it on a workstation, server, or third-party system.
qdisc_get_rtab() and qdisc_put_rtab() manage a process-global singly linked
list, qdisc_rtab_list, and a plain non-atomic int refcnt. Historically,
callers held the RTNL mutex, which serialized access to the list and reference
count.
The flower classifier sets TCF_PROTO_OPS_DOIT_UNLOCKED. An
RTM_NEWTFILTER request for a flower rule can consequently reach a police
action and call the qdisc rate-table helpers without RTNL:
tc_new_tfilter()
-> fl_change()
-> tcf_exts_validate_ex()
-> tcf_action_init()
-> tcf_police_init()
-> qdisc_get_rtab()/qdisc_put_rtab()
Concurrent requests using the same rate table can race the global list and its
reference count. The result is a use-after-free or double-free of
struct qdisc_rate_table, a 1056-byte object allocated from kmalloc-2k on
the tested x86-64 kernel.
Because the list is global rather than per-network-namespace, requests from separate network namespaces still race the same object.
The Linux CNA record identifies introduction commit
470502de5bdb,
released in Linux 5.1.
Distribution kernels frequently backport fixes without changing to the upstream version shown above. Check whether either fixing commit—or the equivalent qdisc rate-table spinlock change—is present in the exact kernel source used by the system.
The exploit was developed and validated against vulnerable commit
92d3817649df2b0b6a008a686c8275c88d7ef594, the direct parent of the mainline
fix. The fixed-kernel control used
f43ee0c0730d6191629b5ee1ceae27b1ebfdc047.
As of 2026-08-12, the
Ubuntu CVE tracker search
did not return an entry for this CVE. The table below is therefore a direct
source inspection, not a Canonical security-status determination. Each linked
Ubuntu tag still has the unlocked qdisc_rtab_list and lacks the fixing
qdisc_rtab_lock.
No corrected Ubuntu package was identified at that inspection date. Future Ubuntu packages should be checked for a backport equivalent to the linked upstream fixes rather than judged only by their version number.
The build-specific Ubuntu 22.04 exploit, QEMU lab, exact image checksum, and
conditions are documented in ubuntu/README.md. It was
validated against the official 5.15.0-187-generic #197-Ubuntu kernel with
memory-cgroup accounting enabled.
The underlying bug and this particular exploit chain have different requirements. The PoC was validated with:
CONFIG_USER_NS=y and CONFIG_NET_NS=y;CONFIG_NET_CLS=y, CONFIG_NET_CLS_FLOWER=y,
CONFIG_NET_CLS_ACT=y, and CONFIG_NET_ACT_POLICE=y;CONFIG_TMPFS_XATTR=y for the simple_xattr heap spray;CONFIG_MODULES=y and a usable /sbin/modprobe for the final root helper;CONFIG_MEMCG=n, so the qdisc/BPF/pipe/xattr allocations used by this chain
share the expected kmalloc-2k cache;CONFIG_SLAB_BUCKETS=y, freelist randomization, and freelist hardening were
enabled in the successful test kernel. KASLR is not inherently bypassed with a
hard-coded address: the PoC obtains the required page and operations pointers
from the pipe leak. The supplied lab used nokaslr to simplify debugging.
Kernels with memory-cgroup accounting enabled or different allocator/cache layouts require a different reclaim strategy. The PoC deliberately refuses to claim portability across arbitrary distribution configurations.
The separate Ubuntu variant implements that different reclaim strategy; its
requirements are intentionally narrower and are listed in
ubuntu/README.md.
Four worker threads enter distinct network namespaces and create flower filters with police actions. Three workers take the successful action path; one supplies a deliberately invalid estimator after acquiring both rate-table references, forcing the cleanup path. This combination makes concurrent reference-count and list manipulation reproducible without sharing flower classifier state between workers.
qdisc_rate_table with classic BPFAfter every netlink request, the same CPU immediately attaches a 133-instruction
classic-BPF filter. Its 1064-byte instruction array is allocated from
kmalloc-2k and is shaped so the bytes overlapping qdisc_rate_table.next and
qdisc_rate_table.refcnt initially remain valid.
The final instruction contains a per-socket marker. SO_GET_FILTER lets the
PoC detect when one socket's orig_prog->filter pointer has been redirected to
another live BPF allocation. This yields two socket objects referring to the
same instruction buffer.
Closing one owner defers the actual free through sk_filter_release_rcu().
After the grace period, the PoC allocates 32-slot pipe rings:
32 * sizeof(struct pipe_buffer) = 32 * 40 = 1280 bytes -> kmalloc-2k
One ring reclaims the aliased BPF buffer. Reading the other socket with
SO_GET_FILTER discloses a live pipe_buffer, including its page and ops
pointers and the offset of the /sbin/modprobe page-cache entry.
Closing the second socket frees the live pipe ring after another RCU grace
period, while the corresponding pipe_inode_info still references it. A
simple_xattr spray reclaims the 1280-byte ring and places the leaked
pipe_buffer at the expected slot with PIPE_BUF_FLAG_CAN_MERGE set.
Writing to each candidate pipe eventually appends attacker-controlled bytes to
the page-cache page backing the ELF entry point of /sbin/modprobe. The PoC
reads the file back and proceeds only after confirming the exact overwrite.
Recent kernels no longer use the historical invalid-binfmt module-autoload
fallback in search_binary_handler(). This PoC instead creates an AF_INET
datagram socket with unsupported protocol 253. inet_create() requests the
missing net-pf-2-proto-253-type-2 module, causing the kernel to execute the
overwritten /sbin/modprobe as initial-namespace root.
The injected entry-point payload writes a proof file and opens a root shell on the QEMU serial console.
The guest binary is statically linked:
./build.sh
Equivalent command:
gcc -O2 -static -pthread -Wall -Wextra -Werror \
-o build/exploit exploit.c
The resulting binary must be exposed inside the guest as
/exploit/CVE-2026-68138. The tested lab used QEMU's read-write 9p share. The
root helper expects that share at /chroot/exploit from the initial mount
namespace; adjust helper_command in exploit.c if the lab uses another
layout.
The relevant test-kernel options are recorded in
config.fragment. They are a fragment, not a complete
production kernel configuration.
From the unprivileged guest shell:
id
/exploit/CVE-2026-68138
The race is probabilistic. On a miss the PoC prints:
[-] no BPF alias found in this attempt
Reboot the VM before retrying. Do not rerun it in the same guest: the global qdisc list may already have been corrupted.
A successful execution reaches:
[+] confirmed dangling orig_prog->filter pointer
[+] pipe leak: page=<kernel pointer> ops=<kernel pointer> off=0x60f len=1 flags=0
[+] page-cache overwrite via dangling pipe
[+] controlled page-cache write confirmed; triggering helper
[+] CVE-2026-68138 initial-namespace root shell
uid=0(root) gid=0(root) groups=0(root)
[+] ROOT PROOF: uid=0(root) gid=0(root) groups=0(root)
The complete sanitized transcript is in
docs/example-output.txt.
The full chain was completed three times from fresh vulnerable VM boots. The final binary produced:
outer identity: uid=1000(user)
alias: classic-BPF orig_prog->filter overlap
leak: live pipe_buffer page and ops pointers
write: verified /sbin/modprobe page-cache modification
final identity: uid=0(root) gid=0(root) groups=0(root)
The same binary was run against the fixed commit with the same four-vCPU topology. Approximately 126,000 race requests completed without a BPF alias, and no pipe or page-cache stage was reached.
The Ubuntu-specific variant was also run against the exact official
5.15.0-187.197 generic kernel. The guest booted with four vCPUs, normal
memory-cgroup accounting, and nokaslr as the only exploit-specific kernel
argument.
The Ubuntu kernel was not recompiled: the test used the distribution's
unchanged vmlinuz-5.15.0-187-generic and matching official modules. The
Ubuntu prerequisite table
separates stock kernel features from runtime and QEMU lab adjustments.
The starting account had no sudo access or supplementary groups:
uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu)
From a fresh snapshot, the exploit obtained the BPF alias, reclaimed it through
cgroup-v2 simple_xattr objects and blocking select() bitmaps, verified that
/proc/sys/kernel/modprobe contained /tmp/x, and reached:
[+] CVE-2026-68138 Ubuntu initial-namespace root shell
uid=0(root) gid=0(root) groups=0(root)
root@cve-2026-68138-jammy:/# id
uid=0(root) gid=0(root) groups=0(root)
See the real QEMU transcript and the
Ubuntu lab instructions. The variant hard-codes the
modprobe_path address for this package and therefore requires nokaslr; it
does not claim a KASLR bypass or compatibility with another Ubuntu build.
The patch is already part of the Linux kernel and is therefore not duplicated in this repository. The canonical stable and mainline fixing commits are linked in the references below.
Research and exploit implementation: A. Ramos <[email protected]>
(Twitter: @aramosf).
| Line | Status | Commit/version |
|---|
| Linux before 5.1 | Not affected | The introducing change is absent |
| Linux 5.1 through 7.1.5 | Affected unless a vendor backport is present | 470502de5bdb through the commit before the stable fix |
| Linux 7.1.y | Fixed | 7.1.6, fb29e1b41052 |
| Linux 7.2 development series | Affected before rc5 | rc1 through rc4 |
| Mainline | Fixed | 7.2-rc5, f43ee0c0730d |
| Ubuntu line | Inspected package/tag | Source result |
|---|
| Ubuntu 22.04 GA | 5.15.0-187.197 | Vulnerable code present; full exploit reproduced in QEMU |
| Ubuntu 22.04 HWE | 6.8.0-136.136~22.04.1 | Vulnerable code present; exploit chain not tested |
| Ubuntu 24.04 HWE | 7.0.0-28.28~24.04.1 | Vulnerable code present; this exploit chain is not compatible with its allocator hardening |
| Ubuntu 26.04 | 7.0.0-28.28 | Vulnerable code present; this exploit chain is not compatible with its allocator hardening |