All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH 00/32] ui: better console hotplug support
@ 2026-05-29 11:16 Marc-André Lureau
  2026-05-29 11:16 ` [PATCH 01/32] ui/gtk: fix bad widget realize on non-GFX VC Marc-André Lureau
                   ` (31 more replies)
  0 siblings, 32 replies; 39+ messages in thread
From: Marc-André Lureau @ 2026-05-29 11:16 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Fabiano Rosas,
	Laurent Vivier, Alex Williamson, Cédric Le Goater,
	Peter Maydell, Akihiko Odaki, Marc-André Lureau

Hi,

This series improves handling of dynamic consoles in ui/ (commit
9588d67e72 "console: minimal hotplug suport" added basic unplug &
replug).  It allows for better hotplug of vfio-pci display devices or
bochs-display (the other device supporting hotplug today), with GTK and
D-Bus backends.

As usual, the first patches are various preliminary cleanups and fixes I
hit while developping the rest, they could be merged earlier. I improved
the display cleanup to replace the scattered atexit() handler, making
leaks more visible to sanitizers.

Then console ADDED/REMOVED notifications are added, so display backends
can react to hotplug. The GTK code is rework a bit to allow easier
hotplug events handling. Finally, D-Bus supports is implemented with a
qtest to exercise it.

thanks

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
Marc-André Lureau (32):
      ui/gtk: fix bad widget realize on non-GFX VC
      build-sys: build with -fno-omit-frame-pointer with ASAN
      irq: add per-IRQ observer to fix qemu_irq_intercept_in leak
      scripts/lsan_suppressions: suppress fontconfig leaks
      vfio/pci: close display console during unrealize, not finalize
      glib-compat: add fallback for g_clear_fd/g_autofd
      ui/dbus: remove mouse handler on dispose
      ui/qmp: keep a reference of console across yield
      ui: stop ui timer when closing
      ui/console: init gl_unblock_timer in qemu_console_init
      ui/spice: remove dead spice_displays
      ui/spice: add cleanup on shutdown
      ui: add display cleanup infrastructure
      ui/curses: implement display cleanup
      ui/sdl2: implement display cleanup
      ui/spice-app: implement display cleanup
      ui/egl: implement display and EGL cleanup
      ui/cocoa: implement display cleanup
      ui/dbus: implement display cleanup
      ui/gtk: implement display cleanup
      ui/console: add console event notifier infrastructure
      ui/console: fire console ADDED/REMOVED notifications
      ui/console-vc: fire ADDED/REMOVED notifications
      ui/gtk: convert VirtualConsole storage from fixed array to GPtrArray
      ui/gtk: move global display settings out of per-console init
      ui/gtk: fix tab re-insertion order on window close
      ui/gtk: centralize console menu and shortcut management
      ui/gtk: handle console hotplug/unplug events
      ui/console: register console in QOM tree dynamically
      ui/console: unregister console from QOM tree on close
      ui/dbus: handle console hotplug/unplug events
      tests/qtest: add D-Bus display hotplug test

 meson.build                     |   2 +
 hw/vfio/pci.h                   |   1 +
 include/glib-compat.h           |  29 ++++
 include/hw/core/irq.h           |   1 +
 include/ui/console.h            |  20 +++
 include/ui/egl-helpers.h        |   1 +
 include/ui/gtk.h                |   6 +-
 include/ui/qemu-spice-module.h  |   1 +
 include/ui/qemu-spice.h         |   9 +-
 ui/dbus.h                       |   3 +
 hw/core/irq.c                   |  10 +-
 hw/vfio/display.c               |  30 ++--
 hw/vfio/pci.c                   |   1 +
 system/qtest.c                  |   3 -
 system/runstate.c               |   4 +-
 tests/qtest/dbus-display-test.c | 101 ++++++++++-
 ui/console-vc.c                 |  13 ++
 ui/console.c                    |  71 +++++++-
 ui/curses.c                     |  17 +-
 ui/dbus-console.c               |   9 +
 ui/dbus.c                       | 106 ++++++++++--
 ui/egl-headless.c               |  31 ++++
 ui/egl-helpers.c                |  19 +++
 ui/gtk-clipboard.c              |  15 ++
 ui/gtk.c                        | 369 +++++++++++++++++++++++++++++++---------
 ui/sdl2.c                       |  23 ++-
 ui/spice-app.c                  |  10 +-
 ui/spice-core.c                 |  25 ++-
 ui/spice-display.c              |  52 ++++++
 ui/spice-input.c                |  52 ++++--
 ui/spice-module.c               |   5 +
 ui/ui-qmp-cmds.c                |   3 +
 scripts/lsan_suppressions.txt   |  15 +-
 ui/cocoa.m                      |  19 ++-
 ui/meson.build                  |   4 +-
 35 files changed, 901 insertions(+), 179 deletions(-)
---
base-commit: 2db91528542672cf0db78b3f2cc0e22b36302b38
change-id: 20260529-b4-ui-909bfa695735

Best regards,
--  
Marc-André Lureau <marcandre.lureau@redhat.com>



^ permalink raw reply	[flat|nested] 39+ messages in thread

* [PATCH 01/32] ui/gtk: fix bad widget realize on non-GFX VC
  2026-05-29 11:16 [PATCH 00/32] ui: better console hotplug support Marc-André Lureau
@ 2026-05-29 11:16 ` Marc-André Lureau
  2026-05-29 11:16 ` [PATCH 02/32] build-sys: build with -fno-omit-frame-pointer with ASAN Marc-André Lureau
                   ` (30 subsequent siblings)
  31 siblings, 0 replies; 39+ messages in thread
From: Marc-André Lureau @ 2026-05-29 11:16 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Fabiano Rosas,
	Laurent Vivier, Alex Williamson, Cédric Le Goater,
	Peter Maydell, Akihiko Odaki, Marc-André Lureau

The GTK VirtualConsole is a union, it may be .gfx or .vte depending on
the type.

Fixes: 565f85a9c2 ("ui/gtk: force realization of drawing area")
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 ui/gtk.c | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/ui/gtk.c b/ui/gtk.c
index 4f706c6bbb2..2ee826b56fb 100644
--- a/ui/gtk.c
+++ b/ui/gtk.c
@@ -2585,7 +2585,9 @@ static void gtk_display_init(DisplayState *ds, DisplayOptions *opts)
         if (!con) {
             break;
         }
-        gtk_widget_realize(s->vc[idx].gfx.drawing_area);
+        if (s->vc[idx].type == GD_VC_GFX) {
+            gtk_widget_realize(s->vc[idx].gfx.drawing_area);
+        }
     }
 
     if (opts->u.gtk.has_show_menubar &&

-- 
2.54.0



^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH 02/32] build-sys: build with -fno-omit-frame-pointer with ASAN
  2026-05-29 11:16 [PATCH 00/32] ui: better console hotplug support Marc-André Lureau
  2026-05-29 11:16 ` [PATCH 01/32] ui/gtk: fix bad widget realize on non-GFX VC Marc-André Lureau
@ 2026-05-29 11:16 ` Marc-André Lureau
  2026-05-29 11:16 ` [PATCH 03/32] irq: add per-IRQ observer to fix qemu_irq_intercept_in leak Marc-André Lureau
                   ` (29 subsequent siblings)
  31 siblings, 0 replies; 39+ messages in thread
From: Marc-André Lureau @ 2026-05-29 11:16 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Fabiano Rosas,
	Laurent Vivier, Alex Williamson, Cédric Le Goater,
	Peter Maydell, Akihiko Odaki, Marc-André Lureau

On fc44, LSan fails to suppress leak:qemu_irq_intercept_in, because
the backtrace isn't deep enough.

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 meson.build | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/meson.build b/meson.build
index eb074918193..224534ac351 100644
--- a/meson.build
+++ b/meson.build
@@ -545,6 +545,8 @@ if get_option('asan')
   if cc.has_argument('-fsanitize=address')
     qemu_cflags = ['-fsanitize=address'] + qemu_cflags
     qemu_ldflags = ['-fsanitize=address'] + qemu_ldflags
+    # Ensure complete stack traces for LSan suppressions to match correctly.
+    qemu_cflags += ['-fno-omit-frame-pointer']
   else
     error('Your compiler does not support -fsanitize=address')
   endif

-- 
2.54.0



^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH 03/32] irq: add per-IRQ observer to fix qemu_irq_intercept_in leak
  2026-05-29 11:16 [PATCH 00/32] ui: better console hotplug support Marc-André Lureau
  2026-05-29 11:16 ` [PATCH 01/32] ui/gtk: fix bad widget realize on non-GFX VC Marc-André Lureau
  2026-05-29 11:16 ` [PATCH 02/32] build-sys: build with -fno-omit-frame-pointer with ASAN Marc-André Lureau
@ 2026-05-29 11:16 ` Marc-André Lureau
  2026-05-29 13:27   ` Fabiano Rosas
  2026-05-29 13:44   ` Peter Maydell
  2026-05-29 11:16 ` [PATCH 04/32] scripts/lsan_suppressions: suppress fontconfig leaks Marc-André Lureau
                   ` (28 subsequent siblings)
  31 siblings, 2 replies; 39+ messages in thread
From: Marc-André Lureau @ 2026-05-29 11:16 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Fabiano Rosas,
	Laurent Vivier, Alex Williamson, Cédric Le Goater,
	Peter Maydell, Akihiko Odaki, Marc-André Lureau

qemu_irq_intercept_in() saves original IRQ handlers by allocating
new QOM objects, which are never freed. On a PC machine, this leaks
IRQ objects (one per IOAPIC pin) on every qtest run.

Rather than tracking allocations to free later, avoid them: add an
"observer" field to IRQState, called by qemu_set_irq() after the
real handler. Interception sets the observer instead of rewriting
handlers, so there's nothing to save and nothing to leak.

Fix qemu_notirq() to route through qemu_set_irq() so inverted IRQs
trigger observers too. Drop the LSan suppression.

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 include/hw/core/irq.h         |  1 +
 hw/core/irq.c                 | 10 +++++-----
 system/qtest.c                |  3 ---
 scripts/lsan_suppressions.txt |  8 --------
 4 files changed, 6 insertions(+), 16 deletions(-)

diff --git a/include/hw/core/irq.h b/include/hw/core/irq.h
index 291fdd67df4..93d5710a73e 100644
--- a/include/hw/core/irq.h
+++ b/include/hw/core/irq.h
@@ -14,6 +14,7 @@ struct IRQState {
     qemu_irq_handler handler;
     void *opaque;
     int n;
+    qemu_irq_handler observer;
 };
 
 void qemu_set_irq(qemu_irq irq, int level);
diff --git a/hw/core/irq.c b/hw/core/irq.c
index 106805e2417..fa11e9bc0aa 100644
--- a/hw/core/irq.c
+++ b/hw/core/irq.c
@@ -32,6 +32,9 @@ void qemu_set_irq(qemu_irq irq, int level)
         return;
 
     irq->handler(irq->opaque, irq->n, level);
+    if (unlikely(irq->observer)) {
+        irq->observer(irq->opaque, irq->n, level);
+    }
 }
 
 static void init_irq_fields(IRQState *irq, qemu_irq_handler handler,
@@ -111,7 +114,7 @@ static void qemu_notirq(void *opaque, int line, int level)
 {
     IRQState *irq = opaque;
 
-    irq->handler(irq->opaque, irq->n, !level);
+    qemu_set_irq(irq, !level);
 }
 
 qemu_irq qemu_irq_invert(qemu_irq irq)
@@ -124,11 +127,8 @@ qemu_irq qemu_irq_invert(qemu_irq irq)
 void qemu_irq_intercept_in(qemu_irq *gpio_in, qemu_irq_handler handler, int n)
 {
     int i;
-    qemu_irq *old_irqs = qemu_allocate_irqs(NULL, NULL, n);
     for (i = 0; i < n; i++) {
-        *old_irqs[i] = *gpio_in[i];
-        gpio_in[i]->handler = handler;
-        gpio_in[i]->opaque = &old_irqs[i];
+        gpio_in[i]->observer = handler;
     }
 }
 
diff --git a/system/qtest.c b/system/qtest.c
index a79d10d1361..b359a3e1c84 100644
--- a/system/qtest.c
+++ b/system/qtest.c
@@ -324,9 +324,6 @@ void qtest_sendf(CharFrontend *chr, const char *fmt, ...)
 
 static void qtest_irq_handler(void *opaque, int n, int level)
 {
-    qemu_irq old_irq = *(qemu_irq *)opaque;
-    qemu_set_irq(old_irq, level);
-
     if (irq_levels[n] != level) {
         CharFrontend *chr = &qtest->qtest_chr;
         irq_levels[n] = level;
diff --git a/scripts/lsan_suppressions.txt b/scripts/lsan_suppressions.txt
index f88bbab18b8..30256bc6d01 100644
--- a/scripts/lsan_suppressions.txt
+++ b/scripts/lsan_suppressions.txt
@@ -16,11 +16,3 @@ leak:/lib64/libxkbcommon.so.0
 # https://github.com/GNOME/glib/blob/main/tools/glib.supp
 # This avoids false positive leak reports for the qga-ssh-test.
 leak:g_set_user_dirs
-
-# qemu_irq_intercept_in is only used by the qtest harness, and
-# its API inherently involves a leak.
-# While we could keep track of the old IRQ data structure
-# in order to free it, it doesn't seem very important to fix
-# since it is only used by the qtest test harness.
-# Just ignore the leak, at least for the moment.
-leak:qemu_irq_intercept_in

-- 
2.54.0



^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH 04/32] scripts/lsan_suppressions: suppress fontconfig leaks
  2026-05-29 11:16 [PATCH 00/32] ui: better console hotplug support Marc-André Lureau
                   ` (2 preceding siblings ...)
  2026-05-29 11:16 ` [PATCH 03/32] irq: add per-IRQ observer to fix qemu_irq_intercept_in leak Marc-André Lureau
@ 2026-05-29 11:16 ` Marc-André Lureau
  2026-05-29 11:16 ` [PATCH 05/32] vfio/pci: close display console during unrealize, not finalize Marc-André Lureau
                   ` (27 subsequent siblings)
  31 siblings, 0 replies; 39+ messages in thread
From: Marc-André Lureau @ 2026-05-29 11:16 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Fabiano Rosas,
	Laurent Vivier, Alex Williamson, Cédric Le Goater,
	Peter Maydell, Akihiko Odaki, Marc-André Lureau

Those are annoying reports for gtk/sdl etc.

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 scripts/lsan_suppressions.txt | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/scripts/lsan_suppressions.txt b/scripts/lsan_suppressions.txt
index 30256bc6d01..f3b827facff 100644
--- a/scripts/lsan_suppressions.txt
+++ b/scripts/lsan_suppressions.txt
@@ -10,6 +10,10 @@ leak:/lib64/libtcmalloc_minimal.so.4
 # libxkbcommon also leaks in qemu-keymap
 leak:/lib64/libxkbcommon.so.0
 
+# libfontconfig leaks are notorious, for ex
+# https://gitlab.freedesktop.org/fontconfig/fontconfig/-/work_items/519
+leak:libfontconfig.so
+
 # g_set_user_dirs() deliberately leaks the previous cached g_get_user_*
 # values. This is documented in upstream glib's valgrind-format
 # suppression file:

-- 
2.54.0



^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH 05/32] vfio/pci: close display console during unrealize, not finalize
  2026-05-29 11:16 [PATCH 00/32] ui: better console hotplug support Marc-André Lureau
                   ` (3 preceding siblings ...)
  2026-05-29 11:16 ` [PATCH 04/32] scripts/lsan_suppressions: suppress fontconfig leaks Marc-André Lureau
@ 2026-05-29 11:16 ` Marc-André Lureau
  2026-05-29 11:16 ` [PATCH 06/32] glib-compat: add fallback for g_clear_fd/g_autofd Marc-André Lureau
                   ` (26 subsequent siblings)
  31 siblings, 0 replies; 39+ messages in thread
From: Marc-André Lureau @ 2026-05-29 11:16 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Fabiano Rosas,
	Laurent Vivier, Alex Williamson, Cédric Le Goater,
	Peter Maydell, Akihiko Odaki, Marc-André Lureau

The QemuGraphicConsole holds a strong QOM link back to the device via
its "device" property (OBJ_PROP_LINK_STRONG). When graphic_console_close()
is only called from vfio_display_finalize() during object finalize, this
creates a ref-cycle deadlock: the device can't reach refcount 0 because
the console holds a strong ref, but the console's ref is only dropped by
graphic_console_close() which runs inside finalize.

Split the display teardown into two phases:
- vfio_display_exit(): called during unrealize (vfio_exitfn), closes
  the graphic console to break the ref cycle, and removes display
  region subregions while the parent memory regions are still alive.
- vfio_display_finalize(): remains in finalize (vfio_pci_put_device),
  frees display region memory, dmabuf, and edid resources. The region
  memory contains QOM child objects (MemoryRegions) that must stay
  alive until QOM finalization has processed them.

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 hw/vfio/pci.h     |  1 +
 hw/vfio/display.c | 30 +++++++++++++++++++-----------
 hw/vfio/pci.c     |  1 +
 3 files changed, 21 insertions(+), 11 deletions(-)

diff --git a/hw/vfio/pci.h b/hw/vfio/pci.h
index c3a1f53d350..cf567115870 100644
--- a/hw/vfio/pci.h
+++ b/hw/vfio/pci.h
@@ -270,6 +270,7 @@ bool vfio_populate_vga(VFIOPCIDevice *vdev, Error **errp);
 
 void vfio_display_reset(VFIOPCIDevice *vdev);
 bool vfio_display_probe(VFIOPCIDevice *vdev, Error **errp);
+void vfio_display_exit(VFIOPCIDevice *vdev);
 void vfio_display_finalize(VFIOPCIDevice *vdev);
 
 extern const VMStateDescription vfio_display_vmstate;
diff --git a/hw/vfio/display.c b/hw/vfio/display.c
index 8f91e83da88..34cc25ee0e0 100644
--- a/hw/vfio/display.c
+++ b/hw/vfio/display.c
@@ -505,15 +505,6 @@ static bool vfio_display_region_init(VFIOPCIDevice *vdev, Error **errp)
     return true;
 }
 
-static void vfio_display_region_exit(VFIODisplay *dpy)
-{
-    if (!dpy->region.buffer.size) {
-        return;
-    }
-
-    vfio_region_exit(&dpy->region.buffer);
-    vfio_region_finalize(&dpy->region.buffer);
-}
 
 /* ---------------------------------------------------------------------- */
 
@@ -547,17 +538,34 @@ bool vfio_display_probe(VFIOPCIDevice *vdev, Error **errp)
     return false;
 }
 
-void vfio_display_finalize(VFIOPCIDevice *vdev)
+void vfio_display_exit(VFIOPCIDevice *vdev)
 {
     if (!vdev->dpy) {
         return;
     }
 
+    if (display_opengl) {
+        qemu_console_set_display_gl_ctx(vdev->dpy->con, NULL);
+    }
     qemu_graphic_console_close(vdev->dpy->con);
+    if (vdev->dpy->region.buffer.size) {
+        vfio_region_exit(&vdev->dpy->region.buffer);
+    }
+}
+
+void vfio_display_finalize(VFIOPCIDevice *vdev)
+{
+    if (!vdev->dpy) {
+        return;
+    }
+
     vfio_display_dmabuf_exit(vdev->dpy);
-    vfio_display_region_exit(vdev->dpy);
+    if (vdev->dpy->region.buffer.size) {
+        vfio_region_finalize(&vdev->dpy->region.buffer);
+    }
     vfio_display_edid_exit(vdev->dpy);
     g_free(vdev->dpy);
+    vdev->dpy = NULL;
 }
 
 static bool migrate_needed(void *opaque)
diff --git a/hw/vfio/pci.c b/hw/vfio/pci.c
index 9c06b25e637..78beacd24e1 100644
--- a/hw/vfio/pci.c
+++ b/hw/vfio/pci.c
@@ -3624,6 +3624,7 @@ static void vfio_exitfn(PCIDevice *pdev)
     VFIOPCIDevice *vdev = VFIO_PCI_DEVICE(pdev);
     VFIODevice *vbasedev = &vdev->vbasedev;
 
+    vfio_display_exit(vdev);
     vfio_unregister_req_notifier(vdev);
     vfio_unregister_err_notifier(vdev);
     pci_device_set_intx_routing_notifier(pdev, NULL);

-- 
2.54.0



^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH 06/32] glib-compat: add fallback for g_clear_fd/g_autofd
  2026-05-29 11:16 [PATCH 00/32] ui: better console hotplug support Marc-André Lureau
                   ` (4 preceding siblings ...)
  2026-05-29 11:16 ` [PATCH 05/32] vfio/pci: close display console during unrealize, not finalize Marc-André Lureau
