# Cloudflareが公表したKV Cacheのリクエスト間データ露出：SGLangはなぜ拒否されたリクエストをhandoffへ進めたのか

> Canonical: https://vxi.me/ja/posts/glm53-kv-cache-cross-request-exposure/
> Locale: ja-jp
> Published: 2026-10-28
> Updated: 2026-10-29
> Markdown: https://vxi.me/ja/posts/glm53-kv-cache-cross-request-exposure.md

<figure class="glm53-kv-rejection-probe not-prose" lang="ja-jp">
<picture><source media="(prefers-reduced-motion: reduce) and (max-width: 640px)" srcset="/images/posts/glm53-kv-cache-cross-request-exposure/glm53-kv-rejection-probe.mobile.still.ja-jp.svg" type="image/svg+xml"><source media="(prefers-reduced-motion: reduce)" srcset="/images/posts/glm53-kv-cache-cross-request-exposure/glm53-kv-rejection-probe.still.ja-jp.svg" type="image/svg+xml"><source media="(max-width: 640px)" srcset="/images/posts/glm53-kv-cache-cross-request-exposure/glm53-kv-rejection-probe.mobile.ja-jp.svg" type="image/svg+xml"><img src="/images/posts/glm53-kv-cache-cross-request-exposure/glm53-kv-rejection-probe.ja-jp.svg" alt="反例プローブ：HTTP 200 と transfer-complete は KV ページの完全性や現在の要求への所有権を証明しない。事故経路は Cloudflare の公表内容、固定版 CPU 差分は GPU/RDMA や実際のクロステナント漏えいの再現ではない。" width="1200" height="700" loading="eager" fetchpriority="high" decoding="async" style="display:block;width:100%;height:auto"></picture>
<figcaption class="mt-2 text-xs leading-relaxed text-slate-500">反例プローブ · 公開された事故経路と固定版 CPU 差分を区別。実際のクロステナント漏えいは再現していない。</figcaption>
</figure>

