1. Overview
A use-after-free vulnerability exists in the Linux kernel’s BPF netfilter link implementation. The bpf_nf_link_lops operations structure uses synchronous deallocation (.dealloc) instead of RCU-deferred freeing (.dealloc_deferred), allowing a use-after-free when concurrent hook enumeration via nfnetlink races with BPF link destruction. The UAF on the kmalloc-192 slab cache is exploitable for local privilege escalation through heap spray and function pointer hijacking. The Linux kernel community addressed this vulnerability in kernel version 7.0-rc5.
2. Vulnerability Type
| Field | Value |
|---|---|
| Primary CWE | CWE-416: Use After Free |
| Related CWE | CWE-362: Concurrent Execution using Shared Resource with Improper Synchronization |
3. Severity
CVSS 4.0
| Metric Group | Metric | Value |
|---|---|---|
| Base – Exploitability | Attack Vector (AV) | Local |
| Attack Complexity (AC) | High | |
| Attack Requirements (AT) | None | |
| Privileges Required (PR) | High | |
| User Interaction (UI) | None | |
| Base – Vulnerable System | Confidentiality (VC) | High |
| Integrity (VI) | High | |
| Availability (VA) | High | |
| Base – Subsequent System | Confidentiality (SC) | None |
| Integrity (SI) | None | |
| Availability (SA) | High | |
| Threat | Exploit Maturity (E) | Proof-of-Concept |
| Field | Value |
|---|---|
| CVSS 4.0 Score | 7.1 (High) |
| CVSS 4.0 Vector | CVSS:4.0/AV:L/AC:H/AT:N/PR:H/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:H/E:P |
4. Affected Products
Affected Versions
| Component | Introduced | Fixed |
|---|---|---|
| BPF netfilter link (nf_bpf_link.c) | 2023-04-21, kernel 6.4-rc1 (commit 84601d6ee68a) | 7.0-rc5 (commit 24f90fa3994b) |
Fix Commit
| Commit | Scope | Description |
|---|---|---|
24f90fa3994b | BPF | Defer hook memory release until RCU readers are done |
Tested Environment (Vulnerable)
| Field | Value |
|---|---|
| Platform | QEMU VM (KASAN-enabled) |
| Kernel | 6.14.0 (custom build, KASAN) |
| Architecture | x86_64 |
Tested Environment (Patched)
| Field | Value |
|---|---|
| Kernel | 7.0.0-rc5+ (mainline) |
| Fix commit | 24f90fa3994b |
5. Root Cause Analysis
5a. Detailed Description
The bpf_nf_link_lops operations struct uses .dealloc (synchronous free) instead of .dealloc_deferred (RCU-deferred free):
// nf_bpf_link.c -- VULNERABLE
static const struct bpf_link_ops bpf_nf_link_lops = {
.release = bpf_nf_link_release,
.dealloc = bpf_nf_link_dealloc, // <-- frees immediately, no RCU wait
.detach = bpf_nf_link_detach,
.show_fdinfo = bpf_nf_link_show_info,
.fill_link_info = bpf_nf_link_fill_link_info,
.update_prog = bpf_nf_link_update,
};
The bpf_nf_link_dealloc() function calls kfree(nf_link) immediately without waiting for an RCU grace period. When a concurrent process enumerates hooks via nfnetlink (NFNL_MSG_HOOK_GET dump), it iterates hook entries under rcu_read_lock(). If a BPF link is freed between the hook entry read and the subsequent field access, nfnl_hook_dump_one() reads from freed slab memory.
The race:
Thread A (netlink dump) Thread B (BPF link destroy)
------------------------------ ----------------------------
rcu_read_lock()
e = rcu_dereference(hooks)
ops = nf_hook_entries_get_hook_ops(e)
bpf_nf_link_release()
nf_unregister_net_hook()
bpf_nf_link_dealloc()
kfree(nf_link) // FREED
nfnl_hook_dump_one(ops[i])
// ops[i] points to freed nf_link->hook_ops
// UAF: reads ops[i]->hook, ops[i]->pf, ops[i]->hook_ops_type
// KASAN: slab-use-after-free
rcu_read_unlock()
5b. Vulnerable Assembly and Call Stack
Linux kernel 6.14.0 (QEMU with KASAN):
BUG: KASAN: slab-use-after-free in nfnl_hook_dump_one.isra.0+0x230/0x760
Read of size 8 at addr ffff88810778e6a0 by task bpf_uaf_crash/88
CPU: 0 UID: 0 PID: 88 Comm: bpf_uaf_crash Not tainted 6.14.0 #2
Call Stack:
nfnl_hook_dump_one.isra.0+0x230/0x760 [nfnetlink_hook]
nfnl_hook_dump+0x164/0x260 [nfnetlink_hook]
netlink_dump+0x316/0x750
__netlink_dump_start+0x3e4/0x4d0
nfnl_hook_get+0x110/0x180 [nfnetlink_hook]
nfnetlink_rcv_msg+0x316/0x4f0 [nfnetlink]
netlink_rcv_skb+0xdf/0x210
nfnetlink_rcv+0xd7/0x220 [nfnetlink]
netlink_unicast+0x38b/0x520
netlink_sendmsg+0x355/0x630
__sys_sendto+0x2f4/0x300
KASAN allocation/free trace:
Allocated by task 93:
bpf_nf_link_attach+0x1b8/0x460
__sys_bpf+0x2e5e/0x3260
Freed by task 93:
kfree+0x121/0x340
bpf_link_release+0x30/0x40
__fput+0x1d5/0x490
__x64_sys_close+0x4f/0x90
The buggy address belongs to the cache kmalloc-192 of size 192
The buggy address is located 96 bytes inside of
freed 192-byte region [ffff88810778e640, ffff88810778e700)
5c. Fix (Patched Version)
Fix commit 24f90fa3994b
Single-line change: replace .dealloc with .dealloc_deferred:
// BEFORE (vulnerable):
.dealloc = bpf_nf_link_dealloc,
// AFTER (patched):
.dealloc_deferred = bpf_nf_link_dealloc,
The .dealloc_deferred field causes bpf_link_free() to defer the kfree() via call_rcu(), ensuring all concurrent rcu_read_lock() holders (including nfnl_hook_dump_one) have exited their critical sections before the memory is freed.
Files changed:
net/netfilter/nf_bpf_link.c | 2 +- (1 line)
5d. Impact
The BPF hook use-after-free allows a local attacker with CAP_BPF and CAP_NET_ADMIN capabilities to achieve local privilege escalation by racing BPF netfilter link creation/destruction against nfnetlink hook enumeration. The freed 192-byte slab object in the kmalloc-192 cache can be reclaimed with attacker-controlled data via heap spraying, allowing the attacker to hijack function pointers read by nfnl_hook_dump_one() during the dangling reference window. In testing, the KASAN-enabled kernel panicked within approximately 1 second of starting the race, with the crash occurring at nfnl_hook_dump_one.isra.0+0x230/0x760 after reading 8 bytes from the freed object. Without KASAN, the UAF was confirmed via kernel WARN traces after approximately 783,000 race iterations, with RAX=0x46 (garbage hook_ops_type value from freed memory). The controlled use-after-free on the kmalloc-192 slab cache, combined with the predictable 96-byte offset into the freed region, makes this exploitable for privilege escalation through slab layout manipulation and function pointer overwrite.
6. Proof-of-Concept
6a. PoC Code
Download poc_cve_2026_23412.c (enterprise email verification required)
| File | Description |
|---|---|
bpf_uaf_crash.c | BPF netfilter hook UAF crash PoC (races BPF link create/destroy vs nfnetlink hook enumeration) |
kasan_crash.log | Full QEMU console output showing KASAN kernel panic |
6b. Reproduce Instructions
Prerequisites:
- Linux kernel 6.4+ with
CONFIG_NETFILTER_BPF_LINK=yandCONFIG_NETFILTER_NETLINK_HOOK=y - For visible crash:
CONFIG_KASAN=yand boot withkasan.fault=panic panic_on_warn=1 - Root privileges (CAP_BPF + CAP_NET_ADMIN)
- gcc and pthreads
Steps:
- Compile:
gcc -O2 -pthread -static -o bpf_uaf_crash bpf_uaf_crash.c - For KASAN testing, build a custom kernel:
# Enable in .config: # CONFIG_KASAN=y, CONFIG_KASAN_GENERIC=y # CONFIG_NETFILTER_BPF_LINK=y, CONFIG_NETFILTER_NETLINK_HOOK=y # Boot with: kasan.fault=panic panic_on_warn=1 oops=panic - Run as root:
./bpf_uaf_crash 30 # 30-second race - On a KASAN-enabled vulnerable kernel, expect:
BUG: KASAN: slab-use-after-free in nfnl_hook_dump_one.isra.0+0x230/0x760 Kernel panic - not syncing: KASAN: panic_on_warn set ...
6c. Test Results
Test 1: kernel 6.14.x (no KASAN)
| Metric | Value |
|---|---|
| Race duration | 30 seconds |
| Successful create/destroy cycles | 783,112 |
| EBUSY retries | 2,688 |
| Kernel WARN traces in dmesg | 2 |
| Crash function | nfnl_hook_dump_one.isra.0+0x296/0x530 |
| RAX (garbage hook_ops_type) | 0x46 |
Test 2: QEMU VM, kernel 6.14.0 (KASAN enabled)
| Metric | Value |
|---|---|
| Time to crash | ~1 second |
| Crash function | nfnl_hook_dump_one.isra.0+0x230/0x760 |
| UAF read size | 8 bytes |
| Freed object cache | kmalloc-192 (192 bytes) |
| Offset into freed region | 96 bytes |
| Result | Kernel panic |
6d. Patched System Verification
On kernel 7.0.0-rc5+ (with fix commit applied):
The .dealloc_deferred field causes kfree() to be deferred via call_rcu(), preventing the freed memory from being read by concurrent nfnl_hook_dump_one() calls. The race is eliminated.
7. Detection
Note: The detection rules below are provided as a starting point. Validate and tune them in your own environment before deploying to production.
7a. Network-Based Detection
This vulnerability is local-only (requires CAP_BPF + CAP_NET_ADMIN on the host). There is no network attack vector, so network-based detection rules are not applicable.
However, post-exploitation network indicators (e.g., reverse shells spawned after privilege escalation) can be monitored. The following Suricata rule detects suspicious BPF-related kernel log messages forwarded via remote syslog, which may indicate exploitation attempts:
alert syslog any any -> any any (msg:"CVE-2026-23412 Possible BPF Netfilter UAF exploitation - KASAN slab-use-after-free in nfnl_hook_dump_one"; content:"slab-use-after-free"; content:"nfnl_hook_dump_one"; within:200; reference:cve,CVE-2026-23412; classtype:attempted-admin; sid:2026234121; rev:1;)
alert syslog any any -> any any (msg:"CVE-2026-23412 Possible BPF Netfilter UAF exploitation - bpf_nf_link freed object access"; content:"bpf_nf_link_attach"; content:"kmalloc-192"; within:400; reference:cve,CVE-2026-23412; classtype:attempted-admin; sid:2026234122; rev:1;)
| Field | Rule 1 (sid:2026234121) | Rule 2 (sid:2026234122) |
|---|---|---|
| Protocol | syslog | syslog |
| Detection | KASAN UAF splat in nfnl_hook_dump_one | Freed bpf_nf_link object in kmalloc-192 |
| Indicator | Post-exploitation kernel log evidence | Post-exploitation kernel log evidence |
7b. Host-Based Detection (YARA)
rule CVE_2026_23412_Vulnerable_Kernel_Version {
meta:
description = "Detects Linux kernel versions vulnerable to CVE-2026-23412 BPF netfilter UAF LPE (6.4.x - 7.0-rc4)"
cve = "CVE-2026-23412"
component = "nf_bpf_link"
severity = "high"
type = "vulnerability"
strings:
// Match kernel version strings in vmlinux or boot images
// Vulnerable range: 6.4.0 through 7.0-rc4 (fixed in 7.0-rc5)
$ver_6_4 = /Linux version 6\.4\.\d{1,3}/ ascii
$ver_6_5 = /Linux version 6\.5\.\d{1,3}/ ascii
$ver_6_6 = /Linux version 6\.6\.\d{1,3}/ ascii
$ver_6_7 = /Linux version 6\.7\.\d{1,3}/ ascii
$ver_6_8 = /Linux version 6\.8\.\d{1,3}/ ascii
$ver_6_9 = /Linux version 6\.9\.\d{1,3}/ ascii
$ver_6_10 = /Linux version 6\.10\.\d{1,3}/ ascii
$ver_6_11 = /Linux version 6\.11\.\d{1,3}/ ascii
$ver_6_12 = /Linux version 6\.12\.\d{1,3}/ ascii
$ver_6_13 = /Linux version 6\.13\.\d{1,3}/ ascii
$ver_6_14 = /Linux version 6\.14\.\d{1,3}/ ascii
$ver_6_15 = /Linux version 6\.15\.\d{1,3}/ ascii
$ver_6_16 = /Linux version 6\.16\.\d{1,3}/ ascii
$ver_6_17 = /Linux version 6\.17\.\d{1,3}/ ascii
$ver_6_18 = /Linux version 6\.18\.\d{1,3}/ ascii
$ver_6_19 = /Linux version 6\.19\.\d{1,3}/ ascii
$ver_7_0_rc = /Linux version 7\.0\.0-rc[1234][^0-9]/ ascii
// Confirm netfilter BPF link support is compiled in (present since 6.4)
$nf_bpf = "nf_bpf_link" ascii
condition:
any of ($ver_6_4, $ver_6_5, $ver_6_6, $ver_6_7, $ver_6_8, $ver_6_9,
$ver_6_10, $ver_6_11, $ver_6_12, $ver_6_13, $ver_6_14,
$ver_6_15, $ver_6_16, $ver_6_17, $ver_6_18, $ver_6_19,
$ver_7_0_rc)
and $nf_bpf
}
rule CVE_2026_23412_Vulnerable_Kernel_Proc {
meta:
description = "Detects vulnerable kernel version from /proc/version or uname output"
cve = "CVE-2026-23412"
component = "nf_bpf_link"
severity = "high"
type = "vulnerability"
strings:
// Matches uname -a or /proc/version output for vulnerable range
// 6.4.x through 6.19.x
$ver_6 = /Linux version 6\.(([4-9]|1[0-9])\.\d{1,3})/ ascii
// 7.0-rc1 through 7.0-rc4
$ver_7_0_rc_vuln = /Linux version 7\.0\.0-rc[1234][^0-9]/ ascii
condition:
filesize < 4KB and
any of ($ver_6, $ver_7_0_rc_vuln)
}
rule CVE_2026_23412_Patched_Kernel {
meta:
description = "Detects patched kernel containing the dealloc_deferred fix for BPF netfilter link"
cve = "CVE-2026-23412"
component = "nf_bpf_link"
severity = "info"
type = "patch_verification"
strings:
// The fix changes .dealloc to .dealloc_deferred in the bpf_nf_link_lops struct.
// In patched vmlinux, the dealloc_deferred function pointer is set at a different
// struct offset than .dealloc, and the string "bpf_nf_link_dealloc" appears
// alongside "dealloc_deferred" in kallsyms/debug info.
$nf_bpf = "nf_bpf_link" ascii
$dealloc_deferred = "dealloc_deferred" ascii
$ver_7_0_rc5_plus = /Linux version 7\.0\.0-rc[5-9]/ ascii
$ver_7_0_release = /Linux version 7\.0\.[0-9]/ ascii
$ver_7_1_plus = /Linux version 7\.[1-9]/ ascii
condition:
($nf_bpf and $dealloc_deferred) or
any of ($ver_7_0_rc5_plus, $ver_7_0_release, $ver_7_1_plus)
}
Usage:
# Scan vmlinux image for vulnerable kernel
yara -s CVE_2026_23412_rules.yar /boot/vmlinuz-$(uname -r)
# Scan /proc/version for quick version check
yara -s CVE_2026_23412_rules.yar /proc/version
Rule summary:
| Rule | Match Means |
|---|---|
CVE_2026_23412_Vulnerable_Kernel_Version | Kernel binary is in vulnerable version range (6.4 - 7.0-rc4) with BPF netfilter support |
CVE_2026_23412_Vulnerable_Kernel_Proc | Running kernel version is in vulnerable range (lightweight /proc/version check) |
CVE_2026_23412_Patched_Kernel | Kernel contains the dealloc_deferred fix or is version 7.0-rc5+ |
8. References
| Source | URL |
|---|---|
| Fix commit (BPF UAF) | https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=24f90fa3994b |
| BPF netfilter link introduction | https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=84601d6ee68a |