@ 2026-05-29 11:16 ` Marc-André Lureau
  2026-05-29 11:16 ` [PATCH 07/32] ui/dbus: remove mouse handler on dispose Marc-André Lureau
                   ` (25 subsequent siblings)
  31 siblings, 0 replies; 39+ messages in thread
From: Marc-André Lureau @ 2026-05-29 11:16 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Fabiano Rosas,
	Laurent Vivier, Alex Williamson, Cédric Le Goater,
	Peter Maydell, Akihiko Odaki, Marc-André Lureau

Those helpers were added in glib 2.76.

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 include/glib-compat.h | 29 +++++++++++++++++++++++++++++
 1 file changed, 29 insertions(+)

diff --git a/include/glib-compat.h b/include/glib-compat.h
index 2e32b90f051..32ee2afdcdf 100644
--- a/include/glib-compat.h
+++ b/include/glib-compat.h
@@ -30,6 +30,7 @@
 #pragma GCC diagnostic ignored "-Wdeprecated-declarations"
 
 #include <glib.h>
+#include <glib/gstdio.h>
 #if defined(G_OS_UNIX)
 #include <glib-unix.h>
 #include <sys/types.h>
@@ -129,6 +130,34 @@ qemu_g_test_slow(void)
 #define g_test_thorough() qemu_g_test_slow()
 #define g_test_quick() (!qemu_g_test_slow())
 
+static inline gboolean g_clear_fd_qemu(int *fd_ptr, GError **error)
+{
+#if GLIB_CHECK_VERSION(2, 76, 0)
+    return g_clear_fd(fd_ptr, error);
+#else
+    int fd = *fd_ptr;
+
+    *fd_ptr = -1;
+
+    if (fd < 0) {
+        return TRUE;
+    }
+
+    return g_close(fd, error);
+#endif
+}
+#define g_clear_fd(fd, err) g_clear_fd_qemu(fd, err)
+
+#if !GLIB_CHECK_VERSION(2, 76, 0)
+static inline void _g_clear_fd_ignore_error(int *fd_ptr)
+{
+    int errsv = errno;
+    g_clear_fd(fd_ptr, NULL);
+    errno = errsv;
+}
+#define g_autofd __attribute__((cleanup(_g_clear_fd_ignore_error)))
+#endif
+
 #pragma GCC diagnostic pop
 
 #ifndef G_NORETURN

-- 
2.54.0



^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH 07/32] ui/dbus: remove mouse handler on dispose
  2026-05-29 11:16 [PATCH 00/32] ui: better console hotplug support Marc-André Lureau
                   ` (5 preceding siblings ...)
  2026-05-29 11:16 ` [PATCH 06/32] glib-compat: add fallback for g_clear_fd/g_autofd Marc-André Lureau
@ 2026-05-29 11:16 ` Marc-André Lureau
  2026-05-29 11:16 ` [PATCH 08/32] ui/qmp: keep a reference of console across yield Marc-André Lureau
                   ` (24 subsequent siblings)
  31 siblings, 0 replies; 39+ messages in thread
From: Marc-André Lureau @ 2026-05-29 11:16 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Fabiano Rosas,
	Laurent Vivier, Alex Williamson, Cédric Le Goater,
	Peter Maydell, Akihiko Odaki, Marc-André Lureau

Fixes: 142ca628a7 ("ui: add a D-Bus display backend")
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 ui/dbus-console.c | 1 +
 1 file changed, 1 insertion(+)

diff --git a/ui/dbus-console.c b/ui/dbus-console.c
index 21eceb24362..0813a08f85e 100644
--- a/ui/dbus-console.c
+++ b/ui/dbus-console.c
@@ -151,6 +151,7 @@ dbus_display_console_dispose(GObject *object)
     DBusDisplayConsole *ddc = DBUS_DISPLAY_CONSOLE(object);
 
     qemu_console_unregister_listener(&ddc->dcl);
+    qemu_remove_mouse_mode_change_notifier(&ddc->mouse_mode_notifier);
     g_clear_object(&ddc->iface_touch);
     g_clear_object(&ddc->iface_mouse);
     g_clear_object(&ddc->iface_kbd);

-- 
2.54.0



^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH 08/32] ui/qmp: keep a reference of console across yield
  2026-05-29 11:16 [PATCH 00/32] ui: better console hotplug support Marc-André Lureau
                   ` (6 preceding siblings ...)
  2026-05-29 11:16 ` [PATCH 07/32] ui/dbus: remove mouse handler on dispose Marc-André Lureau
@ 2026-05-29 11:16 ` Marc-André Lureau
  2026-05-29 11:16 ` [PATCH 09/32] ui: stop ui timer when closing Marc-André Lureau
                   ` (23 subsequent siblings)
  31 siblings, 0 replies; 39+ messages in thread
From: Marc-André Lureau @ 2026-05-29 11:16 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Fabiano Rosas,
	Laurent Vivier, Alex Williamson, Cédric Le Goater,
	Peter Maydell, Akihiko Odaki, Marc-André Lureau

While the coroutine is waiting, the console could be finalized. Keep a
reference to prevent this.

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 ui/ui-qmp-cmds.c | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/ui/ui-qmp-cmds.c b/ui/ui-qmp-cmds.c
index 1173c82cf7f..753cc2bf522 100644
--- a/ui/ui-qmp-cmds.c
+++ b/ui/ui-qmp-cmds.c
@@ -348,6 +348,7 @@ qmp_screendump(const char *filename, const char *device,
         }
     }
 
+    object_ref(con);
     qemu_console_co_wait_update(con);
 
     /*
@@ -358,9 +359,11 @@ qmp_screendump(const char *filename, const char *device,
     surface = qemu_console_surface(con);
     if (!surface) {
         error_setg(errp, "no surface");
+        object_unref(con);
         return;
     }
     image = pixman_image_ref(surface->image);
+    object_unref(con);
 
     fd = qemu_create(filename, O_WRONLY | O_TRUNC | O_BINARY, 0666, errp);
     if (fd == -1) {

-- 
2.54.0



^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH 09/32] ui: stop ui timer when closing
  2026-05-29 11:16 [PATCH 00/32] ui: better console hotplug support Marc-André Lureau
                   ` (7 preceding siblings ...)
  2026-05-29 11:16 ` [PATCH 08/32] ui/qmp: keep a reference of console across yield Marc-André Lureau
@ 2026-05-29 11:16 ` Marc-André Lureau
  2026-05-29 11:16 ` [PATCH 10/32] ui/console: init gl_unblock_timer in qemu_console_init Marc-André Lureau
                   ` (22 subsequent siblings)
  31 siblings, 0 replies; 39+ messages in thread
From: Marc-André Lureau @ 2026-05-29 11:16 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Fabiano Rosas,
	Laurent Vivier, Alex Williamson, Cédric Le Goater,
	Peter Maydell, Akihiko Odaki, Marc-André Lureau

hwops is reset, so if the UI timer is pending it will crash.

Fixes: 9588d67e72 ("console: minimal hotplug suport")
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 ui/console.c | 1 +
 1 file changed, 1 insertion(+)

diff --git a/ui/console.c b/ui/console.c
index a7c977d0c44..436444723a5 100644
--- a/ui/console.c
+++ b/ui/console.c
@@ -1136,6 +1136,7 @@ void qemu_graphic_console_close(QemuConsole *con)
     trace_console_gfx_close(con->index);
     object_property_set_link(OBJECT(con), "device", NULL, &error_abort);
     qemu_graphic_console_set_hwops(con, &unused_ops, NULL);
+    timer_del(con->ui_timer);
 
     if (con->gl) {
         qemu_console_gl_scanout_disable(con);

-- 
2.54.0



^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH 10/32] ui/console: init gl_unblock_timer in qemu_console_init
  2026-05-29 11:16 [PATCH 00/32] ui: better console hotplug support Marc-André Lureau
                   ` (8 preceding siblings ...)
  2026-05-29 11:16 ` [PATCH 09/32] ui: stop ui timer when closing Marc-André Lureau
@ 2026-05-29 11:16 ` Marc-André Lureau
  2026-05-29 11:16 ` [PATCH 11/32] ui/spice: remove dead spice_displays Marc-André Lureau
                   ` (21 subsequent siblings)
  31 siblings, 0 replies; 39+ messages in thread
From: Marc-André Lureau @ 2026-05-29 11:16 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Fabiano Rosas,
	Laurent Vivier, Alex Williamson, Cédric Le Goater,
	Peter Maydell, Akihiko Odaki, Marc-André Lureau

Move gl_unblock_timer allocation from graphic_console_init() to
qemu_console_init(), similar to what was done in commit cfde05d15b
("ui/console: allocate ui_timer in QemuConsole").

This fixes leaking timers on console recycling.

Fixes: a9b1e471e17 ("ui: add a gl-unblock warning timer")
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 ui/console.c | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/ui/console.c b/ui/console.c
index 436444723a5..58f29e82c85 100644
--- a/ui/console.c
+++ b/ui/console.c
@@ -433,6 +433,8 @@ qemu_console_init(Object *obj)
     c->window_id = -1;
     c->ui_timer = timer_new_ms(QEMU_CLOCK_REALTIME,
                                dpy_set_ui_info_timer, c);
+    c->gl_unblock_timer = timer_new_ms(QEMU_CLOCK_REALTIME,
+                                       console_hw_gl_unblock_timer, c);
     qemu_console_register(c);
 }
 
@@ -1116,8 +1118,6 @@ QemuConsole *qemu_graphic_console_create(DeviceState *dev, uint32_t head,
 
     surface = qemu_create_placeholder_surface(width, height, noinit);
     qemu_console_set_surface(s, surface);
-    s->gl_unblock_timer = timer_new_ms(QEMU_CLOCK_REALTIME,
-                                       console_hw_gl_unblock_timer, s);
     return s;
 }
 

-- 
2.54.0



^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH 11/32] ui/spice: remove dead spice_displays
  2026-05-29 11:16 [PATCH 00/32] ui: better console hotplug support Marc-André Lureau
                   ` (9 preceding siblings ...)
  2026-05-29 11:16 ` [PATCH 10/32] ui/console: init gl_unblock_timer in qemu_console_init Marc-André Lureau
@ 2026-05-29 11:16 ` Marc-André Lureau
  2026-05-29 11:16 ` [PATCH 12/32] ui/spice: add cleanup on shutdown Marc-André Lureau
                   ` (20 subsequent siblings)
  31 siblings, 0 replies; 39+ messages in thread
From: Marc-André Lureau @ 2026-05-29 11:16 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Fabiano Rosas,
	Laurent Vivier, Alex Williamson, Cédric Le Goater,
	Peter Maydell, Akihiko Odaki, Marc-André Lureau

This is left-over from commit 9fa032866da ("spice: fix multihead support")

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 include/ui/qemu-spice.h | 7 -------
 1 file changed, 7 deletions(-)

diff --git a/include/ui/qemu-spice.h b/include/ui/qemu-spice.h
index 111a09ceca3..59a68cd9833 100644
--- a/include/ui/qemu-spice.h
+++ b/include/ui/qemu-spice.h
@@ -33,13 +33,6 @@ bool qemu_spice_have_display_interface(QemuConsole *con);
 int qemu_spice_add_display_interface(QXLInstance *qxlin, QemuConsole *con);
 int qemu_spice_migrate_info(const char *hostname, int port, int tls_port,
                             const char *subject);
-
-#else  /* CONFIG_SPICE */
-
-#include "qemu/error-report.h"
-
-#define spice_displays 0
-
 #endif /* CONFIG_SPICE */
 
 static inline bool qemu_using_spice(Error **errp)

-- 
2.54.0



^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH 12/32] ui/spice: add cleanup on shutdown
  2026-05-29 11:16 [PATCH 00/32] ui: better console hotplug support Marc-André Lureau
                   ` (10 preceding siblings ...)
  2026-05-29 11:16 ` [PATCH 11/32] ui/spice: remove dead spice_displays Marc-André Lureau
@ 2026-05-29 11:16 ` Marc-André Lureau
  2026-05-29 11:16 ` [PATCH 13/32] ui: add display cleanup infrastructure Marc-André Lureau
                   ` (19 subsequent siblings)
  31 siblings, 0 replies; 39+ messages in thread
From: Marc-André Lureau @ 2026-05-29 11:16 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Fabiano Rosas,
	Laurent Vivier, Alex Williamson, Cédric Le Goater,
	Peter Maydell, Akihiko Odaki, Marc-André Lureau

SPICE resources were never freed on shutdown. Add per-subsystem
cleanup (display, input, core) and call it from qemu_cleanup().

Move spice-module.c into libui so the qemu_spice ops table links
with the rest of the UI code. Add an LSan suppression for a known
spice-server leak.

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 include/ui/qemu-spice-module.h |  1 +
 include/ui/qemu-spice.h        |  2 ++
 system/runstate.c              |  4 ++++
 ui/spice-core.c                | 25 ++++++++++++++++++--
 ui/spice-display.c             | 52 ++++++++++++++++++++++++++++++++++++++++++
 ui/spice-input.c               | 52 ++++++++++++++++++++++++++++--------------
 ui/spice-module.c              |  5 ++++
 scripts/lsan_suppressions.txt  |  5 ++++
 ui/meson.build                 |  4 ++--
 9 files changed, 129 insertions(+), 21 deletions(-)

diff --git a/include/ui/qemu-spice-module.h b/include/ui/qemu-spice-module.h
index 072efa0c834..bb0f8437c26 100644
--- a/include/ui/qemu-spice-module.h
+++ b/include/ui/qemu-spice-module.h
@@ -26,6 +26,7 @@ typedef struct SpiceInfo SpiceInfo;
 
 struct QemuSpiceOps {
     void (*init)(void);
+    void (*cleanup)(void);
     void (*display_init)(void);
     int (*migrate_info)(const char *h, int p, int t, const char *s);
     int (*set_passwd)(const char *passwd,
diff --git a/include/ui/qemu-spice.h b/include/ui/qemu-spice.h
index 59a68cd9833..2cdf10f0313 100644
--- a/include/ui/qemu-spice.h
+++ b/include/ui/qemu-spice.h
@@ -27,7 +27,9 @@
 #include "qemu/config-file.h"
 
 void qemu_spice_input_init(void);
+void qemu_spice_input_cleanup(void);
 void qemu_spice_display_init(void);
+void qemu_spice_display_cleanup(void);
 void qemu_spice_display_init_done(void);
 bool qemu_spice_have_display_interface(QemuConsole *con);
 int qemu_spice_add_display_interface(QXLInstance *qxlin, QemuConsole *con);
diff --git a/system/runstate.c b/system/runstate.c
index 0e1cb3b4e67..d35fa270bd6 100644
--- a/system/runstate.c
+++ b/system/runstate.c
@@ -62,6 +62,7 @@
 #include "system/system.h"
 #include "system/tpm.h"
 #include "ui/console.h"
+#include "ui/qemu-spice-module.h"
 
 #include "trace.h"
 
@@ -1048,6 +1049,9 @@ void qemu_cleanup(int status)
     user_creatable_cleanup();
 #ifdef CONFIG_VNC
     vnc_cleanup();
+#endif
+#ifdef CONFIG_SPICE
+    qemu_spice.cleanup();
 #endif
     /* TODO: unref root container, check all devices are ok */
 }
diff --git a/ui/spice-core.c b/ui/spice-core.c
index ef1c00134fa..1d2315f0b63 100644
--- a/ui/spice-core.c
+++ b/ui/spice-core.c
@@ -651,12 +651,15 @@ static void vm_change_state_handler(void *opaque, bool running,
     }
 }
 
+static VMChangeStateEntry *vm_change_entry;
+
 void qemu_spice_display_init_done(void)
 {
     if (runstate_is_running()) {
         qemu_spice_display_start();
     }
-    qemu_add_vm_change_state_handler(vm_change_state_handler, NULL);
+    vm_change_entry =
+        qemu_add_vm_change_state_handler(vm_change_state_handler, NULL);
 }
 
 static void qemu_spice_init(void)
@@ -894,7 +897,8 @@ static int qemu_spice_add_interface(SpiceBaseInstance *sin)
         spice_server = spice_server_new();
         spice_server_set_sasl_appname(spice_server, "qemu");
         spice_server_init(spice_server, &core_interface);
-        qemu_add_vm_change_state_handler(vm_change_state_handler, NULL);
+        vm_change_entry =
+            qemu_add_vm_change_state_handler(vm_change_state_handler, NULL);
     }
 
     return spice_server_add_interface(spice_server, sin);
@@ -1005,8 +1009,25 @@ int qemu_spice_display_is_running(SimpleSpiceDisplay *ssd)
     return spice_display_is_running;
 }
 
+static void qemu_spice_cleanup(void)
+{
+    if (!spice_server) {
+        return;
+    }
+
+    qemu_spice_display_cleanup();
+    qemu_spice_input_cleanup();
+    migration_remove_notifier(&migration_state);
+    g_clear_pointer(&spice_consoles, g_slist_free);
+    g_clear_pointer(&auth_passwd, g_free);
+    g_clear_pointer(&spice_server, spice_server_destroy);
+    g_clear_pointer(&vm_change_entry, qemu_del_vm_change_state_handler);
+    using_spice = 0;
+}
+
 static struct QemuSpiceOps real_spice_ops = {
     .init         = qemu_spice_init,
+    .cleanup      = qemu_spice_cleanup,
     .display_init = qemu_spice_display_init,
     .migrate_info = qemu_spice_migrate_info,
     .set_passwd   = qemu_spice_set_passwd,
diff --git a/ui/spice-display.c b/ui/spice-display.c
index e3716127203..75c7df7bb5e 100644
--- a/ui/spice-display.c
+++ b/ui/spice-display.c
@@ -34,6 +34,8 @@ bool spice_opengl;
 bool spice_remote_client;
 int spice_max_refresh_rate;
 
+static GPtrArray *spice_displays;
+
 int qemu_spice_rect_is_empty(const QXLRect* r)
 {
     return r->top == r->bottom || r->left == r->right;
@@ -1421,6 +1423,54 @@ static void qemu_spice_display_init_one(QemuConsole *con)
         qemu_console_set_display_gl_ctx(con, &ssd->dgc);
     }
     qemu_console_register_listener(con, &ssd->dcl, ops);
+    g_ptr_array_add(spice_displays, ssd);
+}
+
+void qemu_spice_display_cleanup(void)
+{
+    if (!spice_displays) {
+        return;
+    }
+
+    for (guint i = 0; i < spice_displays->len; i++) {
+        SimpleSpiceDisplay *ssd = g_ptr_array_index(spice_displays, i);
+        SimpleSpiceUpdate *update;
+
+        qemu_console_unregister_listener(&ssd->dcl);
+#ifdef HAVE_SPICE_GL
+        if (spice_opengl) {
+            qemu_console_set_display_gl_ctx(ssd->dcl.con, NULL);
+        }
+#endif
+
+        if (ssd->ds) {
+            qemu_spice_destroy_host_primary(ssd);
+        }
+        qemu_spice_del_memslot(ssd, MEMSLOT_GROUP_HOST, 0);
+        spice_server_remove_interface(&ssd->qxl.base);
+
+        while ((update = QTAILQ_FIRST(&ssd->updates)) != NULL) {
+            QTAILQ_REMOVE(&ssd->updates, update, next);
+            qemu_spice_destroy_update(ssd, update);
+        }
+        g_clear_pointer(&ssd->ptr_define, g_free);
+        g_clear_pointer(&ssd->ptr_move, g_free);
+        g_clear_pointer(&ssd->cursor, cursor_unref);
+        g_clear_pointer(&ssd->surface, pixman_image_unref);
+        g_clear_pointer(&ssd->mirror, pixman_image_unref);
+        g_clear_pointer(&ssd->buf, g_free);
+#ifdef HAVE_SPICE_GL
+        g_clear_pointer(&ssd->gl_unblock_bh, qemu_bh_delete);
+        g_clear_pointer(&ssd->gl_unblock_timer, timer_free);
+        g_clear_pointer(&ssd->gls, qemu_gl_fini_shader);
+        egl_fb_destroy(&ssd->guest_fb);
+        egl_fb_destroy(&ssd->blit_fb);
+        egl_fb_destroy(&ssd->cursor_fb);
+#endif
+        qemu_mutex_destroy(&ssd->lock);
+        g_free(ssd);
+    }
+    g_clear_pointer(&spice_displays, g_ptr_array_unref);
 }
 
 void qemu_spice_display_init(void)
@@ -1431,6 +1481,8 @@ void qemu_spice_display_init(void)
     const char *str;
     int i;
 
+    spice_displays = g_ptr_array_new();
+
     str = qemu_opt_get(opts, "display");
     if (str) {
         int head = qemu_opt_get_number(opts, "head", 0);
diff --git a/ui/spice-input.c b/ui/spice-input.c
index f0bb915fd77..c975c1e2516 100644
--- a/ui/spice-input.c
+++ b/ui/spice-input.c
@@ -239,23 +239,41 @@ static void mouse_mode_notifier(Notifier *notifier, void *data)
     pointer->absolute = is_absolute;
 }
 
+static QemuSpiceKbd *spice_kbd;
+static QemuSpicePointer *spice_pointer;
+static QEMUPutLEDEntry *spice_led;
+
 void qemu_spice_input_init(void)
 {
-    QemuSpiceKbd *kbd;
-    QemuSpicePointer *pointer;
-
-    kbd = g_malloc0(sizeof(*kbd));
-    kbd->sin.base.sif = &kbd_interface.base;
-    qemu_spice.add_interface(&kbd->sin.base);
-    qemu_add_led_event_handler(kbd_leds, kbd);
-
-    pointer = g_malloc0(sizeof(*pointer));
-    pointer->mouse.base.sif  = &mouse_interface.base;
-    pointer->tablet.base.sif = &tablet_interface.base;
-    qemu_spice.add_interface(&pointer->mouse.base);
-
-    pointer->absolute = false;
-    pointer->mouse_mode.notify = mouse_mode_notifier;
-    qemu_add_mouse_mode_change_notifier(&pointer->mouse_mode);
-    mouse_mode_notifier(&pointer->mouse_mode, NULL);
+    spice_kbd = g_new0(QemuSpiceKbd, 1);
+    spice_kbd->sin.base.sif = &kbd_interface.base;
+    qemu_spice.add_interface(&spice_kbd->sin.base);
+    spice_led = qemu_add_led_event_handler(kbd_leds, spice_kbd);
+
+    spice_pointer = g_new0(QemuSpicePointer, 1);
+    spice_pointer->mouse.base.sif  = &mouse_interface.base;
+    spice_pointer->tablet.base.sif = &tablet_interface.base;
+    qemu_spice.add_interface(&spice_pointer->mouse.base);
+
+    spice_pointer->absolute = false;
+    spice_pointer->mouse_mode.notify = mouse_mode_notifier;
+    qemu_add_mouse_mode_change_notifier(&spice_pointer->mouse_mode);
+    mouse_mode_notifier(&spice_pointer->mouse_mode, NULL);
+}
+
+void qemu_spice_input_cleanup(void)
+{
+    g_clear_pointer(&spice_led, qemu_remove_led_event_handler);
+    if (spice_pointer) {
+        qemu_remove_mouse_mode_change_notifier(&spice_pointer->mouse_mode);
+        if (spice_pointer->absolute) {
+            spice_server_remove_interface(&spice_pointer->tablet.base);
+        }
+        spice_server_remove_interface(&spice_pointer->mouse.base);
+        g_clear_pointer(&spice_pointer, g_free);
+    }
+    if (spice_kbd) {
+        spice_server_remove_interface(&spice_kbd->sin.base);
+        g_clear_pointer(&spice_kbd, g_free);
+    }
 }
diff --git a/ui/spice-module.c b/ui/spice-module.c
index 7651c85885f..1961060d128 100644
--- a/ui/spice-module.c
+++ b/ui/spice-module.c
@@ -62,6 +62,10 @@ static int qemu_spice_display_add_client_stub(int csock, int skipauth,
     return -1;
 }
 
+static void qemu_spice_cleanup_stub(void)
+{
+}
+
 struct QemuSpiceOps qemu_spice = {
     .init         = qemu_spice_init_stub,
     .display_init = qemu_spice_display_init_stub,
@@ -69,6 +73,7 @@ struct QemuSpiceOps qemu_spice = {
     .set_passwd   = qemu_spice_set_passwd_stub,
     .set_pw_expire = qemu_spice_set_pw_expire_stub,
     .display_add_client = qemu_spice_display_add_client_stub,
+    .cleanup = qemu_spice_cleanup_stub,
 };
 
 #ifdef CONFIG_SPICE
diff --git a/scripts/lsan_suppressions.txt b/scripts/lsan_suppressions.txt
index f3b827facff..2dd6581a650 100644
--- a/scripts/lsan_suppressions.txt
+++ b/scripts/lsan_suppressions.txt
@@ -20,3 +20,8 @@ leak:libfontconfig.so
 # https://github.com/GNOME/glib/blob/main/tools/glib.supp
 # This avoids false positive leak reports for the qga-ssh-test.
 leak:g_set_user_dirs
+
+# spice_server_add_interface allocates internal channel data that
+# spice_server_destroy does not free
+# https://gitlab.freedesktop.org/spice/spice/-/merge_requests/246
+leak:spice_server_add_interface
diff --git a/ui/meson.build b/ui/meson.build
index bb01f0728e2..ca903581abd 100644
--- a/ui/meson.build
+++ b/ui/meson.build
@@ -42,13 +42,14 @@ libui_sources = files(
     'kbd-state.c',
     'keymaps.c',
     'qemu-pixman.c',
+    'spice-module.c',
     'vgafont.c',
   )
 if pixman.found()
   libui_sources += files('cp437.c', 'vt100.c')
 endif
 libui = static_library('qemuui', libui_sources + genh,
-  dependencies: [pixman],
+  dependencies: [pixman, spice_headers],
   build_by_default: false)
 ui = declare_dependency(objects: libui.extract_all_objects(recursive: false), dependencies: [pixman])
 system_ss.add(png)
@@ -65,7 +66,6 @@ system_ss.add(when: pixman, if_true: files('console-vc.c'), if_false: files('con
 if dbus_display
   system_ss.add(files('dbus-module.c'))
 endif
-system_ss.add([spice_headers, files('spice-module.c')])
 system_ss.add(when: spice_protocol, if_true: files('vdagent.c'))
 
 if host_os == 'linux'

-- 
2.54.0



^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH 13/32] ui: add display cleanup infrastructure
  2026-05-29 11:16 [PATCH 00/32] ui: better console hotplug support Marc-André Lureau
                   ` (11 preceding siblings ...)
  2026-05-29 11:16 ` [PATCH 12/32] ui/spice: add cleanup on shutdown Marc-André Lureau
@ 2026-05-29 11:16 ` Marc-André Lureau
  2026-05-29 11:16 ` [PATCH 14/32] ui/curses: implement display cleanup Marc-André Lureau
                   ` (18 subsequent siblings)
  31 siblings, 0 replies; 39+ messages in thread
