Abstract
Fraggap (CVE-2026-53362) は、Linux UDPv6 corking パスに存在するバグで、skb_shared_info に対して 15 バイトの OOB 書き込みを可能にします。コードは こちら です。

Vulnerability Summary
CONFIG_IPV6=yが必要です。
- 2 回の
splice()呼び出しが、新規 skb の背後にあるskb_shared_infoの先頭 15 バイトを上書きします。 - この OOB 書き込みにより、ヒープ上の残存パイプアドレスが
frags[0]として扱われ、put_page()が呼び出されてダングリング・パイプページが生成されます。 - そのページは PTE ページとして再利用され、物理メモリの読み書きが可能になります。
core_patternを書き換えて root ヘルパーを実行します。
欠陥のある会計処理はコミット 773ba4fe9104 で導入されました。現在の MSG_SPLICE_PAGES トリガーはコミット ce650a166335 の後に到達可能になりました。IPv6 パスはコミット 736b380e28d0 で修正されました。
Vulnerability Analysis
Overview
UDP corking は複数の送信を 1 つのデータグラムにまとめます。データグラムがフラグメント境界をまたぐ場合、__ip6_append_data() は境界を超えたバイト数を前回の skb からの fraggap としてカウントし、次の skb のリニア領域に移動します。基本的な skb の構造は次のようになります。

fraggap はリニア領域にコピーされるデータです。したがって、新しい skb のリニア割り当ての一部である必要があり、パイプページに残る長さから除外されなければなりません。脆弱なコードはその逆を行います。fraggap をリニアデータ領域から除去し、pagedlen に加えます。
カーネルはデータ領域にスペースを確保せずに fraggap バイトをリニア末尾にコピーします。これにより、末尾の直後にある skb_shared_info を任意の 15 バイトに設定できます。nr_frags バイトのみを 1 に設定すると、範囲外の frags[0] がヒープ上の残存パイプページアドレスを割り当て済みアドレスとして扱います。
Root cause
このバグは、同じバイトがパスによって異なるカウント方法で扱われるために発生します。datalen の構築では fraggap を次の skb が扱うデータとして含めます。一方、ページングブランチでは同じ fraggap を外部データの一部として扱います。
datalen = length + fraggap;
fraglen = datalen + fragheaderlen;
pagedlen = 0;
/* ... */
else {
alloclen = fragheaderlen + transhdrlen;
pagedlen = datalen - transhdrlen;
}
/* ... */
copy = datalen - transhdrlen - fraggap - pagedlen;
if (copy < 0 && !(flags & MSG_SPLICE_PAGES)) {
err = -EINVAL;
goto error;
}
datalen は fraggap を加えますが、alloclen は加えません。pagedlen は datalen から導出されるため、ギャップも含まれます。これにより copy が -fraggap に縮小します。負のコピーチェックは MSG_SPLICE_PAGES が設定されている場合はスキップされます。
その後、コードは fraggap が非リニアデータであることを無視して、実際のリニアコピーを実行します。
data = skb_put(skb, fraglen - pagedlen);
data += fragheaderlen;
if (fraggap) {
skb->csum = skb_copy_and_csum_bits(
skb_prev, maxfraglen,
data + transhdrlen, fraggap);
/* ... */
}
最終的に、ページングブランチはギャップの内容を収まるスペースを確保せずにコピーします。
Triggering the 15-byte OOB
目標は、最初の skb がフラグメント境界よりちょうど 15 バイト大きくなるようにすることです。次の skb のリニア末尾が skb_shared_info の先頭に位置するようになります。
- 320 バイトのルーティングヘッダーと MTU 1287 を持つ UDPv6 ループバックソケット
- 最初の skb をフラグメント境界より 15 バイト大きくする 919 バイトのパイプペイロード
- 同じ cork キューに追加される 2 番目の 20 バイトのパイプペイロード
パイプからソケットへの splice() により splice_to_socket() が bvec イテレータを構築し、MSG_SPLICE_PAGES を設定します。SPLICE_F_MORE は MSG_MORE になります。UDP_CORK と組み合わせることで、最初の skb を送信せずに write キューに保持します。
msg.msg_flags = MSG_SPLICE_PAGES;
if (flags & SPLICE_F_MORE)
msg.msg_flags |= MSG_MORE;
/* ... */
iov_iter_bvec(&msg.msg_iter, ITER_SOURCE, bvec, bc,
len - remain);
ret = sock_sendmsg(sock, &msg);
呼び出しチェーンは udpv6_sendmsg() → ip6_append_data() → __ip6_append_data() → skb_splice_from_iter() です。プレーン UDP が使用されるため udpv6_sendmsg() は ip_generic_getfrag を選択します。__ip6_append_data() はページスプライスパスを維持し、paged = true を設定します。これにより、脆弱なパスに到達し、ペイロードを skb フラグメントとしてアタッチします。
実際のシーケンスは以下の通りです。
- 最初の
splice(919, SPLICE_F_MORE)は 8 バイトの UDP ヘッダーを加えてlength = 927を追加します。320 バイトのルーティングヘッダーによりfragheaderlenは 360 になります。最初の skb の長さはちょうど 1287 になります。 UDP_CORKにより skb はキューに保持されます。計算されたフラグメント境界は 1272 であるため、skb 末尾の最後の 15 バイトは次のフラグメントに移動する必要があります。- 2 番目の
splice(20, SPLICE_F_MORE)は同じキュー内の末尾 skb を見つけます。最初のcopyは 0 で、新しい長さ 20 より小さいため、境界に対してcopy = -15として再計算されます。この値がalloc_new_skbに入り、fraggap = 15で新しい skb が作成されます。

