GLM-5.3 Workers AI 模型中的跨请求 KV 缓存暴露问题,以及我们借助 SGLang 的修复

Cloudflare Blog 2026-10-27T06:01:23+08:00

2026 年 9 月 8 日,Knostic 的安全研究员 Gadi Evron 向我们报告了一个租户隔离问题,影响 GLM-5.3 Workers AI 模型(@cf/zai-org/glm-5.3),问题最早出现在 2026 年 8 月 27 日。我们已修复该问题,并于 9 月 9 日恢复了模型服务。我们确认 Workers AI 上托管的其他模型均未受影响。8 月 27 日至 9 月 9 日期间,少数使用 GLM 5.3 Workers AI 模型的客户收到的响应中,包含了其他客户在此前不久向同一模型发起请求时的数据。共有 155 条响应受到影响,全部来自长度超过 681,280 个 token 的提示词。排除我们自己的测试和研究人员的测试后,我们识别出的请求在时间和数量上与普通的长上下文使用一致,而不是有人反复利用该漏洞。我们没有发现任何故意利用的证据。

今天,我们发布这份报告,同时 SGLang(我们使用的开源推理引擎)也发布了一篇文章,作为协同披露。本文介绍导致该问题的漏洞、我们的响应时间线,以及随后对生产环境 SGLang 部署所做的加固。我们还详细说明了一直以来与 SGLang 上游维护者合作解决此问题及类似漏洞的工作。

发生了什么

GLM 是 Z.ai 推出的文本生成模型,专为智能体编程(agentic coding)工作流设计,是 Workers AI 提供的众多模型之一。Gadi Evron 于 9 月 8 日通过我们的 HackerOne 项目提交报告,称发送给 Workers AI 上 @cf/zai-org/glm-5.3 的超长提示词有时会返回属于其他用户会话的文本。Cloudflare 在次日上午复现了该问题,并在宣布事件后 40 分钟内将模型下线。随后我们部署了修正后的配置和打过补丁的推理引擎,并于当天恢复服务。

这份关于跨请求 KV 缓存暴露的具体报告,是两处问题叠加的结果。第一个问题是 Cloudflare 使用的开源推理引擎 SGLang 配置错误。

我们部署 GLM 5.3 时,给了它 1,048,576 token 的上下文窗口,但预填充 GPU 上的 KV 缓存池只能容纳 681,280 token。上线前我们就知道这个不匹配。我们以为引擎遇到处理不了的提示会直接报错,而且长上下文测试都返回成功,所以没觉得这是个问题。第二个问题出在 SGLang 处理超长请求的方式上:在分离式预填充/解码(disaggregated prefill/decode)模式下,放不进池子的提示会被内部标记为拒绝,并替换成一个 token 的占位符。但预填充并没有中止请求、返回客户端错误,而是给这个占位符提示生成了 KV 缓存,然后传给解码节点。解码节点收到“传输完成”消息,以为预填充已经用当前请求的完整提示覆盖了之前请求的 KV 缓存,实际上大部分旧状态原封不动。解码节点随后生成响应时,注意力落在那些仍然存着早期请求数据的旧缓存页上。结果是,超过 681,280 token 的提示,其响应可能混入其他用户 GLM 5.3 会话的数据。

立即修复

禁用该模型后,我们在恢复公开访问之前采取了以下措施来修复这个漏洞:

修复配置:让缓存池装得下完整上下文。 我们把 GLM 5.3 的配置调整成和 GLM 5.2 一致,释放出足够的 GPU 内存,预填充 KV 池现在能容纳约 204 万 token,大约是 1,048,576 token 上下文长度的两倍。这样,能放进上下文窗口的提示一定放得进池子,合法请求永远不会走到拒绝路径。仅这一项就能让这个 bug 不再被触发。

修复引擎:让拒绝路径本身安全。 只靠池子大小很脆弱:量化方式、张量并行、CUDA 图大小,或者未来某个模型版本,任何一项变动都可能悄悄改变池子大小。而且,预填充调度器还可能在其他情况下拒绝请求,触发同样的 bug。

