技術 約8分で読めます

FragnesiaはDirty Frag対策後も残ったLinuxページキャッシュ権限昇格

いけさん目次

TL;DR

影響 LinuxカーネルのXFRM ESP-in-TCPまわり。CVE-2026-46300、CVSS 7.8 HIGH、通常ユーザーからrootへ上がるローカル権限昇格

対応 ディストリビューションの修正済みカーネルへ更新。上流修正は skb_try_coalesce()SKBFL_SHARED_FRAG を引き継ぐ2行パッチ

暫定 Dirty Fragと同じ esp4esp6rxrpc のロード禁止。IPsec ESPを使うVPNゲートウェイでは通信影響の確認が先


Fragnesiaは、2026年5月13日にV12 SecurityのWilliam Bowlingが公開したLinuxカーネルのローカル権限昇格だ。
CVE番号はCVE-2026-46300。
The Hacker Newsは5月14日に、Dirty Fragの新しい変種としてこの件を取り上げた。

先月のCopy FailAF_ALGalgif_aead が入口だった。
そのあとに出たDirty Fragは、ESPとRxRPCの受信経路でページキャッシュを書き換えた。
FragnesiaはDirty Fragの再告知ではない。
同じESP/XFRM周辺に残っていた別のバグで、すでに公開PoCがある。

ディスク上の /usr/bin/su を書き換えず、RAM上のページキャッシュだけを汚すところは同じだ。
ファイルハッシュは正常に見えるが、execve() は改変済みのキャッシュを読む。
ローカルユーザーやコンテナ内プロセスがホストrootへ上がる経路になるので、共有サーバー、CIランナー、Kubernetesノードでは単なる「ローカルバグ」より扱いが重い。

消えたのはフラグだけだった

今回の中心は skb_try_coalesce() だ。
Linuxネットワークスタックの sk_buff は、パケット本体を線形バッファだけでなくページ断片として持てる。
splice() でファイル由来のページをTCP受信キューへ入れたあと、ソケットが espintcp ULPモードへ切り替わると、そのページがESPの暗号処理に渡る。

ページキャッシュ由来のfragには、本来「共有された外部ページなのでそのまま書くな」という印が必要になる。
その印が SKBFL_SHARED_FRAG
netdevに投稿された修正パッチによると、skb_try_coalesce() がページ断片を別のskbへ移すとき、このフラグを引き継いでいなかった。

ESP入力側は skb_has_shared_frag() を見て、コピーオンライトへ落とすか、インプレース復号してよいかを決める。
フラグが消えると、ページキャッシュ由来のページが普通のskb断片に見える。
その結果、ESPがAES-GCMの復号をページキャッシュ上で直接走らせる。

flowchart TD
    A["読み取り可能なファイル"] --> B["spliceでTCP受信キューへ"]
    B --> C["skb_try_coalesceで断片を移す"]
    C --> D["SKBFL_SHARED_FRAGが消える"]
    D --> E["espintcp ULPでESP処理"]
    E --> F["コピーオンライトせず<br/>ページキャッシュへXOR"]
    F --> G["/usr/bin/suのメモリ上コピーを<br/>小さなELFスタブへ変更"]

V12の解説では、AES-GCMのキーストリームを使い、対象バイトを1回のトリガーで1バイトずつ望む値へ寄せる。
256通りのキーストリームバイトとノンスの対応表を作り、/usr/bin/su の先頭192バイトに setresuid(0,0,0)/bin/sh 実行用の小さなELFスタブを載せる。
ページキャッシュの改変なので、ディスク上のバイナリは変わらない。

PoC実行後は、同じページキャッシュが残っている間、su 実行時に注入済みスタブが動く。
V12は検証後に drop_caches か再起動で掃除するよう書いている。
本番で「脆弱かどうか」をPoCで試すと、自分でsetuidバイナリのキャッシュを汚すことになる。

Dirty Fragと同じ暫定策で止まる

FragnesiaはDirty Fragとは別バグだが、入口は同じESP/XFRM面にある。
V12、CloudLinux、UbuntuはいずれもDirty Fragと同じ緩和策を案内している。
esp4esp6rxrpc をロードできないようにして、必要なら既にロード済みのモジュールを外す。

sudo sh -c "printf 'install esp4 /bin/false\ninstall esp6 /bin/false\ninstall rxrpc /bin/false\n' > /etc/modprobe.d/dirtyfrag.conf"
sudo rmmod esp4 esp6 rxrpc 2>/dev/null || true

すでにDirty Frag対策としてこの設定を入れているなら、Fragnesia向けに別ファイルを増やす必要はない。
CloudLinuxも、同じ緩和を適用済みの顧客は修正済みカーネルまで追加作業なし、と書いている。

