事件调查
Cloudflare:新沙箱里,为什么会有别人的旧文件
Cloudflare 给每个容器安排了独立虚拟机,回收的磁盘块却把上一位用户的数据带了进来。一次小写入让旧内容重新可读;修复既要让新分配的空间清零,也要清走已经保留旧内容的磁盘和镜像缓存。

文章导航
创建一个新沙箱,通常是为了得到一台可以放心折腾、用完就丢的电脑。里面可以编译陌生项目、运行代理生成的代码,出了问题也不该碰到其他人的工作。9 月 24 日,Accomplish 研究员 Oren Yomtov 披露,他在 Cloudflare 的沙箱里读到了其他客户留下的文件内容。研究者列出的类型包括 SQLite 数据库、Chromium 配置、.env 和凭据文件,并将使用同一磁盘实现的 Browser Run 列入影响范围。
数据是从新环境自己的磁盘里读出来的。Cloudflare 与研究团队的联合复盘把原因定位到 Containers、Sandboxes 使用的存储池:磁盘块在不同账户之间回收再分配,重新交付前跳过了清零。一次只覆盖部分空间的写入,就可能让余下的旧字节暴露出来。这条路径尤其值得理解,因为一个看似合理的检查——新盘读出来全是零——恰好可能漏掉它。
1 新磁盘的空间,要等写入时才分配
先把“磁盘”分成两层。程序看到的是一串可以读取和写入的地址;宿主机还要决定,这些地址对应物理存储的哪一块。薄置备,也就是 thin provisioning,把实际分配推迟到需要写入时。一个看起来很大的新盘,可以暂时只占很少的实际空间。Linux 的 dm-thin 用映射记录把两层地址接起来,多个薄盘可以从同一数据池取得空间。
这种安排把一个安全问题放到了分配时刻:一块空间上次归谁使用,这次交付前有没有处理旧内容?删除旧映射只解决“还属于谁”的问题。数据池如果把同一块实际空间给了下一台盘,留下的字节仍需要被新内容覆盖,或者先行清零。给新盘换名称、换标识、建立新的文件系统,都不能单凭这些动作保证每个底层字节已经改写。
事故中,每个容器运行在独立的 Firecracker 虚拟机内,可写根盘是 /dev/vdc。这里的隔离关系可以与 Firecracker 的设备模型一起理解:虚拟机通过交给它的块设备读写,设备背后的资源由宿主机提供。只要宿主机把带着旧内容的空间映射进这台盘,来宾在自己盘内的读取就能拿到它。这个过程不需要先控制另一台正在运行的虚拟机。
下面用公开的 Linux v6.12 实现解释分配逻辑。它用于核对 dm-thin 的行为,Cloudflare 没有公布事故现场的内核版本;本文也没有在该云服务上执行验证。process_cell() 先查地址映射。在没有映射、也没有外部 origin 数据源的读路径上,provision_block() 直接用零填充返回缓冲区,随后结束读取。物理块还没有分配。
所以,此时读到零,只能说明这次读取得到了零。稍后一次写入会进入分配路径,取得真实物理块,盘的状态从“没有映射”变成“已经映射”。要知道交给新用户的空间是否干净,检查必须跨过这个变化。
2 写入一小段,整块旧内容有了新主人
研究者使用 Workers Paid 账户启动自己的环境,直接读写 /dev/vdc 的原始字节,并在来宾 ext4 文件系统的空闲区选择对齐位置。残留内容可以不属于当前盘的任何正常文件;只列出当前目录,也就看不到这条读取路径。
Cloudflare 公布的现场参数是每个 thin block 为 64 KiB,存储池启用了 skip_block_zeroing。一次对齐的 4 KiB 写入触发整块分配,新数据覆盖了十六分之一,剩余 60 KiB 可能仍是上一个使用者的内容。这里的 60 KiB 是未覆盖空间的大小,实际能恢复什么还取决于那块空间的历史。

