5.4 KiB
QEMUtiny
https://github.com/user-attachments/assets/9ff4e5f2-9bfe-405a-a6b9-2ee43fb8352a
Abstract
QEMUtiny is a memory corruption vulnerability in QEMU's implementation of CXL Type-3
device emulation, reported against QEMU master 007b29752e and confirmed
working against 5e61afe (May 11, 2026).
QEMUtiny was discovered autonomously with V12 by Aaron Esau of the V12 security team.
Want to find issues like this in your own code? Try V12 at v12.sh.
The PoC chains two CXL mailbox bugs in hw/cxl/cxl-mailbox-utils.c: an
out-of-bounds read in GET_LOG, followed by an out-of-bounds write in
SET_FEATURE.
- OOB read:
cmd_logs_get_log()treats the CEL log offset as an array index in thememmove()source expression even though the CXL mailbox offset is in bytes. - OOB write:
cmd_features_set_feature()accepts byte offsets into several small feature write-attribute structures without checking thatoffset + bytes_to_copystays inside the selected structure.
We reported the bugs upstream. Maintainers state CXL support is currently for at non-virtualization use cases, so we feel comfortable release the PoC publicly.
The included poc.c is a working exploit that drives the emulated CXL mailbox from the guest through the device BAR. It depends on offsets for the specific QEMU build and host libc layout.
The exploit can be weaponized to work reliably across many QEMU versions using the OOB read to scan memory. However this is out of scope for this PoC.
"QEMUtiny"?
QEMU + Mutiny.
Offsets (USER FRIENDLY VERSION)
./update_poc_offsets.sh
- Replace
0x047E735with$(readelf -s qemu-system-x86_64 | grep cmd_logs_get_log | awk '{print $2}') - Replace
0x0341BB0with$(objdump -S qemu-system-x86_64 | grep "<memmove@plt>:" | awk '{print $1}') - Replace
0x01E72FF8with$(objdump -S qemu-system-x86_64 | grep "libc_start_main" | awk '{print $(NF-1)}') - Find libc:
ldd ./qemu-system-x86_64 | grep libc.so - Replace
0x2A200withreadelf -sW /lib/x86_64-linux-gnu/libc.so.6 | grep -i __libc_start_main | awk '{print $2}' - Replace
0x058750withreadelf -sW /lib/x86_64-linux-gnu/libc.so.6 | grep -i system@ | awk '{print $2}'
Building
./update_poc_offsets.sh
gcc -O2 -Wall -Wextra -o exp poc.c
The reproducer must be run as root inside the guest because it writes PCI config space and mmaps the CXL device BAR through sysfs.
sudo ./exp
One-line version:
git clone https://github.com/v12-security/pocs.git && cd pocs/qemu && gcc -O2 -Wall -Wextra -o exp poc.c && sudo ./exp
Test Setup
Use ./run_qemu_shell.sh. Then in the guest, use /exp
poc.c assumes the CXL Type-3 device appears in the guest at:
/sys/bus/pci/devices/0000:35:00.0
and that BAR2 is exposed as:
/sys/bus/pci/devices/0000:35:00.0/resource2
If your guest enumerates the device at a different BDF, update the two sysfs
paths in main().
How It Works
-
Mailbox access. The guest enables PCI memory decoding for the CXL device, maps BAR2, and sends CXL mailbox commands by writing the mailbox payload, command, and control registers directly.
-
CEL out-of-bounds read.
cmd_logs_get_log()checks the requested CEL range as ifoffsetwere a byte offset, but then performs pointer arithmetic oncci->cel_logas astruct cel_log *.poc.cusesGET_LOG_OOB_BASE_OFFSETto land just past the CEL buffer and read adjacent QEMU CXL state. -
QEMU address discovery. The out-of-bounds CEL read leaks a CXL mailbox command handler pointer and the
CXLType3Devheap address. The handler pointer gives the QEMU PIE base for this build. -
Rank sparing overflow. The demo sends
SET_FEATURE / RANK_SPARINGwith a non-zero feature offset and a large payload. The rank sparing case copies intoct3d->rank_sparing_wr_attrs + hdr->offsetwithout bounding the copy tosizeof(ct3d->rank_sparing_wr_attrs), so the payload continues into laterCXLType3Devfields. -
Fake memory dispatch state. The overflowed payload plants enough fake
FlatView, dispatch, section,MemoryRegion, andMemoryRegionOpsstate for the sanitize path to call a controlledMemoryRegionOps.writecallback. -
Callback trigger.
MEDIA_OPERATIONS / SANITIZEstarts a background operation. When the sanitize worker reachesaddress_space_set(), it walks the corrupted dispatch state and invokes the forged write callback. The demo first uses this to callmemmove()and leak libc, then repoints the callback tosystem("/bin/bash").
Affected Code Paths
The missing SET_FEATURE bounds check affects the PPR paths and the sparing
write-attribute paths:
soft_ppr_wr_attrshard_ppr_wr_attrscacheline_sparing_wr_attrsrow_sparing_wr_attrsbank_sparing_wr_attrsrank_sparing_wr_attrs
patrol_scrub_wr_attrs already has the intended style of bounds check.
Affected Versions
The full QEMUtiny chain uses two bugs.
- OOB read: the vulnerable
GET_LOGpath was introduced by056172691b(hw/cxl/device: Add log commands (8.2.9.4) + CEL), first released in QEMUv7.1.0. - OOB write: the vulnerable PPR and memory sparing
SET_FEATUREpaths were introduced by5e5a86bab8andda5cafdc4d, released in QEMU v11.0.0.
Credit
Found with V12 by Aaron Esau of the V12 security team. The weaponized PoC (qemu escape) was prepared by @xia0o0o0o.