copy-fail-af-alg-splice-page-cache-overwrite-cve-2026-31431.md (8621B)
1 --- 2 title: "Copy Fail: AFALG + splice page-cache overwrite (CVE-2026-31431)" 3 section: "Linux" 4 sectionSlug: "linux-hardening" 5 sourcePath: "src/linux-hardening/main-system-information/kernel-lpe-cves/copy-fail-af_alg-splice-page-cache-overwrite-cve-2026-31431.md" 6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/linux-hardening/main-system-information/kernel-lpe-cves/copy-fail-af_alg-splice-page-cache-overwrite-cve-2026-31431.md" 7 sha: "188de82beb54e70956b2952367a0af91d26758b8" 8 isIndex: false 9 modified: true 10 license: "CC-BY-NC-4.0" 11 --- 12 13 # Copy Fail: AF_ALG + splice page-cache overwrite (CVE-2026-31431) 14 15 This page documents **Copy Fail**: a Linux kernel local privilege escalation where **`AF_ALG` + `splice()`** turns **readable file page-cache pages** into part of a **writable AEAD destination scatterlist**, and `authencesn` then performs a **deterministic 4-byte write past the contractual output boundary**.<sup>[[1]](#references)</sup> 16 17 - Affected component: `crypto/algif_aead.c` in-place decrypt path + `crypto/authencesn.c`.<sup>[[1]](#references)</sup> 18 - Primitive: controlled **4-byte page-cache write** into any file readable by the attacker.<sup>[[1]](#references)</sup> 19 - Reachability: unprivileged local user, `AF_ALG` available, `algif_aead` loaded.<sup>[[1]](#references)</sup> 20 - Impact: immediate system-wide corruption of the page-cache copy used by `read()`, `mmap()`, and `execve()`.<sup>[[1]](#references)</sup> 21 22 This is closer to **Dirty Pipe / Dirty COW style page-cache abuse** than to a classic memory-corruption race:<sup>[[1]](#references)</sup> 23 24 - no race window.<sup>[[1]](#references)</sup> 25 - no repeated retries.<sup>[[1]](#references)</sup> 26 - no on-disk file modification.<sup>[[1]](#references)</sup> 27 - same exploit flow across many distros because the primitive is structural, not offset-dependent.<sup>[[1]](#references)</sup> 28 29 ## Core idea 30 31 `splice()` moves data between a file, a pipe, and another FD **by reference**. If a readable file is spliced into a pipe and then into an `AF_ALG` AEAD socket, the crypto input scatterlist can reference the **same page-cache pages** backing that file.<sup>[[1]](#references)</sup> 32 33 For AEAD decrypt, `algif_aead` historically optimized the request into an **in-place** layout:<sup>[[1]](#references)</sup> 34 35 - **AAD** and **ciphertext** were copied into the user RX buffer 36 - the final **authentication tag** was **not copied** 37 - instead, tag scatterlist entries were appended to the destination with `sg_chain()` 38 - `req->src = req->dst`, so those appended tag pages became part of a **writable destination chain** 39 40 If the tag pages come from spliced file data, the writable destination chain now includes **page-cache pages of a read-only file**.<sup>[[1]](#references)</sup> 41 42 ## The bug in `authencesn` 43 44 `authencesn` is an AEAD wrapper used for IPsec Extended Sequence Numbers (ESN). During decrypt it uses the destination scatterlist as scratch space and writes **4 bytes past the legitimate decrypt output**:<sup>[[1]](#references)</sup> 45 46 ```c 47 scatterwalk_map_and_copy(tmp, dst, 0, 8, 0); 48 scatterwalk_map_and_copy(tmp, dst, 4, 4, 1); 49 scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1); 50 ``` 51 52 The last write stores **`seqno_lo`** (attacker-controlled AAD bytes `4..7`) at `dst[assoclen + cryptlen]`, which is **after the tag** and therefore **outside the contract for AEAD decrypt output**.<sup>[[1]](#references)</sup> 53 54 When `algif_aead` has chained page-cache-backed tag pages into `dst`, that write crosses out of the RX buffer and lands in the victim file's page cache.<sup>[[1]](#references)</sup> 55 56 ## Why this becomes a useful primitive 57 58 The attacker controls:<sup>[[1]](#references)</sup> 59 60 - **Which file**: any file readable by the attacker 61 - **Which offset**: via splice offset/length and AEAD `assoclen` 62 - **Which value**: the 4 bytes written come from attacker-controlled AAD bytes `4..7` 63 64 Even if authentication fails and `recvmsg()` returns an error, the **page-cache overwrite persists** because the scratch write already happened.<sup>[[1]](#references)</sup> 65 66 The corrupted page is **not marked dirty for writeback**, so:<sup>[[1]](#references)</sup> 67 68 - the on-disk file remains unchanged 69 - checksum comparisons on disk miss the attack 70 - all later `read()`, `mmap()`, and `execve()` users consume the modified in-memory page 71 72 ## Typical LPE path 73 74 The public write-up targets a **setuid-root binary** such as `/usr/bin/su`:<sup>[[1]](#references)</sup> 75 76 1. Open `AF_ALG` and bind to `authencesn(hmac(sha256),cbc(aes))` 77 2. Send AAD where bytes `4..7` contain the 4-byte chunk to write 78 3. `splice()` target file data into the AEAD input so the final tag region references the target file's page-cache pages 79 4. Trigger `recv()` / `recvmsg()` to force decrypt 80 5. Repeat until the page-cache copy of the setuid binary is patched 81 6. Execute the binary so the kernel loads the modified cached image and runs attacker code as root 82 83 Conceptual PoC skeleton:<sup>[[1]](#references)</sup> 84 85 ```python 86 a = socket.socket(38, 5, 0) # AF_ALG, SOCK_SEQPACKET 87 a.bind(("aead", "authencesn(hmac(sha256),cbc(aes))")) 88 # set key, accept request socket 89 u.sendmsg([b"A"*4 + payload_chunk], [cmsg_headers], MSG_MORE) 90 os.splice(target_fd, pipe_wr, offset) 91 os.splice(pipe_rd, alg_fd, offset) 92 u.recv(...) # triggers decrypt -> page-cache write 93 ``` 94 95 ## How the bug became exploitable 96 97 - **2011**: `authencesn` introduced for IPsec ESN handling (`a5079d084f8b`).<sup>[[1]](#references)[[6]](#references)</sup> 98 - **2015**: `authencesn` converted to the new AEAD interface and kept the out-of-contract scratch write (`104880a6b470`).<sup>[[1]](#references)[[5]](#references)</sup> 99 - **2017**: `algif_aead` switched decrypt to an in-place design and chained tag pages into the destination (`72548b093ee3`).<sup>[[1]](#references)[[4]](#references)</sup> 100 101 That 2017 change is what turned an internal scratch write into a **page-cache write primitive** reachable from unprivileged userspace.<sup>[[1]](#references)[[4]](#references)</sup> 102 103 ## Fix and mitigations 104 105 Mainline fixed this by reverting `algif_aead` back to **out-of-place** operation (`a664bf3d603d`), so page-cache pages can remain in the source scatterlist but no longer become part of the writable destination chain.<sup>[[1]](#references)[[3]](#references)</sup> 106 107 Useful mitigations:<sup>[[2]](#references)</sup> 108 109 - patch to a kernel carrying `a664bf3d603d` or a distro backport 110 - block `AF_ALG` socket creation with seccomp for untrusted workloads 111 - disable `algif_aead` if you need an immediate stopgap 112 113 Example emergency mitigation:<sup>[[2]](#references)</sup> 114 115 ```bash 116 echo "install algif_aead /bin/false" > /etc/modprobe.d/disable-algif-aead.conf 117 rmmod algif_aead 2>/dev/null || true 118 ``` 119 120 For containerized environments, `AF_ALG` should be treated as a **kernel attack surface**. Even without this specific CVE, it is a good candidate for seccomp denial in CI runners, sandboxes, and multi-tenant containers.<sup>[[2]](#references)</sup> 121 122 ## Detection / review notes 123 124 - A page-cache-only patch means the suspicious effect may be visible only in memory, not on disk.<sup>[[1]](#references)</sup> 125 - Look for unusual `AF_ALG` use on systems that do not intentionally expose kernel crypto sockets to workloads.<sup>[[2]](#references)</sup> 126 - When auditing zero-copy kernel interfaces, treat any path that combines **`splice()`-backed page references** with **scatterlists reused as destinations** as high risk.<sup>[[1]](#references)</sup> 127 - A useful reviewer rule is: if an algorithm writes beyond its documented output length, any caller that chains foreign pages into `dst` may turn it into a write primitive.<sup>[[1]](#references)</sup> 128 129 ## References 130 131 - [1] [Xint write-up: Copy Fail: 732 Bytes to Root on Every Major Linux Distributions](https://xint.io/blog/copy-fail-linux-distributions) 132 - [2] [Copy Fail (CVE-2026-31431) advisory and mitigation page](https://copy.fail/) 133 - [3] [Linux fix: `crypto: algif_aead - Revert to operating out-of-place` (`a664bf3d603d`)](https://github.com/torvalds/linux/commit/a664bf3d603dc3bdcf9ae47cc21e0daec706d7a5) 134 - [4] [Linux commit: `crypto: algif_aead - copy AAD from src to dst` (`72548b093ee3`)](https://github.com/torvalds/linux/commit/72548b093ee3) 135 - [5] [Linux commit: `crypto: authencesn - Convert to new AEAD interface` (`104880a6b470`)](https://github.com/torvalds/linux/commit/104880a6b470) 136 - [6] [Linux commit: `crypto: authencesn - Add algorithm to handle IPsec extended sequence numbers` (`a5079d084f8b`)](https://github.com/torvalds/linux/commit/a5079d084f8b)