名字里的 skip 确实对应一个直接分支。Linux v6.12 的默认设置会清零新块,解析这个选项后则把 zero_new_blocks 设为 false。在 schedule_zero() 中,关闭清零会直接进入准备好的映射处理;启用时,部分写需要先完成清零。
接着,process_prepared_mapping() 把新关系写入映射树。此后读取已有映射会直达对应物理块,已经不走前面的“无映射就返回零”分支。这就把完整过程接上了:第一次读零,写入使真实空间归入当前盘,第二次读取能看见该空间尚未覆盖的部分。
整块覆盖是一个容易误导测试的对照。如果测试先向全部 64 KiB 写入新数据,再读回来,当然只能看见新内容。内核在启用清零时也允许完整覆盖跳过额外擦零,因为这次写入本身将替换整个块。要区分这两种情况,测试输入必须留下未写区域;只验证“我写的内容能读回来”,漏掉的正是其他区域里多出来的东西。
至于残留究竟来自哪里,文件名看着陌生还不够。研究团队使用 ext4 的目录校验机制排除自己的文件系统。ext4 文档说明,目录块校验涉及文件系统 UUID、inode 编号、inode generation 和目录内容。带入自己的文件系统身份计算后,5,614 个可测试目录块没有一个归属于研究者;再拿自己创建、删除的 162 个块作对照,全部正确归属。这组对照把“看起来陌生的字节”推进成了可检验的外来数据判断。
3 清零开关修好了,已有磁盘还留着旧状态
将清零重新打开以后,再走一遍刚才的分配,未写部分就会被清掉。不过,已经映射的块仍沿原来的关系读取。设想一台盘在修复前就取得了那块带残留的空间,修复后只继续读它:整个过程没有发生新分配,新的清零条件也就没有机会参与。
快照又让这些旧映射活得更久。dm-thin 的内部快照起初与原盘共享数据块;源码对写时复制的说明把这个关系写得很清楚:两棵映射树可以指向相同的数据,之后写入再分开。共享块的读取仍能直接使用已有物理地址。因此,修复后创建一个新实例,若继承了修复前的快照,也可能继承那份已经建立的映射。
本次实际需要清理的还包括宿主机上缓存的 OCI 镜像层所对应的预制 dm-thin 快照。镜像层本来用于复用准备工作,却同时保留了当时的磁盘状态。Cloudflare 的处置分成两个阶段:9 月 7 日 06:13 UTC 完成清零配置推广,9 月 19 日 15:03 UTC 完成修复前缓存的清理;期间退役旧容器磁盘、重启虚拟机并清理主机镜像缓存。这两个时间回答的是不同问题:何时不再产生新的脏分配,以及何时清走了仍会继承的旧状态。
这也决定了自建平台应该怎样验收。可以在隔离实验池中,用自有标记数据分别检查四条路径:未映射读取、释放后重新分配的部分写、整块覆盖,以及从修复前快照创建新盘。前三条区分分配和覆盖,最后一条检查存量是否还会传播。这样的测试需要控制物理块确实发生复用,并记录映射状态;没有读到标记时,还应排除“这次碰巧没分到旧块”。这是根据机制提出的验证方法,本文未执行这些实验。
4 平台已经修复,用户还需要知道什么
截至 10 月 2 日核对,Cloudflare 的公开结论是修复和清理已完成,客户无需改变配置或采取额外操作。它在保留的磁盘 I/O 遥测中回查,识别出的相关行为属于获授权的研究与工程验证,没有发现其他恶意利用迹象。公开材料没有给出完整暴露起止时间或逐客户泄露名单;一家公司是否有具体文件被读过,仍需供应商掌握的记录才能回答。
读取机会受同一宿主机上的回收空间限制,调度由平台决定,研究者无法指定客户或正在使用的磁盘。对使用者而言,这让影响判断需要落到实际工作负载:当时哪些任务曾把数据库、浏览器状态或秘密写进受影响的环境?如果供应商通知或自身记录进一步指向某个凭据,再围绕它的权限、有效期和使用记录撤销、更换与追查。
部署代理工作区的人,还应检查凭据是否真的需要进入盘。一次只用于访问外部 API 的秘密,可以由工作区外的受控服务代发请求;确需交给任务的凭据,应限制可访问对象和存活时间。这是减少未来磁盘残留后果的设计选择,不能改变已经发生过的读取,也不能代替平台对存储交接的修复。
这起事故给沙箱评估补上了一个具体问题:任务结束后,哪些状态会被下一次任务接着用?虚拟机进程会退出,物理块、镜像缓存和快照却可能继续存在。沿着这些对象从一个使用者转交给另一个使用者,才找得到这次错误发生的位置;只在新实例里检查一次空盘,会停在故事真正开始之前。
5证据与来源
5.1来源与材料
- Cloudflare 与 Accomplish:跨租户磁盘残留联合复盘https://blog.cloudflare.com/containers-cross-tenant-vulnerability/
- Accomplish:研究发现与受影响产品说明https://www.accomplish.ai/blog/escaping-the-cloudflare-sandbox/
- Linux:thin provisioning、清零与快照接口https://docs.kernel.org/admin-guide/device-mapper/thin-provisioning.html
- Linux v6.12:固定版本的 dm-thin 实现https://github.com/torvalds/linux/blob/adc218676eef25575469234709c2d87185ca223a/drivers/md/dm-thin.c
- ext4:元数据校验组成https://docs.kernel.org/filesystems/ext4/checksums.html
- Firecracker:虚拟机与块设备设计https://github.com/firecracker-microvm/firecracker/blob/e6f007968525f0ede4c9c9587c3ff1bed710a929/docs/design.md