How to Use the QEMU CXL PoC Bootloader: Complete Setup and Execution Guide

The QEMU CXL PoC bootloader is a two-stage system comprising a 16-bit BIOS bootloader (boot.asm) that loads a 32-bit protected-mode payload (stage2.c) from a floppy image to demonstrate CXL mailbox escape vulnerabilities in QEMU.

The bikini/exploitarium repository hosts a proof-of-concept that demonstrates host-side code execution through CXL (Compute Express Link) Type 3 device emulation vulnerabilities in QEMU. The QEMU CXL PoC bootloader serves as the entry point for this exploit, providing a minimal real-mode bootstrap that transitions into a sophisticated protected-mode payload capable of manipulating CXL PCI topology and mailbox registers.

Bootloader Architecture and Components

The bootloader implementation resides in the qemu-cxl-type3-mailbox-escape-poc/ directory and consists of two distinct execution stages that bridge BIOS initialization with the exploit payload.

Real-Mode Bootstrap (boot.asm)

The first stage is implemented in boot.asm as a 16-bit BIOS bootloader that occupies the Master Boot Record (MBR) of the floppy image. This assembly code performs minimal hardware initialization, clears segment registers, establishes a stack at 0x7c00, and saves the boot drive identifier passed by the BIOS in the DL register. Using BIOS interrupt 0x13 (ah=0x02), it reads 16 sectors starting from sector 2 of the floppy into physical memory at address 0x8000, which contains the pre-built stage2 binary.

Protected-Mode Payload (stage2.c)

The second stage resides in stage2.c as a freestanding 32-bit C binary compiled without standard library dependencies. This payload executes immediately after the bootloader switches the CPU to protected mode, configuring the CXL PCI topology, driving the CXL mailbox registers to leak host pointers, and ultimately triggering a host-side system() call through a forged memory-region callback. Unlike the bootloader, which contains no exploit logic, stage2.c houses all vulnerability-specific code that proves code execution inside the QEMU process.

Building the QEMU CXL PoC Bootloader

Creating the bootable floppy image requires assembling the bootloader, compiling the stage2 payload, and linking them into a flat binary format suitable for QEMU emulation.

Prerequisites and Build Scripts

The build process depends on NASM (Netwide Assembler) for the bootloader and a GCC cross-compiler capable of generating 32-bit x86 code (multilib support required). The build.sh script automates the entire compilation pipeline:


# Assemble the 16-bit bootloader

nasm -f bin boot.asm -o boot.bin

# Compile stage2 as freestanding 32-bit code

gcc -ffreestanding -m32 -nostdlib -c stage2.c -o stage2.o

# Link using the custom linker script to produce flat binary

ld -m elf_i386 -T stage2.ld stage2.o -o stage2.bin

The linker script stage2.ld ensures the stage2 binary is position-independent and loads at the expected physical address 0x8000, matching the bootloader's jump target.

Generating the Floppy Image

The build script combines the bootloader and stage2 binary into a 1.44MB floppy image (poc.img) by writing boot.bin to sector 1 and stage2.bin to sector 2:

dd if=/dev/zero of=poc.img bs=512 count=2880
dd if=boot.bin of=poc.img bs=512 count=1 conv=notrunc
dd if=stage2.bin of=poc.img bs=512 seek=1 conv=notrunc

Executing the Exploit in QEMU

Running the QEMU CXL PoC bootloader requires specific command-line arguments to enable CXL Type 3 device emulation and attach the exploit floppy image.

QEMU Configuration Parameters

The run.sh script launches QEMU with the exact configuration required for the exploit:

qemu-system-x86_64 \
  -machine type=q35,cxl=on \
  -device cxl-type3,bus=root_bus0,memdev=cxl-mem0,id=cxl-dcd0 \
  -object memory-backend-file,id=cxl-mem0,share=on,mem-path=cxl-region-0.raw,size=256M \
  -device cxl-type3,bus=root_bus0,memdev=cxl-volatile0,id=cxl-dcd1 \
  -object memory-backend-file,id=cxl-volatile0,share=on,mem-path=cxl-region-1.raw,size=256M \
  -fda poc.img \
  -serial stdio

