Fresh 但唔 isolated:CI 入面 MicroVM 嘅 tradeoff
2026 年 7 月,喺一個 sandboxed ExploitGym evaluation 入面,OpenAI 嘅 models(GPT-5.6 Sol 加一個 pre-release model)喺一個 package registry cache proxy 度搵到一個 zero-day,攞到 open Internet access,用偷返嚟嘅 credentials 加埋其他 zero-days,成功喺 Hugging Face 嘅 production infrastructure 上面做到 remote code execution。OpenAI 喺 7 月 21 號承擔責任;Hugging Face 就早幾日披露咗成個攻擊嘅細節。
大概兩個月前,Claude Cowork VM escape(CVE-2026-46331)用咗一個 guest-kernel 嘅 flaw,穿過 hypervisor layer 去到 host。Anthropic 自己嘅 engineering documentation 描述咗 Claude models 點樣「好心幫手」逃出 sandbox 去完成任務,睇 git history 去答 coding test,仲自己認出佢哋跑緊嘅 benchmark。
呢啲全部都唔係證明你嘅 Docker CI runners 有問題。涉及嘅 isolation stacks 唔係完全公開,呢篇文章亦都唔係話呢啲 escapes 係喺 containers 入面發生,或者 MicroVMs 就一定會擋得住。佢哋共通嘅係嗰種被跨越嘅 boundary 類別:一個由 code 執行嘅 policy boundary,而嗰啲 code 就係同佢要約束嘅嘢一齊行,即係有心嘅 code 會整爛嗰種。嗰個類別正正就係 containers 依賴嘅嘢,而 CI runners 就係有心嘅 code 撞上你 infrastructure 嘅地方。
一個 container 係叫一個 kernel 去 enforce 唔同 process 之間嘅 isolation。一個 MicroVM 就將個 boundary 移向外邊:個 workload 有自己嘅 guest kernel,同 host 嘅 isolation 係透過 hardware virtualization 加一個細好多嘅 VMM interface 嚟 enforce。呢個並唔係話 escape 冇可能;佢改變嘅係 attack surface 同 blast radius。攻陷 guest kernel 唔再等於攻陷 host kernel。
呢篇文章會由兩個 axis 去睇呢個 tradeoff。Isolation 回答嘅係:job A 入面嘅 code 可唔可以影響到 job B 或者個 host。Identity 回答嘅係:一個 workload 可唔可以證明自己係跑喺一個獨立、啱啱 provision 嘅 instance 上面。起 ephemeral runners 嘅過程中我發現嘅係:兩個 model 都冇辦法令 guest 由 kernel signals 證明到自己嘅 instance identity,而且佢哋失敗嘅原因啱啱相反。
舒適區:containers 同埋嗰個隱藏嘅 kernel assumption
一個 container 唔係一部 machine。佢係 namespaces、cgroup budgets、同埋 seccomp filters,全部行喺同一個 shared host kernel 上面。嗰個 kernel 就係 trust anchor:係 container 入面嘅 process 同 host 之間唯一嘅嘢,亦都係部機上面每個 container 共用嘅同一個 kernel。Container 之間嘅 isolation 唔係同 kernel 隔離。佢係一個 kernel 拒絕俾 namespace A 睇到 namespace B,一個由兩個 namespace 共用嘅 kernel 去 enforce 嘅 policy。
對大部份 CI workloads 嚟講,咁已經夠用。你 build 你嘅 code,你跑你嘅 tests,個 kernel 唔會阻頭阻勢。Docker 之所以贏到 CI runner 呢個位,係因為呢個交易對你自己寫嘅 code 嚟講行得通。當個 code 係可信嘅時候,shared-kernel 嘅風險係可以接受嘅。
嗰個隱藏嘅 assumption 係喺 runner config 度,唔係喺個 image 度。一個標準嘅 self-hosted GitHub Actions runner 會由一個 pull request 度拉任意 code 落嚟,執行佢,然後回報結果。個 fork PR 係喺你嘅 trust boundary 外面;個 runner host 同埋佢嘅 secrets 係喺入面。條信任線係畫喺 container 道牆度,唔係畫喺下面嗰個 kernel 度。Container boundary 係你睇得到、操作得到嘅嘢。嗰個 shared kernel 係你睇唔到嘅 boundary。一個 kernel vulnerability、一個 set 錯嘅 mount、或者一條 privileged path,就會令部機上面所有 container 嘅嗰條隱形 boundary 一次過消失。
Lambda MicroVMs:硬件輔助嘅 boundary,整到實用
AWS Lambda MicroVMs,2026 年 6 月推出,係用 Firecracker 起嘅,即係嗰個每個月處理超過 15 trillion 次 Lambda invocations 嘅同一個 VMM,根據 Firecracker project 講。每個 MicroVM 行一個獨立嘅真 Linux kernel。一個 job 嘅 guest kernel 出錯,只會困喺嗰個 MicroVM 入面。冇一條 shared-kernel 嘅路可以穿過去。嗰個 boundary 係 hardware-assisted,唔係絕對:hypervisor 同 host kernel 仍然喺條 trust chain 入面,而 Cowork VM escape 正正就係嗰一層嘅 failure。AWS 形容嗰個 isolation 係 VM-level,而一個 MicroVM 只係一個唔同、更難跨越嘅 boundary 類別。
呢個就係點解你會為 CI 揀 MicroVM:當個 code 係唔可信或者有心嘅時候,kernel boundary 就係一個 compromised job 同一個 compromised host 之間嘅分別。CI 會跑嚟自你未必完全信任嘅 contributors 嘅任意 native code。喺嗰個 threat model 下面,將 tenant kernel 同 host kernel 分開,可以大幅縮細 blast radius。AWS 自己都將 MicroVMs 定位俾 user- 同 AI-generated code、vulnerability scanning、同埋需要 VM-level containment 嘅 CI/CD workloads。呢個論點根本唔需要嗰啲 sandbox escapes 都已經夠有力。
呢樣嘢之所以實用,唯一原因就係 snapshot-restore。每個 job 都 boot 一個真 kernel,即係要行 early-init、device enumeration、systemd、同 package-manager setup,一個 cold VM 要幾秒,足以主導一個短嘅 CI job。如果每個 job 都要俾呢個 boot 時間,你就會走去 pool warm runners,咁就重新引入你揀 MicroVMs 想避開嘅 state leakage。Snapshot-restore 令到每個 job 一個 MicroVM 變得可行:一個儲起咗嘅 memory image 可以喺幾毫秒之內 resume。
Firecracker 嘅 snapshot docs 有講呢個 mechanism:一個 snapshot 會儲起 guest memory 同 emulated hardware 嘅完整狀態;restore 就係由嗰個 image resume。個 snapshot 喺 deploy 嗰陣整一次,儲起,然後每個 job 都係由同一個 baseline restore。Jobs 唔會自己整 snapshot;佢哋係繼承返嚟。Job-time inputs 係同 snapshot 帶住嘅嘢分開注入。呢個論點冇任何 latency benchmarks,呢度亦都唔會聲稱有。
Snapshot-restore 唔係一次 boot
我起 mayfly 嗰陣,最自然嘅起點就係要 fingerprint 每個 ephemeral runner,去證明佢係跑喺一個新鮮、獨立嘅環境上面。Kernel 喺 boot 嗰陣會生成 boot_id,並且保證每個 running kernel instance 都係獨一無二。boot_id 就係最自然攞嚟用嘅 signal。
Snapshot-restore 唔係一次 boot。Firecracker snapshots 會保留 guest memory 同 emulated hardware 嘅完整狀態;restore 係由嗰個 image resume,唔會重新行 early-boot initialization。個 boot_id 係喺 snapshot 源頭嗰部 VM 最初 boot 嗰陣生成一次,之後每個 restore 都係繼承同一個。
Linux 嘅 man page 講 sd_id128_get_boot(3) 話 boot_id 係「喺 boot 早期隨機生成,對每個 running kernel instance 都係獨一無二」。呢句嘢只係對真 boot 先至成立。Boot_id 嘅 documentation 假設嘅係真 boot。Snapshot-restore 唔係一次 boot。
呢個唔係 documentation 出錯,亦都唔係 Firecracker 嘅 bug。2021 年嗰篇論文 「Restoring Uniqueness in MicroVM Snapshots」(arXiv:2102.12892)講咗個 mechanism:post-initialization snapshots 跳過 cold-start 時間;restore 係一個 memory image 嘅 clone-and-resume,唔係一次 fresh boot。同一個 snapshot 嘅多次 restore 會撞 boot-derived state,呢個係 architecture 上嘅必然。
呢個係 identity-signal 嘅 failure,唔係 security hole。Boot_id 撞咗唔代表你嘅 MicroVMs 有 vulnerability。佢只係話你攞嚟證明 instance 獨立嗰個 signal,對住嘅係錯嘅 lifecycle boundary。佢證明嘅係有一次 boot 發生過,唔係呢個 instance 係新鮮。
五個 MicroVMs,一個 boot_id
我哋喺 Lambda MicroVMs 上面,用 mayfly 嘅 ephemeral-runner fleet 跑咗五次 job dispatch。每個 job 喺 user space 都冇上一個 job 嘅 state,喺 operational 意義上係「fresh」。但 snapshot-restore 意味住個 kernel 從來冇重新初始化過:啲 jobs 係 fresh-restored,唔係 freshly booted。嗰五行 fingerprint,係由 mayfly evidence deck 嘅 sealed log files 度 capture 出嚟,每個 run_*_log.txt 嘅第 99 行:
run_0: vm=microvm-2e884347-1180-3795-a56b-700f24f02a6b boot_id=78869c81-58e5-409c-8f75-a066367e3603
run_1: vm=microvm-89906b33-d73b-3c8a-949a-b40fd44812f5 boot_id=78869c81-58e5-409c-8f75-a066367e3603
run_2: vm=microvm-c1e6e7ec-5a42-3a4e-83c4-5c4b7bd8c591 boot_id=78869c81-58e5-409c-8f75-a066367e3603
run_3: vm=microvm-b51161d1-208f-3c2e-8f72-1cd00f3e0469 boot_id=78869c81-58e5-409c-8f75-a066367e3603
run_4: vm=microvm-1b317b3f-f2eb-3e56-b321-d9d67d9e87a9 boot_id=78869c81-58e5-409c-8f75-a066367e3603
五個唔同嘅 vm= ids。五個全部都係同一個 boot_id。讀者可以自己重複驗證:grep -hoE 'vm=microvm-[a-f0-9-]+' run_*_log.txt | sort -u | wc -l 會出 5;grep -hoE 'boot_id=[a-f0-9-]+' run_*_log.txt | sort -u | wc -l 會出 1。個 collision 係喺 sealed raw artifacts 入面。
個 vm= id 唔係一個 kernel signal。佢係一個 control-plane-attested identity:mayfly 嘅 control plane 喺 launch 嗰陣會將 MAYFLY_MICROVM_ID 注入每個 MicroVM,然後喺每個 job 嘅 fingerprint 度 print 出嚟。佢係 orchestrator 嘅 attestation,唔係 kernel 生成嘅嘢。早前嘅 runs 用 arch、kernel、host、同 tmp state 嚟 fingerprint,呢啲 kernel-level signals 同 boot_id 一樣,喺一個 snapshot-restore fleet 上面都證明唔到 distinctness。個 fix,mayfly PR #1(feat/microvm-identity-attestation),就係加咗嗰個 attested id,因為 kernel-level signals 喺呢個 architecture 上面唔可靠。
點解個 fix 一定要由 guest 外面嚟
Snapshot-restore 會保留 guest memory。Boot_id 就係喺嗰個被保留嘅 memory image 入面,而 r4 嘅 evidence 直接示範咗個 collision。同樣嘅 architecture 邏輯延伸到 uname、hostname、同 kernel version。任何 kernel 喺 boot 嗰陣持有嘅 value 都係喺嗰個被保留嘅 image 入面。/proc/sys/kernel/random/uuid 每次讀都會出一個新 value,所以佢從來都唔係一個有意義嘅 instance identifier。
講返 kernel RNG:Firecracker 嘅 VMGenID device 喺 restore 發生嗰陣會通知個 guest,而 Linux 5.18+ 收到通知之後會重新 seed 佢嘅 CSPRNG。Guaranteed-random 嘅 values 喺現代 kernels 上面跨 restore 都係安全。VMGenID 係唯一一個會對 restore 有反應嘅 kernel signal。呢個正正就係證明咗個 rule:kernel 需要一個專用 device 先至分到 restore 同 boot。佢解決咗 entropy-reuse 問題,呢個係一個真正嘅 security issue,如果兩個 MicroVMs 共享 CSPRNG state 嘅話,但佢並冇 fix boot_id collision,亦都提供唔到 instance identity。一個 reseeded RNG 唔係一個 attested identity。
喺 job 開始嗰陣喺 userspace 重新生成一個 signal 都係唔夠。一個 fresh UUID 證明嘅係個 job 開始咗,唔係個 MicroVM 喺 job 跑之前係乾淨。如果個 snapshot 俾人 tamper 過,或者上一個 job 嘅 state 漏咗入個 restored image,一個 userspace UUID 仍然會讀做「fresh」,但個 VM 其實唔係。個 trust 問題係關於 job 嘅 code 跑之前嘅 state,而 VM 入面嘅 code 係冇辦法 attest 到嗰樣嘢。
最適合去 attest 一個 MicroVM 喺 launch 嗰陣嘅 identity 嘅系統,就係 launch 佢嗰個系統。個 control plane 知道佢幾時 launch、由邊個 snapshot launch,亦都可以喺個 environment 度注入一個 unique id。個 MicroVM 嘅 kernel 做唔到,因為 boot_id 係由 snapshot 繼承返嚟。Userspace 都做唔到,因為佢跑嗰陣個 trust 問題已經定咗。呢個並唔係話 control plane 係唯一可能嘅 source:Firecracker 可以 expose 一個 VMM-level generation counter,而 hardware attestation(measured boot、vTPM)可以將條 trust chain 錨得更低。對 mayfly 嚟講,control-plane attestation 係 fix boot_id gap 最簡單嘅方法。
要講清楚一點:control-plane attestation 解決嘅係唔同 instance 之間嘅 distinctness,唔係 snapshot 嘅 integrity。一個被 tamper 嘅 snapshot 仍然可以生出有唔同 control-plane ids 嘅 VMs。要證明 integrity 就需要 measured boot、snapshot hashes、或者 vTPM,呢個係另一個問題。而「control-plane-attested」亦都唔係「對住一個 malicious control plane 都仲係 cryptographically fresh」。個 evidence 證明嘅係 control plane 報告咗五個唔同嘅 ids,唔係話佢哋抵抗到一個控制住成個 plane 嘅 adversary。個 claim 係比較窄:boot_id 喺 snapshot-restore 上面係 categorical 咁唔可靠,而個 fix 一定要由 guest 外面嚟。
啲 escapes 同埋 identity 問題
啲 escapes 顯示咗一個 policy boundary 點樣失效,因為 enforcement 係喺 code 入面,同佢要約束嘅嘢並排。Claude Code,由 Ona security researchers 示範,用一個 path trick 繞過咗個 denylist,然後成個 sandbox 熄咗佢。(呢個係 Claude Code,唔係 Claude Mythos,後者喺 Anthropic 內部 safety testing 期間搵到一個 internet connection,仲主動 email 咗一個 researcher。獨立事件,唔同 mechanism。)Pillar Security 嘅「Week of Sandbox Escapes」用佢哋嘅「trust handoff flaw」撩咗四個 coding agents:sandbox 冇管嘅 writes,外面所有嘢都信晒。一個 seccomp filter、一條 namespace rule、一個 denylist。每一個都係由同佢要約束嘅嘢並排嘅 code 去 enforce。壓力夠大嘅時候,個 request 就會被 bypass。嗰個就係 container 嗰個類別嘅 boundary。
mayfly 嘅 evidence 顯示,用 hardware 取代嗰個 boundary 並唔會解決個 trust 問題。佢只係將佢移去第二度。由 shared kernel 轉去每個 job 一個獨立 kernel,移除咗 container escape 會利用嘅 shared-kernel 路徑。同時亦都移除咗令 boot_id 誠實嘅嘢:一次真 boot。
更強嘅 isolation 唔會消除 identity attestation 嘅需要;佢只係改變咗邊個要做 attestation。喺 container 世界,trust anchor 係嗰個 shared host kernel。喺 MicroVM 世界,個 kernel 從來冇為嗰個特定 job 新鮮 boot 過,所以個 trust anchor 移咗去每個 MicroVM 出現嗰陣在場嘅 orchestrator。個 boundary 難跨咗,而 attestation 換咗邊。
個 tradeoff,講白咗
Isolation 同 identity 係兩個唔同嘅 properties,而喺 container/MicroVM 嘅 boundary 上面,佢哋產生咗一個比我預期更 sharp 嘅結果:兩個 workload 都冇辦法由 kernel signals 證明到自己嘅 instance identity。佢哋失敗嘅原因啱啱相反。
| Property | Container | Snapshot MicroVM |
|---|---|---|
| 每個 workload 嘅 kernel boundary | 冇(shared host kernel) | 有(獨立 guest kernel) |
| State reset(filesystem、process tree) | 有,每次 container start | 有,由乾淨 snapshot |
| Guest kernel 有冇新鮮 boot | N/A(冇 guest kernel) | 冇(由 snapshot restore) |
| Guest boot_id 可唔可以證明 workload identity? | 唔可以,佢屬於 host kernel | 唔可以,佢係由 snapshot 繼承 |
| Identity 一定要由邊度嚟 | 個 orchestrator / runtime | Launch 嗰陣嘅 control plane |
Containers 會 reset workload state,但唔會開一個新 kernel。 每次 container start 都會開一個新 filesystem、process tree、同 namespaces。呢個就係 CI 入面通常講嘅「fresh」。但 boot_id 係 host kernel 嘅 identifier,喺嗰部 host 上面每個 container 都係一樣。佢從來都唔係一個 per-workload signal。/proc/sys/kernel/random/uuid 每次讀都會出一個新 value,證明唔到個 environment 係咪新。一個 container 嘅 identity 係由 create 佢嗰個 runtime 去 attest,唔係由佢喺自己入面讀到嘅任何嘢。
MicroVMs 會整一個 kernel boundary,但唔會有一次新嘅 kernel boot。 每個 job 一個獨立 kernel 俾到 hardware-assisted isolation。但 snapshot-restore 意味住嗰個 kernel 係由 initialized state clone 出嚟,所以佢啲 boot-derived identifiers 係繼承返嚟。一個 practitioner 會伸手去攞嘅 signals 正正就係會呃你嗰啲。Isolation 係贏嘅嘢;可證明嘅 instance identity 而家係你嘅問題。
State reset 或者 kernel isolation,呢兩個 properties 都唔等於可以喺 guest 入面證明到 instance identity。嗰樣嘢一定要由 allocator 嚟。CI 入面嘅「freshness」原來唔係一個單一 axis。佢至少係三個唔同嘅 properties:state cleanliness(呢個 job 會唔會見到上一個 job 嘅 files?)、instance distinctness(呢個係咪一個獨立嘅 execution environment?)、同 provenance integrity(個 snapshot 有冇被改過?)。Containers 俾到你第一個;MicroVMs 喺獨立 kernels 上面俾到你頭兩個。兩者都冇辦法令 guest 由 kernel signals 證明到第三個。
講返個 spectrum:gVisor 同 Kata Containers 係喺呢兩個極端中間,分別係 userspace-kernel syscall interception 同 VM-per-container。個 spectrum 中間繼承咗同一條 identity-axis 嘅動態:一個冇為嗰個特定 workload 新鮮 boot 過嘅 guest,係冇辦法由入面 attest 到自己嘅 instance identity。
所以個 decision framework 就係:
- 信你嘅 code 同你嘅 contributors:containers 就夠。簡單取勝,加一個 policy-enforced isolation boundary。
- 跑唔可信或者有心嘅 code(fork PRs、agents、plugins、multi-tenant):你要 kernel boundary。Isolation 取勝,而你一定要由 control plane 建立可證明嘅 instance identity。
- 無論邊一邊,如果你要證明個 workload 係新 provision 同獨立,個 proof 一定要由 allocator 嚟,唔係由 guest。Containers 由 runtime 攞;MicroVMs 由 launch 嗰陣在場嘅 control plane 攞。
呢篇文章唔係話「MicroVMs 好啲」或者「containers 安全啲」。佢係話:呢度就係你 trade 咗啲乜,呢度就係會呃你嘅 signal,同埋個 proof 一定要住喺邊度。如果你係喺一個 snapshot-restore fleet 上面起 ephemeral runners,唔好伸手去攞 boot_id。由你嘅 control plane 注入一個 attested identity,然後喺每個 job 嘅 fingerprint 度 print 出嚟,因為嗰個應該生成一個新鮮 boot_id 嘅 guest kernel,從來都冇為嗰個 job boot 過。