This post is written by Milot Shala, Cybersecurity Director at ANIMARUM, a Red Team Lead and Offensive Security Architect with 25 years of experience across enterprise security, cloud infrastructure, and adversary simulation. This is a part of In the Field series of blog posts.

Note: All testing described in this post was conducted on systems we own. The exploit code referenced is publicly disclosed by the original researcher Hyunwoo Kim (@v4bel) at github.com/V4bel/dirtyfrag, after the disclosure embargo was broken by an unrelated third party.


The embargo, the quiet window between a private vulnerability report and the public reveal where defenders ship patches first, broke yesterday. The PoC was public by morning. There is no patch for one of the two variants in any kernel tree right now. We spun up our Ubuntu 24.04 and RHEL 10.1 lab boxes the same morning and ran Hyunwoo Kim's published exploit. Both rooted in under a minute.

We want to start this one by connecting two dates: April 29 and May 7 2026. On April 29, 2026, Hyunwoo Kim reported the first variant of this bug class to security@kernel.org with a weaponized proof-of-concept. On May 7, 2026, the same day Hyunwoo submitted coordinated disclosure to linux-distros@vs.openwall.org with a five-day embargo, an unrelated third party published the full exploit publicly. The embargo clause was clear: disclosure by any party during the embargo period triggers immediate full release. Hyunwoo published the complete document that same day.

One day later, the xfrm-ESP variant received a patch in mainline. The RxRPC variant still has none.

What is Dirty Frag

Dirty Frag is two local privilege escalation vulnerabilities in the Linux kernel, both discovered and reported by Hyunwoo Kim (@v4bel). They share a bug class: both allow an unprivileged user to write attacker-influenced bytes into the page cache of a file they can only open read-only, through the frag slot of a sender-side sk_buff. The name is deliberate. The bug dirties the frag. Dirty Pipe dirtied struct pipe_buffer. Dirty Frag dirties struct sk_buff.

CVE-2026-43284 is the xfrm-ESP variant. The vulnerable code has existed since kernel commit cac2661c53f3 on January 17, 2017. A patch landed in mainline at commit f4c50a4034e6 on May 8, 2026. Distribution backports are rolling out under pressure as of the time of writing. CVE-2026-43500 is the RxRPC variant. The vulnerable code has existed since kernel commit 2dc334f1a63a in June 2023. There is no patch in any tree yet. CVE-2026-43500 is reserved only.

The public test grid from Hyunwoo's disclosure covers Ubuntu 24.04.4 (6.17.0-23-generic), RHEL 10.1 (6.12.0-124.49.1.el10_1.x86_64), openSUSE Tumbleweed (7.0.2-1-default), CentOS Stream 10 (6.12.0-224.el10.x86_64), AlmaLinux 10 (6.12.0-124.52.3.el10_1.x86_64), and Fedora 44 (6.19.14-300.fc44.x86_64). Our own scope was narrower. We re-ran the public PoC against our own Ubuntu 24.04 lab boxes the morning the embargo broke, matching the first two entries in that grid. Both rooted cleanly, as documented. We did not test the remaining four.

A second researcher, Kuan-Ting Chen, independently submitted a vulnerability report for the ESP variant with a reproducer approximately nine hours after Hyunwoo's ESP report. Chen also submitted a distinct patch for the same variant four days later, on May 4 2026. The fix that merged into mainline on May 8 is based on Chen's shared-frag approach, not Hyunwoo's v1 patch. Both deserve credit.

The Same Shape As Copy Fail, Two New Sinks

If you read our Copy Fail post, you already know this bug class. If you did not, here is the shape in one paragraph.

splice() is a zero-copy kernel primitive. When the source is a file backed by the page cache, splice does not duplicate the bytes. It hands the kernel a reference to the live page-cache pages. This is what splice was designed to do. It becomes a problem when those page-cache pages end up in the destination scatterlist of a crypto operation that performs in-place writes. Copy Fail got there through algif_aead, the AF_ALG kernel interface for AEAD ciphers. The splice reference reached a page the attacker only had read access to, authencesn's ESN rearrangement wrote four bytes of attacker-controlled values into that page, and the kernel never noticed. The on-disk inode is unchanged. Every process that subsequently reads the file sees the mutated cache.