This configuration enables CXL support on the q35 machine type, instantiates two CXL Type 3 devices with 256 MiB volatile and 256 MiB dynamic-capacity memory backends, and attaches poc.img as floppy drive A (fda).

Boot Sequence and Exploit Flow

When QEMU boots with the floppy attached, the QEMU CXL PoC bootloader executes the following sequence:

  1. Real-mode initialization: Clears interrupts, sets up segment registers (DS, ES, SS), and establishes the stack pointer.
  2. Disk I/O: Invokes int 0x13 to load 16 sectors (8 KB) from sector 2 into the buffer at physical address 0x8000.
  3. Protected mode transition: Loads a Global Descriptor Table (GDT) with code and data segments, sets the Protection Enable (PE) bit in Control Register 0 (CR0), and performs a far jump to 0x08:0x8000 to enter 32-bit protected mode.
  4. Payload execution: The stage2 entry point configures the CXL device mailbox, sends commands to the CXL root complex, and manipulates memory region callbacks to escape the VM and create the marker file /tmp/qemu_cxl_escape_marker on the host filesystem.

Technical Deep Dive: Assembly Boot Sequence

The boot.asm source contains the precise machine instructions that bridge BIOS hand-off to the C payload. The code loads the GDT descriptor and switches CPU modes using privileged instructions:

; Switch to protected mode
lgdt [gdt_desc]           ; Load Global Descriptor Table
mov eax, cr0
or eax, 1                 ; Set Protection Enable bit
mov cr0, eax
jmp 0x08:pmode            ; Far jump to protected mode

BITS 32
pmode:
    mov ax, 0x10          ; Data segment selector
    mov ds, ax
    mov esp, 0x90000      ; New stack for protected mode
    jmp 0x08:0x8000       ; Jump to stage2 entry point

The GDT defines two segments: a code segment at selector 0x08 (base 0, limit 4GB) and a data segment at selector 0x10. The jmp 0x08:0x8000 instruction flushes the CPU pipeline and begins executing the 32-bit payload located at the memory address where the bootloader loaded stage2.c.

Summary

The QEMU CXL PoC bootloader demonstrates a complete chain of trust exploitation from BIOS boot to host code execution:

  • The repository at bikini/exploitarium provides a two-stage bootloader (boot.asm and stage2.c) specifically designed to exploit CXL Type 3 mailbox implementations in QEMU.
  • Building requires NASM and GCC with 32-bit support, using build.sh to produce the poc.img floppy image.
  • Execution depends on QEMU with CXL support enabled, configured with 256 MiB volatile and dynamic-capacity memory regions.
  • The bootloader transitions from real mode to protected mode via GDT initialization and a far jump to 0x8000, handing control to the stage2.c payload.
  • Successful exploitation creates /tmp/qemu_cxl_escape_marker on the host, proving arbitrary code execution within the QEMU process.

Frequently Asked Questions

What hardware and software are required to run the QEMU CXL PoC bootloader?

You need a Linux system with QEMU version 6.2 or later compiled with CXL support, NASM (Netwide Assembler), and GCC with multilib support for 32-bit compilation (gcc -m32). The PoC runs entirely in emulation, so no physical CXL hardware is required.

Is the QEMU CXL PoC bootloader safe to run on production systems?

No. This proof-of-concept is designed specifically to demonstrate security vulnerabilities and achieves host-side code execution. Only run this bootloader in isolated virtual environments or dedicated research sandboxes, as it intentionally exploits memory safety issues to create arbitrary files on the host filesystem.

What CXL device configuration does the exploit emulate?

The bootloader operates alongside QEMU's emulation of CXL Type 3 devices with both volatile and dynamic-capacity memory regions. The run.sh script configures two 256 MiB memory backends (cxl-region-0.raw and cxl-region-1.raw) attached to the CXL root complex, which the stage2 payload manipulates through PCI configuration space and mailbox registers.

How does the bootloader transition from real mode to protected mode?

The transition occurs in four steps: first, the code disables interrupts and loads a minimal GDT descriptor using the lgdt instruction; second, it modifies Control Register 0 (CR0) to set the Protection Enable bit; third, it executes a far jump to selector 0x08 to flush the CPU pipeline; finally, it initializes 32-bit segment registers and jumps to the stage2 entry point at physical address 0x8000.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →