The Rejection Came Back 200

Cloudflare and the SGLang team published a coordinated disclosure today. Cloudflare’s post, by Kevin Flansburg and Michelle Chen, is titled “Cross-request KV cache exposure in the GLM-5.3 Workers AI model, and our remediation with SGLang,” and its second paragraph gives the scope: between August 27 and September 9, “a small number of customers using the GLM 5.3 Workers AI model received responses that contained data from other customers’ requests made to the same model shortly before theirs. In total, 155 responses were affected, all to prompts longer than 681,280 tokens.” The SGLang team’s companion post, “SGLang security update: improving request isolation,” covers the engine side.

Whether 155 is a small number matters less to me than where the checks were. Cloudflare says it found no evidence of deliberate exploitation and that the affected requests look like ordinary long-context use, and I have no basis to argue with that. Every layer of this stack had a way to say no, and each one said no in a form the next layer couldn’t hear. The one test that might have caught it was looking at the HTTP status.

Two numbers that were never compared

Cloudflare deployed GLM-5.3 with a context window of 1,048,576 tokens. On its prefill GPUs, the KV cache pool (the GPU memory that holds the model’s attention state for prompts in flight) had room for 681,280. The post says it plainly: “We were aware of this mismatch before launch.” The team expected the engine to return an error for any prompt it couldn’t fit, and its long-context tests came back successful, so nobody treated the gap as a problem.

The two numbers come from different places, which is how this happens. SGLang sizes the pool at startup from whatever GPU memory is left after the weights load. The context length is a separate configuration value. In Cloudflare’s words: “Nothing forces the two to agree, and it is our job to check that they do.” A change to quantization, tensor parallelism or CUDA graph sizes moves the pool. Nothing moves the context length unless someone edits it.

GLM-5.3 context window against the prefill KV pool, at launch and after the fix Three horizontal bars on one scale. The advertised context window is 1,048,576 tokens. The prefill KV cache pool at launch held 681,280 tokens, about 65 percent of the window. After the fix the pool holds about 2.04 million tokens, roughly twice the window. Prompts between 681,281 and 1,048,576 tokens, a band 367,296 tokens wide, fit the window but not the pool; all 155 affected responses were to prompts in that band. TOKENS · GLM-5.3 ON WORKERS AI · CLOUDFLARE, 26 OCT 2026 Context window Prefill pool at launch Prefill pool after fix 1,048,576 tokens, advertised context length — Cloudflare, 26 Oct 2026 681,280 tokens, prefill KV pool at launch — Cloudflare, 26 Oct 2026 About 2.04 million tokens, prefill KV pool after the fix — Cloudflare, 26 Oct 2026 1,048,576 681,280 ≈2.04M Fits the window, not the pool The shaded band is 367,296 tokens wide. All 155 affected responses were to prompts longer than 681,280 tokens.
About 35 percent of the advertised window could not physically be prefilled. Prompts that landed there were the only ones that leaked.

A mismatch like that is not, by itself, a security bug. A careful engine would turn it into a capacity limit: prompts past 681,280 tokens would fail with an error, a customer would complain that the advertised million-token window was really two-thirds of a million, and someone would fix the config. That is what SGLang does on a single node. The leak came from what the engine did with the rejection when prefill and decode run on separate machines.

A rejection that looked like a live request

Workers AI runs GLM-5.3 in disaggregated mode. One pool of GPUs reads the prompt and builds its KV cache (prefill), then ships that cache over RDMA, using a transfer engine called Mooncake, to a second pool that generates the reply (decode). The cache lives in fixed-size pages. Cloudflare describes those pages this way: “Pages are not currently zeroed between requests; the next request that gets a page simply overwrites it.” That is a deliberate choice for speed, and the post says it is common practice. Paged KV memory came into wide use with vLLM’s PagedAttention, which the SOSP 2023 paper describes as “inspired by the classical virtual memory and paging techniques in operating systems.” One habit of operating systems didn’t make the trip: handing a process a page that still holds another process’s data is something an OS treats as a security bug in itself. Here, whether a stale page could be read depended on bookkeeping.

The bookkeeping failed like this. When a prompt was too long for the pool, SGLang’s scheduler rejected it through a function called set_finish_with_abort. That function recorded the error for the client and swapped the prompt for a one-token placeholder, so the request could drain through the normal batch machinery without allocating memory. The rejection went into a field called to_finish. The admission queues on both prefill and decode, though, checked finished(), which reads a different field. A rejected request reported itself as live.

