跳到正文
Cloudflare Blog·· 1 天前精选AI 评分75

Cloudflare 披露 Workers AI GLM-5.3 跨请求 KV cache 泄露事件及 SGLang 修复

Cross-request KV cache exposure in the GLM-5.3 Workers AI model, and our remediation with SGLang

AI 导读

Cloudflare 披露 Workers AI 上 GLM-5.3 模型存在跨请求 KV cache 泄露:8 月 27 日至 9 月 9 日间,155 条超过 681,280 token 的长提示响应中混入了其他用户会话数据,未发现蓄意利用证据。

推荐理由

Cloudflare 当事方完整复盘 KV cache 跨请求泄露的根因、时间线和修复,其他 SGLang 运营方可直接对照加固。

正文 · AI 翻译

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 的上下文窗口,但我们 prefill GPU 上的 KV 缓存池仅有容纳 681,280 个 token 的空间。我们在上线前就已知晓这一不匹配。我们原本预期引擎会对任何无法处理的提示返回错误,而我们的长上下文测试都返回了成功响应,因此我们并不认为这构成问题。

第二个是 SGLang 在分离式 prefill/decode 模式下处理由此产生的超长请求时存在的缺陷。无法放入缓存池的提示会在内部被标记为拒绝,并被替换为一个单 token 占位符;但 prefill 并未中止请求并返回客户端错误,而是为占位符提示生成了 KV 缓存,并将其传输到 decode 节点。decode 节点收到“传输完成”消息后,便假定 prefill 已用该请求的完整提示覆盖了先前请求的 KV 缓存,而实际上它保留了先前状态的大部分内容。decode 节点随后在仍保存着先前请求数据的过期缓存页上执行注意力计算并生成响应。因此,对长度超过 681,280 个 token 的提示的响应可能包含其他用户 GLM 5.3 会话中的数据。

立即修复

在禁用该模型之后,我们采取了以下措施来修复此漏洞,然后才恢复对该模型的公开访问:

为修复我们的配置,我们让池子容纳完整的上下文。 我们调整了 GLM 5.3 的配置,使其与 GLM 5.2 的配置一致,从而释放了足够的 GPU 内存,使 prefill KV 池现在能容纳约 204 万个 token,大约是 1,048,576 token 上下文长度的两倍。现在,能放入上下文窗口的提示词总是能放入池中,因此对于有效请求永远不会走到拒绝路径。仅此一项就能防止该缺陷被触发。

为修复引擎,我们还让拒绝路径变得安全。 仅依赖池大小是脆弱的:量化、张量并行、CUDA graph 大小或未来模型版本的任何变化都会改变池大小,而这些变化很容易被忽略。此外,还可能有其他情况导致 prefill 调度器拒绝请求并触发同样的缺陷。为防止这种情况,我们还修补了引擎,使携带待处理拒绝的请求在 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 节点上,池大小为 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 两侧的准入队列检查的是请求是否已完成,而不是是否已被拒绝。被拒绝的请求会报告自己仍然存活。于是:

  1. 引导阶段。 路由器告知 decode 节点提示词长度(图中为 8 页)。Decode 为该请求预留了 8 个 KV 页。
  2. Prefill 准入。 Prefill 准入队列会接纳任何 req.finished() 为 False 的请求。被拒绝的请求将其拒绝状态存储在 to_finish,而不是 finished_reason 中,因此 finished() 仍然返回 False。该被拒绝的请求被接纳了。
  3. Prefill 前向传播。 拒绝逻辑已将提示词替换为单 token 占位符。Prefill 在该单 token 上运行了一次简单的前向传播。
  4. KV 传输。 Prefill 向 decode 发送了一页 KV,并发出传输完成的信号。
  5. Decode 准入。 Decode 的准入队列有相同的 finished() 检查,因此它接受了这次交接。
  6. Decode 注意力计算。 Decode 开始生成,注意力读取了全部 8 个预留页。只有页 P0 是由该请求写入的。页 P1 到 P7 仍保留着先前请求遗留的内容:其他用户的提示词和部分生成内容。
  7. 响应。 客户端收到的是 HTTP 200 而不是 HTTP 400。prompt_tokens 匹配了长提示词,回复也是连贯的。它到达的速度比 8 页 prefill 应有的速度更快,并且包含了来自其他用户 KV 页的内容。

