Cloudflare 修复 Workers AI 中 GLM-5.3 的跨请求 KV cache 数据暴露问题
Cross-request KV cache exposure in the GLM-5.3 Workers AI model (155 responses)
Cloudflare 已修复 Workers AI 上 GLM-5.3 的跨请求 KV cache 暴露问题,8 月 27 日至 9 月 9 日共有 155 条响应包含其他客户此前请求的数据,且相关提示词均超过 681,280 tokens。
文章拆解了配置错配与 SGLang 缺陷共同造成数据暴露的路径,并给出引擎升级、缓存池核对和传输校验等部署建议。
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 tokens。
在排除我们自身和研究人员的测试后,我们发现,所识别请求在时间和数量上均与正常的长上下文使用相符,而不符合有人反复利用该漏洞的情况。我们没有发现故意利用该漏洞的证据。
今天,我们将这份报告与来自 SGLang 的一篇文章一并发布,以进行协同披露;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 tokens。上线前我们就知道这种不匹配。我们预期引擎会针对任何无法处理的提示词返回错误,而我们的长上下文测试均成功返回了响应,因此我们没有认为这会是个问题。
第二个问题是 SGLang 在分离式预填充/解码模式下处理由此产生的超长请求时存在缺陷。无法容纳在缓存池中的提示词在内部被标记为已拒绝,并替换为一个单 token 占位符;但引擎并未中止请求并向客户端返回错误,而是为该占位符提示词生成 KV 缓存,并将其传输到解码节点。解码节点收到一条“传输完成”消息,并假定预填充已用本次请求的完整提示词覆盖了先前请求的 KV 缓存,但实际上先前状态的大部分内容仍然保留。随后,解码节点在基于陈旧缓存页进行注意力计算时生成了响应,而这些缓存页仍保存着早期请求的数据。因此,长度超过 681,280 tokens 的提示词,其响应可能包含其他用户的 GLM 5.3 会话数据。
立即修复
在停用该模型后,我们采取了以下措施来修复此漏洞,然后才恢复对模型的公开访问:
为修复配置,我们让池能够容纳完整的上下文。 我们将 GLM 5.3 的配置调整为与 GLM 5.2 相同,释放了足够的 GPU 显存,因此 prefill KV 池目前可容纳约 2.04 million tokens,约为 1,048,576-token 上下文长度的两倍。现在,任何能够装入上下文窗口的提示词也总能装入池中,因此有效请求永远不会进入拒绝路径。仅这一项措施就能防止该漏洞被触发。
为修复引擎,我们还让拒绝路径变得安全。 仅依赖池大小很脆弱:量化、张量并行、CUDA graph 大小或未来模型版本的任何变化,都会以容易被人忽视的方式改变池大小。此外,可能还有其他情况会导致 prefill 调度器拒绝请求并触发同一漏洞。为防止这种情况,我们还修补了引擎,使携带待处理拒绝状态的请求会在任何 bootstrap、分配或 KV 传输发生之前,于分离式接纳队列中以原始错误被终止。我们向上游提交了这项修复,即 #38704,其中还要求 decode 节点验证传输的 token 数量与其预留的数量一致。该修复已经通过 #36988 应用于 SGLang main,于 9 月 6 日针对一条相关故障路径合并,但尚未发布。维护者还合并了 #38935,按照我们的思路,让这些请求更早被拒绝。
在恢复模型之前,我们使用研究人员的复现脚本和我们自己的金丝雀标记测试对这两项更改进行了验证。
背景:我们如何运行 GLM 5.3
Workers AI 使用 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 tokens,而模型的上下文长度为 1,048,576 tokens。所选的 KV 池大小会由 SGLang 在启动日志中打印,并以指标形式公开。我们知道存在这种大小不匹配;然而,在上线前测试期间,我们观察到基准测试返回了成功的 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 已由另一位用户于 July 6, 2026 作为问题 #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 上所有其他分离式部署的 prefill 和 decode KV 池均大于其上下文长度,因此任何请求都不会进入拒绝路径。我们已将下述引擎修复和加固措施应用到所有使用 SGLang 提供的模型,并且我们的部署检查要求每次新部署都必须具备这些修复和加固措施。
时间线
所有时间均为 UTC。
时间 | 事件 |
Aug 27, 2026 | GLM 5.3 在 Workers AI 上线,其 prefill KV 池小于上下文窗口。从上线起,该问题便已暴露。 |
Sep 8, 2026 - 23:11 | 通过 HackerOne 收到报告。 |
Sep 9, 2026 - 08:00 | Cloudflare 开始核实 HackerOne 报告。 |
Sep 9, 2026 - 13:22 | 宣布发生安全事件。Product Security 在 25 分钟内使用两个独立账户复现了泄漏。 |
Sep 9, 2026 - 14:00 | 确认问题的影响范围后,GLM 5.3 被设为 私有。请求返回 HTTP 400。状态页面已更新。 |
Sep 9, 2026 - 14:35 | 根因被确定为 prefill 池小于上下文长度。使用更大内存池的 canary prefill 节点经确认不受该问题影响。 |
Sep 9, 2026 - 14:52 | 所有 prefill 节点均滚动更新至修正后的配置。 |
Sep 9, 2026 - 19:04 | 使用修正后的配置以及由 Cloudflare 编写的引擎补丁重建了整个部署;该补丁会针对被 prefill 调度器拒绝的请求正确返回错误。已针对研究人员的复现脚本完成验证。 |
Sep 9, 2026 - 19:40 | 模型恢复为公开状态。 |
Sep 10, 2026 - 23:30 | 与 SGLang 维护者(RadixArk CTO 和核心工程师)举行会议,就上游修复方案达成一致。 |
Sep 11-19, 2026 | 额外的加固措施已部署到所有生产环境中的 SGLang 部署(详见下文)。 |
Sep 16-Oct 12, 2026 | 上游 SGLang 合并了准入修复、每次传输的校验和以及 abort-ACK 保留机制,并默认启用延后 KV 释放。 |
Oct 2, 2026 | SGLang v0.5.21 发布。 |
Oct 19, 2026 | SGLang v0.5.22 发布。 |
Oct 26, 2026 | 与研究人员及 RadixArk 协调发布。 |
持续的加固工作
这个 bug 属于一类 KV 缓存内存安全 bug,Cloudflare 数月来一直在努力解决这类问题。在每种情况下,某个页面都会在仍被其他组件引用时被释放、复用或读取。我们发现了两个常见原因。
第一个原因是单个节点内的页面生命周期。decode 节点上的多个任务可以同时持有同一页面:token 生成、卸载到主机内存、KV 传输接收端,以及中止和错误处理路径。如果其中一个任务在另一个仍在使用该页面时将其释放,那么新请求可能会被分配到该页面,而此时该页面仍在被读取或写入。我们此前针对 decode 卸载(#24226、#30899)和语法错误导致的重复释放(#30937)所做的修复正属于这一类。
第二种是跨节点的竞态。当请求在预填充仍通过 RDMA 将其 KV 缓存写入解码端时被中止,已经发送的写入无法撤回。如果解码端立即释放这些页面,这些页面可能会被分配给另一个请求,而迟到的写入就会落入另一个请求的 KV 缓存。Cloudflare 已通过提议的修复方案(#31357、#38961)和复现脚本(#32564)参与解决这一竞态。在这两种情况下,结果都是输出损坏,或者一个用户的数据出现在另一个用户的响应中,而且不会报告任何错误。本次事件中的缺陷有所不同:被拒绝的请求每次都会经过交接流程,而不是偶然因时序问题发生。但结果相同:解码端读取了该请求从未写入的页面。上述方案均未涵盖本次事件中的故障,即预填充拒绝了请求,而解码端始终不知情。
自本次事件发生以来,我们:
- 扩展了延迟释放机制,使其也会保留预填充端的源页面,并涵盖所有故障类型,而不只是中止。我们将其作为 #38961 提交到上游,后来被 #40645、#41402 和 #41404 取代。
- 部署了设有上限的 Mooncake 传输批次(#32758,来自 SGLang 社区),因此任何单次传输调用的时长都不会超过释放超时时间。
- 修改了负载均衡器,使客户端断开连接不再导致请求在传输途中被中止。它会将预填充连接保持到预填充完成,并将解码连接保持到产生第一个 token,然后将二者关闭。
- 在每项分离式部署变更中增加了检查:根据上下文长度核对 KV 池大小、两种角色均启用延迟释放、传输批次的上限处于释放超时时间之内,以及所有必需补丁均已应用。
所有这些变更均已发布到我们的分离式生产部署中。
与 SGLang 团队合作
事故次日,也就是 9 月 10 日,我们与 RadixArk 的 SGLang 维护者会面。我们商定,由 RadixArk 调查我们报告的竞态条件,将完整性检查思路提交到上游,并为这类缺陷构建复现测试。
以下变更已合入 SGLang main,用于解决本次事件及相关 KV 缓存竞态条件;其中除一项外,均已包含在 v0.5.22 中。运行分离式 SGLang 的运维人员应更新至 v0.5.22,或根据需要回移植这些补丁。
类别 | 拉取请求 | 发布版本 | 作用 |
事件 | v0.5.20 | 在已中止的预填充请求的结果到达解码端之前将其淘汰。这防止了本次事件中的数据暴露。 | |
事件 | 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 | 发生任何传输失败时都保留页面,而不仅仅是在解码端发起中止时。 | |
竞态 | v0.5.21 | 修复中止通知路径中的一项回归问题。 | |
竞态 | v0.5.22 | Mooncake 发送失败后,不再报告其写入已排空。 | |
竞态 | v0.5.22 | 按请求为中止消息添加标签,使复用的 ID 无法将过期确认视为有效确认。新增 MORI 支持。 | |
竞态 | v0.5.20 | 在 Mooncake、NIXL 和 MORI 之间共享预填充到解码的失败通知。 | |
竞态 | v0.5.20 | 可选择为 Mooncake 传输批次大小设置上限,使单次超时的调用不会导致数量不受限的写入同时进行。 | |
生命周期 | v0.5.21 | 解码在撤销请求并释放其页面前,会等待正在进行的主机内存卸载操作完成。由 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 校验和验证从预填充到解码的交接扩展到每一个解码步骤(见下文“从结构上保证完整性”)
- 扩大 KV Canary 和 KV Checksum 对模型及功能的支持范围
我们向其他运维人员提出的建议
如果以分离式预填充/解码模式运行 SGLang,我们建议如下:
- 测量 KV 池并将其与上下文长度进行比较,在每个预填充和解码节点上,从引擎启动日志中获取数据,并在每次更改权重、量化、并行性或内存占比后执行此操作。如果 KV 池较小,请降低上下文长度以使其适配。不要依赖代理来强制执行此限制,也不要假设引擎会拒绝无法容纳的内容。这正是我们此前未强制执行的检查。
- 升级到包含 #36988 和 #38935 (v0.5.22 或更高版本)的构建版本,这样无论 KV 池大小如何,超长请求都会在交接前被拒绝。这是引擎侧的修复,也是第一步检查有误时的兜底。
- 让两种角色都启用延迟 KV 释放 (在 #41023 之后默认启用),并限制 Mooncake 传输批次大小,使单次超时调用不会造成数量不受限的写入积压。
- 在模型支持的情况下启用损坏检测。
--disaggregation-enable-kv-checksum和--kv-canary尚未涵盖所有模型架构。检查日志,确认请求确实经过了验证。我们正与 SGLang 团队合作,将这两项检查扩展到更多架构,包括 GLM 5.3、GLM 5.3 Flash 和 DeepSeek V4。 - 确保代理不会产生不必要的中止操作。 配置负载均衡器,允许请求至少运行到首个 token,从而避免触发需要跨节点协调的中止。
长期来看
从结构上保证完整性
配置检查和生命周期修复减少了页面可能被误用的途径,但无法彻底杜绝误用。本次事件提醒我们,下一个 bug 会以一种我们从未见过的形态出现。长久的解决方案是让引擎在读取时检测出某个请求即将对不属于它的页面执行注意力计算,并让该请求失败,而不是返回他人的数据。
我们在夏季构建了它的原型,并在 8 月份对其进行了介绍。每个物理 KV 页都携带一个标签,当该页被重新分配时,标签就会改变;每个请求都会记录其预期的标签,并且在 decode 读取缓存之前检查映射。在我们的测试中,它对吞吐量和尾延迟的影响均低于 1%。我们于 8 月中旬在 GLM 5.2 上进行了测试,但不久后因可靠性问题将其禁用。
该原型还与特定的 attention 后端和模型架构绑定,因此无法成为通用修复方案。我们正与 SGLang 团队合作,将生产版本集成到引擎中,使其适用于所有模型,而不是维护 Cloudflare 专用的补丁。#39500 中每次传输的校验和只是第一步:它验证从 prefill 到 decode 的交接。页级标签更进一步,因为它们会检查每一次读取,包括传输很久之后的读取。
借助数学上的不可能性保证完整性
RDMA 会将数据从一个节点的内存直接复制到另一个节点的内存。两台机器必须就数据写入的位置、大小以及传输何时完成达成一致;任何不匹配都可能导致接收方读取到错误数据。此 bug 是分布式系统中一致性失效的一个例子:当时两台机器对系统状态的视图不一致。
在 Cloudflare 的其他地方,我们一直致力于使用形式化验证方法证明分布式系统的安全性。Topaz 为我们的权威 DNS 服务器实现无冲突的 IP 选择。针对 Meerkat 的 QuePaxa 实现建立的模型发现了随后得到修复的一致性 bug。
我们现在正将我们在形式化方法方面的经验用于 KV cache 传输引擎。首先要对基于 RDMA 的 KV cache 传输协议进行建模并证明其安全性。目标是使用 Mooncake 对我们生产环境中的 SGLang 分布式 KV cache 系统进行建模,然后跟踪并修复任何偏离我们希望保障的安全属性的情况。其目标是尽可能证明——这类 bug 不会再次发生。现实地说,我们预计证明工具所能告知我们的信息会存在空白,但证明系统的其余部分能让我们更仔细地审查和监控这些空白。
协调披露
我们发布这份报告是协调披露工作的一部分。我们要感谢研究人员 Gadi Evron;他通过我们的 HackerOne 计划报告了此问题,并提供了清晰、可复现的脚本,还给予我们时间修复问题,并在发布前与 SGLang 维护者协调。我们也要感谢 RadixArk 的 SGLang 团队;他们将此问题视为自己和我们共同的问题,并紧急推动上游修复落地。
更多内容请阅读 SGLang 团队的帖子。
如果您在 Workers AI 中发现安全问题,请通过 hackerone.com/cloudflare 报告。
来源:Hacker News · blog.cloudflare.com