← 返回所有文章
發佈於 · 作者 Renaud Deraison

空閒空間並不是空的

9 月 24 日,Cloudflare 與 Accomplish 的 Oren Yomtov 公開了 Cloudflare Containers 與 Sandboxes 中一個跨租戶的缺陷。在一個新開的沙箱裡寫入 4 KiB,再讀原始磁碟,同一個區塊剩下的 60 KiB 就會帶著前一個租戶的 .env 檔案、憑證檔案與 Chromium 設定檔回來。沒有任何東西逃出任何東西:每一道活著的邊界都守住了,漏的地方在於一個區塊的主人用完它之後發生了什麼。Cloudflare 賣 Sandboxes,就是為了跑語言模型生出來的程式碼。Bromure Agentic Coding 把那份程式碼跑在你自己 Mac 上的一個映像檔裡,而它取用的空閒區塊是你的。

沙箱有兩份工作。關住裡面在跑的東西,以及忘掉之前在裡面跑過的東西。第二份是一個 設定選項。

你把代理的測試套件送進雲端沙箱,因為把沒有審過的程式碼跑在自己筆電上,大概從去年 某個時候起就不再顯得聰明了。沙箱大約一秒就起來,帶著一個完整的 Linux 檔案系統。 代理寫了一個小檔案。在那底下,平台遞給它一個 64 KB 的磁碟區塊,而其中 60 KB 還裝著 別人的 .env。

你不需要從任何地方逃出來,就走到了那裡。

寫入四 KiB,讀出六十

9 月 24 日,Cloudflare 與研究者一起發表了一份報告, 談 Cloudflare Containers 裡一個跨租戶的缺陷。Accomplish 的 Oren Yomtov 在 9 月 4 日 透過 HackerOne 回報,並在同一週 發表了他自己的記述。 The Hacker News 隔天做了報導。

Containers 用 Linux 的 device-mapper 精簡配置,從一個共用池裡給每個客戶一塊磁碟, 區塊大小是 64 KiB。那個池帶著 skip_block_zeroing 選項,而它做的就是名字說的事: dm-thin 在把新配置的區塊交給容器之前,不會先清掉它。容器被刪掉時,它的區塊會帶著 原本的內容回到池裡,而那個池是跨越客戶帳號的。

從那裡開始,手法只有四步,而且沒有任何漏洞利用:

  1. 找出容器自己的 ext4 檔案系統認為是空閒的、對齊到 64 KiB 的區域。
  2. 在每一個裡面寫入單獨一個 4 KiB 的區塊。那就足以讓 dm-thin 從共用池裡配置一個 實體的 64 KiB 區塊。
  3. 讀原始裝置,而不是檔案系統。
  4. 留下另外那 60 KiB。

Cloudflare 自己描述這個原語的那句話,就是整個臭蟲:「覆蓋整個區塊的寫入會取代它 先前的內容,但比較小的寫入只會改動被寫到的那一部分。」

撿回來的區塊裡帶著可用的結構。Cloudflare 報告了「目錄結構、資料庫分頁,以及結構上 完整的 SQLite 資料庫」,並在六個生產放置點上數出2,700 個不同的、屬於他人的目錄 inode,24 個放置點中有 18 個留有殘留材料。The Hacker News 報導,橫跨四個大洲的 22 台底層機器中,有 20 台撿得到資料。Yomtov 列出他從那些區塊上讀到的東西:「目錄 列表、SQLite 資料庫、Chromium 設定檔、.env 檔案,以及憑證檔案。」

Cloudflare 說明了兩個框住這起事件的界限。攻擊者「無法挑選特定的受害者,也無法存取 一塊正在被掛著的磁碟」,所以你拿到的是曾經待在那台機器上的隨便某人,而不是你挑的 目標。而且 Cloudflare 用研究者的概念驗證做出偵測特徵,拿去跑歷史上的磁碟活動遙測, 結果「沒有找到任何證據顯示這個特定的攻擊向量曾被其他人利用」。

