
A report on Dirty Frag, which is a Linux Local Privilege Escalation (LPE) vulnerability chain that allows an unprivileged user to gain root access
Exploit Reproduction and Detection Lab for a Linux kernel local privilege escalation chain.
Status: VERIFIED. I completed the reproduction, the fileless verification, and the syscall-level detection in the lab (kernel 6.18.9+kali-amd64). This document is a lab log. Every claim below was observed during the reproduction run. The screenshots and artifacts are real captures from the VM.
Dirty Frag combines two deterministic logic bugs in the Linux kernel. These bugs allow an unprivileged local user to overwrite the page cache of read-only files (for example, /usr/bin/su) and obtain a root shell:
Both variants use the same root pattern as Dirty Pipe and Copy Fail. The splice(2) syscall places a reference to a page-cache page of a file into the frag slot of a sender-side sk_buff. The attacker can only read this file. Receive-side kernel code then performs an in-place crypto STORE on top of that frag. This mutates the page cache in RAM. No disk write occurs, so file integrity monitoring (AIDE, Tripwire) cannot see it. The attack is deterministic. It has no race window and no kernel panic on failure.
cac2661c53f3 (2017‑01) to f4c50a4034e6 (patched 2026‑05‑05)2dc334f1a63a (2023‑06) to aa54b1d27fe0 (patched 2026‑05‑10)CVE-2026-43284 = 8.8 (High), CVE-2026-43500 = 7.8 (High)Dirty Frag is a fileless LPE. It corrupts the in-memory page cache, not the file on disk. Traditional file integrity monitoring cannot see it. Detection must happen at the syscall layer. The chain uses these syscall primitives: socket(AF_ALG)/socket(AF_RXRPC), splice, and unshare(CLONE_NEWUSER|CLONE_NEWNET). The ESP path also creates AF_INET UDP and netlink sockets. This layer is the focus of the detection engineering in this repo.
Both variants use the same sink: in-place crypto that STOREs bytes onto a page-cache page the attacker places with splice(2).
UDP_ENCAP_ESPINUDP.vmsplice, then 16 bytes from /usr/bin/su at the target file offset with splice.splice pushes the pipe into the send socket. splice_to_socket() sets MSG_SPLICE_PAGES. This places the page-cache page of /usr/bin/su directly into skb->frags[0].xfrm4_udp_encap_rcv, then xfrm_input, then esp_input(). The vulnerable skip_cow branch () bypasses . It performs with the page-cache page as both source and destination.The attacker controls both the location (splice offset) and the value (4 bytes). Authentication verification runs after the store, so the crypto layer never flags the write. This variant requires CAP_NET_ADMIN and uses unshare(CLONE_NEWUSER|CLONE_NEWNET).
rxkad_verify_packet_1() performs a single-block pcbc(fcrypt) decrypt directly on the splice-pinned skb frag. It does not copy the data first. The attacker picks a session key (add_key("rxrpc", …)) so that decrypt(ciphertext) equals desired_plaintext. This produces an 8-byte STORE. This variant targets /etc/passwd. It needs no user namespace. It requires the rxrpc.ko module (loaded by default on Ubuntu).
The public PoC targets /usr/bin/su. It writes 48 ESP stores of 4 bytes each (192 bytes at file offset 0). It replaces the first page-cache bytes with a static root-shell ELF. The ELF entry point runs setgid(0); setuid(0); setgroups(0,NULL); execve("/bin/sh", …). A single execve("/usr/bin/su") then yields a root shell.
The ESP patch (mainline f4c50a4034e6) marks page frags that arrive through splice() with the SKBFL_SHARED_FRAG flag. The skip_cow branch in esp_input() now also checks this flag. Shared-frag skbs go through skb_cow_data() before the in-place AEAD decryption.
The RxRPC patch (mainline aa54b1d27fe0) adds an skb->data_len check next to the existing skb_cloned() check. The kernel copies a non-linear skb with paged data before the in-place pcbc(fcrypt) decryption.
Screenshot of the VirtualBox lab setup:

.
├── README.md # this lab log
├── detection/
│ ├── dirtyfrag.rules # auditd syscall‑level detection rules
│ ├── ausearch_dirtyfrag_observed.txt # real exploit detection output
│ ├── sigma/
│ │ └── dirty_frag_exploit.yml # Sigma rule for SIEM detection
│ └── yara/
│ └── dirty_frag_exploit.yar # YARA rule for PoC code on disk/memory
├── mitigation/
│ └── dirtyfrag_mitigation.sh # module blacklist + page cache flush
├── poc/
│ └── check_vulnerable.py # non‑destructive pre‑flight checker
├── reports/
│ └── incident-dirtyfrag.md # incident response playbook
└── screenshots/ # real captures from the lab VM
poc/check_vulnerable.py and confirm kernel/modules/usernstestuser/usr/bin/su hashesdrop_caches or reboot)detection/dirtyfrag.rules and validate auditd alertscat /etc/os-release | head -3
uname -r
Screenshot of kernel version verification:

The kernel must be older than the May 2026 fixes (f4c50a4034e6 / aa54b1d27fe0). If you ran apt upgrade after that date, the exploit will fail. See Troubleshooting.
A safe checker reports whether the VM is a plausible target. It checks the running kernel, the presence of the esp4/esp6/rxrpc modules, and whether unprivileged user namespaces are available. The ESP variant requires these namespaces.
python3 poc/check_vulnerable.py
Expected verdict: [*] potentially vulnerable -- proceed in a disposable VM only.
An unprivileged testuser simulates an attacker without special rights.
sudo useradd -m testuser
sudo passwd testuser
su - testuser
id
Screenshot: id shows UID 1001. This confirms non-root access.

From the unprivileged account:
git clone https://github.com/V4bel/dirtyfrag.git
cd dirtyfrag
gcc -O0 -Wall -o exp exp.c -lutil
./exp
On success, the exploit patches the page cache of /usr/bin/su. It writes 48 ESP stores of 4 bytes each (a 192-byte root-shell ELF at file offset 0). It then drops an interactive root shell with forkpty.
Screenshot of module availability, source review, and clean compile:

id
whoami
Screenshot: id shows uid=0(root) after ./exp.

sha256sum reads through the page cache. While the exploit's write is active, the hash is different from the original. After a flush, the hash returns to the original. This before/after pair proves that the on-disk binary was never touched.
sha256sum /usr/bin/su # 1) while page cache is contaminated -> DIFFERENT hash
Screenshot: the hash is different from the package hash (RAM poisoned, disk intact).

Observed corrupted (page-cache) hash: 3fc29078bd77150b5d6fbb368f632bf02c1a3704816f03fb694ec2e81e77bde4
After exploitation, the page cache contains the corrupted data. Always flush it:
echo 3 | sudo tee /proc/sys/vm/drop_caches
# or reboot the VM
Then verify the restoration (as testuser):
sha256sum /usr/bin/su # now matches the ORIGINAL package hash
su - # prompts for a password again — no auto-root
Screenshot: restored (original on-disk) hash.

Observed original (on-disk) hash: 2b4f8770bd35bba5cdc5cfe292bc1d988e92ec1786bf91cf83e0e86fac056eb6
After a reboot, dpkg -V util-linux returned no output. The on-disk /usr/bin/su exactly matches the package, so the corrupted 3fc29078… hash existed only in the page cache.
drop_cachesmay not evict the poisoned page. This happens if a running process still pins the page. In that case,sha256sumanddpkg -Vkeep showing the corrupted content. The reliable fix is a reboot. Note thatdpkg -Valso reads through the page cache. While the page is poisoned, it reports??5??????(the MD5 is different; size, mode, owner, and mtime all match). It turns silent again after reboot. That proves the on-disk file was never modified.
Flushing does not disable the exploit. It only clears the poisoned page cache. You must disable the modules separately (see Mitigation). Removing already-loaded modules on an exploited host requires a reboot.
Dirty Frag is invisible to file integrity monitoring. Detection focuses on the syscall primitives that the chain must use.
detection/dirtyfrag.rules)# /etc/audit/rules.d/dirtyfrag.rules
-a always,exit -F arch=b64 -S socket -F a0=38 -F uid!=0 -k dirtyfrag_af_alg
-a always,exit -F arch=b64 -S socket -F a0=33 -F uid!=0 -k dirtyfrag_rxrpc
-a always,exit -F arch=b64 -S splice -F uid!=0 -k dirtyfrag_splice
-a always,exit -F arch=b64 -S unshare -F uid!=0 -k dirtyfrag_namespace
-w /usr/bin/su -p r -k dirtyfrag_suid_read
Note:
AF_ALG = 38andAF_RXRPC = 33on Linux (see/usr/include/bits/socket.h). Earlier drafts useda0=21. That value is incorrect. AF_RXRPC is 33, not 21.
Deploy and verify:
sudo cp detection/dirtyfrag.rules /etc/audit/rules.d/
sudo systemctl restart auditd # or: sudo auditctl -D && sudo auditctl -R /etc/audit/rules.d/dirtyfrag.rules
sudo auditctl -l
Expected alerts after you re-run the exploit (correlate by PID):
ausearch -k dirtyfrag_af_alg
ausearch -k dirtyfrag_rxrpc
ausearch -k dirtyfrag_splice
ausearch -k dirtyfrag_namespace
All five rules loaded (auditctl -R, res=1 for each CONFIG_CHANGE). The rules caught the exploit's namespace setup:
time->Wed Aug 5 08:26:37 2026
type=PROCTITLE msg=audit(1785932797.328:592): proctitle="./exp"
type=SYSCALL msg=audit(1785932797.328:592): arch=c000003e syscall=272 success=yes exit=0 a0=50000000 a1=0 a2=0 a3=0 items=0 ppid=5198 pid=5199 auid=1000 uid=1001 gid=1001 euid=1001 suid=1001 fsuid=1001 egid=1001 sgid=1001 fsgid=1001 tty=pts2 ses=2 comm="exp" exe="/home/testuser/dirtyfrag/exp" subj=unconfined key="dirtyfrag_namespace"
Decoded: syscall=272 (unshare), a0=50000000 equals CLONE_NEWUSER | CLONE_NEWNET, uid=1001 (unprivileged testuser), comm="exp". The full log is in detection/ausearch_dirtyfrag_observed.txt.
Screenshot: ausearch output shows the rule load and the exploit event.

