GLM-5.3 Workers AI 模型中的跨请求 KV 缓存泄露,以及我们使用 SGLang 的修复方案

22

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的配置不当。我们以1,048,576个token的上下文窗口部署了GLM 5.3,但预填充GPU上的KV缓存池仅能容纳681,280个token。

我们在上线前就已知晓这一不匹配。我们预期引擎会对无法处理的提示词返回错误,而我们的长上下文测试也均返回了成功响应,因此并未将其视为问题。

其二,则是SGLang在解耦的预填充/解码模式下处理超出容量请求时存在的一个缺陷。对于无法容纳于缓存池的提示词,引擎内部会将其标记为拒绝,并用一个单token占位符替代;然而,预填充阶段并未中止请求并返回客户端错误,而是为该占位符提示词生成了KV缓存,并将其传递至解码节点。

解码节点接收到“传输完成”消息后,误以为预填充阶段已用本次请求的完整提示词覆盖了先前请求的KV缓存,但实际上,之前的大部分状态仍被保留了下来。

随后,解码节点在使用这些陈旧的缓存页时生成了响应,而这些缓存页中仍残留着早期请求的数据。因此,长度超过681,280个token的提示词的响应可能包含其他用户GLM 5.3会话中的内容。

即时修复措施

在禁用该模型后,我们在恢复其对外访问之前采取了以下措施以修复这一漏洞:

为修正配置,我们使缓存池能够容纳完整的上下文。我们将GLM 5.3的配置调整为与GLM 5.2一致,从而释放出足够的GPU显存,使预填充KV缓存池可容纳约204万个token,约为1,048,576个token上下文长度的两倍。

如今,只要提示词在上下文窗口之内,就必然也能容纳于缓存池,因此合法请求永远不会进入拒绝路径。仅凭这一点即可防止该漏洞被触发。

为修复引擎,我们还使拒绝路径变得安全。仅依赖缓存池大小的做法较为脆弱:量化方式、张量并行度、CUDA图大小的任何变更,或未来某个模型版本的更新,都可能改变缓存池的大小,而这些变化往往不易察觉。

此外,还可能存在其他情况下,预填充调度器会拒绝请求并引发同样的漏洞。为避免这种情况,我们还对引擎进行了修补,使得携带待处理拒绝状态的请求在进入解耦入口队列时即被终止,并附带原始错误信息,而在任何初始化、资源分配或KV传输发生之前便予以处理。

我们已将此修复提案提交至上游,编号为#38704,其中还要求解码节点验证实际传输的token数与其预留的数目是否一致。该修复已于9月6日合并入SGLang的main(#36988#38935,该补丁按照我们的方案更早地拒绝此类请求。

在恢复该模型之前,我们已通过该研究员的复现脚本以及我们自己的信标标记测试验证了这两项变更。

背景:我们如何提供GLM 5.3服务

Workers AI 在一种分离式配置下运行 GLM 5.3 和 SGLang:一组预填充 GPU 处理提示并为其生成模型的 KV 缓存,随后通过 Mooncake 提供的远程直接内存访问(RDMA),将该缓存传递给一组解码 GPU,由它们生成响应。

我们曾在关于高性能 LLM和更小、更快、更安全的模型的博文中介绍过这一架构。

GPU 内存中的 KV 缓存是一个由固定大小页面组成的共享池。每个请求在被接纳时分配页面,并在完成时释放这些页面。目前,页面在不同请求之间不会被清零;下一个获得某页面的请求会直接覆盖其内容。

这种做法在推理引擎中很常见,也是实现最佳性能所必需的,但这意味着推理引擎必须负责跟踪这些内存页面的所有权。KV 缓存页面的所有权与生命周期管理不仅要处理节点内部的共享问题(KV 缓存的卸载与标记生成都会以不同的生命周期访问同一块内存),还要应对与远程预填充节点在技术上共享所有权的情况——后者对该页面持有同时存在的可变引用)。