Cloudflare 動得很快。9 月 4 日 15:26 UTC 回報;18:45 確認生產環境設定錯誤;21:27 合併執行期修正;22:03 合併池設定;23:15 開始推出。這些全都在同一天之內。推出在 9 月 7 日完成,研究者在 9 月 14 日確認他們的概念驗證已死,Cloudflare 在 9 月 19 日 清掉了最後一批緩解前的磁碟快照。客戶沒有任何動作要做,也沒有辦法從沙箱裡面確認 自己的區塊是不是乾淨地送到。

一個 64 KiB 區塊的池、兩個客戶、不清零容器,客戶 A.env憑證sqlite 分頁、chromium 設定檔已刪除 — 區塊回到池裡沙箱,客戶 B在空閒空間寫入 4 KiB讀 /dev/vdc,不是 fs留下另外的 60 KiB一個 Workers Paid 帳號,僅此而已共用 dm-thin 池 — 64 KiB 區塊,跨越客戶帳號skip_block_zeroing — 已釋放的區塊會帶著原內容被重新配置交給客戶 B 的那個區塊已寫入 4 KiB先前在這裡的人的 60 KiBfs 說這是空閒空間;原始裝置不同意
一次 4 KiB 的寫入,一個 64 KiB 的區塊。Cloudflare Containers 從一個區塊大小為 64 KiB 的共用 device-mapper 精簡池裡配置磁碟,而那個池被設成 skip_block_zeroing,所以某個客戶釋放的區塊會帶著原本的內容回到池裡。往容器自己的 ext4 檔案系統相信是空閒的區域寫入 4 KiB,就會觸發其中一個區塊被配置出來;讀原始裝置而不是檔案系統,就會拿回另外那 60 KiB。研究者在六個生產放置點上數出 2,700 個不同的、屬於他人的目錄 inode,而他們讀到的東西包括 .env 檔案、憑證檔案、SQLite 資料庫與 Chromium 設定檔。

每一道活著的邊界都守住了

Yomtov 那篇報告的標題,「Escaping the Cloudflare Sandbox」,低估了這個臭蟲有多安靜。

他沒有找到記憶體安全的臭蟲,沒有權限提升,也沒有容器逃脫。在整起事件期間,圍著一個 正在執行的容器的每一道邊界,都按設計守住了。客戶 B 讀不到客戶 A 活著的容器,而 Cloudflare 也指出,沙箱同樣碰不到一塊正在被掛著的磁碟。

漏的地方坐在交接那一段。Cloudflare 從一個租戶手上釋放一個區塊,再把它發給下一個, 而站在這兩個客戶之間的東西,是池設定裡的一個布林值。

研究者把注意力花在沙箱裡的程式碼現在能碰到什麼,花在上一個住戶留下了什麼的則少得 多。後面那個問題大多數時候有個無聊的答案:平台會把區塊清零。這個答案一直成立,直到 某個工程師為了吞吐量把清零關掉。

Accomplish 把這件算成自 2026 年 7 月以來公開的第六個沙箱逃脫。前五個是:Claude Cowork 的 SharedRoot、Claude Code 的 Beltdown、Cursor CLI 的 Beltdown2、 Docker VMM 裡的一個,以及 OpenAI Codex 沙箱裡的一個。這六個裡有五個,跑的是代理寫出 來的程式碼。Yomtov 自己的結論只有一行:「隔離可以用很多種方式失敗。」

這正是代理的程式碼原本該去的地方

Cloudflare Sandboxes 跑在 Containers 上,所以繼承了這個臭蟲。Sandboxes 也正是 Cloudflare 為這個部落格一直在寫的那件事所賣的產品。照 Cloudflare 自己的文件,這個 SDK 的存在 是為了「在隔離的環境裡安全執行不受信任的程式碼」,它用於「安全而可靠地執行大型語言 模型生成的程式碼」,並且適合「需要執行不受信任程式碼的 AI 代理、程式碼助手與自主 系統」。每個沙箱都跑「在它自己的隔離容器裡,帶著完整的 Linux 環境,在維持效能的同時 提供強固的安全邊界」。

這裡面每一個子句都是合理想要的東西,而它描述 Bromure 的工作區 VM,大致跟描述一個 Cloudflare Sandbox 一樣貼切。差別在於機器上還有誰。

