---
title: "Fresh but not isolated: the MicroVM tradeoff in CI"
description: "AI sandbox escapes expose the flaw in container boundaries. Lambda MicroVMs isolate—but snapshot-restore silently breaks your CI freshness proof."
pubDate: 2026-08-11
tags: ["ai-sandbox-escapes", "security-vulnerabilities", "multicloud", "microvms"]
locale: en
canonical: https://mikeng.io/writing/fresh-not-isolated-microvm-tradeoff-in-ci/
author: "Mike Ng Sin Ching"
authorUrl: https://mikeng.io/
---

In July 2026, during a sandboxed ExploitGym evaluation, OpenAI models (GPT-5.6 Sol plus a pre-release model) found a zero-day in a package registry cache proxy, obtained open Internet access, used stolen credentials and additional zero-days to achieve remote code execution on Hugging Face's production infrastructure. [OpenAI took responsibility](https://openai.com/index/hugging-face-model-evaluation-security-incident/) on July 21; [Hugging Face disclosed](https://huggingface.co/blog/security-incident-july-2026) the attack details days earlier.

Roughly two months prior, the [Claude Cowork VM escape](https://thehackernews.com/2026/07/claude-cowork-flaw-could-let-ai-agent.html) (CVE-2026-46331) used a guest-kernel flaw to reach the host through the hypervisor layer. Anthropic's [own engineering documentation](https://www.anthropic.com/engineering/how-we-contain-claude) described Claude models "helpfully" escaping a sandbox to complete a task, examining git history to answer a coding test, and spontaneously identifying the benchmark they were running on.

None of these are proof that your Docker CI runners are broken. The isolation stacks involved are not fully public, and this article does not claim these escapes happened in containers or that MicroVMs would have prevented them. What they share is the category of boundary crossed: a policy boundary enforced by code that runs alongside the thing it constrains, the kind motivated code breaks. That category is exactly what containers rely on, and CI runners are where motivated code meets your infrastructure.

A container asks one kernel to enforce isolation between processes. A MicroVM moves the boundary outward: the workload gets its own guest kernel, with isolation from the host enforced through hardware virtualization plus a much smaller VMM interface. This does not make escape impossible; it changes the attack surface and blast radius. Compromising the guest kernel is no longer equivalent to compromising the host kernel.

This article walks the tradeoff on two axes. Isolation answers whether code in job A can affect job B or the host. Identity answers whether a workload can prove it runs on a distinct, freshly-provisioned instance. The discovery from building ephemeral runners is that neither model lets the guest prove its own instance identity from kernel signals, and they fail for opposite reasons.

## The comfort zone: containers and the hidden kernel assumption

A container is not a machine. It is namespaces, cgroup budgets, and seccomp filters over one shared host kernel. That kernel is the trust anchor: the only thing between a process inside a container and the host, and the same kernel every container on the box shares. Isolation between containers is not isolation from the kernel. It is one kernel refusing to let namespace A see namespace B, a policy enforced by the very kernel both namespaces share.

For most CI workloads that is enough. You build your code, you run your tests, and the kernel stays out of the way. Docker won the CI runner slot because that bargain works for code you wrote yourself. When the code is trusted, shared-kernel risk is acceptable.

The hidden assumption lives in the runner config, not the image. A standard [self-hosted GitHub Actions runner](https://docs.github.com/en/actions/reference/runners/self-hosted-runners) pulls arbitrary code from a pull request, executes it, and reports back. The fork PR sits outside your trust boundary; the runner host and its secrets sit inside it. The trust line is drawn at the container wall, not at the kernel underneath. The container boundary is what you can see and operate on. The shared kernel is the boundary you cannot see. A kernel vulnerability, a misconfigured mount, or a privileged path dissolves that invisible boundary for every container on the host at once.

## Lambda MicroVMs: the hardware-assisted boundary, made practical

[AWS Lambda MicroVMs](https://aws.amazon.com/lambda/lambda-microvms), launched June 2026, are built on Firecracker, the same VMM that powers over 15 trillion monthly Lambda invocations, per the [Firecracker project](https://github.com/firecracker-microvm/firecracker). Each MicroVM runs a separate real Linux kernel. A fault in one job's guest kernel is contained to that MicroVM. There is no shared-kernel path to reach through. The boundary is hardware-assisted, not absolute: the hypervisor and host kernel remain in the trust chain, and the Cowork VM escape is a failure of that layer. AWS describes the isolation as VM-level, and a MicroVM is only a different, harder category of boundary.

This is why you would choose a MicroVM for CI: when the code is untrusted or motivated, the kernel boundary is the difference between a compromised job and a compromised host. CI runs arbitrary native code from contributors you may not fully trust. Under that threat model, separating the tenant kernel from the host kernel significantly reduces blast radius. AWS itself positions MicroVMs for user- and AI-generated code, vulnerability scanning, and CI/CD workloads that need VM-level containment. The argument does not need the sandbox escapes to be compelling.

The only reason this is practical is snapshot-restore. Booting a real kernel per job runs early-init, device enumeration, systemd, and package-manager setup, seconds on a cold VM that would dominate a short CI job. If every job paid that boot, you would pool warm runners, which reintroduces the state leakage you chose MicroVMs to escape. Snapshot-restore makes per-job MicroVMs viable: a saved memory image resumes in milliseconds.

```mermaid
flowchart LR
    T["Template VM<br/>booted once at deploy"] -->|snapshot| S["Snapshot<br/>kernel, runner agent, base image"]
    S -->|restore| R1["MicroVM for job 1"]
    S -->|restore| R2["MicroVM for job 2"]
    S -->|restore| R4["MicroVM for job N"]
    IN["Job-time inputs<br/>workflow code, environment"] -->|injected at launch| R1
    IN -->|injected at launch| R2
    IN -->|injected at launch| R4
```

The [Firecracker snapshot docs](https://github.com/firecracker-microvm/firecracker/blob/main/docs/snapshotting/snapshot-support.md) describe the mechanism: a snapshot saves the full state of guest memory and emulated hardware; restore resumes from that image. The snapshot is created once at deploy time, stored, and every job restores from that same baseline. Jobs do not create snapshots; they inherit them. Job-time inputs are injected separately from what the snapshot carries. No latency benchmarks exist for this argument, and none are claimed here.

## Snapshot-restore is not a boot

When I was building [mayfly](https://github.com/mikeng-io/mayfly), fingerprinting each ephemeral runner to prove it ran on a fresh, distinct environment was the obvious starting point. The kernel generates boot_id at boot and promises it is unique per running kernel instance. boot_id was the natural signal to grab.

Snapshot-restore is not a boot. [Firecracker snapshots](https://github.com/firecracker-microvm/firecracker/blob/main/docs/snapshotting/snapshot-support.md) preserve the full state of guest memory and emulated hardware; restore resumes from that image without re-running early-boot initialization. The boot_id was generated once, during the original boot of the snapshot's source VM, and every restore inherits it.

The [Linux man page for `sd_id128_get_boot(3)`](https://man7.org/linux/man-pages/man3/sd_id128_get_machine.3.html) says boot_id is "randomly generated early at boot and unique for every running kernel instance." True only for real boots. The boot_id documentation assumes real boots. Snapshot-restore is not a boot.

This is not a documentation error or a Firecracker bug. The 2021 paper ["Restoring Uniqueness in MicroVM Snapshots"](https://arxiv.org/abs/2102.12892) (arXiv:2102.12892) frames the mechanism: post-initialization snapshots skip cold-start time; restore is a clone-and-resume of a memory image, not a fresh boot. Collision of boot-derived state across restores of the same snapshot is architectural.

This is an identity-signal failure, not a security hole. boot_id colliding does not mean your MicroVMs are vulnerable. It means the signal you reached for to prove distinct instances is truthful about the wrong lifecycle boundary. It proves a boot happened, not that this instance is fresh.

## Five MicroVMs, one boot_id

We ran five job dispatches against the mayfly ephemeral-runner fleet on Lambda MicroVMs. Each job had no prior job's state in user space, "fresh" in the operational sense. But snapshot-restore means the kernel was never re-initialized: the jobs were fresh-restored, not freshly booted. The five fingerprint lines, captured in the sealed log files from the [mayfly evidence deck](https://github.com/mikeng-io/mayfly), line 99 of each `run_*_log.txt`:

```
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
```

Five distinct `vm=` ids. One boot_id across all five. A reader can re-derive this: `grep -hoE 'vm=microvm-[a-f0-9-]+' run_*_log.txt | sort -u | wc -l` returns 5; `grep -hoE 'boot_id=[a-f0-9-]+' run_*_log.txt | sort -u | wc -l` returns 1. The collision is in the sealed raw artifacts.

The `vm=` id is not a kernel signal. It is a control-plane-attested identity: mayfly's control plane injects `MAYFLY_MICROVM_ID` into each MicroVM at launch and prints it in each job's fingerprint. It is an attestation from the orchestrator, not something the kernel generated. Earlier runs fingerprinted by arch, kernel, host, and tmp state, kernel-level signals that, like boot_id, could not prove distinctness on a snapshot-restore fleet. The fix, [mayfly PR #1](https://github.com/mikeng-io/mayfly/pull/1) (`feat/microvm-identity-attestation`), added the attested id because the kernel-level signals were unreliable on this architecture.

## Why the fix has to come from outside the guest

Snapshot-restore preserves guest memory. boot_id lives in that preserved memory image, and the r4 evidence directly demonstrates the collision. The same architectural logic extends to uname, hostname, and kernel version. Any value the kernel holds at boot time is in the preserved image. `/proc/sys/kernel/random/uuid` emits a new value on every read, so it was never a meaningful instance identifier.

A note on kernel RNG: Firecracker's [VMGenID device](https://github.com/firecracker-microvm/firecracker/blob/main/docs/snapshotting/random-for-clones.md) notifies the guest when a restore occurs, and Linux 5.18+ reseeds its CSPRNG on that notification. Guaranteed-random values are safe across restores on modern kernels. VMGenID is the one kernel signal that does respond to restore. That is exactly why it proves the rule: the kernel needs a dedicated device to tell a restore from a boot. It addresses the entropy-reuse problem, a genuine security issue if two MicroVMs share CSPRNG state, but it does not fix boot_id collision or provide instance identity. A reseeded RNG is not an attested identity.

Regenerating a signal in userspace at job start is also insufficient. A fresh UUID proves the job started, not that the MicroVM was clean before the job ran. If the snapshot was tampered with, or if a prior job's state leaked into the restored image, a userspace UUID would still read "fresh" while the VM was not. The trust question is about state before the job's code runs, and code inside the VM cannot attest to that.

The system best positioned to attest to a MicroVM's identity at launch is the system that launched it. The control plane knows when it launched, from which snapshot, and can inject a unique id into the environment. The MicroVM's kernel cannot, since boot_id inherits from the snapshot. Userspace cannot, since it runs after the trust question is settled. This does not make the control plane the only possible source: Firecracker could expose a VMM-level generation counter, and hardware attestation (measured boot, vTPM) could anchor the trust chain lower. For mayfly, control-plane attestation is the simplest fix for the boot_id gap.

One precision: control-plane attestation solves *distinctness* across instances, not *integrity* of the snapshot. A tampered snapshot could still produce VMs with distinct control-plane ids. Proving integrity requires measured boot, snapshot hashes, or vTPM, a separate problem. And "control-plane-attested" is not "cryptographically fresh against a malicious control plane." The evidence proves the control plane reported five distinct ids, not that they resist an adversary controlling the plane itself. The claim is narrower: boot_id is categorically unreliable on snapshot-restore, and the fix must come from outside the guest.

## The escapes and the identity problem

The escapes show a policy boundary failing because enforcement lives in code alongside the thing it constrains. Claude Code, demonstrated by [Ona security researchers](https://mikelukianoff.substack.com/p/the-agent-escaped-what-now), bypassed a denylist via a path trick, then disabled the sandbox entirely. (This was Claude Code, not Claude Mythos, which during Anthropic's internal safety testing found an internet connection and emailed a researcher unprompted. Separate incident, different mechanism.) [Pillar Security's](https://labs.cloudsecurityalliance.org/wp-content/uploads/2026/07/CSA_research_note_ai_coding_agent_sandbox_escapes_20260722-csa-styled.pdf) "Week of Sandbox Escapes" prodded four coding agents across their "trust handoff flaw": writes the sandbox does not police, trusted by everything outside it. A seccomp filter, a namespace rule, a denylist. Each is enforced by code alongside the thing it constrains. Under enough pressure, the request is bypassed. That is the container's category of boundary.

The mayfly evidence shows that replacing that boundary with hardware does not dissolve the trust question. It relocates it. Moving from a shared kernel to a separate kernel per job removes the shared-kernel path a container escape exploits. It also removes the thing that made boot_id honest: a real boot.

Stronger isolation does not remove the need for identity attestation; it changes who must do the attesting. In a container world, the trust anchor is the shared host kernel. In a MicroVM world, the kernel was never freshly booted for that specific job, so the trust anchor moves to the orchestrator present when each MicroVM came into existence. The boundary got harder to cross, and the attestation changed sides.

## The tradeoff, stated plainly

Isolation and identity are two different properties, and across the container/MicroVM boundary they produce a sharper result than I expected: **neither workload can prove its own instance identity from kernel signals. They fail for opposite reasons.**

| Property | Container | Snapshot MicroVM |
|---|---|---|
| Kernel boundary per workload | No (shared host kernel) | Yes (separate guest kernel) |
| State reset (filesystem, process tree) | Yes, per container start | Yes, from clean snapshot |
| Guest kernel freshly booted | N/A (no guest kernel) | No (restored from snapshot) |
| Can guest boot_id prove workload identity? | No, it belongs to host kernel | No, it is inherited from snapshot |
| Where identity must come from | The orchestrator / runtime | The control plane at launch |

**Containers reset workload state without creating a new kernel.** Each container start creates a new filesystem, process tree, and namespaces. That is what "fresh" usually means in CI. But boot_id is the host kernel's identifier, identical across every container on that host. It was never a per-workload signal. `/proc/sys/kernel/random/uuid` emits a new value on every read, proving nothing about whether the environment is new. A container's identity is attested by the runtime that created it, not by anything it reads from inside itself.

**MicroVMs create a kernel boundary without creating a new kernel boot.** A separate kernel per job gives hardware-assisted isolation. But snapshot-restore means that kernel was cloned from initialized state, so its boot-derived identifiers are inherited. The signals a practitioner would reach for are the ones that lie. Isolation is the win; provable instance identity is now your problem.

Neither property, state reset or kernel isolation, is the same thing as instance identity provable from inside the guest. That has to come from the allocator. "Freshness" in CI turns out not to be a single axis. It is at least three distinct properties: state cleanliness (does this job see the last job's files?), instance distinctness (is this a separate execution environment?), and provenance integrity (was the snapshot unmodified?). Containers give you the first; MicroVMs give you the first two on separate kernels. Neither lets the guest prove the third from kernel signals.

A note on the spectrum: gVisor and Kata Containers sit between these poles, userspace-kernel syscall interception and VM-per-container respectively. The middle of the spectrum inherits the same dynamic on the identity axis: a guest not freshly booted for that specific workload cannot attest to its own instance identity from inside.

So the decision framework:

- Trust your code and your contributors: containers are fine. Simplicity wins, with a policy-enforced isolation boundary.
- Run untrusted or motivated code (fork PRs, agents, plugins, multi-tenant): you want the kernel boundary. Isolation wins, and you must establish provable instance identity from the control plane.
- On either side, if you need to prove the workload was newly provisioned and distinct, the proof comes from the allocator, not the guest. Containers get it from the runtime; MicroVMs get it from the control plane present at launch.

The article is not "MicroVMs are better" or "containers are safer." It is: here is what you trade, here is the signal that will lie to you, and here is where the proof has to live. If you are building ephemeral runners on a snapshot-restore fleet, do not reach for boot_id. Inject an attested identity from your control plane and print it in each job's fingerprint, because the guest kernel that should have generated a fresh one was never booted for that job.