Cloudflare は 2026 年 10 月、Workers AI 上の GLM-5.3 でリクエストをまたぐ KV cache の露出が起きたと公表した。報告によると、事故期間は 8 月 27 日から 9 月 9 日までで、155 件のレスポンスが影響を受け、該当する prompt はすべて 681,280 tokens を超えていた。同期間の総リクエスト数は公表されていないため、この情報から影響率を計算することはできない。[Cloudflare の事故報告](https://blog.cloudflare.com/kv-cache-exposure-remediation/) · [SGLang の協調開示](https://www.lmsys.org/blog/2026-10-26-cf-security)。

まず、推論クラスターを運用していない読者向けに結論を示す。これはすべての AI API に共通するリスクではない。Cloudflare によれば、Workers AI の他モデルは影響を受けず、GLM-5.3 は 9 月 9 日に修正され、サービスが復旧した。公表された影響範囲は、事故期間中に入力が 681,280 tokens を超えた GLM-5.3 のリクエストだ。API を呼び出すだけなら自分の利用がこの条件に当たるかを確認してほしい。独自の SGLang prefill/decode 分離クラスターを運用しているなら、記事末尾の 4 つの受け入れゲートを確認できる。

調べるべきなのは「モデルはなぜ漏えいするのか」という広い問いではなく、もっと具体的な状態境界だ。**続行すべきでないと判定されたリクエストが、なぜ 1 token だけのプレースホルダー入力を伴って prefill/decode handoff に進めたのか。** 拒否されたのに処理が続く。そこが最も不自然な点だ。今回の事故では 2 つの問題が重なっていた。宣言された context 上限が実際の KV pool の容量を上回っていたこと、そして当時の分離推論の入口で拒否状態がリクエストを十分早く終了させなかったことだ。前者が拒否を引き起こし、後者が拒否後のリクエストを進むべきでない経路に残した。

**Cloudflare が公表した事故当時の 7 ステップ（PR #36988 は 9 月 6 日にマージ済みだったが、9 月 9 日に Cloudflare がエンジン修正をデプロイした時点では本番に反映されていなかった）：** ① **Bootstrap：** router が元の prompt 長を decode に伝え、decode が対応する KV ページを先に予約する。② **Prefill admission：** admission は **req.finished()** を確認するが、拒否理由は **finished_reason** ではなく **to_finish** に記録されていたため、リクエストは未終了と判断され、入場を許されることがあった。③ **Prefill forward：** prefill の入力が 1-token のプレースホルダーに置き換わる。④ **KV transfer：** prefill はそのプレースホルダーに対応する 1 ページ分だけを送る一方、transfer-complete を通知する。⑤ **Decode admission：** decode 側でも同じ終了状態チェックが handoff を受け入れる。⑥ **Decode attention：** decode は予約長に応じて全 KV ページを読むが、現在のリクエストが書き込んだのは最初のページだけで、残りには以前のリクエストのデータが残っている可能性がある。⑦ **Response：** Cloudflare は、期待された HTTP 400 ではなく HTTP 200 がクライアントに返り、別ユーザーのセッション内容が含まれていたと報告した。[Cloudflare の事故報告](https://blog.cloudflare.com/kv-cache-exposure-remediation/) がこの経路を順に説明している。[SGLang issue #30233](https://github.com/sgl-project/sglang/issues/30233) と [PR #38935](https://github.com/sgl-project/sglang/pull/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 の容量説明と修正記録](https://blog.cloudflare.com/kv-cache-exposure-remediation/)。

同じ報告に出てくる 681,280 という数字は、2 つの異なるものを指している。ひとつは影響を受けたリクエストの prompt 長のしきい値、もうひとつは当時の prefill GPU の KV pool 容量だ。数値は同じでも役割は異なる。前者はどのリクエストが影響を受けるかを示し、後者は実行時に実際に保持できる量を示す。

この 2 つは別の問題を扱う。context length はモデル設定上リクエストが到達できる上限であり、KV pool は実行時に token の key/value 状態を割り当てて保持できるメモリプールだ。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 の根本原因説明](https://github.com/sgl-project/sglang/pull/38935)。

この状態の不一致は、ソースコードを直接見ると確認しやすい。以下は固定 commit からの**非連続の抜粋**で、省略した行は明示している。完全な文脈は [`set_finish_with_abort()`（L2053–2072）](https://github.com/sgl-project/sglang/blob/8f3d6944b76c428d884582cb6c82a39db1465360/python/sglang/srt/managers/schedule_batch.py#L2053-L2072)、[`finished()`（L1479–1481）](https://github.com/sgl-project/sglang/blob/8f3d6944b76c428d884582cb6c82a39db1465360/python/sglang/srt/managers/schedule_batch.py#L1479-L1481)、[`update_finish_state()`（L1842–1848）](https://github.com/sgl-project/sglang/blob/8f3d6944b76c428d884582cb6c82a39db1465360/python/sglang/srt/managers/schedule_batch.py#L1842-L1848) で確認できる。

```python
# 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 ページを予約する一方、prefill は 1-token placeholder だけを処理し、不足した KV 状態しか生成・転送しない。すると decode が、現在のリクエストによって上書きされていないページを読む可能性がある。Cloudflare は、実際の露出を以前のリクエストが残した KV データに帰属させている。[Cloudflare の報告](https://blog.cloudflare.com/kv-cache-exposure-remediation/) · [関連する上流 issue #30233](https://github.com/sgl-project/sglang/issues/30233)。

ここでは 3 種類の「完了」シグナルを混同しやすい。

- **HTTP 200** はクライアントが成功の HTTP ステータスを受け取ったことを意味する。レスポンスの意味が正しいことや、KV ページが現在のリクエストに属することまでは証明しない。
- **prefill completion / transfer-complete** は処理または受け渡しのステップが完了シグナルを出したことを示す。実際に転送された token 数が decode の予約した全ページをカバーしていることを自動的に保証するわけではない。
- **decode が完全かつ有効な KV pages を読むこと**が、実際のデータ条件により近い検査だ。「処理が完了したか」だけを確認し、実際の書き込み範囲とページ所有権を確認しなければ、「転送が終了した」を「データが完全で現在のリクエストに属する」と誤認する可能性がある。

フローが最後まで進んだことと、データが正しいことは同じではない。

## #36988、#38935、#38704 は同じ修正ではない

上流の記録は、時系列と役割を分けて読む必要がある。

| 日付 / 記録 | 状態 | 実際に対処したこと |
| --- | --- | --- |
| 2026-07-06、[issue #30233](https://github.com/sgl-project/sglang/issues/30233) | クローズ | PD disaggregation で長い prompt が 1 token だけ転送される類似問題の早期報告。Cloudflare の本番事故全体を独立再現したものではない。 |
| 2026-09-06、[PR #36988](https://github.com/sgl-project/sglang/pull/36988) | マージ済み。Cloudflare は v0.5.20 での公開に関連付けた | aborted prefill result の終了処理と cleanup を修正。送信待ち chunk 状態を消し、abort 結果を未完了キューへ戻さず、metadata/KV/HiCache/Mamba などのリソースを解放し、退役済みリクエストの遅延結果を無視する。これは正しさの欠陥への対処。 |
| 2026-09-16、[PR #38935](https://github.com/sgl-project/sglang/pull/38935) | マージ済み。Cloudflare は v0.5.21 での公開に関連付けた | まだ入場していない拒否リクエストを prefill と decode の admission 入口で早期に終了させ、不要な bootstrap、metadata buffer、sender/receiver 初期化、forward pass を避ける。作者は、これは資源浪費と遅延を減らす修正であり、#36988 に続く二度目の独立した correctness 修正ではないと明記している。 |
| 2026-09-09 作成、2026-10-10 クローズ、[PR #38704](https://github.com/sgl-project/sglang/pull/38704) | クローズ、未マージ。リリースなし | Cloudflare が提案した別の防御案。上流 mainline に入った修正として扱ってはいけない。 |

#38935 の説明は重要な時点を示している。#36988 を含む修正後の main では、decode は不足した handoff を送信しなくなった。上流 PR には 1P1D（2×H200）の前後比較もあり、#38935 のマージ後、decode は 300 秒待って timeout する状態から即座に 400 を返す状態へ変わり、bootstrap/metadata buffer は作成されず、stub batch は 1 から 0 になった。これは上流が示した比較であり、この記事で独自に実行した性能テストではない。

入口ガードを「**to_finish** が空でなければリクエストを終了する」と単純化できない理由もここにある。#38935 は **is_unadmitted_reject** で、まだ入場しておらず資源所有権を取得していない拒否リクエストを識別する。再入場や preemption 済みのリクエストは、すでに KV や metadata などのリソースを保持している場合があるため、abort マーカーだけを理由に早期破棄してはいけない。decode 側でも再入場経路を考慮する必要がある。リソース状態の一部が **Req** オブジェクトに残っていない場合があるからだ。早すぎる終了は、通常のスケジューリング経路が担う cleanup、たとえば streaming session の active slot detach（セッションが使用中の slot を占有する状態の解除）を飛ばす可能性がある。[SGLang PR #38935](https://github.com/sgl-project/sglang/pull/38935)。

後続には関連するものの別の防御もある。[PR #39500](https://github.com/sgl-project/sglang/pull/39500) は任意の handoff KV checksum を追加した。Cloudflare は KV canary（潜在的な破損を検出して記録する仕組み）と read-time page tagging も紹介している。後者は 2026 年夏にテストされたプロトタイプで、GLM-5.2 で試された後、信頼性の問題で停止された。特定の attention backend とモデル構造に依存するため、現在一般的に導入済みの防御ではない。Cloudflare は checksum と canary がすべてのモデル構造をカバーしておらず、GLM-5.3 も対応範囲を拡大中だと説明している。handoff checksum が検証するのは prefill から decode への転送であり、その後のすべての decode 読み取りではない。[PR #40645](https://github.com/sgl-project/sglang/pull/40645) と [PR #41402](https://github.com/sgl-project/sglang/pull/41402) は drain acknowledgement の欠落、またはすべての decode peer をカバーしない問題を共同で修正する。[PR #41404](https://github.com/sgl-project/sglang/pull/41404) は decode が開始した abort 以外の transfer failure にも deferred release を拡張し、進行中の RDMA write がページに触れる可能性がある間にページが解放・再利用されることを避ける。これらは KV page lifetime race に対処するもので、拒否リクエストが確実に不足 handoff に進む問題とは別の失敗経路だ。[Cloudflare の防御策とバージョン一覧](https://blog.cloudflare.com/kv-cache-exposure-remediation/)。

## 固定版 CPU 検証：コード経路を確認したのであり、テナント漏えいを再現したのではない

今回は事故報告と PR を読むだけでなく、検証を 3 層に分けた。それぞれが証明できる範囲は異なる。

| 証拠 | 観察結果 | どこまで裏付けられるか |
| --- | --- | --- |
| 自作 Python 合成状態機械 | handoff モデル 6/6、admission / re-entry モデル 9/9 | 構築した状態モデルのガードが期待どおりに動くこと。SGLang の実行時結果を示すものではない。 |
| 固定 SGLang commit の上流 CPU 単体テスト（P1） | 2 つのテストファイルで合計 19 passed。admission テストでは別途 12 subtests も通過 | 固定版の上流 mixin、queue、retire のコードを実行した。一部の transport/cache/scheduler 協力オブジェクトは Mock。実際の prefill/decode クラスターは動かしていない。 |
| 修正前後の差分（P2） | #38935 admission と #36988 prefill cleanup の両差分で動作の違いを観察 | 固定入力が 2 つのバージョンで異なるコード経路を通ること。CPU コードとプローブの結果に限られる。 |

P1 は SGLang commit **8f3d6944b76c428d884582cb6c82a39db1465360** に固定した（[固定ソース](https://github.com/sgl-project/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 VM、Python 3.12.3、pytest 9.2.0、CPU 版 torch。GPU、モデル重み、RDMA、テナントデータは使っていない。

P2 は複数の修正を混ぜないよう、各 PR の前後バージョンを分けて比較した。

- **実験 A：#38935 admission 入口（base [17ba2c2e](https://github.com/sgl-project/sglang/commit/17ba2c2e7c7b81f31a8a9e693e7435ab262c16b4) → head [8f3d6944](https://github.com/sgl-project/sglang/commit/8f3d6944b76c428d884582cb6c82a39db1465360)）。** 修正前は入口で拒否されたリクエストがキューに入り、sender/receiver の作成を引き起こした。修正後は入口で終了処理され、これらの handoff オブジェクトは作られなかった。有効なリクエストと metadata buffer を持つ再入場リクエストは両方のコードツリーで維持され、誤って退役したことは観察されなかった。
- **実験 B：#36988 prefill 結果 cleanup（base [9c254df8](https://github.com/sgl-project/sglang/commit/9c254df8cdbe99ef5f58e7fcd932da052f258538) → head [2ee5742a](https://github.com/sgl-project/sglang/commit/2ee5742af92db2727236460473554c2ee8c4ddaa)）。** 修正前は abort の最終結果が未完了キャッシュとして扱われ、inflight キューへ再投入されることがあり、退役済みリクエストの遅延結果が KV chunk の送信を引き起こす可能性も観察された。修正後は期待どおり cleanup が完了し、遅延結果はこれらの副作用を起こさなくなった。

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 の pre-fix で失敗した 7 ケースは、通常の transfer 経路へ進んだ際の **AttributeError** が 6 件、**ValueError** が 1 件だった。テスト stub が通常経路をモデル化していなかったために起きた例外で、7 件の業務アサーション失敗ではない。より重要なのは、再キューイング、cleanup、遅延送信という副作用が観察されたことだ。

### 公開した検証資料

2 本の差分プローブ、4 件の生 JSON 出力、P2 receipt、SHA-256 チェックサム一覧を[公開検証バンドル](/evidence/glm53-kv-cache-cross-request-exposure/README.md)にまとめた。2 組の base/head commit と Mock の範囲、CPU-only の制限を記録している。pre-fix の 0/7 は stub fall-through（`AttributeError` が 6 件、`ValueError` が 1 件）であり、7 件の assertion failure ではない。資料が示すのは固定バージョン間のコードパス差分であり、GPU/RDMA や実際のクロステナント漏えいの再現ではない。

## デプロイ側で確認すべき 4 つのゲート

マルチテナント推論サービスや SGLang の prefill/decode 分離構成を運用していると、「v0.5.22 に上げて、長い prompt を 1 つ送って HTTP 400 が返ったら終わり」と考えたくなる。しかし、設定やリソースのライフサイクルにはバージョン番号だけでは捉えられない経路がある。受け入れ確認は 4 つの独立したゲートに分けたい。

1. **Pool / context gate：設定と実行時容量は一致しているか。** worker 起動後、起動ログに実際に表示された KV pool 容量を context 設定と照合し、宣言された context 上限と実容量が一致しているか確認する。設定、GPU 分割、並行処理方針を変えたらログを再確認する。context 上限を超えていないのに現在の KV pool 容量を超える入力を負のケースとして用意し、handoff を続けず明示的に拒否されることを確認する。
2. **Reject / admission gate：拒否状態は入口で有効になるか。** 拒否リクエストが bootstrap、queue admission、metadata buffer 割り当て、sender/receiver 初期化より前に終了処理されるか確認する。新規リクエストだけでなく、有効なリクエスト、再入場、preemption、KV・Mamba・metadata リソースをまだ保持するリクエストもテストする。abort 状態のあるリクエストをすべて捨てると、元のリソース cleanup の責任を壊す可能性がある。
3. **Transfer / read gate：転送範囲は実際の読み取り範囲をカバーしているか。** 予約 token/page 数と、実際に書き込まれ転送された有効範囲を比較する。HTTP 200 や transfer-complete は長さと所有権の検証に代わるものではない。<code>--disaggregation-enable-kv-checksum</code> や <code>--kv-canary</code> を有効にする場合は、まず現在のモデルと backend が対応していることを確認し、ログから検証が実際に行われたことを確かめる。Cloudflare の公表時点では、これらは GLM-5.3 を含むすべてのアーキテクチャをカバーしていなかった。未対応のオプションを有効な防御として数えてはいけない。handoff checksum も decode の各読み取りを検証するものではない。
4. **Lifetime / ownership gate：失敗後、ページをいつ解放・再利用できるか。** abort、遅延結果、進行中の RDMA write に対する負のテストを行い、未完了の書き込みが再割り当てされたページを汚染しないこと、退役済みリクエストの遅延結果が chunk を再送しないこと、リソースが一度だけ解放されることを確認する。prefill と decode の両方で deferred KV release が有効か確認する。Mooncake では transfer batch 上限と release timeout が整合しているかも確認し、タイムアウトで進行中の書き込みが長く残らないようにする。[PR #41023](https://github.com/sgl-project/sglang/pull/41023) を含むバージョンでは、対応 backend で deferred release がデフォルト有効になっているが、実際のバージョンと設定で確認すべきだ。#40645 と #41402 は drain acknowledgement 経路、#41404 は他の transfer failure に対する遅延解放を扱う。これらのライフサイクル問題は、最初の決定的な handoff 欠陥とは分けて検証する。

SGLang の [v0.5.22 release](https://github.com/sgl-project/sglang/releases/tag/v0.5.22) は 2026-10-19 に公開された。Cloudflare は v0.5.22 以降への更新、または必要に応じた該当修正の backport を推奨している。ただし「バージョンを上げた」ことは「デプロイが隔離要件を満たした」ことを意味しない。実行中のコードバージョン、pool/context 設定、admission の負のケース、ページのライフサイクルを確認する必要がある。checksum などの機能が有効でなければ、実際に働く防御として数えてはいけない。

今回の事故から覚えておきたいのは、単一の Boolean が間違っていたという話ではない。拒否状態、データ完全性、リソース所有権は別々の境界だ。リクエストに拒否理由が記録されても admission がそれを認識するとは限らない。transfer が終了しても decode が読むすべてのページを現在のリクエストが書いたとは限らない。abort が発行されても、進行中の書き込みがすべて終わったとは限らない。それぞれを分けて検証してこそ、成功しているように見えるリクエスト経路をデータ隔離の証明と取り違えずに済む。

*テスト実施日：2026 年 10 月 28 日。*

## Verification context

- Level: source-inspected
- Verified: 2026-10-28T00:00:00.000Z
- Environment: Linux; AMD EPYC 9D25; 2 vCPU; 7 GB RAM
- Environment: Python 3.12.3; pytest 9.2.0; torch 2.14.1+cpu
- Evidence mode: mixed
- Public repository: https://github.com/sgl-project/sglang
- Evidence commit: 8f3d6944b76c428d884582cb6c82a39db1465360
- Evidence receipt: https://vxi.me/evidence/glm53-kv-cache-cross-request-exposure.json
