CVE-2026-31431 — Copy Fail: algif_aead authencesn 4-Byte Page-Cache Write → Linux LPE

Last updated: 2026-04-30 Component: Linux kernel — crypto/algif_aead.c (AF_ALG userspace crypto interface) + the authencesn AEAD template (crypto/authencesn.c) Bug Class: Logic flaw — in-place scatterlist optimisation lets splice()-delivered page-cache pages enter the writable destination scatterlist of an AEAD operation; the authencesn template then performs a deterministic 4-byte write past the legitimate output boundary into shared file-backed memory. Patch: Mainline commit a664bf3d603d reverts the 2017 in-place optimisation. Fixed in 6.18.22, 6.19.12, and 7.0. Exploited ITW: No public ITW attribution at disclosure. Universal across distros — verified on Ubuntu 24.04, Amazon Linux 2023, RHEL 10.1, SUSE 16. Discoverers: Taeyang Lee + Brian Pak (Theori / Xint Code) — discovery assisted by Theori’s Xint Code AI scanner; coordinated disclosure with the Linux kernel security team. Related: Race Conditions, Use-After-Free, Integer Overflows Tags: linux, crypto, algif_aead, authencesn, splice, page-cache, lpe, setuid


Summary

A nine-year-old logic flaw in the Linux kernel’s userspace crypto interface (AF_ALG) gives any local user a deterministic 4-byte write into the page cache of any file the user can read() — including read-only setuid binaries owned by root. The exploit pins the modified page in the cache, executes the (now-corrupted-on-disk-image-but-not-on-disk) setuid binary, and inherits root.

It needs no race window, no info leak, no kernel-offset knowledge, no heap massage. The published Python PoC is 732 bytes using only os, socket, and zlib from the standard library, and works unchanged across every major distribution shipping a kernel from 2017 onward.


Root cause

Two separate, individually defensible kernel design decisions interact catastrophically.

1. The 2017 algif_aead in-place optimisation

algif_aead.c exposes the kernel’s AEAD primitives to userspace via an AF_ALG socket. To avoid copying, an optimisation introduced in 2017 lets the kernel build the AEAD operation’s destination scatterlist from the same pages as the source — the pages delivered via splice() from a file descriptor.

The dangerous consequence:

“The pages are not duplicated; the scatterlist entries point at the same physical pages that back every read(), mmap(), and execve() of that file.”

Splicing /usr/bin/su (or any other readable setuid binary) into the AEAD operation thus puts page-cache pages of a root-owned executable into a kernel scatterlist marked writable.

2. authencesn writes past the legitimate output boundary

authencesn is the AEAD wrapper used for IPsec Extended Sequence Number support. Unlike GCM, CCM, or plain authenc, the authencesn template uses memory at offset assoclen + cryptlen as a scratch pad for a 4-byte seqno_lo value during decryption — and never restores it, even when HMAC verification fails.

“The algorithm is using memory it does not own as a scratch pad.”

In any normal user-supplied buffer, this scratch write is harmless. Combined with the algif_aead optimisation above, the scratch write lands directly in a page-cache page of the spliced file.

The two together = an attacker-chosen 4-byte value written to an attacker-chosen offset inside a root-owned file’s cached pages.


The 4-byte primitive

To weaponise:

  1. Open an AF_ALG socket bound to authencesn(hmac(sha256),cbc(aes)).
  2. splice() the target file (e.g. /usr/bin/su) into the socket.
  3. Construct AAD where bytes 4–7 encode the desired 4-byte payload (those bytes become the seqno_lo written by authencesn).
  4. Trigger decryption with recv(). HMAC verification fails (the ciphertext is fabricated) — but the 4-byte scratch write to assoclen + cryptlen already landed in the page-cache page. The error is silent: recv() returns -EBADMSG and the corrupted page stays cached.

The offset is fully attacker-controlled (via assoclen and cryptlen). The value is fully attacker-controlled (via AAD bytes). The write is deterministic — no race, no retry, no timing window.


Exploitation: 732-byte Python script to root

The published PoC chains the primitive into a setuid escalation:

  1. Pick a root-owned setuid binary the attacker can read() (/usr/bin/su, /usr/bin/passwd, /usr/bin/mount, etc.).
  2. Choose a 4-byte patch site inside the binary’s .text. The PoC overwrites a conditional branch in su’s authentication path with a JMP-equivalent that skips authentication and lands directly on the privileged code path.
  3. Use the AF_ALG + splice + authencesn primitive to write the 4-byte patch into the page-cache copy of the binary.
  4. execve() the now-corrupted binary. The kernel page-cache pages back the executable mapping, so the .text page at that offset is the attacker-modified one. setuid stays intact (the inode metadata is untouched). The attacker gets a root shell.

The on-disk file is unchanged — the corruption lives only in cached memory. Reboot or echo 3 > /proc/sys/vm/drop_caches (root-only) clears it. From userspace there is no way for a non-root observer to notice.

The PoC’s portability is the striking part. It targets the algorithmic flaw, not kernel offsets — so the same 732-byte script works across Ubuntu, RHEL, Amazon Linux, SUSE, Debian, Arch.


Why it survived 9 years

  • The algif_aead in-place optimisation was a performance patch reviewed against contemporary AEAD templates (GCM, CCM, authenc). None of those have the post-output scratch write that authencesn does. The bug is interaction between two pieces of code maintained by different sub-teams.
  • authencesn is rare — it exists for IPsec ESN and is otherwise unused. Most fuzzers exercising AEAD APIs concentrate on GCM/ChaCha20-Poly1305.
  • Page-cache writes are silent. There is no oops, no detectable corruption, no audit trail — the write is to a legitimate kernel data structure (the scatterlist), and the kernel never notices the page is read-only-from-userland.

Theori’s Xint Code AI scanner found it after Taeyang Lee identified AF_ALG + splice as a page-cache exposure vector and pointed the scanner at the crypto subsystem. Roughly an hour of automated analysis surfaced Copy Fail as the highest-severity finding.


Detection / mitigations

LayerAction
KernelApply 6.18.22 / 6.19.12 / 7.0 or distro-backport of commit a664bf3d603d.
Pre-patchBlacklist algif_aead via /etc/modprobe.d/blacklist-algif_aead.conf (breaks any userspace consumer of the AF_ALG AEAD interface — usually no one).
Pre-patch / hardenedBlock socket(AF_ALG, ...) via seccomp for untrusted workloads. The kernel-level alternative is CONFIG_CRYPTO_USER_API_AEAD=n.
DetectionAudit socket(AF_ALG) + splice() + recv() = -EBADMSG sequences from non-root processes. The PoC is conspicuous because nothing legitimate uses authencesn from userspace.

Timeline

  • 2026-03-23 — Reported to the Linux kernel security team.
  • 2026-04-01 — Patch a664bf3d603d committed to mainline; backports begin.
  • 2026-04-29 — Coordinated public disclosure (copy.fail, Xint blog, kernel.org advisory).

References