为防止这种情况,我们还修改了引擎:携带待处理拒绝(pending rejection)的请求,会在分离式部署的准入队列阶段就带着原始错误退出,不会进行任何 bootstrap、内存分配或 KV 传输。我们把这一改动作为 #38704 提交到上游,同时让 decode 节点校验实际传输的 token 数量与它预留的数量是否一致。相关修复此前已在 #36988 进入 SGLang 主分支(9 月 6 日合并),针对的是一条相关的失败路径,但尚未发布正式版本。维护者还合并了 #38935,按我们的思路更早地拒绝这类请求。在恢复模型之前,我们用研究者的复现脚本和我们自己的金丝雀标记测试(canary-marker)验证了这两处改动。

背景:我们如何提供服务

GLM 5.3 Workers AI 采用分离式部署(disaggregated configuration)运行 GLM 5.3:一组 prefill GPU 处理提示词,生成对应的 KV 缓存,再通过 RDMA(远程直接内存访问)经由 Mooncake 把缓存交给一组 decode GPU,由后者生成回复。我们之前在高性能 LLM 以及更小、更快、更安全的模型这两篇文章里介绍过这套架构。

GPU 内存中的 KV 缓存是一个共享池,由固定大小的内存页(page)组成。每个请求在准入时分配到若干页,完成时释放。目前页在请求之间不会清零,下一个拿到该页的请求直接覆盖写入。这种做法在推理引擎中很常见,也是达到最佳性能所必需的,但代价是:推理引擎必须自己跟踪这些内存页的归属。

KV 缓存页的归属与生命周期跟踪,要处理的不只是节点内的共享归属(KV 缓存卸载和 token 生成都会访问这块内存,但生命周期不同),还要处理与远端 prefill 节点在技术上共享的归属——远端节点同时持有该页的可变引用。此外,这个池的大小并不是一个配置项。SGLang 在启动时按加载完模型权重后剩余的 GPU 内存自行确定池的大小。模型对外宣称的上下文长度则是另一个独立的配置值。

没有任何机制强制两者保持一致,而检查它们是否一致正是我们的职责。

问题一:配置错误

在我们的配置下,GLM 5.3 的 prefill 节点上,KV 池大小为 681,280 个 token,而模型的上下文长度是 1,048,576 个 token。SGLang 在启动时会把选定的 KV 池大小打印到日志里,也会把它作为指标暴露出来。我们当时就知道这个大小不匹配;但在上线前的测试中,压测时我们看到 HTTP 状态码正常、流式响应体没有错误帧。于是我们假设引擎能通过流式或其他机制,优雅地处理 KV 缓存池与请求上下文窗口之间的差异。这是测试中一个关键盲区:我们只验证了 HTTP 响应码和长上下文下的引擎性能,没有验证模型输出和连贯性。现在,在任何分离式(disaggregated)部署变更上线前,我们都会直接从运行中的引擎检查 KV 池大小与上下文长度是否匹配,而不是从响应码去推断。

问题二:SGLang 的 bug

在 v0.5.21 中,SGLang 团队已经修复了处理超长 prompt(超过 KV 池大小)时的这个 bug。先说说之前发生了什么。

当一个 prompt 到来、长度超过 KV 池容量时,SGLang 的调度器会拒绝它。拒绝路径(Req.set_finish_with_abort)会记录要返回给客户端的错误,并把 prompt 换成单 token 的占位符,这样请求就能走正常的批处理流程排空,而不会分配真实内存。在单节点部署上这没问题:占位符跑完,批次结束,客户端收到错误。

但在我们运行的这种分离式模式下,prefill 和 decode 两边的准入队列检查的是请求是否 finished,而不是是否被拒绝。一个被拒绝的请求会把自己报告为仍然存活。于是:

被拒请求把拒绝状态写进了 to_finish 而不是 finished_reason,导致 finished() 依然返回 False。于是这个被拒请求被准入。预填充前向计算。拒绝逻辑已经把 prompt 替换成了一个单 token 占位符。预填充就在这个单 token 上跑了一次几乎空转的前向计算。KV 传输。预填充把一页 KV 发给解码端,并通知传输完成。解码准入。解码端的准入队列有同样的 finished() 检查,所以它接受了这次交接。解码注意力。解码开始生成,注意力读取了全部 8 个预留页。这个请求只写过 P0 页。P1 到 P7 页仍然是之前请求留下的内容:其他用户的 prompt 和部分生成结果。响应。客户端收到的是 HTTP 200 而不是 HTTP 400。prompt_tokens 与长 prompt 匹配,回复也连贯。它比 8 页预填充本应花费的时间更快到达,而且其中含有其他用户 KV 页的内容。每一个超过阈值的请求都是这个表现。

