CVE-2026-31431 — Linux AF_ALG AEAD Page-Cache Write to Local Root¶
Published 2026-06-06
CISA Known Exploited Vulnerability
CVE-2026-31431 ("Copy Fail") was added to the CISA Known Exploited Vulnerabilities catalog on 2026-05-01 with a remediation due date of 2026-05-15. Treat this as actively exploited.
| Field | Detail |
|---|---|
| Project | Linux kernel |
| Affected component | crypto/algif_aead.c — AF_ALG AEAD socket interface |
| Severity | HIGH |
| CVSS | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H (7.8) |
| CWE | CWE-669 |
| Affected versions | 4.14 – 5.10.253 · 5.11 – 5.15.203 · 5.16 – 6.1.169 · 6.2 – 6.6.136 · 6.7 – 6.12.84 · 6.13 – 6.18.21 · 6.19 – 6.19.11 |
| Fixed version | 5.10.254 · 5.15.204 · 6.1.170 · 6.6.137 · 6.12.85 · 6.18.22 · 6.19.12 |
| Advisory | GHSA-2274-3hgr-wxv6 |
1. Vulnerability Overview¶
The Linux kernel exposes cryptographic operations to userspace through the AF_ALG socket family. One of its interfaces, algif_aead, handles Authenticated Encryption with Associated Data (AEAD) ciphers and is enabled by default on every major Linux distribution since around 2012.
CVE-2026-31431, nicknamed "Copy Fail," allows any local unprivileged user to gain root. By combining the AF_ALG AEAD interface with the splice() system call and the authencesn AEAD template, an attacker obtains a reliable 4-byte write primitive against any file's in-memory page cache, without touching the on-disk copy. Applied iteratively, that primitive is sufficient to overwrite security-sensitive files such as /usr/bin/su or /etc/passwd in memory and obtain a real root shell. No ASLR bypass, kernel symbol leak, or race condition is required; the write is fully deterministic.
The flaw was introduced by commit 72548b093ee3 ("crypto: algif_aead - copy AAD from src to dst") on 2017-07-30 and went undetected for roughly nine years until Theori disclosed it publicly in 2026.
Root Cause¶
The bug is in _aead_recvmsg() in crypto/algif_aead.c. A 2017 performance optimization made algif_aead perform decryption in-place to avoid memory copies. To do this, it built a combined scatterlist: the user's receive buffer (containing AAD and ciphertext) was chained to the page-cache pages from the source TX scatter-gather list that held the authentication tag. This chaining used af_alg_pull_tsgl(sk, processed, areq->tsgl, processed - as), where the dst_offset argument (processed - as) caused only the tag-bearing pages to be reassigned to the destination, meaning original page-cache pages became part of the decryption output scatterlist.
When the authencesn AEAD template processes a decryption request, it uses dst[assoclen + cryptlen] (the 4 bytes immediately past the ciphertext) as scratch space to rearrange Extended Sequence Number (ESN) bytes. It writes seqno_lo (bytes 4–7 of the AAD, fully attacker-controlled) at that offset. Because of the in-place page-cache chaining, that offset resolves into one of the chained page-cache pages. HMAC verification then fails, deliberately, but the page-cache write has already happened. The kernel's writeback machinery never marks the corrupted page dirty, so it is never flushed to disk. Any subsequent read() or exec() of the file returns the attacker's bytes from the in-memory cache rather than the on-disk data.
Patch commit a664bf3d603d, authored by Herbert Xu on 2026-03-26, reverts the in-place optimization entirely. It removes the dst_offset parameter from af_alg_count_tsgl and af_alg_pull_tsgl and rewrites _aead_recvmsg to always operate out-of-place: TX SGL pages are transferred to a per-request buffer, the AAD is copied with memcpy_sglist, and the AEAD request is issued with TX SGL as source and RX SGL as destination. Page-cache pages are never chained into the writable output.
--- a/crypto/algif_aead.c
+++ b/crypto/algif_aead.c
@@ -152,23 +152,24 @@ static int _aead_recvmsg(...)
+ /* Always operate out-of-place */
processed = used + ctx->aead_assoclen;
- /* [old: searched for first valid TX SGL page, then branched on enc/dec
- for decryption: af_alg_pull_tsgl(sk, processed, areq->tsgl, processed - as)
- chained page-cache tag pages into RX SGL destination] */
+ areq->tsgl_entries = af_alg_count_tsgl(sk, processed);
+ areq->tsgl = sock_kmalloc(sk, ...);
+ af_alg_pull_tsgl(sk, processed, areq->tsgl); /* no dst_offset */
+ tsgl_src = areq->tsgl;
+ memcpy_sglist(rsgl_src, tsgl_src, ctx->aead_assoclen); /* copy AAD */
- aead_request_set_crypt(..., rsgl_src, /* src = RX buf */
+ aead_request_set_crypt(..., tsgl_src, /* src = TX buf (copy) */
areq->first_rsgl.sgl.sgt.sgl, used, ctx->iv);
--- a/crypto/af_alg.c
+++ b/crypto/af_alg.c
-unsigned int af_alg_count_tsgl(struct sock *sk, size_t bytes, size_t offset)
+unsigned int af_alg_count_tsgl(struct sock *sk, size_t bytes)
/* offset parameter removed — no more selective starting position */
-void af_alg_pull_tsgl(struct sock *sk, size_t used, struct scatterlist *dst,
- size_t dst_offset)
+void af_alg_pull_tsgl(struct sock *sk, size_t used, struct scatterlist *dst)
/* dst_offset removed — all pages always reassigned from offset 0 */
2. Vulnerable Environment¶
The environment is an AWS EC2 instance running Ubuntu 24.04 LTS, booting AMI ami-0d17372de612983fe (build 20260321) in ap-southeast-1. The stock kernel on that AMI is 6.17.0-1009-aws, which falls within the affected range for the 6.17.x series (fixed at 6.18.22). The instance type is t3.small (Nitro, 2 vCPU burstable). The trigger is deterministic, so burstable compute is adequate.
Infrastructure is defined as code using OpenTofu. The key files are available for download alongside this page:
- env.zip — full environment bundle (IaC + cloud-init)
- env/tofu/main.tf — EC2 instance, security group, key pair
- env/tofu/variables.tf — configurable inputs
- env/tofu/outputs.tf — public IP, instance ID, key path
- env/tofu/scripts/cloud-init.sh — kernel gate and baseline staging
On first boot, the cloud-init script apt-mark holds all kernel packages and masks unattended-upgrades so the kernel cannot be silently patched. It then runs a gate that parses uname -r, computes the per-series fixed stable tag, and writes the readiness marker /root/.lab-ready only when the running kernel is confirmed within the affected range. It also binds an AF_ALG authencesn(hmac(sha256),cbc(aes)) socket to verify the transport is usable. On a patched kernel or the wrong AMI, the gate fails and the marker is never written.
A single unprivileged user lowpriv (uid 1001, no sudo, no capabilities) is created. The VM also stages a clean root-owned baseline:
/usr/bin/su— intact setuid-root binary (sha256c74311fe5636b7d7f9a56239fa8adeeab12ba86fe7d41b91afa85bf9bbdae78b)/etc/passwd— withlowprivat UID 1001:lowpriv:x:1001:1001::/home/lowpriv:/bin/bash/var/lib/lab-marker— a root-only-writable directory for the post-exploit side-effect observation
Baseline metadata is recorded in /root/lab-baseline.json (readable only via the privileged OOB channel).
Verifying the Environment is Vulnerable¶
SSH into the VM as ubuntu and check the readiness marker and kernel version through the privileged channel:
ssh -i env/tofu/lab_key.pem -o StrictHostKeyChecking=accept-new ubuntu@13.229.212.201 \
'uname -r; \
sudo -n test -f /root/.lab-ready && echo LAB-READY || echo NOT-READY; \
sudo -n cat /root/.lab-ready; \
sudo -n cat /root/lab-baseline.json'
Observed output (2026-06-03):
6.17.0-1009-aws
LAB-READY
2026-06-03T01:59:41Z
{ "cve": "CVE-2026-31431", "kernel": "6.17.0-1009-aws", "kernel_version": "6.17.0",
"fixed_tag_for_series": "6.18.22", "vulnerable": true,
"unprivileged_actor": "lowpriv", "lowpriv_uid": 1001,
"lowpriv_passwd_line": "lowpriv:x:1001:1001::/home/lowpriv:/bin/bash",
"setuid_target": "/usr/bin/su",
"setuid_target_sha256_baseline": "c743...ae78b",
"marker_dir": "/var/lib/lab-marker", "marker_dir_owner": "root" }
Note
/root is mode 0700. The readiness marker must be read through the privileged OOB channel (sudo -n ...). A bare cat /root/.lab-ready as the ubuntu user (without sudo) returns Permission denied and would incorrectly indicate the environment is not ready. Always use sudo -n. If the output shows NOT-READY, inspect /var/log/lab-init.log for a SUBSTRATE_FAILED: line; if present, the environment substrate is inadequate and the run must not proceed.
3. How to Exploit¶
The exploit uses the page-cache write primitive to corrupt the lowpriv user's UID and GID fields in /etc/passwd in memory. Once the page cache reflects lowpriv:x:0:0:, su lowpriv reads UID 0 from the cached page and spawns a real root shell, which then drops a root-owned marker file that proves the escalation. The on-disk copy of /etc/passwd is never touched.
The full exploit bundle is available for download: exploit.zip
Individual scripts:
- exploit/run.sh — orchestrating entry point
- exploit/escalate.sh — runs on the VM as
lowpriv; drives the AF_ALG primitive and spawns the root shell - exploit/pagecache_write.py — implements the 4-byte page-cache write primitive via AF_ALG +
splice() - exploit/passwd_patch.py — locates the
lowprivUID/GID fields within/etc/passwdand callspagecache_write.pyfor each 4-byte chunk
Step 1 — Confirm the clean baseline¶
Before running the exploit, verify that the VM is at a clean baseline: lowpriv is UID 1001, the marker directory is empty, and no previous run has dirtied the page cache.
ssh -i env/tofu/lab_key.pem -o StrictHostKeyChecking=accept-new ubuntu@13.229.212.201 \
'uname -r; sudo -n test -f /root/.lab-ready && echo LAB-READY || echo NOT-READY; \
sudo -n id lowpriv; sudo -n ls -la /var/lib/lab-marker/'
Expected output: 6.17.0-1009-aws, LAB-READY, lowpriv reported as uid=1001(lowpriv), and an empty /var/lib/lab-marker.
If the environment is dirty from a prior run, reboot to clear the page cache before proceeding:
ssh -i env/tofu/lab_key.pem ubuntu@13.229.212.201 \
'sudo -n rm -f /var/lib/lab-marker/pwned_by_lowpriv; sudo -n reboot'
Step 2 — Run the exploit as the unprivileged actor¶
run.sh stages all scripts onto the VM, installs them owned by lowpriv, and drives the full escalation chain:
bash exploit/run.sh env/tofu/lab_key.pem 13.229.212.201 ubuntu lowpriv /var/lib/lab-marker pwned_by_lowpriv
| Argument | Value | Meaning |
|---|---|---|
| key | env/tofu/lab_key.pem |
SSH private key (run-dir relative) |
| host | 13.229.212.201 |
VM public IP |
| ssh_user | ubuntu |
SSH login user (passwordless sudo) |
| actor | lowpriv |
Unprivileged actor (uid 1001) the exploit runs as |
| marker_dir | /var/lib/lab-marker |
Root-only directory for the post-exploit side effect |
| marker_name | pwned_by_lowpriv |
Marker file created by the escalated root shell |
Script execution order:
- Copies
pagecache_write.py,passwd_patch.py, andescalate.shto the VM and installs them owned bylowpriv. - Runs
escalate.shaslowpriv. This script callspasswd_patch.py, which locates thelowprivline in/etc/passwd, identifies the byte offsets of the1001UID and GID fields, and callspagecache_write.pytwice: once for the UID field and once for the GID field. Each call opens/etc/passwdread-only, creates an AF_ALGauthencesn(hmac(sha256),cbc(aes))socket,splice()s the relevant page-cache pages into its TX scatter-gather list, and callsrecv(). Theauthencesntemplate writesseqno_lo(the 4 attacker-controlled AAD bytes) into the chained page-cache page.recv()returnsEBADMSGdue to the deliberate HMAC failure; the write has already landed. - With the in-memory
/etc/passwdnow readinglowpriv:x:0:0::/home/lowpriv:/bin/bash,escalate.shrunssu lowpriv. Thesubinary reads the cached UID 0, skips authentication (caller appears as root), and spawns a shell with real UID 0. That shell creates the marker file in/var/lib/lab-marker, a directorylowprivcannot write to under its real identity.
Step 3 — Confirm the exploit succeeded through the independent root channel¶
The proof of escalation is read through the privileged OOB channel (the ubuntu user's passwordless sudo over SSH), which is entirely separate from the exploit process and its output. Reading from this independent channel establishes that the effect was caused by the exploit and not by any legitimate root action:
ssh -i env/tofu/lab_key.pem -o StrictHostKeyChecking=accept-new ubuntu@13.229.212.201 \
'sudo -n stat -c "owner=%U(%u) group=%G(%g) mode=%a name=%n" /var/lib/lab-marker/pwned_by_lowpriv; \
sudo -n cat /var/lib/lab-marker/pwned_by_lowpriv; \
sudo -n id lowpriv; sudo -n getent passwd lowpriv; \
sudo -n -u lowpriv -- id'
Observed output after the exploit:
Primary observable — root-owned side effect in a root-only directory:
owner=root(0) group=root(0) mode=644 name=/var/lib/lab-marker/pwned_by_lowpriv
created-by-unprivileged-actor-via-CVE-2026-31431
The file exists, is owned by UID 0, and lives in a directory that lowpriv cannot write to. Creating it is a UID-0 action that an unprivileged process cannot perform on a patched kernel.
Equivalent observable — the principal is now treated as UID 0:
uid=0(root) gid=0(root) groups=0(root) ← id lowpriv (root OOB channel)
lowpriv:x:0:0::/home/lowpriv:/bin/bash ← getent passwd lowpriv (page-cache read)
uid=0(root) gid=0(root) ← sudo -u lowpriv -- id (independent spawn)
Step 4 — Confirm the corruption is page-cache-only¶
A raw-disk read via O_DIRECT (bypassing the page cache) shows the on-disk /etc/passwd is untouched. This is the definitive signature of the CVE-2026-31431 page-cache write primitive and rules out any legitimate write path:
sudo -n grep "^lowpriv" /etc/passwd # page-cache read
sudo -n python3 /tmp/rawread.py # O_DIRECT raw-disk read
# page-cache read:
lowpriv:x:0000:0000::/home/lowpriv:/bin/bash ← CORRUPTED (in-memory only)
# raw-disk read:
lowpriv:x:1001:1001::/home/lowpriv:/bin/bash ← CLEAN (on-disk untouched)
On a patched kernel the page-cache write never occurs: lowpriv remains UID 1001, no marker is created, and recv() returns EINVAL rather than EBADMSG.
Teardown¶
Once finished, destroy the EC2 instance and all associated resources:
This removes the instance (i-0e8daa1d3611847b9), security group, ephemeral key pair, and the local lab_key.pem.
4. Security Advice¶
Remediation¶
The only complete fix is upgrading to a kernel version that includes patch commit a664bf3d603d. These stable releases carry the fix:
| Stable series | Fixed at |
|---|---|
| 5.10.x | 5.10.254 |
| 5.15.x | 5.15.204 |
| 6.1.x | 6.1.170 |
| 6.6.x | 6.6.137 |
| 6.12.x | 6.12.85 |
| 6.18.x | 6.18.22 |
| 6.19.x | 6.19.12 |
Vendor-specific fixed releases follow their own cadence; consult your distribution's security advisory. Red Hat has published a vendor advisory for RHEL. CISA requires remediation by 2026-05-15 for systems covered by Binding Operational Directive 22-01.
Mitigations and Workarounds¶
If an immediate kernel upgrade is not feasible, disabling the algif_aead kernel module eliminates the vulnerable code path:
This prevents the AF_ALG AEAD socket from being bound. Applications that rely on AF_ALG AEAD will be affected; in practice this is uncommon, as most userspace crypto goes through OpenSSL or a similar library rather than the kernel socket interface. Test before deploying.
A finer-grained alternative is restricting unprivileged access to AF_ALG sockets via a kernel lockdown policy or a seccomp filter that blocks AF_ALG socket creation, for cases where modprobe -r is not an option.
References¶
- GHSA-2274-3hgr-wxv6 — primary advisory, version ranges, CWE, CVSS
- NVD CVE-2026-31431 — CPE configuration version ranges, reference list
- Patch commit a664bf3d603d — primary fix commit
- Buggy commit 72548b093ee3 — 2017 in-place optimization that introduced the flaw
- Theori PoC repository — official Python exploit
- copy.fail — official Theori disclosure site
- Xint blog: copy-fail-linux-distributions — multi-distro technical writeup and exploitation chain details
- WebSec blog — independent technical analysis and
/etc/passwdstrategy - Microsoft Security Blog — cloud impact assessment
- Red Hat Security Advisory — vendor advisory and fixed RHEL versions
- CISA KEV Catalog — added 2026-05-01, remediation due 2026-05-15
- OSS-security disclosure thread (Apr 29) — public disclosure and community analysis