摘要
Fraggap(CVE-2026-53362)是 Linux UDPv6 corking 路徑中的一個漏洞,允許對 skb_shared_info 進行 15 位元組的越界寫入。程式碼在此處。

漏洞摘要
需要
CONFIG_IPV6=y。
- 兩次
splice()呼叫覆寫新 skb 後面的skb_shared_info前 15 個位元組。 - 越界寫入讓堆積上殘留的 pipe 位址作為
frags[0],接著觸發put_page()以建立懸空 pipe 頁。 - 該頁面被回收為 PTE 頁,因此可讀寫實體記憶體。
- 重寫
core_pattern以執行 root 輔助程式。
此錯誤的會計邏輯來自 commit 773ba4fe9104。目前的 MSG_SPLICE_PAGES 觸發在 commit ce650a166335 之後變得可及。IPv6 路徑已在 commit 736b380e28d0 中修復。
漏洞分析
概觀
UDP corking 將多個傳送收集到一個資料報中。資料報可能跨越片段邊界。然後 __ip6_append_data() 將邊界後的位元組計數為前一個 skb 的 fraggap,並將它們移到下一個 skb 的線性區域。基本 skb 如下所示。

fraggap 是將複製到線性區域的資料。因此它必須是新 skb 線性配置的一部分,且必須從 pipe 頁面剩餘的長度中排除。易受攻擊的程式碼卻相反。它從線性資料區域移除 fraggap 並將其放入 pagedlen。
核心在未保留資料區域空間的情況下將 fraggap 位元組複製到線性尾部。這讓我們可以將緊接在尾部後面的 skb_shared_info 設定為任意 15 位元組。只將單一 nr_frags 位元組設為 1,就會讓超出範圍的 frags[0] 將堆積上殘留的 pipe 頁面位址視為已配置位址。
根本原因
此錯誤發生是因為相同的位元組在不同路徑下計數方式不同。建構 datalen 時將 fraggap 包含為下一個 skb 必須處理的資料。而 paged 分支則將相同的 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,因此也包含 gap。這使得 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);
/* ... */
}
最終,paged 分支在未為 gap 內容保留空間的情況下複製 gap 內容。
觸發 15 位元組越界
目標是讓第一個 skb 比片段邊界大剛好 15 位元組。接下來,第二個 skb 的線性尾部會落在 skb_shared_info 的起始位置。
- 一個具有 320 位元組路由標頭和 MTU 1287 的 UDPv6 迴路通訊端
- 一個 919 位元組的 pipe 承載,使第一個 skb 比片段邊界大 15 位元組
- 第二個 20 位元組的 pipe 承載附加到同一個 cork 佇列
從 pipe splice() 到通訊端會使 splice_to_socket() 建立 bvec 迭代器並設定 MSG_SPLICE_PAGES。SPLICE_F_MORE 變成 MSG_MORE。搭配 UDP_CORK,這會將第一個 skb 保留在寫入佇列中而不傳送。
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() 保留 page-splice 路徑並設定 paged = true。這會到達易受攻擊的路徑,將承載附加為 skb 片段。
實際順序如下。
- 第一次
splice(919, SPLICE_F_MORE)在加入 8 位元組 UDP 標頭後附加length = 927。320 位元組路由標頭使fragheaderlen成為 360。第一個 skb 長度變成剛好 1287。 UDP_CORK將 skb 保留在佇列中。計算出的片段邊界為 1272,因此 skb 尾部的最後 15 位元組必須移到下一個片段。- 第二次
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 複製。這讓我們可以將想要的 15 位元組寫入 skb_shared_info。

利用原語
我們現在可以在 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 輸出在附錄中。
由於我們只能控制 15 位元組,因此我們只能控制一個旗標。所以唯一有用的欄位是 nr_frags。將保存真實頁面描述元的 frags[0] 在越界範圍之外。我們如何解決這個問題?
解決以下問題即可獲得 root。
利用細節
利用摘要
- 重用未初始化變數 → 釋放一個持有有效
skb_frag_t的物件,然後將其重新配置為易受攻擊的 skb,以重用未初始化的frags[0] - Fraggap 越界 → 將
nr_frags從 0 翻轉為 1,使frags[0]被視為已配置 - 附加 Pipe → 將第二個 pipe 頁面附加到 slot 1,使
nr_frags == 2 - 不匹配的頁面釋放 → 解構程式對
frags[0]執行put_page() - Dirty Pagetable → 將懸空頁面重新配置為 PTE 頁面
- 實體寫入 → 變更 PTE 以尋找並覆寫
core_pattern並執行 root 輔助程式
利用背景
為什麼陳舊的 frags[] 會存活?
配置新的 skb head 並不會清除 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 越界將 nr_frags 設為 1,會使核心將該頁面視為已配置。
Dirty Pagetable
頁表是來自頁面分配器的 4 KiB 頁面。將懸空 pipe 頁面與 PTE 頁面重疊,可讓 pipe 寫入損毀 PTE。只變更 PTE 的 PFN 即可將我們想要的實體頁面對應到使用者空間。
植入陳舊的 frags[0]
目前我們唯一能控制的欄位是 nr_frags,而我們需要的 frags[0] 在越界範圍之外。因此,在觸發錯誤之前,利用程式在同一個快取中建立有效的描述元,並回收那些位元組仍存在的物件。
首先,它將 256 位元組的標記寫入來源 pipe wp。然後它透過 tee() 到 8 個保留 pipe,以將頁面參考計數提升到 9。這些額外的參考在 grooming 和觸發期間保持來源頁面存活。
描述元是在單一 corked 資料報中建立的。所有 paged 承載都透過 tee() 來自同一個來源頁面。因此,每個目標 head 都會在 frags[0] 中獲得有效的描述元和有效的頁面參考。

觸發通訊端和兩個 pipe 承載是在 groom 通訊端關閉之前準備的。然後兩個 splice 在沒有其他配置的情況下執行。第一個觸發 skb 有 392 個請求,因此它使用 kmalloc-1k 且不會接觸 small-head 空閒清單。只有第二個 384 個請求會重用剛剛釋放的目標之一。
重用的物件現在在 frags[0] 中持有來源頁面描述元位元組,但在 nr_frags == 0 時保持無害。下一階段使用越界來啟動這個陳舊的 slot。
從 nr_frags 到懸空頁面
一旦 Fraggap 越界將 nr_frags = 1 設定後,解構程式會將 frags[0] 視為真正的片段。第二個 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。第二個觸發 pipe 頁面與來源 wp 頁面不同,因此它不會與 frags[0] 合併,而是進入 frags[1]。此路徑執行正常的 get_page() 並將 nr_frags 提升到 2。
關閉通訊端會排空 cork 佇列,skb 解構程式會釋放兩個 slot。
for (i = 0; i < shinfo->nr_frags; i++)
__skb_frag_unref(&shinfo->frags[i], skb->pp_recycle);
因此,會對 frags[0] 執行 put_page() 並降低 refcount。最後關閉 pipe 會留下一個懸空頁面。

回收為 PTE
現在有一個 4 KiB 頁面已返回分配器,但仍被 wp->pipe_buffer 指向。呼叫 mmap() 然後觸碰它以建立 PTE 頁面,即可回收釋放的頁面。
現在可以從使用者空間讀寫任何實體頁面。
尋找並覆寫 core_pattern
透過 PTE 控制的實體讀寫,我們搜尋 core_pattern。
找到後,我們將值變更如下。當子程序引發 SIGSEGV 時,核心會以 root 身分執行以下命令。
|/proc/%P/fd/666 %P
旗標

附錄
已驗證的 skb_shared_info 配置
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 */
};
緩解措施
修補程式
此修補程式恢復了上述不變性。它加入真正複製到線性區域的 fraggap,並在非線性端減去相同的值。
- alloclen = fragheaderlen + transhdrlen;
- pagedlen = datalen - transhdrlen;
+ alloclen = fragheaderlen + transhdrlen + fraggap;
+ pagedlen = datalen - transhdrlen - fraggap;
IPv6 修復也從負數 copy 檢查中移除了 MSG_SPLICE_PAGES 例外。IPv4 路徑有相同的會計錯誤,並已在 commit eca856950f7c 中修復。
部分緩解措施
限制 splice() 有效,但不是好的選項。
時間軸
| 日期 | 事件 |
|---|---|
| 2022-07-12 | 錯誤在 commit 773ba4fe9104 中引入。 |
| 2023-08-02 | 目前的 MSG_SPLICE_PAGES 損毀在 commit ce650a166335 之後變得可能。 |
| 2026-05-15 | 我們私下向 [email protected] 回報漏洞。 |
| 2026-06-21 | IPv6 修復 736b380e28d0 進入 netdev net 樹。 |
| 2026-06-25 | 修復合併到 Linux 主線。 |
| 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 和 exploit。 |
致謝
Fraggap 由 @physicube 和 @qwerty 在 IPv4/IPv6 中發現,並用於 kernelCTF。我們感謝 Sultan Alsawaf(team CIQ)確認 IPv4 錯誤可被利用。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.