get:
Show a patch.

patch:
Update a patch.

put:
Update a patch.

GET /api/1.1/patches/2222111/?format=api
HTTP 200 OK
Allow: GET, PUT, PATCH, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "id": 2222111,
    "url": "http://patchwork.ozlabs.org/api/1.1/patches/2222111/?format=api",
    "web_url": "http://patchwork.ozlabs.org/project/qemu-devel/patch/20260410-qemu-vnc-v2-67-231416f76dc3@redhat.com/",
    "project": {
        "id": 14,
        "url": "http://patchwork.ozlabs.org/api/1.1/projects/14/?format=api",
        "name": "QEMU Development",
        "link_name": "qemu-devel",
        "list_id": "qemu-devel.nongnu.org",
        "list_email": "qemu-devel@nongnu.org",
        "web_url": "",
        "scm_url": "",
        "webscm_url": ""
    },
    "msgid": "<20260410-qemu-vnc-v2-67-231416f76dc3@redhat.com>",
    "date": "2026-04-10T19:19:29",
    "name": "[v2,67/67] tools/qemu-vnc: add standalone VNC server over D-Bus",
    "commit_ref": null,
    "pull_url": null,
    "state": "new",
    "archived": false,
    "hash": "844f5aaa7b4e5956e6e50077b842e4c4c24d95e4",
    "submitter": {
        "id": 66774,
        "url": "http://patchwork.ozlabs.org/api/1.1/people/66774/?format=api",
        "name": "Marc-André Lureau",
        "email": "marcandre.lureau@redhat.com"
    },
    "delegate": null,
    "mbox": "http://patchwork.ozlabs.org/project/qemu-devel/patch/20260410-qemu-vnc-v2-67-231416f76dc3@redhat.com/mbox/",
    "series": [
        {
            "id": 499494,
            "url": "http://patchwork.ozlabs.org/api/1.1/series/499494/?format=api",
            "web_url": "http://patchwork.ozlabs.org/project/qemu-devel/list/?series=499494",
            "date": "2026-04-10T19:18:23",
            "name": "ui: add standalone VNC server over D-Bus",
            "version": 2,
            "mbox": "http://patchwork.ozlabs.org/series/499494/mbox/"
        }
    ],
    "comments": "http://patchwork.ozlabs.org/api/patches/2222111/comments/",
    "check": "pending",
    "checks": "http://patchwork.ozlabs.org/api/patches/2222111/checks/",
    "tags": {},
    "headers": {
        "Return-Path": "<qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org>",
        "X-Original-To": "incoming@patchwork.ozlabs.org",
        "Delivered-To": "patchwork-incoming@legolas.ozlabs.org",
        "Authentication-Results": [
            "legolas.ozlabs.org;\n\tdkim=pass (1024-bit key;\n unprotected) header.d=redhat.com header.i=@redhat.com header.a=rsa-sha256\n header.s=mimecast20190719 header.b=XbU+og8C;\n\tdkim-atps=neutral",
            "legolas.ozlabs.org;\n spf=pass (sender SPF authorized) smtp.mailfrom=nongnu.org\n (client-ip=209.51.188.17; helo=lists.gnu.org;\n envelope-from=qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org;\n receiver=patchwork.ozlabs.org)"
        ],
        "Received": [
            "from lists.gnu.org (lists1p.gnu.org [209.51.188.17])\n\t(using TLSv1.2 with cipher ECDHE-ECDSA-AES256-GCM-SHA384 (256/256 bits))\n\t(No client certificate requested)\n\tby legolas.ozlabs.org (Postfix) with ESMTPS id 4fsmxr6LMzz1yGb\n\tfor <incoming@patchwork.ozlabs.org>; Sat, 11 Apr 2026 05:26:24 +1000 (AEST)",
            "from localhost ([::1] helo=lists1p.gnu.org)\n\tby lists.gnu.org with esmtp (Exim 4.90_1)\n\t(envelope-from <qemu-devel-bounces@nongnu.org>)\n\tid 1wBHUD-0000Vb-PQ; Fri, 10 Apr 2026 15:25:49 -0400",
            "from eggs.gnu.org ([2001:470:142:3::10])\n by lists1p.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)\n (Exim 4.90_1) (envelope-from <marcandre.lureau@redhat.com>)\n id 1wBHUB-0000Kl-GE\n for qemu-devel@nongnu.org; Fri, 10 Apr 2026 15:25:47 -0400",
            "from us-smtp-delivery-124.mimecast.com ([170.10.129.124])\n by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)\n (Exim 4.90_1) (envelope-from <marcandre.lureau@redhat.com>)\n id 1wBHU6-0003II-Gc\n for qemu-devel@nongnu.org; Fri, 10 Apr 2026 15:25:47 -0400",
            "from mx-prod-mc-01.mail-002.prod.us-west-2.aws.redhat.com\n (ec2-54-186-198-63.us-west-2.compute.amazonaws.com [54.186.198.63]) by\n relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3,\n cipher=TLS_AES_256_GCM_SHA384) id us-mta-635-cwiUQQBAMZOHgNdVN4igTw-1; Fri,\n 10 Apr 2026 15:25:39 -0400",
            "from mx-prod-int-01.mail-002.prod.us-west-2.aws.redhat.com\n (mx-prod-int-01.mail-002.prod.us-west-2.aws.redhat.com [10.30.177.4])\n (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits)\n key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest\n SHA256)\n (No client certificate requested)\n by mx-prod-mc-01.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTPS\n id AA8CB195609D\n for <qemu-devel@nongnu.org>; Fri, 10 Apr 2026 19:25:38 +0000 (UTC)",
            "from localhost (unknown [10.44.22.4])\n by mx-prod-int-01.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTP\n id 0D78C3000C16; Fri, 10 Apr 2026 19:25:34 +0000 (UTC)"
        ],
        "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com;\n s=mimecast20190719; t=1775849141;\n h=from:from:reply-to:subject:subject:date:date:message-id:message-id:\n to:to:cc:cc:mime-version:mime-version:content-type:content-type:\n content-transfer-encoding:content-transfer-encoding:\n in-reply-to:in-reply-to:references:references;\n bh=DsgbszMduye2UC15r6dJelAckDT/FxP81KIaolU5B+E=;\n b=XbU+og8CAQ6rMt83w7jMD5GErygt4MmuewtB70Ti+3viEp+mZ5HJbt9yctl+aagAHhTYpC\n mrhqWme1NDj8uINYCp6g7vOVJjn5KbrV+2VYlvvMNkflz5H4M7d4e21WLS/iIsRKITBDrc\n cWngW4M35OwQbR9nUZO1tEVRunkfqeQ=",
        "X-MC-Unique": "cwiUQQBAMZOHgNdVN4igTw-1",
        "X-Mimecast-MFC-AGG-ID": "cwiUQQBAMZOHgNdVN4igTw_1775849138",
        "From": "=?utf-8?q?Marc-Andr=C3=A9_Lureau?= <marcandre.lureau@redhat.com>",
        "Date": "Fri, 10 Apr 2026 23:19:29 +0400",
        "Subject": "[PATCH v2 67/67] tools/qemu-vnc: add standalone VNC server over D-Bus",
        "MIME-Version": "1.0",
        "Content-Type": "text/plain; charset=\"utf-8\"",
        "Content-Transfer-Encoding": "8bit",
        "Message-Id": "<20260410-qemu-vnc-v2-67-231416f76dc3@redhat.com>",
        "References": "<20260410-qemu-vnc-v2-0-231416f76dc3@redhat.com>",
        "In-Reply-To": "<20260410-qemu-vnc-v2-0-231416f76dc3@redhat.com>",
        "To": "qemu-devel@nongnu.org",
        "Cc": "=?utf-8?q?Marc-Andr=C3=A9_Lureau?= <marcandre.lureau@redhat.com>",
        "X-Developer-Signature": "v=1; a=openpgp-sha256; l=155773;\n i=marcandre.lureau@redhat.com; h=from:subject:message-id;\n bh=3zGQNq4A7DRtUjW9gFIJnsuMyEQldufLTpgjuyB/zdY=;\n b=owEBbQKS/ZANAwAKAdro4Ql1lpzlAcsmYgBp2U0XBEuiYqCmGLCAdF76DJ7NnAhACJxlHW0FZ\n 2lNIcoFXlqJAjMEAAEKAB0WIQSHqb2TP4fGBtJ29i3a6OEJdZac5QUCadlNFwAKCRDa6OEJdZac\n 5UQiEAChu5K+uyka5AFlqz4XQ+gLAVUSQaUUq42CFKmPkqmTgiYbIxOR4yrU7NMogWOJPcF+XFd\n IhUflrF5KoiHXi+9BU3jlKxC8Pd2Rbmdwvup8Gva9BGoil15wC3K6rHWtdlyt7v56iOrwU9CoGH\n jelGeeTJn/DXjGTD9soewswLTsLlWR1Vq6Afqe2mQJ9BRPvUOCM+V9cB0nRBgG+JuhK4S4VnOHE\n Ylru/Jp9Xxw+TyR+dnBb5mJOsPOVxCBfRwS++9V91P1BVQUWC8HhmBJJTkZzjv11lryBlm0sAVZ\n PglpUWSTkSeoVvt92nQqaT89qoMx8iRko6Ls2zSpyVPgfU1YJkMGkDG+UfHMmQvIzNKIBe5rOXg\n ysrFpxjDY1s+q58cwHtDwkitumQ5sGr7arvk3hVta60hb2i5OQYBXK5vKiQUd51HydlIBlbENPm\n OLFNeECIxrKRh1KpVp5aP9BiqZtLvKMjKoj6HrRPmec7MeS/P0//jxvG7NvYd3diQgEMdsjlXKD\n f8T65LE3yT40IHx9akieMBHVMjcw01Dzg4Z54eOBmQDVe4VDZ1YWjefH4r0AWRckdtRKYqauDyi\n CFb4rYWtWOY3YM486T99yQSDFmcLkI7XZDDCOvBQrnEvN23vDoqO2gEnd+cEpFg1Z23vsT/uyxJ\n ZwyLFQTz7UVMiBA==",
        "X-Developer-Key": "i=marcandre.lureau@redhat.com; a=openpgp;\n fpr=87A9BD933F87C606D276F62DDAE8E10975969CE5",
        "X-Scanned-By": "MIMEDefang 3.4.1 on 10.30.177.4",
        "Received-SPF": "pass client-ip=170.10.129.124;\n envelope-from=marcandre.lureau@redhat.com;\n helo=us-smtp-delivery-124.mimecast.com",
        "X-Spam_score_int": "7",
        "X-Spam_score": "0.7",
        "X-Spam_bar": "/",
        "X-Spam_report": "(0.7 / 5.0 requ) BAYES_00=-1.9, DKIMWL_WL_HIGH=-0.54,\n DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1,\n RCVD_IN_DNSWL_NONE=-0.0001, RCVD_IN_MSPIKE_H4=0.001, RCVD_IN_MSPIKE_WL=0.001,\n RCVD_IN_SBL_CSS=3.335, RCVD_IN_VALIDITY_RPBL_BLOCKED=0.001,\n RCVD_IN_VALIDITY_SAFE_BLOCKED=0.001, SPF_HELO_PASS=-0.001,\n SPF_PASS=-0.001 autolearn=no autolearn_force=no",
        "X-Spam_action": "no action",
        "X-BeenThere": "qemu-devel@nongnu.org",
        "X-Mailman-Version": "2.1.29",
        "Precedence": "list",
        "List-Id": "qemu development <qemu-devel.nongnu.org>",
        "List-Unsubscribe": "<https://lists.nongnu.org/mailman/options/qemu-devel>,\n <mailto:qemu-devel-request@nongnu.org?subject=unsubscribe>",
        "List-Archive": "<https://lists.nongnu.org/archive/html/qemu-devel>",
        "List-Post": "<mailto:qemu-devel@nongnu.org>",
        "List-Help": "<mailto:qemu-devel-request@nongnu.org?subject=help>",
        "List-Subscribe": "<https://lists.nongnu.org/mailman/listinfo/qemu-devel>,\n <mailto:qemu-devel-request@nongnu.org?subject=subscribe>",
        "Errors-To": "qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org",
        "Sender": "qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org"
    },
    "content": "Add a standalone VNC server binary that connects to a running QEMU\ninstance via the D-Bus display interface (org.qemu.Display1, via the bus\nor directly p2p). This allows serving a VNC display without compiling\nVNC support directly into the QEMU system emulator, and enables running\nthe VNC server as a separate process with independent lifecycle and\nprivilege domain.\n\nBuilt only when both VNC and D-Bus display support are enabled.\nIf we wanted to have qemu -vnc disabled, and qemu-vnc built, we would\nneed to split CONFIG_VNC. This is left as a future exercise.\n\nCurrent omissions include some QEMU VNC runtime features (better handled via\nrestart), legacy options, and Windows support.\n\nSigned-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>\n---\n MAINTAINERS                   |    5 +\n docs/conf.py                  |    3 +\n docs/interop/dbus-display.rst |    2 +\n docs/interop/dbus-vnc.rst     |   26 +\n docs/interop/index.rst        |    1 +\n docs/meson.build              |    1 +\n docs/tools/index.rst          |    1 +\n docs/tools/qemu-vnc.rst       |  222 +++++++\n meson.build                   |   17 +\n tools/qemu-vnc/qemu-vnc.h     |   46 ++\n tools/qemu-vnc/trace.h        |    4 +\n tests/qtest/dbus-vnc-test.c   | 1342 +++++++++++++++++++++++++++++++++++++++++\n tools/qemu-vnc/audio.c        |  307 ++++++++++\n tools/qemu-vnc/chardev.c      |  127 ++++\n tools/qemu-vnc/clipboard.c    |  378 ++++++++++++\n tools/qemu-vnc/console.c      |  168 ++++++\n tools/qemu-vnc/dbus.c         |  439 ++++++++++++++\n tools/qemu-vnc/display.c      |  456 ++++++++++++++\n tools/qemu-vnc/input.c        |  239 ++++++++\n tools/qemu-vnc/qemu-vnc.c     |  491 +++++++++++++++\n tools/qemu-vnc/stubs.c        |   62 ++\n tools/qemu-vnc/utils.c        |   59 ++\n meson_options.txt             |    2 +\n scripts/meson-buildoptions.sh |    3 +\n tests/dbus-daemon.sh          |   14 +-\n tests/qtest/meson.build       |   13 +\n tools/qemu-vnc/meson.build    |   26 +\n tools/qemu-vnc/qemu-vnc1.xml  |  174 ++++++\n tools/qemu-vnc/trace-events   |   20 +\n 29 files changed, 4645 insertions(+), 3 deletions(-)",
    "diff": "diff --git a/MAINTAINERS b/MAINTAINERS\nindex ad215eced84..9cddf898c77 100644\n--- a/MAINTAINERS\n+++ b/MAINTAINERS\n@@ -2825,6 +2825,11 @@ F: docs/interop/vhost-user-gpu.rst\n F: contrib/vhost-user-gpu\n F: hw/display/vhost-user-*\n \n+qemu-vnc:\n+M: Marc-André Lureau <marcandre.lureau@redhat.com>\n+S: Maintained\n+F: tools/qemu-vnc\n+\n Cirrus VGA\n M: Gerd Hoffmann <kraxel@redhat.com>\n S: Odd Fixes\ndiff --git a/docs/conf.py b/docs/conf.py\nindex f835904ba1e..7e35d2158d3 100644\n--- a/docs/conf.py\n+++ b/docs/conf.py\n@@ -333,6 +333,9 @@\n     ('tools/qemu-trace-stap', 'qemu-trace-stap',\n      'QEMU SystemTap trace tool',\n      [], 1),\n+    ('tools/qemu-vnc', 'qemu-vnc',\n+     'QEMU standalone VNC server',\n+     [], 1),\n ]\n man_make_section_directory = False\n \ndiff --git a/docs/interop/dbus-display.rst b/docs/interop/dbus-display.rst\nindex 8c6e8e0f5a8..87648e91dc0 100644\n--- a/docs/interop/dbus-display.rst\n+++ b/docs/interop/dbus-display.rst\n@@ -1,3 +1,5 @@\n+.. _dbus-display:\n+\n D-Bus display\n =============\n \ndiff --git a/docs/interop/dbus-vnc.rst b/docs/interop/dbus-vnc.rst\nnew file mode 100644\nindex 00000000000..d2b77978f63\n--- /dev/null\n+++ b/docs/interop/dbus-vnc.rst\n@@ -0,0 +1,26 @@\n+D-Bus VNC\n+=========\n+\n+The ``qemu-vnc`` standalone VNC server exposes a D-Bus interface for management\n+and monitoring of VNC connections.\n+\n+The service is available on the bus under the well-known name ``org.qemu.vnc``.\n+Objects are exported under ``/org/qemu/Vnc1/``.\n+\n+.. contents::\n+   :local:\n+   :depth: 1\n+\n+.. only:: sphinx4\n+\n+   .. dbus-doc:: tools/qemu-vnc/qemu-vnc1.xml\n+\n+.. only:: not sphinx4\n+\n+   .. warning::\n+      Sphinx 4 is required to build D-Bus documentation.\n+\n+      This is the content of ``tools/qemu-vnc/qemu-vnc1.xml``:\n+\n+   .. literalinclude:: ../../tools/qemu-vnc/qemu-vnc1.xml\n+      :language: xml\ndiff --git a/docs/interop/index.rst b/docs/interop/index.rst\nindex d830c5c4104..2cf3a8c9aa3 100644\n--- a/docs/interop/index.rst\n+++ b/docs/interop/index.rst\n@@ -13,6 +13,7 @@ are useful for making QEMU interoperate with other software.\n    dbus\n    dbus-vmstate\n    dbus-display\n+   dbus-vnc\n    live-block-operations\n    nbd\n    parallels\ndiff --git a/docs/meson.build b/docs/meson.build\nindex 7e54b01e6a0..c3e9fb05846 100644\n--- a/docs/meson.build\n+++ b/docs/meson.build\n@@ -54,6 +54,7 @@ if build_docs\n         'qemu-pr-helper.8': (have_tools ? 'man8' : ''),\n         'qemu-storage-daemon.1': (have_tools ? 'man1' : ''),\n         'qemu-trace-stap.1': (stap.found() ? 'man1' : ''),\n+        'qemu-vnc.1': (have_qemu_vnc ? 'man1' : ''),\n         'qemu.1': 'man1',\n         'qemu-block-drivers.7': 'man7',\n         'qemu-cpu-models.7': 'man7'\ndiff --git a/docs/tools/index.rst b/docs/tools/index.rst\nindex 1e88ae48cdc..868c3c4d9d8 100644\n--- a/docs/tools/index.rst\n+++ b/docs/tools/index.rst\n@@ -16,3 +16,4 @@ command line utilities and other standalone programs.\n    qemu-pr-helper\n    qemu-trace-stap\n    qemu-vmsr-helper\n+   qemu-vnc\ndiff --git a/docs/tools/qemu-vnc.rst b/docs/tools/qemu-vnc.rst\nnew file mode 100644\nindex 00000000000..a4de4a7d4f8\n--- /dev/null\n+++ b/docs/tools/qemu-vnc.rst\n@@ -0,0 +1,222 @@\n+.. _qemu-vnc:\n+\n+==========================\n+QEMU standalone VNC server\n+==========================\n+\n+Synopsis\n+--------\n+\n+**qemu-vnc** [*OPTION*]...\n+\n+Description\n+-----------\n+\n+``qemu-vnc`` is a standalone VNC server that connects to a running QEMU instance\n+via the D-Bus display interface (:ref:`dbus-display`). It serves the guest\n+display, input, audio, clipboard, and serial console chardevs over the VNC\n+protocol, allowing VNC clients to interact with the virtual machine without QEMU\n+itself binding a VNC socket.\n+\n+Options\n+-------\n+\n+.. program:: qemu-vnc\n+\n+.. option:: -h, --help\n+\n+  Display help and exit.\n+\n+.. option:: -V, --version\n+\n+  Print version information and exit.\n+\n+.. option:: -a ADDRESS, --dbus-address=ADDRESS\n+\n+  D-Bus address to connect to. When not specified, ``qemu-vnc`` connects to the\n+  session bus.\n+\n+.. option:: -p FD, --dbus-p2p-fd=FD\n+\n+  File descriptor of an inherited Unix socket for a peer-to-peer D-Bus\n+  connection to QEMU. This is mutually exclusive with ``--dbus-address`` and\n+  ``--bus-name``.\n+\n+.. option:: -n NAME, --bus-name=NAME\n+\n+  D-Bus bus name of the QEMU instance to connect to. The default is\n+  ``org.qemu``. When a custom ``--dbus-address`` is given without a bus name,\n+  peer-to-peer D-Bus is used.\n+\n+.. option:: --password\n+\n+  Require VNC password authentication from connecting clients. The password is\n+  set at runtime via the D-Bus ``SetPassword`` method (see\n+  :doc:`/interop/dbus-vnc`). Clients will not be able to connect until a\n+  password has been set.\n+\n+  This option is ignored when a systemd credential password is present, since\n+  password authentication is already enabled via ``password-secret`` in that\n+  case.\n+\n+.. option:: -l ADDR, --vnc-addr=ADDR\n+\n+  VNC listen address in the same format as the QEMU ``-vnc`` option (default\n+  ``localhost:0``, i.e. TCP port 5900).\n+\n+.. option:: -w ADDR, --websocket=ADDR\n+\n+  Enable WebSocket transport on the given address. *ADDR* can be a port number\n+  or an *address:port* pair.\n+\n+.. option:: -O OBJDEF, --object=OBJDEF\n+\n+  Create a QEMU user-creatable object. *OBJDEF* uses the same key=value syntax\n+  as the QEMU ``-object`` option. This option may be given multiple times. It is\n+  needed, for example, to create authorization objects referenced by\n+  ``--tls-authz``.\n+\n+.. option:: -t DIR, --tls-creds=DIR\n+\n+  Directory containing TLS x509 credentials (``ca-cert.pem``,\n+  ``server-cert.pem``, ``server-key.pem``). When specified, the VNC server\n+  requires TLS from connecting clients.\n+\n+.. option:: --tls-authz=ID\n+\n+  ID of a ``QAuthZ`` object previously created with ``--object`` for TLS client\n+  certificate authorization. When specified, the TLS credentials are created\n+  with ``verify-peer=yes`` so connecting clients must present a valid\n+  certificate. After the TLS handshake, the client certificate Distinguished\n+  Name is checked against the authorization object. This option requires\n+  ``--tls-creds``.\n+\n+.. option:: --sasl\n+\n+  Require that the client use SASL to authenticate with the VNC server. The\n+  exact choice of authentication method used is controlled from the system /\n+  user's SASL configuration file for the 'qemu' service. This is typically found\n+  in ``/etc/sasl2/qemu.conf``. If running QEMU as an unprivileged user, an\n+  environment variable ``SASL_CONF_PATH`` can be used to make it search\n+  alternate locations for the service config. While some SASL auth methods can\n+  also provide data encryption (eg GSSAPI), it is recommended that SASL always\n+  be combined with the 'tls' and 'x509' settings to enable use of SSL and server\n+  certificates. This ensures a data encryption preventing compromise of\n+  authentication credentials. See the :ref:`VNC security` section in the System\n+  Emulation Users Guide for details on using SASL authentication.\n+\n+.. option:: --sasl-authz=ID\n+\n+  ID of a ``QAuthZ`` object previously created with ``--object`` for SASL\n+  username authorization. After successful SASL authentication, the\n+  authenticated username is checked against the authorization object. If the\n+  check fails, the client is disconnected. This option requires ``--sasl``.\n+\n+.. option:: -s POLICY, --share=POLICY\n+\n+  Set display sharing policy. *POLICY* is one of ``allow-exclusive``,\n+  ``force-shared``, or ``ignore``.\n+\n+  ``allow-exclusive`` allows clients to ask for exclusive access. As suggested\n+  by the RFB spec this is implemented by dropping other connections. Connecting\n+  multiple clients in parallel requires all clients asking for a shared session\n+  (vncviewer: -shared switch). This is the default.\n+\n+  ``force-shared`` disables exclusive client access. Useful for shared desktop\n+  sessions, where you don't want someone forgetting to specify -shared\n+  disconnect everybody else.\n+\n+  ``ignore`` completely ignores the shared flag and allows everybody to connect\n+  unconditionally. Doesn't conform to the RFB spec but is traditional QEMU\n+  behavior.\n+\n+.. option:: -C NAME, --vt-chardev=NAME\n+\n+  Chardev type name to expose as a VNC text console. This option may be given\n+  multiple times to expose several chardevs. When not specified, the defaults\n+  ``org.qemu.console.serial.0`` and ``org.qemu.monitor.hmp.0`` are used.\n+\n+.. option:: -N, --no-vt\n+\n+  Do not expose any chardevs as text consoles. This overrides the default\n+  chardev list and any ``--vt-chardev`` options.\n+\n+.. option:: -k LAYOUT, --keyboard-layout=LAYOUT\n+\n+  Keyboard layout (e.g. ``en-us``). Passed through to the VNC server for\n+  key-code translation.\n+\n+.. option:: --lossy\n+\n+  Enable lossy compression methods (gradient, JPEG, ...). If this option is set,\n+  VNC client may receive lossy framebuffer updates depending on its encoding\n+  settings. Enabling this option can save a lot of bandwidth at the expense of\n+  quality.\n+\n+.. option:: --non-adaptive\n+\n+  Disable adaptive encodings. Adaptive encodings are enabled by default. An\n+  adaptive encoding will try to detect frequently updated screen regions, and\n+  send updates in these regions using a lossy encoding (like JPEG). This can be\n+  really helpful to save bandwidth when playing videos. Disabling adaptive\n+  encodings restores the original static behavior of encodings like Tight.\n+\n+.. option:: -T, --trace [[enable=]PATTERN][,events=FILE][,file=FILE]\n+\n+  .. include:: ../qemu-option-trace.rst.inc\n+\n+Examples\n+--------\n+\n+Start QEMU with the D-Bus display backend::\n+\n+    qemu-system-x86_64 -display dbus ...\n+\n+Then attach ``qemu-vnc``::\n+\n+    qemu-vnc\n+\n+A VNC client can now connect to ``localhost:5900``.\n+\n+To listen on a different port with TLS::\n+\n+    qemu-vnc --vnc-addr localhost:1 --tls-creds /etc/pki/qemu-vnc\n+\n+To require TLS with client certificate authorization::\n+\n+    qemu-vnc --object authz-list-file,id=auth0,filename=/etc/qemu/vnc.acl,refresh=on \\\n+             --tls-creds /etc/pki/qemu-vnc --tls-authz auth0\n+\n+To enable SASL authentication with TLS::\n+\n+    qemu-vnc --tls-creds /etc/pki/qemu-vnc --sasl\n+\n+VNC password authentication\n+----------------------------\n+\n+There are two ways to enable VNC password authentication:\n+\n+1. ``--password`` flag -- start ``qemu-vnc`` with ``--password`` and\n+   then set the password at runtime using the D-Bus ``SetPassword``\n+   method.  Clients will be rejected until a password is set.\n+\n+2. systemd credentials -- if the ``CREDENTIALS_DIRECTORY``\n+   environment variable is set (see :manpage:`systemd.exec(5)`) and\n+   contains a file named ``vnc-password``, the VNC server will use\n+   that file's contents as the password automatically.  The\n+   ``--password`` flag is not needed in this case.\n+\n+D-Bus interface\n+---------------\n+\n+``qemu-vnc`` exposes a D-Bus interface for management and monitoring of\n+VNC connections.  See :doc:`/interop/dbus-vnc` for the full interface\n+reference.\n+\n+See also\n+--------\n+\n+:manpage:`qemu(1)`,\n+:doc:`/interop/dbus-display`,\n+:doc:`/interop/dbus-vnc`,\n+`The RFB Protocol <https://github.com/rfbproto/rfbproto>`_\ndiff --git a/meson.build b/meson.build\nindex ab3e97eb9f4..78aa3d490ad 100644\n--- a/meson.build\n+++ b/meson.build\n@@ -2341,6 +2341,17 @@ dbus_display = get_option('dbus_display') \\\n            error_message: gdbus_codegen_error.format('-display dbus')) \\\n   .allowed()\n \n+have_qemu_vnc = get_option('qemu_vnc') \\\n+  .require(have_tools,\n+           error_message: 'qemu-vnc requires tools support') \\\n+  .require(dbus_display,\n+           error_message: 'qemu-vnc requires dbus-display support') \\\n+  .require(vnc.found(),\n+           error_message: 'qemu-vnc requires vnc support') \\\n+  .require(host_os != 'windows',\n+           error_message: 'qemu-vnc is not currently supported on Windows') \\\n+  .allowed()\n+\n have_virtfs = get_option('virtfs') \\\n     .require(host_os == 'linux' or host_os == 'darwin' or host_os == 'freebsd',\n              error_message: 'virtio-9p (virtfs) requires Linux or macOS or FreeBSD') \\\n@@ -3595,6 +3606,7 @@ trace_events_subdirs = [\n   'monitor',\n   'util',\n   'gdbstub',\n+  'tools/qemu-vnc',\n ]\n if have_linux_user\n   trace_events_subdirs += [ 'linux-user' ]\n@@ -4563,6 +4575,10 @@ if have_tools\n     subdir('contrib/ivshmem-client')\n     subdir('contrib/ivshmem-server')\n   endif\n+\n+  if have_qemu_vnc\n+    subdir('tools/qemu-vnc')\n+  endif\n endif\n \n if stap.found()\n@@ -4898,6 +4914,7 @@ if vnc.found()\n   summary_info += {'VNC SASL support':  sasl}\n   summary_info += {'VNC JPEG support':  jpeg}\n endif\n+summary_info += {'VNC D-Bus server (qemu-vnc)': have_qemu_vnc}\n summary_info += {'spice protocol support': spice_protocol}\n if spice_protocol.found()\n   summary_info += {'  spice server support': spice}\ndiff --git a/tools/qemu-vnc/qemu-vnc.h b/tools/qemu-vnc/qemu-vnc.h\nnew file mode 100644\nindex 00000000000..420d5f66d42\n--- /dev/null\n+++ b/tools/qemu-vnc/qemu-vnc.h\n@@ -0,0 +1,46 @@\n+/*\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+#ifndef CONTRIB_QEMU_VNC_H\n+#define CONTRIB_QEMU_VNC_H\n+\n+#include \"qemu/osdep.h\"\n+\n+#include <gio/gunixfdlist.h>\n+#include \"qemu/dbus.h\"\n+#include \"ui/console.h\"\n+#include \"ui/dbus-display1.h\"\n+\n+#define TEXT_COLS 80\n+#define TEXT_ROWS 24\n+#define TEXT_FONT_WIDTH  8\n+#define TEXT_FONT_HEIGHT 16\n+\n+\n+QemuTextConsole *qemu_vnc_text_console_new(const char *name,\n+                                           int fd, bool echo);\n+\n+void input_setup(QemuDBusDisplay1Keyboard *kbd,\n+                 QemuDBusDisplay1Mouse *mouse);\n+bool console_setup(GDBusConnection *bus, const char *bus_name,\n+                   const char *console_path);\n+QemuDBusDisplay1Keyboard *console_get_keyboard(QemuConsole *con);\n+QemuDBusDisplay1Mouse *console_get_mouse(QemuConsole *con);\n+\n+void audio_setup(GDBusObjectManager *manager);\n+void clipboard_setup(GDBusObjectManager *manager, GDBusConnection *bus);\n+void chardev_setup(const char * const *chardev_names,\n+                   GDBusObjectManager *manager);\n+\n+GThread *p2p_dbus_thread_new(int fd);\n+\n+void vnc_dbus_setup(GDBusConnection *bus);\n+void vnc_dbus_cleanup(void);\n+void vnc_dbus_client_connected(const char *host, const char *service,\n+                               const char *family, bool websocket);\n+void vnc_dbus_client_initialized(const char *host, const char *service,\n+                                 const char *x509_dname,\n+                                 const char *sasl_username);\n+void vnc_dbus_client_disconnected(const char *host, const char *service);\n+\n+#endif /* CONTRIB_QEMU_VNC_H */\ndiff --git a/tools/qemu-vnc/trace.h b/tools/qemu-vnc/trace.h\nnew file mode 100644\nindex 00000000000..5fb7b432359\n--- /dev/null\n+++ b/tools/qemu-vnc/trace.h\n@@ -0,0 +1,4 @@\n+/*\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+#include \"trace/trace-tools_qemu_vnc.h\"\ndiff --git a/tests/qtest/dbus-vnc-test.c b/tests/qtest/dbus-vnc-test.c\nnew file mode 100644\nindex 00000000000..cbd6def9d51\n--- /dev/null\n+++ b/tests/qtest/dbus-vnc-test.c\n@@ -0,0 +1,1342 @@\n+/*\n+ * D-Bus VNC server (qemu-vnc) end-to-end test\n+ *\n+ * Copyright (c) 2026 Red Hat, Inc.\n+ *\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+#include <gio/gio.h>\n+#include <gvnc.h>\n+#include <sys/un.h>\n+#include \"qemu/sockets.h\"\n+#include \"libqtest.h\"\n+#include \"qemu-vnc1.h\"\n+#ifdef CONFIG_TASN1\n+#include \"tests/unit/crypto-tls-x509-helpers.h\"\n+#endif\n+\n+#define VNC_TEST_TIMEOUT_MS 10000\n+\n+typedef struct DbusTest {\n+    QTestState *qts;\n+    GSubprocess *vnc_subprocess;\n+    GTestDBus *bus;\n+    GDBusConnection *bus_conn;\n+    GMainLoop *loop;\n+    char *vnc_sock_path;\n+    char *tmp_dir;\n+    char *bus_addr;\n+} DbusTest;\n+\n+typedef struct LifecycleData {\n+    DbusTest *dt;\n+    QemuVnc1Server *server_proxy;\n+    VncConnection *conn;\n+    char *client_path;\n+    gboolean got_connected;\n+    gboolean got_initialized;\n+    gboolean got_disconnected;\n+} LifecycleData;\n+\n+static QemuVnc1Server *\n+create_server_proxy(GDBusConnection *bus_conn, GError **errp)\n+{\n+    return qemu_vnc1_server_proxy_new_sync(\n+        bus_conn,\n+        G_DBUS_PROXY_FLAGS_NONE,\n+        \"org.qemu.vnc\",\n+        \"/org/qemu/Vnc1/Server\",\n+        NULL, errp);\n+}\n+\n+static void\n+on_vnc_error(VncConnection *self, const char *msg)\n+{\n+    g_error(\"vnc-error: %s\", msg);\n+}\n+\n+static void\n+on_vnc_auth_failure(VncConnection *self, const char *msg)\n+{\n+    g_error(\"vnc-auth-failure: %s\", msg);\n+}\n+\n+static void\n+on_vnc_initialized(VncConnection *self, GMainLoop *loop)\n+{\n+    const char *name = vnc_connection_get_name(self);\n+\n+    g_assert_cmpstr(name, ==, \"QEMU (dbus-vnc-test)\");\n+    g_main_loop_quit(loop);\n+}\n+\n+static gboolean\n+timeout_cb(gpointer data)\n+{\n+    g_error(\"test timed out\");\n+    return G_SOURCE_REMOVE;\n+}\n+\n+static int\n+connect_unix_socket(const char *path)\n+{\n+    int fd;\n+    struct sockaddr_un addr = { .sun_family = AF_UNIX };\n+\n+    fd = socket(AF_UNIX, SOCK_STREAM, 0);\n+    g_assert(fd >= 0);\n+\n+    snprintf(addr.sun_path, sizeof(addr.sun_path), \"%s\", path);\n+\n+    if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {\n+        close(fd);\n+        return -1;\n+    }\n+    return fd;\n+}\n+\n+static int\n+wait_for_vnc_socket(const char *path, int timeout_ms)\n+{\n+    int elapsed = 0;\n+    const int interval = 50;\n+\n+    while (elapsed < timeout_ms) {\n+        int fd = connect_unix_socket(path);\n+\n+        if (fd >= 0) {\n+            return fd;\n+        }\n+\n+        g_usleep(interval * 1000);\n+        elapsed += interval;\n+    }\n+    return -1;\n+}\n+\n+static GSubprocess *\n+spawn_qemu_vnc(int dbus_fd, const char *sock_path)\n+{\n+    const char *binary;\n+    g_autoptr(GError) err = NULL;\n+    g_autoptr(GSubprocessLauncher) launcher = NULL;\n+    GSubprocess *proc;\n+    g_autofree char *fd_str = NULL;\n+    g_autofree char *vnc_addr = NULL;\n+\n+    binary = g_getenv(\"QTEST_QEMU_VNC_BINARY\");\n+    g_assert(binary != NULL);\n+\n+    fd_str = g_strdup_printf(\"%d\", dbus_fd);\n+    vnc_addr = g_strdup_printf(\"unix:%s\", sock_path);\n+\n+    launcher = g_subprocess_launcher_new(G_SUBPROCESS_FLAGS_NONE);\n+    g_subprocess_launcher_take_fd(launcher, dbus_fd, dbus_fd);\n+\n+    proc = g_subprocess_launcher_spawn(launcher, &err,\n+                                       binary,\n+                                       \"--dbus-p2p-fd\", fd_str,\n+                                       \"--vnc-addr\", vnc_addr,\n+                                       NULL);\n+    g_assert_no_error(err);\n+    g_assert(proc != NULL);\n+\n+    return proc;\n+}\n+\n+static GSubprocess *\n+spawn_qemu_vnc_bus_full(const char *dbus_addr, const char *sock_path,\n+                        const char *const *extra_args)\n+{\n+    const char *binary;\n+    g_autoptr(GError) err = NULL;\n+    g_autoptr(GSubprocessLauncher) launcher = NULL;\n+    g_autoptr(GPtrArray) argv = NULL;\n+    GSubprocess *proc;\n+    g_autofree char *vnc_addr = NULL;\n+\n+    binary = g_getenv(\"QTEST_QEMU_VNC_BINARY\");\n+    g_assert(binary != NULL);\n+\n+    vnc_addr = g_strdup_printf(\"unix:%s\", sock_path);\n+\n+    argv = g_ptr_array_new();\n+    g_ptr_array_add(argv, (gpointer)binary);\n+    g_ptr_array_add(argv, (gpointer)\"--dbus-address\");\n+    g_ptr_array_add(argv, (gpointer)dbus_addr);\n+    g_ptr_array_add(argv, (gpointer)\"--bus-name\");\n+    g_ptr_array_add(argv, (gpointer)\"org.qemu\");\n+    g_ptr_array_add(argv, (gpointer)\"--vnc-addr\");\n+    g_ptr_array_add(argv, (gpointer)vnc_addr);\n+\n+    if (extra_args) {\n+        for (int i = 0; extra_args[i]; i++) {\n+            g_ptr_array_add(argv, (gpointer)extra_args[i]);\n+        }\n+    }\n+\n+    g_ptr_array_add(argv, NULL);\n+\n+    launcher = g_subprocess_launcher_new(G_SUBPROCESS_FLAGS_NONE);\n+    proc = g_subprocess_launcher_spawnv(launcher, (const char *const *)argv->pdata, &err);\n+    g_assert_no_error(err);\n+    g_assert(proc != NULL);\n+\n+    return proc;\n+}\n+\n+\n+static void\n+name_appeared_cb(GDBusConnection *connection,\n+                 const gchar *name,\n+                 const gchar *name_owner,\n+                 gpointer user_data)\n+{\n+    gboolean *appeared = user_data;\n+    *appeared = TRUE;\n+}\n+\n+static bool\n+setup_dbus_test_full(DbusTest *dt, const char *const *vnc_extra_args)\n+{\n+    g_autoptr(GError) err = NULL;\n+    g_auto(GStrv) addr_parts = NULL;\n+    g_autofree char *qemu_args = NULL;\n+\n+    if (!g_getenv(\"QTEST_QEMU_VNC_BINARY\")) {\n+        g_test_skip(\"QTEST_QEMU_VNC_BINARY not set\");\n+        return false;\n+    }\n+\n+    dt->bus = g_test_dbus_new(G_TEST_DBUS_NONE);\n+    g_test_dbus_up(dt->bus);\n+\n+    /* remove ,guid=foo part */\n+    addr_parts = g_strsplit(g_test_dbus_get_bus_address(dt->bus), \",\", 2);\n+    dt->bus_addr = g_strdup(addr_parts[0]);\n+\n+    dt->bus_conn = g_dbus_connection_new_for_address_sync(\n+        g_test_dbus_get_bus_address(dt->bus),\n+        G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT |\n+        G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION,\n+        NULL, NULL, &err);\n+    g_assert_no_error(err);\n+\n+    qemu_args = g_strdup_printf(\"-display dbus,addr=%s \"\n+                                \"-name dbus-vnc-test\", dt->bus_addr);\n+    dt->qts = qtest_init(qemu_args);\n+\n+    dt->tmp_dir = g_dir_make_tmp(\"dbus-vnc-test-XXXXXX\", NULL);\n+    g_assert(dt->tmp_dir != NULL);\n+    dt->vnc_sock_path = g_build_filename(dt->tmp_dir, \"vnc.sock\", NULL);\n+    dt->vnc_subprocess = spawn_qemu_vnc_bus_full(dt->bus_addr,\n+                                                 dt->vnc_sock_path,\n+                                                 vnc_extra_args);\n+\n+    /*\n+     * Wait for the org.qemu.vnc bus name to appear, which indicates\n+     * qemu-vnc has fully initialized (connected to QEMU, set up the\n+     * display, exported its D-Bus interfaces, and opened the VNC\n+     * socket).\n+     */\n+    {\n+        guint watch_id, timeout_id;\n+        gboolean appeared = FALSE;\n+\n+        watch_id = g_bus_watch_name_on_connection(\n+            dt->bus_conn, \"org.qemu.vnc\",\n+            G_BUS_NAME_WATCHER_FLAGS_NONE,\n+            name_appeared_cb, NULL, &appeared, NULL);\n+        timeout_id = g_timeout_add_seconds(10, timeout_cb, NULL);\n+\n+        while (!appeared &&\n+               g_main_context_iteration(NULL, TRUE)) {\n+        }\n+\n+        g_bus_unwatch_name(watch_id);\n+        g_source_remove(timeout_id);\n+\n+        if (!appeared) {\n+            g_test_fail();\n+            g_test_message(\"Timed out waiting for org.qemu.vnc bus name\");\n+            return false;\n+        }\n+    }\n+\n+    return true;\n+}\n+\n+static bool\n+setup_dbus_test(DbusTest *dt)\n+{\n+    return setup_dbus_test_full(dt, NULL);\n+}\n+\n+static void\n+cleanup_dbus_test(DbusTest *dt)\n+{\n+    if (dt->bus_conn) {\n+        g_dbus_connection_close_sync(dt->bus_conn, NULL, NULL);\n+        g_object_unref(dt->bus_conn);\n+    }\n+    if (dt->vnc_subprocess) {\n+        g_subprocess_force_exit(dt->vnc_subprocess);\n+        g_subprocess_wait(dt->vnc_subprocess, NULL, NULL);\n+        g_object_unref(dt->vnc_subprocess);\n+    }\n+    if (dt->vnc_sock_path) {\n+        unlink(dt->vnc_sock_path);\n+        g_free(dt->vnc_sock_path);\n+    }\n+    if (dt->tmp_dir) {\n+        rmdir(dt->tmp_dir);\n+        g_free(dt->tmp_dir);\n+    }\n+    if (dt->qts) {\n+        qtest_quit(dt->qts);\n+    }\n+    if (dt->bus) {\n+        g_test_dbus_down(dt->bus);\n+        g_object_unref(dt->bus);\n+    }\n+    g_free(dt->bus_addr);\n+}\n+\n+static void\n+test_dbus_vnc_basic(void)\n+{\n+    DbusTest dt = { 0 };\n+    VncConnection *conn = NULL;\n+    GMainLoop *loop = NULL;\n+    int pair[2];\n+    int vnc_fd;\n+    guint timeout_id;\n+\n+    if (!g_getenv(\"QTEST_QEMU_VNC_BINARY\")) {\n+        g_test_skip(\"QTEST_QEMU_VNC_BINARY not set\");\n+        return;\n+    }\n+\n+    dt.qts = qtest_init(\"-display dbus,p2p=yes -name dbus-vnc-test\");\n+\n+    g_assert_cmpint(qemu_socketpair(AF_UNIX, SOCK_STREAM, 0, pair), ==, 0);\n+    qtest_qmp_add_client(dt.qts, \"@dbus-display\", pair[1]);\n+    close(pair[1]);\n+\n+    dt.tmp_dir = g_dir_make_tmp(\"dbus-vnc-test-XXXXXX\", NULL);\n+    g_assert(dt.tmp_dir != NULL);\n+    dt.vnc_sock_path = g_build_filename(dt.tmp_dir, \"vnc.sock\", NULL);\n+\n+    dt.vnc_subprocess = spawn_qemu_vnc(pair[0], dt.vnc_sock_path);\n+\n+    vnc_fd = wait_for_vnc_socket(dt.vnc_sock_path, VNC_TEST_TIMEOUT_MS);\n+    g_assert(vnc_fd >= 0);\n+\n+    loop = g_main_loop_new(NULL, FALSE);\n+\n+    conn = vnc_connection_new();\n+    g_signal_connect(conn, \"vnc-error\",\n+                     G_CALLBACK(on_vnc_error), NULL);\n+    g_signal_connect(conn, \"vnc-auth-failure\",\n+                     G_CALLBACK(on_vnc_auth_failure), NULL);\n+    g_signal_connect(conn, \"vnc-initialized\",\n+                     G_CALLBACK(on_vnc_initialized), loop);\n+    vnc_connection_set_auth_type(conn, VNC_CONNECTION_AUTH_NONE);\n+    vnc_connection_open_fd(conn, vnc_fd);\n+\n+    timeout_id = g_timeout_add_seconds(10, timeout_cb, NULL);\n+    g_main_loop_run(loop);\n+    g_source_remove(timeout_id);\n+\n+    if (conn) {\n+        vnc_connection_shutdown(conn);\n+        g_object_unref(conn);\n+    }\n+    g_clear_pointer(&loop, g_main_loop_unref);\n+    cleanup_dbus_test(&dt);\n+}\n+\n+static void\n+test_dbus_vnc_server_props(void)\n+{\n+    DbusTest dt = { 0 };\n+    QemuVnc1Server *proxy = NULL;\n+    g_autoptr(GError) err = NULL;\n+    const gchar *const *clients;\n+    GVariant *listeners;\n+\n+    if (!setup_dbus_test(&dt)) {\n+        goto cleanup;\n+    }\n+\n+    proxy = create_server_proxy(dt.bus_conn, &err);\n+    g_assert_no_error(err);\n+    g_assert_nonnull(proxy);\n+\n+    g_assert_cmpstr(qemu_vnc1_server_get_name(proxy), ==,\n+                    \"dbus-vnc-test\");\n+    g_assert_cmpstr(qemu_vnc1_server_get_auth(proxy), ==,\n+                    \"none\");\n+    g_assert_cmpstr(qemu_vnc1_server_get_vencrypt_sub_auth(proxy), ==,\n+                    \"\");\n+\n+    clients = qemu_vnc1_server_get_clients(proxy);\n+    g_assert_nonnull(clients);\n+    g_assert_cmpint(g_strv_length((gchar **)clients), ==, 0);\n+\n+    listeners = qemu_vnc1_server_get_listeners(proxy);\n+    g_assert_nonnull(listeners);\n+    g_assert_cmpint(g_variant_n_children(listeners), >, 0);\n+\n+cleanup:\n+    g_clear_object(&proxy);\n+    cleanup_dbus_test(&dt);\n+}\n+\n+static void\n+on_client_connected(QemuVnc1Server *proxy,\n+                    const gchar *client_path,\n+                    LifecycleData *data)\n+{\n+    data->got_connected = TRUE;\n+    data->client_path = g_strdup(client_path);\n+}\n+\n+static void\n+on_client_initialized(QemuVnc1Server *proxy,\n+                      const gchar *client_path,\n+                      LifecycleData *data)\n+{\n+    data->got_initialized = TRUE;\n+    g_main_loop_quit(data->dt->loop);\n+}\n+\n+static void\n+on_client_disconnected(QemuVnc1Server *proxy,\n+                       const gchar *client_path,\n+                       LifecycleData *data)\n+{\n+    data->got_disconnected = TRUE;\n+    g_main_loop_quit(data->dt->loop);\n+}\n+\n+static void\n+test_dbus_vnc_client_lifecycle(void)\n+{\n+    DbusTest dt = { 0 };\n+    QemuVnc1Server *server_proxy = NULL;\n+    QemuVnc1Client *client_proxy = NULL;\n+    g_autoptr(GError) err = NULL;\n+    LifecycleData ldata = { 0 };\n+    int vnc_fd;\n+    guint timeout_id;\n+\n+    if (!setup_dbus_test(&dt)) {\n+        goto cleanup;\n+    }\n+\n+    server_proxy = create_server_proxy(dt.bus_conn, &err);\n+    g_assert_no_error(err);\n+\n+    ldata.dt = &dt;\n+    ldata.server_proxy = server_proxy;\n+\n+    g_signal_connect(server_proxy, \"client-connected\",\n+                     G_CALLBACK(on_client_connected), &ldata);\n+    g_signal_connect(server_proxy, \"client-initialized\",\n+                     G_CALLBACK(on_client_initialized), &ldata);\n+    g_signal_connect(server_proxy, \"client-disconnected\",\n+                     G_CALLBACK(on_client_disconnected), &ldata);\n+\n+    vnc_fd = wait_for_vnc_socket(dt.vnc_sock_path, VNC_TEST_TIMEOUT_MS);\n+    g_assert(vnc_fd >= 0);\n+\n+    ldata.conn = vnc_connection_new();\n+    g_signal_connect(ldata.conn, \"vnc-error\",\n+                     G_CALLBACK(on_vnc_error), NULL);\n+    g_signal_connect(ldata.conn, \"vnc-auth-failure\",\n+                     G_CALLBACK(on_vnc_auth_failure), NULL);\n+    vnc_connection_set_auth_type(ldata.conn, VNC_CONNECTION_AUTH_NONE);\n+    vnc_connection_open_fd(ldata.conn, vnc_fd);\n+\n+    /* wait for ClientInitialized */\n+    dt.loop = g_main_loop_new(NULL, FALSE);\n+    timeout_id = g_timeout_add_seconds(10, timeout_cb, NULL);\n+    g_main_loop_run(dt.loop);\n+    g_source_remove(timeout_id);\n+\n+    g_assert_true(ldata.got_connected);\n+    g_assert_true(ldata.got_initialized);\n+    g_assert_nonnull(ldata.client_path);\n+\n+    /* Check client properties while still connected */\n+    client_proxy = qemu_vnc1_client_proxy_new_sync(\n+        dt.bus_conn,\n+        G_DBUS_PROXY_FLAGS_NONE,\n+        \"org.qemu.vnc\",\n+        ldata.client_path,\n+        NULL, &err);\n+    g_assert_no_error(err);\n+\n+    g_assert_cmpstr(qemu_vnc1_client_get_family(client_proxy), ==,\n+                    \"unix\");\n+    g_assert_false(qemu_vnc1_client_get_web_socket(client_proxy));\n+    g_assert_cmpstr(qemu_vnc1_client_get_x509_dname(client_proxy), ==,\n+                    \"\");\n+    g_assert_cmpstr(qemu_vnc1_client_get_sasl_username(client_proxy),\n+                    ==, \"\");\n+\n+    /* disconnect and wait for ClientDisconnected */\n+    vnc_connection_shutdown(ldata.conn);\n+    timeout_id = g_timeout_add_seconds(10, timeout_cb, NULL);\n+    g_main_loop_run(dt.loop);\n+    g_source_remove(timeout_id);\n+\n+    g_assert_true(ldata.got_disconnected);\n+\n+    g_object_unref(ldata.conn);\n+    g_main_loop_unref(dt.loop);\n+    dt.loop = NULL;\n+    g_free(ldata.client_path);\n+\n+cleanup:\n+    g_clear_object(&server_proxy);\n+    g_clear_object(&client_proxy);\n+    cleanup_dbus_test(&dt);\n+}\n+\n+static void\n+test_dbus_vnc_no_password(void)\n+{\n+    DbusTest dt = { 0 };\n+    QemuVnc1Server *proxy = NULL;\n+    g_autoptr(GError) err = NULL;\n+    gboolean ret;\n+\n+    if (!setup_dbus_test(&dt)) {\n+        goto cleanup;\n+    }\n+\n+    proxy = create_server_proxy(dt.bus_conn, &err);\n+    g_assert_no_error(err);\n+\n+    /*\n+     * With default auth=none, SetPassword should return an error\n+     * because VNC password authentication is not enabled.\n+     */\n+    ret = qemu_vnc1_server_call_set_password_sync(\n+        proxy, \"secret\",\n+        G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err);\n+    g_assert_false(ret);\n+    g_assert_error(err, G_DBUS_ERROR, G_DBUS_ERROR_FAILED);\n+    g_clear_error(&err);\n+\n+    ret = qemu_vnc1_server_call_expire_password_sync(\n+        proxy, \"never\",\n+        G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err);\n+    g_assert_no_error(err);\n+    g_assert_true(ret);\n+\n+    ret = qemu_vnc1_server_call_expire_password_sync(\n+        proxy, \"+3600\",\n+        G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err);\n+    g_assert_no_error(err);\n+    g_assert_true(ret);\n+\n+cleanup:\n+    g_clear_object(&proxy);\n+    cleanup_dbus_test(&dt);\n+}\n+\n+typedef struct PasswordData {\n+    DbusTest *dt;\n+    VncConnection *conn;\n+    const char *password;\n+    gboolean auth_succeeded;\n+    gboolean auth_failed;\n+} PasswordData;\n+\n+G_GNUC_BEGIN_IGNORE_DEPRECATIONS\n+static void\n+on_pw_vnc_auth_credential(VncConnection *conn, GValueArray *creds,\n+                          PasswordData *data)\n+{\n+    for (guint i = 0; i < creds->n_values; i++) {\n+        int type = g_value_get_enum(g_value_array_get_nth(creds, i));\n+\n+        if (type == VNC_CONNECTION_CREDENTIAL_PASSWORD) {\n+            vnc_connection_set_credential(conn, type, data->password);\n+        }\n+    }\n+}\n+G_GNUC_END_IGNORE_DEPRECATIONS\n+\n+static void\n+on_pw_vnc_initialized(VncConnection *conn, PasswordData *data)\n+{\n+    data->auth_succeeded = TRUE;\n+    g_main_loop_quit(data->dt->loop);\n+}\n+\n+static void\n+on_pw_vnc_auth_failure(VncConnection *conn, const char *msg,\n+                       PasswordData *data)\n+{\n+    data->auth_failed = TRUE;\n+    g_main_loop_quit(data->dt->loop);\n+}\n+\n+static void\n+on_pw_vnc_error(VncConnection *conn, const char *msg,\n+                PasswordData *data)\n+{\n+    data->auth_failed = TRUE;\n+    g_main_loop_quit(data->dt->loop);\n+}\n+\n+static void\n+test_dbus_vnc_password_auth(void)\n+{\n+    DbusTest dt = { 0 };\n+    QemuVnc1Server *proxy = NULL;\n+    g_autoptr(GError) err = NULL;\n+    PasswordData pdata = { 0 };\n+    const char *extra_args[] = { \"--password\", NULL };\n+    int vnc_fd;\n+    guint timeout_id;\n+    gboolean ret;\n+\n+    if (!setup_dbus_test_full(&dt, extra_args)) {\n+        goto cleanup;\n+    }\n+\n+    proxy = create_server_proxy(dt.bus_conn, &err);\n+    g_assert_no_error(err);\n+\n+    g_assert_cmpstr(qemu_vnc1_server_get_auth(proxy), ==, \"vnc\");\n+\n+    ret = qemu_vnc1_server_call_set_password_sync(\n+        proxy, \"testpass123\",\n+        G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err);\n+    g_assert_no_error(err);\n+    g_assert_true(ret);\n+\n+    vnc_fd = wait_for_vnc_socket(dt.vnc_sock_path, VNC_TEST_TIMEOUT_MS);\n+    g_assert(vnc_fd >= 0);\n+\n+    pdata.dt = &dt;\n+    pdata.password = \"testpass123\";\n+    pdata.conn = vnc_connection_new();\n+\n+    g_signal_connect(pdata.conn, \"vnc-error\",\n+                     G_CALLBACK(on_pw_vnc_error), &pdata);\n+    g_signal_connect(pdata.conn, \"vnc-auth-failure\",\n+                     G_CALLBACK(on_pw_vnc_auth_failure), &pdata);\n+    g_signal_connect(pdata.conn, \"vnc-auth-credential\",\n+                     G_CALLBACK(on_pw_vnc_auth_credential), &pdata);\n+    g_signal_connect(pdata.conn, \"vnc-initialized\",\n+                     G_CALLBACK(on_pw_vnc_initialized), &pdata);\n+    vnc_connection_set_auth_type(pdata.conn, VNC_CONNECTION_AUTH_VNC);\n+    vnc_connection_open_fd(pdata.conn, vnc_fd);\n+\n+    dt.loop = g_main_loop_new(NULL, FALSE);\n+    timeout_id = g_timeout_add_seconds(10, timeout_cb, NULL);\n+    g_main_loop_run(dt.loop);\n+    g_source_remove(timeout_id);\n+\n+    g_assert_true(pdata.auth_succeeded);\n+    g_assert_false(pdata.auth_failed);\n+\n+    vnc_connection_shutdown(pdata.conn);\n+    g_object_unref(pdata.conn);\n+    g_main_loop_unref(dt.loop);\n+    dt.loop = NULL;\n+\n+cleanup:\n+    g_clear_object(&proxy);\n+    cleanup_dbus_test(&dt);\n+}\n+\n+static void\n+test_dbus_vnc_sasl_authz_no_sasl(void)\n+{\n+    const char *binary;\n+    g_autoptr(GError) err = NULL;\n+    g_autoptr(GSubprocess) proc = NULL;\n+    gboolean ok;\n+\n+    binary = g_getenv(\"QTEST_QEMU_VNC_BINARY\");\n+    if (!binary) {\n+        g_test_skip(\"QTEST_QEMU_VNC_BINARY not set\");\n+        return;\n+    }\n+\n+    proc = g_subprocess_new(G_SUBPROCESS_FLAGS_STDERR_SILENCE,\n+                            &err,\n+                            binary,\n+                            \"--sasl-authz\", \"authz0\",\n+                            NULL);\n+    g_assert_no_error(err);\n+    g_assert_nonnull(proc);\n+\n+    ok = g_subprocess_wait(proc, NULL, &err);\n+    g_assert_no_error(err);\n+    g_assert_true(ok);\n+    g_assert_false(g_subprocess_get_successful(proc));\n+}\n+\n+#ifdef CONFIG_VNC_SASL\n+static void\n+test_dbus_vnc_sasl_server_props(void)\n+{\n+    DbusTest dt = { 0 };\n+    QemuVnc1Server *proxy = NULL;\n+    g_autoptr(GError) err = NULL;\n+    const char *extra_args[] = { \"--sasl\", NULL };\n+\n+    if (!setup_dbus_test_full(&dt, extra_args)) {\n+        goto cleanup;\n+    }\n+\n+    proxy = create_server_proxy(dt.bus_conn, &err);\n+    g_assert_no_error(err);\n+    g_assert_nonnull(proxy);\n+\n+    g_assert_cmpstr(qemu_vnc1_server_get_auth(proxy), ==, \"sasl\");\n+\n+cleanup:\n+    g_clear_object(&proxy);\n+    cleanup_dbus_test(&dt);\n+}\n+\n+#define SASL_TEST_USER \"testuser\"\n+#define SASL_TEST_PASS \"testpass123\"\n+\n+typedef struct SaslAuthData {\n+    DbusTest *dt;\n+    const char *username;\n+    const char *password;\n+    gboolean auth_succeeded;\n+    gboolean auth_failed;\n+} SaslAuthData;\n+\n+typedef struct SaslTestData {\n+    DbusTest dt;\n+    SaslAuthData sdata;\n+    char *sasl_dir;\n+    char *db_path;\n+} SaslTestData;\n+\n+G_GNUC_BEGIN_IGNORE_DEPRECATIONS\n+static void\n+on_sasl_vnc_auth_credential(VncConnection *conn, GValueArray *creds,\n+                            SaslAuthData *data)\n+{\n+    for (guint i = 0; i < creds->n_values; i++) {\n+        int type = g_value_get_enum(g_value_array_get_nth(creds, i));\n+\n+        switch (type) {\n+        case VNC_CONNECTION_CREDENTIAL_USERNAME:\n+            vnc_connection_set_credential(conn, type, data->username);\n+            break;\n+        case VNC_CONNECTION_CREDENTIAL_PASSWORD:\n+            vnc_connection_set_credential(conn, type, data->password);\n+            break;\n+        }\n+    }\n+}\n+G_GNUC_END_IGNORE_DEPRECATIONS\n+\n+static void\n+on_sasl_vnc_initialized(VncConnection *conn, SaslAuthData *data)\n+{\n+    data->auth_succeeded = TRUE;\n+    g_main_loop_quit(data->dt->loop);\n+}\n+\n+static void\n+on_sasl_vnc_auth_failure(VncConnection *conn, const char *msg,\n+                         SaslAuthData *data)\n+{\n+    data->auth_failed = TRUE;\n+    g_main_loop_quit(data->dt->loop);\n+}\n+\n+static void\n+on_sasl_vnc_error(VncConnection *conn, const char *msg,\n+                  SaslAuthData *data)\n+{\n+    data->auth_failed = TRUE;\n+    g_main_loop_quit(data->dt->loop);\n+}\n+\n+/*\n+ * Create a SASL configuration directory with a qemu.conf and a\n+ * sasldb2 user database.  Returns the path to the sasldb file,\n+ * or NULL if saslpasswd2 is not available.\n+ */\n+static char *\n+create_sasl_config(const char *dir, const char *username,\n+                   const char *password)\n+{\n+    g_autofree char *conf_path = g_strdup_printf(\"%s/qemu.conf\", dir);\n+    g_autofree char *db_path = g_strdup_printf(\"%s/sasldb2\", dir);\n+    g_autoptr(GError) err = NULL;\n+    g_autoptr(GSubprocess) proc = NULL;\n+    g_autofree char *conf = NULL;\n+    GOutputStream *stdin_stream;\n+    gboolean ok;\n+\n+    /* use PLAIN, and local auxprop sasldb plugin */\n+    conf = g_strdup_printf(\n+        \"mech_list: plain\\n\"\n+        \"pwcheck_method: auxprop\\n\"\n+        \"auxprop_plugin: sasldb\\n\"\n+        \"sasldb_path: %s\\n\", db_path);\n+    g_assert_true(g_file_set_contents(conf_path, conf, -1, NULL));\n+\n+    proc = g_subprocess_new(\n+        G_SUBPROCESS_FLAGS_STDIN_PIPE |\n+        G_SUBPROCESS_FLAGS_STDOUT_SILENCE |\n+        G_SUBPROCESS_FLAGS_STDERR_SILENCE,\n+        &err,\n+        \"saslpasswd2\", \"-f\", db_path, \"-a\", \"qemu\", \"-p\", \"-c\",\n+        username, NULL);\n+    if (!proc) {\n+        return NULL;\n+    }\n+\n+    stdin_stream = g_subprocess_get_stdin_pipe(proc);\n+    g_output_stream_write_all(stdin_stream, password,\n+                              strlen(password), NULL, NULL, NULL);\n+    g_output_stream_close(stdin_stream, NULL, NULL);\n+\n+    ok = g_subprocess_wait_check(proc, NULL, &err);\n+    if (!ok) {\n+        return NULL;\n+    }\n+\n+    return g_strdup(db_path);\n+}\n+\n+static void\n+cleanup_sasl_config(const char *dir, const char *db_path)\n+{\n+    g_autofree char *conf = g_strdup_printf(\"%s/qemu.conf\", dir);\n+\n+    unlink(conf);\n+    if (db_path) {\n+        unlink(db_path);\n+    }\n+    rmdir(dir);\n+}\n+\n+/*\n+ * Set up SASL environment: create temp config dir, sasldb, and\n+ * start qemu-vnc with the given extra_args.  Returns FALSE if the\n+ * test should be skipped.\n+ */\n+static gboolean\n+setup_sasl_test(SaslTestData *st, const char **extra_args)\n+{\n+    if (!g_getenv(\"QTEST_QEMU_VNC_BINARY\")) {\n+        g_test_skip(\"QTEST_QEMU_VNC_BINARY not set\");\n+        return FALSE;\n+    }\n+\n+    st->sasl_dir = g_dir_make_tmp(\"dbus-vnc-sasl-XXXXXX\", NULL);\n+    g_assert_nonnull(st->sasl_dir);\n+\n+    st->db_path = create_sasl_config(st->sasl_dir, SASL_TEST_USER,\n+                                     SASL_TEST_PASS);\n+    if (!st->db_path) {\n+        g_test_skip(\"saslpasswd2 not available or failed\");\n+        cleanup_sasl_config(st->sasl_dir, NULL);\n+        return FALSE;\n+    }\n+\n+    g_setenv(\"SASL_CONF_PATH\", st->sasl_dir, TRUE);\n+\n+    if (!setup_dbus_test_full(&st->dt, extra_args)) {\n+        return FALSE;\n+    }\n+\n+    return TRUE;\n+}\n+\n+/*\n+ * Connect to the VNC server using SASL and run the main loop\n+ * until authentication completes or times out.\n+ */\n+static void\n+run_sasl_auth(SaslTestData *st, const char *username,\n+              const char *password)\n+{\n+    VncConnection *conn;\n+    guint timeout_id;\n+    int vnc_fd;\n+\n+    st->sdata.dt = &st->dt;\n+    st->sdata.username = username;\n+    st->sdata.password = password;\n+\n+    vnc_fd = wait_for_vnc_socket(st->dt.vnc_sock_path, VNC_TEST_TIMEOUT_MS);\n+    g_assert(vnc_fd >= 0);\n+\n+    conn = vnc_connection_new();\n+    g_signal_connect(conn, \"vnc-error\",\n+                     G_CALLBACK(on_sasl_vnc_error), &st->sdata);\n+    g_signal_connect(conn, \"vnc-auth-failure\",\n+                     G_CALLBACK(on_sasl_vnc_auth_failure), &st->sdata);\n+    g_signal_connect(conn, \"vnc-auth-credential\",\n+                     G_CALLBACK(on_sasl_vnc_auth_credential),\n+                     &st->sdata);\n+    g_signal_connect(conn, \"vnc-initialized\",\n+                     G_CALLBACK(on_sasl_vnc_initialized), &st->sdata);\n+    vnc_connection_set_auth_type(conn, VNC_CONNECTION_AUTH_SASL);\n+    vnc_connection_open_fd(conn, vnc_fd);\n+\n+    st->dt.loop = g_main_loop_new(NULL, FALSE);\n+    timeout_id = g_timeout_add_seconds(10, timeout_cb, NULL);\n+    g_main_loop_run(st->dt.loop);\n+    g_source_remove(timeout_id);\n+\n+    g_signal_handlers_disconnect_by_data(conn, &st->sdata);\n+    vnc_connection_shutdown(conn);\n+    g_object_unref(conn);\n+    g_main_loop_unref(st->dt.loop);\n+    st->dt.loop = NULL;\n+}\n+\n+static void\n+cleanup_sasl_test(SaslTestData *st)\n+{\n+    cleanup_dbus_test(&st->dt);\n+    g_unsetenv(\"SASL_CONF_PATH\");\n+    cleanup_sasl_config(st->sasl_dir, st->db_path);\n+    g_free(st->sasl_dir);\n+    g_free(st->db_path);\n+}\n+\n+static void\n+test_dbus_vnc_sasl_auth(void)\n+{\n+    SaslTestData st = { 0 };\n+    const char *extra_args[] = { \"--sasl\", NULL };\n+\n+    if (!setup_sasl_test(&st, extra_args)) {\n+        return;\n+    }\n+\n+    run_sasl_auth(&st, SASL_TEST_USER, SASL_TEST_PASS);\n+\n+    g_assert_true(st.sdata.auth_succeeded);\n+    g_assert_false(st.sdata.auth_failed);\n+\n+    cleanup_sasl_test(&st);\n+}\n+\n+static void\n+test_dbus_vnc_sasl_auth_bad_password(void)\n+{\n+    SaslTestData st = { 0 };\n+    const char *extra_args[] = { \"--sasl\", NULL };\n+\n+    if (!setup_sasl_test(&st, extra_args)) {\n+        return;\n+    }\n+\n+    run_sasl_auth(&st, SASL_TEST_USER, \"wrongpassword\");\n+\n+    g_assert_false(st.sdata.auth_succeeded);\n+    g_assert_true(st.sdata.auth_failed);\n+\n+    cleanup_sasl_test(&st);\n+}\n+\n+static void\n+test_dbus_vnc_sasl_authz_denied(void)\n+{\n+    SaslTestData st = { 0 };\n+    const char *extra_args[] = {\n+        \"--sasl\",\n+        \"--object\",\n+        \"authz-simple,id=authz0,identity=otheruser\",\n+        \"--sasl-authz\", \"authz0\",\n+        NULL\n+    };\n+\n+    if (!setup_sasl_test(&st, extra_args)) {\n+        return;\n+    }\n+\n+    run_sasl_auth(&st, SASL_TEST_USER, SASL_TEST_PASS);\n+\n+    g_assert_false(st.sdata.auth_succeeded);\n+    g_assert_true(st.sdata.auth_failed);\n+\n+    cleanup_sasl_test(&st);\n+}\n+#endif /* CONFIG_VNC_SASL */\n+\n+static void\n+test_dbus_vnc_tls_authz_no_creds(void)\n+{\n+    const char *binary;\n+    g_autoptr(GError) err = NULL;\n+    g_autoptr(GSubprocess) proc = NULL;\n+    gboolean ok;\n+\n+    binary = g_getenv(\"QTEST_QEMU_VNC_BINARY\");\n+    if (!binary) {\n+        g_test_skip(\"QTEST_QEMU_VNC_BINARY not set\");\n+        return;\n+    }\n+\n+    proc = g_subprocess_new(G_SUBPROCESS_FLAGS_STDERR_SILENCE,\n+                            &err,\n+                            binary,\n+                            \"--tls-authz\", \"authz0\",\n+                            NULL);\n+    g_assert_no_error(err);\n+    g_assert_nonnull(proc);\n+\n+    ok = g_subprocess_wait(proc, NULL, &err);\n+    g_assert_no_error(err);\n+    g_assert_true(ok);\n+    g_assert_false(g_subprocess_get_successful(proc));\n+}\n+\n+#ifdef CONFIG_TASN1\n+#define CLIENT_CERT_CN \"qemu-vnc-test\"\n+\n+static char *\n+create_tls_certs(const char *dir)\n+{\n+    char *keyfile = g_strdup_printf(\"%s/key.pem\", dir);\n+    char *cacert = g_strdup_printf(\"%s/ca-cert.pem\", dir);\n+    char *servercert = g_strdup_printf(\"%s/server-cert.pem\", dir);\n+    char *serverkey = g_strdup_printf(\"%s/server-key.pem\", dir);\n+    char *clientcert = g_strdup_printf(\"%s/client-cert.pem\", dir);\n+\n+    test_tls_init(keyfile);\n+    g_assert(link(keyfile, serverkey) == 0);\n+\n+    TLS_ROOT_REQ_SIMPLE(cacertreq, cacert);\n+    TLS_CERT_REQ_SIMPLE_SERVER(servercertreq, cacertreq,\n+                               servercert, \"localhost\", NULL);\n+    TLS_CERT_REQ_SIMPLE_CLIENT(clientcertreq, cacertreq,\n+                               CLIENT_CERT_CN, clientcert);\n+\n+    test_tls_deinit_cert(&clientcertreq);\n+    test_tls_deinit_cert(&servercertreq);\n+    test_tls_deinit_cert(&cacertreq);\n+\n+    g_free(cacert);\n+    g_free(servercert);\n+    g_free(serverkey);\n+    g_free(clientcert);\n+    return keyfile;\n+}\n+\n+static void\n+cleanup_tls_certs(const char *dir, const char *keyfile)\n+{\n+    g_autofree char *cacert = g_strdup_printf(\"%s/ca-cert.pem\", dir);\n+    g_autofree char *servercert = g_strdup_printf(\"%s/server-cert.pem\", dir);\n+    g_autofree char *serverkey = g_strdup_printf(\"%s/server-key.pem\", dir);\n+    g_autofree char *clientcert = g_strdup_printf(\"%s/client-cert.pem\", dir);\n+\n+    unlink(cacert);\n+    unlink(servercert);\n+    unlink(serverkey);\n+    unlink(clientcert);\n+    unlink(keyfile);\n+    test_tls_cleanup(keyfile);\n+    rmdir(dir);\n+}\n+\n+/*\n+ * Do a minimal VNC/VeNCrypt negotiation on @fd up to the point where\n+ * the TLS handshake should begin, then perform a GnuTLS handshake\n+ * using the given credentials.\n+ */\n+static bool\n+try_raw_tls_connect(int fd, gnutls_certificate_credentials_t cred)\n+{\n+    char buf[13];\n+    uint8_t num_types, type;\n+    uint8_t vencrypt_ver[2], ack;\n+    uint8_t num_sub;\n+    uint32_t subtype;\n+    gnutls_session_t session;\n+    int ret;\n+    bool success;\n+\n+    /* RFB version exchange */\n+    g_assert_cmpint(read(fd, buf, 12), ==, 12);\n+    g_assert_cmpint(write(fd, \"RFB 003.008\\n\", 12), ==, 12);\n+\n+    /* Select VeNCrypt (type 19) from the auth list */\n+    g_assert_cmpint(read(fd, &num_types, 1), ==, 1);\n+    for (int i = 0; i < num_types; i++) {\n+        g_assert_cmpint(read(fd, &type, 1), ==, 1);\n+    }\n+    type = 19;\n+    g_assert_cmpint(write(fd, &type, 1), ==, 1);\n+\n+    /* VeNCrypt version exchange */\n+    g_assert_cmpint(read(fd, vencrypt_ver, 2), ==, 2);\n+    g_assert_cmpint(write(fd, vencrypt_ver, 2), ==, 2);\n+    g_assert_cmpint(read(fd, &ack, 1), ==, 1);\n+    g_assert_cmpint(ack, ==, 0);\n+\n+    /* Select x509-none (260) sub-auth */\n+    g_assert_cmpint(read(fd, &num_sub, 1), ==, 1);\n+    for (int i = 0; i < num_sub; i++) {\n+        g_assert_cmpint(read(fd, &subtype, 4), ==, 4);\n+    }\n+    subtype = htonl(260);\n+    g_assert_cmpint(write(fd, &subtype, 4), ==, 4);\n+\n+    /* Server sends 1-byte ack (1 = accepted) before TLS starts */\n+    g_assert_cmpint(read(fd, &ack, 1), ==, 1);\n+    g_assert_cmpint(ack, ==, 1);\n+\n+    /* TLS handshake */\n+    g_assert_cmpint(gnutls_init(&session, GNUTLS_CLIENT), >=, 0);\n+    g_assert_cmpint(\n+        gnutls_set_default_priority(session), >=, 0);\n+    g_assert_cmpint(\n+        gnutls_credentials_set(session, GNUTLS_CRD_CERTIFICATE, cred),\n+        >=, 0);\n+    gnutls_transport_set_int(session, fd);\n+\n+    do {\n+        ret = gnutls_handshake(session);\n+    } while (ret == GNUTLS_E_AGAIN || ret == GNUTLS_E_INTERRUPTED);\n+\n+    if (ret < 0) {\n+        success = false;\n+    } else {\n+        /*\n+         * Try reading the VNC security-result (4 bytes) — if the\n+         * server rejected us it will have closed the connection.\n+         */\n+        char tmp[4];\n+        do {\n+            ret = gnutls_record_recv(session, tmp, sizeof(tmp));\n+        } while (ret == GNUTLS_E_AGAIN || ret == GNUTLS_E_INTERRUPTED);\n+        success = (ret > 0);\n+    }\n+\n+    gnutls_deinit(session);\n+    return success;\n+}\n+\n+static void\n+test_dbus_vnc_tls_server_props(void)\n+{\n+    DbusTest dt = { 0 };\n+    QemuVnc1Server *proxy = NULL;\n+    g_autoptr(GError) err = NULL;\n+    g_autofree char *tls_dir = NULL;\n+    g_autofree char *keyfile = NULL;\n+\n+    if (!g_getenv(\"QTEST_QEMU_VNC_BINARY\")) {\n+        g_test_skip(\"QTEST_QEMU_VNC_BINARY not set\");\n+        return;\n+    }\n+\n+    tls_dir = g_dir_make_tmp(\"dbus-vnc-tls-XXXXXX\", NULL);\n+    g_assert_nonnull(tls_dir);\n+    keyfile = create_tls_certs(tls_dir);\n+\n+    {\n+        const char *extra_args[] = {\n+            \"--tls-creds\", tls_dir, NULL\n+        };\n+        if (!setup_dbus_test_full(&dt, extra_args)) {\n+            goto cleanup;\n+        }\n+    }\n+\n+    proxy = create_server_proxy(dt.bus_conn, &err);\n+    g_assert_no_error(err);\n+    g_assert_nonnull(proxy);\n+\n+    g_assert_cmpstr(qemu_vnc1_server_get_auth(proxy), ==, \"vencrypt\");\n+    g_assert_cmpstr(qemu_vnc1_server_get_vencrypt_sub_auth(proxy), ==,\n+                    \"x509-none\");\n+\n+    /*\n+     * With verify-peer=no, a client without a certificate should\n+     * be able to connect successfully through TLS.\n+     */\n+    {\n+        g_autofree char *ca_path =\n+            g_strdup_printf(\"%s/ca-cert.pem\", tls_dir);\n+        gnutls_certificate_credentials_t cred;\n+        int fd;\n+\n+        g_assert_cmpint(\n+            gnutls_certificate_allocate_credentials(&cred), >=, 0);\n+        g_assert_cmpint(\n+            gnutls_certificate_set_x509_trust_file(\n+                cred, ca_path, GNUTLS_X509_FMT_PEM), >=, 0);\n+\n+        fd = wait_for_vnc_socket(dt.vnc_sock_path, VNC_TEST_TIMEOUT_MS);\n+        g_assert(fd >= 0);\n+        g_assert_true(try_raw_tls_connect(fd, cred));\n+        close(fd);\n+\n+        gnutls_certificate_free_credentials(cred);\n+    }\n+\n+cleanup:\n+    g_clear_object(&proxy);\n+    cleanup_dbus_test(&dt);\n+    cleanup_tls_certs(tls_dir, keyfile);\n+}\n+\n+static void\n+test_dbus_vnc_tls_authz(void)\n+{\n+    DbusTest dt = { 0 };\n+    g_autofree char *tls_dir = NULL;\n+    g_autofree char *keyfile = NULL;\n+    g_autofree char *ca_path = NULL;\n+\n+    if (!g_getenv(\"QTEST_QEMU_VNC_BINARY\")) {\n+        g_test_skip(\"QTEST_QEMU_VNC_BINARY not set\");\n+        return;\n+    }\n+\n+    tls_dir = g_dir_make_tmp(\"dbus-vnc-tls-XXXXXX\", NULL);\n+    g_assert_nonnull(tls_dir);\n+    keyfile = create_tls_certs(tls_dir);\n+\n+    /*\n+     * The client cert has CN=qemu-vnc-test, so the DN string\n+     * reported by GnuTLS is \"CN=qemu-vnc-test\".  Configure\n+     * authz-simple to accept exactly that identity.\n+     */\n+    {\n+        g_autofree char *identity =\n+            g_strdup_printf(\"CN=%s\", CLIENT_CERT_CN);\n+        const char *extra_args[] = {\n+            \"--tls-creds\", tls_dir,\n+            \"--object\",\n+            NULL, /* filled below */\n+            \"--tls-authz\", \"authz0\",\n+            NULL\n+        };\n+        g_autofree char *object_arg =\n+            g_strdup_printf(\"authz-simple,id=authz0,identity=%s\", identity);\n+        extra_args[3] = object_arg;\n+\n+        if (!setup_dbus_test_full(&dt, extra_args)) {\n+            goto cleanup;\n+        }\n+    }\n+\n+    ca_path = g_strdup_printf(\"%s/ca-cert.pem\", tls_dir);\n+\n+    /*\n+     * Connect without a client certificate.\n+     * With verify-peer=yes the TLS handshake must fail.\n+     */\n+    {\n+        gnutls_certificate_credentials_t cred;\n+        int fd;\n+\n+        g_assert_cmpint(\n+            gnutls_certificate_allocate_credentials(&cred), >=, 0);\n+        g_assert_cmpint(\n+            gnutls_certificate_set_x509_trust_file(\n+                cred, ca_path, GNUTLS_X509_FMT_PEM), >=, 0);\n+\n+        fd = wait_for_vnc_socket(dt.vnc_sock_path, VNC_TEST_TIMEOUT_MS);\n+        g_assert(fd >= 0);\n+        g_assert_false(try_raw_tls_connect(fd, cred));\n+        close(fd);\n+\n+        gnutls_certificate_free_credentials(cred);\n+    }\n+\n+    /*\n+     * Connect with a valid client certificate whose DN\n+     * matches the authz-simple identity.  This must succeed.\n+     */\n+    {\n+        g_autofree char *cert_path =\n+            g_strdup_printf(\"%s/client-cert.pem\", tls_dir);\n+        g_autofree char *key_path =\n+            g_strdup_printf(\"%s/key.pem\", tls_dir);\n+        gnutls_certificate_credentials_t cred;\n+        int fd;\n+\n+        g_assert_cmpint(\n+            gnutls_certificate_allocate_credentials(&cred), >=, 0);\n+        g_assert_cmpint(\n+            gnutls_certificate_set_x509_trust_file(\n+                cred, ca_path, GNUTLS_X509_FMT_PEM), >=, 0);\n+        g_assert_cmpint(\n+            gnutls_certificate_set_x509_key_file(\n+                cred, cert_path, key_path, GNUTLS_X509_FMT_PEM), >=, 0);\n+\n+        fd = wait_for_vnc_socket(dt.vnc_sock_path, VNC_TEST_TIMEOUT_MS);\n+        g_assert(fd >= 0);\n+        g_assert_true(try_raw_tls_connect(fd, cred));\n+        close(fd);\n+\n+        gnutls_certificate_free_credentials(cred);\n+    }\n+\n+cleanup:\n+    cleanup_dbus_test(&dt);\n+    cleanup_tls_certs(tls_dir, keyfile);\n+}\n+#endif /* CONFIG_TASN1 */\n+\n+int\n+main(int argc, char **argv)\n+{\n+    g_log_set_always_fatal(G_LOG_LEVEL_WARNING | G_LOG_LEVEL_CRITICAL);\n+\n+    if (getenv(\"GTK_VNC_DEBUG\")) {\n+        vnc_util_set_debug(true);\n+    }\n+\n+    g_test_init(&argc, &argv, NULL);\n+\n+    qtest_add_func(\"/dbus-vnc/basic\", test_dbus_vnc_basic);\n+    qtest_add_func(\"/dbus-vnc/server-props\", test_dbus_vnc_server_props);\n+    qtest_add_func(\"/dbus-vnc/client-lifecycle\", test_dbus_vnc_client_lifecycle);\n+    qtest_add_func(\"/dbus-vnc/no-password\", test_dbus_vnc_no_password);\n+    qtest_add_func(\"/dbus-vnc/password-auth\", test_dbus_vnc_password_auth);\n+    qtest_add_func(\"/dbus-vnc/sasl-authz-no-sasl\",\n+                   test_dbus_vnc_sasl_authz_no_sasl);\n+#ifdef CONFIG_VNC_SASL\n+    qtest_add_func(\"/dbus-vnc/sasl-server-props\",\n+                   test_dbus_vnc_sasl_server_props);\n+    qtest_add_func(\"/dbus-vnc/sasl-auth\",\n+                   test_dbus_vnc_sasl_auth);\n+    qtest_add_func(\"/dbus-vnc/sasl-auth-bad-password\",\n+                   test_dbus_vnc_sasl_auth_bad_password);\n+    qtest_add_func(\"/dbus-vnc/sasl-authz-denied\",\n+                   test_dbus_vnc_sasl_authz_denied);\n+#endif\n+    qtest_add_func(\"/dbus-vnc/tls-authz-no-creds\",\n+                   test_dbus_vnc_tls_authz_no_creds);\n+#ifdef CONFIG_TASN1\n+    qtest_add_func(\"/dbus-vnc/tls-server-props\",\n+                   test_dbus_vnc_tls_server_props);\n+    qtest_add_func(\"/dbus-vnc/tls-authz\",\n+                   test_dbus_vnc_tls_authz);\n+#endif\n+\n+    return g_test_run();\n+}\ndiff --git a/tools/qemu-vnc/audio.c b/tools/qemu-vnc/audio.c\nnew file mode 100644\nindex 00000000000..b55b04bc92a\n--- /dev/null\n+++ b/tools/qemu-vnc/audio.c\n@@ -0,0 +1,307 @@\n+/*\n+ * Standalone VNC server connecting to QEMU via D-Bus display interface.\n+ * Audio support. Only one audio stream is tracked. Mixing/resampling could be added.\n+ *\n+ * Copyright (C) 2026 Red Hat, Inc.\n+ *\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+\n+#include \"qemu/audio.h\"\n+#include \"qemu/audio-capture.h\"\n+#include \"qemu/sockets.h\"\n+#include \"qemu/error-report.h\"\n+#include \"ui/dbus-display1.h\"\n+#include \"trace.h\"\n+#include \"qemu-vnc.h\"\n+\n+struct CaptureVoiceOut {\n+    struct audsettings as;\n+    struct audio_capture_ops ops;\n+    void *opaque;\n+    QLIST_ENTRY(CaptureVoiceOut) entries;\n+};\n+\n+typedef struct AudioOut {\n+    guint64 id;\n+    struct audsettings as;\n+} AudioOut;\n+\n+static QLIST_HEAD(, CaptureVoiceOut) capture_list =\n+    QLIST_HEAD_INITIALIZER(capture_list);\n+static GDBusConnection *audio_listener_conn;\n+static AudioOut audio_out;\n+\n+static bool audsettings_eq(const struct audsettings *a,\n+                           const struct audsettings *b)\n+{\n+    return a->freq == b->freq &&\n+           a->nchannels == b->nchannels &&\n+           a->fmt == b->fmt &&\n+           a->big_endian == b->big_endian;\n+}\n+\n+static gboolean\n+on_audio_out_init(QemuDBusDisplay1AudioOutListener *listener,\n+                  GDBusMethodInvocation *invocation,\n+                  guint64 id, guchar bits, gboolean is_signed,\n+                  gboolean is_float, guint freq, guchar nchannels,\n+                  guint bytes_per_frame, guint bytes_per_second,\n+                  gboolean be, gpointer user_data)\n+{\n+    AudioFormat fmt;\n+\n+    switch (bits) {\n+    case 8:\n+        fmt = is_signed ? AUDIO_FORMAT_S8 : AUDIO_FORMAT_U8;\n+        break;\n+    case 16:\n+        fmt = is_signed ? AUDIO_FORMAT_S16 : AUDIO_FORMAT_U16;\n+        break;\n+    case 32:\n+        fmt = is_float ? AUDIO_FORMAT_F32 :\n+              is_signed ? AUDIO_FORMAT_S32 : AUDIO_FORMAT_U32;\n+        break;\n+    default:\n+        g_return_val_if_reached(DBUS_METHOD_INVOCATION_HANDLED);\n+    }\n+\n+    struct audsettings as = {\n+        .freq = freq,\n+        .nchannels = nchannels,\n+        .fmt = fmt,\n+        .big_endian = be,\n+    };\n+    audio_out = (AudioOut) {\n+        .id = id,\n+        .as = as,\n+    };\n+\n+    trace_qemu_vnc_audio_out_init(id, freq, nchannels, bits);\n+\n+    qemu_dbus_display1_audio_out_listener_complete_init(\n+        listener, invocation);\n+    return DBUS_METHOD_INVOCATION_HANDLED;\n+}\n+\n+static gboolean\n+on_audio_out_fini(QemuDBusDisplay1AudioOutListener *listener,\n+                  GDBusMethodInvocation *invocation,\n+                  guint64 id, gpointer user_data)\n+{\n+    trace_qemu_vnc_audio_out_fini(id);\n+\n+    qemu_dbus_display1_audio_out_listener_complete_fini(\n+        listener, invocation);\n+    return DBUS_METHOD_INVOCATION_HANDLED;\n+}\n+\n+static gboolean\n+on_audio_out_set_enabled(QemuDBusDisplay1AudioOutListener *listener,\n+                         GDBusMethodInvocation *invocation,\n+                         guint64 id, gboolean enabled,\n+                         gpointer user_data)\n+{\n+    CaptureVoiceOut *cap;\n+\n+    trace_qemu_vnc_audio_out_set_enabled(id, enabled);\n+\n+    if (id == audio_out.id) {\n+        QLIST_FOREACH(cap, &capture_list, entries) {\n+            cap->ops.notify(cap->opaque,\n+                            enabled ? AUD_CNOTIFY_ENABLE\n+                                : AUD_CNOTIFY_DISABLE);\n+        }\n+    }\n+\n+    qemu_dbus_display1_audio_out_listener_complete_set_enabled(\n+        listener, invocation);\n+    return DBUS_METHOD_INVOCATION_HANDLED;\n+}\n+\n+static gboolean\n+on_audio_out_set_volume(QemuDBusDisplay1AudioOutListener *listener,\n+                        GDBusMethodInvocation *invocation,\n+                        guint64 id, gboolean mute,\n+                        GVariant *volume, gpointer user_data)\n+{\n+    qemu_dbus_display1_audio_out_listener_complete_set_volume(\n+        listener, invocation);\n+    return DBUS_METHOD_INVOCATION_HANDLED;\n+}\n+\n+static gboolean\n+on_audio_out_write(QemuDBusDisplay1AudioOutListener *listener,\n+                   GDBusMethodInvocation *invocation,\n+                   guint64 id, GVariant *data,\n+                   gpointer user_data)\n+{\n+    CaptureVoiceOut *cap;\n+    gsize size;\n+    const void *buf;\n+\n+    if (id == audio_out.id) {\n+        buf = g_variant_get_fixed_array(data, &size, 1);\n+\n+        trace_qemu_vnc_audio_out_write(id, size);\n+\n+        QLIST_FOREACH(cap, &capture_list, entries) {\n+            /* we don't handle audio resampling/format conversion */\n+            if (audsettings_eq(&cap->as, &audio_out.as)) {\n+                cap->ops.capture(cap->opaque, buf, size);\n+            }\n+        }\n+    }\n+\n+    qemu_dbus_display1_audio_out_listener_complete_write(\n+        listener, invocation);\n+    return DBUS_METHOD_INVOCATION_HANDLED;\n+}\n+\n+CaptureVoiceOut *audio_be_add_capture(\n+    AudioBackend *be,\n+    const struct audsettings *as,\n+    const struct audio_capture_ops *ops,\n+    void *opaque)\n+{\n+    CaptureVoiceOut *cap;\n+\n+    if (!audio_listener_conn) {\n+        return NULL;\n+    }\n+\n+    cap = g_new0(CaptureVoiceOut, 1);\n+    cap->ops = *ops;\n+    cap->opaque = opaque;\n+    cap->as = *as;\n+    QLIST_INSERT_HEAD(&capture_list, cap, entries);\n+\n+    return cap;\n+}\n+\n+void audio_be_del_capture(\n+    AudioBackend *be,\n+    CaptureVoiceOut *cap,\n+    void *cb_opaque)\n+{\n+    if (!cap) {\n+        return;\n+    }\n+\n+    cap->ops.destroy(cap->opaque);\n+    QLIST_REMOVE(cap, entries);\n+    g_free(cap);\n+}\n+\n+/*\n+ * Dummy audio backend — the VNC server only needs a non-NULL pointer\n+ * so that audio capture registration doesn't bail out.  The pointer\n+ * is never dereferenced by our code (audio_be_add_capture ignores it).\n+ */\n+static AudioBackend dummy_audio_be;\n+\n+AudioBackend *audio_get_default_audio_be(Error **errp)\n+{\n+    return &dummy_audio_be;\n+}\n+\n+AudioBackend *audio_be_by_name(const char *name, Error **errp)\n+{\n+    return NULL;\n+}\n+\n+static void\n+on_register_audio_listener_finished(GObject *source_object,\n+                                    GAsyncResult *res,\n+                                    gpointer user_data)\n+{\n+    GThread *thread = user_data;\n+    g_autoptr(GError) err = NULL;\n+    g_autoptr(GDBusObjectSkeleton) obj = NULL;\n+    GDBusObjectManagerServer *server;\n+    QemuDBusDisplay1AudioOutListener *audio_skel;\n+\n+    qemu_dbus_display1_audio_call_register_out_listener_finish(\n+        QEMU_DBUS_DISPLAY1_AUDIO(source_object),\n+        NULL, res, &err);\n+\n+    if (err) {\n+        error_report(\"RegisterOutListener failed: %s\", err->message);\n+        g_thread_join(thread);\n+        return;\n+    }\n+\n+    audio_listener_conn = g_thread_join(thread);\n+    if (!audio_listener_conn) {\n+        return;\n+    }\n+\n+    server = g_dbus_object_manager_server_new(DBUS_DISPLAY1_ROOT);\n+    obj = g_dbus_object_skeleton_new(\n+        DBUS_DISPLAY1_ROOT \"/AudioOutListener\");\n+\n+    audio_skel = qemu_dbus_display1_audio_out_listener_skeleton_new();\n+    g_object_connect(audio_skel,\n+                     \"signal::handle-init\",\n+                     on_audio_out_init, NULL,\n+                     \"signal::handle-fini\",\n+                     on_audio_out_fini, NULL,\n+                     \"signal::handle-set-enabled\",\n+                     on_audio_out_set_enabled, NULL,\n+                     \"signal::handle-set-volume\",\n+                     on_audio_out_set_volume, NULL,\n+                     \"signal::handle-write\",\n+                     on_audio_out_write, NULL,\n+                     NULL);\n+    g_dbus_object_skeleton_add_interface(\n+        obj, G_DBUS_INTERFACE_SKELETON(audio_skel));\n+\n+    g_dbus_object_manager_server_export(server, obj);\n+    g_dbus_object_manager_server_set_connection(\n+        server, audio_listener_conn);\n+\n+    g_dbus_connection_start_message_processing(audio_listener_conn);\n+}\n+\n+void audio_setup(GDBusObjectManager *manager)\n+{\n+    g_autoptr(GError) err = NULL;\n+    g_autoptr(GUnixFDList) fd_list = NULL;\n+    g_autoptr(GDBusInterface) iface = NULL;\n+    GThread *thread;\n+    int pair[2];\n+    int idx;\n+\n+    iface = g_dbus_object_manager_get_interface(\n+        manager, DBUS_DISPLAY1_ROOT \"/Audio\",\n+        \"org.qemu.Display1.Audio\");\n+    if (!iface) {\n+        return;\n+    }\n+\n+    if (qemu_socketpair(AF_UNIX, SOCK_STREAM, 0, pair) < 0) {\n+        error_report(\"audio socketpair failed: %s\", strerror(errno));\n+        return;\n+    }\n+\n+    fd_list = g_unix_fd_list_new();\n+    idx = g_unix_fd_list_append(fd_list, pair[1], &err);\n+    close(pair[1]);\n+    if (idx < 0) {\n+        close(pair[0]);\n+        error_report(\"Failed to append fd: %s\", err->message);\n+        return;\n+    }\n+\n+    thread = p2p_dbus_thread_new(pair[0]);\n+\n+    qemu_dbus_display1_audio_call_register_out_listener(\n+        QEMU_DBUS_DISPLAY1_AUDIO(iface),\n+        g_variant_new_handle(idx),\n+        G_DBUS_CALL_FLAGS_NONE, -1,\n+        fd_list, NULL,\n+        on_register_audio_listener_finished,\n+        thread);\n+}\ndiff --git a/tools/qemu-vnc/chardev.c b/tools/qemu-vnc/chardev.c\nnew file mode 100644\nindex 00000000000..d9d51973724\n--- /dev/null\n+++ b/tools/qemu-vnc/chardev.c\n@@ -0,0 +1,127 @@\n+/*\n+ * Standalone VNC server connecting to QEMU via D-Bus display interface.\n+ *\n+ * Copyright (C) 2026 Red Hat, Inc.\n+ *\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+\n+#include \"qemu/sockets.h\"\n+#include \"qemu/error-report.h\"\n+#include \"ui/dbus-display1.h\"\n+#include \"trace.h\"\n+#include \"qemu-vnc.h\"\n+\n+typedef struct ChardevRegisterData {\n+    QemuDBusDisplay1Chardev *proxy;\n+    int local_fd;\n+    char *name;\n+    bool echo;\n+} ChardevRegisterData;\n+\n+static void\n+on_chardev_register_finished(GObject *source_object,\n+                             GAsyncResult *res,\n+                             gpointer user_data)\n+{\n+    ChardevRegisterData *data = user_data;\n+    g_autoptr(GError) err = NULL;\n+    QemuTextConsole *tc;\n+\n+    if (!qemu_dbus_display1_chardev_call_register_finish(\n+            data->proxy, NULL, res, &err)) {\n+        error_report(\"Chardev Register failed for %s: %s\",\n+                     data->name, err->message);\n+        close(data->local_fd);\n+        goto out;\n+    }\n+\n+    tc = qemu_vnc_text_console_new(data->name, data->local_fd, data->echo);\n+    if (!tc) {\n+        close(data->local_fd);\n+        goto out;\n+    }\n+\n+    trace_qemu_vnc_chardev_connected(data->name);\n+\n+out:\n+    g_object_unref(data->proxy);\n+    g_free(data->name);\n+    g_free(data);\n+}\n+\n+/* Default chardevs to expose as VNC text consoles */\n+static const char * const default_names[] = {\n+    \"org.qemu.console.serial.0\",\n+    \"org.qemu.monitor.hmp.0\",\n+    NULL,\n+};\n+\n+/* Active chardev names list (points to CLI args or default_names) */\n+static const char * const *names;\n+\n+static void\n+chardev_register(QemuDBusDisplay1Chardev *proxy)\n+{\n+    g_autoptr(GUnixFDList) fd_list = NULL;\n+    ChardevRegisterData *data;\n+    const char *name;\n+    int pair[2];\n+    int idx;\n+\n+    name = qemu_dbus_display1_chardev_get_name(proxy);\n+    if (!name || !g_strv_contains(names, name)) {\n+        return;\n+    }\n+\n+    if (qemu_socketpair(AF_UNIX, SOCK_STREAM, 0, pair) < 0) {\n+        error_report(\"chardev socketpair failed: %s\", strerror(errno));\n+        return;\n+    }\n+\n+    fd_list = g_unix_fd_list_new();\n+    idx = g_unix_fd_list_append(fd_list, pair[1], NULL);\n+    close(pair[1]);\n+\n+    data = g_new0(ChardevRegisterData, 1);\n+    data->proxy = g_object_ref(proxy);\n+    data->local_fd = pair[0];\n+    data->name = g_strdup(name);\n+    data->echo = qemu_dbus_display1_chardev_get_echo(proxy);\n+\n+    qemu_dbus_display1_chardev_call_register(\n+        proxy, g_variant_new_handle(idx),\n+        G_DBUS_CALL_FLAGS_NONE, -1,\n+        fd_list, NULL,\n+        on_chardev_register_finished, data);\n+}\n+\n+void chardev_setup(const char * const *chardev_names,\n+                   GDBusObjectManager *manager)\n+{\n+    GList *objects, *l;\n+\n+    names = chardev_names ? chardev_names : default_names;\n+\n+    objects = g_dbus_object_manager_get_objects(manager);\n+    for (l = objects; l; l = l->next) {\n+        GDBusObject *obj = l->data;\n+        const char *path = g_dbus_object_get_object_path(obj);\n+        g_autoptr(GDBusInterface) iface = NULL;\n+\n+        if (!g_str_has_prefix(path, DBUS_DISPLAY1_ROOT \"/Chardev_\")) {\n+            continue;\n+        }\n+\n+        iface = g_dbus_object_get_interface(\n+            obj, \"org.qemu.Display1.Chardev\");\n+        if (!iface) {\n+            continue;\n+        }\n+\n+        chardev_register(QEMU_DBUS_DISPLAY1_CHARDEV(iface));\n+    }\n+    g_list_free_full(objects, g_object_unref);\n+}\ndiff --git a/tools/qemu-vnc/clipboard.c b/tools/qemu-vnc/clipboard.c\nnew file mode 100644\nindex 00000000000..d1673b97899\n--- /dev/null\n+++ b/tools/qemu-vnc/clipboard.c\n@@ -0,0 +1,378 @@\n+/*\n+ * Standalone VNC server connecting to QEMU via D-Bus display interface.\n+ *\n+ * Copyright (C) 2026 Red Hat, Inc.\n+ *\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+\n+#include \"qemu/error-report.h\"\n+#include \"ui/clipboard.h\"\n+#include \"ui/dbus-display1.h\"\n+#include \"trace.h\"\n+#include \"qemu-vnc.h\"\n+\n+#define MIME_TEXT_PLAIN_UTF8 \"text/plain;charset=utf-8\"\n+\n+typedef struct {\n+    GDBusMethodInvocation *invocation;\n+    QemuClipboardType type;\n+    guint timeout_id;\n+} VncDBusClipboardRequest;\n+\n+static QemuDBusDisplay1Clipboard *clipboard_proxy;\n+static QemuDBusDisplay1Clipboard *clipboard_skel;\n+static QemuClipboardPeer clipboard_peer;\n+static uint32_t clipboard_serial;\n+static VncDBusClipboardRequest\n+    clipboard_request[QEMU_CLIPBOARD_SELECTION__COUNT];\n+\n+static void\n+vnc_dbus_clipboard_complete_request(\n+    GDBusMethodInvocation *invocation,\n+    QemuClipboardInfo *info,\n+    QemuClipboardType type)\n+{\n+    GVariant *v_data = g_variant_new_from_data(\n+        G_VARIANT_TYPE(\"ay\"),\n+        info->types[type].data,\n+        info->types[type].size,\n+        TRUE,\n+        (GDestroyNotify)qemu_clipboard_info_unref,\n+        qemu_clipboard_info_ref(info));\n+\n+    qemu_dbus_display1_clipboard_complete_request(\n+        clipboard_skel, invocation,\n+        MIME_TEXT_PLAIN_UTF8, v_data);\n+}\n+\n+static void\n+vnc_dbus_clipboard_request_cancelled(VncDBusClipboardRequest *req)\n+{\n+    if (!req->invocation) {\n+        return;\n+    }\n+\n+    g_dbus_method_invocation_return_error(\n+        req->invocation,\n+        G_DBUS_ERROR,\n+        G_DBUS_ERROR_FAILED,\n+        \"Cancelled clipboard request\");\n+\n+    g_clear_object(&req->invocation);\n+    g_source_remove(req->timeout_id);\n+    req->timeout_id = 0;\n+}\n+\n+static gboolean\n+vnc_dbus_clipboard_request_timeout(gpointer user_data)\n+{\n+    vnc_dbus_clipboard_request_cancelled(user_data);\n+    return G_SOURCE_REMOVE;\n+}\n+\n+static void\n+vnc_dbus_clipboard_request(QemuClipboardInfo *info,\n+                           QemuClipboardType type)\n+{\n+    g_autofree char *mime = NULL;\n+    g_autoptr(GVariant) v_data = NULL;\n+    g_autoptr(GError) err = NULL;\n+    const char *data = NULL;\n+    const char *mimes[] = { MIME_TEXT_PLAIN_UTF8, NULL };\n+    size_t n;\n+\n+    if (type != QEMU_CLIPBOARD_TYPE_TEXT) {\n+        return;\n+    }\n+\n+    if (!clipboard_proxy) {\n+        return;\n+    }\n+\n+    if (!qemu_dbus_display1_clipboard_call_request_sync(\n+            clipboard_proxy,\n+            info->selection,\n+            mimes,\n+            G_DBUS_CALL_FLAGS_NONE, -1, &mime, &v_data, NULL, &err)) {\n+        error_report(\"Failed to request clipboard: %s\", err->message);\n+        return;\n+    }\n+\n+    if (!g_str_equal(mime, MIME_TEXT_PLAIN_UTF8)) {\n+        error_report(\"Unsupported returned MIME: %s\", mime);\n+        return;\n+    }\n+\n+    data = g_variant_get_fixed_array(v_data, &n, 1);\n+    qemu_clipboard_set_data(&clipboard_peer, info, type,\n+                            n, data, true);\n+}\n+\n+static void\n+vnc_dbus_clipboard_update_info(QemuClipboardInfo *info)\n+{\n+    bool self_update = info->owner == &clipboard_peer;\n+    const char *mime[QEMU_CLIPBOARD_TYPE__COUNT + 1] = { 0, };\n+    VncDBusClipboardRequest *req;\n+    int i = 0;\n+\n+    if (info->owner == NULL) {\n+        if (clipboard_proxy) {\n+            qemu_dbus_display1_clipboard_call_release(\n+                clipboard_proxy,\n+                info->selection,\n+                G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);\n+        }\n+        return;\n+    }\n+\n+    if (self_update) {\n+        return;\n+    }\n+\n+    req = &clipboard_request[info->selection];\n+    if (req->invocation && info->types[req->type].data) {\n+        vnc_dbus_clipboard_complete_request(\n+            req->invocation, info, req->type);\n+        g_clear_object(&req->invocation);\n+        g_source_remove(req->timeout_id);\n+        req->timeout_id = 0;\n+        return;\n+    }\n+\n+    if (info->types[QEMU_CLIPBOARD_TYPE_TEXT].available) {\n+        mime[i++] = MIME_TEXT_PLAIN_UTF8;\n+    }\n+\n+    if (i > 0 && clipboard_proxy) {\n+        uint32_t serial = info->has_serial ?\n+            info->serial : ++clipboard_serial;\n+        qemu_dbus_display1_clipboard_call_grab(\n+            clipboard_proxy,\n+            info->selection,\n+            serial,\n+            mime,\n+            G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);\n+    }\n+}\n+\n+static void\n+vnc_dbus_clipboard_notify(Notifier *notifier, void *data)\n+{\n+    QemuClipboardNotify *notify = data;\n+\n+    switch (notify->type) {\n+    case QEMU_CLIPBOARD_UPDATE_INFO:\n+        vnc_dbus_clipboard_update_info(notify->info);\n+        return;\n+    case QEMU_CLIPBOARD_RESET_SERIAL:\n+        if (clipboard_proxy) {\n+            qemu_dbus_display1_clipboard_call_register(\n+                clipboard_proxy,\n+                G_DBUS_CALL_FLAGS_NONE,\n+                -1, NULL, NULL, NULL);\n+        }\n+        return;\n+    }\n+}\n+\n+static gboolean\n+on_clipboard_register(QemuDBusDisplay1Clipboard *clipboard,\n+                      GDBusMethodInvocation *invocation,\n+                      gpointer user_data)\n+{\n+    clipboard_serial = 0;\n+    qemu_clipboard_reset_serial();\n+\n+    qemu_dbus_display1_clipboard_complete_register(\n+        clipboard, invocation);\n+    return DBUS_METHOD_INVOCATION_HANDLED;\n+}\n+\n+static gboolean\n+on_clipboard_unregister(QemuDBusDisplay1Clipboard *clipboard,\n+                        GDBusMethodInvocation *invocation,\n+                        gpointer user_data)\n+{\n+    int i;\n+\n+    for (i = 0; i < G_N_ELEMENTS(clipboard_request); ++i) {\n+        vnc_dbus_clipboard_request_cancelled(&clipboard_request[i]);\n+    }\n+\n+    qemu_dbus_display1_clipboard_complete_unregister(\n+        clipboard, invocation);\n+    return DBUS_METHOD_INVOCATION_HANDLED;\n+}\n+\n+static gboolean\n+on_clipboard_grab(QemuDBusDisplay1Clipboard *clipboard,\n+                  GDBusMethodInvocation *invocation,\n+                  gint arg_selection,\n+                  guint arg_serial,\n+                  const gchar *const *arg_mimes,\n+                  gpointer user_data)\n+{\n+    QemuClipboardSelection s = arg_selection;\n+    g_autoptr(QemuClipboardInfo) info = NULL;\n+\n+    if (s >= QEMU_CLIPBOARD_SELECTION__COUNT) {\n+        g_dbus_method_invocation_return_error(\n+            invocation,\n+            G_DBUS_ERROR,\n+            G_DBUS_ERROR_FAILED,\n+            \"Invalid clipboard selection: %d\", arg_selection);\n+        return DBUS_METHOD_INVOCATION_HANDLED;\n+    }\n+\n+    trace_qemu_vnc_clipboard_grab(arg_selection, arg_serial);\n+\n+    info = qemu_clipboard_info_new(&clipboard_peer, s);\n+    if (g_strv_contains(arg_mimes, MIME_TEXT_PLAIN_UTF8)) {\n+        info->types[QEMU_CLIPBOARD_TYPE_TEXT].available = true;\n+    }\n+    info->serial = arg_serial;\n+    info->has_serial = true;\n+    if (qemu_clipboard_check_serial(info, true)) {\n+        qemu_clipboard_update(info);\n+    }\n+\n+    qemu_dbus_display1_clipboard_complete_grab(\n+        clipboard, invocation);\n+    return DBUS_METHOD_INVOCATION_HANDLED;\n+}\n+\n+static gboolean\n+on_clipboard_release(QemuDBusDisplay1Clipboard *clipboard,\n+                     GDBusMethodInvocation *invocation,\n+                     gint arg_selection,\n+                     gpointer user_data)\n+{\n+    trace_qemu_vnc_clipboard_release(arg_selection);\n+\n+    qemu_clipboard_peer_release(&clipboard_peer, arg_selection);\n+\n+    qemu_dbus_display1_clipboard_complete_release(\n+        clipboard, invocation);\n+    return DBUS_METHOD_INVOCATION_HANDLED;\n+}\n+\n+static gboolean\n+on_clipboard_request(QemuDBusDisplay1Clipboard *clipboard,\n+                     GDBusMethodInvocation *invocation,\n+                     gint arg_selection,\n+                     const gchar *const *arg_mimes,\n+                     gpointer user_data)\n+{\n+    QemuClipboardSelection s = arg_selection;\n+    QemuClipboardType type = QEMU_CLIPBOARD_TYPE_TEXT;\n+    QemuClipboardInfo *info = NULL;\n+\n+    trace_qemu_vnc_clipboard_request(arg_selection);\n+\n+    if (s >= QEMU_CLIPBOARD_SELECTION__COUNT) {\n+        g_dbus_method_invocation_return_error(\n+            invocation,\n+            G_DBUS_ERROR,\n+            G_DBUS_ERROR_FAILED,\n+            \"Invalid clipboard selection: %d\", arg_selection);\n+        return DBUS_METHOD_INVOCATION_HANDLED;\n+    }\n+\n+    if (clipboard_request[s].invocation) {\n+        g_dbus_method_invocation_return_error(\n+            invocation,\n+            G_DBUS_ERROR,\n+            G_DBUS_ERROR_FAILED,\n+            \"Pending request\");\n+        return DBUS_METHOD_INVOCATION_HANDLED;\n+    }\n+\n+    info = qemu_clipboard_info(s);\n+    if (!info || !info->owner || info->owner == &clipboard_peer) {\n+        g_dbus_method_invocation_return_error(\n+            invocation,\n+            G_DBUS_ERROR,\n+            G_DBUS_ERROR_FAILED,\n+            \"Empty clipboard\");\n+        return DBUS_METHOD_INVOCATION_HANDLED;\n+    }\n+\n+    if (!g_strv_contains(arg_mimes, MIME_TEXT_PLAIN_UTF8) ||\n+        !info->types[type].available) {\n+        g_dbus_method_invocation_return_error(\n+            invocation,\n+            G_DBUS_ERROR,\n+            G_DBUS_ERROR_FAILED,\n+            \"Unhandled MIME types requested\");\n+        return DBUS_METHOD_INVOCATION_HANDLED;\n+    }\n+\n+    if (info->types[type].data) {\n+        vnc_dbus_clipboard_complete_request(invocation, info, type);\n+    } else {\n+        qemu_clipboard_request(info, type);\n+\n+        clipboard_request[s].invocation = g_object_ref(invocation);\n+        clipboard_request[s].type = type;\n+        clipboard_request[s].timeout_id =\n+            g_timeout_add_seconds(5,\n+                                  vnc_dbus_clipboard_request_timeout,\n+                                  &clipboard_request[s]);\n+    }\n+\n+    return DBUS_METHOD_INVOCATION_HANDLED;\n+}\n+\n+void clipboard_setup(GDBusObjectManager *manager, GDBusConnection *bus)\n+{\n+    g_autoptr(GError) err = NULL;\n+    g_autoptr(GDBusInterface) iface = NULL;\n+\n+    iface = g_dbus_object_manager_get_interface(\n+        manager, DBUS_DISPLAY1_ROOT \"/Clipboard\",\n+        \"org.qemu.Display1.Clipboard\");\n+    if (!iface) {\n+        return;\n+    }\n+\n+    clipboard_proxy = g_object_ref(QEMU_DBUS_DISPLAY1_CLIPBOARD(iface));\n+\n+    clipboard_skel = qemu_dbus_display1_clipboard_skeleton_new();\n+    g_object_connect(clipboard_skel,\n+                     \"signal::handle-register\",\n+                     on_clipboard_register, NULL,\n+                     \"signal::handle-unregister\",\n+                     on_clipboard_unregister, NULL,\n+                     \"signal::handle-grab\",\n+                     on_clipboard_grab, NULL,\n+                     \"signal::handle-release\",\n+                     on_clipboard_release, NULL,\n+                     \"signal::handle-request\",\n+                     on_clipboard_request, NULL,\n+                     NULL);\n+\n+    if (!g_dbus_interface_skeleton_export(\n+            G_DBUS_INTERFACE_SKELETON(clipboard_skel),\n+            bus,\n+            DBUS_DISPLAY1_ROOT \"/Clipboard\",\n+            &err)) {\n+        error_report(\"Failed to export clipboard: %s\", err->message);\n+        g_clear_object(&clipboard_skel);\n+        g_clear_object(&clipboard_proxy);\n+        return;\n+    }\n+\n+    clipboard_peer.name = \"dbus\";\n+    clipboard_peer.notifier.notify = vnc_dbus_clipboard_notify;\n+    clipboard_peer.request = vnc_dbus_clipboard_request;\n+    qemu_clipboard_peer_register(&clipboard_peer);\n+\n+    qemu_dbus_display1_clipboard_call_register(\n+        clipboard_proxy,\n+        G_DBUS_CALL_FLAGS_NONE,\n+        -1, NULL, NULL, NULL);\n+}\ndiff --git a/tools/qemu-vnc/console.c b/tools/qemu-vnc/console.c\nnew file mode 100644\nindex 00000000000..076365adf77\n--- /dev/null\n+++ b/tools/qemu-vnc/console.c\n@@ -0,0 +1,168 @@\n+/*\n+ * Minimal QemuConsole helpers for the standalone qemu-vnc binary.\n+ *\n+ * Copyright (C) 2026 Red Hat, Inc.\n+ *\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+\n+#include \"ui/console.h\"\n+#include \"ui/console-priv.h\"\n+#include \"ui/vt100.h\"\n+#include \"qemu-vnc.h\"\n+#include \"trace.h\"\n+\n+/*\n+ * Our own QemuTextConsole definition — the one in console-vc.c uses\n+ * a Chardev* backend which is not available in the standalone binary.\n+ * Here we drive the VT100 emulator directly over a raw file descriptor.\n+ */\n+typedef struct QemuTextConsole {\n+    QemuConsole parent;\n+    QemuVT100 vt;\n+    int chardev_fd;\n+    guint io_watch_id;\n+    char *name;\n+} QemuTextConsole;\n+\n+typedef QemuConsoleClass QemuTextConsoleClass;\n+\n+OBJECT_DEFINE_TYPE(QemuTextConsole, qemu_text_console,\n+                   QEMU_TEXT_CONSOLE, QEMU_CONSOLE)\n+\n+static void qemu_text_console_class_init(ObjectClass *oc, const void *data)\n+{\n+}\n+\n+static void text_console_invalidate(void *opaque)\n+{\n+    QemuTextConsole *s = QEMU_TEXT_CONSOLE(opaque);\n+\n+    vt100_set_image(&s->vt, QEMU_CONSOLE(s)->surface->image);\n+    vt100_refresh(&s->vt);\n+}\n+\n+static const GraphicHwOps text_console_ops = {\n+    .invalidate  = text_console_invalidate,\n+};\n+\n+static void qemu_text_console_init(Object *obj)\n+{\n+    QemuTextConsole *c = QEMU_TEXT_CONSOLE(obj);\n+\n+    QEMU_CONSOLE(c)->hw_ops = &text_console_ops;\n+    QEMU_CONSOLE(c)->hw = c;\n+}\n+\n+static void qemu_text_console_finalize(Object *obj)\n+{\n+    QemuTextConsole *tc = QEMU_TEXT_CONSOLE(obj);\n+\n+    vt100_fini(&tc->vt);\n+    if (tc->io_watch_id) {\n+        g_source_remove(tc->io_watch_id);\n+    }\n+    if (tc->chardev_fd >= 0) {\n+        close(tc->chardev_fd);\n+    }\n+    g_free(tc->name);\n+}\n+\n+\n+static void text_console_out_flush(QemuVT100 *vt)\n+{\n+    QemuTextConsole *tc = container_of(vt, QemuTextConsole, vt);\n+    const uint8_t *data;\n+    uint32_t len;\n+\n+    while (!fifo8_is_empty(&vt->out_fifo)) {\n+        ssize_t ret;\n+\n+        data = fifo8_pop_bufptr(&vt->out_fifo,\n+                                fifo8_num_used(&vt->out_fifo), &len);\n+        ret = write(tc->chardev_fd, data, len);\n+        if (ret < 0) {\n+            trace_qemu_vnc_console_io_error(tc->name);\n+            break;\n+        }\n+    }\n+}\n+\n+static void text_console_image_update(QemuVT100 *vt, int x, int y, int w, int h)\n+{\n+    QemuTextConsole *tc = container_of(vt, QemuTextConsole, vt);\n+    QemuConsole *con = QEMU_CONSOLE(tc);\n+\n+    qemu_console_update(con, x, y, w, h);\n+}\n+\n+static gboolean text_console_io_cb(GIOChannel *source,\n+    GIOCondition cond, gpointer data)\n+{\n+    QemuTextConsole *tc = data;\n+    uint8_t buf[4096];\n+    ssize_t n;\n+\n+    if (cond & (G_IO_HUP | G_IO_ERR)) {\n+        tc->io_watch_id = 0;\n+        return G_SOURCE_REMOVE;\n+    }\n+\n+    n = read(tc->chardev_fd, buf, sizeof(buf));\n+    if (n <= 0) {\n+        trace_qemu_vnc_console_io_error(tc->name);\n+        tc->io_watch_id = 0;\n+        return G_SOURCE_REMOVE;\n+    }\n+\n+    vt100_input(&tc->vt, buf, n);\n+    return G_SOURCE_CONTINUE;\n+}\n+\n+QemuTextConsole *qemu_vnc_text_console_new(const char *name,\n+                                           int fd, bool echo)\n+{\n+    int w = TEXT_COLS * TEXT_FONT_WIDTH;\n+    int h = TEXT_ROWS * TEXT_FONT_HEIGHT;\n+    QemuTextConsole *tc;\n+    QemuConsole *con;\n+    pixman_image_t *image;\n+    GIOChannel *chan;\n+\n+    tc = QEMU_TEXT_CONSOLE(object_new(TYPE_QEMU_TEXT_CONSOLE));\n+    con = QEMU_CONSOLE(tc);\n+\n+    tc->name = g_strdup(name);\n+    tc->chardev_fd = fd;\n+\n+    image = pixman_image_create_bits(PIXMAN_x8r8g8b8, w, h, NULL, 0);\n+    con->surface = qemu_create_displaysurface_pixman(image);\n+    con->scanout.kind = SCANOUT_SURFACE;\n+    qemu_pixman_image_unref(image);\n+\n+    vt100_init(&tc->vt, con->surface->image,\n+               text_console_image_update, text_console_out_flush);\n+    tc->vt.echo = echo;\n+    vt100_refresh(&tc->vt);\n+\n+    chan = g_io_channel_unix_new(fd);\n+    g_io_channel_set_encoding(chan, NULL, NULL);\n+    tc->io_watch_id = g_io_add_watch(chan,\n+                                      G_IO_IN | G_IO_HUP | G_IO_ERR,\n+                                      text_console_io_cb, tc);\n+    g_io_channel_unref(chan);\n+\n+    return tc;\n+}\n+\n+void qemu_text_console_handle_keysym(QemuTextConsole *s, int keysym)\n+{\n+    vt100_keysym(&s->vt, keysym);\n+}\n+\n+void qemu_text_console_update_size(QemuTextConsole *c)\n+{\n+    qemu_console_text_resize(QEMU_CONSOLE(c), c->vt.width, c->vt.height);\n+}\ndiff --git a/tools/qemu-vnc/dbus.c b/tools/qemu-vnc/dbus.c\nnew file mode 100644\nindex 00000000000..0e5f52623ea\n--- /dev/null\n+++ b/tools/qemu-vnc/dbus.c\n@@ -0,0 +1,439 @@\n+/*\n+ * D-Bus interface for qemu-vnc standalone VNC server.\n+ *\n+ * Copyright (C) 2026 Red Hat, Inc.\n+ *\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+\n+#include \"qemu/cutils.h\"\n+#include \"qapi-types-trace.h\"\n+#include \"system/system.h\"\n+#include \"qapi/qapi-types-ui.h\"\n+#include \"qapi/qapi-commands-ui.h\"\n+#include \"qemu-vnc.h\"\n+#include \"qemu-vnc1.h\"\n+#include \"qapi/qapi-emit-events.h\"\n+#include \"qobject/qdict.h\"\n+#include \"ui/vnc.h\"\n+#include \"trace.h\"\n+\n+typedef struct VncDbusClient {\n+    QemuVnc1ClientSkeleton *skeleton;\n+    char *path;\n+    char *host;\n+    char *service;\n+    unsigned int id;\n+    QTAILQ_ENTRY(VncDbusClient) next;\n+} VncDbusClient;\n+\n+static QemuVnc1ServerSkeleton *server_skeleton;\n+static GDBusObjectManagerServer *obj_manager;\n+static unsigned int next_client_id;\n+\n+static QTAILQ_HEAD(, VncDbusClient)\n+    dbus_clients = QTAILQ_HEAD_INITIALIZER(dbus_clients);\n+\n+static VncDbusClient *vnc_dbus_find_client(const char *host,\n+                                           const char *service)\n+{\n+    VncDbusClient *c;\n+\n+    QTAILQ_FOREACH(c, &dbus_clients, next) {\n+        if (g_str_equal(c->host, host) &&\n+            g_str_equal(c->service, service)) {\n+            return c;\n+        }\n+    }\n+    return NULL;\n+}\n+\n+static void vnc_dbus_update_clients_property(void)\n+{\n+    VncDbusClient *c;\n+    GPtrArray *paths;\n+    const char **strv;\n+\n+    paths = g_ptr_array_new();\n+    QTAILQ_FOREACH(c, &dbus_clients, next) {\n+        g_ptr_array_add(paths, c->path);\n+    }\n+    g_ptr_array_add(paths, NULL);\n+\n+    strv = (const char **)paths->pdata;\n+    qemu_vnc1_server_set_clients(QEMU_VNC1_SERVER(server_skeleton), strv);\n+    g_ptr_array_free(paths, TRUE);\n+}\n+\n+void vnc_dbus_client_connected(const char *host, const char *service,\n+                               const char *family, bool websocket)\n+{\n+    VncDbusClient *c;\n+    g_autoptr(GDBusObjectSkeleton) obj = NULL;\n+\n+    if (!server_skeleton) {\n+        return;\n+    }\n+\n+    c = g_new0(VncDbusClient, 1);\n+    c->id = next_client_id++;\n+    c->host = g_strdup(host);\n+    c->service = g_strdup(service);\n+    c->path = g_strdup_printf(\"/org/qemu/Vnc1/Client_%u\", c->id);\n+\n+    c->skeleton = QEMU_VNC1_CLIENT_SKELETON(qemu_vnc1_client_skeleton_new());\n+    qemu_vnc1_client_set_host(QEMU_VNC1_CLIENT(c->skeleton), host);\n+    qemu_vnc1_client_set_service(QEMU_VNC1_CLIENT(c->skeleton), service);\n+    qemu_vnc1_client_set_family(QEMU_VNC1_CLIENT(c->skeleton), family);\n+    qemu_vnc1_client_set_web_socket(QEMU_VNC1_CLIENT(c->skeleton), websocket);\n+    qemu_vnc1_client_set_x509_dname(QEMU_VNC1_CLIENT(c->skeleton), \"\");\n+    qemu_vnc1_client_set_sasl_username(QEMU_VNC1_CLIENT(c->skeleton), \"\");\n+\n+    obj = g_dbus_object_skeleton_new(c->path);\n+    g_dbus_object_skeleton_add_interface(\n+        obj, G_DBUS_INTERFACE_SKELETON(c->skeleton));\n+    g_dbus_object_manager_server_export(obj_manager, obj);\n+\n+    QTAILQ_INSERT_TAIL(&dbus_clients, c, next);\n+    vnc_dbus_update_clients_property();\n+\n+    qemu_vnc1_server_emit_client_connected(\n+        QEMU_VNC1_SERVER(server_skeleton), c->path);\n+}\n+\n+void vnc_dbus_client_initialized(const char *host, const char *service,\n+                                 const char *x509_dname,\n+                                 const char *sasl_username)\n+{\n+    VncDbusClient *c;\n+\n+    if (!server_skeleton) {\n+        return;\n+    }\n+\n+    c = vnc_dbus_find_client(host, service);\n+    if (!c) {\n+        trace_qemu_vnc_client_not_found(host, service);\n+        return;\n+    }\n+\n+    if (x509_dname) {\n+        qemu_vnc1_client_set_x509_dname(\n+            QEMU_VNC1_CLIENT(c->skeleton), x509_dname);\n+    }\n+    if (sasl_username) {\n+        qemu_vnc1_client_set_sasl_username(\n+            QEMU_VNC1_CLIENT(c->skeleton), sasl_username);\n+    }\n+\n+    qemu_vnc1_server_emit_client_initialized(\n+        QEMU_VNC1_SERVER(server_skeleton), c->path);\n+}\n+\n+void vnc_dbus_client_disconnected(const char *host, const char *service)\n+{\n+    VncDbusClient *c;\n+\n+    if (!server_skeleton) {\n+        return;\n+    }\n+\n+    c = vnc_dbus_find_client(host, service);\n+    if (!c) {\n+        trace_qemu_vnc_client_not_found(host, service);\n+        return;\n+    }\n+\n+    qemu_vnc1_server_emit_client_disconnected(\n+        QEMU_VNC1_SERVER(server_skeleton), c->path);\n+\n+    g_dbus_object_manager_server_unexport(obj_manager, c->path);\n+    QTAILQ_REMOVE(&dbus_clients, c, next);\n+    vnc_dbus_update_clients_property();\n+\n+    g_object_unref(c->skeleton);\n+    g_free(c->path);\n+    g_free(c->host);\n+    g_free(c->service);\n+    g_free(c);\n+}\n+\n+static gboolean\n+on_set_password(QemuVnc1Server *iface,\n+                GDBusMethodInvocation *invocation,\n+                const gchar *password,\n+                gpointer user_data)\n+{\n+    Error *err = NULL;\n+\n+    if (vnc_display_password(\"default\", password, &err) < 0) {\n+        g_dbus_method_invocation_return_error(\n+            invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,\n+            \"%s\", error_get_pretty(err));\n+        error_free(err);\n+        return TRUE;\n+    }\n+\n+    qemu_vnc1_server_complete_set_password(iface, invocation);\n+    return TRUE;\n+}\n+\n+static gboolean\n+on_expire_password(QemuVnc1Server *iface,\n+                   GDBusMethodInvocation *invocation,\n+                   const gchar *time_str,\n+                   gpointer user_data)\n+{\n+    time_t when;\n+\n+    if (g_str_equal(time_str, \"now\")) {\n+        when = 0;\n+    } else if (g_str_equal(time_str, \"never\")) {\n+        when = TIME_MAX;\n+    } else if (time_str[0] == '+') {\n+        int seconds;\n+        if (qemu_strtoi(time_str + 1, NULL, 10, &seconds) < 0) {\n+            g_dbus_method_invocation_return_error(\n+                invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,\n+                \"Invalid time format: %s\", time_str);\n+            return TRUE;\n+        }\n+        when = time(NULL) + seconds;\n+    } else {\n+        int64_t epoch;\n+        if (qemu_strtoi64(time_str, NULL, 10, &epoch) < 0) {\n+            g_dbus_method_invocation_return_error(\n+                invocation, G_DBUS_ERROR, G_DBUS_ERROR_INVALID_ARGS,\n+                \"Invalid time format: %s\", time_str);\n+            return TRUE;\n+        }\n+        when = epoch;\n+    }\n+\n+    if (vnc_display_pw_expire(\"default\", when) < 0) {\n+        g_dbus_method_invocation_return_error(\n+            invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,\n+            \"Failed to set password expiry\");\n+        return TRUE;\n+    }\n+\n+    qemu_vnc1_server_complete_expire_password(iface, invocation);\n+    return TRUE;\n+}\n+\n+static gboolean\n+on_reload_certificates(QemuVnc1Server *iface,\n+                       GDBusMethodInvocation *invocation,\n+                       gpointer user_data)\n+{\n+    Error *err = NULL;\n+\n+    if (!vnc_display_reload_certs(\"default\", &err)) {\n+        g_dbus_method_invocation_return_error(\n+            invocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,\n+            \"%s\", error_get_pretty(err));\n+        error_free(err);\n+        return TRUE;\n+    }\n+\n+    qemu_vnc1_server_complete_reload_certificates(iface, invocation);\n+    return TRUE;\n+}\n+\n+static void vnc_dbus_add_listeners(VncInfo2 *info)\n+{\n+    GVariantBuilder builder;\n+    VncServerInfo2List *entry;\n+\n+    g_variant_builder_init(&builder, G_VARIANT_TYPE(\"aa{sv}\"));\n+\n+    for (entry = info->server; entry; entry = entry->next) {\n+        VncServerInfo2 *s = entry->value;\n+        const char *vencrypt_str = \"\";\n+\n+        if (s->has_vencrypt) {\n+            vencrypt_str = VncVencryptSubAuth_str(s->vencrypt);\n+        }\n+\n+        g_variant_builder_open(&builder, G_VARIANT_TYPE(\"a{sv}\"));\n+        g_variant_builder_add(&builder, \"{sv}\", \"Host\",\n+                              g_variant_new_string(s->host));\n+        g_variant_builder_add(&builder, \"{sv}\", \"Service\",\n+                              g_variant_new_string(s->service));\n+        g_variant_builder_add(&builder, \"{sv}\", \"Family\",\n+                              g_variant_new_string(\n+                                  NetworkAddressFamily_str(s->family)));\n+        g_variant_builder_add(&builder, \"{sv}\", \"WebSocket\",\n+                              g_variant_new_boolean(s->websocket));\n+        g_variant_builder_add(&builder, \"{sv}\", \"Auth\",\n+                              g_variant_new_string(\n+                                  VncPrimaryAuth_str(s->auth)));\n+        g_variant_builder_add(&builder, \"{sv}\", \"VencryptSubAuth\",\n+                              g_variant_new_string(vencrypt_str));\n+        g_variant_builder_close(&builder);\n+    }\n+\n+    qemu_vnc1_server_set_listeners(\n+        QEMU_VNC1_SERVER(server_skeleton),\n+        g_variant_builder_end(&builder));\n+}\n+\n+void vnc_dbus_setup(GDBusConnection *bus)\n+{\n+    g_autoptr(GDBusObjectSkeleton) server_obj = NULL;\n+    VncInfo2List *info_list;\n+    Error *err = NULL;\n+    const char *auth_str = \"none\";\n+    const char *vencrypt_str = \"\";\n+\n+    obj_manager = g_dbus_object_manager_server_new(\"/org/qemu/Vnc1\");\n+\n+    server_skeleton = QEMU_VNC1_SERVER_SKELETON(\n+        qemu_vnc1_server_skeleton_new());\n+\n+    qemu_vnc1_server_set_name(QEMU_VNC1_SERVER(server_skeleton),\n+                              qemu_name ? qemu_name : \"\");\n+    qemu_vnc1_server_set_clients(QEMU_VNC1_SERVER(server_skeleton), NULL);\n+\n+    /* Query auth info from the VNC display */\n+    info_list = qmp_query_vnc_servers(&err);\n+    if (info_list) {\n+        VncInfo2 *info = info_list->value;\n+        auth_str = VncPrimaryAuth_str(info->auth);\n+        if (info->has_vencrypt) {\n+            vencrypt_str = VncVencryptSubAuth_str(info->vencrypt);\n+        }\n+        vnc_dbus_add_listeners(info);\n+    }\n+\n+    qemu_vnc1_server_set_auth(QEMU_VNC1_SERVER(server_skeleton), auth_str);\n+    qemu_vnc1_server_set_vencrypt_sub_auth(\n+        QEMU_VNC1_SERVER(server_skeleton), vencrypt_str);\n+\n+    qapi_free_VncInfo2List(info_list);\n+\n+    g_signal_connect(server_skeleton, \"handle-set-password\",\n+                     G_CALLBACK(on_set_password), NULL);\n+    g_signal_connect(server_skeleton, \"handle-expire-password\",\n+                     G_CALLBACK(on_expire_password), NULL);\n+    g_signal_connect(server_skeleton, \"handle-reload-certificates\",\n+                     G_CALLBACK(on_reload_certificates), NULL);\n+\n+    server_obj = g_dbus_object_skeleton_new(\"/org/qemu/Vnc1/Server\");\n+    g_dbus_object_skeleton_add_interface(\n+        server_obj, G_DBUS_INTERFACE_SKELETON(server_skeleton));\n+    g_dbus_object_manager_server_export(obj_manager, server_obj);\n+\n+    g_dbus_object_manager_server_set_connection(obj_manager, bus);\n+\n+    if (g_dbus_connection_get_flags(bus)\n+        & G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION) {\n+        g_bus_own_name_on_connection(\n+            bus, \"org.qemu.vnc\",\n+            G_BUS_NAME_OWNER_FLAGS_NONE,\n+            NULL, NULL, NULL, NULL);\n+    }\n+}\n+\n+void vnc_action_shutdown(VncState *vs)\n+{\n+    VncDbusClient *c;\n+\n+    c = vnc_dbus_find_client(vs->info->host, vs->info->service);\n+    if (!c) {\n+        trace_qemu_vnc_client_not_found(vs->info->host, vs->info->service);\n+        return;\n+    }\n+\n+    qemu_vnc1_client_emit_shutdown_request(QEMU_VNC1_CLIENT(c->skeleton));\n+}\n+\n+void vnc_action_reset(VncState *vs)\n+{\n+    VncDbusClient *c;\n+\n+    c = vnc_dbus_find_client(vs->info->host, vs->info->service);\n+    if (!c) {\n+        trace_qemu_vnc_client_not_found(vs->info->host, vs->info->service);\n+        return;\n+    }\n+\n+    qemu_vnc1_client_emit_reset_request(QEMU_VNC1_CLIENT(c->skeleton));\n+}\n+\n+/*\n+ * Override the stub qapi_event_emit() to capture VNC events\n+ * and forward them to the D-Bus interface.\n+ */\n+void qapi_event_emit(QAPIEvent event, QDict *qdict)\n+{\n+    QDict *data, *client;\n+    const char *host, *service, *family;\n+    bool websocket;\n+\n+    if (event != QAPI_EVENT_VNC_CONNECTED &&\n+        event != QAPI_EVENT_VNC_INITIALIZED &&\n+        event != QAPI_EVENT_VNC_DISCONNECTED) {\n+        return;\n+    }\n+\n+    data = qdict_get_qdict(qdict, \"data\");\n+    if (!data) {\n+        return;\n+    }\n+\n+    client = qdict_get_qdict(data, \"client\");\n+    if (!client) {\n+        return;\n+    }\n+\n+    host = qdict_get_str(client, \"host\");\n+    service = qdict_get_str(client, \"service\");\n+    family = qdict_get_str(client, \"family\");\n+    websocket = qdict_get_bool(client, \"websocket\");\n+\n+    switch (event) {\n+    case QAPI_EVENT_VNC_CONNECTED:\n+        vnc_dbus_client_connected(host, service, family, websocket);\n+        break;\n+    case QAPI_EVENT_VNC_INITIALIZED: {\n+        const char *x509_dname = NULL;\n+        const char *sasl_username = NULL;\n+\n+        if (qdict_haskey(client, \"x509_dname\")) {\n+            x509_dname = qdict_get_str(client, \"x509_dname\");\n+        }\n+        if (qdict_haskey(client, \"sasl_username\")) {\n+            sasl_username = qdict_get_str(client, \"sasl_username\");\n+        }\n+        vnc_dbus_client_initialized(host, service,\n+                                    x509_dname, sasl_username);\n+        break;\n+    }\n+    case QAPI_EVENT_VNC_DISCONNECTED:\n+        vnc_dbus_client_disconnected(host, service);\n+        break;\n+    default:\n+        break;\n+    }\n+}\n+\n+void vnc_dbus_cleanup(void)\n+{\n+    VncDbusClient *c, *next;\n+\n+    QTAILQ_FOREACH_SAFE(c, &dbus_clients, next, next) {\n+        g_dbus_object_manager_server_unexport(obj_manager, c->path);\n+        QTAILQ_REMOVE(&dbus_clients, c, next);\n+        g_object_unref(c->skeleton);\n+        g_free(c->path);\n+        g_free(c->host);\n+        g_free(c->service);\n+        g_free(c);\n+    }\n+\n+    g_clear_object(&server_skeleton);\n+    g_clear_object(&obj_manager);\n+}\ndiff --git a/tools/qemu-vnc/display.c b/tools/qemu-vnc/display.c\nnew file mode 100644\nindex 00000000000..8fe9b6fc898\n--- /dev/null\n+++ b/tools/qemu-vnc/display.c\n@@ -0,0 +1,456 @@\n+/*\n+ * D-Bus display listener — scanout, update and cursor handling.\n+ *\n+ * Copyright (C) 2026 Red Hat, Inc.\n+ *\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+\n+#include \"qemu/sockets.h\"\n+#include \"qemu/error-report.h\"\n+#include \"ui/console-priv.h\"\n+#include \"ui/dbus-display1.h\"\n+#include \"ui/surface.h\"\n+#include \"trace.h\"\n+#include \"qemu-vnc.h\"\n+\n+typedef struct ConsoleData {\n+    QemuDBusDisplay1Console *console_proxy;\n+    QemuDBusDisplay1Keyboard *keyboard_proxy;\n+    QemuDBusDisplay1Mouse *mouse_proxy;\n+    QemuGraphicConsole *gfx_con;\n+    GDBusConnection *listener_conn;\n+    /*\n+     * When true the surface is backed by a read-only mmap (ScanoutMap path)\n+     * and Update messages must be rejected because compositing into the\n+     * surface is not possible.  The plain Scanout path provides a writable\n+     * copy and clears this flag.\n+     */\n+    bool read_only;\n+} ConsoleData;\n+\n+static void display_ui_info(void *opaque, uint32_t head, QemuUIInfo *info)\n+{\n+    ConsoleData *cd = opaque;\n+    g_autoptr(GError) err = NULL;\n+\n+    if (!cd || !cd->console_proxy) {\n+        return;\n+    }\n+\n+    qemu_dbus_display1_console_call_set_uiinfo_sync(\n+        cd->console_proxy,\n+        info->width_mm, info->height_mm,\n+        info->xoff, info->yoff,\n+        info->width, info->height,\n+        G_DBUS_CALL_FLAGS_NONE, -1, NULL, &err);\n+    if (err) {\n+        error_report(\"SetUIInfo failed: %s\", err->message);\n+    }\n+}\n+\n+static void\n+scanout_image_destroy(pixman_image_t *image, void *data)\n+{\n+    g_variant_unref(data);\n+}\n+\n+typedef struct {\n+    void *addr;\n+    size_t len;\n+} ScanoutMapData;\n+\n+static void\n+scanout_map_destroy(pixman_image_t *image, void *data)\n+{\n+    ScanoutMapData *map = data;\n+    munmap(map->addr, map->len);\n+    g_free(map);\n+}\n+\n+static gboolean\n+on_scanout(QemuDBusDisplay1Listener *listener,\n+           GDBusMethodInvocation *invocation,\n+           guint width, guint height, guint stride,\n+           guint pixman_format, GVariant *data,\n+           gpointer user_data)\n+{\n+    ConsoleData *cd = user_data;\n+    QemuConsole *con = QEMU_CONSOLE(cd->gfx_con);\n+    gsize size;\n+    const uint8_t *pixels;\n+    pixman_image_t *image;\n+    DisplaySurface *surface;\n+\n+    trace_qemu_vnc_scanout(width, height, stride, pixman_format);\n+\n+    pixels = g_variant_get_fixed_array(data, &size, 1);\n+\n+    image = pixman_image_create_bits((pixman_format_code_t)pixman_format,\n+        width, height, (uint32_t *)pixels, stride);\n+    assert(image);\n+\n+    g_variant_ref(data);\n+    pixman_image_set_destroy_function(image, scanout_image_destroy, data);\n+\n+    cd->read_only = false;\n+    surface = qemu_create_displaysurface_pixman(image);\n+    qemu_console_set_surface(con, surface);\n+\n+    qemu_dbus_display1_listener_complete_scanout(listener, invocation);\n+    return DBUS_METHOD_INVOCATION_HANDLED;\n+}\n+\n+static gboolean\n+on_update(QemuDBusDisplay1Listener *listener,\n+          GDBusMethodInvocation *invocation,\n+          gint x, gint y, gint w, gint h,\n+          guint stride, guint pixman_format, GVariant *data,\n+          gpointer user_data)\n+{\n+    ConsoleData *cd = user_data;\n+    QemuConsole *con = QEMU_CONSOLE(cd->gfx_con);\n+    DisplaySurface *surface = qemu_console_surface(con);\n+    gsize size;\n+    const uint8_t *pixels;\n+    pixman_image_t *src;\n+\n+    trace_qemu_vnc_update(x, y, w, h, stride, pixman_format);\n+    if (!surface || cd->read_only) {\n+        g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,\n+            G_DBUS_ERROR_FAILED, \"No active or writable console\");\n+        return DBUS_METHOD_INVOCATION_HANDLED;\n+    }\n+\n+    pixels = g_variant_get_fixed_array(data, &size, 1);\n+    src = pixman_image_create_bits((pixman_format_code_t)pixman_format,\n+        w, h, (uint32_t *)pixels, stride);\n+    assert(src);\n+    pixman_image_composite(PIXMAN_OP_SRC, src, NULL,\n+            surface->image,\n+            0, 0, 0, 0, x, y, w, h);\n+    pixman_image_unref(src);\n+\n+    qemu_console_update(con, x, y, w, h);\n+\n+    qemu_dbus_display1_listener_complete_update(listener, invocation);\n+    return DBUS_METHOD_INVOCATION_HANDLED;\n+}\n+\n+static gboolean\n+on_scanout_map(QemuDBusDisplay1ListenerUnixMap *listener,\n+               GDBusMethodInvocation *invocation,\n+               GUnixFDList *fd_list,\n+               GVariant *arg_handle,\n+               guint offset, guint width, guint height,\n+               guint stride, guint pixman_format,\n+               gpointer user_data)\n+{\n+    ConsoleData *cd = user_data;\n+    gint32 handle = g_variant_get_handle(arg_handle);\n+    g_autoptr(GError) err = NULL;\n+    DisplaySurface *surface;\n+    int fd;\n+    void *addr;\n+    size_t len = (size_t)height * stride;\n+    pixman_image_t *image;\n+\n+    trace_qemu_vnc_scanout_map(width, height, stride, pixman_format, offset);\n+\n+    fd = g_unix_fd_list_get(fd_list, handle, &err);\n+    if (fd < 0) {\n+        g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,\n+            G_DBUS_ERROR_FAILED, \"Failed to get fd: %s\", err->message);\n+        return DBUS_METHOD_INVOCATION_HANDLED;\n+    }\n+\n+    /* MAP_PRIVATE: we only read; avoid propagating writes back to QEMU */\n+    addr = mmap(NULL, len, PROT_READ, MAP_PRIVATE, fd, offset);\n+    close(fd);\n+    if (addr == MAP_FAILED) {\n+        g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR,\n+            G_DBUS_ERROR_FAILED, \"mmap failed: %s\", g_strerror(errno));\n+        return DBUS_METHOD_INVOCATION_HANDLED;\n+    }\n+\n+    image = pixman_image_create_bits((pixman_format_code_t)pixman_format,\n+                                     width, height, addr, stride);\n+    assert(image);\n+    {\n+        ScanoutMapData *map = g_new0(ScanoutMapData, 1);\n+        map->addr = addr;\n+        map->len = len;\n+        pixman_image_set_destroy_function(image, scanout_map_destroy, map);\n+    }\n+\n+    cd->read_only = true;\n+    surface = qemu_create_displaysurface_pixman(image);\n+    qemu_console_set_surface(QEMU_CONSOLE(cd->gfx_con), surface);\n+\n+    qemu_dbus_display1_listener_unix_map_complete_scanout_map(\n+        listener, invocation, NULL);\n+    return DBUS_METHOD_INVOCATION_HANDLED;\n+}\n+\n+static gboolean\n+on_update_map(QemuDBusDisplay1ListenerUnixMap *listener,\n+              GDBusMethodInvocation *invocation,\n+              guint x, guint y, guint w, guint h,\n+              gpointer user_data)\n+{\n+    ConsoleData *cd = user_data;\n+\n+    trace_qemu_vnc_update_map(x, y, w, h);\n+\n+    qemu_console_update(QEMU_CONSOLE(cd->gfx_con), x, y, w, h);\n+\n+    qemu_dbus_display1_listener_unix_map_complete_update_map(\n+        listener, invocation);\n+    return DBUS_METHOD_INVOCATION_HANDLED;\n+}\n+\n+static gboolean\n+on_cursor_define(QemuDBusDisplay1Listener *listener,\n+                 GDBusMethodInvocation *invocation,\n+                 gint width, gint height,\n+                 gint hot_x, gint hot_y,\n+                 GVariant *data,\n+                 gpointer user_data)\n+{\n+    ConsoleData *cd = user_data;\n+    gsize size;\n+    const uint8_t *pixels;\n+    QEMUCursor *c;\n+\n+    trace_qemu_vnc_cursor_define(width, height, hot_x, hot_y);\n+\n+    c = cursor_alloc(width, height);\n+    if (!c) {\n+        qemu_dbus_display1_listener_complete_cursor_define(\n+            listener, invocation);\n+        return DBUS_METHOD_INVOCATION_HANDLED;\n+    }\n+\n+    c->hot_x = hot_x;\n+    c->hot_y = hot_y;\n+\n+    pixels = g_variant_get_fixed_array(data, &size, 1);\n+    memcpy(c->data, pixels, MIN(size, (gsize)width * height * 4));\n+\n+    qemu_console_set_cursor(QEMU_CONSOLE(cd->gfx_con), c);\n+    cursor_unref(c);\n+\n+    qemu_dbus_display1_listener_complete_cursor_define(\n+        listener, invocation);\n+    return DBUS_METHOD_INVOCATION_HANDLED;\n+}\n+\n+typedef struct {\n+    GMainLoop *loop;\n+    GThread *thread;\n+    GDBusConnection *listener_conn;\n+} ListenerSetupData;\n+\n+static void\n+on_register_listener_finished(GObject *source_object,\n+                              GAsyncResult *res,\n+                              gpointer user_data)\n+{\n+    ListenerSetupData *data = user_data;\n+    g_autoptr(GError) err = NULL;\n+\n+    qemu_dbus_display1_console_call_register_listener_finish(\n+        QEMU_DBUS_DISPLAY1_CONSOLE(source_object),\n+        NULL,\n+        res, &err);\n+\n+    if (err) {\n+        error_report(\"RegisterListener failed: %s\", err->message);\n+        g_main_loop_quit(data->loop);\n+        return;\n+    }\n+\n+    data->listener_conn = g_thread_join(data->thread);\n+    g_main_loop_quit(data->loop);\n+}\n+\n+static GDBusConnection *\n+console_register_display_listener(QemuDBusDisplay1Console *console)\n+{\n+    g_autoptr(GError) err = NULL;\n+    g_autoptr(GMainLoop) loop = NULL;\n+    g_autoptr(GUnixFDList) fd_list = NULL;\n+    ListenerSetupData data = { 0 };\n+    int pair[2];\n+    int idx;\n+\n+    if (qemu_socketpair(AF_UNIX, SOCK_STREAM, 0, pair) < 0) {\n+        error_report(\"socketpair failed: %s\", strerror(errno));\n+        return NULL;\n+    }\n+\n+    fd_list = g_unix_fd_list_new();\n+    idx = g_unix_fd_list_append(fd_list, pair[1], &err);\n+    close(pair[1]);\n+    if (idx < 0) {\n+        close(pair[0]);\n+        error_report(\"Failed to append fd: %s\", err->message);\n+        return NULL;\n+    }\n+\n+    loop = g_main_loop_new(NULL, FALSE);\n+    data.loop = loop;\n+    data.thread = p2p_dbus_thread_new(pair[0]);\n+\n+    qemu_dbus_display1_console_call_register_listener(\n+        console,\n+        g_variant_new_handle(idx),\n+        G_DBUS_CALL_FLAGS_NONE,\n+        -1,\n+        fd_list,\n+        NULL,\n+        on_register_listener_finished,\n+        &data);\n+\n+    g_main_loop_run(loop);\n+\n+    return data.listener_conn;\n+}\n+\n+static void\n+setup_display_listener(ConsoleData *cd)\n+{\n+    g_autoptr(GDBusObjectSkeleton) obj = NULL;\n+    GDBusObjectManagerServer *server;\n+    QemuDBusDisplay1Listener *iface;\n+    QemuDBusDisplay1ListenerUnixMap *iface_map;\n+\n+    server = g_dbus_object_manager_server_new(DBUS_DISPLAY1_ROOT);\n+    obj = g_dbus_object_skeleton_new(DBUS_DISPLAY1_ROOT \"/Listener\");\n+\n+    /* Main listener interface */\n+    iface = qemu_dbus_display1_listener_skeleton_new();\n+    g_object_connect(iface,\n+                     \"signal::handle-scanout\", on_scanout, cd,\n+                     \"signal::handle-update\", on_update, cd,\n+                     \"signal::handle-cursor-define\", on_cursor_define, cd,\n+                     NULL);\n+    g_dbus_object_skeleton_add_interface(obj,\n+                                         G_DBUS_INTERFACE_SKELETON(iface));\n+\n+    /* Unix shared memory map interface */\n+    iface_map = qemu_dbus_display1_listener_unix_map_skeleton_new();\n+    g_object_connect(iface_map,\n+                     \"signal::handle-scanout-map\", on_scanout_map, cd,\n+                     \"signal::handle-update-map\", on_update_map, cd,\n+                     NULL);\n+    g_dbus_object_skeleton_add_interface(obj,\n+                                         G_DBUS_INTERFACE_SKELETON(iface_map));\n+\n+    {\n+        const gchar *ifaces[] = {\n+            \"org.qemu.Display1.Listener.Unix.Map\", NULL\n+        };\n+        g_object_set(iface, \"interfaces\", ifaces, NULL);\n+    }\n+\n+    g_dbus_object_manager_server_export(server, obj);\n+    g_dbus_object_manager_server_set_connection(server,\n+                                                 cd->listener_conn);\n+\n+    g_dbus_connection_start_message_processing(cd->listener_conn);\n+}\n+\n+static const GraphicHwOps vnc_hw_ops = {\n+    .ui_info = display_ui_info,\n+};\n+\n+bool console_setup(GDBusConnection *bus, const char *bus_name,\n+                   const char *console_path)\n+{\n+    g_autoptr(GError) err = NULL;\n+    ConsoleData *cd;\n+    QemuConsole *con;\n+\n+    cd = g_new0(ConsoleData, 1);\n+\n+    cd->console_proxy = qemu_dbus_display1_console_proxy_new_sync(\n+        bus, G_DBUS_PROXY_FLAGS_NONE, bus_name,\n+        console_path, NULL, &err);\n+    if (!cd->console_proxy) {\n+        error_report(\"Failed to create console proxy for %s: %s\",\n+                     console_path, err->message);\n+        g_free(cd);\n+        return false;\n+    }\n+\n+    cd->keyboard_proxy = QEMU_DBUS_DISPLAY1_KEYBOARD(\n+        qemu_dbus_display1_keyboard_proxy_new_sync(\n+            bus, G_DBUS_PROXY_FLAGS_NONE, bus_name,\n+            console_path, NULL, &err));\n+    if (!cd->keyboard_proxy) {\n+        error_report(\"Failed to create keyboard proxy for %s: %s\",\n+                     console_path, err->message);\n+        g_object_unref(cd->console_proxy);\n+        g_free(cd);\n+        return false;\n+    }\n+\n+    g_clear_error(&err);\n+    cd->mouse_proxy = QEMU_DBUS_DISPLAY1_MOUSE(\n+        qemu_dbus_display1_mouse_proxy_new_sync(\n+            bus, G_DBUS_PROXY_FLAGS_NONE, bus_name,\n+            console_path, NULL, &err));\n+    if (!cd->mouse_proxy) {\n+        error_report(\"Failed to create mouse proxy for %s: %s\",\n+                     console_path, err->message);\n+        g_object_unref(cd->keyboard_proxy);\n+        g_object_unref(cd->console_proxy);\n+        g_free(cd);\n+        return false;\n+    }\n+\n+    con = qemu_graphic_console_create(NULL, 0, &vnc_hw_ops, cd);\n+    cd->gfx_con = QEMU_GRAPHIC_CONSOLE(con);\n+\n+    cd->listener_conn = console_register_display_listener(\n+        cd->console_proxy);\n+    if (!cd->listener_conn) {\n+        error_report(\"Failed to setup D-Bus listener for %s\",\n+                     console_path);\n+        g_object_unref(cd->mouse_proxy);\n+        g_object_unref(cd->keyboard_proxy);\n+        g_object_unref(cd->console_proxy);\n+        g_free(cd);\n+        return false;\n+    }\n+\n+    setup_display_listener(cd);\n+    input_setup(cd->keyboard_proxy, cd->mouse_proxy);\n+\n+    return true;\n+}\n+\n+QemuDBusDisplay1Keyboard *console_get_keyboard(QemuConsole *con)\n+{\n+    ConsoleData *cd;\n+\n+    if (!QEMU_IS_GRAPHIC_CONSOLE(con)) {\n+        return NULL;\n+    }\n+    cd = con->hw;\n+    return cd ? cd->keyboard_proxy : NULL;\n+}\n+\n+QemuDBusDisplay1Mouse *console_get_mouse(QemuConsole *con)\n+{\n+    ConsoleData *cd;\n+\n+    if (!QEMU_IS_GRAPHIC_CONSOLE(con)) {\n+        return NULL;\n+    }\n+    cd = con->hw;\n+    return cd ? cd->mouse_proxy : NULL;\n+}\ndiff --git a/tools/qemu-vnc/input.c b/tools/qemu-vnc/input.c\nnew file mode 100644\nindex 00000000000..2313b0a7c77\n--- /dev/null\n+++ b/tools/qemu-vnc/input.c\n@@ -0,0 +1,239 @@\n+/*\n+ * Keyboard and mouse input dispatch via D-Bus.\n+ *\n+ * Copyright (C) 2026 Red Hat, Inc.\n+ *\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+\n+#include \"ui/dbus-display1.h\"\n+#include \"ui/input.h\"\n+#include \"trace.h\"\n+#include \"qemu-vnc.h\"\n+\n+struct QEMUPutLEDEntry {\n+    QEMUPutLEDEvent *put_led;\n+    void *opaque;\n+    QTAILQ_ENTRY(QEMUPutLEDEntry) next;\n+};\n+\n+static NotifierList mouse_mode_notifiers =\n+    NOTIFIER_LIST_INITIALIZER(mouse_mode_notifiers);\n+static QTAILQ_HEAD(, QEMUPutLEDEntry) led_handlers =\n+    QTAILQ_HEAD_INITIALIZER(led_handlers);\n+\n+/* Track the target console for pending mouse events (used by sync) */\n+static QemuConsole *mouse_target;\n+\n+QEMUPutLEDEntry *qemu_add_led_event_handler(QEMUPutLEDEvent *func,\n+                                            void *opaque)\n+{\n+    QEMUPutLEDEntry *s;\n+\n+    s = g_new0(QEMUPutLEDEntry, 1);\n+    s->put_led = func;\n+    s->opaque = opaque;\n+    QTAILQ_INSERT_TAIL(&led_handlers, s, next);\n+    return s;\n+}\n+\n+void qemu_remove_led_event_handler(QEMUPutLEDEntry *entry)\n+{\n+    if (!entry) {\n+        return;\n+    }\n+    QTAILQ_REMOVE(&led_handlers, entry, next);\n+    g_free(entry);\n+}\n+\n+static void\n+on_keyboard_modifiers_changed(GObject *gobject, GParamSpec *pspec,\n+                              gpointer user_data)\n+{\n+    guint modifiers;\n+    QEMUPutLEDEntry *cursor;\n+\n+    modifiers = qemu_dbus_display1_keyboard_get_modifiers(\n+        QEMU_DBUS_DISPLAY1_KEYBOARD(gobject));\n+\n+    /*\n+     * The D-Bus Keyboard.Modifiers property uses the same\n+     * bit layout as QEMU's LED constants.\n+     */\n+    QTAILQ_FOREACH(cursor, &led_handlers, next) {\n+        cursor->put_led(cursor->opaque, modifiers);\n+    }\n+}\n+\n+void qemu_add_mouse_mode_change_notifier(Notifier *notify)\n+{\n+    notifier_list_add(&mouse_mode_notifiers, notify);\n+}\n+\n+void qemu_remove_mouse_mode_change_notifier(Notifier *notify)\n+{\n+    notifier_remove(notify);\n+}\n+\n+void qemu_input_event_send_key_delay(uint32_t delay_ms)\n+{\n+}\n+\n+void qemu_input_event_send_key_qcode(QemuConsole *src, QKeyCode q, bool down)\n+{\n+    QemuDBusDisplay1Keyboard *kbd;\n+    guint qnum;\n+\n+    trace_qemu_vnc_key_event(q, down);\n+\n+    if (!src) {\n+        return;\n+    }\n+    kbd = console_get_keyboard(src);\n+    if (!kbd) {\n+        return;\n+    }\n+\n+    if (q >= qemu_input_map_qcode_to_qnum_len) {\n+        return;\n+    }\n+    qnum = qemu_input_map_qcode_to_qnum[q];\n+\n+    if (down) {\n+        qemu_dbus_display1_keyboard_call_press(\n+            kbd, qnum,\n+            G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);\n+    } else {\n+        qemu_dbus_display1_keyboard_call_release(\n+            kbd, qnum,\n+            G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);\n+    }\n+}\n+\n+static guint abs_x, abs_y;\n+static bool abs_pending;\n+static gint rel_dx, rel_dy;\n+static bool rel_pending;\n+\n+void qemu_input_queue_abs(QemuConsole *src, InputAxis axis,\n+                          int value, int min_in, int max_in)\n+{\n+    if (axis == INPUT_AXIS_X) {\n+        abs_x = value;\n+    } else if (axis == INPUT_AXIS_Y) {\n+        abs_y = value;\n+    }\n+    abs_pending = true;\n+    mouse_target = src;\n+}\n+\n+void qemu_input_queue_rel(QemuConsole *src, InputAxis axis, int value)\n+{\n+    if (axis == INPUT_AXIS_X) {\n+        rel_dx += value;\n+    } else if (axis == INPUT_AXIS_Y) {\n+        rel_dy += value;\n+    }\n+    rel_pending = true;\n+    mouse_target = src;\n+}\n+\n+void qemu_input_event_sync(void)\n+{\n+    QemuDBusDisplay1Mouse *mouse;\n+\n+    if (!mouse_target) {\n+        return;\n+    }\n+\n+    mouse = console_get_mouse(mouse_target);\n+    if (!mouse) {\n+        abs_pending = false;\n+        rel_pending = false;\n+        return;\n+    }\n+\n+    if (abs_pending) {\n+        trace_qemu_vnc_input_abs(abs_x, abs_y);\n+        abs_pending = false;\n+        qemu_dbus_display1_mouse_call_set_abs_position(\n+            mouse, abs_x, abs_y,\n+            G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);\n+    }\n+\n+    if (rel_pending) {\n+        trace_qemu_vnc_input_rel(rel_dx, rel_dy);\n+        rel_pending = false;\n+        qemu_dbus_display1_mouse_call_rel_motion(\n+            mouse, rel_dx, rel_dy,\n+            G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);\n+        rel_dx = 0;\n+        rel_dy = 0;\n+    }\n+}\n+\n+bool qemu_input_is_absolute(QemuConsole *con)\n+{\n+    QemuDBusDisplay1Mouse *mouse;\n+\n+    if (!con) {\n+        return false;\n+    }\n+    mouse = console_get_mouse(con);\n+\n+    if (!mouse) {\n+        return false;\n+    }\n+    return qemu_dbus_display1_mouse_get_is_absolute(mouse);\n+}\n+\n+static void\n+on_mouse_is_absolute_changed(GObject *gobject, GParamSpec *pspec,\n+                              gpointer user_data)\n+{\n+    notifier_list_notify(&mouse_mode_notifiers, NULL);\n+}\n+\n+void qemu_input_update_buttons(QemuConsole *src, uint32_t *button_map,\n+                               uint32_t button_old, uint32_t button_new)\n+{\n+    QemuDBusDisplay1Mouse *mouse;\n+    uint32_t changed;\n+    int i;\n+\n+    if (!src) {\n+        return;\n+    }\n+    mouse = console_get_mouse(src);\n+    if (!mouse) {\n+        return;\n+    }\n+\n+    changed = button_old ^ button_new;\n+    for (i = 0; i < 32; i++) {\n+        if (!(changed & (1u << i))) {\n+            continue;\n+        }\n+        trace_qemu_vnc_input_btn(i, !!(button_new & (1u << i)));\n+        if (button_new & (1u << i)) {\n+            qemu_dbus_display1_mouse_call_press(\n+                mouse, i,\n+                G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);\n+        } else {\n+            qemu_dbus_display1_mouse_call_release(\n+                mouse, i,\n+                G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL);\n+        }\n+    }\n+}\n+\n+void input_setup(QemuDBusDisplay1Keyboard *kbd,\n+                 QemuDBusDisplay1Mouse *mouse)\n+{\n+    g_signal_connect(kbd, \"notify::modifiers\",\n+                     G_CALLBACK(on_keyboard_modifiers_changed), NULL);\n+    g_signal_connect(mouse, \"notify::is-absolute\",\n+                     G_CALLBACK(on_mouse_is_absolute_changed), NULL);\n+}\ndiff --git a/tools/qemu-vnc/qemu-vnc.c b/tools/qemu-vnc/qemu-vnc.c\nnew file mode 100644\nindex 00000000000..d063aff7a62\n--- /dev/null\n+++ b/tools/qemu-vnc/qemu-vnc.c\n@@ -0,0 +1,491 @@\n+/*\n+ * Standalone VNC server connecting to QEMU via D-Bus display interface.\n+ *\n+ * Copyright (C) 2026 Red Hat, Inc.\n+ *\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+\n+#include \"qemu/cutils.h\"\n+#include \"qemu/datadir.h\"\n+#include \"qemu/error-report.h\"\n+#include \"qemu/config-file.h\"\n+#include \"qemu/option.h\"\n+#include \"qemu/log.h\"\n+#include \"qemu/main-loop.h\"\n+#include \"qemu-version.h\"\n+#include \"ui/vnc.h\"\n+#include \"crypto/secret.h\"\n+#include \"crypto/tlscredsx509.h\"\n+#include \"qom/object_interfaces.h\"\n+#include \"trace.h\"\n+#include \"qemu-vnc.h\"\n+\n+const char *qemu_name;\n+const char *keyboard_layout;\n+\n+static bool terminate;\n+static VncDisplay *vd;\n+\n+static GType\n+dbus_display_get_proxy_type(GDBusObjectManagerClient *manager,\n+                            const gchar *object_path,\n+                            const gchar *interface_name,\n+                            gpointer user_data)\n+{\n+    static const struct {\n+        const char *iface;\n+        GType (*get_type)(void);\n+    } types[] = {\n+        { \"org.qemu.Display1.Clipboard\",\n+          qemu_dbus_display1_clipboard_proxy_get_type },\n+        { \"org.qemu.Display1.Audio\",\n+          qemu_dbus_display1_audio_proxy_get_type },\n+        { \"org.qemu.Display1.Chardev\",\n+          qemu_dbus_display1_chardev_proxy_get_type },\n+    };\n+\n+    if (!interface_name) {\n+        return G_TYPE_DBUS_OBJECT_PROXY;\n+    }\n+\n+    for (int i = 0; i < G_N_ELEMENTS(types); i++) {\n+        if (g_str_equal(interface_name, types[i].iface)) {\n+            return types[i].get_type();\n+        }\n+    }\n+\n+    return G_TYPE_DBUS_PROXY;\n+}\n+\n+static void\n+on_bus_closed(GDBusConnection *connection,\n+              gboolean remote_peer_vanished,\n+              GError *error,\n+              gpointer user_data)\n+{\n+    terminate = true;\n+    qemu_notify_event();\n+}\n+\n+static void\n+on_owner_vanished(GDBusConnection *connection,\n+                  const gchar *name,\n+                  gpointer user_data)\n+{\n+    trace_qemu_vnc_owner_vanished(name);\n+    error_report(\"D-Bus peer %s vanished, terminating\", name);\n+    terminate = true;\n+    qemu_notify_event();\n+}\n+\n+typedef struct {\n+    GDBusConnection *bus;\n+    const char *bus_name;\n+    const char * const *chardev_names;\n+    bool no_vt;\n+} ManagerSetupData;\n+\n+static int\n+compare_console_paths(const void *a, const void *b)\n+{\n+    const char *pa = *(const char **)a;\n+    const char *pb = *(const char **)b;\n+    return strcmp(pa, pb);\n+}\n+\n+static void\n+on_manager_ready(GObject *source_object,\n+                 GAsyncResult *res,\n+                 gpointer user_data)\n+{\n+    ManagerSetupData *data = user_data;\n+    g_autoptr(GError) err = NULL;\n+    g_autoptr(GDBusObjectManager) manager = NULL;\n+    GList *objects, *l;\n+    g_autoptr(GPtrArray) console_paths = NULL;\n+    bool found = false;\n+\n+    manager = G_DBUS_OBJECT_MANAGER(\n+        g_dbus_object_manager_client_new_finish(res, &err));\n+    if (!manager) {\n+        error_report(\"Failed to create object manager: %s\",\n+                     err->message);\n+        terminate = true;\n+        qemu_notify_event();\n+        g_free(data);\n+        return;\n+    }\n+\n+    /*\n+     * Discover all Console objects and sort them so that console\n+     * indices are assigned in a predictable order matching QEMU's.\n+     */\n+    console_paths = g_ptr_array_new_with_free_func(g_free);\n+    objects = g_dbus_object_manager_get_objects(manager);\n+    for (l = objects; l; l = l->next) {\n+        GDBusObject *obj = l->data;\n+        const char *path = g_dbus_object_get_object_path(obj);\n+\n+        if (g_str_has_prefix(path, DBUS_DISPLAY1_ROOT \"/Console_\")) {\n+            g_ptr_array_add(console_paths, g_strdup(path));\n+        }\n+    }\n+    g_list_free_full(objects, g_object_unref);\n+\n+    g_ptr_array_sort(console_paths, compare_console_paths);\n+\n+    for (guint i = 0; i < console_paths->len; i++) {\n+        const char *path = g_ptr_array_index(console_paths, i);\n+\n+        if (!console_setup(data->bus, data->bus_name, path)) {\n+            error_report(\"Failed to setup console %s\", path);\n+            continue;\n+        }\n+        found = true;\n+    }\n+\n+    if (!found) {\n+        error_report(\"No consoles found\");\n+        terminate = true;\n+        qemu_notify_event();\n+        g_free(data);\n+        return;\n+    }\n+\n+    /*\n+     * Create the VNC display now that consoles exist, so that the\n+     * display change listener is registered against a valid console.\n+     */\n+    {\n+        Error *local_err = NULL;\n+\n+        vd = vnc_display_new(\"default\", &local_err);\n+        if (!vd) {\n+            error_report_err(local_err);\n+            terminate = true;\n+            qemu_notify_event();\n+            g_free(data);\n+            return;\n+        }\n+    }\n+\n+    vnc_dbus_setup(data->bus);\n+\n+    clipboard_setup(manager, data->bus);\n+    audio_setup(manager);\n+    if (!data->no_vt) {\n+        chardev_setup(data->chardev_names, manager);\n+    }\n+    g_free(data);\n+}\n+\n+int main(int argc, char *argv[])\n+{\n+    Error *local_err = NULL;\n+    g_autoptr(GError) err = NULL;\n+    g_autoptr(GDBusConnection) bus = NULL;\n+    g_autofree char *dbus_address = NULL;\n+    g_autofree char *bus_name = NULL;\n+    int dbus_p2p_fd = -1;\n+    g_autofree char *vnc_addr = NULL;\n+    g_autofree char *ws_addr = NULL;\n+    g_autofree char *share = NULL;\n+    g_autofree char *tls_creds_dir = NULL;\n+    g_autofree char *tls_authz = NULL;\n+    g_autofree char *sasl_authz = NULL;\n+    g_autofree char *trace_opt = NULL;\n+    g_auto(GStrv) chardev_names = NULL;\n+    g_auto(GStrv) object_strs = NULL;\n+    const char *creds_dir;\n+    bool has_vnc_password = false;\n+    bool show_version = false;\n+    bool no_vt = false;\n+    bool password = false;\n+    bool sasl = false;\n+    bool lossy = false;\n+    bool non_adaptive = false;\n+    g_autoptr(GOptionContext) context = NULL;\n+    GOptionEntry entries[] = {\n+        { \"dbus-address\", 'a', 0, G_OPTION_ARG_STRING, &dbus_address,\n+          \"D-Bus address to connect to (default: session bus)\", \"ADDRESS\" },\n+        { \"dbus-p2p-fd\", 'p', 0, G_OPTION_ARG_INT, &dbus_p2p_fd,\n+          \"D-Bus peer-to-peer socket file descriptor\", \"FD\" },\n+        { \"bus-name\", 'n', 0, G_OPTION_ARG_STRING, &bus_name,\n+          \"D-Bus bus name (default: org.qemu)\", \"NAME\" },\n+       { \"vnc-addr\", 'l', 0, G_OPTION_ARG_STRING, &vnc_addr,\n+          \"VNC display address (default localhost:0)\", \"ADDR\" },\n+        { \"websocket\", 'w', 0, G_OPTION_ARG_STRING, &ws_addr,\n+          \"WebSocket address (e.g. port number or addr:port)\", \"ADDR\" },\n+        { \"share\", 's', 0, G_OPTION_ARG_STRING, &share,\n+          \"Display sharing policy \"\n+          \"(allow-exclusive|force-shared|ignore)\", \"POLICY\" },\n+        { \"tls-creds\", 't', 0, G_OPTION_ARG_STRING, &tls_creds_dir,\n+          \"TLS x509 credentials directory\", \"DIR\" },\n+        { \"tls-authz\", 0, 0, G_OPTION_ARG_STRING, &tls_authz,\n+          \"ID of a QAuthZ object for TLS client certificate \"\n+          \"authorization\", \"ID\" },\n+        { \"object\", 'O', 0, G_OPTION_ARG_STRING_ARRAY, &object_strs,\n+          \"QEMU user-creatable object \"\n+          \"(e.g. authz-list-file,id=auth0,filename=acl.json)\", \"OBJDEF\" },\n+        { \"vt-chardev\", 'C', 0, G_OPTION_ARG_STRING_ARRAY, &chardev_names,\n+          \"Chardev type names to expose as text console (repeatable, \"\n+          \"default: serial & hmp)\", \"NAME\" },\n+        { \"no-vt\", 'N', 0, G_OPTION_ARG_NONE, &no_vt,\n+          \"Do not expose any chardevs as text consoles\", NULL },\n+        { \"keyboard-layout\", 'k', 0, G_OPTION_ARG_STRING, &keyboard_layout,\n+          \"Keyboard layout\", \"LAYOUT\" },\n+        { \"trace\", 'T', 0, G_OPTION_ARG_STRING, &trace_opt,\n+          \"Trace options (same as QEMU -trace)\", \"PATTERN\" },\n+        { \"version\", 'V', 0, G_OPTION_ARG_NONE, &show_version,\n+          \"Print version information and exit\", NULL },\n+        { \"password\", 0, 0, G_OPTION_ARG_NONE, &password,\n+          \"Require password authentication (use D-Bus SetPassword to set)\",\n+          NULL },\n+        { \"lossy\", 0, 0, G_OPTION_ARG_NONE, &lossy,\n+          \"Enable lossy compression\", NULL },\n+        { \"non-adaptive\", 0, 0, G_OPTION_ARG_NONE, &non_adaptive,\n+          \"Disable adaptive encodings\", NULL },\n+        { \"sasl\", 0, 0, G_OPTION_ARG_NONE, &sasl,\n+          \"Enable SASL authentication\", NULL },\n+        { \"sasl-authz\", 0, 0, G_OPTION_ARG_STRING, &sasl_authz,\n+          \"ID of a QAuthZ object for SASL username \"\n+          \"authorization\", \"ID\" },\n+        { NULL }\n+    };\n+\n+    qemu_init_exec_dir(argv[0]);\n+    qemu_add_data_dir(get_relocated_path(CONFIG_QEMU_DATADIR));\n+\n+    module_call_init(MODULE_INIT_TRACE);\n+    module_call_init(MODULE_INIT_QOM);\n+    module_call_init(MODULE_INIT_OPTS);\n+    qemu_add_opts(&qemu_trace_opts);\n+\n+    context = g_option_context_new(\"- standalone VNC server for QEMU\");\n+    g_option_context_add_main_entries(context, entries, NULL);\n+    if (!g_option_context_parse(context, &argc, &argv, &err)) {\n+        error_report(\"Option parsing failed: %s\", err->message);\n+        return 1;\n+    }\n+\n+    if (show_version) {\n+        printf(\"qemu-vnc \" QEMU_FULL_VERSION \"\\n\");\n+        return 0;\n+    }\n+\n+    if (trace_opt) {\n+        trace_opt_parse(trace_opt);\n+        qemu_set_log(LOG_TRACE, &local_err);\n+        if (local_err) {\n+            error_report_err(local_err);\n+            return 1;\n+        }\n+    }\n+    trace_init_file();\n+\n+    if (qemu_init_main_loop(&local_err)) {\n+        error_report_err(local_err);\n+        return 1;\n+    }\n+\n+    if (!vnc_addr) {\n+        vnc_addr = g_strdup(\"localhost:0\");\n+    }\n+\n+    if (object_strs) {\n+        for (int i = 0; object_strs[i]; i++) {\n+            user_creatable_process_cmdline(object_strs[i]);\n+        }\n+    }\n+\n+    if (tls_authz && !tls_creds_dir) {\n+        error_report(\"--tls-authz requires --tls-creds\");\n+        return 1;\n+    }\n+\n+    if (sasl_authz && !sasl) {\n+        error_report(\"--sasl-authz requires --sasl\");\n+        return 1;\n+    }\n+\n+    if (dbus_p2p_fd >= 0 && dbus_address) {\n+        error_report(\"--dbus-p2p-fd and --dbus-address are\"\n+                     \" mutually exclusive\");\n+        return 1;\n+    }\n+\n+    if (dbus_p2p_fd >= 0) {\n+        g_autoptr(GSocket) socket = NULL;\n+        g_autoptr(GSocketConnection) socketc = NULL;\n+\n+        if (bus_name) {\n+            error_report(\"--bus-name is not supported with --dbus-p2p-fd\");\n+            return 1;\n+        }\n+\n+        socket = g_socket_new_from_fd(dbus_p2p_fd, &err);\n+        if (!socket) {\n+            error_report(\"Failed to create socket from fd %d: %s\",\n+                         dbus_p2p_fd, err->message);\n+            return 1;\n+        }\n+\n+        socketc = g_socket_connection_factory_create_connection(socket);\n+        if (!socketc) {\n+            error_report(\"Failed to create socket connection\");\n+            return 1;\n+        }\n+\n+        bus = g_dbus_connection_new_sync(\n+            G_IO_STREAM(socketc), NULL,\n+            G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT,\n+            NULL, NULL, &err);\n+    } else if (dbus_address) {\n+        GDBusConnectionFlags flags =\n+            G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT;\n+        if (bus_name) {\n+            flags |= G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION;\n+        }\n+        bus = g_dbus_connection_new_for_address_sync(\n+            dbus_address, flags, NULL, NULL, &err);\n+    } else {\n+        bus = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &err);\n+        if (!bus_name) {\n+            bus_name = g_strdup(\"org.qemu\");\n+        }\n+    }\n+    if (!bus) {\n+        error_report(\"Failed to connect to D-Bus: %s\", err->message);\n+        return 1;\n+    }\n+\n+    {\n+        g_autoptr(QemuDBusDisplay1VMProxy) vm_proxy = QEMU_DBUS_DISPLAY1_VM_PROXY(\n+            qemu_dbus_display1_vm_proxy_new_sync(\n+                bus, G_DBUS_PROXY_FLAGS_NONE, bus_name,\n+                DBUS_DISPLAY1_ROOT \"/VM\", NULL, NULL));\n+        if (vm_proxy) {\n+            qemu_name = g_strdup(qemu_dbus_display1_vm_get_name(\n+                QEMU_DBUS_DISPLAY1_VM(vm_proxy)));\n+        }\n+    }\n+\n+    /*\n+     * Set up TLS credentials if requested.  The object must exist\n+     * before vnc_display_open() which looks it up by ID.\n+     */\n+    if (tls_creds_dir) {\n+        if (!object_new_with_props(TYPE_QCRYPTO_TLS_CREDS_X509,\n+                                   object_get_objects_root(),\n+                                   \"tlscreds0\",\n+                                   &local_err,\n+                                   \"endpoint\", \"server\",\n+                                   \"dir\", tls_creds_dir,\n+                                   \"verify-peer\", tls_authz ? \"yes\" : \"no\",\n+                                   NULL)) {\n+            error_report_err(local_err);\n+            return 1;\n+        }\n+    }\n+\n+    /*\n+     * Check for systemd credentials: if a vnc-password credential\n+     * file exists, create a QCryptoSecret and enable VNC password auth.\n+     */\n+    creds_dir = g_getenv(\"CREDENTIALS_DIRECTORY\");\n+    if (creds_dir) {\n+        g_autofree char *password_path =\n+            g_build_filename(creds_dir, \"vnc-password\", NULL);\n+        if (g_file_test(password_path, G_FILE_TEST_EXISTS)) {\n+            if (!object_new_with_props(TYPE_QCRYPTO_SECRET,\n+                                       object_get_objects_root(),\n+                                       \"vncsecret0\",\n+                                       &local_err,\n+                                       \"file\", password_path,\n+                                       NULL)) {\n+                error_report_err(local_err);\n+                return 1;\n+            }\n+            has_vnc_password = true;\n+        }\n+    }\n+\n+    {\n+        g_autoptr(GString) vnc_opts = g_string_new(vnc_addr);\n+        QemuOptsList *olist = qemu_find_opts(\"vnc\");\n+        QemuOpts *opts;\n+\n+        if (tls_creds_dir) {\n+            g_string_append(vnc_opts, \",tls-creds=tlscreds0\");\n+        }\n+        if (tls_authz) {\n+            g_string_append_printf(vnc_opts, \",tls-authz=%s\", tls_authz);\n+        }\n+        if (sasl) {\n+            g_string_append(vnc_opts, \",sasl=on\");\n+        }\n+        if (sasl_authz) {\n+            g_string_append_printf(vnc_opts, \",sasl-authz=%s\", sasl_authz);\n+        }\n+        if (has_vnc_password) {\n+            g_string_append(vnc_opts, \",password-secret=vncsecret0\");\n+        }\n+        if (ws_addr) {\n+            g_string_append_printf(vnc_opts, \",websocket=%s\", ws_addr);\n+        }\n+        if (share) {\n+            g_string_append_printf(vnc_opts, \",share=%s\", share);\n+        }\n+        if (password && !has_vnc_password) {\n+            g_string_append(vnc_opts, \",password=on\");\n+        }\n+        if (lossy) {\n+            g_string_append(vnc_opts, \",lossy=on\");\n+        }\n+        if (non_adaptive) {\n+            g_string_append(vnc_opts, \",non-adaptive=on\");\n+        }\n+\n+        opts = qemu_opts_parse_noisily(olist, vnc_opts->str, true);\n+        if (!opts) {\n+            return 1;\n+        }\n+        qemu_opts_set_id(opts, g_strdup(\"default\"));\n+    }\n+\n+    {\n+        ManagerSetupData *mgr_data = g_new0(ManagerSetupData, 1);\n+        mgr_data->bus = bus;\n+        mgr_data->bus_name = bus_name;\n+        mgr_data->chardev_names = (const char * const *)chardev_names;\n+        mgr_data->no_vt = no_vt;\n+\n+        g_dbus_object_manager_client_new(\n+            bus, G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_NONE,\n+            bus_name, DBUS_DISPLAY1_ROOT,\n+            dbus_display_get_proxy_type,\n+            NULL, NULL, NULL,\n+            on_manager_ready, mgr_data);\n+    }\n+\n+    g_signal_connect(bus, \"closed\", G_CALLBACK(on_bus_closed), NULL);\n+\n+    if (bus_name) {\n+        g_bus_watch_name_on_connection(bus, bus_name,\n+                                       G_BUS_NAME_WATCHER_FLAGS_NONE,\n+                                       NULL, on_owner_vanished,\n+                                       NULL, NULL);\n+    }\n+\n+    while (!terminate) {\n+        main_loop_wait(false);\n+    }\n+\n+    vnc_dbus_cleanup();\n+    vnc_cleanup();\n+\n+    return 0;\n+}\ndiff --git a/tools/qemu-vnc/stubs.c b/tools/qemu-vnc/stubs.c\nnew file mode 100644\nindex 00000000000..a865ce85f04\n--- /dev/null\n+++ b/tools/qemu-vnc/stubs.c\n@@ -0,0 +1,62 @@\n+/*\n+ * Stubs for qemu-vnc standalone binary.\n+ *\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+\n+#include \"system/runstate.h\"\n+#include \"hw/core/qdev.h\"\n+#include \"monitor/monitor.h\"\n+#include \"migration/vmstate.h\"\n+\n+bool runstate_is_running(void)\n+{\n+    return true;\n+}\n+\n+bool phase_check(MachineInitPhase phase)\n+{\n+    return true;\n+}\n+\n+DeviceState *qdev_find_recursive(BusState *bus, const char *id)\n+{\n+    return NULL;\n+}\n+\n+/*\n+ * Provide the monitor stubs locally so that the linker does not\n+ * pull stubs/monitor-core.c.o from libqemuutil.a (which would\n+ * bring a conflicting qapi_event_emit definition).\n+ */\n+Monitor *monitor_cur(void)\n+{\n+    return NULL;\n+}\n+\n+bool monitor_cur_is_qmp(void)\n+{\n+    return false;\n+}\n+\n+Monitor *monitor_set_cur(Coroutine *co, Monitor *mon)\n+{\n+    return NULL;\n+}\n+\n+int monitor_vprintf(Monitor *mon, const char *fmt, va_list ap)\n+{\n+    return -1;\n+}\n+\n+/*\n+ * Link-time stubs for VMState symbols referenced by VNC code.\n+ * The standalone binary never performs migration, so these are\n+ * never actually used at runtime.\n+ */\n+const VMStateInfo vmstate_info_bool = {};\n+const VMStateInfo vmstate_info_int32 = {};\n+const VMStateInfo vmstate_info_uint32 = {};\n+const VMStateInfo vmstate_info_buffer = {};\ndiff --git a/tools/qemu-vnc/utils.c b/tools/qemu-vnc/utils.c\nnew file mode 100644\nindex 00000000000..d261aa9eaf0\n--- /dev/null\n+++ b/tools/qemu-vnc/utils.c\n@@ -0,0 +1,59 @@\n+/*\n+ * Standalone VNC server connecting to QEMU via D-Bus display interface.\n+ *\n+ * Copyright (C) 2026 Red Hat, Inc.\n+ *\n+ * SPDX-License-Identifier: GPL-2.0-or-later\n+ */\n+\n+#include \"qemu/osdep.h\"\n+\n+#include \"qemu/error-report.h\"\n+#include \"qemu-vnc.h\"\n+\n+static GDBusConnection *\n+dbus_p2p_from_fd(int fd)\n+{\n+    g_autoptr(GError) err = NULL;\n+    g_autoptr(GSocket) socket = NULL;\n+    g_autoptr(GSocketConnection) socketc = NULL;\n+    GDBusConnection *conn;\n+\n+    socket = g_socket_new_from_fd(fd, &err);\n+    if (!socket) {\n+        error_report(\"Failed to create socket: %s\", err->message);\n+        return NULL;\n+    }\n+\n+    socketc = g_socket_connection_factory_create_connection(socket);\n+    if (!socketc) {\n+        error_report(\"Failed to create socket connection\");\n+        return NULL;\n+    }\n+\n+    conn = g_dbus_connection_new_sync(\n+        G_IO_STREAM(socketc), NULL,\n+        G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT |\n+        G_DBUS_CONNECTION_FLAGS_DELAY_MESSAGE_PROCESSING,\n+        NULL, NULL, &err);\n+    if (!conn) {\n+        error_report(\"Failed to create D-Bus connection: %s\", err->message);\n+        return NULL;\n+    }\n+\n+    return conn;\n+}\n+\n+static gpointer\n+p2p_server_setup_thread(gpointer data)\n+{\n+    return dbus_p2p_from_fd(GPOINTER_TO_INT(data));\n+}\n+\n+GThread *\n+p2p_dbus_thread_new(int fd)\n+{\n+    return g_thread_new(\"p2p-server-setup\",\n+                         p2p_server_setup_thread,\n+                         GINT_TO_POINTER(fd));\n+}\ndiff --git a/meson_options.txt b/meson_options.txt\nindex 31d5916cfce..ef938e74793 100644\n--- a/meson_options.txt\n+++ b/meson_options.txt\n@@ -119,6 +119,8 @@ option('vfio_user_server', type: 'feature', value: 'disabled',\n        description: 'vfio-user server support')\n option('dbus_display', type: 'feature', value: 'auto',\n        description: '-display dbus support')\n+option('qemu_vnc', type: 'feature', value: 'auto',\n+       description: 'standalone VNC server over D-Bus')\n option('tpm', type : 'feature', value : 'auto',\n        description: 'TPM support')\n option('valgrind', type : 'feature', value: 'auto',\ndiff --git a/scripts/meson-buildoptions.sh b/scripts/meson-buildoptions.sh\nindex ca5b113119a..5f7a351ca4a 100644\n--- a/scripts/meson-buildoptions.sh\n+++ b/scripts/meson-buildoptions.sh\n@@ -174,6 +174,7 @@ meson_options_help() {\n   printf \"%s\\n\" '  qatzip          QATzip compression support'\n   printf \"%s\\n\" '  qcow1           qcow1 image format support'\n   printf \"%s\\n\" '  qed             qed image format support'\n+  printf \"%s\\n\" '  qemu-vnc        standalone VNC server over D-Bus'\n   printf \"%s\\n\" '  qga-vss         build QGA VSS support (broken with MinGW)'\n   printf \"%s\\n\" '  qpl             Query Processing Library support'\n   printf \"%s\\n\" '  rbd             Ceph block device driver'\n@@ -458,6 +459,8 @@ _meson_option_parse() {\n     --qemu-ga-manufacturer=*) quote_sh \"-Dqemu_ga_manufacturer=$2\" ;;\n     --qemu-ga-version=*) quote_sh \"-Dqemu_ga_version=$2\" ;;\n     --with-suffix=*) quote_sh \"-Dqemu_suffix=$2\" ;;\n+    --enable-qemu-vnc) printf \"%s\" -Dqemu_vnc=enabled ;;\n+    --disable-qemu-vnc) printf \"%s\" -Dqemu_vnc=disabled ;;\n     --enable-qga-vss) printf \"%s\" -Dqga_vss=enabled ;;\n     --disable-qga-vss) printf \"%s\" -Dqga_vss=disabled ;;\n     --enable-qom-cast-debug) printf \"%s\" -Dqom_cast_debug=true ;;\ndiff --git a/tests/dbus-daemon.sh b/tests/dbus-daemon.sh\nindex c4a50c73774..85f9597db43 100755\n--- a/tests/dbus-daemon.sh\n+++ b/tests/dbus-daemon.sh\n@@ -62,9 +62,17 @@ write_config()\n      <deny send_destination=\"org.freedesktop.DBus\"\n            send_interface=\"org.freedesktop.systemd1.Activator\"/>\n \n-     <allow own=\"org.qemu.VMState1\"/>\n-     <allow send_destination=\"org.qemu.VMState1\"/>\n-     <allow receive_sender=\"org.qemu.VMState1\"/>\n+    <allow own=\"org.qemu\"/>\n+    <allow send_destination=\"org.qemu\"/>\n+    <allow receive_sender=\"org.qemu\"/>\n+\n+    <allow own=\"org.qemu.VMState1\"/>\n+    <allow send_destination=\"org.qemu.VMState1\"/>\n+    <allow receive_sender=\"org.qemu.VMState1\"/>\n+\n+    <allow own=\"org.qemu.vnc\"/>\n+    <allow send_destination=\"org.qemu.vnc\"/>\n+    <allow receive_sender=\"org.qemu.vnc\"/>\n \n   </policy>\n \ndiff --git a/tests/qtest/meson.build b/tests/qtest/meson.build\nindex 5f8cff172c8..e9d23003f8c 100644\n--- a/tests/qtest/meson.build\n+++ b/tests/qtest/meson.build\n@@ -411,6 +411,15 @@ if vnc.found()\n   if gvnc.found()\n     qtests += {'vnc-display-test': [gvnc, keymap_targets]}\n     qtests_generic += [ 'vnc-display-test' ]\n+    if have_qemu_vnc and dbus_display and config_all_devices.has_key('CONFIG_VGA')\n+      dbus_vnc_test_deps = [dbus_display1, qemu_vnc1, gio, gvnc, keymap_targets]\n+      if gnutls.found() and tasn1.found()\n+        dbus_vnc_test_deps += [files('../unit/crypto-tls-x509-helpers.c'),\n+                               gnutls, tasn1]\n+      endif\n+      qtests += {'dbus-vnc-test': dbus_vnc_test_deps}\n+      qtests_x86_64 += ['dbus-vnc-test']\n+    endif\n   endif\n endif\n \n@@ -442,6 +451,10 @@ foreach dir : target_dirs\n     qtest_env.set('QTEST_QEMU_STORAGE_DAEMON_BINARY', './storage-daemon/qemu-storage-daemon')\n     test_deps += [qsd]\n   endif\n+  if have_qemu_vnc\n+    qtest_env.set('QTEST_QEMU_VNC_BINARY', './tools/qemu-vnc/qemu-vnc')\n+    test_deps += [qemu_vnc]\n+  endif\n \n   qtest_env.set('PYTHON', python.full_path())\n \ndiff --git a/tools/qemu-vnc/meson.build b/tools/qemu-vnc/meson.build\nnew file mode 100644\nindex 00000000000..08168da0630\n--- /dev/null\n+++ b/tools/qemu-vnc/meson.build\n@@ -0,0 +1,26 @@\n+vnca = vnc_ss.apply({}, strict: false)\n+\n+qemu_vnc1 = custom_target('qemu-vnc1 gdbus-codegen',\n+                           output: ['qemu-vnc1.h', 'qemu-vnc1.c'],\n+                           input: files('qemu-vnc1.xml'),\n+                           command: [gdbus_codegen, '@INPUT@',\n+                                     '--glib-min-required', '2.64',\n+                                     '--output-directory', meson.current_build_dir(),\n+                                     '--interface-prefix', 'org.qemu.',\n+                                     '--c-namespace', 'Qemu',\n+                                     '--generate-c-code', '@BASENAME@'])\n+\n+qemu_vnc = executable('qemu-vnc',\n+  sources: ['qemu-vnc.c', 'display.c', 'input.c',\n+            'audio.c', 'chardev.c', 'clipboard.c', 'console.c',\n+            'dbus.c', 'stubs.c', 'utils.c',\n+            vnca.sources(), dbus_display1, qemu_vnc1],\n+  dependencies: [vnca.dependencies(), io, crypto, qemuutil, gio, ui])\n+\n+# The executable lives in a subdirectory of the build tree, but\n+# get_relocated_path() looks for qemu-bundle relative to the binary.\n+# Create a symlink so that firmware/keymap lookup works during development.\n+run_command('ln', '-sfn',\n+            '../../qemu-bundle',\n+            meson.current_build_dir() / 'qemu-bundle',\n+            check: false)\ndiff --git a/tools/qemu-vnc/qemu-vnc1.xml b/tools/qemu-vnc/qemu-vnc1.xml\nnew file mode 100644\nindex 00000000000..2037e72ae2a\n--- /dev/null\n+++ b/tools/qemu-vnc/qemu-vnc1.xml\n@@ -0,0 +1,174 @@\n+<?xml version=\"1.0\" encoding=\"utf-8\"?>\n+<node>\n+  <!--\n+      org.qemu.Vnc1.Server:\n+\n+      This interface is implemented on ``/org/qemu/Vnc1/Server``.\n+      It provides management and monitoring of the VNC server.\n+  -->\n+  <interface name=\"org.qemu.Vnc1.Server\">\n+    <!--\n+        Name:\n+\n+        The VM name.\n+    -->\n+    <property name=\"Name\" type=\"s\" access=\"read\"/>\n+\n+    <!--\n+        Auth:\n+\n+        Primary authentication method (none, vnc, vencrypt, sasl, etc.).\n+    -->\n+    <property name=\"Auth\" type=\"s\" access=\"read\"/>\n+\n+    <!--\n+        VencryptSubAuth:\n+\n+        VEncrypt sub-authentication method, if applicable.\n+        Empty string otherwise.\n+    -->\n+    <property name=\"VencryptSubAuth\" type=\"s\" access=\"read\"/>\n+\n+    <!--\n+        Clients:\n+\n+        Object paths of connected VNC clients.\n+    -->\n+    <property name=\"Clients\" type=\"ao\" access=\"read\"/>\n+\n+    <!--\n+        Listeners:\n+\n+        List of listening sockets. Each entry is a dictionary with keys:\n+        ``Host`` (s), ``Service`` (s), ``Family`` (s),\n+        ``WebSocket`` (b), ``Auth`` (s), ``VencryptSubAuth`` (s).\n+    -->\n+    <property name=\"Listeners\" type=\"aa{sv}\" access=\"read\"/>\n+\n+    <!--\n+        SetPassword:\n+        @password: The new VNC password.\n+\n+        Change the VNC password.  Existing clients are unaffected.\n+    -->\n+    <method name=\"SetPassword\">\n+      <arg type=\"s\" name=\"password\" direction=\"in\"/>\n+    </method>\n+\n+    <!--\n+        ExpirePassword:\n+        @time: Expiry specification.\n+\n+        Set password expiry.  Values: ``\"now\"``, ``\"never\"``,\n+        ``\"+N\"`` (seconds from now), ``\"N\"`` (absolute epoch seconds).\n+    -->\n+    <method name=\"ExpirePassword\">\n+      <arg type=\"s\" name=\"time\" direction=\"in\"/>\n+    </method>\n+\n+    <!--\n+        ReloadCertificates:\n+\n+        Reload TLS certificates from disk.\n+    -->\n+    <method name=\"ReloadCertificates\"/>\n+\n+    <!--\n+        ClientConnected:\n+        @client: Object path of the new client.\n+\n+        Emitted when a VNC client TCP connection is established\n+        (before authentication).\n+    -->\n+    <signal name=\"ClientConnected\">\n+      <arg type=\"o\" name=\"client\"/>\n+    </signal>\n+\n+    <!--\n+        ClientInitialized:\n+        @client: Object path of the client.\n+\n+        Emitted when a VNC client has completed authentication\n+        and is active.\n+    -->\n+    <signal name=\"ClientInitialized\">\n+      <arg type=\"o\" name=\"client\"/>\n+    </signal>\n+\n+    <!--\n+        ClientDisconnected:\n+        @client: Object path of the client.\n+\n+        Emitted when a VNC client disconnects.\n+    -->\n+    <signal name=\"ClientDisconnected\">\n+      <arg type=\"o\" name=\"client\"/>\n+    </signal>\n+  </interface>\n+\n+  <!--\n+      org.qemu.Vnc1.Client:\n+\n+      This interface is implemented on ``/org/qemu/Vnc1/Client_$id``.\n+      It exposes information about a connected VNC client.\n+  -->\n+  <interface name=\"org.qemu.Vnc1.Client\">\n+    <!--\n+        Host:\n+\n+        Client IP address.\n+    -->\n+    <property name=\"Host\" type=\"s\" access=\"read\"/>\n+\n+    <!--\n+        Service:\n+\n+        Client port or service name. This may depend on the host system’s\n+        service database so symbolic names should not be relied on.\n+    -->\n+    <property name=\"Service\" type=\"s\" access=\"read\"/>\n+\n+    <!--\n+        Family:\n+\n+        Address family (ipv4, ipv6, unix).\n+    -->\n+    <property name=\"Family\" type=\"s\" access=\"read\"/>\n+\n+    <!--\n+        WebSocket:\n+\n+        Whether this is a WebSocket connection.\n+    -->\n+    <property name=\"WebSocket\" type=\"b\" access=\"read\"/>\n+\n+    <!--\n+        X509Dname:\n+\n+        X.509 distinguished name (empty if not applicable).\n+    -->\n+    <property name=\"X509Dname\" type=\"s\" access=\"read\"/>\n+\n+    <!--\n+        SaslUsername:\n+\n+        SASL username (empty if not applicable).\n+    -->\n+    <property name=\"SaslUsername\" type=\"s\" access=\"read\"/>\n+\n+    <!--\n+        ShutdownRequest:\n+\n+        Emitted when the VNC client requests a guest shutdown.\n+    -->\n+    <signal name=\"ShutdownRequest\"/>\n+\n+    <!--\n+        ResetRequest:\n+\n+        Emitted when the VNC client requests a guest reset.\n+    -->\n+    <signal name=\"ResetRequest\"/>\n+  </interface>\n+\n+</node>\ndiff --git a/tools/qemu-vnc/trace-events b/tools/qemu-vnc/trace-events\nnew file mode 100644\nindex 00000000000..f2d66a80986\n--- /dev/null\n+++ b/tools/qemu-vnc/trace-events\n@@ -0,0 +1,20 @@\n+qemu_vnc_audio_out_fini(uint64_t id) \"id=%\" PRIu64\n+qemu_vnc_audio_out_init(uint64_t id, uint32_t freq, uint8_t channels, uint8_t bits) \"id=%\" PRIu64 \" freq=%u ch=%u bits=%u\"\n+qemu_vnc_audio_out_set_enabled(uint64_t id, bool enabled) \"id=%\" PRIu64 \" enabled=%d\"\n+qemu_vnc_audio_out_write(uint64_t id, size_t size) \"id=%\" PRIu64 \" size=%zu\"\n+qemu_vnc_chardev_connected(const char *name) \"name=%s\"\n+qemu_vnc_clipboard_grab(int selection, uint32_t serial) \"selection=%d serial=%u\"\n+qemu_vnc_clipboard_release(int selection) \"selection=%d\"\n+qemu_vnc_clipboard_request(int selection) \"selection=%d\"\n+qemu_vnc_client_not_found(const char *host, const char *service) \"host=%s service=%s\"\n+qemu_vnc_console_io_error(const char *name) \"name=%s\"\n+qemu_vnc_cursor_define(int width, int height, int hot_x, int hot_y) \"w=%d h=%d hot=%d,%d\"\n+qemu_vnc_input_abs(uint32_t x, uint32_t y) \"x=%u y=%u\"\n+qemu_vnc_input_btn(int button, bool press) \"button=%d press=%d\"\n+qemu_vnc_input_rel(int dx, int dy) \"dx=%d dy=%d\"\n+qemu_vnc_key_event(int qcode, bool down) \"qcode=%d down=%d\"\n+qemu_vnc_owner_vanished(const char *name) \"peer=%s\"\n+qemu_vnc_scanout(uint32_t width, uint32_t height, uint32_t stride, uint32_t format) \"w=%u h=%u stride=%u fmt=0x%x\"\n+qemu_vnc_scanout_map(uint32_t width, uint32_t height, uint32_t stride, uint32_t format, uint32_t offset) \"w=%u h=%u stride=%u fmt=0x%x offset=%u\"\n+qemu_vnc_update(int x, int y, int w, int h, uint32_t stride, uint32_t format) \"x=%d y=%d w=%d h=%d stride=%u fmt=0x%x\"\n+qemu_vnc_update_map(uint32_t x, uint32_t y, uint32_t w, uint32_t h) \"x=%u y=%u w=%u h=%u\"\n",
    "prefixes": [
        "v2",
        "67/67"
    ]
}