diff mbox series

libsoup: patch CVE-2026-4271

Message ID 20260716161842.791090-1-peter.marko@siemens.com
State New
Headers show
Series libsoup: patch CVE-2026-4271 | expand

Commit Message

Peter Marko July 16, 2026, 4:18 p.m. UTC
From: Peter Marko <peter.marko@siemens.com>

Pick patch which has closed gitlab issue for the CVE [1].
The issue is also referenced in [2]

[1] https://gitlab.gnome.org/GNOME/libsoup/-/work_items/496
[2] https://security-tracker.debian.org/tracker/CVE-2026-4271

Signed-off-by: Peter Marko <peter.marko@siemens.com>
---
 .../libsoup/libsoup/CVE-2026-4271.patch       | 362 ++++++++++++++++++
 meta/recipes-support/libsoup/libsoup_3.6.6.bb |   1 +
 2 files changed, 363 insertions(+)
 create mode 100644 meta/recipes-support/libsoup/libsoup/CVE-2026-4271.patch
diff mbox series

Patch

diff --git a/meta/recipes-support/libsoup/libsoup/CVE-2026-4271.patch b/meta/recipes-support/libsoup/libsoup/CVE-2026-4271.patch
new file mode 100644
index 0000000000..ca377e0355
--- /dev/null
+++ b/meta/recipes-support/libsoup/libsoup/CVE-2026-4271.patch
@@ -0,0 +1,362 @@ 
+From 489affa74c8a229b8a4dd541710d4a5debedb7b4 Mon Sep 17 00:00:00 2001
+From: Carlos Garcia Campos <cgarcia@igalia.com>
+Date: Mon, 16 Feb 2026 12:09:08 +0100
+Subject: [PATCH] server: protect message io while reading and writing
+
+Ensure the nghttp2 session is not destroyed while being used.
+
+Closes #496
+
+CVE: CVE-2026-4271
+Upstream-Status: Backport [https://gitlab.gnome.org/GNOME/libsoup/-/commit/489affa74c8a229b8a4dd541710d4a5debedb7b4]
+Signed-off-by: Peter Marko <peter.marko@siemens.com>
+---
+ .../http2/soup-server-message-io-http2.c      | 117 +++++++++++++-----
+ tests/http2-test.c                            |  54 ++++++++
+ 2 files changed, 141 insertions(+), 30 deletions(-)
+
+diff --git a/libsoup/server/http2/soup-server-message-io-http2.c b/libsoup/server/http2/soup-server-message-io-http2.c
+index 913afb46..6f8d1bb6 100644
+--- a/libsoup/server/http2/soup-server-message-io-http2.c
++++ b/libsoup/server/http2/soup-server-message-io-http2.c
+@@ -69,6 +69,8 @@ typedef struct {
+         GHashTable *messages;
+ 
+         guint in_callback;
++        guint protected;
++        gboolean destroyed;
+ } SoupServerMessageIOHTTP2;
+ 
+ static void soup_server_message_io_http2_send_response (SoupServerMessageIOHTTP2 *io,
+@@ -146,6 +148,8 @@ soup_server_message_io_http2_destroy (SoupServerMessageIO *iface)
+ {
+         SoupServerMessageIOHTTP2 *io = (SoupServerMessageIOHTTP2 *)iface;
+ 
++        io->destroyed = TRUE;
++
+         if (io->read_source) {
+                 g_source_destroy (io->read_source);
+                 g_source_unref (io->read_source);
+@@ -160,10 +164,14 @@ soup_server_message_io_http2_destroy (SoupServerMessageIO *iface)
+         }
+ 
+         g_clear_object (&io->iostream);
+-        g_clear_pointer (&io->session, nghttp2_session_del);
+-        g_clear_pointer (&io->messages, g_hash_table_unref);
++        io->istream = NULL;
++        io->ostream = NULL;
+ 
+-        g_free (io);
++        if (io->protected == 0) {
++                g_clear_pointer (&io->session, nghttp2_session_del);
++                g_clear_pointer (&io->messages, g_hash_table_unref);
++                g_free (io);
++        }
+ }
+ 
+ static void
+@@ -321,7 +329,33 @@ static const SoupServerMessageIOFuncs io_funcs = {
+         soup_server_message_io_http2_is_paused
+ };
+ 
++static void
++soup_server_message_io_http2_protect (SoupServerMessageIOHTTP2 *io)
++{
++        io->protected++;
++        g_object_ref (io->conn);
++}
++
+ static gboolean
++soup_server_message_io_http2_unprotect (SoupServerMessageIOHTTP2 *io)
++{
++        g_object_unref (io->conn);
++
++        if (--io->protected > 0)
++                return FALSE;
++
++        if (io->destroyed) {
++                g_clear_pointer (&io->session, nghttp2_session_del);
++                g_clear_pointer (&io->messages, g_hash_table_unref);
++                g_free (io);
++
++                return TRUE;
++        }
++
++        return FALSE;
++}
++
++static void
+ io_write (SoupServerMessageIOHTTP2 *io,
+           GError                  **error)
+ {
+@@ -336,51 +370,57 @@ io_write (SoupServerMessageIOHTTP2 *io,
+                 if (io->write_buffer_size == 0) {
+                         /* Done */
+                         io->write_buffer = NULL;
+-                        return TRUE;
++                        return;
+                 }
+         }
+ 
++        if (!io->ostream)
++                return;
++
+         gssize ret = g_pollable_stream_write (io->ostream,
+                                               io->write_buffer + io->written_bytes,
+                                               io->write_buffer_size - io->written_bytes,
+                                               FALSE, NULL, error);
+-        if (ret < 0)
+-                return FALSE;
+-
+-        io->written_bytes += ret;
+-        return TRUE;
++        if (ret > 0)
++                io->written_bytes += ret;
+ }
+ 
+ static gboolean
+ io_write_ready (GObject                  *stream,
+                 SoupServerMessageIOHTTP2 *io)
+ {
+-        SoupServerConnection *conn = io->conn;
+         GError *error = NULL;
+ 
+-        g_object_ref (conn);
++        soup_server_message_io_http2_protect (io);
++
++        while (!error) {
++                if (io->destroyed)
++                        break;
++
++                if (!nghttp2_session_want_write (io->session))
++                        break;
+ 
+-        while (!error && soup_server_connection_get_io_data (conn) == (SoupServerMessageIO *)io && nghttp2_session_want_write (io->session))
+                 io_write (io, &error);
++        }
+ 
+         if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK)) {
+                 g_error_free (error);
+-                g_object_unref (conn);
++                soup_server_message_io_http2_unprotect (io);
+                 return G_SOURCE_CONTINUE;
+         }
+ 
+-        if (soup_server_connection_get_io_data (conn) == (SoupServerMessageIO *)io) {
++        if (!io->destroyed) {
+                 if (error)
+                         h2_debug (io, NULL, "[SESSION] IO error: %s", error->message);
+ 
+                 g_clear_pointer (&io->write_source, g_source_unref);
+ 
+                 if (error || (!nghttp2_session_want_read (io->session) && !nghttp2_session_want_write (io->session)))
+-                        soup_server_connection_disconnect (conn);
++                        soup_server_connection_disconnect (io->conn);
+         }
+ 
+         g_clear_error (&error);
+-        g_object_unref (conn);
++        soup_server_message_io_http2_unprotect (io);
+ 
+         return G_SOURCE_REMOVE;
+ }
+@@ -390,13 +430,12 @@ static gboolean io_write_idle_cb (SoupServerMessageIOHTTP2* io);
+ static void
+ io_try_write (SoupServerMessageIOHTTP2 *io)
+ {
+-        SoupServerConnection *conn = io->conn;
+         GError *error = NULL;
+ 
+         if (io->write_source)
+                 return;
+ 
+-        if (io->in_callback && soup_server_connection_get_io_data (conn) == (SoupServerMessageIO *)io) {
++        if (io->in_callback && !io->destroyed) {
+                 if (!nghttp2_session_want_write (io->session))
+                         return;
+ 
+@@ -416,12 +455,19 @@ io_try_write (SoupServerMessageIOHTTP2 *io)
+                 g_clear_pointer (&io->write_idle_source, g_source_unref);
+         }
+ 
+-        g_object_ref (conn);
++        soup_server_message_io_http2_protect (io);
++
++        while (!error) {
++                if (io->destroyed)
++                        break;
++
++                if (!nghttp2_session_want_write (io->session))
++                        break;
+ 
+-        while (!error && soup_server_connection_get_io_data (conn) == (SoupServerMessageIO *)io && !io->in_callback && nghttp2_session_want_write (io->session))
+                 io_write (io, &error);
++        }
+ 
+-        if (soup_server_connection_get_io_data (conn) == (SoupServerMessageIO *)io) {
++        if (!io->destroyed) {
+                 if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK)) {
+                         g_clear_error (&error);
+                         io->write_source = g_pollable_output_stream_create_source (G_POLLABLE_OUTPUT_STREAM (io->ostream), NULL);
+@@ -434,11 +480,11 @@ io_try_write (SoupServerMessageIOHTTP2 *io)
+                         h2_debug (io, NULL, "[SESSION] IO error: %s", error->message);
+ 
+                 if (error || (!nghttp2_session_want_read (io->session) && !nghttp2_session_want_write (io->session)))
+-                        soup_server_connection_disconnect (conn);
++                        soup_server_connection_disconnect (io->conn);
+         }
+ 
+         g_clear_error (&error);
+-        g_object_unref (conn);
++        soup_server_message_io_http2_unprotect (io);
+ }
+ 
+ static gboolean
+@@ -481,31 +527,37 @@ static gboolean
+ io_read_ready (GObject                  *stream,
+                SoupServerMessageIOHTTP2 *io)
+ {
+-        SoupServerConnection *conn = io->conn;
+         gboolean progress = TRUE;
+         GError *error = NULL;
+ 
+-        g_object_ref (conn);
++        soup_server_message_io_http2_protect (io);
++
++        while (progress) {
++                if (io->destroyed)
++                        break;
++
++                if (!nghttp2_session_want_read (io->session))
++                        break;
+ 
+-        while (progress && soup_server_connection_get_io_data (conn) == (SoupServerMessageIO *)io && nghttp2_session_want_read (io->session))
+                 progress = io_read (io, &error);
++        }
+ 
+         if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK)) {
+                 g_error_free (error);
+-                g_object_unref (conn);
++                soup_server_message_io_http2_unprotect (io);
+                 return G_SOURCE_CONTINUE;
+         }
+ 
+-        if (soup_server_connection_get_io_data (conn) == (SoupServerMessageIO *)io) {
++        if (!io->destroyed) {
+                 if (error)
+                         h2_debug (io, NULL, "[SESSION] IO error: %s", error->message);
+ 
+                 if (error || (!nghttp2_session_want_read (io->session) && !nghttp2_session_want_write (io->session)))
+-                        soup_server_connection_disconnect (conn);
++                        soup_server_connection_disconnect (io->conn);
+         }
+ 
+         g_clear_error (&error);
+-        g_object_unref (conn);
++        soup_server_message_io_http2_unprotect (io);
+ 
+         return G_SOURCE_REMOVE;
+ }
+@@ -931,5 +983,10 @@ soup_server_message_io_http2_new (SoupServerConnection  *conn,
+         nghttp2_submit_settings (io->session, NGHTTP2_FLAG_NONE, settings, G_N_ELEMENTS (settings));
+         io_try_write (io);
+ 
++#ifdef __clang_analyzer__
++        // Suppress false positive about io being destroyed here, since at this point we have only
++        // send the initial settings and not callback is called.
++        [[clang::suppress]]
++#endif
+         return (SoupServerMessageIO *)io;
+ }
+diff --git a/tests/http2-test.c b/tests/http2-test.c
+index 0846a0a6..d12b4bf7 100644
+--- a/tests/http2-test.c
++++ b/tests/http2-test.c
+@@ -1268,6 +1268,40 @@ do_broken_pseudo_header_test (Test *test, gconstpointer data)
+ 	g_uri_unref (uri);
+ }
+ 
++static void
++disconnect_on_got_headers (SoupServerMessage *msg, gpointer user_data)
++{
++        GUri *uri;
++        SoupServerConnection *conn;
++
++        uri = soup_server_message_get_uri (msg);
++        if (!g_str_equal (g_uri_get_path (uri), "/close-on-got-headers"))
++                return;
++
++        conn = soup_server_message_get_connection (msg);
++        soup_server_connection_disconnect (conn);
++}
++
++static void
++do_server_disconnect_on_got_headers_test (Test *test, gconstpointer data)
++{
++        SoupMessage *msg;
++        GUri *uri;
++        GBytes *response;
++        GError *error = NULL;
++
++        uri = g_uri_parse_relative (base_uri, "/close-on-got-headers", SOUP_HTTP_URI_FLAGS, NULL);
++        msg = soup_message_new_from_uri (SOUP_METHOD_GET, uri);
++
++        response = soup_test_session_async_send (test->session, msg, NULL, &error);
++        g_assert_error (error, G_IO_ERROR, G_IO_ERROR_PARTIAL_INPUT);
++
++        g_clear_error (&error);
++        g_bytes_unref (response);
++        g_object_unref (msg);
++        g_uri_unref (uri);
++}
++
+ static gboolean
+ unpause_message (SoupServerMessage *msg)
+ {
+@@ -1396,12 +1430,26 @@ server_handler (SoupServer        *server,
+                 shutdown (fd, SHUT_WR);
+ #endif
+ 
++                soup_server_message_set_response (msg, "text/plain",
++                                                  SOUP_MEMORY_STATIC,
++                                                  "Success!", 8);
++        } else if (strcmp (path, "/close-on-got-headers") == 0) {
+                 soup_server_message_set_response (msg, "text/plain",
+                                                   SOUP_MEMORY_STATIC,
+                                                   "Success!", 8);
+         }
+ }
+ 
++static void
++server_request_started (SoupServer           *server,
++                        SoupServerMessage    *msg,
++                        SoupServerConnection *conn,
++                        gpointer              user_data)
++{
++        g_signal_connect (msg, "got-headers",
++                          G_CALLBACK (disconnect_on_got_headers), NULL);
++}
++
+ static gboolean
+ server_basic_auth_callback (SoupAuthDomain    *auth_domain,
+                             SoupServerMessage *msg,
+@@ -1428,6 +1476,8 @@ main (int argc, char **argv)
+                 return 0;
+ 
+         server = soup_test_server_new (SOUP_TEST_SERVER_IN_THREAD | SOUP_TEST_SERVER_HTTP2);
++        g_signal_connect (server, "request-started",
++                          G_CALLBACK (server_request_started), NULL);
+         auth = soup_auth_domain_basic_new ("realm", "http2-test",
+                                            "auth-callback", server_basic_auth_callback,
+                                            NULL);
+@@ -1584,6 +1634,10 @@ main (int argc, char **argv)
+                     setup_session,
+                     do_broken_pseudo_header_test,
+                     teardown_session);
++        g_test_add ("/http2/server-disconnect-on-got-headers", Test, NULL,
++                    setup_session,
++                    do_server_disconnect_on_got_headers_test,
++                    teardown_session);
+ 
+ 	ret = g_test_run ();
+ 
diff --git a/meta/recipes-support/libsoup/libsoup_3.6.6.bb b/meta/recipes-support/libsoup/libsoup_3.6.6.bb
index d18a16379b..e5c2d18ded 100644
--- a/meta/recipes-support/libsoup/libsoup_3.6.6.bb
+++ b/meta/recipes-support/libsoup/libsoup_3.6.6.bb
@@ -20,6 +20,7 @@  SRC_URI += "file://CVE-2025-32049-1.patch \
             file://CVE-2026-1539.patch \
             file://CVE-2026-5119.patch \
             file://CVE-2026-2708.patch \
+            file://CVE-2026-4271.patch \
 "
 
 PROVIDES = "libsoup-3.0"