daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

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)