此外,该池的大小并非一个配置项。SGLang 会在启动时根据加载模型权重后剩余的 GPU 内存来确定其大小。而模型所宣称的上下文长度则是另一个独立的配置值。

两者并无强制关联,因此确保二者一致是我们需要检查的工作。

问题一:配置错误

在我们的 GLM 5.3 预填充节点上,按照当前配置,该池的容量为 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 占位符替换原始提示,以便该请求能通过正常的批处理流程顺利结束,而无需分配实际内存。

在单节点部署中,这种方式运作正常:占位符被执行,批次处理完毕,客户端也收到了错误响应。

但在我们采用的分离式模式下,预填充和解码两端的接纳队列均以“请求是否已完成”为判断依据,而非“请求是否被拒绝”。因此,被拒绝的请求仍会被视为处于活动状态。

具体过程如下:

  1. 引导阶段。 路由器将提示长度告知解码节点(图示中为 8 页)。解码节点为该请求预留了 8 个 KV 页面。
  2. 预填充接纳。 预填充接纳队列会接纳所有满足 req.finished() 为 False 的请求。被拒绝的请求将其拒绝信息存储于 to_finish,而非 finished_reason,因此 finished() 依然返回 False。于是,该被拒绝的请求被接纳。
  3. 预填充前向传播。 此时,拒绝操作已将提示替换为一个单 token 占位符。预填充对该单 token 执行了一个简单的前向传播。
  4. KV 传输。 预填充向解码节点发送了一页 KV 数据,并通知传输已完成。
  5. 解码接纳。 解码端的接纳队列同样执行 finished() 检查,因此接受了此次移交。
  6. 解码注意力计算。 解码开始生成文本,注意力机制读取了全部 8 个已预留的页面。其中只有页面 P0 是本次请求写入的内容,而页面 P1 至 P7 仍保留着先前请求留下的数据——其他用户的提示及部分生成结果。
  7. 响应。 客户端收到的是 HTTP 200 状态码,而非 HTTP 400。由于 prompt_tokens 与长提示相匹配,回复内容看似连贯。其生成速度远快于预期的 8 页预填充所需时间,且其中还包含了来自其他用户 KV 页面的内容。

所有超出阈值的请求都表现出同样的行为。这一漏洞已于 2026 年 7 月 6 日被另一位用户报告至 SGLang,编号为 #30233,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 池大小均大于其上下文长度,因此没有任何请求会进入拒绝路径。

我们已将引擎修复以及下文所述的加固措施应用到所有通过 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

查明根本原因是预填充池小于上下文长度。使用更大池的金丝雀预填充节点经验证不受影响。

2026年9月9日 14:52

所有预填充节点均已切换至修正后的配置。

2026年9月9日 19:04

以修正后的配置并附加由 Cloudflare 编写的引擎补丁重新构建了完整部署;该补丁可正确对被预填充调度器拒绝的请求返回错误。已根据研究人员的复现脚本完成验证。

2026年9月9日 19:40

模型恢复为公开模式。

2026年9月10日 23:30

与 SGLang 维护团队(RadixArk 首席技术官及核心工程师)召开会议,就上游修复方案达成一致。

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 协同发布。

持续的加固工作

该漏洞属于一类 KV 缓存内存安全缺陷,Cloudflare 已就此开展数月的应对工作。在每种情形下,都有页面在仍有其他实体对其持有使用权时被释放、重用或读取。

我们观察到两类常见原因。

其一是单个节点内的页面生命周期。解码节点上的多项任务可能同时持有同一页面:标记生成、向主机内存的卸载、KV 传输接收方,以及中止和错误处理路径。