From: Marc-André Lureau @ 2026-05-29 11:16 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Fabiano Rosas,
	Laurent Vivier, Alex Williamson, Cédric Le Goater,
	Peter Maydell, Akihiko Odaki, Marc-André Lureau

Add a cleanup callback to QemuDisplay and a qemu_display_cleanup()
function that iterates all registered display types and calls their
cleanup handler. Wire it into qemu_cleanup() in runstate.c, replacing
the ad-hoc vnc_cleanup() call.

This provides a structured alternative to atexit() handlers, giving
deterministic teardown ordering and making resource leaks visible to
sanitizers.

The cleanup should happen before user_creatable_cleanup(), since some
display have weak user-creatable references to cleanup before.

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 include/ui/console.h |  2 ++
 system/runstate.c    |  8 +-------
 ui/console.c         | 20 +++++++++++++++++++-
 3 files changed, 22 insertions(+), 8 deletions(-)

diff --git a/include/ui/console.h b/include/ui/console.h
index a31c5a4995c..1c7f7e62c1a 100644
--- a/include/ui/console.h
+++ b/include/ui/console.h
@@ -430,6 +430,7 @@ struct QemuDisplay {
     DisplayType type;
     void (*early_init)(DisplayOptions *opts);
     void (*init)(DisplayState *ds, DisplayOptions *opts);
+    void (*cleanup)(void);
     const char *vc;
 };
 
@@ -438,6 +439,7 @@ bool qemu_display_find_default(DisplayOptions *opts);
 void qemu_display_early_init(DisplayOptions *opts);
 void qemu_display_init(DisplayState *ds, DisplayOptions *opts);
 const char *qemu_display_get_vc(DisplayOptions *opts);
+void qemu_display_cleanup(void);
 void qemu_display_help(void);
 
 /* vnc.c */
diff --git a/system/runstate.c b/system/runstate.c
index d35fa270bd6..18e585be47f 100644
--- a/system/runstate.c
+++ b/system/runstate.c
@@ -62,7 +62,6 @@
 #include "system/system.h"
 #include "system/tpm.h"
 #include "ui/console.h"
-#include "ui/qemu-spice-module.h"
 
 #include "trace.h"
 
@@ -1046,12 +1045,7 @@ void qemu_cleanup(int status)
     audio_cleanup();
     monitor_cleanup();
     qemu_chr_cleanup();
+    qemu_display_cleanup();
     user_creatable_cleanup();
-#ifdef CONFIG_VNC
-    vnc_cleanup();
-#endif
-#ifdef CONFIG_SPICE
-    qemu_spice.cleanup();
-#endif
     /* TODO: unref root container, check all devices are ok */
 }
diff --git a/ui/console.c b/ui/console.c
index 58f29e82c85..1fe3e3a3a6c 100644
--- a/ui/console.c
+++ b/ui/console.c
@@ -42,6 +42,7 @@
 #include "qemu/memfd.h"
 #include "ui/vt100.h"
 #include "vgafont.h"
+#include "ui/qemu-spice.h"
 
 #include "console-priv.h"
 
@@ -575,7 +576,7 @@ void qemu_console_set_display_gl_ctx(QemuConsole *con, DisplayGLCtx *gl)
 {
     /* display has opengl support */
     assert(con);
-    if (con->gl) {
+    if (gl && con->gl) {
         error_report("The console already has an OpenGL context.");
         exit(1);
     }
@@ -1414,6 +1415,23 @@ void qemu_display_init(DisplayState *ds, DisplayOptions *opts)
     dpys[opts->type]->init(ds, opts);
 }
 
+void qemu_display_cleanup(void)
+{
+    int i;
+
+    for (i = 0; i < DISPLAY_TYPE__MAX; i++) {
+        if (dpys[i] && dpys[i]->cleanup) {
+            dpys[i]->cleanup();
+        }
+    }
+#ifdef CONFIG_VNC
+    vnc_cleanup();
+#endif
+#ifdef CONFIG_SPICE
+    qemu_spice.cleanup();
+#endif
+}
+
 const char *qemu_display_get_vc(DisplayOptions *opts)
 {
 #ifdef CONFIG_PIXMAN

-- 
2.54.0



^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH 14/32] ui/curses: implement display cleanup
  2026-05-29 11:16 [PATCH 00/32] ui: better console hotplug support Marc-André Lureau
                   ` (12 preceding siblings ...)
  2026-05-29 11:16 ` [PATCH 13/32] ui: add display cleanup infrastructure Marc-André Lureau
@ 2026-05-29 11:16 ` Marc-André Lureau
  2026-05-29 11:16 ` [PATCH 15/32] ui/sdl2: " Marc-André Lureau
                   ` (17 subsequent siblings)
  31 siblings, 0 replies; 39+ messages in thread
From: Marc-André Lureau @ 2026-05-29 11:16 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Fabiano Rosas,
	Laurent Vivier, Alex Williamson, Cédric Le Goater,
	Peter Maydell, Akihiko Odaki, Marc-André Lureau

Replace the atexit() handler with a proper cleanup callback. The new
curses_cleanup() unregisters the display listener, destroy & free the
allocated resources.

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 ui/curses.c | 17 ++++++++++++-----
 1 file changed, 12 insertions(+), 5 deletions(-)

diff --git a/ui/curses.c b/ui/curses.c
index 24d3713e57d..4db32b6168d 100644
--- a/ui/curses.c
+++ b/ui/curses.c
@@ -411,11 +411,19 @@ static void curses_refresh(DisplayChangeListener *dcl)
     }
 }
 
-static void curses_atexit(void)
+static void curses_cleanup(void)
 {
+    if (!dcl) {
+        return;
+    }
+
     endwin();
-    g_free(vga_to_curses);
-    g_free(screen);
+    qemu_console_unregister_listener(dcl);
+    g_clear_pointer(&dcl, g_free);
+    g_clear_pointer(&screenpad, delwin);
+    g_clear_pointer(&vga_to_curses, g_free);
+    g_clear_pointer(&screen, g_free);
+    g_clear_pointer(&kbd_layout, kbd_layout_free);
 }
 
 /*
@@ -799,8 +807,6 @@ static void curses_display_init(DisplayState *ds, DisplayOptions *opts)
     vga_to_curses = g_new0(cchar_t, 256);
     curses_setup();
     curses_keyboard_setup();
-    atexit(curses_atexit);
-
     curses_winch_init();
 
     dcl = g_new0(DisplayChangeListener, 1);
@@ -812,6 +818,7 @@ static void curses_display_init(DisplayState *ds, DisplayOptions *opts)
 static QemuDisplay qemu_display_curses = {
     .type       = DISPLAY_TYPE_CURSES,
     .init       = curses_display_init,
+    .cleanup    = curses_cleanup,
 };
 
 static void register_curses(void)

-- 
2.54.0



^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH 15/32] ui/sdl2: implement display cleanup
  2026-05-29 11:16 [PATCH 00/32] ui: better console hotplug support Marc-André Lureau
                   ` (13 preceding siblings ...)
  2026-05-29 11:16 ` [PATCH 14/32] ui/curses: implement display cleanup Marc-André Lureau
@ 2026-05-29 11:16 ` Marc-André Lureau
  2026-05-29 11:16 ` [PATCH 16/32] ui/spice-app: " Marc-André Lureau
                   ` (16 subsequent siblings)
  31 siblings, 0 replies; 39+ messages in thread
From: Marc-André Lureau @ 2026-05-29 11:16 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Fabiano Rosas,
	Laurent Vivier, Alex Williamson, Cédric Le Goater,
	Peter Maydell, Akihiko Odaki, Marc-André Lureau

Replace the atexit() handler with a proper cleanup callback. Extend
sdl_cleanup() to unregister display listeners, free keyboard state,
destroy windows, and clean up all cursor resources.

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 ui/sdl2.c | 23 +++++++++++++++++++----
 1 file changed, 19 insertions(+), 4 deletions(-)

diff --git a/ui/sdl2.c b/ui/sdl2.c
index 4fcdbd79d3c..6adcbdb5af9 100644
--- a/ui/sdl2.c
+++ b/ui/sdl2.c
@@ -788,9 +788,25 @@ static void sdl_mouse_define(DisplayChangeListener *dcl,
 
 static void sdl_cleanup(void)
 {
-    if (guest_sprite) {
-        SDL_FreeCursor(guest_sprite);
+    int i;
+
+    if (!sdl2_console) {
+        return;
     }
+
+    qemu_remove_mouse_mode_change_notifier(&mouse_mode_notifier);
+
+    for (i = 0; i < sdl2_num_outputs; i++) {
+        qemu_console_unregister_listener(&sdl2_console[i].dcl);
+        qkbd_state_free(sdl2_console[i].kbd);
+        sdl2_window_destroy(&sdl2_console[i]);
+    }
+    g_clear_pointer(&sdl2_console, g_free);
+    sdl2_num_outputs = 0;
+
+    g_clear_pointer(&guest_sprite, SDL_FreeCursor);
+    g_clear_pointer(&guest_sprite_surface, SDL_FreeSurface);
+    g_clear_pointer(&sdl_cursor_hidden, SDL_FreeCursor);
     SDL_QuitSubSystem(SDL_INIT_VIDEO);
 }
 
@@ -995,8 +1011,6 @@ static void sdl2_display_init(DisplayState *ds, DisplayOptions *o)
         sdl_grab_start(&sdl2_console[0]);
     }
 
-    atexit(sdl_cleanup);
-
     /* SDL's event polling (in dpy_refresh) must happen on the main thread. */
     qemu_main = NULL;
 }
@@ -1005,6 +1019,7 @@ static QemuDisplay qemu_display_sdl2 = {
     .type       = DISPLAY_TYPE_SDL,
     .early_init = sdl2_display_early_init,
     .init       = sdl2_display_init,
+    .cleanup    = sdl_cleanup,
 };
 
 static void register_sdl1(void)

-- 
2.54.0



^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH 16/32] ui/spice-app: implement display cleanup
  2026-05-29 11:16 [PATCH 00/32] ui: better console hotplug support Marc-André Lureau
                   ` (14 preceding siblings ...)
  2026-05-29 11:16 ` [PATCH 15/32] ui/sdl2: " Marc-André Lureau
@ 2026-05-29 11:16 ` Marc-André Lureau
  2026-05-29 11:16 ` [PATCH 17/32] ui/egl: implement display and EGL cleanup Marc-André Lureau
                   ` (15 subsequent siblings)
  31 siblings, 0 replies; 39+ messages in thread
From: Marc-André Lureau @ 2026-05-29 11:16 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Fabiano Rosas,
	Laurent Vivier, Alex Williamson, Cédric Le Goater,
	Peter Maydell, Akihiko Odaki, Marc-André Lureau

Replace the atexit() handler with the display cleanup callback,
reusing the existing spice_app_atexit() function.

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 ui/spice-app.c | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/ui/spice-app.c b/ui/spice-app.c
index 0df7325e679..fe3df62bfa5 100644
--- a/ui/spice-app.c
+++ b/ui/spice-app.c
@@ -119,16 +119,17 @@ static const TypeInfo char_vc_type_info = {
     .class_size = sizeof(VCChardevClass),
 };
 
-static void spice_app_atexit(void)
+static void spice_app_cleanup(void)
 {
     if (sock_path) {
         unlink(sock_path);
+        g_clear_pointer(&sock_path, g_free);
     }
     if (tmp_dir) {
         rmdir(tmp_dir);
+        tmp_dir = NULL;
     }
-    g_free(sock_path);
-    g_free(app_dir);
+    g_clear_pointer(&app_dir, g_free);
 }
 
 static void spice_app_display_early_init(DisplayOptions *opts)
@@ -146,8 +147,6 @@ static void spice_app_display_early_init(DisplayOptions *opts)
         exit(1);
     }
 
-    atexit(spice_app_atexit);
-
     if (qemu_name) {
         app_dir = g_build_filename(g_get_user_runtime_dir(),
                                    "qemu", qemu_name, NULL);
@@ -218,6 +217,7 @@ static QemuDisplay qemu_display_spice_app = {
     .type       = DISPLAY_TYPE_SPICE_APP,
     .early_init = spice_app_display_early_init,
     .init       = spice_app_display_init,
+    .cleanup    = spice_app_cleanup,
     .vc         = "vc",
 };
 

-- 
2.54.0



^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH 17/32] ui/egl: implement display and EGL cleanup
  2026-05-29 11:16 [PATCH 00/32] ui: better console hotplug support Marc-André Lureau
                   ` (15 preceding siblings ...)
  2026-05-29 11:16 ` [PATCH 16/32] ui/spice-app: " Marc-André Lureau
@ 2026-05-29 11:16 ` Marc-André Lureau
  2026-05-29 11:16 ` [PATCH 18/32] ui/cocoa: implement display cleanup Marc-André Lureau
                   ` (14 subsequent siblings)
  31 siblings, 0 replies; 39+ messages in thread
From: Marc-André Lureau @ 2026-05-29 11:16 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Fabiano Rosas,
	Laurent Vivier, Alex Williamson, Cédric Le Goater,
	Peter Maydell, Akihiko Odaki, Marc-André Lureau

Add egl_cleanup() to tear down the EGL render node context, GBM device,
and EGL display. Add egl_headless_cleanup() to unregister listeners,
destroy framebuffers, and free per-console state tracked via a new
GPtrArray.

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 include/ui/egl-helpers.h |  1 +
 ui/egl-headless.c        | 31 +++++++++++++++++++++++++++++++
 ui/egl-helpers.c         | 19 +++++++++++++++++++
 3 files changed, 51 insertions(+)

diff --git a/include/ui/egl-helpers.h b/include/ui/egl-helpers.h
index acf993fcf52..dba788fead7 100644
--- a/include/ui/egl-helpers.h
+++ b/include/ui/egl-helpers.h
@@ -76,6 +76,7 @@ EGLContext qemu_egl_init_ctx(void);
 bool qemu_egl_has_dmabuf(void);
 
 bool egl_init(const char *rendernode, DisplayGLMode mode, Error **errp);
+void egl_cleanup(void);
 
 const char *qemu_egl_get_error_string(void);
 
diff --git a/ui/egl-headless.c b/ui/egl-headless.c
index 878bfebb40c..ba27efcf4c3 100644
--- a/ui/egl-headless.c
+++ b/ui/egl-headless.c
@@ -9,6 +9,7 @@
 
 typedef struct egl_dpy {
     DisplayChangeListener dcl;
+    DisplayGLCtx *ctx;
     DisplaySurface *ds;
     QemuGLShader *gls;
     egl_fb guest_fb;
@@ -19,6 +20,8 @@ typedef struct egl_dpy {
     uint32_t pos_y;
 } egl_dpy;
 
+static GPtrArray *egl_dpys;
+
 /* ------------------------------------------------------------------ */
 
 static void egl_refresh(DisplayChangeListener *dcl)
@@ -220,6 +223,8 @@ static void egl_headless_init(DisplayState *ds, DisplayOptions *opts)
     egl_dpy *edpy;
     int idx;
 
+    egl_dpys = g_ptr_array_new();
+
     for (idx = 0;; idx++) {
         DisplayGLCtx *ctx;
 
@@ -232,15 +237,41 @@ static void egl_headless_init(DisplayState *ds, DisplayOptions *opts)
         edpy->gls = qemu_gl_init_shader();
         ctx = g_new0(DisplayGLCtx, 1);
         ctx->ops = &eglctx_ops;
+        edpy->ctx = ctx;
         qemu_console_set_display_gl_ctx(con, ctx);
         qemu_console_register_listener(con, &edpy->dcl, &egl_ops);
+        g_ptr_array_add(egl_dpys, edpy);
+    }
+}
+
+static void egl_headless_cleanup(void)
+{
+    if (!egl_dpys) {
+        return;
     }
+
+    for (guint i = 0; i < egl_dpys->len; i++) {
+        egl_dpy *edpy = g_ptr_array_index(egl_dpys, i);
+
+        qemu_console_unregister_listener(&edpy->dcl);
+        qemu_console_set_display_gl_ctx(edpy->dcl.con, NULL);
+        egl_fb_destroy(&edpy->guest_fb);
+        egl_fb_destroy(&edpy->cursor_fb);
+        egl_fb_destroy(&edpy->blit_fb);
+        qemu_gl_fini_shader(edpy->gls);
+        g_free(edpy->ctx);
+        g_free(edpy);
+    }
+    g_clear_pointer(&egl_dpys, g_ptr_array_unref);
+
+    egl_cleanup();
 }
 
 static QemuDisplay qemu_display_egl = {
     .type       = DISPLAY_TYPE_EGL_HEADLESS,
     .early_init = early_egl_headless_init,
     .init       = egl_headless_init,
+    .cleanup    = egl_headless_cleanup,
 };
 
 static void register_egl(void)
diff --git a/ui/egl-helpers.c b/ui/egl-helpers.c
index e3f2872cc14..a7bfb15fd88 100644
--- a/ui/egl-helpers.c
+++ b/ui/egl-helpers.c
@@ -733,3 +733,22 @@ bool egl_init(const char *rendernode, DisplayGLMode mode, Error **errp)
     display_opengl = 1;
     return true;
 }
+
+void egl_cleanup(void)
+{
+    if (qemu_egl_rn_ctx) {
+        eglDestroyContext(qemu_egl_display, qemu_egl_rn_ctx);
+        qemu_egl_rn_ctx = NULL;
+    }
+
+#ifdef CONFIG_GBM
+    g_clear_pointer(&qemu_egl_rn_gbm_dev, gbm_device_destroy);
+    g_clear_fd(&qemu_egl_rn_fd, NULL);
+#endif
+
+    if (qemu_egl_display) {
+        eglReleaseThread();
+        eglTerminate(qemu_egl_display);
+        qemu_egl_display = NULL;
+    }
+}

-- 
2.54.0



^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH 18/32] ui/cocoa: implement display cleanup
  2026-05-29 11:16 [PATCH 00/32] ui: better console hotplug support Marc-André Lureau
                   ` (16 preceding siblings ...)
  2026-05-29 11:16 ` [PATCH 17/32] ui/egl: implement display and EGL cleanup Marc-André Lureau
