1. 簡介

Checksum offload 是 Linux 網路堆疊中最古老 效能協議(performance contract) 之一:kernel 不必立即計算傳輸層 checksum,而是可以將封包標記為 CHECKSUM_PARTIAL ,並將寫入動作延後到稍後階段執行,同時透過兩個欄位 csum_start 和 csum_offset 記錄 寫入位置 。由 pwn.ai 研究人員記錄並命名為「 PwnEscape 」的 CVE-2026-99999 顯示,當這項延遲寫入協議接收來自較不受信任來源(例如 virtio-net guest)的 metadata 時,驗證與完成之間的差距便成為主機記憶體洩漏操作(disclosure primitive)的路徑,最終在研究人員自行隔離的 KVM fixture 中導致 guest-to-host escape [1] 。

報告說明延遲 checksum metadata 如何改寫已經解析過的 IPv4 header,為何從數學角度來看,這種改寫必然導致 32 位元組的主機記憶體洩漏,以及上游接受的防護措施 ip_check_csum_start() 如何填補此缺口。 。

為什麼 Checksum Offload 會變成 KVM Escape 漏洞?PwnEscape 真相揭秘! | 資訊安全新聞

2. 背景:稍後寫入的承諾

處於 CHECKSUM_PARTIAL 狀態的 sk_buff 尚未包含有效的傳輸層 checksum。kernel 反而儲存一個 start offset 與 offset-from-start,並由後續階段(網路裝置或軟體 fallback skb_checksum_help() )執行實際寫入。文件化的 offload semantics 假設這些欄位始終描述目前存在的封包 [3] 。研究精確地提出 kernel 問題:延遲操作是否能在 kernel 已針對相同位元組做出安全相關決策之後,修改封包位元組 [1] ?

下方的第一段程式碼,說明為何單一邊界檢查並不足夠。 sk_buff 同時具有相對於 frame 的視角(在原始 link-layer frame 內的位置)以及相對於 network-header 的視角(相對於已解析的 IPv4 header 的位置)。僅檢查前者無法確立在剝除 link-layer 位元組後,checksum start 距離 network header 有多遠。

  1. /* Code 1 — conceptual validation gap described in the primary source */
  2. /* Checks only against the current frame-relative transport offset: */
  3. transport_offset >= minimum_network_header_length
  4. /* Does NOT by itself establish distance from the IPv4 network header: */
  5. transport_offset - network_offset >= minimum_network_header_length
  6. /* These two inequalities are not interchangeable — the first can pass */
  7. /* while the checksum-start position still falls inside bytes that */
  8. /* later become the IPv4 header once L2 headers are stripped. */

3. 根本原因:header 已檢查,隨後 header 卻改變

此缺陷是順序問題,而非 Race Condition — 執行完全有確定性。在 Host ingress 解析 IPv4 header,fragment processing 與 routing 運作期間,kernel 仍將其視為 20 位元組、五個 word 的 header( ihl = 5 ,第一位元組 0x45 )。直到稍後的 output fragmentation 階段,暫止的 checksum 才真正完成。在修復前的程式碼路徑中,該完成動作發生在重新導出 fragmentation 所需的 header pointer 之前 :

  1. /* Code 2 — pre-fix ordering in ip_do_fragment(), abbreviated */
  2. if (skb->ip_summed == CHECKSUM_PARTIAL)
  3. skb_checksum_help(skb); /* [A] deferred write happens FIRST */
  4. iph = ip_hdr(skb); /* [B] header pointer re-read AFTER */
  5. /* [C] fragmentation then derives header length from iph->ihl, */
  6. /* trusting whatever value line [A] may have just overwritten. */

由於 skb_checksum_help() 會在 csum_start + csum_offset 處寫入,且該位置受攻擊者影響,因此寫入可能落在已被解析為 IPv4 header 的位元組內。最具關鍵影響的被覆寫欄位是 version/IHL 位元組:在研究案例中,它從 0x45 (IHL 5,20 位元組 header)變更為 0x4f (IHL 15,60 位元組 header),而未觸及 version nibble,因此封包看起來仍是有效的 IPv4,但其宣告的 header length 幾乎增加三倍 [1] 。

3.1 控制流程重建

下圖控制流程追蹤路徑,從 guest 提供的 checksum metadata 到攜帶洩漏 host bytes 的 emitted fragment。

sequenceDiagram participant G as Guest-origin metadata participant V as virtio_net_hdr_to_skb participant S as sk_buff (csum_start/offset) participant I as IPv4 ingress (parse, route) participant H as skb_checksum_help() participant F as ip_do_fragment() participant N as Emitted fragment G->>V: packet + CHECKSUM_PARTIAL metadata V->>S: store csum_start, csum_offset Note right of V: only frame-relative offset checked S->>I: skb enters host IPv4 stack I->>I: parse IHL=5, 20-byte header trusted I->>F: forward for output fragmentation F->>H: checksum still pending H->>S: write at csum_start+csum_offset Note right of H: write lands inside parsed IPv4 header S->>F: ip_hdr(skb) re-read AFTER the write F->>F: ihl now reads 15 (60-byte header) F->>N: copy 60-byte "header" into fragment Note over N: 32 bytes beyond skb_headlen() leak as fake IP options
圖 1. 控制流程追蹤,從 guest-origin checksum metadata 到 emitted IPv4 fragment 中的 host-memory 洩漏。