How a rejected prompt reached decode and read other users' pages Cloudflare's own worked example. The router tells decode the prompt needs 8 pages, and decode reserves pages P0 to P7. Prefill has already rejected the prompt and replaced it with a one-token placeholder, but its admission check still treats the request as live. Prefill runs one token, writes one page, P0, and signals that the transfer is complete. Decode accepts the handoff and attends over all 8 reserved pages. P1 to P7 still hold earlier requests' prompts and partial generations. The client receives HTTP 200 instead of HTTP 400, with a coherent reply that can contain other users' content. ONE OVER-LENGTH REQUEST · 8-PAGE EXAMPLE · CLOUDFLARE, 26 OCT 2026 Prefill node Decode node 1. Prompt too long for pool: rejected, swapped for a 1-token placeholder 2. Admission checks finished(), not the rejection: admitted 3. Prefills 1 token, writes 1 page, signals transfer complete Router said 8 pages, so decode reserved P0 to P7 P0, written by this request's 1-token prefill — Cloudflare, 26 Oct 2026 P1, stale: earlier request's data — Cloudflare, 26 Oct 2026 P2, stale — Cloudflare, 26 Oct 2026 P3, stale — Cloudflare, 26 Oct 2026 P4, stale — Cloudflare, 26 Oct 2026 P5, stale — Cloudflare, 26 Oct 2026 P6, stale — Cloudflare, 26 Oct 2026 P7, stale — Cloudflare, 26 Oct 2026 P0 P1P2P3 P4P5P6P7 written stale: other users' prompts and partial generations 4. Admission has the same finished() check; attention reads all 8 pages Client sees: HTTP 200, not 400. prompt_tokens matches the long prompt. Reply is coherent. Pages are reused without zeroing. Real requests span far more than 8 pages; the 8 is Cloudflare's illustration.
Prefill wrote one page and said it was done. Decode believed it and read seven pages that belonged to other people.

The router had told decode the prompt length, so decode reserved pages for the whole prompt. Prefill ran a forward pass on the one-token placeholder, sent one page and reported the transfer complete. Decode had the same finished() check, accepted the handoff, and generated a reply while attending over every page it had reserved. Only the first had been written by this request. The others still held what earlier requests had left in them.

Cloudflare’s summary of what the client saw: “The client got HTTP 200 instead of HTTP 400. prompt_tokens matched the long prompt and the reply was coherent.” It also arrived faster than an eight-page prefill should take, which is the only external sign I can find in the description.

The test that passed

Cloudflare’s own explanation of why the known mismatch shipped: “we only validated HTTP response codes and engine performance at long contexts, without validating model output and coherence.”

For most software that is a reasonable test. A 200 means the server accepted the request, and you check the body separately when the body is the point. In model serving the body is always the point, and a 200 tells you nothing about which tokens the model attended to. The response object had every field a monitor would look at. The status was right. prompt_tokens matched what the client sent, even though prefill had processed one token. The text read naturally. As far as I can see from the post, the anomaly was latency.

I’ve written before, in the PHP disclosure piece, about how much of a security incident is timing. This one is mostly about the shape of an error. The engine did decide the request was invalid. That decision was stored where only the single-node path read it, and everything downstream saw a request that had finished normally.

The same bug, filed as garbage

Cloudflare says the bug “had been reported to SGLang by another user as issue #30233 on July 6, 2026,” which it found while investigating. I opened the issue. The mechanism is the same one: in disaggregated mode an over-length request goes through set_finish_with_abort, its input is cut to one token, only one token of KV is sent, and decode “starts from 284936 uninitialized KV cache slots.” The reporter’s description of the outcome was “producing garbage output.” Their title calls it garbage too.

That word explains most of the incident to me. On a lightly loaded node, or one where the stale pages hold nothing useful, this bug produces nonsense, and nonsense gets filed as a correctness bug. On a busy multi-tenant service where the pool keeps cycling through other customers’ long prompts, the same stale pages hold readable text, and the model, being a good language model, turns them into a fluent answer. The code path was identical in July and August. The contents of memory were what changed.

From the July bug report to the October disclosure A time axis from 6 July to 26 October 2026. 6 July: SGLang issue 30233 reports the over-length abort bug in disaggregated mode as producing garbage output. 27 August: GLM-5.3 launches on Workers AI with the exposure present, shown as a shaded band that runs to 9 September. 6 September: the upstream fix, pull request 36988, merges to SGLang main, not yet in a release. 8 September at 23:11 UTC: the researcher's report arrives through HackerOne. 9 September: incident declared at 13:22 UTC, model set private at 14:00, restored at 19:40 with a corrected configuration and a patched engine. 18 September: SGLang v0.5.20 ships the fix. 2 October: v0.5.21. 19 October: v0.5.22, the version Cloudflare recommends. 26 October: coordinated disclosure. 6 JUL TO 26 OCT 2026 · CLOUDFLARE, SGLANG, GITHUB 6 Jul: issue #30233 opened — GitHub 27 Aug: GLM-5.3 launches, exposure present — Cloudflare 6 Sep: PR #36988 merged to main — GitHub 8 Sep 23:11 UTC: HackerOne report — Cloudflare 9 Sep 19:40 UTC: model restored — Cloudflare 18 Sep: SGLang v0.5.20 released — GitHub 2 Oct: SGLang v0.5.21 released — GitHub 19 Oct: SGLang v0.5.22 released — GitHub 26 Oct: coordinated disclosure — Cloudflare, SGLang 6 Jul Issue #30233: "garbage output" 27 Aug GLM-5.3 launches 8–9 Sep Report; offline 14:00, back 19:40 6 Sep Fix #36988 merged to main 18 Sep v0.5.20 ships the fix 2 Oct v0.5.21 19 Oct v0.5.22 26 Oct Shaded: the 13-day exposure window, 27 Aug to 9 Sep. Release dates from GitHub; times in UTC.
The bug was filed 52 days before launch, and the fix was on main two days before the report arrived. It still took a security incident to get it into a release.