@ 2026-05-29 11:16 ` Marc-André Lureau
  2026-05-29 11:16 ` [PATCH 19/32] ui/dbus: " Marc-André Lureau
                   ` (13 subsequent siblings)
  31 siblings, 0 replies; 39+ messages in thread
From: Marc-André Lureau @ 2026-05-29 11:16 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Fabiano Rosas,
	Laurent Vivier, Alex Williamson, Cédric Le Goater,
	Peter Maydell, Akihiko Odaki, Marc-André Lureau

Move cbowner release from QemuCocoaAppController -dealloc to
cocoa_display_cleanup(), since cbowner is allocated in
cocoa_display_init() and cleanup is the symmetric teardown path.

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 ui/cocoa.m | 19 +++++++++++++++++--
 1 file changed, 17 insertions(+), 2 deletions(-)

diff --git a/ui/cocoa.m b/ui/cocoa.m
index c5e639ab98d..bd0c5c6ed9e 100644
--- a/ui/cocoa.m
+++ b/ui/cocoa.m
@@ -1331,8 +1331,6 @@ - (void) dealloc
     COCOA_DEBUG("QemuCocoaAppController: dealloc\n");
 
     [cocoaView release];
-    [cbowner release];
-    cbowner = nil;
 
     [super dealloc];
 }
@@ -2163,9 +2161,26 @@ static void cocoa_display_init(DisplayState *ds, DisplayOptions *opts)
     qemu_main = cocoa_main;
 }
 
+static void cocoa_display_cleanup(void)
+{
+    if (!kbd) {
+        return;
+    }
+
+    qemu_console_unregister_listener(&dcl);
+    g_clear_pointer(&kbd, qkbd_state_free);
+    qemu_remove_mouse_mode_change_notifier(&mouse_mode_change_notifier);
+    qemu_clipboard_peer_unregister(&cbpeer);
+    g_clear_pointer(&cbinfo, qemu_clipboard_info_unref);
+    qemu_event_destroy(&cbevent);
+    [cbowner release];
+    cbowner = nil;
+}
+
 static QemuDisplay qemu_display_cocoa = {
     .type       = DISPLAY_TYPE_COCOA,
     .init       = cocoa_display_init,
+    .cleanup    = cocoa_display_cleanup,
 };
 
 static void register_cocoa(void)

-- 
2.54.0



^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH 19/32] ui/dbus: implement display cleanup
  2026-05-29 11:16 [PATCH 00/32] ui: better console hotplug support Marc-André Lureau
                   ` (17 preceding siblings ...)
  2026-05-29 11:16 ` [PATCH 18/32] ui/cocoa: implement display cleanup Marc-André Lureau
@ 2026-05-29 11:16 ` Marc-André Lureau
  2026-05-29 12:17   ` Akihiko Odaki
  2026-05-29 11:16 ` [PATCH 20/32] ui/gtk: " Marc-André Lureau
                   ` (12 subsequent siblings)
  31 siblings, 1 reply; 39+ messages in thread
From: Marc-André Lureau @ 2026-05-29 11:16 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Fabiano Rosas,
	Laurent Vivier, Alex Williamson, Cédric Le Goater,
	Peter Maydell, Akihiko Odaki, Marc-André Lureau

Add dbus_cleanup() to unparent the D-Bus display object, ensuring
proper teardown before user_creatable_cleanup() runs.

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 ui/dbus-console.c |  3 +++
 ui/dbus.c         | 13 +++++++++++++
 2 files changed, 16 insertions(+)

diff --git a/ui/dbus-console.c b/ui/dbus-console.c
index 0813a08f85e..0048951a7ab 100644
--- a/ui/dbus-console.c
+++ b/ui/dbus-console.c
@@ -151,6 +151,9 @@ dbus_display_console_dispose(GObject *object)
     DBusDisplayConsole *ddc = DBUS_DISPLAY_CONSOLE(object);
 
     qemu_console_unregister_listener(&ddc->dcl);
+    if (ddc->dcl.con) {
+        qemu_console_set_display_gl_ctx(ddc->dcl.con, NULL);
+    }
     qemu_remove_mouse_mode_change_notifier(&ddc->mouse_mode_notifier);
     g_clear_object(&ddc->iface_touch);
     g_clear_object(&ddc->iface_mouse);
diff --git a/ui/dbus.c b/ui/dbus.c
index e02a94df2f3..b23cb44c535 100644
--- a/ui/dbus.c
+++ b/ui/dbus.c
@@ -615,10 +615,23 @@ static const TypeInfo dbus_display_info = {
     }
 };
 
+static void
+dbus_cleanup(void)
+{
+    Object *o;
+
+    o = object_resolve_path_component(object_get_objects_root(),
+                                      "dbus-display");
+    if (o) {
+        object_unparent(o);
+    }
+}
+
 static QemuDisplay qemu_display_dbus = {
     .type       = DISPLAY_TYPE_DBUS,
     .early_init = early_dbus_init,
     .init       = dbus_init,
+    .cleanup    = dbus_cleanup,
     .vc         = "vc",
 };
 

-- 
2.54.0



^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH 20/32] ui/gtk: implement display cleanup
  2026-05-29 11:16 [PATCH 00/32] ui: better console hotplug support Marc-André Lureau
                   ` (18 preceding siblings ...)
  2026-05-29 11:16 ` [PATCH 19/32] ui/dbus: " Marc-André Lureau
@ 2026-05-29 11:16 ` Marc-André Lureau
  2026-05-29 11:16 ` [PATCH 21/32] ui/console: add console event notifier infrastructure Marc-André Lureau
                   ` (11 subsequent siblings)
  31 siblings, 0 replies; 39+ messages in thread
From: Marc-André Lureau @ 2026-05-29 11:16 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Fabiano Rosas,
	Laurent Vivier, Alex Williamson, Cédric Le Goater,
	Peter Maydell, Akihiko Odaki, Marc-André Lureau

Add gtk_display_cleanup() to properly tear down GTK display state:
remove console and mouse notifiers, unregister clipboard peer,
destroy the main window and virtual consoles.

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 include/ui/gtk.h   |  1 +
 ui/gtk-clipboard.c | 15 +++++++++++++++
 ui/gtk.c           | 18 ++++++++++++++++++
 3 files changed, 34 insertions(+)

diff --git a/include/ui/gtk.h b/include/ui/gtk.h
index 3e6ce3cb48c..5156a049509 100644
--- a/include/ui/gtk.h
+++ b/include/ui/gtk.h
@@ -225,6 +225,7 @@ int gd_gl_area_make_current(DisplayGLCtx *dgc,
 
 /* gtk-clipboard.c */
 void gd_clipboard_init(GtkDisplayState *gd);
+void gd_clipboard_cleanup(GtkDisplayState *gd);
 
 void gd_update_scale(VirtualConsole *vc, int ww, int wh, int fbw, int fbh);
 
diff --git a/ui/gtk-clipboard.c b/ui/gtk-clipboard.c
index ea9444be70f..476e6b8303c 100644
--- a/ui/gtk-clipboard.c
+++ b/ui/gtk-clipboard.c
@@ -235,3 +235,18 @@ void gd_clipboard_init(GtkDisplayState *gd)
     g_signal_connect(gd->gtkcb[QEMU_CLIPBOARD_SELECTION_SECONDARY],
                      "owner-change", G_CALLBACK(gd_owner_change), gd);
 }
+
+void gd_clipboard_cleanup(GtkDisplayState *gd)
+{
+    if (!gd->cbpeer.name) {
+        return;
+    }
+    qemu_clipboard_peer_unregister(&gd->cbpeer);
+    g_signal_handlers_disconnect_by_data(
+        gd->gtkcb[QEMU_CLIPBOARD_SELECTION_CLIPBOARD], gd);
+    g_signal_handlers_disconnect_by_data(
+        gd->gtkcb[QEMU_CLIPBOARD_SELECTION_PRIMARY], gd);
+    g_signal_handlers_disconnect_by_data(
+        gd->gtkcb[QEMU_CLIPBOARD_SELECTION_SECONDARY], gd);
+    gd->cbpeer.name = NULL;
+}
diff --git a/ui/gtk.c b/ui/gtk.c
index 2ee826b56fb..c26dc83ac3e 100644
--- a/ui/gtk.c
+++ b/ui/gtk.c
@@ -2505,6 +2505,7 @@ static void gd_create_menus(GtkDisplayState *s, DisplayOptions *opts)
 }
 
 
+static GtkDisplayState *gtk_display_state;
 static gboolean gtkinit;
 
 static void gtk_display_init(DisplayState *ds, DisplayOptions *opts)
@@ -2523,6 +2524,7 @@ static void gtk_display_init(DisplayState *ds, DisplayOptions *opts)
     }
     assert(opts->type == DISPLAY_TYPE_GTK);
     s = g_malloc0(sizeof(*s));
+    gtk_display_state = s;
     s->opts = opts;
 
     theme = gtk_icon_theme_get_default();
@@ -2681,10 +2683,26 @@ static void early_gtk_display_init(DisplayOptions *opts)
 #endif
 }
 
+static void gtk_display_cleanup(void)
+{
+    GtkDisplayState *s = gtk_display_state;
+
+    if (!s) {
+        return;
+    }
+
+    qemu_remove_mouse_mode_change_notifier(&s->mouse_mode_notifier);
+    gd_clipboard_cleanup(s);
+    g_clear_pointer(&s->window, gtk_widget_destroy);
+    g_clear_object(&s->null_cursor);
+    g_clear_pointer(&gtk_display_state, g_free);
+}
+
 static QemuDisplay qemu_display_gtk = {
     .type       = DISPLAY_TYPE_GTK,
     .early_init = early_gtk_display_init,
     .init       = gtk_display_init,
+    .cleanup    = gtk_display_cleanup,
     .vc         = "vc",
 };
 

-- 
2.54.0



^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH 21/32] ui/console: add console event notifier infrastructure
  2026-05-29 11:16 [PATCH 00/32] ui: better console hotplug support Marc-André Lureau
                   ` (19 preceding siblings ...)
  2026-05-29 11:16 ` [PATCH 20/32] ui/gtk: " Marc-André Lureau
@ 2026-05-29 11:16 ` Marc-André Lureau
  2026-05-29 11:16 ` [PATCH 22/32] ui/console: fire console ADDED/REMOVED notifications Marc-André Lureau
                   ` (10 subsequent siblings)
  31 siblings, 0 replies; 39+ messages in thread
From: Marc-André Lureau @ 2026-05-29 11:16 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Fabiano Rosas,
	Laurent Vivier, Alex Williamson, Cédric Le Goater,
	Peter Maydell, Akihiko Odaki, Marc-André Lureau

Add a NotifierList to DisplayState so display backends can be notified
when consoles are added or removed at runtime.

No events are fired yet, that follows in the next commits.

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 include/ui/console.h | 18 ++++++++++++++++++
 ui/console.c         | 23 +++++++++++++++++++++++
 2 files changed, 41 insertions(+)

diff --git a/include/ui/console.h b/include/ui/console.h
index 1c7f7e62c1a..44ebc578651 100644
--- a/include/ui/console.h
+++ b/include/ui/console.h
@@ -442,6 +442,24 @@ const char *qemu_display_get_vc(DisplayOptions *opts);
 void qemu_display_cleanup(void);
 void qemu_display_help(void);
 
+/*
+ * Console notifications:
+ * Handlers must be idempotent, it may notify multiple times.
+ */
+typedef enum {
+    QEMU_CONSOLE_ADDED,
+    QEMU_CONSOLE_REMOVED,
+} QemuConsoleEventType;
+
+typedef struct QemuConsoleEvent {
+    QemuConsoleEventType type;
+    QemuConsole *con;
+} QemuConsoleEvent;
+
+void qemu_console_add_notifier(Notifier *notifier);
+void qemu_console_remove_notifier(Notifier *notifier);
+void qemu_console_notify(QemuConsoleEventType type, QemuConsole *con);
+
 /* vnc.c */
 void vnc_display_add_client(const char *id, int csock, bool skipauth);
 int vnc_display_password(const char *id, const char *password, Error **errp);
diff --git a/ui/console.c b/ui/console.c
index 1fe3e3a3a6c..76851bc129d 100644
--- a/ui/console.c
+++ b/ui/console.c
@@ -70,6 +70,7 @@ struct DisplayState {
     bool refreshing;
 
     QLIST_HEAD(, DisplayChangeListener) listeners;
+    NotifierList console_notifiers;
 };
 
 static DisplayState *display_state;
@@ -84,6 +85,16 @@ static bool console_compatible_with(QemuConsole *con,
 static QemuConsole *qemu_graphic_console_lookup_unused(void);
 static void dpy_set_ui_info_timer(void *opaque);
 
+void qemu_console_notify(QemuConsoleEventType type, QemuConsole *con)
+{
+    DisplayState *ds = get_alloc_displaystate();
+    QemuConsoleEvent event = {
+        .type = type,
+        .con = con,
+    };
+    notifier_list_notify(&ds->console_notifiers, &event);
+}
+
 static void gui_update(void *opaque)
 {
     uint64_t interval = GUI_REFRESH_INTERVAL_IDLE;
@@ -1056,10 +1067,22 @@ static DisplayState *get_alloc_displaystate(void)
 {
     if (!display_state) {
         display_state = g_new0(DisplayState, 1);
+        notifier_list_init(&display_state->console_notifiers);
     }
     return display_state;
 }
 
+void qemu_console_add_notifier(Notifier *notifier)
+{
+    DisplayState *ds = get_alloc_displaystate();
+    notifier_list_add(&ds->console_notifiers, notifier);
+}
+
+void qemu_console_remove_notifier(Notifier *notifier)
+{
+    notifier_remove(notifier);
+}
+
 /*
  * Called by main(), after creating QemuConsoles
  * and before initializing ui (sdl/vnc/...).

-- 
2.54.0



^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH 22/32] ui/console: fire console ADDED/REMOVED notifications
  2026-05-29 11:16 [PATCH 00/32] ui: better console hotplug support Marc-André Lureau
                   ` (20 preceding siblings ...)
  2026-05-29 11:16 ` [PATCH 21/32] ui/console: add console event notifier infrastructure Marc-André Lureau
@ 2026-05-29 11:16 ` Marc-André Lureau
  2026-05-29 11:16 ` [PATCH 23/32] ui/console-vc: fire " Marc-André Lureau
                   ` (9 subsequent siblings)
  31 siblings, 0 replies; 39+ messages in thread
From: Marc-André Lureau @ 2026-05-29 11:16 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Fabiano Rosas,
	Laurent Vivier, Alex Williamson, Cédric Le Goater,
	Peter Maydell, Akihiko Odaki, Marc-André Lureau

Fire CONSOLE_ADDED at the end of graphic_console_init() and
CONSOLE_REMOVED at the start of graphic_console_close(), so display
backends can react to console hotplug/unplug events.

REMOVED fires before the device link is cleared and before the
placeholder surface swap, so handlers can unregister their DCL while
the console is still in a known state.

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 ui/console.c | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/ui/console.c b/ui/console.c
index 76851bc129d..975eaf15706 100644
--- a/ui/console.c
+++ b/ui/console.c
@@ -1142,6 +1142,7 @@ QemuConsole *qemu_graphic_console_create(DeviceState *dev, uint32_t head,
 
     surface = qemu_create_placeholder_surface(width, height, noinit);
     qemu_console_set_surface(s, surface);
+    qemu_console_notify(QEMU_CONSOLE_ADDED, s);
     return s;
 }
 
@@ -1158,6 +1159,7 @@ void qemu_graphic_console_close(QemuConsole *con)
     int height = qemu_console_get_height(con, 480);
 
     trace_console_gfx_close(con->index);
+    qemu_console_notify(QEMU_CONSOLE_REMOVED, con);
     object_property_set_link(OBJECT(con), "device", NULL, &error_abort);
     qemu_graphic_console_set_hwops(con, &unused_ops, NULL);
     timer_del(con->ui_timer);

-- 
2.54.0



^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH 23/32] ui/console-vc: fire ADDED/REMOVED notifications
  2026-05-29 11:16 [PATCH 00/32] ui: better console hotplug support Marc-André Lureau
                   ` (21 preceding siblings ...)
  2026-05-29 11:16 ` [PATCH 22/32] ui/console: fire console ADDED/REMOVED notifications Marc-André Lureau
@ 2026-05-29 11:16 ` Marc-André Lureau
  2026-05-29 11:16 ` [PATCH 24/32] ui/gtk: convert VirtualConsole storage from fixed array to GPtrArray Marc-André Lureau
                   ` (8 subsequent siblings)
  31 siblings, 0 replies; 39+ messages in thread
From: Marc-André Lureau @ 2026-05-29 11:16 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Fabiano Rosas,
	Laurent Vivier, Alex Williamson, Cédric Le Goater,
	Peter Maydell, Akihiko Odaki, Marc-André Lureau

Fire CONSOLE_ADDED when the chardev is opened.

Fire CONSOLE_REMOVED in char_vc_finalize() before dropping the console
reference, so the console is still in a valid state when listeners
handle the event. Also fixes a console object leak by adding the
missing object_unref().

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 ui/console-vc.c | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/ui/console-vc.c b/ui/console-vc.c
index 828e78c41ea..53d9e9d39b3 100644
--- a/ui/console-vc.c
+++ b/ui/console-vc.c
@@ -255,6 +255,7 @@ static bool vc_chr_open(Chardev *chr, ChardevBackend *backend, Error **errp)
     }
 
     qemu_chr_be_event(chr, CHR_EVENT_OPENED);
+    qemu_console_notify(QEMU_CONSOLE_ADDED, QEMU_CONSOLE(s));
     return true;
 }
 
@@ -327,12 +328,24 @@ static void char_vc_init(Object *obj)
     vc->encoding = CHARDEV_VC_ENCODING_UTF8;
 }
 
+static void char_vc_finalize(Object *obj)
+{
+    VCChardev *vc = VC_CHARDEV(obj);
+    QemuConsole *con = QEMU_CONSOLE(vc->console);
+
+    if (con) {
+        qemu_console_notify(QEMU_CONSOLE_REMOVED, con);
+        object_unref(con);
+    }
+}
+
 static const TypeInfo char_vc_type_info = {
     .name = TYPE_CHARDEV_VC,
     .parent = TYPE_CHARDEV,
     .instance_size = sizeof(VCChardev),
     .instance_init = char_vc_init,
     .instance_post_init = object_apply_compat_props,
+    .instance_finalize = char_vc_finalize,
     .class_init = char_vc_class_init,
 };
 

-- 
2.54.0



^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH 24/32] ui/gtk: convert VirtualConsole storage from fixed array to GPtrArray
  2026-05-29 11:16 [PATCH 00/32] ui: better console hotplug support Marc-André Lureau
                   ` (22 preceding siblings ...)
  2026-05-29 11:16 ` [PATCH 23/32] ui/console-vc: fire " Marc-André Lureau
@ 2026-05-29 11:16 ` Marc-André Lureau
  2026-05-29 12:21   ` Akihiko Odaki
  2026-05-29 11:16 ` [PATCH 25/32] ui/gtk: move global display settings out of per-console init Marc-André Lureau
                   ` (7 subsequent siblings)
  31 siblings, 1 reply; 39+ messages in thread
From: Marc-André Lureau @ 2026-05-29 11:16 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Fabiano Rosas,
	Laurent Vivier, Alex Williamson, Cédric Le Goater,
	Peter Maydell, Akihiko Odaki, Marc-André Lureau

Replace the fixed-size vc[MAX_VCS] with GPtrArray.

This is a preparatory refactoring for console hotplug support, which
needs to add/remove VCs dynamically.

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 include/ui/gtk.h |   3 +-
 ui/gtk.c         | 102 +++++++++++++++++++++++++++++++++++++++++++------------
 2 files changed, 81 insertions(+), 24 deletions(-)

diff --git a/include/ui/gtk.h b/include/ui/gtk.h
index 5156a049509..4d54de97ea7 100644
--- a/include/ui/gtk.h
+++ b/include/ui/gtk.h
@@ -118,8 +118,7 @@ struct GtkDisplayState {
     GtkWidget *grab_item;
     GtkWidget *grab_on_hover_item;
 
-    int nb_vcs;
-    VirtualConsole vc[MAX_VCS];
+    GPtrArray *vcs;
 
     GtkWidget *show_tabs_item;
     GtkWidget *untabify_item;
diff --git a/ui/gtk.c b/ui/gtk.c
index c26dc83ac3e..141cb69d494 100644
--- a/ui/gtk.c
+++ b/ui/gtk.c
@@ -150,8 +150,8 @@ static VirtualConsole *gd_vc_find_by_menu(GtkDisplayState *s)
     VirtualConsole *vc;
     gint i;
 
-    for (i = 0; i < s->nb_vcs; i++) {
-        vc = &s->vc[i];
+    for (i = 0; i < s->vcs->len; i++) {
+        vc = g_ptr_array_index(s->vcs, i);
         if (gtk_check_menu_item_get_active
             (GTK_CHECK_MENU_ITEM(vc->menu_item))) {
             return vc;
@@ -165,8 +165,11 @@ static VirtualConsole *gd_vc_find_by_page(GtkDisplayState *s, gint page)
     VirtualConsole *vc;
     gint i, p;
 
-    for (i = 0; i < s->nb_vcs; i++) {
-        vc = &s->vc[i];
+    if (!s->vcs) {
+        return NULL;
+    }
+    for (i = 0; i < s->vcs->len; i++) {
+        vc = g_ptr_array_index(s->vcs, i);
         p = gtk_notebook_page_num(GTK_NOTEBOOK(s->notebook), vc->tab_item);
         if (p == page) {
             return vc;
@@ -247,8 +250,8 @@ static void gd_update_caption(GtkDisplayState *s)
     gtk_window_set_title(GTK_WINDOW(s->window), title);
     g_free(title);
 
-    for (i = 0; i < s->nb_vcs; i++) {
-        VirtualConsole *vc = &s->vc[i];
+    for (i = 0; i < s->vcs->len; i++) {
+        VirtualConsole *vc = g_ptr_array_index(s->vcs, i);
 
         if (!vc->window) {
             continue;
@@ -357,7 +360,7 @@ static void gtk_release_modifiers(GtkDisplayState *s)
 {
     VirtualConsole *vc = gd_vc_find_current(s);
 
-    if (vc->type != GD_VC_GFX ||
+    if (!vc || vc->type != GD_VC_GFX ||
         !qemu_console_is_graphic(vc->gfx.dcl.con)) {
         return;
     }
@@ -702,8 +705,8 @@ static void gd_mouse_mode_change(Notifier *notify, void *data)
             gd_ungrab_pointer(s);
         }
     }
-    for (i = 0; i < s->nb_vcs; i++) {
-        VirtualConsole *vc = &s->vc[i];
+    for (i = 0; i < s->vcs->len; i++) {
+        VirtualConsole *vc = g_ptr_array_index(s->vcs, i);
         gd_update_cursor(vc);
     }
 }
@@ -2114,9 +2117,10 @@ static void gd_vcs_init(GtkDisplayState *s, GSList *group,
     int i;
 
     for (i = 0; i < nb_vcs; i++) {
-        VirtualConsole *vc = &s->vc[s->nb_vcs];
-        group = gd_vc_vte_init(s, vc, vcs[i], s->nb_vcs, group, view_menu);
-        s->nb_vcs++;
+        VirtualConsole *vc = g_new0(VirtualConsole, 1);
+        g_ptr_array_add(s->vcs, vc);
+        group = gd_vc_vte_init(s, vc, vcs[i], s->vcs->len - 1,
+                               group, view_menu);
     }
 }
 #endif /* CONFIG_VTE */
@@ -2441,13 +2445,14 @@ static GtkWidget *gd_create_menu_view(GtkDisplayState *s, DisplayOptions *opts)
 
     /* gfx */
     for (vc = 0;; vc++) {
+        VirtualConsole *v;
         con = qemu_console_lookup_by_index(vc);
         if (!con) {
             break;
         }
-        group = gd_vc_gfx_init(s, &s->vc[vc], con,
-                               vc, group, view_menu);
-        s->nb_vcs++;
+        v = g_new0(VirtualConsole, 1);
+        g_ptr_array_add(s->vcs, v);
+        group = gd_vc_gfx_init(s, v, con, vc, group, view_menu);
     }
 
 #if defined(CONFIG_VTE)
@@ -2505,6 +2510,58 @@ static void gd_create_menus(GtkDisplayState *s, DisplayOptions *opts)
 }
 
 
+static void gd_vc_free(void *p)
+{
+    VirtualConsole *vc = p;
+
+    switch (vc->type) {
+    case GD_VC_GFX:
+        qemu_console_unregister_listener(&vc->gfx.dcl);
+#if defined(CONFIG_OPENGL)
+        if (display_opengl) {
+            qemu_console_set_display_gl_ctx(vc->gfx.dcl.con, NULL);
+        }
+        if (vc->gfx.esurface) {
+            eglDestroySurface(qemu_egl_display, vc->gfx.esurface);
+        }
+        if (vc->gfx.ectx) {
+            eglDestroyContext(qemu_egl_display, vc->gfx.ectx);
+        }
+        if (vc->gfx.gls) {
+            surface_gl_destroy_texture(vc->gfx.gls, vc->gfx.ds);
+            qemu_gl_fini_shader(vc->gfx.gls);
+        }
+        egl_fb_destroy(&vc->gfx.guest_fb);
+        egl_fb_destroy(&vc->gfx.win_fb);
+        egl_fb_destroy(&vc->gfx.cursor_fb);
+#endif
+        qkbd_state_free(vc->gfx.kbd);
+        if (vc->gfx.surface) {
+            cairo_surface_destroy(vc->gfx.surface);
+        }
+        if (vc->gfx.convert) {
+            pixman_image_unref(vc->gfx.convert);
+        }
+        break;
+#if defined(CONFIG_VTE)
+    case GD_VC_VTE:
+        fifo8_destroy(&vc->vte.out_fifo);
+        break;
+#endif
+    }
+
+    if (vc->window) {
+        gtk_widget_destroy(vc->window);
+    } else if (vc->tab_item) {
+        gtk_widget_destroy(vc->tab_item);
+    }
+    if (vc->menu_item) {
+        gtk_widget_destroy(vc->menu_item);
+    }
+    g_free(vc->label);
+    g_free(vc);
+}
+
 static GtkDisplayState *gtk_display_state;
 static gboolean gtkinit;
 
@@ -2525,6 +2582,7 @@ static void gtk_display_init(DisplayState *ds, DisplayOptions *opts)
     assert(opts->type == DISPLAY_TYPE_GTK);
     s = g_malloc0(sizeof(*s));
     gtk_display_state = s;
+    s->vcs = g_ptr_array_new_with_free_func(gd_vc_free);
     s->opts = opts;
 
     theme = gtk_icon_theme_get_default();
@@ -2582,13 +2640,10 @@ static void gtk_display_init(DisplayState *ds, DisplayOptions *opts)
 
     gtk_widget_show_all(s->window);
 
-    for (idx = 0;; idx++) {
-        QemuConsole *con = qemu_console_lookup_by_index(idx);
-        if (!con) {
-            break;
-        }
-        if (s->vc[idx].type == GD_VC_GFX) {
-            gtk_widget_realize(s->vc[idx].gfx.drawing_area);
+    for (idx = 0; idx < s->vcs->len; idx++) {
+        VirtualConsole *v = g_ptr_array_index(s->vcs, idx);
+        if (v->type == GD_VC_GFX) {
+            gtk_widget_realize(v->gfx.drawing_area);
         }
     }
 
@@ -2693,6 +2748,9 @@ static void gtk_display_cleanup(void)
 
     qemu_remove_mouse_mode_change_notifier(&s->mouse_mode_notifier);
     gd_clipboard_cleanup(s);
+    g_signal_handlers_disconnect_by_func(s->notebook,
+                                         G_CALLBACK(gd_change_page), s);
+    g_clear_pointer(&s->vcs, g_ptr_array_unref);
     g_clear_pointer(&s->window, gtk_widget_destroy);
     g_clear_object(&s->null_cursor);
     g_clear_pointer(&gtk_display_state, g_free);

-- 
2.54.0



^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH 25/32] ui/gtk: move global display settings out of per-console init
  2026-05-29 11:16 [PATCH 00/32] ui: better console hotplug support Marc-André Lureau
                   ` (23 preceding siblings ...)
  2026-05-29 11:16 ` [PATCH 24/32] ui/gtk: convert VirtualConsole storage from fixed array to GPtrArray Marc-André Lureau
@ 2026-05-29 11:16 ` Marc-André Lureau
  2026-05-29 11:16 ` [PATCH 26/32] ui/gtk: fix tab re-insertion order on window close Marc-André Lureau
                   ` (6 subsequent siblings)
  31 siblings, 0 replies; 39+ messages in thread
