
POC for CVE-2026-31429 (Linux Kernel >= 6.3 < 6.12.82 Slab Cross-Cache Confusion) - vulnerability discovered by Antonius - w1sdom - bluedragonsec.com
bpf_prog_test_run_skbSeverity: Medium (CWE-763: Release of Invalid Pointer or Reference)
Published: 2026-04-20
Affected subsystem: net/core/skbuff.c — skb_kfree_head()
Researcher: Antonius / w1sdom — Blue Dragon Security
Contact: [email protected]
Lore thread: https://lore.kernel.org/netdev/CAK8a0jxC5L5N7hq-DT2_NhUyjBxrPocoiDazzsBk4TGgT1r4-A@mail.gmail.com/
This repository contains the proof-of-concept for CVE-2026-31429 (not a working exploit, just a POC), a slab cross-cache confusion bug in the Linux kernel networking stack. The bug is triggered when KFENCE is enabled and a caller (specifically bpf_test_init in net/bpf/test_run.c) allocates an SKB head buffer via kzalloc() with a size that happens to equal SKB_SMALL_HEAD_CACHE_SIZE. Due to KFENCE's exact-size reporting semantics, the kernel's skb_kfree_head() function incorrectly frees the object back to skb_small_head_cache instead of the original kmalloc-1k cache, corrupting slab metadata.
| Status | Range |
|---|---|
| Affected | Linux >= 6.3 (introduced by bf9f1baa279f) |
| Unaffected | < 6.3 |
| Fixed | >= 6.12.82 |
| Fixed | >= 6.18.23 |
| Fixed | >= 6.19.13 |
| Fixed | >= 7.0 (mainline, commit 0f42e3f4fe2a) |
The vulnerability was introduced by commit bf9f1baa279f ("net: add dedicated kmem_cache for typical/small skb->head"), which added skb_small_head_cache and the conditional free logic in skb_kfree_head().
skb_small_head_cache Design IntentSKB_SMALL_HEAD_CACHE_SIZE is intentionally set to a non-power-of-2 value (e.g. 704 bytes on x86_64) to avoid collisions with generic kmalloc bucket sizes (always powers of 2: 512, 1024, ...). The heuristic in skb_kfree_head() exploits this uniqueness to route frees using only skb_end_offset:
// net/core/skbuff.c (VULNERABLE — pre-fix)
static void skb_kfree_head(void *head, unsigned int end_offset)
{
if (end_offset == SKB_SMALL_HEAD_HEADROOM)
kmem_cache_free(net_hotdata.skb_small_head_cache, head);
else
kfree(head);
}
end_offset == SKB_SMALL_HEAD_HEADROOM → assumed from skb_small_head_cache → kmem_cache_free()kfree()This heuristic is sound only under normal slab semantics, where ksize() returns the bucket size (1024 for a 704-byte request), which is never equal to SKB_SMALL_HEAD_CACHE_SIZE.
KFENCE (Kernel Electric-Fence) intercepts a subset of kernel allocations and serves them from guard-paged memory. Its critical behavioral difference: kfence_ksize() returns the exact requested size, not the slab bucket size.
BPF_PROG_TEST_RUN (syscall 321, cmd BPF_PROG_TEST_RUN=10)
└─> __sys_bpf()
└─> bpf_prog_test_run_skb()
└─> bpf_test_init()
└─> kzalloc(size, GFP_USER)
│ size == SKB_SMALL_HEAD_CACHE_SIZE (704 on x86_64)
│ KFENCE intercepts → object served from kmalloc-1k region
│
└─> slab_build_skb(data, NULL, size)
└─> ksize(data)
└─> kfence_ksize() ← returns 704 (exact!)
└─> skb_end_offset
= ksize(data) - sizeof(skb_shared_info)
= 704 - 320
= 384
= SKB_SMALL_HEAD_HEADROOM ← false match!
[On SKB free path:]
└─> sk_skb_reason_drop()
└─> skb_release_data()
└─> skb_free_head()
└─> skb_kfree_head(head, skb->end)
└─> (end_offset == SKB_SMALL_HEAD_HEADROOM) == TRUE
└─> kmem_cache_free(skb_small_head_cache, head)
↑ BUG: head is from kmalloc-1k, not skb_small_head_cache!
→ warn_free_bad_obj() → SLUB corruption
skb_end_offset = 384?On x86_64:
SKB_SMALL_HEAD_CACHE_SIZE = 704 bytes
sizeof(skb_shared_info) = 320 bytes
SKB_SMALL_HEAD_HEADROOM = 704 - 320 = 384
When KFENCE intercepts the 704-byte kzalloc(), kfence_ksize() returns 704 exactly. The arithmetic produces skb_end_offset = 384 = SKB_SMALL_HEAD_HEADROOM, satisfying the conditional in skb_kfree_head() — triggering the wrong free path.
The upstream fix by Jiayuan Chen (reviewed by Eric Dumazet, merged by Jakub Kicinski) eliminates the heuristic entirely:
// net/core/skbuff.c (FIXED)
static void skb_kfree_head(void *head, unsigned int end_offset)
{
kfree(head); // always generic; works for both cases
}
kfree() is safe for both kmalloc-allocated and skb_small_head_cache-allocated memory because kmem_cache_free() on skb_small_head_cache is no longer necessary — the generic kfree() resolves the correct cache internally via the slab page's kmem_cache pointer.
The reproducer (repro_bpf.c) was run on Linux 7.0.0-rc5 in a QEMU environment (i440FX, BIOS 1.17.0-debian). The following kernel WARNING cascade was observed:
[ 3065.322973] ------------[ cut here ]------------
[ 3065.322990] kmem_cache_free(skbuff_small_head, ffff888186d6e000): object belongs to different cache kmalloc-1k
[ 3065.323005] WARNING: mm/slub.c:6258 at warn_free_bad_obj+0x91/0xc0, CPU#0: repro_bpf/2167
[ 3065.323061] CPU: 0 UID: 0 PID: 2167 Comm: repro_bpf Not tainted 7.0.0-rc5 #1 PREEMPT(lazy)
[ 3065.323098] RIP: 0010:warn_free_bad_obj+0x98/0xc0
...
[ 3065.323231] Call Trace:
[ 3065.323247] skb_free_head+0x1ec/0x290
[ 3065.323267] skb_release_data+0x7a6/0x9d0
[ 3065.323308] bpf_prog_test_run_skb+0x14f8/0x3410
[ 3065.323510] __sys_bpf+0x769/0x4b60
[ 3065.323763] __x64_sys_bpf+0x78/0xc0
[ 3065.323794] do_syscall_64+0x111/0x690
[ 3065.323813] entry_SYSCALL_64_after_hwframe+0x77/0x7f
The WARNING cascade produces 4 separate splats per trigger:
warn_free_bad_obj — primary cross-cache free detection (mm/slub.c:6258)depot_fetch_stack — stack depot pool index out of bounds (lib/stackdepot.c:506) on Allocated trackingstack_depot_print — corrupt handle detected (lib/stackdepot.c:780)depot_fetch_stack + stack_depot_print — same pair repeated for Freed trackingThis cascade indicates the object's SLUB tracking metadata (alloc_track / free_track) references a stack depot handle that becomes corrupted after the wrong-cache free.
Kernel: Linux >= 6.3, compiled with:
CONFIG_KFENCE=y
CONFIG_BPF_SYSCALL=y
CONFIG_NET_SCH_INGRESS=y (or any SCHED_CLS capable driver)
CONFIG_SLUB_DEBUG=y (for warn_free_bad_obj visibility)
CONFIG_STACKDEPOT=y (for full cascade)
Privileges: root (uid=0) — required for BPF_PROG_LOAD
gcc -O2 -o cve-2026-31429-poc-only cve-2026-31429-poc-only.c
sudo ./cve-2026-31429-poc-only
dmesg | grep -E "warn_free_bad_obj|Wrong slab cache|cross-cache"
The PoC loads a minimal 3-instruction BPF program (type BPF_PROG_TYPE_SCHED_CLS):
ld_imm64 r0, 0 ; 2 insns (wide)
exit ; 1 insn
It then calls BPF_PROG_TEST_RUN (cmd=10) with:
data_size_in = 284 bytes of Syzkaller-derived packet dataflags = BPF_F_TEST_RUN_ON_CPU (0x4) — pins execution to CPU 0repeat = 4The 284-byte input data exercises bpf_test_init's allocation path such that the requested buffer size equals SKB_SMALL_HEAD_CACHE_SIZE, probabilistically hitting the KFENCE interception window.
| Commit | Tree | Merged by | Date |
|---|---|---|---|
0f42e3f4fe2a | mainline | Jakub Kicinski | 2026-04-06 |
60313768a8ed | linux-stable | Greg Kroah-Hartman | 2026-04-18 |
2d64618ea846 | linux-stable | Greg Kroah-Hartman | 2026-04-18 |
474e00b935db | linux-stable | Greg Kroah-Hartman | 2026-04-18 |
Signed-off chain: Jiayuan Chen → Reviewed-by Eric Dumazet (Google) → Jakub Kicinski → Greg Kroah-Hartman
Reported-by credit: Antonius <[email protected]> in all 4 commits
Introduced by: bf9f1baa279f ("net: add dedicated kmem_cache for typical/small skb->head")
.
├── README.md — this file
├── cve-2026-31429-poc-only.c — proof-of-concept only (not an exploit)
└── dmesg.txt — raw kernel splat from successful reproduction
| Date | Event |
|---|---|
| ~Early 2026 | Bug discovered via Syzkaller fuzzing on Linux 7.0-rc5 |
| 2026-04-03 | Patch authored by Jiayuan Chen, Reported-by credit to Antonius |
| 2026-04-06 | Mainline commit 0f42e3f4fe2a merged by Jakub Kicinski |
| 2026-04-18 | Stable backports merged by Greg Kroah-Hartman (6.12.x, 6.18.x, 6.19.x) |
| 2026-04-20 | CVE-2026-31429 published |
Antonius (nickname: w1sdom)
Founder & Senior Researcher — Blue Dragon Security
Indonesia
[email protected]
This PoC is released for educational and research purposes after the upstream patch was available. Do not use on systems you do not own or have explicit permission to test. The author bears no responsibility for misuse.