每个超过阈值的请求都会出现这种情况。这一问题曾由另一位用户于 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 是唯一受此缺陷影响的模型。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 合并了准入修复、每次传输校验和、abort-ACK 保留,并将延迟 KV 释放默认开启。

2026 年 10 月 2 日

SGLang v0.5.21 发布。

2026 年 10 月 19 日

SGLang v0.5.22 发布。

2026 年 10 月 26 日

与研究人员和 RadixArk 协调发布。

持续进行的加固工作

此缺陷属于一类 KV 缓存内存安全缺陷,Cloudflare 已努力解决此类问题数月。在每种情况下,某个页面在被释放、复用或读取时,仍有其他组件对其持有所有权。我们看到两个常见原因。

第一个是单个节点内的页面生命周期。decode 节点上的多个任务可能同时持有同一页面:token 生成、卸载到主机内存、KV 传输接收方,以及 abort 和错误路径。如果其中一个任务在另一个任务仍在使用页面时释放了它,新请求就可能在该页面仍被读取或写入时被分配到这个页面。我们此前对 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,或按需回移这些补丁。 

类别

拉取请求

发布版本

功能

事件

#36988

v0.5.20

在其结果到达 decode 之前退役已中止的 prefill 请求。这阻止了本次事件中的暴露。

事件

#38935

v0.5.21

在准入阶段就拒绝这些请求,早于任何 bootstrap、分配或 KV 传输。遵循 Cloudflare 草案补丁 #38704 中的方法。

竞态

#39461, #41612

v0.5.21、v0.5.22

sgl-router 在客户端断开连接或 prefill 失败时取消请求的两半。

竞态

#37454, #41023

v0.5.20、v0.5.21

仅在支持延迟 KV 释放的后端上启用该功能,然后默认开启。

竞态

#40645, #41402

v0.5.21

修复排空确认丢失的情况,使释放按确认发生,而不是等待超时。

竞态

#41404

v0.5.21

在每次传输失败时都保留页,而不仅是在 decode 发起的中止时。

竞态

#41618

v0.5.21

修复中止通知路径中的回归问题。

竞态

#43719

v0.5.22

失败的 Mooncake 发送不再将其写入报告为已排空。

竞态

#37077

v0.5.22

按请求标记中止消息,使复用的 ID 无法消费过期的确认。新增 MORI 支持。

竞态

#36612

v0.5.20

在 Mooncake、NIXL 和 MORI 之间共享 prefill 到 decode 的失败通知。

竞态

#32758

v0.5.20

为 Mooncake 传输批大小设置可选上限,使单次超时调用不会留下数量无上限的未完成写入。

生命周期

#30899

v0.5.21

Retract 请求并释放其页面前,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,用于检测并记录潜在的损坏。

可观测性

#41128

v0.5.22

延迟释放的指标:计数、时长,以及每次保留是以 drain 确认还是超时结束。

以下后续工作仍在进行中:

  • #39729 针对--disaggregation-enable-kv-checksum的额外清理。已合并,等待发布。
  • 释放时将 KV 页清零,草案提案:#46435
  • 将 KV 校验和验证从 prefill 到 decode 的交接扩展到每个 decode 步骤(参见下文“构造即完整”)
  • 更广泛的 KV Canary 与 KV Checksum 模型与特性支持

我们给其他运维者的建议

