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

FieldValue
Primary CWECWE-416: Use After Free
Related CWECWE-362: Concurrent Execution using Shared Resource with Improper Synchronization

3. Severity

CVSS 4.0

Metric GroupMetricValue
Base – ExploitabilityAttack Vector (AV)Local
Attack Complexity (AC)High
Attack Requirements (AT)None
Privileges Required (PR)High
User Interaction (UI)None
Base – Vulnerable SystemConfidentiality (VC)High
Integrity (VI)High
Availability (VA)High
Base – Subsequent SystemConfidentiality (SC)None
Integrity (SI)None
Availability (SA)High
ThreatExploit Maturity (E)Proof-of-Concept
FieldValue
CVSS 4.0 Score7.1 (High)
CVSS 4.0 VectorCVSS: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

ComponentIntroducedFixed
BPF netfilter link (nf_bpf_link.c)2023-04-21, kernel 6.4-rc1 (commit 84601d6ee68a)7.0-rc5 (commit 24f90fa3994b)

Fix Commit

CommitScopeDescription
24f90fa3994bBPFDefer hook memory release until RCU readers are done

Tested Environment (Vulnerable)

FieldValue
PlatformQEMU VM (KASAN-enabled)
Kernel6.14.0 (custom build, KASAN)
Architecturex86_64

Tested Environment (Patched)

FieldValue
Kernel7.0.0-rc5+ (mainline)
Fix commit24f90fa3994b

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)

FileDescription
bpf_uaf_crash.cBPF netfilter hook UAF crash PoC (races BPF link create/destroy vs nfnetlink hook enumeration)
kasan_crash.logFull QEMU console output showing KASAN kernel panic

6b. Reproduce Instructions

Prerequisites:

  • Linux kernel 6.4+ with CONFIG_NETFILTER_BPF_LINK=y and CONFIG_NETFILTER_NETLINK_HOOK=y
  • For visible crash: CONFIG_KASAN=y and boot with kasan.fault=panic panic_on_warn=1
  • Root privileges (CAP_BPF + CAP_NET_ADMIN)
  • gcc and pthreads

Steps:

  1. Compile:
    gcc -O2 -pthread -static -o bpf_uaf_crash bpf_uaf_crash.c
    
  2. 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
    
  3. Run as root:
    ./bpf_uaf_crash 30   # 30-second race
    
  4. 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)

MetricValue
Race duration30 seconds
Successful create/destroy cycles783,112
EBUSY retries2,688
Kernel WARN traces in dmesg2
Crash functionnfnl_hook_dump_one.isra.0+0x296/0x530
RAX (garbage hook_ops_type)0x46

Test 2: QEMU VM, kernel 6.14.0 (KASAN enabled)

MetricValue
Time to crash~1 second
Crash functionnfnl_hook_dump_one.isra.0+0x230/0x760
UAF read size8 bytes
Freed object cachekmalloc-192 (192 bytes)
Offset into freed region96 bytes
ResultKernel 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;)
FieldRule 1 (sid:2026234121)Rule 2 (sid:2026234122)
Protocolsyslogsyslog
DetectionKASAN UAF splat in nfnl_hook_dump_oneFreed bpf_nf_link object in kmalloc-192
IndicatorPost-exploitation kernel log evidencePost-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:

RuleMatch Means
CVE_2026_23412_Vulnerable_Kernel_VersionKernel binary is in vulnerable version range (6.4 - 7.0-rc4) with BPF netfilter support
CVE_2026_23412_Vulnerable_Kernel_ProcRunning kernel version is in vulnerable range (lightweight /proc/version check)
CVE_2026_23412_Patched_KernelKernel contains the dealloc_deferred fix or is version 7.0-rc5+

8. References

SourceURL
Fix commit (BPF UAF)https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=24f90fa3994b
BPF netfilter link introductionhttps://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=84601d6ee68a