From: Marc-André Lureau @ 2026-05-29 11:16 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Fabiano Rosas,
	Laurent Vivier, Alex Williamson, Cédric Le Goater,
	Peter Maydell, Akihiko Odaki, Marc-André Lureau

Move zoom_to_fit, keep_aspect_ratio and touch_slots initialisation
from gd_vc_gfx_init() to gd_create_menu_view(). These are global
display settings that should be set once after all consoles are
created, not repeated on every per-console init (where the last
iteration's values silently win).

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 ui/gtk.c | 46 +++++++++++++++++++++++-----------------------
 1 file changed, 23 insertions(+), 23 deletions(-)

diff --git a/ui/gtk.c b/ui/gtk.c
index 141cb69d494..1b9523739f6 100644
--- a/ui/gtk.c
+++ b/ui/gtk.c
@@ -2269,8 +2269,6 @@ static GSList *gd_vc_gfx_init(GtkDisplayState *s, VirtualConsole *vc,
                               GSList *group, GtkWidget *view_menu)
 {
     const DisplayChangeListenerOps *ops = &dcl_ops;
-    bool zoom_to_fit = false;
-    int i;
 
     vc->label = qemu_console_get_label(con);
     vc->s = s;
@@ -2350,26 +2348,6 @@ static GSList *gd_vc_gfx_init(GtkDisplayState *s, VirtualConsole *vc,
     gd_connect_vc_gfx_signals(vc);
     group = gd_vc_menu_init(s, vc, idx, group, view_menu);
 
-    if (qemu_console_ui_info_supported(vc->gfx.dcl.con)) {
-        zoom_to_fit = true;
-    }
-    if (s->opts->u.gtk.has_zoom_to_fit) {
-        zoom_to_fit = s->opts->u.gtk.zoom_to_fit;
-    }
-    if (zoom_to_fit) {
-        gtk_menu_item_activate(GTK_MENU_ITEM(s->zoom_fit_item));
-        s->free_scale = true;
-    }
-
-    s->keep_aspect_ratio = true;
-    if (s->opts->u.gtk.has_keep_aspect_ratio)
-        s->keep_aspect_ratio = s->opts->u.gtk.keep_aspect_ratio;
-
-    for (i = 0; i < INPUT_EVENT_SLOTS_MAX; i++) {
-        struct touch_slot *slot = &touch_slots[i];
-        slot->tracking_id = -1;
-    }
-
     return group;
 }
 
@@ -2379,7 +2357,8 @@ static GtkWidget *gd_create_menu_view(GtkDisplayState *s, DisplayOptions *opts)
     GtkWidget *view_menu;
     GtkWidget *separator;
     QemuConsole *con;
-    int vc;
+    bool zoom_to_fit = false;
+    int vc, i;
 
     view_menu = gtk_menu_new();
     gtk_menu_set_accel_group(GTK_MENU(view_menu), s->accel_group);
@@ -2453,6 +2432,27 @@ static GtkWidget *gd_create_menu_view(GtkDisplayState *s, DisplayOptions *opts)
         v = g_new0(VirtualConsole, 1);
         g_ptr_array_add(s->vcs, v);
         group = gd_vc_gfx_init(s, v, con, vc, group, view_menu);
+        if (qemu_console_ui_info_supported(con)) {
+            zoom_to_fit = true;
+        }
+    }
+
+    if (s->opts->u.gtk.has_zoom_to_fit) {
+        zoom_to_fit = s->opts->u.gtk.zoom_to_fit;
+    }
+    if (zoom_to_fit) {
+        gtk_menu_item_activate(GTK_MENU_ITEM(s->zoom_fit_item));
+        s->free_scale = true;
+    }
+
+    s->keep_aspect_ratio = true;
+    if (s->opts->u.gtk.has_keep_aspect_ratio) {
+        s->keep_aspect_ratio = s->opts->u.gtk.keep_aspect_ratio;
+    }
+
+    for (i = 0; i < INPUT_EVENT_SLOTS_MAX; i++) {
+        struct touch_slot *slot = &touch_slots[i];
+        slot->tracking_id = -1;
     }
 
 #if defined(CONFIG_VTE)

-- 
2.54.0



^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH 26/32] ui/gtk: fix tab re-insertion order on window close
  2026-05-29 11:16 [PATCH 00/32] ui: better console hotplug support Marc-André Lureau
                   ` (24 preceding siblings ...)
  2026-05-29 11:16 ` [PATCH 25/32] ui/gtk: move global display settings out of per-console init Marc-André Lureau
@ 2026-05-29 11:16 ` Marc-André Lureau
  2026-05-29 11:16 ` [PATCH 27/32] ui/gtk: centralize console menu and shortcut management Marc-André Lureau
                   ` (5 subsequent siblings)
  31 siblings, 0 replies; 39+ messages in thread
From: Marc-André Lureau @ 2026-05-29 11:16 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Fabiano Rosas,
	Laurent Vivier, Alex Williamson, Cédric Le Goater,
	Peter Maydell, Akihiko Odaki, Marc-André Lureau

Add gd_vc_notebook_pos() which computes the correct notebook position
for a console by counting only non-detached (non-windowed) tabs before
it. Use it in gd_tab_window_close() so a re-attached tab is inserted
at its logical position rather than appended at the end.

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 ui/gtk.c | 27 ++++++++++++++++++++++++---
 1 file changed, 24 insertions(+), 3 deletions(-)

diff --git a/ui/gtk.c b/ui/gtk.c
index 1b9523739f6..c4d05757c75 100644
--- a/ui/gtk.c
+++ b/ui/gtk.c
@@ -1464,16 +1464,37 @@ static void gd_menu_show_tabs(GtkMenuItem *item, void *opaque)
     gd_update_windowsize(vc);
 }
 
+static int gd_vc_notebook_pos(GtkDisplayState *s, VirtualConsole *target)
+{
+    int pos = 0;
+    guint i;
+
+    for (i = 0; i < s->vcs->len; i++) {
+        VirtualConsole *vc = g_ptr_array_index(s->vcs, i);
+        if (vc == target) {
+            return pos;
+        }
+        if (!vc->window) {
+            pos++;
+        }
+    }
+    g_assert_not_reached();
+}
+
 static gboolean gd_tab_window_close(GtkWidget *widget, GdkEvent *event,
                                     void *opaque)
 {
     VirtualConsole *vc = opaque;
     GtkDisplayState *s = vc->s;
+    int page;
 
     gtk_widget_set_sensitive(vc->menu_item, true);
-    gd_widget_reparent(vc->window, s->notebook, vc->tab_item);
-    gtk_notebook_set_tab_label_text(GTK_NOTEBOOK(s->notebook),
-                                    vc->tab_item, vc->label);
+    g_object_ref(vc->tab_item);
+    gtk_container_remove(GTK_CONTAINER(vc->window), vc->tab_item);
+    page = gd_vc_notebook_pos(s, vc);
+    gtk_notebook_insert_page(GTK_NOTEBOOK(s->notebook),
+                             vc->tab_item, gtk_label_new(vc->label), page);
+    g_object_unref(vc->tab_item);
     gtk_widget_destroy(vc->window);
     vc->window = NULL;
 #if defined(CONFIG_OPENGL)

-- 
2.54.0



^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH 27/32] ui/gtk: centralize console menu and shortcut management
  2026-05-29 11:16 [PATCH 00/32] ui: better console hotplug support Marc-André Lureau
                   ` (25 preceding siblings ...)
  2026-05-29 11:16 ` [PATCH 26/32] ui/gtk: fix tab re-insertion order on window close Marc-André Lureau
@ 2026-05-29 11:16 ` Marc-André Lureau
  2026-05-29 11:16 ` [PATCH 28/32] ui/gtk: handle console hotplug/unplug events Marc-André Lureau
                   ` (4 subsequent siblings)
  31 siblings, 0 replies; 39+ messages in thread
From: Marc-André Lureau @ 2026-05-29 11:16 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Fabiano Rosas,
	Laurent Vivier, Alex Williamson, Cédric Le Goater,
	Peter Maydell, Akihiko Odaki, Marc-André Lureau

Replace the per-console gd_vc_menu_init() with gd_rebuild_vc_menu()
that tears down and rebuilds all console radio menu items and
Ctrl+Alt+N accelerators at once. This is called from initialization
and whenever consoles are detached or reattached.

Shortcuts now skip detached (windowed) consoles, so they always map
to reachable tabs. Rename gd_vc_gfx_init() to add_gfx_console()
and simplify the init function signatures now that menu creation is
decoupled.

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 include/ui/gtk.h |   1 +
 ui/gtk.c         | 127 ++++++++++++++++++++++++++++++++++++-------------------
 2 files changed, 85 insertions(+), 43 deletions(-)

diff --git a/include/ui/gtk.h b/include/ui/gtk.h
index 4d54de97ea7..b0be5070795 100644
--- a/include/ui/gtk.h
+++ b/include/ui/gtk.h
@@ -120,6 +120,7 @@ struct GtkDisplayState {
 
     GPtrArray *vcs;
 
+    GtkWidget *vc_menu_separator;
     GtkWidget *show_tabs_item;
     GtkWidget *untabify_item;
     GtkWidget *show_menubar_item;
diff --git a/ui/gtk.c b/ui/gtk.c
index c4d05757c75..621bd269b3a 100644
--- a/ui/gtk.c
+++ b/ui/gtk.c
@@ -142,6 +142,7 @@ static void gd_grab_pointer(VirtualConsole *vc, const char *reason);
 static void gd_ungrab_pointer(GtkDisplayState *s);
 static void gd_grab_keyboard(VirtualConsole *vc, const char *reason);
 static void gd_ungrab_keyboard(GtkDisplayState *s);
+static void gd_rebuild_vc_menu(GtkDisplayState *s);
 
 /** Utility Functions **/
 
@@ -1488,7 +1489,6 @@ static gboolean gd_tab_window_close(GtkWidget *widget, GdkEvent *event,
     GtkDisplayState *s = vc->s;
     int page;
 
-    gtk_widget_set_sensitive(vc->menu_item, true);
     g_object_ref(vc->tab_item);
     gtk_container_remove(GTK_CONTAINER(vc->window), vc->tab_item);
     page = gd_vc_notebook_pos(s, vc);
@@ -1508,6 +1508,8 @@ static gboolean gd_tab_window_close(GtkWidget *widget, GdkEvent *event,
     }
 #endif
 
+    gd_rebuild_vc_menu(s);
+
     if (vc == gd_vc_find_by_menu(s)) {
         gtk_widget_grab_focus(vc->focus);
     }
@@ -1539,7 +1541,6 @@ static void gd_menu_untabify(GtkMenuItem *item, void *opaque)
                                        FALSE);
     }
     if (!vc->window) {
-        gtk_widget_set_sensitive(vc->menu_item, false);
         vc->window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
 #if defined(CONFIG_OPENGL)
         if (vc->gfx.esurface) {
@@ -1566,6 +1567,7 @@ static void gd_menu_untabify(GtkMenuItem *item, void *opaque)
             gtk_accel_group_connect(ag, GDK_KEY_g, HOTKEY_MODIFIERS, 0, cb);
         }
 
+        gd_rebuild_vc_menu(s);
         gd_update_geometry_hints(vc);
         gd_update_caption(s);
     }
@@ -1906,22 +1908,73 @@ static gboolean gd_configure(GtkWidget *widget,
 
 /** Virtual Console Callbacks **/
 
-static GSList *gd_vc_menu_init(GtkDisplayState *s, VirtualConsole *vc,
-                               int idx, GSList *group, GtkWidget *view_menu)
+static void gd_rebuild_vc_menu(GtkDisplayState *s)
 {
-    vc->menu_item = gtk_radio_menu_item_new_with_mnemonic(group, vc->label);
-    gtk_accel_group_connect(s->accel_group, GDK_KEY_1 + idx,
-            HOTKEY_MODIFIERS, 0,
-            g_cclosure_new_swap(G_CALLBACK(gd_accel_switch_vc), vc, NULL));
-    gtk_accel_label_set_accel(
-            GTK_ACCEL_LABEL(gtk_bin_get_child(GTK_BIN(vc->menu_item))),
-            GDK_KEY_1 + idx, HOTKEY_MODIFIERS);
+    GSList *group = NULL;
+    VirtualConsole *vc;
+    GList *children;
+    gint insert_pos;
+    int shortcut_idx = 0;
+    guint i;
+
+    for (i = 0; i < s->vcs->len; i++) {
+        vc = g_ptr_array_index(s->vcs, i);
+        if (vc->menu_item) {
+            gtk_widget_destroy(vc->menu_item);
+            vc->menu_item = NULL;
+        }
+    }
+
+    for (i = 0; i < 9; i++) {
+        gtk_accel_group_disconnect_key(s->accel_group,
+                                       GDK_KEY_1 + i, HOTKEY_MODIFIERS);
+    }
+
+    /* find insertion position (just before vc_menu_separator) */
+    children = gtk_container_get_children(GTK_CONTAINER(s->view_menu));
+    insert_pos = g_list_index(children, s->vc_menu_separator);
+    g_list_free(children);
+
+    /* create new menu items for each console */
+    for (i = 0; i < s->vcs->len; i++) {
+        vc = g_ptr_array_index(s->vcs, i);
 
-    g_signal_connect(vc->menu_item, "activate",
-                     G_CALLBACK(gd_menu_switch_vc), s);
-    gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), vc->menu_item);
+        vc->menu_item = gtk_radio_menu_item_new_with_mnemonic(group,
+                                                               vc->label);
+        group = gtk_radio_menu_item_get_group(
+            GTK_RADIO_MENU_ITEM(vc->menu_item));
+
+        if (vc->window) {
+            gtk_widget_set_sensitive(vc->menu_item, false);
+        } else if (shortcut_idx < 9) {
+            guint key = GDK_KEY_1 + shortcut_idx;
+            gtk_accel_group_connect(s->accel_group, key,
+                    HOTKEY_MODIFIERS, 0,
+                    g_cclosure_new_swap(G_CALLBACK(gd_accel_switch_vc),
+                                        vc, NULL));
+            gtk_accel_label_set_accel(
+                    GTK_ACCEL_LABEL(gtk_bin_get_child(GTK_BIN(vc->menu_item))),
+                    key, HOTKEY_MODIFIERS);
+            shortcut_idx++;
+        }
 
-    return gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(vc->menu_item));
+        g_signal_connect(vc->menu_item, "activate",
+                         G_CALLBACK(gd_menu_switch_vc), s);
+        gtk_menu_shell_insert(GTK_MENU_SHELL(s->view_menu),
+                              vc->menu_item, insert_pos + i);
+        gtk_widget_show(vc->menu_item);
+    }
+
+    /* sync active menu item with current notebook page */
+    vc = gd_vc_find_current(s);
+    if (vc && vc->menu_item) {
+        g_signal_handlers_block_by_func(vc->menu_item,
+                                        gd_menu_switch_vc, s);
+        gtk_check_menu_item_set_active(
+            GTK_CHECK_MENU_ITEM(vc->menu_item), TRUE);
+        g_signal_handlers_unblock_by_func(vc->menu_item,
+                                          gd_menu_switch_vc, s);
+    }
 }
 
 #if defined(CONFIG_VTE)
@@ -2064,9 +2117,8 @@ static gboolean gd_vc_in(VteTerminal *terminal, gchar *text, guint size,
     return TRUE;
 }
 
-static GSList *gd_vc_vte_init(GtkDisplayState *s, VirtualConsole *vc,
-                              Chardev *chr, int idx,
-                              GSList *group, GtkWidget *view_menu)
+static void gd_vc_vte_init(GtkDisplayState *s, VirtualConsole *vc,
+                           Chardev *chr, int idx)
 {
     char buffer[32];
     GtkWidget *box;
@@ -2082,7 +2134,6 @@ static GSList *gd_vc_vte_init(GtkDisplayState *s, VirtualConsole *vc,
 
     snprintf(buffer, sizeof(buffer), "vc%d", idx);
     vc->label = g_strdup(vc->vte.chr->label ? : buffer);
-    group = gd_vc_menu_init(s, vc, idx, group, view_menu);
 
     vc->vte.terminal = vte_terminal_new();
     g_signal_connect(vc->vte.terminal, "commit", G_CALLBACK(gd_vc_in), vc);
@@ -2128,20 +2179,16 @@ static GSList *gd_vc_vte_init(GtkDisplayState *s, VirtualConsole *vc,
                              gtk_label_new(vc->label));
 
     qemu_chr_be_event(vc->vte.chr, CHR_EVENT_OPENED);
-
-    return group;
 }
 
-static void gd_vcs_init(GtkDisplayState *s, GSList *group,
-                        GtkWidget *view_menu)
+static void gd_vcs_init(GtkDisplayState *s)
 {
     int i;
 
     for (i = 0; i < nb_vcs; i++) {
         VirtualConsole *vc = g_new0(VirtualConsole, 1);
         g_ptr_array_add(s->vcs, vc);
-        group = gd_vc_vte_init(s, vc, vcs[i], s->vcs->len - 1,
-                               group, view_menu);
+        gd_vc_vte_init(s, vc, vcs[i], s->vcs->len - 1);
     }
 }
 #endif /* CONFIG_VTE */
@@ -2285,12 +2332,12 @@ static bool gd_scale_valid(double scale)
     return scale >= VC_SCALE_MIN && scale <= VC_SCALE_MAX;
 }
 