必要な計算は以下の通りです。
fragheaderlen = 40 + 320 = 360
maxfraglen = ((1287 - 360) & ~7) + 360 - 8 = 1272
first skb len = 360 + 8 + 919 = 1287
fraggap = 1287 - 1272 = 15
second append: length=20, transhdrlen=0
datalen=35, alloclen=360, pagedlen=35, copy=-15
16 バイトのループバックヘッドルームと 8 バイトのフラグメントヘッダーリザーブを加えると、alloc_skb() リクエストは 384 になります。lts-6.12.85 では、このリクエストは 704 バイトの skbuff_small_head オブジェクトを使用します。最初の 384 バイトが skb データ領域で、最後の 320 バイトが skb_shared_info です。
skb_put(360) は末尾を skb->end に正確に移動し、続く data += fragheaderlen は同じアドレスを指します。したがって、前の skb の最後の 15 バイトは skb_shared_info + 0x00 から +0x0e にコピーされます。これにより、skb_shared_info に任意の 15 バイトを書き込むことができます。

Primitive to Exploit
これで skb_shared_info の先頭に 15 バイトを書き込めるようになりました。最初の skb は境界の前に 1272 - 360 - 8 = 904 バイトのペイロードを保持しています。したがって、ソースペイロードの [904..918] が宛先の [+0x00..+0x0e] にマッピングされます。
nr_frags は skb_shared_info + 0x02 にあります。したがって、エクスプロイトはソースバイト 906 のみを 1 に設定し、残りを 0 のままにします。

nr_frags はオフセット 0x02 にあり、frags[0] は 0x30 にあります。全体の skb_shared_info は 320 バイトです。完全な pahole 出力は Appendix にあります。
15 バイトしか制御できないため、フラグ以上のフィールドはほとんど制御できません。したがって、有用なフィールドは nr_frags のみです。実際のページ記述子を保持する frags[0] は OOB 範囲外にあります。どうすればこれを解決できるでしょうか?
以下の問題を解決することで root を取得できます。
Exploit Details
Exploit Summary
- 未初期化変数の再利用 → 有効な
skb_frag_tを保持していたオブジェクトを解放し、脆弱な skb として再割り当てして未初期化のfrags[0]を再利用する - Fraggap OOB →
nr_fragsを 0 から 1 に反転させ、frags[0]を割り当て済みとして扱わせる - パイプの追加 → 2 番目のパイプページをスロット 1 にアタッチし、
nr_frags == 2とする - 一致しないページ解放 → デストラクタが
frags[0]に対してput_page()を実行する - Dirty Pagetable → ダングリングページを PTE ページとして再割り当てする
- 物理書き込み → PTE を変更して
core_patternを見つけ、上書きして root ヘルパーを実行する
Background of the exploit
なぜ古い frags[] が生き残るのか?
新しい skb ヘッドを割り当てても、skb_shared_info のすべてがクリアされるわけではありません。__finalize_skb_around() は dataref までしかゼロクリアしません。
shinfo = skb_shinfo(skb);
memset(shinfo, 0, offsetof(struct skb_shared_info, dataref));
atomic_set(&shinfo->dataref, 1);
frags[] は dataref の後にあるため、前の割り当てからの記述子バイトが生き残る可能性があります。しかし nr_frags は 0 にリセットされるため、古い記述子は通常到達不能です。Fraggap OOB で nr_frags を 1 に設定すると、カーネルはそのページを割り当て済みとして扱います。
Dirty Pagetable
ページテーブルはページアロケータからの 4 KiB ページです。ダングリングパイプページを PTE ページとオーバーラップさせると、パイプ書き込みで PTE を破損できます。PTE の PFN のみを変更することで、希望する物理ページをユーザースペースにマッピングできます。
Planting a stale frags[0]
現時点では制御可能なフィールドは nr_frags のみで、必要な frags[0] は OOB 範囲外にあります。したがって、バグをトリガーする前に、エクスプロイトは同じキャッシュ内に有効な記述子を構築し、それらのバイトが残っているオブジェクトを再利用します。
まず、ソースパイプ wp に 256 バイトのマーカーを書き込みます。次に tee() を 8 つの保持パイプに対して実行し、ページ参照カウントを 9 に引き上げます。これらの追加参照により、グルーミングおよびトリガー中にソースページが生き続けます。
記述子は単一の corked データグラム内で構築されます。すべてのページングペイロードは tee() を介して同じソースページから来ます。したがって、各ターゲットヘッドは有効な記述子と frags[0] 内の有効なページ参照を取得します。

トリガーソケットと 2 つのパイプペイロードは、グルームソケットが閉じられる前に準備されます。次に、2 つの splice が他の割り当てを挟まずに実行されます。最初のトリガー skb は 392 リクエストを持つため kmalloc-1k を使用し、small-head フリーリストには触れません。2 番目の 384 リクエストのみが、解放されたばかりのターゲットの 1 つを再利用します。
再利用されたオブジェクトは frags[0] にソースページ記述子バイトを保持しますが、nr_frags == 0 の間は無害です。次のステージでは OOB を使用してこの古いスロットを活性化します。
From nr_frags to a dangling page
Fraggap OOB が nr_frags = 1 に設定すると、デストラクタは frags[0] を実際のフラグメントとして扱います。2 番目の splice が新しい skb をキューに入れます。次のループ反復で、残りの 20 有効バイトが同じ skb にアタッチされます。
skb_append_pagefrags() は新しい記述子のインデックスとして現在の nr_frags を使用します。
int i = skb_shinfo(skb)->nr_frags;
if (skb_can_coalesce(skb, i, page, offset)) {
skb_frag_size_add(&skb_shinfo(skb)->frags[i - 1], size);
} else if (i < max_frags) {
get_page(page);
skb_fill_page_desc_noacc(skb, i, page, offset, size);
}
インデックスはすでに 1 です。2 番目のトリガーパイプページはソース wp ページとは異なるため、frags[0] と結合せず、frags[1] に入ります。このパスは通常の get_page() を実行し、nr_frags を 2 に引き上げます。
ソケットを閉じると cork キューがフラッシュされ、skb デストラクタが両方のスロットを解放します。
for (i = 0; i < shinfo->nr_frags; i++)
__skb_frag_unref(&shinfo->frags[i], skb->pp_recycle);
したがって frags[0] に対する put_page() が実行され、refcount が減少します。最後にパイプを閉じると、ダングリングページが残ります。

Reclaiming as a PTE
アロケータに戻ったが wp->pipe_buffer からまだ指されている 4 KiB ページが存在します。mmap() を呼び出し、PTE ページを作成するためにタッチすると、解放されたページが再利用されます。
これで、ユーザースペースから任意の物理ページを読み書きできるようになりました。
Finding and overwriting core_pattern
PTE 制御による物理的な読み書きにより、core_pattern を検索します。
見つけたら、値を以下のように変更します。子プロセスが SIGSEGV を発生させると、カーネルは以下のコマンドを root として実行します。
|/proc/%P/fd/666 %P
Flag

Appendix
Verified skb_shared_info layout
struct skb_shared_info {
__u8 flags; /* 0 0x1 */
__u8 meta_len; /* 0x1 0x1 */
__u8 nr_frags; /* 0x2 0x1 */
__u8 tx_flags; /* 0x3 0x1 */
short unsigned int gso_size; /* 0x4 0x2 */
short unsigned int gso_segs; /* 0x6 0x2 */
struct sk_buff * frag_list; /* 0x8 0x8 */
union {
struct skb_shared_hwtstamps hwtstamps; /* 0x10 0x8 */
struct xsk_tx_metadata_compl xsk_meta; /* 0x10 0x8 */
}; /* 0x10 0x8 */
unsigned int gso_type; /* 0x18 0x4 */
u32 tskey; /* 0x1c 0x4 */
atomic_t dataref; /* 0x20 0x4 */
unsigned int xdp_frags_size; /* 0x24 0x4 */
void * destructor_arg; /* 0x28 0x8 */
skb_frag_t frags[17]; /* 0x30 0x110 */
/* size: 320, cachelines: 5, members: 14 */
};
Mitigation
The patch
パッチは上記の不変条件を復元します。リニア領域に実際にコピーされる fraggap を追加し、非リニア側で同じ値を減算します。
- alloclen = fragheaderlen + transhdrlen;
- pagedlen = datalen - transhdrlen;
+ alloclen = fragheaderlen + transhdrlen + fraggap;
+ pagedlen = datalen - transhdrlen - fraggap;
IPv6 修正では、負の copy チェックから MSG_SPLICE_PAGES 例外も削除されます。IPv4 パスにも同じ会計バグがあり、コミット eca856950f7c で修正されました。
Partial mitigations
splice() を制限することは機能しますが、良い選択肢ではありません。
Timeline
| Date | Event |
|---|---|
| 2022-07-12 | バグはコミット 773ba4fe9104 で導入されました。 |
| 2023-08-02 | 現在の MSG_SPLICE_PAGES 破損はコミット ce650a166335 の後に可能になりました。 |
| 2026-05-15 | 脆弱性を [email protected] に非公開で報告しました。 |
| 2026-06-21 | IPv6 修正 736b380e28d0 が netdev net ツリーに取り込まれました。 |
| 2026-06-25 | 修正が Linux mainline にマージされました。 |
| 2026-07-04 | Linux kernel CNA が CVE-2026-53362 を公開しました。 |
| 2026-07-14 | Openwall linux-distros リストに通知しました。 |
| 2026-07-16 | Linux kernel CNA が CVE-2026-53366 を公開しました。 |
| 2026-07-20 | writeup とエクスプロイトを公開しました。 |
Acknowledgements
Fraggap は IPv4/IPv6 で @physicube と @qwerty によって発見され、kernelCTF で使用されました。IPv4 バグが悪用可能であることを確認してくれた Sultan Alsawaf (team CIQ) に感謝します。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.