The full playbook is in reports/incident-dirtyfrag.md: executive summary, timeline, IoCs, MITRE ATT&CK mapping, containment, eradication, recovery, and lessons learned.
Immediate runtime mitigation. It does not survive a reboot for already-loaded modules (see the note below):
sudo mitigation/dirtyfrag_mitigation.sh
What it does:
/etc/modprobe.d/dirtyfrag.conf to block esp4, esp6, and rxrpc. It includes the blacklist and alias … off lines. A simple one-liner misses these lines (autoload-by-alias still worked otherwise).Impact: disabling these modules makes IPsec VPNs (ESP) and AFS filesystem (RxRPC) functionality stop working.
Permanent fix: upgrade to a kernel that contains the upstream patches. Or blacklist the modules at boot (initcall_blacklist=esp4,esp6,rxrpc).
This repository is for authorized defensive security research and training only.
See LICENSE. This lab is for educational use in your own isolated environment only. Do not run the PoC on systems you are not authorized to test.
| Variant | CVE | Sink | Trigger path | Needs unprivileged userns |
|---|
| xfrm‑ESP Page‑Cache Write | CVE‑2026‑43284 | crypto_authenc_esn_decrypt() in esp_input() | socket(AF_INET) with UDP‑encap, then xfrm_input() | Yes (CAP_NET_ADMIN) |
| RxRPC Page‑Cache Write | CVE‑2026‑43500 | rxkad_verify_packet_1() (pcbc(fcrypt)) | socket(AF_RXRPC) | No |
!skb_cloned() && !skb_has_frag_list()skb_cow_data()crypto_authenc_esn_decrypt() emits a STORE of the high-order 32 bits of the ESN. That value is replay_esn->seq_hi. The attacker chooses this value at SA registration with the XFRMA_REPLAY_ESN_VAL netlink attribute.| Component | Details |
|---|
| Hypervisor | VirtualBox |
| Target VM | Kali Linux 2026.1 (snapshot restored to a vulnerable state) |
| Kernel | 6.18.9+kali‑amd64 (older than the May 2026 fixes) |
| Exploit PoC | V4bel/dirtyfrag (single C file) |
| Detection | auditd (rules in detection/dirtyfrag.rules) |
| Layer | What it sees | Status |
|---|
| auditd | AF_ALG and AF_RXRPC sockets, splice, unshare, SUID reads | Yes. Deployed and validated (rule file detection/dirtyfrag.rules) |
| Sigma | Syscall patterns for SIEM | Yes. Rule ready (detection/sigma/dirty_frag_exploit.yml) |
| YARA | PoC code on disk or in memory | Yes. Rule ready (detection/yara/dirty_frag_exploit.yar) |
| FIM (AIDE/Tripwire) | File changes | No. Blind, because no disk write occurs |
| Symptom | Likely cause | Fix |
|---|
./exp prints failed / post-write verify failed | Kernel is patched (from May 2026 or later) | Boot an older kernel or restore a pre-update snapshot. Re-check with poc/check_vulnerable.py |
unshare(CLONE_NEWUSER) returns EPERM | Unprivileged user namespaces disabled (AppArmor or sysctl) | Check sysctl kernel.unprivileged_userns_clone. On Ubuntu, check AppArmor. Kali allows it by default |
esp4/esp6/rxrpc not loaded | Modules not available | sudo modprobe esp4 esp6 rxrpc (RxRPC auto-loads with socket(AF_RXRPC)) |
su binary "patched" but no root shell | drop_caches already ran, or page cache pinned by processes | Re-run the exploit. If stuck, reboot the VM |
| Exploit ran, then still root-able after mitigation | Page cache not flushed, or modules already loaded before blacklist | echo 3 > /proc/sys/vm/drop_caches. Full stop requires reboot |