The timeline has an awkward middle. PR #36988, the change Cloudflare’s table of upstream fixes credits with stopping this exposure, merged on September 6, two days before the researcher, Gadi Evron of Knostic, reported the leak through Cloudflare’s HackerOne program. It was titled “[VLM] Retire aborted disaggregated prefill results,” a fix for a vision-model path, and it wasn’t in a release. v0.5.19 had shipped the day before. The fix reached users in v0.5.20 on September 18, nine days after Cloudflare restored the model with its own patch. Cloudflare’s response time was fast: incident declared at 13:22 UTC on September 9, model private at 14:00, restored at 19:40. Most of the calendar on this incident sits before the report.

What the literature was looking at

I went looking for academic work on cross-user leakage through shared KV cache. Most of what exists is about timing. “The Early Bird Catches the Leak,” by Linke Song and colleagues in IEEE Transactions on Information Forensics and Security, describes timing side channels “arising from shared caches and GPU memory allocations” that let an attacker infer other users’ prompts by measuring whether a request hits the cache. That threat model assumes isolation holds and asks what leaks around it, with prompts recovered through a token-by-token search.

The Cloudflare incident needed no probing, no classifier and no attacker. An ordinary customer sending a long prompt got other customers’ text back in the response body. The academic framing treats shared KV state as a channel that leaks through side effects. This incident shows the engine’s own page accounting can be the direct path, and the SGLang post says the related race bugs behave the same way: “Each occurrence was silent.”

Where I would hold back

Three things I can’t check. The count of 155 responses and the conclusion that none of it was deliberate exploitation come from Cloudflare’s logs, which nobody outside can audit; the post says the counts exclude its own testing and the researcher’s. The researcher’s own write-up is listed by SGLang as “available soon,” so the discovery side of the story is still one-sided. And the “about 2.04 million tokens” figure for the new pool is Cloudflare’s, stated once, with no log line shown.

I’d also be careful about reading this as a story about SGLang being unsafe. The engine had a real bug, but the condition that made it reachable at scale was a configuration Cloudflare chose knowingly, and Cloudflare says so. Every other disaggregated model on Workers AI had pools larger than its context window and couldn’t reach the rejection path.

What I take from it

The remediation list is long and mostly good: admission-time rejection, per-transfer checksums behind --disaggregation-enable-kv-checksum, deferred page release, a draft proposal to zero pages on release, and a plan to model the RDMA transfer protocol formally. The part I’d most like to see finished is the one Cloudflare already built and switched off. Over the summer it prototyped page tags that change when a page is reallocated and are checked before decode reads, and reported that “In our tests it cost under 1% in throughput and tail latency.” Cloudflare tested it on GLM-5.2 in mid-August and disabled it shortly afterwards “due to reliability issues,” not long before GLM-5.3 launched on August 27.

The first item on Cloudflare’s advice to other operators is the cheapest: compare the KV pool size in your startup log against your context length, on every node, after every change to weights, quantization, parallelism or memory fraction. The post calls it “the check we did not enforce.” I’d add one more. Long-context tests should assert on the content of the reply, ideally with a canary string planted in an earlier request, because a 200 with a fluent body was exactly what the failure looked like.


References

  1. Flansburg, K. & Chen, M. (2026). Cross-request KV cache exposure in the GLM-5.3 Workers AI model, and our remediation with SGLang. Cloudflare Blog, 26 October 2026.
  2. SGLang Team (2026). SGLang security update: improving request isolation. LMSYS Org blog, 26 October 2026.
  3. sgl-project/sglang (2026). Issue #30233: PD disaggregation: aborted prefill requests (input too long) cause decode to generate garbage from incomplete KV cache. Opened 6 July 2026, closed 5 September 2026.
  4. sgl-project/sglang (2026). PR #36988: [VLM] Retire aborted disaggregated prefill results. Merged 6 September 2026.
  5. sgl-project/sglang (2026). PR #38935: [PD] Do not admit intake-rejected requests to a PD handoff. Merged 16 September 2026.
  6. sgl-project/sglang (2026). Releases: v0.5.19 (5 September), v0.5.20 (18 September), v0.5.21 (2 October), v0.5.22 (19 October 2026).
  7. Kwon, W., Li, Z., Zhuang, S., Sheng, Y., Zheng, L., Yu, C. H. et al. (2023). Efficient Memory Management for Large Language Model Serving with PagedAttention. Proceedings of the 29th Symposium on Operating Systems Principles (SOSP), 23 October 2023.
  8. Song, L., Pang, Z., Wang, W., Wang, Z., Wang, X., Chen, H. et al. (2025). The Early Bird Catches the Leak: Unveiling Timing Side Channels in LLM Serving Systems. IEEE Transactions on Information Forensics and Security, 2025.
  9. Gautam Parab (2026). Five Finders, One Fix, and a Honeypot Hit on the 17th. 22 October 2026.