為什麼 Checksum Offload 會變成 KVM Escape 漏洞?
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()
如何填補此缺口。
。
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 有多遠。
- /* Code 1 — conceptual validation gap described in the primary source */
- /* Checks only against the current frame-relative transport offset: */
- transport_offset >= minimum_network_header_length
- /* Does NOT by itself establish distance from the IPv4 network header: */
- transport_offset - network_offset >= minimum_network_header_length
- /* These two inequalities are not interchangeable — the first can pass */
- /* while the checksum-start position still falls inside bytes that */
- /* 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
之前
:
- /* Code 2 — pre-fix ordering in ip_do_fragment(), abbreviated */
- if (skb->ip_summed == CHECKSUM_PARTIAL)
- skb_checksum_help(skb); /* [A] deferred write happens FIRST */
- iph = ip_hdr(skb); /* [B] header pointer re-read AFTER */
- /* [C] fragmentation then derives header length from iph->ihl, */
- /* 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。
4. 從 header mutation 到 32 位元組洩漏
洩漏大小可透過封包
邏輯線性資料
的簡單算術得出,並非源自任何底層記憶體配置的違規。在研究幾何結構中,
sk_buff
的線性 head 持有 20 位元組的原始 IPv4 header 加上 8 位元組的第一個 fragment payload:
- /* Code 3 — logical linear-head accounting (from the primary source) */
- 20 bytes /* original IPv4 header, genuinely valid */
- + 8 bytes /* first-fragment payload, also genuinely valid */
- ---------
- 28 bytes /* skb_headlen(skb): the packet's real linear data */
- 60 bytes /* header length the fragmenter now believes (ihl=15) */
- - 28 bytes /* actual logical linear data available */
- ---------
- 32 bytes /* copied into the outgoing header but NOT part of */
- /* 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 的每一個呼叫:
- /* Code 4 — ip_check_csum_start(), added in include/net/ip.h */
- static inline int ip_check_csum_start(const struct sk_buff *skb)
- {
- /* skb->csum_start : where the pending checksum write begins */
- /* skb->network_header : start of the parsed IPv4 header */
- /* ip_hdrlen(skb) : length of that already-parsed header */
- if (unlikely(skb->csum_start <
- skb->network_header + ip_hdrlen(skb)))
- return -EINVAL; /* write would land inside the IPv4 header */
- return 0; /* write target is safely past the header */
- }
此不變性設計狹窄:
csum_start
不得落在已驗證的 IPv4 header 結束位置之前。對於非負數的
csum_offset
,這保證最終寫入位置不會與kernel 已信任的 header 位元組重疊 — 它並未認證目的地是正確的 TCP 或 UDP checksum 欄位,commit message 明確將此視為另一個更狹窄的主張
[2]
。Call site 已重新排序,在完成前先驗證:
- /* Code 5 — post-fix ordering in ip_do_fragment() */
- if (skb->ip_summed == CHECKSUM_PARTIAL) {
- err = ip_check_csum_start(skb); /* [A] validate FIRST */
- if (!err)
- err = skb_checksum_help(skb); /* [B] only then complete */
- if (err)
- goto fail; /* [C] reject malformed meta */
- }
這關閉了第 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] 。