[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

[PATCH] qemu: Fix CVE-2025-11234 for Bullseye - sponsor needed



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 <vejeta@gmail.com>
From: Daniel P. Berrangé <berrange@redhat.com>
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);
 }


Reply to: