摘要

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

fraggap exploit flow

漏洞摘要

需要 CONFIG_IPV6=y

  1. 兩次 splice() 呼叫覆寫新 skb 後面的 skb_shared_info 前 15 個位元組。
  2. 越界寫入讓堆積上殘留的 pipe 位址作為 frags[0],接著觸發 put_page() 以建立懸空 pipe 頁。
  3. 該頁面被回收為 PTE 頁,因此可讀寫實體記憶體。
  4. 重寫 core_pattern 以執行 root 輔助程式。

此錯誤的會計邏輯來自 commit 773ba4fe9104。目前的 MSG_SPLICE_PAGES 觸發在 commit ce650a166335 之後變得可及。IPv6 路徑已在 commit 736b380e28d0 中修復。

漏洞分析

概觀

UDP corking 將多個傳送收集到一個資料報中。資料報可能跨越片段邊界。然後 __ip6_append_data() 將邊界後的位元組計數為前一個 skb 的 fraggap,並將它們移到下一個 skb 的線性區域。基本 skb 如下所示。

pasted image 1784187985518

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 的起始位置。

  1. 一個具有 320 位元組路由標頭和 MTU 1287 的 UDPv6 迴路通訊端
  2. 一個 919 位元組的 pipe 承載,使第一個 skb 比片段邊界大 15 位元組
  3. 第二個 20 位元組的 pipe 承載附加到同一個 cork 佇列

從 pipe splice() 到通訊端會使 splice_to_socket() 建立 bvec 迭代器並設定 MSG_SPLICE_PAGESSPLICE_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 片段。

實際順序如下。

  1. 第一次 splice(919, SPLICE_F_MORE) 在加入 8 位元組 UDP 標頭後附加 length = 927。320 位元組路由標頭使 fragheaderlen 成為 360。第一個 skb 長度變成剛好 1287。
  2. UDP_CORK 將 skb 保留在佇列中。計算出的片段邊界為 1272,因此 skb 尾部的最後 15 位元組必須移到下一個片段。
  3. 第二次 splice(20, SPLICE_F_MORE) 找到同一個佇列中的尾部 skb。第一個 copy 為 0 且小於新長度 20,因此根據邊界重新計算為 copy = -15。此值進入 alloc_new_skb 並建立一個 fraggap = 15 的新 skb。

pasted image 1784190488457

所需的計算如下。

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

pasted image 1784190717346

利用原語

我們現在可以在 skb_shared_info 的開頭寫入 15 位元組。第一個 skb 在邊界之前持有 1272 - 360 - 8 = 904 個承載位元組。因此,來源承載 [904..918] 對應到目的端 [+0x00..+0x0e]

nr_frags 位於 skb_shared_info + 0x02。因此,利用程式只將來源位元組 906 設為 1,其餘保持為 0。

pasted image 1784190867845

nr_frags 在偏移量 0x02frags[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] 中獲得有效的描述元和有效的頁面參考。

pasted image 1784192239418

觸發通訊端和兩個 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 會留下一個懸空頁面。

pasted image 1784191876526

回收為 PTE

現在有一個 4 KiB 頁面已返回分配器,但仍被 wp->pipe_buffer 指向。呼叫 mmap() 然後觸碰它以建立 PTE 頁面,即可回收釋放的頁面。

現在可以從使用者空間讀寫任何實體頁面。

尋找並覆寫 core_pattern

透過 PTE 控制的實體讀寫,我們搜尋 core_pattern

找到後,我們將值變更如下。當子程序引發 SIGSEGV 時,核心會以 root 身分執行以下命令。

|/proc/%P/fd/666 %P

旗標

Fraggap kernelCTF result

附錄

已驗證的 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-21IPv6 修復 736b380e28d0 進入 netdev net 樹。
2026-06-25修復合併到 Linux 主線。
2026-07-04Linux kernel CNA 發布 CVE-2026-53362
2026-07-14我們通知 Openwall linux-distros 清單。
2026-07-16Linux kernel CNA 發布 CVE-2026-53366
2026-07-20我們發布 writeup 和 exploit。

致謝

Fraggap 由 @physicube@qwerty 在 IPv4/IPv6 中發現,並用於 kernelCTF。我們感謝 Sultan Alsawaf(team CIQ)確認 IPv4 錯誤可被利用。

參考資料