Heap out-of-bound read in QEMU host through a batched SCSI WRITE SAME and have it written back to its own disk image
An untrusted guest driving a virtio-scsi HBA with an attached scsi-hd can force QEMU's SCSI disk emulation to read roughly 60 KiB past the end of a heap buffer, and, on the non zero data path, copy that out of bounds host memory into the guest's own disk image where the guest can read it straight back. The primitive is built from two ordinary SCSI commands placed in a single virtqueue notify: a MODE SELECT(10) that raises the emulated logical block size to 65024 bytes, batched ahead of a WRITE SAME(10) whose data buffer was already frozen at 4096 bytes. Because virtio_scsi_handle_cmd_vq() prepares every request in the batch before submitting any of them, the WRITE SAME buffer is sized from the old 512 byte block size while the later buffer_is_zero() and the memcpy() fallback both use the new 65024 byte block size, so QEMU reads and copies blocksize bytes out of a 4096 byte allocation. The only precondition is the ability to issue SCSI commands through virtio-scsi, which any guest with the device attached has; the block size change and the WRITE SAME must ride in the same virtqueue notify. The impact is a guest to host out of bounds read that both leaks host heap into the guest (memory disclosure) and, when ASan or a redzone is not present, can crash QEMU.
Root cause
WRITE SAME freezes the request transfer length at the current block size when the request is prepared. In scsi_req_parse_cdb() the WRITE_SAME_10/WRITE_SAME_16 case sets cmd->xfer from dev->blocksize at prepare time (512 by default), independently of whatever the block size becomes later.
case WRITE_SAME_10:
case WRITE_SAME_16:
cmd->xfer = buf[1] & 1 ? 0 : dev->blocksize;
break;scsi_disk_emulate_command() sizes the request buffer from that frozen cmd.xfer. The req->cmd.xfer > 65536 guard bounds only the frozen transfer length; with 512 byte blocks cmd.xfer is 512 so r->buflen is MAX(4096, 512) = 4096, and blk_blockalign() allocates exactly 4096 bytes. Nothing here is tied to the block size that WRITE SAME will read later.
if (req->cmd.xfer > 65536) {
goto illegal_request;
}
r->buflen = MAX(4096, req->cmd.xfer);
if (!r->iov.iov_base) {
r->iov.iov_base = blk_blockalign(s->qdev.conf.blk, r->buflen);
}
outbuf = r->iov.iov_base;
memset(outbuf, 0, r->buflen);MODE SELECT lets the guest raise the logical block size to any value whose low bits fit 0xfe00, up to 65024, with no relationship to the 4096 byte buffer above.
if (bs && !(bs & ~0xfe00) && bs != s->qdev.blocksize) {
s->qdev.blocksize = bs;
trace_scsi_disk_mode_select_set_blocksize(s->qdev.blocksize);
}scsi_disk_emulate_write_same() then reads and copies s->qdev.blocksize bytes out of inbuf, which is the 4096 byte r->iov.iov_base. buffer_is_zero(inbuf, s->qdev.blocksize) reads inbuf[blocksize - 1], i.e. inbuf[65023], about 60 KiB past the allocation. When the buffer is not all zero the fallback loop copies up to blocksize bytes per iteration from inbuf into a freshly allocated disk write buffer, so the same out of bounds host bytes are written to the image and become readable by the guest.
if ((req->cmd.buf[1] & 0x1) || buffer_is_zero(inbuf, s->qdev.blocksize)) {
......
}
data = g_new0(WriteSameCBData, 1);
......
for (i = 0; i < data->iov.iov_len; i += l) {
l = MIN(s->qdev.blocksize, data->iov.iov_len - i);
memcpy(&buf[i], inbuf, l);
}inbuf is r->iov.iov_base, the 4096 byte buffer, passed straight in from the WRITE SAME data-out handler.
case WRITE_SAME_10:
case WRITE_SAME_16:
scsi_disk_emulate_write_same(r, r->iov.iov_base);
break;The actual out of bounds access is the byte load in buffer_is_zero_sample3(), which the compiler inlines into WRITE SAME's zero check for the constant sized fast path.
static inline bool buffer_is_zero_sample3(const char *buf, size_t len)
{
......
return !buf[0] && !buf[len - 1] && !buf[len / 2];
}The batching is what keeps the WRITE SAME buffer at 4096 bytes while the block size is already 65024. virtio_scsi_handle_cmd_vq() pops and prepares every request in the notify in the first loop (each WRITE SAME's cmd.xfer frozen at 512), then submits them in the following QTAILQ_FOREACH_SAFE. A MODE SELECT queued ahead of the WRITE SAME in the same notify is submitted first and has already set blocksize = 65024 by the time the WRITE SAME data-out phase runs.
while ((req = virtio_scsi_pop_req(s, vq, cdb_size, NULL))) {
ret = virtio_scsi_handle_cmd_req_prepare(s, req, cdb_size);
if (!ret) {
QTAILQ_INSERT_TAIL(&reqs, req, next);
}
......
}
......
QTAILQ_FOREACH_SAFE(req, &reqs, next, next) {
virtio_scsi_handle_cmd_req_submit(s, req);
}Proof of Concept
The PoC builds a real, unmodified qemu-system-x86_64 from source at the pin with AddressSanitizer and drives the real virtio-scsi, scsi-disk and block emulation through the qtest line protocol. A single run asserts the checked-out tree is the pinned commit, then issues the two SCSI commands in one virtqueue notify: a MODE SELECT(10) that raises the block size to 65024 batched ahead of a WRITE SAME(10) whose data buffer was frozen at 4096 bytes. qtest is only the transport that lets the harness act as the guest driver: it programs the virtio-scsi-pci BAR, builds a virtqueue in guest RAM and kicks it, so every byte the device sees is a byte a real guest driver could write. What is executed is the genuine out of bounds read: ASan traps the load in buffer_is_zero_sample3() reached from scsi_disk_emulate_write_same(). What is cited rather than executed is the subsequent memcpy() to the disk write buffer that turns the overread into a host heap disclosure readable by the guest; because ASan aborts at the read on line 1933 the write-back path on lines 1958 to 1960 is not reached under this build, and is shown from source above.
docker build -t poc . && docker run --rm pocResult
PIN OK: HEAD == 2be159078ea26feac4c9c9902acf8906f1a05c2a
==10==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7d675b477dff at pc 0x5635fc95aa9b bp 0x7ffc52620b00 sp 0x7ffc52620af8
READ of size 1 at 0x7d675b477dff thread T0
#0 0x5635fc95aa9a in buffer_is_zero_sample3 /src/repo/include/qemu/cutils.h:193:24
#1 0x5635fc95aa9a in buffer_is_zero_ool /src/repo/build/../util/bufferiszero.c:94:10
#2 0x5635fbfedfed in buffer_is_zero /src/repo/include/qemu/cutils.h:202:15
#3 0x5635fbfedfed in scsi_disk_emulate_write_same /src/repo/build/../hw/scsi/scsi-disk.c:1933:36
#4 0x5635fbfedfed in scsi_disk_emulate_write_data /src/repo/build/../hw/scsi/scsi-disk.c:2005:9
#5 0x5635fbfec7da in scsi_disk_emulate_write_data /src/repo/build/../hw/scsi/scsi-disk.c:1980:9
#6 0x5635fc051b15 in virtio_scsi_handle_cmd_req_submit /src/repo/build/../hw/scsi/virtio-scsi.c:902:9
#7 0x5635fc051b15 in virtio_scsi_handle_cmd_vq /src/repo/build/../hw/scsi/virtio-scsi.c:949:9
#8 0x5635fc051b15 in virtio_scsi_handle_cmd /src/repo/build/../hw/scsi/virtio-scsi.c:962:5
#9 0x5635fc1c1d10 in virtio_queue_notify_vq /src/repo/build/../hw/virtio/virtio.c:2516:9
#10 0x5635fc1c1d10 in virtio_queue_host_notifier_read /src/repo/build/../hw/virtio/virtio.c:4180:9
#11 0x5635fc90ef46 in aio_dispatch_handler /src/repo/build/../util/aio-posix.c:344:9
#12 0x5635fc90ef46 in aio_dispatch_ready_handlers /src/repo/build/../util/aio-posix.c:370:20
#13 0x5635fc90e85e in aio_dispatch /src/repo/build/../util/aio-posix.c:399:5
#14 0x5635fc945f6d in aio_ctx_dispatch /src/repo/build/../util/async.c:365:5
#15 0x7f175c533584 (/lib/x86_64-linux-gnu/libglib-2.0.so.0+0x5d584) (BuildId: 116e142b9b52c8a4dfd403e759e71ab8f95d8bb3)
#16 0x7f175c5336cf in g_main_context_dispatch (/lib/x86_64-linux-gnu/libglib-2.0.so.0+0x5d6cf) (BuildId: 116e142b9b52c8a4dfd403e759e71ab8f95d8bb3)
#17 0x5635fc9470d8 in glib_pollfds_poll /src/repo/build/../util/main-loop.c:292:9
#18 0x5635fc9470d8 in os_host_main_loop_wait /src/repo/build/../util/main-loop.c:315:5
#19 0x5635fc9470d8 in main_loop_wait /src/repo/build/../util/main-loop.c:594:11
#20 0x5635fc2a24c1 in qemu_main_loop /src/repo/build/../system/runstate.c:1104:9
#21 0x5635fc7b8700 in qemu_default_main /src/repo/build/../system/main.c:50:14
#22 0x5635fc7b86d1 in main /src/repo/build/../system/main.c:93:9
#23 0x7f175c14a1c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#24 0x7f175c14a28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#25 0x5635fb8c8b24 in _start (/src/repo/build/qemu-system-x86_64+0x991b24) (BuildId: abe80b4960a5f841ce9f9f03a929e80976f47403)
Address 0x7d675b477dff is a wild pointer inside of access range of size 0x000000000001.
SUMMARY: AddressSanitizer: heap-buffer-overflow /src/repo/include/qemu/cutils.h:193:24 in buffer_is_zero_sample3
==10==ABORTINGThe run batches a MODE SELECT(10) that sets the block size to 65024 ahead of a WRITE SAME(10) in the same notify, and AddressSanitizer reports a heap-buffer-overflow READ of size 1 in buffer_is_zero_sample3 (include/qemu/cutils.h) with the calling frames scsi_disk_emulate_write_same and virtio_scsi_handle_cmd_vq, confirming the roughly 60 KiB overread out of the 4096 byte request buffer. The only precondition is that the block size change and the WRITE SAME travel in the same virtqueue notify so that the WRITE SAME buffer stays sized from the old 512 byte block size.
Mitigation
The read length in WRITE SAME must be bounded by the buffer that was actually allocated, not by the mutable s->qdev.blocksize. At hw/scsi/scsi-disk.c:1933 and in the memcpy() fallback at lines 1958 to 1960, clamp the length used against inbuf to r->buflen (the size passed to blk_blockalign()), for example by testing buffer_is_zero(inbuf, MIN(s->qdev.blocksize, r->buflen)) and copying at most r->buflen bytes out of inbuf per iteration. More robustly, re-validate req->cmd.xfer against the current s->qdev.blocksize when the request reaches its data-out phase, or size the WRITE SAME buffer from the live block size so that a MODE SELECT batched ahead of the WRITE SAME cannot leave the buffer undersized. Rejecting a WRITE SAME whose frozen cmd.xfer no longer matches the current block size would also close the window.
Attribution
This vulnerability was discovered by Claude, Anthropic's AI assistant, and triaged manually with manual report writing by Ada Logics in collaboration with Anthropic Research.