
authencesn の論理的な欠陥が AF_ALG と splice() を介して連鎖し、システム上の任意の読み取り可能なファイルのページキャッシュへの制御された 4 バイト書き込みに到達します。レースコンディションもオフセットも、コンパイル済みペイロードも必要ありません。同じ 732 バイトのスクリプトで、2017 年以降のすべての Linux ディストリビューションで root を取得できます。
CVE-2026-31431 - Copy Fail は、Linux カーネルの authencesn 暗号テンプレートにおける論理的な欠陥です。これにより、特権のないローカルユーザーが、ディスク上のファイルを変更することなく、システム上の任意の読み取り可能なファイルのページキャッシュに対して制御された 4 バイト書き込みを実行できます。
このバグは、3つのコンポーネントのいずれにも単体では存在しません。それらの相互作用から発生します。``` 2011 ────────────────────────────────────────────────────────────────────── - authencesn added to the kernel (a5079d084f8b). - Uses the caller's destination scatterlist as scratch space. - Reorder ESN bytes before HMAC computation. - Only caller: internal xfrm layer. Harmless.
2015 ────────────────────────────────────────────────────────────────────── - algif_aead.c gains AEAD support with splice() path (104880a6b470). - splice() can deliver page cache pages to the TX scatterlist. - AF_ALG uses out-of-place operation: req->src != req->dst. - Page cache pages remain read-only. Not exploitable.
2017 ────────────────────────────────────────────────────────────────────── - In-place optimization in algif_aead.c (72548b093ee3). - Copies AAD+CT to RX buffer but chains authentication tag pages via sg_chain(). - Sets req->src = req->dst. - Page cache pages now reside in WRITABLE dst. - authencesn writes past boundary → page cache corruption.
2026 ────────────────────────────────────────────────────────────────────── - Copy Fail - CVE-2026-31431. Discovered by Theori / Xint Code. - Exploitable across all distros since 2017.
---
---
---
<div id='root-cause'/>
## ***🧬 根本原因分析***
<div id='primitive'/>
### ***AF_ALG + splice() プリミティブ***
AF_ALG (*[AF_ALG = 38](https://docs.kernel.org/crypto/userspace-if.html#user-space-api-general-remarks)*) は、カーネルの暗号APIを非特権ユーザー空間に公開するソケットタイプです。非特権プロセスは以下の操作が可能です:
1. AF_ALG / SOCK_SEQPACKET ソケットを開く。
2. カーネル暗号APIが公開する任意の利用可能な AEAD テンプレートに bind() する。
3. 設定したアルゴリズムに対して setsockopt(SOL_ALG, ALG_SET_KEY, ...) で暗号鍵を設定する。
4. accept() を呼び出して、暗号化・復号リクエストを処理する専用の操作ソケットを取得する。
5. sendmsg() で細工したデータを送信し、recvmsg() で処理結果を受け取ることで、カーネルの暗号サブシステムと完全にやり取りする。
これは、主要な全ディストリビューションのカーネル設定でデフォルトで有効化されています (CONFIG_CRYPTO_USER_API_AEAD=y)。
**[splice(2)](https://man7.org/linux/man-pages/man2/splice.2.html)** は、コピーを行わずにファイルディスクリプタ間でデータを転送します。ページへの参照を渡すのであって、コピーを渡すわけではありません。関連するフローは以下の通りです:```
open("/usr/bin/su") -> fd_file
pipe() -> pipe_rd, pipe_wr
# moves N bytes from the file into the pipe
# the pipe buffer now contains a reference to the same physical page in the page cache
splice(fd_file, pipe_wr, N)
# delivers that reference to the AF_ALG socket
# the TX scatterlist of algif_aead now points to the page cache page of /usr/bin/su
splice(pipe_rd, alg_fd, N)
AF_ALG ソケットの TX scatterlist は、ファイルのあらゆる read()、mmap()、execve() の際にカーネルが使用するのと同じ物理ページへの直接参照を含みます。コピーは一切行われません。
コミット 72548b093ee3、algif_aead.c。復号の場合、実装は次のとおりです。
In-place operation: RX SGL (req->dst): [ user buffer: AAD (copy) || CT (copy) ] --sg_chain--> [ Tag (page cache pages) ] req->src = req->dst = RX SGL
Result: page cache pages from /usr/bin/su are now part of the WRITABLE scatterlist passed to the crypto algorithm.
<div id='authencesn'/>
### ***authencesn における領域外書き込み***
authencesn は、拡張シーケンス番号(RFC 4303)を伴う IPsec で使用されるカーネル AEAD ラッパーです。IPsec は 64 ビットのシーケンス番号を使用します:
- seqno_hi - 上位 32 ビット(AAD のバイト 0〜3)
- seqno_lo - 下位 32 ビット(AAD のバイト 4〜7)
ワイヤ上で送信されるのは seqno_lo のみであり、seqno_hi は暗黙的なコンテキストです。HMAC 計算では、authencesn はこれらのバイトを並べ替える必要があります:ハッシュ入力の先頭に seqno_hi、末尾に seqno_lo を配置します。
この並べ替えは、呼び出し元の宛先 scatterlist をスクラッチ領域として使用して実行されます:```c
/* crypto/authencesn.c - crypto_authenc_esn_decrypt() */
// [1] Read bytes 0-7 of the AAD from dst
scatterwalk_map_and_copy(tmp, dst, 0, 8, 0);
// [2] Overwrite dst[4..7] with seqno_hi (temporary modification for HMAC)
scatterwalk_map_and_copy(tmp, dst, 4, 4, 1);
// [3] *** THE BUG ***
// Writes seqno_lo at dst[assoclen + cryptlen]
// This offset is AFTER the authentication tag - outside the legitimate AEAD output region.
// authencesn uses this position as scratch space and NEVER restores the original bytes.
scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1);
Call [3] は、dst[assoclen + cryptlen] に 4 バイトを書き込みます。AEAD API の復号における出力契約は、AAD || plaintext、つまり正確に assoclen + (cryptlen - authsize) バイトです。assoclen + cryptlen は認証タグの後方に位置します。authencesn は、自分が所有しないメモリに書き込みを行います。
crypto_authenc_esn_decrypt_tail() は seqno_lo を読み戻して正しい AAD を再構築しますが、dst[assoclen + cryptlen] の元のバイトを復元することはありません。HMAC チェックが成功するか失敗するかに関係なく、この上書きは永続的です。
カーネル内の他の標準 AEAD アルゴリズムは、このような動作をしません。GCM、CCM、標準 authenc は、正当な出力領域にのみ書き込みを厳密に限定しています。