diff mbox series

[scarthgap,3/5] libsoup-2.4: fix CVE-2026-2443

Message ID 20260929205055.2403390-4-jason.stasiak@garmin.com
State New
Headers show
Series libsoup-2.4: Fix several CVEs | expand

Commit Message

Jason Stasiak Sept. 29, 2026, 8:50 p.m. UTC
Backport fix for CVE-2026-2443 from meta-oe to OE-core. Update CVE
patch to restore the range-test validation unit test from the
upstream libsoup3 patch (b9a1c0663ff8ab6e79715db4b35b54f560416ddd).

(From meta-oe rev: 07d67228162018f5f619dce7183f85e79293378d)

Signed-off-by: Jason Stasiak <jason.stasiak@gmail.com>
---
 .../libsoup/libsoup-2.4/CVE-2026-2443.patch   | 390 ++++++++++++++++++
 .../libsoup/libsoup-2.4_2.74.3.bb             |   1 +
 2 files changed, 391 insertions(+)
 create mode 100644 meta/recipes-support/libsoup/libsoup-2.4/CVE-2026-2443.patch
diff mbox series

Patch

diff --git a/meta/recipes-support/libsoup/libsoup-2.4/CVE-2026-2443.patch b/meta/recipes-support/libsoup/libsoup-2.4/CVE-2026-2443.patch
new file mode 100644
index 0000000000..e4d5f184fa
--- /dev/null
+++ b/meta/recipes-support/libsoup/libsoup-2.4/CVE-2026-2443.patch
@@ -0,0 +1,390 @@ 
+From 7bb3115a296154e3f465900ea5c984a493385a7f Mon Sep 17 00:00:00 2001
+From: Philip Withnall <pwithnall@gnome.org>
+Date: Fri, 19 Dec 2025 23:49:05 +0000
+Subject: [PATCH] Fix CVE-2026-2443
+
+Upstream-Status: Backport [
+c1796442 soup-message-headers: Rework Range response statuses to match Apache
+191ef313 soup-message-headers: Fix rejection of Range headers with trailing garbage
+be677bea soup-message-headers: Fix parsing of invalid Range suffix lengths
+2bbfdfe8 soup-message-headers: Reject ranges where end is before start
+739bf7cb soup-message-headers: Reject invalid Range ends longer than the content
+]
+CVE: CVE-2026-2443
+
+Signed-off-by: Changqing Li <changqing.li@windriver.com>
+
+Backport the range-test validation unit tests from the upstream libsoup3 patches.
+
+Upstream-Status: Backport [
+5890c42d tests: Add more tests for invalid Range headers
+c1796442 soup-message-headers: Rework Range response statuses to match Apache
+6574a84f tests: Allow range tests to check more response statuses
+191ef313 soup-message-headers: Fix rejection of Range headers with trailing garbage
+be677bea soup-message-headers: Fix parsing of invalid Range suffix lengths
+2bbfdfe8 soup-message-headers: Reject ranges where end is before start
+739bf7cb soup-message-headers: Reject invalid Range ends longer than the content
+]
+
+Signed-off-by: Jason Stasiak <jason.stasiak@gmail.com>
+---
+ libsoup/soup-message-headers.c |  62 ++++++++++----
+ tests/range-test.c             | 149 ++++++++++++++++++++++++++++-----
+ 2 files changed, 172 insertions(+), 39 deletions(-)
+
+diff --git a/libsoup/soup-message-headers.c b/libsoup/soup-message-headers.c
+index ff10e103..f30dd461 100644
+--- a/libsoup/soup-message-headers.c
++++ b/libsoup/soup-message-headers.c
+@@ -940,10 +940,16 @@ sort_ranges (gconstpointer a, gconstpointer b)
+ }
+ 
+ /* like soup_message_headers_get_ranges(), except it returns:
+- *   SOUP_STATUS_OK if there is no Range or it should be ignored.
+- *   SOUP_STATUS_PARTIAL_CONTENT if there is at least one satisfiable range.
+- *   SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE if @check_satisfiable
+- *     is %TRUE and the request is not satisfiable given @total_length.
++ *  - SOUP_STATUS_OK if there is no Range or it should be ignored due to being
++ *    entirely invalid.
++ *  - SOUP_STATUS_PARTIAL_CONTENT if there is at least one satisfiable range.
++ *  - SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE if @check_satisfiable
++ *     is %TRUE, the Range is valid, but no part of the request is satisfiable
++ *     given @total_length.
++ *
++ * @ranges and @length are only set if SOUP_STATUS_PARTIAL_CONTENT is returned.
++ *
++ * See https://httpwg.org/specs/rfc9110.html#field.range
+  */
+ guint
+ soup_message_headers_get_ranges_internal (SoupMessageHeaders  *hdrs,
+@@ -957,22 +963,28 @@ soup_message_headers_get_ranges_internal (SoupMessageHeaders  *hdrs,
+ 	GArray *array;
+ 	char *spec, *end;
+ 	guint status = SOUP_STATUS_OK;
++	gboolean is_all_valid = TRUE;
+ 
+ 	if (!range || strncmp (range, "bytes", 5) != 0)
+-		return status;
++		return SOUP_STATUS_OK;  /* invalid header or unknown range unit */
+ 
+ 	range += 5;
+ 	while (g_ascii_isspace (*range))
+ 		range++;
+ 	if (*range++ != '=')
+-		return status;
++		return SOUP_STATUS_OK;  /* invalid header */
+ 	while (g_ascii_isspace (*range))
+ 		range++;
+ 
+ 	range_list = soup_header_parse_list (range);
+ 	if (!range_list)
+-		return status;
++		return SOUP_STATUS_OK;  /* invalid list */
+ 
++	/* Loop through the ranges and modify the status accordingly. Default to
++	 * status 200 (OK, ignoring the ranges). Switch to status 206 (Partial
++	 * Content) if there is at least one partially valid range. Switch to
++	 * status 416 (Range Not Satisfiable) if there are no partially valid
++	 * ranges at all. */
+ 	array = g_array_new (FALSE, FALSE, sizeof (SoupRange));
+ 	for (r = range_list; r; r = r->next) {
+ 		SoupRange cur;
+@@ -985,30 +997,44 @@ soup_message_headers_get_ranges_internal (SoupMessageHeaders  *hdrs,
+ 			cur.start = g_ascii_strtoull (spec, &end, 10);
+ 			if (*end == '-')
+ 				end++;
+-			if (*end) {
++			if (*end)
+ 				cur.end = g_ascii_strtoull (end, &end, 10);
+-				if (cur.end < cur.start) {
+-					status = SOUP_STATUS_OK;
+-					break;
+-				}
+-			} else
++			else
+ 				cur.end = total_length - 1;
+ 		}
++
+ 		if (*end) {
+-			status = SOUP_STATUS_OK;
+-			break;
+-		} else if (check_satisfiable && cur.start >= total_length) {
+-			if (status == SOUP_STATUS_OK)
+-				status = SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE;
++			/* Junk after the range */
++			is_all_valid = FALSE;
++			continue;
++		}
++
++		if (cur.end < cur.start) {
++			is_all_valid = FALSE;
++			continue;
++		}
++
++		g_assert (cur.start >= 0);
++		if (cur.end >= total_length)
++			cur.end = total_length - 1;
++
++		if (cur.start >= total_length) {
++			/* Range is valid, but unsatisfiable */
+ 			continue;
+ 		}
+ 
++		/* We have at least one (at least partially) satisfiable range */
+ 		g_array_append_val (array, cur);
+ 		status = SOUP_STATUS_PARTIAL_CONTENT;
+ 	}
+ 	soup_header_free_list (range_list);
+ 
+ 	if (status != SOUP_STATUS_PARTIAL_CONTENT) {
++		g_assert (status == SOUP_STATUS_OK);
++
++		if (is_all_valid && check_satisfiable)
++			status = SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE;
++
+ 		g_array_free (array, TRUE);
+ 		return status;
+ 	}
+diff --git a/tests/range-test.c b/tests/range-test.c
+index d3c49963..8bd0fa1c 100644
+--- a/tests/range-test.c
++++ b/tests/range-test.c
+@@ -57,7 +57,8 @@ check_part (SoupMessageHeaders *headers, const char *body, gsize body_len,
+ 
+ static void
+ do_single_range (SoupSession *session, SoupMessage *msg,
+-		 int start, int end, gboolean succeed)
++		 int start, int end, SoupStatus expected_status,
++		 int expected_start, int expected_end)
+ {
+ 	const char *content_type;
+ 
+@@ -66,7 +67,7 @@ do_single_range (SoupSession *session, SoupMessage *msg,
+ 
+ 	soup_session_send_message (session, msg);
+ 
+-	if (!succeed) {
++	if (expected_status == SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE) {
+ 		soup_test_assert_message_status (msg, SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE);
+ 		if (msg->status_code != SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE) {
+ 			const char *content_range;
+@@ -76,31 +77,78 @@ do_single_range (SoupSession *session, SoupMessage *msg,
+ 			if (content_range)
+ 				debug_printf (1, "    Content-Range: %s\n", content_range);
+ 		}
+-
+ 		g_object_unref (msg);
+ 		return;
++	} else if (expected_status == SOUP_STATUS_OK) {
++		soup_test_assert_message_status (msg, SOUP_STATUS_OK);
++
++		content_type = soup_message_headers_get_content_type(msg->response_headers, NULL);
++		g_assert_cmpstr (content_type, !=, "multipart/byteranges");
++
++		g_assert_false (soup_message_headers_get_content_range (msg->response_headers, NULL,
++			NULL, NULL));
++		g_assert_cmpint (soup_message_headers_get_content_length (msg->response_headers),
++			==, full_response->length);
++		} else {
++		soup_test_assert_message_status (msg, SOUP_STATUS_PARTIAL_CONTENT);
++		content_type = soup_message_headers_get_content_type (msg->response_headers, NULL);
++		g_assert_cmpstr (content_type, !=, "multipart/byteranges");
++
++		check_part (msg->response_headers, msg->response_body->data,
++			msg->response_body->length, TRUE, expected_start, expected_end);
+ 	}
+ 
+-	soup_test_assert_message_status (msg, SOUP_STATUS_PARTIAL_CONTENT);
+-
+-	content_type = soup_message_headers_get_content_type (
+-		msg->response_headers, NULL);
+-	g_assert_cmpstr (content_type, !=, "multipart/byteranges");
+-
+-	check_part (msg->response_headers, msg->response_body->data,
+-		    msg->response_body->length, TRUE, start, end);
+ 	g_object_unref (msg);
+ }
+ 
+ static void
+ request_single_range (SoupSession *session, const char *uri,
+-		      int start, int end, gboolean succeed)
++		      int start, int end, SoupStatus expected_status,
++			  int expected_start, int expected_end)
+ {
+ 	SoupMessage *msg;
+ 
+ 	msg = soup_message_new ("GET", uri);
+ 	soup_message_headers_set_range (msg->request_headers, start, end);
+-	do_single_range (session, msg, start, end, succeed);
++	do_single_range (session, msg, start, end, expected_status, expected_start, expected_end);
++}
++
++/* This always asserts failure (either 406 or 200 with no Content-Range); it’s
++ * intended to be used for passing invalid
++ * Range header formats which can’t be built by calling
++ * soup_message_headers_set_range(). */
++static void
++request_single_range_by_string (SoupSession *session, const char *uri,
++             const char *range, SoupStatus expected_status)
++{
++	SoupMessage *msg;
++
++	msg = soup_message_new ("GET", uri);
++	soup_message_headers_replace (msg->request_headers, "Range", range);
++
++	debug_printf (1, "    Range: %s\n",
++	soup_message_headers_get_one (msg->request_headers, "Range"));
++
++	soup_session_send_message (session, msg);
++
++	if (expected_status == SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE) {
++		soup_test_assert_message_status (msg, SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE);
++	} else {
++		const char *content_type;
++
++		soup_test_assert_message_status (msg, SOUP_STATUS_OK);
++
++		content_type = soup_message_headers_get_content_type (msg->response_headers, NULL);
++		g_assert_cmpstr (content_type, !=, "multipart/byteranges");
++
++		g_assert_false (soup_message_headers_get_content_range (msg->response_headers,
++			NULL, NULL, NULL));
++
++		g_assert_cmpint (soup_message_headers_get_content_length (msg->response_headers),
++			==, full_response->length);
++	}
++
++	g_object_unref (msg);
+ }
+ 
+ static void
+@@ -165,7 +213,9 @@ request_double_range (SoupSession *session, const char *uri,
+ 		do_single_range (session, msg,
+ 				 MIN (first_start, second_start),
+ 				 MAX (first_end, second_end),
+-				 TRUE);
++				 SOUP_STATUS_PARTIAL_CONTENT,
++				 MIN (first_start, second_start),
++				 MAX (first_end, second_end));
+ 	} else
+ 		do_multi_range (session, msg, expected_return_ranges);
+ }
+@@ -193,7 +243,9 @@ request_triple_range (SoupSession *session, const char *uri,
+ 		do_single_range (session, msg,
+ 				 MIN (first_start, MIN (second_start, third_start)),
+ 				 MAX (first_end, MAX (second_end, third_end)),
+-				 TRUE);
++				 SOUP_STATUS_PARTIAL_CONTENT,
++				 MIN (first_start, MIN (second_start, third_start)),
++				 MAX (first_end, MAX (second_end, third_end)));
+ 	} else
+ 		do_multi_range (session, msg, expected_return_ranges);
+ }
+@@ -248,7 +300,8 @@ do_range_test (SoupSession *session, const char *uri,
+ 	debug_printf (1, "Requesting %d-%d\n", 0 * twelfths, 1 * twelfths);
+ 	request_single_range (session, uri,
+ 			      0 * twelfths, 1 * twelfths,
+-			      TRUE);
++			      SOUP_STATUS_PARTIAL_CONTENT,
++			      0 * twelfths, 1 * twelfths);
+ 
+ 	/* B: 11, end-relative request. These two are mostly redundant
+ 	 * in terms of data coverage, but they may still catch
+@@ -257,11 +310,13 @@ do_range_test (SoupSession *session, const char *uri,
+ 	debug_printf (1, "Requesting %d-\n", 11 * twelfths);
+ 	request_single_range (session, uri,
+ 			      11 * twelfths, -1,
+-			      TRUE);
++			      SOUP_STATUS_PARTIAL_CONTENT,
++			      11 * twelfths, -1);
+ 	debug_printf (1, "Requesting -%d\n", 1 * twelfths);
+ 	request_single_range (session, uri,
+ 			      -1 * twelfths, -1,
+-			      TRUE);
++			      SOUP_STATUS_PARTIAL_CONTENT,
++			      -1 * twelfths, -1);
+ 
+ 	/* C: 2 and 5 */
+ 	debug_printf (1, "Requesting %d-%d,%d-%d\n",
+@@ -314,7 +369,8 @@ do_range_test (SoupSession *session, const char *uri,
+ 		      (int) full_response->length + 100);
+ 	request_single_range (session, uri,
+ 			      full_response->length + 1, full_response->length + 100,
+-			      FALSE);
++			      SOUP_STATUS_REQUESTED_RANGE_NOT_SATISFIABLE,
++			      0, 0);
+ 
+ 	debug_printf (1, "Requesting (semi-invalid) 1-10,%d-%d,20-30\n",
+ 		      (int) full_response->length + 1,
+@@ -322,7 +378,58 @@ do_range_test (SoupSession *session, const char *uri,
+ 	request_semi_invalid_range (session, uri,
+ 				    1, 10,
+ 				    full_response->length + 1, full_response->length + 100,
+-				    20, 30); 
++				    20, 30);
++
++	debug_printf (1, "Requesting (invalid end) %d-%d\n",
++		      1,
++		      (int) full_response->length + 1000);
++	request_single_range (session, uri,
++			      1, full_response->length + 1000,
++			      SOUP_STATUS_PARTIAL_CONTENT,
++			      1, full_response->length - 1);
++
++	debug_printf (1, "Requesting (end before start) %d-%d\n",
++		      10,
++		      1);
++	request_single_range (session, uri,
++			      10, 1,
++			      SOUP_STATUS_OK,
++			      1, full_response->length);
++
++	debug_printf (1, "Requesting (malformed suffix length) -0\n");
++	request_single_range_by_string (session, uri,
++					"bytes=-0",
++					SOUP_STATUS_OK);
++
++	debug_printf (1, "Requesting (extra content after valid header value) 0-10\n");
++	request_single_range_by_string (session, uri,
++					"bytes=0-10 but with weird trailing content",
++					SOUP_STATUS_OK);
++
++	debug_printf (1, "Requesting (invalid range dash) 0a10\n");
++	request_single_range_by_string (session, uri,
++					"bytes=0a10",
++					SOUP_STATUS_OK);
++
++	debug_printf (1, "Requesting (invalid range unit) 0-10\n");
++	request_single_range_by_string (session, uri,
++					"horses=0-10",
++					SOUP_STATUS_OK);
++
++	debug_printf (1, "Requesting (missing equals) 0-10\n");
++	request_single_range_by_string (session, uri,
++					"bytes 0-10",
++					SOUP_STATUS_OK);
++
++	debug_printf (1, "Requesting (end before start but with whitespace) 10-1\n");
++	request_single_range_by_string (session, uri,
++					"bytes \t = \t 10-1",
++					SOUP_STATUS_OK);
++
++	debug_printf (1, "Requesting (delimiters but no ranges)\n");
++	request_single_range_by_string (session, uri,
++					"bytes=, ,,\t, ",
++					SOUP_STATUS_OK);
+ }
+ 
+ static void
+@@ -341,7 +448,7 @@ do_apache_range_test (void)
+ 
+ static void
+ server_handler (SoupServer        *server,
+-		SoupMessage       *msg, 
++		SoupMessage       *msg,
+ 		const char        *path,
+ 		GHashTable        *query,
+ 		SoupClientContext *client,
+-- 
+2.55.0
+
diff --git a/meta/recipes-support/libsoup/libsoup-2.4_2.74.3.bb b/meta/recipes-support/libsoup/libsoup-2.4_2.74.3.bb
index 9da2dde714..18f82f8ef7 100644
--- a/meta/recipes-support/libsoup/libsoup-2.4_2.74.3.bb
+++ b/meta/recipes-support/libsoup/libsoup-2.4_2.74.3.bb
@@ -43,6 +43,7 @@  SRC_URI = "${GNOME_MIRROR}/libsoup/${SHRT_VER}/libsoup-${PV}.tar.xz \
            file://CVE-2025-4945.patch \
            file://CVE-2026-1539.patch \
            file://CVE-2026-1801.patch \
+           file://CVE-2026-2443.patch \
 "
 SRC_URI[sha256sum] = "e4b77c41cfc4c8c5a035fcdc320c7bc6cfb75ef7c5a034153df1413fa1d92f13"