这个 bug 已由另一位用户于 2026 年 7 月 6 日作为 issue #30233 报告给 SGLang,Cloudflare 在调查本次事故时注意到了它。我们自 v0.5.10 起就一直在向 SGLang 反馈这类问题,这次安全事件加快了 Cloudflare 与 SGLang 团队已经启动的合作工作。该问题的补丁在 SGLang v0.5.20 中发布,后续修复在 v0.5.21 和 v0.5.22 中跟进,所有依赖 SGLang 的运维方均可使用。我们强烈建议所有运维方将 SGLang 依赖升级到 v0.5.22 或更高版本。具体 PR 的更多细节见下文。

GLM 5.3 是唯一受此 bug 影响的模型。Workers AI 上其他所有分离式部署的 KV 池在预填充和解码两端都大于其上下文长度,因此没有请求能走到拒绝路径。我们已对用 SGLang 服务的每个模型应用了引擎修复和下文所述的加固措施,并且我们的部署检查要求每个新部署都必须包含这些措施。

时间线

所有时间均为 UTC。

时间 事件
2026 年 8 月 27 日 GLM 5.3 在 Workers AI 上线,其预填充 KV 池小于上下文窗口。暴露问题从上线起就存在。

2026 年 9 月 8 日 23:11 通过 HackerOne 收到报告。
2026 年 9 月 9 日 08:00 Cloudflare 开始核实该 HackerOne 报告。
2026 年 9 月 9 日 13:22 宣布进入安全事件状态。产品安全团队用两个独立账号在 25 分钟内复现了泄漏。
2026 年 9 月 9 日 14:00 确认影响范围后,GLM 5.3 被设为私有,请求一律返回 HTTP 400,状态页同步更新。
2026 年 9 月 9 日 14:35 定位根因:prefill 池小于上下文长度。换用更大池的 canary prefill 节点后,确认不受影响。
2026 年 9 月 9 日 14:52 所有 prefill 节点滚动更新到修正后的配置。
2026 年 9 月 9 日 19:04 完整部署重建,使用修正后的配置,并加上由 Cloudflare 编写的引擎补丁:被 prefill 调度器拒绝的请求现在能正确返回错误。已用研究人员的复现脚本验证通过。
2026 年 9 月 9 日 19:40 模型恢复为公开可用。
2026 年 9 月 10 日 23:30 与 SGLang 维护者(RadixArk CTO 及核心工程师)开会,就上游修复方案达成一致。
2026 年 9 月 11–19 日 对全部生产环境 SGLang 部署追加加固(详见下文)。
2026 年 9 月 16 日–10 月 12 日 上游 SGLang 合并了准入修复、逐次传输校验和、abort-ACK 保留,并将延迟释放 KV 设为默认开启。
2026 年 10 月 2 日 SGLang v0.5.21 发布。
2026 年 10 月 19 日 SGLang v0.5.22 发布。
2026 年 10 月 26 日 与研究人员及 RadixArk 协调后对外公开。

持续的加固工作

这个 bug 属于一类 KV 缓存内存安全问题,Cloudflare 已经为此投入了数月。这类问题的共同特征是:某个页(page)被释放、复用或读取时,还有其他东西仍然对它持有使用权。

我们观察到两种常见成因。

