Cloudflare 披露的 KV Cache 跨請求暴露:SGLang 為何仍將被拒絕的請求送入 handoff?
Cloudflare 在 2026 年 10 月公開披露,Workers AI 上的 GLM-5.3 曾發生跨請求 KV cache 暴露。依照 Cloudflare 的報告,事故窗口為 8 月 27 日至 9 月 9 日,共有 155 個回應受到影響,相關 prompt 均超過 681,280 tokens。報告沒有提供同期總請求量,因此無法據此計算受影響比例。Cloudflare 事故披露;SGLang 協調披露。
先給不會執行推論叢集的讀者一個結論:這不是所有 AI API 的普遍風險。Cloudflare 表示,其他 Workers AI 模型未受影響,GLM-5.3 已於 9 月 9 日完成修復並恢復服務。披露中明確受影響的是事故窗口內輸入超過 681,280 tokens 的 GLM-5.3 請求。如果你只是呼叫 API,先確認自己是否屬於這個情境;如果你維運自有的 SGLang prefill/decode 分離式叢集,可以直接看文末的四道驗收門。
值得追查的不是「模型怎麼會洩露」這種寬泛問題,而是一個更具體的狀態邊界:請求已經被判定不該繼續,為什麼它仍能帶著只有 1 token 的佔位輸入進入 prefill/decode handoff? 已拒絕,卻還在往下走,這就是最反常的地方。這次事故把兩個問題疊在一起:宣告的 context 上限大於實際 KV pool 能容納的長度;拒絕狀態又沒有在當時的分離式推論入口及時終止請求。前者觸發拒絕,後者讓被拒絕的請求繼續走上不該走的路徑。
事故發生時的七步流程(Cloudflare 披露的生產路徑;PR #36988 雖於 9 月 6 日合併,但在 Cloudflare 9 月 9 日部署引擎修正時,尚未進入其生產環境): ① Bootstrap: router 將原始 prompt 長度告知 decode,decode 先預留相應的 KV 頁;② Prefill admission: admission 檢查 req.finished(),但拒絕原因記錄在 to_finish 而不是 finished_reason,所以請求仍可能被視為未結束並獲准入場;③ Prefill forward: prefill 處理被替換成 1-token 佔位輸入;④ KV transfer: prefill 只傳送該佔位輸入對應的一頁 KV,卻發出 transfer-complete 訊號;⑤ Decode admission: decode 端相同的結束狀態檢查也接受了 handoff;⑥ Decode attention: decode 按預留長度讀取全部 KV 頁,但只有第一頁由目前請求寫入,其餘頁面仍可能留有先前請求的資料;⑦ Response: Cloudflare 表示客戶端收到的是 HTTP 200,而不是預期的 HTTP 400,回應內容包含其他使用者工作階段的資料。Cloudflare 事故披露逐步描述了這條路徑;SGLang issue #30233 與 PR #38935 可交叉核對相關狀態與 admission 修復。
單看每一步似乎都在正常運作,合起來才看出問題:拒絕狀態沒有及時生效,後續步驟卻繼續把請求當成正常請求推進。
681,280 與 1,048,576 是兩種不同的容量
Cloudflare 披露的部署把模型 context 長度設為 1,048,576 tokens,而事故發生時 prefill GPU 上的 KV pool 容量為 681,280 tokens。Cloudflare 後來把 pool 容量提高到約 2.04 million tokens,並補上引擎端的拒絕安全防線。Cloudflare 的容量說明與修復紀錄。
這兩個 681,280 出自同一份披露,指的是兩件事:一個是受影響請求的 prompt 長度門檻,另一個是當時 prefill GPU 的 KV pool 容量。數字相同,角色不同——前者描述什麼樣的請求會受到影響,後者描述執行環境實際能容納多少。
這兩個數字管的是不同事情。context length 是模型設定層允許請求達到的上下文邊界;KV pool 則是執行時真正能為 token 分配並保存鍵值狀態的顯存池。把 context 設得更大,不會自動把 KV pool 也擴到同樣容量。因此,一個請求可能尚未超過宣告的 context 上限,卻已超過目前 worker 實際能容納的 KV 狀態。
pool 容量不足本應觸發拒絕;真正的問題是拒絕狀態沒有及時攔住後續 handoff。Cloudflare 披露與上游原始碼都把問題指向 abort 狀態在分離式推論路徑中的傳播缺口。
拒絕原因已記錄,不代表 admission 已終止請求
SGLang 的相關路徑中,Req.set_finish_with_abort() 會把拒絕原因記錄到 to_finish,同時把輸入 prompt 換成 1-token placeholder。但當時的 finished() 判斷讀取的是 finished_reason。換句話說,拒絕原因已記錄在請求物件上,但用來判斷「請求是否結束」的 finished() 仍可能讓它看起來像一個還在正常排程中的請求。SGLang PR #38935 的根因說明。
把這個狀態差異直接放到原始碼中,更容易核對。以下是固定 commit 的非連續摘錄,省略行已標示;完整上下文分別見 set_finish_with_abort()(L2053–2072)、finished()(L1479–1481) 與 update_finish_state()(L1842–1848):
# Req.set_finish_with_abort() — excerpt
self.origin_input_ids = array(
"q", [0]
) # set it to one token to skip the long prefill
# ... omitted ...
self.to_finish = FINISH_ABORT(error_msg, status_code, err_type)
# Req.finished()
return self.finished_reason is not None
# Req.update_finish_state()
if self.to_finish:
self.finished_reason = self.to_finish
self.to_finish = None
這不代表 to_finish 永遠不會生效:update_finish_state() 之後會把它移到 finished_reason。問題在於 admission 先檢查 finished(),此時狀態轉換還沒發生。
Cloudflare 在 9 月 9 日部署引擎修正之前,這種狀態差異會沿著分離式 prefill/decode 流程繼續傳播:admission 把請求當成仍可入場的請求;decode 端為它預留足夠的 KV pages;prefill 實際處理的卻是 1-token 佔位輸入,只產生並轉移不足量的 KV 狀態。decode 之後可能讀取目前請求根本沒有覆寫的頁面。Cloudflare 將實際暴露歸因於先前請求留下的 KV 資料。Cloudflare 報告;上游相關 issue #30233。
這裡最容易混淆的是三種「完成」訊號:
- HTTP 200 表示客戶端收到成功的 HTTP 狀態碼;它本身不證明回應語意正確,更不證明 KV 頁屬於目前請求。
- prefill completion / transfer-complete 表示某個處理或交接步驟發出了完成訊號;它不會自動證明實際傳輸的 token 數量覆蓋了 decode 預留的所有頁面。
- decode 讀取到完整且有效的 KV pages 才是更接近真正資料條件的檢查。如果只核對「流程完成」,沒有核對實際寫入範圍與頁面所有權,就可能把「傳輸步驟結束」誤當成「資料完整且屬於目前請求」。
流程走完了,不等於資料正確。
#36988、#38935 與 #38704:不能把三者寫成同一次修復
這幾筆上游紀錄的時間順序與職責很重要。
| 時間 / 紀錄 | 狀態 | 實際解決什麼 |
|---|---|---|
| 2026-07-06,issue #30233 | 已關閉 | 早先已回報 PD disaggregation 中,超長 prompt 只傳遞 1 token 的相似問題。它是相關缺陷報告,不等於獨立復現了 Cloudflare 完整的生產事故。 |
| 2026-09-06,PR #36988 | 已合併;Cloudflare 標示於 v0.5.20 發布 | 修正 aborted prefill result 的終止處理與清理路徑:清除待傳送的 chunk 狀態、不把 abort 結果重新排回未完成佇列、釋放 metadata/KV/HiCache/Mamba 等資源,並忽略已退役請求的延遲結果。它補的是正確性缺口。 |
| 2026-09-16,PR #38935 | 已合併;Cloudflare 標示於 v0.5.21 發布 | 把尚未入場的拒絕請求提前在 prefill 與 decode admission 入口終止,避免不必要的 bootstrap、metadata buffer、sender/receiver 初始化與 forward pass。PR 作者明確說明,這是減少資源浪費與延遲的修復,不是 #36988 之後第二次獨立的 correctness 修復。 |
| 2026-09-09 建立、2026-10-10 關閉,PR #38704 | 已關閉、未合併;沒有發布版本 | Cloudflare 提出的另一種防線方案,不能當成已進入上游主線的修復。 |
#38935 的說明提供了關鍵時間錨點:在包含 #36988 的修復後 main 上,decode 不再提交不足量 handoff。上游 PR 也給出一組 1P1D(2×H200)前後行為對照:合入 #38935 後,decode 從等待 300 秒後逾時變成立即回傳 400,bootstrap/metadata buffer 不再建立,stub batch 從 1 變為 0。這是上游提供的前後比較,不是本文自行執行的效能測試。
這也說明為什麼入口守衛不能簡化成「只要 to_finish 非空就終止請求」。#38935 使用 is_unadmitted_reject 識別真正尚未入場、尚未取得資源所有權的拒絕請求;重入或已搶占的請求可能已持有 KV 或 metadata 等資源,不能只憑 abort 標記就提前丟棄。decode 端也需要考慮重入路徑,因為部分資源狀態不一定保留在 Req 物件上。否則,過早終止請求可能跳過正常排程路徑負責的清理,例如 streaming session 的 active slot detach(解除工作階段對活躍 slot 的占用)。SGLang PR #38935。
後續還有幾類相關但不同的防線。PR #39500 加入可選的 handoff KV checksum;Cloudflare 也介紹了 KV canary(偵測並記錄潛在損壞)與 read-time page tagging。後者是 Cloudflare 在 2026 年夏季測試的原型,曾在 GLM-5.2 上試用,之後因可靠性問題停用,且依賴特定 attention backend 與模型架構;它不是目前已上線的通用防線。Cloudflare 同時說明 checksum 與 canary 尚未涵蓋所有模型架構,GLM-5.3 仍在擴大支援範圍;handoff checksum 驗證的是 prefill 到 decode 的傳輸,不代表每一步 decode 讀取都經過驗證。PR #40645 與 PR #41402 共同修復 drain acknowledgement 遺失或未涵蓋所有 decode peer 的問題;PR #41404 則把 deferred release 擴大到不只是 decode 主動發起 abort 的其他 transfer failure,避免在途 RDMA 寫入仍可能碰到頁面時就釋放或重用頁面。這些修復處理的是 KV page lifetime race,與拒絕請求確定性地進入不足量 handoff 是不同的失效路徑。Cloudflare 的防線與版本清單。
固定版本 CPU 驗證:證實程式碼路徑,不是復現租戶資料洩露
這輪不只閱讀事故報告與 PR。驗證分成三層,但每一層能證明的事情不同。
| 證據 | 觀察結果 | 證據能支持到哪裡 |
|---|---|---|
| 自建 Python 合成狀態機 | handoff 模型 6/6;admission / re-entry 模型 9/9 | 說明我們構造的狀態模型守衛符合預期;不代表 SGLang 執行期結果。 |
| 固定 SGLang commit 的上游 CPU 單元測試(P1) | 兩份測試合計 19 passed;其中 admission 測試另有 12 個 subtests | 執行了固定版本的真實上游 mixin、queue 與 retire 程式碼;transport、cache、scheduler 的部分協作者仍是 Mock。沒有執行真實 prefill/decode 叢集。 |
| 修復前後差分(P2) | #38935 admission 差分與 #36988 prefill cleanup 差分都觀察到行為變化 | 說明固定輸入在兩個版本觸發了不同程式碼路徑;差分僅涵蓋 CPU 程式碼與探針行為。 |
P1 固定在 SGLang commit 8f3d6944b76c428d884582cb6c82a39db1465360(固定原始碼)。執行的是上游的 test_prefill_abort_result_cleanup.py(11 passed)與 test_admission_abort_not_enqueued.py(8 passed,另有 12 個 subtests),合計 19 passed。環境為 Linux CPU-only 虛擬機、Python 3.12.3、pytest 9.2.0 與 CPU 版 torch;沒有 GPU、模型權重、RDMA 或租戶資料。
P2 則分別隔離每個 PR 的前後版本,避免把多個修復混在同一組比較中:
- 實驗 A:#38935 admission 入口(base 17ba2c2e → head 8f3d6944)。 修復前,入口拒絕的請求仍會進入佇列並觸發 sender/receiver 建立;修復後,請求在入口就被終止處理,不再建立這些交接物件。對照中的有效請求,以及攜帶 metadata buffer 的重入請求,在兩邊都被保留,未觀察到這兩類請求被誤退役。
- 實驗 B:#36988 prefill 結果清理(base 9c254df8 → head 2ee5742a)。 探針觀察到,修復前 abort 的最終結果會被當成未完成快取處理並重新進入 inflight 佇列;已終止請求的延遲結果仍可能觸發 KV chunk 傳送。修復後,請求按預期完成清理,延遲結果不再觸發這些副作用。
P2 的 7/7 與 P1 的 11 passed 來自不同版本。P2 使用 #36988 head commit 2ee5742a 當時的 7 個測試案例;P1 固定的 8f3d6944 後來加入 sampling-mask 參數化測試,展開後該檔案有 11 項。因此,P2 的 0/7 → 7/7 是同一批 7 個案例的修復前後對照,不能與 P1 的 11 passed 混算。P2 的 7 個 pre-fix 失敗中,6 個是程式碼落入正常 transfer 路徑後產生的 AttributeError,另 1 個是 ValueError;測試 stub 沒有建模正常路徑,所以這些例外不是 7 個業務斷言失敗。更有解釋力的是探針觀察到的重新入列、清理與延遲傳送行為。
公開複核材料
我把兩份差分探針、四份原始 JSON 輸出、P2 收據與 SHA-256 校驗清單放進公開實驗材料包與重現說明。材料固定了兩組 base/head commit,並記錄 Mock 邊界與 CPU-only 限制。P2 的 pre-fix 0/7 是 stub fall-through(6 個 AttributeError、1 個 ValueError),不是 7 個斷言失敗;這些材料證明的是固定版本的程式路徑差分,不是 GPU/RDMA 或真實跨租戶洩露復現。
部署端應該驗收哪四道門
如果你正在維護多租戶推論服務,或正在使用 SGLang 的 prefill/decode 分離架構,最容易犯的懶就是:「群組裡說升到 v0.5.22,隨便發一條長 prompt 測試返回 400,就可以下班了。」別這樣做;線上配置與生命週期路徑有很多方式會繞過只看版本號的驗收。我會把檢查拆成四道獨立的門:
- Pool / context gate:設定是否與執行時容量一致。 worker 啟動後,先看啟動日誌實際列出的 KV pool 容量,再與 context 設定核對,確認宣告的 context 上限與執行時實際容量一致。設定、GPU 分配或並行策略變更後,重新核驗啟動日誌。負向案例應涵蓋「輸入沒有超過 context 上限,卻超過目前 KV pool 能容納的長度」,並確認系統明確拒絕,而不是繼續建立 handoff。
- Reject / admission gate:拒絕狀態是否在入口生效。 檢查被拒絕的請求是否在 bootstrap、queue admission、metadata buffer 分配與 sender/receiver 初始化之前終止。測試不能只涵蓋新請求,也應包含有效請求、重入請求、已搶占請求,以及仍持有 KV、Mamba 或 metadata 資源的請求。不能把所有帶有 abort 狀態的請求一律丟掉,否則可能破壞原請求的資源清理責任。
- Transfer / read gate:實際傳輸範圍是否涵蓋實際讀取範圍。 比較預留的 token/page 數量與實際寫入、傳輸的有效範圍。HTTP 200 或 transfer-complete 不能取代長度與所有權驗證;如果考慮啟用
—disaggregation-enable-kv-checksum或—kv-canary,先確認目前模型和 backend 支援,再查看日誌確認請求確實經過驗證。Cloudflare 披露時,這兩項尚未涵蓋 GLM-5.3 等所有架構,不能把未支援的選項算成有效防線;handoff checksum 也不會驗證每一次 decode 讀取。 - Lifetime / ownership gate:失敗後,頁面何時才能釋放或重用。 對 abort、延遲結果與在途 RDMA write 加入負向測試,確保未結束的寫入不會污染重新分配的頁面、已終止請求的延遲結果不會再傳送 chunk,並且資源只釋放一次。確認 prefill 與 decode 兩端都啟用了 deferred KV release。對 Mooncake,還要核對 transfer batch 上限與 release timeout 是否匹配,避免單次逾時留下過長的在途寫入尾端。包含 PR #41023 的版本已對支援的後端預設啟用 deferred release,但仍應以實際執行版本與設定為準。#40645 與 #41402 處理 drain 確認路徑,#41404 處理其他 transfer failure 的延遲釋放;這些生命週期風險應與最初的確定性 handoff 缺陷分開驗收。
SGLang 的 v0.5.22 release 於 2026-10-19 發布。Cloudflare 建議使用 v0.5.22 或更新版本,或視需求 backport 對應修復。但「已升級版本」不等於「部署已通過隔離驗收」:還要核對實際執行的程式碼版本、pool/context 設定、admission 負向案例與頁面生命週期。如果 checksum 等功能沒有啟用,就不能把它算成已生效的防線。
這次事故最值得記住的不是某一個布林值寫錯,而是錯誤狀態、資料完整性與資源所有權分屬不同邊界。拒絕原因存在於請求物件中,不代表 admission 一定看得到;一次 transfer 結束,不代表 decode 讀到的每一頁都由目前請求寫入;發出 abort,也不代表所有在途寫入都已結束。只有把這些事情分開驗證,才不會讓一條看似成功的請求路徑替真正的資料隔離背書。
測試日期:2026 年 10 月 28 日。
SGLang 七步事故鏈
逐步檢查拒絕狀態如何穿過 admission、KV transfer 與 decode。這是對 Cloudflare 披露路徑的互動閱讀,不是在瀏覽器執行 SGLang。
即使沒有 JavaScript,以下七步仍完整可讀。
- Cloudflare 生產事故披露
1. Bootstrap:預留 KV 頁
Router 將原始 prompt 長度告知 decode;decode 預留相應 KV 頁。
預留長度來自原始 prompt;這不能證明後續傳輸涵蓋完整長度。
- 固定版本 CPU 差分
2. Prefill admission:拒絕未擋住准入
req.finished() 檢查讀取 finished_reason,而拒絕原因當時在 to_finish;舊路徑仍可能把請求視為未結束。
固定 CPU 差分:舊版拒絕請求仍入列並建立 sender;修正版在 admission 入口 retire。
固定原始碼 · 8f3d6944b76c · L1479-L1481本地證據: probe-admission-pre-20261028.json本地證據: probe-admission-post-20261028.json
- Cloudflare 生產事故披露
3. Prefill forward:輸入縮成 1 token
為略過過長 prefill,輸入替換成 1-token placeholder;這不證明原請求的 KV 狀態完整。
Placeholder 是控制流程訊號,不是完整 KV 內容的證明。
- Cloudflare 生產事故披露
4. KV transfer:一頁資料卻回報完成
Prefill 只傳送 placeholder 對應的一頁 KV,卻發出 transfer-complete;完成訊號不驗證涵蓋範圍。
事故路徑來自 Cloudflare 披露;本專案未獨立重現 GPU/RDMA 傳輸。
固定原始碼 · 8f3d6944b76c · L2053-L2072本地證據: sglang-cpu-p2-receipt.json
- 固定版本 CPU 差分
5. Decode admission:錯誤狀態再次放行
Decode 端也接受 handoff;固定 CPU 差分顯示舊版建立 receiver,修正版在入口 retire 遭拒請求。
固定 CPU 差分顯示舊版為遭拒 decode 請求建立 receiver;修正版在 admission 入口 retire。
固定原始碼 · 8f3d6944b76c · L1479-L1481本地證據: probe-admission-pre-20261028.json本地證據: probe-admission-post-20261028.json
- Cloudflare 生產事故披露
6. Decode attention:讀取範圍超過有效寫入
Decode 依預留長度讀取 KV 頁,但事故路徑中只有第一頁由目前請求寫入,其餘頁可能殘留先前請求資料。
披露的事故路徑;公開 CPU 差分驗證相關 abort/佇列/清理路徑,不是 GPU attention 重現。
固定原始碼 · 8f3d6944b76c · L1479-L1481本地證據: probe-prefill-pre-20261028.json本地證據: probe-prefill-post-20261028.json
- Cloudflare 生產事故披露
7. Response:HTTP 200 不等於資料正確
Cloudflare 披露客戶端收到 HTTP 200 而非預期 HTTP 400,回應包含其他使用者工作階段內容。
這是廠商披露的事故結果;本專案沒有重現真實跨租戶回應。
固定原始碼 · 8f3d6944b76c · L1479-L1481本地證據: sglang-cpu-p2-receipt.json