QEMU 11.1.2 leftovers
QEMU v11.1.2 closed the xHCI timer reentrancy. xhci_complete still DMA-writes the event ring into the doorbell. NVMe SGL still spans into BAR0. COLO parse_packet_early still has no L4 floor.
- Name
- QEMU 11.1.2 leftovers
- Series
- QEMU 11.1.2 leftovers
- Phase
- 0 · The pin
- Type
- 0-day analysis
- CVE
- n/a
- CVE Risk
- high
- Disclosure Status
- public
- Vendor
- QEMU
- Affected
- QEMU v11.1.2 (4fc49f46dc95d4a27de2509e7fceb2931e91faeb). Debian sid 1:11.1.2+ds-3. Virtualization use case: KVM/HVF + x86_64 pc/q35/microvm.
- Published
- 16 Oct 2026
- Updated
- 16 Oct 2026
- Tags
- 0-day, qemu, uaf, oob, xhci, nvme, colo, local
The timer was closed. The complete path still writes the doorbell.
A named CVE is a fence. The leftover is whatever still walks around it.
I am abraxas. This is the series hub for three leftovers I labbed on QEMU v11.1.2 (4fc49f46dc95d4a27de2509e7fceb2931e91faeb), posted as @abraxas_null. Debian sid is 1:11.1.2+ds-3. The virtualization use case in docs/system/security.rst is KVM or HVF plus x86_64 pc / q35 / microvm. Guest, VNC/SPICE/WebSocket, NBD and live migration, user-supplied files, and PCI/USB passthrough are untrusted. TCG isolation is not a guarantee. Monitor and QMP are privileged control.
v11.1.2 closed the xHCI timer reentrancy. xhci_complete still DMA-writes the event ring with the TYPE_XHCI guard clear. NVMe still rejects a mapping only when the start sits in BAR0. COLO parse_packet_early still never requires an L4 header. Three independent leftovers. Same pin. Same class of mistake: the public fix named one entry and left the writer next to it.
Vendor intake is a GitLab confidential issue on qemu-project/qemu. The project no longer takes email as the primary path. Packs are AGPLv3 under github.com/abraxas. Labs clone github.com/qemu/QEMU tag v11.1.2 inside Docker, talk to loopback, and print a witness. No shells.
The three posts:
Why this product
QEMU is the userspace half of almost every KVM box I actually see. libvirt talks to it. cloud-hypervisor and Firecracker are other trees. This tree is the one that still emulates xHCI, NVMe, VGA, virtio, NBD, and a COLO proxy in one process.
I wanted unpublished Critical or High on the current GA, not a replay of the 2026 CVE wave. Guest-to-host on the documented virtualization machines. Device DMA that re-enters MMIO. Network parsers that the security chapter already called untrusted. A leftover of a public fix is more interesting than a brand-new assert.
The security chapter is honest about the boundary. Guest isolation means the guest must not gain control of QEMU. An emulated device that DMA-writes its own doorbell is exactly that sentence, written as C.
How the pin was chosen
Latest GA. Tag v11.1.2. VERSION is 11.1.2. Peel is 4fc49f46dc95d4a27de2509e7fceb2931e91faeb, "Update version for 11.1.2 release". GitHub qemu/QEMU is the clone; GitLab qemu-project/qemu is the same tree.
Debian sid had already packaged it as 1:11.1.2+ds-3. That is a useful check: the pin is what operators pull, not a random master SHA. I did not chase a later rc. Master moves. A leftover on a GA tag is a leftover someone can reproduce.
Labs rebuild x86_64-softmmu from that peel inside Docker, with ASan on the two paths that needed a heap oracle. qtest and a COLO socket netdev stay on 127.0.0.1. Replay is ./run.sh in each pack.
The skip list is half the hunt
Before you look for leftovers, you write down what is already public so you do not "discover" a CVE with a press release. On this pin I did not re-file:
- CVE-2026-17588 xHCI timer reentrancy.
xhci_mfwrap_timerandxhci_ep_kick_timernow setxhci->parent.mem_reentrancy_guard. That is the named fence. The complete callback is the leftover. - CVE-2026-77913 VGA text-mode OOB.
vga_draw_textcompares against the live surface. Graphic and blank still trustlast_*. That sibling did not prove in the lab. - CVE-2026-81627 VAPIC D_LCK / SMM inject.
- CVE-2026-93834 9pfs FID path race UAF.
- CVE-2026-66020 virtio-gpu blob scanouts.
- CVE-2021-3929 NVMe start-in-BAR0.
nvme_addr_is_iomemstill tests the start. A spanning SGL is the leftover. - CVE-2021-3713 UAS-object UAF. Status is a guarded BH on the UAS
DeviceState. The leftover is the xHCI object via event-ring DMA, not a 2021 n-day.
Also closed on this pin, so they are not this series: virtio packed vring / queue-size, slirp glue (libslirp is out of tree), QMP as a privileged control plane, TCG-only machines, EHCI async complete already deferred onto a guarded BH, NVMe admin SGL reject, PRP that cannot span a page-aligned BAR, CMB/PMR mappings that already check both ends, TAP/socket length paths, default QEMU without COLO.
Guest-root assert and unbounded host alloc are hardening. vhost-user backends are not a security boundary versus QEMU. I left them on the floor.
Surfaces that returned nothing useful
Eight leftovers looked like High on the source walk. Three labbed. Five did not prove. Those five stay unpublished as findings.
#2 VGA graphic leftover of CVE-2026-77913. vga_draw_graphic / vga_draw_blank still skip resize when VGA last_* matches and a smaller virtio-gpu scanout sits on the shared console. The lab saw the skip: 640x480 then a 16x16 scanout, skipped-resize=1. ASan stayed quiet. No crash. VNC pixels were 0x0. Not proven OOB.
#4 NBD reply.cookie. nbd_co_receive_one_chunk clears the cookie off receive_mutex. A rejected cookie can stay in place. Source said COOKIE_TO_INDEX(0) wraps to requests[-1] and default asserts abort the hypervisor. The lab did not abort. Control and aio qemu-io exited 0. The harness and qemu-system timed out. Attack-no-abort.
#5 virtio-scsi TMF after emulate. Historic SCSI DMA UAFs were fenced by mem_reentrancy_guard only when the outer access is already MMIO. Default KVM virtio-scsi completion is ioeventfd. The idea was an INQUIRY SGL aimed at the notify BAR, then a nested TMF LUN reset freeing VirtIOSCSIReq.qsgl mid-dma_buf_rw. The lab ran TMF after emulate completed. No nested cancel. The SGL notify plus three later entries survived. No UAF.
#6 postcopy RAM discard. Incoming MIG_CMD_POSTCOPY_RAM_DISCARD calls bitmap_clear on receivedmap before ram_block_discard_range checks max_length. That is a dest heap overwrite with zeros, on paper. The lab host has no userfaultfd (ENOSYS). Dest never reached bitmap_clear.
#7 virtio-gpu scanout restore. virtio_gpu_post_load trusts stream fb.offset. Guest-only SET_SCANOUT is bounded on this pin. The restore lab never got a QMP greeting. Connection refused.
Other walks that did not become leftovers: qemu-ga listen width after the in-tree SSH-key fix, SCSI HBA doorbell siblings of already-public GitLab work, virtio-gpu 2d/blob after the 2026 named CVEs, e1000/igb migrated rx_desc_len, ACPI ERST. Those are closed, out of tree, or already filed.
Three leftovers that labbed
| Leftover | Class | Reach | Pack | Blog |
|---|---|---|---|---|
1 xhci_complete |
UAF | Local (guest) | qemu-xhci-complete-uaf | /research/qemu-xhci-complete-uaf |
| 3 NVMe SGL BAR | UAF | Local (guest) | qemu-nvme-sgl-bar | /research/qemu-nvme-sgl-bar |
| 8 COLO L4 | OOB | Local (guest) / Remote (COLO peer) | qemu-colo-l4-oob | /research/qemu-colo-l4-oob |
All three: 8.2 High CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H. Guest kernel (or a COLO peer) into the QEMU process. Changed scope. Not a pasteable host shell. The labs are witnesses.
Leftover 1: xhci_complete leftover of CVE-2026-17588
q35 ships xHCI. Any bulk device that completes USB_RET_ASYNC later is a partner: usb-uas status BH, usb-storage SCSI AIO, usb-redir chardev, usb-host libusb. HID NAK retry is the kick timer, and that timer is now guarded.
CVE-2026-17588 is the event ring aimed at the doorbell BAR. xhci_write_event DMA-writes a 16-byte TRB. First dword 0 hits doorbell 0 and runs xhci_process_commands. A prepared CR_DISABLE_SLOT frees the live XHCITransfer while the outer frame still holds it. The 11.1.2 fix engages the guard only in the two timer callbacks.
xhci_complete is the other non-MMIO writer of that event:
static void xhci_complete(USBPort *port, USBPacket *packet)
{
XHCITransfer *xfer = container_of(packet, XHCITransfer, packet);
if (packet->status == USB_RET_REMOVE_FROM_QUEUE) {
xhci_ep_nuke_one_xfer(xfer, 0);
return;
}
xhci_try_complete_packet(xfer);
xhci_kick_epctx(xfer->epctx, xfer->streamid);
if (xfer->complete) {
xhci_ep_free_xfer(xfer);
}
}EHCI already refused to write back from the USB complete callback. It marks FINISHED and schedules a guarded BH. xHCI writes the event ring here, with the TYPE_XHCI guard clear, because the original doorbell MMIO already returned.
The lab is qtest on 127.0.0.1:18700. Guest-side: enable slot, address, configure endpoints, plant ERST at the doorbell (0xe0002000), queue CR_DISABLE_SLOT without ringing doorbell 0, start an async usb-uas bulk, wait. ASan reported heap-use-after-free. disable-slot-fetch=1. The process is the QEMU heap, not a guest kernel oops.
Not RCE in the lab. Heap UAF of the QEMU process. That is still guest-to-host on the security chapter's terms.
Pack: abraxas/qemu-xhci-complete-uaf (lab/). CWE-416. Sink hw/usb/hcd-xhci.c:3282-3295.
SUCCESS qemu-xhci-complete-uaf how=ERROR: AddressSanitizer: heap-use-after-free erst=doorbell async=1 success=1 disable-slot-fetch=1 QEMU-XHCI-COMPLETE-UAF-WITNESSWhat an attacker can do
| Method | Sink | Privilege |
|---|---|---|
| Guest aims ERST at the xHCI doorbell BAR, then an async usb-uas complete | xhci_complete -> xhci_write_event DMA into doorbell 0 -> xhci_process_commands -> CR_DISABLE_SLOT |
Integrity (lab-proven): heap-use-after-free of live XHCITransfer / XHCIEPContext in the QEMU process. ASan heap-use-after-free. |
| Same complete path after the slot is nuked | xhci_kick_epctx(xfer->epctx) / second xhci_ep_free_xfer |
Availability (lab-proven): QEMU abort once ASan or the allocator notices. |
| Enumerate qemu-xhci on q35 | PCI vendor 0x1b36 device 0x000d |
Recon (lab-proven): the device is there. No extra leak required. |
Inferred, not in the lab: controlled reuse of the freed XHCITransfer as a write gadget, then host code execution. I did not groom the heap. I did not claim RCE.
Closed: timer callbacks (CVE-2026-17588). EHCI async complete BH. UAS-object UAF (CVE-2021-3713). OHCI inf-loop. UHCI guarded BH. Guest-root UAS tag asserts. HID kick-timer NAK retry.
Suggested fix: set the TYPE_XHCI mem_reentrancy_guard in xhci_complete, or stop writing the event ring from the USB complete callback and defer onto a guarded BH the way EHCI already does.
Leftover 3: NVMe SGL still spans into BAR0
CVE-2021-3929 taught NVMe not to DMA into its own BAR. nvme_map_addr still only asks whether the start of a mapping sits in BAR0.
static inline bool nvme_addr_is_iomem(NvmeCtrl *n, hwaddr addr)
{
hwaddr hi, lo;
lo = n->bar0.addr;
hi = lo + int128_get64(n->bar0.size);
return addr >= lo && addr < hi;
} if (nvme_addr_is_iomem(n, addr)) {
return NVME_DATA_TRAS_ERROR;
}PRP entries are page-bounded, so they cannot sneak into a page-aligned BAR from the previous page. SGL data blocks are not. Identify reports NVME_CTRL_SGLS_SUPPORT_NO_ALIGN. CMB and PMR mappings already check both ends. BAR0 does not.
An NVMe READ with a spanning SGL goes through nvme_blk_read -> dma_blk_read. Bounce DMA maps MMIO, then address_space_unmap writes the bounce into BAR0. That write is a DMA helper callback, not the NVMe mem_reentrancy_guard BH. Nested MMIO is allowed. Guest disk bytes can land on CC and call nvme_ctrl_reset while NvmeRequest is still live in the completion path.
The lab is qtest on 127.0.0.1:18720. BAR0 at 0xe0000000, size 0x4000. Control Identify with start-in-BAR0 still returns 0x4004 (NVME_DATA_TRAS_ERROR | DNR). That is CVE-2021-3929 still doing its job. Span SGL 0xdfffffec-0xe0000fec starts 20 bytes before BAR0 so CC sits in the first mapped page. Bounce writes zeros onto CC. CSTS.RDY drops. mmio-stopped=1. ASan stayed 0. qemu died.
So the lab-proven effect is controller reset from a spanning SGL, not an ASan UAF stack. The UAF of NvmeRequest is the source class. The witness is the RDY bit and a dead MMIO socket.
Pack: abraxas/qemu-nvme-sgl-bar (lab/). CWE-119 / CWE-416. Sink nvme_map_addr / nvme_addr_is_iomem.
SUCCESS qemu-nvme-sgl-bar control-sc=0x4004 span-addr=0xdfffffec span-end=0xe0000fec csts-rdy-before=1 csts-rdy-after=0 mmio-stopped=1 asan=0 QEMU-NVME-SGL-BAR-WITNESSWhat an attacker can do
| Method | Sink | Privilege |
|---|---|---|
NVMe READ SGL 0xdfffffec-0xe0000fec (start just before BAR0, length 4096) |
nvme_map_addr start-only iomem check, bounce DMA address_space_write into BAR0 |
Integrity (lab-proven): zeros land on CC. CSTS.RDY 1 then 0. |
Same write of CC |
nvme_write_bar -> nvme_ctrl_reset |
Availability (lab-proven): controller reset, MMIO stopped, qemu died. |
| Identify / map with start inside BAR0 | same nvme_addr_is_iomem |
Closed (lab-proven): status 0x4004. That is CVE-2021-3929. |
Inferred, not in the lab: UAF of the live NvmeRequest / SQ / CQ during reset-from-DMA, then controlled reuse. ASan did not fire on this run. Guest-controlled non-zero disk bytes onto other BAR0 registers, not only CC.
Closed: start-in-BAR0. Admin Identify/Get Log SGL reject. PRP cannot span. CMB/PMR both-ends. CQ/SQ-in-BAR0 CQE posts (nvme_post_cqes BH is guarded). Cross-namespace Copy reset UAF / DIF bounce overflow already fixed on this pin.
Suggested fix: test the span, not the start. addr and addr + len - 1 both have to miss BAR0, the way CMB/PMR already do.
Leftover 8: COLO parse_packet_early still has no L4 floor
COLO is not default. You opt in with -object filter-rewriter (and usually colo-compare on a chardev pair). When that object is on, guest TX/RX and the COLO peer become an untrusted network protocol in the same process.
parse_packet_early accepts IPv4 once L2 plus ip_hl*4 fits, then sets transport_header at that offset. It never requires a minimum L4 header. The size floor is Ethernet plus VLAN plus vnet_hdr_len. fill_connection_key then reads 4 bytes (8 for AH) at transport_header with no remaining-length check.
filter-rewriter copies the frame with g_malloc0(size) and, for TCP, always loads th_flags. After SYN has moved the connection to TCPS_SYN_RECEIVED, an ACK with conn->offset != 0 writes th_ack / th_seq past the chunk when the captured length ends at or before the transport header. IP options (ip_hl > 5) shift that write onto the next heap chunk.
colo-compare is the sibling: fill_pkt_tcp_info takes header_size from th_off<<2 with no check against pkt->size. Underflow, then memcmp past both packet allocations.
The lab is a COLO-gated qemu-system on loopback. Netdev 127.0.0.1:18770, colo-compare primary 127.0.0.1:18771. Control: full TCP SYN then ACK, 54-byte frames, qemu still running. Overflow: SYN 54, then a 34-byte IPv4 ACK that ends at the transport header. ASan: heap-buffer-overflow, access READ, sink filter-rewriter, fill_connection_key. Control stayed alive. That is the proven leftover.
The WRITE of th_ack / th_seq past g_malloc0, and the colo-compare th_off underflow memcmp, are inferred from the same missing L4 floor. The lab oracle was the READ.
Pack: abraxas/qemu-colo-l4-oob (lab/). CWE-125 / CWE-787. Sinks net/colo.c parse_packet_early, net/filter-rewriter.c.
SUCCESS qemu-colo-l4-oob asan=heap-buffer-overflow access=READ sink=filter-rewriter control=alive QEMU-COLO-L4-OOB-WITNESSWhat an attacker can do
| Method | Sink | Privilege |
|---|---|---|
| 34-byte IPv4/TCP frame on a COLO rewriter netdev (guest TX/RX or peer) | parse_packet_early then fill_connection_key |
Recon + availability (lab-proven): ASan heap-buffer-overflow READ of size 4 past the g_malloc0 chunk. QEMU can abort. |
Same short frame after SYN moved the connection, conn->offset != 0 |
handle_primary_tcp_pkt / handle_secondary_tcp_pkt stores to th_ack / th_seq |
Integrity (inferred): heap WRITE past the rewriter allocation. Not the ASan hit in this run. |
Hostile colo-compare peer, th_off larger than the captured tail |
fill_pkt_tcp_info then colo_compare_packet_payload memcmp |
Recon + availability (inferred): over-read past both packet allocations. |
Closed: TAP/socket/bridge/l2tpv3/stream/dgram length paths on this pin. Default QEMU without -object filter-rewriter. VLAN frames (COLO proxy refuses them). Kernel TAP bugs are a different product.
Suggested fix: require a real L4 minimum in parse_packet_early before setting transport_header. TCP needs 20 bytes. fill_connection_key and the rewriter stores need a remaining-length check. colo-compare must cap th_off against pkt->size.
How they sit next to each other
They are independent. There is no chain that turns leftover 1 into leftover 3, or leftover 3 into leftover 8. Three sinks, three objects, three heaps.
xHCI is default-ish. q35 brings up qemu-xhci. A guest kernel that can program the controller is the documented untrusted guest. usb-uas is a common bulk gadget. You do not need COLO. You do not need NVMe.
NVMe is a common -device nvme (and a common libvirt disk). It is not on every q35 XML, but it is not an experimental flag either. The spanning SGL is guest DMA. COLO is irrelevant.
COLO is config-gated. -object filter-rewriter is an operator choice for checkpoint / failover. Default qemu-system does not parse guest frames through parse_packet_early. The leftover is still High on the machines that turned COLO on, including a remote COLO peer. It is not a default-cloud finding.
PR:H on all three is the guest kernel (or the COLO peer). An unprivileged process inside the guest that only talks to /dev/sd* or a userspace USB gadget cannot aim ERST or an NVMe SGL at a BAR. That is the CVSS. The security chapter still calls the guest untrusted.
What stays closed next to them: QMP, TCG-only, slirp-out-of-tree, virtio packed vring, the xHCI timers, NVMe start-in-BAR0, admin SGL, PRP, CMB/PMR both-ends, TAP length, default QEMU without COLO. I am not inflating a heap UAF or a 34-byte READ into a lab RCE.
The fix, in one place
Three small fences, one per leftover.
- xHCI. Guard
xhci_completethe way the timers are guarded, or stop DMA-writing the event ring from the USB complete callback. EHCI already deferred. Copy that. - NVMe.
nvme_addr_is_iomemhas to see the span. Start-only was the 2021 fix. SGL is not PRP. - COLO.
parse_packet_earlyhas to require an L4 header. A 34-byte IPv4 frame is not a TCP segment.
Until then, q35 plus usb-uas is leftover 1, NVMe plus guest SGL is leftover 3, and -object filter-rewriter is leftover 8. They do not wait on each other.
References
- Pin: QEMU v11.1.2 /
4fc49f46dc95d4a27de2509e7fceb2931e91faeb. Debian sid1:11.1.2+ds-3. - Policy:
docs/system/security.rst - Sinks:
hw/usb/hcd-xhci.c,hw/nvme/ctrl.c,net/colo.c,net/filter-rewriter.c - Named fences: CVE-2026-17588, CVE-2021-3929, CVE-2021-3713
- Packs: qemu-xhci-complete-uaf, qemu-nvme-sgl-bar, qemu-colo-l4-oob
- Posts: /research/qemu-xhci-complete-uaf, /research/qemu-nvme-sgl-bar, /research/qemu-colo-l4-oob
- Vendor: GitLab confidential issues on qemu-project/qemu
Contact
Abraxas Labs. github.com/abraxas. @abraxas_null. abraxaslabs.tech. abraxas.null@proton.me.