Commit 95bc45193b for qemu.org

commit 95bc45193bbebcb11a45fa40c5642712cd4401bc
Author: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>
Date:   Tue Aug 25 20:20:50 2026 +0300

    hw/display/qxl: validate replayed commands in qxl_post_load

    On incoming migration qxl_post_load() replays the tracked cursor and
    surface commands by handing their guest addresses straight to spice,
    without revalidating them. Those addresses were checked when the guest
    submitted them, but the guest may have freed or reused that memory
    before migration, so qxl's tracked pointer can be stale. spice-server
    then re-parses the command from that memory and, for a stale cursor,
    reads a garbage shape pointer -- aborting the target in memslot_get_virt()
    (again, spice-server function) and failing the migration.

    Validate each replayed command with qxl_guest_phys2virt(report_bug=false)
    before adding it to the replay list, and for a cursor also validate the
    nested shape pointer. Commands that no longer resolve are skipped rather
    than replayed. report_bug is false so a stale pointer is not mistaken for
    a live guest error, which would needlessly disable a healthy guest's
    display.

    Message-ID: <20260825172051.435372-3-andrey.drobyshev@virtuozzo.com>
    Reviewed-by: Marc-André Lureau <marcandre.lureau@redhat.com>
    Signed-off-by: Andrey Drobyshev <andrey.drobyshev@virtuozzo.com>

diff --git a/hw/display/qxl.c b/hw/display/qxl.c
index b6bc182fc2..fb77f217b1 100644
--- a/hw/display/qxl.c
+++ b/hw/display/qxl.c
@@ -2390,6 +2390,37 @@ static void qxl_create_memslots(PCIQXLDevice *d)
     }
 }

+/*
+ * Validate a command tracked for loadvm replay before handing its guest
+ * address to spice-server.
+ */
+static bool qxl_loadvm_cmd_valid(PCIQXLDevice *d, QXLPHYSICAL data,
+                                 uint32_t type)
+{
+    switch (type) {
+    case QXL_CMD_SURFACE:
+        return qxl_guest_phys2virt(d, data,
+                                   sizeof(QXLSurfaceCmd), false) != NULL;
+
+    case QXL_CMD_CURSOR: {
+        QXLCursorCmd *cmd = qxl_guest_phys2virt(d, data, sizeof(QXLCursorCmd),
+                                                false);
+
+        if (!cmd) {
+            return false;
+        }
+        if (le32_to_cpu(cmd->type) == QXL_CURSOR_SET) {
+            return qxl_guest_phys2virt(d, le64_to_cpu(cmd->u.set.shape),
+                                       sizeof(QXLCursor), false) != NULL;
+        }
+        return true;
+    }
+
+    default:
+        g_assert_not_reached();
+    }
+}
+
 static int qxl_post_load(void *opaque, int version)
 {
     PCIQXLDevice* d = opaque;
@@ -2428,12 +2459,17 @@ static int qxl_post_load(void *opaque, int version)
             if (d->guest_surfaces.cmds[in] == 0) {
                 continue;
             }
+            if (!qxl_loadvm_cmd_valid(d, d->guest_surfaces.cmds[in],
+                                      QXL_CMD_SURFACE)) {
+                continue;
+            }
             cmds[out].cmd.data = d->guest_surfaces.cmds[in];
             cmds[out].cmd.type = QXL_CMD_SURFACE;
             cmds[out].group_id = MEMSLOT_GROUP_GUEST;
             out++;
         }
-        if (d->guest_cursor) {
+        if (d->guest_cursor &&
+            qxl_loadvm_cmd_valid(d, d->guest_cursor, QXL_CMD_CURSOR)) {
             cmds[out].cmd.data = d->guest_cursor;
             cmds[out].cmd.type = QXL_CMD_CURSOR;
             cmds[out].group_id = MEMSLOT_GROUP_GUEST;