@samitouri / QOSamiQemu / commits / 6d2cf4466f

hw/display/qxl: fix TOCTOU in cursor chunk data_size handling

Snapshot chunk.data_size into a host-local variable before passing it to qxl_phys2virt() for validation, and pass it through qxl_cursor() and qxl_unpack_chunks() so that no subsequent code re-reads the field. Without this, a racing vCPU can inflate data_size between the qxl_phys2virt() validation and the memcpy in qxl_unpack_chunks(), causing a source read past the validated region. In practice the read stays within the guest's own VRAM mmap, so the impact is limited. Resolves: https://gitlab.com/qemu-project/qemu/-/work_items/3757 Reviewed-by: Philippe Mathieu-Daudé <philmd@oss.qualcomm.com> Reported-by: Feifan Qian <bea1e@proton.me> Signed-off-by: Marc-Andre Lureau <marcandre.lureau@redhat.com>

Marc-André Lureau committed Jul 9, 2026 at 15:15 UTC 6d2cf4466f1b118350b77838217b79e2c2f0833c
1 file changed +17 -11
hw/display/qxl-render.c
+17 -11
@@ -217,7 +217,8 @@ void qxl_render_update_area_done(PCIQXLDevice *qxl, QXLCookie *cookie)
217 }
218
219 static void qxl_unpack_chunks(void *dest, size_t size, PCIQXLDevice *qxl,
220 - QXLDataChunk *chunk, uint32_t group_id)
220 + QXLDataChunk *chunk, uint32_t group_id,
221 + uint32_t chunk_data_size)
222 {
223 uint32_t max_chunks = 32;
224 size_t offset = 0;
@@ -225,22 +226,21 @@ static void qxl_unpack_chunks(void *dest, size_t size, PCIQXLDevice *qxl,
226 QXLPHYSICAL next_chunk_phys = 0;
227
228 for (;;) {
228 - bytes = MIN(size - offset, chunk->data_size);
229 + bytes = MIN(size - offset, chunk_data_size);
230 memcpy(dest + offset, chunk->data, bytes);
231 offset += bytes;
232 if (offset == size) {
233 return;
234 }
235 next_chunk_phys = chunk->next_chunk;
235 - /* fist time, only get the next chunk's data size */
236 chunk = qxl_phys2virt(qxl, next_chunk_phys, group_id,
237 sizeof(QXLDataChunk));
238 if (!chunk) {
239 return;
240 }
241 - /* second time, check data size and get data */
241 + chunk_data_size = chunk->data_size;
242 chunk = qxl_phys2virt(qxl, next_chunk_phys, group_id,
243 - sizeof(QXLDataChunk) + chunk->data_size);
243 + sizeof(QXLDataChunk) + chunk_data_size);
244 if (!chunk) {
245 return;
246 }
@@ -252,7 +252,7 @@ static void qxl_unpack_chunks(void *dest, size_t size, PCIQXLDevice *qxl,
252 }
253
254 static QEMUCursor *qxl_cursor(PCIQXLDevice *qxl, QXLCursor *cursor,
255 - uint32_t group_id)
255 + uint32_t group_id, uint32_t chunk_data_size)
256 {
257 QEMUCursor *c;
258 uint8_t *and_mask, *xor_mask;
@@ -272,11 +272,11 @@ static QEMUCursor *qxl_cursor(PCIQXLDevice *qxl, QXLCursor *cursor,
272 case SPICE_CURSOR_TYPE_MONO:
273 /* Assume that the full cursor is available in a single chunk. */
274 size = 2 * cursor_get_mono_bpl(c) * c->height;
275 - if (size != cursor->data_size || cursor->chunk.data_size < size) {
275 + if (size != cursor->data_size || chunk_data_size < size) {
276 qxl_set_guest_bug(qxl, "%s: bad monochrome cursor %ux%u"
277 " data_size %u chunk_size %u",
278 __func__, c->width, c->height,
279 - cursor->data_size, cursor->chunk.data_size);
279 + cursor->data_size, chunk_data_size);
280 goto fail;
281 }
282 and_mask = cursor->chunk.data;
@@ -288,7 +288,8 @@ static QEMUCursor *qxl_cursor(PCIQXLDevice *qxl, QXLCursor *cursor,
288 break;
289 case SPICE_CURSOR_TYPE_ALPHA:
290 size = sizeof(uint32_t) * c->width * c->height;
291 - qxl_unpack_chunks(c->data, size, qxl, &cursor->chunk, group_id);
291 + qxl_unpack_chunks(c->data, size, qxl, &cursor->chunk, group_id,
292 + chunk_data_size);
293 if (qxl->debug > 2) {
294 cursor_print_ascii_art(c, "qxl/alpha");
295 }
@@ -325,19 +326,23 @@ int qxl_render_cursor(PCIQXLDevice *qxl, QXLCommandExt *ext)
326 }
327 switch (cmd->type) {
328 case QXL_CURSOR_SET:
329 + {
330 + uint32_t chunk_data_size;
331 +
332 /* First read the QXLCursor to get QXLDataChunk::data_size ... */
333 cursor = qxl_phys2virt(qxl, cmd->u.set.shape, ext->group_id,
334 sizeof(QXLCursor));
335 if (!cursor) {
336 return 1;
337 }
338 + chunk_data_size = cursor->chunk.data_size;
339 /* Then read including the chunked data following QXLCursor. */
340 cursor = qxl_phys2virt(qxl, cmd->u.set.shape, ext->group_id,
336 - sizeof(QXLCursor) + cursor->chunk.data_size);
341 + sizeof(QXLCursor) + chunk_data_size);
342 if (!cursor) {
343 return 1;
344 }
340 - c = qxl_cursor(qxl, cursor, ext->group_id);
345 + c = qxl_cursor(qxl, cursor, ext->group_id, chunk_data_size);
346 if (c == NULL) {
347 c = cursor_builtin_left_ptr();
348 }
@@ -351,6 +356,7 @@ int qxl_render_cursor(PCIQXLDevice *qxl, QXLCommandExt *ext)
356 qemu_mutex_unlock(&qxl->ssd.lock);
357 qemu_bh_schedule(qxl->ssd.cursor_bh);
358 break;
359 + }
360 case QXL_CURSOR_MOVE:
361 qemu_mutex_lock(&qxl->ssd.lock);
362 qxl->ssd.mouse_x = cmd->u.position.x;