4. 從 header mutation 到 32 位元組洩漏

洩漏大小可透過封包 邏輯線性資料 的簡單算術得出,並非源自任何底層記憶體配置的違規。在研究幾何結構中, sk_buff 的線性 head 持有 20 位元組的原始 IPv4 header 加上 8 位元組的第一個 fragment payload:

  1. /* Code 3 — logical linear-head accounting (from the primary source) */
  2. 20 bytes /* original IPv4 header, genuinely valid */
  3. + 8 bytes /* first-fragment payload, also genuinely valid */
  4. ---------
  5. 28 bytes /* skb_headlen(skb): the packet's real linear data */
  6. 60 bytes /* header length the fragmenter now believes (ihl=15) */
  7. - 28 bytes /* actual logical linear data available */
  8. ---------
  9. 32 bytes /* copied into the outgoing header but NOT part of */
  10. /* the sender's logical packet -> host-memory leak */

Fragmenter 複製 60 位元組,認為它們是 IPv4 header 加上 options;但其中只有 28 位元組是真正的封包資料,因此剩餘的 32 位元組來自主機kernel 映射在 skb_headlen() 之外的任何內容 — allocator 已接觸過的 tailroom。由於 fragmenter 將這些位元組作為傳出網路 header 的一部分發出,這些位元組便從內部主機狀態跨越到外部可觀察的 Artifact:越界讀取變成傳輸洩漏 [1] 。來源強調這是 邏輯 skb 邊界違規,而不一定是跨配置違規,這正是它能可靠重現而非依賴 allocator 布局的原因 [1] 。

5. 修復:在完成前先驗證

已接受的上游 patch,commit 6785011f8b16 ,新增單一 IPv4 特定防護,而非重新設計 checksum-offload API [2] 。該防護置於共用 helper 中,並在延遲寫入發生前,於五個 checksum-completion call sites 的每一個呼叫:

  1. /* Code 4 — ip_check_csum_start(), added in include/net/ip.h */
  2. static inline int ip_check_csum_start(const struct sk_buff *skb)
  3. {
  4. /* skb->csum_start : where the pending checksum write begins */
  5. /* skb->network_header : start of the parsed IPv4 header */
  6. /* ip_hdrlen(skb) : length of that already-parsed header */
  7. if (unlikely(skb->csum_start <
  8. skb->network_header + ip_hdrlen(skb)))
  9. return -EINVAL; /* write would land inside the IPv4 header */
  10. return 0; /* write target is safely past the header */
  11. }

此不變性設計狹窄: csum_start 不得落在已驗證的 IPv4 header 結束位置之前。對於非負數的 csum_offset ,這保證最終寫入位置不會與kernel 已信任的 header 位元組重疊 — 它並未認證目的地是正確的 TCP 或 UDP checksum 欄位,commit message 明確將此視為另一個更狹窄的主張 [2] 。Call site 已重新排序,在完成前先驗證:

  1. /* Code 5 — post-fix ordering in ip_do_fragment() */
  2. if (skb->ip_summed == CHECKSUM_PARTIAL) {
  3. err = ip_check_csum_start(skb); /* [A] validate FIRST */
  4. if (!err)
  5. err = skb_checksum_help(skb); /* [B] only then complete */
  6. if (err)
  7. goto fail; /* [C] reject malformed meta */
  8. }

這關閉了第 3 節中展示的特定 write-after-parse 重疊,同時將審查者指出為獨立關切事項的更廣泛 ingress-side 驗證留待未來工作 [2] 。

6. 討論:可重複使用的失敗模式

來源將此 bug 概括為三個鏈接的信任轉換:untrusted metadata 在准許時進入,隨著 headers 正規化,封包的表示形式發生變化,而延遲操作後來在另一個 consumer 讀取之前突變已解析的狀態 — 研究人員稱此模式為「deferred-metadata trust inversion」 [1] 。這是一種不需要協同的 time-of-check/time-of-use 失敗變體:分隔檢查與使用的「時間」僅僅是 kernel 操作的普通序列,中間插入了受攻擊者影響的延遲寫入。這可推廣到任何子系統,其中 metadata 針對物件的一種表示形式進行驗證,但後續不同順序的 consumer 卻針對 metadata 仍能影響的另一種表示形式行動 — 這是一個有用的稽核問題,遠遠超出 IPv4 fragmentation [1] ,並且與早期針對 hypervisor proper 之外虛擬化邊界假設的 guest-to-host 研究相同 [4] 。

7. 結論

CVE-2026-99999 證明,安全邊界的強度僅取決於其最弱的 deferred consumer。此漏洞不存在於 KVM 的指令模擬或記憶體管理單元處理中;它存在於普通 Linux 網路中的效能優化裡,之所以可被觸發,是因為來自較不受信任 guest 的 checksum-offload metadata 針對錯誤的座標系統進行驗證,然後被允許突變kernel 已信任的 header。修補本身很小 — 僅是在五個位置於呼叫函式前多檢查一個不等式 — 但背後的教訓,即每個後續轉換狀態的 consumer 都必須重新驗證其所依賴的不變性,適用於遠遠超出此單一代碼路徑的情境 [1] [2] 。