Dirty Frag follows the same shape. splice() pins a read-only page-cache page into the frag slot of a sender-side sk_buff. The receiver-side kernel code then performs in-place crypto on top of that frag. The page cache of files the attacker can only read is modified in RAM, and every subsequent read sees the modified copy. The disk is never touched.

The difference is the sink. Copy Fail used algif_aead. Dirty Frag uses two completely different network paths: xfrm-ESP input and RxRPC rxkad verify. These are independent code paths with no shared gate.

This means the mitigation you applied for Copy Fail does not protect against Dirty Frag. Blacklisting algif_aead closes the Copy Fail path. It does not close esp4, esp6, or rxrpc. Anyone who patched for Copy Fail must still act on Dirty Frag. We will return to this in the "What To Do Right Now" section.

The xfrm-ESP Variant

The Root Cause

The vulnerability lives in esp_input() in the kernel's IPsec ESP implementation. Here is the relevant branch:

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]
        }
    }
    err = skb_cow_data(skb, 0, &trailer);
    [...]

skb_cow_data() is the correct path. It copies the frag into a fresh kernel allocation before any in-place modification. The branch at [2] skips it. When the skb has a frag but no frag list, the code jumps directly to skip_cow and performs in-place crypto on whatever memory backs that frag. If the attacker has used vmsplice() and splice() to pin a page-cache page into the frag, that page becomes both src and dst for the crypto operation.

The in-place crypto path reaches crypto_authenc_esn_decrypt(), which contains:

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]
    [...]

The 4-byte STORE at [3] writes tmp + 1 into the destination SGL at offset assoclen + cryptlen. tmp + 1 traces back through esp_input_set_header() to XFRM_SKB_CB(skb)->seq.input.hi, which is replay_esn->seq_hi. The user controls seq_hi via the XFRMA_REPLAY_ESN_VAL netlink attribute at XFRM SA registration time. The attacker controls the offset by choosing assoclen and cryptlen. The attacker controls the written value through seq_hi. Both are set when the SA is registered.

AEAD authentication is checked after the STORE, not before. The authentication fails and returns an error. The page-cache write is already done. The error does not undo it.

Registering an XFRM SA requires CAP_NET_ADMIN. The exploit solves this by calling unshare(CLONE_NEWUSER | CLONE_NEWNET), which creates a new user namespace and a new network namespace. Inside the new user namespace, the unprivileged caller is the namespace root and holds CAP_NET_ADMIN over the new namespace's network stack. The ESP tunnel runs on loopback inside that namespace with UDP encapsulation on port 4500.

The Exploit Flow

The target is the page cache of /usr/bin/su. The exploit writes 192 bytes of static root-shellcode ELF into that page cache, four bytes at a time, across 48 separate XFRM SAs.

Each SA has SPI 0xDEADBE10 + i, and XFRMA_REPLAY_ESN_VAL.seq_hi set to shellcode[i*4..(i+1)*4]. Per chunk, the flow is: vmsplice() pushes a 24-byte forged ESP wire header (SPI plus sequence number plus 16-byte IV filled with 0xCC) into a pipe; splice() moves 16 bytes from /usr/bin/su at file offset i*4 into the next pipe slot; splice() sends the pipe into the UDP socket with MSG_SPLICE_PAGES.

The receiver path is: udp_rcv to xfrm4_udp_encap_rcv to xfrm_input to esp_input, hitting the vulnerable skip_cow branch, to crypto_authenc_esn_decrypt, writing 4 bytes at file offset i*4.

The shellcode entry point at 0x400078 calls setgid(0), setuid(0), setgroups(0, NULL), and execve("/bin/sh", NULL, ["TERM=xterm", NULL]). Once all 48 chunks are written, the parent process opens a PTY, execs /usr/bin/su, and the kernel loads the mutated page-cache image. The setuid-root bit is still on the on-disk inode. The kernel grants euid 0. The shellcode runs.