把代理的執行搬離你的筆電,是把一個信任決定換了位置,而不是把它移除。你現在信任的是 一個你看不到的池配置器,在一個你沒有挑的放置點裡,旁邊是沒有人會替你指名的租戶。那 可以是對的交換。要刻意做這個交換,並且先問殘留的問題:我的空閒區塊是從哪裡來的, 裡面原本有什麼。

磁碟是你 Mac 上的一個檔案

Bromure Agentic Coding 回答這個問題的方式,是沒有池。

每個工作區對應到剛好一個 VM。兩個工作區永遠不共用一個 VM,一個工作區也永遠不會有 兩個。VM 從兩塊虛擬磁碟開機,而它們是你 Mac 上那個工作區自己目錄裡的普通檔案。 disk.img 是系統磁碟,第一次啟動時作為本機 Ubuntu 基礎映像的 APFS 寫入時複製複本 建立出來:瞬間完成,而且在客體寫入之前不佔空間。home.img 是一個私有的稀疏 ext4 映像,裝著 /home/ubuntu,以第二個 virtio-blk 裝置掛上。

那四步的手法在這裡沒有地方可以落地。客體裡的代理可以讀原始的區塊裝置而不是檔案 系統,因為它在裡面是 root,那沒關係。它碰到的是屬於一個工作區的一個映像檔。那個檔案 長大時會長進去的區塊,來自你自己 Mac 的 APFS 儲存區,而你 Mac 上一個空閒區塊的上一個 住戶就是你。

工作區之間有一個共用的東西,而「什麼都不共用」會是錯的說法,所以把它指名出來:每一塊 工作區磁碟都是從 base.img 複製出來的。Ubuntu 24.04,邏輯 24 GB、實體 6 到 8 GB, 對著一份簽章過的映像目錄取得,那份目錄帶著用同一把替應用程式更新簽章的金鑰所做的 ed25519 簽章,涵蓋映像的身分、它的 SHA-256 以及每一道 postinstall 指令,有防回滾 保護,也會拒絕沒有簽章的目錄。它在每一台安裝上都逐位元組相同,並且不含任何屬於你的 東西。那正是適合共用的那種東西:一個工作就是要處處相同的公開產物。

一個多租戶的放置點客戶 A客戶 B…以及其他所有人一個 64 KiB 區塊的池釋放的區塊會重新發給下一個人到手是乾淨的:那是設定,不是保證區塊裡原本有什麼.env、憑證檔案sqlite 資料庫、chromium 設定檔六個放置點上有 2,700 個他人的目錄 inode。24 個中有 18 個留有殘留。客戶端無事可做。Bromure Agentic Coding,在你的 Mac 上base.imgUbuntu 24.04、簽章目錄、處處相同、裡面沒有你的東西工作區一disk.img — CoW 複本home.img — 私有 ext4工作區二disk.img — CoW 複本home.img — 私有 ext4那些區塊裡有什麼ghp_… 與 sk-ant-api03-brm-… — 佔位符空閒區塊來自你自己的儲存區。真正的祕密留在主機代理裡,並在送出時換上。
同一個問題的兩種答案。在一個多租戶的放置點上,沙箱的磁碟是對一個池裡區塊的租用,而那個池和一些沒有人告訴你的客戶共用,那些區塊是不是乾淨地送到,是平台設定的性質。在 Bromure Agentic Coding 裡,工作區的系統磁碟是一份簽章過、逐位元組相同的公開基礎映像的 APFS 寫入時複製複本,它的 home 是一個私有的稀疏 ext4 映像,兩者都是那個工作區自己目錄裡的檔案,而它們長大時會長進去的空閒區塊來自你自己的儲存區。第二個差別在於那些區塊裡被寫進了什麼:Cloudflare 案例中撿回來的產物是 .env 與憑證檔案,而在 Bromure 的客體裡,那些檔案裝的是保住結構的假值,真正的值留在 Mac 上的主機代理裡。

而那些殘留一文不值

區塊的配置方式是答案的一半。另一半是一個寫程式的代理往磁碟上寫了什麼,而那正是讓 這個臭蟲值得被回報的原因:.env 檔案、憑證檔案、Chromium 設定檔。

在 Bromure 的工作區裡,那些檔案存在,讀它們的工具也照常運作,只是裡面的值是假的。 VM 裡沒有任何檔案、環境變數或行程,曾經持有真的 API 金鑰、OAuth token、AWS 祕密或 SSH 私鑰。客體拿到的是保住結構的佔位符:sk-ant-api03-brm-…、一個長得像 ghp_ 的 GitHub token、brm_…,每台安裝各自決定性地產生,所以客戶端的驗證器會滿意,也 不會有工具以為某把金鑰在兩次工作階段之間輪換過。真正的值住在 Mac 上主機代理的記憶體 內置換表裡,每一個都綁到它所屬的目的主機,並在送出時被換上,範圍是完全相符或子網域, 絕不是子字串比對。AWS 請求在主機端重新簽章:客體用一個假的祕密簽,主機把那個簽章 剝掉,再重新簽一次。SSH 是透過 vsock 橋接的純簽章,私鑰的位元組完全不會進到客體裡。

Chromium 設定檔,Bromure 另外處理。代理操作的那個瀏覽器窗格,跑在它自己的短命 Alpine VM 裡,是暫存目錄中的一個寫入時複製複本,你把窗格收起來時應用程式就會刪掉它。 保留登入狀態是選擇加入的,而那會把設定檔放到一塊每個工作區各自的加密磁碟上。

如果未來某個缺陷暴露了一個 Bromure 客體的區塊,讀的人拿走的會是一棵目錄樹、你已經 有了的原始碼,以及一個在任何地方都花不出去的憑證檔案。

把可丟棄當成動詞

工作區是刻意持久的。寫程式需要持久的狀態:複製下來的儲存庫、套件快取、工具鏈、shell 歷史。所以 Bromure 控制的是 VM 被允許持有什麼,而不是每晚把它毀掉,而抹掉一塊 磁碟是你要求的事。應用程式對每一種要求的方式都講得很明白:

重設回基礎

刪掉 disk.img,讓下一次啟動重新複製一份新的基礎映像。對話框會指名哪些東西會 消失:你 apt install 過的一切,以及你對 /etc、/var、/usr、/opt 的 修改。home 會留著。

抹掉 home、刪除工作區

抹掉 home 會丟掉 /home/ubuntu 和產生出來的金鑰對。刪除工作區會移走整個設定檔 目錄、兩個映像、加密的祕密檔案、每一個檢查點以及主機端的 SSH 材料,然後釋放它 保留的 MAC 位址。

用完就丟與檢查點

bromure-cli vm run --rm 給你一個 docker 風格的工作區,VM 停下來時就被刪掉。 每一次成功開機都會留下一個磁碟與 home 的寫入時複製檢查點,而且只在一次產生出可用 終端機的開機之後才取,所以每一個都被證明得以開機。

強制抹除

如果代理伺服器抓到一個佔位憑證正前往它不該去的地方,Bromure 會把那個工作區標成 已遭入侵,而它在你抹掉它的磁碟與 home 之前不會再開機。你的設定、token 與 SSH 金鑰都保住,而抹除永遠不會碰到共用的主機資料夾。

這一組動詞,就是你要求過的抹除與你假設有發生的抹除之間的差別。

問問上一個房客留下了什麼

Cloudflare 找到了設定錯誤,在九小時內修好,清掉了池與快照,做出偵測特徵去檢查過去, 並公開了區塊大小與那個旗標的名字。把這件事讀成一家廠商把糟糕的一天處理得不錯。

「沙箱」為一個關於邊界的希望取了名字,而這道邊界結果是一個資源的生命週期,而不是 一道牆。業界對於代理執行沒有審過的程式碼,給出的答案一直是把執行搬到別的地方,並且 相信那個別的地方會乾淨地送到。它大多數時候是乾淨的。你沒辦法從沙箱裡面檢查這件事, 這週不行,任何一週都不行。

把程式碼跑在你看得到的基礎設施上,殘留的問題就換成由你自己回答:每個工作區一個映像 檔、來自你自己儲存區的空閒區塊、只有一個共用的東西而那是一份簽章過的公開基礎映像, 而且就算那塊磁碟真的漏了,上面也沒有什麼值得讀。抹掉它是一個選單項目。