From 446b2e0a97ac1c241e9c58545f1b0f5f962d98e2 Mon Sep 17 00:00:00 2001
From: Milan Crha <mcrha@redhat.com>
Date: Tue, 15 Apr 2025 12:17:39 +0200
Subject: [PATCH] soup-message-headers: Correct merge of ranges

It had been skipping every second range, which generated an array
of a lot of insane ranges, causing large memory usage by the server.

Closes #428
---
 libsoup/soup-message-headers.c |   1 +
 tests/meson.build              |   1 +
 tests/server-mem-limit-test.c  | 172 +++++++++++++++++++++++++++++++++
 3 files changed, 174 insertions(+)
 create mode 100644 tests/server-mem-limit-test.c

diff --git a/libsoup/soup-message-headers.c b/libsoup/soup-message-headers.c
index ee7a3cb1..f101d4b4 100644
--- a/libsoup/soup-message-headers.c
+++ b/libsoup/soup-message-headers.c
@@ -1244,6 +1244,7 @@ soup_message_headers_get_ranges_internal (SoupMessageHeaders  *hdrs,
 			if (cur->start <= prev->end) {
 				prev->end = MAX (prev->end, cur->end);
 				g_array_remove_index (array, i);
+				i--;
 			}
 		}
 	}
diff --git a/tests/meson.build b/tests/meson.build
index ee118a01..8e7b51d1 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -102,6 +102,7 @@ tests = [
   {'name': 'samesite'},
   {'name': 'session'},
   {'name': 'server-auth'},
+  {'name': 'server-mem-limit'},
   {'name': 'server'},
   {'name': 'sniffing',
     'depends': [test_resources],
diff --git a/tests/server-mem-limit-test.c b/tests/server-mem-limit-test.c
new file mode 100644
index 00000000..929f1a01
--- /dev/null
+++ b/tests/server-mem-limit-test.c
@@ -0,0 +1,172 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- */
+/*
+ * Copyright 2025 Red Hat <www.redhat.com>
+ */
+
+#include "test-utils.h"
+
+#include <sys/resource.h>
+
+typedef struct {
+	SoupServer *server;
+	GUri *base_uri, *ssl_base_uri;
+	GSList *handlers;
+} ServerData;
+
+static void
+server_callback (SoupServer        *server,
+		 SoupServerMessage *msg,
+		 const char        *path,
+		 GHashTable        *query,
+		 gpointer           data)
+{
+	const char *method;
+
+	soup_message_headers_append (soup_server_message_get_response_headers (msg),
+				     "X-Handled-By", "server_callback");
+
+	if (!strcmp (path, "*")) {
+		soup_test_assert (FALSE, "default server_callback got request for '*'");
+		soup_server_message_set_status (msg, SOUP_STATUS_INTERNAL_SERVER_ERROR, NULL);
+		return;
+	}
+
+	method = soup_server_message_get_method (msg);
+	if (method != SOUP_METHOD_GET && method != SOUP_METHOD_POST) {
+		soup_server_message_set_status (msg, SOUP_STATUS_NOT_IMPLEMENTED, NULL);
+		return;
+	}
+
+	soup_server_message_set_status (msg, SOUP_STATUS_OK, NULL);
+	soup_server_message_set_response (msg, "text/plain",
+					  SOUP_MEMORY_STATIC, "index", 5);
+}
+
+static void
+server_setup_nohandler (ServerData *sd, gconstpointer test_data)
+{
+	sd->server = soup_test_server_new (SOUP_TEST_SERVER_IN_THREAD);
+	sd->base_uri = soup_test_server_get_uri (sd->server, "http", NULL);
+	if (tls_available)
+		sd->ssl_base_uri = soup_test_server_get_uri (sd->server, "https", NULL);
+}
+
+static void
+server_add_handler (ServerData         *sd,
+		    const char         *path,
+		    SoupServerCallback  callback,
+		    gpointer            user_data,
+		    GDestroyNotify      destroy)
+{
+	soup_server_add_handler (sd->server, path, callback, user_data, destroy);
+	sd->handlers = g_slist_prepend (sd->handlers, g_strdup (path));
+}
+
+static void
+server_setup (ServerData *sd, gconstpointer test_data)
+{
+	server_setup_nohandler (sd, test_data);
+	server_add_handler (sd, NULL, server_callback, NULL, NULL);
+}
+
+static void
+server_teardown (ServerData *sd, gconstpointer test_data)
+{
+	GSList *iter;
+
+	for (iter = sd->handlers; iter; iter = iter->next)
+		soup_server_remove_handler (sd->server, iter->data);
+	g_slist_free_full (sd->handlers, g_free);
+
+	g_clear_pointer (&sd->server, soup_test_server_quit_unref);
+	g_clear_pointer (&sd->base_uri, g_uri_unref);
+	g_clear_pointer (&sd->ssl_base_uri, g_uri_unref);
+}
+
+static void
+server_file_callback (SoupServer        *server,
+		      SoupServerMessage *msg,
+		      const char        *path,
+		      GHashTable        *query,
+		      gpointer           data)
+{
+	void *mem;
+
+	if (strcmp (path, "/file") != 0) {
+		soup_test_assert (FALSE, "server_file_callback got request for '%s'", path);
+		soup_server_message_set_status (msg, SOUP_STATUS_INTERNAL_SERVER_ERROR, NULL);
+		return;
+	}
+
+	if (soup_server_message_get_method (msg) != SOUP_METHOD_GET) {
+		soup_server_message_set_status (msg, SOUP_STATUS_METHOD_NOT_ALLOWED, NULL);
+		return;
+	}
+
+	mem = g_malloc0 (sizeof (char) * 1024 *1024);
+	soup_server_message_set_response (msg, "application/octet-stream", SOUP_MEMORY_TAKE, mem, sizeof (char) * 1024 *1024);
+	soup_server_message_set_status (msg, SOUP_STATUS_OK, NULL);
+}
+
+static void
+do_ranges_overlaps_test (ServerData *sd, gconstpointer test_data)
+{
+	SoupSession *session;
+	SoupMessage *msg;
+	GString *range;
+	GUri *uri;
+	const char *chunk = ",0,0,0,0,0,0,0,0,0,0,0";
+
+	g_test_bug ("428");
+
+	#ifdef G_OS_WIN32
+	g_test_skip ("Cannot run under windows");
+	return;
+	#endif
+
+	range = g_string_sized_new (99 * 1024);
+	g_string_append (range, "bytes=1024");
+	while (range->len < 99 * 1024)
+		g_string_append (range, chunk);
+
+	session = soup_test_session_new (NULL);
+	server_add_handler (sd, "/file", server_file_callback, NULL, NULL);
+
+	uri = g_uri_parse_relative (sd->base_uri, "/file", SOUP_HTTP_URI_FLAGS, NULL);
+
+	msg = soup_message_new_from_uri ("GET", uri);
+	soup_message_headers_append (soup_message_get_request_headers (msg), "Range", range->str);
+
+	soup_test_session_send_message (session, msg);
+
+	soup_test_assert_message_status (msg, SOUP_STATUS_PARTIAL_CONTENT);
+
+	g_object_unref (msg);
+
+	g_string_free (range, TRUE);
+	g_uri_unref (uri);
+
+	soup_test_session_abort_unref (session);
+}
+
+int
+main (int argc, char **argv)
+{
+	int ret;
+
+	test_init (argc, argv, NULL);
+
+	#ifndef G_OS_WIN32
+	struct rlimit new_rlimit = { 1024 * 1024 * 64, 1024 * 1024 * 64 };
+	/* limit memory usage, to trigger too large memory allocation abort */
+	g_assert_cmpint (setrlimit (RLIMIT_DATA, &new_rlimit), ==, 0);
+	#endif
+
+	g_test_add ("/server-mem/range-overlaps", ServerData, NULL,
+		    server_setup, do_ranges_overlaps_test, server_teardown);
+
+	ret = g_test_run ();
+
+	test_cleanup ();
+	return ret;
+}
-- 
GitLab


