The xfrm-ESP Page-Cache Write vulnerability is a kernel local privilege escalation (LPE) in the IPsec ESP receive path. When aDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/V4bel/dirtyfrag/llms.txt
Use this file to discover all available pages before exploring further.
splice()-planted page cache page lands in frag[0] of a non-linear skb, esp_input() skips the copy-on-write buffer allocation and runs AEAD decryption directly on the frag. A 4-byte sequence-number rearrangement step inside crypto_authenc_esn_decrypt() then writes attacker-chosen bytes permanently into the read-only page cache — even though authentication fails afterward. An unprivileged user who can create a user namespace can exploit this to overwrite the first 192 bytes of /usr/bin/su with a root-shell ELF and execute it.
Root cause
Before performing in-place AEAD decryption on an ESP payload,esp_input() is supposed to call skb_cow_data() whenever the skb is non-linear, so that the frag data is copied into a private kernel buffer before any in-place modifications. The following branch creates a path that bypasses that copy:
esp4.c
[1], the code correctly skips skb_cow_data when the skb is fully linear (no frags). The bug is the branch at [2]: when the skb is non-linear but has no frag_list, the code also jumps directly to skip_cow and performs in-place crypto on whatever page is sitting in frags[0]. If an attacker has pinned a read-only page cache page into that frag via splice, that page becomes both src and dst for the AEAD operation.
The 4-byte STORE
The write happens not during decryption itself but during a byte-rearrangement step insidecrypto_authenc_esn_decrypt(). The ESP + ESN + authencesn(...) combination moves the high-order 32 bits of the sequence number to the end of the source SGL before passing it to the AEAD:
crypto/authencesn.c
[3] writes 4 bytes at position assoclen + cryptlen within the destination SGL. You control exactly where that position falls by tuning the ESP payload length so that the page cache page occupies that offset.
The 4 bytes written are the value of tmp + 1 — the high-order 32 bits of the ESP sequence number. Tracing the value back through esp_input_set_header():
net/ipv4/esp4.c
XFRM_SKB_CB(skb)->seq.input.hi is set from replay_esn->seq_hi, which you freely specify at SA registration time via the XFRMA_REPLAY_ESN_VAL netlink attribute. This gives you full control over both the location (file offset) and the value (4 bytes) of every STORE. AEAD authentication runs after the STORE and always returns -EBADMSG, but by then the page cache modification is permanent.
The STORE survives until the kernel evicts the page (e.g., via
drop_caches or reboot). Every subsequent read() or mmap() of the affected file sees the modified bytes.Privilege required
esp_input() is reached only when an XFRM SA is registered, which requires CAP_NET_ADMIN. You obtain that capability inside an unprivileged user namespace:
0 <real_uid> 1) means the process is root inside the new namespace and can register XFRM SAs within that netns.
On Ubuntu configurations where AppArmor blocks unprivileged user namespace creation,
unshare(CLONE_NEWUSER) returns -EPERM and the ESP variant cannot run. See Chaining the two variants for how the RxRPC variant covers this blind spot.Exploit overview
The target is/usr/bin/su. The exploit replaces the first 192 bytes (file offset 0) of its page cache with a static x86-64 root-shell ELF, leaving the setuid-root bit intact. The new ELF maps 0xb8 bytes at virtual address 0x400000 as R+X via a single PT_LOAD segment. The entry point is at 0x400078 (file offset 0x78).
What the shellcode does at entry 0x400078:
Set up user + net namespace
Fork a child process, call
unshare(CLONE_NEWUSER | CLONE_NEWNET), write identity uid/gid maps, and bring lo up with SIOCSIFFLAGS.Register 48 XFRM SAs
Create one SA per 4-byte chunk. Each SA has a unique SPI (
0xDEADBE10 + i), mode XFRM_MODE_TRANSPORT, flag XFRM_STATE_ESN, algorithm authencesn(hmac(sha256),cbc(aes)), UDP encap sport=dport=4500, and seq_hi set to the 4 bytes you want to write:Trigger each STORE with vmsplice + splice
For each chunk
i, create a fresh sk_recv (bound to 127.0.0.1:4500 with UDP_ENCAP_ESPINUDP) and sk_send (connected to 127.0.0.1:4500). Build a 24-byte forged ESP wire header in a pipe with vmsplice, then splice 16 bytes from file offset i*4 of /usr/bin/su into the next pipe slot, and finally splice the pipe into sk_send:splice_to_socket() automatically sets MSG_SPLICE_PAGES, planting the page cache page P of /usr/bin/su directly into frag[0] of the sender skb.Verify and execute
After all 48 triggers, read back bytes at file offset
0x78 and 0x79. If they are 0x31 and 0xff (the xor edi, edi at the shellcode entry), the patch succeeded. The parent process then executes /usr/bin/su - via forkpty + execve, which maps the modified page cache, gains euid=0 from the setuid bit, and runs the shellcode.What the skb looks like on the receive side
After thesplice, the skb that udp_rcv sees has this layout:
Call chain
Patch
The fix sets theSKBFL_SHARED_FRAG flag on page frags that arrive via splice in the IPv4/IPv6 datagram append paths. The skip-cow branch in esp_input / esp6_input then checks this flag and routes shared-frag skbs through skb_cow_data instead of operating on the attacker-pinned page directly.
The final merged patch uses the
SKBFL_SHARED_FRAG approach submitted by Kuan-Ting Chen four days after the original patch. The commit is f4c50a4034e62ab75f1d5cdd191dd5f9c77fdff4 in the netdev tree and mainline.Disclosure timeline
2026-04-30 — Initial disclosure and patch
2026-04-30 — Initial disclosure and patch
Detailed information about the ESP vulnerability and a weaponized exploit were submitted to
security@kernel.org. A patch was submitted to the netdev mailing list and the issue was made public.2026-04-30 (+9h) — Independent report
2026-04-30 (+9h) — Independent report
Kuan-Ting Chen submitted an independent vulnerability report with a reproducer to
security@kernel.org.2026-05-04 — Shared-frag patch
2026-05-04 — Shared-frag patch
2026-05-07 — Merged and embargo broken
2026-05-07 — Merged and embargo broken
The patch was merged into the netdev tree. Separately, an unrelated third party published the exploit publicly, breaking the linux-distros embargo. Full disclosure followed after agreement from distribution maintainers.
2026-05-08 — Mainline merge and CVE assignment
2026-05-08 — Mainline merge and CVE assignment
Commit
f4c50a4034e6 was merged into mainline. CVE-2026-43284 was assigned.