若其中一者在另一者仍在使用时释放该页面,则后续请求可能在页面仍被读写时被分配到该页。我们此前针对解码卸载(#24226、#30899)以及语法错误时的双重释放(#30937)所作的修复均属此类。

其二是跨节点的竞争条件。当请求在预填充阶段仍通过 RDMA 向解码端写入 KV 缓存时被中止,已发出的写操作无法撤回。若解码端立即释放这些页面,这些页面便可能被分配给另一请求,导致后续的写操作落入该请求的 KV 缓存中。

Cloudflare 通过提出的修复方案(#31357、#38961)以及一份复现脚本(#32564)为解决这一竞争条件作出了贡献。在这两种情况下,结果都是输出被破坏,或一个用户的数据出现在另一个用户的响应中,且未触发任何错误。

本次事件中的漏洞有所不同:被拒绝的请求每次都顺利完成了交接,而非因 timing 问题所致。但其结果相同:解码端读取了该请求从未写入的页面。

上述各项均未能覆盖此次事件中预填充拒绝请求而解码端对此毫不知情的情形。

自事件发生以来,我们已:

  • 将延迟释放机制扩展至同时保留预填充阶段的源页面,并覆盖所有失败类型,而不仅限于中止情况。我们已在上游提出相关建议(#38961),后被 #40645、#41402 和 #41404 取代。
  • 部署了受限的 Mooncake 传输批次(#32758,来自 SGLang 社区),以确保单次传输调用不会超过释放超时时间。
  • 调整了负载均衡器,使客户端断开连接不再在传输过程中中止请求。它会保持预填充连接直至预填充完成,并保持解码连接直至首个标记生成,随后再同时关闭两端连接。
  • 在每次分离式部署变更时新增一项检查:KV 池大小需与上下文长度相匹配,两端角色均启用延迟释放,传输批次限制在释放超时之内,且所有必要补丁均已应用。

以上所有变更均已应用于我们的生产环境中的分离式部署。

与 SGLang 团队的合作

我们在事件发生后的第二天,即9月10日,与SGLang的维护者们在RadixArk进行了会面。我们一致同意由RadixArk调查我们报告的竞争条件问题,将完整性校验的思路反馈至上游,并针对此类漏洞构建复现测试。

为应对此次事件及相关KV缓存竞争条件,以下补丁已合并至SGLang的main分支,其中除一项外均已纳入v0.5.22版本。运行分离式SGLang的运营商应根据需要升级至v0.5.22,或回迁这些补丁。

类别

Pull请求

发布版本

作用

事件

#36988

v0.5.20

在预填充请求的结果到达解码阶段之前将其终止。此举可避免本次事件中的数据泄露。

事件

#38935

v0.5.21

在准入阶段即拒绝此类请求,无需进行任何引导、分配或KV传输操作。该方案沿用了Cloudflare草案补丁#38704中的做法。

竞争

#39461、#41612

v0.5.21、v0.5.22

sgl-router在客户端断开连接或预填充失败时,取消请求的两个部分。

竞争

#37454、#41023

v0.5.20、v0.5.21

仅在支持延迟释放KV的后端启用该功能,并将其设为默认开启。

竞争

#40645、#41402

v0.5.21

修复了确认释放被丢失的情况,使释放操作改为在收到确认时执行,而非等待超时。

竞争

#41404

v0.5.21

在每次传输失败时均保留页面,而不仅限于由解码发起的中止操作。

竞争

#41618

v0.5.21

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

竞争

#43719

v0.5.22

发送失败的Mooncake不再将其写入标记为已释放。

竞争

#37077

v0.5.22

为每条请求的消息打上标签,防止重复使用的ID消费过时的确认信息。同时新增对MORI的支持。

竞争

#36612

v0.5.20

在Mooncake、NIXL和MORI之间共享从预填充到解码的失败通知。

竞争

#32758

v0.5.20

可选地限制Mooncake传输批次的大小,以防止单次超时调用导致未完成的写入数量无限增长。

生命周期

#30899

v0.5.21

解码在撤销请求并释放其页面之前,会等待正在进行的卸载操作完成。该补丁由Cloudflare贡献。

生命周期

#43562

v0.5.22

当主机内存缓存恢复失败时,保持张量并行各Rank的一致性。

可观测性

#39500

v0.5.21

新增--disaggregation-enable-kv-checksum:解码在生成前会对传输的KV进行校验和验证。相关清理工作见#39729,已合并并待发布。

可观测性

#38596、#40642

v0.5.20、v0.5.21

修复了SGLang的KV金丝雀机制,用于检测并记录潜在的损坏情况。

可观测性

#41128

v0.5.22

新增关于延迟释放的指标:次数、持续时间,以及每次保留是因释放确认还是超时而结束。

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

  • #39729 针对--disaggregation-enable-kv-checksum的进一步清理。已合并,待发布。
  • 释放时将KV页面置零,提案草案:#46435
  • 将KV校验和验证从预填充到解码的交接点扩展至解码的每个步骤(参见下文“通过设计保障完整性”)
  • 更广泛的KV金丝雀及KV校验和模型与功能支持

我们对其他运营商的建议

如果您以分离式预填充/解码模式运行SGLang,我们建议如下:

  1. 在每次预填充和解码节点上,测量您的 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,从而避免触发需要跨节点协调的中止操作。

长期措施

通过设计保障完整性

配置检查和生命周期修复减少了页面被误用的可能性,但并不能完全杜绝误用。此次事件提醒我们,下一个漏洞可能会呈现出我们未曾见过的形态。

根本性的解决方案是让引擎在读取时检测到某个请求即将访问不属于自己的页面,并直接失败该请求,而不是返回其他人的数据。

我们在今年夏天构建了一个原型,并于八月进行了描述。相关说明见此处。每个物理 KV 页面都携带一个标签,该标签在页面被重新分配时会发生变化;每个请求都会记录其预期的标签,并且在解码阶段读取缓存之前会对映射关系进行检查。

在我们的测试中,这一机制对吞吐量和尾延迟的影响不到 1%。我们在八月中旬对 GLM 5.2 进行了测试,但随后因可靠性问题将其禁用了。

该原型还与特定的注意力后端和模型架构绑定,因此无法作为通用修复方案。我们正与 SGLang 团队合作,将这一功能以生产级的形式集成到引擎中,使其适用于所有模型,而非维护 Cloudflare 特有的补丁。

在 #39500 中引入的逐次传输校验和是一个初步步骤:它用于验证从预填充到解码的移交过程。而页面级别的标签则更进一步,因为它会在每次读取时进行检查,包括在传输完成很久之后的读取操作。

通过数学上的不可能性保障完整性

RDMA 可以将数据直接从一台节点的内存复制到另一台节点的内存。两台机器必须就数据的去向、大小以及传输完成时刻达成一致;任何不一致都可能导致接收方读取错误的数据。

本次漏洞正是分布式系统中一致性失效的一个实例,其中两台机器对该系统的状态持有不一致的看法。

在 Cloudflare 的其他领域,我们曾致力于利用形式化验证方法证明分布式系统的安全性。Topaz 使我们的权威 DNS 服务器能够实现无冲突的 IP 选择。

针对 Meerkat 的 QuePaxa 实现所建立的模型,曾揭示出一些后续被修复的一致性缺陷。

如今,我们将形式化方法方面的经验应用于 KV 缓存传输引擎。首先将对基于 RDMA 的 KV 缓存传输协议进行建模并证明其安全性。目标是使用 Mooncake 对我们生产的 SGLang 分布式 KV 缓存系统进行建模,然后追踪并修复与我们希望保证的安全属性之间的任何偏差。

我们的目的是尽可能地证明,类似本次的漏洞不会再发生。现实情况下,形式化验证工具所能提供的信息难免存在 gaps,但对系统其余部分的证明使我们能够更密切地审查和监控这些 gaps。

协同披露

我们发布本报告是为了配合协同披露流程。在此感谢研究人员 Gadi Evron,他通过我们的 HackerOne 计划提交了一份清晰、可复现的漏洞利用脚本,并在公开披露前留给我们充足的时间进行修复,同时与 SGLang 维护者协调。

我们也感谢 RadixArk 的 SGLang 团队,他们将此事视为自身的问题,并迅速完成了上游修复。

更多信息请参阅 SGLang 团队的公告。

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

参与讨论