<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Hamed Oukhay</title>
    <description>The latest articles on DEV Community by Hamed Oukhay (@oukhay).</description>
    <link>https://dev.to/oukhay</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3882804%2F13cb0cc8-478b-47ac-8dba-cc03dcfb6ab1.png</url>
      <title>DEV Community: Hamed Oukhay</title>
      <link>https://dev.to/oukhay</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/oukhay"/>
    <language>en</language>
    <item>
      <title>Firecracker MicroVMs: Run Untrusted Code Like Lambda Does, Then Snapshot It</title>
      <dc:creator>Hamed Oukhay</dc:creator>
      <pubDate>Tue, 20 Oct 2026 13:48:30 +0000</pubDate>
      <link>https://dev.to/oukhay/firecracker-microvms-run-untrusted-code-like-lambda-does-then-snapshot-it-2a6j</link>
      <guid>https://dev.to/oukhay/firecracker-microvms-run-untrusted-code-like-lambda-does-then-snapshot-it-2a6j</guid>
      <description>&lt;p&gt;Your AI agent just wrote a script. It looks reasonable. Now what? Run it on your laptop? In a container that shares your host kernel? Or wait 30 seconds for a full VM to boot?&lt;/p&gt;

&lt;p&gt;AWS Lambda faced this exact problem at enormous scale: run millions of untrusted functions, keep them isolated from each other, start them fast. Their answer is &lt;strong&gt;Firecracker&lt;/strong&gt;, an open-source microVM manager built on KVM. It boots a hardware-isolated virtual machine in about &lt;strong&gt;125 milliseconds&lt;/strong&gt; with under &lt;strong&gt;5 MiB&lt;/strong&gt; of memory overhead. It powers Lambda and Fargate, and Netlify recently migrated their edge functions to it, reporting roughly &lt;strong&gt;2 ms p99&lt;/strong&gt; cold-start latency.&lt;/p&gt;

&lt;p&gt;In this tutorial you will boot a Firecracker microVM, run an untrusted script inside it, &lt;strong&gt;snapshot&lt;/strong&gt; the running VM, and &lt;strong&gt;restore&lt;/strong&gt; it in a brand-new Firecracker process. Snapshot and restore is the killer feature here. It is the same mechanism behind Lambda SnapStart: boot once, snapshot at the ready state, restore in about a second whenever work arrives.&lt;/p&gt;