副作用もDirty Fragと同じだ。
esp4esp6 はIPsec ESPのカーネル側処理なので、strongSwanやLibreswanでIPsec VPNを終端しているホストでは通信が壊れる。
このブログでもIKEv2(strongSwan)サーバー構築メモを書いているが、あの種の構成では緩和策を入れる前にトンネルの方式と影響範囲を確認する。
一般的なWebサーバーやCIランナーなら使っていないことも多い。

AWSのアドバイザリは少し違う判断を出している。
Amazon LinuxはPoCが使うロード可能モジュールの espintcp を提供していないため影響なし、と説明した。
それでも同種のネットワークプロトコル実装を硬くする目的で、中核ネットワークコードへ正しさを補うパッチを入れるとしている。
ここは「Linuxなら全部同じ」ではなく、ディストリビューションがどのモジュールを出しているかで判定が変わる部分だ。

パッチ待ちの間に確認するもの

修正パッチは net/core/skbuff.c で2行を足す内容だ。
skb_try_coalesce() が断片を移したとき、転送元の SKBFL_SHARED_FRAG を転送先へ引き継ぐ。
これでESP入力側が共有断片を見落とさず、インプレース復号ではなくコピーオンライトへ落ちる。

netdevに投稿されたパッチの中身はこれだけだ。

// net/core/skbuff.c の skb_try_coalesce()
        skb_shinfo(to)->nr_frags = to_shinfo->nr_frags + from_shinfo->nr_frags;
+       if (from_shinfo->flags & SKBFL_SHARED_FRAG)
+               to_shinfo->flags |= SKBFL_SHARED_FRAG;
        if (skb_cloned(to))
                return false;

splice() 由来のページがマージ先のskbへ移っても、共有フラグが付き直す。
ESP入力経路の skb_has_shared_frag() が正しく真を返し、復号は新規確保したバッファに対して行われる。
ロジックの修正というより、フラグの引き継ぎ漏れを埋める性質のパッチだ。

AlmaLinuxは2026年5月13日時点で、Red Hatの更新を待たずにテスト用リポジトリへ修正済みカーネルを出した。
UbuntuのCVEページは同日時点で各リリースをNeeds evaluationとしており、Dirty Fragと同じ緩和策を参照している。
CloudLinuxは対象ストリームごとのカーネル更新とKernelCareライブパッチをビルド・テスト中とした。

uname -r だけでは安全判定にならない。
各ディストリビューションは上流のバージョン番号を上げずに修正をバックポートする。
見るのは、使っているカーネルパッケージのアドバイザリ、CVE-2026-46300の修正済みバージョン、esp4esp6rxrpcespintcp のロード状態だ。

uname -r
lsmod | grep -E '^(esp4|esp6|rxrpc|espintcp)\b'
modprobe -n -v esp4
modprobe -n -v esp6
modprobe -n -v rxrpc

Dirty Fragのときにも書いたが、ページキャッシュ汚染は再起動で消える一方、rootを取られた後の永続化は別の問題として残る。
疑わしいホストでは、PoC実行で再現確認するより、カーネル更新、モジュール制限、ユーザー名前空間の設定、CIやコンテナの実行履歴を先に見る。

マルチテナント環境では「1ノードに1個」では足りない

コンテナはカーネルをホストと共有する。
Fragnesiaのようにカーネルが入口の脆弱性は、コンテナ内の通常ユーザーがホスト側ページキャッシュへ書き込めるかどうかで影響範囲が決まる。
共有ノードに乗っている全コンテナが、同じ脆弱性で同じrootへ向かう。

特に注意がいる構成。

  • userns無効のKubernetesノード: User namespacesがオフだと、コンテナのUID 0がホストのUID 0と同じ意味になる。PoC(悪用コード)が取ったrootはそのままノード全体のrootだ
  • マルチテナントCIランナー: PRごとに任意コードを実行する構成(GitHub Actionsのself-hosted runner、GitLabのshared runner等)は、攻撃者が普通にユーザーとして入ってくる
  • 共有開発サーバー: 開発者ごとに非rootアカウントを切っている学内・社内サーバー
  • eBPF・PaaSノード: 顧客コードを近距離で動かす構成

ノードレベルの確認では、esp4esp6rxrpc の遮断が全ノードに行き渡っているかを見る。
KubernetesならDaemonSet/etc/modprobe.d/ を配るか、Ansibleやイメージ側で固める。
1ノードに入れて満足すると、別ノードで同じCIジョブが動いたときに通る。

# ノード横断で確認する例
for node in $(kubectl get nodes -o name); do
  kubectl debug $node -it --image=busybox -- sh -c \
    'lsmod | grep -E "^(esp4|esp6|rxrpc|espintcp)\b"; cat /etc/modprobe.d/*.conf 2>/dev/null | grep -E "esp4|esp6|rxrpc"'
done

PoCはローカルユーザー権限から動く以上、攻撃面はSSH越しの侵入よりコンテナ・CIの方が現実的だ。
カーネル更新までの間は、ノード入口とジョブ入口の両方でアカウントとモジュール状態を確認する。

参考