I read the first PwnEscape write-up in October and came away with a leak and a promise. The pwn.ai post showed a guest VM pulling roughly 32 bytes of host kernel memory out with every crafted packet, then stopped, because the rest of the chain was being held back. The follow-up is out now, three weeks after the patch, and it delivers: the memory primitive becomes a function call of the guest’s choosing, executed inside the Firecracker process running the VM.
Most reaction I saw treated it as proof that Firecracker is broken, which skips the useful part. The demo targeted Firecracker v1.16.1 running as root, with no jailer, launched under systemd-run and pinned with taskset. Firecracker’s own documentation says production deployments should start it only through the jailer, which drops privileges and then execs the binary as an unprivileged process. The escape is real, and what decided how far it went was a deployment flag.
Both of those facts need to stay in the same sentence.
🔗 What Part 2 Did That Part 1 Could Not
Part 1 gave the guest a read primitive into host physical memory. Part 2 needed three more things: find the Firecracker process, find code worth calling, and find the saved registers of the thread running the VM.
The first step is a search problem with a checksum. The guest writes a nonce into one of its own pages; one resolver walks Extended Page Table roots until a translation lands on a page containing it, a second walks the host page tables backwards to recover the VMM’s page table root, and a third finds the ELF image and recovers the load bias ASLR stole.
The frame finder is the clever bit. While the guest runs, each vCPU thread is parked inside ioctl(vcpu_fd, KVM_RUN, 0), and the kernel keeps that thread’s userspace registers in a pt_regs frame, because returning to userspace means restoring them. The instruction pointer says where Firecracker resumes. The stack pointer says which stack it uses. Once you can write the frame, both are yours. Stale frames match on address alone, so the search also checked the syscall in progress, the KVM_RUN request, and the resolved stack pointer. That left one.
The rewrite was five registers: RIP to the write wrapper, RDI to 2 for stderr, RSI to a marker string on Firecracker’s own stack, RDX to 8, and RSP to a two-word return sequence. Then the guest wrote one byte to its serial port. That forced the vCPU out of KVM_RUN, the kernel restored the modified frame, and Firecracker’s thread ran write(2, "N8V23M2\n", 8) with its own privileges.
Recovery was planned. A successful write returns 8 while KVM_RUN should return zero, so the first return address pointed at xor eax,eax; ret, and the second sent execution back to the original ioctl continuation. The helper restored the three stack words it borrowed. Firecracker stayed alive on PID 2936, SSH stayed responsive, and the marker appeared on the host’s stderr once.
🎯 The Boring Syscall Is the Scary Part
The proof call is write to stderr, and that choice is the lesson. It is a call Firecracker must be allowed to make, because that is how it emits logs. Seccomp filters which system calls a process may issue, not which arguments you hand them, so an allowed call with an attacker-chosen buffer and length still executes. When people say seccomp would have stopped this, they usually mean the jailer would have, and those are not the same control.
What changes the outcome is the uid. Run the VMM as root and the guest’s chosen call runs as root. Run it through the jailer and the same chain lands you as an unprivileged user in a chroot with whatever capabilities survived the drop, and host root needs a second escalation from there.
The difference shows in what an operator sees. The crash path is loud: two crafted packets took down a host and every VM on it. The takeover path is quiet. The VM keeps running, the guest keeps its shell, nothing panics, and no log line says the vCPU’s saved instruction pointer was rewritten. As far as the public remediation analysis goes, there is no indicator of compromise for this bug, so detection has to be behavioral: panics that line up with guest traffic, abnormal fragmentation, odd VMM behavior after guest network activity. A silent escape that leaves the VM healthy is a worse property than a crash. It is also what an attacker would pay for.
🧱 The MicroVM Was Only Half the Boundary
Firecracker’s documentation is blunt about its threat model. It says all vCPU threads are considered to be running malicious code as soon as they start, and that containment comes from nesting trust zones between guest and host. The jailer sets up cgroups and chroot, drops privileges, and execs the VMM unprivileged. Seccomp filters ship by default, with a --no-seccomp flag the docs label as not for production. The microVM was always layer one.
This is where the scope argument starts, and both sides are fair. The headline is accurate for the lab: code ran as root on the host, in a configuration with no jailer. The skeptical reading, which showed up in comment threads within hours, is that a jailed deployment would absorb the chain into an unprivileged process and require a second bug. Analysis of the incident agrees: Vercel runs the jailer, restricts host permissions per microVM, and disables nested virtualization. None of that fixes the kernel bug, but it shrinks what an escape reaches.
What does not depend on any of it is the leak. Thirty-two bytes per packet, repeatable as long as the guest keeps sending, demonstrated against Vercel’s production sandboxes. Vercel says no report showed anyone reaching customer data, and I believe the scoping. But a boundary that leaks its neighbor’s kernel memory on request is not one you keep calling sound. The crash and the code execution were lab results on hardware built to match production. The leak was production.
🩹 A Defense-in-Depth Patch on an 18-Year-Old Bug
The kernel side is CVE-2026-99999, published October 15, 2026 by the kernel CNA. The component is the IPv4 stack, specifically skb_checksum_help() during fragmentation in net/ipv4/ip_output.c, fed by virtio-net checksum metadata the guest supplies. The guest says “compute this checksum, starting here”, the host defers it, and the deferred write lands inside an IPv4 header the rest of the stack already trusted. In the published example the header length field moved from 5 to 15, so fragmentation treated a 20-byte header as 60 and copied 40 bytes of host memory into the fragments.
The CNA and NVD rate it 9.3 with AV:L/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H; Red Hat and SUSE rate it 8.8 with PR:L, reasoning that sending packets from a guest is itself a privilege. Either way, scope change and all three impacts are maxed.
The code dates to Linux 2.6.27 in 2008. The fix is commit 6785011f8b16, “ipv4: validate checksum_start before completing checksum”, written by Michael S. Tsirkin at Red Hat, merged to mainline on October 14 and shipped in stable 5.10.272, 5.15.223, 6.1.190, 6.6.159, 6.12.113, 6.18.57, and 7.2.11. Yibelo reported it privately on September 4.
Upstream calls that fix defense in depth, not closure: packets carrying metadata like this should not be accepted at all, and more complete validation is still being worked on. This instance is fixed, the class is open. Budget for the next one instead of treating this as the one that got away.
AWS published bulletin 2026-137 the same day: managed EC2, Lambda, and Fargate are not affected, self-managed KVM with virtio-net is, and there are no workarounds.
🧪 What I Would Check Before Monday
If you run untrusted code in microVMs, the checklist is short.
- Patch the host kernel and confirm with
uname -rthat you booted it. A fixed kernel in the package cache protects nothing. - Confirm Firecracker starts through the jailer. Grep your launchers for direct invocations and for
--no-seccomp. - Confirm the VMM is not root. That control decided the severity of the published demo, and it is one
psaway. - Turn off nested virtualization on hosts that run other people’s guests, and if you cannot patch today, disable checksum offload on untrusted guests to buy a window. Vendor analyses describe that stopgap as untested against this exact flaw.
- Watch for the loud version: host panics correlated with guest network activity, and fragmentation rates. The quiet version leaves nothing to grep.
The AI agent case needs its own line. A lot of us now put model-driven code into microVMs because we do not trust it, and the appeal is a boundary small enough to reason about. Part 2 is a reminder that the boundary is the guest kernel, the host kernel, the VMM process, and the sandbox around that process. The last one is configuration, not architecture, and configuration rots.
🧠 Final Thoughts
The interesting thing about Part 2 is not that a microVM fell over. It is that the fall stopped where a uid drop had been configured, and kept going where it had not. An 18-year-old bounds check produced a 32-byte leak, the leak produced a register rewrite, and the rewrite produced a call that returned cleanly enough to leave the VM healthy. The kernel fix is explicitly defense in depth. The jailer is a handful of flags.
So patch the kernel, then go read your jailer flags, in that order. Only one of those two was ever going to be a single command.
Thanks for reading.