
Race reproducer and stress toolkit for CVE-2026-52910, a Linux kernel use-after-free in reuseport cBPF selector programs, with dmesg and leak checks.
A race reproducer and stress toolkit for CVE-2026-52910, a use-after-free (UAF) in the Linux kernel's handling of classic BPF (cBPF) reuseport selector programs, fixed upstream by commit "bpf: Free reuseport cBPF prog after RCU grace period".
| CVE | CVE-2026-52910 |
| Type | Use-after-free / out-of-bounds read (CWE-125), CVSS 3.1 7.8 HIGH AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H |
| Introduced | v4.5 (with reuseport cBPF support) |
| Fixed in | 5.10.259, 5.15.210, 6.1.176, 6.6.143, 6.12.94, 6.18.36, 7.0.13 (stable); mainline v7.1 |
| Upstream splat | BUG: KASAN: vmalloc-out-of-bounds in reuseport_select_sock (net/core/sock_reuseport.c:596) |
| Reported by | Eulgyu Kim |
[!WARNING] This is a kernel stress tool. On a vulnerable kernel it deliberately widens a use-after-free race window; a hit can crash or corrupt the box. Run it only on machines you own or are explicitly authorized to test (test VMs, throwaway CI machines), never on production systems.
SO_REUSEPORT allows many sockets to bind the same UDP port; for each
incoming packet the kernel picks one socket of the group in
reuseport_select_sock() (net/core/sock_reuseport.c). A group can install a
selector program — a classic BPF program attached with
setsockopt(SO_ATTACH_REUSEPORT_CBPF) — which decides per packet which socket
of the group receives it. The program executes in the RX softirq (network
receive processing) inside an RCU read-side critical section.
The bug: when the program is replaced or detached via setsockopt()
(reuseport_attach_prog() / reuseport_detach_prog()), the old cBPF program
is freed immediately by sk_reuseport_prog_free(), without waiting for
in-flight RCU readers. A CPU still walking the freed program's instructions
reads freed vmalloc'd memory:
sequenceDiagram
autonumber
participant C as CPU0 — churner thread
participant K as setsockopt() path
participant R as CPU1 — RX softirq
R->>R: rcu_read_lock()
R->>R: prog = rcu_dereference(reuse->prog)
C->>K: setsockopt(SO_ATTACH_REUSEPORT_CBPF, progB)
K->>K: swap progA → progB
K->>K: sk_reuseport_prog_free(progA)
Note right of K: unfixed kernels: bpf_prog_free()<br/>runs NOW — no RCU grace period
R->>R: execute progA->insns (run_bpf_filter)
Note right of R: progA was already freed<br/>KASAN: vmalloc-out-of-bounds
Note over K: fix: call_rcu(sk_reuseport_prog_free_rcu) —<br/>free deferred by one RCU grace periodThe eBPF selector path (SO_ATTACH_REUSEPORT_EBPF) is not affected: it
already releases programs through deferred bpf_prog_put() stages. The fix
gives the cBPF path the same treatment — one RCU grace period before the old
program is freed.
The upstream KASAN report (on a 7.0 debug kernel):
BUG: KASAN: vmalloc-out-of-bounds in reuseport_select_sock+0xedc/0x1220
Read of size 4 at addr ffffc9000051e004 by task slowme/10208
net/core/sock_reuseport.c:596
| File | Purpose |
|---|---|
reuseport_race_hammer.c | The reproducer: multithreaded hammer that churns the cBPF selector under full UDP load, then verifies delivery integrity. |
run_hammer.sh | One-run wrapper: raises net.core.optmem_max, runs the hammer, then checks dmesg for splats, /proc/vmallocinfo for leaked bpf_prog allocations, and optionally kmemleak. |
livepatch_cycle.sh | Applies/reverts a livepatch carrying the fix while the hammer runs — hunts livepatch lifecycle hazards of the call_rcu()-based fix. |
Makefile | Builds the hammer. |
.github/workflows/ci.yml | CI: build + shellcheck (no runtime kernel tests; see CI). |
On a Linux test box:
$ make
$ sudo ./run_hammer.sh 600 # 10-minute run
...
== result: RC=0 (0 clean / 1 setup / 2 splat / 3 leak / 4 integrity) ==
Requirements:
gcc and bash.dmesg, /proc/vmallocinfo, sysctl);
the hammer itself runs unprivileged (the upstream repro ran as UID 1000).Expected results:
RC=2; occasionally the
hammer's integrity check fires first → RC=4.RC=0, stable bpf_prog vmalloc count.The race window is tiny (free vs. in-flight RX execution), so treat a single clean run as inconclusive. For real testing run hours, e.g.:
$ sudo ./run_hammer.sh 86400 512 8 16 4 127.0.0.1 0
reuseport_race_hammer$ ./reuseport_race_hammer [dur_sec] [insns] [nports] [nsocks] [nsenders] [ip] [ebpf]
| Argument | Default | Meaning |
|---|---|---|
dur_sec | 300 (min 45) | total run seconds |
insns | 256 | filler instructions in the churned cBPF program; a bigger program is a bigger freed region to hit. If attach fails with ENOMEM, raise net.core.optmem_max (the wrapper does this for you). |
nports | 4 (max 64) | reuseport groups (one UDP port each, from 21000) |
nsocks | 8 (max 512) | sockets per group |
nsenders | 4 (max 32) | UDP sender threads per group |
ip | 127.0.0.1 | target address; use a physical NIC IP to spread RX softirqs across CPUs (RSS) |
ebpf | 0 | 1 = also churn SO_ATTACH_REUSEPORT_EBPF attach/detach (needs CAP_BPF/CAP_NET_ADMIN); that path is not vulnerable, this is for comparison/coverage |
Each group runs one churner thread that swaps and detaches the cBPF
selector via setsockopt() in a tight loop, and sender threads flooding
64-byte UDP datagrams at the group while receivers count per-socket delivery.
Run phases (T = dur_sec):
time ──────────────────────────────────────────────────────────────►
[0 ──────────── T-15s) [T-15s ── T-10s) [T-10s ─────────── T]
CHURN + FLOOD SETTLE MEASURE
churner swaps/detaches churn frozen, deterministic program
the selector prog at final program (selects the LAST
max rate under full attached socket): EVERY packet
UDP flood — THE (selects LAST must land on the LAST
race window open socket) socket; snapshot A →
run → snapshot B
Integrity check: during the measure phase the final program deterministically selects the last socket of the group. If packets received by the group during that window did not all land on that socket, selection went wrong (a possible UAF effect even without KASAN) → exit code 2.
Hammer exit codes: 0 PASS · 1 setup/runtime error · 2 integrity WARN.
run_hammer.sh — one-run wrapperRuns the hammer and adds the checks that make a single run meaningful:
bpf_prog allocation count in /proc/vmallocinfo;net.core.optmem_max so multi-KB cBPF programs attach cleanly;DRAIN (default 30s) for RCU/workqueue deferred frees to finish;BUG:, Oops:, WARNING:, RIP:,
leaked, stuck);bpf_prog vmalloc count before/after (leak check), and
optionally scans kmemleak if /sys/kernel/debug/kmemleak exists.| Env | Default | Meaning |
|---|---|---|
HAMMER | ./reuseport_race_hammer | hammer binary |
DRAIN | 30 | seconds to wait after the run before checking |
OPTMEM_MAX | 131072 | value for net.core.optmem_max; 0 = don't touch |
Exit codes: 0 clean · 1 setup error (including hammer setup failure) ·
2 kernel splat seen · 3 possible bpf_prog leak · 4 hammer integrity
WARN.
livepatch_cycle.sh — livepatch lifecycle testingThe fix frees the old cBPF program from an call_rcu() callback. If you ship
the fix as a livepatch (kernel live patching — code patched into a
running kernel), the callback function itself lives in the patch module: a
revert/unload while callbacks are still pending frees the module text under
the callback's feet. This script exercises apply/revert cycles while the
hammer keeps the race window hot, and watches
/sys/kernel/livepatch/*/transition and dmesg.
$ MODE=rcu ./livepatch_cycle.sh 20 120 # 20 cycles × 120s hammer each
| Mode | Behavior |
|---|---|
cycle (default) | apply → revert, both under sustained hammer load |
rcu | revert immediately at max churn — the hazard case above |
safe | stop hammer → sleep GRACE (default 30s, one grace period) → revert |
| Env | Default | Meaning |
|---|---|---|
APPLY_CMD / REVERT_CMD | kpatch load $PATCH / kpatch unload $PATCH | livepatch commands |
PATCH | ./livepatch-reuseport.ko | patch module |
HAMMER | ./reuseport_race_hammer | hammer binary |
HAMMER_ARGS | 256 4 8 4 127.0.0.1 0 | hammer arguments |
TRANSITION_TIMEOUT | 60 | max seconds to wait for a livepatch transition |
GRACE | 30 | grace-period sleep for MODE=safe |
FORCE | 0 | 1 = run even if no livepatch transition is detected |
Exit codes: 0 clean · 1 command failure · 2 splat or stuck transition ·
4 integrity WARN from the hammer.
CI builds the hammer with two flag sets and runs shellcheck on the scripts. Kernel runtime tests are intentionally not run on shared CI runners: the reproducer needs control over the runner's kernel version (and on a vulnerable kernel could oops the runner). Run those on real test machines.
GPL-2.0-only — see LICENSE.