-static GSList *gd_vc_gfx_init(GtkDisplayState *s, VirtualConsole *vc,
-                              QemuConsole *con, int idx,
-                              GSList *group, GtkWidget *view_menu)
+static void add_gfx_console(GtkDisplayState *s, QemuConsole *con)
 {
+    VirtualConsole *vc = g_new0(VirtualConsole, 1);
     const DisplayChangeListenerOps *ops = &dcl_ops;
 
+    g_ptr_array_add(s->vcs, vc);
     vc->label = qemu_console_get_label(con);
     vc->s = s;
     vc->gfx.preferred_scale = 1.0;
@@ -2367,14 +2414,10 @@ static GSList *gd_vc_gfx_init(GtkDisplayState *s, VirtualConsole *vc,
     qemu_console_register_listener(con, &vc->gfx.dcl, ops);
 
     gd_connect_vc_gfx_signals(vc);
-    group = gd_vc_menu_init(s, vc, idx, group, view_menu);
-
-    return group;
 }
 
-static GtkWidget *gd_create_menu_view(GtkDisplayState *s, DisplayOptions *opts)
+static void gd_create_menu_view(GtkDisplayState *s, DisplayOptions *opts)
 {
-    GSList *group = NULL;
     GtkWidget *view_menu;
     GtkWidget *separator;
     QemuConsole *con;
@@ -2382,6 +2425,7 @@ static GtkWidget *gd_create_menu_view(GtkDisplayState *s, DisplayOptions *opts)
     int vc, i;
 
     view_menu = gtk_menu_new();
+    s->view_menu = view_menu;
     gtk_menu_set_accel_group(GTK_MENU(view_menu), s->accel_group);
 
     s->full_screen_item = gtk_menu_item_new_with_mnemonic(_("_Fullscreen"));
@@ -2445,14 +2489,11 @@ static GtkWidget *gd_create_menu_view(GtkDisplayState *s, DisplayOptions *opts)
 
     /* gfx */
     for (vc = 0;; vc++) {
-        VirtualConsole *v;
         con = qemu_console_lookup_by_index(vc);
         if (!con) {
             break;
         }
-        v = g_new0(VirtualConsole, 1);
-        g_ptr_array_add(s->vcs, v);
-        group = gd_vc_gfx_init(s, v, con, vc, group, view_menu);
+        add_gfx_console(s, con);
         if (qemu_console_ui_info_supported(con)) {
             zoom_to_fit = true;
         }
@@ -2478,11 +2519,13 @@ static GtkWidget *gd_create_menu_view(GtkDisplayState *s, DisplayOptions *opts)
 
 #if defined(CONFIG_VTE)
     /* vte */
-    gd_vcs_init(s, group, view_menu);
+    gd_vcs_init(s);
 #endif
 
-    separator = gtk_separator_menu_item_new();
-    gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), separator);
+    s->vc_menu_separator = gtk_separator_menu_item_new();
+    gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), s->vc_menu_separator);
+
+    gd_rebuild_vc_menu(s);
 
     s->show_tabs_item = gtk_check_menu_item_new_with_mnemonic(_("Show _Tabs"));
     gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), s->show_tabs_item);
@@ -2501,8 +2544,6 @@ static GtkWidget *gd_create_menu_view(GtkDisplayState *s, DisplayOptions *opts)
             GTK_ACCEL_LABEL(gtk_bin_get_child(GTK_BIN(s->show_menubar_item))),
             GDK_KEY_m, HOTKEY_MODIFIERS);
     gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), s->show_menubar_item);
-
-    return view_menu;
 }
 
 static void gd_create_menus(GtkDisplayState *s, DisplayOptions *opts)
@@ -2511,7 +2552,7 @@ static void gd_create_menus(GtkDisplayState *s, DisplayOptions *opts)
 
     s->accel_group = gtk_accel_group_new();
     s->machine_menu = gd_create_menu_machine(s);
-    s->view_menu = gd_create_menu_view(s, opts);
+    gd_create_menu_view(s, opts);
 
     s->machine_menu_item = gtk_menu_item_new_with_mnemonic(_("_Machine"));
     gtk_menu_item_set_submenu(GTK_MENU_ITEM(s->machine_menu_item),

-- 
2.54.0



^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH 28/32] ui/gtk: handle console hotplug/unplug events
  2026-05-29 11:16 [PATCH 00/32] ui: better console hotplug support Marc-André Lureau
                   ` (26 preceding siblings ...)
  2026-05-29 11:16 ` [PATCH 27/32] ui/gtk: centralize console menu and shortcut management Marc-André Lureau
@ 2026-05-29 11:16 ` Marc-André Lureau
  2026-05-29 11:16 ` [PATCH 29/32] ui/console: register console in QOM tree dynamically Marc-André Lureau
                   ` (3 subsequent siblings)
  31 siblings, 0 replies; 39+ messages in thread
From: Marc-André Lureau @ 2026-05-29 11:16 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Fabiano Rosas,
	Laurent Vivier, Alex Williamson, Cédric Le Goater,
	Peter Maydell, Akihiko Odaki, Marc-André Lureau

Register a console notifier so the GTK display dynamically creates and
destroys VirtualConsole tabs when graphic consoles are added or removed
at runtime (e.g. vfio-pci with display=on hotplug).

Add skips consoles that already have a VC binding, remove skips unknown
consoles.

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 include/ui/gtk.h |  1 +
 ui/gtk.c         | 77 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 77 insertions(+), 1 deletion(-)

diff --git a/include/ui/gtk.h b/include/ui/gtk.h
index b0be5070795..6cb611ce474 100644
--- a/include/ui/gtk.h
+++ b/include/ui/gtk.h
@@ -140,6 +140,7 @@ struct GtkDisplayState {
 
     GdkCursor *null_cursor;
     Notifier mouse_mode_notifier;
+    Notifier console_notifier;
     gboolean free_scale;
     gboolean keep_aspect_ratio;
 
diff --git a/ui/gtk.c b/ui/gtk.c
index 621bd269b3a..484dd9159c2 100644
--- a/ui/gtk.c
+++ b/ui/gtk.c
@@ -2332,7 +2332,8 @@ static bool gd_scale_valid(double scale)
     return scale >= VC_SCALE_MIN && scale <= VC_SCALE_MAX;
 }
 
-static void add_gfx_console(GtkDisplayState *s, QemuConsole *con)
+static VirtualConsole *
+add_gfx_console(GtkDisplayState *s, QemuConsole *con)
 {
     VirtualConsole *vc = g_new0(VirtualConsole, 1);
     const DisplayChangeListenerOps *ops = &dcl_ops;
@@ -2414,6 +2415,76 @@ static void add_gfx_console(GtkDisplayState *s, QemuConsole *con)
     qemu_console_register_listener(con, &vc->gfx.dcl, ops);
 
     gd_connect_vc_gfx_signals(vc);
+    return vc;
+}
+
+static void gd_vc_add_gfx(GtkDisplayState *s, QemuConsole *con)
+{
+    VirtualConsole *vc;
+    int i;
+
+    for (i = 0; i < (int)s->vcs->len; i++) {
+        VirtualConsole *v = g_ptr_array_index(s->vcs, i);
+        if (v->type == GD_VC_GFX && v->gfx.dcl.con == con) {
+            return;
+        }
+    }
+
+    vc = add_gfx_console(s, con);
+    gtk_widget_show_all(vc->tab_item);
+    gtk_widget_realize(vc->gfx.drawing_area);
+
+    if (s->free_scale) {
+        gd_update_windowsize(vc);
+    }
+
+    gd_update_caption(s);
+    gd_rebuild_vc_menu(s);
+
+    gtk_notebook_set_show_tabs(GTK_NOTEBOOK(s->notebook), true);
+}
+
+static void gd_vc_remove_gfx(GtkDisplayState *s, QemuConsole *con)
+{
+    VirtualConsole *vc = NULL;
+    guint idx;
+
+    for (idx = 0; idx < s->vcs->len; idx++) {
+        VirtualConsole *v = g_ptr_array_index(s->vcs, idx);
+        if (v->type == GD_VC_GFX && v->gfx.dcl.con == con) {
+            vc = v;
+            break;
+        }
+    }
+    if (!vc) {
+        return;
+    }
+
+    if (s->kbd_owner == vc) {
+        gd_ungrab_keyboard(s);
+    }
+    if (s->ptr_owner == vc) {
+        gd_ungrab_pointer(s);
+    }
+
+    g_ptr_array_remove_index(s->vcs, idx);
+    gd_rebuild_vc_menu(s);
+    gd_update_caption(s);
+}
+
+static void gd_console_notify(Notifier *n, void *data)
+{
+    GtkDisplayState *s = container_of(n, GtkDisplayState, console_notifier);
+    QemuConsoleEvent *event = data;
+
+    switch (event->type) {
+    case QEMU_CONSOLE_ADDED:
+        gd_vc_add_gfx(s, event->con);
+        break;
+    case QEMU_CONSOLE_REMOVED:
+        gd_vc_remove_gfx(s, event->con);
+        break;
+    }
 }
 
 static void gd_create_menu_view(GtkDisplayState *s, DisplayOptions *opts)
@@ -2688,6 +2759,9 @@ static void gtk_display_init(DisplayState *ds, DisplayOptions *opts)
 
     gd_create_menus(s, opts);
 
+    s->console_notifier.notify = gd_console_notify;
+    qemu_console_add_notifier(&s->console_notifier);
+
     gd_connect_signals(s);
 
     gtk_notebook_set_show_tabs(GTK_NOTEBOOK(s->notebook), FALSE);
@@ -2808,6 +2882,7 @@ static void gtk_display_cleanup(void)
         return;
     }
 
+    qemu_console_remove_notifier(&s->console_notifier);
     qemu_remove_mouse_mode_change_notifier(&s->mouse_mode_notifier);
     gd_clipboard_cleanup(s);
     g_signal_handlers_disconnect_by_func(s->notebook,

-- 
2.54.0



^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH 29/32] ui/console: register console in QOM tree dynamically
  2026-05-29 11:16 [PATCH 00/32] ui: better console hotplug support Marc-André Lureau
                   ` (27 preceding siblings ...)
  2026-05-29 11:16 ` [PATCH 28/32] ui/gtk: handle console hotplug/unplug events Marc-André Lureau
@ 2026-05-29 11:16 ` Marc-André Lureau
  2026-05-29 11:16 ` [PATCH 30/32] ui/console: unregister console from QOM tree on close Marc-André Lureau
                   ` (2 subsequent siblings)
  31 siblings, 0 replies; 39+ messages in thread
From: Marc-André Lureau @ 2026-05-29 11:16 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Fabiano Rosas,
	Laurent Vivier, Alex Williamson, Cédric Le Goater,
	Peter Maydell, Akihiko Odaki, Marc-André Lureau

Consoles created after init_displaystate() (e.g. hotplugged
display devices) were never added to the /backend/console[N]
QOM tree. Extract qemu_console_add_to_qom() and call it from
qemu_console_register() when the display is already initialized.

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 ui/console.c | 17 ++++++++++++-----
 1 file changed, 12 insertions(+), 5 deletions(-)

diff --git a/ui/console.c b/ui/console.c
index 975eaf15706..e01d893df4b 100644
--- a/ui/console.c
+++ b/ui/console.c
@@ -376,6 +376,13 @@ void qemu_text_console_put_string(QemuTextConsole *s, const char *str, int len)
     }
 }
 
+static void qemu_console_add_to_qom(QemuConsole *con)
+{
+    g_autofree gchar *name = g_strdup_printf("console[%d]", con->index);
+    object_property_add_child(object_get_container("backend"),
+                              name, OBJECT(con));
+}
+
 static void
 qemu_console_register(QemuConsole *c)
 {
@@ -413,6 +420,10 @@ qemu_console_register(QemuConsole *c)
             }
         }
     }
+
+    if (phase_check(PHASE_MACHINE_READY)) {
+        qemu_console_add_to_qom(c);
+    }
 }
 
 static void
@@ -1089,17 +1100,13 @@ void qemu_console_remove_notifier(Notifier *notifier)
  */
 DisplayState *init_displaystate(void)
 {
-    gchar *name;
     QemuConsole *con;
 
     QTAILQ_FOREACH(con, &consoles, next) {
         /* Hook up into the qom tree here (not in object_new()), once
          * all QemuConsoles are created and the order / numbering
          * doesn't change any more */
-        name = g_strdup_printf("console[%d]", con->index);
-        object_property_add_child(object_get_container("backend"),
-                                  name, OBJECT(con));
-        g_free(name);
+        qemu_console_add_to_qom(con);
     }
 
     return display_state;

-- 
2.54.0



^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH 30/32] ui/console: unregister console from QOM tree on close
  2026-05-29 11:16 [PATCH 00/32] ui: better console hotplug support Marc-André Lureau
                   ` (28 preceding siblings ...)
  2026-05-29 11:16 ` [PATCH 29/32] ui/console: register console in QOM tree dynamically Marc-André Lureau
