Security

DirtyFrag: Universal Linux Local Privilege Escalation via Page-Cache Write

A technical breakdown of DirtyFrag, a pair of Linux kernel local privilege escalation vulnerabilities (CVE-2026-43284 and CVE-2026-43500, CVSS 7.8 HIGH) that allow any unprivileged local user to obtain root on most major Linux distributions. By chaining an xfrm-ESP and an RxRPC in-place decryption path flaw, both rooted in the same page-cache write primitive as Dirty Pipe and Copy Fail, the exploit overwrites read-only page cache pages without a race condition, achieving near-100% reliability.

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 0x400000 via PT_LOAD.
  • Entry point 0x400078 calls setgid(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:

  1. Computing the chained ciphertext (accounting for previous STOREs already applied to the page).
  2. Brute-forcing K such that fcrypt_decrypt(C_actual, K) produces the desired plaintext.
  3. Registering the key with add_key, setting up a loopback AF_RXRPC handshake, precomputing the wire checksum, and delivering a forged DATA packet via vmsplice + 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

System info — Ubuntu 24.04.1, kernel 6.17.0-23-generic
Figure 1: Target system — Ubuntu 24.04.1, kernel 6.17.0-23-generic (unpatched)

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:

DirtyFrag PoC — root shell obtained
Figure 2: Unprivileged user obtains 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, and rxrpc modules 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