Hi LTS team, I've prepared a security fix for qemu in Bullseye and I'm looking for a sponsor to review and upload it since I a not a DM/DD.
(Note: I am working on several packages including chromium-embedded-framework and plan to apply for NM soon) This is my first LTS contribution, I took this one as a way to learn the process. ## Summary - Package: qemu - Version: 1:5.2+dfsg-11+deb11u4 (current: 1:5.2+dfsg-11+deb11u3) - CVE: CVE-2025-11234 - Debian Bug: #1117153 - Severity: Medium (use-after-free, potential code execution) ## Vulnerability Description CVE-2025-11234 is a use-after-free vulnerability in QEMU's WebSocket channel implementation (QIOChannelWebsock). When a QIOChannelWebsock object is freed while waiting for a handshake to complete, the associated GSource is not cleaned up properly. This causes callbacks to be invoked on already-freed memory. Attack vector: An attacker can trigger this by sending incomplete WebSocket connections to a QEMU VNC server with WebSocket enabled (-vnc :0,websocket=PORT). ## The Fix The fix backports upstream commit cebdbd038e44af56e74272924dc2bf595a51fd8f (included in QEMU v7.2.22). The changes are: 1. Add new field `guint hs_io_tag` to QIOChannelWebsock structure to track the GSource associated with the handshake (separate from the existing io_tag used for normal I/O). 2. Store the GSource ID when scheduling handshake callbacks in qio_channel_websock_handshake() and qio_channel_websock_handshake_io(). 3. Clear hs_io_tag when handshake callbacks complete. 4. Add cleanup of hs_io_tag in qio_channel_websock_finalize() and qio_channel_websock_close() to prevent use-after-free. Files modified: - include/io/channel-websock.h (add hs_io_tag field) - io/channel-websock.c (track and cleanup GSource) ## Testing Performed Build testing: - Built successfully with pbuilder in a clean Bullseye chroot - All binary packages generated correctly - Patch applies cleanly with quilt Functional testing: - Installed patched QEMU in Docker container (Debian Bullseye) - Started QEMU with VNC WebSocket enabled (-vnc :99,websocket=5700) - Stress tested with 50,000+ incomplete WebSocket handshakes - QEMU remained stable throughout testing (both patched and unpatched) Note on crash-based testing: The use-after-free race condition did not trigger a visible crash in our containerized test environment. This is what I interpret as expected behavior for UAF bugs - the race window is extremely small and modern memory allocators delay reuse of freed memory. Definitive runtime verification would require rebuilding QEMU with AddressSanitizer (--enable-sanitizers). The fix is verified correct by: 1. Code review against upstream commit cebdbd038e44 2. Analysis confirming the GSource leak in pre-fix code 3. The fix has been included in QEMU v7.2.22, v10.0.7, v10.1.3 ## Patch The adapted patch for QEMU 5.2 is attached. The original upstream commit can be found at: https://gitlab.com/qemu-project/qemu/-/commit/cebdbd038e44af56e74272924dc2bf595a51fd8f ## Additional Notes - Bookworm (QEMU 7.2) is already fixed in 1:7.2+dfsg-7+deb12u18 - The security tracker marks Bullseye as <no-dsa> (Minor issue) - The fix is minimal, affecting only WebSocket handling I'm happy to make any changes requested and to provide additional testing or information as needed, or do a salsa Merge Request instead. Thanks for your time, Juan Manuel Méndez Rey <[email protected]>
From: Daniel P. Berrangé <[email protected]> Date: Tue, 30 Sep 2025 12:03:15 +0100 Subject: io: fix use after free in websocket handshake code Origin: upstream, https://gitlab.com/qemu-project/qemu/-/commit/cebdbd038e44af56e74272924dc2bf595a51fd8f Bug-Debian: https://security-tracker.debian.org/tracker/CVE-2025-11234 If the QIOChannelWebsock object is freed while it is waiting to complete a handshake, a GSource is leaked. This can lead to the callback firing later on and triggering a use-after-free in the use of the channel. This patch adds a separate hs_io_tag field to track the handshake GSource, and ensures it is properly cleaned up in finalize() and close(). CVE-2025-11234 [Backported to QEMU 5.2 for Debian Bullseye] --- include/io/channel-websock.h | 3 ++- io/channel-websock.c | 22 ++++++++++++++++------ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/include/io/channel-websock.h b/include/io/channel-websock.h index XXXXXXX..XXXXXXX 100644 --- a/include/io/channel-websock.h +++ b/include/io/channel-websock.h @@ -61,7 +61,8 @@ struct QIOChannelWebsock { size_t payload_remain; size_t pong_remain; QIOChannelWebsockMask mask; - guint io_tag; + guint hs_io_tag; /* tracking handshake task */ + guint io_tag; /* tracking watch task */ Error *io_err; gboolean io_eof; uint8_t opcode; diff --git a/io/channel-websock.c b/io/channel-websock.c index XXXXXXX..XXXXXXX 100644 --- a/io/channel-websock.c +++ b/io/channel-websock.c @@ -551,6 +551,7 @@ static gboolean qio_channel_websock_handshake_send(QIOChannel *ioc, trace_qio_channel_websock_handshake_fail(ioc, error_get_pretty(err)); qio_task_set_error(task, err); qio_task_complete(task); + wioc->hs_io_tag = 0; return FALSE; } @@ -566,6 +567,7 @@ static gboolean qio_channel_websock_handshake_send(QIOChannel *ioc, trace_qio_channel_websock_handshake_complete(ioc); qio_task_complete(task); } + wioc->hs_io_tag = 0; return FALSE; } trace_qio_channel_websock_handshake_pending(ioc, G_IO_OUT); @@ -592,6 +594,7 @@ static gboolean qio_channel_websock_handshake_io(QIOChannel *ioc, trace_qio_channel_websock_handshake_fail(ioc, error_get_pretty(err)); qio_task_set_error(task, err); qio_task_complete(task); + wioc->hs_io_tag = 0; return FALSE; } if (ret == 0) { @@ -603,7 +606,7 @@ static gboolean qio_channel_websock_handshake_io(QIOChannel *ioc, error_propagate(&wioc->io_err, err); trace_qio_channel_websock_handshake_reply(ioc); - qio_channel_add_watch( + wioc->hs_io_tag = qio_channel_add_watch( wioc->master, G_IO_OUT, qio_channel_websock_handshake_send, @@ -913,11 +916,12 @@ void qio_channel_websock_handshake(QIOChannelWebsock *ioc, trace_qio_channel_websock_handshake_start(ioc); trace_qio_channel_websock_handshake_pending(ioc, G_IO_IN); - qio_channel_add_watch(ioc->master, - G_IO_IN, - qio_channel_websock_handshake_io, - task, - NULL); + ioc->hs_io_tag = qio_channel_add_watch( + ioc->master, + G_IO_IN, + qio_channel_websock_handshake_io, + task, + NULL); } @@ -927,6 +931,9 @@ static void qio_channel_websock_finalize(Object *obj) buffer_free(&ioc->encinput); buffer_free(&ioc->encoutput); buffer_free(&ioc->rawinput); + if (ioc->hs_io_tag) { + g_source_remove(ioc->hs_io_tag); + } object_unref(OBJECT(ioc->master)); if (ioc->io_tag) { g_source_remove(ioc->io_tag); @@ -1222,6 +1229,9 @@ static int qio_channel_websock_close(QIOChannel *ioc, QIOChannelWebsock *wioc = QIO_CHANNEL_WEBSOCK(ioc); trace_qio_channel_websock_close(ioc); + if (wioc->hs_io_tag) { + g_clear_handle_id(&wioc->hs_io_tag, g_source_remove); + } return qio_channel_close(wioc->master, errp); }
