## https://sploitus.com/exploit?id=300E7A67-B398-5EF3-A196-45B3440BD648
# DIRTYFAIL
> A unified detector and PoC harness for the **Copy Fail** and **Dirty Frag**
> Linux page-cache write vulnerability families.
```
βββββββ ββββββββββ ββββββββββββ βββββββββββ ββββββ ββββββ
ββββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββ
βββ ββββββββββββββ βββ βββββββ ββββββ ββββββββββββββ
βββ ββββββββββββββ βββ βββββ ββββββ ββββββββββββββ
ββββββββββββββ βββ βββ βββ βββ βββ ββββββββββββββ
βββββββ ββββββ βββ βββ βββ βββ βββ ββββββββββββββ
```
DIRTYFAIL is a small, well-documented C tool for security researchers.
It detects whether a Linux host is vulnerable to the three CVEs in this
family, and β with explicit, typed confirmation β runs a real
proof-of-concept that drops the caller into a root shell on a
vulnerable system.
| CVE | Name | DIRTYFAIL coverage |
|-----|------|--------------------|
| **CVE-2026-31431** | Copy Fail (algif_aead `authencesn` page-cache write) | Detect + full PoC |
| **CVE-2026-43284** | Dirty Frag β xfrm-ESP page-cache write | Detect + full PoC |
| **CVE-2026-43500** | Dirty Frag β RxRPC page-cache write | Detect only |
> **Authorized testing only.** Use DIRTYFAIL only on systems you own or
> are explicitly engaged to assess. The exploit modes corrupt
> `/etc/passwd` *in the kernel page cache* (the on-disk file is never
> touched). Cleanup is `dirtyfail --cleanup` or
> `echo 3 > /proc/sys/vm/drop_caches`.
---
## Table of contents
1. [The bug class](#1-the-bug-class)
2. [CVE-2026-31431 β Copy Fail](#2-cve-2026-31431--copy-fail)
3. [CVE-2026-43284 β Dirty Frag (xfrm-ESP)](#3-cve-2026-43284--dirty-frag-xfrm-esp)
4. [CVE-2026-43500 β Dirty Frag (RxRPC)](#4-cve-2026-43500--dirty-frag-rxrpc)
5. [Build](#5-build)
6. [Usage](#6-usage)
7. [How DIRTYFAIL detects each CVE](#7-how-dirtyfail-detects-each-cve)
8. [How DIRTYFAIL exploits each CVE](#8-how-dirtyfail-exploits-each-cve)
9. [Mitigations](#9-mitigations)
10. [Ethics & disclosure](#10-ethics--disclosure)
11. [Credits](#11-credits)
---
## 1. The bug class
**Page-cache write** vulnerabilities let an unprivileged user modify
the kernel's in-memory copy of a file they only have read access to.
The on-disk file is never written; the modification persists in RAM
until the page is evicted (`drop_caches`, memory pressure, or reboot).
This class started with **Dirty Pipe** (CVE-2022-0847), which abused
`pipe_buffer` flags. Copy Fail and Dirty Frag are descendants that
target the `frag` member of `struct sk_buff` instead. The mechanism is
always the same:
1. Userspace `splice()`s a page-cache page from a readable file (e.g.
`/etc/passwd`, `/usr/bin/su`) into the frag of a kernel buffer.
2. A receive path runs **in-place** crypto on that buffer β the same
pages are both source and destination of the operation.
3. The crypto routine performs a "scratch" STORE outside the data
region (a sequence-number rearrangement, a single-block decrypt,
etc.) that lands inside the user-pinned page.
4. The page-cache copy of the file is now permanently modified for
every reader on the host, until the page is evicted.
Because the bug is a **deterministic logic flaw**, not a race, success
rates are essentially 100% and the kernel does not panic on failure.
---
## 2. CVE-2026-31431 β Copy Fail
* Disclosure: **2026-04-29**
* Site:
* Original PoC (C): [Smarttfoxx/copyfail](https://github.com/Smarttfoxx/copyfail)
* Original PoC (Python): [rootsecdev/cve_2026_31431](https://github.com/rootsecdev/cve_2026_31431)
* Introduced by commit: `72548b093ee3` (2017)
* Fixed by commit: `a664bf3d` (mainline 6.12 / 6.17 / 6.18 stables)
* Confirmed affected: Ubuntu 24.04 LTS, Amazon Linux 2023, RHEL 14.3, SUSE 16
### Root cause
The kernel's `algif_aead` module exposes the AEAD crypto API to
userspace via `AF_ALG`. The `authencesn(hmac(sha256), cbc(aes))`
template implements RFC-4303 ESN (Extended Sequence Numbers); part of
its decryption path performs a **4-byte scratch write** to rearrange
the sequence number:
```c
static int crypto_authenc_esn_decrypt(struct aead_request *req)
{
/* Move high-order bits of sequence number to the end. */
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); // β
...
```
The STORE at β is harmless on a normal IPsec packet β it lands inside
the skb's tag area, which is kernel-owned. The crypto template
**assumes** `src` and `dst` point into kernel memory.
`algif_aead` violates that assumption. It accepts `splice()` from
userspace, which plants page-cache pages into the request's
scatterlist. Because the AEAD runs in-place (`req->dst = req->src`),
the page-cache page now sits at the destination scatterlist offset
that the scratch write targets.
The 4 bytes that get written are bytes 4..7 of the AAD that userspace
sent β the "seqno_lo" field of an ESP header, which the attacker fills
with whatever they want.
**Net primitive**: 4-byte arbitrary-offset write into the page cache
of any file the attacker can `open(O_RDONLY)`.
### Exploitation
The simplest weaponization is in `/etc/passwd`. A normal user line
looks like:
```
kara:x:1000:1000:Kara,,,:/home/kara:/bin/bash
```
Flipping `1000` (the UID field, exactly 4 ASCII bytes for any UID
1000β9999) to `0000` makes glibc's `getpwnam()` report uid=0 for
that user. PAM, however, still authenticates against the on-disk
`/etc/shadow` (which is untouched), so `su ` prompts for the
real password, validates it, then `setuid(0)` β and lands at root
because the page-cache copy of `/etc/passwd` says we are root.
`/etc/shadow` integrity is preserved. On-disk `/etc/passwd` is
preserved. Only the kernel's RAM copy of `/etc/passwd` is corrupted,
and only until `drop_caches` or reboot.
---
## 3. CVE-2026-43284 β Dirty Frag (xfrm-ESP)
* Disclosure: **2026-04-30 β 2026-05-08**
* Original PoC (C): [V4bel/dirtyfrag](https://github.com/V4bel/dirtyfrag)
* Researcher: Hyunwoo Kim ([@v4bel](https://x.com/v4bel))
* Introduced by commit: `cac2661c53f3` (2017-01-17)
* Fixed by commit: `f4c50a4034e6` (mainline net.git, merged 2026-05-07)
* Confirmed affected: Ubuntu 24.04, RHEL 10.1, openSUSE Tumbleweed,
CentOS Stream 10, AlmaLinux 10, Fedora 44
### Root cause
`esp_input()` is supposed to call `skb_cow_data()` before in-place AEAD
decryption when an skb is non-linear (i.e. has frags). The code path
has a short-circuit:
```c
if (!skb_cloned(skb)) {
if (!skb_is_nonlinear(skb)) {
nfrags = 1;
goto skip_cow;
} else if (!skb_has_frag_list(skb)) { // β bug
nfrags = skb_shinfo(skb)->nr_frags;
nfrags++;
goto skip_cow;
}
}
```
If the skb has frags but no `frag_list`, esp_input bypasses
`skb_cow_data` and hands the user-supplied frag straight to the AEAD
template. The same `authencesn(...)` scratch write that powers Copy
Fail then lands at file offset `(assoclen + cryptlen)` of the spliced
page.
The 4 STOREd bytes are `seq_hi` from the SA's `replay_esn` state β
attacker-controlled at SA registration time via the
`XFRMA_REPLAY_ESN_VAL` netlink attribute.
**Cost**: registering an XFRM SA needs `CAP_NET_ADMIN`, so the
attacker enters a fresh user namespace via `unshare(CLONE_NEWUSER)`
first. This is allowed by default on most distros (Ubuntu's hardened
profile is the notable exception).
**Crucially, this primitive works even when the algif_aead Copy Fail
mitigation is in place** β the xfrm path doesn't go through algif_aead.
A defender who only blacklisted `algif_aead` is still vulnerable to
Dirty Frag.
### Exploitation
V4bel's published PoC writes a 192-byte static "root-shell" ELF over
the first 192 bytes of `/usr/bin/su`'s page cache, using 48 sequential
4-byte STOREs. After modification, `execve("/usr/bin/su")` runs the
new ELF entry point with the setuid-root bit intact, drops PAM
entirely, and `execve("/bin/sh")` from inside the shellcode.
DIRTYFAIL takes the simpler `/etc/passwd` UID-flip approach (one
4-byte STORE β the same target as Copy Fail) for two reasons:
1. It is a single-write primitive demonstration, easier to study.
2. It is fully reversible with `POSIX_FADV_DONTNEED` and does not
leave `/usr/bin/su` in a corrupt state for other users on the
system.
---
## 4. CVE-2026-43500 β Dirty Frag (RxRPC)
* Disclosure: **2026-04-29 β 2026-05-08**
* Patch: not in any tree as of 2026-05-08; researcher's patch
pending: `lore.kernel.org/all/afKV2zGR6rrelPC7@v4bel/`
* Researcher: Hyunwoo Kim ([@v4bel](https://x.com/v4bel))
* Introduced by commit: `2dc334f1a63a` (2023-06)
### Root cause
`rxkad_verify_packet_1()` performs an **in-place** `pcbc(fcrypt)`
single-block decryption on the first 8 bytes of an RxRPC data packet:
```c
sg_init_table(sg, ARRAY_SIZE(sg));
ret = skb_to_sgvec(skb, sg, sp->offset, 8);
memset(&iv, 0, sizeof(iv));
skcipher_request_set_crypt(req, sg, sg, 8, iv.x); // β src == dst
ret = crypto_skcipher_decrypt(req); // β 8-byte STORE
```
If a page-cache page has been spliced into the skb's frag, the 8-byte
decrypt is performed on top of it.
**Difference from xfrm-ESP**: the 8 bytes that get STOREd are
`fcrypt_decrypt(C, K)`, where `C` is the existing ciphertext at that
file offset and `K` is the session key from an RxRPC v1 token the
attacker registered via `add_key("rxrpc", ...)`. The attacker doesn't
control the STORE value directly β they have to brute-force `K` until
`fcrypt_decrypt(C, K)` produces the desired plaintext.
`fcrypt` is an Andrew File System cipher with a **56-bit key** and
8-byte block. It is deterministic; it ports cleanly to user space; and
its key space is small enough that a constrained 8-byte target can be
brute-forced in milliseconds to seconds depending on the constraint
budget.
**Crucially, this path does NOT need namespace privileges** β
`add_key`, `socket(AF_RXRPC)`, `socket(AF_ALG)`, `splice` are all
available to any unprivileged user. RxRPC fills the gap on Ubuntu's
hardened-userns profile (where xfrm-ESP is blocked) because
`rxrpc.ko` ships in the default Ubuntu build.
### Exploitation
The full exploit:
1. Brute-force `K_A`, `K_B`, `K_C` in user-space such that the three
STOREs at `/etc/passwd` offsets 4, 6, 8 produce
`"::"`, `"0:"`, `"0:GGGGGG:"` respectively (last-write-wins).
2. For each `K_i`, register an RxRPC v1 token with `add_key`, perform
a forged AF_RXRPC handshake against a fake UDP server in the same
process, and trigger `rxkad_verify_packet_1` via splice.
3. The page-cache copy of `/etc/passwd` line 1 is now
`root::0:0:GGGGGG:/root:/bin/bash` β an empty password field.
4. PAM with `pam_unix.so nullok` accepts the empty password; `su -`
drops a root shell.
### DIRTYFAIL coverage
DIRTYFAIL **detects** this CVE (kernel version + rxrpc availability +
AF_RXRPC openable) but does **not** ship a full PoC. Reimplementing
the fcrypt brute-force, the AF_RXRPC handshake forgery, and the rxkad
checksum derivation cleanly from scratch is on the order of 800 lines
on top of what is already public.
For end-to-end exploitation testing, run V4bel's `exp.c` on a
sandboxed VM: .
---
## 5. Build
### Prerequisites
* **Linux** (this binary is Linux-only at runtime).
* `gcc` or `clang`, `make`.
* Linux UAPI headers β specifically ``, ``,
``, ``.
| Distro | Install |
|-------------------|------------------------------------------------------|
| Debian / Ubuntu | `sudo apt install build-essential linux-libc-dev` |
| RHEL / CentOS | `sudo dnf install gcc make kernel-headers glibc-devel` |
| Fedora | `sudo dnf install gcc make kernel-headers` |
| Arch | `sudo pacman -S base-devel` |
### Build commands
```sh
git clone https://github.com//DIRTYFAIL.git
cd DIRTYFAIL
make # release build β ./dirtyfail
make debug # -O0 -g3 for gdb
make static # static link (musl-gcc recommended)
make clean
```
The default build produces a single ~80 KB binary at `./dirtyfail`.
For a portable build that runs on any kernel-compatible Linux without
glibc dependency drift:
```sh
make static CC=musl-gcc
```
(install `musl-tools` on Debian/Ubuntu, or build musl from source).
---
## 6. Usage
```
$ ./dirtyfail --help
Usage: ./dirtyfail [MODE] [OPTIONS]
Modes (pick one; default is --scan):
--scan detect all three CVEs (no system modification)
--check-copyfail Copy Fail (CVE-2026-31431) detection only
--check-esp Dirty Frag xfrm-ESP (CVE-2026-43284) detection only
--check-rxrpc Dirty Frag RxRPC (CVE-2026-43500) detection only
--exploit-copyfail real PoC: flip /etc/passwd UID via algif_aead
--exploit-esp real PoC: flip /etc/passwd UID via xfrm-ESP
--cleanup evict /etc/passwd from page cache and drop_caches
Options:
--no-shell after a successful exploit, do NOT execve `su`;
instead revert the page-cache patch and exit
--no-color disable ANSI color in output
```
### Detection (safe; no system modification)
```sh
./dirtyfail --scan
```
Sample output on a vulnerable Ubuntu 24.04:
```
[*] Copy Fail (CVE-2026-31431) β detection
[i] kernel 6.17.x (affected lines: 6.12, 6.17, 6.18)
[+] AF_ALG + authencesn(hmac(sha256),cbc(aes)) loadable
[*] triggering primitive against /tmp/copyfail-sentinel.XXXX with marker 'PWND'
[!] VULNERABLE β marker 'PWND' landed at sentinel offset 24
[*] Dirty Frag β xfrm-ESP variant (CVE-2026-43284) β detection
[i] kernel 6.17.x
[i] esp4 currently loaded: yes
[i] esp6 currently loaded: yes
[i] unprivileged user namespace: allowed
[!] VULNERABLE (preconditions met) β userns + xfrm SA registration available
[*] Dirty Frag β RxRPC variant (CVE-2026-43500) β detection
[i] rxrpc currently loaded: yes
[i] AF_RXRPC socket: openable
[!] VULNERABLE β RxRPC variant of Dirty Frag is reachable
```
### Exploit (requires typed confirmation)
```sh
./dirtyfail --exploit-copyfail
```
You will be prompted to type `DIRTYFAIL` and press enter before any
page-cache modification happens. After a successful flip, DIRTYFAIL
invokes `su `; type your **own** password β PAM still
checks against the untouched `/etc/shadow`, then `setuid(0)` lands
at root because the page cache says you are uid 0.
To run the exploit without spawning a shell (useful for CI / repeated
testing):
```sh
./dirtyfail --exploit-copyfail --no-shell
```
This applies the patch, verifies it landed, then evicts
`/etc/passwd` from the page cache via `POSIX_FADV_DONTNEED` so the
system returns to a clean state.
### Cleanup
After any exploit run:
```sh
./dirtyfail --cleanup # evict /etc/passwd from page cache
sudo ./dirtyfail --cleanup # also drop_caches (requires root)
```
Or directly:
```sh
sudo sh -c 'echo 3 > /proc/sys/vm/drop_caches'
```
---
## 7. How DIRTYFAIL detects each CVE
### Copy Fail (active sentinel probe)
Detection actually triggers the primitive against a sentinel file in
`/tmp`:
1. Probe `socket(AF_ALG, SOCK_SEQPACKET, 0)` and `bind` to
`authencesn(hmac(sha256), cbc(aes))`.
2. Create a 4 KiB sentinel file in `/tmp` and fault its first page
into the cache.
3. Run the full exploit primitive against it: `sendmsg` AAD with
`seqno_lo = "PWND"`, splice 32 bytes of the sentinel into the AF_ALG
op socket, drive `recv` to fire the scratch write.
4. Re-read the sentinel and look for `PWND` anywhere in the first
page.
Marker found β vulnerable. Marker absent but page contents differ β
the primitive partially fired (still vulnerable). Page identical β
not vulnerable on this kernel.
### Dirty Frag xfrm-ESP (precondition-based)
Detection is precondition-only β we don't enter a user namespace in
detect mode (it would side-effect networking inside that namespace).
We check:
* kernel version within affected window
* `esp4` / `esp6` currently loaded or autoloadable
* unprivileged user namespace creation succeeds (probed via fork β
child `unshare(CLONE_NEWUSER)`)
All three present β VULNERABLE.
### Dirty Frag RxRPC (precondition-based)
* `rxrpc` in `/proc/modules` or autoloadable
* `socket(AF_RXRPC, SOCK_DGRAM, 0)` succeeds
Either β VULNERABLE.
---
## 8. How DIRTYFAIL exploits each CVE
### Copy Fail exploit (`copyfail.c`)
Single 4-byte STORE through `algif_aead`:
```
[/etc/passwd page cache]
user ββsendmsg(AAD = SPI||"0000")βββΆ AF_ALG op
ββsplice(passwd_fd, 32B)βββββββΆ AF_ALG op (in-place dst SGL)
ββrecv()ββββββββββββββββββββββΆ kernel runs authencesn_decrypt
scratch write: "0000" β uid_off
EBADMSG returned to user (we ignore)
user ββopen(passwd, RDONLY)βreadβββΆ "kara:x:0000:1000:..." ββ page cache
user ββexeclp("su", "kara")βββββββΆ PAM β on /etc/shadow β setuid(0)
ββββββΊ root shell
```
### Dirty Frag xfrm-ESP exploit (`dirtyfrag_esp.c`)
Same end-state as Copy Fail, reached through `xfrm_input` instead of
`algif_aead`:
```
[/etc/passwd page cache]
unshare(USER|NET); setup uid_map; ifup lo
NETLINK_XFRM βNEWSA(seq_hi="0000", encap=ESPINUDP/4500)ββΆ kernel
udp_recv bind 127.0.0.1:4500, UDP_ENCAP_ESPINUDP
udp_send connect 127.0.0.1:4500
vmsplice ESP wire header (24B) ββΆ pipe
splice /etc/passwd@uid_off (16B) ββΆ pipe
splice pipe (40B) ββΆ udp_send
udp loopback ββΆ udp_recv (UDP_ENCAP) ββΆ xfrm_input ββΆ esp_input
skb has frags, no frag_list ββΆ goto skip_cow (THE BUG)
crypto_authenc_esn_decrypt:
scratch_write(seq_hi="0000" β page_addr+uid_off) ββ 4-byte STORE
AEAD auth fails (EBADMSG) β but the STORE is permanent
page-cache copy of /etc/passwd now reports uid 0 for the user
```
Then exit the namespace, `execlp("su", user)` from the parent β same
final step as Copy Fail.
### Dirty Frag RxRPC exploit
Out of scope for DIRTYFAIL. Use `V4bel/dirtyfrag::exp.c`.
---
## 9. Mitigations
### Copy Fail (CVE-2026-31431)
1. **Apply the patch.** Mainline `a664bf3d`; backports landed on the
6.12 / 6.17 / 6.18 stable lines.
2. **Interim**: blacklist `algif_aead`:
```sh
echo 'install algif_aead /bin/false' | sudo tee /etc/modprobe.d/copyfail.conf
sudo rmmod algif_aead 2>/dev/null
```
β Note: this **does not** mitigate Dirty Frag. The xfrm-ESP path
reaches the same authencesn primitive without going through
algif_aead.
### Dirty Frag xfrm-ESP (CVE-2026-43284)
1. **Apply the patch.** Mainline `f4c50a4034e6` (merged 2026-05-07).
Distro backports rolling out as of 2026-05-08.
2. **Interim**: blacklist `esp4` and `esp6`:
```sh
sudo tee /etc/modprobe.d/dirtyfrag-esp.conf /dev/null
sudo sysctl vm.drop_caches=3
```
β This breaks IPsec / strongSwan / libreswan VPNs.
3. **Defense in depth**: disallow unprivileged user namespaces.
Ubuntu does this by default via AppArmor; on other distros:
```sh
sudo sysctl -w kernel.unprivileged_userns_clone=0
```
### Dirty Frag RxRPC (CVE-2026-43500)
1. **No upstream patch yet.** Researcher patch on lkml; not merged at
time of writing (2026-05-08).
2. **Interim**: blacklist `rxrpc`:
```sh
sudo tee /etc/modprobe.d/dirtyfrag-rxrpc.conf /dev/null
sudo sysctl vm.drop_caches=3
```
β This breaks AFS distributed file system clients. Most servers
don't need rxrpc.
### Combined one-liner (all three)
```sh
sudo sh -c '
cat > /etc/modprobe.d/dirtyfail.conf /dev/null
sysctl vm.drop_caches=3
'
```
---
## 10. Ethics & disclosure
DIRTYFAIL is a research tool. The vulnerabilities it covers are
**already publicly disclosed** with weaponized PoCs in the wild
(see [Credits](#11-credits)) β DIRTYFAIL adds detection coverage,
unified documentation, and a gentler PoC variant (UID-flip vs ELF
overwrite of `/usr/bin/su`).
* **Do not run `--exploit-*` modes on systems you do not own or are
not explicitly authorized to test.** Page-cache modifications are
reversible with `drop_caches`, but they are still privilege
escalation while they persist.
* **Do not deploy DIRTYFAIL as a "scanner" against third-party
infrastructure** without written authorization. The detection mode
is non-modifying for system files but does open a sentinel file in
`/tmp` and exercise the kernel crypto API.
* If you find a vulnerable system in the wild, follow responsible
disclosure to the operator, not the public.
---
## 11. Credits
DIRTYFAIL is original code, but the techniques it implements were
developed by the researchers below. Read their primary sources before
deploying this tool β they are the canonical references.
| Source | Researcher | Contribution |
|--------|------------|--------------|
| | Anonymous | Original Copy Fail disclosure |
| | Smarttfoxx | C PoC (shellcode-in-`su` variant) |
| | rootsecdev | Python detector + UID-flip PoC; the ergonomics of DIRTYFAIL's `--exploit-copyfail` mode follow this approach. |
| | Hyunwoo Kim ([@v4bel](https://x.com/v4bel)) | Dirty Frag discovery, full chain PoC, kernel patches |
| | BleepingComputer | Public reporting |
Patch authors:
* `f4c50a4034e6` (Dirty Frag xfrm-ESP) β based on Hyunwoo Kim's v1
patch, with the merged shared-frag approach by Kuan-Ting Chen.
* RxRPC patch β Hyunwoo Kim, pending merge.
---
## License
MIT. See [LICENSE](LICENSE).
---
## Contact
Open an issue on this repository, or reach out at the address listed
in the commit history. For coordinated disclosure of related issues,
contact the upstream researchers above directly.