第一种是单节点内的页生命周期问题。decode 节点上的多个任务可能同时持有同一个页:token 生成、卸载到主机内存、KV 传输接收端,以及 abort 和错误处理路径。如果其中一个任务释放了页,而另一个还在使用,新请求就可能拿到这个仍在被读写的页。我们此前针对 decode 卸载(#24226、#30899)和语法错误导致重复释放(#30937)的修复,都属于这一类。

第二个是跨节点的竞争条件。请求中止时,prefill 还在通过 RDMA 向 decode 写 KV 缓存,已发出的写入收不回来。如果 decode 立刻释放这些页面,页面就可能分给另一个请求,迟到的写入便落进那个请求的 KV 缓存。Cloudflare 也参与了修复,提交了修复提案(#31357、#38961)和复现脚本(#32564)。这两种情况的结果都是输出损坏,或者一个用户的数据出现在另一个用户的响应里,且不报任何错误。这次事件的 bug 不一样:被拒绝的请求每次都会走完交接流程,不是碰巧撞上时序问题。但结果相同:decode 读到了这个请求从未写过的页面。现有修复都没覆盖这次事件的失败情形——prefill 拒绝了请求,decode 却完全不知情。

事件发生后,我们做了这些事:

这些变更都已发布到我们的分离式生产部署。

与 SGLang 团队合作

9 月 10 日,也就是事件发生后的第二天,我们与 RadixArk 的 SGLang 维护者会面。双方商定:由 RadixArk 调查我们报告的竞争条件,把完整性检查的思路推到上游,并为这类 bug 构建复现测试。

以下补丁已合入 SGLang 主分支,用于处置本次事件以及相关的 KV cache 竞态问题。除一项外,其余都已进入 v0.5.22。运行分离式(disaggregated)部署的运维人员应升级到 v0.5.22,或按需回移这些补丁。

类别 PR 版本 作用
事件修复 #36988 v0.5.20 在被中止的 prefill 请求结果送达 decode 之前就将其回收,从源头切断了本次事件中的泄露路径。
事件修复 #38935 v0.5.21 在准入阶段直接拒绝这类请求,此时还没做任何 bootstrap、资源分配或 KV 传输。思路与 Cloudflare 的草案补丁 #38704 一致。
竞态 #39461、#41612 v0.5.21、v0.5.22 客户端断连或 prefill 失败时,sgl-router 会同时取消请求的两半(prefill 侧与 decode 侧)。
竞态 #37454、#41023 v0.5.20、v0.5.21 只在支持延迟释放 KV 的后端上启用该能力,随后再默认打开。
竞态 #40645、#41402 v0.5.21 修复 drain 确认丢失的情况:改为收到确认即释放,而不是一直等到超时。
竞态 #41404 v0.5.21 任何传输失败都会保留页面,不再只覆盖 decode 主动中止的场景。
竞态 #41618 v0.5.21 修复中止通知路径上的一处回归问题。
竞态 #43719 v0.5.22 Mooncake 发送失败时,不再把自己的写入误报为已 drain。
竞态 #37077 v0.5.22 给中止消息打上请求标识,避免 ID 复用时消费到过期的确认;同时新增 MORI 支持。
竞态 #36612 v0.5.20 让 prefill 到 decode 的失败通知在 Mooncake、NIXL 和 MORI 之间共用同一套机制。
竞态 #32758 v0.5.20 为 Mooncake 传输批量大小加上可选上限,避免一次调用超时后仍有数量不受限的写操作在途。
生命周期 #30899 v0.5.21 decode 在撤回请求、释放页面之前,会先等待正在进行的 host memory(主机内存)卸载完成。该补丁由 Cloudflare 提交。
生命周期 #43562 v0.5.22 host memory 缓存恢复失败时,保证各 tensor-parallel rank 的状态仍然一致。
可观测性 #39500 v0.5.21 新增 --disaggregation-enable-kv-checksum:decode 侧在生成前先校验收到 KV 的校验和。相关清理工作在 #39729,已合并、等待发版。
可观测性 #38596、#40642 v0.5.20、v0.5.21 修复 SGLang 的 KV canary(用于发现并记录潜在数据损坏的探针)。

可观测性

下面这些后续工作仍在进行中:

我们给其他运维者的建议

如果你在分离式(disaggregated)prefill/decode 模式下使用 SGLang,我们建议:

  1. 量一下你的 KV pool,再和 context length 对比。 在每台 prefill 和 decode 节点上,每次改动权重、量化、并行度或显存比例之后,都从引擎启动日志里读出 KV pool 的大小。如果 pool 比 context length 小,就把 context length 降到装得下为止。不要指望代理层去兜这个限制,也不要假设引擎会拒绝装不下的请求——这正是我们当初没有落实的那道检查。
  2. 升级到包含 #36988 和 #38935 的构建(v0.5.22 或更新版本),这样超长请求会在交接之前就被拒掉,与 pool 大小无关。这是引擎侧的修复,也是第一步检查出错时的兜底。
  3. 在两个角色上都开启延迟 KV 释放(#41023 之后为默认行为),并给 Mooncake 的传输批次设上限,避免一次调用超时后留下一长串没有边界的写操作。
  4. 在模型支持的前提下开启损坏检测。 --disaggregation-enable-kv-checksum 和 --kv-canary 目前还没覆盖所有模型架构,需要查看日志确认请求确实在被校验。我们正与 SGLang 团队合作,把这两项检查扩展到更多架构,包括 GLM 5.3、GLM 5.3 Flash 和 DeepSeek V4。
  5. 确认你的代理没有产生多余的 abort。 配置负载均衡器,让请求能一路走到生成第一个 token,这样就不会触发需要跨节点协调的 abort。

更长远:构建即完整

配置检查和生命周期修复,减少了 KV 页被误用的途径。

这些措施并不能杜绝滥用。这次事故也提醒我们:下一个 bug 会长成我们没见过的样子。真正能长期奏效的做法,是让引擎在读取时就能发现——某个请求即将访问一块不属于它的 KV 页,然后只让这一个请求失败,而不是把别人的数据返回回去。今年夏天我们做了一个原型,并在 8 月的文章里介绍过。具体做法是:每个物理 KV 页都带一个标签,页面被重新分配时标签随之改变;每个请求记录自己期望的标签;解码阶段读取缓存之前先校验这层映射。在我们的测试里,吞吐量和尾延迟的损耗都不到 1%。8 月中旬我们在 GLM 5.2 上测过它,但不久后因为可靠性问题关掉了。这个原型还绑定了特定的注意力后端和模型架构,因此无法作为通用修复方案。我们正在和 SGLang 团队合作,把这个机制做进引擎的生产版本,让它对所有模型都生效,而不是靠 Cloudflare 自己维护补丁。#39500 里那个按每次传输计算的校验和是第一步:它校验从 prefill 到 decode 的交接过程。页级标签则更进一步,因为它能检查每一次读取,包括传输结束很久之后的读取。

靠数学上的不可能性来保证完整性

RDMA 会把数据从一台机器的内存直接拷贝到另一台机器的内存。这就要求两台机器对「数据写到哪、有多大、什么时候写完」达成一致,任何不一致都可能让接收方读到错误的数据。这次的 bug 就是一个分布式系统一致性故障的例子:两台机器对系统状态的认知并不相同。

在 Cloudflare 的其他项目里,我们一直在用形式化验证(formal verification)来证明分布式系统的安全性。Topaz 让我们的权威 DNS 服务器能够无冲突地选择 IP。对 Meerkat 中 QuePaxa 实现的建模,曾暴露出一些一致性 bug,这些 bug 后来都被修复了。现在,我们正把在形式化方法上的经验用到 KV 缓存传输引擎上。第一步是对一套基于 RDMA 的 KV 缓存传输协议进行建模,并证明其安全性。

这样做的目的,是用 Mooncake 搭建一套模型,模拟生产环境中 SGLang 的分布式 KV 缓存系统,然后追踪并修复任何偏离预期安全属性的地方。目标是尽可能证明:这类漏洞不会再发生。现实中,证明器能给出的结论必然存在盲区,但只要把系统的其余部分证明清楚,我们就能更细致地审查和监控这些盲区。

协调披露

本报告属于一次协调披露。我们要感谢研究员 Gadi Evron——他通过我们的 HackerOne 项目提交了清晰、可复现的脚本,并给了我们修复问题、与 SGLang 维护者协调的时间。也要感谢 RadixArk 的 SGLang 团队,他们把这当成自己的问题,并迅速完成了上游修复。更多内容可查看 SGLang 团队的博文。

如果你在 Workers AI 中发现安全问题,请通过 hackerone.com/cloudflare 报告。

Workers AI 上 GLM 5.3 的一处配置不匹配触发了 SGLang 的一个 bug,导致 KV 缓存页跨请求暴露。以下是我们处理该问题的方式。

查看原文