@ 2026-05-29 11:16 ` Marc-André Lureau
  2026-05-29 11:16 ` [PATCH 31/32] ui/dbus: handle console hotplug/unplug events Marc-André Lureau
  2026-05-29 11:16 ` [PATCH 32/32] tests/qtest: add D-Bus display hotplug test Marc-André Lureau
  31 siblings, 0 replies; 39+ messages in thread
From: Marc-André Lureau @ 2026-05-29 11:16 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Fabiano Rosas,
	Laurent Vivier, Alex Williamson, Cédric Le Goater,
	Peter Maydell, Akihiko Odaki, Marc-André Lureau

Call object_unparent() in graphic_console_close() to remove the
console from /backend/console[N]. This drops the QOM tree
reference, while the initial object_new() reference keeps the
console alive for potential reuse.

When graphic_console_init() reuses a closed console, re-register
it in the QOM tree.

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 ui/console.c | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/ui/console.c b/ui/console.c
index e01d893df4b..4d10884dc11 100644
--- a/ui/console.c
+++ b/ui/console.c
@@ -1136,6 +1136,9 @@ QemuConsole *qemu_graphic_console_create(DeviceState *dev, uint32_t head,
         trace_console_gfx_reuse(s->index);
         width = qemu_console_get_width(s, 0);
         height = qemu_console_get_height(s, 0);
+        if (phase_check(PHASE_MACHINE_READY)) {
+            qemu_console_add_to_qom(s);
+        }
     } else {
         trace_console_gfx_new();
         s = (QemuConsole *)object_new(TYPE_QEMU_GRAPHIC_CONSOLE);
@@ -1176,6 +1179,7 @@ void qemu_graphic_console_close(QemuConsole *con)
     }
     surface = qemu_create_placeholder_surface(width, height, unplugged);
     qemu_console_set_surface(con, surface);
+    object_unparent(OBJECT(con));
 }
 
 QemuConsole *qemu_console_lookup_default(void)

-- 
2.54.0



^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH 31/32] ui/dbus: handle console hotplug/unplug events
  2026-05-29 11:16 [PATCH 00/32] ui: better console hotplug support Marc-André Lureau
                   ` (29 preceding siblings ...)
  2026-05-29 11:16 ` [PATCH 30/32] ui/console: unregister console from QOM tree on close Marc-André Lureau
@ 2026-05-29 11:16 ` Marc-André Lureau
  2026-05-29 11:16 ` [PATCH 32/32] tests/qtest: add D-Bus display hotplug test Marc-André Lureau
  31 siblings, 0 replies; 39+ messages in thread
From: Marc-André Lureau @ 2026-05-29 11:16 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Fabiano Rosas,
	Laurent Vivier, Alex Williamson, Cédric Le Goater,
	Peter Maydell, Akihiko Odaki, Marc-André Lureau

Subscribe to QemuConsoleEvent notifications to dynamically add and
remove D-Bus display consoles. This mirrors the GTK backend's
handling added in the previous commits.

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 ui/dbus.h         |  3 ++
 ui/dbus-console.c |  5 +++
 ui/dbus.c         | 93 +++++++++++++++++++++++++++++++++++++++++++++----------
 3 files changed, 84 insertions(+), 17 deletions(-)

diff --git a/ui/dbus.h b/ui/dbus.h
index e4e78590b49..d2cc176648a 100644
--- a/ui/dbus.h
+++ b/ui/dbus.h
@@ -60,6 +60,7 @@ struct DBusDisplay {
     DBusClipboardRequest clipboard_request[QEMU_CLIPBOARD_SELECTION__COUNT];
 
     Notifier notifier;
+    Notifier console_notifier;
 };
 
 #ifdef WIN32
@@ -86,6 +87,8 @@ dbus_display_console_new(DBusDisplay *display, QemuConsole *con);
 int
 dbus_display_console_get_index(DBusDisplayConsole *ddc);
 
+QemuConsole *
+dbus_display_console_get_qemu_console(DBusDisplayConsole *ddc);
 
 extern const DisplayChangeListenerOps dbus_console_dcl_ops;
 
diff --git a/ui/dbus-console.c b/ui/dbus-console.c
index 0048951a7ab..dd1a18c7460 100644
--- a/ui/dbus-console.c
+++ b/ui/dbus-console.c
@@ -531,6 +531,11 @@ int dbus_display_console_get_index(DBusDisplayConsole *ddc)
     return qemu_console_get_index(ddc->dcl.con);
 }
 
+QemuConsole *dbus_display_console_get_qemu_console(DBusDisplayConsole *ddc)
+{
+    return ddc->dcl.con;
+}
+
 DBusDisplayConsole *
 dbus_display_console_new(DBusDisplay *display, QemuConsole *con)
 {
diff --git a/ui/dbus.c b/ui/dbus.c
index b23cb44c535..7be0f8e2611 100644
--- a/ui/dbus.c
+++ b/ui/dbus.c
@@ -142,6 +142,9 @@ dbus_display_finalize(Object *o)
 {
     DBusDisplay *dd = DBUS_DISPLAY(o);
 
+    if (dd->console_notifier.notify) {
+        qemu_console_remove_notifier(&dd->console_notifier);
+    }
     if (dd->notifier.notify) {
         dbus_display_notifier_remove(&dd->notifier);
     }
@@ -164,14 +167,35 @@ dbus_display_finalize(Object *o)
     dbus_display = NULL;
 }
 
+static void
+dbus_update_console_ids(DBusDisplay *dd)
+{
+    g_autoptr(GArray) arr = g_array_new(FALSE, FALSE, sizeof(guint32));
+
+    for (guint i = 0; i < dd->consoles->len; i++) {
+        DBusDisplayConsole *ddc = g_ptr_array_index(dd->consoles, i);
+        guint32 idx = dbus_display_console_get_index(ddc);
+        g_array_append_val(arr, idx);
+    }
+
+    g_object_set(dd->iface, "console-ids",
+        g_variant_new_fixed_array(G_VARIANT_TYPE("u"),
+            arr->data, arr->len,
+            sizeof(guint32)),
+        NULL);
+}
+
 static bool
-dbus_display_add_console(DBusDisplay *dd, int idx, Error **errp)
+dbus_display_add_console(DBusDisplay *dd, QemuConsole *con, Error **errp)
 {
-    QemuConsole *con;
     DBusDisplayConsole *dbus_console;
 
-    con = qemu_console_lookup_by_index(idx);
-    assert(con);
+    for (guint i = 0; i < dd->consoles->len; i++) {
+        DBusDisplayConsole *ddc = g_ptr_array_index(dd->consoles, i);
+        if (dbus_display_console_get_qemu_console(ddc) == con) {
+            return true;
+        }
+    }
 
     if (qemu_console_is_graphic(con) &&
         dd->gl_mode != DISPLAY_GL_MODE_OFF) {
@@ -179,20 +203,58 @@ dbus_display_add_console(DBusDisplay *dd, int idx, Error **errp)
     }
 
     dbus_console = dbus_display_console_new(dd, con);
-    g_ptr_array_insert(dd->consoles, idx, dbus_console);
+    g_ptr_array_add(dd->consoles, dbus_console);
     g_dbus_object_manager_server_export(dd->server,
                                         G_DBUS_OBJECT_SKELETON(dbus_console));
+    dbus_update_console_ids(dd);
     return true;
 }
 
+static void
+dbus_display_remove_console(DBusDisplay *dd, QemuConsole *con)
+{
+    for (guint i = 0; i < dd->consoles->len; i++) {
+        DBusDisplayConsole *ddc = g_ptr_array_index(dd->consoles, i);
+        if (dbus_display_console_get_qemu_console(ddc) == con) {
+            if (display_opengl) {
+                qemu_console_set_display_gl_ctx(con, NULL);
+            }
+            g_dbus_object_manager_server_unexport(
+                dd->server,
+                g_dbus_object_get_object_path(G_DBUS_OBJECT(ddc)));
+            g_ptr_array_remove_index(dd->consoles, i);
+            dbus_update_console_ids(dd);
+            break;
+        }
+    }
+}
+
+static void
+dbus_console_notify(Notifier *n, void *data)
+{
+    DBusDisplay *dd = container_of(n, DBusDisplay, console_notifier);
+    QemuConsoleEvent *event = data;
+
+    switch (event->type) {
+    case QEMU_CONSOLE_ADDED: {
+        Error *err = NULL;
+        if (!dbus_display_add_console(dd, event->con, &err)) {
+            error_report_err(err);
+        }
+        break;
+    }
+    case QEMU_CONSOLE_REMOVED:
+        dbus_display_remove_console(dd, event->con);
+        break;
+    }
+}
+
 static void
 dbus_display_complete(UserCreatable *uc, Error **errp)
 {
     DBusDisplay *dd = DBUS_DISPLAY(uc);
     g_autoptr(GError) err = NULL;
     g_autofree char *uuid = qemu_uuid_unparse_strdup(&qemu_uuid);
-    g_autoptr(GArray) consoles = NULL;
-    GVariant *console_ids;
     int idx;
 
     if (!object_resolve_path_type("", TYPE_DBUS_DISPLAY, NULL)) {
@@ -233,27 +295,24 @@ dbus_display_complete(UserCreatable *uc, Error **errp)
         }
     }
 
-    consoles = g_array_new(FALSE, FALSE, sizeof(guint32));
     for (idx = 0;; idx++) {
-        if (!qemu_console_lookup_by_index(idx)) {
+        QemuConsole *con = qemu_console_lookup_by_index(idx);
+        if (!con) {
             break;
         }
-        if (!dbus_display_add_console(dd, idx, errp)) {
+        if (!dbus_display_add_console(dd, con, errp)) {
             return;
         }
-        g_array_append_val(consoles, idx);
     }
 
-    console_ids = g_variant_new_from_data(
-        G_VARIANT_TYPE("au"),
-        consoles->data, consoles->len * sizeof(guint32), TRUE,
-        (GDestroyNotify)g_array_unref, consoles);
-    g_steal_pointer(&consoles);
     g_object_set(dd->iface,
                  "name", qemu_name ?: "QEMU " QEMU_VERSION,
                  "uuid", uuid,
-                 "console-ids", console_ids,
                  NULL);
+    dbus_update_console_ids(dd);
+
+    dd->console_notifier.notify = dbus_console_notify;
+    qemu_console_add_notifier(&dd->console_notifier);
 
     if (dd->bus) {
         g_dbus_object_manager_server_set_connection(dd->server, dd->bus);

-- 
2.54.0



^ permalink raw reply related	[flat|nested] 39+ messages in thread

* [PATCH 32/32] tests/qtest: add D-Bus display hotplug test
  2026-05-29 11:16 [PATCH 00/32] ui: better console hotplug support Marc-André Lureau
                   ` (30 preceding siblings ...)
  2026-05-29 11:16 ` [PATCH 31/32] ui/dbus: handle console hotplug/unplug events Marc-André Lureau
@ 2026-05-29 11:16 ` Marc-André Lureau
  2026-05-29 13:34   ` Fabiano Rosas
  31 siblings, 1 reply; 39+ messages in thread
From: Marc-André Lureau @ 2026-05-29 11:16 UTC (permalink / raw)
  To: qemu-devel
  Cc: Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Fabiano Rosas,
	Laurent Vivier, Alex Williamson, Cédric Le Goater,
	Peter Maydell, Akihiko Odaki, Marc-André Lureau

Add a qtest that verifies display consoles are dynamically added and
removed over D-Bus when a bochs-display device is hotplugged and
unplugged on a q35 machine.

The test plugs device_add a bochs-display, waits for the DEVICE_ADDED
QMP event, and checks that the D-Bus VM interface reports a second
console. It then device_del it, forces a system reset
(q35 removal is ACPI-based and needs guest cooperation qtest cannot
provide), waits for DEVICE_DELETED, and checks the console count again.

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
 tests/qtest/dbus-display-test.c | 101 +++++++++++++++++++++++++++++++++++++++-
 1 file changed, 99 insertions(+), 2 deletions(-)

diff --git a/tests/qtest/dbus-display-test.c b/tests/qtest/dbus-display-test.c
index 5773776cad5..7838ce7323f 100644
--- a/tests/qtest/dbus-display-test.c
+++ b/tests/qtest/dbus-display-test.c
@@ -7,6 +7,8 @@
 #include <gio/gio.h>
 #include <gio/gunixfdlist.h>
 #include "libqtest.h"
+#include "qobject/qdict.h"
+#include "qobject/qstring.h"
 #include "ui/dbus-display1.h"
 
 static GDBusConnection*
@@ -38,11 +40,11 @@ test_dbus_p2p_from_fd(int fd)
 }
 
 static void
-test_setup(QTestState **qts, GDBusConnection **conn)
+test_setup_args(QTestState **qts, GDBusConnection **conn, const char *args)
 {
     int pair[2];
 
-    *qts = qtest_init("-display dbus,p2p=yes -name dbus-test");
+    *qts = qtest_init(args);
 
     g_assert_cmpint(qemu_socketpair(AF_UNIX, SOCK_STREAM, 0, pair), ==, 0);
 
@@ -52,6 +54,12 @@ test_setup(QTestState **qts, GDBusConnection **conn)
     g_dbus_connection_start_message_processing(*conn);
 }
 
+static void
+test_setup(QTestState **qts, GDBusConnection **conn)
+{
+    test_setup_args(qts, conn, "-display dbus,p2p=yes -name dbus-test");
+}
+
 static void
 test_dbus_display_vm(void)
 {
@@ -360,6 +368,92 @@ test_dbus_display_keyboard(void)
     qtest_quit(qts);
 }
 
+static gsize
+get_console_ids_count(GDBusConnection *conn)
+{
+    g_autoptr(GError) err = NULL;
+    g_autoptr(QemuDBusDisplay1VMProxy) vm = NULL;
+    GVariant *console_ids;
+    gsize n_ids = 0;
+
+    vm = QEMU_DBUS_DISPLAY1_VM_PROXY(
+        qemu_dbus_display1_vm_proxy_new_sync(
+            conn,
+            G_DBUS_PROXY_FLAGS_NONE,
+            NULL,
+            DBUS_DISPLAY1_ROOT "/VM",
+            NULL,
+            &err));
+    g_assert_no_error(err);
+
+    console_ids = qemu_dbus_display1_vm_get_console_ids(
+        QEMU_DBUS_DISPLAY1_VM(vm));
+    if (console_ids) {
+        n_ids = g_variant_n_children(console_ids);
+    }
+    return n_ids;
+}
+
+static void
+wait_device_event(QTestState *qts, const char *event_name, const char *id)
+{
+    QDict *resp, *data;
+    QString *qstr;
+
+    for (;;) {
+        resp = qtest_qmp_eventwait_ref(qts, event_name);
+        data = qdict_get_qdict(resp, "data");
+        if (!data || !qdict_get(data, "device")) {
+            qobject_unref(resp);
+            continue;
+        }
+        qstr = qobject_to(QString, qdict_get(data, "device"));
+        if (!strcmp(qstring_get_str(qstr), id)) {
+            qobject_unref(resp);
+            break;
+        }
+        qobject_unref(resp);
+    }
+}
+
+static void
+test_dbus_display_hotplug(void)
+{
+    g_autoptr(GDBusConnection) conn = NULL;
+    QTestState *qts = NULL;
+    gsize n;
+
+    test_setup_args(&qts, &conn,
+        "-machine q35"
+        " -device pcie-root-port,id=rp0"
+        " -display dbus,p2p=yes"
+        " -name dbus-test");
+
+    n = get_console_ids_count(conn);
+    g_assert_cmpuint(n, ==, 1);
+
+    qtest_qmp_device_add(qts, "bochs-display", "bochs0",
+                         "{'bus': 'rp0'}");
+
+    n = get_console_ids_count(conn);
+    g_assert_cmpuint(n, ==, 2);
+
+    /*
+     * On q35, PCI device removal is ACPI-based and requires guest
+     * acknowledgement. Since qtest has no guest OS, issue the delete
+     * request and force removal via system reset.
+     */
+    qtest_qmp_device_del_send(qts, "bochs0");
+    qtest_system_reset_nowait(qts);
+    wait_device_event(qts, "DEVICE_DELETED", "bochs0");
+
+    n = get_console_ids_count(conn);
+    g_assert_cmpuint(n, ==, 1);
+
+    g_clear_object(&conn);
+    qtest_quit(qts);
+}
+
 int
 main(int argc, char **argv)
 {
@@ -369,6 +463,9 @@ main(int argc, char **argv)
     qtest_add_data_func("/dbus-display/console", GINT_TO_POINTER(false), test_dbus_display_console);
     qtest_add_data_func("/dbus-display/console/map", GINT_TO_POINTER(true), test_dbus_display_console);
     qtest_add_func("/dbus-display/keyboard", test_dbus_display_keyboard);
+    if (qtest_has_machine("q35") && qtest_has_device("bochs-display")) {
+        qtest_add_func("/dbus-display/hotplug", test_dbus_display_hotplug);
+    }
 
     return g_test_run();
 }

-- 
2.54.0



^ permalink raw reply related	[flat|nested] 39+ messages in thread

* Re: [PATCH 19/32] ui/dbus: implement display cleanup
  2026-05-29 11:16 ` [PATCH 19/32] ui/dbus: " Marc-André Lureau
@ 2026-05-29 12:17   ` Akihiko Odaki
  0 siblings, 0 replies; 39+ messages in thread
From: Akihiko Odaki @ 2026-05-29 12:17 UTC (permalink / raw)
  To: Marc-André Lureau, qemu-devel
  Cc: Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Fabiano Rosas,
	Laurent Vivier, Alex Williamson, Cédric Le Goater,
	Peter Maydell

On 2026/05/29 20:16, Marc-André Lureau wrote:
> Add dbus_cleanup() to unparent the D-Bus display object, ensuring
> proper teardown before user_creatable_cleanup() runs.

"[PATCH 07/32] ui/dbus: remove mouse handler on dispose" removes the 
mouse handler, but the LED event handler still remains.

Regards,
Akihiko Odaki

> 
> Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
> ---
>   ui/dbus-console.c |  3 +++
>   ui/dbus.c         | 13 +++++++++++++
>   2 files changed, 16 insertions(+)
> 
> diff --git a/ui/dbus-console.c b/ui/dbus-console.c
> index 0813a08f85e..0048951a7ab 100644
> --- a/ui/dbus-console.c
> +++ b/ui/dbus-console.c
> @@ -151,6 +151,9 @@ dbus_display_console_dispose(GObject *object)
>       DBusDisplayConsole *ddc = DBUS_DISPLAY_CONSOLE(object);
>   
>       qemu_console_unregister_listener(&ddc->dcl);
> +    if (ddc->dcl.con) {
> +        qemu_console_set_display_gl_ctx(ddc->dcl.con, NULL);
> +    }
>       qemu_remove_mouse_mode_change_notifier(&ddc->mouse_mode_notifier);
>       g_clear_object(&ddc->iface_touch);
>       g_clear_object(&ddc->iface_mouse);
> diff --git a/ui/dbus.c b/ui/dbus.c
> index e02a94df2f3..b23cb44c535 100644
> --- a/ui/dbus.c
> +++ b/ui/dbus.c
> @@ -615,10 +615,23 @@ static const TypeInfo dbus_display_info = {
>       }
>   };
>   
> +static void
> +dbus_cleanup(void)
> +{
> +    Object *o;
> +
> +    o = object_resolve_path_component(object_get_objects_root(),
> +                                      "dbus-display");
> +    if (o) {
> +        object_unparent(o);
> +    }
> +}
> +
>   static QemuDisplay qemu_display_dbus = {
>       .type       = DISPLAY_TYPE_DBUS,
>       .early_init = early_dbus_init,
>       .init       = dbus_init,
> +    .cleanup    = dbus_cleanup,
>       .vc         = "vc",
>   };
>   
> 



^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH 24/32] ui/gtk: convert VirtualConsole storage from fixed array to GPtrArray
  2026-05-29 11:16 ` [PATCH 24/32] ui/gtk: convert VirtualConsole storage from fixed array to GPtrArray Marc-André Lureau
@ 2026-05-29 12:21   ` Akihiko Odaki
  0 siblings, 0 replies; 39+ messages in thread
From: Akihiko Odaki @ 2026-05-29 12:21 UTC (permalink / raw)
  To: Marc-André Lureau, qemu-devel
  Cc: Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Fabiano Rosas,
	Laurent Vivier, Alex Williamson, Cédric Le Goater,
	Peter Maydell

On 2026/05/29 20:16, Marc-André Lureau wrote:
> Replace the fixed-size vc[MAX_VCS] with GPtrArray.
> 
> This is a preparatory refactoring for console hotplug support, which
> needs to add/remove VCs dynamically.
> 
> Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
> ---
>   include/ui/gtk.h |   3 +-
>   ui/gtk.c         | 102 +++++++++++++++++++++++++++++++++++++++++++------------
>   2 files changed, 81 insertions(+), 24 deletions(-)
> 
> diff --git a/include/ui/gtk.h b/include/ui/gtk.h
> index 5156a049509..4d54de97ea7 100644
> --- a/include/ui/gtk.h
> +++ b/include/ui/gtk.h
> @@ -118,8 +118,7 @@ struct GtkDisplayState {
>       GtkWidget *grab_item;
>       GtkWidget *grab_on_hover_item;
>   
> -    int nb_vcs;
> -    VirtualConsole vc[MAX_VCS];
> +    GPtrArray *vcs;
>   
>       GtkWidget *show_tabs_item;
>       GtkWidget *untabify_item;
> diff --git a/ui/gtk.c b/ui/gtk.c
> index c26dc83ac3e..141cb69d494 100644
> --- a/ui/gtk.c
> +++ b/ui/gtk.c
> @@ -150,8 +150,8 @@ static VirtualConsole *gd_vc_find_by_menu(GtkDisplayState *s)
>       VirtualConsole *vc;
>       gint i;
>   
> -    for (i = 0; i < s->nb_vcs; i++) {
> -        vc = &s->vc[i];
> +    for (i = 0; i < s->vcs->len; i++) {
> +        vc = g_ptr_array_index(s->vcs, i);
>           if (gtk_check_menu_item_get_active
>               (GTK_CHECK_MENU_ITEM(vc->menu_item))) {
>               return vc;
> @@ -165,8 +165,11 @@ static VirtualConsole *gd_vc_find_by_page(GtkDisplayState *s, gint page)
>       VirtualConsole *vc;
>       gint i, p;
>   
> -    for (i = 0; i < s->nb_vcs; i++) {
> -        vc = &s->vc[i];
> +    if (!s->vcs) {
> +        return NULL;
> +    }
> +    for (i = 0; i < s->vcs->len; i++) {
> +        vc = g_ptr_array_index(s->vcs, i);
>           p = gtk_notebook_page_num(GTK_NOTEBOOK(s->notebook), vc->tab_item);
>           if (p == page) {
>               return vc;
> @@ -247,8 +250,8 @@ static void gd_update_caption(GtkDisplayState *s)
>       gtk_window_set_title(GTK_WINDOW(s->window), title);
>       g_free(title);
>   
> -    for (i = 0; i < s->nb_vcs; i++) {
> -        VirtualConsole *vc = &s->vc[i];
> +    for (i = 0; i < s->vcs->len; i++) {
> +        VirtualConsole *vc = g_ptr_array_index(s->vcs, i);
>   
>           if (!vc->window) {
>               continue;
> @@ -357,7 +360,7 @@ static void gtk_release_modifiers(GtkDisplayState *s)
>   {
>       VirtualConsole *vc = gd_vc_find_current(s);
>   
> -    if (vc->type != GD_VC_GFX ||
> +    if (!vc || vc->type != GD_VC_GFX ||
>           !qemu_console_is_graphic(vc->gfx.dcl.con)) {
>           return;
>       }
> @@ -702,8 +705,8 @@ static void gd_mouse_mode_change(Notifier *notify, void *data)
>               gd_ungrab_pointer(s);
>           }
>       }
> -    for (i = 0; i < s->nb_vcs; i++) {
> -        VirtualConsole *vc = &s->vc[i];
> +    for (i = 0; i < s->vcs->len; i++) {
> +        VirtualConsole *vc = g_ptr_array_index(s->vcs, i);
>           gd_update_cursor(vc);
>       }
>   }
> @@ -2114,9 +2117,10 @@ static void gd_vcs_init(GtkDisplayState *s, GSList *group,
>       int i;
>   
>       for (i = 0; i < nb_vcs; i++) {
> -        VirtualConsole *vc = &s->vc[s->nb_vcs];
> -        group = gd_vc_vte_init(s, vc, vcs[i], s->nb_vcs, group, view_menu);
> -        s->nb_vcs++;
> +        VirtualConsole *vc = g_new0(VirtualConsole, 1);
> +        g_ptr_array_add(s->vcs, vc);
> +        group = gd_vc_vte_init(s, vc, vcs[i], s->vcs->len - 1,
> +                               group, view_menu);
>       }
>   }
>   #endif /* CONFIG_VTE */
> @@ -2441,13 +2445,14 @@ static GtkWidget *gd_create_menu_view(GtkDisplayState *s, DisplayOptions *opts)
>   
>       /* gfx */
>       for (vc = 0;; vc++) {
> +        VirtualConsole *v;
>           con = qemu_console_lookup_by_index(vc);
>           if (!con) {
>               break;
>           }
> -        group = gd_vc_gfx_init(s, &s->vc[vc], con,
> -                               vc, group, view_menu);
> -        s->nb_vcs++;
> +        v = g_new0(VirtualConsole, 1);
> +        g_ptr_array_add(s->vcs, v);
> +        group = gd_vc_gfx_init(s, v, con, vc, group, view_menu);
>       }
>   
>   #if defined(CONFIG_VTE)
> @@ -2505,6 +2510,58 @@ static void gd_create_menus(GtkDisplayState *s, DisplayOptions *opts)
>   }
>   
>   
> +static void gd_vc_free(void *p)
> +{
> +    VirtualConsole *vc = p;
> +
> +    switch (vc->type) {
> +    case GD_VC_GFX:
> +        qemu_console_unregister_listener(&vc->gfx.dcl);
> +#if defined(CONFIG_OPENGL)
> +        if (display_opengl) {
> +            qemu_console_set_display_gl_ctx(vc->gfx.dcl.con, NULL);
> +        }
> +        if (vc->gfx.esurface) {
> +            eglDestroySurface(qemu_egl_display, vc->gfx.esurface);
> +        }
> +        if (vc->gfx.ectx) {
> +            eglDestroyContext(qemu_egl_display, vc->gfx.ectx);
> +        }
> +        if (vc->gfx.gls) {
> +            surface_gl_destroy_texture(vc->gfx.gls, vc->gfx.ds);
> +            qemu_gl_fini_shader(vc->gfx.gls);
> +        }
> +        egl_fb_destroy(&vc->gfx.guest_fb);
> +        egl_fb_destroy(&vc->gfx.win_fb);
> +        egl_fb_destroy(&vc->gfx.cursor_fb);
> +#endif
> +        qkbd_state_free(vc->gfx.kbd);
> +        if (vc->gfx.surface) {
> +            cairo_surface_destroy(vc->gfx.surface);
> +        }
> +        if (vc->gfx.convert) {
> +            pixman_image_unref(vc->gfx.convert);
> +        }
> +        break;
> +#if defined(CONFIG_VTE)
> +    case GD_VC_VTE:
> +        fifo8_destroy(&vc->vte.out_fifo);
> +        break;
> +#endif

GCC emits a warning if !defined(CONFIG_VTE) because this doesn't handle 
GD_VC_VTE while the enum value is still present.

Regards,
Akihiko Odaki

> +    }
> +
> +    if (vc->window) {
> +        gtk_widget_destroy(vc->window);
> +    } else if (vc->tab_item) {
> +        gtk_widget_destroy(vc->tab_item);
> +    }
> +    if (vc->menu_item) {
> +        gtk_widget_destroy(vc->menu_item);
> +    }
> +    g_free(vc->label);
> +    g_free(vc);
> +}
> +
>   static GtkDisplayState *gtk_display_state;
>   static gboolean gtkinit;
>   
> @@ -2525,6 +2582,7 @@ static void gtk_display_init(DisplayState *ds, DisplayOptions *opts)
>       assert(opts->type == DISPLAY_TYPE_GTK);
>       s = g_malloc0(sizeof(*s));
>       gtk_display_state = s;
> +    s->vcs = g_ptr_array_new_with_free_func(gd_vc_free);
>       s->opts = opts;
>   
>       theme = gtk_icon_theme_get_default();
> @@ -2582,13 +2640,10 @@ static void gtk_display_init(DisplayState *ds, DisplayOptions *opts)
>   
>       gtk_widget_show_all(s->window);
>   
> -    for (idx = 0;; idx++) {
> -        QemuConsole *con = qemu_console_lookup_by_index(idx);
> -        if (!con) {
> -            break;
> -        }
> -        if (s->vc[idx].type == GD_VC_GFX) {
> -            gtk_widget_realize(s->vc[idx].gfx.drawing_area);
> +    for (idx = 0; idx < s->vcs->len; idx++) {
> +        VirtualConsole *v = g_ptr_array_index(s->vcs, idx);
> +        if (v->type == GD_VC_GFX) {
> +            gtk_widget_realize(v->gfx.drawing_area);
>           }
>       }
>   
> @@ -2693,6 +2748,9 @@ static void gtk_display_cleanup(void)
>   
>       qemu_remove_mouse_mode_change_notifier(&s->mouse_mode_notifier);
>       gd_clipboard_cleanup(s);
> +    g_signal_handlers_disconnect_by_func(s->notebook,
> +                                         G_CALLBACK(gd_change_page), s);
> +    g_clear_pointer(&s->vcs, g_ptr_array_unref);
>       g_clear_pointer(&s->window, gtk_widget_destroy);
>       g_clear_object(&s->null_cursor);
>       g_clear_pointer(&gtk_display_state, g_free);
> 



^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH 03/32] irq: add per-IRQ observer to fix qemu_irq_intercept_in leak
  2026-05-29 11:16 ` [PATCH 03/32] irq: add per-IRQ observer to fix qemu_irq_intercept_in leak Marc-André Lureau
@ 2026-05-29 13:27   ` Fabiano Rosas
  2026-05-29 13:44   ` Peter Maydell
  1 sibling, 0 replies; 39+ messages in thread
From: Fabiano Rosas @ 2026-05-29 13:27 UTC (permalink / raw)
  To: Marc-André Lureau, qemu-devel
  Cc: Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Laurent Vivier,
	Alex Williamson, Cédric Le Goater, Peter Maydell,
	Akihiko Odaki, Marc-André Lureau

