Cloudflare 披露 GLM-5.3 Workers AI 跨请求 KV cache 泄露事件
Cross-request KV cache exposure in the GLM-5.3 Workers AI model (155 responses)
Cloudflare 披露 GLM-5.3 Workers AI 模型存在跨请求 KV cache 泄露,2026 年 8 月 27 日至 9 月 9 日期间共 155 条响应受影响,均来自超过 681,280 token 的提示词。
Cloudflare 复盘了 GLM-5.3 跨请求 KV cache 泄露的成因与修复,并给出 SGLang 运维方可直接照做的检查项。
2026 年 9 月 8 日,我们收到安全研究员、来自 Knostic 的 Gadi Evron 的提醒,得知自 2026 年 8 月 27 日起,GLM-5.3 Workers AI 模型(@cf/zai-org/glm-5.3)存在租户隔离问题。我们已修复该问题,并于 9 月 9 日恢复了该模型。我们已确认 Workers AI 上托管的其他模型均未受到影响。
在 8 月 27 日至 9 月 9 日期间,少数使用 GLM 5.3 Workers AI 模型的客户收到的回复中,包含了其他客户在此前不久向同一模型发出的请求中的数据。共有 155 条回复受到影响,且全部针对长度超过 681,280 个 token 的提示词。
在排除我们自身测试和研究人员的测试后,我们识别出的请求在时间和数量上与普通的长上下文使用情况一致,而非有人反复利用该漏洞。我们未发现任何故意利用的证据。
今天,我们与我们所使用的开源推理引擎 SGLang 的一篇博文协同披露,发布这份报告。 该博文描述了导致此问题的漏洞、我们的响应时间线,以及我们随后对生产环境 SGLang 部署所做的加固工作。我们还详细介绍了我们与 SGLang 上游维护者持续合作,以解决此漏洞及类似漏洞的情况。
发生了什么
GLM 是来自 Z.ai 的文本生成模型,专为智能体编程工作流而构建;它是 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 在分离式预填充/解码模式下处理由此产生的超长请求时存在的漏洞。无法放入缓存池的提示词会在内部被标记为已拒绝,并替换为一个单 token 占位符,但预填充并没有中止请求并返回客户端错误,而是为占位符提示词生成了 KV 缓存,并将其传输到解码节点。解码节点收到“传输完成”消息后,便假定预填充已用此请求的完整提示词覆盖了先前请求的 KV 缓存,而实际上它保留了先前状态的大部分内容。解码节点随后在关注仍保留着先前请求数据的过期缓存页的同时生成了响应。因此,对长度超过 681,280 个 token 的提示词的响应可能包含其他用户 GLM 5.3 会话中的数据。
立即修复
在禁用该模型后,我们采取了以下措施来修复此漏洞,然后才恢复对其的公开访问:
为了修复我们的配置,我们让池持有完整的上下文。 我们将 GLM 5.3 的配置调整为与 GLM 5.2 的配置一致,从而释放了足够的 GPU 内存,使 prefill KV 池现在可容纳约 204 万个 token,大约是 1,048,576 token 上下文长度的两倍。现在,适合上下文窗口的提示词总能放入池中,因此对于有效请求永远不会走到拒绝路径。仅此一项就能防止该 bug 被触发。
为了修复引擎,我们还让拒绝路径变得安全。 仅依赖池大小是脆弱的:量化、张量并行、CUDA 图大小或未来模型版本的任何变化都会以容易遗漏的方式改变池大小。此外,可能还有其他情况会使 prefill 调度器拒绝请求并触发相同的 bug。为防止这种情况,我们还修补了引擎,使携带待处理拒绝的请求在 disaggregation 准入队列处、在任何 bootstrap、分配或 KV 传输发生之前,带着其原始错误被退役。我们将此作为 #38704 向上游提出,该补丁还让 decode 节点验证传输的 token 数量与其预留的数量匹配。该修复已在 SGLang main 的 #36988 中,于 9 月 6 日针对相关失败路径合并,但尚未发布。维护者还合并了 #38935,该补丁遵循我们的方法更早地拒绝这些请求。
在恢复模型之前,我们使用研究人员的复现脚本和我们自己的金丝雀标记测试验证了这两项更改。
背景:我们如何服务 GLM 5.3
Workers AI 以 disaggregated 配置使用 SGLang 运行 GLM 5.3:一组 prefill GPU 处理提示词并为其生成模型的 KV 缓存,然后通过 Mooncake 以远程直接内存访问(RDMA)方式将该缓存交给一组 decode GPU,由后者生成响应。我们之前已在关于高性能 LLM和更小、更快、更安全的模型的文章中介绍过这种架构。
GPU 内存中的 KV 缓存是一个由固定大小页面组成的共享池。每个请求在被准入时分配页面,并在完成时释放它们。目前请求之间不会将页面清零;下一个获得页面的请求会直接覆盖它。这种方法在推理引擎中很常见,并且是实现最佳性能所必需的,但这意味着推理引擎负责跟踪这些内存页面的所有权。KV 缓存页面的所有权和生命周期跟踪不仅要处理节点内的共享所有权(KV 缓存卸载和 token 生成都以不同的生命周期访问内存),还要处理技术上与远程 prefill 节点共享的所有权(该节点同时持有该页面的可变引用)。
此外,此池的大小不是配置值。SGLang 在启动时根据加载模型权重后剩余的 GPU 内存来确定其大小。模型宣称的上下文长度是一个单独的配置值。没有任何机制强制两者一致,检查它们是否一致是我们的职责。
问题一:配置错误
在我们配置的 GLM 5.3 prefill 节点上,KV 池达到 681,280 个 token,而模型的上下文长度为 1,048,576 个 token。SGLang 在启动时会在日志中打印所选的 KV 池大小,并将其作为指标暴露出来。我们当时知道这一大小不匹配;然而,在发布前测试中,我们在基准测试期间观察到成功的 HTTP 状态码以及没有错误帧的流式响应体。我们以为引擎能够通过流式传输或其他机制优雅地处理 KV 缓存池与请求上下文窗口之间的差异。
这是我们测试中的一个关键缺口:我们只验证了长上下文下的 HTTP 响应码和引擎性能,而没有验证模型输出和连贯性。 现在,在任何分离式部署变更上线之前,我们会直接从运行中的引擎检查 KV 池大小与上下文长度是否匹配,而不是从响应码推断。
问题二:SGLang 缺陷
在 v0.5.21 中,SGLang 团队现已修复了其在处理长于 KV 池大小的提示词时的这一缺陷。以下是此前发生的情况。
当到达的提示词长度超过 KV 池可容纳的范围时,SGLang 的调度器会拒绝它。拒绝路径(Req.set_finish_with_abort)记录要返回给客户端的错误,并将提示词替换为一个单 token 占位符,以便请求能够通过正常批处理机制排空,而不分配真实内存。在单节点部署中这是有效的:占位符运行,批次完成,客户端收到错误。