如果你在分离式 prefill/decode 模式下运行 SGLang,我们建议如下:

  1. 在每个 prefill 和 decode 节点上,从引擎启动日志中,测量你的 KV 池并与你的上下文长度比较,且在每次更改权重、量化、并行度或内存比例之后都这样做。如果池较小,就降低上下文长度以适配。不要依赖代理来强制执行该限制,也不要假设引擎会拒绝放不下的内容。这就是我们未强制执行的检查。
  2. 升级到包含 #36988 和 #38935 的构建版本(v0.5.22 或更高版本),这样无论池大小如何,超长请求都会在交接前被拒绝。这是引擎侧的修复,也是你第一步检查出错时的兜底。
  3. 在两个角色上都启用延迟 KV 释放运行 (#41023 之后的默认行为),并限制 Mooncake 传输批次,使单次超时调用不会留下无上限的写入尾部。
  4. 在你的模型支持的地方开启损坏检测。 --disaggregation-enable-kv-checksum 和--kv-canary 尚未覆盖所有模型架构。检查你的日志以确认请求正在被验证。我们正与 SGLang 团队合作,将这两项检查扩展到更多架构,包括 GLM 5.3、GLM 5.3 Flash 和 DeepSeek V4。
  5. 确保你的代理不会产生不必要的中止。配置负载均衡器,允许请求进行到第一个 token,以免触发需要跨节点协调的中止。

更长期

构造即完整

配置检查和生命周期修复减少了页面被误用的途径。它们并不能使误用变得不可能,而这次事件提醒我们,下一个 bug 会有着我们未曾见过的形态。持久的答案是让引擎在读取时检测到某个请求即将对不属于它的页面进行注意力计算,并让那一个请求失败,而不是返回别人的数据。

我们在夏天构建了此功能的原型,并在八月进行了描述。每个物理 KV 页都带有一个标签,当页面被重新分配时该标签会改变;每个请求记录它期望的标签,并在解码读取缓存前检查映射关系。在我们的测试中,它带来的吞吐量和尾部延迟损失低于 1%。我们在八月中旬于 GLM 5.2 上进行了测试,但随后不久因可靠性问题将其禁用。

该原型还与特定的注意力后端和模型架构绑定,因此无法成为通用修复方案。我们正在与 SGLang 团队合作,在引擎中构建一个适用于所有模型的生产版本,而不是维护 Cloudflare 专用的补丁。#39500 中的每次传输校验和是第一步:它验证从预填充到解码的交接。页级标签则更进一步,因为它们检查每一次读取,包括传输很久之后的读取。

数学上不可能出现的不完整性

RDMA 将数据从一个节点的内存直接复制到另一个节点的内存。两台机器必须就数据去向、数据大小以及何时完成达成一致;任何不一致都可能导致接收方读取错误的数据。这个 bug 是分布式系统中一致性故障的一个例子,其中两台机器对该系统的状态持有不一致的视图。

在 Cloudflare 的其他工作中,我们投入精力使用形式化验证方法来证明分布式系统的安全性。Topaz 为我们的权威 DNS 服务器实现了无冲突的 IP 选择。Meerkat 的 QuePaxa 实现的模型已经证明了随后被修复的一致性 bug。

我们现在正将我们在形式化方法方面的经验引导到 KV 缓存传输引擎上。这将从建模并证明一个基于 RDMA 的 KV 缓存传输协议的安全性开始。目标是使用 Mooncake 对我们生产环境 SGLang 的分布式 KV 缓存系统进行建模,然后跟踪并修复与我们想要保证的安全属性的任何偏差。目的是尽可能证明此类 bug 不会再次发生。现实地说,我们预计证明器能告诉我们的内容会存在空白,但证明系统的其余部分使我们能够更仔细地审查和监控这些空白。

协调披露

我们发布这份报告是协调披露的一部分。我们要感谢通过我们的 HackerOne 项目报告此问题的研究员 Gadi Evron,他提供了清晰、可复现的脚本,并给了我们时间来修复问题,并在发布前与 SGLang 维护者协调。我们还要感谢 RadixArk 的 SGLang 团队,他们将其视为自己的问题以及我们的问题,并紧急落地了上游修复。

在SGLang 团队的帖子中阅读更多内容。

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

来源:Cloudflare Blog · blog.cloudflare.com