Marc-André Lureau <marcandre.lureau@redhat.com> writes:

> qemu_irq_intercept_in() saves original IRQ handlers by allocating
> new QOM objects, which are never freed. On a PC machine, this leaks
> IRQ objects (one per IOAPIC pin) on every qtest run.
>
> Rather than tracking allocations to free later, avoid them: add an
> "observer" field to IRQState, called by qemu_set_irq() after the
> real handler. Interception sets the observer instead of rewriting
> handlers, so there's nothing to save and nothing to leak.
>
> Fix qemu_notirq() to route through qemu_set_irq() so inverted IRQs
> trigger observers too. Drop the LSan suppression.
>
> Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
> ---
>  include/hw/core/irq.h         |  1 +
>  hw/core/irq.c                 | 10 +++++-----
>  system/qtest.c                |  3 ---
>  scripts/lsan_suppressions.txt |  8 --------
>  4 files changed, 6 insertions(+), 16 deletions(-)
>
> diff --git a/include/hw/core/irq.h b/include/hw/core/irq.h
> index 291fdd67df4..93d5710a73e 100644
> --- a/include/hw/core/irq.h
> +++ b/include/hw/core/irq.h
> @@ -14,6 +14,7 @@ struct IRQState {
>      qemu_irq_handler handler;
>      void *opaque;
>      int n;
> +    qemu_irq_handler observer;
>  };
>  
>  void qemu_set_irq(qemu_irq irq, int level);
> diff --git a/hw/core/irq.c b/hw/core/irq.c
> index 106805e2417..fa11e9bc0aa 100644
> --- a/hw/core/irq.c
> +++ b/hw/core/irq.c
> @@ -32,6 +32,9 @@ void qemu_set_irq(qemu_irq irq, int level)
>          return;
>  
>      irq->handler(irq->opaque, irq->n, level);
> +    if (unlikely(irq->observer)) {
> +        irq->observer(irq->opaque, irq->n, level);
> +    }
>  }
>  
>  static void init_irq_fields(IRQState *irq, qemu_irq_handler handler,
> @@ -111,7 +114,7 @@ static void qemu_notirq(void *opaque, int line, int level)
>  {
>      IRQState *irq = opaque;
>  
> -    irq->handler(irq->opaque, irq->n, !level);
> +    qemu_set_irq(irq, !level);
>  }
>  
>  qemu_irq qemu_irq_invert(qemu_irq irq)
> @@ -124,11 +127,8 @@ qemu_irq qemu_irq_invert(qemu_irq irq)
>  void qemu_irq_intercept_in(qemu_irq *gpio_in, qemu_irq_handler handler, int n)
>  {
>      int i;
> -    qemu_irq *old_irqs = qemu_allocate_irqs(NULL, NULL, n);
>      for (i = 0; i < n; i++) {
> -        *old_irqs[i] = *gpio_in[i];
> -        gpio_in[i]->handler = handler;
> -        gpio_in[i]->opaque = &old_irqs[i];
> +        gpio_in[i]->observer = handler;
>      }
>  }
>  
> diff --git a/system/qtest.c b/system/qtest.c
> index a79d10d1361..b359a3e1c84 100644
> --- a/system/qtest.c
> +++ b/system/qtest.c
> @@ -324,9 +324,6 @@ void qtest_sendf(CharFrontend *chr, const char *fmt, ...)
>  
>  static void qtest_irq_handler(void *opaque, int n, int level)
>  {
> -    qemu_irq old_irq = *(qemu_irq *)opaque;
> -    qemu_set_irq(old_irq, level);
> -
>      if (irq_levels[n] != level) {
>          CharFrontend *chr = &qtest->qtest_chr;
>          irq_levels[n] = level;
> diff --git a/scripts/lsan_suppressions.txt b/scripts/lsan_suppressions.txt
> index f88bbab18b8..30256bc6d01 100644
> --- a/scripts/lsan_suppressions.txt
> +++ b/scripts/lsan_suppressions.txt
> @@ -16,11 +16,3 @@ leak:/lib64/libxkbcommon.so.0
>  # https://github.com/GNOME/glib/blob/main/tools/glib.supp
>  # This avoids false positive leak reports for the qga-ssh-test.
>  leak:g_set_user_dirs
> -
> -# qemu_irq_intercept_in is only used by the qtest harness, and
> -# its API inherently involves a leak.
> -# While we could keep track of the old IRQ data structure
> -# in order to free it, it doesn't seem very important to fix
> -# since it is only used by the qtest test harness.
> -# Just ignore the leak, at least for the moment.
> -leak:qemu_irq_intercept_in

Reviewed-by: Fabiano Rosas <farosas@suse.de>


^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH 32/32] tests/qtest: add D-Bus display hotplug test
  2026-05-29 11:16 ` [PATCH 32/32] tests/qtest: add D-Bus display hotplug test Marc-André Lureau
@ 2026-05-29 13:34   ` Fabiano Rosas
  0 siblings, 0 replies; 39+ messages in thread
From: Fabiano Rosas @ 2026-05-29 13:34 UTC (permalink / raw)
  To: Marc-André Lureau, qemu-devel
  Cc: Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Laurent Vivier,
	Alex Williamson, Cédric Le Goater, Peter Maydell,
	Akihiko Odaki, Marc-André Lureau

Marc-André Lureau <marcandre.lureau@redhat.com> writes:

> Add a qtest that verifies display consoles are dynamically added and
> removed over D-Bus when a bochs-display device is hotplugged and
> unplugged on a q35 machine.
>
> The test plugs device_add a bochs-display, waits for the DEVICE_ADDED
> QMP event, and checks that the D-Bus VM interface reports a second
> console. It then device_del it, forces a system reset
> (q35 removal is ACPI-based and needs guest cooperation qtest cannot
> provide), waits for DEVICE_DELETED, and checks the console count again.
>
> Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
> ---
>  tests/qtest/dbus-display-test.c | 101 +++++++++++++++++++++++++++++++++++++++-
>  1 file changed, 99 insertions(+), 2 deletions(-)
>
> diff --git a/tests/qtest/dbus-display-test.c b/tests/qtest/dbus-display-test.c
> index 5773776cad5..7838ce7323f 100644
> --- a/tests/qtest/dbus-display-test.c
> +++ b/tests/qtest/dbus-display-test.c
> @@ -7,6 +7,8 @@
>  #include <gio/gio.h>
>  #include <gio/gunixfdlist.h>
>  #include "libqtest.h"
> +#include "qobject/qdict.h"
> +#include "qobject/qstring.h"
>  #include "ui/dbus-display1.h"
>  
>  static GDBusConnection*
> @@ -38,11 +40,11 @@ test_dbus_p2p_from_fd(int fd)
>  }
>  
>  static void
> -test_setup(QTestState **qts, GDBusConnection **conn)
> +test_setup_args(QTestState **qts, GDBusConnection **conn, const char *args)
>  {
>      int pair[2];
>  
> -    *qts = qtest_init("-display dbus,p2p=yes -name dbus-test");
> +    *qts = qtest_init(args);
>  
>      g_assert_cmpint(qemu_socketpair(AF_UNIX, SOCK_STREAM, 0, pair), ==, 0);
>  
> @@ -52,6 +54,12 @@ test_setup(QTestState **qts, GDBusConnection **conn)
>      g_dbus_connection_start_message_processing(*conn);
>  }
>  
> +static void
> +test_setup(QTestState **qts, GDBusConnection **conn)
> +{
> +    test_setup_args(qts, conn, "-display dbus,p2p=yes -name dbus-test");
> +}
> +
>  static void
>  test_dbus_display_vm(void)
>  {
> @@ -360,6 +368,92 @@ test_dbus_display_keyboard(void)
>      qtest_quit(qts);
>  }
>  
> +static gsize
> +get_console_ids_count(GDBusConnection *conn)
> +{
> +    g_autoptr(GError) err = NULL;
> +    g_autoptr(QemuDBusDisplay1VMProxy) vm = NULL;
> +    GVariant *console_ids;
> +    gsize n_ids = 0;
> +
> +    vm = QEMU_DBUS_DISPLAY1_VM_PROXY(
> +        qemu_dbus_display1_vm_proxy_new_sync(
> +            conn,
> +            G_DBUS_PROXY_FLAGS_NONE,
> +            NULL,
> +            DBUS_DISPLAY1_ROOT "/VM",
> +            NULL,
> +            &err));
> +    g_assert_no_error(err);
> +
> +    console_ids = qemu_dbus_display1_vm_get_console_ids(
> +        QEMU_DBUS_DISPLAY1_VM(vm));
> +    if (console_ids) {
> +        n_ids = g_variant_n_children(console_ids);
> +    }
> +    return n_ids;
> +}
> +
> +static void
> +wait_device_event(QTestState *qts, const char *event_name, const char *id)
> +{
> +    QDict *resp, *data;
> +    QString *qstr;
> +
> +    for (;;) {
> +        resp = qtest_qmp_eventwait_ref(qts, event_name);
> +        data = qdict_get_qdict(resp, "data");
> +        if (!data || !qdict_get(data, "device")) {
> +            qobject_unref(resp);
> +            continue;
> +        }
> +        qstr = qobject_to(QString, qdict_get(data, "device"));
> +        if (!strcmp(qstring_get_str(qstr), id)) {
> +            qobject_unref(resp);
> +            break;
> +        }
> +        qobject_unref(resp);
> +    }
> +}
> +
> +static void
> +test_dbus_display_hotplug(void)
> +{
> +    g_autoptr(GDBusConnection) conn = NULL;
> +    QTestState *qts = NULL;
> +    gsize n;
> +
> +    test_setup_args(&qts, &conn,
> +        "-machine q35"
> +        " -device pcie-root-port,id=rp0"
> +        " -display dbus,p2p=yes"
> +        " -name dbus-test");
> +
> +    n = get_console_ids_count(conn);
> +    g_assert_cmpuint(n, ==, 1);
> +
> +    qtest_qmp_device_add(qts, "bochs-display", "bochs0",
> +                         "{'bus': 'rp0'}");
> +
> +    n = get_console_ids_count(conn);
> +    g_assert_cmpuint(n, ==, 2);
> +
> +    /*
> +     * On q35, PCI device removal is ACPI-based and requires guest
> +     * acknowledgement. Since qtest has no guest OS, issue the delete
> +     * request and force removal via system reset.
> +     */
> +    qtest_qmp_device_del_send(qts, "bochs0");
> +    qtest_system_reset_nowait(qts);
> +    wait_device_event(qts, "DEVICE_DELETED", "bochs0");
> +
> +    n = get_console_ids_count(conn);
> +    g_assert_cmpuint(n, ==, 1);
> +
> +    g_clear_object(&conn);
> +    qtest_quit(qts);
> +}
> +
>  int
>  main(int argc, char **argv)
>  {
> @@ -369,6 +463,9 @@ main(int argc, char **argv)
>      qtest_add_data_func("/dbus-display/console", GINT_TO_POINTER(false), test_dbus_display_console);
>      qtest_add_data_func("/dbus-display/console/map", GINT_TO_POINTER(true), test_dbus_display_console);
>      qtest_add_func("/dbus-display/keyboard", test_dbus_display_keyboard);
> +    if (qtest_has_machine("q35") && qtest_has_device("bochs-display")) {
> +        qtest_add_func("/dbus-display/hotplug", test_dbus_display_hotplug);
> +    }
>  
>      return g_test_run();
>  }

Reviewed-by: Fabiano Rosas <farosas@suse.de>


^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH 03/32] irq: add per-IRQ observer to fix qemu_irq_intercept_in leak
  2026-05-29 11:16 ` [PATCH 03/32] irq: add per-IRQ observer to fix qemu_irq_intercept_in leak Marc-André Lureau
  2026-05-29 13:27   ` Fabiano Rosas
@ 2026-05-29 13:44   ` Peter Maydell
  2026-05-31  5:31     ` Marc-André Lureau
  1 sibling, 1 reply; 39+ messages in thread
From: Peter Maydell @ 2026-05-29 13:44 UTC (permalink / raw)
  To: Marc-André Lureau
  Cc: qemu-devel, Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Fabiano Rosas,
	Laurent Vivier, Alex Williamson, Cédric Le Goater,
	Akihiko Odaki

On Fri, 29 May 2026 at 12:17, Marc-André Lureau
<marcandre.lureau@redhat.com> wrote:
>
> qemu_irq_intercept_in() saves original IRQ handlers by allocating
> new QOM objects, which are never freed. On a PC machine, this leaks
> IRQ objects (one per IOAPIC pin) on every qtest run.
>
> Rather than tracking allocations to free later, avoid them: add an
> "observer" field to IRQState, called by qemu_set_irq() after the
> real handler. Interception sets the observer instead of rewriting
> handlers, so there's nothing to save and nothing to leak.
>
> Fix qemu_notirq() to route through qemu_set_irq() so inverted IRQs
> trigger observers too. Drop the LSan suppression.
>
> Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
> ---
>  include/hw/core/irq.h         |  1 +
>  hw/core/irq.c                 | 10 +++++-----
>  system/qtest.c                |  3 ---
>  scripts/lsan_suppressions.txt |  8 --------
>  4 files changed, 6 insertions(+), 16 deletions(-)
>
> diff --git a/include/hw/core/irq.h b/include/hw/core/irq.h
> index 291fdd67df4..93d5710a73e 100644
> --- a/include/hw/core/irq.h
> +++ b/include/hw/core/irq.h
> @@ -14,6 +14,7 @@ struct IRQState {
>      qemu_irq_handler handler;
>      void *opaque;
>      int n;
> +    qemu_irq_handler observer;
>  };
>
>  void qemu_set_irq(qemu_irq irq, int level);
> diff --git a/hw/core/irq.c b/hw/core/irq.c
> index 106805e2417..fa11e9bc0aa 100644
> --- a/hw/core/irq.c
> +++ b/hw/core/irq.c
> @@ -32,6 +32,9 @@ void qemu_set_irq(qemu_irq irq, int level)
>          return;
>
>      irq->handler(irq->opaque, irq->n, level);
> +    if (unlikely(irq->observer)) {
> +        irq->observer(irq->opaque, irq->n, level);
> +    }
>  }

Hmm. "observer" semantics are probably nicer than "steal
the irq from the thing that would otherwise be connected to it",
but on the other hand this means that this purely test-relevant
thing is now in the code path for the common "signal an IRQ"
that we use everywhere...

-- PMM


^ permalink raw reply	[flat|nested] 39+ messages in thread

* Re: [PATCH 03/32] irq: add per-IRQ observer to fix qemu_irq_intercept_in leak
  2026-05-29 13:44   ` Peter Maydell
@ 2026-05-31  5:31     ` Marc-André Lureau
  0 siblings, 0 replies; 39+ messages in thread
From: Marc-André Lureau @ 2026-05-31  5:31 UTC (permalink / raw)
  To: Peter Maydell
  Cc: qemu-devel, Paolo Bonzini, Daniel P. Berrangé,
	Philippe Mathieu-Daudé, Pierrick Bouvier, Fabiano Rosas,
	Laurent Vivier, Alex Williamson, Cédric Le Goater,
	Akihiko Odaki

Hi

On Fri, May 29, 2026 at 5:44 PM Peter Maydell <peter.maydell@linaro.org> wrote:
>
> On Fri, 29 May 2026 at 12:17, Marc-André Lureau
> <marcandre.lureau@redhat.com> wrote:
> >
> > qemu_irq_intercept_in() saves original IRQ handlers by allocating
> > new QOM objects, which are never freed. On a PC machine, this leaks
> > IRQ objects (one per IOAPIC pin) on every qtest run.
> >
> > Rather than tracking allocations to free later, avoid them: add an
> > "observer" field to IRQState, called by qemu_set_irq() after the
> > real handler. Interception sets the observer instead of rewriting
> > handlers, so there's nothing to save and nothing to leak.
> >
> > Fix qemu_notirq() to route through qemu_set_irq() so inverted IRQs
> > trigger observers too. Drop the LSan suppression.
> >
> > Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
> > ---
> >  include/hw/core/irq.h         |  1 +
> >  hw/core/irq.c                 | 10 +++++-----
> >  system/qtest.c                |  3 ---
> >  scripts/lsan_suppressions.txt |  8 --------
> >  4 files changed, 6 insertions(+), 16 deletions(-)
> >
> > diff --git a/include/hw/core/irq.h b/include/hw/core/irq.h
> > index 291fdd67df4..93d5710a73e 100644
> > --- a/include/hw/core/irq.h
> > +++ b/include/hw/core/irq.h
> > @@ -14,6 +14,7 @@ struct IRQState {
> >      qemu_irq_handler handler;
> >      void *opaque;
> >      int n;
> > +    qemu_irq_handler observer;
> >  };
> >
> >  void qemu_set_irq(qemu_irq irq, int level);
> > diff --git a/hw/core/irq.c b/hw/core/irq.c
> > index 106805e2417..fa11e9bc0aa 100644
> > --- a/hw/core/irq.c
> > +++ b/hw/core/irq.c
> > @@ -32,6 +32,9 @@ void qemu_set_irq(qemu_irq irq, int level)
> >          return;
> >
> >      irq->handler(irq->opaque, irq->n, level);
> > +    if (unlikely(irq->observer)) {
> > +        irq->observer(irq->opaque, irq->n, level);
> > +    }
> >  }
>
> Hmm. "observer" semantics are probably nicer than "steal
> the irq from the thing that would otherwise be connected to it",
> but on the other hand this means that this purely test-relevant
> thing is now in the code path for the common "signal an IRQ"
> that we use everywhere...

Sure, but is this more intrusive or expansive than a trace at this point?



^ permalink raw reply	[flat|nested] 39+ messages in thread

end of thread, other threads:[~2026-05-31  5:32 UTC | newest]

Thread overview: 39+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-05-29 11:16 [PATCH 00/32] ui: better console hotplug support Marc-André Lureau
2026-05-29 11:16 ` [PATCH 01/32] ui/gtk: fix bad widget realize on non-GFX VC Marc-André Lureau
2026-05-29 11:16 ` [PATCH 02/32] build-sys: build with -fno-omit-frame-pointer with ASAN Marc-André Lureau
2026-05-29 11:16 ` [PATCH 03/32] irq: add per-IRQ observer to fix qemu_irq_intercept_in leak Marc-André Lureau
2026-05-29 13:27   ` Fabiano Rosas
2026-05-29 13:44   ` Peter Maydell
2026-05-31  5:31     ` Marc-André Lureau
2026-05-29 11:16 ` [PATCH 04/32] scripts/lsan_suppressions: suppress fontconfig leaks Marc-André Lureau
2026-05-29 11:16 ` [PATCH 05/32] vfio/pci: close display console during unrealize, not finalize Marc-André Lureau
2026-05-29 11:16 ` [PATCH 06/32] glib-compat: add fallback for g_clear_fd/g_autofd Marc-André Lureau
2026-05-29 11:16 ` [PATCH 07/32] ui/dbus: remove mouse handler on dispose Marc-André Lureau
2026-05-29 11:16 ` [PATCH 08/32] ui/qmp: keep a reference of console across yield Marc-André Lureau
2026-05-29 11:16 ` [PATCH 09/32] ui: stop ui timer when closing Marc-André Lureau
2026-05-29 11:16 ` [PATCH 10/32] ui/console: init gl_unblock_timer in qemu_console_init Marc-André Lureau
2026-05-29 11:16 ` [PATCH 11/32] ui/spice: remove dead spice_displays Marc-André Lureau
2026-05-29 11:16 ` [PATCH 12/32] ui/spice: add cleanup on shutdown Marc-André Lureau
2026-05-29 11:16 ` [PATCH 13/32] ui: add display cleanup infrastructure Marc-André Lureau
2026-05-29 11:16 ` [PATCH 14/32] ui/curses: implement display cleanup Marc-André Lureau
2026-05-29 11:16 ` [PATCH 15/32] ui/sdl2: " Marc-André Lureau
2026-05-29 11:16 ` [PATCH 16/32] ui/spice-app: " Marc-André Lureau
2026-05-29 11:16 ` [PATCH 17/32] ui/egl: implement display and EGL cleanup Marc-André Lureau
2026-05-29 11:16 ` [PATCH 18/32] ui/cocoa: implement display cleanup Marc-André Lureau
2026-05-29 11:16 ` [PATCH 19/32] ui/dbus: " Marc-André Lureau
2026-05-29 12:17   ` Akihiko Odaki
2026-05-29 11:16 ` [PATCH 20/32] ui/gtk: " Marc-André Lureau
2026-05-29 11:16 ` [PATCH 21/32] ui/console: add console event notifier infrastructure Marc-André Lureau
2026-05-29 11:16 ` [PATCH 22/32] ui/console: fire console ADDED/REMOVED notifications Marc-André Lureau
2026-05-29 11:16 ` [PATCH 23/32] ui/console-vc: fire " Marc-André Lureau
2026-05-29 11:16 ` [PATCH 24/32] ui/gtk: convert VirtualConsole storage from fixed array to GPtrArray Marc-André Lureau
2026-05-29 12:21   ` Akihiko Odaki
2026-05-29 11:16 ` [PATCH 25/32] ui/gtk: move global display settings out of per-console init Marc-André Lureau
2026-05-29 11:16 ` [PATCH 26/32] ui/gtk: fix tab re-insertion order on window close Marc-André Lureau
2026-05-29 11:16 ` [PATCH 27/32] ui/gtk: centralize console menu and shortcut management Marc-André Lureau
2026-05-29 11:16 ` [PATCH 28/32] ui/gtk: handle console hotplug/unplug events Marc-André Lureau
2026-05-29 11:16 ` [PATCH 29/32] ui/console: register console in QOM tree dynamically Marc-André Lureau
2026-05-29 11:16 ` [PATCH 30/32] ui/console: unregister console from QOM tree on close Marc-André Lureau
2026-05-29 11:16 ` [PATCH 31/32] ui/dbus: handle console hotplug/unplug events Marc-André Lureau
2026-05-29 11:16 ` [PATCH 32/32] tests/qtest: add D-Bus display hotplug test Marc-André Lureau
2026-05-29 13:34   ` Fabiano Rosas

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.