commit 346edfd89768039ff0f725f0069be5025f297e82
parent f5fe41cfc69871caf7981220af99513d7cd1b225
Author: Christian Grothoff <christian@grothoff.org>
Date: Sun, 7 Dec 2025 14:08:39 +0100
add OOM test
Diffstat:
2 files changed, 726 insertions(+), 0 deletions(-)
diff --git a/src/tests/raw/Makefile.am b/src/tests/raw/Makefile.am
@@ -25,10 +25,16 @@ $(top_builddir)/src/mhd2/libmicrohttpd2.la: $(top_builddir)/src/mhd2/Makefile
$(am__cd) $(top_builddir)/src/mhd2 && $(MAKE) $(AM_MAKEFLAGS) libmicrohttpd2.la
check_PROGRAMS = \
+ test_ram \
test_raw
TESTS = $(check_PROGRAMS)
+test_ram_SOURCES = \
+ test_ram.c
+test_ram_LDADD = \
+ -lmicrohttpd2
+
test_raw_SOURCES = \
test_raw.c
test_raw_LDADD = \
diff --git a/src/tests/raw/test_ram.c b/src/tests/raw/test_ram.c
@@ -0,0 +1,720 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later OR (GPL-2.0-or-later WITH eCos-exception-2.0) */
+/*
+ This file is part of GNU libmicrohttpd.
+ Copyright (C) 2025 Christian Grothoff
+
+ GNU libmicrohttpd is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ GNU libmicrohttpd is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ Alternatively, you can redistribute GNU libmicrohttpd and/or
+ modify it under the terms of the GNU General Public License as
+ published by the Free Software Foundation; either version 2 of
+ the License, or (at your option) any later version, together
+ with the eCos exception, as follows:
+
+ As a special exception, if other files instantiate templates or
+ use macros or inline functions from this file, or you compile this
+ file and link it with other works to produce a work based on this
+ file, this file does not by itself cause the resulting work to be
+ covered by the GNU General Public License. However the source code
+ for this file must still be made available in accordance with
+ section (3) of the GNU General Public License v2.
+
+ This exception does not invalidate any other reasons why a work
+ based on this file might be covered by the GNU General Public
+ License.
+
+ You should have received copies of the GNU Lesser General Public
+ License and the GNU General Public License along with this library;
+ if not, see <https://www.gnu.org/licenses/>.
+*/
+
+/**
+ * @file test_ram.c
+ * @brief tests handling of memory pool exhaustion
+ * @author Christian Grothoff
+ */
+#include <stdio.h>
+#include <stdbool.h>
+#include <errno.h>
+#include <string.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <arpa/inet.h>
+#include <netinet/ip.h>
+#include "microhttpd2.h"
+
+
+/**
+ * How big do we make the MHD buffer? Use a small value so we
+ * can trigger OOM in a reasonable amount of time.
+ */
+#define BUFFER_SIZE 2048
+
+/**
+ * What is the step size. Should eventually use 1, but
+ * as long as we get tons of failures, a larger step size
+ * is probably nicer.
+ */
+#define STEP 71
+
+/**
+ * Our port.
+ */
+static uint16_t port;
+
+/**
+ * Set to true once we hit the out-of-memory condition.
+ */
+static bool out_of_memory;
+
+/**
+ * Callback used by libmicrohttpd in order to obtain content. The
+ * callback is to copy at most @a max bytes of content into @a buf or
+ * provide zero-copy data for #MHD_DCC_action_continue_zc().
+ *
+ * @param dyn_cont_cls closure argument to the callback
+ * @param ctx the context to produce the action to return,
+ * the pointer is only valid until the callback returns
+ * @param pos position in the datastream to access;
+ * note that if a `struct MHD_Response` object is re-used,
+ * it is possible for the same content reader to
+ * be queried multiple times for the same data;
+ * however, if a `struct MHD_Response` is not re-used,
+ * libmicrohttpd guarantees that "pos" will be
+ * the sum of all data sizes provided by this callback
+ * @param[out] buf where to copy the data
+ * @param max maximum number of bytes to copy to @a buf (size of @a buf),
+ if the size of the content of the response is known then size
+ of the buffer is never larger than amount of the content left
+ * @return action to use,
+ * NULL in case of any error (the response will be aborted)
+ */
+static const struct MHD_DynamicContentCreatorAction *
+dyn_cc (void *dyn_cont_cls,
+ struct MHD_DynamicContentCreatorContext *ctx,
+ uint_fast64_t pos,
+ void *buf,
+ size_t max)
+{
+ int *flag = dyn_cont_cls;
+ struct MHD_NameValueCStr footer = {
+ .name = "Footer",
+ .value = "Value"
+ };
+
+ if (0 == *flag)
+ return MHD_DCC_action_finish_with_footer (ctx,
+ 1,
+ &footer);
+ (*flag) = 0;
+ memset (buf,
+ 'a',
+ max);
+ return MHD_DCC_action_continue (ctx,
+ max);
+}
+
+
+/**
+ * This method is called by libmicrohttpd when response with dynamic content
+ * is being destroyed. It should be used to free resources associated
+ * with the dynamic content.
+ *
+ * @param[in] free_cls closure
+ * @ingroup response
+ */
+static void
+dyn_cc_free (void *free_cls)
+{
+ free (free_cls);
+}
+
+
+/**
+ * Function to process data uploaded by a client.
+ *
+ * @param upload_cls the argument given together with the function
+ * pointer when the handler was registered with MHD
+ * @param request the request is being processed
+ * @param content_data_size the size of the @a content_data,
+ * zero when all data have been processed
+ * @param[in] content_data the uploaded content data,
+ * may be modified in the callback,
+ * valid only until return from the callback,
+ * NULL when all data have been processed
+ * @return action specifying how to proceed:
+ * #MHD_upload_action_continue() to continue upload (for incremental
+ * upload processing only),
+ * #MHD_upload_action_suspend() to stop reading the upload until
+ * the request is resumed,
+ * #MHD_upload_action_abort_request() to close the socket,
+ * or a response to discard the rest of the upload and transmit
+ * the response
+ * @ingroup action
+ */
+static const struct MHD_UploadAction *
+upload_cb (void *upload_cls,
+ struct MHD_Request *request,
+ size_t content_data_size,
+ void *content_data)
+{
+ int *flag;
+
+ (void) upload_cls;
+ (void) content_data_size;
+ (void) content_data;
+ flag = malloc (sizeof (int));
+ *flag = 1;
+
+ return MHD_upload_action_from_response (
+ request,
+ MHD_response_from_callback (MHD_HTTP_STATUS_OK,
+ MHD_SIZE_UNKNOWN,
+ &dyn_cc,
+ flag,
+ &dyn_cc_free));
+}
+
+
+/**
+ * A client has requested the given url using the given method
+ * (#MHD_HTTP_METHOD_GET, #MHD_HTTP_METHOD_PUT,
+ * #MHD_HTTP_METHOD_DELETE, #MHD_HTTP_METHOD_POST, etc).
+ * If @a upload_size is not zero and response action is provided by this
+ * callback, then upload will be discarded and the stream (the connection for
+ * HTTP/1.1) will be closed after sending the response.
+ *
+ * @param cls argument given together with the function
+ * pointer when the handler was registered with MHD
+ * @param request the request object
+ * @param path the requested uri (without arguments after "?")
+ * @param method the HTTP method used (#MHD_HTTP_METHOD_GET,
+ * #MHD_HTTP_METHOD_PUT, etc.)
+ * @param upload_size the size of the message upload content payload,
+ * #MHD_SIZE_UNKNOWN for chunked uploads (if the
+ * final chunk has not been processed yet)
+ * @return action how to proceed, NULL
+ * if the request must be aborted due to a serious
+ * error while handling the request (implies closure
+ * of underling data stream, for HTTP/1.1 it means
+ * socket closure).
+ */
+static const struct MHD_Action *
+server_req_cb (void *cls,
+ struct MHD_Request *MHD_RESTRICT request,
+ const struct MHD_String *MHD_RESTRICT path,
+ enum MHD_HTTP_Method method,
+ uint_fast64_t upload_size)
+{
+ (void) path;
+ (void) method;
+ (void) upload_size;
+ return MHD_action_process_upload_full (request,
+ upload_size,
+ &upload_cb,
+ NULL);
+}
+
+
+/**
+ * Helper function to deal with partial writes.
+ * Fails hard (calls exit() on failures)!
+ *
+ * @param fd where to write to
+ * @param buf what to write
+ * @param buf_size number of bytes in @a buf
+ */
+static void
+write_all (int fd,
+ const void *buf,
+ size_t buf_size)
+{
+ const char *cbuf = buf;
+ size_t off;
+
+ off = 0;
+ while (off < buf_size)
+ {
+ ssize_t ret;
+
+ ret = write (fd,
+ &cbuf[off],
+ buf_size - off);
+ if (ret <= 0)
+ {
+ fprintf (stderr,
+ "Writing %u bytes to %d failed: %s\n",
+ (unsigned int) (buf_size - off),
+ fd,
+ strerror (errno));
+ exit (1);
+ }
+ off += ret;
+ }
+}
+
+
+static int
+run_test (unsigned int url_len,
+ unsigned int query_len,
+ unsigned int header_len,
+ unsigned int cookie_len,
+ unsigned int body_len)
+{
+ char filler[BUFFER_SIZE + 1];
+ int s;
+
+ out_of_memory = false;
+ memset (filler,
+ 'a',
+ BUFFER_SIZE);
+ filler[BUFFER_SIZE] = '\0'; /* just to be conservative */
+ s = socket (AF_INET,
+ SOCK_STREAM,
+ 0);
+ if (-1 == s)
+ {
+ fprintf (stderr,
+ "socket() failed: %s\n",
+ strerror (errno));
+ return -1;
+ }
+
+ {
+ struct sockaddr_in sa = {
+ .sin_family = AF_INET,
+ .sin_port = htons (port),
+ };
+ inet_pton (AF_INET,
+ "127.0.0.1",
+ &sa.sin_addr);
+ if (0 != connect (s,
+ (struct sockaddr *) &sa,
+ sizeof (sa)))
+ {
+ fprintf (stderr,
+ "bind() failed: %s\n",
+ strerror (errno));
+ close (s);
+ return -1;
+ }
+ }
+
+ {
+ char upload[BUFFER_SIZE * 2];
+ int iret;
+
+ iret = snprintf (upload,
+ sizeof (upload),
+ "PUT /%.*s?q=%.*s HTTP/1.0\r\n"
+ "Content-Length: %u\r\n"
+ "Key: %.*s\r\n"
+ "Cookie: a=%.*s\r\n\r\n"
+ "%.*s",
+ (int) url_len,
+ filler,
+ (int) query_len,
+ filler,
+ body_len,
+ (int) header_len,
+ filler,
+ (int) cookie_len,
+ filler,
+ (int) body_len,
+ filler);
+ if ( (-1 == iret) ||
+ (iret > sizeof (upload)) )
+ {
+ fprintf (stderr,
+ "failed to build request buffer: %d\n",
+ iret);
+ close (s);
+ return -1;
+ }
+ write_all (s,
+ upload,
+ strlen (upload));
+ }
+ /* read and discard response */
+ {
+ bool got_data = false;
+ bool nice = false;
+ char dummy[16 * 1024];
+ int flags = 0;
+
+ while (1)
+ {
+ ssize_t res;
+
+ res = recv (s,
+ &dummy,
+ sizeof (dummy),
+ flags);
+ flags = MSG_DONTWAIT;
+ if (res > 0)
+ {
+ got_data = true;
+ dummy[res] = '\0';
+ /* FIXME: allow other "too large" responses to also count as
+ 'nice' here */
+ if (NULL !=
+ strstr (dummy,
+ "431 Request Header Fields Too Large"))
+ nice = true;
+ }
+ if (res <= 0)
+ break;
+ }
+ if (nice)
+ out_of_memory = true;
+ if (! got_data)
+ {
+ out_of_memory = true;
+ fprintf (stderr,
+ "Response was not nice (%u/%u/%u/%u/%u)\n",
+ url_len,
+ query_len,
+ header_len,
+ cookie_len,
+ body_len);
+ }
+ }
+ close (s);
+ return out_of_memory ? 1 : 0;
+}
+
+
+static int
+test_url (void)
+{
+ bool oom_hit;
+
+ oom_hit = false;
+ for (unsigned int i = 0;
+ i < BUFFER_SIZE;
+ i += STEP)
+ {
+ int ret;
+
+ ret = run_test (i, 0, 0, 0, 0);
+ if (-1 == ret)
+ {
+ return 1;
+ }
+ if (1 == ret)
+ {
+ oom_hit = true;
+ }
+ if ( (oom_hit) && (1 != ret) )
+ {
+ fprintf (stderr,
+ "Strange: OOM stopped at %u after being hit earlier (url)?\n",
+ i);
+ }
+ }
+ if (! oom_hit)
+ {
+ fprintf (stderr,
+ "Failed to trigger OOM condition via URL\n");
+ return 1;
+ }
+ return 0;
+}
+
+
+static int
+test_query (void)
+{
+ bool oom_hit;
+
+ oom_hit = false;
+ for (unsigned int i = 0;
+ i < BUFFER_SIZE;
+ i += STEP)
+ {
+ int ret;
+
+ ret = run_test (0, i, 0, 0, 0);
+ if (-1 == ret)
+ {
+ return 1;
+ }
+ if (1 == ret)
+ {
+ oom_hit = true;
+ }
+ if ( (oom_hit) && (1 != ret) )
+ {
+ fprintf (stderr,
+ "Strange: OOM stopped at %u after being hit earlier (query)?\n",
+ i);
+ }
+ }
+ if (! oom_hit)
+ {
+ fprintf (stderr,
+ "Failed to trigger OOM condition via query\n");
+ return 1;
+ }
+ return 0;
+}
+
+
+static int
+test_header (void)
+{
+ bool oom_hit;
+
+ oom_hit = false;
+ for (unsigned int i = 0;
+ i < BUFFER_SIZE;
+ i += STEP)
+ {
+ int ret;
+
+ ret = run_test (0, 0, i, 0, 0);
+ if (-1 == ret)
+ {
+ return 1;
+ }
+ if (1 == ret)
+ {
+ oom_hit = true;
+ }
+ if ( (oom_hit) && (1 != ret) )
+ {
+ fprintf (stderr,
+ "Strange: OOM stopped at %u after being hit earlier (header)?\n",
+ i);
+ }
+ }
+ if (! oom_hit)
+ {
+ fprintf (stderr,
+ "Failed to trigger OOM condition via header\n");
+ return 1;
+ }
+ return 0;
+}
+
+
+static int
+test_cookie (void)
+{
+ bool oom_hit;
+
+ oom_hit = false;
+ for (unsigned int i = 0;
+ i < BUFFER_SIZE;
+ i += STEP)
+ {
+ int ret;
+
+ ret = run_test (0, 0, 0, i, 0);
+ if (-1 == ret)
+ {
+ return 1;
+ }
+ if (1 == ret)
+ {
+ oom_hit = true;
+ }
+ if ( (oom_hit) && (1 != ret) )
+ {
+ fprintf (stderr,
+ "Strange: OOM stopped at %u after being hit earlier (cookie)?\n",
+ i);
+ }
+ }
+ if (! oom_hit)
+ {
+ fprintf (stderr,
+ "Failed to trigger OOM condition via cookie\n");
+ return 1;
+ }
+ return 0;
+}
+
+
+static int
+test_body (void)
+{
+ bool oom_hit;
+
+ oom_hit = false;
+ for (unsigned int i = 0;
+ i < BUFFER_SIZE;
+ i += STEP)
+ {
+ int ret;
+
+ ret = run_test (0, 0, 0, 0, i);
+ if (-1 == ret)
+ {
+ return 1;
+ }
+ if (1 == ret)
+ {
+ oom_hit = true;
+ }
+ if ( (oom_hit) && (1 != ret) )
+ {
+ fprintf (stderr,
+ "Strange: OOM stopped at %u after being hit earlier (body)?\n",
+ i);
+ }
+ }
+ if (! oom_hit)
+ {
+ fprintf (stderr,
+ "Failed to trigger OOM condition via body\n");
+ return 1;
+ }
+ return 0;
+}
+
+
+static int
+test_mix (void)
+{
+ bool oom_hit;
+
+ /* mix and match path */
+ for (unsigned int i = 0;
+ i < BUFFER_SIZE;
+ i += STEP)
+ {
+ int ret;
+
+ ret = run_test (i / 5 + 1, i / 5 + 1, i / 5 + 1, i / 5 + 1, i / 5 + 1);
+ if (-1 == ret)
+ {
+ return 1;
+ }
+ if (1 == ret)
+ {
+ oom_hit = true;
+ }
+ if ( (oom_hit) && (1 != ret) )
+ {
+ fprintf (stderr,
+ "Strange: OOM stopped at %u after being hit earlier (mix)?\n",
+ i);
+ }
+ }
+ if (! oom_hit)
+ {
+ fprintf (stderr,
+ "Failed to trigger OOM condition in mix-and-match\n");
+ return 1;
+ }
+
+
+ return 0;
+}
+
+
+static int
+run_tests (void)
+{
+ int ret = 0;
+
+#if 1
+ ret |= test_url ();
+ ret |= test_query ();
+ ret |= test_header ();
+ ret |= test_cookie ();
+ ret |= test_body ();
+ ret |= test_mix ();
+#endif
+ return ret;
+}
+
+
+static void
+no_log (void *cls,
+ enum MHD_StatusCode sc,
+ const char *fm,
+ va_list ap)
+{
+ /* intentionally empty */
+}
+
+
+int
+main ()
+{
+ struct MHD_Daemon *d;
+
+ d = MHD_daemon_create (&server_req_cb,
+ NULL);
+ if (MHD_SC_OK !=
+ MHD_DAEMON_SET_OPTIONS (
+ d,
+ MHD_D_OPTION_WM_WORKER_THREADS (2),
+ MHD_D_OPTION_LOG_CALLBACK (&no_log, NULL),
+ MHD_D_OPTION_CONN_MEMORY_LIMIT (BUFFER_SIZE),
+ MHD_D_OPTION_DEFAULT_TIMEOUT (1),
+ MHD_D_OPTION_BIND_PORT (MHD_AF_AUTO,
+ 0)))
+ {
+ fprintf (stderr,
+ "Failed to configure daemon!");
+ return 1;
+ }
+
+ {
+ enum MHD_StatusCode sc;
+
+ sc = MHD_daemon_start (d);
+ if (MHD_SC_OK != sc)
+ {
+#ifdef FIXME_STATUS_CODE_TO_STRING_NOT_IMPLEMENTED
+ fprintf (stderr,
+ "Failed to start server: %s\n",
+ MHD_status_code_to_string_lazy (sc));
+#else
+ fprintf (stderr,
+ "Failed to start server: %u\n",
+ (unsigned int) sc);
+#endif
+ MHD_daemon_destroy (d);
+ return 1;
+ }
+ }
+
+ {
+ union MHD_DaemonInfoFixedData info;
+ enum MHD_StatusCode sc;
+
+ sc = MHD_daemon_get_info_fixed (
+ d,
+ MHD_DAEMON_INFO_FIXED_BIND_PORT,
+ &info);
+ if (MHD_SC_OK != sc)
+ {
+ fprintf (stderr,
+ "Failed to determine our port: %u\n",
+ (unsigned int) sc);
+ MHD_daemon_destroy (d);
+ return 1;
+ }
+ port = info.v_bind_port_uint16;
+ }
+
+ {
+ int result;
+
+ result = run_tests ();
+ MHD_daemon_destroy (d);
+ return result;
+ }
+}