在我们运行的分离式模式下,prefill 和 decode 两侧的准入队列检查的是请求是否已完成,而不是是否被拒绝。被拒绝的请求会报告自己仍然存活。于是:
- 引导。 路由器告诉 decode 节点提示词长度(图中为 8 页)。Decode 为该请求预留了 8 个 KV 页。
- Prefill 准入。 Prefill 准入队列接纳任何
req.finished()为 False 的请求。被拒绝的请求将其拒绝状态存储在to_finish中,而不是finished_reason中,因此finished()仍返回 False。被拒绝的请求被接纳了。 - Prefill 前向传播。 拒绝操作已将提示词替换为一个单 token 占位符。Prefill 对该单 token 执行了一次简单的前向传播。
- KV 传输。 Prefill 向 decode 发送了一页 KV,并发出传输完成的信号。
- Decode 准入。 Decode 的准入队列有同样的
finished()检查,因此它接受了这次交接。 - Decode 注意力。 Decode 开始生成,注意力读取了全部 8 个预留页。只有页 P0 是由该请求写入的。页 P1 到 P7 仍保留着先前请求留下的内容:其他用户的提示词和部分生成结果。
- 响应。 客户端收到的是 HTTP 200 而不是 HTTP 400。
prompt_tokens匹配了长提示词,回复是连贯的。它到达的速度比 8 页 prefill 应有的速度更快,并且包含了其他用户 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 池在 prefill 和 decode 两端都大于其上下文长度,因此没有任何请求会走到拒绝路径。我们已对使用 SGLang 提供服务的每个模型应用了引擎修复和下文所述的加固措施,并且我们的部署检查要求每个新部署都必须具备这些措施。
时间线
所有时间均为 UTC。
时间 | 事件 |
2026 年 8 月 27 日 | GLM 5.3 在 Workers AI 上线,其 prefill 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 合并了准入修复、每次传输校验和、中止 ACK 保留,并将延迟 KV 释放默认开启。 |
2026 年 10 月 2 日 | SGLang v0.5.21 发布。 |
2026 年 10 月 19 日 | SGLang v0.5.22 发布。 |
2026 年 10 月 26 日 | 与研究者和 RadixArk 协调发布。 |
持续进行的加固工作
该 bug 属于一类 KV 缓存内存安全 bug,Cloudflare 已为此努力数月。在每种情况下,一个页面在被释放、复用或读取时,仍有其他东西对它持有主张。我们看到两个常见原因。
第一个是单个节点内的页面生命周期。decode 节点上的多个任务可能同时持有同一页面:token 生成、卸载到主机内存、KV 传输接收方,以及中止和错误路径。如果其中一个在另一个仍在使用该页面时将其释放,新请求就可能在该页面仍被读取或写入时被分配给它。我们之前针对 decode 卸载(#24226、#30899)和语法错误时双重释放(#30937)的修复就属于这一类。
第二个是跨节点的竞态。当 prefill 仍在通过 RDMA 将 KV 缓存写入 decode 时请求被中止,已经发出的写入无法撤回。如果 decode 立即释放这些页,这些页就可以分配给另一个请求,迟到的写入就会落到那个请求的 KV 缓存中。Cloudflare 通过提议的修复(#31357、#38961)和一个复现脚本(#32564)为修复这一竞态做出了贡献。在这两种情况下,结果都是输出损坏,或者一个用户的数据出现在另一个用户的响应中,且不会引发任何错误。本次事件中的 bug 有所不同:被拒绝的请求每次都会经过交接,而不是因时序巧合。但结果相同:decode 读取了该请求从未写入的页。这些都没有覆盖本次事件中的故障——prefill 拒绝了请求,而 decode 从未得知。
自事件发生以来,我们已:
- 扩展延迟释放以同时保留 prefill 的源页,并覆盖所有失败类型,而不仅仅是中止。我们将其作为 #38961 向上游提出,该提案随后被 #40645、#41402 和 #41404 取代。
- 部署了有界的 Mooncake 传输批次(#32758,来自 SGLang 社区),因此任何单次传输调用都不会超过释放超时。
- 更改了负载均衡器,使客户端断开连接不再在传输中途中止请求。它会保持 prefill 连接直到 prefill 完成,并保持 decode 连接直到第一个 token,然后关闭两者。
- 为每次分离式部署变更添加了检查:根据上下文长度测量 KV 池大小、两个角色上的延迟释放、释放超时内的传输批次边界,以及所有必需的补丁均已应用。
所有这些变更都已发布到我们的分离式生产部署。
与 SGLang 团队合作
9 月 10 日,即事件发生后的第二天,我们在 RadixArk 与 SGLang 维护者会面。我们一致同意由 RadixArk 调查我们报告的竞态条件、将完整性检查的想法上游化,并为这类 bug 构建复现测试。
以下内容已在 SGLang main 上落地,以解决本次事件及相关的 KV 缓存竞态条件,除一个外均包含在 v0.5.22 中。运行分离式 SGLang 的运维人员应更新到 v0.5.22 或根据需要回移植这些补丁。
类别 | 拉取请求 | 版本 | 作用 |
事件 | v0.5.20 | 在已中止的 prefill 请求的结果到达 decode 之前将其退役。这阻止了本次事件中的暴露。 | |
事件 | v0.5.21 | 在准入阶段拒绝这些请求,即在任何 bootstrap、分配或 KV 传输之前。遵循 Cloudflare 草稿补丁 #38704 中的方法。 | |
竞态 | v0.5.21、v0.5.22 |
| |
竞态 | v0.5.20、v0.5.21 | 仅在支持延迟 KV 释放的后端上启用该功能,然后默认开启。 | |
竞态 | v0.5.21 | 修复了排空确认丢失的情况,使释放发生在确认时,而不是等待超时。 | |
竞态 | v0.5.21 | 在每次传输失败时保留页,而不仅限于 decode 发起的中止。 | |
竞态 | v0.5.21 | 修复了中止通知路径中的回归。 | |
竞态 | v0.5.22 | 失败的 Mooncake 发送不再将其写入报告为已排空。 | |
竞态 | v0.5.22 | 按请求标记中止消息,使复用的 ID 无法消费过期的确认。添加 MORI 支持。 | |
竞态 | v0.5.20 | 在 Mooncake、NIXL 和 MORI 之间共享 prefill 到 decode 的失败通知。 | |
竞态 | v0.5.20 | 可选限制 Mooncake 传输批次大小,使单次超时调用不会留下无上限的进行中写入。 | |
生命周期 | v0.5.21 | Decode 在撤回请求并释放其页面之前,会等待正在进行的主机内存卸载完成。由 Cloudflare 编写。 | |
生命周期 | v0.5.22 | 当主机内存缓存恢复失败时,保持张量并行各 rank 之间的一致性。 | |
可观测性 | v0.5.21 | 新增 | |
可观测性 | v0.5.20、v0.5.21 | 修复 SGLang 的 KV canary,该机制用于检测并记录潜在的损坏。 | |
可观测性 | v0.5.22 | 延迟释放的指标:计数、持续时间,以及每次持有是以排空确认还是超时结束。 |
以下后续工作仍在进行中:
- #39729 针对
--disaggregation-enable-kv-checksum的额外清理。已合并,等待发布。 - 释放时将 KV 页面清零,草案提案:#46435
- 将 KV 校验和验证从 prefill 到 decode 的交接扩展到每个 decode 步骤(见下文“构造即完整”)
- 更广泛的 KV Canary 和 KV Checksum 模型与功能支持
我们给其他运维者的建议
如果你在分离式 prefill/decode 模式下运行 SGLang,我们建议如下:
- 测量你的 KV 池并将其与上下文长度比较,在每个 prefill 和 decode 节点上,从引擎的启动日志中,在每次更改权重、量化、并行度或内存比例之后。如果池更小,就降低上下文长度以适配。不要依赖代理来强制执行限制,也不要假设引擎会拒绝放不下的内容。这正是我们未能强制执行的检查。
- 升级到包含 #36988 和 #38935 (v0.5.22 或更高版本)的构建,这样无论池大小如何,超长请求都会在交接前被拒绝。这是引擎侧的修复,也是当你在第一步中的检查出错时的兜底。
- 在两个角色上都启用延迟 KV 释放 (在 #41023 之后为默认值),并限制你的 Mooncake 传输批次,使单次超时调用不会留下无上限的写入尾部。
- 在你的模型支持的情况下开启损坏检测。
--disaggregation-enable-kv-checksum和--kv-canary尚未覆盖所有模型架构。检查你的日志以确认请求正在被验证。我们正在与 SGLang 团队合作,将这两项检查扩展到更多架构,包括 GLM 5.3、GLM 5.3 Flash 和 DeepSeek V4。 - 确保你的代理不会产生不必要的中止。配置你的负载均衡器,允许请求进行到第一个 token,这样你就不会触发需要跨节点协调的中止。
长期来看
构造即完整
配置检查和生命周期修复减少了页面被误用的方式。它们并不能使误用变得不可能,而这次事件提醒我们,下一个 bug 将具有我们尚未见过的形态。持久的答案是让引擎在读取时检测到某个请求即将访问一个它并不拥有的页面,并让该单个请求失败,而不是返回他人的数据。
我们在夏天构建了这个原型,并在八月份进行了描述。每个物理 KV 页都带有一个标签,当页面被重新分配时该标签会改变;每个请求记录它期望的标签,并在解码读取缓存之前检查映射关系。在我们的测试中,它带来的吞吐量和尾部延迟损失不到 1%。我们在八月中旬于 GLM 5.2 上进行了测试,但随后不久因可靠性问题将其禁用。
该原型还与特定的注意力后端和模型架构绑定,因此无法成为通用修复方案。我们正与 SGLang 团队合作,在引擎中构建一个适用于所有模型的生产版本,而不是维护 Cloudflare 专用的补丁。#39500 中的每次传输校验和是第一步:它验证从预填充到解码的交接。页级标签则更进一步,因为它们会检查每一次读取,包括传输很久之后的读取。
以数学上的不可能性保障完整性
RDMA 将数据从一个节点的内存直接复制到另一个节点。两台机器必须在数据去向、数据大小以及传输何时完成上达成一致;任何不匹配都可能导致接收方读取错误的数据。这个 bug 是分布式系统中一致性故障的一个例子,其中两台机器对该系统的状态持有不一致的视图。
在 Cloudflare 的其他地方,我们投入精力使用形式化验证方法来证明分布式系统的安全性。Topaz 为我们的权威 DNS 服务器实现了无冲突的 IP 选择。Meerkat 的 QuePaxa 实现模型已揭示出一致性 bug,这些 bug 随后得到了修复。
我们现在正将形式化方法方面的经验用于 KV 缓存传输引擎。这将从对基于 RDMA 的 KV 缓存传输协议进行建模和安全性证明开始。目标是使用 Mooncake 对我们生产环境 SGLang 的分布式 KV 缓存系统进行建模,然后跟踪并修复任何偏离我们希望保证的安全属性的情况。目的是尽可能证明此类 bug 不会再次发生。现实中,我们预计证明器能告诉我们的内容会存在空白,但证明系统的其余部分使我们能够更仔细地审查和监控这些空白。
协同披露
我们发布此报告是协同披露的一部分。我们要感谢研究员 Gadi Evron,他通过我们的 HackerOne 项目报告了此问题,提供了清晰、可复现的脚本,并给了我们时间来修复问题,并在发布前与 SGLang 维护者协调。我们还要感谢 RadixArk 的 SGLang 团队,他们将此视为自己的问题以及我们的问题,并紧急提交了上游修复。
在SGLang 团队的帖子中阅读更多内容。
如果您在 Workers AI 中发现安全问题,请在hackerone.com/cloudflare 报告。
来源:Hacker News · blog.cloudflare.com