Mar 17, 2026 From virtio-snd 0-Day to Hypervisor Escape: Exploiting QEMU with an Uncontrolled Heap Overflow Turning an uncontrolled heap overflow into a reliable QEMU guest-to-host escape using new glibc allocator behavior and QEMU-specific heap spray techniques. Hrvoje Mišetić 3/17/2026 Copy Link Heap overflows are often exploitable, but far less so when the corrupted bytes are not under your control. In many cases, that kind of bug is written off as a crash and nothing more. However, in this post we show how we turned such an overflow into a reliable QEMU guest-to-host escape by abusing new glibc allocator behavior and QEMU-specific heap spray techniques. QEMU QEMU is a machine emulator and virtualizer that lets a host system run guest operating systems. It presents the guest with virtual hardware, while the logic backing that hardware runs inside the host-side QEMU process. Virtio Devices For guest-to-host escape research, the interesting part of QEMU is the interface between the guest and those host-side device implementations. Every request sent by the guest is eventually parsed and handled by code running in the QEMU process. This is interesting because any unhandled edge case in the device could lead to some kind of host state corruption. At a high level, the communication between the driver running in the guest and the device running on the host is simple - the guest-side virtio driver shares requests over virtqueues, while the host-side virtio device consumes those requests, processes and returns responses. Finding a Bug While looking for devices to research, we focused on ones that seemed to have received less scrutiny in the past. With that in mind, we started with the sound device virtio-snd . virtio-snd From the official documentation: Virtio sound implements capture and playback from inside a guest using the configured audio backend of the host machine. Essentially, it allows software running inside the guest to interact with the host's audio stack through a paravirtualized sound device. Playback streams send guest-provided audio data to the host backend, while capture streams let the guest receive audio input from the host. Audio Data Buffers This audio data flows through buffers allocated by the host-side virtio-snd device and stored in a FIFO linked list for the corresponding stream. For example, the following is virtio_snd_handle_rx_xfer , which is responsible for allocating buffers for an input audio stream: /* * The rx virtqueue handler. Makes the buffers available to their * respective streams for consumption. * * @vdev: VirtIOSound device * @vq: rx virtqueue */ static void virtio_snd_handle_rx_xfer (VirtIODevice * vdev , VirtQueue * vq ) { VirtQueueElement * elem; [...] for (;;) { VirtIOSoundPCMStream * stream; elem = virtqueue_pop (vq, sizeof (VirtQueueElement)); // [1] if ( ! elem) { break ; } [...] WITH_QEMU_LOCK_GUARD ( & stream -> queue_mutex ) { size = iov_size ( elem -> in_sg , elem -> in_num ) - sizeof (virtio_snd_pcm_status); // [2] buffer = g_malloc0 ( sizeof (VirtIOSoundPCMBuffer) + size); buffer -> elem = elem; buffer -> vq = vq; buffer -> size = 0 ; buffer -> offset = 0 ; QSIMPLEQ_INSERT_TAIL ( & stream -> queue , buffer, entry); // [3] } continue ; [...] } At [1] , a VirtQueueElement *elem is popped from the virtqueue. It contains the in_sg and out_sg iovecs that describe the guest request, and is therefore fully guest-controlled. Further at [2] , the device computes the size of the data buffer as iov_size(elem->in_sg, elem->in_num) - sizeof(virtio_snd_pcm_status) . That value is then used in the allocation: g_malloc0(sizeof(VirtIOSoundPCMBuffer) + size) . Finally, at [3] , the newly allocated buffer is appended to the stream->queue linked list. Because both the in_sg iovec and the in_num field are guest-controlled, and there is no check that the total in_sg size is at least sizeof(virtio_snd_pcm_status) , this calculation can underflow if the guest provides a smaller input buffer - that gives us our first bug. From the guest driver, we can provide an empty in_sg iovec. In that case, the calculation becomes 0 - sizeof(virtio_snd_pcm_status) , so the allocation size effectively becomes sizeof(VirtIOSoundPCMBuffer) - 8 . Given the definition of VirtIOSoundPCMBuffer : struct VirtIOSoundPCMBuffer { QSIMPLEQ_ENTRY (VirtIOSoundPCMBuffer) entry; VirtQueueElement * elem; VirtQueue * vq; size_t size; uint64_t offset; /* Used for the TX queue for lazy I/O copy from `elem` */ bool populated; uint8_t data [] ; }; That under-allocation removes the populated field along with the variable-sized data array. As the comment says, populated is only relevant to the TX path and is not used for audio input. However, by making the iovec size 1 , the device believes data should be 1 byte, while the actual allocation is sizeof(VirtIOSoundPCMBuffer) - 7 . Populating Data Buffers Let's take a look at how the allocated data buffer for the input stream is filled: /* * AUD_* input callback. * * @data:...
This article details a guest-to-host hypervisor escape exploit in QEMU, leveraging an uncontrolled heap overflow in the virtio-snd device. The attack is made reliable by exploiting new glibc allocator behavior combined with QEMU-specific heap spray techniques to turn a memory corruption bug into a full escape from the guest VM to the host.