&lt;p&gt;The full code for this tutorial, every config and script, lives in the accompanying repo: &lt;a href="https://github.com/oukhay/draft-dev-samples" rel="noopener noreferrer"&gt;oukhay/draft-dev-samples&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A host with &lt;strong&gt;KVM access&lt;/strong&gt;: a physical machine with virtualization enabled in the BIOS, or an EC2 virtual instance with nested virtualization turned on (for example &lt;code&gt;c8i.xlarge&lt;/code&gt; with &lt;code&gt;--cpu-options "NestedVirtualization=enabled"&lt;/code&gt;; supported on the c8i, m8i and r8i families, off by default, enable at launch). Bare metal (for example &lt;code&gt;i3.metal&lt;/code&gt;) still works but is only a fallback now. Check with:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; /dev/kvm &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; /dev/kvm &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"KVM OK"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"KVM MISSING"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It will not work on a regular virtualized EC2 instance with nested virtualization off, or on a laptop without KVM. See &lt;code&gt;KVM-TEST-RUN.md&lt;/code&gt; for the full host options and costs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Firecracker &lt;strong&gt;v1.17.0&lt;/strong&gt; (latest at the time of writing), &lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;wget&lt;/code&gt;, &lt;code&gt;iproute2&lt;/code&gt;, &lt;code&gt;iptables&lt;/code&gt;, &lt;code&gt;ssh&lt;/code&gt;, plus &lt;code&gt;squashfs-tools&lt;/code&gt; and &lt;code&gt;e2fsprogs&lt;/code&gt; (Step 1 builds the root filesystem with &lt;code&gt;unsquashfs&lt;/code&gt; and &lt;code&gt;mkfs.ext4&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Download Firecracker, a kernel, and a root filesystem
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;ARCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;FC_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;v1.17.0
&lt;span class="nv"&gt;S3&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"https://s3.amazonaws.com/spec.ccfc.min"&lt;/span&gt;

&lt;span class="c"&gt;# Firecracker binary (the binary inside the tarball carries a version suffix)&lt;/span&gt;
curl &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; firecracker.tgz &lt;span class="se"&gt;\&lt;/span&gt;
  https://github.com/firecracker-microvm/firecracker/releases/download/&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;FC_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/firecracker-&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;FC_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;-&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ARCH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;.tgz
&lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-xzf&lt;/span&gt; firecracker.tgz
&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 755 release-&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;FC_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;-&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ARCH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;/firecracker-&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;FC_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;-&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ARCH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  /usr/local/bin/firecracker

&lt;span class="c"&gt;# The kernel and root filesystem come from the Firecracker CI artifacts, the&lt;/span&gt;
&lt;span class="c"&gt;# same source the official getting-started guide uses. Find the latest build:&lt;/span&gt;
&lt;span class="nv"&gt;CI_ARTIFACTS_PREFIX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$S3&lt;/span&gt;&lt;span class="s2"&gt;?list-type=2&amp;amp;prefix=firecracker-ci/&amp;amp;delimiter=/"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oP&lt;/span&gt; &lt;span class="s2"&gt;"(?&amp;lt;=&amp;lt;Prefix&amp;gt;)firecracker-ci/[0-9]{8}-[^/]+/(?=&amp;lt;/Prefix&amp;gt;)"&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Kernel: grab the newest vmlinux-* and save it as ./vmlinux&lt;/span&gt;
&lt;span class="nv"&gt;latest_kernel_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$S3&lt;/span&gt;&lt;span class="s2"&gt;?list-type=2&amp;amp;prefix=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CI_ARTIFACTS_PREFIX&lt;/span&gt;&lt;span class="k"&gt;}${&lt;/span&gt;&lt;span class="nv"&gt;ARCH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/vmlinux-"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oP&lt;/span&gt; &lt;span class="s2"&gt;"(?&amp;lt;=&amp;lt;Key&amp;gt;)(&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CI_ARTIFACTS_PREFIX&lt;/span&gt;&lt;span class="k"&gt;}${&lt;/span&gt;&lt;span class="nv"&gt;ARCH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/vmlinux-[0-9]+&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;[0-9]+&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;[0-9]{1,3})(?=&amp;lt;/Key&amp;gt;)"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-V&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
wget &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$S3&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;latest_kernel_key&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-O&lt;/span&gt; vmlinux

&lt;span class="c"&gt;# Root filesystem: the CI image is a squashfs with no SSH key baked in, so&lt;/span&gt;
&lt;span class="c"&gt;# generate one, patch the public key into the image, and build an ext4 from it&lt;/span&gt;
&lt;span class="nv"&gt;latest_ubuntu_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$S3&lt;/span&gt;&lt;span class="s2"&gt;?list-type=2&amp;amp;prefix=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CI_ARTIFACTS_PREFIX&lt;/span&gt;&lt;span class="k"&gt;}${&lt;/span&gt;&lt;span class="nv"&gt;ARCH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/ubuntu-"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oP&lt;/span&gt; &lt;span class="s2"&gt;"(?&amp;lt;=&amp;lt;Key&amp;gt;)(&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CI_ARTIFACTS_PREFIX&lt;/span&gt;&lt;span class="k"&gt;}${&lt;/span&gt;&lt;span class="nv"&gt;ARCH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/ubuntu-[0-9]+&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;[0-9]+&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;squashfs)(?=&amp;lt;/Key&amp;gt;)"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-V&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;ubuntu_version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;basename&lt;/span&gt; &lt;span class="nv"&gt;$latest_ubuntu_key&lt;/span&gt; .squashfs | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oE&lt;/span&gt; &lt;span class="s1"&gt;'[0-9]+\.[0-9]+'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
wget &lt;span class="nt"&gt;-O&lt;/span&gt; ubuntu-&lt;span class="nv"&gt;$ubuntu_version&lt;/span&gt;.squashfs.upstream &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$S3&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$latest_ubuntu_key&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
unsquashfs ubuntu-&lt;span class="nv"&gt;$ubuntu_version&lt;/span&gt;.squashfs.upstream
ssh-keygen &lt;span class="nt"&gt;-f&lt;/span&gt; id_rsa &lt;span class="nt"&gt;-N&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; squashfs-root/root/.ssh
&lt;span class="nb"&gt;cp &lt;/span&gt;id_rsa.pub squashfs-root/root/.ssh/authorized_keys
&lt;span class="nb"&gt;mv &lt;/span&gt;id_rsa ./ubuntu-&lt;span class="nv"&gt;$ubuntu_version&lt;/span&gt;.id_rsa
&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; root:root squashfs-root
&lt;span class="nb"&gt;truncate&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; 1G ubuntu-&lt;span class="nv"&gt;$ubuntu_version&lt;/span&gt;.ext4
&lt;span class="nb"&gt;sudo &lt;/span&gt;mkfs.ext4 &lt;span class="nt"&gt;-d&lt;/span&gt; squashfs-root &lt;span class="nt"&gt;-F&lt;/span&gt; ubuntu-&lt;span class="nv"&gt;$ubuntu_version&lt;/span&gt;.ext4
&lt;span class="nb"&gt;chmod &lt;/span&gt;400 ./ubuntu-&lt;span class="nv"&gt;$ubuntu_version&lt;/span&gt;.id_rsa
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two traps this step avoids, both verified the hard way. First, the old&lt;br&gt;
quickstart kernel still floating around (&lt;code&gt;vmlinux.bin&lt;/code&gt;, kernel 4.14) boots,&lt;br&gt;
but Ubuntu 24.04's systemd warns it is below its 4.15 baseline and the&lt;br&gt;
guest's sshd stalls for many minutes waiting on entropy, so every SSH step&lt;br&gt;
below looks broken. The CI kernel (6.18 in the verified run) is the matched&lt;br&gt;
pair for the CI rootfs and boots to SSH in about a minute. Second, there is&lt;br&gt;
no prebuilt &lt;code&gt;ubuntu-24.04.ext4&lt;/code&gt; to download anymore; the squashfs build&lt;br&gt;
above is the current official flow.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 2: Set up host networking
&lt;/h2&gt;

&lt;p&gt;The guest gets its IP from its MAC address. We create a tap device on the host and let the guest talk through it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ip tuntap add dev tap0 mode tap
&lt;span class="nb"&gt;sudo &lt;/span&gt;ip addr add 172.16.0.1/30 dev tap0
&lt;span class="nb"&gt;sudo &lt;/span&gt;ip &lt;span class="nb"&gt;link set &lt;/span&gt;dev tap0 up
&lt;span class="nb"&gt;sudo &lt;/span&gt;sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"echo 1 &amp;gt; /proc/sys/net/ipv4/ip_forward"&lt;/span&gt;

&lt;span class="nv"&gt;HOST_IFACE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;ip &lt;span class="nt"&gt;-j&lt;/span&gt; route list default | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.[0].dev'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;iptables &lt;span class="nt"&gt;-t&lt;/span&gt; nat &lt;span class="nt"&gt;-A&lt;/span&gt; POSTROUTING &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOST_IFACE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-j&lt;/span&gt; MASQUERADE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Boot the microVM through the Firecracker API
&lt;/h2&gt;

&lt;p&gt;Firecracker is configured entirely through a REST API on a Unix socket. Start it in one terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /tmp/fc-orig.socket
&lt;span class="nb"&gt;sudo &lt;/span&gt;firecracker &lt;span class="nt"&gt;--api-sock&lt;/span&gt; /tmp/fc-orig.socket
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a second terminal, configure and boot (the JSON files live in &lt;code&gt;configs/&lt;/code&gt; in the accompanying repo):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;SOCK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/tmp/fc-orig.socket

curl &lt;span class="nt"&gt;--unix-socket&lt;/span&gt; &lt;span class="nv"&gt;$SOCK&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; PUT http://localhost/boot-source &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; @configs/boot-source.json

curl &lt;span class="nt"&gt;--unix-socket&lt;/span&gt; &lt;span class="nv"&gt;$SOCK&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; PUT http://localhost/machine-config &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; @configs/machine-config.json

curl &lt;span class="nt"&gt;--unix-socket&lt;/span&gt; &lt;span class="nv"&gt;$SOCK&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; PUT http://localhost/drives/rootfs &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; @configs/drive-rootfs.json

curl &lt;span class="nt"&gt;--unix-socket&lt;/span&gt; &lt;span class="nv"&gt;$SOCK&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; PUT http://localhost/network-interfaces/net1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; @configs/network.json

&lt;span class="c"&gt;# API calls are async: give the configuration a beat to land&lt;/span&gt;
&lt;span class="nb"&gt;sleep &lt;/span&gt;0.5

curl &lt;span class="nt"&gt;--unix-socket&lt;/span&gt; &lt;span class="nv"&gt;$SOCK&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; PUT http://localhost/actions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"action_type": "InstanceStart"}'&lt;/span&gt;

&lt;span class="nb"&gt;sleep &lt;/span&gt;2
ssh &lt;span class="nt"&gt;-i&lt;/span&gt; ./ubuntu-24.04.id_rsa &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;StrictHostKeyChecking&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;no root@172.16.0.2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"ip route add default via 172.16.0.1 &amp;amp;&amp;amp; echo 'nameserver 8.8.8.8' &amp;gt; /etc/resolv.conf"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The configs are small and readable. &lt;code&gt;boot-source.json&lt;/code&gt; points at the kernel, &lt;code&gt;machine-config.json&lt;/code&gt; asks for 2 vCPUs and 1 GiB of RAM, &lt;code&gt;drive-rootfs.json&lt;/code&gt; attaches the Ubuntu image as the root device, and &lt;code&gt;network.json&lt;/code&gt; wires the tap device to the guest MAC &lt;code&gt;06:00:AC:10:00:02&lt;/code&gt;, which the guest maps to &lt;code&gt;172.16.0.2&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Run the untrusted script
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;guest/untrusted.sh&lt;/code&gt; stands in for code an AI agent just generated. We have not audited it. That is the entire point: it executes inside a hardware-isolated microVM, not on our host.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; ./ubuntu-24.04.id_rsa root@172.16.0.2 &lt;span class="s1"&gt;'bash -s'&lt;/span&gt; &amp;lt; guest/untrusted.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output (abridged):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;== whoami / hostname ==
root
ubuntu-fc-uvm
== init process (should be the guest's, not the host's) ==
    PID
      1 systemd
== trying to see the host (should fail) ==
ls: cannot access '/host': No such file or directory
== kernel cmdline (microVM kernel) ==
console=ttyS0 reboot=k panic=1
== done. The host never saw any of this. ==
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The guest has its own PID 1, its own kernel command line, and no path to the host. A container gives you namespaces and cgroups. This gives you a virtual hardware boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Snapshot the running microVM
&lt;/h2&gt;

&lt;p&gt;Snapshotting is a two-phase dance. First pause the VM, freezing all vCPUs and device emulation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--unix-socket&lt;/span&gt; &lt;span class="nv"&gt;$SOCK&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; PATCH http://localhost/vm &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"state": "Paused"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then create a &lt;strong&gt;full&lt;/strong&gt; snapshot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--unix-socket&lt;/span&gt; &lt;span class="nv"&gt;$SOCK&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; PUT http://localhost/snapshot/create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; @configs/snapshot-create.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;where &lt;code&gt;snapshot-create.json&lt;/code&gt; is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"snapshot_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Full"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"snapshot_path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./snapshot/vm.state"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"mem_file_path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./snapshot/vm.mem"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This produces three artifacts: the &lt;strong&gt;state file&lt;/strong&gt; (device and emulation state), the &lt;strong&gt;memory file&lt;/strong&gt; (a full copy of guest RAM), and your &lt;strong&gt;disk files&lt;/strong&gt;, which Firecracker leaves for you to manage. It drains and &lt;code&gt;fsync&lt;/code&gt;s the block devices during creation, and &lt;code&gt;fsync&lt;/code&gt;s the snapshot files by default, so the result is durable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Restore it in a new process, and time it
&lt;/h2&gt;

&lt;p&gt;Kill the original Firecracker process. The snapshot is now just files on disk. Start a fresh process on a new socket, then load:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /tmp/fc-restore.socket
&lt;span class="nb"&gt;sudo &lt;/span&gt;firecracker &lt;span class="nt"&gt;--api-sock&lt;/span&gt; /tmp/fc-restore.socket &amp;amp;
&lt;span class="nb"&gt;sleep &lt;/span&gt;0.5

&lt;span class="nb"&gt;time &lt;/span&gt;curl &lt;span class="nt"&gt;--unix-socket&lt;/span&gt; /tmp/fc-restore.socket &lt;span class="nt"&gt;-X&lt;/span&gt; PUT http://localhost/snapshot/load &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; @configs/snapshot-load.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;with &lt;code&gt;snapshot-load.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"snapshot_path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./snapshot/vm.state"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"mem_backend"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"backend_path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./snapshot/vm.mem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"backend_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"File"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"resume_vm"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Measured on the c8i.xlarge test host (nested virtualization), that &lt;code&gt;time&lt;/code&gt;&lt;br&gt;
came back at &lt;strong&gt;0.093s&lt;/strong&gt; real, and Firecracker's own log put the load request&lt;br&gt;
at 4.1 ms. The restored guest answered SSH immediately, with its uptime&lt;br&gt;
continuing from the original boot, because the guest clock is part of the&lt;br&gt;
snapshot.&lt;/p&gt;

&lt;p&gt;A few things worth noticing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Load is only allowed before the VM is configured.&lt;/strong&gt; The only things you may set up beforehand are logging and metrics.&lt;/li&gt;
&lt;li&gt;The memory file is &lt;strong&gt;memory-mapped&lt;/strong&gt; (&lt;code&gt;MAP_PRIVATE&lt;/code&gt;), so pages load on demand instead of all up front. That is why restore lands in about a second even for larger guests. (There is also a &lt;code&gt;Uffd&lt;/code&gt; backend if you want a userspace process to serve page faults.)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;resume_vm: true&lt;/code&gt; skips the separate resume call. Without it, the loaded VM sits in &lt;code&gt;Paused&lt;/code&gt; and you resume it with &lt;code&gt;PATCH /vm {"state": "Resumed"}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The TAP device and disk files must exist at the &lt;strong&gt;same relative paths&lt;/strong&gt; for the new process.&lt;/li&gt;
&lt;li&gt;Treat the memory file as &lt;strong&gt;immutable&lt;/strong&gt; after loading. It backs guest memory through the page cache; modifying it corrupts the guest.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Verify the restored guest is alive and identical:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; ./ubuntu-24.04.id_rsa root@172.16.0.2 &lt;span class="s2"&gt;"uptime; ls /tmp"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The pattern: pre-warmed sandboxes for agent code
&lt;/h2&gt;

&lt;p&gt;This is the use case that makes the feature matter. Instead of booting a sandbox per task:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Boot one template microVM with your toolchain baked into the rootfs.&lt;/li&gt;
&lt;li&gt;Warm it to the ready state (agent runtime listening, dependencies loaded).&lt;/li&gt;
&lt;li&gt;Snapshot it. Terminate the original.&lt;/li&gt;
&lt;li&gt;For every task: load the snapshot in a fresh Firecracker process, hand the task to the agent, destroy the VM when done.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every task gets a pristine, hardware-isolated environment in about a second, and nothing persists between tasks. Boot cost is paid once, amortized over thousands of restores. This is Lambda SnapStart's trick, and it is exactly what Netlify now does at the edge: snapshot when the runtime is listening, scale to zero, restore from the memory-mapped snapshot on the next request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas worth knowing before production
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vsock resets on snapshot.&lt;/strong&gt; Open vsock connections close; listen sockets in the guest survive and accept new connections after resume.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network is not guaranteed across restore.&lt;/strong&gt; Plan to re-establish connectivity rather than assuming it survives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snapshot files are only CRC-checked.&lt;/strong&gt; Firecracker trusts the host and the files. If snapshots cross a trust boundary, encrypt and authenticate them yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never resume the same snapshot twice&lt;/strong&gt; in production. Identifiers, entropy pools, and tokens would be duplicated. If guests must detect restores, expose the VMGenID device.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The guest clock freezes&lt;/strong&gt; at snapshot time. Update it after restore, or pass &lt;code&gt;clock_realtime: true&lt;/code&gt; on load (needs host kernel 5.16+).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snapshot after the kernel has booted&lt;/strong&gt;, not during early boot, or the restore may crash.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diff snapshots are still developer preview.&lt;/strong&gt; Full snapshots are the stable path.&lt;/li&gt;
&lt;li&gt;In production, run Firecracker under the &lt;strong&gt;&lt;code&gt;jailer&lt;/code&gt;&lt;/strong&gt; for an extra containment layer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reality check: your microVM is the security boundary, and boundaries get breached
&lt;/h2&gt;

&lt;p&gt;This tutorial treats the Firecracker microVM as a hardware-isolated sandbox for untrusted agent code. In the fall of 2026, security researcher Paulos Yibelo showed that the isolation boundary itself is not bulletproof. Through Vercel's Sandbox bug bounty challenge, he reported a flaw that let code inside a guest VM read host memory, and in his lab, on machines matching Vercel's configuration, he took it all the way to host root. Vercel confirmed the finding and paid the program's maximum $50,000 bounty. CEO Guillermo Rauch announced it on October 3, 2026: "We've confirmed a KVM 0day through our Vercel Sandbox bounty program."&lt;/p&gt;

&lt;p&gt;Why this matters for you: Vercel Sandbox runs untrusted AI workloads, including AI agent code, inside Firecracker microVMs on bare-metal EC2 hosts, and Vercel identifies the microVM, not the container, as its primary security boundary. The exact boundary this tutorial builds on. Yibelo's finding crosses exactly that line.&lt;/p&gt;

&lt;p&gt;The technical shape is worth understanding, because the bug was not in KVM at all. It was in the host Linux kernel's IPv4 stack (CVE-2026-99999, CVSS 9.3). A guest can hand the host deferred-checksum metadata (CHECKSUM_PARTIAL, via virtio-net and the TAP device) that the host trusts without validating it properly. When the host completes the checksum and later fragments the packet, it copies bytes of adjacent host memory into the fragments. Repeat that and you have a reliable host-memory leak from inside the guest, and Yibelo's write-up ("PwnEscape") shows a two-packet host crash and the lab escalation to root on top of it. KVM is the boundary that got crossed, not the code that was buggy. The vulnerable code was introduced in kernel 2.6.27, so effectively every host kernel before the fix was exposed.&lt;/p&gt;

&lt;p&gt;What to do with this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Patch the host.&lt;/strong&gt; The fix landed upstream on October 14, 2026; the first fixed stable kernels include 5.10.272, 5.15.223, 6.1.190, 6.6.159, 6.12.113 and 6.18.57, and a host reboot is required. Patching the guest does nothing for this class of bug, because the flaw lives below the guest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat it as a threat-model lesson, not just a patch instruction:&lt;/strong&gt; snapshot and restore is a deployment optimization, not a security control. Your actual containment stack is the &lt;code&gt;jailer&lt;/code&gt;, cgroup limits, host egress filtering, and keeping the host kernel patched.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assume breach:&lt;/strong&gt; log and monitor the host side too, because the boundary you trust the most deserves the most suspicion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calibrate the fear:&lt;/strong&gt; no exploitation in the wild and no customer data impact has been reported for this CVE, and the write-up publishes the mechanism but not the full root chain or exploit code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://pwn.ai/blog/pwnescape" rel="noopener noreferrer"&gt;https://pwn.ai/blog/pwnescape&lt;/a&gt; (Yibelo's write-up, part 1)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.cve.org/CVERecord?id=CVE-2026-99999" rel="noopener noreferrer"&gt;https://www.cve.org/CVERecord?id=CVE-2026-99999&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://vercel.com/blog/findings-from-the-1-million-dollar-vercel-sandbox-challenge" rel="noopener noreferrer"&gt;https://vercel.com/blog/findings-from-the-1-million-dollar-vercel-sandbox-challenge&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://aws.amazon.com/security/security-bulletins/2026-137-aws/" rel="noopener noreferrer"&gt;https://aws.amazon.com/security/security-bulletins/2026-137-aws/&lt;/a&gt; (AWS: managed EC2, Lambda and Fargate not affected; self-managed KVM and Firecracker hosts are)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://aws.amazon.com/security/security-bulletins/2026-124-aws/" rel="noopener noreferrer"&gt;https://aws.amazon.com/security/security-bulletins/2026-124-aws/&lt;/a&gt; (related, same week: an auth bypass and two more issues in Loom for AWS, AWS's agent orchestration platform)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;You booted a hardware-isolated microVM in a fraction of a second, ran untrusted code inside it, froze it to disk, and brought it back to life in a new process. That loop, boot, snapshot, restore, is the core mechanic behind serverless cold starts done right.&lt;/p&gt;

&lt;p&gt;Natural next steps: the &lt;code&gt;jailer&lt;/code&gt; for production hardening, vsock for host-to-guest communication without networking, and if you manage fleets of these, the Terraform provider for Firecracker to keep the host pools declarative.&lt;/p&gt;

</description>
      <category>firecracker</category>
      <category>aws</category>
      <category>linux</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