The RxRPC Variant

The Root Cause

The second vulnerability lives in rxkad_verify_packet_1() in the kernel's RxRPC implementation, the protocol used by AFS (Andrew File System):

static int rxkad_verify_packet_1(struct rxrpc_call *call, struct sk_buff *skb,
                                 rxrpc_seq_t seq,
                                 struct skcipher_request *req)
{
    [...]
    sg_init_table(sg, ARRAY_SIZE(sg));
    ret = skb_to_sgvec(skb, sg, sp->offset, 8);
    [...]
    memset(&iv, 0, sizeof(iv));
    skcipher_request_set_sync_tfm(req, call->conn->rxkad.cipher);
    skcipher_request_set_callback(req, 0, NULL, NULL);
    skcipher_request_set_crypt(req, sg, sg, 8, iv.x);     // [4]
    ret = crypto_skcipher_decrypt(req);                   // [5]

At [4], src and dst are the same SGL, both pointing directly to the frag the attacker has pinned via splice. skb_to_sgvec() converts the skb's frag into the SGL without copying it. At [5], crypto_skcipher_decrypt() performs an 8-byte in-place write on top of that frag. If the attacker pinned /etc/passwd page cache pages into the frag, 8 bytes of /etc/passwd are now overwritten in RAM.

Unlike the xfrm-ESP variant, the 8 written bytes are not directly attacker-controlled. The cipher is pcbc(fcrypt) with IV=0, and with a single 8-byte block, pcbc(fcrypt) reduces to a single call to fcrypt_decrypt(C, K). C is whatever ciphertext sits at that file offset immediately before the write. K is the 8-byte session key from the RxRPC v1 token, which the unprivileged user registers via add_key("rxrpc", ...). The attacker cannot choose the output directly, but they can choose K. They can run fcrypt_decrypt(C, K) in user space for any candidate K until the output equals the desired plaintext. That is a brute-force search over the 8-byte session key space, but fcrypt is the AFS-dedicated cipher with a 56-bit effective key and an 8-byte block, and its design makes it fast to compute. Hyunwoo's implementation reaches roughly 18 million keys per second on a modern laptop core. A 5-millisecond search finds the right key for most target byte sequences.

Critically, this variant requires no namespace privilege. There is no unshare() call, no CAP_NET_ADMIN, no user namespace at all. However, rxrpc.ko must be loaded. Ubuntu loads it by default. RHEL, SUSE, and AlmaLinux do not.

The Exploit Flow

The target is line 1 of /etc/passwd: the root entry. The goal is to replace bytes 4 through 15, mutating root:x:0:0:... into root::0:0:GGGGGG:.... An empty password field in /etc/passwd passes pam_unix.so with the nullok option, which is enabled by default in common-auth on Ubuntu. Three 8-byte STOREs cover bytes 4 through 11 of the root entry.

The 8-byte write primitive requires 12 bytes of total plaintext to land correctly across the three STOREs. Each STORE alters the ciphertext that the next STORE sees, because pcbc is a chaining mode. The exploit solves the three resulting brute-force problems in sequence: K_A (covering bytes 4..11), K_B (covering bytes 6..13, with C corrected for what STORE one wrote), and K_C (covering bytes 8..15). On the hardware we tested, K_A and K_B each resolve in roughly 5 milliseconds. K_C takes up to a second because the constraint window is tighter.

Each key is planted via add_key("rxrpc", desc, ..., KEY_SPEC_PROCESS_KEYRING) with an RxRPC v1 token carrying sec_ix=2 RXKAD and the chosen 8-byte session key. A forged RxRPC handshake on loopback completes the setup: a fake UDP server sends a CHALLENGE, the client returns a RESPONSE bound to K, and the server drains it. A forged DATA packet follows with a wire checksum derived from K via pcbc(fcrypt) on AF_ALG. Then vmsplice() and double-splice() deliver the /etc/passwd page-cache frag into the receiving sk_buff, triggering rxkad_verify_packet_1().

After the three STOREs land, the parent process opens a PTY and execs /usr/bin/su -. PAM's pam_unix.so nullok admits the empty password for root. su calls setresuid(0,0,0) and executes /bin/bash. The page-cache /etc/passwd has been modified. The on-disk file is untouched. The root shell is live.

Why You Need Both Variants

The two variants are not redundant. They cover each other's blind spots, and any serious operational deployment of this exploit needs both.

Ubuntu by default blocks unprivileged user namespace creation via AppArmor. Without CLONE_NEWUSER, the xfrm-ESP exploit cannot acquire the CAP_NET_ADMIN it needs to register XFRM SAs. It fails with EPERM. On Ubuntu, the ESP variant does not work out of the box. The RxRPC variant does, because Ubuntu loads rxrpc.ko by default and the variant needs no namespace privilege at all.

RHEL, SUSE, and AlmaLinux allow unprivileged user namespace creation by default. The ESP variant works. But those distributions do not load rxrpc.ko as part of their default kernel configuration. The RxRPC variant fails at socket setup because the module is not present.

A single binary that tries the ESP variant first and falls back to RxRPC covers every major distribution in the public test grid. The chain is not about chaining to escalate privilege. Both variants reach root independently. The chain is about coverage.

The Patch That Exists, And The One That Does Not

xfrm-ESP: Merged

The fix for CVE-2026-43284 merged into mainline on May 8, 2026, at commit f4c50a4034e6. The approach is based on Kuan-Ting Chen's shared-frag patch from May 4.

The patch adds a SKBFL_SHARED_FRAG flag that marks page frags arriving through the IPv4 and IPv6 datagram append paths when those frags were pinned via splice. The skip_cow branch in esp_input and esp6_input now checks this flag before skipping skb_cow_data(). Any skb carrying externally pinned pages is forced through the copy-on-write path:

-        } else if (!skb_has_frag_list(skb)) {
+        } else if (!skb_has_frag_list(skb) &&
+               !skb_has_shared_frag(skb)) {
             nfrags = skb_shinfo(skb)->nr_frags;

Hyunwoo's v1 patch took a different approach: calling skb_cow_data() unconditionally in the ESP input fast path. The kernel maintainers chose the shared-frag approach because it places the guard at the frag annotation site rather than the ESP input site, making the protection more general. Credit for the merged fix goes to Kuan-Ting Chen. Hyunwoo Kim gets credit for the discovery and the disclosure.

Distribution backports of f4c50a4034e6 were rolling out as of May 8. Expect a lag of 24 to 72 hours for most enterprise distributions, longer for those with slower update cycles.

RxRPC: No Patch Yet

CVE-2026-43500 has no patch in any upstream or distribution kernel tree as of May 8, 2026. Hyunwoo submitted a fix: add || skb->data_len to the skb_cloned(skb) gate before the in-place decrypt in rxkad_verify_packet_1(). When the skb has a non-zero data_len, indicating that payload lives in frags rather than in the skb's linear buffer, the code would route through skb_copy() to produce an isolated copy before decryption. That patch is under review on netdev. It has not merged. It has not been backported. No distribution ships it.

The only mitigation for CVE-2026-43500 right now is to unload rxrpc.ko and blacklist it from reloading.

What To Do Right Now

You may want to run this on every host:

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"

This command does three things. It writes persistent module blacklist entries for esp4, esp6, and rxrpc, so they do not load on the next boot or on-demand load. It unloads the modules from the running kernel if they are present. And it flushes the page cache.

The page-cache flush matters and most people will skip it. If an attacker has already run the exploit before you apply this mitigation, the poisoned page-cache pages for /usr/bin/su or /etc/passwd are still live in RAM. They will be served to every process that reads those files until memory pressure evicts them or the cache is explicitly cleared. Flushing drop_caches to 3 evicts all clean file-backed pages, which includes any poisoned ones. It is a blunt instrument and it will briefly increase disk I/O on busy systems, but it is the only way to confirm that no poisoned pages are still in flight after the module removal.

What this breaks is a short list. esp4 and esp6 are only used by systems that terminate IPsec tunnels directly in the kernel. If your server is a client behind a VPN gateway, or if IPsec is managed at the network boundary rather than on the host itself, removing these modules has no operational impact. If your server is itself an IPsec endpoint, test the blacklist in staging before applying it in production. rxrpc is used by AFS clients and servers. Outside specific academic and historical-Unix environments, AFS is rare. The vast majority of servers in enterprise environments are not running AFS.

If you cannot immediately apply the blacklist because of IPsec termination requirements, the interim priority is to apply the distribution backport of f4c50a4034e6 as soon as it is available. That patches CVE-2026-43284. You still need to blacklist rxrpc separately, because CVE-2026-43500 has no kernel patch yet and the module blacklist is the only available mitigation for that variant.

One more thing to check: if you applied the Copy Fail mitigation last week by blacklisting algif_aead, that blacklist does not cover Dirty Frag. The /etc/modprobe.d/disable-algif-aead.conf entry from Copy Fail remains correct and should stay in place. Dirty Frag requires additional entries for esp4, esp6, and rxrpc. They are separate file entries targeting separate modules. There is no overlap.

Why This One Hurts More Than Copy Fail

Copy Fail had a nine-day coordinated window. The mainline patch landed on April 29 and disclosure happened the same day, but the kernel community knew it was coming. Distribution security teams had time to prepare. Backports were staged. The CERT-EU advisory came out April 30 with actionable patch guidance already written. Users who were paying attention had a narrow but real window to patch before widespread public exploit availability.

Dirty Frag had none of that. The embargo broke the same day Hyunwoo submitted coordinated disclosure to linux-distros@vs.openwall.org. The intended five-day coordination window collapsed in hours. Distribution security teams got no lead time. The public PoC at github.com/V4bel/dirtyfrag was available before most distribution package maintainers had read the disclosure. The mainline patch for the ESP variant did not land until the day after the public PoC dropped. The RxRPC patch is still in review.

The practical consequence is that anyone running a vulnerable kernel right now is running with a public, working, multi-distro privilege escalation exploit and no kernel-level fix for one of the two variants. Backport timelines are typically 24 to 72 hours after mainline merge for the ESP variant. For RxRPC, the patch has to merge into mainline first, and then backports have to follow. Best case, that is days away. More likely it is a week or more.

There is a second thing Copy Fail had that Dirty Frag does not: a clean detection story. We shipped a non-destructive runtime checker for Copy Fail that any team could drop into CI and get a clear pass or fail. For Dirty Frag, the two-variant chain complicates detection. A host can be immune to the ESP variant (because it does not load esp4.ko, or because AppArmor blocks user namespaces) and still be fully exposed via RxRPC, or vice versa. Module presence, namespace policy, and kernel version all interact. A single binary check has to evaluate all of those axes independently. We are working on a detector and will publish it when we have one that returns accurate results across the full distro matrix.

The last point is structural. The bug class that spans Dirty Pipe, Copy Fail, and Dirty Frag is not a coincidence. All three share the same shape: a legitimate kernel zero-copy primitive plants a reference to read-only memory into a data structure that a separate kernel path treats as writable. The specific primitive changes (pipe buffer, AF_ALG socket, sk_buff frag). The specific write sink changes (pipe_buffer fill, authencesn ESN scratch, ESP in-place decrypt, RxRPC in-place decrypt). The abstraction stays constant. Wherever the kernel allows a zero-copy handoff of memory from a lower-privilege context into a higher-privilege write path, this bug class can occur. Hyunwoo's disclosure identifies splice() plus any in-place crypto sink as the general attack surface. There is no reason to believe that esp4, esp6, and rxrpc are the last sinks in that surface. The kernel has dozens of in-place crypto paths.

The Close

The embargo clause said: if anyone publishes during the embargo, full disclosure follows immediately. Someone did. It did.

The question your ops team should be answering right now is not whether the embargo was fair. It is whether you have time to patch before the next machine that reads your /etc/passwd reads something other than what you put there.